@libraz/libsonare 1.5.2 → 1.5.4
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 +69 -725
- package/dist/index.d.ts +1155 -183
- package/dist/index.js +4924 -3935
- 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 +1141 -970
- package/dist/worklet.js.map +1 -1
- package/package.json +15 -3
- package/src/_chain_config.ts +46 -0
- package/src/audio.ts +24 -4
- package/src/codes.ts +27 -4
- package/src/effects_mastering.ts +25 -1
- package/src/effects_transform.ts +292 -46
- package/src/effects_voice_change.ts +46 -53
- package/src/feature_core.ts +316 -12
- package/src/feature_music.ts +371 -4
- 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/index.ts +130 -1
- package/src/mastering_chain.ts +305 -50
- package/src/mastering_core.ts +232 -16
- package/src/mastering_dynamics.ts +66 -10
- package/src/mastering_repair.ts +119 -7
- package/src/metering.ts +370 -83
- package/src/mixer.ts +9 -2
- package/src/mixing_oneshot.ts +25 -5
- package/src/module_state.ts +1 -1
- package/src/project_class.ts +30 -6
- package/src/project_internal.ts +12 -2
- package/src/project_types.ts +25 -2
- package/src/public_types_mastering.ts +114 -4
- package/src/public_types_spectral.ts +7 -0
- package/src/quick_analysis.ts +325 -117
- package/src/realtime_engine.ts +8 -0
- package/src/realtime_voice_changer.ts +79 -3
- package/src/sonare.js.d.ts +35 -0
- package/src/stream_analyzer.ts +20 -1
- package/src/stream_types.ts +12 -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
|
}
|
|
@@ -1,5 +1,77 @@
|
|
|
1
1
|
import { getSonareModule } from './module_state';
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
RealtimeVoiceChangerConfigInput,
|
|
4
|
+
RealtimeVoiceChangerPodConfig,
|
|
5
|
+
VoicePresetId,
|
|
6
|
+
} from './public_types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* True when `config` is a flat POD (the shape `realtimeVoiceChangerPresetConfig`
|
|
10
|
+
* returns) rather than a preset name or a nested preset. `retuneSemitones` only
|
|
11
|
+
* exists on the flat POD; the nested form spells it `retune.semitones`.
|
|
12
|
+
*/
|
|
13
|
+
function isFlatVoiceChangerPod(config: unknown): config is RealtimeVoiceChangerPodConfig {
|
|
14
|
+
return typeof config === 'object' && config !== null && 'retuneSemitones' in config;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Rewrite the flat 36-field POD into the nested shape the native config parser
|
|
19
|
+
* reads. Passing the flat POD straight to `setConfig` would leave every nested
|
|
20
|
+
* field at its default (only the three root fields are read flat), silently
|
|
21
|
+
* discarding retune/EQ/reverb/etc.
|
|
22
|
+
*/
|
|
23
|
+
function flatVoiceChangerPodToNested(pod: RealtimeVoiceChangerPodConfig): Record<string, unknown> {
|
|
24
|
+
return {
|
|
25
|
+
inputGainDb: pod.inputGainDb,
|
|
26
|
+
outputGainDb: pod.outputGainDb,
|
|
27
|
+
wetMix: pod.wetMix,
|
|
28
|
+
retune: { semitones: pod.retuneSemitones, mix: pod.retuneMix, grainSize: pod.retuneGrainSize },
|
|
29
|
+
formant: {
|
|
30
|
+
factor: pod.formantFactor,
|
|
31
|
+
amount: pod.formantAmount,
|
|
32
|
+
body: pod.formantBody,
|
|
33
|
+
brightness: pod.formantBrightness,
|
|
34
|
+
nasal: pod.formantNasal,
|
|
35
|
+
},
|
|
36
|
+
eq: {
|
|
37
|
+
highpassHz: pod.eqHighpassHz,
|
|
38
|
+
bodyDb: pod.eqBodyDb,
|
|
39
|
+
presenceDb: pod.eqPresenceDb,
|
|
40
|
+
airDb: pod.eqAirDb,
|
|
41
|
+
},
|
|
42
|
+
gate: {
|
|
43
|
+
thresholdDb: pod.gateThresholdDb,
|
|
44
|
+
attackMs: pod.gateAttackMs,
|
|
45
|
+
releaseMs: pod.gateReleaseMs,
|
|
46
|
+
rangeDb: pod.gateRangeDb,
|
|
47
|
+
},
|
|
48
|
+
compressor: {
|
|
49
|
+
thresholdDb: pod.compressorThresholdDb,
|
|
50
|
+
ratio: pod.compressorRatio,
|
|
51
|
+
attackMs: pod.compressorAttackMs,
|
|
52
|
+
releaseMs: pod.compressorReleaseMs,
|
|
53
|
+
makeupGainDb: pod.compressorMakeupGainDb,
|
|
54
|
+
},
|
|
55
|
+
deesser: {
|
|
56
|
+
frequencyHz: pod.deesserFrequencyHz,
|
|
57
|
+
thresholdDb: pod.deesserThresholdDb,
|
|
58
|
+
ratio: pod.deesserRatio,
|
|
59
|
+
rangeDb: pod.deesserRangeDb,
|
|
60
|
+
},
|
|
61
|
+
reverb: {
|
|
62
|
+
mix: pod.reverbMix,
|
|
63
|
+
timeMs: pod.reverbTimeMs,
|
|
64
|
+
damping: pod.reverbDamping,
|
|
65
|
+
seed: pod.reverbSeed,
|
|
66
|
+
},
|
|
67
|
+
limiter: {
|
|
68
|
+
ceilingDb: pod.limiterCeilingDb,
|
|
69
|
+
releaseMs: pod.limiterReleaseMs,
|
|
70
|
+
enableIspLimiter: pod.limiterEnableIspLimiter,
|
|
71
|
+
ispCeilingDbtp: pod.limiterIspCeilingDbtp,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
3
75
|
|
|
4
76
|
/**
|
|
5
77
|
* Zero-copy realtime buffer pair for {@link RealtimeVoiceChanger} mono
|
|
@@ -58,8 +130,12 @@ export class RealtimeVoiceChanger {
|
|
|
58
130
|
this.changer.reset();
|
|
59
131
|
}
|
|
60
132
|
|
|
61
|
-
setConfig(config: RealtimeVoiceChangerConfigInput): void {
|
|
62
|
-
|
|
133
|
+
setConfig(config: RealtimeVoiceChangerConfigInput | RealtimeVoiceChangerPodConfig): void {
|
|
134
|
+
// A flat POD (from realtimeVoiceChangerPresetConfig) is rewritten to the
|
|
135
|
+
// nested shape the native parser reads; a preset name or nested preset is
|
|
136
|
+
// passed through unchanged.
|
|
137
|
+
const resolved = isFlatVoiceChangerPod(config) ? flatVoiceChangerPodToNested(config) : config;
|
|
138
|
+
this.changer.setConfig(resolved as Record<string, unknown> | string);
|
|
63
139
|
}
|
|
64
140
|
|
|
65
141
|
configJson(): string {
|
package/src/sonare.js.d.ts
CHANGED
|
@@ -470,8 +470,16 @@ export interface WasmMasteringResult {
|
|
|
470
470
|
latencySamples?: number;
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
+
export interface WasmStageGainReduction {
|
|
474
|
+
stage: string;
|
|
475
|
+
gainReductionDb: number;
|
|
476
|
+
}
|
|
477
|
+
|
|
473
478
|
export interface WasmMasteringChainResult extends WasmMasteringResult {
|
|
474
479
|
stages: string[];
|
|
480
|
+
outputTruePeakDbtp: number;
|
|
481
|
+
outputLra: number;
|
|
482
|
+
stageGainReductions: WasmStageGainReduction[];
|
|
475
483
|
}
|
|
476
484
|
|
|
477
485
|
export interface WasmMasteringStereoChainResult {
|
|
@@ -482,6 +490,9 @@ export interface WasmMasteringStereoChainResult {
|
|
|
482
490
|
outputLufs: number;
|
|
483
491
|
appliedGainDb: number;
|
|
484
492
|
stages: string[];
|
|
493
|
+
outputTruePeakDbtp: number;
|
|
494
|
+
outputLra: number;
|
|
495
|
+
stageGainReductions: WasmStageGainReduction[];
|
|
485
496
|
}
|
|
486
497
|
|
|
487
498
|
export interface WasmMasteringStereoResult {
|
|
@@ -734,6 +745,10 @@ export interface WasmEngineTransportState {
|
|
|
734
745
|
barCount: number;
|
|
735
746
|
/** Time signature in effect at the current PPQ. */
|
|
736
747
|
timeSignature: { numerator: number; denominator: number; confidence: number };
|
|
748
|
+
/** One-based beat within the current bar (`barCount` is zero-based). */
|
|
749
|
+
beat: number;
|
|
750
|
+
/** Fractional position within the current beat, in [0, 1). */
|
|
751
|
+
beatFraction: number;
|
|
737
752
|
}
|
|
738
753
|
|
|
739
754
|
export interface WasmEngineBounceOptions {
|
|
@@ -848,6 +863,7 @@ export interface WasmRealtimeEngine {
|
|
|
848
863
|
graphNodeCount: () => number;
|
|
849
864
|
graphConnectionCount: () => number;
|
|
850
865
|
setClips: (clips: WasmEngineClip[]) => void;
|
|
866
|
+
prebakedClipChannels: (clipId: number) => Float32Array[] | null;
|
|
851
867
|
clipCount: () => number;
|
|
852
868
|
setTrackLanes: (lanes: Array<number | WasmEngineTrackLane>) => void;
|
|
853
869
|
setLaneSidechain: (trackId: number, insertIndex: number, sourceTrackId: number) => void;
|
|
@@ -1412,12 +1428,25 @@ export interface SonareModule {
|
|
|
1412
1428
|
offsetSample: number,
|
|
1413
1429
|
stretchRatio: number,
|
|
1414
1430
|
) => Float32Array;
|
|
1431
|
+
noteMove: (
|
|
1432
|
+
samples: Float32Array,
|
|
1433
|
+
sampleRate: number,
|
|
1434
|
+
onsetSample: number,
|
|
1435
|
+
offsetSample: number,
|
|
1436
|
+
targetOnsetSample: number,
|
|
1437
|
+
) => Float32Array;
|
|
1415
1438
|
voiceChange: (
|
|
1416
1439
|
samples: Float32Array,
|
|
1417
1440
|
sampleRate: number,
|
|
1418
1441
|
pitchSemitones: number,
|
|
1419
1442
|
formantFactor: number,
|
|
1420
1443
|
) => Float32Array;
|
|
1444
|
+
voiceChangeRealtime: (
|
|
1445
|
+
samples: Float32Array,
|
|
1446
|
+
sampleRate: number,
|
|
1447
|
+
preset: string,
|
|
1448
|
+
channels: number,
|
|
1449
|
+
) => Float32Array;
|
|
1421
1450
|
normalize: (samples: Float32Array, sampleRate: number, targetDb: number) => Float32Array;
|
|
1422
1451
|
mastering: (
|
|
1423
1452
|
samples: Float32Array,
|
|
@@ -2336,6 +2365,8 @@ export interface WasmFrameBuffer {
|
|
|
2336
2365
|
nFrames: number;
|
|
2337
2366
|
/** Number of mel bands; flat `mel` is `[nFrames * nMels]` row-major. */
|
|
2338
2367
|
nMels: number;
|
|
2368
|
+
nChroma: number;
|
|
2369
|
+
featureFlags: number;
|
|
2339
2370
|
timestamps: Float32Array;
|
|
2340
2371
|
mel: Float32Array;
|
|
2341
2372
|
chroma: Float32Array;
|
|
@@ -2351,6 +2382,8 @@ export interface WasmFrameBuffer {
|
|
|
2351
2382
|
export interface WasmStreamFramesU8 {
|
|
2352
2383
|
nFrames: number;
|
|
2353
2384
|
nMels: number;
|
|
2385
|
+
nChroma: number;
|
|
2386
|
+
featureFlags: number;
|
|
2354
2387
|
timestamps: Float32Array;
|
|
2355
2388
|
mel: Uint8Array;
|
|
2356
2389
|
chroma: Uint8Array;
|
|
@@ -2363,6 +2396,8 @@ export interface WasmStreamFramesU8 {
|
|
|
2363
2396
|
export interface WasmStreamFramesI16 {
|
|
2364
2397
|
nFrames: number;
|
|
2365
2398
|
nMels: number;
|
|
2399
|
+
nChroma: number;
|
|
2400
|
+
featureFlags: number;
|
|
2366
2401
|
timestamps: Float32Array;
|
|
2367
2402
|
mel: Int16Array;
|
|
2368
2403
|
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(
|
|
@@ -101,7 +120,7 @@ export class StreamAnalyzer {
|
|
|
101
120
|
}
|
|
102
121
|
|
|
103
122
|
/**
|
|
104
|
-
*
|
|
123
|
+
* Drain any high-rate resampler tail, then zero-pad the final partial frame.
|
|
105
124
|
*/
|
|
106
125
|
finalize(): void {
|
|
107
126
|
this.analyzer.finalize();
|
package/src/stream_types.ts
CHANGED
|
@@ -78,6 +78,10 @@ export interface FrameBuffer {
|
|
|
78
78
|
nFrames: number;
|
|
79
79
|
/** Number of mel bands; flat `mel` is `[nFrames * nMels]` row-major. */
|
|
80
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;
|
|
81
85
|
timestamps: Float32Array;
|
|
82
86
|
/**
|
|
83
87
|
* Mel spectrogram in LINEAR power (not dB) — the raw per-frame mel energies.
|
|
@@ -117,6 +121,9 @@ export interface StreamQuantizeConfig {
|
|
|
117
121
|
export interface StreamFramesU8 {
|
|
118
122
|
nFrames: number;
|
|
119
123
|
nMels: number;
|
|
124
|
+
nChroma: number;
|
|
125
|
+
/** MEL=1, CHROMA=2, ONSET=4, SPECTRAL=8. */
|
|
126
|
+
featureFlags: number;
|
|
120
127
|
timestamps: Float32Array;
|
|
121
128
|
/** Row-major `[nFrames * nMels]` mel in dB, quantized over `[melDbMin, melDbMax]`. */
|
|
122
129
|
mel: Uint8Array;
|
|
@@ -130,6 +137,9 @@ export interface StreamFramesU8 {
|
|
|
130
137
|
export interface StreamFramesI16 {
|
|
131
138
|
nFrames: number;
|
|
132
139
|
nMels: number;
|
|
140
|
+
nChroma: number;
|
|
141
|
+
/** MEL=1, CHROMA=2, ONSET=4, SPECTRAL=8. */
|
|
142
|
+
featureFlags: number;
|
|
133
143
|
timestamps: Float32Array;
|
|
134
144
|
/** Row-major `[nFrames * nMels]` mel in dB, quantized over `[melDbMin, melDbMax]`. */
|
|
135
145
|
mel: Int16Array;
|
|
@@ -163,13 +173,14 @@ export interface StreamConfig {
|
|
|
163
173
|
computeSpectral?: boolean;
|
|
164
174
|
emitEveryNFrames?: number;
|
|
165
175
|
magnitudeDownsample?: number;
|
|
166
|
-
/** Maximum unread frames; overflow drops the
|
|
176
|
+
/** Maximum unread frames; overflow drops the newly produced frame. */
|
|
167
177
|
maxPendingFrames?: number;
|
|
168
178
|
/** Maximum retained chord and bar progression entries; overflow drops oldest. */
|
|
169
179
|
maxProgressionEntries?: number;
|
|
170
180
|
keyUpdateIntervalSec?: number;
|
|
171
181
|
bpmUpdateIntervalSec?: number;
|
|
172
182
|
window?: number;
|
|
183
|
+
/** @deprecated Must be 0 (Float32). Use readFramesU8/readFramesI16 explicitly. */
|
|
173
184
|
outputFormat?: number;
|
|
174
185
|
}
|
|
175
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
|