@libraz/libsonare 1.3.0 → 1.3.2
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 +232 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +147 -22
- package/dist/index.js.map +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 +52 -2
- package/dist/worklet.js +92 -4
- 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 +2 -0
- package/src/live_audio.ts +6 -4
- package/src/mixer.ts +11 -0
- package/src/module_state.ts +115 -4
- package/src/opfs_clip_pages.ts +19 -4
- package/src/sonare.js.d.ts +6 -0
- package/src/web_midi.ts +10 -11
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,
|
package/src/live_audio.ts
CHANGED
|
@@ -16,11 +16,13 @@ export async function bindMicrophoneInput(
|
|
|
16
16
|
engine: SonareRealtimeEngineNode | AudioWorkletNode,
|
|
17
17
|
options: BindMicrophoneInputOptions = {},
|
|
18
18
|
): Promise<MicrophoneInputBinding> {
|
|
19
|
+
const { stream: providedStream, stopTracksOnClose = true, ...constraints } = options;
|
|
19
20
|
const stream =
|
|
20
|
-
|
|
21
|
+
providedStream ??
|
|
21
22
|
(await navigator.mediaDevices.getUserMedia({
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
...constraints,
|
|
24
|
+
audio: constraints.audio ?? true,
|
|
25
|
+
video: constraints.video ?? false,
|
|
24
26
|
}));
|
|
25
27
|
const source = context.createMediaStreamSource(stream);
|
|
26
28
|
const node = 'node' in engine ? engine.node : engine;
|
|
@@ -35,7 +37,7 @@ export async function bindMicrophoneInput(
|
|
|
35
37
|
}
|
|
36
38
|
closed = true;
|
|
37
39
|
source.disconnect();
|
|
38
|
-
if (
|
|
40
|
+
if (stopTracksOnClose) {
|
|
39
41
|
for (const track of stream.getAudioTracks()) {
|
|
40
42
|
track.stop();
|
|
41
43
|
}
|
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,125 @@
|
|
|
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 convert = (error: unknown): never => {
|
|
74
|
+
const ptr = nativeExceptionPtr(error);
|
|
75
|
+
if (ptr !== null) {
|
|
76
|
+
throw makeSonareError(raw, ptr);
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
};
|
|
80
|
+
return new Proxy(raw, {
|
|
81
|
+
get(target, prop, receiver) {
|
|
82
|
+
const value = Reflect.get(target, prop, receiver);
|
|
83
|
+
if (typeof value !== 'function') {
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
const cached = cache.get(prop);
|
|
87
|
+
if (cached) {
|
|
88
|
+
return cached;
|
|
89
|
+
}
|
|
90
|
+
// Wrap as a Proxy (not a plain function) so embind class constructors
|
|
91
|
+
// invoked via `new module.Foo(...)` keep their `[[Construct]]` behaviour
|
|
92
|
+
// and prototype while still converting thrown native pointers.
|
|
93
|
+
const fn = value as (...a: unknown[]) => unknown;
|
|
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
|
+
});
|
|
110
|
+
cache.set(prop, wrapped);
|
|
111
|
+
return wrapped;
|
|
112
|
+
},
|
|
113
|
+
}) as SonareModule;
|
|
114
|
+
}
|
|
4
115
|
|
|
5
116
|
export function setSonareModule(module: SonareModule): void {
|
|
6
|
-
|
|
117
|
+
wrappedModule = wrapModuleErrors(module);
|
|
7
118
|
}
|
|
8
119
|
|
|
9
120
|
export function getSonareModule(): SonareModule {
|
|
10
|
-
if (!
|
|
121
|
+
if (!wrappedModule) {
|
|
11
122
|
throw new Error('Module not initialized. Call init() first.');
|
|
12
123
|
}
|
|
13
|
-
return
|
|
124
|
+
return wrappedModule;
|
|
14
125
|
}
|
package/src/opfs_clip_pages.ts
CHANGED
|
@@ -55,12 +55,22 @@ self.onmessage = async (event) => {
|
|
|
55
55
|
const frames = Math.min(pageFrames, numSamples - startFrame);
|
|
56
56
|
const frameBytes = numChannels * 4;
|
|
57
57
|
const bytes = new Uint8Array(frames * frameBytes);
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
58
|
+
let bytesReadTotal = 0;
|
|
59
|
+
const readOffset = dataOffsetBytes + startFrame * frameBytes;
|
|
60
|
+
while (bytesReadTotal < bytes.byteLength) {
|
|
61
|
+
const bytesRead = access.read(bytes.subarray(bytesReadTotal), {
|
|
62
|
+
at: readOffset + bytesReadTotal,
|
|
63
|
+
});
|
|
64
|
+
if (bytesRead <= 0) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
bytesReadTotal += bytesRead;
|
|
68
|
+
}
|
|
69
|
+
if (bytesReadTotal !== bytes.byteLength || bytesReadTotal % frameBytes !== 0) {
|
|
61
70
|
self.postMessage({ type: 'sonare:clip-page', requestId, pageIndex, ok: false });
|
|
62
71
|
return;
|
|
63
72
|
}
|
|
73
|
+
const framesRead = bytesReadTotal / frameBytes;
|
|
64
74
|
const view = new DataView(bytes.buffer, 0, framesRead * frameBytes);
|
|
65
75
|
const channelBuffers = Array.from({ length: numChannels }, () => new ArrayBuffer(framesRead * 4));
|
|
66
76
|
for (let ch = 0; ch < numChannels; ++ch) {
|
|
@@ -137,7 +147,12 @@ export function createOpfsClipPageProvider(
|
|
|
137
147
|
entry.resolve(false);
|
|
138
148
|
return;
|
|
139
149
|
}
|
|
140
|
-
|
|
150
|
+
try {
|
|
151
|
+
provider.supply(response.pageIndex, channels);
|
|
152
|
+
} catch {
|
|
153
|
+
entry.resolve(false);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
141
156
|
entry.resolve(true);
|
|
142
157
|
};
|
|
143
158
|
worker.addEventListener('message', onMessage as EventListener);
|
package/src/sonare.js.d.ts
CHANGED
|
@@ -1845,6 +1845,11 @@ export interface SonareModule {
|
|
|
1845
1845
|
|
|
1846
1846
|
// Mixing - scene-based Mixer
|
|
1847
1847
|
createMixerFromSceneJson: (json: string, sampleRate: number, blockSize: number) => WasmMixer;
|
|
1848
|
+
|
|
1849
|
+
// Decodes a thrown native exception-object pointer (emscripten classic EH
|
|
1850
|
+
// surfaces a C++ throw as the raw pointer number) into a structured error.
|
|
1851
|
+
// Consumed by the module-error wrapper in module_state.ts.
|
|
1852
|
+
sonareExceptionInfo: (ptr: number) => { code: number; codeName: string; message: string };
|
|
1848
1853
|
}
|
|
1849
1854
|
|
|
1850
1855
|
export interface WasmStreamingMasteringChain {
|
|
@@ -1947,6 +1952,7 @@ export interface WasmMixer {
|
|
|
1947
1952
|
outputRightView: () => Float32Array;
|
|
1948
1953
|
processPreparedStereo: (numSamples: number) => void;
|
|
1949
1954
|
stripCount: () => number;
|
|
1955
|
+
sceneWarnings: () => string[];
|
|
1950
1956
|
scheduleInsertAutomation: (
|
|
1951
1957
|
stripIndex: number,
|
|
1952
1958
|
insertIndex: number,
|
package/src/web_midi.ts
CHANGED
|
@@ -105,9 +105,8 @@ export async function bindWebMidi(
|
|
|
105
105
|
engine: RealtimeEngine,
|
|
106
106
|
options: BindWebMidiOptions = {},
|
|
107
107
|
): Promise<WebMidiBinding> {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
if (typeof requestMIDIAccess !== 'function') {
|
|
108
|
+
const navigatorWithMidi = globalThis.navigator as NavigatorWithMidi | undefined;
|
|
109
|
+
if (typeof navigatorWithMidi?.requestMIDIAccess !== 'function') {
|
|
111
110
|
throw new Error('Web MIDI is not available in this environment');
|
|
112
111
|
}
|
|
113
112
|
|
|
@@ -115,7 +114,9 @@ export async function bindWebMidi(
|
|
|
115
114
|
assertNibble('bindWebMidi', group, 'group');
|
|
116
115
|
const destinationId = options.destinationId ?? 0;
|
|
117
116
|
const selectedIds = new Set(options.inputIds ?? []);
|
|
118
|
-
|
|
117
|
+
// Invoke through the navigator so the browser's native method keeps its
|
|
118
|
+
// required `this` binding (detached calls throw "Illegal invocation").
|
|
119
|
+
const access = await navigatorWithMidi.requestMIDIAccess({
|
|
119
120
|
sysex: options.sysex ?? false,
|
|
120
121
|
software: options.software ?? true,
|
|
121
122
|
});
|
|
@@ -154,9 +155,7 @@ export async function bindWebMidi(
|
|
|
154
155
|
runningStatus,
|
|
155
156
|
options.timestampToSamples,
|
|
156
157
|
);
|
|
157
|
-
|
|
158
|
-
runningStatus = status;
|
|
159
|
-
}
|
|
158
|
+
runningStatus = status;
|
|
160
159
|
};
|
|
161
160
|
if (input.addEventListener) {
|
|
162
161
|
input.addEventListener('midimessage', listener);
|
|
@@ -268,12 +267,12 @@ function dispatchMidiMessage(
|
|
|
268
267
|
const message = status & 0xf0;
|
|
269
268
|
const channel = status & 0x0f;
|
|
270
269
|
if (message < 0x80 || message > 0xe0) {
|
|
271
|
-
return status;
|
|
270
|
+
return status >= 0xf8 ? runningStatus : 0;
|
|
272
271
|
}
|
|
273
272
|
|
|
274
273
|
const a = readU7(data, offset);
|
|
275
274
|
const b = readU7(data, offset + 1);
|
|
276
|
-
if (a < 0) {
|
|
275
|
+
if (a < 0 || b < 0) {
|
|
277
276
|
return status;
|
|
278
277
|
}
|
|
279
278
|
|
|
@@ -282,9 +281,9 @@ function dispatchMidiMessage(
|
|
|
282
281
|
: 0;
|
|
283
282
|
|
|
284
283
|
if (message === 0x80) {
|
|
285
|
-
engine.pushMidiInputNoteOff(group, channel, a, b
|
|
284
|
+
engine.pushMidiInputNoteOff(group, channel, a, b, portTimeSamples);
|
|
286
285
|
} else if (message === 0x90) {
|
|
287
|
-
if (
|
|
286
|
+
if (b === 0) {
|
|
288
287
|
engine.pushMidiInputNoteOff(group, channel, a, 0, portTimeSamples);
|
|
289
288
|
} else {
|
|
290
289
|
engine.pushMidiInputNoteOn(group, channel, a, b, portTimeSamples);
|