@libraz/libsonare 1.6.0 → 1.7.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 +2 -2
- package/dist/analysis.d.ts +139 -11
- package/dist/analysis.js +162 -42
- package/dist/analysis.js.map +1 -1
- package/dist/index.d.ts +211 -39
- package/dist/index.js +501 -142
- package/dist/index.js.map +1 -1
- package/dist/schemas/realtime-voice-changer-preset.schema.json +2 -2
- package/dist/sonare-analysis.js +1 -1
- package/dist/sonare-analysis.wasm +0 -0
- package/dist/sonare.js +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/worker.js +56 -29
- package/dist/worker.js.map +1 -1
- package/dist/worklet.d.ts +67 -18
- package/dist/worklet.js +193 -42
- package/dist/worklet.js.map +1 -1
- package/package.json +3 -3
- package/src/_chain_config.ts +6 -1
- package/src/analysis_helpers.ts +34 -29
- package/src/audio.ts +7 -2
- package/src/codes.ts +47 -11
- package/src/effects_mastering.ts +5 -0
- package/src/effects_transform.ts +202 -22
- package/src/feature_music.ts +8 -1
- package/src/feature_spectral.ts +64 -2
- package/src/feature_spectrogram.ts +44 -3
- package/src/index.ts +28 -3
- package/src/mastering_core.ts +70 -0
- package/src/metering.ts +18 -0
- package/src/mixer.ts +3 -3
- package/src/project.ts +6 -0
- package/src/project_class.ts +143 -16
- package/src/project_internal.ts +14 -5
- package/src/project_types.ts +50 -9
- package/src/public_types_mastering.ts +9 -0
- package/src/public_types_mixing.ts +16 -0
- package/src/public_types_realtime.ts +23 -5
- package/src/public_types_spectral.ts +16 -2
- package/src/quick_analysis.ts +21 -3
- package/src/realtime_engine.ts +14 -3
- package/src/sonare.js.d.ts +87 -0
- package/src/stream_analyzer.ts +6 -1
- package/src/web_midi.ts +10 -7
- package/src/worklet/engine-node.ts +54 -19
- package/src/worklet/engine-parameter-facade.ts +30 -1
- package/src/worklet/engine-processor.ts +52 -20
- package/src/worklet/engine-register.ts +1 -1
- package/src/worklet/engine-strips.ts +2 -2
- package/src/worklet/engine.ts +10 -0
- package/src/worklet/guards.ts +99 -7
- package/src/worklet/messages.ts +41 -1
- package/src/worklet/protocol.ts +4 -0
package/src/sonare.js.d.ts
CHANGED
|
@@ -699,6 +699,10 @@ export interface WasmProjectSource {
|
|
|
699
699
|
storageHandleId: number;
|
|
700
700
|
sampleRateHint: number;
|
|
701
701
|
nameOrUri: string;
|
|
702
|
+
/** Owning content hash for audio sources; empty for MIDI sources. */
|
|
703
|
+
contentHash: string;
|
|
704
|
+
/** External source-separation role for audio sources; empty for MIDI sources. */
|
|
705
|
+
externalStemRole: string;
|
|
702
706
|
}
|
|
703
707
|
|
|
704
708
|
export interface WasmEngineMetronomeConfig {
|
|
@@ -914,6 +918,7 @@ export interface WasmRealtimeEngine {
|
|
|
914
918
|
setParameterSmoothed: (paramId: number, value: number, renderFrame: number) => void;
|
|
915
919
|
setParamSmoothingMs: (smoothingMs: number) => void;
|
|
916
920
|
setSoloMute: (laneIndex: number, solo: boolean, mute: boolean, renderFrame: number) => void;
|
|
921
|
+
setTrackMonitorMode: (laneIndex: number, mode: number, renderFrame: number) => void;
|
|
917
922
|
getTransportState: () => WasmEngineTransportState;
|
|
918
923
|
play: (renderFrame: number) => void;
|
|
919
924
|
stop: (renderFrame: number) => void;
|
|
@@ -1234,6 +1239,12 @@ export interface SonareModule {
|
|
|
1234
1239
|
sampleRate: number,
|
|
1235
1240
|
nOctaveBands: number,
|
|
1236
1241
|
) => WasmAcousticResult;
|
|
1242
|
+
analyzeImpulseResponseEx: (
|
|
1243
|
+
samples: Float32Array,
|
|
1244
|
+
sampleRate: number,
|
|
1245
|
+
nOctaveBands: number,
|
|
1246
|
+
minDecayDb: number,
|
|
1247
|
+
) => WasmAcousticResult;
|
|
1237
1248
|
detectAcoustic: (
|
|
1238
1249
|
samples: Float32Array,
|
|
1239
1250
|
sampleRate: number,
|
|
@@ -1317,6 +1328,11 @@ export interface SonareModule {
|
|
|
1317
1328
|
hopLength: number,
|
|
1318
1329
|
) => number;
|
|
1319
1330
|
meteringCrestFactorDb: (samples: Float32Array, sampleRate: number) => number;
|
|
1331
|
+
meteringCrestFactorDbStereo: (
|
|
1332
|
+
left: Float32Array,
|
|
1333
|
+
right: Float32Array,
|
|
1334
|
+
sampleRate: number,
|
|
1335
|
+
) => number;
|
|
1320
1336
|
meteringDcOffset: (samples: Float32Array, sampleRate: number) => number;
|
|
1321
1337
|
meteringTruePeakDb: (
|
|
1322
1338
|
samples: Float32Array,
|
|
@@ -1473,12 +1489,30 @@ export interface SonareModule {
|
|
|
1473
1489
|
kernelHarmonic: number,
|
|
1474
1490
|
kernelPercussive: number,
|
|
1475
1491
|
) => WasmHpssResult;
|
|
1492
|
+
hpssEx: (
|
|
1493
|
+
samples: Float32Array,
|
|
1494
|
+
sampleRate: number,
|
|
1495
|
+
kernelHarmonic: number,
|
|
1496
|
+
kernelPercussive: number,
|
|
1497
|
+
nFft: number,
|
|
1498
|
+
hopLength: number,
|
|
1499
|
+
hardMask: boolean,
|
|
1500
|
+
) => WasmHpssResult;
|
|
1476
1501
|
hpssWithResidual: (
|
|
1477
1502
|
samples: Float32Array,
|
|
1478
1503
|
sampleRate: number,
|
|
1479
1504
|
kernelHarmonic: number,
|
|
1480
1505
|
kernelPercussive: number,
|
|
1481
1506
|
) => WasmHpssWithResidualResult;
|
|
1507
|
+
hpssWithResidualEx: (
|
|
1508
|
+
samples: Float32Array,
|
|
1509
|
+
sampleRate: number,
|
|
1510
|
+
kernelHarmonic: number,
|
|
1511
|
+
kernelPercussive: number,
|
|
1512
|
+
nFft: number,
|
|
1513
|
+
hopLength: number,
|
|
1514
|
+
hardMask: boolean,
|
|
1515
|
+
) => WasmHpssWithResidualResult;
|
|
1482
1516
|
decompose: (
|
|
1483
1517
|
s: Float32Array,
|
|
1484
1518
|
nFeatures: number,
|
|
@@ -1520,7 +1554,21 @@ export interface SonareModule {
|
|
|
1520
1554
|
harmonic: (samples: Float32Array, sampleRate: number) => Float32Array;
|
|
1521
1555
|
percussive: (samples: Float32Array, sampleRate: number) => Float32Array;
|
|
1522
1556
|
timeStretch: (samples: Float32Array, sampleRate: number, rate: number) => Float32Array;
|
|
1557
|
+
timeStretchEx: (
|
|
1558
|
+
samples: Float32Array,
|
|
1559
|
+
sampleRate: number,
|
|
1560
|
+
rate: number,
|
|
1561
|
+
nFft: number,
|
|
1562
|
+
hopLength: number,
|
|
1563
|
+
) => Float32Array;
|
|
1523
1564
|
pitchShift: (samples: Float32Array, sampleRate: number, semitones: number) => Float32Array;
|
|
1565
|
+
pitchShiftEx: (
|
|
1566
|
+
samples: Float32Array,
|
|
1567
|
+
sampleRate: number,
|
|
1568
|
+
semitones: number,
|
|
1569
|
+
nFft: number,
|
|
1570
|
+
hopLength: number,
|
|
1571
|
+
) => Float32Array;
|
|
1524
1572
|
pitchCorrectToMidi: (
|
|
1525
1573
|
samples: Float32Array,
|
|
1526
1574
|
sampleRate: number,
|
|
@@ -1582,6 +1630,12 @@ export interface SonareModule {
|
|
|
1582
1630
|
channels: number,
|
|
1583
1631
|
) => Float32Array;
|
|
1584
1632
|
normalize: (samples: Float32Array, sampleRate: number, targetDb: number) => Float32Array;
|
|
1633
|
+
normalizeEx: (
|
|
1634
|
+
samples: Float32Array,
|
|
1635
|
+
sampleRate: number,
|
|
1636
|
+
targetDb: number,
|
|
1637
|
+
mode: string,
|
|
1638
|
+
) => Float32Array;
|
|
1585
1639
|
mastering: (
|
|
1586
1640
|
samples: Float32Array,
|
|
1587
1641
|
sampleRate: number,
|
|
@@ -1644,6 +1698,24 @@ export interface SonareModule {
|
|
|
1644
1698
|
sampleRate: number,
|
|
1645
1699
|
platforms: Array<{ name: string; targetLufs: number; ceilingDb: number }>,
|
|
1646
1700
|
) => string;
|
|
1701
|
+
masteringAssistantSuggestStereo: (
|
|
1702
|
+
left: Float32Array,
|
|
1703
|
+
right: Float32Array,
|
|
1704
|
+
sampleRate: number,
|
|
1705
|
+
params: Record<string, number | boolean>,
|
|
1706
|
+
) => string;
|
|
1707
|
+
masteringAudioProfileStereo: (
|
|
1708
|
+
left: Float32Array,
|
|
1709
|
+
right: Float32Array,
|
|
1710
|
+
sampleRate: number,
|
|
1711
|
+
params: Record<string, number | boolean>,
|
|
1712
|
+
) => string;
|
|
1713
|
+
masteringStreamingPreviewStereo: (
|
|
1714
|
+
left: Float32Array,
|
|
1715
|
+
right: Float32Array,
|
|
1716
|
+
sampleRate: number,
|
|
1717
|
+
platforms: Array<{ name: string; targetLufs: number; ceilingDb: number }>,
|
|
1718
|
+
) => string;
|
|
1647
1719
|
masteringRepairDeclick: (
|
|
1648
1720
|
samples: Float32Array,
|
|
1649
1721
|
sampleRate: number,
|
|
@@ -1760,6 +1832,13 @@ export interface SonareModule {
|
|
|
1760
1832
|
options: Record<string, unknown>,
|
|
1761
1833
|
) => WasmMixResult;
|
|
1762
1834
|
trim: (samples: Float32Array, sampleRate: number, thresholdDb: number) => Float32Array;
|
|
1835
|
+
trimEx: (
|
|
1836
|
+
samples: Float32Array,
|
|
1837
|
+
sampleRate: number,
|
|
1838
|
+
thresholdDb: number,
|
|
1839
|
+
frameLength: number,
|
|
1840
|
+
hopLength: number,
|
|
1841
|
+
) => Float32Array;
|
|
1763
1842
|
spectralEdit: (
|
|
1764
1843
|
samples: Float32Array,
|
|
1765
1844
|
sampleRate: number,
|
|
@@ -2190,6 +2269,14 @@ export interface SonareModule {
|
|
|
2190
2269
|
stftBlendWeight: number,
|
|
2191
2270
|
stftBlendNFft: number,
|
|
2192
2271
|
) => WasmNnlsChromaResult;
|
|
2272
|
+
nnlsChromaEx: (
|
|
2273
|
+
samples: Float32Array,
|
|
2274
|
+
sampleRate: number,
|
|
2275
|
+
enableStftBlend: boolean,
|
|
2276
|
+
stftBlendWeight: number,
|
|
2277
|
+
stftBlendNFft: number,
|
|
2278
|
+
hopLength: number,
|
|
2279
|
+
) => WasmNnlsChromaResult;
|
|
2193
2280
|
cqt: (
|
|
2194
2281
|
samples: Float32Array,
|
|
2195
2282
|
sampleRate: number,
|
package/src/stream_analyzer.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveOrdinalInRange } from './codes';
|
|
1
2
|
import { getSonareModule } from './module_state';
|
|
2
3
|
import type { ChordQuality, PitchClass } from './public_types';
|
|
3
4
|
import type { WasmStreamAnalyzer } from './sonare.js';
|
|
@@ -73,6 +74,10 @@ export class StreamAnalyzer {
|
|
|
73
74
|
) {
|
|
74
75
|
throw new TypeError('outputFormat must be the integer 0 (Float32)');
|
|
75
76
|
}
|
|
77
|
+
const window =
|
|
78
|
+
config.window === undefined
|
|
79
|
+
? undefined
|
|
80
|
+
: resolveOrdinalInRange(config.window, 0, 3, 'stream analyzer window');
|
|
76
81
|
const module = getSonareModule();
|
|
77
82
|
const defaults = streamAnalyzerConfigDefaults();
|
|
78
83
|
this.analyzer = new module.StreamAnalyzer(
|
|
@@ -94,7 +99,7 @@ export class StreamAnalyzer {
|
|
|
94
99
|
config.maxProgressionEntries ?? defaults.maxProgressionEntries,
|
|
95
100
|
config.keyUpdateIntervalSec ?? defaults.keyUpdateIntervalSec,
|
|
96
101
|
config.bpmUpdateIntervalSec ?? defaults.bpmUpdateIntervalSec,
|
|
97
|
-
|
|
102
|
+
window ?? defaults.window,
|
|
98
103
|
config.outputFormat ?? defaults.outputFormat,
|
|
99
104
|
);
|
|
100
105
|
}
|
package/src/web_midi.ts
CHANGED
|
@@ -214,6 +214,10 @@ export async function bindWebMidi(
|
|
|
214
214
|
bound.delete(input.id);
|
|
215
215
|
};
|
|
216
216
|
|
|
217
|
+
// Does not call notify() itself: both call sites below (the initial sync
|
|
218
|
+
// and stateListener's fallback branch) already notify exactly once after
|
|
219
|
+
// calling this, so notifying here too would fire onInputsChanged twice per
|
|
220
|
+
// hotplug event that goes through the fallback branch.
|
|
217
221
|
const refreshInputs = () => {
|
|
218
222
|
for (const [, entry] of bound) {
|
|
219
223
|
if (!shouldBind(entry.input)) {
|
|
@@ -223,7 +227,6 @@ export async function bindWebMidi(
|
|
|
223
227
|
for (const [, input] of iterInputs(access)) {
|
|
224
228
|
bindInput(input);
|
|
225
229
|
}
|
|
226
|
-
notify();
|
|
227
230
|
};
|
|
228
231
|
|
|
229
232
|
const stateListener = (event: MidiConnectionEventLike) => {
|
|
@@ -244,6 +247,7 @@ export async function bindWebMidi(
|
|
|
244
247
|
};
|
|
245
248
|
|
|
246
249
|
refreshInputs();
|
|
250
|
+
notify();
|
|
247
251
|
if (access.addEventListener) {
|
|
248
252
|
access.addEventListener('statechange', stateListener);
|
|
249
253
|
} else {
|
|
@@ -368,12 +372,11 @@ function dispatchUmpMessage(
|
|
|
368
372
|
if (status === 0x8) {
|
|
369
373
|
engine.pushMidiInputNoteOff(group, channel, data1, (word1 >>> 25) & 0x7f, portTimeSamples);
|
|
370
374
|
} else if (status === 0x9) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
375
|
+
// MIDI 2.0 note-on is distinct from note-off (status 0x8). Downsample
|
|
376
|
+
// its 16-bit velocity to the engine's 7-bit API, retaining a nonzero
|
|
377
|
+
// note-on even when the top seven bits are all zero.
|
|
378
|
+
const velocity = Math.max(1, (word1 >>> 25) & 0x7f);
|
|
379
|
+
engine.pushMidiInputNoteOn(group, channel, data1, velocity, portTimeSamples);
|
|
377
380
|
} else if (status === 0xb) {
|
|
378
381
|
engine.pushMidiInputCc(group, channel, data1, (word1 >>> 25) & 0x7f, portTimeSamples);
|
|
379
382
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { EngineCaptureStatus, EngineTransportState } from '../index';
|
|
2
2
|
import { engineCapabilities } from '../index';
|
|
3
3
|
import {
|
|
4
|
+
engineCaptureResponseRequestId,
|
|
4
5
|
isClipPageRequestMessage,
|
|
6
|
+
isEngineCaptureResponseForOperation,
|
|
5
7
|
isEngineCaptureResponseMessage,
|
|
6
8
|
isEngineTelemetryRecord,
|
|
7
9
|
isEngineTransportResponseMessage,
|
|
@@ -10,7 +12,7 @@ import {
|
|
|
10
12
|
} from './guards';
|
|
11
13
|
import type {
|
|
12
14
|
SonareEngineCaptureRequestMessage,
|
|
13
|
-
|
|
15
|
+
SonareEngineCaptureResponseMessageInternal,
|
|
14
16
|
SonareEngineClipPageRequestMessage,
|
|
15
17
|
SonareEngineSyncErrorMessage,
|
|
16
18
|
SonareEngineTransportResponseMessage,
|
|
@@ -53,12 +55,28 @@ function isFiniteInteger(value: number | bigint | undefined): boolean {
|
|
|
53
55
|
return typeof value === 'bigint' || (Number.isFinite(value) && Number.isSafeInteger(value));
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
function isTrackMonitorMode(value: number | bigint | undefined): boolean {
|
|
59
|
+
if (value === undefined) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const mode = typeof value === 'bigint' ? Number(value) : value;
|
|
63
|
+
return typeof mode === 'number' && Number.isSafeInteger(mode) && mode >= 0 && mode <= 2;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A track-monitor command cannot infer a lane: its target is a uint32 index. */
|
|
67
|
+
function isTrackMonitorLaneIndex(value: number | undefined): boolean {
|
|
68
|
+
return (
|
|
69
|
+
typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= 0xffff_ffff
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
56
73
|
function isValidCommandRecord(command: SonareEngineCommandRecord): boolean {
|
|
57
74
|
const type = Number(command.type);
|
|
58
75
|
if (
|
|
59
76
|
!Number.isSafeInteger(type) ||
|
|
60
77
|
type < SonareEngineCommandType.SetParam ||
|
|
61
|
-
type > SonareEngineCommandType.SeekMarker
|
|
78
|
+
(type > SonareEngineCommandType.SeekMarker &&
|
|
79
|
+
type !== SonareEngineCommandType.SetTrackMonitorMode)
|
|
62
80
|
) {
|
|
63
81
|
return false;
|
|
64
82
|
}
|
|
@@ -66,7 +84,9 @@ function isValidCommandRecord(command: SonareEngineCommandRecord): boolean {
|
|
|
66
84
|
(command.targetId === undefined || Number.isSafeInteger(command.targetId)) &&
|
|
67
85
|
(command.argFloat === undefined || Number.isFinite(command.argFloat)) &&
|
|
68
86
|
isFiniteInteger(command.argInt) &&
|
|
69
|
-
isFiniteInteger(command.sampleTime)
|
|
87
|
+
isFiniteInteger(command.sampleTime) &&
|
|
88
|
+
(type !== SonareEngineCommandType.SetTrackMonitorMode ||
|
|
89
|
+
(isTrackMonitorLaneIndex(command.targetId) && isTrackMonitorMode(command.argInt)))
|
|
70
90
|
);
|
|
71
91
|
}
|
|
72
92
|
|
|
@@ -109,7 +129,8 @@ export class SonareRealtimeEngineNode {
|
|
|
109
129
|
private readonly captureRequests = new Map<
|
|
110
130
|
number,
|
|
111
131
|
{
|
|
112
|
-
|
|
132
|
+
op: SonareEngineCaptureRequestMessage['op'];
|
|
133
|
+
resolve: (response: SonareEngineCaptureResponseMessageInternal) => void;
|
|
113
134
|
reject: (reason?: unknown) => void;
|
|
114
135
|
}
|
|
115
136
|
>();
|
|
@@ -151,14 +172,19 @@ export class SonareRealtimeEngineNode {
|
|
|
151
172
|
this.resolveReady();
|
|
152
173
|
}
|
|
153
174
|
this.node.port.onmessage = (event: MessageEvent<unknown>) => {
|
|
154
|
-
|
|
155
|
-
|
|
175
|
+
const captureRequestId = engineCaptureResponseRequestId(event.data);
|
|
176
|
+
if (captureRequestId !== undefined) {
|
|
177
|
+
const pending = this.captureRequests.get(captureRequestId);
|
|
156
178
|
if (pending) {
|
|
157
|
-
this.captureRequests.delete(
|
|
158
|
-
if (event.data
|
|
179
|
+
this.captureRequests.delete(captureRequestId);
|
|
180
|
+
if (!isEngineCaptureResponseMessage(event.data)) {
|
|
181
|
+
pending.reject(new Error('Malformed capture response.'));
|
|
182
|
+
} else if (!isEngineCaptureResponseForOperation(event.data, pending.op)) {
|
|
183
|
+
pending.reject(new Error('Capture response does not match request operation.'));
|
|
184
|
+
} else if (event.data.ok) {
|
|
159
185
|
pending.resolve(event.data);
|
|
160
186
|
} else {
|
|
161
|
-
pending.reject(new Error(event.data.error
|
|
187
|
+
pending.reject(new Error(event.data.error));
|
|
162
188
|
}
|
|
163
189
|
}
|
|
164
190
|
} else if (isEngineTransportResponseMessage(event.data)) {
|
|
@@ -375,7 +401,7 @@ export class SonareRealtimeEngineNode {
|
|
|
375
401
|
|
|
376
402
|
requestCaptureStatus(): Promise<EngineCaptureStatus> {
|
|
377
403
|
return this.sendCaptureRequest('status').then((response) => {
|
|
378
|
-
if (!response.status) {
|
|
404
|
+
if (!response.ok || !('status' in response)) {
|
|
379
405
|
throw new Error('Capture status response is missing status.');
|
|
380
406
|
}
|
|
381
407
|
return response.status;
|
|
@@ -383,11 +409,12 @@ export class SonareRealtimeEngineNode {
|
|
|
383
409
|
}
|
|
384
410
|
|
|
385
411
|
requestCapturedAudio(): Promise<Float32Array[]> {
|
|
386
|
-
return this.sendCaptureRequest('read').then((response) =>
|
|
387
|
-
(response.channels
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
412
|
+
return this.sendCaptureRequest('read').then((response) => {
|
|
413
|
+
if (!response.ok || !('channels' in response)) {
|
|
414
|
+
throw new Error('Capture read response is missing channels.');
|
|
415
|
+
}
|
|
416
|
+
return response.channels;
|
|
417
|
+
});
|
|
391
418
|
}
|
|
392
419
|
|
|
393
420
|
requestCaptureReset(): Promise<void> {
|
|
@@ -529,6 +556,7 @@ export class SonareRealtimeEngineNode {
|
|
|
529
556
|
this.startRingPolling();
|
|
530
557
|
return () => {
|
|
531
558
|
this.midiOutListeners.delete(callback);
|
|
559
|
+
this.stopRingPollingIfUnused();
|
|
532
560
|
};
|
|
533
561
|
}
|
|
534
562
|
|
|
@@ -633,15 +661,22 @@ export class SonareRealtimeEngineNode {
|
|
|
633
661
|
|
|
634
662
|
private sendCaptureRequest(
|
|
635
663
|
op: SonareEngineCaptureRequestMessage['op'],
|
|
636
|
-
): Promise<
|
|
664
|
+
): Promise<SonareEngineCaptureResponseMessageInternal> {
|
|
637
665
|
if (this.destroyed) {
|
|
638
666
|
return Promise.reject(new Error('Realtime engine node is destroyed.'));
|
|
639
667
|
}
|
|
640
668
|
const requestId = this.captureRequestId++;
|
|
641
|
-
|
|
642
|
-
|
|
669
|
+
let rejectRequest!: (reason?: unknown) => void;
|
|
670
|
+
const promise = new Promise<SonareEngineCaptureResponseMessageInternal>((resolve, reject) => {
|
|
671
|
+
rejectRequest = reject;
|
|
672
|
+
this.captureRequests.set(requestId, { op, resolve, reject });
|
|
643
673
|
});
|
|
644
|
-
|
|
674
|
+
try {
|
|
675
|
+
this.node.port.postMessage({ type: 'captureRequest', requestId, op });
|
|
676
|
+
} catch (error) {
|
|
677
|
+
this.captureRequests.delete(requestId);
|
|
678
|
+
rejectRequest(error);
|
|
679
|
+
}
|
|
645
680
|
return promise;
|
|
646
681
|
}
|
|
647
682
|
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { trackMonitorModeCode } from '../codes';
|
|
2
|
+
import type {
|
|
3
|
+
EngineAutomationPoint,
|
|
4
|
+
EngineParameterInfo,
|
|
5
|
+
EngineTrackMonitorMode,
|
|
6
|
+
RealtimeEngine,
|
|
7
|
+
} from '../index';
|
|
2
8
|
import type { SonareEngineSyncMessage } from './messages';
|
|
3
9
|
import {
|
|
4
10
|
ENGINE_MIXER_PARAM_FADER_DB,
|
|
@@ -67,6 +73,29 @@ export function setSoloMute(
|
|
|
67
73
|
});
|
|
68
74
|
}
|
|
69
75
|
|
|
76
|
+
export function setTrackMonitorMode(
|
|
77
|
+
ctx: EngineParameterContext,
|
|
78
|
+
target: string | number,
|
|
79
|
+
mode: EngineTrackMonitorMode,
|
|
80
|
+
renderFrame = -1,
|
|
81
|
+
): boolean {
|
|
82
|
+
// Resolve the mode before declaring a lane or touching either engine. This
|
|
83
|
+
// keeps booleans, fractions, and unknown spellings from reaching the queue.
|
|
84
|
+
const modeOrdinal = trackMonitorModeCode(mode);
|
|
85
|
+
const laneIndex = ctx.ensureTrackLane(target);
|
|
86
|
+
ctx.offlineEngine.setTrackMonitorMode(
|
|
87
|
+
laneIndex,
|
|
88
|
+
modeOrdinal as EngineTrackMonitorMode,
|
|
89
|
+
renderFrame,
|
|
90
|
+
);
|
|
91
|
+
return ctx.sendCommand({
|
|
92
|
+
type: SonareEngineCommandType.SetTrackMonitorMode,
|
|
93
|
+
targetId: laneIndex,
|
|
94
|
+
sampleTime: renderFrame,
|
|
95
|
+
argInt: modeOrdinal,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
70
99
|
/**
|
|
71
100
|
* Returns the automation target id for a mixer strip parameter.
|
|
72
101
|
*
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type ResolvedMetronomeConfig,
|
|
7
7
|
resolveMetronomeConfig,
|
|
8
8
|
type SonareEngineCaptureRequestMessage,
|
|
9
|
-
type
|
|
9
|
+
type SonareEngineCaptureResponseMessageInternal,
|
|
10
10
|
type SonareEngineSyncMessage,
|
|
11
11
|
type SonareEngineTransportRequestMessage,
|
|
12
12
|
type SonareEngineTransportResponseMessage,
|
|
@@ -46,6 +46,22 @@ import {
|
|
|
46
46
|
writeSonareEngineTelemetryRingBuffer,
|
|
47
47
|
} from './protocol';
|
|
48
48
|
|
|
49
|
+
function captureTransferList(channels: readonly Float32Array[]): Transferable[] {
|
|
50
|
+
const transfers: ArrayBuffer[] = [];
|
|
51
|
+
const seen = new Set<ArrayBuffer>();
|
|
52
|
+
for (const channel of channels) {
|
|
53
|
+
const buffer = channel.buffer;
|
|
54
|
+
if (!(buffer instanceof ArrayBuffer)) {
|
|
55
|
+
throw new TypeError('capture response channels must use plain ArrayBuffers');
|
|
56
|
+
}
|
|
57
|
+
if (!seen.has(buffer)) {
|
|
58
|
+
seen.add(buffer);
|
|
59
|
+
transfers.push(buffer);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return transfers;
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
/**
|
|
50
66
|
* AudioWorklet-style bridge for the DAW realtime engine facade.
|
|
51
67
|
*
|
|
@@ -599,26 +615,24 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
599
615
|
source: status.source,
|
|
600
616
|
recordOffsetSamples: status.recordOffsetSamples,
|
|
601
617
|
},
|
|
602
|
-
} satisfies
|
|
618
|
+
} satisfies SonareEngineCaptureResponseMessageInternal);
|
|
603
619
|
return;
|
|
604
620
|
}
|
|
605
621
|
if (message.op === 'read') {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
channels,
|
|
621
|
-
} satisfies SonareEngineCaptureResponseMessage);
|
|
622
|
+
// Embind's outer val::array is array-like but not structured-cloneable
|
|
623
|
+
// in every host. Copy only that tiny container; keep each native
|
|
624
|
+
// Float32Array intact so the channel payload remains transferable.
|
|
625
|
+
const channels = Array.from(this.engine.capturedAudio());
|
|
626
|
+
const transfer = captureTransferList(channels);
|
|
627
|
+
this.transport?.postMessage?.(
|
|
628
|
+
{
|
|
629
|
+
type: 'captureResponse',
|
|
630
|
+
requestId: message.requestId,
|
|
631
|
+
ok: true,
|
|
632
|
+
channels,
|
|
633
|
+
} satisfies SonareEngineCaptureResponseMessageInternal,
|
|
634
|
+
transfer,
|
|
635
|
+
);
|
|
622
636
|
return;
|
|
623
637
|
}
|
|
624
638
|
this.engine.resetCapture();
|
|
@@ -626,14 +640,14 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
626
640
|
type: 'captureResponse',
|
|
627
641
|
requestId: message.requestId,
|
|
628
642
|
ok: true,
|
|
629
|
-
} satisfies
|
|
643
|
+
} satisfies SonareEngineCaptureResponseMessageInternal);
|
|
630
644
|
} catch (error) {
|
|
631
645
|
this.transport?.postMessage?.({
|
|
632
646
|
type: 'captureResponse',
|
|
633
647
|
requestId: message.requestId,
|
|
634
648
|
ok: false,
|
|
635
649
|
error: error instanceof Error ? error.message : String(error),
|
|
636
|
-
} satisfies
|
|
650
|
+
} satisfies SonareEngineCaptureResponseMessageInternal);
|
|
637
651
|
}
|
|
638
652
|
}
|
|
639
653
|
|
|
@@ -773,6 +787,24 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
773
787
|
sampleTime,
|
|
774
788
|
);
|
|
775
789
|
break;
|
|
790
|
+
case SonareEngineCommandType.SetTrackMonitorMode: {
|
|
791
|
+
const rawMode = command.argInt;
|
|
792
|
+
const mode = typeof rawMode === 'bigint' ? Number(rawMode) : rawMode;
|
|
793
|
+
if (typeof mode !== 'number' || !Number.isSafeInteger(mode) || mode < 0 || mode > 2) {
|
|
794
|
+
throw new RangeError(`Invalid track monitor mode: ${String(rawMode)}`);
|
|
795
|
+
}
|
|
796
|
+
const laneIndex = command.targetId;
|
|
797
|
+
if (
|
|
798
|
+
typeof laneIndex !== 'number' ||
|
|
799
|
+
!Number.isSafeInteger(laneIndex) ||
|
|
800
|
+
laneIndex < 0 ||
|
|
801
|
+
laneIndex > 0xffff_ffff
|
|
802
|
+
) {
|
|
803
|
+
throw new RangeError(`Invalid track monitor lane index: ${String(laneIndex)}`);
|
|
804
|
+
}
|
|
805
|
+
this.engine.setTrackMonitorMode(laneIndex, mode as 0 | 1 | 2, sampleTime);
|
|
806
|
+
break;
|
|
807
|
+
}
|
|
776
808
|
default:
|
|
777
809
|
this.publishTelemetryRecord({
|
|
778
810
|
type: SonareEngineTelemetryType.Error,
|
|
@@ -121,7 +121,7 @@ export function registerSonareRealtimeEngineWorkletProcessor(
|
|
|
121
121
|
throw new Error('AudioWorklet initialization message buffer overflowed.');
|
|
122
122
|
}
|
|
123
123
|
this.bridge = new SonareRealtimeEngineWorkletProcessor(options, {
|
|
124
|
-
postMessage: (message) => port?.postMessage?.(message),
|
|
124
|
+
postMessage: (message, transfer) => port?.postMessage?.(message, transfer),
|
|
125
125
|
onMeter: (meter) => port?.postMessage?.(meter),
|
|
126
126
|
});
|
|
127
127
|
for (const message of options.initialSyncMessages ?? []) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { panLawCode, panModeCode } from '../codes';
|
|
2
|
-
import type { EqBand,
|
|
2
|
+
import type { EqBand, PanLawInput, PanMode, RealtimeEngine } from '../index';
|
|
3
3
|
import type { SonareEngineInstrumentSyncMessage, SonareEngineSyncMessage } from './messages';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -85,7 +85,7 @@ export function setTrackStripPan(
|
|
|
85
85
|
export function setTrackStripPanLaw(
|
|
86
86
|
ctx: EngineStripContext,
|
|
87
87
|
target: string | number,
|
|
88
|
-
panLaw:
|
|
88
|
+
panLaw: PanLawInput,
|
|
89
89
|
): void {
|
|
90
90
|
const trackId = trackIdFor(ctx, target);
|
|
91
91
|
const code = panLawCode(panLaw);
|
package/src/worklet/engine.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
EngineTempoSegment,
|
|
16
16
|
EngineTimeSignatureSegment,
|
|
17
17
|
EngineTrackLane,
|
|
18
|
+
EngineTrackMonitorMode,
|
|
18
19
|
EngineTrackSend,
|
|
19
20
|
EngineTransportState,
|
|
20
21
|
EqBand,
|
|
@@ -386,6 +387,15 @@ export class SonareEngine {
|
|
|
386
387
|
return parameter.setSoloMute(this.parameterContext, target, solo, mute);
|
|
387
388
|
}
|
|
388
389
|
|
|
390
|
+
/** Queues a per-track PFL/AFL monitor tap mode change. */
|
|
391
|
+
setTrackMonitorMode(
|
|
392
|
+
target: string | number,
|
|
393
|
+
mode: EngineTrackMonitorMode,
|
|
394
|
+
renderFrame = -1,
|
|
395
|
+
): boolean {
|
|
396
|
+
return parameter.setTrackMonitorMode(this.parameterContext, target, mode, renderFrame);
|
|
397
|
+
}
|
|
398
|
+
|
|
389
399
|
setStripGain(target: string | number, db: number): boolean {
|
|
390
400
|
return this.sendSmoothedParam(this.stripParamId(target, ENGINE_MIXER_PARAM_FADER_DB), db);
|
|
391
401
|
}
|