Microphone capture for in-browser Python (Pyodide / JupyterLite) via the Web Audio API.
Project description
browseraudio
Microphone capture for in-browser Python — record from the mic in Pyodide, JupyterLite, and thebe, straight into a NumPy array.
In the browser, Python usually runs in a Web Worker with no access to
getUserMedia or the Web Audio API — so the usual sounddevice → PortAudio
stack can't run. browseraudio captures audio on the page's main thread (via a
tiny anywidget frontend) and hands the float32 samples
back to the kernel, so recording works even from a worker.
Status: recording only — the foundation. Playback and a drop-in
sounddevicereplacement are on the roadmap.
Contents
- Requirements
- Install
- Quickstart
- API reference
- How it works
- Troubleshooting
- Development
- Roadmap
- License
Requirements
| Python | 3.9+ |
| Runtime | A browser Python kernel that supports Jupyter widgets — JupyterLite, thebe-lite, classic Jupyter, or marimo. (Also works on a native kernel, but native code should just use sounddevice.) |
| Browser | Any current Chromium, Firefox, or Safari (needs the Web Audio API + getUserMedia). |
| Context | A secure context — https:// or http://localhost. Browsers block microphone access on plain http://. |
| Permission | The user must grant the microphone permission prompt, triggered by clicking the Record button. |
Runtime dependencies: anywidget and numpy.
pyquist is optional (only for
Recorder.to_pyquist()).
Install
pip install browseraudio
In a browser kernel (JupyterLite / thebe), install at runtime with micropip:
import micropip
await micropip.install("browseraudio")
Quickstart
Recording finishes after you click, so display the recorder in one cell and read the result in the next.
from browseraudio import record
rec = record(3.0) # shows "● Record 3s" — click it, allow the mic, speak
An inline player appears so you can hear the take. Then, in a new cell:
rec.samples # float32 ndarray, shape (n_frames, 1)
rec.sample_rate # e.g. 48000
rec.to_pyquist() # a pyquist.Audio, if pyquist is installed
Why two cells? A single-cell
await record()can't work in Jupyter/thebe: the kernel doesn't process the widget's reply while that same cell is still running, so the recording would never arrive.
API reference
record(duration=3.0)
Create a Recorder, display it, and return it. Convenience wrapper
for the common case. Click Record, then read the result (see
Recorder) in a later cell.
| Parameter | Type | Default | Description |
|---|---|---|---|
duration |
float |
3.0 |
Length to record, in seconds. |
Returns a Recorder.
Recorder
An anywidget that records from the microphone. Display
it (or use record()), click Record, then read its
attributes in a separate cell once capture finishes.
from browseraudio import Recorder
rec = Recorder(duration=5.0)
rec # display it; click Record
Constructor
| Parameter | Type | Default | Description |
|---|---|---|---|
duration |
float |
3.0 |
Length to record, in seconds. |
Attributes
| Attribute | Type | Description |
|---|---|---|
samples |
numpy.ndarray | None |
The latest take as float32, shape (n_frames, 1); None before anything is recorded. |
sample_rate |
int |
The browser AudioContext rate (e.g. 48000); 0 before recording. |
duration |
float |
The requested recording length, in seconds. |
error |
str | None |
A message if the last attempt failed (permission denied, no input, …), else None. |
Methods
| Method | Returns | Description |
|---|---|---|
to_pyquist() |
pyquist.Audio |
The take as a pyquist.Audio. Raises RuntimeError if nothing has been recorded, and ImportError if pyquist isn't installed. |
How it works
A browser tab has two Python-relevant execution contexts, and browseraudio uses both:
- The page (main thread) has the Web Audio API and
getUserMedia, but not your Python kernel. An anywidget frontend lives here and does the actual capture, then encodes the float32 samples. - The worker runs your Python kernel (this is how Pyodide/JupyterLite keep
the page responsive), but it can't reach those audio APIs. It receives the
encoded samples and decodes them into a NumPy array as
rec.samples.
The two contexts talk over the standard Jupyter widget comm channel — the
same mechanism any ipywidgets widget uses — so browseraudio works wherever
widgets do: JupyterLite, thebe-lite, classic Jupyter, and marimo.
Under the hood the frontend uses a ScriptProcessorNode to accumulate audio for
duration seconds, base64-encodes the float32 buffer, and sends it over the
comm; the Python side decodes it with numpy.frombuffer. (Both are deliberately
simple — see the roadmap for the planned AudioWorklet upgrade.)
Troubleshooting
| Symptom | Cause / fix |
|---|---|
| No Record button appears | The frontend needs a Jupyter-widget-capable runtime. In a bare Pyodide page without the widget manager, widgets don't render — use JupyterLite, thebe-lite, or Jupyter. |
| Permission denied / no prompt | The mic needs a secure context (https:// or localhost) and a user gesture. Click the button; if you previously blocked the mic, re-allow it in the browser's site settings. |
rec.samples is None |
You haven't recorded yet, or you read it in the same cell that created the recorder. Click Record, then read it in a new cell. |
| Recorded, but silent | Check rec.error, and that the right input device is selected and unmuted at the OS/browser level. |
await record() hangs |
Not supported — the kernel can't process the widget reply mid-cell. Use the two-cell flow. |
to_pyquist() raises ImportError |
Install pyquist (pip install pyquist), or use rec.samples / rec.sample_rate directly. |
Development
git clone https://github.com/jiaweil6/browseraudio
cd browseraudio
pip install -e ".[pyquist]" # editable install with the optional extra
python -m build # build the wheel + sdist into dist/
python -m twine check dist/* # validate package metadata
Project layout:
| Path | Purpose |
|---|---|
browseraudio/__init__.py |
Public API (Recorder, record) and version. |
browseraudio/_recorder.py |
The Recorder widget and record() (Python side). |
browseraudio/static/recorder.js |
The anywidget frontend (Web Audio capture). |
pyproject.toml |
Packaging metadata; static/*.js is shipped as package data. |
There is no automated test suite yet — the recording round-trip is exercised end-to-end (headless Chrome with a fake media stream) in the consuming project. Contributions, issues, and a proper test harness are welcome.
Roadmap
- Playback — push a buffer to a main-thread
AudioContext. - AudioWorklet backend — replace the deprecated
ScriptProcessorNode. sounddevice-compatible facade —play/rec/waitso libraries like pyquist run in the browser unchanged (a real replacement, not a stub).- Binary comm transport — send raw buffers instead of base64.
- Streaming (stretch) — generator → ring buffer → AudioWorklet. Bounded by
the browser: Python can't run in the audio thread, and
SharedArrayBufferneeds cross-origin-isolation (COOP/COEP) headers.
License
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file browseraudio-0.1.2.tar.gz.
File metadata
- Download URL: browseraudio-0.1.2.tar.gz
- Upload date:
- Size: 9.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c900c24a0ba1ae096863a0af27f24383933451be25f4bf6d3650db22f88d50b8
|
|
| MD5 |
fee3443e4cb75124aa1e188e794f65ca
|
|
| BLAKE2b-256 |
77653c900c42fe0d61f039a5598b91545046ee8d4e216ad875ade024329bcba7
|
File details
Details for the file browseraudio-0.1.2-py3-none-any.whl.
File metadata
- Download URL: browseraudio-0.1.2-py3-none-any.whl
- Upload date:
- Size: 10.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
314d64d46c7533492ebf74bd0c685062cf36f50823ec45c9a3f119203374d111
|
|
| MD5 |
f0978e03ed83f564b859c797bb0a94c0
|
|
| BLAKE2b-256 |
6403b830b56ae27a88e33396ccb85799f8eec970142331dc5df9a8c94ec3f5a1
|