@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.
- package/README.md +50 -19
- package/dist/index.d.ts +5384 -1
- package/dist/index.js +867 -573
- package/dist/index.js.map +1 -1
- package/dist/sonare.js +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/worklet.d.ts +1083 -5227
- 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 -1089
- package/src/effects_transform.ts +286 -0
- package/src/effects_voice_change.ts +118 -0
- 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 -1712
- 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 -1221
- 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/realtime_engine.ts +94 -0
- package/src/sonare.js.d.ts +72 -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
package/src/realtime_engine.ts
CHANGED
|
@@ -24,9 +24,12 @@ import type {
|
|
|
24
24
|
WasmEngineTempoSegment,
|
|
25
25
|
WasmEngineTimeSignatureSegment,
|
|
26
26
|
WasmEngineTransportState,
|
|
27
|
+
WasmExternalMidiEvent,
|
|
27
28
|
WasmRealtimeEngine,
|
|
28
29
|
} from './sonare.js';
|
|
29
30
|
|
|
31
|
+
export type ExternalMidiEvent = WasmExternalMidiEvent;
|
|
32
|
+
|
|
30
33
|
export type EngineClip = WasmEngineClip;
|
|
31
34
|
export type ClipPageRequest = WasmClipPageRequest;
|
|
32
35
|
export type EngineParameterInfo = WasmEngineParameterInfo;
|
|
@@ -193,6 +196,15 @@ export class RealtimeEngine {
|
|
|
193
196
|
this.native.setParameterSmoothed(paramId, value, renderFrame);
|
|
194
197
|
}
|
|
195
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Set the default ramp time (ms) for engine-level smoothed parameters —
|
|
201
|
+
* fader/pan glides, insert-parameter automation, and MIDI-CC mappings. The
|
|
202
|
+
* default is 20 ms; pass `0` for instant (un-ramped) changes.
|
|
203
|
+
*/
|
|
204
|
+
setParamSmoothingMs(smoothingMs: number): void {
|
|
205
|
+
this.native.setParamSmoothingMs(smoothingMs);
|
|
206
|
+
}
|
|
207
|
+
|
|
196
208
|
setSoloMute(laneIndex: number, solo: boolean, mute: boolean, renderFrame = -1): void {
|
|
197
209
|
this.native.setSoloMute(laneIndex, solo, mute, renderFrame);
|
|
198
210
|
}
|
|
@@ -308,6 +320,39 @@ export class RealtimeEngine {
|
|
|
308
320
|
return this.native.midiInputPendingCount();
|
|
309
321
|
}
|
|
310
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Route a destination's (track lane's) MIDI to the external output queue
|
|
325
|
+
* instead of the internal instrument rack, so the track plays an external
|
|
326
|
+
* device. Clearing it restores internal-synth playback.
|
|
327
|
+
*/
|
|
328
|
+
setMidiDestinationExternal(destinationId: number, external: boolean): void {
|
|
329
|
+
this.native.setMidiDestinationExternal(destinationId, external);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Enable/disable forwarding MIDI clock + transport (start/continue/stop) to
|
|
334
|
+
* the external output queue so external gear tracks the transport tempo.
|
|
335
|
+
*/
|
|
336
|
+
setExternalMidiClockEnabled(enabled: boolean): void {
|
|
337
|
+
this.native.setExternalMidiClockEnabled(enabled);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Count of external-MIDI events dropped because the output queue was full. */
|
|
341
|
+
externalMidiDroppedCount(): number {
|
|
342
|
+
return this.native.externalMidiDroppedCount();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Drain queued external-MIDI events, already lowered to MIDI 1.0 byte
|
|
347
|
+
* messages ready to write to a Web MIDI output port. Call once per audio
|
|
348
|
+
* block / animation frame. `maxRecords` caps the number of output events
|
|
349
|
+
* returned — the shared unit across every surface. Events past the cap stay
|
|
350
|
+
* queued for the next call (lossless); call again to drain the rest.
|
|
351
|
+
*/
|
|
352
|
+
drainExternalMidi(maxRecords = 1024): WasmExternalMidiEvent[] {
|
|
353
|
+
return this.native.drainExternalMidi(maxRecords);
|
|
354
|
+
}
|
|
355
|
+
|
|
311
356
|
pushMidiInputNoteOn(
|
|
312
357
|
group: number,
|
|
313
358
|
channel: number,
|
|
@@ -377,6 +422,16 @@ export class RealtimeEngine {
|
|
|
377
422
|
this.native.pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
|
|
378
423
|
}
|
|
379
424
|
|
|
425
|
+
/**
|
|
426
|
+
* Queue an immediate (live) MIDI SysEx frame to a MIDI destination. `data` is
|
|
427
|
+
* the full message including the leading 0xF0 and trailing 0xF7 (1..512
|
|
428
|
+
* bytes). `renderFrame` is the frame to fire at, or -1 for immediate. Mirrors
|
|
429
|
+
* the Node/Python/C-ABI `pushMidiSysex`.
|
|
430
|
+
*/
|
|
431
|
+
pushMidiSysex(destinationId: number, data: Uint8Array, renderFrame = -1): void {
|
|
432
|
+
this.native.pushMidiSysex(destinationId, data, renderFrame);
|
|
433
|
+
}
|
|
434
|
+
|
|
380
435
|
/**
|
|
381
436
|
* Queue a MIDI panic (all-notes-off) releasing every sounding note at
|
|
382
437
|
* `renderFrame` (-1 = immediate). Mirrors the C-ABI `pushMidiPanic`.
|
|
@@ -662,6 +717,45 @@ export class RealtimeEngine {
|
|
|
662
717
|
this.native.setMasterStripInsertParamByName(insertIndex, paramName, value);
|
|
663
718
|
}
|
|
664
719
|
|
|
720
|
+
/** Bus-strip counterpart of {@link setTrackStripInsertParamByName}. */
|
|
721
|
+
setBusStripInsertParamByName(
|
|
722
|
+
busId: number,
|
|
723
|
+
insertIndex: number,
|
|
724
|
+
paramName: string,
|
|
725
|
+
value: number,
|
|
726
|
+
): void {
|
|
727
|
+
this.native.setBusStripInsertParamByName(busId, insertIndex, paramName, value);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/** Bus-strip counterpart of {@link setTrackStripInsertBypassed}. */
|
|
731
|
+
setBusStripInsertBypassed(
|
|
732
|
+
busId: number,
|
|
733
|
+
insertIndex: number,
|
|
734
|
+
bypassed: boolean,
|
|
735
|
+
resetOnBypass = false,
|
|
736
|
+
): void {
|
|
737
|
+
this.native.setBusStripInsertBypassed(busId, insertIndex, bypassed, resetOnBypass);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Resolves a track-lane insert parameter (by its JSON-key name) to the
|
|
742
|
+
* reserved automation id usable with `setAutomationLane` / `setParameter`.
|
|
743
|
+
* Returns `-1` when the track, insert, or name is unknown. (The Python binding
|
|
744
|
+
* raises a `SonareError` for an unknown id where Node/WASM return the `-1`
|
|
745
|
+
* sentinel.)
|
|
746
|
+
*/
|
|
747
|
+
resolveTrackInsertAutomationId(trackId: number, insertIndex: number, paramName: string): number {
|
|
748
|
+
return this.native.resolveTrackInsertAutomationId(trackId, insertIndex, paramName);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
resolveMasterInsertAutomationId(insertIndex: number, paramName: string): number {
|
|
752
|
+
return this.native.resolveMasterInsertAutomationId(insertIndex, paramName);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
resolveBusInsertAutomationId(busId: number, insertIndex: number, paramName: string): number {
|
|
756
|
+
return this.native.resolveBusInsertAutomationId(busId, insertIndex, paramName);
|
|
757
|
+
}
|
|
758
|
+
|
|
665
759
|
/** Sets a track lane strip's pan position in realtime (glitch-free). */
|
|
666
760
|
setTrackStripPan(trackId: number, pan: number): void {
|
|
667
761
|
this.native.setTrackStripPan(trackId, pan);
|
package/src/sonare.js.d.ts
CHANGED
|
@@ -510,6 +510,10 @@ export interface WasmMixMeterSnapshot {
|
|
|
510
510
|
truePeakDbR: number;
|
|
511
511
|
maxTruePeakDb: number;
|
|
512
512
|
seq: number;
|
|
513
|
+
channelCount: number;
|
|
514
|
+
peakDb: number[];
|
|
515
|
+
rmsDb: number[];
|
|
516
|
+
truePeakDb: number[];
|
|
513
517
|
}
|
|
514
518
|
|
|
515
519
|
export interface WasmMixResult {
|
|
@@ -783,6 +787,23 @@ export interface WasmEngineTimeSignatureSegment {
|
|
|
783
787
|
denominator: number;
|
|
784
788
|
}
|
|
785
789
|
|
|
790
|
+
/**
|
|
791
|
+
* One external-MIDI event drained from the engine, already lowered to MIDI 1.0
|
|
792
|
+
* bytes ready to write to a Web MIDI output port.
|
|
793
|
+
*/
|
|
794
|
+
export interface WasmExternalMidiEvent {
|
|
795
|
+
/**
|
|
796
|
+
* Originating track lane (Track.midi_destination_id), or the transport
|
|
797
|
+
* sentinel 0xFFFFFFFF for clock / start / continue / stop bytes meant for
|
|
798
|
+
* every external port.
|
|
799
|
+
*/
|
|
800
|
+
destinationId: number;
|
|
801
|
+
/** Sample position within the producing block at which the event fires. */
|
|
802
|
+
renderFrame: number;
|
|
803
|
+
/** MIDI 1.0 status + data bytes (1..3 entries). */
|
|
804
|
+
bytes: number[];
|
|
805
|
+
}
|
|
806
|
+
|
|
786
807
|
export interface WasmRealtimeEngine {
|
|
787
808
|
prepare: (
|
|
788
809
|
sampleRate: number,
|
|
@@ -792,6 +813,7 @@ export interface WasmRealtimeEngine {
|
|
|
792
813
|
) => void;
|
|
793
814
|
setParameter: (paramId: number, value: number, renderFrame: number) => void;
|
|
794
815
|
setParameterSmoothed: (paramId: number, value: number, renderFrame: number) => void;
|
|
816
|
+
setParamSmoothingMs: (smoothingMs: number) => void;
|
|
795
817
|
setSoloMute: (laneIndex: number, solo: boolean, mute: boolean, renderFrame: number) => void;
|
|
796
818
|
getTransportState: () => WasmEngineTransportState;
|
|
797
819
|
play: (renderFrame: number) => void;
|
|
@@ -851,6 +873,25 @@ export interface WasmRealtimeEngine {
|
|
|
851
873
|
value: number,
|
|
852
874
|
) => void;
|
|
853
875
|
setMasterStripInsertParamByName: (insertIndex: number, paramName: string, value: number) => void;
|
|
876
|
+
setBusStripInsertParamByName: (
|
|
877
|
+
busId: number,
|
|
878
|
+
insertIndex: number,
|
|
879
|
+
paramName: string,
|
|
880
|
+
value: number,
|
|
881
|
+
) => void;
|
|
882
|
+
setBusStripInsertBypassed: (
|
|
883
|
+
busId: number,
|
|
884
|
+
insertIndex: number,
|
|
885
|
+
bypassed: boolean,
|
|
886
|
+
resetOnBypass: boolean,
|
|
887
|
+
) => void;
|
|
888
|
+
resolveTrackInsertAutomationId: (
|
|
889
|
+
trackId: number,
|
|
890
|
+
insertIndex: number,
|
|
891
|
+
paramName: string,
|
|
892
|
+
) => number;
|
|
893
|
+
resolveMasterInsertAutomationId: (insertIndex: number, paramName: string) => number;
|
|
894
|
+
resolveBusInsertAutomationId: (busId: number, insertIndex: number, paramName: string) => number;
|
|
854
895
|
setTrackStripPan: (trackId: number, pan: number) => void;
|
|
855
896
|
setTrackStripPanLaw: (trackId: number, panLaw: number) => void;
|
|
856
897
|
setTrackStripPanMode: (trackId: number, panMode: number) => void;
|
|
@@ -936,7 +977,12 @@ export interface WasmRealtimeEngine {
|
|
|
936
977
|
value: number,
|
|
937
978
|
renderFrame: number,
|
|
938
979
|
) => void;
|
|
980
|
+
pushMidiSysex: (destinationId: number, data: Uint8Array, renderFrame: number) => void;
|
|
939
981
|
pushMidiPanic: (renderFrame: number) => void;
|
|
982
|
+
setMidiDestinationExternal: (destinationId: number, external: boolean) => void;
|
|
983
|
+
setExternalMidiClockEnabled: (enabled: boolean) => void;
|
|
984
|
+
externalMidiDroppedCount: () => number;
|
|
985
|
+
drainExternalMidi: (maxRecords: number) => WasmExternalMidiEvent[];
|
|
940
986
|
clearParameters: () => void;
|
|
941
987
|
process: (channels: Float32Array[]) => Float32Array[];
|
|
942
988
|
prepareChannels: (numChannels: number, maxFrames: number) => void;
|
|
@@ -1337,6 +1383,25 @@ export interface SonareModule {
|
|
|
1337
1383
|
voiced?: Float32Array,
|
|
1338
1384
|
voicedProb?: Float32Array,
|
|
1339
1385
|
) => Float32Array;
|
|
1386
|
+
pitchCorrectTimevarying: (
|
|
1387
|
+
samples: Float32Array,
|
|
1388
|
+
sampleRate: number,
|
|
1389
|
+
f0Hz: Float32Array,
|
|
1390
|
+
hopLength: number,
|
|
1391
|
+
options: {
|
|
1392
|
+
mode?: string;
|
|
1393
|
+
targetMidi?: number;
|
|
1394
|
+
scaleRoot?: number;
|
|
1395
|
+
scaleModeMask?: number;
|
|
1396
|
+
referenceMidi?: number;
|
|
1397
|
+
retuneAmount?: number;
|
|
1398
|
+
maxCorrectionSemitones?: number;
|
|
1399
|
+
retuneSpeedMs?: number;
|
|
1400
|
+
vibratoThresholdCents?: number;
|
|
1401
|
+
voiced?: Float32Array;
|
|
1402
|
+
voicedProb?: Float32Array;
|
|
1403
|
+
},
|
|
1404
|
+
) => Float32Array;
|
|
1340
1405
|
noteStretch: (
|
|
1341
1406
|
samples: Float32Array,
|
|
1342
1407
|
sampleRate: number,
|
|
@@ -1567,6 +1632,7 @@ export interface SonareModule {
|
|
|
1567
1632
|
fmin: number,
|
|
1568
1633
|
fmax: number,
|
|
1569
1634
|
htk: boolean,
|
|
1635
|
+
lifter: number,
|
|
1570
1636
|
) => WasmMfccResult;
|
|
1571
1637
|
|
|
1572
1638
|
// Features - Inverse reconstruction
|
|
@@ -1792,6 +1858,12 @@ export interface SonareModule {
|
|
|
1792
1858
|
hopLength: number,
|
|
1793
1859
|
nChroma: number,
|
|
1794
1860
|
) => WasmChromaResult;
|
|
1861
|
+
chromaCqt: (
|
|
1862
|
+
samples: Float32Array,
|
|
1863
|
+
sampleRate: number,
|
|
1864
|
+
hopLength: number,
|
|
1865
|
+
nChroma: number,
|
|
1866
|
+
) => WasmChromaResult;
|
|
1795
1867
|
bassChroma: (
|
|
1796
1868
|
samples: Float32Array,
|
|
1797
1869
|
sampleRate: number,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { EngineAutomationPoint, RealtimeEngine } from '../index';
|
|
2
|
+
import { curveCode } from './engine-sync';
|
|
3
|
+
import type { SonareEngineSyncMessage } from './messages';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Collaborator surface the automation-lane helpers need from the owning
|
|
7
|
+
* {@link SonareEngine}: the lane store they mutate, the offline engine they
|
|
8
|
+
* mirror into, the sync poster, and the parameter-id resolver.
|
|
9
|
+
*/
|
|
10
|
+
export interface EngineAutomationContext {
|
|
11
|
+
readonly offlineEngine: RealtimeEngine;
|
|
12
|
+
readonly automationLanes: Map<number, EngineAutomationPoint[]>;
|
|
13
|
+
postSync(message: SonareEngineSyncMessage): void;
|
|
14
|
+
resolveParamId(nodeId: string, param: string | number): number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function scheduleParam(
|
|
18
|
+
ctx: EngineAutomationContext,
|
|
19
|
+
nodeId: string,
|
|
20
|
+
param: string | number,
|
|
21
|
+
ppq: number,
|
|
22
|
+
value: number,
|
|
23
|
+
curve: number | 'linear' | 'exponential' = 'linear',
|
|
24
|
+
): void {
|
|
25
|
+
const paramId = ctx.resolveParamId(nodeId, param);
|
|
26
|
+
const lane = ctx.automationLanes.get(paramId) ?? [];
|
|
27
|
+
lane.push({ ppq, value, curveToNext: curveCode(curve) });
|
|
28
|
+
lane.sort((a, b) => a.ppq - b.ppq);
|
|
29
|
+
ctx.automationLanes.set(paramId, lane);
|
|
30
|
+
ctx.offlineEngine.setAutomationLane(paramId, lane);
|
|
31
|
+
// Mirror the lane to the live worklet engine so scheduled automation plays
|
|
32
|
+
// back in real time, not just in renderOffline(). Lanes can exceed the
|
|
33
|
+
// fixed-size SAB command record, so they ride an out-of-band 'syncAutomation'
|
|
34
|
+
// message applied outside process() (like syncClips/syncMarkers).
|
|
35
|
+
ctx.postSync({ type: 'syncAutomation', paramId, points: lane });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function addAutomationPoint(
|
|
39
|
+
ctx: EngineAutomationContext,
|
|
40
|
+
laneId: string | number,
|
|
41
|
+
ppq: number,
|
|
42
|
+
value: number,
|
|
43
|
+
curve: number | 'linear' | 'exponential' = 'linear',
|
|
44
|
+
): void {
|
|
45
|
+
scheduleParam(ctx, '', laneId, ppq, value, curve);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Replaces the automation lane for `paramId` with the given breakpoints.
|
|
50
|
+
*
|
|
51
|
+
* Unlike scheduleParam (which appends a single point), this sets the whole
|
|
52
|
+
* lane at once; an empty array clears the lane. The points are defensively
|
|
53
|
+
* copied and sorted by ppq before being mirrored to the offline engine and
|
|
54
|
+
* the live worklet engine.
|
|
55
|
+
*
|
|
56
|
+
* @param paramId Automation target id (registered parameter or a reserved
|
|
57
|
+
* engine mixer target from automationParamId/busAutomationParamId).
|
|
58
|
+
* @param points Lane breakpoints; order does not matter.
|
|
59
|
+
*/
|
|
60
|
+
export function setAutomationLane(
|
|
61
|
+
ctx: EngineAutomationContext,
|
|
62
|
+
paramId: number,
|
|
63
|
+
points: ReadonlyArray<EngineAutomationPoint>,
|
|
64
|
+
): void {
|
|
65
|
+
const sorted = points.map((point) => ({ ...point })).sort((a, b) => a.ppq - b.ppq);
|
|
66
|
+
if (sorted.length === 0) {
|
|
67
|
+
ctx.automationLanes.delete(paramId);
|
|
68
|
+
} else {
|
|
69
|
+
ctx.automationLanes.set(paramId, sorted);
|
|
70
|
+
}
|
|
71
|
+
ctx.offlineEngine.setAutomationLane(paramId, sorted);
|
|
72
|
+
ctx.postSync({ type: 'syncAutomation', paramId, points: sorted });
|
|
73
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { EngineCaptureStatus, RealtimeEngine } from '../index';
|
|
2
|
+
import type { SonareRealtimeEngineNode } from './engine-node';
|
|
3
|
+
import { buildCaptureConfig, type CaptureOptions } from './engine-offline';
|
|
4
|
+
import type { SonareEngineSyncCaptureMessage, SonareEngineSyncMessage } from './messages';
|
|
5
|
+
import { SonareEngineCommandType } from './protocol';
|
|
6
|
+
|
|
7
|
+
type CaptureConfig = Omit<SonareEngineSyncCaptureMessage, 'type'>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Collaborator surface the capture/record/punch setters need from the owning
|
|
11
|
+
* {@link SonareEngine}: the offline engine they mirror into, the realtime node
|
|
12
|
+
* they command and query, the channel count, the out-of-band sync poster, the
|
|
13
|
+
* capture-config accessor, and the target-id resolver.
|
|
14
|
+
*/
|
|
15
|
+
export interface EngineCaptureContext {
|
|
16
|
+
readonly offlineEngine: RealtimeEngine;
|
|
17
|
+
readonly realtimeNode: SonareRealtimeEngineNode;
|
|
18
|
+
readonly offlineChannelCount: number;
|
|
19
|
+
postSync(message: SonareEngineSyncMessage): void;
|
|
20
|
+
getCaptureConfig(): CaptureConfig | undefined;
|
|
21
|
+
setCaptureConfig(config: CaptureConfig): void;
|
|
22
|
+
resolveTargetId(target: string | number): number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function configureCapture(ctx: EngineCaptureContext, options: CaptureOptions): void {
|
|
26
|
+
const config = buildCaptureConfig(options, ctx.offlineChannelCount);
|
|
27
|
+
ctx.offlineEngine.setCaptureBuffer(config.channels, config.bufferFrames);
|
|
28
|
+
ctx.offlineEngine.setCaptureSource(config.source);
|
|
29
|
+
ctx.offlineEngine.setRecordOffsetSamples(config.recordOffsetSamples);
|
|
30
|
+
ctx.offlineEngine.setInputMonitor(config.inputMonitor.enabled, config.inputMonitor.gain);
|
|
31
|
+
ctx.setCaptureConfig(config);
|
|
32
|
+
ctx.postSync({ type: 'syncCapture', ...config });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function armRecord(
|
|
36
|
+
ctx: EngineCaptureContext,
|
|
37
|
+
trackId: string | number,
|
|
38
|
+
enabled: boolean,
|
|
39
|
+
): boolean {
|
|
40
|
+
if (enabled && !ctx.getCaptureConfig()) {
|
|
41
|
+
throw new Error('Capture buffer is not configured');
|
|
42
|
+
}
|
|
43
|
+
ctx.offlineEngine.armCapture(enabled);
|
|
44
|
+
return ctx.realtimeNode.sendCommand({
|
|
45
|
+
type: SonareEngineCommandType.ArmRecord,
|
|
46
|
+
targetId: ctx.resolveTargetId(trackId),
|
|
47
|
+
sampleTime: -1,
|
|
48
|
+
argInt: enabled ? 1 : 0,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function punch(ctx: EngineCaptureContext, inPpq: number, outPpq: number): boolean {
|
|
53
|
+
const inSample = ctx.offlineEngine.sampleAtPpq(inPpq);
|
|
54
|
+
const outSample = ctx.offlineEngine.sampleAtPpq(outPpq);
|
|
55
|
+
ctx.offlineEngine.setCapturePunch(inSample, outSample, true);
|
|
56
|
+
// Carry BOTH endpoints as already-converted SAMPLES so the realtime engine
|
|
57
|
+
// agrees with the offline engine. The previous code sent the raw PPQ out
|
|
58
|
+
// point and let the consumer multiply by sampleRate (treating PPQ as
|
|
59
|
+
// seconds), which ignored tempo and produced a punch-out ~2x too large at
|
|
60
|
+
// 120 BPM. argInt = in sample, argFloat = out sample (full-precision double).
|
|
61
|
+
return ctx.realtimeNode.sendCommand({
|
|
62
|
+
type: SonareEngineCommandType.Punch,
|
|
63
|
+
sampleTime: -1,
|
|
64
|
+
argInt: inSample,
|
|
65
|
+
argFloat: outSample,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function captureStatus(ctx: EngineCaptureContext): Promise<EngineCaptureStatus> {
|
|
70
|
+
return ctx.realtimeNode.requestCaptureStatus();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function capturedAudio(ctx: EngineCaptureContext): Promise<Float32Array[]> {
|
|
74
|
+
return ctx.realtimeNode.requestCapturedAudio();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function resetCapture(ctx: EngineCaptureContext): Promise<void> {
|
|
78
|
+
ctx.offlineEngine.resetCapture();
|
|
79
|
+
await ctx.realtimeNode.requestCaptureReset();
|
|
80
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { EngineClip, EngineMidiClipSchedule, RealtimeEngine } from '../index';
|
|
2
|
+
import type { SonareEngineSyncMessage } from './messages';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Collaborator surface the audio/MIDI clip scheduling helpers need from the
|
|
6
|
+
* owning {@link SonareEngine}: the clip stores they mutate, the offline engine
|
|
7
|
+
* they mirror into, the sync poster, and the clip-id allocator / lane resolvers.
|
|
8
|
+
*/
|
|
9
|
+
export interface EngineClipContext {
|
|
10
|
+
readonly offlineEngine: RealtimeEngine;
|
|
11
|
+
readonly clips: Map<number, EngineClip>;
|
|
12
|
+
readonly midiClips: Map<number, EngineMidiClipSchedule>;
|
|
13
|
+
allocateClipId(): number;
|
|
14
|
+
postSync(message: SonareEngineSyncMessage): void;
|
|
15
|
+
resolveTargetId(target: string | number): number;
|
|
16
|
+
ensureTrackLane(target: string | number): number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function addClip(
|
|
20
|
+
ctx: EngineClipContext,
|
|
21
|
+
trackId: string | number,
|
|
22
|
+
buffer: Float32Array[],
|
|
23
|
+
startPpq: number,
|
|
24
|
+
opts: Partial<Omit<EngineClip, 'channels' | 'startPpq'>> = {},
|
|
25
|
+
): number {
|
|
26
|
+
const id = opts.id ?? ctx.allocateClipId();
|
|
27
|
+
const clip: EngineClip = {
|
|
28
|
+
...opts,
|
|
29
|
+
id,
|
|
30
|
+
channels: buffer,
|
|
31
|
+
startPpq,
|
|
32
|
+
trackId: ctx.resolveTargetId(trackId),
|
|
33
|
+
};
|
|
34
|
+
ctx.ensureTrackLane(trackId);
|
|
35
|
+
ctx.clips.set(id, clip);
|
|
36
|
+
syncClipsDelta(ctx, [clip], []);
|
|
37
|
+
return id;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function removeClip(ctx: EngineClipContext, clipId: number): void {
|
|
41
|
+
ctx.clips.delete(clipId);
|
|
42
|
+
syncClipsDelta(ctx, [], [clipId]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function setMidiClips(
|
|
46
|
+
ctx: EngineClipContext,
|
|
47
|
+
clips: readonly EngineMidiClipSchedule[],
|
|
48
|
+
): void {
|
|
49
|
+
ctx.midiClips.clear();
|
|
50
|
+
for (const clip of clips) {
|
|
51
|
+
const id = clip.id ?? ctx.allocateClipId();
|
|
52
|
+
ctx.midiClips.set(id, { ...clip, id, events: clip.events.map((event) => ({ ...event })) });
|
|
53
|
+
}
|
|
54
|
+
syncMidiClips(ctx);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function syncClipsDelta(ctx: EngineClipContext, upserts: EngineClip[], removeIds: number[]): void {
|
|
58
|
+
const clips = Array.from(ctx.clips.values());
|
|
59
|
+
ctx.offlineEngine.setClips(clips);
|
|
60
|
+
ctx.postSync({
|
|
61
|
+
type: 'syncClipsDelta',
|
|
62
|
+
upserts,
|
|
63
|
+
removeIds,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function syncMidiClips(ctx: EngineClipContext): void {
|
|
68
|
+
const clips = Array.from(ctx.midiClips.values());
|
|
69
|
+
ctx.offlineEngine.setMidiClips(clips);
|
|
70
|
+
ctx.postSync({ type: 'syncMidiClips', clips });
|
|
71
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { EngineMarker, RealtimeEngine } from '../index';
|
|
2
|
+
import { resolveMarkerSet } from './engine-offline';
|
|
3
|
+
import type { SonareEngineSyncMessage } from './messages';
|
|
4
|
+
import { type SonareEngineCommandRecord, SonareEngineCommandType } from './protocol';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Collaborator surface the marker helpers need from the owning
|
|
8
|
+
* {@link SonareEngine}: the marker store and id counter, the offline engine they
|
|
9
|
+
* mirror into, the sync poster, the realtime command sender, and the loop setter.
|
|
10
|
+
*/
|
|
11
|
+
export interface EngineMarkerContext {
|
|
12
|
+
readonly offlineEngine: RealtimeEngine;
|
|
13
|
+
readonly markers: Map<number, EngineMarker>;
|
|
14
|
+
getNextMarkerId(): number;
|
|
15
|
+
setNextMarkerId(value: number): void;
|
|
16
|
+
postSync(message: SonareEngineSyncMessage): void;
|
|
17
|
+
sendCommand(command: SonareEngineCommandRecord): boolean;
|
|
18
|
+
setLoop(startPpq: number, endPpq: number, enabled: boolean): boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function addMarker(ctx: EngineMarkerContext, ppq: number, name = ''): number {
|
|
22
|
+
const id = ctx.getNextMarkerId();
|
|
23
|
+
ctx.setNextMarkerId(id + 1);
|
|
24
|
+
ctx.markers.set(id, { id, ppq, name });
|
|
25
|
+
syncMarkers(ctx);
|
|
26
|
+
return id;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Replaces the whole marker set in one call.
|
|
31
|
+
*
|
|
32
|
+
* Entries without an `id` are assigned fresh ids; entries carrying an `id`
|
|
33
|
+
* keep it (ids must be positive and unique within the list). Returns the
|
|
34
|
+
* resolved markers in the order given, so a caller can map its own marker
|
|
35
|
+
* identities to the engine ids used by `seekMarker`/`setLoopFromMarkers`.
|
|
36
|
+
*
|
|
37
|
+
* @param markers The full marker list (an empty list clears all markers).
|
|
38
|
+
* @returns The markers with their resolved engine ids.
|
|
39
|
+
*/
|
|
40
|
+
export function setMarkers(
|
|
41
|
+
ctx: EngineMarkerContext,
|
|
42
|
+
markers: ReadonlyArray<{ ppq: number; name?: string; id?: number }>,
|
|
43
|
+
): EngineMarker[] {
|
|
44
|
+
const { resolved, nextMarkerId } = resolveMarkerSet(markers, ctx.getNextMarkerId());
|
|
45
|
+
ctx.setNextMarkerId(nextMarkerId);
|
|
46
|
+
ctx.markers.clear();
|
|
47
|
+
for (const marker of resolved) {
|
|
48
|
+
ctx.markers.set(marker.id, marker);
|
|
49
|
+
}
|
|
50
|
+
syncMarkers(ctx);
|
|
51
|
+
return resolved.map((marker) => ({ ...marker }));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function markerCount(ctx: EngineMarkerContext): number {
|
|
55
|
+
return ctx.offlineEngine.markerCount();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function markerByIndex(ctx: EngineMarkerContext, index: number): EngineMarker {
|
|
59
|
+
return ctx.offlineEngine.markerByIndex(index);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function marker(ctx: EngineMarkerContext, markerId: number): EngineMarker {
|
|
63
|
+
return ctx.offlineEngine.marker(markerId);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function seekMarker(ctx: EngineMarkerContext, markerId: number): boolean {
|
|
67
|
+
ctx.offlineEngine.seekMarker(markerId);
|
|
68
|
+
// Forward to the live worklet engine. Its marker set is kept in sync via the
|
|
69
|
+
// 'syncMarkers' message (see syncMarkers), so a queued kSeekMarker resolves
|
|
70
|
+
// the marker id to its frame on the audio thread.
|
|
71
|
+
return ctx.sendCommand({
|
|
72
|
+
type: SonareEngineCommandType.SeekMarker,
|
|
73
|
+
targetId: markerId,
|
|
74
|
+
sampleTime: -1,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function setLoopFromMarkers(
|
|
79
|
+
ctx: EngineMarkerContext,
|
|
80
|
+
startMarkerId: number,
|
|
81
|
+
endMarkerId: number,
|
|
82
|
+
): boolean {
|
|
83
|
+
ctx.offlineEngine.setLoopFromMarkers(startMarkerId, endMarkerId);
|
|
84
|
+
const start = ctx.offlineEngine.marker(startMarkerId);
|
|
85
|
+
const end = ctx.offlineEngine.marker(endMarkerId);
|
|
86
|
+
return ctx.setLoop(start.ppq, end.ppq, true);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function syncMarkers(ctx: EngineMarkerContext): void {
|
|
90
|
+
const markers = Array.from(ctx.markers.values()).sort((a, b) => a.ppq - b.ppq);
|
|
91
|
+
ctx.offlineEngine.setMarkers(markers);
|
|
92
|
+
ctx.postSync({ type: 'syncMarkers', markers });
|
|
93
|
+
}
|