@hraness/dawg 0.6.0 → 0.6.1
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 +34 -0
- package/DAWG.md +338 -51
- package/README.md +4 -4
- package/core/chords.ts +492 -4
- package/core/diff.ts +5 -0
- package/core/expression.ts +143 -6
- package/core/fx.ts +258 -6
- package/core/granular.ts +91 -0
- package/core/instruments.ts +161 -0
- package/core/keys.ts +573 -4
- package/core/resonators.ts +291 -3
- package/core/score.ts +349 -3
- package/core/sdk/print.ts +78 -3
- package/core/sdk/v1.ts +1320 -36
- package/core/sections.ts +12 -4
- package/core/strings.ts +237 -2
- package/core/winds.ts +652 -0
- package/guides/automation.md +1 -0
- package/guides/chords.md +3 -1
- package/guides/effects.md +3 -2
- package/guides/media.md +1 -1
- package/guides/performance.md +3 -0
- package/guides/resample.md +26 -0
- package/guides/sounds.md +7 -6
- package/package.json +1 -1
- package/src/agent/agent.ts +8 -2
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/ops.ts +4 -1
- package/src/agent/preview-tool.ts +2 -1
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/tools.ts +200 -15
- package/src/agent/xcb-agent.ts +8 -2
- package/src/audio/arrange.ts +21 -0
- package/src/audio/dsp/envelope.ts +161 -0
- package/src/audio/dsp/shift.ts +256 -0
- package/src/audio/effects/chain.ts +18 -3
- package/src/audio/effects/common.ts +14 -0
- package/src/audio/effects/convolution.ts +106 -1
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/engine.ts +46 -5
- package/src/audio/fit.ts +45 -0
- package/src/audio/granular.ts +352 -21
- package/src/audio/instrument-check.ts +9 -5
- package/src/audio/instruments.ts +32 -3
- package/src/audio/keys/electric.ts +420 -0
- package/src/audio/keys/engine.ts +143 -19
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +50 -6
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/live-worker.ts +3 -1
- package/src/audio/live.ts +136 -9
- package/src/audio/preview.ts +15 -1
- package/src/audio/resample.ts +285 -0
- package/src/audio/resonators.ts +10 -2
- package/src/audio/sampler.ts +168 -21
- package/src/audio/samples.ts +26 -1
- package/src/audio/strings/body.ts +29 -16
- package/src/audio/strings/bow.ts +656 -0
- package/src/audio/strings/engine.ts +266 -20
- package/src/audio/strings/measure.test-helpers.ts +2 -0
- package/src/audio/wav.ts +119 -19
- package/src/audio/winds/engine.ts +302 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +80 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/voice.ts +479 -0
- package/src/commands/expression.ts +103 -22
- package/src/commands/fx.ts +4 -1
- package/src/commands/granular.ts +7 -2
- package/src/commands/help.ts +91 -8
- package/src/commands/keys.ts +287 -11
- package/src/commands/modal.ts +67 -4
- package/src/commands/resample.ts +281 -0
- package/src/commands/rig.ts +5 -1
- package/src/commands/sample.ts +49 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/string.ts +28 -2
- package/src/commands/strum.ts +473 -0
- package/src/commands/wind.ts +244 -0
- package/src/main.ts +129 -7
- package/src/project/check.ts +5 -1
- package/src/render.ts +16 -0
- package/src/session/presence.ts +16 -3
- package/src/tui/audition.ts +1 -1
- package/src/tui/menu.ts +637 -35
- package/src/tui/modal-menu.ts +34 -7
- package/src/tui/performance-menu.ts +42 -0
- package/src/tui/play-chords.ts +104 -3
- package/src/tui/play-mode.ts +1 -0
- package/src/tui/play-session.ts +34 -9
- package/src/tui/wind-menu.ts +144 -0
package/guides/performance.md
CHANGED
|
@@ -12,13 +12,16 @@ Targets are `all` (default), `bar 3`, `bars 2-4` or note ids.
|
|
|
12
12
|
|
|
13
13
|
- "make the bass slide like a 303" · "ghost the off-beat hats"
|
|
14
14
|
- "pedal the piano every bar" · "humanize the drums a little"
|
|
15
|
+
- "play the piano una corda" · "hold the bass note with sostenuto"
|
|
15
16
|
|
|
16
17
|
## Type it yourself
|
|
17
18
|
|
|
18
19
|
- `art staccato|legato|accent|tenuto|marcato|ghost [target]`
|
|
19
20
|
- `glide 60ms legato` per track · `glide 60ms bar 2` per note
|
|
21
|
+
- bowed strings slur overlapping or `art legato` notes in one bow stroke
|
|
20
22
|
- `bend scoop|fall|doit|-200 [target]` · `vibrato 5.5 30 0.2`
|
|
21
23
|
- `pedal 0-3.5 4-7.5` (beats) · `pedal bars` · `pedal off`
|
|
24
|
+
- `pedal soft 0-8` una corda · `pedal sost 0-4` holds keys down at 0
|
|
22
25
|
- `velcurve soft|hard|fixed 0.7` · `humanize 8 5 10 seed 3`
|
|
23
26
|
|
|
24
27
|
## Menu
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: resample
|
|
3
|
+
title: Resample, grains and shift
|
|
4
|
+
parent: sound
|
|
5
|
+
order: 8
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Turn a part into a sample, grain it, and play the grains like an instrument.
|
|
9
|
+
|
|
10
|
+
## Ask
|
|
11
|
+
|
|
12
|
+
- "resample the lead and make a grain cloud of bars 1-2"
|
|
13
|
+
- "shift the vocal sample up a fifth but keep its character"
|
|
14
|
+
|
|
15
|
+
## Type it yourself
|
|
16
|
+
|
|
17
|
+
- `resample lead` · `resample drums bars 1-2 grain` · `resample master section chorus`
|
|
18
|
+
- `shift 7` same length · `shift 7 formant keep` · `fade out 0.5` · `fade in 0.05`
|
|
19
|
+
- `grain sync 1/16` · `grain quant scale` · `grain mono on` · `grain pedal on`
|
|
20
|
+
- `automate grain-pos points 0:0.1 8:0.9` sweeps the head
|
|
21
|
+
|
|
22
|
+
## Menu
|
|
23
|
+
|
|
24
|
+
- Ctrl-K › Project › Resample: a track, an orbit or the mix
|
|
25
|
+
- Ctrl-K › Sound › granular: Sync, Quant, Mono, Pedal
|
|
26
|
+
- Ctrl-K › Sound › Sample: Shift, Formant, Fade in, Fade out
|
package/guides/sounds.md
CHANGED
|
@@ -14,16 +14,17 @@ order: 1
|
|
|
14
14
|
|
|
15
15
|
- `instrument sawtooth` · sine square triangle supersaw pulse
|
|
16
16
|
- `synth preset pluck` · pad lead bass acid keys bell organ strings
|
|
17
|
-
- `synth lpf 800 lpenv 3`
|
|
18
|
-
- `
|
|
19
|
-
- `
|
|
17
|
+
- `synth lpf 800 lpenv 3` · `synth adsr 0.01 0.2 0.5 0.3` · `synth reset`
|
|
18
|
+
- `piano felt` · `epiano suitcase` · `wurli` · `clav` · `keys hardness 0.3`
|
|
19
|
+
- `tonewheel` gospel · `combo` vox · `pipe` flutes · `rotary fast` · `keys`
|
|
20
20
|
- `wt list` tables · `wt pwm` load one · `wt 0.5` position
|
|
21
|
-
- `string sitar` ·
|
|
21
|
+
- `string sitar` · `bowed violin` · cellos erhu koto … · `string presets`
|
|
22
|
+
- `wind sax` · flute trumpet … · `wind breath 0.8` · `modal gamelan` bronzes
|
|
22
23
|
- `/pack list` · `/pack use <pack>/<sound> as kick` · `/pack info vcsl`
|
|
23
24
|
- `/sample samples/kick.wav as kick` add a sampler voice
|
|
24
|
-
- `/bpm 174`
|
|
25
|
+
- `/bpm 174` sample tempo (`bpm` is the song's) · `fitmode beats` · `len 8`
|
|
25
26
|
|
|
26
27
|
## Menu
|
|
27
28
|
|
|
28
29
|
- Ctrl-K › Sound: instrument, preset, envelope, filter, browse sounds
|
|
29
|
-
- Ctrl-K › Sound › browse sounds › Strings:
|
|
30
|
+
- Ctrl-K › Sound › browse sounds › Strings: plucked presets, Bowed
|
package/package.json
CHANGED
package/src/agent/agent.ts
CHANGED
|
@@ -203,15 +203,21 @@ export const AGENT_SYSTEM_PROMPT = [
|
|
|
203
203
|
CHORD_PROCESS,
|
|
204
204
|
"Synth voices: set_synth shapes a synth track with Strudel params (ADSR, lpf/lpenv filter envelopes, fm/fmh, supersaw unison/detune, vib, penv, noise; z_* ZzFX sounds take slide/pitchJump/lfo/zcrush…) or a preset (pad lead pluck bass keys bell…).",
|
|
205
205
|
"Plucked strings: set_string makes a track a physical-model string (nylon steel electric jangle ebass slap upright motown sitar tanpura harpsichord lute oud setar tar santur dulcimer koto harp banjo tres requinto) and overrides params (ring bright damp pos mute buzz body sym). The bare words sitar, ebass, pluck keep their older voices; use set_string for the modelled ones.",
|
|
206
|
-
"
|
|
206
|
+
"Bowed strings: set_string preset violin|viola|cello|contrabass|fiddle|erhu|kamancheh|violins|violas|cellos|contrabasses|pizz|trem plays a bowed physical model (bow params pressure speed attack vib vibmod vibdelay tremhz sord dyn). Overlapping or legato notes slur in one bow. The bare words cello, contrabass and strings keep their older voices; use set_string (or the words violin, viola, fiddle, erhu, violins, cellos) for the bowed ones. Sampler voices take vel [lo, hi] velocity layers and rr round-robin groups via set_sample.",
|
|
207
|
+
"Pianos: set_instrument piano (or grand upright felt honkytonk prepared, ballad, lofi) loads a modelled piano with real hammers, stretched inharmonic strings, dampers and the sustain pedal; set_keys tweaks it (hardness touch decay release felt stretch body…, automate as keys-<param> lanes; sym 0.5 adds sympathetic bloom under the sustain pedal for pedalled Romantic and Impressionist playing). A track already stored as instrument piano keeps the legacy tone until you set it again.",
|
|
208
|
+
"Electric keys: set_instrument epiano (rhodes; suitcase stereo vibe, dyno bright 80s), wurli, clav (funkclav); set_keys tweaks bark bell tone vibe (epiano), trem (wurli), pickup neck|bridge|both|out and mute (clav). Soft and sostenuto pedals: set_piano_pedals on any modelled piano.",
|
|
209
|
+
'Organs: set_instrument tonewheel (hammond b3 gospel jazzorgan), combo (farfisa vox) or pipe (church flutes cornet reeds celeste) loads a modelled organ; instrument organ stays the legacy sine. set_keys drawbars "888800008" (16\' to 1\'), registers "08880" (combo), stops ["principal8","octave4"] or plenum|flutes|cornet|reeds|strings|full (pipe), rotary slow|fast|stop, params perc 3rd percdecay percvol click scanner c3 drive chiff wind trem (each family takes only its own rows); automate keys-rotary and keys-drive lanes.',
|
|
207
210
|
"Mallets and bells: set_instrument vibes (or marimba via modal, xylophone glock celesta chimes kalimba mbira steelpan bowl gong timpani) plays the modal engine; set_modal changes mallet (yarn…brass), hardness, position, ring, damp, motor/motordepth (vibes tremolo), buzz, click. Plain instrument marimba stays the legacy tone.",
|
|
211
|
+
"Gamelan: set_modal preset saron demung slenthem gangsa gender bonang kenong kethuk kempul (gong too) with tuning slendro or pelog; for ombak (the shimmer) put the same part on two gangsa tracks and set_modal pair <other track> on one: it sounds ombak Hz (default 6) above its partner. Frame drums: daf bodhran tabla.",
|
|
212
|
+
"Winds and brass: set_instrument flute recorder whistle ney shakuhachi panpipe suling bansuri clarinet bassclarinet oboe bassoon sax altosax barisax trumpet harmon plunger trombone tuba horn plays the wind engine; set_wind changes breath, bright, mute (open straight cup harmon plunger), players (2..8 spreads a section over chord tones), growl, vib. Lines slur unless re-tongued (gaps or articulations). Plain instrument wind stays the legacy tone.",
|
|
208
213
|
"Sampler voices: set_sample sets Strudel sample controls per voice (begin end speed unit loop loopBegin loopEnd clip/legato fit loopAt accelerate squiz cut); splice = slices with fit.",
|
|
209
214
|
"Fitting samples (0.6): fit_sample sets a voice's own bpm (or len in beats) so it follows the song's tempo map; fitmode beats for drums and speech (hits stay sharp), tones for pads and vocals (pitch kept), repitch (default) like tape.",
|
|
210
215
|
"Effects (set_fx; set_effects and set_automation): the brief's effects list is the fixed chain order with each effect's simple params. set_fx turns an effect on with good defaults, loads a preset or sets params by dawg or Strudel name. Lanes: volume, pan, filter, resonance, delay-feedback, delay-mix, wt and <effect>-<param> (e.g. autofilter-cutoff). Sidechain: put pads/bass on `orbit` 2 and give the kick `duck` (orbit 2, preset pump); a ducker never ducks itself.",
|
|
211
216
|
"Guitar rig (set_rig): rig loads a whole rig (clean crunch punk ragged lead metal fuzz octave funk wah bachata spring bassdrive reese jangle alt) on a guitar or bass track; stomp, head and cab tweak the pedal, amp (level-matched, gate in dB) and cabinet. Tracks named jangle punk funk ragged gtr-lead gtr-metal bachata start with a guitar voice and that rig. `amp` is Strudel gain; the guitar amp is head.",
|
|
217
|
+
"Shoegaze: rig shoegaze|glide|dreampop|swell|ebow (or a track named shoegaze); set_fx wobble (tremolo-arm bend: depth cents, rate, drift), bloom (feedback swell on the top note), swell (volume swell: time), double (second take: time, drift, width); reverb ir builtin:reverse|gate|spring. Strumming: set_guitar (tune dadgad, capo, hand, ring), strum_chords (chords or the track's own, strokes folk|pop|punk… or D-DU-UDU, speed ms), write_chords perform guitar.",
|
|
212
218
|
"Previewing: preview_sound renders a track, or candidate sound tool calls (changes: [{tool, args}]), without committing, returns loudness, brightness and a comparison, and plays it once in the user's window. Use it when choosing between sounds (tables, presets, effect amounts), say how it sounds, then commit with the normal tools.",
|
|
213
219
|
"Wavetable synth: set_wavetable picks a table (built-ins offline, Strudel wt_ sets fetched once) and scans it by position wt.",
|
|
214
|
-
`Granular: set_granular makes a track a grain cloud (presets cloud hold sparkle swarm stutter microloop backwards dust) of a synth source (src synth:pad, the default; synth:<name>@<note>) or one of its own sampler voices, and overrides params (${Object.keys(GRANULAR_PARAMS).join(" ")}; repeat and hold latch the head for beat repeats, begin and end pick a region, root takes a MIDI number or note name); off returns to the previous voice; the same seed gives the same grains.`,
|
|
220
|
+
`Granular: set_granular makes a track a grain cloud (presets cloud hold sparkle swarm stutter microloop backwards dust) of a synth source (src synth:pad, the default; synth:<name>@<note>) or one of its own sampler voices, and overrides params (${Object.keys(GRANULAR_PARAMS).join(" ")}; repeat and hold latch the head for beat repeats, begin and end pick a region, root takes a MIDI number or note name); off returns to the previous voice; the same seed gives the same grains. Grain play: sync "1/16" puts grain onsets on the tempo grid, quant scale|chord snaps grain pitches, mono true glides legato notes through one cloud, pedal true freezes the head while the sustain pedal is down; set_automation grain-<param> lanes (grain-pos, grain-scan, grain-pitch …) move a parameter over beats. Resample: the resample tool renders a track, orbit or the mix (a section or bars) to a pinned WAV and adds a sampler track, or with grain a granular track, that plays it; set_sample shift (semitones, length kept, formant 0 keeps a voice's character) and fadeInTime/fadeTime shape a sampler voice.`,
|
|
215
221
|
"Mastering: set_master sets the song master and loudness target; measure_mix reports LUFS, true peak, balance and correlation. Master only when asked for loudness or a finished sound, and measure before and after.",
|
|
216
222
|
"If a call is rejected, read the diagnostic and either fix the arguments or stop.",
|
|
217
223
|
WORKSPACE_PROMPT,
|
package/src/agent/chord-tools.ts
CHANGED
|
@@ -12,8 +12,12 @@ import {
|
|
|
12
12
|
generateProgression,
|
|
13
13
|
keyName,
|
|
14
14
|
parseKey,
|
|
15
|
+
DEFAULT_STROKE_SPEED,
|
|
16
|
+
GUITAR_TUNING_NAMES,
|
|
15
17
|
PERFORM_MODES,
|
|
16
18
|
PROGRESSION_PRESETS,
|
|
19
|
+
STROKE_PATTERN_NAMES,
|
|
20
|
+
strokeGrid,
|
|
17
21
|
PROGRESSION_STYLES,
|
|
18
22
|
renderProgression,
|
|
19
23
|
resolveChord,
|
|
@@ -27,6 +31,12 @@ import { midiToPitch } from "../../core/pitch.ts";
|
|
|
27
31
|
import { resolveTuning, snapToTuning } from "../../core/tuning.ts";
|
|
28
32
|
import { SCORE_LIMITS, type ScoreOperation } from "../../core/score.ts";
|
|
29
33
|
import type { AgentTool, ToolContext } from "./tools.ts";
|
|
34
|
+
import {
|
|
35
|
+
applyGuitarCommand,
|
|
36
|
+
parseGuitarCommand,
|
|
37
|
+
planStrum,
|
|
38
|
+
StrumError,
|
|
39
|
+
} from "../commands/strum.ts";
|
|
30
40
|
|
|
31
41
|
/** Most notes one write_chords call may add (arpeggios multiply fast). */
|
|
32
42
|
export const MAX_CHORD_NOTES = 512;
|
|
@@ -193,7 +203,9 @@ export const CHORD_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
193
203
|
perform: { type: "string", enum: [...PERFORM_MODES] },
|
|
194
204
|
rate: { ...num, description: "arp step, beats" },
|
|
195
205
|
octaves: int,
|
|
196
|
-
strum: num,
|
|
206
|
+
strum: { ...num, description: "strum/strum-up gap, beats" },
|
|
207
|
+
strokes: { ...str, description: "perform guitar: see strum_chords" },
|
|
208
|
+
speed: { ...num, description: "perform guitar: ms per stroke" },
|
|
197
209
|
velocity: num,
|
|
198
210
|
pattern: { type: "string", enum: CHORD_PATTERNS.map((p) => p.name) },
|
|
199
211
|
bass: bool,
|
|
@@ -225,6 +237,15 @@ export const CHORD_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
225
237
|
const velocity = finite(args, "velocity", 0, 1);
|
|
226
238
|
const rate = finite(args, "rate", 0.0625, 4);
|
|
227
239
|
const strum = finite(args, "strum", 0, 1);
|
|
240
|
+
const speedMs = finite(args, "speed", 0, 200);
|
|
241
|
+
const strokes = args.strokes;
|
|
242
|
+
if (
|
|
243
|
+
strokes !== undefined &&
|
|
244
|
+
(typeof strokes !== "string" || !strokeGrid(strokes))
|
|
245
|
+
)
|
|
246
|
+
throw new ChordToolError(
|
|
247
|
+
`strokes must be ${STROKE_PATTERN_NAMES.join(", ")} or a grid of D U d u x - .`,
|
|
248
|
+
);
|
|
228
249
|
const octaves = integer(args, "octaves", 1, 4);
|
|
229
250
|
const pattern = args.pattern;
|
|
230
251
|
if (
|
|
@@ -259,6 +280,16 @@ export const CHORD_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
259
280
|
...(velocity !== undefined ? { velocity } : {}),
|
|
260
281
|
seed: integer(args, "seed", 0, 2 ** 31) ?? 0,
|
|
261
282
|
...(typeof pattern === "string" ? { pattern } : {}),
|
|
283
|
+
...(mode === "guitar"
|
|
284
|
+
? {
|
|
285
|
+
...(typeof strokes === "string" ? { strokes } : {}),
|
|
286
|
+
speed: (speedMs ?? DEFAULT_STROKE_SPEED * 1000) / 1000,
|
|
287
|
+
tempo: context.score.tempoBpm,
|
|
288
|
+
...(guitarOf(context, trackId)
|
|
289
|
+
? { guitar: guitarOf(context, trackId) }
|
|
290
|
+
: {}),
|
|
291
|
+
}
|
|
292
|
+
: {}),
|
|
262
293
|
},
|
|
263
294
|
});
|
|
264
295
|
const total = rendered.notes.length + rendered.bass.length;
|
|
@@ -308,8 +339,129 @@ export const CHORD_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
308
339
|
};
|
|
309
340
|
},
|
|
310
341
|
},
|
|
342
|
+
{
|
|
343
|
+
name: "strum_chords",
|
|
344
|
+
description: `Strum chords as fretted guitar notes (set_guitar). chords omitted strums the track's block chords. strokes ${STROKE_PATTERN_NAMES.join("|")} or a DUdux-. grid; speed ms/stroke (22); step grid beats; each beats/chord (a bar).`,
|
|
345
|
+
parameters: {
|
|
346
|
+
type: "object",
|
|
347
|
+
properties: {
|
|
348
|
+
trackId: str,
|
|
349
|
+
chords: chordsSchema,
|
|
350
|
+
strokes: str,
|
|
351
|
+
speed: num,
|
|
352
|
+
step: num,
|
|
353
|
+
each: num,
|
|
354
|
+
at: num,
|
|
355
|
+
velocity: num,
|
|
356
|
+
seed: int,
|
|
357
|
+
},
|
|
358
|
+
additionalProperties: false,
|
|
359
|
+
},
|
|
360
|
+
plan(args, context) {
|
|
361
|
+
const trackId = trackOf(args.trackId, context, "trackId");
|
|
362
|
+
const chords = args.chords;
|
|
363
|
+
if (
|
|
364
|
+
chords !== undefined &&
|
|
365
|
+
(!Array.isArray(chords) ||
|
|
366
|
+
chords.length > MAX_CHORDS ||
|
|
367
|
+
!chords.every((c) => typeof c === "string"))
|
|
368
|
+
)
|
|
369
|
+
throw new ChordToolError(`chords must be up to ${MAX_CHORDS} strings`);
|
|
370
|
+
if (args.strokes !== undefined && typeof args.strokes !== "string")
|
|
371
|
+
throw new ChordToolError("strokes must be a string");
|
|
372
|
+
const speedMs = finite(args, "speed", 0, 200);
|
|
373
|
+
const step = finite(args, "step", 1 / 32, 4);
|
|
374
|
+
const each = finite(args, "each", 1e-3, 64);
|
|
375
|
+
const at = finite(args, "at", 0, Infinity);
|
|
376
|
+
const velocity = finite(args, "velocity", 0, 1);
|
|
377
|
+
const seed = integer(args, "seed", 0, 2 ** 31);
|
|
378
|
+
try {
|
|
379
|
+
const plan = planStrum(
|
|
380
|
+
context.score,
|
|
381
|
+
trackId,
|
|
382
|
+
{
|
|
383
|
+
chords: (chords as string[] | undefined) ?? [],
|
|
384
|
+
...(typeof args.strokes === "string"
|
|
385
|
+
? { strokes: args.strokes }
|
|
386
|
+
: {}),
|
|
387
|
+
...(speedMs !== undefined ? { speedMs } : {}),
|
|
388
|
+
...(step !== undefined ? { step } : {}),
|
|
389
|
+
...(each !== undefined ? { each } : {}),
|
|
390
|
+
...(at !== undefined ? { at } : {}),
|
|
391
|
+
...(velocity !== undefined ? { velocity } : {}),
|
|
392
|
+
...(seed !== undefined ? { seed } : {}),
|
|
393
|
+
},
|
|
394
|
+
(index) => context.newNoteId(trackId, index),
|
|
395
|
+
MAX_CHORD_NOTES,
|
|
396
|
+
);
|
|
397
|
+
return {
|
|
398
|
+
kind: "score",
|
|
399
|
+
operations: plan.operations,
|
|
400
|
+
trackId,
|
|
401
|
+
summary: `${trackId} strum ${plan.names.join(" ")}`.slice(0, 160),
|
|
402
|
+
};
|
|
403
|
+
} catch (error) {
|
|
404
|
+
if (error instanceof StrumError)
|
|
405
|
+
throw new ChordToolError(error.message);
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
name: "set_guitar",
|
|
412
|
+
description: `Guitar fretting for strum: tune ${GUITAR_TUNING_NAMES.join("|")} or notes low-high ("D A D G A D"), capo 0..12, hand 3..6 frets, ring 0..1, position 0..12, reset.`,
|
|
413
|
+
parameters: {
|
|
414
|
+
type: "object",
|
|
415
|
+
properties: {
|
|
416
|
+
trackId: str,
|
|
417
|
+
tune: str,
|
|
418
|
+
capo: int,
|
|
419
|
+
hand: int,
|
|
420
|
+
ring: num,
|
|
421
|
+
position: int,
|
|
422
|
+
reset: bool,
|
|
423
|
+
},
|
|
424
|
+
additionalProperties: false,
|
|
425
|
+
},
|
|
426
|
+
plan(args, context) {
|
|
427
|
+
const trackId = trackOf(args.trackId, context, "trackId");
|
|
428
|
+
let score = context.score;
|
|
429
|
+
const messages: string[] = [];
|
|
430
|
+
const run = (command: string) => {
|
|
431
|
+
const parsed = parseGuitarCommand(command);
|
|
432
|
+
const result = parsed
|
|
433
|
+
? applyGuitarCommand(score, trackId, parsed)
|
|
434
|
+
: { ok: false, message: `cannot parse ${command}` };
|
|
435
|
+
if (!result.ok) throw new ChordToolError(result.message);
|
|
436
|
+
if ("next" in result && result.next) score = result.next;
|
|
437
|
+
messages.push(result.message);
|
|
438
|
+
};
|
|
439
|
+
if (args.reset === true) run("guitar reset");
|
|
440
|
+
if (args.tune !== undefined) run(`guitar tune ${String(args.tune)}`);
|
|
441
|
+
for (const field of ["capo", "hand", "ring", "position"] as const)
|
|
442
|
+
if (args[field] !== undefined)
|
|
443
|
+
run(`guitar ${field} ${String(args[field])}`);
|
|
444
|
+
if (messages.length === 0)
|
|
445
|
+
throw new ChordToolError(
|
|
446
|
+
"give tune, capo, hand, ring, position or reset",
|
|
447
|
+
);
|
|
448
|
+
const guitar = score.tracks.find((t) => t.id === trackId)?.guitar;
|
|
449
|
+
return {
|
|
450
|
+
kind: "score",
|
|
451
|
+
operations: [
|
|
452
|
+
{ type: "updateTrack", trackId, patch: { guitar: guitar ?? null } },
|
|
453
|
+
],
|
|
454
|
+
trackId,
|
|
455
|
+
summary: messages.at(-1)!,
|
|
456
|
+
};
|
|
457
|
+
},
|
|
458
|
+
},
|
|
311
459
|
]);
|
|
312
460
|
|
|
461
|
+
function guitarOf(context: ToolContext, trackId: string) {
|
|
462
|
+
return context.score.tracks.find((track) => track.id === trackId)?.guitar;
|
|
463
|
+
}
|
|
464
|
+
|
|
313
465
|
function trackOf(value: unknown, context: ToolContext, label: string): string {
|
|
314
466
|
const trackId = value ?? context.focusedTrackId;
|
|
315
467
|
if (typeof trackId !== "string" || trackId.length > SCORE_LIMITS.maxIdLength)
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
ExpressionValidationError,
|
|
10
10
|
GLIDE_MODES,
|
|
11
11
|
PEDAL_STATES,
|
|
12
|
+
SOSTENUTO_STATES,
|
|
12
13
|
VELOCITY_CURVES,
|
|
13
14
|
normalizeArticulation,
|
|
14
15
|
normalizeBend,
|
|
@@ -20,12 +21,14 @@ import {
|
|
|
20
21
|
normalizeVibrato,
|
|
21
22
|
normalizeNoteHumanize,
|
|
22
23
|
type NoteExpressionPatch,
|
|
24
|
+
type PedalState,
|
|
23
25
|
} from "../../core/expression.ts";
|
|
24
26
|
import {
|
|
25
27
|
SCORE_LIMITS,
|
|
26
28
|
type ScoreOperation,
|
|
27
29
|
type TrackPatch,
|
|
28
30
|
} from "../../core/score.ts";
|
|
31
|
+
import { softPedalNote } from "../../core/keys.ts";
|
|
29
32
|
import { loopTicksOf } from "../../core/tempo.ts";
|
|
30
33
|
import { BEND_SHAPES, barPedal } from "../commands/expression.ts";
|
|
31
34
|
import type { AgentTool, ToolContext } from "./tools.ts";
|
|
@@ -333,4 +336,92 @@ export const EXPRESSION_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
333
336
|
};
|
|
334
337
|
},
|
|
335
338
|
},
|
|
339
|
+
{
|
|
340
|
+
// keys-electric (0.6.1): the modelled piano's soft and middle pedals.
|
|
341
|
+
name: "set_piano_pedals",
|
|
342
|
+
description:
|
|
343
|
+
'Modelled piano soft (una corda: fewer strings, softer, darker) and sostenuto (holds only keys down when it presses) pedals. Each: [{beat, state}], "bars" (whole loop) or null. Sostenuto has no half.',
|
|
344
|
+
parameters: {
|
|
345
|
+
type: "object",
|
|
346
|
+
properties: {
|
|
347
|
+
trackId: { type: "string" },
|
|
348
|
+
soft: pedalSchema(PEDAL_STATES),
|
|
349
|
+
sostenuto: pedalSchema(SOSTENUTO_STATES),
|
|
350
|
+
},
|
|
351
|
+
additionalProperties: false,
|
|
352
|
+
},
|
|
353
|
+
plan(args, context) {
|
|
354
|
+
const trackId = trackOf(args, context);
|
|
355
|
+
const score = context.score;
|
|
356
|
+
const patch: Record<string, unknown> = {};
|
|
357
|
+
if (args.soft !== undefined)
|
|
358
|
+
patch.softPedal = pedalLane(args.soft, "soft", score, PEDAL_STATES);
|
|
359
|
+
if (args.sostenuto !== undefined)
|
|
360
|
+
patch.sostenuto = pedalLane(
|
|
361
|
+
args.sostenuto,
|
|
362
|
+
"sostenuto",
|
|
363
|
+
score,
|
|
364
|
+
SOSTENUTO_STATES,
|
|
365
|
+
);
|
|
366
|
+
if (Object.keys(patch).length === 0)
|
|
367
|
+
throw new ExpressionToolError("set soft, sostenuto or both");
|
|
368
|
+
const track = score.tracks.find((candidate) => candidate.id === trackId);
|
|
369
|
+
const silent =
|
|
370
|
+
patch.softPedal && track ? softPedalNote(track) : undefined;
|
|
371
|
+
if (silent) throw new ExpressionToolError(`soft pedal · ${silent}`);
|
|
372
|
+
return {
|
|
373
|
+
kind: "score",
|
|
374
|
+
operations: [
|
|
375
|
+
{ type: "updateTrack", trackId, patch: patch as TrackPatch },
|
|
376
|
+
],
|
|
377
|
+
trackId,
|
|
378
|
+
summary: `${trackId} ${Object.entries(patch)
|
|
379
|
+
.map(([key, value]) => (value === null ? `${key} off` : key))
|
|
380
|
+
.join(", ")}`,
|
|
381
|
+
};
|
|
382
|
+
},
|
|
383
|
+
},
|
|
336
384
|
] satisfies AgentTool[]);
|
|
385
|
+
|
|
386
|
+
function pedalSchema(states: readonly string[]) {
|
|
387
|
+
return {
|
|
388
|
+
type: ["array", "string", "null"],
|
|
389
|
+
items: {
|
|
390
|
+
type: "object",
|
|
391
|
+
properties: {
|
|
392
|
+
beat: { type: "number" },
|
|
393
|
+
state: { type: "string", enum: states },
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** A soft or sostenuto lane from tool arguments: events, "bars" or null. */
|
|
400
|
+
function pedalLane(
|
|
401
|
+
value: unknown,
|
|
402
|
+
label: string,
|
|
403
|
+
score: ToolContext["score"],
|
|
404
|
+
states: readonly PedalState[],
|
|
405
|
+
): unknown {
|
|
406
|
+
if (value === null) return null;
|
|
407
|
+
// Soft and sostenuto hold through the loop (no re-pedal at each bar).
|
|
408
|
+
if (value === "bars")
|
|
409
|
+
return [
|
|
410
|
+
{ tick: 0, state: "down" },
|
|
411
|
+
{ tick: loopTicksOf(score), state: "up" },
|
|
412
|
+
];
|
|
413
|
+
if (!Array.isArray(value))
|
|
414
|
+
throw new ExpressionToolError(`${label} must be an array, "bars" or null`);
|
|
415
|
+
const events = value.map((item: unknown, index) => {
|
|
416
|
+
const event = (item ?? {}) as Record<string, unknown>;
|
|
417
|
+
const at = beat(event, "beat");
|
|
418
|
+
if (at === undefined)
|
|
419
|
+
throw new ExpressionToolError(`${label}[${index}].beat is required`);
|
|
420
|
+
return { tick: Math.round(at * score.ticksPerBeat), state: event.state };
|
|
421
|
+
});
|
|
422
|
+
return (
|
|
423
|
+
validated(() =>
|
|
424
|
+
normalizePedal(events, loopTicksOf(score), label, states),
|
|
425
|
+
) ?? null
|
|
426
|
+
);
|
|
427
|
+
}
|
package/src/agent/ops.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { pitchToMidi } from "../../core/pitch.ts";
|
|
|
3
3
|
import {
|
|
4
4
|
instrumentPatchForWord,
|
|
5
5
|
type TrackModal,
|
|
6
|
+
type TrackWind,
|
|
6
7
|
} from "../../core/resonators.ts";
|
|
7
8
|
import { SCORE_LIMITS } from "../../core/score.ts";
|
|
8
9
|
|
|
@@ -206,10 +207,12 @@ export function instrumentPatch(word: string): {
|
|
|
206
207
|
instrument: string;
|
|
207
208
|
string?: { preset: string };
|
|
208
209
|
modal?: TrackModal;
|
|
210
|
+
wind?: TrackWind;
|
|
209
211
|
} {
|
|
210
212
|
const meaning = resolveInstrumentWord(word);
|
|
211
213
|
if (!meaning) return { instrument: word };
|
|
212
|
-
if (meaning.field === "modal"
|
|
214
|
+
if (meaning.field === "modal" || meaning.field === "wind")
|
|
215
|
+
return instrumentPatchForWord(word);
|
|
213
216
|
if (meaning.field === "string" && meaning.preset)
|
|
214
217
|
return {
|
|
215
218
|
instrument: meaning.instrument,
|
|
@@ -40,6 +40,7 @@ export const PREVIEWABLE_TOOLS: readonly string[] = Object.freeze([
|
|
|
40
40
|
"set_synth",
|
|
41
41
|
"set_string",
|
|
42
42
|
"set_modal",
|
|
43
|
+
"set_wind",
|
|
43
44
|
"set_sample",
|
|
44
45
|
"fit_sample",
|
|
45
46
|
"set_effects",
|
|
@@ -159,7 +160,7 @@ export function previewSoundTool(
|
|
|
159
160
|
return {
|
|
160
161
|
name: "preview_sound",
|
|
161
162
|
description:
|
|
162
|
-
"Hear a track, or a candidate sound change, without committing it. changes: sound tool calls to try ({tool, args} for set_fx, set_rig, set_synth, set_string, set_modal, set_wavetable, set_instrument, set_sample, fit_sample, set_effects, set_mix, set_automation, set_drum_kit, use_sound), applied to a copy. Renders the track's notes over up to 4 bars (or a short phrase by role when it has none), solo or in context, and returns RMS/peak dBFS, spectral centroid and a one-line description for the current and the candidate sound. When the user's window is open it plays the snippet once. Then commit with the normal tools if it sounds right.",
|
|
163
|
+
"Hear a track, or a candidate sound change, without committing it. changes: sound tool calls to try ({tool, args} for set_fx, set_rig, set_synth, set_string, set_modal, set_wind, set_wavetable, set_instrument, set_sample, fit_sample, set_effects, set_mix, set_automation, set_drum_kit, use_sound), applied to a copy. Renders the track's notes over up to 4 bars (or a short phrase by role when it has none), solo or in context, and returns RMS/peak dBFS, spectral centroid and a one-line description for the current and the candidate sound. When the user's window is open it plays the snippet once. Then commit with the normal tools if it sounds right.",
|
|
163
164
|
parameters: {
|
|
164
165
|
type: "object",
|
|
165
166
|
properties: {
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent tool `resample` (0.6.1): the `resample` prompt command for the
|
|
3
|
+
* model. Renders a track, an orbit or the master over the song, a section
|
|
4
|
+
* or a bar range to a pinned WAV under the project and adds a sampler (or,
|
|
5
|
+
* with grain, a granular) track that plays it, committed as ordinary score
|
|
6
|
+
* operations.
|
|
7
|
+
*/
|
|
8
|
+
import { SCORE_LIMITS } from "../../core/score.ts";
|
|
9
|
+
import {
|
|
10
|
+
parseResampleCommand,
|
|
11
|
+
runResample,
|
|
12
|
+
type ResampleCommand,
|
|
13
|
+
} from "../commands/resample.ts";
|
|
14
|
+
import { hasSamplerTracks, SampleLibrary } from "../audio/samples.ts";
|
|
15
|
+
import type { AgentTool } from "./tools.ts";
|
|
16
|
+
|
|
17
|
+
class ResampleToolError extends Error {
|
|
18
|
+
constructor(message: string) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "ToolArgumentError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The tool's arguments as the prompt command they stand for. */
|
|
25
|
+
export function resampleToolCommand(
|
|
26
|
+
args: Record<string, unknown>,
|
|
27
|
+
focusedTrackId: string,
|
|
28
|
+
): ResampleCommand {
|
|
29
|
+
const words = ["resample"];
|
|
30
|
+
const source = args.source ?? "track";
|
|
31
|
+
if (source === "track") {
|
|
32
|
+
const trackId = args.trackId ?? focusedTrackId;
|
|
33
|
+
if (typeof trackId !== "string" || trackId.length === 0)
|
|
34
|
+
throw new ResampleToolError("trackId must be a track id");
|
|
35
|
+
words.push(trackId);
|
|
36
|
+
} else if (source === "orbit") {
|
|
37
|
+
if (!Number.isInteger(args.orbit))
|
|
38
|
+
throw new ResampleToolError("orbit must be an integer");
|
|
39
|
+
words.push("orbit", String(args.orbit));
|
|
40
|
+
} else if (source === "master") words.push("master");
|
|
41
|
+
else throw new ResampleToolError("source is track, orbit or master");
|
|
42
|
+
if (args.section !== undefined) {
|
|
43
|
+
if (typeof args.section !== "string" || args.section.length === 0)
|
|
44
|
+
throw new ResampleToolError("section must be a section name");
|
|
45
|
+
words.push("section", args.section);
|
|
46
|
+
} else if (args.bars !== undefined) {
|
|
47
|
+
const bars = args.bars;
|
|
48
|
+
if (
|
|
49
|
+
!Array.isArray(bars) ||
|
|
50
|
+
bars.length !== 2 ||
|
|
51
|
+
!bars.every((bar) => Number.isInteger(bar))
|
|
52
|
+
)
|
|
53
|
+
throw new ResampleToolError("bars must be [first, last], 1-based");
|
|
54
|
+
words.push("bars", `${bars[0]}-${bars[1]}`);
|
|
55
|
+
}
|
|
56
|
+
if (args.post === true) words.push("post");
|
|
57
|
+
if (args.grain === true) words.push("grain");
|
|
58
|
+
if (args.as !== undefined) {
|
|
59
|
+
if (typeof args.as !== "string")
|
|
60
|
+
throw new ResampleToolError("as must be a track id");
|
|
61
|
+
words.push("as", args.as);
|
|
62
|
+
}
|
|
63
|
+
const command = parseResampleCommand(words.join(" "));
|
|
64
|
+
if (!command) throw new ResampleToolError(`cannot ${words.join(" ")}`);
|
|
65
|
+
return command;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const RESAMPLE_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
69
|
+
{
|
|
70
|
+
name: "resample",
|
|
71
|
+
description:
|
|
72
|
+
"Bounce a track (default focused), an orbit (no master) or the master over the song, a section or bars [first,last] to a pinned WAV, adding a one-shot sampler track that plays it in place; grain adds a granular track instead; post keeps the master chain.",
|
|
73
|
+
parameters: {
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: {
|
|
76
|
+
source: { type: "string", enum: ["track", "orbit", "master"] },
|
|
77
|
+
trackId: { type: "string", maxLength: SCORE_LIMITS.maxIdLength },
|
|
78
|
+
orbit: { type: "integer", minimum: 1, maximum: 16 },
|
|
79
|
+
section: { type: "string" },
|
|
80
|
+
bars: {
|
|
81
|
+
type: "array",
|
|
82
|
+
items: { type: "integer", minimum: 1 },
|
|
83
|
+
minItems: 2,
|
|
84
|
+
maxItems: 2,
|
|
85
|
+
},
|
|
86
|
+
grain: { type: "boolean" },
|
|
87
|
+
post: { type: "boolean" },
|
|
88
|
+
as: { type: "string", maxLength: SCORE_LIMITS.maxIdLength },
|
|
89
|
+
},
|
|
90
|
+
additionalProperties: false,
|
|
91
|
+
},
|
|
92
|
+
plan(args, context) {
|
|
93
|
+
const command = resampleToolCommand(args, context.focusedTrackId);
|
|
94
|
+
return {
|
|
95
|
+
kind: "prepare",
|
|
96
|
+
summary: "resample",
|
|
97
|
+
run: async (action) => {
|
|
98
|
+
const root = action.workspace?.root ?? process.cwd();
|
|
99
|
+
const samples = hasSamplerTracks(context.score)
|
|
100
|
+
? await new SampleLibrary({
|
|
101
|
+
projectRoot: root,
|
|
102
|
+
...(action.packs ? { packs: action.packs } : {}),
|
|
103
|
+
}).load(context.score)
|
|
104
|
+
: undefined;
|
|
105
|
+
const result = await runResample({
|
|
106
|
+
projectRoot: root,
|
|
107
|
+
score: context.score,
|
|
108
|
+
command,
|
|
109
|
+
...(samples ? { samples } : {}),
|
|
110
|
+
});
|
|
111
|
+
if (!result.ok) throw new ResampleToolError(result.message);
|
|
112
|
+
const track = result.next.tracks.find(
|
|
113
|
+
(t) => t.id === result.trackId,
|
|
114
|
+
)!;
|
|
115
|
+
const notes = result.next.notes.filter(
|
|
116
|
+
(note) => note.trackId === result.trackId,
|
|
117
|
+
);
|
|
118
|
+
return {
|
|
119
|
+
kind: "score",
|
|
120
|
+
operations: [
|
|
121
|
+
{ type: "addTrack", track },
|
|
122
|
+
...notes.map((note) => ({ type: "addNote" as const, note })),
|
|
123
|
+
],
|
|
124
|
+
summary: result.message,
|
|
125
|
+
trackId: result.trackId,
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
]);
|