@libraz/libsonare 1.5.5 → 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.
Files changed (80) hide show
  1. package/README.md +109 -20
  2. package/dist/analysis.d.ts +5809 -0
  3. package/dist/analysis.js +2574 -0
  4. package/dist/analysis.js.map +1 -0
  5. package/dist/index.d.ts +884 -55
  6. package/dist/index.js +1302 -292
  7. package/dist/index.js.map +1 -1
  8. package/dist/schemas/realtime-voice-changer-preset-pack.schema.json +27 -0
  9. package/dist/schemas/realtime-voice-changer-preset.schema.json +140 -0
  10. package/dist/sonare-analysis.js +2 -0
  11. package/dist/sonare-analysis.wasm +0 -0
  12. package/dist/sonare.js +1 -1
  13. package/dist/sonare.wasm +0 -0
  14. package/dist/worker.d.ts +65 -0
  15. package/dist/worker.js +734 -0
  16. package/dist/worker.js.map +1 -0
  17. package/dist/worklet.d.ts +628 -88
  18. package/dist/worklet.js +1816 -266
  19. package/dist/worklet.js.map +1 -1
  20. package/package.json +23 -11
  21. package/src/_chain_config.ts +5 -3
  22. package/src/analysis.ts +137 -0
  23. package/src/analysis_helpers.ts +40 -29
  24. package/src/audio.ts +7 -2
  25. package/src/clip_page_streamer.ts +62 -8
  26. package/src/codes.ts +87 -64
  27. package/src/effects_mastering.ts +1 -0
  28. package/src/effects_transform.ts +179 -10
  29. package/src/errors.ts +1 -0
  30. package/src/feature_core.ts +140 -2
  31. package/src/feature_music.ts +25 -3
  32. package/src/feature_pitch.ts +70 -1
  33. package/src/feature_spectral.ts +295 -7
  34. package/src/feature_spectrogram.ts +203 -3
  35. package/src/features.ts +16 -1
  36. package/src/index.ts +120 -8
  37. package/src/mastering_chain.ts +20 -8
  38. package/src/mastering_core.ts +5 -0
  39. package/src/mastering_dynamics.ts +7 -1
  40. package/src/metering.ts +6 -1
  41. package/src/mixer.ts +3 -3
  42. package/src/opfs_clip_pages.ts +26 -1
  43. package/src/project.ts +13 -0
  44. package/src/project_class.ts +204 -23
  45. package/src/project_internal.ts +38 -34
  46. package/src/project_types.ts +114 -11
  47. package/src/public_types.ts +68 -0
  48. package/src/public_types_mastering.ts +22 -0
  49. package/src/public_types_mixing.ts +27 -2
  50. package/src/public_types_music.ts +9 -0
  51. package/src/public_types_realtime.ts +23 -5
  52. package/src/public_types_spectral.ts +33 -0
  53. package/src/quick_analysis.ts +46 -10
  54. package/src/realtime_engine.ts +167 -6
  55. package/src/realtime_voice_changer.ts +51 -77
  56. package/src/sonare-analysis.js.d.ts +8 -0
  57. package/src/sonare.js.d.ts +340 -6
  58. package/src/stream_analyzer.ts +6 -1
  59. package/src/streaming_processors.ts +15 -0
  60. package/src/web_midi.ts +46 -11
  61. package/src/worker.ts +196 -0
  62. package/src/worker_client.ts +352 -0
  63. package/src/worker_protocol.ts +59 -0
  64. package/src/worklet/engine-automation.ts +2 -1
  65. package/src/worklet/engine-capture-facade.ts +13 -4
  66. package/src/worklet/engine-clips.ts +12 -3
  67. package/src/worklet/engine-node.ts +264 -21
  68. package/src/worklet/engine-offline.ts +7 -2
  69. package/src/worklet/engine-parameter-facade.ts +55 -5
  70. package/src/worklet/engine-processor.ts +435 -62
  71. package/src/worklet/engine-register.ts +9 -1
  72. package/src/worklet/engine-strips.ts +19 -3
  73. package/src/worklet/engine-sync.ts +2 -6
  74. package/src/worklet/engine-tempo-facade.ts +8 -6
  75. package/src/worklet/engine.ts +390 -13
  76. package/src/worklet/guards.ts +121 -8
  77. package/src/worklet/messages.ts +166 -8
  78. package/src/worklet/protocol.ts +305 -17
  79. package/src/worklet/voice-changer-processor.ts +12 -15
  80. package/src/worklet.ts +20 -1
@@ -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
- return requireModule().hpss(
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
- ? { samples, sampleRate, rate: rate as number, ...options }
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
- return requireModule().timeStretch(request.samples, request.sampleRate ?? 22050, request.rate);
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
- ? { samples, sampleRate, semitones: semitones as number, ...options }
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
- return requireModule().pitchShift(
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 ? { samples, sampleRate, targetDb, ...options } : samples;
655
+ samples instanceof Float32Array
656
+ ? { samples, sampleRate, targetDb, mode: positionalMode, ...positionalOptions }
657
+ : samples;
491
658
  assertSamples('normalize', request.samples, request.validate !== false);
492
- return requireModule().normalize(
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/errors.ts CHANGED
@@ -12,6 +12,7 @@ export enum ErrorCode {
12
12
  OutOfMemory = 5,
13
13
  NotSupported = 6,
14
14
  InvalidState = 7,
15
+ Cancelled = 8,
15
16
  Unknown = 99,
16
17
  }
17
18
 
@@ -11,6 +11,85 @@ function requireModule() {
11
11
  return getSonareModule();
12
12
  }
13
13
 
14
+ export function tone(request?: ToneRequest): Float32Array;
15
+ export function tone(
16
+ frequency?: number,
17
+ sampleRate?: number,
18
+ duration?: number,
19
+ phase?: number,
20
+ amplitude?: number,
21
+ ): Float32Array;
22
+ export function tone(
23
+ frequency: number | ToneRequest = 440,
24
+ sampleRate = 22050,
25
+ duration = 1,
26
+ phase = 0,
27
+ amplitude = 1,
28
+ ): Float32Array {
29
+ const request =
30
+ typeof frequency === 'number'
31
+ ? { frequency, sampleRate, duration, phase, amplitude }
32
+ : frequency;
33
+ return requireModule().tone(
34
+ request.frequency ?? 440,
35
+ request.sampleRate ?? 22050,
36
+ request.duration ?? 1,
37
+ request.phase ?? 0,
38
+ request.amplitude ?? 1,
39
+ );
40
+ }
41
+
42
+ export function chirp(request?: ChirpRequest): Float32Array;
43
+ export function chirp(
44
+ fmin?: number,
45
+ fmax?: number,
46
+ sampleRate?: number,
47
+ duration?: number,
48
+ linear?: boolean,
49
+ ): Float32Array;
50
+ export function chirp(
51
+ fmin: number | ChirpRequest = 440,
52
+ fmax = 880,
53
+ sampleRate = 22050,
54
+ duration = 1,
55
+ linear = true,
56
+ ): Float32Array {
57
+ const request = typeof fmin === 'number' ? { fmin, fmax, sampleRate, duration, linear } : fmin;
58
+ return requireModule().chirp(
59
+ request.fmin ?? 440,
60
+ request.fmax ?? 880,
61
+ request.sampleRate ?? 22050,
62
+ request.duration ?? 1,
63
+ request.linear ?? true,
64
+ );
65
+ }
66
+
67
+ export function clicks(request: ClicksRequest): Float32Array;
68
+ export function clicks(
69
+ times: Float32Array,
70
+ sampleRate?: number,
71
+ length?: number,
72
+ frequency?: number,
73
+ clickDuration?: number,
74
+ ): Float32Array;
75
+ export function clicks(
76
+ times: Float32Array | ClicksRequest,
77
+ sampleRate = 22050,
78
+ length = 0,
79
+ frequency = 1000,
80
+ clickDuration = 0.1,
81
+ ): Float32Array {
82
+ const request =
83
+ times instanceof Float32Array ? { times, sampleRate, length, frequency, clickDuration } : times;
84
+ return requireModule().clicks(
85
+ request.times,
86
+ request.sampleRate ?? 22050,
87
+ request.length ?? 0,
88
+ request.frequency ?? 1000,
89
+ request.clickDuration ?? 0.1,
90
+ );
91
+ }
92
+
14
93
  export interface DbConversionRequest {
15
94
  values: Float32Array;
16
95
  ref?: number;
@@ -30,6 +109,27 @@ export interface FrameSignalRequest {
30
109
  frameLength: number;
31
110
  hopLength: number;
32
111
  }
112
+ export interface ToneRequest {
113
+ frequency?: number;
114
+ sampleRate?: number;
115
+ duration?: number;
116
+ phase?: number;
117
+ amplitude?: number;
118
+ }
119
+ export interface ChirpRequest {
120
+ fmin?: number;
121
+ fmax?: number;
122
+ sampleRate?: number;
123
+ duration?: number;
124
+ linear?: boolean;
125
+ }
126
+ export interface ClicksRequest {
127
+ times: Float32Array;
128
+ sampleRate?: number;
129
+ length?: number;
130
+ frequency?: number;
131
+ clickDuration?: number;
132
+ }
33
133
  /** Canonical request form for pre/de-emphasis filters. */
34
134
  export interface EmphasisRequest {
35
135
  samples: Float32Array;
@@ -55,6 +155,10 @@ export interface FixFramesRequest {
55
155
  xMax?: number;
56
156
  pad?: boolean;
57
157
  }
158
+ export interface OnsetBacktrackRequest {
159
+ events: Int32Array;
160
+ energy: Float32Array;
161
+ }
58
162
  /** Canonical request form for peak selection. */
59
163
  export interface PeakPickRequest {
60
164
  values: Float32Array;
@@ -97,6 +201,8 @@ export interface TempogramRequest {
97
201
  hopLength?: number;
98
202
  winLength?: number;
99
203
  mode?: TempogramMode;
204
+ center?: boolean;
205
+ norm?: boolean;
100
206
  }
101
207
  export interface CyclicTempogramRequest {
102
208
  onsetEnvelope: Float32Array;
@@ -391,6 +497,18 @@ export function fixFrames(
391
497
  return requireModule().fixFrames(frames, xMin, xMax, pad);
392
498
  }
393
499
 
500
+ export function onsetBacktrack(request: OnsetBacktrackRequest): Int32Array;
501
+ export function onsetBacktrack(events: Int32Array, energy: Float32Array): Int32Array;
502
+ export function onsetBacktrack(
503
+ events: Int32Array | OnsetBacktrackRequest,
504
+ energy?: Float32Array,
505
+ ): Int32Array {
506
+ if (!(events instanceof Int32Array)) {
507
+ return onsetBacktrack(events.events, events.energy);
508
+ }
509
+ return requireModule().onsetBacktrack(events, energy as Float32Array);
510
+ }
511
+
394
512
  export function peakPick(request: PeakPickRequest): Int32Array;
395
513
  export function peakPick(
396
514
  values: Float32Array,
@@ -492,6 +610,8 @@ export function tempogram(
492
610
  hopLength?: number,
493
611
  winLength?: number,
494
612
  mode?: TempogramMode,
613
+ center?: boolean,
614
+ norm?: boolean,
495
615
  ): WasmTempogramResult;
496
616
  export function tempogram(
497
617
  onsetEnvelope: Float32Array | TempogramRequest,
@@ -499,12 +619,30 @@ export function tempogram(
499
619
  hopLength = 512,
500
620
  winLength = 384,
501
621
  mode: TempogramMode = 'autocorrelation',
622
+ center = true,
623
+ norm = true,
502
624
  ): WasmTempogramResult {
503
625
  if (!(onsetEnvelope instanceof Float32Array)) {
504
626
  const r = onsetEnvelope;
505
- return tempogram(r.onsetEnvelope, r.sampleRate, r.hopLength, r.winLength, r.mode);
627
+ return tempogram(
628
+ r.onsetEnvelope,
629
+ r.sampleRate,
630
+ r.hopLength,
631
+ r.winLength,
632
+ r.mode,
633
+ r.center,
634
+ r.norm,
635
+ );
506
636
  }
507
- return requireModule().tempogram(onsetEnvelope, sampleRate, hopLength, winLength, mode);
637
+ return requireModule().tempogram(
638
+ onsetEnvelope,
639
+ sampleRate,
640
+ hopLength,
641
+ winLength,
642
+ mode,
643
+ center,
644
+ norm,
645
+ );
508
646
  }
509
647
 
510
648
  export function cyclicTempogram(request: CyclicTempogramRequest): WasmCyclicTempogramResult;
@@ -58,6 +58,8 @@ export interface FourierTempogramRequest extends GuardedOptions {
58
58
  sampleRate?: number;
59
59
  hopLength?: number;
60
60
  winLength?: number;
61
+ center?: boolean;
62
+ norm?: boolean;
61
63
  }
62
64
 
63
65
  export interface TempogramRatioRequest extends GuardedOptions {
@@ -101,6 +103,7 @@ export interface NnlsChromaRequest extends GuardedOptions {
101
103
  enableStftBlend?: boolean;
102
104
  stftBlendWeight?: number;
103
105
  stftBlendNFft?: number;
106
+ hopLength?: number;
104
107
  }
105
108
 
106
109
  function validateMusicSamples(
@@ -154,12 +157,18 @@ export function nnlsChroma(
154
157
  return nnlsChroma(samples.samples, samples.sampleRate, samples);
155
158
  }
156
159
  validateMusicSamples('nnlsChroma', samples, sampleRate, options);
157
- return requireModule().nnlsChroma(
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(
158
166
  samples,
159
167
  sampleRate,
160
168
  options.enableStftBlend ?? true,
161
169
  options.stftBlendWeight ?? 0.55,
162
170
  options.stftBlendNFft ?? 4096,
171
+ hopLength,
163
172
  );
164
173
  }
165
174
 
@@ -584,7 +593,7 @@ export interface MelodyOptions {
584
593
  */
585
594
  usePyin?: boolean;
586
595
  /**
587
- * When {@link usePyin} is `true`, reflect-pad by `frameLength / 2` so frame
596
+ * When {@link usePyin} is `true`, zero-pad by `frameLength / 2` so frame
588
597
  * `i` is centered at `i * hopLength` (matches `librosa.pyin(center=True)`).
589
598
  * Ignored by the plain-YIN path. Defaults to `true`.
590
599
  */
@@ -760,6 +769,8 @@ export function fourierTempogram(
760
769
  sampleRate?: number,
761
770
  hopLength?: number,
762
771
  winLength?: number,
772
+ center?: boolean,
773
+ norm?: boolean,
763
774
  options?: GuardedOptions,
764
775
  ): WasmFourierTempogramResult;
765
776
  export function fourierTempogram(
@@ -767,6 +778,8 @@ export function fourierTempogram(
767
778
  sampleRate = 22050,
768
779
  hopLength = 512,
769
780
  winLength = 384,
781
+ center = true,
782
+ norm = true,
770
783
  options: GuardedOptions = {},
771
784
  ): WasmFourierTempogramResult {
772
785
  if (!(onsetEnvelope instanceof Float32Array)) {
@@ -776,13 +789,22 @@ export function fourierTempogram(
776
789
  request.sampleRate,
777
790
  request.hopLength,
778
791
  request.winLength,
792
+ request.center,
793
+ request.norm,
779
794
  request,
780
795
  );
781
796
  }
782
797
  assertSampleRate('fourierTempogram', sampleRate);
783
798
  assertSamples('fourierTempogram', onsetEnvelope, options.validate !== false, 'onsetEnvelope');
784
799
  validatePositiveIntegers('fourierTempogram', { hopLength, winLength });
785
- return requireModule().fourierTempogram(onsetEnvelope, sampleRate, hopLength, winLength);
800
+ return requireModule().fourierTempogram(
801
+ onsetEnvelope,
802
+ sampleRate,
803
+ hopLength,
804
+ winLength,
805
+ center,
806
+ norm,
807
+ );
786
808
  }
787
809
 
788
810
  /**
@@ -1,5 +1,5 @@
1
1
  import { getSonareModule } from './module_state';
2
- import type { PitchResult } from './public_types';
2
+ import type { NoteSegment, PiptrackResult, PitchResult } from './public_types';
3
3
 
4
4
  function requireModule() {
5
5
  return getSonareModule();
@@ -33,6 +33,51 @@ export interface PitchYinRequest {
33
33
  fillNa?: boolean;
34
34
  }
35
35
 
36
+ export interface PiptrackRequest {
37
+ samples: Float32Array;
38
+ sampleRate?: number;
39
+ nFft?: number;
40
+ hopLength?: number;
41
+ fmin?: number;
42
+ fmax?: number;
43
+ threshold?: number;
44
+ }
45
+
46
+ /** Per-bin spectral pitch candidates and peak magnitudes (librosa.piptrack). */
47
+ export function piptrack(request: PiptrackRequest): PiptrackResult;
48
+ export function piptrack(
49
+ samples: Float32Array,
50
+ sampleRate?: number,
51
+ nFft?: number,
52
+ hopLength?: number,
53
+ fmin?: number,
54
+ fmax?: number,
55
+ threshold?: number,
56
+ ): PiptrackResult;
57
+ export function piptrack(
58
+ samples: Float32Array | PiptrackRequest,
59
+ sampleRate = 22050,
60
+ nFft = 2048,
61
+ hopLength = 512,
62
+ fmin = 150,
63
+ fmax = 4000,
64
+ threshold = 0.1,
65
+ ): PiptrackResult {
66
+ if (!(samples instanceof Float32Array)) {
67
+ const request = samples;
68
+ return piptrack(
69
+ request.samples,
70
+ request.sampleRate,
71
+ request.nFft,
72
+ request.hopLength,
73
+ request.fmin,
74
+ request.fmax,
75
+ request.threshold,
76
+ );
77
+ }
78
+ return requireModule().piptrack(samples, sampleRate, nFft, hopLength, fmin, fmax, threshold);
79
+ }
80
+
36
81
  export function pitchYin(request: PitchYinRequest): PitchResult;
37
82
  export function pitchYin(
38
83
  samples: Float32Array,
@@ -139,3 +184,27 @@ export function pitchPyin(
139
184
  fillNa,
140
185
  );
141
186
  }
187
+
188
+ /** Parameters for segmenting an F0 track into stable monophonic notes. */
189
+ export interface NoteSegmentsRequest {
190
+ f0Hz: Float32Array;
191
+ voicedProb: Float32Array;
192
+ frameRate: number;
193
+ segmentationThresholdCents?: number;
194
+ minNoteMs?: number;
195
+ referenceHz?: number;
196
+ }
197
+
198
+ /**
199
+ * Segment a caller-supplied monophonic F0 track into stable note regions.
200
+ *
201
+ * `f0Hz` and `voicedProb` must have the same non-zero length. Zero-Hz frames
202
+ * and probabilities below 0.5 are treated as unvoiced.
203
+ */
204
+ export function noteSegments(request: NoteSegmentsRequest): NoteSegment[] {
205
+ return requireModule().noteSegments(request.f0Hz, request.voicedProb, request.frameRate, {
206
+ segmentationThresholdCents: request.segmentationThresholdCents,
207
+ minNoteMs: request.minNoteMs,
208
+ referenceHz: request.referenceHz,
209
+ });
210
+ }