@libraz/libsonare 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +403 -70
- package/dist/index.js.map +1 -1
- package/dist/sonare-rt-module.js +2 -2
- package/dist/sonare-rt.js +2 -2
- package/dist/sonare-rt.wasm +0 -0
- package/dist/sonare.js +2 -2
- package/dist/sonare.wasm +0 -0
- package/dist/worklet.d.ts +907 -144
- package/dist/worklet.js +1803 -207
- package/dist/worklet.js.map +1 -1
- package/package.json +1 -1
- package/src/codes.ts +6 -1
- package/src/effects_mastering.ts +103 -1
- package/src/feature_music.ts +18 -4
- package/src/feature_spectral.ts +7 -1
- package/src/index.ts +27 -1
- package/src/mixer.ts +9 -0
- package/src/module_state.ts +82 -17
- package/src/opfs_clip_pages.ts +43 -9
- package/src/project.ts +74 -0
- package/src/public_types.ts +52 -0
- package/src/realtime_engine.ts +313 -109
- package/src/sonare.js.d.ts +140 -0
- package/src/stream_types.ts +7 -0
- package/src/validation.ts +7 -0
- package/src/web_midi.ts +15 -11
- package/src/worklet/audio_types.ts +2 -0
- package/src/worklet/guards.ts +146 -0
- package/src/worklet/messages.ts +461 -0
- package/src/worklet/protocol.ts +767 -0
- package/src/worklet.ts +1659 -888
package/package.json
CHANGED
package/src/codes.ts
CHANGED
|
@@ -52,5 +52,10 @@ export function meterTapCode(tap: MeterTap | number): number {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export function sendTimingCode(timing: SendTiming | number): number {
|
|
55
|
-
|
|
55
|
+
// Mirrors SonareSendTiming: post-fader is 0 (so an omitted/zeroed value is
|
|
56
|
+
// post-fader), pre-fader is 1. A raw number is passed through as the C ABI int.
|
|
57
|
+
if (typeof timing === 'number') {
|
|
58
|
+
return timing;
|
|
59
|
+
}
|
|
60
|
+
return timing === 'preFader' ? 1 : 0;
|
|
56
61
|
}
|
package/src/effects_mastering.ts
CHANGED
|
@@ -16,13 +16,15 @@ import type {
|
|
|
16
16
|
PairProcessor,
|
|
17
17
|
RealtimeVoiceChangerConfigInput,
|
|
18
18
|
SoloProcessor,
|
|
19
|
+
SpectralEditOptions,
|
|
20
|
+
SpectralRegionOp,
|
|
19
21
|
StereoAnalysis,
|
|
20
22
|
StreamingPlatform,
|
|
21
23
|
} from './public_types';
|
|
22
24
|
import type { ProgressCallback } from './sonare.js';
|
|
23
25
|
import { RealtimeVoiceChanger } from './streaming_mixing';
|
|
24
26
|
import type { ValidateOptions } from './validation';
|
|
25
|
-
import { assertSamples } from './validation';
|
|
27
|
+
import { assertSampleRate, assertSamples } from './validation';
|
|
26
28
|
|
|
27
29
|
function requireModule() {
|
|
28
30
|
return getSonareModule();
|
|
@@ -339,6 +341,31 @@ export function normalize(
|
|
|
339
341
|
return requireModule().normalize(samples, sampleRate, targetDb);
|
|
340
342
|
}
|
|
341
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Apply region-based spectral edits (gain/attenuate/mute/heal) to mono audio.
|
|
346
|
+
*
|
|
347
|
+
* Each op is a time x frequency rectangle applied in array order over a single
|
|
348
|
+
* STFT buffer, so a later op observes the result of earlier ops. The output has
|
|
349
|
+
* the same length and sample rate as the input; an empty `ops` list is an
|
|
350
|
+
* identity transform (within the iSTFT's own tolerance).
|
|
351
|
+
*
|
|
352
|
+
* @param samples - Audio samples (mono, float32)
|
|
353
|
+
* @param sampleRate - Sample rate in Hz
|
|
354
|
+
* @param ops - Region edit ops applied in order ({@link SpectralRegionOp})
|
|
355
|
+
* @param options - STFT + heal configuration ({@link SpectralEditOptions})
|
|
356
|
+
* @returns Edited audio
|
|
357
|
+
*/
|
|
358
|
+
export function spectralEdit(
|
|
359
|
+
samples: Float32Array,
|
|
360
|
+
sampleRate: number,
|
|
361
|
+
ops: SpectralRegionOp[] = [],
|
|
362
|
+
options: SpectralEditOptions & ValidateOptions = {},
|
|
363
|
+
): Float32Array {
|
|
364
|
+
assertSamples('spectralEdit', samples, options.validate !== false);
|
|
365
|
+
assertSampleRate('spectralEdit', sampleRate);
|
|
366
|
+
return requireModule().spectralEdit(samples, sampleRate, ops, options as Record<string, unknown>);
|
|
367
|
+
}
|
|
368
|
+
|
|
342
369
|
/**
|
|
343
370
|
* Apply mastering loudness normalization with a true-peak ceiling.
|
|
344
371
|
*
|
|
@@ -392,6 +419,81 @@ export function masteringInsertParamNames(name: string): string[] {
|
|
|
392
419
|
).masteringInsertParamNames(name);
|
|
393
420
|
}
|
|
394
421
|
|
|
422
|
+
/** One realtime-automatable parameter of an insert processor. */
|
|
423
|
+
export interface MasteringInsertParamInfo {
|
|
424
|
+
/** JSON-key parameter name, as used in scene insert params. */
|
|
425
|
+
name: string;
|
|
426
|
+
/** Integer param id for realtime automation lanes / MIDI-CC binding. */
|
|
427
|
+
id: number;
|
|
428
|
+
/** Whether the param can be changed live from the audio thread. */
|
|
429
|
+
rtSafe: boolean;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Returns the realtime-automatable parameter descriptors for an insert / FX
|
|
434
|
+
* processor: each entry maps a JSON-key parameter name to the integer id used by
|
|
435
|
+
* realtime automation and reports whether it is realtime-safe. Unlike
|
|
436
|
+
* {@link masteringInsertParamNames} (every construction key), this lists only the
|
|
437
|
+
* realtime-controllable subset — the keys accepted by
|
|
438
|
+
* {@link RealtimeEngine.setTrackStripInsertParamByName}. Returns an empty array
|
|
439
|
+
* for an unknown name or a processor with no automatable parameters.
|
|
440
|
+
*
|
|
441
|
+
* @param name - Insert processor name (see {@link masteringInsertNames}).
|
|
442
|
+
*/
|
|
443
|
+
export function masteringInsertParamInfo(name: string): MasteringInsertParamInfo[] {
|
|
444
|
+
const json = (
|
|
445
|
+
requireModule() as unknown as { masteringInsertParamInfo: (name: string) => string }
|
|
446
|
+
).masteringInsertParamInfo(name);
|
|
447
|
+
return JSON.parse(json) as MasteringInsertParamInfo[];
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* How a processor handles a buffer with more than two channels (a surround
|
|
452
|
+
* bed). "multichannel" processes every plane in one call; "stereoPairOnly"
|
|
453
|
+
* operates on the front L/R pair and passes any surround planes through dry.
|
|
454
|
+
* "perChannel"/"passthrough" are reserved and unused by the current catalog.
|
|
455
|
+
*/
|
|
456
|
+
export type MasteringChannelPolicy =
|
|
457
|
+
| 'multichannel'
|
|
458
|
+
| 'stereoPairOnly'
|
|
459
|
+
| 'perChannel'
|
|
460
|
+
| 'passthrough';
|
|
461
|
+
|
|
462
|
+
/** One processor's realtime/offline/pair classification in the catalog. */
|
|
463
|
+
export interface MasteringProcessorCatalogEntry {
|
|
464
|
+
/** Processor id (the name used for scene inserts / named processors). */
|
|
465
|
+
id: string;
|
|
466
|
+
/**
|
|
467
|
+
* Primary classification, by precedence pair > realtime > offline: "pair" for
|
|
468
|
+
* two-input match.* processors, "realtime" for ids that build as a realtime
|
|
469
|
+
* scene insert, "offline" for whole-file-only processors.
|
|
470
|
+
*/
|
|
471
|
+
kind: 'realtime' | 'offline' | 'pair';
|
|
472
|
+
/** True exactly for ids that always succeed as a realtime scene insert. */
|
|
473
|
+
realtimeInsertable: boolean;
|
|
474
|
+
/** True for processors with no mono implementation (stereo-only). */
|
|
475
|
+
stereoOnly: boolean;
|
|
476
|
+
/**
|
|
477
|
+
* How the mixer wraps the processor on a >2-channel (surround) bus insert:
|
|
478
|
+
* "multichannel" (one full-buffer call) or "stereoPairOnly" (front L/R pair,
|
|
479
|
+
* surround planes passed through dry).
|
|
480
|
+
*/
|
|
481
|
+
channelPolicy: MasteringChannelPolicy;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Returns the machine-readable classification catalog for every named processor
|
|
486
|
+
* id, merging the offline registry, the realtime insert factory, and the pair
|
|
487
|
+
* registry. Lets a host filter a processor picker by realtime insertability
|
|
488
|
+
* instead of offering ids the realtime strip would reject.
|
|
489
|
+
*/
|
|
490
|
+
export function masteringProcessorCatalog(): MasteringProcessorCatalogEntry[] {
|
|
491
|
+
const json = (
|
|
492
|
+
requireModule() as unknown as { masteringProcessorCatalog: () => string }
|
|
493
|
+
).masteringProcessorCatalog();
|
|
494
|
+
return JSON.parse(json) as MasteringProcessorCatalogEntry[];
|
|
495
|
+
}
|
|
496
|
+
|
|
395
497
|
export function masteringPairProcessorNames(): PairProcessor[] {
|
|
396
498
|
return requireModule().masteringPairProcessorNames() as PairProcessor[];
|
|
397
499
|
}
|
package/src/feature_music.ts
CHANGED
|
@@ -257,11 +257,21 @@ export function analyzeMelody(
|
|
|
257
257
|
const fmin = options.fmin ?? 65.0;
|
|
258
258
|
const fmax = options.fmax ?? 2093.0;
|
|
259
259
|
validateFrequencyBounds('analyzeMelody', fmin, fmax);
|
|
260
|
+
// The melody tracker's fmin is a YIN pitch floor: 0 is meaningless, and the
|
|
261
|
+
// flat C ABI (sonare_analyze_melody) rejects it. validateFrequencyBounds only
|
|
262
|
+
// guards fmin >= 0, so enforce strict positivity here for parity.
|
|
263
|
+
if (fmin <= 0) {
|
|
264
|
+
throw new RangeError('analyzeMelody: fmin must be positive');
|
|
265
|
+
}
|
|
260
266
|
validatePositiveIntegers('analyzeMelody', {
|
|
261
267
|
frameLength: options.frameLength ?? 2048,
|
|
262
268
|
hopLength: options.hopLength ?? 256,
|
|
263
269
|
});
|
|
264
|
-
|
|
270
|
+
const threshold = options.threshold ?? 0.1;
|
|
271
|
+
assertFiniteScalar('analyzeMelody', threshold, 'threshold');
|
|
272
|
+
if (threshold <= 0) {
|
|
273
|
+
throw new RangeError('analyzeMelody: threshold must be positive');
|
|
274
|
+
}
|
|
265
275
|
return requireModule().analyzeMelody(
|
|
266
276
|
samples,
|
|
267
277
|
sampleRate,
|
|
@@ -371,7 +381,9 @@ export function tempogramRatio(
|
|
|
371
381
|
* Measure loudness (EBU R128 / ITU-R BS.1770).
|
|
372
382
|
*
|
|
373
383
|
* @param samples - Audio samples (mono, float32)
|
|
374
|
-
* @param sampleRate - Sample rate in Hz
|
|
384
|
+
* @param sampleRate - Sample rate in Hz. The default (22050) is non-standard for
|
|
385
|
+
* audio; pass the buffer's actual rate, as K-weighting is sample-rate
|
|
386
|
+
* dependent and a wrong rate yields wrong loudness.
|
|
375
387
|
* @returns Loudness measurement result
|
|
376
388
|
*/
|
|
377
389
|
export function lufs(
|
|
@@ -388,7 +400,8 @@ export function lufs(
|
|
|
388
400
|
* Compute the momentary loudness (LUFS) over time.
|
|
389
401
|
*
|
|
390
402
|
* @param samples - Audio samples (mono, float32)
|
|
391
|
-
* @param sampleRate - Sample rate in Hz
|
|
403
|
+
* @param sampleRate - Sample rate in Hz. The default (22050) is non-standard and
|
|
404
|
+
* K-weighting is sample-rate dependent; pass the buffer's actual rate.
|
|
392
405
|
* @returns Momentary LUFS values over time
|
|
393
406
|
*/
|
|
394
407
|
export function momentaryLufs(
|
|
@@ -405,7 +418,8 @@ export function momentaryLufs(
|
|
|
405
418
|
* Compute the short-term loudness (LUFS) over time.
|
|
406
419
|
*
|
|
407
420
|
* @param samples - Audio samples (mono, float32)
|
|
408
|
-
* @param sampleRate - Sample rate in Hz
|
|
421
|
+
* @param sampleRate - Sample rate in Hz. The default (22050) is non-standard and
|
|
422
|
+
* K-weighting is sample-rate dependent; pass the buffer's actual rate.
|
|
409
423
|
* @returns Short-term LUFS values over time
|
|
410
424
|
*/
|
|
411
425
|
export function shortTermLufs(
|
package/src/feature_spectral.ts
CHANGED
|
@@ -218,6 +218,10 @@ export function hpssWithResidual(
|
|
|
218
218
|
* Channel-weighted multichannel integrated loudness + LRA (ITU-R BS.1770 /
|
|
219
219
|
* EBU R128) from an interleaved buffer of `frames * channels` samples. The
|
|
220
220
|
* per-channel frame count is derived from the buffer length and `channels`.
|
|
221
|
+
*
|
|
222
|
+
* Pass the buffer's actual `sampleRate`: the default (22050) is non-standard for
|
|
223
|
+
* audio, and K-weighting is sample-rate dependent, so a wrong rate yields wrong
|
|
224
|
+
* loudness.
|
|
221
225
|
*/
|
|
222
226
|
export function lufsInterleaved(
|
|
223
227
|
samples: Float32Array,
|
|
@@ -231,7 +235,9 @@ export function lufsInterleaved(
|
|
|
231
235
|
}
|
|
232
236
|
|
|
233
237
|
/**
|
|
234
|
-
* Standards-compliant EBU R128 loudness range (LRA) in LU.
|
|
238
|
+
* Standards-compliant EBU R128 loudness range (LRA) in LU. Pass the buffer's
|
|
239
|
+
* actual `sampleRate`: the default (22050) is non-standard and K-weighting is
|
|
240
|
+
* sample-rate dependent.
|
|
235
241
|
*/
|
|
236
242
|
export function ebur128LoudnessRange(samples: Float32Array, sampleRate = 22050): number {
|
|
237
243
|
return requireModule().ebur128LoudnessRange(samples, sampleRate);
|
package/src/index.ts
CHANGED
|
@@ -43,6 +43,9 @@ export type {
|
|
|
43
43
|
DereverbClassicalOptions,
|
|
44
44
|
DynamicsResult,
|
|
45
45
|
GateOptions,
|
|
46
|
+
MasteringChannelPolicy,
|
|
47
|
+
MasteringInsertParamInfo,
|
|
48
|
+
MasteringProcessorCatalogEntry,
|
|
46
49
|
TransientShaperOptions,
|
|
47
50
|
TrimSilenceMode,
|
|
48
51
|
TrimSilenceOptions,
|
|
@@ -67,6 +70,7 @@ export {
|
|
|
67
70
|
masteringDynamicsGate,
|
|
68
71
|
masteringDynamicsTransientShaper,
|
|
69
72
|
masteringInsertNames,
|
|
73
|
+
masteringInsertParamInfo,
|
|
70
74
|
masteringInsertParamNames,
|
|
71
75
|
masteringPairAnalysisNames,
|
|
72
76
|
masteringPairAnalyze,
|
|
@@ -74,6 +78,7 @@ export {
|
|
|
74
78
|
masteringPairProcessorNames,
|
|
75
79
|
masteringPresetNames,
|
|
76
80
|
masteringProcess,
|
|
81
|
+
masteringProcessorCatalog,
|
|
77
82
|
masteringProcessorNames,
|
|
78
83
|
masteringProcessStereo,
|
|
79
84
|
masteringRepairDeclick,
|
|
@@ -95,6 +100,7 @@ export {
|
|
|
95
100
|
pitchCorrectToMidi,
|
|
96
101
|
pitchCorrectToMidiTimevarying,
|
|
97
102
|
pitchShift,
|
|
103
|
+
spectralEdit,
|
|
98
104
|
timeStretch,
|
|
99
105
|
voiceChange,
|
|
100
106
|
voiceChangeRealtime,
|
|
@@ -245,6 +251,7 @@ export type {
|
|
|
245
251
|
ProjectLoopMode,
|
|
246
252
|
ProjectLoopRecordingDesc,
|
|
247
253
|
ProjectLoopRecordingResult,
|
|
254
|
+
ProjectMarker,
|
|
248
255
|
ProjectMidiClipResult,
|
|
249
256
|
ProjectMidiEvent,
|
|
250
257
|
ProjectNotePairValidation,
|
|
@@ -268,6 +275,7 @@ export type {
|
|
|
268
275
|
} from './project';
|
|
269
276
|
export {
|
|
270
277
|
EXPECTED_PROJECT_ABI_VERSION,
|
|
278
|
+
MarkerKind,
|
|
271
279
|
Project,
|
|
272
280
|
projectAbiVersion,
|
|
273
281
|
SYNTH_BODY_TYPES,
|
|
@@ -348,6 +356,10 @@ export type {
|
|
|
348
356
|
Section,
|
|
349
357
|
SendTiming,
|
|
350
358
|
SoloProcessor,
|
|
359
|
+
SpectralEditMode,
|
|
360
|
+
SpectralEditOptions,
|
|
361
|
+
SpectralEditWindow,
|
|
362
|
+
SpectralRegionOp,
|
|
351
363
|
StereoAnalysis,
|
|
352
364
|
StftPowerResult,
|
|
353
365
|
StftResult,
|
|
@@ -401,6 +413,7 @@ export type {
|
|
|
401
413
|
EngineAutomationPoint,
|
|
402
414
|
EngineBounceOptions,
|
|
403
415
|
EngineBounceResult,
|
|
416
|
+
EngineBus,
|
|
404
417
|
EngineCapabilities,
|
|
405
418
|
EngineCaptureStatus,
|
|
406
419
|
EngineClip,
|
|
@@ -409,9 +422,17 @@ export type {
|
|
|
409
422
|
EngineGraphSpec,
|
|
410
423
|
EngineMarker,
|
|
411
424
|
EngineMeterTelemetry,
|
|
425
|
+
EngineMeterTelemetryWide,
|
|
412
426
|
EngineMetronomeConfig,
|
|
427
|
+
EngineMidiClipSchedule,
|
|
428
|
+
EngineMidiEvent,
|
|
413
429
|
EngineParameterInfo,
|
|
430
|
+
EngineScopeTelemetry,
|
|
414
431
|
EngineTelemetry,
|
|
432
|
+
EngineTempoSegment,
|
|
433
|
+
EngineTimeSignatureSegment,
|
|
434
|
+
EngineTrackLane,
|
|
435
|
+
EngineTrackSend,
|
|
415
436
|
EngineTransportState,
|
|
416
437
|
MidiCcBindOptions,
|
|
417
438
|
} from './realtime_engine';
|
|
@@ -484,6 +505,11 @@ let initPromise: Promise<void> | null = null;
|
|
|
484
505
|
*/
|
|
485
506
|
export async function init(options?: {
|
|
486
507
|
locateFile?: (path: string, prefix: string) => string;
|
|
508
|
+
wasmBinary?: ArrayBuffer | Uint8Array;
|
|
509
|
+
moduleFactory?: (options?: {
|
|
510
|
+
locateFile?: (path: string, prefix: string) => string;
|
|
511
|
+
wasmBinary?: ArrayBuffer | Uint8Array;
|
|
512
|
+
}) => Promise<SonareModule>;
|
|
487
513
|
}): Promise<void> {
|
|
488
514
|
if (module) {
|
|
489
515
|
return;
|
|
@@ -495,7 +521,7 @@ export async function init(options?: {
|
|
|
495
521
|
|
|
496
522
|
initPromise = (async () => {
|
|
497
523
|
try {
|
|
498
|
-
const createModule = (await import('./sonare.js')).default;
|
|
524
|
+
const createModule = options?.moduleFactory ?? (await import('./sonare.js')).default;
|
|
499
525
|
module = await createModule(options);
|
|
500
526
|
setSonareModule(module);
|
|
501
527
|
} catch (error) {
|
package/src/mixer.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
PanLaw,
|
|
16
16
|
PanMode,
|
|
17
17
|
SendTiming,
|
|
18
|
+
SurroundPan,
|
|
18
19
|
} from './public_types';
|
|
19
20
|
|
|
20
21
|
export interface MixerRealtimeBuffer {
|
|
@@ -348,6 +349,14 @@ export class Mixer {
|
|
|
348
349
|
this.mixer.setDualPan(stripIndex, leftPan, rightPan);
|
|
349
350
|
}
|
|
350
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Set the strip's surround pan position, used when it feeds a >2-channel bus.
|
|
354
|
+
* Stored on the scene; inert until the surround DSP path applies it.
|
|
355
|
+
*/
|
|
356
|
+
setSurroundPan(stripIndex: number, pan: SurroundPan): void {
|
|
357
|
+
this.mixer.setSurroundPan(stripIndex, pan);
|
|
358
|
+
}
|
|
359
|
+
|
|
351
360
|
/**
|
|
352
361
|
* Add a send to a strip after construction.
|
|
353
362
|
*
|
package/src/module_state.ts
CHANGED
|
@@ -70,6 +70,7 @@ function makeSonareError(raw: SonareModule, thrown: number): SonareError {
|
|
|
70
70
|
*/
|
|
71
71
|
function wrapModuleErrors(raw: SonareModule): SonareModule {
|
|
72
72
|
const cache = new Map<PropertyKey, unknown>();
|
|
73
|
+
const objectCache = new WeakMap<object, unknown>();
|
|
73
74
|
const convert = (error: unknown): never => {
|
|
74
75
|
const ptr = nativeExceptionPtr(error);
|
|
75
76
|
if (ptr !== null) {
|
|
@@ -77,6 +78,86 @@ function wrapModuleErrors(raw: SonareModule): SonareModule {
|
|
|
77
78
|
}
|
|
78
79
|
throw error;
|
|
79
80
|
};
|
|
81
|
+
|
|
82
|
+
const wrapNativeObject = (value: unknown): unknown => {
|
|
83
|
+
if (value === null || typeof value !== 'object') {
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer || value instanceof Promise) {
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
const objectValue = value as object;
|
|
90
|
+
const cached = objectCache.get(objectValue);
|
|
91
|
+
if (cached) {
|
|
92
|
+
return cached;
|
|
93
|
+
}
|
|
94
|
+
const methodCache = new Map<PropertyKey, unknown>();
|
|
95
|
+
const wrapped = new Proxy(objectValue, {
|
|
96
|
+
get(target, prop, receiver) {
|
|
97
|
+
const member = Reflect.get(target, prop, receiver);
|
|
98
|
+
if (typeof member !== 'function') {
|
|
99
|
+
return member;
|
|
100
|
+
}
|
|
101
|
+
const cachedMethod = methodCache.get(prop);
|
|
102
|
+
if (cachedMethod) {
|
|
103
|
+
return cachedMethod;
|
|
104
|
+
}
|
|
105
|
+
const method = member as (...a: unknown[]) => unknown;
|
|
106
|
+
const wrappedMethod = (...args: unknown[]) => {
|
|
107
|
+
try {
|
|
108
|
+
return wrapNativeObject(Reflect.apply(method, target, args));
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return convert(error);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
methodCache.set(prop, wrappedMethod);
|
|
114
|
+
return wrappedMethod;
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
objectCache.set(objectValue, wrapped);
|
|
118
|
+
return wrapped;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const wrapFunction = (value: (...a: unknown[]) => unknown): unknown => {
|
|
122
|
+
const fnCache = new Map<PropertyKey, unknown>();
|
|
123
|
+
return new Proxy(value, {
|
|
124
|
+
get(target, prop, receiver) {
|
|
125
|
+
const member = Reflect.get(target, prop, receiver);
|
|
126
|
+
if (typeof member !== 'function') {
|
|
127
|
+
return member;
|
|
128
|
+
}
|
|
129
|
+
const cachedMember = fnCache.get(prop);
|
|
130
|
+
if (cachedMember) {
|
|
131
|
+
return cachedMember;
|
|
132
|
+
}
|
|
133
|
+
const fn = member as (...a: unknown[]) => unknown;
|
|
134
|
+
const wrappedMember = (...args: unknown[]) => {
|
|
135
|
+
try {
|
|
136
|
+
return wrapNativeObject(Reflect.apply(fn, target, args));
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return convert(error);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
fnCache.set(prop, wrappedMember);
|
|
142
|
+
return wrappedMember;
|
|
143
|
+
},
|
|
144
|
+
apply(t, thisArg, args) {
|
|
145
|
+
try {
|
|
146
|
+
return wrapNativeObject(Reflect.apply(t, thisArg, args as unknown[]));
|
|
147
|
+
} catch (error) {
|
|
148
|
+
return convert(error);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
construct(t, args, newTarget) {
|
|
152
|
+
try {
|
|
153
|
+
return wrapNativeObject(Reflect.construct(t, args as unknown[], newTarget));
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return convert(error) as object;
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
|
|
80
161
|
return new Proxy(raw, {
|
|
81
162
|
get(target, prop, receiver) {
|
|
82
163
|
const value = Reflect.get(target, prop, receiver);
|
|
@@ -90,23 +171,7 @@ function wrapModuleErrors(raw: SonareModule): SonareModule {
|
|
|
90
171
|
// Wrap as a Proxy (not a plain function) so embind class constructors
|
|
91
172
|
// invoked via `new module.Foo(...)` keep their `[[Construct]]` behaviour
|
|
92
173
|
// and prototype while still converting thrown native pointers.
|
|
93
|
-
const
|
|
94
|
-
const wrapped = new Proxy(fn, {
|
|
95
|
-
apply(t, thisArg, args) {
|
|
96
|
-
try {
|
|
97
|
-
return Reflect.apply(t, thisArg, args as unknown[]);
|
|
98
|
-
} catch (error) {
|
|
99
|
-
return convert(error);
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
construct(t, args, newTarget) {
|
|
103
|
-
try {
|
|
104
|
-
return Reflect.construct(t, args as unknown[], newTarget) as object;
|
|
105
|
-
} catch (error) {
|
|
106
|
-
return convert(error) as object;
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
});
|
|
174
|
+
const wrapped = wrapFunction(value as (...a: unknown[]) => unknown);
|
|
110
175
|
cache.set(prop, wrapped);
|
|
111
176
|
return wrapped;
|
|
112
177
|
},
|
package/src/opfs_clip_pages.ts
CHANGED
|
@@ -29,10 +29,25 @@ interface PageResponse {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export const opfsClipPageWorkerSource = `
|
|
32
|
+
const sonareClipPageReadQueues = new Map();
|
|
33
|
+
|
|
34
|
+
function sonareEnqueueClipPageRead(key, task) {
|
|
35
|
+
const previous = sonareClipPageReadQueues.get(key) || Promise.resolve();
|
|
36
|
+
const next = previous.catch(() => undefined).then(task);
|
|
37
|
+
const queued = next.finally(() => {
|
|
38
|
+
if (sonareClipPageReadQueues.get(key) === queued) {
|
|
39
|
+
sonareClipPageReadQueues.delete(key);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
sonareClipPageReadQueues.set(key, queued);
|
|
43
|
+
return next;
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
self.onmessage = async (event) => {
|
|
33
47
|
const message = event.data;
|
|
34
48
|
if (!message || message.type !== 'sonare:read-clip-page') return;
|
|
35
49
|
const { requestId, path, pageIndex, numChannels, numSamples, pageFrames, dataOffsetBytes = 0 } = message;
|
|
50
|
+
await sonareEnqueueClipPageRead(String(path), async () => {
|
|
36
51
|
try {
|
|
37
52
|
if (pageIndex < 0) {
|
|
38
53
|
self.postMessage({ type: 'sonare:clip-page', requestId, pageIndex, ok: false });
|
|
@@ -95,6 +110,7 @@ self.onmessage = async (event) => {
|
|
|
95
110
|
error: error instanceof Error ? error.message : String(error),
|
|
96
111
|
});
|
|
97
112
|
}
|
|
113
|
+
});
|
|
98
114
|
};
|
|
99
115
|
`;
|
|
100
116
|
|
|
@@ -119,6 +135,7 @@ export function createOpfsClipPageProvider(
|
|
|
119
135
|
const ownsWorker = options.worker === undefined || options.terminateWorkerOnClose === true;
|
|
120
136
|
let nextRequestId = 1;
|
|
121
137
|
let closed = false;
|
|
138
|
+
let readQueue: Promise<void> = Promise.resolve();
|
|
122
139
|
const pending = new Map<
|
|
123
140
|
number,
|
|
124
141
|
{ resolve: (value: boolean) => void; reject: (reason: unknown) => void }
|
|
@@ -165,15 +182,32 @@ export function createOpfsClipPageProvider(
|
|
|
165
182
|
const promise = new Promise<boolean>((resolve, reject) => {
|
|
166
183
|
pending.set(requestId, { resolve, reject });
|
|
167
184
|
});
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
185
|
+
readQueue = readQueue
|
|
186
|
+
.catch(() => undefined)
|
|
187
|
+
.then(() => {
|
|
188
|
+
if (closed) {
|
|
189
|
+
const entry = pending.get(requestId);
|
|
190
|
+
pending.delete(requestId);
|
|
191
|
+
entry?.reject(new Error('OpfsClipPageProvider is closed'));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
worker.postMessage({
|
|
195
|
+
type: 'sonare:read-clip-page',
|
|
196
|
+
requestId,
|
|
197
|
+
path: options.path,
|
|
198
|
+
pageIndex,
|
|
199
|
+
numChannels: options.numChannels,
|
|
200
|
+
numSamples: options.numSamples,
|
|
201
|
+
pageFrames: options.pageFrames,
|
|
202
|
+
dataOffsetBytes: options.dataOffsetBytes ?? 0,
|
|
203
|
+
});
|
|
204
|
+
return promise.then(
|
|
205
|
+
() => undefined,
|
|
206
|
+
() => undefined,
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
readQueue.catch(() => {
|
|
210
|
+
// The per-request promise carries the user-visible failure.
|
|
177
211
|
});
|
|
178
212
|
return promise;
|
|
179
213
|
};
|
package/src/project.ts
CHANGED
|
@@ -25,6 +25,34 @@ export interface ProjectBounceOptions {
|
|
|
25
25
|
instrumentLatencySamples?: number;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Marker kind ordinals. Mirrors `SonareMarkerKind` in `src/sonare_c_types.h`;
|
|
30
|
+
* the values are part of the ABI and must not be renumbered.
|
|
31
|
+
*/
|
|
32
|
+
export const MarkerKind = {
|
|
33
|
+
marker: 0,
|
|
34
|
+
text: 1,
|
|
35
|
+
lyric: 2,
|
|
36
|
+
cuePoint: 3,
|
|
37
|
+
keySignature: 4,
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
/** A project timeline marker with its kind and (for key signatures) the key. */
|
|
41
|
+
export interface ProjectMarker {
|
|
42
|
+
/** Stable marker id (0 when allocating a new id via {@link Project.setMarkerEx}). */
|
|
43
|
+
id: number;
|
|
44
|
+
/** Marker position in PPQ (quarter notes). */
|
|
45
|
+
ppq: number;
|
|
46
|
+
/** Marker label. */
|
|
47
|
+
name?: string;
|
|
48
|
+
/** {@link MarkerKind} ordinal (default 0 = marker). */
|
|
49
|
+
kind?: number;
|
|
50
|
+
/** Key signature only: -7..7 (sharps positive). */
|
|
51
|
+
keyFifths?: number;
|
|
52
|
+
/** Key signature only: false = major, true = minor. */
|
|
53
|
+
keyMinor?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
28
56
|
/** Oscillator waveform for the built-in synth. */
|
|
29
57
|
export type BuiltinSynthWaveform =
|
|
30
58
|
| 'sine'
|
|
@@ -544,6 +572,10 @@ interface WasmProject {
|
|
|
544
572
|
setWarpMap: (map: ProjectWarpMapDesc) => void;
|
|
545
573
|
removeWarpMap: (warpRefId: number) => void;
|
|
546
574
|
setTrackMidiDestination: (trackId: number, destinationId: number) => void;
|
|
575
|
+
setTrackGain: (trackId: number, gain: number) => void;
|
|
576
|
+
setTrackMute: (trackId: number, mute: boolean) => void;
|
|
577
|
+
setTrackSolo: (trackId: number, solo: boolean) => void;
|
|
578
|
+
setTrackPan: (trackId: number, pan: number) => void;
|
|
547
579
|
undo: () => void;
|
|
548
580
|
redo: () => void;
|
|
549
581
|
setMidiEvents: (
|
|
@@ -627,6 +659,9 @@ interface WasmProject {
|
|
|
627
659
|
getSampleRate: () => number;
|
|
628
660
|
setMixerSceneJson: (sceneJson: string) => void;
|
|
629
661
|
setMarker: (markerId: number, ppq: number, name: string) => number;
|
|
662
|
+
setMarkerEx: (marker: ProjectMarker) => number;
|
|
663
|
+
markerByIndex: (index: number) => ProjectMarker;
|
|
664
|
+
markerCount: () => number;
|
|
630
665
|
trackCount: () => number;
|
|
631
666
|
sourceCount: () => number;
|
|
632
667
|
tempoSegmentCount: () => number;
|
|
@@ -1166,6 +1201,26 @@ export class Project {
|
|
|
1166
1201
|
this.native.setTrackMidiDestination(trackId, destinationId);
|
|
1167
1202
|
}
|
|
1168
1203
|
|
|
1204
|
+
/** Set a track's linear playback gain (1.0 = unity; >= 0) via an undoable edit. */
|
|
1205
|
+
setTrackGain(trackId: number, gain: number): void {
|
|
1206
|
+
this.native.setTrackGain(trackId, gain);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/** Set a track's mute flag via an undoable edit (a muted track is silent). */
|
|
1210
|
+
setTrackMute(trackId: number, mute: boolean): void {
|
|
1211
|
+
this.native.setTrackMute(trackId, mute);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/** Set a track's solo flag via an undoable edit (when any track is soloed, only soloed tracks sound). */
|
|
1215
|
+
setTrackSolo(trackId: number, solo: boolean): void {
|
|
1216
|
+
this.native.setTrackSolo(trackId, solo);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/** Set a track's stereo balance in [-1, +1] (0 = center) via an undoable edit. */
|
|
1220
|
+
setTrackPan(trackId: number, pan: number): void {
|
|
1221
|
+
this.native.setTrackPan(trackId, pan);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1169
1224
|
/** Undo the most recent edit. */
|
|
1170
1225
|
undo(): void {
|
|
1171
1226
|
this.native.undo();
|
|
@@ -1527,6 +1582,25 @@ export class Project {
|
|
|
1527
1582
|
return this.native.setMarker(markerId, ppq, name);
|
|
1528
1583
|
}
|
|
1529
1584
|
|
|
1585
|
+
/**
|
|
1586
|
+
* Add or replace a marker from a full {@link ProjectMarker}, including its
|
|
1587
|
+
* {@link MarkerKind} and (for key signatures) the key. Pass `id` 0 to allocate
|
|
1588
|
+
* a new id; returns the stable marker id.
|
|
1589
|
+
*/
|
|
1590
|
+
setMarkerEx(marker: ProjectMarker): number {
|
|
1591
|
+
return this.native.setMarkerEx(marker);
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
/** Read a project marker by index (0-based, in stored order). */
|
|
1595
|
+
markerByIndex(index: number): ProjectMarker {
|
|
1596
|
+
return this.native.markerByIndex(index);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
/** Number of markers in the project. */
|
|
1600
|
+
markerCount(): number {
|
|
1601
|
+
return this.native.markerCount();
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1530
1604
|
/** Number of tracks in the project. */
|
|
1531
1605
|
trackCount(): number {
|
|
1532
1606
|
return this.native.trackCount();
|