@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.
Files changed (66) hide show
  1. package/README.md +51 -20
  2. package/dist/index.d.ts +5416 -1
  3. package/dist/index.js +938 -583
  4. package/dist/index.js.map +1 -1
  5. package/dist/sonare.js +2 -2
  6. package/dist/sonare.wasm +0 -0
  7. package/dist/worklet.d.ts +1083 -5227
  8. package/dist/worklet.js +2683 -2451
  9. package/dist/worklet.js.map +1 -1
  10. package/package.json +4 -9
  11. package/src/clip_page_streamer.ts +298 -0
  12. package/src/effects_mastering.ts +85 -1089
  13. package/src/effects_transform.ts +286 -0
  14. package/src/effects_voice_change.ts +118 -0
  15. package/src/feature_music.ts +5 -2
  16. package/src/feature_spectrogram.ts +42 -2
  17. package/src/features.ts +1 -0
  18. package/src/index.ts +23 -0
  19. package/src/mastering_chain.ts +200 -0
  20. package/src/mastering_core.ts +248 -0
  21. package/src/mastering_dynamics.ts +105 -0
  22. package/src/mastering_repair.ts +161 -0
  23. package/src/mixer.ts +8 -0
  24. package/src/mixing_oneshot.ts +54 -0
  25. package/src/module_state.ts +1 -2
  26. package/src/project.ts +71 -1712
  27. package/src/project_class.ts +871 -0
  28. package/src/project_internal.ts +333 -0
  29. package/src/project_synth.ts +43 -0
  30. package/src/project_types.ts +570 -0
  31. package/src/public_types.ts +6 -1221
  32. package/src/public_types_acoustic.ts +115 -0
  33. package/src/public_types_mastering.ts +333 -0
  34. package/src/public_types_mixing.ts +97 -0
  35. package/src/public_types_music.ts +352 -0
  36. package/src/public_types_realtime.ts +163 -0
  37. package/src/public_types_spectral.ts +194 -0
  38. package/src/realtime_engine.ts +94 -0
  39. package/src/sonare.js.d.ts +117 -38
  40. package/src/stream_analyzer.ts +3 -0
  41. package/src/stream_types.ts +4 -0
  42. package/src/worklet/engine-automation.ts +73 -0
  43. package/src/worklet/engine-capture-facade.ts +80 -0
  44. package/src/worklet/engine-clips.ts +71 -0
  45. package/src/worklet/engine-markers.ts +93 -0
  46. package/src/worklet/engine-mixer-facade.ts +186 -0
  47. package/src/worklet/engine-node.ts +451 -0
  48. package/src/worklet/engine-offline.ts +162 -0
  49. package/src/worklet/engine-options.ts +13 -0
  50. package/src/worklet/engine-parameter-facade.ts +172 -0
  51. package/src/worklet/engine-processor.ts +764 -0
  52. package/src/worklet/engine-register.ts +136 -0
  53. package/src/worklet/engine-strips.ts +315 -0
  54. package/src/worklet/engine-sync.ts +94 -0
  55. package/src/worklet/engine-tempo-facade.ts +141 -0
  56. package/src/worklet/engine.ts +998 -0
  57. package/src/worklet/guards.ts +14 -1
  58. package/src/worklet/messages.ts +60 -20
  59. package/src/worklet/mixer-processor.ts +368 -0
  60. package/src/worklet/protocol.ts +3 -0
  61. package/src/worklet/voice-changer-processor.ts +246 -0
  62. package/src/worklet.ts +20 -3549
  63. package/dist/sonare-rt-module.js +0 -2
  64. package/dist/sonare-rt.js +0 -2
  65. package/dist/sonare-rt.wasm +0 -0
  66. 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
+ }