@hraness/dawg 0.5.0 → 0.6.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/DAWG.md +269 -13
  3. package/README.md +4 -4
  4. package/core/diff.ts +4 -0
  5. package/core/fx.ts +437 -2
  6. package/core/granular.ts +528 -0
  7. package/core/instruments.ts +281 -0
  8. package/core/keys.ts +386 -0
  9. package/core/resonators.ts +574 -0
  10. package/core/score.ts +181 -6
  11. package/core/sdk/eval-child.ts +2 -0
  12. package/core/sdk/print.ts +169 -13
  13. package/core/sdk/sync-instruments.ts +58 -0
  14. package/core/sdk/v1.ts +1114 -21
  15. package/core/sections.ts +34 -8
  16. package/core/strings.ts +845 -0
  17. package/guides/effects.md +3 -1
  18. package/guides/sounds.md +4 -0
  19. package/guides/tempo.md +1 -0
  20. package/package.json +1 -1
  21. package/src/agent/agent.ts +7 -0
  22. package/src/agent/granular-tools.ts +138 -0
  23. package/src/agent/models.ts +4 -4
  24. package/src/agent/ops.ts +39 -2
  25. package/src/agent/preview-tool.ts +6 -1
  26. package/src/agent/tools.ts +538 -14
  27. package/src/agent/xcb-agent.ts +7 -0
  28. package/src/audio/arrange.ts +21 -3
  29. package/src/audio/dsp/bank.ts +233 -0
  30. package/src/audio/dsp/fft.ts +6 -0
  31. package/src/audio/dsp/filters.ts +57 -0
  32. package/src/audio/dsp/interp.ts +112 -0
  33. package/src/audio/dsp/modal.ts +684 -0
  34. package/src/audio/dsp/onset.ts +197 -0
  35. package/src/audio/dsp/oversample.ts +202 -0
  36. package/src/audio/dsp/rng.ts +37 -0
  37. package/src/audio/dsp/shape.ts +81 -0
  38. package/src/audio/dsp/stft.ts +73 -0
  39. package/src/audio/dsp/window.ts +77 -0
  40. package/src/audio/effects/chain.ts +6 -1
  41. package/src/audio/effects/rig/cab.ts +99 -0
  42. package/src/audio/effects/rig/filters.ts +152 -0
  43. package/src/audio/effects/rig/gate.ts +39 -0
  44. package/src/audio/effects/rig/head.ts +487 -0
  45. package/src/audio/effects/rig/index.ts +170 -0
  46. package/src/audio/effects/rig/section.ts +78 -0
  47. package/src/audio/effects/rig/stomp.ts +238 -0
  48. package/src/audio/engine.ts +10 -1
  49. package/src/audio/fit.ts +402 -0
  50. package/src/audio/granular.ts +664 -0
  51. package/src/audio/instrument-check.ts +133 -0
  52. package/src/audio/instruments.ts +111 -0
  53. package/src/audio/keys/dsp.ts +323 -0
  54. package/src/audio/keys/engine.ts +361 -0
  55. package/src/audio/keys/piano.ts +432 -0
  56. package/src/audio/live-worker.ts +54 -0
  57. package/src/audio/live.ts +231 -12
  58. package/src/audio/loudness.ts +65 -20
  59. package/src/audio/preview.ts +2 -0
  60. package/src/audio/resonators.ts +287 -0
  61. package/src/audio/sampler.ts +163 -17
  62. package/src/audio/samples.ts +30 -7
  63. package/src/audio/strings/body.ts +250 -0
  64. package/src/audio/strings/engine.ts +369 -0
  65. package/src/audio/strings/loop.ts +119 -0
  66. package/src/audio/strings/measure.test-helpers.ts +198 -0
  67. package/src/audio/strings/pluck.ts +354 -0
  68. package/src/audio/warp.ts +61 -0
  69. package/src/audio/wav.ts +78 -6
  70. package/src/commands/expression.ts +9 -2
  71. package/src/commands/fit.ts +135 -0
  72. package/src/commands/fx.ts +31 -0
  73. package/src/commands/granular.ts +427 -0
  74. package/src/commands/help.ts +74 -3
  75. package/src/commands/keys.ts +353 -0
  76. package/src/commands/modal.ts +247 -0
  77. package/src/commands/rig.ts +260 -0
  78. package/src/commands/sample.ts +3 -0
  79. package/src/commands/string.ts +175 -0
  80. package/src/commands/time.ts +4 -1
  81. package/src/main.ts +210 -8
  82. package/src/project/check.ts +13 -0
  83. package/src/render.ts +33 -4
  84. package/src/tui/audition.ts +2 -2
  85. package/src/tui/granular-menu.ts +278 -0
  86. package/src/tui/menu.ts +332 -9
  87. package/src/tui/modal-menu.ts +118 -0
  88. package/src/tui/play-session.ts +45 -2
package/guides/effects.md CHANGED
@@ -11,6 +11,7 @@ chorus, delay, reverb, and more (phaser, bitcrush, vowel, leslie).
11
11
  ## Ask
12
12
 
13
13
  - "add a dub delay to the snare" · "more room on the keys"
14
+ - "give the guitar a crunchy amp"
14
15
 
15
16
  ## Type it yourself
16
17
 
@@ -19,9 +20,10 @@ chorus, delay, reverb, and more (phaser, bitcrush, vowel, leslie).
19
20
  - `fx distort drive 4 tone 5000` · `fx reverb ir hall`
20
21
  - `fx chorus on` · `fx chorus off` · `fx chorus reset`
21
22
  - `fx compressor preset <name>` load a preset
23
+ - `rig crunch` guitar rig · `head lead` · `cab 4x12` · `stomp fuzz`
22
24
  - `filter 1200 0.6` · `delay 0.375 0.3` · `reverb 0.4 0.8` · `filter off`
23
25
 
24
26
  ## Menu
25
27
 
26
- - Ctrl-K › Effects: core effects first, then more effects, advanced
28
+ - Ctrl-K › Effects: core effects, Guitar rig, more effects, advanced
27
29
  - `fx filter` alone opens the filter's faders, stacked
package/guides/sounds.md CHANGED
@@ -16,10 +16,14 @@ order: 1
16
16
  - `synth preset pluck` · pad lead bass acid keys bell organ strings
17
17
  - `synth lpf 800 lpenv 3` any parameter · `synth adsr 0.01 0.2 0.5 0.3`
18
18
  - `synth lpf off` · `synth reset` · `synth` lists what is set
19
+ - `piano felt` · grand ballad upright lofi · `keys hardness 0.3` · `keys`
19
20
  - `wt list` tables · `wt pwm` load one · `wt 0.5` position
21
+ - `string sitar` · nylon koto harp … · `string buzz 0.8` · `string presets`
20
22
  - `/pack list` · `/pack use <pack>/<sound> as kick` · `/pack info vcsl`
21
23
  - `/sample samples/kick.wav as kick` add a sampler voice
24
+ - `/bpm 174` the sample's tempo (a bare `bpm 174` is the song's) · `fitmode beats` (drums) or `tones` (pads) · `len 8`
22
25
 
23
26
  ## Menu
24
27
 
25
28
  - Ctrl-K › Sound: instrument, preset, envelope, filter, browse sounds
29
+ - Ctrl-K › Sound › browse sounds › Strings: the plucked string presets
package/guides/tempo.md CHANGED
@@ -20,6 +20,7 @@ Positions are beats from 0 or `bar <n>`.
20
20
  - `fermata at 31 2` hold beat 31 for 2 extra beats
21
21
  - `meter 7/8 at bar 5` · `tempo map` show it · `tempo clear`
22
22
  - `track rate 3/2` · `track cycle 3` · `track phasing 3` · `track time off`
23
+ - `/bpm 174` on a sampler voice follows the map · `fitmode beats|tones` keeps pitch
23
24
 
24
25
  ## Menu
25
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hraness/dawg",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,3 +1,4 @@
1
+ import { GRANULAR_PARAMS } from "../../core/granular.ts";
1
2
  import {
2
3
  applyScoreOperation,
3
4
  type ScoreOperation,
@@ -201,10 +202,16 @@ export const AGENT_SYSTEM_PROMPT = [
201
202
  "Song structure: list_sections shows named sections (bars count from 1); edit_section marks, adds, duplicates, moves, renames, deletes, mutes, varies (transpose/gain) and loops them; set_form sets the play order with repeats (intro verse chorus*2 outro); add_transition generates builds (riser, snare roll, filter sweep, uplifter), drops (cut plus impact) and drum fills at section boundaries.",
202
203
  CHORD_PROCESS,
203
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
+ "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
+ "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). A track already stored as instrument piano keeps the legacy tone until you set it again.",
207
+ "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.",
204
208
  "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
+ "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.",
205
210
  "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
+ "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.",
206
212
  "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.",
207
213
  "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.`,
208
215
  "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.",
209
216
  "If a call is rejected, read the diagnostic and either fix the arguments or stop.",
210
217
  WORKSPACE_PROMPT,
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Agent tool for the granular instrument: `set_granular` edits a track's
3
+ * `Track.granular` through the same path as the `grain` prompt command, so
4
+ * the command, the ctrl-k menu, the tool and the SDK store the same object.
5
+ */
6
+ import {
7
+ GRANULAR_PARAMS,
8
+ GRANULAR_PRESET_NAMES,
9
+ isGranularPreset,
10
+ synthSourceNames,
11
+ } from "../../core/granular.ts";
12
+ import { SCORE_LIMITS } from "../../core/score.ts";
13
+ import {
14
+ applyGranularCommand,
15
+ type GranularCommand,
16
+ } from "../commands/granular.ts";
17
+ import type { AgentTool, ToolContext } from "./tools.ts";
18
+
19
+ class GranularToolError extends Error {
20
+ constructor(message: string) {
21
+ super(message);
22
+ this.name = "ToolArgumentError";
23
+ }
24
+ }
25
+
26
+ function targetTrack(args: Record<string, unknown>, context: ToolContext) {
27
+ const trackId = args.trackId ?? context.focusedTrackId;
28
+ if (typeof trackId !== "string" || trackId.length > SCORE_LIMITS.maxIdLength)
29
+ throw new GranularToolError("trackId must be a short string");
30
+ if (!context.score.tracks.some((track) => track.id === trackId))
31
+ throw new GranularToolError(
32
+ `unknown track ${trackId}; create it with create_track first`,
33
+ );
34
+ return trackId;
35
+ }
36
+
37
+ /** One tool call becomes the `grain` commands it stands for, in order. */
38
+ export function granularToolCommands(
39
+ args: Record<string, unknown>,
40
+ ): GranularCommand[] {
41
+ const commands: GranularCommand[] = [];
42
+ if (args.off === true) return [{ type: "grain-off" }];
43
+ const voice =
44
+ typeof args.voice === "string" && args.voice.length > 0
45
+ ? args.voice.toLowerCase()
46
+ : undefined;
47
+ if (args.preset !== undefined) {
48
+ if (typeof args.preset !== "string" || !isGranularPreset(args.preset))
49
+ throw new GranularToolError(
50
+ `granular presets: ${GRANULAR_PRESET_NAMES.join(", ")}`,
51
+ );
52
+ commands.push({ type: "grain-preset", preset: args.preset, voice });
53
+ } else if (args.reset === true) commands.push({ type: "grain-reset" });
54
+ else commands.push({ type: "grain-on", voice });
55
+ if (args.src !== undefined) {
56
+ if (typeof args.src !== "string")
57
+ throw new GranularToolError("src must be synth:<name>[@note]");
58
+ const text = args.src.startsWith("synth:") ? args.src : `synth:${args.src}`;
59
+ commands.push({ type: "grain-src", synth: text.toLowerCase() });
60
+ }
61
+ if (args.params !== undefined) {
62
+ if (
63
+ typeof args.params !== "object" ||
64
+ args.params === null ||
65
+ Array.isArray(args.params)
66
+ )
67
+ throw new GranularToolError("params must be an object");
68
+ const values: Record<string, number | string | boolean | null> = {};
69
+ for (const [name, value] of Object.entries(args.params)) {
70
+ if (!Object.prototype.hasOwnProperty.call(GRANULAR_PARAMS, name))
71
+ throw new GranularToolError(
72
+ `granular has no parameter ${name}; it takes ${Object.keys(GRANULAR_PARAMS).join(", ")}`,
73
+ );
74
+ if (
75
+ value !== null &&
76
+ typeof value !== "number" &&
77
+ typeof value !== "string" &&
78
+ typeof value !== "boolean"
79
+ )
80
+ throw new GranularToolError(`granular ${name} must be a value`);
81
+ values[name] = value;
82
+ }
83
+ commands.push({ type: "grain-set", values });
84
+ }
85
+ return commands;
86
+ }
87
+
88
+ export const GRANULAR_TOOLS: readonly AgentTool[] = Object.freeze([
89
+ {
90
+ name: "set_granular",
91
+ description: `Make a track a granular instrument (offline, nothing to download) or shape it. preset: ${GRANULAR_PRESET_NAMES.join(" ")}. src: a built-in synth source synth:<${synthSourceNames().slice(0, 8).join("|")}|…>[@note]; on a sampler track the first (or named voice) sample is the source. params (null unsets): grain s, overlap, scan (head speed, 0 held), pos, begin, end, spray, jitter, pitch st, detune, shimmer, shimint, spread, window (hann tukey gauss tri perc rperc), reverse 0..1, freeze, repeat, hold, drift, drate, attack, release, veltone, gain, seed, root. reset keeps preset and source; off returns to the previous voice.`,
92
+ parameters: {
93
+ type: "object",
94
+ properties: {
95
+ trackId: { type: "string" },
96
+ preset: { type: "string", enum: [...GRANULAR_PRESET_NAMES] },
97
+ src: { type: "string" },
98
+ voice: { type: "string" },
99
+ reset: { type: "boolean" },
100
+ off: { type: "boolean" },
101
+ params: {
102
+ type: "object",
103
+ additionalProperties: {
104
+ type: ["number", "string", "boolean", "null"],
105
+ },
106
+ },
107
+ },
108
+ additionalProperties: false,
109
+ },
110
+ plan(args, context) {
111
+ const trackId = targetTrack(args, context);
112
+ let score = context.score;
113
+ let message = "";
114
+ for (const command of granularToolCommands(args)) {
115
+ const result = applyGranularCommand(score, trackId, command);
116
+ if (!result.ok) throw new GranularToolError(result.message);
117
+ if (result.next) score = result.next;
118
+ message = result.message;
119
+ }
120
+ const next = score.tracks.find((t) => t.id === trackId)!;
121
+ return {
122
+ kind: "score",
123
+ operations: [
124
+ {
125
+ type: "updateTrack",
126
+ trackId,
127
+ patch: {
128
+ instrument: next.instrument,
129
+ granular: next.granular ?? null,
130
+ },
131
+ },
132
+ ],
133
+ trackId,
134
+ summary: `${trackId} ${message}`,
135
+ };
136
+ },
137
+ },
138
+ ]);
@@ -486,17 +486,17 @@ export async function loadLiveCatalog(
486
486
  * and `chatTools()` serialized, plus `compositionBrief` of a 3-track, 48-note
487
487
  * loop; see models.test.ts, which keeps these in step with the code):
488
488
  *
489
- * - system prompt ≈ 4.3 KB, tool schemas ≈ 38 KB, brief ≈ 1.6 KB typical
490
- * (capped at 12 KiB), user request ≈ 0.1 KB → ≈ 44 KB ≈ 11,000 tokens
489
+ * - system prompt ≈ 7 KB, tool schemas ≈ 51 KB, brief ≈ 1.6 KB typical
490
+ * (capped at 12 KiB), user request ≈ 0.1 KB → ≈ 59 KB ≈ 14,800 tokens
491
491
  * per request at ~4 bytes per token;
492
492
  * - about 2 requests per prompt (tool calls, then a short reply), the second
493
493
  * also carrying the first step's tool calls and results (≈ 0.6 KB);
494
494
  * - output ≈ 450 tokens of tool-call arguments and ≈ 150 of reply.
495
495
  *
496
- * So ≈ 22,100 input and ≈ 600 output tokens per prompt.
496
+ * So ≈ 29,800 input and ≈ 600 output tokens per prompt.
497
497
  */
498
498
  export const TYPICAL_PROMPT = Object.freeze({
499
- inputTokens: 22_100,
499
+ inputTokens: 29_800,
500
500
  outputTokens: 600,
501
501
  });
502
502
 
package/src/agent/ops.ts CHANGED
@@ -1,4 +1,9 @@
1
+ import { resolveInstrumentWord } from "../../core/instruments.ts";
1
2
  import { pitchToMidi } from "../../core/pitch.ts";
3
+ import {
4
+ instrumentPatchForWord,
5
+ type TrackModal,
6
+ } from "../../core/resonators.ts";
2
7
  import { SCORE_LIMITS } from "../../core/score.ts";
3
8
 
4
9
  export { pitchToMidi };
@@ -34,11 +39,16 @@ export type AgentOperation =
34
39
  type: "track";
35
40
  patch: {
36
41
  instrument?: string;
42
+ string?: { preset: string };
43
+ /** 0.6: a modal preset word (`instrument vibes`) sets this too. */
44
+ modal?: TrackModal;
37
45
  muted?: boolean;
38
46
  volume?: number;
39
47
  pan?: number;
40
48
  name?: string;
41
49
  };
50
+ /** The instrument word as typed (a guitar alias also loads its rig). */
51
+ word?: string;
42
52
  }
43
53
  | {
44
54
  type: "automation";
@@ -74,8 +84,14 @@ export function parsePrompt(prompt: string): AgentOperation | undefined {
74
84
  const instrument = text.match(
75
85
  /^(?:instrument|sound|voice)\s+([a-z0-9._ -]{1,64})$/,
76
86
  );
77
- if (instrument)
78
- return { type: "track", patch: { instrument: instrument[1]!.trim() } };
87
+ if (instrument) {
88
+ const word = instrument[1]!.trim();
89
+ return {
90
+ type: "track",
91
+ patch: instrumentPatch(word),
92
+ ...(resolveInstrumentWord(word)?.fx ? { word } : {}),
93
+ };
94
+ }
79
95
  if (/^(?:mute|silence)\b/.test(text))
80
96
  return { type: "track", patch: { muted: true } };
81
97
  if (/^(?:unmute|unsilence)\b/.test(text))
@@ -180,3 +196,24 @@ export function parsePrompt(prompt: string): AgentOperation | undefined {
180
196
  ...(offset !== 0 ? { cents: offset } : {}),
181
197
  };
182
198
  }
199
+
200
+ /**
201
+ * The track patch an instrument word means: a 0.6 resolver word for the
202
+ * string engine also writes its preset (`instrument nylon` plays the nylon
203
+ * string); every other word stores the instrument as before.
204
+ */
205
+ export function instrumentPatch(word: string): {
206
+ instrument: string;
207
+ string?: { preset: string };
208
+ modal?: TrackModal;
209
+ } {
210
+ const meaning = resolveInstrumentWord(word);
211
+ if (!meaning) return { instrument: word };
212
+ if (meaning.field === "modal") return instrumentPatchForWord(word);
213
+ if (meaning.field === "string" && meaning.preset)
214
+ return {
215
+ instrument: meaning.instrument,
216
+ string: { preset: meaning.preset },
217
+ };
218
+ return { instrument: meaning.instrument };
219
+ }
@@ -36,10 +36,15 @@ export const PREVIEWABLE_TOOLS: readonly string[] = Object.freeze([
36
36
  "set_mix",
37
37
  "set_automation",
38
38
  "set_fx",
39
+ "set_rig",
39
40
  "set_synth",
41
+ "set_string",
42
+ "set_modal",
40
43
  "set_sample",
44
+ "fit_sample",
41
45
  "set_effects",
42
46
  "set_wavetable",
47
+ "set_granular",
43
48
  "set_drum_kit",
44
49
  "use_sound",
45
50
  ]);
@@ -154,7 +159,7 @@ export function previewSoundTool(
154
159
  return {
155
160
  name: "preview_sound",
156
161
  description:
157
- "Hear a track, or a candidate sound change, without committing it. changes: sound tool calls to try ({tool, args} for set_fx, set_synth, set_wavetable, set_instrument, set_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.",
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.",
158
163
  parameters: {
159
164
  type: "object",
160
165
  properties: {