cacophony 0.32.1 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1600 -0
- package/README.md +680 -484
- package/dist/automation.cjs +2 -0
- package/dist/automation.cjs.map +1 -0
- package/dist/automation.d.ts +8 -0
- package/dist/automation.mjs +15 -0
- package/dist/automation.mjs.map +1 -0
- package/dist/autoplayUnlock.cjs +1 -1
- package/dist/autoplayUnlock.cjs.map +1 -1
- package/dist/autoplayUnlock.d.ts +2 -0
- package/dist/autoplayUnlock.mjs +32 -26
- package/dist/autoplayUnlock.mjs.map +1 -1
- package/dist/basePlayback.cjs +1 -1
- package/dist/basePlayback.cjs.map +1 -1
- package/dist/basePlayback.d.ts +6 -18
- package/dist/basePlayback.mjs +5 -2
- package/dist/basePlayback.mjs.map +1 -1
- package/dist/bundles/dattorro-reverb-bundle.js_url.cjs +1 -1
- package/dist/bundles/dattorro-reverb-bundle.js_url.cjs.map +1 -1
- package/dist/bundles/dattorro-reverb-bundle.js_url.mjs +1 -1
- package/dist/bundles/dattorro-reverb-bundle.js_url.mjs.map +1 -1
- package/dist/bundles/fdn-reverb-bundle.js_url.cjs +1 -1
- package/dist/bundles/fdn-reverb-bundle.js_url.cjs.map +1 -1
- package/dist/bundles/fdn-reverb-bundle.js_url.mjs +1 -1
- package/dist/bundles/fdn-reverb-bundle.js_url.mjs.map +1 -1
- package/dist/bundles/modulated-delay-bundle.js_url.cjs +1 -1
- package/dist/bundles/modulated-delay-bundle.js_url.cjs.map +1 -1
- package/dist/bundles/modulated-delay-bundle.js_url.mjs +1 -1
- package/dist/bundles/modulated-delay-bundle.js_url.mjs.map +1 -1
- package/dist/cacophony.cjs +1 -1
- package/dist/cacophony.cjs.map +1 -1
- package/dist/cacophony.d.ts +60 -9
- package/dist/cacophony.mjs +157 -80
- package/dist/cacophony.mjs.map +1 -1
- package/dist/container.cjs +1 -1
- package/dist/container.cjs.map +1 -1
- package/dist/container.d.ts +7 -1
- package/dist/container.mjs +28 -21
- package/dist/container.mjs.map +1 -1
- package/dist/context.d.ts +6 -0
- package/dist/effectChain.cjs +1 -1
- package/dist/effectChain.cjs.map +1 -1
- package/dist/effectChain.mjs +22 -27
- package/dist/effectChain.mjs.map +1 -1
- package/dist/effects.cjs +1 -1
- package/dist/effects.cjs.map +1 -1
- package/dist/effects.d.ts +2 -1
- package/dist/effects.mjs +56 -47
- package/dist/effects.mjs.map +1 -1
- package/dist/events.d.ts +13 -3
- package/dist/group.cjs +1 -1
- package/dist/group.cjs.map +1 -1
- package/dist/group.d.ts +5 -2
- package/dist/group.mjs +13 -7
- package/dist/group.mjs.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/mediaStream.cjs +1 -1
- package/dist/mediaStream.cjs.map +1 -1
- package/dist/mediaStream.d.ts +1 -1
- package/dist/mediaStream.mjs +7 -7
- package/dist/mediaStream.mjs.map +1 -1
- package/dist/node.cjs +2 -2
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.ts +8 -7
- package/dist/node.mjs +49 -44
- package/dist/node.mjs.map +1 -1
- package/dist/occlusion.cjs +2 -0
- package/dist/occlusion.cjs.map +1 -0
- package/dist/occlusion.d.ts +19 -0
- package/dist/occlusion.mjs +41 -0
- package/dist/occlusion.mjs.map +1 -0
- package/dist/pannerMixin.cjs +1 -1
- package/dist/pannerMixin.cjs.map +1 -1
- package/dist/pannerMixin.d.ts +23 -50
- package/dist/pannerMixin.mjs +57 -29
- package/dist/pannerMixin.mjs.map +1 -1
- package/dist/pcmStream.cjs +1 -1
- package/dist/pcmStream.cjs.map +1 -1
- package/dist/pcmStream.d.ts +1 -1
- package/dist/pcmStream.mjs +8 -8
- package/dist/pcmStream.mjs.map +1 -1
- package/dist/playOptions.cjs +1 -1
- package/dist/playOptions.cjs.map +1 -1
- package/dist/playOptions.d.ts +5 -1
- package/dist/playOptions.mjs +27 -20
- package/dist/playOptions.mjs.map +1 -1
- package/dist/playback.cjs +1 -1
- package/dist/playback.cjs.map +1 -1
- package/dist/playback.d.ts +19 -1
- package/dist/playback.mjs +46 -24
- package/dist/playback.mjs.map +1 -1
- package/dist/routableSource.cjs +1 -1
- package/dist/routableSource.cjs.map +1 -1
- package/dist/routableSource.d.ts +26 -3
- package/dist/routableSource.mjs +69 -34
- package/dist/routableSource.mjs.map +1 -1
- package/dist/sessionLifecycle.cjs +2 -0
- package/dist/sessionLifecycle.cjs.map +1 -0
- package/dist/sessionLifecycle.d.ts +13 -0
- package/dist/sessionLifecycle.mjs +55 -0
- package/dist/sessionLifecycle.mjs.map +1 -0
- package/dist/sound.cjs +1 -1
- package/dist/sound.cjs.map +1 -1
- package/dist/sound.mjs +22 -18
- package/dist/sound.mjs.map +1 -1
- package/dist/spatialAutomation.cjs +2 -0
- package/dist/spatialAutomation.cjs.map +1 -0
- package/dist/spatialAutomation.d.ts +11 -0
- package/dist/spatialAutomation.mjs +53 -0
- package/dist/spatialAutomation.mjs.map +1 -0
- package/dist/synth.cjs +1 -1
- package/dist/synth.cjs.map +1 -1
- package/dist/synth.mjs +13 -9
- package/dist/synth.mjs.map +1 -1
- package/dist/synthGroup.cjs +1 -1
- package/dist/synthGroup.cjs.map +1 -1
- package/dist/synthGroup.mjs +2 -0
- package/dist/synthGroup.mjs.map +1 -1
- package/dist/synthPlayback.cjs +1 -1
- package/dist/synthPlayback.cjs.map +1 -1
- package/dist/synthPlayback.mjs +1 -6
- package/dist/synthPlayback.mjs.map +1 -1
- package/docs/assets/hierarchy.js +1 -1
- package/docs/assets/highlight.css +4 -4
- package/docs/assets/navigation.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/classes/AudioCache.html +4 -4
- package/docs/classes/AudioSprite.html +7 -7
- package/docs/classes/BarberpoleEffect.html +7 -7
- package/docs/classes/BiquadEffect.html +3 -3
- package/docs/classes/Bus.html +21 -21
- package/docs/classes/Cacophony.html +186 -98
- package/docs/classes/DynamicsEffect.html +7 -7
- package/docs/classes/FdnReverbEffect.html +7 -7
- package/docs/classes/FoaDecoder.html +5 -5
- package/docs/classes/FoaDecoderEffect.html +3 -3
- package/docs/classes/FrequencyShifterEffect.html +7 -7
- package/docs/classes/Group.html +53 -36
- package/docs/classes/HarmonizerEffect.html +7 -7
- package/docs/classes/ImpulseResponseEffect.html +3 -3
- package/docs/classes/KWeightingFilter.html +4 -4
- package/docs/classes/LoudnessMeter.html +11 -11
- package/docs/classes/MediaStreamPlayback.html +146 -147
- package/docs/classes/MediaStreamSound.html +118 -54
- package/docs/classes/MicrophoneStream.html +119 -55
- package/docs/classes/ModulatedDelayEffect.html +7 -7
- package/docs/classes/PcmStreamPlayback.html +146 -147
- package/docs/classes/PcmStreamSound.html +128 -64
- package/docs/classes/PhaserEffect.html +7 -7
- package/docs/classes/Playback.html +218 -175
- package/docs/classes/ReverbEffect.html +7 -7
- package/docs/classes/Scheduler.html +3 -3
- package/docs/classes/ShareEffect.html +3 -3
- package/docs/classes/Sound.html +136 -78
- package/docs/classes/SpectralFreezeEffect.html +7 -7
- package/docs/classes/StereoWidenerEffect.html +3 -3
- package/docs/classes/Synth.html +126 -68
- package/docs/classes/SynthGroup.html +28 -28
- package/docs/classes/TremoloEffect.html +7 -7
- package/docs/classes/TruePeakDetector.html +8 -8
- package/docs/classes/WaveshaperEffect.html +7 -7
- package/docs/functions/encodeMonoToFoaSN3D.html +1 -1
- package/docs/functions/integratedLoudness.html +1 -1
- package/docs/functions/integratedUngatedLoudness.html +1 -1
- package/docs/functions/loudnessRange.html +1 -1
- package/docs/functions/timeStretch.html +1 -1
- package/docs/functions/timeStretchChannels.html +1 -1
- package/docs/functions/truePeakDb.html +1 -1
- package/docs/hierarchy.html +28 -28
- package/docs/index.html +165 -2
- package/docs/interfaces/AudioBuffer.html +8 -8
- package/docs/interfaces/AudioBufferSourceNode.html +22 -22
- package/docs/interfaces/AudioEventCallbacks.html +2 -2
- package/docs/interfaces/AudioListener.html +12 -12
- package/docs/interfaces/AudioNode.html +14 -14
- package/docs/interfaces/AudioParam.html +26 -8
- package/docs/interfaces/AudioWorklet.html +2 -2
- package/docs/interfaces/AudioWorkletNode.html +16 -16
- package/docs/interfaces/BarberpoleOptions.html +5 -5
- package/docs/interfaces/BaseContext.html +25 -25
- package/docs/interfaces/BaseSound.html +16 -16
- package/docs/interfaces/BiquadCoefficients.html +6 -6
- package/docs/interfaces/BiquadFilterNode.html +20 -20
- package/docs/interfaces/BuiltEffectGraph.html +6 -6
- package/docs/interfaces/CacheErrorEvent.html +5 -5
- package/docs/interfaces/CacheHitEvent.html +4 -4
- package/docs/interfaces/CacheMissEvent.html +4 -4
- package/docs/interfaces/CacophonyEffect.html +2 -2
- package/docs/interfaces/CacophonyLogger.html +4 -4
- package/docs/interfaces/ChannelMergerNode.html +14 -14
- package/docs/interfaces/ChannelSplitterNode.html +14 -14
- package/docs/interfaces/ConvolverNode.html +16 -16
- package/docs/interfaces/CreateSpriteOptions.html +3 -3
- package/docs/interfaces/DeviceChangeEvent.html +64 -0
- package/docs/interfaces/DynamicsOptions.html +7 -7
- package/docs/interfaces/FadeStartEvent.html +4 -4
- package/docs/interfaces/FdnReverbOptions.html +6 -6
- package/docs/interfaces/FoaDecoderOptions.html +2 -2
- package/docs/interfaces/FrequencyShifterOptions.html +3 -3
- package/docs/interfaces/GainNode.html +15 -15
- package/docs/interfaces/GlobalPlaybackEvent.html +3 -3
- package/docs/interfaces/HarmonizerOptions.html +6 -6
- package/docs/interfaces/ImpulseResponseOptions.html +5 -5
- package/docs/interfaces/LoadingCompleteEvent.html +5 -5
- package/docs/interfaces/LoadingErrorEvent.html +5 -5
- package/docs/interfaces/LoadingProgressEvent.html +6 -6
- package/docs/interfaces/LoadingStartEvent.html +3 -3
- package/docs/interfaces/LoudnessChannelInput.html +3 -3
- package/docs/interfaces/LoudnessReading.html +5 -5
- package/docs/interfaces/MediaElementSourceNode.html +15 -15
- package/docs/interfaces/MediaStreamAudioSourceNode.html +15 -15
- package/docs/interfaces/MediaStreamSoundOptions.html +5 -5
- package/docs/interfaces/MicrophoneStreamOptions.html +8 -8
- package/docs/interfaces/ModulatedDelayOptions.html +8 -8
- package/docs/interfaces/OfflineOptions.html +5 -5
- package/docs/interfaces/OscillatorNode.html +20 -20
- package/docs/interfaces/PannerNode.html +28 -28
- package/docs/interfaces/PcmStreamBufferEvent.html +2 -2
- package/docs/interfaces/PcmStreamSoundOptions.html +6 -6
- package/docs/interfaces/PhaserOptions.html +7 -7
- package/docs/interfaces/PlayOptions.html +13 -13
- package/docs/interfaces/PlaybackErrorEvent.html +5 -5
- package/docs/interfaces/ReverbOptions.html +13 -13
- package/docs/interfaces/RuntimeOptions.html +41 -15
- package/docs/interfaces/ScheduledCallbackHandle.html +2 -2
- package/docs/interfaces/SendGainOptions.html +68 -0
- package/docs/interfaces/SoundCleanupHoldings.html +4 -4
- package/docs/interfaces/SoundErrorEvent.html +6 -6
- package/docs/interfaces/SpectralFreezeOptions.html +4 -4
- package/docs/interfaces/SpriteRegion.html +4 -4
- package/docs/interfaces/StereoPannerNode.html +15 -15
- package/docs/interfaces/StereoToBFormatOptions.html +2 -2
- package/docs/interfaces/StereoWidenerOptions.html +4 -4
- package/docs/interfaces/StreamCapabilities.html +5 -5
- package/docs/interfaces/TimeStretchOptions.html +5 -5
- package/docs/interfaces/TremoloOptions.html +5 -5
- package/docs/interfaces/WaveshaperOptions.html +5 -5
- package/docs/modules.html +4 -4
- package/docs/types/AudioSinkId.html +35 -0
- package/docs/types/BaseAudioEvents.html +11 -11
- package/docs/types/BuiltEffect.html +1 -1
- package/docs/types/BusConnectionTarget.html +1 -1
- package/docs/types/CacheEventCallback.html +1 -1
- package/docs/types/CacophonyEvents.html +40 -20
- package/docs/types/ErrorEventCallback.html +1 -1
- package/docs/types/FadeType.html +1 -1
- package/docs/types/HrtfPannerOptions.html +1 -1
- package/docs/types/ImpulseResponseSource.html +1 -1
- package/docs/types/LoadingEventCallback.html +1 -1
- package/docs/types/LoopCount.html +1 -1
- package/docs/types/LoudnessChannel.html +1 -1
- package/docs/types/Orientation.html +3 -3
- package/docs/types/PanCloneOverrides.html +13 -7
- package/docs/types/PanType.html +1 -1
- package/docs/types/PcmStreamEvents.html +6 -6
- package/docs/types/PcmStreamState.html +1 -1
- package/docs/types/PlaybackEvents.html +1 -1
- package/docs/types/Position.html +1 -1
- package/docs/types/SoundEvents.html +1 -1
- package/docs/types/SoundType.html +1 -1
- package/docs/types/SourceNode.html +1 -1
- package/docs/types/SpriteMap.html +1 -1
- package/docs/types/SynthEvents.html +1 -1
- package/docs/types/ThreeDOptions.html +5 -15
- package/docs/types/WebCodecsStreamSound.html +1 -1
- package/docs/variables/CHANNEL_WEIGHTS.html +1 -1
- package/docs/variables/K_WEIGHTING_STAGE1_48K.html +1 -1
- package/docs/variables/K_WEIGHTING_STAGE2_48K.html +1 -1
- package/docs/variables/consoleLogger.html +1 -1
- package/docs/variables/noopLogger.html +1 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -19,36 +19,36 @@ Cacophony is a powerful and intuitive audio library designed for modern web appl
|
|
|
19
19
|
## Installation
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
npm install cacophony
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## Bundling and lazy loading
|
|
26
|
-
|
|
27
|
-
Use named ESM imports so your bundler can discard unused modules:
|
|
28
|
-
|
|
29
|
-
```typescript
|
|
30
|
-
import { AudioCache } from 'cacophony';
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
The package preserves its module boundaries and declares import side effects.
|
|
34
|
-
Importing `AudioCache` or an individual effect does not pull in the full
|
|
35
|
-
`Cacophony` class or the offline FFT implementation. The `Cacophony` class keeps
|
|
36
|
-
its complete factory API, so importing that class still includes the code its
|
|
37
|
-
methods reference.
|
|
38
|
-
|
|
39
|
-
`createStream()` loads MediaBunny on demand when using the WebCodecs transport.
|
|
40
|
-
HLS streams and browsers without `AudioDecoder` take their existing fallback
|
|
41
|
-
paths without loading it. Each AudioWorklet payload loads when its processor is
|
|
42
|
-
first needed. Call `await audio.loadWorklets()` to preload all processors.
|
|
43
|
-
|
|
44
|
-
Consumer bundlers must support code splitting to keep these imports in separate
|
|
45
|
-
downloadable chunks. Deploy all generated chunks with the application. MediaBunny
|
|
46
|
-
remains an installed dependency; lazy loading defers its runtime load.
|
|
47
|
-
|
|
48
|
-
Run `npm run build` followed by `npm run check:bundles -- --report` to inspect
|
|
49
|
-
initial bundle sizes for cache-only, full-factory, and single-effect consumers.
|
|
50
|
-
|
|
51
|
-
## Quick Start
|
|
22
|
+
npm install cacophony
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Bundling and lazy loading
|
|
26
|
+
|
|
27
|
+
Use named ESM imports so your bundler can discard unused modules:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { AudioCache } from 'cacophony';
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The package preserves its module boundaries and declares import side effects.
|
|
34
|
+
Importing `AudioCache` or an individual effect does not pull in the full
|
|
35
|
+
`Cacophony` class or the offline FFT implementation. The `Cacophony` class keeps
|
|
36
|
+
its complete factory API, so importing that class still includes the code its
|
|
37
|
+
methods reference.
|
|
38
|
+
|
|
39
|
+
`createStream()` loads MediaBunny on demand when using the WebCodecs transport.
|
|
40
|
+
HLS streams and browsers without `AudioDecoder` take their existing fallback
|
|
41
|
+
paths without loading it. Each AudioWorklet payload loads when its processor is
|
|
42
|
+
first needed. Call `await audio.loadWorklets()` to preload all processors.
|
|
43
|
+
|
|
44
|
+
Consumer bundlers must support code splitting to keep these imports in separate
|
|
45
|
+
downloadable chunks. Deploy all generated chunks with the application. MediaBunny
|
|
46
|
+
remains an installed dependency; lazy loading defers its runtime load.
|
|
47
|
+
|
|
48
|
+
Run `npm run build` followed by `npm run check:bundles -- --report` to inspect
|
|
49
|
+
initial bundle sizes for cache-only, full-factory, and single-effect consumers.
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
52
|
|
|
53
53
|
```typescript
|
|
54
54
|
import { Cacophony } from 'cacophony';
|
|
@@ -81,6 +81,37 @@ async function audioDemo() {
|
|
|
81
81
|
audioDemo();
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
## Per-playback occlusion
|
|
85
|
+
|
|
86
|
+
Apply obstruction to an individual `Playback` using a normalized amount:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
const [voice] = sound.play({ panType: 'HRTF', position: [5, 0, 0] });
|
|
90
|
+
voice.setOcclusion(0.7); // Smooth transition over 50 ms by default.
|
|
91
|
+
voice.setOcclusion(0, 100); // Restore the clear path over 100 ms.
|
|
92
|
+
console.log(voice.occlusion); // Last requested amount, independent of transition progress.
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The caller computes the amount and owns geometry, materials, and policy.
|
|
96
|
+
Cacophony renders it with a dedicated low-pass and attenuation stage after user
|
|
97
|
+
effects and before the panner. Finite amounts clamp to `[0, 1]`; non-finite
|
|
98
|
+
amounts throw `RangeError`. The optional transition duration is in milliseconds:
|
|
99
|
+
`0` applies immediately; negative or non-finite durations throw `RangeError`.
|
|
100
|
+
|
|
101
|
+
At `0`, the low-pass opens to the context's Nyquist frequency and attenuation
|
|
102
|
+
returns to unity, restoring the unobstructed response. At `1`, the low-pass
|
|
103
|
+
cutoff is 800 Hz (limited to Nyquist on lower-rate contexts) and attenuation is
|
|
104
|
+
-18 dB. Intermediate amounts map logarithmically to cutoff and linearly in
|
|
105
|
+
decibels to attenuation. The filter has a Butterworth response without a resonant
|
|
106
|
+
peak. Changes use linear AudioParam ramps, cancelling and re-anchoring any prior
|
|
107
|
+
occlusion transition at its current value.
|
|
108
|
+
|
|
109
|
+
Occlusion leaves ordinary volume and fades independent. Each voice starts clear;
|
|
110
|
+
updates affect only that voice. Pause/resume and panner changes retain its
|
|
111
|
+
occlusion, and `Playback.clone()` copies the requested amount into independent
|
|
112
|
+
nodes. One lazily allocated filter/gain pair is reused even after resetting to
|
|
113
|
+
`0`, then disconnected on cleanup. Updating a cleaned-up playback throws.
|
|
114
|
+
|
|
84
115
|
## Mobile Autoplay Handling
|
|
85
116
|
|
|
86
117
|
Modern browsers — especially iOS Safari and Chrome on Android — refuse to
|
|
@@ -118,6 +149,92 @@ const cacophony = new Cacophony(undefined, undefined, { autoUnlock: false });
|
|
|
118
149
|
The auto-unlock has no effect on offline contexts or in non-browser
|
|
119
150
|
environments (`typeof document === 'undefined'`).
|
|
120
151
|
|
|
152
|
+
## Mobile Robustness and Audio Sessions
|
|
153
|
+
|
|
154
|
+
By default, browser instances observe context `statechange` and emit `suspend`,
|
|
155
|
+
`resume`, or `interrupted` once per transition, including changes initiated by
|
|
156
|
+
the platform or by code using the context directly. Construction does not emit
|
|
157
|
+
an initial state event. A gesture unlock therefore emits `resume` along with
|
|
158
|
+
`unlock`. Once the context has run, `autoRecover` attempts silent resume after
|
|
159
|
+
suspension or interruption only while the document is visible, and retries on
|
|
160
|
+
`visibilitychange`, `pageshow`, and window `focus`. A context that has never run
|
|
161
|
+
(blocked by autoplay policy) is left to the gesture unlock, so `unlock` still fires
|
|
162
|
+
on the first interaction. It does not suspend audio merely because the page
|
|
163
|
+
is hidden. Failed recovery is logged through the instance logger; the existing
|
|
164
|
+
gesture unlock remains available when the browser requires interaction.
|
|
165
|
+
|
|
166
|
+
`await cacophony.pause()` records user intent before suspending. Neither automatic
|
|
167
|
+
recovery nor subsequent gestures undo that pause; call `await cacophony.resume()`
|
|
168
|
+
to continue. This also protects pauses made while a recovery attempt is pending.
|
|
169
|
+
Set `{ autoRecover: false }` to disable browser session and device-list listeners.
|
|
170
|
+
`autoUnlock` controls gesture handling separately; disable both to manage all
|
|
171
|
+
recovery yourself. Offline and non-browser contexts install no session listeners.
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
const cacophony = new Cacophony();
|
|
175
|
+
cacophony.on('interrupted', () => showAudioStatus('Audio interrupted'));
|
|
176
|
+
cacophony.on('resume', () => showAudioStatus('Audio running'));
|
|
177
|
+
cacophony.on('devicechange', ({ devices }) => updateOutputPicker(devices));
|
|
178
|
+
cacophony.on('sinkChange', ({ sinkId }) => showSelectedOutput(sinkId));
|
|
179
|
+
|
|
180
|
+
// When the instance is no longer needed:
|
|
181
|
+
cacophony.dispose(); // Removes platform and gesture listeners and event subscriptions.
|
|
182
|
+
await cacophony.context.close?.(); // The caller owns context and sound cleanup.
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`devicechange` carries `{ devices: MediaDeviceInfo[], timestamp: number }`, with
|
|
186
|
+
only `audiooutput` devices. Enumeration does not request permission; labels and
|
|
187
|
+
the device list may be restricted. Failed enumeration is logged and emits no
|
|
188
|
+
partial result. Closing the context also removes browser session, sink, and gesture
|
|
189
|
+
listeners when `autoRecover` is enabled; your event subscriptions stay attached
|
|
190
|
+
until `dispose()`. `sinkChange` reports the active sink independently of
|
|
191
|
+
the available device list; neither event selects a replacement automatically.
|
|
192
|
+
|
|
193
|
+
Automated tests cover native context transitions and synthetic device-list
|
|
194
|
+
notifications. Real hardware and OS interruptions require this manual matrix:
|
|
195
|
+
|
|
196
|
+
| Platform | Action | Expected result |
|
|
197
|
+
| --- | --- | --- |
|
|
198
|
+
| iOS Safari | Interrupt playback with a call or Siri, then return | `interrupted`/`suspend` when reported by the OS; visible-page recovery or gesture unlock restores audio |
|
|
199
|
+
| Android Chrome | Unplug headphones during playback | Device-list notification when delivered; context transitions are observable and recovery is attempted |
|
|
200
|
+
| Both | Background and foreground the page | No forced background suspension; retry on foreground if the context needs recovery |
|
|
201
|
+
| Both | Pause explicitly, then repeat these actions and tap | Audio remains paused until the app calls `resume()` |
|
|
202
|
+
|
|
203
|
+
## Output Device Selection
|
|
204
|
+
|
|
205
|
+
`setOutputDevice(sinkId)` delegates to the supplied context's runtime `setSinkId`
|
|
206
|
+
capability. `outputDevice` reads its current sink: `''` means the system default,
|
|
207
|
+
a device id selects that output, and `{ type: 'none' }` selects silent processing.
|
|
208
|
+
The same API works with the Node backend; its constructor `sinkId` option remains
|
|
209
|
+
available. Switching does not recreate the context or audio graph.
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
// Run from a user action. selectAudioOutput is optional and browser-dependent.
|
|
213
|
+
const mediaDevices = navigator.mediaDevices as MediaDevices & {
|
|
214
|
+
selectAudioOutput?: () => Promise<MediaDeviceInfo>;
|
|
215
|
+
};
|
|
216
|
+
if (mediaDevices.selectAudioOutput) {
|
|
217
|
+
const output = await mediaDevices.selectAudioOutput();
|
|
218
|
+
await cacophony.setOutputDevice(output.deviceId);
|
|
219
|
+
}
|
|
220
|
+
await cacophony.setOutputDevice(''); // Return to the system default.
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Concrete device selection requires a secure context and any permissions required
|
|
224
|
+
by the platform; obtaining authorized ids (through `selectAudioOutput` or permitted
|
|
225
|
+
`enumerateDevices`/`getUserMedia`) belongs to the app. Platform errors such as
|
|
226
|
+
`NotAllowedError` and `NotFoundError` propagate unchanged. Unsupported real-time
|
|
227
|
+
contexts reject with `Error("Output device selection is not supported by this context")`
|
|
228
|
+
until the shared typed-error API in #169 lands. Offline selection is a no-op and
|
|
229
|
+
`outputDevice` is `undefined`.
|
|
230
|
+
|
|
231
|
+
Support is detected on the actual context, never inferred from a browser name.
|
|
232
|
+
Native Chromium supports selection; Safari and Firefox versions lacking
|
|
233
|
+
`AudioContext.setSinkId` reject it. Standardized-audio-context wrappers that do
|
|
234
|
+
not expose `setSinkId` are unsupported even when the native browser supports it.
|
|
235
|
+
See [Chrome's output-selection guide](https://developer.chrome.com/blog/audiocontext-setsinkid/)
|
|
236
|
+
for platform permissions and silent-sink behavior.
|
|
237
|
+
|
|
121
238
|
## Core Concepts
|
|
122
239
|
|
|
123
240
|
### Sound vs Playback Architecture
|
|
@@ -127,30 +244,30 @@ Cacophony uses a two-tier architecture that separates audio assets from their pl
|
|
|
127
244
|
- **Sound**: Represents an audio asset (file or buffer). Acts as a container managing multiple playback instances.
|
|
128
245
|
- **Playback**: Represents a single playback instance of a Sound. Each call to `play()` creates a new Playback.
|
|
129
246
|
|
|
130
|
-
This design allows the same sound to be played multiple times simultaneously with different settings (volume, position, playback rate, etc.).
|
|
131
|
-
|
|
132
|
-
Pass invocation settings to `play(options)` to configure each voice before audio
|
|
133
|
-
starts. Omitted values inherit source defaults; `0` is valid for volume, stereo
|
|
134
|
-
pan, and loop count. Sound defaults and overlapping voices stay independent.
|
|
135
|
-
|
|
136
|
-
```typescript
|
|
137
|
-
const [voice] = sound.play({ volume: 0.5, playbackRate: 1.2, loopCount: 0,
|
|
138
|
-
panType: 'stereo', stereoPan: 0 });
|
|
139
|
-
const step = group.playOrdered(true, { volume: 0.7, panType: 'HRTF',
|
|
140
|
-
position: [10, 0, 5], threeDOptions: { distanceModel: 'linear', rolloffFactor: 0.1 } });
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
`stereoPan` requires the effective stereo mode; `position` and `threeDOptions`
|
|
144
|
-
require HRTF. Partial HRTF options preserve unspecified settings. Group all,
|
|
145
|
-
random, and ordered playback and sprite children accept these same options.
|
|
146
|
-
Invalid options throw before voice preparation, and group playback preflights
|
|
147
|
-
every member. Direct `Playback.play(options)` also supports these settings,
|
|
148
|
-
scheduling, and fades. Calling play on an already playing voice remains a no-op
|
|
149
|
-
after validation.
|
|
150
|
-
|
|
151
|
-
Synths and live streams support volume, spatial settings, and fade-in. They
|
|
152
|
-
reject playback rate, loop count, scheduled starts, end fades, and per-loop fade
|
|
153
|
-
settings because these sources do not support those controls.
|
|
247
|
+
This design allows the same sound to be played multiple times simultaneously with different settings (volume, position, playback rate, etc.).
|
|
248
|
+
|
|
249
|
+
Pass invocation settings to `play(options)` to configure each voice before audio
|
|
250
|
+
starts. Omitted values inherit source defaults; `0` is valid for volume, stereo
|
|
251
|
+
pan, and loop count. Sound defaults and overlapping voices stay independent.
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
const [voice] = sound.play({ volume: 0.5, playbackRate: 1.2, loopCount: 0,
|
|
255
|
+
panType: 'stereo', stereoPan: 0 });
|
|
256
|
+
const step = group.playOrdered(true, { volume: 0.7, panType: 'HRTF',
|
|
257
|
+
position: [10, 0, 5], threeDOptions: { distanceModel: 'linear', rolloffFactor: 0.1 } });
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
`stereoPan` requires the effective stereo mode; `position` and `threeDOptions`
|
|
261
|
+
require HRTF. Partial HRTF options preserve unspecified settings. Group all,
|
|
262
|
+
random, and ordered playback and sprite children accept these same options.
|
|
263
|
+
Invalid options throw before voice preparation, and group playback preflights
|
|
264
|
+
every member. Direct `Playback.play(options)` also supports these settings,
|
|
265
|
+
scheduling, and fades. Calling play on an already playing voice remains a no-op
|
|
266
|
+
after validation.
|
|
267
|
+
|
|
268
|
+
Synths and live streams support volume, spatial settings, and fade-in. They
|
|
269
|
+
reject playback rate, loop count, scheduled starts, end fades, and per-loop fade
|
|
270
|
+
settings because these sources do not support those controls.
|
|
154
271
|
|
|
155
272
|
```typescript
|
|
156
273
|
const sound = await cacophony.createSound('laser.mp3');
|
|
@@ -173,111 +290,111 @@ setTimeout(() => playback1.pause(), 1000);
|
|
|
173
290
|
setTimeout(() => playback2.stop(), 2000);
|
|
174
291
|
|
|
175
292
|
// Or control all playbacks through the Sound
|
|
176
|
-
sound.stop(); // Stops all three playbacks
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
Each playback exposes a read-only `state`: `"unplayed"`, `"playing"`, `"paused"`,
|
|
180
|
-
or `"stopped"`. When `sound.preplay()` or `sound.play()` prepares a new voice,
|
|
181
|
-
it removes stopped entries from `sound.playbacks`. Repeated play/stop calls
|
|
182
|
-
therefore retain at most the most recently stopped voice in that collection.
|
|
183
|
-
Unplayed, paused, and playing voices remain registered.
|
|
184
|
-
|
|
185
|
-
An individual `playback.stop()` preserves the ability to call `playback.play()`
|
|
186
|
-
again. A successfully restarted playback rejoins `sound.playbacks` and receives
|
|
187
|
-
Sound controls again, even if it was previously reaped. Reaping disconnects its
|
|
188
|
-
output and sends; replay restores the Sound's current routing and event forwarding.
|
|
189
|
-
The playback retains its individual volume, rate, and other settings while reaped.
|
|
190
|
-
Reaping does not call `cleanup()`; call `playback.cleanup()` when a retained playback
|
|
191
|
-
is no longer needed.
|
|
192
|
-
|
|
193
|
-
### Audio Sprites for Games
|
|
194
|
-
|
|
195
|
-
Pack many short effects into one decoded WAV atlas while keeping each named
|
|
196
|
-
region as an ordinary, independently configurable `Sound`:
|
|
197
|
-
|
|
198
|
-
```bash
|
|
199
|
-
cacophony sprite sfx/laser.wav sfx/engine.wav \
|
|
200
|
-
--out public/sfx-atlas.wav --map public/sfx-atlas.json --gap 10
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
```typescript
|
|
204
|
-
import { Cacophony, type SpriteMap } from 'cacophony';
|
|
205
|
-
|
|
206
|
-
const map = {
|
|
207
|
-
laser: { start: 0, duration: 0.35 },
|
|
208
|
-
engine: { start: 0.36, duration: 1.2, loopCount: 'infinite' },
|
|
209
|
-
} as const satisfies SpriteMap;
|
|
210
|
-
|
|
211
|
-
const cacophony = new Cacophony();
|
|
212
|
-
const sprite = await cacophony.createSprite('/sfx-atlas.wav', map);
|
|
213
|
-
|
|
214
|
-
// Literal keys stay typed. Each child uses the normal Sound API.
|
|
215
|
-
sprite.sounds.laser.volume = 0.7;
|
|
216
|
-
sprite.sounds.laser.play();
|
|
217
|
-
|
|
218
|
-
// Dynamic names use the checked lookup.
|
|
219
|
-
const selected: string = chooseEffectName();
|
|
220
|
-
if (sprite.has(selected)) sprite.get(selected).play();
|
|
221
|
-
|
|
222
|
-
// Child configuration is independent; the atlas has no shared play controls.
|
|
223
|
-
sprite.sounds.engine.loop('infinite');
|
|
224
|
-
sprite.sounds.engine.play();
|
|
225
|
-
|
|
226
|
-
sprite.cleanup();
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
All child Sounds share the atlas `AudioBuffer`. `cleanup()` owns those children
|
|
230
|
-
but not clones created with `sound.clone()`, which remain independently usable.
|
|
231
|
-
|
|
232
|
-
## Sound Types
|
|
293
|
+
sound.stop(); // Stops all three playbacks
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Each playback exposes a read-only `state`: `"unplayed"`, `"playing"`, `"paused"`,
|
|
297
|
+
or `"stopped"`. When `sound.preplay()` or `sound.play()` prepares a new voice,
|
|
298
|
+
it removes stopped entries from `sound.playbacks`. Repeated play/stop calls
|
|
299
|
+
therefore retain at most the most recently stopped voice in that collection.
|
|
300
|
+
Unplayed, paused, and playing voices remain registered.
|
|
301
|
+
|
|
302
|
+
An individual `playback.stop()` preserves the ability to call `playback.play()`
|
|
303
|
+
again. A successfully restarted playback rejoins `sound.playbacks` and receives
|
|
304
|
+
Sound controls again, even if it was previously reaped. Reaping disconnects its
|
|
305
|
+
output and sends; replay restores the Sound's current routing and event forwarding.
|
|
306
|
+
The playback retains its individual volume, rate, and other settings while reaped.
|
|
307
|
+
Reaping does not call `cleanup()`; call `playback.cleanup()` when a retained playback
|
|
308
|
+
is no longer needed.
|
|
309
|
+
|
|
310
|
+
### Audio Sprites for Games
|
|
311
|
+
|
|
312
|
+
Pack many short effects into one decoded WAV atlas while keeping each named
|
|
313
|
+
region as an ordinary, independently configurable `Sound`:
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
cacophony sprite sfx/laser.wav sfx/engine.wav \
|
|
317
|
+
--out public/sfx-atlas.wav --map public/sfx-atlas.json --gap 10
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
import { Cacophony, type SpriteMap } from 'cacophony';
|
|
322
|
+
|
|
323
|
+
const map = {
|
|
324
|
+
laser: { start: 0, duration: 0.35 },
|
|
325
|
+
engine: { start: 0.36, duration: 1.2, loopCount: 'infinite' },
|
|
326
|
+
} as const satisfies SpriteMap;
|
|
327
|
+
|
|
328
|
+
const cacophony = new Cacophony();
|
|
329
|
+
const sprite = await cacophony.createSprite('/sfx-atlas.wav', map);
|
|
330
|
+
|
|
331
|
+
// Literal keys stay typed. Each child uses the normal Sound API.
|
|
332
|
+
sprite.sounds.laser.volume = 0.7;
|
|
333
|
+
sprite.sounds.laser.play();
|
|
233
334
|
|
|
234
|
-
|
|
335
|
+
// Dynamic names use the checked lookup.
|
|
336
|
+
const selected: string = chooseEffectName();
|
|
337
|
+
if (sprite.has(selected)) sprite.get(selected).play();
|
|
338
|
+
|
|
339
|
+
// Child configuration is independent; the atlas has no shared play controls.
|
|
340
|
+
sprite.sounds.engine.loop('infinite');
|
|
341
|
+
sprite.sounds.engine.play();
|
|
342
|
+
|
|
343
|
+
sprite.cleanup();
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
All child Sounds share the atlas `AudioBuffer`. `cleanup()` owns those children
|
|
347
|
+
but not clones created with `sound.clone()`, which remain independently usable.
|
|
348
|
+
|
|
349
|
+
## Sound Types
|
|
350
|
+
|
|
351
|
+
Cacophony supports three URL-backed sound labels:
|
|
235
352
|
|
|
236
353
|
| Type | Memory | Latency | Seeking | Multiple Instances | Best For |
|
|
237
354
|
|------|--------|---------|---------|-------------------|----------|
|
|
238
355
|
| **Buffer** (default) | High | None | Full | Yes | Sound effects, UI sounds, short music clips |
|
|
239
|
-
| **HTML** | Medium | Low | Full | Yes | Media-element-backed background music, large audio files, and podcasts |
|
|
240
|
-
| **Streaming** | Bounded | Low | Range-backed | One stream per source | Sample-accurate URL audio with effects, panning, and metering |
|
|
356
|
+
| **HTML** | Medium | Low | Full | Yes | Media-element-backed background music, large audio files, and podcasts |
|
|
357
|
+
| **Streaming** | Bounded | Low | Range-backed | One stream per source | Sample-accurate URL audio with effects, panning, and metering |
|
|
241
358
|
|
|
242
359
|
```typescript
|
|
243
360
|
// Buffer - entire file loaded into memory
|
|
244
361
|
const sfx = await cacophony.createSound('explosion.mp3', 'buffer');
|
|
245
362
|
|
|
246
|
-
// HTML - media-element-backed playback, good for large files
|
|
247
|
-
const music = await cacophony.createSound('bgm.mp3', 'html');
|
|
248
|
-
|
|
249
|
-
// Streaming - incremental demux + WebCodecs decode into a PCM worklet
|
|
250
|
-
const radio = await cacophony.createStream('https://example.com/live-radio.mp3');
|
|
251
|
-
console.log(radio.streamCapabilities);
|
|
252
|
-
// { transport: 'webcodecs', seekable: false, live: true, duration: Infinity }
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
`createStream(url)` uses a fetch → demux → WebCodecs `AudioDecoder` → PCM
|
|
256
|
-
worklet path when WebCodecs is present and can decode the primary audio track.
|
|
257
|
-
The returned source has `soundType === 'streaming'` and uses the same volume,
|
|
258
|
-
pan/HRTF, filter, bus, send, and loudness-meter APIs as every other routable
|
|
259
|
-
source. MP3, ADTS/MP4 AAC, Ogg, FLAC, and WAVE containers are supported.
|
|
260
|
-
|
|
261
|
-
If WebCodecs is absent or the primary track has no WebCodecs decoder,
|
|
262
|
-
`createStream()` falls back to a media-element `Sound`. Check
|
|
263
|
-
`streamCapabilities.transport` for `'webcodecs'` or `'media-element'` rather
|
|
264
|
-
than assuming a tier.
|
|
265
|
-
|
|
266
|
-
Native HLS (`.m3u8`) playback, notably in Safari, uses the media element
|
|
267
|
-
directly. In browsers without native HLS, `createStream()` lazily uses
|
|
268
|
-
`hls.js`, declared as an optional peer dependency:
|
|
269
|
-
|
|
270
|
-
```bash
|
|
271
|
-
npm install hls.js
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
If neither native HLS nor an installed, MSE-capable `hls.js` is available,
|
|
275
|
-
`createStream()` rejects with install guidance instead of leaving a media
|
|
276
|
-
element silently waiting. hls.js failures after loading are emitted through
|
|
277
|
-
the Sound's `soundError` event, and `sound.cleanup()` detaches and destroys the
|
|
278
|
-
hls.js instance.
|
|
279
|
-
|
|
280
|
-
### Format Fallback (Howler-style)
|
|
363
|
+
// HTML - media-element-backed playback, good for large files
|
|
364
|
+
const music = await cacophony.createSound('bgm.mp3', 'html');
|
|
365
|
+
|
|
366
|
+
// Streaming - incremental demux + WebCodecs decode into a PCM worklet
|
|
367
|
+
const radio = await cacophony.createStream('https://example.com/live-radio.mp3');
|
|
368
|
+
console.log(radio.streamCapabilities);
|
|
369
|
+
// { transport: 'webcodecs', seekable: false, live: true, duration: Infinity }
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
`createStream(url)` uses a fetch → demux → WebCodecs `AudioDecoder` → PCM
|
|
373
|
+
worklet path when WebCodecs is present and can decode the primary audio track.
|
|
374
|
+
The returned source has `soundType === 'streaming'` and uses the same volume,
|
|
375
|
+
pan/HRTF, filter, bus, send, and loudness-meter APIs as every other routable
|
|
376
|
+
source. MP3, ADTS/MP4 AAC, Ogg, FLAC, and WAVE containers are supported.
|
|
377
|
+
|
|
378
|
+
If WebCodecs is absent or the primary track has no WebCodecs decoder,
|
|
379
|
+
`createStream()` falls back to a media-element `Sound`. Check
|
|
380
|
+
`streamCapabilities.transport` for `'webcodecs'` or `'media-element'` rather
|
|
381
|
+
than assuming a tier.
|
|
382
|
+
|
|
383
|
+
Native HLS (`.m3u8`) playback, notably in Safari, uses the media element
|
|
384
|
+
directly. In browsers without native HLS, `createStream()` lazily uses
|
|
385
|
+
`hls.js`, declared as an optional peer dependency:
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
npm install hls.js
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
If neither native HLS nor an installed, MSE-capable `hls.js` is available,
|
|
392
|
+
`createStream()` rejects with install guidance instead of leaving a media
|
|
393
|
+
element silently waiting. hls.js failures after loading are emitted through
|
|
394
|
+
the Sound's `soundError` event, and `sound.cleanup()` detaches and destroys the
|
|
395
|
+
hls.js instance.
|
|
396
|
+
|
|
397
|
+
### Format Fallback (Howler-style)
|
|
281
398
|
|
|
282
399
|
Pass an array of URLs and Cacophony picks the first one the browser can play.
|
|
283
400
|
It queries `HTMLAudioElement.canPlayType` per extension and fetches only the
|
|
@@ -419,86 +536,86 @@ const playback = sound.play()[0];
|
|
|
419
536
|
playback.filters[0].frequency.value = 500; // Only affects this playback
|
|
420
537
|
```
|
|
421
538
|
|
|
422
|
-
See [TypeDoc](https://cacophony.js.org) for complete filter parameters and options.
|
|
423
|
-
|
|
424
|
-
### Per-source effects
|
|
425
|
-
|
|
426
|
-
Use `addEffect` for source character such as distortion, EQ, tremolo, or pitch
|
|
427
|
-
processing. The source stores the `CacophonyEffect` as a recipe and builds a
|
|
428
|
-
fresh effect instance for every playback, before that playback's panner. The
|
|
429
|
-
returned playback handle can be automated without changing another playback.
|
|
430
|
-
|
|
431
|
-
```typescript
|
|
432
|
-
const guitar = await cacophony.createSound('guitar.mp3');
|
|
433
|
-
const distortion = cacophony.createDistortion({ drive: 4 });
|
|
434
|
-
guitar.addEffect(distortion);
|
|
435
|
-
|
|
436
|
-
const [take] = guitar.play();
|
|
437
|
-
const liveTremolo = await take.addEffect(cacophony.createTremolo({ rate: 5 }));
|
|
438
|
-
take.rampEffectParam(liveTremolo, 'depth', 0.8, { duration: 250 });
|
|
439
|
-
|
|
440
|
-
guitar.removeEffect(distortion); // future playbacks no longer build it
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
### Advanced paper-backed effects
|
|
444
|
-
|
|
445
|
-
The advanced suite adds five non-neural effects derived from published DSP
|
|
446
|
-
algorithms. They work as per-source recipes or shared bus effects and expose
|
|
447
|
-
their live controls through `rampEffectParam`.
|
|
448
|
-
|
|
449
|
-
```typescript
|
|
450
|
-
// Equal-Hz SSB translation: harmonic material becomes bell-like/inharmonic.
|
|
451
|
-
guitar.addEffect(cacophony.createFrequencyShifter({ frequency: 180 }));
|
|
452
|
-
|
|
453
|
-
// Endlessly descending notches (use a positive rate to reverse direction).
|
|
454
|
-
drums.addEffect(cacophony.createBarberpole({ rate: -0.1, stages: 32 }));
|
|
455
|
-
|
|
456
|
-
// Dry voice plus a fifth and octave, all produced in one STFT analysis pass.
|
|
457
|
-
vocals.addEffect(cacophony.createHarmonizer({
|
|
458
|
-
semitonesA: 7,
|
|
459
|
-
semitonesB: 12,
|
|
460
|
-
gainA: 0.6,
|
|
461
|
-
gainB: 0.45,
|
|
462
|
-
}));
|
|
463
|
-
|
|
464
|
-
// Capture/release a phase-continuing spectral texture at runtime.
|
|
465
|
-
const freeze = await playback.addEffect(cacophony.createSpectralFreeze());
|
|
466
|
-
playback.rampEffectParam(freeze, 'freeze', 1); // capture
|
|
467
|
-
playback.rampEffectParam(freeze, 'freeze', 0); // return to live input
|
|
468
|
-
|
|
469
|
-
// Convert mono or stereo material to transient-preserving decorrelated stereo.
|
|
470
|
-
ambience.addEffect(cacophony.createStereoWidener({
|
|
471
|
-
width: 0.75,
|
|
472
|
-
decorrelation: 1,
|
|
473
|
-
transientProtection: 0.8,
|
|
474
|
-
}));
|
|
475
|
-
```
|
|
476
|
-
|
|
477
|
-
`createFrequencyShifter` implements Wardle's Hilbert-transform single-sideband
|
|
478
|
-
modulator (DAFx-98). `createBarberpole` implements the warped spectral-delay SSB
|
|
479
|
-
structure of Esqueda, Valimaki and Parker (DAFx-15). `createHarmonizer` extends
|
|
480
|
-
the Laroche-Dolson identity-phase-locked peak shifter to two simultaneous
|
|
481
|
-
voices. `createSpectralFreeze` preserves measured inter-frame phase advance
|
|
482
|
-
instead of looping one static FFT frame. `createStereoWidener` uses sparse
|
|
483
|
-
velvet-noise decorrelation and temporarily retreats to the direct path for
|
|
484
|
-
transients.
|
|
485
|
-
|
|
486
|
-
Keep shared space effects such as reverb on a bus and feed them with a wet
|
|
487
|
-
send. This leaves the playback's dry character chain intact while every source
|
|
488
|
-
sent to the bus shares one live reverb instance.
|
|
489
|
-
|
|
490
|
-
```typescript
|
|
491
|
-
const vocals = await cacophony.createSound('vocals.mp3');
|
|
492
|
-
vocals.addEffect(cacophony.createDistortion({ drive: 1.5 })); // dry, per playback
|
|
493
|
-
|
|
494
|
-
const chamber = cacophony.createBus('chamber');
|
|
495
|
-
await chamber.addFilter(cacophony.createReverb({ wet: 1, dry: 0 }));
|
|
496
|
-
vocals.routeTo(chamber, 0.25); // 25% wet send; primary output stays on master
|
|
497
|
-
|
|
498
|
-
vocals.play();
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
## Custom Audio Routing
|
|
539
|
+
See [TypeDoc](https://cacophony.js.org) for complete filter parameters and options.
|
|
540
|
+
|
|
541
|
+
### Per-source effects
|
|
542
|
+
|
|
543
|
+
Use `addEffect` for source character such as distortion, EQ, tremolo, or pitch
|
|
544
|
+
processing. The source stores the `CacophonyEffect` as a recipe and builds a
|
|
545
|
+
fresh effect instance for every playback, before that playback's panner. The
|
|
546
|
+
returned playback handle can be automated without changing another playback.
|
|
547
|
+
|
|
548
|
+
```typescript
|
|
549
|
+
const guitar = await cacophony.createSound('guitar.mp3');
|
|
550
|
+
const distortion = cacophony.createDistortion({ drive: 4 });
|
|
551
|
+
guitar.addEffect(distortion);
|
|
552
|
+
|
|
553
|
+
const [take] = guitar.play();
|
|
554
|
+
const liveTremolo = await take.addEffect(cacophony.createTremolo({ rate: 5 }));
|
|
555
|
+
take.rampEffectParam(liveTremolo, 'depth', 0.8, { duration: 250 });
|
|
556
|
+
|
|
557
|
+
guitar.removeEffect(distortion); // future playbacks no longer build it
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### Advanced paper-backed effects
|
|
561
|
+
|
|
562
|
+
The advanced suite adds five non-neural effects derived from published DSP
|
|
563
|
+
algorithms. They work as per-source recipes or shared bus effects and expose
|
|
564
|
+
their live controls through `rampEffectParam`.
|
|
565
|
+
|
|
566
|
+
```typescript
|
|
567
|
+
// Equal-Hz SSB translation: harmonic material becomes bell-like/inharmonic.
|
|
568
|
+
guitar.addEffect(cacophony.createFrequencyShifter({ frequency: 180 }));
|
|
569
|
+
|
|
570
|
+
// Endlessly descending notches (use a positive rate to reverse direction).
|
|
571
|
+
drums.addEffect(cacophony.createBarberpole({ rate: -0.1, stages: 32 }));
|
|
572
|
+
|
|
573
|
+
// Dry voice plus a fifth and octave, all produced in one STFT analysis pass.
|
|
574
|
+
vocals.addEffect(cacophony.createHarmonizer({
|
|
575
|
+
semitonesA: 7,
|
|
576
|
+
semitonesB: 12,
|
|
577
|
+
gainA: 0.6,
|
|
578
|
+
gainB: 0.45,
|
|
579
|
+
}));
|
|
580
|
+
|
|
581
|
+
// Capture/release a phase-continuing spectral texture at runtime.
|
|
582
|
+
const freeze = await playback.addEffect(cacophony.createSpectralFreeze());
|
|
583
|
+
playback.rampEffectParam(freeze, 'freeze', 1); // capture
|
|
584
|
+
playback.rampEffectParam(freeze, 'freeze', 0); // return to live input
|
|
585
|
+
|
|
586
|
+
// Convert mono or stereo material to transient-preserving decorrelated stereo.
|
|
587
|
+
ambience.addEffect(cacophony.createStereoWidener({
|
|
588
|
+
width: 0.75,
|
|
589
|
+
decorrelation: 1,
|
|
590
|
+
transientProtection: 0.8,
|
|
591
|
+
}));
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
`createFrequencyShifter` implements Wardle's Hilbert-transform single-sideband
|
|
595
|
+
modulator (DAFx-98). `createBarberpole` implements the warped spectral-delay SSB
|
|
596
|
+
structure of Esqueda, Valimaki and Parker (DAFx-15). `createHarmonizer` extends
|
|
597
|
+
the Laroche-Dolson identity-phase-locked peak shifter to two simultaneous
|
|
598
|
+
voices. `createSpectralFreeze` preserves measured inter-frame phase advance
|
|
599
|
+
instead of looping one static FFT frame. `createStereoWidener` uses sparse
|
|
600
|
+
velvet-noise decorrelation and temporarily retreats to the direct path for
|
|
601
|
+
transients.
|
|
602
|
+
|
|
603
|
+
Keep shared space effects such as reverb on a bus and feed them with a wet
|
|
604
|
+
send. This leaves the playback's dry character chain intact while every source
|
|
605
|
+
sent to the bus shares one live reverb instance.
|
|
606
|
+
|
|
607
|
+
```typescript
|
|
608
|
+
const vocals = await cacophony.createSound('vocals.mp3');
|
|
609
|
+
vocals.addEffect(cacophony.createDistortion({ drive: 1.5 })); // dry, per playback
|
|
610
|
+
|
|
611
|
+
const chamber = cacophony.createBus('chamber');
|
|
612
|
+
await chamber.addFilter(cacophony.createReverb({ wet: 1, dry: 0 }));
|
|
613
|
+
vocals.routeTo(chamber, 0.25); // 25% wet send; primary output stays on master
|
|
614
|
+
|
|
615
|
+
vocals.play();
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
## Custom Audio Routing
|
|
502
619
|
|
|
503
620
|
Cacophony exposes the underlying Web Audio graph through Playback instances, enabling manual routing through custom effects chains. This is the low-level foundation for building complex audio processing pipelines.
|
|
504
621
|
|
|
@@ -556,9 +673,9 @@ API documented below. The Bus class supersedes the older user-built
|
|
|
556
673
|
A `Bus` is a named summing node with its own filter chain and per-edge
|
|
557
674
|
gain on outgoing connections. Sounds and synths route to buses via
|
|
558
675
|
`routeTo`; buses can carry rich effects (not just BiquadFilter) by
|
|
559
|
-
adding a `CacophonyEffect` to their filter chain. The built-in
|
|
560
|
-
`cacophony.createReverb()` and `cacophony.createImpulseResponse()` effects
|
|
561
|
-
are ready to drop into a bus.
|
|
676
|
+
adding a `CacophonyEffect` to their filter chain. The built-in
|
|
677
|
+
`cacophony.createReverb()` and `cacophony.createImpulseResponse()` effects
|
|
678
|
+
are ready to drop into a bus.
|
|
562
679
|
|
|
563
680
|
```typescript
|
|
564
681
|
// 1. Create a named bus
|
|
@@ -582,6 +699,28 @@ drums.routeTo(reverbBus, 0.3);
|
|
|
582
699
|
drums.play();
|
|
583
700
|
```
|
|
584
701
|
|
|
702
|
+
Source sends can be changed and removed while voices are playing:
|
|
703
|
+
|
|
704
|
+
```typescript
|
|
705
|
+
drums.routeTo(reverbBus, 0.1, { duration: 250 }); // ramp the existing send over 250 ms
|
|
706
|
+
drums.routeTo(reverbBus, 0, { duration: 500, type: 'exponential' });
|
|
707
|
+
drums.removeSend(reverbBus); // disconnect the send on every voice and forget it for future voices
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
Gain changes are immediate by default. Ramps reuse each voice's send GainNode,
|
|
711
|
+
cancel earlier automation, and start from the current envelope value. New sends
|
|
712
|
+
and future voices start at the requested gain. Exponential fades to zero reach
|
|
713
|
+
exact silence at the endpoint; transitions from zero or involving negative gains
|
|
714
|
+
use a linear ramp. Durations are finite, non-negative milliseconds and gains must
|
|
715
|
+
be finite.
|
|
716
|
+
|
|
717
|
+
`removeSend` leaves primary routing and other sends alone, even when the removed
|
|
718
|
+
send's bus is also the primary target. It is a no-op if the source has no send to
|
|
719
|
+
that bus. Both methods accept a registered bus name and work on Sound, Synth,
|
|
720
|
+
stream sources, and Group members. To remove a send after its named bus has been
|
|
721
|
+
destroyed, pass the retained bus reference; its name is no longer registered.
|
|
722
|
+
Removal disconnects immediately.
|
|
723
|
+
|
|
585
724
|
### Looking up buses by name
|
|
586
725
|
|
|
587
726
|
```typescript
|
|
@@ -599,81 +738,81 @@ the same node as `cacophony.globalGainNode`, so the existing
|
|
|
599
738
|
transparently. Routing a sound to `cacophony.master` (or never calling
|
|
600
739
|
`routeTo`) sends it through the master path.
|
|
601
740
|
|
|
602
|
-
### Bus-to-bus routing with per-edge gain
|
|
741
|
+
### Bus-to-bus routing with per-edge gain
|
|
603
742
|
|
|
604
743
|
```typescript
|
|
605
744
|
const groupBus = cacophony.createBus('group');
|
|
606
745
|
const sendBus = cacophony.createBus('aux');
|
|
607
746
|
groupBus.connect(sendBus, 0.2); // 20% send: groupBus.output → sendGain(0.2) → sendBus.input
|
|
608
|
-
groupBus.disconnect(sendBus); // tears down the sendGain too
|
|
609
|
-
```
|
|
610
|
-
|
|
611
|
-
### Native impulse responses
|
|
612
|
-
|
|
613
|
-
`createImpulseResponse()` builds a native `ConvolverNode` effect from an
|
|
614
|
-
`AudioBuffer` or URL. URL-backed IRs are fetched and decoded once per audio
|
|
615
|
-
context and URL; failed loads are evicted so a later call can retry. Convolver
|
|
616
|
-
normalization defaults to `false`, preserving measured IR gain.
|
|
617
|
-
|
|
618
|
-
```typescript
|
|
619
|
-
// Wet-only send bus: returns a single ConvolverNode internally.
|
|
620
|
-
const chamber = cacophony.createBus('chamber');
|
|
621
|
-
await chamber.addFilter(cacophony.createImpulseResponse('/irs/chamber.wav'));
|
|
622
|
-
vocals.routeTo(chamber, 0.25);
|
|
623
|
-
|
|
624
|
-
// Inline dry/wet graph: exposes dry/wet AudioParams for bus automation.
|
|
625
|
-
const room = cacophony.createImpulseResponse('/irs/room.wav', {
|
|
626
|
-
dry: 0.7,
|
|
627
|
-
wet: 0.3,
|
|
628
|
-
normalize: false,
|
|
629
|
-
});
|
|
630
|
-
const handle = await bus.addFilter(room);
|
|
631
|
-
bus.rampFilterParam(handle, 'wet', 0.6, { duration: 500 });
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
### Adding custom effects to a bus
|
|
635
|
-
|
|
636
|
-
Bus filter chains accept Cacophony-built BiquadFilters and any
|
|
637
|
-
`CacophonyEffect`. Raw third-party AudioNodes are rejected unless you
|
|
638
|
-
wrap them explicitly with `cacophony.shareEffect(node)` — this surfaces
|
|
639
|
-
the shared-state intent (the same node will run on every bus that adds it).
|
|
640
|
-
An effect can build either a single `AudioNode` or a `BuiltEffectGraph` with
|
|
641
|
-
separate `input`/`output` endpoints for multi-node processors.
|
|
747
|
+
groupBus.disconnect(sendBus); // tears down the sendGain too
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
### Native impulse responses
|
|
751
|
+
|
|
752
|
+
`createImpulseResponse()` builds a native `ConvolverNode` effect from an
|
|
753
|
+
`AudioBuffer` or URL. URL-backed IRs are fetched and decoded once per audio
|
|
754
|
+
context and URL; failed loads are evicted so a later call can retry. Convolver
|
|
755
|
+
normalization defaults to `false`, preserving measured IR gain.
|
|
756
|
+
|
|
757
|
+
```typescript
|
|
758
|
+
// Wet-only send bus: returns a single ConvolverNode internally.
|
|
759
|
+
const chamber = cacophony.createBus('chamber');
|
|
760
|
+
await chamber.addFilter(cacophony.createImpulseResponse('/irs/chamber.wav'));
|
|
761
|
+
vocals.routeTo(chamber, 0.25);
|
|
762
|
+
|
|
763
|
+
// Inline dry/wet graph: exposes dry/wet AudioParams for bus automation.
|
|
764
|
+
const room = cacophony.createImpulseResponse('/irs/room.wav', {
|
|
765
|
+
dry: 0.7,
|
|
766
|
+
wet: 0.3,
|
|
767
|
+
normalize: false,
|
|
768
|
+
});
|
|
769
|
+
const handle = await bus.addFilter(room);
|
|
770
|
+
bus.rampFilterParam(handle, 'wet', 0.6, { duration: 500 });
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
### Adding custom effects to a bus
|
|
774
|
+
|
|
775
|
+
Bus filter chains accept Cacophony-built BiquadFilters and any
|
|
776
|
+
`CacophonyEffect`. Raw third-party AudioNodes are rejected unless you
|
|
777
|
+
wrap them explicitly with `cacophony.shareEffect(node)` — this surfaces
|
|
778
|
+
the shared-state intent (the same node will run on every bus that adds it).
|
|
779
|
+
An effect can build either a single `AudioNode` or a `BuiltEffectGraph` with
|
|
780
|
+
separate `input`/`output` endpoints for multi-node processors.
|
|
642
781
|
|
|
643
782
|
```typescript
|
|
644
783
|
const eq = cacophony.createBiquadFilter({ type: 'highshelf', frequency: 4000, gain: 3 });
|
|
645
784
|
await bus.addFilter(eq);
|
|
646
785
|
|
|
647
|
-
const sharedWorklet = new AudioWorkletNode(cacophony.context, 'my-fx');
|
|
648
|
-
await bus.addFilter(cacophony.shareEffect(sharedWorklet));
|
|
649
|
-
```
|
|
650
|
-
|
|
651
|
-
FOA binaural decoding is available in both forms. Use
|
|
652
|
-
`createFoaDecoder()` when you want explicit custom wiring through
|
|
653
|
-
`decoder.input` and `decoder.output`; use `createFoaDecoderEffect()` only on a
|
|
654
|
-
dedicated 4-channel ACN/SN3D FOA bus, typically as the first and only filter
|
|
655
|
-
that converts that bus to stereo binaural output.
|
|
656
|
-
|
|
657
|
-
For physically correct mono positioning, use `encodeMonoToFoaSN3D()` and feed
|
|
658
|
-
its ACN/SN3D `[W, Y, Z, X]` output to the decoder. Stereo recordings can instead
|
|
659
|
-
be perceptually upmixed with `createStereoToBFormatNode()`. The default
|
|
660
|
-
`algorithm: 'perceptual'` path preserves the existing three-band front cue;
|
|
661
|
-
`algorithm: 'bcc'` uses frequency-domain coherence and pan analysis:
|
|
662
|
-
|
|
663
|
-
```typescript
|
|
664
|
-
const upmixer = await cacophony.createStereoToBFormatNode({ algorithm: 'bcc' });
|
|
665
|
-
const decoder = await cacophony.createFoaDecoder();
|
|
666
|
-
stereoSource.connect(upmixer);
|
|
667
|
-
upmixer.connect(decoder.input);
|
|
668
|
-
decoder.output.connect(cacophony.context.destination);
|
|
669
|
-
|
|
670
|
-
// BCC analysis is stateful. Reset it after a seek or other discontinuity.
|
|
671
|
-
upmixer.port.postMessage({ type: 'reset' });
|
|
672
|
-
```
|
|
673
|
-
|
|
674
|
-
Both stereo upmix algorithms emit ACN `[W, Y, Z, X]` but are perceptual
|
|
675
|
-
approximations, neither N3D nor SN3D. The BCC path produces W/Y only and keeps
|
|
676
|
-
Z/X at zero, trading the default path's front cue for per-band spatial analysis.
|
|
786
|
+
const sharedWorklet = new AudioWorkletNode(cacophony.context, 'my-fx');
|
|
787
|
+
await bus.addFilter(cacophony.shareEffect(sharedWorklet));
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
FOA binaural decoding is available in both forms. Use
|
|
791
|
+
`createFoaDecoder()` when you want explicit custom wiring through
|
|
792
|
+
`decoder.input` and `decoder.output`; use `createFoaDecoderEffect()` only on a
|
|
793
|
+
dedicated 4-channel ACN/SN3D FOA bus, typically as the first and only filter
|
|
794
|
+
that converts that bus to stereo binaural output.
|
|
795
|
+
|
|
796
|
+
For physically correct mono positioning, use `encodeMonoToFoaSN3D()` and feed
|
|
797
|
+
its ACN/SN3D `[W, Y, Z, X]` output to the decoder. Stereo recordings can instead
|
|
798
|
+
be perceptually upmixed with `createStereoToBFormatNode()`. The default
|
|
799
|
+
`algorithm: 'perceptual'` path preserves the existing three-band front cue;
|
|
800
|
+
`algorithm: 'bcc'` uses frequency-domain coherence and pan analysis:
|
|
801
|
+
|
|
802
|
+
```typescript
|
|
803
|
+
const upmixer = await cacophony.createStereoToBFormatNode({ algorithm: 'bcc' });
|
|
804
|
+
const decoder = await cacophony.createFoaDecoder();
|
|
805
|
+
stereoSource.connect(upmixer);
|
|
806
|
+
upmixer.connect(decoder.input);
|
|
807
|
+
decoder.output.connect(cacophony.context.destination);
|
|
808
|
+
|
|
809
|
+
// BCC analysis is stateful. Reset it after a seek or other discontinuity.
|
|
810
|
+
upmixer.port.postMessage({ type: 'reset' });
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
Both stereo upmix algorithms emit ACN `[W, Y, Z, X]` but are perceptual
|
|
814
|
+
approximations, neither N3D nor SN3D. The BCC path produces W/Y only and keeps
|
|
815
|
+
Z/X at zero, trading the default path's front cue for per-band spatial analysis.
|
|
677
816
|
|
|
678
817
|
### Cleaning up
|
|
679
818
|
|
|
@@ -956,7 +1095,64 @@ stereoSound.stereoPan = 0.5; // -1 (left) to 1 (right)
|
|
|
956
1095
|
stereoSound.play();
|
|
957
1096
|
```
|
|
958
1097
|
|
|
959
|
-
See [TypeDoc](https://cacophony.js.org) for distance models, cone effects, and advanced 3D audio options.
|
|
1098
|
+
See [TypeDoc](https://cacophony.js.org) for distance models, cone effects, and advanced 3D audio options.
|
|
1099
|
+
|
|
1100
|
+
`stereoPan` requires stereo mode; `position` and `threeDOptions` require HRTF mode.
|
|
1101
|
+
These setters validate the source and every voice before changing defaults or live
|
|
1102
|
+
settings. An incompatible mode, invalid value, or cleaned-up voice rejects the
|
|
1103
|
+
whole update. Group spatial setters also validate every member first.
|
|
1104
|
+
|
|
1105
|
+
`threeDOptions` configures an HRTF panner without switching its mode. Select the
|
|
1106
|
+
mode when creating the source or with `play({ panType, ... })`. If per-playback
|
|
1107
|
+
overrides produce mixed stereo/HRTF voices, use each voice's spatial controls.
|
|
1108
|
+
|
|
1109
|
+
`Sound.clone`, `Synth.clone`, and `Playback.clone` validate spatial overrides against
|
|
1110
|
+
the clone's target mode before allocation. Supply `stereoPan` for stereo clones,
|
|
1111
|
+
or `position` and `threeDOptions` for HRTF clones; incompatible overrides throw.
|
|
1112
|
+
|
|
1113
|
+
### Motion smoothing
|
|
1114
|
+
|
|
1115
|
+
Source position, source orientation, stereo pan, and modern listener pose updates can
|
|
1116
|
+
approach their targets exponentially instead of changing in steps. Smoothing is opt-in:
|
|
1117
|
+
the default time constant is `0`, which keeps immediate writes.
|
|
1118
|
+
|
|
1119
|
+
```typescript
|
|
1120
|
+
const audio = new Cacophony(undefined, undefined, { spatialSmoothingTau: 0.03 });
|
|
1121
|
+
const moving = await audio.createSound('bird.mp3', 'buffer', 'HRTF');
|
|
1122
|
+
moving.position = [10, 5, 0];
|
|
1123
|
+
const [voice] = moving.play(); // Starts at this pose immediately
|
|
1124
|
+
|
|
1125
|
+
moving.position = [12, 5, 0]; // Current voices approach the new position
|
|
1126
|
+
moving.spatialSmoothingTau = 0.05; // Applies to current and future voices
|
|
1127
|
+
voice.spatialSmoothingTau = 0.02; // Independent until the next source assignment
|
|
1128
|
+
audio.listenerSmoothingTau = 0.04; // Independent of source defaults
|
|
1129
|
+
audio.listenerPosition = [1, 0, 0];
|
|
1130
|
+
|
|
1131
|
+
moving.spatialSmoothingTau = 0; // Snap pending motion to its target
|
|
1132
|
+
moving.position = [30, 5, 0]; // An immediate teleport
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1135
|
+
Time constants are **seconds**, unlike fade/ramp durations in milliseconds. After
|
|
1136
|
+
one time constant, roughly 63% of the change is complete; after three, roughly 95%.
|
|
1137
|
+
Negative or non-finite constants throw `RangeError`. The same runtime option is
|
|
1138
|
+
available on `createNodeCacophony()` and `createOfflineNodeCacophony()`.
|
|
1139
|
+
|
|
1140
|
+
Sound, Synth, and stream sources inherit the instance default. A source assignment
|
|
1141
|
+
updates its current voices and the default for future voices. Sound, Synth, and
|
|
1142
|
+
Playback clones preserve the setting and requested pose. Initial source and
|
|
1143
|
+
`play(options)` poses are immediate; subsequent changes, including while paused,
|
|
1144
|
+
are smoothed. Stopping a voice snaps pending motion to its target, and a stopped
|
|
1145
|
+
voice takes new poses immediately, so a replayed voice starts where it was asked to.
|
|
1146
|
+
Partial `threeDOptions` updates leave omitted axes alone.
|
|
1147
|
+
|
|
1148
|
+
Spatial getters report the last requested targets, including during transitions.
|
|
1149
|
+
Use these setters consistently: direct automation or mutation of the underlying
|
|
1150
|
+
panner parameters is outside this target-state contract. Orientation components
|
|
1151
|
+
are smoothed independently; this does not provide constant-speed angular rotation.
|
|
1152
|
+
HRTF processing uses control-rate parameters, so smoothing reduces motion artifacts
|
|
1153
|
+
without promising sample-perfect continuity. Legacy listeners using
|
|
1154
|
+
`setPosition`/`setOrientation`, and custom parameters lacking `setTargetAtTime`,
|
|
1155
|
+
continue to update immediately.
|
|
960
1156
|
|
|
961
1157
|
## Microphone Input
|
|
962
1158
|
|
|
@@ -965,22 +1161,22 @@ Capture, process, and manipulate live audio input:
|
|
|
965
1161
|
```typescript
|
|
966
1162
|
const cacophony = new Cacophony();
|
|
967
1163
|
|
|
968
|
-
try {
|
|
969
|
-
const micStream = await cacophony.getMicrophoneStream({
|
|
970
|
-
panType: 'stereo',
|
|
971
|
-
stereoPan: 0,
|
|
972
|
-
constraints: {
|
|
973
|
-
audio: { echoCancellation: true }
|
|
974
|
-
}
|
|
975
|
-
});
|
|
976
|
-
|
|
977
|
-
// Apply filters to microphone input
|
|
978
|
-
const lowPassFilter = cacophony.createBiquadFilter({ type: 'lowpass', frequency: 1000 });
|
|
979
|
-
micStream.addFilter(lowPassFilter);
|
|
980
|
-
|
|
981
|
-
// Control microphone volume
|
|
982
|
-
micStream.volume = 0.8;
|
|
983
|
-
micStream.play();
|
|
1164
|
+
try {
|
|
1165
|
+
const micStream = await cacophony.getMicrophoneStream({
|
|
1166
|
+
panType: 'stereo',
|
|
1167
|
+
stereoPan: 0,
|
|
1168
|
+
constraints: {
|
|
1169
|
+
audio: { echoCancellation: true }
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
// Apply filters to microphone input
|
|
1174
|
+
const lowPassFilter = cacophony.createBiquadFilter({ type: 'lowpass', frequency: 1000 });
|
|
1175
|
+
micStream.addFilter(lowPassFilter);
|
|
1176
|
+
|
|
1177
|
+
// Control microphone volume
|
|
1178
|
+
micStream.volume = 0.8;
|
|
1179
|
+
micStream.play();
|
|
984
1180
|
|
|
985
1181
|
// Pause and resume
|
|
986
1182
|
setTimeout(() => {
|
|
@@ -988,142 +1184,142 @@ try {
|
|
|
988
1184
|
setTimeout(() => micStream.resume(), 2000);
|
|
989
1185
|
}, 5000);
|
|
990
1186
|
|
|
991
|
-
} catch (error) {
|
|
992
|
-
// Permission and device failures reject getMicrophoneStream().
|
|
993
|
-
console.error("Error accessing microphone:", error);
|
|
994
|
-
}
|
|
995
|
-
```
|
|
996
|
-
|
|
997
|
-
Microphone monitoring is routed through the master bus, so global volume,
|
|
998
|
-
mute, master effects, and metering apply. Each playback exposes the standard
|
|
999
|
-
play, pause, stop, and error event surface and is fully disconnected when the
|
|
1000
|
-
microphone is stopped.
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
// Permission and device failures reject getMicrophoneStream().
|
|
1189
|
+
console.error("Error accessing microphone:", error);
|
|
1190
|
+
}
|
|
1191
|
+
```
|
|
1001
1192
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
//
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
`
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
`
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
`
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
`
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
`
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1193
|
+
Microphone monitoring is routed through the master bus, so global volume,
|
|
1194
|
+
mute, master effects, and metering apply. Each playback exposes the standard
|
|
1195
|
+
play, pause, stop, and error event surface and is fully disconnected when the
|
|
1196
|
+
microphone is stopped.
|
|
1197
|
+
|
|
1198
|
+
## Audio Streaming
|
|
1199
|
+
|
|
1200
|
+
`createStream()` incrementally fetches and demuxes MP3, ADTS/MP4 AAC, Ogg,
|
|
1201
|
+
FLAC, and WAVE audio. A WebCodecs `AudioDecoder` produces PCM chunks, which are
|
|
1202
|
+
resampled to the active audio context and written into the same AudioWorklet
|
|
1203
|
+
ring buffer used by `createPcmStreamSound()`. The returned stream is therefore
|
|
1204
|
+
sample-accurately scheduled, pannable, filterable, bus-routable, and meterable.
|
|
1205
|
+
|
|
1206
|
+
The current streaming resampler uses linear interpolation without an
|
|
1207
|
+
anti-aliasing filter. When downsampling, source frequencies above the target
|
|
1208
|
+
Nyquist frequency can alias into the audible band; this is an accepted v1
|
|
1209
|
+
quality trade-off pending a future band-limited resampler.
|
|
1210
|
+
|
|
1211
|
+
```typescript
|
|
1212
|
+
const controller = new AbortController();
|
|
1213
|
+
const stream = await cacophony.createStream('/music.ogg', controller.signal, 'stereo');
|
|
1214
|
+
|
|
1215
|
+
console.log(stream.streamCapabilities);
|
|
1216
|
+
// {
|
|
1217
|
+
// transport: 'webcodecs',
|
|
1218
|
+
// seekable: true,
|
|
1219
|
+
// live: false,
|
|
1220
|
+
// duration: 183.42,
|
|
1221
|
+
// }
|
|
1222
|
+
|
|
1223
|
+
stream.routeTo(musicBus);
|
|
1224
|
+
stream.addFilter(cacophony.createBiquadFilter({ type: 'lowpass', frequency: 1800 }));
|
|
1225
|
+
await cacophony.createLoudnessMeter(musicBus);
|
|
1226
|
+
stream.play();
|
|
1227
|
+
stream.seek(30); // reopens the decoder at 30s when the server supports byte ranges
|
|
1228
|
+
|
|
1229
|
+
controller.abort(); // cancels fetch/decoder work and tears down the PCM worklet
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
`seekable` becomes `true` only after the server answers a byte-range request
|
|
1233
|
+
with a range response. Live sources report `live: true`, `seekable: false`, and
|
|
1234
|
+
`duration: Infinity`; calling `seek()` on one throws a clear error. A finite
|
|
1235
|
+
file whose duration metadata is unavailable also reports `Infinity` rather
|
|
1236
|
+
than inventing a duration.
|
|
1237
|
+
|
|
1238
|
+
When `AudioDecoder` is unavailable, or the primary audio track cannot be
|
|
1239
|
+
decoded by WebCodecs, `createStream()` returns the compatibility
|
|
1240
|
+
media-element tier. Its `streamCapabilities.transport` is `'media-element'`;
|
|
1241
|
+
the browser owns buffering, seeking, and live-stream behavior in that tier.
|
|
1242
|
+
|
|
1243
|
+
### Media-element streaming contract
|
|
1244
|
+
|
|
1245
|
+
`createSound(url, 'streaming')` selects the media-element tier directly.
|
|
1246
|
+
`createStream(url)` returns the same tier when WebCodecs is unavailable or
|
|
1247
|
+
cannot decode the primary track, and always uses it for HLS. After metadata
|
|
1248
|
+
loads, `streamCapabilities` reports the browser's duration and whether it
|
|
1249
|
+
currently exposes a seekable range. A live source has `live: true` and
|
|
1250
|
+
`duration: Infinity`; an ordinary finite resource reports its real duration.
|
|
1251
|
+
The `Sound` itself has `duration === NaN` while unplayed because it has no
|
|
1252
|
+
playback yet. After `preplay()` or `play()`, `sound.duration` and
|
|
1253
|
+
`playback.duration` read the loaded media element and return the finite value
|
|
1254
|
+
or `Infinity` for a live source.
|
|
1255
|
+
|
|
1256
|
+
Media playback moves from unplayed to playing only after the element's
|
|
1257
|
+
`play()` promise resolves. `pause()` moves it to paused, `resume()` calls
|
|
1258
|
+
`play()` again and returns it to playing, and `stop()` moves it to stopped and
|
|
1259
|
+
resets it. Playback events follow those accepted transitions: `play`, `pause`,
|
|
1260
|
+
then both `play` and `resume` on a resume, followed by `stop`. The owning
|
|
1261
|
+
`Sound` emits `play` after the initial accepted play, plus its collective
|
|
1262
|
+
`pause`, `resume`, and `stop` events.
|
|
1263
|
+
|
|
1264
|
+
`seek(time)` writes the media element's `currentTime` only when the browser
|
|
1265
|
+
exposes a seekable range. A live or finite media source without such a range
|
|
1266
|
+
throws a descriptive error and remains at its current position. Infinite
|
|
1267
|
+
looping uses the media element's native `loop`; finite loop counts are managed
|
|
1268
|
+
by Cacophony and emit `loopEnd` between iterations before the final `ended`.
|
|
1269
|
+
|
|
1270
|
+
Every `preplay()` creates an independent media element, so a media-backed
|
|
1271
|
+
`Sound` supports concurrent playback instances. `cleanup()` pauses each
|
|
1272
|
+
element, clears its `src`, calls `load()` to release the resource, disconnects
|
|
1273
|
+
its Web Audio nodes, and removes the playback. Offline `timeStretch()` requires
|
|
1274
|
+
a decoded `AudioBuffer`, so calling `timeStretch()` on this tier throws a
|
|
1275
|
+
predictable buffer-required error.
|
|
1276
|
+
|
|
1277
|
+
For audio that arrives as decoded samples instead of a URL, use the
|
|
1278
|
+
AudioWorklet-backed push source:
|
|
1279
|
+
|
|
1280
|
+
```typescript
|
|
1281
|
+
const pcm = await cacophony.createPcmStreamSound({
|
|
1282
|
+
channelCount: 1,
|
|
1283
|
+
bufferDuration: 1,
|
|
1284
|
+
latency: 0.05,
|
|
1285
|
+
signal: abortController.signal,
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
pcm.on('underrun', () => console.warn('PCM producer fell behind'));
|
|
1289
|
+
pcm.on('drain', () => writeNextChunk());
|
|
1290
|
+
pcm.on('ended', () => console.log('PCM playback finished'));
|
|
1291
|
+
|
|
1292
|
+
pcm.play();
|
|
1293
|
+
const hasCapacity = pcm.write(samples); // interleaved Float32Array at context.sampleRate
|
|
1294
|
+
if (!hasCapacity) {
|
|
1295
|
+
// The accepted write filled the ring buffer, or the chunk was rejected
|
|
1296
|
+
// because it would exceed capacity. Wait for drain before writing again.
|
|
1297
|
+
}
|
|
1298
|
+
console.log(pcm.bufferedDuration);
|
|
1299
|
+
pcm.end(); // no more writes; "ended" fires after buffered PCM is consumed
|
|
1300
|
+
```
|
|
1301
|
+
|
|
1302
|
+
`bufferDuration` fixes the ring-buffer capacity; `latency` sets how much PCM is
|
|
1303
|
+
collected before initial consumption. `write()` returns `false` when the buffer
|
|
1304
|
+
needs producer backpressure. An accepted write that fills the buffer also
|
|
1305
|
+
returns `false`; a chunk that would exceed remaining capacity is rejected and
|
|
1306
|
+
must be retried after `drain`.
|
|
1307
|
+
|
|
1308
|
+
PCM underrun emits silence plus one `underrun` event per underrun episode;
|
|
1309
|
+
writing more samples recovers playback without repeating stale audio.
|
|
1310
|
+
`pause()` stops consuming and `resume()` continues from the buffered frame.
|
|
1311
|
+
Volume, stereo/HRTF pan, filters, buses, and sends use the same public APIs as
|
|
1312
|
+
other sources. Seek is not supported for a push PCM source. Loop is not
|
|
1313
|
+
supported because the source does not retain consumed samples.
|
|
1314
|
+
|
|
1315
|
+
Adaptive HLS/DASH and DRM are outside the WebCodecs pull transport. `.m3u8`
|
|
1316
|
+
URLs use the media tier: native HLS where `HTMLAudioElement.canPlayType()`
|
|
1317
|
+
reports support (notably Safari), then the optional `hls.js` peer in
|
|
1318
|
+
MSE-capable browsers. Install that peer with `npm install hls.js`. DASH and DRM
|
|
1319
|
+
remain outside this integration.
|
|
1320
|
+
|
|
1321
|
+
```typescript
|
|
1322
|
+
const cacophony = new Cacophony();
|
|
1127
1323
|
|
|
1128
1324
|
try {
|
|
1129
1325
|
const streamedSound = await cacophony.createStream('https://example.com/live_radio_stream');
|
|
@@ -1304,18 +1500,18 @@ synth.type = 'square'; // Triggers typeChange event
|
|
|
1304
1500
|
| `pause` | `void` | Fired when playback pauses. |
|
|
1305
1501
|
| `resume` | `void` | Fired when playback resumes after being paused. |
|
|
1306
1502
|
| `ended` | `void` | Fired when playback ends naturally. |
|
|
1307
|
-
| `seek` | `number` | Fired once after a successful `Playback.seek(time)`, with the requested time in seconds. Rejected seeks do not emit. |
|
|
1503
|
+
| `seek` | `number` | Fired once after a successful `Playback.seek(time)`, with the requested time in seconds. Rejected seeks do not emit. |
|
|
1308
1504
|
| `volumeChange` | `number` | Fired when playback volume changes. Receives new volume value. |
|
|
1309
1505
|
| `error` | `PlaybackErrorEvent` | Fired on playback errors. Contains `error`, `errorType`, `timestamp`, `recoverable`. |
|
|
1310
1506
|
|
|
1311
|
-
`seek` is specific to `Playback` (buffer and media-element sources). Subscribe on
|
|
1312
|
-
each playback to observe seeks, including those requested through `Sound.seek()`.
|
|
1313
|
-
`Sound` does not relay a collective `seek`: its concurrent voices can have
|
|
1314
|
-
different positions, and a collective seek can fail after some voices have moved.
|
|
1315
|
-
For media elements, the event reports acceptance of the `currentTime` write;
|
|
1316
|
-
it does not wait for the browser's asynchronous `seeked` event or playback resumption.
|
|
1317
|
-
|
|
1318
|
-
#### Synth Events
|
|
1507
|
+
`seek` is specific to `Playback` (buffer and media-element sources). Subscribe on
|
|
1508
|
+
each playback to observe seeks, including those requested through `Sound.seek()`.
|
|
1509
|
+
`Sound` does not relay a collective `seek`: its concurrent voices can have
|
|
1510
|
+
different positions, and a collective seek can fail after some voices have moved.
|
|
1511
|
+
For media elements, the event reports acceptance of the `currentTime` write;
|
|
1512
|
+
it does not wait for the browser's asynchronous `seeked` event or playback resumption.
|
|
1513
|
+
|
|
1514
|
+
#### Synth Events
|
|
1319
1515
|
|
|
1320
1516
|
| Event | Payload | Description |
|
|
1321
1517
|
|-------|---------|-------------|
|
|
@@ -1336,29 +1532,29 @@ Cacophony implements intelligent three-layer caching for optimal performance:
|
|
|
1336
1532
|
|
|
1337
1533
|
**Memory Cache (LRU)** → **Browser Cache API** → **Network**
|
|
1338
1534
|
|
|
1339
|
-
HTTP reuse decisions are made by `http-cache-semantics` for both decoded memory entries and persistent responses, using the response headers available to the runtime. Fresh responses can be reused; stale responses are conditionally validated when ETag or Last-Modified is available. `no-cache` requires validation, and `no-store` prevents retention in either layer. A successful matching 304 can reuse the decoded buffer while updating its policy.
|
|
1340
|
-
|
|
1341
|
-
If a 304 cannot be matched to the stored representation, Cacophony retries with an unconditional Fetch reload. This includes a 304 that omits the ETag of an ETag-bearing stored response. Compared with the previous cache implementation, such servers can incur an additional request; return matching validators on 304 responses to allow reuse.
|
|
1342
|
-
|
|
1343
|
-
A matching 304 emits `onCacheHit` with `cacheType: "conditional"`. When it reuses an already decoded buffer, it does not emit `onLoadingComplete`; when it decodes retained bytes, it emits both events. This differs from the previous implementation's completion event on every 304. Await `getAudioBuffer()` to observe completion regardless of the cache path.
|
|
1344
|
-
|
|
1345
|
-
The 24-hour default TTL applies only when the response supplies no Cache-Control, Expires, Pragma, or validators. Explicit expiry never falls back to that TTL. Custom TTL values use milliseconds and are rounded down to whole seconds. Persistence uses a versioned response containing both bytes and policy; older cache entries are ignored and fetched again. On first persistent-cache access, Cacophony attempts to delete its legacy `audio-cache` namespace. Cleanup failure is reported through `onCacheError` and does not prevent loading or use of the current cache.
|
|
1346
|
-
|
|
1347
|
-
`AudioCache.setCacheExpirationTime()` affects newly created response policies. Existing decoded and persisted entries keep the lifetime assigned when their policy was created, including across cache instances. Clearing decoded memory does not rewrite persistent policies. Unlike the previous implementation, negative or non-finite TTL values throw `RangeError` and leave the configured TTL unchanged; zero is valid and disables fallback freshness.
|
|
1348
|
-
|
|
1349
|
-
Compatibility change: a response with only `Cache-Control: public` or `private` and no expiry or validators no longer receives the fallback TTL. These directives permit storage but do not specify how long the response is fresh. Such responses can be stored, but subsequent loads go through Fetch again. Cacophony deliberately does not assign heuristic freshness to responses with explicit cache directives. To enable fresh reuse, serve an appropriate `max-age` (for example, `private, max-age=60`) or `Expires`; increasing the fallback TTL does not override those responses.
|
|
1350
|
-
|
|
1351
|
-
Cross-origin audio uses both decoded memory and persistent caching. CORS exposes Cache-Control, Expires, and Last-Modified without extra server configuration. For cross-origin entries requiring validation, Cacophony asks Fetch to revalidate through the browser HTTP cache, avoiding application-added conditional headers and their CORS preflight requirements. Browser HTTP caching remains enabled. A browser-validated response is decoded again because Fetch delivers the resulting representation rather than its internal 304 exchange.
|
|
1352
|
-
|
|
1353
|
-
In browsers, the guarantee is HTTP policy based on browser-visible headers. Cross-origin caching remains enabled when CORS hides headers. A hidden Age can overestimate remaining freshness, and a hidden Vary cannot participate in application-cache matching. Browser validation applies when Fetch runs; a fresh application-cache hit does not consult the browser HTTP cache. This URL-only API explicitly controls Accept; a visible Vary on other, host-managed headers prevents application-cache retention. Data-URL decoding and caching are unchanged.
|
|
1354
|
-
|
|
1355
|
-
For static audio, use versioned or content-hashed URLs such as `/audio/chime.a1b2c3.ogg` with a long `Cache-Control: max-age`. Publish changed audio at a new URL. For mutable audio requiring validation on every load, use `Cache-Control: no-cache`. Expose relevant response headers when freshness or variants depend on them:
|
|
1356
|
-
|
|
1357
|
-
```http
|
|
1358
|
-
Access-Control-Expose-Headers: Age, Date, ETag, Vary
|
|
1359
|
-
```
|
|
1360
|
-
|
|
1361
|
-
This header supplements the server's normal CORS access configuration. Exposing ETag does not cause Cacophony to add conditional request headers to cross-origin requests; Fetch still handles validation.
|
|
1535
|
+
HTTP reuse decisions are made by `http-cache-semantics` for both decoded memory entries and persistent responses, using the response headers available to the runtime. Fresh responses can be reused; stale responses are conditionally validated when ETag or Last-Modified is available. `no-cache` requires validation, and `no-store` prevents retention in either layer. A successful matching 304 can reuse the decoded buffer while updating its policy.
|
|
1536
|
+
|
|
1537
|
+
If a 304 cannot be matched to the stored representation, Cacophony retries with an unconditional Fetch reload. This includes a 304 that omits the ETag of an ETag-bearing stored response. Compared with the previous cache implementation, such servers can incur an additional request; return matching validators on 304 responses to allow reuse.
|
|
1538
|
+
|
|
1539
|
+
A matching 304 emits `onCacheHit` with `cacheType: "conditional"`. When it reuses an already decoded buffer, it does not emit `onLoadingComplete`; when it decodes retained bytes, it emits both events. This differs from the previous implementation's completion event on every 304. Await `getAudioBuffer()` to observe completion regardless of the cache path.
|
|
1540
|
+
|
|
1541
|
+
The 24-hour default TTL applies only when the response supplies no Cache-Control, Expires, Pragma, or validators. Explicit expiry never falls back to that TTL. Custom TTL values use milliseconds and are rounded down to whole seconds. Persistence uses a versioned response containing both bytes and policy; older cache entries are ignored and fetched again. On first persistent-cache access, Cacophony attempts to delete its legacy `audio-cache` namespace. Cleanup failure is reported through `onCacheError` and does not prevent loading or use of the current cache.
|
|
1542
|
+
|
|
1543
|
+
`AudioCache.setCacheExpirationTime()` affects newly created response policies. Existing decoded and persisted entries keep the lifetime assigned when their policy was created, including across cache instances. Clearing decoded memory does not rewrite persistent policies. Unlike the previous implementation, negative or non-finite TTL values throw `RangeError` and leave the configured TTL unchanged; zero is valid and disables fallback freshness.
|
|
1544
|
+
|
|
1545
|
+
Compatibility change: a response with only `Cache-Control: public` or `private` and no expiry or validators no longer receives the fallback TTL. These directives permit storage but do not specify how long the response is fresh. Such responses can be stored, but subsequent loads go through Fetch again. Cacophony deliberately does not assign heuristic freshness to responses with explicit cache directives. To enable fresh reuse, serve an appropriate `max-age` (for example, `private, max-age=60`) or `Expires`; increasing the fallback TTL does not override those responses.
|
|
1546
|
+
|
|
1547
|
+
Cross-origin audio uses both decoded memory and persistent caching. CORS exposes Cache-Control, Expires, and Last-Modified without extra server configuration. For cross-origin entries requiring validation, Cacophony asks Fetch to revalidate through the browser HTTP cache, avoiding application-added conditional headers and their CORS preflight requirements. Browser HTTP caching remains enabled. A browser-validated response is decoded again because Fetch delivers the resulting representation rather than its internal 304 exchange.
|
|
1548
|
+
|
|
1549
|
+
In browsers, the guarantee is HTTP policy based on browser-visible headers. Cross-origin caching remains enabled when CORS hides headers. A hidden Age can overestimate remaining freshness, and a hidden Vary cannot participate in application-cache matching. Browser validation applies when Fetch runs; a fresh application-cache hit does not consult the browser HTTP cache. This URL-only API explicitly controls Accept; a visible Vary on other, host-managed headers prevents application-cache retention. Data-URL decoding and caching are unchanged.
|
|
1550
|
+
|
|
1551
|
+
For static audio, use versioned or content-hashed URLs such as `/audio/chime.a1b2c3.ogg` with a long `Cache-Control: max-age`. Publish changed audio at a new URL. For mutable audio requiring validation on every load, use `Cache-Control: no-cache`. Expose relevant response headers when freshness or variants depend on them:
|
|
1552
|
+
|
|
1553
|
+
```http
|
|
1554
|
+
Access-Control-Expose-Headers: Age, Date, ETag, Vary
|
|
1555
|
+
```
|
|
1556
|
+
|
|
1557
|
+
This header supplements the server's normal CORS access configuration. Exposing ETag does not cause Cacophony to add conditional request headers to cross-origin requests; Fetch still handles validation.
|
|
1362
1558
|
|
|
1363
1559
|
```typescript
|
|
1364
1560
|
const cacophony = new Cacophony();
|
|
@@ -1366,7 +1562,7 @@ const cacophony = new Cacophony();
|
|
|
1366
1562
|
// First load - fetches from network, stores in cache
|
|
1367
1563
|
const sound1 = await cacophony.createSound('audio.mp3');
|
|
1368
1564
|
|
|
1369
|
-
// Second load - from memory while the HTTP policy permits reuse
|
|
1565
|
+
// Second load - from memory while the HTTP policy permits reuse
|
|
1370
1566
|
const sound2 = await cacophony.createSound('audio.mp3');
|
|
1371
1567
|
|
|
1372
1568
|
// Monitor cache performance via events
|
|
@@ -1383,12 +1579,12 @@ cacophony.on('cacheMiss', (event) => {
|
|
|
1383
1579
|
// Clear memory cache (browser cache persists)
|
|
1384
1580
|
cacophony.clearMemoryCache();
|
|
1385
1581
|
|
|
1386
|
-
// Optional: configure TTL for responses without cache directives, expiry, or validators
|
|
1582
|
+
// Optional: configure TTL for responses without cache directives, expiry, or validators
|
|
1387
1583
|
import { AudioCache } from 'cacophony';
|
|
1388
1584
|
AudioCache.setCacheExpirationTime(60 * 60 * 1000); // 1 hour
|
|
1389
1585
|
```
|
|
1390
1586
|
|
|
1391
|
-
Decoded buffers remain bounded to 64 MiB per audio context. Concurrent callers share fetching and decoding; cancelling one caller does not cancel the others. Storage errors are reported through cacheError while a successfully downloaded response can still be delivered.
|
|
1587
|
+
Decoded buffers remain bounded to 64 MiB per audio context. Concurrent callers share fetching and decoding; cancelling one caller does not cancel the others. Storage errors are reported through cacheError while a successfully downloaded response can still be delivered.
|
|
1392
1588
|
|
|
1393
1589
|
## Cancellation with AbortSignal
|
|
1394
1590
|
|