oxideav-midi
Pure-Rust MIDI — Standard MIDI File (.mid / SMF) parser + transport
metadata + soft-synth scaffold. Zero C dependencies, zero FFI, zero
*-sys.
External instruments (SoundFont 2 .sf2, SFZ, DLS Level 1/2) are loaded
from disk at runtime; nothing is bundled in the binary. A pure-tone
oscillator fallback lets the synth produce some output even when no
instrument bank is installed.
Part of the oxideav framework but usable standalone.
Status
smf— full SMF (Type 0 / 1 / 2) parser. Header (MThd), tracks (MTrk), variable-length quantities (bounded to 4 bytes per spec), every channel-voice message, sysex (F0/F7), and the common meta events (tempo, time signature, key signature, text, marker, end-of-track, SMPTE offset, sequencer-specific). Running status is honoured; chunk lengths are validated against remaining bytes; total events per file are capped at 1 M to keep malformed input bounded. Round 122 addsSmfFile::time_signatures()— a sorted iteration helper that returns everyFF 58 04 nn dd cc bbchange as aTimeSignatureChange { tick, track, numerator, denominator_pow2, clocks_per_click, notated_32nd_per_quarter }with the absolute tick (cumulative delta-sum) on the parent track. Per-track sequences are stably merged so two changes at the same tick keep(track, in-track)order — the same convention the scheduler uses.TimeSignatureChange::denominator()returns the decoded1 << dd, saturated atu32::MAXso pathologicaldd >= 32can't overflow. Round 125 adds the analogousSmfFile::tempo_map()— everyFF 51 03 tt tt ttSet Tempo as aTempoChange { tick, track, microseconds_per_quarter_note, bpm }with the absolute tick on the parent track. Per-track sequences are stably merged by tick (track 0 before track 1 at the same tick).bpmis pre-computed as60_000_000.0 / µs_per_qn;µs_per_qn == 0maps tof64::INFINITYrather than divide-by-zero. Players that need an initial tempo before any explicit Set Tempo should assume 500 000 µs/qn = 120 BPM per convention. Round 128 addsSmfFile::key_signatures()— everyFF 59 02 sf michange as aKeySignatureChange { tick, track, sharps_flats, mode }with the absolute tick on the parent track, stably merged across tracks (track 0 before track 1 at the same tick).KeySignatureChange::tonic_name()/name()resolve the signed-7..=+7accidental count and0/1mode bit to a textbook circle-of-fifths label ("C major","A minor","F# major","Bb minor", …); out-of-rangesfor unknownmodereturnsNonerather than fabricating a label. Round 176 addsSmfFile::markers()— everyFF 06 len textmarker meta event as aMarkerEvent { tick, track, text }with the absolute tick on the parent track, stably merged across tracks (track 0 before track 1 at the same tick). OnlyFF 06is selected — neighbouring text-kind metas (FF 03track name,FF 05lyric, etc.) are filtered out so the helper matches the DAW convention of one section-label list per song.MarkerEvent::text_bytes()returns the raw payload (the SMF spec leaves the encoding unspecified);text_lossy()returns a UTF-8-decodedCow<str>withU+FFFDsubstitutes for invalid sequences so callers don't have to encode-detect themselves. Round 182 adds the karaoke companionSmfFile::lyrics()— everyFF 05 len textlyric meta event as aLyricEvent { tick, track, text }with the absolute tick on the parent track, stably merged across tracks (track 0 before track 1 at the same tick) under the same merge rule asmarkers()/tempo_map()/time_signatures()/key_signatures(). OnlyFF 05is selected so the.karsyllable stream comes out as a clean time-ordered list, independent of any surroundingFF 03track name /FF 06marker /FF 07cue point events. Same accessor shape as the marker helper:LyricEvent::text_bytes()for the raw payload (encoding is spec-unspecified — historically Latin-1, modern files emit UTF-8),text_lossy()for aCow<str>UTF-8 decode withU+FFFDsubstitutes for invalid sequences. Round 186 adds the film-score sync companionSmfFile::cue_points()— everyFF 07 len textcue-point meta event as aCueEvent { tick, track, text }with the absolute tick on the parent track, stably merged across tracks (track 0 before track 1 at the same tick) under the same merge rule asmarkers()/lyrics()/tempo_map()/time_signatures()/key_signatures(). OnlyFF 07is selected so callers driving external synchronisation (scene change, SFX trigger, video cue) get a clean time-ordered list independent of the surroundingFF 03track name /FF 05lyric /FF 06marker streams. Same accessor shape as the marker and lyric helpers:CueEvent::text_bytes()for the raw payload (encoding is spec-unspecified),text_lossy()for aCow<str>UTF-8 decode withU+FFFDsubstitutes for invalid sequences. Round 192 adds the DAW-track-list companionSmfFile::track_names()— everyFF 03 len texttrack-name meta event as aTrackNameEvent { tick, track, text }with the absolute tick on the parent track, stably merged across tracks (track 0 before track 1 at the same tick) under the same merge rule ascue_points()/markers()/lyrics()/tempo_map()/time_signatures()/key_signatures(). OnlyFF 03is selected so callers populating the DAW track list get a clean per-track label stream independent of the surroundingFF 01general text /FF 02copyright /FF 04instrument name /FF 05lyric /FF 06marker /FF 07cue point events. Authoring tools conventionally emit at most oneFF 03per track at tick 0 (on a format-0 file the single track'sFF 03is read as the sequence title); the helper surfaces every occurrence so callers that only want the first name per track can collect into aHashMap<usize, TrackNameEvent>keyed onTrackNameEvent::track. Same accessor shape as the cue / marker / lyric helpers:TrackNameEvent::text_bytes()for the raw payload (encoding is spec-unspecified — historically Latin-1, modern files emit UTF-8),text_lossy()for aCow<str>UTF-8 decode withU+FFFDsubstitutes for invalid sequences. Round 196 adds the voice/patch companionSmfFile::instrument_names()— everyFF 04 len textinstrument-name meta event as anInstrumentNameEvent { tick, track, text }, distinct from theFF 03track-list label so a single track may legally carry both. Pinned to the absolute tick on the parent track, stably merged across tracks (track 0 before track 1 at the same tick) under the same merge rule as the other seven text-meta helpers; onlyFF 04is selected so callers populating per-track patch / preset metadata get a clean per-track instrument stream independent of the surroundingFF 01general text /FF 02copyright /FF 03track name /FF 05lyric /FF 06marker /FF 07cue point events. Lifts the family from 7 to 8 helpers (SmfFile::{tempo_map,time_signatures,key_signatures,markers, lyrics,cue_points,track_names,instrument_names}). Same accessor shape as the others:InstrumentNameEvent::text_bytes()for the raw payload,text_lossy()for aCow<str>UTF-8 decode withU+FFFDsubstitutes for invalid sequences. Round 202 closes the text-meta family with two more helpers:SmfFile::texts()(FF 01general / free-form text) andSmfFile::copyrights()(FF 02copyright notice), each surfacing the matching event kind as aTextEvent/CopyrightEvent { tick, track, text }value pinned to the absolute tick on the parent track and stably merged across tracks under the same track-0-before-track-1-at-the-same-tick rule as the prior six text-meta helpers and the scheduler.FF 01is the catch-all annotation kind (production notes, "do not edit", version stamps);FF 02declares the sequence's copyright notice (the SMF specification recommends placing it on the first track at tick 0 but the helper surfaces every occurrence in time order). Same accessor shape as the rest of the family:TextEvent::text_bytes()/CopyrightEvent::text_bytes()for the raw payload (encoding is spec-unspecified),text_lossy()for aCow<str>UTF-8 decode withU+FFFDsubstitutes for invalid sequences. Lifts the family from 8 to 10 helpers (SmfFile::{tempo_map,time_signatures,key_signatures,markers, lyrics,cue_points,track_names,instrument_names,texts,copyrights}), covering everyFF 01..=07text-flavour meta event the spec defines. Round 208 adds the SMPTE wall-clock companionSmfFile::smpte_offsets()— everyFF 54 05 hr mn se fr ffSMPTE Offset meta event as anSmpteOffsetEvent { tick, track, hours_raw, minutes, seconds, frames, subframes }with the absolute tick on the parent track, stably merged across tracks (track 0 before track 1 at the same tick) under the same merge rule as the ten text-meta / rhythmic helpers and the scheduler. Thehrbyte packs the SMPTE frame rate in bits 5-6 per the MIDI Time Code spec (RP-004/008 §"HOURS COUNT"):00=24fps,01=25fps,10=30fps drop-frame,11=30fps non-drop. A newFrameRateenum (Fps24/Fps25/Fps30DropFrame/Fps30NonDrop) plusFrameRate::from_hours_byte(hr)decodes the packed bits;SmpteOffsetEvent::frame_rate()/hours_count()(bits 0-4) /seconds_total()(wall-clock seconds:h*3600 + m*60 + s + (frames + subframes/100)/fps) surface the SMPTE-cueing semantics without forcing callers to re-mask the byte themselves. Lifts the SMF meta-event iterator family from 10 to 11 total (SmfFile::{tempo_map,time_signatures,key_signatures,markers, lyrics,cue_points,track_names,instrument_names,texts,copyrights, smpte_offsets}), covering every rhythmic + text + cueing meta event the spec defines a per-event "when it fires" lens for. Round 219 closes the meta-event helper family with the sequencer-private companionSmfFile::sequencer_specifics()— everyFF 7F len dataSequencer-Specific Meta-Event as aSequencerSpecificEvent { tick, track, data }with the absolute tick on the parent track, stably merged across tracks (track 0 before track 1 at the same tick) under the same merge rule as every existing iteration helper and the scheduler.FF 7Fis the SMF escape hatch for sequencer-private or manufacturer-private payloads carried inline with the music data: the spec leaves the payload bytes opaque (by SysEx conventiondata[0]— ordata[0..=2]whendata[0] == 0x00— holds the manufacturer ID), and the parser preserves them verbatim so a caller routing by ID can decode while a generic player can ignore per the spec's "unknown meta events SHOULD be ignored" rule. OnlyFF 7Fis selected — channel-messageF0/F7SysEx events travel through the scheduler's SysEx pump rather than the meta-event family, so a DAW round-trip workflow (load → save) can preserve every private blob without re-reading the wire-event stream. Empty payloads (FF 7F 00) are surfaced asdata.is_empty()rather than filtered out — the spec permits a zero-length blob.SequencerSpecificEvent::data_bytes()borrows the raw payload. Lifts the SMF meta-event iterator family from 11 to 12 total (SmfFile::{tempo_map,time_signatures,key_signatures,markers, lyrics,cue_points,track_names,instrument_names,texts,copyrights, smpte_offsets,sequencer_specifics}), covering every rhythmic + text + cueing + sequencer-private meta event the spec defines a per-event "when it fires" lens for. Round 224 adds the pattern-cueing companionSmfFile::sequence_numbers()— everyFF 00 02 ssssSequence Number Meta-Event as aSequenceNumberEvent { tick, track, number }with the absolute tick on the parent track, stably merged across tracks (track 0 before track 1 at the same tick) under the same merge rule as every existing iteration helper and the scheduler.ssssis decoded big-endian into au16. The Standard MIDI File Specification 1.0 reserves the event for delta-time zero (the first event of a track) — on a format-2 file it labels each pattern so the sequence can be cued from a Song Select, and on a format-1 / format-0 file it labels the file as a whole — but the helper surfaces every occurrence rather than enforcing the placement rule so files that carry the label later in a track still round-trip.SequenceNumberEvent::number()returns the decoded identifier. Lifts the SMF meta-event iterator family from 12 to 13 total (SmfFile::{tempo_map,time_signatures,key_signatures,markers, lyrics,cue_points,track_names,instrument_names,texts,copyrights, smpte_offsets,sequencer_specifics,sequence_numbers}). Round 230 adds the multi-port-routing companionSmfFile::midi_ports()— everyFF 21 01 ppMIDI Port Meta-Event as aMidiPortEvent { tick, track, port }with the absolute tick on the parent track, stably merged across tracks (track 0 before track 1 at the same tick) under the same merge rule as every existing iteration helper and the scheduler.ppis the physical port byte (0..=127); the Standard MIDI File Specification 1.0 leaves the mapping from port index to physical output up to the receiving application — typically0is the first output port,1the second, and so on. The pre-multi-port convention places oneFF 21near the start of a track (delta zero, before the first channel-voice event) so a multi-port back-end can dispatch each track's channel stream through 16 × N channels, but the helper surfaces every occurrence rather than enforcing the placement rule so files that re-route mid-track still round-trip.MidiPortEvent::port()returns the decoded byte. OnlyFF 21is selected — the neighbouringFF 20channel-prefix hint stays on its own (different routing semantics: per-message channel override versus per-track physical port assignment) so the port-routing layer gets a clean time-ordered list independent of the surrounding meta streams. Lifts the SMF meta-event iterator family from 13 to 14 total (SmfFile::{tempo_map,time_signatures,key_signatures,markers, lyrics,cue_points,track_names,instrument_names,texts,copyrights, smpte_offsets,sequencer_specifics,sequence_numbers,midi_ports}). Round 240 adds the channel-binding companionSmfFile::channel_prefixes()— everyFF 20 01 ccChannel Prefix Meta-Event as aChannelPrefixEvent { tick, track, channel }with the absolute tick on the parent track, stably merged across tracks (track 0 before track 1 at the same tick) under the same merge rule as every existing iteration helper and the scheduler.cccarries the channel-binding hint for non-channel events that follow on the same track (text, lyric, marker, cue point, sysex — until anotherFF 20, the next channel-voice event, or end of track). The Standard MIDI File Specification 1.0 lists the event as part of the meta-event vocabulary; modern authoring tools prefer explicit per-track channel-voice streams plusFF 21port hints, but older files still emit it and a round-trip workflow must preserve it.ChannelPrefixEvent::channel()returns the spec-clamped channel asOption<u8>(Some(c)forc < 16,Nonefor an out-of-spec high-nibble byte — a receiver should fall back to the most recent channel-voice channel rather than silently mask). The raw byte stays on thechannelfield so files with out-of-spec values still round-trip. OnlyFF 20is selected — the neighbouringFF 21port-hint sibling stays on its own (different routing semantics: per-track physical port assignment versus per-message channel override) so the channel-binding layer gets a clean time-ordered list independent of the surrounding meta streams. Lifts the SMF meta-event iterator family from 14 to 15 total (SmfFile::{tempo_map,time_signatures,key_signatures,markers, lyrics,cue_points,track_names,instrument_names,texts,copyrights, smpte_offsets,sequencer_specifics,sequence_numbers,midi_ports, channel_prefixes}). Round 243 surfaces the SysEx wire-event channel alongside the 15-helper meta-event family withSmfFile::sysex_events() -> Vec<SysExEvent>: every System Exclusive event — both theF0start and theF7continuation / escape flavours — as aSysExEvent { tick, track, is_escape, data }with the absolute tick on the parent track, stably merged across tracks (track 0 before track 1 at the same tick) under the same merge rule as every existing iteration helper and the scheduler. SMF spec §"System Exclusive Events" definesF0 <varlen> <payload>(a complete-or-starting message; the trailingF7, when present, is included as the final byte of<payload>) andF7 <varlen> <payload>(a continuation packet for a previously-startedF0or an arbitrary escape sequence shipped verbatim to the wire). Both forms surface through the helper;datais reproduced verbatim so a writer can round-trip the output throughto_bytes(). Convenience accessors:SysExEvent::ends_with_eox()flags a payload terminating with0xF7;is_complete_message()is sugar for!is_escape && ends_with_eox()(the common universal-SysEx case — GM-onF0 7E 7F 09 01 F7, Master Volume, Master Tuning);manufacturer_id()returns the leading byte of anF0payload (0x7Enon-real-time /0x7Freal-time for the universal vocabulary, otherwise a vendor ID — expanded0x00- prefixed three-byte IDs surface asSome(0x00)and the caller inspectsdata[0..=2]) andNoneforF7or an empty payload. TheFF 7FSequencerSpecificchannel — surfaced throughsequencer_specifics()— is not selected here; the two carry different semantics (SysEx travels to the MIDI wire;FF 7Fis file-private metadata that does not) so a file may carry both anF0 7E 7F 09 01 F7Universal Non-Real-Time GM-On packet and a privateFF 7Fplugin-state blob alongside it. Round 246 adds the Table 4 vocabulary decoder atop the sameSysExEventchannel:SysExEvent::universal_classification() -> Option<UniversalSysEx>reads the<realm> <device_id> <sub_id1> [<sub_id2>]shape of a Universal SysExF0packet against Table 4 of the MIDI 1.0 Universal System Exclusive Messages document and returns aUniversalSysEx { realm, device_id, sub_id1 }naming the parsed category. Therealmfield distinguishes Non-Real-Time (0x7E) versus Real-Time (0x7F); thesub_id1field is aUniversalSubId1enum that names every category in the table — Sample Dump Header / Data / Request, MIDI Time Code (Non-RT Setup + RT Quarter-Frame families), Sample Dump Extensions, General Information (Identity Request / Reply), File Dump, MIDI Tuning Standard, General MIDI 1 / GM Off / GM 2 System On, Downloadable Sounds (DLS On / Off / Voice Allocation On / Off), File Reference Message, MIDI Visual Control, MIDI Capability Inquiry, MIDI Show Control, Notation Information (Bar Number, Time Signature Immediate / Delayed), Device Control (Master Volume / Balance / Fine Tuning / Coarse Tuning / Global Parameter Control), Real-Time MTC Cueing, Controller Destination Setting (Channel Pressure / Polyphonic Key Pressure / Control Change), Key-Based Instrument Control, Scalable Polyphony MIP, Mobile Phone Control — plus the five Non-Real-Time singletons (End of File, Wait, Cancel, NAK, ACK). The classifier is realm-aware: the same(sub_id1, sub_id2)byte pair names different messages in the two realms (e.g.0x09 0x01isGeneralMidi1SystemOnin Non-Real-Time andControllerDestinationChannelPressurein Real-Time, per Table 4); the decoder resolves the realm-dependent semantics before matching. Sub-ID #1 / Sub-ID #2 values outside the Table 4 vocabulary surface throughUniversalSubId1::Other(raw)/UniversalSubId2::Other(raw)so callers with deeper or more recent vocabulary can route the packet through a fallback path. Round 251 lifts the same Table-4 classifier to a file-wide iteration helper withSmfFile::universal_sysex_events() -> Vec<UniversalSysExEvent>: everyF0 7E …/F0 7F …Universal SysEx packet on every track, pinned to its absolute tick, with the classification eagerly resolved into aUniversalSysExEvent { tick, track, classification, data }. Manufacturer-prefixedF0packets (Roland0x41, Yamaha0x43, any leading byte other than0x7E/0x7F) andF7continuation / escape packets are filtered out — callers reading those route throughSmfFile::sysex_events()andSysExEvent::manufacturer_id()directly.F0packets truncated before Sub-ID #1 (payload shorter than 3 bytes) are also filtered, matching the underlying classifier's contract. Per-track sequences are stably merged by absolute tick — track 0's universal packets fire before track 1's at the same tick — the same convention used bysysex_events()and every meta-event helper. The verbatim payload bytes (leading<realm>byte through trailingF7, when present) are preserved onUniversalSysExEvent::dataso callers reading Sub-ID #2-derived arguments (Master Volume's 14-bit value, MTC Full Message'shr / mn / se / frquartet, MTS Single Note Tuning's note + tuning triple, …) don't have to re-walksysex_events()alongside the typed list. Round 254 adds the channel-voice patch-change companionSmfFile::program_changes() -> Vec<ProgramChangeEvent>: everyCn ppProgram Change on every track, pinned to the absolute tick at which it fires, with the channel index and program byte surfaced as aProgramChangeEvent { tick, track, channel, program }. The status nibble's low four bits decode to the spec's0..=15channel index (channel "1" in human-facing tools is index0); the single data byteppis the0..=127patch number. Resolution against a patch list (General MIDI 1 / 2, GS, XG, …) is left to the receiving application — the helper stays bank-agnostic and surfaces the raw program byte so callers driving an instrument-list view pick their own bank-select policy. Per-track sequences are stably merged by absolute tick — track 0's events fire before track 1's at the same tick — the same convention used by every meta-event and SysEx helper. Companion to the wire-state primitivechannel_snapshot_atwhich folds the last Program Change per channel intoSmfChannelSnapshot::programfor seek initialisation: where the snapshot answers "what patch is this channel on at tick T?",program_changes()answers "give me every patch change in song order" — a DAW track-inspector view highlighting the bar each instrument enters reads the latter. Round 260 extends the channel-voice typed-iterator family withSmfFile::control_changes() -> Vec<ControlChangeEvent>: everyBn cc vvControl Change on every track, pinned to the absolute tick at which it fires, with the channel index plus the rawcc/vvdata bytes surfaced as aControlChangeEvent { tick, track, channel, controller, value }. The status nibble's low four bits decode to the spec's0..=15channel index; the first data byteccis the controller number selected from the MIDI 1.0 Control Change Messages — Data Bytes table (Bank Select MSB / LSB0/32, Modulation1, Volume7, Pan10, Expression11, Data Entry6/38, RPN MSB / LSB100/101, NRPN MSB / LSB98/99, Sustain64, …); the second data bytevvis the raw value. The channel-mode family (controllerin120..=127: All Sound Off / Reset All Controllers / Local Control / All Notes Off / Omni Off / Omni On / Mono Mode On / Poly Mode On) is not diverted to a separate surface — those events ride the sameBnlane, and theControlChangeEvent::is_channel_mode()predicate gives callers a one-line route into reset detection without re-checking the controller range. Resolution against a controller vocabulary (the 14-bit MSB / LSB pairing for controllers0..=31plus32..=63, the on-off thresholdvalue >= 64for switch controllers, the Data Entry pump that drives RPN / NRPN parameter writes) is left to the receiving application — the helper stays controller-agnostic and surfaces the raw value byte so callers building a DAW lane editor / a CC-1 modulation-curve renderer / a CC-7 / CC-11 volume-expression curve view / an RPN-NRPN pair reassembler pick their own controller-vocabulary policy. Per-track sequences are stably merged by absolute tick — track 0's events fire before track 1's at the same tick — the same convention used by every meta-event and SysEx helper. Companion to the wire-state primitivechannel_snapshot_atwhich folds the last value of the six snapshot-tracked controllers (Bank MSB / LSB, Modulation, Volume, Pan, Expression, Sustain) into the snapshot for seek initialisation: where the snapshot answers "what value is CC-7 / CC-10 / … at tick T?",control_changes()answers "give me every controller change in song order, including the controllers the snapshot doesn't track" — a DAW automation lane reads the latter. Round 267 extends the channel-voice typed-iterator family withSmfFile::pitch_bends() -> Vec<PitchBendEvent>: everyEn lsb msbPitch Bend on every track, pinned to the absolute tick at which it fires, with the channel index plus the assembled 14-bit value surfaced as aPitchBendEvent { tick, track, channel, value }. The status nibble's low four bits decode to the spec's0..=15channel index; the two data bytes combine into(msb << 7) | lsb,0..=0x3FFF, with the no-bend centre at0x2000(the parser assembles the value at decode time).PitchBendEvent::signed_value()returns the displacement from centre as a signed-8192..=8191(0x2000→0,0x0000→-8192,0x3FFF→8191);is_centre()flags the no-bend position for bend-lane collapse or wheel-release detection. Resolving the 14-bit code to an actual pitch displacement requires the channel's Pitch Bend Sensitivity (RPN 0, default ±2 semitones), left to the receiving application — the helper stays sensitivity-agnostic and surfaces the raw code. Per-track sequences are stably merged by absolute tick — track 0's events fire before track 1's at the same tick — the same convention used by every meta-event, SysEx, and channel-voice helper. Companion to the wire-state primitivechannel_snapshot_atwhich folds the last pitch bend per channel intoSmfChannelSnapshot::pitch_bendfor seek initialisation: where the snapshot answers "what is the wheel position on channel N at tick T?",pitch_bends()answers "give me every bend in song order" — a DAW bend-lane editor reads the latter. Round 275 extends the channel-voice typed-iterator family withSmfFile::channel_pressures() -> Vec<ChannelPressureEvent>: everyDn ppChannel Pressure (mono aftertouch) on every track, pinned to the absolute tick at which it fires, with the channel index plus the single pressure byte surfaced as aChannelPressureEvent { tick, track, channel, pressure }. Per the MIDI 1.0 Summary of MIDI Messages Table 1, status nibble1101carries one data bytepp(0..=127) holding "the single greatest pressure value (of all the current depressed keys)" — distinct from polyphonic key pressure (An, per-key), which keeps its own surface. The status nibble's low four bits decode to the spec's0..=15channel index;ChannelPressureEvent::channel()/pressure()surface the decoded fields. The pressure value's musical effect (typically routed to volume, vibrato depth, or filter cutoff by the receiving instrument) is the receiver's concern, so the helper stays routing-agnostic and surfaces the raw0..=127byte. Per-track sequences are stably merged by absolute tick — track 0's events fire before track 1's at the same tick — the same convention used by every meta-event, SysEx, and channel-voice helper. A round-trip throughto_bytes()/parsepreserves the stream byte-for-byte. Round 292 adds the piano-roll companionSmfFile::notes() -> Vec<Note>: where every prior channel-voice helper surfaces one value per wire event,notes()pairs each Note On (9n key vel,vel > 0) with the Note Off that releases it and returns oneNote { start_tick, end_tick, track, channel, key, velocity, off_velocity }span per sounding note, ordered by onset — the primitive a DAW note-lane / piano-roll view consumes directly without re-deriving on/off pairing. The MIDI 1.0 Summary of MIDI Messages Table 1 velocity-0 convention is honoured: a9n key 0is the running-status Note-Off form and closes the earliest open note of that pitch (FIFO) withoff_velocity == 0, while an explicit8n key off_velcarries its release velocity through toNote::off_velocity. Matching walks the globally merged event stream sorted by(absolute tick, track, in-track position)— the same stable-merge convention every meta / SysEx / channel-voice helper and the scheduler use — so a Note Off on a different track from its Note On still pairs; overlapping notes of the same(channel, key)are matched FIFO, and an unmatched release or a hanging onset (no Note Off before EOF) is dropped from the span list.Note::duration_ticks()returnsend_tick - start_tick(always non-negative, possibly zero when an off lands on the onset tick);channel()/key()/velocity()/off_velocity()surface the decoded fields. Round 301 completes the per-message channel-voice typed-iterator family withSmfFile::poly_aftertouches() -> Vec<PolyAftertouchEvent>: everyAn kk ppPolyphonic Key Pressure (per-key aftertouch) event as aPolyAftertouchEvent { tick, track, channel, key, pressure }with the absolute tick on the parent track, stably merged across tracks (track 0 before track 1 at the same tick) under the same merge rule as every existing iteration helper and the scheduler.Anis the only channel-voice status without a dedicated extraction helper before this round; it is distinct from Channel Pressure (Dn, surfaced bychannel_pressures()— the single greatest pressure over all depressed keys) in thatAncarries a per-keykkbyte so per-voice aftertouch can be rebuilt. OnlyAnis selected — Control Change (Bn), Program Change (Cn), pitch-bend (En), channel-pressure (Dn), and note (8n/9n) events stay on their own surfaces. Accessorschannel()/key()/pressure()return the decoded0..=15/0..=127/0..=127fields. With this the eight channel-voice typed iterators (program_changes,control_changes,pitch_bends,channel_pressures,poly_aftertouches,notes, plussysex_events/universal_sysex_events) cover every status nibble the wire defines. Round 307 adds the sounding-note seek lensSmfFile::active_notes_at(tick) -> Vec<Note>— the piano-roll companion tonotes()and the note-level analogue of the channel-statechannel_snapshot_atprimitive. Where the snapshot answers "what controller / program / bend state does a channel carry at tick T?",active_notes_atanswers "which keys are held down at tick T?" — exactly the set a DAW must re-trigger (or a renderer must prime into the voice pool) when seeking into the middle of a file rather than playing from the top. A note is sounding whenstart_tick <= tick && end_tick > tick, the half-open interval[start_tick, end_tick): the onset tick is inclusive (the lens reflects state immediately after that tick's events fire, the same convention aschannel_snapshot_at), the release tick is exclusive (the key has come up), and a zero-duration note (start_tick == end_tick) is sounding at no tick. The result reuses the matched spans fromnotes()verbatim — same velocity-0 Note-Off convention, FIFO re-strike matching, and drop of hanging onsets / unmatched releases — and preserves thenotes()(start_tick, track)order so chord notes stay grouped and track 0 precedes track 1. Round 234 closes the SMF read-vs-write asymmetry: the parser has always materialised the full event vocabulary (Channel,Sysex,Meta), and round 234 adds the matching writer.SmfFile::to_bytes(&self) -> Result<Vec<u8>>serialises a parsed file back to a complete SMF byte stream (MThd+ oneMTrkperTrack) suitable to hand back toparsefor a structural round-trip;Track::to_bytes_chunk(&self) -> Result<Vec<u8>>emits one self-containedMTrkchunk so callers building a multi-track file from independent track sources can splice the chunks under a singleMThdheader. Output uses explicit status bytes throughout — the SMF specification permits but does not require running-status compression on the wire, and the explicit form keeps the writer deterministic regardless of internal track ordering. Every channel-voice variant, every concreteMetaEventvariant (including passthroughUnknownfor forward compatibility), and both sysex forms (F0start /F7continuation-escape) round-trip byte-for-byte throughto_bytes->parse. The writer surfacesError::InvalidDataat encode time for any value that cannot fit the wire format: a new public constantMAX_VLQ_VALUE = 0x0FFF_FFFFmatches the parser's 4-byte VLQ cap (delta-times, meta payload lengths, sysex payload lengths); data bytes must have the MIDI status bit clear (<= 0x7F); pitch-bend values must fit 14 bits; tempo values must fit 24 bits;KeySignature.modemust be0or1; SMPTEframes_per_secondmust be one of{24, 25, 29, 30};TicksPerQuartermust be1..=0x7FFF;Text.kindmust be0x01..=0x0F;header.ntrksmust matchtracks.len(); each track must end with exactly oneMetaEvent::EndOfTrackas its final event (the writer never auto-appends — the scheduler keys final-tempo / final-CC events off the EOT tick, so silently moving it would change semantics). The 17 new writer tests cover spec-VLQ encoding (10 worked examples from §"Variable-Length Quantities"), every meta variant, every channel-voice variant, both sysex forms, a multi-track Format-1 file, the long-VLQ (4-byte) delta branch, and every validation rejection. Round 213 lifts the SMF-file accessor surface beyond the "iterate every meta event" lens with a channel-state snapshot primitive for seeking:SmfChannelSnapshot { program, bank_msb, bank_lsb, volume, pan, expression, modulation, sustain, pitch_bend }plusSmfFile::channel_snapshot_at(channel, tick)/SmfFile::channel_snapshots_at(tick) -> [SmfChannelSnapshot; 16]. Each method replays every channel-voice event from every track up to and including the requested tick — in scheduler order (a stable merge by(tick, track, in-track), track 0 winning over track 1 at the same tick, the same convention as every existing iteration helper andscheduler.rs§"merged event list, sorted by absolute tick") — and folds each Program Change, Pitch Bend, and CC 0 / 1 / 7 / 10 / 11 / 32 / 64 into the snapshot. Fields that no matching event has touched stay at the SMF + GM 1 (RP-003) recommended defaults: volume 100, pan 64, expression 127, modulation 0, sustain off, pitch_bend0x2000; program / bank stayNoneso a seek-time initialiser can skip emitting CCs the file never wrote. Notes / aftertouch are ignored — they affect voice state, not channel state — so the snapshot is cheap to compute for any tick without enumerating sounding notes. Events at exactlytickare included (the snapshot reflects state immediately after that tick fires). Channels>= 16fall through to the default snapshot rather than panic. Sustain pedal decodes the CC 64 value with the spec threshold (value >= 64= on). The fold operation is also exposed publicly asSmfChannelSnapshot::apply(&ChannelBody)so callers running custom replay (e.g. against a custom track ordering) can reuse the same wire semantics. The bulk accessor pools events into one pass so initialising every channel at a seek target is single-pass rather than 16-pass — the natural primitive for a DAW seeking into the middle of a file.paths— per-OS SoundFont/SFZ/DLS search paths plus theOXIDEAV_SOUNDFONT_PATHenv-var override.instruments::sf2— full SoundFont 2 RIFF reader and voice generator. WalksRIFF/sfbk→LIST INFO/LIST sdta(smpl + optional sm24) /LIST pdta(phdr / pbag / pgen / inst / ibag / igen / shdr); cross-resolves the preset → instrument → zone → sample chain. Honours thekeyRange/velRangefilters; thesampleID/instrument/sampleModes/*Tune/overridingRootKeygenerators; the volume DAHDSR envelope (gens 33-38) andinitialAttenuation(gen 48); the modulation DAHDSR envelope (gens 25-30) routed into pitch (gen 7) and filter cutoff (gen 11); the initial low-pass biquad filter (gens 8/9); and the exclusive-class drum cut (gen 57). PCM storage is signed 24-bit (i32) —sm24lower bytes are combined withsmpl's 16-bit upper bytes when present, otherwise the 16-bit value is widened. Stereo zones (LEFT/RIGHTsample_type+ cross-linkedsample_link) render natively in stereo, bypassing the mixer's mono-pan law. Chunk lengths and array indices are bounds-checked against the loaded data; total samples capped at 256 Mi frames, total pdta records capped at 16 Mi, so malformed files cannot allocate beyond the spec ceiling.instruments::sfz— text patch reader plus voice generator. Tokenises SFZ syntax (line + block comments, headers, opcodename=valuepairs with space-bearing values), walks<control>/<global>/<master>/<group>/<region>sections, flattens inheritance into one fully-resolved opcode map per region, and (viaSfzInstrument::open) reads every referenced sample off disk against the SFZ file's directory + the activedefault_path. Strongly-typed fields:lokey/hikey/lovel/hivel,pitch_keycenter,key(sets all three),loop_start/loop_end/loop_mode,tune/transpose,volume,pan,trigger. Note names (C4,c#4,Db5) parse alongside decimal MIDI keys. Voice generation decodes the WAV sample bytes (8/16/24/32-bit PCM and IEEE_FLOAT) into mono f32, picks the matching region by (key, velocity), shifts pitch offpitch_keycenter+tune+transpose, applies a DAHDSR amplitude envelope fromampeg_*opcodes, runs a vibrato LFO fromlfo01_freq/lfo01_pitch/lfo01_delay, and (round 95) drives a filter envelope fromfileg_*opcodes through afil_type-aware biquad —lpf_1p/hpf_1p/lpf_2p(default) /hpf_2p/bpf_2p/brf_2pper the SFZ-legacyfil_typetable, withcutoff=(Hz → SF2 absolute cents) andresonance=(dB → centibels) feeding the round-91 RBJ biquad andfileg_depthdriving the EG2 → cutoff routing.#includeis rejected withError::Unsupported;#defineis preserved verbatim.instruments::dls— DLS Level 1 + 2 RIFF reader plus voice generator with articulation interpretation (round 80) and EG2 + 2-pole resonant low-pass filter wiring (round 91). Walks theRIFF/DLSform (colh/vers/ptbl/lins-list/wvpl-list/INFO-list), surfaces the parsed bank with instrument → region → wave-pool topology,wsmploop / pitch / gain headers,wlnkcue-table references, andart1/art2articulation connection blocks. Voice generation picks the matching instrument by MIDI program, picks a region by (key, velocity), resolves thewlnk→ptbl→ wave-pool entry, decodes the PCM (8/16-bit WAV-shaped) into mono f32, shifts pitch off thewsmp.unity_note, evaluates the region + instrument articulation throughinstruments::articulation::Articulation::evaluate, and plays the sample through the shared sample-playback voice with the resolved DAHDSR envelope + vibrato LFO + tuning + gain + the modulation envelope (EG2) routed into a 2-pole resonant low-pass filter cutoff (round 91). Loop modes: forward loop (WLOOP_TYPE_FORWARD, DLS1) and release loop (WLOOP_TYPE_RELEASE, DLS2).instruments::articulation— DLS Level 1/2 connection-block evaluator backed by MMA DLS1 v1.1b Tables 1–2 + MMA DLS2.2 v1.0 Amendment 2 Tables 5–10. Named constants for everyCONN_SRC_*/CONN_DST_*/CONN_TRN_*enum + theABSOLUTE_ZEROsentinel. SupportedSRC_NONE → DST_xdefaults: Vol EG DAHDSR (delay / attack / hold / decay / sustain / release), Mod EG DAHDSR (raw — surfaced for a later round), modulator + vibrato LFO frequency + start delay, filter cutoff + Q, tuning, gain, pan. Supported modulator routings:SRC_LFO → DST_PITCH(vibrato on DLS1),SRC_LFO → DST_GAIN(tremolo),SRC_VIBRATO → DST_PITCH(dedicated DLS2 vibrato — wins over the LFO routing),SRC_EG2 → DST_PITCH+SRC_EG2 → DST_FILTER_CUTOFF(mod-env, raw),SRC_KEYONVELOCITY → DST_EG1_ATTACKTIME(raw). Unit conversions: time-cents → seconds (clamped at 60 s), absolute-pitch → cents (clamped at ±14 400), absolute-pitch → Hz for LFO frequency (clamped at 50 Hz), gain → linear (clamped at -96..+48 dB), sustain-percent → 0..=1, pan-percent → ±50. Region blocks override instrument-level blocks per spec; an emptylartlist falls back to SamplePlayer defaults so banks with no articulation are byte-identical to round-75 output.instruments::tone— pure-tone fallback (sine / triangle / saw / square) so the synth produces something even with no on-disk bank.mixer— polyphonic voice pool (32 voices) with stereo mixdown, per-channel volume / pan / sustain pedal handling, oldest-voice preemption when the pool is full, channel/poly aftertouch routed to per-voice pressure, RPN 0 (pitch-bend range) handling, and exclusive-class drum cuts. Native stereo voices (SF2 stereo zones) are rendered through their own L/R buses, bypassing the mono-pan law. Round 75 adds: RPN 1 (channel fine tune, ±100 c) / RPN 2 (channel coarse tune, ±63 semis) / RPN 5 (modulation depth range, CA-26) / RPN 6 (MPE Configuration Message — see below). Round 102 adds Data Increment (CC 96) / Data Decrement (CC 97) per RP-018: the value byte is ignored and each message steps the RP-018-prescribed sub-field of the selected RPN by one — the LSB (cents) for RPN 0 / 1 / 5 (with RPN 0's LSB wrapping into the semitone MSB at 100, the borrow falling out of the combined base-100 cents store) and the MSB (one semitone) for RPN 2; RPN Null and unmodelled / NRPN selections are a no-op. CC 1 (mod wheel) routed to voices through the new [Voice::set_mod_depth_cents] hook; CC 74 (MPE "third dimension" / brightness) routed through [Voice::set_timbre]. Master state on the mixer adds Master Volume (Universal Real-Time SysEx7F 7F 04 01) applied as a global gain at mix-time, and Master Fine / Master Coarse Tuning (CA-25, sub-IDs04 03/04 04) summed with the per-channel fine + coarse tune to derive the effective pitch each voice receives. Drum channel (MIDI 10 = index 9) is exempt from tuning per CA-25. Round 105 adds Master Balance (Universal Real-Time SysEx7F 7F 04 02 lsb msb) per the M1 v4.2.1 Detailed Specification §"DEVICE CONTROL — MASTER VOLUME AND MASTER BALANCE" (p.57): 14-bit value with00 00 = hard left,7F 7F = hard right, centre =0x2000. Stored verbatim and folded into the mix-time per-side gains via [Mixer::master_balance_gains] using the textbook balance law (the far side attenuates while the near side stays at unity, so a stereo source panned hard one way mutes the opposite bus without boosting the near bus). Default0x2000produces the identity gains(1.0, 1.0), keeping the mix bit-identical to the pre-round-105 output until a SysEx moves balance off centre. GM 1 / GM 2 System On / GM System Off also reset Master Balance to centre. Round 114 adds the GM2 Global Parameter Control state (mixer::GmEffects, CA-024 Universal Real-Time SysEx04 05): the system-wide Reverb (slot0101) and Chorus (slot0102) parameters, decoded to engineering units via the CA-024 GM2 tables (Reverb Type / Timert = exp((val-40)·0.025)s; Chorus Type / Mod-Rateval·0.122Hz / Mod-Depth(val+1)/3.2ms / Feedbackval·0.763% / Send-to-Reverbval·0.787%) via [Mixer::set_gm_reverb_param] / [Mixer::set_gm_chorus_param]. Defaults are the GM2 recommended initial settings (Reverb Type 4 Large Hall, Chorus Type 2 Chorus 3); GM System On/Off resets them. The parameters are decoded and observable but not yet applied as a reverb/chorus DSP send — a later round can wire the effects bus without re-parsing the SysEx.mixer::MpeZone/mixer::MpeRole— MIDI Polyphonic Expression (M1-100-UM v1.1) support. The MCM (RPN 0x0006 on channel 0 for Lower, channel 15 for Upper) configures one or two zones; each zone's Manager Channel carries zone-wide CCs and its Member Channels host per-note Pitch Bend / Channel Pressure / CC 74. Per Appendix C the Member Channel pitch bend sums in cents with the Manager's bend before reaching the voice. Per §2.2.5 the receiver sets default PB Sensitivity to 2 semitones on the Manager and 48 semitones on every Member at MCM time. Per §2.2.7 Polyphonic Key Pressure on a Member is silently dropped. Per §2.2.3 a zone reconfiguration stops every Sounding Note on the affected channels and resets their controllers.scheduler— SMF event scheduler. Merges every track into a single time-ordered stream, converts ticks → samples against the current tempo + division (samples_per_tick = us_per_quarter * sample_rate / (1_000_000 * ticks_per_quarter)), and dispatches every event into the mixer at the right audio sample. Round 75 wires the Universal Real-Time / Non-Real-Time SysEx surface: GM 1 / GM 2 System On (sub-IDs09 01/09 03) reset all controllers + master tuning- master volume; GM System Off (
09 02) does the same; Master Volume (04 01), Master Fine Tuning (04 03) + Master Coarse Tuning (04 04) all route into the mixer's master-state setters. Round 105 routes Master Balance (04 02) intoMixer::set_master_balance_14. CC 1 / CC 74 are pumped into the new mixer hooks; the MPE Configuration Message (RPN 6 on the Lower / Upper Manager Channel) reaches the mixer via the existing RPN data-entry pipeline. Round 98 routes sub-ID#108(MIDI Tuning Standard) in both Universal areas: Single-Note Tuning Change (sub-ID#202+ bank form07) and Scale/Octave Tuning 1-byte (08) / 2-byte (09) forms into thetuningtable; GM System On/Off additionally reset MTS tuning to equal temperament. Round 102 routes CC 96 / CC 97 (Data Increment / Decrement, RP-018) intoMixer::data_inc_dec. Round 114 routes the Global Parameter Control message (04 05, CA-024): it parses the Slot Path Length / Parameter-ID Width / Value Width header, walks the GM2-reserved slot path (Slot Path Length 1, Slot MSB 1; Slot LSB01= Reverb,02= Chorus), and applies each parameter-value pair (MSB-first ID, LSB-first value) into the mixer's GM2 effect setters, ignoring unrecognised slots/parameters per the spec.
- master volume; GM System Off (
tuning— MIDI Tuning Standard (MTS) microtuning state + Universal SysEx data-format decoders, per the MMA MIDI Tuning Messages specification (CA-020 / CA-021 / RP-020). ATuningTableholds a global 128-entry key-based table (the current tuning program) and per-channel 12-entry scale/octave tables, both as signed cents added to a key's equal-tempered pitch (default = equal temperament everywhere, so untuned playback is byte-identical to the pre-MTS path). Decoders cover the 3-byte frequency word (semitone + fraction14/16384, with the reserved7F 7F 7F"no change" sentinel), the scale/octave 1-byte (00=-64c / 40=0c / 7F=+63c) and 2-byte (14-bit, ±100 c) offsets, and theff gg hhchannel bitmap (with the reservedffbits 2–6 ignored). The mixer folds the per-key offset into every voice-pitch composition; the real-time message forms retune sounding notes immediately while the non-real-time "setup" forms update only the stored table. Drum channel (MIDI 10) is exempt from retuning per CA-25's no-note-shifting rule.downloader— stub that names a planned default bank (TimGM6mb) but currently returnsError::Unsupported.
The decoder factory is registered under codec id "midi". Round-3
wires SMF events end-to-end: send_packet parses the SMF and primes
the scheduler; receive_frame returns interleaved S16 stereo PCM
(1024 samples per channel at 44 100 Hz) until both the event stream
and the voice pool have run dry. Without an on-disk bank the
registry-built decoder uses the pure-tone fallback; for SoundFont 2
playback build a MidiDecoder directly with an Sf2Instrument.
Coverage today (round 91): full SF2 voice with sm24 24-bit samples,
stereo zones, DAHDSR volume + modulation envelopes, low-pass biquad
filter (gens 8/9), modEnv→pitch / modEnv→filter routing (gens 7/11),
exclusive-class drum cuts (gen 57); pitch bend with RPN 0 / 1 / 2 /
5 / 6 (range, channel fine tune, channel coarse tune, modulation
depth range, MPE configuration); channel/poly aftertouch; SFZ voice
generator with DAHDSR amplitude envelope (ampeg_*) and vibrato
LFO (lfo01_freq / lfo01_pitch); DLS Level 1 + 2 voice
generator with art1/art2 connection-block interpretation
(round 80) — Vol EG DAHDSR, vibrato LFO, tuning, gain, pan, plus the
well-known SRC_LFO → DST_PITCH / SRC_VIBRATO → DST_PITCH /
SRC_LFO → DST_GAIN routings; round 91 lands EG2 + filter
rendering on the shared SamplePlayer — SRC_NONE → DST_FILTER_CUTOFF / DST_FILTER_Q initialise a per-voice 2-pole
resonant low-pass biquad (RBJ low-pass against the SF2 v2.04 §8.1.3
cents reference fc_hz = 8.176 * 2^(cents/1200)), and the
SRC_EG2 → DST_FILTER_CUTOFF routing sweeps the cutoff each output
frame from the EG2 DAHDSR envelope (every CONN_DST_EG2_*
destination interpreted at voice-build time). All three instrument
paths share one SamplePlayer voice for sample playback + DAHDSR
amplitude envelope + vibrato + pitch bend + EG2 + filter (the SF2
voice keeps its own parallel filter path for compatibility with
stereo zones + 24-bit sm24 samples; both biquads land the same
RBJ cookbook math against the SF2 §8.1.3 reference).
Round 75 also delivers the MIDI Polyphonic Expression (MPE) v1.1
control surface (M1-100-UM): MCM-driven Lower / Upper zone
configuration, per-note pitch bend / channel pressure / CC #74 on
Member Channels, Appendix-C combining of Member + Manager pitch
bend, §2.2.5 default 48-semi Member PB sensitivity, §2.2.7 drop of
Polyphonic Key Pressure on Member Channels, §2.2.3 sounding-note
reset on zone reconfiguration. Plus Universal Real-Time SysEx
Master Volume (F0 7F <dev> 04 01 lsb msb F7), Master Balance
(04 02), Master Fine / Master Coarse Tuning (CA-25, 04 03 /
04 04), GM2 Global Parameter Control (CA-024, 04 05 — Reverb
slot 0101 / Chorus slot 0102), and GM 1 / GM 2 System On / GM
System Off (Non-Real-Time, 09 01 / 09 02 / 09 03).
Fuzzing
Round 172 stands up a cargo-fuzz (libfuzzer-sys) harness over every
attacker-facing parser:
cargo +nightly fuzz run smf # smf::parse + tempo/time/key iterators
cargo +nightly fuzz run sf2 # instruments::sf2::Sf2Bank::parse
cargo +nightly fuzz run dls # instruments::dls::DlsBank::parse
cargo +nightly fuzz run sfz # instruments::sfz::parse_str
Each target asserts the contract every parser already advertises:
arbitrary bytes return a Result, with no panic / OOM / integer
overflow / out-of-bounds index on any path. Curated seed corpora
under fuzz/corpus/<target>/ give the fuzzer a head start across
the well-formed, partial-but-legal, and known-edge shapes. The
sfz corpus also keeps the round-172 regression input
(regression_r172_octave_overflow.sfz.bin) so the
parse_key("C-2011420400") overflow stays under perpetual
fuzzer pressure after the fix.
Initial 4×~45 s runs cleared 30+ million inputs total across smf /
sf2 / dls and 2 M across sfz with zero remaining crashes. The
single round-172 finding (overflow in the SFZ key-name octave
multiplication) was fixed in-place and pinned by a new
parse_key_octave_extremes_do_not_overflow unit test.
Profiling
Round 285 adds benches/synth_render.rs (harness = false) — a
repeatable SMF→PCM wall-clock harness over a dense 8-channel /
32-voice score (24 s of music, pitch-bend sweeps, volume + pan CCs)
rendered through an in-memory looping SF2 bank, plus a --corpus
mode that hashes (FNV-1a-64) the PCM produced for every in-tree
fixture SMF through both the SF2 bank and the tone fallback, and a
--spin SECS mode that loops the render as a stable sampling-profiler
target.
Profiling that harness put Sf2Voice::render at ~89 % of the wall
clock, and within it the per-sample DAHDSR volume-envelope stage walk
at ~31 % of the total (sample fetch + linear interpolation ~15 %).
The envelope is now evaluated in stage-segmented runs into a 256-entry
stack buffer (envelope_run): constant stages (delay / hold /
sustain) become slice fills and the ramp stages (attack / decay /
release) become element-wise loops with no loop-carried dependency,
which the compiler vectorises (NEON fdiv.4s on aarch64). Every
per-sample expression is kept verbatim from the scalar evaluator, so
the rendered PCM is bit-identical (corpus hashes unchanged; the
envelope_run_matches_envelope_at_per_sample unit test pins
to_bits() equality across all stage boundaries, the release tail,
and the elapsed-wrap fallback). Dense-score render time: 80.2 ms →
64.2 ms (-20 %) on an Apple-silicon dev box.