@libraz/libsonare 1.6.0 → 1.7.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 +2 -2
- package/dist/analysis.d.ts +88 -9
- package/dist/analysis.js +152 -42
- package/dist/analysis.js.map +1 -1
- package/dist/index.d.ts +129 -24
- package/dist/index.js +455 -140
- 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 +62 -10
- package/dist/worklet.js +193 -42
- package/dist/worklet.js.map +1 -1
- package/package.json +3 -3
- package/src/analysis_helpers.ts +34 -29
- package/src/audio.ts +7 -2
- package/src/codes.ts +47 -11
- package/src/effects_transform.ts +179 -10
- 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 +21 -3
- 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 +45 -1
- package/src/public_types_mixing.ts +16 -0
- package/src/public_types_realtime.ts +23 -5
- package/src/quick_analysis.ts +21 -3
- package/src/realtime_engine.ts +14 -3
- package/src/sonare.js.d.ts +64 -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/quick_analysis.ts
CHANGED
|
@@ -29,7 +29,12 @@ import type {
|
|
|
29
29
|
import { Mode, PitchClass } from './public_types';
|
|
30
30
|
import type { ProgressCallback, WasmAcousticResult } from './sonare.js';
|
|
31
31
|
import type { ValidateOptions } from './validation';
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
assertFiniteScalar,
|
|
34
|
+
assertNonNegativeInteger,
|
|
35
|
+
assertSampleRate,
|
|
36
|
+
assertSamples,
|
|
37
|
+
} from './validation';
|
|
33
38
|
|
|
34
39
|
function requireModule() {
|
|
35
40
|
return getSonareModule();
|
|
@@ -81,6 +86,7 @@ export interface ChordFunctionalAnalysisRequest extends DetectChordsRequest {
|
|
|
81
86
|
/** Canonical request form for impulse-response analysis. */
|
|
82
87
|
export interface AnalyzeImpulseResponseRequest extends SamplesRequest {
|
|
83
88
|
nOctaveBands?: number;
|
|
89
|
+
minDecayDb?: number;
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
/** Canonical request form for acoustic analysis. */
|
|
@@ -435,23 +441,35 @@ export function analyzeImpulseResponse(
|
|
|
435
441
|
samples: Float32Array,
|
|
436
442
|
sampleRate?: number,
|
|
437
443
|
nOctaveBands?: number,
|
|
444
|
+
minDecayDb?: number,
|
|
438
445
|
): AcousticResult;
|
|
439
446
|
export function analyzeImpulseResponse(
|
|
440
447
|
samples: Float32Array | AnalyzeImpulseResponseRequest,
|
|
441
448
|
sampleRate = 48000,
|
|
442
449
|
nOctaveBands = 6,
|
|
450
|
+
minDecayDb?: number,
|
|
443
451
|
): AcousticResult {
|
|
444
|
-
const request =
|
|
452
|
+
const request =
|
|
453
|
+
samples instanceof Float32Array ? { samples, sampleRate, nOctaveBands, minDecayDb } : samples;
|
|
454
|
+
if (request.minDecayDb === null) {
|
|
455
|
+
throw new TypeError('analyzeImpulseResponse: minDecayDb must be a finite number');
|
|
456
|
+
}
|
|
457
|
+
const resolvedMinDecayDb = request.minDecayDb === undefined ? 30.0 : request.minDecayDb;
|
|
458
|
+
assertFiniteScalar('analyzeImpulseResponse', resolvedMinDecayDb, 'minDecayDb');
|
|
459
|
+
if (resolvedMinDecayDb <= 0) {
|
|
460
|
+
throw new RangeError('analyzeImpulseResponse: minDecayDb must be greater than zero');
|
|
461
|
+
}
|
|
445
462
|
validateAnalysisInput(
|
|
446
463
|
'analyzeImpulseResponse',
|
|
447
464
|
request.samples,
|
|
448
465
|
request.sampleRate ?? 48000,
|
|
449
466
|
request,
|
|
450
467
|
);
|
|
451
|
-
const result: WasmAcousticResult = requireModule().
|
|
468
|
+
const result: WasmAcousticResult = requireModule().analyzeImpulseResponseEx(
|
|
452
469
|
request.samples,
|
|
453
470
|
request.sampleRate ?? 48000,
|
|
454
471
|
request.nOctaveBands ?? 6,
|
|
472
|
+
resolvedMinDecayDb,
|
|
455
473
|
);
|
|
456
474
|
return result;
|
|
457
475
|
}
|
package/src/realtime_engine.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { panLawCode, panModeCode, sendTimingCode } from './codes';
|
|
1
|
+
import { panLawCode, panModeCode, sendTimingCode, trackMonitorModeCode } from './codes';
|
|
2
2
|
import { ErrorCode, SonareError } from './errors';
|
|
3
3
|
import { getSonareModule } from './module_state';
|
|
4
4
|
import type { ProjectMidiCcBinding, SynthPatch } from './project';
|
|
5
|
-
import type { EqBand,
|
|
5
|
+
import type { EqBand, PanLawInput, PanMode, SendTiming } from './public_types';
|
|
6
6
|
import type {
|
|
7
7
|
WasmClipPageRequest,
|
|
8
8
|
WasmEngineAutomationPoint,
|
|
@@ -78,6 +78,12 @@ export interface EngineTrackLane {
|
|
|
78
78
|
sourceChannelLayout?: number;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** Per-track cue/monitor tap mode: off, pre-fader listen, or after-fader listen. */
|
|
82
|
+
export type EngineTrackMonitorMode = 'off' | 'pfl' | 'afl' | 0 | 1 | 2;
|
|
83
|
+
|
|
84
|
+
/** Short alias for {@link EngineTrackMonitorMode}. */
|
|
85
|
+
export type TrackMonitorMode = EngineTrackMonitorMode;
|
|
86
|
+
|
|
81
87
|
export interface EngineBus {
|
|
82
88
|
busId: number;
|
|
83
89
|
gainDb?: number;
|
|
@@ -219,6 +225,11 @@ export class RealtimeEngine {
|
|
|
219
225
|
this.native.setSoloMute(laneIndex, solo, mute, renderFrame);
|
|
220
226
|
}
|
|
221
227
|
|
|
228
|
+
/** Queue a per-track PFL/AFL monitor tap mode change. */
|
|
229
|
+
setTrackMonitorMode(laneIndex: number, mode: EngineTrackMonitorMode, renderFrame = -1): void {
|
|
230
|
+
this.native.setTrackMonitorMode(laneIndex, trackMonitorModeCode(mode), renderFrame);
|
|
231
|
+
}
|
|
232
|
+
|
|
222
233
|
setMidiClips(clips: readonly EngineMidiClipSchedule[]): void {
|
|
223
234
|
this.native.setMidiClips(clips);
|
|
224
235
|
}
|
|
@@ -831,7 +842,7 @@ export class RealtimeEngine {
|
|
|
831
842
|
}
|
|
832
843
|
|
|
833
844
|
/** Sets a track lane strip's pan law in realtime. */
|
|
834
|
-
setTrackStripPanLaw(trackId: number, panLaw:
|
|
845
|
+
setTrackStripPanLaw(trackId: number, panLaw: PanLawInput): void {
|
|
835
846
|
this.native.setTrackStripPanLaw(trackId, panLawCode(panLaw));
|
|
836
847
|
}
|
|
837
848
|
|
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,
|
|
@@ -1473,12 +1484,30 @@ export interface SonareModule {
|
|
|
1473
1484
|
kernelHarmonic: number,
|
|
1474
1485
|
kernelPercussive: number,
|
|
1475
1486
|
) => WasmHpssResult;
|
|
1487
|
+
hpssEx: (
|
|
1488
|
+
samples: Float32Array,
|
|
1489
|
+
sampleRate: number,
|
|
1490
|
+
kernelHarmonic: number,
|
|
1491
|
+
kernelPercussive: number,
|
|
1492
|
+
nFft: number,
|
|
1493
|
+
hopLength: number,
|
|
1494
|
+
hardMask: boolean,
|
|
1495
|
+
) => WasmHpssResult;
|
|
1476
1496
|
hpssWithResidual: (
|
|
1477
1497
|
samples: Float32Array,
|
|
1478
1498
|
sampleRate: number,
|
|
1479
1499
|
kernelHarmonic: number,
|
|
1480
1500
|
kernelPercussive: number,
|
|
1481
1501
|
) => WasmHpssWithResidualResult;
|
|
1502
|
+
hpssWithResidualEx: (
|
|
1503
|
+
samples: Float32Array,
|
|
1504
|
+
sampleRate: number,
|
|
1505
|
+
kernelHarmonic: number,
|
|
1506
|
+
kernelPercussive: number,
|
|
1507
|
+
nFft: number,
|
|
1508
|
+
hopLength: number,
|
|
1509
|
+
hardMask: boolean,
|
|
1510
|
+
) => WasmHpssWithResidualResult;
|
|
1482
1511
|
decompose: (
|
|
1483
1512
|
s: Float32Array,
|
|
1484
1513
|
nFeatures: number,
|
|
@@ -1520,7 +1549,21 @@ export interface SonareModule {
|
|
|
1520
1549
|
harmonic: (samples: Float32Array, sampleRate: number) => Float32Array;
|
|
1521
1550
|
percussive: (samples: Float32Array, sampleRate: number) => Float32Array;
|
|
1522
1551
|
timeStretch: (samples: Float32Array, sampleRate: number, rate: number) => Float32Array;
|
|
1552
|
+
timeStretchEx: (
|
|
1553
|
+
samples: Float32Array,
|
|
1554
|
+
sampleRate: number,
|
|
1555
|
+
rate: number,
|
|
1556
|
+
nFft: number,
|
|
1557
|
+
hopLength: number,
|
|
1558
|
+
) => Float32Array;
|
|
1523
1559
|
pitchShift: (samples: Float32Array, sampleRate: number, semitones: number) => Float32Array;
|
|
1560
|
+
pitchShiftEx: (
|
|
1561
|
+
samples: Float32Array,
|
|
1562
|
+
sampleRate: number,
|
|
1563
|
+
semitones: number,
|
|
1564
|
+
nFft: number,
|
|
1565
|
+
hopLength: number,
|
|
1566
|
+
) => Float32Array;
|
|
1524
1567
|
pitchCorrectToMidi: (
|
|
1525
1568
|
samples: Float32Array,
|
|
1526
1569
|
sampleRate: number,
|
|
@@ -1582,6 +1625,12 @@ export interface SonareModule {
|
|
|
1582
1625
|
channels: number,
|
|
1583
1626
|
) => Float32Array;
|
|
1584
1627
|
normalize: (samples: Float32Array, sampleRate: number, targetDb: number) => Float32Array;
|
|
1628
|
+
normalizeEx: (
|
|
1629
|
+
samples: Float32Array,
|
|
1630
|
+
sampleRate: number,
|
|
1631
|
+
targetDb: number,
|
|
1632
|
+
mode: string,
|
|
1633
|
+
) => Float32Array;
|
|
1585
1634
|
mastering: (
|
|
1586
1635
|
samples: Float32Array,
|
|
1587
1636
|
sampleRate: number,
|
|
@@ -1760,6 +1809,13 @@ export interface SonareModule {
|
|
|
1760
1809
|
options: Record<string, unknown>,
|
|
1761
1810
|
) => WasmMixResult;
|
|
1762
1811
|
trim: (samples: Float32Array, sampleRate: number, thresholdDb: number) => Float32Array;
|
|
1812
|
+
trimEx: (
|
|
1813
|
+
samples: Float32Array,
|
|
1814
|
+
sampleRate: number,
|
|
1815
|
+
thresholdDb: number,
|
|
1816
|
+
frameLength: number,
|
|
1817
|
+
hopLength: number,
|
|
1818
|
+
) => Float32Array;
|
|
1763
1819
|
spectralEdit: (
|
|
1764
1820
|
samples: Float32Array,
|
|
1765
1821
|
sampleRate: number,
|
|
@@ -2190,6 +2246,14 @@ export interface SonareModule {
|
|
|
2190
2246
|
stftBlendWeight: number,
|
|
2191
2247
|
stftBlendNFft: number,
|
|
2192
2248
|
) => WasmNnlsChromaResult;
|
|
2249
|
+
nnlsChromaEx: (
|
|
2250
|
+
samples: Float32Array,
|
|
2251
|
+
sampleRate: number,
|
|
2252
|
+
enableStftBlend: boolean,
|
|
2253
|
+
stftBlendWeight: number,
|
|
2254
|
+
stftBlendNFft: number,
|
|
2255
|
+
hopLength: number,
|
|
2256
|
+
) => WasmNnlsChromaResult;
|
|
2193
2257
|
cqt: (
|
|
2194
2258
|
samples: Float32Array,
|
|
2195
2259
|
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
|
}
|