@hraness/dawg 0.4.1 → 0.6.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/CHANGELOG.md +99 -0
- package/DAWG.md +551 -33
- package/README.md +4 -4
- package/core/chords.ts +288 -7
- package/core/diff.ts +41 -12
- package/core/expression.ts +1241 -0
- package/core/fx.ts +437 -2
- package/core/granular.ts +528 -0
- package/core/instruments.ts +281 -0
- package/core/keys.ts +386 -0
- package/core/loop.ts +23 -0
- package/core/master.ts +455 -0
- package/core/midi.ts +452 -0
- package/core/resonators.ts +574 -0
- package/core/rhythm.ts +7 -2
- package/core/score.ts +717 -62
- package/core/sdk/eval-child.ts +40 -3
- package/core/sdk/print.ts +614 -27
- package/core/sdk/sync-instruments.ts +58 -0
- package/core/sdk/v1.ts +2824 -45
- package/core/sections.ts +2072 -0
- package/core/strings.ts +845 -0
- package/core/synth.ts +11 -1
- package/core/tempo.ts +1318 -0
- package/core/tuning.ts +1180 -0
- package/guides/audition.md +26 -0
- package/guides/automation.md +26 -0
- package/guides/chords.md +28 -0
- package/guides/effects.md +29 -0
- package/guides/faders.md +29 -0
- package/guides/files.md +28 -0
- package/guides/getting-started.md +26 -0
- package/guides/index.ts +75 -0
- package/guides/keys.md +27 -0
- package/guides/media.md +27 -0
- package/guides/mix.md +23 -0
- package/guides/music.md +14 -0
- package/guides/notes.md +26 -0
- package/guides/performance.md +26 -0
- package/guides/play.md +24 -0
- package/guides/project.md +13 -0
- package/guides/providers.md +25 -0
- package/guides/rhythm.md +26 -0
- package/guides/sessions.md +21 -0
- package/guides/sound.md +15 -0
- package/guides/sounds.md +29 -0
- package/guides/tempo.md +27 -0
- package/guides/tracks.md +25 -0
- package/guides/web-search.md +21 -0
- package/package.json +3 -1
- package/src/agent/agent.ts +9 -0
- package/src/agent/brief.ts +77 -3
- package/src/agent/chord-tools.ts +9 -1
- package/src/agent/expression-tools.ts +336 -0
- package/src/agent/granular-tools.ts +138 -0
- package/src/agent/master-tools.ts +299 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +68 -6
- package/src/agent/planner.ts +45 -2
- package/src/agent/preview-tool.ts +27 -4
- package/src/agent/section-tools.ts +411 -0
- package/src/agent/time-tools.ts +290 -0
- package/src/agent/tools.ts +574 -16
- package/src/agent/tuning-tools.ts +301 -0
- package/src/agent/xcb-agent.ts +9 -0
- package/src/audio/arrange.ts +489 -0
- package/src/audio/audition.ts +16 -3
- package/src/audio/click.ts +113 -1
- package/src/audio/clock.ts +71 -5
- package/src/audio/dsp/bank.ts +233 -0
- package/src/audio/dsp/fft.ts +6 -0
- package/src/audio/dsp/filters.ts +57 -0
- package/src/audio/dsp/interp.ts +112 -0
- package/src/audio/dsp/modal.ts +684 -0
- package/src/audio/dsp/onset.ts +197 -0
- package/src/audio/dsp/oversample.ts +202 -0
- package/src/audio/dsp/rng.ts +37 -0
- package/src/audio/dsp/shape.ts +81 -0
- package/src/audio/dsp/stft.ts +73 -0
- package/src/audio/dsp/window.ts +77 -0
- package/src/audio/effects/bus.ts +5 -4
- package/src/audio/effects/chain.ts +6 -1
- package/src/audio/effects/common.ts +30 -1
- package/src/audio/effects/dynamics.ts +2 -1
- package/src/audio/effects/filter.ts +5 -3
- package/src/audio/effects/modulation.ts +5 -1
- package/src/audio/effects/rig/cab.ts +99 -0
- package/src/audio/effects/rig/filters.ts +152 -0
- package/src/audio/effects/rig/gate.ts +39 -0
- package/src/audio/effects/rig/head.ts +487 -0
- package/src/audio/effects/rig/index.ts +170 -0
- package/src/audio/effects/rig/section.ts +78 -0
- package/src/audio/effects/rig/stomp.ts +238 -0
- package/src/audio/effects/space.ts +92 -2
- package/src/audio/engine.ts +100 -21
- package/src/audio/fit.ts +402 -0
- package/src/audio/granular.ts +664 -0
- package/src/audio/instrument-check.ts +133 -0
- package/src/audio/instruments.ts +111 -0
- package/src/audio/keys/dsp.ts +323 -0
- package/src/audio/keys/engine.ts +361 -0
- package/src/audio/keys/piano.ts +432 -0
- package/src/audio/live-worker.ts +54 -0
- package/src/audio/live.ts +263 -17
- package/src/audio/loudness.ts +596 -0
- package/src/audio/master.ts +660 -0
- package/src/audio/measure-worker.ts +45 -0
- package/src/audio/measure.ts +110 -0
- package/src/audio/player.ts +11 -6
- package/src/audio/preview.ts +76 -14
- package/src/audio/render-worker.ts +6 -1
- package/src/audio/renderer.ts +7 -1
- package/src/audio/resonators.ts +287 -0
- package/src/audio/sampler.ts +269 -34
- package/src/audio/samples.ts +30 -7
- package/src/audio/strings/body.ts +250 -0
- package/src/audio/strings/engine.ts +369 -0
- package/src/audio/strings/loop.ts +119 -0
- package/src/audio/strings/measure.test-helpers.ts +198 -0
- package/src/audio/strings/pluck.ts +354 -0
- package/src/audio/synth/voice.ts +60 -13
- package/src/audio/synth/zzfx.ts +10 -4
- package/src/audio/warp.ts +147 -0
- package/src/audio/wav.ts +377 -30
- package/src/commands/arrange.ts +949 -0
- package/src/commands/expression.ts +941 -0
- package/src/commands/fit.ts +135 -0
- package/src/commands/fx.ts +31 -0
- package/src/commands/granular.ts +427 -0
- package/src/commands/help.ts +355 -27
- package/src/commands/keys.ts +353 -0
- package/src/commands/master.ts +361 -0
- package/src/commands/modal.ts +247 -0
- package/src/commands/music.ts +6 -1
- package/src/commands/rig.ts +260 -0
- package/src/commands/sample.ts +3 -0
- package/src/commands/string.ts +175 -0
- package/src/commands/synth.ts +11 -1
- package/src/commands/time.ts +967 -0
- package/src/commands/tuning.ts +490 -0
- package/src/main.ts +919 -41
- package/src/project/check.ts +13 -0
- package/src/render.ts +139 -8
- package/src/session/daemon.ts +18 -8
- package/src/session/naming.ts +8 -1
- package/src/session/rebase.ts +21 -0
- package/src/tui/arrange-menu.ts +390 -0
- package/src/tui/audition.ts +38 -5
- package/src/tui/fader.ts +409 -0
- package/src/tui/granular-menu.ts +278 -0
- package/src/tui/menu-time.ts +401 -0
- package/src/tui/menu.ts +967 -23
- package/src/tui/modal-menu.ts +118 -0
- package/src/tui/performance-menu.ts +235 -0
- package/src/tui/play-chords.ts +3 -1
- package/src/tui/play-mode.ts +150 -6
- package/src/tui/play-session.ts +459 -43
- package/tui/app.ts +262 -13
- package/tui/arrange-strip.ts +174 -0
- package/tui/drawer.ts +478 -0
- package/tui/grammar.ts +63 -1
- package/tui/guide.ts +351 -0
- package/tui/highway.ts +87 -4
- package/tui/hits.ts +68 -0
- package/tui/input.ts +7 -0
- package/tui/keys.ts +72 -0
package/src/audio/live.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* The offline renderer is untouched, so loop renders and exports stay
|
|
9
9
|
* byte-identical. Live audio never reaches a render or an export: the engine
|
|
10
|
-
* mixes it into the stream only.
|
|
10
|
+
* mixes it into the stream only. Like auditions, live notes are pre-master
|
|
11
|
+
* (see `audition.ts`): the song master is a whole-mix stage.
|
|
11
12
|
*/
|
|
12
13
|
import {
|
|
13
14
|
SCORE_LIMITS,
|
|
@@ -15,11 +16,33 @@ import {
|
|
|
15
16
|
isSamplerInstrument,
|
|
16
17
|
type Track,
|
|
17
18
|
} from "../../core/score.ts";
|
|
18
|
-
import {
|
|
19
|
+
import { bpmAtTick } from "../../core/tempo.ts";
|
|
20
|
+
import { EMPTY_SAMPLE_BANK, sampleKey, type SampleBank } from "./samples.ts";
|
|
19
21
|
import { RENDER_CHANNELS, renderScorePcm } from "./wav.ts";
|
|
22
|
+
import { engineFor } from "./instruments.ts";
|
|
23
|
+
import { noteHz, resolveTuning } from "../../core/tuning.ts";
|
|
24
|
+
import { liveFitPending, withLiveFit } from "./fit.ts";
|
|
25
|
+
import type { LiveFullReply, LiveFullRequest } from "./live-worker.ts";
|
|
20
26
|
|
|
21
27
|
/** A rendered live note: interleaved stereo 16-bit PCM. */
|
|
22
|
-
export type LiveNotePcm = Readonly<{
|
|
28
|
+
export type LiveNotePcm = Readonly<{
|
|
29
|
+
pcm: Int16Array;
|
|
30
|
+
frames: number;
|
|
31
|
+
/** Release fade on note-off, in seconds; absent is the 10 ms default. */
|
|
32
|
+
releaseSeconds?: number;
|
|
33
|
+
/**
|
|
34
|
+
* A fitted sample window (0.6 `bpm`/`len`/`fitmode`) longer than 8 s is
|
|
35
|
+
* still being computed: the note is silent and not cached; play it again
|
|
36
|
+
* once its background fit (fit.ts `onFitReady`) lands.
|
|
37
|
+
*/
|
|
38
|
+
fitting?: true;
|
|
39
|
+
/**
|
|
40
|
+
* True when this is only the first window of a guitar-rig note
|
|
41
|
+
* (`LIVE_RIG_WINDOW_SECONDS`): call `LiveSynth.render` again with
|
|
42
|
+
* `full: true` off the key path and swap the result in by voice id.
|
|
43
|
+
*/
|
|
44
|
+
partial?: boolean;
|
|
45
|
+
}>;
|
|
23
46
|
|
|
24
47
|
export type LiveNoteRequest = Readonly<{
|
|
25
48
|
score: TrackScore;
|
|
@@ -31,8 +54,20 @@ export type LiveNoteRequest = Readonly<{
|
|
|
31
54
|
seconds: number;
|
|
32
55
|
/** Decoded sampler voices; without it sampler tracks are silent. */
|
|
33
56
|
samples?: SampleBank;
|
|
57
|
+
/** Song tick being played: the tempo there sets length and synced effects. */
|
|
58
|
+
tick?: number;
|
|
59
|
+
/** Render the whole note even through a guitar rig (the background pass). */
|
|
60
|
+
full?: boolean;
|
|
34
61
|
}>;
|
|
35
62
|
|
|
63
|
+
/**
|
|
64
|
+
* A guitar rig (stomp, head, cab) is oversampled and costs 15-25 ms per
|
|
65
|
+
* track-second, so a held note through one first renders only this window,
|
|
66
|
+
* at render quality, and the rest follows in the background. Every rig stage
|
|
67
|
+
* is causal, so the window is the exact prefix of the full note.
|
|
68
|
+
*/
|
|
69
|
+
export const LIVE_RIG_WINDOW_SECONDS = 0.75;
|
|
70
|
+
|
|
36
71
|
/** Longest a single live note is rendered for (a held sustain pedal). */
|
|
37
72
|
export const MAX_LIVE_NOTE_SECONDS = 8;
|
|
38
73
|
const CACHE_ENTRIES = 96;
|
|
@@ -54,13 +89,29 @@ export class LiveSynth {
|
|
|
54
89
|
return this.cache.size;
|
|
55
90
|
}
|
|
56
91
|
|
|
57
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Stores a whole rig note rendered elsewhere (the background worker) so the
|
|
94
|
+
* next press of that key hits the cache and skips the window.
|
|
95
|
+
*/
|
|
96
|
+
public adopt(request: LiveNoteRequest, pcm: LiveNotePcm): void {
|
|
97
|
+
const plan = this.plan(request);
|
|
98
|
+
if (!plan || pcm.partial) return;
|
|
99
|
+
this.cache.set(plan.key, pcm);
|
|
100
|
+
this.trim();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private plan(request: LiveNoteRequest) {
|
|
58
104
|
const track = request.score.tracks.find(
|
|
59
105
|
(candidate) => candidate.id === request.trackId,
|
|
60
106
|
);
|
|
61
107
|
if (!track) return undefined;
|
|
62
108
|
const { score } = request;
|
|
63
|
-
const
|
|
109
|
+
const liveEngine = engineFor(track);
|
|
110
|
+
const bpm =
|
|
111
|
+
request.tick === undefined
|
|
112
|
+
? score.tempoBpm
|
|
113
|
+
: bpmAtTick(score, request.tick);
|
|
114
|
+
const ticksPerSecond = (score.ticksPerBeat * bpm) / 60;
|
|
64
115
|
const seconds = Math.max(
|
|
65
116
|
0.01,
|
|
66
117
|
Math.min(MAX_LIVE_NOTE_SECONDS, request.seconds),
|
|
@@ -76,11 +127,47 @@ export class LiveSynth {
|
|
|
76
127
|
pitch,
|
|
77
128
|
velocity,
|
|
78
129
|
durationTicks,
|
|
79
|
-
|
|
130
|
+
bpm,
|
|
80
131
|
score.ticksPerBeat,
|
|
81
132
|
this.sampleRate,
|
|
82
133
|
samplerDigest(track, request.samples),
|
|
134
|
+
// Assets a 0.6 engine reads (a granular sample source).
|
|
135
|
+
...engineDigest(track, request.samples),
|
|
136
|
+
// The song tuning and key (the default tuning root) retune live notes.
|
|
137
|
+
...(score.tuning || track.tuning
|
|
138
|
+
? [score.tuning ?? null, score.key]
|
|
139
|
+
: []),
|
|
140
|
+
// A 0.6 engine's assets, and the key it may tune to.
|
|
141
|
+
...(liveEngine
|
|
142
|
+
? [
|
|
143
|
+
liveEngine.assetDigests?.(
|
|
144
|
+
track,
|
|
145
|
+
request.samples ?? EMPTY_SAMPLE_BANK,
|
|
146
|
+
) ?? [],
|
|
147
|
+
score.key ?? null,
|
|
148
|
+
]
|
|
149
|
+
: []),
|
|
83
150
|
]);
|
|
151
|
+
return { track, score, bpm, seconds, durationTicks, velocity, pitch, key };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
public render(request: LiveNoteRequest): LiveNotePcm | undefined {
|
|
155
|
+
const plan = this.plan(request);
|
|
156
|
+
if (!plan) return undefined;
|
|
157
|
+
const { track, score, bpm, seconds, durationTicks, velocity, pitch, key } =
|
|
158
|
+
plan;
|
|
159
|
+
const liveEngine = engineFor(track);
|
|
160
|
+
// The first window of a rig note: its own cache entry, swapped for the
|
|
161
|
+
// whole note once the background pass has rendered it.
|
|
162
|
+
const windowed =
|
|
163
|
+
request.full !== true &&
|
|
164
|
+
hasRig(track) &&
|
|
165
|
+
seconds > LIVE_RIG_WINDOW_SECONDS &&
|
|
166
|
+
!this.cache.has(key);
|
|
167
|
+
if (windowed) {
|
|
168
|
+
const first = this.cache.get(`${key}#window`);
|
|
169
|
+
if (first) return first;
|
|
170
|
+
}
|
|
84
171
|
const hit = this.cache.get(key);
|
|
85
172
|
if (hit) {
|
|
86
173
|
// Refresh LRU order.
|
|
@@ -94,10 +181,13 @@ export class LiveSynth {
|
|
|
94
181
|
Math.min(SCORE_LIMITS.maxBars, Math.ceil(beats / score.beatsPerBar)),
|
|
95
182
|
);
|
|
96
183
|
const single = new TrackScore({
|
|
97
|
-
tempoBpm:
|
|
184
|
+
tempoBpm: bpm,
|
|
98
185
|
beatsPerBar: score.beatsPerBar,
|
|
99
186
|
bars,
|
|
100
187
|
ticksPerBeat: score.ticksPerBeat,
|
|
188
|
+
// The song tuning, and the key whose tonic is the default root.
|
|
189
|
+
...(score.tuning ? { tuning: score.tuning } : {}),
|
|
190
|
+
...(score.tuning || track.tuning || liveEngine ? { key: score.key } : {}),
|
|
101
191
|
tracks: [liveTrack(track)],
|
|
102
192
|
notes: [
|
|
103
193
|
{
|
|
@@ -110,32 +200,96 @@ export class LiveSynth {
|
|
|
110
200
|
},
|
|
111
201
|
],
|
|
112
202
|
});
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
203
|
+
// A 0.6 engine's ring-out sets the one-note length and the key release.
|
|
204
|
+
const tail = liveEngine
|
|
205
|
+
? Math.max(0, liveEngine.tailSeconds(track, pitch))
|
|
206
|
+
: 0;
|
|
207
|
+
// Fitted sample windows over 8 s fit in the background (silent until
|
|
208
|
+
// ready, never at the wrong pitch); shorter ones fit synchronously.
|
|
209
|
+
const audio = withLiveFit(() =>
|
|
210
|
+
renderScorePcm(single, {
|
|
211
|
+
sampleRate: this.sampleRate,
|
|
212
|
+
// The renderer's shortest one-shot is 1 s; the window is cut below.
|
|
213
|
+
maxSeconds: windowed
|
|
214
|
+
? 1
|
|
215
|
+
: Math.max(MAX_LIVE_NOTE_SECONDS + 4, seconds + tail),
|
|
216
|
+
...(request.samples ? { samples: request.samples } : {}),
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
const frames = windowed
|
|
220
|
+
? Math.min(
|
|
221
|
+
audibleFrames(audio.pcm),
|
|
222
|
+
Math.round(LIVE_RIG_WINDOW_SECONDS * this.sampleRate),
|
|
223
|
+
)
|
|
224
|
+
: audibleFrames(audio.pcm);
|
|
225
|
+
const release = liveEngine?.releaseSeconds
|
|
226
|
+
? liveEngine.releaseSeconds(
|
|
227
|
+
track,
|
|
228
|
+
pitch,
|
|
229
|
+
noteHz(
|
|
230
|
+
pitch,
|
|
231
|
+
undefined,
|
|
232
|
+
resolveTuning(score.tuning, track.tuning, score.key),
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
: tail;
|
|
119
236
|
const rendered: LiveNotePcm = {
|
|
120
237
|
pcm: audio.pcm.subarray(0, frames * RENDER_CHANNELS),
|
|
121
238
|
frames,
|
|
239
|
+
...(liveEngine
|
|
240
|
+
? {
|
|
241
|
+
releaseSeconds: Math.min(
|
|
242
|
+
Math.max(0, release),
|
|
243
|
+
MAX_LIVE_NOTE_SECONDS,
|
|
244
|
+
),
|
|
245
|
+
}
|
|
246
|
+
: {}),
|
|
247
|
+
...(windowed ? { partial: true } : {}),
|
|
122
248
|
};
|
|
123
|
-
|
|
249
|
+
if (liveFitPending()) return { ...rendered, fitting: true };
|
|
250
|
+
this.cache.set(windowed ? `${key}#window` : key, rendered);
|
|
251
|
+
this.trim();
|
|
252
|
+
return rendered;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private trim(): void {
|
|
124
256
|
while (this.cache.size > CACHE_ENTRIES) {
|
|
125
257
|
const oldest = this.cache.keys().next().value;
|
|
126
258
|
if (oldest === undefined) break;
|
|
127
259
|
this.cache.delete(oldest);
|
|
128
260
|
}
|
|
129
|
-
return rendered;
|
|
130
261
|
}
|
|
131
262
|
}
|
|
132
263
|
|
|
133
|
-
/**
|
|
264
|
+
/**
|
|
265
|
+
* The track as it sounds live: audible regardless of mute and solo, and
|
|
266
|
+
* without track `time`, so a key sounds once, as played, not re-placed.
|
|
267
|
+
*/
|
|
134
268
|
function liveTrack(track: Track): Track {
|
|
135
|
-
|
|
269
|
+
// A held key is the performance: the recorded pedal and the render-time
|
|
270
|
+
// humanize stay out of it. The velocity curve stays, so a key sounds like
|
|
271
|
+
// the note it records as.
|
|
272
|
+
const {
|
|
273
|
+
solo: _solo,
|
|
274
|
+
time: _time,
|
|
275
|
+
pedal: _pedal,
|
|
276
|
+
humanize: _humanize,
|
|
277
|
+
...rest
|
|
278
|
+
} = track;
|
|
136
279
|
return { ...rest, muted: false };
|
|
137
280
|
}
|
|
138
281
|
|
|
282
|
+
/** Whether the track plays through a guitar rig stage. */
|
|
283
|
+
function hasRig(track: Track): boolean {
|
|
284
|
+
return Boolean(track.fx?.stomp || track.fx?.head || track.fx?.cab);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function engineDigest(track: Track, bank: SampleBank | undefined): string[] {
|
|
288
|
+
const engine = engineFor(track);
|
|
289
|
+
if (!engine?.assetDigests || !bank) return [];
|
|
290
|
+
return [...engine.assetDigests(track, bank)];
|
|
291
|
+
}
|
|
292
|
+
|
|
139
293
|
function samplerDigest(track: Track, bank: SampleBank | undefined): string[] {
|
|
140
294
|
if (!isSamplerInstrument(track.instrument) || !track.sampler || !bank)
|
|
141
295
|
return [];
|
|
@@ -150,3 +304,95 @@ function audibleFrames(pcm: Int16Array): number {
|
|
|
150
304
|
while (last >= 0 && pcm[last] === 0) last -= 1;
|
|
151
305
|
return Math.ceil((last + 1) / RENDER_CHANNELS);
|
|
152
306
|
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Renders whole guitar-rig notes on a worker (`live-worker.ts`), in request
|
|
310
|
+
* order. When a worker cannot start, `full` falls back to rendering on the
|
|
311
|
+
* calling thread in a later tick, as before.
|
|
312
|
+
*/
|
|
313
|
+
export class LiveFullRenderer {
|
|
314
|
+
private worker: Bun.Worker | undefined;
|
|
315
|
+
private broken = false;
|
|
316
|
+
private nextId = 1;
|
|
317
|
+
private readonly pending = new Map<
|
|
318
|
+
number,
|
|
319
|
+
(pcm: LiveNotePcm | undefined) => void
|
|
320
|
+
>();
|
|
321
|
+
|
|
322
|
+
public full(
|
|
323
|
+
synth: LiveSynth,
|
|
324
|
+
request: LiveNoteRequest,
|
|
325
|
+
): Promise<LiveNotePcm | undefined> {
|
|
326
|
+
const worker = this.ensure();
|
|
327
|
+
if (!worker)
|
|
328
|
+
return new Promise((resolve) =>
|
|
329
|
+
setTimeout(() => resolve(synth.render({ ...request, full: true })), 0),
|
|
330
|
+
);
|
|
331
|
+
const id = this.nextId;
|
|
332
|
+
this.nextId += 1;
|
|
333
|
+
return new Promise((resolve) => {
|
|
334
|
+
this.pending.set(id, (pcm) => {
|
|
335
|
+
if (pcm) synth.adopt(request, pcm);
|
|
336
|
+
resolve(pcm);
|
|
337
|
+
});
|
|
338
|
+
const message: LiveFullRequest = {
|
|
339
|
+
id,
|
|
340
|
+
sampleRate: synth.rate,
|
|
341
|
+
score: request.score.toJSON(),
|
|
342
|
+
trackId: request.trackId,
|
|
343
|
+
pitch: request.pitch,
|
|
344
|
+
velocity: request.velocity,
|
|
345
|
+
seconds: request.seconds,
|
|
346
|
+
...(request.tick === undefined ? {} : { tick: request.tick }),
|
|
347
|
+
...(request.samples ? { samples: request.samples } : {}),
|
|
348
|
+
};
|
|
349
|
+
try {
|
|
350
|
+
worker.postMessage(message);
|
|
351
|
+
} catch {
|
|
352
|
+
this.pending.delete(id);
|
|
353
|
+
this.fail();
|
|
354
|
+
setTimeout(() => resolve(synth.render({ ...request, full: true })), 0);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Stops the worker; pending notes resolve without a full pass. */
|
|
360
|
+
public close(): void {
|
|
361
|
+
this.fail();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private ensure(): Bun.Worker | undefined {
|
|
365
|
+
if (this.worker) return this.worker;
|
|
366
|
+
if (this.broken) return undefined;
|
|
367
|
+
try {
|
|
368
|
+
// The DOM lib's Worker type lacks Bun's `unref`; the runtime is Bun's.
|
|
369
|
+
const worker = new Worker(
|
|
370
|
+
new URL("./live-worker.ts", import.meta.url).href,
|
|
371
|
+
) as unknown as Bun.Worker;
|
|
372
|
+
worker.addEventListener("message", (event: Event) => {
|
|
373
|
+
const reply = (event as MessageEvent<LiveFullReply>).data;
|
|
374
|
+
const done = this.pending.get(reply.id);
|
|
375
|
+
if (!done) return;
|
|
376
|
+
this.pending.delete(reply.id);
|
|
377
|
+
done(reply.pcm);
|
|
378
|
+
});
|
|
379
|
+
worker.addEventListener("error", () => this.fail());
|
|
380
|
+
worker.unref();
|
|
381
|
+
this.worker = worker;
|
|
382
|
+
return worker;
|
|
383
|
+
} catch {
|
|
384
|
+
this.broken = true;
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private fail(): void {
|
|
390
|
+
this.broken = true;
|
|
391
|
+
const worker = this.worker;
|
|
392
|
+
this.worker = undefined;
|
|
393
|
+
worker?.terminate();
|
|
394
|
+
const waiters = [...this.pending.values()];
|
|
395
|
+
this.pending.clear();
|
|
396
|
+
for (const done of waiters) done(undefined);
|
|
397
|
+
}
|
|
398
|
+
}
|