@libraz/libsonare 1.4.1 → 1.5.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 +51 -20
- package/dist/index.d.ts +5416 -1
- package/dist/index.js +938 -583
- package/dist/index.js.map +1 -1
- package/dist/sonare.js +2 -2
- package/dist/sonare.wasm +0 -0
- package/dist/worklet.d.ts +1083 -5227
- package/dist/worklet.js +2683 -2451
- package/dist/worklet.js.map +1 -1
- package/package.json +4 -9
- package/src/clip_page_streamer.ts +298 -0
- package/src/effects_mastering.ts +85 -1089
- package/src/effects_transform.ts +286 -0
- package/src/effects_voice_change.ts +118 -0
- package/src/feature_music.ts +5 -2
- package/src/feature_spectrogram.ts +42 -2
- package/src/features.ts +1 -0
- package/src/index.ts +23 -0
- package/src/mastering_chain.ts +200 -0
- package/src/mastering_core.ts +248 -0
- package/src/mastering_dynamics.ts +105 -0
- package/src/mastering_repair.ts +161 -0
- package/src/mixer.ts +8 -0
- package/src/mixing_oneshot.ts +54 -0
- package/src/module_state.ts +1 -2
- package/src/project.ts +71 -1712
- package/src/project_class.ts +871 -0
- package/src/project_internal.ts +333 -0
- package/src/project_synth.ts +43 -0
- package/src/project_types.ts +570 -0
- package/src/public_types.ts +6 -1221
- package/src/public_types_acoustic.ts +115 -0
- package/src/public_types_mastering.ts +333 -0
- package/src/public_types_mixing.ts +97 -0
- package/src/public_types_music.ts +352 -0
- package/src/public_types_realtime.ts +163 -0
- package/src/public_types_spectral.ts +194 -0
- package/src/realtime_engine.ts +94 -0
- package/src/sonare.js.d.ts +117 -38
- package/src/stream_analyzer.ts +3 -0
- package/src/stream_types.ts +4 -0
- package/src/worklet/engine-automation.ts +73 -0
- package/src/worklet/engine-capture-facade.ts +80 -0
- package/src/worklet/engine-clips.ts +71 -0
- package/src/worklet/engine-markers.ts +93 -0
- package/src/worklet/engine-mixer-facade.ts +186 -0
- package/src/worklet/engine-node.ts +451 -0
- package/src/worklet/engine-offline.ts +162 -0
- package/src/worklet/engine-options.ts +13 -0
- package/src/worklet/engine-parameter-facade.ts +172 -0
- package/src/worklet/engine-processor.ts +764 -0
- package/src/worklet/engine-register.ts +136 -0
- package/src/worklet/engine-strips.ts +315 -0
- package/src/worklet/engine-sync.ts +94 -0
- package/src/worklet/engine-tempo-facade.ts +141 -0
- package/src/worklet/engine.ts +998 -0
- package/src/worklet/guards.ts +14 -1
- package/src/worklet/messages.ts +60 -20
- package/src/worklet/mixer-processor.ts +368 -0
- package/src/worklet/protocol.ts +3 -0
- package/src/worklet/voice-changer-processor.ts +246 -0
- package/src/worklet.ts +20 -3549
- package/dist/sonare-rt-module.js +0 -2
- package/dist/sonare-rt.js +0 -2
- package/dist/sonare-rt.wasm +0 -0
- package/src/sonare-rt.d.ts +0 -93
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { RealtimeVoiceChanger } from '../index';
|
|
2
|
+
import type { WorkletInput, WorkletOutput } from './audio_types';
|
|
3
|
+
import { isRealtimeVoiceChangerMessage } from './guards';
|
|
4
|
+
import type {
|
|
5
|
+
SonareRealtimeVoiceChangerMessage,
|
|
6
|
+
SonareRealtimeVoiceChangerWorkletProcessorOptions,
|
|
7
|
+
WorkletPort,
|
|
8
|
+
} from './messages';
|
|
9
|
+
|
|
10
|
+
export class SonareRealtimeVoiceChangerWorkletProcessor {
|
|
11
|
+
private static warnedMonoOverflow = false;
|
|
12
|
+
private static warnedInterleavedOverflow = false;
|
|
13
|
+
private changer: RealtimeVoiceChanger;
|
|
14
|
+
private readonly sampleRate: number;
|
|
15
|
+
private readonly blockSize: number;
|
|
16
|
+
private readonly channelCount: number;
|
|
17
|
+
// WASM-heap typed-memory views, sized to the worst case (blockSize *
|
|
18
|
+
// channelCount). Acquired on the main thread (constructor) so the
|
|
19
|
+
// audio-thread process() never crosses an allocation boundary.
|
|
20
|
+
private monoInput: Float32Array;
|
|
21
|
+
private monoOutput: Float32Array;
|
|
22
|
+
// Planar heap-backed views (one Float32Array per channel) used by the
|
|
23
|
+
// multi-channel path. AudioWorklet inputs/outputs are already planar
|
|
24
|
+
// Float32Arrays, so this avoids the per-sample interleave/deinterleave
|
|
25
|
+
// passes that the older interleaved path needed.
|
|
26
|
+
private planarChannels: Float32Array[];
|
|
27
|
+
private destroyed = false;
|
|
28
|
+
|
|
29
|
+
constructor(options: SonareRealtimeVoiceChangerWorkletProcessorOptions = {}) {
|
|
30
|
+
this.sampleRate = options.sampleRate ?? 48000;
|
|
31
|
+
this.blockSize = options.blockSize ?? 128;
|
|
32
|
+
this.channelCount = Math.max(1, Math.floor(options.channelCount ?? 1));
|
|
33
|
+
this.changer = new RealtimeVoiceChanger(options.preset ?? 'neutral-monitor');
|
|
34
|
+
this.changer.prepare(this.sampleRate, this.blockSize, this.channelCount);
|
|
35
|
+
// Acquire WASM-heap views once, sized to the worst case. These are alive
|
|
36
|
+
// for the lifetime of the changer; if the host requests more frames per
|
|
37
|
+
// process() than blockSize, we clamp (see ensure*Capacity).
|
|
38
|
+
this.monoInput = this.changer.getMonoInputBuffer(this.blockSize);
|
|
39
|
+
this.monoOutput = this.changer.getMonoOutputBuffer(this.blockSize);
|
|
40
|
+
this.planarChannels = [];
|
|
41
|
+
if (this.channelCount > 1) {
|
|
42
|
+
for (let ch = 0; ch < this.channelCount; ch++) {
|
|
43
|
+
this.planarChannels.push(this.changer.getPlanarChannelBuffer(ch, this.blockSize));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Handles a control-plane message from the main thread. Runs on the
|
|
50
|
+
* AudioWorklet global scope but OUTSIDE of `process()` (i.e. outside the
|
|
51
|
+
* realtime audio callback), so it is safe to perform JSON parsing and
|
|
52
|
+
* DSP coefficient recomputation here. `setConfig` MUST NOT be deferred
|
|
53
|
+
* into `process()` because that would block the audio thread for longer
|
|
54
|
+
* than one render quantum (e.g. 128 samples / 44.1 kHz = ~2.9 ms).
|
|
55
|
+
*/
|
|
56
|
+
receiveMessage(message: SonareRealtimeVoiceChangerMessage): void {
|
|
57
|
+
if (this.destroyed) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (message.type === 'setConfig') {
|
|
61
|
+
// Apply synchronously on the message-handler thread. `setConfig` may
|
|
62
|
+
// allocate and parse JSON internally; doing it here keeps `process()`
|
|
63
|
+
// realtime-safe.
|
|
64
|
+
this.changer.setConfig(message.preset);
|
|
65
|
+
} else if (message.type === 'reset') {
|
|
66
|
+
this.changer.reset();
|
|
67
|
+
} else if (message.type === 'destroy') {
|
|
68
|
+
this.destroy();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
process(inputs: WorkletInput, outputs: WorkletOutput): boolean {
|
|
73
|
+
const output = outputs[0];
|
|
74
|
+
if (this.destroyed || !output || output.length === 0) {
|
|
75
|
+
return !this.destroyed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The cached heap views can detach if WASM linear memory grows (the embind
|
|
79
|
+
// module is built ALLOW_MEMORY_GROWTH). Re-acquire them if detached
|
|
80
|
+
// (byteLength === 0) before touching them; in the common no-growth case this
|
|
81
|
+
// is a cheap branch with no allocation.
|
|
82
|
+
if (this.monoInput.byteLength === 0) {
|
|
83
|
+
this.reacquireBuffers();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const input = inputs[0];
|
|
87
|
+
const requestedFrames = output[0]?.length ?? 0;
|
|
88
|
+
const requestedChannels = Math.min(this.channelCount, output.length);
|
|
89
|
+
if (requestedFrames === 0 || requestedChannels === 0) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (requestedChannels === 1) {
|
|
94
|
+
// Clamp to the pre-allocated capacity; warn (once) if the host violated
|
|
95
|
+
// the contract. We never reallocate on the audio thread.
|
|
96
|
+
const frames = this.ensureMonoCapacity(requestedFrames);
|
|
97
|
+
const source = input?.[0];
|
|
98
|
+
if (source) {
|
|
99
|
+
this.monoInput.set(source.subarray(0, frames));
|
|
100
|
+
} else {
|
|
101
|
+
this.monoInput.fill(0, 0, frames);
|
|
102
|
+
}
|
|
103
|
+
this.changer.processMonoInto(
|
|
104
|
+
this.monoInput.subarray(0, frames),
|
|
105
|
+
this.monoOutput.subarray(0, frames),
|
|
106
|
+
);
|
|
107
|
+
output[0].set(this.monoOutput.subarray(0, frames));
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const frames = this.ensureInterleavedCapacity(requestedFrames, requestedChannels);
|
|
112
|
+
const channels = requestedChannels;
|
|
113
|
+
// Planar zero-copy path: AudioWorklet's input[ch] is already a
|
|
114
|
+
// Float32Array per channel, so we set() straight into the heap-backed
|
|
115
|
+
// planar view and processPreparedPlanar runs in place.
|
|
116
|
+
for (let ch = 0; ch < channels; ch++) {
|
|
117
|
+
const src = input?.[ch];
|
|
118
|
+
const dst = this.planarChannels[ch];
|
|
119
|
+
if (!dst) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (src) {
|
|
123
|
+
dst.set(src.subarray(0, frames));
|
|
124
|
+
} else {
|
|
125
|
+
dst.fill(0, 0, frames);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
this.changer.processPreparedPlanar(frames);
|
|
129
|
+
for (let ch = 0; ch < channels; ch++) {
|
|
130
|
+
const src = this.planarChannels[ch];
|
|
131
|
+
if (src) {
|
|
132
|
+
output[ch].set(src.subarray(0, frames));
|
|
133
|
+
}
|
|
134
|
+
// No `for frame` inner loop needed; output[ch] is a Float32Array.
|
|
135
|
+
}
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
destroy(): void {
|
|
140
|
+
if (this.destroyed) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.destroyed = true;
|
|
144
|
+
this.changer.delete();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Re-acquires the cached WASM-heap views after a memory-growth detachment.
|
|
148
|
+
// The underlying C++ vectors are pre-warmed (ensure_*_capacity ran at prepare
|
|
149
|
+
// time), so getMono*/getPlanar* return fresh views onto the SAME storage
|
|
150
|
+
// without reallocating it.
|
|
151
|
+
private reacquireBuffers(): void {
|
|
152
|
+
this.monoInput = this.changer.getMonoInputBuffer(this.blockSize);
|
|
153
|
+
this.monoOutput = this.changer.getMonoOutputBuffer(this.blockSize);
|
|
154
|
+
if (this.channelCount > 1) {
|
|
155
|
+
for (let ch = 0; ch < this.channelCount; ch++) {
|
|
156
|
+
this.planarChannels[ch] = this.changer.getPlanarChannelBuffer(ch, this.blockSize);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Returns the number of frames we can actually process given the
|
|
163
|
+
* pre-allocated capacity. If the host requests more frames than the
|
|
164
|
+
* worst-case block size declared at construction time, we clamp to the
|
|
165
|
+
* available capacity and warn once — we MUST NOT reallocate on the
|
|
166
|
+
* realtime audio thread.
|
|
167
|
+
*/
|
|
168
|
+
private ensureMonoCapacity(frames: number): number {
|
|
169
|
+
const capacity = this.monoInput.length;
|
|
170
|
+
if (frames <= capacity) {
|
|
171
|
+
return frames;
|
|
172
|
+
}
|
|
173
|
+
if (!SonareRealtimeVoiceChangerWorkletProcessor.warnedMonoOverflow) {
|
|
174
|
+
SonareRealtimeVoiceChangerWorkletProcessor.warnedMonoOverflow = true;
|
|
175
|
+
// biome-ignore lint/suspicious/noConsole: realtime-safety diagnostic.
|
|
176
|
+
console.warn(
|
|
177
|
+
`SonareRealtimeVoiceChangerWorkletProcessor: requested ${frames} mono frames ` +
|
|
178
|
+
`exceeds pre-allocated capacity ${capacity}; clamping. ` +
|
|
179
|
+
'Increase blockSize at construction time to avoid this.',
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
return capacity;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Same contract as ensureMonoCapacity but for the planar per-channel
|
|
187
|
+
* scratch. Returns the number of frames that fit in the available capacity.
|
|
188
|
+
*/
|
|
189
|
+
private ensureInterleavedCapacity(frames: number, channels: number): number {
|
|
190
|
+
const capacity = this.planarChannels[0]?.length ?? 0;
|
|
191
|
+
if (frames <= capacity) {
|
|
192
|
+
return frames;
|
|
193
|
+
}
|
|
194
|
+
if (!SonareRealtimeVoiceChangerWorkletProcessor.warnedInterleavedOverflow) {
|
|
195
|
+
SonareRealtimeVoiceChangerWorkletProcessor.warnedInterleavedOverflow = true;
|
|
196
|
+
// biome-ignore lint/suspicious/noConsole: realtime-safety diagnostic.
|
|
197
|
+
console.warn(
|
|
198
|
+
`SonareRealtimeVoiceChangerWorkletProcessor: requested ${frames}x${channels} ` +
|
|
199
|
+
`planar frames exceeds pre-allocated capacity ${capacity}; clamping. ` +
|
|
200
|
+
'Increase blockSize or channelCount at construction time to avoid this.',
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return capacity;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function registerSonareRealtimeVoiceChangerWorkletProcessor(
|
|
208
|
+
name = 'sonare-realtime-voice-changer-processor',
|
|
209
|
+
): void {
|
|
210
|
+
const scope = globalThis as unknown as {
|
|
211
|
+
AudioWorkletProcessor?: new () => object;
|
|
212
|
+
registerProcessor?: (processorName: string, processorCtor: unknown) => void;
|
|
213
|
+
};
|
|
214
|
+
if (!scope.AudioWorkletProcessor || !scope.registerProcessor) {
|
|
215
|
+
throw new Error('AudioWorkletProcessor is not available in this context.');
|
|
216
|
+
}
|
|
217
|
+
const Base = scope.AudioWorkletProcessor;
|
|
218
|
+
class RegisteredSonareRealtimeVoiceChangerWorkletProcessor extends Base {
|
|
219
|
+
private bridge: SonareRealtimeVoiceChangerWorkletProcessor;
|
|
220
|
+
readonly port?: WorkletPort;
|
|
221
|
+
|
|
222
|
+
constructor(options?: {
|
|
223
|
+
processorOptions?: SonareRealtimeVoiceChangerWorkletProcessorOptions;
|
|
224
|
+
}) {
|
|
225
|
+
super();
|
|
226
|
+
const port = this.port;
|
|
227
|
+
this.bridge = new SonareRealtimeVoiceChangerWorkletProcessor(options?.processorOptions ?? {});
|
|
228
|
+
const onMessage = (event: { data: unknown }) => {
|
|
229
|
+
if (isRealtimeVoiceChangerMessage(event.data)) {
|
|
230
|
+
this.bridge.receiveMessage(event.data);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
if (port?.addEventListener) {
|
|
234
|
+
port.addEventListener('message', onMessage);
|
|
235
|
+
port.start?.();
|
|
236
|
+
} else if (port) {
|
|
237
|
+
port.onmessage = onMessage;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
process(inputs: WorkletInput, outputs: WorkletOutput): boolean {
|
|
242
|
+
return this.bridge.process(inputs, outputs);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
scope.registerProcessor(name, RegisteredSonareRealtimeVoiceChangerWorkletProcessor);
|
|
246
|
+
}
|