@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
package/src/validation.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ErrorCode, SonareError } from './errors';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Per-call validation options accepted by guarded wrappers. Empty-buffer
|
|
3
5
|
* checks are always performed; pass `{ validate: false }` to opt out of the
|
|
@@ -6,9 +8,11 @@
|
|
|
6
8
|
* `{ validate: false }` only skips this JS-side pre-scan (which raises a
|
|
7
9
|
* `RangeError` naming the exact offending index). It is NOT a way to push
|
|
8
10
|
* non-finite samples into the core: the native layer always re-validates the
|
|
9
|
-
* buffer
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* buffer — through `validate_offline_audio_input` for the offline-analysis
|
|
12
|
+
* entry points, and through the computation's own per-sample guard where a call
|
|
13
|
+
* takes no sample rate to validate against (the waveform bucket kernels) —
|
|
14
|
+
* matching the C ABI / Node / Python surfaces, so an NaN/Inf buffer still
|
|
15
|
+
* throws, just with a generic native message instead of the indexed JS one.
|
|
12
16
|
*/
|
|
13
17
|
export interface ValidateOptions {
|
|
14
18
|
validate?: boolean;
|
|
@@ -60,20 +64,68 @@ export function assertSamples(
|
|
|
60
64
|
assertFiniteSamples(fnName, samples, validate, argName);
|
|
61
65
|
}
|
|
62
66
|
|
|
67
|
+
/**
|
|
68
|
+
* `assertSamples` restricted to the span a windowed entry point actually reads.
|
|
69
|
+
*
|
|
70
|
+
* The emptiness check still covers the whole buffer, and the reported index is
|
|
71
|
+
* the absolute one, so the message keeps naming the sample the caller passed.
|
|
72
|
+
* What narrows is the scan: a windowed call refuses a non-finite sample inside
|
|
73
|
+
* its frame and is indifferent to one outside it, which is the contract the C
|
|
74
|
+
* ABI states and the cost model it promises -- per call the scan is bounded by
|
|
75
|
+
* the frame, not by the length of the buffer being polled.
|
|
76
|
+
*/
|
|
77
|
+
export function assertSamplesInWindow(
|
|
78
|
+
fnName: string,
|
|
79
|
+
samples: ArrayLike<number>,
|
|
80
|
+
validate: boolean,
|
|
81
|
+
windowStart: number,
|
|
82
|
+
windowLength: number,
|
|
83
|
+
argName = 'samples',
|
|
84
|
+
): void {
|
|
85
|
+
assertNonEmptySamples(fnName, samples, argName);
|
|
86
|
+
if (!validate) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Floored and ceiled before use: a fractional offset is refused by the layer
|
|
90
|
+
// that owns it, but an index of 100.5 reads `undefined` out of the buffer and
|
|
91
|
+
// would be reported here as a non-finite sample that is not there.
|
|
92
|
+
const start = Math.min(Math.max(Math.floor(windowStart), 0), samples.length);
|
|
93
|
+
const stop = Math.min(start + Math.max(Math.ceil(windowLength), 0), samples.length);
|
|
94
|
+
for (let i = start; i < stop; i++) {
|
|
95
|
+
const v = samples[i] as number;
|
|
96
|
+
if (!Number.isFinite(v)) {
|
|
97
|
+
throw new RangeError(`${fnName}: ${argName} contains NaN or Inf at index ${i}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
63
102
|
export function assertFiniteScalar(fnName: string, value: number, argName: string): void {
|
|
64
103
|
if (!Number.isFinite(value)) {
|
|
65
104
|
throw new RangeError(`${fnName}: ${argName} must be a finite number`);
|
|
66
105
|
}
|
|
67
106
|
}
|
|
68
107
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
) {
|
|
108
|
+
/**
|
|
109
|
+
* A NaN gamma is the automatic-bandwidth sentinel the variable-Q transform
|
|
110
|
+
* documents, alongside a negative value, so only an infinity is out of domain.
|
|
111
|
+
*/
|
|
112
|
+
export function assertVqtGamma(fnName: string, gamma: number): void {
|
|
113
|
+
if (gamma === Number.POSITIVE_INFINITY || gamma === Number.NEGATIVE_INFINITY) {
|
|
114
|
+
throw new RangeError(`${fnName}: gamma must not be infinite`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function assertSampleRate(fnName: string, sampleRate: number, argName = 'sampleRate'): void {
|
|
119
|
+
// Two refusals, not one: 22050.7 sits inside the range, so reporting it as
|
|
120
|
+
// out of range names an argument that is not the one at fault. `argName` is
|
|
121
|
+
// the same point for a rate the caller spelled something else, such as a
|
|
122
|
+
// resampler's source and target.
|
|
123
|
+
if (!Number.isInteger(sampleRate)) {
|
|
124
|
+
throw new RangeError(`${fnName}: ${argName} must be an integer`);
|
|
125
|
+
}
|
|
126
|
+
if (sampleRate < MIN_AUDIO_SAMPLE_RATE || sampleRate > MAX_AUDIO_SAMPLE_RATE) {
|
|
75
127
|
throw new RangeError(
|
|
76
|
-
`${fnName}:
|
|
128
|
+
`${fnName}: ${argName} out of supported range [${MIN_AUDIO_SAMPLE_RATE}, ${MAX_AUDIO_SAMPLE_RATE}]`,
|
|
77
129
|
);
|
|
78
130
|
}
|
|
79
131
|
}
|
|
@@ -84,18 +136,240 @@ export function validateAudioBuffer(samples: Float32Array, sampleRate: number):
|
|
|
84
136
|
assertSampleRate('Audio.fromBuffer', sampleRate);
|
|
85
137
|
}
|
|
86
138
|
|
|
139
|
+
/** Bounds of the native `int` every embind argument below is narrowed into. */
|
|
140
|
+
export const C_INT_MIN = -2147483648;
|
|
141
|
+
export const C_INT_MAX = 2147483647;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Reject an argument embind's declared-`int` narrowing would wrap.
|
|
145
|
+
*
|
|
146
|
+
* A positional embind parameter declared `int` WRAPS rather than saturates, so
|
|
147
|
+
* a kernel of `2 ** 32` arrives as 0 and `2 ** 32 + 1` as 1 — both values the
|
|
148
|
+
* native guards accept, so the call succeeds having separated on a setting the
|
|
149
|
+
* caller never asked for. (The options-object path narrows through
|
|
150
|
+
* `checkedIntFromVal`, which refuses the same inputs in the module; this is the
|
|
151
|
+
* positional path's equivalent.)
|
|
152
|
+
*
|
|
153
|
+
* Reported as the branded `SonareError` carrying `InvalidParameter` rather than
|
|
154
|
+
* a `RangeError`, because the class follows what the rejection stands in for: a
|
|
155
|
+
* `RangeError` is this surface refusing an argument on its own authority, while
|
|
156
|
+
* this one pre-empts a native refusal the caller would have received under that
|
|
157
|
+
* code had the narrowing not wrapped the value into the accepted domain first.
|
|
158
|
+
* The agreement with the Node and Python surfaces is asserted by the Node
|
|
159
|
+
* package's `tests/narrowing-code-parity.test.ts`, which drives one value
|
|
160
|
+
* through all three — weakening this check turns that red.
|
|
161
|
+
*/
|
|
162
|
+
export function assertInt32(fnName: string, value: number, argName: string): void {
|
|
163
|
+
if (!Number.isInteger(value) || value < C_INT_MIN || value > C_INT_MAX) {
|
|
164
|
+
throw new SonareError(
|
|
165
|
+
ErrorCode.InvalidParameter,
|
|
166
|
+
'InvalidParameter',
|
|
167
|
+
`${fnName}: ${argName} must be an integer within the signed 32-bit range`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Check both HPSS kernels before embind narrows them.
|
|
174
|
+
*
|
|
175
|
+
* Parity, positivity and the ceiling stay the core's to enforce, and it names
|
|
176
|
+
* the median filter that rejected the value. What cannot be deferred is the
|
|
177
|
+
* narrowing itself: a wrapped kernel arrives as a legal one and separates on it.
|
|
178
|
+
*
|
|
179
|
+
* These two arrive POSITIONALLY, so they never pass through the options-bag
|
|
180
|
+
* reader and inherit none of its checks. That is what separates this from
|
|
181
|
+
* {@link assertPercussiveSeparation}, which duplicates a reader check to improve
|
|
182
|
+
* a message; here there is no reader to duplicate.
|
|
183
|
+
*/
|
|
184
|
+
export function assertHpssKernels(
|
|
185
|
+
fnName: string,
|
|
186
|
+
kernelHarmonic: number,
|
|
187
|
+
kernelPercussive: number,
|
|
188
|
+
): void {
|
|
189
|
+
assertInt32(fnName, kernelHarmonic, 'kernelHarmonic');
|
|
190
|
+
assertInt32(fnName, kernelPercussive, 'kernelPercussive');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function assertIntegralField(fnName: string, value: number, argName: string): void {
|
|
194
|
+
if (!Number.isInteger(value)) {
|
|
195
|
+
throw new SonareError(
|
|
196
|
+
ErrorCode.InvalidParameter,
|
|
197
|
+
'InvalidParameter',
|
|
198
|
+
`${fnName}: ${argName} must be an integer`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Percussive-event separation fields, as the request objects carry them.
|
|
205
|
+
*/
|
|
206
|
+
export interface PercussiveSeparationFields {
|
|
207
|
+
nFft?: number;
|
|
208
|
+
hopLength?: number;
|
|
209
|
+
hpssKernelHarmonic?: number;
|
|
210
|
+
hpssKernelPercussive?: number;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Check the percussive-event separation's framing and kernels for integrality.
|
|
215
|
+
*
|
|
216
|
+
* All four fields reach the module through one options-bag reader, which now
|
|
217
|
+
* refuses a fractional value itself, so this is the diagnostic rather than the
|
|
218
|
+
* guarantee: it fires first and names the function, where the reader can only
|
|
219
|
+
* name the field. Both reject the same set, so they cannot disagree about an
|
|
220
|
+
* input — only about how the message reads. Do not narrow this to fields the
|
|
221
|
+
* reader misses; there are none, and a check scoped to a gap that no longer
|
|
222
|
+
* exists is how a stale justification outlives its divergence.
|
|
223
|
+
*
|
|
224
|
+
* The fields are iterated rather than named at each call site so a new one is
|
|
225
|
+
* visible here. Absence means the default, so an omitted field is not resolved.
|
|
226
|
+
*/
|
|
227
|
+
export function assertPercussiveSeparation(
|
|
228
|
+
fnName: string,
|
|
229
|
+
options: PercussiveSeparationFields,
|
|
230
|
+
): void {
|
|
231
|
+
const fields = ['nFft', 'hopLength', 'hpssKernelHarmonic', 'hpssKernelPercussive'] as const;
|
|
232
|
+
for (const field of fields) {
|
|
233
|
+
const value = options[field];
|
|
234
|
+
if (value !== undefined) {
|
|
235
|
+
assertIntegralField(fnName, value, field);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
87
240
|
export function assertNonNegativeInteger(fnName: string, value: number, argName: string): void {
|
|
88
241
|
if (!Number.isInteger(value) || value < 0) {
|
|
89
242
|
throw new RangeError(`${fnName}: ${argName} must be a non-negative integer`);
|
|
90
243
|
}
|
|
91
244
|
}
|
|
92
245
|
|
|
246
|
+
/** Integer strictly greater than zero, up to the native `int` ceiling. */
|
|
93
247
|
export function assertPositiveInteger(fnName: string, value: number, argName: string): void {
|
|
94
|
-
|
|
248
|
+
// Two refusals, not one: 512.7 is positive, so reporting it as non-positive
|
|
249
|
+
// names a property it has. The ceiling travels with the sign rather than with
|
|
250
|
+
// integrality, because both describe a value the native `int` can carry.
|
|
251
|
+
if (!Number.isInteger(value)) {
|
|
252
|
+
throw new RangeError(`${fnName}: ${argName} must be an integer`);
|
|
253
|
+
}
|
|
254
|
+
if (value <= 0 || value > C_INT_MAX) {
|
|
95
255
|
throw new RangeError(`${fnName}: ${argName} must be a positive integer`);
|
|
96
256
|
}
|
|
97
257
|
}
|
|
98
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Split "not an integer" into the two mistakes it can be: `TypeError` when the
|
|
261
|
+
* value is not a `number` at all, `RangeError` when it is a number but not
|
|
262
|
+
* integral -- the right-type, wrong-domain half of the same message.
|
|
263
|
+
*
|
|
264
|
+
* `Number.isInteger` alone answers both, which is why a single check reads as
|
|
265
|
+
* sufficient; what it cannot do is say which one happened, and a caller can act
|
|
266
|
+
* only on the mistake they made. The classes are the contract on this surface,
|
|
267
|
+
* so a sibling argument in the same call must not report a fraction differently
|
|
268
|
+
* from this one.
|
|
269
|
+
*/
|
|
270
|
+
export function assertIntegerValue(
|
|
271
|
+
fnName: string,
|
|
272
|
+
value: unknown,
|
|
273
|
+
argName: string,
|
|
274
|
+
): asserts value is number {
|
|
275
|
+
if (typeof value !== 'number') {
|
|
276
|
+
throw new TypeError(`${fnName}: ${argName} must be an integer`);
|
|
277
|
+
}
|
|
278
|
+
if (!Number.isInteger(value)) {
|
|
279
|
+
throw new RangeError(`${fnName}: ${argName} must be an integer`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Even integer in `[min, max]`, for an FFT-style size where only parity matters. */
|
|
284
|
+
export function assertEvenIntegerAtLeast(
|
|
285
|
+
fnName: string,
|
|
286
|
+
value: number,
|
|
287
|
+
argName: string,
|
|
288
|
+
min: number,
|
|
289
|
+
max: number,
|
|
290
|
+
): void {
|
|
291
|
+
// Range and parity are separate refusals: 2 ** 31 is already even, so telling
|
|
292
|
+
// its caller the value must be even names a property it has.
|
|
293
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
294
|
+
throw new RangeError(`${fnName}: ${argName} must be an integer in [${min}, ${max}]`);
|
|
295
|
+
}
|
|
296
|
+
if (value % 2 !== 0) {
|
|
297
|
+
throw new RangeError(`${fnName}: ${argName} must be an even integer`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** General integer-in-`[min, max]` check, for a bound {@link assertU7}/{@link assertNibble} don't cover. */
|
|
302
|
+
export function assertBoundedInteger(
|
|
303
|
+
fnName: string,
|
|
304
|
+
value: number,
|
|
305
|
+
argName: string,
|
|
306
|
+
min: number,
|
|
307
|
+
max: number,
|
|
308
|
+
): void {
|
|
309
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
310
|
+
throw new RangeError(`${fnName}: ${argName} must be an integer in [${min}, ${max}]`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function assertU7(fnName: string, value: number, argName: string): number {
|
|
315
|
+
if (!Number.isInteger(value) || value < 0 || value > 127) {
|
|
316
|
+
throw new RangeError(`${fnName}: ${argName} must be an integer in [0, 127]`);
|
|
317
|
+
}
|
|
318
|
+
return value;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function assertNibble(fnName: string, value: number, argName: string): number {
|
|
322
|
+
if (!Number.isInteger(value) || value < 0 || value > 15) {
|
|
323
|
+
throw new RangeError(`${fnName}: ${argName} must be an integer in [0, 15]`);
|
|
324
|
+
}
|
|
325
|
+
return value;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function assertU32(fnName: string, value: number, argName: string): void {
|
|
329
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xffffffff) {
|
|
330
|
+
throw new RangeError(`${fnName}: ${argName} must be an integer in [0, 4294967295]`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Convert a caller-supplied index array to `Int32Array`, refusing any element
|
|
336
|
+
* the conversion would change rather than folding it.
|
|
337
|
+
*
|
|
338
|
+
* `Int32Array.from(values, Math.trunc)` turns the sample index `1000.7` into
|
|
339
|
+
* `1000` and `2 ** 31` into `-2 ** 31`. The C ABI takes `const int*`, so both
|
|
340
|
+
* arrive as values nothing downstream can separate from ones the caller chose.
|
|
341
|
+
* An `Int32Array` is returned as-is: its elements are already exact.
|
|
342
|
+
*/
|
|
343
|
+
export function toInt32Array(
|
|
344
|
+
fnName: string,
|
|
345
|
+
values: Int32Array | ArrayLike<number>,
|
|
346
|
+
argName: string,
|
|
347
|
+
): Int32Array {
|
|
348
|
+
if (values instanceof Int32Array) {
|
|
349
|
+
return values;
|
|
350
|
+
}
|
|
351
|
+
const out = new Int32Array(values.length);
|
|
352
|
+
for (let i = 0; i < values.length; i++) {
|
|
353
|
+
const element = values[i];
|
|
354
|
+
// The element shape of `assertIntegerValue`, and it splits the same way: the
|
|
355
|
+
// class says whether the entry was the wrong type or the wrong domain, and a
|
|
356
|
+
// sibling element in the same array must not report a fraction differently.
|
|
357
|
+
if (typeof element !== 'number') {
|
|
358
|
+
throw new TypeError(`${fnName}: ${argName}[${i}] must be an integer`);
|
|
359
|
+
}
|
|
360
|
+
if (!Number.isInteger(element)) {
|
|
361
|
+
throw new RangeError(`${fnName}: ${argName}[${i}] must be an integer`);
|
|
362
|
+
}
|
|
363
|
+
if (element < C_INT_MIN || element > C_INT_MAX) {
|
|
364
|
+
throw new RangeError(
|
|
365
|
+
`${fnName}: ${argName}[${i}] must be an integer in [${C_INT_MIN}, ${C_INT_MAX}]`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
out[i] = element;
|
|
369
|
+
}
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
372
|
+
|
|
99
373
|
export function assertInterleavedSamples(
|
|
100
374
|
fnName: string,
|
|
101
375
|
samples: ArrayLike<number>,
|
package/src/web_midi.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MidiCcBindOptions } from './realtime_engine';
|
|
2
|
+
import { assertNibble } from './validation';
|
|
2
3
|
|
|
3
4
|
export interface WebMidiEngine {
|
|
4
5
|
bindMidiCc(
|
|
@@ -394,12 +395,6 @@ function readU7(data: ArrayLike<number>, index: number): number {
|
|
|
394
395
|
return value;
|
|
395
396
|
}
|
|
396
397
|
|
|
397
|
-
function assertNibble(fnName: string, value: number, field: string): void {
|
|
398
|
-
if (!Number.isInteger(value) || value < 0 || value > 15) {
|
|
399
|
-
throw new RangeError(`${fnName}: ${field} must be an integer in [0, 15]`);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
398
|
function iterInputs(access: MidiAccessLike): Iterable<[string, MidiInputLike]> {
|
|
404
399
|
return access.inputs instanceof Map ? access.inputs.entries() : access.inputs;
|
|
405
400
|
}
|
package/src/worker.ts
CHANGED
|
@@ -98,8 +98,16 @@ function cancelledError(): SonareError {
|
|
|
98
98
|
*/
|
|
99
99
|
export function installOfflineWorkerEndpoint(endpoint: OfflineWorkerEndpoint): void {
|
|
100
100
|
const cancelled = new Set<number>();
|
|
101
|
+
// IDs with a run() in flight, from message dispatch to its finally block.
|
|
102
|
+
// A cancel naming an ID outside this set has nothing left to cancel -- the
|
|
103
|
+
// run already settled, or never started -- and must not be recorded, or it
|
|
104
|
+
// leaks a Set entry no later message ever clears.
|
|
105
|
+
const inFlight = new Set<number>();
|
|
101
106
|
|
|
102
107
|
const run = async (message: OfflineWorkerRunMessage): Promise<void> => {
|
|
108
|
+
// Synchronous: runs before this async function's first `await`, so it is
|
|
109
|
+
// visible to the listener below before any other message can be handled.
|
|
110
|
+
inFlight.add(message.id);
|
|
103
111
|
const cancelFlag = message.cancelBuffer ? new Int32Array(message.cancelBuffer) : undefined;
|
|
104
112
|
const isCancelled = (): boolean =>
|
|
105
113
|
cancelled.has(message.id) || (cancelFlag !== undefined && Atomics.load(cancelFlag, 0) !== 0);
|
|
@@ -107,19 +115,22 @@ export function installOfflineWorkerEndpoint(endpoint: OfflineWorkerEndpoint): v
|
|
|
107
115
|
endpoint.postMessage({ type: 'sonare:offline-progress', id: message.id, progress, stage });
|
|
108
116
|
return isCancelled() ? false : undefined;
|
|
109
117
|
};
|
|
110
|
-
|
|
111
118
|
try {
|
|
112
119
|
await init();
|
|
113
120
|
if (isCancelled()) {
|
|
114
121
|
throw cancelledError();
|
|
115
122
|
}
|
|
116
123
|
|
|
124
|
+
// `cancel` is a separate channel from `onProgress`: the native call polls
|
|
125
|
+
// it and never reads what the progress callback returns, so passing only
|
|
126
|
+
// onProgress left a running operation no way to learn it was cancelled.
|
|
117
127
|
let result: unknown;
|
|
118
128
|
switch (message.operation) {
|
|
119
129
|
case 'analyze':
|
|
120
130
|
result = analyzeWithProgress({
|
|
121
131
|
...(message.request as unknown as MusicAnalyzeRequest),
|
|
122
132
|
onProgress,
|
|
133
|
+
cancel: isCancelled,
|
|
123
134
|
});
|
|
124
135
|
break;
|
|
125
136
|
case 'detectBpm':
|
|
@@ -135,12 +146,14 @@ export function installOfflineWorkerEndpoint(endpoint: OfflineWorkerEndpoint): v
|
|
|
135
146
|
result = masterAudio({
|
|
136
147
|
...(message.request as unknown as MasterAudioRequest),
|
|
137
148
|
onProgress,
|
|
149
|
+
cancel: isCancelled,
|
|
138
150
|
});
|
|
139
151
|
break;
|
|
140
152
|
case 'masterAudioStereo':
|
|
141
153
|
result = masterAudioStereo({
|
|
142
154
|
...(message.request as unknown as MasterAudioStereoRequest),
|
|
143
155
|
onProgress,
|
|
156
|
+
cancel: isCancelled,
|
|
144
157
|
});
|
|
145
158
|
break;
|
|
146
159
|
}
|
|
@@ -159,13 +172,16 @@ export function installOfflineWorkerEndpoint(endpoint: OfflineWorkerEndpoint): v
|
|
|
159
172
|
});
|
|
160
173
|
} finally {
|
|
161
174
|
cancelled.delete(message.id);
|
|
175
|
+
inFlight.delete(message.id);
|
|
162
176
|
}
|
|
163
177
|
};
|
|
164
178
|
|
|
165
179
|
endpoint.addEventListener('message', (event) => {
|
|
166
180
|
const message = event.data;
|
|
167
181
|
if (message.type === 'sonare:offline-cancel') {
|
|
168
|
-
|
|
182
|
+
if (inFlight.has(message.id)) {
|
|
183
|
+
cancelled.add(message.id);
|
|
184
|
+
}
|
|
169
185
|
return;
|
|
170
186
|
}
|
|
171
187
|
void run(message);
|
|
@@ -1,2 +1,39 @@
|
|
|
1
1
|
export type WorkletInput = readonly (readonly Float32Array[])[];
|
|
2
2
|
export type WorkletOutput = Float32Array[][];
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Copies one plane per output channel. Shared by every worklet output — the
|
|
6
|
+
* engine's program and cue buses and the voice changer — so they cannot drift
|
|
7
|
+
* in their padding behaviour. Allocation-free.
|
|
8
|
+
*
|
|
9
|
+
* A host output wider than the engine's plane count is mapped the way Web Audio
|
|
10
|
+
* up-mixes rather than by padding with a copy of plane 0:
|
|
11
|
+
* - A single plane fans out to every output channel, so a mono engine driving a
|
|
12
|
+
* stereo host stays centred instead of hard-panned left.
|
|
13
|
+
* - With two or more planes, an output channel past the last plane is silence.
|
|
14
|
+
* Duplicating plane 0 into it would put the left signal in a rear or centre
|
|
15
|
+
* channel and add correlated energy the engine never produced.
|
|
16
|
+
*
|
|
17
|
+
* Everything the source does not fill is zeroed — the tail past `frames`, and
|
|
18
|
+
* the remainder of a plane shorter than `frames` — so no sample of the previous
|
|
19
|
+
* block survives into this one.
|
|
20
|
+
*/
|
|
21
|
+
export function copyPlanesToOutput(
|
|
22
|
+
output: Float32Array[],
|
|
23
|
+
planes: readonly Float32Array[],
|
|
24
|
+
frames: number,
|
|
25
|
+
): void {
|
|
26
|
+
const monoFanOut = planes.length === 1;
|
|
27
|
+
for (let ch = 0; ch < output.length; ch++) {
|
|
28
|
+
const target = output[ch];
|
|
29
|
+
const source = monoFanOut ? planes[0] : planes[ch];
|
|
30
|
+
let copied = 0;
|
|
31
|
+
if (source) {
|
|
32
|
+
copied = Math.min(target.length, frames, source.length);
|
|
33
|
+
target.set(source.subarray(0, copied));
|
|
34
|
+
}
|
|
35
|
+
if (copied < target.length) {
|
|
36
|
+
target.fill(0, copied);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|