@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,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio clips (0.7): files placed on a track's timeline, summed into the
|
|
3
|
+
* track's dry buffer before its effect chain (sources.md section 7). The
|
|
4
|
+
* pure placement and section cuts live in core/clips.ts; this module reads
|
|
5
|
+
* the decoded audio, places it through the tempo map, resamples it to the
|
|
6
|
+
* render rate (4-point Hermite), applies the take's clock-drift stretch,
|
|
7
|
+
* reverse, gain and equal-power fades, and multiplies by the track's
|
|
8
|
+
* volume the way every voice does. A clip whose audio did not load renders
|
|
9
|
+
* silence (the loader reported it as a warning).
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
CLIP_VOICE_PREFIX,
|
|
13
|
+
DEFAULT_CLIP_FADE,
|
|
14
|
+
clipSongTick,
|
|
15
|
+
resolveClipLengths,
|
|
16
|
+
} from "../../core/clips.ts";
|
|
17
|
+
import type { AudioClip, Take, Track, TrackScore } from "../../core/score.ts";
|
|
18
|
+
import { autotuneClip } from "./autotune.ts";
|
|
19
|
+
import { sampleKey, type DecodedSample, type SampleBank } from "./samples.ts";
|
|
20
|
+
import type { SampleWarp } from "./warp.ts";
|
|
21
|
+
|
|
22
|
+
/** What clip rendering reads of the render context. */
|
|
23
|
+
export type ClipContext = Readonly<{
|
|
24
|
+
sampleRate: number;
|
|
25
|
+
samples: number;
|
|
26
|
+
samplesPerTick: number;
|
|
27
|
+
warp?: SampleWarp;
|
|
28
|
+
}>;
|
|
29
|
+
|
|
30
|
+
/** True when a track has clips that sound. */
|
|
31
|
+
export function hasClips(track: Track | undefined): boolean {
|
|
32
|
+
return track?.clips?.some((clip) => !clip.mute) ?? false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The decoded audio of a clip, if it loaded. */
|
|
36
|
+
export function clipAudio(
|
|
37
|
+
bank: SampleBank | undefined,
|
|
38
|
+
trackId: string,
|
|
39
|
+
clip: AudioClip,
|
|
40
|
+
): DecodedSample | undefined {
|
|
41
|
+
if (!bank) return undefined;
|
|
42
|
+
const exact = bank.voices.get(
|
|
43
|
+
sampleKey(trackId, `${CLIP_VOICE_PREFIX}${clip.id}`),
|
|
44
|
+
);
|
|
45
|
+
if (exact) return exact;
|
|
46
|
+
// A section or form pass of a clip is `<id>~<n>`: it plays the audio
|
|
47
|
+
// the bank loaded for `<id>` from the unsliced score.
|
|
48
|
+
const pass = clip.id.lastIndexOf("~");
|
|
49
|
+
return pass > 0
|
|
50
|
+
? bank.voices.get(
|
|
51
|
+
sampleKey(trackId, `${CLIP_VOICE_PREFIX}${clip.id.slice(0, pass)}`),
|
|
52
|
+
)
|
|
53
|
+
: undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* `score` with each loaded clip's missing `dur` resolved from its file
|
|
58
|
+
* (`resolveClipLengths`), so section, form and window cuts see its real
|
|
59
|
+
* end. A score without clips, or without a bank, comes back as it is.
|
|
60
|
+
*/
|
|
61
|
+
export function withClipLengths(
|
|
62
|
+
score: TrackScore,
|
|
63
|
+
bank: SampleBank | undefined,
|
|
64
|
+
): TrackScore {
|
|
65
|
+
if (!bank || !score.tracks.some((track) => track.clips)) return score;
|
|
66
|
+
return resolveClipLengths(score, (trackId, clip) => {
|
|
67
|
+
const audio = clipAudio(bank, trackId, clip);
|
|
68
|
+
return audio && audio.frames > 0
|
|
69
|
+
? audio.frames / audio.sampleRate
|
|
70
|
+
: undefined;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Fractional render sample where a score tick sounds. */
|
|
75
|
+
function sampleAt(context: ClipContext, tick: number): number {
|
|
76
|
+
return context.warp
|
|
77
|
+
? context.warp.sample(tick)
|
|
78
|
+
: tick * context.samplesPerTick;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The take a clip plays from, if any. */
|
|
82
|
+
function takeOf(track: Track, clip: AudioClip): Take | undefined {
|
|
83
|
+
if (clip.take === undefined) return undefined;
|
|
84
|
+
return track.takes?.find((take) => take.name === clip.take);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Catmull-Rom (4-point Hermite) read at a fractional frame, 0 outside. */
|
|
88
|
+
function readHermite(data: Float32Array, position: number): number {
|
|
89
|
+
const base = Math.floor(position);
|
|
90
|
+
const t = position - base;
|
|
91
|
+
const at = (index: number) =>
|
|
92
|
+
index >= 0 && index < data.length ? data[index]! : 0;
|
|
93
|
+
const y0 = at(base - 1);
|
|
94
|
+
const y1 = at(base);
|
|
95
|
+
const y2 = at(base + 1);
|
|
96
|
+
const y3 = at(base + 2);
|
|
97
|
+
const c1 = 0.5 * (y2 - y0);
|
|
98
|
+
const c2 = y0 - 2.5 * y1 + 2 * y2 - 0.5 * y3;
|
|
99
|
+
const c3 = 0.5 * (y3 - y0) + 1.5 * (y1 - y2);
|
|
100
|
+
return ((c3 * t + c2) * t + c1) * t + y1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Equal-power fade gain for `x` in 0..1 (sin of a quarter turn). */
|
|
104
|
+
export function equalPowerFade(x: number): number {
|
|
105
|
+
if (x <= 0) return 0;
|
|
106
|
+
if (x >= 1) return 1;
|
|
107
|
+
return Math.sin((x * Math.PI) / 2);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Sum `track`'s clips into `target` (the mono dry buffer). `gainAt(tick)`
|
|
112
|
+
* is the track volume with automation; `tickAt(sample)` maps a render
|
|
113
|
+
* sample back to a score tick for it. Returns the clips that did not
|
|
114
|
+
* load (they stay silent). With `score` and a `Track.autotune` (0.7), each
|
|
115
|
+
* clip plays retuned (`autotuneClip`, keyed by its offset and nudge);
|
|
116
|
+
* reversed clips play untuned.
|
|
117
|
+
*/
|
|
118
|
+
export function renderClips(
|
|
119
|
+
target: Float64Array,
|
|
120
|
+
track: Track,
|
|
121
|
+
context: ClipContext,
|
|
122
|
+
bank: SampleBank | undefined,
|
|
123
|
+
gainAt: (tick: number) => number,
|
|
124
|
+
score?: TrackScore,
|
|
125
|
+
): string[] {
|
|
126
|
+
const missing: string[] = [];
|
|
127
|
+
if (!track.clips) return missing;
|
|
128
|
+
const { sampleRate, samples } = context;
|
|
129
|
+
const tickAt = (index: number) =>
|
|
130
|
+
context.warp ? context.warp.tick(index) : index / context.samplesPerTick;
|
|
131
|
+
for (const clip of track.clips) {
|
|
132
|
+
if (clip.mute) continue;
|
|
133
|
+
const audio = clipAudio(bank, track.id, clip);
|
|
134
|
+
if (!audio || audio.frames === 0) {
|
|
135
|
+
missing.push(clip.id);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const take = takeOf(track, clip);
|
|
139
|
+
// A take recorded on a drifting clock (`ppm`) plays slightly faster or
|
|
140
|
+
// slower so it stays on the grid; `nudge` moves it in milliseconds.
|
|
141
|
+
const drift = 1 + (take?.ppm ?? 0) / 1e6;
|
|
142
|
+
const nudge = (take?.nudge ?? 0) / 1000;
|
|
143
|
+
const fileSeconds = audio.frames / audio.sampleRate;
|
|
144
|
+
const offset = Math.min(fileSeconds, clip.offset ?? 0);
|
|
145
|
+
const length = Math.max(
|
|
146
|
+
0,
|
|
147
|
+
Math.min(
|
|
148
|
+
clip.dur ?? Number.POSITIVE_INFINITY,
|
|
149
|
+
(fileSeconds - offset) / drift,
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
if (length <= 0) continue;
|
|
153
|
+
const start =
|
|
154
|
+
sampleAt(context, clipSongTick(clip, track.time)) + nudge * sampleRate;
|
|
155
|
+
const total = length * sampleRate;
|
|
156
|
+
const first = Math.max(0, Math.ceil(start));
|
|
157
|
+
const last = Math.min(samples, Math.ceil(start + total));
|
|
158
|
+
if (first >= last) continue;
|
|
159
|
+
const step = (audio.sampleRate / sampleRate) * drift;
|
|
160
|
+
const begin = offset * audio.sampleRate;
|
|
161
|
+
const span = length * audio.sampleRate * drift;
|
|
162
|
+
const fadeIn = Math.min(clip.fadeInTime ?? DEFAULT_CLIP_FADE, length / 2);
|
|
163
|
+
const fadeOut = Math.min(clip.fadeTime ?? DEFAULT_CLIP_FADE, length / 2);
|
|
164
|
+
const fadeInFrames = fadeIn * sampleRate;
|
|
165
|
+
const fadeOutFrames = fadeOut * sampleRate;
|
|
166
|
+
const gain = clip.gain ?? 1;
|
|
167
|
+
// 0.7 autotune: the clip's audio retuned at the song second its
|
|
168
|
+
// offset sounds (`start` already holds the nudge).
|
|
169
|
+
const tuned =
|
|
170
|
+
track.autotune && score && !clip.rev
|
|
171
|
+
? autotuneClip(
|
|
172
|
+
score,
|
|
173
|
+
track,
|
|
174
|
+
{
|
|
175
|
+
sha256: audio.sha256,
|
|
176
|
+
sampleRate: audio.sampleRate,
|
|
177
|
+
mono: audio.mono,
|
|
178
|
+
},
|
|
179
|
+
start / sampleRate,
|
|
180
|
+
offset,
|
|
181
|
+
clip.id,
|
|
182
|
+
0,
|
|
183
|
+
nudge,
|
|
184
|
+
)
|
|
185
|
+
: undefined;
|
|
186
|
+
const data = tuned ? tuned.mono : audio.mono;
|
|
187
|
+
const shiftFrom = tuned ? tuned.from : 0;
|
|
188
|
+
// Track volume (with automation) is read once per 32-sample block.
|
|
189
|
+
let blockGain = 0;
|
|
190
|
+
for (let index = first; index < last; index += 1) {
|
|
191
|
+
if ((index - first) % 32 === 0) blockGain = gainAt(tickAt(index));
|
|
192
|
+
const elapsed = index - start;
|
|
193
|
+
let position = elapsed * step;
|
|
194
|
+
if (position >= span) break;
|
|
195
|
+
position = clip.rev ? begin + span - 1 - position : begin + position;
|
|
196
|
+
let shape = gain * blockGain;
|
|
197
|
+
if (fadeInFrames > 0 && elapsed < fadeInFrames)
|
|
198
|
+
shape *= equalPowerFade(elapsed / fadeInFrames);
|
|
199
|
+
const remaining = total - elapsed;
|
|
200
|
+
if (fadeOutFrames > 0 && remaining < fadeOutFrames)
|
|
201
|
+
shape *= equalPowerFade(remaining / fadeOutFrames);
|
|
202
|
+
target[index]! += readHermite(data, position - shiftFrom) * shape;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return missing;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Stem-cache key part for a track's clips: each clip's sha256 (the one
|
|
210
|
+
* loaded, so an edited file re-renders), placement, cut, gain, fades and
|
|
211
|
+
* reverse, and the drift of the take it plays from.
|
|
212
|
+
*/
|
|
213
|
+
export function clipsDigest(
|
|
214
|
+
track: Track,
|
|
215
|
+
bank: SampleBank | undefined,
|
|
216
|
+
guide = false,
|
|
217
|
+
): string | undefined {
|
|
218
|
+
if (!track.clips || track.clips.length === 0) return undefined;
|
|
219
|
+
return (
|
|
220
|
+
(guide ? "guide|" : "") +
|
|
221
|
+
track.clips
|
|
222
|
+
.map((clip) => {
|
|
223
|
+
const take = takeOf(track, clip);
|
|
224
|
+
return [
|
|
225
|
+
clip.id,
|
|
226
|
+
clipAudio(bank, track.id, clip)?.sha256 ?? `missing:${clip.sha256}`,
|
|
227
|
+
clip.startTick,
|
|
228
|
+
clip.offset ?? "",
|
|
229
|
+
clip.dur ?? "",
|
|
230
|
+
clip.gain ?? "",
|
|
231
|
+
clip.fadeInTime ?? "",
|
|
232
|
+
clip.fadeTime ?? "",
|
|
233
|
+
clip.rev ? "r" : "",
|
|
234
|
+
clip.mute ? "m" : "",
|
|
235
|
+
take ? `${take.ppm ?? 0}/${take.nudge ?? 0}` : "",
|
|
236
|
+
].join(",");
|
|
237
|
+
})
|
|
238
|
+
.join(";")
|
|
239
|
+
);
|
|
240
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dawg doctor`: how dawg makes sound on this machine. The backend and why
|
|
3
|
+
* it was chosen, the native sink's verification (or why it fell back), the
|
|
4
|
+
* audio devices, and the play-mode lead.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
AudioEngine,
|
|
8
|
+
detectAudioBackend,
|
|
9
|
+
type AudioBackendInfo,
|
|
10
|
+
} from "./engine.ts";
|
|
11
|
+
import { PREBUILT_DIR, nativeTarget } from "./native.ts";
|
|
12
|
+
import { DEFAULT_SAMPLE_RATE } from "./wav.ts";
|
|
13
|
+
|
|
14
|
+
export type DoctorReport = Readonly<{
|
|
15
|
+
backend: AudioBackendInfo["backend"];
|
|
16
|
+
detail: string;
|
|
17
|
+
native: Readonly<{
|
|
18
|
+
target: string | null;
|
|
19
|
+
dir: string;
|
|
20
|
+
loaded: boolean;
|
|
21
|
+
reason?: string;
|
|
22
|
+
}>;
|
|
23
|
+
playLeadMs: number;
|
|
24
|
+
outputs: readonly string[];
|
|
25
|
+
inputs: readonly string[];
|
|
26
|
+
}>;
|
|
27
|
+
|
|
28
|
+
export function audioDoctor(info = detectAudioBackend()): DoctorReport {
|
|
29
|
+
const engine = new AudioEngine({ info, worker: false, timer: false });
|
|
30
|
+
const playLeadMs = engine.playLeadMs;
|
|
31
|
+
void engine.dispose();
|
|
32
|
+
const list = (input: boolean) =>
|
|
33
|
+
info.native
|
|
34
|
+
?.devices(input)
|
|
35
|
+
.map(
|
|
36
|
+
(d) =>
|
|
37
|
+
`${d.name}${d.default ? " (default)" : ""} · ${d.channels} ch · ${d.rate} Hz`,
|
|
38
|
+
) ?? [];
|
|
39
|
+
return {
|
|
40
|
+
backend: info.backend,
|
|
41
|
+
detail: info.detail,
|
|
42
|
+
native: {
|
|
43
|
+
target: nativeTarget() ?? null,
|
|
44
|
+
dir: PREBUILT_DIR,
|
|
45
|
+
loaded: info.backend === "native",
|
|
46
|
+
...(info.nativeUnavailable ? { reason: info.nativeUnavailable } : {}),
|
|
47
|
+
},
|
|
48
|
+
playLeadMs,
|
|
49
|
+
outputs: list(false),
|
|
50
|
+
inputs: list(true),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function formatAudioDoctor(report: DoctorReport): string[] {
|
|
55
|
+
const lines = [
|
|
56
|
+
`audio: ${report.backend} · ${report.detail}`,
|
|
57
|
+
report.native.loaded
|
|
58
|
+
? `native sink: loaded (${report.native.target})`
|
|
59
|
+
: `native sink: not in use · ${report.native.reason ?? "another backend was forced"} · falls back to ffplay, sox or afplay`,
|
|
60
|
+
`play lead: ${report.playLeadMs} ms · render rate ${DEFAULT_SAMPLE_RATE} Hz`,
|
|
61
|
+
];
|
|
62
|
+
if (report.outputs.length > 0)
|
|
63
|
+
lines.push("outputs:", ...report.outputs.map((d) => ` ${d}`));
|
|
64
|
+
if (report.inputs.length > 0)
|
|
65
|
+
lines.push("inputs:", ...report.inputs.map((d) => ` ${d}`));
|
|
66
|
+
return lines;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function runDoctorCommand(
|
|
70
|
+
args: readonly string[],
|
|
71
|
+
stdout: { write(text: string): unknown },
|
|
72
|
+
): Promise<number> {
|
|
73
|
+
if (args.some((arg) => arg === "--help" || arg === "-h")) {
|
|
74
|
+
stdout.write(
|
|
75
|
+
"usage: dawg doctor [--json] audio backend, native sink, devices\n",
|
|
76
|
+
);
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
const report = audioDoctor();
|
|
80
|
+
stdout.write(
|
|
81
|
+
args.includes("--json")
|
|
82
|
+
? `${JSON.stringify(report, null, 2)}\n`
|
|
83
|
+
: `${formatAudioDoctor(report).join("\n")}\n`,
|
|
84
|
+
);
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Band-pass banks (0.7 vocoder): RBJ biquads, their group delay and a
|
|
3
|
+
* sample-rate-independent band layout. Band centres and widths depend only
|
|
4
|
+
* on the band count, range and width, never on the sample rate: a band whose
|
|
5
|
+
* centre lies above 0.45 sr is muted instead, so a 22 050 Hz audition and a
|
|
6
|
+
* 48 kHz export have the same layout.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type Biquad = Readonly<{
|
|
10
|
+
b0: number;
|
|
11
|
+
b1: number;
|
|
12
|
+
b2: number;
|
|
13
|
+
a1: number;
|
|
14
|
+
a2: number;
|
|
15
|
+
}>;
|
|
16
|
+
|
|
17
|
+
/** RBJ constant-0-dB-peak band-pass, bandwidth in octaves, pre-warped. */
|
|
18
|
+
export function bandpass(
|
|
19
|
+
hz: number,
|
|
20
|
+
octaves: number,
|
|
21
|
+
sampleRate: number,
|
|
22
|
+
): Biquad {
|
|
23
|
+
const w = (2 * Math.PI * hz) / sampleRate;
|
|
24
|
+
const s = Math.sin(w);
|
|
25
|
+
const alpha = s * Math.sinh(((Math.LN2 / 2) * octaves * w) / s);
|
|
26
|
+
const a0 = 1 + alpha;
|
|
27
|
+
return {
|
|
28
|
+
b0: alpha / a0,
|
|
29
|
+
b1: 0,
|
|
30
|
+
b2: -alpha / a0,
|
|
31
|
+
a1: (-2 * Math.cos(w)) / a0,
|
|
32
|
+
a2: (1 - alpha) / a0,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** RBJ Butterworth high-pass (Q = 1/sqrt 2). */
|
|
37
|
+
export function highpass(hz: number, sampleRate: number): Biquad {
|
|
38
|
+
const w = (2 * Math.PI * hz) / sampleRate;
|
|
39
|
+
const alpha = Math.sin(w) / (2 * Math.SQRT1_2);
|
|
40
|
+
const c = Math.cos(w);
|
|
41
|
+
const a0 = 1 + alpha;
|
|
42
|
+
return {
|
|
43
|
+
b0: (1 + c) / 2 / a0,
|
|
44
|
+
b1: -(1 + c) / a0,
|
|
45
|
+
b2: (1 + c) / 2 / a0,
|
|
46
|
+
a1: (-2 * c) / a0,
|
|
47
|
+
a2: (1 - alpha) / a0,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Group delay in samples of one biquad at angle `w` (numeric phase slope). */
|
|
52
|
+
export function groupDelay(f: Biquad, w: number): number {
|
|
53
|
+
const phase = (x: number): number => {
|
|
54
|
+
const nr = f.b0 + f.b1 * Math.cos(x) + f.b2 * Math.cos(2 * x);
|
|
55
|
+
const ni = -(f.b1 * Math.sin(x) + f.b2 * Math.sin(2 * x));
|
|
56
|
+
const dr = 1 + f.a1 * Math.cos(x) + f.a2 * Math.cos(2 * x);
|
|
57
|
+
const di = -(f.a1 * Math.sin(x) + f.a2 * Math.sin(2 * x));
|
|
58
|
+
return Math.atan2(ni, nr) - Math.atan2(di, dr);
|
|
59
|
+
};
|
|
60
|
+
const d = 1e-5;
|
|
61
|
+
let dp = phase(w + d) - phase(w - d);
|
|
62
|
+
while (dp > Math.PI) dp -= 2 * Math.PI;
|
|
63
|
+
while (dp < -Math.PI) dp += 2 * Math.PI;
|
|
64
|
+
return -dp / (2 * d);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Runs a biquad over `x` in place (transposed direct form II). */
|
|
68
|
+
export function runBiquad(f: Biquad, x: Float64Array): void {
|
|
69
|
+
let z1 = 0;
|
|
70
|
+
let z2 = 0;
|
|
71
|
+
const { b0, b1, b2, a1, a2 } = f;
|
|
72
|
+
for (let i = 0; i < x.length; i += 1) {
|
|
73
|
+
const v = x[i]!;
|
|
74
|
+
const y = b0 * v + z1;
|
|
75
|
+
z1 = b1 * v - a1 * y + z2;
|
|
76
|
+
z2 = b2 * v - a2 * y;
|
|
77
|
+
x[i] = y;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type BandLayout = Readonly<{
|
|
82
|
+
bands: number;
|
|
83
|
+
lo: number;
|
|
84
|
+
hi: number;
|
|
85
|
+
width: number;
|
|
86
|
+
}>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Log-spaced centres from `lo` to `hi`. Each of the two cascaded sections is
|
|
90
|
+
* about 1.55x wider than the target so the pair's -3 dB width matches
|
|
91
|
+
* spacing x width.
|
|
92
|
+
*/
|
|
93
|
+
export function bandLayout(p: BandLayout): {
|
|
94
|
+
centres: number[];
|
|
95
|
+
ratio: number;
|
|
96
|
+
octaves: number;
|
|
97
|
+
} {
|
|
98
|
+
const n = Math.max(2, Math.round(p.bands));
|
|
99
|
+
const ratio = (p.hi / p.lo) ** (1 / (n - 1));
|
|
100
|
+
const centres = Array.from({ length: n }, (_, k) => p.lo * ratio ** k);
|
|
101
|
+
return { centres, ratio, octaves: Math.log2(ratio) * p.width * 1.55 };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type BankBand = Readonly<{
|
|
105
|
+
hz: number;
|
|
106
|
+
live: boolean;
|
|
107
|
+
filter: Biquad;
|
|
108
|
+
/** Modulator advance: the pair's group delay at hz plus the attack. */
|
|
109
|
+
advance: number;
|
|
110
|
+
}>;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The bank at one sample rate. Each band's modulator advance cancels the
|
|
114
|
+
* analysis filters' group delay and the follower attack, so consonants land
|
|
115
|
+
* on the beat.
|
|
116
|
+
*/
|
|
117
|
+
export function bankDesign(
|
|
118
|
+
p: BandLayout & { attack?: number },
|
|
119
|
+
sampleRate: number,
|
|
120
|
+
): { bands: BankBand[]; ratio: number } {
|
|
121
|
+
const { centres, ratio, octaves } = bandLayout(p);
|
|
122
|
+
const bands = centres.map((hz) => {
|
|
123
|
+
const live = hz <= 0.45 * sampleRate;
|
|
124
|
+
const filter = bandpass(
|
|
125
|
+
Math.min(hz, 0.45 * sampleRate),
|
|
126
|
+
octaves,
|
|
127
|
+
sampleRate,
|
|
128
|
+
);
|
|
129
|
+
const advance = live
|
|
130
|
+
? Math.round(
|
|
131
|
+
2 * groupDelay(filter, (2 * Math.PI * hz) / sampleRate) +
|
|
132
|
+
(p.attack ?? 0) * sampleRate,
|
|
133
|
+
)
|
|
134
|
+
: 0;
|
|
135
|
+
return { hz, live, filter, advance };
|
|
136
|
+
});
|
|
137
|
+
return { bands, ratio };
|
|
138
|
+
}
|
|
@@ -93,6 +93,15 @@ export function cepstralEnvelope(
|
|
|
93
93
|
return out;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* A peak joins the envelope only within this factor (-9 dB) of the smooth
|
|
98
|
+
* cepstral envelope: harmonics sit on it, while window sidelobes and breath
|
|
99
|
+
* noise in the valleys between harmonics sit far below. Without this the
|
|
100
|
+
* envelope followed the source's harmonic comb, and `formant 0` imposed the
|
|
101
|
+
* old pitch on the shifted one (pitch.md 3.5).
|
|
102
|
+
*/
|
|
103
|
+
const HARMONIC_PEAK_FLOOR = 10 ** (-9 / 20);
|
|
104
|
+
|
|
96
105
|
/**
|
|
97
106
|
* Harmonic-peak envelope (0.6.1): log magnitude interpolated linearly
|
|
98
107
|
* between the spectral peaks (each refined by a parabola to its true bin
|
|
@@ -122,6 +131,7 @@ export function peakEnvelope(
|
|
|
122
131
|
const v = mag[k]!;
|
|
123
132
|
if (
|
|
124
133
|
v > floor &&
|
|
134
|
+
v >= smooth[k]! * HARMONIC_PEAK_FLOOR &&
|
|
125
135
|
v > mag[k - 1]! &&
|
|
126
136
|
v >= mag[k + 1]! &&
|
|
127
137
|
v > mag[k - 2]! &&
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Envelope followers (0.7 vocoder): one-pole attack/release on |x|, and the
|
|
3
|
+
* gate curve built on one. Pure functions of the input buffer, so a render
|
|
4
|
+
* window with enough pre-roll settles to the full render's values.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** One-pole coefficient for a time constant in seconds (0 = instant). */
|
|
8
|
+
export function followCoef(seconds: number, sampleRate: number): number {
|
|
9
|
+
return seconds <= 0 ? 0 : Math.exp(-1 / (seconds * sampleRate));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Follows |x| with separate attack and release times. Where `hold[i]` is
|
|
14
|
+
* non-zero the envelope keeps its last value (the vocoder's freeze).
|
|
15
|
+
*/
|
|
16
|
+
export function follow(
|
|
17
|
+
x: Float64Array,
|
|
18
|
+
attack: number,
|
|
19
|
+
release: number,
|
|
20
|
+
sampleRate: number,
|
|
21
|
+
hold?: Uint8Array,
|
|
22
|
+
): Float64Array {
|
|
23
|
+
const a = followCoef(attack, sampleRate);
|
|
24
|
+
const r = followCoef(release, sampleRate);
|
|
25
|
+
const out = new Float64Array(x.length);
|
|
26
|
+
let e = 0;
|
|
27
|
+
for (let i = 0; i < x.length; i += 1) {
|
|
28
|
+
if (!hold || hold[i] === 0) {
|
|
29
|
+
const v = Math.abs(x[i]!);
|
|
30
|
+
const c = v > e ? a : r;
|
|
31
|
+
e = c * e + (1 - c) * v;
|
|
32
|
+
}
|
|
33
|
+
out[i] = e;
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Gate gain 0..1 per sample: a broadband follower against `thresholdDb`
|
|
40
|
+
* with a 6 dB soft knee, smoothed over 5 ms. Undefined when the gate is off
|
|
41
|
+
* (threshold at or below -119 dBFS).
|
|
42
|
+
*/
|
|
43
|
+
export function gateCurve(
|
|
44
|
+
x: Float64Array,
|
|
45
|
+
sampleRate: number,
|
|
46
|
+
thresholdDb: number,
|
|
47
|
+
): Float64Array | undefined {
|
|
48
|
+
if (thresholdDb <= -119) return undefined;
|
|
49
|
+
const level = follow(x, 0.001, 0.05, sampleRate);
|
|
50
|
+
const out = new Float64Array(x.length);
|
|
51
|
+
const c = followCoef(0.005, sampleRate);
|
|
52
|
+
let s = 0;
|
|
53
|
+
for (let i = 0; i < x.length; i += 1) {
|
|
54
|
+
const db = 20 * Math.log10(level[i]! + 1e-12);
|
|
55
|
+
const t = Math.min(1, Math.max(0, (db - thresholdDb) / 6));
|
|
56
|
+
s = c * s + (1 - c) * t;
|
|
57
|
+
out[i] = s;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Where `hold[i]` is non-zero, `curve` keeps its value from the sample
|
|
64
|
+
* before (in place): a frozen vocoder keeps its gate where it was, so the
|
|
65
|
+
* held vowel does not fade out once the modulator goes quiet.
|
|
66
|
+
*/
|
|
67
|
+
export function holdCurve(curve: Float64Array, hold: Uint8Array): void {
|
|
68
|
+
for (let i = 1; i < curve.length && i < hold.length; i += 1)
|
|
69
|
+
if (hold[i] !== 0) curve[i] = curve[i - 1]!;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The static `freeze` hold: 1 wherever the modulator is quiet (under the
|
|
74
|
+
* gate, or under -50 dBFS when the gate is lower or off), engaged
|
|
75
|
+
* `lead` seconds before the voice falls quiet so the envelopes are caught
|
|
76
|
+
* before their release. The carrier keeps the last sung vowel through every
|
|
77
|
+
* rest and follows the voice while it sings. Before the first note it holds
|
|
78
|
+
* silence. Local (the mod span plus its lookahead), so windows agree.
|
|
79
|
+
*/
|
|
80
|
+
export function quietHold(
|
|
81
|
+
x: Float64Array,
|
|
82
|
+
n: number,
|
|
83
|
+
sampleRate: number,
|
|
84
|
+
thresholdDb: number,
|
|
85
|
+
lead = 0.03,
|
|
86
|
+
): Uint8Array {
|
|
87
|
+
const level = follow(x, 0.001, 0.01, sampleRate);
|
|
88
|
+
const floor = 10 ** (Math.max(thresholdDb, -50) / 20);
|
|
89
|
+
const ahead = Math.max(0, Math.round(lead * sampleRate));
|
|
90
|
+
const hold = new Uint8Array(n);
|
|
91
|
+
let nextQuiet = Infinity;
|
|
92
|
+
for (let i = Math.min(level.length, n + ahead) - 1; i >= 0; i -= 1) {
|
|
93
|
+
if (level[i]! < floor) {
|
|
94
|
+
// the start of a quiet run reaches back `ahead` samples
|
|
95
|
+
if (i === 0 || level[i - 1]! >= floor) nextQuiet = i;
|
|
96
|
+
if (i < n) hold[i] = 1;
|
|
97
|
+
} else if (i < n && nextQuiet - i <= ahead) hold[i] = 1;
|
|
98
|
+
}
|
|
99
|
+
for (let i = level.length; i < n; i += 1) hold[i] = 1;
|
|
100
|
+
return hold;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Auto gate threshold for a whole asset: the 10th-percentile 10 ms frame
|
|
105
|
+
* level plus 6 dB (the mean-abs follower reads about 1 dB under RMS). A
|
|
106
|
+
* property of the asset, so every render window agrees. -120 when silent.
|
|
107
|
+
*/
|
|
108
|
+
export function autoGateDb(asset: Float64Array, sampleRate: number): number {
|
|
109
|
+
const hop = Math.max(1, Math.round(0.01 * sampleRate));
|
|
110
|
+
const levels: number[] = [];
|
|
111
|
+
for (let s = 0; s + hop <= asset.length; s += hop) {
|
|
112
|
+
let e = 0;
|
|
113
|
+
for (let i = s; i < s + hop; i += 1) e += asset[i]! * asset[i]!;
|
|
114
|
+
const db = 10 * Math.log10(e / hop + 1e-24);
|
|
115
|
+
if (db > -100) levels.push(db);
|
|
116
|
+
}
|
|
117
|
+
if (levels.length === 0) return -120;
|
|
118
|
+
levels.sort((a, b) => a - b);
|
|
119
|
+
return levels[Math.floor(levels.length * 0.1)]! + 6;
|
|
120
|
+
}
|