@hraness/dawg 0.6.0 → 0.7.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 +126 -0
- package/DAWG.md +655 -141
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +759 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -99
- package/core/expression.ts +158 -6
- package/core/fx.ts +354 -32
- package/core/granular.ts +91 -0
- package/core/instruments.ts +180 -0
- package/core/keys.ts +573 -4
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +306 -4
- package/core/routing.ts +165 -0
- package/core/score.ts +1181 -16
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +354 -6
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +3160 -69
- package/core/sections.ts +298 -24
- package/core/sing.ts +815 -0
- package/core/strings.ts +237 -2
- package/core/style-provenance.ts +80 -0
- package/core/styles/africa-mena-southasia.ts +2893 -0
- package/core/styles/americas.ts +3810 -0
- package/core/styles/art.ts +4993 -0
- package/core/styles/base.ts +123 -0
- package/core/styles/cycles.ts +106 -0
- package/core/styles/electronic.ts +2723 -0
- package/core/styles/europe-asia-pacific.ts +2838 -0
- package/core/styles/excerpt.ts +29 -0
- package/core/styles/gamelan.ts +283 -0
- package/core/styles/generate.ts +2199 -0
- package/core/styles/index.ts +515 -0
- package/core/styles/parts.ts +106 -0
- package/core/styles/pop.ts +3189 -0
- package/core/styles/rock.ts +2993 -0
- package/core/styles/roots.ts +4175 -0
- package/core/styles/schema.ts +429 -0
- package/core/styles/taxonomy.ts +940 -0
- package/core/styles/validate.ts +528 -0
- package/core/tempo.ts +32 -2
- package/core/tuning.ts +19 -3
- package/core/vocoder.ts +524 -0
- package/core/winds.ts +652 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +30 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -6
- package/guides/chords.md +16 -14
- package/guides/effects.md +17 -15
- package/guides/faders.md +20 -16
- package/guides/files.md +13 -9
- package/guides/getting-started.md +15 -11
- package/guides/keys.md +19 -15
- package/guides/media.md +17 -12
- package/guides/mix.md +12 -6
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/performance.md +15 -10
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +32 -0
- package/guides/rhythm.md +17 -13
- package/guides/sessions.md +17 -7
- package/guides/show-me.md +31 -0
- package/guides/sound.md +25 -9
- package/guides/sounds.md +15 -12
- package/guides/styles.md +31 -0
- package/guides/tempo.md +15 -10
- package/guides/tracks.md +15 -10
- package/guides/tuning.md +32 -0
- package/guides/voice.md +31 -0
- package/guides/web-search.md +18 -8
- package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
- package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
- package/native/prebuilt/manifest.json +21 -0
- package/package.json +5 -2
- package/src/agent/agent.ts +131 -13
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +369 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +15 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +13 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +5 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/rhythm-tools.ts +1 -1
- package/src/agent/section-tools.ts +1 -1
- package/src/agent/show-me.ts +497 -0
- package/src/agent/steer.ts +15 -0
- package/src/agent/style-tools.ts +217 -0
- package/src/agent/tool-error.ts +12 -0
- package/src/agent/tools.ts +307 -37
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +16 -6
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +58 -3
- package/src/audio/autotune-engine.ts +101 -0
- package/src/audio/autotune.ts +640 -0
- package/src/audio/clips.ts +240 -0
- package/src/audio/doctor.ts +86 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +171 -0
- package/src/audio/dsp/follow.ts +120 -0
- package/src/audio/dsp/formant.ts +427 -0
- package/src/audio/dsp/glottal.ts +243 -0
- package/src/audio/dsp/interp.ts +7 -2
- package/src/audio/dsp/lpc.ts +50 -0
- package/src/audio/dsp/periodicity.ts +59 -0
- package/src/audio/dsp/pitch.ts +995 -0
- package/src/audio/dsp/psola.ts +199 -0
- package/src/audio/dsp/shift.ts +256 -0
- package/src/audio/effects/chain.ts +21 -4
- package/src/audio/effects/common.ts +57 -0
- package/src/audio/effects/convolution.ts +113 -5
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/engine.ts +206 -33
- package/src/audio/fit.ts +80 -3
- package/src/audio/granular.ts +352 -21
- package/src/audio/instrument-check.ts +67 -47
- package/src/audio/instruments.ts +36 -3
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +427 -0
- package/src/audio/keys/engine.ts +156 -20
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +72 -8
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/kits.ts +135 -6
- package/src/audio/live-worker.ts +3 -1
- package/src/audio/live.ts +247 -26
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +45 -3
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +286 -0
- package/src/audio/resonators.ts +10 -2
- package/src/audio/sampler.ts +252 -24
- package/src/audio/samples.ts +46 -3
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/body.ts +29 -16
- package/src/audio/strings/bow.ts +699 -0
- package/src/audio/strings/engine.ts +274 -22
- package/src/audio/strings/measure.test-helpers.ts +2 -0
- package/src/audio/synth/oscillators.ts +31 -21
- package/src/audio/synth/voice.ts +34 -1
- package/src/audio/vocoder/bank.ts +314 -0
- package/src/audio/vocoder/carrier.ts +165 -0
- package/src/audio/vocoder/control.ts +68 -0
- package/src/audio/vocoder/detect.ts +50 -0
- package/src/audio/vocoder/index.ts +304 -0
- package/src/audio/vocoder/talkbox.ts +143 -0
- package/src/audio/wav.ts +608 -85
- package/src/audio/winds/engine.ts +306 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +104 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +492 -0
- package/src/auth/cli.ts +38 -36
- package/src/auth/credentials.ts +30 -1
- package/src/auth/login.ts +15 -9
- package/src/auth/tui.ts +19 -10
- package/src/commands/arrange.ts +44 -29
- package/src/commands/autotune.ts +421 -0
- package/src/commands/calibration.ts +74 -0
- package/src/commands/clips.ts +887 -0
- package/src/commands/drums.ts +3 -2
- package/src/commands/edit.ts +11 -4
- package/src/commands/expression.ts +104 -23
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +105 -33
- package/src/commands/grammar.ts +558 -0
- package/src/commands/granular.ts +7 -2
- package/src/commands/help.ts +652 -339
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +293 -17
- package/src/commands/modal.ts +67 -4
- package/src/commands/music.ts +1 -1
- package/src/commands/nearest.ts +53 -0
- package/src/commands/pack.ts +9 -2
- package/src/commands/param-range.ts +56 -0
- package/src/commands/parses.ts +130 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/resample.ts +281 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +8 -25
- package/src/commands/sample.ts +49 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/sing.ts +478 -0
- package/src/commands/string.ts +28 -2
- package/src/commands/strum.ts +485 -0
- package/src/commands/style.ts +415 -0
- package/src/commands/time.ts +6 -3
- package/src/commands/tuning.ts +3 -3
- package/src/commands/vocal-pitch.ts +616 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +244 -0
- package/src/fs/durable.ts +50 -0
- package/src/lang/glossary.ts +493 -0
- package/src/launch-args.ts +163 -0
- package/src/main.ts +1521 -255
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +26 -3
- package/src/project/clip-pins.ts +72 -0
- package/src/project/init.ts +23 -8
- package/src/project/sync.ts +418 -86
- package/src/render.ts +36 -1
- package/src/session/daemon.ts +3 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +154 -0
- package/src/session/port.ts +22 -4
- package/src/session/presence.ts +50 -7
- package/src/session/protocol.ts +5 -1
- package/src/session/rebase.ts +18 -5
- package/src/session/receipt.ts +258 -0
- package/src/session/store.ts +65 -43
- package/src/tui/arrange-menu.ts +65 -31
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +18 -13
- package/src/tui/fader.ts +228 -41
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/menu-clips.ts +297 -0
- package/src/tui/menu-time.ts +20 -13
- package/src/tui/menu-voice.ts +405 -0
- package/src/tui/menu.ts +1377 -193
- package/src/tui/modal-menu.ts +38 -11
- package/src/tui/performance-menu.ts +46 -1
- package/src/tui/play-chords.ts +108 -5
- package/src/tui/play-mode.ts +16 -1
- package/src/tui/play-session.ts +81 -16
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +144 -0
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +274 -40
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +144 -0
- package/tui/drawer.ts +70 -22
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +112 -63
- package/tui/guide.ts +42 -4
- package/tui/highway.ts +269 -25
- package/tui/hints.ts +192 -0
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/play-strip.ts +68 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +144 -14
- package/tui/theme.ts +27 -0
package/src/audio/live.ts
CHANGED
|
@@ -19,10 +19,18 @@ import {
|
|
|
19
19
|
import { bpmAtTick } from "../../core/tempo.ts";
|
|
20
20
|
import { EMPTY_SAMPLE_BANK, sampleKey, type SampleBank } from "./samples.ts";
|
|
21
21
|
import { RENDER_CHANNELS, renderScorePcm } from "./wav.ts";
|
|
22
|
-
import { engineFor } from "./instruments.ts";
|
|
22
|
+
import { engineFor, RING_OUT_FADE_SECONDS } from "./instruments.ts";
|
|
23
|
+
import { chordDigest, chordTimeline } from "./granular.ts";
|
|
24
|
+
import { resolveGranular } from "../../core/granular.ts";
|
|
23
25
|
import { noteHz, resolveTuning } from "../../core/tuning.ts";
|
|
24
26
|
import { liveFitPending, withLiveFit } from "./fit.ts";
|
|
27
|
+
import { isOrganFamily } from "../../core/keys.ts";
|
|
25
28
|
import type { LiveFullReply, LiveFullRequest } from "./live-worker.ts";
|
|
29
|
+
import { isBowed, liveStringTrack } from "./strings/engine.ts";
|
|
30
|
+
import { resolveString } from "../../core/strings.ts";
|
|
31
|
+
import { singTrack } from "../../core/sing.ts";
|
|
32
|
+
import { warmGlottal } from "./dsp/glottal.ts";
|
|
33
|
+
import { withSingHorizon } from "./sing/engine.ts";
|
|
26
34
|
|
|
27
35
|
/** A rendered live note: interleaved stereo 16-bit PCM. */
|
|
28
36
|
export type LiveNotePcm = Readonly<{
|
|
@@ -37,11 +45,22 @@ export type LiveNotePcm = Readonly<{
|
|
|
37
45
|
*/
|
|
38
46
|
fitting?: true;
|
|
39
47
|
/**
|
|
40
|
-
* True when this is only the first window of a guitar-rig note
|
|
48
|
+
* True when this is only the first window of a guitar-rig or bowed note
|
|
41
49
|
* (`LIVE_RIG_WINDOW_SECONDS`): call `LiveSynth.render` again with
|
|
42
50
|
* `full: true` off the key path and swap the result in by voice id.
|
|
43
51
|
*/
|
|
44
52
|
partial?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* The organ clock tick a first window rendered at: pass it back as the
|
|
55
|
+
* request's `clock` for the full pass so the wheels and rotors continue.
|
|
56
|
+
*/
|
|
57
|
+
clock?: number;
|
|
58
|
+
/**
|
|
59
|
+
* The key's release does not fade the voice: it rings to the end of its
|
|
60
|
+
* rendered tail (0.6.1 modal `damp 0`), which ends in a `RING_OUT_FADE_SECONDS`
|
|
61
|
+
* fade when the length cap cut it.
|
|
62
|
+
*/
|
|
63
|
+
ringOut?: true;
|
|
45
64
|
}>;
|
|
46
65
|
|
|
47
66
|
export type LiveNoteRequest = Readonly<{
|
|
@@ -58,6 +77,8 @@ export type LiveNoteRequest = Readonly<{
|
|
|
58
77
|
tick?: number;
|
|
59
78
|
/** Render the whole note even through a guitar rig (the background pass). */
|
|
60
79
|
full?: boolean;
|
|
80
|
+
/** An organ note's clock tick from its first window (`LiveNotePcm.clock`). */
|
|
81
|
+
clock?: number;
|
|
61
82
|
}>;
|
|
62
83
|
|
|
63
84
|
/**
|
|
@@ -78,8 +99,21 @@ const CACHE_ENTRIES = 96;
|
|
|
78
99
|
*/
|
|
79
100
|
export class LiveSynth {
|
|
80
101
|
private readonly cache = new Map<string, LiveNotePcm>();
|
|
102
|
+
/** f061-organ: the live organ clock's origin (performance.now ms). */
|
|
103
|
+
private readonly organEpoch = performance.now();
|
|
81
104
|
|
|
82
|
-
|
|
105
|
+
/**
|
|
106
|
+
* `fastTail: false` always renders the whole tail window (tests compare
|
|
107
|
+
* the release-sized buffer against it).
|
|
108
|
+
*/
|
|
109
|
+
public constructor(
|
|
110
|
+
private readonly sampleRate: number,
|
|
111
|
+
private readonly options: Readonly<{
|
|
112
|
+
fastTail?: boolean;
|
|
113
|
+
/** Called with each one-note render's buffer length, in frames. */
|
|
114
|
+
onRender?: (frames: number) => void;
|
|
115
|
+
}> = {},
|
|
116
|
+
) {}
|
|
83
117
|
|
|
84
118
|
public get rate(): number {
|
|
85
119
|
return this.sampleRate;
|
|
@@ -122,6 +156,38 @@ export class LiveSynth {
|
|
|
122
156
|
);
|
|
123
157
|
const velocity = Math.max(0, Math.min(1, request.velocity));
|
|
124
158
|
const pitch = Math.max(0, Math.min(127, Math.round(request.pitch)));
|
|
159
|
+
// f061-organ: one rotating speaker and one set of free-running wheels
|
|
160
|
+
// per organ track. Each key renders from the song tick it sounds at
|
|
161
|
+
// (or the synth's own clock when stopped), so successive notes share
|
|
162
|
+
// the track's rotor and wheel phases instead of restarting them.
|
|
163
|
+
const organTick =
|
|
164
|
+
track.keys && isOrganFamily(track.instrument)
|
|
165
|
+
? Math.max(
|
|
166
|
+
0,
|
|
167
|
+
Math.round(
|
|
168
|
+
request.clock ??
|
|
169
|
+
request.tick ??
|
|
170
|
+
((performance.now() - this.organEpoch) / 1000) * ticksPerSecond,
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
: undefined;
|
|
174
|
+
// Granular `quant chord` (0.6.1): a live note snaps to the song's
|
|
175
|
+
// chords at the played tick, as the offline render of the same note.
|
|
176
|
+
const quantChord =
|
|
177
|
+
track.granular !== undefined &&
|
|
178
|
+
liveEngine?.id === "granular" &&
|
|
179
|
+
resolveGranular(track.granular).quant === "chord"
|
|
180
|
+
? { score, tick: request.tick ?? 0 }
|
|
181
|
+
: undefined;
|
|
182
|
+
const chord = quantChord
|
|
183
|
+
? chordDigest(
|
|
184
|
+
chordTimeline(score),
|
|
185
|
+
quantChord.tick,
|
|
186
|
+
quantChord.tick +
|
|
187
|
+
durationTicks +
|
|
188
|
+
Math.ceil(liveEngine!.tailSeconds(track, pitch) * ticksPerSecond),
|
|
189
|
+
)
|
|
190
|
+
: undefined;
|
|
125
191
|
const key = JSON.stringify([
|
|
126
192
|
liveTrack(track),
|
|
127
193
|
pitch,
|
|
@@ -137,6 +203,7 @@ export class LiveSynth {
|
|
|
137
203
|
...(score.tuning || track.tuning
|
|
138
204
|
? [score.tuning ?? null, score.key]
|
|
139
205
|
: []),
|
|
206
|
+
...(organTick !== undefined ? [`organ:${organTick}`] : []),
|
|
140
207
|
// A 0.6 engine's assets, and the key it may tune to.
|
|
141
208
|
...(liveEngine
|
|
142
209
|
? [
|
|
@@ -147,21 +214,48 @@ export class LiveSynth {
|
|
|
147
214
|
score.key ?? null,
|
|
148
215
|
]
|
|
149
216
|
: []),
|
|
217
|
+
...(chord ? [chord] : []),
|
|
150
218
|
]);
|
|
151
|
-
return {
|
|
219
|
+
return {
|
|
220
|
+
track,
|
|
221
|
+
score,
|
|
222
|
+
bpm,
|
|
223
|
+
seconds,
|
|
224
|
+
durationTicks,
|
|
225
|
+
velocity,
|
|
226
|
+
pitch,
|
|
227
|
+
key,
|
|
228
|
+
organTick,
|
|
229
|
+
quantChord,
|
|
230
|
+
};
|
|
152
231
|
}
|
|
153
232
|
|
|
154
233
|
public render(request: LiveNoteRequest): LiveNotePcm | undefined {
|
|
155
234
|
const plan = this.plan(request);
|
|
156
235
|
if (!plan) return undefined;
|
|
157
|
-
const {
|
|
158
|
-
|
|
236
|
+
const {
|
|
237
|
+
track,
|
|
238
|
+
score,
|
|
239
|
+
bpm,
|
|
240
|
+
seconds,
|
|
241
|
+
durationTicks,
|
|
242
|
+
velocity,
|
|
243
|
+
pitch,
|
|
244
|
+
key,
|
|
245
|
+
organTick,
|
|
246
|
+
quantChord,
|
|
247
|
+
} = plan;
|
|
159
248
|
const liveEngine = engineFor(track);
|
|
160
249
|
// The first window of a rig note: its own cache entry, swapped for the
|
|
161
250
|
// whole note once the background pass has rendered it.
|
|
251
|
+
// Organs too: wheels, rotors and drive cost 4-8 ms per note-second.
|
|
162
252
|
const windowed =
|
|
163
253
|
request.full !== true &&
|
|
164
|
-
hasRig(track)
|
|
254
|
+
(hasRig(track) ||
|
|
255
|
+
organTick !== undefined ||
|
|
256
|
+
bowedTrack(track) ||
|
|
257
|
+
hasFormant(track) ||
|
|
258
|
+
singTrack(track)) &&
|
|
165
259
|
seconds > LIVE_RIG_WINDOW_SECONDS &&
|
|
166
260
|
!this.cache.has(key);
|
|
167
261
|
if (windowed) {
|
|
@@ -206,22 +300,8 @@ export class LiveSynth {
|
|
|
206
300
|
: 0;
|
|
207
301
|
// Fitted sample windows over 8 s fit in the background (silent until
|
|
208
302
|
// ready, never at the wrong pitch); shorter ones fit synchronously.
|
|
209
|
-
const
|
|
210
|
-
|
|
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);
|
|
303
|
+
const windowFrames = Math.round(LIVE_RIG_WINDOW_SECONDS * this.sampleRate);
|
|
304
|
+
const horizon = windowed ? windowFrames : Infinity;
|
|
225
305
|
const release = liveEngine?.releaseSeconds
|
|
226
306
|
? liveEngine.releaseSeconds(
|
|
227
307
|
track,
|
|
@@ -233,9 +313,51 @@ export class LiveSynth {
|
|
|
233
313
|
),
|
|
234
314
|
)
|
|
235
315
|
: tail;
|
|
316
|
+
const renderFor = (maxSeconds: number) => {
|
|
317
|
+
const out = withLiveFit(() =>
|
|
318
|
+
withSingHorizon(horizon, () =>
|
|
319
|
+
renderScorePcm(single, {
|
|
320
|
+
sampleRate: this.sampleRate,
|
|
321
|
+
maxSeconds,
|
|
322
|
+
...(request.samples ? { samples: request.samples } : {}),
|
|
323
|
+
...(organTick ? { seedTick: organTick } : {}),
|
|
324
|
+
...(quantChord ? { quantChord } : {}),
|
|
325
|
+
}),
|
|
326
|
+
),
|
|
327
|
+
);
|
|
328
|
+
this.options.onRender?.(out.frames);
|
|
329
|
+
return out;
|
|
330
|
+
};
|
|
331
|
+
const fullSeconds = Math.max(MAX_LIVE_NOTE_SECONDS + 4, seconds + tail);
|
|
332
|
+
// A damped key decays 60 dB per 4.6 x its release (a linear fade of the
|
|
333
|
+
// same energy), so 12 x release is past -150 dB: silent in 16 bits. The
|
|
334
|
+
// renderer is causal, so the short buffer is the full one's prefix; a
|
|
335
|
+
// note still sounding at its end falls back to the full window.
|
|
336
|
+
const shortSeconds =
|
|
337
|
+
!windowed &&
|
|
338
|
+
this.options.fastTail !== false &&
|
|
339
|
+
liveFastTail(track, liveEngine !== undefined)
|
|
340
|
+
? seconds + Math.min(tail, LIVE_TAIL_RELEASES * release) + 0.1
|
|
341
|
+
: Infinity;
|
|
342
|
+
let audio =
|
|
343
|
+
// The renderer's shortest one-shot is 1 s; the window is cut below.
|
|
344
|
+
windowed ? renderFor(1) : renderFor(Math.min(fullSeconds, shortSeconds));
|
|
345
|
+
if (
|
|
346
|
+
shortSeconds < fullSeconds &&
|
|
347
|
+
audibleFrames(audio.pcm) >
|
|
348
|
+
audio.frames - Math.round(LIVE_TAIL_GUARD_SECONDS * this.sampleRate)
|
|
349
|
+
)
|
|
350
|
+
audio = renderFor(fullSeconds);
|
|
351
|
+
const frames = windowed
|
|
352
|
+
? Math.min(audibleFrames(audio.pcm), windowFrames)
|
|
353
|
+
: audibleFrames(audio.pcm);
|
|
354
|
+
const ringOut = !windowed && liveEngine?.ringOut?.(track) === true;
|
|
355
|
+
const pcm = audio.pcm.subarray(0, frames * RENDER_CHANNELS);
|
|
356
|
+
if (ringOut) fadeCutTail(pcm, frames, this.sampleRate);
|
|
236
357
|
const rendered: LiveNotePcm = {
|
|
237
|
-
pcm
|
|
358
|
+
pcm,
|
|
238
359
|
frames,
|
|
360
|
+
...(ringOut ? { ringOut: true as const } : {}),
|
|
239
361
|
...(liveEngine
|
|
240
362
|
? {
|
|
241
363
|
releaseSeconds: Math.min(
|
|
@@ -245,8 +367,11 @@ export class LiveSynth {
|
|
|
245
367
|
}
|
|
246
368
|
: {}),
|
|
247
369
|
...(windowed ? { partial: true } : {}),
|
|
370
|
+
...(windowed && organTick !== undefined ? { clock: organTick } : {}),
|
|
248
371
|
};
|
|
249
372
|
if (liveFitPending()) return { ...rendered, fitting: true };
|
|
373
|
+
// Organ keys render at their song tick: caching them would only evict.
|
|
374
|
+
if (organTick !== undefined) return rendered;
|
|
250
375
|
this.cache.set(windowed ? `${key}#window` : key, rendered);
|
|
251
376
|
this.trim();
|
|
252
377
|
return rendered;
|
|
@@ -261,22 +386,70 @@ export class LiveSynth {
|
|
|
261
386
|
}
|
|
262
387
|
}
|
|
263
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Pays a voice's one-off costs before a key can sound (formant.md 4.5): the
|
|
391
|
+
* sing engine builds its LF glottal tables (about 50 ms) on first use and
|
|
392
|
+
* its inner loops need a pass to compile, so the first sing key of a session
|
|
393
|
+
* would miss the 15 ms first-window budget by 4x. Every other voice has the
|
|
394
|
+
* same shape at a smaller scale: a cold first press of a grand, a bowed
|
|
395
|
+
* cello, a granular swarm or a track with effects costs 2-4x a warm one
|
|
396
|
+
* (docs/perf.md, `live.*`). Play mode calls this when it opens on a track,
|
|
397
|
+
* so the first key costs what later keys do. The warm render goes
|
|
398
|
+
* through a throwaway synth, so no cache entry or state is left behind.
|
|
399
|
+
*/
|
|
400
|
+
export function warmLive(
|
|
401
|
+
score: TrackScore,
|
|
402
|
+
trackId: string,
|
|
403
|
+
sampleRate: number,
|
|
404
|
+
): void {
|
|
405
|
+
const track = score.tracks.find((candidate) => candidate.id === trackId);
|
|
406
|
+
if (!track) return;
|
|
407
|
+
if (singTrack(track)) warmGlottal();
|
|
408
|
+
const synth = new LiveSynth(sampleRate);
|
|
409
|
+
for (const pitch of [48, 60])
|
|
410
|
+
synth.render({ score, trackId, pitch, velocity: 0.8, seconds: 0.3 });
|
|
411
|
+
}
|
|
412
|
+
|
|
264
413
|
/**
|
|
265
414
|
* The track as it sounds live: audible regardless of mute and solo, and
|
|
266
415
|
* without track `time`, so a key sounds once, as played, not re-placed.
|
|
267
416
|
*/
|
|
268
417
|
function liveTrack(track: Track): Track {
|
|
269
|
-
// A held key is the performance: the recorded
|
|
418
|
+
// A held key is the performance: the recorded pedals and the render-time
|
|
270
419
|
// humanize stay out of it. The velocity curve stays, so a key sounds like
|
|
271
420
|
// the note it records as.
|
|
272
421
|
const {
|
|
273
422
|
solo: _solo,
|
|
274
423
|
time: _time,
|
|
275
424
|
pedal: _pedal,
|
|
425
|
+
softPedal: _softPedal,
|
|
426
|
+
sostenuto: _sostenuto,
|
|
276
427
|
humanize: _humanize,
|
|
277
428
|
...rest
|
|
278
429
|
} = track;
|
|
279
|
-
|
|
430
|
+
// A bowed section plays two players live (f061-bowed render budget).
|
|
431
|
+
return liveStringTrack({ ...rest, muted: false });
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Whether the track is a bowed string: a held bow costs about 5 ms per
|
|
436
|
+
* second, so it windows like a rig (the bow loop is causal too).
|
|
437
|
+
*/
|
|
438
|
+
function bowedTrack(track: Track): boolean {
|
|
439
|
+
return (
|
|
440
|
+
track.instrument === "string" &&
|
|
441
|
+
track.string !== undefined &&
|
|
442
|
+
isBowed(resolveString(track.string))
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Whether the track plays through the 0.7 formant fx. It is not causal (one
|
|
448
|
+
* STFT frame of lookahead), but the window renders a 1 s source, well past
|
|
449
|
+
* the 0.75 s window plus one frame, so the window is an exact prefix.
|
|
450
|
+
*/
|
|
451
|
+
function hasFormant(track: Track): boolean {
|
|
452
|
+
return Boolean(track.fx?.formant);
|
|
280
453
|
}
|
|
281
454
|
|
|
282
455
|
/** Whether the track plays through a guitar rig stage. */
|
|
@@ -299,6 +472,53 @@ function samplerDigest(track: Track, bank: SampleBank | undefined): string[] {
|
|
|
299
472
|
}
|
|
300
473
|
|
|
301
474
|
/** Frames up to the last non-silent sample, so mixing skips the dead tail. */
|
|
475
|
+
/**
|
|
476
|
+
* A ring-out note cut by the render length still sounds at its last frame:
|
|
477
|
+
* fade its final `RING_OUT_FADE_SECONDS` linearly to zero so the cap never
|
|
478
|
+
* clicks (each step stays under -60 dB of full scale). A tail that already
|
|
479
|
+
* decayed to silence is left alone.
|
|
480
|
+
*/
|
|
481
|
+
export function fadeCutTail(
|
|
482
|
+
pcm: Int16Array,
|
|
483
|
+
frames: number,
|
|
484
|
+
sampleRate: number,
|
|
485
|
+
): void {
|
|
486
|
+
if (frames === 0) return;
|
|
487
|
+
const last = (frames - 1) * RENDER_CHANNELS;
|
|
488
|
+
// Below -60 dBFS the cut is inaudible.
|
|
489
|
+
if (Math.abs(pcm[last]!) < 33 && Math.abs(pcm[last + 1]!) < 33) return;
|
|
490
|
+
const fade = Math.min(frames, Math.round(RING_OUT_FADE_SECONDS * sampleRate));
|
|
491
|
+
for (let i = 0; i < fade; i += 1) {
|
|
492
|
+
const frame = frames - fade + i;
|
|
493
|
+
const gain = (fade - 1 - i) / fade;
|
|
494
|
+
for (let c = 0; c < RENDER_CHANNELS; c += 1) {
|
|
495
|
+
const at = frame * RENDER_CHANNELS + c;
|
|
496
|
+
pcm[at] = Math.round(pcm[at]! * gain);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** A short live render ends at 12 x the engine's release past the note. */
|
|
502
|
+
const LIVE_TAIL_RELEASES = 12;
|
|
503
|
+
/** Audio within this much of a short render's end means it was cut. */
|
|
504
|
+
const LIVE_TAIL_GUARD_SECONDS = 0.05;
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Whether a live note may render a buffer sized by its engine's release
|
|
508
|
+
* rather than the whole tail window: only a 0.6 engine's dry track (echoes
|
|
509
|
+
* and reverb tails are not bounded by the key's release).
|
|
510
|
+
*/
|
|
511
|
+
function liveFastTail(track: Track, engine: boolean): boolean {
|
|
512
|
+
return (
|
|
513
|
+
engine &&
|
|
514
|
+
track.delay === undefined &&
|
|
515
|
+
track.reverb === undefined &&
|
|
516
|
+
(track.fx === undefined || Object.keys(track.fx).length === 0) &&
|
|
517
|
+
(track.fxAutomation === undefined ||
|
|
518
|
+
Object.keys(track.fxAutomation).length === 0)
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
302
522
|
function audibleFrames(pcm: Int16Array): number {
|
|
303
523
|
let last = pcm.length - 1;
|
|
304
524
|
while (last >= 0 && pcm[last] === 0) last -= 1;
|
|
@@ -344,6 +564,7 @@ export class LiveFullRenderer {
|
|
|
344
564
|
velocity: request.velocity,
|
|
345
565
|
seconds: request.seconds,
|
|
346
566
|
...(request.tick === undefined ? {} : { tick: request.tick }),
|
|
567
|
+
...(request.clock === undefined ? {} : { clock: request.clock }),
|
|
347
568
|
...(request.samples ? { samples: request.samples } : {}),
|
|
348
569
|
};
|
|
349
570
|
try {
|