Skip to content

Replace postMessage with SharedArrayBuffer for lock-free audio transfer#1295

Draft
kixelated wants to merge 2 commits intomainfrom
claude/refine-local-plan-8ImKA
Draft

Replace postMessage with SharedArrayBuffer for lock-free audio transfer#1295
kixelated wants to merge 2 commits intomainfrom
claude/refine-local-plan-8ImKA

Conversation

@kixelated
Copy link
Copy Markdown
Collaborator

Summary

Replaces the message-passing audio transfer mechanism with a lock-free shared ring buffer implementation using SharedArrayBuffer, eliminating sample drops and reducing latency overhead in audio worklet communication.

Key Changes

  • New SharedRingBuffer implementation (shared-ring-buffer.ts):

    • Lock-free ring buffer using atomic operations for thread-safe coordination between main thread and AudioWorklet
    • Handles out-of-order writes, gap filling with zeros, and automatic overflow management
    • Supports dynamic latency adjustment via setLatency()
    • Provides modular i32 arithmetic for correct wrapping behavior with large sample indices
  • Comprehensive test suite (shared-ring-buffer.test.ts):

    • 527 lines of tests covering initialization, insertion, reading, stall behavior, ring wrapping, multi-channel audio, overflow handling, and edge cases
    • Tests verify gap filling, out-of-order writes, latency skipping, and timestamp tracking
  • Decoder refactoring (decoder.ts):

    • Allocates SharedRingBuffer on worklet initialization with 1-second capacity
    • Writes decoded audio directly to shared memory via insert() instead of postMessage()
    • Polls shared buffer state (timestamp, stalled flag) every 50ms instead of receiving postMessage updates
    • Dynamically re-allocates buffer if capacity becomes insufficient for target latency
  • AudioWorklet simplification (render-worklet.ts):

    • Removes AudioRingBuffer dependency and complex state tracking
    • Simplified process() to directly read from SharedRingBuffer
    • Eliminates underflow tracking and periodic state messages
  • Message protocol cleanup (render.ts):

    • Removes Data and Latency message types
    • Init message now carries SharedRingBufferInit data (samples and control SharedArrayBuffers)
    • Eliminates ToMain state messages in favor of atomic polling

Notable Implementation Details

  • Uses atomic operations (Atomics.load, Atomics.store) for lock-free coordination
  • Implements modular i32 arithmetic (i32Max, slot()) to handle wrapping with signed 32-bit integers
  • Automatically advances READ pointer on overflow to prevent buffer exhaustion
  • Implements latency skipping to maintain target latency distance between READ and WRITE pointers
  • Handles fractional timestamps by rounding to nearest sample index

https://claude.ai/code/session_01DyHBcF8vEs1wtbJubRVExp

claude added 2 commits April 12, 2026 17:39
Audio decoded on the main thread was sent to the AudioWorklet via
postMessage with transferred Float32Array buffers, dropping samples
when the worklet was busy. This replaces that path with a
SharedRingBuffer backed by SharedArrayBuffer for lock-free, zero-copy
audio transfer via Atomics.

- SharedRingBuffer supports timestamp-based insertion, out-of-order
  writes, gap zero-filling, and overflow protection (READ advance)
- Reader skips ahead when buffered data exceeds LATENCY target
- Buffer starts at 1s capacity, re-allocates when LATENCY grows
- Main thread polls shared state (timestamp, stalled) instead of
  receiving postMessage from worklet

https://claude.ai/code/session_01DyHBcF8vEs1wtbJubRVExp
…h SAB + postMessage fallback

The previous commit replaced the postMessage audio path outright. This re-introduces
the postMessage transport as a fallback for environments without SharedArrayBuffer
(no cross-origin isolation), and hides both transports behind a single AudioBuffer
interface so the Decoder no longer needs to know which is in use.

- buffer.ts: new AudioBuffer interface, supportsSharedArrayBuffer() detector, and
  createAudioBuffer() factory. Wraps SharedRingBuffer or AudioRingBuffer depending
  on environment support.
- render.ts: messages split into init-shared / init-post plus data/latency/state
  for the fallback path.
- render-worklet.ts: handles both init paths; state messages are only sent in
  post mode (shared mode polls shared memory directly).
- decoder.ts: uses AudioBuffer via createAudioBuffer(); no direct knowledge of
  either transport.
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