@libraz/libsonare 1.5.5 → 1.6.0

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.
Files changed (75) hide show
  1. package/README.md +109 -20
  2. package/dist/analysis.d.ts +5730 -0
  3. package/dist/analysis.js +2464 -0
  4. package/dist/analysis.js.map +1 -0
  5. package/dist/index.d.ts +759 -35
  6. package/dist/index.js +904 -209
  7. package/dist/index.js.map +1 -1
  8. package/dist/schemas/realtime-voice-changer-preset-pack.schema.json +27 -0
  9. package/dist/schemas/realtime-voice-changer-preset.schema.json +140 -0
  10. package/dist/sonare-analysis.js +2 -0
  11. package/dist/sonare-analysis.wasm +0 -0
  12. package/dist/sonare.js +1 -1
  13. package/dist/sonare.wasm +0 -0
  14. package/dist/worker.d.ts +65 -0
  15. package/dist/worker.js +707 -0
  16. package/dist/worker.js.map +1 -0
  17. package/dist/worklet.d.ts +574 -86
  18. package/dist/worklet.js +1634 -235
  19. package/dist/worklet.js.map +1 -1
  20. package/package.json +23 -11
  21. package/src/_chain_config.ts +5 -3
  22. package/src/analysis.ts +137 -0
  23. package/src/analysis_helpers.ts +6 -0
  24. package/src/clip_page_streamer.ts +62 -8
  25. package/src/codes.ts +51 -64
  26. package/src/effects_mastering.ts +1 -0
  27. package/src/errors.ts +1 -0
  28. package/src/feature_core.ts +140 -2
  29. package/src/feature_music.ts +17 -2
  30. package/src/feature_pitch.ts +70 -1
  31. package/src/feature_spectral.ts +231 -5
  32. package/src/feature_spectrogram.ts +159 -0
  33. package/src/features.ts +16 -1
  34. package/src/index.ts +101 -7
  35. package/src/mastering_chain.ts +20 -8
  36. package/src/mastering_core.ts +5 -0
  37. package/src/mastering_dynamics.ts +7 -1
  38. package/src/metering.ts +6 -1
  39. package/src/opfs_clip_pages.ts +26 -1
  40. package/src/project.ts +7 -0
  41. package/src/project_class.ts +62 -8
  42. package/src/project_internal.ts +24 -29
  43. package/src/project_types.ts +69 -10
  44. package/src/public_types.ts +68 -0
  45. package/src/public_types_mastering.ts +22 -0
  46. package/src/public_types_mixing.ts +11 -2
  47. package/src/public_types_music.ts +9 -0
  48. package/src/public_types_spectral.ts +33 -0
  49. package/src/quick_analysis.ts +25 -7
  50. package/src/realtime_engine.ts +153 -3
  51. package/src/realtime_voice_changer.ts +51 -77
  52. package/src/sonare-analysis.js.d.ts +8 -0
  53. package/src/sonare.js.d.ts +276 -6
  54. package/src/streaming_processors.ts +15 -0
  55. package/src/web_midi.ts +36 -4
  56. package/src/worker.ts +196 -0
  57. package/src/worker_client.ts +352 -0
  58. package/src/worker_protocol.ts +59 -0
  59. package/src/worklet/engine-automation.ts +2 -1
  60. package/src/worklet/engine-capture-facade.ts +13 -4
  61. package/src/worklet/engine-clips.ts +12 -3
  62. package/src/worklet/engine-node.ts +212 -4
  63. package/src/worklet/engine-offline.ts +7 -2
  64. package/src/worklet/engine-parameter-facade.ts +26 -5
  65. package/src/worklet/engine-processor.ts +383 -42
  66. package/src/worklet/engine-register.ts +8 -0
  67. package/src/worklet/engine-strips.ts +17 -1
  68. package/src/worklet/engine-sync.ts +2 -6
  69. package/src/worklet/engine-tempo-facade.ts +8 -6
  70. package/src/worklet/engine.ts +380 -13
  71. package/src/worklet/guards.ts +22 -1
  72. package/src/worklet/messages.ts +125 -7
  73. package/src/worklet/protocol.ts +301 -17
  74. package/src/worklet/voice-changer-processor.ts +12 -15
  75. package/src/worklet.ts +20 -1
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@libraz/libsonare",
3
- "version": "1.5.5",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
- "packageManager": "yarn@4.15.0",
5
+ "packageManager": "yarn@4.18.0",
6
6
  "description": "Audio analysis, mastering, mixing, and MIDI synthesis in WebAssembly",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",
@@ -11,11 +11,21 @@
11
11
  "types": "./dist/index.d.ts",
12
12
  "import": "./dist/index.js"
13
13
  },
14
+ "./analysis": {
15
+ "types": "./dist/analysis.d.ts",
16
+ "import": "./dist/analysis.js"
17
+ },
14
18
  "./worklet": {
15
19
  "types": "./dist/worklet.d.ts",
16
20
  "import": "./dist/worklet.js"
17
21
  },
22
+ "./worker": {
23
+ "types": "./dist/worker.d.ts",
24
+ "import": "./dist/worker.js"
25
+ },
18
26
  "./wasm": "./dist/sonare.wasm",
27
+ "./schemas/realtime-voice-changer-preset.schema.json": "./dist/schemas/realtime-voice-changer-preset.schema.json",
28
+ "./schemas/realtime-voice-changer-preset-pack.schema.json": "./dist/schemas/realtime-voice-changer-preset-pack.schema.json",
19
29
  "./package.json": "./package.json"
20
30
  },
21
31
  "files": [
@@ -25,19 +35,24 @@
25
35
  "sideEffects": false,
26
36
  "scripts": {
27
37
  "build": "yarn build:wasm && yarn build:js",
28
- "build:wasm": "mkdir -p build-wasm/em-cache && EM_CACHE=$PWD/build-wasm/em-cache emcmake cmake -S ../.. -B build-wasm -DBUILD_WASM=ON -DBUILD_MASTERING=ON -DCMAKE_BUILD_TYPE=Release && EM_CACHE=$PWD/build-wasm/em-cache cmake --build build-wasm",
38
+ "build:wasm": "yarn build:wasm:full && yarn build:wasm:analysis",
39
+ "build:wasm:full": "mkdir -p build-wasm/em-cache && EM_NODE_JS=\"$(mise which node 2>/dev/null || command -v node)\" EM_CACHE=$PWD/build-wasm/em-cache emcmake cmake -S ../.. -B build-wasm -DBUILD_WASM=ON -DSONARE_WASM_ANALYSIS_ONLY=OFF -DSONARE_WASM_OUTPUT_NAME=sonare -DBUILD_MASTERING=ON -DBUILD_MIXING=ON -DBUILD_GRAPH=ON -DBUILD_FX=ON -DBUILD_ACOUSTIC_SIM=ON -DBUILD_PITCH_EDITOR=ON -DBUILD_VOICE_CHANGER=ON -DBUILD_ARRANGEMENT=ON -DBUILD_ASSIST=ON -DCMAKE_BUILD_TYPE=Release && EM_NODE_JS=\"$(mise which node 2>/dev/null || command -v node)\" EM_CACHE=$PWD/build-wasm/em-cache cmake --build build-wasm",
40
+ "build:wasm:analysis": "mkdir -p build-wasm-analysis/em-cache && EM_NODE_JS=\"$(mise which node 2>/dev/null || command -v node)\" EM_CACHE=$PWD/build-wasm-analysis/em-cache emcmake cmake -S ../.. -B build-wasm-analysis -DBUILD_WASM=ON -DSONARE_WASM_ANALYSIS_ONLY=ON -DSONARE_WASM_OUTPUT_NAME=sonare-analysis -DBUILD_MASTERING=OFF -DBUILD_MIXING=OFF -DBUILD_GRAPH=OFF -DBUILD_FX=OFF -DBUILD_ACOUSTIC_SIM=OFF -DBUILD_VOICE_CHANGER=OFF -DBUILD_ARRANGEMENT=OFF -DBUILD_ASSIST=OFF -DCMAKE_BUILD_TYPE=Release && EM_NODE_JS=\"$(mise which node 2>/dev/null || command -v node)\" EM_CACHE=$PWD/build-wasm-analysis/em-cache cmake --build build-wasm-analysis --target sonare",
41
+ "check:wasm-size": "python3 ../../tools/check_wasm_size.py --baseline wasm-size-baseline.json --artifact sonare.wasm=dist/sonare.wasm --artifact sonare-analysis.wasm=dist/sonare-analysis.wasm",
29
42
  "bench:wasm:isp": "mkdir -p build-wasm-bench/em-cache /tmp/libsonare-wasm-bench && EM_CACHE=$PWD/build-wasm-bench/em-cache emcmake cmake -S ../.. -B build-wasm-bench -DBUILD_WASM=ON -DBUILD_MASTERING=ON -DBUILD_BENCH=ON -DBUILD_TESTING=OFF -DBUILD_CLI=OFF -DCMAKE_BUILD_TYPE=Release && EM_CACHE=$PWD/build-wasm-bench/em-cache cmake --build build-wasm-bench --target sonare_mastering_isp_bench && cp build-wasm-bench/bin/sonare_mastering_isp_bench.js /tmp/libsonare-wasm-bench/sonare_mastering_isp_bench.cjs && cp build-wasm-bench/bin/sonare_mastering_isp_bench.wasm /tmp/libsonare-wasm-bench/sonare_mastering_isp_bench.wasm && node /tmp/libsonare-wasm-bench/sonare_mastering_isp_bench.cjs",
30
43
  "bench:wasm:phase-vocoder": "mkdir -p build-wasm-bench/em-cache /tmp/libsonare-wasm-bench && EM_CACHE=$PWD/build-wasm-bench/em-cache emcmake cmake -S ../.. -B build-wasm-bench -DBUILD_WASM=ON -DBUILD_BENCH=ON -DBUILD_TESTING=OFF -DBUILD_CLI=OFF -DCMAKE_BUILD_TYPE=Release && EM_CACHE=$PWD/build-wasm-bench/em-cache cmake --build build-wasm-bench --target sonare_phase_vocoder_streaming_bench && cp build-wasm-bench/bin/sonare_phase_vocoder_streaming_bench.js /tmp/libsonare-wasm-bench/sonare_phase_vocoder_streaming_bench.cjs && cp build-wasm-bench/bin/sonare_phase_vocoder_streaming_bench.wasm /tmp/libsonare-wasm-bench/sonare_phase_vocoder_streaming_bench.wasm && node /tmp/libsonare-wasm-bench/sonare_phase_vocoder_streaming_bench.cjs",
31
- "build:js": "tsup",
32
- "clean": "rm -rf dist build-wasm LICENSE",
44
+ "build:js": "tsup && node scripts/copy-schemas.mjs",
45
+ "clean": "rm -rf dist build-wasm build-wasm-analysis LICENSE",
33
46
  "lint": "biome check src/ tests/",
34
47
  "lint:fix": "biome check --write src/ tests/",
35
48
  "format": "biome format --write src/ tests/",
36
49
  "test": "vitest run",
50
+ "test:types": "tsc --noEmit --ignoreConfig --strict --module ESNext --moduleResolution bundler --target ES2020 tests/typing/analysis-entry.ts",
37
51
  "test:browser": "node scripts/audio-worklet-smoke.mjs",
38
- "test:browser:audit": "node scripts/audit-boundary-smoke.mjs",
52
+ "test:browser:worker": "node scripts/offline-worker-smoke.mjs",
53
+ "test:browser:boundary": "node scripts/boundary-smoke.mjs",
39
54
  "test:watch": "vitest",
40
- "prepublishOnly": "yarn clean && cp ../../LICENSE LICENSE && yarn build && yarn lint && yarn test"
55
+ "prepublishOnly": "yarn clean && cp ../../LICENSE LICENSE && yarn build && yarn lint && yarn test && yarn test:types"
41
56
  },
42
57
  "keywords": [
43
58
  "audio",
@@ -76,12 +91,9 @@
76
91
  },
77
92
  "devDependencies": {
78
93
  "@biomejs/biome": "^2.4.16",
94
+ "ajv": "^8.17.1",
79
95
  "tsup": "^8.5.1",
80
96
  "typescript": "^6.0.3",
81
97
  "vitest": "^4.1.8"
82
- },
83
- "volta": {
84
- "node": "22.22.3",
85
- "yarn": "4.15.0"
86
98
  }
87
99
  }
@@ -4,9 +4,9 @@ type ChainSection = { [key: string]: number | boolean | ChainSection | undefined
4
4
 
5
5
  /**
6
6
  * Flattens a nested {@link MasteringChainConfig} into the dot-notation
7
- * `{ "module.processor.param": value }` map the WASM core consumes. Internal
8
- * helper shared by the mastering-chain / master-audio entry points; mirrors the
9
- * Node facade so a nested config produces identical overrides on both surfaces.
7
+ * `{ "module.processor.param": value }` map the core consumes. Internal helper
8
+ * shared by the mastering-chain / master-audio entry points. The core owns
9
+ * legacy flat-key aliases, so this remains a structural flattening step.
10
10
  */
11
11
  export function flattenChainConfig(config: MasteringChainConfig): Record<string, number | boolean> {
12
12
  const out: Record<string, number | boolean> = {};
@@ -17,6 +17,8 @@ export function flattenChainConfig(config: MasteringChainConfig): Record<string,
17
17
  out[path] = value;
18
18
  } else if (value !== null && typeof value === 'object') {
19
19
  walk(value, path);
20
+ } else if (value !== undefined) {
21
+ throw new TypeError(`Mastering override '${path}' must be a number or boolean.`);
20
22
  }
21
23
  }
22
24
  };
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Analysis and feature-extraction entry point for the smaller WASM binary.
3
+ *
4
+ * This entry intentionally omits mastering, mixing, realtime engines, Project,
5
+ * and other native-handle APIs. Import the package root when those surfaces are
6
+ * required.
7
+ */
8
+
9
+ import { setSonareModule } from './module_state';
10
+ import type { SonareCapabilities } from './public_types';
11
+ import type { SonareModule } from './sonare.js';
12
+
13
+ export { ErrorCode, isSonareError, SonareError } from './errors';
14
+ export * from './feature_core';
15
+ export * from './feature_music';
16
+ export * from './feature_pitch';
17
+ export * from './feature_resample';
18
+ export * from './feature_spectral';
19
+ export * from './feature_spectrogram';
20
+ export * from './metering';
21
+ export * from './public_types';
22
+ export type {
23
+ AnalyzeBpmRequest,
24
+ AnalyzeDynamicsRequest,
25
+ AnalyzeImpulseResponseRequest,
26
+ AnalyzeRhythmRequest,
27
+ AnalyzeTimbreRequest,
28
+ AnalyzeWithProgressRequest,
29
+ BpmAnalysisResult,
30
+ BpmCandidate,
31
+ ChordFunctionalAnalysisRequest,
32
+ DetectAcousticRequest,
33
+ DetectChordsRequest,
34
+ DetectKeyRequest,
35
+ DynamicsAnalysisResult,
36
+ MusicAnalyzeOptions,
37
+ MusicAnalyzeRequest,
38
+ RhythmAnalysisResult,
39
+ SamplesRequest,
40
+ TimbreAnalysisResult,
41
+ TimbreFrame,
42
+ } from './quick_analysis';
43
+ export {
44
+ analyze,
45
+ analyzeBpm,
46
+ analyzeDynamics,
47
+ analyzeImpulseResponse,
48
+ analyzeRhythm,
49
+ analyzeTimbre,
50
+ analyzeWithProgress,
51
+ chordFunctionalAnalysis,
52
+ detectAcoustic,
53
+ detectBeats,
54
+ detectBpm,
55
+ detectChords,
56
+ detectDownbeats,
57
+ detectKey,
58
+ detectKeyCandidates,
59
+ detectOnsets,
60
+ hasFfmpegSupport,
61
+ } from './quick_analysis';
62
+
63
+ let module: SonareModule | null = null;
64
+ let initPromise: Promise<void> | null = null;
65
+
66
+ /** Initialize the analysis-only WASM module. */
67
+ export async function init(options?: {
68
+ locateFile?: (path: string, prefix: string) => string;
69
+ wasmBinary?: ArrayBuffer | Uint8Array;
70
+ moduleFactory?: (options?: {
71
+ locateFile?: (path: string, prefix: string) => string;
72
+ wasmBinary?: ArrayBuffer | Uint8Array;
73
+ }) => Promise<SonareModule>;
74
+ }): Promise<void> {
75
+ if (module) {
76
+ return;
77
+ }
78
+ if (initPromise) {
79
+ return initPromise;
80
+ }
81
+ initPromise = (async () => {
82
+ try {
83
+ const createModule = options?.moduleFactory ?? (await import('./sonare-analysis.js')).default;
84
+ module = await createModule(options);
85
+ setSonareModule(module);
86
+ } catch (error) {
87
+ initPromise = null;
88
+ throw error;
89
+ }
90
+ })();
91
+ return initPromise;
92
+ }
93
+
94
+ /** Whether this analysis entry has loaded its WASM module. */
95
+ export function isInitialized(): boolean {
96
+ return module !== null;
97
+ }
98
+
99
+ /** Version reported by the loaded analysis WASM module. */
100
+ export function version(): string {
101
+ if (!module) {
102
+ throw new Error('Module not initialized. Call init() first.');
103
+ }
104
+ return module.version();
105
+ }
106
+
107
+ /** Build capabilities for the loaded analysis-only module. */
108
+ export function capabilities(): SonareCapabilities {
109
+ if (!module) {
110
+ throw new Error('Module not initialized. Call init() first.');
111
+ }
112
+ return module.capabilities();
113
+ }
114
+
115
+ /** Packed C-ABI version for compatibility checks. */
116
+ export function abiVersion(): number {
117
+ if (!module) {
118
+ throw new Error('Module not initialized. Call init() first.');
119
+ }
120
+ return module.abiVersion();
121
+ }
122
+
123
+ /** Realtime command-queue ABI version shared with the full entry. */
124
+ export function engineAbiVersion(): number {
125
+ if (!module) {
126
+ throw new Error('Module not initialized. Call init() first.');
127
+ }
128
+ return module.engineAbiVersion();
129
+ }
130
+
131
+ /** Voice-changer ABI version retained for cross-entry compatibility checks. */
132
+ export function voiceChangerAbiVersion(): number {
133
+ if (!module) {
134
+ throw new Error('Module not initialized. Call init() first.');
135
+ }
136
+ return module.voiceChangerAbiVersion();
137
+ }
@@ -137,6 +137,11 @@ export function convertAnalysisResult(wasm: WasmAnalysisResult): AnalysisResult
137
137
  return {
138
138
  bpm: wasm.bpm,
139
139
  bpmConfidence: wasm.bpmConfidence,
140
+ bpmCandidates: wasm.bpmCandidates.map((candidate) => ({
141
+ value: candidate.value,
142
+ confidence: candidate.confidence,
143
+ relation: candidate.relation,
144
+ })),
140
145
  key: {
141
146
  root: wasm.key.root as PitchClass,
142
147
  mode: wasm.key.mode as Mode,
@@ -145,6 +150,7 @@ export function convertAnalysisResult(wasm: WasmAnalysisResult): AnalysisResult
145
150
  shortName: wasm.key.shortName,
146
151
  },
147
152
  timeSignature: wasm.timeSignature,
153
+ timeSignatureCandidates: wasm.timeSignatureCandidates,
148
154
  beatTimes,
149
155
  beats: wasm.beats,
150
156
  chords: wasm.chords.map((c) => ({
@@ -11,7 +11,17 @@ import type { ClipPageProvider, ClipPageRequest, RealtimeEngine } from './realti
11
11
  */
12
12
  export interface ClipPageStreamerEngine {
13
13
  /** Drain one pending audio-thread page-miss request, or `null` when empty. */
14
- popClipPageRequest(): ClipPageRequest | null;
14
+ popClipPageRequest(): ClipPageStreamerRequest | null;
15
+ }
16
+
17
+ /**
18
+ * A page miss reported by either a direct engine (`sample`) or the worklet
19
+ * bridge (`pageIndex`). Exactly one position form is required.
20
+ */
21
+ export interface ClipPageStreamerRequest {
22
+ clipId: number;
23
+ sample?: number;
24
+ pageIndex?: number;
15
25
  }
16
26
 
17
27
  /** A paged clip the streamer keeps fed from its backing store. */
@@ -153,7 +163,13 @@ export class ClipPageStreamer {
153
163
  if (!state) {
154
164
  continue;
155
165
  }
156
- const page = Math.floor(request.sample / state.source.pageFrames);
166
+ const page =
167
+ request.pageIndex !== undefined
168
+ ? request.pageIndex
169
+ : Math.floor((request.sample ?? Number.NaN) / state.source.pageFrames);
170
+ if (!Number.isInteger(page) || page < 0 || page > state.lastPage) {
171
+ continue;
172
+ }
157
173
  frontiers.set(request.clipId, page);
158
174
  }
159
175
 
@@ -197,7 +213,7 @@ export class ClipPageStreamer {
197
213
  // the bound by transiently holding old pages alongside new ones.
198
214
  for (const page of state.resident.keys()) {
199
215
  if (page < low || page > high) {
200
- state.source.binding.provider.clear(page);
216
+ this.clearPage(state, page);
201
217
  state.resident.delete(page);
202
218
  }
203
219
  }
@@ -217,7 +233,7 @@ export class ClipPageStreamer {
217
233
  if (state.generation !== generation) {
218
234
  // The fetch crossed a seek/reset boundary. supplyPage may have
219
235
  // installed it after resetState's clear pass, so clear it again.
220
- state.source.binding.provider.clear(pageIndex);
236
+ this.clearPage(state, pageIndex);
221
237
  } else if (!ok && state.resident.get(pageIndex) === generation) {
222
238
  state.resident.delete(pageIndex);
223
239
  }
@@ -239,10 +255,20 @@ export class ClipPageStreamer {
239
255
  state.generation += 1;
240
256
  state.lastFrontier = null;
241
257
  for (const page of state.resident.keys()) {
242
- state.source.binding.provider.clear(page);
258
+ this.clearPage(state, page);
243
259
  }
244
260
  state.resident.clear();
245
261
  }
262
+
263
+ private clearPage(state: SourceState, pageIndex: number): void {
264
+ // `clearPage` mirrors eviction into the AudioWorklet. Keep the older
265
+ // provider-only form working for direct-engine callers and test doubles.
266
+ if (state.source.binding.clearPage) {
267
+ state.source.binding.clearPage(pageIndex);
268
+ } else {
269
+ state.source.binding.provider.clear(pageIndex);
270
+ }
271
+ }
246
272
  }
247
273
 
248
274
  export interface OpfsClipStreamOptions extends OpfsClipPageProviderOptions {
@@ -257,25 +283,53 @@ export interface OpfsClipStreamOptions extends OpfsClipPageProviderOptions {
257
283
 
258
284
  export interface OpfsClipStream {
259
285
  binding: OpfsClipPageProviderBinding;
260
- /** Pass to `setClips({ pageProvider })` to schedule the streaming clip. */
286
+ /** Pass as `addClip`'s buffer argument to schedule the streaming clip. */
261
287
  provider: ClipPageProvider;
262
288
  }
263
289
 
290
+ /** Structural seam implemented by the AudioWorklet-backed `SonareEngine`. */
291
+ export interface WorkletOpfsClipStreamHost {
292
+ attachOpfsClipStream(options: OpfsClipStreamOptions): Promise<OpfsClipStream>;
293
+ }
294
+
264
295
  /**
265
296
  * One-call wiring of an OPFS-backed streaming clip: creates the page provider,
266
297
  * primes the leading pages, and registers it with `streamer` so later misses are
267
298
  * serviced within the bounded window. Returns the binding (for `close`) and the
268
- * provider to schedule via `setClips({ pageProvider })`.
299
+ * provider to schedule via `addClip(trackId, provider, startPpq, { id: clipId })`.
269
300
  *
270
301
  * @param streamer Shared streamer pumped on the control thread.
271
302
  * @param engine Engine the provider is created on (the same one `streamer` drives).
272
303
  * @param options Provider options plus `clipId` and optional `primePages`.
273
304
  */
274
- export async function attachOpfsClipStream(
305
+ export function attachOpfsClipStream(
275
306
  streamer: ClipPageStreamer,
276
307
  engine: RealtimeEngine,
277
308
  options: OpfsClipStreamOptions,
309
+ ): Promise<OpfsClipStream>;
310
+ /**
311
+ * Worklet overload: delegates to `SonareEngine.attachOpfsClipStream`, which
312
+ * creates the remote provider and drives this same sliding-window policy from
313
+ * worklet page-miss messages.
314
+ */
315
+ export function attachOpfsClipStream(
316
+ engine: WorkletOpfsClipStreamHost,
317
+ options: OpfsClipStreamOptions,
318
+ ): Promise<OpfsClipStream>;
319
+ export async function attachOpfsClipStream(
320
+ streamerOrEngine: ClipPageStreamer | WorkletOpfsClipStreamHost,
321
+ engineOrOptions: RealtimeEngine | OpfsClipStreamOptions,
322
+ maybeOptions?: OpfsClipStreamOptions,
278
323
  ): Promise<OpfsClipStream> {
324
+ if (!(streamerOrEngine instanceof ClipPageStreamer)) {
325
+ return streamerOrEngine.attachOpfsClipStream(engineOrOptions as OpfsClipStreamOptions);
326
+ }
327
+ const streamer = streamerOrEngine;
328
+ const engine = engineOrOptions as RealtimeEngine;
329
+ const options = maybeOptions;
330
+ if (!options) {
331
+ throw new Error('attachOpfsClipStream requires options.');
332
+ }
279
333
  const { clipId, primePages = 1, ...providerOptions } = options;
280
334
  const binding = createOpfsClipPageProvider(engine, providerOptions);
281
335
  const lastPage = Math.ceil(providerOptions.numSamples / providerOptions.pageFrames) - 1;
package/src/codes.ts CHANGED
@@ -1,68 +1,65 @@
1
1
  import type { AutomationCurve, MeterTap, PanLaw, PanMode, SendTiming } from './public_types';
2
2
 
3
- export function automationCurveCode(curve: AutomationCurve): number {
4
- switch (curve) {
5
- case 'linear':
6
- return 0;
7
- case 'exponential':
8
- return 1;
9
- case 'hold':
10
- return 2;
11
- case 's-curve':
12
- return 3;
13
- default:
14
- throw new Error(`Invalid automation curve: ${curve}`);
3
+ /** Resolve a public enum spelling or ordinal without permitting unknown values. */
4
+ export function resolveEnumOrdinal(
5
+ value: unknown,
6
+ values: Readonly<Record<string, number>>,
7
+ enumName: string,
8
+ ): number {
9
+ if (typeof value === 'number') {
10
+ if (!Number.isSafeInteger(value) || !Object.values(values).includes(value)) {
11
+ throw new RangeError(`Invalid ${enumName}: ${String(value)}`);
12
+ }
13
+ return value;
14
+ }
15
+ if (typeof value === 'string') {
16
+ const ordinal = values[value];
17
+ if (ordinal !== undefined) {
18
+ return ordinal;
19
+ }
15
20
  }
21
+ throw new RangeError(`Invalid ${enumName}: ${String(value)}`);
22
+ }
23
+
24
+ const AUTOMATION_CURVE_VALUES = {
25
+ linear: 0,
26
+ exponential: 1,
27
+ hold: 2,
28
+ 's-curve': 3,
29
+ } as const;
30
+ const PAN_LAW_VALUES = {
31
+ const3dB: 0,
32
+ 'const4.5dB': 1,
33
+ const6dB: 2,
34
+ linear0dB: 3,
35
+ } as const;
36
+ const PAN_MODE_VALUES = {
37
+ balance: 0,
38
+ pan: 0,
39
+ stereopan: 1,
40
+ 'stereo-pan': 1,
41
+ dualpan: 2,
42
+ 'dual-pan': 2,
43
+ } as const;
44
+ const METER_TAP_VALUES = { preFader: 0, postFader: 1 } as const;
45
+ const SEND_TIMING_VALUES = { postFader: 0, preFader: 1 } as const;
46
+
47
+ export function automationCurveCode(curve: AutomationCurve): number {
48
+ return resolveEnumOrdinal(curve, AUTOMATION_CURVE_VALUES, 'automation curve');
16
49
  }
17
50
 
18
51
  export function panLawCode(panLaw: PanLaw | number): number {
19
- if (typeof panLaw === 'number') {
20
- return panLaw;
21
- }
22
- switch (panLaw) {
23
- case 'const3dB':
24
- return 0;
25
- case 'const4.5dB':
26
- return 1;
27
- case 'const6dB':
28
- return 2;
29
- case 'linear0dB':
30
- return 3;
31
- default:
32
- throw new Error(`Invalid pan law: ${panLaw}`);
33
- }
52
+ return resolveEnumOrdinal(panLaw, PAN_LAW_VALUES, 'pan law');
34
53
  }
35
54
 
36
55
  export function panModeCode(panMode: PanMode | number): number {
37
- if (typeof panMode === 'number') {
38
- return panMode;
39
- }
40
- switch (panMode) {
41
- case 'balance':
42
- return 0;
43
- case 'stereoPan':
44
- case 'stereo-pan':
45
- return 1;
46
- case 'dualPan':
47
- case 'dual-pan':
48
- return 2;
49
- default:
50
- throw new Error(`Invalid pan mode: ${panMode}`);
51
- }
56
+ const normalized =
57
+ typeof panMode === 'string' ? panMode.replace(/_/g, '-').toLowerCase() : panMode;
58
+ return resolveEnumOrdinal(normalized, PAN_MODE_VALUES, 'pan mode');
52
59
  }
53
60
 
54
61
  export function meterTapCode(tap: MeterTap | number): number {
55
- if (typeof tap === 'number') {
56
- return tap;
57
- }
58
- switch (tap) {
59
- case 'preFader':
60
- return 0;
61
- case 'postFader':
62
- return 1;
63
- default:
64
- throw new Error(`Invalid meter tap: ${tap}`);
65
- }
62
+ return resolveEnumOrdinal(tap, METER_TAP_VALUES, 'meter tap');
66
63
  }
67
64
 
68
65
  export function sendTimingCode(timing: SendTiming | number): number {
@@ -70,15 +67,5 @@ export function sendTimingCode(timing: SendTiming | number): number {
70
67
  // post-fader), pre-fader is 1. A raw number is passed through as the C ABI int.
71
68
  // An unknown string is rejected rather than silently routed to post-fader,
72
69
  // matching the sibling enum-code helpers and Node's sendTimingValue.
73
- if (typeof timing === 'number') {
74
- return timing;
75
- }
76
- switch (timing) {
77
- case 'postFader':
78
- return 0;
79
- case 'preFader':
80
- return 1;
81
- default:
82
- throw new Error(`Invalid send timing: ${timing}`);
83
- }
70
+ return resolveEnumOrdinal(timing, SEND_TIMING_VALUES, 'send timing');
84
71
  }
@@ -38,6 +38,7 @@ export type {
38
38
  MasteringProcessorCatalogEntry,
39
39
  MasteringProcessRequest,
40
40
  MasteringProcessStereoRequest,
41
+ MasteringRealtimeCost,
41
42
  MasteringSamplesParamsRequest,
42
43
  MasteringStereoAnalyzeRequest,
43
44
  MasteringStreamingPreviewRequest,
package/src/errors.ts CHANGED
@@ -12,6 +12,7 @@ export enum ErrorCode {
12
12
  OutOfMemory = 5,
13
13
  NotSupported = 6,
14
14
  InvalidState = 7,
15
+ Cancelled = 8,
15
16
  Unknown = 99,
16
17
  }
17
18