Merge listen-talkover: non-blocking ready tone (talk over the dial)
This commit is contained in:
commit
08f40ae2dc
@ -238,6 +238,27 @@ async def _terminate_quietly(proc) -> None:
|
||||
await proc.wait()
|
||||
|
||||
|
||||
def _fire_ready_tone(ready_tone: Path | None) -> "asyncio.Task | None":
|
||||
"""Play the "go" cue WITHOUT blocking capture, so the person can talk over it.
|
||||
|
||||
The old behaviour awaited the tone before the read loop began, adding ~1s
|
||||
of dead time. Firing it as a background task lets capture start immediately;
|
||||
the tone bleeds into the recorded head but VAD skips it (pure tone !=
|
||||
speech) and the min_speech_ms guard keeps it from ending the recording.
|
||||
Returns the task (or None) so the caller can cancel it on cleanup.
|
||||
"""
|
||||
if ready_tone is None:
|
||||
return None
|
||||
|
||||
async def _play() -> None:
|
||||
try:
|
||||
await play_audio(ready_tone, expected_seconds=0.3)
|
||||
except Exception:
|
||||
pass # the cue is optional — never fail a recording over it
|
||||
|
||||
return asyncio.create_task(_play())
|
||||
|
||||
|
||||
async def record_audio_until_silence(
|
||||
out_path: Path,
|
||||
silence_threshold_ms: int = 1500,
|
||||
@ -291,16 +312,13 @@ async def record_audio_until_silence(
|
||||
stderr=asyncio.subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
# Cue the person only AFTER the mic is live (see warmup rationale above), so
|
||||
# the first word isn't clipped by pw-record's stream startup. The beep is
|
||||
# captured as leading audio but VAD skips it (pure tone != speech) and the
|
||||
# min_speech_ms guard below keeps it from ending the recording.
|
||||
# Warm up (mic live before the cue so the first word isn't clipped), then
|
||||
# fire the "go" tone WITHOUT awaiting it and drop straight into the VAD
|
||||
# loop — so the person can talk right over the tone instead of waiting ~1s
|
||||
# for it to finish.
|
||||
if ready_tone is not None:
|
||||
await asyncio.sleep(warmup_ms / 1000)
|
||||
try:
|
||||
await play_audio(ready_tone, expected_seconds=0.3)
|
||||
except Exception:
|
||||
pass # tone failure is non-fatal — keep recording
|
||||
tone_task = _fire_ready_tone(ready_tone)
|
||||
|
||||
pcm_buf = bytearray()
|
||||
leftover = b""
|
||||
@ -343,6 +361,8 @@ async def record_audio_until_silence(
|
||||
# The inner read returned no data — break outer loop
|
||||
break
|
||||
finally:
|
||||
if tone_task is not None and not tone_task.done():
|
||||
tone_task.cancel() # don't let a short recording outlive by the tone
|
||||
await _terminate_quietly(proc)
|
||||
|
||||
if not pcm_buf:
|
||||
@ -415,15 +435,12 @@ async def record_audio(
|
||||
stdout=asyncio.subprocess.DEVNULL,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
# Cue the person only AFTER the mic is live so the first word isn't clipped
|
||||
# by pw-record's stream startup (the beep bleeds harmlessly into the head of
|
||||
# the WAV; Parakeet ignores it). The duration timeout starts after the cue.
|
||||
# Warm up (mic live before the cue), then fire the "go" tone WITHOUT
|
||||
# awaiting it so recording is underway immediately and the person can talk
|
||||
# over it. The beep bleeds harmlessly into the head of the WAV.
|
||||
if ready_tone is not None:
|
||||
await asyncio.sleep(warmup_ms / 1000)
|
||||
try:
|
||||
await play_audio(ready_tone, expected_seconds=0.3)
|
||||
except Exception:
|
||||
pass # tone failure is non-fatal — keep recording
|
||||
tone_task = _fire_ready_tone(ready_tone)
|
||||
try:
|
||||
# pw-record never exits on its own — wait the desired duration then term.
|
||||
await asyncio.wait_for(proc.wait(), timeout=duration_seconds)
|
||||
@ -438,6 +455,9 @@ async def record_audio(
|
||||
pass
|
||||
await proc.wait()
|
||||
raise
|
||||
finally:
|
||||
if tone_task is not None and not tone_task.done():
|
||||
tone_task.cancel()
|
||||
|
||||
# pw-record's exit code on SIGTERM is non-deterministic: some versions
|
||||
# return 0, some 1, some -15. The reliable signal is whether the WAV
|
||||
@ -477,12 +497,11 @@ async def _record_fixed_stream(
|
||||
stderr=asyncio.subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
# Warm up, then fire the "go" tone WITHOUT awaiting so capture is underway
|
||||
# immediately and the person can talk over it.
|
||||
if ready_tone is not None:
|
||||
await asyncio.sleep(warmup_ms / 1000)
|
||||
try:
|
||||
await play_audio(ready_tone, expected_seconds=0.3)
|
||||
except Exception:
|
||||
pass # tone failure is non-fatal — keep recording
|
||||
tone_task = _fire_ready_tone(ready_tone)
|
||||
|
||||
pcm = bytearray()
|
||||
|
||||
@ -504,6 +523,9 @@ async def _record_fixed_stream(
|
||||
pass
|
||||
await proc.wait()
|
||||
raise
|
||||
finally:
|
||||
if tone_task is not None and not tone_task.done():
|
||||
tone_task.cancel()
|
||||
|
||||
if len(pcm) < 100:
|
||||
raise PlaybackError(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user