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
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")
|