@libraz/libsonare 1.2.3 → 1.3.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 (43) hide show
  1. package/README.md +38 -1
  2. package/dist/index.d.ts +1 -2842
  3. package/dist/index.js +3602 -1934
  4. package/dist/index.js.map +1 -1
  5. package/dist/sonare-rt-module.js +1 -1
  6. package/dist/sonare-rt.js +1 -1
  7. package/dist/sonare-rt.wasm +0 -0
  8. package/dist/sonare.js +1 -1
  9. package/dist/sonare.wasm +0 -0
  10. package/dist/worklet.d.ts +4816 -483
  11. package/dist/worklet.js +747 -440
  12. package/dist/worklet.js.map +1 -1
  13. package/package.json +2 -1
  14. package/src/analysis_helpers.ts +152 -0
  15. package/src/audio.ts +493 -0
  16. package/src/codes.ts +56 -0
  17. package/src/effects_mastering.ts +964 -0
  18. package/src/feature_core.ts +248 -0
  19. package/src/feature_music.ts +419 -0
  20. package/src/feature_pitch.ts +80 -0
  21. package/src/feature_resample.ts +21 -0
  22. package/src/feature_spectral.ts +330 -0
  23. package/src/feature_spectrogram.ts +454 -0
  24. package/src/features.ts +84 -0
  25. package/src/index.ts +341 -4963
  26. package/src/live_audio.ts +45 -0
  27. package/src/metering.ts +380 -0
  28. package/src/mixer.ts +523 -0
  29. package/src/module_state.ts +14 -0
  30. package/src/opfs_clip_pages.ts +188 -0
  31. package/src/project.ts +1614 -0
  32. package/src/public_types.ts +177 -2
  33. package/src/quick_analysis.ts +508 -0
  34. package/src/realtime_engine.ts +667 -0
  35. package/src/realtime_voice_changer.ts +275 -0
  36. package/src/scale.ts +42 -0
  37. package/src/sonare.js.d.ts +302 -4
  38. package/src/stream_analyzer.ts +275 -0
  39. package/src/stream_types.ts +26 -1
  40. package/src/streaming_mixing.ts +18 -0
  41. package/src/streaming_processors.ts +335 -0
  42. package/src/validation.ts +82 -0
  43. package/src/web_midi.ts +367 -0
@@ -0,0 +1,275 @@
1
+ import { getSonareModule } from './module_state';
2
+ import type { ChordQuality, PitchClass } from './public_types';
3
+ import type { WasmStreamAnalyzer } from './sonare.js';
4
+ import type {
5
+ AnalyzerStats,
6
+ FrameBuffer,
7
+ StreamConfig,
8
+ StreamConfigDefaults,
9
+ StreamFramesI16,
10
+ StreamFramesU8,
11
+ StreamQuantizeConfig,
12
+ } from './stream_types';
13
+
14
+ // ============================================================================
15
+ // StreamAnalyzer Class
16
+ // ============================================================================
17
+
18
+ export function streamAnalyzerConfigDefaults(): StreamConfigDefaults {
19
+ return getSonareModule().streamAnalyzerConfigDefault();
20
+ }
21
+
22
+ /**
23
+ * Real-time streaming audio analyzer.
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * import { init, StreamAnalyzer } from '@libraz/libsonare';
28
+ *
29
+ * await init();
30
+ *
31
+ * const analyzer = new StreamAnalyzer({ sampleRate: 44100 });
32
+ *
33
+ * // In audio processing callback
34
+ * analyzer.process(samples);
35
+ *
36
+ * // Get current analysis state
37
+ * const stats = analyzer.stats();
38
+ * console.log('BPM:', stats.estimate.bpm);
39
+ * console.log('Key:', stats.estimate.key);
40
+ * console.log('Chord progression:', stats.estimate.chordProgression);
41
+ * ```
42
+ */
43
+ export class StreamAnalyzer {
44
+ private analyzer: WasmStreamAnalyzer;
45
+
46
+ /**
47
+ * Create a new StreamAnalyzer.
48
+ *
49
+ * @param config - Configuration options
50
+ */
51
+ constructor(config: StreamConfig = {}) {
52
+ if (config.computeMagnitude) {
53
+ throw new Error(
54
+ 'computeMagnitude is not supported because magnitude frames are not exposed by StreamAnalyzer read paths.',
55
+ );
56
+ }
57
+ const module = getSonareModule();
58
+ const defaults = streamAnalyzerConfigDefaults();
59
+ this.analyzer = new module.StreamAnalyzer(
60
+ config.sampleRate ?? defaults.sampleRate,
61
+ config.nFft ?? defaults.nFft,
62
+ config.hopLength ?? defaults.hopLength,
63
+ config.nMels ?? defaults.nMels,
64
+ config.fmin ?? defaults.fmin,
65
+ config.fmax ?? defaults.fmax,
66
+ config.tuningRefHz ?? defaults.tuningRefHz,
67
+ config.computeMagnitude ?? defaults.computeMagnitude,
68
+ config.computeMel ?? defaults.computeMel,
69
+ config.computeChroma ?? defaults.computeChroma,
70
+ config.computeOnset ?? defaults.computeOnset,
71
+ config.computeSpectral ?? defaults.computeSpectral,
72
+ config.emitEveryNFrames ?? defaults.emitEveryNFrames,
73
+ config.magnitudeDownsample ?? defaults.magnitudeDownsample,
74
+ config.keyUpdateIntervalSec ?? defaults.keyUpdateIntervalSec,
75
+ config.bpmUpdateIntervalSec ?? defaults.bpmUpdateIntervalSec,
76
+ config.window ?? defaults.window,
77
+ config.outputFormat ?? defaults.outputFormat,
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Process audio samples.
83
+ *
84
+ * @param samples - Audio samples (mono, float32)
85
+ */
86
+ process(samples: Float32Array): void {
87
+ this.analyzer.process(samples);
88
+ }
89
+
90
+ /**
91
+ * Process audio samples with explicit sample offset.
92
+ *
93
+ * @param samples - Audio samples (mono, float32)
94
+ * @param sampleOffset - Cumulative sample count at start of this chunk
95
+ */
96
+ processWithOffset(samples: Float32Array, sampleOffset: number): void {
97
+ this.analyzer.processWithOffset(samples, sampleOffset);
98
+ }
99
+
100
+ /**
101
+ * Flush the final partial frame with zero-padding.
102
+ */
103
+ finalize(): void {
104
+ this.analyzer.finalize();
105
+ }
106
+
107
+ /**
108
+ * Get the number of frames available to read.
109
+ */
110
+ availableFrames(): number {
111
+ return this.analyzer.availableFrames();
112
+ }
113
+
114
+ /**
115
+ * Read processed frames as Structure of Arrays.
116
+ *
117
+ * @param maxFrames - Maximum number of frames to read
118
+ * @returns Frame buffer with analysis results
119
+ */
120
+ readFrames(maxFrames: number): FrameBuffer {
121
+ return this.analyzer.readFramesSoa(maxFrames);
122
+ }
123
+
124
+ /**
125
+ * Read frames as uint8-quantized arrays.
126
+ *
127
+ * @param maxFrames - Maximum number of frames to read
128
+ * @param quantizeConfig - Optional quantization ranges; widen these for a
129
+ * stream louder or quieter than the defaults (omitted keeps the defaults)
130
+ */
131
+ readFramesU8(maxFrames: number, quantizeConfig?: StreamQuantizeConfig): StreamFramesU8 {
132
+ return this.analyzer.readFramesU8(maxFrames, quantizeConfig) as StreamFramesU8;
133
+ }
134
+
135
+ /**
136
+ * Read frames as int16-quantized arrays.
137
+ *
138
+ * @param maxFrames - Maximum number of frames to read
139
+ * @param quantizeConfig - Optional quantization ranges; widen these for a
140
+ * stream louder or quieter than the defaults (omitted keeps the defaults)
141
+ */
142
+ readFramesI16(maxFrames: number, quantizeConfig?: StreamQuantizeConfig): StreamFramesI16 {
143
+ return this.analyzer.readFramesI16(maxFrames, quantizeConfig) as StreamFramesI16;
144
+ }
145
+
146
+ /**
147
+ * Reset the analyzer state.
148
+ *
149
+ * @param baseSampleOffset - Starting sample offset (default 0)
150
+ */
151
+ reset(baseSampleOffset = 0): void {
152
+ this.analyzer.reset(baseSampleOffset);
153
+ }
154
+
155
+ /**
156
+ * Get current statistics and progressive estimates.
157
+ *
158
+ * @returns Analyzer statistics including BPM, key, and chord progression
159
+ */
160
+ stats(): AnalyzerStats {
161
+ const s = this.analyzer.stats();
162
+ return {
163
+ totalFrames: s.totalFrames,
164
+ totalSamples: s.totalSamples,
165
+ durationSeconds: s.durationSeconds,
166
+ estimate: {
167
+ bpm: s.estimate.bpm,
168
+ bpmConfidence: s.estimate.bpmConfidence,
169
+ bpmCandidateCount: s.estimate.bpmCandidateCount,
170
+ key: s.estimate.key as PitchClass,
171
+ keyMinor: s.estimate.keyMinor,
172
+ keyConfidence: s.estimate.keyConfidence,
173
+ chordRoot: s.estimate.chordRoot as PitchClass,
174
+ chordQuality: s.estimate.chordQuality as ChordQuality,
175
+ chordConfidence: s.estimate.chordConfidence,
176
+ chordStartTime: s.estimate.chordStartTime,
177
+ chordProgression: s.estimate.chordProgression.map((c) => ({
178
+ root: c.root as PitchClass,
179
+ quality: c.quality as ChordQuality,
180
+ startTime: c.startTime,
181
+ confidence: c.confidence,
182
+ })),
183
+ barChordProgression: s.estimate.barChordProgression.map((c) => ({
184
+ barIndex: c.barIndex,
185
+ root: c.root as PitchClass,
186
+ quality: c.quality as ChordQuality,
187
+ startTime: c.startTime,
188
+ confidence: c.confidence,
189
+ })),
190
+ currentBar: s.estimate.currentBar,
191
+ barDuration: s.estimate.barDuration,
192
+ votedPattern: (s.estimate.votedPattern || []).map((c) => ({
193
+ barIndex: c.barIndex,
194
+ root: c.root as PitchClass,
195
+ quality: c.quality as ChordQuality,
196
+ startTime: c.startTime,
197
+ confidence: c.confidence,
198
+ })),
199
+ patternLength: s.estimate.patternLength,
200
+ detectedPatternName: s.estimate.detectedPatternName || '',
201
+ detectedPatternScore: s.estimate.detectedPatternScore || 0,
202
+ allPatternScores: (s.estimate.allPatternScores || []).map((p) => ({
203
+ name: p.name,
204
+ score: p.score,
205
+ })),
206
+ accumulatedSeconds: s.estimate.accumulatedSeconds,
207
+ usedFrames: s.estimate.usedFrames,
208
+ updated: s.estimate.updated,
209
+ },
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Get total frames processed.
215
+ */
216
+ frameCount(): number {
217
+ return this.analyzer.frameCount();
218
+ }
219
+
220
+ /**
221
+ * Get current time position in seconds.
222
+ */
223
+ currentTime(): number {
224
+ return this.analyzer.currentTime();
225
+ }
226
+
227
+ /**
228
+ * Get the sample rate.
229
+ */
230
+ sampleRate(): number {
231
+ return this.analyzer.sampleRate();
232
+ }
233
+
234
+ /**
235
+ * Set the expected total duration for pattern lock timing.
236
+ *
237
+ * @param durationSeconds - Total duration in seconds
238
+ */
239
+ setExpectedDuration(durationSeconds: number): void {
240
+ this.analyzer.setExpectedDuration(durationSeconds);
241
+ }
242
+
243
+ /**
244
+ * Set normalization gain for loud/compressed audio.
245
+ *
246
+ * @param gain - Gain factor to apply (e.g., 0.5 for -6dB reduction)
247
+ */
248
+ setNormalizationGain(gain: number): void {
249
+ this.analyzer.setNormalizationGain(gain);
250
+ }
251
+
252
+ /**
253
+ * Set tuning reference frequency for non-standard tuning.
254
+ *
255
+ * @param refHz - Reference frequency for A4 (default 440 Hz)
256
+ * @example
257
+ * // If audio is 1 semitone sharp (A4 = 466.16 Hz)
258
+ * analyzer.setTuningRefHz(466.16);
259
+ * // If audio is 1 semitone flat (A4 = 415.30 Hz)
260
+ * analyzer.setTuningRefHz(415.30);
261
+ */
262
+ setTuningRefHz(refHz: number): void {
263
+ this.analyzer.setTuningRefHz(refHz);
264
+ }
265
+
266
+ /** Release the underlying WASM object. Safe to call only once. */
267
+ delete(): void {
268
+ this.analyzer.delete();
269
+ }
270
+
271
+ /** Alias for {@link delete}, kept for backward compatibility (historical name). */
272
+ dispose(): void {
273
+ this.delete();
274
+ }
275
+ }
@@ -86,6 +86,25 @@ export interface FrameBuffer {
86
86
  chordConfidence: Float32Array;
87
87
  }
88
88
 
89
+ /**
90
+ * Quantization ranges for the uint8/int16 bandwidth-reduction read paths
91
+ * (`StreamAnalyzer.readFramesU8` / `readFramesI16`). Omitted fields fall back to
92
+ * the library defaults shown below; widen any range whose source values exceed
93
+ * the defaults, otherwise a louder/quieter stream saturates to the endpoints.
94
+ */
95
+ export interface StreamQuantizeConfig {
96
+ /** dB floor for mel quantization (default -80). */
97
+ melDbMin?: number;
98
+ /** dB ceiling for mel quantization (default 0). */
99
+ melDbMax?: number;
100
+ /** Max expected onset strength (default 50). */
101
+ onsetMax?: number;
102
+ /** Max expected RMS energy (default 1). */
103
+ rmsMax?: number;
104
+ /** Max expected spectral centroid in Hz (default 11025). */
105
+ centroidMax?: number;
106
+ }
107
+
89
108
  export interface StreamFramesU8 {
90
109
  nFrames: number;
91
110
  nMels: number;
@@ -112,9 +131,12 @@ export interface StreamFramesI16 {
112
131
 
113
132
  /**
114
133
  * Configuration for StreamAnalyzer
134
+ *
135
+ * Omitted values are read from the native StreamConfig defaults via
136
+ * streamAnalyzerConfigDefault(), keeping the WASM wrapper in sync with core.
115
137
  */
116
138
  export interface StreamConfig {
117
- /** Sample rate in Hz. Optional for parity with the Node/Python bindings (default 44100). */
139
+ /** Sample rate in Hz. Optional for parity with the Node/Python bindings. */
118
140
  sampleRate?: number;
119
141
  nFft?: number;
120
142
  hopLength?: number;
@@ -122,6 +144,7 @@ export interface StreamConfig {
122
144
  fmin?: number;
123
145
  fmax?: number;
124
146
  tuningRefHz?: number;
147
+ /** Unsupported: no read path surfaces per-frame magnitude spectra. */
125
148
  computeMagnitude?: boolean;
126
149
  computeMel?: boolean;
127
150
  computeChroma?: boolean;
@@ -134,3 +157,5 @@ export interface StreamConfig {
134
157
  window?: number;
135
158
  outputFormat?: number;
136
159
  }
160
+
161
+ export type StreamConfigDefaults = Required<StreamConfig>;
@@ -0,0 +1,18 @@
1
+ export type { MixerRealtimeBuffer } from './mixer';
2
+ export { Mixer } from './mixer';
3
+ export type {
4
+ RealtimeVoiceChangerInterleavedBuffer,
5
+ RealtimeVoiceChangerMonoBuffer,
6
+ RealtimeVoiceChangerPlanarBuffer,
7
+ } from './realtime_voice_changer';
8
+ export {
9
+ RealtimeVoiceChanger,
10
+ realtimeVoiceChangerPresetJson,
11
+ realtimeVoiceChangerPresetNames,
12
+ validateRealtimeVoiceChangerPresetJson,
13
+ } from './realtime_voice_changer';
14
+ export {
15
+ StreamingEqualizer,
16
+ StreamingMasteringChain,
17
+ StreamingRetune,
18
+ } from './streaming_processors';
@@ -0,0 +1,335 @@
1
+ import { getSonareModule } from './module_state';
2
+ import type {
3
+ EqBand,
4
+ EqMatchOptions,
5
+ EqSpectrumSnapshot,
6
+ StreamingEqualizerConfig,
7
+ StreamingMasteringChainConfig,
8
+ StreamingRetuneConfig,
9
+ } from './public_types';
10
+
11
+ type EqPhaseMode =
12
+ | 'zero'
13
+ | 'zero-latency'
14
+ | 'zero_latency'
15
+ | 'natural'
16
+ | 'natural-phase'
17
+ | 'natural_phase'
18
+ | 'linear'
19
+ | 'linear-phase'
20
+ | 'linear_phase'
21
+ | number;
22
+
23
+ const EQ_PHASE_MODES: Record<string, number> = {
24
+ zero: 1,
25
+ 'zero-latency': 1,
26
+ zero_latency: 1,
27
+ natural: 2,
28
+ 'natural-phase': 2,
29
+ natural_phase: 2,
30
+ linear: 3,
31
+ 'linear-phase': 3,
32
+ linear_phase: 3,
33
+ };
34
+
35
+ // ============================================================================
36
+ // StreamingMasteringChain Class
37
+ // ============================================================================
38
+
39
+ /**
40
+ * Block-by-block streaming variant of {@link masteringChain}.
41
+ *
42
+ * Maintains processor state across {@link processMono}/{@link processStereo}
43
+ * calls. Only ProcessorBase-backed stages are supported. Configurations that
44
+ * enable `repair.denoise` throw at construction. An enabled `loudness` stage
45
+ * also throws unless {@link StreamingMasteringChainConfig.loudnessStaticGainDb}
46
+ * supplies a precomputed normalization gain.
47
+ *
48
+ * Call {@link delete} (or use a `try/finally`) to release the underlying WASM
49
+ * object — the embind handle is not garbage-collected automatically.
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * const chain = new StreamingMasteringChain({ eq: { tiltDb: 1.0 } });
54
+ * try {
55
+ * chain.prepare(44100, 512, 1);
56
+ * const out = chain.processMono(blockSamples);
57
+ * } finally {
58
+ * chain.delete();
59
+ * }
60
+ * ```
61
+ */
62
+ export class StreamingMasteringChain {
63
+ private chain: import('./sonare.js').WasmStreamingMasteringChain;
64
+
65
+ constructor(config: StreamingMasteringChainConfig) {
66
+ const module = getSonareModule();
67
+ this.chain = module.createStreamingMasteringChain(config as Record<string, unknown>);
68
+ }
69
+
70
+ /**
71
+ * Initialize processors for the given sample rate and block layout.
72
+ *
73
+ * @param sampleRate - Sample rate in Hz
74
+ * @param maxBlockSize - Maximum block size per process call
75
+ * @param numChannels - 1 (mono) or 2 (stereo)
76
+ */
77
+ prepare(sampleRate: number, maxBlockSize: number, numChannels: number): void {
78
+ this.chain.prepare(sampleRate, maxBlockSize, numChannels);
79
+ }
80
+
81
+ /**
82
+ * Process one mono block, returning the processed samples (same length).
83
+ */
84
+ processMono(samples: Float32Array): Float32Array {
85
+ return this.chain.processMono(samples);
86
+ }
87
+
88
+ /**
89
+ * Process one stereo block, returning the processed channels.
90
+ */
91
+ processStereo(
92
+ left: Float32Array,
93
+ right: Float32Array,
94
+ ): { left: Float32Array; right: Float32Array } {
95
+ if (left.length !== right.length) {
96
+ throw new Error('Stereo channel lengths must match.');
97
+ }
98
+ return this.chain.processStereo(left, right);
99
+ }
100
+
101
+ /** Reset all processor state without rebuilding. */
102
+ reset(): void {
103
+ this.chain.reset();
104
+ }
105
+
106
+ /** Total reported latency in samples across all active processors. */
107
+ latencySamples(): number {
108
+ return this.chain.latencySamples();
109
+ }
110
+
111
+ /** Ordered stage names that will run (e.g. `"eq.tilt"`). */
112
+ stageNames(): string[] {
113
+ return this.chain.stageNames();
114
+ }
115
+
116
+ /** Release the underlying WASM object. Safe to call only once. */
117
+ delete(): void {
118
+ this.chain.delete();
119
+ }
120
+ }
121
+
122
+ // ============================================================================
123
+ // StreamingEqualizer Class
124
+ // ============================================================================
125
+
126
+ /**
127
+ * Block-by-block streaming equalizer wrapping the unified C++
128
+ * `EqualizerProcessor` (up to 24 bands, RBJ/Vicanek biquads, dynamic EQ,
129
+ * linear-phase FIR, mid/side processing, and auto-gain).
130
+ *
131
+ * State is maintained across {@link processMono}/{@link processStereo} calls.
132
+ * Call {@link delete} (or use `try/finally`) to release the underlying WASM
133
+ * object — the embind handle is not garbage-collected automatically.
134
+ *
135
+ * @example
136
+ * ```typescript
137
+ * const eq = new StreamingEqualizer({ sampleRate: 48000, maxBlockSize: 512 });
138
+ * try {
139
+ * eq.setBand(0, { type: 'HighShelf', frequencyHz: 8000, gainDb: 6, enabled: true });
140
+ * const out = eq.processStereo(left, right);
141
+ * const snapshot = eq.spectrum();
142
+ * } finally {
143
+ * eq.delete();
144
+ * }
145
+ * ```
146
+ */
147
+ export class StreamingEqualizer {
148
+ private eq: import('./sonare.js').WasmStreamingEqualizer;
149
+
150
+ constructor(config: StreamingEqualizerConfig = {}) {
151
+ const module = getSonareModule();
152
+ this.eq = module.createEqualizer(config as Record<string, unknown>);
153
+ }
154
+
155
+ /**
156
+ * Configure the band at `index` (0..23). Omitted fields use C++ defaults.
157
+ */
158
+ setBand(index: number, band: EqBand): void {
159
+ this.eq.setBand(index, band as Record<string, unknown>);
160
+ }
161
+
162
+ /** Disable and reset every band. */
163
+ clear(): void {
164
+ this.eq.clear();
165
+ }
166
+
167
+ /**
168
+ * Set the global phase mode: `'zero'` | `'natural'` | `'linear'` or 1/2/3.
169
+ */
170
+ setPhaseMode(mode: EqPhaseMode): void {
171
+ const value = typeof mode === 'number' ? mode : EQ_PHASE_MODES[mode.toLowerCase()];
172
+ if (value === undefined) {
173
+ throw new Error(`unknown EQ phase mode: ${mode}`);
174
+ }
175
+ this.eq.setPhaseMode(value);
176
+ }
177
+
178
+ /** Enable or disable output auto-gain compensation. */
179
+ setAutoGain(enabled: boolean): void {
180
+ this.eq.setAutoGain(enabled);
181
+ }
182
+
183
+ /** Set all-band EQ gain scale as a 0.0..2.0 multiplier. */
184
+ setGainScale(scale: number): void {
185
+ this.eq.setGainScale(scale);
186
+ }
187
+
188
+ /** Set post-EQ output gain in dB. */
189
+ setOutputGainDb(gainDb: number): void {
190
+ this.eq.setOutputGainDb(gainDb);
191
+ }
192
+
193
+ /** Set post-EQ stereo balance in -1.0..1.0; mono input ignores pan. */
194
+ setOutputPan(pan: number): void {
195
+ this.eq.setOutputPan(pan);
196
+ }
197
+
198
+ /**
199
+ * Provide a mono external sidechain key for dynamic bands that opt into
200
+ * `external_sidechain`. The samples are copied into an owned buffer.
201
+ */
202
+ setSidechainMono(samples: Float32Array): void {
203
+ this.eq.setSidechainMono(samples);
204
+ }
205
+
206
+ /**
207
+ * Provide a stereo external sidechain key. Both channels must match length.
208
+ */
209
+ setSidechainStereo(left: Float32Array, right: Float32Array): void {
210
+ if (left.length !== right.length) {
211
+ throw new Error('Sidechain channel lengths must match.');
212
+ }
213
+ this.eq.setSidechainStereo(left, right);
214
+ }
215
+
216
+ /** Release any borrowed external sidechain buffers. */
217
+ clearSidechain(): void {
218
+ this.eq.clearSidechain();
219
+ }
220
+
221
+ /** Auto-gain applied on the most recent block, in dB. */
222
+ lastAutoGainDb(): number {
223
+ return this.eq.lastAutoGainDb();
224
+ }
225
+
226
+ /** Reported processing latency in samples (non-zero for linear-phase bands). */
227
+ latencySamples(): number {
228
+ return this.eq.latencySamples();
229
+ }
230
+
231
+ /**
232
+ * Process one mono block, returning the equalized samples (same length).
233
+ */
234
+ processMono(samples: Float32Array): Float32Array {
235
+ return this.eq.processMono(samples);
236
+ }
237
+
238
+ /**
239
+ * Process one stereo block, returning the equalized channels.
240
+ */
241
+ processStereo(
242
+ left: Float32Array,
243
+ right: Float32Array,
244
+ ): { left: Float32Array; right: Float32Array } {
245
+ if (left.length !== right.length) {
246
+ throw new Error('Stereo channel lengths must match.');
247
+ }
248
+ return this.eq.processStereo(left, right);
249
+ }
250
+
251
+ /**
252
+ * Read the latest pre/post spectrum snapshot for metering. `seq` increments
253
+ * each time a new snapshot is published.
254
+ */
255
+ spectrum(): EqSpectrumSnapshot {
256
+ return this.eq.spectrum();
257
+ }
258
+
259
+ /**
260
+ * Configure bands so the source spectrum matches the reference spectrum.
261
+ *
262
+ * @param source - Source audio (mono samples)
263
+ * @param reference - Reference audio (mono samples)
264
+ * @param options - `sampleRate` (default 48000) and `maxBands` (default 8)
265
+ */
266
+ match(source: Float32Array, reference: Float32Array, options: EqMatchOptions = {}): void {
267
+ this.eq.match(source, reference, options as Record<string, unknown>);
268
+ }
269
+
270
+ /** Release the underlying WASM object. Safe to call only once. */
271
+ delete(): void {
272
+ this.eq.delete();
273
+ }
274
+ }
275
+
276
+ // ============================================================================
277
+ // StreamingRetune Class
278
+ // ============================================================================
279
+
280
+ /**
281
+ * Block-by-block mono voice retune / pitch shifter.
282
+ *
283
+ * State is maintained across {@link processMono} calls. Call {@link prepare}
284
+ * before processing, and call {@link delete} (or use `try/finally`) to release
285
+ * the underlying WASM object.
286
+ */
287
+ export class StreamingRetune {
288
+ private retune: import('./sonare.js').WasmStreamingRetune;
289
+
290
+ constructor(config: StreamingRetuneConfig = {}) {
291
+ const module = getSonareModule();
292
+ this.retune = module.createStreamingRetune(config as Record<string, unknown>);
293
+ }
294
+
295
+ /**
296
+ * Allocate and initialize native state for the given sample rate and maximum
297
+ * process block size.
298
+ */
299
+ prepare(sampleRate: number, maxBlockSize: number): void {
300
+ this.retune.prepare(sampleRate, maxBlockSize);
301
+ }
302
+
303
+ /** Reset delay, grain, and overlap-add state without changing config. */
304
+ reset(): void {
305
+ this.retune.reset();
306
+ }
307
+
308
+ /**
309
+ * Update retune settings. Changing `grainSize` takes effect after the next
310
+ * {@link prepare} call.
311
+ */
312
+ setConfig(config: StreamingRetuneConfig): void {
313
+ this.retune.setConfig(config as Record<string, unknown>);
314
+ }
315
+
316
+ /** Current native config. */
317
+ config(): Required<StreamingRetuneConfig> {
318
+ return this.retune.config();
319
+ }
320
+
321
+ /** Resolved grain size in samples after {@link prepare}. */
322
+ grainSize(): number {
323
+ return this.retune.grainSize();
324
+ }
325
+
326
+ /** Process one mono block, returning the shifted samples (same length). */
327
+ processMono(samples: Float32Array): Float32Array {
328
+ return this.retune.processMono(samples);
329
+ }
330
+
331
+ /** Release the underlying WASM object. Safe to call only once. */
332
+ delete(): void {
333
+ this.retune.delete();
334
+ }
335
+ }