@libraz/libsonare 1.3.0 → 1.3.1

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.
Binary file
package/dist/sonare.wasm CHANGED
Binary file
package/dist/worklet.d.ts CHANGED
@@ -3740,7 +3740,7 @@ interface OpfsClipPageProviderBinding {
3740
3740
  supplyRequest(request: ClipPageRequest): Promise<boolean>;
3741
3741
  close(): void;
3742
3742
  }
3743
- declare const opfsClipPageWorkerSource = "\nself.onmessage = async (event) => {\n const message = event.data;\n if (!message || message.type !== 'sonare:read-clip-page') return;\n const { requestId, path, pageIndex, numChannels, numSamples, pageFrames, dataOffsetBytes = 0 } = message;\n try {\n if (pageIndex < 0) {\n self.postMessage({ type: 'sonare:clip-page', requestId, pageIndex, ok: false });\n return;\n }\n const startFrame = pageIndex * pageFrames;\n if (startFrame >= numSamples) {\n self.postMessage({ type: 'sonare:clip-page', requestId, pageIndex, ok: false });\n return;\n }\n const root = await self.navigator.storage.getDirectory();\n let dir = root;\n const parts = String(path).split('/').filter(Boolean);\n for (let i = 0; i < parts.length - 1; ++i) {\n dir = await dir.getDirectoryHandle(parts[i]);\n }\n const fileHandle = await dir.getFileHandle(parts[parts.length - 1]);\n const access = await fileHandle.createSyncAccessHandle();\n try {\n const frames = Math.min(pageFrames, numSamples - startFrame);\n const frameBytes = numChannels * 4;\n const bytes = new Uint8Array(frames * frameBytes);\n const bytesRead = access.read(bytes, { at: dataOffsetBytes + startFrame * frameBytes });\n const framesRead = Math.floor(bytesRead / frameBytes);\n if (framesRead <= 0) {\n self.postMessage({ type: 'sonare:clip-page', requestId, pageIndex, ok: false });\n return;\n }\n const view = new DataView(bytes.buffer, 0, framesRead * frameBytes);\n const channelBuffers = Array.from({ length: numChannels }, () => new ArrayBuffer(framesRead * 4));\n for (let ch = 0; ch < numChannels; ++ch) {\n const channel = new Float32Array(channelBuffers[ch]);\n for (let frame = 0; frame < framesRead; ++frame) {\n channel[frame] = view.getFloat32((frame * numChannels + ch) * 4, true);\n }\n }\n self.postMessage(\n { type: 'sonare:clip-page', requestId, pageIndex, ok: true, frames: framesRead, channelBuffers },\n channelBuffers,\n );\n } finally {\n access.close();\n }\n } catch (error) {\n self.postMessage({\n type: 'sonare:clip-page',\n requestId,\n pageIndex,\n ok: false,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n};\n";
3743
+ declare const opfsClipPageWorkerSource = "\nself.onmessage = async (event) => {\n const message = event.data;\n if (!message || message.type !== 'sonare:read-clip-page') return;\n const { requestId, path, pageIndex, numChannels, numSamples, pageFrames, dataOffsetBytes = 0 } = message;\n try {\n if (pageIndex < 0) {\n self.postMessage({ type: 'sonare:clip-page', requestId, pageIndex, ok: false });\n return;\n }\n const startFrame = pageIndex * pageFrames;\n if (startFrame >= numSamples) {\n self.postMessage({ type: 'sonare:clip-page', requestId, pageIndex, ok: false });\n return;\n }\n const root = await self.navigator.storage.getDirectory();\n let dir = root;\n const parts = String(path).split('/').filter(Boolean);\n for (let i = 0; i < parts.length - 1; ++i) {\n dir = await dir.getDirectoryHandle(parts[i]);\n }\n const fileHandle = await dir.getFileHandle(parts[parts.length - 1]);\n const access = await fileHandle.createSyncAccessHandle();\n try {\n const frames = Math.min(pageFrames, numSamples - startFrame);\n const frameBytes = numChannels * 4;\n const bytes = new Uint8Array(frames * frameBytes);\n let bytesReadTotal = 0;\n const readOffset = dataOffsetBytes + startFrame * frameBytes;\n while (bytesReadTotal < bytes.byteLength) {\n const bytesRead = access.read(bytes.subarray(bytesReadTotal), {\n at: readOffset + bytesReadTotal,\n });\n if (bytesRead <= 0) {\n break;\n }\n bytesReadTotal += bytesRead;\n }\n if (bytesReadTotal !== bytes.byteLength || bytesReadTotal % frameBytes !== 0) {\n self.postMessage({ type: 'sonare:clip-page', requestId, pageIndex, ok: false });\n return;\n }\n const framesRead = bytesReadTotal / frameBytes;\n const view = new DataView(bytes.buffer, 0, framesRead * frameBytes);\n const channelBuffers = Array.from({ length: numChannels }, () => new ArrayBuffer(framesRead * 4));\n for (let ch = 0; ch < numChannels; ++ch) {\n const channel = new Float32Array(channelBuffers[ch]);\n for (let frame = 0; frame < framesRead; ++frame) {\n channel[frame] = view.getFloat32((frame * numChannels + ch) * 4, true);\n }\n }\n self.postMessage(\n { type: 'sonare:clip-page', requestId, pageIndex, ok: true, frames: framesRead, channelBuffers },\n channelBuffers,\n );\n } finally {\n access.close();\n }\n } catch (error) {\n self.postMessage({\n type: 'sonare:clip-page',\n requestId,\n pageIndex,\n ok: false,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n};\n";
3744
3744
  declare function createOpfsClipPageWorker(): Worker;
3745
3745
  declare function createOpfsClipPageProvider(engine: RealtimeEngine, options: OpfsClipPageProviderOptions): OpfsClipPageProviderBinding;
3746
3746
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@libraz/libsonare",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "type": "module",
5
5
  "packageManager": "yarn@4.15.0",
6
6
  "description": "Audio analysis library for music information retrieval",
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
- options.stream ??
21
+ providedStream ??
21
22
  (await navigator.mediaDevices.getUserMedia({
22
- audio: options.audio ?? true,
23
- video: false,
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 (options.stopTracksOnClose !== false) {
40
+ if (stopTracksOnClose) {
39
41
  for (const track of stream.getAudioTracks()) {
40
42
  track.stop();
41
43
  }
@@ -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
- const bytesRead = access.read(bytes, { at: dataOffsetBytes + startFrame * frameBytes });
59
- const framesRead = Math.floor(bytesRead / frameBytes);
60
- if (framesRead <= 0) {
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
- provider.supply(response.pageIndex, channels);
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/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 requestMIDIAccess = (globalThis.navigator as NavigatorWithMidi | undefined)
109
- ?.requestMIDIAccess;
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
- const access = await requestMIDIAccess({
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
- if (status !== 0) {
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 < 0 ? 0 : b, portTimeSamples);
284
+ engine.pushMidiInputNoteOff(group, channel, a, b, portTimeSamples);
286
285
  } else if (message === 0x90) {
287
- if ((b < 0 ? 0 : b) === 0) {
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);