@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,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The vocoder stage and its built-in carrier (0.7). `applyVocoder` runs
|
|
3
|
+
* right after a carrier track's voice renders into `dry` (and `dryR`), before
|
|
4
|
+
* its FX_CHAIN: the modulator (another track's tap) shapes the carrier
|
|
5
|
+
* through a channel bank or an LPC talkbox. Output gain is fixed by the
|
|
6
|
+
* layout times `gain`, then a stateless soft peak guard, then `mix`; nothing
|
|
7
|
+
* is measured over the render span, so a window equals the full render.
|
|
8
|
+
* `VOCODER_ENGINE` plays instrument `vocoder`: a saw, supersaw, pulse or
|
|
9
|
+
* noise carrier following the track's notes, the song's chords or a drone.
|
|
10
|
+
*/
|
|
11
|
+
import type { PerformedNote } from "../../../core/expression.ts";
|
|
12
|
+
import type { AutomationPoint, Track } from "../../../core/score.ts";
|
|
13
|
+
import {
|
|
14
|
+
resolveVocoder,
|
|
15
|
+
vocoderTailSeconds,
|
|
16
|
+
VOCODER_INSTRUMENT,
|
|
17
|
+
VOCODER_LANE_PARAMS,
|
|
18
|
+
type ResolvedVocoder,
|
|
19
|
+
} from "../../../core/vocoder.ts";
|
|
20
|
+
import { seedHash } from "../dsp/rng.ts";
|
|
21
|
+
import { interpolateAutomation } from "../effects/common.ts";
|
|
22
|
+
import { chordAt, chordTimeline } from "../granular.ts";
|
|
23
|
+
import type { EngineContext, InstrumentEngine } from "../instruments.ts";
|
|
24
|
+
import type { RenderContext } from "../wav.ts";
|
|
25
|
+
import { warpedSpan } from "../warp.ts";
|
|
26
|
+
import { channelVocode } from "./bank.ts";
|
|
27
|
+
import {
|
|
28
|
+
chordSpans,
|
|
29
|
+
noteSpans,
|
|
30
|
+
renderCarrierSpan,
|
|
31
|
+
type CarrierSpan,
|
|
32
|
+
} from "./carrier.ts";
|
|
33
|
+
import {
|
|
34
|
+
blockCurve,
|
|
35
|
+
type VocoderControl,
|
|
36
|
+
type VocoderCurves,
|
|
37
|
+
} from "./control.ts";
|
|
38
|
+
import { talkboxVocode } from "./talkbox.ts";
|
|
39
|
+
import { quietHold } from "../dsp/follow.ts";
|
|
40
|
+
|
|
41
|
+
export { autoGateDb } from "../dsp/follow.ts";
|
|
42
|
+
|
|
43
|
+
/** Stateless soft knee: identity below 0.9, ceiling 1.0 (window-safe). */
|
|
44
|
+
export function peakGuard(x: Float64Array): void {
|
|
45
|
+
for (let i = 0; i < x.length; i += 1) {
|
|
46
|
+
const v = x[i]!;
|
|
47
|
+
const a = Math.abs(v);
|
|
48
|
+
if (a > 0.9) x[i] = Math.sign(v) * (0.9 + 0.1 * Math.tanh((a - 0.9) / 0.1));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type LaneContext = Pick<
|
|
53
|
+
RenderContext,
|
|
54
|
+
"sampleRate" | "samplesPerTick" | "warp" | "seedSeconds"
|
|
55
|
+
>;
|
|
56
|
+
|
|
57
|
+
/** Absolute song sample of a render's index 0. */
|
|
58
|
+
export function originOf(
|
|
59
|
+
context: Pick<RenderContext, "sampleRate" | "seedSeconds">,
|
|
60
|
+
): number {
|
|
61
|
+
return Math.round((context.seedSeconds ?? 0) * context.sampleRate);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The noise seed: the stored one, else a hash of the track id. */
|
|
65
|
+
export function vocoderSeed(track: Track, settings: ResolvedVocoder): number {
|
|
66
|
+
return settings.seed ?? seedHash(`vocoder:${track.id}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function laneOf(
|
|
70
|
+
track: Track,
|
|
71
|
+
param: string,
|
|
72
|
+
): readonly AutomationPoint[] | undefined {
|
|
73
|
+
const lanes = track.fxAutomation as
|
|
74
|
+
Readonly<Record<string, readonly AutomationPoint[]>> | undefined;
|
|
75
|
+
const points = lanes?.[`vocoder-${param}`];
|
|
76
|
+
return points && points.length > 0 ? points : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function tickAt(context: LaneContext, index: number): number {
|
|
80
|
+
return context.warp
|
|
81
|
+
? context.warp.tick(Math.max(0, index))
|
|
82
|
+
: index / context.samplesPerTick;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The control values for one render: settings, lane curves, hold. */
|
|
86
|
+
export function vocoderControl(
|
|
87
|
+
track: Track,
|
|
88
|
+
n: number,
|
|
89
|
+
context: LaneContext,
|
|
90
|
+
gateDb: number,
|
|
91
|
+
mod?: Float64Array,
|
|
92
|
+
): VocoderControl {
|
|
93
|
+
const settings = resolveVocoder(track.vocoder);
|
|
94
|
+
const origin = originOf(context);
|
|
95
|
+
const curves: Record<string, Float64Array> = {};
|
|
96
|
+
let hold: Uint8Array | undefined;
|
|
97
|
+
for (const { param } of VOCODER_LANE_PARAMS) {
|
|
98
|
+
const points = laneOf(track, param);
|
|
99
|
+
if (!points) continue;
|
|
100
|
+
const fallback =
|
|
101
|
+
param === "freeze"
|
|
102
|
+
? settings.freeze
|
|
103
|
+
? 1
|
|
104
|
+
: 0
|
|
105
|
+
: (settings as Record<string, unknown>)[param];
|
|
106
|
+
const curve = blockCurve(n, origin, (index) =>
|
|
107
|
+
interpolateAutomation(points, tickAt(context, index), fallback as number),
|
|
108
|
+
);
|
|
109
|
+
if (param === "freeze") {
|
|
110
|
+
hold = new Uint8Array(n);
|
|
111
|
+
for (let i = 0; i < n; i += 1) hold[i] = curve[i]! >= 0.5 ? 1 : 0;
|
|
112
|
+
} else curves[param] = curve;
|
|
113
|
+
}
|
|
114
|
+
// A static freeze holds the last sung vowel through every rest (a hold
|
|
115
|
+
// from sample 0 would freeze the silence before the voice comes in).
|
|
116
|
+
if (!hold && settings.freeze)
|
|
117
|
+
hold = mod
|
|
118
|
+
? quietHold(mod, n, context.sampleRate, gateDb)
|
|
119
|
+
: new Uint8Array(n).fill(1);
|
|
120
|
+
return {
|
|
121
|
+
settings,
|
|
122
|
+
sampleRate: context.sampleRate,
|
|
123
|
+
origin,
|
|
124
|
+
seed: vocoderSeed(track, settings),
|
|
125
|
+
gateDb,
|
|
126
|
+
curves: curves as VocoderCurves,
|
|
127
|
+
...(hold ? { hold } : {}),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Vocodes `dry` (and `dryR`) in place with modulator `mod` (the source's
|
|
133
|
+
* mono tap over the same span, plus any lookahead past it).
|
|
134
|
+
*/
|
|
135
|
+
export function applyVocoder(
|
|
136
|
+
dry: Float64Array,
|
|
137
|
+
dryR: Float64Array | undefined,
|
|
138
|
+
mod: Float64Array,
|
|
139
|
+
track: Track,
|
|
140
|
+
context: LaneContext,
|
|
141
|
+
gateDb: number,
|
|
142
|
+
): void {
|
|
143
|
+
const n = dry.length;
|
|
144
|
+
const cars = dryR ? [dry, dryR] : [dry];
|
|
145
|
+
const padded =
|
|
146
|
+
mod.length >= n
|
|
147
|
+
? mod
|
|
148
|
+
: (() => {
|
|
149
|
+
const out = new Float64Array(n);
|
|
150
|
+
out.set(mod);
|
|
151
|
+
return out;
|
|
152
|
+
})();
|
|
153
|
+
const control = vocoderControl(track, n, context, gateDb, padded);
|
|
154
|
+
const outs =
|
|
155
|
+
control.settings.mode === "talkbox"
|
|
156
|
+
? talkboxVocode(padded, cars, control)
|
|
157
|
+
: channelVocode(padded, cars, control);
|
|
158
|
+
const { curves, settings } = control;
|
|
159
|
+
outs.forEach((out, c) => {
|
|
160
|
+
const car = cars[c]!;
|
|
161
|
+
const gain = curves.gain;
|
|
162
|
+
if (gain || settings.gain !== 0) {
|
|
163
|
+
const fixed = 10 ** (settings.gain / 20);
|
|
164
|
+
for (let i = 0; i < n; i += 1)
|
|
165
|
+
out[i] = out[i]! * (gain ? 10 ** (gain[i]! / 20) : fixed);
|
|
166
|
+
}
|
|
167
|
+
peakGuard(out);
|
|
168
|
+
const mix = curves.mix;
|
|
169
|
+
if (mix || settings.mix < 1)
|
|
170
|
+
for (let i = 0; i < n; i += 1) {
|
|
171
|
+
const m = mix ? mix[i]! : settings.mix;
|
|
172
|
+
out[i] = out[i]! * m + car[i]! * (1 - m);
|
|
173
|
+
}
|
|
174
|
+
car.set(out.subarray(0, n));
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function noteSpanOf(
|
|
179
|
+
note: PerformedNote,
|
|
180
|
+
context: EngineContext,
|
|
181
|
+
): { start: number; length: number } {
|
|
182
|
+
if (context.warp)
|
|
183
|
+
return warpedSpan(context.warp, note.startTick, note.durationTicks);
|
|
184
|
+
const perTick =
|
|
185
|
+
(60 * context.sampleRate) / (context.tempoBpm * context.ticksPerBeat);
|
|
186
|
+
return {
|
|
187
|
+
start: Math.max(0, Math.floor(note.startTick * perTick)),
|
|
188
|
+
length: Math.max(1, Math.floor(note.durationTicks * perTick)),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function sampleAtTick(context: EngineContext, tick: number): number {
|
|
193
|
+
if (context.warp) return warpedSpan(context.warp, tick, 0).start;
|
|
194
|
+
return Math.max(0, Math.floor(tick * context.samplesPerTick));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
type ChordChange = { start: number; end: number; pcs: readonly number[] };
|
|
198
|
+
|
|
199
|
+
const samePcs = (a: readonly number[], b: readonly number[]) =>
|
|
200
|
+
a.length === b.length && a.every((pc, index) => pc === b[index]);
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Chord changes as the carrier holds them: neighbours with the same pitch
|
|
204
|
+
* classes join (a staccato bass under a held chord never re-strikes the
|
|
205
|
+
* pad), and a silence shorter than `hold` samples holds the chord before it.
|
|
206
|
+
*/
|
|
207
|
+
export function mergeChordChanges(
|
|
208
|
+
changes: readonly ChordChange[],
|
|
209
|
+
hold: number,
|
|
210
|
+
): ChordChange[] {
|
|
211
|
+
const merged: ChordChange[] = [];
|
|
212
|
+
for (const change of changes) {
|
|
213
|
+
if (change.end <= change.start) continue;
|
|
214
|
+
const last = merged[merged.length - 1];
|
|
215
|
+
if (
|
|
216
|
+
last &&
|
|
217
|
+
(samePcs(last.pcs, change.pcs) ||
|
|
218
|
+
(change.pcs.length === 0 &&
|
|
219
|
+
last.pcs.length > 0 &&
|
|
220
|
+
change.end - change.start < hold))
|
|
221
|
+
) {
|
|
222
|
+
last.end = change.end;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
merged.push({ ...change });
|
|
226
|
+
}
|
|
227
|
+
return merged;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** The built-in carrier's spans for a track. */
|
|
231
|
+
export function carrierSpans(
|
|
232
|
+
notes: readonly PerformedNote[],
|
|
233
|
+
track: Track,
|
|
234
|
+
settings: ResolvedVocoder,
|
|
235
|
+
context: EngineContext,
|
|
236
|
+
): CarrierSpan[] {
|
|
237
|
+
const seed = vocoderSeed(track, settings);
|
|
238
|
+
if (settings.follow === "drone")
|
|
239
|
+
return [
|
|
240
|
+
{
|
|
241
|
+
start: 0,
|
|
242
|
+
length: context.samples,
|
|
243
|
+
pitches: [settings.root],
|
|
244
|
+
velocity: 0.8,
|
|
245
|
+
seed,
|
|
246
|
+
absolute: true,
|
|
247
|
+
},
|
|
248
|
+
];
|
|
249
|
+
if (settings.follow === "chords") {
|
|
250
|
+
const timeline = chordTimeline(context.score);
|
|
251
|
+
const changes: { start: number; end: number; pcs: readonly number[] }[] =
|
|
252
|
+
[];
|
|
253
|
+
const ticks = [0, ...timeline.ticks.filter((tick) => tick > 0)];
|
|
254
|
+
ticks.forEach((tick, index) => {
|
|
255
|
+
const next = ticks[index + 1];
|
|
256
|
+
changes.push({
|
|
257
|
+
start: sampleAtTick(context, tick),
|
|
258
|
+
end: next === undefined ? context.samples : sampleAtTick(context, next),
|
|
259
|
+
pcs: chordAt(timeline, tick),
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
return chordSpans(
|
|
263
|
+
mergeChordChanges(changes, Math.round(0.05 * context.sampleRate)),
|
|
264
|
+
settings.root,
|
|
265
|
+
seed,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
return noteSpans(
|
|
269
|
+
notes,
|
|
270
|
+
(note) => noteSpanOf(note, context),
|
|
271
|
+
(note) => seedHash(`${seed}:${note.id}`),
|
|
272
|
+
).map((span, index) => {
|
|
273
|
+
const cents = notes[index]!.cents;
|
|
274
|
+
if (!cents) return span;
|
|
275
|
+
const curve = span.cents;
|
|
276
|
+
return {
|
|
277
|
+
...span,
|
|
278
|
+
cents: curve ? (t: number) => cents + curve(t) : () => cents,
|
|
279
|
+
};
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Instrument `vocoder`: the built-in carrier (its field is `vocoder`). */
|
|
284
|
+
export const VOCODER_ENGINE: InstrumentEngine = {
|
|
285
|
+
id: VOCODER_INSTRUMENT,
|
|
286
|
+
field: "vocoder",
|
|
287
|
+
render(dry, dryR, notes, track, context) {
|
|
288
|
+
const settings = resolveVocoder(track.vocoder);
|
|
289
|
+
const origin = originOf(context);
|
|
290
|
+
for (const span of carrierSpans(notes, track, settings, context))
|
|
291
|
+
renderCarrierSpan(dry, dryR, span, settings, {
|
|
292
|
+
sampleRate: context.sampleRate,
|
|
293
|
+
origin,
|
|
294
|
+
seed: span.seed,
|
|
295
|
+
...(context.tuning ? { tuning: context.tuning } : {}),
|
|
296
|
+
});
|
|
297
|
+
},
|
|
298
|
+
tailSeconds(track) {
|
|
299
|
+
return vocoderTailSeconds(track.vocoder);
|
|
300
|
+
},
|
|
301
|
+
stereo(track) {
|
|
302
|
+
return resolveVocoder(track.vocoder).carrier === "supersaw";
|
|
303
|
+
},
|
|
304
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The LPC talkbox (0.7, after mda Talkbox): per 20 ms sqrt-Hann frame at
|
|
3
|
+
* 50 % overlap, the modulator's spectral envelope (Levinson-Durbin, order
|
|
4
|
+
* round(sr / 2000), pre-emphasised) filters the pre-whitened carrier. Frames
|
|
5
|
+
* sit on the absolute song grid. Formant shift resamples the analysis frame
|
|
6
|
+
* (±12 st) and a per-frame energy normaliser, measured after de-emphasis,
|
|
7
|
+
* keeps the frame at the unshifted modulator frame's energy.
|
|
8
|
+
*/
|
|
9
|
+
import { gateCurve, holdCurve } from "../dsp/follow.ts";
|
|
10
|
+
import { autocorrelate, levinson } from "../dsp/lpc.ts";
|
|
11
|
+
import { TALKBOX_FORMANT_MAX } from "../../../core/vocoder.ts";
|
|
12
|
+
import { noiseSample } from "./bank.ts";
|
|
13
|
+
import type { VocoderControl } from "./control.ts";
|
|
14
|
+
import { at } from "./control.ts";
|
|
15
|
+
import { hissPath, unvoicedCurve } from "./detect.ts";
|
|
16
|
+
|
|
17
|
+
/** Fixed talkbox makeup (measured once on the voice fixture). */
|
|
18
|
+
export const TALKBOX_MAKEUP = 0.97;
|
|
19
|
+
|
|
20
|
+
const posMod = (a: number, m: number): number => ((a % m) + m) % m;
|
|
21
|
+
|
|
22
|
+
export function talkboxVocode(
|
|
23
|
+
mod: Float64Array,
|
|
24
|
+
cars: readonly Float64Array[],
|
|
25
|
+
control: VocoderControl,
|
|
26
|
+
): Float64Array[] {
|
|
27
|
+
const { settings: p, sampleRate: sr, origin, seed } = control;
|
|
28
|
+
const n = Math.min(mod.length, ...cars.map((c) => c.length));
|
|
29
|
+
const N = 2 * Math.round(0.01 * sr);
|
|
30
|
+
const hop = N / 2;
|
|
31
|
+
const order = Math.max(8, Math.round(sr / 2000));
|
|
32
|
+
const corder = 12;
|
|
33
|
+
const win = new Float64Array(N);
|
|
34
|
+
const sq = new Float64Array(N);
|
|
35
|
+
for (let i = 0; i < N; i += 1) {
|
|
36
|
+
win[i] = 0.5 - 0.5 * Math.cos((2 * Math.PI * i) / N);
|
|
37
|
+
sq[i] = Math.sqrt(win[i]!);
|
|
38
|
+
}
|
|
39
|
+
const acc = cars.map(() => new Float64Array(n + N));
|
|
40
|
+
const pre = new Float64Array(n);
|
|
41
|
+
for (let i = 0; i < n; i += 1)
|
|
42
|
+
pre[i] = mod[i]! - 0.97 * (i > 0 ? mod[i - 1]! : 0);
|
|
43
|
+
const mf = new Float64Array(N);
|
|
44
|
+
const cf = new Float64Array(N);
|
|
45
|
+
const y = new Float64Array(N);
|
|
46
|
+
const r = new Float64Array(order + 1);
|
|
47
|
+
const a = new Float64Array(order + 1);
|
|
48
|
+
const rc = new Float64Array(corder + 1);
|
|
49
|
+
const ac = new Float64Array(corder + 1);
|
|
50
|
+
const gate = gateCurve(mod.subarray(0, n), sr, control.gateDb);
|
|
51
|
+
if (gate && control.hold) holdCurve(gate, control.hold);
|
|
52
|
+
const unvoicedOn = p.unvoiced > 0 || control.curves.unvoiced !== undefined;
|
|
53
|
+
const u = unvoicedOn
|
|
54
|
+
? unvoicedCurve(mod.subarray(0, n), sr, p.sens, origin, gate)
|
|
55
|
+
: undefined;
|
|
56
|
+
let heldMe0 = 0;
|
|
57
|
+
for (let start = -posMod(origin, hop) - hop; start < n; start += hop) {
|
|
58
|
+
const centre = Math.min(n - 1, Math.max(0, start + hop));
|
|
59
|
+
const st = Math.max(
|
|
60
|
+
-TALKBOX_FORMANT_MAX,
|
|
61
|
+
Math.min(TALKBOX_FORMANT_MAX, at(control, "formant", centre)),
|
|
62
|
+
);
|
|
63
|
+
const shift = 2 ** (st / 12);
|
|
64
|
+
// freeze: a held frame reuses the last analysed filter and level
|
|
65
|
+
const held = heldMe0 > 0 && control.hold?.[centre] === 1;
|
|
66
|
+
let me = held ? 1 : 0;
|
|
67
|
+
let me0 = held ? heldMe0 : 0;
|
|
68
|
+
for (let i = 0; !held && i < N; i += 1) {
|
|
69
|
+
const j = start + i;
|
|
70
|
+
const jj = start + hop + (i - hop) * shift;
|
|
71
|
+
const k0 = Math.floor(jj);
|
|
72
|
+
const fr = jj - k0;
|
|
73
|
+
const m =
|
|
74
|
+
k0 >= 0 && k0 + 1 < n ? pre[k0]! * (1 - fr) + pre[k0 + 1]! * fr : 0;
|
|
75
|
+
const g = gate && j >= 0 && j < n ? gate[j]! : 1;
|
|
76
|
+
mf[i] = m * win[i]! * g;
|
|
77
|
+
me += mf[i]! * mf[i]!;
|
|
78
|
+
const raw = (j >= 0 && j < n ? mod[j]! : 0) * win[i]! * g;
|
|
79
|
+
me0 += raw * raw;
|
|
80
|
+
}
|
|
81
|
+
if (me < 1e-10 || me0 < 1e-10) continue;
|
|
82
|
+
if (!held) {
|
|
83
|
+
levinson(autocorrelate(mf, order, r, sr), order, a);
|
|
84
|
+
heldMe0 = me0;
|
|
85
|
+
}
|
|
86
|
+
for (let c = 0; c < cars.length; c += 1) {
|
|
87
|
+
const car = cars[c]!;
|
|
88
|
+
for (let i = 0; i < N; i += 1) {
|
|
89
|
+
const j = start + i;
|
|
90
|
+
let v = j >= 0 && j < n ? car[j]! : 0;
|
|
91
|
+
if (u && j >= 0 && j < n) {
|
|
92
|
+
const w = u[j]! * at(control, "unvoiced", j);
|
|
93
|
+
v = v * (1 - w) + noiseSample(seed, origin + j) * 0.5 * w;
|
|
94
|
+
}
|
|
95
|
+
cf[i] = v * sq[i]!;
|
|
96
|
+
}
|
|
97
|
+
if (p.enhance) {
|
|
98
|
+
levinson(autocorrelate(cf, corder, rc, sr), corder, ac);
|
|
99
|
+
for (let i = N - 1; i >= 0; i -= 1) {
|
|
100
|
+
let s = cf[i]!;
|
|
101
|
+
for (let j = 1; j <= corder && j <= i; j += 1)
|
|
102
|
+
s += ac[j]! * cf[i - j]!;
|
|
103
|
+
cf[i] = s;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
let ce = 0;
|
|
107
|
+
for (let i = 0; i < N; i += 1) ce += cf[i]! * cf[i]!;
|
|
108
|
+
if (ce <= 1e-12) continue;
|
|
109
|
+
for (let i = 0; i < N; i += 1) {
|
|
110
|
+
let s = cf[i]!;
|
|
111
|
+
for (let j = 1; j <= order && j <= i; j += 1) s -= a[j]! * y[i - j]!;
|
|
112
|
+
y[i] = s;
|
|
113
|
+
}
|
|
114
|
+
let ye = 0;
|
|
115
|
+
let de = 0;
|
|
116
|
+
for (let i = 0; i < N; i += 1) {
|
|
117
|
+
de = y[i]! * sq[i]! + 0.97 * de;
|
|
118
|
+
const v = de * sq[i]!;
|
|
119
|
+
ye += v * v;
|
|
120
|
+
}
|
|
121
|
+
const g = ye > 1e-20 ? Math.sqrt(me0 / ye) * TALKBOX_MAKEUP : 0;
|
|
122
|
+
const o = acc[c]!;
|
|
123
|
+
for (let i = 0; i < N; i += 1) {
|
|
124
|
+
const j = start + i;
|
|
125
|
+
if (j >= 0) o[j]! += y[i]! * sq[i]! * g;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const hiss =
|
|
130
|
+
p.hiss > 0 || control.curves.hiss ? hissPath(mod, n, sr) : undefined;
|
|
131
|
+
return acc.map((o) => {
|
|
132
|
+
const res = new Float64Array(n);
|
|
133
|
+
let dd = 0;
|
|
134
|
+
for (let i = 0; i < n; i += 1) {
|
|
135
|
+
dd = o[i]! + 0.97 * dd;
|
|
136
|
+
res[i] = dd;
|
|
137
|
+
}
|
|
138
|
+
if (hiss)
|
|
139
|
+
for (let i = 0; i < n; i += 1)
|
|
140
|
+
res[i]! += at(control, "hiss", i) * hiss[i]! * (gate ? gate[i]! : 1);
|
|
141
|
+
return res;
|
|
142
|
+
});
|
|
143
|
+
}
|