Skip to content

dasAudio: strudel in the WASM playground + single-threaded teardown - #3272

Merged
borisbat merged 1 commit into
masterfrom
bbatkin/wasm-audio
Jun 26, 2026
Merged

dasAudio: strudel in the WASM playground + single-threaded teardown#3272
borisbat merged 1 commit into
masterfrom
bbatkin/wasm-audio

Conversation

@borisbat

Copy link
Copy Markdown
Collaborator

What

Enables dasAudio in the emscripten/WASM build so strudel makes sound in the web playground — single-threaded. miniaudio's emscripten backend uses a ScriptProcessorNode whose audio callback runs on the main thread, and strudel's strudel_tick main-thread mode is non-blocking, so the whole stack collapses to one thread: no -pthread, no SharedArrayBuffer, no COOP/COEP. (The "is jobque compatible with wasm" question turned out to be the wrong question — the audio path routes around it.)

You can hear it in the playground: pick "Audio: strudel synth arpeggio" → Run.

The single-threaded teardown fix (the meat)

Single-threaded wasm has no separate audio thread, which breaks two teardown assumptions:

  1. JobStatus::Wait() (used by Stream/LockBox join) would deadlock waiting for a thread that can't exist → no-op on single-threaded wasm (#if defined(__EMSCRIPTEN__) && !defined(__EMSCRIPTEN_PTHREADS__)). Native unchanged.
  2. set_status_update defers a ref-release to an audio-thread command (notify_and_release). Single-threaded there's no thread to process it, so strudel_shutdown must drain command_processor itself. But draining on the main thread races the audio thread on native (concurrent command_processor on the non-thread-safe mixer context → heap corruption, caught under AOT). Fix: record at init whether the audio system came up single-threaded (g_audio_single_threaded) and drain only then; threaded builds keep the original join path untouched.

Also fixes a false-positive leak dump: the wasm CLI's end-of-callMain DumpJobQueLeaks + exit(1) fired the instant a browser-loop program (init/update/shutdown) installed its loop — while its objects were legitimately still alive. Gated off when a loop is active, with a real post-shutdown() leak check in stop_browser_loop.

dasAudio CMake fix (independent, worth landing on its own)

modules/dasAudio/CMakeLists.txt was missing strudel_sfx / strudel_sfxr / strudel_modal / strudel_scales registrations → breaks any static/embedded build (the wasm one, and any embedded SDK). Desktop hid it via a real-FS fallback.

Tests

  • tests/strudel_device/test_device_teardown.das — drives the real device (miniaudio null backend, headless/CI) through create/tick/shutdown + re-run, asserts no net JobQue leak. Interpreter-only (gated in tests/.das_test): AOT'ing a device+thread test surfaces the pre-existing main-thread drain race in audio_system_finalize; no device test is AOT'd anywhere.
  • site/tests/playground/audio.spec.js (@wasm) — runs the strudel sample in the playground, taps the Web Audio graph for real non-zero output, confirms it stops.

Notes for review

  • installAudioUnlock (main.js) resumes the AudioContext on the Run-click gesture — standard Web Audio autoplay handling (the playground's run path awaits before callMain).
  • New introspection/test bindings: count_jobque_leaks, sound_set_null_device, audio_is_single_threaded (documented).

Validation

Native interp + AOT (no regression; MCP failures during testing were dashv staleness, gone with a clean build), wasm playground (play/stop/re-run, leak-clean), lint + format + das2rst/sphinx doc gates, JIT smoke (no verifier errors).

🤖 Generated with Claude Code

https://claude.ai/code/session_01XvWu2izV1ebb2LgHwHaWjp

Copilot AI review requested due to automatic review settings June 25, 2026 21:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enables dasAudio + strudel playback in the Emscripten/WASM playground (single-threaded Web Audio backend), and adjusts teardown/leak-reporting behavior so browser-loop programs can stop cleanly and be re-run without false-positive leak dumps.

Changes:

  • Enable dasAudio for the web build and embed dasAudio/strudel .das module sources into the WASM package.
  • Fix single-threaded WASM teardown assumptions (jobque Wait() behavior + strudel shutdown draining) and add introspection helpers (count_jobque_leaks, sound_set_null_device, audio_is_single_threaded).
  • Add new playground audio samples plus Playwright/@wasm and interpreter-only device teardown tests.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
web/examples/ui/src/main.js Adds an AudioContext “autoplay unlock” wrapper to resume contexts created during a Run.
web/examples/ui/samples/examples/audio_strudel.das New strudel synth arpeggio sample using init/update/shutdown lifecycle.
web/examples/ui/samples/examples/audio_sine.das New simple sine-beep sample for WASM audio smoke testing.
web/examples/ui/samples/data.json Registers the new audio samples in the playground picker.
web/CMakeLists.txt Enables dasAudio on web builds and embeds dasAudio .das trees into the WASM filesystem.
utils/daScript/main.cpp Adjusts leak reporting for Emscripten browser-loop programs; adds post-loop leak check.
tests/strudel_device/test_device_teardown.das Adds headless/null-backend device teardown regression tests (interpreter-only).
tests/.das_test Gates tests/strudel_device off under --use-aot due to known AOT timing/race issues.
src/misc/job_que.cpp Makes JobStatus::Wait() a no-op on single-threaded Emscripten (no pthreads).
src/builtin/module_builtin_jobque.cpp Exposes count_jobque_leaks() to scripts for before/after leak assertions.
site/tests/playground/audio.spec.js Adds Playwright tests that verify real non-zero Web Audio output and eventual stop.
modules/dasAudio/strudel/strudel_player.das Drains audio command queue during shutdown only when audio is single-threaded.
modules/dasAudio/src/dasAudio.h Declares dasAudio_set_null_device and dasAudio_is_single_threaded API.
modules/dasAudio/src/dasAudio.cpp Implements null-backend forcing and records single-threaded init mode.
modules/dasAudio/CMakeLists.txt Registers missing strudel module .das files for static/embedded builds.
include/daScript/simulate/aot_builtin_jobque.h Adds AOT header declaration for count_jobque_leaks().
doc/source/stdlib/handmade/function-jobque-count_jobque_leaks-0xb4ae32ea24efdbdf.rst Documents count_jobque_leaks.
doc/source/stdlib/handmade/function-audio-sound_set_null_device-0x113af6c1887feebf.rst Documents sound_set_null_device.
doc/source/stdlib/handmade/function-audio-audio_is_single_threaded-0xce7d8afbe2050e04.rst Documents audio_is_single_threaded.
doc/reflections/das2rst.das Updates reflection grouping so new APIs appear in generated docs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread web/examples/ui/src/main.js Outdated
Comment thread utils/daScript/main.cpp Outdated
Comment thread modules/dasAudio/src/dasAudio.cpp

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.

Comment thread tests/strudel_device/test_device_teardown.das
Comment thread tests/strudel_device/test_device_teardown.das
Enables dasAudio in the emscripten build so strudel plays in the web playground,
single-threaded: miniaudio's ScriptProcessor backend runs its callback on the
main thread, so no -pthread / SharedArrayBuffer / cross-origin isolation is
needed. strudel_tick main-thread mode is non-blocking and the audio path is one
thread, so the jobque-on-wasm problem is sidestepped rather than solved.

Single-threaded teardown fix:
- JobStatus::Wait() is a no-op on single-threaded wasm (no worker thread can ever
  signal it; a condition wait would deadlock).
- strudel_shutdown drains the audio command queue to run the deferred status-box
  release ONLY when the audio system came up single-threaded (recorded at init,
  g_audio_single_threaded). Threaded builds keep the original join path -- a
  main-thread drain would race the audio thread's command_processor on the
  non-thread-safe mixer context (heap corruption, caught under AOT).
- The wasm CLI's end-of-callMain DumpJobQueLeaks + exit(1) fired while a
  browser-loop program was still alive (false positives); gated off when a loop
  is active, with a real post-shutdown leak check in stop_browser_loop.

dasAudio/CMakeLists.txt: register strudel_sfx/sfxr/modal/scales (were missing,
which breaks any static/embedded build, including wasm).

Tests:
- tests/strudel_device/test_device_teardown.das drives the real device
  (miniaudio null backend, headless) through create/tick/shutdown + re-run and
  asserts no net JobQue leak. Interpreter-only (gated in .das_test): AOT'ing a
  device+thread test surfaces a pre-existing main-thread drain race in
  audio_system_finalize; no device test is AOT'd.
- site/tests/playground/audio.spec.js (@wasm) runs the strudel sample, taps the
  Web Audio graph for real output, and confirms it stops.

New bindings: count_jobque_leaks, sound_set_null_device, audio_is_single_threaded.
Playground: installAudioUnlock (resumes the AudioContext on the Run gesture) plus
two audio samples.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XvWu2izV1ebb2LgHwHaWjp

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.

@borisbat
borisbat merged commit 70f25a8 into master Jun 26, 2026
38 checks passed
borisbat added a commit that referenced this pull request Jul 25, 2026
…ry/ doc archiving (#3563)

* release: bump version to 0.6.4 + 0.6.4 changelist

Version 0.6.3 -> 0.6.4 across platform.h, CMake, Sphinx conf, the
get_das_version doc example, and install/README. CHANGELIST.md gains the
0.6.4 (July 2026) section covering #3272-#3562. version_update.md skill
corrected: conf.py carries the full X.Y.Z in both fields (de-facto since
0.6.3).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* install: sync SDK CLAUDE.md to 0.6.4, ship dashv + find_dupe skills, scrub personal paths

install/CLAUDE.md picks up the 0.6.4-era language content (gen2 default
parser, bare named calls, with (module), cast parens, 16/8-bit lattice,
distinct types, addr<T?> sugar, delete-on-pointer-containers, options
stack, reserved words, the require hyphen rule, STYLE034/035 rows) plus
dashv/find_dupe skill rows; install/skills.list ships both skills.
Skills scrub: no personal names or machine-local paths in skill content
(external_module_debugging examples use a generic package root now).
Top-level CLAUDE.md: the named-arguments bullet documents the bare
foo(pos, name = value) form (probe-verified), and the stale pre-#3420
reinterpret operand-swallow gotcha is removed (probe-verified: the cast
is self-delimiting now).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* history/: archive completed-arc docs + doc_archiving skill

New skills/doc_archiving.md defines the archive-vs-stay test and the
process: git mv under history/<area>/, update path-qualified references,
add a note in the area's living index doc, and record every doc in the
Archive log at the bottom of history/README.md (the findability ledger).

Swept per the skill: the root rework docs (FIXED_ARRAY_REWORK,
ANNOTATION_INFO_REWORK, QUOTE_LOWERING, COVERAGE_GAP), the dasSQLITE API
rework corpus + tutorial-mockup design artifacts, the dasSpirv PHASE6-9
working plans, closed dasLLAMA docs (avx_kernel_matrix,
epyc9654_measurements, model_expansion_plan), the benchmarks/sql
linq_fold plans/audits, example plans (sfx_lab, scalar_packing, sequence,
Cadmus), and the daScriptTest migration checklist. ~33 path-qualified
references updated across daslib, tests, tutorials, RST, and skills;
living contracts (PROVIDER_CONTRACT, the MASTERPLANs, cited dasLLAMA
specs, results.md ledgers) stay put and carry archive notes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* tests/linq: clear pre-existing lint in files touched by the archive sweep

The archive sweep's comment-path updates put 7 linq_fold test files into
CI's changed-files lint set, surfacing 20 pre-existing warnings: unused
eid block arguments on create_entities callbacks (now
[unused_argument(eid)] per the in-file precedent — the name is decs-bound
and cannot be underscore-renamed) and unused require math / require
strings lines (dropped). tests/linq suite: 2007/2007 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* release: finish 0.6.4 docs, site, and archive ledger

Update the two manual titles and all live site version chips to 0.6.4, correct the version-update instructions to match established full-X.Y.Z practice, and clarify the Sphinx version comment. Expand the archive ledger to one exact old-to-new entry per moved document and repair local links in the archived docs.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants