@hraness/dawg 0.5.0 → 0.6.1
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 +63 -0
- package/DAWG.md +592 -49
- package/README.md +4 -4
- package/core/chords.ts +492 -4
- package/core/diff.ts +9 -0
- package/core/expression.ts +143 -6
- package/core/fx.ts +689 -2
- package/core/granular.ts +619 -0
- package/core/instruments.ts +442 -0
- package/core/keys.ts +955 -0
- package/core/resonators.ts +862 -0
- package/core/score.ts +529 -8
- package/core/sdk/eval-child.ts +2 -0
- package/core/sdk/print.ts +247 -16
- package/core/sdk/sync-instruments.ts +58 -0
- package/core/sdk/v1.ts +2417 -40
- package/core/sections.ts +44 -10
- package/core/strings.ts +1080 -0
- package/core/winds.ts +652 -0
- package/guides/automation.md +1 -0
- package/guides/chords.md +3 -1
- package/guides/effects.md +5 -2
- package/guides/media.md +1 -1
- package/guides/performance.md +3 -0
- package/guides/resample.md +26 -0
- package/guides/sounds.md +7 -2
- package/guides/tempo.md +1 -0
- package/package.json +1 -1
- package/src/agent/agent.ts +13 -0
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/granular-tools.ts +138 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +42 -2
- package/src/agent/preview-tool.ts +7 -1
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/tools.ts +726 -17
- package/src/agent/xcb-agent.ts +13 -0
- package/src/audio/arrange.ts +42 -3
- package/src/audio/dsp/bank.ts +233 -0
- package/src/audio/dsp/envelope.ts +161 -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/shift.ts +256 -0
- package/src/audio/dsp/stft.ts +73 -0
- package/src/audio/dsp/window.ts +77 -0
- package/src/audio/effects/chain.ts +23 -3
- package/src/audio/effects/common.ts +14 -0
- package/src/audio/effects/convolution.ts +106 -1
- package/src/audio/effects/gaze.ts +354 -0
- 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/engine.ts +56 -6
- package/src/audio/fit.ts +447 -0
- package/src/audio/granular.ts +995 -0
- package/src/audio/instrument-check.ts +137 -0
- package/src/audio/instruments.ts +140 -0
- package/src/audio/keys/dsp.ts +323 -0
- package/src/audio/keys/electric.ts +420 -0
- package/src/audio/keys/engine.ts +485 -0
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +476 -0
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/live-worker.ts +56 -0
- package/src/audio/live.ts +361 -15
- package/src/audio/loudness.ts +65 -20
- package/src/audio/preview.ts +17 -1
- package/src/audio/resample.ts +285 -0
- package/src/audio/resonators.ts +295 -0
- package/src/audio/sampler.ts +328 -35
- package/src/audio/samples.ts +56 -8
- package/src/audio/strings/body.ts +263 -0
- package/src/audio/strings/bow.ts +656 -0
- package/src/audio/strings/engine.ts +615 -0
- package/src/audio/strings/loop.ts +119 -0
- package/src/audio/strings/measure.test-helpers.ts +200 -0
- package/src/audio/strings/pluck.ts +354 -0
- package/src/audio/warp.ts +61 -0
- package/src/audio/wav.ts +191 -19
- package/src/audio/winds/engine.ts +302 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +80 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/voice.ts +479 -0
- package/src/commands/expression.ts +111 -23
- package/src/commands/fit.ts +135 -0
- package/src/commands/fx.ts +35 -1
- package/src/commands/granular.ts +432 -0
- package/src/commands/help.ts +159 -5
- package/src/commands/keys.ts +629 -0
- package/src/commands/modal.ts +310 -0
- package/src/commands/resample.ts +281 -0
- package/src/commands/rig.ts +264 -0
- package/src/commands/sample.ts +52 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/string.ts +201 -0
- package/src/commands/strum.ts +473 -0
- package/src/commands/time.ts +4 -1
- package/src/commands/wind.ts +244 -0
- package/src/main.ts +339 -15
- package/src/project/check.ts +17 -0
- package/src/render.ts +49 -4
- package/src/session/presence.ts +16 -3
- package/src/tui/audition.ts +2 -2
- package/src/tui/granular-menu.ts +278 -0
- package/src/tui/menu.ts +934 -9
- package/src/tui/modal-menu.ts +145 -0
- package/src/tui/performance-menu.ts +42 -0
- package/src/tui/play-chords.ts +104 -3
- package/src/tui/play-mode.ts +1 -0
- package/src/tui/play-session.ts +70 -2
- package/src/tui/wind-menu.ts +144 -0
package/src/audio/warp.ts
CHANGED
|
@@ -84,3 +84,64 @@ export function warpedSpan(
|
|
|
84
84
|
length: Math.max(1, Math.floor(to - from)),
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A fitted sample window's position map (0.6 `bpm`/`len`/`fit`): output
|
|
90
|
+
* frames after the note start to window frames and back, both at the
|
|
91
|
+
* render rate. Through a tempo map the window follows ramps and steps;
|
|
92
|
+
* at constant tempo both directions are linear.
|
|
93
|
+
*/
|
|
94
|
+
export type FitMap = Readonly<{
|
|
95
|
+
/** Output frames the whole window takes. */
|
|
96
|
+
length: number;
|
|
97
|
+
/** Window frame sounding `e` output frames after the start. */
|
|
98
|
+
at(e: number): number;
|
|
99
|
+
/** Output frame (after the start) where window frame `p` sounds. */
|
|
100
|
+
outAt(p: number): number;
|
|
101
|
+
}>;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The map for a window of `frames` render-rate frames that advances
|
|
105
|
+
* `perBeat` window frames per song beat from `startTick`. Without a warp the
|
|
106
|
+
* song runs at `tempoBpm` throughout.
|
|
107
|
+
*/
|
|
108
|
+
export function beatFitMap(
|
|
109
|
+
warp: SampleWarp | undefined,
|
|
110
|
+
timing: Readonly<{
|
|
111
|
+
tempoBpm: number;
|
|
112
|
+
ticksPerBeat: number;
|
|
113
|
+
sampleRate: number;
|
|
114
|
+
}>,
|
|
115
|
+
startTick: number,
|
|
116
|
+
frames: number,
|
|
117
|
+
perBeat: number,
|
|
118
|
+
): FitMap {
|
|
119
|
+
const { tempoBpm, ticksPerBeat, sampleRate } = timing;
|
|
120
|
+
if (!warp) {
|
|
121
|
+
const ratio = perBeat / ((sampleRate * 60) / tempoBpm);
|
|
122
|
+
return Object.freeze({
|
|
123
|
+
length: frames / ratio,
|
|
124
|
+
at: (e: number) => e * ratio,
|
|
125
|
+
outAt: (p: number) => p / ratio,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const origin = warp.sample(startTick);
|
|
129
|
+
const outAt = (p: number) =>
|
|
130
|
+
warp.sample(startTick + (p / perBeat) * ticksPerBeat) - origin;
|
|
131
|
+
return Object.freeze({
|
|
132
|
+
length: outAt(frames),
|
|
133
|
+
at: (e: number) =>
|
|
134
|
+
((warp.tick(origin + e) - startTick) / ticksPerBeat) * perBeat,
|
|
135
|
+
outAt,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** `fit`: the window fills `held` output frames, linearly. */
|
|
140
|
+
export function spanFitMap(frames: number, held: number): FitMap {
|
|
141
|
+
const ratio = frames / Math.max(1, held);
|
|
142
|
+
return Object.freeze({
|
|
143
|
+
length: held,
|
|
144
|
+
at: (e: number) => e * ratio,
|
|
145
|
+
outAt: (p: number) => p / ratio,
|
|
146
|
+
});
|
|
147
|
+
}
|
package/src/audio/wav.ts
CHANGED
|
@@ -23,6 +23,11 @@ import {
|
|
|
23
23
|
} from "./sampler.ts";
|
|
24
24
|
import { EFFECT_NAMES, FX_PRESETS, effectSpec } from "../../core/fx.ts";
|
|
25
25
|
import { seededRandom } from "./random.ts";
|
|
26
|
+
import {
|
|
27
|
+
engineFor,
|
|
28
|
+
engineTailSeconds,
|
|
29
|
+
RING_OUT_FADE_SECONDS,
|
|
30
|
+
} from "./instruments.ts";
|
|
26
31
|
import {
|
|
27
32
|
isStereoVoice,
|
|
28
33
|
renderSynthNote,
|
|
@@ -45,6 +50,8 @@ import {
|
|
|
45
50
|
applyStereoChain,
|
|
46
51
|
delayTailFor,
|
|
47
52
|
interpolateAutomation,
|
|
53
|
+
needsEffectNotes,
|
|
54
|
+
type EffectNote,
|
|
48
55
|
reverbActive,
|
|
49
56
|
reverbImpulse,
|
|
50
57
|
reverbTailFor,
|
|
@@ -96,6 +103,19 @@ export type RenderOptions = WavOptions &
|
|
|
96
103
|
* a single pass of the whole song. Zero (the default) changes nothing.
|
|
97
104
|
*/
|
|
98
105
|
seedTick?: number;
|
|
106
|
+
/**
|
|
107
|
+
* Song seconds at an arranged window's first frame (with a tempo map,
|
|
108
|
+
* more than `seedTick` at the base tempo); engines whose state runs on
|
|
109
|
+
* the song clock (organ wheels and rotors) start there.
|
|
110
|
+
*/
|
|
111
|
+
seedSeconds?: number;
|
|
112
|
+
/** Per-track engine state at the window origin (`windowSeed`). */
|
|
113
|
+
seedState?: Readonly<Record<string, string>>;
|
|
114
|
+
/**
|
|
115
|
+
* The song whose chords a granular `quant chord` reads, at `tick` for
|
|
116
|
+
* this render's tick 0 (a live note renders alone). Absent: the score.
|
|
117
|
+
*/
|
|
118
|
+
quantChord?: Readonly<{ score: TrackScore; tick: number }>;
|
|
99
119
|
}>;
|
|
100
120
|
|
|
101
121
|
/** Interleaved stereo 16-bit PCM plus its frame count. */
|
|
@@ -139,6 +159,9 @@ export const AVAILABLE_INSTRUMENTS = Object.freeze([
|
|
|
139
159
|
"z_square",
|
|
140
160
|
"z_tan",
|
|
141
161
|
"z_noise",
|
|
162
|
+
// 0.6 instruments (src/audio/instruments.ts), one line per lane.
|
|
163
|
+
"granular",
|
|
164
|
+
"modal",
|
|
142
165
|
] as const);
|
|
143
166
|
|
|
144
167
|
/** Per-track effects understood by the renderer and the agent, in chain
|
|
@@ -180,8 +203,33 @@ function oneShotSeconds(
|
|
|
180
203
|
}
|
|
181
204
|
/** Loop renders fold at most this much tail back onto the loop start. */
|
|
182
205
|
const MAX_LOOP_TAIL_SECONDS = 8;
|
|
206
|
+
/** A 0.6 engine's one-shot ring-out is capped here, not at the loop fold. */
|
|
207
|
+
const MAX_ENGINE_TAIL_SECONDS = 30;
|
|
183
208
|
|
|
184
|
-
|
|
209
|
+
/**
|
|
210
|
+
* The longest 0.6 engine ring-out in `score`, capped at 30 s. One-shot
|
|
211
|
+
* renders (exports, previews) let engine tails reach their own cap; the
|
|
212
|
+
* 8 s cap applies only to loop folding. Zero without an engine, so legacy
|
|
213
|
+
* renders are unchanged. `ringing` counts only ring-out tracks (0.6.1).
|
|
214
|
+
*/
|
|
215
|
+
function oneShotEngineTail(score: TrackScore, ringing = false): number {
|
|
216
|
+
let tail = 0;
|
|
217
|
+
for (const track of score.tracks) {
|
|
218
|
+
const engine = engineFor(track);
|
|
219
|
+
if (!engine || (ringing && engine.ringOut?.(track) !== true)) continue;
|
|
220
|
+
let lowest: number | undefined;
|
|
221
|
+
for (const note of score.notes)
|
|
222
|
+
if (
|
|
223
|
+
note.trackId === track.id &&
|
|
224
|
+
(lowest === undefined || note.pitch < lowest)
|
|
225
|
+
)
|
|
226
|
+
lowest = note.pitch;
|
|
227
|
+
tail = Math.max(tail, engineTailSeconds(track, lowest));
|
|
228
|
+
}
|
|
229
|
+
return Math.min(MAX_ENGINE_TAIL_SECONDS, tail);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export type RenderContext = Readonly<{
|
|
185
233
|
score: TrackScore;
|
|
186
234
|
sampleRate: number;
|
|
187
235
|
samples: number;
|
|
@@ -191,6 +239,9 @@ type RenderContext = Readonly<{
|
|
|
191
239
|
/** Tempo map in samples (core/tempo.ts); absent: constant tempo. */
|
|
192
240
|
warp?: SampleWarp;
|
|
193
241
|
seedTick?: number;
|
|
242
|
+
seedSeconds?: number;
|
|
243
|
+
seedState?: Readonly<Record<string, string>>;
|
|
244
|
+
quantChord?: Readonly<{ score: TrackScore; tick: number }>;
|
|
194
245
|
}>;
|
|
195
246
|
|
|
196
247
|
/** Exact (fractional) loop length in frames at a sample rate. */
|
|
@@ -314,9 +365,15 @@ export class StemRenderer {
|
|
|
314
365
|
Math.max(
|
|
315
366
|
samplerTailSeconds(score, bank, sampleRate),
|
|
316
367
|
...score.tracks.map(synthTailSeconds),
|
|
368
|
+
// Zero without a registered 0.6 instrument engine.
|
|
369
|
+
...score.tracks.map((track) => engineTailSeconds(track)),
|
|
317
370
|
),
|
|
318
371
|
);
|
|
319
|
-
return oneShotSeconds(
|
|
372
|
+
return oneShotSeconds(
|
|
373
|
+
loopSecondsOf(score),
|
|
374
|
+
Math.max(samplerTail, oneShotEngineTail(score)),
|
|
375
|
+
reverbTail,
|
|
376
|
+
);
|
|
320
377
|
}
|
|
321
378
|
|
|
322
379
|
public render(score: TrackScore, options: RenderOptions = {}): RenderedAudio {
|
|
@@ -338,6 +395,8 @@ export class StemRenderer {
|
|
|
338
395
|
Math.max(
|
|
339
396
|
samplerTailSeconds(score, bank, sampleRate),
|
|
340
397
|
...score.tracks.map(synthTailSeconds),
|
|
398
|
+
// Zero without a registered 0.6 instrument engine.
|
|
399
|
+
...score.tracks.map((track) => engineTailSeconds(track)),
|
|
341
400
|
),
|
|
342
401
|
);
|
|
343
402
|
let frames: number;
|
|
@@ -367,7 +426,11 @@ export class StemRenderer {
|
|
|
367
426
|
} else {
|
|
368
427
|
const seconds = Math.min(
|
|
369
428
|
maxSeconds,
|
|
370
|
-
oneShotSeconds(
|
|
429
|
+
oneShotSeconds(
|
|
430
|
+
loopSeconds,
|
|
431
|
+
Math.max(samplerTail, oneShotEngineTail(score)),
|
|
432
|
+
reverbTail,
|
|
433
|
+
),
|
|
371
434
|
);
|
|
372
435
|
frames = Math.max(1, Math.ceil(seconds * sampleRate));
|
|
373
436
|
samples = frames;
|
|
@@ -381,6 +444,9 @@ export class StemRenderer {
|
|
|
381
444
|
...(bank.irs ? { irs: bank.irs } : {}),
|
|
382
445
|
...(warp ? { warp } : {}),
|
|
383
446
|
...(options.seedTick ? { seedTick: options.seedTick } : {}),
|
|
447
|
+
...(options.seedSeconds ? { seedSeconds: options.seedSeconds } : {}),
|
|
448
|
+
...(options.seedState ? { seedState: options.seedState } : {}),
|
|
449
|
+
...(options.quantChord ? { quantChord: options.quantChord } : {}),
|
|
384
450
|
};
|
|
385
451
|
this.renders += 1;
|
|
386
452
|
const { dry, dryR, left, right, mixL, mixR } = this.scratchFor(samples);
|
|
@@ -437,6 +503,18 @@ export class StemRenderer {
|
|
|
437
503
|
const played = performed.get(trackId)!;
|
|
438
504
|
const busOrbit = sharedOrbitOf(track);
|
|
439
505
|
const sampler = isSamplerInstrument(track?.instrument);
|
|
506
|
+
// A 0.6 instrument engine (src/audio/instruments.ts), if registered
|
|
507
|
+
// and the track carries its field; undefined keeps today's voices.
|
|
508
|
+
const engine = engineFor(track);
|
|
509
|
+
// The song key always joins an engine's stem key: engines may tune to
|
|
510
|
+
// it (sympathetic strings) without a song tuning.
|
|
511
|
+
const engineDigests =
|
|
512
|
+
engine && track
|
|
513
|
+
? [
|
|
514
|
+
...(engine.assetDigests?.(track, bank, score) ?? []),
|
|
515
|
+
`key:${score.key ?? ""}`,
|
|
516
|
+
]
|
|
517
|
+
: undefined;
|
|
440
518
|
// Wavetable hook: the oscillator factory for a wavetable track (its
|
|
441
519
|
// table id joins the stem key), undefined for every other instrument.
|
|
442
520
|
const wavetable = track ? wavetableHook(track, bank, context) : undefined;
|
|
@@ -449,6 +527,7 @@ export class StemRenderer {
|
|
|
449
527
|
sampler ? bank : undefined,
|
|
450
528
|
wavetable?.id,
|
|
451
529
|
track ? reverbImpulse(track, sampleRate, bank.irs)?.id : undefined,
|
|
530
|
+
engineDigests,
|
|
452
531
|
);
|
|
453
532
|
let stem = this.stems.get(trackId);
|
|
454
533
|
const caching = this.maxCacheBytes > 0;
|
|
@@ -464,6 +543,7 @@ export class StemRenderer {
|
|
|
464
543
|
sampler ? bank : undefined,
|
|
465
544
|
wavetable?.id,
|
|
466
545
|
reverbImpulse(track, sampleRate, bank.irs)?.id,
|
|
546
|
+
engineDigests,
|
|
467
547
|
)
|
|
468
548
|
: undefined;
|
|
469
549
|
if (stem?.key === key) stem.used = this.renders;
|
|
@@ -490,11 +570,35 @@ export class StemRenderer {
|
|
|
490
570
|
}
|
|
491
571
|
: { left, right };
|
|
492
572
|
dry.fill(0);
|
|
493
|
-
const synthVoice = !sampler && usesSynthVoice(track);
|
|
494
|
-
|
|
573
|
+
const synthVoice = !engine && !sampler && usesSynthVoice(track);
|
|
574
|
+
let stereo = engine
|
|
575
|
+
? engine.stereo(track!)
|
|
576
|
+
: synthVoice && isStereoVoice(track);
|
|
495
577
|
if (stereo) dryR.fill(0);
|
|
496
|
-
if (
|
|
497
|
-
|
|
578
|
+
if (engine && track) {
|
|
579
|
+
engine.render(
|
|
580
|
+
dry,
|
|
581
|
+
stereo ? dryR : undefined,
|
|
582
|
+
played,
|
|
583
|
+
track,
|
|
584
|
+
{
|
|
585
|
+
...context,
|
|
586
|
+
ticksPerBeat: score.ticksPerBeat,
|
|
587
|
+
...(tuning ? { tuning } : {}),
|
|
588
|
+
},
|
|
589
|
+
bank,
|
|
590
|
+
);
|
|
591
|
+
} else if (sampler) {
|
|
592
|
+
// A resampled stereo file plays as stereo (0.6.1).
|
|
593
|
+
if (track)
|
|
594
|
+
stereo = renderSamplerNotes(
|
|
595
|
+
dry,
|
|
596
|
+
played,
|
|
597
|
+
track,
|
|
598
|
+
context,
|
|
599
|
+
bank,
|
|
600
|
+
dryR,
|
|
601
|
+
);
|
|
498
602
|
} else if (synthVoice && track) {
|
|
499
603
|
const gainAt = (tick: number) => trackGainAt(track, tick);
|
|
500
604
|
const voice = {
|
|
@@ -534,8 +638,13 @@ export class StemRenderer {
|
|
|
534
638
|
else renderToneNote(dry, note, track, context, tuning);
|
|
535
639
|
}
|
|
536
640
|
}
|
|
537
|
-
|
|
538
|
-
|
|
641
|
+
// Note-aware stages (bloom, swell) see the notes; others never do.
|
|
642
|
+
const chain =
|
|
643
|
+
track && needsEffectNotes(track)
|
|
644
|
+
? { ...context, notes: effectNotes(played, context, tuning) }
|
|
645
|
+
: context;
|
|
646
|
+
if (track) applyMonoChain(dry, track, chain);
|
|
647
|
+
if (stereo && track) applyMonoChain(dryR, track, chain);
|
|
539
648
|
applyPan(
|
|
540
649
|
dry,
|
|
541
650
|
target.left,
|
|
@@ -551,7 +660,7 @@ export class StemRenderer {
|
|
|
551
660
|
target.left,
|
|
552
661
|
target.right,
|
|
553
662
|
track,
|
|
554
|
-
|
|
663
|
+
chain,
|
|
555
664
|
true,
|
|
556
665
|
false,
|
|
557
666
|
);
|
|
@@ -569,7 +678,7 @@ export class StemRenderer {
|
|
|
569
678
|
target.left,
|
|
570
679
|
target.right,
|
|
571
680
|
track,
|
|
572
|
-
|
|
681
|
+
chain,
|
|
573
682
|
busOrbit === undefined,
|
|
574
683
|
);
|
|
575
684
|
stem = {
|
|
@@ -625,6 +734,20 @@ export class StemRenderer {
|
|
|
625
734
|
if (!groups.has(trackId)) this.evict(trackId);
|
|
626
735
|
// Linear effects superpose, so folding the tail onto the start yields
|
|
627
736
|
// the steady state of the loop playing forever.
|
|
737
|
+
// A ring-out longer than the fold (a gong past 8 s) would be cut there:
|
|
738
|
+
// fade its last RING_OUT_FADE_SECONDS so the fold never clicks. Only
|
|
739
|
+
// ringing 0.6 engine tracks qualify, so legacy loops are untouched.
|
|
740
|
+
if (
|
|
741
|
+
options.loop &&
|
|
742
|
+
samples > frames &&
|
|
743
|
+
oneShotEngineTail(score, true) > (samples - frames) / sampleRate
|
|
744
|
+
)
|
|
745
|
+
fadeEnd(
|
|
746
|
+
mixL,
|
|
747
|
+
mixR,
|
|
748
|
+
samples,
|
|
749
|
+
Math.round(RING_OUT_FADE_SECONDS * sampleRate),
|
|
750
|
+
);
|
|
628
751
|
if (samples > frames) foldTail(mixL, mixR, frames, samples);
|
|
629
752
|
// The song master (src/audio/master.ts); none leaves the mix untouched.
|
|
630
753
|
const mastered = applyMaster(
|
|
@@ -731,6 +854,21 @@ function addStem(
|
|
|
731
854
|
}
|
|
732
855
|
|
|
733
856
|
/** Fold everything past `frames` back onto the loop start. */
|
|
857
|
+
/** Fade the last `fade` of `samples` mix frames linearly to zero. */
|
|
858
|
+
function fadeEnd(
|
|
859
|
+
mixL: Float64Array,
|
|
860
|
+
mixR: Float64Array,
|
|
861
|
+
samples: number,
|
|
862
|
+
fade: number,
|
|
863
|
+
): void {
|
|
864
|
+
const n = Math.min(fade, samples);
|
|
865
|
+
for (let i = 0; i < n; i += 1) {
|
|
866
|
+
const gain = (n - 1 - i) / n;
|
|
867
|
+
mixL[samples - n + i]! *= gain;
|
|
868
|
+
mixR[samples - n + i]! *= gain;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
734
872
|
function foldTail(
|
|
735
873
|
mixL: Float64Array,
|
|
736
874
|
mixR: Float64Array,
|
|
@@ -826,6 +964,7 @@ function stemKey(
|
|
|
826
964
|
bank?: SampleBank,
|
|
827
965
|
wavetableId?: string,
|
|
828
966
|
impulseId?: string,
|
|
967
|
+
engineDigests?: readonly string[],
|
|
829
968
|
): string {
|
|
830
969
|
let settings: Record<string, unknown> | null = null;
|
|
831
970
|
if (track) {
|
|
@@ -849,12 +988,18 @@ function stemKey(
|
|
|
849
988
|
...(wavetableId ? [wavetableId] : []),
|
|
850
989
|
// A convolution reverb's impulse (its sha256 for a sample).
|
|
851
990
|
...(impulseId ? [impulseId] : []),
|
|
991
|
+
// Assets a 0.6 instrument engine reads (absent without an engine).
|
|
992
|
+
...(engineDigests ? [engineDigests] : []),
|
|
852
993
|
// The song tuning, and the key whose tonic is the default root.
|
|
853
994
|
...(context.score.tuning || track?.tuning
|
|
854
995
|
? [context.score.tuning ?? null, context.score.key]
|
|
855
996
|
: []),
|
|
856
997
|
// Seeded noise follows the window origin of an arranged render.
|
|
857
998
|
...(context.seedTick ? [`seed:${context.seedTick}`] : []),
|
|
999
|
+
...(context.seedSeconds ? [`at:${context.seedSeconds}`] : []),
|
|
1000
|
+
...(track && context.seedState?.[track.id]
|
|
1001
|
+
? [`state:${context.seedState[track.id]}`]
|
|
1002
|
+
: []),
|
|
858
1003
|
]);
|
|
859
1004
|
}
|
|
860
1005
|
|
|
@@ -869,25 +1014,34 @@ function samplerVoiceDigest(track: Track, bank: SampleBank): string[] {
|
|
|
869
1014
|
});
|
|
870
1015
|
}
|
|
871
1016
|
|
|
872
|
-
/**
|
|
1017
|
+
/**
|
|
1018
|
+
* Sample voices of a sampler track into its mono dry buffer. When a voice
|
|
1019
|
+
* plays a resampled stereo file (0.6.1), `target` takes the left channel,
|
|
1020
|
+
* `targetRight` the right, and the result is true (the track is stereo).
|
|
1021
|
+
*/
|
|
873
1022
|
function renderSamplerNotes(
|
|
874
1023
|
target: Float64Array,
|
|
875
1024
|
notes: readonly Note[],
|
|
876
1025
|
track: Track,
|
|
877
1026
|
context: RenderContext,
|
|
878
1027
|
bank: SampleBank,
|
|
879
|
-
|
|
1028
|
+
targetRight: Float64Array,
|
|
1029
|
+
): boolean {
|
|
880
1030
|
const timing = {
|
|
881
1031
|
score: context.score,
|
|
882
1032
|
sampleRate: context.sampleRate,
|
|
883
1033
|
...(context.warp ? { warp: context.warp } : {}),
|
|
884
1034
|
};
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
timing,
|
|
889
|
-
|
|
890
|
-
|
|
1035
|
+
const voices = planSamplerVoices(track, notes, bank, timing);
|
|
1036
|
+
const gainAt = (tick: number) => trackGainAt(track, tick);
|
|
1037
|
+
if (!voices.some((voice) => voice.stereo)) {
|
|
1038
|
+
renderSamplerVoices(target, voices, timing, gainAt);
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
targetRight.fill(0);
|
|
1042
|
+
renderSamplerVoices(target, voices, timing, gainAt, "left");
|
|
1043
|
+
renderSamplerVoices(targetRight, voices, timing, gainAt, "right");
|
|
1044
|
+
return true;
|
|
891
1045
|
}
|
|
892
1046
|
|
|
893
1047
|
/** Track volume and volume automation at a score tick (pan is applied in stereo). */
|
|
@@ -901,6 +1055,24 @@ function trackGainAt(track: Track | undefined, tick: number): number {
|
|
|
901
1055
|
return trackGain * automatedGain;
|
|
902
1056
|
}
|
|
903
1057
|
|
|
1058
|
+
/** The notes as note-aware effects see them (EffectContext.notes). */
|
|
1059
|
+
function effectNotes(
|
|
1060
|
+
notes: readonly Note[],
|
|
1061
|
+
context: RenderContext,
|
|
1062
|
+
tuning: TuningTable | undefined,
|
|
1063
|
+
): EffectNote[] {
|
|
1064
|
+
return notes.map((note) => {
|
|
1065
|
+
const { start, length } = noteSpan(note, context);
|
|
1066
|
+
return {
|
|
1067
|
+
id: note.id,
|
|
1068
|
+
start,
|
|
1069
|
+
length,
|
|
1070
|
+
hz: noteHz(note.pitch, note.cents, tuning),
|
|
1071
|
+
velocity: note.velocity,
|
|
1072
|
+
};
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
|
|
904
1076
|
function noteSpan(
|
|
905
1077
|
note: Note,
|
|
906
1078
|
context: RenderContext,
|