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