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