Skip to content

sound: stream playback instead of generating the whole buffer before starting #834

Description

@martin-henz

Sounds are currently generated fully before playback starts: sampleWave (src/bundles/sound/src/functions.ts) samples the whole duration into a Float32Array first, and only then does play() fire off io().playSamples(...) to the host for actual AudioContext output. For a duration that generates faster than real-time (the common case), this means waiting out the entire sound's worth of generation before hearing anything, even though playback itself could have started almost immediately.

Idea: stream samples to playback as they're generated instead of batching the whole buffer first - start playing very soon after play_wave/play() is called, and only pause/throttle generation if it ever gets ahead of what's actually been consumed by playback. If generation is faster than real-time (typical), the result is a much more responsive "instant start" instead of the current wait-for-the-whole-buffer delay; if generation is ever slower than real-time (e.g. an expensive wave function evaluated through a tree-walking interpreter), playback would just naturally throttle to match.

This is the standard producer/consumer streaming-audio pattern: a ring buffer feeding a Web Audio AudioWorklet, with the producer pausing once the buffer's full and resuming once it drains.

Two things worth noting for anyone picking this up:

  • sampleWave's existing per-sample yield* wave(i / FS) delegation already gives a natural checkpoint between samples to hook an "await until there's room in the buffer" backpressure check into - the throttling point is closer to already-there than it looks.
  • The real cost is that sample generation happens in Conductor's runner Worker, while playback only works on the browser main thread (see the comment on SoundTabPlugin/SoundTabRpc). Today's single big-array handoff at the end would need to become a genuine streaming transport - either chunked postMessage calls or a SharedArrayBuffer ring buffer (which needs cross-origin-isolation headers). That's a real feature, not a small tweak to the current fire-and-forget design.

Side benefit: this would also naturally pace print() output (from a student's wave function) at real playback speed instead of the current all-at-once flood - loosely related to source-academy/py-slang#355 / source-academy/frontend#4183, though not required by either.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions