Skip to main content

Microphone capture for in-browser Python (Pyodide / JupyterLite) via the Web Audio API.

Project description

browseraudio

PyPI Python License: MIT

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 sounddevice replacement are on the roadmap.

Contents

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 contexthttps:// 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:

  1. 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.
  2. 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 facadeplay / rec / wait so 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 SharedArrayBuffer needs cross-origin-isolation (COOP/COEP) headers.

License

MIT

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

browseraudio-0.1.1.tar.gz (9.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

browseraudio-0.1.1-py3-none-any.whl (10.1 kB view details)

Uploaded Python 3

File details

Details for the file browseraudio-0.1.1.tar.gz.

File metadata

  • Download URL: browseraudio-0.1.1.tar.gz
  • Upload date:
  • Size: 9.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for browseraudio-0.1.1.tar.gz
Algorithm Hash digest
SHA256 d5c8f377a02b44faaec7a895869cbeeae38089a63c6a81c58b56a848ff10484e
MD5 e758a16a483c050e7d1c62007de00678
BLAKE2b-256 500ef11468deb11319a601db14f502099dade573ea2ac877c41a918e721026fc

See more details on using hashes here.

File details

Details for the file browseraudio-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: browseraudio-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 10.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for browseraudio-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 fa77d9a0154fa202d99e6c3f06d4c233e29468dd73b7929fac614fac1d200309
MD5 e8f32f8e18e750cd1ded8ecc677c9473
BLAKE2b-256 dea28b903e7d92ccfc662ddf3fe302e3200abcc2b8b7ab4dd9e6016f14aba20f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page