@libraz/libsonare 1.4.1 → 1.5.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 (62) hide show
  1. package/README.md +50 -19
  2. package/dist/index.d.ts +5384 -1
  3. package/dist/index.js +867 -573
  4. package/dist/index.js.map +1 -1
  5. package/dist/sonare.js +1 -1
  6. package/dist/sonare.wasm +0 -0
  7. package/dist/worklet.d.ts +1083 -5227
  8. package/dist/worklet.js +2677 -2452
  9. package/dist/worklet.js.map +1 -1
  10. package/package.json +4 -9
  11. package/src/clip_page_streamer.ts +250 -0
  12. package/src/effects_mastering.ts +85 -1089
  13. package/src/effects_transform.ts +286 -0
  14. package/src/effects_voice_change.ts +118 -0
  15. package/src/feature_spectrogram.ts +42 -2
  16. package/src/features.ts +1 -0
  17. package/src/index.ts +11 -0
  18. package/src/mastering_chain.ts +200 -0
  19. package/src/mastering_core.ts +248 -0
  20. package/src/mastering_dynamics.ts +105 -0
  21. package/src/mastering_repair.ts +161 -0
  22. package/src/mixing_oneshot.ts +54 -0
  23. package/src/module_state.ts +1 -2
  24. package/src/project.ts +71 -1712
  25. package/src/project_class.ts +861 -0
  26. package/src/project_internal.ts +332 -0
  27. package/src/project_synth.ts +43 -0
  28. package/src/project_types.ts +570 -0
  29. package/src/public_types.ts +6 -1221
  30. package/src/public_types_acoustic.ts +115 -0
  31. package/src/public_types_mastering.ts +333 -0
  32. package/src/public_types_mixing.ts +97 -0
  33. package/src/public_types_music.ts +352 -0
  34. package/src/public_types_realtime.ts +163 -0
  35. package/src/public_types_spectral.ts +194 -0
  36. package/src/realtime_engine.ts +94 -0
  37. package/src/sonare.js.d.ts +72 -0
  38. package/src/worklet/engine-automation.ts +73 -0
  39. package/src/worklet/engine-capture-facade.ts +80 -0
  40. package/src/worklet/engine-clips.ts +71 -0
  41. package/src/worklet/engine-markers.ts +93 -0
  42. package/src/worklet/engine-mixer-facade.ts +186 -0
  43. package/src/worklet/engine-node.ts +451 -0
  44. package/src/worklet/engine-offline.ts +162 -0
  45. package/src/worklet/engine-options.ts +13 -0
  46. package/src/worklet/engine-parameter-facade.ts +172 -0
  47. package/src/worklet/engine-processor.ts +764 -0
  48. package/src/worklet/engine-register.ts +136 -0
  49. package/src/worklet/engine-strips.ts +315 -0
  50. package/src/worklet/engine-sync.ts +94 -0
  51. package/src/worklet/engine-tempo-facade.ts +141 -0
  52. package/src/worklet/engine.ts +998 -0
  53. package/src/worklet/guards.ts +14 -1
  54. package/src/worklet/messages.ts +60 -20
  55. package/src/worklet/mixer-processor.ts +368 -0
  56. package/src/worklet/protocol.ts +3 -0
  57. package/src/worklet/voice-changer-processor.ts +246 -0
  58. package/src/worklet.ts +20 -3549
  59. package/dist/sonare-rt-module.js +0 -2
  60. package/dist/sonare-rt.js +0 -2
  61. package/dist/sonare-rt.wasm +0 -0
  62. package/src/sonare-rt.d.ts +0 -93
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@libraz/libsonare",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "packageManager": "yarn@4.15.0",
6
6
  "description": "Audio analysis library for music information retrieval",
@@ -16,11 +16,6 @@
16
16
  "import": "./dist/worklet.js"
17
17
  },
18
18
  "./wasm": "./dist/sonare.wasm",
19
- "./rt": {
20
- "types": "./src/sonare-rt.d.ts",
21
- "import": "./dist/sonare-rt.js"
22
- },
23
- "./rt-wasm": "./dist/sonare-rt.wasm",
24
19
  "./package.json": "./package.json"
25
20
  },
26
21
  "files": [
@@ -29,11 +24,11 @@
29
24
  ],
30
25
  "sideEffects": false,
31
26
  "scripts": {
32
- "build": "yarn build:js && yarn build:wasm",
33
- "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 && node scripts/write-rt-module-variant.mjs",
27
+ "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",
34
29
  "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",
35
30
  "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",
36
- "build:js": "find dist -maxdepth 1 -type f \\( -name 'chunk-*.js' -o -name 'chunk-*.js.map' -o -name '*-*.d.ts' \\) -delete 2>/dev/null; tsup",
31
+ "build:js": "tsup",
37
32
  "clean": "rm -rf dist build-wasm LICENSE",
38
33
  "lint": "biome check src/ tests/",
39
34
  "lint:fix": "biome check --write src/ tests/",
@@ -0,0 +1,250 @@
1
+ import {
2
+ createOpfsClipPageProvider,
3
+ type OpfsClipPageProviderBinding,
4
+ type OpfsClipPageProviderOptions,
5
+ } from './opfs_clip_pages';
6
+ import type { ClipPageProvider, ClipPageRequest, RealtimeEngine } from './realtime_engine';
7
+
8
+ /**
9
+ * Minimal engine surface the streamer drives. {@link RealtimeEngine} satisfies
10
+ * this structurally; tests can supply a lightweight stand-in.
11
+ */
12
+ export interface ClipPageStreamerEngine {
13
+ /** Drain one pending audio-thread page-miss request, or `null` when empty. */
14
+ popClipPageRequest(): ClipPageRequest | null;
15
+ }
16
+
17
+ /** A paged clip the streamer keeps fed from its backing store. */
18
+ export interface ClipPageStreamSource {
19
+ /** Clip schedule id passed to `setClips` (matches {@link ClipPageRequest.clipId}). */
20
+ clipId: number;
21
+ /** OPFS-backed page provider binding for this clip. */
22
+ binding: OpfsClipPageProviderBinding;
23
+ /** Page size in frames (must equal the provider's `pageFrames`). */
24
+ pageFrames: number;
25
+ /** Total sample count of the clip source. */
26
+ numSamples: number;
27
+ }
28
+
29
+ export interface ClipPageStreamerOptions {
30
+ /**
31
+ * Pages to prefetch ahead of the page a miss was reported for. Larger values
32
+ * hide fetch latency at the cost of more resident memory. Default 2.
33
+ */
34
+ readAheadPages?: number;
35
+ /**
36
+ * Pages to retain behind the playback frontier before eviction, so a small
37
+ * backward seek does not immediately miss. Default 1.
38
+ */
39
+ retainBehindPages?: number;
40
+ /**
41
+ * Upper bound on requests drained per {@link ClipPageStreamer.pump} call, so a
42
+ * burst of misses cannot spin unbounded. Default 256.
43
+ */
44
+ maxRequestsPerPump?: number;
45
+ }
46
+
47
+ interface SourceState {
48
+ source: ClipPageStreamSource;
49
+ lastPage: number;
50
+ /** Page indices currently supplied to the provider (the resident set). */
51
+ resident: Set<number>;
52
+ }
53
+
54
+ /**
55
+ * Keeps OPFS-paged clips fed within a bounded sliding window around the live
56
+ * playback position, so a multitrack arrangement never holds its full PCM in
57
+ * WASM memory.
58
+ *
59
+ * The audio thread reports a page miss whenever the {@link ClipPlayer} reads a
60
+ * sample whose page is not resident. {@link pump} drains those requests, fetches
61
+ * the missing page plus a read-ahead window from each clip's backing store, and
62
+ * evicts pages that fall outside the window via the provider's `clear`. The
63
+ * resident set per clip is therefore bounded to
64
+ * `retainBehindPages + readAheadPages + 1` pages regardless of clip length.
65
+ *
66
+ * Call {@link pump} on a cadence that keeps up with playback — typically once
67
+ * per animation frame or per worklet control tick on the main/control thread
68
+ * (never the audio thread; fetches are asynchronous).
69
+ */
70
+ export class ClipPageStreamer {
71
+ private readonly engine: ClipPageStreamerEngine;
72
+ private readonly readAheadPages: number;
73
+ private readonly retainBehindPages: number;
74
+ private readonly maxRequestsPerPump: number;
75
+ private readonly sources = new Map<number, SourceState>();
76
+ private closed = false;
77
+
78
+ constructor(engine: ClipPageStreamerEngine, options: ClipPageStreamerOptions = {}) {
79
+ this.engine = engine;
80
+ this.readAheadPages = Math.max(0, Math.floor(options.readAheadPages ?? 2));
81
+ this.retainBehindPages = Math.max(0, Math.floor(options.retainBehindPages ?? 1));
82
+ this.maxRequestsPerPump = Math.max(1, Math.floor(options.maxRequestsPerPump ?? 256));
83
+ }
84
+
85
+ /**
86
+ * Register a paged clip. Pages already supplied to the provider before
87
+ * registration (for example a primed first page) should be passed in
88
+ * `initialResidentPages` so they participate in eviction.
89
+ */
90
+ addSource(source: ClipPageStreamSource, initialResidentPages: Iterable<number> = []): void {
91
+ if (source.pageFrames <= 0 || source.numSamples <= 0) {
92
+ throw new Error('pageFrames and numSamples must be positive');
93
+ }
94
+ const lastPage = Math.ceil(source.numSamples / source.pageFrames) - 1;
95
+ this.sources.set(source.clipId, {
96
+ source,
97
+ lastPage,
98
+ resident: new Set(initialResidentPages),
99
+ });
100
+ }
101
+
102
+ /** Stop tracking a clip. Does not close its binding (the caller owns that). */
103
+ removeSource(clipId: number): void {
104
+ this.sources.delete(clipId);
105
+ }
106
+
107
+ /**
108
+ * Drain pending page-miss requests, fetch the missing pages plus their
109
+ * read-ahead window, and evict out-of-window pages. Resolves once this round's
110
+ * fetches settle. Concurrent fetches are serialized inside each binding.
111
+ */
112
+ async pump(): Promise<void> {
113
+ if (this.closed) {
114
+ return;
115
+ }
116
+ // Collapse this round's misses to the furthest-advanced frontier per clip:
117
+ // multiple channels of one clip miss the same page, and a run of misses
118
+ // walks forward, so only the latest position needs servicing.
119
+ const frontiers = new Map<number, number>();
120
+ for (let drained = 0; drained < this.maxRequestsPerPump; ++drained) {
121
+ const request = this.engine.popClipPageRequest();
122
+ if (!request) {
123
+ break;
124
+ }
125
+ const state = this.sources.get(request.clipId);
126
+ if (!state) {
127
+ continue;
128
+ }
129
+ const page = Math.floor(request.sample / state.source.pageFrames);
130
+ const previous = frontiers.get(request.clipId);
131
+ if (previous === undefined || page > previous) {
132
+ frontiers.set(request.clipId, page);
133
+ }
134
+ }
135
+
136
+ const fetches: Promise<unknown>[] = [];
137
+ for (const [clipId, frontier] of frontiers) {
138
+ const state = this.sources.get(clipId);
139
+ if (!state) {
140
+ continue;
141
+ }
142
+ fetches.push(...this.serviceFrontier(state, frontier));
143
+ }
144
+ await Promise.all(fetches);
145
+ }
146
+
147
+ /** Close every registered clip's binding and stop tracking. */
148
+ close(): void {
149
+ if (this.closed) {
150
+ return;
151
+ }
152
+ this.closed = true;
153
+ for (const state of this.sources.values()) {
154
+ state.source.binding.close();
155
+ }
156
+ this.sources.clear();
157
+ }
158
+
159
+ private serviceFrontier(state: SourceState, frontier: number): Promise<unknown>[] {
160
+ const low = Math.max(0, frontier - this.retainBehindPages);
161
+ const high = Math.min(state.lastPage, frontier + this.readAheadPages);
162
+
163
+ // Evict pages outside the window first so a burst of fetches never exceeds
164
+ // the bound by transiently holding old pages alongside new ones.
165
+ for (const page of state.resident) {
166
+ if (page < low || page > high) {
167
+ state.source.binding.provider.clear(page);
168
+ state.resident.delete(page);
169
+ }
170
+ }
171
+
172
+ const fetches: Promise<unknown>[] = [];
173
+ for (let page = low; page <= high; ++page) {
174
+ if (state.resident.has(page)) {
175
+ continue;
176
+ }
177
+ // Mark resident eagerly so the same page is not fetched twice across
178
+ // overlapping windows; drop it again if the fetch reports a miss.
179
+ state.resident.add(page);
180
+ const pageIndex = page;
181
+ fetches.push(
182
+ state.source.binding.supplyPage(pageIndex).then(
183
+ (ok) => {
184
+ if (!ok) {
185
+ state.resident.delete(pageIndex);
186
+ }
187
+ return ok;
188
+ },
189
+ (error) => {
190
+ state.resident.delete(pageIndex);
191
+ throw error;
192
+ },
193
+ ),
194
+ );
195
+ }
196
+ return fetches;
197
+ }
198
+ }
199
+
200
+ export interface OpfsClipStreamOptions extends OpfsClipPageProviderOptions {
201
+ /** Clip schedule id used in `setClips` (matches the page-miss request clipId). */
202
+ clipId: number;
203
+ /**
204
+ * Leading pages fetched synchronously before returning, so playback can start
205
+ * without an immediate miss. Default 1.
206
+ */
207
+ primePages?: number;
208
+ }
209
+
210
+ export interface OpfsClipStream {
211
+ binding: OpfsClipPageProviderBinding;
212
+ /** Pass to `setClips({ pageProvider })` to schedule the streaming clip. */
213
+ provider: ClipPageProvider;
214
+ }
215
+
216
+ /**
217
+ * One-call wiring of an OPFS-backed streaming clip: creates the page provider,
218
+ * primes the leading pages, and registers it with `streamer` so later misses are
219
+ * serviced within the bounded window. Returns the binding (for `close`) and the
220
+ * provider to schedule via `setClips({ pageProvider })`.
221
+ *
222
+ * @param streamer Shared streamer pumped on the control thread.
223
+ * @param engine Engine the provider is created on (the same one `streamer` drives).
224
+ * @param options Provider options plus `clipId` and optional `primePages`.
225
+ */
226
+ export async function attachOpfsClipStream(
227
+ streamer: ClipPageStreamer,
228
+ engine: RealtimeEngine,
229
+ options: OpfsClipStreamOptions,
230
+ ): Promise<OpfsClipStream> {
231
+ const { clipId, primePages = 1, ...providerOptions } = options;
232
+ const binding = createOpfsClipPageProvider(engine, providerOptions);
233
+ const lastPage = Math.ceil(providerOptions.numSamples / providerOptions.pageFrames) - 1;
234
+ const primed: number[] = [];
235
+ for (let page = 0; page < primePages && page <= lastPage; ++page) {
236
+ if (await binding.supplyPage(page)) {
237
+ primed.push(page);
238
+ }
239
+ }
240
+ streamer.addSource(
241
+ {
242
+ clipId,
243
+ binding,
244
+ pageFrames: providerOptions.pageFrames,
245
+ numSamples: providerOptions.numSamples,
246
+ },
247
+ primed,
248
+ );
249
+ return { binding, provider: binding.provider };
250
+ }