@libraz/libsonare 1.4.0 → 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.
- package/README.md +50 -19
- package/dist/index.d.ts +5384 -1
- package/dist/index.js +884 -574
- package/dist/index.js.map +1 -1
- package/dist/sonare.js +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/worklet.d.ts +1084 -5220
- package/dist/worklet.js +2677 -2452
- package/dist/worklet.js.map +1 -1
- package/package.json +4 -9
- package/src/clip_page_streamer.ts +250 -0
- package/src/effects_mastering.ts +85 -1082
- package/src/effects_transform.ts +286 -0
- package/src/effects_voice_change.ts +118 -0
- package/src/feature_music.ts +13 -9
- package/src/feature_spectrogram.ts +42 -2
- package/src/features.ts +1 -0
- package/src/index.ts +11 -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/mixing_oneshot.ts +54 -0
- package/src/module_state.ts +1 -2
- package/src/project.ts +71 -1688
- package/src/project_class.ts +861 -0
- package/src/project_internal.ts +332 -0
- package/src/project_synth.ts +43 -0
- package/src/project_types.ts +570 -0
- package/src/public_types.ts +6 -1217
- 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/quick_analysis.ts +18 -14
- package/src/realtime_engine.ts +94 -0
- package/src/realtime_voice_changer.ts +2 -1
- package/src/sonare.js.d.ts +74 -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,186 @@
|
|
|
1
|
+
import type { EngineBus, EngineTrackLane, EngineTrackSend, RealtimeEngine } from '../index';
|
|
2
|
+
import { normalizeTrackLanes } from './engine-offline';
|
|
3
|
+
import { buildMixerLanes } from './engine-sync';
|
|
4
|
+
import type { SonareEngineSyncMessage } from './messages';
|
|
5
|
+
import { ENGINE_MIXER_PARAM_FADER_DB, engineMixerBusTarget } from './protocol';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Collaborator surface the mixer/routing setters need from the owning
|
|
9
|
+
* {@link SonareEngine}: the offline engine they mirror writes into, the mutable
|
|
10
|
+
* routing stores (held by reference), the out-of-band sync poster, and the
|
|
11
|
+
* lane/bus declaration and mixer-sync helpers.
|
|
12
|
+
*/
|
|
13
|
+
export interface EngineMixerContext {
|
|
14
|
+
readonly offlineEngine: RealtimeEngine;
|
|
15
|
+
readonly trackLaneIds: number[];
|
|
16
|
+
readonly trackSends: Map<number, EngineTrackSend[]>;
|
|
17
|
+
readonly trackOutputBus: Map<number, number>;
|
|
18
|
+
readonly laneSidechains: Map<
|
|
19
|
+
string,
|
|
20
|
+
{ trackId: number; insertIndex: number; sourceTrackId: number }
|
|
21
|
+
>;
|
|
22
|
+
readonly buses: EngineBus[];
|
|
23
|
+
readonly trackStripJson: Map<number, string>;
|
|
24
|
+
readonly busStripJson: Map<number, string>;
|
|
25
|
+
postSync(message: SonareEngineSyncMessage): void;
|
|
26
|
+
ensureTrackLane(target: string | number): number;
|
|
27
|
+
ensureBus(busId: number): number;
|
|
28
|
+
mixerLanes(): EngineTrackLane[];
|
|
29
|
+
syncMixer(): void;
|
|
30
|
+
sendSmoothedParam(paramId: number, value: number): boolean;
|
|
31
|
+
getMasterStripJson(): string | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Builds the engine's track-lane descriptors from the current routing stores. */
|
|
35
|
+
export function mixerLanes(ctx: EngineMixerContext): EngineTrackLane[] {
|
|
36
|
+
return buildMixerLanes(ctx.trackLaneIds, ctx.trackSends, ctx.trackOutputBus);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Mirrors the current mixer routing into the offline engine and posts the full
|
|
41
|
+
* mixer-sync message (lanes, buses, strip JSON, sidechains) to the worklet.
|
|
42
|
+
*/
|
|
43
|
+
export function syncMixer(ctx: EngineMixerContext): void {
|
|
44
|
+
const lanes = mixerLanes(ctx);
|
|
45
|
+
const buses = ctx.buses.map((bus) => ({ ...bus }));
|
|
46
|
+
ctx.offlineEngine.setTrackBuses(buses);
|
|
47
|
+
if (lanes.length > 0) {
|
|
48
|
+
ctx.offlineEngine.setTrackLanes(lanes);
|
|
49
|
+
}
|
|
50
|
+
const trackStrips = Array.from(ctx.trackStripJson, ([trackId, sceneJson]) => ({
|
|
51
|
+
trackId,
|
|
52
|
+
sceneJson,
|
|
53
|
+
}));
|
|
54
|
+
const busStrips = Array.from(ctx.busStripJson, ([busId, sceneJson]) => ({
|
|
55
|
+
busId,
|
|
56
|
+
sceneJson,
|
|
57
|
+
}));
|
|
58
|
+
ctx.postSync({
|
|
59
|
+
type: 'syncMixer',
|
|
60
|
+
lanes,
|
|
61
|
+
buses,
|
|
62
|
+
trackStrips,
|
|
63
|
+
laneSidechains: Array.from(ctx.laneSidechains.values()),
|
|
64
|
+
busStrips,
|
|
65
|
+
masterStripJson: ctx.getMasterStripJson(),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Declares the mixer track lanes in an explicit order.
|
|
71
|
+
*
|
|
72
|
+
* Lane indices are append-only: once a track id occupies a lane, its index
|
|
73
|
+
* stays fixed for the engine's lifetime. The given list must therefore start
|
|
74
|
+
* with the already-declared lane ids in their current order and may only
|
|
75
|
+
* append new track ids after them. Entries carrying `sends` replace that
|
|
76
|
+
* track's send list; entries without `sends` leave existing sends untouched.
|
|
77
|
+
*
|
|
78
|
+
* @param lanes Track ids or lane descriptors in the desired lane order.
|
|
79
|
+
*/
|
|
80
|
+
export function setTrackLanes(
|
|
81
|
+
ctx: EngineMixerContext,
|
|
82
|
+
lanes: ReadonlyArray<number | EngineTrackLane>,
|
|
83
|
+
): void {
|
|
84
|
+
const { entries, ids } = normalizeTrackLanes(ctx.trackLaneIds, lanes);
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
if (entry.sends) {
|
|
87
|
+
ctx.trackSends.set(
|
|
88
|
+
entry.trackId,
|
|
89
|
+
entry.sends.map((send) => ({ ...send })),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (entry.outputBusId !== undefined) {
|
|
93
|
+
if (entry.outputBusId === 0) {
|
|
94
|
+
ctx.trackOutputBus.delete(entry.trackId);
|
|
95
|
+
} else {
|
|
96
|
+
ctx.trackOutputBus.set(entry.trackId, entry.outputBusId);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
ctx.trackLaneIds.splice(0, ctx.trackLaneIds.length, ...ids);
|
|
101
|
+
ctx.syncMixer();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Routes a track lane's post-fader output into a declared bus instead of
|
|
106
|
+
* the master mix (group/folder routing); busId 0 restores the master mix.
|
|
107
|
+
*/
|
|
108
|
+
export function setTrackOutputBus(
|
|
109
|
+
ctx: EngineMixerContext,
|
|
110
|
+
target: string | number,
|
|
111
|
+
busId: number,
|
|
112
|
+
): void {
|
|
113
|
+
const laneIndex = ctx.ensureTrackLane(target);
|
|
114
|
+
const trackId = ctx.trackLaneIds[laneIndex];
|
|
115
|
+
if (busId === 0) {
|
|
116
|
+
ctx.trackOutputBus.delete(trackId);
|
|
117
|
+
} else {
|
|
118
|
+
ctx.trackOutputBus.set(trackId, busId);
|
|
119
|
+
}
|
|
120
|
+
ctx.syncMixer();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Keys one insert of a lane strip from another lane's post-strip pre-fader
|
|
125
|
+
* audio (ducking/sidechainRouter inserts). sourceTarget null removes the
|
|
126
|
+
* binding.
|
|
127
|
+
*/
|
|
128
|
+
export function setLaneSidechain(
|
|
129
|
+
ctx: EngineMixerContext,
|
|
130
|
+
target: string | number,
|
|
131
|
+
insertIndex: number,
|
|
132
|
+
sourceTarget: string | number | null,
|
|
133
|
+
): void {
|
|
134
|
+
const laneIndex = ctx.ensureTrackLane(target);
|
|
135
|
+
const trackId = ctx.trackLaneIds[laneIndex];
|
|
136
|
+
const key = `${trackId}:${insertIndex}`;
|
|
137
|
+
let sourceTrackId = 0;
|
|
138
|
+
if (sourceTarget !== null) {
|
|
139
|
+
const sourceIndex = ctx.ensureTrackLane(sourceTarget);
|
|
140
|
+
sourceTrackId = ctx.trackLaneIds[sourceIndex];
|
|
141
|
+
}
|
|
142
|
+
if (sourceTrackId === 0) {
|
|
143
|
+
ctx.laneSidechains.delete(key);
|
|
144
|
+
} else {
|
|
145
|
+
ctx.laneSidechains.set(key, { trackId, insertIndex, sourceTrackId });
|
|
146
|
+
}
|
|
147
|
+
ctx.offlineEngine.setLaneSidechain(trackId, insertIndex, sourceTrackId);
|
|
148
|
+
ctx.postSync({
|
|
149
|
+
type: 'syncMixer',
|
|
150
|
+
lanes: ctx.mixerLanes(),
|
|
151
|
+
laneSidechains: [{ trackId, insertIndex, sourceTrackId }],
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function setSends(
|
|
156
|
+
ctx: EngineMixerContext,
|
|
157
|
+
target: string | number,
|
|
158
|
+
sends: EngineTrackSend[],
|
|
159
|
+
): void {
|
|
160
|
+
const laneIndex = ctx.ensureTrackLane(target);
|
|
161
|
+
const trackId = ctx.trackLaneIds[laneIndex];
|
|
162
|
+
ctx.trackSends.set(
|
|
163
|
+
trackId,
|
|
164
|
+
sends.map((send) => ({ ...send })),
|
|
165
|
+
);
|
|
166
|
+
ctx.syncMixer();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function setTrackBuses(ctx: EngineMixerContext, buses: EngineBus[]): void {
|
|
170
|
+
ctx.buses.splice(0, ctx.buses.length, ...buses.map((bus) => ({ ...bus })));
|
|
171
|
+
ctx.syncMixer();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function setBusGain(ctx: EngineMixerContext, busId: number, db: number): boolean {
|
|
175
|
+
const busIndex = ctx.ensureBus(busId);
|
|
176
|
+
ctx.buses[busIndex] = { ...ctx.buses[busIndex], busId, gainDb: db };
|
|
177
|
+
ctx.offlineEngine.setTrackBuses(ctx.buses);
|
|
178
|
+
return ctx.sendSmoothedParam(engineMixerBusTarget(busIndex, ENGINE_MIXER_PARAM_FADER_DB), db);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function setBusStripJson(ctx: EngineMixerContext, busId: number, sceneJson: string): void {
|
|
182
|
+
ctx.ensureBus(busId);
|
|
183
|
+
ctx.offlineEngine.setBusStripJson(busId, sceneJson);
|
|
184
|
+
ctx.busStripJson.set(busId, sceneJson);
|
|
185
|
+
ctx.syncMixer();
|
|
186
|
+
}
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import type { EngineCaptureStatus, EngineTransportState } from '../index';
|
|
2
|
+
import { engineCapabilities } from '../index';
|
|
3
|
+
import {
|
|
4
|
+
isEngineCaptureResponseMessage,
|
|
5
|
+
isEngineTelemetryRecord,
|
|
6
|
+
isEngineTransportResponseMessage,
|
|
7
|
+
isExternalMidiBatchMessage,
|
|
8
|
+
isMeterSnapshot,
|
|
9
|
+
} from './guards';
|
|
10
|
+
import type {
|
|
11
|
+
SonareEngineCaptureRequestMessage,
|
|
12
|
+
SonareEngineCaptureResponseMessage,
|
|
13
|
+
SonareEngineTransportResponseMessage,
|
|
14
|
+
SonareRealtimeEngineNodeCapabilities,
|
|
15
|
+
SonareRealtimeEngineNodeOptions,
|
|
16
|
+
SonareRealtimeEngineWorkletProcessorOptions,
|
|
17
|
+
SonareWorkletExternalMidiEvent,
|
|
18
|
+
} from './messages';
|
|
19
|
+
import {
|
|
20
|
+
createSonareEngineCommandRingBuffer,
|
|
21
|
+
createSonareEngineTelemetryRingBuffer,
|
|
22
|
+
createSonareMeterRingBuffer,
|
|
23
|
+
createSonareScopeRingBuffer,
|
|
24
|
+
isRecord,
|
|
25
|
+
pushSonareEngineCommandRingBuffer,
|
|
26
|
+
readSonareEngineTelemetryRingBuffer,
|
|
27
|
+
readSonareMeterRingBuffer,
|
|
28
|
+
readSonareScopeRingBuffer,
|
|
29
|
+
type SonareEngineCommandRecord,
|
|
30
|
+
type SonareEngineCommandRingBuffer,
|
|
31
|
+
SonareEngineCommandType,
|
|
32
|
+
type SonareEngineTelemetryRecord,
|
|
33
|
+
type SonareEngineTelemetryRingBuffer,
|
|
34
|
+
type SonareMeterRingBuffer,
|
|
35
|
+
type SonareScopeRingBuffer,
|
|
36
|
+
type SonareWorkletMeterSnapshot,
|
|
37
|
+
type SonareWorkletScopeSnapshot,
|
|
38
|
+
} from './protocol';
|
|
39
|
+
|
|
40
|
+
export class SonareRealtimeEngineNode {
|
|
41
|
+
readonly node: AudioWorkletNode;
|
|
42
|
+
readonly capabilities: SonareRealtimeEngineNodeCapabilities;
|
|
43
|
+
readonly commandRing?: SonareEngineCommandRingBuffer;
|
|
44
|
+
readonly telemetryRing?: SonareEngineTelemetryRingBuffer;
|
|
45
|
+
readonly meterRing?: SonareMeterRingBuffer;
|
|
46
|
+
readonly scopeRing?: SonareScopeRingBuffer;
|
|
47
|
+
readonly ready: Promise<void>;
|
|
48
|
+
private telemetryReadIndex = 0;
|
|
49
|
+
private meterReadIndex = 0;
|
|
50
|
+
private scopeReadIndex = 0;
|
|
51
|
+
private telemetryListeners = new Set<(telemetry: SonareEngineTelemetryRecord) => void>();
|
|
52
|
+
private meterListeners = new Set<(meter: SonareWorkletMeterSnapshot) => void>();
|
|
53
|
+
private scopeListeners = new Set<(scope: SonareWorkletScopeSnapshot) => void>();
|
|
54
|
+
private midiOutListeners = new Set<(events: SonareWorkletExternalMidiEvent[]) => void>();
|
|
55
|
+
private captureRequestId = 1;
|
|
56
|
+
private readonly captureRequests = new Map<
|
|
57
|
+
number,
|
|
58
|
+
{
|
|
59
|
+
resolve: (response: SonareEngineCaptureResponseMessage) => void;
|
|
60
|
+
reject: (reason?: unknown) => void;
|
|
61
|
+
}
|
|
62
|
+
>();
|
|
63
|
+
private transportRequestId = 1;
|
|
64
|
+
private readonly transportRequests = new Map<
|
|
65
|
+
number,
|
|
66
|
+
{
|
|
67
|
+
resolve: (response: SonareEngineTransportResponseMessage) => void;
|
|
68
|
+
reject: (reason?: unknown) => void;
|
|
69
|
+
}
|
|
70
|
+
>();
|
|
71
|
+
private resolveReady!: () => void;
|
|
72
|
+
private rejectReady!: (reason?: unknown) => void;
|
|
73
|
+
private destroyed = false;
|
|
74
|
+
|
|
75
|
+
private constructor(
|
|
76
|
+
node: AudioWorkletNode,
|
|
77
|
+
capabilities: SonareRealtimeEngineNodeCapabilities,
|
|
78
|
+
commandRing?: SonareEngineCommandRingBuffer,
|
|
79
|
+
telemetryRing?: SonareEngineTelemetryRingBuffer,
|
|
80
|
+
meterRing?: SonareMeterRingBuffer,
|
|
81
|
+
scopeRing?: SonareScopeRingBuffer,
|
|
82
|
+
) {
|
|
83
|
+
this.node = node;
|
|
84
|
+
this.capabilities = capabilities;
|
|
85
|
+
this.commandRing = commandRing;
|
|
86
|
+
this.telemetryRing = telemetryRing;
|
|
87
|
+
this.meterRing = meterRing;
|
|
88
|
+
this.scopeRing = scopeRing;
|
|
89
|
+
this.ready = new Promise((resolve, reject) => {
|
|
90
|
+
this.resolveReady = resolve;
|
|
91
|
+
this.rejectReady = reject;
|
|
92
|
+
});
|
|
93
|
+
if (!capabilities.readyMessage) {
|
|
94
|
+
this.resolveReady();
|
|
95
|
+
}
|
|
96
|
+
this.node.port.onmessage = (event: MessageEvent<unknown>) => {
|
|
97
|
+
if (isEngineCaptureResponseMessage(event.data)) {
|
|
98
|
+
const pending = this.captureRequests.get(event.data.requestId);
|
|
99
|
+
if (pending) {
|
|
100
|
+
this.captureRequests.delete(event.data.requestId);
|
|
101
|
+
if (event.data.ok) {
|
|
102
|
+
pending.resolve(event.data);
|
|
103
|
+
} else {
|
|
104
|
+
pending.reject(new Error(event.data.error ?? 'Capture request failed'));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} else if (isEngineTransportResponseMessage(event.data)) {
|
|
108
|
+
const pending = this.transportRequests.get(event.data.requestId);
|
|
109
|
+
if (pending) {
|
|
110
|
+
this.transportRequests.delete(event.data.requestId);
|
|
111
|
+
if (event.data.ok) {
|
|
112
|
+
pending.resolve(event.data);
|
|
113
|
+
} else {
|
|
114
|
+
pending.reject(new Error(event.data.error ?? 'Transport request failed'));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} else if (isEngineTelemetryRecord(event.data)) {
|
|
118
|
+
this.emitTelemetry(event.data);
|
|
119
|
+
} else if (isMeterSnapshot(event.data)) {
|
|
120
|
+
this.emitMeter(event.data);
|
|
121
|
+
} else if (isExternalMidiBatchMessage(event.data)) {
|
|
122
|
+
this.emitMidiOut(event.data.events);
|
|
123
|
+
} else if (isRecord(event.data) && event.data.type === 'ready') {
|
|
124
|
+
this.resolveReady();
|
|
125
|
+
} else if (isRecord(event.data) && event.data.type === 'error') {
|
|
126
|
+
this.rejectReady(new Error(String(event.data.message ?? 'AudioWorklet error')));
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
static async create(
|
|
132
|
+
context: BaseAudioContext,
|
|
133
|
+
options: SonareRealtimeEngineNodeOptions = {},
|
|
134
|
+
): Promise<SonareRealtimeEngineNode> {
|
|
135
|
+
const processorName = options.processorName ?? 'sonare-realtime-engine-processor';
|
|
136
|
+
const moduleUrl = options.moduleUrl;
|
|
137
|
+
if (moduleUrl && context.audioWorklet?.addModule) {
|
|
138
|
+
await context.audioWorklet.addModule(moduleUrl);
|
|
139
|
+
}
|
|
140
|
+
const detectedCapabilities =
|
|
141
|
+
options.engineAbiVersion !== undefined
|
|
142
|
+
? {
|
|
143
|
+
engineAbiVersion: options.engineAbiVersion,
|
|
144
|
+
expectedEngineAbiVersion: options.expectedEngineAbiVersion ?? options.engineAbiVersion,
|
|
145
|
+
abiCompatible:
|
|
146
|
+
options.engineAbiVersion ===
|
|
147
|
+
(options.expectedEngineAbiVersion ?? options.engineAbiVersion),
|
|
148
|
+
}
|
|
149
|
+
: engineCapabilities();
|
|
150
|
+
if (options.requireAbiCompatible !== false && detectedCapabilities?.abiCompatible === false) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Engine ABI mismatch: wasm=${detectedCapabilities.engineAbiVersion}, expected=${detectedCapabilities.expectedEngineAbiVersion}`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const sharedArrayBuffer = typeof globalThis.SharedArrayBuffer === 'function';
|
|
156
|
+
const atomics = typeof globalThis.Atomics === 'object';
|
|
157
|
+
const audioWorklet = typeof AudioWorkletNode !== 'undefined' || !!options.nodeFactory;
|
|
158
|
+
const degradedReason =
|
|
159
|
+
options.mode !== 'postMessage' && (!sharedArrayBuffer || !atomics)
|
|
160
|
+
? 'SharedArrayBuffer or Atomics unavailable; using postMessage transport.'
|
|
161
|
+
: undefined;
|
|
162
|
+
const mode =
|
|
163
|
+
options.mode === 'postMessage' || !sharedArrayBuffer || !atomics ? 'postMessage' : 'sab';
|
|
164
|
+
if (options.mode === 'sab' && mode !== 'sab') {
|
|
165
|
+
throw new Error(
|
|
166
|
+
'SharedArrayBuffer mode requested but SharedArrayBuffer/Atomics are unavailable.',
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const commandRing =
|
|
171
|
+
mode === 'sab'
|
|
172
|
+
? createSonareEngineCommandRingBuffer(options.commandRingCapacity ?? 128)
|
|
173
|
+
: undefined;
|
|
174
|
+
const telemetryRing =
|
|
175
|
+
mode === 'sab'
|
|
176
|
+
? createSonareEngineTelemetryRingBuffer(options.telemetryRingCapacity ?? 128)
|
|
177
|
+
: undefined;
|
|
178
|
+
// Meter ring: the engine publishes meters into a SAB ring. Lock-free meter
|
|
179
|
+
// delivery matches the telemetry path and keeps the audio render callback
|
|
180
|
+
// allocation-free in SAB mode.
|
|
181
|
+
const meterRing =
|
|
182
|
+
mode === 'sab' ? createSonareMeterRingBuffer(options.meterRingCapacity ?? 128) : undefined;
|
|
183
|
+
// Scope ring (FFT spectrum + goniometer): opt-in. The per-block FFT is
|
|
184
|
+
// heavier than the meter path, so it is created only when the caller
|
|
185
|
+
// requests scope telemetry via scopeIntervalFrames > 0.
|
|
186
|
+
const scopeIntervalFrames = Math.max(0, Math.floor(options.scopeIntervalFrames ?? 0));
|
|
187
|
+
const scopeRing =
|
|
188
|
+
mode === 'sab' && scopeIntervalFrames > 0
|
|
189
|
+
? createSonareScopeRingBuffer(options.scopeRingCapacity ?? 64, options.scopeBands ?? 48)
|
|
190
|
+
: undefined;
|
|
191
|
+
const channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
|
|
192
|
+
const processorOptions: SonareRealtimeEngineWorkletProcessorOptions = {
|
|
193
|
+
sampleRate: options.sampleRate ?? context.sampleRate,
|
|
194
|
+
blockSize: options.blockSize,
|
|
195
|
+
channelCount,
|
|
196
|
+
commandSharedBuffer: commandRing?.sharedBuffer,
|
|
197
|
+
commandRingCapacity: commandRing?.capacity,
|
|
198
|
+
telemetrySharedBuffer: telemetryRing?.sharedBuffer,
|
|
199
|
+
telemetryRingCapacity: telemetryRing?.capacity,
|
|
200
|
+
meterSharedBuffer: meterRing?.sharedBuffer,
|
|
201
|
+
meterRingCapacity: meterRing?.capacity,
|
|
202
|
+
scopeSharedBuffer: scopeRing?.sharedBuffer,
|
|
203
|
+
scopeRingCapacity: scopeRing?.capacity,
|
|
204
|
+
scopeBands: scopeRing?.bands,
|
|
205
|
+
scopeIntervalFrames: scopeRing ? scopeIntervalFrames : undefined,
|
|
206
|
+
wasmBinary: options.wasmBinary,
|
|
207
|
+
initialSyncMessages: options.initialSyncMessages,
|
|
208
|
+
initialCommands: options.initialCommands,
|
|
209
|
+
};
|
|
210
|
+
const factory =
|
|
211
|
+
options.nodeFactory ??
|
|
212
|
+
((ctx: BaseAudioContext, name: string, nodeOptions: AudioWorkletNodeOptions) =>
|
|
213
|
+
new AudioWorkletNode(ctx, name, nodeOptions));
|
|
214
|
+
const node = factory(context, processorName, {
|
|
215
|
+
numberOfInputs: 1,
|
|
216
|
+
numberOfOutputs: 1,
|
|
217
|
+
outputChannelCount: [channelCount],
|
|
218
|
+
processorOptions,
|
|
219
|
+
});
|
|
220
|
+
return new SonareRealtimeEngineNode(
|
|
221
|
+
node,
|
|
222
|
+
{
|
|
223
|
+
mode,
|
|
224
|
+
runtimeTarget: 'embind',
|
|
225
|
+
sharedArrayBuffer,
|
|
226
|
+
atomics,
|
|
227
|
+
audioWorklet,
|
|
228
|
+
engineAbiVersion: detectedCapabilities?.engineAbiVersion,
|
|
229
|
+
expectedEngineAbiVersion: detectedCapabilities?.expectedEngineAbiVersion,
|
|
230
|
+
abiCompatible: detectedCapabilities?.abiCompatible,
|
|
231
|
+
degradedReason,
|
|
232
|
+
readyMessage: moduleUrl !== undefined && !options.nodeFactory,
|
|
233
|
+
},
|
|
234
|
+
commandRing,
|
|
235
|
+
telemetryRing,
|
|
236
|
+
meterRing,
|
|
237
|
+
scopeRing,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
play(sampleTime = -1): boolean {
|
|
242
|
+
return this.sendCommand({ type: SonareEngineCommandType.TransportPlay, sampleTime });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
stop(sampleTime = -1): boolean {
|
|
246
|
+
return this.sendCommand({ type: SonareEngineCommandType.TransportStop, sampleTime });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
seekSample(timelineSample: number, sampleTime = -1): boolean {
|
|
250
|
+
return this.sendCommand({
|
|
251
|
+
type: SonareEngineCommandType.TransportSeekSample,
|
|
252
|
+
sampleTime,
|
|
253
|
+
argInt: timelineSample,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
seekPpq(ppq: number, sampleTime = -1): boolean {
|
|
258
|
+
return this.sendCommand({
|
|
259
|
+
type: SonareEngineCommandType.TransportSeekPpq,
|
|
260
|
+
sampleTime,
|
|
261
|
+
argFloat: ppq,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
sendCommand(command: SonareEngineCommandRecord): boolean {
|
|
266
|
+
if (this.destroyed) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
if (this.commandRing) {
|
|
270
|
+
return pushSonareEngineCommandRingBuffer(this.commandRing, command);
|
|
271
|
+
}
|
|
272
|
+
this.node.port.postMessage(command);
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
requestCaptureStatus(): Promise<EngineCaptureStatus> {
|
|
277
|
+
return this.sendCaptureRequest('status').then((response) => {
|
|
278
|
+
if (!response.status) {
|
|
279
|
+
throw new Error('Capture status response is missing status.');
|
|
280
|
+
}
|
|
281
|
+
return response.status;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
requestCapturedAudio(): Promise<Float32Array[]> {
|
|
286
|
+
return this.sendCaptureRequest('read').then((response) =>
|
|
287
|
+
(response.channels ?? []).map((channel) =>
|
|
288
|
+
channel instanceof Float32Array ? channel : new Float32Array(channel),
|
|
289
|
+
),
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
requestCaptureReset(): Promise<void> {
|
|
294
|
+
return this.sendCaptureRequest('reset').then(() => undefined);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
requestTransportState(): Promise<EngineTransportState> {
|
|
298
|
+
return this.sendTransportRequest().then((response) => {
|
|
299
|
+
if (!response.state) {
|
|
300
|
+
throw new Error('Transport state response is missing state.');
|
|
301
|
+
}
|
|
302
|
+
return response.state;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
pollTelemetry(): SonareEngineTelemetryRecord[] {
|
|
307
|
+
if (!this.telemetryRing) {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
const read = readSonareEngineTelemetryRingBuffer(this.telemetryRing, this.telemetryReadIndex);
|
|
311
|
+
this.telemetryReadIndex = read.nextReadIndex;
|
|
312
|
+
for (const telemetry of read.telemetry) {
|
|
313
|
+
this.emitTelemetry(telemetry);
|
|
314
|
+
}
|
|
315
|
+
return read.telemetry;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Drains any meters published into the SAB meter ring (embind SAB mode) and
|
|
319
|
+
// forwards them to onMeter listeners. In postMessage mode meters arrive via
|
|
320
|
+
// node.port.onmessage instead, so this is a no-op then.
|
|
321
|
+
pollMeters(): SonareWorkletMeterSnapshot[] {
|
|
322
|
+
if (!this.meterRing) {
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
const read = readSonareMeterRingBuffer(this.meterRing, this.meterReadIndex);
|
|
326
|
+
this.meterReadIndex = read.nextReadIndex;
|
|
327
|
+
for (const meter of read.meters) {
|
|
328
|
+
this.emitMeter(meter);
|
|
329
|
+
}
|
|
330
|
+
return read.meters;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Drains scope telemetry (FFT spectrum + goniometer points) published into the
|
|
334
|
+
// SAB scope ring and forwards each record to onScope listeners. A no-op unless
|
|
335
|
+
// the node was created with scopeIntervalFrames > 0 (embind SAB mode).
|
|
336
|
+
pollScope(): SonareWorkletScopeSnapshot[] {
|
|
337
|
+
if (!this.scopeRing) {
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
const read = readSonareScopeRingBuffer(this.scopeRing, this.scopeReadIndex);
|
|
341
|
+
this.scopeReadIndex = read.nextReadIndex;
|
|
342
|
+
for (const scope of read.scopes) {
|
|
343
|
+
this.emitScope(scope);
|
|
344
|
+
}
|
|
345
|
+
return read.scopes;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
onTelemetry(callback: (telemetry: SonareEngineTelemetryRecord) => void): () => void {
|
|
349
|
+
this.telemetryListeners.add(callback);
|
|
350
|
+
return () => {
|
|
351
|
+
this.telemetryListeners.delete(callback);
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
onMeter(callback: (meter: SonareWorkletMeterSnapshot) => void): () => void {
|
|
356
|
+
this.meterListeners.add(callback);
|
|
357
|
+
return () => {
|
|
358
|
+
this.meterListeners.delete(callback);
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
onScope(callback: (scope: SonareWorkletScopeSnapshot) => void): () => void {
|
|
363
|
+
this.scopeListeners.add(callback);
|
|
364
|
+
return () => {
|
|
365
|
+
this.scopeListeners.delete(callback);
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Subscribe to external-MIDI batches drained from the engine (one call per
|
|
371
|
+
* render block that produced events), already lowered to MIDI 1.0 bytes for a
|
|
372
|
+
* Web MIDI output port. Returns an unsubscribe function.
|
|
373
|
+
*/
|
|
374
|
+
onMidiOut(callback: (events: SonareWorkletExternalMidiEvent[]) => void): () => void {
|
|
375
|
+
this.midiOutListeners.add(callback);
|
|
376
|
+
return () => {
|
|
377
|
+
this.midiOutListeners.delete(callback);
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
destroy(): void {
|
|
382
|
+
if (this.destroyed) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
this.destroyed = true;
|
|
386
|
+
this.node.port.postMessage({ type: SonareEngineCommandType.TransportStop, sampleTime: -1 });
|
|
387
|
+
this.node.disconnect();
|
|
388
|
+
for (const pending of this.captureRequests.values()) {
|
|
389
|
+
pending.reject(new Error('Realtime engine node is destroyed.'));
|
|
390
|
+
}
|
|
391
|
+
this.captureRequests.clear();
|
|
392
|
+
for (const pending of this.transportRequests.values()) {
|
|
393
|
+
pending.reject(new Error('Realtime engine node is destroyed.'));
|
|
394
|
+
}
|
|
395
|
+
this.transportRequests.clear();
|
|
396
|
+
this.telemetryListeners.clear();
|
|
397
|
+
this.meterListeners.clear();
|
|
398
|
+
this.scopeListeners.clear();
|
|
399
|
+
this.midiOutListeners.clear();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private emitTelemetry(telemetry: SonareEngineTelemetryRecord): void {
|
|
403
|
+
for (const listener of this.telemetryListeners) {
|
|
404
|
+
listener(telemetry);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private emitMeter(meter: SonareWorkletMeterSnapshot): void {
|
|
409
|
+
for (const listener of this.meterListeners) {
|
|
410
|
+
listener(meter);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private emitMidiOut(events: SonareWorkletExternalMidiEvent[]): void {
|
|
415
|
+
for (const listener of this.midiOutListeners) {
|
|
416
|
+
listener(events);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private emitScope(scope: SonareWorkletScopeSnapshot): void {
|
|
421
|
+
for (const listener of this.scopeListeners) {
|
|
422
|
+
listener(scope);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private sendCaptureRequest(
|
|
427
|
+
op: SonareEngineCaptureRequestMessage['op'],
|
|
428
|
+
): Promise<SonareEngineCaptureResponseMessage> {
|
|
429
|
+
if (this.destroyed) {
|
|
430
|
+
return Promise.reject(new Error('Realtime engine node is destroyed.'));
|
|
431
|
+
}
|
|
432
|
+
const requestId = this.captureRequestId++;
|
|
433
|
+
const promise = new Promise<SonareEngineCaptureResponseMessage>((resolve, reject) => {
|
|
434
|
+
this.captureRequests.set(requestId, { resolve, reject });
|
|
435
|
+
});
|
|
436
|
+
this.node.port.postMessage({ type: 'captureRequest', requestId, op });
|
|
437
|
+
return promise;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private sendTransportRequest(): Promise<SonareEngineTransportResponseMessage> {
|
|
441
|
+
if (this.destroyed) {
|
|
442
|
+
return Promise.reject(new Error('Realtime engine node is destroyed.'));
|
|
443
|
+
}
|
|
444
|
+
const requestId = this.transportRequestId++;
|
|
445
|
+
const promise = new Promise<SonareEngineTransportResponseMessage>((resolve, reject) => {
|
|
446
|
+
this.transportRequests.set(requestId, { resolve, reject });
|
|
447
|
+
});
|
|
448
|
+
this.node.port.postMessage({ type: 'transportRequest', requestId, op: 'state' });
|
|
449
|
+
return promise;
|
|
450
|
+
}
|
|
451
|
+
}
|