@hraness/dawg 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +126 -0
- package/DAWG.md +655 -141
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +759 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -99
- package/core/expression.ts +158 -6
- package/core/fx.ts +354 -32
- package/core/granular.ts +91 -0
- package/core/instruments.ts +180 -0
- package/core/keys.ts +573 -4
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +306 -4
- package/core/routing.ts +165 -0
- package/core/score.ts +1181 -16
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +354 -6
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +3160 -69
- package/core/sections.ts +298 -24
- package/core/sing.ts +815 -0
- package/core/strings.ts +237 -2
- package/core/style-provenance.ts +80 -0
- package/core/styles/africa-mena-southasia.ts +2893 -0
- package/core/styles/americas.ts +3810 -0
- package/core/styles/art.ts +4993 -0
- package/core/styles/base.ts +123 -0
- package/core/styles/cycles.ts +106 -0
- package/core/styles/electronic.ts +2723 -0
- package/core/styles/europe-asia-pacific.ts +2838 -0
- package/core/styles/excerpt.ts +29 -0
- package/core/styles/gamelan.ts +283 -0
- package/core/styles/generate.ts +2199 -0
- package/core/styles/index.ts +515 -0
- package/core/styles/parts.ts +106 -0
- package/core/styles/pop.ts +3189 -0
- package/core/styles/rock.ts +2993 -0
- package/core/styles/roots.ts +4175 -0
- package/core/styles/schema.ts +429 -0
- package/core/styles/taxonomy.ts +940 -0
- package/core/styles/validate.ts +528 -0
- package/core/tempo.ts +32 -2
- package/core/tuning.ts +19 -3
- package/core/vocoder.ts +524 -0
- package/core/winds.ts +652 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +30 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -6
- package/guides/chords.md +16 -14
- package/guides/effects.md +17 -15
- package/guides/faders.md +20 -16
- package/guides/files.md +13 -9
- package/guides/getting-started.md +15 -11
- package/guides/keys.md +19 -15
- package/guides/media.md +17 -12
- package/guides/mix.md +12 -6
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/performance.md +15 -10
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +32 -0
- package/guides/rhythm.md +17 -13
- package/guides/sessions.md +17 -7
- package/guides/show-me.md +31 -0
- package/guides/sound.md +25 -9
- package/guides/sounds.md +15 -12
- package/guides/styles.md +31 -0
- package/guides/tempo.md +15 -10
- package/guides/tracks.md +15 -10
- package/guides/tuning.md +32 -0
- package/guides/voice.md +31 -0
- package/guides/web-search.md +18 -8
- package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
- package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
- package/native/prebuilt/manifest.json +21 -0
- package/package.json +5 -2
- package/src/agent/agent.ts +131 -13
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +369 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +15 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +13 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +5 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/rhythm-tools.ts +1 -1
- package/src/agent/section-tools.ts +1 -1
- package/src/agent/show-me.ts +497 -0
- package/src/agent/steer.ts +15 -0
- package/src/agent/style-tools.ts +217 -0
- package/src/agent/tool-error.ts +12 -0
- package/src/agent/tools.ts +307 -37
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +16 -6
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +58 -3
- package/src/audio/autotune-engine.ts +101 -0
- package/src/audio/autotune.ts +640 -0
- package/src/audio/clips.ts +240 -0
- package/src/audio/doctor.ts +86 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +171 -0
- package/src/audio/dsp/follow.ts +120 -0
- package/src/audio/dsp/formant.ts +427 -0
- package/src/audio/dsp/glottal.ts +243 -0
- package/src/audio/dsp/interp.ts +7 -2
- package/src/audio/dsp/lpc.ts +50 -0
- package/src/audio/dsp/periodicity.ts +59 -0
- package/src/audio/dsp/pitch.ts +995 -0
- package/src/audio/dsp/psola.ts +199 -0
- package/src/audio/dsp/shift.ts +256 -0
- package/src/audio/effects/chain.ts +21 -4
- package/src/audio/effects/common.ts +57 -0
- package/src/audio/effects/convolution.ts +113 -5
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/engine.ts +206 -33
- package/src/audio/fit.ts +80 -3
- package/src/audio/granular.ts +352 -21
- package/src/audio/instrument-check.ts +67 -47
- package/src/audio/instruments.ts +36 -3
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +427 -0
- package/src/audio/keys/engine.ts +156 -20
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +72 -8
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/kits.ts +135 -6
- package/src/audio/live-worker.ts +3 -1
- package/src/audio/live.ts +247 -26
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +45 -3
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +286 -0
- package/src/audio/resonators.ts +10 -2
- package/src/audio/sampler.ts +252 -24
- package/src/audio/samples.ts +46 -3
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/body.ts +29 -16
- package/src/audio/strings/bow.ts +699 -0
- package/src/audio/strings/engine.ts +274 -22
- package/src/audio/strings/measure.test-helpers.ts +2 -0
- package/src/audio/synth/oscillators.ts +31 -21
- package/src/audio/synth/voice.ts +34 -1
- package/src/audio/vocoder/bank.ts +314 -0
- package/src/audio/vocoder/carrier.ts +165 -0
- package/src/audio/vocoder/control.ts +68 -0
- package/src/audio/vocoder/detect.ts +50 -0
- package/src/audio/vocoder/index.ts +304 -0
- package/src/audio/vocoder/talkbox.ts +143 -0
- package/src/audio/wav.ts +608 -85
- package/src/audio/winds/engine.ts +306 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +104 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +492 -0
- package/src/auth/cli.ts +38 -36
- package/src/auth/credentials.ts +30 -1
- package/src/auth/login.ts +15 -9
- package/src/auth/tui.ts +19 -10
- package/src/commands/arrange.ts +44 -29
- package/src/commands/autotune.ts +421 -0
- package/src/commands/calibration.ts +74 -0
- package/src/commands/clips.ts +887 -0
- package/src/commands/drums.ts +3 -2
- package/src/commands/edit.ts +11 -4
- package/src/commands/expression.ts +104 -23
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +105 -33
- package/src/commands/grammar.ts +558 -0
- package/src/commands/granular.ts +7 -2
- package/src/commands/help.ts +652 -339
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +293 -17
- package/src/commands/modal.ts +67 -4
- package/src/commands/music.ts +1 -1
- package/src/commands/nearest.ts +53 -0
- package/src/commands/pack.ts +9 -2
- package/src/commands/param-range.ts +56 -0
- package/src/commands/parses.ts +130 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/resample.ts +281 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +8 -25
- package/src/commands/sample.ts +49 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/sing.ts +478 -0
- package/src/commands/string.ts +28 -2
- package/src/commands/strum.ts +485 -0
- package/src/commands/style.ts +415 -0
- package/src/commands/time.ts +6 -3
- package/src/commands/tuning.ts +3 -3
- package/src/commands/vocal-pitch.ts +616 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +244 -0
- package/src/fs/durable.ts +50 -0
- package/src/lang/glossary.ts +493 -0
- package/src/launch-args.ts +163 -0
- package/src/main.ts +1521 -255
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +26 -3
- package/src/project/clip-pins.ts +72 -0
- package/src/project/init.ts +23 -8
- package/src/project/sync.ts +418 -86
- package/src/render.ts +36 -1
- package/src/session/daemon.ts +3 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +154 -0
- package/src/session/port.ts +22 -4
- package/src/session/presence.ts +50 -7
- package/src/session/protocol.ts +5 -1
- package/src/session/rebase.ts +18 -5
- package/src/session/receipt.ts +258 -0
- package/src/session/store.ts +65 -43
- package/src/tui/arrange-menu.ts +65 -31
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +18 -13
- package/src/tui/fader.ts +228 -41
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/menu-clips.ts +297 -0
- package/src/tui/menu-time.ts +20 -13
- package/src/tui/menu-voice.ts +405 -0
- package/src/tui/menu.ts +1377 -193
- package/src/tui/modal-menu.ts +38 -11
- package/src/tui/performance-menu.ts +46 -1
- package/src/tui/play-chords.ts +108 -5
- package/src/tui/play-mode.ts +16 -1
- package/src/tui/play-session.ts +81 -16
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +144 -0
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +274 -40
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +144 -0
- package/tui/drawer.ts +70 -22
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +112 -63
- package/tui/guide.ts +42 -4
- package/tui/highway.ts +269 -25
- package/tui/hints.ts +192 -0
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/play-strip.ts +68 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +144 -14
- package/tui/theme.ts +27 -0
package/src/commands/rig.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type FxValues,
|
|
28
28
|
type RigStage,
|
|
29
29
|
type TrackFx,
|
|
30
|
+
SHOEGAZE_EFFECTS,
|
|
30
31
|
} from "../../core/fx.ts";
|
|
31
32
|
import {
|
|
32
33
|
instrumentForWord,
|
|
@@ -44,6 +45,7 @@ import {
|
|
|
44
45
|
type FxCommand,
|
|
45
46
|
type FxResult,
|
|
46
47
|
} from "./fx.ts";
|
|
48
|
+
import { nearest } from "./nearest.ts";
|
|
47
49
|
|
|
48
50
|
export type RigCommand =
|
|
49
51
|
| { type: "rig-show" }
|
|
@@ -66,33 +68,11 @@ function isStage(word: string): word is RigStage {
|
|
|
66
68
|
|
|
67
69
|
/** `unknown rig crunh · did you mean crunch? · rigs clean crunch …` */
|
|
68
70
|
function unknownRigMessage(name: string): string {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const distance = editDistance(name, candidate);
|
|
72
|
-
if (!best || distance < best.distance) best = { name: candidate, distance };
|
|
73
|
-
}
|
|
74
|
-
const near =
|
|
75
|
-
best && best.distance <= Math.max(2, Math.floor(name.length / 3))
|
|
76
|
-
? ` · did you mean ${best.name}?`
|
|
77
|
-
: "";
|
|
71
|
+
const match = nearest(name, RIG_PRESET_NAMES);
|
|
72
|
+
const near = match ? ` · did you mean ${match}?` : "";
|
|
78
73
|
return `unknown rig ${name}${near} · rigs ${RIG_PRESET_NAMES.join(" ")} reset`;
|
|
79
74
|
}
|
|
80
75
|
|
|
81
|
-
function editDistance(a: string, b: string): number {
|
|
82
|
-
let row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
83
|
-
for (let i = 1; i <= a.length; i++) {
|
|
84
|
-
const next = [i];
|
|
85
|
-
for (let j = 1; j <= b.length; j++)
|
|
86
|
-
next[j] = Math.min(
|
|
87
|
-
row[j]! + 1,
|
|
88
|
-
next[j - 1]! + 1,
|
|
89
|
-
row[j - 1]! + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
90
|
-
);
|
|
91
|
-
row = next;
|
|
92
|
-
}
|
|
93
|
-
return row[b.length]!;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
76
|
export function parseRigCommand(prompt: string): RigCommand | undefined {
|
|
97
77
|
if (prompt.length > 1_024) return undefined;
|
|
98
78
|
const words = prompt.trim().replace(/^\//, "").toLowerCase().split(/\s+/);
|
|
@@ -140,7 +120,7 @@ export function parseRigCommand(prompt: string): RigCommand | undefined {
|
|
|
140
120
|
}
|
|
141
121
|
|
|
142
122
|
function describeRig(values: Readonly<Record<string, FxValues | undefined>>) {
|
|
143
|
-
|
|
123
|
+
const stages = RIG_STAGES.filter((stage) => values[stage])
|
|
144
124
|
.map((stage) => {
|
|
145
125
|
const v = values[stage]!;
|
|
146
126
|
const type = String(v.type);
|
|
@@ -148,6 +128,9 @@ function describeRig(values: Readonly<Record<string, FxValues | undefined>>) {
|
|
|
148
128
|
return `${stage} ${type}${gain}`;
|
|
149
129
|
})
|
|
150
130
|
.join(" → ");
|
|
131
|
+
// 0.6.1 shoegaze stages follow the cab in the chain.
|
|
132
|
+
const gaze = SHOEGAZE_EFFECTS.filter((effect) => values[effect]);
|
|
133
|
+
return gaze.length ? `${stages} + ${gaze.join(" ")}` : stages;
|
|
151
134
|
}
|
|
152
135
|
|
|
153
136
|
export function applyRigCommand(
|
package/src/commands/sample.ts
CHANGED
|
@@ -38,7 +38,8 @@ export type SampleCommand =
|
|
|
38
38
|
}>;
|
|
39
39
|
|
|
40
40
|
/** A sample control value: a number, a switch, a unit, or null to unset. */
|
|
41
|
-
export type SampleControlValue =
|
|
41
|
+
export type SampleControlValue =
|
|
42
|
+
number | string | boolean | null | readonly number[];
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
45
|
* Per-voice sample controls `/sample set` and `set_sample` change, by
|
|
@@ -62,6 +63,12 @@ export const SAMPLE_CONTROLS = Object.freeze({
|
|
|
62
63
|
bpm: "the sample's own tempo: it follows the song's tempo map",
|
|
63
64
|
fitmode: "how a fitted sample changes time: repitch, beats or tones",
|
|
64
65
|
len: "the window lasts n beats of the song's tempo map",
|
|
66
|
+
shift: "pitch in semitones (−24..24), length unchanged",
|
|
67
|
+
formant: "formants in semitones with shift: 0 keeps the voice, off follows",
|
|
68
|
+
fadeTime: "release fade in seconds (0..2); alias fadeout",
|
|
69
|
+
fadeInTime: "attack fade in seconds (0..2); alias fadein",
|
|
70
|
+
vel: "velocity layer lo-hi (MIDI 0-127): plays only for those velocities",
|
|
71
|
+
rr: "round-robin group name: voices in a group take turns",
|
|
65
72
|
});
|
|
66
73
|
export type SampleControl = keyof typeof SAMPLE_CONTROLS;
|
|
67
74
|
|
|
@@ -70,6 +77,8 @@ const SAMPLE_ALIASES: Readonly<Record<string, string>> = {
|
|
|
70
77
|
loope: "loopEnd",
|
|
71
78
|
legato: "clip",
|
|
72
79
|
choke: "cut",
|
|
80
|
+
fadeout: "fadeTime",
|
|
81
|
+
fadein: "fadeInTime",
|
|
73
82
|
};
|
|
74
83
|
|
|
75
84
|
function controlName(raw: string): SampleControl | undefined {
|
|
@@ -176,6 +185,18 @@ export function setSampleControls(
|
|
|
176
185
|
continue;
|
|
177
186
|
}
|
|
178
187
|
if (name === "cut" && typeof value === "number") value = `cut${value}`;
|
|
188
|
+
if (name === "rr" && typeof value === "number") value = `rr${value}`;
|
|
189
|
+
if (name === "vel" && value !== null) {
|
|
190
|
+
const range = parseVelocityRange(value);
|
|
191
|
+
if (!range)
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
message: "sample · vel needs lo-hi MIDI velocities, e.g. vel 0-63",
|
|
195
|
+
};
|
|
196
|
+
next.vel = range;
|
|
197
|
+
changed.push(`vel ${range[0]}-${range[1]}`);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
179
200
|
if (value === null) delete next[field];
|
|
180
201
|
else next[field] = value;
|
|
181
202
|
changed.push(`${raw} ${value === null ? "off" : String(value)}`);
|
|
@@ -203,10 +224,24 @@ export function setSampleControls(
|
|
|
203
224
|
return {
|
|
204
225
|
ok: true,
|
|
205
226
|
next: updated,
|
|
206
|
-
message: `sample · ${trackId}/${voice} · ${changed.join(" · ")}`,
|
|
227
|
+
message: `sample · ${trackId}/${voice} · ${changed.join(" · ")}${layerHint(updated, trackId, voice)}`,
|
|
207
228
|
};
|
|
208
229
|
}
|
|
209
230
|
|
|
231
|
+
/** `0-63`, `0..63`, `64` (one velocity) → [lo, hi]; else undefined. */
|
|
232
|
+
export function parseVelocityRange(
|
|
233
|
+
value: SampleControlValue | readonly number[],
|
|
234
|
+
): [number, number] | undefined {
|
|
235
|
+
if (Array.isArray(value))
|
|
236
|
+
return value.length === 2 && value.every((v) => typeof v === "number")
|
|
237
|
+
? [value[0] as number, value[1] as number]
|
|
238
|
+
: undefined;
|
|
239
|
+
if (typeof value === "number") return [value, value];
|
|
240
|
+
if (typeof value !== "string") return undefined;
|
|
241
|
+
const match = value.trim().match(/^(\d{1,3})\s*(?:-|\.\.|,)\s*(\d{1,3})$/);
|
|
242
|
+
return match ? [Number(match[1]), Number(match[2])] : undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
210
245
|
function unquote(value: string): string {
|
|
211
246
|
const quoted = value.match(/^(["'])(.*)\1$/);
|
|
212
247
|
return quoted ? quoted[2]! : value;
|
|
@@ -372,6 +407,18 @@ export function freeVoiceName(
|
|
|
372
407
|
}
|
|
373
408
|
}
|
|
374
409
|
|
|
410
|
+
/**
|
|
411
|
+
* One-shot velocity layers swap only within a round-robin group: a kit
|
|
412
|
+
* voice with `vel` and no `rr` keeps its own pad, so say how to join them.
|
|
413
|
+
*/
|
|
414
|
+
function layerHint(score: TrackScore, trackId: string, voice: string): string {
|
|
415
|
+
const sampler = score.tracks.find((item) => item.id === trackId)?.sampler;
|
|
416
|
+
const ref = sampler?.voices[voice];
|
|
417
|
+
if (sampler?.mode !== "oneshot" || !ref?.vel || ref.rr !== undefined)
|
|
418
|
+
return "";
|
|
419
|
+
return ` · one-shot layers share a pad only within an rr group: sample ${voice} rr <group> on each layer`;
|
|
420
|
+
}
|
|
421
|
+
|
|
375
422
|
export class SamplePlacementError extends Error {}
|
|
376
423
|
|
|
377
424
|
/**
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `shift <semitones> [formant keep|follow|<n>] [<voice>]` and
|
|
3
|
+
* `fade [in|out] <seconds> [<voice>]` (0.6.1): pitch a sampler voice
|
|
4
|
+
* without changing its length, and set its fade in and out. They act on
|
|
5
|
+
* the focused sampler track's voice (named, or its only one) through
|
|
6
|
+
* `setSampleControls`, so validation and printing stay the sampler's.
|
|
7
|
+
*/
|
|
8
|
+
import type { TrackScore } from "../../core/score.ts";
|
|
9
|
+
import { fitVoice } from "./fit.ts";
|
|
10
|
+
import {
|
|
11
|
+
setSampleControls,
|
|
12
|
+
type SampleControlValue,
|
|
13
|
+
type SetSampleResult,
|
|
14
|
+
} from "./sample.ts";
|
|
15
|
+
|
|
16
|
+
export type ShiftCommand =
|
|
17
|
+
| Readonly<{
|
|
18
|
+
kind: "shift";
|
|
19
|
+
/** Semitones; null clears shift and formant. */
|
|
20
|
+
semitones: number | null;
|
|
21
|
+
/** 0 keeps the formants, null follows the pitch, undefined leaves it. */
|
|
22
|
+
formant?: number | null;
|
|
23
|
+
voice?: string;
|
|
24
|
+
}>
|
|
25
|
+
| Readonly<{
|
|
26
|
+
kind: "fade";
|
|
27
|
+
/** `in` is fadeInTime, `out` fadeTime, `both` sets each. */
|
|
28
|
+
edge: "in" | "out" | "both";
|
|
29
|
+
seconds: number | null;
|
|
30
|
+
voice?: string;
|
|
31
|
+
}>;
|
|
32
|
+
|
|
33
|
+
const VOICE = "([a-z][a-z0-9_]{0,31})";
|
|
34
|
+
const NUMBER = "([+-]?\\d+(?:\\.\\d+)?|off|none)";
|
|
35
|
+
|
|
36
|
+
/** Parse `shift ...` and `fade ...` (with or without the slash). */
|
|
37
|
+
export function parseShiftCommand(command: string): ShiftCommand | undefined {
|
|
38
|
+
const text = command.trim();
|
|
39
|
+
const shift = text.match(
|
|
40
|
+
new RegExp(
|
|
41
|
+
`^/?shift\\s+${NUMBER}(?:\\s+formant\\s+(keep|follow|[+-]?\\d+(?:\\.\\d+)?))?(?:\\s+(?:voice\\s+)?${VOICE})?$`,
|
|
42
|
+
"i",
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
if (shift) {
|
|
46
|
+
const raw = shift[1]!.toLowerCase();
|
|
47
|
+
const formant = shift[2]?.toLowerCase();
|
|
48
|
+
return {
|
|
49
|
+
kind: "shift",
|
|
50
|
+
semitones: raw === "off" || raw === "none" ? null : Number(raw),
|
|
51
|
+
...(formant === undefined
|
|
52
|
+
? {}
|
|
53
|
+
: {
|
|
54
|
+
formant:
|
|
55
|
+
formant === "keep"
|
|
56
|
+
? 0
|
|
57
|
+
: formant === "follow"
|
|
58
|
+
? null
|
|
59
|
+
: Number(formant),
|
|
60
|
+
}),
|
|
61
|
+
...(shift[3] ? { voice: shift[3].toLowerCase() } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const fade = text.match(
|
|
65
|
+
new RegExp(
|
|
66
|
+
`^/?fade(?:\\s+(in|out))?\\s+${NUMBER}(?:\\s+(?:voice\\s+)?${VOICE})?$`,
|
|
67
|
+
"i",
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
if (!fade) return undefined;
|
|
71
|
+
const raw = fade[2]!.toLowerCase();
|
|
72
|
+
return {
|
|
73
|
+
kind: "fade",
|
|
74
|
+
edge: (fade[1]?.toLowerCase() as "in" | "out" | undefined) ?? "both",
|
|
75
|
+
seconds: raw === "off" || raw === "none" ? null : Number(raw),
|
|
76
|
+
...(fade[3] ? { voice: fade[3].toLowerCase() } : {}),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Apply a parsed `shift` or `fade` to the focused track's voice. */
|
|
81
|
+
export function applyShiftCommand(
|
|
82
|
+
score: TrackScore,
|
|
83
|
+
trackId: string,
|
|
84
|
+
command: ShiftCommand,
|
|
85
|
+
): SetSampleResult {
|
|
86
|
+
const target = fitVoice(score, trackId, command.voice);
|
|
87
|
+
if ("error" in target)
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
message: target.error.replace(/^fit · /, `${command.kind} · `),
|
|
91
|
+
};
|
|
92
|
+
const values: Record<string, SampleControlValue> = {};
|
|
93
|
+
if (command.kind === "shift") {
|
|
94
|
+
// `shift off` clears the shift and its formant setting. `shift 0`
|
|
95
|
+
// clears the shift and a kept formant (0, a no-op without a shift) but
|
|
96
|
+
// keeps a formant move, so `shift 0 formant 3` is a formant-only move
|
|
97
|
+
// and stepping the shift through 0 keeps a stored formant.
|
|
98
|
+
if (command.semitones === null) {
|
|
99
|
+
values.shift = null;
|
|
100
|
+
values.formant = null;
|
|
101
|
+
} else {
|
|
102
|
+
values.shift = command.semitones === 0 ? null : command.semitones;
|
|
103
|
+
const formant =
|
|
104
|
+
command.formant !== undefined
|
|
105
|
+
? command.formant
|
|
106
|
+
: score.tracks.find((track) => track.id === trackId)?.sampler?.voices[
|
|
107
|
+
target.voice
|
|
108
|
+
]?.formant;
|
|
109
|
+
if (command.semitones === 0 && (formant === 0 || formant === null))
|
|
110
|
+
values.formant = null;
|
|
111
|
+
else if (command.formant !== undefined) values.formant = command.formant;
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
const seconds = command.seconds === 0 ? null : command.seconds;
|
|
115
|
+
if (command.edge !== "out") values.fadeInTime = seconds;
|
|
116
|
+
if (command.edge !== "in") values.fadeTime = seconds;
|
|
117
|
+
}
|
|
118
|
+
return setSampleControls(score, trackId, target.voice, values);
|
|
119
|
+
}
|