Introduction
sound-to-patch gives a producer a starting point in a synth for one sound. Vital and Serum 2 are the target synths, and Vital comes first. It has two paths:
- Audio path. One stem of one instrument and its BPM go in. The notes as
notes.midand a preset for the target synth come out (preset.vitalorpreset.SerumPreset), with anexplanation.mdthat says how the preset works and which controls to turn next. - Text path. The producer describes the sound in their own words. An agent turns the description into a preset and its explanation, and into notes when the description asks for them.
The project is at an early release. sound-to-patch analyze reads a stem
and writes a report of its tempo, notes, level, spectrum, band edge and
stereo image with a spectrogram, as the analysis guide
shows. sound-to-patch preset inspect prints the header and the decoded
settings of a Vital preset, and sound-to-patch preset apply writes one
from an edit list, as the preset guide shows. The layer
estimators and the text path arrive in later releases.
The API documentation covers the library. The development chapters describe how the project is tested and released.
Vital and Serum 2 are trademarks of their owners. This project is not affiliated with or endorsed by them.
Analysing a stem
sound-to-patch analyze reads one stem and writes a report of its tempo,
notes, level, spectrum, band edge, stereo image, glides, pitch drops,
vibrato, amplitude envelope, sidechain pump and voices, with a spectrogram image
and the final notes.mid. The report is what an agent reads
before it writes an edit list for the preset commands. The
CLI reference lists every flag, and the
layer report page every field.
Run it
Export one instrument as a WAV, AIFF, FLAC, MP3 or AAC file at 8000 to 192000 Hz and pass its tempo. A file at another rate is refused with its rate and the range; 44.1 or 48 kHz is the usual export. A file of three or more channels is read from its first two, with a warning that names the channel count:
sound-to-patch analyze bass.wav --bpm 120
tempo: 120 BPM as given; the stem holds 7 whole beats at 116.51 BPM, the slowest tempo of the search, and refining needs 8
detected range: MIDI 30 to 36, from the f0 track
detected voicing: monophonic, chord share 0.00
register window: 20 dB
cleanup voicing: monophonic, as detected
settings: detected, MIDI 30 to 36, minimum note 5 frames (58.0 ms), melodia on
decoded: 16 notes
octave check: 16 notes, 0 moved down an octave, 0 flagged, 0 unchecked
ghost merge: 16 notes
attack split: 16 notes
split join: 16 notes
quiet-note guard: 16 notes, 0 dropped below 40 dB
glides: 0, 0 too short
pitch drops: 0 of 15 low strikes
drop skips: 1 no onset, 15 no fall
vibrato: 0 of 0 steady spans
envelope: 16 of 16 notes read, 16 pooled, 16 with a note-off knee
envelope: attack 19 ms (upper bound), hold 0 ms, decay none, sustain 0.0 dB
tail: release none, rt60 none, weight 0.01, 1 of 5 reverb votes
pump: too few sustained beats
voices: mono, up to 1, mean 1.00; 15 joins, 0 legato
wrote bass/layers.json
wrote bass/spectrogram.png
wrote bass/notes.mid
The first line is the tempo: --bpm refined from the stem’s onsets, as
notes does it; --bpm-exact keeps --bpm as it is. This
4 s stem is too short to refine it. The next lines are the notes step of
notes, which analyze runs once to know the stem’s notes;
--profile overrides its settings as there.
The four lines after them are the pitch layer: this
saw repeats one note with no glide or vibrato, and none of its 15 low
strikes falls in pitch. Each line lists what it found, with its numbers;
drop skips counts the low notes with no drop by the gate that stopped
each: no onset, settling, quiet, no strike, weak fundamental, no fall, tau
on bound, residual or off note.
The next four lines are the envelope layer.
The first counts the notes read, those pooled and those with a note-off
knee, and any skipped. The second gives the pooled medians: this saw
starts each note at full level, under 1.5 periods of 55 Hz, so the attack
is an upper bound; it holds its level, so it has no decay and a sustain
of 0 dB. The third gives the release, the RT60 and weight of any tail, and how
many of five signs say the tail is reverb. The last is the pump: no beat here lies inside one held note from
50 ms before it, so none counts.
The last line before the files is the voices layer:
one voice at a time, and each of the 15 joins falls to silence, so none reads
legato.
The files go into a folder named after the stem, next to it. notes.mid
is the final note list, below; it replaces the
first file of notes. --out picks
another folder. A warning about a packet that did not decode goes to
standard error; the stem keeps its timing, with silence in its place.
The final notes.mid
analyze rewrites the note list with the layers and writes it as
notes.mid, one track on channel 1 at the used tempo, 480 ticks per
quarter note. The preset carries what the routes model, so the file holds
only notes.
Every rule below reads one voicing (owner decision, 2026-11-05):
| Line | When | Rules |
|---|---|---|
| Mono | The voices layer reads at most one voice at a time, or counts none, and the notes step ran without --profile pad | Glide joins, next-note cuts, one note per tick, bends; a level span stops at the next later onset |
| Polyphonic | The voices layer reads two voices or more at once, or the notes step ran with --profile pad | Pad ends; a level span runs past the next onset |
Both lines end the other notes at their knees or 50 ms before their list
ends. The voicing of the notes step, from its chord share, sets only the
notes step’s settings and the pitch layer. So a stem of chords whose
chord share stays under 0.68 is still a polyphonic line here. Given
notes take no profile, so --notes leaves the voicing to the voices
layer. A layer that counts no voices leaves the line mono, because the
mono rules only join and cut notes.
- Each note ends at its note-off knee, so the release lives in the amp
envelope and a render does not play it twice. A note without a knee,
such as a pluck that decays at one rate, ends 50 ms before its end in the
list; so does a note the envelope layer could not read, marked
unread. - A note of a polyphonic line ends where its level falls 20 dB below its
plateau, marked
pad: the plateau of its one harmonic, or of the full band when no harmonic of it is free, as in an octave stack. The search stops at the list end: the note-off is the gate, and the release past it belongs to the preset’s amp envelope (owner decision, 2026-11-05). A note whose level falls short of 20 dB by then ends by the other rules. - No note ends after its end in the list, except at a glide join. A knee,
no-knee or pad cut never makes a note shorter than the minimum the BPM
rule of the decoder gives at the tempo, 58 to 128 ms. The decoder drops
notes that short on mono lines, and on polyphonic and pad lines it drops
notes of 128 ms or less, whatever the tempo; the cleanup and a given
--notesfile can still hold shorter ones. A note the list gives shorter keeps its length. In a mono line a note ends where the next starts, even when that is shorter than the minimum. - A glide becomes two overlapping notes on its two pitches. The later note starts at the glide’s start, or at its own onset when the glide starts after it, and the earlier one ends 5 ms later, rounded up to whole ticks. In a mono line no other notes overlap. Of notes that start on one tick, one stays and the report counts the others: a note the transcriber does not flag as an octave guess before one it flags, then the longer, then the lower key. Their levels cannot rank them: both read the same mix from the same start.
- A pitch drop becomes one note at its settled pitch, and a note under a vibrato takes the key of the vibrato’s mean pitch. Only the glides, drops and vibratos the report trusts above low count.
- Velocity follows the full-band level of each note when the spread of
those levels exceeds 3 dB: the loudest note gets 127, and a note
ddB under it127 * 10^(-d / 40), at least 1. Otherwise every note gets 100. The full-band level is the peak of the one-period RMS of the whole mix from the onset to the note-off knee, so two notes compare even when they read different harmonics. On a chord it is the level of the mix, and tones of different levels do not tell apart. Each tone reads the mix with its own one-period window: the tones of a C3, E3 and G3 chord read within 0.65 dB of each other, and in an octave stack the lower tones read up to about 3 dB less. A stagger of the onsets moves no tone’s level by 1 dB [synthetic]. A note with no frame to read reads its written span, which on a mono line stops at the next later onset; with no frame there either it gets 100. - Pitch motion that no route models becomes pitch bends: a note of a mono
line whose pitch stays 50 cents or more off its key, on one side, for at
least the pitch track’s reach (a quarter of its frame plus one period,
26 ms at 392 Hz to 49 ms at 39 Hz), outside every glide, drop and
vibrato. A shorter excursion fits inside what one track frame reads, so
jitter on a low note or a hit inside a merged note writes no bend
(owner decision, 2026-11-05). The pitch track reads a little of the note
before at each note change, and while two notes sound it follows only
one of them, so those frames count for no bend. Bends are clamped to 2 semitones, so set the synth’s
bend range to 2 semitones.
renderreads no bend.
The notes group of layers.json lists each written note with the rule
that set its end, its level and the transcriber’s octave flag.
Limits
- A bend of a semitone or more becomes its own note at the transcriber, so it plays as a note change, not a bend. Of generated tones with a held bend, every +100 and +200 cent bend (120 of 120) split into its own note; of the +60 cent bends, 41 of 60 wrote bends, 13 split, 5 stayed flat and 1 had no note [synthetic].
- A slow ramp that passes 50 cents only in its last frame writes no bend, because the offset never persists for the track’s reach. Of the four generated bends of the tally set, two lost their bends this way: a rise of 0.5 semitones over 1.5 s, whose note reads +5 to +55 cents, and a 2-semitone ramp over 1 s that the transcriber split into notes 57, 58 and 59, where note 58 reads -35 to +55 cents [synthetic].
- Of the tally’s signals with motion that no route models, 7 runs lost bends to the persistence rule: 2 bends, 2 trills and 3 kick drops [synthetic]. The rule also took bends from 51 runs of glides, drops and vibratos that the routes model.
- Steady lines still bend in 27 of 502 generated runs without pitch motion, 3,194 events, against 156 runs and 28,807 events without the persistence rule [synthetic].
- A pad note whose level falls 20 dB only after its list end keeps the knee, no-knee or minimum end. Against the gate note-offs of the generated tally’s 1,776 transcribed notes, a search past the list end raised the error from 239 to 346 ms MAE, and gave 177 ends more than 1 s late [synthetic].
- The transcriber’s notes of a mono line can overlap, so the voices layer reads the line as polyphonic and its notes get pad ends but no glide joins or bends: 41 of 133 transcribed runs of generated 808 and bass lines in one tally set [synthetic].
- The pitch layer still reads the notes step’s
voicing, from its chord share, not the voices layer. On a stem of chords
whose chord share stays under 0.68 it reads glides, drops and vibratos
as on one line, while
notes.midtreats the line as polyphonic and writes no glide join or bend. #327 aligns the two.
Give the notes
When the MIDI file that played the stem exists, pass it:
sound-to-patch analyze bass.wav --bpm 120 --notes bass.mid
The file’s notes replace the notes step, and the transcription model does
not run. The f0 track still reads the stem for the pitch layer, unless the
notes play chords.
The notes set the frame length: at 32 to 48 kHz, 8192 samples for a bass
below 100 Hz and 4096 above, scaled with the rate at other rates, so
96 kHz takes 16384 and 8192. They also let the report fill centroid_over_f0 and the stereo phase
ipd_deg. The file’s tempo must match --bpm or the refined tempo, so a
file that notes wrote at the refined tempo reads too. Notes on MIDI
channel 10, the drum channel, are left out, because their keys name drums,
not pitches.
Read the report
These excerpts come from the run above: a generated 4 s stereo tone at 48 kHz, a band-limited 55 Hz sawtooth at amplitude 0.25 that sounds for 0.2 s of every 0.25 s, with the right channel at 0.8 of the left:
{
"schema": "layers/1",
"source": "bass.wav",
"source_sha256": "<64 hex digits>",
"synth": "vital",
...
}
source names the stem, and source_sha256 is the checksum of its file.
A second stem that would write into the same folder, such as bass.aiff
next to bass.wav, or the bass.wav of another song in a shared --out
folder, stops with an error unless you pass --out or --force. A new
export of the same bass.wav has other bytes, so it stops the same way,
and --force replaces its report. A folder that holds a
spectrogram.png with no report, or a layers.json this tool did not
write, stops the run too.
"centroid_hz": {
"median": 3596.5198523563367,
"iqr": 22.42862967814881,
"confidence": {
"level": "high",
"reasons": [{"code": "active_frames", "value": 94}],
"spread": 22.42862967814881,
"estimate": true
}
}
A statistic over frames gives the median and the interquartile range of the
frames with sound, and its confidence names the evidence: here 94 active
frames of 8192 samples, the frame length that a note list with a median f0
below 100 Hz picks at 48 kHz. A clip with fewer than 16 active frames gets no value
and the reason too_few_frames.
"band_edge_hz": {
"value": 24000.0,
"confidence": {
"level": "low",
"reasons": [
{"code": "active_frames", "value": 94},
{"code": "band_edge_drop", "value": 2.6500554517088695},
{"code": "no_band_edge", "value": true}
],
"spread": 5.859375,
"estimate": true
}
}
The sawtooth has harmonics up to the Nyquist frequency and no steep cut:
band_edge_drop says the steepest fall was under 3 dB. No bin rises the
fallback margin above the floor either, so the band edge is the Nyquist
frequency at low confidence, with the reason no_band_edge.
The conventions group says how each number was measured, under the same
key as the number.
Read the spectrogram
spectrogram.png shows the busiest 10 s of the stem in two panels: the
whole range from 20 Hz to 20 kHz, and 20 to 500 Hz with finer frequency
steps for the bass. Bright is loud; the colour bar spans 80 dB below the
loudest point of the stem. The image shows shape; the numbers are in the
report.
Transcribing a stem
sound-to-patch notes reads one stem and writes its notes as a MIDI file,
notes.mid, with the tempo you give. It finds the pitch range and whether
the part plays one note at a time or chords, decodes the notes, and cleans
them up. The CLI reference lists every flag,
and the pipeline page gives each
step with its settings and limits.
Run it
Export one instrument as a WAV, AIFF, FLAC, MP3 or AAC file and pass its tempo:
sound-to-patch notes bass.wav --bpm 120
detected range: MIDI 30 to 48, from the f0 track
detected voicing: monophonic, chord share 0.12
register window: 20 dB
cleanup voicing: monophonic, as detected
settings: detected, MIDI 30 to 48, minimum note 5 frames (58.0 ms), melodia on
decoded: 24 notes
octave check: 24 notes, 4 moved down an octave, 4 flagged, 0 unchecked
ghost merge: 20 notes
attack split: 20 notes
split join: 20 notes
quiet-note guard: 20 notes, 0 dropped below 40 dB
wrote bass/notes.mid
This run read a generated bass line, a mono 16-bit WAV at 44.1 kHz, 4.5 s long. It holds 16 notes on the eighth notes of 120 BPM from 0.5 s, each 0.2 s long with no fade: MIDI 33, 36, 40, 43, 45, 43, 40 and 36, twice. Each note is a band-limited sawtooth, the sum of for every harmonic below 22.05 kHz, at amplitude 0.3. The file holds all 16 notes at the right pitch, each within 21 ms of its onset. The 4 extra notes are short fragments of the attack, 70 to 106 ms before a true note of the same key.
The file goes into a folder named after the stem, next to it, the same
folder analyze writes into. --out picks another folder. Only
notes.mid is replaced; nothing else in the folder changes. The file
names its stem in its track name, so a run of another stem into the same
folder stops unless you pass --out or --force. After analyze, the
folder’s layers.json describes the final notes.mid, so notes stops
there too: --out writes the first list elsewhere, and --force
replaces the final notes and removes the report that described them.
Each line names one step:
detected rangeanddetected voicing: the pitch range the stem plays in and whether it plays chords, read from the stem itself.register window: the f0 frames that set the range lie within this many decibels of the stem’s loud passages, so quiet bleed does not widen it.cleanup voicing: whether the cleanup treats the part as one note at a time, which runs the octave check, or as chords, and whether that came from detection or from--profile.settings: what the decoder used, the detected settings or those of--profile, with the shortest note it keeps.decodedtoquiet-note guard: the note count after each cleanup step.moved down an octavecounts octave copies above low notes,flaggedthe notes whose pitch the f0 track disputes, and the guard drops notes that sound only far below the stem’s loudest passage.
Tempo
--bpm sets the tempo of the file and the shortest note the decoder
keeps: half a sixteenth note, between 58 ms and 127.7 ms. A tempo from a
file name is often close but not exact, so the command refines it from the
stem’s onsets, from 2.91 % below to 3.09 % above it, and prints the result
first:
tempo: 119.62 BPM, refined from --bpm 120 (peak ratio 8.05)
This line comes from a generated 16 s bass line at 119.62 BPM, eighth
notes of A1, C2, E2 and G1, passed as --bpm 120. The file then plays at
119.62 BPM. Set your DAW project to the printed tempo, or the notes drift
off the grid over the song. When the onsets do not settle the tempo, as in
a pad that changes chord once a bar, the line says why and the file keeps
--bpm. --bpm-exact uses --bpm as it is.
The tempo section gives the method.
Profiles
Detection fits most stems. When it misreads one, --profile sets the
range and the minimum length for a kind of stem:
| Profile | Range | Minimum length | Cleanup |
|---|---|---|---|
bass | B0 to F#4 (MIDI 23 to 66) | from the BPM | one note at a time |
lead | C2 to B6 (MIDI 36 to 95), plucks too | from the BPM | one note at a time |
pad | C2 to B6 (MIDI 36 to 95), chords too | 127.7 ms | chords: no octave check |
Detection still runs and prints its findings, so you can compare them
with the profile. notes decides voicing once for the whole stem. A
stem that mixes a chord part and a line gets a chord share between the
two and can read as one note at a time. The cleanup then runs the octave
check on its chords. When detected voicing reads monophonic on a stem
with held chords in it, pass --profile pad. Three more cases need a
profile: bleed from another instrument louder than -20 dB in a passage
where the part rests, a line with held double stops (--profile pad), and a strummed part
(--profile lead), where the pad settings lose notes.
Check the file
Open notes.mid in a piano roll at the same tempo. It holds one track on
MIDI channel 1 with a tempo event and 4/4 at the start; every note has
velocity 100. Then check what the model cannot get right:
-
No note below A0 (27.5 Hz): the model’s lowest key.
-
Note ends come late: a note often lasts until the next one starts.
-
Octave errors at 110 Hz and below. The cleanup moves a weak octave copy that sounds inside its true note, but a lone note an octave off stays and is only flagged. A chord part keeps its octave copies: three generated triads give 10 notes for 9.
-
A repeated note can merge into the one before it when the model misses its onset and the stem has no clear attack before it. A clear attack is a fall of 19 dB or more and a rise back about as fast as a step. Over a steady floor the fall must reach 20 dB below the note. The note before it must also have held for 60 ms and for 1.5 times the gap. These repeats merge this way:
- a legato repeat;
- an attack slower than about 10 ms;
- a repeat after a note shorter than about 60 ms;
- a gap longer than about two thirds of the note before it, such as staccato eighths at half length;
- a gap that a tail or reverb fills.
Most natural attacks are slower than 10 ms: a bowed, blown or sung re-attack rises like a tremolo, and the cleanup reads only the level. The model can read a low repeat with a short gap as part of one note: a 55 Hz saw gated with 30 ms gaps decodes as 9 notes for 16. From A1 (55 Hz) up the cleanup splits such a note at a clear attack inside it, and the saw gives its 16. Below A1 the note stays whole, because there a fast sidechain duck looks like a gate.
-
A square LFO that cuts the level for 20 to 40 % of each cycle looks, by level, like a gated note with short gaps. The cleanup reads it as a gate and can split a held note into repeats: an A1 under a 6 Hz square off for 30 % of each cycle gives 24 notes, one a pulse, where the model’s onsets alone give 6. A vibrato under such a square splits the same way. Only a cue such as a phase reset at each repeat could tell the two apart. A square off half of each cycle holds only as long as it dips. No generated one split a held note further than the model’s onsets do, and no sine tremolo of 2 to 20 Hz did.
-
A held low note under a louder gated note of another key can split at that note’s gates when the model splits it there: an A1 20 dB under a gated A2 gives 12 notes. Below A2 the cleanup cannot tell the two keys apart by their harmonics.
Plan a manual pass in the piano roll. The pipeline page gives the scores on generated parts; real stems score lower.
When it fails
A stem that gives no note writes nothing and exits with code 1. The
message names the stem and suggests a profile. Only digital silence,
every sample exactly 0, is sure to give no note: the guard gate is
relative to the loudest passage, so a file of DC alone or of faint hiss
gives notes. A folder that
holds the report of another stem, or of a stem of the same name with other
content,
stops the run unless you pass --out or --force. A failed write leaves
the folder as it was.
Presets
The preset commands read a preset file and write a new one from an edit
list. An edit list names controls and their values, such as a filter cutoff
of 400 Hz. This page shows the steps for Vital. The
CLI reference lists every flag, and the
format page states what is known
about the file.
The commands on this page run in the test suite, so their output is the
output you get. They read bass.json and typo.json from
preset.in/.
Vital
Choose the synth
--synth vital picks Vital. You can leave it out: the extension of the
preset file picks the synth, and Vital is the default.
Write an edit list
A Vital preset holds about 800 controls. A modulation slot joins a source, such as an envelope, to a control it moves. Vital has 64 slots. This edit list turns on filter 1, picks its ladder model, sets the cutoff and the attack, and lets envelope 2 open the filter:
{
"synth": "vital",
"edits": [
{ "param": "filter_1_on", "value": true, "reason": "send the oscillators through filter 1" },
{ "menu": "filter_1_model", "choice": "Ladder", "reason": "a warm ladder low-pass" },
{ "param": "filter_1_cutoff", "value": "400 Hz", "reason": "a dark bass" },
{ "param": "env_1_attack", "value": "5 ms", "reason": "a quick attack" },
{
"route": { "source": "env_2", "dest": "filter_1_cutoff", "amount": "+24 st" },
"reason": "envelope 2 opens the filter on each note"
}
]
}
Each value carries its unit. A route amount is in semitones when its destination is a pitch or a cutoff, and in percent otherwise. Every edit needs a reason.
Apply it
$ sound-to-patch preset apply bass.json -o Bass.vital
wrote Bass.vital ("Bass") from built-in default
build 1.0.6, kept from the template
set filter_1_on = on, stored 1.0
set filter_1_model = Ladder, stored 2.0
set filter_1_cutoff = 400 Hz, stored 67.34995771509334
set env_1_attack = 0.005 s, stored 0.26591479484724945
set slot 1 = env_2 -> filter_1_cutoff, amount 0.1875
The command starts from the built-in template: every control at its default, our own sawtooth wavetables and an empty modulation matrix. A wavetable is a set of single-cycle waveforms that an oscillator plays. The built-in template is not Vital’s Init preset.
The preset takes its name from the output file, so name the file after the
preset. Vital also shows the file name. The command refuses an existing
output file unless you pass --force. It never writes over its edit list or
its template, and it refuses a path inside Vital’s own Factory folder.
A wrong unit stops the command before it writes anything. The message names the edit list, the edit, the cause and the fix:
$ sound-to-patch preset apply typo.json -o Typo.vital
? failed
error: typo.json: the edit list has 1 problem(s):
edit 1 (env_1_attack): env_1_attack: cannot read "5 Hz" as Seconds: unknown suffix "Hz". Write a number followed by one of: s, ms, such as "800 ms"
Read the result
preset inspect prints the header and the controls:
$ sound-to-patch preset apply bass.json -o Bass.vital --force
...
$ sound-to-patch preset inspect Bass.vital
build 1.0.6
name Bass
author sound-to-patch
style (none)
controls 772
unknown keys 0
...
env_1_attack: 0.26591479484724945
...
filter_1_cutoff: 67.34995771509334
...
filter_1_model: 2.0
...
filter_1_on: 1.0
...
The first line is the stamp, the Vital build the file claims. Vital
refuses a file whose stamp is newer than the app in its first two numbers.
Long lists show their first items; --full prints everything.
Start from your own preset
A preset you saved from Vital carries the stamp your app expects. Pass it
with --template, and the edits apply on top of it:
$ sound-to-patch preset apply bass.json -o Bass.vital --force
...
$ sound-to-patch preset apply bass.json --template Bass.vital -o Again.vital
...
The built-in template is stamped 1.0.6, the open-source build, which
Vital 1.0.7 loads. For a newer build, such as the early-access 1.6, start
from a preset saved by your own app.
Load it in Vital
- Copy the file into
~/Music/Vital/User/Presets, or use the menu item “Open External Preset”. - Load the preset in a fresh instance.
- Check that a control you edited shows its new value.
- Play a note and check that the sound differs from the default patch.
A file is untrusted until Vital has loaded it. Check the sound, not only the name: in a plugin host, Vital ignores a file it rejects and keeps the last sound without a dialog.
-
Results on Vital 1.0.7, the stable build:
Check Result A preset with the cutoff and attack edits of the README quick start loads; the attack shows 1 s and the note swells The same file stamped 1.5.5,1.6.0or1.7.0refused: the stamp is newer than the app The same file with keys of later builds loads Save it again from the app every control kept An unknown key, no name fields, a route from each source, a 100-point LFO shape, wavetables of 1 and 257 frames each loads with no dialog The format page lists what is still open.
-
The 1.0.7 macOS bundles are x86_64 only (
lipo -archs, 2026-10-26). A plugin host running natively on Apple silicon cannot load the plugin. On Apple silicon, macOS runs an x86_64 app under Rosetta. -
Presets saved by Vital 1.6 do not open in 1.5.
-
Factory presets, wavetables and samples cannot be shared. Do not share a preset that embeds them.
Rendering a preset
sound-to-patch render plays a MIDI file through a preset in the synth’s
own plug-in and writes the audio as a WAV file. It runs on macOS with
Vital. Serum 2 follows. The CLI reference
lists every flag.
What it needs
- Vital, installed as a VST3 plug-in in
/Library/Audio/Plug-Ins/VST3or in the same folder under your user’sLibrary.--pluginnames another bundle. sound-to-patch-host, the program that loads the plug-in. It ships in the release archive next tosound-to-patch, andrenderlooks for it there.SOUND_TO_PATCH_HOSTnames another copy.- Rosetta on Apple silicon. Vital 1.0.7 holds Intel code only, so the host
runs as an Intel program for it.
softwareupdate --install-rosettainstalls Rosetta.
The plug-in loads as a library, with no window, and the Vital app never starts.
Run it
Pass the preset, a MIDI file with its tempo, and the WAV file to write:
sound-to-patch render bass.vital --midi notes.mid --bpm 120 -o bass.wav
plug-in: Vital 1.0.7 from /Library/Audio/Plug-Ins/VST3/Vital.vst3, in sound-to-patch-host 0.18.0 (vst3-host 0.10.0) as x86_64
preset: routes, 2x oversampling
notes: 3, the last note-off at 3.000 s, 120 BPM
init patch: 2 renders, 0.04 dB apart
load check: passed; the saved state matches, and the render lies 58.20 dB from the init patch, beyond the margin of 0.11 dB
clip: 5.589 s, the tail went quiet
wrote bass.wav
wrote bass.json
This is one local run: it played the routes.vital test fixture, the
built-in template with three routes, through Vital 1.0.7. The MIDI file
holds MIDI 48, 55 and 60 at velocity 100, one second each, back to back.
The dB figures change a little from run to run.
Each line names one step:
plug-in: the synth, its build, the bundle and the host, with the architecture the host runs as.preset: the preset’s name and the oversampling of the render. Every Vital render uses 2x oversampling, whatever the preset holds, because oversampling changes the sound.notes: how many notes the MIDI file holds, when the last one ends and the tempo. The file must be one thatanalyze --notesreads: every tempo in it within 0.01 BPM of--bpm, and the notes off channel 10.init patch: the plug-in renders the same notes twice with its own init patch before the preset loads. The two renders differ a little: Vital’s oscillators randomize their start phase fully by default (osc_1_random_phasedefaults to 1 in the registry).load check: see below.clip: the length of the WAV file. A clip is at least 4 s long. It ends once the output has stayed below -96 dBFS for 1 s after the last note-off. A tail still sounding 30 s after the last note-off is cut there, and the line says so.
The load check
A plug-in can report that it loaded a preset and still play its old sound. So a render counts only when two checks pass:
- The clip sounds. A clip whose every sample stays below -96 dBFS is silent, and the error names what in the preset can explain it, such as no oscillator and no sample being on.
- The state the plug-in saves after the render holds the preset: its
name, its tuning and every numeric control, each number within one step
of a 32-bit float of the sent value. The tempo control is the
exception: the plug-in sets it from
--bpm, so it must lie within one step of the tempo Vital computes from--bpm, in beats per second. A preset without a tuning plays in Vital’s default tuning, never in the tuning of the preset rendered before it. When the saved state is the init patch, the plug-in did not load the preset at all, as with a preset saved by a newer build of Vital.
Each failure has its own message, and the command writes no file.
A third check only warns. Each clip is measured as its level in octave
bands, and the distance from the init render is the largest band
difference in dB. Only the bands within 30 dB of the loudest band count,
and only those above a floor of -90 dB of mean bin power. That floor is
not a level in dBFS: a band of many bins reads lower than a band of few
for one tone, so the floor passes a sine above about -50 dBFS in the top
band and above about -74 dBFS in the lowest band at 44.1 kHz. A clip within three times the distance between the two
init renders, and within at least 0.1 dB, sounds like the init patch. The
render still counts, and render prints a warning. Mean band levels miss
a change of envelope or modulation: a slower attack on the init patch can
lie within the margin. The margin is a stopgap. Two init renders give no
distribution, and a later release measures the spread over many renders.
The output
The WAV file holds 32-bit float stereo at 44.1 kHz. The record next to it,
bass.json here, names everything the audio depends on: the synth and its
build, the host, the recipe version, the sample rate and block size, the
settle time, the oversampling, the tempo, the clip length, the SHA-256 of
the MIDI file and the load check. The
render record page lists every
field.
Pass the WAV file and the MIDI file to analyze to measure the render as
you would measure a stem:
sound-to-patch analyze bass.wav --bpm 120 --notes notes.mid
Two renders of one preset are not identical, so compare renders within their spread, never sample by sample.
Using the synth reference
The synth reference is an agent plugin, sound-to-patch-reference. An
agent reads it when it writes an edit list for sound-to-patch preset apply, writes notes from a producer’s words, or explains a preset. It
lists the controls of each target synth by layer, with the on-screen
label, the preset key, the unit, the range and how to tune each control by
ear. It also holds the music dictionary and the word dictionary: what
words such as warm or plucky mean, and how far listeners agree on them.
Only Vital is covered today. Serum 2 follows with its adapter.
Install it
Add the repository as a marketplace, then install the plugin:
claude plugin marketplace add Akurganow/sound-to-patch
claude plugin install sound-to-patch-reference@sound-to-patch
Each release also ships the plugin as sound-to-patch-reference-vX.Y.Z.zip.
Pass the unpacked folder or the zip to a session with --plugin-dir.
A session opened on a clone of this repository needs no install: it finds
the same skill under .claude/skills/.
On Windows, let git create symbolic links before you install from GitHub
or clone the repository. Enable Developer Mode (or run the shell
elevated). Then run git config --global core.symlinks true, or clone
with git clone -c core.symlinks=true …. Without symbolic links the
plugin installs with no reference, and a clone of the repository loads no
skill. The
agent reference page
lists which install paths carry the reference.
Use it
Ask the agent for the sound in your own words. The skill loads when the
request needs a control, a note rule or a sound word, so the agent names
each control by its on-screen label and checks its edit list with
sound-to-patch preset apply before it hands you the preset.
Vital and Serum 2 are trademarks of their owners. This project is not affiliated with or endorsed by them.
Analysis pipeline
sound-to-patch analyze turns a stem into layers.json,
spectrogram.png and the final notes.mid, and sound-to-patch notes
turns it into the first note list, a notes.mid of its own.
This page follows the stem through each stage, with the formula, the
convention and the limits of each. The
common methods page holds the rules that every stage
shares: the window, the frame rule, active frames and the statistics.
Every statement carries a tag:
- [fact]: the code does this, or a cited work states it.
- [synthetic]: a figure measured on generated signals. Generated signals are a best case: a real stem adds bleed, effects and noise.
- [proposal]: a threshold or rule chosen for this project that later work may tune.
analyze runs decoding, the tempo, the notes step (unless --notes gives
the notes), the pitch layer, the
envelope layer, the voices layer, the stem, the stereo screen, the
descriptors and the report today [fact]. With --notes the f0 track still
reads the stem, for the pitch layer [fact]. notes runs decoding, the tempo and the notes step [fact].
The analytic signal, the masks, the modulation spectrum, the periodogram and
the decay fits are library stages that the layer estimators of later
releases call [fact].
Decoding
- The stem decodes at its native sample rate with every channel kept, as
f64samples [fact]. The default audio track is the one the container flags as default, else the first with a known codec [fact]. - The rate must lie between 8000 and 192000 Hz, or the file is refused with its rate and the range [fact]. The band-edge rule looks for its fall above 2 kHz, so it needs a Nyquist frequency well above that [fact]. 192 kHz is the highest standard export rate [fact].
- The cost of the analysis grows with the sample rate and with the number
of notes, so a stem of many short notes costs far more than a sustained
one [synthetic]. Examples from a release build on generated 4 s mono
stems of a 55 Hz band-limited sawtooth [synthetic]:
- Sustained: 0.08 s per second of audio at 48 kHz and 0.17 s at 192 kHz.
- Notes of 0.2 s, four per second: 0.28 s per second of audio at 48 kHz and 1.6 s at 192 kHz. With the bound lifted, the same stem cost 1.8 s at 200 kHz, 2.5 s at 256 kHz and 5.1 s at 384 kHz.
- The build reads WAV, AIFF, FLAC, MP3, AAC (ADTS), MP4 or M4A, Ogg, MKV and WebM containers [fact]. A missing codec fails with its name and, if one exists, the decoder feature that adds it [fact].
- A packet that fails to decode becomes silence of its duration, so later samples keep their time, and the command prints a warning [fact]. More than 4 such packets fail the file [proposal].
- Mono is the channel mean, , with no gain normalisation [fact]. A file of three or more channels mixes only the first two, as the stereo measures read them, and the command warns with the channel count [fact]. Analysis never resamples [fact].
- Resampling serves only a consumer with a fixed input rate, such as the 22050 Hz transcription model [fact]. It is a band-limited FFT conversion with output samples [fact]. Some rate pairs, among them 48 kHz to 22050 Hz, lag half an output sample, 23 µs; the lag is documented and not compensated [synthetic]. A NaN or infinite sample stops the conversion with its index [fact].
Tempo
--bpm is close to the stem’s tempo, not exact: a tempo taken from a file
name is often 1 to 2 % off, and that drifts notes off the grid within a few
bars [proposal]. Both commands refine it from the stem before any step that
reads it: the minimum note length, the MIDI tempo and the beat grid
[fact]. --bpm-exact skips the search and uses --bpm as given [fact].
-
Onset strength: the mono mix in periodic Hann frames of 46.4 ms (2048 samples at 44.1 kHz; the power of two nearest 46.4 ms at other rates) with a hop of about 1 ms [fact]. The bins sum to power bands a third of an octave wide from 30 Hz, each at least 6 bins, so a band holds whole main lobes and a steady partial keeps its energy from frame to frame [fact]. Each band power is compressed as , with the knee 60 dB below a full-scale bin [fact]. The strength is the summed rise of every band against the frame 10 ms earlier [fact].
-
The strength is low-passed at 20 Hz forward and backward, its centred mean over 100 ms is removed, and what stays below 10 % of the largest rise becomes 0, so only onsets remain [fact].
-
Search: every tempo in steps of 0.01 BPM with [fact]. Each scores the unbiased autocorrelation of the strength, read by linear interpolation at seconds and summed over to beats, with [fact]. The search spans 2.91 % below to 3.09 % above
--bpm, so a--bpm3 % off either way still holds the truth [fact]. The window is so narrow that half and double the tempo lie outside it [fact]. -
Trust: the best tempo replaces
--bpmwhen three things hold [proposal]:- Its score is at least 1.5 times the median score of the search
(
tempo_peak). - The onsets are hits, not swells: at least 0.9 of the energy of the
band rise, before its low-pass, lies within 25 ms of its peaks, each
the largest within 30 ms (
onset_sharpness). - One of two shares is high enough. The beat share is the best score over
times the zero-lag autocorrelation, and needs 0.4
(
beat_periodicity). The sparse share reads the same sum on every second or every fourth beat lag alone, the better of the two, and needs 0.75 (sparse_periodicity), with at most 0.8 peaks of the onset strength per beat at the slowest tempo of the search (onsets_per_beat). Both shares stop at 1.
The sparse share is read once the stem holds 4 such lags, 12 beats for every second lag and 20 for every fourth [fact]. Otherwise the tempo is reported at low confidence and
--bpmstays [fact]. A stem of fewer than 8 whole beats, or one without onsets, keeps--bpmand reports no refined tempo [fact]. The beats are counted at the slowest tempo of the search,--bpm/ 1.03, so the longest lag keeps 4 beats of overlap at every candidate: a 4 s stem at 120 BPM holds 8 beats, but 7.77 at 116.51 BPM, and keeps--bpm[fact].cycles_in_spanreports that count [fact]. - Its score is at least 1.5 times the median score of the search
(
-
Generated drum, bass and lead parts at 70.3, 97.55, 123.4, 147.81 and 178.2 BPM, 37 beats each at 22,050 Hz, with
--bpmoff by -3, -1, +1 and +3 %: every refined tempo lies within 0.05 BPM of the truth [synthetic]. Theirbeat_periodicityreads 0.78 to 0.99; pad chords that change once a bar read 0.04 at most and keep--bpm[synthetic]. The 0.4 threshold sits between the two [proposal]. -
One kick a bar repeats on a quarter of the beat lags, so its
beat_periodicityreads 0.22 to 0.24 and itssparse_periodicity0.95 to 0.97 at 37 beats [synthetic]. Kicks a bar and kicks on every other beat, on the same grid, refine to the exact tempo [synthetic]. A kick a bar at 120 BPM refines from 21 beats, once the share reads every fourth lag; at 20 beats it reads 0.34 [synthetic]. -
The sparse share needs more than the beat share, because its fewer lags line up by chance more often [fact]. Kick parts that keep
--bpm, with it 3.5 % or 4 % off or under a tempo ramp of 1 %, read at most 0.63; sparse parts read 0.8 or more, swung or with onsets moved by 8 ms [synthetic]. The 0.75 threshold sits between them [proposal]. -
A dense grid lines up with any spacing that is a whole number of its steps [fact]. Sixteenth hats at 70.3 BPM with
--bpm3.5 % off read asparse_periodicityof 0.98 at 16/15 of the truth, where 4 beats are 15 sixteenths [synthetic]. Kick-a-bar parts read 0.19 to 0.26 onsets per beat, kicks on every other beat 0.48 to 0.52, leads 1.23 or more and hats 3 or more [synthetic]. The 0.8 limit sits between them, so a lead or a hat part needs the beat share [proposal]. -
A pad under a smooth tremolo repeats as well as a drum part, and reads a beat or sparse share near 1 [synthetic]. Its rises spread over 180 ms or more [fact]. A chord under a cosine tremolo of half a beat to 4 beats reads an
onset_sharpnessof 0.80 at most where its shares pass, and keeps--bpm[synthetic]. Chord pads read 0.76 at most; drum, hat, bass, lead and kick parts read 0.985 or more, and a bass with an 80 ms attack 0.92 [synthetic]. The 0.9 threshold sits between them [proposal]. -
Only a smooth tremolo keeps
--bpm: one at an eighth-note rate or slower, with a gentle shape [synthetic]. A gate reads as hits [fact]. A chord gated by a square LFO, or pumped with an 80 ms recovery as by a sidechain, every half beat to 2 beats at 117 or 123 BPM with--bpm 120, refines to the tempo of the gate [synthetic]. That is the right tempo when the gate is locked to the song [fact]. -
Swing at two thirds of the beat refines within 0.05 BPM, and onsets moved by up to 8 ms at random within 0.1 BPM [synthetic]. A
--bpm3.5 % or 4 % off, and a tempo ramp of 2 % or 3 % either way, keep--bpm[synthetic]. -
A kick a bar at any tempo from 40 to 180 BPM in 0.5 BPM steps, 37 to 64 beats long, with
--bpm1 % off, refines to within 0.01 BPM [synthetic]. Most candidates score exactly 0 for such a part, so the median score can be 0; the peak ratio divides by at least of the best score, and reads at most [fact]. -
Limits:
- A loop whose bar is not 4 beats long but lies within 3 % of it, such as a kick every 3.9 or 4.1 beats, refines to the tempo at which the loop is 4 beats [synthetic]. The onsets cannot tell such a loop from one at that tempo [fact].
- Silence dilutes the sparse share [fact]. A kick a bar of 37 beats keeps
--bpmwith 12 beats of silence after it or 24 before it [synthetic]. - A soft attack lowers the sharpness [fact]. A bass in eighth notes with
a linear attack of 100 ms keeps
--bpmat 178.2 BPM, and one of 120 ms at every tempo tested [synthetic]. Another sound may reach that point sooner.
-
A steady tone whose period divides the beat, such as 110 Hz at 120 BPM, finds no onset and keeps
--bpm[synthetic]. -
The beat offset stays 0: only the period is refined [fact]. A stem whose tempo drifts is not followed [fact].
The command prints one line for the step: the tempo used and why.
The notes step
sound-to-patch notes turns a stem into notes.mid: one model run, one f0
track, the decode settings, the decode, the cleanup and the MIDI file, in
that order [fact]. analyze runs the same step once, without the MIDI file,
unless --notes gives the notes, and the model then never runs [fact]. The
notes set the frame length of the stem and fill centroid_over_f0 and the
stereo phase [fact].
Transcription model
- The notes come from a transcription model that ships inside the binary and runs on a pure-Rust inference engine, with no native library [fact]. The model and its decoder are credited on the credits page.
- Licence of the weights [fact]. The bundled model file has no licence
statement of its own. It equals the file in the upstream repository
byte for byte: both have the SHA-256
2c3c1d144bfa61ad236e92e169c13535c880469a12a047d4e73451f2c059a0ec, and a test pins that hash against the bundled bytes. The upstream repository grants Apache-2.0 to every file it holds, the model included. The upstream NOTICE text ships inTHIRD-PARTY-LICENSES.htmlwith every release. - The model reads the mono stem resampled to 22050 Hz as 32-bit floats, in 2 s windows that start every 1.64 s [fact]. It returns three activation matrices with one frame every 256 samples (11.6 ms): note, onset and fine pitch, 88 keys from A0 to C8 [fact].
- It runs once per stem, and every decode reads that one run [fact]. The thread count does not change the activations; a test compares 1 and 8 threads bit for bit [fact]. The inference engine’s output differs between processors by up to about 5e-4, so a note near a threshold can flip from one machine to another [fact].
- A NaN or infinite sample stops the run with its index before any window runs [fact]. Silence gives no note and no error from the model [fact].
Profiles
| Profile | Range | Minimum length | Voicing for the cleanup |
|---|---|---|---|
bass | B0 to F#4, MIDI 23 to 66 | from the BPM | one note at a time |
lead | C2 to B6, MIDI 36 to 95 | from the BPM | one note at a time |
pad | C2 to B6, MIDI 36 to 95 | 11 frames, 127.7 ms | chords |
- A range becomes the decoder’s frequency bounds: the frequency of its lowest key and of the key above its highest. Every range from MIDI 21 to 108 keeps exactly its own keys [fact].
- Every profile keeps the decoder’s thresholds (onset 0.5, note 0.3, 11 quiet frames end a note), turns on the decode of notes without an onset, and turns pitch bends off [fact]. The ranges and lengths are [proposal].
Minimum note length
- With the BPM rule, the shortest kept note is half a sixteenth note, ms, clamped to 58 to 127.7 ms and rounded to whole model frames, half to even: 5 to 11 frames [proposal]. At 120 BPM that is 5 frames, 58.0 ms [fact].
- The decoder keeps a note strictly longer than the setting: 11 frames keep notes of 12 frames, 139.3 ms, or more [fact]. A note grown without an onset needs one frame more [fact].
f0 track
- One probabilistic YIN run covers the whole stem at 22050 Hz: frames of 2048 samples, a hop of 128 (5.8 ms), pitch bins of a tenth of a semitone, from 25 Hz to 4200 Hz [fact]. The ceiling sits just above C8, the top of the transcription model: with a 400 Hz ceiling every tone above it read as a subharmonic [synthetic].
- The copy the tracker reads is scaled to a peak of 1, because the tracker treats a frame below a fixed energy as silence [fact]. A sine bank at 30.87, 41.2 and 55 Hz reads within 5 cents with no frame an octave off [synthetic]. Glides of 150 and 400 ms rise within 5 % of their true 10-90 % time; a 50 ms glide reads 41 ms, where the reference implementation reads 23 ms, because a 93 ms frame cannot resolve it [synthetic].
- The track fills each note’s f0 contour with its voiced frames, and sets the register and the octave check [fact].
Register and voicing
- Without
--profile, the detection sets the decode settings from a probe decode over the model’s whole range and the f0 track [fact]. - Voicing: a part is polyphonic when 68 % or more of the probe notes share at least half their span with a note of another pitch class [proposal]. On the generated set lines read 0.56 or less, the highest a melody with reverb, and chord parts 0.79 or more, the lowest a broken chord with the sustain held [synthetic]. Chords with releases 150 ms apart, a moving inner voice under held notes and strums 30 ms between strings all read 1.00 [synthetic]. An octave-up ghost shares its note’s pitch class and does not count [fact].
- Register of a monophonic part: the 1st to 99th percentile of the voiced f0 frames within 20 dB of a reference level, plus 2 semitones on each side [proposal]. The reference is the 95th percentile of the voiced frames’ levels, so a short loud burst, ring or thump does not move it [synthetic]. A polyphonic part takes the lowest and highest probe notes instead, because the tracker reads a chord as one low common period [synthetic].
- The range of a monophonic part widens to every hold candidate outside it, so a note the tracker misreads still lands inside [proposal]. A hold candidate is a probe note whose median frame lies within 10 dB of the reference and that sounds alone for at least half its span [proposal]. A line plays one note at a time, so an octave-up ghost or bleed over the line shares its span with another pitch and does not count; a weaker note an octave away does not count against a note, since the decoder gives every note such a twin [synthetic]. In a fast run at 140 BPM that jumps from A2 to A3, the tracker reads 43 to 52 and the A3 widens the range to 41 to 59 [synthetic].
- A monophonic part takes the minimum note length from the BPM; a polyphonic part keeps the decoder’s 127.7 ms [proposal].
- Limit: bleed within 20 dB of the stem’s loud frames, in a passage where
the part rests, enters the register window, so the range widens to its
notes [fact]. Pass
--profilefor such a stem. - Limit: a line with an occasional held double stop reads as monophonic,
and the upper note of the stop can fall outside the range [synthetic].
Pass
--profile padfor such a stem.
Cleanup
The cleanup runs five steps and prints the note count after each [fact].
- Octave check, on a part played one note at a time: each note meets the median pitch of the voiced f0 frames inside it, given 3 or more [proposal]. Within 1 semitone the note is fine. A note 12 semitones above the median, within half a semitone, over a median no more than half a semitone above A2 (110 Hz), is a copy when a note of the lower key holds it within 2 model frames at each end and has a higher velocity [proposal]. The copy moves onto that note, its key and span [fact]. Any other gap keeps the pitch and sets the note’s octave flag [fact]. A chord part skips the check, because the tracker reads a chord as one low common period [synthetic].
- Why the velocity: the decoder gives every note of an octave bass a twin an octave away with nearly its span, and the tracker can hold the lower octave through a true upper note [synthetic]. On a bass that alternates A2 and A3 in sixteenths, true notes decode at velocity 81 to 100 and their twins at 34 to 52 [synthetic]. The velocity of a decoded note is its mean activation [fact].
- Ghost merge: notes of one key that overlap become one note, from the first onset to the last offset [fact]. A moved copy has its lower note’s span, so it merges into that note alone and never bridges two repeats [fact]. The decoder gives no two notes of one key that overlap, so a decoded repeat never merges [fact]: on every generated part the merge removes exactly the moved copies [synthetic].
- Attack split: a note of A1 (55 Hz) or above splits where the stem has an attack of its own inside it [proposal]. A 25 ms frame of the note marks a dip when it is no louder than the frame before it, quieter than the frame after it, and 19 dB or more below the loudest frame in the 50 ms after it. The note must hold a frame at six tenths of that loudest frame or more, centred 60 ms or more before the dip. The split falls on the last envelope reading at a tenth of the loudest reading after the dip, before the first reading at nine tenths after the quietest of the dip frame’s 26 readings, 1 ms apart, read as in the rise test below. The note before the dip can read louder than the one after it, so the climb starts at the quietest reading, and a deeper gap later in the 50 ms belongs to a dip of its own, so it cannot move the cut past a repeat [fact]. At the same times the level at the note’s fundamental alone, one DFT bin over one period, must reach 23 dB above its quietest reading before the split in a reading from the split on. The piece after the split must last 50 ms, and the attack rule below must hold between the two pieces. Each piece keeps the note’s key, octave flag and velocity [fact]. A last piece of at most 100 ms that runs into the decoder’s next note of its key, within 2 model frames, starts that note instead, unless that note has an attack of its own after the piece by the same rule [fact].
- Why the split: the decoder can read a low repeat with a short gap as part of one long note, and no later step sees two notes [synthetic]. With 30 ms gaps the gated 55 Hz saw decodes as 9 notes for 16, or 8 with a 5 ms tail, and the split gives all 16 in both [synthetic].
- Why the loud frame before the dip: the decoder can start a note in the gap before its own attack, and a split there would cut off a piece of silence [proposal].
- Why the fundamental: below about A2 the harmonics’ window of the attack rule cannot tell a note from a key nearby, so a held note under a louder gated voice of another key dips at its harmonics as well as in the stem [fact]. A bin over one period has its zeros on every harmonic of the note when the period is a whole number of samples, so a tone an octave up reads nothing there [fact]. The period is rounded to whole samples: at 22,050 Hz and at the worst phase a harmonic leaks about -70 dB at A1 and -46 to -50 dB from A2 to A5 [fact]. The bin rejects only the note’s harmonics: a key 1 to 3 semitones up reads within 1 dB of the note, and a fifth up 2 to 6 dB under it [fact]. A held A1 20 dB under an A2 gated for 0.2 s in every 0.25 s gave 16 notes where the split join alone keeps 12; with the fundamental’s dip it keeps the 12 [synthetic]. Held A1s 6 to 30 dB under a gated D2, C2, E2, A2 or A3 give the split join’s notes in every case [synthetic]. Held A1s 6 to 30 dB under a gated key 1 to 3 semitones above or below, F#1 to C2, give main’s note counts too, though the bin reads such a key nearly as loud as the note [synthetic]. There the decoder already cuts the A1 at the gates or drops it, so no dip falls inside an A1 note: where the A1 decodes across a gate, 6 to 14 dB down, the stem dips there by about that much, short of 19 dB [synthetic]. No generated semitone case reaches the fundamental test [synthetic].
- Why 23 dB: from 21 to 25 dB the generated sets give the same notes, and 23 dB lies in the middle [synthetic]. At 19 and 20 dB squares loud for 60 to 80 % of the cycle split a held note further in 72 runs where 23 dB gives 52, and at 30 dB gated runs hit 8 fewer gates [synthetic].
- Why the next note starts at the cut: on a real vocal stem the decoder started the next note of a key 58 ms after its attack, and the cut there left a fragment of 58 ms before it [fact].
- Why 100 ms: the real fragment lasted 58 ms, and a piece much longer than the decoder’s lag is more likely a note of its own [proposal]. A2 gates to 200 ms, then a repeat after a gap of 10 to 20 ms, which the decoder starts 5 ms early: the attack rule finds no attack there, and without the limit the 200 ms piece would start the repeat [synthetic]. The limit leaves the same loss for a shorter gate, as the limits below give [synthetic].
- Why the 50 ms after the split: a decoded note that starts late in one gated repeat runs into the next, and a split at that repeat left a fragment of about 17 ms before the gate closed [synthetic].
- Why A1: below it the envelope window of one period lasts 19 ms or more, as long as the recovery of a fast sidechain duck [fact]. Ducks to -40 dB split at B0 and C#1, and so do ducks to -40 dB that recover in 30 to 45 ms on every key from D1 to G#1 [synthetic]. Full-depth sine tremolos of 10 Hz split at A0 and B0 [synthetic]. At E1 such a duck and a gate with a 30 ms gap and a 5 ms tail read the same trough, 0.069 and 0.066 of the peak, and the same onset activation, 0.27 to 0.44 and 0.23 to 0.39 [synthetic]. From A1 to C4 no duck to -40 or -48 dB that recovers in 25 to 45 ms splits [synthetic].
- What the split was measured on [synthetic]: the four sets below and 16 A1 sixteenths with gaps of 35 to 44 ms, each stem decoded with the detected profile and with the bass profile, a sweep of 1053 ducked and tremolo stems, and the 37 eval parts. The sweep holds ducks to -40 and -48 dB that recover in 25 to 60 ms, linear or exponential in decibels, at three phases, on every key from D1 to A2 and on C3, E3, A3 and C4, with full-depth sine tremolos of 8 to 14 Hz from D1 to A1.
- On those sets no held run splits further than without the split, but for the squares the limits below give [synthetic]. On the first set 8 of the 716 runs come closer to the true notes, and gated runs hit 1245 gates where they hit 1205. On the second set 12 of the 130 gated runs and 10 of the 24 two-voice runs keep more repeats, 1666 gates hit where 1558 were. The fourth set keeps the same notes. Every A1 sixteenths run keeps more: 872 of 1024 gates hit where 214 were [synthetic]. On the eval the dry 110 Hz saw with a 0.3 s release keeps all 6 of its notes where it kept 5 [synthetic]. The bass clean group’s onset F1 rises by 0.002 to 0.003 under every setting and the bass recall after the cleanup from 0.955 to 0.958; no other cell moves [synthetic].
- Split join: a note that starts within 2 model frames (23 ms) of the end of a note of the same key joins it when three things hold [proposal]. Its onset activation lies below 0.68, its velocity is no higher than that note’s, and the stem gives it no attack of its own.
- The attack rule tells a repeat from a split [proposal]. It reads the
stem in the guard’s 25 ms frames, one every 6.25 ms, with any DC offset
removed. The quiet frames are those centred from 75 ms before the onset
to the onset, but not before the earlier note starts. The loud frames
are those centred from the onset to 50 ms after it, but not past the
note’s end. A note has its own attack when three things hold:
- The stem’s loudest frame lies 19 dB or more above its quietest one.
- The level at the note’s first 8 harmonics, read with a Hann window over each frame, lies 22 dB or more above its quietest one.
- The stem does not rise slowly. Its envelope, read every 1 ms over one period of the note and at least 4 ms, climbs from a tenth of its loudest reading after the onset to nine tenths of it. The fastest such climb, from a start no later than 20 ms after the onset, takes at most 0.8 of the window plus 7 ms, the limit. A step climbs through the window in 0.8 of it, the difference of the squares of the two shares. The readings run to 20 ms plus three limits after the onset. An envelope that never falls to a tenth climbs from no silence and fails, so a steady floor must lie 20 dB down. A climb counts only after a hold. The dip runs from the last reading at six tenths of the loudest one or more, at most 400 ms before the climb, to the first such reading after it starts. Before the dip the envelope stayed above half of the loudest reading for 60 ms and for 1.5 times the dip. An envelope that stays below six tenths for the whole 400 ms dipped too long for a tremolo, and holds. The level before the stem starts is unknown, so a dip or a hold that reaches the start is too short.
- Why the stem’s dip: under a line that moves to other keys, a split piece of the old key sees the new note’s attack in the stem, but the stem never falls far between them [synthetic]. On the generated set such pieces dip 14.6 dB or less, a sidechain duck to -16 dB 14.3 dB or less, and a gated repeat 19.6 dB or more [synthetic].
- Why the harmonics’ dip: through reverb, the old key’s tail sits under a new note of another key, and the stem then dips up to 19.8 dB [synthetic]. The tail’s own harmonics dip 18.1 dB or less there [synthetic]. Below about A2 the window cannot tell a note from a tone a fifth or a tritone away, so there the harmonics’ dip is a dip of the stem’s low band [fact].
- Why the rise test: a tremolo to silence dips as far as a gate, but
rises slowly [synthetic]. A full-depth sine tremolo at
rHz climbs in about 0.3 /rs: 37 ms at 8 Hz, 21 ms at 14 Hz [fact]. A gate climbs in 0.8 of the window plus about its attack. One period reads a tone steady and keeps the dip of a tremolo at A1 [synthetic]. Two periods smooth that dip until its climb cannot be timed, and allow about 36 ms at A1 [synthetic]. A window of 1.5 periods ripples, and a gate then climbs too slowly [synthetic]. A tremolo that climbs within the limit peaks about 1.7 limits after its trough [fact]. Readings that end before that peak take a level on the climb for the loudest one, and a slow climb to it reads fast: at A1 a full-depth tremolo of 12 Hz split at some phases with readings to 50 ms [synthetic]. Three limits past 20 ms hold the peak [synthetic]. - Why the hold: below A1 one period spans 24 to 36 ms, as long as the climb of a full-depth tremolo of 14 to 20 Hz, so the climb alone cannot tell it from a gate [synthetic]. A gated note holds its level until its gap. A full-depth sine tremolo stays above half its peak for 0.47 of its cycle before it falls to six tenths, under 60 ms above 7.8 Hz [fact]. The fall of a gate ripples by a few hundredths through the window, so a hold that ended at half could end on a ripple: at E1 a repeat after a 50 ms gap read just under half its peak 1 ms before the fall and joined [synthetic]. On the generated sets 50 to 70 ms keep every repeat and split no sine tremolo [synthetic]. At 45 ms full-depth tremolos of 14 Hz on E1 split, and at 80 ms sixteenths on E1 with attacks of 19 and 23 ms join [synthetic].
- Why 1.5 times the dip: a square tremolo rises like a gate, and slower than about 9 Hz it holds through each pulse for 60 ms [synthetic]. Off half its cycle it holds as long as it dips, and a gated note holds longer than its gap [fact]. At 1.5 no square tremolo off half its cycle splits a held note on the generated sets [synthetic]. At 1.25 a 9.1 Hz square on C2 splits a held note into 10 where the onset rule gives 5. At 2 eighths on E1 with a 10 ms attack and a 10 ms tail over a -40 dB floor keep 3 of 16 repeats in the bass profile, as the onset rule does, where 1.5 keeps 12 [synthetic].
- Why 400 ms: a sine tremolo that reads as fast runs faster than about 8 Hz, and at full depth stays below six tenths of its peak for about 70 ms of each cycle at 8 Hz [fact]. A square tremolo off half its cycle stays low for 167 ms at 3 Hz, and the search must reach its fall for the ratio to weigh it [fact]. On the generated sets 200 to 500 ms give the same notes; at 150 ms an E1 under a 3 Hz square off half its cycle splits into 12 notes where the onset rule gives 7 [synthetic].
- Why the stem start counts as unknown: a stem can start in the trough of a tremolo, and its first climb then follows a dip that the search cannot see the start of [synthetic]. Read as silence, that dip held. On the third set one stem split that way: an A2 under a full-depth 14 Hz square loud for half its cycle, with 12 ms edges, that turns off 2 ms after the stem starts. It gave 4 notes where the onset rule gives 2, with the detected profile and with the bass profile [synthetic].
- Why the onset rule alone fails: the decoder can fill the gap before a low repeat and read its onset weak [synthetic]. A 55 Hz saw gated on for 0.2 s in every 0.25 s decodes as 16 notes that touch, 12 of them on onsets of 0.54 to 0.59. The onset rule alone joins them into 6, and the attack rule keeps all 16 [synthetic].
- What the rule was measured on [synthetic]: four generated sets, each
stem decoded with the detected profile and with the bass profile, and
the 37 eval parts.
- The first set holds 358 stems. Held saws from A0 to A4 sit under full-depth sine tremolos of 2 to 20 Hz, at three phases up to A2, triangle tremolos, tremolos of depth 0.8 and 0.9, a vibrato of 50 cents, and sidechain ducks of -16 to -40 dB that recover in 25 to 100 ms. Gated saws at B0, E1, A1, E2 and A2 have gaps of 30 to 100 ms, tails of up to 40 ms, noise floors at -40 and -60 dB and attacks of 5 to 25 ms. Two voices a fifth and an octave apart and a DC offset complete it.
- The second set holds 523 stems with random settings. Held saws from A0 to A2 sit under sine, triangle and square tremolos of 2 to 20 Hz, ducks of -20 to -40 dB and tremolos with vibrato. 150 more stems sweep full-depth sine tremolos of 12 to 20 Hz on A0, B0, E1, A1 and A2 at six random phases each. Gated saws and two voices a fifth or an octave apart complete it.
- The third set holds 528 held saws at E1, A1, E2 and A2 under square tremolos of 3 to 20 Hz. Each is loud for 50 to 80 % of its cycle, with edges of 1 to 12 ms and depths of 0.8 to 1. 48 of them add a vibrato of 30 to 80 cents.
- The fourth set holds 40 gated saws at B0, E1, A1, E2 and A2, one every 0.25 s, with gaps of 80 to 150 ms.
- On those sets no gated run keeps fewer repeats than the onset rule alone, and 45 of the 716 first-set runs, 69 of the 154 gated second-set runs and 14 of the 80 fourth-set runs keep more [synthetic]. The rule keeps every repeat the decoder gives for the gated A1 with 50 ms gaps, with 30 ms gaps, with a DC offset and with A2 above it [synthetic]. It keeps part of the rest: 11 of 14 A1 repeats under a fifth, 13 of 16 eighths with a 20 ms tail, and fewer as the attack slows past 10 ms [synthetic]. Every sine tremolo, duck, triangle tremolo and tremolo with vibrato gives the same notes as the onset rule alone, at every phase swept, and so does every square tremolo loud for half its cycle [synthetic]. Squares loud for 60 to 80 % of the cycle can split a held note further, as the limits below give [synthetic]. The eval scores match main in every cell [synthetic]. A slack of 6 to 8 ms gives the same held notes on the first set. At 9 ms a duck to -40 dB on E1 splits, and on the second set 8 ms splits one on A1; each ms less keeps fewer repeats [synthetic]. Off these sets the margins are thinner: a 55 Hz repeat under a reverb tail 20 dB down can read a harmonics’ dip near 20 dB and join [synthetic].
- A weaker piece before a stronger note is an early fragment of the attack and stays, since joining it would move the onset [synthetic].
- Limits that remain [proposal]:
- A repeat played legato, with no drop in level, still depends on the onset margin, which is thin on both sides [synthetic]. The weakest repeat onsets on the generated set are sixteenths at 175 BPM, at 0.70 [synthetic].
- A repeat whose gap a tail or reverb fills to within 19 dB, or to within 22 dB at its harmonics, joins the note before it [synthetic]. So does one whose attack is slower than about 7 ms plus 0.8 of the envelope window: at A2 a linear attack of 13 ms stays apart and one of 14 ms joins [synthetic].
- The rise test passes a full-depth tremolo to silence faster than
about 0.3 / (0.8 /
f0+ 7 ms) Hz: 8 Hz at A0, 11 Hz at E1, 14 Hz at A1, 21 Hz at A2 and 29 Hz from B3 up, where the window stays 4 ms [fact]. The hold fails such a tremolo above about 7.8 Hz, and the window lengthens the hold a little, so at A0 a tremolo near 8 Hz can pass both tests [fact]. None of the generated ones did [synthetic]. - A square LFO off for 20 to 40 % of each cycle is, by level, a gated repeat with short gaps: it rises like a gate and holds for 1.5 times its dip or more [fact]. To the cleanup it is a gate. On the third set squares loud for 60, 70 and 80 % of the cycle split a held note further in 38, 57 and 22 of their 240 runs: an A1 under a 6 Hz square loud for 70 % into 22 notes where the onset rule alone gives 6, and with the attack split into 24, one a pulse [synthetic]. A vibrato under such a square splits the same way, in 18 of the 96 runs, all loud for 60 or 70 % [synthetic]. The attack split cuts such a note at its pulses as well: squares loud for 60, 70 and 80 % split further than without the split in 15, 20 and 7 of their 240 runs, and a vibrato under one in 10 of 96. An A1 under an 8 Hz square loud for 70 % gives 32 notes, one a pulse, where it gave 8 [synthetic]. The model’s onset activation at those pulses, 0.25 to 0.67, matches that of gated repeats [synthetic]. Only a cue such as a phase reset at each repeat could tell the two apart [proposal].
- Below A1 the attack split stays off, so a repeat the decoder reads as part of one note stays joined there [synthetic]. From E1 the split, measured before its fundamental test, would keep more repeats in 33 of the 242 gated runs of the second and fourth sets and the A1 sixteenths, and in 8 of the 24 two-voice runs, 238 more gates hit; it would also split the ducks above [synthetic].
- Below about A2 the harmonics’ window cannot tell a note from a key nearby, so a held note under a louder gated voice of another key dips at its harmonics too, and the split join keeps the decoder’s pieces of it apart [fact]. A held A1 20 dB under an A2 gated for 0.2 s in every 0.25 s gives 12 notes, and under a gated E2 13 [synthetic]. The attack split adds no cut there, since the held note’s fundamental never dips [synthetic].
- From about A6 up a period lasts 13 samples or fewer at 22,050 Hz, and at the worst phase a harmonic leaks into the fundamental’s bin at -23 dB at A6 and -17 dB at A7 [fact]. There the fundamental’s dip guards less well against a louder gated voice an octave or more up [fact]. The generated sets hold no such pair [synthetic].
- A gated repeat of 60 to 90 ms after a gap of 10 to 20 ms, which the decoder starts 5 ms early and with a higher velocity, still loses its onset: the cut piece before it lasts 100 ms or less and starts it, so the stem gives 2 notes for 3 and the repeat starts at the cut [synthetic]. The 60 ms hold and the 25 ms dip frame hide so short a gap from the attack rule [fact]. After a 25 ms gap all but the 80 ms gate lose it too; gates of 95, 100 and 120 ms keep 3 [synthetic]. The join exists for the real 58 ms fragment above, which would stay a note of its own without it [fact].
- A held A1 under a 6 dB amplitude LFO of one cycle per eighth note at 120 BPM gives 2 notes, as the onset rule alone does: the decoder reads a strong onset of its own at 0.19 s [synthetic]. The attack split adds no cut there, under such an LFO at 1/8 or 1/4, at A1 or E1 [synthetic]. Keeping such a note whole is #306.
- A repeat after a note that holds above half its level for less than 60 ms joins the note before it when the decoder reads its onset weak [synthetic]. At A2 a note of 59 ms before a 30 ms gap holds long enough and one of 58 ms does not [synthetic]. Sixteenths played at three quarters of their length last 11250 / BPM ms, under 59 ms above about 190 BPM [fact].
- A repeat whose gap lasts longer than about two thirds of the note before it joins as by the onset rule alone, since that note holds for less than 1.5 times the dip [fact]. Staccato eighths at half length, gated for 125 ms in every 250 ms, keep 3 of 16 repeats at E1 in the bass profile, as the onset rule does; with 100 ms gaps they keep 8 where it keeps 3 [synthetic]. Sixteenths pay it too: A1 sixteenths with 41 to 44 ms gaps and a 10 ms attack kept 3 to 6 of 32 repeats where a hold of 60 ms alone kept 7 or 8; the attack split now gives them 14 to 27 [synthetic].
- Most bowed, blown and sung re-attacks rise slower than about 10 ms, and such a re-attack joins when the decoder reads its onset weak [proposal]. The rule reads only the stem’s level, and a slow re-attack climbs like a tremolo. A spectral cue such as new partials or a phase reset could tell them apart, but the generated sets change only the level, so they cannot measure it [synthetic].
- The decoder itself can miss a repeat: with 30 ms gaps the gated 55 Hz saw decodes as 9 notes for 16, or 8 with a 5 ms tail [synthetic]. From A1 up the attack split restores them; below it nothing does [synthetic]. At 110 Hz the bass profile decodes the same signal as 29 or 30 notes, an octave copy over most of them, and the cleanup gives the 16 [synthetic].
- Quiet-note guard: a note whose span holds no 25 ms frame within 40 dB of the stem’s loudest frame is dropped [proposal]. The model scales each window by its own level, so near-silent bleed fills its range and gives notes [fact].
- The ghost: a dry 110 Hz saw (a band-limited saw at amplitude 0.3, 0.5 s
notes with a 5 ms attack and a 0.3 s linear release) decodes with the
bass profile as the true note plus a 70 ms copy at MIDI 57 that ends
about 80 ms after the onset [synthetic]. The copy needs a minimum length
below 70 ms: the 11 frames of the
padprofile remove it [synthetic]. After the cleanup the stem gives the A2 alone, all 6 of its notes: the model misses the onset of the fourth, and the attack split restores it [synthetic]. Six such notes through a 1.5 s reverb give seven notes, one of them a tail fragment, and six after the cleanup [synthetic]. - Limit: a lone note an octave above the tracker’s reading is flagged, never moved [synthetic]. Only a weaker copy inside its lower note moves.
- Limit: a chord part keeps its octave copies, since it skips the check. Three generated triads decode as 10 notes for 9 [synthetic].
- Limit: the guard gate is relative to the loudest frame, so only digital silence, every sample exactly 0, gives no note. A file of DC alone or of hiss at -70 dBFS gives notes [synthetic].
- Limit: a repeated note whose onset the model misses merges into the note before it, and note ends stay late; a later release fixes ends [synthetic].
MIDI file
- Format 0, one track, 480 ticks per beat, a tempo event of µs per beat and 4/4 at tick 0, every note on channel 1 [fact]. Our own MIDI reader reads the file back [fact].
- Every note has velocity 100: the decoder’s velocity is a confidence, not a loudness [proposal].
- Times become ticks with [fact]. A note lasts at least 10 ms and one tick; a note of one key is cut at the next onset of that key, and a note left with no length is dropped [fact]. A fractional pitch rounds to the nearest key [fact].
- A list with no note is an error, so a silent stem writes nothing [fact].
Evaluation
cargo xtask eval scores the notes step on 37 generated parts with known
notes: saws, sines, an 808, plucks, pads, strums, a broken chord with
sustain, glides, octave basses, sixteenths at 175 BPM with repeats, a
fast run with an octave jump, reverb, bleed, loud transients over a bass line,
silence and an A6 saw [fact]. A note matches when its onset lies within
50 ms and its pitch within 50 cents, one-to-one [fact]. The truth of a
note is its gate: the onset comes before the attack, the offset before
the release, a kick’s pitch is the one it falls to, and a glide splits at
its midpoint into two notes [fact]. F1off adds the offset within 20 %
of the duration or 50 ms, as mir_eval does; F1o20 drops the 50 ms
floor, which accepts most of a short note; F1rel scores the offset at
the end of the release of the dry render, the gate plus the release
[fact]. That is where the dry render ends, not where the sound ends on the
three reverb parts, whose tail runs on for about 1.5 s [fact]. On the lead
plucks the model’s offsets come 50 to 75 ms after the gate, against a
tolerance of 50 ms, so the 20 ms release brings 13 more of the 16 notes
within it: F1off reads 0.062 and F1rel 0.875 [synthetic]. Onset F1 with
matched, estimated and reference notes, raw and after the cleanup
[synthetic]:
| Group | Right profile, raw | Right profile, clean | Detected, raw | Detected, clean |
|---|---|---|---|---|
| bass, 13 clean parts | 0.755 (239/381/252) | 0.896 (240/284/252) | 0.791 (239/352/252) | 0.907 (240/277/252) |
| bass, 4 parts with bleed | 0.457 (32/108/32) | 0.627 (32/70/32) | 0.615 (32/72/32) | 0.865 (32/42/32) |
| bass, 3 parts with transients | 0.622 (23/50/24) | 0.821 (23/32/24) | 0.742 (23/38/24) | 0.920 (23/26/24) |
| lead, 9 parts | 0.945 (130/143/132) | 0.974 (130/135/132) | 0.945 (130/143/132) | 0.974 (130/135/132) |
| pad, 7 parts | 0.784 (80/111/93) | 0.856 (80/94/93) | 0.784 (80/111/93) | 0.856 (80/94/93) |
Recall over each class, raw and after the cleanup, is the same for the right profile and for detection [synthetic]:
| Class | Raw | Clean |
|---|---|---|
| bass, 20 parts | 0.955 | 0.958 |
| lead, 9 parts | 0.985 | 0.985 |
| pad, 7 parts | 0.860 | 0.860 |
- The cleanup removes no matched note on any part [synthetic].
- Silence gives no note under any setting [synthetic]. Detection scores no worse than the right profile on every class, raw and clean [synthetic].
- The strummed part matches 19 of its 32 notes under the pad settings,
and 31 under the lead settings [synthetic]. Pass
--profile leadfor a strummed part. - The 808 parts score an onset F1 of 0: the pitch is right, but the onsets come 100 to 170 ms late, after the pitch drop [synthetic].
- These are generated signals, a best case. Real stems add bleed, effects and noise, and score lower [proposal].
Time cost
cargo run --release --example notes_memory -- <stage> runs one stage of
the notes step on one generated 60 s stem at 22050 Hz, saw and sine notes
from A2 to G4 [fact]. CPU time (user) and peak resident size of one
process per stage, the median of five runs of a release build
[synthetic]:
| Stage | CPU time | Peak memory |
|---|---|---|
| The stem alone | 0.08 s | 14 MB |
| Model run | 1.20 s | 175 MB |
| f0 track | 1.08 s | 65 MB |
| Model run, decode and cleanup | 1.22 s | 190 MB |
| End to end | 2.26 s | 230 MB |
- The f0 track costs about the same CPU time as the model run, on one thread. Its time and its memory grow linearly with the stem [fact].
- The decode and the cleanup add 15 MB to the model run [synthetic].
- On a real stem of 221 s,
sound-to-patch notestook 11.8 s and peaked at 0.73 GB. The dense Viterbi search that the track used before took 290 s and 4.2 GB on the same stem, with the same notes [fact]. - A debug build runs the f0 track at about half a second of CPU time per second of audio, 25 times slower than a release build [synthetic].
cargo bench --bench notestimes the same steps by wall clock. Its medians depend on a quiet machine and were not taken for the current tracker [fact].
The stem
- Mid and side are and [fact]. A one-channel file’s mid is its channel and it has no side [fact].
- The beat grid takes the period seconds from
--bpmand an offset of 0. The offset is given, never estimated [fact]. - The frame length of harmonic analysis at 44.1 kHz is 8192 samples when the note list has a median f0 below 100 Hz, else 4096 [fact]. A Hann main lobe is about 4 bins wide, so harmonics apart need [fact]. At another rate the length is the power of two nearest, in log, to that length times , so the frame keeps its duration within a factor of : 32 and 48 kHz keep 4096 and 8192, 88.2 and 96 kHz take 8192 and 16384, 176.4 and 192 kHz 16384 and 32768 [fact]. The long frame meets the rule down to 30.5 Hz at every rate, and to 23.4 Hz or lower at the standard ones [fact].
Band edge
The band edge is the highest frequency that stays above the noise floor. A cutoff estimate within one octave of it is never reported [fact].
- The long-term power spectrum of the mid is averaged over the active frames that lie wholly inside the stem, in dB [fact].
- A running median over one semitone on each side smooths it [fact].
- Above 2 kHz, the steepest fall of the smoothed level over one semitone is found. When it is at least 25 dB and the median level of the octave below lies within 20 dB of where the fall starts, the edge is its half-depth point, at high confidence [fact, proposal for the 25 and 20 dB].
- Otherwise the floor is the lowest level above 1 kHz, and the edge is
the highest bin at least 30 dB above it, at low confidence. With no such
bin the edge is the Nyquist frequency with the reason
no_band_edge[fact, proposal for the 30 dB].
The reason band_edge_drop carries the depth of the steepest fall [fact].
The plateau check of step 3 keeps a lone tone’s skirt from counting as a
cut [fact].
- Synthetic cuts fall 57 to 91 dB over one semitone, and a 2-pole roll-off at 3 kHz about 1.3 dB, so the 25 dB threshold separates them with room on both sides [synthetic].
- On white, and noise cut at 8 and 16 kHz at 44.1 and 48 kHz, the steepest fall erred +0.01 to +0.07 semitone and the margin rule +0.03 to +0.16 [synthetic]. With a noise shelf 60 dB down above a 16 kHz cut, the margin rule erred +3.2 semitones and the steepest fall +0.02 [synthetic]. Full-band white noise and the 2-pole roll-off have no steep fall, and both rules give the same margin value [synthetic].
Flat tops
hard_limited is true when runs of at least 1 ms of samples within 1e-5
of the channel peak cover at least 0.1 % of one of the first two
channels [fact, proposal for the thresholds]. A run counts only when the samples on both sides lie at
least half the peak on the same side: clipping runs into the ceiling,
while a square or pulse wave jumps to it [fact]. Square and pulse waves at
55 and 220 Hz do not flag [synthetic]. The level descriptors carry the
flag as a reason [fact].
Stereo screen
The screen runs on L and R with 8192-point Hann frames for every f0 [fact].
- is the Pearson correlation of L and R at zero lag [fact].
- [fact].
- Per band the screen sums , and over the bins inside the band, DC left out, and forms both values from the sums [fact]. The bands are sub below 120 Hz, low_mid from 120 Hz to 1 kHz and high above [fact, proposal for the edges].
ipd_degneeds the median f0 of a note list. At each harmonic , , it takes the largest cross-spectrum bin within 2 bins and the phase , weighted by , then the mean over active frames [fact].- A frame is active from the RMS over both channels, , so an anti-phase stem with no mid still counts [fact]. reads 180 degrees [synthetic].
- The screen cannot see an anti-phase chorus or a light reverb, and a
mono source can still get width from a reverb tail. Its confidence
carries the reason
stereo_blind_spot[fact]. - Detuned unison voices share one bin at harmonics 1 and 2 of an
8192-point frame, so their beat phase raises
ipd_degto 12 to 43 degrees for panned 3- and 7-voice unison; harmonics where the voices resolve read 0.2 to 4.6 degrees [synthetic].
STFT and peaks
- Frames are centred: frame covers samples to , zero-padded past the ends [fact]. A signal of samples gives frames for an even [fact].
- The transform is not normalised: , with oversampling that zero-pads each frame [fact]. Oversampling refines peak positions and changes no resolution [fact].
- A frame is computed one at a time, so a long stem never holds its whole spectrogram [fact].
- A peak is a strict local maximum, refined by a parabola through the log magnitudes of the bin and its neighbours: [fact]. Each peak also records its height over its neighbours in dB [fact].
- For one 100 Hz sine at 30 dB SNR in 8192-point frames at 44.1 kHz, 300 trials, the Hann peak has a standard deviation of 1.6 mHz against a bound of 0.7 mHz, and the rectangular window 22 mHz [synthetic]. Without noise the Hann bias stays below 0.016 bin without padding and 1.1 mHz with 4 times padding [synthetic].
Analytic signal
- The analytic signal comes from a zero-padded FFT [fact]. It means something only for one narrowband component, so a caller band-passes around one partial first [fact]. Its memory grows with the segment, so it serves note-sized segments only [fact].
- The heterodyne multiplies by , low-passes with a fourth-order Butterworth and decimates [fact]. A partial comes out as , so the envelope reads [fact].
- The zero-phase low-pass runs the Butterworth forward and backward: -6 dB at the cutoff, 48 dB per octave, no delay [fact]. Each end is extended by an odd reflection of three cutoff periods, and each pass starts in the steady state of its input, so a constant or a ramp passes without a transient [fact]. Attack envelopes use a 150 Hz cutoff, because a 5 Hz smoother smears attacks shorter than tens of milliseconds [fact, proposal for 150 Hz].
- The band envelope mixes the band centre to DC, low-passes at half the bandwidth forward and backward with an even reflection at each end, and takes the magnitude [fact]. Its memory stays linear in the length [fact]. A 10 % AM at 55 Hz reads a depth of 0.1013 over the whole track [synthetic].
Note masks
- The mask of note at frequency and time is , with following the note’s contour [fact].
- The defaults are harmonics, (one unpadded bin) and a threshold of , the mask value two from a harmonic, which covers the 4-bin Hann main lobe [fact, proposal for the values]. Harmonics at or above the Nyquist frequency are skipped [fact].
- Each bin of a frame records the note that claims it and whether a second note claims it too [fact].
- The out-of-mask share is the power outside every mask over all cells: bleed plus any note the list missed [fact].
analyze --notesreads the note list from a Standard MIDI File: note-on to the next note-off of the same key and channel, first in, first out, with the times from the file’s tempo map [fact]. Notes on channel 10, the percussion channel, are left out, because their keys name drums [fact].
Modulation spectrum
- Band envelopes are tracked at 200 Hz, a 5 ms hop [fact].
- The relative modulation of a track is its natural log, floored 60 dB below the peak, minus its least-squares line [fact]. The log turns a decay in dB times an LFO into a line plus the LFO [fact]. A cubic lost a swell’s 5 Hz LFO and a quintic misread a 2-cycle 4-bar LFO, where the line kept both [synthetic].
- The spectrum is the magnitude of the Hann-windowed track, zero-padded 8 times; the resolution stays [fact].
- The scan runs from two semitones below to 20 Hz, so the 2-bar and 4-bar divisions have room for their peak [fact]. Above 20 Hz a modulation shows as sidebands, not in the envelope [fact].
- The rate is the strongest strict maximum in the scan, with guard points
outside it as neighbours, refined by a least-squares sine fit [fact]. It
needs a line 8 times the median amplitude of the scan, a depth of at
least 0.5 % and two full cycles, else no rate [fact, proposal for the
thresholds]. The reasons
line_prominenceandcycles_in_spancarry both values, with a rate or without one [fact]. - On 200 seeded noise tracks the largest line stood at most 4.4 times the median [synthetic]. On a 104-case grid of periods, depths and rates the strongest line missed 0 sine, 8 saw, 4 square and 4 triangle cases [synthetic]. Noise, a steady tone and a pure decay give no rate [synthetic].
- The period search uses the cumulative mean normalised difference of the detrended track: the first lag below 0.1, else the global minimum, refined by a parabola [fact]. A 7.3 Hz modulation reads 7.30 Hz [synthetic].
- Folding averages the track over a period into 64 phase bins [fact]. The shape reads square-like with a plateau of 35 % of the cycle, a ramp up with a rise of 75 % or more, a ramp down with 25 % or less, and smooth otherwise [proposal].
- A rate snaps to the nearest note division in log distance, at the BPM and at tempos within 2 % in 0.1 % steps [fact].
Periodogram
Note gaps make a plain FFT see the note rhythm as modulation, so gated samples go to the Lomb-Scargle periodogram [fact].
- A sample counts when its level lies within 40 dB of the track’s peak, inside a note, at least 60 ms after the onset [fact, proposal for 60 ms].
- Each trial frequency fits a mean, a sine and a cosine together: the floating-mean form [fact]. On gated notes the plain form put a rate at the note rate on its harmonics [synthetic].
- The power is for a sine of amplitude over samples, and a constant offset changes nothing [fact]. The line amplitude is of the log level, the same depth as in the modulation spectrum [fact].
- The grid steps by 0.01 Hz, and the pick and its thresholds match the modulation spectrum [fact]. The spread is the resolution of the span [fact].
Backward integration
- The decay curve of a tail after a stop is the Schroeder integral of the squared signal, from each sample to the end [fact]. A stem has no impulse response, so the stop comes from the note list, never from a silence [fact].
- The analysis starts at the loudest 10 ms block. The floor is the mean power of the last 10 % of the tail [fact].
- A line through the 10 ms block levels down to 10 dB above the floor gives the time where the decay meets it; the integral stops there and adds the energy the fitted decay would add after it [fact].
- The decay time is of a line fitted between -5 and -25 dB (T20) or -5 and -35 dB (T30) [fact]. A fit fails when the floor lies less than 10 dB below the bottom of its range [fact, proposal for 10 dB].
Descriptors
Descriptors run on the mid with Hann frames of the frame rule, over active frames [fact].
- Shape descriptors use magnitude weights divided by the window sum [fact]. The centroid is , the spread , and the rolloff the lowest bin where the cumulative magnitude reaches 85 % or 95 % [fact].
- Flatness is the geometric over the arithmetic mean of the magnitude from 100 Hz to 10 kHz, each bin floored at after the window-sum scale [fact].
- Band shares use power summed over active frames per band, divided by the power in the bands reported; the edges are 20, 60, 250 Hz, 1, 4, 10 and 20 kHz [fact, proposal for the edges]. A band that starts at or above the Nyquist frequency is left out, and one that crosses it ends there, so a 16 kHz file reports five bands, the last from 4 to 8 kHz [fact].
- Every statistic over frames, the band shares and the band edge need at
least 16 active frames, else no estimate with the reason
too_few_frames[fact, proposal for 16]. On noise, sawtooth and sine clips at 48 kHz the worst error of the medians against a 10 s run was 4 % at 10 frames and 2 % at 16, and many times the value at 8 or fewer [synthetic]. - Peak and RMS are of the mono mix over the whole stem in dB re full scale
1.0, and the crest is their difference [fact]. Digital silence gives no
estimate with the reason
silent_stem, never [fact]. - The conventions of published tools differ in weights, floors and rolloff arguments, so a value is comparable only with its convention [fact]. An ideal 55 Hz sawtooth has a magnitude-weighted centroid of 3348 Hz and a power-weighted one of 220 Hz, 15 times lower [synthetic].
centroid_over_f0divides each frame’s centroid by the median f0 of the note list [fact].
The report
layers.jsonholds theschemaandsynthfields, thestemgroup, thetranscription,pitch,voices,envelopeandnotesgroups and theconventionsgroup; the layer report page lists them [fact].- Output is deterministic: two runs on the same input write the same bytes
[fact].
layers.jsonnames its stem insourceand the SHA-256 of the stem file insource_sha256. A run of another stem, or of a stem of the same name with other content, into the same folder stops unless forced [fact]. - Each file is written to a hidden temporary sibling and renamed into
place:
notes.midfirst, thenspectrogram.png, andlayers.jsonlast. The oldlayers.jsonmoves to a hidden backup before any file changes, and each old file moves aside before its new one lands. A run without notes removes the oldnotes.mid. Alayers.jsonthat exists therefore always belongs to the spectrogram and thenotes.midnext to it, and no file is ever cut short [fact]. A failed write renames the backups back, so the folder keeps the old files [fact].noteskeeps that true: it stops before it replaces anotes.midthat alayers.jsondescribes, and--forceremoves that report once the newnotes.midis in place [fact]. spectrogram.pngis 1500 by 512 pixels, below the size at which a hosted image reader downscales [fact]. The image gives coarse shape; the numbers live in the JSON [fact].- The top panel uses 4096-point frames from 20 Hz to 20 kHz, the bottom panel 16384-point frames from 20 to 500 Hz, both on a log-frequency axis [fact]. Each row takes the largest bin magnitude in its band, or the linear interpolation at its centre when the row is narrower than a bin [fact]. One frame is centred on each column [fact].
- The passage is the 10 s window with the most active 4096-point frames, the earliest on a tie, or the whole stem when shorter [fact, proposal for the rule].
- Colour is viridis over 80 dB below the stem peak: the loudest bin above DC over the whole stem at the panel’s frame length [fact]. A relative scale keeps strong partials from clipping to the top colour [fact]. Every level is finite and clamped before the colour map [fact].
Sources
Read for the problem statements and the formulas; the code is our own.
- Window choice and the peak accuracy bound: Harris, On the use of windows for harmonic analysis with the discrete Fourier transform; Rife and Boorstyn, Single-tone parameter estimation from discrete-time observations.
- Analytic signal: Boashash, Estimating and interpreting the instantaneous frequency of a signal.
- Harmonic masks: Klapuri, Multiple fundamental frequency estimation based on harmonicity and spectral smoothness.
- Modulation spectrum and period search: Atlas and Shamma, Joint Acoustic and Modulation Frequency; de Cheveigné and Kawahara, YIN, a fundamental frequency estimator for speech and music.
- Periodogram: Lomb, Least-squares frequency analysis of unequally spaced data; Scargle, Studies in astronomical time series analysis. II; Zechmeister and Kürster, The generalised Lomb-Scargle periodogram.
- Backward integration: Schroeder, New Method of Measuring Reverberation Time.
- Descriptor definitions: Peeters et al., The Timbre Toolbox.
Pitch layer
analyze reads the pitch layer of a monophonic line after the notes step:
the glides between notes, the pitch drop at the strike of a low note, and
vibrato. The layer writes the pitch group of layers.json; the
layer report page lists
its fields. The tags are those of the pipeline page.
The glides and the vibrato read the f0 track. The drops read the sub-band
line, because the track averages over a 93 ms frame and smears a drop
that settles in 20 ms [fact]. With --notes the model does not run, but
the f0 track still reads the stem, unless the notes play chords or are
empty and the layer is skipped [fact].
Every figure below comes from generated signals at 22,050 Hz, unless it names another rate, through the transcriber’s notes and the f0 track of the release build [synthetic]. A real stem adds filters, reverb and separation artefacts.
What the layer reads
- The voicing of the line. A line that plays chords gets
values: nullwith the reasonpolyphonic_lineand the flagchord_skipped: no method here separates the voices [fact]. The voicing comes from the notes step, or with--notesfrom the same chord rule applied to the given notes [fact]. That rule calls a part chords from a chord share of 0.68 [fact]. Real stems split into shares of 0.41 or less and 0.52 to 0.67, so a part at 0.52 to 0.67 runs this layer as a line; on those stems the layer read 1 false drop, 1 false glide and 4 glides an independent f0 confirmed [fact]. - A stem without notes gets
values: nullwith the reasonno_note_listand the flagno_notes[fact]. - Every time in the group is audio time from the start of the stem [fact].
The track’s lag
The tracker compares the first half of each centred frame with the samples one period later. A frame therefore reads the audio from a quarter frame before its centre to one period after it, so the track lags the audio by a quarter frame, 23.2 ms, less half a period [fact]. That is 18.7 ms at 110 Hz and 14.1 ms at 55 Hz [fact]. The layer subtracts the lag at the pitch it reads; a mono glide’s start then lands within 4 ms of the truth at 110 Hz [synthetic].
The sub-band line
- A fourth-order Butterworth high-pass at 20 Hz and low-pass at 200 Hz keep the fundamental of a low note, each run forward and backward with the odd reflection of the analytic signal stage [fact].
- The analytic signal of the band, zero-padded by its own length, gives the frequency between consecutive samples, [fact].
- A centred median of samples, made odd, removes the phase spikes: 31 samples at 22,050 Hz, 63 at 44,100 Hz and 67 at 48,000 Hz [fact]. Near an end the window shrinks to the samples that exist, so the end is not pulled towards a padding value [fact].
- The line matches SciPy’s
sosfiltfilt,hilbertand a shrinking median on the same 808 tone within 3.2e-4 Hz, a relative 1e-5 [synthetic]. - The analytic signal of a segment ripples at twice the tone near each cut end, by an amount that falls as one over the distance from the end [fact]. A steady 41.2 Hz sine reads its frequency within 0.01 Hz on average and within 0.5 Hz everywhere from 0.23 s inside each end [synthetic]. The layer keeps 0.1 s before the onset search and 0.5 s after the fit span [fact].
- A tone switched on from silence overshoots the line too: by about 11 Hz at 3 ms after the onset, 1 to 4 Hz at 7 ms, and within 1 Hz by 30 ms, at 30 to 150 Hz [synthetic]. A plain 41.2 Hz tone reads 9.4 Hz high at 3 ms and 0.9 Hz high at 7 ms [synthetic].
- The band keeps the fundamental only. When the second harmonic is stronger the line follows it; the drop then fails the weak-fundamental check below [fact].
Pitch drops
A drop is the fast fall of an 808 or a pluck from a higher start into its settled pitch.
- Candidates. Each note whose pitch lies in 20 to 200 Hz is a candidate strike [fact, proposal for the rule].
- Onset. The transcriber places the onset of a falling note late, by up to 230 ms on the rows below [synthetic]. The layer looks for the onset from 0.5 s before the note, or from where the previous note’s search ended, to 0.1 s after it [proposal]. The band envelope is averaged over a centred 10 ms; the onset is the first sample after its lowest point in that span where it reaches half the peak that follows [proposal]. When the lowest point already stands above half that peak, no attack lies inside the span and the note is no candidate [fact]. The average keeps a held saw from reading as a strike: its partials beat inside the band, so the raw envelope dips every period [synthetic]. A centred average leaves the midpoint of a step in place [fact]. A note that the transcriber splits off a held note, as under vibrato, has no attack and no candidate [fact].
- Strike. The rise is the full-band RMS over the 20 ms after the onset
against the RMS from 45 to 25 ms before it [fact]. A drop needs a rise
of 6 dB when its is under 10 ms, when its fall starts at 200 Hz
or above, or when the band envelope takes over 30 ms from 10 to 90 % of
its peak; otherwise it needs no rise [proposal]. A drop with
under 10 ms also needs an attack of 30 ms or less: the fall ends inside
a slower attack, where the line wanders on the rising level [proposal].
On a real stem a fall with = 5.8 ms in the noise floor read an
attack of 208 ms; of the generated drops, only a false one at a soft
retrigger had both [fact, synthetic].
- A saw falling from 220 Hz into the band raises the band envelope with no strike; without the rule its 4-semitone glide also read as a drop of 4.6 semitones [synthetic]. A 7-voice unison falling the same way reads an attack of 191 ms [synthetic].
- Where a ringing tone restarts its phase, the line swings and fits a fast fall: a plain retriggered 808 read 32 semitones with of 4 ms [synthetic].
- An 808 note struck while the one before still rings rises only by that note’s decay. With 0.4 s notes the rise is 9 to 10 dB at a decay of 0.3 s, 4 to 5 dB at 0.6 s, 2 to 3 dB at 1 s and under 1.5 dB at 2 s [synthetic]. Its drop starts near 100 Hz with an attack of 9 to 13 ms, so it needs no rise [synthetic].
- The span before ends 25 ms early, since the band onset of white noise lags the start of the noise by 14 ms [synthetic].
- Level. A strike whose RMS over the 20 ms after the onset lies more than 40 dB below the loudest 25 ms frame of the stem is no candidate, the gate of active frames [proposal]. On a real stem a fast fall settling on a note two octaves under the line read as a drop at -60 dB, where an independent f0 found no voice [fact]. A generated drop 60 dB below a loud passage reads none, 30 dB below it reads [synthetic]. A strike whose 20 to 200 Hz RMS over those 20 ms lies more than 18 dB below the loudest 25 ms frame of that band is no candidate either [proposal]. Checked against an independent IF on real stems, the drops of a line’s own low notes read 3 to 12 dB below the loudest passage of the band; two quiet falls under a louder line in the band read 22 and 28.5 dB below it, and the fall in the noise floor 33 dB [fact]. A generated drop 24 dB under an 87 Hz line reads none, 10 dB under it reads [synthetic].
- Fit. is fitted by least
squares to the line from 7 ms to 400 ms after the onset, or to the end of
the note [fact]. For each the fit is linear in and
; a log grid of 240 points and a golden-section search find
from 3.04 ms, where a tenth of the fall is left at 7 ms,
, to 1 s [fact, proposal for the floor].
The fit matches SciPy’s
least_squareswith the same bounds within a relative 1.5e-6 [synthetic]. - Fit start. From 3 ms the onset overshoot of the line reads as a fast fall: a plain 41.2 Hz tone with a 4 Hz drop fitted a drop of 9.9 semitones with = 3 ms [synthetic]. The fit starts at 7 ms [proposal].
- Refit. A fit with under 10 ms, or after a strike that rises 20 dB, is fitted again on a line whose high-pass sits at 10 Hz, and the refit decides [proposal]. Where a tone below 40 Hz starts from silence or rises, the 20 Hz high-pass raises the line for about 10 ms, which fits as a fast fall; how far depends on the tone’s start phase [synthetic]. On first notes at 32.7 and 36.7 Hz, 6 of 162 random start phases read falls of 10 to 14 semitones with of 3 to 5 ms; on the 10 Hz line none does [synthetic]. The 10 Hz line everywhere loses 3 of 18 drops on ringing 808 lines, so slow fits keep the 20 Hz line [synthetic].
- Residual. For a fit with of 10 ms or more, the error is averaged over half a period of before its RMS [proposal]. The third harmonic of a driven note beats with the fundamental and swings the line at twice the note; the average removes that swing and keeps a slower error, from a wrong model [fact]. A driven 808 drop left a raw residual of 23 % and fitted and within 1 % [synthetic]. Over a full period the average also hid the swing where a retrigger restarts the phase: two same-pitch retriggers read falls of 4 and 11 semitones with of 12 ms [synthetic]. A fall faster than 10 ms ends inside one period, and the average would spread its misfit, so it keeps the raw RMS [fact]. A fall whose spans 1.5 periods of or more changes little within one, so its error is averaged over a full period, which removes the swing of a second harmonic too [proposal]. A sweep into 32.7 Hz with = 90 ms and a second harmonic at half the fundamental failed on the residual with half a period and reads with a full one [synthetic].
- Note line. A fit after a 6 dB strike that is no drop, by any test of the rule or because it settles on no note, is fitted again on a line whose low-pass sits at 1.5 times the note’s pitch, when that edge lies under 200 Hz, so the note sits under 133 Hz [proposal]. Below 66 Hz the second and third harmonics of a note lie inside the band [fact]. On a real sweep into 32.7 Hz the second to fourth harmonics sat 5 to 14 dB under the fundamental, the 20 to 200 Hz line swung from -144 to +83 Hz while the spectral peak fell smoothly from 69 Hz, and the fit’s residual was 2.5, against 0.15 [fact]. At 1.5 times the note the second harmonic falls 10 dB more, while a fall that starts higher stays the strongest component on its way down [fact]. A generated sweep of that shape with harmonics 3 to 8 dB under the fundamental settles near 62 Hz on the 20 to 200 Hz line, a fall of the wrong sign, and reads on the note line with within 1 Hz and within 20 % [synthetic]. Octave falls into 82.4 and 103.8 Hz whose second harmonic is as strong as the fundamental or 5.6 dB stronger settle 120 and 130 cents sharp on the 20 to 200 Hz line and read on the note line [synthetic]. Falls with = 12 ms into A1 and C2 fit on the bound of the search on both band lines and read on the note line [synthetic]. At 1.2 times the note a generated sweep into C1 with strong overtones read 24 % long and one struck again inside its note read none; at 3 times both read none [synthetic].
- Note line span. The refit’s span ends one period of the note before the first point where the note line’s frequency falls to 0 Hz or below [proposal]. The phase of a line that holds one tone never runs backwards; it does where that tone breaks off and a second sound starts inside the same note, and the low-pass smears the break over about a period [fact]. A real sweep into 32.7 Hz was struck again 390 ms later inside the same note: the note line’s frequency fell to -39 Hz there, the fit over the whole 400 ms span missed on a residual of 0.24, and fits ending 200 to 300 ms after the onset read 32 Hz, a fall of 30.5 Hz and of 26 to 28 ms with a residual of 0.05 [fact]. A generated restrike of that shape reads within 1 Hz of the note; ending the span at the break itself read 30.1 Hz, outside the note [synthetic].
- Note line tests. The refit counts when it passes every test of the rule, when its is 10 ms or more, and when its RMS envelope over the fall, from 7 ms to 5 , lies at most 6 dB below the line the first fit read [proposal]. A line low-passed under 70 Hz cannot follow a fall that ends within a period: generated kicks from 255 into 55 Hz with = 3 ms read a fifth of their fall with of 5 ms, and sub kicks over a held note read falls of 3 to 4 ms [synthetic]. Over the fall of four true drops on real stems the note line lay 2.2 to 3.8 dB below the band, and generated sweeps 2.2 to 3.6 dB [fact]. A tone at 127 Hz over a note at 61.7 Hz, with the note’s own tone entering 80 ms after the strike, lay 10.8 dB below: the note line held the upper tone 18 dB down and fell from it to the lower one as that entered, a step between two tones [fact]. Generated tones of that shape lie 4.8 to 7.6 dB under; the two in the tests, 6.6 and 7.3 dB, read as drops without the cap, so the margins are 2.2 dB under it and 0.6 dB over it [synthetic]. Without a strike the note line smooths the swing where a retriggered tone restarts its phase into a fall: three generated retriggers that rose 0.6 to 1.6 dB read one [synthetic].
- Same strike. Each note’s onset search starts where the one before
ended, and a strike on that border rises across it: the next search
finds it again and reads it as a second drop [synthetic]. A search
that starts inside that rise is lowest at its first sample, and a new
strike dips inside the search first [fact]. So a search that starts on
that border, before the drop read last has settled, five time
constants after its onset, holds the same strike when its band
envelope, averaged over two periods of the note, is lowest at its
first sample [proposal].
- The average. A second harmonic in the band beats with the fundamental and dips a 10 ms average by up to 12 dB [synthetic]. The test set is 456 generated sweeps into C1 [synthetic]. They have attacks of 3, 30 and 80 ms, of 30 and 60 ms and second harmonics of 0 to 1. The border lies 20 ms before to 70 ms into the strike. Over 10 ms, 21 of them read a second drop; over one period 5, over 1.5 periods 1 and over two none [synthetic]. Eight read no drop under every rule. Over three periods, 90 runs of 808 hits struck every 115 ms lost 2 of their 270 drops [synthetic].
- Against a fixed span. An onset within 20 ms of the border read 47 of the 456 sweeps as two drops: an 80 ms attack crosses half its peak 22 to 35 ms after a border 5 to 15 ms in [synthetic]. 808 hits struck every 115 ms land 15 ms after their border. That span skipped 59 of their onsets and lost 15 of the 270 drops [synthetic]. Hits struck every 250 ms with drops of = 60 ms read three drops either way, where skipping every onset inside the settling read two [synthetic].
- Rule. A drop counts when in semitones, , is above 2, when the fitted fall from 7 ms on, , is above 2 semitones too, and when the RMS residual is below 15 % of that fall [fact]. Both tests read the fall the fit span shows, not the one extrapolated to the onset [fact]. Against the extrapolated the residual misleads: on a 55 Hz tone with five harmonics, three of them inside the band, the line swings at the onset and the fit found = 0.7 ms with an extrapolated of 776 kHz, a residual of 0.003 % of it but 48 % of the fall from 7 ms on [synthetic].
- Bounds. must lie above the floor of the search, and at most 36 semitones, three octaves [proposal]. A fit on the floor means the best fit lies below it [fact]. Retriggered 808 hits restart the sine’s phase and the line swings at each join: with the floor at 0.5 ms and no cap, five abutting hits at 43.65 Hz read falls of 141 and 152 semitones with of 0.7 ms [synthetic]. The line reads up to 200 Hz, so a fall from more than three octaves above a low note starts far outside the band and is extrapolated from its tail [fact].
- Weak fundamental. After the fit, the medians of the half-octave bands
around the note’s pitch and twice it are read from 50 ms after the onset
or the note’s start, whichever is later, to 400 ms after the onset or the
note’s end, whichever is earlier [fact, proposal for the window]. The
window starts no earlier than the time the fitted pitch falls inside
the band around : a slow sweep from 80 Hz into 32.7 Hz
crosses the band around twice the note, and without that start a pure
sine read as a weak fundamental [synthetic]. When
the upper band exceeds the lower by a factor of 2, 6 dB, the line would
follow the second harmonic: the layer fits nothing for that note and sets
the flag
weak_fundamental[proposal]. White noise reads the upper band 1.41 times the lower, since it is twice as wide [fact]. Below the factor a fit that reads the harmonic settles 1200 cents off the note and fails the settling rule [fact]. - Settling. A drop also settles on a note. One note sounds at some point while the drop is settled, from five time constants after the onset to the end of the fit span. Its pitch lies within 100 cents of , and it starts 150 ms or less before the onset [proposal]. A note that started earlier belongs to a sound before the strike: on real stems a fit on the lead-in to a step settled on the note that had sounded for 790 ms, and a fit inside a chord on a note an octave below that had started 210 ms before [fact]. A generated fall into C1, found in the search of a higher note struck with it, settles on a C1 note that started 100 ms before the strike and on none that started 200 ms before [synthetic]. A C1 note that ends 46 ms after such a fall has settled takes the drop, and one that ends 34 ms before it does not [synthetic]. A slow fall not settled by the end of the span needs the note at its end: with = 90 ms on a 350 ms note, five time constants end after the note, and the sweep read as a glide from silence [synthetic]. At a saw’s onset several partials share the band and the line swings; on a generated 55 Hz saw that swing fitted a fall of 62 semitones with = 2 ms settling at 49.7 Hz, which this rule rejects [synthetic].
- Skips. The summary line
drop skipscounts the low notes the search read no drop at, by the first gate that stopped each: no onset, settling, quiet, no strike, weak fundamental, no fall, tau on bound, residual and off note [fact]. - Confidence. Low when the attack exceeds 30 ms: a slow swell is no
strike [proposal]. High needs a residual below 5 %,
of 10 ms or more and an attack of 12 ms or less, else medium
[proposal]. The reasons are
fit_residualandattack_time[fact].- The fit sees of the fall, a quarter at = 5 ms: kick drops with of 3 to 8 ms read from -31 to +25 % [synthetic].
- During a slower attack the pitch falls before the onset, and reads low: by up to 30 % with a 10 ms ramp, 46 % with 20 ms and 73 % with 40 ms [synthetic]. The 10 ms average spreads a sharp strike to an attack of 9 to 11 ms, a 20 ms ramp to about 17.5 ms [synthetic].
- On the generated sets, high-confidence drops read within 17 %, with one at -30 % (a 10 ms ramp on a 15 ms fall), within 8.3 % and within 0.1 Hz [synthetic].
Drops on an 808 tone, a sine times a 0.5 s amplitude decay, through the transcriber’s notes [synthetic]:
| True / / | Read | Read | Read |
|---|---|---|---|
| 41.2 Hz / 41.2 Hz / 20 ms | 41.17 Hz | 37.57 Hz (-8.8 %) | 21.6 ms (+8.0 %) |
| 41.2 Hz / 41.2 Hz / 50 ms | 41.15 Hz | 40.34 Hz (-2.1 %) | 51.3 ms (+2.6 %) |
| 55 Hz / 165 Hz / 30 ms | 54.99 Hz | 156.34 Hz (-5.2 %) | 30.0 ms (0.0 %) |
| 41.2 Hz / 13.8 Hz / 100 ms | 40.67 Hz | 13.20 Hz (-4.3 %) | 118.8 ms (+18.8 %) |
The acceptance bounds are 1.5 Hz on , 25 % on , and 25 % on up to 50 ms, 40 % at 100 ms [fact]. White noise 30 dB below the tone’s peak keeps within 1 Hz and the drop accepted [synthetic].
The pYIN columns that #67 quotes are re-run in
67-drop-pyin.json:
librosa 1.0.0 pYIN at frames 2048 and 4096, hop 128, on the same tones,
and the same fit from 3 to 400 ms after the onset [synthetic]. Two of the
eight rows reproduce within 0.1 Hz and 5 %, the 50 ms and the 100 ms rows
at frame 2048 [synthetic]. At frame 2048 the 20 ms row reads
55 % high and 24 % short of the issue, and the 30 ms row
8.7 % low [synthetic]. At frame 4096 no row reproduces, but no
field lies more than 5.7 % or 0.31 Hz off the issue [synthetic]. The
smear shows in kind, not within the tolerance: the 20 ms drop reads
29.5 ms at frame 2048 and 76.0 ms at frame 4096, against the
issue’s 72.3 ms there and 21.6 ms from the sub-band line [synthetic]. The
Hilbert column, checked in sub-band-line.json, reproduces 1 of its 4
rows [fact].
Limits:
- Drive and harmonics.
tanh(4 x)puts the third harmonic, 124 Hz, inside the band, and a saw below 100 Hz its second and third. With the average, the drop of the 41.2 Hz row throughtanh(4 x)reads 14 % low and 13 % long [synthetic]. A 12-semitone drop with = 30 ms on a driven sine at 41.2, 55 and 82.4 Hz reads within 0.7 semitones and 7 % on , at high confidence [synthetic]. On a saw the second harmonic swings the line at the note itself, which half a period does not average out, and no drop with under 1.5 periods is read [synthetic]. A second harmonic stronger than the fundamental takes the line: of 36 generated sweeps from silence with one 3.5 dB above it, none read [synthetic]. - A fall under a louder line in the band, or under a louder passage of the same line, is not read 18 dB or more below it, so a soft note after loud ones loses its drop [fact].
- A note above 200 Hz is not read: the band stops there [fact].
- The note line helps only notes under 133 Hz, where 1.5 times the note lies under the top of the band [fact]. It runs only after a strike of 6 dB, so a fall that the band fit misses under a note still ringing at nearly its level stays lost [fact].
- A tone an octave above the note, with the note’s own tone entering soon after it, can still read as a drop on the note line: generated tones of that shape whose note line lay 4.8 to 6 dB below the band read falls from the upper tone into the note [synthetic].
- A true drop whose note started more than 150 ms before its onset, as when the transcriber merges a restrike into the note before it, is rejected [fact].
- A kick at 55 Hz that decays over a quieter held sub at 43.6 Hz can read as a fall from the kick into the sub on the 20 to 200 Hz line, with of 17 to 40 ms [synthetic].
- A kick with no pitch drop and a noise click, over a sub, reads a fast drop of 9 to 27 semitones at medium when it starts from silence, like a kick whose pitch drops [synthetic]. Inside the first period of the note the click and a fall of under 10 ms look alike; rejecting fast falls from silence would lose 26 true kick drops of the generated sets [synthetic].
- A soft retrigger or an envelope dip on a low note mid-line can still read a false fast drop at medium: of 82 generated lines of that kind, one does with the transcriber’s notes, another with the given notes, 15 to 16 semitones with under 4 ms [synthetic].
- A legato note, which follows the previous run with no fall of the band envelope, has no onset and no drop [fact].
- A struck note whose band envelope does not dip first has no onset and no drop: with no release before it and a decay of 1 s or more, the dip of an 808’s 3 ms attack is lost in the 10 ms average [synthetic]. Of 18 drops on generated 808 lines with given notes, 14 read; the misses are those notes and a soft note after a loud one [synthetic]. Through the transcriber’s notes fewer read, since its onsets move the search [synthetic].
- A fast drop, under 10 ms, after a ringing note needs a 6 dB strike, and a drop with a slow attack needs one too [fact].
- Retriggered 808 hits at one pitch, with and without gaps, ringing or not, and 808 lines stepping up to 7 semitones report no drop with of 10 ms or more [synthetic]. A false fast drop, under 10 ms, needs a 6 dB strike and survives the refit on the 10 Hz line only where the limits above say [synthetic].
Glides
A glide is pitch moving from one stable pitch to another.
- The track’s voiced frames, in cents, split into runs of consecutive voiced frames [fact].
- Gaps. A run that starts within 100 ms of the one before joins it when the full-band RMS does not rise 6 dB in the gap, and the pitch keeps moving after the gap the way it jumped across it: by 20 cents or more over the jump and over the next two frames [proposal]. A unison whose voices start in phase beats hard enough to leave the track unvoiced for 75 ms inside a glide; read apart, the rest read as a glide from silence of 3.9 semitones for 7 [synthetic]. At a step between two notes the track goes unvoiced too, but lands on the new pitch and holds it, or swings back; at an 808 step the new strike raises the level [synthetic]. The jump across the gap must be 30 cents per millisecond or slower [proposal]: a 50 ms glide of 7 semitones jumps 20 across two unvoiced frames, the unison above 3.6 [synthetic]. On a real stem a note struck again 10 semitones lower, across three unvoiced frames, read as a glide; that is about 40 cents per millisecond [fact]. Nor is a gap bridged where the full-band RMS, over 10 ms, falls 20 dB below the 20 ms before or after it, or 40 dB below the loudest frame of the stem [proposal]: on real stems steps read as glides across a dip of 41 dB and at a noise floor of -60 dB [fact].
- Steps across a gap. Two stable pitches with a bridged gap between them join no glide when the pitch jumps across the gap against the interval by 20 cents or more, or by more than 60 % of it [proposal]. A step and the settle after it keep moving the way the step jumped, at 5 to 17 cents per millisecond, so neither the slope limit nor the motion test tells them from a glide [fact]. On real stems steps of 3 to 10.5 semitones read so [fact]. A 2.1-semitone fall near 44 Hz on a real stem jumped 200 of its 210 cents across a gap while the level rose 17.5 dB, and reads none [fact]. Unison glides whose voices leave the track unvoiced for 75 ms jump 0.53 and 0.54 of the interval across the gap; a step between 808 notes jumped 0.72 [synthetic].
- A stable pitch is a span of 50 ms or more whose smoothed pitch stays within 30 cents of its running mean and whose least-squares slope stays at 86 cents per second or less [proposal]. 86 cents per second is the floor of a published portamento detector, untuned [fact].
- A glide joins two consecutive stable pitches 100 cents or more apart that each match a note within 50 cents, with the two notes of different pitch [proposal]. The note test keeps vibrato out, which the transcriber gives as notes of one pitch at ±35 cents [synthetic]. No glide is read on any vibrato signal below, ±35 or ±60 cents [synthetic].
- The motion is the span between the middles of the two stable pitches where the slope of the 5-frame moving average, 29 ms, stays at 86 cents per second or more in the direction of the motion [fact]. Under 35 ms it is a step between notes [fact, proposal for 35 ms].
- Then the fitted time decides with the same bounds: under 35 ms a step,
from 35 to 90 ms a glide too short to read, counted and flagged
glide_too_short, from 90 ms on a glide [proposal]. A fit off the curve bound that passes every test of a glide over a detected span of 35 to 90 ms reads as a glide at low confidence, and a motion whose fit fails over such a span is too short [proposal]. The 90 ms is the shortest transition of a published portamento detector, cited and untuned [fact]. On real stems a slide of a semitone over about 60 ms, clear in the spectrum, fell under it [fact]. Reading such fits changes no record of the generated sets [synthetic]. The track smears a step between two 808 notes over a detected span of 100 to 450 ms, while the fit finds the motion short [synthetic]. - Fit. between the two stable pitches is fitted by least squares over the motion and 50 ms of each stable pitch, with the start, the full time and free [fact]. starts fast, starts slow, is linear in pitch [fact]. A grid of 9 powers, 8 times and 13 starts seeds a Nelder-Mead simplex; stays within 0.2 to 5 [fact].
- Shape. A fit of 90 ms or more with within 5 % of 0.2, or with an RMS residual above 40 cents, does not follow the curve and is no glide [proposal]. The glides of the generated signals fit within 22 cents; the false glides at 808 steps sat on a bound of or fitted under 90 ms [synthetic].
- Curve bound. An exponential (RC) portamento, ,
approaches its target slower than any power of the curve, so its
sits on the upper bound of 5 [fact]. A fit within 5 % of that bound
counts when its residual is 20 cents or less, its detected span lasts
90 ms or more, and it does not come from silence; it reads low with the
reason
curve_bound[proposal]. Without those conditions a melody step on a sine and two driven drops read as glides on the bound [synthetic]. - Span ratio. A glide on the bound also needs a detected span of at most 2.5 times its fitted time [proposal]. The track smears a step into a fast curve on the bound and keeps drifting after it, so the slope stays up long after the fitted motion ends: 6.2 times its fitted time at the semitone step on one 808 oscillator that keeps its phase [synthetic], 2.9 and 9.1 at two steps on real stems that an independent f0 read as a step and as no slide [fact]. The RC slides of the tests read 0.77, 0.80 and 2.27 [synthetic]; 26 small slides of 1 to 2.1 semitones on real stems that the independent f0 confirmed in full or in part read 0.44 to 2.28 [fact]. The cut sits 10 % above the highest slide and 14 % below the lowest step, so the margin is thin on both sides [fact]. The level does not tell the two apart: at those real slides the full-band RMS over one period jumped 2.1 to 19 dB [fact]. An RC slide of 1.2 semitones at 110 Hz reads on the bound [synthetic]; one of a semitone reads 90 cents on the grid, under the 100-cent interval, and is not read [synthetic].
time_sis the full 0 to 100 % time [fact].start_sis the fitted start less the track’s lag at the start pitch [fact].- From silence. When a voiced run starts above or below its first stable
pitch by 100 cents or more, and the motion into it lasts 90 ms or more,
the glide carries
from_silence: true[fact]. A shorter motion from silence is neither read nor counted towardglide_too_short, which names a motion between two notes [fact]. The run must settle on that stable pitch [proposal]. Its median level lies within 40 dB of the loudest frame, the gate of active frames [fact]. And within half a period of 3 Hz, the slowest vibrato, the smoothed pitch does not swing back half the lead [proposal]. The motion after the first stable pitch is a step, and the window holds nothing, when the next stable pitch lies 100 cents or more away and matches another note, and the pitch, once past half the way to it, stays there up to that stable pitch and to the window’s end [proposal]. A wobble’s crest can match a neighbouring note, but the pitch comes back from it [synthetic]. When two notes match a stable pitch, the nearest one counts [proposal]. The end of the transcribed note does not cut the window: on real stems the transcriber ended a wobbling note inside its first trough [fact]. A step to a pitch that no transcribed note matches counts as a swing back, since a wobble’s crest matches none either [proposal]. On real stems a wobble of ±60 cents at 6 to 7 Hz read its first half-cycle as a fall from silence, coming back 82 % of its lead [fact]. A quiet onset after digital silence read one into a stable pitch at -41.9 dB, 1.9 dB past the gate [fact]. True falls there came back 0 and 24 % and settled at -16 and -9 dB [fact]. Generated falls that swing back or settle at -42 dB read none [synthetic]. So does a wobble whose note ends inside its first stable pitch, and one at 6 to 7 Hz with a neighbouring note at its crest [synthetic]. One that rebounds a quarter or 45 % of its lead reads [synthetic]. So does one that steps to the next note 30 ms after the stable pitch, with the first note ending at the step’s start, 20 ms into it or after it, and one that slides there over 80 to 400 ms [synthetic]. A generated fall from 150 Hz into 50 Hz with = 40 ms under a 300 ms attack fails the strike rule as a drop [synthetic]. It reads as a fall from silence at medium, 14.6 of its 19.0 semitones: it starts near 116 Hz, where the fall stands 17 ms after the onset [synthetic]. The sound must start up to 100 ms before the run: the full-band RMS over 30 ms rises 20 dB above the RMS from 60 to 30 ms before, searched in 5 ms steps [proposal]. The track voices a run some frames after the sound starts, and a beating unison dips without falling silent [synthetic]. The run’s first frame fixes the start and the fit also finds the start pitch [fact]. A run that starts within 0.2 s of an accepted drop is the drop, not a glide [proposal]. - Confidence. The reasons are the motion span, the RMS residual of the fit
in cents and the jitter: the RMS deviation of the two stable pitches
from their medians, and
curve_bound[fact]. High needs a residual of 15 cents or less, a jitter of 3 cents or less, a motion of 150 ms or more and of 3 or less; low comes with a jitter above 3.4 cents, a curve on the bound or a detected span under 90 ms [proposal].- A faster start reads its rise less well: on the generated sets, up to 11 % off at 4.4, while high-confidence glides read their rise within 7.1 % [synthetic].
- A unison beats and the track wanders on the stable pitches, but the jitter tells a unison only roughly. On a larger generated set the unison glides jitter 3.77 to 7.07 cents at 22.05 kHz and 3.45 to 6.93 at 44.1 kHz, the mono ones 0.66 to 8.03 at both rates; one unison sets the 3.45, 0.05 cents over the cut [synthetic]. In the committed fixtures the unison glides jitter 3.54 cents or more, 0.14 over the cut, and no glide falls from 3 to 3.4 cents [synthetic]. All unison glides read low; from 3 to 3.4 cents only mono glides fall, and they read medium, and a mono glide above 3.4 cents reads low too [synthetic]. One threshold would raise those mono glides to high on no measured gain, so the two stay [proposal].
Glides of 7 semitones up from 110 Hz on a band-limited saw, 0.5 s holds [synthetic]. The rise is the 10 to 90 % time derived from the fitted time and , with ; its error is against the true rise:
| Glide | Voices | Read time | Read | Rise error | Confidence |
|---|---|---|---|---|---|
| 150 ms, 1 | 1 | 151 ms | 1.02 | +0.3 % | high |
| 150 ms, 2.5 | 1 | 156 ms | 2.53 | +3.6 % | high |
| 400 ms, 1 | 1 | 396 ms | 1.01 | -1.3 % | high |
| 400 ms, 2.5 | 1 | 387 ms | 2.41 | -1.2 % | high |
| 150 ms, 1 | 7, ±25 ct | 158 ms | 1.05 | +4.2 % | low |
| 150 ms, 2.5 | 7, ±25 ct | 133 ms | 1.83 | +4.4 % | low |
| 400 ms, 1 | 7, ±25 ct | 405 ms | 1.02 | +1.0 % | low |
| 400 ms, 2.5 | 7, ±25 ct | 411 ms | 2.45 | +3.8 % | low |
| 200 ms, 1 | 7, ±25 ct | 202 ms | 1.00 | +1.0 % | low |
| 200 ms, 1 | 7 in phase | 206 ms | 0.93 | +4.4 % | low |
| 300 ms, 1 | 7 in phase | 304 ms | 1.00 | +1.3 % | low |
| 300 ms, 2.5 | 7 in phase | 375 ms | 3.32 | +4.5 % | low |
| 200 ms, 2.5 | 7 in phase | none | |||
| 50 ms, 1 or 2.5 | 1 | none, too short | |||
| step | 1 | none, no flag |
The #66 table measured the 10 to 90 % span straight off a librosa pYIN
track.
66-glide.json
re-runs it on these signals with librosa 1.0.0 at frame 2048, hop 128,
fmin 30 Hz and fmax 400 Hz [synthetic]. Every true span reproduces within
0.5 ms, the issue’s rounding [synthetic]. Five of the 6 mono figures
reproduce within one hop, 5.80 ms; two of them pass only just, 5.78 and
5.54 ms off [synthetic]. The 50 ms fast-start glide is lost: its first
frame at or above 10 % already stands at 90 %, where the issue read 6 ms
[synthetic]. Of the unison figures only two reproduce:
the 50 ms fast start, lost, and the 400 ms fast start [synthetic]. On
this unison librosa reads the linear glides like the mono ones, 116 and
319 ms against the issue’s 134 and 331 ms, the 50 ms linear glide at
29 ms against lost, and the 150 ms fast start at 11.6 ms against 58 ms
[synthetic]. The unison’s start phases decide it: over 20 seeds the
150 ms linear glide reads 104.5 to 133.5 ms, the 150 ms fast start 11.6 to
98.7 ms and the 400 ms linear glide 307.7 to 336.7 ms, and the 50 ms
glides are lost on 6 and 17 of the seeds [synthetic]. Every unison figure
of the issue lies within one hop of some seed’s reading [synthetic]. #66
gives no unison phases, so its unison figures hold for its own signal
only [fact]. This track fits the curve instead:
it reads the rise of every 150 and 400 ms glide within 4.4 %, that 150 ms
fast-start unison included, and no glide at 50 ms [synthetic].
The acceptance bound is 5 % on the rise from 150 ms on, mono [fact]. The mono interval reads 6.9 semitones for 7: the track’s 10-cent bins round the start 5 cents up and the end 5 cents down [synthetic]. The fast-start unison at 150 ms reads its rise within 4.4 % but its at 1.83, and its time 11 % short; the low confidence says so [synthetic]. “In phase” voices start at phase 0, so the unison beats hardest [fact].
Other glides, mono [synthetic]:
| Glide | Read |
|---|---|
| 220 Hz down 4 semitones, 200 ms, 2.5 | -4.00 st, 184 ms, 2.26, rise -2.3 %, high; no drop |
| 82.4 Hz up 5 semitones, 50 ms | none, too short (fitted 61 ms) |
| 220 Hz down 4 semitones, 50 ms | none, too short (fitted 43 ms), no drop |
| From silence into 82.4 Hz, up 5 semitones, 150 ms, 1 | +4.70 st, 150 ms, 0.85, rise +1.9 %, low |
| From silence into 82.4 Hz, up 5 semitones, 300 ms, 2.5 | +4.48 st, 252 ms, 1.74, rise +1.0 %, low |
| 808 lines stepping up to 7 semitones, legato, driven or staccato | none |
| RC portamento from 110 Hz up 7 semitones, 50 ms | +6.9 st, 5.00, low, curve_bound |
| RC portamento from 196 Hz down 5 semitones, 80 ms | -5.0 st, 5.00, low, curve_bound |
The track’s first frames of a tone read low, so a glide from silence reads its interval up to 0.6 semitones short [synthetic]. Of 16 generated RC glides, 30 to 120 ms at 110 to 247 Hz, all read, at low confidence [synthetic].
Limits:
- A fall from silence whose stable pitch sits just past the 40 dB gate is not read: on real stems a fall of about 1.4 semitones over 200 ms, in which the first three partials all fall, settled at -40.9 dB and was lost [fact].
- A glide that the transcriber merges into one note, with its two ends in one note, is not read [fact].
- A fall from silence followed within 167 ms by a step back toward its start, to a pitch that no transcribed note matches, reads as a swing back and is lost [synthetic]. So is one whose portamento to the next note stalls under half the way for 100 ms: the stall forms its own stable pitch, which no note matches; a stall of 60 ms reads [synthetic]. So is one followed by a move back toward its start of under 100 cents to another note, past half the lead [synthetic]. A wobble at 3 to 4 Hz whose crest matches a neighbouring note still reads as a fall from silence: the crest comes at the window’s end, and the pitch returns after it [synthetic]. Of 49 generated wobbles of ±40 to ±70 cents at 3 to 8 Hz with the neighbour sounding, 10 read, all at 3 or 4 Hz [synthetic].
- A step between two 808 notes can raise
glide_too_shortwhen the fit finds 35 to 90 ms: one of three generated 808 lines does [synthetic]. - A vibrato of ±100 cents at 3 Hz holds each extreme long enough to read as a stable pitch, and its swings read as glides of 1.9 semitones, each at low confidence [synthetic].
- An 808 slide reads low: its stable pitches jitter like a unison’s [synthetic].
- A falling slide of 7 semitones from 49 Hz is not read [synthetic].
- A glide inside chords has no method here [fact].
- Short glides of 1 to 3 semitones on real stems mostly read low, yet an independent f0 confirmed most of them: their confidence is pessimistic [fact].
- A slide on the bound whose track keeps drifting for over 2.5 times its fitted time is not read: a 2-semitone slide near 69 Hz on a real stem, which an independent f0 confirmed, drifted for 4.3 times its fitted time [fact].
- Where the tracker reads a tone’s second harmonic, a glide still reads its interval: on a real stem a tone rose 2.2 semitones from 921 Hz, the track followed its second harmonic from 1845 to 2100 Hz, and the glide read +2.0 semitones [fact]. An independent f0 that jumped to another source there read a step [fact].
- A glide that moves more than 60 % of its interval inside an unvoiced gap is not read: a 7-voice unison with a fast start jumped 0.76 of it across 75 ms [synthetic].
Vibrato
- Spans. The voiced runs of the track split where two stable pitches lie 150 cents or more apart, so a step between notes never reads as a vibrato, while a vibrato of ±60 cents, whose stable pitches lie 120 cents apart, stays whole [proposal]. Each span loses 50 ms at each end and the frames where a glide moves [fact]. A drop inside a span splits it: the part before ends 50 ms before the onset, and the part after starts once the drop has settled, five time constants, at least 100 ms [fact]. A generated vibrato on A1, restruck mid-span with a fast drop the track does not show, reads before the drop; starting the whole span after the settling left 0.38 s, under the 0.5 s the search reads [synthetic]. A generated octave drop into A1 with = 60 ms, then a vibrato, reads that vibrato; skipping only 100 ms left a fifth of the fall in the span, and the search read none [synthetic].
- A glide moves where its fitted curve lies more than 10 cents, the smallest extent of a vibrato, from both its pitches [proposal]. A curve on the upper bound of comes within 3 % of its interval of its end halfway through its time [fact]. On a real stem a vibrato of 3.1 cycles at 5.9 Hz began where a 1.8-semitone glide on that bound ended; held for the glide’s full fitted time of 122 ms, its span after the trim fell from 0.53 to 0.47 s, under the 0.5 s the search reads [fact]. A generated glide and vibrato of that shape keep the vibrato [synthetic].
- A span of 0.5 s or more is searched [fact]. A least-squares line and sine are fitted together at each rate from 1 Hz, or 1.5 cycles over the span, to 20 Hz in 0.01 Hz steps; the rate whose sine explains the most power beyond the line wins, refined by golden-section search [fact]. Fitting the line and the sine together keeps a sine over a fraction of a cycle from leaking into the line [fact].
extent_raw_ctis the sine’s amplitude, half the peak-to-peak swing [fact].shareis the power the sine explains over the power left after the line alone [fact]. The power it explains is the line’s residual less that of the line and sine fitted together [fact]. The sine term alone can share power with the line: a 20-cent sine over three cycles under 20.6 cents of noise read a share of 0.524 that way and 0.490 by the residuals [synthetic]. On the generated sets the residuals drop 4 false vibratos of 26 and keep every true one [synthetic].- Rule. A vibrato counts at 3 to 12 Hz with 1 % slack, an extent of 10 cents or more after the correction, a share of 0.5 or more and 2.5 cycles or more over the span [fact, proposal for the share and the cycles]. Checked against an independent f0 on real stems, every vibrato of 2.5 cycles or more matched it; the shorter ones, 1.6 to 2.4 cycles at 3 to 4.7 Hz over spans of 0.5 to 0.8 s, did not, or matched under 2 cycles [fact]. Through the track the rates below read within 0.8 % of the truth, so the slack keeps a vibrato at either edge [synthetic].
- Correction. The track’s 93 ms frame smooths a fast swing, so the raw
extent falls with the rate [synthetic].
extent_factoris one over the share of the true extent the track reads, interpolated linearly between the measured rates and held at the ends [fact].extent_ctisextent_raw_cttimesextent_factor[fact]. - Confidence. Low when the kurtosis of the contour after the line, its
fourth moment over the squared second, is below 1.25: a sine reads 1.5,
a square wave 1.0 [fact, proposal for 1.25]. Low too on a pitch below
100 Hz [proposal]. Else high with 4 cycles or more in the span, else
medium [proposal]. The generated vibratos read a kurtosis of 1.36 to
1.66, a semitone trill at 6 Hz 1.06 [synthetic].
- On real stems every vibrato above 100 Hz with 4 cycles or more matched an independent f0 [fact]. The 23 vibratos the independent f0 found plausible read a share of 0.502 to 0.838: the 12 of them at high 0.502 to 0.630, the 10 at medium 0.519 to 0.838 and the one at low 0.575. The 12 vibratos it did not find plausible read 0.501 to 0.734, so the share does not tell the two groups apart [fact]. None read under 0.5: the lowest, 0.501, sits at the threshold, so the margin there is nil [fact].
- Below 100 Hz few matched, and the extent read up to 5 times too wide [fact]. A generated low saw with amplitude modulation, two beating voices or none reads no vibrato, and one with a ±30-cent vibrato reads it within 2 cents down to 41 Hz, so the cause is not known [synthetic].
The share of the true extent the track reads, on one 1 s mono saw at 220 Hz with ±35 cents per rate [synthetic]:
| Rate (Hz) | 3 | 4 | 5 | 5.5 | 6 | 7 | 8 | 10 | 12 |
|---|---|---|---|---|---|---|---|---|---|
| Share | 1.003 | 0.973 | 0.944 | 0.939 | 0.909 | 0.812 | 0.817 | 0.800 | 0.732 |
Each share comes from one signal, so the table carries that signal’s quantisation to the track’s 10-cent bins: 7 Hz reads lower than 8 Hz [synthetic]. The share belongs to this track: another pYIN implementation at another frequency ceiling reads another share [fact].
The #67 figures are re-run in
67-vibrato.json:
librosa 1.0.0 pYIN at frame 2048, hop 128, fmin 30 Hz and fmax 400 Hz on
the signals below, and the FFT peak of the detrended cents over the tone
less 50 ms at each end, 0.9 s [synthetic]. The table reads the extent as
the amplitude of the sine at that peak [fact]. #67 defines the extent as
half the peak-to-peak span, which equals that amplitude only for a clean
sine [fact]. With the amplitude, the ±35-cent mono saw reproduces the
issue: 5.50 Hz and 23.98 cents, a share of 0.685 against 0.69
[synthetic]. Half its peak-to-peak span reads 20.40 cents, a share of
0.58, which does not [synthetic]. This track reads the same saw at a
share of 0.939 [synthetic]. The unison and ±60-cent rates read 5.47 and
5.49 Hz against the issue’s 5.49 and 5.53 Hz, 0.3 % and 0.7 % low
[synthetic]. Over windows of 0.5 to 1 s they read 5.43 to 5.48 Hz and
5.46 to 5.49 Hz, so neither figure lies in its range [synthetic]. The
librosa share moves with depth and voices: 0.621 on the unison, 0.782 at
±60 cents [synthetic]. On the unison half the peak-to-peak span reads
29.63 cents against an amplitude of 21.73 [synthetic].
This track reads:
| Case | Read rate | Read extent |
|---|---|---|
| 5.5 Hz, ±35 ct, mono | 5.51 Hz | 35.0 ct |
| 5.5 Hz, ±35 ct, 7 voices over ±25 ct | 5.49 Hz | 33.5 ct |
| 5.5 Hz, ±60 ct, mono | 5.51 Hz | 60.8 ct |
| 2 Hz, 15 Hz, or 5 ct | none |
The acceptance bounds are 0.1 Hz on the rate and 15 % on the extent after the correction [fact]. The correction is measured at ±35 cents only; the ±60 case reads 1.4 % high with it, and ±25 cents at 9 Hz reads 11 to 14 % low [synthetic].
Limits:
- A trill between two notes a semitone apart at a vibrato rate reads as a vibrato, since its stable pitches lie 100 cents apart, but at low confidence: its contour jumps between two levels [fact].
- From 4.5 Hz the transcriber gives a ±100-cent vibrato as overlapping notes, the line reads as chords, and the layer is skipped [synthetic].
- A vibrato whose depth ramps in over the span reads its extent 9 to 51 % low, since the sine averages the ramp [synthetic].
- A vibrato of 3.5 Hz over 0.7 s holds 2.2 cycles and is not read [synthetic].
- A triangle LFO reads its extent 20 to 26 % low at high confidence: the fitted sine holds of a triangle’s amplitude, and the extent is the sine’s [fact, synthetic for the range].
- A vibrato whose rate sweeps from 4 to 6 Hz reads no vibrato [synthetic].
- A vibrato that starts late in a note is diluted by the steady part before it [fact].
Sources
- Glide floor and minimum length: Yang, Rajab and Chew, AVA: an interactive system for visual and quantitative analyses of vibrato and portamento performance styles, ISMIR 2016, CC BY 4.0. Inspiration only.
- Analytic signal: Boashash, Estimating and interpreting the instantaneous frequency of a signal.
- f0 track: Mauch and Dixon, pYIN: a fundamental frequency estimator using probabilistic threshold distributions.
Envelope layer
analyze reads the envelope layer after the pitch layer: the amplitude
envelope of each note, pooled over the notes, and the sidechain pump of
the stem. The layer writes the envelope group of layers.json; the
layer report page
lists its fields. The tags are those of the pipeline page.
The stages follow the envelope of a synth: an attack from the note-on to the peak, a hold at the peak, a decay from the peak to the sustain level, the sustain level, and a release from the note-off [fact]. The layer reads the attack, hold, decay and sustain, the time of the note-off knee, and the release and tail after it [fact].
Figures come from cargo xtask eval --layer envelope, 1191 generated parts
at 44,100 Hz scored against their recipes, or from the acceptance tests at
22,050 Hz, through the release build and with the true note list [synthetic].
The tables of the release and tail come from one-off generated sets, named
where they appear, that are not committed [synthetic]. A real stem adds filters, reverb, bleed and
separation artefacts.
The eval is local [fact]. It writes one file, the --json path, and only
when that flag is given [fact]. Run it with a release build; a debug build
of the library is far slower:
cargo run --release -p xtask -- eval --layer envelope --json records.jsonl
--json writes one record per part, so two builds compare record by
record [fact]. A part renders only when --part selects it, and the run
holds one part at a time: in a release build the whole set took 53.5 s
and 56 s of wall time and 0.6 GB over two runs, against 160 to 179 s and
4.4 GB before the change [synthetic].
Each check pairs a read note with its true note by onset, and a note the layer skips fails its check [fact]. A row that scores only the parts that read a value prints how many read none [fact].
What the layer reads
- The note list after the cleanup, or the notes that
--notesgives [fact]. A stem without notes getsvalues: nullwith the reasonno_note_listand the flagno_notes[fact]. - The mono mix at the native rate [fact].
- The two channels of the stem for the side vote of the tail, when it has two [fact].
- The beat grid of the used tempo, from 0 s, for the pump [fact]. The offset is never estimated: a stem whose first beat is late shifts every beat window [fact].
- A note with no harmonic to read, or that is too short for a track, is skipped and counted in the summary [fact]. A harmonic cannot be read when it lies within 30 Hz of a harmonic of a note it overlaps, or when its band reaches 0 Hz or the Nyquist frequency; the summary counts these as no free harmonic [fact]. A note whose amp proxy peaks at -120 dB or lower is skipped as silent: over a silent stretch, a listed note would read a 0 dB sustain at the tracks’ floor [fact, proposal for -120 dB].
- Every time is audio time from the start of the stem [fact].
Level tracks
Each note is read on a 1 ms hop from 30 ms before its onset to its end [fact]. Three tracks exist:
- Centred RMS over whole periods of the fundamental: one period for a note’s attack, the fewest whole periods that span 2 ms or more on a high note [fact]. One period gave the rise times the issue measured; two periods read a 16 ms rise at 55 Hz as 34 ms [synthetic].
- A sliding DFT of one harmonic over one period of the fundamental, for a note that overlaps no other [fact]. It has no filter transient, so the peak does not overshoot [fact]. A fourth-order low-pass on the band magnitude overshot the peak and read decays long [synthetic].
- The zero-phase band magnitude of one harmonic, for a note that overlaps another [fact]. The low-pass cutoff is half the distance to the nearest harmonic of another sounding note, or to the note’s own next harmonic, clamped to 12 to 40 Hz [fact]. The filter runs forward and backward, so the track does not lag [fact].
The amp proxy is the note’s lowest harmonic among the first three that lies 30 Hz or more from every harmonic of the notes sounding with it [fact]. A filter sweep moves the high harmonics and leaves the low ones, so the lowest free harmonic follows the amp envelope [fact]. On a 110 Hz saw under a closing low-pass, the sum of harmonics 1 to 3 read the fall 8 % short, and the fundamental alone within 10 % [synthetic]. Octaves and unisons leave the upper note no free harmonic; the layer skips it and has no residual method [fact].
Levels are dB from the note’s peak on the amp proxy, read no lower than -60 dB [fact].
Attack
attack_sis 1.25 times the 10 to 90 % rise of the linear amplitude: the full 0 to 100 % time of a linear ramp [fact]. The rise reads the RMS track for a note that overlaps no other, and the amp proxy for one that does [fact].- The rise ends at the attack’s top: the first frame from the onset that the level exceeds by less than 1 dB over as long again as it took to rise to it from under 10 % of it, and at least over the shortest rise the track can tell, the same figure as the upper bound’s below [fact]. Without that floor, a wobble of the window early in the rise read as a top [synthetic]. The 10 and 90 % shares are of the highest frame over that span [fact]. A slow attack has no top before its end, since a linear ramp keeps rising by more than 1 dB until 1 dB below its peak [fact].
- The peak of the whole note is no top: a real note swells or ripples after its attack [fact]. At 110 and 261.63 Hz, a 50 ms attack that swells by 3 dB over 600 ms read 514 ms against the peak and reads 52 to 55 ms against the top; at 261.63 Hz, a 10 ms attack under a 6 dB ripple at 5 Hz read 30 ms and reads 12.5 ms [synthetic].
- A swell finds no top when it rises 1 dB or more over the attack’s own length [fact]. A 300 ms attack under a swell over 0.8 s reads 324 to 370 ms up to 2.9 dB and 640 to 706 ms from 3.1 dB, at 55 to 440 Hz and 22.05 to 48 kHz; at 3 dB it reads either [synthetic]. The 50 ms attacks above lie far from that edge [synthetic].
- The factor of 1.25 holds for a linear rise, so a rise that flattens near its top reads long [fact]. Loud isolated bass notes on real stems read 141 and 156 ms where the level crosses -1 dB at 103 and 105 ms and -3 dB at 72 and 42 ms, on 9 and 22 notes [fact]. Loud isolated keyboard and synth notes on real stems read 15 and 20 ms against -1 dB crossings at 18 and 14 ms [fact].
- A periodic wave gives one amplitude sample per period, so a rise under
about 1.5 periods cannot be told from a shorter one [fact]. The attack
is an upper bound when the rise is shorter than 1.5 RMS windows, or, on
the amp proxy, shorter than 0.98 over its cutoff, which is twice the
rise of the filter on an instant step [fact].
attack_is_upper_boundis set when more than half the pooled attacks are upper bounds [fact].attack_sthen still reports the read, and a preset should not take it [fact]. Each note record carries its ownattack_upper_bound[fact]. - #69 writes an attack from 1.5 periods. The layer tests the rise, 0.8 of a linear attack, so its flag covers attacks up to about 1.9 periods, which is 45 ms at 41.2 Hz [fact]. It departs from #69 on the safe side: a 2.2 period attack is the shortest in the tests that reads its time [synthetic].
The attack ladder: one saw note with a linear attack, read attack in ms,
bound for an upper bound [synthetic]:
| f0 | 1 ms | 5 ms | 10 ms | 20 ms | 50 ms | 100 ms | 300 ms |
|---|---|---|---|---|---|---|---|
| 41.2 Hz | bound | bound | bound | bound | 53.3 | 95.9 | 285.2 |
| 55 Hz | bound | bound | bound | bound | 50.8 | 94.1 | 295.3 |
| 110 Hz | bound | bound | bound | 19.4 | 47.5 | 100.7 | 297.5 |
| 220 Hz | bound | bound | 9.4 | 18.7 | 50.5 | 99.0 | 300.1 |
| 440 Hz | bound | 5.2 | 9.5 | 20.0 | 49.6 | 99.9 | 299.6 |
Every attack over 4 periods reads within 15 %, and every one under 1.5 periods reads as a bound [synthetic].
Hold, decay and sustain
hold_sis the time from the first frame within 1 dB of the peak to the start of the decay, 0 when under 10 ms [fact, proposal for 10 ms].decay_sis the time between the first frames after the peak at which the level falls through 10 % and 90 % of the sustain level in dB, linear between frames, divided by the gap between the two shares [fact]. The first share is at least 0.5 dB down, so ripple at the peak does not end the hold, and it rises above 10 % for a sustain shallower than -5 dB [fact]. The decay starts that share of its time before the first share’s crossing [fact]. A regression in dB from the peak read the decay 6 to 22 % long, since the knee at the sustain level bends the line [synthetic].sustain_dbis the median of the last 30 % of the held part [fact]. The held part runs from the first frame within 1 dB of the peak to the note-off knee, or to 25 ms before the note’s end without one [fact].- Decay and sustain are null when a note with a knee holds for less than the attack, the hold and three decay times [fact]. A note without a knee, such as a pluck, sustains at its end level [fact].
fit_residual_dbis the median over the pool of the RMS in dB of each held part against the model: 0 dB over the hold, a fall linear in dB to the sustain level over the decay, then the sustain level [fact].ripple_dbis the median, over the pooled notes whose sustain is observable, of the 90th minus the 10th percentile of the sustain window after its line [fact]. Unison and chord beating show here [fact].- A Levenberg-Marquardt refine of a piecewise linear model over the pooled dB curve, with a floor, a peak and a fixed note-off, is not applied [fact]. Fitted from each note’s top with the peak free, it moved the decay error on 72 lines with decays of 50 to 600 ms from a median of 4.4 % and a 90th percentile of 8.7 % to 9.9 % and 26 %, and left the median sustain error at 0.24 dB [synthetic]. A fit in dB trades the two ends of a fall against each other, which the read at 10 and 90 % of the fall does not [fact].
On a grid of 240 lines of eight saw notes, decays of 50 to 600 ms, sustain levels of 0 to -24 dB, holds of 0 and 30 ms and releases of 20 to 300 ms [synthetic]:
| Decay law | Decay read over true | Sustain error | Hold, true 30 ms | Fit residual |
|---|---|---|---|---|
| Linear in dB | 0.925 to 1.015 | at most 1.1 dB | 28.5 to 30.5 ms | 0.02 to 0.63 dB |
| Linear in amplitude | 0.88 to 1.0 | at most 0.51 dB | up to 130 ms | 0.02 to 1.55 dB |
A decay linear in amplitude starts slowly in dB, so its first 0.5 dB reads as hold: up to 104 ms with no true hold [synthetic]. The fit assumes a decay linear in dB, and its residual rises on the other law [fact].
Plucks on 55 and 220 Hz with an exponential decay and no note-off knee sustain at their end level: -57 dB for a time constant of 150 ms, -21.5 dB for 400 ms and -8.6 dB for 1 s [synthetic].
Note-off knee
The knee is the time the note-off starts the release [fact]. It is read on the linear amplitude of the amp proxy, from the first frame within 1 dB of the peak to 25 ms before the note’s end [fact]:
- At each split, a line over up to 100 ms before it and one over up to 200 ms after it are fitted against one line over both [fact]. The right line stops where the level falls under 2 % of the mean before it, so silence after the release does not flatten it [fact].
- A split counts when the right line is steeper by half the mean before it per second, falls by half that mean over its side, and the two lines leave at most half the squared error of one [fact, proposal for the shares].
- Splits that count in a row form a corner, and within a corner the split with the largest gain wins [fact]. A hold that breaks into a linear decay forms an earlier corner; the largest gain over all splits took it for the note-off [synthetic].
- A later corner replaces an earlier one unless it looks like a fade of a
reverb tail, whose fades form corners too [fact]. Taking the last corner put
a knee over 1 s past the note-off [synthetic]. Two tests tell a fade from a
note-off [fact, proposal for both]:
-
The level between the corners bends: the median size of its second difference in dB, 10 ms each way, is over 0.4 dB. A decay and a sustain are near 0 dB, as both are straight in dB, however short the sustain.
-
The level after the corner rises by more than a twentieth of the mean before it. A note-off falls for good. The rise reads the first 20 ms as they are and the mean over every 20 ms span, since a noise floor wobbles over the silence after a release [fact, proposal for 20 ms]. The rise and the read of the fall stop before a next note that overlaps, by the frames that the band’s filter rings ahead of its attack, and half a level window before the end of a note that overlaps no other [fact]. A later corner past the stop reads no rise, so it replaces an earlier one whenever the bend allows it [fact]. That keeps the note-off of a note held over a shorter one [synthetic].
-
A later corner past the stop replaces nothing when the note has ended and the level has come back [fact]. The note has ended when the mean over the last 10 ms before the stop lies under a fiftieth of the earlier corner’s level, -34 dB, the floor of the release side. The level has come back when the mean over the 100 ms before the later corner lies at least a quarter above it, and the left line’s end lies above it too [fact, proposal for the quarter]. The corner is then the next note’s. Without this test, a long gap after a note-off took the next note’s corner, since the bend over silence reads near 0 dB (#312) [synthetic].
-
Each read guards one case [synthetic]. A held sustain under the floor, with its note-off past the stop, reads a mean near 1 times the stop level: a ripple of 1 % or a rise of 1e-6 a frame must not decide it, so the mean must lie a quarter above. The 10 ms mean at the stop keeps a ripple of 25 % over 10 frames from deciding it too. A transcribed note on the generated tally rises over the last 10 frames into its note-off: its line ends above the stop level, while its mean reads 1.08 times it. A note that falls straight into a note-off past the stop has a mean over a quarter above, while its line ends under it. A next note after a silent gap reads mostly 1 to 3 times the stop level on the bass line below. A quarter keeps 8.9 % of the long notes late under the pump there, against 7.1 % at a tenth and 13.0 % at a half.
-
Under a pump that strips, the test also reads the note’s level as it was before the strip, and the later corner replaces nothing when either level shows the note ended and the level come back [fact]. The strip lifts the frames in a beat’s dip by up to the pump’s depth, and the last 10 ms before the stop can lie in a dip while the next note’s band already rings ahead of its attack. Read on the stripped level alone, the stop then lay over a fiftieth of the hold, or the next note’s corner lay under a quarter above the stop, and the knee took the next note’s corner (#323) [synthetic]. On the long notes of the bass line below, the strip put 156 knees over 15 ms further off than the same signals read unstripped, by a median of 0.56 s [synthetic]. Read on the level before the strip alone, the ring in the dip can come back by under a quarter there while the stripped level comes back by more: under a 12 dB pump with list ends 5 ms past the next onset, 2 of 2137 notes then read about 0.5 s late [synthetic].
-
Notes on the signals that strip, against the same signals generated dry, mean error and notes over 150 ms off [synthetic]:
Pump, notes, list end past the next onset Notes Dry Test on the stripped level This rule 5 dB, 1.37 to 2.74 s, 20 to 200 ms 2757 40.8 ms, 148 66.2 ms, 249 33.9 ms, 117 5 dB, 0.34 to 1.37 s, 20 to 200 ms 2818 10.9 ms, 35 14.5 ms, 53 10.4 ms, 28 5 dB, 1.37 to 2.74 s, 5 ms 2748 17.9 ms, 56 24.3 ms, 83 20.1 ms, 63 5 dB, 0.34 to 1.37 s, 5 ms 2854 5.8 ms, 11 8.0 ms, 17 7.3 ms, 13 8 dB, 1.37 to 2.74 s, 5 ms 2065 19.8 ms, 47 29.4 ms, 74 22.7 ms, 53 12 dB, 1.37 to 2.74 s, 5 ms 1913 17.9 ms, 39 37.9 ms, 86 23.9 ms, 45 24 dB, 1.37 to 2.74 s, 20 to 200 ms 1885 41.3 ms, 101 314.3 ms, 853 96.5 ms, 253 Only the 5 dB pump with list ends 20 to 200 ms past the next onset reads no worse than dry. With list ends at the next onset, no note changes. No note reads over 15 ms worse than the test on the stripped level, except one at 24 dB, which reads 0.43 s early against 0.40 s late: there the stripped track holds no corner at the note-off, and both knees take another corner [synthetic]. The strips do not change [synthetic].
-
- When the level stays at 85 % or more of the left line’s end for over 10 ms more, the knee is where a line over its fall from there to half that level crosses that level [fact, proposal for 85 %]. A hold that wobbles under a reverb can win a split 50 to 100 ms before the note-off, and an exponential release can win one past it [synthetic].
- Of the eval’s knees within 15 ms, a sustain shorter than its decay reads 120 of 144 and a dry tremolo with a release of 200 ms or more reads 12 of 64, against 108 and 9 at commit 057d63c, which took the last corner [synthetic].
- The knee is where the left line crosses a line over the first 10 ms after the split, or the split itself when the crossing lies outside the two lines [fact]. A step down followed by a slow fall crossed 20 ms before the search started [synthetic]. The crossing of the two fitted lines read a linear release late, since the right line averages the release’s tail [synthetic].
- No split counts on a pluck that decays at one rate, and its knee is null [fact].
note_off_offset_s is the note’s end in the note list, or the stem’s end when that
comes first, minus its knee,
over the pooled notes with a knee [fact]. On the grid, all 1920 knees lie
-2.0 to +0.01 ms from the true note-off [synthetic]. Read in dB, a 200 ms
linear release placed the knee 168 ms late, because a linear fall in
amplitude bends in dB [synthetic].
Release and tail
After the knee, relative to the median level over the 50 ms before it, or since the note’s onset when that is nearer, the level follows a linear release plus a decaying tail [fact]:
env(t) = max(1 - t / release_s, 0) + tail_weight * 10^(-3 t / tail_rt60_s)
- The fit is a simplex on the squared error in dB, from 4 releases and 3 RT60s as starts, down to -55 dB [fact]. It keeps every frame for 200 ms and every fifth after that, weighted to count the same [fact]. The project has no least-squares crate, so it uses the simplex of the pitch fits [fact].
- The fit agrees with
scipy.optimize.least_squareson the curves of the golden vectortail-fit, within 1 % and 2 ms or 0.002 dB [synthetic].cargo xtask goldenregenerates it [fact]. - It reads the one-period RMS, not the amp proxy: a reverb of one harmonic fades deeply, and many harmonics fade apart [synthetic]. A note that overlaps another reads the band of the amp proxy [fact].
- It stops at the note’s end or the next note that overlaps it, half a level window before that note [fact]. At 41 and 55 Hz that window otherwise reads the next note and left residuals of 6 dB on dry lines [synthetic].
- A note with no knee has no fit, so a pluck reports no release [fact]. A given note list that ends at the note-off shows no knee and reads no release [fact].
The write rule reads the medians over the pooled notes with a fit, and never the release, which is what fails to fit [fact]:
- A
tail_weightover 0.5 or a residual over 3 dB writes norelease_s, andrelease_confidencereads low [fact, proposal for 3 dB]. Both still report the tail [fact]. - A written
release_sreads low, withtail_wobble, when the median residual is over 2 dB and the median second difference of the level in dB over the 300 ms before the knee, 25 ms each way, is over 0.3 dB [fact, proposal for all three]. A tremolo carries on through the release, which the fit cannot follow [synthetic]. Of 2000 dry tremolos of 0.3 to 3 dB at 1 to 12 Hz, 727 read medium and 139 of them lay over 50 % off before this rule. It lowers 96 of the 139 wrong ones and 141 of the 588 right ones, which leaves 43 of 490 [synthetic]. On 2000 stems each it lowers 18 wrong and 11 right short sustains, and 3 wrong and 3 right reverbs, 1 wrong and 3 right on the first 1000. It lowers none of the plain dry or legato stems and 2 wrong chords [synthetic]. The rest of the move in the reverb figures below comes from the knee rule [synthetic]. Its effect depends on the recipe: another set of generated tremolos, with a wrong share of 23 %, lost 66 of its 637 right and 63 of its 189 wrong medium releases, for a wrong share of 18 % [synthetic]. tail_rt60_sreads over the notes with atail_weightof 0.05 or more whose frames span a 15 dB fall of it, and is null, withtail_light, under a mediantail_weightof 0.05, because a dry note often leaves a little weight whose RT60 is noise [fact, proposal for both].tail_rt60_sis null, withtail_smooth, when the mediantail_weightis over 0.5 and the median residual is at most 1.1 dB [fact, proposal for 1.1 dB]. A dry exponential release fits the tail term alone, at a weight of about 1 and an RT60 equal to its fall, while a reverb’s level wobbles [synthetic]. Of 15 dry falls of 60 dB over 0.1 to 1.5 s at 55 to 220 Hz, 14 read this way, and the fall of 0.1 s at 55 Hz stays a false RT60 at a residual of 2.7 dB [synthetic]. A curved release model waits for the calibration of the decay law under #69 [fact].tail_rt60_sis null, withtail_band, when a note that overlaps another reads a median RT60 under 6 periods of its narrowest band’s cutoff [fact, proposal for 6]. The band rings by itself: a dry chord with a release of 20 or 50 ms read 0.13 to 0.2 s on 18 of 18 stems before this rule and on none after it [synthetic]. The same chords through a reverb read an RT60 on 13 of 18 stems at a true 0.3 s and 36 of 36 at 0.6 and 1.5 s [synthetic].release_confidenceis null when no pooled note has a fit [fact].
The set: 4000 stems sampled over 41 to 440 Hz, releases of 20 to 800 ms, dry or with an RT60 of 0.3 to 3 s at 0.05 to 0.8 wet, three timbres, 1 to 6 notes of 0.3 to 1.2 s, gaps of 0.2 to 1.5 s, sustains of 0 to -12 dB, mono or wide reverbs, and a reverb whose highs fade first [synthetic]. Shares hold within about 2 percentage points at this size. On it:
| Stems | Count | Release written | Within 50 % of the truth |
|---|---|---|---|
| Dry, release up to 400 ms | 1157 | 1152 | 1108 |
| Dry, release 800 ms | 215 | 0 | - |
| Reverb, release up to 400 ms | 2227 | 1418 | 1302 |
| Reverb, release 800 ms | 401 | 28 | 4 |
- The median written release over its truth on dry stems is 0.98 to 1.12 by release, with the 1.12 at 20 ms [synthetic].
- Over the true RT60, the median read is 1.16 at 0.3 s, 1.02 at 0.6 s, 0.95 at 1.5 s and 0.95 at 3 s, and 33 to 44 % lie within 10 % [synthetic].
tail_weightover 0.5 holds out 4.8 % of stems at a true 0.05 wet, 30 % at 0.4 and 55 % at 0.8 [synthetic]. Of the 661 reverb stems over 0.5, 79 read as smooth and lose their RT60 [synthetic].- A knee lies within 15 ms of the note-off in 98.8 % on dry stems and 69.6 % under a reverb, against 98.8 % and 42.1 % at commit 057d63c [synthetic].
- On a line of 6 notes with a release of 50 ms at 0.15 wet, over 60 noise reverbs each, the RT60 lies within 10 % on 53 of 60 at 0.6 s and 56 of 60 at 1.5 s, with misses from 0.529 to 0.692 s and from 1.289 to 1.768 s, and the release reads 43 and 44 ms [synthetic].
Votes that a tail is reverb
tail_votes counts five signs, each over the pooled notes with a fit and by
majority where it reads notes, and null where nothing could be read [fact]. All
thresholds are proposals [fact].
| Vote | True when | Reads true, dry / reverb |
|---|---|---|
outlasts_gap | Before the next note, past the release, with no other note sounding, the level lies above -40 dB of the note body | none read / 45 % |
side_grows | Side over mid grows 3 dB or more from the body to the tail | none read / 83 % |
flatness_rises | Flatness rises 0.1 or more from the body to the tail | none read / 87 % |
same_rate | The RT60 of 3 or more notes that show one spreads under a quarter of its median | 88 % of 25 / 86 % |
high_band_faster | The share of the bands from 1 kHz falls 3 dB or more over the tail | none read / 37 % |
A dry stem reads none of the first three, as its tail ends with the release
[synthetic]. Two or more votes pass on none of the 1372 dry stems and on 68 % of the
2628 under a reverb [synthetic]. A noise floor reads as a tail: on dry lines at 55 to
220 Hz under noise at -50 and -40 dBFS, 54 of 54 stems pass two votes [synthetic]. tail_is_reverb stays false: a reverb is stated only
when the votes agree and a space layer reads medium or better, and no space layer
exists yet [fact].
Pooling and confidence
- The pool leaves out a note whose peak lies 12 dB or more below the peak of a note it overlaps: a quiet second voice, such as another part’s bleed or a ghost note, has an envelope of its own [fact, proposal for 12 dB]. Eight A2 notes decaying over 300 ms, each over an E3 18 dB quieter decaying over 50 ms, read the A2 decay within 20 % with the gate; the E3 notes would otherwise fill half the pool [synthetic].
- Of the other notes, the pool holds those whose length to the knee, or to the end without one, lies within a factor of 2 of their median length, and whose pitch lies within 12 semitones of their median pitch [fact, proposal].
- Each statistic is the median and the interquartile range over the pool
[fact]. It reads low over fewer than 3 notes, or with an interquartile
range above half its median [fact]. Its reasons are
too_few_notesandrelative_spread, and the attack addsupper_bound_share[fact]. A median of 0 under an interquartile range above 0 has no ratio, so the statistic reads low withzero_median, that range, in place ofrelative_spread[fact]. level_spread_dbis the 90th minus the 10th percentile of the full-band levels of the pooled notes, null under 3 pooled notes with a level and always low: no stem has tested the rule [fact, proposal]. A note’s full-band level is the peak of the centred RMS of the mono mix over one period of its fundamental, on a 1 ms hop, from its onset to its note-off knee, or to 50 ms before its end in the list without one, never past the next later onset on a mono line [fact]. A mono line is one whose voices layer reads at most one voice at a time, or counts none, when--profile paddid not run the notes step; so the layer reads the levels after the voices layer [fact]. The amp proxy’speak_dbreads one harmonic per note, so two notes on different harmonics do not compare; the full band does (owner decision, 2026-11-05) [fact]. Notes 0, 3, 6 and 12 dB apart read 0.1, 3.0, 6.0 and 12.0 dB [synthetic]. On the generated chords of equal notes, the amp proxy read 4.6 to 7.8 dB and the full band reads 0.9 dB [synthetic]. On a chord the full band is the mix, so it cannot tell two chord tones of different levels apart [fact].- The group’s own confidence is medium with 3 or more pooled notes, else low, and no estimate when no note was read [fact].
Sidechain pump
A sidechain compressor keyed by a kick ducks a held part on every beat and lets it recover until the next [fact]. The layer reads the dip on the beats that one held note covers, and strips it before it reads the notes [fact].
-
Track. The full-band centred RMS in dB over two periods of the lowest note, on a 1 ms hop [fact].
-
Beats. A beat of the grid counts when the held part of one note covers it from 50 ms or more before its start to its end [fact]. A pluck struck on the beat never counts, nor a release at the end of a beat [fact]. The held parts come from a first read of the notes without the pump, and the notes are read again with it stripped [fact]. Plucks on every beat read 13.9 dB of ducking without this rule [synthetic].
-
Curve. Each counted beat is binned into 64 phase bins and taken relative to the 90th percentile of its bins; the curve is the median over the beats [fact]. The pump needs 4 counted beats; with fewer it is null [fact].
-
Period. A sidechain follows the song’s tempo, and a stem’s tempo may be off by 1 or 2 % [fact]. Over 32 beats a 1 % error moves the duck a third of a beat, so its beats no longer fold onto one curve [fact]. The layer searches periods within 2.5 % of the grid’s for the one whose counted beats fold onto the deepest mean curve, over up to 64 beats of the longest run of counted beats and then over all of them, in steps that move the last beat of the span by 1 bin [fact, proposal for 2.5 %]. The second stage searches one step either side of the first, so the reach follows the longest run of counted beats: up to 2.73 % at 4 beats, 2.81 % at 5, 2.60 % at 6, 2.68 % at 7, 2.66 % at 10, 2.58 % at 20 and 2.51 % at 64 [fact]. Of 72 ducks up to 2 % off the grid over 32 and 64 beats, 15 locked and stripped on the grid alone, and all 72 do with the search [synthetic].
pump_bpmreports the tempo of the read that stays [fact]. The search’s read must lock to the beat, and it replaces the grid’s read as follows [fact, proposal for 0.9, 0.15 dB, 60 cents, 3 bins and half a window]:- The grid’s read follows the notes: always, with no shape test.
- The grid’s read locks to the beat: when both reads strip or neither does, the search’s fold error must be under 0.9 of the grid’s and 0.15 dB lower. The fold error is the standard deviation of the level minus the curve over the counted beats, each beat about its own mean, so a fade or a level step between beats does not count as misfit [fact]. When only one read strips, as a slower period counts fewer beats, the search’s read must be the one that strips and have the lower fold error. A 12 dB duck 2.5 % off over 16 beats reads 4.4 dB on the grid and 9.8 dB at its own period [synthetic]. Depth is no test: the search deepens a duck on the tempo by fitting a chord’s beating, and on the flat level of the 76,836 stems of Limits a gain of 1.5 dB moved 15 ducks on the tempo, against 3 with this rule [synthetic]. A sub’s fold error is small enough that a share alone moved ducks on the tempo at 174 BPM, and the sustain with them by up to 2.6 dB [synthetic].
- The grid’s read runs free: when the line lies within 60 cents of a division, and the search’s curve falls faster than it recovers and takes long enough from half its depth back to a quarter: 3 bins, and half the level window in bins. A gate near a division folds onto one curve as well, but comes back at once, within the smear of the window [synthetic].
- The grid’s read reads none: when the search’s curve falls faster than it recovers and takes 3 bins from half its depth back to a quarter. A gate off the tempo often reads no lock on the grid, and the level window’s share is left out because it costs real ducks under a low note; see Limits [synthetic].
The set behind the numbers below holds 123,840 generated signals: a three-note chord from 55, 110 and 220 Hz, 60 to 174 BPM, runs of 8.9 to 32 beats, four phases for ducks and two for gates and saw tremolos. Each cell counts the signals that strip, from one build. The columns differ only for a grid read that runs free: it stays free, it takes any search read that locks, or it takes one under this rule. A free read never strips, so this rule reads no more than the column before it [synthetic].
Signal Off the tempo Signals Free read stays No free rule This rule Duck, release 80 ms or more up to 2.5 % 47520 36008 36107 36078 3 % 8640 1343 5028 4883 3.5 % 8640 169 4132 1716 4 to 5 % 17280 122 4717 128 Duck, release 30 ms up to 2.5 % 15840 7440 7462 7453 3 % 2880 622 801 734 3.5 % 2880 426 612 459 4 to 5 % 5760 160 287 160 Square gate 3 % 2880 21 768 25 3.5 % 2880 42 667 47 4 to 5 % 5760 14 603 14 Saw tremolo 3 % 720 71 498 498 3.5 % 720 0 352 148 4 to 5 % 1440 0 293 0 The first read’s strips on gates come from their own lock on the grid. A gate up to 2.5 % off strips either way; see Limits [synthetic].
-
pump_depth_dbis minus the curve’s minimum [fact]. -
pump_recovery_sis the time from the minimum until the curve comes back within 1.5 dB, going on over the beat’s wrap, so a duck that sits late in the beat recovers into the next [fact]. It is null when the curve does not come back within one beat [fact]. -
pump_tau_msis the recovery over , the time constant of an exponential recovery in dB, and null unless the pump locks to the beat: a dip that follows no beat has no recovery to model [fact]. -
pump_beatscounts the beats behind the curve [fact].
pump_locked_to names what the dip follows, tested in this order
[fact, proposal for the thresholds]:
note: the beats with a note onset inside dip 3 dB or more, and the others under 1.5 dB, each group of 2 beats or more. An onset up to 50 ms before a beat belongs to that beat, as the transcriber places onsets a few milliseconds early [fact].free: the level swings 1 dB or more at a rate of 0.5 to 20 Hz that lies 50 cents or more from every note division, read on the modulation spectrum over the longest covered span [fact]. It comes before the beat: a 5.3 Hz tremolo over 17 beats at 128 BPM left a 3.6 dB dip in the median curve that passed the beat test [synthetic]. A duck’s strongest line lies on the beat rate, so it reads free when that rate lies 50 cents or more from every division of the grid [fact]. That holds 3 % off the tempo, and over a short span from 2.5 %, where the line reads 50 to 52 cents [synthetic]. The period search then decides, as above [fact].beat: the curve dips 3 dB or more in one run of bins half its depth down, and falls into its dip within 8 bins, reading on into the beat before and not round the beat’s wrap. The odd beats and the even beats must each do the same on their own median curves, to half its depth at its dip. Half the beats must fall by half its depth into its dip: from their highest bin over the 8 bins before the 3 bins either side of it to their lowest within those. A note that decays across the beat falls over most of it, and fails the 8 bins [fact]. The one run stops a tremolo at a division of the beat [fact]. A level that rises over each bar or two beats and drops back makes every beat’s curve start low, and round the wrap that read as a fall on every beat: 12 dB per bar and 6 and 12 dB per two beats all stripped [synthetic]. Those falls only come on the first beat of each bar or pair, so the rule reads no pump there [synthetic].noneotherwise.
The layer strips the curve from the note tracks only when it locks to the
beat over 8 counted beats or more, and sets pump_stripped [fact,
proposal for 8]. A stem can duck in one section only, so a note takes the
strip only when the median fall of the beats over it reaches half the
curve’s depth [fact]. With fewer beats, half of them is two or three, and
other sounds on the beat pass the lock: a sine sub under a kick on every
beat, whose tail beats against the sub, read 3.4 to 4.6 dB locked to the
beat over 4 or 5 beats [synthetic]. On 1375 runs over the generated stems
of the pitch layer, which hold no duck, 4 read such a lock, and none is
stripped [synthetic].
A duck’s dip can read as a note-off knee in the first read and cut the held parts short [synthetic]. A read of the held parts that finds no pump, or one that locks to the beat over fewer than 8 beats, is therefore counted again over each note to 25 ms before its end, and strips when that count locks to the beat over 8 beats or more [fact]. A 6 dB duck on the chord at 174 BPM under noise at -20 dB read a false knee on each of its three notes and was not stripped; counted over the notes, it strips, and no knee remains [synthetic].
A constant three-note saw chord reads 1.64 to 1.94 dB from its own beating, so the 3 dB floor sits above it [synthetic]. Over all 1191 parts, all 826 locks read as built [synthetic]. A part with no pump reads as no lock, and a 3 dB duck counts as right when it locks to the beat or reads no pump, never when it reads free [fact].
Depth bias
The read depth is shallower than the duck, and the bias depends on the duck’s shape [synthetic]. A duck whose gain recovers exponentially, , has a sharp minimum in dB; the RMS window and the 64 bins round it off [fact]. A cosine dip has a round minimum and reads near its depth [synthetic].
Measured minus true depth on a held saw chord and a held 55 Hz sub, two seeds, noiseless and with white noise at -30 and -20 dB, releases of 50, 80, 150 and 300 ms; median and range over 48 parts per cell [synthetic]:
| Depth | 90 BPM | 128 BPM | 174 BPM |
|---|---|---|---|
| 3 dB | -0.46 (-1.18 to 0.50) | -0.48 (-1.12 to 0.51) | -0.63 (-1.39 to 0.81) |
| 6 dB | -1.39 (-2.69 to -0.32) | -1.45 (-2.60 to -0.39) | -1.75 (-2.83 to -0.25) |
| 9 dB | -2.65 (-4.57 to -1.07) | -2.77 (-4.39 to -1.13) | -3.03 (-4.68 to -1.41) |
| 12 dB | -4.29 (-6.75 to -1.91) | -4.18 (-6.45 to -2.03) | -4.69 (-6.79 to -2.42) |
| 18 dB | -8.39 (-11.76 to -3.62) | -8.03 (-11.29 to -4.31) | -8.40 (-11.63 to -4.87) |
- No duck reads 0 to 0.09 dB on the sub, and 1.64 to 1.94 dB on the chord from its beating [synthetic].
- Noise at -20 dB fills the dip: a 12 dB duck reads 1.5 dB shallower than without noise [synthetic].
- At 128 BPM on the noiseless chord, the bias over the releases is -0.2 to -5.5 dB for a duck linear in dB, -0.3 to -7.2 dB for one with a 5 ms fall, and +1.2 to -2.6 dB for a cosine dip [synthetic].
- The bias changes with the shape at the same depth, so no single factor corrects it, and the layer reports the depth as read [fact].
The recovery over the true time back within 1.5 dB, at depths of 6 dB or more, is 0.64 to 1.44 by release on the chord and 0.55 to 0.80 on the sub [synthetic]. The shallower read reaches 1.5 dB sooner on the sub, and the chord’s beating moves the crossing [synthetic].
Limits
-
An attack under about 1.9 periods of the fundamental is an upper bound, and at 41.2 Hz that covers attacks up to 45 ms [fact].
-
A decay linear in amplitude reads part of its start as hold [synthetic].
-
The upper note of an octave or a unison has no free harmonic and is skipped [fact].
-
A resonant filter that closes onto the fundamental changes the amp proxy itself [fact].
-
A release that falls, flattens and falls again reads its knee at the second fall, since a later corner replaces an earlier one to pass over a decay [synthetic]. Its sustain window then reaches into the first fall [fact].
-
A release over about 400 ms shows no knee, because its right line must fall by half the held level over 200 ms: no dry 800 ms release reads [synthetic]. Under a reverb, 28 of 401 such stems wrote a release, 4 of them within 50 %, from a knee in the tail [synthetic].
-
Under a reverb, 30.4 % of the knees on the 4000 stems lie over 15 ms from the note-off [synthetic]. A fade in the tail with a smooth level before it and no rise after it still replaces the note-off [fact].
-
A tremolo bends too: a swing of 1 dB at 12 Hz reads 0.38 dB and 6 dB at 5 Hz about 0.4 dB. When an earlier decay forms a corner, the note-off of such a sustain can lose to it [fact].
-
The bend and rise rule loses some notes that commit 057d63c read within 15 ms, on a one-off sweep of 1000 generated stems a set, and gains more [synthetic]. Commit 2d63f2e holds that rule with its stop, and the last column adds the test of the level at the stop. Worse and better count the notes of the last column against 057d63c:
Set Notes 057d63c 2d63f2e This rule Worse Better Plain dry 2986 2453 2453 2453 0 0 Short sustain 2698 2017 2064 2064 0 47 Tremolo 2991 732 777 777 107 152 Legato 3011 2962 2967 2967 0 5 Reverb 3042 1220 1662 1662 25 467 Chord 1176 820 841 861 6 47 Noise at -35 dBFS 2898 2010 2065 2065 82 137 Noise at -40 dBFS 2898 2200 2215 2215 33 48 Noise at -50 dBFS 2898 2286 2315 2315 0 29 Noise at -60 dBFS 2898 2306 2327 2327 0 21 Legato, note end 10 ms past the next onset 1583 1194 1201 1222 0 28 Legato, note end 20 ms past the next onset 1583 1220 1224 1229 1 10 Legato, note end 40 ms past the next onset 1583 607 662 1091 0 484 No note of any set reads worse than at commit 2d63f2e [synthetic]. Every column comes from the same sweep, run at each commit. The three legato rows list every note up to the next onset and the time named, on the legato notes of the plain set. They replace rows read on an earlier version of the sweep, which this one does not reproduce at commit 057d63c: 1227, 1172 and 1015 there.
-
On the same sweep, 600 stems a row and 964 notes, a note list that runs further past the next onset [synthetic]:
List end past the next onset 057d63c 2d63f2e Worse This rule Worse 120 ms 378 412 0 639 0 220 ms 386 426 0 675 0 420 ms 387 431 0 669 0 Worse counts against 057d63c. These rows replace a table read on an earlier version of the sweep, which put 604, 609 and 589 at commit 057d63c. The rows without the stop are not measured again.
-
Other generated recipes still read knees over 15 ms worse than commit 057d63c [synthetic]:
Recipe Notes Worse Better Mono bass line at 175 BPM, 14 notes a signal, under a 5 dB pump left unstripped, see #313 3570 11 2652 The same line, medium notes, see #313 3530 31 2401 A tremolo under a reverb 1500 42 270 A next note 15 dB louder 1419 3 - -
On that bass line, with a gate of 55 to 90 % and list ends 20 to 200 ms past the next onset, over 500 signals, knees over 15 ms late [synthetic]:
Notes 057d63c 2d63f2e This rule Stripped notes tested before the strip 1.37 to 2.74 s, under the pump 81.4 % 53.9 % 8.9 % 5.2 % 0.34 to 1.37 s, under the pump 61.4 % 11.6 % 4.2 % 3.5 % 1.37 to 2.74 s, dry 80.9 % 53.5 % 5.4 % 5.4 % 0.34 to 1.37 s, dry 60.8 % 11.3 % 3.2 % 3.2 % Both pump rows above and these rows come from this line. Notes count where both commits read a knee. The last column tests a stripped note’s level before the strip, see the rule above (#323). The long-note row replaces one read on another version of the line, 3038 notes with 44 worse, which this one does not reproduce.
Dry, no note reads worse than at commit 2d63f2e. Under the pump, 25 long and 25 medium notes that commit 2d63f2e read right leave the 15 ms, 19 and 15 of them early [synthetic]. Of these, 21 and 13 lie in signals whose pump read changes: the pump strips on 42 and 7 fewer of the 500 signals, and on 21 and 4 more [synthetic]. The pump read counts beats over the held parts that the first read’s knees bound, so a knee that moves changes the parts it counts, see #313 [fact].
-
A next note’s band rings ahead of its attack, and the 10 ms before the stop can read that ring over a fiftieth of the hold, with or without a pump [synthetic]. Then the knee takes the next note’s corner: on the long dry notes of that bass line, 188 of 3570 read over 150 ms late, and on long stripped notes 117 of 2757 [synthetic]. Read as the lowest 10 ms since the first corner, the stop cut the dry ones to 0, but lost the note-off of a filter that closes past the band’s harmonic before a next note that starts at the note-off, on 2 of the 1405 tally runs, 0.57 s early [synthetic].
-
A sustain under -34 dB of the earlier corner’s level, with its note-off past the stop, loses that note-off to the decay corner when its level comes back without a next note: the knee reads 0.46 to 0.8 s early, where commit 2d63f2e reads it [synthetic, from generated probes]. This happens in two shapes:
- A rise of about 1.3 times, 2.3 dB, or more over the last 100 to 400 ms before the note-off. The rise that loses it is 2.0 times over 60 frames, 1.5 over 100, 1.4 over 200 and 1.3 over 400.
- A swell like a tremolo of ±15 % or more at 2.5 Hz or slower. At ±25 % over 400 frames, 12 to 20 of 96 cells lose it with the stop 150 to 200 frames before the note-off; at ±15 % over 400 frames, 24 of 96 with the stop 200 frames before.
-
A next note at 1.4 to 1.7 times the level of a silent gap still takes its own corner, as commit 057d63c and 2d63f2e do [synthetic]. On a gap at 0.01 with the stop at its end, it forms a corner where its level flattens, which sees its release within 200 ms and whose 100 ms mean reads about 1.17 times the gap. That corner replaces the note-off, and the next note’s own corner then replaces it in turn. At 1.3 and at 1.8 or more the note-off holds.
-
A medium release lies over 50 % off on 50 of the 875 reverb stems, 26 of them with a release of 20 ms, and on 23 of the 779 dry stems, all with a release of 20 ms, on the 4000 stems [synthetic]. Lowering every reverb release with a true vote or a median
tail_weightof 0.05 would leave 2 of 8, and would also lower the 819 right ones, so the level stays [synthetic]. -
Under a tremolo, 43 of the 490 medium releases lie over 50 % off, on 2000 dry stems, after the rule above [synthetic].
-
The RT60 reads about 50 % high under a release of 400 ms: at a true 0.6 s and 0.15 or 0.4 wet the median is 0.90 s, on about half of 300 stems each [synthetic]. The space layer takes this up [fact].
-
A dry stem can report an RT60, on 2000 generated stems a set [synthetic]:
Set With an RT60 Plain dry 82 Short sustain 211 Tremolo 330 Legato 25 Chord 185 The 4000 stems above read it on 55 of 1372 dry stems, so that 4 % is no general rate [synthetic].
-
A note list that ends at the note-off shows no knee, so
--noteswith such a list reads no release [fact]. -
The 3 dB residual, the 0.05 weight of an RT60 and the five vote thresholds were chosen on generated signals and are untested on real tails; #91 checks them [fact].
-
A release of 20 ms at 41 Hz is shorter than the level window: of the 116 reverb stems written more than 50 % off with a release up to 400 ms, 64 have a release of 20 ms [synthetic].
-
#70 asks that the fit never returns a confident release more than 50 % off. Read with its scope, where the fit reports a
tail_weightover 0.5, it holds, because no release is written there. It does not hold unscoped: the medium releases above lie over 50 % off under that weight [synthetic]. -
The fit has no echo term, so a delay reads as release or tail [fact].
-
The bench has no pre-delay, and the reverb is noise [fact].
-
The beat grid starts at 0 s with the tempo as used. The period search finds one constant tempo within up to 2.81 % of it; a tempo that moves inside the stem still smears the curve [fact]. Its first stage reads up to 64 beats of the longest run, so a duck off the tempo that starts later in one held note can go unfound [fact]. On the 110 Hz chord, over runs of 10, 12, 16 and 32 beats and four phases, up to 2.5 % off the tempo, of 176 cells at each tempo [synthetic]:
- A 12 dB duck with an 80 ms release locks in 173, 175, 176, 176 and 173 at 60, 80, 96, 128 and 174 BPM.
- A 6 dB duck with a 150 ms release locks in 172, 165, 135, 159 and 131 at 60, 80, 96, 128 and 174 BPM. It locks in 8 of 16 on the tempo at 96 BPM, and in 1 of 16 at 2.5 % above at 174 BPM.
-
A read that stays on the grid reports the grid’s tempo, though the duck can lie off it [fact]. Of 11,161 ducks up to 2.5 % off that stayed, the tempo was off by a median of 0.99 % and at most 2.56 %; of 33,787 fitted reads, by a median of 0.08 % and at most 2.22 % [synthetic].
-
A duck on the tempo can move from a grid lock. On the 123,840 signals, 1 of 4847 grid locks does, 6 dB at 110 Hz and 60 BPM, by 0.89 %. The 76,836 stems, read through the layer, hold runs of 8.9 to 32 beats, ducks of 6, 12 and 18 dB and 4 phases, on a flat level, under fades of 12, 18 and 36 dB and under 6 dB steps every 4 beats. There 3 of 4595 grid locks move, by up to 0.89 %: 3 on the flat level and 0 under each fade or the steps. A wider set reaches more: 9,600 stems on the tempo per level, from 3 pitches and 5 tempos, with 10 runs of 8.9 to 32 beats, 8 duck settings of 3 to 18 dB and 8 phases. Its grid locks move in 6 stems on a flat level, 7, 12 and 10 under those fades, and 0 under the steps. These are ducks of 6 dB or less over 8.9 to 12.3 beats, by up to 2.7 %. A 6 dB, 150 ms duck at 110 Hz and 60 BPM over 10 beats, under the 36 dB fade, reads 59.479 BPM [synthetic].
-
A grid read of none takes a fitted beat lock only when its curve falls faster than it recovers and takes 3 bins from half its depth back to a quarter, which holds out most gates. The price in ducks is small for releases of 80 ms or more and large at 30 ms, which loses 2004 of 6406 below. The set: 38,400 ducks on the tempo, from 3 pitches, 5 tempos, 10 runs of 8.9 to 32 beats, 8 duck settings of 3 to 18 dB and 8 phases; 46,080 ducks 0.5 to 2.5 % off, of 6, 12 and 18 dB with releases of 80 and 150 ms; 13,824 gates; flat and under fades of 12, 18 and 36 dB. Counted on this path alone [synthetic]:
Fitted read over a grid read of none Moves on the tempo Ducks off the tempo that strip Gates that strip No test 839 16,427 1733 This test 653 16,150 59 - The lost ducks are 277, or 1.7 %. Of them 132 lie at 55 Hz, 117 have a release of 80 ms and 160 of 150 ms [synthetic].
- Short releases lose more, and slow tempos with them, as the test asks for 3 bins of a beat. On the 123,840 signals, on this path, ducks that would strip lose 2004 of 6406 at 30 ms, 61 of 4773 at 80 ms, 7 of 4079 at 150 ms and 2 of 3976 at 300 ms, and 998 of 4065 at 60 BPM, 523 of 3649 at 80, 326 of 4930 at 96, 141 of 3422 at 128 and 86 of 3168 at 174. Over all ducks of 30 ms up to 2.5 % off, 7453 of 15,840 strip, against 8996 with no test. A 12 dB, 10 ms duck over 480 beats does not strip [synthetic].
- The moves left are 118 on a flat level and 181, 177 and 177 under the fades, by up to 2.46 %. There the duck is real: a move mostly shows that the grid read found no lock [synthetic].
-
A chord whose level wanders with no pump can read as a pump. Under linear interpolation of Gaussian dB knots, spaced 0.25 to 1 beat, with sigmas of 1, 2 and 4 dB, at 55, 110 and 220 Hz, 60 to 174 BPM and 9, 16 and 32 beats, 5 of 2,592 strip, at 3 to 4 dB, 4 of them at a sigma of 4 dB [synthetic].
-
A level step inside a beat stays in that beat’s fold error, and a step on a beat edge does not, so a slightly different period can win by moving an edge onto the step. One step of 6, 12 or 24 dB, about 0.35 or 0.65 through the run and up to 0.15 of a beat from a beat edge, at 55 and 110 Hz, 96 to 174 BPM and 16 and 32 beats, moves 21 of 4465 grid locks, by up to 1.15 % and with up to 1.09 dB of lost depth. A 6 dB, 150 ms duck at 110 Hz and 128 BPM over 16 beats, with a 12 dB step at 0.63 of the stem, reads 127.571 BPM. Its recovery reads 130 ms and its ripple 1.31 dB, against 203 ms and 0.69 dB on the grid. The same cause can hold a duck off its tempo. At 1 % off, a 48 dB fade over the last quarter finds the duck’s tempo in 18 of 29, and 6 dB steps every 4 beats in 22 of 31, against 28 of 31 with no steps [synthetic].
-
A chord’s own beating sets a floor of about 1.3 dB under the fold error, so a smeared duck can stay on the grid. On the 76,836 stems, of 9205 ducks that stayed where both reads lock, 654 would have read 1 dB or more less ripple at the fitted period, 2.12 dB on average. A 12 dB duck 1 % slow over 12.3 beats at 96 BPM reads 6.5 dB of ripple on the grid and 1.1 dB at its own period, at fold errors of 1.42 and 1.33 dB [synthetic].
-
The shape test can pass a gate. Over 8 phases and 9 to 32 beats, a 12 dB gate 3 % below the tempo at 55 Hz and 174 BPM strips in 4 of 376 runs [synthetic].
-
The depth reads shallow, and the bias depends on the shape: an 18 dB duck reads up to 11.8 dB short in the set of Depth bias. On the noiseless chords of the 123,840 signals, on the tempo, with releases of 80 ms or more, it reads a median of 4.9 dB and at most 9.2 dB short [synthetic].
-
peak_dbcompares two notes directly only when they read the same harmonic, so the pool’s level gate can leave out a note that reads a higher harmonic of a timbre with weak upper harmonics [fact]. -
A second sound on the beat, such as a kick in the same stem, can read as a beat-locked pump; over fewer than 8 beats it is not stripped [synthetic].
-
The search takes the deepest fold, so a second modulation near the tempo can take it [fact]. Under a duck on the tempo, a deeper second duck 0.5 to 2.5 % off took 53 of 64 locks, and a shallower sine, duck or gate took 15 of 623 [synthetic].
-
A gate cuts across notes on the beat as a pump does; no test tells the two apart on a read that locks on the grid [fact]. A lock on a level with a duck’s shape off a division, such as a saw tremolo that falls at once and rises over the beat, strips as a duck too. Of 360 saw tremolos each, 266 strip at 3 % above the tempo against 63 with no search on free reads, 232 against 8 at 3 % below, 138 against 0 at 3.5 % above and 10 against 0 at 3.5 % below, and none at 4 to 5 %, on the 123,840 signals [synthetic].
-
A heavy tail hides the release, so
release_sis null over a mediantail_weightof 0.5 [fact].
Sources
- Envelope stages: Xfer Records, Serum 2 manual. Facts only.
- Attack and decay descriptors: Peeters, Giordano, Susini, Misdariis and McAdams, The Timbre Toolbox: extracting audio descriptors from musical signals, JASA 2011. Inspiration only.
Voices
The voices layer counts how many notes sound at once and, on a mono line,
reads how each note joins the next. analyze writes it to the voices group
of layers.json. It reads any line, chords too, so its fields sit in a group
of their own and the pitch group stays null on chords [fact].
Voicing, the chord gate of the transcriber and the pitch layer, does not read
this count [fact]. The final notes do: a line with at most one voice at a time,
or with no count, is a mono line for every rule of notes.mid and for the
span of the full-band level, unless --profile pad ran the notes step
(owner decision, 2026-11-05) [fact].
The count
- A note holds a voice from its onset to its note-off knee, or to 25 ms before its end without a knee, so a release tail holds no voice, and a note the envelope layer reads as silent holds none. A held part stops at the stem end, so a note listed past the stem holds none either. A note of 25 ms or less holds none without a knee [fact]. The issue asks for the first note to lie within 3 dB of its sustain level. This reads the knee instead and has no level test [fact].
- Pitches within 60 cents form one voice, so seven detuned copies of one pitch count once [fact, proposal for 60 cents].
max_polyphonyis the most voices that have started by any onset and are held 50 ms after it, so a held overlap under 50 ms does not count [fact, proposal for 50 ms].monois true when it is 1, so one overlap of 50 ms or more makes a line poly, with no tolerance [fact].mean_polyphonyis the mean count over the time any note is held [fact]. An overlap under 50 ms counts here and not inmax_polyphony, so the mean can exceed the maximum [fact].- The confidence is the share of that time whose count agrees with the verdict: one voice on a mono line, two or more otherwise. It reads high from 0.9 and medium from 0.7 [fact, proposal for both].
Joins
Consecutive notes of a mono line join when the later onset lies under 30 ms past
the earlier end in the note list, starts after the earlier onset, and lies less
than 50 ms inside the earlier held part. A note stacked on another joins nothing
and leaves the earlier note in place unless the stacked note ends later, and a
silent note leaves it in place too [fact]. gap_ms is
negative when they overlap in the list. The split join of the notes step merges a same-key repeat with no
attack of its own, so that repeat reaches no join [fact].
dip_dbis the level of the quieter note minus the lowest level from 25 ms before the onset, or from the earlier held end when that comes first, to 25 ms after it, on an RMS over one period of the lower note [fact]. A transcribed onset can land after the silence it follows, which the held end still sees [synthetic]. Each note’s level is the 10th percentile over up to 100 ms beside the join, because that window still ripples on the higher note [fact].centroid_changeis the centroid over f0 after the join over the same before it, minus 1, over 20 ms or two periods each side, and null when either side holds no signal or runs past either end of the stem [fact]. The verdict does not read it, because it gained nothing on the set below [synthetic].legatois true whendip_dbis under 1 [fact, proposal for 1 dB]. A 12 dB dip that rises over 5 ms at 110 Hz reads 1.35 dB [synthetic].dip_dbandlegatoare null, with no estimate, when neither note has a level above the -120 dB silence floor beside the join, as with notes of about 25 ms or notes listed past the stem [fact].- A join reads low when its verdict is legato, as a retrigger with an instant attack keeps its level too; when no attack was read; when half a period before the earlier level window or one period before the dip read falls before the stem start; when one period past the 25 ms after the onset falls past the stem end, as a level is then unread or short; or when the median attack of the pool is over 100 ms, because a long attack hides a retrigger [fact, proposal for 100 ms]. Otherwise it reads medium [fact].
The set: 4000 joins sampled over 41 to 440 Hz, steps of -12 to +12 semitones, four timbres, dips of 3 to 60 dB, attacks of 1 to 50 ms, level changes of up to 6 dB, onsets off by up to 20 ms and gaps of -20 to 10 ms, of which 3669 reach a join. At 1 dB, 91 % of the legato joins read legato [synthetic]. Of the retriggers of 12 dB or more [synthetic]:
| Attack | Read as a retrigger |
|---|---|
| 1 ms | 100 of 274 |
| 5 ms | 188 of 288 |
| 20 ms | 253 of 271 |
| 50 ms | 292 of 296 |
Limits
-
A window of one period of the lower note smears a short new attack, so a retrigger with an attack of 5 ms or less often reads legato, as the table shows. No level read shorter than a period holds still on a periodic wave [fact].
-
A retrigger with an instant attack keeps its level, so on generated lines with exact note lists it mostly reads legato: 232 of 270 joins of a retriggered 808, 106 of 127 of a soft retrigger and 81 of 131 of a stepped one, against 150 of 174 of a legato 808. A legato line with a dip reads legato in 46 of 73 joins [synthetic].
-
The count reads the note list as it is, so a transcriber’s wrong overlap counts, and one overlap of 50 ms or more makes a line poly. On 889 generated mono lines the chord gate calls monophonic, 136 read poly from transcribed notes, 131 of them at low confidence. With exact note lists, 463 of 463 read mono [synthetic]. On 22 real instrument stems, 1 read mono and none reported a join, at mostly low confidence. The other 21 read a maximum of 3 to 8 and a mean of 1.06 to 2.52 [fact]. So a mapper must not take
mono: falseor missing joins as evidence at low confidence. -
The verdict depends on where the join falls in the period, and the acceptance test sits on a whole-cycle position. Moving a join from 1.0 s in 0.31 ms steps over 40 positions, between the same saws, read these counts [synthetic]:
Hz 0 dB join read legato 12 dB, 5 ms retrigger found 110, 165 31 24 220, 330 34 40 110, 146.83 37 24 82.41, 123.47 29 24 -
With under 30 ms of audio after a join, the later note has no level, so a legato join reads as a retrigger: 682 of 960 over four pairs, against 17 of 160 at each length from 40 ms. A 12 dB, 20 ms retrigger reads as one in all but 27 of 960, so the verdict stays and the join reads low, rather than losing those [synthetic]. At the stem start, a join reads low while its earlier level window reaches the start, so up to a lead of about 125 ms. A legato join there reads as a retrigger in 18 to 47 of 160 by lead, from 150 ms on in 27 to 40, and from 1.0 s in 29, so this low level marks a short first note and not a higher error rate. A 12 dB, 20 ms retrigger reads as one in 160 of 160 at every lead [synthetic].
-
dip_dbreaches 100 to 190 dB on exact digital zeros, since the -200 dB floor stands in for a level. The verdict does not change [synthetic]. -
A filter that restarts with no dip in level reads legato. In the set, 228 of 433 filter joins read as a retrigger, against 289 of 436 to 358 of 476 for the other timbres [synthetic].
-
A long attack hides a retrigger, and the amplitude envelope may not restart from zero, which makes a dip shallower [fact].
-
A pad’s count is a lower bound, because pads are the weakest class of the transcriber [fact].
-
Glides and legato inside chords have no method here [fact].
Suggested edits
The mapping step turns the pitch, voices and envelope groups of
layers.json into edits for one synth. It runs in two parts. A
synth-neutral plan decides what the patch should hold, in physical units.
The synth’s adapter then writes each setting to its own controls with its
own laws. This page states the plan.
The output holds the edits, an unrepresentable list for values the synth
cannot reach, and an unwritten list for measured fields the plan leaves
out on purpose. analyze writes the edits and the unrepresentable
entries into each group of layers.json; see
Layer report. The unwritten list stays
in the library. Each unwritten entry names the field by its path in the
group’s values, such as attack_s or glides.curve, and gives one
reason:
| Reason | Meaning |
|---|---|
low_confidence | The field has no estimate, or a low one. |
upper_bound | The field is an upper bound, such as an attack faster than the track can tell. |
not_identifiable | The report cannot separate the value from what hides it, such as a release under a heavy tail. |
curve_not_set | The curve stays at its default; tune it by ear. |
unprobed | The control exists, but no probe has shown what it does. |
Voicing
- Mono, legato and the voice count need a voices verdict of medium
confidence or higher. A
voicesgroup at low confidence writes nothing and listsmono,joins.legatoandmax_polyphony; set them by ear. A low count is no evidence of voicing (see Voices). - A mono stem writes mono on and no voice count.
- Legato is on when a join of medium or high confidence reads legato. It is
off when every such join retriggers. A synth’s legato acts only where notes
overlap, so one legato join needs it on. Joins with no confident verdict
write nothing and list
joins.legato, so legato keeps the template’s setting; set it by ear. - A poly stem writes mono off, legato off and a voice count of
max_polyphony, times 1 unison voice and 1 oscillator. The unison step sets the final count once it knows those numbers.
Glide and pitch drop
- A record of low confidence counts for nothing.
- The glide time is the median
time_sof the confident glides: the full 0 to 100 % time. A time beyond the synth’s maximum lands on the maximum and is listed inunrepresentable. Always-glide is on when one of those glides starts from silence. - No law maps the fitted glide shape
pto a synth’s glide curve, so the curve stays at its default andglides.curvereadscurve_not_set. - The pitch drop is the median
df_stand the mediantau_sof the confident drops. The adapter writes a unipolar envelope route into oscillator pitch. A drop beyond the destination’s range lands on the range, and the rest goes tounrepresentable. The route amount is an interim law, always markeduncalibrateduntil the calibration set measures it. An edit of the drop envelope is markeduncalibratedwhere the registry tags its controlInterim. - Vibrato writes no route here.
Amplitude envelope
- A stage of low confidence writes nothing. An edit’s confidence is the lowest level of the records behind it, capped by the group’s level.
- An attack flagged
attack_is_upper_boundwrites nothing and readsupper_bound. - A hold is written only for a plateau over 10 ms; the envelope layer reads a shorter one as 0.
- The decay runs from the peak to the sustain level. The sustain is the held level from the peak in dB; the adapter’s law turns it into its stored value.
- A release is written only when
release_confidenceis medium or high: the envelope layer leaves no release under a tail heavier than 0.5 or with a residual above 3 dB. Otherwiserelease_sreadsnot_identifiable. - The attack, decay and release curves stay at their defaults and read
curve_not_set. - The tail and the pump have no envelope control. They belong to the space and modulation layers.
- A value equal to the control’s default writes no edit.
Velocity
Velocity gets a path to the level when level_spread_db has a value, read
over at least 3 pooled notes, and exceeds 3 dB. The edit is always low
confidence, as the spread is.
Common methods
The rules on this page bind every stage of the pipeline. Units and confidence have their own page, Units, which this page does not restate. The tags are those of the pipeline page.
Samples in f64
Samples, spectra and every intermediate value are f64 from the decoder
on, at the stem’s native rate [fact]. f32 appears only where a file
format stores 32-bit floats [fact]. Analysis never resamples; only a
consumer with a fixed input rate does [fact].
The window
The analysis window is the periodic Hann, for : the first samples of a symmetric window of length [fact]. Its main lobe is 4 bins wide null to null, and its -3 dB width is 1.44 bins [fact]. The symmetric form would shift every bin slightly [fact]. A four-term Blackman-Harris window, with lower side lobes and a wider main lobe, and the rectangular window exist for comparison only [fact].
The frame rule
- Frames are centred and zero-padded, with hop [fact].
- At 44.1 kHz the frame length is 8192 samples when the note list has a median f0 below 100 Hz, else 4096, and 4096 without a note list [fact]. At another rate the length is the power of two nearest, in log, to that length times : the same at 32 and 48 kHz, twice it at 88.2 and 96 kHz, four times at 176.4 and 192 kHz, half at 22.05 kHz [fact]. Harmonics apart need for the 4-bin Hann main lobe; at 48 kHz, 4096 points resolve harmonics down to 47 Hz, and 8192 down to 23 Hz, as do 16384 points at 96 kHz [fact].
- The stereo screen uses 8192 points for every f0, and the spectrogram’s zoom panel 16384 [fact].
layers.jsonrecords the length used asconventions.frame_size[fact].
Active frames and statistics
- A frame is active when its RMS lies within 40 dB of the loudest frame’s [fact, proposal for 40 dB]. The stereo screen takes the RMS over both channels [fact].
- A statistic over active frames is the median and the interquartile range [fact]. Both resist outliers, such as the frames of a note onset [fact].
- Quantiles interpolate linearly between the sorted values at position , the convention of the common numeric libraries [fact]. The IQR is the 75th minus the 25th percentile [fact].
- A statistic without an active frame is no estimate with the reason
silent_stem, never a number [fact]. One with fewer than 16 active frames is no estimate with the reasontoo_few_frames: the first and last two frames of a clip reach into the zero padding, and a median over few frames follows them [fact, proposal for 16].
Conventions next to every number
Published tools disagree on weights, floors, windows and statistics, so a
number means something only with its convention [fact]. Every number in
the stem group of layers.json has a convention under the same key in
conventions, and a test fails when a field lacks one [fact]. The
convention names the weighting, the window, the floor and the statistic
[fact].
Golden vectors
Expected values in tests come from an independent reference, never from our own code [fact]. The testing page gives the commands.
- A pinned Python generator computes each reference value once, from a recipe that the JSON vector stores [fact]. The interpreter and every package are pinned with hashes, so a second run writes byte-identical files [fact].
- Each vector holds the recipe, the generator, the tool versions, the values and the tolerance [fact]. The Rust test rebuilds the signal from the recipe and compares with [fact].
- Noise comes from a SplitMix64 generator that both languages implement bit for bit [fact]. A statistic over noise is still compared as a statistic, with a tolerance that covers its spread [fact].
- CI never runs Python. It checks that every vector has its fields and names the pinned versions [fact].
- The figures that task issues quoted live as prototype tables with their
recipe. A figure the recipe does not reproduce is recorded with a
reproduced: falseflag, and the committed table replaces it [fact].
Command line
The binary is sound-to-patch. This page shows the help of each command.
The help texts run in the test suite, so they match the built binary.
cargo xtask dist also renders them as man pages.
sound-to-patch
$ sound-to-patch --help
Turn an audio stem or a written sound description into a starting point in Vital or Serum 2.
Usage: sound-to-patch <COMMAND>
Commands:
analyze Analyse a stem and write layers.json, spectrogram.png and notes.mid
notes Transcribe a stem and write notes.mid
preset Read, inspect and write synth presets
render Render a preset and a MIDI file to a WAV file through the synth
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print version
analyze
$ sound-to-patch analyze --help
Analyse a stem and write layers.json, spectrogram.png and notes.mid.
The tempo is refined from the stem near --bpm, and the stem is transcribed unless --notes gives its notes. Each step prints what it found.
Example: sound-to-patch analyze bass.wav --bpm 140
Usage: sound-to-patch analyze [OPTIONS] --bpm <BPM> <STEM>
Arguments:
<STEM>
The audio stem: WAV, AIFF, FLAC, MP3, AAC or M4A
Options:
--bpm <BPM>
The tempo of the stem in beats per minute, 4 to 1000. It is refined from the stem, 2.91 % below to 3.09 % above it, unless --bpm-exact is given
--bpm-exact
Use --bpm as it is, without refining it from the stem
--notes <FILE>
A MIDI file with the stem's notes, instead of transcribing the stem; the f0 track still reads the stem unless the notes play chords. Its tempo must match --bpm or the refined tempo
--profile <PROFILE>
Decode with the settings of this kind of stem instead of the detected ones
Possible values:
- bass: B0 to F#4, with the minimum length from the BPM
- lead: C2 to B6, with the minimum length from the BPM. Plucks use it too
- pad: C2 to B6, with the decoder's default minimum length. Chords use it too
-o, --out <DIR>
The output folder [default: a folder named after the stem, next to it]
--force
Replace files in the output folder that this stem's report does not own: a report or notes of another stem, a report of the same name with other content, a file this tool did not write, or a spectrogram.png without a report
--synth <SYNTH>
The target synth that the report names [default: vital]
Possible values:
- vital: Vital, with `.vital` preset files
- serum2: Serum 2, with `.SerumPreset` preset files
-h, --help
Print help (see a summary with '-h')
analyze decodes the stem at its native rate, runs the stem estimators
and writes three files into the output folder, which it creates:
layers.json: the report. The layer report page lists every field.spectrogram.png: a 1500 by 512 pixel image of the busiest 10 s, in two panels. The pipeline page gives its layout.notes.mid: the final note list, the notes rewritten with the layers, as the analyze guide explains. It replaces the first file ofnotes. A stem without notes writes none.
The files are replaced on the next run of the same stem file; nothing else
in the folder changes. A stem of the same name with other bytes, such as a
new export, needs --force, below. The same stem and flags give the same
bytes. The stem is read once, so a pipe such as /dev/stdin works as a
stem. Each file
is written to a hidden temporary sibling and renamed into place,
notes.mid first and layers.json last; the old layers.json moves
aside before any file changes. So no run leaves a cut-short file, and a
layers.json that exists belongs to the files next to it. A write that
fails, such as on a full disk, puts the old files back, leaves no
temporary file and removes a folder the run created. A run killed during
the renames, such as by a power loss, can leave the old files as hidden
backups named .<file>.<random>.old, such as
.layers.json.a1B2c3.old, with no layers.json. The next run stops and
names every backup: rename each backup back to its file name to keep the
old report, or pass --force to write new files and remove the backups
once they are in place. The
command prints one wrote <path> line per file to standard output, and a
warning line for each corrupt packet that became silence to standard
error.
layers.json records the stem’s file name in source and the SHA-256 of
its bytes in source_sha256. The command stops before it decodes, names
the file and asks for --out or --force when the folder holds:
- the report of another stem, such as
bass.aiffnext tobass.wavsharingbass/; - a report of the same name with other content: another stem of that
name, such as the
bass.wavof another song in a shared--outfolder, or this stem changed since that run, such as a new export; a report from release 0.31 or earlier has no checksum and is matched by its name; - a
layers.jsonthat is not a report of this tool: it cannot be read, is not JSON or names nosource; - a
spectrogram.pngand nolayers.json, a file no report owns; - a
notes.midof another stem by its track name, or one with no track name, as innotes.
--out picks another folder, and --force replaces the files. An
output folder that is a file, by default or by --out, such as a file
bass next to bass.wav, stops the command with a request for --out.
Without --out, the folder is named after the stem file and sits next to
it: stems/bass.wav writes stems/bass/. A stem whose name without the
extension is . or .., such as ..wav, would name its own folder or the
one above, and a stem without an extension, such as bass or .wav,
would name the stem file itself, so each needs --out.
- Tempo:
--bpmis refined from the stem’s onsets, from 2.91 % below to 3.09 % above it, as the tempo section describes, and the first line of standard output says which tempo every later step uses and why.--bpm-exactuses--bpmas it is. The report records the given and the refined tempo. - Notes: without
--notes, the stem runs through the notes step ofnotesonce, and the next lines of standard output are its steps;--profileoverrides the detected settings as there. Thetranscriptiongroup of the report records the model and the settings. - With
--notes, the MIDI file gives the note list and the model never runs;--profileis then a usage error, andtranscriptionisnull. The note times follow the file’s tempo map, and every tempo in the file must lie within 0.01 BPM of--bpmor of the refined tempo; a file without a tempo event plays at the MIDI default of 120 BPM. Notes on channel 10, the percussion channel, are left out. - Standard output ends with
wrote <path>for each file. A warning line for each corrupt packet that became silence goes to standard error.
| Exit code | Meaning |
|---|---|
| 0 | The files were written. |
| 1 | The stem or the note file cannot be read or is invalid, the notes step fails, the selected synth has no adapter yet, the output folder is a file, has no name of its own or holds a file the run does not own, an output cannot be written, or the analysis fails internally. A refusal and a failure share this code. The message names the cause and the fix, and the file when one is involved. |
| 2 | A usage error, such as a missing --bpm, a tempo outside 4 to 1000, or --profile with --notes. |
notes
$ sound-to-patch notes --help
Transcribe a stem and write notes.mid.
The pitch range, the voicing and the minimum note length come from the stem unless --profile sets them. Each step prints what it found.
Example: sound-to-patch notes bass.wav --bpm 140
Usage: sound-to-patch notes [OPTIONS] --bpm <BPM> <STEM>
Arguments:
<STEM>
The audio stem: WAV, AIFF, FLAC, MP3, AAC or M4A
Options:
--bpm <BPM>
The tempo of the stem in beats per minute, 4 to 1000. It is refined from the stem, 2.91 % below to 3.09 % above it, unless --bpm-exact is given
--bpm-exact
Use --bpm as it is, without refining it from the stem
--profile <PROFILE>
Decode with the settings of this kind of stem instead of the detected ones
Possible values:
- bass: B0 to F#4, with the minimum length from the BPM
- lead: C2 to B6, with the minimum length from the BPM. Plucks use it too
- pad: C2 to B6, with the decoder's default minimum length. Chords use it too
-o, --out <DIR>
The output folder [default: a folder named after the stem, next to it]
--force
Write next to a report that is not this stem's, replace a notes.mid of another stem or with no track name, or replace the final notes and remove the report that describes them
-h, --help
Print help (see a summary with '-h')
notes decodes the stem at its native rate, runs the transcription model
once and the f0 track once, and writes notes.mid into the output folder,
which it creates. The pipeline page
gives each step.
- Settings: without
--profile, the pitch range, the voicing and the minimum note length come from the stem.--profile bass,leadorpadsets them instead; detection still runs and prints its findings. - Tempo:
--bpmis refined from the stem as foranalyze, and every later step uses the printed tempo;--bpm-exactuses--bpmas it is. - Output: standard output gets one line per step, in order: the tempo,
the detected range, the detected voicing, the register window, the
voicing the cleanup used and where it came from, the settings used, the
decoded count, then the note count after the octave check, the ghost
merge, the attack split, the split join and the quiet-note guard with
its drop count, and last
wrote <path>. A warning line for each corrupt packet that became silence goes to standard error. - The file: a format 0 MIDI file with 480 ticks per beat, the stem’s file
name as the track name, the printed tempo and 4/4 at tick 0, and
every note on channel 1 at velocity 100.
It is written to a hidden temporary sibling and renamed into place. A
failed write leaves the old files and no temporary file. Only
notes.midis replaced; nothing else in the folder changes, except the report that--forceremoves, below. - The folder: as for
analyze,stems/bass.wavwrites intostems/bass/without--out, and a stem such as..wav,.wavorbassneeds--out. When the folder holds anotes.midwhose track name is another stem, or none, the command stops before it decodes and names the file;--forcereplaces that file. When it holds alayers.jsonof another stem, of the same name with other content, or one that is not a report, it stops the same way;--forcewritesnotes.midnext to that file and leaves it. When it holds alayers.jsonwith anotesgroup, the report ofanalyzethat describes the finalnotes.mid, it stops whatever the stem, because the first list would replace the file the report describes;--forcereplacesnotes.midand removes that report once the new file is in place, so no report describes a file that is gone. A backup that a killed run left, as foranalyze, stops it too;--forcewritesnotes.midand then removes the backups.--outpicks another folder in every case.
| Exit code | Meaning |
|---|---|
| 0 | notes.mid was written. |
| 1 | The stem cannot be read, gives no note (digital silence, for one), or cannot be transcribed; the output folder is a file, has no name of its own or holds a file the run does not own; or the file cannot be written. Nothing is written. A refusal and a failure share this code. The message names the cause and the fix. |
| 2 | A usage error: a missing --bpm, a tempo outside 4 to 1000 or not a number, or a profile other than bass, lead and pad. |
preset
$ sound-to-patch preset --help
Read, inspect and write synth presets
Usage: sound-to-patch preset <COMMAND>
Commands:
inspect Print the header fields and the decoded tree of a preset file
apply Apply an edit list to a template and write a preset
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
preset inspect
$ sound-to-patch preset inspect --help
Print the header fields and the decoded tree of a preset file.
Example: sound-to-patch preset inspect bass.vital
Usage: sound-to-patch preset inspect [OPTIONS] <FILE>
Arguments:
<FILE>
The preset file, such as `bass.vital`
Options:
--synth <SYNTH>
The target synth [default: the extension of the preset file, else vital]
Possible values:
- vital: Vital, with `.vital` preset files
- serum2: Serum 2, with `.SerumPreset` preset files
--full
Print every list element and every text in full
--shown <N>
List elements printed before the rest is counted
[default: 3]
-h, --help
Print help (see a summary with '-h')
preset inspect refuses a file without a stamp, settings or sample, and
a file with a known control that holds a text, a list, an object or null in
place of a number. Vital fails to load a text in a control and loads a
flag, so a flag reads. The message names the key.
preset apply
$ sound-to-patch preset apply --help
Apply an edit list to a template and write a preset.
Values are written with their unit, such as "1.2 kHz", "800 ms", "-6 dB", "+12 st" or "25%".
Example: sound-to-patch preset apply edits.json -o bass.vital
Usage: sound-to-patch preset apply [OPTIONS] --out <FILE> <EDITS>
Arguments:
<EDITS>
The edit list, a JSON file
Options:
-o, --out <FILE>
The preset to write, such as `bass.vital`
--template <FILE>
A preset to start from instead of the synth's built-in template
--name <NAME>
The preset name. Without it, the name of the output file
--force
Replace the output file if it exists
--synth <SYNTH>
The target synth [default: the extension of the preset file, else vital]
Possible values:
- vital: Vital, with `.vital` preset files
- serum2: Serum 2, with `.SerumPreset` preset files
-h, --help
Print help (see a summary with '-h')
The output file:
- must carry the synth’s extension, such as
.vital; - must not exist, unless
--forceis given; - must not be the edit list or the template;
- must not lie inside Vital’s own
Factoryfolder.
The output keeps the template’s stamp, and the summary prints it. A stamp
with a newer first or second number than the stable build 1.0.7, such as
1.1.0, gives a warning: that build refuses it. The load gate ignores the
third number, so 1.0.8 gives none. A run that ends with an error leaves no
file. A run killed during the write leaves at most a temporary file whose name
ends in .partial next to the output. A folder
without hard links, such as one on an exFAT or FAT32 drive, gets the file
too, and an existing file there is still replaced only with --force.
There, without --force, the file is written in place: it is visible while
it is written, an error removes it, and a run killed during the write
leaves a partial output file. An
empty --name, such as an unset shell variable, is a usage error.
The edit list
preset apply reads an edit list: a JSON object with the target synth and
a list of edits, in a UTF-8 file of at most 1 MiB. A leading byte order
mark is skipped. Each edit is one of four records, and each carries a
reason.
| Record | Fields | Example |
|---|---|---|
| A control | param, value | {"param": "filter_1_cutoff", "value": "400 Hz", "reason": "dark"} |
| A menu choice | menu, choice | {"menu": "filter_1_model", "choice": "Ladder", "reason": "warm"} |
| A route | route: source, dest, amount, optional polarity | {"route": {"source": "env_2", "dest": "filter_1_cutoff", "amount": "+24 st"}, "reason": "pluck"} |
| A raw key | raw: key, value | {"raw": {"key": "custom_warps_count", "value": 2}, "reason": "a newer build"} |
- A control value carries its unit, as the units page lists:
"1.2 kHz","800 ms","-6 dB","+12 st"or"25%". A switch takestrueorfalse, and a count takes a number. - A route amount is in semitones when the destination is stored in semitones, such as a cutoff or a pitch, and in percent otherwise. A depth in semitones is divided by the destination’s stored span, as Vital shows it.
- A route may target a number, a count, a menu such as
lfo_1_tempo, or the amount or power of another slot. A switch and the bipolar, stereo and bypass flags of a slot take no route. - A route amount lies within -100% to 100% of the destination’s range.
- A route that joins the same source and destination as an existing one edits it in place and keeps its polarity, curve and flags unless the edit sets them.
- A raw key is written as it is, outside the registry, and the output lists it as raw. A raw edit never replaces a key of the template that holds a list or an object.
- A list edits each control once and each source and destination once. A second edit of either is refused with the number of the first.
- Every problem of a list is reported at once, up to 50, each with the file, the edit, the cause and the fix.
The guide walks through one edit list.
render
$ sound-to-patch render --help
Render a preset and a MIDI file to a WAV file through the synth.
The synth's plug-in loads headless in the plug-in host process. A render counts only after the load check: the clip sounds, and the state the plug-in saves back holds the preset. A clip whose octave bands match the plug-in's init patch gives a warning. Each step prints what it found.
Example: sound-to-patch render bass.vital --midi notes.mid --bpm 140 -o bass.wav
Usage: sound-to-patch render [OPTIONS] --midi <FILE> --bpm <BPM> --out <WAV> <PRESET>
Arguments:
<PRESET>
The preset to render
Options:
--midi <FILE>
The MIDI file with the notes, read as `analyze --notes` reads it
--bpm <BPM>
The tempo of the MIDI file in beats per minute, 4 to 1000
-o, --out <WAV>
The WAV file to write; the render record goes next to it with .json
--plugin <BUNDLE>
The synth's .vst3 bundle [default: the bundle in /Library/Audio/Plug-Ins/VST3, else in the user's VST3 folder]
--synth <SYNTH>
The target synth [default: the extension of the preset file, else vital]
Possible values:
- vital: Vital, with `.vital` preset files
- serum2: Serum 2, with `.SerumPreset` preset files
--force
Replace an existing WAV or record file
-h, --help
Print help (see a summary with '-h')
render writes two files: the WAV file named by --out, 32-bit float
stereo at 44.1 kHz, and the render record next to it, with .json in
place of .wav. The record follows schemas/render-record.schema.json;
the render record page lists its fields.
Both go to temporary files in the same folder first, and replace the
outputs only once both writes succeeded. Without --force, a failed
write leaves the folder as it was. With --force, a failure while placing
the files can leave the new WAV file beside the old record. The error then
names both files and says that they no longer match. Make the record
replaceable and render again with --force.
The plug-in loads in sound-to-patch-host, a second program that ships
next to sound-to-patch. SOUND_TO_PATCH_HOST names another copy. The
guide walks through one render and the load check.
Units
Each physical unit the library handles has a unit type. A value is
checked once, when it is built from a number, a JSON field or, for a type
with a suffix, a unit string, and is valid by type afterwards. Not every
public function takes a unit type. The constructors and the ratio and gain
conversions take a bare float, and so do many signal-processing and rhythm
helpers and some fields of the estimators. For example,
music::rhythm::ticks_for_ms takes a time in milliseconds and a tempo as
plain numbers.
The code examples on this page come from examples/units.rs, which CI
compiles. cargo run --example units prints their results.
Unit types
Each type holds an f64 that is never NaN or infinite. Its constructor
returns an error that names the type and the accepted range, for example
Normalized out of range: got 1.5, expected 0.0 to 1.0. The rustdoc of each
type states its exact range.
| Type | Holds |
|---|---|
Hz | a frequency, zero or more |
Seconds | a duration, zero or more |
Semitones | an interval, either sign |
Cents | an interval in hundredths of a semitone, either sign |
Octaves | an interval, either sign |
Decibels | a level, either sign |
Percent | a percentage of a unipolar or a bipolar control |
Normalized | a knob position |
MidiPitch | a fractional MIDI note |
Correlation | a Pearson correlation, -1 to 1 |
Degrees | an angle, such as a phase difference, either sign |
Bpm | a tempo, 4 to 1000 beats per minute |
Ratio | a ratio of two values of one unit, zero or more |
Percent has two ranges, fixed by its type parameter.
Percent<Unipolar> is for controls that go one way from zero.
Percent<Bipolar> is for controls that go both ways, such as
a modulation amount. A unipolar value widens to a bipolar one; the reverse
is checked.
Bpm cannot be the spread of a confidence, because its range excludes
zero; a tempo spread would be given in Ratio or Percent.
No milliseconds type exists. A value in milliseconds converts to Seconds
where it enters, with Seconds::from_millis or the ms suffix.
Conversions
MIDI pitch and frequency use equal temperament with note 69, A4, at 440 Hz:
Every MIDI pitch has a frequency, so Hz::from a MidiPitch cannot fail.
The reverse fails for 0 Hz, which has no pitch, and for frequencies outside
notes 0 to 127, about 8.18 Hz to 12 544 Hz. A note number from a MIDI file
converts with MidiPitch::try_from a u8, which rejects numbers above 127.
Intervals convert to frequency ratios. One semitone is 100 cents, one octave 1200 cents:
Decibels convert two ways. An amplitude gain scales a magnitude, such as a sample or a spectral magnitude weight. A power ratio scales an energy, such as the share of a frequency band:
A gain or a ratio of zero has no decibel or cent value, so the inverse conversions return an error for it. The forward conversions return an error beyond a conservative cap, such as 1000 octaves, that keeps the ratio finite and nonzero.
The conversion methods are
Cents::to_ratio,
Cents::from_ratio,
Semitones::to_ratio,
Octaves::to_ratio,
Decibels::to_amplitude_gain,
Decibels::from_amplitude_gain,
Decibels::to_power_ratio
and Decibels::from_power_ratio.
let a4 = Hz::from(MidiPitch::new(69.0)?);
let pitch = MidiPitch::try_from(Hz::new(220.0)?)?;
let detune = Cents::new(7.0)?.to_ratio()?;
let gain = Decibels::new(-6.0)?.to_amplitude_gain()?;
Unit strings
The agent’s edit list writes each value as a unit string: a number, optional whitespace and a suffix. The parser accepts these suffixes, and only these, matched case-sensitively:
| Suffix | Unit | Type |
|---|---|---|
Hz | hertz | Hz |
kHz | kilohertz | Hz |
s | seconds | Seconds |
ms | milliseconds | Seconds |
ct | cents | Cents |
st | semitones | Semitones |
oct | octaves | Octaves |
dB | decibels | Decibels |
% | percent | Percent |
midi | MIDI note number | MidiPitch |
A target type accepts only its own suffixes. A suffix of another unit, an
unknown suffix and a missing number are errors that list the accepted
suffixes. A bare number for a control with a physical unit is an error;
only counts, bools, menu labels and the stored value of a control without
a physical unit take bare values. The parsed value then passes the range
check of its type, so "-5 Hz" fails with the range of Hz, and "-40%"
parses for a bipolar control but fails for a unipolar one.
let cutoff: Hz = "1.2 kHz".parse()?;
let pre_delay: Seconds = "20 ms".parse()?;
let amount: Percent<Bipolar> = "-40%".parse()?;
// A suffix of another unit is an error that lists the accepted suffixes.
let wrong = "1.2 s".parse::<Hz>();
Each type in the suffix table prints in a form that parses back to the
same value, such as 1200 Hz or -40%. Normalized, Correlation,
Degrees, Bpm and Ratio have no suffix, so they have neither a unit
string nor a printed form.
JSON forms
Values reach the library in two JSON forms, and both parse through the same range checks:
layers.jsonholds plain numbers with the unit in the field name, such as"cutoff_hz": 410or"attack_s": 0.02. An integer is accepted for a float field. A string in a number field fails with the parser’s type message.- The agent’s edit list holds unit strings, such as
"value": "410 Hz". The edit-list contract (#94) documents where each string goes.
A value outside its range fails with the type’s message, and the JSON reader adds the line and column. NaN and infinity never reach a file, because no unit type can hold them.
Confidence
Every estimate in the layer report carries a
Confidence:
- a level:
low,mediumorhigh. The estimator stores it from its own signals; the type does not derive it; - one or more reasons. Each pairs a stable
code, such as
active_frames, with the value of the signal behind it: a count, a number or a bool; - an optional spread, never negative, in the unit the type parameter
names. A cutoff is a frequency, but its spread is in octaves, so its
confidence is a
Confidence<Octaves>. An estimate without a spread, such as a menu choice, usesConfidence<NoSpread>; - or a “no estimate” state. A layer with too little evidence reports no value, no level and no spread, and at least one reason.
The report writes one object per confidence. spread is left out when
there is none:
{"level": "medium", "reasons": [{"code": "active_frames", "value": 412}], "spread": 0.3, "estimate": true}
{"reasons": [{"code": "too_few_notes", "value": 2}], "estimate": false}
Reading a report applies the same rules: a level on a “no estimate” state, an estimate without a level, an empty reason list, a negative spread and an unknown field or code are errors.
let frames = Reason::new(ReasonCode::ActiveFrames, Signal::Count(412))?;
let cutoff = Confidence::estimate(Level::Medium, Some(Octaves::new(0.3)?), vec![frames])?;
// {"level":"medium","reasons":[{"code":"active_frames","value":412}],"spread":0.3,"estimate":true}
println!("{}", serde_json::to_string(&cutoff)?);
let notes = Reason::new(ReasonCode::TooFewNotes, Signal::Count(2))?;
let envelope = Confidence::<Seconds>::no_estimate(vec![notes])?;
// {"reasons":[{"code":"too_few_notes","value":2}],"estimate":false}
println!("{}", serde_json::to_string(&envelope)?);
Unit traps
Some Serum 2 controls store a knob position where a reader expects a
physical unit. Such a control takes a Normalized value. No conversion
from Normalized to Hz or Cents exists, because the law from knob to
unit is not measured yet. A cutoff in Hz therefore has no unit type for its
knob: the preset writer will map it through the interim law of the
parameter registry
(#107) and mark
the value uncalibrated.
Each claim below has a build column and a status: verified in a
Serum 2.1.5 session, reported by an outside source, or open. The
outside source records no build, so every row says not recorded and is
reported. The probe presets of
#42 will check
each claim on build 2.1.5 and fill in the build.
| Control | Stored as | Unit type | Build | Status |
|---|---|---|---|---|
| Voice filter cutoff | knob position 0 to 1 | Normalized | not recorded | reported |
| FX Distortion frequency | knob position 0 to 1 | Normalized | not recorded | reported |
| FX Filter frequency | knob position 0 to 1 | Normalized | not recorded | reported |
| EQ, Phaser and Chorus frequencies | hertz | Hz | not recorded | reported |
| Unison detune | knob position 0 to 1 | Normalized | not recorded | reported |
| Reverb pre-delay | milliseconds | Seconds, converted | not recorded | reported |
| Delay time | seconds | Seconds | not recorded | reported |
| Modulation amount | percent, -100 to 100 | Percent<Bipolar> | not recorded | reported |
Sources
- Preset parameter tables: serum-mcp.
The Vital preset file
This page states what is known about the .vital preset file. No official
format document exists. The facts come from the open-source snapshot of
Vital, from renders of its open engine and from sampled presets.
Every row names a build, a status and a source.
- Builds.
source 1.0.6is the open-source snapshot at commit636ca0e.open engineis that engine compiled into thevita0.1.0 Python binding, which reports itself as99999.9.9. A retail build is named by its version, such as1.0.7. - Statuses.
checked: read in the source.verified: confirmed by a probe or a load check of ours on the named build.reported: an outside source says so.open: unknown.untestable by rule: a check that needs a step the project never takes, such as running the app.
A row verified on the open engine is not verified on retail. The pinned retail build is stable 1.0.7. The early-access build is 1.6.4 (reported). The checks still open are listed under Open items.
File and top-level keys
| Fact | Build | Status | Source |
|---|---|---|---|
| A preset is one compact JSON text. | source 1.0.6 | checked | load_save.cpp |
The top level holds synth_version, preset_name, author, comments, preset_style, macro1 to macro4 and settings. | open engine, 1.0.7 | verified | probe: saved default states |
settings holds 772 numeric controls and four structured parts: sample, modulations, wavetables and lfos. | open engine, 1.0.7 | verified | probe: saved default states |
| The app saves the keys of each object in alphabetical order. | 1.0.7 | verified | load check: the re-save of Bass |
A re-save keeps every control within one 32-bit float step and rewrites the version of each wavetable to the app’s build. A load and save rewrites the sample: our noise sample came back 4 frames later with some values one step off, so compare a re-saved sample by its name, length and rate, not its bytes. | 1.0.7 | verified | load check: the re-save of Bass; the open engine keeps the sample |
A file without synth_version, settings or settings.sample, or a sample without length, sample_rate or samples, makes the app throw. | source 1.0.6 | checked | load_save.cpp |
| A missing control takes its default. | open engine | verified | probe: minimal.vital reads back 772 controls, each at the fresh engine’s default |
An unknown key loads and is dropped on save, in settings and at the top level. | open engine | verified | load check: the unknown-keys variant |
A file without preset_name and author loads. | open engine | verified | load check: the no-name variant |
| An out-of-range control is applied as it is. The loader does not clamp. | source 1.0.6 | checked | load_save.cpp |
Factory presets hold values outside the table range, such as a dry/wet above its maximum or the 32-bit float form of a bound. preset apply warns about such a value in a key no edit touched and refuses it in an edited key. | 1.0.x | verified | observed in factory presets of Vital 1.0.x |
| A text in a control fails the load. A bool loads. The writer of this project writes neither. | open engine | verified | probe: filter_1_cutoff set to "x" and to true |
The app overwrites preset_name with the file name on open. | source 1.0.6 | checked | synth_base.cpp |
The writer of this project prints each number as the shortest text that reads back to the same value. 1E5 becomes 100000.0. Values stay bit for bit. | this project | verified | unit test number_text_is_canonical_and_values_are_kept |
Version gate
The app compares the major.minor part of synth_version with its own
build. The patch number is ignored.
| Case | Build | Status | Source |
|---|---|---|---|
| A file whose major and minor are newer is rejected with a dialog. | source 1.0.6 | checked | load_save.cpp |
| A file one patch newer loads. | source 1.0.6 | checked | load_save.cpp |
| An older file runs about 30 upgrade steps before it loads. Files from 1.0.0 on need none. | source 1.0.6 | checked | load_save.cpp |
| The plugin state path ignores a rejected file. The patch keeps its last sound, and no dialog appears. | source 1.0.6 | checked | synth_plugin.cpp |
The stamps 1.0.6, 1.0.9, 1.1.0, 1.5.5 and 0.9.0 load. 100000.0.0 and a text that is not a version are rejected. | open engine | verified | probe 5 and the stamp fixtures |
A file stamped 1.0.6 loads, with every edited control in place. | 1.0.7 | verified | load check: Bass |
Files stamped 1.5.5, 1.6.0 and 1.7.0 are refused with the dialog “Preset was created with a newer version”. | 1.0.7 | verified | load check: the stamp variants |
The app writes its own build, 1.0.7, as the stamp on save. | 1.0.7 | verified | load check: the re-save of Bass |
| Presets saved by 1.6.x do not open in 1.5.5. | 1.5.5 | reported | Vital forum |
| Which stamps the early-access 1.6.4 accepts. | 1.6.4 | open | optional forward check |
Stamp rule. Files built from the built-in template carry 1.0.6, the
open-source build, as DEFAULT_STAMP. Stable 1.0.7 loads them: the gate
compares only major and minor, and files from 1.0.0 on run no upgrade step.
preset apply keeps the stamp of its template and never lowers it. A
template older than 1.0.0, or one that holds sub_octave, is refused. For
the early-access builds, pass a template saved by your own app with
--template.
Retail-only keys
Retail builds write keys that the open source does not know.
| Key | Type | First seen | Default | Status | Source |
|---|---|---|---|---|---|
custom_warps | list of line-generator states, one per oscillator | 1.5.3 | open | checked | 8 of 24 sampled presets |
random_values | list of {seed} objects, one per random LFO | 1.5.3 | open | checked | the same sample |
osc_1_spectral_morph_phase to osc_3_ | number | 1.5.3 | open | checked | the same sample |
Stable 1.0.7 writes none of them. It loads a file that holds them, and so
does the open engine, which drops them on save (verified, load check: the
retail-keys variant). preset inspect marks them. Whether builds 1.6.x add
more keys is open.
Template
| Fact | Build | Status | Source |
|---|---|---|---|
| The built-in template holds all 772 controls at their registry defaults, 64 empty modulation slots, three sawtooth wavetables, eight triangle LFO shapes and a seeded noise sample. | this project | checked | src/preset/vital/template.rs |
| Its key set and every default match a fresh open engine and the saved default state of 1.0.7. | open engine, 1.0.7 | verified | probe 8: 0 keys differ |
| It loads, and every control reads back unchanged. | open engine | verified | load check: the template variant |
| The built-in template is not the app’s Init preset. Its wavetables and sample are our own. | this project | checked | src/preset/vital/template.rs |
Parameters and laws
The registry in
src/preset/vital/registry.json
holds 794 controls. 22 are legacy keys that no module creates. Our own tool
extracts the names, ranges, defaults, engine scales and labels from the
source; the units, laws and provenance are ours.
| Fact | Build | Status | Source |
|---|---|---|---|
| The open engine and 1.0.7 create exactly the 772 non-legacy controls. | open engine, 1.0.7 | verified | probe: key sets |
The 1.0.7 default state holds all 772 controls at the registry default. These carry verified_on = 1.0.7, which confirms the key and the default only. | 1.0.7 | verified | load check: the 1.0.7 default state |
| The open engine reports the same range, default and first build as the registry for 763 controls; the other 9 are listed below. The registry evidence says which. | open engine | verified | probe: control details |
| A filter cutoff follows Hz: stored 66.86 shows about 390 Hz. | 1.0.7 | verified | probe: a UI edit read back from the saved file |
Display laws read and typed in the value box, each matching the registry law: portamento_time is seconds (0.5 s stores -1), osc_1_unison_detune is percent (25 % stores 5), filter_1_cutoff shows semitones (12 stores 72), distortion_drive is dB (12 stores 12). The registry tags these four measured, with verified_on = 1.0.7. | 1.0.7 | verified | probe: values typed in the app and read back from the saved file |
A route amount shows in the destination’s units, as the amount times the destination’s stored span. On filter_1_cutoff, span 128, 64 semitones stores 0.5. preset apply converts a semitone amount the same way. | 1.0.7 | verified | probe: a value typed in the app; unit tests of the edit list |
An LFO tempo menu stores the index of its label: 2/1 is 5 and 1/4 is 8. | 1.0.7 | verified | probe: a UI edit read back from the saved file |
The macOS bundles of 1.0.7 are x86_64 only, checked with lipo -archs on 2026-10-26. A plugin host running natively on arm64 cannot load them. On Apple silicon an x86_64 app runs under Rosetta. | 1.0.7 | verified | probe 1; Rosetta is a platform fact |
delay_tempo and delay_aux_tempo show the menu label at the stored value itself. Value 4 is 4/1. The vita binding counts from the range start instead and shows Freeze. | source 1.0.6 | checked | synth_slider.cpp; the binding differs |
filter_*_style has values 0 to 9 and five labels. | source 1.0.6 and open engine | verified | probe: control details |
osc_*_destination and sample_destination allow 0 to 14, but only 0 to 13 have labels. The open engine reports 13 as the top. | source 1.0.6 | checked | synth_parameters.cpp; the binding differs |
| An oscillator level is the square of its stored value: . | open engine | verified | render: halving the stored level quarters the amplitude |
The outer unison offset is detune_range times unison_detune squared, in cents. | open engine | verified | render: range 2 and detune 10 give 200 cents |
Unison voice offsets follow laws::unison_offsets for 4 and 8 voices at powers 0 and 1.5, within 0.03 cents. Odd counts are not rendered. | open engine | verified | render, unit test offsets_match_the_render |
Unison gains follow laws::unison_gains for 3, 4, 5 and 8 voices, within 0.001 of the single-voice amplitude. A gain belongs to a pair of voices, which share it equally. | open engine | verified | render, unit test gains_match_the_render |
The voice gain is , with envelope , velocity , velocity track and voice_amplitude . | open engine | verified | render, unit test the_amp_gain_squares_like_the_render |
Laws tagged interim in the registry: resonance, filter drive, filter shape, distortion except distortion_drive, compressor, EQ, effect feedback, depth and mix, reverb, oscillator warp, curve powers, and the controls with no display unit. | source 1.0.6 | checked | src/preset/vital/registry.rs; each carries a tuning note |
The interim laws wait for the render calibration. They are not settled here.
Modulation, LFO shapes, wavetables and names
| Fact | Build | Status | Source |
|---|---|---|---|
modulations holds 64 {source, destination} entries. A slot’s amount, bipolar flag, power, stereo and bypass are the controls modulation_N_*. | open engine | verified | probe: a fresh engine’s saved state |
| The 32 source names. A route from each of them loads. | open engine, 1.0.7 | verified | load check: the every-source variant; in 1.0.7 the matrix shows 32 rows, and the value box reads 12.8 for amount 0.1 into the cutoff |
| A route amount is a fraction of the destination’s stored range: . | source 1.0.6 | checked | laws::modulation_delta |
Routes target numbers, counts, menus such as osc_1_transpose and lfo_1_tempo, and the amount or power of another slot, never a switch. preset apply refuses a switch and the bipolar, stereo and bypass flags of a slot. | 1.0.x | verified | observed in factory presets of Vital 1.0.x |
A Wave Source holds name, author, version, remove_all_dc, full_normalize and groups. A component holds type, interpolation, interpolation_style and keyframes. A keyframe holds position and wave_data. | open engine | verified | probe 9: the engine’s saved state has the same fields as ours |
wave_data is base64 of 2048 little-endian 32-bit floats: 10924 characters. Positions run from 0 to 256. | source 1.0.6 | checked | wave_source.cpp |
| Tables of 1 and of 257 keyframes load. One keyframe shows a single frame; with 257 the frame position sweeps the wave. | open engine, 1.0.7 | verified | load check: the wavetable variants |
An LFO shape holds name, num_points, points, powers and smooth, with one power per point. A shape of 100 points loads. | open engine, 1.0.7 | verified | probe 9 and the 100-point variant |
A stored point y is drawn upside down: the output is , so a stored 0 is the top. The default triangle stores and peaks at the centre. LfoShape takes outputs and stores . | 1.0.7 | verified | load check: a shape of rising stored values showed falling saws |
effect_chain_order codes the effect list, top to bottom. Code 0 is Chorus, Compressor, Delay, Distortion, EQ, Filter, Flanger, Phaser, Reverb. Number the effects in that order. Each effect after the first adds a mixed-radix digit, the count of effects above it with a higher number, in base its position plus one. The first digit is the most significant: for from 1 to 8, . The codes run from 0 to . | 1.0.7 | verified | probe 10: lists rearranged in the app saved 260650, 8 and 181440; unit tests the_effect_order_codes_match_vital and every_effect_order_code_round_trips |
Fixtures
The fixtures in tests/fixtures/vital/ are generated by
cargo xtask vital-fixtures and listed with their SHA-256 in
tests/fixtures/PROVENANCE.md. cargo xtask vital-fixtures --check
checks them.
| File | Holds | Tests |
|---|---|---|
minimal.vital | the stamp and the three sample fields | every_fixture_meets_the_format_contract |
marks.vital | every value preset inspect marks | the preset-inspect command cases |
routes.vital | three routes, an LFO shape, a two-frame wavetable | the_routes_fixture_holds_its_generated_values, the_routes_wavetable_has_two_full_frames |
stamps/*.vital | the version-gate set | the_stamps_pass_the_version_gate |
The round trip read, write, read is checked for every fixture by
every_fixture_meets_the_format_contract.
Limits
| Fact | Build | Status | Source |
|---|---|---|---|
| The reader refuses files over 64 MiB and nesting over 128 levels. | this project | verified | unit tests the_size_cap_applies_to_both_reads, deep_nesting_names_the_depth_limit |
| Three tables of 257 keyframes come to 8,422,404 bytes of base64. A sample adds more. | source 1.0.6 | checked | wave_source.cpp |
| Parsing takes about 3 times the file size for long texts and up to about 38 times for short numbers. | this project | verified | measured in a release build |
| Ten minutes of fuzzing per target found no crash. | this project | verified | fuzz/: 5.4 million reads, 4.0 million round trips, 95 thousand writes |
Builds seen
| Build | Seen how |
|---|---|
| source 1.0.6 | read, commit 636ca0e |
open engine (vita 0.1.0) | probes and load checks of ours |
| stable 1.0.7 | probes and load checks of ours |
| 1.5.3, 1.5.5 | sampled presets |
| early access 1.6.4 | reported, not checked |
Terms
Factory presets, samples and wavetables are not redistributable, and no file here holds them. The source is GPL-3.0-or-later. This page states facts in our own words and copies no code, comment or table.
Plugin state
A host loads a preset into the VST3 plugin through its state, not through a
file. The rows below come from the host spike (issue 244):
the retail plugin loaded headless as a library in an x86_64 host process
through vst3-host 0.10.0, with no editor window. The probe output is in
issue 10.
| Fact | Build | Status | Source |
|---|---|---|---|
The plugin state is the preset JSON plus a tuning object, followed by a NUL. | source 1.0.6 | checked | synth_plugin.cpp |
| The macOS bundles hold only an x86_64 binary, so a host process must run as x86_64. | 1.0.7 | verified | lipo -archs of the bundle |
| The plugin loads and renders with no editor window. The probe process wrote nothing to standard error and exited with code 0. | 1.0.7 | verified | host spike: minimal.vital named SpikeProbe with env_1_attack 1, filter_1_on 1 and filter_1_cutoff 50, MIDI 60 at velocity 100 |
| The single audio output bus has 2 channels and reports itself inactive after the load. With the bus left inactive, the render was silent; after the host activated it, the render sounded. | 1.0.7 | verified | host spike: minimal.vital named SpikeProbe with env_1_attack 1, filter_1_on 1 and filter_1_cutoff 50, MIDI 60 at velocity 100; default patch |
The saved component state starts with VstW, then CcnK at byte 16. The preset JSON starts at byte 176 and is followed by a NUL, zero bytes and the text JUCEPrivateData. The JSON holds tuning and the app’s stamp 1.0.7. The plugin saves no controller state. | 1.0.7 | verified | host spike: minimal.vital named SpikeProbe with env_1_attack 1, filter_1_on 1 and filter_1_cutoff 50, MIDI 60 at velocity 100; state read-back |
A bare preset JSON passed to IComponent::setState loads, with or without a trailing NUL. The saved state then holds the name SpikeProbe and the key filter_1_cutoff. | 1.0.7 | verified | host spike: minimal.vital named SpikeProbe with env_1_attack 1, filter_1_on 1 and filter_1_cutoff 50, MIDI 60 at velocity 100; state read-back |
A host needs no VC2! envelope, the form some Python hosts save: the bare JSON loads. | 1.0.7 | verified | host spike: minimal.vital named SpikeProbe with env_1_attack 1, filter_1_on 1 and filter_1_cutoff 50, MIDI 60 at velocity 100 |
A state without tuning keeps the instance’s current tuning: setStateInformation loads tuning only when the key is there. The app reads no tuning from a preset file. | source 1.0.6 | checked | synth_plugin.cpp, synth_base.cpp |
Tuning::jsonToState reads scale_start_midi_note, reference_midi_note, tuning_name, mapping_name and scale without a check, and default and mapping when present. A missing key or a wrong type throws, and the plugin then shows a dialog box. The default tuning starts the scale at MIDI 60 with reference 0 and the scale 0 to 12. | source 1.0.6 | checked | tuning.cpp, synth_plugin.cpp |
In one plugin instance, a preset with reference_midi_note 2 played MIDI 60 at 293.66 Hz. A preset without tuning loaded next still played at 293.66 Hz. With the default tuning added to its state, it played at 261.63 Hz. | 1.0.7 | verified | render_vital::a_tuning_does_not_carry_over_to_the_next_preset: minimal.vital, the lead material at 120 BPM |
In a host, the plugin sets the engine’s tempo from the host’s tempo on every block, so beats_per_minute, in beats per second, follows the host whatever the preset holds. SoundEngine::setBpm divides the tempo by 60 in 32-bit floats. | source 1.0.6 | checked | synth_plugin.cpp, sound_engine.cpp SoundEngine::setBpm |
The plugin stores f32(bpm) * f32(1 / 60), a multiply by the reciprocal: at 140 BPM it saves 2.3333334922790527, and at 100.3 BPM a value two f32 steps from the correctly rounded 100.3 / 60. A preset with beats_per_minute 2 rendered at another tempo saves that value. | 1.0.7 | verified | render_vital::a_render_at_another_tempo_passes_the_load_check at 140 and 100.3 BPM, and the_saved_tempo_follows_the_model_over_a_sweep: every whole tempo 4 to 1000, every tenth 4.0 to 400.0 and 600 random tempos on 0.01 and 0.001 steps |
| A loaded preset renders unlike the default patch: RMS 0.017 for the probe preset against 0.118 for the default patch. | 1.0.7 | verified | host spike: minimal.vital named SpikeProbe with env_1_attack 1, filter_1_on 1 and filter_1_cutoff 50, MIDI 60 at velocity 100; default patch |
| Two renders of one patch differ sample by sample. | 1.0.7 | verified | host spike: minimal.vital named SpikeProbe with env_1_attack 1, filter_1_on 1 and filter_1_cutoff 50, MIDI 60 at velocity 100; default patch |
| In one probe, offline against realtime processing changed the render by less than two repeat renders differ: a difference RMS of 0.0100 against 0.0132. | 1.0.7 | verified | host spike: minimal.vital named SpikeProbe with env_1_attack 1, filter_1_on 1 and filter_1_cutoff 50, MIDI 60 at velocity 100 |
| Whether the block size changes the render by more than repeat renders differ. In one probe, blocks of 64 against 512 samples gave a difference RMS of 0.0235, against 0.0132 between two repeat renders. The 64-sample render ran in a second instance. | 1.0.7 | open | host spike: minimal.vital named SpikeProbe with env_1_attack 1, filter_1_on 1 and filter_1_cutoff 50, MIDI 60 at velocity 100 |
IMidiMapping returns a parameter for CC 1, CC 7, CC 64, channel aftertouch and pitch bend. A full bend up raised MIDI 60 by 2 semitones. | 1.0.7 | verified | host spike: default patch, MIDI 60 with and without a full bend; test a_full_pitch_bend_raises_the_note_two_semitones |
CC 1 from the host moves a route from mod_wheel: with the route at -1 on osc_1_level, a full wheel lowered the level. | 1.0.7 | verified | test the_mod_wheel_reaches_a_route |
A state stamped 1.7.0 loads nothing. The plug-in keeps its init patch and saves the init patch back. | 1.0.7 | verified | test a_newer_stamp_falls_back_to_init |
A settings key that Vital does not know is gone from the state it saves after the load. | 1.0.7 | verified | test a_key_vital_drops_is_a_readback_mismatch |
After a load, the saved state holds every control and the name as sent, a re-encoded sample.samples text and a new version in each wavetable. | 1.0.7 | verified | render of routes.vital |
| In one probe, a note right after the state load differed from a note after 0.5 s of silence by less than two repeat renders differ. | 1.0.7 | verified | host spike: minimal.vital named SpikeProbe with env_1_attack 1, filter_1_on 1 and filter_1_cutoff 50, MIDI 60 at velocity 100 |
| A second instance in the same process, with blocks of 64 samples, loaded the probe preset and rendered it. | 1.0.7 | verified | host spike: minimal.vital named SpikeProbe with env_1_attack 1, filter_1_on 1 and filter_1_cutoff 50, MIDI 60 at velocity 100 |
The standalone app’s --headless --render flags. | 1.0.7 | untestable by rule | the project never runs the app |
Open items
Stable 1.0.7 loaded a written preset with its edits, refused the newer
stamps and re-saved the preset with every control kept. The ignored test
a_preset_saved_by_vital_keeps_the_edits reads that re-save from
VITAL_SAVED_PRESET and passes. The unknown-key, no-name, every-source,
LFO-shape and wavetable variants loaded in 1.0.7 with no dialog.
A bare preset JSON loads as plugin state in an x86_64 host (see Plugin state).
Still open:
- optionally, the stamps and the retail-only keys of early access 1.6.4.
- whether the block size changes a render by more than repeat renders differ. The recipe fixes blocks of 512 samples, so renders compare at one block size.
Sources
- Vital source, https://github.com/mtytel/vital, GPL-3.0-or-later, facts
only, commit
636ca0e.
Layer report
analyze writes layers.json, the report that an agent reads before it
writes an edit list. This page lists the fields of version layers/1. The
committed JSON Schema,
schemas/layers.schema.json,
holds the same contract, and the test suite validates every generated
report against it.
Top level
| Field | Value |
|---|---|
schema | "layers/1". |
source | The file name of the stem, without its folder. A run of another stem into the same folder stops unless --force is given. |
source_sha256 | The SHA-256 of the stem file’s bytes, in lower-case hex. A run into the same folder of a stem with the same name and other bytes stops unless --force is given. Such a stem is another stem of that name or a new export of this one. A report without it, from release 0.31 or earlier, is matched by source alone. |
synth | The --synth value, "vital" or "serum2". Later mappers pick the synth from it; the layer fields stay synth-neutral. |
stem | The stem group below. |
transcription | The transcriber’s model and settings, the group below. null with --notes, which skips the transcriber. |
notes | The final note list of notes.mid, the group below. null without notes. |
pitch | The pitch layer group below: glides, pitch drops and vibrato. |
voices | The voices layer group below: the count of voices and the joins of a mono line. |
envelope | The envelope layer group below: the amplitude envelope of the notes and the sidechain pump. |
conventions | The convention of every number in stem and of every record of the layer groups, keyed by its field name, plus frame_size, statistics, spectral and confidence. |
The top level takes no other field. notes is no layer group. Each layer adds one group with
values, confidence, flags, needs_render, suggested_edits and
unrepresentable, and extends the schema in the same change. A group with
too little evidence writes values: null and says why in its confidence.
values holds plain numbers with the unit in the field name.
suggested_edits is a list of records in the edit-list format, each a
param or a route with unit strings, so a route keeps its source, amount
and polarity. Each record’s reason is the group’s name. The mapper of the
report’s synth fills the list; see Suggested edits.
unrepresentable lists the values of the group that the synth cannot reach:
| Field | Value |
|---|---|
field | The path of the value in the group’s values, with a dot into a list’s records, such as glides.time_s. |
wanted | The measured value, {"value": 20.0, "unit": "s"}, or a choice name. The unit is count for a whole number and empty for a stored value without a unit. |
nearest | The nearest value the synth reaches, in the same form. Absent when it has none. |
reason | above_range, below_range, no_such_choice, not_in_synth or slot_limit. |
residual | nearest minus wanted, when both are numbers in one unit. |
A synth whose mapper writes no edit for a group leaves both lists empty. Vital writes none for the pitch, voices and envelope groups yet.
The pitch group breaks reports of layers/1 from releases up to 0.23:
suggested_edits changed from an object of numbers to a list of records,
pitch is a new required top-level key, and conventions requires the
keys pitch, glides, drops, sub_band_line and vibrato. A reader of
older reports keeps their schema.
The envelope group breaks reports of layers/1 from releases up to 0.25:
envelope is a new required top-level key, and conventions requires the
keys envelope, note_off, level_spread_db and pump.
The tail breaks reports of layers/1 from releases up to 0.27: values of
envelope requires release_s, tail_rt60_s, tail_weight,
release_confidence, tail_votes, tail_vote_count and tail_is_reverb, and
conventions requires the key tail.
The voices group breaks reports of layers/1 from releases up to 0.26:
voices is a new required top-level key, and conventions requires the
keys voices and joins.
The mapping lists break reports of layers/1 from releases up to 0.28.x:
every group requires unrepresentable, and a suggested_edits record must
be a param or a route with a reason.
The notes group breaks reports of layers/1 from releases up to 0.30:
notes is a new required top-level key, and conventions requires the
key notes.
The checksum breaks reports of layers/1 from releases up to 0.31:
source_sha256 is a new required top-level key.
The transcription group
The group records what produced the notes, so a note that flips between two machines can be traced to its settings.
| Field | Value |
|---|---|
model_sha256 | The SHA-256 of the bundled model weights, 64 lowercase hex digits. |
crate_version | The version of the transcription crate, "0.1.0". |
profile | One of "bass", "lead", "pad" and "detected", the last when the stem chose the settings. |
note_params | Every decoder parameter by the crate’s own name: onset_threshold, frame_threshold, minimum_note_length_ms, minimum_frequency, maximum_frequency (both in Hz), infer_onsets, melodia_trick, include_pitch_bends, energy_tolerance and bend_offset. Numbers and booleans only. |
The notes group
The notes that analyze writes to notes.mid: the cleaned note list
rewritten with the pitch, voices and envelope groups. The
analyze guide explains the
rules.
| Field | Value |
|---|---|
file | "notes.mid". |
ticks_per_beat | 480. |
velocity | "level" when level_spread_db exceeds 3 dB and each velocity follows the note’s level, else "constant", 100 for every note. |
dropped | The notes of a mono line left out because another started on the same tick. Of such notes one stays: a note without the octave flag before one with it, then the longer, then the lower key. |
bend_events | The pitch-bend events in the file. A note of a mono line whose f0 contour stays 50 cents or more off its key, on one side, for at least the tracker’s reach of the note (a quarter frame plus one period), outside every glide, drop and vibrato span, gets bends, clamped to 2 semitones; every other note gets none. A host must set a bend range of 2 semitones; render reads no bend. |
list | One record per written note, by start, then key. |
A note record:
| Field | Unit | Range | Value |
|---|---|---|---|
onset_s | seconds | 0 or more | The note-on tick in seconds: round(seconds * bpm / 60 * 480) ticks. |
end_s | seconds | above 0 | The note-off tick in seconds. |
list_onset_s | seconds | 0 or more | The onset in the cleaned note list; the earliest onset of the notes a pitch drop merged. |
list_end_s | seconds | above 0 | The end in the note list; the latest end of the notes a pitch drop merged. |
key | MIDI note | 0 to 127 | The written key. |
velocity | 1 to 127 | The written velocity. | |
end_rule | What set the end: knee, no_knee (50 ms before the list end, read without a knee), unread (the same cut for a note the envelope layer skipped), pad (20 dB under the plateau on a polyphonic line, no later than the list end), minimum (the decoder’s minimum length by the BPM rule), next_note (the next note of a mono line) or glide (5 ms after the next note-on, at a glide join). | ||
level_db | dB | any | The note’s full-band level, as level_spread_db defines it; the highest of the notes a pitch drop merged; read from the note’s start to its end before the joins and cuts of a mono line, on a mono line no later than the next later onset, when that span holds no frame. Unlike peak_db, it compares notes on different harmonics. null when neither span holds a frame. |
octave_uncertain | flag | The transcriber’s octave flag of the note; null when no transcriber ran. |
Naming
- Field names are snake_case ASCII.
- A field with a unit carries it as a suffix:
_hz,_s,_ms,_db,_dbfs,_deg,_st(semitones) or_ct(cents). A field without a suffix is a count, a ratio, a share from 0 to 1, a power, a flag or a tempo in BPM. - Integers are written as integers; every other number is a float.
Value objects
A single estimate is {"value": ..., "confidence": ...}. A statistic over
active frames is {"median": ..., "iqr": ..., "confidence": ...}, the
median and the interquartile range. The value, or the median and the IQR,
is null exactly when the confidence states no estimate.
The confidence object follows the units page:
{"level": "high", "reasons": [{"code": "active_frames", "value": 412}], "spread": 310.0, "estimate": true}
levelislow,mediumorhigh.reasonsholds at least one{code, value}pair. The value is a count, a number or a flag.spreadis optional, in the unit of the value: the IQR of a statistic, one bin width for the band edge."estimate": falsemeans no value. Such an object has reasons and neither a level nor a spread.- An unknown field is an error.
The stem group
Every statistic over frames, band_share and band_edge_hz need at
least 16 active frames; with fewer they give no estimate with the reason
too_few_frames. The ranges are those of the unit types and the schema. Each convention is
the text that conventions holds under the same key; the
pipeline page explains the rules behind
them.
| Field | Unit | Range | Shape | Convention |
|---|---|---|---|---|
bpm | beats per minute | 4 to 1000 | number | The tempo of the beat grid and of every later step: bpm_refined when its confidence is high, else bpm_given. |
bpm_given | beats per minute | 4 to 1000 | number | Given by --bpm. |
bpm_refined | beats per minute | 4 to 1000 | value | The tempo refined from the onsets, from 2.91 % below to 3.09 % above bpm_given, in 0.01 BPM steps; see the tempo section. Its reasons give tempo_peak, beat_periodicity, sparse_periodicity, onset_sharpness, onsets_per_beat and the whole beats of the stem as cycles_in_span, both counted at the slowest tempo of the search, bpm_given / 1.03. No value with --bpm-exact (bpm_exact), for a stem of fewer than 8 such beats or for one without onsets. |
duration_s | seconds | 0 or more | number | Decoded frames over the sample rate. |
sample_rate_hz | hertz | 8000 to 192000 | integer | The native rate of the file; no resampling. |
band_edge_hz | hertz | 0 to Nyquist | value | The steepest fall of at least 25 dB per semitone above 2 kHz on a plateau, else the highest bin 30 dB above the floor at low confidence. |
hard_limited | flag | boolean | Flat runs of at least 1 ms within 1e-5 of the channel peak, entered and left from at least half the peak, cover 0.1 % of one of the first two channels. | |
peak_dbfs | dB re full scale 1.0 | 0 or less for unclipped audio | value | Peak of the mono mix over the whole stem. |
rms_dbfs | dB re full scale 1.0 | below peak_dbfs | value | RMS of the mono mix over the whole stem; a full-scale sine reads -3.01 dB. |
crest_db | dB | 0 or more | value | peak_dbfs - rms_dbfs. |
centroid_hz | hertz | 0 to Nyquist | statistic | Magnitude-weighted mean frequency per active frame. |
centroid_over_f0 | ratio | 0 or more | statistic or null | The centroid over the median f0 of the note list; null without --notes. |
spread_hz | hertz | 0 or more | statistic | Magnitude-weighted standard deviation around the centroid. |
rolloff85_hz | hertz | 0 to Nyquist | statistic | Frequency below which 85 % of the frame’s magnitude lies. |
rolloff95_hz | hertz | 0 to Nyquist | statistic | The same at 95 %. |
flatness | ratio | 0 to 1 | statistic | Geometric over arithmetic mean of the magnitude, 100 Hz to 10 kHz, floored at 1e-5. |
band_share | share | each 0 to 1, sum 1 | value with a list | Power per band over active frames divided by the power in the bands reported; each item is {lo_hz, hi_hz, share} for the bands 20, 60, 250 Hz, 1, 4, 10 and 20 kHz. A band at or above the Nyquist frequency is left out, and one that crosses it ends there. |
stereo | object | See below. |
stereo holds full, sub (below 120 Hz), low_mid (120 Hz to 1 kHz)
and high (above 1 kHz), each {rho, sm_db, ipd_deg}, and one
confidence for the screen:
| Field | Unit | Range | Convention |
|---|---|---|---|
rho | correlation | -1 to 1 | Pearson correlation of L and R at zero lag; per band from the 8192-point STFT. |
sm_db | dB | any | 20 log10(rms(S) / rms(M)) with M = (L + R) / 2, S = (L - R) / 2. |
ipd_deg | degrees | 0 to 180 | The magnitude-weighted phase difference of L and R at harmonics 1 to 20 of the median f0, averaged over active frames; null without --notes. |
A stereo value is null when it does not exist: a one-channel file, a
silent channel, or a zero mid or side.
The pitch group
The pitch layer of a monophonic line; the
pitch layer page explains each method. Every
record carries its own confidence, always an estimate. The group’s own
confidence is medium, or low when fewer than half of the track frames
inside the notes are voiced within 100 cents of a sounding note, as under
noise or DC. Its reasons are voiced_frames, the voiced frames of the f0
track, and note_frames, that share.
values is null when the line plays chords (reason polyphonic_line)
or the stem has no notes (reason no_note_list). Otherwise it holds three
lists, each in time order and each empty when nothing was found:
glides, drops and vibrato. Every time is audio time from the start
of the stem.
A glide:
| Field | Unit | Range | Value |
|---|---|---|---|
start_s | seconds | 0 or more | The fitted start of the motion. |
interval_st | semitones | any | From the start pitch to the end pitch; negative for a glide down. |
time_s | seconds | 0.09 or more | The full 0 to 100 % time. |
curve | power | above 0.21, at most 5 | p of s(x) = 1 - (1 - x)^p: above 1 starts fast, 1 is linear in pitch. A fit within 5 % of the lower bound 0.2 is no glide; one within 5 % of the upper bound 5, such as an exponential portamento, needs a detected span of at most 2.5 times its fitted time, and reads low with curve_bound. |
from_silence | flag | The glide leads into the first stable pitch of a voiced run whose sound starts up to 100 ms before it. That stable pitch has a median level within 40 dB of the loudest frame, and the pitch does not swing back half the lead within 167 ms after it, unless the run’s next stable pitch lies 100 cents or more away and matches another note, and the pitch, once past half the way to it, stays there up to that stable pitch and to the window’s end. |
Its reasons are motion_span (seconds), fit_residual (cents),
pitch_jitter (cents) and curve_bound (flag). A glide whose
motion_span is under 0.09 reads low.
A drop:
| Field | Unit | Range | Value |
|---|---|---|---|
onset_s | seconds | 0 or more | The onset of the strike from the band envelope. |
f_end_hz | hertz | above 0 | The settled frequency. |
df_st | semitones | above 2, at most 36 | The drop at the onset, 12 log2((f_end + df) / f_end). |
df_hz | hertz | above 0 | The same in hertz above f_end_hz. |
tau_s | seconds | above 0.003 | The time constant of f_end + df exp(-t / tau), above the 3.04 ms floor of the search. |
residual | share | 0 to 0.15 | The RMS residual of the fit over the fall it shows from 7 ms on, df_hz exp(-7 ms / tau_s); with tau_s of 0.01 or more, of the error averaged over half a period of f_end_hz, or over a full period when tau_s spans 1.5 periods or more. |
Its reasons are fit_residual and attack_time (seconds): the time the
band envelope of the strike takes from 10 % to 90 % of its peak. A drop
with an attack over 30 ms reads low, and one with tau_s under 0.01 or an
attack over 12 ms at most medium.
A vibrato:
| Field | Unit | Range | Value |
|---|---|---|---|
start_s, end_s | seconds | 0 or more | The first and last frame read. |
rate_hz | hertz | 2.97 to 12.12 | The rate, 3 to 12 Hz with 1 % slack. |
extent_ct | cents | 10 or more | Half the peak-to-peak swing, after the correction. |
extent_raw_ct | cents | 0 or more | The same as the f0 track reads it. |
extent_factor | ratio | above 0 | The correction for the track’s smoothing at this rate; extent_ct = extent_raw_ct * extent_factor. |
share | share | 0.5 to 1 | The power the fitted sine explains over the power left after the line alone. |
Its reasons are sine_share, cycles_in_span and contour_kurtosis. A
vibrato holds 2.5 cycles or more; from 4 it reads high. A kurtosis below
1.25 reads low, since the contour jumps between two levels, as a trill
does, and so does a vibrato on a pitch below 100 Hz.
flags holds four booleans: chord_skipped, glide_too_short (a motion
between two notes lasted 35 to 90 ms over its fitted time, or over its
detected span when the fit failed), weak_fundamental (a low note’s
second harmonic exceeded its fundamental by 6 dB, so its drop was not
reported) and no_notes. At a sample rate under about 4 times a note’s
pitch the second harmonic cannot be measured, and that note is not
tested for weak_fundamental.
needs_render is false.
The voices group
The count of voices, read on any line, and the joins of a mono line; the
voices layer page explains each method. values
is null without notes (reason no_note_list) or when no note holds a sounding
part in the stem (reason too_few_notes). The group’s confidence reads
the share of the held time whose count agrees with the verdict (reason
count_agreement). flags is empty and needs_render is false.
| Field | Unit | Range | Meaning |
|---|---|---|---|
mono | max_polyphony is 1. | ||
max_polyphony | count | 1 or more | The most voices that have started by any onset and are held 50 ms after it. |
mean_polyphony | count | 0 or more | The mean count over the time any note is held; it can exceed max_polyphony. |
joins | One record per join, in time order; empty on a line that is not mono. |
A join record:
| Field | Unit | Range | Meaning |
|---|---|---|---|
time_s | seconds | 0 or more | The later note’s onset. |
gap_ms | milliseconds | under 30 | The later onset minus the earlier end in the note list; negative when they overlap there. |
dip_db | dB | 0 or more | How far the level falls at the join under the quieter note; null when neither note has a level beside the join. |
centroid_change | ratio | The centroid over f0 after the join over the same before it, minus 1; null when either side holds no signal or runs past either end of the stem. | |
legato | dip_db is under 1; null without a dip. | ||
confidence | Low when the verdict is legato (reason level_kept_at_join), no attack was read (no_attack_read), the stem’s edge reaches a level window beside the join (stem_edge_at_join) or the median attack is over 100 ms (attack_hides_join), else medium; no estimate without a dip. |
The envelope group
The amplitude envelope over the pooled notes, each read note’s record, and
the sidechain pump; the
envelope layer page explains each method.
values is null when no note was read: the stem has no notes (reason
no_note_list) or every note was skipped (reason too_few_notes). The
group’s own confidence is medium with 3 or more pooled notes, else low.
| Field | Unit | Range | Value |
|---|---|---|---|
attack_s | seconds | 0 or more | Statistic: 1.25 times the 10 to 90 % rise of each pooled note to the attack’s top, its full time for a linear ramp. An upper bound when attack_is_upper_bound is set. |
hold_s | seconds | 0 or more | Statistic: the time at the peak before the decay, 0 under 10 ms. |
decay_s | seconds | 0 or more | Statistic: the fall from the peak to the sustain level, from the 10 and 90 % shares of the fall in dB. |
sustain_db | dB | 0 or less | Statistic: the level of the last 30 % of the held part, from the peak. |
note_off_offset_s | seconds | any | Statistic: the note’s end in the note list, or the stem’s end when that comes first, minus its note-off knee, over the pooled notes with a knee. |
release_s | seconds | 0 or more | Statistic: the linear release of each pooled note with a tail fit; null when the median tail_weight is over 0.5 (reason tail_weight, a tail too heavy) or the median residual is over 3 dB (reason tail_residual). |
tail_rt60_s | seconds | 0 or more | Statistic: the RT60 of the tail, over the notes with a tail_weight of 0.05 or more whose frames span a 15 dB fall of it, leaving out notes that read as heavy and smooth or under what their band shows; null with tail_light, a tail too light, under a median tail_weight of 0.05, and with tail_smooth when the tail is heavy and smooth, as the note’s own exponential release is, and with tail_band when it fades faster than the band of an overlapping note shows. |
tail_weight | ratio | 0 or more | Statistic: the tail’s level at the note-off over the level before it. |
release_confidence | medium, low or null | low when no release_s is written, the level of release_s otherwise, which reads low with tail_wobble when the level wobbles over the hold and the fit is poor, null when no pooled note has a fit. | |
tail_votes | Five signs that the tail is reverb, each true, false or null when nothing could be read: outlasts_gap, side_grows, flatness_rises, same_rate, high_band_faster. | ||
tail_vote_count | count | 0 to 5 | The votes that are true. |
tail_is_reverb | false until a space layer exists. | ||
level_spread_db | dB | 0 or more | Value: the 90th minus the 10th percentile of the full-band levels of the pooled notes; always low. A note’s full-band level is the peak of the centred RMS of the mono mix over one period of its fundamental, or the fewest whole periods that span 2 ms, on a 1 ms hop, from its onset to its note-off knee, or to 50 ms before its list end without one, never past the next later onset on a mono line, in dB of full-scale power. |
ripple_db | dB | 0 or more | The median ripple of the sustain window after its line; null without a sustain. |
fit_residual_db | dB | 0 or more | The median RMS of the held parts against the hold, decay and sustain model. |
n_notes_used | count | 0 or more | The pooled notes. |
overlap_fraction | share | 0 to 1 | The share of the notes in the list that overlap another. |
pump_depth_db | dB | 0 or more | Minus the minimum of the pump curve; null with fewer than 4 counted beats. |
pump_recovery_s | seconds | 0 or more | From the curve’s minimum back to within 1.5 dB, over the beat’s wrap; null when it does not within a beat. |
pump_tau_ms | milliseconds | 0 or more | pump_recovery_s over ln(pump_depth_db / 1.5), the one time field in milliseconds; null unless the pump locks to the beat. |
pump_locked_to | beat, note, free or none; null without a pump. | ||
pump_beats | count | 0 or more | The beats behind the curve. |
pump_bpm | BPM | above 0 | The tempo the curve was read at: bpm for a read that stays on the grid, or a fitted tempo up to 2.81 % from it, by the longest run of counted beats, when that read replaced it. A sidechain’s rate follows a fitted tempo closely. A read that stays on the grid says only that the duck locks there: up to 2.56 % off on generated ducks, as the envelope page says. null without a pump. |
notes | One record per read note, in note order. |
A statistic reads low over fewer than 3 notes or with an interquartile
range above half its median. Its reasons are too_few_notes and
relative_spread; attack_s adds upper_bound_share, the share of the
pooled attacks that are upper bounds. A median of 0 under an interquartile
range above 0 has no ratio, so the statistic reads low and writes
zero_median, that range in the statistic’s unit, in place of
relative_spread.
A note record:
| Field | Unit | Range | Value |
|---|---|---|---|
onset_s | seconds | 0 or more | The onset in the note list. |
peak_db | dB | any | The peak power of the note’s amp proxy, the one harmonic it reads; two notes compare directly when they read the same harmonic. |
attack_s | seconds | 0 or more | The note’s attack, as above; null when the rise is not read. |
attack_upper_bound | flag | The note’s attack is an upper bound. | |
knee_s | seconds | 0 or more | The note-off knee; null for a note that decays at one rate. |
pooled | flag | The pool holds the note. |
flags holds three booleans: no_notes, attack_is_upper_bound (more
than half the pooled attacks are upper bounds, so a preset should not
take the attack) and pump_stripped (the pump locks to the beat over 8
counted beats or more, and its curve was taken out of the note tracks).
needs_render is false.
Conventions
| Key | Holds |
|---|---|
frame_size | The STFT frame length of the descriptors and the band edge: at 44.1 kHz 8192 when the note list has a median f0 below 100 Hz, else 4096, and 4096 without a note list; at another rate the power of two nearest, in log, to that length times the rate over 44100 Hz. |
statistics | Median and IQR over active frames, with linear-interpolation quantiles; the active-frame gate. |
spectral | Magnitude weights for shape, power for band shares; the window and the hop. |
confidence | What the level, the reasons and the spread mean. |
every stem field | The convention of that field, as in the tables above. |
pitch | What the pitch layer reads, when values is null, and the time base. |
glides, drops, vibrato | The method and the rule of each list of the pitch group. |
sub_band_line | The line the drops read. |
envelope | The tracks, the amp proxy, the stages and the pool of the envelope group. |
note_off | The note-off knee and note_off_offset_s. |
tail | The release, the tail fit, the write rule and the votes. |
level_spread_db | The level spread. |
pump | The pump’s track, beats, curve, fields and lock, and when it is stripped. |
notes | The ticks, the merges, the end rules, the level and the velocity law of the notes group. |
Render record
render writes a record next to each WAV file: everything the audio
depends on, so a later comparison can cite it and analyze the audio with
the same notes. The committed JSON Schema,
schemas/render-record.schema.json,
holds the same contract, and the test suite validates records against it.
Times are in seconds and the tempo in beats per minute.
| Field | Value |
|---|---|
identity.synth | "vital" or "serum2". |
identity.build | The plug-in’s build, such as "1.0.7". |
identity.host | The host program and the vst3-host version it is built on. |
identity.arch | The architecture the host ran as, such as "x86_64". |
recipe_version | The version of the render recipe. Any change to the recipe raises it. |
sample_rate | 44100 in recipe version 1. |
block_size | The samples per processing block, 512 in recipe version 1. |
process_mode | "offline": the host tells the plug-in that it renders a file. |
settle | The silence rendered after the state loads and before the warm-up note. |
oversampling | The oversampling of a Vital render, "2x" for every render of recipe version 1. |
bpm | The tempo the MIDI file was read at; synced LFOs and delays follow it. |
last_note_off | The end of the last note. |
length | The clip length. |
tail_capped | true when the tail still sounded 30 s after the last note-off and the clip was cut there. |
midi_sha256 | The SHA-256 of the MIDI file, 64 lowercase hex digits. |
load_check | The load check: init_distance_db, init_spread_db, margin_db and init_like, which is true when the clip lies within the margin and render warned. |
midi_sha256 and load_check are absent from a record whose notes came
from no file or whose render skipped the load check, such as the renders
of the test suite.
Before the clip
The record names the recipe, and the recipe fixes what plays before the clip, in one processing run with the clip:
- 0.5 s of silence, so smoothed parameters settle.
- A warm-up note, MIDI 60 at velocity 100 for 0.5 s in a 1 s step, which clears what an earlier render left behind.
- 0.5 s of silence again.
The host renders each step in whole blocks, so a step lasts up to one block longer. Its audio is dropped, and the clip starts on a block boundary at the transport’s start.
Render spread
Two renders of one preset are not identical: on one Vital instance, each render differs a little from the one before. The spread file says how far renders of one preset lie apart, so a comparison can tell a real difference from render noise. It is the noise floor that calibration, compare, refine and the bench cite.
cargo xtask render-spread --out spread.json writes it: the
testing page gives
the method. The file is local and depends on the synth’s build and its
quality setting. The repository keeps no spread of record; the
per-recording metrics are on the
reference recordings page
only.
The draws
A fresh Vital instance renders bit-identically to any other fresh instance, and each later render on one instance differs: an ignored Vital test pins both. So the 20 renders of a spread are drawn in sequence on one fresh instance, and a set with a repeated render fails. Two workers would each render the same sequence and turn 20 renders into 10 distinct ones.
A spread takes 3 to 256 renders. Fewer leave no leave-one-out median of two distances. After 256 renders the render session restarts its instance, whose first render repeats the first draw.
The statistic
For each output, each of the 190 pairs of renders gives a distance: the
absolute difference of the output’s values, or for an output of two
clips, such as the envelope distance, the distance itself. The spread is
the 95th percentile of those distances: numpy.quantile’s default,
linear between the two nearest pairs. It says how far two renders lie
apart, in the output’s own unit.
The tolerance
A comparison of one clip with the renders takes the median of the clip’s distances from each render. The tolerance measures the renders the same way: for each render, the median of its distances from the other 19. Sorted in ascending order, the tolerance is the median at rank ⌈0.95 (n + 1)⌉, at most n. For 20 renders that is the 20th, the largest.
The rank is the quantile of conformal prediction. If a clip of the same sound and the n renders were exchangeable, the clip’s median would lie at or below the median at rank k with a probability of k / (n + 1): for 20 renders 20 / 21, 95.2 %. The clip’s median runs over n distances and each render’s over n - 1, so this holds only nearly. Below 19 renders the rank stops at n and the coverage falls below 95 %: 3 renders cover 3 / 4. The file records the coverage.
held_out_within measures the coverage on the renders themselves. On
the renders of the 38 reference recordings, each render held out in
turn against the other 19 lies within their limit in 94.6 % to 95.5 %
of the cases per output, as the
reference recordings page
shows. The 95th percentile of the medians, the tolerance before the
rank, lies near the second-largest of 20 and covered 90.7 % to 91.3 % in
the same check.
Fields
| Field | Value |
|---|---|
identity | The synth, its build, the host and the architecture, as in the render record. |
recipe_version | The recipe version of the renders. |
oversampling | The quality setting: "2x" for every Vital render of recipe version 1. |
n | How many renders, 3 to 256 and 20 by default, each a distinct draw. |
statistic | "p95_pairwise_distance". |
tolerance | "conformal_95_leave_one_out_median". |
coverage | The share of clips of the same sound that lie within the tolerance: the rank over n + 1, 20 / 21 for 20 renders. |
preset_sha256 | The SHA-256 of the preset file. |
midi_sha256 | The SHA-256 of the MIDI file. |
bpm | The tempo. |
outputs | One row per output: output, its unit, spread, tolerance, floor, held_out_within, adjacent_ratio and lag1_autocorrelation. |
held_out_within counts the renders that lie within the limit of the
other renders, each held out in turn and compared as a clip. It checks
the coverage on the renders themselves: with 19 renders left, the rank
covers 19 in 20.
Two diagnostics describe the draws. Nothing acts on them.
adjacent_ratiois the median distance of the 19 pairs of renders next to each other in the sequence over the median distance of all 190 pairs. It lies near 1 when a draw does not depend on the one before. With 19 pairs against 190 it is noisy. It is absent when every pair lies at 0.lag1_autocorrelationis the lag-1 autocorrelation of the output’s value over the sequence. For independent draws it lies near -1 / n, within about 2 / √n: -0.05 ± 0.45 for 20 renders. A drift over the sequence takes it towards 1. It is absent for the envelope distance, which only two clips have, and for a value that every render repeats.
Outputs
Each clip is measured from its onset, the first sample within 40 dB of its peak, to its end, the last sample above the recipe’s tail floor of -96 dBFS. Both channels count. The first five rows measure the clip:
| Output | Unit | What |
|---|---|---|
rms_db | dB | The RMS level, in dBFS. Two clips compare over their common span: from the onset for the shorter clip’s length, so a cut tail does not change the level. |
peak_db | dB | The largest sample magnitude, in dBFS. |
peak_to_rms_db | dB | The peak over the RMS level, the crest factor. Two clips compare with the RMS level over their common span. A gain shifts the peak and the RMS level alike, so it moves this output only where it moves the clip’s end. |
duration_s | s | From onset to end. |
envelope_distance_db | dB | The distance of two envelopes of 10 ms frames, aligned at their onsets: the mean absolute level difference over the frames where either lies within 40 dB of the loudest frame of both. A frame below that range, or past the end of the shorter clip, counts at the bottom of the range, so a longer tail adds to the distance. |
The other rows come from the layer report of each render,
which analyze writes with the render’s MIDI file and the exact tempo.
Every group gives rows: stem, notes, pitch, voices and
envelope. The groups schema, source, synth, transcription and
conventions hold no estimate and give none. A row’s name is the path
of the number, such as stem.rms_dbfs, stem.band_share[1].share or
envelope.values.attack_s, without its value or median key.
A row’s unit comes from the field name’s suffix: _hz (Hz), _s (s),
_ms (ms), _db or _dbfs (dB), _deg (deg), _st (st) or _ct
(ct). A name without a suffix has its unit in a fixed list:
| Unit | Fields |
|---|---|
ratio | centroid_change, centroid_over_f0, curve, extent_factor, flatness, overlap_fraction, residual, rho, share, tail_weight |
count | bend_events, dropped, max_polyphony, mean_polyphony, n_notes_used, pump_beats, tail_vote_count |
st | key, a MIDI key, whose distances are semitones |
velocity | velocity, a MIDI velocity |
BPM | pump_bpm |
A field with no unit fails the run, so a new field gets a unit before it
gets a spread. A test runs the walk on a report that holds every number
the layer schema defines, so a field or group a later release adds fails
that test first. Confidences, IQRs, band edges, the run’s inputs (bpm,
bpm_given, bpm_refined and sample_rate_hz), the file constant
ticks_per_beat and the mapper’s suggested_edits and
unrepresentable have no row. Nor does a number that some render left
without an estimate, such as a list record that one render lacks.
Comparing with the spread
A comparison of one clip with the renders reports, for each clip output, the clip’s value and the renders’ median, the difference, the limit, their ratio and whether the difference lies within the limit. The ratio shows how close a match is, beyond a yes or no: a ratio near 1 lies at the edge of the renders. The clip and the renders must share a sample rate, and the spread must hold a row for every clip output.
A recording made elsewhere can differ in level alone. A gain fit takes that out before the verdict:
- Fit the gain: the median over the renders of their RMS level minus the recording’s, each pair over its common span.
- Apply the gain to the recording and measure it again. Its end moves with the gain, since the tail floor stays fixed.
- Compare it on the outputs of the sound’s shape:
peak_to_rms_db,duration_sandenvelope_distance_db.
The duration and the envelope distance still depend on the level, which
is why they compare only after the gain; the peak-to-RMS ratio moves
with it only where the clip’s end moves. The recording matches when all
three lie within their limits. compare_fitted gives this verdict, and
compare the comparison of all five outputs as recorded. A gain beyond
±3 dB is a finding to explain, not a failure: the compare stage of #12
matches the loudness of two sounds before its distances.
Testing
Tests follow the usual Rust layout. Agents write most of the code, so the compiler, the lints and the tests must catch their typical mistakes.
Layers
L0 is the compiler. L1 to L11 are tests. Each epic picks the layers it needs from the matrix below and names them in its issue.
| Layer | What | Where |
|---|---|---|
| L0 | rustc, Clippy pedantic, rustdoc lints, units in types | [workspace.lints] in Cargo.toml, clippy.toml |
| L1 | Unit tests | #[cfg(test)] mod tests next to the code |
| L2 | Integration tests | the one test binary in tests/it/, one module per feature |
| L3 | Command-line cases | declarative files in tests/cmd/, and the console blocks of the README, the guide and the CLI reference, run by trycmd |
| L4 | Doc tests and runnable examples | rustdoc examples, examples/ |
| L5 | Snapshots of the shape of our own output | --help, report and explanation templates, with insta or trycmd |
| L6 | Property tests | round trips, unit conversions, invariants |
| L7 | Golden numbers with explicit tolerances | JSON vectors in tests/fixtures/golden/ |
| L8 | Fuzzing | fuzz/, for code that reads untrusted bytes |
| L9 | Benchmarks | benches/; CI only compiles them |
| L10 | Test-quality reports | mutation testing and coverage, at epic close |
| L11 | Dataset evaluation and the per-synth bench | xtask, local only |
● needed, ○ optional, – not applicable.
| Epic kind | L1 | L2 | L3 | L4 | L5 | L6 | L7 | L8 | L9 | L10 | L11 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Foundation | ○ | – | ● | – | ○ | – | – | – | – | – | – |
| Types and units | ● | – | – | ● | – | ● | – | – | – | ● | – |
preset | ● | ● | ● | ● | ● | ● | ○ | ● | ○ | ● | ○ |
analyze | ● | ● | ● | ● | ● | ● | ● | – | ○ | ○ | ● |
notes | ● | ● | ● | ● | ○ | ○ | ● | – | ● | ○ | ● |
compare | ● | ● | ● | ● | ○ | ● | ● | – | ● | ● | ● |
render | ○ | ○ | ● | ● | – | – | ○ | – | ○ | – | ● |
refine | ● | ● | ● | ● | ○ | ○ | ○ | – | ● | ○ | ● |
| Test bench | ● | ○ | – | – | ○ | ○ | – | – | – | – | ● |
| Skill and docs | – | – | ● | ● | ○ | – | – | – | – | – | ○ |
Benchmarks
cargo bench --bench notes runs the benchmark harness groups of the notes step on
one generated 60 s stem: the model load, the model run at one thread and
at all threads, the f0 track, the MIDI write and the whole step at both
thread counts. CI compiles the benches and never runs them.
A bench measures wall-clock time, so its medians need a quiet machine.
cargo run --release --example notes_memory -- <stage> runs one stage on
the same stem, so a tool that reports a process’s CPU time and peak memory
measures each stage. The pipeline page
gives both for every stage.
Render timing needs the synth, so it is an xtask task, not a bench:
see Render timing.
Running the tests
cargo nextest run --workspace
cargo test --workspace --doc
nextest runs each test in its own process, so an in-process mutex does not
serialize tests; use a nextest test group instead. nextest does not run
doc tests, hence the second command. CI uses the ci profile from
.config/nextest.toml: no fail-fast and a slow-test timeout.
Test builds optimise our own crates at opt-level = 1 ([profile.test] in
Cargo.toml), and the dependencies at opt-level = 3. The numeric tests
run FFTs and trackers over whole stems, so unoptimised code made the suite
take about 15 CPU-minutes. With the optimised build it takes about one,
and the doc tests reuse the same build. Debug assertions and overflow
checks stay on; tests/it/profile.rs fails if a change drops either from
the test profile of the root package. It does not check a
[profile.test.package.<member>] override. The weekly
coverage job builds with the test profile too. Optimised code can shift
the line attribution of a coverage report; that effect is not measured. Plain
cargo build and cargo run keep the unoptimised dev profile.
Continuous integration
Three workflows run on a pull request.
| Workflow | Jobs | Starts on |
|---|---|---|
ci.yml | fmt, clippy, test, doc, licenses, docs, test-linux, open-engine, pitch-fixtures, vital-registry | a pull request that is opened, pushed to or reopened, and a push to main |
commits.yml | commits | the same events, and a change of the title or description |
codeql.yml | Analyze (rust) | a pull request to main, a push to main, weekly, and by hand |
Two jobs of ci.yml check generated files that a normal test cannot
rebuild. Neither is a required check.
pitch-fixturesrunspitch_fixtures_are_currentin a release build on macOS. It fails when the transcriber or the cleanup now gives other notes or frames for a signal oftests/fixtures/pitch/.vital-registrychecks out the Vital source at the commit thatsrc/preset/vital/registry.jsonnames, pinned by its commit hash, and runscargo xtask vital-registry --check. It fails when a change to the generator leaves the committed registry stale. A registry made from a newer Vital commit needs the new hash in the job.
commits has its own workflow because it reads only the title and the
description. Editing them runs that one Linux job and none of the heavy
ones. The job has no if: and no path filter: a skipped job reports
success under its own name and would hide an earlier failure on the same
commit. The main ruleset requires fmt, clippy, test, doc,
commits, licenses, docs and Analyze (rust) by job name, so a job
keeps its name when it moves between workflows.
Each workflow cancels the run of an older push to the same pull request. A
push to main never cancels another run, because every merged commit
needs its checks and may need a release. The release and pages jobs
keep their own groups. They never cancel a running job, but a newer run
replaces an older job that still waits in the same group. A release run
covers every commit since the last tag, and pages deploys the newest
documentation, so a replaced job loses nothing.
A change of the base branch (edited with a new base) starts only
commits.yml. codeql.yml never ran on it. After retargeting a pull
request, reopen it or push a commit: both ci.yml and codeql.yml then run
against the new base. No trigger can tell a base change from a title edit
before a job starts, and a job-level if: that reacted to the retarget
would report skipped jobs, which hide earlier failures.
The build cache of the dependencies comes from Swatinem/rust-cache,
through .github/actions/setup-rust. A key holds the job, the lockfiles
and manifests, the compiler version and a hash of the CARGO*, RUST* and
CC* environment variables, so a new stable release starts a fresh cache
and the env: of ci.yml and commits.yml must stay equal. Only a run on main saves a cache. A pull request restores
the newest cache of main and compiles its own crates on top; it saves
nothing, so branches cannot fill the repository quota. The docs
and commits jobs share one Linux cache (linux-xtask), because they
build the same xtask with the same features. The release job uses no
cache, because it holds the deploy key. The test job builds
the dependencies twice, once for the tests and once for xtask, because
cargo xtask resolves fewer features than the test build. The cache holds
both builds.
CI versus local
-
A test that needs a synth (Vital or Serum 2),
vitaor a dataset carries#[ignore = "needs <thing>"], such as#[ignore = "needs Vital and SOUND_TO_PATCH_HOST"]. When its input is missing, it panics with a message that names the input and how to get it. Run such tests withcargo nextest run --run-ignored only. -
These ignored tests run in no CI job. Each needs something a hosted runner lacks, so run them locally before a change that touches their code:
Tests Why not in CI Command the 21 render_vitalteststhey need the retail Vital plug-in, which needs a licence, and an x86_64 host under Rosetta see Rendering vita::a_preset_saved_by_vital_keeps_the_editsit needs a copy of a written preset saved by the Vital app VITAL_SAVED_PRESET=<file> cargo nextest run --test it --run-ignored only -E 'test(a_preset_saved_by_vital_keeps_the_edits)'preset::a_retail_default_preset_matches_the_builtin_templateit needs the default preset of the Vital app VITAL_DEFAULT_PRESET=<file> cargo nextest run --test it --run-ignored only -E 'test(a_retail_default_preset_matches_the_builtin_template)'reference::the_catalogue_covers_every_key_of_a_saved_presetit needs a preset saved by the Vital app VITAL_PROBE_PRESET=<file> cargo nextest run --test it --run-ignored only -E 'test(the_catalogue_covers_every_key_of_a_saved_preset)'vital::tests::the_committed_registry_matches_the_checkoutinxtaskit needs a Vital checkout; the vital-registryjob runs the same comparison through the commandVITAL_SOURCE=<checkout> cargo nextest run -p xtask --run-ignored only -E 'test(the_committed_registry_matches_the_checkout)'The 11 open-engine tests run in the
open-enginejob, andpitch_fixtures_are_currentruns in thepitch-fixturesjob. -
Never skip a test at run time with an early
return. It reports a pass that never happened. -
Never gate a test behind a Cargo feature. CI builds with
--all-features, so the gate would not hold, and a local run without the feature would silently drop the test. -
Datasets download through
cargo xtask fetch, which keeps a file only when its SHA-256 matches the pin. Evaluations on datasets run throughxtask, outside CI.
Naming and assertions
- Name a test after the behaviour it checks, with no
test_prefix:hz_from_midi_69_is_440. - Do not use
#[should_panic]. Return or match anErrand assert on it. clippy::float_cmpdenies==on floats. Compare with a tolerance.- Always pass both
epsilonandmax_relativetoapprox: its default tolerance is machine epsilon (f32::EPSILON, about1.2e-7, andf64::EPSILON, about2.2e-16), too tight for signal processing. - Clippy allows
unwrap,expectandpanicinside#[test]functions and#[cfg(test)]modules only. A helper module oftests/it/is declared with#[cfg(test)] mod support;and its items arepub(crate), becauseunreachable_pubdeniespub. - Examples and benches are not test code for Clippy. They return
Resultand use?.
Test data
- Generated signals are the default input: sines, band-limited saws, detuned pairs, noise from a seeded generator written in the test code.
- A committed fixture is small and has a row in
tests/fixtures/PROVENANCE.md: origin, version, license, copyright line, SHA-256, date and purpose. Its license text goes in the same file. - Nothing licensed or private enters git: no factory presets, no renders of them, no user stems, no personal Init preset. No Git LFS.
- A Vital fixture is a file our code generated, or a file a maintainer
saved from the app after loading a file we generated. The generated files
come from
cargo xtask vital-fixtures; after a change to the writers, run it and update the SHA-256 rows.cargo xtask vital-fixtures --checkcompares the committed files with the generator and checks every row, the size limit of 256 KiB, the licence section and the wavetable authors.cargo xtask vital-fixtures --coveragelists the registry keys no fixture holds. - Expected values never come from an AI. They come from a formula in the facts, a file saved by the synth, a render or an independent reference.
- A snapshot accepts every diff as the new truth, so CI runs
instawithINSTA_UPDATE=no. Snapshot the decoded values of a preset, never its bytes, and never a number that must match an outside reference. Review a changed snapshot like code; record one locally withINSTA_UPDATE=alwaysorTRYCMD=overwrite.
A Markdown page whose commands write files keeps its inputs in a
<page>.in/ folder and holds a <page>.out/ folder. With the .out folder,
trycmd runs the commands in a copy of .in, so the files they write never
land in the repository. It compares only the files that .out holds, so a
copy of one input is enough.
The open Vital engine
The open-engine tests run in the open Vital engine through the GPL-3.0
vita package, which runs only as a separate Python process. No crate of
ours depends on it. They need VITA_PYTHON, a Python with the packages of
tools/vita/requirements.txt, pinned with their hashes, made outside the
repository:
uv venv --python 3.12.14 /tmp/vita-env
uv pip install --python /tmp/vita-env/bin/python --require-hashes -r tools/vita/requirements.txt
VITA_PYTHON=/tmp/vita-env/bin/python cargo nextest run --test it --run-ignored only -E 'test(/open_engine/)'
Each run of the script stops after 120 s, so a Python that hangs fails the test with the script’s last line of output.
The CI job open-engine runs the same commands on each pull request:
written_presets_load_in_the_open_enginewrites the load-check variants and loads each throughtools/vita/load_check.py.the_open_engine_passes_the_single_note_contractruns the renderer contract suite onrender::vita::VitaRenderer, with its single-note cases: the bass and lead notes, and a note at 1 s.- Two more refuse a MIDI file with more than one note and pass the load check at 140 BPM.
the_open_engine_renders_a_job_identically_twicepins that the open engine’s spread is 0.the_open_engine_renders_every_length_it_is_asked_forrenders 12 onsets and lengths that are not whole milliseconds.the_open_engine_script_renders_and_releases_at_the_frames_it_is_asked_forsends render and note lengths straight to the script.the_open_engine_refusal_of_a_preset_is_a_state_errorsends a preset that the engine does not load through the renderer, which reports a state error.the_open_engine_prints_stay_off_the_replymakes the engine write to file descriptor 1 as it starts, and the reply still parses.
The refusals of a job the engine cannot play, another synth’s state, more
than one note, controller changes and a custom tuning, come before the
script runs. Unit tests of render::vita check them in every CI run.
Stub scripts, run with python3, check that exit code 3 maps to a state
error and any other failure to a host error, that a refused load leaves
no loaded state and no tempo, and that a clip of another length than
asked is a crash. Without python3 the stub tests skip and say so.
VitaRenderer starts tools/vita/render.py once per request, with JSON
in and JSON out. The engine renders one note per call and takes no MIDI
file, so a job with more than one note or with controller changes is
refused. Each call starts with no sound left from the one before. The
renderer plays the recipe’s warm-up note after the load and drops it, so
smoothed parameters settle. The engine has no call that renders silence,
so the recipe’s settle steps add to that warm-up render’s length. The
clip renders to the tail cap and ends where the recipe’s tail watch ends
it. The engine reads no tuning from a preset, so a custom tuning is
refused. The script writes the job’s tempo into beats_per_minute, as a
plug-in in a host takes its tempo from the host. Two renders of one
preset, each in its own process, come out identical, so the open engine’s
spread is 0.
Probed on vita 0.1.0:
- Velocity 0 still sounds: a note at velocity 0 renders at an RMS of 0.10. The renderer never sends one, since a note’s velocity is 1 to 127.
set_bpmbefore the load leaves the savedbeats_per_minuteas the preset holds it: 2.0 afterset_bpm(140). After the load it saves the new tempo, 2.333 beats per second. The script calls it before the load and writes the tempo into the preset, so both agree.load_init_presetresetsbeats_per_minuteto 2.0, 120 BPM, whateverset_bpmor an earlier preset set.- The engine multiplies a length in seconds by the rate in float32 and
truncates, so a length of n frames sent as n / rate can come back one
frame short. At 400,000 to 520,000 frames it did for 16 of 121 lengths;
that float32 product predicts each of them. The script passes
(n + 0.5) / rateand checks the float32 product, and the renderer refuses a clip of another length than it asked for. - The engine releases a note at the end of a block of 64 frames: a note
of 64 k + 1 frames releases a block later than one of 64 k. With
n / rate, a note of 64 k + 1 frames could truncate to 64 k and release
a block early.
the_open_engine_script_renders_and_releases_at_the_frames_it_is_asked_forsends render and note lengths that truncated that way straight to the script, so it fails if either goes back to n / rate. - The engine’s prints would mix into the JSON reply, so the script sends its standard output to standard error and writes the reply to the saved descriptor. A preset the engine does not load and samples that are not finite exit with code 3, which the renderer reports as a state error.
- The recipe’s warm-up note changes the probe preset’s clip by at most 1.6e-7, about one float32 step, and a preset whose LFO retriggers on each note by as little. On a preset with an LFO in sync mode, which runs on the time the engine has rendered, it changes the clip by up to 1.65, the LFO’s whole phase. The renderer keeps the warm-up, so such a preset starts after the same pre-roll as on the plug-in.
The cross-engine check needs Vital, SOUND_TO_PATCH_HOST and
VITA_PYTHON: render_vital::retail_and_vita_renders_compare_on_one_note
renders the probe preset’s lead note 20 times on retail, in sequence on
one instance, and once in the open engine. It compares the open render
with the retail ones against the retail tolerance and prints each
output. The levels must agree within coarse bounds, 0.1 dB on the RMS
level and the peak and 1.0 dB on the envelope distance, so a broken or
wrong render fails: an open render at -12 dB, or of the init patch,
fails by 12.0 dB and 4.0 dB. The engines differ in build, so a miss of
the retail limit is a finding about the engines, printed here, not a
failure of either.
a_preset_saved_by_vital_keeps_the_edits reads a file the retail app
saved after loading a written preset, from VITAL_SAVED_PRESET.
Rendering
What runs in CI, with no plug-in:
- The renderer contract suite on the fake renderer, and two tests that show the suite fails a silent renderer and a renderer with one silent channel.
- The load check on the fake: a passing render, one case each for silence, an init fallback, a changed saved state and a plug-in fault, and the warning of an init-like render. A scripted host that crashes inside the load check gives the plug-in fault too.
- The
rendercommand’s files on the fake: the.wavname in any case, an existing output without--force, a missing folder, a folder in place of an output, a read-only WAV file that--forcereplaces, and a read-only folder or a failed second write, which leave the existing files as they were. - The render session on the fake: a crash, a hang or a panic tried once more on a new renderer with the same job, a render that fails twice giving the sentinel while the session goes on, a panic named with its job, a renderer that does not start, a wrong synth failing at once on the same renderer, a restart after 256 renders, one live renderer per worker, the flush before every render and the order of the outcomes. A stub host that exits on its first render is retried on a new host.
- The timing table on the fake: every row with a median and a 95th percentile, the build and the architecture, and the clip length of a long tail.
- The recipe, its snapshot and its hash, the note events, the tail watch, the MIDI reading with its hash, and the record against its schema.
- The wire protocol, the host’s request loop, the architecture pick from Mach-O headers, and the WAV writer read back through the decoder.
- The
rendercommand cases intests/cmd/, with the hidden--backend fake. - Clippy on the host crate for
x86_64-apple-darwin.
What needs Vital: the render_vital tests render generated presets
through the installed plug-in, in the plug-in host process. The host loads
the plug-in as a library and opens no window. They need:
- Vital in a standard VST3 folder:
/Library/Audio/Plug-Ins/VST3, or the same folder under the user’sLibrary. SOUND_TO_PATCH_HOST, the path of asound-to-patch-hostbinary that loads Vital. Vital 1.0.7 holds x86_64 code only, so on Apple silicon the host needs an x86_64 slice and runs under Rosetta.VITA_PYTHONfor one of them, the cross-engine checkretail_and_vita_renders_compare_on_one_note, which also renders in the open engine. Without it that test fails at once and names the variable.
rustup target add x86_64-apple-darwin
cargo build --release -p sound-to-patch-host --target x86_64-apple-darwin
SOUND_TO_PATCH_HOST="$PWD/target/x86_64-apple-darwin/release/sound-to-patch-host" \
cargo nextest run --run-ignored only -E 'test(render_vital)'
They run the contract suite, read the saved state back, check the forced 2x oversampling, and run the load check on a loaded preset, the init patch, a slower attack on the init patch, a newer stamp, a preset with no sound source, a dropped key and a bundle that is no plug-in. Two more send a full pitch bend and a mod wheel move and measure their effect. A session of two workers renders four presets of rising level in order, and the timing table fills every row with two runs. Two fresh instances render the same first clip bit for bit, and an instance’s second render differs from its first.
What needs Serum 2: nothing yet. Its renderer follows in its own epic.
The failure paths of the host run in CI without a plug-in: unit tests in
render::plugin start shell scripts that stand in for the host. They
cover a host that exits, hangs, prints bytes that are not UTF-8 or a line
without its end, comes from another release, closes its pipes and
lingers, never answers quit, lingers after quit, or writes a clip that
does not decode. A unit test cuts plug-in output without line ends at
64 KB and reads a longer reply whole.
Render timing
cargo xtask render-timing --out "$TMPDIR/timing.json" times the Vital
plug-in on this machine and writes the table to a local file, outside
the repository. Refine sets its render budget from it. The table stays
local: it describes the machine, so the repository holds the method and
no numbers. It takes the same
SOUND_TO_PATCH_HOST as the Vital tests, or --host. --preset names
the preset, by default the routes.vital fixture, and --runs the
measurements per row, by default 20.
Each row holds the median and the 95th percentile, linear between the two nearest measurements:
plugin_load: starting a renderer, the host process and the plug-in load included, in seconds.state_load: loading the preset state into the plug-in, timed in the host around thevst3-hoststate load, in seconds.render: one render of the recipe’s lead note, MIDI 60 held 3 s, with the state load and the pre-roll, in seconds.clip_length: the length of that clip, at least 4 s and longer while the tail sounds.renders_per_second, for 1, 2, 4 and 8 workers: each measurement is one batch of one job per worker in a session whose workers have started. Its 95th percentile is the rate at the 95th percentile of the batch time, the slow end.
The table also names the synth, its build, the host, the architecture of the host process, the recipe version and the number of runs. It measures only: nothing in it tunes a render.
Repeat-render spread
cargo run --release -p xtask -- render-spread --out spread.json renders
one preset 20 times and writes its spread, the noise floor every later
comparison cites. The
render spread page lists the
outputs, the statistic and the tolerance. --preset, --midi and
--bpm name what it renders, by default the routes.vital fixture and
tests/cmd/data/three-notes.mid at 120 BPM. The load check runs first on
a fresh renderer, so a spread never measures a preset the plug-in did
not load.
A fresh Vital instance renders bit-identically to any other fresh
instance, and each later render on one instance differs. So the 20
draws render in sequence on one fresh instance, through
render::spread::draw. A draw whose samples repeat another’s, by their
SHA-256, fails the run: a repeat would count as a draw. --runs takes 3
to 256 renders, the most one instance renders before the session
restarts it. Each output’s row counts the renders that lie within the
limit of the others, each held out in turn, and records two diagnostics
of the draws: the adjacent ratio and the lag-1 autocorrelation.
Each render is stored as a WAV file in a folder beside the output, named
after it with -renders, and measured from the stored file, as a
consumer reads it. analyze writes each render’s layer report there,
with the MIDI file and the exact tempo. The file is local: the spread
depends on the synth’s build and its quality setting. The repository
keeps no spread of record; the per-recording metrics are on the
reference recordings page only.
On the fake, render_spread checks that a spread’s 20 draws are
distinct, that a draw refuses fewer than 3 renders and more than 256,
and that the distinct check refuses the renders of two workers that
start alike. It recomputes the 95th percentile of the 190 pairs and the
largest leave-one-out median from 20 stored renders with its own
quantile and median. It checks that a render of the same preset matches
while a louder one does not. After the gain fit, a louder render and one
8 dB quieter match on the shape, the quieter one flagged beyond ±3 dB,
and a longer release does not match. Two more tests read the committed
reference report: one checks its ids, hashes, gains and verdicts, the
other recomputes every count, range and table of the reference page. On
Vital, the_draws_of_a_spread_on_vital_are_distinct draws 20 renders of
one lead note on the plug-in and finds all of them distinct.
Reference recordings
cargo run --release -p xtask -- render-reference --root DIR --manifest FILE --out report.json
compares reference recordings with automatic renders. A reference
recording is a render of a generated preset and MIDI file made in a
standard DAW host, not by our host, so the comparison is not circular.
Each manifest line names the preset_id, the preset_file, the
midi_file, the bpm and the render_file, with paths from --root,
and a state_file when the DAW exported the state it played.
The task reads every manifest line first: the preset, the MIDI file, the
recording and the exported state. It refuses two lines with one id, the
preset id and the MIDI file’s name, since both would write one renders
folder. Then, for each recording, it runs the load check, draws 20
renders of the preset with the same MIDI file and tempo, and measures
their spread. --workers recordings render at once, 4 by default, each
on its own fresh instance. The recording compares with the renders on
the clip outputs of the spread page, against the tolerance. An
exported state is read back against the preset as the load check does,
the tempo against the recording’s tempo. The name and the tuning are
left out, because the DAW names the export and saves no tuning in a
preset file. A recording that differs beyond the tolerance is reported
with its numbers. Nothing is tuned to make it match.
Recordings and renders stay local. The repository holds the preset ids, the hashes and the metrics of the comparison, on the reference recordings page.
The verdict comes after a gain fit, as the render spread page describes: one gain per recording matches its level to the renders, and the recording matches on the peak-to-RMS ratio, the duration and the envelope. The report records each gain, flags one beyond ±3 dB, and keeps the comparison as recorded on all five outputs for information.
Fuzzing
The fuzz targets live in fuzz/, a workspace of its own pinned to a dated
nightly in fuzz/rust-toolchain.toml. CI lints them on stable, and a
scheduled job runs each one for five minutes. A failed scheduled run opens
an issue titled “The weekly scheduled run failed”, or comments on the open
one, so the failure reaches the repository’s watchers without a visit to
the Actions page. Run one target locally from the repository root, seeded
from the fixtures:
mkdir -p fuzz/corpus/vital_read
cp tests/fixtures/vital/*.vital fuzz/corpus/vital_read/
RUSTFLAGS=-Cllvm-args=-asan-globals=0 cargo +nightly-2026-10-20 fuzz run vital_read -- -max_total_time=600
RUSTFLAGS turns off the AddressSanitizer checks of globals; stack and
heap checks stay on. Without it the link fails on macOS with
initializer pointer has no target: the sanitizer pads each constructor
pointer that the tract crates register, and the linker reads the padding
as pointers.
Seed edits_apply, which runs edit lists through preset apply on the
routes fixture, from tests/cmd/data/*.json. The corpus and the crash
artifacts stay out of git. A crash becomes a
unit test with the input bytes inline, next to the code it broke.
Golden numbers
-
The recipe of the test signal lives in code, in the JSON vector that the generator writes and the Rust test reads.
-
The expected value comes from an independent reference: librosa, SciPy or a closed form, never our own code. The generator,
xtask/golden/generate.py, computes it once. Its output is committed as JSON undertests/fixtures/golden/with the recipe, the generator, the tool versions, the values and the tolerance. -
xtask/golden/.python-versionpins the interpreter andxtask/golden/requirements.txtpins every package with its hashes.cargo xtask goldeninstalls them with uv intotarget/golden-venv, runs the generator’s tests inxtask/golden/test_generate.py, and rewrites every vector. A second run with the same pins writes byte-identical files. -
Each vector stores the SHA-256 of its script and of
requirements.txt, with LF line ends, undersources.cargo xtask goldenprints each value that changed, and fails after the rewrite when an issue figure’s outcome flipped, areproducedflag or any boolean inrows: check each flip against its issue before you commit it. -
requirements.inlists the direct packages. To change a pin, edit it and regenerate the lock from the repository root:uv pip compile xtask/golden/requirements.in --generate-hashes --python-version 3.14.7 --output-file xtask/golden/requirements.txtAdd
--upgradeto move every package to its latest release. The command also stands in the header ofrequirements.txt. -
CI never runs Python. The
testjob runs the xtask testthe_committed_vectors_match_the_pins, which fails when a vector lacks a field, lacks the Python version intools, names a version other than the pin, or was made from another version of its script or ofrequirements.txt. It also fails when a vector ofVECTORSinxtask/src/golden.rsis missing, when a file is in no recipe, and when areproducedflag beside itsrowsdisagrees with them. A test tiesVECTORSto the recipes ofgenerate.py. Two checks run only insidecargo xtask golden, in no CI job: the generator’s tests inxtask/golden/test_generate.py, among them the rule of a glide’s rise, and the flips of thereproducedflags that have norowsbeside them, 42 of the 49. The same job runsthe_workspace_lock_has_no_ort, which fails whenCargo.lockholds the ONNX Runtime crate of the transcription model, andthe_fuzz_lock_names_the_workspace_version, which fails whenfuzz/Cargo.locknames another version of the crate thanCargo.lock.cargo xtask golden-check,check-no-ortandcheck-fuzz-lockrun the same checks locally. -
cargo run --release -p xtask -- evalscores the notes step on generated parts with known notes and prints its tables. It stays local, renders its audio on each run and commits nothing. Run it as a release build: the f0 tracker is part of our crate, so a debug run costs about 15 times the CPU time: 160 s against 10 s for the whole table.--part TEXTruns the matching parts and prints their notes, raw and clean. With--layer envelope,--partrenders only the matching parts. It fails when a golden of the cleanup breaks. The dry 110 Hz saw must show its octave copy at MIDI 57 before the cleanup, and all 6 of its true notes after it: the model misses the fourth onset, and the attack split restores it. Its six notes through reverb must give the six true notes. Those stems cost seconds of CPU time each in a debug build, so no test in CI runs them. -
Noise comes from SplitMix64, which the generator and
tests/it/support/rng.rsimplement bit for bit, so a noise recipe gives the same signal in both languages. -
Tests compare through
assert_closeintests/it/support/golden.rswith the stored tolerance,|actual - expected| <= abs + rel |expected|, and name the source of the number in a comment. -
tests/fixtures/golden/prototype/holds the tables that task issues quote. Each is the issue’s method run on a recipe given in the file; where the issue stated no recipe, the file’s recipe defines it and its numbers replace the issue’s. -
To change a reference: edit the generator or the pins, run
cargo xtask golden, and commit the vectors with the change.
Closing an epic
The epic issue holds a definition-of-done checklist. Two reports are read at close. Both report and do not gate:
- Mutation testing on the new modules:
cargo mutants --test-tool nextest --in-place. Do not setRUSTFLAGS=-D warningsfor it: a deleted line tripsdeny(unused)and turns a mutant unviable. - Coverage:
cargo llvm-cov nextest --workspace, which needs thellvm-tools-previewcomponent. The scheduled workflow publishes the same report every week. A coverage percentage is never a gate, because it rewards tests that run code without asserting.
Tests write only to temporary directories, because mutation testing runs them against broken code.
Reference recordings
Reference recordings check the automatic render against a render made elsewhere. Each one renders a generated Vital preset and MIDI file in a standard DAW host, on the retail Vital 1.0.7, exported as 32-bit float stereo at 44.1 kHz with its tail. Our host draws 20 renders of the same preset, MIDI file and tempo, in sequence on one fresh plug-in instance. One gain per recording matches its level to those renders. The comparison then asks whether the recording’s shape lies as close to the renders as they lie to each other: its peak-to-RMS ratio, its duration to the tail floor and its envelope. The testing page gives the method, and the render spread page the outputs, the gain fit, the tolerance and the floor.
The recordings and the renders stay local. This page and the report next
to it hold the preset ids, the hashes of the preset, the MIDI file and
the recording, and the metrics.
report.json holds, per recording,
the fitted gain, the comparison after it and the comparison as recorded.
38 recordings cover isolated controls, musical presets of each role, look-alike pairs and complex presets, at 120 and 140 BPM. For the 34 whose DAW exported the state it played, the exported state matches the preset on every control, at the recording’s tempo.
Result
The acceptance of #256 asks every recording to match after its gain, so
it is not met. A level difference within ±3 dB is no fault: the compare
stage of #12 matches the loudness of two sounds before its distances.
render::spread::compare_fitted gives the verdict after the gain, and
render::spread::compare the comparison as recorded, for information
only.
- After the gain fit, 12 of 38 recordings match the renders on all three outputs of the sound’s shape: the peak-to-RMS ratio on 37, the duration on 14 and the envelope on 31.
- As recorded, for information, 0 of 38 lie within the limit on all five outputs: the RMS level on 0, the peak on 1, the peak-to-RMS ratio on 37, the duration on 12 and the envelope on 0.
- The fitted gains lie from +1.91 to +2.43 dB, with a median of +2.17 dB. None lies beyond ±3 dB.
Every gain is positive: each recording is quieter than the renders, by the amounts above. The cause is not known, on either side: our host’s gain staging and export are as much in question as the DAW’s.
What remains unexplained
Nothing was tuned to make a recording match.
- Duration. A clip ends at its last sample above -96 dBFS. The gain fit
raises each recording before its end is measured, so the level offset
does not explain an early end. The data point to the export stopping
early, so an early end measures where the export stopped, not how the
tail decays.
- 21 recordings end earlier than the renders, beyond the limit, and 3 end later.
- 14 of the earlier ones end 0.1 s or more early, from 0.22 to 1.68 s. The other 7 end 0.5 to 53.8 ms early.
- The later ones end late by 44.2 ms (
cplx_22__pad_chords_120bpm), 40.9 ms (mus_sub_bass_00__bass_octave_120bpm) and 21.7 ms (cplx_17__arp_120bpm).
- 19 recordings miss on the duration alone.
- 7 recordings lie beyond the limit on the envelope, at ratios from 1.02 to 2.96. 5 of them also lie beyond it on the duration.
- The peak-to-RMS ratio lies beyond the limit on 1:
iso_porta_force_0.1__chord_long_C4maj_v102, at a ratio of 1.98, and 2.96 on the envelope.
The recordings and renders stay local, so no test recomputes these numbers; they were measured on the local files after the gain fit:
- In each of the 21 recordings that end earlier than the renders, the file ends within 371 frames, 8.4 ms, of the recording’s last sample above -96 dBFS.
- In the 14 that end 0.1 s or more early, the last 50 ms of the recording lie within 1.9 dB of the renders’ median over the same span from the onset. The delay preset stops at -76.1 dBFS against -75.4 for the renders, so its sound still sits 20 dB above the tail floor when the file ends.
So each recording stops while it still sounds as the renders do: the export ended before the tail did. This is an inference. The calibration recording, signals of known level and decay sent through the DAW chain of the references and through our host, will settle it.
The draws
The renders of each recording also check the tolerance and the draws.
- Each render, held out in turn and compared with the other 19 as a clip, lies within their limit at these rates over the 760 cases: 95.1 % on the RMS level, 95.1 % on the peak, 94.6 % on the peak-to-RMS ratio, 95.0 % on the duration and 95.5 % on the envelope. The rank of 19 renders covers 19 in 20, 95.0 %.
- The lag-1 autocorrelation of each set of 20 has a median of -0.17 on the RMS level (-0.41 to 0.69), -0.05 on the peak (-0.49 to 0.48), -0.05 on the peak-to-RMS ratio (-0.48 to 0.48) and -0.06 on the duration (-0.50 to 0.40).
- The adjacent ratio has a median of 1.09 on the RMS level (0.44 to 1.66), 1.01 on the peak (0.52 to 1.36), 1.03 on the peak-to-RMS ratio (0.50 to 1.53), 0.97 on the duration (0.28 to 1.45) and 1.01 on the envelope (0.89 to 1.30).
The lag-1 autocorrelation of independent draws lies near -0.05, within about ±0.45 for 20 renders, so these medians show no drift over a sequence.
Each recording
The gain is the fitted gain in dB. The duration difference is the recording’s duration after the gain minus the median of the renders, in ms. Each ratio is the difference over its limit: above 1 lies beyond the limit. The first three ratios are the verdict after the gain, the last five the comparison as recorded.
| Id | BPM | Gain (dB) | Duration difference (ms) | Peak-to-RMS | Duration | Envelope | RMS, as recorded | Peak, as recorded | Peak-to-RMS, as recorded | Duration, as recorded | Envelope, as recorded |
|---|---|---|---|---|---|---|---|---|---|---|---|
iso_init__chord_long_C4maj_v102 | 120 | +2.20 | +0.3 | 0.80 | 0.44 | 0.77 | 41.72 | 3.91 | 0.80 | 0.44 | 1.82 |
iso_uni_v7_det40__chord_long_C4maj_v102 | 120 | +2.16 | -0.1 | 0.21 | 0.36 | 0.95 | 52.55 | 2.06 | 0.21 | 0.36 | 1.67 |
iso_chorus_mix100__chord_long_C4maj_v102 | 120 | +2.20 | -18.1 | 0.41 | 1.33 | 0.89 | 35.56 | 2.06 | 0.41 | 1.76 | 2.81 |
iso_amp_rel1__chord_short_C4maj_v102 | 120 | +2.34 | +2.6 | 0.39 | 0.93 | 1.03 | 7.39 | 1.64 | 0.39 | 1.88 | 1.96 |
iso_reverb_d1__chord_short_C4maj_v102 | 120 | +2.32 | -22.0 | 0.36 | 0.50 | 0.70 | 4.64 | 1.65 | 0.36 | 2.63 | 1.11 |
iso_lfo_amp_1_8__chord_long_C4maj_v102 | 120 | +2.17 | +0.2 | 0.24 | 0.25 | 0.84 | 28.80 | 15.47 | 0.24 | 0.30 | 1.72 |
iso_comp_fast__chord_long_C4maj_v102 | 120 | +2.17 | -2.2 | 0.66 | 0.73 | 0.93 | 38.68 | 2.97 | 0.66 | 0.77 | 3.47 |
iso_dist_soft_12db__chord_long_C4maj_v102 | 120 | +2.18 | -0.5 | 0.38 | 1.16 | 0.85 | 183.43 | 8.38 | 0.38 | 1.37 | 5.97 |
iso_delay_1_8_fb50__chord_short_C4maj_v102 | 120 | +2.43 | -1683.4 | 0.43 | 187.46 | 0.88 | 7.49 | 1.94 | 0.43 | 187.48 | 1.91 |
mus_pad_00__chord_long_C4maj_v102 | 120 | +2.00 | -1062.4 | 0.34 | 4.22 | 1.02 | 3.24 | 1.41 | 0.34 | 4.22 | 1.11 |
mus_supersaw_00__chord_long_C4maj_v102 | 120 | +2.10 | -707.3 | 0.48 | 5.53 | 0.96 | 19.80 | 2.58 | 0.48 | 5.56 | 2.32 |
mus_keys_00__phrase_pad_chords_120bpm_v102 | 120 | +2.18 | -40.4 | 0.24 | 0.34 | 0.94 | 4.78 | 1.29 | 0.24 | 0.34 | 1.68 |
mus_stab_00__chord_short_C4maj_v102 | 120 | +2.14 | -238.8 | 0.46 | 4.05 | 0.75 | 5.85 | 2.75 | 0.46 | 4.18 | 1.35 |
mus_pluck_00__strum_120bpm | 120 | +2.15 | -606.0 | 0.34 | 53.34 | 0.98 | 10.28 | 1.34 | 0.34 | 53.93 | 2.47 |
iso_porta_force_0.1__chord_long_C4maj_v102 | 120 | +2.16 | +0.6 | 1.98 | 0.54 | 2.96 | 216.10 | 104.93 | 1.98 | 0.50 | 14.68 |
mus_pad_00__pad_chords_120bpm | 120 | +2.18 | -1202.2 | 0.46 | 5.10 | 1.03 | 4.49 | 1.55 | 0.46 | 5.12 | 1.24 |
mus_arp_00__arp_120bpm | 120 | +2.20 | -782.5 | 0.44 | 5.96 | 1.04 | 11.27 | 1.10 | 0.44 | 5.98 | 3.22 |
mus_lead_00__lead_legato_120bpm | 120 | +2.16 | -388.4 | 0.38 | 19.18 | 0.75 | 78.48 | 3.33 | 0.38 | 19.47 | 1.03 |
mus_reese_bass_00__bass_octave_120bpm | 120 | +2.05 | -65.6 | 0.47 | 0.50 | 0.87 | 3.93 | 94.14 | 0.47 | 0.53 | 1.48 |
iso_porta_0.1__lead_glide_120bpm | 120 | +2.13 | +0.2 | 0.52 | 0.38 | 0.99 | 54.97 | 1.14 | 0.52 | 0.40 | 9.91 |
mus_fx_00__chord_long_C4maj_v102 | 120 | +2.18 | -1253.9 | 0.38 | 5.49 | 1.13 | 9.79 | 1.70 | 0.38 | 5.52 | 1.96 |
conf_cutoff_vs_wavetable_v1_a__chord_long_C4maj_v102 | 120 | +2.15 | +0.2 | 0.47 | 0.32 | 0.94 | 72.93 | 16.82 | 0.47 | 0.30 | 1.54 |
conf_cutoff_vs_wavetable_v1_b__chord_long_C4maj_v102 | 120 | +2.17 | -0.2 | 0.59 | 0.21 | 0.95 | 217.37 | 109.93 | 0.59 | 0.26 | 1.35 |
conf_phaser_flanger_chorus_v1_a__chord_long_C4maj_v102 | 120 | +2.17 | -2.8 | 0.34 | 1.39 | 0.99 | 49.78 | 2.18 | 0.34 | 2.42 | 2.75 |
conf_phaser_flanger_chorus_v1_b__chord_long_C4maj_v102 | 120 | +2.13 | -3.2 | 0.96 | 1.96 | 0.87 | 62.04 | 4.39 | 0.96 | 2.13 | 2.32 |
conf_phaser_flanger_chorus_v1_c__chord_long_C4maj_v102 | 120 | +2.26 | -17.7 | 0.33 | 1.43 | 0.98 | 44.42 | 1.45 | 0.33 | 1.81 | 2.60 |
conf_spread_vs_stereo_fx_v1_a__chord_long_C4maj_v102 | 120 | +2.17 | -0.8 | 0.31 | 1.28 | 0.82 | 21.18 | 1.91 | 0.31 | 1.39 | 1.57 |
conf_spread_vs_stereo_fx_v1_b__chord_long_C4maj_v102 | 120 | +2.18 | -53.8 | 0.19 | 3.12 | 0.80 | 27.90 | 2.41 | 0.19 | 3.53 | 2.24 |
mus_pad_05__chord_long_C2maj_v102 | 120 | +1.99 | -860.0 | 0.50 | 2.83 | 0.96 | 5.93 | 2.63 | 0.50 | 2.86 | 1.15 |
mus_keys_04__chord_short_C5maj_v102 | 120 | +2.39 | -217.6 | 0.26 | 2.19 | 0.73 | 8.91 | 1.64 | 0.26 | 2.22 | 1.08 |
mus_bell_00__pad_chords_120bpm | 120 | +2.12 | -470.0 | 0.72 | 1.94 | 0.91 | 5.57 | 2.36 | 0.72 | 1.97 | 1.47 |
cplx_22__pad_chords_120bpm | 120 | +2.17 | +44.2 | 0.54 | 24.67 | 0.95 | 38.62 | 1.56 | 0.54 | 23.90 | 1.10 |
mus_808_00__bass_glide_120bpm | 120 | +2.13 | -2.4 | 0.25 | 0.73 | 0.89 | 23.62 | 4.09 | 0.25 | 0.87 | 4.02 |
mus_reese_bass_05__bass_glide_120bpm | 120 | +2.06 | +0.4 | 0.32 | 0.25 | 0.94 | 3.46 | 3.33 | 0.32 | 0.37 | 1.22 |
mus_sub_bass_00__bass_octave_120bpm | 120 | +2.20 | +40.9 | 0.82 | 47.47 | 1.09 | 157.96 | 46.02 | 0.82 | 47.16 | 4.10 |
cplx_17__arp_120bpm | 120 | +2.21 | +21.7 | 0.82 | 59.94 | 0.97 | 64.42 | 4.85 | 0.82 | 59.81 | 4.03 |
mus_arp_02__arp_140bpm | 140 | +1.91 | -349.1 | 0.46 | 3.90 | 0.97 | 6.28 | 0.54 | 0.46 | 3.95 | 1.88 |
mus_lead_03__lead_glide_140bpm | 140 | +2.23 | -381.6 | 0.64 | 7.05 | 0.90 | 14.60 | 1.54 | 0.64 | 7.16 | 1.17 |
Release
Releases happen on their own when a pull request merges into main. No
one tags or uploads by hand.
What a merge does
- The CI workflow runs every check on the merged commit. The
commitscheck belongs to a pull request and has its own workflow, so it does not run here. - If
fmt,clippy,test,doc,licensesanddocspass, thereleasejob runscog bump --auto. It reads the commit subjects since the last tag:- a
featcommit bumps the minor version; - a
fixcommit bumps the patch version; - any other type alone bumps nothing. The job ends green and the change ships with the next release;
- a breaking change (
!after the type or scope) bumps nothing more while the project is below 1.0:feat!bumps the minor version,fix!the patch version, andchore!nothing. From 1.0 on, a commit of any type with!, or with a body line such asBREAKING CHANGE: the flag is goneorBREAKING-CHANGE: ..., bumps the major version.
- a
- A bump runs
cargo xtask set-versionas its pre-bump hook, which writes the version intoCargo.toml,Cargo.lockandfuzz/Cargo.lock. Cog then updatesCHANGELOG.mdand creates the commitchore(version): vX.Y.Zwith the tagvX.Y.Z. - The job pushes the commit and the tag in one atomic push with the
release deploy key. The main ruleset lets only that key push to
maindirectly. The private key is a secret of thereleaseenvironment, which onlymainmay deploy to, so a workflow on another branch cannot read it. - The tag starts the publish workflow. It stops unless the tag is
vX.Y.Zand its commit is onmain. It builds the macOS arm64 binary, the universal plug-in host, the man pages, the shell completions andTHIRD-PARTY-LICENSES.html. It checks that the host holds both slices and that both binaries print the tag’s version. The host’s x86_64 slice runs only where the runner has Rosetta; without it, the workflow warns and checks the arm64 slice alone. The notices cover the workspace: both binaries, which share only the core crate, and the three crates that only xtask uses. The workflow checks that they namevst3-host. The job packs each folder inplugins/as its own zip archive and publishes a release with the changelog section as its notes. The notes end with a line that names both binaries.
The version commit starts the CI workflow again. The release job skips a
commit whose subject starts with chore(version), so the loop stops there.
The test job runs the xtask test
the_fuzz_lock_names_the_workspace_version, which fails when Cargo.lock
and fuzz/Cargo.lock name different versions of the library or its core.
cargo xtask check-fuzz-lock runs the same check locally.
The release archive
sound-to-patch-vX.Y.Z-aarch64-apple-darwin.tar.gz holds:
sound-to-patch, the binary, for Apple silicon;sound-to-patch-host, the plug-in host thatrenderstarts, a universal binary for Apple silicon and Intel: Vital 1.0.7 holds Intel code only, so the host runs as Intel under Rosetta for it;man/, one page per command:sound-to-patch.1andsound-to-patch-<subcommand>.1for each subcommand, such assound-to-patch-analyze.1andsound-to-patch-preset-apply.1;completions/for bash, zsh and fish;THIRD-PARTY-LICENSES.html;LICENSE.
THIRD-PARTY-LICENSES.html is also a release asset of its own. Each agent
plugin ships as <plugin>-vX.Y.Z.zip, its folder under plugins/.
Releases are immutable
Once a release is published, its tag and assets cannot change. The title and notes stay editable. A deleted release’s tag can never be reused. The tags ruleset covers every tag: only the release deploy key creates one, and no one deletes or moves one, including a tag whose release failed.
When something fails
- A check fails on
main. Thereleasejob does not run. Fix the problem in a new pull request; its merge releases everything since the last tag. - The publish workflow fails. The tag exists and no later merge bumps
it again. Fix the cause on
mainif needed, then re-run the failed publish run, or start the publish workflow by hand with the tag name. It skips a release that already exists. Never delete and re-create a tag. - Two merges land close together. The
releasejob first checks thatmainstill points at the commit its run tested. When another merge, or the version commit of its release, landed in between, the job ends green with a notice, and the run of the newer commit releases both. The same notice ends the job when its push fails becausemainmoved during the job. A push that fails whilemainis unchanged stays red: re-run the job. When the newer run fails a check, nothing is released until a later merge passes or its failed jobs are re-run.
Agent reference
The agent reference is a folder of plain files, reference/. It tells an
agent which controls a synth has, by layer, with the preset key, the unit,
the range, the default, how sure each value is and how to tune it by ear.
Two thin entry points route an agent into it, and both read the same
files:
- the plugin
plugins/sound-to-patch-reference, the entry point for local sessions. Its skillsynth-referencelinks the folder asreference. The audio path and the text path will ship as plugins of their own that depend on it; - the project skill
.claude/skills/synth-reference, a link to the skill of the plugin, which a session opened on a clone of this repository loads.
Layout
reference/ the shared reference, one copy
README.md which file answers which question
core/<topic>.md generated: one music file per topic
core/words-<axis>.md generated: one word file per axis
core/words-lists.md generated: ladder, style and technique words
vital/<layer>.md generated: one file per layer
vital/global.md generated: controls outside the layers
vital/examples/*.json edit lists that CI applies
.claude-plugin/marketplace.json the marketplace: one entry per plugin
plugins/sound-to-patch-reference/
.claude-plugin/plugin.json name, description, licence; no version
README.md
skills/synth-reference/
SKILL.md the rules to keep and a pointer
reference -> ../../../../reference
.claude/skills/synth-reference -> ../../plugins/sound-to-patch-reference/skills/synth-reference
dictionary/
music/<topic>.json music entries, one file per topic
music/stats.json the shares the music entries rest on
music/plan.json the planned entry count per group and tier
words/<axis>.json neutral word entries, one file per axis
words/agreement.json derived listener numbers per word
words/ladder.json the intensity ladder and its anchors
words/style.json, technique.json style and technique words by rank
words/lexicon.json the describing-word lexicon and word groups
words/ranks.json the recomputed ranked list, counts only
sources.json every source the dictionary cites
probes.json one probe question per reference file
examples.json the expected number of examples per synth
schemas/music.schema.json, schemas/stats.schema.json,
schemas/words.schema.json, schemas/agreement.schema.json,
schemas/ladder.schema.json, schemas/ranked.schema.json,
schemas/lexicon.schema.json, schemas/ranks.schema.json,
schemas/sources.schema.json
Edit the reference only in reference/. An installed plugin keeps the
linked files: the harness copies a linked folder into its cache when it
installs a plugin from a repository, while files outside the plugin with
no link are left out. A zip of the plugin folder carries them too, because
zip stores the linked files.
The plugin carries no version. Users who install it from the marketplace
track the commits of main, and each release also ships the folder as
sound-to-patch-reference-vX.Y.Z.zip.
The generated catalogue
cargo xtask reference writes the files under reference/<synth>/ for
each synth that has an adapter. It reads the parameter registry, the same
data that preset apply checks edit lists against, so the reference and
the writer never disagree. Never edit a generated file by hand: change the
registry or the generator, then run the command again. cargo xtask reference --check fails when a committed file differs; CI runs it.
- The layer of each key comes from the adapter of its synth
(
preset::catalogue). Keys that belong to no layer, such as the tempo or the volume, go toglobal.md. - A control with several instances is one row, with
Nin its key and the instance numbers in a column. Vital has 248 such patterns for its 794 keys. Instances that differ in their tag get a row each. - The tag comes from the provenance of the registry entry:
from facts,measured, oruncalibratedfor an interim law. An uncalibrated row shows its tuning note. - Legacy keys, which readers accept and writers refuse, and keys that only
newer builds of the app write, are listed apart in
global.md. - Numbers show five significant digits. The registry keeps the exact values.
The written part of a layer file
Each Vital layer file opens with a written part above its rows: how the
layer works, then one entry per control or group of controls. The text
lives in dictionary/vital/<layer>.md, and cargo xtask reference
merges it, so edit it there.
- An entry is a
###heading with three fields, one line each:Keys,EffectandWords. An optionalTuningline extends the tuning note of the registry where a source gives more. Keyslists key patterns as the rows write them, such asosc_N_level. Every pattern of the layer sits in one entry, and an entry names no key of another layer. The label, unit, range, default, tag, verified build and tuning note come from the rows of those keys.Effectsays what the control does and ends with its source: a file of the open source snapshot, a law measured in the app or on the open engine, orno source.Wordslinks the technique words that name the control, or readsnone in the word lists. The word maps of the descriptive words add their Vital controls here, so they revise those lines.- The command fails and names the entry when a field is missing, an effect has no source, a link names no technique word, or a pattern belongs to another layer, to no entry or to two entries.
- The text above the first
##section, such as the build the facts were checked on, opens the file. A contents list follows, with the written sections first and the row sections apart.
The music dictionary
The music dictionary turns a producer’s note words into checkable rules:
keys, scales, chords, voicings, progressions, rhythm, articulation,
phrase and genre words. The data lives in dictionary/music/, one file
per topic, and the module src/music embeds those files with
include_str! and parses them once. So the binary and the generated
reference read the same data, and nothing can drift between them.
Each entry is one sense of a word:
idis<group>.<word>or<group>.<word>.<sense>, such asscale.phrygianorposition.offbeat.eighth.rulehas a kind from a fixed list, such aspitch_set,chord_tones,cycle,onset_setorregister, and the numbers of that kind. Free text is rejected.nonemarks a word that names no note rule, such as a section word.example_passandexample_failare small note fragments that meet and break the rule. The loader checks that they are well formed and in range; the text-to-notes task evaluates them.tiersays how much the entry is needed: every planned M1 entry must land, at least half of M2 and a quarter of M3.sourcenames ids fromdictionary/sources.json.tagsays how the claim is known, one ofsource states,measured,derived(our computation on public data),inferred(our reading),snippet(a search excerpt),single sourceandproposal(a value or policy we chose, such as a default pick or a velocity band). An entry that is mostly facts takes the tag of its facts.- A word with several senses has one entry per sense and exactly one default. An ask word carries the one question on its default sense.
- Chord entries list their spellings in
symbols; the chord parser reads only those, so a spelling outside the dictionary is an error that names it.
Roman numerals take the mode of the key when it is given. Without one, a
line that starts with I reads as major, because a major key borrows
III, VI and VII as secondary chords. Any other line is minor when it
starts with i or holds a bare III, VI or VII. A line read as major
without a minor sign is flagged as an assumption. X/Y is a secondary
chord read in the key of Y; I/Y is refused, since it could be a slash
chord.
The middle-C convention is named by the label a program gives note 60:
C3, C4 (scientific) or C5. Notes are stored as MIDI numbers, and a
note name with no stated convention is read in the role range and flagged
as an assumption. SynthCapabilities::middle_c holds the label of each
synth’s own note display: Serum 2 labels note 60 C3, and the Vital label
stays unset because its source draws no note names; a probe of the app
settles it.
To add or change a music entry:
- Edit the topic file under
dictionary/music/. - Run
cargo nextest run --test it music::; fix each problem it names. - Add any new share to
dictionary/music/stats.json, with counts when the source gives them. - Raise the group’s count in
dictionary/music/plan.jsonwhen the entry is planned. - Run
cargo xtask referenceto rewritereference/core/.
The oracle libraries never enter the repository, and they cover only part of the dictionary:
| Oracle | Where it runs | What it compares |
|---|---|---|
rust-music-theory | CI, offsets pinned in the test | 13 of the 57 scale rows; 23 chord qualities |
chordparser | CI, dev-dependency | every chord spelling; 14 of 137 do not parse |
pychord | golden tools, local | every chord spelling; 87 of 137 do not parse |
music21 | golden tools, local | every chord spelling; 65 of 137 do not parse |
The npm table the published lexicon was checked against is not run, so
the lexicon’s 56-of-57 scale and 22-of-31 chord comparisons are not
repeated here. cargo xtask golden passes the spellings on stdin to
xtask/golden/music_oracle.py, which runs in the temp folder and writes
only which spellings agree, differ or do not parse. The tests pin the
spellings that differ, so a change in either the dictionary or an oracle
fails them. Snapshot tests pin every scale’s offsets, every onset set and
pattern, every tempo and progression root and every stated share, so a
changed number shows in review.
Most shares in dictionary/music/stats.json are transcribed from the
published lexicon, whose aggregation scripts are not committed; they carry
the tag single source and cite lexicon next to their corpus. Only a
share with counts is recomputed. A source whose terms forbid bulk
extraction or model evaluation, or whose fields come from a commercial
API, gives no share. A non-commercial chord database gives aggregate
shares only, never a row or a song. A genre entry that cites the
non-commercial genre guide keeps its tempo range or drum skeleton only
when a second source agrees and cites both; otherwise it carries the tag
single source. Tempos and skeletons are written in this dictionary’s
own form: a range with a centre and slot sets on a 16-step grid.
The word dictionary
The word dictionary says what a producer’s sound word means, with no synth
control: “warm” is more low-mid weight, duller highs or too much mid-bass.
The synth references bind each sense to controls. The data lives in
dictionary/words/, one file per axis (tone, body, texture, structure,
space, movement, envelope, mood), and the words module embeds it.
Each entry is one sense. It has a plain meaning, the roles it applies to,
the senses it conflicts with, the listener agreement of its word, the
analyzer number that checks it (measure) and its sources. A word with
several senses has one default and a question built from the senses’
labels, such as “Warm: more low-mid weight (default), duller highs or too
much mid-bass?”. When the request names a role and exactly one sense lists
it, that sense wins. A sense lists a role only where a source ties the
sense to it; big, thin and wobble have such a role. Mood and character words name no control:
they carry no control, a card hint or unmapped, and links to the mood
entries of the music dictionary.
Every number sits in values with its unit, its sources and a tag that
says how many sources back it: source states for two or more, single source for one, snippet when only search excerpts back it. A meaning
holds no digit. A meaning that rests on the listener data alone is
crowd data; the listener data is derived, so it never counts toward
source states or single source. An entry no source describes is
inferred and holds no number. A fact that rests only on a store or vendor tag list, on a synth’s
own control names, or on a research note with no public source is left
out.
cargo xtask agreement --data <folder> fetches the EQ and reverb tables
of the pinned listener data (source crowd-eq) by SHA-256 into a folder
outside the repository, and refuses one inside it, a .. path and a
link that leads into it. It runs xtask/golden/word_agreement.py with
the pinned tools. The script groups the EQ curves by word and writes the
mean pairwise correlation of each word’s curves, rounded to two decimals,
with the curve count. The class is high from 0.45, medium from 0.20 and
low below, and is known only from 8 curves; fewer set the few-sample flag.
Reverb rows carry comma-separated words and count once per word; each
word’s effect is its mean minus the global mean over the global standard
deviation, and its spread ratio is its variance over the global variance,
averaged over five parameters. Band means and reverb numbers need five
rows, so no single row can be read back. A word with fewer than five
curves is written only when it has an entry, so no free-text label of a
single listener lands in the file; the xtask passes the entry words to the
script on stdin, and the script fails if its output holds another label
or a source row. After adding an entry, run the task again. CI needs no
Python: the tests recompute class and flag from
the stored numbers and compare each entry with its word’s numbers.
dictionary/words/ladder.json maps modifiers such as “slightly” and
“very” to a mild, medium or strong step. No source quantifies the
modifiers, so the ladder is inferred; each anchor that sets a step for a
kind of control is a sourced value. The style and technique lists follow
the rank order of the published word lexicon. A style word’s hint reads
pending until the routing data fills it; the first sixteen technique
words name a neutral control group.
cargo xtask word-ranks recomputes the ranked list of describing words
locally from the pinned preset banks, sound tags and captions, and writes
counts only to dictionary/words/ranks.json, keyed by neutral source
ids. The describing-word counts land within 1 to 3 of the published ones.
The style and technique counts are fits: the published list names the
groups but not their variants, and 12 of 26 style groups and 7 of 26
technique groups match in every cell. A test fails when a word of
tier W1 or W2, or a word of the task drafts, has no entry.
To add a word, add its entry to the axis file, cite only ids of
dictionary/sources.json, run cargo xtask reference and the tests.
Checks
The test job runs these checks as the xtask test the_real_plugins_pass,
and cargo xtask check-plugins runs them locally. They fail on:
- a marketplace entry whose source does not start with
./, leaves the repository or names another plugin, and a plugin folder with no entry; - a manifest whose
namediffers from its folder or whose description lacks the non-affiliation line, and a plugin README without that line; - a
SKILL.mdwith a frontmatter field outside the six fields of the skill specification, anamethat differs from its folder, a description over 1,024 characters, 500 lines or more, more than 20,000 characters, or no non-affiliation line; - a reference file over 300 lines, or over 100 lines without a
## Contentslist before its first section; - a plugin skill whose
referenceis not a relative link toreference/, and a project skill under.claude/skills/that is not a relative link to the skill of the plugin; a link checked out as a plain file is reported as “symlink not checked out”; - a file under
reference/that is neither Markdown nor JSON; - a phrase that a folder must not hold: for Vital, the name of the other
synth; for
core/, a program or service name other than Serum 2 and Vital, kept in the source as SHA-256 digests so the names stay out of the tree; for both, version comparisons such as “older version”; - a reference file without a probe in
dictionary/probes.json, and a probe answer that appears in no file or in more than one.
cargo xtask reference fails when a planned M1 or M2 group of the music
dictionary falls under its floor, and names the group.
The integration tests check the dictionary files against their schemas,
apply every example edit list to the built-in template of its synth, and
compare the number of examples with dictionary/examples.json. A local test,
ignored by default, checks that the catalogue covers every key of a preset
that the app saved; set VITAL_PROBE_PRESET to the preset’s path and run
it with --run-ignored only.
cargo xtask check-credits also reads the plugin pages and
dictionary/sources.json: every source they cite needs a README credit.
To check a plugin with the agent harness itself, run it locally:
claude plugin validate .
claude plugin validate plugins/sound-to-patch-reference
Plugin loading
Checked on 2026-10-28, in a local session:
| Check | Result |
|---|---|
| Files outside the plugin folder after an install from the repository on GitHub | absent: the plugin is copied into a cache. A link inside the plugin to a folder elsewhere in the repository is copied as that folder, with its files |
plugin validate on the marketplace root and on each plugin folder | passes; it warns that no version is set, which is intended |
marketplace add <path> and install | the plugin’s skill loads; a local folder is read in place |
Installing a plugin whose plugin.json lists another in dependencies | installs both; uninstalling it marks the other as no longer needed |
A zip of the plugin folder, nested or flat, as --plugin-dir | loads the skill |
A plugin folder under .claude/skills/ as <name>@skills-dir | not listed by plugin list or plugin details. The docs say it loads once the folder is trusted, from the primary working folder only, and in a worktree from the main checkout. Not used |
The linked project skill .claude/skills/synth-reference in a clean export of a commit | a headless session lists synth-reference, and its reference link resolves into reference/ |
| Cloud sessions | not a target and not run. The docs say a committed project skill loads there and an installed plugin does not |
Which install paths carry reference/:
| Install path | Carries reference/? |
|---|---|
marketplace add Akurganow/sound-to-patch and install (a copy from GitHub into the cache) | yes, checked: the link is copied as the folder |
A release zip, through --plugin-dir | yes, checked: zip stores the linked files, and the release checks it |
marketplace add <local folder> and install | yes, checked: the plugin is read in place, and its reference/ lists README.md and every file of vital/ |
A plugin copied from a local path, or a command source in its default copy mode | no: the docs say such a copy keeps only links inside the plugin’s own folder |
| A clone of the repository, for the project skill | yes, where the checkout makes symbolic links |
Windows: with symbolic links off in git (core.symlinks=false), both links
check out as small text files. The plugin then installs with no reference,
and the project skill does not load. The
guide gives the git settings
that avoid it. cargo xtask check-plugins reports such a file as
“symlink not checked out”.
Evaluation
Three eval cases under plugins/sound-to-patch-reference/evals/ measure
how far the reference helps an agent. Each is a single-fact lookup:
control-lookupasks for the key, unit and default of a Vital control;word-lookupasks for the bass sense of “warm”, the report key that checks it and how far listeners agree on it;rhythm-lookupasks for the slots and note length of a bass grid in the music dictionary.
The cases check that the agent reaches the reference and uses it. They do
not check every rule of SKILL.md. Most of the answers appear only in the
reference; the control key is general knowledge of the open Vital source.
Regex graders check each value next to its label or unit in the last
message. A tool_used: Skill grader shows whether the skill fired, and it
counts toward no score.
Each run starts in an empty folder, so the run without the plugin sees no
reference file. The plugin loads in place from the repository. The run
with it therefore needs read access to the skill folder and to
reference/, the target of the skill’s reference link. The grant leaves
out the plugin’s evals/ folder, which holds the expected answers. In a
permission rule, a path that starts with // is absolute, so /$PWD
gives // plus the repository path. Run the cases from the repository
root:
P=plugins/sound-to-patch-reference
A=(); for t in Read Glob Grep; do A+=("$t(/$PWD/$P/skills/**)" "$t(/$PWD/reference/**)"); done
claude plugin eval $P --tag baseline --runs 3 --model <model> \
--no-publish --threshold 0 --allow-tools "${A[@]}"
--ablation with-without is the default for a plugin path. Every case
runs three times without the plugin and three times with it, in the same
eval run. The verdict of each grader in each run is in
aggregate-result.json and report.html under evals/results/, which
git ignores. --threshold 0 lets the command pass whatever the scores, so
the comparison is read by hand. A change to the skill passes when, in one
eval run, its with-plugin score beats the no-plugin score of the same run
on at least two of the three cases. A pull request that reruns the cases
records the score of each run, the model and the date, and nothing about
the machine or the account.
Sources
- Plugin dependencies: https://code.claude.com/docs/en/plugins/dependencies
- Plugin and marketplace reference: https://code.claude.com/docs/en/plugins-reference
- Cloud environments: https://code.claude.com/docs/en/cloud-environments
- Skills: https://code.claude.com/docs/en/skills
- Sharing files within a marketplace with symlinks: https://code.claude.com/docs/en/plugins/host-marketplace#share-files-within-a-marketplace-with-symlinks
- Agent Skills specification: https://agentskills.io/specification
- Plugin evals: https://code.claude.com/docs/en/plugin-evals
Credits
This list is the credits section of the project README.
This project builds on the research and open-source work below. Each entry gives the work’s license. “Inspiration only” marks a work we read for facts and ideas, from which we copy no code, text or data.
Research
- A DAW vendor’s FAQ on keys and scales (no license found): the snap rule of out-of-scale notes. Inspiration only.
- A home-studio table of middle-C labels (no license found): the three octave labels of MIDI note 60. Inspiration only.
- An electronic genre guide (CC BY-NC-SA 4.0): tempo ranges and drum skeletons of the genre templates. Inspiration only.
- A producer guide to trap beats (no license found): the trap tempo range, drum skeleton and hat velocities. Inspiration only.
- Camelot and Open Key tables of a key-detection vendor (no license found): the key codes the dictionary computes by formula. Inspiration only.
- Tuomas Eerola, Anders Friberg and Roberto Bresin, Emotional expression in music: contribution, linearity, and additivity of primary musical cues (CC BY 3.0), Frontiers in Psychology 2013: mode as the strongest of six cues, with additive cues, behind the mood defaults of the music dictionary.
- English Wikipedia pages on scales, chords, progressions and rhythms (CC BY-SA 4.0): scale sets, chord names and rhythm names, facts only. Inspiration only.
- Producer guides and tutorials on scales, chords, voicing, rhythm and technique, about 70 pages listed by kind in the published music lexicon (no license found): source counts, tiers and idiom rules of the music dictionary. Inspiration only.
- A mix engineer’s glossary of words for mix revisions (no license found): bands and senses of sound words in the word dictionary. Inspiration only.
- A musician’s sound dictionary with frequency labels (no license found): frequency ranges of sound words in the word dictionary. Inspiration only.
- An article on what muddy, boxy and tinny sound like (no license found): frequency ranges of sound words in the word dictionary. Inspiration only.
- A one-word description of each frequency band (no license found): one sound word per band in the word dictionary. Inspiration only.
- A guide to stereo width in mastering (no license found): mono crossover and width ranges in the word dictionary. Inspiration only.
- A blog post on wobble-bass LFO basics (no license found): wobble rates, sweep range and sub handling in the word dictionary. Inspiration only.
- A guide to dubstep bass design (no license found): resonance, formant and growl numbers in the word dictionary. Inspiration only.
- A guide to designing a pluck synth (no license found): pluck envelope, filter and detune numbers in the word dictionary. Inspiration only.
- A guide to a lo-fi piano sound (no license found): the lo-fi chain with its numbers in the word dictionary. Inspiration only.
- A synth-basics guide on pulse waves (no license found): pulse widths and detune beating in the word dictionary. Inspiration only.
- An article on making digital synths sound more like analog synths (no license found): drift, detune and envelope hold in the word dictionary. Inspiration only.
- A guide chapter on bass design and the low end (CC BY-NC-SA 4.0): sub, reese and mono numbers in the word dictionary. Inspiration only.
- A guide chapter on envelopes, filters and amplifiers (CC BY-NC-SA 4.0): envelope shapes by name in the word dictionary. Inspiration only.
- An article on synth pads (no license found): pad and lush senses in the word dictionary. Inspiration only.
- A blog post on lo-fi mixing (no license found): lo-fi parameters in the word dictionary. Inspiration only.
- Forum threads on what thin, warm and fat mean (no license found): producer senses of thin, warm and fat in the word dictionary. Inspiration only.
- An encyclopedia page on the just-noticeable difference (CC BY-SA 4.0): the level step of the intensity ladder in the word dictionary. Inspiration only.
- J.-T. Milde, Using natural language to specify sound parameters (CC BY 3.0), ICMC/SMC 2014: modifiers as negation and gradation, behind the intensity ladder.
- An encyclopedia entry on the frequency just-noticeable difference (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - A decibel loudness comparison chart (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - Tips to make a bass sound better (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - A bitcrusher explainer (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - Tips for lo-fi tape sounds (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - How to design trance plucks (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - A guide to pluck envelopes (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - An article on mixing bass (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - The mono stack method for wide synths (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - A synth vocabulary column from 1982 (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - A video on why synth presenters use adjectives (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - A forum thread on lush pads (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - A video on making a synth sound evil (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - A video on analog-style fine tuning (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - A guide to evolving wavetable sounds (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - An article on modulation in synthesis (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - A short video post on fatness from movement (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - An article on stereo width in electronic music (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - Video captions on a quarter-duty pulse wave (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - A forum thread on clean bell sounds (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - Synthesis waveform basics (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - A forum thread on growl basses (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - Techniques to make a track sound vintage (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - A video on what makes a synth analog (no license found), read through a search excerpt: one
snippetfact of the word dictionary. Inspiration only. - Published word lexicon and ranked list (MIT, as this repository): the style and technique word ranks and the describing-word lexicon.
- Kevin Thor Hansen, Sound2Serum: Neural Sound Matching System for Serum 2 (no license found), master thesis, Aalborg University Copenhagen, 2026, supervisor Cumhur Erkut: fixed-template presets, bank-toggle reload and per-category parameter priors. Inspiration only.
- Joakim Andén and Stéphane Mallat, Deep Scattering Spectrum (in copyright), IEEE TSP 2014: wavelet scattering coefficients as timbre descriptors. Inspiration only.
- Joakim Andén et al., Joint Time-Frequency Scattering (in copyright), IEEE TSP 2019: joint time-frequency scattering for modulation structure. Inspiration only.
- Les Atlas and Shihab A. Shamma, Joint Acoustic and Modulation Frequency (no license found), EURASIP JASP 2003: modulation-spectrum view for LFO rate analysis. Inspiration only.
- François Auger and Patrick Flandrin, Improving the readability of time-frequency and time-scale representations by the reassignment method (in copyright), IEEE TSP 1995: reassigned spectrogram for sharper partial peaks. Inspiration only.
- Roland Badeau et al., Performance of ESPRIT for Estimating Mixtures of Complex Exponentials Modulated by Polynomials (in copyright), IEEE TSP 2008: ESPRIT accuracy for decaying partials. Inspiration only.
- Oren Barkan et al., InverSynth: Deep Estimation of Synthesizer Parameter Configurations from Audio Signals (arXiv non-exclusive license), IEEE/ACM TASLP 2019: ordinal-bin classification and per-parameter identifiability findings. Inspiration only.
- Emmanouil Benetos and Simon Dixon, Polyphonic music transcription using note onset and offset detection (in copyright), ICASSP 2011: note offsets are harder to find than onsets. Inspiration only.
- Nancy Bertin et al., Enforcing Harmonicity and Smoothness in Bayesian Non-Negative Matrix Factorization Applied to Polyphonic Music Transcription (in copyright), IEEE TASLP 2010: harmonic NMF for partial separation. Inspiration only.
- Rachel M. Bittner et al., A Lightweight Instrument-Agnostic Model for Polyphonic Note Transcription and Multipitch Estimation (CC BY-NC-SA 4.0), ICASSP 2022: the Basic Pitch model. Inspiration only.
- Boualem Boashash, Estimating and interpreting the instantaneous frequency of a signal. I. Fundamentals (in copyright), Proceedings of the IEEE 1992: instantaneous frequency from the analytic signal. Inspiration only.
- Philipp Bogdan, INSTRUMENTAL: Automatic Synthesizer Parameter Recovery from Audio via Evolutionary Optimization (CC BY 4.0), arXiv 2026: bounded CMA-ES settings, composite spectral loss, parameter-count lesson.
- Stephen Brade et al., SynthScribe: Deep Multimodal Tools for Synthesizer Sound Retrieval and Exploration (CC BY 4.0), arXiv 2023: CLAP retrieval over rendered presets and the middle-C 4 s render recipe.
- Judith C. Brown, Calculation of a constant Q spectral transform (in copyright), JASA 1991: constant-Q transform for bass-range resolution. Inspiration only.
- Fred Bruford et al., Synthesizer Sound Matching Using Audio Spectrogram Transformers (CC BY 4.0), DAFx 2024: factory-distribution sampling and audio-based scoring.
- Paul Callens and Milos Cernak, Joint Blind Room Acoustic Characterization From Speech And Music Signals Using Convolutional Recurrent Neural Networks (arXiv non-exclusive license), arXiv 2020: neural estimates of reverb time and clarity. Inspiration only.
- Alistair Carson et al., Resampling Filter Design for Multirate Neural Audio Effect Processing (CC BY 4.0), arXiv 2025: aliasing-to-signal ratio for judging distortion aliasing.
- H. T. Carvalho et al., Bayesian suppression of memoryless nonlinear audio distortion (in copyright, free access), EUSIPCO 2015: blind piecewise-linear distortion-curve estimate. Inspiration only.
- Sungkyun Chang et al., YourMT3+: Multi-instrument Music Transcription with Enhanced Transformer Architectures and Cross-dataset Stem Augmentation (arXiv non-exclusive license), MLSP 2024: note-level evaluation metrics and the octave-error finding. Inspiration only.
- Théo Chasle Cauchy et al., Predicting Timbre Traits for Interpretable Assessment of Musical Sound Synthesizers (CC BY 4.0), arXiv 2026: timbre-trait probes on frozen audio embeddings.
- Ojas Chaturvedi et al., Advancing Multi-Instrument Music Transcription: Results from the 2025 AMT Challenge (CC BY 4.0), NeurIPS 2025 AI for Music Workshop: the polyphony score drop.
- Sanyuan Chen et al., BEATs: Audio Pre-Training with Acoustic Tokenizers (arXiv non-exclusive license), arXiv 2022: audio tagger architecture read for the model survey. Inspiration only.
- Zui Chen et al., Sound2Synth: Interpreting Sound via FM Synthesizer Parameters Estimation (CC BY-NC-SA 4.0), IJCAI 2022: ordinal classification with label smoothing, and the feature set. Inspiration only.
- Alain de Cheveigné and Hideki Kawahara, YIN, a fundamental frequency estimator for speech and music (in copyright), JASA 2002: the YIN period estimator. Inspiration only.
- Joseph T. Colonel et al., Reverse engineering memoryless distortion effects with differentiable waveshapers (no license found), AES Convention 153, 2022: Wiener-Hammerstein distortion model with a learned waveshaper. Inspiration only.
- Paolo Combes et al., Neural Proxies for Sound Synthesizers: Learning Perceptually Informed Preset Representations (CC BY 4.0), JAES 2025: audio-metric evaluation set for comparing sounds.
- Marco Comunità et al., Guitar Effects Recognition and Parameter Estimation with Convolutional Neural Networks (CC BY-NC-SA 4.0), JAES 2021: pedal classification and setting estimation for distortion. Inspiration only.
- Helena Cuesta et al., A Framework for Multi-f0 Modeling in SATB Choir Recordings (CC BY 4.0), SMC 2019: spectral peak width as a proxy for f0 spread.
- Orchisama Das, An Open Source Stereo Widening Plugin (CC BY 4.0), DAFx 2024: catalogue of width methods such as mid/side, velvet-noise and allpass decorrelators.
- Torsten Dau et al., Modeling auditory processing of amplitude modulation. I. Detection and masking with narrow-band carriers (in copyright), JASA 1997: modulation filterbank layout for LFO bands. Inspiration only.
- Ingrid Daubechies et al., Synchrosqueezed wavelet transforms: An empirical mode decomposition-like tool (in copyright, free access), ACHA 2011: synchrosqueezing for sharp time-frequency ridges. Inspiration only.
- Qixin Deng et al., Do Joint Language-Audio Embeddings Encode Perceptual Timbre Semantics? (CC BY 4.0), arXiv 2025: CLAP aligns weakly with timbre ratings.
- Satvik Dixit et al., Vision Language Models Are Few-Shot Audio Spectrogram Classifiers (CC BY 4.0), arXiv 2024: spectrogram reading by language models.
- Seungheon Doh et al., Can Large Language Models Predict Audio Effects Parameters from Natural Language? (CC BY-NC-SA 4.0), WASPAA 2025: prompt layout, in-context ablation, and bound tests for feature choice. Inspiration only.
- Seungheon Doh et al., LLM2Fx-Tools: Tool Calling For Music Post-Production (CC BY-NC-SA 4.0), ICLR 2026: tool-call schema idea for preset edits. Inspiration only.
- Jonathan Driedger et al., Template-Based Vibrato Analysis of Music Signals (CC BY 4.0), ISMIR 2016: vibrato detection on the spectrogram, without an f0 stage.
- Jonathan Driedger et al., Extending Harmonic-Percussive Separation of Audio Signals (CC BY 4.0), ISMIR 2014: harmonic-percussive separation with a residual.
- James Eaton et al., Estimation of Room Acoustic Parameters: The ACE Challenge (in copyright), IEEE/ACM TASLP 2016: benchmark for blind reverb-time estimates. Inspiration only.
- Felix Eichas and Udo Zölzer, Black-box Modeling of Distortion Circuits with Block-Oriented Models (in copyright, free access), DAFx 2016: block-oriented models of distortion circuits. Inspiration only.
- Amro El-Jaroudi and John Makhoul, Discrete all-pole modeling (in copyright), IEEE TSP 1991: all-pole envelope fitted at harmonic frequencies. Inspiration only.
- Sean Enderby and Ryan Stables, A nonlinear method for manipulating warmth and brightness (in copyright, free access), DAFx 2017: centroid-like features track brightness. Inspiration only.
- Fabián Esqueda et al., Virtual Analog Models of the Lockhart and Serge Wavefolders (CC BY 4.0), Applied Sciences 2017: wavefolder transfer curves for the distortion layer.
- Angelo Farina, Simultaneous measurement of impulse response and distortion with a swept-sine technique (in copyright), AES Convention 108, 2000: swept-sine separation of harmonic distortion orders. Inspiration only.
- Cédric Févotte and Jérôme Idier, Algorithms for Nonnegative Matrix Factorization with the β-Divergence (in copyright), Neural Computation 2011: NMF update rules for spectrogram factorization. Inspiration only.
- Cédric Févotte et al., Nonnegative Matrix Factorization with the Itakura-Saito Divergence: With Application to Music Analysis (in copyright, free access), Neural Computation 2009: Itakura-Saito NMF for note and component separation. Inspiration only.
- Kelly R. Fitz and Sean A. Fulop, A Unified Theory of Time-Frequency Reassignment (arXiv non-exclusive license), arXiv 2009: reassignment theory behind sharper spectrogram peaks. Inspiration only.
- Derry FitzGerald, Harmonic/Percussive Separation using Median Filtering (in copyright, free access), DAFx 2010: median-filter harmonic-percussive separation. Inspiration only.
- Hannes Gamper and Ivan J. Tashev, Blind Reverberation Time Estimation Using a Convolutional Neural Network (in copyright), IWAENC 2018: neural blind reverb-time estimate, trained on speech. Inspiration only.
- Philipp Götz et al., Blind Acoustic Parameter Estimation Through Task-Agnostic Embeddings Using Latent Approximations (CC BY-NC-SA 4.0), IWAENC 2024: neural blind estimates of room acoustic parameters. Inspiration only.
- Masato Hagiwara et al., Modeling Animal Vocalizations through Synthesizers (CC BY 4.0), arXiv 2022: optimizer comparison on a parametric synthesizer.
- Fredric J. Harris, On the use of windows for harmonic analysis with the discrete Fourier transform (in copyright), Proceedings of the IEEE 1978: the Blackman-Harris window coefficients. Inspiration only.
- Ben Hayes et al., Audio synthesizer inversion in symmetric parameter spaces with approximately equivariant flow matching (arXiv non-exclusive license), ISMIR 2025: audio metrics, not parameter distance, score a sound match. Inspiration only.
- Ben Hayes, Off-manifold robustness in synthesizer inversion with joint distribution flow matching (arXiv non-exclusive license), arXiv 2026: real audio sits off the synth manifold, so exact matches are unlikely. Inspiration only.
- Perfecto Herrera and Jordi Bonada, Vibrato Extraction and Parameterization in the Spectral Modeling Synthesis Framework (in copyright, free access), DAFx 1998: vibrato and tremolo rate and depth from the f0 track. Inspiration only.
- Reemt Hinrichs et al., Convolutional neural networks for the classification of guitar effects and extraction of the parameter settings of single and multi-guitar effects from instrument mixes (CC BY 4.0), EURASIP JASM 2022: effect class and setting estimates from mixes.
- Nicki Holighaus et al., A Framework for Invertible, Real-Time Constant-Q Transforms (in copyright), IEEE TASLP 2013: invertible constant-Q transform. Inspiration only.
- Andrew Horner et al., Methods for Multiple Wavetable Synthesis of Musical Instrument Tones (in copyright), JAES 1993: basis spectra for multiple-wavetable matching. Inspiration only.
- Yingbo Hua and Tapan K. Sarkar, Matrix pencil method for estimating parameters of exponentially damped/undamped sinusoids in noise (in copyright), IEEE TASSP 1990: matrix pencil method for partial frequency and damping. Inspiration only.
- Johannes Imort et al., Distortion Audio Effects: Learning How to Recover the Clean Signal (CC BY 4.0), ISMIR 2022: restoration baselines for overdrive and hard clipping.
- Anssi Klapuri, Multiple fundamental frequency estimation based on harmonicity and spectral smoothness (in copyright), IEEE TSAP 11(6) 2003: harmonic masks around the partials of each note. Inspiration only.
- Anssi Klapuri et al., Robust multipitch estimation for the analysis and manipulation of polyphonic musical signals (in copyright, free access), DAFx 2000: iterative estimate-and-cancel multipitch for polyphony counts. Inspiration only.
- Ramdas Kumaresan and Donald W. Tufts, Estimating the parameters of exponentially damped sinusoids and pole-zero modeling in noise (in copyright), IEEE TASSP 1982: damped-sinusoid parameter estimation. Inspiration only.
- Jean Laroche, The use of the matrix pencil method for the spectrum analysis of musical signals (in copyright), JASA 1993: matrix pencil analysis applied to music. Inspiration only.
- Sungho Lee et al., Blind Estimation of Audio Processing Graph (CC BY-SA 4.0), ICASSP 2023: effect graph estimate from a wet reference. Inspiration only.
- Hazounne Lee et al., Wavespace: A Highly Explorable Wavetable Generator (CC BY 4.0), arXiv 2024: wavetable descriptors such as brightness, richness, fullness, undulation and symmetry.
- Ying-Shuo Lee et al., Distortion Recovery: A Two-Stage Method for Guitar Effect Removal (CC BY 4.0), DAFx 2024: two-stage restoration of overdrive and amp effects.
- Yizhi Li et al., MERT: Acoustic Music Understanding Model with Large-Scale Self-supervised Training (CC BY-SA 4.0), ICLR 2024: music embedding architecture read for the model survey. Inspiration only.
- N. R. Lomb, Least-squares frequency analysis of unequally spaced data (in copyright), Astrophysics and Space Science 1976: the periodogram for the gappy envelope track of note frames. Inspiration only.
- Low-interval-limit charts (no license found): the lowest pitch of each interval before it turns muddy, kept as soft warnings. Inspiration only.
- John Makhoul, Linear prediction: A tutorial review (in copyright), Proceedings of the IEEE 1975: linear prediction for all-pole filter envelopes. Inspiration only.
- Héctor Martel et al., Probing Low-Level Acoustic Attribute Encoding in CLAP Audio Embeddings (CC BY 4.0), DAFx 2026: centroid is not linearly decodable from CLAP.
- Naotake Masuda and Daisuke Saito, Quality Diversity for Synthesizer Sound Matching (CC BY 3.0), DAFx 2021: log-spectral fitness and novelty-based diversity.
- Matthias Mauch and Simon Dixon, pYIN: A fundamental frequency estimator using probabilistic threshold distributions (in copyright), ICASSP 2014: the probabilistic YIN f0 track with its voiced flag and Viterbi decode. Inspiration only.
- Kazuki Matsumoto and Kohei Yatabe, Estimation of the number of unison and degree of detune of SuperSaw in subtractive synthesizers (in copyright, free access), ASJ Autumn Meeting 2022: cost function for unison count and detune. Inspiration only.
- Robert J. McAulay and Thomas F. Quatieri, Speech analysis/Synthesis based on a sinusoidal representation (in copyright), IEEE TASSP 1986: sinusoidal model and partial tracking. Inspiration only.
- Christopher Mitcheltree and Hideki Koike, SerumRNN: Step by Step Audio VST Effect Programming (CC BY-NC-SA 4.0), EvoMUSART 2021: iterative one-block-at-a-time matching with a stop rule. Inspiration only.
- Christopher Mitcheltree et al., Modulation Extraction for LFO-driven Audio Effects (arXiv non-exclusive license), DAFx 2023: the only learned extractor of the LFO behind phaser, flanger and chorus effects. Inspiration only.
- Christopher Mitcheltree et al., Modulation Discovery with Differentiable Digital Signal Processing (arXiv non-exclusive license), WASPAA 2025: recovering modulation curves of oscillator, filter and envelope with differentiable synthesis. Inspiration only.
- Meinard Müller, Fundamentals of Music Processing, notebook C1S3 Timbre (CC BY-NC-SA 4.0), Springer 2015: limits of the ADSR envelope model. Inspiration only.
- John Muradeli et al., Differentiable Time-Frequency Scattering on GPU (CC BY 4.0), DAFx 2022: differentiable joint time-frequency scattering.
- Anastasia Natsiou et al., Interpretable Timbre Synthesis using Variational Autoencoders Regularized on Timbre Descriptors (arXiv non-exclusive license), arXiv 2023: the 10-90 percent attack threshold, as weak evidence. Inspiration only.
- Shahan Nercessian, Neural Parametric Equalizer Matching Using Differentiable Biquads (CC BY 3.0), DAFx 2020: frequency-domain loss beats parameter loss for equalizer matching.
- Julian Neri and Philippe Depalle, Fast Partial Tracking of Audio with Real-Time Capability through Linear Programming (in copyright, free access), DAFx 2018: partial tracking by linear programming. Inspiration only.
- Antonín Novák et al., Nonlinear System Identification Using Exponential Swept-Sine Signal (in copyright), IEEE TIM 2010: exponential swept-sine harmonic response analysis. Inspiration only.
- Youichi Okita and Haruhiro Katayose, Audio Effect Estimation with DNN-Based Prediction and Search Algorithm (arXiv non-exclusive license), ICASSP 2026: network predicts effect types, then search sets order and parameters. Inspiration only.
- Julian D. Parker et al., Reducing the Aliasing of Nonlinear Waveshaping Using Continuous-Time Convolution (in copyright, free access), DAFx 2016: antiderivative anti-aliasing for waveshapers. Inspiration only.
- Geoffroy Peeters et al., The Timbre Toolbox: Extracting audio descriptors from musical signals (in copyright), JASA 2011: descriptor definitions, the weakest-effort attack method, and the attack and decay descriptors of the envelope layer. Inspiration only.
- Côme Peladeau and Geoffroy Peeters, Blind estimation of audio effects using an auto-encoder approach and differentiable digital signal processing (CC BY 4.0), ICASSP 2024: audio loss gives better sound, parameter loss better parameters.
- Antonio Pertusa, Computationally efficient methods for polyphonic music transcription (in copyright, free access), PhD thesis, University of Alicante, 2010: loudness and spectral smoothness criterion for polyphony estimates. Inspiration only.
- Rama Ratnam et al., Blind estimation of reverberation time (in copyright), JASA 2003: maximum-likelihood decay-rate formulas for blind reverb time. Inspiration only.
- David C. Rife and Robert R. Boorstyn, Single-tone parameter estimation from discrete-time observations (in copyright), IEEE Transactions on Information Theory 1974: the frequency accuracy bound for a peak. Inspiration only.
- Xavier Riley and Simon Dixon, FiloBass: A Dataset and Corpus Based Study of Jazz Basslines (CC BY 4.0), ISMIR 2023: bass transcription baseline numbers.
- Xavier Riley and Simon Dixon, CREPE Notes: A new method for segmenting pitch contours into discrete notes (CC BY 4.0), SMC 2023: combining inverted pitch confidence with the pitch gradient to find note boundaries.
- Alain Riou et al., PESTO: Pitch Estimation with Self-supervised Transposition-equivariant Objective (CC BY 4.0), ISMIR 2023: compared as an f0 model, not used.
- Axel Röbel and Xavier Rodet, Efficient spectral envelope estimation and its application to pitch shifting and envelope preservation (in copyright, free access), DAFx 2005: True Envelope through harmonic peaks. Inspiration only.
- Jonas Roth et al., Alias-Free Oscillator Synchronization via Additive Synthesis (CC BY 4.0), DAFx 2026: sync waveform model behind the sync-ratio estimate.
- Richard Roy and Thomas Kailath, ESPRIT–Estimation of Signal Parameters via Rotational Invariance Techniques (in copyright), Optical Engineering 1990: ESPRIT subspace method for line spectra. Inspiration only.
- Charalampos Saitis and Stefan Weinzierl, The Semantics of Timbre (in copyright, free access), Timbre: Acoustics, Perception, and Cognition, 2019: semantic dimensions of timbre and their acoustic correlates for the sound-word dictionary. Inspiration only.
- Hiroaki Sakoe and Seibi Chiba, Dynamic programming algorithm optimization for spoken word recognition (in copyright), IEEE TASSP 1978: dynamic time warping for aligning renders. Inspiration only.
- S Sakshi et al., MMAU: A Massive Multi-Task Audio Understanding and Reasoning Benchmark (CC BY 4.0), arXiv 2024: perception, not reasoning, limits audio language models.
- Amir Salimi et al., Evaluating Sound Similarity Metrics for Differentiable, Iterative Sound-Matching (CC BY 4.0), arXiv 2025: the case for several named distances.
- Jeffrey D. Scargle, Studies in astronomical time series analysis. II. Statistical aspects of spectral analysis of unevenly spaced data (in copyright), The Astrophysical Journal 1982: the normalised form of the Lomb periodogram. Inspiration only.
- Ralph O. Schmidt, Multiple emitter location and signal parameter estimation (in copyright), IEEE TAP 1986: MUSIC subspace method for line spectra. Inspiration only.
- Manfred R. Schroeder, New Method of Measuring Reverberation Time (in copyright), JASA 1965: backward integration of decay curves. Inspiration only.
- Xavier Serra and Julius O. Smith, Spectral Modeling Synthesis: A Sound Analysis/Synthesis System Based on a Deterministic Plus Stochastic Decomposition (in copyright), Computer Music Journal 1990: sines-plus-noise decomposition. Inspiration only.
- Serum 2 User Guide (all rights reserved), manual 1.0.3: the C3 label of note 60, the glide controls and the envelope stage definitions, cited only. Inspiration only.
- Siyuan Shan et al., Differentiable Wavetable Synthesis (CC BY 4.0), ICASSP 2022: learned dictionary of wavetables for neural synthesis.
- Di Sheng and George Fazekas, Feature Selection for Dynamic Range Compressor Parameter Estimation (in copyright), AES 2018: standard features give threshold and ratio, not attack and release. Inspiration only.
- Christian J. Steinmetz et al., Filtered Noise Shaping for Time Domain Room Impulse Response Estimation From Reverberant Speech (arXiv non-exclusive license), WASPAA 2021: room impulse response from filtered noise. Inspiration only.
- Fabian-Robert Stöter et al., Unison Source Separation (in copyright, free access), DAFx 2014: AM and FM differences separate unison sources. Inspiration only.
- Haoran Sun et al., Neural-Enhanced Dynamic Range Compression Inversion: A Hybrid Approach for Restoring Audio Dynamics (CC BY 4.0), arXiv 2024: compressor parameter and dry-signal recovery.
- Haoran Sun et al., Black-Box Optimization for Identifying and Inverting Audio Dynamic Range Control Effects (arXiv non-exclusive license), arXiv 2026: search over compressor settings with dynamics descriptors. Inspiration only.
- Yirong Sun et al., SonicBench: Dissecting the Physical Perception Bottleneck in Large Audio Language Models (arXiv non-exclusive license), arXiv 2026: audio language models are weak on physical attributes. Inspiration only.
- David Temperley and Daphne Tan, Emotional connotations of diatonic modes (in copyright), Music Perception 2013: listener happiness ranks of six modes, behind the mood defaults of the music dictionary. Inspiration only.
- Godfried Toussaint, The Euclidean Algorithm Generates Traditional Musical Rhythms (no license found), Bridges 2005: the grouping algorithm and its printed patterns, the test vectors of the Euclidean function. Inspiration only.
- Michal Švento et al., Estimation and Restoration of Unknown Nonlinear Distortion using Diffusion (CC BY 4.0), arXiv 2025: spline waveshaper fitted blindly to distorted audio.
- Adam Szabo, How to Emulate the Super Saw (in copyright, free access), KTH degree project 2010: measured supersaw detune and mix. Inspiration only.
- Ryoya Tabata et al., FM Parameter Estimation with Low-Order Rational Constraints on Wasserstein Loss Landscape (CC BY 4.0), DAFx 2026: low-order rational ratio constraint for FM parameter search.
- Osamu Take et al., Audio Effect Chain Estimation and Dry Signal Recovery From Multi-Effect-Processed Musical Signals (CC BY 4.0), DAFx 2024: undo the last effect, then repeat to recover the chain.
- Gaurav Thakur et al., The Synchrosqueezing algorithm for time-varying spectral analysis: Robustness properties and new paleoclimate applications (in copyright), Signal Processing 2013: robustness of synchrosqueezing under noise. Inspiration only.
- Haokun Tian et al., Assessing the Alignment of Audio Representations with Timbre Similarity Ratings (CC BY 4.0), ISMIR 2025: the MFCC baseline and the alignment protocol for compare.
- Bernardo Torres et al., Unsupervised Harmonic Parameter Estimation Using Differentiable DSP and Spectral Optimal Transport (arXiv non-exclusive license), ICASSP 2024: optimal-transport loss for f0 and harmonic amplitudes. Inspiration only.
- Joseph Turian et al., One Billion Audio Sounds from GPU-enabled Modular Synthesis (CC BY 4.0), arXiv 2021: rank tests for audio distances.
- Joseph Turian and Max Henry, I’m Sorry for Your Loss: Spectrally-Based Audio Distances Are Bad at Pitch (CC BY 4.0), ICBINB at NeurIPS 2020: spectral losses are weak on pitch, so align pitch first.
- Noy Uzrad et al., DiffMoog: a Differentiable Modular Synthesizer for Sound Matching (CC BY 4.0), arXiv 2024: sound matching with a differentiable modular synthesizer.
- Cyrus Vahidi et al., Timbre Space Representation of a Subtractive Synthesizer (arXiv non-exclusive license), Timbre 2020: descriptors in a subtractive-synthesizer timbre space. Inspiration only.
- Cyrus Vahidi et al., Mesostructures: Beyond Spectrogram Loss in Differentiable Time-Frequency Analysis (CC BY 4.0), arXiv 2023: scattering-based similarity of arpeggio-scale parameters.
- Benno Weck et al., MuChoMusic: Evaluating Music Understanding in Multimodal Audio-Language Models (CC BY 4.0), ISMIR 2024: audio language models lean on the text prior.
- Luwei Yang et al., AVA: An Interactive System for Visual and Quantitative Analyses of Vibrato and Portamento Performance Styles (CC BY 4.0), ISMIR 2016: vibrato detection and a logistic portamento model for glide.
- Chin-Yun Yu et al., Differentiable All-pole Filters for Time-varying Audio Systems (arXiv non-exclusive license), DAFx 2024: time-varying all-pole filter matching phaser and acid-style filter sweeps. Inspiration only.
- Chin-Yun Yu et al., Improving Inference-Time Optimisation for Vocal Effects Style Transfer with a Gaussian Prior (arXiv non-exclusive license), WASPAA 2025: a preset-distribution prior for effect parameter search. Inspiration only.
- Tsugumasa Yutani et al., Wavetable Synthesis Using CVAE for Timbre Control Based on Semantic Label (CC BY 4.0), APSIPA ASC 2024: wavetable generation from semantic labels.
- Pavel Záviška et al., A survey and an extensive evaluation of popular audio declipping methods (CC BY-NC-SA 4.0), IEEE JSTSP 2021: declipping methods for clipped distortion. Inspiration only.
- Mathias Zechmeister and Martin Kürster, The generalised Lomb-Scargle periodogram (in copyright), Astronomy & Astrophysics 2009: the floating-mean periodogram for the gated envelope of note frames. Inspiration only.
- Haina Zhu et al., MuQ: Self-Supervised Music Representation Learning with Mel Residual Vector Quantization (CC BY-NC-SA 4.0), arXiv 2025: music embedding architecture read for the model survey. Inspiration only.
Open-source projects
- ableton-lom-skill (no license found): an instrument API reference packed as one agent skill with a topic router, as a model for the Serum 2 reference. Inspiration only.
- ableton-mcp (MIT): parameter-by-index tool design, kept as a counter-example to named parameters.
- Audio Flamingo (MIT code, non-commercial weights): audio language model judged weak on synth attributes. Inspiration only.
- audio.cpp (Apache-2.0): facts on running audio models locally in C++.
- auraloss (Apache-2.0): multi-resolution STFT loss, reimplemented in Rust.
- AutoSynth (Apache-2.0): perturb-and-render recipe with logged edit masks for the test bench.
- Basic Pitch (Apache-2.0): the upstream transcriber and decoder whose figures the notes epic reproduces.
- basic-pitch-rs (Apache-2.0): the input format of the transcription model, a 22050 Hz mono signal, which sets the resample target of the stem intake.
- BE-AFX (no license): blind effect-estimation auto-encoder with differentiable effects, read for design. Inspiration only.
- BEATs (MIT): audio tagger facts for the model survey.
- blind_rt60 (MIT): blind reverb-time formulas, reimplemented in Rust.
- burn (MIT OR Apache-2.0): Rust inference runtime facts for the fallback plan.
- candle (MIT OR Apache-2.0): Rust inference runtime facts for the fallback plan.
- chordparser (MIT): a chord-symbol parser crate, a test-only oracle of the chord parser of the music dictionary.
- CLAP (microsoft/CLAP) (MIT): audio tower used for the style-embedding study.
- claude-collider (MIT): server paired with theory skills, and a short pitfall list.
- cochlea (MIT OR Apache-2.0): spectrogram PNG for agents, and stereo probe formulas.
- coreml-native (Apache-2.0 OR MIT): platform ML binding facts for the deferred runtime option.
- CREPE (MIT): 360-bin pitch table and ONNX weights.
- CREPE Notes (GPL-3.0): inverted pitch confidence plus pitch gradient to find note boundaries. Inspiration only.
- CTAG (MIT): gradient-free, embedding-guided parameter search loop.
- dasp-pytorch (Apache-2.0): tanh drive formula.
- DawDreamer (GPL-3.0): how a JUCE host loads plug-in state, schedules MIDI and renders offline. Inspiration only.
- DDSP (Apache-2.0): multi-scale spectral loss definition.
- DDSynth-RL (Apache-2.0): multi-metric render-and-compare recipe, group-relative scoring, crash-isolated render workers.
- DeepAFx-ST (non-commercial research license): neural effect style transfer, surveyed for the effects layer. Inspiration only.
- DeepFormants (MIT): neural formant tracker, surveyed for filter resonance estimates.
- dereverb-room (GPL-3.0): voice reverb-removal model, surveyed for wet-signal handling. Inspiration only.
- dereverb_mel_band_roformer (GPL-3.0): vocal reverb-removal checkpoints, surveyed for wet-signal handling. Inspiration only.
- Descript Audio Codec (MIT): neural codec latents, surveyed as audio features.
- diff-wave-synth (no license): unofficial port of differentiable wavetable synthesis. Inspiration only.
- DiffAPF TB-303 (MPL-2.0): differentiable all-pole filter experiments on acid-style sweeps. Inspiration only.
- DiffMoog (MIT): chain-with-activation-flags template and spectral-distance findings.
- diffsynth (MIT): differentiable ADSR module design.
- DiffVox (MIT): chain design, losses and parameter priors for vocal effects.
- EnCodec (MIT): neural codec latents, surveyed as audio features.
- Essentia (AGPL-3.0): audio descriptor extractor design. Inspiration only.
- Essentia models (CC BY-NC-SA 4.0): bright/dark, instrument and dry/wet classifier heads trained on NSynth. Inspiration only.
- FCPE (MIT): comparison model and local-argmax decoding.
- FCPE ONNX export (license not checked): the ONNX form of FCPE, tested for the f0 track and not used. Inspiration only.
- ferraus-pitch (Apache-2.0 OR MIT): pYIN note segmentation defaults.
- FL-Automation (MIT, in its
sdk/folders): frame-by-frame labels of Serum 2 wavetables and a state schema. - Flow Synthesizer (MIT): nested parameter subsets, audio-versus-parameter error, timbre tag vocabulary.
- flp-extract-fxp (MIT): a corpus of Serum 2 factory preset metadata, tags, value ranges and description words.
- FM-SynAPSE (MIT): shared audio and preset embedding, and the MFCC, CLAP, PANNs timbre comparison.
- Fx-Encoder++ (CC BY-NC 4.0): instrument-wise effect embeddings, surveyed for distortion and effect layers. Inspiration only.
- FXencoder (music_mixing_style_transfer) (MIT code, unlicensed weights): mixing-style effect embedding, surveyed for width and effect layers. Inspiration only.
- gfx_classifier (BSD-3-Clause): guitar-effect classifier as a distortion bench probe, with its published models.
- GRAFX (Apache-2.0): differentiable effect-graph framework, read for chain design.
- Helm (GPL-3.0): preset role taxonomy of a synthesizer. Inspiration only.
- helm_patches (CC BY 4.0, in
Factory Presets/): preset category and name-word counts, and describing-word counts of the ranked word list at commit5fafa70. - IIRNet (Apache-2.0): neural IIR filter design from a magnitude response.
- JUCE (AGPL-3.0): VST3 state envelope and base64 facts. Inspiration only.
- Kimi-Audio (Apache-2.0 and MIT): audio language model tested on timbre attributes, CUDA-only.
- kymatio (BSD-3-Clause): scattering transform implementation, read for joint time-frequency scattering.
- LAION-CLAP (CC0-1.0): 48 kHz, 10 s, 64-band log-mel recipe.
- libf0 (MIT): YIN, pYIN and salience f0 estimator definitions.
- librosa (ISC): MFCC, mel and spectral descriptor definitions, reimplemented in Rust, and the
pyinfunction of release 0.9.1 that the f0 tracker follows step by step. Its notice ships inTHIRD-PARTY-LICENSES.html. - llama-cpp-2 (MIT OR Apache-2.0): Rust binding facts for local audio language models.
- LTU (no license): audio language model judged weak on timbre attributes. Inspiration only.
- MERT (Apache-2.0 code, CC BY-NC 4.0 weights): music embedding model, surveyed for pitch and reverb probes. Inspiration only.
- mir_eval (MIT): the note-transcription matching rules, reimplemented in the evaluation matcher (rules only), and the reference scores of its golden cases.
- mlx-audio (MIT): facts on running audio models with MLX.
- mlx-rs (MIT OR Apache-2.0): Rust MLX binding facts for the fallback plan.
- mod_discovery (MPL-2.0): envelope and LFO control signals found with differentiable DSP. Inspiration only.
- mod_extraction (MPL-2.0): LFO extraction from dry and wet audio, which needs a dry signal. Inspiration only.
- MT3 (Apache-2.0): segment-seam bookkeeping and overlap clean-up rules.
- MU-LLaMA (GPL-3.0): music language model judged weak on synth attributes. Inspiration only.
- MuQ (MIT code, CC BY-NC 4.0 weights): music embedding model, surveyed for instrument and reverb probes. Inspiration only.
- MuScriptor (MIT code, CC BY-NC 4.0 weights): hard instrument mask, gap filling, onset-lag calibration ideas. Inspiration only.
- music21 (BSD-3-Clause): chord symbols in the pinned golden tools, a local oracle of the chord tones of the music dictionary.
- nara_wpe (MIT): weighted prediction error dereverberation, read for reverb handling.
- node-serum2-preset-packager (MIT): container layout, zstd level 3, and the Init preset shape.
- objc2-core-ml (Zlib OR Apache-2.0 OR MIT): platform ML binding facts for the deferred runtime option.
- Odin 2 (GPL-3.0 code, OFL-1.1 font, no preset terms stated): factory preset category counts and describing-word counts of the ranked word list at commit
265e9e2. Inspiration only. - onnxcrepe (MIT): ONNX export of CREPE and its weights.
- OpenL3 (MIT code, CC BY 4.0 weights): audio embedding surveyed, with no ONNX export.
- Pedalboard (GPL-3.0): headless VST3 instrument hosting design with MIDI in, audio out and state get and set. Inspiration only.
- PESTO (LGPL-3.0): harmonic CQT and cached-convolution streaming idea. Inspiration only.
- pitch-estimate (MIT): per-frame 55 Hz regression test layout.
- Plugalyzer (GPL-3.0): CLI shape for rendering a VST3 instrument from MIDI to WAV. Inspiration only.
- producer-pal (GPL-3.0-or-later): how an agent server assembles its skill text from fragments. Inspiration only.
- pychord (MIT): a chord-quality table in the pinned golden tools, a local oracle of the chord tones of the music dictionary.
- pYIN and Tony (GPL-2.0-or-later): pYIN note-tracking HMM on the pitch track. Inspiration only.
- pyin-rs (MIT): the Rust port of librosa’s
pyin. The f0 tracker insrc/notes/pyin.rsports its numerics and returns the same frames, with a banded Viterbi search. Its notice ships inTHIRD-PARTY-LICENSES.html. - Qwen2-Audio (no license, Apache-2.0 weights): audio language model tested on timbre attributes. Inspiration only.
- rack (MIT OR Apache-2.0): reference for plug-in state and MIDI mapping across formats.
- RecklessSerum2Bank (MIT code, presets free to use per its README): 250 generated Serum 2 presets, the one open bank of parser fixtures.
- RemFx (Apache-2.0 code, non-commercial weights): effect detection and removal chain, surveyed for the effects layer. Inspiration only.
- RMVPE (Apache-2.0): local weighted-average decode over 9 bins.
- rust-music-theory (MIT): scales and chords, the source of the offsets a test of the music dictionary pins.
- SALMONN (Apache-2.0): audio language model judged weak on synth attributes.
- SciPy (BSD-3-Clause): reference signal-processing routines, such as Hilbert and Butterworth filters.
- serum (rich6feet) (no license found): an index of Serum 2 factory table names and content paths. Inspiration only.
- Serum 2 preset format gist (0xdevalias) (no license): original reverse-engineering thread for the Serum preset format. Inspiration only.
- Serum 2 parameter dump gist (KennethWussmann) (no license): parameter defaults of a freshly loaded Serum 2. Inspiration only.
- serum-2 (cboone) (MIT code, CC0 1.0 assets): a working user-wavetable recipe and the Serum 2 file format notes.
- serum-mcp (MIT): Serum 2 preset generation driven by a language model, and its parameter tables.
- serum-preset-packager (no license): first public decoding of the Serum 2 container. Inspiration only.
- serum-render (GPL-3.0): render reproducibility: batch state carry-over and warm-up render. Inspiration only.
- serum2-neurosync (no license found): the folder listing of factory presets as a role taxonomy; no files copied; describing-word counts of its preset names for the ranked word list at commit
cabec31. Inspiration only. - serum2-preset-loader (MIT): Serum 2 presets as plugin state, and JUCE state notes.
- serum2gen (MIT): Serum 2 preset reading, writing and parameter aliases.
- serum2vital (GPL-3.0): container facts, modulation source ids, LFO fields, FX rack structure, hosting quirks. Inspiration only.
- SerumPreset (lwy0v0) (no license found): decoded preset payloads and the hash rule of the preset container. Inspiration only.
- sms-tools (AGPL-3.0): spectral modeling analysis and synthesis reference. Inspiration only.
- SPTK (Apache-2.0): reference LPC and cepstrum routines.
- ssqueezepy (MIT): synchrosqueezing and wavelet transform implementation.
- StemFX (MIT): per-stem effect chain retrieval and transfer.
- Surge XT (GPL-3.0): the wavetable chunk format and the factory patch folders, and describing-word counts of patch names and comments for the ranked word list at commit
a30341b. Inspiration only. - surge-xt-mcp (MIT): the pattern of setting any plugin parameter by name, and what it cannot reach.
- SwiftF0 (MIT): change-point note segmentation with loudness-dip splits.
- synth-permutations (MIT): metric definitions MSS, wMFCC, SOT and RMS, checked against our own tests.
- synth-rl (GPL-3.0-or-later): edit, repair and match tasks, and the mute and blind controls. Inspiration only.
- synth-setter (GPL-3.0): render loop and noise-floor tests. Inspiration only.
- Syntheon (Apache-2.0): Init-preset-plus-minimal-subset approach and single-cycle wavetable extraction.
- tonal (MIT): the chord, scale and Roman-numeral tables the music lexicon was compared with; the dictionary commits no row of them. Inspiration only.
- torchlpc (MIT): differentiable linear-prediction filter, read for time-varying filters.
- torchsynth (Apache-2.0): seeded random-parameter synthesizer idea for optimizer tests.
- u-he Preset Randomizer (MIT): per-synth statistical guides and clamp-and-blend variation.
- Vita (GPL-3.0): Python bindings over the open Vital engine, run as a separate process for test renders. Inspiration only.
- Vital (GPL-3.0-or-later): preset file structure, parameter laws, and the mechanisms of every synth layer that the Vital reference files describe, in our own words. Our own tool extracts its parameter facts (names, ranges, defaults, engine scales, UI and menu labels) into
src/preset/vital/registry.jsonfor compatibility; no code or comments are copied. Inspiration only. - vital-mcp (GPL-3.0): render-measure-fix loop and its descriptor set. Inspiration only.
- vital_discord_preset_archive (no license found): preset role and macro-label counts; no files copied; describing-word counts of preset names and comments for the ranked word list at commit
c821ad9. Inspiration only. - WildFX (no license): DAW-hosted plugin pipeline for effect-estimation data. Inspiration only.
- WORLD (BSD-3-Clause): CheapTrick F0-adaptive spectral envelope, ported with attribution if needed.
- YAMNet (Apache-2.0): AudioSet event classifier, read as a distortion tag probe.
- YourMT3 (GPL-3.0): the rules that cut a note at the next onset of its key and drop zero-length notes, in the MIDI writer. Inspiration only.
Datasets
- AKWF-FREE (CC0-1.0): single-cycle waveforms as bench data for wavetable matching.
- AudioSet ontology (CC BY-SA 4.0): class names for effect tags. Inspiration only.
- Chordonomicon (CC BY-NC 4.0): progression shares and chord frequencies of the music dictionary, counts only. Inspiration only.
- DX7 timbre dataset (CC BY 4.0): fixed-pitch timbre-order test for compare.
- dx7-patches-and-prompts (Apache-2.0 per its card, patch origin and caption terms unclear): the vocabulary of generated patch captions. Inspiration only.
- Faronbi et al. preset dataset (CC BY 4.0): Serum parameter-vector layout as a reference; its preset names feed only the describing-word counts of the ranked word list, as an observation corpus (record revision 2).
- Freesound metadata scrape (CC BY-SA 3.0 per its card, each sound under its own license): tag, title and description word counts only; the uploader counts of the ranked word list at sha
5c427d9. Inspiration only. - GiantSteps drum-pattern datasets (no license found): shares of house, techno and breakbeat grids with a given kick or snare, counts only. Inspiration only.
- GiantSteps key dataset (no license found): key and mode shares of annotated EDM tracks, counts only. Inspiration only.
- GiantSteps MTG key dataset (no license found): key and mode shares by store genre, counts only. Inspiration only.
- Groove MIDI Dataset (CC BY 4.0), Gillick, Roberts, Engel, Eck and Bamman: onset spread, offsets and velocities of real drum performances, computed by filtering and aggregation.
- IDMT-SMT-Audio-Effects (CC BY-NC-ND 4.0): labelled guitar and bass effect recordings, surveyed for modulation and distortion. Inspiration only.
- Lakh MIDI Dataset, clean subset (CC BY 4.0 for the collection, the songs keep their copyright): pitch ranges, note counts and repetition of instrument groups, counts only.
- McGill Billboard annotations, through mirdata (CC0 per its loader): progression shares and harmonic rhythm of chart songs, counts only.
- MidiCaps (CC BY-SA 4.0): mode and tempo shares by caption tag, counts only. Inspiration only.
- MusicCaps (CC BY-SA 4.0): how listeners describe synth parts, from caption word counts; the caption counts of the ranked word list at sha
0a51889. Inspiration only. - NSynth (CC BY 4.0): bright/dark and pitch labels as a feature sanity check.
- POP909 (MIT, the songs keep their copyright): the share of melody notes outside the key, counts only.
- ReverbFX (CC BY-NC 4.0): room impulse responses derived from reverb plugins, as an effect-estimation reference. Inspiration only.
- SocialFX listener data, re-upload (no license found), revision
d7e266c: per-word agreement, curve counts, band means and reverb word effects of the word dictionary, derived bycargo xtask agreement; no row is committed. Inspiration only. - Surge pitch dataset (CC BY 4.0): pitch-labelled test audio and rank-based compare tests.
- timbremetrics timbre ratings (Thoret et al., Vahidi et al.) (no license): 21 human timbre-rating sets as the compare benchmark. Inspiration only.