@libraz/libsonare 1.7.2 → 1.8.1
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/NOTICE +178 -0
- package/README.md +26 -203
- package/dist/_chain_config.d.ts +14 -0
- package/dist/_chain_config.d.ts.map +1 -0
- package/dist/_effects_common.d.ts +7 -0
- package/dist/_effects_common.d.ts.map +1 -0
- package/dist/_feature_validation.d.ts +8 -0
- package/dist/_feature_validation.d.ts.map +1 -0
- package/dist/_fft_options.d.ts +24 -0
- package/dist/_fft_options.d.ts.map +1 -0
- package/dist/align_take.d.ts +50 -0
- package/dist/align_take.d.ts.map +1 -0
- package/dist/analysis.d.ts +29 -5840
- package/dist/analysis.d.ts.map +1 -0
- package/dist/analysis.js +874 -722
- package/dist/analysis.js.map +1 -1
- package/dist/analysis_helpers.d.ts +9 -0
- package/dist/analysis_helpers.d.ts.map +1 -0
- package/dist/audio.d.ts +163 -0
- package/dist/audio.d.ts.map +1 -0
- package/dist/clip_page_streamer.d.ts +133 -0
- package/dist/clip_page_streamer.d.ts.map +1 -0
- package/dist/codes.d.ts +44 -0
- package/dist/codes.d.ts.map +1 -0
- package/dist/effects_mastering.d.ts +23 -0
- package/dist/effects_mastering.d.ts.map +1 -0
- package/dist/effects_note_ops.d.ts +502 -0
- package/dist/effects_note_ops.d.ts.map +1 -0
- package/dist/effects_percussive.d.ts +185 -0
- package/dist/effects_percussive.d.ts.map +1 -0
- package/dist/effects_separation.d.ts +65 -0
- package/dist/effects_separation.d.ts.map +1 -0
- package/dist/effects_spectral.d.ts +28 -0
- package/dist/effects_spectral.d.ts.map +1 -0
- package/dist/effects_timepitch.d.ts +134 -0
- package/dist/effects_timepitch.d.ts.map +1 -0
- package/dist/effects_voice_change.d.ts +53 -0
- package/dist/effects_voice_change.d.ts.map +1 -0
- package/dist/errors.d.ts +51 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/feature_core.d.ts +341 -0
- package/dist/feature_core.d.ts.map +1 -0
- package/dist/feature_decompose.d.ts +278 -0
- package/dist/feature_decompose.d.ts.map +1 -0
- package/dist/feature_inverse.d.ts +128 -0
- package/dist/feature_inverse.d.ts.map +1 -0
- package/dist/feature_loudness.d.ts +66 -0
- package/dist/feature_loudness.d.ts.map +1 -0
- package/dist/feature_music.d.ts +307 -0
- package/dist/feature_music.d.ts.map +1 -0
- package/dist/feature_pitch.d.ts +108 -0
- package/dist/feature_pitch.d.ts.map +1 -0
- package/dist/feature_resample.d.ts +16 -0
- package/dist/feature_resample.d.ts.map +1 -0
- package/dist/feature_spectral.d.ts +137 -0
- package/dist/feature_spectral.d.ts.map +1 -0
- package/dist/feature_spectrogram.d.ts +198 -0
- package/dist/feature_spectrogram.d.ts.map +1 -0
- package/dist/features.d.ts +10 -0
- package/dist/features.d.ts.map +1 -0
- package/dist/hrtf/default.shrf +0 -0
- package/dist/index.d.ts +74 -7431
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4173 -1621
- package/dist/index.js.map +1 -1
- package/dist/instrument_types.d.ts +517 -0
- package/dist/instrument_types.d.ts.map +1 -0
- package/dist/live_audio.d.ts +35 -0
- package/dist/live_audio.d.ts.map +1 -0
- package/dist/mastering_chain.d.ts +213 -0
- package/dist/mastering_chain.d.ts.map +1 -0
- package/dist/mastering_core.d.ts +483 -0
- package/dist/mastering_core.d.ts.map +1 -0
- package/dist/mastering_dynamics.d.ts +80 -0
- package/dist/mastering_dynamics.d.ts.map +1 -0
- package/dist/metering.d.ts +287 -0
- package/dist/metering.d.ts.map +1 -0
- package/dist/mixer.d.ts +464 -0
- package/dist/mixer.d.ts.map +1 -0
- package/dist/mixing_assistant.d.ts +62 -0
- package/dist/mixing_assistant.d.ts.map +1 -0
- package/dist/mixing_oneshot.d.ts +40 -0
- package/dist/mixing_oneshot.d.ts.map +1 -0
- package/dist/module_state.d.ts +15 -0
- package/dist/module_state.d.ts.map +1 -0
- package/dist/opfs_clip_pages.d.ts +28 -0
- package/dist/opfs_clip_pages.d.ts.map +1 -0
- package/dist/playback_renderer.d.ts +128 -0
- package/dist/playback_renderer.d.ts.map +1 -0
- package/dist/polyphony.d.ts +202 -0
- package/dist/polyphony.d.ts.map +1 -0
- package/dist/project.d.ts +8 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project_class.d.ts +562 -0
- package/dist/project_class.d.ts.map +1 -0
- package/dist/project_internal.d.ts +194 -0
- package/dist/project_internal.d.ts.map +1 -0
- package/dist/project_synth.d.ts +74 -0
- package/dist/project_synth.d.ts.map +1 -0
- package/dist/project_types.d.ts +654 -0
- package/dist/project_types.d.ts.map +1 -0
- package/dist/public_types.d.ts +185 -0
- package/dist/public_types.d.ts.map +1 -0
- package/dist/public_types_acoustic.d.ts +215 -0
- package/dist/public_types_acoustic.d.ts.map +1 -0
- package/dist/public_types_mastering.d.ts +528 -0
- package/dist/public_types_mastering.d.ts.map +1 -0
- package/dist/public_types_mixing.d.ts +436 -0
- package/dist/public_types_mixing.d.ts.map +1 -0
- package/dist/public_types_music.d.ts +619 -0
- package/dist/public_types_music.d.ts.map +1 -0
- package/dist/public_types_playback.d.ts +165 -0
- package/dist/public_types_playback.d.ts.map +1 -0
- package/dist/public_types_realtime.d.ts +174 -0
- package/dist/public_types_realtime.d.ts.map +1 -0
- package/dist/public_types_repair.d.ts +424 -0
- package/dist/public_types_repair.d.ts.map +1 -0
- package/dist/public_types_spectral.d.ts +697 -0
- package/dist/public_types_spectral.d.ts.map +1 -0
- package/dist/quick_analysis.d.ts +445 -0
- package/dist/quick_analysis.d.ts.map +1 -0
- package/dist/realtime_engine.d.ts +893 -0
- package/dist/realtime_engine.d.ts.map +1 -0
- package/dist/realtime_voice_changer.d.ts +158 -0
- package/dist/realtime_voice_changer.d.ts.map +1 -0
- package/dist/repair_dereverb.d.ts +187 -0
- package/dist/repair_dereverb.d.ts.map +1 -0
- package/dist/repair_impulsive.d.ts +186 -0
- package/dist/repair_impulsive.d.ts.map +1 -0
- package/dist/repair_noise.d.ts +239 -0
- package/dist/repair_noise.d.ts.map +1 -0
- package/dist/repair_trim.d.ts +123 -0
- package/dist/repair_trim.d.ts.map +1 -0
- package/dist/sample_bank.d.ts +84 -0
- package/dist/sample_bank.d.ts.map +1 -0
- package/dist/scale.d.ts +10 -0
- package/dist/scale.d.ts.map +1 -0
- package/dist/schemas/mixer-scene.schema.json +393 -0
- package/dist/schemas/playback-renderer-config.schema.json +392 -0
- package/dist/sonare-analysis.d.ts +8 -0
- package/dist/sonare-analysis.js +2 -2
- package/dist/sonare-analysis.wasm +0 -0
- package/dist/sonare.d.ts +3945 -0
- package/dist/sonare.js +2 -2
- package/dist/sonare.wasm +0 -0
- package/dist/stream_analyzer.d.ts +163 -0
- package/dist/stream_analyzer.d.ts.map +1 -0
- package/dist/stream_types.d.ts +214 -0
- package/dist/stream_types.d.ts.map +1 -0
- package/dist/streaming_mixing.d.ts +6 -0
- package/dist/streaming_mixing.d.ts.map +1 -0
- package/dist/streaming_processors.d.ts +340 -0
- package/dist/streaming_processors.d.ts.map +1 -0
- package/dist/transcribe.d.ts +77 -0
- package/dist/transcribe.d.ts.map +1 -0
- package/dist/validation.d.ts +140 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/web_midi.d.ts +77 -0
- package/dist/web_midi.d.ts.map +1 -0
- package/dist/worker.d.ts +5 -48
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +94 -41
- package/dist/worker.js.map +1 -1
- package/dist/worker_client.d.ts +96 -0
- package/dist/worker_client.d.ts.map +1 -0
- package/dist/worker_protocol.d.ts +43 -0
- package/dist/worker_protocol.d.ts.map +1 -0
- package/dist/worklet/audio_types.d.ts +21 -0
- package/dist/worklet/audio_types.d.ts.map +1 -0
- package/dist/worklet/engine-automation.d.ts +29 -0
- package/dist/worklet/engine-automation.d.ts.map +1 -0
- package/dist/worklet/engine-capture-facade.d.ts +35 -0
- package/dist/worklet/engine-capture-facade.d.ts.map +1 -0
- package/dist/worklet/engine-clips.d.ts +23 -0
- package/dist/worklet/engine-clips.d.ts.map +1 -0
- package/dist/worklet/engine-markers.d.ts +40 -0
- package/dist/worklet/engine-markers.d.ts.map +1 -0
- package/dist/worklet/engine-mixer-facade.d.ts +164 -0
- package/dist/worklet/engine-mixer-facade.d.ts.map +1 -0
- package/dist/worklet/engine-node.d.ts +83 -0
- package/dist/worklet/engine-node.d.ts.map +1 -0
- package/dist/worklet/engine-offline.d.ts +81 -0
- package/dist/worklet/engine-offline.d.ts.map +1 -0
- package/dist/worklet/engine-options.d.ts +12 -0
- package/dist/worklet/engine-options.d.ts.map +1 -0
- package/dist/worklet/engine-parameter-facade.d.ts +106 -0
- package/dist/worklet/engine-parameter-facade.d.ts.map +1 -0
- package/dist/worklet/engine-processor.d.ts +72 -0
- package/dist/worklet/engine-processor.d.ts.map +1 -0
- package/dist/worklet/engine-register.d.ts +2 -0
- package/dist/worklet/engine-register.d.ts.map +1 -0
- package/dist/worklet/engine-strips.d.ts +75 -0
- package/dist/worklet/engine-strips.d.ts.map +1 -0
- package/dist/worklet/engine-sync.d.ts +39 -0
- package/dist/worklet/engine-sync.d.ts.map +1 -0
- package/dist/worklet/engine-tempo-facade.d.ts +48 -0
- package/dist/worklet/engine-tempo-facade.d.ts.map +1 -0
- package/dist/worklet/engine.d.ts +418 -0
- package/dist/worklet/engine.d.ts.map +1 -0
- package/dist/worklet/guards.d.ts +53 -0
- package/dist/worklet/guards.d.ts.map +1 -0
- package/dist/worklet/messages.d.ts +710 -0
- package/dist/worklet/messages.d.ts.map +1 -0
- package/dist/worklet/mixer-processor.d.ts +46 -0
- package/dist/worklet/mixer-processor.d.ts.map +1 -0
- package/dist/worklet/playback-processor.d.ts +62 -0
- package/dist/worklet/playback-processor.d.ts.map +1 -0
- package/dist/worklet/protocol.d.ts +331 -0
- package/dist/worklet/protocol.d.ts.map +1 -0
- package/dist/worklet/voice-changer-processor.d.ts +41 -0
- package/dist/worklet/voice-changer-processor.d.ts.map +1 -0
- package/dist/worklet.d.ts +16 -2515
- package/dist/worklet.d.ts.map +1 -0
- package/dist/worklet.js +3200 -541
- package/dist/worklet.js.map +1 -1
- package/package.json +23 -12
- package/src/_effects_common.ts +47 -0
- package/src/_feature_validation.ts +34 -0
- package/src/_fft_options.ts +39 -0
- package/src/align_take.ts +64 -0
- package/src/analysis.ts +56 -3
- package/src/analysis_helpers.ts +7 -0
- package/src/audio.ts +106 -3
- package/src/codes.ts +39 -2
- package/src/effects_mastering.ts +101 -22
- package/src/effects_note_ops.ts +683 -0
- package/src/effects_percussive.ts +217 -0
- package/src/effects_separation.ts +150 -0
- package/src/effects_spectral.ts +60 -0
- package/src/effects_timepitch.ts +388 -0
- package/src/errors.ts +23 -1
- package/src/feature_core.ts +127 -2
- package/src/feature_decompose.ts +633 -0
- package/src/feature_inverse.ts +454 -0
- package/src/feature_loudness.ts +125 -0
- package/src/feature_music.ts +107 -14
- package/src/feature_pitch.ts +96 -1
- package/src/feature_spectral.ts +16 -611
- package/src/feature_spectrogram.ts +63 -450
- package/src/features.ts +36 -22
- package/src/index.ts +288 -30
- package/src/instrument_types.ts +645 -0
- package/src/live_audio.ts +27 -1
- package/src/mastering_chain.ts +184 -0
- package/src/mastering_core.ts +441 -32
- package/src/mastering_dynamics.ts +22 -11
- package/src/metering.ts +67 -24
- package/src/mixer.ts +251 -22
- package/src/mixing_assistant.ts +138 -0
- package/src/mixing_oneshot.ts +10 -5
- package/src/module_state.ts +24 -2
- package/src/playback_renderer.ts +252 -0
- package/src/polyphony.ts +279 -0
- package/src/project.ts +61 -24
- package/src/project_class.ts +450 -27
- package/src/project_internal.ts +149 -42
- package/src/project_synth.ts +67 -1
- package/src/project_types.ts +271 -271
- package/src/public_types.ts +122 -3
- package/src/public_types_acoustic.ts +112 -3
- package/src/public_types_mastering.ts +275 -73
- package/src/public_types_mixing.ts +363 -1
- package/src/public_types_music.ts +312 -2
- package/src/public_types_playback.ts +196 -0
- package/src/public_types_realtime.ts +39 -7
- package/src/public_types_repair.ts +446 -0
- package/src/public_types_spectral.ts +491 -5
- package/src/quick_analysis.ts +203 -26
- package/src/realtime_engine.ts +773 -34
- package/src/realtime_voice_changer.ts +55 -1
- package/src/repair_dereverb.ts +299 -0
- package/src/repair_impulsive.ts +395 -0
- package/src/repair_noise.ts +425 -0
- package/src/repair_trim.ts +226 -0
- package/src/sample_bank.ts +113 -0
- package/src/sonare.js.d.ts +1158 -30
- package/src/stream_analyzer.ts +36 -4
- package/src/stream_types.ts +37 -0
- package/src/streaming_mixing.ts +1 -1
- package/src/streaming_processors.ts +202 -10
- package/src/transcribe.ts +89 -0
- package/src/validation.ts +285 -11
- package/src/web_midi.ts +1 -6
- package/src/worker.ts +18 -2
- package/src/worklet/audio_types.ts +37 -0
- package/src/worklet/engine-mixer-facade.ts +800 -32
- package/src/worklet/engine-node.ts +99 -29
- package/src/worklet/engine-offline.ts +14 -8
- package/src/worklet/engine-parameter-facade.ts +21 -0
- package/src/worklet/engine-processor.ts +332 -93
- package/src/worklet/engine-register.ts +32 -18
- package/src/worklet/engine-strips.ts +275 -9
- package/src/worklet/engine-sync.ts +20 -7
- package/src/worklet/engine.ts +394 -48
- package/src/worklet/guards.ts +195 -44
- package/src/worklet/messages.ts +229 -2
- package/src/worklet/mixer-processor.ts +117 -48
- package/src/worklet/playback-processor.ts +300 -0
- package/src/worklet/protocol.ts +82 -11
- package/src/worklet/voice-changer-processor.ts +17 -11
- package/src/worklet.ts +17 -0
- package/src/effects_transform.ts +0 -718
- package/src/mastering_repair.ts +0 -273
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { MixerRealtimeBuffer } from '../index';
|
|
2
2
|
import { Mixer } from '../index';
|
|
3
3
|
import type { WorkletInput, WorkletOutput } from './audio_types';
|
|
4
|
-
import { isWorkletMessage } from './guards';
|
|
4
|
+
import { isWorkletMessage, requireIntegerOption } from './guards';
|
|
5
5
|
import type {
|
|
6
6
|
SonareWorkletMessage,
|
|
7
7
|
SonareWorkletProcessorOptions,
|
|
@@ -15,11 +15,11 @@ import {
|
|
|
15
15
|
meterRingFromSharedBuffer,
|
|
16
16
|
type SharedMeterRingWriter,
|
|
17
17
|
type SharedSpectrumRingWriter,
|
|
18
|
+
SONARE_FLOOR_DB,
|
|
18
19
|
SONARE_METER_RING_RECORD_FLOATS,
|
|
19
20
|
type SonareWorkletMeterSnapshot,
|
|
20
21
|
type SonareWorkletSpectrumSnapshot,
|
|
21
22
|
spectrumRingFromSharedBuffer,
|
|
22
|
-
toDb,
|
|
23
23
|
} from './protocol';
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -43,6 +43,32 @@ export class SonareWorkletProcessor {
|
|
|
43
43
|
private lastSpectrumFrame = 0;
|
|
44
44
|
private transport?: WorkletTransport;
|
|
45
45
|
private meterRing?: SharedMeterRingWriter;
|
|
46
|
+
/**
|
|
47
|
+
* Reused meter record, so a publish writes fields instead of allocating one.
|
|
48
|
+
*
|
|
49
|
+
* `targetId` is always the master and the four LUFS / gain-reduction fields
|
|
50
|
+
* are always unavailable here — the mixer worklet does not run the
|
|
51
|
+
* K-weighting filters or the engine's pre-trim input tap — so finite floor
|
|
52
|
+
* values are set once rather than per interval.
|
|
53
|
+
*/
|
|
54
|
+
private readonly meterScratch: SonareWorkletMeterSnapshot = {
|
|
55
|
+
type: 'meter',
|
|
56
|
+
targetId: 0,
|
|
57
|
+
frame: 0,
|
|
58
|
+
peakDbL: 0,
|
|
59
|
+
peakDbR: 0,
|
|
60
|
+
rmsDbL: 0,
|
|
61
|
+
rmsDbR: 0,
|
|
62
|
+
correlation: 0,
|
|
63
|
+
truePeakDbL: 0,
|
|
64
|
+
truePeakDbR: 0,
|
|
65
|
+
momentaryLufs: Number.NaN,
|
|
66
|
+
shortTermLufs: Number.NaN,
|
|
67
|
+
integratedLufs: Number.NaN,
|
|
68
|
+
gainReductionDb: Number.NaN,
|
|
69
|
+
inputPeakDbL: SONARE_FLOOR_DB,
|
|
70
|
+
inputPeakDbR: SONARE_FLOOR_DB,
|
|
71
|
+
};
|
|
46
72
|
private spectrumRing?: SharedSpectrumRingWriter;
|
|
47
73
|
private spectrumBands: Float32Array;
|
|
48
74
|
|
|
@@ -52,10 +78,20 @@ export class SonareWorkletProcessor {
|
|
|
52
78
|
}
|
|
53
79
|
this.sampleRate = options.sampleRate ?? 48000;
|
|
54
80
|
this.blockSize = options.blockSize ?? 128;
|
|
55
|
-
|
|
56
|
-
this.
|
|
81
|
+
// Zero is the off switch for both publications, so the floor is 0.
|
|
82
|
+
this.meterIntervalFrames = requireIntegerOption(
|
|
83
|
+
options.meterIntervalFrames,
|
|
84
|
+
2048,
|
|
85
|
+
'meterIntervalFrames',
|
|
86
|
+
0,
|
|
87
|
+
);
|
|
88
|
+
this.spectrumIntervalFrames = requireIntegerOption(
|
|
89
|
+
options.spectrumIntervalFrames,
|
|
90
|
+
0,
|
|
91
|
+
'spectrumIntervalFrames',
|
|
92
|
+
0,
|
|
93
|
+
);
|
|
57
94
|
this.transport = transport;
|
|
58
|
-
this.meterIntervalFrames = Math.max(0, Math.floor(options.meterIntervalFrames ?? 2048));
|
|
59
95
|
this.meterRing = options.meterSharedBuffer
|
|
60
96
|
? meterRingFromSharedBuffer(options.meterSharedBuffer, options.meterRingCapacity)
|
|
61
97
|
: undefined;
|
|
@@ -76,6 +112,10 @@ export class SonareWorkletProcessor {
|
|
|
76
112
|
throw new Error('stripCount must match the scene strip count.');
|
|
77
113
|
}
|
|
78
114
|
this.realtime = this.mixer.createRealtimeBuffer();
|
|
115
|
+
// The mixer meters the master it just produced, so the true-peak filter sees
|
|
116
|
+
// every block without the worklet copying the output back in. 4x is the
|
|
117
|
+
// BS.1770-4 Annex 2 minimum.
|
|
118
|
+
this.mixer.configureMeter(this.meterIntervalFrames > 0, 4);
|
|
79
119
|
}
|
|
80
120
|
|
|
81
121
|
process(inputs: WorkletInput, outputs: WorkletOutput): boolean {
|
|
@@ -132,10 +172,7 @@ export class SonareWorkletProcessor {
|
|
|
132
172
|
}
|
|
133
173
|
}
|
|
134
174
|
this.processedFrames += usable;
|
|
135
|
-
this.publishMeter(
|
|
136
|
-
this.realtime.outLeft.subarray(0, usable),
|
|
137
|
-
this.realtime.outRight.subarray(0, usable),
|
|
138
|
-
);
|
|
175
|
+
this.publishMeter();
|
|
139
176
|
this.publishSpectrum(
|
|
140
177
|
this.realtime.outLeft.subarray(0, usable),
|
|
141
178
|
this.realtime.outRight.subarray(0, usable),
|
|
@@ -152,7 +189,15 @@ export class SonareWorkletProcessor {
|
|
|
152
189
|
return;
|
|
153
190
|
}
|
|
154
191
|
if (message.type === 'setMeterInterval') {
|
|
155
|
-
|
|
192
|
+
const frames = Math.max(0, Math.floor(message.frames));
|
|
193
|
+
// Toggling metering also toggles the mixer's meter, so a disabled meter
|
|
194
|
+
// costs nothing per block. Re-enabling restarts it: the filter history and
|
|
195
|
+
// the published snapshot are from before the gap, and carrying them
|
|
196
|
+
// forward would report a peak the caller never asked to be measured.
|
|
197
|
+
if (frames > 0 !== this.meterIntervalFrames > 0) {
|
|
198
|
+
this.mixer.configureMeter(frames > 0, 4);
|
|
199
|
+
}
|
|
200
|
+
this.meterIntervalFrames = frames;
|
|
156
201
|
return;
|
|
157
202
|
}
|
|
158
203
|
if (message.type === 'scheduleInsertAutomation') {
|
|
@@ -174,8 +219,10 @@ export class SonareWorkletProcessor {
|
|
|
174
219
|
}
|
|
175
220
|
}
|
|
176
221
|
|
|
177
|
-
private publishMeter(
|
|
178
|
-
|
|
222
|
+
private publishMeter(): void {
|
|
223
|
+
// Symmetric with the engine processor: a ring-only configuration still has
|
|
224
|
+
// meters to publish, so the absence of a transport alone must not skip it.
|
|
225
|
+
if ((!this.transport && !this.meterRing) || this.meterIntervalFrames <= 0) {
|
|
179
226
|
return;
|
|
180
227
|
}
|
|
181
228
|
if (this.processedFrames - this.lastMeterFrame < this.meterIntervalFrames) {
|
|
@@ -183,50 +230,66 @@ export class SonareWorkletProcessor {
|
|
|
183
230
|
}
|
|
184
231
|
this.lastMeterFrame = this.processedFrames;
|
|
185
232
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (absL > peakL) {
|
|
197
|
-
peakL = absL;
|
|
198
|
-
}
|
|
199
|
-
if (absR > peakR) {
|
|
200
|
-
peakR = absR;
|
|
201
|
-
}
|
|
202
|
-
sumL += l * l;
|
|
203
|
-
sumR += r * r;
|
|
204
|
-
sumLR += l * r;
|
|
233
|
+
// Latch the reading into the mixer's own scratch and read the seven fields
|
|
234
|
+
// back as numbers. `meterSnapshot()` returns a fresh embind object, so
|
|
235
|
+
// calling it here would allocate on the render thread once per interval —
|
|
236
|
+
// about 23 per second at the default — no matter what this file then does
|
|
237
|
+
// with the result. Scratch field order, shared with the engine's
|
|
238
|
+
// meterScratchValue: 0 peakDbL, 1 peakDbR, 2 rmsDbL, 3 rmsDbR,
|
|
239
|
+
// 4 correlation, 5 truePeakDbL, 6 truePeakDbR.
|
|
240
|
+
if (!this.mixer.latchMeterSnapshot()) {
|
|
241
|
+
// The meter has never been enabled, so there is no reading to publish.
|
|
242
|
+
return;
|
|
205
243
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
244
|
+
if (this.meterRing) {
|
|
245
|
+
// Fill the reusable record and serialise it straight into the ring, which
|
|
246
|
+
// exists precisely so this thread allocates and posts nothing. The record
|
|
247
|
+
// never escapes, so reuse is safe here in a way it is not on the
|
|
248
|
+
// postMessage path below.
|
|
249
|
+
const meter = this.meterScratch;
|
|
250
|
+
meter.frame = this.processedFrames;
|
|
251
|
+
meter.peakDbL = this.mixer.meterScratchValue(0);
|
|
252
|
+
meter.peakDbR = this.mixer.meterScratchValue(1);
|
|
253
|
+
meter.rmsDbL = this.mixer.meterScratchValue(2);
|
|
254
|
+
meter.rmsDbR = this.mixer.meterScratchValue(3);
|
|
255
|
+
meter.correlation = this.mixer.meterScratchValue(4);
|
|
256
|
+
meter.truePeakDbL = this.mixer.meterScratchValue(5);
|
|
257
|
+
meter.truePeakDbR = this.mixer.meterScratchValue(6);
|
|
258
|
+
this.writeMeterRing(meter);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
// No ring: this is the structured-clone fallback, already off the
|
|
262
|
+
// zero-allocation contract, and a listener may retain what it is handed. It
|
|
263
|
+
// still reads the latched scalars rather than `meterSnapshot()`, so the one
|
|
264
|
+
// object it does build is the one it posts.
|
|
209
265
|
const meter: SonareWorkletMeterSnapshot = {
|
|
210
266
|
type: 'meter',
|
|
211
267
|
targetId: 0,
|
|
212
268
|
frame: this.processedFrames,
|
|
213
|
-
peakDbL:
|
|
214
|
-
peakDbR:
|
|
215
|
-
rmsDbL:
|
|
216
|
-
rmsDbR:
|
|
217
|
-
correlation:
|
|
218
|
-
truePeakDbL:
|
|
219
|
-
truePeakDbR:
|
|
269
|
+
peakDbL: this.mixer.meterScratchValue(0),
|
|
270
|
+
peakDbR: this.mixer.meterScratchValue(1),
|
|
271
|
+
rmsDbL: this.mixer.meterScratchValue(2),
|
|
272
|
+
rmsDbR: this.mixer.meterScratchValue(3),
|
|
273
|
+
correlation: this.mixer.meterScratchValue(4),
|
|
274
|
+
truePeakDbL: this.mixer.meterScratchValue(5),
|
|
275
|
+
truePeakDbR: this.mixer.meterScratchValue(6),
|
|
276
|
+
// Declared unavailable rather than floored: the mixer worklet does not run
|
|
277
|
+
// the K-weighting filters, and a floor value would read as silence.
|
|
220
278
|
momentaryLufs: Number.NaN,
|
|
221
279
|
shortTermLufs: Number.NaN,
|
|
222
280
|
integratedLufs: Number.NaN,
|
|
223
281
|
gainReductionDb: Number.NaN,
|
|
282
|
+
inputPeakDbL: SONARE_FLOOR_DB,
|
|
283
|
+
inputPeakDbR: SONARE_FLOOR_DB,
|
|
224
284
|
};
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
285
|
+
// Alternative channels for one record, not a broadcast pair: a transport
|
|
286
|
+
// that supplies both (the engine registration resolves both to
|
|
287
|
+
// `port.postMessage`) would otherwise deliver every record twice and double
|
|
288
|
+
// the host's peak-hold decay rate.
|
|
289
|
+
if (this.transport?.onMeter) {
|
|
290
|
+
this.transport.onMeter(meter);
|
|
228
291
|
} else {
|
|
229
|
-
this.transport
|
|
292
|
+
this.transport?.postMessage?.(meter);
|
|
230
293
|
}
|
|
231
294
|
}
|
|
232
295
|
|
|
@@ -251,6 +314,8 @@ export class SonareWorkletProcessor {
|
|
|
251
314
|
ring.records[offset + 11] = meter.shortTermLufs;
|
|
252
315
|
ring.records[offset + 12] = meter.integratedLufs;
|
|
253
316
|
ring.records[offset + 13] = meter.gainReductionDb;
|
|
317
|
+
ring.records[offset + 14] = meter.inputPeakDbL;
|
|
318
|
+
ring.records[offset + 15] = meter.inputPeakDbR;
|
|
254
319
|
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
255
320
|
// writeIndex is a free-running monotonic counter, so an overflow guard here
|
|
256
321
|
// would fire on essentially every write past the first `capacity` records
|
|
@@ -277,8 +342,12 @@ export class SonareWorkletProcessor {
|
|
|
277
342
|
frame: this.processedFrames,
|
|
278
343
|
bands: new Float32Array(this.spectrumBands),
|
|
279
344
|
};
|
|
280
|
-
|
|
281
|
-
this.transport?.
|
|
345
|
+
// One record, one delivery -- see publishMeter above.
|
|
346
|
+
if (this.transport?.onSpectrum) {
|
|
347
|
+
this.transport.onSpectrum(spectrum);
|
|
348
|
+
} else {
|
|
349
|
+
this.transport?.postMessage?.(spectrum);
|
|
350
|
+
}
|
|
282
351
|
}
|
|
283
352
|
|
|
284
353
|
private computeSpectrum(left: Float32Array, right: Float32Array): void {
|
|
@@ -315,7 +384,7 @@ export class SonareWorkletProcessor {
|
|
|
315
384
|
return;
|
|
316
385
|
}
|
|
317
386
|
const writeIndex = Atomics.load(ring.header, 0);
|
|
318
|
-
const offset = (writeIndex % ring.capacity) *
|
|
387
|
+
const offset = (writeIndex % ring.capacity) * SONARE_METER_RING_RECORD_FLOATS;
|
|
319
388
|
ring.records[offset] = encodeFrameLo(frame);
|
|
320
389
|
ring.records[offset + 1] = encodeFrameHi(frame);
|
|
321
390
|
ring.records[offset + 2] = bands.length;
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { HrtfSet, PlaybackRenderer } from '../index';
|
|
2
|
+
import type { PlaybackRendererConfig } from '../public_types_playback';
|
|
3
|
+
import type { WasmPlaybackRenderer } from '../sonare.js';
|
|
4
|
+
import { copyPlanesToOutput, type WorkletInput, type WorkletOutput } from './audio_types';
|
|
5
|
+
import { isPlaybackMessage, requireIntegerOption } from './guards';
|
|
6
|
+
import type {
|
|
7
|
+
SonarePlaybackDiagnosticsReplyMessage,
|
|
8
|
+
SonarePlaybackErrorMessage,
|
|
9
|
+
SonarePlaybackMessage,
|
|
10
|
+
SonarePlaybackNodeOptions,
|
|
11
|
+
SonarePlaybackWorkletProcessorOptions,
|
|
12
|
+
WorkletPort,
|
|
13
|
+
} from './messages';
|
|
14
|
+
|
|
15
|
+
/** Largest input channel count any layout accepts (7.1). */
|
|
16
|
+
const MAX_INPUT_CHANNELS = 8;
|
|
17
|
+
const SPEAKER_LAYOUT_CHANNELS: Readonly<Record<string, number>> = { stereo: 2, '5.1': 6, '7.1': 8 };
|
|
18
|
+
|
|
19
|
+
function configObject(config: PlaybackRendererConfig | string | undefined): PlaybackRendererConfig {
|
|
20
|
+
if (config === undefined) {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
return typeof config === 'string' ? (JSON.parse(config) as PlaybackRendererConfig) : config;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The target's channel count, known before any renderer exists (headphones: 2). */
|
|
27
|
+
function outputChannelCount(config: PlaybackRendererConfig): number {
|
|
28
|
+
if (config.target?.kind !== 'speakers') {
|
|
29
|
+
return 2;
|
|
30
|
+
}
|
|
31
|
+
const count = SPEAKER_LAYOUT_CHANNELS[config.target.layout ?? ''];
|
|
32
|
+
if (count === undefined) {
|
|
33
|
+
throw new RangeError('target.layout must be "stereo", "5.1" or "7.1" for a speakers target');
|
|
34
|
+
}
|
|
35
|
+
return count;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The playback renderer inside an AudioWorklet.
|
|
40
|
+
*
|
|
41
|
+
* `inputs[0].length` is the block's input channel count, so a
|
|
42
|
+
* `MediaElementAudioSourceNode` whose channel count follows the media drives
|
|
43
|
+
* `input.layout: "auto"` directly. A block with no input channels (no active
|
|
44
|
+
* connection: paused, ended, loading) renders silence on the active layout. A
|
|
45
|
+
* block whose channel count the renderer refuses renders silence the same way
|
|
46
|
+
* and counts toward `unsupported_input_blocks`; either way the renderer
|
|
47
|
+
* advances by the block, so the timeline never shifts. Nothing throws on the
|
|
48
|
+
* audio thread.
|
|
49
|
+
*
|
|
50
|
+
* Every buffer is allocated in the constructor: `process()` copies into WASM
|
|
51
|
+
* heap planes and allocates nothing.
|
|
52
|
+
*/
|
|
53
|
+
export class SonarePlaybackWorkletProcessor {
|
|
54
|
+
private static warnedBlockOverflow = false;
|
|
55
|
+
private readonly renderer: PlaybackRenderer;
|
|
56
|
+
private readonly native: WasmPlaybackRenderer;
|
|
57
|
+
private readonly port?: WorkletPort;
|
|
58
|
+
private readonly maxBlockSize: number;
|
|
59
|
+
private readonly outputChannels: number;
|
|
60
|
+
private inputPlanes: Float32Array[] = [];
|
|
61
|
+
private outputPlanes: Float32Array[] = [];
|
|
62
|
+
private unsupportedInputBlocks = 0;
|
|
63
|
+
private destroyed = false;
|
|
64
|
+
|
|
65
|
+
constructor(options: SonarePlaybackWorkletProcessorOptions = {}, port?: WorkletPort) {
|
|
66
|
+
this.port = port;
|
|
67
|
+
this.maxBlockSize = requireIntegerOption(options.maxBlockSize, 128, 'maxBlockSize', 1);
|
|
68
|
+
const scopeRate = (globalThis as { sampleRate?: unknown }).sampleRate;
|
|
69
|
+
const sampleRate = options.sampleRate ?? (typeof scopeRate === 'number' ? scopeRate : 48000);
|
|
70
|
+
const hrtf = options.hrtf ? HrtfSet.fromBytes(new Uint8Array(options.hrtf)) : undefined;
|
|
71
|
+
try {
|
|
72
|
+
this.renderer = new PlaybackRenderer({
|
|
73
|
+
config: options.config ?? {},
|
|
74
|
+
hrtf,
|
|
75
|
+
sampleRate,
|
|
76
|
+
maxBlockSize: this.maxBlockSize,
|
|
77
|
+
});
|
|
78
|
+
} finally {
|
|
79
|
+
// The renderer keeps its own copy of the set.
|
|
80
|
+
hrtf?.delete();
|
|
81
|
+
}
|
|
82
|
+
// One reviewed read of the facade's private handle, for the heap-plane path.
|
|
83
|
+
this.native = (this.renderer as unknown as { native: WasmPlaybackRenderer }).native;
|
|
84
|
+
this.outputChannels = this.native.outputChannels();
|
|
85
|
+
this.acquirePlanes();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Handles a control-plane message. AudioWorklet port handlers run on the
|
|
90
|
+
* rendering thread between `process()` calls, which is what makes `reset`
|
|
91
|
+
* safe here. A refused message is answered with an `error` message.
|
|
92
|
+
*/
|
|
93
|
+
receiveMessage(message: SonarePlaybackMessage): void {
|
|
94
|
+
if (this.destroyed) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
switch (message.type) {
|
|
99
|
+
case 'config':
|
|
100
|
+
this.renderer.setConfig(message.config);
|
|
101
|
+
break;
|
|
102
|
+
case 'orientation':
|
|
103
|
+
this.renderer.setHeadOrientation(message.yaw, message.pitch ?? 0, message.roll ?? 0);
|
|
104
|
+
break;
|
|
105
|
+
case 'reset':
|
|
106
|
+
this.renderer.reset();
|
|
107
|
+
break;
|
|
108
|
+
case 'diagnostics':
|
|
109
|
+
this.port?.postMessage?.({
|
|
110
|
+
type: 'diagnostics',
|
|
111
|
+
diagnostics: this.diagnostics(),
|
|
112
|
+
} satisfies SonarePlaybackDiagnosticsReplyMessage);
|
|
113
|
+
break;
|
|
114
|
+
case 'destroy':
|
|
115
|
+
this.destroy();
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
this.port?.postMessage?.({
|
|
120
|
+
type: 'error',
|
|
121
|
+
request: message.type,
|
|
122
|
+
message: error instanceof Error ? error.message : String(error),
|
|
123
|
+
} satisfies SonarePlaybackErrorMessage);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The renderer's diagnostics plus `unsupported_input_blocks`, as a plain object. */
|
|
128
|
+
diagnostics(): SonarePlaybackDiagnosticsReplyMessage['diagnostics'] {
|
|
129
|
+
return {
|
|
130
|
+
...this.renderer.diagnostics(),
|
|
131
|
+
unsupported_input_blocks: this.unsupportedInputBlocks,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
process(inputs: WorkletInput, outputs: WorkletOutput): boolean {
|
|
136
|
+
if (this.destroyed) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
const output = outputs[0];
|
|
140
|
+
const requested = output?.[0]?.length ?? 0;
|
|
141
|
+
if (!output || requested === 0) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
const frames = this.clampFrames(requested);
|
|
145
|
+
// Heap views detach when WASM linear memory grows; the storage behind them
|
|
146
|
+
// never moves, so re-acquiring is allocation-free on the native side.
|
|
147
|
+
if (this.inputPlanes[0]?.byteLength === 0 || this.outputPlanes[0]?.byteLength === 0) {
|
|
148
|
+
this.acquirePlanes();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const input = inputs[0];
|
|
152
|
+
const inChannels = input?.length ?? 0;
|
|
153
|
+
let code: number;
|
|
154
|
+
if (inChannels === 0) {
|
|
155
|
+
code = this.native.processPreparedSilence(frames);
|
|
156
|
+
} else if (inChannels > MAX_INPUT_CHANNELS) {
|
|
157
|
+
this.unsupportedInputBlocks++;
|
|
158
|
+
code = this.native.processPreparedSilence(frames);
|
|
159
|
+
} else {
|
|
160
|
+
for (let ch = 0; ch < inChannels; ch++) {
|
|
161
|
+
const source = input[ch];
|
|
162
|
+
const plane = this.inputPlanes[ch];
|
|
163
|
+
const copied = Math.min(frames, source.length);
|
|
164
|
+
plane.set(copied === source.length ? source : source.subarray(0, copied));
|
|
165
|
+
if (copied < frames) {
|
|
166
|
+
plane.fill(0, copied, frames);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
code = this.native.processPrepared(inChannels, frames);
|
|
170
|
+
if (code !== 0) {
|
|
171
|
+
// A refused block leaves the renderer untouched; render silence instead.
|
|
172
|
+
this.unsupportedInputBlocks++;
|
|
173
|
+
code = this.native.processPreparedSilence(frames);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (code === 0) {
|
|
177
|
+
copyPlanesToOutput(output, this.outputPlanes, frames);
|
|
178
|
+
} else {
|
|
179
|
+
for (const channel of output) {
|
|
180
|
+
channel.fill(0);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
destroy(): void {
|
|
187
|
+
if (this.destroyed) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
this.destroyed = true;
|
|
191
|
+
this.renderer.delete();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private acquirePlanes(): void {
|
|
195
|
+
this.inputPlanes = [];
|
|
196
|
+
for (let ch = 0; ch < MAX_INPUT_CHANNELS; ch++) {
|
|
197
|
+
this.inputPlanes.push(this.native.inputPlane(ch));
|
|
198
|
+
}
|
|
199
|
+
this.outputPlanes = [];
|
|
200
|
+
for (let ch = 0; ch < this.outputChannels; ch++) {
|
|
201
|
+
this.outputPlanes.push(this.native.outputPlane(ch));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Frames past the construction-time capacity are left silent rather than
|
|
206
|
+
// reallocating on the audio thread.
|
|
207
|
+
private clampFrames(frames: number): number {
|
|
208
|
+
if (frames <= this.maxBlockSize) {
|
|
209
|
+
return frames;
|
|
210
|
+
}
|
|
211
|
+
if (!SonarePlaybackWorkletProcessor.warnedBlockOverflow) {
|
|
212
|
+
SonarePlaybackWorkletProcessor.warnedBlockOverflow = true;
|
|
213
|
+
// biome-ignore lint/suspicious/noConsole: realtime-safety diagnostic.
|
|
214
|
+
console.warn(
|
|
215
|
+
`SonarePlaybackWorkletProcessor: requested ${frames} frames exceeds maxBlockSize ` +
|
|
216
|
+
`${this.maxBlockSize}; clamping.`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return this.maxBlockSize;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function registerSonarePlaybackWorkletProcessor(name = 'sonare-playback-processor'): void {
|
|
224
|
+
const scope = globalThis as unknown as {
|
|
225
|
+
AudioWorkletProcessor?: new () => object;
|
|
226
|
+
registerProcessor?: (processorName: string, processorCtor: unknown) => void;
|
|
227
|
+
};
|
|
228
|
+
if (!scope.AudioWorkletProcessor || !scope.registerProcessor) {
|
|
229
|
+
throw new Error('AudioWorkletProcessor is not available in this context.');
|
|
230
|
+
}
|
|
231
|
+
const Base = scope.AudioWorkletProcessor;
|
|
232
|
+
class RegisteredSonarePlaybackWorkletProcessor extends Base {
|
|
233
|
+
private bridge: SonarePlaybackWorkletProcessor;
|
|
234
|
+
readonly port?: WorkletPort;
|
|
235
|
+
|
|
236
|
+
constructor(options?: { processorOptions?: SonarePlaybackWorkletProcessorOptions }) {
|
|
237
|
+
super();
|
|
238
|
+
const port = this.port;
|
|
239
|
+
this.bridge = new SonarePlaybackWorkletProcessor(options?.processorOptions ?? {}, port);
|
|
240
|
+
const onMessage = (event: { data: unknown }) => {
|
|
241
|
+
if (isPlaybackMessage(event.data)) {
|
|
242
|
+
this.bridge.receiveMessage(event.data);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
if (port?.addEventListener) {
|
|
246
|
+
port.addEventListener('message', onMessage);
|
|
247
|
+
port.start?.();
|
|
248
|
+
} else if (port) {
|
|
249
|
+
port.onmessage = onMessage;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
process(inputs: WorkletInput, outputs: WorkletOutput): boolean {
|
|
254
|
+
return this.bridge.process(inputs, outputs);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
scope.registerProcessor(name, RegisteredSonarePlaybackWorkletProcessor);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Creates the AudioWorkletNode for a processor registered with
|
|
262
|
+
* {@link registerSonarePlaybackWorkletProcessor}. The node's input follows its
|
|
263
|
+
* source's channel count without any browser up/down mix
|
|
264
|
+
* (`channelCountMode: "max"`, `channelInterpretation: "discrete"`), and its
|
|
265
|
+
* output carries the target's channel count. The configuration is serialized
|
|
266
|
+
* here, on the main thread.
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```ts
|
|
270
|
+
* await context.audioWorklet.addModule(playbackWorkletUrl);
|
|
271
|
+
* const hrtf = await (await fetch(hrtfUrl)).arrayBuffer();
|
|
272
|
+
* const node = createSonarePlaybackNode(context, { config: {}, hrtf });
|
|
273
|
+
* context.createMediaElementSource(video).connect(node).connect(context.destination);
|
|
274
|
+
* video.addEventListener('seeking', () => node.port.postMessage({ type: 'reset' }));
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
export function createSonarePlaybackNode(
|
|
278
|
+
context: BaseAudioContext,
|
|
279
|
+
options: SonarePlaybackNodeOptions = {},
|
|
280
|
+
): AudioWorkletNode {
|
|
281
|
+
const config = configObject(options.config);
|
|
282
|
+
const factory =
|
|
283
|
+
options.nodeFactory ??
|
|
284
|
+
((ctx: BaseAudioContext, name: string, nodeOptions: AudioWorkletNodeOptions) =>
|
|
285
|
+
new AudioWorkletNode(ctx, name, nodeOptions));
|
|
286
|
+
const processorOptions: SonarePlaybackWorkletProcessorOptions = {
|
|
287
|
+
config: JSON.stringify(config),
|
|
288
|
+
hrtf: options.hrtf,
|
|
289
|
+
maxBlockSize: options.maxBlockSize,
|
|
290
|
+
sampleRate: options.sampleRate ?? context.sampleRate,
|
|
291
|
+
};
|
|
292
|
+
return factory(context, options.processorName ?? 'sonare-playback-processor', {
|
|
293
|
+
numberOfInputs: 1,
|
|
294
|
+
numberOfOutputs: 1,
|
|
295
|
+
outputChannelCount: [outputChannelCount(config)],
|
|
296
|
+
channelCountMode: 'max',
|
|
297
|
+
channelInterpretation: 'discrete',
|
|
298
|
+
processorOptions,
|
|
299
|
+
});
|
|
300
|
+
}
|