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