Skip to main content

sva_cli/
help.rs

1// Concern: the `--help` page, each flag's default printed from its own constant | Non-concern: parsing those flags (args/), the JSON a subcommand answers (output.rs) | IO: () -> the page
2
3use sva_core::DEFAULT_STORE_BYTES;
4use sva_core::solve;
5use sva_engine::resample;
6use sva_engine::{
7    DEFAULT_BEATING_WINDOW_SECS, DEFAULT_BUFFER_SECS, DEFAULT_CELL_SECS, DEFAULT_FLOOR_DB,
8    DEFAULT_FRAME_SECS, DEFAULT_LEDGER_DEPTH, DEFAULT_LOUDNESS_WINDOW_SECS, DEFAULT_MAX_PAIRS,
9    DEFAULT_MAX_PEAKS, DEFAULT_MODULATION_WINDOW_SECS, DEFAULT_NOVELTY_WINDOW_SECS,
10    DEFAULT_OVERSAMPLE, DEFAULT_PARTIALS_FLOOR_DB, DEFAULT_PARTIALS_FRAME_SECS,
11    DEFAULT_PARTIALS_HOP_SECS, DEFAULT_PARTIALS_TRACKS, DEFAULT_PARTIALS_WINDOW_SECS,
12    DEFAULT_PATTERN_FRAME_SECS, DEFAULT_PERIODICITY_WINDOW_SECS, DEFAULT_SAMPLE_RATE,
13    DEFAULT_SMOOTH_SECS, DEFAULT_SPL_DB, PSYCHOACOUSTIC_V1, READINGS, Reads,
14    self_similarity::MAX_CELLS, spectrum,
15};
16
17/// Read off the constants the parser itself defaults to, so a printed default cannot drift
18/// from the one a render actually uses.
19pub fn help_text() -> String {
20    let bits = PSYCHOACOUSTIC_V1.precision_bits;
21    let store_gb = DEFAULT_STORE_BYTES >> 30;
22    let body_chars = crate::lint::BODY_CHARS;
23    let representations = representations();
24    let (min_frame, max_frame) = (spectrum::MIN_FRAME, spectrum::MAX_FRAME);
25    let rules = solve_rules();
26    let floor = solve::FLOOR_DB;
27    let (max_renders, step_tol, cost_tol) = (
28        solve::DEFAULT_MAX_RENDERS,
29        solve::DEFAULT_STEP_TOL,
30        solve::DEFAULT_COST_TOL,
31    );
32    let (passband, stopband) = (resample::PASSBAND, resample::STOPBAND);
33    let bound = format!("{} dB", 20.0 * libm::log10(resample::BOUND));
34    let formats = crate::recording::READS
35        .map(|e| format!("`.{e}`"))
36        .join(", ");
37    let unreadable = crate::recording::UNREADABLE;
38    format!(
39        r#"USAGE:
40  sva-cli (render | analyze | solve | lint | trace | builtins | outline | new) [arguments]
41
42DESCRIPTION:
43  A composition is a directory of node files, each one closed-form expression in
44  `t` or `f` on one line, or a score placing notes in time. sva-cli reads that
45  composition and prints what it is and what it sounds like, as JSON on stdout.
46  `render`, `solve`, `lint` and `trace` read the current directory; `new` writes
47  beside it and `analyze` reads a file. `solve` fits variables to a reference.
48
49SCORES:
50  A score is a `.jsonl` file: JSON Lines, one object per line, sorted by `at`;
51  `@score/drop` reads `score/drop.jsonl`, and a ref never writes `.jsonl`. A
52  file with no extension opening with `{{` refuses with the rename. Line 1 is
53  its annotation, the doc comment's four fields, which `lint` holds it to:
54    {{"annotation": "Models: ... | Neglects: ... | IO: (t) -> amplitude | Tags: ..."}}
55  Every other line is one kind, told by its keys, and may add `"comment"`:
56    event    "at", "node", "deg", "len" and the node's parameters: plays it
57    mark     "at", "mark": a point `mark(name)` reads in this file's times
58    comment  "at", "comment": a note to the reader at a time
59    move     "at", "lane", "to", and "node", "len", "shape" where written
60  A score lowers to equations, the sum of its events in written order, each
61  its node shifted to its `at`; marks, comments and moves sound nothing.
62    at     a time string, "0.25b" or "0.5s", or arithmetic over times, "2b +
63           1b/3" for a triplet; a bare number has no unit and refuses. A file
64           counts every time in bars or every one in seconds. "mark(verse) -
65           2b" is a position, "mark(drop) - mark(build)" a duration.
66    node   an `@ref` from the score's own directory: a sound, an equation or
67           another score. On a line placing a score `len` crops it there and
68           nothing else binds; a `node` list mixing scores and sounds refuses
69           `len`.
70    deg    a whole scale degree counted from 1, the tonic: sugar for "f0":
71           "deg(n)". In a seven-row scale 8 is the tonic an octave up, 0 the
72           seventh below and -6 the tonic an octave down. A chromatic note
73           binds "f0": "deg(7)*1st"; a line writing both refuses.
74    len    a time as `at` takes, bound as the node's `len`.
75    other  binds the node's parameter of that name: a number, or a constant in
76           a string, "0.03s", "-6db", "900hz", "A1", "0.8*0.9", "deg(5)/2".
77  Any field but `comment` may be a list. A line listing N items is N events,
78  moves or marks, item i of every list going to the i-th and a single value to
79  all; lists of different lengths refuse. A chord is a `deg` list on one `at`:
80    {{"at": "0b", "node": "@../instruments/keys", "deg": [1, 3, 5],
81      "vel": [0.8, 0.5, 0.6]}}
82  `deg(n)` is the tonic in `variables/key` times degree n's ratio in the scale
83  `variables/scale` names, read where the line sits, in a score or in any
84  equation. A scale is one the language names, as `minor` or `dorian` (`sva-cli
85  builtins` lists each with its cents), or a file of one row per degree in cents
86  above the tonic, ascending, the last the period, named by a ref: a file
87  `scales/custom` holding 200 400 500 700 900 1100 1200 is the major scale. A
88  value reading `t` or a `@ref`, a field the node does not read, a parameter it
89  reads that nothing binds, and a `deg` with no scale refuse before any audio,
90  each naming the score's file, line and column.
91
92  A lane is one value moving through the song: a node's parameter, a move
93  naming "node" and "lane": "cutoff", or a variable under `variables/`, a move
94  naming "lane" alone: "energy", "key", "scale", "bpm" (never "meter"). A move
95  with no `len` sets the lane at `at`; with "len" and "shape", `lin` or `exp`,
96  it ramps there from the value it holds:
97    {{"at": "24b", "node": "@../instruments/saw", "lane": "cutoff",
98      "to": "9khz", "len": "8b", "shape": "exp"}}
99    {{"at": "72b", "lane": "key", "to": "key*2st", "comment": "a tone up"}}
100    {{"at": "92b", "lane": "bpm", "to": "bpm*8/9", "len": "4b", "shape": "lin"}}
101  A `to` naming the lane means its home value, the node's default or the
102  variable's file, so it follows `--set`. A lane reaches every instance of its
103  node, from a score or an equation, inside a wrapper too, unless the line or
104  an invoking equation passes that parameter, which wins. Where it holds one
105  value over an instance's span it binds that number; where it moves there the
106  instance reads it at its song time, and where the engine refuses a moving
107  value, such as `sat`'s `drive`, the move refuses as `bad-lane`: pass a
108  constant, move the lane between notes, or close the sound's tail with a crop.
109  A score's times ride the tempo curve the `bpm` lane draws; `b` in an equation
110  stays a bar of `variables/bpm`. A song places its sections at marks:
111    {{"at": "0b", "mark": "intro"}}
112    {{"at": "mark(intro)", "node": "@intro"}}
113    {{"at": "mark(intro) + 8b", "mark": "verse"}}
114    {{"at": "mark(verse)", "node": "@verse"}}
115  `concat` and `repeat` place parts on the curve too: `repeat(@score/riff-1b, 4)`.
116  An equation reading a score at `t - d`, an echo or a send, hears the song as
117  it was d earlier: its lines keep their song time, and a move however many
118  reads reach it is one move.
119  A control score is one no sounding score places, read by an equation as a
120  signal: `@score/song(t) * (1 - @score/pump(t))` ducks the song under it.
121  `sva-cli outline @score/song` prints a song's marks, tempo, lanes, events and
122  size. Lint and render warn of a line passing a parameter whose lane ramps
123  under it (`line-overrides-lane`), and of a lane no instance reads
124  (`lane-reaches-nothing`).
125
126RESERVED VARIABLES:
127  Four nodes under `variables/` are reserved: a ref reads one like any node,
128  and a bar or a degree reads one by name:
129    variables/bpm     a number, the beats per minute: 120
130    variables/meter   beats per bar over the note: 4/4
131    variables/key     the tonic `deg` counts from, a note or hertz: C3
132    variables/scale   the scale `deg` counts in, a built-in's name: major, or a
133                      ref to a scale file: @../scales/custom
134  A `b` literal or a file's `-2b` span reads bpm and meter; a `deg(n)` reads key
135  and scale. Before anything renders, each read is replaced by its value, by one
136  pass for all four: a bar becomes `n*(beats*60/bpm)` seconds and `deg(n)`
137  becomes `@variables/key` times degree n's ratio, the tonic still a ref. A
138  read with no value to replace it refuses as `bad-tempo` or `bad-scale`, as
139  does a variable holding no such value, such as bpm without meter, or a
140  scale name no built-in has, which refuses listing every built-in. A stream's
141  edit to a variable, or to the scale file it names, replaces every read of it
142  again. A node elsewhere named `bpm` is an ordinary node. Any other file under
143  `variables/`, as `variables/energy` holding 0.5, is a variable of the
144  composition's own: a ref reads it, `--set` sets it, and a score's moves move
145  it, as they move the reserved ones but `meter` (SCORES).
146
147RENDER:
148  sva-cli render '<expression>' --representation <list> [--until '<condition>']
149                 [--bits <n>] [--rate <hz>] [--threads <n>] [--cache <path|none>]
150                 [--set <name>=<text>]... [--confirm]
151
152  Renders one expression, in the grammar a node file's body uses, and prints one
153  reading per representation under `data.representations`. `@path` reads a node
154  from the current directory and `@/abs/path` one anywhere; there is no default
155  target. Every reading states what it was read with: `representation`, its
156  name, and `args`, every argument it takes as it used it, a plain number as a
157  number and the rest as `--representation` spells them (`pitch` alone prints
158  `"peaks": 16, "frame": "50ms"`), a default written out and a `spectrum`
159  frame left to fit the range as the frame it fitted. Null is an argument that
160  did not apply: a frame read exactly off the math, `modulation`'s unwritten
161  `bpm`, `samples`' `buffer` with no device. Its `args` written back as
162  `name(key=value, ...)`, nulls left out, ask the same reading. It states its
163  `source` (exact or measured), the conformance profile it ran under, and its
164  rate; `analyze` prints the same.
165
166  The target's own ref may read an interval: `@piano4([0, 2b], f0=C4,
167  vel=0.5, release=1s)`, where `release` is the instrument's own parameter. Its ends are the language's literals (`s`, `ms`, `b`,
168  `sp`, or a bare `0`); `[a, inf)` and `[a,)` leave the end open; the other
169  arguments are the node's own named ones. The start trims the output alone:
170  history before it is still computed, so a loop or a filter carries the state
171  it had there. With no interval the render starts at 0, earlier only where a
172  crop reaches before it, and its end is open. Inside an expression a window is
173  a `crop`. A target that samples its one ref read, `sample(@piano4([0, 2b],
174  f0=C4))`, reads that ref's interval and binds and the same node values.
175
176  `sample("<path>")` is a recording: a file relative to the directory sva-cli
177  runs in, or absolute, read as `analyze` reads one (ANALYZE); keep it outside
178  the composition, where it would be read as a node. It is the file's samples
179  at the render's rate, one component per channel as `join` makes them, silent
180  outside the file's span. At its own rate every sample passes unchanged. At
181  another a cos^16-windowed sinc reads it: a tone up to {passband} of the lower
182  rate reads within {bound} of itself, and one from {stopband} of it at most
183  {bound} of its level, a bound proven for the kernel that the label states
184  under `resampled` beside the rate it came from. The path is the language's
185  one string, in double quotes and only as `sample`'s one argument; one naming
186  no file refuses as `not_found`, and one `analyze` cannot read as
187  `{unreadable}`. A store keys a recording by its samples and rate, never its
188  path.
189
190  Every node is computed over its support met with what reads it, and nowhere
191  else: outside its support a node is exactly zero. A closed interval renders
192  exactly its length, nothing cut. An open one ends where the root's support
193  does: a crop's end, the exact underflow of an `exp`, a clamp holding a factor
194  at zero; or earlier, where a proven upper bound on the root's magnitude stays
195  under the profile's silence threshold (-120 dBFS) from a sample on. A root
196  with no such bound (a held sine, a loop) is never cut. The label's `pruned`
197  states the threshold as `db` and the root's cut, `from` the sample the render
198  is treated as silent from. A stream's term leaves `@notes` where its support
199  ends. An open interval over a root whose support never ends and is never cut
200  (a held sine, a physical solver) refuses as `render.no_end`. A short-time
201  transform reads its input whole, and refuses one with no end as
202  `engine.unbounded_extent`. A form in `f` with no dual in `t` has no samples
203  and refuses as its dual does, `cast.left_algebra`.
204
205  A render is a stream pulled to its end: a closed form, a short-time
206  transform and what either reads are computed over their whole extent first,
207  and every other node block by block. `--until '<condition>'` stops the render
208  at the first sample the condition holds at, or at the interval's end,
209  whichever is first. It is checked as each block is pulled, so no node driven
210  block by block runs past the block it holds in. A condition compares (`<`,
211  `<=`, `>`, `>=`) `t`, `envelope(t)` (the RMS over every channel of the
212  `envelope` representation's frame holding `t`, framed by its own `frame`
213  where one is asked) and literals, joined by `and`/`or`.
214
215  `--representation <list>` takes a comma list of readings and may repeat. Each
216  is a call in the language's own syntax, its options its named arguments:
217  `spectrum(peaks=8, frame=50ms)`, `ledger(depth=3, brief=1)`. A `frame`,
218  `window`, `cell` or `smooth` is a time as a score writes one, in seconds,
219  `50ms`, or in bars, `1b/8` (`1/8b` is one over eight bars), a bar lasting
220  what the render's tempo says; where none is known, as in `analyze`, bars
221  refuse as `tempo.invalid_bpm_meter`. An entry
222  followed by `=<path>` writes a file instead, listed under `written`:
223  `samples=<path>.wav` writes audio, encoded as `--bits` says; any other path
224  takes the reading's JSON, uncapped. A path that already holds a file refuses
225  unless `--confirm` is written. `samples=alsa:<device>` plays the samples on
226  that ALSA device (`alsa:default`, `alsa:hw:1`, `alsa:plughw:1,0`) as each
227  block is pulled, in real time, through `aplay` from alsa-utils; Linux only,
228  anywhere else it refuses as `destination.unsupported_platform`. It sends
229  32-bit floats, or integer PCM where `--bits` is 16 or under. A device that
230  cannot take the format refuses with aplay's reason, and one aplay says plays
231  another rate than the render's stops there as `destination.rate_inexact`.
232  `samples(buffer=<t>)` is how much the device holds ahead of what it plays.
233  Once it has played out, `data.played` states the device, its seconds, rate,
234  channels, format, the buffer asked and the one granted, and every underrun
235  aplay reported, with what it cannot see.
236  `lines`, `atoms`, `derivative`, `bindings`, `arguments`, and a closed form's
237  `spectrum`, `envelope` and `pitch`, read the expression and ignore the range.
238
239  A closed form's `spectrum` and `pitch` are its exact lines, so a term under a
240  crop or an envelope, which has a width and is no line, refuses them; so do
241  `spectrum(frame=)` and `envelope(frame=)`, as a closed form has no frames. To
242  measure any of them frame by frame, a reader samples the target's ref,
243  `sample(@x([0, 1s]))`, and an author writes `sample(...)` inside the node.
244  A closed form with no exact envelope, such as a crop of a decay, refuses
245  `envelope` with the reason rather than measuring it unasked; `sample(...)`
246  asks for the measured one. A knot table, `pwl(t, 0s, 0, 5ms, 1, 0.4s, 0)`,
247  is the straight lines through its knots, and `pwl_integral` with the same
248  knots that curve's integral from the first, so `cos(2*pi*pwl_integral(t, 0s,
249  440hz, 1s, 880hz))` glides an octave with an exact phase; either is exact at
250  every instant and holds any number of knots at no cost per sample, but has no
251  atoms, so these exact readings refuse it as `cast.left_algebra` and read it
252  measured off `sample(...)`. `sva-cli builtins` states both.
253  A measured `spectrum` is one spectrum: whole
254  windows across the range, averaged, the first at its first sample and the last
255  ending at its last, evenly at most half a window apart; a window is `frame`
256  rounded up to a power of two samples, at least {min_frame}, or without `frame`
257  the range's length so rounded, at most {max_frame}. `pitch` is framed, one entry
258  per `frame`; for the spectrum at one instant, read a range one frame long.
259  A measured `envelope` is framed too, and reads every channel: a frame's
260  `rms` is the root mean square of all its channels' samples together, its
261  `peak` the largest magnitude in any channel. `score` and `partials` read
262  every channel's samples averaged, one mono signal. Every
263  other measured reading but `stereo` reads every channel in power, as
264  `envelope` reads a level: a spectrum's power per bin (`spectrum`, `pitch`,
265  the partials `dissonance`, `voice_leading`, `tonal_distance`,
266  `inharmonicity`, `self_similarity`, `loudness_pattern`, `sharpness` and
267  `beating` pick), a formant frame's
268  autocorrelation and each auditory band's energy (`bands`, `periodicity`,
269  `modulation`, `self_similarity`) are each channel's averaged, so a channel
270  copied to two reads as itself; `crest` sets any channel's peak against the
271  RMS of all of them together. A closed form's exact readings read every
272  channel in power too: a partial's squared amplitude (`pitch`, `dissonance`,
273  `voice_leading`, `tonal_distance`, `inharmonicity`) is summed over the
274  channels at its frequency and divided by their count, and a spectrum of more
275  than one channel lists each frequency once, its amplitude the root of that
276  mean power, real, as channels share no phase.
277
278  `--bits <n>` is the precision every sample is written to, from 2 to 52: the
279  point where a series is truncated, and the encoding of a `.wav`, integer PCM
280  at n bits up to 16 and 32-bit float above. `--rate <hz>` (default
281  {DEFAULT_SAMPLE_RATE}) is the one rate a render samples at: the target is read
282  at its instants, and every node at the instants its reader asks for. A
283  closed form is exact at any instant, and so is a continuous loop, one
284  constant delay at a gain under 1 over a closed form such as
285  `x + 0.5*self(t - 17ms)`, which is its series. A filter, a discrete loop, a
286  solver and `rand` step at the rate asked for, where `1sp` is one step, so an
287  `sp` count or `self[idx(t) - 1]` means one sample at whatever rate is asked.
288  `rand` draws once per step, keyed by that step's index; a key between steps
289  reads the step nearest it, ties to even.
290
291  `--set <name>=<text>` renders as if the variable `variables/<name>` held
292  `<text>`, and writes no file: `--set key=G3` transposes every degree to G3,
293  `--set scale=dorian` changes mode (a scale file is a ref written from
294  `variables/`, as the file holds it: `--set scale=@../scales/custom`), `--set
295  bpm=140` moves every bar, `--set meter=3/4` regroups them and `--set
296  energy=0.8` sets a variable of the composition's own. It sets a lane's home
297  value: a move whose `to` names it follows. It may repeat, once per variable: a
298  variable set twice refuses as `render.set_twice`, a name with no file under
299  `variables/` that is no reserved one as `render.set_unreserved`, listing the
300  variables there, and the file's path, `variables/key=G3`, as
301  `render.set_path`, naming the short form.
302
303  `--threads <n>` (default: every core the machine reports) is the most
304  threads a render computes on; `1` computes every value in turn on one.
305  Values none of the others reads, and the blocks of a value that holds no
306  state, compute apart, to the same bits on any number of threads.
307
308  A discrete loop reads its own past only by index; `self(t - d)` in one
309  refuses as `type.discrete_self_at_time`, naming what made it discrete. A
310  read `@x(k*t - d)` steps `x` at `k` times the step, every input it reads,
311  `sp` and `idx` with it, and rounds `d` to the nearest sample of that step,
312  ties to even: shifts are rounded to the nearest sample so placements share
313  one cached value; timing is exact to half a sample, and the label's
314  `moved_s` states the most any read moved. A node is computed once
315  per step however many reads ask for it. A node that holds state read at a
316  time that moves refuses as `type.stateful_warp`, naming what
317  holds its state; `@x[idx(...)]` reads its nearest step instead, as `p[idx(...)]`
318  does a signal passed in as parameter `p`, and any `idx`, as
319  `idx(t - 5ms - 2ms*sin(2*pi*t))`, is read sample by sample. An edit to a
320  stream plays from the next sample on.
321
322  A render keeps in a store on disk its target and, under it, each node of
323  samples that holds state (a filter, a discrete loop or a solver in it) or
324  that two or more nodes read. Nothing else is kept. A closed form, `sample`
325  of one and `rand` have a value at any instant and are computed where read.
326  A node without state that one node alone reads, however many times, is
327  computed again by that reader from what the node itself reads: no other
328  reader could ask for it, and without state it restarts at any sample, where
329  a filter or a loop must run from its start. So a stage worth keeping is a
330  node of its own that ends in a filter or a loop, or one two nodes read. A
331  kept value is named by what it computes: never its file's name, directory,
332  comments, spacing, or the order its named arguments are written in, so a
333  renamed or moved copy, a constant lifted into a default, an expression split
334  into a file of its own or written back inline, and two addends swapped all
335  read one entry. The next render of anything that reads the same value reads
336  it back, bit for bit, and computes nothing under it. The store is on by
337  default, at `$XDG_CACHE_HOME/sva`, else `~/.cache/sva`; `--cache <path>`
338  moves it (a `/dev/shm` path keeps it in memory) and `--cache none` turns it
339  off. It holds at most {store_gb} GB, the least recently used values going
340  first, and a store another build of sva-cli wrote is emptied when opened. A
341  render stages what it computes beside the store as it goes, and the store
342  itself is written once, when the render ends, fails, or is stopped by SIGINT
343  or SIGTERM.
344
345  `ledger` prints one row per node under the target. A row's `share` is the
346  part of its reader's own energy that row accounts for, so one reader's refs
347  sum to 1; a ref no addend isolates, such as one factor of a product, and
348  every row under a silent reader, which has no energy to share out, print
349  `null`. A ref carrying a share reads as the addend it gave its reader, its
350  `rms`, `peak` and `clipped` too: in `2*@a`, `a`'s row is `a` doubled, and in
351  `@a - @a` it is silence. Each ref carrying a share is collapsed once on its
352  own, so a ledger costs one collapse per attributed ref beyond the render,
353  and `depth` bounds how many, counted in refs from one file to another. Every
354  row is computed whole over the range, however late a reader reads it.
355  `brief=1` keeps only the rows that clipped, `skim=1` drops the wider fields.
356
357  `arguments` renders nothing: for every instance under the target it prints
358  each builtin call's named arguments as the numbers the call was lowered
359  with, a solver's whole parameter set with `written: false` on each default
360  it filled in, and the operand each `min`/`max` `chosen` where a call folds
361  one to a number: in named arguments and a solver's, modal bank's or
362  `noise`'s positionals. One inside a filter's or cast's positional or
363  `rand`'s key or seed is not listed. `at` spans are bytes of the instance's
364  own body, as `sva-cli outline` counts them.
365
366REPRESENTATIONS:
367  Each one a `--representation` call, its arguments in parentheses, every
368  default under DEFAULTS. `exact` is read off the written math, `measured` off
369  samples.
370
371{representations}
372
373SOLVE:
374  sva-cli solve ('<cost>' | @fits/<name>) --vary <name>=<lo>..<hi>[/<step>][,log]...
375                [--max-renders <n>] [--step-tol <x>] [--cost-tol <x>]
376                [--set <name>=<text>]... [--rate <hz>] [--threads <n>]
377                [--cache <path|none>] [--write [--confirm]]
378
379  Fits variables under `variables/` so the targets a cost reads match a
380  reference, and prints the values it found. It edits no file unless `--write
381  --confirm` says so.
382
383  The cost is one expression typing to one number. Its leaves are
384  `repr.<reading>(<target>, <args>)`: `<reading>` spelled as `--representation`
385  spells it, `<target>` any render target with its interval as render writes
386  one, `<args>` the reading's own: `repr.pitch(@lead([0, 2s], f0=C4),
387  frame=50ms)`. Every leaf is measured off its target's samples, as `analyze`
388  reads a file, so a recording and a render are read alike. Two leaves of one
389  reading with one set of arguments subtract to a residual vector:
390{rules}
391  A level is floored at {floor} dB before it subtracts. A residual inside both
392  sides' stated bounds counts as zero; of these readings only `score` states
393  one, and `loudness_pattern` counts zero a cell both sides hold under its
394  threshold. Then `norm(v)` (the root of the sum of squares), `sum(v)`, `max(v)`,
395  `abs(x)` (each value's magnitude), `dot([w1, w2, ...], v)` (one literal
396  weight per value), numbers, `+`, `-`, `*` and `/` make one number. A reading
397  left unsubtracted, a vector left unreduced and a leaf of any other reading
398  refuse before anything renders. In a node, `repr` and `load_json` are names
399  the language does not know and refuse as any such name does, with a help
400  naming `solve`.
401
402  `load_json("<file>", "<jq path>")` is a leaf read off a JSON file instead of
403  a render: plain JSON, so a `.json.gz` is unpacked first (`gunzip -k`). The
404  path is relative to the directory sva-cli runs in, or absolute, as
405  `sample("<path>")`'s is. The jq path is steps only, none of jq's filters:
406  `.name`, `[3]`, and a key that is not a plain name in single quotes,
407  `['envelope(frame=10ms)']`, so it needs no escaping in the cost's double
408  quotes: `load_json("../crop.json", ".readings['periodicity(window=5.08s)']")`.
409  A number there loads as a number and a list of numbers as a vector, both
410  usable as weights and in reducers. An object loads only as the other side of
411  a subtraction from a `repr.` leaf, and is a reading as `analyze` prints its
412  `data` (the object holding `interval` and `representations`): from a bench
413  download `.readings['<call>']`, from `analyze ref.wav --representation
414  bands=ref.json` the path `.data`. The leaf's reading reads it, and refuses
415  before the first render where its kind, its arguments, its rate, its
416  profile or its interval's length differ from what the leaf renders, or
417  where a list in it was cut short (`has_more: true`; a reading written to a
418  file is whole). Its arguments are its `args`, compared one by one with the
419  leaf's, each resolved at the solve's rate, so `frame=0.05` matches
420  `frame=50ms`; a mismatch names the argument and both values, and a reading
421  with no `representation` and `args` refuses. Each file is read once per
422  solve.
423
424  `@fits/<name>` reads the cost from the file `fits/<name>` of the
425  composition, written as a node is: an annotation line `; Models: ... |
426  Neglects: ... | IO: ... | Tags: ...`, then the cost, over as many lines as
427  it needs, every `;` comment dropped. Its quotes need no shell escaping, and
428  a cost that does not parse refuses at its line and byte in that file. No
429  node lives under `fits/`, and `lint` reads nothing there. `--vary` and the
430  other flags stay on the command line:
431    ; Models: the lead's bands against the bench crop | Neglects: pitch | IO: (cutoff) -> cost | Tags: fit
432    norm(repr.bands(@lead([0, 5.083291666666667s]))
433         - load_json("../crop.json", ".readings.bands"))
434
435  `--vary <name>=<lo>..<hi>[/<step>][,log]`, once per variable, names a file
436  `variables/<name>` holding one number and the box it moves in, both bounds in
437  one unit: none, `hz`, `khz`, `s`, `ms`, `db`, `ct` or `st`. The search starts
438  at the file's value read in that unit (`0.5` is `-6.02db` under `db` bounds).
439  `/<step>` is the first step and the grid values land on, counted from `<lo>`;
440  without it the first step is a quarter of the bounds' span. `,log` moves it on
441  a log scale, where `/<step>` is the ratio a step multiplies by, a bare number
442  above 1, as `/1.25`. A name with no file, a file holding no number in that
443  unit, and a variable no target of the cost reads each refuse before the first
444  render. `--set <name>=<text>` holds a variable at `<text>`, as render takes
445  it; a name both set and varied refuses.
446
447  The search is a deterministic compass search: from its best point it renders
448  each variable one step up and one down, in the order the `--vary`s are
449  written, and moves to the best of them that improves; where none does, every
450  step halves. A point already rendered is not rendered again. One render
451  renders every target of the cost once, through render's own path, and takes
452  every reading. A target no variable reaches, such as the reference, is read
453  back from the store on every render after its first, and `trace` shows it
454  computing no samples. A solve stops `converged` once every step is under
455  `--step-tol` of its first step or the cost is at most `--cost-tol`, `budget`
456  after `--max-renders` renders, and `cancelled` on SIGINT or SIGTERM. The
457  budget is a result, warned as `solve.budget`, not an error. Each render
458  prints one JSON line on stderr:
459    {{"render": 3, "values": {{"gain": "0.4"}}, "cost": 1.25, "best": 0.98}}
460
461  `--write` writes each fitted value to its `variables/` file, its comment line
462  kept. A file it would change refuses as `conflict` unless `--confirm` is
463  written too; that refusal still carries the fit, and `would_write`, nothing
464  written. A cancelled solve writes nothing.
465
466  Its `data`:
467    {{"cost_expression": "...", "values": {{"gain": {{"value": 0.5, "text": "0.5",
468      "unit": "", "start": "0.3", "on_bound": false}}}}, "cost": {{"total": 0.09,
469      "terms": {{"items": [{{"term": "norm(...)", "value": 0.08}}], ...}}}},
470      "renders": 49, "stopped_by": "converged", "resampled": {{"items": []}},
471      "trace": {{"items": [{{"render": 1, "values": {{"gain": {{"value": 0.3,
472      "text": "0.3"}}}}, "cost": 52.1, "computed_samples": {{"@tone": 44100,
473      "sample(\"../ref.wav\")": 44101}}}}]}}}}
474  `values` is the best point, each `text` as `--set` and a file take it; `cost`
475  its total and each top-level `+` or `-` term's signed share; `trace` every
476  render in order. A value `on_bound` sat on a bound, warned as
477  `solve.on_bound`: widen it, or the model lacks what the reference holds.
478  `--write` adds `written`, each path with its `from` and `to`. Stopped by a
479  signal, the answer is the error `cancelled`, exit 130, its `data` the best so
480  far.
481
482ANALYZE:
483  sva-cli analyze <file> --representation <list> [--confirm]
484
485  Runs the same readings over a whole recording at its own rate, never
486  resampled. It reads these formats:
487    {formats}
488  `.wav` is read exactly as written; the rest decode in sva-cli itself, an
489  MP4's encoder delay and padding trimmed as its edit list states (a bare
490  `.aac` keeps them). A lossy decode may differ slightly from a browser's
491  decoding of the same file, so its readings may differ slightly from a page's
492  readings of it. Any other format, or a file that does not decode, refuses
493  as `{unreadable}`, naming the formats it reads. Only the readings a
494  buffer answers alone apply, each listed under REPRESENTATIONS as `measured` or
495  `exact off a closed form, measured off samples`; the rest need the graph
496  behind it. Each reading states its `representation` and `args` as
497  `render`'s do, a time in seconds.
498
499LINT:
500  sva-cli lint ['<expression>'] [--format <json|text>] [--body-chars <n>]
501
502  Checks the current directory without rendering a sample. With no target it
503  checks every file under its own rules, and types every entry point, which
504  types every file it reaches. With a target, in render's grammar, it checks
505  the files that target reaches, and prints the interval a render of it reads.
506  Either refuses a type error as `render` does.
507
508  A score's annotation is its doc comment, under the same codes, and its lines
509  no body, so the length checks pass it by; a file of two or more lines that is
510  neither a score nor a scale refuses as `multiline-body` before any check
511  runs. Each song lint types prints its warnings beside the findings.
512
513  Every check prints one `data.diagnostics` item. `warning` exits 0, `error`
514  exits non-zero, so branch on the verdict and never on whether the array is
515  empty. No flag downgrades an error.
516
517  error    missing-comment       no `;` comment line
518           multiline-comment     more than one
519           malformed-comment     not four ` | ` fields, `Models:` `Neglects:`
520                                 `IO: <in> -> <out>` `Tags: <tag>[, <tag>...]`
521           long-comment-block    a `;` block over 1000 characters, the line-1
522                                 doc comment's own run exempted
523           long-expression-body  a body over `--body-chars` characters, a
524                                 backstop rather than a complexity budget; a
525                                 knot table's literals, its data, count none
526           literal-sample-rate   a written rate where `sp` belongs
527           arity                 a builtin called without an argument it reads,
528                                 or with one it does not, as `rand(seed=k)`
529  warning  key-is-not-a-pitch    `variables/key` holds neither a note name nor
530                                 a number of hertz
531           parameter-has-no-default
532                                 a node reads a parameter no `name = value` line
533                                 binds, so a bare `@node` cannot play
534           support-never-ends    a node's support, cut where its bound falls
535                                 under the silence threshold, has no end for a
536                                 bare render to stop at
537
538TRACE:
539  sva-cli trace <node|expression>
540
541  Prints one node's position without rendering audio: what it reads (`down`, one
542  hop), everything that reads it (`up`, transitively to an entry point), each
543  beside the expression doing the reading, the node that made it discrete, and
544  the feedback loop it sits in, if any. A score is shown as its line count, its
545  first and last `at` and the nodes it plays, never the sum it lowers to; every
546  entry point's song is lowered first, each at song time 0. An interval the
547  target reads is ignored.
548
549BUILTINS:
550  sva-cli builtins
551
552  Prints the whole callable and syntactic vocabulary: every builtin with its
553  arity and named arguments, each argument's `meaning`, `unit`, the model
554  `part` it sets and whether it `moves` with `t` (a filter's cutoff, q and gain;
555  every other named argument is one number, refused when it names none), each
556  positional's meaning where the model states one, unit suffixes, the note-name
557  grammar, the scales `variables/scale` may name with their cents, reserved
558  identifiers, special call shapes, and what has no operator at all.
559
560OUTLINE:
561  sva-cli outline <expression>
562
563  Prints the parse tree the engine builds from one expression, each node with
564  the byte `span` it was written in: calls by `name` with positional and named
565  `args`, operators by `op`, refs by `path` with their `binds`, refs and `self`
566  by how they `read`, `time` or `index`, literals by `value` and `unit`, names
567  by `name`. A node the parser supplies itself, the `0` of a prefix minus or the
568  `t` of a bare `@ref`, has `written: false`. Reads no composition. Score text,
569  JSON opening with `{{`, prints `score` instead of `outline`: one item per item
570  a line lists, with its `kind`, `line`, `span`, `at` with marks resolved, and
571  its `node`, `deg`, `args`, `mark` or `lane`, `to`, `len` and `shape`.
572
573  sva-cli outline @score/song
574
575  Run in a composition, outlines that score as the song a render of it is:
576  `marks` in bars and seconds, the `tempo` curve's pieces, each lane with its
577  home and every move resolved with its placement chain, how many instances it
578  reaches and how many see it move, and each placed event once, however many
579  reads reach it: its song time `at` (where it reads lanes, key and scale), when
580  it first `sounds` (later by the least, over the reads reaching it, of the
581  delays of the `t - d` reads of a score above it), its `len` in song time where
582  its line writes one, node, pitch in hertz and where each parameter comes from
583  (`line`, `lane` or `default`), and the `size`: placed events and the engine's
584  instances beside its limit.
585
586NEW:
587  sva-cli new <name> [--idempotency-key <key>]
588
589  Writes a starter composition at ./<name>, and refuses if that directory
590  exists. `--idempotency-key <key>` records the key beside the composition, so a
591  retry under the same key succeeds identically while the tree still holds what
592  was written. Any other key, or an edited tree, refuses.
593
594EXAMPLES:
595  sva-cli new song1 && cd song1
596  sva-cli render '@master' --representation samples=/tmp/song1.wav
597  sva-cli render '@master([0, 8b])' --representation 'ledger(depth=2),loudness'
598  sva-cli render '@voice/note([0, inf), f0=C4, len=2s)' \
599    --until 'envelope(t) < -60db and t > 1s' --representation samples=/tmp/note.wav
600  sva-cli render 'sample(@chord/home)' --representation 'spectrum(peaks=8)' --rate 48000
601  sva-cli render 'sample(@voice/note([0, 2s], f0=C4))' --representation pitch
602  sva-cli render '@master' --set key=G3 --set bpm=140 --set scale=dorian \
603    --representation samples=/tmp/song1-g.wav
604  sva-cli render '@master' \
605    --representation 'samples(buffer=100ms)=alsa:default,loudness'
606  sva-cli solve 'norm(repr.bands(@master([0, 4s])) - repr.bands(sample("../ref.wav")))
607    + 0.5*norm(repr.loudness(@master([0, 4s])) - repr.loudness(sample("../ref.wav")))' \
608    --vary gain=0..2/0.25 --vary cutoff=200hz..8000hz/1.25,log --rate 48000
609  sva-cli solve 'sum(abs(repr.score(@score/song([0, 8s]))
610    - repr.score(sample("/tmp/take.wav"))))' \
611    --vary bpm=100..140/2 --set key=D3 --max-renders 60 --write --confirm
612  sva-cli solve 'norm(repr.pitch(@lead([0, 2s])) - load_json("../ref.json", ".data"))' \
613    --vary detune=-50ct..50ct/10ct
614  sva-cli solve @fits/lead --vary cutoff=200hz..8000hz/1.25,log --rate 48000
615  sva-cli lint
616  sva-cli lint '@master'
617  sva-cli trace score/phrase
618  sva-cli outline @score/phrase
619  sva-cli builtins
620
621OUTPUT:
622  {{"status": "success", "data": {{"target": "@master([0, 8b])", "sample_rate":
623  {DEFAULT_SAMPLE_RATE}, "bits": {bits}, "profile": "psychoacoustic-v1",
624  "interval": {{"start_secs":
625  0, "end_secs": 16}}, "label": {{...}}, "written": {{"items": [...]}},
626  "representations": {{"ledger": {{...}}}}, "diagnostics": {{"items": []}}}},
627  "meta": {{"request_id": "req_...", "timestamp": 1700000000}}}}
628
629  `interval` is null where no reading read samples. An error adds "error":
630  {{"code", "message", "details": {{"count", "codes"}}}}. Success or error,
631  every response carries every finding in full at "data": {{"diagnostics":
632  {{"items": [{{"code", "severity", "message", "location", "help"}}],
633  "pagination": {{"count", "has_more", "next_cursor"}}}}}}, empty where it found
634  none.
635
636  Every collection carries that same {{items, pagination}} pair. `count` is the
637  whole reading's, `has_more` says `items` holds less than that, and
638  `next_cursor` is the interval start, as `0.5s`, the rest is read from. A
639  reading written to a file is written whole, uncapped. This page is an
640  envelope of its own, at "data": {{"help"}}.
641
642DEFAULTS:
643  --rate <hz>          the one rate a render samples at, and `1sp`.
644                       Default {DEFAULT_SAMPLE_RATE}.
645  --bits <n>           the precision every sample is written to. Default {bits},
646                       the `psychoacoustic-v1` profile's own.
647  ledger(depth=<n>)    how deep below its target a `ledger` walks.
648                       Default {DEFAULT_LEDGER_DEPTH}.
649  (peaks=<n>)          peaks a `spectrum` keeps, notes a `pitch`, formants a
650                       `formants`, stripes and novelty peaks a `self_similarity`
651                       lists per view. Default {DEFAULT_MAX_PEAKS}; `partials`'
652                       own is below.
653  alias(oversample=<n>) the multiple `alias` re-renders at to hear what folded.
654                       Default {DEFAULT_OVERSAMPLE}.
655  (frame=<t>)          the step a framed reading advances by, in seconds.
656                       Default {DEFAULT_FRAME_SECS}, except `spectrum`, which sizes
657                       its own transform to the range unless it is set,
658                       `loudness_pattern`, `sharpness` and `beating`, which step
659                       {DEFAULT_PATTERN_FRAME_SECS}, and `partials`, below.
660  (spl=<db>)           the level in dB SPL a full-scale sine plays at, as
661                       `90db`, for `loudness_pattern`, `sharpness` and `beating`.
662                       Default {DEFAULT_SPL_DB}db.
663  (floor=<db>)         the amplitude, as `-40db`, under which a harmonic reading
664                       or `self_similarity` drops a partial, and under which
665                       `self_similarity` counts a band level as zero.
666                       Default {DEFAULT_FLOOR_DB}db; `partials`' own is below.
667  voice_leading(norm=<l1|l2>) how a voice leading's moves add up. Default l1.
668  dissonance(pairs=<n>) the most partial pairs one frame sums before it refuses.
669                       Default {DEFAULT_MAX_PAIRS}.
670  loudness(window=<t>) the length of one momentary block, in seconds, each a
671                       quarter of one after the last, the integrated loudness
672                       gated over them; both are whole samples, at least 4 in
673                       a block, and the reading states them as `momentary_secs`
674                       and `hop_secs`. A window under the default reads a sound
675                       shorter than the default, whose integrated is null there.
676                       Default {DEFAULT_LOUDNESS_WINDOW_SECS}.
677  periodicity(window=<t>) the span of onset strength one window autocorrelates,
678                       in seconds, at least twice the shortest period it reads;
679                       it reads periods up to half its span.
680                       Default {DEFAULT_PERIODICITY_WINDOW_SECS}.
681  periodicity(curve=<0|1>) `1` adds every window's height at every lag.
682                       Default 0.
683  modulation(window=<t>) the span of band envelope one window transforms, in
684                       seconds, at least twice the fastest rate's period; it
685                       reads rates down to two periods per span.
686                       Default {DEFAULT_MODULATION_WINDOW_SECS}.
687  modulation(bpm=<n>)  a tempo, as `140`, adding each rate's period in beats.
688                       Default none.
689  beating(window=<t>)  the span of frames one window fits each band's trend
690                       over, in seconds, at least 20 frames; a swing slower
691                       than the window reads as trend.
692                       Default {DEFAULT_BEATING_WINDOW_SECS}.
693  self_similarity(cell=<t>) the side of one matrix cell, in seconds, a whole
694                       number of frames holding a sample of the band
695                       envelopes; the target may span at most
696                       {MAX_CELLS} cells. Default {DEFAULT_CELL_SECS}.
697  self_similarity(smooth=<t>) the length each diagonal is averaged over
698                       before stripes are picked, in seconds.
699                       Default {DEFAULT_SMOOTH_SECS}.
700  self_similarity(window=<t>) the whole width of the novelty kernel, in
701                       seconds, at most {MAX_CELLS} cells a side.
702                       Default {DEFAULT_NOVELTY_WINDOW_SECS}.
703  self_similarity(curve=<0|1>) `1` adds every cell's chroma and band levels and
704                       each view's matrix. Default 0.
705  partials(frame=<t>)  the Hann frame `partials` finds peaks in, in seconds: two
706                       partials closer than some 2 / frame Hz are one peak.
707                       Default {DEFAULT_PARTIALS_FRAME_SECS}.
708  partials(window=<t>) the span of samples whose frames' power `partials`
709                       averages at each hop before picking peaks, so a noise's
710                       chance peaks average away; a glide crossing a frame's
711                       resolution within it smears, so read one with a window
712                       as short as the frame. Default {DEFAULT_PARTIALS_WINDOW_SECS}.
713  partials(hop=<t>)    how often `partials` reads each track, its knots' finest
714                       spacing. Default {DEFAULT_PARTIALS_HOP_SECS}.
715  partials(floor=<db>) the amplitude under which `partials` picks no peak and
716                       a track reads silent. Default {DEFAULT_PARTIALS_FLOOR_DB}db.
717  partials(peaks=<n>)  the most tracks `partials` keeps, those of most energy;
718                       each costs its own reads of its band, in time and memory.
719                       Default {DEFAULT_PARTIALS_TRACKS}.
720  bindings(node=@<path>) the instance whose bindings are read; required. An
721                       instance is named exactly as the render spells it; any
722                       other spelling refuses, listing its file's instances.
723  samples(buffer=<t>)  how much an `alsa:` device holds ahead of what it plays,
724                       in seconds. Default {DEFAULT_BUFFER_SECS}; only a device
725                       has one.
726  ledger(brief, skim)  `0` unless set `1`.
727  --format <json|text> how `lint` prints its findings: the envelope, or one
728                       terminal line each, colored where stdout is a terminal.
729                       The same objects either way. Default json.
730  --body-chars <n>     the most characters a body's expression holds before
731                       `long-expression-body` refuses it, a knot table's literals
732                       apart. Default {body_chars}.
733  --cache <path|none>  where the store lives. Default `$XDG_CACHE_HOME/sva`, else
734                       `~/.cache/sva`, holding at most {store_gb} GB; `none` keeps
735                       no store. A store that fails fails no render: it answers
736                       as without one, warned why in `diagnostics`.
737  --set <name>=<text>  a variable read as `<text>` for this render, or held for
738                       a solve. Default none: each is read off its file.
739  --max-renders <n>    the most renders a solve takes. Default {max_renders}.
740  --step-tol <x>       the fraction of each variable's first step under which a
741                       solve's search stops. Default {step_tol}.
742  --cost-tol <x>       the cost at or under which a solve stops. Default {cost_tol}.
743  --write              a solve writes its fitted values to their `variables/`
744                       files, with `--confirm`. Default off.
745  --confirm            replaces a destination that already holds a file. Without
746                       it a path already taken refuses as `conflict` and nothing
747                       is written.
748
749ENVIRONMENT:
750  SVA_LOG=debug        `render` logs, once it ends, what it asked of each value
751                       to stderr; stdout and the samples are unchanged. Any
752                       other value, or none, logs nothing. One line per second
753                       of output, then one per node and a total:
754                         sva-cache pass hit=0 miss=190 ... reused=0 cum-hit=0.0%
755                         sva-cache t=1.022s hit=1 miss=2 ... cum-hit=0.5%
756                         sva-cache node hit=1 miss=0 ... reused=3 <node>
757                         sva-cache total hit=1 miss=198 ... hit-rate=0.5% ...
758                       `pass` is what was looked up before the first block.
759                       Each value is looked up once: a `hit` was answered by
760                       the store, `prefix` found a stored run up to a switch,
761                       `miss` computed it, `new` held it in memory, where the
762                       store takes what it keeps from. Each read of a value
763                       past its first is `reused`.
764
765EXIT CODES:
766  0  success (error.code absent)
767  1  internal_error (a destination could not be written)
768  3  validation_error (bad arguments, a composition that failed to parse, or a
769     render the engine refused)
770  4  conflict (a name `new` would overwrite, a destination already holding a
771     file, or a variable file `solve --write` would change, without `--confirm`)
772  24 not_found (a `.wav` file, a `sample("<path>")` naming no file, or a node
773     this composition does not define)
774  130 cancelled (a solve stopped by SIGINT or SIGTERM, `data` the best so far)
775
776VERB ALIASES:
777  validate = lint, list = builtins, create = new, show = trace, from the `cli`
778  standard's own verb list. `render` and `analyze` take a reading, which that
779  list has no word for, so they keep their own names.
780
781SEE ALSO:
782  sva-cli --version    Show version information"#
783    )
784}
785
786/// One line per reading a cost subtracts, off the table the subtraction itself keeps.
787fn solve_rules() -> String {
788    let mut out = Vec::new();
789    for (name, rule) in solve::RULES {
790        let mut line = format!("    {name:<10}");
791        if name.len() >= 10 {
792            out.push(line);
793            line = " ".repeat(14);
794        }
795        for word in rule.split(' ') {
796            if line.len() + 1 + word.len() > 80 {
797                out.push(line);
798                line = " ".repeat(15);
799            } else {
800                line.push(' ');
801            }
802            line.push_str(word);
803        }
804        out.push(line);
805    }
806    out.join("\n")
807}
808
809/// One entry per reading, off the one table: its call, what it answers off, and its line.
810fn representations() -> String {
811    READINGS
812        .iter()
813        .map(|row| {
814            let keys: Vec<&str> = row.takes.iter().map(|arg| arg.key()).collect();
815            let call = match keys[..] {
816                [] => row.name.to_string(),
817                _ => format!("{}({})", row.name, keys.join(", ")),
818            };
819            let source = match row.reads {
820                Reads::ClosedForm | Reads::Structure => "exact",
821                Reads::Either => "exact off a closed form, measured off samples",
822                Reads::Samples => "measured",
823                Reads::Graph => "measured, and the graph behind the samples",
824            };
825            format!("  {call:<26} {source}\n      {}", row.help)
826        })
827        .collect::<Vec<_>>()
828        .join("\n")
829}