@hraness/dawg 0.6.0 → 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 +126 -0
- package/DAWG.md +655 -141
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +759 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -99
- package/core/expression.ts +158 -6
- package/core/fx.ts +354 -32
- package/core/granular.ts +91 -0
- package/core/instruments.ts +180 -0
- package/core/keys.ts +573 -4
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +306 -4
- package/core/routing.ts +165 -0
- package/core/score.ts +1181 -16
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +354 -6
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +3160 -69
- package/core/sections.ts +298 -24
- package/core/sing.ts +815 -0
- package/core/strings.ts +237 -2
- 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/core/winds.ts +652 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +30 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -6
- package/guides/chords.md +16 -14
- package/guides/effects.md +17 -15
- 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 -10
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +32 -0
- 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 -12
- 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 +131 -13
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/chord-tools.ts +153 -1
- 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 +91 -0
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +15 -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 +5 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/resample-tool.ts +131 -0
- 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 +307 -37
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +16 -6
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +58 -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 +171 -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/dsp/shift.ts +256 -0
- package/src/audio/effects/chain.ts +21 -4
- package/src/audio/effects/common.ts +57 -0
- package/src/audio/effects/convolution.ts +113 -5
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/engine.ts +206 -33
- package/src/audio/fit.ts +80 -3
- package/src/audio/granular.ts +352 -21
- package/src/audio/instrument-check.ts +67 -47
- package/src/audio/instruments.ts +36 -3
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +427 -0
- package/src/audio/keys/engine.ts +156 -20
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +72 -8
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/kits.ts +135 -6
- package/src/audio/live-worker.ts +3 -1
- package/src/audio/live.ts +247 -26
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +45 -3
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +286 -0
- package/src/audio/resonators.ts +10 -2
- package/src/audio/sampler.ts +252 -24
- package/src/audio/samples.ts +46 -3
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/body.ts +29 -16
- package/src/audio/strings/bow.ts +699 -0
- package/src/audio/strings/engine.ts +274 -22
- package/src/audio/strings/measure.test-helpers.ts +2 -0
- 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 +608 -85
- package/src/audio/winds/engine.ts +306 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +104 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +492 -0
- 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 +104 -23
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +105 -33
- package/src/commands/grammar.ts +558 -0
- package/src/commands/granular.ts +7 -2
- package/src/commands/help.ts +652 -339
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +293 -17
- package/src/commands/modal.ts +67 -4
- 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/resample.ts +281 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +8 -25
- package/src/commands/sample.ts +49 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/sing.ts +478 -0
- package/src/commands/string.ts +28 -2
- package/src/commands/strum.ts +485 -0
- 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 +244 -0
- 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 +1521 -255
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +26 -3
- 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 +36 -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 +50 -7
- 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 +1377 -193
- package/src/tui/modal-menu.ts +38 -11
- package/src/tui/performance-menu.ts +46 -1
- package/src/tui/play-chords.ts +108 -5
- package/src/tui/play-mode.ts +16 -1
- package/src/tui/play-session.ts +81 -16
- 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 +144 -0
- 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
|
@@ -20,8 +20,15 @@ import { interpolateAutomation } from "../effects/common.ts";
|
|
|
20
20
|
import { seedHash, unit } from "../dsp/rng.ts";
|
|
21
21
|
import type { EngineContext, InstrumentEngine } from "../instruments.ts";
|
|
22
22
|
import { applyBody, bodyFor, symKeys, sympathetic } from "./body.ts";
|
|
23
|
+
import { keysTrim } from "../keys/calibration.ts";
|
|
23
24
|
import { clamp } from "./loop.ts";
|
|
24
25
|
import { PluckString, type Exciter, type PluckSpec } from "./pluck.ts";
|
|
26
|
+
import {
|
|
27
|
+
BowedString,
|
|
28
|
+
type BowControl,
|
|
29
|
+
type BowSpec,
|
|
30
|
+
type BowNote,
|
|
31
|
+
} from "./bow.ts";
|
|
25
32
|
|
|
26
33
|
const BLOCK = 128;
|
|
27
34
|
/** Longest a single string may ring (seconds). */
|
|
@@ -84,8 +91,155 @@ export function noteSpec(
|
|
|
84
91
|
};
|
|
85
92
|
}
|
|
86
93
|
|
|
94
|
+
/** Whether resolved values play the bowed voice (`exciter bow`). */
|
|
95
|
+
export function isBowed(values: StringValues): boolean {
|
|
96
|
+
return values.exciter === "bow";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Play mode keeps a bowed section to two players (live budget). */
|
|
100
|
+
export const LIVE_BOW_UNISON = 2;
|
|
101
|
+
|
|
102
|
+
/** The track as play mode hears it: a bowed section capped at 2 players. */
|
|
103
|
+
export function liveStringTrack(track: Track): Track {
|
|
104
|
+
if (track.instrument !== "string" || !track.string) return track;
|
|
105
|
+
const values = resolveString(track.string);
|
|
106
|
+
if (!isBowed(values) || (values.unison as number) <= LIVE_BOW_UNISON)
|
|
107
|
+
return track;
|
|
108
|
+
return { ...track, string: { ...track.string, unison: LIVE_BOW_UNISON } };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Bow lanes read every 32 samples (`string-<name>`, absent: the value). */
|
|
112
|
+
const BOW_LANES = ["pressure", "speed", "sord", "dyn"] as const;
|
|
113
|
+
|
|
114
|
+
/** The bowed spec and note fields for one note. */
|
|
115
|
+
export function bowSpec(
|
|
116
|
+
values: StringValues,
|
|
117
|
+
note: PerformedNote,
|
|
118
|
+
track: Track,
|
|
119
|
+
context: EngineContext,
|
|
120
|
+
): { spec: BowSpec; velocity: number; control?: (t: number) => BowControl } {
|
|
121
|
+
const at = (name: string) => valueAt(track, values, name, note.startTick);
|
|
122
|
+
let velocity = clamp(note.velocity, 0, 1);
|
|
123
|
+
const articulation = note.articulation;
|
|
124
|
+
const sens = clamp(values.vel as number, 0, 1);
|
|
125
|
+
velocity = 1 - sens * (1 - velocity);
|
|
126
|
+
let attack = values.attack as number;
|
|
127
|
+
let release = values.release as number;
|
|
128
|
+
// Detache (spec 4): staccato and ghost are short strokes, quick on and off.
|
|
129
|
+
if (articulation === "staccato" || articulation === "ghost") {
|
|
130
|
+
attack = Math.min(attack, 0.01);
|
|
131
|
+
release = Math.min(release, 0.03);
|
|
132
|
+
}
|
|
133
|
+
// A harder stroke presses harder (spec 4: pressure += 0.3 (vel - 0.5)).
|
|
134
|
+
const press = (p: number) => clamp(p + 0.3 * (velocity - 0.5), 0, 1);
|
|
135
|
+
// Accent and marcato bite: pressure +0.2 for the first 80 ms.
|
|
136
|
+
const bite = articulation === "accent" || articulation === "marcato";
|
|
137
|
+
const spec: BowSpec = {
|
|
138
|
+
decay: Math.max(0.02, at("ring")),
|
|
139
|
+
track: values.track as number,
|
|
140
|
+
damp: clamp(values.damp as number, 0, 1),
|
|
141
|
+
pos: clamp(at("pos"), 0.01, 0.5),
|
|
142
|
+
pressure: press(at("pressure")),
|
|
143
|
+
speed: clamp(at("speed"), 0, 1),
|
|
144
|
+
attack,
|
|
145
|
+
release,
|
|
146
|
+
tremhz: values.tremhz as number,
|
|
147
|
+
sord: clamp(at("sord"), 0, 1),
|
|
148
|
+
};
|
|
149
|
+
const lanes = BOW_LANES.filter(
|
|
150
|
+
(name) =>
|
|
151
|
+
(track.fxAutomation?.[`string-${name}` as FxLane]?.length ?? 0) > 0,
|
|
152
|
+
);
|
|
153
|
+
const dyn = clamp(values.dyn as number, 0, 1);
|
|
154
|
+
if (lanes.length === 0 && dyn === 1 && !bite) return { spec, velocity };
|
|
155
|
+
const fixed: BowControl = {
|
|
156
|
+
pressure: spec.pressure,
|
|
157
|
+
speed: spec.speed,
|
|
158
|
+
sord: spec.sord,
|
|
159
|
+
dyn,
|
|
160
|
+
};
|
|
161
|
+
const bitten: BowControl = {
|
|
162
|
+
...fixed,
|
|
163
|
+
pressure: clamp(spec.pressure + 0.2, 0, 1),
|
|
164
|
+
};
|
|
165
|
+
if (lanes.length === 0)
|
|
166
|
+
return {
|
|
167
|
+
spec,
|
|
168
|
+
velocity,
|
|
169
|
+
control: bite ? (t) => (t < 0.08 ? bitten : fixed) : () => fixed,
|
|
170
|
+
};
|
|
171
|
+
// Lanes are read in ticks; warped songs read them at the note's tempo.
|
|
172
|
+
const ticksPerSecond = context.sampleRate / context.samplesPerTick;
|
|
173
|
+
const control = (t: number): BowControl => {
|
|
174
|
+
const tick = note.startTick + t * ticksPerSecond;
|
|
175
|
+
const lane = (name: string) =>
|
|
176
|
+
clamp(valueAt(track, values, name, tick), 0, 1);
|
|
177
|
+
return {
|
|
178
|
+
pressure: clamp(
|
|
179
|
+
press(lane("pressure")) + (bite && t < 0.08 ? 0.2 : 0),
|
|
180
|
+
0,
|
|
181
|
+
1,
|
|
182
|
+
),
|
|
183
|
+
speed: lane("speed"),
|
|
184
|
+
sord: lane("sord"),
|
|
185
|
+
dyn: lane("dyn"),
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
return { spec, velocity, control };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The preset vibrato alone (a slur keeps it running). */
|
|
192
|
+
function vibratoCurve(
|
|
193
|
+
note: PerformedNote,
|
|
194
|
+
values: StringValues,
|
|
195
|
+
track: Track,
|
|
196
|
+
player: Readonly<{ rate: number; depth: number; phase: number }> = SOLO,
|
|
197
|
+
): ((t: number) => number) | undefined {
|
|
198
|
+
if (note.performance?.replaceVibrato) return undefined;
|
|
199
|
+
const rate = valueAt(track, values, "vib", note.startTick) * player.rate;
|
|
200
|
+
const depth =
|
|
201
|
+
valueAt(track, values, "vibmod", note.startTick) * 100 * player.depth;
|
|
202
|
+
const delay = values.vibdelay as number;
|
|
203
|
+
if (!(rate > 0 && depth > 0)) return undefined;
|
|
204
|
+
const phase = player.phase;
|
|
205
|
+
return (t: number) => {
|
|
206
|
+
if (t <= delay) return 0;
|
|
207
|
+
const fade = Math.min(1, (t - delay) / 0.15);
|
|
208
|
+
return depth * fade * Math.sin(2 * Math.PI * (rate * (t - delay) + phase));
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const SOLO = { rate: 1, depth: 1, phase: 0 } as const;
|
|
213
|
+
|
|
214
|
+
/** Longest seeded onset spread of a bowed section's players (spec 2). */
|
|
215
|
+
export const SECTION_SPREAD = 0.025;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* One section player (spec 2, Ensemble): seeded vibrato rate x0.92-1.08,
|
|
219
|
+
* depth x0.8-1.2 and phase, and an onset up to 25 ms late (scaled down
|
|
220
|
+
* for notes under 0.25 s). The first player stays on the grid.
|
|
221
|
+
*/
|
|
222
|
+
export function sectionPlayer(
|
|
223
|
+
seed: number,
|
|
224
|
+
index: number,
|
|
225
|
+
hold: number,
|
|
226
|
+
): { rate: number; depth: number; phase: number; onset: number } {
|
|
227
|
+
const h = seedHash(`${seed}:${index}:player`);
|
|
228
|
+
return {
|
|
229
|
+
rate: 0.92 + 0.16 * unit(h, 0, 1),
|
|
230
|
+
depth: 0.8 + 0.4 * unit(h, 1, 1),
|
|
231
|
+
phase: unit(h, 2, 1),
|
|
232
|
+
onset:
|
|
233
|
+
index === 0
|
|
234
|
+
? 0
|
|
235
|
+
: SECTION_SPREAD * unit(h, 3, 1) * Math.min(1, hold / 0.25),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
87
239
|
type Voice = {
|
|
88
|
-
string: PluckString;
|
|
240
|
+
string: PluckString | BowedString;
|
|
241
|
+
/** The string's pitch over the note's base pitch (a slur keeps it). */
|
|
242
|
+
ratio: number;
|
|
89
243
|
start: number;
|
|
90
244
|
pitch: number;
|
|
91
245
|
group: number;
|
|
@@ -94,6 +248,26 @@ type Voice = {
|
|
|
94
248
|
damp?: Readonly<{ from: number; tau: number }>;
|
|
95
249
|
};
|
|
96
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Whether pending note `index` (starting at `start`) is a lone note: no
|
|
253
|
+
* other note starts with it, so a held single note may slur into it.
|
|
254
|
+
*/
|
|
255
|
+
function slurs(
|
|
256
|
+
pending: readonly { start: number }[],
|
|
257
|
+
index: number,
|
|
258
|
+
start: number,
|
|
259
|
+
): boolean {
|
|
260
|
+
return (
|
|
261
|
+
pending[index - 1]?.start !== start && pending[index + 1]?.start !== start
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** A 64th note in samples at the song's start tempo (the slur window). */
|
|
266
|
+
function slurWindow(context: EngineContext): number {
|
|
267
|
+
const ticks = (context.score.ticksPerBeat ?? 480) / 16;
|
|
268
|
+
return ticks * context.samplesPerTick;
|
|
269
|
+
}
|
|
270
|
+
|
|
97
271
|
/** Ring-out after the last note-off, in seconds. */
|
|
98
272
|
export function stringTailSeconds(track: Track): number {
|
|
99
273
|
const values = resolveString(track.string);
|
|
@@ -163,6 +337,11 @@ export function renderStrings(
|
|
|
163
337
|
const octbelow = values.octbelow as number;
|
|
164
338
|
const cap = Math.max(1, Math.round(values.voices as number));
|
|
165
339
|
const tuning = context.tuning;
|
|
340
|
+
const bowed = isBowed(values);
|
|
341
|
+
// Calibration 1 (q08): the keyboard string presets (the harpsichord) take
|
|
342
|
+
// the keys' key-tracked level trim; other presets have no row.
|
|
343
|
+
const trimPreset =
|
|
344
|
+
(context.score.calibration ?? 0) >= 1 ? track.string?.preset : undefined;
|
|
166
345
|
const L = new Float64Array(total);
|
|
167
346
|
const R = dryR ? new Float64Array(total) : undefined;
|
|
168
347
|
const voices: Voice[] = [];
|
|
@@ -183,6 +362,47 @@ export function renderStrings(
|
|
|
183
362
|
next += 1;
|
|
184
363
|
const baseHz = noteHz(note.pitch, note.cents, tuning);
|
|
185
364
|
if (!(baseHz > 0) || baseHz >= 0.45 * sr) continue;
|
|
365
|
+
// Slur (bowed): a single note that starts while one earlier single
|
|
366
|
+
// note is still bowed moves that bow to the new pitch in 7 ms, with
|
|
367
|
+
// no new attack (legato articulation overlaps its notes, so it
|
|
368
|
+
// slurs too). Chords and double stops start new strokes.
|
|
369
|
+
if (bowed && slurs(pending, next - 1, start)) {
|
|
370
|
+
const held = voices.filter(
|
|
371
|
+
(v) => !v.string.done && v.string.offAt > start - v.start,
|
|
372
|
+
);
|
|
373
|
+
const groups = new Set(held.map((v) => v.group));
|
|
374
|
+
const pitches = new Set(held.map((v) => v.pitch));
|
|
375
|
+
// A true legato only: the held note lets go within a short window
|
|
376
|
+
// of the new one (a 64th note or 150 ms, legato's own overlap), or
|
|
377
|
+
// the new note is marked legato. A pedal or a note held under a
|
|
378
|
+
// moving line keeps sounding and the new note takes a new stroke.
|
|
379
|
+
const window = Math.max(0.15 * sr, slurWindow(context));
|
|
380
|
+
const letsGo =
|
|
381
|
+
note.articulation === "legato" ||
|
|
382
|
+
held.every((v) => v.start + v.string.offAt <= start + window);
|
|
383
|
+
const range = held.every((v) =>
|
|
384
|
+
(v.string as BowedString).canSlurTo(baseHz * v.ratio),
|
|
385
|
+
);
|
|
386
|
+
if (
|
|
387
|
+
letsGo &&
|
|
388
|
+
range &&
|
|
389
|
+
groups.size === 1 &&
|
|
390
|
+
pitches.size === 1 &&
|
|
391
|
+
held[0]!.start < start
|
|
392
|
+
) {
|
|
393
|
+
const hold = holdSamples(note, start, context) / sr;
|
|
394
|
+
for (const v of held) {
|
|
395
|
+
(v.string as BowedString).slurTo(
|
|
396
|
+
baseHz * v.ratio,
|
|
397
|
+
start - v.start,
|
|
398
|
+
Math.max(0.01, hold),
|
|
399
|
+
note.performance?.cents,
|
|
400
|
+
);
|
|
401
|
+
v.pitch = note.pitch;
|
|
402
|
+
}
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
186
406
|
// Restrike: the same key damps its previous string.
|
|
187
407
|
for (const v of voices)
|
|
188
408
|
if (v.pitch === note.pitch) v.string.releaseAt(start - v.start);
|
|
@@ -197,11 +417,11 @@ export function renderStrings(
|
|
|
197
417
|
for (const v of held)
|
|
198
418
|
if (v.group === oldest) v.string.releaseAt(start - v.start);
|
|
199
419
|
}
|
|
200
|
-
const { spec, velocity } = noteSpec(values, note, track);
|
|
201
420
|
const seed = seedHash(
|
|
202
421
|
`${note.id}:${note.startTick + (context.seedTick ?? 0)}:string`,
|
|
203
422
|
);
|
|
204
|
-
const strings: { hz: number; gain: number }[] = [];
|
|
423
|
+
const strings: { hz: number; gain: number; ratio: number }[] = [];
|
|
424
|
+
const trim = trimPreset ? keysTrim(trimPreset, note.pitch) : 1;
|
|
205
425
|
// The course's tuning error belongs to the key, not to the strike: it
|
|
206
426
|
// is seeded by track and key (so every velocity of a key beats the
|
|
207
427
|
// same way) and centred, so the course's mean pitch is the table's.
|
|
@@ -213,42 +433,74 @@ export function renderStrings(
|
|
|
213
433
|
for (let i = 0; i < unison; i += 1) {
|
|
214
434
|
const d = unison > 1 ? (i / (unison - 1) - 0.5) * detune : 0;
|
|
215
435
|
const jitter = jitters[i]! - meanJitter;
|
|
436
|
+
const ratio = 2 ** ((d * 100 + jitter) / 1200);
|
|
216
437
|
strings.push({
|
|
217
|
-
hz: baseHz *
|
|
218
|
-
gain:
|
|
438
|
+
hz: baseHz * ratio,
|
|
439
|
+
gain: trim / Math.sqrt(unison),
|
|
440
|
+
ratio,
|
|
219
441
|
});
|
|
220
442
|
}
|
|
221
443
|
if (oct > 0 && note.pitch < octbelow) {
|
|
222
444
|
const up = noteHz(Math.min(127, note.pitch + 12), note.cents, tuning);
|
|
223
445
|
const hz = up > 0 ? up : baseHz * 2;
|
|
224
|
-
if (hz < 0.45 * sr)
|
|
446
|
+
if (hz < 0.45 * sr) {
|
|
447
|
+
const tuned =
|
|
448
|
+
hz * 2 ** (((unit(course, unison, 1) - 0.5) * 4) / 1200);
|
|
225
449
|
strings.push({
|
|
226
|
-
hz:
|
|
227
|
-
gain: oct / Math.sqrt(unison),
|
|
450
|
+
hz: tuned,
|
|
451
|
+
gain: (trim * oct) / Math.sqrt(unison),
|
|
452
|
+
ratio: tuned / baseHz,
|
|
228
453
|
});
|
|
454
|
+
}
|
|
229
455
|
}
|
|
230
456
|
const hold = holdSamples(note, start, context) / sr;
|
|
231
|
-
const
|
|
457
|
+
const pluck = bowed ? undefined : noteSpec(values, note, track);
|
|
458
|
+
const bow = bowed ? bowSpec(values, note, track, context) : undefined;
|
|
459
|
+
const cents = bowed
|
|
460
|
+
? note.performance?.cents
|
|
461
|
+
: centsCurve(note, values, track);
|
|
462
|
+
const section = bowed && unison > 1;
|
|
232
463
|
strings.forEach((s, i) => {
|
|
464
|
+
// Section players (not the octave string) are seeded as players.
|
|
465
|
+
const player =
|
|
466
|
+
section && i < unison ? sectionPlayer(seed, i, hold) : undefined;
|
|
467
|
+
const onset = player ? Math.round(player.onset * sr) : 0;
|
|
468
|
+
const vibrato = bowed
|
|
469
|
+
? vibratoCurve(note, values, track, player)
|
|
470
|
+
: undefined;
|
|
233
471
|
const pan =
|
|
234
472
|
strings.length > 1 ? (i / (strings.length - 1) - 0.5) * spread : 0;
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
473
|
+
const common = {
|
|
474
|
+
hz: s.hz,
|
|
475
|
+
hold: Math.max(0.01, hold - onset / sr),
|
|
476
|
+
seed: seedHash(`${seed}:${i}`),
|
|
477
|
+
...(cents ? { cents } : {}),
|
|
478
|
+
};
|
|
479
|
+
const string = bow
|
|
480
|
+
? new BowedString(
|
|
481
|
+
bow.spec,
|
|
482
|
+
{
|
|
483
|
+
...common,
|
|
484
|
+
velocity: bow.velocity,
|
|
485
|
+
...(vibrato ? { vibrato } : {}),
|
|
486
|
+
...(bow.control ? { control: bow.control } : {}),
|
|
487
|
+
} satisfies BowNote,
|
|
488
|
+
sr,
|
|
489
|
+
// A bow sustains for as long as the note is held.
|
|
490
|
+
Math.max(MAX_RING, hold + bow.spec.release + 1),
|
|
491
|
+
)
|
|
492
|
+
: new PluckString(
|
|
493
|
+
pluck!.spec,
|
|
494
|
+
{ ...common, velocity: pluck!.velocity },
|
|
495
|
+
sr,
|
|
496
|
+
MAX_RING,
|
|
497
|
+
);
|
|
247
498
|
const gl = R ? s.gain * Math.sqrt(0.5 - pan / 2) * Math.SQRT2 : s.gain;
|
|
248
499
|
const gr = s.gain * Math.sqrt(0.5 + pan / 2) * Math.SQRT2;
|
|
249
500
|
voices.push({
|
|
250
501
|
string,
|
|
251
|
-
|
|
502
|
+
ratio: s.ratio,
|
|
503
|
+
start: start + onset,
|
|
252
504
|
pitch: note.pitch,
|
|
253
505
|
group: start,
|
|
254
506
|
gainL: gl,
|
|
@@ -24,6 +24,7 @@ export type TestNote = {
|
|
|
24
24
|
seconds?: number;
|
|
25
25
|
velocity?: number;
|
|
26
26
|
performance?: PerformedNote["performance"];
|
|
27
|
+
articulation?: PerformedNote["articulation"];
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
/** Renders notes on a string track (mono, before effects). */
|
|
@@ -58,6 +59,7 @@ export function render(
|
|
|
58
59
|
startTick: Math.round(((n.start ?? 0) * SR) / SPT),
|
|
59
60
|
durationTicks: Math.round(((n.seconds ?? seconds) * SR) / SPT),
|
|
60
61
|
...(n.performance ? { performance: n.performance } : {}),
|
|
62
|
+
...(n.articulation ? { articulation: n.articulation } : {}),
|
|
61
63
|
})) as unknown as PerformedNote[];
|
|
62
64
|
const dry = new Float64Array(samples);
|
|
63
65
|
renderStrings(dry, undefined, performed, track, context);
|
|
@@ -110,28 +110,38 @@ function triangle(phase: number): number {
|
|
|
110
110
|
* names when a track sets synth parameters.
|
|
111
111
|
*/
|
|
112
112
|
export function legacyWave(instrument: string, phase: number): number {
|
|
113
|
+
return legacyWaveFor(instrument)(phase);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* `legacyWave` with the tone chosen once: `legacyWaveFor(name)(phase)`
|
|
118
|
+
* equals `legacyWave(name, phase)`, without matching the name per sample.
|
|
119
|
+
*/
|
|
120
|
+
export function legacyWaveFor(instrument: string): (phase: number) => number {
|
|
113
121
|
const name = instrument.trim().toLowerCase();
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (name.includes("
|
|
117
|
-
|
|
118
|
-
if (name.includes("triangle"))
|
|
119
|
-
|
|
122
|
+
if (name.includes("square"))
|
|
123
|
+
return (phase) => (phase - Math.floor(phase) < 0.5 ? 1 : -1);
|
|
124
|
+
if (name.includes("saw"))
|
|
125
|
+
return (phase) => 2 * (phase - Math.floor(phase)) - 1;
|
|
126
|
+
if (name.includes("triangle"))
|
|
127
|
+
return (phase) => 1 - 4 * Math.abs(phase - Math.floor(phase) - 0.5);
|
|
128
|
+
if (name.includes("bass"))
|
|
120
129
|
// A rounded fundamental plus a quiet octave gives bass tracks useful weight.
|
|
121
|
-
return
|
|
122
|
-
|
|
123
|
-
0.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (name.includes("piano") || name.includes("pluck"))
|
|
130
|
+
return (phase) =>
|
|
131
|
+
Math.tanh(
|
|
132
|
+
0.9 * Math.sin(2 * Math.PI * phase) +
|
|
133
|
+
0.25 * Math.sin(4 * Math.PI * phase),
|
|
134
|
+
);
|
|
135
|
+
if (name.includes("piano") || name.includes("pluck"))
|
|
127
136
|
// Add stable harmonics; the envelope above supplies the note decay.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
return (phase) => {
|
|
138
|
+
const harmonic =
|
|
139
|
+
Math.sin(4 * Math.PI * phase) * 0.28 +
|
|
140
|
+
Math.sin(6 * Math.PI * phase) * 0.12;
|
|
141
|
+
return Math.tanh(Math.sin(2 * Math.PI * phase) + harmonic);
|
|
142
|
+
};
|
|
133
143
|
// Unknown instruments deliberately fall back to the original sine voice.
|
|
134
|
-
return
|
|
144
|
+
return (phase) => Math.sin(2 * Math.PI * phase);
|
|
135
145
|
}
|
|
136
146
|
|
|
137
147
|
/**
|
|
@@ -230,9 +240,9 @@ const BUILT_IN: Readonly<Record<string, OscillatorFactory>> = Object.freeze({
|
|
|
230
240
|
pulse: (init) => (phase, increment) => pulse(phase, increment, init.width()),
|
|
231
241
|
triangle: (init) => (init.partials ? additive(init) : triangle),
|
|
232
242
|
user: (init) => additive(init),
|
|
233
|
-
piano: () => (
|
|
234
|
-
pluck: () => (
|
|
235
|
-
bass: () => (
|
|
243
|
+
piano: () => legacyWaveFor("piano"),
|
|
244
|
+
pluck: () => legacyWaveFor("pluck"),
|
|
245
|
+
bass: () => legacyWaveFor("bass"),
|
|
236
246
|
white: (init) => () => (init.random() * 2 - 1) * 0.6,
|
|
237
247
|
pink: (init) => pinkNoise(init.random),
|
|
238
248
|
brown: (init) => {
|
package/src/audio/synth/voice.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from "../../../core/synth.ts";
|
|
32
32
|
import {
|
|
33
33
|
Biquad,
|
|
34
|
+
Svf,
|
|
34
35
|
CONTROL_SAMPLES,
|
|
35
36
|
clamp,
|
|
36
37
|
interpolateAutomation,
|
|
@@ -197,6 +198,8 @@ const BUTTERWORTH_Q2 = 1.3066;
|
|
|
197
198
|
class NoteFilter {
|
|
198
199
|
private readonly first = new Biquad();
|
|
199
200
|
private readonly second = new Biquad();
|
|
201
|
+
private readonly svfFirst: Svf;
|
|
202
|
+
private readonly svfSecond: Svf;
|
|
200
203
|
private readonly ladder = new Float64Array(4);
|
|
201
204
|
private ladderG = 0;
|
|
202
205
|
private ladderK = 0;
|
|
@@ -210,7 +213,22 @@ class NoteFilter {
|
|
|
210
213
|
readonly anchor: number,
|
|
211
214
|
readonly envelope: Envelope,
|
|
212
215
|
private readonly sampleRate: number,
|
|
213
|
-
) {
|
|
216
|
+
) {
|
|
217
|
+
this.svfFirst = new Svf(type);
|
|
218
|
+
this.svfSecond = new Svf(type);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* A negative depth sweeps the cutoff down during the attack, often to
|
|
223
|
+
* the 20 Hz floor within a few milliseconds. A direct-form biquad
|
|
224
|
+
* re-designed every control block blows up under that fall (its state
|
|
225
|
+
* belongs to the old coefficients), so those notes run on the SVF, whose
|
|
226
|
+
* state stays valid. Positive and zero depths keep the biquad, so their
|
|
227
|
+
* renders are unchanged.
|
|
228
|
+
*/
|
|
229
|
+
private get isSvf(): boolean {
|
|
230
|
+
return this.depth < 0;
|
|
231
|
+
}
|
|
214
232
|
|
|
215
233
|
private get isLadder(): boolean {
|
|
216
234
|
return this.slope === "ladder" && this.type === "lpf";
|
|
@@ -231,6 +249,17 @@ class NoteFilter {
|
|
|
231
249
|
this.ladderK = 3.9 * clamp((q - 0.707) / 11.3, 0, 1);
|
|
232
250
|
return;
|
|
233
251
|
}
|
|
252
|
+
if (this.isSvf) {
|
|
253
|
+
if (this.slope === "24db") {
|
|
254
|
+
this.svfFirst.set(frequency, BUTTERWORTH_Q1, this.sampleRate);
|
|
255
|
+
this.svfSecond.set(
|
|
256
|
+
frequency,
|
|
257
|
+
(q * BUTTERWORTH_Q2) / 0.707,
|
|
258
|
+
this.sampleRate,
|
|
259
|
+
);
|
|
260
|
+
} else this.svfFirst.set(frequency, q, this.sampleRate);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
234
263
|
if (this.slope === "24db") {
|
|
235
264
|
this.first.set(this.type, frequency, BUTTERWORTH_Q1, this.sampleRate);
|
|
236
265
|
this.second.set(
|
|
@@ -255,6 +284,10 @@ class NoteFilter {
|
|
|
255
284
|
stages[3]! += g * (stages[2]! - stages[3]!);
|
|
256
285
|
return stages[3]! * (1 + 0.5 * this.ladderK);
|
|
257
286
|
}
|
|
287
|
+
if (this.isSvf) {
|
|
288
|
+
const once = this.svfFirst.process(input);
|
|
289
|
+
return this.slope === "24db" ? this.svfSecond.process(once) : once;
|
|
290
|
+
}
|
|
258
291
|
const once = this.first.process(input);
|
|
259
292
|
return this.slope === "24db" ? this.second.process(once) : once;
|
|
260
293
|
}
|