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_LEDGER_DEPTH, DEFAULT_MAX_PEAKS, DEFAULT_OVERSAMPLE, DEFAULT_STORE_BYTES};
4use sva_engine::{
5    DEFAULT_FRAME_SECS, DEFAULT_SAMPLE_RATE, PSYCHOACOUSTIC_V1, QUIET_AFTER_SECS, QUIET_LEVEL,
6};
7
8/// Read off the constants the parser itself defaults to, so a printed default cannot drift
9/// from the one a render actually uses.
10pub fn help_text() -> String {
11    let budget = PSYCHOACOUSTIC_V1.flop_budget;
12    let bits = PSYCHOACOUSTIC_V1.precision_bits;
13    let quiet = 20.0 * QUIET_LEVEL.log10();
14    let store_gb = DEFAULT_STORE_BYTES >> 30;
15    format!(
16        r#"USAGE:
17  sva-cli (render | analyze | lint | trace | builtins | outline | new) [arguments]
18
19DESCRIPTION:
20  A composition is a directory of node files, each one closed-form expression in
21  `t` or `f`. sva-cli reads that composition and prints what it is and what it
22  sounds like, as JSON on stdout. `render`, `lint` and `trace` read the current
23  directory; `new` writes beside it and `analyze` reads a file.
24
25RENDER:
26  sva-cli render '<expression>' --representation <list> [--until '<condition>']
27                 [--bits <n>] [--rate <hz>] [--flop-budget <n>] [--cache <path|none>]
28                 [--confirm]
29
30  Renders one expression, in the grammar a node file's body uses, and prints one
31  reading per representation under `data.representations`. `@path` reads a node
32  from the current directory and `@/abs/path` one anywhere; there is no default
33  target. Every reading states its `source` (exact or measured), the
34  conformance profile it ran under, and its rate.
35
36  The target's own ref may read an interval: `@piano4([0, 2b], f0=C4,
37  vel=0.5, release=1s)`, where `release` is the instrument's own parameter. Its ends are the language's literals (`s`, `ms`, `b`,
38  `sp`, or a bare `0`); `[a, inf)` and `[a,)` leave the end open; the other
39  arguments are the node's own named ones. The start trims the output alone:
40  history before it is still computed, so a loop or a filter carries the state
41  it had there. With no interval the render starts at 0, earlier only where a
42  crop reaches before it, and its end is open. Inside an expression a window is
43  a `crop`.
44
45  Every node is computed over its support met with what reads it, and nowhere
46  else: outside its support a node is exactly zero. A closed interval renders
47  exactly its length; an open one ends where the root's support does, at a
48  crop's end, or at the exact underflow of an `exp` a crop opens. An open interval over a root whose support never ends (a held
49  sine, a physical solver) refuses as `render.no_end`. A short-time transform
50  reads its input whole, and refuses one with no end as
51  `engine.unbounded_extent`.
52
53  A render is a stream pulled to its end: a closed form, a short-time
54  transform and what either reads are computed over their whole extent first,
55  and every other node block by block. `--until '<condition>'` stops the render
56  at the first sample the condition holds at, or at the interval's end,
57  whichever is first. It is checked as each block is pulled, so no node driven
58  block by block runs past the block it holds in. A condition compares (`<`,
59  `<=`, `>`, `>=`) `t`, `envelope(t)` (the RMS over every channel of the
60  `envelope` representation's frame holding `t`, framed by its own `frame`
61  where one is asked) and literals, joined by `and`/`or`.
62
63  `--representation <list>` takes a comma list of readings and may repeat. Each
64  is a call in the language's own syntax, its options its named arguments:
65  `spectrum(peaks=8, frame=50ms)`, `ledger(depth=3, brief=1)`. An entry
66  followed by `=<path>` writes a file instead, listed under `written`:
67  `samples=<path>.wav` writes audio, encoded as `--bits` says; any other path
68  takes the reading's JSON, uncapped. A path that already holds a file refuses
69  unless `--confirm` is written. `lines`, `atoms`, `derivative`, `bindings`,
70  `arguments`, and a closed form's `spectrum`, `envelope` and `pitch`, read the
71  expression and ignore the range.
72
73  A closed form's `spectrum` and `pitch` are its exact lines, so a term under a
74  crop or an envelope, which has a width and is no line, refuses them; so does
75  `spectrum(frame=)`, as a closed form has no frames. `sample(...)` of it
76  measures either frame by frame. A measured `spectrum` is one spectrum: every
77  `frame`-long window across the range, averaged. `pitch` is framed, one entry
78  per `frame`; for the spectrum at one instant, read a range one frame long.
79  A measured `envelope` is framed too, and reads every channel: a frame's
80  `rms` is the root mean square of all its channels' samples together, its
81  `peak` the largest magnitude in any channel.
82
83  `--bits <n>` is the precision every sample is written to, from 2 to 52: the
84  point where a series is truncated, and the encoding of a `.wav`, integer PCM
85  at n bits up to 16 and 32-bit float above. `--rate <hz>` (default
86  {DEFAULT_SAMPLE_RATE}) is the one rate a render samples at: the target is read
87  at its instants, and every node at the instants its reader asks for. A
88  closed form is exact at any instant, and so is a continuous loop, one
89  constant delay at a gain under 1 over a closed form such as
90  `x + 0.5*self(t - 17ms)`, which is its series. A filter, a discrete loop, a
91  solver and `rand` step at the rate asked for, where `1sp` is one step, so an
92  `sp` count or `self[idx(t) - 1]` means one sample at whatever rate is asked.
93  `rand` draws once per step, keyed by that step's index; a key between steps
94  reads the step nearest it, ties to even.
95  A discrete loop reads its own past only by index; `self(t - d)` in one
96  refuses as `type.discrete_self_at_time`, naming what made it discrete. A
97  read `@x(k*t - d)` steps `x` at `k` times the step, every input it reads,
98  `sp` and `idx` with it, and rounds `d` to the nearest sample of that step,
99  ties to even: shifts are rounded to the nearest sample so placements share
100  one cached value; timing is exact to half a sample, and the label's
101  `moved_s` states the most any read moved. A node is computed once
102  per step however many reads ask for it. A node that holds state read at a
103  time that moves refuses as `type.stateful_warp`, naming what
104  holds its state; `@x[idx(...)]` reads its nearest step instead, as `p[idx(...)]`
105  does a signal passed in as parameter `p`, and any `idx`, as
106  `idx(t - 5ms - 2ms*sin(2*pi*t))`, is read sample by sample. An edit to a
107  stream plays from the next sample on.
108  `--flop-budget <n>` is the operation count paid before a render refuses.
109
110  Every node's value a render computes is kept in a store on disk, named by
111  the node's file text, its bindings and what it reads, so the next render of
112  anything that reads the same node reads it back, bit for bit, and neither
113  types nor computes anything under it. The store is on by default, at
114  `$XDG_CACHE_HOME/sva`, else `~/.cache/sva`; `--cache <path>` moves it (a
115  `/dev/shm` path keeps it in memory) and `--cache none` turns it off. It holds
116  at most {store_gb} GB, the least recently used values going first, and a
117  store another build of sva-cli wrote is emptied when opened. A render stages
118  what it computes beside the store as it goes, and the store itself is written
119  once, when the render ends, fails, or is stopped by SIGINT or SIGTERM.
120
121  `ledger` prints one row per node under the target. A row's `share` is the
122  part of its reader's own energy that row accounts for, so one reader's refs
123  sum to 1; a ref no addend isolates, such as one factor of a product, prints
124  `null`. Each ref carrying a share is collapsed once on its own, so a ledger
125  costs one collapse per attributed ref beyond the render, and `depth` bounds
126  how many, counted in refs from one file to another. Every row is computed
127  whole over the range, however late a reader reads it. `brief=1` keeps only
128  the rows that clipped, `skim=1` drops the wider fields.
129
130  `arguments` renders nothing: for every instance under the target it prints
131  each builtin call's named arguments as the numbers the call was lowered
132  with, a solver's whole parameter set with `written: false` on each default
133  it filled in, and the operand each `min`/`max` `chosen` where a call folds
134  one to a number: in named arguments and a solver's, modal bank's or
135  `noise`'s positionals. One inside a filter's or cast's positional or
136  `rand`'s key or seed is not listed. `at` spans are bytes of the instance's
137  own body, as `sva-cli outline` counts them.
138
139ANALYZE:
140  sva-cli analyze <file.wav> --representation <list> [--confirm]
141
142  Runs the same readings over a whole external `.wav` at its own rate, never
143  resampled. Only the readings a buffer answers alone apply; the rest need the
144  graph behind it. `masking(against=<file.wav>)` names the second signal
145  masking reads against.
146
147LINT:
148  sva-cli lint ['<expression>'] [--format <json|text>]
149
150  Checks the current directory without rendering a sample. With no target it
151  checks every file under its own rules, types every entry point, which types
152  every file it reaches, and checks the tails under every entry point a render
153  can take. With a target, in render's grammar, it checks the files that target
154  reaches, and prints the interval a render of it reads. Either refuses a type
155  error as `render` does.
156
157  `quiet-tail` names a file some instance of which is proven under {quiet:.1}
158  dBFS, half a 24-bit step, from a second T on, while its extent runs on more
159  than {QUIET_AFTER_SECS}s past T, or never ends. The proof reads the file's own
160  closed form, its atoms or its formula, and nothing it reads; a solver, a
161  filter and a loop are never named. Crop it at T, or accept the error.
162
163  Every check prints one `data.diagnostics` item. `warning` exits 0, `error`
164  exits non-zero, so branch on the verdict and never on whether the array is
165  empty. No flag downgrades an error.
166
167  error    missing-comment       no `;` comment line
168           multiline-comment     more than one
169           malformed-comment     not four ` | ` fields, `Models:` `Neglects:`
170                                 `IO: <in> -> <out>` `Tags: <tag>[, <tag>...]`
171           long-comment-block    a `;` block over 1000 characters, the line-1
172                                 doc comment's own run exempted
173           long-expression-body  a body over 10000 characters, a backstop rather
174                                 than a complexity budget
175           literal-sample-rate   a written rate where `sp` belongs
176           arity                 a builtin called without an argument it reads,
177                                 or with one it does not, as `rand(seed=k)`
178           quiet-tail            computed well past where it is under the
179                                 output's resolution
180  warning  grid-rows-per-bar     a TSV grid's row count does not divide evenly
181                                 into its filename's bar span
182           key-is-not-a-pitch    `variables/key` holds neither a note name nor
183                                 a number of hertz
184
185TRACE:
186  sva-cli trace <node|expression>
187
188  Prints one node's position without rendering audio: what it reads (`down`, one
189  hop), everything that reads it (`up`, transitively to an entry point), each
190  beside the expression doing the reading, the node that made it discrete, and
191  the feedback loop it sits in, if any. An interval the target reads is ignored.
192
193BUILTINS:
194  sva-cli builtins
195
196  Prints the whole callable and syntactic vocabulary: every builtin with its
197  arity and named arguments, each argument's `meaning`, `unit`, the model
198  `part` it sets and whether it `moves` with `t` (a filter's cutoff, q and gain;
199  every other named argument is one number, refused when it names none), each
200  positional's meaning where the model states one, unit suffixes, the note-name
201  grammar, reserved identifiers, special call shapes, and what has no operator
202  at all.
203
204OUTLINE:
205  sva-cli outline <expression>
206
207  Prints the parse tree the engine builds from one expression, each node with
208  the byte `span` it was written in: calls by `name` with positional and named
209  `args`, operators by `op`, refs by `path` with their `binds`, refs and `self`
210  by how they `read`, `time` or `index`, literals by `value` and `unit`, names
211  by `name`. A node the parser supplies itself, the `0` of a prefix minus or the
212  `t` of a bare `@ref`, has `written: false`. Reads no composition.
213
214NEW:
215  sva-cli new <name> [--idempotency-key <key>]
216
217  Writes a starter composition at ./<name>, and refuses if that directory
218  exists. `--idempotency-key <key>` records the key beside the composition, so a
219  retry under the same key succeeds identically while the tree still holds what
220  was written. Any other key, or an edited tree, refuses.
221
222EXAMPLES:
223  sva-cli new song1 && cd song1
224  sva-cli render '@master' --representation samples=/tmp/song1.wav
225  sva-cli render '@master([0, 8b])' --representation 'ledger(depth=2),loudness'
226  sva-cli render '@voice/note([0, inf), f0=C4, len=2s)' \
227    --until 'envelope(t) < -60db and t > 1s' --representation samples=/tmp/note.wav
228  sva-cli render '@chord/home' --representation 'spectrum(peaks=8)' --rate 48000
229  sva-cli lint
230  sva-cli lint '@master'
231  sva-cli trace grid/phrase-2b
232  sva-cli builtins
233
234OUTPUT:
235  {{"status": "success", "data": {{"target": "@master([0, 8b])", "sample_rate":
236  {DEFAULT_SAMPLE_RATE}, "bits": {bits}, "profile": "psychoacoustic-v1",
237  "interval": {{"start_secs":
238  0, "end_secs": 16}}, "label": {{...}}, "written": {{"items": [...]}},
239  "representations": {{"ledger": {{...}}}}, "diagnostics": {{"items": []}}}},
240  "meta": {{"request_id": "req_...", "timestamp": 1700000000}}}}
241
242  `interval` is null where no reading read samples. An error adds "error":
243  {{"code", "message", "details": {{"count", "codes"}}}}. Success or error,
244  every response carries every finding in full at "data": {{"diagnostics":
245  {{"items": [{{"code", "severity", "message", "location", "help"}}],
246  "pagination": {{"count", "has_more", "next_cursor"}}}}}}, empty where it found
247  none.
248
249  Every collection carries that same {{items, pagination}} pair. `count` is the
250  whole reading's, `has_more` says `items` holds less than that, and
251  `next_cursor` is the interval start, as `0.5s`, the rest is read from. A
252  reading written to a file is written whole, uncapped. This page is an
253  envelope of its own, at "data": {{"help"}}.
254
255DEFAULTS:
256  --rate <hz>          the one rate a render samples at, and `1sp`.
257                       Default {DEFAULT_SAMPLE_RATE}.
258  --bits <n>           the precision every sample is written to. Default {bits},
259                       the `psychoacoustic-v1` profile's own.
260  --flop-budget <n>    the operation count paid before a render refuses.
261                       Default {budget}, the `psychoacoustic-v1` profile's own.
262  ledger(depth=<n>)    how deep below its target a `ledger` walks.
263                       Default {DEFAULT_LEDGER_DEPTH}.
264  (peaks=<n>)          peaks a `spectrum` keeps, notes a `pitch`, formants a
265                       `formants`. Default {DEFAULT_MAX_PEAKS}.
266  alias(oversample=<n>) the multiple `alias` re-renders at to hear what folded.
267                       Default {DEFAULT_OVERSAMPLE}.
268  (frame=<t>)          the step a framed reading advances by, in seconds.
269                       Default {DEFAULT_FRAME_SECS}, except `spectrum`, which sizes
270                       its own transform to the range unless it is set.
271  bindings(node=@<path>) the instance whose bindings are read; required.
272  ledger(brief, skim)  `0` unless set `1`.
273  --format <json|text> how `lint` prints its findings: the envelope, or one
274                       terminal line each, colored where stdout is a terminal.
275                       The same objects either way. Default json.
276  --cache <path|none>  where the store lives. Default `$XDG_CACHE_HOME/sva`, else
277                       `~/.cache/sva`, holding at most {store_gb} GB; `none` keeps
278                       no store. A store that fails fails no render: it answers
279                       as without one, warned why in `diagnostics`.
280  --confirm            replaces a destination that already holds a file. Without
281                       it a path already taken refuses as `conflict` and nothing
282                       is written.
283
284ENVIRONMENT:
285  SVA_LOG=debug        `render` logs, once it ends, what it asked of each value
286                       to stderr; stdout and the samples are unchanged. Any
287                       other value, or none, logs nothing. One line per second
288                       of output, then one per node and a total:
289                         sva-cache pass hit=0 miss=190 ... store-miss=190 cum-hit=0.0%
290                         sva-cache t=1.022s hit=1 miss=2 ... cum-hit=0.5%
291                         sva-cache node hit=0 miss=2 ... store-hit=1 <node>
292                         sva-cache total hit=1 miss=198 ... hit-rate=0.5% ...
293                       `pass` is what was looked up before the first block;
294                       each read of a value past its first is a `hit`, reusing
295                       it; `prefix` found a stored run up to a switch, `miss`
296                       computed it, `new` kept it for the store. `store-hit`
297                       and `store-miss` count what the store on disk answered
298                       each value's first lookup.
299
300EXIT CODES:
301  0  success (error.code absent)
302  1  internal_error (a destination could not be written)
303  3  validation_error (bad arguments, a composition that failed to parse, or a
304     render the engine refused)
305  4  conflict (a name `new` would overwrite, or a destination already holding a
306     file, without `--confirm`)
307  24 not_found (a `.wav` file, or a node this composition does not define)
308
309VERB ALIASES:
310  validate = lint, list = builtins, create = new, show = trace, from the `cli`
311  standard's own verb list. `render` and `analyze` take a reading, which that
312  list has no word for, so they keep their own names.
313
314SEE ALSO:
315  sva-cli --version    Show version information"#
316    )
317}