1use sva_core::DEFAULT_STORE_BYTES;
4use sva_core::solve;
5use sva_engine::resample;
6use sva_engine::{
7 DEFAULT_BUFFER_SECS, DEFAULT_CELL_SECS, DEFAULT_FLOOR_DB, DEFAULT_FRAME_SECS,
8 DEFAULT_LEDGER_DEPTH, DEFAULT_MAX_PAIRS, DEFAULT_MAX_PEAKS, DEFAULT_MODULATION_WINDOW_SECS,
9 DEFAULT_NOVELTY_WINDOW_SECS, DEFAULT_OVERSAMPLE, DEFAULT_PERIODICITY_WINDOW_SECS,
10 DEFAULT_SAMPLE_RATE, DEFAULT_SMOOTH_SECS, PSYCHOACOUSTIC_V1, READINGS, Reads,
11 self_similarity::MAX_CELLS, spectrum,
12};
13
14pub fn help_text() -> String {
17 let bits = PSYCHOACOUSTIC_V1.precision_bits;
18 let store_gb = DEFAULT_STORE_BYTES >> 30;
19 let representations = representations();
20 let (min_frame, max_frame) = (spectrum::MIN_FRAME, spectrum::MAX_FRAME);
21 let rules = solve_rules();
22 let floor = solve::FLOOR_DB;
23 let (max_renders, step_tol, cost_tol) = (
24 solve::DEFAULT_MAX_RENDERS,
25 solve::DEFAULT_STEP_TOL,
26 solve::DEFAULT_COST_TOL,
27 );
28 let (passband, stopband) = (resample::PASSBAND, resample::STOPBAND);
29 let bound = format!("{} dB", 20.0 * resample::BOUND.log10());
30 let formats = crate::recording::READS
31 .map(|e| format!("`.{e}`"))
32 .join(", ");
33 let unreadable = crate::recording::UNREADABLE;
34 format!(
35 r#"USAGE:
36 sva-cli (render | analyze | solve | lint | trace | builtins | outline | new) [arguments]
37
38DESCRIPTION:
39 A composition is a directory of node files, each one closed-form expression in
40 `t` or `f` on one line, or a score placing notes in time. sva-cli reads that
41 composition and prints what it is and what it sounds like, as JSON on stdout.
42 `render`, `solve`, `lint` and `trace` read the current directory; `new` writes
43 beside it and `analyze` reads a file. `solve` fits variables to a reference.
44
45SCORES:
46 A score is a `.jsonl` file: JSON Lines, one object per line, sorted by `at`;
47 `@score/drop` reads `score/drop.jsonl`, and a ref never writes `.jsonl`. A
48 file with no extension opening with `{{` refuses with the rename. Line 1 is
49 its annotation, the doc comment's four fields, which `lint` holds it to:
50 {{"annotation": "Models: ... | Neglects: ... | IO: (t) -> amplitude | Tags: ..."}}
51 Every other line is one kind, told by its keys, and may add `"comment"`:
52 event "at", "node", "deg", "len" and the node's parameters: plays it
53 mark "at", "mark": a point `mark(name)` reads in this file's times
54 comment "at", "comment": a note to the reader at a time
55 move "at", "lane", "to", and "node", "len", "shape" where written
56 A score lowers to equations, the sum of its events in written order, each
57 its node shifted to its `at`; marks, comments and moves sound nothing.
58 at a time string, "0.25b" or "0.5s", or arithmetic over times, "2b +
59 1b/3" for a triplet; a bare number has no unit and refuses. A file
60 counts every time in bars or every one in seconds. "mark(verse) -
61 2b" is a position, "mark(drop) - mark(build)" a duration.
62 node an `@ref` from the score's own directory: a sound, an equation or
63 another score. On a line placing a score `len` crops it there and
64 nothing else binds; a `node` list mixing scores and sounds refuses
65 `len`.
66 deg a whole scale degree counted from 1, the tonic: sugar for "f0":
67 "deg(n)". In a seven-row scale 8 is the tonic an octave up, 0 the
68 seventh below and -6 the tonic an octave down. A chromatic note
69 binds "f0": "deg(7)*1st"; a line writing both refuses.
70 len a time as `at` takes, bound as the node's `len`.
71 other binds the node's parameter of that name: a number, or a constant in
72 a string, "0.03s", "-6db", "900hz", "A1", "0.8*0.9", "deg(5)/2".
73 Any field but `comment` may be a list. A line listing N items is N events,
74 moves or marks, item i of every list going to the i-th and a single value to
75 all; lists of different lengths refuse. A chord is a `deg` list on one `at`:
76 {{"at": "0b", "node": "@../instruments/keys", "deg": [1, 3, 5],
77 "vel": [0.8, 0.5, 0.6]}}
78 `deg(n)` is the tonic in `variables/key` times degree n's ratio in the scale
79 `variables/scale` names, read where the line sits, in a score or in any
80 equation. A scale is one the language names, as `minor` or `dorian` (`sva-cli
81 builtins` lists each with its cents), or a file of one row per degree in cents
82 above the tonic, ascending, the last the period, named by a ref: a file
83 `scales/custom` holding 200 400 500 700 900 1100 1200 is the major scale. A
84 value reading `t` or a `@ref`, a field the node does not read, a parameter it
85 reads that nothing binds, and a `deg` with no scale refuse before any audio,
86 each naming the score's file, line and column.
87
88 A lane is one value moving through the song: a node's parameter, a move
89 naming "node" and "lane": "cutoff", or a variable under `variables/`, a move
90 naming "lane" alone: "energy", "key", "scale", "bpm" (never "meter"). A move
91 with no `len` sets the lane at `at`; with "len" and "shape", `lin` or `exp`,
92 it ramps there from the value it holds:
93 {{"at": "24b", "node": "@../instruments/saw", "lane": "cutoff",
94 "to": "9khz", "len": "8b", "shape": "exp"}}
95 {{"at": "72b", "lane": "key", "to": "key*2st", "comment": "a tone up"}}
96 {{"at": "92b", "lane": "bpm", "to": "bpm*8/9", "len": "4b", "shape": "lin"}}
97 A `to` naming the lane means its home value, the node's default or the
98 variable's file, so it follows `--set`. A lane reaches every instance of its
99 node, from a score or an equation, inside a wrapper too, unless the line or
100 an invoking equation passes that parameter, which wins. Where it holds one
101 value over an instance's span it binds that number; where it moves there the
102 instance reads it at its song time, and where the engine refuses a moving
103 value, such as `sat`'s `drive`, the move refuses as `bad-lane`: pass a
104 constant, move the lane between notes, or close the sound's tail with a crop.
105 A score's times ride the tempo curve the `bpm` lane draws; `b` in an equation
106 stays a bar of `variables/bpm`. A song places its sections at marks:
107 {{"at": "0b", "mark": "intro"}}
108 {{"at": "mark(intro)", "node": "@intro"}}
109 {{"at": "mark(intro) + 8b", "mark": "verse"}}
110 {{"at": "mark(verse)", "node": "@verse"}}
111 `concat` and `repeat` place parts on the curve too: `repeat(@score/riff-1b, 4)`.
112 An equation reading a score at `t - d`, an echo or a send, hears the song as
113 it was d earlier: its lines keep their song time, and a move however many
114 reads reach it is one move.
115 A control score is one no sounding score places, read by an equation as a
116 signal: `@score/song(t) * (1 - @score/pump(t))` ducks the song under it.
117 `sva-cli outline @score/song` prints a song's marks, tempo, lanes, events and
118 size. Lint and render warn of a line passing a parameter whose lane ramps
119 under it (`line-overrides-lane`), and of a lane no instance reads
120 (`lane-reaches-nothing`).
121
122RESERVED VARIABLES:
123 Four nodes under `variables/` are reserved: a ref reads one like any node,
124 and a bar or a degree reads one by name:
125 variables/bpm a number, the beats per minute: 120
126 variables/meter beats per bar over the note: 4/4
127 variables/key the tonic `deg` counts from, a note or hertz: C3
128 variables/scale the scale `deg` counts in, a built-in's name: major, or a
129 ref to a scale file: @../scales/custom
130 A `b` literal or a file's `-2b` span reads bpm and meter; a `deg(n)` reads key
131 and scale. Before anything renders, each read is replaced by its value, by one
132 pass for all four: a bar becomes `n*(beats*60/bpm)` seconds and `deg(n)`
133 becomes `@variables/key` times degree n's ratio, the tonic still a ref. A
134 read with no value to replace it refuses as `bad-tempo` or `bad-scale`, as
135 does a variable holding no such value, such as bpm without meter, or a
136 scale name no built-in has, which refuses listing every built-in. A stream's
137 edit to a variable, or to the scale file it names, replaces every read of it
138 again. A node elsewhere named `bpm` is an ordinary node. Any other file under
139 `variables/`, as `variables/energy` holding 0.5, is a variable of the
140 composition's own: a ref reads it, `--set` sets it, and a score's moves move
141 it, as they move the reserved ones but `meter` (SCORES).
142
143RENDER:
144 sva-cli render '<expression>' --representation <list> [--until '<condition>']
145 [--bits <n>] [--rate <hz>] [--threads <n>] [--cache <path|none>]
146 [--set <name>=<text>]... [--confirm]
147
148 Renders one expression, in the grammar a node file's body uses, and prints one
149 reading per representation under `data.representations`. `@path` reads a node
150 from the current directory and `@/abs/path` one anywhere; there is no default
151 target. Every reading states what it was read with: `representation`, its
152 name, and `args`, every argument it takes as it used it, a plain number as a
153 number and the rest as `--representation` spells them (`pitch` alone prints
154 `"peaks": 16, "frame": "50ms"`), a default written out and a `spectrum`
155 frame left to fit the range as the frame it fitted. Null is an argument that
156 did not apply: a frame read exactly off the math, `modulation`'s unwritten
157 `bpm`, `samples`' `buffer` with no device. Its `args` written back as
158 `name(key=value, ...)`, nulls left out, ask the same reading. It states its
159 `source` (exact or measured), the conformance profile it ran under, and its
160 rate; `analyze` prints the same.
161
162 The target's own ref may read an interval: `@piano4([0, 2b], f0=C4,
163 vel=0.5, release=1s)`, where `release` is the instrument's own parameter. Its ends are the language's literals (`s`, `ms`, `b`,
164 `sp`, or a bare `0`); `[a, inf)` and `[a,)` leave the end open; the other
165 arguments are the node's own named ones. The start trims the output alone:
166 history before it is still computed, so a loop or a filter carries the state
167 it had there. With no interval the render starts at 0, earlier only where a
168 crop reaches before it, and its end is open. Inside an expression a window is
169 a `crop`. A target that samples its one ref read, `sample(@piano4([0, 2b],
170 f0=C4))`, reads that ref's interval and binds and the same node values.
171
172 `sample("<path>")` is a recording: a file relative to the directory sva-cli
173 runs in, or absolute, read as `analyze` reads one (ANALYZE); keep it outside
174 the composition, where it would be read as a node. It is the file's samples
175 at the render's rate, one component per channel as `join` makes them, silent
176 outside the file's span. At its own rate every sample passes unchanged. At
177 another a cos^16-windowed sinc reads it: a tone up to {passband} of the lower
178 rate reads within {bound} of itself, and one from {stopband} of it at most
179 {bound} of its level, a bound proven for the kernel that the label states
180 under `resampled` beside the rate it came from. The path is the language's
181 one string, in double quotes and only as `sample`'s one argument; one naming
182 no file refuses as `not_found`, and one `analyze` cannot read as
183 `{unreadable}`. A store keys a recording by its samples and rate, never its
184 path.
185
186 Every node is computed over its support met with what reads it, and nowhere
187 else: outside its support a node is exactly zero. A closed interval renders
188 exactly its length, nothing cut. An open one ends where the root's support
189 does: a crop's end, the exact underflow of an `exp`, a clamp holding a factor
190 at zero; or earlier, where a proven upper bound on the root's magnitude stays
191 under the profile's silence threshold (-120 dBFS) from a sample on. A root
192 with no such bound (a held sine, a loop) is never cut. The label's `pruned`
193 states the threshold as `db` and the root's cut, `from` the sample the render
194 is treated as silent from. A stream's term leaves `@notes` where its support
195 ends. An open interval over a root whose support never ends and is never cut
196 (a held sine, a physical solver) refuses as `render.no_end`. A short-time
197 transform reads its input whole, and refuses one with no end as
198 `engine.unbounded_extent`. A form in `f` with no dual in `t` has no samples
199 and refuses as its dual does, `cast.left_algebra`.
200
201 A render is a stream pulled to its end: a closed form, a short-time
202 transform and what either reads are computed over their whole extent first,
203 and every other node block by block. `--until '<condition>'` stops the render
204 at the first sample the condition holds at, or at the interval's end,
205 whichever is first. It is checked as each block is pulled, so no node driven
206 block by block runs past the block it holds in. A condition compares (`<`,
207 `<=`, `>`, `>=`) `t`, `envelope(t)` (the RMS over every channel of the
208 `envelope` representation's frame holding `t`, framed by its own `frame`
209 where one is asked) and literals, joined by `and`/`or`.
210
211 `--representation <list>` takes a comma list of readings and may repeat. Each
212 is a call in the language's own syntax, its options its named arguments:
213 `spectrum(peaks=8, frame=50ms)`, `ledger(depth=3, brief=1)`. A `frame`,
214 `window`, `cell` or `smooth` is a time as a score writes one, in seconds,
215 `50ms`, or in bars, `1b/8` (`1/8b` is one over eight bars), a bar lasting
216 what the render's tempo says; where none is known, as in `analyze`, bars
217 refuse as `tempo.invalid_bpm_meter`. An entry
218 followed by `=<path>` writes a file instead, listed under `written`:
219 `samples=<path>.wav` writes audio, encoded as `--bits` says; any other path
220 takes the reading's JSON, uncapped. A path that already holds a file refuses
221 unless `--confirm` is written. `samples=alsa:<device>` plays the samples on
222 that ALSA device (`alsa:default`, `alsa:hw:1`, `alsa:plughw:1,0`) as each
223 block is pulled, in real time, through `aplay` from alsa-utils; Linux only,
224 anywhere else it refuses as `destination.unsupported_platform`. It sends
225 32-bit floats, or integer PCM where `--bits` is 16 or under. A device that
226 cannot take the format refuses with aplay's reason, and one aplay says plays
227 another rate than the render's stops there as `destination.rate_inexact`.
228 `samples(buffer=<t>)` is how much the device holds ahead of what it plays.
229 Once it has played out, `data.played` states the device, its seconds, rate,
230 channels, format, the buffer asked and the one granted, and every underrun
231 aplay reported, with what it cannot see.
232 `lines`, `atoms`, `derivative`, `bindings`, `arguments`, and a closed form's
233 `spectrum`, `envelope` and `pitch`, read the expression and ignore the range.
234
235 A closed form's `spectrum` and `pitch` are its exact lines, so a term under a
236 crop or an envelope, which has a width and is no line, refuses them; so do
237 `spectrum(frame=)` and `envelope(frame=)`, as a closed form has no frames. To
238 measure any of them frame by frame, a reader samples the target's ref,
239 `sample(@x([0, 1s]))`, and an author writes `sample(...)` inside the node.
240 A closed form with no exact envelope, such as a crop of a decay, refuses
241 `envelope` with the reason rather than measuring it unasked; `sample(...)`
242 asks for the measured one. A measured `spectrum` is one spectrum: whole
243 windows across the range, averaged, the first at its first sample and the last
244 ending at its last, evenly at most half a window apart; a window is `frame`
245 rounded up to a power of two samples, at least {min_frame}, or without `frame`
246 the range's length so rounded, at most {max_frame}. `pitch` is framed, one entry
247 per `frame`; for the spectrum at one instant, read a range one frame long.
248 A measured `envelope` is framed too, and reads every channel: a frame's
249 `rms` is the root mean square of all its channels' samples together, its
250 `peak` the largest magnitude in any channel. `onsets` reads every channel
251 as well: a frame's flux is the sum of each channel's, and a strike's energy
252 that of all channels' samples together, so opposite phases cancel nothing.
253 Every other measured reading but `stereo` reads every channel in power, as
254 `envelope` reads a level: a spectrum's power per bin (`spectrum`, `pitch`,
255 the partials `dissonance`, `voice_leading`, `tonal_distance`,
256 `inharmonicity` and `self_similarity` pick), a formant frame's
257 autocorrelation and each auditory band's energy (`bands`, `periodicity`,
258 `modulation`, `self_similarity`) are each channel's averaged, so a channel
259 copied to two reads as itself; `crest` sets any channel's peak against the
260 RMS of all of them together. A closed form's exact readings read every
261 channel in power too: a partial's squared amplitude (`pitch`, `dissonance`,
262 `voice_leading`, `tonal_distance`, `inharmonicity`) is summed over the
263 channels at its frequency and divided by their count, and a spectrum of more
264 than one channel lists each frequency once, its amplitude the root of that
265 mean power, real, as channels share no phase.
266
267 `--bits <n>` is the precision every sample is written to, from 2 to 52: the
268 point where a series is truncated, and the encoding of a `.wav`, integer PCM
269 at n bits up to 16 and 32-bit float above. `--rate <hz>` (default
270 {DEFAULT_SAMPLE_RATE}) is the one rate a render samples at: the target is read
271 at its instants, and every node at the instants its reader asks for. A
272 closed form is exact at any instant, and so is a continuous loop, one
273 constant delay at a gain under 1 over a closed form such as
274 `x + 0.5*self(t - 17ms)`, which is its series. A filter, a discrete loop, a
275 solver and `rand` step at the rate asked for, where `1sp` is one step, so an
276 `sp` count or `self[idx(t) - 1]` means one sample at whatever rate is asked.
277 `rand` draws once per step, keyed by that step's index; a key between steps
278 reads the step nearest it, ties to even.
279
280 `--set <name>=<text>` renders as if the variable `variables/<name>` held
281 `<text>`, and writes no file: `--set key=G3` transposes every degree to G3,
282 `--set scale=dorian` changes mode (a scale file is a ref written from
283 `variables/`, as the file holds it: `--set scale=@../scales/custom`), `--set
284 bpm=140` moves every bar, `--set meter=3/4` regroups them and `--set
285 energy=0.8` sets a variable of the composition's own. It sets a lane's home
286 value: a move whose `to` names it follows. It may repeat, once per variable: a
287 variable set twice refuses as `render.set_twice`, a name with no file under
288 `variables/` that is no reserved one as `render.set_unreserved`, listing the
289 variables there, and the file's path, `variables/key=G3`, as
290 `render.set_path`, naming the short form.
291
292 `--threads <n>` (default: every core the machine reports) is the most
293 threads a render computes on; `1` computes every value in turn on one.
294 Values none of the others reads, and the blocks of a value that holds no
295 state, compute apart, to the same bits on any number of threads.
296
297 A discrete loop reads its own past only by index; `self(t - d)` in one
298 refuses as `type.discrete_self_at_time`, naming what made it discrete. A
299 read `@x(k*t - d)` steps `x` at `k` times the step, every input it reads,
300 `sp` and `idx` with it, and rounds `d` to the nearest sample of that step,
301 ties to even: shifts are rounded to the nearest sample so placements share
302 one cached value; timing is exact to half a sample, and the label's
303 `moved_s` states the most any read moved. A node is computed once
304 per step however many reads ask for it. A node that holds state read at a
305 time that moves refuses as `type.stateful_warp`, naming what
306 holds its state; `@x[idx(...)]` reads its nearest step instead, as `p[idx(...)]`
307 does a signal passed in as parameter `p`, and any `idx`, as
308 `idx(t - 5ms - 2ms*sin(2*pi*t))`, is read sample by sample. An edit to a
309 stream plays from the next sample on.
310
311 A render keeps in a store on disk its target and, under it, each node of
312 samples that holds state (a filter, a discrete loop or a solver in it) or
313 that two or more nodes read. Nothing else is kept. A closed form, `sample`
314 of one and `rand` have a value at any instant and are computed where read.
315 A node without state that one node alone reads, however many times, is
316 computed again by that reader from what the node itself reads: no other
317 reader could ask for it, and without state it restarts at any sample, where
318 a filter or a loop must run from its start. So a stage worth keeping is a
319 node of its own that ends in a filter or a loop, or one two nodes read. A
320 kept value is named by what it computes: never its file's name, directory,
321 comments, spacing, or the order its named arguments are written in, so a
322 renamed or moved copy, a constant lifted into a default, an expression split
323 into a file of its own or written back inline, and two addends swapped all
324 read one entry. The next render of anything that reads the same value reads
325 it back, bit for bit, and computes nothing under it. The store is on by
326 default, at `$XDG_CACHE_HOME/sva`, else `~/.cache/sva`; `--cache <path>`
327 moves it (a `/dev/shm` path keeps it in memory) and `--cache none` turns it
328 off. It holds at most {store_gb} GB, the least recently used values going
329 first, and a store another build of sva-cli wrote is emptied when opened. A
330 render stages what it computes beside the store as it goes, and the store
331 itself is written once, when the render ends, fails, or is stopped by SIGINT
332 or SIGTERM.
333
334 `ledger` prints one row per node under the target. A row's `share` is the
335 part of its reader's own energy that row accounts for, so one reader's refs
336 sum to 1; a ref no addend isolates, such as one factor of a product, and
337 every row under a silent reader, which has no energy to share out, print
338 `null`. A ref carrying a share reads as the addend it gave its reader, its
339 `rms`, `peak` and `clipped` too: in `2*@a`, `a`'s row is `a` doubled, and in
340 `@a - @a` it is silence. Each ref carrying a share is collapsed once on its
341 own, so a ledger costs one collapse per attributed ref beyond the render,
342 and `depth` bounds how many, counted in refs from one file to another. Every
343 row is computed whole over the range, however late a reader reads it.
344 `brief=1` keeps only the rows that clipped, `skim=1` drops the wider fields.
345
346 `arguments` renders nothing: for every instance under the target it prints
347 each builtin call's named arguments as the numbers the call was lowered
348 with, a solver's whole parameter set with `written: false` on each default
349 it filled in, and the operand each `min`/`max` `chosen` where a call folds
350 one to a number: in named arguments and a solver's, modal bank's or
351 `noise`'s positionals. One inside a filter's or cast's positional or
352 `rand`'s key or seed is not listed. `at` spans are bytes of the instance's
353 own body, as `sva-cli outline` counts them.
354
355REPRESENTATIONS:
356 Each one a `--representation` call, its arguments in parentheses, every
357 default under DEFAULTS. `exact` is read off the written math, `measured` off
358 samples.
359
360{representations}
361
362SOLVE:
363 sva-cli solve ('<cost>' | @fits/<name>) --vary <name>=<lo>..<hi>[/<step>][,log]...
364 [--max-renders <n>] [--step-tol <x>] [--cost-tol <x>]
365 [--set <name>=<text>]... [--rate <hz>] [--threads <n>]
366 [--cache <path|none>] [--write [--confirm]]
367
368 Fits variables under `variables/` so the targets a cost reads match a
369 reference, and prints the values it found. It edits no file unless `--write
370 --confirm` says so.
371
372 The cost is one expression typing to one number. Its leaves are
373 `repr.<reading>(<target>, <args>)`: `<reading>` spelled as `--representation`
374 spells it, `<target>` any render target with its interval as render writes
375 one, `<args>` the reading's own: `repr.pitch(@lead([0, 2s], f0=C4),
376 frame=50ms)`. Every leaf is measured off its target's samples, as `analyze`
377 reads a file, so a recording and a render are read alike. Two leaves of one
378 reading with one set of arguments subtract to a residual vector:
379{rules}
380 A level is floored at {floor} dB before it subtracts. A residual inside both
381 sides' stated bounds counts as zero; of these readings only `onsets` states
382 one. Then `norm(v)` (the root of the sum of squares), `sum(v)`, `max(v)`,
383 `abs(x)` (each value's magnitude), `dot([w1, w2, ...], v)` (one literal
384 weight per value), numbers, `+`, `-`, `*` and `/` make one number. A reading
385 left unsubtracted, a vector left unreduced and a leaf of any other reading
386 refuse before anything renders. In a node, `repr` and `load_json` are names
387 the language does not know and refuse as any such name does, with a help
388 naming `solve`.
389
390 `load_json("<file>", "<jq path>")` is a leaf read off a JSON file instead of
391 a render: plain JSON, so a `.json.gz` is unpacked first (`gunzip -k`). The
392 path is relative to the directory sva-cli runs in, or absolute, as
393 `sample("<path>")`'s is. The jq path is steps only, none of jq's filters:
394 `.name`, `[3]`, and a key that is not a plain name in single quotes,
395 `['envelope(frame=10ms)']`, so it needs no escaping in the cost's double
396 quotes: `load_json("../crop.json", ".readings['periodicity(window=5.08s)']")`.
397 A number there loads as a number and a list of numbers as a vector, both
398 usable as weights and in reducers. An object loads only as the other side of
399 a subtraction from a `repr.` leaf, and is a reading as `analyze` prints its
400 `data` (the object holding `interval` and `representations`): from a bench
401 download `.readings['<call>']`, from `analyze ref.wav --representation
402 bands=ref.json` the path `.data`. The leaf's reading reads it, and refuses
403 before the first render where its kind, its arguments, its rate, its
404 profile or its interval's length differ from what the leaf renders, or
405 where a list in it was cut short (`has_more: true`; a reading written to a
406 file is whole). Its arguments are its `args`, compared one by one with the
407 leaf's, each resolved at the solve's rate, so `frame=0.05` matches
408 `frame=50ms`; a mismatch names the argument and both values, and a reading
409 with no `representation` and `args` refuses. Each file is read once per
410 solve.
411
412 `@fits/<name>` reads the cost from the file `fits/<name>` of the
413 composition, written as a node is: an annotation line `; Models: ... |
414 Neglects: ... | IO: ... | Tags: ...`, then the cost, over as many lines as
415 it needs, every `;` comment dropped. Its quotes need no shell escaping, and
416 a cost that does not parse refuses at its line and byte in that file. No
417 node lives under `fits/`, and `lint` reads nothing there. `--vary` and the
418 other flags stay on the command line:
419 ; Models: the lead's bands against the bench crop | Neglects: pitch | IO: (cutoff) -> cost | Tags: fit
420 norm(repr.bands(@lead([0, 5.083291666666667s]))
421 - load_json("../crop.json", ".readings.bands"))
422
423 `--vary <name>=<lo>..<hi>[/<step>][,log]`, once per variable, names a file
424 `variables/<name>` holding one number and the box it moves in, both bounds in
425 one unit: none, `hz`, `khz`, `s`, `ms`, `db`, `ct` or `st`. The search starts
426 at the file's value read in that unit (`0.5` is `-6.02db` under `db` bounds).
427 `/<step>` is the first step and the grid values land on, counted from `<lo>`;
428 without it the first step is a quarter of the bounds' span. `,log` moves it on
429 a log scale, where `/<step>` is the ratio a step multiplies by, a bare number
430 above 1, as `/1.25`. A name with no file, a file holding no number in that
431 unit, and a variable no target of the cost reads each refuse before the first
432 render. `--set <name>=<text>` holds a variable at `<text>`, as render takes
433 it; a name both set and varied refuses.
434
435 The search is a deterministic compass search: from its best point it renders
436 each variable one step up and one down, in the order the `--vary`s are
437 written, and moves to the best of them that improves; where none does, every
438 step halves. A point already rendered is not rendered again. One render
439 renders every target of the cost once, through render's own path, and takes
440 every reading. A target no variable reaches, such as the reference, is read
441 back from the store on every render after its first, and `trace` shows it
442 computing no samples. A solve stops `converged` once every step is under
443 `--step-tol` of its first step or the cost is at most `--cost-tol`, `budget`
444 after `--max-renders` renders, and `cancelled` on SIGINT or SIGTERM. The
445 budget is a result, warned as `solve.budget`, not an error. Each render
446 prints one JSON line on stderr:
447 {{"render": 3, "values": {{"gain": "0.4"}}, "cost": 1.25, "best": 0.98}}
448
449 `--write` writes each fitted value to its `variables/` file, its comment line
450 kept. A file it would change refuses as `conflict` unless `--confirm` is
451 written too; that refusal still carries the fit, and `would_write`, nothing
452 written. A cancelled solve writes nothing.
453
454 Its `data`:
455 {{"cost_expression": "...", "values": {{"gain": {{"value": 0.5, "text": "0.5",
456 "unit": "", "start": "0.3", "on_bound": false}}}}, "cost": {{"total": 0.09,
457 "terms": {{"items": [{{"term": "norm(...)", "value": 0.08}}], ...}}}},
458 "renders": 49, "stopped_by": "converged", "resampled": {{"items": []}},
459 "trace": {{"items": [{{"render": 1, "values": {{"gain": {{"value": 0.3,
460 "text": "0.3"}}}}, "cost": 52.1, "computed_samples": {{"@tone": 44100,
461 "sample(\"../ref.wav\")": 44101}}}}]}}}}
462 `values` is the best point, each `text` as `--set` and a file take it; `cost`
463 its total and each top-level `+` or `-` term's signed share; `trace` every
464 render in order. A value `on_bound` sat on a bound, warned as
465 `solve.on_bound`: widen it, or the model lacks what the reference holds.
466 `--write` adds `written`, each path with its `from` and `to`. Stopped by a
467 signal, the answer is the error `cancelled`, exit 130, its `data` the best so
468 far.
469
470ANALYZE:
471 sva-cli analyze <file> --representation <list> [--confirm]
472
473 Runs the same readings over a whole recording at its own rate, never
474 resampled. It reads these formats:
475 {formats}
476 `.wav` is read exactly as written; the rest decode in sva-cli itself, an
477 MP4's encoder delay and padding trimmed as its edit list states (a bare
478 `.aac` keeps them). A lossy decode may differ slightly from a browser's
479 decoding of the same file, so its readings may differ slightly from a page's
480 readings of it. Any other format, or a file that does not decode, refuses
481 as `{unreadable}`, naming the formats it reads. Only the readings a
482 buffer answers alone apply, each listed under REPRESENTATIONS as `measured` or
483 `exact off a closed form, measured off samples`; the rest need the graph
484 behind it. Each reading states its `representation` and `args` as
485 `render`'s do, a time in seconds.
486
487LINT:
488 sva-cli lint ['<expression>'] [--format <json|text>]
489
490 Checks the current directory without rendering a sample. With no target it
491 checks every file under its own rules, and types every entry point, which
492 types every file it reaches. With a target, in render's grammar, it checks
493 the files that target reaches, and prints the interval a render of it reads.
494 Either refuses a type error as `render` does.
495
496 A score's annotation is its doc comment, under the same codes, and its lines
497 no body, so the length checks pass it by; a file of two or more lines that is
498 neither a score nor a scale refuses as `multiline-body` before any check
499 runs. Each song lint types prints its warnings beside the findings.
500
501 Every check prints one `data.diagnostics` item. `warning` exits 0, `error`
502 exits non-zero, so branch on the verdict and never on whether the array is
503 empty. No flag downgrades an error.
504
505 error missing-comment no `;` comment line
506 multiline-comment more than one
507 malformed-comment not four ` | ` fields, `Models:` `Neglects:`
508 `IO: <in> -> <out>` `Tags: <tag>[, <tag>...]`
509 long-comment-block a `;` block over 1000 characters, the line-1
510 doc comment's own run exempted
511 long-expression-body a body over 10000 characters, a backstop rather
512 than a complexity budget
513 literal-sample-rate a written rate where `sp` belongs
514 arity a builtin called without an argument it reads,
515 or with one it does not, as `rand(seed=k)`
516 warning key-is-not-a-pitch `variables/key` holds neither a note name nor
517 a number of hertz
518 parameter-has-no-default
519 a node reads a parameter no `name = value` line
520 binds, so a bare `@node` cannot play
521 support-never-ends a node's support, cut where its bound falls
522 under the silence threshold, has no end for a
523 bare render to stop at
524
525TRACE:
526 sva-cli trace <node|expression>
527
528 Prints one node's position without rendering audio: what it reads (`down`, one
529 hop), everything that reads it (`up`, transitively to an entry point), each
530 beside the expression doing the reading, the node that made it discrete, and
531 the feedback loop it sits in, if any. A score is shown as its line count, its
532 first and last `at` and the nodes it plays, never the sum it lowers to; every
533 entry point's song is lowered first, each at song time 0. An interval the
534 target reads is ignored.
535
536BUILTINS:
537 sva-cli builtins
538
539 Prints the whole callable and syntactic vocabulary: every builtin with its
540 arity and named arguments, each argument's `meaning`, `unit`, the model
541 `part` it sets and whether it `moves` with `t` (a filter's cutoff, q and gain;
542 every other named argument is one number, refused when it names none), each
543 positional's meaning where the model states one, unit suffixes, the note-name
544 grammar, the scales `variables/scale` may name with their cents, reserved
545 identifiers, special call shapes, and what has no operator at all.
546
547OUTLINE:
548 sva-cli outline <expression>
549
550 Prints the parse tree the engine builds from one expression, each node with
551 the byte `span` it was written in: calls by `name` with positional and named
552 `args`, operators by `op`, refs by `path` with their `binds`, refs and `self`
553 by how they `read`, `time` or `index`, literals by `value` and `unit`, names
554 by `name`. A node the parser supplies itself, the `0` of a prefix minus or the
555 `t` of a bare `@ref`, has `written: false`. Reads no composition. Score text,
556 JSON opening with `{{`, prints `score` instead of `outline`: one item per item
557 a line lists, with its `kind`, `line`, `span`, `at` with marks resolved, and
558 its `node`, `deg`, `args`, `mark` or `lane`, `to`, `len` and `shape`.
559
560 sva-cli outline @score/song
561
562 Run in a composition, outlines that score as the song a render of it is:
563 `marks` in bars and seconds, the `tempo` curve's pieces, each lane with its
564 home and every move resolved with its placement chain, how many instances it
565 reaches and how many see it move, and each placed event once, however many
566 reads reach it: its song time `at` (where it reads lanes, key and scale), when
567 it first `sounds` (later by the least, over the reads reaching it, of the
568 delays of the `t - d` reads of a score above it), its `len` in song time where
569 its line writes one, node, pitch in hertz and where each parameter comes from
570 (`line`, `lane` or `default`), and the `size`: placed events and the engine's
571 instances beside its limit.
572
573NEW:
574 sva-cli new <name> [--idempotency-key <key>]
575
576 Writes a starter composition at ./<name>, and refuses if that directory
577 exists. `--idempotency-key <key>` records the key beside the composition, so a
578 retry under the same key succeeds identically while the tree still holds what
579 was written. Any other key, or an edited tree, refuses.
580
581EXAMPLES:
582 sva-cli new song1 && cd song1
583 sva-cli render '@master' --representation samples=/tmp/song1.wav
584 sva-cli render '@master([0, 8b])' --representation 'ledger(depth=2),loudness'
585 sva-cli render '@voice/note([0, inf), f0=C4, len=2s)' \
586 --until 'envelope(t) < -60db and t > 1s' --representation samples=/tmp/note.wav
587 sva-cli render 'sample(@chord/home)' --representation 'spectrum(peaks=8)' --rate 48000
588 sva-cli render 'sample(@voice/note([0, 2s], f0=C4))' --representation pitch
589 sva-cli render '@master' --set key=G3 --set bpm=140 --set scale=dorian \
590 --representation samples=/tmp/song1-g.wav
591 sva-cli render '@master' \
592 --representation 'samples(buffer=100ms)=alsa:default,loudness'
593 sva-cli solve 'norm(repr.bands(@master([0, 4s])) - repr.bands(sample("../ref.wav")))
594 + 0.5*norm(repr.loudness(@master([0, 4s])) - repr.loudness(sample("../ref.wav")))' \
595 --vary gain=0..2/0.25 --vary cutoff=200hz..8000hz/1.25,log --rate 48000
596 sva-cli solve 'sum(abs(repr.onsets(@score/song([0, 8s]))
597 - repr.onsets(sample("/tmp/take.wav"))))' \
598 --vary bpm=100..140/2 --set key=D3 --max-renders 60 --write --confirm
599 sva-cli solve 'norm(repr.pitch(@lead([0, 2s])) - load_json("../ref.json", ".data"))' \
600 --vary detune=-50ct..50ct/10ct
601 sva-cli solve @fits/lead --vary cutoff=200hz..8000hz/1.25,log --rate 48000
602 sva-cli lint
603 sva-cli lint '@master'
604 sva-cli trace score/phrase
605 sva-cli outline @score/phrase
606 sva-cli builtins
607
608OUTPUT:
609 {{"status": "success", "data": {{"target": "@master([0, 8b])", "sample_rate":
610 {DEFAULT_SAMPLE_RATE}, "bits": {bits}, "profile": "psychoacoustic-v1",
611 "interval": {{"start_secs":
612 0, "end_secs": 16}}, "label": {{...}}, "written": {{"items": [...]}},
613 "representations": {{"ledger": {{...}}}}, "diagnostics": {{"items": []}}}},
614 "meta": {{"request_id": "req_...", "timestamp": 1700000000}}}}
615
616 `interval` is null where no reading read samples. An error adds "error":
617 {{"code", "message", "details": {{"count", "codes"}}}}. Success or error,
618 every response carries every finding in full at "data": {{"diagnostics":
619 {{"items": [{{"code", "severity", "message", "location", "help"}}],
620 "pagination": {{"count", "has_more", "next_cursor"}}}}}}, empty where it found
621 none.
622
623 Every collection carries that same {{items, pagination}} pair. `count` is the
624 whole reading's, `has_more` says `items` holds less than that, and
625 `next_cursor` is the interval start, as `0.5s`, the rest is read from. A
626 reading written to a file is written whole, uncapped. This page is an
627 envelope of its own, at "data": {{"help"}}.
628
629DEFAULTS:
630 --rate <hz> the one rate a render samples at, and `1sp`.
631 Default {DEFAULT_SAMPLE_RATE}.
632 --bits <n> the precision every sample is written to. Default {bits},
633 the `psychoacoustic-v1` profile's own.
634 ledger(depth=<n>) how deep below its target a `ledger` walks.
635 Default {DEFAULT_LEDGER_DEPTH}.
636 (peaks=<n>) peaks a `spectrum` keeps, notes a `pitch`, formants a
637 `formants`, stripes and novelty peaks a `self_similarity`
638 lists per view. Default {DEFAULT_MAX_PEAKS}.
639 alias(oversample=<n>) the multiple `alias` re-renders at to hear what folded.
640 Default {DEFAULT_OVERSAMPLE}.
641 (frame=<t>) the step a framed reading advances by, in seconds.
642 Default {DEFAULT_FRAME_SECS}, except `spectrum`, which sizes
643 its own transform to the range unless it is set.
644 (floor=<db>) the amplitude, as `-40db`, under which a harmonic reading
645 or `self_similarity` drops a partial, and under which
646 `self_similarity` counts a band level as zero.
647 Default {DEFAULT_FLOOR_DB}db.
648 voice_leading(norm=<l1|l2>) how a voice leading's moves add up. Default l1.
649 dissonance(pairs=<n>) the most partial pairs one frame sums before it refuses.
650 Default {DEFAULT_MAX_PAIRS}.
651 periodicity(window=<t>) the span of onset strength one window autocorrelates,
652 in seconds, at least twice the shortest period it reads;
653 it reads periods up to half its span.
654 Default {DEFAULT_PERIODICITY_WINDOW_SECS}.
655 periodicity(curve=<0|1>) `1` adds every window's height at every lag.
656 Default 0.
657 modulation(window=<t>) the span of band envelope one window transforms, in
658 seconds, at least twice the fastest rate's period; it
659 reads rates down to two periods per span.
660 Default {DEFAULT_MODULATION_WINDOW_SECS}.
661 modulation(bpm=<n>) a tempo, as `140`, adding each rate's period in beats.
662 Default none.
663 self_similarity(cell=<t>) the side of one matrix cell, in seconds, a whole
664 number of frames holding a sample of the band
665 envelopes; the target may span at most
666 {MAX_CELLS} cells. Default {DEFAULT_CELL_SECS}.
667 self_similarity(smooth=<t>) the length each diagonal is averaged over
668 before stripes are picked, in seconds.
669 Default {DEFAULT_SMOOTH_SECS}.
670 self_similarity(window=<t>) the whole width of the novelty kernel, in
671 seconds, at most {MAX_CELLS} cells a side.
672 Default {DEFAULT_NOVELTY_WINDOW_SECS}.
673 self_similarity(curve=<0|1>) `1` adds every cell's chroma and band levels and
674 each view's matrix. Default 0.
675 bindings(node=@<path>) the instance whose bindings are read; required. An
676 instance is named exactly as the render spells it; any
677 other spelling refuses, listing its file's instances.
678 samples(buffer=<t>) how much an `alsa:` device holds ahead of what it plays,
679 in seconds. Default {DEFAULT_BUFFER_SECS}; only a device
680 has one.
681 ledger(brief, skim) `0` unless set `1`.
682 --format <json|text> how `lint` prints its findings: the envelope, or one
683 terminal line each, colored where stdout is a terminal.
684 The same objects either way. Default json.
685 --cache <path|none> where the store lives. Default `$XDG_CACHE_HOME/sva`, else
686 `~/.cache/sva`, holding at most {store_gb} GB; `none` keeps
687 no store. A store that fails fails no render: it answers
688 as without one, warned why in `diagnostics`.
689 --set <name>=<text> a variable read as `<text>` for this render, or held for
690 a solve. Default none: each is read off its file.
691 --max-renders <n> the most renders a solve takes. Default {max_renders}.
692 --step-tol <x> the fraction of each variable's first step under which a
693 solve's search stops. Default {step_tol}.
694 --cost-tol <x> the cost at or under which a solve stops. Default {cost_tol}.
695 --write a solve writes its fitted values to their `variables/`
696 files, with `--confirm`. Default off.
697 --confirm replaces a destination that already holds a file. Without
698 it a path already taken refuses as `conflict` and nothing
699 is written.
700
701ENVIRONMENT:
702 SVA_LOG=debug `render` logs, once it ends, what it asked of each value
703 to stderr; stdout and the samples are unchanged. Any
704 other value, or none, logs nothing. One line per second
705 of output, then one per node and a total:
706 sva-cache pass hit=0 miss=190 ... reused=0 cum-hit=0.0%
707 sva-cache t=1.022s hit=1 miss=2 ... cum-hit=0.5%
708 sva-cache node hit=1 miss=0 ... reused=3 <node>
709 sva-cache total hit=1 miss=198 ... hit-rate=0.5% ...
710 `pass` is what was looked up before the first block.
711 Each value is looked up once: a `hit` was answered by
712 the store, `prefix` found a stored run up to a switch,
713 `miss` computed it, `new` held it in memory, where the
714 store takes what it keeps from. Each read of a value
715 past its first is `reused`.
716
717EXIT CODES:
718 0 success (error.code absent)
719 1 internal_error (a destination could not be written)
720 3 validation_error (bad arguments, a composition that failed to parse, or a
721 render the engine refused)
722 4 conflict (a name `new` would overwrite, a destination already holding a
723 file, or a variable file `solve --write` would change, without `--confirm`)
724 24 not_found (a `.wav` file, a `sample("<path>")` naming no file, or a node
725 this composition does not define)
726 130 cancelled (a solve stopped by SIGINT or SIGTERM, `data` the best so far)
727
728VERB ALIASES:
729 validate = lint, list = builtins, create = new, show = trace, from the `cli`
730 standard's own verb list. `render` and `analyze` take a reading, which that
731 list has no word for, so they keep their own names.
732
733SEE ALSO:
734 sva-cli --version Show version information"#
735 )
736}
737
738fn solve_rules() -> String {
740 let mut out = Vec::new();
741 for (name, rule) in solve::RULES {
742 let mut line = format!(" {name:<10}");
743 if name.len() >= 10 {
744 out.push(line);
745 line = " ".repeat(14);
746 }
747 for word in rule.split(' ') {
748 if line.len() + 1 + word.len() > 80 {
749 out.push(line);
750 line = " ".repeat(15);
751 } else {
752 line.push(' ');
753 }
754 line.push_str(word);
755 }
756 out.push(line);
757 }
758 out.join("\n")
759}
760
761fn representations() -> String {
763 READINGS
764 .iter()
765 .map(|row| {
766 let keys: Vec<&str> = row.takes.iter().map(|arg| arg.key()).collect();
767 let call = match keys[..] {
768 [] => row.name.to_string(),
769 _ => format!("{}({})", row.name, keys.join(", ")),
770 };
771 let source = match row.reads {
772 Reads::ClosedForm | Reads::Structure => "exact",
773 Reads::Either => "exact off a closed form, measured off samples",
774 Reads::Samples => "measured",
775 Reads::Graph => "measured, and the graph behind the samples",
776 };
777 format!(" {call:<26} {source}\n {}", row.help)
778 })
779 .collect::<Vec<_>>()
780 .join("\n")
781}