@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
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
* playing a test tone silently.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { isGuideInstrument } from "../../core/clips.ts";
|
|
9
10
|
import { isDrumInstrument } from "../../core/drums.ts";
|
|
10
11
|
import { INSTRUMENT_WORDS, LEGACY_WORDS } from "../../core/instruments.ts";
|
|
11
12
|
import { KEYS_FAMILIES } from "../../core/keys.ts";
|
|
12
13
|
import { isSamplerInstrument, type Track } from "../../core/score.ts";
|
|
14
|
+
import { nearest } from "../commands/nearest.ts";
|
|
13
15
|
import { engineFor, registeredEngines } from "./instruments.ts";
|
|
14
16
|
import { resolveOscillator } from "./synth/oscillators.ts";
|
|
15
17
|
import { AVAILABLE_INSTRUMENTS } from "./wav.ts";
|
|
@@ -41,6 +43,24 @@ function knownWords(): readonly string[] {
|
|
|
41
43
|
];
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
/**
|
|
47
|
+
* `instrument list` rows: every exact instrument word, sorted, wrapped to
|
|
48
|
+
* `width` columns.
|
|
49
|
+
*/
|
|
50
|
+
export function instrumentListLines(width = 72): string[] {
|
|
51
|
+
const words = [...new Set(["sine", ...knownWords()])].sort();
|
|
52
|
+
const lines: string[] = [];
|
|
53
|
+
let line = "";
|
|
54
|
+
for (const word of words) {
|
|
55
|
+
if (line && line.length + 1 + word.length > width) {
|
|
56
|
+
lines.push(line);
|
|
57
|
+
line = word;
|
|
58
|
+
} else line = line ? `${line} ${word}` : word;
|
|
59
|
+
}
|
|
60
|
+
if (line) lines.push(line);
|
|
61
|
+
return lines;
|
|
62
|
+
}
|
|
63
|
+
|
|
44
64
|
/** Whether `instrument` (on `track`) renders as the plain sine fallback. */
|
|
45
65
|
export function playsPlainSine(
|
|
46
66
|
instrument: string,
|
|
@@ -54,6 +74,7 @@ export function playsPlainSine(
|
|
|
54
74
|
if ((KEYS_FAMILIES as readonly string[]).includes(name)) return false;
|
|
55
75
|
if (isDrumInstrument(name) || isSamplerInstrument(name)) return false;
|
|
56
76
|
if (name === "wavetable" || resolveOscillator(name)) return false;
|
|
77
|
+
if (isGuideInstrument(name)) return false;
|
|
57
78
|
return !LEGACY_TONES.some((tone) => name.includes(tone));
|
|
58
79
|
}
|
|
59
80
|
|
|
@@ -73,22 +94,52 @@ export function plainSineAdvice(
|
|
|
73
94
|
const advice = LEGACY_ADVICE[name];
|
|
74
95
|
return `"${name}" is dawg's plain sine (kept for old projects)${advice ? ` · ${advice}` : ""}`;
|
|
75
96
|
}
|
|
76
|
-
const near = nearestWord(name, knownWords());
|
|
77
97
|
// A resolver word stored raw (an older project): the word itself now
|
|
78
98
|
// picks a voice when typed again.
|
|
79
|
-
if (
|
|
99
|
+
if (knownWords().includes(name))
|
|
80
100
|
return `"${name}" is stored as a bare word and plays a plain sine · type instrument ${name} again for its voice`;
|
|
101
|
+
const near = nearest(name, knownWords());
|
|
81
102
|
return `"${name}" is not a dawg instrument and plays a plain sine${near ? ` · did you mean ${near}?` : ""}`;
|
|
82
103
|
}
|
|
83
104
|
|
|
84
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* Whether `word` names an instrument exactly: an instrument word, preset,
|
|
107
|
+
* engine, keys family, oscillator, drum, sampler or guide word, or a legacy
|
|
108
|
+
* tone. The write path (`instrument <word>`) refuses anything else, so a typo
|
|
109
|
+
* such as `sawtoth` is never stored. Stored projects are not checked here:
|
|
110
|
+
* the read and render paths keep playing whatever they hold.
|
|
111
|
+
*/
|
|
112
|
+
export function isExactInstrument(word: string): boolean {
|
|
113
|
+
const name = word.trim().toLowerCase();
|
|
114
|
+
if (name === "") return false;
|
|
115
|
+
if (
|
|
116
|
+
name === "sine" ||
|
|
117
|
+
LEGACY_TONES.includes(name) ||
|
|
118
|
+
LEGACY_WORDS.includes(name) ||
|
|
119
|
+
knownWords().includes(name)
|
|
120
|
+
)
|
|
121
|
+
return true;
|
|
122
|
+
if (registeredEngines().includes(name)) return true;
|
|
123
|
+
if ((KEYS_FAMILIES as readonly string[]).includes(name)) return true;
|
|
124
|
+
if (isDrumInstrument(name) || isSamplerInstrument(name)) return true;
|
|
125
|
+
if (isGuideInstrument(name)) return true;
|
|
126
|
+
return name === "wavetable" || resolveOscillator(name) !== undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Whether `word` is not an exact instrument name (see isExactInstrument). */
|
|
85
130
|
export function isUnknownInstrument(word: string): boolean {
|
|
131
|
+
return !isExactInstrument(word);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* `✗ instrument sawtoth · did you mean sawtooth? · instrument list`: the
|
|
136
|
+
* refusal for an instrument write that names nothing exactly.
|
|
137
|
+
*/
|
|
138
|
+
export function unknownInstrumentMessage(word: string): string {
|
|
86
139
|
const name = word.trim().toLowerCase();
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
!INSTRUMENT_WORDS.some((row) => row.word === name)
|
|
91
|
-
);
|
|
140
|
+
const near = nearest(name, [...knownWords(), ...LEGACY_TONES, "sine"]);
|
|
141
|
+
const shown = name.length > 32 ? `${name.slice(0, 31)}…` : name;
|
|
142
|
+
return `instrument ${shown}${near ? ` · did you mean ${near}?` : ""} · instrument list`;
|
|
92
143
|
}
|
|
93
144
|
|
|
94
145
|
/** `dawg check` warnings for every track that plays the plain sine. */
|
|
@@ -100,38 +151,3 @@ export function plainSineWarnings(tracks: readonly Track[]): string[] {
|
|
|
100
151
|
}
|
|
101
152
|
return out;
|
|
102
153
|
}
|
|
103
|
-
|
|
104
|
-
/** The closest of `words` to `word` within two edits, if any. */
|
|
105
|
-
export function nearestWord(
|
|
106
|
-
word: string,
|
|
107
|
-
words: readonly string[],
|
|
108
|
-
): string | undefined {
|
|
109
|
-
let best: string | undefined;
|
|
110
|
-
let bestDistance = 3;
|
|
111
|
-
for (const candidate of words) {
|
|
112
|
-
const distance = editDistance(word, candidate);
|
|
113
|
-
if (distance < bestDistance) {
|
|
114
|
-
best = candidate;
|
|
115
|
-
bestDistance = distance;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return best;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function editDistance(a: string, b: string): number {
|
|
122
|
-
const row = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
123
|
-
for (let i = 1; i <= a.length; i += 1) {
|
|
124
|
-
let previous = row[0]!;
|
|
125
|
-
row[0] = i;
|
|
126
|
-
for (let j = 1; j <= b.length; j += 1) {
|
|
127
|
-
const saved = row[j]!;
|
|
128
|
-
row[j] = Math.min(
|
|
129
|
-
row[j]! + 1,
|
|
130
|
-
row[j - 1]! + 1,
|
|
131
|
-
previous + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
132
|
-
);
|
|
133
|
-
previous = saved;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return row[b.length]!;
|
|
137
|
-
}
|
package/src/audio/instruments.ts
CHANGED
|
@@ -10,12 +10,14 @@ import type { PerformedNote } from "../../core/expression.ts";
|
|
|
10
10
|
import type { Track, TrackScore } from "../../core/score.ts";
|
|
11
11
|
import type { TuningTable } from "../../core/tuning.ts";
|
|
12
12
|
import { WIND_ENGINE } from "./winds/engine.ts";
|
|
13
|
+
import { SING_ENGINE } from "./sing/engine.ts";
|
|
13
14
|
import { MODAL_ENGINE } from "./resonators.ts";
|
|
14
15
|
import type { SampleBank } from "./samples.ts";
|
|
15
16
|
import type { RenderContext } from "./wav.ts";
|
|
16
17
|
import { STRING_ENGINE } from "./strings/engine.ts";
|
|
17
18
|
import { GRANULAR_ENGINE } from "./granular.ts";
|
|
18
19
|
import { KEYS_ENGINES } from "./keys/engine.ts";
|
|
20
|
+
import { VOCODER_ENGINE } from "./vocoder/index.ts";
|
|
19
21
|
|
|
20
22
|
/** What an engine renders with: the track's render context plus its tuning. */
|
|
21
23
|
export type EngineContext = RenderContext &
|
|
@@ -138,3 +140,5 @@ registerEngine(GRANULAR_ENGINE);
|
|
|
138
140
|
for (const engine of KEYS_ENGINES) registerEngine(engine);
|
|
139
141
|
registerEngine(MODAL_ENGINE);
|
|
140
142
|
registerEngine(WIND_ENGINE);
|
|
143
|
+
registerEngine(SING_ENGINE);
|
|
144
|
+
registerEngine(VOCODER_ENGINE);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calibration 1 (q08): per-preset, key-tracked level trims for the keys
|
|
3
|
+
* engine and the harpsichord. The modelled pianos, electric pianos and
|
|
4
|
+
* clavs were levelled one family at a time, so at velocity 0.8 they sat 2
|
|
5
|
+
* to 40 dB under the `piano` synth and fell steeply up the keyboard (the
|
|
6
|
+
* hammer and tangent pulses filter the upper keys' partials hard). Each
|
|
7
|
+
* row is the gain in dB, fitted from renders, that brings one note at
|
|
8
|
+
* velocity 0.8 to the `piano` reference (-18 LUFS momentary maximum), at
|
|
9
|
+
* keys 24, 36, ... 108, capped so the note's sample peak stays at or under
|
|
10
|
+
* -4 dBFS; the top keys' hammer click is all crest, so there the cap wins
|
|
11
|
+
* and they sit a few dB under. Between keys the trim is interpolated in
|
|
12
|
+
* dB, and outside it holds the end value. Trims are linear, so velocity
|
|
13
|
+
* response is kept. Legacy songs (no calibration) never read this table.
|
|
14
|
+
*
|
|
15
|
+
* Pinned: never edit a row; a later revision appends a new table.
|
|
16
|
+
*/
|
|
17
|
+
export const KEYS_TRIM_KEYS = Object.freeze([
|
|
18
|
+
24, 36, 48, 60, 72, 84, 96, 108,
|
|
19
|
+
] as const);
|
|
20
|
+
|
|
21
|
+
/** Highest boost at any key, so the top octave is not pulled out of noise. */
|
|
22
|
+
export const KEYS_TRIM_MAX_DB = 40;
|
|
23
|
+
|
|
24
|
+
export const KEYS_TRIM_DB_1: Readonly<Record<string, readonly number[]>> =
|
|
25
|
+
Object.freeze({
|
|
26
|
+
harpsichord: [4.1, 7.0, 9.9, 13.1, 16.2, 18.6, 18.2, 17.5],
|
|
27
|
+
grand: [-1.4, -1.5, -0.6, 0.8, 3.8, 4.8, 5.5, 8.3],
|
|
28
|
+
ballad: [-4.1, -4.7, -3.4, -2.9, -0.5, 3.1, 5.4, 8.2],
|
|
29
|
+
upright: [1.4, 0.4, 0.5, 2.2, 4.0, 4.2, 4.8, 8.7],
|
|
30
|
+
felt: [3.9, 4.1, 4.5, 5.2, 6.6, 8.9, 10.6, 15.4],
|
|
31
|
+
lofi: [4.1, 4.1, 4.3, 5.7, 7.8, 8.2, 9.2, 15.0],
|
|
32
|
+
honkytonk: [1.2, 2.9, 3.2, 4.1, 4.0, 3.5, 3.6, 6.2],
|
|
33
|
+
prepared: [2.3, -1.5, 1.2, 5.7, 2.0, 6.2, 4.5, 7.6],
|
|
34
|
+
epiano: [6.0, 5.2, 4.8, 5.3, 7.3, 7.6, 6.5, 8.5],
|
|
35
|
+
suitcase: [7.6, 6.3, 5.6, 5.9, 7.7, 7.9, 6.6, 8.5],
|
|
36
|
+
dyno: [3.0, 4.1, 4.5, 5.1, 6.6, 7.5, 6.2, 7.7],
|
|
37
|
+
wurli: [9.6, 6.1, 5.3, 6.0, 7.8, 6.1, 7.6, 11.1],
|
|
38
|
+
clav: [8.1, 8.8, 8.9, 10.4, 11.2, 10.8, 8.5, 2.7],
|
|
39
|
+
funkclav: [7.1, 7.9, 9.0, 9.1, 9.0, 5.1, 7.8, 5.9],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** Linear gain for a preset at a key under calibration 1. */
|
|
43
|
+
export function keysTrim(preset: string, key: number): number {
|
|
44
|
+
const row = KEYS_TRIM_DB_1[preset];
|
|
45
|
+
if (!row || row.length !== KEYS_TRIM_KEYS.length) return 1;
|
|
46
|
+
const keys = KEYS_TRIM_KEYS;
|
|
47
|
+
let db: number;
|
|
48
|
+
if (key <= keys[0]) db = row[0]!;
|
|
49
|
+
else if (key >= keys[keys.length - 1]!) db = row[row.length - 1]!;
|
|
50
|
+
else {
|
|
51
|
+
const i = Math.min(keys.length - 2, Math.floor((key - keys[0]) / 12));
|
|
52
|
+
const t = (key - keys[i]!) / 12;
|
|
53
|
+
db = row[i]! + (row[i + 1]! - row[i]!) * t;
|
|
54
|
+
}
|
|
55
|
+
return 10 ** (Math.min(KEYS_TRIM_MAX_DB, db) / 20);
|
|
56
|
+
}
|
|
@@ -60,6 +60,8 @@ export type ElectricNoteOn = Readonly<{
|
|
|
60
60
|
/** Track seed hash (clav pickup positions are per track). */
|
|
61
61
|
trackSeed: number;
|
|
62
62
|
noteSeed: string;
|
|
63
|
+
/** Song calibration (q08); 1+ keeps the tine hammer under the period. */
|
|
64
|
+
calibration?: number;
|
|
63
65
|
}>;
|
|
64
66
|
|
|
65
67
|
/** Clamped-free cantilever mode ratios. */
|
|
@@ -130,7 +132,12 @@ export class TineVoice implements KeysVoice {
|
|
|
130
132
|
const drive = lerp(0.35, rhodes ? 1.6 : 1.5, p.bark);
|
|
131
133
|
const A = (0.15 + 0.85 * v) * drive;
|
|
132
134
|
// Hardness scales the hammer contact (0.5 is the prototype's).
|
|
133
|
-
const
|
|
135
|
+
const legacy = lerp(2.2, 0.9, v) * (1.4 - 0.8 * p.hardness);
|
|
136
|
+
// Calibration 1: the contact stays under 0.6 of the tine's period, so
|
|
137
|
+
// the pulse's first null never lands under the fundamental (the top
|
|
138
|
+
// octave fell 35 dB).
|
|
139
|
+
const contact =
|
|
140
|
+
(note.calibration ?? 0) >= 1 ? Math.min(legacy, 600 / f) : legacy;
|
|
134
141
|
this.pulse = hammerPulse(Math.max(2, Math.round(contact * 1e-3 * sr)), 1.5);
|
|
135
142
|
this.bank = new ModeBank(4);
|
|
136
143
|
this.bank.add(f, T, A * 0.85, 1, sr);
|
package/src/audio/keys/engine.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { interpolateAutomation } from "../effects/common.ts";
|
|
|
30
30
|
import type { EngineContext, InstrumentEngine } from "../instruments.ts";
|
|
31
31
|
import { Biquad2, clamp, CONTROL, LN1000 } from "./dsp.ts";
|
|
32
32
|
import { ORGAN_ENGINES } from "./organ.ts";
|
|
33
|
+
import { keysTrim } from "./calibration.ts";
|
|
33
34
|
import {
|
|
34
35
|
damperT60,
|
|
35
36
|
physicalKey,
|
|
@@ -154,6 +155,8 @@ export function bodyOf(values: Values): string {
|
|
|
154
155
|
|
|
155
156
|
type Live = {
|
|
156
157
|
voice: KeysVoice;
|
|
158
|
+
/** Calibration 1 level trim (1 for legacy songs). */
|
|
159
|
+
trim: number;
|
|
157
160
|
/** Sample index the voice started at. */
|
|
158
161
|
start: number;
|
|
159
162
|
/** Sample index of key-up. */
|
|
@@ -216,6 +219,7 @@ export function renderKeysTrack(
|
|
|
216
219
|
const seedTick = context.seedTick ?? 0;
|
|
217
220
|
const window = Math.round(KEYS_LIMITS.tailSeconds * sr);
|
|
218
221
|
const taper = Math.round(KEYS_LIMITS.taperSeconds * sr);
|
|
222
|
+
const calibrated = (context.score.calibration ?? 0) >= 1;
|
|
219
223
|
const onsets: Onset[] = [];
|
|
220
224
|
notes.forEach((note) => {
|
|
221
225
|
const hz = noteHz(note.pitch, note.cents, context.tuning);
|
|
@@ -267,7 +271,14 @@ export function renderKeysTrack(
|
|
|
267
271
|
trackSeed,
|
|
268
272
|
noteSeed: `${track.id}:${note.id}:${note.startTick + seedTick}`,
|
|
269
273
|
...(soft > 0 ? { soft } : {}),
|
|
274
|
+
...(calibrated ? { calibration: 1 } : {}),
|
|
270
275
|
};
|
|
276
|
+
const trim = calibrated
|
|
277
|
+
? keysTrim(
|
|
278
|
+
track.keys?.preset ?? track.instrument ?? "grand",
|
|
279
|
+
note.pitch,
|
|
280
|
+
)
|
|
281
|
+
: 1;
|
|
271
282
|
const voice: KeysVoice = electric
|
|
272
283
|
? electricVoice(
|
|
273
284
|
noteOn,
|
|
@@ -292,6 +303,7 @@ export function renderKeysTrack(
|
|
|
292
303
|
bend && wow ? (t: number) => bend(t) + wow(t) : (bend ?? wow);
|
|
293
304
|
live.push({
|
|
294
305
|
voice,
|
|
306
|
+
trim,
|
|
295
307
|
start: onset.start,
|
|
296
308
|
off: onset.off,
|
|
297
309
|
end: onset.off + window,
|
|
@@ -331,7 +343,7 @@ export function renderKeysTrack(
|
|
|
331
343
|
gain *= Math.max(0, x.fade) / x.fadeLength;
|
|
332
344
|
x.fade -= 1;
|
|
333
345
|
}
|
|
334
|
-
const y = block[s]! * gain;
|
|
346
|
+
const y = block[s]! * gain * x.trim;
|
|
335
347
|
left[index]! += y * gl;
|
|
336
348
|
right[index]! += y * gr;
|
|
337
349
|
}
|
package/src/audio/keys/piano.ts
CHANGED
|
@@ -162,6 +162,11 @@ export type PianoNoteOn = Readonly<{
|
|
|
162
162
|
* the unstruck string's aftersound.
|
|
163
163
|
*/
|
|
164
164
|
soft?: number;
|
|
165
|
+
/**
|
|
166
|
+
* Song calibration (q08); 1+ keeps the hammer contact shorter than the
|
|
167
|
+
* fundamental's period and levels on what the hammer excites.
|
|
168
|
+
*/
|
|
169
|
+
calibration?: number;
|
|
165
170
|
}>;
|
|
166
171
|
|
|
167
172
|
type Rattle = {
|
|
@@ -242,8 +247,13 @@ export class PianoVoice {
|
|
|
242
247
|
)
|
|
243
248
|
: 1;
|
|
244
249
|
if (soft > 0) after = clamp(after + (1 - after) * 0.4 * soft, 0, 0.95);
|
|
250
|
+
const calibrated = (note.calibration ?? 0) >= 1;
|
|
251
|
+
// Calibration 1: a contact longer than about 0.6 of the fundamental's
|
|
252
|
+
// period puts the pulse's first spectral null under partial 1, and
|
|
253
|
+
// the upper keys came out as hammer noise (40 dB down at C7).
|
|
254
|
+
const contactMs = calibrated ? Math.min(thMs, 600 / f1) : thMs;
|
|
245
255
|
this.pulse = hammerPulse(
|
|
246
|
-
Math.max(2, Math.round(
|
|
256
|
+
Math.max(2, Math.round(contactMs * 1e-3 * sr)),
|
|
247
257
|
1 + 2 * v * p.hardness,
|
|
248
258
|
);
|
|
249
259
|
const strings = stringsFor(key);
|
|
@@ -311,10 +321,20 @@ export class PianoVoice {
|
|
|
311
321
|
// amplitude) is made up so the extra loss stays about 1 dB.
|
|
312
322
|
if (soft > 0)
|
|
313
323
|
loud *= (1 - 0.3 * soft) * Math.sqrt(sum0 / Math.max(sumW, 1e-9));
|
|
314
|
-
const G = (0.35 * loud) / Math.sqrt(Math.max(energy, 1e-9));
|
|
315
324
|
const mags = candidates.map(
|
|
316
325
|
(c) => pulseMag(this.pulse, (TAU * c.hz) / sr) * Math.abs(c.u),
|
|
317
326
|
);
|
|
327
|
+
// Calibration 1 levels on the partials the hammer actually excites
|
|
328
|
+
// (as the electric keys do), so the hammer filter no longer sets the
|
|
329
|
+
// level; legacy levels on the unfiltered strike.
|
|
330
|
+
const heard = calibrated
|
|
331
|
+
? mags.reduce((sum, m) => sum + m * m, 0) /
|
|
332
|
+
Math.max(
|
|
333
|
+
1e-12,
|
|
334
|
+
candidates.reduce((sum, c) => sum + c.u * c.u, 0),
|
|
335
|
+
)
|
|
336
|
+
: 1;
|
|
337
|
+
const G = (0.35 * loud) / Math.sqrt(Math.max(energy * heard, 1e-9));
|
|
318
338
|
const peak = Math.max(0, ...mags);
|
|
319
339
|
candidates.forEach((c, i) => {
|
|
320
340
|
// Below -80 dB after the hammer filter: not worth a mode.
|
package/src/audio/kits.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* arithmetic on the seeded noise `wav.ts` passes in, so a kit renders the
|
|
8
8
|
* same buffer on every run.
|
|
9
9
|
*/
|
|
10
|
-
import type
|
|
10
|
+
import { type DrumVoice, isDrumInstrument } from "../../core/drums.ts";
|
|
11
11
|
import {
|
|
12
12
|
SYNTH_KITS,
|
|
13
13
|
type DrumBodyParams,
|
|
@@ -48,9 +48,10 @@ function body(
|
|
|
48
48
|
noise: number,
|
|
49
49
|
state: KitVoiceState,
|
|
50
50
|
sampleRate: number,
|
|
51
|
+
ratio = 1,
|
|
51
52
|
): number {
|
|
52
53
|
const frequency =
|
|
53
|
-
params.base + params.sweep * Math.exp(-t * params.sweepRate);
|
|
54
|
+
(params.base + params.sweep * Math.exp(-t * params.sweepRate)) * ratio;
|
|
54
55
|
state.phase += frequency / sampleRate;
|
|
55
56
|
return (
|
|
56
57
|
Math.sin(2 * Math.PI * state.phase) *
|
|
@@ -60,19 +61,52 @@ function body(
|
|
|
60
61
|
);
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
/** PolyBLEP residual for a unit step at phase 0 (Välimäki). */
|
|
65
|
+
function polyBlep(phase: number, step: number): number {
|
|
66
|
+
if (phase < step) {
|
|
67
|
+
const x = phase / step;
|
|
68
|
+
return x + x - x * x - 1;
|
|
69
|
+
}
|
|
70
|
+
if (phase > 1 - step) {
|
|
71
|
+
const x = (phase - 1) / step;
|
|
72
|
+
return x * x + x + x + 1;
|
|
73
|
+
}
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A band-limited square at `hz`, `t` seconds in (closed-form phase). */
|
|
78
|
+
export function blepSquare(hz: number, t: number, sampleRate: number): number {
|
|
79
|
+
const phase = (t * hz) % 1;
|
|
80
|
+
const step = Math.min(0.5, hz / sampleRate);
|
|
81
|
+
return (
|
|
82
|
+
(phase < 0.5 ? 1 : -1) +
|
|
83
|
+
polyBlep(phase, step) -
|
|
84
|
+
polyBlep((phase + 0.5) % 1, step)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
63
88
|
function hat(
|
|
64
89
|
params: HatParams,
|
|
65
90
|
t: number,
|
|
66
91
|
noise: number,
|
|
67
92
|
bright: number,
|
|
68
93
|
state: KitVoiceState,
|
|
94
|
+
sampleRate = 0,
|
|
69
95
|
): number {
|
|
70
96
|
state.dark += 0.3 * (noise - state.dark);
|
|
71
97
|
const noisy = params.tone * bright + (1 - params.tone) * state.dark * 0.8;
|
|
72
98
|
let source = noisy;
|
|
73
99
|
if (params.metal > 0) {
|
|
74
100
|
let square = 0;
|
|
75
|
-
|
|
101
|
+
// Calibrated (sampleRate > 0): band-limited squares, so the metal
|
|
102
|
+
// does not alias and sounds the same at every rate.
|
|
103
|
+
for (const hz of METAL_HZ)
|
|
104
|
+
square +=
|
|
105
|
+
sampleRate > 0
|
|
106
|
+
? blepSquare(hz, t, sampleRate)
|
|
107
|
+
: (t * hz) % 1 < 0.5
|
|
108
|
+
? 1
|
|
109
|
+
: -1;
|
|
76
110
|
// First difference: keep the metallic top end, drop the low beating.
|
|
77
111
|
const metal = (square / METAL_HZ.length - state.metalPrevious) * 1.6;
|
|
78
112
|
state.metalPrevious = square / METAL_HZ.length;
|
|
@@ -90,10 +124,21 @@ export function kitDrumSample(
|
|
|
90
124
|
bright: number,
|
|
91
125
|
state: KitVoiceState,
|
|
92
126
|
sampleRate: number,
|
|
127
|
+
/** Calibration 1+ (0.7): pitch ratio for toms, band-limited metal. */
|
|
128
|
+
calibrated?: Readonly<{ tomRatio: number }>,
|
|
93
129
|
): number {
|
|
94
130
|
let sample: number;
|
|
131
|
+
const metalRate = calibrated ? sampleRate : 0;
|
|
95
132
|
if (voice === "kick") sample = body(kit.kick, t, noise, state, sampleRate);
|
|
96
|
-
else if (voice === "tom")
|
|
133
|
+
else if (voice === "tom")
|
|
134
|
+
sample = body(
|
|
135
|
+
kit.tom,
|
|
136
|
+
t,
|
|
137
|
+
noise,
|
|
138
|
+
state,
|
|
139
|
+
sampleRate,
|
|
140
|
+
calibrated?.tomRatio ?? 1,
|
|
141
|
+
);
|
|
97
142
|
else if (voice === "snare") {
|
|
98
143
|
const p = kit.snare;
|
|
99
144
|
state.phase += p.tone / sampleRate;
|
|
@@ -112,9 +157,10 @@ export function kitDrumSample(
|
|
|
112
157
|
state.band *
|
|
113
158
|
p.level *
|
|
114
159
|
(burst + 0.8 * Math.exp(-(t - span) * p.tail) * +(t >= span));
|
|
115
|
-
} else if (voice === "hat")
|
|
160
|
+
} else if (voice === "hat")
|
|
161
|
+
sample = hat(kit.hat, t, noise, bright, state, metalRate);
|
|
116
162
|
else if (voice === "openhat")
|
|
117
|
-
sample = hat(kit.openhat, t, noise, bright, state);
|
|
163
|
+
sample = hat(kit.openhat, t, noise, bright, state, metalRate);
|
|
118
164
|
else {
|
|
119
165
|
const p = kit.rim;
|
|
120
166
|
sample =
|
|
@@ -142,9 +188,92 @@ export function kitDrumSample(
|
|
|
142
188
|
return sample * kit.gain;
|
|
143
189
|
}
|
|
144
190
|
|
|
191
|
+
// ── calibrated GM extras (0.7) ─────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
/** GM cymbal and bell numbers that calibration 1+ plays as metal. */
|
|
194
|
+
export type MetalKind = "crash" | "ride" | "cowbell";
|
|
195
|
+
|
|
196
|
+
const METAL_PITCHES: Readonly<Record<number, MetalKind>> = Object.freeze({
|
|
197
|
+
49: "crash",
|
|
198
|
+
52: "crash",
|
|
199
|
+
55: "crash",
|
|
200
|
+
57: "crash",
|
|
201
|
+
51: "ride",
|
|
202
|
+
53: "ride",
|
|
203
|
+
59: "ride",
|
|
204
|
+
56: "cowbell",
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
/** The metal voice for a GM pitch, or undefined for every other number. */
|
|
208
|
+
export function metalKindForPitch(pitch: number): MetalKind | undefined {
|
|
209
|
+
return METAL_PITCHES[pitch];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* GM tom pitch ratio: 45 (low tom) is the voice's own tuning, and each GM
|
|
214
|
+
* semitone moves the body by two thirds of a semitone, so 41 (low floor)
|
|
215
|
+
* through 50 (high tom) spread across about half an octave.
|
|
216
|
+
*/
|
|
217
|
+
export function tomRatio(pitch: number): number {
|
|
218
|
+
const clamped = Math.max(35, Math.min(55, pitch));
|
|
219
|
+
return 2 ** (((clamped - 45) * 2) / 3 / 12);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* One sample of a crash, ride or cowbell, `t` seconds after the hit.
|
|
224
|
+
* Crash and ride are band-limited inharmonic squares through a first
|
|
225
|
+
* difference plus bright noise; the cowbell is the classic 540/800 Hz
|
|
226
|
+
* square pair. Deterministic: closed-form phase and the caller's noise.
|
|
227
|
+
*/
|
|
228
|
+
export function metalSample(
|
|
229
|
+
kind: MetalKind,
|
|
230
|
+
t: number,
|
|
231
|
+
bright: number,
|
|
232
|
+
state: KitVoiceState,
|
|
233
|
+
sampleRate: number,
|
|
234
|
+
): number {
|
|
235
|
+
if (kind === "cowbell") {
|
|
236
|
+
const tone =
|
|
237
|
+
(blepSquare(540, t, sampleRate) + blepSquare(800, t, sampleRate)) / 2;
|
|
238
|
+
state.band += 0.45 * (tone - state.band);
|
|
239
|
+
const envelope = 0.7 * Math.exp(-t * 60) + 0.3 * Math.exp(-t * 9);
|
|
240
|
+
return state.band * 0.5 * envelope;
|
|
241
|
+
}
|
|
242
|
+
let square = 0;
|
|
243
|
+
for (const hz of METAL_HZ) square += blepSquare(hz * 1.9, t, sampleRate);
|
|
244
|
+
const metal = (square / METAL_HZ.length - state.metalPrevious) * 1.6;
|
|
245
|
+
state.metalPrevious = square / METAL_HZ.length;
|
|
246
|
+
if (kind === "crash") {
|
|
247
|
+
const swell = Math.min(1, t / 0.004);
|
|
248
|
+
return (0.55 * bright + 0.45 * metal) * 0.42 * swell * Math.exp(-t * 4.2);
|
|
249
|
+
}
|
|
250
|
+
// Ride: a pinged bell over a long, quieter wash.
|
|
251
|
+
const ping = (0.6 * metal + 0.4 * bright) * 0.3 * Math.exp(-t * 28);
|
|
252
|
+
const wash = (0.35 * metal + 0.65 * bright) * 0.16 * Math.exp(-t * 3.5);
|
|
253
|
+
return ping + wash;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Length of a calibrated crash, ride or cowbell one-shot. */
|
|
257
|
+
export const METAL_SECONDS = 1.4;
|
|
258
|
+
|
|
145
259
|
/** Longest synth-kit one-shot in the score; 0 without kits (so renders without kits are unchanged). */
|
|
146
260
|
export function kitTailSeconds(score: TrackScore): number {
|
|
147
261
|
let seconds = 0;
|
|
262
|
+
if ((score.calibration ?? 0) >= 1) {
|
|
263
|
+
const drums = new Set(
|
|
264
|
+
score.tracks
|
|
265
|
+
.filter((track) => isDrumInstrument(track.instrument))
|
|
266
|
+
.map((track) => track.id),
|
|
267
|
+
);
|
|
268
|
+
if (
|
|
269
|
+
score.notes.some(
|
|
270
|
+
(note) =>
|
|
271
|
+
drums.has(note.trackId) &&
|
|
272
|
+
metalKindForPitch(note.pitch) !== undefined,
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
seconds = METAL_SECONDS;
|
|
276
|
+
}
|
|
148
277
|
for (const track of score.tracks)
|
|
149
278
|
seconds = Math.max(seconds, synthKit(track.kit)?.seconds ?? 0);
|
|
150
279
|
return seconds;
|