@libraz/libsonare 1.3.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@libraz/libsonare",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "type": "module",
5
5
  "packageManager": "yarn@4.15.0",
6
6
  "description": "Audio analysis library for music information retrieval",
@@ -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/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
  *
@@ -1,14 +1,125 @@
1
+ import { ErrorCode, SonareError } from './errors';
1
2
  import type { SonareModule } from './sonare.js';
2
3
 
3
- let wasmModule: SonareModule | null = null;
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
- wasmModule = module;
117
+ wrappedModule = wrapModuleErrors(module);
7
118
  }
8
119
 
9
120
  export function getSonareModule(): SonareModule {
10
- if (!wasmModule) {
121
+ if (!wrappedModule) {
11
122
  throw new Error('Module not initialized. Call init() first.');
12
123
  }
13
- return wasmModule;
124
+ return wrappedModule;
14
125
  }
@@ -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,