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_engine::{
5    DEFAULT_BUFFER_SECS, DEFAULT_CELL_SECS, DEFAULT_FLOOR_DB, DEFAULT_FRAME_SECS,
6    DEFAULT_LEDGER_DEPTH, DEFAULT_MAX_PAIRS, DEFAULT_MAX_PEAKS, DEFAULT_MODULATION_WINDOW_SECS,
7    DEFAULT_NOVELTY_WINDOW_SECS, DEFAULT_OVERSAMPLE, DEFAULT_PERIODICITY_WINDOW_SECS,
8    DEFAULT_SAMPLE_RATE, DEFAULT_SMOOTH_SECS, PSYCHOACOUSTIC_V1, READINGS, Reads,
9    self_similarity::MAX_CELLS,
10};
11
12/// Read off the constants the parser itself defaults to, so a printed default cannot drift
13/// from the one a render actually uses.
14pub fn help_text() -> String {
15    let bits = PSYCHOACOUSTIC_V1.precision_bits;
16    let store_gb = DEFAULT_STORE_BYTES >> 30;
17    let representations = representations();
18    format!(
19        r#"USAGE:
20  sva-cli (render | analyze | lint | trace | builtins | outline | new) [arguments]
21
22DESCRIPTION:
23  A composition is a directory of node files, each one closed-form expression in
24  `t` or `f`. sva-cli reads that composition and prints what it is and what it
25  sounds like, as JSON on stdout. `render`, `lint` and `trace` read the current
26  directory; `new` writes beside it and `analyze` reads a file.
27
28RENDER:
29  sva-cli render '<expression>' --representation <list> [--until '<condition>']
30                 [--bits <n>] [--rate <hz>] [--threads <n>] [--cache <path|none>]
31                 [--confirm]
32
33  Renders one expression, in the grammar a node file's body uses, and prints one
34  reading per representation under `data.representations`. `@path` reads a node
35  from the current directory and `@/abs/path` one anywhere; there is no default
36  target. Every reading states its `source` (exact or measured), the
37  conformance profile it ran under, and its rate.
38
39  The target's own ref may read an interval: `@piano4([0, 2b], f0=C4,
40  vel=0.5, release=1s)`, where `release` is the instrument's own parameter. Its ends are the language's literals (`s`, `ms`, `b`,
41  `sp`, or a bare `0`); `[a, inf)` and `[a,)` leave the end open; the other
42  arguments are the node's own named ones. The start trims the output alone:
43  history before it is still computed, so a loop or a filter carries the state
44  it had there. With no interval the render starts at 0, earlier only where a
45  crop reaches before it, and its end is open. Inside an expression a window is
46  a `crop`. A target that samples its one ref read, `sample(@piano4([0, 2b],
47  f0=C4))`, reads that ref's interval and binds and the same node values.
48
49  Every node is computed over its support met with what reads it, and nowhere
50  else: outside its support a node is exactly zero. A closed interval renders
51  exactly its length, nothing cut. An open one ends where the root's support
52  does: a crop's end, the exact underflow of an `exp`, a clamp holding a factor
53  at zero; or earlier, where a proven upper bound on the root's magnitude stays
54  under the profile's silence threshold (-120 dBFS) from a sample on. A root
55  with no such bound (a held sine, a loop) is never cut. The label's `pruned`
56  states the threshold as `db` and the root's cut, `from` the sample the render
57  is treated as silent from. A stream's term leaves `@notes` where its support
58  ends. An open interval over a root whose support never ends and is never cut
59  (a held sine, a physical solver) refuses as `render.no_end`. A short-time
60  transform reads its input whole, and refuses one with no end as
61  `engine.unbounded_extent`. A form in `f` with no dual in `t` has no samples
62  and refuses as its dual does, `cast.left_algebra`.
63
64  A render is a stream pulled to its end: a closed form, a short-time
65  transform and what either reads are computed over their whole extent first,
66  and every other node block by block. `--until '<condition>'` stops the render
67  at the first sample the condition holds at, or at the interval's end,
68  whichever is first. It is checked as each block is pulled, so no node driven
69  block by block runs past the block it holds in. A condition compares (`<`,
70  `<=`, `>`, `>=`) `t`, `envelope(t)` (the RMS over every channel of the
71  `envelope` representation's frame holding `t`, framed by its own `frame`
72  where one is asked) and literals, joined by `and`/`or`.
73
74  `--representation <list>` takes a comma list of readings and may repeat. Each
75  is a call in the language's own syntax, its options its named arguments:
76  `spectrum(peaks=8, frame=50ms)`, `ledger(depth=3, brief=1)`. An entry
77  followed by `=<path>` writes a file instead, listed under `written`:
78  `samples=<path>.wav` writes audio, encoded as `--bits` says; any other path
79  takes the reading's JSON, uncapped. A path that already holds a file refuses
80  unless `--confirm` is written. `samples=alsa:<device>` plays the samples on
81  that ALSA device (`alsa:default`, `alsa:hw:1`, `alsa:plughw:1,0`) as each
82  block is pulled, in real time, through `aplay` from alsa-utils; Linux only,
83  anywhere else it refuses as `destination.unsupported_platform`. It sends
84  32-bit floats, or integer PCM where `--bits` is 16 or under. A device that
85  cannot take the format refuses with aplay's reason, and one aplay says plays
86  another rate than the render's stops there as `destination.rate_inexact`.
87  `samples(buffer=<t>)` is how much the device holds ahead of what it plays.
88  Once it has played out, `data.played` states the device, its seconds, rate,
89  channels, format, the buffer asked and the one granted, and every underrun
90  aplay reported, with what it cannot see.
91  `lines`, `atoms`, `derivative`, `bindings`, `arguments`, and a closed form's
92  `spectrum`, `envelope` and `pitch`, read the expression and ignore the range.
93
94  A closed form's `spectrum` and `pitch` are its exact lines, so a term under a
95  crop or an envelope, which has a width and is no line, refuses them; so do
96  `spectrum(frame=)` and `envelope(frame=)`, as a closed form has no frames. To
97  measure any of them frame by frame, a reader samples the target's ref,
98  `sample(@x([0, 1s]))`, and an author writes `sample(...)` inside the node.
99  A closed form with no exact envelope, such as a crop of a decay, refuses
100  `envelope` with the reason rather than measuring it unasked; `sample(...)`
101  asks for the measured one. A measured `spectrum` is one spectrum: every
102  `frame`-long window across the range, averaged. `pitch` is framed, one entry
103  per `frame`; for the spectrum at one instant, read a range one frame long.
104  A measured `envelope` is framed too, and reads every channel: a frame's
105  `rms` is the root mean square of all its channels' samples together, its
106  `peak` the largest magnitude in any channel.
107
108  `--bits <n>` is the precision every sample is written to, from 2 to 52: the
109  point where a series is truncated, and the encoding of a `.wav`, integer PCM
110  at n bits up to 16 and 32-bit float above. `--rate <hz>` (default
111  {DEFAULT_SAMPLE_RATE}) is the one rate a render samples at: the target is read
112  at its instants, and every node at the instants its reader asks for. A
113  closed form is exact at any instant, and so is a continuous loop, one
114  constant delay at a gain under 1 over a closed form such as
115  `x + 0.5*self(t - 17ms)`, which is its series. A filter, a discrete loop, a
116  solver and `rand` step at the rate asked for, where `1sp` is one step, so an
117  `sp` count or `self[idx(t) - 1]` means one sample at whatever rate is asked.
118  `rand` draws once per step, keyed by that step's index; a key between steps
119  reads the step nearest it, ties to even.
120
121  `--threads <n>` (default: every core the machine reports) is the most
122  threads a render computes on; `1` computes every value in turn on one.
123  Values none of the others reads, and the blocks of a value that holds no
124  state, compute apart, to the same bits on any number of threads.
125
126  A discrete loop reads its own past only by index; `self(t - d)` in one
127  refuses as `type.discrete_self_at_time`, naming what made it discrete. A
128  read `@x(k*t - d)` steps `x` at `k` times the step, every input it reads,
129  `sp` and `idx` with it, and rounds `d` to the nearest sample of that step,
130  ties to even: shifts are rounded to the nearest sample so placements share
131  one cached value; timing is exact to half a sample, and the label's
132  `moved_s` states the most any read moved. A node is computed once
133  per step however many reads ask for it. A node that holds state read at a
134  time that moves refuses as `type.stateful_warp`, naming what
135  holds its state; `@x[idx(...)]` reads its nearest step instead, as `p[idx(...)]`
136  does a signal passed in as parameter `p`, and any `idx`, as
137  `idx(t - 5ms - 2ms*sin(2*pi*t))`, is read sample by sample. An edit to a
138  stream plays from the next sample on.
139
140  A render keeps in a store on disk its target and, under it, each node of
141  samples that holds state (a filter, a discrete loop or a solver in it) or
142  that two or more nodes read. Nothing else is kept. A closed form, `sample`
143  of one and `rand` have a value at any instant and are computed where read.
144  A node without state that one node alone reads, however many times, is
145  computed again by that reader from what the node itself reads: no other
146  reader could ask for it, and without state it restarts at any sample, where
147  a filter or a loop must run from its start. So a stage worth keeping is a
148  node of its own that ends in a filter or a loop, or one two nodes read. A
149  kept value is named by what it computes: never its file's name, directory,
150  comments, spacing, or the order its named arguments are written in, so a
151  renamed or moved copy, a constant lifted into a default, an expression split
152  into a file of its own or written back inline, and two addends swapped all
153  read one entry. The next render of anything that reads the same value reads
154  it back, bit for bit, and computes nothing under it. The store is on by
155  default, at `$XDG_CACHE_HOME/sva`, else `~/.cache/sva`; `--cache <path>`
156  moves it (a `/dev/shm` path keeps it in memory) and `--cache none` turns it
157  off. It holds at most {store_gb} GB, the least recently used values going
158  first, and a store another build of sva-cli wrote is emptied when opened. A
159  render stages what it computes beside the store as it goes, and the store
160  itself is written once, when the render ends, fails, or is stopped by SIGINT
161  or SIGTERM.
162
163  `ledger` prints one row per node under the target. A row's `share` is the
164  part of its reader's own energy that row accounts for, so one reader's refs
165  sum to 1; a ref no addend isolates, such as one factor of a product, prints
166  `null`. Each ref carrying a share is collapsed once on its own, so a ledger
167  costs one collapse per attributed ref beyond the render, and `depth` bounds
168  how many, counted in refs from one file to another. Every row is computed
169  whole over the range, however late a reader reads it. `brief=1` keeps only
170  the rows that clipped, `skim=1` drops the wider fields.
171
172  `arguments` renders nothing: for every instance under the target it prints
173  each builtin call's named arguments as the numbers the call was lowered
174  with, a solver's whole parameter set with `written: false` on each default
175  it filled in, and the operand each `min`/`max` `chosen` where a call folds
176  one to a number: in named arguments and a solver's, modal bank's or
177  `noise`'s positionals. One inside a filter's or cast's positional or
178  `rand`'s key or seed is not listed. `at` spans are bytes of the instance's
179  own body, as `sva-cli outline` counts them.
180
181REPRESENTATIONS:
182  Each one a `--representation` call, its arguments in parentheses, every
183  default under DEFAULTS. `exact` is read off the written math, `measured` off
184  samples.
185
186{representations}
187
188ANALYZE:
189  sva-cli analyze <file.wav> --representation <list> [--confirm]
190
191  Runs the same readings over a whole external `.wav` at its own rate, never
192  resampled. Only the readings a buffer answers alone apply, each listed under
193  REPRESENTATIONS as `measured` or `exact off a closed form, measured off
194  samples`; the rest need the graph behind it.
195
196LINT:
197  sva-cli lint ['<expression>'] [--format <json|text>]
198
199  Checks the current directory without rendering a sample. With no target it
200  checks every file under its own rules, and types every entry point, which
201  types every file it reaches. With a target, in render's grammar, it checks
202  the files that target reaches, and prints the interval a render of it reads.
203  Either refuses a type error as `render` does.
204
205  Every check prints one `data.diagnostics` item. `warning` exits 0, `error`
206  exits non-zero, so branch on the verdict and never on whether the array is
207  empty. No flag downgrades an error.
208
209  error    missing-comment       no `;` comment line
210           multiline-comment     more than one
211           malformed-comment     not four ` | ` fields, `Models:` `Neglects:`
212                                 `IO: <in> -> <out>` `Tags: <tag>[, <tag>...]`
213           long-comment-block    a `;` block over 1000 characters, the line-1
214                                 doc comment's own run exempted
215           long-expression-body  a body over 10000 characters, a backstop rather
216                                 than a complexity budget
217           literal-sample-rate   a written rate where `sp` belongs
218           arity                 a builtin called without an argument it reads,
219                                 or with one it does not, as `rand(seed=k)`
220  warning  grid-rows-per-bar     a TSV grid's row count does not divide evenly
221                                 into its filename's bar span
222           key-is-not-a-pitch    `variables/key` holds neither a note name nor
223                                 a number of hertz
224           parameter-has-no-default
225                                 a node reads a parameter no `name = value` line
226                                 binds, so a bare `@node` cannot play
227           support-never-ends    a node's support, cut where its bound falls
228                                 under the silence threshold, has no end for a
229                                 bare render to stop at
230
231TRACE:
232  sva-cli trace <node|expression>
233
234  Prints one node's position without rendering audio: what it reads (`down`, one
235  hop), everything that reads it (`up`, transitively to an entry point), each
236  beside the expression doing the reading, the node that made it discrete, and
237  the feedback loop it sits in, if any. An interval the target reads is ignored.
238
239BUILTINS:
240  sva-cli builtins
241
242  Prints the whole callable and syntactic vocabulary: every builtin with its
243  arity and named arguments, each argument's `meaning`, `unit`, the model
244  `part` it sets and whether it `moves` with `t` (a filter's cutoff, q and gain;
245  every other named argument is one number, refused when it names none), each
246  positional's meaning where the model states one, unit suffixes, the note-name
247  grammar, reserved identifiers, special call shapes, and what has no operator
248  at all.
249
250OUTLINE:
251  sva-cli outline <expression>
252
253  Prints the parse tree the engine builds from one expression, each node with
254  the byte `span` it was written in: calls by `name` with positional and named
255  `args`, operators by `op`, refs by `path` with their `binds`, refs and `self`
256  by how they `read`, `time` or `index`, literals by `value` and `unit`, names
257  by `name`. A node the parser supplies itself, the `0` of a prefix minus or the
258  `t` of a bare `@ref`, has `written: false`. Reads no composition.
259
260NEW:
261  sva-cli new <name> [--idempotency-key <key>]
262
263  Writes a starter composition at ./<name>, and refuses if that directory
264  exists. `--idempotency-key <key>` records the key beside the composition, so a
265  retry under the same key succeeds identically while the tree still holds what
266  was written. Any other key, or an edited tree, refuses.
267
268EXAMPLES:
269  sva-cli new song1 && cd song1
270  sva-cli render '@master' --representation samples=/tmp/song1.wav
271  sva-cli render '@master([0, 8b])' --representation 'ledger(depth=2),loudness'
272  sva-cli render '@voice/note([0, inf), f0=C4, len=2s)' \
273    --until 'envelope(t) < -60db and t > 1s' --representation samples=/tmp/note.wav
274  sva-cli render 'sample(@chord/home)' --representation 'spectrum(peaks=8)' --rate 48000
275  sva-cli render 'sample(@voice/note([0, 2s], f0=C4))' --representation pitch
276  sva-cli render '@master' \
277    --representation 'samples(buffer=100ms)=alsa:default,loudness'
278  sva-cli lint
279  sva-cli lint '@master'
280  sva-cli trace grid/phrase-2b
281  sva-cli builtins
282
283OUTPUT:
284  {{"status": "success", "data": {{"target": "@master([0, 8b])", "sample_rate":
285  {DEFAULT_SAMPLE_RATE}, "bits": {bits}, "profile": "psychoacoustic-v1",
286  "interval": {{"start_secs":
287  0, "end_secs": 16}}, "label": {{...}}, "written": {{"items": [...]}},
288  "representations": {{"ledger": {{...}}}}, "diagnostics": {{"items": []}}}},
289  "meta": {{"request_id": "req_...", "timestamp": 1700000000}}}}
290
291  `interval` is null where no reading read samples. An error adds "error":
292  {{"code", "message", "details": {{"count", "codes"}}}}. Success or error,
293  every response carries every finding in full at "data": {{"diagnostics":
294  {{"items": [{{"code", "severity", "message", "location", "help"}}],
295  "pagination": {{"count", "has_more", "next_cursor"}}}}}}, empty where it found
296  none.
297
298  Every collection carries that same {{items, pagination}} pair. `count` is the
299  whole reading's, `has_more` says `items` holds less than that, and
300  `next_cursor` is the interval start, as `0.5s`, the rest is read from. A
301  reading written to a file is written whole, uncapped. This page is an
302  envelope of its own, at "data": {{"help"}}.
303
304DEFAULTS:
305  --rate <hz>          the one rate a render samples at, and `1sp`.
306                       Default {DEFAULT_SAMPLE_RATE}.
307  --bits <n>           the precision every sample is written to. Default {bits},
308                       the `psychoacoustic-v1` profile's own.
309  ledger(depth=<n>)    how deep below its target a `ledger` walks.
310                       Default {DEFAULT_LEDGER_DEPTH}.
311  (peaks=<n>)          peaks a `spectrum` keeps, notes a `pitch`, formants a
312                       `formants`, stripes and novelty peaks a `self_similarity`
313                       lists per view. Default {DEFAULT_MAX_PEAKS}.
314  alias(oversample=<n>) the multiple `alias` re-renders at to hear what folded.
315                       Default {DEFAULT_OVERSAMPLE}.
316  (frame=<t>)          the step a framed reading advances by, in seconds.
317                       Default {DEFAULT_FRAME_SECS}, except `spectrum`, which sizes
318                       its own transform to the range unless it is set.
319  (floor=<db>)         the amplitude, as `-40db`, under which a harmonic reading
320                       or `self_similarity` drops a partial, and under which
321                       `self_similarity` counts a band level as zero.
322                       Default {DEFAULT_FLOOR_DB}db.
323  voice_leading(norm=<l1|l2>) how a voice leading's moves add up. Default l1.
324  dissonance(pairs=<n>) the most partial pairs one frame sums before it refuses.
325                       Default {DEFAULT_MAX_PAIRS}.
326  periodicity(window=<t>) the span of onset strength one window autocorrelates,
327                       in seconds, at least twice the longest period it reads.
328                       Default {DEFAULT_PERIODICITY_WINDOW_SECS}.
329  periodicity(curve=<0|1>) `1` adds every window's height at every lag.
330                       Default 0.
331  modulation(window=<t>) the span of band envelope one window transforms, in
332                       seconds, at least twice the slowest period it reads.
333                       Default {DEFAULT_MODULATION_WINDOW_SECS}.
334  modulation(bpm=<n>)  a tempo, as `140`, adding each rate's period in beats.
335                       Default none.
336  self_similarity(cell=<t>) the side of one matrix cell, in seconds, a whole
337                       number of frames; the target may span at most
338                       {MAX_CELLS} cells. Default {DEFAULT_CELL_SECS}.
339  self_similarity(smooth=<t>) the length each diagonal is averaged over
340                       before stripes are picked, in seconds.
341                       Default {DEFAULT_SMOOTH_SECS}.
342  self_similarity(window=<t>) the whole width of the novelty kernel, in
343                       seconds. Default {DEFAULT_NOVELTY_WINDOW_SECS}.
344  self_similarity(curve=<0|1>) `1` adds every cell's chroma and band levels and
345                       each view's matrix. Default 0.
346  bindings(node=@<path>) the instance whose bindings are read; required.
347  samples(buffer=<t>)  how much an `alsa:` device holds ahead of what it plays,
348                       in seconds. Default {DEFAULT_BUFFER_SECS}; only a device
349                       has one.
350  ledger(brief, skim)  `0` unless set `1`.
351  --format <json|text> how `lint` prints its findings: the envelope, or one
352                       terminal line each, colored where stdout is a terminal.
353                       The same objects either way. Default json.
354  --cache <path|none>  where the store lives. Default `$XDG_CACHE_HOME/sva`, else
355                       `~/.cache/sva`, holding at most {store_gb} GB; `none` keeps
356                       no store. A store that fails fails no render: it answers
357                       as without one, warned why in `diagnostics`.
358  --confirm            replaces a destination that already holds a file. Without
359                       it a path already taken refuses as `conflict` and nothing
360                       is written.
361
362ENVIRONMENT:
363  SVA_LOG=debug        `render` logs, once it ends, what it asked of each value
364                       to stderr; stdout and the samples are unchanged. Any
365                       other value, or none, logs nothing. One line per second
366                       of output, then one per node and a total:
367                         sva-cache pass hit=0 miss=190 ... reused=0 cum-hit=0.0%
368                         sva-cache t=1.022s hit=1 miss=2 ... cum-hit=0.5%
369                         sva-cache node hit=1 miss=0 ... reused=3 <node>
370                         sva-cache total hit=1 miss=198 ... hit-rate=0.5% ...
371                       `pass` is what was looked up before the first block.
372                       Each value is looked up once: a `hit` was answered by
373                       the store, `prefix` found a stored run up to a switch,
374                       `miss` computed it, `new` held it in memory, where the
375                       store takes what it keeps from. Each read of a value
376                       past its first is `reused`.
377
378EXIT CODES:
379  0  success (error.code absent)
380  1  internal_error (a destination could not be written)
381  3  validation_error (bad arguments, a composition that failed to parse, or a
382     render the engine refused)
383  4  conflict (a name `new` would overwrite, or a destination already holding a
384     file, without `--confirm`)
385  24 not_found (a `.wav` file, or a node this composition does not define)
386
387VERB ALIASES:
388  validate = lint, list = builtins, create = new, show = trace, from the `cli`
389  standard's own verb list. `render` and `analyze` take a reading, which that
390  list has no word for, so they keep their own names.
391
392SEE ALSO:
393  sva-cli --version    Show version information"#
394    )
395}
396
397/// One entry per reading, off the one table: its call, what it answers off, and its line.
398fn representations() -> String {
399    READINGS
400        .iter()
401        .map(|row| {
402            let keys: Vec<&str> = row.takes.iter().map(|arg| arg.key()).collect();
403            let call = match keys[..] {
404                [] => row.name.to_string(),
405                _ => format!("{}({})", row.name, keys.join(", ")),
406            };
407            let source = match row.reads {
408                Reads::ClosedForm | Reads::Structure => "exact",
409                Reads::Either => "exact off a closed form, measured off samples",
410                Reads::Samples => "measured",
411                Reads::Graph => "measured, and the graph behind the samples",
412            };
413            format!("  {call:<27}{source}\n      {}", row.help)
414        })
415        .collect::<Vec<_>>()
416        .join("\n")
417}