@libraz/libsonare 1.4.1 → 1.5.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 (62) hide show
  1. package/README.md +50 -19
  2. package/dist/index.d.ts +5384 -1
  3. package/dist/index.js +867 -573
  4. package/dist/index.js.map +1 -1
  5. package/dist/sonare.js +1 -1
  6. package/dist/sonare.wasm +0 -0
  7. package/dist/worklet.d.ts +1083 -5227
  8. package/dist/worklet.js +2677 -2452
  9. package/dist/worklet.js.map +1 -1
  10. package/package.json +4 -9
  11. package/src/clip_page_streamer.ts +250 -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_spectrogram.ts +42 -2
  16. package/src/features.ts +1 -0
  17. package/src/index.ts +11 -0
  18. package/src/mastering_chain.ts +200 -0
  19. package/src/mastering_core.ts +248 -0
  20. package/src/mastering_dynamics.ts +105 -0
  21. package/src/mastering_repair.ts +161 -0
  22. package/src/mixing_oneshot.ts +54 -0
  23. package/src/module_state.ts +1 -2
  24. package/src/project.ts +71 -1712
  25. package/src/project_class.ts +861 -0
  26. package/src/project_internal.ts +332 -0
  27. package/src/project_synth.ts +43 -0
  28. package/src/project_types.ts +570 -0
  29. package/src/public_types.ts +6 -1221
  30. package/src/public_types_acoustic.ts +115 -0
  31. package/src/public_types_mastering.ts +333 -0
  32. package/src/public_types_mixing.ts +97 -0
  33. package/src/public_types_music.ts +352 -0
  34. package/src/public_types_realtime.ts +163 -0
  35. package/src/public_types_spectral.ts +194 -0
  36. package/src/realtime_engine.ts +94 -0
  37. package/src/sonare.js.d.ts +72 -0
  38. package/src/worklet/engine-automation.ts +73 -0
  39. package/src/worklet/engine-capture-facade.ts +80 -0
  40. package/src/worklet/engine-clips.ts +71 -0
  41. package/src/worklet/engine-markers.ts +93 -0
  42. package/src/worklet/engine-mixer-facade.ts +186 -0
  43. package/src/worklet/engine-node.ts +451 -0
  44. package/src/worklet/engine-offline.ts +162 -0
  45. package/src/worklet/engine-options.ts +13 -0
  46. package/src/worklet/engine-parameter-facade.ts +172 -0
  47. package/src/worklet/engine-processor.ts +764 -0
  48. package/src/worklet/engine-register.ts +136 -0
  49. package/src/worklet/engine-strips.ts +315 -0
  50. package/src/worklet/engine-sync.ts +94 -0
  51. package/src/worklet/engine-tempo-facade.ts +141 -0
  52. package/src/worklet/engine.ts +998 -0
  53. package/src/worklet/guards.ts +14 -1
  54. package/src/worklet/messages.ts +60 -20
  55. package/src/worklet/mixer-processor.ts +368 -0
  56. package/src/worklet/protocol.ts +3 -0
  57. package/src/worklet/voice-changer-processor.ts +246 -0
  58. package/src/worklet.ts +20 -3549
  59. package/dist/sonare-rt-module.js +0 -2
  60. package/dist/sonare-rt.js +0 -2
  61. package/dist/sonare-rt.wasm +0 -0
  62. package/src/sonare-rt.d.ts +0 -93
@@ -0,0 +1,136 @@
1
+ import { init as initSonareModule, isInitialized } from '../index';
2
+ import type { SonareModule } from '../sonare.js';
3
+ import type { WorkletInput, WorkletOutput } from './audio_types';
4
+ import { SonareRealtimeEngineWorkletProcessor } from './engine-processor';
5
+ import {
6
+ isEngineCaptureRequestMessage,
7
+ isEngineCommandRecord,
8
+ isEngineSyncMessage,
9
+ isEngineTransportRequestMessage,
10
+ } from './guards';
11
+ import type { SonareRealtimeEngineWorkletProcessorOptions, WorkletPort } from './messages';
12
+
13
+ export function registerSonareRealtimeEngineWorkletProcessor(
14
+ name = 'sonare-realtime-engine-processor',
15
+ ): void {
16
+ const scope = globalThis as unknown as {
17
+ AudioWorkletProcessor?: new () => object;
18
+ registerProcessor?: (processorName: string, processorCtor: unknown) => void;
19
+ };
20
+ if (!scope.AudioWorkletProcessor || !scope.registerProcessor) {
21
+ throw new Error('AudioWorkletProcessor is not available in this context.');
22
+ }
23
+ const Base = scope.AudioWorkletProcessor;
24
+ class RegisteredSonareRealtimeEngineWorkletProcessor extends Base {
25
+ private bridge?: SonareRealtimeEngineWorkletProcessor;
26
+ private readonly pendingMessages: unknown[] = [];
27
+ readonly port?: WorkletPort;
28
+
29
+ constructor(options?: { processorOptions?: SonareRealtimeEngineWorkletProcessorOptions }) {
30
+ super();
31
+ const port = this.port;
32
+ const processorOptions = options?.processorOptions ?? {};
33
+ void this.initializeEmbind(processorOptions, port);
34
+ const onMessage = (event: { data: unknown }) => {
35
+ if (!this.bridge) {
36
+ if (this.pendingMessages.length < 1024) {
37
+ this.pendingMessages.push(event.data);
38
+ }
39
+ return;
40
+ }
41
+ if (isEngineCommandRecord(event.data)) {
42
+ this.bridge.receiveCommand(event.data);
43
+ } else if (isEngineSyncMessage(event.data)) {
44
+ this.bridge.receiveSync(event.data);
45
+ } else if (isEngineCaptureRequestMessage(event.data)) {
46
+ this.bridge.receiveCaptureRequest(event.data);
47
+ } else if (isEngineTransportRequestMessage(event.data)) {
48
+ this.bridge.receiveTransportRequest(event.data);
49
+ }
50
+ };
51
+ if (port?.addEventListener) {
52
+ port.addEventListener('message', onMessage);
53
+ port.start?.();
54
+ } else if (port) {
55
+ port.onmessage = onMessage;
56
+ }
57
+ }
58
+
59
+ process(inputs: WorkletInput, outputs: WorkletOutput): boolean {
60
+ if (this.bridge) {
61
+ return this.bridge.process(inputs, outputs);
62
+ }
63
+ const output = outputs[0];
64
+ for (const channel of output ?? []) {
65
+ channel.fill(0);
66
+ }
67
+ return true;
68
+ }
69
+
70
+ private replayPendingMessages(): void {
71
+ const messages = this.pendingMessages.splice(0);
72
+ for (const data of messages) {
73
+ if (isEngineCommandRecord(data)) {
74
+ this.bridge?.receiveCommand(data);
75
+ } else if (isEngineSyncMessage(data)) {
76
+ this.bridge?.receiveSync(data);
77
+ } else if (isEngineCaptureRequestMessage(data)) {
78
+ this.bridge?.receiveCaptureRequest(data);
79
+ } else if (isEngineTransportRequestMessage(data)) {
80
+ this.bridge?.receiveTransportRequest(data);
81
+ }
82
+ }
83
+ }
84
+
85
+ private async initializeEmbind(
86
+ options: SonareRealtimeEngineWorkletProcessorOptions,
87
+ port?: WorkletPort,
88
+ ): Promise<void> {
89
+ try {
90
+ const initPromise = (
91
+ globalThis as typeof globalThis & { SonareEmbindInitPromise?: Promise<void> }
92
+ ).SonareEmbindInitPromise;
93
+ if (initPromise) {
94
+ await initPromise;
95
+ }
96
+ if (!isInitialized()) {
97
+ type EmbindModuleFactory = (options?: {
98
+ locateFile?: (path: string, prefix: string) => string;
99
+ wasmBinary?: ArrayBuffer | Uint8Array;
100
+ }) => Promise<SonareModule>;
101
+ const moduleFactory = (
102
+ globalThis as typeof globalThis & {
103
+ SonareEmbindModuleFactory?: EmbindModuleFactory;
104
+ }
105
+ ).SonareEmbindModuleFactory;
106
+ if (!moduleFactory) {
107
+ throw new Error('embind realtime engine module is not initialized.');
108
+ }
109
+ await initSonareModule({
110
+ locateFile: (path) => path,
111
+ wasmBinary: options.wasmBinary,
112
+ moduleFactory,
113
+ });
114
+ }
115
+ this.bridge = new SonareRealtimeEngineWorkletProcessor(options, {
116
+ postMessage: (message) => port?.postMessage?.(message),
117
+ onMeter: (meter) => port?.postMessage?.(meter),
118
+ });
119
+ for (const message of options.initialSyncMessages ?? []) {
120
+ this.bridge.receiveSync(message);
121
+ }
122
+ for (const command of options.initialCommands ?? []) {
123
+ this.bridge.receiveCommand(command);
124
+ }
125
+ this.replayPendingMessages();
126
+ port?.postMessage?.({ type: 'ready', runtimeTarget: 'embind' });
127
+ } catch (error) {
128
+ port?.postMessage?.({
129
+ type: 'error',
130
+ message: error instanceof Error ? error.message : String(error),
131
+ });
132
+ }
133
+ }
134
+ }
135
+ scope.registerProcessor(name, RegisteredSonareRealtimeEngineWorkletProcessor);
136
+ }
@@ -0,0 +1,315 @@
1
+ import { panLawCode, panModeCode } from '../codes';
2
+ import type { EqBand, PanLaw, PanMode, RealtimeEngine } from '../index';
3
+ import type { SonareEngineInstrumentSyncMessage, SonareEngineSyncMessage } from './messages';
4
+
5
+ /**
6
+ * Collaborator surface the strip/pan/EQ/insert/instrument/MIDI setters need from
7
+ * the owning {@link SonareEngine}: the offline engine they mirror writes into,
8
+ * the out-of-band sync posters, and the lane-resolution helpers.
9
+ */
10
+ export interface EngineStripContext {
11
+ readonly offlineEngine: RealtimeEngine;
12
+ readonly trackLaneIds: number[];
13
+ postSync(message: SonareEngineSyncMessage): void;
14
+ postInstrumentSync(message: SonareEngineInstrumentSyncMessage): void;
15
+ ensureTrackLane(target: string | number): number;
16
+ resolveTargetId(target: string | number): number;
17
+ }
18
+
19
+ function trackIdFor(ctx: EngineStripContext, target: string | number): number {
20
+ return ctx.trackLaneIds[ctx.ensureTrackLane(target)];
21
+ }
22
+
23
+ export function setTrackStripJson(
24
+ ctx: EngineStripContext,
25
+ trackId: number,
26
+ sceneJson: string,
27
+ trackStripJson: Map<number, string>,
28
+ ): void {
29
+ ctx.offlineEngine.setTrackStripJson(trackId, sceneJson);
30
+ trackStripJson.set(trackId, sceneJson);
31
+ }
32
+
33
+ export function setTrackStripEqBand(
34
+ ctx: EngineStripContext,
35
+ target: string | number,
36
+ bandIndex: number,
37
+ band: EqBand | string,
38
+ ): void {
39
+ const trackId = trackIdFor(ctx, target);
40
+ const bandJson = typeof band === 'string' ? band : JSON.stringify(band);
41
+ ctx.offlineEngine.setTrackStripEqBandJson(trackId, bandIndex, bandJson);
42
+ ctx.postSync({ type: 'syncTrackStripEqBand', trackId, bandIndex, bandJson });
43
+ }
44
+
45
+ export function setTrackStripInsertBypassed(
46
+ ctx: EngineStripContext,
47
+ target: string | number,
48
+ insertIndex: number,
49
+ bypassed: boolean,
50
+ resetOnBypass: boolean,
51
+ ): void {
52
+ const trackId = trackIdFor(ctx, target);
53
+ ctx.offlineEngine.setTrackStripInsertBypassed(trackId, insertIndex, bypassed, resetOnBypass);
54
+ ctx.postSync({
55
+ type: 'syncTrackStripInsertBypassed',
56
+ trackId,
57
+ insertIndex,
58
+ bypassed,
59
+ resetOnBypass,
60
+ });
61
+ }
62
+
63
+ export function setTrackStripInsertParamByName(
64
+ ctx: EngineStripContext,
65
+ target: string | number,
66
+ insertIndex: number,
67
+ paramName: string,
68
+ value: number,
69
+ ): void {
70
+ const trackId = trackIdFor(ctx, target);
71
+ ctx.offlineEngine.setTrackStripInsertParamByName(trackId, insertIndex, paramName, value);
72
+ ctx.postSync({ type: 'syncTrackStripInsertParamByName', trackId, insertIndex, paramName, value });
73
+ }
74
+
75
+ export function setTrackStripPan(
76
+ ctx: EngineStripContext,
77
+ target: string | number,
78
+ pan: number,
79
+ ): void {
80
+ const trackId = trackIdFor(ctx, target);
81
+ ctx.offlineEngine.setTrackStripPan(trackId, pan);
82
+ ctx.postSync({ type: 'syncTrackStripPan', trackId, pan });
83
+ }
84
+
85
+ export function setTrackStripPanLaw(
86
+ ctx: EngineStripContext,
87
+ target: string | number,
88
+ panLaw: PanLaw | number,
89
+ ): void {
90
+ const trackId = trackIdFor(ctx, target);
91
+ const code = panLawCode(panLaw);
92
+ ctx.offlineEngine.setTrackStripPanLaw(trackId, code);
93
+ ctx.postSync({ type: 'syncTrackStripPanLaw', trackId, panLaw: code });
94
+ }
95
+
96
+ export function setTrackStripPanMode(
97
+ ctx: EngineStripContext,
98
+ target: string | number,
99
+ panMode: PanMode | number,
100
+ ): void {
101
+ const trackId = trackIdFor(ctx, target);
102
+ const code = panModeCode(panMode);
103
+ ctx.offlineEngine.setTrackStripPanMode(trackId, code);
104
+ ctx.postSync({ type: 'syncTrackStripPanMode', trackId, panMode: code });
105
+ }
106
+
107
+ export function setTrackStripDualPan(
108
+ ctx: EngineStripContext,
109
+ target: string | number,
110
+ leftPan: number,
111
+ rightPan: number,
112
+ ): void {
113
+ const trackId = trackIdFor(ctx, target);
114
+ ctx.offlineEngine.setTrackStripDualPan(trackId, leftPan, rightPan);
115
+ ctx.postSync({ type: 'syncTrackStripDualPan', trackId, leftPan, rightPan });
116
+ }
117
+
118
+ export function setTrackStripChannelDelaySamples(
119
+ ctx: EngineStripContext,
120
+ target: string | number,
121
+ delaySamples: number,
122
+ ): void {
123
+ const trackId = trackIdFor(ctx, target);
124
+ ctx.offlineEngine.setTrackStripChannelDelaySamples(trackId, delaySamples);
125
+ ctx.postSync({ type: 'syncTrackStripChannelDelaySamples', trackId, delaySamples });
126
+ }
127
+
128
+ export function setMasterStripEqBand(
129
+ ctx: EngineStripContext,
130
+ bandIndex: number,
131
+ band: EqBand | string,
132
+ ): void {
133
+ const bandJson = typeof band === 'string' ? band : JSON.stringify(band);
134
+ ctx.offlineEngine.setMasterStripEqBandJson(bandIndex, bandJson);
135
+ ctx.postSync({ type: 'syncMasterStripEqBand', bandIndex, bandJson });
136
+ }
137
+
138
+ export function setMasterStripInsertBypassed(
139
+ ctx: EngineStripContext,
140
+ insertIndex: number,
141
+ bypassed: boolean,
142
+ resetOnBypass: boolean,
143
+ ): void {
144
+ ctx.offlineEngine.setMasterStripInsertBypassed(insertIndex, bypassed, resetOnBypass);
145
+ ctx.postSync({ type: 'syncMasterStripInsertBypassed', insertIndex, bypassed, resetOnBypass });
146
+ }
147
+
148
+ export function setMasterStripInsertParamByName(
149
+ ctx: EngineStripContext,
150
+ insertIndex: number,
151
+ paramName: string,
152
+ value: number,
153
+ ): void {
154
+ ctx.offlineEngine.setMasterStripInsertParamByName(insertIndex, paramName, value);
155
+ ctx.postSync({ type: 'syncMasterStripInsertParamByName', insertIndex, paramName, value });
156
+ }
157
+
158
+ export function setBusStripInsertParamByName(
159
+ ctx: EngineStripContext,
160
+ busId: number,
161
+ insertIndex: number,
162
+ paramName: string,
163
+ value: number,
164
+ ): void {
165
+ ctx.offlineEngine.setBusStripInsertParamByName(busId, insertIndex, paramName, value);
166
+ ctx.postSync({ type: 'syncBusStripInsertParamByName', busId, insertIndex, paramName, value });
167
+ }
168
+
169
+ export function pushMidiNoteOn(
170
+ ctx: EngineStripContext,
171
+ trackId: string | number,
172
+ group: number,
173
+ channel: number,
174
+ note: number,
175
+ velocity: number,
176
+ renderFrame: number,
177
+ ): void {
178
+ const destinationId = ctx.resolveTargetId(trackId);
179
+ ctx.offlineEngine.pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame);
180
+ ctx.postSync({
181
+ type: 'syncMidiNoteOn',
182
+ destinationId,
183
+ group,
184
+ channel,
185
+ note,
186
+ velocity,
187
+ renderFrame,
188
+ });
189
+ }
190
+
191
+ export function pushMidiNoteOff(
192
+ ctx: EngineStripContext,
193
+ trackId: string | number,
194
+ group: number,
195
+ channel: number,
196
+ note: number,
197
+ velocity: number,
198
+ renderFrame: number,
199
+ ): void {
200
+ const destinationId = ctx.resolveTargetId(trackId);
201
+ ctx.offlineEngine.pushMidiNoteOff(destinationId, group, channel, note, velocity, renderFrame);
202
+ ctx.postSync({
203
+ type: 'syncMidiNoteOff',
204
+ destinationId,
205
+ group,
206
+ channel,
207
+ note,
208
+ velocity,
209
+ renderFrame,
210
+ });
211
+ }
212
+
213
+ export function pushMidiCc(
214
+ ctx: EngineStripContext,
215
+ trackId: string | number,
216
+ group: number,
217
+ channel: number,
218
+ controller: number,
219
+ value: number,
220
+ renderFrame: number,
221
+ ): void {
222
+ const destinationId = ctx.resolveTargetId(trackId);
223
+ ctx.offlineEngine.pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
224
+ ctx.postSync({
225
+ type: 'syncMidiCc',
226
+ destinationId,
227
+ group,
228
+ channel,
229
+ controller,
230
+ value,
231
+ renderFrame,
232
+ });
233
+ }
234
+
235
+ export function setBuiltinInstrument(
236
+ ctx: EngineStripContext,
237
+ trackId: string | number,
238
+ config: { destinationId?: number } & Record<string, unknown>,
239
+ ): void {
240
+ const destinationId = ctx.resolveTargetId(trackId);
241
+ ctx.offlineEngine.setBuiltinInstrument(config, destinationId);
242
+ ctx.postInstrumentSync({ type: 'syncBuiltinInstrument', destinationId, config });
243
+ }
244
+
245
+ export function setSynthInstrument(
246
+ ctx: EngineStripContext,
247
+ trackId: string | number,
248
+ patch: Record<string, unknown> | string,
249
+ ): void {
250
+ const destinationId = ctx.resolveTargetId(trackId);
251
+ ctx.offlineEngine.setSynthInstrument(patch, destinationId);
252
+ ctx.postInstrumentSync({ type: 'syncSynthInstrument', destinationId, patch });
253
+ }
254
+
255
+ export function loadSoundFont(ctx: EngineStripContext, data: Uint8Array): void {
256
+ ctx.offlineEngine.loadSoundFont(data);
257
+ ctx.postInstrumentSync({ type: 'syncLoadSoundFont', data });
258
+ }
259
+
260
+ export function setSf2Instrument(
261
+ ctx: EngineStripContext,
262
+ trackId: string | number,
263
+ config: { destinationId?: number; gain?: number; polyphony?: number },
264
+ ): void {
265
+ const destinationId = ctx.resolveTargetId(trackId);
266
+ ctx.offlineEngine.setSf2Instrument(config, destinationId);
267
+ ctx.postInstrumentSync({ type: 'syncSf2Instrument', destinationId, config });
268
+ }
269
+
270
+ export function setMidiDestinationExternal(
271
+ ctx: EngineStripContext,
272
+ trackId: string | number,
273
+ external: boolean,
274
+ ): void {
275
+ const destinationId = ctx.resolveTargetId(trackId);
276
+ ctx.offlineEngine.setMidiDestinationExternal(destinationId, external);
277
+ ctx.postSync({ type: 'syncMidiDestinationExternal', destinationId, external });
278
+ }
279
+
280
+ export function setExternalMidiClockEnabled(ctx: EngineStripContext, enabled: boolean): void {
281
+ ctx.offlineEngine.setExternalMidiClockEnabled(enabled);
282
+ ctx.postSync({ type: 'syncExternalMidiClock', enabled });
283
+ }
284
+
285
+ export function setMidiFx(
286
+ ctx: EngineStripContext,
287
+ trackId: string | number,
288
+ configJson: string,
289
+ ): void {
290
+ const destinationId = ctx.resolveTargetId(trackId);
291
+ ctx.offlineEngine.setMidiFx(destinationId, configJson);
292
+ ctx.postInstrumentSync({ type: 'syncMidiFx', destinationId, configJson });
293
+ }
294
+
295
+ export function clearMidiFx(ctx: EngineStripContext, trackId: string | number): void {
296
+ const destinationId = ctx.resolveTargetId(trackId);
297
+ ctx.offlineEngine.clearMidiFx(destinationId);
298
+ ctx.postInstrumentSync({ type: 'syncClearMidiFx', destinationId });
299
+ }
300
+
301
+ export function pushMidiSysex(
302
+ ctx: EngineStripContext,
303
+ trackId: string | number,
304
+ data: Uint8Array,
305
+ renderFrame: number,
306
+ ): void {
307
+ const destinationId = ctx.resolveTargetId(trackId);
308
+ ctx.offlineEngine.pushMidiSysex(destinationId, data, renderFrame);
309
+ ctx.postSync({ type: 'syncMidiSysex', destinationId, data, renderFrame });
310
+ }
311
+
312
+ export function pushMidiPanic(ctx: EngineStripContext, renderFrame: number): void {
313
+ ctx.offlineEngine.pushMidiPanic(renderFrame);
314
+ ctx.postSync({ type: 'syncMidiPanic', renderFrame });
315
+ }
@@ -0,0 +1,94 @@
1
+ import type {
2
+ EngineParameterInfo,
3
+ EngineTempoSegment,
4
+ EngineTimeSignatureSegment,
5
+ EngineTrackLane,
6
+ EngineTrackSend,
7
+ } from '../index';
8
+ import type { SonareEngineSyncTempoMessage } from './messages';
9
+
10
+ /**
11
+ * Builds the ordered mixer-lane descriptors for a sync message.
12
+ *
13
+ * Each declared track id is paired with its current send list and output bus
14
+ * (both omitted when absent), defensively copying the send entries.
15
+ *
16
+ * @param trackLaneIds Lane order (track ids) declared on the engine.
17
+ * @param trackSends Per-track send lists.
18
+ * @param trackOutputBus Per-track output bus routing.
19
+ * @returns Lane descriptors in lane order.
20
+ */
21
+ export function buildMixerLanes(
22
+ trackLaneIds: readonly number[],
23
+ trackSends: ReadonlyMap<number, EngineTrackSend[]>,
24
+ trackOutputBus: ReadonlyMap<number, number>,
25
+ ): EngineTrackLane[] {
26
+ return trackLaneIds.map((trackId) => {
27
+ const sends = trackSends.get(trackId);
28
+ const outputBusId = trackOutputBus.get(trackId);
29
+ return {
30
+ trackId,
31
+ ...(sends && sends.length > 0 ? { sends: sends.map((send) => ({ ...send })) } : {}),
32
+ ...(outputBusId !== undefined ? { outputBusId } : {}),
33
+ };
34
+ });
35
+ }
36
+
37
+ /**
38
+ * Builds the out-of-band tempo/time-signature sync message, deep-copying the
39
+ * tempo and time-signature segments so the consumer cannot alias engine state.
40
+ */
41
+ export function buildTempoSync(
42
+ tempoBpm: number,
43
+ timeSignature: { numerator: number; denominator: number },
44
+ tempoSegments: readonly EngineTempoSegment[],
45
+ timeSignatureSegments: readonly EngineTimeSignatureSegment[],
46
+ ): SonareEngineSyncTempoMessage {
47
+ return {
48
+ type: 'syncTempo',
49
+ bpm: tempoBpm,
50
+ timeSignature: { ...timeSignature },
51
+ tempoSegments: tempoSegments.map((segment) => ({ ...segment })),
52
+ timeSignatureSegments: timeSignatureSegments.map((segment) => ({ ...segment })),
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Resolves a target id from a string or number; non-numeric strings resolve to
58
+ * 0, mirroring the engine's integer-id namespace.
59
+ */
60
+ export function resolveTargetId(target: string | number): number {
61
+ if (typeof target === 'number') {
62
+ return target;
63
+ }
64
+ const parsed = Number.parseInt(target, 10);
65
+ return Number.isFinite(parsed) ? parsed : 0;
66
+ }
67
+
68
+ /**
69
+ * Resolves a parameter id from a name or numeric id. Numeric params pass
70
+ * through; named params are matched against the parameter list, then fall back
71
+ * to a target-id resolution of the name (or the node id).
72
+ */
73
+ export function resolveParamId(
74
+ parameters: readonly EngineParameterInfo[],
75
+ nodeId: string,
76
+ param: string | number,
77
+ ): number {
78
+ if (typeof param === 'number') {
79
+ return param;
80
+ }
81
+ const byName = parameters.find((info) => info.name === param);
82
+ if (byName) {
83
+ return byName.id;
84
+ }
85
+ return resolveTargetId(param || nodeId);
86
+ }
87
+
88
+ /** Encodes an automation curve descriptor to the engine's numeric curve code. */
89
+ export function curveCode(curve: number | 'linear' | 'exponential'): number {
90
+ if (typeof curve === 'number') {
91
+ return curve;
92
+ }
93
+ return curve === 'exponential' ? 1 : 0;
94
+ }
@@ -0,0 +1,141 @@
1
+ import type {
2
+ EngineTempoSegment,
3
+ EngineTimeSignatureSegment,
4
+ EngineTransportState,
5
+ RealtimeEngine,
6
+ } from '../index';
7
+ import type { SonareRealtimeEngineNode } from './engine-node';
8
+ import { buildTempoSync } from './engine-sync';
9
+ import type { SonareEngineSyncMessage } from './messages';
10
+ import { SonareEngineCommandType } from './protocol';
11
+
12
+ interface TimeSignature {
13
+ numerator: number;
14
+ denominator: number;
15
+ }
16
+
17
+ /**
18
+ * Collaborator surface the tempo / time-signature / loop / transport helpers
19
+ * need from the owning {@link SonareEngine}: the offline engine they mirror
20
+ * into, the realtime node they command and query, the out-of-band sync poster,
21
+ * and getters/setters for the tempo-map and cached-transport state the engine
22
+ * holds (so the helpers mutate it by reference).
23
+ */
24
+ export interface EngineTempoContext {
25
+ readonly offlineEngine: RealtimeEngine;
26
+ readonly realtimeNode: SonareRealtimeEngineNode;
27
+ postSync(message: SonareEngineSyncMessage): void;
28
+ getTempoBpm(): number;
29
+ setTempoBpm(bpm: number): void;
30
+ getTimeSignature(): TimeSignature;
31
+ setTimeSignature(signature: TimeSignature): void;
32
+ getTempoSegments(): EngineTempoSegment[];
33
+ setTempoSegments(segments: EngineTempoSegment[]): void;
34
+ getTimeSignatureSegments(): EngineTimeSignatureSegment[];
35
+ setTimeSignatureSegments(segments: EngineTimeSignatureSegment[]): void;
36
+ setLatestTransportState(state: EngineTransportState): void;
37
+ getLatestTransportState(): EngineTransportState | undefined;
38
+ }
39
+
40
+ // Posts the full tempo/time-signature map to the worklet engine processor.
41
+ export function postTempoSync(ctx: EngineTempoContext): void {
42
+ ctx.postSync(
43
+ buildTempoSync(
44
+ ctx.getTempoBpm(),
45
+ ctx.getTimeSignature(),
46
+ ctx.getTempoSegments(),
47
+ ctx.getTimeSignatureSegments(),
48
+ ),
49
+ );
50
+ }
51
+
52
+ export function setTempo(ctx: EngineTempoContext, bpm: number): void {
53
+ ctx.setTempoBpm(bpm);
54
+ ctx.setTempoSegments([{ startPpq: 0, bpm }]);
55
+ ctx.offlineEngine.setTempo(bpm);
56
+ postTempoSync(ctx);
57
+ ctx.realtimeNode.sendCommand({
58
+ type: SonareEngineCommandType.SetTempoMap,
59
+ sampleTime: -1,
60
+ argFloat: bpm,
61
+ });
62
+ }
63
+
64
+ export function setTempoSegments(
65
+ ctx: EngineTempoContext,
66
+ segments: readonly EngineTempoSegment[],
67
+ ): void {
68
+ const copied = segments.map((segment) => ({ ...segment }));
69
+ ctx.setTempoSegments(copied);
70
+ ctx.setTempoBpm(copied[0]?.bpm ?? ctx.getTempoBpm());
71
+ ctx.offlineEngine.setTempoSegments(copied);
72
+ postTempoSync(ctx);
73
+ }
74
+
75
+ export function setTimeSignature(
76
+ ctx: EngineTempoContext,
77
+ numerator: number,
78
+ denominator: number,
79
+ ): void {
80
+ ctx.setTimeSignature({ numerator, denominator });
81
+ ctx.setTimeSignatureSegments([{ startPpq: 0, numerator, denominator }]);
82
+ ctx.offlineEngine.setTimeSignature(numerator, denominator);
83
+ postTempoSync(ctx);
84
+ }
85
+
86
+ export function setTimeSignatureSegments(
87
+ ctx: EngineTempoContext,
88
+ segments: readonly EngineTimeSignatureSegment[],
89
+ ): void {
90
+ const copied = segments.map((segment) => ({ ...segment }));
91
+ ctx.setTimeSignatureSegments(copied);
92
+ const first = copied[0];
93
+ if (first) {
94
+ ctx.setTimeSignature({ numerator: first.numerator, denominator: first.denominator });
95
+ }
96
+ ctx.offlineEngine.setTimeSignatureSegments(copied);
97
+ postTempoSync(ctx);
98
+ }
99
+
100
+ export function setLoop(
101
+ ctx: EngineTempoContext,
102
+ startPpq: number,
103
+ endPpq: number,
104
+ enabled = true,
105
+ ): boolean {
106
+ ctx.offlineEngine.setLoop(startPpq, endPpq, enabled);
107
+ // Transport precision contract: the SAB command record carries exactly one
108
+ // Float64 lane (argFloat) and one Int64 lane (argInt). startPpq travels in
109
+ // argFloat with full double precision, matching the offline engine; endPpq
110
+ // is carried as micro-PPQ (round(endPpq * 1e6)) in the integer lane and
111
+ // divided back by 1e6 on the consumer. Loop ENDS are therefore snapped to
112
+ // the nearest 1e-6 PPQ over the realtime transport (max 5e-7 PPQ drift),
113
+ // while loop STARTS and the offline path stay exact. This is intentional:
114
+ // the record has no second free Float64 lane, and a micro-PPQ grid on the
115
+ // loop end is well below audible/sample-accurate resolution at any tempo.
116
+ return ctx.realtimeNode.sendCommand({
117
+ type: SonareEngineCommandType.SetLoop,
118
+ targetId: enabled ? 1 : 0,
119
+ sampleTime: -1,
120
+ argFloat: startPpq,
121
+ argInt: Math.round(endPpq * 1_000_000),
122
+ });
123
+ }
124
+
125
+ export function countInEndSample(
126
+ ctx: EngineTempoContext,
127
+ startSample: number,
128
+ bars: number,
129
+ ): number {
130
+ return ctx.offlineEngine.countInEndSample(startSample, bars);
131
+ }
132
+
133
+ export async function getTransportState(ctx: EngineTempoContext): Promise<EngineTransportState> {
134
+ const state = await ctx.realtimeNode.requestTransportState();
135
+ ctx.setLatestTransportState(state);
136
+ return state;
137
+ }
138
+
139
+ export function cachedTransportState(ctx: EngineTempoContext): EngineTransportState | undefined {
140
+ return ctx.getLatestTransportState();
141
+ }