Skip to content

mic_capture

mic_capture

Microphone capture for push-to-talk, outside any WebView.

Diapason's dictation captured audio with getUserMedia inside the WKWebView, so closing the window silenced the mic — the global hotkey would fire and record nothing. Capturing here, in the Python process via sounddevice, has no such coupling: the hotkey works with every window closed, which is the whole point of a push-to-talk key.

The DSP helpers (RMS level, linear resample, mono downmix) are separated from the live sounddevice stream so they can be unit-tested without a device.

Classes

MicCapture

MicCapture(
    *,
    target_rate: int = TARGET_SAMPLE_RATE,
    level_cb=None,
    bands_cb=None,
)

Accumulate 16 kHz mono float audio while active; report a live level.

start() opens a sounddevice input stream on a background thread; stop() returns the captured mono buffer at 16 kHz. level is the most recent RMS, for a waveform/meter. The buffering itself is plain and testable via _ingest; only start/stop touch hardware.

Source code in src/diapason/desktop/mic_capture.py
def __init__(
    self,
    *,
    target_rate: int = TARGET_SAMPLE_RATE,
    level_cb=None,
    bands_cb=None,
):
    self._target_rate = target_rate
    self._level_cb = level_cb
    # Spectral shape, for a display that behaves like an equaliser. A
    # single RMS number can only move everything together; bands say where
    # in the spectrum the energy actually is. Optional: nothing downstream
    # is required to want them.
    self._bands_cb = bands_cb
    self._smoother = None
    self._chunks: List["Any"] = []  # noqa: F821
    self._lock = threading.Lock()
    self._stream = None
    self._src_rate = target_rate
    self.level = 0.0
Methods:
buffer
buffer() -> 'Any'

Concatenate everything captured so far as one 16 kHz mono array.

Source code in src/diapason/desktop/mic_capture.py
def buffer(self) -> "Any":  # noqa: F821
    """Concatenate everything captured so far as one 16 kHz mono array."""
    import numpy as np

    with self._lock:
        if not self._chunks:
            return np.zeros(0, dtype="float32")
        return np.concatenate(self._chunks).astype("float32")

Functions:

rms_level

rms_level(block: 'Any') -> float

Root-mean-square of a float block, as 0.0–100.0 for a UI meter.

Source code in src/diapason/desktop/mic_capture.py
def rms_level(block: "Any") -> float:  # noqa: F821 - numpy at runtime
    """Root-mean-square of a float block, as 0.0–100.0 for a UI meter."""
    import numpy as np

    arr = np.asarray(block, dtype="float32").reshape(-1)
    if arr.size == 0:
        return 0.0
    return float(np.sqrt(np.mean(arr * arr)) * 100.0)

to_mono

to_mono(block: 'Any') -> 'Any'

Average channels down to mono, leaving a 1-D array.

Source code in src/diapason/desktop/mic_capture.py
def to_mono(block: "Any") -> "Any":  # noqa: F821
    """Average channels down to mono, leaving a 1-D array."""
    import numpy as np

    arr = np.asarray(block, dtype="float32")
    if arr.ndim == 2 and arr.shape[1] > 1:
        return arr.mean(axis=1)
    return arr.reshape(-1)

resample_linear

resample_linear(
    block: "Any", src_rate: int, dst_rate: int
) -> "Any"

Linear resample a mono block. Good enough for speech; no SciPy needed.

The same linear interpolation Diapason's native recorder used to reach 16 kHz — cheap, dependency-free, and inaudible on voice.

Source code in src/diapason/desktop/mic_capture.py
def resample_linear(block: "Any", src_rate: int, dst_rate: int) -> "Any":  # noqa: F821
    """Linear resample a mono block. Good enough for speech; no SciPy needed.

    The same linear interpolation Diapason's native recorder used to reach
    16 kHz — cheap, dependency-free, and inaudible on voice.
    """
    import numpy as np

    arr = np.asarray(block, dtype="float32").reshape(-1)
    if src_rate == dst_rate or arr.size == 0:
        return arr
    n_out = int(round(arr.size * dst_rate / src_rate))
    if n_out <= 0:
        return np.zeros(0, dtype="float32")
    src_idx = np.linspace(0.0, arr.size - 1, num=n_out, dtype="float32")
    left = np.floor(src_idx).astype("int64")
    right = np.minimum(left + 1, arr.size - 1)
    frac = src_idx - left
    return (arr[left] * (1.0 - frac) + arr[right] * frac).astype("float32")