@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.
Files changed (53) hide show
  1. package/README.md +2 -2
  2. package/dist/analysis.d.ts +139 -11
  3. package/dist/analysis.js +162 -42
  4. package/dist/analysis.js.map +1 -1
  5. package/dist/index.d.ts +211 -39
  6. package/dist/index.js +501 -142
  7. package/dist/index.js.map +1 -1
  8. package/dist/schemas/realtime-voice-changer-preset.schema.json +2 -2
  9. package/dist/sonare-analysis.js +1 -1
  10. package/dist/sonare-analysis.wasm +0 -0
  11. package/dist/sonare.js +1 -1
  12. package/dist/sonare.wasm +0 -0
  13. package/dist/worker.js +56 -29
  14. package/dist/worker.js.map +1 -1
  15. package/dist/worklet.d.ts +67 -18
  16. package/dist/worklet.js +193 -42
  17. package/dist/worklet.js.map +1 -1
  18. package/package.json +3 -3
  19. package/src/_chain_config.ts +6 -1
  20. package/src/analysis_helpers.ts +34 -29
  21. package/src/audio.ts +7 -2
  22. package/src/codes.ts +47 -11
  23. package/src/effects_mastering.ts +5 -0
  24. package/src/effects_transform.ts +202 -22
  25. package/src/feature_music.ts +8 -1
  26. package/src/feature_spectral.ts +64 -2
  27. package/src/feature_spectrogram.ts +44 -3
  28. package/src/index.ts +28 -3
  29. package/src/mastering_core.ts +70 -0
  30. package/src/metering.ts +18 -0
  31. package/src/mixer.ts +3 -3
  32. package/src/project.ts +6 -0
  33. package/src/project_class.ts +143 -16
  34. package/src/project_internal.ts +14 -5
  35. package/src/project_types.ts +50 -9
  36. package/src/public_types_mastering.ts +9 -0
  37. package/src/public_types_mixing.ts +16 -0
  38. package/src/public_types_realtime.ts +23 -5
  39. package/src/public_types_spectral.ts +16 -2
  40. package/src/quick_analysis.ts +21 -3
  41. package/src/realtime_engine.ts +14 -3
  42. package/src/sonare.js.d.ts +87 -0
  43. package/src/stream_analyzer.ts +6 -1
  44. package/src/web_midi.ts +10 -7
  45. package/src/worklet/engine-node.ts +54 -19
  46. package/src/worklet/engine-parameter-facade.ts +30 -1
  47. package/src/worklet/engine-processor.ts +52 -20
  48. package/src/worklet/engine-register.ts +1 -1
  49. package/src/worklet/engine-strips.ts +2 -2
  50. package/src/worklet/engine.ts +10 -0
  51. package/src/worklet/guards.ts +99 -7
  52. package/src/worklet/messages.ts +41 -1
  53. 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.6.0",
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-baseline.json --artifact sonare.wasm=dist/sonare.wasm --artifact sonare-analysis.wasm=dist/sonare-analysis.wasm",
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",
@@ -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 flat-key aliases, so this remains a structural flattening step.
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> = {};
@@ -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
- const names = {
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
- if (typeof profile === 'number') {
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
- /** The raw audio samples. */
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, PanLaw, PanMode, SendTiming } from './public_types';
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
- if (!Number.isSafeInteger(value) || !Object.values(values).includes(value)) {
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 value;
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
- const3dB: 0,
32
- 'const4.5dB': 1,
33
- const6dB: 2,
34
- linear0dB: 3,
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: 0,
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: PanLaw | number): number {
52
- return resolveEnumOrdinal(panLaw, PAN_LAW_VALUES, 'pan law');
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
+ }
@@ -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,
@@ -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?: Int32Array;
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
- return requireModule().hpss(
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
- ? { samples, sampleRate, rate: rate as number, ...options }
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
- return requireModule().timeStretch(request.samples, request.sampleRate ?? 22050, request.rate);
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
- ? { samples, sampleRate, semitones: semitones as number, ...options }
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
- return requireModule().pitchShift(
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` (non-zero = voiced) and `voicedProb` ([0,1]) are optional;
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 zero, matching pYIN output.
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 (non-zero = voiced)
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?: Int32Array,
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?: Int32Array,
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
- // The embind layer reads the companion arrays as Float32Array (voiced uses
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 ? Float32Array.from(request.voiced) : undefined,
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 ? { samples, sampleRate, targetDb, ...options } : samples;
666
+ samples instanceof Float32Array
667
+ ? { samples, sampleRate, targetDb, mode: positionalMode, ...positionalOptions }
668
+ : samples;
491
669
  assertSamples('normalize', request.samples, request.validate !== false);
492
- return requireModule().normalize(
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