@libraz/libsonare 1.2.3 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -1
- package/dist/index.d.ts +1 -2842
- package/dist/index.js +3667 -1984
- package/dist/index.js.map +1 -1
- package/dist/sonare-rt-module.js +1 -1
- package/dist/sonare-rt.js +1 -1
- package/dist/sonare-rt.wasm +0 -0
- package/dist/sonare.js +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/worklet.d.ts +4816 -483
- package/dist/worklet.js +747 -440
- package/dist/worklet.js.map +1 -1
- package/package.json +2 -1
- package/src/analysis_helpers.ts +152 -0
- package/src/audio.ts +493 -0
- package/src/codes.ts +56 -0
- package/src/effects_mastering.ts +964 -0
- package/src/feature_core.ts +248 -0
- package/src/feature_music.ts +419 -0
- package/src/feature_pitch.ts +80 -0
- package/src/feature_resample.ts +21 -0
- package/src/feature_spectral.ts +330 -0
- package/src/feature_spectrogram.ts +454 -0
- package/src/features.ts +84 -0
- package/src/index.ts +341 -4963
- package/src/live_audio.ts +47 -0
- package/src/metering.ts +380 -0
- package/src/mixer.ts +523 -0
- package/src/module_state.ts +14 -0
- package/src/opfs_clip_pages.ts +203 -0
- package/src/project.ts +1614 -0
- package/src/public_types.ts +177 -2
- package/src/quick_analysis.ts +508 -0
- package/src/realtime_engine.ts +667 -0
- package/src/realtime_voice_changer.ts +275 -0
- package/src/scale.ts +42 -0
- package/src/sonare.js.d.ts +302 -4
- package/src/stream_analyzer.ts +275 -0
- package/src/stream_types.ts +26 -1
- package/src/streaming_mixing.ts +18 -0
- package/src/streaming_processors.ts +335 -0
- package/src/validation.ts +82 -0
- package/src/web_midi.ts +366 -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
|
+
}
|
package/src/stream_types.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|