@libraz/libsonare 1.4.1 → 1.5.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.
Files changed (66) hide show
  1. package/README.md +51 -20
  2. package/dist/index.d.ts +5416 -1
  3. package/dist/index.js +938 -583
  4. package/dist/index.js.map +1 -1
  5. package/dist/sonare.js +2 -2
  6. package/dist/sonare.wasm +0 -0
  7. package/dist/worklet.d.ts +1083 -5227
  8. package/dist/worklet.js +2683 -2451
  9. package/dist/worklet.js.map +1 -1
  10. package/package.json +4 -9
  11. package/src/clip_page_streamer.ts +298 -0
  12. package/src/effects_mastering.ts +85 -1089
  13. package/src/effects_transform.ts +286 -0
  14. package/src/effects_voice_change.ts +118 -0
  15. package/src/feature_music.ts +5 -2
  16. package/src/feature_spectrogram.ts +42 -2
  17. package/src/features.ts +1 -0
  18. package/src/index.ts +23 -0
  19. package/src/mastering_chain.ts +200 -0
  20. package/src/mastering_core.ts +248 -0
  21. package/src/mastering_dynamics.ts +105 -0
  22. package/src/mastering_repair.ts +161 -0
  23. package/src/mixer.ts +8 -0
  24. package/src/mixing_oneshot.ts +54 -0
  25. package/src/module_state.ts +1 -2
  26. package/src/project.ts +71 -1712
  27. package/src/project_class.ts +871 -0
  28. package/src/project_internal.ts +333 -0
  29. package/src/project_synth.ts +43 -0
  30. package/src/project_types.ts +570 -0
  31. package/src/public_types.ts +6 -1221
  32. package/src/public_types_acoustic.ts +115 -0
  33. package/src/public_types_mastering.ts +333 -0
  34. package/src/public_types_mixing.ts +97 -0
  35. package/src/public_types_music.ts +352 -0
  36. package/src/public_types_realtime.ts +163 -0
  37. package/src/public_types_spectral.ts +194 -0
  38. package/src/realtime_engine.ts +94 -0
  39. package/src/sonare.js.d.ts +117 -38
  40. package/src/stream_analyzer.ts +3 -0
  41. package/src/stream_types.ts +4 -0
  42. package/src/worklet/engine-automation.ts +73 -0
  43. package/src/worklet/engine-capture-facade.ts +80 -0
  44. package/src/worklet/engine-clips.ts +71 -0
  45. package/src/worklet/engine-markers.ts +93 -0
  46. package/src/worklet/engine-mixer-facade.ts +186 -0
  47. package/src/worklet/engine-node.ts +451 -0
  48. package/src/worklet/engine-offline.ts +162 -0
  49. package/src/worklet/engine-options.ts +13 -0
  50. package/src/worklet/engine-parameter-facade.ts +172 -0
  51. package/src/worklet/engine-processor.ts +764 -0
  52. package/src/worklet/engine-register.ts +136 -0
  53. package/src/worklet/engine-strips.ts +315 -0
  54. package/src/worklet/engine-sync.ts +94 -0
  55. package/src/worklet/engine-tempo-facade.ts +141 -0
  56. package/src/worklet/engine.ts +998 -0
  57. package/src/worklet/guards.ts +14 -1
  58. package/src/worklet/messages.ts +60 -20
  59. package/src/worklet/mixer-processor.ts +368 -0
  60. package/src/worklet/protocol.ts +3 -0
  61. package/src/worklet/voice-changer-processor.ts +246 -0
  62. package/src/worklet.ts +20 -3549
  63. package/dist/sonare-rt-module.js +0 -2
  64. package/dist/sonare-rt.js +0 -2
  65. package/dist/sonare-rt.wasm +0 -0
  66. package/src/sonare-rt.d.ts +0 -93
@@ -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);
@@ -26,45 +26,45 @@ export interface WasmKeyCandidateResult {
26
26
  correlation: number;
27
27
  }
28
28
 
29
- // Flat POD mirror of SonareRealtimeVoiceChangerConfig. Field names match the
30
- // C ABI / Python POD exactly (snake_case).
29
+ // Flat POD mirror of SonareRealtimeVoiceChangerConfig. Keys are camelCase to
30
+ // match the Node addon getter (the underlying C ABI / Python POD is snake_case).
31
31
  export interface WasmRealtimeVoiceChangerPodConfig {
32
- input_gain_db: number;
33
- output_gain_db: number;
34
- wet_mix: number;
35
- retune_semitones: number;
36
- retune_mix: number;
37
- retune_grain_size: number;
38
- formant_factor: number;
39
- formant_amount: number;
40
- formant_body: number;
41
- formant_brightness: number;
42
- formant_nasal: number;
43
- eq_highpass_hz: number;
44
- eq_body_db: number;
45
- eq_presence_db: number;
46
- eq_air_db: number;
47
- gate_threshold_db: number;
48
- gate_attack_ms: number;
49
- gate_release_ms: number;
50
- gate_range_db: number;
51
- compressor_threshold_db: number;
52
- compressor_ratio: number;
53
- compressor_attack_ms: number;
54
- compressor_release_ms: number;
55
- compressor_makeup_gain_db: number;
56
- deesser_frequency_hz: number;
57
- deesser_threshold_db: number;
58
- deesser_ratio: number;
59
- deesser_range_db: number;
60
- reverb_mix: number;
61
- reverb_time_ms: number;
62
- reverb_damping: number;
63
- reverb_seed: number;
64
- limiter_ceiling_db: number;
65
- limiter_release_ms: number;
66
- limiter_enable_isp_limiter: boolean;
67
- limiter_isp_ceiling_dbtp: number;
32
+ inputGainDb: number;
33
+ outputGainDb: number;
34
+ wetMix: number;
35
+ retuneSemitones: number;
36
+ retuneMix: number;
37
+ retuneGrainSize: number;
38
+ formantFactor: number;
39
+ formantAmount: number;
40
+ formantBody: number;
41
+ formantBrightness: number;
42
+ formantNasal: number;
43
+ eqHighpassHz: number;
44
+ eqBodyDb: number;
45
+ eqPresenceDb: number;
46
+ eqAirDb: number;
47
+ gateThresholdDb: number;
48
+ gateAttackMs: number;
49
+ gateReleaseMs: number;
50
+ gateRangeDb: number;
51
+ compressorThresholdDb: number;
52
+ compressorRatio: number;
53
+ compressorAttackMs: number;
54
+ compressorReleaseMs: number;
55
+ compressorMakeupGainDb: number;
56
+ deesserFrequencyHz: number;
57
+ deesserThresholdDb: number;
58
+ deesserRatio: number;
59
+ deesserRangeDb: number;
60
+ reverbMix: number;
61
+ reverbTimeMs: number;
62
+ reverbDamping: number;
63
+ reverbSeed: number;
64
+ limiterCeilingDb: number;
65
+ limiterReleaseMs: number;
66
+ limiterEnableIspLimiter: boolean;
67
+ limiterIspCeilingDbtp: number;
68
68
  }
69
69
 
70
70
  export interface WasmBeatResult {
@@ -194,6 +194,7 @@ export interface WasmStreamConfigDefaults {
194
194
  computeSpectral: boolean;
195
195
  emitEveryNFrames: number;
196
196
  magnitudeDownsample: number;
197
+ maxPendingFrames: number;
197
198
  keyUpdateIntervalSec: number;
198
199
  bpmUpdateIntervalSec: number;
199
200
  window: number;
@@ -510,6 +511,10 @@ export interface WasmMixMeterSnapshot {
510
511
  truePeakDbR: number;
511
512
  maxTruePeakDb: number;
512
513
  seq: number;
514
+ channelCount: number;
515
+ peakDb: number[];
516
+ rmsDb: number[];
517
+ truePeakDb: number[];
513
518
  }
514
519
 
515
520
  export interface WasmMixResult {
@@ -783,6 +788,23 @@ export interface WasmEngineTimeSignatureSegment {
783
788
  denominator: number;
784
789
  }
785
790
 
791
+ /**
792
+ * One external-MIDI event drained from the engine, already lowered to MIDI 1.0
793
+ * bytes ready to write to a Web MIDI output port.
794
+ */
795
+ export interface WasmExternalMidiEvent {
796
+ /**
797
+ * Originating track lane (Track.midi_destination_id), or the transport
798
+ * sentinel 0xFFFFFFFF for clock / start / continue / stop bytes meant for
799
+ * every external port.
800
+ */
801
+ destinationId: number;
802
+ /** Sample position within the producing block at which the event fires. */
803
+ renderFrame: number;
804
+ /** MIDI 1.0 status + data bytes (1..3 entries). */
805
+ bytes: number[];
806
+ }
807
+
786
808
  export interface WasmRealtimeEngine {
787
809
  prepare: (
788
810
  sampleRate: number,
@@ -792,6 +814,7 @@ export interface WasmRealtimeEngine {
792
814
  ) => void;
793
815
  setParameter: (paramId: number, value: number, renderFrame: number) => void;
794
816
  setParameterSmoothed: (paramId: number, value: number, renderFrame: number) => void;
817
+ setParamSmoothingMs: (smoothingMs: number) => void;
795
818
  setSoloMute: (laneIndex: number, solo: boolean, mute: boolean, renderFrame: number) => void;
796
819
  getTransportState: () => WasmEngineTransportState;
797
820
  play: (renderFrame: number) => void;
@@ -851,6 +874,25 @@ export interface WasmRealtimeEngine {
851
874
  value: number,
852
875
  ) => void;
853
876
  setMasterStripInsertParamByName: (insertIndex: number, paramName: string, value: number) => void;
877
+ setBusStripInsertParamByName: (
878
+ busId: number,
879
+ insertIndex: number,
880
+ paramName: string,
881
+ value: number,
882
+ ) => void;
883
+ setBusStripInsertBypassed: (
884
+ busId: number,
885
+ insertIndex: number,
886
+ bypassed: boolean,
887
+ resetOnBypass: boolean,
888
+ ) => void;
889
+ resolveTrackInsertAutomationId: (
890
+ trackId: number,
891
+ insertIndex: number,
892
+ paramName: string,
893
+ ) => number;
894
+ resolveMasterInsertAutomationId: (insertIndex: number, paramName: string) => number;
895
+ resolveBusInsertAutomationId: (busId: number, insertIndex: number, paramName: string) => number;
854
896
  setTrackStripPan: (trackId: number, pan: number) => void;
855
897
  setTrackStripPanLaw: (trackId: number, panLaw: number) => void;
856
898
  setTrackStripPanMode: (trackId: number, panMode: number) => void;
@@ -936,7 +978,12 @@ export interface WasmRealtimeEngine {
936
978
  value: number,
937
979
  renderFrame: number,
938
980
  ) => void;
981
+ pushMidiSysex: (destinationId: number, data: Uint8Array, renderFrame: number) => void;
939
982
  pushMidiPanic: (renderFrame: number) => void;
983
+ setMidiDestinationExternal: (destinationId: number, external: boolean) => void;
984
+ setExternalMidiClockEnabled: (enabled: boolean) => void;
985
+ externalMidiDroppedCount: () => number;
986
+ drainExternalMidi: (maxRecords: number) => WasmExternalMidiEvent[];
940
987
  clearParameters: () => void;
941
988
  process: (channels: Float32Array[]) => Float32Array[];
942
989
  prepareChannels: (numChannels: number, maxFrames: number) => void;
@@ -1110,6 +1157,7 @@ export interface SonareModule {
1110
1157
  detectKeyCandidates: (samples: Float32Array, sampleRate: number) => WasmKeyCandidateResult[];
1111
1158
  hasFfmpegSupport: () => boolean;
1112
1159
  version: () => string;
1160
+ abiVersion: () => number;
1113
1161
  engineAbiVersion: () => number;
1114
1162
  voiceChangerAbiVersion: () => number;
1115
1163
  voiceCharacterPresetId: (preset: number) => string | null;
@@ -1337,6 +1385,25 @@ export interface SonareModule {
1337
1385
  voiced?: Float32Array,
1338
1386
  voicedProb?: Float32Array,
1339
1387
  ) => Float32Array;
1388
+ pitchCorrectTimevarying: (
1389
+ samples: Float32Array,
1390
+ sampleRate: number,
1391
+ f0Hz: Float32Array,
1392
+ hopLength: number,
1393
+ options: {
1394
+ mode?: string;
1395
+ targetMidi?: number;
1396
+ scaleRoot?: number;
1397
+ scaleModeMask?: number;
1398
+ referenceMidi?: number;
1399
+ retuneAmount?: number;
1400
+ maxCorrectionSemitones?: number;
1401
+ retuneSpeedMs?: number;
1402
+ vibratoThresholdCents?: number;
1403
+ voiced?: Float32Array;
1404
+ voicedProb?: Float32Array;
1405
+ },
1406
+ ) => Float32Array;
1340
1407
  noteStretch: (
1341
1408
  samples: Float32Array,
1342
1409
  sampleRate: number,
@@ -1567,6 +1634,7 @@ export interface SonareModule {
1567
1634
  fmin: number,
1568
1635
  fmax: number,
1569
1636
  htk: boolean,
1637
+ lifter: number,
1570
1638
  ) => WasmMfccResult;
1571
1639
 
1572
1640
  // Features - Inverse reconstruction
@@ -1792,6 +1860,12 @@ export interface SonareModule {
1792
1860
  hopLength: number,
1793
1861
  nChroma: number,
1794
1862
  ) => WasmChromaResult;
1863
+ chromaCqt: (
1864
+ samples: Float32Array,
1865
+ sampleRate: number,
1866
+ hopLength: number,
1867
+ nChroma: number,
1868
+ ) => WasmChromaResult;
1795
1869
  bassChroma: (
1796
1870
  samples: Float32Array,
1797
1871
  sampleRate: number,
@@ -1876,6 +1950,7 @@ export interface SonareModule {
1876
1950
  winLength: number,
1877
1951
  sampleRate: number,
1878
1952
  hopLength: number,
1953
+ factors?: Float32Array | number[],
1879
1954
  ) => Float32Array;
1880
1955
  lufs: (samples: Float32Array, sampleRate: number) => WasmLufsResult;
1881
1956
  momentaryLufs: (samples: Float32Array, sampleRate: number) => Float32Array;
@@ -1952,6 +2027,7 @@ export interface SonareModule {
1952
2027
  computeSpectral: boolean,
1953
2028
  emitEveryNFrames: number,
1954
2029
  magnitudeDownsample: number,
2030
+ maxPendingFrames: number,
1955
2031
  keyUpdateIntervalSec: number,
1956
2032
  bpmUpdateIntervalSec: number,
1957
2033
  window: number,
@@ -2162,6 +2238,7 @@ export interface WasmMixer {
2162
2238
  vcaGroupCount: () => number;
2163
2239
  toSceneJson: () => string;
2164
2240
  tailSamples: () => number;
2241
+ latencySamples: () => number;
2165
2242
  drainTailStereo: (numSamples: number) => {
2166
2243
  left: Float32Array;
2167
2244
  right: Float32Array;
@@ -2225,6 +2302,8 @@ export interface WasmAnalyzerStats {
2225
2302
  totalFrames: number;
2226
2303
  totalSamples: number;
2227
2304
  durationSeconds: number;
2305
+ pendingFrames: number;
2306
+ droppedOutputFrames: number;
2228
2307
  estimate: WasmProgressiveEstimate;
2229
2308
  }
2230
2309
 
@@ -71,6 +71,7 @@ export class StreamAnalyzer {
71
71
  config.computeSpectral ?? defaults.computeSpectral,
72
72
  config.emitEveryNFrames ?? defaults.emitEveryNFrames,
73
73
  config.magnitudeDownsample ?? defaults.magnitudeDownsample,
74
+ config.maxPendingFrames ?? defaults.maxPendingFrames,
74
75
  config.keyUpdateIntervalSec ?? defaults.keyUpdateIntervalSec,
75
76
  config.bpmUpdateIntervalSec ?? defaults.bpmUpdateIntervalSec,
76
77
  config.window ?? defaults.window,
@@ -163,6 +164,8 @@ export class StreamAnalyzer {
163
164
  totalFrames: s.totalFrames,
164
165
  totalSamples: s.totalSamples,
165
166
  durationSeconds: s.durationSeconds,
167
+ pendingFrames: s.pendingFrames,
168
+ droppedOutputFrames: s.droppedOutputFrames,
166
169
  estimate: {
167
170
  bpm: s.estimate.bpm,
168
171
  bpmConfidence: s.estimate.bpmConfidence,
@@ -64,6 +64,8 @@ export interface AnalyzerStats {
64
64
  totalFrames: number;
65
65
  totalSamples: number;
66
66
  durationSeconds: number;
67
+ pendingFrames: number;
68
+ droppedOutputFrames: number;
67
69
  estimate: ProgressiveEstimate;
68
70
  }
69
71
 
@@ -159,6 +161,8 @@ export interface StreamConfig {
159
161
  computeSpectral?: boolean;
160
162
  emitEveryNFrames?: number;
161
163
  magnitudeDownsample?: number;
164
+ /** Maximum unread frames; overflow drops the oldest frame. */
165
+ maxPendingFrames?: number;
162
166
  keyUpdateIntervalSec?: number;
163
167
  bpmUpdateIntervalSec?: number;
164
168
  window?: 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
+ }