@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
package/src/metering.ts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { ErrorCode, SonareError } from './errors';
|
|
2
2
|
import { getSonareModule } from './module_state';
|
|
3
3
|
import type { ValidateOptions } from './validation';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
assertInterleavedSamples,
|
|
6
|
+
assertNonNegativeInteger,
|
|
7
|
+
assertPositiveInteger,
|
|
8
|
+
assertSamples,
|
|
9
|
+
assertSamplesInWindow,
|
|
10
|
+
} from './validation';
|
|
11
|
+
|
|
12
|
+
// The FFT size the library falls back to when `nFft` is 0 or omitted. Mirrored
|
|
13
|
+
// here so the windowed pre-scan covers exactly the span the call will read; a
|
|
14
|
+
// test pins it against the `nFft` the library reports back for a 0 request.
|
|
15
|
+
const DEFAULT_SPECTRUM_N_FFT = 2048;
|
|
5
16
|
|
|
6
17
|
/**
|
|
7
18
|
* Validates a true-peak oversample factor: `0` (meaning "use the default 4") or
|
|
@@ -267,9 +278,7 @@ export function meteringDetectClipping(
|
|
|
267
278
|
const request = samples instanceof Float32Array ? { samples, sampleRate, ...options } : samples;
|
|
268
279
|
assertSamples('meteringDetectClipping', request.samples, request.validate !== false);
|
|
269
280
|
const minRegionSamples = request.minRegionSamples ?? 1;
|
|
270
|
-
|
|
271
|
-
throw new RangeError('meteringDetectClipping: minRegionSamples must be a non-negative integer');
|
|
272
|
-
}
|
|
281
|
+
assertNonNegativeInteger('meteringDetectClipping', minRegionSamples, 'minRegionSamples');
|
|
273
282
|
return requireModule().meteringDetectClipping(
|
|
274
283
|
request.samples,
|
|
275
284
|
request.sampleRate ?? 22050,
|
|
@@ -447,10 +456,14 @@ export function meteringStereoCorrelation(
|
|
|
447
456
|
}
|
|
448
457
|
|
|
449
458
|
/**
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
459
|
+
* Stereo width as `sqrt(side_energy / mid_energy)` in `[0, +Infinity)`: the
|
|
460
|
+
* side/mid RMS *amplitude* ratio, not the energy ratio. 0 = pure mono, ~1 =
|
|
461
|
+
* wide stereo, larger = increasingly decorrelated / out-of-phase. The value is
|
|
462
|
+
* unbounded and returns `Infinity` when the mid channel is silent (a
|
|
463
|
+
* mono-collapsed / fully out-of-phase signal).
|
|
464
|
+
*
|
|
465
|
+
* Convert to dB with `20 * Math.log10(value)`; `10 * Math.log10` would
|
|
466
|
+
* understate the true energy ratio by half.
|
|
454
467
|
*/
|
|
455
468
|
export function meteringStereoWidth(request: MeteringStereoRequest): number;
|
|
456
469
|
export function meteringStereoWidth(
|
|
@@ -651,6 +664,12 @@ export function meteringSpectrum(
|
|
|
651
664
|
* `nFft`-length FFT), for spectrum-analyzer "moment" snapshots that must not be
|
|
652
665
|
* time-averaged like {@link meteringSpectrum}. The analysis frame spans
|
|
653
666
|
* `[frameOffset, frameOffset + nFft)`; samples past the end are zero-padded.
|
|
667
|
+
*
|
|
668
|
+
* The frame is also the only span validated: a non-finite sample inside it is
|
|
669
|
+
* rejected, while one outside it neither reaches the FFT nor refuses the call.
|
|
670
|
+
* The emptiness and `sampleRate` checks still cover the whole buffer. Cost per
|
|
671
|
+
* call is therefore set by `nFft` rather than by the length of the buffer, so an
|
|
672
|
+
* analyzer may poll a long recording frame by frame.
|
|
654
673
|
*/
|
|
655
674
|
export function meteringSpectrumFrame(request: MeteringSpectrumFrameRequest): SpectrumReport;
|
|
656
675
|
export function meteringSpectrumFrame(
|
|
@@ -667,7 +686,14 @@ export function meteringSpectrumFrame(
|
|
|
667
686
|
): SpectrumReport {
|
|
668
687
|
const request =
|
|
669
688
|
samples instanceof Float32Array ? { samples, sampleRate, frameOffset, ...options } : samples;
|
|
670
|
-
|
|
689
|
+
const nFft = request.nFft ?? 0;
|
|
690
|
+
assertSamplesInWindow(
|
|
691
|
+
'meteringSpectrumFrame',
|
|
692
|
+
request.samples,
|
|
693
|
+
request.validate !== false,
|
|
694
|
+
request.frameOffset ?? 0,
|
|
695
|
+
nFft > 0 ? nFft : DEFAULT_SPECTRUM_N_FFT,
|
|
696
|
+
);
|
|
671
697
|
return requireModule().meteringSpectrumFrame(
|
|
672
698
|
request.samples,
|
|
673
699
|
request.sampleRate ?? 22050,
|
|
@@ -676,7 +702,14 @@ export function meteringSpectrumFrame(
|
|
|
676
702
|
);
|
|
677
703
|
}
|
|
678
704
|
|
|
679
|
-
/**
|
|
705
|
+
/**
|
|
706
|
+
* Compute per-channel min/max waveform buckets from interleaved audio.
|
|
707
|
+
*
|
|
708
|
+
* A non-finite sample is rejected rather than skipped, and `{ validate: false }`
|
|
709
|
+
* does not change that — it only skips the JS pre-scan that names the offending
|
|
710
|
+
* index. A bucket whose samples are not finite has no min/max to report, and the
|
|
711
|
+
* `0`/`0` it would otherwise carry is what a waveform display draws as silence.
|
|
712
|
+
*/
|
|
680
713
|
export function waveformPeaks(request: WaveformPeaksRequest): WaveformPeaksReport;
|
|
681
714
|
export function waveformPeaks(
|
|
682
715
|
samples: Float32Array,
|
|
@@ -692,18 +725,23 @@ export function waveformPeaks(
|
|
|
692
725
|
samples instanceof Float32Array
|
|
693
726
|
? { samples, channels: channels as number, ...options }
|
|
694
727
|
: samples;
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
728
|
+
assertInterleavedSamples(
|
|
729
|
+
'waveformPeaks',
|
|
730
|
+
request.samples,
|
|
731
|
+
request.channels,
|
|
732
|
+
request.validate !== false,
|
|
733
|
+
);
|
|
699
734
|
const samplesPerBucket = request.samplesPerBucket ?? 512;
|
|
700
|
-
|
|
701
|
-
throw new RangeError('waveformPeaks: samplesPerBucket must be > 0');
|
|
702
|
-
}
|
|
735
|
+
assertPositiveInteger('waveformPeaks', samplesPerBucket, 'samplesPerBucket');
|
|
703
736
|
return requireModule().waveformPeaks(request.samples, request.channels, samplesPerBucket);
|
|
704
737
|
}
|
|
705
738
|
|
|
706
|
-
/**
|
|
739
|
+
/**
|
|
740
|
+
* Compute waveform peak buckets for several zoom levels.
|
|
741
|
+
*
|
|
742
|
+
* Shares {@link waveformPeaks}' bucket kernel, so a non-finite sample is
|
|
743
|
+
* rejected here on the same rule.
|
|
744
|
+
*/
|
|
707
745
|
export function waveformPeakPyramid(request: WaveformPeakPyramidRequest): WaveformPeaksReport[];
|
|
708
746
|
export function waveformPeakPyramid(
|
|
709
747
|
samples: Float32Array,
|
|
@@ -719,13 +757,18 @@ export function waveformPeakPyramid(
|
|
|
719
757
|
samples instanceof Float32Array
|
|
720
758
|
? { samples, channels: channels as number, ...options }
|
|
721
759
|
: samples;
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
760
|
+
assertInterleavedSamples(
|
|
761
|
+
'waveformPeakPyramid',
|
|
762
|
+
request.samples,
|
|
763
|
+
request.channels,
|
|
764
|
+
request.validate !== false,
|
|
765
|
+
);
|
|
726
766
|
const levels = request.samplesPerBucketLevels ?? [512, 1024, 2048, 4096];
|
|
727
|
-
if (levels.length === 0
|
|
728
|
-
throw new RangeError('waveformPeakPyramid: samplesPerBucketLevels must be
|
|
767
|
+
if (levels.length === 0) {
|
|
768
|
+
throw new RangeError('waveformPeakPyramid: samplesPerBucketLevels must not be empty');
|
|
729
769
|
}
|
|
770
|
+
levels.forEach((level, index) => {
|
|
771
|
+
assertPositiveInteger('waveformPeakPyramid', level, `samplesPerBucketLevels[${index}]`);
|
|
772
|
+
});
|
|
730
773
|
return requireModule().waveformPeakPyramid(request.samples, request.channels, levels);
|
|
731
774
|
}
|
package/src/mixer.ts
CHANGED
|
@@ -18,6 +18,49 @@ import type {
|
|
|
18
18
|
SurroundPan,
|
|
19
19
|
} from './public_types';
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* One master-output meter reading. All dB fields are finite and floored at
|
|
23
|
+
* -120; `truePeakDb*` is an inter-sample peak from the ITU-R BS.1770-4
|
|
24
|
+
* polyphase reconstruction at 4x, not a sample peak. That reconstruction is a
|
|
25
|
+
* streaming measurement: its centered stencil needs a few future samples a
|
|
26
|
+
* realtime path does not have, so each block's last samples read marginally low
|
|
27
|
+
* (about 0.1 dB across 64..8192-sample blocks on a near-Nyquist tone, always
|
|
28
|
+
* under-reading). Use `meteringTruePeakDb` over the whole signal for an exact
|
|
29
|
+
* dBTP number.
|
|
30
|
+
*/
|
|
31
|
+
export interface MixerMeterSnapshot {
|
|
32
|
+
peakDbL: number;
|
|
33
|
+
peakDbR: number;
|
|
34
|
+
rmsDbL: number;
|
|
35
|
+
rmsDbR: number;
|
|
36
|
+
correlation: number;
|
|
37
|
+
truePeakDbL: number;
|
|
38
|
+
truePeakDbR: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Meter configuration for a strip added with {@link Mixer.addStrip}.
|
|
43
|
+
*
|
|
44
|
+
* The field names and defaults are the scene document's `strips[].metering`
|
|
45
|
+
* object, so a strip added imperatively and one declared in a scene describe the
|
|
46
|
+
* same thing. A strip's meters size their buffers when the strip is built, so
|
|
47
|
+
* this is the only place the configuration can be chosen — there is no setter.
|
|
48
|
+
* A full meter costs about 646 KB at 48 kHz and a strip carries two of them.
|
|
49
|
+
*/
|
|
50
|
+
export interface StripMeteringOptions {
|
|
51
|
+
/** Both meters; `false` drops them (about 145 KB for the strip instead of 1.4 MB). Default `true`. */
|
|
52
|
+
enabled?: boolean;
|
|
53
|
+
/** LUFS measurement; `false` takes one meter to about 83 KB. Default `true`. */
|
|
54
|
+
lufs?: boolean;
|
|
55
|
+
/** Inter-sample (true) peak measurement. Default `true`. */
|
|
56
|
+
truePeak?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Requested true-peak oversampling factor in `[0, 16]`; the meter resolves it
|
|
59
|
+
* to the nearest of 2x / 4x / 8x. `0` selects the library default (4x).
|
|
60
|
+
*/
|
|
61
|
+
truePeakOversample?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
21
64
|
export interface MixerRealtimeBuffer {
|
|
22
65
|
leftInputs: Float32Array[];
|
|
23
66
|
rightInputs: Float32Array[];
|
|
@@ -45,7 +88,7 @@ export interface MixerRealtimeBuffer {
|
|
|
45
88
|
*
|
|
46
89
|
* @example
|
|
47
90
|
* ```typescript
|
|
48
|
-
* const mixer = Mixer.fromSceneJson(mixingScenePresetJson('
|
|
91
|
+
* const mixer = Mixer.fromSceneJson(mixingScenePresetJson('vocalReverbSend'), 48000, 512);
|
|
49
92
|
* try {
|
|
50
93
|
* const out = mixer.processStereo([stripL], [stripR]);
|
|
51
94
|
* } finally {
|
|
@@ -55,6 +98,7 @@ export interface MixerRealtimeBuffer {
|
|
|
55
98
|
*/
|
|
56
99
|
export class Mixer {
|
|
57
100
|
private mixer: import('./sonare.js').WasmMixer;
|
|
101
|
+
private released = false;
|
|
58
102
|
private readonly blockSize: number;
|
|
59
103
|
|
|
60
104
|
private constructor(mixer: import('./sonare.js').WasmMixer, blockSize: number) {
|
|
@@ -65,6 +109,13 @@ export class Mixer {
|
|
|
65
109
|
/**
|
|
66
110
|
* Build a mixer from a scene JSON string.
|
|
67
111
|
*
|
|
112
|
+
* A strip's meters are sized when the strip is built, so this is where their
|
|
113
|
+
* configuration is chosen: an optional `metering` object on the strip
|
|
114
|
+
* (`enabled` / `lufs` / `truePeak` / `truePeakOversample`) selects it, and
|
|
115
|
+
* leaving it out keeps the full default (LUFS + true peak at 4x, about 1.4 MB
|
|
116
|
+
* per strip at 48 kHz). `{"enabled": false}` drops both meters for a strip
|
|
117
|
+
* whose snapshots are never read.
|
|
118
|
+
*
|
|
68
119
|
* @param json - Scene JSON (strips, buses, sends, connections, inserts)
|
|
69
120
|
* @param sampleRate - Sample rate in Hz (default: 48000)
|
|
70
121
|
* @param blockSize - Maximum block size per {@link processStereo} call (default: 512)
|
|
@@ -137,54 +188,128 @@ export class Mixer {
|
|
|
137
188
|
* after {@link delete}.
|
|
138
189
|
*/
|
|
139
190
|
createRealtimeBuffer(): MixerRealtimeBuffer {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
let rightInputs: Float32Array[] = [];
|
|
191
|
+
const leftInputs: Float32Array[] = [];
|
|
192
|
+
const rightInputs: Float32Array[] = [];
|
|
143
193
|
let outLeft = this.mixer.outputLeftView();
|
|
144
194
|
let outRight = this.mixer.outputRightView();
|
|
195
|
+
let acquiredStripCount = -1;
|
|
196
|
+
|
|
197
|
+
// Every view shares one heap buffer, so a growth detaches the output views too.
|
|
198
|
+
const viewsDetached = (): boolean => outLeft.byteLength === 0 || outRight.byteLength === 0;
|
|
199
|
+
|
|
145
200
|
const acquire = (): void => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
201
|
+
const stripCount = this.stripCount();
|
|
202
|
+
const detached = viewsDetached();
|
|
203
|
+
if (detached) {
|
|
204
|
+
// A heap growth detached every view: reacquire all planes.
|
|
205
|
+
leftInputs.length = 0;
|
|
206
|
+
rightInputs.length = 0;
|
|
207
|
+
} else {
|
|
208
|
+
// Topology growth keeps existing planes in place: append views for new strips only.
|
|
209
|
+
leftInputs.length = Math.min(leftInputs.length, stripCount);
|
|
210
|
+
rightInputs.length = Math.min(rightInputs.length, stripCount);
|
|
211
|
+
}
|
|
212
|
+
for (let index = leftInputs.length; index < stripCount; index++) {
|
|
149
213
|
leftInputs.push(this.mixer.inputLeftView(index));
|
|
214
|
+
}
|
|
215
|
+
for (let index = rightInputs.length; index < stripCount; index++) {
|
|
150
216
|
rightInputs.push(this.mixer.inputRightView(index));
|
|
151
217
|
}
|
|
152
|
-
|
|
153
|
-
|
|
218
|
+
if (detached) {
|
|
219
|
+
outLeft = this.mixer.outputLeftView();
|
|
220
|
+
outRight = this.mixer.outputRightView();
|
|
221
|
+
}
|
|
222
|
+
acquiredStripCount = stripCount;
|
|
154
223
|
};
|
|
155
224
|
acquire();
|
|
225
|
+
|
|
156
226
|
// The cached heap views can detach if WASM linear memory grows (the embind
|
|
157
|
-
// module is built ALLOW_MEMORY_GROWTH).
|
|
158
|
-
//
|
|
159
|
-
const
|
|
160
|
-
if (
|
|
227
|
+
// module is built ALLOW_MEMORY_GROWTH). Also refresh the view list when a
|
|
228
|
+
// caller adds a strip and recompiles the graph after this buffer was made.
|
|
229
|
+
const acquireIfNeeded = (): void => {
|
|
230
|
+
if (acquiredStripCount !== this.stripCount() || viewsDetached()) {
|
|
161
231
|
acquire();
|
|
162
232
|
}
|
|
163
233
|
};
|
|
164
234
|
return {
|
|
165
235
|
get leftInputs(): Float32Array[] {
|
|
166
|
-
|
|
236
|
+
acquireIfNeeded();
|
|
167
237
|
return leftInputs;
|
|
168
238
|
},
|
|
169
239
|
get rightInputs(): Float32Array[] {
|
|
170
|
-
|
|
240
|
+
acquireIfNeeded();
|
|
171
241
|
return rightInputs;
|
|
172
242
|
},
|
|
173
243
|
get outLeft(): Float32Array {
|
|
174
|
-
|
|
244
|
+
acquireIfNeeded();
|
|
175
245
|
return outLeft;
|
|
176
246
|
},
|
|
177
247
|
get outRight(): Float32Array {
|
|
178
|
-
|
|
248
|
+
acquireIfNeeded();
|
|
179
249
|
return outRight;
|
|
180
250
|
},
|
|
181
|
-
process: (numSamples
|
|
182
|
-
|
|
183
|
-
|
|
251
|
+
process: (numSamples?: number) => {
|
|
252
|
+
acquireIfNeeded();
|
|
253
|
+
// Resolve the default only after reacquiring: a detached view reports length 0.
|
|
254
|
+
this.mixer.processPreparedStereo(numSamples ?? outLeft.length);
|
|
184
255
|
},
|
|
185
256
|
};
|
|
186
257
|
}
|
|
187
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Turn the master-output meter on or off.
|
|
261
|
+
*
|
|
262
|
+
* While on, every {@link MixerRealtimeBuffer.process} call meters the stereo
|
|
263
|
+
* master it just produced, so a caller reads {@link meterSnapshot} instead of
|
|
264
|
+
* copying the output and measuring it again. `truePeakDb*` is an inter-sample
|
|
265
|
+
* peak taken after oversampling (ITU-R BS.1770-4 Annex 2 requires at least
|
|
266
|
+
* 4x), which is a different and higher quantity than the sample peak.
|
|
267
|
+
*
|
|
268
|
+
* Enabling resets the meter, so a reading never mixes in audio from a period
|
|
269
|
+
* when metering was off.
|
|
270
|
+
*
|
|
271
|
+
* @param enabled - Whether to meter the master output.
|
|
272
|
+
* @param truePeakOversample - 0 (= 4x) or a power of two in [1, 16].
|
|
273
|
+
*/
|
|
274
|
+
configureMeter(enabled: boolean, truePeakOversample = 4): void {
|
|
275
|
+
this.mixer.configureMeter(enabled, truePeakOversample);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Latest master-output meter reading, describing the most recently metered
|
|
280
|
+
* block. All dB fields are finite and floored at -120.
|
|
281
|
+
*
|
|
282
|
+
* @throws When the meter has never been enabled.
|
|
283
|
+
*/
|
|
284
|
+
meterSnapshot(): MixerMeterSnapshot {
|
|
285
|
+
return this.mixer.meterSnapshot();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Latch the latest meter reading into the mixer's internal scratch so
|
|
290
|
+
* {@link meterScratchValue} can read it back one number at a time.
|
|
291
|
+
*
|
|
292
|
+
* This is the allocation-free form of {@link meterSnapshot}, for an audio
|
|
293
|
+
* render callback that must not create a JS object per interval. It returns
|
|
294
|
+
* `false` instead of throwing when the meter has never been enabled.
|
|
295
|
+
*
|
|
296
|
+
* @returns Whether a reading was latched.
|
|
297
|
+
*/
|
|
298
|
+
latchMeterSnapshot(): boolean {
|
|
299
|
+
return this.mixer.latchMeterSnapshot();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Read one field of the snapshot latched by {@link latchMeterSnapshot}.
|
|
304
|
+
*
|
|
305
|
+
* @param field - `0` peakDbL, `1` peakDbR, `2` rmsDbL, `3` rmsDbR,
|
|
306
|
+
* `4` correlation, `5` truePeakDbL, `6` truePeakDbR. Any other index
|
|
307
|
+
* reads `0`.
|
|
308
|
+
*/
|
|
309
|
+
meterScratchValue(field: number): number {
|
|
310
|
+
return this.mixer.meterScratchValue(field);
|
|
311
|
+
}
|
|
312
|
+
|
|
188
313
|
/** Number of strips in the mixer (e.g. strips loaded from the scene). */
|
|
189
314
|
stripCount(): number {
|
|
190
315
|
return this.mixer.stripCount();
|
|
@@ -232,6 +357,18 @@ export class Mixer {
|
|
|
232
357
|
return index < 0 ? null : index;
|
|
233
358
|
}
|
|
234
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Add a channel strip to the mixer topology. `metering` configures the strip's
|
|
362
|
+
* pre/post taps; omitting it keeps the full default (LUFS + true peak at 4x,
|
|
363
|
+
* about 1.4 MB per strip at 48 kHz). Marks the routing graph dirty; call
|
|
364
|
+
* {@link compile} (or {@link processStereo}) to rebuild.
|
|
365
|
+
*
|
|
366
|
+
* @throws If the id is already taken, or `truePeakOversample` is outside `[0, 16]`
|
|
367
|
+
*/
|
|
368
|
+
addStrip(id: string, metering: StripMeteringOptions = {}): void {
|
|
369
|
+
this.mixer.addStrip(id, metering);
|
|
370
|
+
}
|
|
371
|
+
|
|
235
372
|
/**
|
|
236
373
|
* Add a bus to the mixer topology. `role` is one of `'master'`, `'aux'`, or
|
|
237
374
|
* `'submix'` (defaults to `'aux'`). Marks the routing graph dirty; call
|
|
@@ -310,6 +447,31 @@ export class Mixer {
|
|
|
310
447
|
this.mixer.setWidth(stripIndex, width);
|
|
311
448
|
}
|
|
312
449
|
|
|
450
|
+
/**
|
|
451
|
+
* Snap the strip's input-trim, fader, pan and width smoothers to the values
|
|
452
|
+
* already set on it, so the next processed block opens at those values
|
|
453
|
+
* instead of gliding to them over the smoothing window (~5 ms).
|
|
454
|
+
*
|
|
455
|
+
* Call it after configuring a strip and before rendering a finite buffer: a
|
|
456
|
+
* strip is smoothed for a live fader, and an offline render that does not
|
|
457
|
+
* settle carries that glide as a level and image sweep across the head of
|
|
458
|
+
* its output. Unlike a reset it clears nothing — automation, meters and
|
|
459
|
+
* insert state are untouched.
|
|
460
|
+
*
|
|
461
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
462
|
+
*
|
|
463
|
+
* @example
|
|
464
|
+
* ```typescript
|
|
465
|
+
* mixer.setFaderDb(0, -3);
|
|
466
|
+
* mixer.setPan(0, 0.3);
|
|
467
|
+
* mixer.settle(0);
|
|
468
|
+
* const { left, right } = mixer.processStereo([dryLeft], [dryRight]);
|
|
469
|
+
* ```
|
|
470
|
+
*/
|
|
471
|
+
settle(stripIndex: number): void {
|
|
472
|
+
this.mixer.settle(stripIndex);
|
|
473
|
+
}
|
|
474
|
+
|
|
313
475
|
/** Set the strip's mute state. */
|
|
314
476
|
setMuted(stripIndex: number, muted: boolean): void {
|
|
315
477
|
this.mixer.setMuted(stripIndex, muted);
|
|
@@ -361,7 +523,10 @@ export class Mixer {
|
|
|
361
523
|
|
|
362
524
|
/**
|
|
363
525
|
* Set the strip's surround pan position, used when it feeds a >2-channel bus.
|
|
364
|
-
*
|
|
526
|
+
*
|
|
527
|
+
* Applied when the engine's track mixer renders this strip's lane into a
|
|
528
|
+
* destination with more than two channels. This stereo-only mixer's own
|
|
529
|
+
* block entry points ignore it.
|
|
365
530
|
*/
|
|
366
531
|
setSurroundPan(stripIndex: number, pan: SurroundPan): void {
|
|
367
532
|
this.mixer.setSurroundPan(stripIndex, pan);
|
|
@@ -440,6 +605,61 @@ export class Mixer {
|
|
|
440
605
|
return this.mixer.busMeter(busId);
|
|
441
606
|
}
|
|
442
607
|
|
|
608
|
+
/**
|
|
609
|
+
* Number of blocks in which the strip discarded recursive state because a
|
|
610
|
+
* non-finite value had reached it.
|
|
611
|
+
*
|
|
612
|
+
* Advisory telemetry, and the only thing that separates a degraded strip
|
|
613
|
+
* from a clean one. A discard returns the affected state to its
|
|
614
|
+
* post-reset value, so the strip recovers in silence and the output stays
|
|
615
|
+
* finite and in range while carrying samples unrelated to the input;
|
|
616
|
+
* nothing else reports that this happened.
|
|
617
|
+
*
|
|
618
|
+
* The count covers the strip's own state, its EQ, every insert it owns and
|
|
619
|
+
* both of its meters. None of those is separately addressable here, so a
|
|
620
|
+
* discard inside one is observable only through this number -- and a
|
|
621
|
+
* meter that loses its loudness window then reports the floor, which is
|
|
622
|
+
* exactly what a genuinely silent strip reports, so nothing else
|
|
623
|
+
* distinguishes the two.
|
|
624
|
+
*
|
|
625
|
+
* A meter's own discard lags by one block: it checks its loudness state at
|
|
626
|
+
* the top of a block, before consuming that block's samples, so the block
|
|
627
|
+
* that corrupts it is not the block the count moves on -- read this again
|
|
628
|
+
* after one more block has processed. The EQ and inserts have no such lag;
|
|
629
|
+
* they discard at the end of their own process, in the same block that
|
|
630
|
+
* carried the poison.
|
|
631
|
+
*
|
|
632
|
+
* Cumulative since the strip was created and never cleared, so two
|
|
633
|
+
* readings bracket a span of audio. The unit is one processed block, never
|
|
634
|
+
* a channel, so a stereo block that discards on both channels adds one and
|
|
635
|
+
* the number does not depend on a dimension the caller did not choose.
|
|
636
|
+
*
|
|
637
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
638
|
+
*/
|
|
639
|
+
stripNonFiniteDiscardCount(stripIndex: number): number {
|
|
640
|
+
return this.mixer.stripNonFiniteDiscardCount(stripIndex);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Number of blocks in which a bus discarded recursive state because a
|
|
645
|
+
* non-finite value had reached it. Same contract as
|
|
646
|
+
* {@link stripNonFiniteDiscardCount}, for a bus: covers every insert the
|
|
647
|
+
* bus owns and its meter, neither separately addressable, so a discard
|
|
648
|
+
* inside one is observable only here. Cumulative across graph recompiles
|
|
649
|
+
* -- the count lives with the bus, not the compiled node, so an unrelated
|
|
650
|
+
* edit elsewhere in the mixer does not reset it.
|
|
651
|
+
*
|
|
652
|
+
* A bus's DSP record is created by the first {@link compile}. Throws for a
|
|
653
|
+
* bus that has been declared with {@link addBus} but never compiled --
|
|
654
|
+
* reading zero there would read as clean, and it is not. Also throws for
|
|
655
|
+
* an unknown bus id.
|
|
656
|
+
*
|
|
657
|
+
* @param busId - Bus id, as passed to {@link addBus} or declared in scene JSON
|
|
658
|
+
*/
|
|
659
|
+
busNonFiniteDiscardCount(busId: string): number {
|
|
660
|
+
return this.mixer.busNonFiniteDiscardCount(busId);
|
|
661
|
+
}
|
|
662
|
+
|
|
443
663
|
/**
|
|
444
664
|
* Schedule sample-accurate fader automation on a strip.
|
|
445
665
|
*
|
|
@@ -519,6 +739,11 @@ export class Mixer {
|
|
|
519
739
|
/**
|
|
520
740
|
* Read up to `maxPoints` of a strip's most recent goniometer samples
|
|
521
741
|
* (oldest to newest).
|
|
742
|
+
*
|
|
743
|
+
* `maxPoints` must be a finite non-negative integer; anything else throws an
|
|
744
|
+
* `InvalidParameter` error. It is a request rather than an allocation size —
|
|
745
|
+
* a value beyond the strip's goniometer ring simply returns every point the
|
|
746
|
+
* ring holds.
|
|
522
747
|
*/
|
|
523
748
|
readGoniometerLatest(stripIndex: number, maxPoints: number): GoniometerPoint[] {
|
|
524
749
|
return this.mixer.readGoniometerLatest(stripIndex, maxPoints);
|
|
@@ -559,8 +784,12 @@ export class Mixer {
|
|
|
559
784
|
return this.mixer.drainTailStereo(numSamples);
|
|
560
785
|
}
|
|
561
786
|
|
|
562
|
-
/** Release the underlying WASM object.
|
|
787
|
+
/** Release the underlying WASM object. Idempotent, as the Node facade is. */
|
|
563
788
|
delete(): void {
|
|
789
|
+
if (this.released) {
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
this.released = true;
|
|
564
793
|
this.mixer.delete();
|
|
565
794
|
}
|
|
566
795
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { getSonareModule } from './module_state';
|
|
2
|
+
import type { MixAssistantOptions, MixAssistantResult, MixAssistantTrack } from './public_types';
|
|
3
|
+
import { assertSampleRate } from './validation';
|
|
4
|
+
|
|
5
|
+
function requireModule() {
|
|
6
|
+
return getSonareModule();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Inputs for the {@link suggestMixScene} / {@link suggestMixSceneJson} facades. */
|
|
10
|
+
export interface SuggestMixSceneRequest {
|
|
11
|
+
/** Tracks to mix, in the order their profiles are reported. */
|
|
12
|
+
tracks: MixAssistantTrack[];
|
|
13
|
+
/**
|
|
14
|
+
* Shared sample rate in Hz for every track. Required: every band edge,
|
|
15
|
+
* high-pass corner, sibilance band and alignment lag is derived from it, so a
|
|
16
|
+
* guessed rate would silently misread 44.1 kHz material by 8.8% and report
|
|
17
|
+
* `channelDelaySamples` and `durationSec` wrong with it.
|
|
18
|
+
*/
|
|
19
|
+
sampleRate: number;
|
|
20
|
+
/** Assistant tunables; every field falls back to the core default. */
|
|
21
|
+
options?: MixAssistantOptions;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The four parallel arrays the embind entry points take. */
|
|
25
|
+
interface PlanarTracks {
|
|
26
|
+
left: Float32Array[];
|
|
27
|
+
right: (Float32Array | null)[];
|
|
28
|
+
ids: string[];
|
|
29
|
+
names: (string | null)[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Splits the request's track list into the planar per-track arrays the binding
|
|
34
|
+
* takes, rejecting the shapes that would otherwise reach the analysis as a
|
|
35
|
+
* missing buffer or a nameless strip.
|
|
36
|
+
*/
|
|
37
|
+
function planarTracks(tracks: MixAssistantTrack[]): PlanarTracks {
|
|
38
|
+
if (!Array.isArray(tracks)) {
|
|
39
|
+
throw new Error('tracks must be an array.');
|
|
40
|
+
}
|
|
41
|
+
const left: Float32Array[] = [];
|
|
42
|
+
const right: (Float32Array | null)[] = [];
|
|
43
|
+
const ids: string[] = [];
|
|
44
|
+
const names: (string | null)[] = [];
|
|
45
|
+
for (let index = 0; index < tracks.length; index++) {
|
|
46
|
+
const track = tracks[index];
|
|
47
|
+
if (track === null || typeof track !== 'object') {
|
|
48
|
+
throw new Error(`tracks[${index}] must be an object.`);
|
|
49
|
+
}
|
|
50
|
+
if (typeof track.id !== 'string' || track.id.length === 0) {
|
|
51
|
+
throw new Error(`tracks[${index}].id must be a non-empty string.`);
|
|
52
|
+
}
|
|
53
|
+
if (!(track.left instanceof Float32Array)) {
|
|
54
|
+
throw new Error(`tracks[${index}].left must be a Float32Array.`);
|
|
55
|
+
}
|
|
56
|
+
if (track.right !== undefined && !(track.right instanceof Float32Array)) {
|
|
57
|
+
throw new Error(`tracks[${index}].right must be a Float32Array when present.`);
|
|
58
|
+
}
|
|
59
|
+
left.push(track.left);
|
|
60
|
+
right.push(track.right ?? null);
|
|
61
|
+
ids.push(track.id);
|
|
62
|
+
names.push(track.name ?? null);
|
|
63
|
+
}
|
|
64
|
+
return { left, right, ids, names };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function suggestJson(fnName: string, request: SuggestMixSceneRequest, sceneOnly: boolean): string {
|
|
68
|
+
// Required, not defaulted: this surface used to invent 48000, which read
|
|
69
|
+
// 44.1 kHz material 8.8% off across every band edge and every lag without
|
|
70
|
+
// saying so. Node and Python both demand it, and a request ported from either
|
|
71
|
+
// must not change behaviour by arriving here.
|
|
72
|
+
assertSampleRate(fnName, request.sampleRate);
|
|
73
|
+
const { left, right, ids, names } = planarTracks(request.tracks);
|
|
74
|
+
const sampleRate = request.sampleRate;
|
|
75
|
+
const params = (request.options ?? {}) as Record<string, number | boolean>;
|
|
76
|
+
const module = requireModule();
|
|
77
|
+
return sceneOnly
|
|
78
|
+
? module.mixingAssistantSuggestSceneJson(left, right, ids, names, sampleRate, params)
|
|
79
|
+
: module.mixingAssistantSuggest(left, right, ids, names, sampleRate, params);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Analyze a set of tracks and suggest a mixer scene.
|
|
84
|
+
*
|
|
85
|
+
* Offline only: the pipeline runs an STFT per track and evaluates every track
|
|
86
|
+
* pair, so it is measured in milliseconds per track and must never be called
|
|
87
|
+
* from an audio callback.
|
|
88
|
+
*
|
|
89
|
+
* The assistant suggests, it does not apply. Nothing is processed and no audio
|
|
90
|
+
* is returned; realizing the suggestion means handing the scene to
|
|
91
|
+
* {@link Mixer.fromSceneJson} as an explicit second step, for which
|
|
92
|
+
* {@link suggestMixSceneJson} returns the scene already serialized.
|
|
93
|
+
*
|
|
94
|
+
* Degenerate input is not an error: no tracks, all-silent tracks or tracks too
|
|
95
|
+
* short to measure yield an empty scene and an empty `explanation`.
|
|
96
|
+
*
|
|
97
|
+
* @param request - Tracks, shared sample rate and assistant options
|
|
98
|
+
* @returns The suggested scene, per-track profiles, cross-track measurements
|
|
99
|
+
* and the explanation behind each change
|
|
100
|
+
*/
|
|
101
|
+
export function suggestMixScene(request: SuggestMixSceneRequest): MixAssistantResult {
|
|
102
|
+
return JSON.parse(suggestJson('suggestMixScene', request, false)) as MixAssistantResult;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Suggest a mixer scene and return only the scene, as JSON.
|
|
107
|
+
*
|
|
108
|
+
* The same analysis as {@link suggestMixScene}, serialized in the schema
|
|
109
|
+
* {@link Mixer.fromSceneJson} reads, so a caller that only wants to apply the
|
|
110
|
+
* suggestion neither digs the scene out of the fuller result nor re-serializes
|
|
111
|
+
* it.
|
|
112
|
+
*
|
|
113
|
+
* @param request - Tracks, shared sample rate and assistant options
|
|
114
|
+
* @returns Scene JSON string
|
|
115
|
+
*/
|
|
116
|
+
export function suggestMixSceneJson(request: SuggestMixSceneRequest): string {
|
|
117
|
+
return suggestJson('suggestMixSceneJson', request, true);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Source-class identifiers the assistant can report, in enum order.
|
|
122
|
+
*
|
|
123
|
+
* The index of a name in this list is the value
|
|
124
|
+
* {@link mixSourceClassFromName} resolves it to.
|
|
125
|
+
*/
|
|
126
|
+
export function mixSourceClassNames(): string[] {
|
|
127
|
+
return Array.from(requireModule().mixingAssistantSourceClassNames());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Resolve a source-class identifier to its index in {@link mixSourceClassNames}.
|
|
132
|
+
*
|
|
133
|
+
* @param name - Source-class identifier, e.g. `"kick"`
|
|
134
|
+
* @returns The index, or -1 when the name is unknown
|
|
135
|
+
*/
|
|
136
|
+
export function mixSourceClassFromName(name: string): number {
|
|
137
|
+
return requireModule().mixingAssistantSourceClassFromName(name);
|
|
138
|
+
}
|