@libraz/libsonare 1.2.2 → 1.3.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 +38 -1
- package/dist/index.d.ts +1 -2722
- package/dist/index.js +3659 -1896
- package/dist/index.js.map +1 -1
- package/dist/sonare-rt-module.js +1 -1
- package/dist/sonare-rt.js +1 -1
- package/dist/sonare-rt.wasm +0 -0
- package/dist/sonare.js +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/worklet.d.ts +4827 -455
- package/dist/worklet.js +1076 -494
- package/dist/worklet.js.map +1 -1
- package/package.json +2 -1
- package/src/analysis_helpers.ts +152 -0
- package/src/audio.ts +493 -0
- package/src/codes.ts +56 -0
- package/src/effects_mastering.ts +964 -0
- package/src/feature_core.ts +248 -0
- package/src/feature_music.ts +419 -0
- package/src/feature_pitch.ts +80 -0
- package/src/feature_resample.ts +21 -0
- package/src/feature_spectral.ts +330 -0
- package/src/feature_spectrogram.ts +454 -0
- package/src/features.ts +84 -0
- package/src/index.ts +352 -4793
- package/src/live_audio.ts +45 -0
- package/src/metering.ts +380 -0
- package/src/mixer.ts +523 -0
- package/src/module_state.ts +14 -0
- package/src/opfs_clip_pages.ts +188 -0
- package/src/project.ts +1614 -0
- package/src/public_types.ts +244 -2
- package/src/quick_analysis.ts +508 -0
- package/src/realtime_engine.ts +667 -0
- package/src/realtime_voice_changer.ts +275 -0
- package/src/scale.ts +42 -0
- package/src/sonare.js.d.ts +386 -4
- package/src/stream_analyzer.ts +275 -0
- package/src/stream_types.ts +29 -1
- package/src/streaming_mixing.ts +18 -0
- package/src/streaming_processors.ts +335 -0
- package/src/validation.ts +82 -0
- package/src/web_midi.ts +367 -0
- package/src/worklet.ts +525 -81
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { ClipPageProvider, ClipPageRequest, RealtimeEngine } from './realtime_engine';
|
|
2
|
+
|
|
3
|
+
export interface OpfsClipPageProviderOptions {
|
|
4
|
+
path: string;
|
|
5
|
+
numChannels: number;
|
|
6
|
+
numSamples: number;
|
|
7
|
+
pageFrames: number;
|
|
8
|
+
dataOffsetBytes?: number;
|
|
9
|
+
worker?: Worker;
|
|
10
|
+
terminateWorkerOnClose?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface OpfsClipPageProviderBinding {
|
|
14
|
+
provider: ClipPageProvider;
|
|
15
|
+
supplyPage(pageIndex: number): Promise<boolean>;
|
|
16
|
+
supplyRequest(request: ClipPageRequest): Promise<boolean>;
|
|
17
|
+
close(): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface PageResponse {
|
|
21
|
+
type: 'sonare:clip-page';
|
|
22
|
+
requestId: number;
|
|
23
|
+
pageIndex: number;
|
|
24
|
+
ok: boolean;
|
|
25
|
+
frames?: number;
|
|
26
|
+
channels?: Float32Array[];
|
|
27
|
+
channelBuffers?: ArrayBufferLike[];
|
|
28
|
+
error?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const opfsClipPageWorkerSource = `
|
|
32
|
+
self.onmessage = async (event) => {
|
|
33
|
+
const message = event.data;
|
|
34
|
+
if (!message || message.type !== 'sonare:read-clip-page') return;
|
|
35
|
+
const { requestId, path, pageIndex, numChannels, numSamples, pageFrames, dataOffsetBytes = 0 } = message;
|
|
36
|
+
try {
|
|
37
|
+
if (pageIndex < 0) {
|
|
38
|
+
self.postMessage({ type: 'sonare:clip-page', requestId, pageIndex, ok: false });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const startFrame = pageIndex * pageFrames;
|
|
42
|
+
if (startFrame >= numSamples) {
|
|
43
|
+
self.postMessage({ type: 'sonare:clip-page', requestId, pageIndex, ok: false });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const root = await self.navigator.storage.getDirectory();
|
|
47
|
+
let dir = root;
|
|
48
|
+
const parts = String(path).split('/').filter(Boolean);
|
|
49
|
+
for (let i = 0; i < parts.length - 1; ++i) {
|
|
50
|
+
dir = await dir.getDirectoryHandle(parts[i]);
|
|
51
|
+
}
|
|
52
|
+
const fileHandle = await dir.getFileHandle(parts[parts.length - 1]);
|
|
53
|
+
const access = await fileHandle.createSyncAccessHandle();
|
|
54
|
+
try {
|
|
55
|
+
const frames = Math.min(pageFrames, numSamples - startFrame);
|
|
56
|
+
const frameBytes = numChannels * 4;
|
|
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) {
|
|
61
|
+
self.postMessage({ type: 'sonare:clip-page', requestId, pageIndex, ok: false });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const view = new DataView(bytes.buffer, 0, framesRead * frameBytes);
|
|
65
|
+
const channelBuffers = Array.from({ length: numChannels }, () => new ArrayBuffer(framesRead * 4));
|
|
66
|
+
for (let ch = 0; ch < numChannels; ++ch) {
|
|
67
|
+
const channel = new Float32Array(channelBuffers[ch]);
|
|
68
|
+
for (let frame = 0; frame < framesRead; ++frame) {
|
|
69
|
+
channel[frame] = view.getFloat32((frame * numChannels + ch) * 4, true);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
self.postMessage(
|
|
73
|
+
{ type: 'sonare:clip-page', requestId, pageIndex, ok: true, frames: framesRead, channelBuffers },
|
|
74
|
+
channelBuffers,
|
|
75
|
+
);
|
|
76
|
+
} finally {
|
|
77
|
+
access.close();
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
self.postMessage({
|
|
81
|
+
type: 'sonare:clip-page',
|
|
82
|
+
requestId,
|
|
83
|
+
pageIndex,
|
|
84
|
+
ok: false,
|
|
85
|
+
error: error instanceof Error ? error.message : String(error),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
export function createOpfsClipPageWorker(): Worker {
|
|
92
|
+
const blob = new Blob([opfsClipPageWorkerSource], { type: 'text/javascript' });
|
|
93
|
+
return new Worker(URL.createObjectURL(blob));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function createOpfsClipPageProvider(
|
|
97
|
+
engine: RealtimeEngine,
|
|
98
|
+
options: OpfsClipPageProviderOptions,
|
|
99
|
+
): OpfsClipPageProviderBinding {
|
|
100
|
+
if (options.numChannels <= 0 || options.numSamples <= 0 || options.pageFrames <= 0) {
|
|
101
|
+
throw new Error('numChannels, numSamples, and pageFrames must be positive');
|
|
102
|
+
}
|
|
103
|
+
const provider = engine.createClipPageProvider(
|
|
104
|
+
options.numChannels,
|
|
105
|
+
options.numSamples,
|
|
106
|
+
options.pageFrames,
|
|
107
|
+
);
|
|
108
|
+
const worker = options.worker ?? createOpfsClipPageWorker();
|
|
109
|
+
const ownsWorker = options.worker === undefined || options.terminateWorkerOnClose === true;
|
|
110
|
+
let nextRequestId = 1;
|
|
111
|
+
let closed = false;
|
|
112
|
+
const pending = new Map<
|
|
113
|
+
number,
|
|
114
|
+
{ resolve: (value: boolean) => void; reject: (reason: unknown) => void }
|
|
115
|
+
>();
|
|
116
|
+
|
|
117
|
+
const onMessage = (event: MessageEvent<PageResponse>) => {
|
|
118
|
+
const response = event.data;
|
|
119
|
+
if (response?.type !== 'sonare:clip-page') {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const entry = pending.get(response.requestId);
|
|
123
|
+
if (!entry) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
pending.delete(response.requestId);
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
entry.resolve(false);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const channels =
|
|
132
|
+
response.channels ??
|
|
133
|
+
response.channelBuffers?.map(
|
|
134
|
+
(buffer) => new Float32Array(buffer, 0, response.frames ?? buffer.byteLength / 4),
|
|
135
|
+
);
|
|
136
|
+
if (!channels || channels.length === 0) {
|
|
137
|
+
entry.resolve(false);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
provider.supply(response.pageIndex, channels);
|
|
141
|
+
entry.resolve(true);
|
|
142
|
+
};
|
|
143
|
+
worker.addEventListener('message', onMessage as EventListener);
|
|
144
|
+
|
|
145
|
+
const supplyPage = (pageIndex: number): Promise<boolean> => {
|
|
146
|
+
if (closed) {
|
|
147
|
+
return Promise.reject(new Error('OpfsClipPageProvider is closed'));
|
|
148
|
+
}
|
|
149
|
+
const requestId = nextRequestId++;
|
|
150
|
+
const promise = new Promise<boolean>((resolve, reject) => {
|
|
151
|
+
pending.set(requestId, { resolve, reject });
|
|
152
|
+
});
|
|
153
|
+
worker.postMessage({
|
|
154
|
+
type: 'sonare:read-clip-page',
|
|
155
|
+
requestId,
|
|
156
|
+
path: options.path,
|
|
157
|
+
pageIndex,
|
|
158
|
+
numChannels: options.numChannels,
|
|
159
|
+
numSamples: options.numSamples,
|
|
160
|
+
pageFrames: options.pageFrames,
|
|
161
|
+
dataOffsetBytes: options.dataOffsetBytes ?? 0,
|
|
162
|
+
});
|
|
163
|
+
return promise;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
provider,
|
|
168
|
+
supplyPage,
|
|
169
|
+
supplyRequest(request: ClipPageRequest) {
|
|
170
|
+
return supplyPage(Math.floor(request.sample / options.pageFrames));
|
|
171
|
+
},
|
|
172
|
+
close() {
|
|
173
|
+
if (closed) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
closed = true;
|
|
177
|
+
worker.removeEventListener('message', onMessage as EventListener);
|
|
178
|
+
for (const entry of pending.values()) {
|
|
179
|
+
entry.reject(new Error('OpfsClipPageProvider is closed'));
|
|
180
|
+
}
|
|
181
|
+
pending.clear();
|
|
182
|
+
provider.destroy();
|
|
183
|
+
if (ownsWorker) {
|
|
184
|
+
worker.terminate();
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|