@libraz/libsonare 1.3.1 → 1.3.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 +277 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +333 -55
- package/dist/index.js.map +1 -1
- package/dist/sonare-rt-module.js +1 -1
- package/dist/sonare-rt.js +1 -1
- package/dist/sonare-rt.wasm +0 -0
- package/dist/sonare.js +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/worklet.d.ts +397 -9
- package/dist/worklet.js +1259 -87
- package/dist/worklet.js.map +1 -1
- package/package.json +1 -1
- package/src/effects_mastering.ts +16 -0
- package/src/errors.ts +44 -0
- package/src/index.ts +15 -1
- package/src/mixer.ts +11 -0
- package/src/module_state.ts +180 -4
- package/src/opfs_clip_pages.ts +43 -9
- package/src/realtime_engine.ts +174 -109
- package/src/sonare.js.d.ts +65 -0
- package/src/web_midi.ts +15 -11
- package/src/worklet.ts +1402 -66
package/package.json
CHANGED
package/src/effects_mastering.ts
CHANGED
|
@@ -376,6 +376,22 @@ export function masteringInsertNames(): string[] {
|
|
|
376
376
|
).masteringInsertNames();
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
/**
|
|
380
|
+
* Returns the camelCase parameter names a given insert / FX processor reads, for
|
|
381
|
+
* tooling/validation. Any key NOT in this list is silently ignored by the
|
|
382
|
+
* processor (and would be reported via {@link Mixer.sceneWarnings} when a scene
|
|
383
|
+
* carrying it is loaded). Band/sub-band processors enumerate their indexed
|
|
384
|
+
* `band{i}.<field>` keys. Returns an empty array for an unknown name (or one
|
|
385
|
+
* whose insert needs an unavailable build feature, e.g. FX).
|
|
386
|
+
*
|
|
387
|
+
* @param name - Insert processor name (see {@link masteringInsertNames}).
|
|
388
|
+
*/
|
|
389
|
+
export function masteringInsertParamNames(name: string): string[] {
|
|
390
|
+
return (
|
|
391
|
+
requireModule() as unknown as { masteringInsertParamNames: (name: string) => string[] }
|
|
392
|
+
).masteringInsertParamNames(name);
|
|
393
|
+
}
|
|
394
|
+
|
|
379
395
|
export function masteringPairProcessorNames(): PairProcessor[] {
|
|
380
396
|
return requireModule().masteringPairProcessorNames() as PairProcessor[];
|
|
381
397
|
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Numeric error codes carried by a {@link SonareError}. Mirrors the C ABI
|
|
3
|
+
* `SonareError` enum (and the Node / Python surfaces), so the same failure
|
|
4
|
+
* reports the same numeric code on every binding.
|
|
5
|
+
*/
|
|
6
|
+
export enum ErrorCode {
|
|
7
|
+
Ok = 0,
|
|
8
|
+
FileNotFound = 1,
|
|
9
|
+
InvalidFormat = 2,
|
|
10
|
+
DecodeFailed = 3,
|
|
11
|
+
InvalidParameter = 4,
|
|
12
|
+
OutOfMemory = 5,
|
|
13
|
+
NotSupported = 6,
|
|
14
|
+
InvalidState = 7,
|
|
15
|
+
Unknown = 99,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Error thrown by libsonare on a native (C++) failure. Carries a numeric
|
|
20
|
+
* {@link ErrorCode} `code` plus its canonical `codeName`, so callers can branch
|
|
21
|
+
* on the cause instead of matching message text.
|
|
22
|
+
*/
|
|
23
|
+
export class SonareError extends Error {
|
|
24
|
+
/** Numeric error code, equal to an {@link ErrorCode} value. */
|
|
25
|
+
readonly code: number;
|
|
26
|
+
/** Canonical name of `code`, e.g. `'InvalidParameter'`. */
|
|
27
|
+
readonly codeName: string;
|
|
28
|
+
|
|
29
|
+
constructor(code: number, codeName: string, message: string) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = 'SonareError';
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.codeName = codeName;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Type guard: whether a caught value is a libsonare {@link SonareError}. */
|
|
38
|
+
export function isSonareError(value: unknown): value is SonareError {
|
|
39
|
+
return (
|
|
40
|
+
value instanceof Error &&
|
|
41
|
+
(value as { name?: unknown }).name === 'SonareError' &&
|
|
42
|
+
typeof (value as { code?: unknown }).code === 'number'
|
|
43
|
+
);
|
|
44
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -67,6 +67,7 @@ export {
|
|
|
67
67
|
masteringDynamicsGate,
|
|
68
68
|
masteringDynamicsTransientShaper,
|
|
69
69
|
masteringInsertNames,
|
|
70
|
+
masteringInsertParamNames,
|
|
70
71
|
masteringPairAnalysisNames,
|
|
71
72
|
masteringPairAnalyze,
|
|
72
73
|
masteringPairProcess,
|
|
@@ -98,6 +99,7 @@ export {
|
|
|
98
99
|
voiceChange,
|
|
99
100
|
voiceChangeRealtime,
|
|
100
101
|
} from './effects_mastering';
|
|
102
|
+
export { ErrorCode, isSonareError, SonareError } from './errors';
|
|
101
103
|
export type { MelodyOptions } from './feature_music';
|
|
102
104
|
export {
|
|
103
105
|
amplitudeToDb,
|
|
@@ -399,6 +401,7 @@ export type {
|
|
|
399
401
|
EngineAutomationPoint,
|
|
400
402
|
EngineBounceOptions,
|
|
401
403
|
EngineBounceResult,
|
|
404
|
+
EngineBus,
|
|
402
405
|
EngineCapabilities,
|
|
403
406
|
EngineCaptureStatus,
|
|
404
407
|
EngineClip,
|
|
@@ -408,8 +411,14 @@ export type {
|
|
|
408
411
|
EngineMarker,
|
|
409
412
|
EngineMeterTelemetry,
|
|
410
413
|
EngineMetronomeConfig,
|
|
414
|
+
EngineMidiClipSchedule,
|
|
415
|
+
EngineMidiEvent,
|
|
411
416
|
EngineParameterInfo,
|
|
412
417
|
EngineTelemetry,
|
|
418
|
+
EngineTempoSegment,
|
|
419
|
+
EngineTimeSignatureSegment,
|
|
420
|
+
EngineTrackLane,
|
|
421
|
+
EngineTrackSend,
|
|
413
422
|
EngineTransportState,
|
|
414
423
|
MidiCcBindOptions,
|
|
415
424
|
} from './realtime_engine';
|
|
@@ -482,6 +491,11 @@ let initPromise: Promise<void> | null = null;
|
|
|
482
491
|
*/
|
|
483
492
|
export async function init(options?: {
|
|
484
493
|
locateFile?: (path: string, prefix: string) => string;
|
|
494
|
+
wasmBinary?: ArrayBuffer | Uint8Array;
|
|
495
|
+
moduleFactory?: (options?: {
|
|
496
|
+
locateFile?: (path: string, prefix: string) => string;
|
|
497
|
+
wasmBinary?: ArrayBuffer | Uint8Array;
|
|
498
|
+
}) => Promise<SonareModule>;
|
|
485
499
|
}): Promise<void> {
|
|
486
500
|
if (module) {
|
|
487
501
|
return;
|
|
@@ -493,7 +507,7 @@ export async function init(options?: {
|
|
|
493
507
|
|
|
494
508
|
initPromise = (async () => {
|
|
495
509
|
try {
|
|
496
|
-
const createModule = (await import('./sonare.js')).default;
|
|
510
|
+
const createModule = options?.moduleFactory ?? (await import('./sonare.js')).default;
|
|
497
511
|
module = await createModule(options);
|
|
498
512
|
setSonareModule(module);
|
|
499
513
|
} catch (error) {
|
package/src/mixer.ts
CHANGED
|
@@ -76,6 +76,17 @@ export class Mixer {
|
|
|
76
76
|
this.mixer.compile();
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Non-fatal warnings captured when this mixer was built from scene JSON: one
|
|
81
|
+
* entry per channel-strip insert that was handed param keys it does not read
|
|
82
|
+
* (a likely typo, or a key meant for a different processor). The scene still
|
|
83
|
+
* loaded; these keys simply took no effect. Empty when every key was consumed.
|
|
84
|
+
* Use {@link masteringInsertParamNames} to discover the keys an insert accepts.
|
|
85
|
+
*/
|
|
86
|
+
sceneWarnings(): string[] {
|
|
87
|
+
return this.mixer.sceneWarnings();
|
|
88
|
+
}
|
|
89
|
+
|
|
79
90
|
/**
|
|
80
91
|
* Mix one block of per-strip stereo audio into the stereo master.
|
|
81
92
|
*
|
package/src/module_state.ts
CHANGED
|
@@ -1,14 +1,190 @@
|
|
|
1
|
+
import { ErrorCode, SonareError } from './errors';
|
|
1
2
|
import type { SonareModule } from './sonare.js';
|
|
2
3
|
|
|
3
|
-
let
|
|
4
|
+
let wrappedModule: SonareModule | null = null;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Shape of the structured info the native `sonareExceptionInfo(ptr)` returns.
|
|
8
|
+
*/
|
|
9
|
+
interface NativeExceptionInfo {
|
|
10
|
+
code: number;
|
|
11
|
+
codeName: string;
|
|
12
|
+
message: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Recover the native exception-object pointer from a value thrown across the
|
|
17
|
+
* WASM boundary. emscripten surfaces a C++ throw in two shapes depending on the
|
|
18
|
+
* toolchain/exception mode:
|
|
19
|
+
* - a raw pointer number (older / classic surfacing), or
|
|
20
|
+
* - a `CppException` object exposing the pointer as `excPtr` (emscripten with
|
|
21
|
+
* `-fexceptions`).
|
|
22
|
+
* Returns null when the thrown value is neither (a genuine JS error), so the
|
|
23
|
+
* caller rethrows it unchanged.
|
|
24
|
+
*/
|
|
25
|
+
function nativeExceptionPtr(error: unknown): number | null {
|
|
26
|
+
if (typeof error === 'number') {
|
|
27
|
+
return error;
|
|
28
|
+
}
|
|
29
|
+
if (error !== null && typeof error === 'object') {
|
|
30
|
+
const ptr = (error as { excPtr?: unknown }).excPtr;
|
|
31
|
+
if (typeof ptr === 'number') {
|
|
32
|
+
return ptr;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Turn a thrown native exception pointer into a {@link SonareError}. The bound
|
|
40
|
+
* `sonareExceptionInfo` decodes the pointer back into { code, codeName,
|
|
41
|
+
* message }.
|
|
42
|
+
*/
|
|
43
|
+
function makeSonareError(raw: SonareModule, thrown: number): SonareError {
|
|
44
|
+
let code: number = ErrorCode.Unknown;
|
|
45
|
+
let codeName = 'Unknown';
|
|
46
|
+
let message = `libsonare native exception (${thrown})`;
|
|
47
|
+
try {
|
|
48
|
+
const info = (
|
|
49
|
+
raw as unknown as { sonareExceptionInfo?: (ptr: number) => NativeExceptionInfo }
|
|
50
|
+
).sonareExceptionInfo?.(thrown);
|
|
51
|
+
if (info) {
|
|
52
|
+
code = info.code ?? code;
|
|
53
|
+
codeName = info.codeName ?? codeName;
|
|
54
|
+
message = info.message || message;
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Fall back to the generic message if decoding fails.
|
|
58
|
+
}
|
|
59
|
+
return new SonareError(code, codeName, message);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Wrap the embind module so a native C++ exception (which surfaces as a raw
|
|
64
|
+
* pointer number or a `CppException` carrying one) is rethrown as a
|
|
65
|
+
* {@link SonareError}. Only function-valued
|
|
66
|
+
* members are wrapped, and the wrapper is cached per member so repeated access
|
|
67
|
+
* stays cheap; non-function members (typed-array heap views, etc.) pass through
|
|
68
|
+
* unchanged. The dedicated realtime `sonare-rt` module is separate and is not
|
|
69
|
+
* affected by this wrapper.
|
|
70
|
+
*/
|
|
71
|
+
function wrapModuleErrors(raw: SonareModule): SonareModule {
|
|
72
|
+
const cache = new Map<PropertyKey, unknown>();
|
|
73
|
+
const objectCache = new WeakMap<object, unknown>();
|
|
74
|
+
const convert = (error: unknown): never => {
|
|
75
|
+
const ptr = nativeExceptionPtr(error);
|
|
76
|
+
if (ptr !== null) {
|
|
77
|
+
throw makeSonareError(raw, ptr);
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
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
|
+
|
|
161
|
+
return new Proxy(raw, {
|
|
162
|
+
get(target, prop, receiver) {
|
|
163
|
+
const value = Reflect.get(target, prop, receiver);
|
|
164
|
+
if (typeof value !== 'function') {
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
const cached = cache.get(prop);
|
|
168
|
+
if (cached) {
|
|
169
|
+
return cached;
|
|
170
|
+
}
|
|
171
|
+
// Wrap as a Proxy (not a plain function) so embind class constructors
|
|
172
|
+
// invoked via `new module.Foo(...)` keep their `[[Construct]]` behaviour
|
|
173
|
+
// and prototype while still converting thrown native pointers.
|
|
174
|
+
const wrapped = wrapFunction(value as (...a: unknown[]) => unknown);
|
|
175
|
+
cache.set(prop, wrapped);
|
|
176
|
+
return wrapped;
|
|
177
|
+
},
|
|
178
|
+
}) as SonareModule;
|
|
179
|
+
}
|
|
4
180
|
|
|
5
181
|
export function setSonareModule(module: SonareModule): void {
|
|
6
|
-
|
|
182
|
+
wrappedModule = wrapModuleErrors(module);
|
|
7
183
|
}
|
|
8
184
|
|
|
9
185
|
export function getSonareModule(): SonareModule {
|
|
10
|
-
if (!
|
|
186
|
+
if (!wrappedModule) {
|
|
11
187
|
throw new Error('Module not initialized. Call init() first.');
|
|
12
188
|
}
|
|
13
|
-
return
|
|
189
|
+
return wrappedModule;
|
|
14
190
|
}
|
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
|
};
|