@libraz/libsonare 1.5.3 → 1.5.5
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/README.md +69 -740
- package/dist/index.d.ts +131 -39
- package/dist/index.js +247 -66
- package/dist/index.js.map +1 -1
- package/dist/sonare.js +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/worklet.d.ts +23 -1
- package/dist/worklet.js +109 -8
- package/dist/worklet.js.map +1 -1
- package/package.json +14 -3
- package/src/_chain_config.ts +0 -20
- package/src/analysis_helpers.ts +23 -0
- package/src/audio.ts +5 -3
- package/src/codes.ts +27 -4
- package/src/effects_transform.ts +32 -20
- package/src/feature_core.ts +19 -1
- package/src/feature_music.ts +32 -18
- package/src/feature_pitch.ts +5 -5
- package/src/feature_spectral.ts +3 -3
- package/src/feature_spectrogram.ts +27 -5
- package/src/index.ts +3 -0
- package/src/mastering_core.ts +2 -0
- package/src/metering.ts +49 -6
- package/src/mixer.ts +14 -1
- package/src/project_class.ts +25 -5
- package/src/project_internal.ts +2 -0
- package/src/public_types_mastering.ts +6 -0
- package/src/public_types_music.ts +4 -0
- package/src/public_types_spectral.ts +10 -3
- package/src/quick_analysis.ts +23 -5
- package/src/realtime_engine.ts +9 -1
- package/src/realtime_voice_changer.ts +79 -3
- package/src/sonare.js.d.ts +33 -3
- package/src/streaming_processors.ts +7 -1
- package/src/validation.ts +6 -0
- package/src/worklet/messages.ts +16 -2
- package/src/worklet/protocol.ts +2 -0
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@libraz/libsonare",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "yarn@4.15.0",
|
|
6
|
-
"description": "Audio analysis
|
|
6
|
+
"description": "Audio analysis, mastering, mixing, and MIDI synthesis in WebAssembly",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
9
9
|
"exports": {
|
|
@@ -47,6 +47,17 @@
|
|
|
47
47
|
"bpm",
|
|
48
48
|
"key",
|
|
49
49
|
"tempo",
|
|
50
|
+
"chords",
|
|
51
|
+
"audio-processing",
|
|
52
|
+
"dsp",
|
|
53
|
+
"mastering",
|
|
54
|
+
"mixing",
|
|
55
|
+
"loudness",
|
|
56
|
+
"lufs",
|
|
57
|
+
"midi",
|
|
58
|
+
"synthesizer",
|
|
59
|
+
"soundfont",
|
|
60
|
+
"room-acoustics",
|
|
50
61
|
"wasm",
|
|
51
62
|
"webassembly"
|
|
52
63
|
],
|
|
@@ -59,7 +70,7 @@
|
|
|
59
70
|
"bugs": {
|
|
60
71
|
"url": "https://github.com/libraz/libsonare/issues"
|
|
61
72
|
},
|
|
62
|
-
"homepage": "https://
|
|
73
|
+
"homepage": "https://libsonare.libraz.net",
|
|
63
74
|
"engines": {
|
|
64
75
|
"node": ">=18.0.0"
|
|
65
76
|
},
|
package/src/_chain_config.ts
CHANGED
|
@@ -22,25 +22,5 @@ export function flattenChainConfig(config: MasteringChainConfig): Record<string,
|
|
|
22
22
|
};
|
|
23
23
|
walk(config as ChainSection, '');
|
|
24
24
|
|
|
25
|
-
// Compatibility aliases for the original WASM-only shorthand. Normalize at
|
|
26
|
-
// this boundary so every native entry point receives the core parser's one
|
|
27
|
-
// canonical vocabulary; public types can migrate to the nested spelling
|
|
28
|
-
// without preserving a second C++ parser indefinitely.
|
|
29
|
-
const aliases: Record<string, string> = {
|
|
30
|
-
'repair.denoise': 'repair.denoise.enabled',
|
|
31
|
-
'repair.nFft': 'repair.denoise.nFft',
|
|
32
|
-
'repair.hopLength': 'repair.denoise.hopLength',
|
|
33
|
-
'repair.ddAlpha': 'repair.denoise.ddAlpha',
|
|
34
|
-
'repair.gainFloor': 'repair.denoise.gainFloor',
|
|
35
|
-
'eq.tiltDb': 'eq.tilt.tiltDb',
|
|
36
|
-
'eq.pivotHz': 'eq.tilt.pivotHz',
|
|
37
|
-
};
|
|
38
|
-
for (const [legacy, canonical] of Object.entries(aliases)) {
|
|
39
|
-
const value = out[legacy];
|
|
40
|
-
if (value !== undefined) {
|
|
41
|
-
out[canonical] = value;
|
|
42
|
-
delete out[legacy];
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
25
|
return out;
|
|
46
26
|
}
|
package/src/analysis_helpers.ts
CHANGED
|
@@ -16,6 +16,25 @@ import type {
|
|
|
16
16
|
WasmKeyCandidateResult,
|
|
17
17
|
} from './sonare.js';
|
|
18
18
|
|
|
19
|
+
const PITCH_CLASS_NAMES = [
|
|
20
|
+
'C',
|
|
21
|
+
'C#',
|
|
22
|
+
'D',
|
|
23
|
+
'D#',
|
|
24
|
+
'E',
|
|
25
|
+
'F',
|
|
26
|
+
'F#',
|
|
27
|
+
'G',
|
|
28
|
+
'G#',
|
|
29
|
+
'A',
|
|
30
|
+
'A#',
|
|
31
|
+
'B',
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
function pitchClassName(value: number): string {
|
|
35
|
+
return PITCH_CLASS_NAMES[value] ?? 'C';
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
export function convertKeyCandidate(wasm: WasmKeyCandidateResult): KeyCandidate {
|
|
20
39
|
return {
|
|
21
40
|
key: {
|
|
@@ -89,6 +108,8 @@ export function convertChordAnalysisResult(wasm: WasmChordAnalysisResult): Chord
|
|
|
89
108
|
chords: wasm.chords.map((c) => ({
|
|
90
109
|
root: c.root as PitchClass,
|
|
91
110
|
bass: c.bass as PitchClass,
|
|
111
|
+
rootName: pitchClassName(c.root),
|
|
112
|
+
bassName: pitchClassName(c.bass),
|
|
92
113
|
quality: c.quality as ChordQuality,
|
|
93
114
|
start: c.start,
|
|
94
115
|
end: c.end,
|
|
@@ -129,6 +150,8 @@ export function convertAnalysisResult(wasm: WasmAnalysisResult): AnalysisResult
|
|
|
129
150
|
chords: wasm.chords.map((c) => ({
|
|
130
151
|
root: c.root as PitchClass,
|
|
131
152
|
bass: c.bass as PitchClass,
|
|
153
|
+
rootName: pitchClassName(c.root),
|
|
154
|
+
bassName: pitchClassName(c.bass),
|
|
132
155
|
quality: c.quality as ChordQuality,
|
|
133
156
|
start: c.start,
|
|
134
157
|
end: c.end,
|
package/src/audio.ts
CHANGED
|
@@ -76,6 +76,7 @@ import {
|
|
|
76
76
|
detectOnsets,
|
|
77
77
|
} from './quick_analysis';
|
|
78
78
|
import type { ProgressCallback, WasmNnlsChromaResult } from './sonare.js';
|
|
79
|
+
import { validateAudioBuffer } from './validation';
|
|
79
80
|
|
|
80
81
|
// ============================================================================
|
|
81
82
|
// Audio Class
|
|
@@ -177,7 +178,8 @@ export class Audio {
|
|
|
177
178
|
* Node/Python surfaces).
|
|
178
179
|
*/
|
|
179
180
|
static fromBuffer(samples: Float32Array, sampleRate = 48000): Audio {
|
|
180
|
-
|
|
181
|
+
validateAudioBuffer(samples, sampleRate);
|
|
182
|
+
return new Audio(samples.slice(), sampleRate);
|
|
181
183
|
}
|
|
182
184
|
|
|
183
185
|
/**
|
|
@@ -472,7 +474,7 @@ export class Audio {
|
|
|
472
474
|
hopLength = 512,
|
|
473
475
|
fmin = 65.0,
|
|
474
476
|
fmax = 2093.0,
|
|
475
|
-
threshold = 0.
|
|
477
|
+
threshold = 0.1,
|
|
476
478
|
fillNa = false,
|
|
477
479
|
): PitchResult {
|
|
478
480
|
return pitchYin(
|
|
@@ -492,7 +494,7 @@ export class Audio {
|
|
|
492
494
|
hopLength = 512,
|
|
493
495
|
fmin = 65.0,
|
|
494
496
|
fmax = 2093.0,
|
|
495
|
-
threshold = 0.
|
|
497
|
+
threshold = 0.1,
|
|
496
498
|
fillNa = false,
|
|
497
499
|
): PitchResult {
|
|
498
500
|
return pitchPyin(
|
package/src/codes.ts
CHANGED
|
@@ -20,6 +20,8 @@ export function panLawCode(panLaw: PanLaw | number): number {
|
|
|
20
20
|
return panLaw;
|
|
21
21
|
}
|
|
22
22
|
switch (panLaw) {
|
|
23
|
+
case 'const3dB':
|
|
24
|
+
return 0;
|
|
23
25
|
case 'const4.5dB':
|
|
24
26
|
return 1;
|
|
25
27
|
case 'const6dB':
|
|
@@ -27,7 +29,7 @@ export function panLawCode(panLaw: PanLaw | number): number {
|
|
|
27
29
|
case 'linear0dB':
|
|
28
30
|
return 3;
|
|
29
31
|
default:
|
|
30
|
-
|
|
32
|
+
throw new Error(`Invalid pan law: ${panLaw}`);
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -36,6 +38,8 @@ export function panModeCode(panMode: PanMode | number): number {
|
|
|
36
38
|
return panMode;
|
|
37
39
|
}
|
|
38
40
|
switch (panMode) {
|
|
41
|
+
case 'balance':
|
|
42
|
+
return 0;
|
|
39
43
|
case 'stereoPan':
|
|
40
44
|
case 'stereo-pan':
|
|
41
45
|
return 1;
|
|
@@ -43,19 +47,38 @@ export function panModeCode(panMode: PanMode | number): number {
|
|
|
43
47
|
case 'dual-pan':
|
|
44
48
|
return 2;
|
|
45
49
|
default:
|
|
46
|
-
|
|
50
|
+
throw new Error(`Invalid pan mode: ${panMode}`);
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
export function meterTapCode(tap: MeterTap | number): number {
|
|
51
|
-
|
|
55
|
+
if (typeof tap === 'number') {
|
|
56
|
+
return tap;
|
|
57
|
+
}
|
|
58
|
+
switch (tap) {
|
|
59
|
+
case 'preFader':
|
|
60
|
+
return 0;
|
|
61
|
+
case 'postFader':
|
|
62
|
+
return 1;
|
|
63
|
+
default:
|
|
64
|
+
throw new Error(`Invalid meter tap: ${tap}`);
|
|
65
|
+
}
|
|
52
66
|
}
|
|
53
67
|
|
|
54
68
|
export function sendTimingCode(timing: SendTiming | number): number {
|
|
55
69
|
// Mirrors SonareSendTiming: post-fader is 0 (so an omitted/zeroed value is
|
|
56
70
|
// post-fader), pre-fader is 1. A raw number is passed through as the C ABI int.
|
|
71
|
+
// An unknown string is rejected rather than silently routed to post-fader,
|
|
72
|
+
// matching the sibling enum-code helpers and Node's sendTimingValue.
|
|
57
73
|
if (typeof timing === 'number') {
|
|
58
74
|
return timing;
|
|
59
75
|
}
|
|
60
|
-
|
|
76
|
+
switch (timing) {
|
|
77
|
+
case 'postFader':
|
|
78
|
+
return 0;
|
|
79
|
+
case 'preFader':
|
|
80
|
+
return 1;
|
|
81
|
+
default:
|
|
82
|
+
throw new Error(`Invalid send timing: ${timing}`);
|
|
83
|
+
}
|
|
61
84
|
}
|
package/src/effects_transform.ts
CHANGED
|
@@ -34,13 +34,13 @@ export interface PercussiveRequest extends ValidateOptions {
|
|
|
34
34
|
|
|
35
35
|
export interface TimeStretchRequest extends ValidateOptions {
|
|
36
36
|
samples: Float32Array;
|
|
37
|
-
sampleRate
|
|
37
|
+
sampleRate?: number;
|
|
38
38
|
rate: number;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export interface PitchShiftRequest extends ValidateOptions {
|
|
42
42
|
samples: Float32Array;
|
|
43
|
-
sampleRate
|
|
43
|
+
sampleRate?: number;
|
|
44
44
|
semitones: number;
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -79,7 +79,7 @@ export interface NoteMoveRequest extends NoteMoveOptions, ValidateOptions {
|
|
|
79
79
|
|
|
80
80
|
export interface NormalizeRequest extends ValidateOptions {
|
|
81
81
|
samples: Float32Array;
|
|
82
|
-
sampleRate
|
|
82
|
+
sampleRate?: number;
|
|
83
83
|
targetDb?: number;
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -177,7 +177,7 @@ export function percussive(
|
|
|
177
177
|
* Time-stretch audio without changing pitch.
|
|
178
178
|
*
|
|
179
179
|
* @param samples - Audio samples (mono, float32)
|
|
180
|
-
* @param sampleRate - Sample rate in Hz
|
|
180
|
+
* @param sampleRate - Sample rate in Hz (default: 22050)
|
|
181
181
|
* @param rate - Time stretch rate (0.5 = double duration, 2.0 = half duration)
|
|
182
182
|
* @returns Time-stretched audio
|
|
183
183
|
*/
|
|
@@ -196,17 +196,17 @@ export function timeStretch(
|
|
|
196
196
|
): Float32Array {
|
|
197
197
|
const request: TimeStretchRequest =
|
|
198
198
|
samples instanceof Float32Array
|
|
199
|
-
? { samples, sampleRate
|
|
199
|
+
? { samples, sampleRate, rate: rate as number, ...options }
|
|
200
200
|
: samples;
|
|
201
201
|
assertSamples('timeStretch', request.samples, request.validate !== false);
|
|
202
|
-
return requireModule().timeStretch(request.samples, request.sampleRate, request.rate);
|
|
202
|
+
return requireModule().timeStretch(request.samples, request.sampleRate ?? 22050, request.rate);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
/**
|
|
206
206
|
* Pitch-shift audio without changing duration.
|
|
207
207
|
*
|
|
208
208
|
* @param samples - Audio samples (mono, float32)
|
|
209
|
-
* @param sampleRate - Sample rate in Hz
|
|
209
|
+
* @param sampleRate - Sample rate in Hz (default: 22050)
|
|
210
210
|
* @param semitones - Pitch shift in semitones (+12 = one octave up, -12 = one octave down)
|
|
211
211
|
* @returns Pitch-shifted audio
|
|
212
212
|
*/
|
|
@@ -225,15 +225,23 @@ export function pitchShift(
|
|
|
225
225
|
): Float32Array {
|
|
226
226
|
const request: PitchShiftRequest =
|
|
227
227
|
samples instanceof Float32Array
|
|
228
|
-
? { samples, sampleRate
|
|
228
|
+
? { samples, sampleRate, semitones: semitones as number, ...options }
|
|
229
229
|
: samples;
|
|
230
230
|
assertSamples('pitchShift', request.samples, request.validate !== false);
|
|
231
|
-
return requireModule().pitchShift(
|
|
231
|
+
return requireModule().pitchShift(
|
|
232
|
+
request.samples,
|
|
233
|
+
request.sampleRate ?? 22050,
|
|
234
|
+
request.semitones,
|
|
235
|
+
);
|
|
232
236
|
}
|
|
233
237
|
|
|
234
238
|
/**
|
|
235
239
|
* Pitch-correct audio from a current MIDI note to a target MIDI note.
|
|
236
240
|
*
|
|
241
|
+
* Applies one constant, immediate transpose with no retune glide and preserves
|
|
242
|
+
* the input buffer length. Use {@link pitchCorrectToMidiTimevarying} for a
|
|
243
|
+
* caller-supplied pitch contour.
|
|
244
|
+
*
|
|
237
245
|
* @param samples - Audio samples (mono, float32)
|
|
238
246
|
* @param sampleRate - Sample rate in Hz
|
|
239
247
|
* @param currentMidi - Detected/current MIDI note number
|
|
@@ -275,7 +283,8 @@ export function pitchCorrectToMidi(
|
|
|
275
283
|
* the caller-supplied per-frame `f0Hz` contour and retunes every voiced frame
|
|
276
284
|
* toward `targetMidi`, so vibrato/drift in the source is tracked rather than
|
|
277
285
|
* flattened. `voiced` (non-zero = voiced) and `voicedProb` ([0,1]) are optional;
|
|
278
|
-
* omitting them treats every frame as voiced.
|
|
286
|
+
* omitting them treats every frame as voiced. An `f0Hz` NaN is accepted only
|
|
287
|
+
* when the corresponding `voiced` entry is zero, matching pYIN output.
|
|
279
288
|
*
|
|
280
289
|
* @param samples - Audio samples (mono, float32)
|
|
281
290
|
* @param f0Hz - Per-frame measured F0 in Hz (one entry per analysis frame)
|
|
@@ -350,7 +359,8 @@ export function pitchCorrectToMidiTimevarying(
|
|
|
350
359
|
* Generalises {@link pitchCorrectToMidiTimevarying}: the same caller-supplied
|
|
351
360
|
* per-frame `f0Hz` contour drives correction, but `options.mode` selects between
|
|
352
361
|
* a fixed-MIDI target (`'midi'`, default) and scale quantisation (`'scale'`),
|
|
353
|
-
* and the retune knobs shape natural-vs-robotic correction.
|
|
362
|
+
* and the retune knobs shape natural-vs-robotic correction. An `f0Hz` NaN is
|
|
363
|
+
* accepted only for a frame marked unvoiced.
|
|
354
364
|
*
|
|
355
365
|
* @param samples - Audio samples (mono, float32)
|
|
356
366
|
* @param f0Hz - Per-frame measured F0 in Hz (one entry per analysis frame)
|
|
@@ -407,7 +417,7 @@ export function pitchCorrectTimevarying(
|
|
|
407
417
|
* @param sampleRate - Sample rate in Hz
|
|
408
418
|
* @param onsetSample - Note onset position in samples
|
|
409
419
|
* @param offsetSample - Note offset position in samples
|
|
410
|
-
* @param stretchRatio - Stretch ratio (0.5 =
|
|
420
|
+
* @param stretchRatio - Stretch ratio (0.5 = half duration, 2.0 = double duration)
|
|
411
421
|
* @returns Audio with the note region stretched
|
|
412
422
|
*/
|
|
413
423
|
export function noteStretch(request: NoteStretchRequest): Float32Array;
|
|
@@ -427,7 +437,7 @@ export function noteStretch(
|
|
|
427
437
|
request.samples,
|
|
428
438
|
request.sampleRate ?? 22050,
|
|
429
439
|
request.onsetSample ?? 0,
|
|
430
|
-
request.offsetSample ??
|
|
440
|
+
request.offsetSample ?? request.samples.length,
|
|
431
441
|
request.stretchRatio ?? 1.0,
|
|
432
442
|
);
|
|
433
443
|
}
|
|
@@ -450,7 +460,7 @@ export function noteMove(
|
|
|
450
460
|
request.samples,
|
|
451
461
|
request.sampleRate ?? 22050,
|
|
452
462
|
request.onsetSample ?? 0,
|
|
453
|
-
request.offsetSample ??
|
|
463
|
+
request.offsetSample ?? request.samples.length,
|
|
454
464
|
request.targetOnsetSample ?? 0,
|
|
455
465
|
);
|
|
456
466
|
}
|
|
@@ -459,8 +469,8 @@ export function noteMove(
|
|
|
459
469
|
* Normalize audio to target peak level.
|
|
460
470
|
*
|
|
461
471
|
* @param samples - Audio samples (mono, float32)
|
|
462
|
-
* @param sampleRate - Sample rate in Hz
|
|
463
|
-
* @param targetDb -
|
|
472
|
+
* @param sampleRate - Sample rate in Hz (default: 22050)
|
|
473
|
+
* @param targetDb - Finite target at or below 0 dBFS (default: 0 dB = full scale)
|
|
464
474
|
* @returns Normalized audio
|
|
465
475
|
*/
|
|
466
476
|
export function normalize(request: NormalizeRequest): Float32Array;
|
|
@@ -477,11 +487,13 @@ export function normalize(
|
|
|
477
487
|
options: ValidateOptions = {},
|
|
478
488
|
): Float32Array {
|
|
479
489
|
const request: NormalizeRequest =
|
|
480
|
-
samples instanceof Float32Array
|
|
481
|
-
? { samples, sampleRate: sampleRate as number, targetDb, ...options }
|
|
482
|
-
: samples;
|
|
490
|
+
samples instanceof Float32Array ? { samples, sampleRate, targetDb, ...options } : samples;
|
|
483
491
|
assertSamples('normalize', request.samples, request.validate !== false);
|
|
484
|
-
return requireModule().normalize(
|
|
492
|
+
return requireModule().normalize(
|
|
493
|
+
request.samples,
|
|
494
|
+
request.sampleRate ?? 22050,
|
|
495
|
+
request.targetDb ?? 0.0,
|
|
496
|
+
);
|
|
485
497
|
}
|
|
486
498
|
|
|
487
499
|
/**
|
package/src/feature_core.ts
CHANGED
|
@@ -81,6 +81,14 @@ export interface PcenRequest {
|
|
|
81
81
|
values: Float32Array;
|
|
82
82
|
nBins: number;
|
|
83
83
|
nFrames: number;
|
|
84
|
+
sampleRate?: number;
|
|
85
|
+
hopLength?: number;
|
|
86
|
+
timeConstant?: number;
|
|
87
|
+
gain?: number;
|
|
88
|
+
bias?: number;
|
|
89
|
+
power?: number;
|
|
90
|
+
eps?: number;
|
|
91
|
+
/** @deprecated Put PCEN fields directly on the request object. */
|
|
84
92
|
options?: Record<string, number>;
|
|
85
93
|
}
|
|
86
94
|
export interface TempogramRequest {
|
|
@@ -449,7 +457,17 @@ export function pcen(
|
|
|
449
457
|
): Float32Array {
|
|
450
458
|
if (!(values instanceof Float32Array)) {
|
|
451
459
|
const r = values;
|
|
452
|
-
|
|
460
|
+
const {
|
|
461
|
+
values: requestValues,
|
|
462
|
+
nBins: requestBins,
|
|
463
|
+
nFrames: requestFrames,
|
|
464
|
+
options: legacyOptions,
|
|
465
|
+
...flatOptions
|
|
466
|
+
} = r;
|
|
467
|
+
return pcen(requestValues, requestBins, requestFrames, {
|
|
468
|
+
...legacyOptions,
|
|
469
|
+
...flatOptions,
|
|
470
|
+
});
|
|
453
471
|
}
|
|
454
472
|
return requireModule().pcen(values, nBins, nFrames, options);
|
|
455
473
|
}
|
package/src/feature_music.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ErrorCode, SonareError } from './errors';
|
|
1
2
|
import { getSonareModule } from './module_state';
|
|
2
3
|
import type {
|
|
3
4
|
AnalyzeSectionsOptions,
|
|
@@ -97,6 +98,9 @@ export interface LufsRequest extends ValidateOptions {
|
|
|
97
98
|
export interface NnlsChromaRequest extends GuardedOptions {
|
|
98
99
|
samples: Float32Array;
|
|
99
100
|
sampleRate?: number;
|
|
101
|
+
enableStftBlend?: boolean;
|
|
102
|
+
stftBlendWeight?: number;
|
|
103
|
+
stftBlendNFft?: number;
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
function validateMusicSamples(
|
|
@@ -139,18 +143,24 @@ export function nnlsChroma(request: NnlsChromaRequest): WasmNnlsChromaResult;
|
|
|
139
143
|
export function nnlsChroma(
|
|
140
144
|
samples: Float32Array,
|
|
141
145
|
sampleRate?: number,
|
|
142
|
-
options?:
|
|
146
|
+
options?: Omit<NnlsChromaRequest, 'samples' | 'sampleRate'>,
|
|
143
147
|
): WasmNnlsChromaResult;
|
|
144
148
|
export function nnlsChroma(
|
|
145
149
|
samples: Float32Array | NnlsChromaRequest,
|
|
146
150
|
sampleRate = 22050,
|
|
147
|
-
options:
|
|
151
|
+
options: Omit<NnlsChromaRequest, 'samples' | 'sampleRate'> = {},
|
|
148
152
|
): WasmNnlsChromaResult {
|
|
149
153
|
if (!(samples instanceof Float32Array)) {
|
|
150
154
|
return nnlsChroma(samples.samples, samples.sampleRate, samples);
|
|
151
155
|
}
|
|
152
156
|
validateMusicSamples('nnlsChroma', samples, sampleRate, options);
|
|
153
|
-
return requireModule().nnlsChroma(
|
|
157
|
+
return requireModule().nnlsChroma(
|
|
158
|
+
samples,
|
|
159
|
+
sampleRate,
|
|
160
|
+
options.enableStftBlend ?? true,
|
|
161
|
+
options.stftBlendWeight ?? 0.55,
|
|
162
|
+
options.stftBlendNFft ?? 4096,
|
|
163
|
+
);
|
|
154
164
|
}
|
|
155
165
|
|
|
156
166
|
/**
|
|
@@ -306,7 +316,8 @@ export function hybridCqt(
|
|
|
306
316
|
* @param fmin - Minimum frequency in Hz (default: 32.70319566257483, C1)
|
|
307
317
|
* @param nBins - Number of frequency bins (default: 84)
|
|
308
318
|
* @param binsPerOctave - Bins per octave (default: 12)
|
|
309
|
-
* @param gamma - Bandwidth offset;
|
|
319
|
+
* @param gamma - Bandwidth offset; negative selects the automatic ERB-derived
|
|
320
|
+
* value, while 0 is equivalent to CQT (default: -1)
|
|
310
321
|
* @returns VQT magnitude result (same shape as CQT)
|
|
311
322
|
*/
|
|
312
323
|
export function vqt(request: VqtRequest): CqtResult;
|
|
@@ -327,7 +338,7 @@ export function vqt(
|
|
|
327
338
|
fmin = 32.70319566257483,
|
|
328
339
|
nBins = 84,
|
|
329
340
|
binsPerOctave = 12,
|
|
330
|
-
gamma =
|
|
341
|
+
gamma = -1,
|
|
331
342
|
options: GuardedOptions = {},
|
|
332
343
|
): CqtResult {
|
|
333
344
|
if (!(samples instanceof Float32Array)) {
|
|
@@ -347,9 +358,6 @@ export function vqt(
|
|
|
347
358
|
validatePositiveIntegers('vqt', { hopLength, nBins, binsPerOctave });
|
|
348
359
|
validateFrequencyBounds('vqt', fmin);
|
|
349
360
|
assertFiniteScalar('vqt', gamma, 'gamma');
|
|
350
|
-
if (gamma < 0) {
|
|
351
|
-
throw new RangeError('vqt: gamma must be non-negative');
|
|
352
|
-
}
|
|
353
361
|
return requireModule().vqt(samples, sampleRate, hopLength, fmin, nBins, binsPerOctave, gamma);
|
|
354
362
|
}
|
|
355
363
|
|
|
@@ -464,7 +472,7 @@ export function vqtToAudio(
|
|
|
464
472
|
hopLength = 512,
|
|
465
473
|
fmin = 32.70319566257483,
|
|
466
474
|
binsPerOctave = 12,
|
|
467
|
-
gamma =
|
|
475
|
+
gamma = -1,
|
|
468
476
|
nIter = 32,
|
|
469
477
|
options: GuardedOptions = {},
|
|
470
478
|
): Float32Array {
|
|
@@ -496,9 +504,6 @@ export function vqtToAudio(
|
|
|
496
504
|
options,
|
|
497
505
|
);
|
|
498
506
|
assertFiniteScalar('vqtToAudio', gamma, 'gamma');
|
|
499
|
-
if (gamma < 0) {
|
|
500
|
-
throw new RangeError('vqtToAudio: gamma must be non-negative');
|
|
501
|
-
}
|
|
502
507
|
return requireModule().vqtToAudio(
|
|
503
508
|
magnitude,
|
|
504
509
|
nBins,
|
|
@@ -543,8 +548,8 @@ export function analyzeSections(
|
|
|
543
548
|
hopLength: options.hopLength ?? 512,
|
|
544
549
|
});
|
|
545
550
|
assertFiniteScalar('analyzeSections', options.minSectionSec ?? 4.0, 'minSectionSec');
|
|
546
|
-
if ((options.minSectionSec ?? 4.0)
|
|
547
|
-
throw new RangeError('analyzeSections: minSectionSec must be
|
|
551
|
+
if ((options.minSectionSec ?? 4.0) < 0) {
|
|
552
|
+
throw new RangeError('analyzeSections: minSectionSec must be non-negative');
|
|
548
553
|
}
|
|
549
554
|
// The embind value marshalling returns an array whose constructor is not this
|
|
550
555
|
// realm's Array; chaining .map() onto it propagates that constructor via
|
|
@@ -614,10 +619,15 @@ export function analyzeMelody(
|
|
|
614
619
|
const fmax = options.fmax ?? 2093.0;
|
|
615
620
|
validateFrequencyBounds('analyzeMelody', fmin, fmax);
|
|
616
621
|
// The melody tracker's fmin is a YIN pitch floor: 0 is meaningless, and the
|
|
617
|
-
// flat C ABI (sonare_analyze_melody) rejects it
|
|
618
|
-
// guards fmin >= 0, so enforce strict positivity
|
|
622
|
+
// flat C ABI (sonare_analyze_melody) rejects it with InvalidParameter.
|
|
623
|
+
// validateFrequencyBounds only guards fmin >= 0, so enforce strict positivity
|
|
624
|
+
// here and report the same branded error the Node/Python surfaces raise.
|
|
619
625
|
if (fmin <= 0) {
|
|
620
|
-
throw new
|
|
626
|
+
throw new SonareError(
|
|
627
|
+
ErrorCode.InvalidParameter,
|
|
628
|
+
'InvalidParameter',
|
|
629
|
+
'analyzeMelody: fmin must be positive',
|
|
630
|
+
);
|
|
621
631
|
}
|
|
622
632
|
validatePositiveIntegers('analyzeMelody', {
|
|
623
633
|
frameLength: options.frameLength ?? 2048,
|
|
@@ -626,7 +636,11 @@ export function analyzeMelody(
|
|
|
626
636
|
const threshold = options.threshold ?? 0.1;
|
|
627
637
|
assertFiniteScalar('analyzeMelody', threshold, 'threshold');
|
|
628
638
|
if (threshold <= 0) {
|
|
629
|
-
throw new
|
|
639
|
+
throw new SonareError(
|
|
640
|
+
ErrorCode.InvalidParameter,
|
|
641
|
+
'InvalidParameter',
|
|
642
|
+
'analyzeMelody: threshold must be positive',
|
|
643
|
+
);
|
|
630
644
|
}
|
|
631
645
|
return requireModule().analyzeMelody(
|
|
632
646
|
samples,
|
package/src/feature_pitch.ts
CHANGED
|
@@ -18,8 +18,8 @@ function requireModule() {
|
|
|
18
18
|
* @param hopLength - Hop length (default: 512)
|
|
19
19
|
* @param fmin - Minimum frequency in Hz (default: 65)
|
|
20
20
|
* @param fmax - Maximum frequency in Hz (default: 2093)
|
|
21
|
-
* @param threshold - YIN threshold (default: 0.
|
|
22
|
-
* @param fillNa -
|
|
21
|
+
* @param threshold - YIN threshold (default: 0.1)
|
|
22
|
+
* @param fillNa - Retained for compatibility; YIN always returns a finite per-frame estimate.
|
|
23
23
|
* @returns Pitch detection result
|
|
24
24
|
*/
|
|
25
25
|
export interface PitchYinRequest {
|
|
@@ -51,7 +51,7 @@ export function pitchYin(
|
|
|
51
51
|
hopLength = 512,
|
|
52
52
|
fmin = 65.0,
|
|
53
53
|
fmax = 2093.0,
|
|
54
|
-
threshold = 0.
|
|
54
|
+
threshold = 0.1,
|
|
55
55
|
fillNa = false,
|
|
56
56
|
): PitchResult {
|
|
57
57
|
if (!(samples instanceof Float32Array)) {
|
|
@@ -88,7 +88,7 @@ export function pitchYin(
|
|
|
88
88
|
* @param hopLength - Hop length (default: 512)
|
|
89
89
|
* @param fmin - Minimum frequency in Hz (default: 65)
|
|
90
90
|
* @param fmax - Maximum frequency in Hz (default: 2093)
|
|
91
|
-
* @param threshold - YIN threshold (default: 0.
|
|
91
|
+
* @param threshold - YIN threshold (default: 0.1)
|
|
92
92
|
* @param fillNa - If true, return 0 for unvoiced f0 frames; otherwise keep NaN (default: false)
|
|
93
93
|
* @returns Pitch detection result
|
|
94
94
|
*/
|
|
@@ -112,7 +112,7 @@ export function pitchPyin(
|
|
|
112
112
|
hopLength = 512,
|
|
113
113
|
fmin = 65.0,
|
|
114
114
|
fmax = 2093.0,
|
|
115
|
-
threshold = 0.
|
|
115
|
+
threshold = 0.1,
|
|
116
116
|
fillNa = false,
|
|
117
117
|
): PitchResult {
|
|
118
118
|
if (!(samples instanceof Float32Array)) {
|
package/src/feature_spectral.ts
CHANGED
|
@@ -438,21 +438,21 @@ export function remix(
|
|
|
438
438
|
export function phaseVocoder(request: PhaseVocoderRequest): Float32Array;
|
|
439
439
|
export function phaseVocoder(
|
|
440
440
|
samples: Float32Array,
|
|
441
|
+
sampleRate: number,
|
|
441
442
|
rate: number,
|
|
442
|
-
sampleRate?: number,
|
|
443
443
|
nFft?: number,
|
|
444
444
|
hopLength?: number,
|
|
445
445
|
): Float32Array;
|
|
446
446
|
export function phaseVocoder(
|
|
447
447
|
samples: Float32Array | PhaseVocoderRequest,
|
|
448
|
-
rate = 1,
|
|
449
448
|
sampleRate = 22050,
|
|
449
|
+
rate = 1,
|
|
450
450
|
nFft = 2048,
|
|
451
451
|
hopLength = 512,
|
|
452
452
|
): Float32Array {
|
|
453
453
|
if (!(samples instanceof Float32Array)) {
|
|
454
454
|
const r = samples;
|
|
455
|
-
return phaseVocoder(r.samples, r.
|
|
455
|
+
return phaseVocoder(r.samples, r.sampleRate ?? 22050, r.rate, r.nFft, r.hopLength);
|
|
456
456
|
}
|
|
457
457
|
return requireModule().phaseVocoder(samples, sampleRate, rate, nFft, hopLength);
|
|
458
458
|
}
|