@libraz/libsonare 1.5.0 → 1.5.2
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 +1 -1
- package/dist/index.d.ts +108 -55
- package/dist/index.js +162 -27
- package/dist/index.js.map +1 -1
- package/dist/sonare.js +2 -2
- package/dist/sonare.wasm +0 -0
- package/dist/worklet.js +8 -1
- package/dist/worklet.js.map +1 -1
- package/package.json +1 -1
- package/src/clip_page_streamer.ts +63 -15
- package/src/effects_voice_change.ts +7 -7
- package/src/feature_music.ts +110 -2
- package/src/features.ts +2 -0
- package/src/index.ts +15 -0
- package/src/mastering_core.ts +10 -0
- package/src/mixer.ts +9 -1
- package/src/project_class.ts +17 -7
- package/src/project_internal.ts +1 -0
- package/src/public_types_music.ts +1 -0
- package/src/public_types_realtime.ts +39 -39
- package/src/sonare.js.d.ts +70 -38
- package/src/stream_analyzer.ts +8 -1
- package/src/stream_types.ts +8 -0
package/package.json
CHANGED
|
@@ -47,8 +47,12 @@ export interface ClipPageStreamerOptions {
|
|
|
47
47
|
interface SourceState {
|
|
48
48
|
source: ClipPageStreamSource;
|
|
49
49
|
lastPage: number;
|
|
50
|
-
/**
|
|
51
|
-
|
|
50
|
+
/** Playback-window generation; advanced on reset or backward discontinuity. */
|
|
51
|
+
generation: number;
|
|
52
|
+
/** Most recently serviced playback frontier, or null before the first miss. */
|
|
53
|
+
lastFrontier: number | null;
|
|
54
|
+
/** Resident page -> generation, so in-flight stale fetches cannot alias. */
|
|
55
|
+
resident: Map<number, number>;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
/**
|
|
@@ -92,18 +96,40 @@ export class ClipPageStreamer {
|
|
|
92
96
|
throw new Error('pageFrames and numSamples must be positive');
|
|
93
97
|
}
|
|
94
98
|
const lastPage = Math.ceil(source.numSamples / source.pageFrames) - 1;
|
|
99
|
+
const previous = this.sources.get(source.clipId);
|
|
100
|
+
if (previous) {
|
|
101
|
+
this.resetState(previous);
|
|
102
|
+
}
|
|
95
103
|
this.sources.set(source.clipId, {
|
|
96
104
|
source,
|
|
97
105
|
lastPage,
|
|
98
|
-
|
|
106
|
+
generation: 0,
|
|
107
|
+
lastFrontier: null,
|
|
108
|
+
resident: new Map(Array.from(initialResidentPages, (page) => [page, 0])),
|
|
99
109
|
});
|
|
100
110
|
}
|
|
101
111
|
|
|
102
112
|
/** Stop tracking a clip. Does not close its binding (the caller owns that). */
|
|
103
113
|
removeSource(clipId: number): void {
|
|
114
|
+
const state = this.sources.get(clipId);
|
|
115
|
+
if (state) {
|
|
116
|
+
this.resetState(state);
|
|
117
|
+
}
|
|
104
118
|
this.sources.delete(clipId);
|
|
105
119
|
}
|
|
106
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Explicitly start a new playback generation after a host seek/loop. Resident
|
|
123
|
+
* pages are evicted and any older in-flight fetch is cleared when it settles.
|
|
124
|
+
* The next miss establishes the new bounded window.
|
|
125
|
+
*/
|
|
126
|
+
resetSource(clipId: number): void {
|
|
127
|
+
const state = this.sources.get(clipId);
|
|
128
|
+
if (state) {
|
|
129
|
+
this.resetState(state);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
107
133
|
/**
|
|
108
134
|
* Drain pending page-miss requests, fetch the missing pages plus their
|
|
109
135
|
* read-ahead window, and evict out-of-window pages. Resolves once this round's
|
|
@@ -113,9 +139,10 @@ export class ClipPageStreamer {
|
|
|
113
139
|
if (this.closed) {
|
|
114
140
|
return;
|
|
115
141
|
}
|
|
116
|
-
// Collapse this round's misses to the
|
|
117
|
-
//
|
|
118
|
-
//
|
|
142
|
+
// Collapse this round's misses to the latest observed frontier per clip.
|
|
143
|
+
// Queue order reflects playback order, so a backward seek/loop request that
|
|
144
|
+
// follows a stale high-page miss must win even though its page index is
|
|
145
|
+
// smaller. Multiple channel misses still collapse to one service window.
|
|
119
146
|
const frontiers = new Map<number, number>();
|
|
120
147
|
for (let drained = 0; drained < this.maxRequestsPerPump; ++drained) {
|
|
121
148
|
const request = this.engine.popClipPageRequest();
|
|
@@ -127,10 +154,7 @@ export class ClipPageStreamer {
|
|
|
127
154
|
continue;
|
|
128
155
|
}
|
|
129
156
|
const page = Math.floor(request.sample / state.source.pageFrames);
|
|
130
|
-
|
|
131
|
-
if (previous === undefined || page > previous) {
|
|
132
|
-
frontiers.set(request.clipId, page);
|
|
133
|
-
}
|
|
157
|
+
frontiers.set(request.clipId, page);
|
|
134
158
|
}
|
|
135
159
|
|
|
136
160
|
const fetches: Promise<unknown>[] = [];
|
|
@@ -151,18 +175,27 @@ export class ClipPageStreamer {
|
|
|
151
175
|
}
|
|
152
176
|
this.closed = true;
|
|
153
177
|
for (const state of this.sources.values()) {
|
|
178
|
+
this.resetState(state);
|
|
154
179
|
state.source.binding.close();
|
|
155
180
|
}
|
|
156
181
|
this.sources.clear();
|
|
157
182
|
}
|
|
158
183
|
|
|
159
184
|
private serviceFrontier(state: SourceState, frontier: number): Promise<unknown>[] {
|
|
185
|
+
// A lower frontier after a previously serviced high page is a seek/loop
|
|
186
|
+
// discontinuity. Advance generation before scheduling its new window so an
|
|
187
|
+
// older asynchronous fetch can never become resident in the new window.
|
|
188
|
+
if (state.lastFrontier !== null && frontier < state.lastFrontier) {
|
|
189
|
+
this.resetState(state);
|
|
190
|
+
}
|
|
191
|
+
state.lastFrontier = frontier;
|
|
192
|
+
const generation = state.generation;
|
|
160
193
|
const low = Math.max(0, frontier - this.retainBehindPages);
|
|
161
194
|
const high = Math.min(state.lastPage, frontier + this.readAheadPages);
|
|
162
195
|
|
|
163
196
|
// Evict pages outside the window first so a burst of fetches never exceeds
|
|
164
197
|
// the bound by transiently holding old pages alongside new ones.
|
|
165
|
-
for (const page of state.resident) {
|
|
198
|
+
for (const page of state.resident.keys()) {
|
|
166
199
|
if (page < low || page > high) {
|
|
167
200
|
state.source.binding.provider.clear(page);
|
|
168
201
|
state.resident.delete(page);
|
|
@@ -171,23 +204,29 @@ export class ClipPageStreamer {
|
|
|
171
204
|
|
|
172
205
|
const fetches: Promise<unknown>[] = [];
|
|
173
206
|
for (let page = low; page <= high; ++page) {
|
|
174
|
-
if (state.resident.
|
|
207
|
+
if (state.resident.get(page) === generation) {
|
|
175
208
|
continue;
|
|
176
209
|
}
|
|
177
210
|
// Mark resident eagerly so the same page is not fetched twice across
|
|
178
211
|
// overlapping windows; drop it again if the fetch reports a miss.
|
|
179
|
-
state.resident.
|
|
212
|
+
state.resident.set(page, generation);
|
|
180
213
|
const pageIndex = page;
|
|
181
214
|
fetches.push(
|
|
182
215
|
state.source.binding.supplyPage(pageIndex).then(
|
|
183
216
|
(ok) => {
|
|
184
|
-
if (
|
|
217
|
+
if (state.generation !== generation) {
|
|
218
|
+
// The fetch crossed a seek/reset boundary. supplyPage may have
|
|
219
|
+
// installed it after resetState's clear pass, so clear it again.
|
|
220
|
+
state.source.binding.provider.clear(pageIndex);
|
|
221
|
+
} else if (!ok && state.resident.get(pageIndex) === generation) {
|
|
185
222
|
state.resident.delete(pageIndex);
|
|
186
223
|
}
|
|
187
224
|
return ok;
|
|
188
225
|
},
|
|
189
226
|
(error) => {
|
|
190
|
-
state.resident.
|
|
227
|
+
if (state.resident.get(pageIndex) === generation) {
|
|
228
|
+
state.resident.delete(pageIndex);
|
|
229
|
+
}
|
|
191
230
|
throw error;
|
|
192
231
|
},
|
|
193
232
|
),
|
|
@@ -195,6 +234,15 @@ export class ClipPageStreamer {
|
|
|
195
234
|
}
|
|
196
235
|
return fetches;
|
|
197
236
|
}
|
|
237
|
+
|
|
238
|
+
private resetState(state: SourceState): void {
|
|
239
|
+
state.generation += 1;
|
|
240
|
+
state.lastFrontier = null;
|
|
241
|
+
for (const page of state.resident.keys()) {
|
|
242
|
+
state.source.binding.provider.clear(page);
|
|
243
|
+
}
|
|
244
|
+
state.resident.clear();
|
|
245
|
+
}
|
|
198
246
|
}
|
|
199
247
|
|
|
200
248
|
export interface OpfsClipStreamOptions extends OpfsClipPageProviderOptions {
|
|
@@ -40,9 +40,6 @@ export function voiceChange(
|
|
|
40
40
|
|
|
41
41
|
/** Options for the offline {@link voiceChangeRealtime} convenience wrapper. */
|
|
42
42
|
export interface VoiceChangeRealtimeOptions extends ValidateOptions {
|
|
43
|
-
sampleRate?: number;
|
|
44
|
-
/** Voice-changer preset id or full config object. */
|
|
45
|
-
preset?: RealtimeVoiceChangerConfigInput;
|
|
46
43
|
/** Channel count (1 = mono, 2 = interleaved stereo). */
|
|
47
44
|
channels?: 1 | 2;
|
|
48
45
|
/** Block size for the internal render loop (default 512). */
|
|
@@ -90,10 +87,16 @@ function latencyCompensatedVoiceChange(
|
|
|
90
87
|
* and Node `voiceChangeRealtime` convenience wrappers. For mono, `samples` is a
|
|
91
88
|
* plain mono buffer; for stereo, `samples` is interleaved (L0,R0,L1,R1,...).
|
|
92
89
|
*
|
|
90
|
+
* @param samples - Audio samples (mono, or interleaved stereo when channels=2)
|
|
91
|
+
* @param sampleRate - Sample rate in Hz (default 48000, matching Python/Node)
|
|
92
|
+
* @param preset - Voice-changer preset id or full config object
|
|
93
|
+
* @param options - Channel count and block size ({@link VoiceChangeRealtimeOptions})
|
|
93
94
|
* @returns The processed buffer (same layout/length as the input).
|
|
94
95
|
*/
|
|
95
96
|
export function voiceChangeRealtime(
|
|
96
97
|
samples: Float32Array,
|
|
98
|
+
sampleRate = 48000,
|
|
99
|
+
preset: RealtimeVoiceChangerConfigInput = 'neutral-monitor',
|
|
97
100
|
options: VoiceChangeRealtimeOptions = {},
|
|
98
101
|
): Float32Array {
|
|
99
102
|
assertSamples('voiceChangeRealtime', samples, options.validate !== false);
|
|
@@ -104,11 +107,8 @@ export function voiceChangeRealtime(
|
|
|
104
107
|
if (channels === 2 && samples.length % 2 !== 0) {
|
|
105
108
|
throw new Error('voiceChangeRealtime: stereo input length must be a multiple of 2.');
|
|
106
109
|
}
|
|
107
|
-
// 48000 matches the Python voice_change_realtime and Node voiceChangeRealtime
|
|
108
|
-
// convenience wrappers (and the RealtimeVoiceChanger default).
|
|
109
|
-
const sampleRate = options.sampleRate ?? 48000;
|
|
110
110
|
const blockSize = Math.max(1, Math.floor(options.blockSize ?? 512));
|
|
111
|
-
const changer = new RealtimeVoiceChanger(
|
|
111
|
+
const changer = new RealtimeVoiceChanger(preset);
|
|
112
112
|
try {
|
|
113
113
|
changer.prepare(sampleRate, blockSize, channels);
|
|
114
114
|
return latencyCompensatedVoiceChange(changer, samples, channels, blockSize);
|
package/src/feature_music.ts
CHANGED
|
@@ -180,6 +180,111 @@ export function vqt(
|
|
|
180
180
|
return requireModule().vqt(samples, sampleRate, hopLength, fmin, nBins, binsPerOctave, gamma);
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
function validateCqtInverse(
|
|
184
|
+
fnName: string,
|
|
185
|
+
magnitude: Float32Array,
|
|
186
|
+
nBins: number,
|
|
187
|
+
nFrames: number,
|
|
188
|
+
sampleRate: number,
|
|
189
|
+
hopLength: number,
|
|
190
|
+
fmin: number,
|
|
191
|
+
binsPerOctave: number,
|
|
192
|
+
nIter: number,
|
|
193
|
+
options: GuardedOptions,
|
|
194
|
+
): void {
|
|
195
|
+
assertSampleRate(fnName, sampleRate);
|
|
196
|
+
validatePositiveIntegers(fnName, { nBins, nFrames, hopLength, binsPerOctave, nIter });
|
|
197
|
+
if (nIter > 256) {
|
|
198
|
+
throw new RangeError(`${fnName}: nIter must be at most 256`);
|
|
199
|
+
}
|
|
200
|
+
validateFrequencyBounds(fnName, fmin);
|
|
201
|
+
if (fmin === 0) {
|
|
202
|
+
throw new RangeError(`${fnName}: fmin must be positive`);
|
|
203
|
+
}
|
|
204
|
+
if (magnitude.length !== nBins * nFrames) {
|
|
205
|
+
throw new RangeError(`${fnName}: magnitude length must equal nBins * nFrames`);
|
|
206
|
+
}
|
|
207
|
+
assertSamples(fnName, magnitude, options.validate !== false);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Reconstruct mono audio from row-major CQT magnitude via Griffin-Lim. */
|
|
211
|
+
export function cqtToAudio(
|
|
212
|
+
magnitude: Float32Array,
|
|
213
|
+
nBins: number,
|
|
214
|
+
nFrames: number,
|
|
215
|
+
sampleRate = 22050,
|
|
216
|
+
hopLength = 512,
|
|
217
|
+
fmin = 32.70319566257483,
|
|
218
|
+
binsPerOctave = 12,
|
|
219
|
+
nIter = 32,
|
|
220
|
+
options: GuardedOptions = {},
|
|
221
|
+
): Float32Array {
|
|
222
|
+
validateCqtInverse(
|
|
223
|
+
'cqtToAudio',
|
|
224
|
+
magnitude,
|
|
225
|
+
nBins,
|
|
226
|
+
nFrames,
|
|
227
|
+
sampleRate,
|
|
228
|
+
hopLength,
|
|
229
|
+
fmin,
|
|
230
|
+
binsPerOctave,
|
|
231
|
+
nIter,
|
|
232
|
+
options,
|
|
233
|
+
);
|
|
234
|
+
return requireModule().cqtToAudio(
|
|
235
|
+
magnitude,
|
|
236
|
+
nBins,
|
|
237
|
+
nFrames,
|
|
238
|
+
sampleRate,
|
|
239
|
+
hopLength,
|
|
240
|
+
fmin,
|
|
241
|
+
binsPerOctave,
|
|
242
|
+
nIter,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Reconstruct mono audio from row-major VQT magnitude via Griffin-Lim. */
|
|
247
|
+
export function vqtToAudio(
|
|
248
|
+
magnitude: Float32Array,
|
|
249
|
+
nBins: number,
|
|
250
|
+
nFrames: number,
|
|
251
|
+
sampleRate = 22050,
|
|
252
|
+
hopLength = 512,
|
|
253
|
+
fmin = 32.70319566257483,
|
|
254
|
+
binsPerOctave = 12,
|
|
255
|
+
gamma = 0,
|
|
256
|
+
nIter = 32,
|
|
257
|
+
options: GuardedOptions = {},
|
|
258
|
+
): Float32Array {
|
|
259
|
+
validateCqtInverse(
|
|
260
|
+
'vqtToAudio',
|
|
261
|
+
magnitude,
|
|
262
|
+
nBins,
|
|
263
|
+
nFrames,
|
|
264
|
+
sampleRate,
|
|
265
|
+
hopLength,
|
|
266
|
+
fmin,
|
|
267
|
+
binsPerOctave,
|
|
268
|
+
nIter,
|
|
269
|
+
options,
|
|
270
|
+
);
|
|
271
|
+
assertFiniteScalar('vqtToAudio', gamma, 'gamma');
|
|
272
|
+
if (gamma < 0) {
|
|
273
|
+
throw new RangeError('vqtToAudio: gamma must be non-negative');
|
|
274
|
+
}
|
|
275
|
+
return requireModule().vqtToAudio(
|
|
276
|
+
magnitude,
|
|
277
|
+
nBins,
|
|
278
|
+
nFrames,
|
|
279
|
+
sampleRate,
|
|
280
|
+
hopLength,
|
|
281
|
+
fmin,
|
|
282
|
+
binsPerOctave,
|
|
283
|
+
gamma,
|
|
284
|
+
nIter,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
183
288
|
/**
|
|
184
289
|
* Detect song-structure sections (intro/verse/chorus/...).
|
|
185
290
|
*
|
|
@@ -366,19 +471,22 @@ export function fourierTempogram(
|
|
|
366
471
|
* @param winLength - Window length in frames (default: 384)
|
|
367
472
|
* @param sampleRate - Sample rate in Hz (default: 22050)
|
|
368
473
|
* @param hopLength - Hop length (default: 512)
|
|
369
|
-
* @
|
|
474
|
+
* @param factors - Lag ratios to evaluate. When omitted or empty, the library
|
|
475
|
+
* default {0.5, 1, 2, 3, 4} is used.
|
|
476
|
+
* @returns Tempogram ratio features (one value per factor)
|
|
370
477
|
*/
|
|
371
478
|
export function tempogramRatio(
|
|
372
479
|
tempogramData: Float32Array,
|
|
373
480
|
winLength = 384,
|
|
374
481
|
sampleRate = 22050,
|
|
375
482
|
hopLength = 512,
|
|
483
|
+
factors?: Float32Array | number[],
|
|
376
484
|
options: GuardedOptions = {},
|
|
377
485
|
): Float32Array {
|
|
378
486
|
assertSampleRate('tempogramRatio', sampleRate);
|
|
379
487
|
assertSamples('tempogramRatio', tempogramData, options.validate !== false, 'tempogramData');
|
|
380
488
|
validatePositiveIntegers('tempogramRatio', { winLength, hopLength });
|
|
381
|
-
return requireModule().tempogramRatio(tempogramData, winLength, sampleRate, hopLength);
|
|
489
|
+
return requireModule().tempogramRatio(tempogramData, winLength, sampleRate, hopLength, factors);
|
|
382
490
|
}
|
|
383
491
|
|
|
384
492
|
/**
|
package/src/features.ts
CHANGED
|
@@ -33,6 +33,7 @@ export {
|
|
|
33
33
|
analyzeMelody,
|
|
34
34
|
analyzeSections,
|
|
35
35
|
cqt,
|
|
36
|
+
cqtToAudio,
|
|
36
37
|
fourierTempogram,
|
|
37
38
|
hybridCqt,
|
|
38
39
|
lufs,
|
|
@@ -44,6 +45,7 @@ export {
|
|
|
44
45
|
shortTermLufs,
|
|
45
46
|
tempogramRatio,
|
|
46
47
|
vqt,
|
|
48
|
+
vqtToAudio,
|
|
47
49
|
} from './feature_music';
|
|
48
50
|
export { pitchPyin, pitchYin } from './feature_pitch';
|
|
49
51
|
export { resample } from './feature_resample';
|
package/src/index.ts
CHANGED
|
@@ -125,6 +125,7 @@ export {
|
|
|
125
125
|
chromaCens,
|
|
126
126
|
chromaCqt,
|
|
127
127
|
cqt,
|
|
128
|
+
cqtToAudio,
|
|
128
129
|
cyclicTempogram,
|
|
129
130
|
dbToAmplitude,
|
|
130
131
|
dbToPower,
|
|
@@ -193,6 +194,7 @@ export {
|
|
|
193
194
|
trimSilence,
|
|
194
195
|
vectorNormalize,
|
|
195
196
|
vqt,
|
|
197
|
+
vqtToAudio,
|
|
196
198
|
zeroCrossingRate,
|
|
197
199
|
zeroCrossings,
|
|
198
200
|
} from './features';
|
|
@@ -445,6 +447,7 @@ export type {
|
|
|
445
447
|
EngineTrackLane,
|
|
446
448
|
EngineTrackSend,
|
|
447
449
|
EngineTransportState,
|
|
450
|
+
ExternalMidiEvent,
|
|
448
451
|
MidiCcBindOptions,
|
|
449
452
|
} from './realtime_engine';
|
|
450
453
|
export { EXPECTED_ENGINE_ABI_VERSION, engineCapabilities, RealtimeEngine } from './realtime_engine';
|
|
@@ -561,6 +564,18 @@ export function version(): string {
|
|
|
561
564
|
return module.version();
|
|
562
565
|
}
|
|
563
566
|
|
|
567
|
+
/**
|
|
568
|
+
* Aggregate native ABI version: the per-subsystem ABI macros folded into one
|
|
569
|
+
* 32-bit value. It bumps whenever any flat C POD layout changes, so callers can
|
|
570
|
+
* detect an incompatible prebuilt binary. Matches the Node/Python `abiVersion()`.
|
|
571
|
+
*/
|
|
572
|
+
export function abiVersion(): number {
|
|
573
|
+
if (!module) {
|
|
574
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
575
|
+
}
|
|
576
|
+
return module.abiVersion();
|
|
577
|
+
}
|
|
578
|
+
|
|
564
579
|
export function engineAbiVersion(): number {
|
|
565
580
|
if (!module) {
|
|
566
581
|
throw new Error('Module not initialized. Call init() first.');
|
package/src/mastering_core.ts
CHANGED
|
@@ -129,6 +129,16 @@ export interface MasteringProcessorCatalogEntry {
|
|
|
129
129
|
realtimeInsertable: boolean;
|
|
130
130
|
/** True for processors with no mono implementation (stereo-only). */
|
|
131
131
|
stereoOnly: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* Reported latency for the default 48 kHz / 512-sample probe configuration.
|
|
134
|
+
* Zero for offline processors; configuration-dependent values are estimates.
|
|
135
|
+
*/
|
|
136
|
+
latencySamples: number;
|
|
137
|
+
/**
|
|
138
|
+
* Audible decay length for the same default prepared probe. Zero for
|
|
139
|
+
* offline, dry-only, and no-tail processors.
|
|
140
|
+
*/
|
|
141
|
+
tailSamples: number;
|
|
132
142
|
/**
|
|
133
143
|
* How the mixer wraps the processor on a >2-channel (surround) bus insert:
|
|
134
144
|
* "multichannel" (one full-buffer call) or "stereoPairOnly" (front L/R pair,
|
package/src/mixer.ts
CHANGED
|
@@ -515,13 +515,21 @@ export class Mixer {
|
|
|
515
515
|
}
|
|
516
516
|
|
|
517
517
|
/**
|
|
518
|
-
*
|
|
518
|
+
* Longest audible serial processor-tail path to the master, in samples. Lazily
|
|
519
519
|
* compiles the routing graph if the topology is dirty.
|
|
520
520
|
*/
|
|
521
521
|
tailSamples(): number {
|
|
522
522
|
return this.mixer.tailSamples();
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Reported latency (samples) of the compiled mixer graph, for aligning
|
|
527
|
+
* dry/wet material. Lazily compiles the routing graph if the topology is dirty.
|
|
528
|
+
*/
|
|
529
|
+
latencySamples(): number {
|
|
530
|
+
return this.mixer.latencySamples();
|
|
531
|
+
}
|
|
532
|
+
|
|
525
533
|
/**
|
|
526
534
|
* Drain delayed / tail audio by processing a zero-input block of `numSamples`
|
|
527
535
|
* frames after the host stops feeding strip inputs. Returns the mixed stereo
|
package/src/project_class.ts
CHANGED
|
@@ -542,8 +542,10 @@ export class Project {
|
|
|
542
542
|
* built-in oscillator synth so a MIDI-only arrangement bounces to audible
|
|
543
543
|
* audio. Pass a {@link BuiltinSynthBinding} (or an array of them) to choose
|
|
544
544
|
* the patch and MIDI destination; omit it (or pass `{}`) for one
|
|
545
|
-
* default-destination sine patch.
|
|
546
|
-
*
|
|
545
|
+
* default-destination sine patch. Because the parameter defaults to `{}`,
|
|
546
|
+
* omission and explicit `undefined` both create that one default binding.
|
|
547
|
+
* Use an explicitly empty array `[]` (or runtime `null`) for zero bindings,
|
|
548
|
+
* so MIDI tracks render silently.
|
|
547
549
|
*
|
|
548
550
|
* Like {@link bounce}, omitting `totalFrames` auto-derives the render length
|
|
549
551
|
* from the arrangement plus the synth's release tail.
|
|
@@ -572,9 +574,10 @@ export class Project {
|
|
|
572
574
|
* string (`'saw-lead'` / `'va:saw-lead'`; see {@link synthPresetNames}), or
|
|
573
575
|
* an array of either; each object entry may carry a `destinationId` binding
|
|
574
576
|
* convenience (default 0), which is not part of the NativeSynth patch itself.
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
*
|
|
577
|
+
* Because the parameter defaults to `{}`, omission and explicit `undefined`
|
|
578
|
+
* both create one default binding. Use an explicitly empty array `[]` (or
|
|
579
|
+
* runtime `null`) for zero bindings. Unknown preset names throw.
|
|
580
|
+
* Deterministic for a fixed project + options + patch.
|
|
578
581
|
*/
|
|
579
582
|
bounceWithSynthInstrument(
|
|
580
583
|
instrument: SynthPatch | string | ReadonlyArray<SynthPatch | string> = {},
|
|
@@ -624,8 +627,10 @@ export class Project {
|
|
|
624
627
|
* honored. Programs the SoundFont does not cover — including bouncing with
|
|
625
628
|
* no SoundFont loaded at all — play through the built-in synthesizer GM
|
|
626
629
|
* fallback bank (the data-free floor; see {@link soundFontManifest} for the
|
|
627
|
-
* per-program backend).
|
|
628
|
-
* `
|
|
630
|
+
* per-program backend). Because the parameter defaults to `{}`, omission and
|
|
631
|
+
* explicit `undefined` both create one default binding. Use an explicitly
|
|
632
|
+
* empty array `[]` (or runtime `null`) for zero bindings, so MIDI tracks
|
|
633
|
+
* render silently.
|
|
629
634
|
*/
|
|
630
635
|
bounceWithSf2Instrument(
|
|
631
636
|
instrument: Sf2InstrumentConfig | ReadonlyArray<Sf2InstrumentConfig> = {},
|
|
@@ -815,6 +820,11 @@ export class Project {
|
|
|
815
820
|
return this.native.trackCount();
|
|
816
821
|
}
|
|
817
822
|
|
|
823
|
+
/** Number of clips in the project. */
|
|
824
|
+
clipCount(): number {
|
|
825
|
+
return this.native.clipCount();
|
|
826
|
+
}
|
|
827
|
+
|
|
818
828
|
/** Number of audio sources registered on the project. */
|
|
819
829
|
sourceCount(): number {
|
|
820
830
|
return this.native.sourceCount();
|
package/src/project_internal.ts
CHANGED
|
@@ -146,6 +146,7 @@ export interface WasmProject {
|
|
|
146
146
|
markerByIndex: (index: number) => ProjectMarker;
|
|
147
147
|
markerCount: () => number;
|
|
148
148
|
trackCount: () => number;
|
|
149
|
+
clipCount: () => number;
|
|
149
150
|
sourceCount: () => number;
|
|
150
151
|
tempoSegmentCount: () => number;
|
|
151
152
|
timeSignatureCount: () => number;
|
|
@@ -140,6 +140,7 @@ export interface KeyCandidate {
|
|
|
140
140
|
export interface ChordDetectionOptions extends ValidateOptions {
|
|
141
141
|
minDuration?: number;
|
|
142
142
|
smoothingWindow?: number;
|
|
143
|
+
/** Final-template correlation threshold in [0, 1]; below it emits Unknown / N.C. */
|
|
143
144
|
threshold?: number;
|
|
144
145
|
useTriadsOnly?: boolean;
|
|
145
146
|
nFft?: number;
|
|
@@ -117,47 +117,47 @@ export interface RealtimeVoiceChangerPreset {
|
|
|
117
117
|
export type RealtimeVoiceChangerConfigInput = VoicePresetId | RealtimeVoiceChangerPreset;
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
|
-
* Flat (POD) realtime voice-changer configuration.
|
|
121
|
-
*
|
|
122
|
-
*
|
|
120
|
+
* Flat (POD) realtime voice-changer configuration. Keys are camelCase to match
|
|
121
|
+
* the Node addon getter, so a preset config reads identically across both JS
|
|
122
|
+
* surfaces (the underlying C ABI / Python POD uses the snake_case equivalents).
|
|
123
123
|
*/
|
|
124
124
|
export interface RealtimeVoiceChangerPodConfig {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
125
|
+
inputGainDb: number;
|
|
126
|
+
outputGainDb: number;
|
|
127
|
+
wetMix: number;
|
|
128
|
+
retuneSemitones: number;
|
|
129
|
+
retuneMix: number;
|
|
130
|
+
retuneGrainSize: number;
|
|
131
|
+
formantFactor: number;
|
|
132
|
+
formantAmount: number;
|
|
133
|
+
formantBody: number;
|
|
134
|
+
formantBrightness: number;
|
|
135
|
+
formantNasal: number;
|
|
136
|
+
eqHighpassHz: number;
|
|
137
|
+
eqBodyDb: number;
|
|
138
|
+
eqPresenceDb: number;
|
|
139
|
+
eqAirDb: number;
|
|
140
|
+
gateThresholdDb: number;
|
|
141
|
+
gateAttackMs: number;
|
|
142
|
+
gateReleaseMs: number;
|
|
143
|
+
gateRangeDb: number;
|
|
144
|
+
compressorThresholdDb: number;
|
|
145
|
+
compressorRatio: number;
|
|
146
|
+
compressorAttackMs: number;
|
|
147
|
+
compressorReleaseMs: number;
|
|
148
|
+
compressorMakeupGainDb: number;
|
|
149
|
+
deesserFrequencyHz: number;
|
|
150
|
+
deesserThresholdDb: number;
|
|
151
|
+
deesserRatio: number;
|
|
152
|
+
deesserRangeDb: number;
|
|
153
|
+
reverbMix: number;
|
|
154
|
+
reverbTimeMs: number;
|
|
155
|
+
reverbDamping: number;
|
|
156
|
+
reverbSeed: number;
|
|
157
|
+
limiterCeilingDb: number;
|
|
158
|
+
limiterReleaseMs: number;
|
|
159
159
|
/** Non-zero enables the 4x-oversampled inter-sample-peak limiter (default enabled). */
|
|
160
|
-
|
|
160
|
+
limiterEnableIspLimiter: boolean;
|
|
161
161
|
/** True-peak ceiling in dBTP applied by the ISP limiter (default -1.0). */
|
|
162
|
-
|
|
162
|
+
limiterIspCeilingDbtp: number;
|
|
163
163
|
}
|