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