@libraz/libsonare 1.4.0 → 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.
- package/README.md +50 -19
- package/dist/index.d.ts +5384 -1
- package/dist/index.js +884 -574
- package/dist/index.js.map +1 -1
- package/dist/sonare.js +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/worklet.d.ts +1084 -5220
- package/dist/worklet.js +2677 -2452
- package/dist/worklet.js.map +1 -1
- package/package.json +4 -9
- package/src/clip_page_streamer.ts +250 -0
- package/src/effects_mastering.ts +85 -1082
- package/src/effects_transform.ts +286 -0
- package/src/effects_voice_change.ts +118 -0
- package/src/feature_music.ts +13 -9
- package/src/feature_spectrogram.ts +42 -2
- package/src/features.ts +1 -0
- package/src/index.ts +11 -0
- package/src/mastering_chain.ts +200 -0
- package/src/mastering_core.ts +248 -0
- package/src/mastering_dynamics.ts +105 -0
- package/src/mastering_repair.ts +161 -0
- package/src/mixing_oneshot.ts +54 -0
- package/src/module_state.ts +1 -2
- package/src/project.ts +71 -1688
- package/src/project_class.ts +861 -0
- package/src/project_internal.ts +332 -0
- package/src/project_synth.ts +43 -0
- package/src/project_types.ts +570 -0
- package/src/public_types.ts +6 -1217
- package/src/public_types_acoustic.ts +115 -0
- package/src/public_types_mastering.ts +333 -0
- package/src/public_types_mixing.ts +97 -0
- package/src/public_types_music.ts +352 -0
- package/src/public_types_realtime.ts +163 -0
- package/src/public_types_spectral.ts +194 -0
- package/src/quick_analysis.ts +18 -14
- package/src/realtime_engine.ts +94 -0
- package/src/realtime_voice_changer.ts +2 -1
- package/src/sonare.js.d.ts +74 -0
- package/src/worklet/engine-automation.ts +73 -0
- package/src/worklet/engine-capture-facade.ts +80 -0
- package/src/worklet/engine-clips.ts +71 -0
- package/src/worklet/engine-markers.ts +93 -0
- package/src/worklet/engine-mixer-facade.ts +186 -0
- package/src/worklet/engine-node.ts +451 -0
- package/src/worklet/engine-offline.ts +162 -0
- package/src/worklet/engine-options.ts +13 -0
- package/src/worklet/engine-parameter-facade.ts +172 -0
- package/src/worklet/engine-processor.ts +764 -0
- package/src/worklet/engine-register.ts +136 -0
- package/src/worklet/engine-strips.ts +315 -0
- package/src/worklet/engine-sync.ts +94 -0
- package/src/worklet/engine-tempo-facade.ts +141 -0
- package/src/worklet/engine.ts +998 -0
- package/src/worklet/guards.ts +14 -1
- package/src/worklet/messages.ts +60 -20
- package/src/worklet/mixer-processor.ts +368 -0
- package/src/worklet/protocol.ts +3 -0
- package/src/worklet/voice-changer-processor.ts +246 -0
- package/src/worklet.ts +20 -3549
- package/dist/sonare-rt-module.js +0 -2
- package/dist/sonare-rt.js +0 -2
- package/dist/sonare-rt.wasm +0 -0
- 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.
|
|
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:
|
|
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
|
|
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": "
|
|
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
|
+
}
|