@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,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The channel vocoder (0.7): a bank of cascaded RBJ band-passes analyses
|
|
3
|
+
* the modulator, each band's follower shapes the same band of the carrier.
|
|
4
|
+
* Ported from the reviewed prototype (proto/vocoder, DSP v2):
|
|
5
|
+
* - layout from bands/lo/hi/width only (sample-rate independent);
|
|
6
|
+
* - band k reads the modulator advanced by its group delay plus the attack;
|
|
7
|
+
* - formant: synthesis band k takes the envelope at fractional analysis
|
|
8
|
+
* index k - formant / 12 / log2(ratio);
|
|
9
|
+
* - enhance whitens the carrier per band (each band normalised by its own
|
|
10
|
+
* follower), so a dull carrier still speaks in every band;
|
|
11
|
+
* - fixed makeup gain from the layout only, never from the rendered span.
|
|
12
|
+
* A stereo carrier runs one analysis and one synthesis bank per channel.
|
|
13
|
+
*/
|
|
14
|
+
import {
|
|
15
|
+
bandpass,
|
|
16
|
+
bankDesign,
|
|
17
|
+
runBiquad,
|
|
18
|
+
type Biquad,
|
|
19
|
+
} from "../dsp/bandbank.ts";
|
|
20
|
+
import { follow, followCoef, gateCurve, holdCurve } from "../dsp/follow.ts";
|
|
21
|
+
import { unit } from "../dsp/rng.ts";
|
|
22
|
+
import { at, VOCODER_BLOCK, type VocoderControl } from "./control.ts";
|
|
23
|
+
import { hissPath, unvoicedCurve } from "./detect.ts";
|
|
24
|
+
|
|
25
|
+
/** Fixed channel makeup from the layout only (fitted once on the voice fixture). */
|
|
26
|
+
export function channelMakeup(bands: number, width: number): number {
|
|
27
|
+
return (
|
|
28
|
+
(1.4 * (bands / 16) ** 0.29) / (width <= 1 ? width ** 0.35 : width ** 1.1)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Reference band level for depth < 1: envelope^depth * REF^(1 - depth). */
|
|
33
|
+
const REF = 0.02;
|
|
34
|
+
|
|
35
|
+
/** A noise sample in -1..1 at an absolute song sample. */
|
|
36
|
+
export function noiseSample(seed: number, songSample: number): number {
|
|
37
|
+
return unit(seed, songSample, 11) * 2 - 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Follower with an optional per-sample release curve and hold. */
|
|
41
|
+
function followVarying(
|
|
42
|
+
x: Float64Array,
|
|
43
|
+
attack: number,
|
|
44
|
+
release: number,
|
|
45
|
+
releaseCurve: Float64Array | undefined,
|
|
46
|
+
sampleRate: number,
|
|
47
|
+
hold?: Uint8Array,
|
|
48
|
+
): Float64Array {
|
|
49
|
+
if (!releaseCurve) return follow(x, attack, release, sampleRate, hold);
|
|
50
|
+
const a = followCoef(attack, sampleRate);
|
|
51
|
+
const out = new Float64Array(x.length);
|
|
52
|
+
let e = 0;
|
|
53
|
+
let lastRelease = Number.NaN;
|
|
54
|
+
let r = 0;
|
|
55
|
+
for (let i = 0; i < x.length; i += 1) {
|
|
56
|
+
if (releaseCurve[i] !== lastRelease) {
|
|
57
|
+
lastRelease = releaseCurve[i]!;
|
|
58
|
+
r = followCoef(lastRelease, sampleRate);
|
|
59
|
+
}
|
|
60
|
+
if (!hold || hold[i] === 0) {
|
|
61
|
+
const v = Math.abs(x[i]!);
|
|
62
|
+
const c = v > e ? a : r;
|
|
63
|
+
e = c * e + (1 - c) * v;
|
|
64
|
+
}
|
|
65
|
+
out[i] = e;
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Runs a biquad whose coefficients may change at 32-sample blocks. */
|
|
71
|
+
function runBand(
|
|
72
|
+
x: Float64Array,
|
|
73
|
+
filter: Biquad,
|
|
74
|
+
filterAt: ((i: number) => Biquad) | undefined,
|
|
75
|
+
): void {
|
|
76
|
+
if (!filterAt) {
|
|
77
|
+
runBiquad(filter, x);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
let z1 = 0;
|
|
81
|
+
let z2 = 0;
|
|
82
|
+
let f = filterAt(0);
|
|
83
|
+
for (let i = 0; i < x.length; i += 1) {
|
|
84
|
+
if (i % VOCODER_BLOCK === 0) f = filterAt(i);
|
|
85
|
+
const v = x[i]!;
|
|
86
|
+
const y = f.b0 * v + z1;
|
|
87
|
+
z1 = f.b1 * v - f.a1 * y + z2;
|
|
88
|
+
z2 = f.b2 * v - f.a2 * y;
|
|
89
|
+
x[i] = y;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The band filter run twice in cascade, in one pass when the coefficients
|
|
95
|
+
* are fixed (the same arithmetic per sample as two passes).
|
|
96
|
+
*/
|
|
97
|
+
function runBandTwice(
|
|
98
|
+
x: Float64Array,
|
|
99
|
+
filter: Biquad,
|
|
100
|
+
filterAt: ((i: number) => Biquad) | undefined,
|
|
101
|
+
): void {
|
|
102
|
+
if (filterAt) {
|
|
103
|
+
runBand(x, filter, filterAt);
|
|
104
|
+
runBand(x, filter, filterAt);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const { b0, b1, b2, a1, a2 } = filter;
|
|
108
|
+
let p1 = 0;
|
|
109
|
+
let p2 = 0;
|
|
110
|
+
let q1 = 0;
|
|
111
|
+
let q2 = 0;
|
|
112
|
+
for (let i = 0; i < x.length; i += 1) {
|
|
113
|
+
const v = x[i]!;
|
|
114
|
+
const y = b0 * v + p1;
|
|
115
|
+
p1 = b1 * v - a1 * y + p2;
|
|
116
|
+
p2 = b2 * v - a2 * y;
|
|
117
|
+
const w = b0 * y + q1;
|
|
118
|
+
q1 = b1 * y - a1 * w + q2;
|
|
119
|
+
q2 = b2 * y - a2 * w;
|
|
120
|
+
x[i] = w;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** `e[i] *= g[i]` (its own function so the loop is optimised). */
|
|
125
|
+
function multiplyInto(e: Float64Array, g: Float64Array): void {
|
|
126
|
+
for (let i = 0; i < e.length; i += 1) e[i] = e[i]! * g[i]!;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Cross-fades the carrier toward level-matched noise where `u` is unvoiced. */
|
|
130
|
+
function blendUnvoiced(
|
|
131
|
+
carrier: Float64Array,
|
|
132
|
+
u: Float64Array,
|
|
133
|
+
control: VocoderControl,
|
|
134
|
+
seed: number,
|
|
135
|
+
origin: number,
|
|
136
|
+
sr: number,
|
|
137
|
+
): void {
|
|
138
|
+
const level = follow(carrier, 0.01, 0.1, sr);
|
|
139
|
+
const curve = control.curves.unvoiced;
|
|
140
|
+
const fixed = control.settings.unvoiced;
|
|
141
|
+
for (let i = 0; i < carrier.length; i += 1) {
|
|
142
|
+
const w = u[i]! * (curve ? curve[i]! : fixed);
|
|
143
|
+
if (w <= 0) continue;
|
|
144
|
+
const noise = noiseSample(seed, origin + i);
|
|
145
|
+
carrier[i] =
|
|
146
|
+
carrier[i]! * (1 - w) + noise * 1.7 * Math.max(level[i]!, 0.05) * w;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* One synthesis band into `out` with a fixed formant shift and depth: the
|
|
152
|
+
* envelope interpolated between analysis bands `ea` and `eb` by `fr`, shaped
|
|
153
|
+
* by `depth`, over the carrier band `syn` (whitened by `ec` when enhancing).
|
|
154
|
+
* A small function of its own so the JIT optimises the loop: inlined in
|
|
155
|
+
* `channelVocode`, which runs once per render, it stayed in the baseline tier.
|
|
156
|
+
*/
|
|
157
|
+
function mixBandFixed(
|
|
158
|
+
out: Float64Array,
|
|
159
|
+
syn: Float64Array,
|
|
160
|
+
ea: Float64Array,
|
|
161
|
+
eb: Float64Array,
|
|
162
|
+
fr: number,
|
|
163
|
+
ec: Float64Array | undefined,
|
|
164
|
+
depth: number,
|
|
165
|
+
scale: number,
|
|
166
|
+
): void {
|
|
167
|
+
const n = out.length;
|
|
168
|
+
const refPow = REF ** (1 - depth);
|
|
169
|
+
if (ec)
|
|
170
|
+
for (let i = 0; i < n; i += 1) {
|
|
171
|
+
const e = ea[i]! * (1 - fr) + eb[i]! * fr;
|
|
172
|
+
const level = depth === 1 ? e : e ** depth * refPow;
|
|
173
|
+
const g = level / (ec[i]! + 1e-5);
|
|
174
|
+
out[i]! += syn[i]! * Math.min(g, 1e3) * scale;
|
|
175
|
+
}
|
|
176
|
+
else
|
|
177
|
+
for (let i = 0; i < n; i += 1) {
|
|
178
|
+
const e = ea[i]! * (1 - fr) + eb[i]! * fr;
|
|
179
|
+
const level = depth === 1 ? e : e ** depth * refPow;
|
|
180
|
+
out[i]! += syn[i]! * level * 8 * scale;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Vocodes each carrier channel with `mod`. The modulator is read up to the
|
|
186
|
+
* largest band advance past each output sample (zero past its end).
|
|
187
|
+
* Returns the wet signal per channel, after `gain` and before the peak guard
|
|
188
|
+
* and `mix` (`applyVocoderStage` adds those).
|
|
189
|
+
*/
|
|
190
|
+
export function channelVocode(
|
|
191
|
+
mod: Float64Array,
|
|
192
|
+
cars: readonly Float64Array[],
|
|
193
|
+
control: VocoderControl,
|
|
194
|
+
): Float64Array[] {
|
|
195
|
+
const { settings: p, sampleRate: sr, origin, seed } = control;
|
|
196
|
+
const n = Math.min(mod.length, ...cars.map((c) => c.length));
|
|
197
|
+
const { bands, ratio } = bankDesign(p, sr);
|
|
198
|
+
const nb = bands.length;
|
|
199
|
+
const widthCurve = control.curves.width;
|
|
200
|
+
// Width automation re-designs the band filters per block.
|
|
201
|
+
const designs = new Map<number, Biquad[]>();
|
|
202
|
+
const filtersFor = (width: number): Biquad[] => {
|
|
203
|
+
let known = designs.get(width);
|
|
204
|
+
if (!known) {
|
|
205
|
+
const octaves = Math.log2(ratio) * width * 1.55;
|
|
206
|
+
known = bands.map((b) =>
|
|
207
|
+
bandpass(Math.min(b.hz, 0.45 * sr), octaves, sr),
|
|
208
|
+
);
|
|
209
|
+
designs.set(width, known);
|
|
210
|
+
}
|
|
211
|
+
return known;
|
|
212
|
+
};
|
|
213
|
+
const filterAt = (k: number, offset: number) =>
|
|
214
|
+
widthCurve
|
|
215
|
+
? (i: number): Biquad =>
|
|
216
|
+
filtersFor(widthCurve[Math.min(n - 1, i + offset)]!)[k]!
|
|
217
|
+
: undefined;
|
|
218
|
+
const gate = gateCurve(mod.subarray(0, n), sr, control.gateDb);
|
|
219
|
+
if (gate && control.hold) holdCurve(gate, control.hold);
|
|
220
|
+
const unvoicedOn = p.unvoiced > 0 || control.curves.unvoiced !== undefined;
|
|
221
|
+
const u = unvoicedOn
|
|
222
|
+
? unvoicedCurve(mod.subarray(0, n), sr, p.sens, origin, gate)
|
|
223
|
+
: undefined;
|
|
224
|
+
// Analysis: one envelope per band.
|
|
225
|
+
const env: Float64Array[] = [];
|
|
226
|
+
const band = new Float64Array(n);
|
|
227
|
+
for (let k = 0; k < nb; k += 1) {
|
|
228
|
+
const b = bands[k]!;
|
|
229
|
+
if (!b.live) {
|
|
230
|
+
env.push(new Float64Array(n));
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
// The modulator advanced by the band's delay, zero past its end.
|
|
234
|
+
const from = Math.min(b.advance, mod.length);
|
|
235
|
+
const take = Math.min(n, mod.length - from);
|
|
236
|
+
band.set(mod.subarray(from, from + take));
|
|
237
|
+
band.fill(0, take);
|
|
238
|
+
runBandTwice(band, b.filter, filterAt(k, b.advance));
|
|
239
|
+
const e = followVarying(
|
|
240
|
+
band,
|
|
241
|
+
p.attack,
|
|
242
|
+
p.release,
|
|
243
|
+
control.curves.release,
|
|
244
|
+
sr,
|
|
245
|
+
control.hold,
|
|
246
|
+
);
|
|
247
|
+
if (gate) multiplyInto(e, gate);
|
|
248
|
+
env.push(e);
|
|
249
|
+
}
|
|
250
|
+
const shiftOf = (i: number): number =>
|
|
251
|
+
at(control, "formant", i) / 12 / Math.log2(ratio);
|
|
252
|
+
const staticShift = control.curves.formant ? undefined : shiftOf(0);
|
|
253
|
+
const synthEnv = (k: number, i: number): number => {
|
|
254
|
+
const pos = k - (staticShift ?? shiftOf(i));
|
|
255
|
+
const i0 = Math.floor(pos);
|
|
256
|
+
const fr = pos - i0;
|
|
257
|
+
const a = i0 >= 0 && i0 < nb ? env[i0]![i]! : 0;
|
|
258
|
+
const b = i0 + 1 >= 0 && i0 + 1 < nb ? env[i0 + 1]![i]! : 0;
|
|
259
|
+
return a * (1 - fr) + b * fr;
|
|
260
|
+
};
|
|
261
|
+
const scale = channelMakeup(p.bands, p.width);
|
|
262
|
+
const depthCurve = control.curves.depth;
|
|
263
|
+
const shape = (e: number, i: number): number => {
|
|
264
|
+
const depth = depthCurve ? depthCurve[i]! : p.depth;
|
|
265
|
+
return depth === 1 ? e : e ** depth * REF ** (1 - depth);
|
|
266
|
+
};
|
|
267
|
+
const hiss =
|
|
268
|
+
p.hiss > 0 || control.curves.hiss ? hissPath(mod, n, sr) : undefined;
|
|
269
|
+
const zeros = new Float64Array(n);
|
|
270
|
+
const outs: Float64Array[] = [];
|
|
271
|
+
for (const car of cars) {
|
|
272
|
+
const carrier = car.slice(0, n);
|
|
273
|
+
if (u) blendUnvoiced(carrier, u, control, seed, origin, sr);
|
|
274
|
+
const out = new Float64Array(n);
|
|
275
|
+
const syn = new Float64Array(n);
|
|
276
|
+
for (let k = 0; k < nb; k += 1) {
|
|
277
|
+
const b = bands[k]!;
|
|
278
|
+
if (!b.live) continue;
|
|
279
|
+
syn.set(carrier);
|
|
280
|
+
runBandTwice(syn, b.filter, filterAt(k, 0));
|
|
281
|
+
// The envelope this band reads: a fixed pair of analysis bands and
|
|
282
|
+
// weight unless the formant is automated (same arithmetic as
|
|
283
|
+
// `synthEnv`, without a call per sample).
|
|
284
|
+
let ea: Float64Array = zeros;
|
|
285
|
+
let eb: Float64Array = zeros;
|
|
286
|
+
let fr = 0;
|
|
287
|
+
if (staticShift !== undefined) {
|
|
288
|
+
const pos = k - staticShift;
|
|
289
|
+
const i0 = Math.floor(pos);
|
|
290
|
+
fr = pos - i0;
|
|
291
|
+
if (i0 >= 0 && i0 < nb) ea = env[i0]!;
|
|
292
|
+
if (i0 + 1 >= 0 && i0 + 1 < nb) eb = env[i0 + 1]!;
|
|
293
|
+
}
|
|
294
|
+
const ec = p.enhance
|
|
295
|
+
? followVarying(syn, p.attack, p.release, control.curves.release, sr)
|
|
296
|
+
: undefined;
|
|
297
|
+
if (staticShift !== undefined && depthCurve === undefined)
|
|
298
|
+
mixBandFixed(out, syn, ea, eb, fr, ec, p.depth, scale);
|
|
299
|
+
else
|
|
300
|
+
for (let i = 0; i < n; i += 1) {
|
|
301
|
+
const level = shape(synthEnv(k, i), i);
|
|
302
|
+
if (ec) {
|
|
303
|
+
const g = level / (ec[i]! + 1e-5);
|
|
304
|
+
out[i]! += syn[i]! * Math.min(g, 1e3) * scale;
|
|
305
|
+
} else out[i]! += syn[i]! * level * 8 * scale;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (hiss)
|
|
309
|
+
for (let i = 0; i < n; i += 1)
|
|
310
|
+
out[i]! += at(control, "hiss", i) * hiss[i]! * (gate ? gate[i]! : 1);
|
|
311
|
+
outs.push(out);
|
|
312
|
+
}
|
|
313
|
+
return outs;
|
|
314
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in carriers for instrument `vocoder` (0.7): saw, supersaw, pulse or
|
|
3
|
+
* noise, pitched by the track's performed notes, the song's chord timeline
|
|
4
|
+
* or a drone at `root`, through the track tuning. PolyBLEP edges (the synth
|
|
5
|
+
* oscillators' residual); supersaw unison copies alternate left and right
|
|
6
|
+
* on a stereo carrier. Phases are seeded from the note (never Math.random);
|
|
7
|
+
* noise is keyed to the absolute song sample.
|
|
8
|
+
*/
|
|
9
|
+
import type { PerformedNote } from "../../../core/expression.ts";
|
|
10
|
+
import { noteHz, type TuningTable } from "../../../core/tuning.ts";
|
|
11
|
+
import type { ResolvedVocoder } from "../../../core/vocoder.ts";
|
|
12
|
+
import { unit } from "../dsp/rng.ts";
|
|
13
|
+
import { polyBlep } from "../synth/oscillators.ts";
|
|
14
|
+
|
|
15
|
+
/** Unison copies of a supersaw, in units of `spread` semitones. */
|
|
16
|
+
const UNISON = Object.freeze([0, -0.73, 0.73, -1.67, 1.67, -2.67, 2.67]);
|
|
17
|
+
/** Short fades so carrier notes never click (the vocoder adds no envelope). */
|
|
18
|
+
const FADE_SECONDS = 0.004;
|
|
19
|
+
|
|
20
|
+
export type CarrierSpan = Readonly<{
|
|
21
|
+
start: number;
|
|
22
|
+
length: number;
|
|
23
|
+
pitches: readonly number[];
|
|
24
|
+
/** Pitch offset in cents at `t` seconds from the span start (glide, bend, vibrato). */
|
|
25
|
+
cents?: (t: number) => number;
|
|
26
|
+
velocity: number;
|
|
27
|
+
seed: number;
|
|
28
|
+
/**
|
|
29
|
+
* Phase runs on the absolute song sample (chord and drone spans), so a
|
|
30
|
+
* window that cuts into the span hears the same wave as one pass.
|
|
31
|
+
*/
|
|
32
|
+
absolute?: boolean;
|
|
33
|
+
}>;
|
|
34
|
+
|
|
35
|
+
export type CarrierOptions = Readonly<{
|
|
36
|
+
sampleRate: number;
|
|
37
|
+
/** Absolute song sample of index 0 (noise only). */
|
|
38
|
+
origin: number;
|
|
39
|
+
seed: number;
|
|
40
|
+
tuning?: TuningTable;
|
|
41
|
+
}>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Adds one span (one note, chord or drone segment) of carrier into `left`
|
|
45
|
+
* (and `right`). Supersaw copies after the centre alternate channels.
|
|
46
|
+
*/
|
|
47
|
+
export function renderCarrierSpan(
|
|
48
|
+
left: Float64Array,
|
|
49
|
+
right: Float64Array | undefined,
|
|
50
|
+
span: CarrierSpan,
|
|
51
|
+
settings: Pick<ResolvedVocoder, "carrier" | "spread">,
|
|
52
|
+
options: CarrierOptions,
|
|
53
|
+
): void {
|
|
54
|
+
const { sampleRate } = options;
|
|
55
|
+
const end = Math.min(left.length, span.start + span.length);
|
|
56
|
+
const begin = Math.max(0, span.start);
|
|
57
|
+
if (end <= begin) return;
|
|
58
|
+
const fade = Math.max(1, Math.round(FADE_SECONDS * sampleRate));
|
|
59
|
+
const level = 0.3 * (0.4 + 0.6 * Math.max(0, Math.min(1, span.velocity)));
|
|
60
|
+
const envelope = (i: number): number => {
|
|
61
|
+
const a = i - span.start;
|
|
62
|
+
const b = span.start + span.length - i;
|
|
63
|
+
return Math.min(1, a / fade, b / fade);
|
|
64
|
+
};
|
|
65
|
+
if (settings.carrier === "noise") {
|
|
66
|
+
for (let i = begin; i < end; i += 1) {
|
|
67
|
+
const v = (unit(options.seed, options.origin + i, 7) * 2 - 1) * 0.5;
|
|
68
|
+
const e = envelope(i) * level;
|
|
69
|
+
left[i]! += v * e;
|
|
70
|
+
if (right) right[i]! += v * e;
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const copies = settings.carrier === "supersaw" ? UNISON : [0];
|
|
75
|
+
const voices = span.pitches.length * copies.length;
|
|
76
|
+
const norm = 1 / Math.sqrt(Math.max(1, voices));
|
|
77
|
+
span.pitches.forEach((pitch, p) => {
|
|
78
|
+
const base = noteHz(pitch, undefined, options.tuning);
|
|
79
|
+
const bend = span.cents;
|
|
80
|
+
copies.forEach((offset, c) => {
|
|
81
|
+
const hz = base * 2 ** ((offset * settings.spread) / 12);
|
|
82
|
+
let dt = hz / sampleRate;
|
|
83
|
+
if (!(dt > 0 && dt < 0.5)) return;
|
|
84
|
+
let phase =
|
|
85
|
+
settings.carrier === "supersaw" ? unit(span.seed, p * 8 + c, 3) : 0;
|
|
86
|
+
// Phase at `begin` (a span that starts before the buffer), or on the
|
|
87
|
+
// absolute song sample for a span a render window may cut into.
|
|
88
|
+
phase = span.absolute
|
|
89
|
+
? (phase + (((options.origin + begin) * dt) % 1)) % 1
|
|
90
|
+
: (phase + (begin - span.start) * dt) % 1;
|
|
91
|
+
const weight = (c === 0 ? 1 : 0.6) * norm * level;
|
|
92
|
+
const side = c === 0 ? 0 : c % 2 === 1 ? -1 : 1;
|
|
93
|
+
for (let i = begin; i < end; i += 1) {
|
|
94
|
+
// Bends are read every 32 samples (control rate).
|
|
95
|
+
if (bend && (i - begin) % 32 === 0)
|
|
96
|
+
dt = Math.min(
|
|
97
|
+
0.49,
|
|
98
|
+
(hz * 2 ** (bend((i - span.start) / sampleRate) / 1200)) /
|
|
99
|
+
sampleRate,
|
|
100
|
+
);
|
|
101
|
+
const v =
|
|
102
|
+
settings.carrier === "pulse"
|
|
103
|
+
? (phase < 0.5 ? 1 : -1) +
|
|
104
|
+
polyBlep(phase, dt) -
|
|
105
|
+
polyBlep((phase + 0.5) % 1, dt)
|
|
106
|
+
: 2 * phase - 1 - polyBlep(phase, dt);
|
|
107
|
+
const s = v * weight * envelope(i);
|
|
108
|
+
if (!right || side === 0) {
|
|
109
|
+
left[i]! += s;
|
|
110
|
+
if (right) right[i]! += s;
|
|
111
|
+
} else if (side < 0) left[i]! += s * 1.4;
|
|
112
|
+
else right[i]! += s * 1.4;
|
|
113
|
+
phase += dt;
|
|
114
|
+
if (phase >= 1) phase -= 1;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Spans for `follow: notes`: one per performed note. */
|
|
121
|
+
export function noteSpans(
|
|
122
|
+
notes: readonly PerformedNote[],
|
|
123
|
+
span: (note: PerformedNote) => { start: number; length: number },
|
|
124
|
+
seedOf: (note: PerformedNote) => number,
|
|
125
|
+
): CarrierSpan[] {
|
|
126
|
+
return notes.map((note) => {
|
|
127
|
+
const { start, length } = span(note);
|
|
128
|
+
const cents = note.performance?.cents;
|
|
129
|
+
return {
|
|
130
|
+
start,
|
|
131
|
+
length,
|
|
132
|
+
pitches: [note.pitch],
|
|
133
|
+
...(cents ? { cents } : {}),
|
|
134
|
+
velocity: note.velocity,
|
|
135
|
+
seed: seedOf(note),
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Spans for `follow: chords`: the chord timeline's pitch classes voiced in
|
|
142
|
+
* the octave from `root` upward, one span per chord change; no chord, no
|
|
143
|
+
* carrier.
|
|
144
|
+
*/
|
|
145
|
+
export function chordSpans(
|
|
146
|
+
changes: readonly Readonly<{
|
|
147
|
+
start: number;
|
|
148
|
+
end: number;
|
|
149
|
+
pcs: readonly number[];
|
|
150
|
+
}>[],
|
|
151
|
+
root: number,
|
|
152
|
+
seed: number,
|
|
153
|
+
): CarrierSpan[] {
|
|
154
|
+
return changes
|
|
155
|
+
.filter((change) => change.pcs.length > 0 && change.end > change.start)
|
|
156
|
+
.map((change) => ({
|
|
157
|
+
start: change.start,
|
|
158
|
+
length: change.end - change.start,
|
|
159
|
+
pitches: change.pcs.map((pc) => root + ((((pc - root) % 12) + 12) % 12)),
|
|
160
|
+
velocity: 0.8,
|
|
161
|
+
// One seed for the track: a render window's chord index differs.
|
|
162
|
+
seed,
|
|
163
|
+
absolute: true,
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control values for one vocoder render: the resolved settings plus, for
|
|
3
|
+
* each automated parameter, a per-sample curve read once per 32-sample block
|
|
4
|
+
* on the absolute song grid (so a window and the full render agree).
|
|
5
|
+
*/
|
|
6
|
+
import type { ResolvedVocoder } from "../../../core/vocoder.ts";
|
|
7
|
+
|
|
8
|
+
export const VOCODER_BLOCK = 32;
|
|
9
|
+
|
|
10
|
+
export type VocoderCurves = Partial<
|
|
11
|
+
Record<
|
|
12
|
+
| "spread"
|
|
13
|
+
| "width"
|
|
14
|
+
| "release"
|
|
15
|
+
| "formant"
|
|
16
|
+
| "unvoiced"
|
|
17
|
+
| "hiss"
|
|
18
|
+
| "depth"
|
|
19
|
+
| "mix"
|
|
20
|
+
| "gain",
|
|
21
|
+
Float64Array
|
|
22
|
+
>
|
|
23
|
+
>;
|
|
24
|
+
|
|
25
|
+
export type VocoderControl = Readonly<{
|
|
26
|
+
settings: ResolvedVocoder;
|
|
27
|
+
sampleRate: number;
|
|
28
|
+
/** Absolute song sample of index 0. */
|
|
29
|
+
origin: number;
|
|
30
|
+
seed: number;
|
|
31
|
+
/** Gate threshold in dBFS (auto already resolved); -120 is off. */
|
|
32
|
+
gateDb: number;
|
|
33
|
+
curves: VocoderCurves;
|
|
34
|
+
/** Non-zero where the envelopes hold (freeze on, or its lane at 1). */
|
|
35
|
+
hold?: Uint8Array;
|
|
36
|
+
}>;
|
|
37
|
+
|
|
38
|
+
/** A value at sample `i`: its curve when automated, else the static value. */
|
|
39
|
+
export function at(
|
|
40
|
+
control: VocoderControl,
|
|
41
|
+
name: keyof VocoderCurves,
|
|
42
|
+
i: number,
|
|
43
|
+
): number {
|
|
44
|
+
const curve = control.curves[name];
|
|
45
|
+
return curve ? curve[i]! : (control.settings[name] as number);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Per-sample curve from `valueAt(index)`, sampled at each block's first
|
|
50
|
+
* song-grid sample (clamped to 0) and held for the block.
|
|
51
|
+
*/
|
|
52
|
+
export function blockCurve(
|
|
53
|
+
n: number,
|
|
54
|
+
origin: number,
|
|
55
|
+
valueAt: (index: number) => number,
|
|
56
|
+
): Float64Array {
|
|
57
|
+
const out = new Float64Array(n);
|
|
58
|
+
const phase = ((origin % VOCODER_BLOCK) + VOCODER_BLOCK) % VOCODER_BLOCK;
|
|
59
|
+
let i = 0;
|
|
60
|
+
let start = -phase;
|
|
61
|
+
while (i < n) {
|
|
62
|
+
const end = Math.min(n, start + VOCODER_BLOCK);
|
|
63
|
+
const value = valueAt(start);
|
|
64
|
+
for (; i < end; i += 1) out[i] = value;
|
|
65
|
+
start += VOCODER_BLOCK;
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The vocoder's detectors (0.7): the unvoiced curve (fricatives and
|
|
3
|
+
* breaths get a noise carrier). A sample counts as unvoiced when its share
|
|
4
|
+
* of energy above 4 kHz passes a `sens`-scaled threshold AND the modulator
|
|
5
|
+
* is aperiodic (low normalised autocorrelation), and only while the gate is
|
|
6
|
+
* open: either cue alone misfires on bright vowels or on reverb tails.
|
|
7
|
+
*/
|
|
8
|
+
import { highpass, runBiquad } from "../dsp/bandbank.ts";
|
|
9
|
+
import { follow, followCoef } from "../dsp/follow.ts";
|
|
10
|
+
import { periodicityCurve } from "../dsp/periodicity.ts";
|
|
11
|
+
|
|
12
|
+
export function unvoicedCurve(
|
|
13
|
+
mod: Float64Array,
|
|
14
|
+
sampleRate: number,
|
|
15
|
+
sens: number,
|
|
16
|
+
origin = 0,
|
|
17
|
+
gate?: Float64Array,
|
|
18
|
+
): Float64Array {
|
|
19
|
+
const hp = Float64Array.from(mod);
|
|
20
|
+
const filter = highpass(4000, sampleRate);
|
|
21
|
+
runBiquad(filter, hp);
|
|
22
|
+
runBiquad(filter, hp);
|
|
23
|
+
const high = follow(hp, 0.002, 0.02, sampleRate);
|
|
24
|
+
const full = follow(mod, 0.002, 0.02, sampleRate);
|
|
25
|
+
const periodic = periodicityCurve(mod, sampleRate, origin);
|
|
26
|
+
const threshold = 0.5 - 0.35 * sens;
|
|
27
|
+
const out = new Float64Array(mod.length);
|
|
28
|
+
const c = followCoef(0.004, sampleRate);
|
|
29
|
+
let s = 0;
|
|
30
|
+
for (let i = 0; i < mod.length; i += 1) {
|
|
31
|
+
const ratio = full[i]! > 1e-4 ? high[i]! / full[i]! : 0;
|
|
32
|
+
const share = Math.min(1, Math.max(0, (ratio - threshold) / 0.15));
|
|
33
|
+
const aperiodic = Math.min(1, Math.max(0, (0.75 - periodic[i]!) / 0.2));
|
|
34
|
+
const t = share * aperiodic * (gate ? gate[i]! : 1);
|
|
35
|
+
s = c * s + (1 - c) * t;
|
|
36
|
+
out[i] = s;
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The modulator above 5 kHz, gated: the `hiss` path that keeps "s" crisp. */
|
|
42
|
+
export function hissPath(
|
|
43
|
+
mod: Float64Array,
|
|
44
|
+
n: number,
|
|
45
|
+
sampleRate: number,
|
|
46
|
+
): Float64Array {
|
|
47
|
+
const hp = mod.slice(0, n);
|
|
48
|
+
runBiquad(highpass(5000, sampleRate), hp);
|
|
49
|
+
return hp;
|
|
50
|
+
}
|