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