@libraz/libsonare 1.5.1 → 1.5.3
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 +18 -3
- package/dist/index.d.ts +1165 -178
- package/dist/index.js +4830 -3871
- package/dist/index.js.map +1 -1
- package/dist/sonare.js +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/worklet.d.ts +29 -1
- package/dist/worklet.js +1068 -974
- package/dist/worklet.js.map +1 -1
- package/package.json +2 -1
- package/src/_chain_config.ts +46 -0
- package/src/audio.ts +24 -4
- package/src/effects_mastering.ts +25 -1
- package/src/effects_transform.ts +283 -43
- package/src/effects_voice_change.ts +46 -53
- package/src/feature_core.ts +316 -12
- package/src/feature_music.ts +462 -0
- package/src/feature_pitch.ts +61 -0
- package/src/feature_resample.ts +17 -2
- package/src/feature_spectral.ts +349 -2
- package/src/feature_spectrogram.ts +336 -0
- package/src/features.ts +2 -0
- package/src/index.ts +133 -1
- package/src/mastering_chain.ts +305 -50
- package/src/mastering_core.ts +242 -16
- package/src/mastering_dynamics.ts +66 -10
- package/src/mastering_repair.ts +119 -7
- package/src/metering.ts +366 -81
- package/src/mixer.ts +10 -3
- package/src/mixing_oneshot.ts +25 -5
- package/src/module_state.ts +1 -1
- package/src/project_class.ts +17 -6
- package/src/project_internal.ts +10 -2
- package/src/project_types.ts +25 -2
- package/src/public_types_mastering.ts +114 -4
- package/src/public_types_music.ts +1 -0
- package/src/public_types_spectral.ts +7 -0
- package/src/quick_analysis.ts +325 -117
- package/src/realtime_engine.ts +8 -0
- package/src/sonare.js.d.ts +56 -0
- package/src/stream_analyzer.ts +25 -2
- package/src/stream_types.ts +16 -1
- package/src/validation.ts +17 -2
- package/src/worklet/engine-clips.ts +67 -2
- package/src/worklet/engine-processor.ts +33 -5
- package/src/worklet/engine.ts +7 -3
- package/src/worklet/guards.ts +3 -0
- package/src/worklet/messages.ts +27 -0
package/src/realtime_engine.ts
CHANGED
|
@@ -587,6 +587,14 @@ export class RealtimeEngine {
|
|
|
587
587
|
);
|
|
588
588
|
}
|
|
589
589
|
|
|
590
|
+
/**
|
|
591
|
+
* Returns the PCM generated for a tempo-sync clip by the control-thread
|
|
592
|
+
* setter, or `null` when the clip did not require a tempo-sync bake.
|
|
593
|
+
*/
|
|
594
|
+
prebakedClipChannels(clipId: number): Float32Array[] | null {
|
|
595
|
+
return this.native.prebakedClipChannels(clipId);
|
|
596
|
+
}
|
|
597
|
+
|
|
590
598
|
clipCount(): number {
|
|
591
599
|
return this.native.clipCount();
|
|
592
600
|
}
|
package/src/sonare.js.d.ts
CHANGED
|
@@ -195,6 +195,7 @@ export interface WasmStreamConfigDefaults {
|
|
|
195
195
|
emitEveryNFrames: number;
|
|
196
196
|
magnitudeDownsample: number;
|
|
197
197
|
maxPendingFrames: number;
|
|
198
|
+
maxProgressionEntries: number;
|
|
198
199
|
keyUpdateIntervalSec: number;
|
|
199
200
|
bpmUpdateIntervalSec: number;
|
|
200
201
|
window: number;
|
|
@@ -469,8 +470,16 @@ export interface WasmMasteringResult {
|
|
|
469
470
|
latencySamples?: number;
|
|
470
471
|
}
|
|
471
472
|
|
|
473
|
+
export interface WasmStageGainReduction {
|
|
474
|
+
stage: string;
|
|
475
|
+
gainReductionDb: number;
|
|
476
|
+
}
|
|
477
|
+
|
|
472
478
|
export interface WasmMasteringChainResult extends WasmMasteringResult {
|
|
473
479
|
stages: string[];
|
|
480
|
+
outputTruePeakDbtp: number;
|
|
481
|
+
outputLra: number;
|
|
482
|
+
stageGainReductions: WasmStageGainReduction[];
|
|
474
483
|
}
|
|
475
484
|
|
|
476
485
|
export interface WasmMasteringStereoChainResult {
|
|
@@ -481,6 +490,9 @@ export interface WasmMasteringStereoChainResult {
|
|
|
481
490
|
outputLufs: number;
|
|
482
491
|
appliedGainDb: number;
|
|
483
492
|
stages: string[];
|
|
493
|
+
outputTruePeakDbtp: number;
|
|
494
|
+
outputLra: number;
|
|
495
|
+
stageGainReductions: WasmStageGainReduction[];
|
|
484
496
|
}
|
|
485
497
|
|
|
486
498
|
export interface WasmMasteringStereoResult {
|
|
@@ -847,6 +859,7 @@ export interface WasmRealtimeEngine {
|
|
|
847
859
|
graphNodeCount: () => number;
|
|
848
860
|
graphConnectionCount: () => number;
|
|
849
861
|
setClips: (clips: WasmEngineClip[]) => void;
|
|
862
|
+
prebakedClipChannels: (clipId: number) => Float32Array[] | null;
|
|
850
863
|
clipCount: () => number;
|
|
851
864
|
setTrackLanes: (lanes: Array<number | WasmEngineTrackLane>) => void;
|
|
852
865
|
setLaneSidechain: (trackId: number, insertIndex: number, sourceTrackId: number) => void;
|
|
@@ -1411,12 +1424,25 @@ export interface SonareModule {
|
|
|
1411
1424
|
offsetSample: number,
|
|
1412
1425
|
stretchRatio: number,
|
|
1413
1426
|
) => Float32Array;
|
|
1427
|
+
noteMove: (
|
|
1428
|
+
samples: Float32Array,
|
|
1429
|
+
sampleRate: number,
|
|
1430
|
+
onsetSample: number,
|
|
1431
|
+
offsetSample: number,
|
|
1432
|
+
targetOnsetSample: number,
|
|
1433
|
+
) => Float32Array;
|
|
1414
1434
|
voiceChange: (
|
|
1415
1435
|
samples: Float32Array,
|
|
1416
1436
|
sampleRate: number,
|
|
1417
1437
|
pitchSemitones: number,
|
|
1418
1438
|
formantFactor: number,
|
|
1419
1439
|
) => Float32Array;
|
|
1440
|
+
voiceChangeRealtime: (
|
|
1441
|
+
samples: Float32Array,
|
|
1442
|
+
sampleRate: number,
|
|
1443
|
+
preset: string,
|
|
1444
|
+
channels: number,
|
|
1445
|
+
) => Float32Array;
|
|
1420
1446
|
normalize: (samples: Float32Array, sampleRate: number, targetDb: number) => Float32Array;
|
|
1421
1447
|
mastering: (
|
|
1422
1448
|
samples: Float32Array,
|
|
@@ -1906,6 +1932,27 @@ export interface SonareModule {
|
|
|
1906
1932
|
binsPerOctave: number,
|
|
1907
1933
|
gamma: number,
|
|
1908
1934
|
) => WasmCqtResult;
|
|
1935
|
+
cqtToAudio: (
|
|
1936
|
+
magnitude: Float32Array,
|
|
1937
|
+
nBins: number,
|
|
1938
|
+
nFrames: number,
|
|
1939
|
+
sampleRate: number,
|
|
1940
|
+
hopLength: number,
|
|
1941
|
+
fmin: number,
|
|
1942
|
+
binsPerOctave: number,
|
|
1943
|
+
nIter: number,
|
|
1944
|
+
) => Float32Array;
|
|
1945
|
+
vqtToAudio: (
|
|
1946
|
+
magnitude: Float32Array,
|
|
1947
|
+
nBins: number,
|
|
1948
|
+
nFrames: number,
|
|
1949
|
+
sampleRate: number,
|
|
1950
|
+
hopLength: number,
|
|
1951
|
+
fmin: number,
|
|
1952
|
+
binsPerOctave: number,
|
|
1953
|
+
gamma: number,
|
|
1954
|
+
nIter: number,
|
|
1955
|
+
) => Float32Array;
|
|
1909
1956
|
analyzeSections: (
|
|
1910
1957
|
samples: Float32Array,
|
|
1911
1958
|
sampleRate: number,
|
|
@@ -2028,6 +2075,7 @@ export interface SonareModule {
|
|
|
2028
2075
|
emitEveryNFrames: number,
|
|
2029
2076
|
magnitudeDownsample: number,
|
|
2030
2077
|
maxPendingFrames: number,
|
|
2078
|
+
maxProgressionEntries: number,
|
|
2031
2079
|
keyUpdateIntervalSec: number,
|
|
2032
2080
|
bpmUpdateIntervalSec: number,
|
|
2033
2081
|
window: number,
|
|
@@ -2304,6 +2352,8 @@ export interface WasmAnalyzerStats {
|
|
|
2304
2352
|
durationSeconds: number;
|
|
2305
2353
|
pendingFrames: number;
|
|
2306
2354
|
droppedOutputFrames: number;
|
|
2355
|
+
droppedChordProgressionEntries: number;
|
|
2356
|
+
droppedBarProgressionEntries: number;
|
|
2307
2357
|
estimate: WasmProgressiveEstimate;
|
|
2308
2358
|
}
|
|
2309
2359
|
|
|
@@ -2311,6 +2361,8 @@ export interface WasmFrameBuffer {
|
|
|
2311
2361
|
nFrames: number;
|
|
2312
2362
|
/** Number of mel bands; flat `mel` is `[nFrames * nMels]` row-major. */
|
|
2313
2363
|
nMels: number;
|
|
2364
|
+
nChroma: number;
|
|
2365
|
+
featureFlags: number;
|
|
2314
2366
|
timestamps: Float32Array;
|
|
2315
2367
|
mel: Float32Array;
|
|
2316
2368
|
chroma: Float32Array;
|
|
@@ -2326,6 +2378,8 @@ export interface WasmFrameBuffer {
|
|
|
2326
2378
|
export interface WasmStreamFramesU8 {
|
|
2327
2379
|
nFrames: number;
|
|
2328
2380
|
nMels: number;
|
|
2381
|
+
nChroma: number;
|
|
2382
|
+
featureFlags: number;
|
|
2329
2383
|
timestamps: Float32Array;
|
|
2330
2384
|
mel: Uint8Array;
|
|
2331
2385
|
chroma: Uint8Array;
|
|
@@ -2338,6 +2392,8 @@ export interface WasmStreamFramesU8 {
|
|
|
2338
2392
|
export interface WasmStreamFramesI16 {
|
|
2339
2393
|
nFrames: number;
|
|
2340
2394
|
nMels: number;
|
|
2395
|
+
nChroma: number;
|
|
2396
|
+
featureFlags: number;
|
|
2341
2397
|
timestamps: Float32Array;
|
|
2342
2398
|
mel: Int16Array;
|
|
2343
2399
|
chroma: Int16Array;
|
package/src/stream_analyzer.ts
CHANGED
|
@@ -39,6 +39,16 @@ export function streamAnalyzerConfigDefaults(): StreamConfigDefaults {
|
|
|
39
39
|
* console.log('Key:', stats.estimate.key);
|
|
40
40
|
* console.log('Chord progression:', stats.estimate.chordProgression);
|
|
41
41
|
* ```
|
|
42
|
+
*
|
|
43
|
+
* The native analyzer supports one serialized producer (`process`,
|
|
44
|
+
* `processWithOffset`, or `finalize`) concurrently with one serialized consumer
|
|
45
|
+
* (`availableFrames`, a `readFrames*` method, `stats`, `frameCount`, or
|
|
46
|
+
* `currentTime`) when a threaded WASM host shares the native instance.
|
|
47
|
+
* Publication is allocation-free release/acquire. `reset`, setters, and
|
|
48
|
+
* deletion require both roles to be stopped. A full pending ring drops the
|
|
49
|
+
* newly produced output frame while analysis totals keep advancing. Ordinary
|
|
50
|
+
* browser builds remain single-threaded unless the host explicitly provisions
|
|
51
|
+
* shared-memory worker support.
|
|
42
52
|
*/
|
|
43
53
|
export class StreamAnalyzer {
|
|
44
54
|
private analyzer: WasmStreamAnalyzer;
|
|
@@ -54,6 +64,15 @@ export class StreamAnalyzer {
|
|
|
54
64
|
'computeMagnitude is not supported because magnitude frames are not exposed by StreamAnalyzer read paths.',
|
|
55
65
|
);
|
|
56
66
|
}
|
|
67
|
+
if (
|
|
68
|
+
config.outputFormat !== undefined &&
|
|
69
|
+
(typeof config.outputFormat !== 'number' ||
|
|
70
|
+
!Number.isFinite(config.outputFormat) ||
|
|
71
|
+
!Number.isInteger(config.outputFormat) ||
|
|
72
|
+
config.outputFormat !== 0)
|
|
73
|
+
) {
|
|
74
|
+
throw new TypeError('outputFormat must be the integer 0 (Float32)');
|
|
75
|
+
}
|
|
57
76
|
const module = getSonareModule();
|
|
58
77
|
const defaults = streamAnalyzerConfigDefaults();
|
|
59
78
|
this.analyzer = new module.StreamAnalyzer(
|
|
@@ -72,6 +91,7 @@ export class StreamAnalyzer {
|
|
|
72
91
|
config.emitEveryNFrames ?? defaults.emitEveryNFrames,
|
|
73
92
|
config.magnitudeDownsample ?? defaults.magnitudeDownsample,
|
|
74
93
|
config.maxPendingFrames ?? defaults.maxPendingFrames,
|
|
94
|
+
config.maxProgressionEntries ?? defaults.maxProgressionEntries,
|
|
75
95
|
config.keyUpdateIntervalSec ?? defaults.keyUpdateIntervalSec,
|
|
76
96
|
config.bpmUpdateIntervalSec ?? defaults.bpmUpdateIntervalSec,
|
|
77
97
|
config.window ?? defaults.window,
|
|
@@ -89,7 +109,8 @@ export class StreamAnalyzer {
|
|
|
89
109
|
}
|
|
90
110
|
|
|
91
111
|
/**
|
|
92
|
-
* Process audio samples with explicit sample offset.
|
|
112
|
+
* Process audio samples with a contiguous explicit sample offset. A gap,
|
|
113
|
+
* seek, or switch from `process()` requires `reset()` first.
|
|
93
114
|
*
|
|
94
115
|
* @param samples - Audio samples (mono, float32)
|
|
95
116
|
* @param sampleOffset - Cumulative sample count at start of this chunk
|
|
@@ -99,7 +120,7 @@ export class StreamAnalyzer {
|
|
|
99
120
|
}
|
|
100
121
|
|
|
101
122
|
/**
|
|
102
|
-
*
|
|
123
|
+
* Drain any high-rate resampler tail, then zero-pad the final partial frame.
|
|
103
124
|
*/
|
|
104
125
|
finalize(): void {
|
|
105
126
|
this.analyzer.finalize();
|
|
@@ -166,6 +187,8 @@ export class StreamAnalyzer {
|
|
|
166
187
|
durationSeconds: s.durationSeconds,
|
|
167
188
|
pendingFrames: s.pendingFrames,
|
|
168
189
|
droppedOutputFrames: s.droppedOutputFrames,
|
|
190
|
+
droppedChordProgressionEntries: s.droppedChordProgressionEntries,
|
|
191
|
+
droppedBarProgressionEntries: s.droppedBarProgressionEntries,
|
|
169
192
|
estimate: {
|
|
170
193
|
bpm: s.estimate.bpm,
|
|
171
194
|
bpmConfidence: s.estimate.bpmConfidence,
|
package/src/stream_types.ts
CHANGED
|
@@ -66,6 +66,8 @@ export interface AnalyzerStats {
|
|
|
66
66
|
durationSeconds: number;
|
|
67
67
|
pendingFrames: number;
|
|
68
68
|
droppedOutputFrames: number;
|
|
69
|
+
droppedChordProgressionEntries: number;
|
|
70
|
+
droppedBarProgressionEntries: number;
|
|
69
71
|
estimate: ProgressiveEstimate;
|
|
70
72
|
}
|
|
71
73
|
|
|
@@ -76,6 +78,10 @@ export interface FrameBuffer {
|
|
|
76
78
|
nFrames: number;
|
|
77
79
|
/** Number of mel bands; flat `mel` is `[nFrames * nMels]` row-major. */
|
|
78
80
|
nMels: number;
|
|
81
|
+
/** Chroma stride: 12 when enabled, otherwise 0. */
|
|
82
|
+
nChroma: number;
|
|
83
|
+
/** MEL=1, CHROMA=2, ONSET=4, SPECTRAL=8. */
|
|
84
|
+
featureFlags: number;
|
|
79
85
|
timestamps: Float32Array;
|
|
80
86
|
/**
|
|
81
87
|
* Mel spectrogram in LINEAR power (not dB) — the raw per-frame mel energies.
|
|
@@ -115,6 +121,9 @@ export interface StreamQuantizeConfig {
|
|
|
115
121
|
export interface StreamFramesU8 {
|
|
116
122
|
nFrames: number;
|
|
117
123
|
nMels: number;
|
|
124
|
+
nChroma: number;
|
|
125
|
+
/** MEL=1, CHROMA=2, ONSET=4, SPECTRAL=8. */
|
|
126
|
+
featureFlags: number;
|
|
118
127
|
timestamps: Float32Array;
|
|
119
128
|
/** Row-major `[nFrames * nMels]` mel in dB, quantized over `[melDbMin, melDbMax]`. */
|
|
120
129
|
mel: Uint8Array;
|
|
@@ -128,6 +137,9 @@ export interface StreamFramesU8 {
|
|
|
128
137
|
export interface StreamFramesI16 {
|
|
129
138
|
nFrames: number;
|
|
130
139
|
nMels: number;
|
|
140
|
+
nChroma: number;
|
|
141
|
+
/** MEL=1, CHROMA=2, ONSET=4, SPECTRAL=8. */
|
|
142
|
+
featureFlags: number;
|
|
131
143
|
timestamps: Float32Array;
|
|
132
144
|
/** Row-major `[nFrames * nMels]` mel in dB, quantized over `[melDbMin, melDbMax]`. */
|
|
133
145
|
mel: Int16Array;
|
|
@@ -161,11 +173,14 @@ export interface StreamConfig {
|
|
|
161
173
|
computeSpectral?: boolean;
|
|
162
174
|
emitEveryNFrames?: number;
|
|
163
175
|
magnitudeDownsample?: number;
|
|
164
|
-
/** Maximum unread frames; overflow drops the
|
|
176
|
+
/** Maximum unread frames; overflow drops the newly produced frame. */
|
|
165
177
|
maxPendingFrames?: number;
|
|
178
|
+
/** Maximum retained chord and bar progression entries; overflow drops oldest. */
|
|
179
|
+
maxProgressionEntries?: number;
|
|
166
180
|
keyUpdateIntervalSec?: number;
|
|
167
181
|
bpmUpdateIntervalSec?: number;
|
|
168
182
|
window?: number;
|
|
183
|
+
/** @deprecated Must be 0 (Float32). Use readFramesU8/readFramesI16 explicitly. */
|
|
169
184
|
outputFormat?: number;
|
|
170
185
|
}
|
|
171
186
|
|
package/src/validation.ts
CHANGED
|
@@ -14,6 +14,15 @@ export interface ValidateOptions {
|
|
|
14
14
|
validate?: boolean;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Offline-analysis sample-rate bounds, mirroring the C++ core limits
|
|
19
|
+
* (`sonare::kMinAudioSampleRate` / `kMaxAudioSampleRate` in `core/audio.h`).
|
|
20
|
+
* Every guarded WASM entry point rejects the same out-of-range rates the C ABI,
|
|
21
|
+
* Node, and Python surfaces do.
|
|
22
|
+
*/
|
|
23
|
+
export const MIN_AUDIO_SAMPLE_RATE = 8000;
|
|
24
|
+
export const MAX_AUDIO_SAMPLE_RATE = 384000;
|
|
25
|
+
|
|
17
26
|
function assertNonEmptySamples(
|
|
18
27
|
fnName: string,
|
|
19
28
|
samples: ArrayLike<number>,
|
|
@@ -58,8 +67,14 @@ export function assertFiniteScalar(fnName: string, value: number, argName: strin
|
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
export function assertSampleRate(fnName: string, sampleRate: number): void {
|
|
61
|
-
if (
|
|
62
|
-
|
|
70
|
+
if (
|
|
71
|
+
!Number.isInteger(sampleRate) ||
|
|
72
|
+
sampleRate < MIN_AUDIO_SAMPLE_RATE ||
|
|
73
|
+
sampleRate > MAX_AUDIO_SAMPLE_RATE
|
|
74
|
+
) {
|
|
75
|
+
throw new RangeError(
|
|
76
|
+
`${fnName}: sampleRate out of supported range [${MIN_AUDIO_SAMPLE_RATE}, ${MAX_AUDIO_SAMPLE_RATE}]`,
|
|
77
|
+
);
|
|
63
78
|
}
|
|
64
79
|
}
|
|
65
80
|
|
|
@@ -11,11 +11,17 @@ export interface EngineClipContext {
|
|
|
11
11
|
readonly clips: Map<number, EngineClip>;
|
|
12
12
|
readonly midiClips: Map<number, EngineMidiClipSchedule>;
|
|
13
13
|
allocateClipId(): number;
|
|
14
|
-
postSync(message: SonareEngineSyncMessage): void;
|
|
14
|
+
postSync(message: SonareEngineSyncMessage, transfer?: Transferable[]): void;
|
|
15
15
|
resolveTargetId(target: string | number): number;
|
|
16
16
|
ensureTrackLane(target: string | number): number;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// Keep control-message work bounded even for long tempo-synced clips. The
|
|
20
|
+
// worklet creates a native page provider, receives these transferable PCM
|
|
21
|
+
// chunks, and only schedules the clip after all pages arrive.
|
|
22
|
+
const PREBAKED_CLIP_PAGE_THRESHOLD = 16_384;
|
|
23
|
+
const PREBAKED_CLIP_PAGE_FRAMES = 4_096;
|
|
24
|
+
|
|
19
25
|
export function addClip(
|
|
20
26
|
ctx: EngineClipContext,
|
|
21
27
|
trackId: string | number,
|
|
@@ -57,9 +63,68 @@ export function setMidiClips(
|
|
|
57
63
|
function syncClipsDelta(ctx: EngineClipContext, upserts: EngineClip[], removeIds: number[]): void {
|
|
58
64
|
const clips = Array.from(ctx.clips.values());
|
|
59
65
|
ctx.offlineEngine.setClips(clips);
|
|
66
|
+
const preparedById = new Map<number, EngineClip>();
|
|
67
|
+
for (const clip of clips) {
|
|
68
|
+
if (clip.id === undefined) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const bakedChannels = ctx.offlineEngine.prebakedClipChannels(clip.id);
|
|
72
|
+
preparedById.set(
|
|
73
|
+
clip.id,
|
|
74
|
+
bakedChannels === null
|
|
75
|
+
? clip
|
|
76
|
+
: {
|
|
77
|
+
...clip,
|
|
78
|
+
channels: bakedChannels,
|
|
79
|
+
clipOffsetSamples: 0,
|
|
80
|
+
lengthSamples: bakedChannels[0]?.length ?? 0,
|
|
81
|
+
loop: false,
|
|
82
|
+
warpMode: 'off',
|
|
83
|
+
warpAnchors: undefined,
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const inlineUpserts: EngineClip[] = [];
|
|
88
|
+
for (const clip of upserts) {
|
|
89
|
+
const prepared = clip.id === undefined ? clip : (preparedById.get(clip.id) ?? clip);
|
|
90
|
+
const channels = prepared.channels;
|
|
91
|
+
if (
|
|
92
|
+
prepared.id === undefined ||
|
|
93
|
+
prepared.warpMode !== 'off' ||
|
|
94
|
+
!channels ||
|
|
95
|
+
channels.length === 0 ||
|
|
96
|
+
channels[0].length <= PREBAKED_CLIP_PAGE_THRESHOLD
|
|
97
|
+
) {
|
|
98
|
+
inlineUpserts.push(prepared);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const numSamples = channels[0].length;
|
|
102
|
+
ctx.postSync({
|
|
103
|
+
type: 'syncClipPageProvider',
|
|
104
|
+
clipId: prepared.id,
|
|
105
|
+
clip: { ...prepared, channels: undefined, pageProvider: undefined },
|
|
106
|
+
numChannels: channels.length,
|
|
107
|
+
numSamples,
|
|
108
|
+
pageFrames: PREBAKED_CLIP_PAGE_FRAMES,
|
|
109
|
+
});
|
|
110
|
+
for (
|
|
111
|
+
let start = 0, pageIndex = 0;
|
|
112
|
+
start < numSamples;
|
|
113
|
+
start += PREBAKED_CLIP_PAGE_FRAMES, pageIndex++
|
|
114
|
+
) {
|
|
115
|
+
const page = channels.map((channel) =>
|
|
116
|
+
channel.slice(start, start + PREBAKED_CLIP_PAGE_FRAMES),
|
|
117
|
+
);
|
|
118
|
+
ctx.postSync(
|
|
119
|
+
{ type: 'syncClipPage', clipId: prepared.id, pageIndex, channels: page },
|
|
120
|
+
page.map((channel) => channel.buffer as Transferable),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
ctx.postSync({ type: 'syncClipPageCommit', clipId: prepared.id });
|
|
124
|
+
}
|
|
60
125
|
ctx.postSync({
|
|
61
126
|
type: 'syncClipsDelta',
|
|
62
|
-
upserts,
|
|
127
|
+
upserts: inlineUpserts,
|
|
63
128
|
removeIds,
|
|
64
129
|
});
|
|
65
130
|
}
|
|
@@ -70,6 +70,8 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
70
70
|
// arrays on both heaps every block, an RT-safety hazard).
|
|
71
71
|
private channelBuffers: Float32Array[];
|
|
72
72
|
private readonly liveClips = new Map<number, EngineClip>();
|
|
73
|
+
private readonly pagedClipProviders = new Map<number, number>();
|
|
74
|
+
private readonly pendingPagedClips = new Map<number, EngineClip>();
|
|
73
75
|
|
|
74
76
|
constructor(
|
|
75
77
|
options: SonareRealtimeEngineWorkletProcessorOptions = {},
|
|
@@ -208,11 +210,10 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
208
210
|
}
|
|
209
211
|
}
|
|
210
212
|
|
|
211
|
-
// Applies an out-of-band control-plane sync message
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
// of the engine's control-thread RtPublisher setters.
|
|
213
|
+
// Applies an out-of-band control-plane sync message on the AudioWorklet
|
|
214
|
+
// thread. These handlers must remain bounded: expensive clip transforms are
|
|
215
|
+
// performed on the main-thread mirror, and long pre-baked PCM arrives in
|
|
216
|
+
// small pages before the final lightweight clip schedule is committed.
|
|
216
217
|
receiveSync(message: SonareEngineSyncMessage): void {
|
|
217
218
|
if (this.closed) {
|
|
218
219
|
return;
|
|
@@ -238,6 +239,33 @@ export class SonareRealtimeEngineWorkletProcessor {
|
|
|
238
239
|
}
|
|
239
240
|
this.engine.setClips(Array.from(this.liveClips.values()));
|
|
240
241
|
break;
|
|
242
|
+
case 'syncClipPageProvider': {
|
|
243
|
+
const provider = this.engine.createClipPageProvider(
|
|
244
|
+
message.numChannels,
|
|
245
|
+
message.numSamples,
|
|
246
|
+
message.pageFrames,
|
|
247
|
+
);
|
|
248
|
+
this.pagedClipProviders.set(message.clipId, provider.id);
|
|
249
|
+
this.pendingPagedClips.set(message.clipId, message.clip);
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
case 'syncClipPage': {
|
|
253
|
+
const providerId = this.pagedClipProviders.get(message.clipId);
|
|
254
|
+
if (providerId !== undefined) {
|
|
255
|
+
this.engine.supplyClipPage(providerId, message.pageIndex, message.channels);
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
case 'syncClipPageCommit': {
|
|
260
|
+
const providerId = this.pagedClipProviders.get(message.clipId);
|
|
261
|
+
const clip = this.pendingPagedClips.get(message.clipId);
|
|
262
|
+
if (providerId !== undefined && clip) {
|
|
263
|
+
this.liveClips.set(message.clipId, { ...clip, pageProvider: providerId });
|
|
264
|
+
this.pendingPagedClips.delete(message.clipId);
|
|
265
|
+
this.engine.setClips(Array.from(this.liveClips.values()));
|
|
266
|
+
}
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
241
269
|
case 'syncMidiClips':
|
|
242
270
|
this.engine.setMidiClips(message.clips);
|
|
243
271
|
break;
|
package/src/worklet/engine.ts
CHANGED
|
@@ -787,11 +787,15 @@ export class SonareEngine {
|
|
|
787
787
|
// Posts an out-of-band control-sync message to the worklet engine processor.
|
|
788
788
|
// Sync messages use a string `type` so the worklet's message handler routes
|
|
789
789
|
// them to receiveSync() (numeric `type` is reserved for SonareEngineCommandRecord).
|
|
790
|
-
private postSync(message: SonareEngineSyncMessage): void {
|
|
790
|
+
private postSync(message: SonareEngineSyncMessage, transfer?: Transferable[]): void {
|
|
791
791
|
if (this.destroyed) {
|
|
792
792
|
return;
|
|
793
793
|
}
|
|
794
|
-
|
|
794
|
+
if (transfer && transfer.length > 0) {
|
|
795
|
+
this.realtimeNode.node.port.postMessage(message, transfer);
|
|
796
|
+
} else {
|
|
797
|
+
this.realtimeNode.node.port.postMessage(message);
|
|
798
|
+
}
|
|
795
799
|
}
|
|
796
800
|
|
|
797
801
|
// Collaborator surface handed to the mixer/routing free functions so they can
|
|
@@ -916,7 +920,7 @@ export class SonareEngine {
|
|
|
916
920
|
clips: this.clips,
|
|
917
921
|
midiClips: this.midiClips,
|
|
918
922
|
allocateClipId: () => this.nextClipId++,
|
|
919
|
-
postSync: (message) => this.postSync(message),
|
|
923
|
+
postSync: (message, transfer) => this.postSync(message, transfer),
|
|
920
924
|
ensureTrackLane: (target) => this.ensureTrackLane(target),
|
|
921
925
|
resolveTargetId: (target) => this.resolveTargetId(target),
|
|
922
926
|
};
|
package/src/worklet/guards.ts
CHANGED
|
@@ -37,6 +37,9 @@ export function isEngineSyncMessage(value: unknown): value is SonareEngineSyncMe
|
|
|
37
37
|
return (
|
|
38
38
|
value.type === 'syncClips' ||
|
|
39
39
|
value.type === 'syncClipsDelta' ||
|
|
40
|
+
value.type === 'syncClipPageProvider' ||
|
|
41
|
+
value.type === 'syncClipPage' ||
|
|
42
|
+
value.type === 'syncClipPageCommit' ||
|
|
40
43
|
value.type === 'syncMidiClips' ||
|
|
41
44
|
value.type === 'syncMarkers' ||
|
|
42
45
|
value.type === 'syncMetronome' ||
|
package/src/worklet/messages.ts
CHANGED
|
@@ -219,6 +219,30 @@ export interface SonareEngineSyncClipsDeltaMessage {
|
|
|
219
219
|
removeIds: number[];
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
/** Begins a paged, pre-baked clip transfer. PCM pages follow in FIFO order. */
|
|
223
|
+
export interface SonareEngineSyncClipPageProviderMessage {
|
|
224
|
+
type: 'syncClipPageProvider';
|
|
225
|
+
clipId: number;
|
|
226
|
+
clip: EngineClip;
|
|
227
|
+
numChannels: number;
|
|
228
|
+
numSamples: number;
|
|
229
|
+
pageFrames: number;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Supplies one bounded PCM page for a pending pre-baked clip transfer. */
|
|
233
|
+
export interface SonareEngineSyncClipPageMessage {
|
|
234
|
+
type: 'syncClipPage';
|
|
235
|
+
clipId: number;
|
|
236
|
+
pageIndex: number;
|
|
237
|
+
channels: Float32Array[];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Makes a fully supplied paged clip visible to the audio engine. */
|
|
241
|
+
export interface SonareEngineSyncClipPageCommitMessage {
|
|
242
|
+
type: 'syncClipPageCommit';
|
|
243
|
+
clipId: number;
|
|
244
|
+
}
|
|
245
|
+
|
|
222
246
|
export interface SonareEngineSyncMidiClipsMessage {
|
|
223
247
|
type: 'syncMidiClips';
|
|
224
248
|
clips: EngineMidiClipSchedule[];
|
|
@@ -433,6 +457,9 @@ export type SonareEngineInstrumentSyncMessage =
|
|
|
433
457
|
export type SonareEngineSyncMessage =
|
|
434
458
|
| SonareEngineSyncClipsMessage
|
|
435
459
|
| SonareEngineSyncClipsDeltaMessage
|
|
460
|
+
| SonareEngineSyncClipPageProviderMessage
|
|
461
|
+
| SonareEngineSyncClipPageMessage
|
|
462
|
+
| SonareEngineSyncClipPageCommitMessage
|
|
436
463
|
| SonareEngineSyncMidiClipsMessage
|
|
437
464
|
| SonareEngineSyncMarkersMessage
|
|
438
465
|
| SonareEngineSyncMetronomeMessage
|