@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
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A bowed string (0.6.1 lane f061-bowed), ported from the 0.6 strings
|
|
3
|
+
* prototype's `BowedString`: the STK `Bowed` topology (Cook and Scavone)
|
|
4
|
+
* with a neck and a bridge delay line split at the bow point, the bow
|
|
5
|
+
* table `(|dv| x slope + 0.75)^-4` and a one-pole bridge loss, plus the
|
|
6
|
+
* review fixes:
|
|
7
|
+
*
|
|
8
|
+
* - Exact tuning: the neck line carries a first-order Thiran fraction and
|
|
9
|
+
* the loop length counts the loss filter's phase delay (the junction adds
|
|
10
|
+
* no measurable lag, so the prototype's bow-lag calibration is the
|
|
11
|
+
* frozen constant 0, not module state).
|
|
12
|
+
* - Playable beta: `pos` maps onto 0.08..0.4 of the loop, and the bridge
|
|
13
|
+
* segment never falls under 4 samples. A period under 24 samples runs
|
|
14
|
+
* the loop at twice the rate through the shared half-band decimator.
|
|
15
|
+
* - Schelleng window: bow force follows `pressure` between the minimum
|
|
16
|
+
* (proportional to v / beta^2) and maximum (proportional to v / beta)
|
|
17
|
+
* force for the bow speed and beta (Schelleng 1973; Woodhouse 2014), so
|
|
18
|
+
* every pressure stays in Helmholtz motion.
|
|
19
|
+
* - Dynamics: `dyn` x velocity drives bow speed over 8:1, a per-voice gain
|
|
20
|
+
* law (28 dB across the range) and a tone filter whose corner rises with
|
|
21
|
+
* force and falls with beta and the mute (`sord`).
|
|
22
|
+
* - Slur: `slurTo` retunes over 7 ms and keeps the bow (no new attack).
|
|
23
|
+
* - Tremolo: `tremhz` reverses the bow that many times a second, with a
|
|
24
|
+
* seeded per-voice rate jitter. At each reversal the bow force eases off
|
|
25
|
+
* with the bow speed (the friction curve narrows as dir^2), so the
|
|
26
|
+
* string rings through the turn instead of sticking to a slow bow and
|
|
27
|
+
* flattening; the pitch lock averages each stroke's middle (one update
|
|
28
|
+
* per stroke) instead of chasing the turns' transient periods.
|
|
29
|
+
*/
|
|
30
|
+
import { Decimate2 } from "../dsp/oversample.ts";
|
|
31
|
+
import { thiranFor } from "../dsp/interp.ts";
|
|
32
|
+
import { unit } from "../dsp/rng.ts";
|
|
33
|
+
import { clamp, designLoss, onePoleDelay } from "./loop.ts";
|
|
34
|
+
import { C4 } from "./pluck.ts";
|
|
35
|
+
|
|
36
|
+
/** Control-rate step (base-rate samples), the same as dawg's lanes. */
|
|
37
|
+
const CONTROL = 32;
|
|
38
|
+
/** Playable bow positions (fraction of the loop from the bridge). */
|
|
39
|
+
export const BETA_MIN = 0.08;
|
|
40
|
+
export const BETA_MAX = 0.4;
|
|
41
|
+
/** Shortest bridge segment in loop samples. */
|
|
42
|
+
export const BRIDGE_FLOOR = 4;
|
|
43
|
+
/** Periods shorter than this (base-rate samples) run at 2x. */
|
|
44
|
+
export const OVERSAMPLE_BELOW = 24;
|
|
45
|
+
/** Slur retune time in seconds (the review asks for 5-10 ms). */
|
|
46
|
+
export const SLUR_SECONDS = 0.007;
|
|
47
|
+
/** Widest slur in semitones (the loop buffer holds two octaves down). */
|
|
48
|
+
export const SLUR_RANGE = 24;
|
|
49
|
+
/**
|
|
50
|
+
* Pitch lock: the stick-slip corner travels the loop faster than the loss
|
|
51
|
+
* filter's phase delay at f0 says (its high partials see less delay), so
|
|
52
|
+
* an open loop plays up to 15 cents sharp. Each period the lock measures
|
|
53
|
+
* the band-passed bridge velocity's zero-crossing period and moves the
|
|
54
|
+
* loop length by LOCK_GAIN of the error, inside +-LOCK_RANGE. Its state is
|
|
55
|
+
* per voice, so renders stay order-independent.
|
|
56
|
+
*/
|
|
57
|
+
const LOCK_GAIN = 0.25;
|
|
58
|
+
const LOCK_RANGE = 0.03;
|
|
59
|
+
/** Tremolo: |sin| of the reversal phase past which a stroke is steady. */
|
|
60
|
+
const STROKE_STEADY = 0.7;
|
|
61
|
+
/** Tremolo: lock step per stroke, on the stroke's mean period error. */
|
|
62
|
+
const STROKE_LOCK_GAIN = 0.2;
|
|
63
|
+
/** Tremolo: the friction slope widens at most 1 / FLOOR^2 at a turn. */
|
|
64
|
+
const TURN_FORCE_FLOOR = 0.05;
|
|
65
|
+
/** Speed ratio between dyn 0 and dyn 1 (the review's 8:1). */
|
|
66
|
+
const SPEED_RATIO = 8;
|
|
67
|
+
/** Extra gain law across dyn, in dB (with the 18 dB speed law: 28 dB). */
|
|
68
|
+
const GAIN_LAW_DB = 10;
|
|
69
|
+
/**
|
|
70
|
+
* Schelleng window, measured. In the bow table's units the friction
|
|
71
|
+
* capacity over bow speed is r = 0.25 / (slope x v); Schelleng's minimum
|
|
72
|
+
* and maximum bow force bound r from below and above, and both move with
|
|
73
|
+
* beta and the loop length (Schelleng 1973; Woodhouse 2014). Rather than
|
|
74
|
+
* fit one constant (the prototype's guard, which the review showed stalls
|
|
75
|
+
* the string over part of the range), the window is a frozen table. Each
|
|
76
|
+
* cell covers a log2 loop length step of 0.25 from 4.5 (rows) and a beta
|
|
77
|
+
* step of 0.02 from 0.08 (columns); it holds the steps i of
|
|
78
|
+
* r = 0.01 x 2^(i/4) (one character each, i = code - 48) that stay
|
|
79
|
+
* f0-dominant at velocity 0.15, 0.5 and 0.95, at 22.05 and 48 kHz, at all
|
|
80
|
+
* four cell corners.
|
|
81
|
+
* `pressure` moves log-linearly inside the window less a margin, so every
|
|
82
|
+
* pressure stays in Helmholtz motion (bow.test.ts sweeps it).
|
|
83
|
+
*/
|
|
84
|
+
const WINDOW_LP0 = 4.5;
|
|
85
|
+
const WINDOW_LO: readonly string[] = [
|
|
86
|
+
"CLLDC??<;:988GG7",
|
|
87
|
+
"CCCJJ??>>;988887",
|
|
88
|
+
"CCDJJEE>>;998888",
|
|
89
|
+
"EELLEEE>>:999988",
|
|
90
|
+
"EEMMEAA>>::99999",
|
|
91
|
+
"FCMMEAA?>;:::999",
|
|
92
|
+
"FFFFBBB@=;:::::9",
|
|
93
|
+
"HFGGBBBAA;;;::::",
|
|
94
|
+
"HFGGECCCB<<<;;;;",
|
|
95
|
+
"HHHHFCDDB==<<<<;",
|
|
96
|
+
"JHHHGCDDCB===<<<",
|
|
97
|
+
"JIIIGDDDDC====<<",
|
|
98
|
+
"JKKKHEEEDD>=====",
|
|
99
|
+
"KKKKHGEEED>>====",
|
|
100
|
+
"KLLJIGEEEE>>>===",
|
|
101
|
+
"MMMLJHFFFF>>>===",
|
|
102
|
+
"MMMLJIFGGF>>>>==",
|
|
103
|
+
"KKKKJIGGGF?>>>==",
|
|
104
|
+
"JJJJJIHOOH?>>>>=",
|
|
105
|
+
"HHHHHHHOOI?>>>>=",
|
|
106
|
+
"MMMMMMMMMJD>>>>=",
|
|
107
|
+
"MMMMMMMMMKK>>>>=",
|
|
108
|
+
"KKKKKKKKKKK>>>>=",
|
|
109
|
+
">>>>>>>>>>>>>>>=",
|
|
110
|
+
];
|
|
111
|
+
const WINDOW_HI: readonly string[] = [
|
|
112
|
+
"KKKKIHEEEICCCFGA",
|
|
113
|
+
"IIIIIIEEEFCCCGGA",
|
|
114
|
+
"IIIIKKHHHFFFFHHA",
|
|
115
|
+
"XLIIHHIIJLJJHHHB",
|
|
116
|
+
"NLLNHHHHLLLLIIMB",
|
|
117
|
+
"NNPMMSHHKXLHHIRC",
|
|
118
|
+
"TXXMMKJJKXLHHKXC",
|
|
119
|
+
"TVWRRKJJXXXXLLXD",
|
|
120
|
+
"TVWXXJJKXXXXXXXD",
|
|
121
|
+
"TVWXXJJKXXXXXXFD",
|
|
122
|
+
"TVWXXJJLXXXXXKFD",
|
|
123
|
+
"TVVWXJJLXXXXXJFE",
|
|
124
|
+
"SUVWWLLMXXXXXJFE",
|
|
125
|
+
"SUVVWXXXXXXXXJFE",
|
|
126
|
+
"STUUVWWXXXXXXJFE",
|
|
127
|
+
"TTTTUVWXXXXXXJFE",
|
|
128
|
+
"TTTTTUVWXXXXXIFE",
|
|
129
|
+
"TTTTTTUVXXXXXIFE",
|
|
130
|
+
"TTTTTTTTXXXXXIFE",
|
|
131
|
+
"RRRRRRRRTXXXXIFF",
|
|
132
|
+
"OOOOOOOOOSXXXJFF",
|
|
133
|
+
"OOOOOOOOOSXXXKFF",
|
|
134
|
+
"XXXXXXXXXXXXXLGF",
|
|
135
|
+
"XXXXXXXXXXXXXXGG",
|
|
136
|
+
];
|
|
137
|
+
/** Widest window used (steps of 2^(1/4)): pressure spans 8x at most. */
|
|
138
|
+
const WINDOW_SPAN = 10;
|
|
139
|
+
/** Margin inside each measured edge, in steps. */
|
|
140
|
+
const WINDOW_MARGIN = 3;
|
|
141
|
+
|
|
142
|
+
function cell(table: readonly string[], lp: number, beta: number): number {
|
|
143
|
+
const y = clamp(Math.floor((lp - WINDOW_LP0) / 0.25), 0, table.length - 1);
|
|
144
|
+
const row = table[y]!;
|
|
145
|
+
const x = clamp(
|
|
146
|
+
Math.floor((beta - BETA_MIN + 1e-9) / 0.02),
|
|
147
|
+
0,
|
|
148
|
+
row.length - 1,
|
|
149
|
+
);
|
|
150
|
+
return row.charCodeAt(x) - 48;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Resolved bowed-string parameters for one note. */
|
|
154
|
+
export type BowSpec = Readonly<{
|
|
155
|
+
/** Free-string T60 (s) at C4 with the bow lifted. */
|
|
156
|
+
decay: number;
|
|
157
|
+
/** Decay key tracking: T60 x (f/C4)^-track. */
|
|
158
|
+
track: number;
|
|
159
|
+
/** 0..1 high-frequency loss. */
|
|
160
|
+
damp: number;
|
|
161
|
+
/** Bow position, 0.01..0.5 asked; played inside BETA_MIN..BETA_MAX. */
|
|
162
|
+
pos: number;
|
|
163
|
+
/** 0..1 of the Schelleng window. */
|
|
164
|
+
pressure: number;
|
|
165
|
+
/** 0..1 bow speed at full dynamics. */
|
|
166
|
+
speed: number;
|
|
167
|
+
/** Seconds of bow-speed ramp at the start of a stroke. */
|
|
168
|
+
attack: number;
|
|
169
|
+
/** Seconds for the bow to lift after note-off. */
|
|
170
|
+
release: number;
|
|
171
|
+
/** Bow reversals per second (0 = a sustained stroke). */
|
|
172
|
+
tremhz: number;
|
|
173
|
+
/** 0..1 practice mute. */
|
|
174
|
+
sord: number;
|
|
175
|
+
}>;
|
|
176
|
+
|
|
177
|
+
/** The continuously read (every 32 samples) bow controls. */
|
|
178
|
+
export type BowControl = Readonly<{
|
|
179
|
+
pressure: number;
|
|
180
|
+
speed: number;
|
|
181
|
+
sord: number;
|
|
182
|
+
dyn: number;
|
|
183
|
+
}>;
|
|
184
|
+
|
|
185
|
+
export type BowNote = Readonly<{
|
|
186
|
+
hz: number;
|
|
187
|
+
/** 0..1 after the velocity curve and the preset's sensitivity. */
|
|
188
|
+
velocity: number;
|
|
189
|
+
/** Seconds to note-off. */
|
|
190
|
+
hold: number;
|
|
191
|
+
seed: number;
|
|
192
|
+
/** The note's own pitch curve in cents at `t` seconds from its start. */
|
|
193
|
+
cents?: (t: number) => number;
|
|
194
|
+
/** Vibrato in cents at `t` seconds from the bow's start (kept by a slur). */
|
|
195
|
+
vibrato?: (t: number) => number;
|
|
196
|
+
/** Lane values at `t` seconds from the bow's start (absent: the spec). */
|
|
197
|
+
control?: (t: number) => BowControl;
|
|
198
|
+
/** Calibration only: a fixed friction capacity r (slope = 0.25 / (r v)). */
|
|
199
|
+
capacity?: number;
|
|
200
|
+
}>;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Playable bow positions, measured (0.6.1 review). Helmholtz motion holds
|
|
204
|
+
* only on some bow positions, and which ones depends on the loop length
|
|
205
|
+
* and on the loop's own loss: a long, slowly decaying loop (a cello or
|
|
206
|
+
* double bass, `ring` 0.9-1.2) has holes in beta where the string flips
|
|
207
|
+
* to its octave or goes raucous at every pressure. Each cell covers a
|
|
208
|
+
* log2 loop length step of 0.25 from 5 (rows) and a loop T60 interval
|
|
209
|
+
* between BETA_T60S (4 base-36 characters per interval); it holds a
|
|
210
|
+
* bitmask of the betas 0.08 + 0.02 k that stay f0-dominant at pressure 0,
|
|
211
|
+
* 0.5 and 1, speed 0.1 and 1, velocity 0.2 and 1, at 22.05 and 48 kHz,
|
|
212
|
+
* at all four cell corners (a few cells with none hold the least bad).
|
|
213
|
+
*/
|
|
214
|
+
const BETA_LP0 = 5;
|
|
215
|
+
const BETA_T60S: readonly number[] = [0.4, 0.6, 1.0, 1.5, 2.2, 3.2, 4.5];
|
|
216
|
+
const BETA_SAFE: readonly string[] = [
|
|
217
|
+
"2t4v2sjj1dvz1dvz1dvz118v",
|
|
218
|
+
"2sqn2sqn1ekf1ekf1ehr1e3j",
|
|
219
|
+
"2sql2sql1ejh1ejj1ea71dvz",
|
|
220
|
+
"2t4t2sjg1dy40ov20olq0olq",
|
|
221
|
+
"2t4v2sji0ony0ota0olq0olq",
|
|
222
|
+
"2t4v2sqn0ov30osv0olr0olr",
|
|
223
|
+
"2t4v2sqm1e660omn0oj30oj3",
|
|
224
|
+
"2t4u2sqm1e260oj30oi70oi7",
|
|
225
|
+
"2t4u2t4u0orz0oi70olr0olr",
|
|
226
|
+
"2t4s2t4u0onj0olr0olr0omn",
|
|
227
|
+
"2t4o2t4u0oi70oi70olr0olr",
|
|
228
|
+
"2t4g2t4s0nq70npr0nta0npq",
|
|
229
|
+
"1k2o2t0w1dem0npr0npq0npq",
|
|
230
|
+
"00ow2t0g2rks0npq0npr0npq",
|
|
231
|
+
"00lc2szk2rmg1czy0npq0npq",
|
|
232
|
+
"06bk0k5c2se82rk01czq1czy",
|
|
233
|
+
"06bk00e82shs2rk02rk61czq",
|
|
234
|
+
"06bk0cn42shs2sg02rnm1d3a",
|
|
235
|
+
"06bk06bk21hc2sg02rnk2rnq",
|
|
236
|
+
"0cn40cn40cn42scg2scg2scg",
|
|
237
|
+
"0cn40cn40cn42rk02rk02rk0",
|
|
238
|
+
"0cn40cn42pz42rk02rk02rk0",
|
|
239
|
+
"0cn40cn42pz42rk02rk02rk0",
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
function safeBetas(lp: number, t60: number): number {
|
|
243
|
+
const y = clamp(Math.floor((lp - BETA_LP0) / 0.25), 0, BETA_SAFE.length - 1);
|
|
244
|
+
let x = 0;
|
|
245
|
+
while (x < BETA_T60S.length - 2 && t60 >= BETA_T60S[x + 1]!) x += 1;
|
|
246
|
+
return parseInt(BETA_SAFE[y]!.slice(4 * x, 4 * x + 4), 36);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Beta actually played for `pos` on a loop of `period` samples whose T60
|
|
251
|
+
* is `t60` seconds: inside 0.08..0.4, at least 4 samples of bridge
|
|
252
|
+
* segment, then the measured playable beta nearest to it (the review's
|
|
253
|
+
* "playable beta per register").
|
|
254
|
+
*/
|
|
255
|
+
export function playableBeta(pos: number, period: number, t60 = 1): number {
|
|
256
|
+
const floor = Math.min(BETA_MAX, BRIDGE_FLOOR / Math.max(1, period));
|
|
257
|
+
const want = clamp(pos, Math.max(BETA_MIN, floor), BETA_MAX);
|
|
258
|
+
const raw = safeBetas(Math.log2(Math.max(1, period)), t60);
|
|
259
|
+
// Prefer a beta whose neighbours are playable too: the cells are
|
|
260
|
+
// measured at their corners, and an isolated safe beta is a thin margin.
|
|
261
|
+
const inner = raw & (raw << 1) & (raw >> 1);
|
|
262
|
+
const mask = inner || raw;
|
|
263
|
+
let best = want;
|
|
264
|
+
let gap = Infinity;
|
|
265
|
+
for (let k = 0; k < 17; k += 1) {
|
|
266
|
+
if (!(mask & (1 << k))) continue;
|
|
267
|
+
const beta = BETA_MIN + 0.02 * k;
|
|
268
|
+
if (beta < floor - 1e-9) continue;
|
|
269
|
+
const d = Math.abs(beta - want);
|
|
270
|
+
if (d < gap - 1e-9) {
|
|
271
|
+
gap = d;
|
|
272
|
+
best = beta;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return best;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Whether a bowed note at `hz` runs at twice the rate `sr`. */
|
|
279
|
+
export function bowOversample(hz: number, sr: number): 1 | 2 {
|
|
280
|
+
return sr / hz < OVERSAMPLE_BELOW ? 2 : 1;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Bow-table slope for `pressure` inside the Schelleng window at `beta` on
|
|
285
|
+
* a loop of `period` samples, for bow speed `v`. Pressure 0 sits near the
|
|
286
|
+
* minimum bow force (light, flautando), 1 near the maximum (gritty).
|
|
287
|
+
*/
|
|
288
|
+
export function schellengSlope(
|
|
289
|
+
pressure: number,
|
|
290
|
+
beta: number,
|
|
291
|
+
v: number,
|
|
292
|
+
period: number,
|
|
293
|
+
): number {
|
|
294
|
+
const lp = Math.log2(Math.max(1, period));
|
|
295
|
+
const lo = cell(WINDOW_LO, lp, beta);
|
|
296
|
+
const hi = Math.min(cell(WINDOW_HI, lp, beta), lo + WINDOW_SPAN);
|
|
297
|
+
const a = lo + WINDOW_MARGIN;
|
|
298
|
+
const b = hi - WINDOW_MARGIN;
|
|
299
|
+
const step = b > a ? a + (b - a) * clamp(pressure, 0, 1) : (lo + hi) / 2;
|
|
300
|
+
const r = 0.01 * 2 ** (step / 4);
|
|
301
|
+
return 0.25 / (r * Math.max(1e-6, v));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** One bowed string, streamed in blocks. */
|
|
305
|
+
export class BowedString {
|
|
306
|
+
private readonly os: 1 | 2;
|
|
307
|
+
private readonly rate: number;
|
|
308
|
+
private readonly nb: Float64Array;
|
|
309
|
+
private readonly bb: Float64Array;
|
|
310
|
+
private readonly mask: number;
|
|
311
|
+
private nw = 0;
|
|
312
|
+
private bw = 0;
|
|
313
|
+
private Nn = 1;
|
|
314
|
+
private Nb = 1;
|
|
315
|
+
private a = 0;
|
|
316
|
+
private tx1 = 0;
|
|
317
|
+
private ty1 = 0;
|
|
318
|
+
private sg: number;
|
|
319
|
+
private sp: number;
|
|
320
|
+
private sy = 0;
|
|
321
|
+
private beta: number;
|
|
322
|
+
private fromBeta = 0;
|
|
323
|
+
private toBeta = 0;
|
|
324
|
+
private slurLoss?: { g: number; p: number };
|
|
325
|
+
private readonly f0: number;
|
|
326
|
+
private hz: number;
|
|
327
|
+
private fromHz: number;
|
|
328
|
+
private slurAt = -1;
|
|
329
|
+
private slurLen = 1;
|
|
330
|
+
private cents?: (t: number) => number;
|
|
331
|
+
private centsAt = 0;
|
|
332
|
+
private readonly vibrato?: (t: number) => number;
|
|
333
|
+
private readonly decim?: Decimate2;
|
|
334
|
+
private readonly tremRate: number;
|
|
335
|
+
private readonly tremPhase: number;
|
|
336
|
+
/** Tremolo: between turns, and that stroke's lock measurements. */
|
|
337
|
+
private steady = true;
|
|
338
|
+
private strokeErr = 0;
|
|
339
|
+
private strokeCount = 0;
|
|
340
|
+
private strokeSkip = 0;
|
|
341
|
+
/** Internal-rate sample count. */
|
|
342
|
+
private n = 0;
|
|
343
|
+
private off: number;
|
|
344
|
+
private quiet = 0;
|
|
345
|
+
// Control-rate state.
|
|
346
|
+
private slope = 3;
|
|
347
|
+
private vtarget = 0.1;
|
|
348
|
+
private gain = 1;
|
|
349
|
+
private toneA = 1;
|
|
350
|
+
private toneY = 0;
|
|
351
|
+
// Pitch lock: loop length x (1 + lock); band-pass at the target pitch.
|
|
352
|
+
private lock = 0;
|
|
353
|
+
private target = 1;
|
|
354
|
+
private bpB = 0;
|
|
355
|
+
private bpA1 = 0;
|
|
356
|
+
private bpA2 = 0;
|
|
357
|
+
private bpX1 = 0;
|
|
358
|
+
private bpX2 = 0;
|
|
359
|
+
private bpY1 = 0;
|
|
360
|
+
private bpY2 = 0;
|
|
361
|
+
private lastCross = -1;
|
|
362
|
+
// Output DC blocker (10 Hz): a lifted loop with near-unity DC gain keeps
|
|
363
|
+
// a tiny offset that would otherwise hold the voice open.
|
|
364
|
+
private dcR = 1;
|
|
365
|
+
private dcX = 0;
|
|
366
|
+
private dcY = 0;
|
|
367
|
+
done = false;
|
|
368
|
+
|
|
369
|
+
constructor(
|
|
370
|
+
readonly spec: BowSpec,
|
|
371
|
+
readonly note: BowNote,
|
|
372
|
+
readonly sr: number,
|
|
373
|
+
readonly maxSeconds = 12,
|
|
374
|
+
) {
|
|
375
|
+
const f0 = note.hz;
|
|
376
|
+
this.f0 = f0;
|
|
377
|
+
this.os = bowOversample(f0, sr);
|
|
378
|
+
this.rate = sr * this.os;
|
|
379
|
+
const loss = this.loss(f0);
|
|
380
|
+
this.sg = loss.g;
|
|
381
|
+
this.sp = loss.p;
|
|
382
|
+
// Room for a two-octave slur down (SLUR_RANGE) and a wide vibrato.
|
|
383
|
+
let size = 16;
|
|
384
|
+
while (size < (this.rate / f0) * 4.5 + 16) size <<= 1;
|
|
385
|
+
this.nb = new Float64Array(size);
|
|
386
|
+
this.bb = new Float64Array(size);
|
|
387
|
+
this.mask = size - 1;
|
|
388
|
+
if (this.os === 2) this.decim = new Decimate2();
|
|
389
|
+
this.hz = f0;
|
|
390
|
+
this.fromHz = f0;
|
|
391
|
+
this.beta = playableBeta(spec.pos, this.rate / f0, this.t60(f0));
|
|
392
|
+
if (note.cents) this.cents = note.cents;
|
|
393
|
+
if (note.vibrato) this.vibrato = note.vibrato;
|
|
394
|
+
this.tremRate = spec.tremhz * (1 + 0.08 * (unit(note.seed, 0, 7) - 0.5));
|
|
395
|
+
this.tremPhase = unit(note.seed, 0, 8);
|
|
396
|
+
this.off = Math.round(note.hold * this.rate);
|
|
397
|
+
this.dcR = 1 - (2 * Math.PI * 10) / sr;
|
|
398
|
+
this.control(0);
|
|
399
|
+
this.retune(f0, true);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Loop T60 (s) at `hz`: the free-string decay with key tracking. */
|
|
403
|
+
private t60(hz: number): number {
|
|
404
|
+
return this.spec.decay * (hz / C4) ** -this.spec.track;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** The loop's loss filter at `hz` (sordino darkens it). */
|
|
408
|
+
private loss(hz: number): { g: number; p: number } {
|
|
409
|
+
const spec = this.spec;
|
|
410
|
+
const t0 = this.t60(hz);
|
|
411
|
+
const damp = clamp(spec.damp + (1 - spec.damp) * 0.5 * spec.sord, 0, 1);
|
|
412
|
+
return designLoss(
|
|
413
|
+
hz,
|
|
414
|
+
t0,
|
|
415
|
+
Math.max(3000, 2 * hz),
|
|
416
|
+
t0 / (1 + 99 * damp * damp),
|
|
417
|
+
this.rate,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Whether this string can slur to `hz` without a new stroke: within
|
|
423
|
+
* SLUR_RANGE semitones of its first note and at the same internal rate
|
|
424
|
+
* (a slur up into the oversampled register restarts the bow instead).
|
|
425
|
+
*/
|
|
426
|
+
canSlurTo(hz: number): boolean {
|
|
427
|
+
const semis = Math.abs(12 * Math.log2(hz / this.f0));
|
|
428
|
+
return semis <= SLUR_RANGE && bowOversample(hz, this.sr) === this.os;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Note-off in base-rate samples from the string's start. */
|
|
432
|
+
get offAt(): number {
|
|
433
|
+
return Math.round(this.off / this.os);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Moves note-off to `at` base-rate samples (restrike, steal). */
|
|
437
|
+
releaseAt(at: number): void {
|
|
438
|
+
const n = at * this.os;
|
|
439
|
+
if (n < this.off) this.off = Math.max(this.n, n);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Slurs to `hz` at `at` base-rate samples from the start: the pitch moves
|
|
444
|
+
* over SLUR_SECONDS, the bow keeps going, and note-off moves to `hold`
|
|
445
|
+
* seconds after `at`. The new note's own pitch curve replaces the old.
|
|
446
|
+
*/
|
|
447
|
+
slurTo(
|
|
448
|
+
hz: number,
|
|
449
|
+
at: number,
|
|
450
|
+
hold: number,
|
|
451
|
+
cents?: (t: number) => number,
|
|
452
|
+
): void {
|
|
453
|
+
const n = Math.max(this.n, at * this.os);
|
|
454
|
+
this.fromHz = this.currentHz();
|
|
455
|
+
this.hz = hz;
|
|
456
|
+
this.slurAt = n;
|
|
457
|
+
this.slurLen = Math.max(1, Math.round(SLUR_SECONDS * this.rate));
|
|
458
|
+
this.off = n + Math.round(hold * this.rate);
|
|
459
|
+
this.cents = cents;
|
|
460
|
+
this.centsAt = n;
|
|
461
|
+
// The bow point, its gain and Schelleng slope, and the loop loss follow
|
|
462
|
+
// the new pitch: the bridge tap glides with the pitch over the slur.
|
|
463
|
+
this.fromBeta = this.beta;
|
|
464
|
+
this.toBeta = playableBeta(this.spec.pos, this.rate / hz, this.t60(hz));
|
|
465
|
+
this.slurLoss = this.loss(hz);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** The loop's base pitch now (inside a slur, the glide position). */
|
|
469
|
+
private currentHz(): number {
|
|
470
|
+
if (this.slurAt < 0 || this.n >= this.slurAt + this.slurLen) return this.hz;
|
|
471
|
+
if (this.n < this.slurAt) return this.fromHz;
|
|
472
|
+
const k = (this.n - this.slurAt) / this.slurLen;
|
|
473
|
+
return this.fromHz * (this.hz / this.fromHz) ** k;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Loop delay for `hz`: bridge segment at beta, Thiran on the neck. */
|
|
477
|
+
private retune(hz: number, first = false): void {
|
|
478
|
+
const rate = this.rate;
|
|
479
|
+
const w0 = (2 * Math.PI * hz) / rate;
|
|
480
|
+
const total = (rate / hz) * (1 + this.lock) - onePoleDelay(this.sp, w0);
|
|
481
|
+
this.target = rate / hz;
|
|
482
|
+
// Band-pass (Q 3) at the target pitch for the lock's period detector.
|
|
483
|
+
const alpha = Math.sin(w0) / 6;
|
|
484
|
+
const a0 = 1 + alpha;
|
|
485
|
+
this.bpB = alpha / a0;
|
|
486
|
+
this.bpA1 = (-2 * Math.cos(w0)) / a0;
|
|
487
|
+
this.bpA2 = (1 - alpha) / a0;
|
|
488
|
+
if (first) {
|
|
489
|
+
this.beta = playableBeta(this.spec.pos, total, this.t60(hz));
|
|
490
|
+
this.Nb = Math.max(1, Math.round(this.beta * total));
|
|
491
|
+
} else if (this.slurAt >= 0 && this.n >= this.slurAt) {
|
|
492
|
+
if (this.slurLoss) {
|
|
493
|
+
this.sg = this.slurLoss.g;
|
|
494
|
+
this.sp = this.slurLoss.p;
|
|
495
|
+
this.slurLoss = undefined;
|
|
496
|
+
this.lock = 0;
|
|
497
|
+
}
|
|
498
|
+
const k = Math.min(1, (this.n - this.slurAt) / this.slurLen);
|
|
499
|
+
this.beta = this.fromBeta + (this.toBeta - this.fromBeta) * k;
|
|
500
|
+
this.Nb = Math.max(1, Math.round(this.beta * total));
|
|
501
|
+
}
|
|
502
|
+
const rest = total - this.Nb;
|
|
503
|
+
const Nn = Math.max(1, Math.floor(rest - 0.5));
|
|
504
|
+
const D = clamp(rest - Nn, 0.5, 1.5);
|
|
505
|
+
const a = thiranFor(D, w0);
|
|
506
|
+
if (!first && Nn !== this.Nn) {
|
|
507
|
+
const m = this.mask;
|
|
508
|
+
this.tx1 = this.nb[(this.nw - 1 - Nn) & m]!;
|
|
509
|
+
const pos = this.nw - 1 - Nn - D;
|
|
510
|
+
const i0 = Math.floor(pos);
|
|
511
|
+
const fr = pos - i0;
|
|
512
|
+
this.ty1 = this.nb[i0 & m]! * (1 - fr) + this.nb[(i0 + 1) & m]! * fr;
|
|
513
|
+
}
|
|
514
|
+
this.Nn = Nn;
|
|
515
|
+
this.a = a;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** Reads the lanes and sets bow speed, slope, gain and tone. */
|
|
519
|
+
private control(t: number): void {
|
|
520
|
+
const spec = this.spec;
|
|
521
|
+
const c = this.note.control?.(t);
|
|
522
|
+
const pressure = c ? c.pressure : spec.pressure;
|
|
523
|
+
const speed = c ? c.speed : spec.speed;
|
|
524
|
+
const sord = clamp(c ? c.sord : spec.sord, 0, 1);
|
|
525
|
+
const d = clamp(this.note.velocity * (c ? c.dyn : 1), 0, 1);
|
|
526
|
+
const v =
|
|
527
|
+
(0.05 + 0.25 * clamp(speed, 0, 1)) * SPEED_RATIO ** (d - 1) * 0.9 + 0.005;
|
|
528
|
+
this.vtarget = v;
|
|
529
|
+
this.slope =
|
|
530
|
+
this.note.capacity !== undefined
|
|
531
|
+
? 0.25 / (this.note.capacity * v)
|
|
532
|
+
: schellengSlope(pressure, this.beta, v, this.rate / this.hz);
|
|
533
|
+
// Amplitude at the bridge is proportional to v / beta: undo the beta.
|
|
534
|
+
this.gain =
|
|
535
|
+
1.6 *
|
|
536
|
+
(this.beta / 0.12) *
|
|
537
|
+
10 ** ((-GAIN_LAW_DB * (1 - d)) / 20) *
|
|
538
|
+
Math.min(1, d / 0.05) *
|
|
539
|
+
(1 - 0.45 * sord);
|
|
540
|
+
const fc = Math.min(
|
|
541
|
+
0.45 * this.sr,
|
|
542
|
+
1400 *
|
|
543
|
+
2 ** (3 * d) *
|
|
544
|
+
(0.6 + 0.8 * clamp(pressure, 0, 1)) *
|
|
545
|
+
Math.sqrt(0.12 / this.beta) *
|
|
546
|
+
(1 - 0.7 * sord),
|
|
547
|
+
);
|
|
548
|
+
this.toneA = 1 - Math.exp((-2 * Math.PI * fc) / this.sr);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** One internal-rate sample of the bridge velocity. */
|
|
552
|
+
private tick(attack: number, lift: number): number {
|
|
553
|
+
const { nb, bb, mask } = this;
|
|
554
|
+
const n = this.n;
|
|
555
|
+
const off = this.off;
|
|
556
|
+
const on = n < off;
|
|
557
|
+
const env = on
|
|
558
|
+
? Math.min(1, (n + 1) / attack)
|
|
559
|
+
: Math.max(0, 1 - (n - off) / lift);
|
|
560
|
+
let dir = 1;
|
|
561
|
+
if (this.tremRate > 0) {
|
|
562
|
+
const s = Math.sin(
|
|
563
|
+
Math.PI * (this.tremRate * (n / this.rate) + this.tremPhase),
|
|
564
|
+
);
|
|
565
|
+
dir = Math.tanh(4 * s);
|
|
566
|
+
const steady = Math.abs(s) > STROKE_STEADY;
|
|
567
|
+
if (steady !== this.steady) {
|
|
568
|
+
// Leaving a stroke's middle: one lock step on its mean error.
|
|
569
|
+
if (this.steady && this.strokeCount > 0)
|
|
570
|
+
this.lock = clamp(
|
|
571
|
+
this.lock - (STROKE_LOCK_GAIN * this.strokeErr) / this.strokeCount,
|
|
572
|
+
-LOCK_RANGE,
|
|
573
|
+
LOCK_RANGE,
|
|
574
|
+
);
|
|
575
|
+
this.steady = steady;
|
|
576
|
+
this.strokeErr = 0;
|
|
577
|
+
this.strokeCount = 0;
|
|
578
|
+
this.strokeSkip = 0;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
// Note-off lifts the bow: the normal force falls over `release` (the
|
|
582
|
+
// friction curve narrows, STK's bow pressure), with the bow still
|
|
583
|
+
// moving, so the string leaves Helmholtz motion swinging and rings
|
|
584
|
+
// freely (`ring`). Fading the contact instead would pass through a
|
|
585
|
+
// half-reflecting junction that brakes the string.
|
|
586
|
+
const bowVel = this.vtarget * (on ? env : 1) * dir;
|
|
587
|
+
const contact = on || env > 0 ? 1 : 0;
|
|
588
|
+
let slope = on ? this.slope : this.slope / Math.max(1e-3, env * env);
|
|
589
|
+
// A turning bow presses less: the friction curve narrows with speed.
|
|
590
|
+
if (this.tremRate > 0)
|
|
591
|
+
slope /= Math.max(TURN_FORCE_FLOOR, Math.abs(dir)) ** 2;
|
|
592
|
+
const bOut = bb[(this.bw - this.Nb) & mask]!;
|
|
593
|
+
const raw = nb[(this.nw - this.Nn) & mask]!;
|
|
594
|
+
const nOut = this.a * raw + this.tx1 - this.a * this.ty1;
|
|
595
|
+
this.tx1 = raw;
|
|
596
|
+
this.ty1 = nOut;
|
|
597
|
+
this.sy = this.sg * (1 - this.sp) * bOut + this.sp * this.sy;
|
|
598
|
+
const bridgeRefl = -this.sy;
|
|
599
|
+
const nutRefl = -nOut;
|
|
600
|
+
const dv = bowVel - (bridgeRefl + nutRefl);
|
|
601
|
+
const r = 1 / (Math.abs(dv * slope) + 0.75);
|
|
602
|
+
const r2 = r * r;
|
|
603
|
+
let f = r2 * r2;
|
|
604
|
+
if (f > 1) f = 1;
|
|
605
|
+
const nv = contact * dv * f;
|
|
606
|
+
nb[this.nw & mask] = bridgeRefl + nv;
|
|
607
|
+
bb[this.bw & mask] = nutRefl + nv;
|
|
608
|
+
this.nw += 1;
|
|
609
|
+
this.bw += 1;
|
|
610
|
+
if (on) this.track(bOut, n);
|
|
611
|
+
this.n += 1;
|
|
612
|
+
return bOut;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** Pitch lock: one bridge sample into the period detector. */
|
|
616
|
+
private track(x: number, n: number): void {
|
|
617
|
+
const y =
|
|
618
|
+
this.bpB * (x - this.bpX2) -
|
|
619
|
+
this.bpA1 * this.bpY1 -
|
|
620
|
+
this.bpA2 * this.bpY2;
|
|
621
|
+
this.bpX2 = this.bpX1;
|
|
622
|
+
this.bpX1 = x;
|
|
623
|
+
const prev = this.bpY1;
|
|
624
|
+
this.bpY2 = prev;
|
|
625
|
+
this.bpY1 = y;
|
|
626
|
+
if (!(prev < 0 && y >= 0)) return;
|
|
627
|
+
const cross = n - 1 + prev / (prev - y);
|
|
628
|
+
// Tremolo: only periods wholly inside a stroke's middle count.
|
|
629
|
+
const last = this.steady ? this.lastCross : -1;
|
|
630
|
+
this.lastCross = this.steady ? cross : -1;
|
|
631
|
+
// Settle first: the attack and a slur's glide are not steady periods.
|
|
632
|
+
if (last < 0 || n < 0.05 * this.rate) return;
|
|
633
|
+
if (this.slurAt >= 0 && n < this.slurAt + this.slurLen + 0.02 * this.rate)
|
|
634
|
+
return;
|
|
635
|
+
const period = cross - last;
|
|
636
|
+
const err = period / this.target - 1;
|
|
637
|
+
if (Math.abs(err) > 0.1) return;
|
|
638
|
+
if (this.tremRate > 0) {
|
|
639
|
+
// The first period of a stroke still carries the turn.
|
|
640
|
+
if ((this.strokeSkip += 1) > 1) {
|
|
641
|
+
this.strokeErr += err;
|
|
642
|
+
this.strokeCount += 1;
|
|
643
|
+
}
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
this.lock = clamp(this.lock - LOCK_GAIN * err, -LOCK_RANGE, LOCK_RANGE);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Adds `count` samples x `gain` into out[offset..] (and x `gainR` into
|
|
651
|
+
* `right` when given); returns false once silent.
|
|
652
|
+
*/
|
|
653
|
+
process(
|
|
654
|
+
out: Float64Array,
|
|
655
|
+
offset: number,
|
|
656
|
+
count: number,
|
|
657
|
+
gain = 1,
|
|
658
|
+
right?: Float64Array,
|
|
659
|
+
gainR = 0,
|
|
660
|
+
): boolean {
|
|
661
|
+
if (this.done) return false;
|
|
662
|
+
const os = this.os;
|
|
663
|
+
const attack = Math.max(1, this.spec.attack * this.rate);
|
|
664
|
+
const lift = Math.max(1, this.spec.release * this.rate);
|
|
665
|
+
const decim = this.decim;
|
|
666
|
+
let peak = 0;
|
|
667
|
+
for (let i = 0; i < count; i += 1) {
|
|
668
|
+
const base = this.n / os;
|
|
669
|
+
if (base % CONTROL === 0) {
|
|
670
|
+
const t = base / this.sr;
|
|
671
|
+
this.control(t);
|
|
672
|
+
let cents = this.vibrato ? this.vibrato(this.n / this.rate) : 0;
|
|
673
|
+
if (this.cents)
|
|
674
|
+
cents += this.cents((this.n - this.centsAt) / this.rate);
|
|
675
|
+
this.retune(this.currentHz() * 2 ** (cents / 1200));
|
|
676
|
+
}
|
|
677
|
+
let y: number;
|
|
678
|
+
if (decim) {
|
|
679
|
+
const a = this.tick(attack, lift);
|
|
680
|
+
y = decim.process(a, this.tick(attack, lift));
|
|
681
|
+
} else y = this.tick(attack, lift);
|
|
682
|
+
y *= this.gain;
|
|
683
|
+
this.toneY += this.toneA * (y - this.toneY);
|
|
684
|
+
const s = this.toneY - this.dcX + this.dcR * this.dcY;
|
|
685
|
+
this.dcX = this.toneY;
|
|
686
|
+
this.dcY = s;
|
|
687
|
+
out[offset + i] = out[offset + i]! + s * gain;
|
|
688
|
+
if (right) right[offset + i] = right[offset + i]! + s * gainR;
|
|
689
|
+
const m = Math.abs(s);
|
|
690
|
+
if (m > peak) peak = m;
|
|
691
|
+
}
|
|
692
|
+
if (this.n > this.off + this.spec.release * this.rate && peak < 1e-5) {
|
|
693
|
+
this.quiet += count;
|
|
694
|
+
if (this.quiet >= 2048) this.done = true;
|
|
695
|
+
} else this.quiet = 0;
|
|
696
|
+
if (this.n >= this.maxSeconds * this.rate) this.done = true;
|
|
697
|
+
return !this.done;
|
|
698
|
+
}
|
|
699
|
+
}
|