@libraz/libsonare 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +50 -19
  2. package/dist/index.d.ts +5384 -1
  3. package/dist/index.js +884 -574
  4. package/dist/index.js.map +1 -1
  5. package/dist/sonare.js +1 -1
  6. package/dist/sonare.wasm +0 -0
  7. package/dist/worklet.d.ts +1084 -5220
  8. package/dist/worklet.js +2677 -2452
  9. package/dist/worklet.js.map +1 -1
  10. package/package.json +4 -9
  11. package/src/clip_page_streamer.ts +250 -0
  12. package/src/effects_mastering.ts +85 -1082
  13. package/src/effects_transform.ts +286 -0
  14. package/src/effects_voice_change.ts +118 -0
  15. package/src/feature_music.ts +13 -9
  16. package/src/feature_spectrogram.ts +42 -2
  17. package/src/features.ts +1 -0
  18. package/src/index.ts +11 -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/mixing_oneshot.ts +54 -0
  24. package/src/module_state.ts +1 -2
  25. package/src/project.ts +71 -1688
  26. package/src/project_class.ts +861 -0
  27. package/src/project_internal.ts +332 -0
  28. package/src/project_synth.ts +43 -0
  29. package/src/project_types.ts +570 -0
  30. package/src/public_types.ts +6 -1217
  31. package/src/public_types_acoustic.ts +115 -0
  32. package/src/public_types_mastering.ts +333 -0
  33. package/src/public_types_mixing.ts +97 -0
  34. package/src/public_types_music.ts +352 -0
  35. package/src/public_types_realtime.ts +163 -0
  36. package/src/public_types_spectral.ts +194 -0
  37. package/src/quick_analysis.ts +18 -14
  38. package/src/realtime_engine.ts +94 -0
  39. package/src/realtime_voice_changer.ts +2 -1
  40. package/src/sonare.js.d.ts +74 -0
  41. package/src/worklet/engine-automation.ts +73 -0
  42. package/src/worklet/engine-capture-facade.ts +80 -0
  43. package/src/worklet/engine-clips.ts +71 -0
  44. package/src/worklet/engine-markers.ts +93 -0
  45. package/src/worklet/engine-mixer-facade.ts +186 -0
  46. package/src/worklet/engine-node.ts +451 -0
  47. package/src/worklet/engine-offline.ts +162 -0
  48. package/src/worklet/engine-options.ts +13 -0
  49. package/src/worklet/engine-parameter-facade.ts +172 -0
  50. package/src/worklet/engine-processor.ts +764 -0
  51. package/src/worklet/engine-register.ts +136 -0
  52. package/src/worklet/engine-strips.ts +315 -0
  53. package/src/worklet/engine-sync.ts +94 -0
  54. package/src/worklet/engine-tempo-facade.ts +141 -0
  55. package/src/worklet/engine.ts +998 -0
  56. package/src/worklet/guards.ts +14 -1
  57. package/src/worklet/messages.ts +60 -20
  58. package/src/worklet/mixer-processor.ts +368 -0
  59. package/src/worklet/protocol.ts +3 -0
  60. package/src/worklet/voice-changer-processor.ts +246 -0
  61. package/src/worklet.ts +20 -3549
  62. package/dist/sonare-rt-module.js +0 -2
  63. package/dist/sonare-rt.js +0 -2
  64. package/dist/sonare-rt.wasm +0 -0
  65. package/src/sonare-rt.d.ts +0 -93
@@ -107,20 +107,24 @@ export function detectKeyCandidates(
107
107
  options: KeyDetectionOptions = {},
108
108
  ): KeyCandidate[] {
109
109
  validateAnalysisInput('detectKeyCandidates', samples, sampleRate, options);
110
- return requireModule()
111
- ._detectKeyCandidates(
112
- samples,
113
- sampleRate,
114
- options.nFft ?? 4096,
115
- options.hopLength ?? 512,
116
- options.useHpss ?? false,
117
- options.loudnessWeighted ?? false,
118
- options.highPassHz ?? 0,
119
- keyModeValues(options.modes),
120
- keyProfileValue(options.profile),
121
- options.genreHint ?? '',
122
- )
123
- .map(convertKeyCandidate);
110
+ // The embind value marshalling returns an array whose constructor is not this
111
+ // realm's Array; chaining .map() onto it propagates that constructor via
112
+ // Symbol.species, leaving a result that structuredClone (and so postMessage to
113
+ // a Worker) rejects with "could not be cloned". Array.from() re-roots it as a
114
+ // plain Array before mapping.
115
+ const candidates = requireModule()._detectKeyCandidates(
116
+ samples,
117
+ sampleRate,
118
+ options.nFft ?? 4096,
119
+ options.hopLength ?? 512,
120
+ options.useHpss ?? false,
121
+ options.loudnessWeighted ?? false,
122
+ options.highPassHz ?? 0,
123
+ keyModeValues(options.modes),
124
+ keyProfileValue(options.profile),
125
+ options.genreHint ?? '',
126
+ );
127
+ return Array.from(candidates, convertKeyCandidate);
124
128
  }
125
129
 
126
130
  /**
@@ -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);
@@ -259,7 +259,8 @@ export class RealtimeVoiceChanger {
259
259
  }
260
260
 
261
261
  export function realtimeVoiceChangerPresetNames(): VoicePresetId[] {
262
- return getSonareModule().realtimeVoiceChangerPresetNames() as VoicePresetId[];
262
+ // Array.from re-roots embind's vector as a plain, structured-cloneable Array.
263
+ return Array.from(getSonareModule().realtimeVoiceChangerPresetNames()) as VoicePresetId[];
263
264
  }
264
265
 
265
266
  export function realtimeVoiceChangerPresetJson(name: VoicePresetId): string {
@@ -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,
@@ -1357,6 +1422,8 @@ export interface SonareModule {
1357
1422
  targetLufs: number,
1358
1423
  ceilingDb: number,
1359
1424
  truePeakOversample: number,
1425
+ releaseMs: number,
1426
+ applyGainAtInputRate: boolean,
1360
1427
  ) => WasmMasteringResult;
1361
1428
  masteringProcessorNames: () => string[];
1362
1429
  masteringPairProcessorNames: () => string[];
@@ -1565,6 +1632,7 @@ export interface SonareModule {
1565
1632
  fmin: number,
1566
1633
  fmax: number,
1567
1634
  htk: boolean,
1635
+ lifter: number,
1568
1636
  ) => WasmMfccResult;
1569
1637
 
1570
1638
  // Features - Inverse reconstruction
@@ -1790,6 +1858,12 @@ export interface SonareModule {
1790
1858
  hopLength: number,
1791
1859
  nChroma: number,
1792
1860
  ) => WasmChromaResult;
1861
+ chromaCqt: (
1862
+ samples: Float32Array,
1863
+ sampleRate: number,
1864
+ hopLength: number,
1865
+ nChroma: number,
1866
+ ) => WasmChromaResult;
1793
1867
  bassChroma: (
1794
1868
  samples: Float32Array,
1795
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
+ }