Skip to content

Commit 5528258

Browse files
committed
playground: add webaudio_minimal_beep
1 parent 1bc46ac commit 5528258

2 files changed

Lines changed: 84 additions & 0 deletions

File tree

bindings/imgui_bundle/demos_python/playground/examples/examples.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@
8585
{
8686
"label": "WebGL: 3D cube rendered to a texture",
8787
"filename": "webgl_texture_in_image.py"
88+
},
89+
{
90+
"label": "WebAudio: Minimal Beep",
91+
"filename": "webaudio_minimal_beep.py"
8892
}
8993
]
9094
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""
2+
WebAudio: minimal beep — Pyodide sanity check.
3+
4+
**Pyodide only.** Calls the browser's WebAudio API via Python's `js` interop.
5+
Click **Beep** to hear a short sine tone whose frequency and duration you
6+
can slide.
7+
8+
Why this exists: confirms that audio works in the Pyodide playground
9+
without needing an SDL2-audio-enabled build or a Python audio binding.
10+
Same pattern as the `webgl_minimal_mandelbrot.py` demo, just talking to
11+
`AudioContext` instead of `WebGL2RenderingContext`.
12+
13+
## Gotcha
14+
15+
Browsers require a *user gesture* before an `AudioContext` is allowed to
16+
produce sound. We create the context lazily inside the Beep button
17+
handler — the button click is the gesture, so playback starts on the
18+
very first press.
19+
"""
20+
from imgui_bundle import imgui, immapp
21+
22+
23+
_audio_ctx = None # js AudioContext (Pyodide); None on desktop or until first click
24+
_frequency = 440.0 # Hz
25+
_duration_ms = 200 # ms
26+
_status = "" # last-action message shown under the controls
27+
28+
29+
def _ensure_audio_context():
30+
"""Lazy-init the browser's AudioContext on first call. No-op on desktop."""
31+
global _audio_ctx, _status
32+
if _audio_ctx is not None:
33+
return _audio_ctx
34+
try:
35+
from js import AudioContext # type: ignore[import-not-found,import-untyped]
36+
_audio_ctx = AudioContext.new()
37+
_status = "AudioContext ready"
38+
except Exception as e:
39+
_status = f"AudioContext unavailable: {e}"
40+
return _audio_ctx
41+
42+
43+
def _beep():
44+
global _status
45+
ctx = _ensure_audio_context()
46+
if ctx is None:
47+
return
48+
try:
49+
osc = ctx.createOscillator()
50+
gain = ctx.createGain()
51+
osc.type = "sine"
52+
osc.frequency.value = _frequency
53+
now = ctx.currentTime
54+
end = now + _duration_ms / 1000.0
55+
# Short attack/release ramp avoids speaker-click on start/stop
56+
gain.gain.setValueAtTime(0.0, now)
57+
gain.gain.linearRampToValueAtTime(0.3, now + 0.01)
58+
gain.gain.linearRampToValueAtTime(0.0, end)
59+
osc.connect(gain)
60+
gain.connect(ctx.destination)
61+
osc.start(now)
62+
osc.stop(end + 0.05)
63+
_status = f"Beep @ {_frequency:.0f} Hz for {_duration_ms} ms"
64+
except Exception as e:
65+
_status = f"Beep failed: {e}"
66+
67+
68+
def gui():
69+
global _frequency, _duration_ms
70+
imgui.text("WebAudio sanity check (Pyodide only)")
71+
imgui.separator()
72+
_, _frequency = imgui.slider_float("Frequency (Hz)", _frequency, 80.0, 2000.0)
73+
_, _duration_ms = imgui.slider_int("Duration (ms)", _duration_ms, 50, 1000)
74+
if imgui.button("Beep"):
75+
_beep()
76+
if _status:
77+
imgui.text_disabled(_status)
78+
79+
80+
immapp.run(gui, window_title="WebAudio beep", window_size=(500, 220))

0 commit comments

Comments
 (0)