1use 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
6pub 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 the node's file text, its bindings and what it reads, so the next render of
117 anything that reads the same node reads it back, bit for bit, and neither
118 types nor computes anything under it. The store is on by default, at
119 `$XDG_CACHE_HOME/sva`, else `~/.cache/sva`; `--cache <path>` moves it (a
120 `/dev/shm` path keeps it in memory) and `--cache none` turns it off. It holds
121 at most {store_gb} GB, the least recently used values going first, and a
122 store another build of sva-cli wrote is emptied when opened. A render stages
123 what it computes beside the store as it goes, and the store itself is written
124 once, when the render ends, fails, or is stopped by SIGINT or SIGTERM.
125
126 `ledger` prints one row per node under the target. A row's `share` is the
127 part of its reader's own energy that row accounts for, so one reader's refs
128 sum to 1; a ref no addend isolates, such as one factor of a product, prints
129 `null`. Each ref carrying a share is collapsed once on its own, so a ledger
130 costs one collapse per attributed ref beyond the render, and `depth` bounds
131 how many, counted in refs from one file to another. Every row is computed
132 whole over the range, however late a reader reads it. `brief=1` keeps only
133 the rows that clipped, `skim=1` drops the wider fields.
134
135 `arguments` renders nothing: for every instance under the target it prints
136 each builtin call's named arguments as the numbers the call was lowered
137 with, a solver's whole parameter set with `written: false` on each default
138 it filled in, and the operand each `min`/`max` `chosen` where a call folds
139 one to a number: in named arguments and a solver's, modal bank's or
140 `noise`'s positionals. One inside a filter's or cast's positional or
141 `rand`'s key or seed is not listed. `at` spans are bytes of the instance's
142 own body, as `sva-cli outline` counts them.
143
144ANALYZE:
145 sva-cli analyze <file.wav> --representation <list> [--confirm]
146
147 Runs the same readings over a whole external `.wav` at its own rate, never
148 resampled. Only the readings a buffer answers alone apply; the rest need the
149 graph behind it. `masking(against=<file.wav>)` names the second signal
150 masking reads against.
151
152LINT:
153 sva-cli lint ['<expression>'] [--format <json|text>]
154
155 Checks the current directory without rendering a sample. With no target it
156 checks every file under its own rules, and types every entry point, which
157 types every file it reaches. With a target, in render's grammar, it checks
158 the files that target reaches, and prints the interval a render of it reads.
159 Either refuses a type error as `render` does.
160
161 Every check prints one `data.diagnostics` item. `warning` exits 0, `error`
162 exits non-zero, so branch on the verdict and never on whether the array is
163 empty. No flag downgrades an error.
164
165 error missing-comment no `;` comment line
166 multiline-comment more than one
167 malformed-comment not four ` | ` fields, `Models:` `Neglects:`
168 `IO: <in> -> <out>` `Tags: <tag>[, <tag>...]`
169 long-comment-block a `;` block over 1000 characters, the line-1
170 doc comment's own run exempted
171 long-expression-body a body over 10000 characters, a backstop rather
172 than a complexity budget
173 literal-sample-rate a written rate where `sp` belongs
174 arity a builtin called without an argument it reads,
175 or with one it does not, as `rand(seed=k)`
176 warning grid-rows-per-bar a TSV grid's row count does not divide evenly
177 into its filename's bar span
178 key-is-not-a-pitch `variables/key` holds neither a note name nor
179 a number of hertz
180 parameter-has-no-default
181 a node reads a parameter no `name = value` line
182 binds, so a bare `@node` cannot play
183 support-never-ends a node's support, pruned at the profile's
184 floor, has no end for a bare render to stop at
185
186TRACE:
187 sva-cli trace <node|expression>
188
189 Prints one node's position without rendering audio: what it reads (`down`, one
190 hop), everything that reads it (`up`, transitively to an entry point), each
191 beside the expression doing the reading, the node that made it discrete, and
192 the feedback loop it sits in, if any. An interval the target reads is ignored.
193
194BUILTINS:
195 sva-cli builtins
196
197 Prints the whole callable and syntactic vocabulary: every builtin with its
198 arity and named arguments, each argument's `meaning`, `unit`, the model
199 `part` it sets and whether it `moves` with `t` (a filter's cutoff, q and gain;
200 every other named argument is one number, refused when it names none), each
201 positional's meaning where the model states one, unit suffixes, the note-name
202 grammar, reserved identifiers, special call shapes, and what has no operator
203 at all.
204
205OUTLINE:
206 sva-cli outline <expression>
207
208 Prints the parse tree the engine builds from one expression, each node with
209 the byte `span` it was written in: calls by `name` with positional and named
210 `args`, operators by `op`, refs by `path` with their `binds`, refs and `self`
211 by how they `read`, `time` or `index`, literals by `value` and `unit`, names
212 by `name`. A node the parser supplies itself, the `0` of a prefix minus or the
213 `t` of a bare `@ref`, has `written: false`. Reads no composition.
214
215NEW:
216 sva-cli new <name> [--idempotency-key <key>]
217
218 Writes a starter composition at ./<name>, and refuses if that directory
219 exists. `--idempotency-key <key>` records the key beside the composition, so a
220 retry under the same key succeeds identically while the tree still holds what
221 was written. Any other key, or an edited tree, refuses.
222
223EXAMPLES:
224 sva-cli new song1 && cd song1
225 sva-cli render '@master' --representation samples=/tmp/song1.wav
226 sva-cli render '@master([0, 8b])' --representation 'ledger(depth=2),loudness'
227 sva-cli render '@voice/note([0, inf), f0=C4, len=2s)' \
228 --until 'envelope(t) < -60db and t > 1s' --representation samples=/tmp/note.wav
229 sva-cli render 'sample(@chord/home)' --representation 'spectrum(peaks=8)' --rate 48000
230 sva-cli render 'sample(@voice/note([0, 2s], f0=C4))' --representation pitch
231 sva-cli lint
232 sva-cli lint '@master'
233 sva-cli trace grid/phrase-2b
234 sva-cli builtins
235
236OUTPUT:
237 {{"status": "success", "data": {{"target": "@master([0, 8b])", "sample_rate":
238 {DEFAULT_SAMPLE_RATE}, "bits": {bits}, "profile": "psychoacoustic-v1",
239 "interval": {{"start_secs":
240 0, "end_secs": 16}}, "label": {{...}}, "written": {{"items": [...]}},
241 "representations": {{"ledger": {{...}}}}, "diagnostics": {{"items": []}}}},
242 "meta": {{"request_id": "req_...", "timestamp": 1700000000}}}}
243
244 `interval` is null where no reading read samples. An error adds "error":
245 {{"code", "message", "details": {{"count", "codes"}}}}. Success or error,
246 every response carries every finding in full at "data": {{"diagnostics":
247 {{"items": [{{"code", "severity", "message", "location", "help"}}],
248 "pagination": {{"count", "has_more", "next_cursor"}}}}}}, empty where it found
249 none.
250
251 Every collection carries that same {{items, pagination}} pair. `count` is the
252 whole reading's, `has_more` says `items` holds less than that, and
253 `next_cursor` is the interval start, as `0.5s`, the rest is read from. A
254 reading written to a file is written whole, uncapped. This page is an
255 envelope of its own, at "data": {{"help"}}.
256
257DEFAULTS:
258 --rate <hz> the one rate a render samples at, and `1sp`.
259 Default {DEFAULT_SAMPLE_RATE}.
260 --bits <n> the precision every sample is written to. Default {bits},
261 the `psychoacoustic-v1` profile's own.
262 --flop-budget <n> the operation count paid before a render refuses.
263 Default {budget}, the `psychoacoustic-v1` profile's own.
264 ledger(depth=<n>) how deep below its target a `ledger` walks.
265 Default {DEFAULT_LEDGER_DEPTH}.
266 (peaks=<n>) peaks a `spectrum` keeps, notes a `pitch`, formants a
267 `formants`. Default {DEFAULT_MAX_PEAKS}.
268 alias(oversample=<n>) the multiple `alias` re-renders at to hear what folded.
269 Default {DEFAULT_OVERSAMPLE}.
270 (frame=<t>) the step a framed reading advances by, in seconds.
271 Default {DEFAULT_FRAME_SECS}, except `spectrum`, which sizes
272 its own transform to the range unless it is set.
273 bindings(node=@<path>) the instance whose bindings are read; required.
274 ledger(brief, skim) `0` unless set `1`.
275 --format <json|text> how `lint` prints its findings: the envelope, or one
276 terminal line each, colored where stdout is a terminal.
277 The same objects either way. Default json.
278 --cache <path|none> where the store lives. Default `$XDG_CACHE_HOME/sva`, else
279 `~/.cache/sva`, holding at most {store_gb} GB; `none` keeps
280 no store. A store that fails fails no render: it answers
281 as without one, warned why in `diagnostics`.
282 --confirm replaces a destination that already holds a file. Without
283 it a path already taken refuses as `conflict` and nothing
284 is written.
285
286ENVIRONMENT:
287 SVA_LOG=debug `render` logs, once it ends, what it asked of each value
288 to stderr; stdout and the samples are unchanged. Any
289 other value, or none, logs nothing. One line per second
290 of output, then one per node and a total:
291 sva-cache pass hit=0 miss=190 ... store-miss=190 cum-hit=0.0%
292 sva-cache t=1.022s hit=1 miss=2 ... cum-hit=0.5%
293 sva-cache node hit=0 miss=2 ... store-hit=1 <node>
294 sva-cache total hit=1 miss=198 ... hit-rate=0.5% ...
295 `pass` is what was looked up before the first block;
296 each read of a value past its first is a `hit`, reusing
297 it; `prefix` found a stored run up to a switch, `miss`
298 computed it, `new` kept it for the store. `store-hit`
299 and `store-miss` count what the store on disk answered
300 each value's first lookup.
301
302EXIT CODES:
303 0 success (error.code absent)
304 1 internal_error (a destination could not be written)
305 3 validation_error (bad arguments, a composition that failed to parse, or a
306 render the engine refused)
307 4 conflict (a name `new` would overwrite, or a destination already holding a
308 file, without `--confirm`)
309 24 not_found (a `.wav` file, or a node this composition does not define)
310
311VERB ALIASES:
312 validate = lint, list = builtins, create = new, show = trace, from the `cli`
313 standard's own verb list. `render` and `analyze` take a reading, which that
314 list has no word for, so they keep their own names.
315
316SEE ALSO:
317 sva-cli --version Show version information"#
318 )
319}