@libraz/libsonare 1.6.0 → 1.7.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/README.md +2 -2
- package/dist/analysis.d.ts +139 -11
- package/dist/analysis.js +162 -42
- package/dist/analysis.js.map +1 -1
- package/dist/index.d.ts +211 -39
- package/dist/index.js +501 -142
- package/dist/index.js.map +1 -1
- package/dist/schemas/realtime-voice-changer-preset.schema.json +2 -2
- package/dist/sonare-analysis.js +1 -1
- package/dist/sonare-analysis.wasm +0 -0
- package/dist/sonare.js +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/worker.js +56 -29
- package/dist/worker.js.map +1 -1
- package/dist/worklet.d.ts +67 -18
- package/dist/worklet.js +193 -42
- package/dist/worklet.js.map +1 -1
- package/package.json +3 -3
- package/src/_chain_config.ts +6 -1
- package/src/analysis_helpers.ts +34 -29
- package/src/audio.ts +7 -2
- package/src/codes.ts +47 -11
- package/src/effects_mastering.ts +5 -0
- package/src/effects_transform.ts +202 -22
- package/src/feature_music.ts +8 -1
- package/src/feature_spectral.ts +64 -2
- package/src/feature_spectrogram.ts +44 -3
- package/src/index.ts +28 -3
- package/src/mastering_core.ts +70 -0
- package/src/metering.ts +18 -0
- package/src/mixer.ts +3 -3
- package/src/project.ts +6 -0
- package/src/project_class.ts +143 -16
- package/src/project_internal.ts +14 -5
- package/src/project_types.ts +50 -9
- package/src/public_types_mastering.ts +9 -0
- package/src/public_types_mixing.ts +16 -0
- package/src/public_types_realtime.ts +23 -5
- package/src/public_types_spectral.ts +16 -2
- package/src/quick_analysis.ts +21 -3
- package/src/realtime_engine.ts +14 -3
- package/src/sonare.js.d.ts +87 -0
- package/src/stream_analyzer.ts +6 -1
- package/src/web_midi.ts +10 -7
- package/src/worklet/engine-node.ts +54 -19
- package/src/worklet/engine-parameter-facade.ts +30 -1
- package/src/worklet/engine-processor.ts +52 -20
- package/src/worklet/engine-register.ts +1 -1
- package/src/worklet/engine-strips.ts +2 -2
- package/src/worklet/engine.ts +10 -0
- package/src/worklet/guards.ts +99 -7
- package/src/worklet/messages.ts +41 -1
- package/src/worklet/protocol.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@libraz/libsonare",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "yarn@4.18.0",
|
|
6
6
|
"description": "Audio analysis, mastering, mixing, and MIDI synthesis in WebAssembly",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"build:wasm": "yarn build:wasm:full && yarn build:wasm:analysis",
|
|
39
39
|
"build:wasm:full": "mkdir -p build-wasm/em-cache && EM_NODE_JS=\"$(mise which node 2>/dev/null || command -v node)\" EM_CACHE=$PWD/build-wasm/em-cache emcmake cmake -S ../.. -B build-wasm -DBUILD_WASM=ON -DSONARE_WASM_ANALYSIS_ONLY=OFF -DSONARE_WASM_OUTPUT_NAME=sonare -DBUILD_MASTERING=ON -DBUILD_MIXING=ON -DBUILD_GRAPH=ON -DBUILD_FX=ON -DBUILD_ACOUSTIC_SIM=ON -DBUILD_PITCH_EDITOR=ON -DBUILD_VOICE_CHANGER=ON -DBUILD_ARRANGEMENT=ON -DBUILD_ASSIST=ON -DCMAKE_BUILD_TYPE=Release && EM_NODE_JS=\"$(mise which node 2>/dev/null || command -v node)\" EM_CACHE=$PWD/build-wasm/em-cache cmake --build build-wasm",
|
|
40
40
|
"build:wasm:analysis": "mkdir -p build-wasm-analysis/em-cache && EM_NODE_JS=\"$(mise which node 2>/dev/null || command -v node)\" EM_CACHE=$PWD/build-wasm-analysis/em-cache emcmake cmake -S ../.. -B build-wasm-analysis -DBUILD_WASM=ON -DSONARE_WASM_ANALYSIS_ONLY=ON -DSONARE_WASM_OUTPUT_NAME=sonare-analysis -DBUILD_MASTERING=OFF -DBUILD_MIXING=OFF -DBUILD_GRAPH=OFF -DBUILD_FX=OFF -DBUILD_ACOUSTIC_SIM=OFF -DBUILD_VOICE_CHANGER=OFF -DBUILD_ARRANGEMENT=OFF -DBUILD_ASSIST=OFF -DCMAKE_BUILD_TYPE=Release && EM_NODE_JS=\"$(mise which node 2>/dev/null || command -v node)\" EM_CACHE=$PWD/build-wasm-analysis/em-cache cmake --build build-wasm-analysis --target sonare",
|
|
41
|
-
"check:wasm-size": "python3 ../../tools/check_wasm_size.py --baseline wasm-size-
|
|
41
|
+
"check:wasm-size": "python3 ../../tools/check_wasm_size.py --baseline wasm-size-budget.json --artifact sonare.wasm=dist/sonare.wasm --artifact sonare-analysis.wasm=dist/sonare-analysis.wasm",
|
|
42
42
|
"bench:wasm:isp": "mkdir -p build-wasm-bench/em-cache /tmp/libsonare-wasm-bench && EM_CACHE=$PWD/build-wasm-bench/em-cache emcmake cmake -S ../.. -B build-wasm-bench -DBUILD_WASM=ON -DBUILD_MASTERING=ON -DBUILD_BENCH=ON -DBUILD_TESTING=OFF -DBUILD_CLI=OFF -DCMAKE_BUILD_TYPE=Release && EM_CACHE=$PWD/build-wasm-bench/em-cache cmake --build build-wasm-bench --target sonare_mastering_isp_bench && cp build-wasm-bench/bin/sonare_mastering_isp_bench.js /tmp/libsonare-wasm-bench/sonare_mastering_isp_bench.cjs && cp build-wasm-bench/bin/sonare_mastering_isp_bench.wasm /tmp/libsonare-wasm-bench/sonare_mastering_isp_bench.wasm && node /tmp/libsonare-wasm-bench/sonare_mastering_isp_bench.cjs",
|
|
43
43
|
"bench:wasm:phase-vocoder": "mkdir -p build-wasm-bench/em-cache /tmp/libsonare-wasm-bench && EM_CACHE=$PWD/build-wasm-bench/em-cache emcmake cmake -S ../.. -B build-wasm-bench -DBUILD_WASM=ON -DBUILD_BENCH=ON -DBUILD_TESTING=OFF -DBUILD_CLI=OFF -DCMAKE_BUILD_TYPE=Release && EM_CACHE=$PWD/build-wasm-bench/em-cache cmake --build build-wasm-bench --target sonare_phase_vocoder_streaming_bench && cp build-wasm-bench/bin/sonare_phase_vocoder_streaming_bench.js /tmp/libsonare-wasm-bench/sonare_phase_vocoder_streaming_bench.cjs && cp build-wasm-bench/bin/sonare_phase_vocoder_streaming_bench.wasm /tmp/libsonare-wasm-bench/sonare_phase_vocoder_streaming_bench.wasm && node /tmp/libsonare-wasm-bench/sonare_phase_vocoder_streaming_bench.cjs",
|
|
44
44
|
"build:js": "tsup && node scripts/copy-schemas.mjs",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"lint:fix": "biome check --write src/ tests/",
|
|
48
48
|
"format": "biome format --write src/ tests/",
|
|
49
49
|
"test": "vitest run",
|
|
50
|
-
"test:types": "tsc --noEmit --ignoreConfig --strict --module ESNext --moduleResolution bundler --target ES2020 tests/typing/analysis-entry.ts",
|
|
50
|
+
"test:types": "tsc --noEmit --ignoreConfig --strict --module ESNext --moduleResolution bundler --target ES2020 tests/typing/analysis-entry.ts tests/typing/worklet-capture-response.ts",
|
|
51
51
|
"test:browser": "node scripts/audio-worklet-smoke.mjs",
|
|
52
52
|
"test:browser:worker": "node scripts/offline-worker-smoke.mjs",
|
|
53
53
|
"test:browser:boundary": "node scripts/boundary-smoke.mjs",
|
package/src/_chain_config.ts
CHANGED
|
@@ -6,7 +6,12 @@ type ChainSection = { [key: string]: number | boolean | ChainSection | undefined
|
|
|
6
6
|
* Flattens a nested {@link MasteringChainConfig} into the dot-notation
|
|
7
7
|
* `{ "module.processor.param": value }` map the core consumes. Internal helper
|
|
8
8
|
* shared by the mastering-chain / master-audio entry points. The core owns
|
|
9
|
-
* legacy
|
|
9
|
+
* legacy leaf-name aliases, so this remains a structural flattening step.
|
|
10
|
+
*
|
|
11
|
+
* A key the caller already wrote in dot notation carries through untouched, so
|
|
12
|
+
* both spellings a {@link MasteringChainConfig} accepts reach the core as the
|
|
13
|
+
* same parameter — matching what the Python binding documents. An unknown key
|
|
14
|
+
* in either spelling is rejected by the core, not here.
|
|
10
15
|
*/
|
|
11
16
|
export function flattenChainConfig(config: MasteringChainConfig): Record<string, number | boolean> {
|
|
12
17
|
const out: Record<string, number | boolean> = {};
|
package/src/analysis_helpers.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveEnumOrdinal } from './codes';
|
|
1
2
|
import type {
|
|
2
3
|
AnalysisResult,
|
|
3
4
|
ChordAnalysisResult,
|
|
@@ -48,6 +49,37 @@ export function convertKeyCandidate(wasm: WasmKeyCandidateResult): KeyCandidate
|
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
// Consolidated spelling -> ordinal tables, resolved through the same
|
|
53
|
+
// resolveEnumOrdinal() primitive as panModeCode/panLawCode/etc. in ./codes.
|
|
54
|
+
// An unmapped string or an out-of-range numeric ordinal throws RangeError
|
|
55
|
+
// instead of silently resolving to `undefined` (which used to reach the WASM
|
|
56
|
+
// layer as a mistyped argument) or passing an unvalidated raw number through.
|
|
57
|
+
const KEY_MODE_VALUES: Readonly<Record<string, number>> = {
|
|
58
|
+
major: Mode.Major,
|
|
59
|
+
minor: Mode.Minor,
|
|
60
|
+
dorian: Mode.Dorian,
|
|
61
|
+
phrygian: Mode.Phrygian,
|
|
62
|
+
lydian: Mode.Lydian,
|
|
63
|
+
mixolydian: Mode.Mixolydian,
|
|
64
|
+
locrian: Mode.Locrian,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const KEY_PROFILE_VALUES: Readonly<Record<KeyProfileName, number>> = {
|
|
68
|
+
ks: KeyProfileValues.KrumhanslSchmuckler,
|
|
69
|
+
krumhansl: KeyProfileValues.KrumhanslSchmuckler,
|
|
70
|
+
temperley: KeyProfileValues.Temperley,
|
|
71
|
+
shaath: KeyProfileValues.Shaath,
|
|
72
|
+
keyfinder: KeyProfileValues.Shaath,
|
|
73
|
+
'faraldo-edmt': KeyProfileValues.FaraldoEDMT,
|
|
74
|
+
edmt: KeyProfileValues.FaraldoEDMT,
|
|
75
|
+
'faraldo-edma': KeyProfileValues.FaraldoEDMA,
|
|
76
|
+
edma: KeyProfileValues.FaraldoEDMA,
|
|
77
|
+
'faraldo-edmm': KeyProfileValues.FaraldoEDMM,
|
|
78
|
+
edmm: KeyProfileValues.FaraldoEDMM,
|
|
79
|
+
'bellman-budge': KeyProfileValues.BellmanBudge,
|
|
80
|
+
bellman: KeyProfileValues.BellmanBudge,
|
|
81
|
+
};
|
|
82
|
+
|
|
51
83
|
export function keyModeValues(modes: KeyDetectionOptions['modes'] | undefined): number[] {
|
|
52
84
|
if (!modes) {
|
|
53
85
|
return [];
|
|
@@ -66,41 +98,14 @@ export function keyModeValues(modes: KeyDetectionOptions['modes'] | undefined):
|
|
|
66
98
|
Mode.Locrian,
|
|
67
99
|
];
|
|
68
100
|
}
|
|
69
|
-
|
|
70
|
-
major: Mode.Major,
|
|
71
|
-
minor: Mode.Minor,
|
|
72
|
-
dorian: Mode.Dorian,
|
|
73
|
-
phrygian: Mode.Phrygian,
|
|
74
|
-
lydian: Mode.Lydian,
|
|
75
|
-
mixolydian: Mode.Mixolydian,
|
|
76
|
-
locrian: Mode.Locrian,
|
|
77
|
-
} as const;
|
|
78
|
-
return modes.map((mode) => (typeof mode === 'number' ? mode : names[mode]));
|
|
101
|
+
return modes.map((mode) => resolveEnumOrdinal(mode, KEY_MODE_VALUES, 'key mode'));
|
|
79
102
|
}
|
|
80
103
|
|
|
81
104
|
export function keyProfileValue(profile: KeyDetectionOptions['profile'] | undefined): number {
|
|
82
105
|
if (profile === undefined) {
|
|
83
106
|
return -1;
|
|
84
107
|
}
|
|
85
|
-
|
|
86
|
-
return profile;
|
|
87
|
-
}
|
|
88
|
-
const names: Record<KeyProfileName, number> = {
|
|
89
|
-
ks: KeyProfileValues.KrumhanslSchmuckler,
|
|
90
|
-
krumhansl: KeyProfileValues.KrumhanslSchmuckler,
|
|
91
|
-
temperley: KeyProfileValues.Temperley,
|
|
92
|
-
shaath: KeyProfileValues.Shaath,
|
|
93
|
-
keyfinder: KeyProfileValues.Shaath,
|
|
94
|
-
'faraldo-edmt': KeyProfileValues.FaraldoEDMT,
|
|
95
|
-
edmt: KeyProfileValues.FaraldoEDMT,
|
|
96
|
-
'faraldo-edma': KeyProfileValues.FaraldoEDMA,
|
|
97
|
-
edma: KeyProfileValues.FaraldoEDMA,
|
|
98
|
-
'faraldo-edmm': KeyProfileValues.FaraldoEDMM,
|
|
99
|
-
edmm: KeyProfileValues.FaraldoEDMM,
|
|
100
|
-
'bellman-budge': KeyProfileValues.BellmanBudge,
|
|
101
|
-
bellman: KeyProfileValues.BellmanBudge,
|
|
102
|
-
};
|
|
103
|
-
return names[profile];
|
|
108
|
+
return resolveEnumOrdinal(profile, KEY_PROFILE_VALUES, 'key profile');
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
export function convertChordAnalysisResult(wasm: WasmChordAnalysisResult): ChordAnalysisResult {
|
package/src/audio.ts
CHANGED
|
@@ -239,9 +239,14 @@ export class Audio {
|
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
/**
|
|
242
|
+
/**
|
|
243
|
+
* A copy of the raw audio samples. Mirrors Node's `getData()` contract: the
|
|
244
|
+
* returned array is independent of the Audio's internal buffer, so mutating
|
|
245
|
+
* it (or transferring it to a Worker) does not affect subsequent facade
|
|
246
|
+
* calls, which all read the internal snapshot directly.
|
|
247
|
+
*/
|
|
243
248
|
get data(): Float32Array {
|
|
244
|
-
return this._samples;
|
|
249
|
+
return this._samples.slice();
|
|
245
250
|
}
|
|
246
251
|
|
|
247
252
|
/** Number of samples. */
|
package/src/codes.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
import type { AutomationCurve, MeterTap,
|
|
1
|
+
import type { AutomationCurve, MeterTap, PanLawInput, PanMode, SendTiming } from './public_types';
|
|
2
|
+
|
|
3
|
+
/** Resolve a numeric ordinal in an inclusive range without coercion. */
|
|
4
|
+
export function resolveOrdinalInRange(
|
|
5
|
+
value: unknown,
|
|
6
|
+
min: number,
|
|
7
|
+
max: number,
|
|
8
|
+
enumName: string,
|
|
9
|
+
): number {
|
|
10
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < min || value > max) {
|
|
11
|
+
throw new RangeError(`Invalid ${enumName}: ${String(value)}`);
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
2
15
|
|
|
3
16
|
/** Resolve a public enum spelling or ordinal without permitting unknown values. */
|
|
4
17
|
export function resolveEnumOrdinal(
|
|
@@ -7,10 +20,17 @@ export function resolveEnumOrdinal(
|
|
|
7
20
|
enumName: string,
|
|
8
21
|
): number {
|
|
9
22
|
if (typeof value === 'number') {
|
|
10
|
-
|
|
23
|
+
const ordinals = Object.values(values);
|
|
24
|
+
const ordinal = resolveOrdinalInRange(
|
|
25
|
+
value,
|
|
26
|
+
Math.min(...ordinals),
|
|
27
|
+
Math.max(...ordinals),
|
|
28
|
+
enumName,
|
|
29
|
+
);
|
|
30
|
+
if (!ordinals.includes(ordinal)) {
|
|
11
31
|
throw new RangeError(`Invalid ${enumName}: ${String(value)}`);
|
|
12
32
|
}
|
|
13
|
-
return
|
|
33
|
+
return ordinal;
|
|
14
34
|
}
|
|
15
35
|
if (typeof value === 'string') {
|
|
16
36
|
const ordinal = values[value];
|
|
@@ -27,15 +47,24 @@ const AUTOMATION_CURVE_VALUES = {
|
|
|
27
47
|
hold: 2,
|
|
28
48
|
's-curve': 3,
|
|
29
49
|
} as const;
|
|
30
|
-
const PAN_LAW_VALUES = {
|
|
31
|
-
|
|
32
|
-
'
|
|
33
|
-
|
|
34
|
-
|
|
50
|
+
const PAN_LAW_VALUES: Readonly<Record<string, number>> = {
|
|
51
|
+
const3db: 0,
|
|
52
|
+
'const-3db': 0,
|
|
53
|
+
'-3db': 0,
|
|
54
|
+
'const4.5db': 1,
|
|
55
|
+
'const-4.5db': 1,
|
|
56
|
+
'-4.5db': 1,
|
|
57
|
+
const6db: 2,
|
|
58
|
+
'const-6db': 2,
|
|
59
|
+
'-6db': 2,
|
|
60
|
+
linear0db: 3,
|
|
61
|
+
'linear-0db': 3,
|
|
62
|
+
linear: 3,
|
|
63
|
+
'0db': 3,
|
|
35
64
|
} as const;
|
|
36
65
|
const PAN_MODE_VALUES = {
|
|
37
66
|
balance: 0,
|
|
38
|
-
pan:
|
|
67
|
+
pan: 1,
|
|
39
68
|
stereopan: 1,
|
|
40
69
|
'stereo-pan': 1,
|
|
41
70
|
dualpan: 2,
|
|
@@ -43,13 +72,15 @@ const PAN_MODE_VALUES = {
|
|
|
43
72
|
} as const;
|
|
44
73
|
const METER_TAP_VALUES = { preFader: 0, postFader: 1 } as const;
|
|
45
74
|
const SEND_TIMING_VALUES = { postFader: 0, preFader: 1 } as const;
|
|
75
|
+
const TRACK_MONITOR_MODE_VALUES = { off: 0, pfl: 1, afl: 2 } as const;
|
|
46
76
|
|
|
47
77
|
export function automationCurveCode(curve: AutomationCurve): number {
|
|
48
78
|
return resolveEnumOrdinal(curve, AUTOMATION_CURVE_VALUES, 'automation curve');
|
|
49
79
|
}
|
|
50
80
|
|
|
51
|
-
export function panLawCode(panLaw:
|
|
52
|
-
|
|
81
|
+
export function panLawCode(panLaw: PanLawInput): number {
|
|
82
|
+
const normalized = typeof panLaw === 'string' ? panLaw.toLowerCase().replace(/_/g, '-') : panLaw;
|
|
83
|
+
return resolveEnumOrdinal(normalized, PAN_LAW_VALUES, 'pan law');
|
|
53
84
|
}
|
|
54
85
|
|
|
55
86
|
export function panModeCode(panMode: PanMode | number): number {
|
|
@@ -69,3 +100,8 @@ export function sendTimingCode(timing: SendTiming | number): number {
|
|
|
69
100
|
// matching the sibling enum-code helpers and Node's sendTimingValue.
|
|
70
101
|
return resolveEnumOrdinal(timing, SEND_TIMING_VALUES, 'send timing');
|
|
71
102
|
}
|
|
103
|
+
|
|
104
|
+
/** Resolve a per-track PFL/AFL monitor mode to its C-ABI ordinal. */
|
|
105
|
+
export function trackMonitorModeCode(mode: unknown): number {
|
|
106
|
+
return resolveEnumOrdinal(mode, TRACK_MONITOR_MODE_VALUES, 'track monitor mode');
|
|
107
|
+
}
|
package/src/effects_mastering.ts
CHANGED
|
@@ -41,12 +41,16 @@ export type {
|
|
|
41
41
|
MasteringRealtimeCost,
|
|
42
42
|
MasteringSamplesParamsRequest,
|
|
43
43
|
MasteringStereoAnalyzeRequest,
|
|
44
|
+
MasteringStereoParamsRequest,
|
|
44
45
|
MasteringStreamingPreviewRequest,
|
|
46
|
+
MasteringStreamingPreviewStereoRequest,
|
|
45
47
|
} from './mastering_core';
|
|
46
48
|
export {
|
|
47
49
|
mastering,
|
|
48
50
|
masteringAssistantSuggest,
|
|
51
|
+
masteringAssistantSuggestStereo,
|
|
49
52
|
masteringAudioProfile,
|
|
53
|
+
masteringAudioProfileStereo,
|
|
50
54
|
masteringInsertNames,
|
|
51
55
|
masteringInsertParamInfo,
|
|
52
56
|
masteringInsertParamNames,
|
|
@@ -61,6 +65,7 @@ export {
|
|
|
61
65
|
masteringStereoAnalysisNames,
|
|
62
66
|
masteringStereoAnalyze,
|
|
63
67
|
masteringStreamingPreview,
|
|
68
|
+
masteringStreamingPreviewStereo,
|
|
64
69
|
} from './mastering_core';
|
|
65
70
|
export type {
|
|
66
71
|
CompressorDetector,
|
package/src/effects_transform.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
PitchCorrectOptions,
|
|
7
7
|
SpectralEditOptions,
|
|
8
8
|
SpectralRegionOp,
|
|
9
|
+
VoicedFlags,
|
|
9
10
|
} from './public_types';
|
|
10
11
|
import type { ValidateOptions } from './validation';
|
|
11
12
|
import { assertSampleRate, assertSamples } from './validation';
|
|
@@ -14,12 +15,77 @@ function requireModule() {
|
|
|
14
15
|
return getSonareModule();
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
// The embind layer reads the companion voicing array as Float32Array. A flag is
|
|
19
|
+
// a decision, not a magnitude: collapse to 1/0 on truthiness, which is the same
|
|
20
|
+
// reduction the Node facade applies, so both surfaces agree on every accepted
|
|
21
|
+
// input type.
|
|
22
|
+
function toVoicedFloat32(voiced: VoicedFlags): Float32Array {
|
|
23
|
+
const out = new Float32Array(voiced.length);
|
|
24
|
+
for (let index = 0; index < voiced.length; index += 1) {
|
|
25
|
+
out[index] = voiced[index] ? 1 : 0;
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveEffectFftOptions(
|
|
31
|
+
fnName: string,
|
|
32
|
+
nFft: unknown,
|
|
33
|
+
hopLength: unknown,
|
|
34
|
+
): { nFft: number; hopLength: number } {
|
|
35
|
+
const resolvedNFft = nFft === undefined ? 2048 : nFft;
|
|
36
|
+
const resolvedHopLength = hopLength === undefined ? 512 : hopLength;
|
|
37
|
+
if (typeof resolvedNFft !== 'number' || !Number.isInteger(resolvedNFft)) {
|
|
38
|
+
throw new TypeError(`${fnName}: nFft must be an integer`);
|
|
39
|
+
}
|
|
40
|
+
if (resolvedNFft < 2 || resolvedNFft > 2 ** 30) {
|
|
41
|
+
throw new RangeError(`${fnName}: nFft must be an even power of two >= 2`);
|
|
42
|
+
}
|
|
43
|
+
if ((resolvedNFft & (resolvedNFft - 1)) !== 0) {
|
|
44
|
+
throw new RangeError(`${fnName}: nFft must be an even power of two >= 2`);
|
|
45
|
+
}
|
|
46
|
+
if (typeof resolvedHopLength !== 'number' || !Number.isInteger(resolvedHopLength)) {
|
|
47
|
+
throw new TypeError(`${fnName}: hopLength must be an integer`);
|
|
48
|
+
}
|
|
49
|
+
if (resolvedHopLength <= 0 || resolvedHopLength > 2 ** 31 - 1) {
|
|
50
|
+
throw new RangeError(`${fnName}: hopLength must be a positive integer`);
|
|
51
|
+
}
|
|
52
|
+
return { nFft: resolvedNFft, hopLength: resolvedHopLength };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type NormalizeMode = 'peak' | 'rms';
|
|
56
|
+
|
|
57
|
+
function resolveNormalizeMode(value: unknown): NormalizeMode {
|
|
58
|
+
if (value === undefined) {
|
|
59
|
+
return 'peak';
|
|
60
|
+
}
|
|
61
|
+
if (typeof value !== 'string') {
|
|
62
|
+
throw new TypeError("normalize: mode must be the string 'peak' or 'rms'");
|
|
63
|
+
}
|
|
64
|
+
if (value !== 'peak' && value !== 'rms') {
|
|
65
|
+
throw new RangeError("normalize: mode must be the string 'peak' or 'rms'");
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resolveHardMask(value: unknown, fnName: string): boolean {
|
|
71
|
+
if (value === undefined) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
if (typeof value !== 'boolean') {
|
|
75
|
+
throw new TypeError(`${fnName}: hardMask must be a boolean`);
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
17
80
|
/** Canonical request form for HPSS. */
|
|
18
81
|
export interface HpssRequest {
|
|
19
82
|
samples: Float32Array;
|
|
20
83
|
sampleRate?: number;
|
|
21
84
|
kernelHarmonic?: number;
|
|
22
85
|
kernelPercussive?: number;
|
|
86
|
+
nFft?: number;
|
|
87
|
+
hopLength?: number;
|
|
88
|
+
hardMask?: boolean;
|
|
23
89
|
}
|
|
24
90
|
|
|
25
91
|
export interface HarmonicRequest extends ValidateOptions {
|
|
@@ -36,12 +102,16 @@ export interface TimeStretchRequest extends ValidateOptions {
|
|
|
36
102
|
samples: Float32Array;
|
|
37
103
|
sampleRate?: number;
|
|
38
104
|
rate: number;
|
|
105
|
+
nFft?: number;
|
|
106
|
+
hopLength?: number;
|
|
39
107
|
}
|
|
40
108
|
|
|
41
109
|
export interface PitchShiftRequest extends ValidateOptions {
|
|
42
110
|
samples: Float32Array;
|
|
43
111
|
sampleRate?: number;
|
|
44
112
|
semitones: number;
|
|
113
|
+
nFft?: number;
|
|
114
|
+
hopLength?: number;
|
|
45
115
|
}
|
|
46
116
|
|
|
47
117
|
export interface PitchCorrectToMidiRequest extends ValidateOptions {
|
|
@@ -57,7 +127,7 @@ export interface PitchCorrectToMidiTimevaryingRequest extends ValidateOptions {
|
|
|
57
127
|
targetMidi: number;
|
|
58
128
|
sampleRate?: number;
|
|
59
129
|
hopLength?: number;
|
|
60
|
-
voiced?:
|
|
130
|
+
voiced?: VoicedFlags;
|
|
61
131
|
voicedProb?: Float32Array;
|
|
62
132
|
}
|
|
63
133
|
|
|
@@ -81,6 +151,7 @@ export interface NormalizeRequest extends ValidateOptions {
|
|
|
81
151
|
samples: Float32Array;
|
|
82
152
|
sampleRate?: number;
|
|
83
153
|
targetDb?: number;
|
|
154
|
+
mode?: NormalizeMode;
|
|
84
155
|
}
|
|
85
156
|
|
|
86
157
|
export interface SpectralEditRequest extends SpectralEditOptions, ValidateOptions {
|
|
@@ -108,22 +179,33 @@ export function hpss(
|
|
|
108
179
|
sampleRate?: number,
|
|
109
180
|
kernelHarmonic?: number,
|
|
110
181
|
kernelPercussive?: number,
|
|
182
|
+
nFft?: number,
|
|
183
|
+
hopLength?: number,
|
|
184
|
+
hardMask?: boolean,
|
|
111
185
|
): HpssResult;
|
|
112
186
|
export function hpss(
|
|
113
187
|
samples: Float32Array | HpssRequest,
|
|
114
188
|
sampleRate = 22050,
|
|
115
189
|
kernelHarmonic = 31,
|
|
116
190
|
kernelPercussive = 31,
|
|
191
|
+
nFft?: number,
|
|
192
|
+
hopLength?: number,
|
|
193
|
+
hardMask?: boolean,
|
|
117
194
|
): HpssResult {
|
|
118
195
|
const request =
|
|
119
196
|
samples instanceof Float32Array
|
|
120
|
-
? { samples, sampleRate, kernelHarmonic, kernelPercussive }
|
|
197
|
+
? { samples, sampleRate, kernelHarmonic, kernelPercussive, nFft, hopLength, hardMask }
|
|
121
198
|
: samples;
|
|
122
|
-
|
|
199
|
+
const fftOptions = resolveEffectFftOptions('hpss', request.nFft, request.hopLength);
|
|
200
|
+
const resolvedHardMask = resolveHardMask(request.hardMask, 'hpss');
|
|
201
|
+
return requireModule().hpssEx(
|
|
123
202
|
request.samples,
|
|
124
203
|
request.sampleRate ?? 22050,
|
|
125
204
|
request.kernelHarmonic ?? 31,
|
|
126
205
|
request.kernelPercussive ?? 31,
|
|
206
|
+
fftOptions.nFft,
|
|
207
|
+
fftOptions.hopLength,
|
|
208
|
+
resolvedHardMask,
|
|
127
209
|
);
|
|
128
210
|
}
|
|
129
211
|
|
|
@@ -188,18 +270,56 @@ export function timeStretch(
|
|
|
188
270
|
rate: number,
|
|
189
271
|
options?: ValidateOptions,
|
|
190
272
|
): Float32Array;
|
|
273
|
+
export function timeStretch(
|
|
274
|
+
samples: Float32Array,
|
|
275
|
+
sampleRate: number,
|
|
276
|
+
rate: number,
|
|
277
|
+
nFft?: number,
|
|
278
|
+
hopLength?: number,
|
|
279
|
+
options?: ValidateOptions,
|
|
280
|
+
): Float32Array;
|
|
191
281
|
export function timeStretch(
|
|
192
282
|
samples: Float32Array | TimeStretchRequest,
|
|
193
283
|
sampleRate?: number,
|
|
194
284
|
rate?: number,
|
|
285
|
+
nFftOrOptions?: number | ValidateOptions,
|
|
286
|
+
hopLength?: number,
|
|
195
287
|
options: ValidateOptions = {},
|
|
196
288
|
): Float32Array {
|
|
289
|
+
if (
|
|
290
|
+
nFftOrOptions !== undefined &&
|
|
291
|
+
nFftOrOptions !== null &&
|
|
292
|
+
typeof nFftOrOptions !== 'number' &&
|
|
293
|
+
typeof nFftOrOptions !== 'object'
|
|
294
|
+
) {
|
|
295
|
+
throw new TypeError('timeStretch: nFft must be an integer or options object');
|
|
296
|
+
}
|
|
297
|
+
if (nFftOrOptions === null) {
|
|
298
|
+
throw new TypeError('timeStretch: nFft must be an integer or options object');
|
|
299
|
+
}
|
|
300
|
+
const positionalOptions =
|
|
301
|
+
typeof nFftOrOptions === 'object' && nFftOrOptions !== null ? nFftOrOptions : options;
|
|
302
|
+
const positionalNFft = typeof nFftOrOptions === 'number' ? nFftOrOptions : undefined;
|
|
197
303
|
const request: TimeStretchRequest =
|
|
198
304
|
samples instanceof Float32Array
|
|
199
|
-
? {
|
|
305
|
+
? {
|
|
306
|
+
samples,
|
|
307
|
+
sampleRate,
|
|
308
|
+
rate: rate as number,
|
|
309
|
+
nFft: positionalNFft,
|
|
310
|
+
hopLength,
|
|
311
|
+
...positionalOptions,
|
|
312
|
+
}
|
|
200
313
|
: samples;
|
|
201
314
|
assertSamples('timeStretch', request.samples, request.validate !== false);
|
|
202
|
-
|
|
315
|
+
const fftOptions = resolveEffectFftOptions('timeStretch', request.nFft, request.hopLength);
|
|
316
|
+
return requireModule().timeStretchEx(
|
|
317
|
+
request.samples,
|
|
318
|
+
request.sampleRate ?? 22050,
|
|
319
|
+
request.rate,
|
|
320
|
+
fftOptions.nFft,
|
|
321
|
+
fftOptions.hopLength,
|
|
322
|
+
);
|
|
203
323
|
}
|
|
204
324
|
|
|
205
325
|
/**
|
|
@@ -217,21 +337,55 @@ export function pitchShift(
|
|
|
217
337
|
semitones: number,
|
|
218
338
|
options?: ValidateOptions,
|
|
219
339
|
): Float32Array;
|
|
340
|
+
export function pitchShift(
|
|
341
|
+
samples: Float32Array,
|
|
342
|
+
sampleRate: number,
|
|
343
|
+
semitones: number,
|
|
344
|
+
nFft?: number,
|
|
345
|
+
hopLength?: number,
|
|
346
|
+
options?: ValidateOptions,
|
|
347
|
+
): Float32Array;
|
|
220
348
|
export function pitchShift(
|
|
221
349
|
samples: Float32Array | PitchShiftRequest,
|
|
222
350
|
sampleRate?: number,
|
|
223
351
|
semitones?: number,
|
|
352
|
+
nFftOrOptions?: number | ValidateOptions,
|
|
353
|
+
hopLength?: number,
|
|
224
354
|
options: ValidateOptions = {},
|
|
225
355
|
): Float32Array {
|
|
356
|
+
if (
|
|
357
|
+
nFftOrOptions !== undefined &&
|
|
358
|
+
nFftOrOptions !== null &&
|
|
359
|
+
typeof nFftOrOptions !== 'number' &&
|
|
360
|
+
typeof nFftOrOptions !== 'object'
|
|
361
|
+
) {
|
|
362
|
+
throw new TypeError('pitchShift: nFft must be an integer or options object');
|
|
363
|
+
}
|
|
364
|
+
if (nFftOrOptions === null) {
|
|
365
|
+
throw new TypeError('pitchShift: nFft must be an integer or options object');
|
|
366
|
+
}
|
|
367
|
+
const positionalOptions =
|
|
368
|
+
typeof nFftOrOptions === 'object' && nFftOrOptions !== null ? nFftOrOptions : options;
|
|
369
|
+
const positionalNFft = typeof nFftOrOptions === 'number' ? nFftOrOptions : undefined;
|
|
226
370
|
const request: PitchShiftRequest =
|
|
227
371
|
samples instanceof Float32Array
|
|
228
|
-
? {
|
|
372
|
+
? {
|
|
373
|
+
samples,
|
|
374
|
+
sampleRate,
|
|
375
|
+
semitones: semitones as number,
|
|
376
|
+
nFft: positionalNFft,
|
|
377
|
+
hopLength,
|
|
378
|
+
...positionalOptions,
|
|
379
|
+
}
|
|
229
380
|
: samples;
|
|
230
381
|
assertSamples('pitchShift', request.samples, request.validate !== false);
|
|
231
|
-
|
|
382
|
+
const fftOptions = resolveEffectFftOptions('pitchShift', request.nFft, request.hopLength);
|
|
383
|
+
return requireModule().pitchShiftEx(
|
|
232
384
|
request.samples,
|
|
233
385
|
request.sampleRate ?? 22050,
|
|
234
386
|
request.semitones,
|
|
387
|
+
fftOptions.nFft,
|
|
388
|
+
fftOptions.hopLength,
|
|
235
389
|
);
|
|
236
390
|
}
|
|
237
391
|
|
|
@@ -282,16 +436,18 @@ export function pitchCorrectToMidi(
|
|
|
282
436
|
* Unlike {@link pitchCorrectToMidi} (a single constant transpose), this follows
|
|
283
437
|
* the caller-supplied per-frame `f0Hz` contour and retunes every voiced frame
|
|
284
438
|
* toward `targetMidi`, so vibrato/drift in the source is tracked rather than
|
|
285
|
-
* flattened. `voiced` (
|
|
439
|
+
* flattened. `voiced` (truthy = voiced) and `voicedProb` ([0,1]) are optional;
|
|
286
440
|
* omitting them treats every frame as voiced. An `f0Hz` NaN is accepted only
|
|
287
|
-
* when the corresponding `voiced` entry is
|
|
441
|
+
* when the corresponding `voiced` entry is falsy, matching pYIN output. The
|
|
442
|
+
* `voicedFlag` / `voicedProb` arrays of a {@link PitchResult} can be passed
|
|
443
|
+
* through directly.
|
|
288
444
|
*
|
|
289
445
|
* @param samples - Audio samples (mono, float32)
|
|
290
446
|
* @param f0Hz - Per-frame measured F0 in Hz (one entry per analysis frame)
|
|
291
447
|
* @param targetMidi - Desired MIDI note number
|
|
292
448
|
* @param sampleRate - Sample rate in Hz
|
|
293
449
|
* @param hopLength - F0 hop in samples (frame i covers sample i*hopLength)
|
|
294
|
-
* @param voiced - Optional per-frame voiced flags (
|
|
450
|
+
* @param voiced - Optional per-frame voiced flags (truthy = voiced)
|
|
295
451
|
* @param voicedProb - Optional per-frame voicing probability in [0, 1]
|
|
296
452
|
* @returns Pitch-corrected audio
|
|
297
453
|
*/
|
|
@@ -304,7 +460,7 @@ export function pitchCorrectToMidiTimevarying(
|
|
|
304
460
|
targetMidi: number,
|
|
305
461
|
sampleRate?: number,
|
|
306
462
|
hopLength?: number,
|
|
307
|
-
voiced?:
|
|
463
|
+
voiced?: VoicedFlags,
|
|
308
464
|
voicedProb?: Float32Array,
|
|
309
465
|
options?: ValidateOptions,
|
|
310
466
|
): Float32Array;
|
|
@@ -314,7 +470,7 @@ export function pitchCorrectToMidiTimevarying(
|
|
|
314
470
|
targetMidi?: number,
|
|
315
471
|
sampleRate = 22050,
|
|
316
472
|
hopLength = 512,
|
|
317
|
-
voiced?:
|
|
473
|
+
voiced?: VoicedFlags,
|
|
318
474
|
voicedProb?: Float32Array,
|
|
319
475
|
options: ValidateOptions = {},
|
|
320
476
|
): Float32Array {
|
|
@@ -338,9 +494,7 @@ export function pitchCorrectToMidiTimevarying(
|
|
|
338
494
|
if (request.voicedProb && request.voicedProb.length !== request.f0Hz.length) {
|
|
339
495
|
throw new RangeError('pitchCorrectToMidiTimevarying: voicedProb length must match f0Hz length');
|
|
340
496
|
}
|
|
341
|
-
|
|
342
|
-
// 0.0/1.0); convert here so a single native conversion path suffices.
|
|
343
|
-
const voicedF32 = request.voiced ? Float32Array.from(request.voiced) : undefined;
|
|
497
|
+
const voicedF32 = request.voiced ? toVoicedFloat32(request.voiced) : undefined;
|
|
344
498
|
return requireModule().pitchCorrectToMidiTimevarying(
|
|
345
499
|
request.samples,
|
|
346
500
|
request.sampleRate ?? 22050,
|
|
@@ -395,11 +549,9 @@ export function pitchCorrectTimevarying(
|
|
|
395
549
|
if (request.voicedProb && request.voicedProb.length !== request.f0Hz.length) {
|
|
396
550
|
throw new RangeError('pitchCorrectTimevarying: voicedProb length must match f0Hz length');
|
|
397
551
|
}
|
|
398
|
-
// The embind layer reads the companion arrays as Float32Array (voiced uses
|
|
399
|
-
// 0.0/1.0); convert here so a single native conversion path suffices.
|
|
400
552
|
const nativeOptions = {
|
|
401
553
|
...request,
|
|
402
|
-
voiced: request.voiced ?
|
|
554
|
+
voiced: request.voiced ? toVoicedFloat32(request.voiced) : undefined,
|
|
403
555
|
};
|
|
404
556
|
return requireModule().pitchCorrectTimevarying(
|
|
405
557
|
request.samples,
|
|
@@ -466,11 +618,13 @@ export function noteMove(
|
|
|
466
618
|
}
|
|
467
619
|
|
|
468
620
|
/**
|
|
469
|
-
* Normalize audio to target peak level.
|
|
621
|
+
* Normalize audio to a target peak or RMS level.
|
|
470
622
|
*
|
|
471
623
|
* @param samples - Audio samples (mono, float32)
|
|
472
624
|
* @param sampleRate - Sample rate in Hz (default: 22050)
|
|
473
|
-
* @param targetDb - Finite target at or below 0 dBFS (default: 0 dB = full scale)
|
|
625
|
+
* @param targetDb - Finite target at or below 0 dBFS (default: 0 dB = full scale).
|
|
626
|
+
* For `mode: 'peak'`, this is the peak target; for `mode: 'rms'`, this is the RMS target.
|
|
627
|
+
* @param mode - Normalization mode: `'peak'` (default) or `'rms'`.
|
|
474
628
|
* @returns Normalized audio
|
|
475
629
|
*/
|
|
476
630
|
export function normalize(request: NormalizeRequest): Float32Array;
|
|
@@ -480,19 +634,45 @@ export function normalize(
|
|
|
480
634
|
targetDb?: number,
|
|
481
635
|
options?: ValidateOptions,
|
|
482
636
|
): Float32Array;
|
|
637
|
+
export function normalize(
|
|
638
|
+
samples: Float32Array,
|
|
639
|
+
sampleRate: number,
|
|
640
|
+
targetDb?: number,
|
|
641
|
+
mode?: NormalizeMode,
|
|
642
|
+
options?: ValidateOptions,
|
|
643
|
+
): Float32Array;
|
|
483
644
|
export function normalize(
|
|
484
645
|
samples: Float32Array | NormalizeRequest,
|
|
485
646
|
sampleRate?: number,
|
|
486
647
|
targetDb = 0.0,
|
|
648
|
+
modeOrOptions: NormalizeMode | ValidateOptions = 'peak',
|
|
487
649
|
options: ValidateOptions = {},
|
|
488
650
|
): Float32Array {
|
|
651
|
+
if (
|
|
652
|
+
modeOrOptions !== undefined &&
|
|
653
|
+
modeOrOptions !== null &&
|
|
654
|
+
typeof modeOrOptions !== 'string' &&
|
|
655
|
+
typeof modeOrOptions !== 'object'
|
|
656
|
+
) {
|
|
657
|
+
throw new TypeError("normalize: mode must be the string 'peak' or 'rms'");
|
|
658
|
+
}
|
|
659
|
+
if (modeOrOptions === null) {
|
|
660
|
+
throw new TypeError("normalize: mode must be the string 'peak' or 'rms'");
|
|
661
|
+
}
|
|
662
|
+
const positionalOptions =
|
|
663
|
+
typeof modeOrOptions === 'object' && modeOrOptions !== null ? modeOrOptions : options;
|
|
664
|
+
const positionalMode = typeof modeOrOptions === 'string' ? modeOrOptions : undefined;
|
|
489
665
|
const request: NormalizeRequest =
|
|
490
|
-
samples instanceof Float32Array
|
|
666
|
+
samples instanceof Float32Array
|
|
667
|
+
? { samples, sampleRate, targetDb, mode: positionalMode, ...positionalOptions }
|
|
668
|
+
: samples;
|
|
491
669
|
assertSamples('normalize', request.samples, request.validate !== false);
|
|
492
|
-
|
|
670
|
+
const mode = resolveNormalizeMode(request.mode);
|
|
671
|
+
return requireModule().normalizeEx(
|
|
493
672
|
request.samples,
|
|
494
673
|
request.sampleRate ?? 22050,
|
|
495
674
|
request.targetDb ?? 0.0,
|
|
675
|
+
mode,
|
|
496
676
|
);
|
|
497
677
|
}
|
|
498
678
|
|