@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
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.7 autotune render hooks (pitch.md section 7): the sampler calls
|
|
3
|
+
* `autotuneVoice` after the 0.6.1 shift step, and the clips renderer calls
|
|
4
|
+
* `autotuneClip` per clip. Both track the played buffer with the pitch
|
|
5
|
+
* engine, build targets (scale through the 0.5 tuning, the chord timeline,
|
|
6
|
+
* or guide notes through the tempo map), run `autotuneBuffer` from
|
|
7
|
+
* core/autotune.ts and keep the result in a tuned-span cache with its own
|
|
8
|
+
* 128 MB budget. A track without `autotune` never reaches this module.
|
|
9
|
+
*
|
|
10
|
+
* The pitch tracker and PSOLA belong to the pitch lane
|
|
11
|
+
* (src/audio/dsp/pitch.ts, src/audio/dsp/psola.ts, src/audio/analysis.ts).
|
|
12
|
+
* They reach this module as `builtinPitchEngine` (./autotune-engine.ts);
|
|
13
|
+
* tests swap it with `setPitchEngine`, and without an engine every hook
|
|
14
|
+
* returns the buffer untuned.
|
|
15
|
+
*/
|
|
16
|
+
import {
|
|
17
|
+
autotuneDigest,
|
|
18
|
+
autotuneJob,
|
|
19
|
+
centsOfHz,
|
|
20
|
+
classGrid,
|
|
21
|
+
chromaticGrid,
|
|
22
|
+
resolveAutotune,
|
|
23
|
+
scaleGrid,
|
|
24
|
+
type AutotuneCurve,
|
|
25
|
+
type AutotuneTargets,
|
|
26
|
+
type GuideNote,
|
|
27
|
+
type PsolaJob,
|
|
28
|
+
type PsolaShift,
|
|
29
|
+
type ResolvedAutotune,
|
|
30
|
+
} from "../../core/autotune.ts";
|
|
31
|
+
import { parseKey } from "../../core/chords.ts";
|
|
32
|
+
import type { Track, TrackScore } from "../../core/score.ts";
|
|
33
|
+
import { secondsAtTick, tickAtSeconds } from "../../core/tempo.ts";
|
|
34
|
+
import {
|
|
35
|
+
noteHz,
|
|
36
|
+
resolveTuning,
|
|
37
|
+
tuningPreset,
|
|
38
|
+
type TuningTable,
|
|
39
|
+
} from "../../core/tuning.ts";
|
|
40
|
+
import { deferLiveJob, liveFitActive } from "./fit.ts";
|
|
41
|
+
import { builtinPitchEngine } from "./autotune-engine.ts";
|
|
42
|
+
import { chordAt, chordDigest, chordTimeline } from "./granular.ts";
|
|
43
|
+
|
|
44
|
+
/** Tuned buffers stay under this many bytes (separate from the fit cache). */
|
|
45
|
+
export const AUTOTUNE_CACHE_BYTES = 128 * 1024 * 1024;
|
|
46
|
+
|
|
47
|
+
/** Live tunes buffers up to this long at once; longer ones run between blocks. */
|
|
48
|
+
export const LIVE_SYNC_AUTOTUNE_SECONDS = 0.25;
|
|
49
|
+
|
|
50
|
+
/** Source seconds kept either side of a timed span (PSOLA grains, glides). */
|
|
51
|
+
export const AUTOTUNE_SPAN_MARGIN_SECONDS = 0.05;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A buffer the engine tracks. `id` names the audio itself when it is not
|
|
55
|
+
* the decoded file (a shifted or fitted copy keeps the file's `sha256` but
|
|
56
|
+
* not its audio); the engine keys curves by `id ?? sha256`. Without either
|
|
57
|
+
* the buffer keys by identity.
|
|
58
|
+
*/
|
|
59
|
+
export type TunableBuffer = Readonly<{
|
|
60
|
+
id?: string;
|
|
61
|
+
sha256?: string;
|
|
62
|
+
sampleRate: number;
|
|
63
|
+
mono: Float32Array;
|
|
64
|
+
/** Stereo channels, tuned with the mono curve when present. */
|
|
65
|
+
left?: Float32Array;
|
|
66
|
+
right?: Float32Array;
|
|
67
|
+
}>;
|
|
68
|
+
|
|
69
|
+
/** Tuned audio: `from` is the buffer frame its first frame came from. */
|
|
70
|
+
export type TunedAudio = Readonly<{
|
|
71
|
+
mono: Float32Array;
|
|
72
|
+
left?: Float32Array;
|
|
73
|
+
right?: Float32Array;
|
|
74
|
+
from: number;
|
|
75
|
+
}>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* What the pitch lane provides: a curve for a buffer and the PSOLA pass.
|
|
79
|
+
* With `defer` (the live path) the engine must not analyse synchronously:
|
|
80
|
+
* it returns undefined and analyses in the background until the curve is
|
|
81
|
+
* ready. `psolaJob`, when given, is the resumable form of `psola` (same
|
|
82
|
+
* output) the live scheduler runs in slices. `version` joins every cache
|
|
83
|
+
* and stem key.
|
|
84
|
+
*/
|
|
85
|
+
export type PitchEngine = Readonly<{
|
|
86
|
+
version: string;
|
|
87
|
+
curve(
|
|
88
|
+
buffer: TunableBuffer,
|
|
89
|
+
voice: ResolvedAutotune["voice"],
|
|
90
|
+
options?: Readonly<{ defer?: boolean }>,
|
|
91
|
+
): AutotuneCurve | undefined;
|
|
92
|
+
psola: PsolaShift;
|
|
93
|
+
psolaJob?: PsolaJob;
|
|
94
|
+
}>;
|
|
95
|
+
|
|
96
|
+
let engine: PitchEngine | undefined = builtinPitchEngine;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Installs a pitch engine (tests); `undefined` leaves autotune inert and
|
|
100
|
+
* `builtinPitchEngine` restores the default.
|
|
101
|
+
*/
|
|
102
|
+
export function setPitchEngine(next: PitchEngine | undefined): void {
|
|
103
|
+
engine = next;
|
|
104
|
+
clearAutotuneCache();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The installed engine, if any. */
|
|
108
|
+
export function pitchEngine(): PitchEngine | undefined {
|
|
109
|
+
return engine;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Receipt suffix for the command, menu and agent tool: says so when the
|
|
114
|
+
* build has no pitch engine and the settings cannot change the audio yet.
|
|
115
|
+
*/
|
|
116
|
+
export function autotuneEngineNote(): string {
|
|
117
|
+
return engine
|
|
118
|
+
? ""
|
|
119
|
+
: " · no pitch tracker in this build: settings saved, audio plays untuned";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const cache = new Map<string, TunedAudio>();
|
|
123
|
+
let cachedBytes = 0;
|
|
124
|
+
const counters = { hits: 0, misses: 0, tuned: 0 };
|
|
125
|
+
|
|
126
|
+
/** Empties the tuned-span cache. */
|
|
127
|
+
export function clearAutotuneCache(): void {
|
|
128
|
+
cache.clear();
|
|
129
|
+
cachedBytes = 0;
|
|
130
|
+
counters.hits = 0;
|
|
131
|
+
counters.misses = 0;
|
|
132
|
+
counters.tuned = 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Bytes held and hit counts, for /pack cache and tests. */
|
|
136
|
+
export function autotuneCacheStatus(): Readonly<{
|
|
137
|
+
bytes: number;
|
|
138
|
+
entries: number;
|
|
139
|
+
hits: number;
|
|
140
|
+
misses: number;
|
|
141
|
+
tuned: number;
|
|
142
|
+
}> {
|
|
143
|
+
return { bytes: cachedBytes, entries: cache.size, ...counters };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function bytesOf(audio: TunedAudio): number {
|
|
147
|
+
return (
|
|
148
|
+
audio.mono.byteLength +
|
|
149
|
+
(audio.left?.byteLength ?? 0) +
|
|
150
|
+
(audio.right?.byteLength ?? 0)
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function remember(key: string, out: TunedAudio): void {
|
|
155
|
+
const size = bytesOf(out);
|
|
156
|
+
if (size > AUTOTUNE_CACHE_BYTES) return;
|
|
157
|
+
const old = cache.get(key);
|
|
158
|
+
if (old) {
|
|
159
|
+
cache.delete(key);
|
|
160
|
+
cachedBytes -= bytesOf(old);
|
|
161
|
+
}
|
|
162
|
+
cache.set(key, out);
|
|
163
|
+
cachedBytes += size;
|
|
164
|
+
for (const [stale, audio] of cache) {
|
|
165
|
+
if (cachedBytes <= AUTOTUNE_CACHE_BYTES) break;
|
|
166
|
+
cache.delete(stale);
|
|
167
|
+
cachedBytes -= bytesOf(audio);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Buffers without an id or content hash key by identity. */
|
|
172
|
+
const bufferIds = new WeakMap<Float32Array, string>();
|
|
173
|
+
let bufferSerial = 0;
|
|
174
|
+
|
|
175
|
+
function bufferId(buffer: TunableBuffer): string {
|
|
176
|
+
if (buffer.id) return `id:${buffer.id}`;
|
|
177
|
+
if (buffer.sha256) return `${buffer.sha256}:${buffer.mono.length}`;
|
|
178
|
+
let id = bufferIds.get(buffer.mono);
|
|
179
|
+
if (id === undefined)
|
|
180
|
+
bufferIds.set(buffer.mono, (id = `buf#${(bufferSerial += 1)}`));
|
|
181
|
+
return id;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** FNV-1a of a string, for digests that would otherwise grow with a song. */
|
|
185
|
+
function fnv(text: string): string {
|
|
186
|
+
let h = 0x811c9dc5;
|
|
187
|
+
for (let i = 0; i < text.length; i += 1)
|
|
188
|
+
h = Math.imul(h ^ text.charCodeAt(i), 0x01000193) >>> 0;
|
|
189
|
+
return h.toString(16).padStart(8, "0");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Where a buffer plays in the song: buffer second `b` sounds at song
|
|
194
|
+
* second `start + (b - offset) / rate`, at `rate` times its pitch.
|
|
195
|
+
*/
|
|
196
|
+
export type Placement = Readonly<{
|
|
197
|
+
/** Song seconds the buffer's `offset` sounds at. */
|
|
198
|
+
start: number;
|
|
199
|
+
/** Buffer seconds that play first (region begin, clip offset). */
|
|
200
|
+
offset: number;
|
|
201
|
+
/** Playback rate: 2 is an octave up and twice as fast (tape). */
|
|
202
|
+
rate: number;
|
|
203
|
+
}>;
|
|
204
|
+
|
|
205
|
+
/** The targets source a track uses, before placement. */
|
|
206
|
+
type TargetPlan =
|
|
207
|
+
| Readonly<{ kind: "static"; digest: string; targets: AutotuneTargets }>
|
|
208
|
+
| Readonly<{
|
|
209
|
+
kind: "timed";
|
|
210
|
+
digest: string;
|
|
211
|
+
build(p: Placement): AutotuneTargets;
|
|
212
|
+
}>;
|
|
213
|
+
|
|
214
|
+
/** The track the guide notes come from: `from`, else the track itself. */
|
|
215
|
+
function guideTrack(
|
|
216
|
+
score: TrackScore,
|
|
217
|
+
track: Track,
|
|
218
|
+
r: ResolvedAutotune,
|
|
219
|
+
): Track | undefined {
|
|
220
|
+
if (r.from === undefined) return track;
|
|
221
|
+
return score.tracks.find((other) => other.id === r.from);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Guide notes in song seconds, cents re A440 through the guide's tuning. */
|
|
225
|
+
export function guideNotes(
|
|
226
|
+
score: TrackScore,
|
|
227
|
+
guide: Track,
|
|
228
|
+
): readonly GuideNote[] {
|
|
229
|
+
const table = resolveTuning(score.tuning, guide.tuning, score.key);
|
|
230
|
+
const out: GuideNote[] = [];
|
|
231
|
+
for (const note of score.notes) {
|
|
232
|
+
if (note.trackId !== guide.id || note.durationTicks <= 0) continue;
|
|
233
|
+
const start = secondsAtTick(score, note.startTick);
|
|
234
|
+
const end = secondsAtTick(score, note.startTick + note.durationTicks);
|
|
235
|
+
out.push({
|
|
236
|
+
start,
|
|
237
|
+
end,
|
|
238
|
+
cents: centsOfHz(noteHz(note.pitch, note.cents, table)),
|
|
239
|
+
...(note.drift !== undefined ? { drift: note.drift } : {}),
|
|
240
|
+
...(note.articulation === "ghost" ? { ghost: true } : {}),
|
|
241
|
+
...(note.glide !== undefined ? { glide: note.glide } : {}),
|
|
242
|
+
...(note.vibrato
|
|
243
|
+
? {
|
|
244
|
+
vib: note.vibrato.rate,
|
|
245
|
+
vibmod: note.vibrato.depth / 100,
|
|
246
|
+
...(note.vibrato.delay ? { vibDelay: note.vibrato.delay } : {}),
|
|
247
|
+
}
|
|
248
|
+
: {}),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return out.sort((a, b) => a.start - b.start || a.cents - b.cents);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Whether the track has notes of its own to guide `to: notes`. */
|
|
255
|
+
export function hasGuideNotes(score: TrackScore, trackId: string): boolean {
|
|
256
|
+
return score.notes.some(
|
|
257
|
+
(note) => note.trackId === trackId && note.durationTicks > 0,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function guideDigest(notes: readonly GuideNote[]): string {
|
|
262
|
+
return `guide:${notes.length}:${fnv(
|
|
263
|
+
notes
|
|
264
|
+
.map(
|
|
265
|
+
(n) =>
|
|
266
|
+
`${n.start.toFixed(6)}-${n.end.toFixed(6)}@${n.cents.toFixed(3)}${n.drift !== undefined ? `d${n.drift}` : ""}${n.ghost ? "g" : ""}${n.glide !== undefined ? `l${n.glide}` : ""}${n.vib !== undefined ? `v${n.vib}/${n.vibmod}/${n.vibDelay ?? 0}` : ""}`,
|
|
267
|
+
)
|
|
268
|
+
.join(","),
|
|
269
|
+
)}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function tableDigest(table: TuningTable | undefined): string {
|
|
273
|
+
if (!table) return "12tet";
|
|
274
|
+
return `tun:${fnv(Array.from(table.hz, (hz) => hz.toFixed(6)).join(","))}`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The scale preset a key override brings when neither the song nor the
|
|
279
|
+
* track sets a tuning (`key D bayati` targets bayati's quarter tones,
|
|
280
|
+
* `key C yaman` its shruti intonation). Undefined otherwise.
|
|
281
|
+
*/
|
|
282
|
+
export function autotuneKeyTuning(
|
|
283
|
+
score: TrackScore,
|
|
284
|
+
track: Track,
|
|
285
|
+
): string | undefined {
|
|
286
|
+
const keyText = track.autotune?.key;
|
|
287
|
+
if (!keyText || score.tuning || track.tuning) return undefined;
|
|
288
|
+
const scale = parseKey(keyText)?.scale;
|
|
289
|
+
return scale && tuningPreset(scale) ? scale : undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function tableFor(
|
|
293
|
+
score: TrackScore,
|
|
294
|
+
track: Track,
|
|
295
|
+
r: ResolvedAutotune,
|
|
296
|
+
): TuningTable | undefined {
|
|
297
|
+
const preset = autotuneKeyTuning(score, track);
|
|
298
|
+
if (preset) return resolveTuning({ name: preset }, undefined, r.key);
|
|
299
|
+
return resolveTuning(score.tuning, track.tuning, score.key);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The score the chord target reads: without the tuned track, the track
|
|
304
|
+
* it follows and every vocal track, so sung and guide pitches never count
|
|
305
|
+
* as chord tones.
|
|
306
|
+
*/
|
|
307
|
+
function chordSource(
|
|
308
|
+
score: TrackScore,
|
|
309
|
+
track: Track,
|
|
310
|
+
r: ResolvedAutotune,
|
|
311
|
+
): Readonly<{ score: TrackScore; excluded: readonly string[] }> {
|
|
312
|
+
const excluded = new Set<string>([track.id]);
|
|
313
|
+
if (r.from !== undefined) excluded.add(r.from);
|
|
314
|
+
for (const other of score.tracks)
|
|
315
|
+
if (other.instrument === "vocal" || other.autotune) excluded.add(other.id);
|
|
316
|
+
// chordTimeline skips muted tracks: mute the excluded ones in a copy.
|
|
317
|
+
const source = score.withTracks(
|
|
318
|
+
score.tracks.map((other) =>
|
|
319
|
+
excluded.has(other.id) ? { ...other, muted: true } : other,
|
|
320
|
+
),
|
|
321
|
+
);
|
|
322
|
+
return { score: source, excluded: [...excluded].sort() };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Plans per score, by track and settings (a note-on reuses its plan). */
|
|
326
|
+
const plans = new WeakMap<TrackScore, Map<string, TargetPlan>>();
|
|
327
|
+
|
|
328
|
+
function plan(
|
|
329
|
+
score: TrackScore,
|
|
330
|
+
track: Track,
|
|
331
|
+
r: ResolvedAutotune,
|
|
332
|
+
): TargetPlan {
|
|
333
|
+
let known = plans.get(score);
|
|
334
|
+
if (!known) plans.set(score, (known = new Map()));
|
|
335
|
+
const memo = `${track.id}|${autotuneDigest(r)}|${JSON.stringify(track.tuning ?? null)}`;
|
|
336
|
+
const hit = known.get(memo);
|
|
337
|
+
if (hit) return hit;
|
|
338
|
+
const made = buildPlan(score, track, r);
|
|
339
|
+
known.set(memo, made);
|
|
340
|
+
return made;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function buildPlan(
|
|
344
|
+
score: TrackScore,
|
|
345
|
+
track: Track,
|
|
346
|
+
r: ResolvedAutotune,
|
|
347
|
+
): TargetPlan {
|
|
348
|
+
const table = tableFor(score, track, r);
|
|
349
|
+
if (r.to === "chromatic")
|
|
350
|
+
return {
|
|
351
|
+
kind: "static",
|
|
352
|
+
digest: `chromatic:${tableDigest(table)}`,
|
|
353
|
+
targets: { kind: "grid", grid: chromaticGrid(table) },
|
|
354
|
+
};
|
|
355
|
+
if (r.to === "scale") {
|
|
356
|
+
const keyText = r.key ?? score.key;
|
|
357
|
+
return {
|
|
358
|
+
kind: "static",
|
|
359
|
+
digest: `scale:${keyText ?? ""}:${tableDigest(table)}`,
|
|
360
|
+
targets: { kind: "grid", grid: scaleGrid(keyText, table) },
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (r.to === "chord") {
|
|
364
|
+
const source = chordSource(score, track, r);
|
|
365
|
+
const timeline = chordTimeline(source.score);
|
|
366
|
+
const fallback = scaleGrid(r.key ?? score.key, table);
|
|
367
|
+
const grids = new Map<string, ReturnType<typeof classGrid>>();
|
|
368
|
+
return {
|
|
369
|
+
kind: "timed",
|
|
370
|
+
digest: `chord:${fnv(`${source.excluded.join(",")}|${chordDigest(timeline)}`)}:${r.key ?? score.key ?? ""}:${tableDigest(table)}`,
|
|
371
|
+
build: (p) => ({
|
|
372
|
+
kind: "grid",
|
|
373
|
+
grid: (seconds: number) => {
|
|
374
|
+
const song = p.start + (seconds - p.offset) / p.rate;
|
|
375
|
+
const classes = chordAt(timeline, tickAtSeconds(score, song));
|
|
376
|
+
if (classes.length === 0) return fallback;
|
|
377
|
+
const id = classes.join(".");
|
|
378
|
+
let grid = grids.get(id);
|
|
379
|
+
if (!grid) grids.set(id, (grid = classGrid(classes, table)));
|
|
380
|
+
return grid;
|
|
381
|
+
},
|
|
382
|
+
}),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
const guide = guideTrack(score, track, r);
|
|
386
|
+
const notes = guide ? guideNotes(score, guide) : [];
|
|
387
|
+
return {
|
|
388
|
+
kind: "timed",
|
|
389
|
+
digest: guideDigest(notes),
|
|
390
|
+
build: (p) => {
|
|
391
|
+
// Only the notes that reach the buffer's span (song seconds).
|
|
392
|
+
const out: GuideNote[] = [];
|
|
393
|
+
for (const n of notes)
|
|
394
|
+
out.push({
|
|
395
|
+
...n,
|
|
396
|
+
start: p.offset + (n.start - p.start) * p.rate,
|
|
397
|
+
end: p.offset + (n.end - p.start) * p.rate,
|
|
398
|
+
});
|
|
399
|
+
return { kind: "notes", notes: out };
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Stem-key digests for a track with autotune: the engine version and the
|
|
406
|
+
* targets it reads (guide notes, chord timeline, key and tuning). Track
|
|
407
|
+
* settings already join the key with the track; clip offsets, latency and
|
|
408
|
+
* nudge join through the clips' own digests (`autotuneClipDigest`).
|
|
409
|
+
*/
|
|
410
|
+
export function autotuneStemDigests(
|
|
411
|
+
track: Track,
|
|
412
|
+
score: TrackScore,
|
|
413
|
+
): readonly string[] {
|
|
414
|
+
if (!track.autotune) return [];
|
|
415
|
+
const r = resolveAutotune(track.autotune);
|
|
416
|
+
return [
|
|
417
|
+
`autotune:${engine?.version ?? "none"}`,
|
|
418
|
+
plan(score, track, r).digest,
|
|
419
|
+
];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** A curve with every voiced f0 times `rate` (the pitch it sounds at). */
|
|
423
|
+
function atRate(curve: AutotuneCurve, rate: number): AutotuneCurve {
|
|
424
|
+
if (rate === 1) return curve;
|
|
425
|
+
const f0 = new Float32Array(curve.f0.length);
|
|
426
|
+
for (let f = 0; f < f0.length; f += 1) {
|
|
427
|
+
const hz = curve.f0[f]!;
|
|
428
|
+
f0[f] = hz > 0 ? hz * rate : 0;
|
|
429
|
+
}
|
|
430
|
+
return { ...curve, f0 };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** The frames of `curve` covering buffer seconds [from, to), re-based to `from`. */
|
|
434
|
+
export function sliceCurve(
|
|
435
|
+
curve: AutotuneCurve,
|
|
436
|
+
from: number,
|
|
437
|
+
to: number,
|
|
438
|
+
): AutotuneCurve {
|
|
439
|
+
const n = curve.f0.length;
|
|
440
|
+
const a = Math.max(0, Math.floor((from - curve.t0) / curve.hop) - 1);
|
|
441
|
+
const b = Math.min(n, Math.ceil((to - curve.t0) / curve.hop) + 2);
|
|
442
|
+
if (a === 0 && b === n && from === 0) return curve;
|
|
443
|
+
const take = (x: ArrayLike<number>) =>
|
|
444
|
+
Array.prototype.slice.call(x, a, Math.max(a, b)) as number[];
|
|
445
|
+
return {
|
|
446
|
+
t0: curve.t0 + a * curve.hop - from,
|
|
447
|
+
hop: curve.hop,
|
|
448
|
+
f0: Float32Array.from(take(curve.f0)),
|
|
449
|
+
prob: Uint8Array.from(take(curve.prob)),
|
|
450
|
+
aperiodic: Uint8Array.from(take(curve.aperiodic)),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Buffer frames [from, to) a voice reads (timed targets tune only these). */
|
|
455
|
+
export type Span = Readonly<{ from: number; to: number }>;
|
|
456
|
+
|
|
457
|
+
function untuned(buffer: TunableBuffer): TunedAudio {
|
|
458
|
+
return {
|
|
459
|
+
mono: buffer.mono,
|
|
460
|
+
...(buffer.left ? { left: buffer.left } : {}),
|
|
461
|
+
...(buffer.right ? { right: buffer.right } : {}),
|
|
462
|
+
from: 0,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* The tuned copy of `buffer` (or of its `span` for timed targets), or the
|
|
468
|
+
* buffer itself when nothing moves, there is no engine, the curve is not
|
|
469
|
+
* ready, or a live job is still running.
|
|
470
|
+
*/
|
|
471
|
+
export function autotuneSpan(
|
|
472
|
+
score: TrackScore,
|
|
473
|
+
track: Track,
|
|
474
|
+
buffer: TunableBuffer,
|
|
475
|
+
placement: Placement,
|
|
476
|
+
extraKey = "",
|
|
477
|
+
span?: Span,
|
|
478
|
+
): TunedAudio {
|
|
479
|
+
const settings = track.autotune;
|
|
480
|
+
const pitch = engine;
|
|
481
|
+
if (!settings || !pitch) return untuned(buffer);
|
|
482
|
+
const r = resolveAutotune(settings);
|
|
483
|
+
const targetPlan = plan(score, track, r);
|
|
484
|
+
const timed = targetPlan.kind === "timed";
|
|
485
|
+
const sr = buffer.sampleRate;
|
|
486
|
+
const length = buffer.mono.length;
|
|
487
|
+
// Timed targets differ per placement, so each voice tunes only what it
|
|
488
|
+
// reads (plus a margin); static targets share one whole-buffer result.
|
|
489
|
+
const margin = Math.round(AUTOTUNE_SPAN_MARGIN_SECONDS * sr);
|
|
490
|
+
const from = timed && span ? Math.max(0, Math.floor(span.from) - margin) : 0;
|
|
491
|
+
const to =
|
|
492
|
+
timed && span ? Math.min(length, Math.ceil(span.to) + margin) : length;
|
|
493
|
+
if (to <= from) return untuned(buffer);
|
|
494
|
+
const key = [
|
|
495
|
+
"at",
|
|
496
|
+
bufferId(buffer),
|
|
497
|
+
sr,
|
|
498
|
+
buffer.left && buffer.right ? "st" : "mo",
|
|
499
|
+
placement.rate.toFixed(9),
|
|
500
|
+
timed
|
|
501
|
+
? `${placement.start.toFixed(6)}:${placement.offset.toFixed(6)}:${from}-${to}`
|
|
502
|
+
: "",
|
|
503
|
+
pitch.version,
|
|
504
|
+
autotuneDigest(r),
|
|
505
|
+
targetPlan.digest,
|
|
506
|
+
extraKey,
|
|
507
|
+
].join("|");
|
|
508
|
+
const hit = cache.get(key);
|
|
509
|
+
if (hit) {
|
|
510
|
+
cache.delete(key);
|
|
511
|
+
cache.set(key, hit);
|
|
512
|
+
counters.hits += 1;
|
|
513
|
+
return hit;
|
|
514
|
+
}
|
|
515
|
+
counters.misses += 1;
|
|
516
|
+
const live = liveFitActive();
|
|
517
|
+
// Live, the engine analyses in the background: untuned until ready.
|
|
518
|
+
const whole = pitch.curve(buffer, r.voice, live ? { defer: true } : {});
|
|
519
|
+
if (!whole) return untuned(buffer);
|
|
520
|
+
const curve = atRate(sliceCurve(whole, from / sr, to / sr), placement.rate);
|
|
521
|
+
const local: Placement = {
|
|
522
|
+
...placement,
|
|
523
|
+
offset: placement.offset - from / sr,
|
|
524
|
+
};
|
|
525
|
+
const targets = timed ? targetPlan.build(local) : targetPlan.targets;
|
|
526
|
+
const cut = (x: Float32Array) =>
|
|
527
|
+
from === 0 && to === length ? x : x.subarray(from, to);
|
|
528
|
+
const job = function* (): Generator<void, TunedAudio> {
|
|
529
|
+
counters.tuned += 1;
|
|
530
|
+
const run = (x: Float32Array) =>
|
|
531
|
+
autotuneJob(
|
|
532
|
+
cut(x),
|
|
533
|
+
sr,
|
|
534
|
+
curve,
|
|
535
|
+
targets,
|
|
536
|
+
r,
|
|
537
|
+
pitch.psola,
|
|
538
|
+
12,
|
|
539
|
+
pitch.psolaJob,
|
|
540
|
+
);
|
|
541
|
+
const source = cut(buffer.mono);
|
|
542
|
+
const mono = yield* autotuneJob(
|
|
543
|
+
source,
|
|
544
|
+
sr,
|
|
545
|
+
curve,
|
|
546
|
+
targets,
|
|
547
|
+
r,
|
|
548
|
+
pitch.psola,
|
|
549
|
+
12,
|
|
550
|
+
pitch.psolaJob,
|
|
551
|
+
);
|
|
552
|
+
if (mono === source) {
|
|
553
|
+
// nothing moves: the whole buffer plays as it is
|
|
554
|
+
return untuned(buffer);
|
|
555
|
+
}
|
|
556
|
+
const left = buffer.left ? yield* run(buffer.left) : undefined;
|
|
557
|
+
const right = buffer.right ? yield* run(buffer.right) : undefined;
|
|
558
|
+
return {
|
|
559
|
+
mono,
|
|
560
|
+
...(left && right ? { left, right } : {}),
|
|
561
|
+
from,
|
|
562
|
+
};
|
|
563
|
+
};
|
|
564
|
+
if (live && to - from > LIVE_SYNC_AUTOTUNE_SECONDS * sr) {
|
|
565
|
+
// Until it lands the voice plays untuned, like a long 0.6.1 shift.
|
|
566
|
+
deferLiveJob(key, job, (out) => remember(key, out));
|
|
567
|
+
return untuned(buffer);
|
|
568
|
+
}
|
|
569
|
+
const steps = job();
|
|
570
|
+
for (;;) {
|
|
571
|
+
const step = steps.next();
|
|
572
|
+
if (step.done) {
|
|
573
|
+
remember(key, step.value);
|
|
574
|
+
return step.value;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Sampler hook: the voice's played buffer (after fit and shift) tuned at
|
|
581
|
+
* the pitch and place it sounds. `rate` is the voice's source frames per
|
|
582
|
+
* output second over the source rate; `regionStart` in source frames;
|
|
583
|
+
* `span` the source frames the voice can read.
|
|
584
|
+
*/
|
|
585
|
+
export function autotuneVoice(
|
|
586
|
+
score: TrackScore,
|
|
587
|
+
track: Track,
|
|
588
|
+
buffer: TunableBuffer,
|
|
589
|
+
startSeconds: number,
|
|
590
|
+
regionStart: number,
|
|
591
|
+
rate: number,
|
|
592
|
+
span?: Span,
|
|
593
|
+
): TunedAudio {
|
|
594
|
+
return autotuneSpan(
|
|
595
|
+
score,
|
|
596
|
+
track,
|
|
597
|
+
buffer,
|
|
598
|
+
{
|
|
599
|
+
start: startSeconds,
|
|
600
|
+
offset: regionStart / buffer.sampleRate,
|
|
601
|
+
rate,
|
|
602
|
+
},
|
|
603
|
+
"",
|
|
604
|
+
span,
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** The key part of a clip's placement (offset, latency and nudge). */
|
|
609
|
+
export function autotuneClipDigest(
|
|
610
|
+
offsetSeconds: number,
|
|
611
|
+
latencySeconds = 0,
|
|
612
|
+
nudgeSeconds = 0,
|
|
613
|
+
): string {
|
|
614
|
+
return `o${offsetSeconds.toFixed(6)}l${latencySeconds.toFixed(6)}n${nudgeSeconds.toFixed(6)}`;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Clip hook (for src/audio/clips.ts `renderClips`, per clip): `buffer` is
|
|
619
|
+
* the clip's audio and `startSeconds` the song second its clip offset
|
|
620
|
+
* sounds at, after latency and nudge (`startTick` seconds + latency +
|
|
621
|
+
* nudge). Offset, latency and nudge join the key.
|
|
622
|
+
*/
|
|
623
|
+
export function autotuneClip(
|
|
624
|
+
score: TrackScore,
|
|
625
|
+
track: Track,
|
|
626
|
+
buffer: TunableBuffer,
|
|
627
|
+
startSeconds: number,
|
|
628
|
+
offsetSeconds: number,
|
|
629
|
+
clipKey = "",
|
|
630
|
+
latencySeconds = 0,
|
|
631
|
+
nudgeSeconds = 0,
|
|
632
|
+
): TunedAudio {
|
|
633
|
+
return autotuneSpan(
|
|
634
|
+
score,
|
|
635
|
+
track,
|
|
636
|
+
buffer,
|
|
637
|
+
{ start: startSeconds, offset: offsetSeconds, rate: 1 },
|
|
638
|
+
`clip:${clipKey}:${autotuneClipDigest(offsetSeconds, latencySeconds, nudgeSeconds)}`,
|
|
639
|
+
);
|
|
640
|
+
}
|