@libraz/libsonare 1.6.0 → 1.7.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/README.md +2 -2
- package/dist/analysis.d.ts +88 -9
- package/dist/analysis.js +152 -42
- package/dist/analysis.js.map +1 -1
- package/dist/index.d.ts +129 -24
- package/dist/index.js +455 -140
- 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 +62 -10
- package/dist/worklet.js +193 -42
- package/dist/worklet.js.map +1 -1
- package/package.json +3 -3
- package/src/analysis_helpers.ts +34 -29
- package/src/audio.ts +7 -2
- package/src/codes.ts +47 -11
- package/src/effects_transform.ts +179 -10
- 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 +21 -3
- 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 +45 -1
- package/src/public_types_mixing.ts +16 -0
- package/src/public_types_realtime.ts +23 -5
- package/src/quick_analysis.ts +21 -3
- package/src/realtime_engine.ts +14 -3
- package/src/sonare.js.d.ts +64 -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.0",
|
|
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/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_transform.ts
CHANGED
|
@@ -14,12 +14,65 @@ function requireModule() {
|
|
|
14
14
|
return getSonareModule();
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function resolveEffectFftOptions(
|
|
18
|
+
fnName: string,
|
|
19
|
+
nFft: unknown,
|
|
20
|
+
hopLength: unknown,
|
|
21
|
+
): { nFft: number; hopLength: number } {
|
|
22
|
+
const resolvedNFft = nFft === undefined ? 2048 : nFft;
|
|
23
|
+
const resolvedHopLength = hopLength === undefined ? 512 : hopLength;
|
|
24
|
+
if (typeof resolvedNFft !== 'number' || !Number.isInteger(resolvedNFft)) {
|
|
25
|
+
throw new TypeError(`${fnName}: nFft must be an integer`);
|
|
26
|
+
}
|
|
27
|
+
if (resolvedNFft < 2 || resolvedNFft > 2 ** 30) {
|
|
28
|
+
throw new RangeError(`${fnName}: nFft must be an even power of two >= 2`);
|
|
29
|
+
}
|
|
30
|
+
if ((resolvedNFft & (resolvedNFft - 1)) !== 0) {
|
|
31
|
+
throw new RangeError(`${fnName}: nFft must be an even power of two >= 2`);
|
|
32
|
+
}
|
|
33
|
+
if (typeof resolvedHopLength !== 'number' || !Number.isInteger(resolvedHopLength)) {
|
|
34
|
+
throw new TypeError(`${fnName}: hopLength must be an integer`);
|
|
35
|
+
}
|
|
36
|
+
if (resolvedHopLength <= 0 || resolvedHopLength > 2 ** 31 - 1) {
|
|
37
|
+
throw new RangeError(`${fnName}: hopLength must be a positive integer`);
|
|
38
|
+
}
|
|
39
|
+
return { nFft: resolvedNFft, hopLength: resolvedHopLength };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type NormalizeMode = 'peak' | 'rms';
|
|
43
|
+
|
|
44
|
+
function resolveNormalizeMode(value: unknown): NormalizeMode {
|
|
45
|
+
if (value === undefined) {
|
|
46
|
+
return 'peak';
|
|
47
|
+
}
|
|
48
|
+
if (typeof value !== 'string') {
|
|
49
|
+
throw new TypeError("normalize: mode must be the string 'peak' or 'rms'");
|
|
50
|
+
}
|
|
51
|
+
if (value !== 'peak' && value !== 'rms') {
|
|
52
|
+
throw new RangeError("normalize: mode must be the string 'peak' or 'rms'");
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resolveHardMask(value: unknown, fnName: string): boolean {
|
|
58
|
+
if (value === undefined) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (typeof value !== 'boolean') {
|
|
62
|
+
throw new TypeError(`${fnName}: hardMask must be a boolean`);
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
|
|
17
67
|
/** Canonical request form for HPSS. */
|
|
18
68
|
export interface HpssRequest {
|
|
19
69
|
samples: Float32Array;
|
|
20
70
|
sampleRate?: number;
|
|
21
71
|
kernelHarmonic?: number;
|
|
22
72
|
kernelPercussive?: number;
|
|
73
|
+
nFft?: number;
|
|
74
|
+
hopLength?: number;
|
|
75
|
+
hardMask?: boolean;
|
|
23
76
|
}
|
|
24
77
|
|
|
25
78
|
export interface HarmonicRequest extends ValidateOptions {
|
|
@@ -36,12 +89,16 @@ export interface TimeStretchRequest extends ValidateOptions {
|
|
|
36
89
|
samples: Float32Array;
|
|
37
90
|
sampleRate?: number;
|
|
38
91
|
rate: number;
|
|
92
|
+
nFft?: number;
|
|
93
|
+
hopLength?: number;
|
|
39
94
|
}
|
|
40
95
|
|
|
41
96
|
export interface PitchShiftRequest extends ValidateOptions {
|
|
42
97
|
samples: Float32Array;
|
|
43
98
|
sampleRate?: number;
|
|
44
99
|
semitones: number;
|
|
100
|
+
nFft?: number;
|
|
101
|
+
hopLength?: number;
|
|
45
102
|
}
|
|
46
103
|
|
|
47
104
|
export interface PitchCorrectToMidiRequest extends ValidateOptions {
|
|
@@ -81,6 +138,7 @@ export interface NormalizeRequest extends ValidateOptions {
|
|
|
81
138
|
samples: Float32Array;
|
|
82
139
|
sampleRate?: number;
|
|
83
140
|
targetDb?: number;
|
|
141
|
+
mode?: NormalizeMode;
|
|
84
142
|
}
|
|
85
143
|
|
|
86
144
|
export interface SpectralEditRequest extends SpectralEditOptions, ValidateOptions {
|
|
@@ -108,22 +166,33 @@ export function hpss(
|
|
|
108
166
|
sampleRate?: number,
|
|
109
167
|
kernelHarmonic?: number,
|
|
110
168
|
kernelPercussive?: number,
|
|
169
|
+
nFft?: number,
|
|
170
|
+
hopLength?: number,
|
|
171
|
+
hardMask?: boolean,
|
|
111
172
|
): HpssResult;
|
|
112
173
|
export function hpss(
|
|
113
174
|
samples: Float32Array | HpssRequest,
|
|
114
175
|
sampleRate = 22050,
|
|
115
176
|
kernelHarmonic = 31,
|
|
116
177
|
kernelPercussive = 31,
|
|
178
|
+
nFft?: number,
|
|
179
|
+
hopLength?: number,
|
|
180
|
+
hardMask?: boolean,
|
|
117
181
|
): HpssResult {
|
|
118
182
|
const request =
|
|
119
183
|
samples instanceof Float32Array
|
|
120
|
-
? { samples, sampleRate, kernelHarmonic, kernelPercussive }
|
|
184
|
+
? { samples, sampleRate, kernelHarmonic, kernelPercussive, nFft, hopLength, hardMask }
|
|
121
185
|
: samples;
|
|
122
|
-
|
|
186
|
+
const fftOptions = resolveEffectFftOptions('hpss', request.nFft, request.hopLength);
|
|
187
|
+
const resolvedHardMask = resolveHardMask(request.hardMask, 'hpss');
|
|
188
|
+
return requireModule().hpssEx(
|
|
123
189
|
request.samples,
|
|
124
190
|
request.sampleRate ?? 22050,
|
|
125
191
|
request.kernelHarmonic ?? 31,
|
|
126
192
|
request.kernelPercussive ?? 31,
|
|
193
|
+
fftOptions.nFft,
|
|
194
|
+
fftOptions.hopLength,
|
|
195
|
+
resolvedHardMask,
|
|
127
196
|
);
|
|
128
197
|
}
|
|
129
198
|
|
|
@@ -188,18 +257,56 @@ export function timeStretch(
|
|
|
188
257
|
rate: number,
|
|
189
258
|
options?: ValidateOptions,
|
|
190
259
|
): Float32Array;
|
|
260
|
+
export function timeStretch(
|
|
261
|
+
samples: Float32Array,
|
|
262
|
+
sampleRate: number,
|
|
263
|
+
rate: number,
|
|
264
|
+
nFft?: number,
|
|
265
|
+
hopLength?: number,
|
|
266
|
+
options?: ValidateOptions,
|
|
267
|
+
): Float32Array;
|
|
191
268
|
export function timeStretch(
|
|
192
269
|
samples: Float32Array | TimeStretchRequest,
|
|
193
270
|
sampleRate?: number,
|
|
194
271
|
rate?: number,
|
|
272
|
+
nFftOrOptions?: number | ValidateOptions,
|
|
273
|
+
hopLength?: number,
|
|
195
274
|
options: ValidateOptions = {},
|
|
196
275
|
): Float32Array {
|
|
276
|
+
if (
|
|
277
|
+
nFftOrOptions !== undefined &&
|
|
278
|
+
nFftOrOptions !== null &&
|
|
279
|
+
typeof nFftOrOptions !== 'number' &&
|
|
280
|
+
typeof nFftOrOptions !== 'object'
|
|
281
|
+
) {
|
|
282
|
+
throw new TypeError('timeStretch: nFft must be an integer or options object');
|
|
283
|
+
}
|
|
284
|
+
if (nFftOrOptions === null) {
|
|
285
|
+
throw new TypeError('timeStretch: nFft must be an integer or options object');
|
|
286
|
+
}
|
|
287
|
+
const positionalOptions =
|
|
288
|
+
typeof nFftOrOptions === 'object' && nFftOrOptions !== null ? nFftOrOptions : options;
|
|
289
|
+
const positionalNFft = typeof nFftOrOptions === 'number' ? nFftOrOptions : undefined;
|
|
197
290
|
const request: TimeStretchRequest =
|
|
198
291
|
samples instanceof Float32Array
|
|
199
|
-
? {
|
|
292
|
+
? {
|
|
293
|
+
samples,
|
|
294
|
+
sampleRate,
|
|
295
|
+
rate: rate as number,
|
|
296
|
+
nFft: positionalNFft,
|
|
297
|
+
hopLength,
|
|
298
|
+
...positionalOptions,
|
|
299
|
+
}
|
|
200
300
|
: samples;
|
|
201
301
|
assertSamples('timeStretch', request.samples, request.validate !== false);
|
|
202
|
-
|
|
302
|
+
const fftOptions = resolveEffectFftOptions('timeStretch', request.nFft, request.hopLength);
|
|
303
|
+
return requireModule().timeStretchEx(
|
|
304
|
+
request.samples,
|
|
305
|
+
request.sampleRate ?? 22050,
|
|
306
|
+
request.rate,
|
|
307
|
+
fftOptions.nFft,
|
|
308
|
+
fftOptions.hopLength,
|
|
309
|
+
);
|
|
203
310
|
}
|
|
204
311
|
|
|
205
312
|
/**
|
|
@@ -217,21 +324,55 @@ export function pitchShift(
|
|
|
217
324
|
semitones: number,
|
|
218
325
|
options?: ValidateOptions,
|
|
219
326
|
): Float32Array;
|
|
327
|
+
export function pitchShift(
|
|
328
|
+
samples: Float32Array,
|
|
329
|
+
sampleRate: number,
|
|
330
|
+
semitones: number,
|
|
331
|
+
nFft?: number,
|
|
332
|
+
hopLength?: number,
|
|
333
|
+
options?: ValidateOptions,
|
|
334
|
+
): Float32Array;
|
|
220
335
|
export function pitchShift(
|
|
221
336
|
samples: Float32Array | PitchShiftRequest,
|
|
222
337
|
sampleRate?: number,
|
|
223
338
|
semitones?: number,
|
|
339
|
+
nFftOrOptions?: number | ValidateOptions,
|
|
340
|
+
hopLength?: number,
|
|
224
341
|
options: ValidateOptions = {},
|
|
225
342
|
): Float32Array {
|
|
343
|
+
if (
|
|
344
|
+
nFftOrOptions !== undefined &&
|
|
345
|
+
nFftOrOptions !== null &&
|
|
346
|
+
typeof nFftOrOptions !== 'number' &&
|
|
347
|
+
typeof nFftOrOptions !== 'object'
|
|
348
|
+
) {
|
|
349
|
+
throw new TypeError('pitchShift: nFft must be an integer or options object');
|
|
350
|
+
}
|
|
351
|
+
if (nFftOrOptions === null) {
|
|
352
|
+
throw new TypeError('pitchShift: nFft must be an integer or options object');
|
|
353
|
+
}
|
|
354
|
+
const positionalOptions =
|
|
355
|
+
typeof nFftOrOptions === 'object' && nFftOrOptions !== null ? nFftOrOptions : options;
|
|
356
|
+
const positionalNFft = typeof nFftOrOptions === 'number' ? nFftOrOptions : undefined;
|
|
226
357
|
const request: PitchShiftRequest =
|
|
227
358
|
samples instanceof Float32Array
|
|
228
|
-
? {
|
|
359
|
+
? {
|
|
360
|
+
samples,
|
|
361
|
+
sampleRate,
|
|
362
|
+
semitones: semitones as number,
|
|
363
|
+
nFft: positionalNFft,
|
|
364
|
+
hopLength,
|
|
365
|
+
...positionalOptions,
|
|
366
|
+
}
|
|
229
367
|
: samples;
|
|
230
368
|
assertSamples('pitchShift', request.samples, request.validate !== false);
|
|
231
|
-
|
|
369
|
+
const fftOptions = resolveEffectFftOptions('pitchShift', request.nFft, request.hopLength);
|
|
370
|
+
return requireModule().pitchShiftEx(
|
|
232
371
|
request.samples,
|
|
233
372
|
request.sampleRate ?? 22050,
|
|
234
373
|
request.semitones,
|
|
374
|
+
fftOptions.nFft,
|
|
375
|
+
fftOptions.hopLength,
|
|
235
376
|
);
|
|
236
377
|
}
|
|
237
378
|
|
|
@@ -466,11 +607,13 @@ export function noteMove(
|
|
|
466
607
|
}
|
|
467
608
|
|
|
468
609
|
/**
|
|
469
|
-
* Normalize audio to target peak level.
|
|
610
|
+
* Normalize audio to a target peak or RMS level.
|
|
470
611
|
*
|
|
471
612
|
* @param samples - Audio samples (mono, float32)
|
|
472
613
|
* @param sampleRate - Sample rate in Hz (default: 22050)
|
|
473
|
-
* @param targetDb - Finite target at or below 0 dBFS (default: 0 dB = full scale)
|
|
614
|
+
* @param targetDb - Finite target at or below 0 dBFS (default: 0 dB = full scale).
|
|
615
|
+
* For `mode: 'peak'`, this is the peak target; for `mode: 'rms'`, this is the RMS target.
|
|
616
|
+
* @param mode - Normalization mode: `'peak'` (default) or `'rms'`.
|
|
474
617
|
* @returns Normalized audio
|
|
475
618
|
*/
|
|
476
619
|
export function normalize(request: NormalizeRequest): Float32Array;
|
|
@@ -480,19 +623,45 @@ export function normalize(
|
|
|
480
623
|
targetDb?: number,
|
|
481
624
|
options?: ValidateOptions,
|
|
482
625
|
): Float32Array;
|
|
626
|
+
export function normalize(
|
|
627
|
+
samples: Float32Array,
|
|
628
|
+
sampleRate: number,
|
|
629
|
+
targetDb?: number,
|
|
630
|
+
mode?: NormalizeMode,
|
|
631
|
+
options?: ValidateOptions,
|
|
632
|
+
): Float32Array;
|
|
483
633
|
export function normalize(
|
|
484
634
|
samples: Float32Array | NormalizeRequest,
|
|
485
635
|
sampleRate?: number,
|
|
486
636
|
targetDb = 0.0,
|
|
637
|
+
modeOrOptions: NormalizeMode | ValidateOptions = 'peak',
|
|
487
638
|
options: ValidateOptions = {},
|
|
488
639
|
): Float32Array {
|
|
640
|
+
if (
|
|
641
|
+
modeOrOptions !== undefined &&
|
|
642
|
+
modeOrOptions !== null &&
|
|
643
|
+
typeof modeOrOptions !== 'string' &&
|
|
644
|
+
typeof modeOrOptions !== 'object'
|
|
645
|
+
) {
|
|
646
|
+
throw new TypeError("normalize: mode must be the string 'peak' or 'rms'");
|
|
647
|
+
}
|
|
648
|
+
if (modeOrOptions === null) {
|
|
649
|
+
throw new TypeError("normalize: mode must be the string 'peak' or 'rms'");
|
|
650
|
+
}
|
|
651
|
+
const positionalOptions =
|
|
652
|
+
typeof modeOrOptions === 'object' && modeOrOptions !== null ? modeOrOptions : options;
|
|
653
|
+
const positionalMode = typeof modeOrOptions === 'string' ? modeOrOptions : undefined;
|
|
489
654
|
const request: NormalizeRequest =
|
|
490
|
-
samples instanceof Float32Array
|
|
655
|
+
samples instanceof Float32Array
|
|
656
|
+
? { samples, sampleRate, targetDb, mode: positionalMode, ...positionalOptions }
|
|
657
|
+
: samples;
|
|
491
658
|
assertSamples('normalize', request.samples, request.validate !== false);
|
|
492
|
-
|
|
659
|
+
const mode = resolveNormalizeMode(request.mode);
|
|
660
|
+
return requireModule().normalizeEx(
|
|
493
661
|
request.samples,
|
|
494
662
|
request.sampleRate ?? 22050,
|
|
495
663
|
request.targetDb ?? 0.0,
|
|
664
|
+
mode,
|
|
496
665
|
);
|
|
497
666
|
}
|
|
498
667
|
|
package/src/feature_music.ts
CHANGED
|
@@ -103,6 +103,7 @@ export interface NnlsChromaRequest extends GuardedOptions {
|
|
|
103
103
|
enableStftBlend?: boolean;
|
|
104
104
|
stftBlendWeight?: number;
|
|
105
105
|
stftBlendNFft?: number;
|
|
106
|
+
hopLength?: number;
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
function validateMusicSamples(
|
|
@@ -156,12 +157,18 @@ export function nnlsChroma(
|
|
|
156
157
|
return nnlsChroma(samples.samples, samples.sampleRate, samples);
|
|
157
158
|
}
|
|
158
159
|
validateMusicSamples('nnlsChroma', samples, sampleRate, options);
|
|
159
|
-
|
|
160
|
+
const hopLength = options.hopLength === undefined ? 512 : options.hopLength;
|
|
161
|
+
assertPositiveInteger('nnlsChroma', hopLength, 'hopLength');
|
|
162
|
+
if (hopLength > 2 ** 31 - 1) {
|
|
163
|
+
throw new RangeError('nnlsChroma: hopLength must fit in a signed 32-bit integer');
|
|
164
|
+
}
|
|
165
|
+
return requireModule().nnlsChromaEx(
|
|
160
166
|
samples,
|
|
161
167
|
sampleRate,
|
|
162
168
|
options.enableStftBlend ?? true,
|
|
163
169
|
options.stftBlendWeight ?? 0.55,
|
|
164
170
|
options.stftBlendNFft ?? 4096,
|
|
171
|
+
hopLength,
|
|
165
172
|
);
|
|
166
173
|
}
|
|
167
174
|
|
package/src/feature_spectral.ts
CHANGED
|
@@ -19,6 +19,41 @@ function requireModule() {
|
|
|
19
19
|
return getSonareModule();
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function resolveEffectFftOptions(
|
|
23
|
+
fnName: string,
|
|
24
|
+
nFft: unknown,
|
|
25
|
+
hopLength: unknown,
|
|
26
|
+
): { nFft: number; hopLength: number } {
|
|
27
|
+
const resolvedNFft = nFft === undefined ? 2048 : nFft;
|
|
28
|
+
const resolvedHopLength = hopLength === undefined ? 512 : hopLength;
|
|
29
|
+
if (typeof resolvedNFft !== 'number' || !Number.isInteger(resolvedNFft)) {
|
|
30
|
+
throw new TypeError(`${fnName}: nFft must be an integer`);
|
|
31
|
+
}
|
|
32
|
+
if (resolvedNFft < 2 || resolvedNFft > 2 ** 30) {
|
|
33
|
+
throw new RangeError(`${fnName}: nFft must be an even power of two >= 2`);
|
|
34
|
+
}
|
|
35
|
+
if ((resolvedNFft & (resolvedNFft - 1)) !== 0) {
|
|
36
|
+
throw new RangeError(`${fnName}: nFft must be an even power of two >= 2`);
|
|
37
|
+
}
|
|
38
|
+
if (typeof resolvedHopLength !== 'number' || !Number.isInteger(resolvedHopLength)) {
|
|
39
|
+
throw new TypeError(`${fnName}: hopLength must be an integer`);
|
|
40
|
+
}
|
|
41
|
+
if (resolvedHopLength <= 0 || resolvedHopLength > 2 ** 31 - 1) {
|
|
42
|
+
throw new RangeError(`${fnName}: hopLength must be a positive integer`);
|
|
43
|
+
}
|
|
44
|
+
return { nFft: resolvedNFft, hopLength: resolvedHopLength };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveHardMask(fnName: string, value: unknown): boolean {
|
|
48
|
+
if (value === undefined) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (typeof value !== 'boolean') {
|
|
52
|
+
throw new TypeError(`${fnName}: hardMask must be a boolean`);
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
22
57
|
/** Canonical request form for frame-based spectral feature extraction. */
|
|
23
58
|
export interface SpectralFrameRequest {
|
|
24
59
|
samples: Float32Array;
|
|
@@ -166,6 +201,9 @@ export interface HpssWithResidualRequest {
|
|
|
166
201
|
sampleRate?: number;
|
|
167
202
|
kernelHarmonic?: number;
|
|
168
203
|
kernelPercussive?: number;
|
|
204
|
+
nFft?: number;
|
|
205
|
+
hopLength?: number;
|
|
206
|
+
hardMask?: boolean;
|
|
169
207
|
}
|
|
170
208
|
export interface LufsInterleavedRequest extends ValidateOptions {
|
|
171
209
|
samples: Float32Array;
|
|
@@ -542,18 +580,42 @@ export function hpssWithResidual(
|
|
|
542
580
|
sampleRate?: number,
|
|
543
581
|
kernelHarmonic?: number,
|
|
544
582
|
kernelPercussive?: number,
|
|
583
|
+
nFft?: number,
|
|
584
|
+
hopLength?: number,
|
|
585
|
+
hardMask?: boolean,
|
|
545
586
|
): WasmHpssWithResidualResult;
|
|
546
587
|
export function hpssWithResidual(
|
|
547
588
|
samples: Float32Array | HpssWithResidualRequest,
|
|
548
589
|
sampleRate = 22050,
|
|
549
590
|
kernelHarmonic = 31,
|
|
550
591
|
kernelPercussive = 31,
|
|
592
|
+
nFft?: number,
|
|
593
|
+
hopLength?: number,
|
|
594
|
+
hardMask?: boolean,
|
|
551
595
|
): WasmHpssWithResidualResult {
|
|
552
596
|
if (!(samples instanceof Float32Array)) {
|
|
553
597
|
const r = samples;
|
|
554
|
-
return hpssWithResidual(
|
|
598
|
+
return hpssWithResidual(
|
|
599
|
+
r.samples,
|
|
600
|
+
r.sampleRate,
|
|
601
|
+
r.kernelHarmonic,
|
|
602
|
+
r.kernelPercussive,
|
|
603
|
+
r.nFft,
|
|
604
|
+
r.hopLength,
|
|
605
|
+
r.hardMask,
|
|
606
|
+
);
|
|
555
607
|
}
|
|
556
|
-
|
|
608
|
+
const fftOptions = resolveEffectFftOptions('hpssWithResidual', nFft, hopLength);
|
|
609
|
+
const resolvedHardMask = resolveHardMask('hpssWithResidual', hardMask);
|
|
610
|
+
return requireModule().hpssWithResidualEx(
|
|
611
|
+
samples,
|
|
612
|
+
sampleRate,
|
|
613
|
+
kernelHarmonic,
|
|
614
|
+
kernelPercussive,
|
|
615
|
+
fftOptions.nFft,
|
|
616
|
+
fftOptions.hopLength,
|
|
617
|
+
resolvedHardMask,
|
|
618
|
+
);
|
|
557
619
|
}
|
|
558
620
|
|
|
559
621
|
/**
|