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 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}