@libraz/libsonare 1.7.2 → 1.8.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/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 +6 -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 +873 -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 +477 -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 +129 -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 +3758 -1362
- 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 +457 -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 +652 -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 +510 -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 +164 -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 +850 -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 +3909 -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 +335 -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 +128 -0
- package/dist/worklet/engine-mixer-facade.d.ts.map +1 -0
- package/dist/worklet/engine-node.d.ts +81 -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 +68 -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 +73 -0
- package/dist/worklet/engine-strips.d.ts.map +1 -0
- package/dist/worklet/engine-sync.d.ts +37 -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 +397 -0
- package/dist/worklet/engine.d.ts.map +1 -0
- package/dist/worklet/guards.d.ts +47 -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 +323 -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 +2763 -501
- package/dist/worklet.js.map +1 -1
- package/package.json +23 -12
- package/src/_effects_common.ts +17 -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 +97 -22
- package/src/effects_note_ops.ts +635 -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 +377 -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 +282 -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 +346 -32
- package/src/mastering_dynamics.ts +22 -11
- package/src/metering.ts +67 -24
- package/src/mixer.ts +212 -3
- 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 +268 -270
- package/src/public_types.ts +122 -3
- package/src/public_types_acoustic.ts +112 -3
- package/src/public_types_mastering.ts +254 -73
- package/src/public_types_mixing.ts +363 -1
- package/src/public_types_music.ts +312 -2
- package/src/public_types_playback.ts +195 -0
- package/src/public_types_realtime.ts +39 -7
- package/src/public_types_repair.ts +446 -0
- package/src/public_types_spectral.ts +487 -1
- package/src/quick_analysis.ts +203 -26
- package/src/realtime_engine.ts +711 -28
- 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 +1122 -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 +194 -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 +444 -10
- package/src/worklet/engine-node.ts +78 -26
- package/src/worklet/engine-offline.ts +14 -8
- package/src/worklet/engine-parameter-facade.ts +21 -0
- package/src/worklet/engine-processor.ts +272 -83
- package/src/worklet/engine-register.ts +32 -18
- package/src/worklet/engine-strips.ts +280 -9
- package/src/worklet/engine-sync.ts +14 -6
- package/src/worklet/engine.ts +365 -31
- package/src/worklet/guards.ts +140 -44
- package/src/worklet/messages.ts +227 -2
- package/src/worklet/mixer-processor.ts +109 -47
- package/src/worklet/playback-processor.ts +300 -0
- package/src/worklet/protocol.ts +53 -3
- 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,
|
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
type SonareWorkletMeterSnapshot,
|
|
20
20
|
type SonareWorkletSpectrumSnapshot,
|
|
21
21
|
spectrumRingFromSharedBuffer,
|
|
22
|
-
toDb,
|
|
23
22
|
} from './protocol';
|
|
24
23
|
|
|
25
24
|
/**
|
|
@@ -43,6 +42,30 @@ export class SonareWorkletProcessor {
|
|
|
43
42
|
private lastSpectrumFrame = 0;
|
|
44
43
|
private transport?: WorkletTransport;
|
|
45
44
|
private meterRing?: SharedMeterRingWriter;
|
|
45
|
+
/**
|
|
46
|
+
* Reused meter record, so a publish writes fields instead of allocating one.
|
|
47
|
+
*
|
|
48
|
+
* `targetId` is always the master and the four LUFS / gain-reduction fields
|
|
49
|
+
* are always unavailable here — the mixer worklet does not run the
|
|
50
|
+
* K-weighting filters, and a floor value would read as silence — so both are
|
|
51
|
+
* set once rather than per interval.
|
|
52
|
+
*/
|
|
53
|
+
private readonly meterScratch: SonareWorkletMeterSnapshot = {
|
|
54
|
+
type: 'meter',
|
|
55
|
+
targetId: 0,
|
|
56
|
+
frame: 0,
|
|
57
|
+
peakDbL: 0,
|
|
58
|
+
peakDbR: 0,
|
|
59
|
+
rmsDbL: 0,
|
|
60
|
+
rmsDbR: 0,
|
|
61
|
+
correlation: 0,
|
|
62
|
+
truePeakDbL: 0,
|
|
63
|
+
truePeakDbR: 0,
|
|
64
|
+
momentaryLufs: Number.NaN,
|
|
65
|
+
shortTermLufs: Number.NaN,
|
|
66
|
+
integratedLufs: Number.NaN,
|
|
67
|
+
gainReductionDb: Number.NaN,
|
|
68
|
+
};
|
|
46
69
|
private spectrumRing?: SharedSpectrumRingWriter;
|
|
47
70
|
private spectrumBands: Float32Array;
|
|
48
71
|
|
|
@@ -52,10 +75,20 @@ export class SonareWorkletProcessor {
|
|
|
52
75
|
}
|
|
53
76
|
this.sampleRate = options.sampleRate ?? 48000;
|
|
54
77
|
this.blockSize = options.blockSize ?? 128;
|
|
55
|
-
|
|
56
|
-
this.
|
|
78
|
+
// Zero is the off switch for both publications, so the floor is 0.
|
|
79
|
+
this.meterIntervalFrames = requireIntegerOption(
|
|
80
|
+
options.meterIntervalFrames,
|
|
81
|
+
2048,
|
|
82
|
+
'meterIntervalFrames',
|
|
83
|
+
0,
|
|
84
|
+
);
|
|
85
|
+
this.spectrumIntervalFrames = requireIntegerOption(
|
|
86
|
+
options.spectrumIntervalFrames,
|
|
87
|
+
0,
|
|
88
|
+
'spectrumIntervalFrames',
|
|
89
|
+
0,
|
|
90
|
+
);
|
|
57
91
|
this.transport = transport;
|
|
58
|
-
this.meterIntervalFrames = Math.max(0, Math.floor(options.meterIntervalFrames ?? 2048));
|
|
59
92
|
this.meterRing = options.meterSharedBuffer
|
|
60
93
|
? meterRingFromSharedBuffer(options.meterSharedBuffer, options.meterRingCapacity)
|
|
61
94
|
: undefined;
|
|
@@ -76,6 +109,10 @@ export class SonareWorkletProcessor {
|
|
|
76
109
|
throw new Error('stripCount must match the scene strip count.');
|
|
77
110
|
}
|
|
78
111
|
this.realtime = this.mixer.createRealtimeBuffer();
|
|
112
|
+
// The mixer meters the master it just produced, so the true-peak filter sees
|
|
113
|
+
// every block without the worklet copying the output back in. 4x is the
|
|
114
|
+
// BS.1770-4 Annex 2 minimum.
|
|
115
|
+
this.mixer.configureMeter(this.meterIntervalFrames > 0, 4);
|
|
79
116
|
}
|
|
80
117
|
|
|
81
118
|
process(inputs: WorkletInput, outputs: WorkletOutput): boolean {
|
|
@@ -132,10 +169,7 @@ export class SonareWorkletProcessor {
|
|
|
132
169
|
}
|
|
133
170
|
}
|
|
134
171
|
this.processedFrames += usable;
|
|
135
|
-
this.publishMeter(
|
|
136
|
-
this.realtime.outLeft.subarray(0, usable),
|
|
137
|
-
this.realtime.outRight.subarray(0, usable),
|
|
138
|
-
);
|
|
172
|
+
this.publishMeter();
|
|
139
173
|
this.publishSpectrum(
|
|
140
174
|
this.realtime.outLeft.subarray(0, usable),
|
|
141
175
|
this.realtime.outRight.subarray(0, usable),
|
|
@@ -152,7 +186,15 @@ export class SonareWorkletProcessor {
|
|
|
152
186
|
return;
|
|
153
187
|
}
|
|
154
188
|
if (message.type === 'setMeterInterval') {
|
|
155
|
-
|
|
189
|
+
const frames = Math.max(0, Math.floor(message.frames));
|
|
190
|
+
// Toggling metering also toggles the mixer's meter, so a disabled meter
|
|
191
|
+
// costs nothing per block. Re-enabling restarts it: the filter history and
|
|
192
|
+
// the published snapshot are from before the gap, and carrying them
|
|
193
|
+
// forward would report a peak the caller never asked to be measured.
|
|
194
|
+
if (frames > 0 !== this.meterIntervalFrames > 0) {
|
|
195
|
+
this.mixer.configureMeter(frames > 0, 4);
|
|
196
|
+
}
|
|
197
|
+
this.meterIntervalFrames = frames;
|
|
156
198
|
return;
|
|
157
199
|
}
|
|
158
200
|
if (message.type === 'scheduleInsertAutomation') {
|
|
@@ -174,8 +216,10 @@ export class SonareWorkletProcessor {
|
|
|
174
216
|
}
|
|
175
217
|
}
|
|
176
218
|
|
|
177
|
-
private publishMeter(
|
|
178
|
-
|
|
219
|
+
private publishMeter(): void {
|
|
220
|
+
// Symmetric with the engine processor: a ring-only configuration still has
|
|
221
|
+
// meters to publish, so the absence of a transport alone must not skip it.
|
|
222
|
+
if ((!this.transport && !this.meterRing) || this.meterIntervalFrames <= 0) {
|
|
179
223
|
return;
|
|
180
224
|
}
|
|
181
225
|
if (this.processedFrames - this.lastMeterFrame < this.meterIntervalFrames) {
|
|
@@ -183,50 +227,64 @@ export class SonareWorkletProcessor {
|
|
|
183
227
|
}
|
|
184
228
|
this.lastMeterFrame = this.processedFrames;
|
|
185
229
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
230
|
+
// Latch the reading into the mixer's own scratch and read the seven fields
|
|
231
|
+
// back as numbers. `meterSnapshot()` returns a fresh embind object, so
|
|
232
|
+
// calling it here would allocate on the render thread once per interval —
|
|
233
|
+
// about 23 per second at the default — no matter what this file then does
|
|
234
|
+
// with the result. Scratch field order, shared with the engine's
|
|
235
|
+
// meterScratchValue: 0 peakDbL, 1 peakDbR, 2 rmsDbL, 3 rmsDbR,
|
|
236
|
+
// 4 correlation, 5 truePeakDbL, 6 truePeakDbR.
|
|
237
|
+
if (!this.mixer.latchMeterSnapshot()) {
|
|
238
|
+
// The meter has never been enabled, so there is no reading to publish.
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (this.meterRing) {
|
|
242
|
+
// Fill the reusable record and serialise it straight into the ring, which
|
|
243
|
+
// exists precisely so this thread allocates and posts nothing. The record
|
|
244
|
+
// never escapes, so reuse is safe here in a way it is not on the
|
|
245
|
+
// postMessage path below.
|
|
246
|
+
const meter = this.meterScratch;
|
|
247
|
+
meter.frame = this.processedFrames;
|
|
248
|
+
meter.peakDbL = this.mixer.meterScratchValue(0);
|
|
249
|
+
meter.peakDbR = this.mixer.meterScratchValue(1);
|
|
250
|
+
meter.rmsDbL = this.mixer.meterScratchValue(2);
|
|
251
|
+
meter.rmsDbR = this.mixer.meterScratchValue(3);
|
|
252
|
+
meter.correlation = this.mixer.meterScratchValue(4);
|
|
253
|
+
meter.truePeakDbL = this.mixer.meterScratchValue(5);
|
|
254
|
+
meter.truePeakDbR = this.mixer.meterScratchValue(6);
|
|
255
|
+
this.writeMeterRing(meter);
|
|
256
|
+
return;
|
|
205
257
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
258
|
+
// No ring: this is the structured-clone fallback, already off the
|
|
259
|
+
// zero-allocation contract, and a listener may retain what it is handed. It
|
|
260
|
+
// still reads the latched scalars rather than `meterSnapshot()`, so the one
|
|
261
|
+
// object it does build is the one it posts.
|
|
209
262
|
const meter: SonareWorkletMeterSnapshot = {
|
|
210
263
|
type: 'meter',
|
|
211
264
|
targetId: 0,
|
|
212
265
|
frame: this.processedFrames,
|
|
213
|
-
peakDbL:
|
|
214
|
-
peakDbR:
|
|
215
|
-
rmsDbL:
|
|
216
|
-
rmsDbR:
|
|
217
|
-
correlation:
|
|
218
|
-
truePeakDbL:
|
|
219
|
-
truePeakDbR:
|
|
266
|
+
peakDbL: this.mixer.meterScratchValue(0),
|
|
267
|
+
peakDbR: this.mixer.meterScratchValue(1),
|
|
268
|
+
rmsDbL: this.mixer.meterScratchValue(2),
|
|
269
|
+
rmsDbR: this.mixer.meterScratchValue(3),
|
|
270
|
+
correlation: this.mixer.meterScratchValue(4),
|
|
271
|
+
truePeakDbL: this.mixer.meterScratchValue(5),
|
|
272
|
+
truePeakDbR: this.mixer.meterScratchValue(6),
|
|
273
|
+
// Declared unavailable rather than floored: the mixer worklet does not run
|
|
274
|
+
// the K-weighting filters, and a floor value would read as silence.
|
|
220
275
|
momentaryLufs: Number.NaN,
|
|
221
276
|
shortTermLufs: Number.NaN,
|
|
222
277
|
integratedLufs: Number.NaN,
|
|
223
278
|
gainReductionDb: Number.NaN,
|
|
224
279
|
};
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
280
|
+
// Alternative channels for one record, not a broadcast pair: a transport
|
|
281
|
+
// that supplies both (the engine registration resolves both to
|
|
282
|
+
// `port.postMessage`) would otherwise deliver every record twice and double
|
|
283
|
+
// the host's peak-hold decay rate.
|
|
284
|
+
if (this.transport?.onMeter) {
|
|
285
|
+
this.transport.onMeter(meter);
|
|
228
286
|
} else {
|
|
229
|
-
this.transport
|
|
287
|
+
this.transport?.postMessage?.(meter);
|
|
230
288
|
}
|
|
231
289
|
}
|
|
232
290
|
|
|
@@ -277,8 +335,12 @@ export class SonareWorkletProcessor {
|
|
|
277
335
|
frame: this.processedFrames,
|
|
278
336
|
bands: new Float32Array(this.spectrumBands),
|
|
279
337
|
};
|
|
280
|
-
|
|
281
|
-
this.transport?.
|
|
338
|
+
// One record, one delivery -- see publishMeter above.
|
|
339
|
+
if (this.transport?.onSpectrum) {
|
|
340
|
+
this.transport.onSpectrum(spectrum);
|
|
341
|
+
} else {
|
|
342
|
+
this.transport?.postMessage?.(spectrum);
|
|
343
|
+
}
|
|
282
344
|
}
|
|
283
345
|
|
|
284
346
|
private computeSpectrum(left: Float32Array, right: Float32Array): void {
|
|
@@ -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
|
+
}
|
package/src/worklet/protocol.ts
CHANGED
|
@@ -25,7 +25,17 @@ export interface SonareWorkletMeterSnapshot {
|
|
|
25
25
|
rmsDbL: number;
|
|
26
26
|
rmsDbR: number;
|
|
27
27
|
correlation: number;
|
|
28
|
+
/**
|
|
29
|
+
* Left-channel inter-sample (true) peak in dB, from the ITU-R BS.1770-4
|
|
30
|
+
* polyphase reconstruction at 4x. A streaming measurement, and the render
|
|
31
|
+
* quantum makes that visible here: the centered reconstruction stencil needs a
|
|
32
|
+
* few future samples a realtime path does not have, so each block's last
|
|
33
|
+
* samples read marginally low (about 0.1 dB across 64..8192-sample blocks on a
|
|
34
|
+
* near-Nyquist tone, always under-reading). Use `meteringTruePeakDb` over the
|
|
35
|
+
* whole signal for an exact dBTP number.
|
|
36
|
+
*/
|
|
28
37
|
truePeakDbL: number;
|
|
38
|
+
/** Right-channel inter-sample (true) peak in dB. See {@link truePeakDbL}. */
|
|
29
39
|
truePeakDbR: number;
|
|
30
40
|
momentaryLufs: number;
|
|
31
41
|
shortTermLufs: number;
|
|
@@ -140,6 +150,7 @@ export enum SonareEngineTelemetryError {
|
|
|
140
150
|
MetronomeOverflow = 18,
|
|
141
151
|
InvalidCommand = 19,
|
|
142
152
|
MaxChannelsExceeded = 20,
|
|
153
|
+
ParameterBaseOverflow = 21,
|
|
143
154
|
}
|
|
144
155
|
|
|
145
156
|
export interface SonareMeterRingBuffer {
|
|
@@ -307,8 +318,24 @@ export interface SharedSpectrumRingWriter {
|
|
|
307
318
|
recordFloats: number;
|
|
308
319
|
}
|
|
309
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Shared finite dB floor (`sonare::constants::kFloorDb`). Every level-in-dB
|
|
323
|
+
* field on a worklet snapshot bottoms out here rather than at `-Infinity`.
|
|
324
|
+
*/
|
|
325
|
+
export const SONARE_FLOOR_DB = -120;
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Magnitude to dB with the shared finite floor.
|
|
329
|
+
*
|
|
330
|
+
* `-Infinity` is not a usable value for a meter reading: it propagates NaN
|
|
331
|
+
* through the usual `(db + 60) / 60 * height` bar geometry, serializes to
|
|
332
|
+
* `null` through `JSON.stringify`, and poisons `Math.min`/`Math.max`
|
|
333
|
+
* aggregation. The native meters have always floored at `kFloorDb`, so a
|
|
334
|
+
* producer emitting `-Infinity` also made the two worklets disagree about
|
|
335
|
+
* silence.
|
|
336
|
+
*/
|
|
310
337
|
export function toDb(value: number): number {
|
|
311
|
-
return value > 0 ? 20 * Math.log10(value) :
|
|
338
|
+
return value > 0 ? Math.max(20 * Math.log10(value), SONARE_FLOOR_DB) : SONARE_FLOOR_DB;
|
|
312
339
|
}
|
|
313
340
|
|
|
314
341
|
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -948,6 +975,27 @@ function recordOffset(index: number, capacity: number, recordBytes: number): num
|
|
|
948
975
|
return (index % capacity) * recordBytes;
|
|
949
976
|
}
|
|
950
977
|
|
|
978
|
+
/**
|
|
979
|
+
* The domain of a uint32 record slot: no fraction, no sign, no overflow.
|
|
980
|
+
*
|
|
981
|
+
* `setUint32` folds anything outside it into a legal value, so a negative id
|
|
982
|
+
* becomes a large one and 2**32 becomes 0. An absent value is out of domain;
|
|
983
|
+
* a slot with a default resolves it before asking.
|
|
984
|
+
*/
|
|
985
|
+
export function isUint32Slot(value: number | undefined): boolean {
|
|
986
|
+
return (
|
|
987
|
+
typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= 0xffff_ffff
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function toUint32Slot(value: number | undefined, fallback: number, name: string): number {
|
|
992
|
+
const resolved = value ?? fallback;
|
|
993
|
+
if (!isUint32Slot(resolved)) {
|
|
994
|
+
throw new RangeError(`${name} must be an integer within [0, 4294967295]`);
|
|
995
|
+
}
|
|
996
|
+
return resolved;
|
|
997
|
+
}
|
|
998
|
+
|
|
951
999
|
function toSafeInteger(value: number | bigint | undefined, fallback: number): number {
|
|
952
1000
|
const resolved = typeof value === 'bigint' ? Number(value) : value;
|
|
953
1001
|
if (resolved === undefined) {
|
|
@@ -976,8 +1024,10 @@ function writeEngineCommandRecord(
|
|
|
976
1024
|
offset: number,
|
|
977
1025
|
command: SonareEngineCommandRecord,
|
|
978
1026
|
): void {
|
|
979
|
-
|
|
980
|
-
|
|
1027
|
+
// Both slots are uint32 and the readers are unsigned, so the writer holds
|
|
1028
|
+
// them to that domain; the command vocabulary itself is the node's check.
|
|
1029
|
+
view.setUint32(offset, toUint32Slot(command.type, 0, 'type'), true);
|
|
1030
|
+
view.setUint32(offset + 4, toUint32Slot(command.targetId, 0, 'targetId'), true);
|
|
981
1031
|
writeInt64Words(view, offset + 8, toSafeInteger(command.sampleTime, -1));
|
|
982
1032
|
// argFloat occupies a full 8-byte Float64 slot (replacing the old Float32 +
|
|
983
1033
|
// 4-byte pad) so PPQ scalars carried here keep full double precision over the
|