@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
package/src/audio/keys/piano.ts
CHANGED
|
@@ -156,6 +156,17 @@ export type PianoNoteOn = Readonly<{
|
|
|
156
156
|
trackSeed: number;
|
|
157
157
|
/** Track seed plus note id (this strike's own randomness). */
|
|
158
158
|
noteSeed: string;
|
|
159
|
+
/**
|
|
160
|
+
* Una corda (0.6.1) at the onset, 0..1 (half pedal 0.5); absent is 0.
|
|
161
|
+
* The hammer shifts: fewer strings struck, a softer felt spot, more of
|
|
162
|
+
* the unstruck string's aftersound.
|
|
163
|
+
*/
|
|
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;
|
|
159
170
|
}>;
|
|
160
171
|
|
|
161
172
|
type Rattle = {
|
|
@@ -207,18 +218,52 @@ export class PianoVoice {
|
|
|
207
218
|
p.decay *
|
|
208
219
|
(1 - 0.15 * p.felt) *
|
|
209
220
|
(prepKind === 1 ? 0.12 : prepKind >= 0 ? 0.5 : 1);
|
|
210
|
-
const
|
|
221
|
+
const soft = clamp(note.soft ?? 0, 0, 1);
|
|
222
|
+
let after = clamp(p.after, 0, 0.95);
|
|
211
223
|
const thMs =
|
|
212
224
|
lerp(3.5, 0.8, keyPos) *
|
|
213
225
|
lerp(1.6, 0.7, v) *
|
|
214
226
|
(1 + 1.5 * p.felt) *
|
|
215
227
|
(1.3 - 0.6 * p.hardness);
|
|
228
|
+
// Una corda (keys.md 2.1): the shifted hammer meets the strings with
|
|
229
|
+
// its softer felt. Modelled spectrally, so the pulse length stays and
|
|
230
|
+
// its nulls never land on a low partial: mode input gains get a smooth
|
|
231
|
+
// two-pole low-pass relative to partial 1 (cutoff at a tenth of the
|
|
232
|
+
// spec's hammer cutoff, lowered 25% at full pedal), floored at -12 dB
|
|
233
|
+
// so no low harmonic is notched out; more of the slow aftersound is
|
|
234
|
+
// excited. Measured at keys 48-84, velocity 0.5-0.9: centroid -12 to
|
|
235
|
+
// -20% through key 72 (-5% at 84), peak -2.3 to -4.8 dB, no partial of
|
|
236
|
+
// the first eight more than 9 dB down (src/audio/keys/pedals.test.ts).
|
|
237
|
+
const softFc =
|
|
238
|
+
35 *
|
|
239
|
+
2 ** (5 * v * (0.4 + 0.6 * p.hardness) + 1.5 * keyPos) *
|
|
240
|
+
0.75 ** soft;
|
|
241
|
+
const softFloor = 1 - 0.75 * soft;
|
|
242
|
+
const softW = (hz: number) =>
|
|
243
|
+
soft > 0
|
|
244
|
+
? Math.max(
|
|
245
|
+
softFloor,
|
|
246
|
+
((1 + (f1 / softFc) ** 2) / (1 + (hz / softFc) ** 2)) ** soft,
|
|
247
|
+
)
|
|
248
|
+
: 1;
|
|
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;
|
|
216
255
|
this.pulse = hammerPulse(
|
|
217
|
-
Math.max(2, Math.round(
|
|
256
|
+
Math.max(2, Math.round(contactMs * 1e-3 * sr)),
|
|
218
257
|
1 + 2 * v * p.hardness,
|
|
219
258
|
);
|
|
220
259
|
const strings = stringsFor(key);
|
|
221
|
-
|
|
260
|
+
// Una corda strikes one string fewer: the unison spread narrows.
|
|
261
|
+
const spread =
|
|
262
|
+
strings === 1
|
|
263
|
+
? 0.08
|
|
264
|
+
: soft > 0
|
|
265
|
+
? p.unison * (1 - (soft * 1) / strings)
|
|
266
|
+
: p.unison;
|
|
222
267
|
const maxHz = MODE_CEILING * sr;
|
|
223
268
|
let partials = 0;
|
|
224
269
|
while (partials < 160) {
|
|
@@ -234,6 +279,8 @@ export class PianoVoice {
|
|
|
234
279
|
const shift = prepKind === 0 || prepKind === 2 ? 0.04 + 0.1 * prep() : 0;
|
|
235
280
|
const candidates: { hz: number; t60: number; u: number }[] = [];
|
|
236
281
|
let energy = 0;
|
|
282
|
+
let sum0 = 0;
|
|
283
|
+
let sumW = 0;
|
|
237
284
|
for (let n = 1; n <= partials; n += 1) {
|
|
238
285
|
let fn = n * f0 * Math.sqrt(1 + B * n * n);
|
|
239
286
|
if (shift && n > 1) fn *= 1 - shift * Math.sin(n * Math.PI * xb) ** 2;
|
|
@@ -241,8 +288,11 @@ export class PianoVoice {
|
|
|
241
288
|
if (Math.abs(shape) < 1e-3) continue;
|
|
242
289
|
const tn = 1 / (1 / T1 + (fn / 3000) ** 2 / (1.2 * p.decay));
|
|
243
290
|
const damped = prepKind === 1 && n > 3 ? 0.3 : 1;
|
|
244
|
-
const
|
|
245
|
-
energy +=
|
|
291
|
+
const u0 = shape * damped;
|
|
292
|
+
energy += u0 * u0;
|
|
293
|
+
const u = u0 * softW(fn);
|
|
294
|
+
sum0 += Math.abs(u0);
|
|
295
|
+
sumW += Math.abs(u);
|
|
246
296
|
if (n <= 12) {
|
|
247
297
|
// Prompt sound at the centre, aftersound split into a symmetric pair.
|
|
248
298
|
const d = (spread / 2) * (1 + 0.3 * (random() - 0.5));
|
|
@@ -266,11 +316,25 @@ export class PianoVoice {
|
|
|
266
316
|
t60: 0.6 + 1.2 * prep(),
|
|
267
317
|
u: 0.35,
|
|
268
318
|
});
|
|
269
|
-
|
|
270
|
-
|
|
319
|
+
let loud = v ** 1.4 * (1 - 0.4 * p.felt);
|
|
320
|
+
// Una corda's level is 30% down: half the low-pass loss (in summed mode
|
|
321
|
+
// amplitude) is made up so the extra loss stays about 1 dB.
|
|
322
|
+
if (soft > 0)
|
|
323
|
+
loud *= (1 - 0.3 * soft) * Math.sqrt(sum0 / Math.max(sumW, 1e-9));
|
|
271
324
|
const mags = candidates.map(
|
|
272
325
|
(c) => pulseMag(this.pulse, (TAU * c.hz) / sr) * Math.abs(c.u),
|
|
273
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));
|
|
274
338
|
const peak = Math.max(0, ...mags);
|
|
275
339
|
candidates.forEach((c, i) => {
|
|
276
340
|
// Below -80 dB after the hammer filter: not worth a mode.
|
|
@@ -295,7 +359,7 @@ export class PianoVoice {
|
|
|
295
359
|
2800,
|
|
296
360
|
1.5,
|
|
297
361
|
0.002,
|
|
298
|
-
0.02 * v * v * p.hardness * (1 - p.felt),
|
|
362
|
+
0.02 * v * v * p.hardness * (1 - p.felt) * (1 - 0.3 * soft),
|
|
299
363
|
sr,
|
|
300
364
|
),
|
|
301
365
|
);
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sympathetic resonance for the modelled pianos (0.6.1, keys-electric
|
|
3
|
+
* lane): a per-track bank of 36 tuned feedback combs (C2..B4), fed by the
|
|
4
|
+
* dry sum, ringing with T60 4 s while the sustain pedal is down (all
|
|
5
|
+
* dampers up) and 0.06 s when it is up. Ported from the design prototype
|
|
6
|
+
* (proto/keys/keys.ts `Sympathetic`).
|
|
7
|
+
*
|
|
8
|
+
* Gating (zero cost otherwise): it runs only on a piano track with `sym`
|
|
9
|
+
* above 0 and at least one sustain pedal `down` or `half` event; between
|
|
10
|
+
* pedal-down spans it stops processing once its energy is below -90 dB.
|
|
11
|
+
* At 44.1 kHz and above it runs at half rate (pairs averaged in, output
|
|
12
|
+
* linearly interpolated), which keeps 48 kHz near the 22.05 kHz cost; the
|
|
13
|
+
* combs top out at B4 (494 Hz), far below the halved Nyquist.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { hermite4 } from "../dsp/interp.ts";
|
|
17
|
+
|
|
18
|
+
const TAU = 2 * Math.PI;
|
|
19
|
+
const LOW = 36;
|
|
20
|
+
const HIGH = 71;
|
|
21
|
+
/** Energy below which the bank goes quiet while the pedal is up. */
|
|
22
|
+
const QUIET = 1e-9;
|
|
23
|
+
|
|
24
|
+
export class Sympathetic {
|
|
25
|
+
private readonly lines: Float64Array[] = [];
|
|
26
|
+
private readonly delay: number[] = [];
|
|
27
|
+
private readonly lp: number[] = [];
|
|
28
|
+
private readonly ws: number[] = [];
|
|
29
|
+
private readonly gUp: number[] = [];
|
|
30
|
+
private readonly gDown: number[] = [];
|
|
31
|
+
private readonly a: number;
|
|
32
|
+
/** Internal decimation: 1 at 22.05 kHz, 2 at 44.1/48 kHz. */
|
|
33
|
+
readonly factor: number;
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
hzOf: (pitch: number) => number,
|
|
37
|
+
readonly amount: number,
|
|
38
|
+
sr: number,
|
|
39
|
+
) {
|
|
40
|
+
this.factor = sr >= 40_000 ? 2 : 1;
|
|
41
|
+
const rate = sr / this.factor;
|
|
42
|
+
this.a = Math.exp((-TAU * 4000) / rate);
|
|
43
|
+
const lpDelay = this.a / (1 - this.a);
|
|
44
|
+
for (let q = LOW; q <= HIGH; q += 1) {
|
|
45
|
+
const hz = hzOf(q);
|
|
46
|
+
// An unmapped tuning degree has no string to ring.
|
|
47
|
+
if (!(hz > 0) || !Number.isFinite(hz)) continue;
|
|
48
|
+
const d = Math.max(2, rate / hz - lpDelay);
|
|
49
|
+
this.lines.push(new Float64Array(Math.ceil(d) + 8));
|
|
50
|
+
this.delay.push(d);
|
|
51
|
+
this.lp.push(0);
|
|
52
|
+
this.ws.push(0);
|
|
53
|
+
this.gDown.push(10 ** ((-3 * d) / (rate * 4)));
|
|
54
|
+
this.gUp.push(10 ** ((-3 * d) / (rate * 0.06)));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** One internal-rate step: input `x`, returns [left, right] resonance. */
|
|
59
|
+
private step(x: number, pedal: boolean, out: [number, number]): void {
|
|
60
|
+
let yl = 0;
|
|
61
|
+
let yr = 0;
|
|
62
|
+
const a = this.a;
|
|
63
|
+
for (let k = 0; k < this.lines.length; k += 1) {
|
|
64
|
+
const line = this.lines[k]!;
|
|
65
|
+
const size = line.length;
|
|
66
|
+
const w = this.ws[k]!;
|
|
67
|
+
const pos = w - this.delay[k]!;
|
|
68
|
+
const i = Math.floor(pos);
|
|
69
|
+
const at = (j: number) => line[((j % size) + size) % size]!;
|
|
70
|
+
const y = hermite4(at(i - 1), at(i), at(i + 1), at(i + 2), pos - i);
|
|
71
|
+
const f = (this.lp[k] = y + a * (this.lp[k]! - y));
|
|
72
|
+
line[w % size] = x + (pedal ? this.gDown[k]! : this.gUp[k]!) * f;
|
|
73
|
+
this.ws[k] = (w + 1) % size;
|
|
74
|
+
if (k & 1) yr += f;
|
|
75
|
+
else yl += f;
|
|
76
|
+
}
|
|
77
|
+
out[0] = yl * 0.5;
|
|
78
|
+
out[1] = yr * 0.5;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* In place: adds resonance to `left`/`right`. `down(i)` says whether the
|
|
83
|
+
* sustain pedal is down at sample `i` (read per sample pair at most).
|
|
84
|
+
*/
|
|
85
|
+
process(
|
|
86
|
+
left: Float64Array,
|
|
87
|
+
right: Float64Array,
|
|
88
|
+
down: (sample: number) => boolean,
|
|
89
|
+
): void {
|
|
90
|
+
const c = 0.02 * this.amount;
|
|
91
|
+
const out: [number, number] = [0, 0];
|
|
92
|
+
const f = this.factor;
|
|
93
|
+
let prevL = 0;
|
|
94
|
+
let prevR = 0;
|
|
95
|
+
let energy = 0;
|
|
96
|
+
for (let i = 0; i < left.length; i += f) {
|
|
97
|
+
const pedal = down(i);
|
|
98
|
+
let x = 0;
|
|
99
|
+
for (let j = 0; j < f && i + j < left.length; j += 1)
|
|
100
|
+
x += left[i + j]! + right[i + j]!;
|
|
101
|
+
x = (x / (2 * f)) * c;
|
|
102
|
+
// Dampers down and the combs quiet: skip them entirely.
|
|
103
|
+
if (!pedal && energy < QUIET) {
|
|
104
|
+
prevL = 0;
|
|
105
|
+
prevR = 0;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
this.step(x, pedal, out);
|
|
109
|
+
energy = 0.999 * energy + 0.001 * (out[0] * out[0] + out[1] * out[1]);
|
|
110
|
+
if (f === 1) {
|
|
111
|
+
left[i]! += out[0];
|
|
112
|
+
right[i]! += out[1];
|
|
113
|
+
} else {
|
|
114
|
+
// Linear interpolation back to the full rate (one sample latency
|
|
115
|
+
// at half rate, inaudible on a 4 s resonance).
|
|
116
|
+
left[i]! += 0.5 * (prevL + out[0]);
|
|
117
|
+
right[i]! += 0.5 * (prevR + out[1]);
|
|
118
|
+
if (i + 1 < left.length) {
|
|
119
|
+
left[i + 1]! += out[0];
|
|
120
|
+
right[i + 1]! += out[1];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
prevL = out[0];
|
|
124
|
+
prevR = out[1];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
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;
|
package/src/audio/live-worker.ts
CHANGED
|
@@ -18,6 +18,7 @@ export type LiveFullRequest = Readonly<{
|
|
|
18
18
|
velocity: number;
|
|
19
19
|
seconds: number;
|
|
20
20
|
tick?: number;
|
|
21
|
+
clock?: number;
|
|
21
22
|
samples?: SampleBank;
|
|
22
23
|
}>;
|
|
23
24
|
|
|
@@ -32,7 +33,7 @@ declare const self: Worker;
|
|
|
32
33
|
let synth: LiveSynth | undefined;
|
|
33
34
|
|
|
34
35
|
self.onmessage = (event: MessageEvent<LiveFullRequest>) => {
|
|
35
|
-
const { id, sampleRate, score, samples, tick, ...note } = event.data;
|
|
36
|
+
const { id, sampleRate, score, samples, tick, clock, ...note } = event.data;
|
|
36
37
|
try {
|
|
37
38
|
if (synth?.rate !== sampleRate) synth = new LiveSynth(sampleRate);
|
|
38
39
|
const pcm = synth.render({
|
|
@@ -41,6 +42,7 @@ self.onmessage = (event: MessageEvent<LiveFullRequest>) => {
|
|
|
41
42
|
full: true,
|
|
42
43
|
...(samples ? { samples } : {}),
|
|
43
44
|
...(tick === undefined ? {} : { tick }),
|
|
45
|
+
...(clock === undefined ? {} : { clock }),
|
|
44
46
|
});
|
|
45
47
|
const reply: LiveFullReply = pcm ? { id, pcm } : { id };
|
|
46
48
|
self.postMessage(reply);
|