@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
package/src/project.ts
ADDED
|
@@ -0,0 +1,1614 @@
|
|
|
1
|
+
import { getSonareModule } from './module_state';
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Headless DAW Project
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Expected project ABI version. Mirrors `SONARE_PROJECT_ABI_VERSION` in
|
|
9
|
+
* `src/sonare_c_project.h`; checked against {@link projectAbiVersion} to detect
|
|
10
|
+
* a WASM build whose flat project POD layout has drifted from this wrapper.
|
|
11
|
+
*/
|
|
12
|
+
export const EXPECTED_PROJECT_ABI_VERSION = 1;
|
|
13
|
+
|
|
14
|
+
/** Render options for {@link Project.bounce}. All fields are optional. */
|
|
15
|
+
export interface ProjectBounceOptions {
|
|
16
|
+
/** Render length in frames at the output sample rate. */
|
|
17
|
+
totalFrames?: number;
|
|
18
|
+
/** Render block size; <= 0 uses the engine default (128). */
|
|
19
|
+
blockSize?: number;
|
|
20
|
+
/** Output channel count; <= 0 uses the default (2). */
|
|
21
|
+
numChannels?: number;
|
|
22
|
+
/** Output sample rate; <= 0 uses the project sample rate. */
|
|
23
|
+
sampleRate?: number;
|
|
24
|
+
/** Host-instrument PDC (latency) fed to the compiler. */
|
|
25
|
+
instrumentLatencySamples?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Oscillator waveform for the built-in synth. */
|
|
29
|
+
export type BuiltinSynthWaveform =
|
|
30
|
+
| 'sine'
|
|
31
|
+
| 'saw'
|
|
32
|
+
| 'sawtooth'
|
|
33
|
+
| 'square'
|
|
34
|
+
| 'triangle'
|
|
35
|
+
| 0
|
|
36
|
+
| 1
|
|
37
|
+
| 2
|
|
38
|
+
| 3;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Built-in synth patch + MIDI routing for
|
|
42
|
+
* {@link Project.bounceWithBuiltinInstrument}. Every field is optional; a
|
|
43
|
+
* non-positive (or omitted) numeric field falls back to the C-ABI default
|
|
44
|
+
* (gain 0.2, attack 5ms, decay 60ms, sustain 0.7, release 120ms, 16 voices),
|
|
45
|
+
* so `{}` is a usable default sine patch.
|
|
46
|
+
*/
|
|
47
|
+
export interface BuiltinSynthBinding {
|
|
48
|
+
/** MIDI destination id this patch answers to (default 0; see {@link Project.setTrackMidiDestination}). */
|
|
49
|
+
destinationId?: number;
|
|
50
|
+
/** Oscillator waveform (default `'sine'`). */
|
|
51
|
+
waveform?: BuiltinSynthWaveform;
|
|
52
|
+
/** Master output gain, linear (0 => 0.2). */
|
|
53
|
+
gain?: number;
|
|
54
|
+
/** ADSR attack in ms (0 => 5). */
|
|
55
|
+
attackMs?: number;
|
|
56
|
+
/** ADSR decay in ms (0 => 60). */
|
|
57
|
+
decayMs?: number;
|
|
58
|
+
/** ADSR sustain level [0,1] (0 => 0.7). */
|
|
59
|
+
sustain?: number;
|
|
60
|
+
/** ADSR release in ms (0 => 120). */
|
|
61
|
+
releaseMs?: number;
|
|
62
|
+
/** Max simultaneous voices (0 => 16, clamped to [1, 64]). */
|
|
63
|
+
polyphony?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Cross-binding alias of {@link BuiltinSynthBinding}. The same built-in-synth
|
|
68
|
+
* patch concept is named `BuiltinSynthConfig` in the Python binding; this alias
|
|
69
|
+
* lets portable code use that shared name on the WASM surface too.
|
|
70
|
+
*/
|
|
71
|
+
export type BuiltinSynthConfig = BuiltinSynthBinding;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* SoundFont (SF2) player patch + MIDI routing for
|
|
75
|
+
* {@link Project.bounceWithSf2Instrument}. Every field is optional; a
|
|
76
|
+
* non-positive (or omitted) numeric field falls back to the C-ABI default
|
|
77
|
+
* (gain 0.5, 48 voices), so `{}` is a usable default patch.
|
|
78
|
+
*/
|
|
79
|
+
export interface Sf2InstrumentConfig {
|
|
80
|
+
/** MIDI destination id this player answers to (default 0; see {@link Project.setTrackMidiDestination}). */
|
|
81
|
+
destinationId?: number;
|
|
82
|
+
/** Master output gain, linear (0 => 0.5). */
|
|
83
|
+
gain?: number;
|
|
84
|
+
/** Max simultaneous voices (0 => 48, clamped to [1, 64]). */
|
|
85
|
+
polyphony?: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Source backend a resolved MIDI program renders through. */
|
|
89
|
+
export type SourceBackend = 'sf2' | 'synth';
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* One {@link Project.soundFontManifest} entry: a (channel, bank, program)
|
|
93
|
+
* combination the arrangement plays, with the backend it resolves to.
|
|
94
|
+
*/
|
|
95
|
+
export interface Sf2ProgramStatus {
|
|
96
|
+
/** MIDI channel (0-15). */
|
|
97
|
+
channel: number;
|
|
98
|
+
/** Effective SF2 bank (drum channels report 128). */
|
|
99
|
+
bank: number;
|
|
100
|
+
/** Program number (0-127). */
|
|
101
|
+
program: number;
|
|
102
|
+
/** `'sf2'` when the loaded SoundFont covers the program, else `'synth'`. */
|
|
103
|
+
backend: SourceBackend;
|
|
104
|
+
/** Resolved SF2 preset name (GS fallback included); empty for `'synth'`. */
|
|
105
|
+
presetName: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const SYNTH_ENGINE_MODES = [
|
|
109
|
+
'default',
|
|
110
|
+
'subtractive',
|
|
111
|
+
'fm',
|
|
112
|
+
'karplus-strong',
|
|
113
|
+
'modal',
|
|
114
|
+
'additive',
|
|
115
|
+
'percussion',
|
|
116
|
+
'piano',
|
|
117
|
+
] as const;
|
|
118
|
+
export const SYNTH_OSC_WAVEFORMS = [
|
|
119
|
+
'default',
|
|
120
|
+
'sine',
|
|
121
|
+
'saw',
|
|
122
|
+
'square',
|
|
123
|
+
'triangle',
|
|
124
|
+
'noise',
|
|
125
|
+
] as const;
|
|
126
|
+
export const SYNTH_FILTER_MODELS = [
|
|
127
|
+
'default',
|
|
128
|
+
'svf',
|
|
129
|
+
'moog-ladder',
|
|
130
|
+
'diode-ladder',
|
|
131
|
+
'sallen-key',
|
|
132
|
+
] as const;
|
|
133
|
+
export const SYNTH_FILTER_OUTPUTS = ['default', 'lowpass', 'bandpass', 'highpass'] as const;
|
|
134
|
+
export const SYNTH_BODY_TYPES = ['default', 'none', 'guitar', 'violin', 'wood-tube'] as const;
|
|
135
|
+
export const SYNTH_MOD_SOURCES = [
|
|
136
|
+
'none',
|
|
137
|
+
'amp-env',
|
|
138
|
+
'filter-env',
|
|
139
|
+
'lfo1',
|
|
140
|
+
'lfo2',
|
|
141
|
+
'velocity',
|
|
142
|
+
'key-track',
|
|
143
|
+
'mod-wheel',
|
|
144
|
+
'random',
|
|
145
|
+
] as const;
|
|
146
|
+
export const SYNTH_MOD_DESTINATIONS = [
|
|
147
|
+
'none',
|
|
148
|
+
'pitch-cents',
|
|
149
|
+
'cutoff-cents',
|
|
150
|
+
'amp-gain',
|
|
151
|
+
'pan-units',
|
|
152
|
+
] as const;
|
|
153
|
+
|
|
154
|
+
export interface SynthEnumTables {
|
|
155
|
+
engineModes: string[];
|
|
156
|
+
waveforms: string[];
|
|
157
|
+
filterModels: string[];
|
|
158
|
+
filterOutputs: string[];
|
|
159
|
+
bodyTypes: string[];
|
|
160
|
+
modSources: string[];
|
|
161
|
+
modDestinations: string[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** NativeSynth engine selector ({@link SynthPatch}; `'default'` keeps the base patch's). */
|
|
165
|
+
export type SynthEngineMode = (typeof SYNTH_ENGINE_MODES)[number];
|
|
166
|
+
|
|
167
|
+
/** NativeSynth oscillator waveform (`'default'` keeps the base patch's). */
|
|
168
|
+
export type SynthOscWaveform = (typeof SYNTH_OSC_WAVEFORMS)[number];
|
|
169
|
+
|
|
170
|
+
/** NativeSynth filter model — the character core (`'default'` keeps the base patch's). */
|
|
171
|
+
export type SynthFilterModel = (typeof SYNTH_FILTER_MODELS)[number];
|
|
172
|
+
|
|
173
|
+
/** NativeSynth filter output (SVF only; `'default'` keeps the base patch's). */
|
|
174
|
+
export type SynthFilterOutput = (typeof SYNTH_FILTER_OUTPUTS)[number];
|
|
175
|
+
|
|
176
|
+
/** NativeSynth body/formant resonance voicing (`'default'` keeps the base patch's). */
|
|
177
|
+
export type SynthBodyType = (typeof SYNTH_BODY_TYPES)[number];
|
|
178
|
+
|
|
179
|
+
/** {@link SynthPatch} mod-matrix source. */
|
|
180
|
+
export type SynthModSource = (typeof SYNTH_MOD_SOURCES)[number];
|
|
181
|
+
|
|
182
|
+
/** {@link SynthPatch} mod-matrix destination. */
|
|
183
|
+
export type SynthModDestination = (typeof SYNTH_MOD_DESTINATIONS)[number];
|
|
184
|
+
|
|
185
|
+
/** One {@link SynthPatch} mod-matrix routing (name or C ordinal per field). */
|
|
186
|
+
export interface SynthModRouting {
|
|
187
|
+
source: SynthModSource | number;
|
|
188
|
+
destination: SynthModDestination | number;
|
|
189
|
+
/** Destination units at full source deflection. */
|
|
190
|
+
depth: number;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Versioned NativeSynth patch for {@link Project.bounceWithSynthInstrument}
|
|
195
|
+
* and {@link RealtimeEngine.setSynthInstrument}.
|
|
196
|
+
*
|
|
197
|
+
* The patch starts from a BASE — the named `preset` (see
|
|
198
|
+
* {@link synthPresetNames}; a `"va:"` routing prefix is accepted) or, when
|
|
199
|
+
* `preset` is omitted, the default subtractive patch. Every numeric field then
|
|
200
|
+
* uses "0 / omit => keep the base value" (non-zero values override, clamped to
|
|
201
|
+
* their audible ranges) and the enum fields reserve `'default'` as keep. The
|
|
202
|
+
* frozen C ABI has no per-field presence bits, so explicit zero numeric
|
|
203
|
+
* overrides (for example `ampSustain: 0`) cannot be represented; they keep the
|
|
204
|
+
* base value. A non-empty `modRoutings` REPLACES the base mod matrix.
|
|
205
|
+
*
|
|
206
|
+
* Mode-specific deep parameters (FM operator stacks, modal mode tables,
|
|
207
|
+
* drawbar registrations, kit pieces, piano strings) travel inside the named
|
|
208
|
+
* presets; the patch exposes the wrapper sections every engine shares.
|
|
209
|
+
*/
|
|
210
|
+
export interface SynthPatch {
|
|
211
|
+
/**
|
|
212
|
+
* Optional binding convenience for JS realtime/offline helpers. It is not
|
|
213
|
+
* part of the NativeSynth patch itself; Python uses explicit
|
|
214
|
+
* `(destination_id, patch)` bindings instead. Defaults to `0`.
|
|
215
|
+
*/
|
|
216
|
+
destinationId?: number;
|
|
217
|
+
/** Base preset name (see {@link synthPresetNames}); omit for the init patch. */
|
|
218
|
+
preset?: string;
|
|
219
|
+
engineMode?: SynthEngineMode | number;
|
|
220
|
+
waveform?: SynthOscWaveform | number;
|
|
221
|
+
/** Detuned-stack width [1, 7]. */
|
|
222
|
+
unison?: number;
|
|
223
|
+
detuneCents?: number;
|
|
224
|
+
/** Per-voice slow pitch drift depth (cents). */
|
|
225
|
+
driftCents?: number;
|
|
226
|
+
/** Pre-filter drive [0, 1]. */
|
|
227
|
+
drive?: number;
|
|
228
|
+
filterModel?: SynthFilterModel | number;
|
|
229
|
+
filterOutput?: SynthFilterOutput | number;
|
|
230
|
+
cutoffHz?: number;
|
|
231
|
+
resonanceQ?: number;
|
|
232
|
+
/** Cutoff keyboard tracking [0, 1]. */
|
|
233
|
+
keyTrack?: number;
|
|
234
|
+
envToCutoffCents?: number;
|
|
235
|
+
velToCutoffCents?: number;
|
|
236
|
+
ampAttackMs?: number;
|
|
237
|
+
ampDecayMs?: number;
|
|
238
|
+
/** 0 / omit keeps the base value; explicit zero sustain is not representable. */
|
|
239
|
+
ampSustain?: number;
|
|
240
|
+
ampReleaseMs?: number;
|
|
241
|
+
filterAttackMs?: number;
|
|
242
|
+
filterDecayMs?: number;
|
|
243
|
+
/** 0 / omit keeps the base value; explicit zero sustain is not representable. */
|
|
244
|
+
filterSustain?: number;
|
|
245
|
+
filterReleaseMs?: number;
|
|
246
|
+
lfoRateHz?: number;
|
|
247
|
+
lfoToPitchCents?: number;
|
|
248
|
+
lfo2RateHz?: number;
|
|
249
|
+
glideMs?: number;
|
|
250
|
+
body?: SynthBodyType | number;
|
|
251
|
+
/** Body resonance mix [0, 1]. */
|
|
252
|
+
bodyMix?: number;
|
|
253
|
+
/** Seeded per-voice pan scatter [0, 1]. */
|
|
254
|
+
stereoSpread?: number;
|
|
255
|
+
/** Mod matrix (at most 8 routings; REPLACES the base matrix when non-empty). */
|
|
256
|
+
modRoutings?: SynthModRouting[];
|
|
257
|
+
/** Master output gain (linear). */
|
|
258
|
+
gain?: number;
|
|
259
|
+
/** Max simultaneous voices [1, 64]. */
|
|
260
|
+
polyphony?: number;
|
|
261
|
+
/** Gain-neutral bus saturation [0, 1]. */
|
|
262
|
+
busDrive?: number;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Clip fade-curve for {@link Project.setClipFade}. */
|
|
266
|
+
export type ProjectFadeCurve =
|
|
267
|
+
| 'linear'
|
|
268
|
+
| 'equal-power'
|
|
269
|
+
| 'equal_power'
|
|
270
|
+
| 'equalPower'
|
|
271
|
+
| 'exponential'
|
|
272
|
+
| 'logarithmic'
|
|
273
|
+
| 0
|
|
274
|
+
| 1
|
|
275
|
+
| 2
|
|
276
|
+
| 3;
|
|
277
|
+
|
|
278
|
+
/** One clip fade region for {@link Project.setClipFade}. */
|
|
279
|
+
export interface ProjectClipFade {
|
|
280
|
+
/** Fade length in PPQ (>= 0; 0 = no fade). */
|
|
281
|
+
lengthPpq?: number;
|
|
282
|
+
/** Fade curve (default `'linear'`). */
|
|
283
|
+
curve?: ProjectFadeCurve;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** One alternate take for {@link Project.setClipTakes}. */
|
|
287
|
+
export interface ProjectClipTake {
|
|
288
|
+
id: number;
|
|
289
|
+
sourceId?: number;
|
|
290
|
+
sourceOffsetPpq?: number;
|
|
291
|
+
name?: string;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** One comp segment for {@link Project.setClipCompSegments}. */
|
|
295
|
+
export interface ProjectClipCompSegment {
|
|
296
|
+
startPpq: number;
|
|
297
|
+
endPpq: number;
|
|
298
|
+
takeId?: number;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Descriptor for {@link Project.addLoopRecordingTakes}. */
|
|
302
|
+
export interface ProjectLoopRecordingDesc {
|
|
303
|
+
trackId: number;
|
|
304
|
+
startPpq?: number;
|
|
305
|
+
loopLengthPpq: number;
|
|
306
|
+
audio: Float32Array;
|
|
307
|
+
audioChannels?: number;
|
|
308
|
+
audioSampleRate?: number;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Result returned by {@link Project.addLoopRecordingTakes}. */
|
|
312
|
+
export interface ProjectLoopRecordingResult {
|
|
313
|
+
clipId: number;
|
|
314
|
+
takeCount: number;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Clip loop mode for {@link Project.setClipLoop}. */
|
|
318
|
+
export type ProjectLoopMode = 'off' | 'loop' | 0 | 1;
|
|
319
|
+
export type ProjectWarpMode = 'off' | 'repitch' | 'tempo-sync' | 0 | 1 | 2;
|
|
320
|
+
|
|
321
|
+
/** Automation breakpoint interpolation for {@link ProjectAutomationPoint}. */
|
|
322
|
+
export type ProjectAutomationCurve = 'linear' | 'exponential' | 'hold' | 'scurve' | 0 | 1 | 2 | 3;
|
|
323
|
+
|
|
324
|
+
/** One automation breakpoint accepted by the automation-lane edit ops. */
|
|
325
|
+
export interface ProjectAutomationPoint {
|
|
326
|
+
/** Breakpoint position in PPQ. */
|
|
327
|
+
ppq: number;
|
|
328
|
+
/** Breakpoint value. */
|
|
329
|
+
value: number;
|
|
330
|
+
/** Curve to the next breakpoint (default `'linear'`). */
|
|
331
|
+
curve?: ProjectAutomationCurve;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Automation-lane descriptor for {@link Project.addAutomationLane}. */
|
|
335
|
+
export interface ProjectAutomationLaneDesc {
|
|
336
|
+
/** Host-defined id of the parameter the lane drives. */
|
|
337
|
+
targetParamId: number;
|
|
338
|
+
/** Breakpoints (stored verbatim). */
|
|
339
|
+
points: ReadonlyArray<ProjectAutomationPoint>;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** One tempo segment for {@link Project.setTempoSegments}. */
|
|
343
|
+
export interface ProjectTempoSegment {
|
|
344
|
+
/** Segment start in PPQ. */
|
|
345
|
+
startPpq: number;
|
|
346
|
+
/** Tempo in beats per minute at the segment start. */
|
|
347
|
+
bpm: number;
|
|
348
|
+
/** Derived segment start in samples. Accepted for compatibility, ignored on input. */
|
|
349
|
+
startSample?: number;
|
|
350
|
+
/** Optional ramp end tempo in BPM (0 = constant tempo over the segment). */
|
|
351
|
+
endBpm?: number;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** One time-signature segment for {@link Project.setTimeSignatures}. */
|
|
355
|
+
export interface ProjectTimeSignatureSegment {
|
|
356
|
+
/** Segment start in PPQ. */
|
|
357
|
+
startPpq: number;
|
|
358
|
+
/** Beats per bar (time-signature numerator). */
|
|
359
|
+
numerator: number;
|
|
360
|
+
/** Beat unit (time-signature denominator, e.g. 4 or 8). */
|
|
361
|
+
denominator: number;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Key segment for {@link Project.annotateKeys}. */
|
|
365
|
+
export interface ProjectKeySegment {
|
|
366
|
+
startPpq: number;
|
|
367
|
+
endPpq: number;
|
|
368
|
+
/** Tonic pitch class 0..11 (C=0) or 255 for unknown. */
|
|
369
|
+
tonicPc?: number;
|
|
370
|
+
/** KeyMode ordinal (0 unknown, 1 major, 2 minor, 3 dorian, ...). */
|
|
371
|
+
mode?: number;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Chord symbol for {@link Project.annotateChords}. */
|
|
375
|
+
export interface ProjectChordSymbol {
|
|
376
|
+
startPpq: number;
|
|
377
|
+
endPpq: number;
|
|
378
|
+
/** Root pitch class 0..11 (C=0) or 255 for unknown. */
|
|
379
|
+
rootPc?: number;
|
|
380
|
+
/** ChordQuality ordinal (0 unknown, 1 major, 2 minor, ...). */
|
|
381
|
+
quality?: number;
|
|
382
|
+
/** Extension semitone offsets (up to 8). */
|
|
383
|
+
extensions?: ReadonlyArray<number>;
|
|
384
|
+
/** Slash-bass pitch class 0..11 or 255 for none. */
|
|
385
|
+
slashBassPc?: number;
|
|
386
|
+
/** Optional roman-numeral label. */
|
|
387
|
+
romanNumeral?: string;
|
|
388
|
+
/** True at a modulation boundary. */
|
|
389
|
+
modulationBoundary?: boolean;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Assist sidecar snapshot returned by {@link Project.getAssistSidecar}. */
|
|
393
|
+
export interface ProjectAssistSidecar {
|
|
394
|
+
moduleId: string;
|
|
395
|
+
schemaVersion: number;
|
|
396
|
+
targetTrackId: number;
|
|
397
|
+
regionStartPpq: number;
|
|
398
|
+
regionEndPpq: number;
|
|
399
|
+
payload: Uint8Array;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Track kind for {@link Project.addTrack}. */
|
|
403
|
+
export type ProjectTrackKind = 'audio' | 'midi' | 'aux' | 0 | 1 | 2;
|
|
404
|
+
|
|
405
|
+
/** Descriptor for {@link Project.addTrack}. */
|
|
406
|
+
export interface ProjectTrackDesc {
|
|
407
|
+
kind?: ProjectTrackKind;
|
|
408
|
+
name?: string;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export interface ProjectWarpAnchor {
|
|
412
|
+
warpSample: number;
|
|
413
|
+
sourceSample: number;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export interface ProjectWarpMapDesc {
|
|
417
|
+
id: number;
|
|
418
|
+
name?: string;
|
|
419
|
+
anchors: ProjectWarpAnchor[];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** Descriptor for {@link Project.addClip}. */
|
|
423
|
+
export interface ProjectClipDesc {
|
|
424
|
+
trackId: number;
|
|
425
|
+
isMidi?: boolean;
|
|
426
|
+
startPpq?: number;
|
|
427
|
+
lengthPpq: number;
|
|
428
|
+
sourceOffsetPpq?: number;
|
|
429
|
+
gain?: number;
|
|
430
|
+
audio?: Float32Array;
|
|
431
|
+
audioChannels?: number;
|
|
432
|
+
audioSampleRate?: number;
|
|
433
|
+
sourceUri?: string;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Result returned by {@link Project.addMidiClip}. */
|
|
437
|
+
export interface ProjectMidiClipResult {
|
|
438
|
+
trackId: number;
|
|
439
|
+
clipId: number;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Flat MIDI event accepted by {@link Project.setMidiEvents}. */
|
|
443
|
+
export interface ProjectMidiEvent {
|
|
444
|
+
ppq: number;
|
|
445
|
+
data0: number;
|
|
446
|
+
data1?: number;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Options for {@link Project.midiRouteEvents}. `null`/omitted filter fields mean any/no remap. */
|
|
450
|
+
export interface ProjectMidiRouteConfig {
|
|
451
|
+
filterGroup?: number | null;
|
|
452
|
+
filterChannel?: number | null;
|
|
453
|
+
remapChannel?: number | null;
|
|
454
|
+
thru?: boolean;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Result of {@link Project.midiRouteEvents}. */
|
|
458
|
+
export interface ProjectMidiRouteResult {
|
|
459
|
+
events: ProjectMidiEvent[];
|
|
460
|
+
overflowed: boolean;
|
|
461
|
+
overflowCount: number;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export type ProjectMidiCcBindingKind = 0 | 1 | 2 | 3;
|
|
465
|
+
|
|
466
|
+
/** Options for {@link Project.midiCcLearn}. All fields are optional. */
|
|
467
|
+
export interface MidiCcLearnOptions {
|
|
468
|
+
/** Lower end of the mapped parameter range. Default `0`. */
|
|
469
|
+
minValue?: number;
|
|
470
|
+
/** Upper end of the mapped parameter range. Default `1`. */
|
|
471
|
+
maxValue?: number;
|
|
472
|
+
/** Minimum normalized CC movement required to learn a binding. Default `0`. */
|
|
473
|
+
minMovement?: number;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** MIDI CC <-> automation binding descriptor used by CC learn/conversion helpers. */
|
|
477
|
+
export interface ProjectMidiCcBinding {
|
|
478
|
+
ccNumber: number;
|
|
479
|
+
/** MIDI channel 0..15, or 255 for any channel. */
|
|
480
|
+
channel: number;
|
|
481
|
+
/** 0 = 7-bit CC, 1 = 14-bit CC, 2 = RPN, 3 = NRPN. */
|
|
482
|
+
kind: ProjectMidiCcBindingKind;
|
|
483
|
+
ccLsbNumber?: number;
|
|
484
|
+
selectorMsb?: number;
|
|
485
|
+
selectorLsb?: number;
|
|
486
|
+
paramId: number;
|
|
487
|
+
minValue: number;
|
|
488
|
+
maxValue: number;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Result of {@link Project.validateMidiNotes}. */
|
|
492
|
+
export interface ProjectNotePairValidation {
|
|
493
|
+
/** True when every note-on has a matching note-off (and vice versa). */
|
|
494
|
+
ok: boolean;
|
|
495
|
+
/** Count of note-ons that never received a matching note-off. */
|
|
496
|
+
unmatchedNoteOns: number;
|
|
497
|
+
/** Count of note-offs with no preceding matching note-on. */
|
|
498
|
+
unmatchedNoteOffs: number;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** One compile diagnostic (mirrors SonareProjectDiagnostic). */
|
|
502
|
+
export interface ProjectDiagnostic {
|
|
503
|
+
code: number;
|
|
504
|
+
/** 0 = error, 1 = warning. */
|
|
505
|
+
severity: number;
|
|
506
|
+
/** Affected clip / track / source id (0 = n/a). */
|
|
507
|
+
targetId: number;
|
|
508
|
+
/** Human-readable message for this diagnostic. */
|
|
509
|
+
message: string;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Diagnostics summary returned by {@link Project.compile}. */
|
|
513
|
+
export interface ProjectCompileResult {
|
|
514
|
+
/** Number of diagnostics surfaced by the compile. Kept for backward compatibility. */
|
|
515
|
+
diagnosticCount: number;
|
|
516
|
+
/** True when compilation produced a renderable timeline (no error diagnostics). */
|
|
517
|
+
hasTimeline: boolean;
|
|
518
|
+
/** Newline-joined human-readable detail of every diagnostic. */
|
|
519
|
+
messages: string;
|
|
520
|
+
diagnostics: ProjectDiagnostic[];
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export interface ProjectDeserializeResult {
|
|
524
|
+
project: Project;
|
|
525
|
+
diagnostics: string;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Embind handle for the C++ `ProjectWasm` class. The generated `SonareModule`
|
|
529
|
+
// type only gains `Project` / `projectAbiVersion` after a WASM rebuild, so the
|
|
530
|
+
// module is cast through this shape here.
|
|
531
|
+
interface WasmProject {
|
|
532
|
+
toJson: () => string;
|
|
533
|
+
setSampleRate: (sampleRate: number) => void;
|
|
534
|
+
addTrack: (desc: { kind?: number | string; name?: string }) => number;
|
|
535
|
+
addClip: (desc: ProjectClipDesc) => number;
|
|
536
|
+
addLoopRecordingTakes: (desc: ProjectLoopRecordingDesc) => ProjectLoopRecordingResult;
|
|
537
|
+
addMidiClip: (startPpq: number, lengthPpq: number) => ProjectMidiClipResult;
|
|
538
|
+
splitClip: (clipId: number, splitPpq: number) => number;
|
|
539
|
+
trimClip: (clipId: number, newStartPpq: number, newLengthPpq: number) => void;
|
|
540
|
+
moveClip: (clipId: number, newStartPpq: number, newTrackId: number) => void;
|
|
541
|
+
setTrackKind: (trackId: number, kind: number) => void;
|
|
542
|
+
setClipWarpRef: (clipId: number, warpRefId: number) => void;
|
|
543
|
+
setClipWarpMode: (clipId: number, mode: number) => void;
|
|
544
|
+
setWarpMap: (map: ProjectWarpMapDesc) => void;
|
|
545
|
+
removeWarpMap: (warpRefId: number) => void;
|
|
546
|
+
setTrackMidiDestination: (trackId: number, destinationId: number) => void;
|
|
547
|
+
undo: () => void;
|
|
548
|
+
redo: () => void;
|
|
549
|
+
setMidiEvents: (
|
|
550
|
+
clipId: number,
|
|
551
|
+
events: ReadonlyArray<ProjectMidiEvent | readonly [number, number, number]>,
|
|
552
|
+
) => void;
|
|
553
|
+
importSmf: (data: Uint8Array) => number;
|
|
554
|
+
exportSmf: () => Uint8Array;
|
|
555
|
+
importClipFile: (data: Uint8Array) => number;
|
|
556
|
+
exportClipFile: () => Uint8Array;
|
|
557
|
+
setProgram: (clipId: number, program: number, bank: number) => void;
|
|
558
|
+
setProgramOnChannel: (
|
|
559
|
+
clipId: number,
|
|
560
|
+
group: number,
|
|
561
|
+
channel: number,
|
|
562
|
+
program: number,
|
|
563
|
+
bank: number,
|
|
564
|
+
) => void;
|
|
565
|
+
bakeMidiFx: (clipId: number, configJson: string) => void;
|
|
566
|
+
setMidiFx: (clipId: number, configJson: string) => void;
|
|
567
|
+
validateMidiNotes: (clipId: number) => ProjectNotePairValidation;
|
|
568
|
+
autoTempo: (audio: Float32Array, sampleRate: number) => number;
|
|
569
|
+
snapToGrid: (ppq: number, strength: number) => number;
|
|
570
|
+
compile: () => ProjectCompileResult;
|
|
571
|
+
bounce: (options: ProjectBounceOptions) => Float32Array;
|
|
572
|
+
bounceWithBuiltinInstrument: (
|
|
573
|
+
bindings: BuiltinSynthBinding | ReadonlyArray<BuiltinSynthBinding> | undefined,
|
|
574
|
+
options: ProjectBounceOptions,
|
|
575
|
+
) => Float32Array;
|
|
576
|
+
bounceWithSynthInstrument: (
|
|
577
|
+
bindings: SynthPatch | string | ReadonlyArray<SynthPatch | string> | undefined,
|
|
578
|
+
options: ProjectBounceOptions,
|
|
579
|
+
) => Float32Array;
|
|
580
|
+
loadSoundFont: (data: Uint8Array) => void;
|
|
581
|
+
clearSoundFont: () => void;
|
|
582
|
+
soundFontPresetCount: () => number;
|
|
583
|
+
soundFontManifest: () => Sf2ProgramStatus[];
|
|
584
|
+
bounceWithSf2Instrument: (
|
|
585
|
+
bindings: Sf2InstrumentConfig | ReadonlyArray<Sf2InstrumentConfig> | undefined,
|
|
586
|
+
options: ProjectBounceOptions,
|
|
587
|
+
) => Float32Array;
|
|
588
|
+
removeClip: (clipId: number) => void;
|
|
589
|
+
setClipGain: (clipId: number, gain: number) => void;
|
|
590
|
+
setClipFade: (clipId: number, fadeIn: ProjectClipFade, fadeOut: ProjectClipFade) => void;
|
|
591
|
+
setClipTakes: (
|
|
592
|
+
clipId: number,
|
|
593
|
+
takes: ReadonlyArray<ProjectClipTake>,
|
|
594
|
+
activeTakeId: number,
|
|
595
|
+
) => void;
|
|
596
|
+
setClipCompSegments: (clipId: number, segments: ReadonlyArray<ProjectClipCompSegment>) => void;
|
|
597
|
+
setClipLoop: (clipId: number, loopMode: number, loopLengthPpq: number) => void;
|
|
598
|
+
setClipSource: (clipId: number, sourceId: number) => void;
|
|
599
|
+
duplicateClip: (clipId: number, newStartPpq: number) => number;
|
|
600
|
+
removeTrack: (trackId: number) => void;
|
|
601
|
+
renameTrack: (trackId: number, name: string) => void;
|
|
602
|
+
setTrackRoute: (trackId: number, channelStripRef: string, outputTarget: string) => void;
|
|
603
|
+
addAutomationLane: (
|
|
604
|
+
trackId: number,
|
|
605
|
+
desc: { targetParamId: number; points: ReadonlyArray<ProjectAutomationPoint> },
|
|
606
|
+
) => number;
|
|
607
|
+
editAutomationLane: (
|
|
608
|
+
trackId: number,
|
|
609
|
+
laneIndex: number,
|
|
610
|
+
desc: { targetParamId: number; points: ReadonlyArray<ProjectAutomationPoint> },
|
|
611
|
+
) => void;
|
|
612
|
+
removeAutomationLane: (trackId: number, laneIndex: number) => void;
|
|
613
|
+
annotateKeys: (keys: ReadonlyArray<ProjectKeySegment>) => void;
|
|
614
|
+
annotateChords: (chords: ReadonlyArray<ProjectChordSymbol>) => void;
|
|
615
|
+
setAssistSidecar: (
|
|
616
|
+
moduleId: string,
|
|
617
|
+
schemaVersion: number,
|
|
618
|
+
targetTrackId: number,
|
|
619
|
+
regionStartPpq: number,
|
|
620
|
+
regionEndPpq: number,
|
|
621
|
+
payload: Uint8Array,
|
|
622
|
+
) => void;
|
|
623
|
+
assistSidecarCount: () => number;
|
|
624
|
+
getAssistSidecar: (index: number) => ProjectAssistSidecar;
|
|
625
|
+
setOverlapPolicy: (policy: number) => void;
|
|
626
|
+
getOverlapPolicy: () => number;
|
|
627
|
+
getSampleRate: () => number;
|
|
628
|
+
setMixerSceneJson: (sceneJson: string) => void;
|
|
629
|
+
setMarker: (markerId: number, ppq: number, name: string) => number;
|
|
630
|
+
trackCount: () => number;
|
|
631
|
+
sourceCount: () => number;
|
|
632
|
+
tempoSegmentCount: () => number;
|
|
633
|
+
timeSignatureCount: () => number;
|
|
634
|
+
setTempoSegments: (segments: ReadonlyArray<ProjectTempoSegment>) => void;
|
|
635
|
+
setTimeSignatures: (segments: ReadonlyArray<ProjectTimeSignatureSegment>) => void;
|
|
636
|
+
lastBounceCompileResult: () => ProjectCompileResult;
|
|
637
|
+
delete: () => void;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
interface ProjectModule {
|
|
641
|
+
Project: {
|
|
642
|
+
new (): WasmProject;
|
|
643
|
+
fromJson: (json: string) => WasmProject;
|
|
644
|
+
fromJsonWithDiagnostics: (json: string) => { project: WasmProject; diagnostics: string };
|
|
645
|
+
};
|
|
646
|
+
projectAbiVersion: () => number;
|
|
647
|
+
synthPresetNames: () => string[];
|
|
648
|
+
synthPresetPatch: (name: string) => SynthPatch;
|
|
649
|
+
_synthEnumTables: () => SynthEnumTables;
|
|
650
|
+
_synthPatchRoundTrip: (patch: SynthPatch) => SynthPatch;
|
|
651
|
+
midiGmInstrumentName: (program: number) => string | null;
|
|
652
|
+
midiGmProgramForName: (name: string) => number;
|
|
653
|
+
midiGmFamilyName: (family: number) => string | null;
|
|
654
|
+
midiGmFamilyFirstProgram: (family: number) => number;
|
|
655
|
+
midiGm2InstrumentName: (bankLsb: number, program: number) => string | null;
|
|
656
|
+
midiGmDrumName: (note: number) => string | null;
|
|
657
|
+
midiGmDrumNoteForName: (name: string) => number;
|
|
658
|
+
midiGm2DrumSetName: (bankLsb: number) => string | null;
|
|
659
|
+
midiGm2DrumName: (bankLsb: number, note: number) => string | null;
|
|
660
|
+
midiCcName: (controller: number) => string | null;
|
|
661
|
+
midiCcIndexForName: (name: string) => number;
|
|
662
|
+
midiPerNoteControllerName: (index: number) => string | null;
|
|
663
|
+
midiBankProgram: (
|
|
664
|
+
ppq: number,
|
|
665
|
+
group: number,
|
|
666
|
+
channel: number,
|
|
667
|
+
bankMsb: number,
|
|
668
|
+
bankLsb: number,
|
|
669
|
+
program: number,
|
|
670
|
+
) => ProjectMidiEvent[];
|
|
671
|
+
midiRouteEvents: (
|
|
672
|
+
events: ReadonlyArray<ProjectMidiEvent>,
|
|
673
|
+
config: ProjectMidiRouteConfig,
|
|
674
|
+
) => ProjectMidiRouteResult;
|
|
675
|
+
midiCcLearn: (
|
|
676
|
+
events: ReadonlyArray<ProjectMidiEvent>,
|
|
677
|
+
paramId: number,
|
|
678
|
+
minValue: number,
|
|
679
|
+
maxValue: number,
|
|
680
|
+
minMovement: number,
|
|
681
|
+
) => ProjectMidiCcBinding | null;
|
|
682
|
+
midiCcToBreakpoint: (
|
|
683
|
+
bindings: ReadonlyArray<ProjectMidiCcBinding>,
|
|
684
|
+
event: ProjectMidiEvent,
|
|
685
|
+
) => ProjectAutomationPoint | null;
|
|
686
|
+
midiParamToCc: (
|
|
687
|
+
bindings: ReadonlyArray<ProjectMidiCcBinding>,
|
|
688
|
+
paramId: number,
|
|
689
|
+
unitValue: number,
|
|
690
|
+
group: number,
|
|
691
|
+
ppq: number,
|
|
692
|
+
) => ProjectMidiEvent | null;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function projectModule(): ProjectModule {
|
|
696
|
+
const candidate = getSonareModule() as unknown as Partial<ProjectModule>;
|
|
697
|
+
if (typeof candidate.projectAbiVersion !== 'function' || candidate.Project === undefined) {
|
|
698
|
+
throw new Error('libsonare was built without arrangement (headless DAW) support');
|
|
699
|
+
}
|
|
700
|
+
return candidate as ProjectModule;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function assertProjectU7(fnName: string, value: number, argName: string): number {
|
|
704
|
+
if (!Number.isInteger(value) || value < 0 || value > 127) {
|
|
705
|
+
throw new RangeError(`${fnName}: ${argName} must be an integer in [0, 127]`);
|
|
706
|
+
}
|
|
707
|
+
return value;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function assertProjectNibble(fnName: string, value: number, argName: string): number {
|
|
711
|
+
if (!Number.isInteger(value) || value < 0 || value > 15) {
|
|
712
|
+
throw new RangeError(`${fnName}: ${argName} must be an integer in [0, 15]`);
|
|
713
|
+
}
|
|
714
|
+
return value;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function projectMidi1Event(
|
|
718
|
+
fnName: string,
|
|
719
|
+
ppq: number,
|
|
720
|
+
group: number,
|
|
721
|
+
status: number,
|
|
722
|
+
channel: number,
|
|
723
|
+
data1: number,
|
|
724
|
+
data2 = 0,
|
|
725
|
+
): ProjectMidiEvent {
|
|
726
|
+
if (!Number.isFinite(ppq) || ppq < 0) {
|
|
727
|
+
throw new RangeError(`${fnName}: ppq must be a non-negative finite number`);
|
|
728
|
+
}
|
|
729
|
+
const g = assertProjectNibble(fnName, group, 'group');
|
|
730
|
+
const ch = assertProjectNibble(fnName, channel, 'channel');
|
|
731
|
+
const d1 = assertProjectU7(fnName, data1, 'data1');
|
|
732
|
+
const d2 = assertProjectU7(fnName, data2, 'data2');
|
|
733
|
+
// UMP MIDI-1.0 channel-voice word (message type 0x2). Canonical layout is
|
|
734
|
+
// sonare::midi::make_midi1_* (C-ABI sonare_midi_*, which Python delegates to);
|
|
735
|
+
// this hand-written copy is locked against those words by the golden vectors
|
|
736
|
+
// in project.test.ts (mirrored in the Node suite) so it cannot silently drift.
|
|
737
|
+
const word = ((0x2 << 28) | (g << 24) | (status << 20) | (ch << 16) | (d1 << 8) | d2) >>> 0;
|
|
738
|
+
return { ppq, data0: word, data1: 0 };
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function assertProjectU32(fnName: string, value: number, argName: string): void {
|
|
742
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xffffffff) {
|
|
743
|
+
throw new RangeError(`${fnName}: ${argName} must be an integer in [0, 4294967295]`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function assertProjectMidiEvents(
|
|
748
|
+
fnName: string,
|
|
749
|
+
events: ReadonlyArray<ProjectMidiEvent | readonly [number, number, number]>,
|
|
750
|
+
): void {
|
|
751
|
+
if (!Array.isArray(events)) {
|
|
752
|
+
throw new TypeError(`${fnName}: events must be an array`);
|
|
753
|
+
}
|
|
754
|
+
events.forEach((event, index) => {
|
|
755
|
+
const prefix = `events[${index}]`;
|
|
756
|
+
if (Array.isArray(event)) {
|
|
757
|
+
if (event.length < 3) {
|
|
758
|
+
throw new TypeError(`${fnName}: ${prefix} must contain [ppq, data0, data1]`);
|
|
759
|
+
}
|
|
760
|
+
if (!Number.isFinite(event[0]) || event[0] < 0) {
|
|
761
|
+
throw new RangeError(`${fnName}: ${prefix}.ppq must be a non-negative finite number`);
|
|
762
|
+
}
|
|
763
|
+
assertProjectU32(fnName, event[1], `${prefix}.data0`);
|
|
764
|
+
assertProjectU32(fnName, event[2], `${prefix}.data1`);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (event === null || typeof event !== 'object') {
|
|
768
|
+
throw new TypeError(`${fnName}: ${prefix} must be a MIDI event object or tuple`);
|
|
769
|
+
}
|
|
770
|
+
if (!Number.isFinite(event.ppq) || event.ppq < 0) {
|
|
771
|
+
throw new RangeError(`${fnName}: ${prefix}.ppq must be a non-negative finite number`);
|
|
772
|
+
}
|
|
773
|
+
assertProjectU32(fnName, event.data0, `${prefix}.data0`);
|
|
774
|
+
if (event.data1 !== undefined) {
|
|
775
|
+
assertProjectU32(fnName, event.data1, `${prefix}.data1`);
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Runtime ABI version of the flat project POD layout exposed by this WASM
|
|
782
|
+
* build. Equals {@link EXPECTED_PROJECT_ABI_VERSION} when the arrangement
|
|
783
|
+
* subsystem is compiled in. Mirrors the C-ABI `sonare_project_abi_version`.
|
|
784
|
+
*/
|
|
785
|
+
export function projectAbiVersion(): number {
|
|
786
|
+
return projectModule().projectAbiVersion();
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* NativeSynth preset catalog names (`'sine'`, `'saw-lead'`, `'e-piano'`,
|
|
791
|
+
* `'drum-kit'`, ...). Use these to discover valid {@link SynthPatch} preset
|
|
792
|
+
* names instead of hardcoding magic strings.
|
|
793
|
+
*/
|
|
794
|
+
export function synthPresetNames(): string[] {
|
|
795
|
+
return projectModule().synthPresetNames();
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Fetch a named catalog preset as a {@link SynthPatch} (the preset name plus
|
|
800
|
+
* the wrapper-section values), so hosts can inspect a preset and tweak fields
|
|
801
|
+
* before binding it. A `"va:"` routing prefix is accepted; unknown names
|
|
802
|
+
* throw.
|
|
803
|
+
*/
|
|
804
|
+
export function synthPresetPatch(name: string): SynthPatch {
|
|
805
|
+
return projectModule().synthPresetPatch(name);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
export function synthEnumTables(): SynthEnumTables {
|
|
809
|
+
return projectModule()._synthEnumTables();
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
export function synthPatchRoundTripForTest(patch: SynthPatch): SynthPatch {
|
|
813
|
+
return projectModule()._synthPatchRoundTrip(patch);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Headless DAW project (control-thread-only arrangement model).
|
|
818
|
+
*
|
|
819
|
+
* Wraps the embind `Project` class over the C-ABI keystone
|
|
820
|
+
* `sonare_c_project.{h,cpp}`. Construct an empty project with `new Project()`,
|
|
821
|
+
* or deserialize one with {@link Project.fromJson}; serialize back with
|
|
822
|
+
* {@link toJson}; compile to a renderable timeline with {@link compile}; render
|
|
823
|
+
* offline to interleaved float audio with {@link bounce}. The edit and MIDI
|
|
824
|
+
* methods mirror the Node/Python project bindings.
|
|
825
|
+
*
|
|
826
|
+
* Call {@link delete} (or use a `try/finally`) to release the underlying WASM
|
|
827
|
+
* object — the embind handle is not garbage-collected automatically.
|
|
828
|
+
*
|
|
829
|
+
* @example
|
|
830
|
+
* ```typescript
|
|
831
|
+
* const project = new Project();
|
|
832
|
+
* try {
|
|
833
|
+
* project.setSampleRate(48000);
|
|
834
|
+
* const json = project.toJson();
|
|
835
|
+
* const restored = Project.fromJson(json);
|
|
836
|
+
* restored.delete();
|
|
837
|
+
* } finally {
|
|
838
|
+
* project.delete();
|
|
839
|
+
* }
|
|
840
|
+
* ```
|
|
841
|
+
*/
|
|
842
|
+
export class Project {
|
|
843
|
+
private native: WasmProject;
|
|
844
|
+
|
|
845
|
+
constructor() {
|
|
846
|
+
this.native = new (projectModule().Project)();
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/** Pack a MIDI 1.0 note-on event accepted by {@link setMidiEvents}. */
|
|
850
|
+
static midiNoteOn(
|
|
851
|
+
ppq: number,
|
|
852
|
+
group: number,
|
|
853
|
+
channel: number,
|
|
854
|
+
note: number,
|
|
855
|
+
velocity: number,
|
|
856
|
+
): ProjectMidiEvent {
|
|
857
|
+
return projectMidi1Event('Project.midiNoteOn', ppq, group, 0x9, channel, note, velocity);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** Pack a MIDI 1.0 note-off event accepted by {@link setMidiEvents}. */
|
|
861
|
+
static midiNoteOff(
|
|
862
|
+
ppq: number,
|
|
863
|
+
group: number,
|
|
864
|
+
channel: number,
|
|
865
|
+
note: number,
|
|
866
|
+
velocity = 0,
|
|
867
|
+
): ProjectMidiEvent {
|
|
868
|
+
return projectMidi1Event('Project.midiNoteOff', ppq, group, 0x8, channel, note, velocity);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/** Pack a MIDI 1.0 control-change event. */
|
|
872
|
+
static midiCc(
|
|
873
|
+
ppq: number,
|
|
874
|
+
group: number,
|
|
875
|
+
channel: number,
|
|
876
|
+
controller: number,
|
|
877
|
+
value: number,
|
|
878
|
+
): ProjectMidiEvent {
|
|
879
|
+
return projectMidi1Event('Project.midiCc', ppq, group, 0xb, channel, controller, value);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** Pack a MIDI 1.0 poly-pressure event. */
|
|
883
|
+
static midiPolyPressure(
|
|
884
|
+
ppq: number,
|
|
885
|
+
group: number,
|
|
886
|
+
channel: number,
|
|
887
|
+
note: number,
|
|
888
|
+
pressure: number,
|
|
889
|
+
): ProjectMidiEvent {
|
|
890
|
+
return projectMidi1Event('Project.midiPolyPressure', ppq, group, 0xa, channel, note, pressure);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/** Pack a MIDI 1.0 program-change event. */
|
|
894
|
+
static midiProgram(
|
|
895
|
+
ppq: number,
|
|
896
|
+
group: number,
|
|
897
|
+
channel: number,
|
|
898
|
+
program: number,
|
|
899
|
+
): ProjectMidiEvent {
|
|
900
|
+
return projectMidi1Event('Project.midiProgram', ppq, group, 0xc, channel, program, 0);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/** Return the General MIDI instrument name for `program`, or `null` when out of range. */
|
|
904
|
+
static gmInstrumentName(program: number): string | null {
|
|
905
|
+
return projectModule().midiGmInstrumentName(program);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/** Return the General MIDI program number for a canonical instrument name, or `-1`. */
|
|
909
|
+
static gmProgramForName(name: string): number {
|
|
910
|
+
return projectModule().midiGmProgramForName(name);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/** Return the General MIDI family name for `family`, or `null` when out of range. */
|
|
914
|
+
static gmFamilyName(family: number): string | null {
|
|
915
|
+
return projectModule().midiGmFamilyName(family);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/** Return the first General MIDI program number in `family`, or `-1`. */
|
|
919
|
+
static gmFamilyFirstProgram(family: number): number {
|
|
920
|
+
return projectModule().midiGmFamilyFirstProgram(family);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** Return the GM2 bank/program instrument variation name, or `null` when unavailable. */
|
|
924
|
+
static gm2InstrumentName(bankLsb: number, program: number): string | null {
|
|
925
|
+
return projectModule().midiGm2InstrumentName(bankLsb, program);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/** Return the General MIDI drum name for `note`, or `null` when out of range. */
|
|
929
|
+
static gmDrumName(note: number): string | null {
|
|
930
|
+
return projectModule().midiGmDrumName(note);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/** Return the General MIDI drum note for a canonical drum name, or `-1`. */
|
|
934
|
+
static gmDrumNoteForName(name: string): number {
|
|
935
|
+
return projectModule().midiGmDrumNoteForName(name);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/** Return the GM2 drum-set name for `bankLsb`, or `null` when unavailable. */
|
|
939
|
+
static gm2DrumSetName(bankLsb: number): string | null {
|
|
940
|
+
return projectModule().midiGm2DrumSetName(bankLsb);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/** Return the GM2 drum name for `bankLsb`/`note`, or `null` when unavailable. */
|
|
944
|
+
static gm2DrumName(bankLsb: number, note: number): string | null {
|
|
945
|
+
return projectModule().midiGm2DrumName(bankLsb, note);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/** Return the MIDI CC name for `controller`, or `null` when out of range. */
|
|
949
|
+
static midiCcName(controller: number): string | null {
|
|
950
|
+
return projectModule().midiCcName(controller);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/** Return the MIDI CC number for a canonical controller name, or `-1`. */
|
|
954
|
+
static midiCcIndexForName(name: string): number {
|
|
955
|
+
return projectModule().midiCcIndexForName(name);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/** Return the MIDI 2.0 per-note controller name for `index`, or `null`. */
|
|
959
|
+
static perNoteControllerName(index: number): string | null {
|
|
960
|
+
return projectModule().midiPerNoteControllerName(index);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/** Expand bank-select + program-change into MIDI events accepted by {@link setMidiEvents}. */
|
|
964
|
+
static midiBankProgram(
|
|
965
|
+
ppq: number,
|
|
966
|
+
group: number,
|
|
967
|
+
channel: number,
|
|
968
|
+
bankMsb: number,
|
|
969
|
+
bankLsb: number,
|
|
970
|
+
program: number,
|
|
971
|
+
): ProjectMidiEvent[] {
|
|
972
|
+
return projectModule().midiBankProgram(ppq, group, channel, bankMsb, bankLsb, program);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/** Route MIDI events through the native MidiRouter filter/remap/thru logic. */
|
|
976
|
+
static midiRouteEvents(
|
|
977
|
+
events: ReadonlyArray<ProjectMidiEvent>,
|
|
978
|
+
config: ProjectMidiRouteConfig = {},
|
|
979
|
+
): ProjectMidiRouteResult {
|
|
980
|
+
return projectModule().midiRouteEvents(events, config);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/** Run native MIDI learn over an event stream; returns `null` when nothing is learned. */
|
|
984
|
+
static midiCcLearn(
|
|
985
|
+
events: ReadonlyArray<ProjectMidiEvent>,
|
|
986
|
+
paramId: number,
|
|
987
|
+
options: MidiCcLearnOptions = {},
|
|
988
|
+
): ProjectMidiCcBinding | null {
|
|
989
|
+
return projectModule().midiCcLearn(
|
|
990
|
+
events,
|
|
991
|
+
paramId,
|
|
992
|
+
options.minValue ?? 0,
|
|
993
|
+
options.maxValue ?? 1,
|
|
994
|
+
options.minMovement ?? 0,
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/** Convert one CC event to an automation breakpoint using native CcMap. */
|
|
999
|
+
static midiCcToBreakpoint(
|
|
1000
|
+
bindings: ReadonlyArray<ProjectMidiCcBinding>,
|
|
1001
|
+
event: ProjectMidiEvent,
|
|
1002
|
+
): ProjectAutomationPoint | null {
|
|
1003
|
+
return projectModule().midiCcToBreakpoint(bindings, event);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/** Convert one automation value back to a CC UMP event using native CcMap. */
|
|
1007
|
+
static midiParamToCc(
|
|
1008
|
+
bindings: ReadonlyArray<ProjectMidiCcBinding>,
|
|
1009
|
+
paramId: number,
|
|
1010
|
+
unitValue: number,
|
|
1011
|
+
group: number,
|
|
1012
|
+
ppq = 0,
|
|
1013
|
+
): ProjectMidiEvent | null {
|
|
1014
|
+
return projectModule().midiParamToCc(bindings, paramId, unitValue, group, ppq);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/** Pack a MIDI 1.0 channel-pressure event. */
|
|
1018
|
+
static midiChannelPressure(
|
|
1019
|
+
ppq: number,
|
|
1020
|
+
group: number,
|
|
1021
|
+
channel: number,
|
|
1022
|
+
pressure: number,
|
|
1023
|
+
): ProjectMidiEvent {
|
|
1024
|
+
return projectMidi1Event('Project.midiChannelPressure', ppq, group, 0xd, channel, pressure, 0);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/** Pack a MIDI 1.0 pitch-bend event (`bend` is unsigned 14-bit, center = 8192). */
|
|
1028
|
+
static midiPitchBend(
|
|
1029
|
+
ppq: number,
|
|
1030
|
+
group: number,
|
|
1031
|
+
channel: number,
|
|
1032
|
+
bend: number,
|
|
1033
|
+
): ProjectMidiEvent {
|
|
1034
|
+
if (!Number.isInteger(bend) || bend < 0 || bend > 0x3fff) {
|
|
1035
|
+
throw new RangeError('Project.midiPitchBend: bend must be an integer in [0, 16383]');
|
|
1036
|
+
}
|
|
1037
|
+
return projectMidi1Event(
|
|
1038
|
+
'Project.midiPitchBend',
|
|
1039
|
+
ppq,
|
|
1040
|
+
group,
|
|
1041
|
+
0xe,
|
|
1042
|
+
channel,
|
|
1043
|
+
bend & 0x7f,
|
|
1044
|
+
bend >> 7,
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Deserialize project JSON into a new {@link Project}. Throws if the JSON is
|
|
1050
|
+
* malformed, surfacing the joined diagnostic messages.
|
|
1051
|
+
*/
|
|
1052
|
+
static fromJson(json: string): Project {
|
|
1053
|
+
const project = new Project();
|
|
1054
|
+
// Replace the freshly-created empty handle with the deserialized one. If
|
|
1055
|
+
// fromJson throws (malformed JSON) the empty handle is released first so no
|
|
1056
|
+
// WASM object leaks.
|
|
1057
|
+
const restored = (() => {
|
|
1058
|
+
try {
|
|
1059
|
+
return projectModule().Project.fromJson(json);
|
|
1060
|
+
} catch (error) {
|
|
1061
|
+
project.native.delete();
|
|
1062
|
+
throw error;
|
|
1063
|
+
}
|
|
1064
|
+
})();
|
|
1065
|
+
project.native.delete();
|
|
1066
|
+
project.native = restored;
|
|
1067
|
+
return project;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Deserialize project JSON and return native warning diagnostics emitted on
|
|
1072
|
+
* successful loads, such as dangling source references preserved for repair.
|
|
1073
|
+
*/
|
|
1074
|
+
static fromJsonWithDiagnostics(json: string): ProjectDeserializeResult {
|
|
1075
|
+
const project = new Project();
|
|
1076
|
+
const restored = (() => {
|
|
1077
|
+
try {
|
|
1078
|
+
return projectModule().Project.fromJsonWithDiagnostics(json);
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
project.native.delete();
|
|
1081
|
+
throw error;
|
|
1082
|
+
}
|
|
1083
|
+
})();
|
|
1084
|
+
project.native.delete();
|
|
1085
|
+
project.native = restored.project;
|
|
1086
|
+
return { project, diagnostics: restored.diagnostics };
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/** Serialize the project (+ MIDI content) to deterministic JSON. */
|
|
1090
|
+
toJson(): string {
|
|
1091
|
+
return this.native.toJson();
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/** Set the project sample rate in Hz. Must be > 0. */
|
|
1095
|
+
setSampleRate(sampleRate: number): void {
|
|
1096
|
+
this.native.setSampleRate(sampleRate);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/** Add a track and return its allocated stable id. */
|
|
1100
|
+
addTrack(desc: ProjectTrackDesc = {}): number {
|
|
1101
|
+
return this.native.addTrack({ ...desc, kind: projectTrackKindValue(desc.kind) });
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/** Add an audio or MIDI clip and return its allocated clip id. */
|
|
1105
|
+
addClip(desc: ProjectClipDesc): number {
|
|
1106
|
+
return this.native.addClip(desc);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/** Split captured loop-recording audio into takes and add one clip. */
|
|
1110
|
+
addLoopRecordingTakes(desc: ProjectLoopRecordingDesc): ProjectLoopRecordingResult {
|
|
1111
|
+
return this.native.addLoopRecordingTakes(desc);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/** Create a MIDI track + clip; returns `{ trackId, clipId }`. */
|
|
1115
|
+
addMidiClip(startPpq: number, lengthPpq: number): ProjectMidiClipResult {
|
|
1116
|
+
return this.native.addMidiClip(startPpq, lengthPpq);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/** Split a clip at `splitPpq` and return the new clip id. */
|
|
1120
|
+
splitClip(clipId: number, splitPpq: number): number {
|
|
1121
|
+
return this.native.splitClip(clipId, splitPpq);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/** Trim a clip's start / length in PPQ. */
|
|
1125
|
+
trimClip(clipId: number, newStartPpq: number, newLengthPpq: number): void {
|
|
1126
|
+
this.native.trimClip(clipId, newStartPpq, newLengthPpq);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/** Move a clip to `newStartPpq` and optionally another track. */
|
|
1130
|
+
moveClip(clipId: number, newStartPpq: number, newTrackId = 0): void {
|
|
1131
|
+
this.native.moveClip(clipId, newStartPpq, newTrackId);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/** Change a track kind via an undoable edit. */
|
|
1135
|
+
setTrackKind(trackId: number, kind: ProjectTrackKind): void {
|
|
1136
|
+
this.native.setTrackKind(trackId, projectTrackKindValue(kind));
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/** Set a clip's warp reference id (0 clears it). */
|
|
1140
|
+
setClipWarpRef(clipId: number, warpRefId: number): void {
|
|
1141
|
+
this.native.setClipWarpRef(clipId, warpRefId);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/** Set a clip's warp playback mode. */
|
|
1145
|
+
setClipWarpMode(clipId: number, mode: ProjectWarpMode): void {
|
|
1146
|
+
this.native.setClipWarpMode(clipId, projectWarpModeValue(mode));
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/** Add or replace a first-class warp map referenced by clip warp ids. */
|
|
1150
|
+
setWarpMap(map: ProjectWarpMapDesc): void {
|
|
1151
|
+
this.native.setWarpMap(map);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/** Remove a first-class warp map by id. */
|
|
1155
|
+
removeWarpMap(warpRefId: number): void {
|
|
1156
|
+
this.native.removeWarpMap(warpRefId);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* Route a track's MIDI to host-instrument `destinationId` (0 = default). The
|
|
1161
|
+
* compiler stamps every MIDI clip on the track with this id so the engine
|
|
1162
|
+
* dispatches its events to the instrument registered for that destination.
|
|
1163
|
+
* Routes through an undoable edit command.
|
|
1164
|
+
*/
|
|
1165
|
+
setTrackMidiDestination(trackId: number, destinationId: number): void {
|
|
1166
|
+
this.native.setTrackMidiDestination(trackId, destinationId);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/** Undo the most recent edit. */
|
|
1170
|
+
undo(): void {
|
|
1171
|
+
this.native.undo();
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/** Redo the most recently undone edit. */
|
|
1175
|
+
redo(): void {
|
|
1176
|
+
this.native.redo();
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/** Replace a MIDI clip's entire event list. */
|
|
1180
|
+
setMidiEvents(
|
|
1181
|
+
clipId: number,
|
|
1182
|
+
events: ReadonlyArray<ProjectMidiEvent | readonly [number, number, number]>,
|
|
1183
|
+
): void {
|
|
1184
|
+
assertProjectMidiEvents('Project.setMidiEvents', events);
|
|
1185
|
+
this.native.setMidiEvents(clipId, events);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/** Import an in-memory SMF buffer; returns the first added clip id. */
|
|
1189
|
+
importSmf(data: Uint8Array): number {
|
|
1190
|
+
return this.native.importSmf(data);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/** Export the project's tempo map + MIDI clips to an SMF byte buffer. */
|
|
1194
|
+
exportSmf(): Uint8Array {
|
|
1195
|
+
return this.native.exportSmf();
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* Import a MIDI 2.0 Clip File (`SMF2CLIP`); returns the first added clip id.
|
|
1200
|
+
* Unlike {@link importSmf}, MIDI 2.0 channel-voice messages (16-bit velocity,
|
|
1201
|
+
* 32-bit CC, per-note / registered controllers, bank-valid Program Change)
|
|
1202
|
+
* survive without loss.
|
|
1203
|
+
*/
|
|
1204
|
+
importClipFile(data: Uint8Array): number {
|
|
1205
|
+
return this.native.importClipFile(data);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/**
|
|
1209
|
+
* Export the project's tempo map + MIDI clips to a MIDI 2.0 Clip File
|
|
1210
|
+
* (`SMF2CLIP`) byte buffer. MIDI 2.0-only events are written without loss —
|
|
1211
|
+
* prefer this over {@link exportSmf} when MIDI 2.0 fidelity matters.
|
|
1212
|
+
*/
|
|
1213
|
+
exportClipFile(): Uint8Array {
|
|
1214
|
+
return this.native.exportClipFile();
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* Set a MIDI clip's channel-0 program / bank at source PPQ 0. `bank` defaults
|
|
1219
|
+
* to `-1` (no Bank Select emitted), matching `setProgramOnChannel` and the
|
|
1220
|
+
* Node/Python surfaces; pass `>= 0` to emit a Bank Select.
|
|
1221
|
+
*/
|
|
1222
|
+
setProgram(clipId: number, program: number, bank = -1): void {
|
|
1223
|
+
this.native.setProgram(clipId, program, bank);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/** Set a MIDI clip's program / bank for one UMP group and channel. */
|
|
1227
|
+
setProgramOnChannel(
|
|
1228
|
+
clipId: number,
|
|
1229
|
+
group: number,
|
|
1230
|
+
channel: number,
|
|
1231
|
+
program: number,
|
|
1232
|
+
bank = -1,
|
|
1233
|
+
): void {
|
|
1234
|
+
this.native.setProgramOnChannel(clipId, group, channel, program, bank);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/** Destructively bake a MIDI-FX chain into a clip's stored MIDI events. */
|
|
1238
|
+
bakeMidiFx(clipId: number, configJson: string): void {
|
|
1239
|
+
this.native.bakeMidiFx(clipId, configJson);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/** Backward alias for {@link bakeMidiFx}. */
|
|
1243
|
+
setMidiFx(clipId: number, configJson: string): void {
|
|
1244
|
+
this.bakeMidiFx(clipId, configJson);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Pre-flight check for hanging / unmatched notes in a MIDI clip: reports
|
|
1249
|
+
* whether every note-on has a matching note-off (FIFO per channel+note).
|
|
1250
|
+
* Useful before bouncing to catch a stuck note. Throws if `clipId` is unknown
|
|
1251
|
+
* or not a MIDI clip.
|
|
1252
|
+
*/
|
|
1253
|
+
validateMidiNotes(clipId: number): ProjectNotePairValidation {
|
|
1254
|
+
return this.native.validateMidiNotes(clipId);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/** Detect tempo from a mono buffer and install it; returns the primary BPM. */
|
|
1258
|
+
autoTempo(audio: Float32Array, sampleRate: number): number {
|
|
1259
|
+
return this.native.autoTempo(audio, sampleRate);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/** Snap a PPQ coordinate to the nearest beat of the project grid. */
|
|
1263
|
+
snapToGrid(ppq: number, strength = 1.0): number {
|
|
1264
|
+
return this.native.snapToGrid(ppq, strength);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/** Compile the project into a renderable timeline, surfacing diagnostics. */
|
|
1268
|
+
compile(): ProjectCompileResult {
|
|
1269
|
+
return this.native.compile();
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Compile + render the project offline to interleaved float audio. MIDI
|
|
1274
|
+
* tracks render silently here (no instrument is bound) — use
|
|
1275
|
+
* {@link bounceWithBuiltinInstrument} to make MIDI audible.
|
|
1276
|
+
*
|
|
1277
|
+
* When `totalFrames` is omitted (or `<= 0`) the render length is auto-derived
|
|
1278
|
+
* from the arrangement, so a project with content renders without computing a
|
|
1279
|
+
* frame count; an empty project yields an empty buffer.
|
|
1280
|
+
*
|
|
1281
|
+
* @example
|
|
1282
|
+
* ```typescript
|
|
1283
|
+
* const audio = project.bounce({ numChannels: 2 });
|
|
1284
|
+
* ```
|
|
1285
|
+
*/
|
|
1286
|
+
bounce(options: ProjectBounceOptions = {}): Float32Array {
|
|
1287
|
+
return this.native.bounce(options);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* Compile + render the project offline, routing MIDI tracks through the
|
|
1292
|
+
* built-in oscillator synth so a MIDI-only arrangement bounces to audible
|
|
1293
|
+
* audio. Pass a {@link BuiltinSynthBinding} (or an array of them) to choose
|
|
1294
|
+
* the patch and MIDI destination; omit it (or pass `{}`) for one
|
|
1295
|
+
* default-destination sine patch. An explicitly empty array `[]` (or
|
|
1296
|
+
* `undefined` / `null`) produces zero bindings, so MIDI tracks render silently.
|
|
1297
|
+
*
|
|
1298
|
+
* Like {@link bounce}, omitting `totalFrames` auto-derives the render length
|
|
1299
|
+
* from the arrangement plus the synth's release tail.
|
|
1300
|
+
*
|
|
1301
|
+
* @example
|
|
1302
|
+
* ```typescript
|
|
1303
|
+
* // MIDI-only project -> non-silent stereo audio.
|
|
1304
|
+
* const audio = project.bounceWithBuiltinInstrument(
|
|
1305
|
+
* { waveform: 'saw' },
|
|
1306
|
+
* { numChannels: 2 },
|
|
1307
|
+
* );
|
|
1308
|
+
* ```
|
|
1309
|
+
*/
|
|
1310
|
+
bounceWithBuiltinInstrument(
|
|
1311
|
+
instrument: BuiltinSynthBinding | ReadonlyArray<BuiltinSynthBinding> = {},
|
|
1312
|
+
options: ProjectBounceOptions = {},
|
|
1313
|
+
): Float32Array {
|
|
1314
|
+
return this.native.bounceWithBuiltinInstrument(instrument, options);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* Compile + render the project offline, routing MIDI tracks through the
|
|
1319
|
+
* patch-driven NativeSynth — the full synthesizer (subtractive / FM /
|
|
1320
|
+
* Karplus-Strong / modal / additive / percussion / extended-waveguide-piano
|
|
1321
|
+
* engines plus the realism layer). Pass a {@link SynthPatch}, a preset-name
|
|
1322
|
+
* string (`'saw-lead'` / `'va:saw-lead'`; see {@link synthPresetNames}), or
|
|
1323
|
+
* an array of either; each object entry may carry a `destinationId` binding
|
|
1324
|
+
* convenience (default 0), which is not part of the NativeSynth patch itself.
|
|
1325
|
+
* An explicitly empty array (or `undefined` / `null`) produces zero bindings.
|
|
1326
|
+
* Unknown preset names throw. Deterministic for a fixed project + options +
|
|
1327
|
+
* patch.
|
|
1328
|
+
*/
|
|
1329
|
+
bounceWithSynthInstrument(
|
|
1330
|
+
instrument: SynthPatch | string | ReadonlyArray<SynthPatch | string> = {},
|
|
1331
|
+
options: ProjectBounceOptions = {},
|
|
1332
|
+
): Float32Array {
|
|
1333
|
+
return this.native.bounceWithSynthInstrument(instrument, options);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* Load (parse) SoundFont 2 bytes into the project: presets / instruments /
|
|
1338
|
+
* sample headers plus the sample PCM decoded to a float pool. The host
|
|
1339
|
+
* fetches the `.sf2` and passes the raw bytes; they are copied into linear
|
|
1340
|
+
* memory for the call and not referenced afterwards. Replaces any previously
|
|
1341
|
+
* loaded SoundFont; throws on malformed input (the previous SoundFont is
|
|
1342
|
+
* kept).
|
|
1343
|
+
*/
|
|
1344
|
+
loadSoundFont(data: Uint8Array): void {
|
|
1345
|
+
this.native.loadSoundFont(data);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/** Release the project's loaded SoundFont (no-op when none is loaded). */
|
|
1349
|
+
clearSoundFont(): void {
|
|
1350
|
+
this.native.clearSoundFont();
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/** Number of presets in the loaded SoundFont (0 when none is loaded). */
|
|
1354
|
+
soundFontPresetCount(): number {
|
|
1355
|
+
return this.native.soundFontPresetCount();
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/**
|
|
1359
|
+
* Enumerate every (channel, bank, program) combination the arrangement plays
|
|
1360
|
+
* a note through, in first-use order, reporting whether each resolves in the
|
|
1361
|
+
* loaded SoundFont (`'sf2'`, GS variation/drum fallbacks included) or would
|
|
1362
|
+
* fall back to the built-in synth (`'synth'`). Without a loaded SoundFont
|
|
1363
|
+
* every entry is a synth fallback.
|
|
1364
|
+
*/
|
|
1365
|
+
soundFontManifest(): Sf2ProgramStatus[] {
|
|
1366
|
+
return this.native.soundFontManifest();
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/**
|
|
1370
|
+
* Like {@link bounceWithBuiltinInstrument}, but each bound destination
|
|
1371
|
+
* renders through a GS-compatible SoundFont player fed by the project's
|
|
1372
|
+
* loaded SoundFont ({@link loadSoundFont}): 16 MIDI channels per player,
|
|
1373
|
+
* channel 10 drums via bank 128, GS NRPN part edits and GS/GM SysEx resets
|
|
1374
|
+
* honored. Programs the SoundFont does not cover — including bouncing with
|
|
1375
|
+
* no SoundFont loaded at all — play through the built-in synthesizer GM
|
|
1376
|
+
* fallback bank (the data-free floor; see {@link soundFontManifest} for the
|
|
1377
|
+
* per-program backend). An explicitly empty array `[]` (or `undefined` /
|
|
1378
|
+
* `null`) produces zero bindings, so MIDI tracks render silently.
|
|
1379
|
+
*/
|
|
1380
|
+
bounceWithSf2Instrument(
|
|
1381
|
+
instrument: Sf2InstrumentConfig | ReadonlyArray<Sf2InstrumentConfig> = {},
|
|
1382
|
+
options: ProjectBounceOptions = {},
|
|
1383
|
+
): Float32Array {
|
|
1384
|
+
return this.native.bounceWithSf2Instrument(instrument, options);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/** Remove a clip (undoable). */
|
|
1388
|
+
removeClip(clipId: number): void {
|
|
1389
|
+
this.native.removeClip(clipId);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/** Set a clip's linear playback gain (>= 0; undoable). */
|
|
1393
|
+
setClipGain(clipId: number, gain: number): void {
|
|
1394
|
+
this.native.setClipGain(clipId, gain);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
/** Set a clip's fade-in / fade-out regions (undoable). */
|
|
1398
|
+
setClipFade(clipId: number, fadeIn: ProjectClipFade = {}, fadeOut: ProjectClipFade = {}): void {
|
|
1399
|
+
this.native.setClipFade(clipId, fadeIn, fadeOut);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
/** Replace a clip's take list and active take id (undoable). */
|
|
1403
|
+
setClipTakes(clipId: number, takes: ReadonlyArray<ProjectClipTake>, activeTakeId = 0): void {
|
|
1404
|
+
this.native.setClipTakes(clipId, takes, activeTakeId);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
/** Replace a clip's comp segments (undoable). */
|
|
1408
|
+
setClipCompSegments(clipId: number, segments: ReadonlyArray<ProjectClipCompSegment>): void {
|
|
1409
|
+
this.native.setClipCompSegments(clipId, segments);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/** Set a clip's loop mode + loop length in PPQ (undoable). */
|
|
1413
|
+
setClipLoop(clipId: number, loopMode: ProjectLoopMode, loopLengthPpq = 0): void {
|
|
1414
|
+
this.native.setClipLoop(clipId, projectLoopModeValue(loopMode), loopLengthPpq);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/** Rebind a clip to a different (already-registered) source (undoable). */
|
|
1418
|
+
setClipSource(clipId: number, sourceId: number): void {
|
|
1419
|
+
this.native.setClipSource(clipId, sourceId);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/** Duplicate a clip at `newStartPpq` (same track); returns the new clip id. */
|
|
1423
|
+
duplicateClip(clipId: number, newStartPpq: number): number {
|
|
1424
|
+
return this.native.duplicateClip(clipId, newStartPpq);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/** Remove a track and its clips (undoable). */
|
|
1428
|
+
removeTrack(trackId: number): void {
|
|
1429
|
+
this.native.removeTrack(trackId);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/** Rename a track (undoable). */
|
|
1433
|
+
renameTrack(trackId: number, name: string): void {
|
|
1434
|
+
this.native.renameTrack(trackId, name);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
/** Set a track's mixer-strip binding + output target (undoable; omit / '' clears). */
|
|
1438
|
+
setTrackRoute(trackId: number, channelStripRef?: string, outputTarget?: string): void {
|
|
1439
|
+
this.native.setTrackRoute(trackId, channelStripRef ?? '', outputTarget ?? '');
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/** Append an automation lane to a track; returns the lane index (undoable). */
|
|
1443
|
+
addAutomationLane(trackId: number, desc: ProjectAutomationLaneDesc): number {
|
|
1444
|
+
return this.native.addAutomationLane(trackId, {
|
|
1445
|
+
targetParamId: desc.targetParamId,
|
|
1446
|
+
points: desc.points,
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
/** Replace an existing automation lane in place (undoable). */
|
|
1451
|
+
editAutomationLane(trackId: number, laneIndex: number, desc: ProjectAutomationLaneDesc): void {
|
|
1452
|
+
this.native.editAutomationLane(trackId, laneIndex, {
|
|
1453
|
+
targetParamId: desc.targetParamId,
|
|
1454
|
+
points: desc.points,
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/** Remove an automation lane from a track (undoable). */
|
|
1459
|
+
removeAutomationLane(trackId: number, laneIndex: number): void {
|
|
1460
|
+
this.native.removeAutomationLane(trackId, laneIndex);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/** Replace the project's key annotation stream (undoable). */
|
|
1464
|
+
annotateKeys(keys: ReadonlyArray<ProjectKeySegment>): void {
|
|
1465
|
+
this.native.annotateKeys(keys);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
/** Replace the project's chord-symbol annotation stream (undoable). */
|
|
1469
|
+
annotateChords(chords: ReadonlyArray<ProjectChordSymbol>): void {
|
|
1470
|
+
this.native.annotateChords(chords);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
/** Add or update an opaque assist sidecar by module id + target scope (undoable). */
|
|
1474
|
+
setAssistSidecar(
|
|
1475
|
+
moduleId: string,
|
|
1476
|
+
schemaVersion: number,
|
|
1477
|
+
targetTrackId: number,
|
|
1478
|
+
regionStartPpq: number,
|
|
1479
|
+
regionEndPpq: number,
|
|
1480
|
+
payload: Uint8Array,
|
|
1481
|
+
): void {
|
|
1482
|
+
this.native.setAssistSidecar(
|
|
1483
|
+
moduleId,
|
|
1484
|
+
schemaVersion,
|
|
1485
|
+
targetTrackId,
|
|
1486
|
+
regionStartPpq,
|
|
1487
|
+
regionEndPpq,
|
|
1488
|
+
payload,
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/** Number of assist sidecars currently stored on the project. */
|
|
1493
|
+
assistSidecarCount(): number {
|
|
1494
|
+
return this.native.assistSidecarCount();
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
/** Read one assist sidecar by stable project order. */
|
|
1498
|
+
getAssistSidecar(index: number): ProjectAssistSidecar {
|
|
1499
|
+
return this.native.getAssistSidecar(index);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/** Set the project's clip-overlap policy (SonareProjectOverlapPolicy ordinal). */
|
|
1503
|
+
setOverlapPolicy(policy: number): void {
|
|
1504
|
+
this.native.setOverlapPolicy(policy);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/** Read the project's clip-overlap policy (SonareProjectOverlapPolicy ordinal). */
|
|
1508
|
+
getOverlapPolicy(): number {
|
|
1509
|
+
return this.native.getOverlapPolicy();
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/** Read the project sample rate in Hz. */
|
|
1513
|
+
getSampleRate(): number {
|
|
1514
|
+
return this.native.getSampleRate();
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
/** Replace the project's mixer scene from a scene JSON string. */
|
|
1518
|
+
setMixerSceneJson(sceneJson: string): void {
|
|
1519
|
+
this.native.setMixerSceneJson(sceneJson);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
/**
|
|
1523
|
+
* Add or replace a marker. Pass `markerId` 0 to allocate a new id; returns the
|
|
1524
|
+
* stable marker id (the allocated id when 0 was passed).
|
|
1525
|
+
*/
|
|
1526
|
+
setMarker(markerId: number, ppq: number, name: string): number {
|
|
1527
|
+
return this.native.setMarker(markerId, ppq, name);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
/** Number of tracks in the project. */
|
|
1531
|
+
trackCount(): number {
|
|
1532
|
+
return this.native.trackCount();
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
/** Number of audio sources registered on the project. */
|
|
1536
|
+
sourceCount(): number {
|
|
1537
|
+
return this.native.sourceCount();
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
/** Number of tempo-map segments on the project. */
|
|
1541
|
+
tempoSegmentCount(): number {
|
|
1542
|
+
return this.native.tempoSegmentCount();
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
/** Number of time-signature segments on the project. */
|
|
1546
|
+
timeSignatureCount(): number {
|
|
1547
|
+
return this.native.timeSignatureCount();
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/** Replace the project's tempo map with the given segments. */
|
|
1551
|
+
setTempoSegments(segments: ReadonlyArray<ProjectTempoSegment>): void {
|
|
1552
|
+
this.native.setTempoSegments(segments);
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
/** Replace the project's time-signature map with the given segments. */
|
|
1556
|
+
setTimeSignatures(segments: ReadonlyArray<ProjectTimeSignatureSegment>): void {
|
|
1557
|
+
this.native.setTimeSignatures(segments);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
/**
|
|
1561
|
+
* Compile diagnostics produced by the most recent bounce on this project
|
|
1562
|
+
* (e.g. MIDI clips rendering silently without a bound instrument). When no
|
|
1563
|
+
* bounce has run, the result is empty with `hasTimeline` set.
|
|
1564
|
+
*/
|
|
1565
|
+
lastBounceCompileResult(): ProjectCompileResult {
|
|
1566
|
+
return this.native.lastBounceCompileResult();
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
/** Release the underlying WASM object. Safe to call only once. */
|
|
1570
|
+
delete(): void {
|
|
1571
|
+
this.native.delete();
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/** Alias for {@link delete}, provided for cross-binding (Node) compatibility. */
|
|
1575
|
+
destroy(): void {
|
|
1576
|
+
this.delete();
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
function projectTrackKindValue(kind: ProjectTrackKind | undefined): number {
|
|
1581
|
+
if (kind === undefined || kind === 'audio') {
|
|
1582
|
+
return 0;
|
|
1583
|
+
}
|
|
1584
|
+
if (kind === 'midi') {
|
|
1585
|
+
return 1;
|
|
1586
|
+
}
|
|
1587
|
+
if (kind === 'aux') {
|
|
1588
|
+
return 2;
|
|
1589
|
+
}
|
|
1590
|
+
return kind;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
function projectWarpModeValue(mode: ProjectWarpMode | undefined): number {
|
|
1594
|
+
if (mode === undefined || mode === 'off') {
|
|
1595
|
+
return 0;
|
|
1596
|
+
}
|
|
1597
|
+
if (mode === 'repitch') {
|
|
1598
|
+
return 1;
|
|
1599
|
+
}
|
|
1600
|
+
if (mode === 'tempo-sync') {
|
|
1601
|
+
return 2;
|
|
1602
|
+
}
|
|
1603
|
+
return mode;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
function projectLoopModeValue(mode: ProjectLoopMode | undefined): number {
|
|
1607
|
+
if (mode === undefined || mode === 'off') {
|
|
1608
|
+
return 0;
|
|
1609
|
+
}
|
|
1610
|
+
if (mode === 'loop') {
|
|
1611
|
+
return 1;
|
|
1612
|
+
}
|
|
1613
|
+
return mode;
|
|
1614
|
+
}
|