@libraz/libsonare 1.3.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@libraz/libsonare",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "type": "module",
5
5
  "packageManager": "yarn@4.15.0",
6
6
  "description": "Audio analysis library for music information retrieval",
package/src/index.ts CHANGED
@@ -401,6 +401,7 @@ export type {
401
401
  EngineAutomationPoint,
402
402
  EngineBounceOptions,
403
403
  EngineBounceResult,
404
+ EngineBus,
404
405
  EngineCapabilities,
405
406
  EngineCaptureStatus,
406
407
  EngineClip,
@@ -410,8 +411,14 @@ export type {
410
411
  EngineMarker,
411
412
  EngineMeterTelemetry,
412
413
  EngineMetronomeConfig,
414
+ EngineMidiClipSchedule,
415
+ EngineMidiEvent,
413
416
  EngineParameterInfo,
414
417
  EngineTelemetry,
418
+ EngineTempoSegment,
419
+ EngineTimeSignatureSegment,
420
+ EngineTrackLane,
421
+ EngineTrackSend,
415
422
  EngineTransportState,
416
423
  MidiCcBindOptions,
417
424
  } from './realtime_engine';
@@ -484,6 +491,11 @@ let initPromise: Promise<void> | null = null;
484
491
  */
485
492
  export async function init(options?: {
486
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>;
487
499
  }): Promise<void> {
488
500
  if (module) {
489
501
  return;
@@ -495,7 +507,7 @@ export async function init(options?: {
495
507
 
496
508
  initPromise = (async () => {
497
509
  try {
498
- const createModule = (await import('./sonare.js')).default;
510
+ const createModule = options?.moduleFactory ?? (await import('./sonare.js')).default;
499
511
  module = await createModule(options);
500
512
  setSonareModule(module);
501
513
  } catch (error) {
@@ -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 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
- });
174
+ const wrapped = wrapFunction(value as (...a: unknown[]) => unknown);
110
175
  cache.set(prop, wrapped);
111
176
  return wrapped;
112
177
  },
@@ -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
- worker.postMessage({
169
- type: 'sonare:read-clip-page',
170
- requestId,
171
- path: options.path,
172
- pageIndex,
173
- numChannels: options.numChannels,
174
- numSamples: options.numSamples,
175
- pageFrames: options.pageFrames,
176
- dataOffsetBytes: options.dataOffsetBytes ?? 0,
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
  };