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