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