@hraness/dawg 0.6.1 → 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 +92 -0
- package/DAWG.md +343 -116
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +271 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -104
- package/core/expression.ts +15 -0
- package/core/fx.ts +99 -29
- package/core/instruments.ts +19 -0
- package/core/keys.ts +3 -3
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +16 -2
- package/core/routing.ts +165 -0
- package/core/score.ts +832 -13
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +276 -3
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +1849 -42
- package/core/sections.ts +292 -26
- package/core/sing.ts +815 -0
- 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/guides/agent.md +29 -0
- package/guides/arrange.md +30 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -7
- package/guides/chords.md +15 -15
- package/guides/effects.md +17 -16
- 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 -13
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +15 -9
- 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 -13
- 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 +126 -14
- package/src/agent/calibration-tools.ts +53 -0
- 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 +1 -1
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +12 -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 +4 -1
- package/src/agent/provider.ts +22 -8
- 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 +108 -23
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +11 -7
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +37 -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 +10 -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/effects/chain.ts +3 -1
- package/src/audio/effects/common.ts +43 -0
- package/src/audio/effects/convolution.ts +7 -4
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/engine.ts +160 -28
- package/src/audio/fit.ts +35 -3
- package/src/audio/instrument-check.ts +59 -43
- package/src/audio/instruments.ts +4 -0
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +8 -1
- package/src/audio/keys/engine.ts +13 -1
- package/src/audio/keys/piano.ts +22 -2
- package/src/audio/kits.ts +135 -6
- package/src/audio/live.ts +114 -20
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +30 -2
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +2 -1
- package/src/audio/sampler.ts +85 -4
- package/src/audio/samples.ts +20 -2
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/bow.ts +48 -5
- package/src/audio/strings/engine.ts +8 -2
- 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 +500 -77
- package/src/audio/winds/engine.ts +5 -1
- package/src/audio/winds/trim.ts +28 -4
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +15 -2
- 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 +1 -1
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +101 -32
- package/src/commands/grammar.ts +558 -0
- package/src/commands/help.ts +610 -380
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +8 -8
- package/src/commands/modal.ts +1 -1
- 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/rhythm.ts +3 -0
- package/src/commands/rig.ts +3 -24
- package/src/commands/sing.ts +478 -0
- package/src/commands/strum.ts +13 -1
- 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 +2 -2
- 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 +1397 -253
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +21 -2
- 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 +20 -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 +34 -4
- 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 +759 -177
- package/src/tui/modal-menu.ts +6 -6
- package/src/tui/performance-menu.ts +5 -2
- package/src/tui/play-chords.ts +4 -2
- package/src/tui/play-mode.ts +15 -1
- package/src/tui/play-session.ts +47 -7
- 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 +3 -3
- 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/wav.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type Track,
|
|
10
10
|
type TrackScore,
|
|
11
11
|
} from "../../core/score.ts";
|
|
12
|
+
import { autotuneStemDigests } from "./autotune.ts";
|
|
12
13
|
import { drumVoiceForPitch, isDrumInstrument } from "../../core/drums.ts";
|
|
13
14
|
import {
|
|
14
15
|
performanceTimingFor,
|
|
@@ -21,12 +22,18 @@ import {
|
|
|
21
22
|
renderSamplerVoices,
|
|
22
23
|
samplerTailSeconds,
|
|
23
24
|
} from "./sampler.ts";
|
|
24
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
EFFECT_NAMES,
|
|
27
|
+
FX_CHAIN,
|
|
28
|
+
FX_PRESETS,
|
|
29
|
+
effectSpec,
|
|
30
|
+
} from "../../core/fx.ts";
|
|
25
31
|
import { seededRandom } from "./random.ts";
|
|
26
32
|
import {
|
|
27
33
|
engineFor,
|
|
28
34
|
engineTailSeconds,
|
|
29
35
|
RING_OUT_FADE_SECONDS,
|
|
36
|
+
type InstrumentEngine,
|
|
30
37
|
} from "./instruments.ts";
|
|
31
38
|
import {
|
|
32
39
|
isStereoVoice,
|
|
@@ -35,7 +42,7 @@ import {
|
|
|
35
42
|
usesSynthVoice,
|
|
36
43
|
type VoiceContext,
|
|
37
44
|
} from "./synth/voice.ts";
|
|
38
|
-
import {
|
|
45
|
+
import { legacyWaveFor } from "./synth/oscillators.ts";
|
|
39
46
|
import {
|
|
40
47
|
EMPTY_SAMPLE_BANK,
|
|
41
48
|
sampleKey,
|
|
@@ -43,7 +50,15 @@ import {
|
|
|
43
50
|
type SampleBank,
|
|
44
51
|
} from "./samples.ts";
|
|
45
52
|
import { synthKit } from "../../core/kits.ts";
|
|
46
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
kitDrumSample,
|
|
55
|
+
kitTailSeconds,
|
|
56
|
+
METAL_SECONDS,
|
|
57
|
+
metalKindForPitch,
|
|
58
|
+
metalSample,
|
|
59
|
+
newKitVoiceState,
|
|
60
|
+
tomRatio,
|
|
61
|
+
} from "./kits.ts";
|
|
47
62
|
import {
|
|
48
63
|
addReverbWet,
|
|
49
64
|
applyMonoChain,
|
|
@@ -79,8 +94,13 @@ import {
|
|
|
79
94
|
slowestBpmOf,
|
|
80
95
|
} from "../../core/tempo.ts";
|
|
81
96
|
import { sampleWarpFor, warpedSpan, type SampleWarp } from "./warp.ts";
|
|
97
|
+
import { clipsDigest, hasClips, renderClips } from "./clips.ts";
|
|
98
|
+
import { isGuideInstrument } from "../../core/clips.ts";
|
|
82
99
|
import { applyMaster, type MasterReport } from "./master.ts";
|
|
83
100
|
import type { SongMaster } from "../../core/master.ts";
|
|
101
|
+
import { resolveTrackRef } from "../../core/routing.ts";
|
|
102
|
+
import { resolveVocoder, VOCODER_INSTRUMENT } from "../../core/vocoder.ts";
|
|
103
|
+
import { applyVocoder, autoGateDb } from "./vocoder/index.ts";
|
|
84
104
|
|
|
85
105
|
export type WavOptions = Readonly<{ sampleRate?: number; maxSeconds?: number }>;
|
|
86
106
|
|
|
@@ -92,6 +112,12 @@ export type RenderOptions = WavOptions &
|
|
|
92
112
|
* what the streaming engine plays; exports keep the default one-shot.
|
|
93
113
|
*/
|
|
94
114
|
loop?: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* 0.7 clips: a `vocal` track's guide notes sound as a soft sine. The
|
|
117
|
+
* live loop (audition, play mode) sets it; exports never do, so a
|
|
118
|
+
* rendered file holds only the clips.
|
|
119
|
+
*/
|
|
120
|
+
guide?: boolean;
|
|
95
121
|
/**
|
|
96
122
|
* Decoded sampler voices (`SampleLibrary.load`). Without it sampler
|
|
97
123
|
* tracks render silent; scores without samplers are unaffected.
|
|
@@ -126,8 +152,36 @@ export type RenderedAudio = Readonly<{
|
|
|
126
152
|
pcm: Int16Array;
|
|
127
153
|
/** Loudness the song master reached; absent without a master. */
|
|
128
154
|
master?: MasterReport;
|
|
155
|
+
/**
|
|
156
|
+
* Non-finite samples (NaN, Infinity) a track's source or effects made,
|
|
157
|
+
* zeroed before they could poison a feedback stage or the mix (q08);
|
|
158
|
+
* absent when every sample was finite.
|
|
159
|
+
*/
|
|
160
|
+
nonFinite?: NonFiniteReport;
|
|
129
161
|
}>;
|
|
130
162
|
|
|
163
|
+
/** Where a render zeroed non-finite samples. */
|
|
164
|
+
export type NonFiniteReport = Readonly<{
|
|
165
|
+
samples: number;
|
|
166
|
+
/** Track ids in render order, then `bus` for the bus returns. */
|
|
167
|
+
tracks: readonly string[];
|
|
168
|
+
}>;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Zeroes NaN and Infinity in `buffer` and returns how many it found. A
|
|
172
|
+
* NaN written into an Int16Array stores 0, and one fed to a filter, comb
|
|
173
|
+
* or reverb feedback silences everything after it.
|
|
174
|
+
*/
|
|
175
|
+
export function scrubNonFinite(buffer: Float64Array): number {
|
|
176
|
+
let count = 0;
|
|
177
|
+
for (let i = 0; i < buffer.length; i += 1)
|
|
178
|
+
if (!Number.isFinite(buffer[i]!)) {
|
|
179
|
+
buffer[i] = 0;
|
|
180
|
+
count += 1;
|
|
181
|
+
}
|
|
182
|
+
return count;
|
|
183
|
+
}
|
|
184
|
+
|
|
131
185
|
/** Output channel count for every render and export. */
|
|
132
186
|
export const RENDER_CHANNELS = 2 as const;
|
|
133
187
|
export const DEFAULT_SAMPLE_RATE = 22_050;
|
|
@@ -162,6 +216,8 @@ export const AVAILABLE_INSTRUMENTS = Object.freeze([
|
|
|
162
216
|
// 0.6 instruments (src/audio/instruments.ts), one line per lane.
|
|
163
217
|
"granular",
|
|
164
218
|
"modal",
|
|
219
|
+
// 0.7 clips: a track of audio clips whose notes are guides.
|
|
220
|
+
"vocal",
|
|
165
221
|
] as const);
|
|
166
222
|
|
|
167
223
|
/** Per-track effects understood by the renderer and the agent, in chain
|
|
@@ -288,6 +344,8 @@ type Stem = {
|
|
|
288
344
|
/** Stem, pre-reverb pair and tail (counted in the cache budget). */
|
|
289
345
|
bytes: number;
|
|
290
346
|
used: number;
|
|
347
|
+
/** Non-finite samples zeroed while rendering it (0 or absent: none). */
|
|
348
|
+
nonFinite?: number;
|
|
291
349
|
/** For a track with a reverb: its stem before the reverb, and the tail. */
|
|
292
350
|
room?: Readonly<{
|
|
293
351
|
/** `stemKey` of the track with its reverb mix and mix lane left out. */
|
|
@@ -306,6 +364,9 @@ type Stem = {
|
|
|
306
364
|
* byte-identical to a cold render; mute and solo only pick which stems are
|
|
307
365
|
* summed. Scratch buffers are reused across renders.
|
|
308
366
|
*/
|
|
367
|
+
/** A guide note's level against a plain sine note (about -12 dB). */
|
|
368
|
+
const GUIDE_LEVEL = 0.25;
|
|
369
|
+
|
|
309
370
|
export class StemRenderer {
|
|
310
371
|
private readonly maxCacheBytes: number;
|
|
311
372
|
private readonly stems = new Map<string, Stem>();
|
|
@@ -316,6 +377,11 @@ export class StemRenderer {
|
|
|
316
377
|
>();
|
|
317
378
|
private cacheBytes = 0;
|
|
318
379
|
private renders = 0;
|
|
380
|
+
/** Vocoder modulator taps by source key (0.7), kept one render. */
|
|
381
|
+
private readonly modSources = new Map<
|
|
382
|
+
string,
|
|
383
|
+
{ tap: Float64Array; used: number }
|
|
384
|
+
>();
|
|
319
385
|
private scratch: {
|
|
320
386
|
dry: Float64Array;
|
|
321
387
|
dryR: Float64Array;
|
|
@@ -450,6 +516,10 @@ export class StemRenderer {
|
|
|
450
516
|
};
|
|
451
517
|
this.renders += 1;
|
|
452
518
|
const { dry, dryR, left, right, mixL, mixR } = this.scratchFor(samples);
|
|
519
|
+
const nonFinite: { samples: number; tracks: string[] } = {
|
|
520
|
+
samples: 0,
|
|
521
|
+
tracks: [],
|
|
522
|
+
};
|
|
453
523
|
mixL.fill(0);
|
|
454
524
|
mixR.fill(0);
|
|
455
525
|
const tracks = new Map(score.tracks.map((track) => [track.id, track]));
|
|
@@ -460,6 +530,14 @@ export class StemRenderer {
|
|
|
460
530
|
if (group) group.push(note);
|
|
461
531
|
else groups.set(note.trackId, [note]);
|
|
462
532
|
}
|
|
533
|
+
// Tracks with audio clips (0.7) render even without notes, after the
|
|
534
|
+
// tracks with notes, so songs without clips keep their sum order.
|
|
535
|
+
for (const track of score.tracks)
|
|
536
|
+
if (hasClips(track) && !groups.has(track.id)) groups.set(track.id, []);
|
|
537
|
+
// A built-in vocoder carrier following chords or a drone needs no notes.
|
|
538
|
+
for (const track of score.tracks)
|
|
539
|
+
if (!groups.has(track.id) && vocoderPlaysWithoutNotes(track))
|
|
540
|
+
groups.set(track.id, []);
|
|
463
541
|
// Note expression and track performance (core/expression.ts): the
|
|
464
542
|
// notes as played. A track with neither gets its notes back unchanged.
|
|
465
543
|
const timing = performanceTimingFor(score);
|
|
@@ -508,13 +586,31 @@ export class StemRenderer {
|
|
|
508
586
|
const engine = engineFor(track);
|
|
509
587
|
// The song key always joins an engine's stem key: engines may tune to
|
|
510
588
|
// it (sympathetic strings) without a song tuning.
|
|
511
|
-
const
|
|
589
|
+
const clipDigest = track
|
|
590
|
+
? clipsDigest(track, bank, options.guide === true)
|
|
591
|
+
: undefined;
|
|
592
|
+
const baseDigests =
|
|
512
593
|
engine && track
|
|
513
594
|
? [
|
|
514
595
|
...(engine.assetDigests?.(track, bank, score) ?? []),
|
|
515
596
|
`key:${score.key ?? ""}`,
|
|
597
|
+
...(clipDigest === undefined ? [] : [`clips:${clipDigest}`]),
|
|
516
598
|
]
|
|
517
|
-
: undefined
|
|
599
|
+
: clipDigest === undefined
|
|
600
|
+
? undefined
|
|
601
|
+
: [`clips:${clipDigest}`];
|
|
602
|
+
// A vocoder carrier (0.7) also keys on its modulator's tap.
|
|
603
|
+
const vocoderSource = track?.vocoder
|
|
604
|
+
? vocoderSourceOf(track, groups, performed, context, bank)
|
|
605
|
+
: undefined;
|
|
606
|
+
const withVocoder = vocoderSource
|
|
607
|
+
? [...(baseDigests ?? []), vocoderSource.key]
|
|
608
|
+
: baseDigests;
|
|
609
|
+
// 0.7 autotune: its targets (guide notes, chords, key, tuning) and the
|
|
610
|
+
// pitch engine version join the stem key; absent adds nothing.
|
|
611
|
+
const engineDigests = track?.autotune
|
|
612
|
+
? [...(withVocoder ?? []), ...autotuneStemDigests(track, score)]
|
|
613
|
+
: withVocoder;
|
|
518
614
|
// Wavetable hook: the oscillator factory for a wavetable track (its
|
|
519
615
|
// table id joins the stem key), undefined for every other instrument.
|
|
520
616
|
const wavetable = track ? wavetableHook(track, bank, context) : undefined;
|
|
@@ -570,74 +666,32 @@ export class StemRenderer {
|
|
|
570
666
|
}
|
|
571
667
|
: { left, right };
|
|
572
668
|
dry.fill(0);
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
669
|
+
const stereo = renderVoiceInto(
|
|
670
|
+
dry,
|
|
671
|
+
dryR,
|
|
672
|
+
played,
|
|
673
|
+
track,
|
|
674
|
+
context,
|
|
675
|
+
score,
|
|
676
|
+
bank,
|
|
677
|
+
engine,
|
|
678
|
+
sampler,
|
|
679
|
+
wavetable,
|
|
680
|
+
tuning,
|
|
681
|
+
options.guide === true,
|
|
682
|
+
);
|
|
683
|
+
// The vocoder stage (0.7): after the voice, before the chain.
|
|
684
|
+
if (track && vocoderSource)
|
|
685
|
+
applyVocoder(
|
|
580
686
|
dry,
|
|
581
687
|
stereo ? dryR : undefined,
|
|
582
|
-
|
|
688
|
+
this.vocoderTap(vocoderSource, context, bank),
|
|
583
689
|
track,
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
ticksPerBeat: score.ticksPerBeat,
|
|
587
|
-
...(tuning ? { tuning } : {}),
|
|
588
|
-
},
|
|
589
|
-
bank,
|
|
690
|
+
context,
|
|
691
|
+
vocoderSource.gateDb,
|
|
590
692
|
);
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
if (track)
|
|
594
|
-
stereo = renderSamplerNotes(
|
|
595
|
-
dry,
|
|
596
|
-
played,
|
|
597
|
-
track,
|
|
598
|
-
context,
|
|
599
|
-
bank,
|
|
600
|
-
dryR,
|
|
601
|
-
);
|
|
602
|
-
} else if (synthVoice && track) {
|
|
603
|
-
const gainAt = (tick: number) => trackGainAt(track, tick);
|
|
604
|
-
const voice = {
|
|
605
|
-
...context,
|
|
606
|
-
ticksPerBeat: score.ticksPerBeat,
|
|
607
|
-
...(wavetable ? { oscillatorFor: wavetable.oscillatorFor } : {}),
|
|
608
|
-
...(tuning ? { tuning } : {}),
|
|
609
|
-
};
|
|
610
|
-
const warp = context.warp;
|
|
611
|
-
for (const note of played) {
|
|
612
|
-
const { start, length } = noteSpan(note, context);
|
|
613
|
-
// The voice asks for `startTick + elapsed / samplesPerTick`;
|
|
614
|
-
// through a tempo map that sample's tick comes from the map.
|
|
615
|
-
const noteGainAt = warp
|
|
616
|
-
? (tick: number) =>
|
|
617
|
-
gainAt(
|
|
618
|
-
warp.tick(
|
|
619
|
-
start + (tick - note.startTick) * context.samplesPerTick,
|
|
620
|
-
),
|
|
621
|
-
)
|
|
622
|
-
: gainAt;
|
|
623
|
-
renderSynthNote(
|
|
624
|
-
dry,
|
|
625
|
-
stereo ? dryR : undefined,
|
|
626
|
-
note,
|
|
627
|
-
track,
|
|
628
|
-
start,
|
|
629
|
-
length,
|
|
630
|
-
voice,
|
|
631
|
-
noteGainAt,
|
|
632
|
-
);
|
|
633
|
-
}
|
|
634
|
-
} else {
|
|
635
|
-
const drums = isDrumInstrument(track?.instrument);
|
|
636
|
-
for (const note of played) {
|
|
637
|
-
if (drums) renderDrumNote(dry, note, track, context);
|
|
638
|
-
else renderToneNote(dry, note, track, context, tuning);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
693
|
+
// A non-finite source sample would poison every feedback stage.
|
|
694
|
+
let bad = scrubNonFinite(dry) + (stereo ? scrubNonFinite(dryR) : 0);
|
|
641
695
|
// Note-aware stages (bloom, swell) see the notes; others never do.
|
|
642
696
|
const chain =
|
|
643
697
|
track && needsEffectNotes(track)
|
|
@@ -664,6 +718,8 @@ export class StemRenderer {
|
|
|
664
718
|
true,
|
|
665
719
|
false,
|
|
666
720
|
);
|
|
721
|
+
// The room's input: scrubbed so the saved pair is clean too.
|
|
722
|
+
bad += scrubNonFinite(target.left) + scrubNonFinite(target.right);
|
|
667
723
|
const pre = {
|
|
668
724
|
left: Float64Array.from(target.left),
|
|
669
725
|
right: Float64Array.from(target.right),
|
|
@@ -681,6 +737,7 @@ export class StemRenderer {
|
|
|
681
737
|
chain,
|
|
682
738
|
busOrbit === undefined,
|
|
683
739
|
);
|
|
740
|
+
bad += scrubNonFinite(target.left) + scrubNonFinite(target.right);
|
|
684
741
|
stem = {
|
|
685
742
|
key,
|
|
686
743
|
left: target.left,
|
|
@@ -688,9 +745,14 @@ export class StemRenderer {
|
|
|
688
745
|
bytes: target.left.byteLength * (saved ? 6 : 2),
|
|
689
746
|
used: this.renders,
|
|
690
747
|
...(saved ? { room: saved } : {}),
|
|
748
|
+
...(bad > 0 ? { nonFinite: bad } : {}),
|
|
691
749
|
};
|
|
692
750
|
if (caching) this.store(trackId, stem);
|
|
693
751
|
}
|
|
752
|
+
if (stem.nonFinite) {
|
|
753
|
+
nonFinite.samples += stem.nonFinite;
|
|
754
|
+
nonFinite.tracks.push(trackId);
|
|
755
|
+
}
|
|
694
756
|
if (track && busOrbit !== undefined) {
|
|
695
757
|
let bus = buses.get(busOrbit);
|
|
696
758
|
if (!bus)
|
|
@@ -749,6 +811,12 @@ export class StemRenderer {
|
|
|
749
811
|
Math.round(RING_OUT_FADE_SECONDS * sampleRate),
|
|
750
812
|
);
|
|
751
813
|
if (samples > frames) foldTail(mixL, mixR, frames, samples);
|
|
814
|
+
// Bus returns: a last scrub before the master's own feedback stages.
|
|
815
|
+
const busBad = scrubNonFinite(mixL) + scrubNonFinite(mixR);
|
|
816
|
+
if (busBad > 0) {
|
|
817
|
+
nonFinite.samples += busBad;
|
|
818
|
+
nonFinite.tracks.push("bus");
|
|
819
|
+
}
|
|
752
820
|
// The song master (src/audio/master.ts); none leaves the mix untouched.
|
|
753
821
|
const mastered = applyMaster(
|
|
754
822
|
mixL,
|
|
@@ -769,9 +837,40 @@ export class StemRenderer {
|
|
|
769
837
|
frames,
|
|
770
838
|
pcm,
|
|
771
839
|
...(mastered ? { master: mastered.report } : {}),
|
|
840
|
+
...(nonFinite.samples > 0
|
|
841
|
+
? {
|
|
842
|
+
nonFinite: Object.freeze({
|
|
843
|
+
samples: nonFinite.samples,
|
|
844
|
+
tracks: Object.freeze([...nonFinite.tracks]),
|
|
845
|
+
}),
|
|
846
|
+
}
|
|
847
|
+
: {}),
|
|
772
848
|
});
|
|
773
849
|
}
|
|
774
850
|
|
|
851
|
+
/**
|
|
852
|
+
* A vocoder modulator's mono tap over this render: the source's voice,
|
|
853
|
+
* through its mono chain for `tap: chain`, stereo summed (L+R)/2. Kept
|
|
854
|
+
* across renders under the stem cache budget, keyed by the source key.
|
|
855
|
+
*/
|
|
856
|
+
private vocoderTap(
|
|
857
|
+
source: VocoderSource,
|
|
858
|
+
context: RenderContext,
|
|
859
|
+
bank: SampleBank,
|
|
860
|
+
): Float64Array {
|
|
861
|
+
const known = this.modSources.get(source.key);
|
|
862
|
+
if (known && known.tap.length === context.samples) {
|
|
863
|
+
known.used = this.renders;
|
|
864
|
+
return known.tap;
|
|
865
|
+
}
|
|
866
|
+
const tap = renderVocoderTap(source, context, bank);
|
|
867
|
+
for (const [key, entry] of this.modSources)
|
|
868
|
+
if (entry.used < this.renders - 1) this.modSources.delete(key);
|
|
869
|
+
if (this.maxCacheBytes > 0)
|
|
870
|
+
this.modSources.set(source.key, { tap, used: this.renders });
|
|
871
|
+
return tap;
|
|
872
|
+
}
|
|
873
|
+
|
|
775
874
|
private scratchFor(samples: number) {
|
|
776
875
|
if (this.scratch.dry.length < samples) {
|
|
777
876
|
this.scratch = {
|
|
@@ -994,6 +1093,8 @@ function stemKey(
|
|
|
994
1093
|
...(context.score.tuning || track?.tuning
|
|
995
1094
|
? [context.score.tuning ?? null, context.score.key]
|
|
996
1095
|
: []),
|
|
1096
|
+
// Calibrated engines (0.7) render differently; absent keeps old keys.
|
|
1097
|
+
...(context.score.calibration ? [`cal:${context.score.calibration}`] : []),
|
|
997
1098
|
// Seeded noise follows the window origin of an arranged render.
|
|
998
1099
|
...(context.seedTick ? [`seed:${context.seedTick}`] : []),
|
|
999
1100
|
...(context.seedSeconds ? [`at:${context.seedSeconds}`] : []),
|
|
@@ -1014,6 +1115,279 @@ function samplerVoiceDigest(track: Track, bank: SampleBank): string[] {
|
|
|
1014
1115
|
});
|
|
1015
1116
|
}
|
|
1016
1117
|
|
|
1118
|
+
/** A vocoder carrier's modulator: its track, notes, key and gate. */
|
|
1119
|
+
/** FX_CHAIN index of pan: a `tap: chain` modulator runs the stages before. */
|
|
1120
|
+
const TAP_END = FX_CHAIN.indexOf("pan");
|
|
1121
|
+
|
|
1122
|
+
type VocoderSource = Readonly<{
|
|
1123
|
+
track: Track;
|
|
1124
|
+
played: readonly PerformedNote[];
|
|
1125
|
+
tap: "chain" | "dry";
|
|
1126
|
+
key: string;
|
|
1127
|
+
gateDb: number;
|
|
1128
|
+
}>;
|
|
1129
|
+
|
|
1130
|
+
/** True for a built-in carrier that sounds without notes (chords, drone). */
|
|
1131
|
+
function vocoderPlaysWithoutNotes(track: Track): boolean {
|
|
1132
|
+
if (!track.vocoder || track.instrument !== VOCODER_INSTRUMENT) return false;
|
|
1133
|
+
const follow = resolveVocoder(track.vocoder).follow;
|
|
1134
|
+
return follow === "chords" || follow === "drone";
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const assetGates = new WeakMap<DecodedSample, number>();
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* The auto gate from a source's assets (its sampler files): the quietest
|
|
1141
|
+
* file's 10th-percentile 10 ms frame level + 6 dB, never the render span.
|
|
1142
|
+
* A source without assets (a synth) has no noise floor: the gate is off.
|
|
1143
|
+
*/
|
|
1144
|
+
function sourceGateDb(source: Track, bank: SampleBank): number {
|
|
1145
|
+
let gate: number | undefined;
|
|
1146
|
+
for (const voice of Object.keys(source.sampler?.voices ?? {}).sort()) {
|
|
1147
|
+
const sample = bank.voices.get(sampleKey(source.id, voice));
|
|
1148
|
+
if (!sample) continue;
|
|
1149
|
+
let level = assetGates.get(sample);
|
|
1150
|
+
if (level === undefined) {
|
|
1151
|
+
level = autoGateDb(Float64Array.from(sample.mono), sample.sampleRate);
|
|
1152
|
+
assetGates.set(sample, level);
|
|
1153
|
+
}
|
|
1154
|
+
if (level > -120) gate = gate === undefined ? level : Math.min(gate, level);
|
|
1155
|
+
}
|
|
1156
|
+
return gate ?? -120;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/** Resolves a carrier's modulator, or undefined (the stage is bypassed). */
|
|
1160
|
+
function vocoderSourceOf(
|
|
1161
|
+
track: Track,
|
|
1162
|
+
groups: ReadonlyMap<string, readonly Note[]>,
|
|
1163
|
+
performed: ReadonlyMap<string, readonly PerformedNote[]>,
|
|
1164
|
+
context: RenderContext,
|
|
1165
|
+
bank: SampleBank,
|
|
1166
|
+
): VocoderSource | undefined {
|
|
1167
|
+
const ref = track.vocoder?.src;
|
|
1168
|
+
if (ref === undefined) return undefined;
|
|
1169
|
+
const source = resolveTrackRef(context.score, ref);
|
|
1170
|
+
if (!source || source.id === track.id) return undefined;
|
|
1171
|
+
const settings = resolveVocoder(track.vocoder);
|
|
1172
|
+
const notes = groups.get(source.id) ?? [];
|
|
1173
|
+
const engine = engineFor(source);
|
|
1174
|
+
// Post-tap fields never change the tap (mute, solo and volume included).
|
|
1175
|
+
const {
|
|
1176
|
+
vocoder: _vocoder,
|
|
1177
|
+
pan: _pan,
|
|
1178
|
+
volume: _volume,
|
|
1179
|
+
volumeAutomation: _volumeAutomation,
|
|
1180
|
+
panAutomation: _panAutomation,
|
|
1181
|
+
delay: _delay,
|
|
1182
|
+
delayFeedbackAutomation: _delayFeedback,
|
|
1183
|
+
delayMixAutomation: _delayMix,
|
|
1184
|
+
reverb: _reverb,
|
|
1185
|
+
...kept
|
|
1186
|
+
} = source;
|
|
1187
|
+
// Only the stages the tap runs (before pan, and none for `tap: dry`):
|
|
1188
|
+
// a source's reverb, delay, double or other post-pan fx never re-render
|
|
1189
|
+
// its carriers. fxAutomation stays (rarely post-pan; a cheap re-render).
|
|
1190
|
+
const tapped = settings.tap === "chain" ? FX_CHAIN.slice(0, TAP_END) : [];
|
|
1191
|
+
const fx = kept.fx
|
|
1192
|
+
? Object.fromEntries(
|
|
1193
|
+
Object.entries(kept.fx).filter(
|
|
1194
|
+
([stage]) =>
|
|
1195
|
+
tapped.includes(stage as never) ||
|
|
1196
|
+
!FX_CHAIN.includes(stage as never),
|
|
1197
|
+
),
|
|
1198
|
+
)
|
|
1199
|
+
: undefined;
|
|
1200
|
+
const stripped = {
|
|
1201
|
+
...kept,
|
|
1202
|
+
fx: fx && Object.keys(fx).length > 0 ? fx : undefined,
|
|
1203
|
+
} as unknown as Track;
|
|
1204
|
+
const gateDb =
|
|
1205
|
+
settings.gate === "auto" ? sourceGateDb(source, bank) : settings.gate;
|
|
1206
|
+
// The modulator's clips (0.7) are heard; guide tones never are.
|
|
1207
|
+
const clipDigest = clipsDigest(source, bank, false);
|
|
1208
|
+
// A tuned modulator (0.7 autotune) keys on its targets too: its guide
|
|
1209
|
+
// track's notes, the chord timeline and the key change the tap.
|
|
1210
|
+
const clipKeys = [
|
|
1211
|
+
...(clipDigest === undefined ? [] : [`clips:${clipDigest}`]),
|
|
1212
|
+
...(source.autotune ? autotuneStemDigests(source, context.score) : []),
|
|
1213
|
+
];
|
|
1214
|
+
const key = stemKey(
|
|
1215
|
+
stripped,
|
|
1216
|
+
notes,
|
|
1217
|
+
context,
|
|
1218
|
+
isSamplerInstrument(source.instrument) ? bank : undefined,
|
|
1219
|
+
wavetableHook(source, bank, context)?.id,
|
|
1220
|
+
undefined,
|
|
1221
|
+
engine
|
|
1222
|
+
? [
|
|
1223
|
+
...(engine.assetDigests?.(source, bank, context.score) ?? []),
|
|
1224
|
+
`key:${context.score.key ?? ""}`,
|
|
1225
|
+
...clipKeys,
|
|
1226
|
+
]
|
|
1227
|
+
: clipKeys.length > 0
|
|
1228
|
+
? clipKeys
|
|
1229
|
+
: undefined,
|
|
1230
|
+
);
|
|
1231
|
+
return {
|
|
1232
|
+
track: source,
|
|
1233
|
+
played: performed.get(source.id) ?? [],
|
|
1234
|
+
tap: settings.tap,
|
|
1235
|
+
key: `vocsrc:${settings.tap}:${gateDb}:${key}`,
|
|
1236
|
+
gateDb,
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/** Renders a modulator's mono tap (see `StemRenderer.vocoderTap`). */
|
|
1241
|
+
function renderVocoderTap(
|
|
1242
|
+
source: VocoderSource,
|
|
1243
|
+
context: RenderContext,
|
|
1244
|
+
bank: SampleBank,
|
|
1245
|
+
): Float64Array {
|
|
1246
|
+
const { played } = source;
|
|
1247
|
+
// The tap is rendered at unity gain: the voice bakes track volume in
|
|
1248
|
+
// (trackGainAt), and volume is a post-tap field the cache key leaves out.
|
|
1249
|
+
const track: Track = {
|
|
1250
|
+
...source.track,
|
|
1251
|
+
volume: 1,
|
|
1252
|
+
volumeAutomation: [],
|
|
1253
|
+
};
|
|
1254
|
+
const { score } = context;
|
|
1255
|
+
const dry = new Float64Array(context.samples);
|
|
1256
|
+
const dryR = new Float64Array(context.samples);
|
|
1257
|
+
const tuning = resolveTuning(score.tuning, track.tuning, score.key);
|
|
1258
|
+
const stereo = renderVoiceInto(
|
|
1259
|
+
dry,
|
|
1260
|
+
dryR,
|
|
1261
|
+
played,
|
|
1262
|
+
track,
|
|
1263
|
+
context,
|
|
1264
|
+
score,
|
|
1265
|
+
bank,
|
|
1266
|
+
engineFor(track),
|
|
1267
|
+
isSamplerInstrument(track.instrument),
|
|
1268
|
+
wavetableHook(track, bank, context),
|
|
1269
|
+
tuning,
|
|
1270
|
+
false,
|
|
1271
|
+
);
|
|
1272
|
+
if (source.tap === "chain") {
|
|
1273
|
+
const chain = needsEffectNotes(track)
|
|
1274
|
+
? { ...context, notes: effectNotes(played, context, tuning) }
|
|
1275
|
+
: context;
|
|
1276
|
+
applyMonoChain(dry, track, chain);
|
|
1277
|
+
if (stereo) applyMonoChain(dryR, track, chain);
|
|
1278
|
+
}
|
|
1279
|
+
if (stereo)
|
|
1280
|
+
for (let i = 0; i < dry.length; i += 1) dry[i] = (dry[i]! + dryR[i]!) / 2;
|
|
1281
|
+
return dry;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* Renders a track's voice (engine, sampler, synth or tone/drum notes) into
|
|
1286
|
+
* `dry` (and `dryR` when stereo) before any effect; returns whether it
|
|
1287
|
+
* wrote a right channel. Shared by the stem pass and the vocoder tap.
|
|
1288
|
+
*/
|
|
1289
|
+
function renderVoiceInto(
|
|
1290
|
+
dry: Float64Array,
|
|
1291
|
+
dryR: Float64Array,
|
|
1292
|
+
played: readonly PerformedNote[],
|
|
1293
|
+
track: Track | undefined,
|
|
1294
|
+
context: RenderContext,
|
|
1295
|
+
score: TrackScore,
|
|
1296
|
+
bank: SampleBank,
|
|
1297
|
+
engine: InstrumentEngine | undefined,
|
|
1298
|
+
sampler: boolean,
|
|
1299
|
+
wavetable: ReturnType<typeof wavetableHook> | undefined,
|
|
1300
|
+
tuning: TuningTable | undefined,
|
|
1301
|
+
guide: boolean,
|
|
1302
|
+
): boolean {
|
|
1303
|
+
const synthVoice = !engine && !sampler && usesSynthVoice(track);
|
|
1304
|
+
let stereo = engine
|
|
1305
|
+
? engine.stereo(track!)
|
|
1306
|
+
: synthVoice && isStereoVoice(track);
|
|
1307
|
+
if (stereo) dryR.fill(0);
|
|
1308
|
+
if (engine && track) {
|
|
1309
|
+
engine.render(
|
|
1310
|
+
dry,
|
|
1311
|
+
stereo ? dryR : undefined,
|
|
1312
|
+
played,
|
|
1313
|
+
track,
|
|
1314
|
+
{
|
|
1315
|
+
...context,
|
|
1316
|
+
ticksPerBeat: score.ticksPerBeat,
|
|
1317
|
+
...(tuning ? { tuning } : {}),
|
|
1318
|
+
},
|
|
1319
|
+
bank,
|
|
1320
|
+
);
|
|
1321
|
+
} else if (sampler) {
|
|
1322
|
+
// A resampled stereo file plays as stereo (0.6.1).
|
|
1323
|
+
if (track)
|
|
1324
|
+
stereo = renderSamplerNotes(dry, played, track, context, bank, dryR);
|
|
1325
|
+
} else if (synthVoice && track) {
|
|
1326
|
+
const gainAt = (tick: number) => trackGainAt(track, tick);
|
|
1327
|
+
const voice = {
|
|
1328
|
+
...context,
|
|
1329
|
+
ticksPerBeat: score.ticksPerBeat,
|
|
1330
|
+
...(wavetable ? { oscillatorFor: wavetable.oscillatorFor } : {}),
|
|
1331
|
+
...(tuning ? { tuning } : {}),
|
|
1332
|
+
};
|
|
1333
|
+
const warp = context.warp;
|
|
1334
|
+
for (const note of played) {
|
|
1335
|
+
const { start, length } = noteSpan(note, context);
|
|
1336
|
+
// The voice asks for `startTick + elapsed / samplesPerTick`;
|
|
1337
|
+
// through a tempo map that sample's tick comes from the map.
|
|
1338
|
+
const noteGainAt = warp
|
|
1339
|
+
? (tick: number) =>
|
|
1340
|
+
gainAt(
|
|
1341
|
+
warp.tick(
|
|
1342
|
+
start + (tick - note.startTick) * context.samplesPerTick,
|
|
1343
|
+
),
|
|
1344
|
+
)
|
|
1345
|
+
: gainAt;
|
|
1346
|
+
renderSynthNote(
|
|
1347
|
+
dry,
|
|
1348
|
+
stereo ? dryR : undefined,
|
|
1349
|
+
note,
|
|
1350
|
+
track,
|
|
1351
|
+
start,
|
|
1352
|
+
length,
|
|
1353
|
+
voice,
|
|
1354
|
+
noteGainAt,
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
} else if (track?.clips && isGuideInstrument(track.instrument)) {
|
|
1358
|
+
// A `vocal` track's notes guide its clips: silent in a render,
|
|
1359
|
+
// a soft sine in the live loop with `guide` on. (Without clips,
|
|
1360
|
+
// an older project's `vocal` keeps its tone.)
|
|
1361
|
+
if (guide)
|
|
1362
|
+
for (const note of played)
|
|
1363
|
+
renderToneNote(
|
|
1364
|
+
dry,
|
|
1365
|
+
{ ...note, velocity: note.velocity * GUIDE_LEVEL },
|
|
1366
|
+
{ ...track, instrument: "sine" },
|
|
1367
|
+
context,
|
|
1368
|
+
tuning,
|
|
1369
|
+
);
|
|
1370
|
+
} else {
|
|
1371
|
+
const drums = isDrumInstrument(track?.instrument);
|
|
1372
|
+
// Calibration 1+ (0.7): a closed or pedal hat chokes a ringing open hat.
|
|
1373
|
+
const chokes =
|
|
1374
|
+
drums && (context.score.calibration ?? 0) >= 1
|
|
1375
|
+
? hatChokes(played, context)
|
|
1376
|
+
: undefined;
|
|
1377
|
+
for (const note of played) {
|
|
1378
|
+
if (drums) renderDrumNote(dry, note, track, context, chokes?.get(note));
|
|
1379
|
+
else renderToneNote(dry, note, track, context, tuning);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
// Audio clips (0.7) sum into the dry buffer before the chain.
|
|
1383
|
+
if (track?.clips) {
|
|
1384
|
+
const gainAt = (tick: number) => trackGainAt(track, tick);
|
|
1385
|
+
renderClips(dry, track, context, bank, gainAt, score);
|
|
1386
|
+
if (stereo) renderClips(dryR, track, context, bank, gainAt, score);
|
|
1387
|
+
}
|
|
1388
|
+
return stereo;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1017
1391
|
/**
|
|
1018
1392
|
* Sample voices of a sampler track into its mono dry buffer. When a voice
|
|
1019
1393
|
* plays a resampled stereo file (0.6.1), `target` takes the left channel,
|
|
@@ -1113,6 +1487,7 @@ function renderToneNote(
|
|
|
1113
1487
|
const performance = (note as PerformedNote).performance;
|
|
1114
1488
|
const cents = performance?.cents;
|
|
1115
1489
|
let bent = 0;
|
|
1490
|
+
const wave = legacyWaveFor(instrument);
|
|
1116
1491
|
for (let index = start; index < end; index += 1) {
|
|
1117
1492
|
const elapsed = index - start;
|
|
1118
1493
|
const remaining = end - index;
|
|
@@ -1139,7 +1514,7 @@ function renderToneNote(
|
|
|
1139
1514
|
const t = elapsed / sampleRate;
|
|
1140
1515
|
const held =
|
|
1141
1516
|
damp && t > damp.from ? Math.exp(-(t - damp.from) / damp.tau) : 1;
|
|
1142
|
-
target[index]! +=
|
|
1517
|
+
target[index]! += wave(phase) * envelope * held;
|
|
1143
1518
|
}
|
|
1144
1519
|
}
|
|
1145
1520
|
|
|
@@ -1148,11 +1523,40 @@ function renderToneNote(
|
|
|
1148
1523
|
* id and start tick, so a hit sounds identical on every render and in every
|
|
1149
1524
|
* window regardless of note order.
|
|
1150
1525
|
*/
|
|
1526
|
+
/** Closed hat (42) and pedal hat (44) GM numbers that choke an open hat. */
|
|
1527
|
+
const CHOKING_HATS = new Set([42, 44]);
|
|
1528
|
+
/** Open hat fade once choked, in seconds. */
|
|
1529
|
+
const CHOKE_SECONDS = 0.008;
|
|
1530
|
+
|
|
1531
|
+
/**
|
|
1532
|
+
* For each open hat (46), the sample where the next closed or pedal hat
|
|
1533
|
+
* on the same track starts, as on a real hi-hat stand.
|
|
1534
|
+
*/
|
|
1535
|
+
function hatChokes(
|
|
1536
|
+
notes: readonly Note[],
|
|
1537
|
+
context: RenderContext,
|
|
1538
|
+
): Map<Note, number> {
|
|
1539
|
+
const closers = notes
|
|
1540
|
+
.filter((note) => CHOKING_HATS.has(note.pitch))
|
|
1541
|
+
.map((note) => noteSpan(note, context).start)
|
|
1542
|
+
.sort((a, b) => a - b);
|
|
1543
|
+
const chokes = new Map<Note, number>();
|
|
1544
|
+
if (closers.length === 0) return chokes;
|
|
1545
|
+
for (const note of notes) {
|
|
1546
|
+
if (note.pitch !== 46) continue;
|
|
1547
|
+
const start = noteSpan(note, context).start;
|
|
1548
|
+
const next = closers.find((closer) => closer > start);
|
|
1549
|
+
if (next !== undefined) chokes.set(note, next);
|
|
1550
|
+
}
|
|
1551
|
+
return chokes;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1151
1554
|
function renderDrumNote(
|
|
1152
1555
|
target: Float64Array,
|
|
1153
1556
|
note: Note,
|
|
1154
1557
|
track: Track | undefined,
|
|
1155
1558
|
context: RenderContext,
|
|
1559
|
+
chokeAt?: number,
|
|
1156
1560
|
): void {
|
|
1157
1561
|
const { sampleRate, samples, samplesPerTick } = context;
|
|
1158
1562
|
const { start } = noteSpan(note, context);
|
|
@@ -1160,9 +1564,24 @@ function renderDrumNote(
|
|
|
1160
1564
|
// one the voices below render exactly as they always have.
|
|
1161
1565
|
const kit = synthKit(track?.kit);
|
|
1162
1566
|
const kitState = kit ? newKitVoiceState() : undefined;
|
|
1567
|
+
// Calibration 1+ (0.7): pitched toms, GM cymbals and cowbell as metal
|
|
1568
|
+
// instead of a rim click, band-limited kit hats, and hat choke.
|
|
1569
|
+
const calibrated = (context.score.calibration ?? 0) >= 1;
|
|
1570
|
+
const metal = calibrated ? metalKindForPitch(note.pitch) : undefined;
|
|
1571
|
+
const metalState = metal ? newKitVoiceState() : undefined;
|
|
1572
|
+
const ratio = calibrated ? tomRatio(note.pitch) : 1;
|
|
1573
|
+
const chokeEnd =
|
|
1574
|
+
chokeAt === undefined
|
|
1575
|
+
? Number.POSITIVE_INFINITY
|
|
1576
|
+
: chokeAt + Math.ceil(CHOKE_SECONDS * sampleRate);
|
|
1163
1577
|
const end = Math.min(
|
|
1164
1578
|
samples,
|
|
1165
|
-
|
|
1579
|
+
chokeEnd,
|
|
1580
|
+
start +
|
|
1581
|
+
Math.ceil(
|
|
1582
|
+
(metal ? METAL_SECONDS : (kit?.seconds ?? MAX_DRUM_SECONDS)) *
|
|
1583
|
+
sampleRate,
|
|
1584
|
+
),
|
|
1166
1585
|
);
|
|
1167
1586
|
const voice = drumVoiceForPitch(note.pitch);
|
|
1168
1587
|
const random = seededRandom(
|
|
@@ -1179,7 +1598,10 @@ function renderDrumNote(
|
|
|
1179
1598
|
const bright = noise - previousNoise;
|
|
1180
1599
|
previousNoise = noise;
|
|
1181
1600
|
let sample = 0;
|
|
1182
|
-
if (
|
|
1601
|
+
if (metal && metalState) {
|
|
1602
|
+
sample = metalSample(metal, t, bright, metalState, sampleRate);
|
|
1603
|
+
if (kit) sample *= kit.gain;
|
|
1604
|
+
} else if (kit && kitState)
|
|
1183
1605
|
sample = kitDrumSample(
|
|
1184
1606
|
kit,
|
|
1185
1607
|
voice,
|
|
@@ -1188,6 +1610,7 @@ function renderDrumNote(
|
|
|
1188
1610
|
bright,
|
|
1189
1611
|
kitState,
|
|
1190
1612
|
sampleRate,
|
|
1613
|
+
calibrated ? { tomRatio: ratio } : undefined,
|
|
1191
1614
|
);
|
|
1192
1615
|
else if (voice === "kick") {
|
|
1193
1616
|
const frequency = 45 + 105 * Math.exp(-t * 28);
|
|
@@ -1196,7 +1619,7 @@ function renderDrumNote(
|
|
|
1196
1619
|
Math.sin(2 * Math.PI * phase) * Math.exp(-t * 7.5) +
|
|
1197
1620
|
noise * 0.12 * Math.exp(-t * 300);
|
|
1198
1621
|
} else if (voice === "tom") {
|
|
1199
|
-
const frequency = 105 + 95 * Math.exp(-t * 18);
|
|
1622
|
+
const frequency = (105 + 95 * Math.exp(-t * 18)) * ratio;
|
|
1200
1623
|
phase += frequency / sampleRate;
|
|
1201
1624
|
sample = Math.sin(2 * Math.PI * phase) * Math.exp(-t * 9);
|
|
1202
1625
|
} else if (voice === "snare") {
|
|
@@ -1223,10 +1646,10 @@ function renderDrumNote(
|
|
|
1223
1646
|
1,
|
|
1224
1647
|
(index - start) / Math.max(1, sampleRate * 0.001),
|
|
1225
1648
|
);
|
|
1226
|
-
const release =
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1649
|
+
const release =
|
|
1650
|
+
chokeAt !== undefined && index >= chokeAt
|
|
1651
|
+
? Math.max(0, chokeEnd - index) / Math.max(1, chokeEnd - chokeAt)
|
|
1652
|
+
: Math.min(1, (end - index) / Math.max(1, sampleRate * 0.005));
|
|
1230
1653
|
target[index]! +=
|
|
1231
1654
|
sample *
|
|
1232
1655
|
attack *
|