Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b5a3bfe
test: add WPT smoke harness on top of main
mdydek Jul 2, 2026
08cbf92
feat: job
mdydek Jul 2, 2026
5e08c27
feat: parallel runner
mdydek Jul 3, 2026
171dae1
fix: wpt tests
mdydek Jul 6, 2026
15b1f15
fix: 2nd try
mdydek Jul 6, 2026
2942e66
feat: removed parallel runner, add summary in docs
mdydek Jul 6, 2026
7b4d34d
feat: removed wpt tests from ci
mdydek Jul 6, 2026
97ddfb6
Merge branch 'main' into feat/wpt-integration
mdydek Jul 6, 2026
777291e
fix: revert absn
mdydek Jul 6, 2026
f0537c9
feat: removed float32array assertions from library code
mdydek Jul 6, 2026
8b9a62c
feat: increase sample rate range
mdydek Jul 6, 2026
9ee4492
feat: analyser and biquad wpt improvements
mdydek Jul 6, 2026
8705ee0
Merge branch 'main' into feat/wpt-improvements1
mdydek Jul 6, 2026
93556b9
feat: fast biquad by using k-rate back again
mdydek Jul 7, 2026
c46dd5d
feat: audiobuffer wpt improvements
mdydek Jul 7, 2026
eb28219
feat: audio node settable channel properties
mdydek Jul 7, 2026
63fbb75
fix: race conditions
mdydek Jul 7, 2026
b87c143
fix: setting null buffer
mdydek Jul 8, 2026
b566cb9
Merge branch 'main' into feat/wpt-improvements2
mdydek Jul 13, 2026
1b12737
feat: final polish
mdydek Jul 16, 2026
67ca07c
Merge branch 'main' into feat/wpt-improvements2
mdydek Jul 16, 2026
ffc65d1
fix: test
mdydek Jul 16, 2026
43dee97
fix: 1st try with tests
mdydek Jul 17, 2026
caa4d65
Merge branch 'main' into feat/wpt-improvements2
mdydek Jul 17, 2026
5243c61
feat: validating node options in constructor
mdydek Jul 17, 2026
8088e78
fix: tets
mdydek Jul 17, 2026
1835cec
feat: typed ptr to const in all ho
mdydek Jul 21, 2026
4c4bcdd
feat: comments
mdydek Jul 21, 2026
f742ac6
Merge branch 'main' into feat/wpt-improvements2
mdydek Jul 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .claude/skills/audio-nodes/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,18 @@ Callback IDs are stored as `std::atomic<uint64_t>` on the node. `0` means no lis
### JS → Audio (graph mutations: connect/disconnect)
All graph mutations are queued via `AudioGraphManager` using its own SPSC channel (`addPendingNodeConnection`, `addPendingParamConnection`). The audio thread calls `graphManager_->preProcessGraph()` before each render pass to apply pending changes.

### Settable channel attributes (channelCount / channelCountMode / channelInterpretation)
These are mutable after construction. `AudioNode` (core) exposes virtual `setChannelCount` / `setChannelCountMode` / `setChannelInterpretation`. `channelCount` and `channelCountMode` are read only on the host thread during negotiation, so the JSI setter updates the core field directly then calls `HostNode::renegotiate()` → `Graph::renegotiateNodeChannels()` → `HostGraph::renegotiateNodeChannels()` (reuses `collectNegotiations` + an `AGEvent` buffer swap, self-drain aware when there is no audio/render consumer — offline construction/suspend and realtime suspended/stopped windows). When `AudioBufferSourceNode` `setBuffer` changes channel width, update `channelCount_` on the host thread then `renegotiate()` so MAX/CLAMPED_MAX downstream nodes update; the audio event still installs the prebuilt buffer (no audio-thread alloc). `channelInterpretation` is read on the audio thread in `processInputs` (`getInputBuffer()->sum(*input, channelInterpretation_)`), so it MUST be applied via `scheduleAudioEvent`, not mutated directly.

### Idle-node stale-buffer gating (isProcessable() at the mix boundary)
`AudioGraph::iter()` filters to `isProcessable()` nodes, so a node that has gone idle (e.g. a finished source, or the gain feeding off it) is skipped and its output buffer is NOT refreshed — it keeps the samples from an earlier quantum. Because downstream consumers read input buffers via `pool_.view(input_head)` regardless of processable state, that stale buffer would otherwise get re-summed into the destination every quantum, producing ghost echoes (this broke the `audionode-channel-rules` ~170-node discrete-mixing WPT test).

Fix: `GraphObject::process()` skips any input whose `isProcessable()` is false when collecting buffers to mix — an idle node presents silence to its consumers instead of a stale buffer. Nodes are visited in topological order (sources before consumers), so an upstream node's processable state for the quantum is already settled by the time a consumer reads it.

For this gate to be correct, a node must stay `isProcessable()` for the **entire** quantum in which it produces its final output, and flip on the **next** quantum. A one-shot source would otherwise flip to `NOT_PROCESSABLE` mid-quantum (inside `processNode` → `handleStopScheduled` → `disable`), which — since consumers run later in the same topological pass — would drop that final output (produces `0` where a real sample was expected). `AudioNode::disable()` therefore only sets `pendingDisable_`; `AudioNode::processInputs()` commits `setProcessableState(NOT_PROCESSABLE)` at the start of the *next* quantum and presents silence. Deferral lives at this single chokepoint, so no per-source-type edits are needed.

Tail-bearing nodes (Delay/Convolver/Biquad) need no special handling: while connected they stay `CONDITIONAL_PROCESSABLE` (processable via the first `isProcessable()` branch) so they are never skipped and never go stale; the `tailState_ != FINISHED` branch only matters after a disconnect, where either there is no consumer or consumers correctly gate on `isProcessable()`.

---

## Implementing a New Node — Checklist
Expand Down Expand Up @@ -328,7 +340,7 @@ All graph mutations are queued via `AudioGraphManager` using its own SPSC channe

7. **Spec compliance**
- Check the Web Audio API spec for default values, parameter ranges, and behavior
- See `web-audio-api.md` skill
- See `web-audio-api` skill

8. **Tests and docs** — see the `flow` skill

Expand Down
9 changes: 6 additions & 3 deletions .claude/skills/flow/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,17 @@ Files: `packages/react-native-audio-api/src/core/`
import { MyNodeOptions } from '../types';

export default class MyNode extends AudioNode implements IMyNode {
constructor(context: BaseAudioContext, options: MyNodeOptions) {
const node = context.context.createMyNode(options);
super(context, node);
constructor(context: BaseAudioContext, options?: MyNodeOptions) {
// Node-specific validation (if any) goes here, before create.
const node = context.context.createMyNode(options || {});
// Pass options so AudioNode validates channelCount / mode / interpretation.
super(context, node, options);
}
// getters/setters forwarded to (this.node as IMyNode)
}
```

`MyNodeOptions` must extend `AudioNodeOptions`. Shared `AudioNodeOptions` validation lives in `AudioNode`'s constructor (`validateAudioNodeOptions`) — do not re-validate those fields in per-node validators.
2. Add a factory method `createMyNode(options?)` to `src/core/BaseAudioContext.ts`.

3. Export from `src/index.ts`.
Expand Down
1 change: 1 addition & 0 deletions .claude/skills/host-objects/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Golden references: `GainNodeHostObject.h/.cpp` (effect node), `OscillatorNodeHos
- **Clear callback IDs in the destructor** for any HO that registers audio events. Otherwise the audio thread fires into a destroyed JS function.
- **Call `setExternalMemoryPressure`** when returning HOs or typed arrays backed by large native buffers.
- **Shadow state must be initialized** from `options` in the constructor — JS may read a property before ever setting it.
- **Typed node pointers are `T *const`.** Mirror `AudioNode *const audioNode_` — declare as `GainNode *const gainNode_;` (const pointer, mutable object), initialize in the ctor initializer list via `typedAudioNode<T>(node_)`. Never use `T *foo_ = nullptr`.

---

Expand Down
2 changes: 2 additions & 0 deletions .claude/skills/thread-safety-itc/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ On Android, `AudioPlayer::onErrorAfterClose` also takes `driverMutex_` because O

**Live `AudioContext` render quiescence:** `currentRenders_` on `AudioContext` is incremented at the start of each platform I/O callback (`IOSAudioPlayer::deliverOutputBuffers` / `AudioPlayer::onAudioReady`) via a reference passed in `initialize()`, and decremented when the callback returns (RAII scope). `suspend()` and `close()` call `waitForRenderQuiescence()` (under `driverMutex_`) before `processAudioEvents()` / `cleanup()`. Platform drivers share the `CommonPlayer` abstract base (`common/cpp/audioapi/core/CommonPlayer.h`).

**Graph Channel A producer self-drain:** `Graph::setProducerSelfDrain(true)` makes the JS/main producer drain Channel A after each enqueue. Enable only when there is no audio/render consumer (realtime: construction + after `suspend`/`close` quiescence; offline: before `startRendering` and after a scheduled suspend). Before disabling for `start`/`resume`/`renderAudio`, call `processEvents()` once (still as sole consumer) so the bounded channel is empty, then disable, then start the audio/render consumer; re-enable if start/resume fails. After enabling, call `processEvents()` once to flush backlog (avoids `WAIT_ON_FULL` deadlock if the channel was already full).

---

## Common Mistakes
Expand Down
Loading
Loading