Skip to content

audio_cues

audio_cues

Audible feedback for dictation — a blip down, a blip up.

Push-to-talk is a blind contract: you hold a key and hope a microphone opened. Everything that can go wrong (permission missing, tap starved, key not registered) fails the same silent way, so the single most useful signal is a short sound the instant the key is seen. It confirms the tap fired, before any device work has happened.

Two design choices worth stating:

  • The tones are synthesised, not shipped. jarvis-main plays a WAV through sounddevice, but its only asset is a 14.5-second spoken welcome phrase — a start cue must be under a tenth of a second. Rather than commit a binary asset that has to be found, licensed and reviewed, the cues are ~40 lines of arithmetic, so they are diffable, tunable, and testable without a sound card.
  • Playback goes through NSSound, not sounddevice. The capture stream is a PortAudio input stream opening at the same moment; driving output through the same library invites device contention on the one code path that must never stall. NSSound loads the clip into memory once at startup and play() returns immediately, so the cue costs nothing at keypress.

Nothing here is allowed to break dictation: every failure degrades to silence. A missing sound is a cosmetic loss; a raised exception on the tap callback thread is a dead hotkey.

Classes

CuePlayer

CuePlayer(
    *,
    enabled: bool = True,
    directory: Optional[Path] = None,
)

Plays the cues, or silently does nothing. Never raises.

prime() does all the expensive work — synthesis, disk write, NSSound load — so that play() is a single message send on the hot path.

Source code in src/diapason/desktop/audio_cues.py
def __init__(
    self,
    *,
    enabled: bool = True,
    directory: Optional[Path] = None,
) -> None:
    self.enabled = enabled
    self._dir = directory
    self._paths: Dict[str, Path] = {}
    self._sounds: Dict[str, object] = {}
    self._primed = False
Methods:
prime
prime() -> bool

Synthesise, cache and preload. True when at least one cue is ready.

Source code in src/diapason/desktop/audio_cues.py
def prime(self) -> bool:
    """Synthesise, cache and preload. True when at least one cue is ready."""
    if not self.enabled or self._primed:
        return bool(self._paths)
    self._primed = True
    try:
        self._write_files()
    except Exception:  # noqa: BLE001 - a cue is never worth failing over
        logger.debug("could not write cue files", exc_info=True)
        return False
    try:
        from AppKit import NSSound  # type: ignore

        for name, path in self._paths.items():
            sound = NSSound.alloc().initWithContentsOfFile_byReference_(
                str(path), True
            )
            if sound is not None:
                self._sounds[name] = sound
    except Exception:  # noqa: BLE001 - afplay fallback below still works
        logger.debug("NSSound unavailable; falling back to afplay", exc_info=True)
    return bool(self._paths)
play
play(name: str) -> bool

Fire a cue. Returns True if something was actually played.

Called from the key-tap callback thread, so it must not block: NSSound plays asynchronously, and the afplay fallback is spawned, never waited on.

Source code in src/diapason/desktop/audio_cues.py
def play(self, name: str) -> bool:
    """Fire a cue. Returns True if something was actually played.

    Called from the key-tap callback thread, so it must not block: NSSound
    plays asynchronously, and the afplay fallback is spawned, never waited
    on.
    """
    if not self.enabled:
        return False
    if not self._primed:
        self.prime()
    sound = self._sounds.get(name)
    if sound is not None:
        try:
            # Restart from the top if the previous cue is still ringing —
            # rapid push-to-talk taps must not swallow their own feedback.
            if sound.isPlaying():
                sound.stop()
            return bool(sound.play())
        except Exception:  # noqa: BLE001
            logger.debug("NSSound play failed", exc_info=True)
    path = self._paths.get(name)
    if path is None:
        return False
    try:
        subprocess.Popen(  # noqa: S603 - fixed binary, generated path
            ["/usr/bin/afplay", str(path)],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
        return True
    except Exception:  # noqa: BLE001
        logger.debug("afplay fallback failed", exc_info=True)
        return False

Functions:

glide

glide(
    f0: float,
    f1: float,
    duration_s: float,
    *,
    sample_rate: int = SAMPLE_RATE,
    amplitude: float = AMPLITUDE,
) -> List[float]

A sine sweeping f0 → f1, enveloped so it neither clicks nor thumps.

The phase is integrated rather than computed per-sample from the instantaneous frequency (sin(2π f(t) t)), which would fold the frequency change into the argument twice and bend the pitch the wrong way. Accumulating 2π f(t)/rate keeps the waveform continuous.

Source code in src/diapason/desktop/audio_cues.py
def glide(
    f0: float,
    f1: float,
    duration_s: float,
    *,
    sample_rate: int = SAMPLE_RATE,
    amplitude: float = AMPLITUDE,
) -> List[float]:
    """A sine sweeping f0 → f1, enveloped so it neither clicks nor thumps.

    The phase is *integrated* rather than computed per-sample from the
    instantaneous frequency (``sin(2π f(t) t)``), which would fold the
    frequency change into the argument twice and bend the pitch the wrong
    way. Accumulating ``2π f(t)/rate`` keeps the waveform continuous.
    """
    n = max(1, int(round(duration_s * sample_rate)))
    edge = min(int(EDGE_S * sample_rate), n // 2)
    out: List[float] = []
    phase = 0.0
    for i in range(n):
        t = i / (n - 1) if n > 1 else 0.0
        freq = f0 + (f1 - f0) * t
        phase += 2.0 * math.pi * freq / sample_rate
        gain = 1.0
        if edge:
            if i < edge:
                gain = 0.5 * (1.0 - math.cos(math.pi * i / edge))
            elif i >= n - edge:
                gain = 0.5 * (1.0 - math.cos(math.pi * (n - 1 - i) / edge))
        out.append(amplitude * gain * math.sin(phase))
    return out

wav_bytes

wav_bytes(
    samples: Sequence[float],
    *,
    sample_rate: int = SAMPLE_RATE,
) -> bytes

16-bit mono PCM WAV, stdlib only.

Source code in src/diapason/desktop/audio_cues.py
def wav_bytes(samples: Sequence[float], *, sample_rate: int = SAMPLE_RATE) -> bytes:
    """16-bit mono PCM WAV, stdlib only."""
    frames = b"".join(
        struct.pack("<h", int(max(-1.0, min(1.0, s)) * 32767.0)) for s in samples
    )
    import io

    buf = io.BytesIO()
    with wave.open(buf, "wb") as wav:
        wav.setnchannels(1)
        wav.setsampwidth(2)
        wav.setframerate(sample_rate)
        wav.writeframes(frames)
    return buf.getvalue()

cue_wav

cue_wav(name: str) -> bytes

WAV bytes for a named cue. Raises KeyError on an unknown name.

Source code in src/diapason/desktop/audio_cues.py
def cue_wav(name: str) -> bytes:
    """WAV bytes for a named cue. Raises KeyError on an unknown name."""
    return wav_bytes(CUES[name]())