Skip to content

v0.0.4

Latest

Choose a tag to compare

@MagicalTux MagicalTux released this 15 Jun 05:11
· 4 commits to master since this release
c5caa30

Other

  • midi r307: SmfFile::active_notes_at() sounding-note seek lens
  • add SmfFile::poly_aftertouches() per-key aftertouch iterator
  • add SmfFile::notes() Note On/Off pairing into sounding-note spans
  • run-segmented volume-envelope evaluation — bit-identical, ~20% faster SMF→PCM synthesis
  • add SmfFile::channel_pressures() — Dn-pp mono-aftertouch iteration helper
  • SMF SmfFile::pitch_bends() — En-lsb-msb channel-voice pitch-bend iteration helper
  • SmfFile::control_changes() — Bn-cc-vv channel-voice continuous-controller / channel-mode iteration helper
  • SmfFile::program_changes() — Cn-pp channel-voice patch-select iteration helper
  • SmfFile::universal_sysex_events() — Table-4-classified file-wide iteration helper
  • SysExEvent::universal_classification() — Table 4 Universal SysEx classifier
  • SmfFile::sysex_events() iteration helper (F0 / F7)
  • SmfFile::channel_prefixes() iteration helper (FF 20 01 cc)
  • SmfFile::to_bytes() / Track::to_bytes_chunk() mux-side writer
  • SmfFile::midi_ports() iteration helper (FF 21 01 pp)
  • SmfFile::sequence_numbers() iteration helper (FF 00 02 ssss)
  • SmfFile::sequencer_specifics() iteration helper (FF 7F)
  • SmfFile::channel_snapshot_at / channel_snapshots_at (channel-state seek primitive)
  • SmfFile::smpte_offsets() iteration helper + FrameRate decoder (FF 54)
  • SmfFile::texts() + copyrights() iteration helpers (FF 01 + FF 02)
  • SmfFile::instrument_names() iteration helper (FF 04)

Round 307 — SmfFile::active_notes_at() sounding-note seek lens

  • New SmfFile::active_notes_at(tick) -> Vec<Note>: every Note span
    sounding at the absolute tick — the piano-roll / seek companion to
    notes(), and the note-level analogue of the channel-state
    channel_snapshot_at primitive. Where the snapshot answers "what
    controller / program / bend state does a channel carry at tick T?",
    this answers "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 when start_tick <= tick && end_tick > tick — the
    half-open interval [start_tick, end_tick). The onset tick is
    inclusive (the snapshot reflects state immediately after that tick's
    events fire, the same convention as channel_snapshot_at); the release
    tick is exclusive (the key has come up). A zero-duration note
    (start_tick == end_tick) is sounding at no tick. Hanging onsets and
    unmatched releases — already dropped by notes() — cannot be reported.
    The result preserves the notes() (start_tick, track) order so chord
    notes stay grouped and track 0 precedes track 1.
  • Seven new unit tests: empty-when-silent, half-open boundary
    (onset-inclusive / release-exclusive / mid-span / after-release),
    before-onset silence, zero-duration never sounds, chord returns all
    held keys in onset order, staggered overlap window (only-n1 / both /
    only-n2), and hanging-note never sounds. Full lib suite 550 → 557
    tests, zero ignored.

Round 301 — SmfFile::poly_aftertouches() Polyphonic Key Pressure stream

  • New SmfFile::poly_aftertouches() -> Vec<PolyAftertouchEvent>: every
    An kk pp Polyphonic Key Pressure (per-key aftertouch) channel-voice
    event as a PolyAftertouchEvent { tick, track, channel, key, pressure } pinned to the absolute tick on its parent track, stably merged
    across tracks (track 0 before track 1 at the same tick) under the same
    convention every existing iteration helper and the scheduler use.
  • An was the only channel-voice status nibble without a dedicated
    typed extraction helper. Distinct from Channel Pressure (Dn,
    surfaced by channel_pressures()) — An carries a per-key kk byte,
    so per-voice aftertouch automation can be rebuilt. Accessors
    channel() / key() / pressure() return the decoded fields.
  • Nine new unit tests: tick-zero decode, low-nibble channel index,
    running-status (key, pressure) pair chaining, late-position absolute
    tick, two-track stable sort, cross-track tick merge, filter exclusion
    of every other channel-voice kind, to_bytes()/parse() round trip,
    and empty-when-none. Full lib suite 541 → 550 tests, zero ignored.

Round 292 — SmfFile::notes() Note On / Note Off pairing into sounding-note spans

  • New SmfFile::notes() -> Vec<Note>: pairs every Note On
    (9n key vel, vel > 0) with the Note Off that releases it and
    returns one Note { start_tick, end_tick, track, channel, key, velocity, off_velocity } span per sounding note, ordered by onset.
    Where the channel-voice helpers (program_changes /
    control_changes / pitch_bends / channel_pressures) surface one
    value per wire event, this helper joins the two wire events that
    bracket a note into a single span carrying its duration — the
    primitive a piano-roll / DAW note-lane view consumes directly.
  • Honours the MIDI 1.0 Summary of MIDI Messages Table 1 velocity-0
    convention: a 9n key 0 is treated as a Note Off and closes the
    earliest open note of that pitch (FIFO), with off_velocity == 0.
    An explicit 8n key off_vel carries its release velocity through to
    Note::off_velocity.
  • Matches over the globally merged event stream sorted by
    (absolute tick, track, in-track position) — the same stable-merge
    convention every other iteration helper and the scheduler use — so a
    Note Off on a different track from its Note On still pairs correctly.
    Overlapping notes of the same (channel, key) are matched FIFO; an
    unmatched release or a hanging onset is dropped from the span list.
  • Note accessors: channel() / key() / velocity() /
    off_velocity() / duration_ticks() (end_tick - start_tick).
  • 13 new unit tests: single-note pairing, velocity-0 close, channel
    decode, FIFO overlap, chord (distinct pitches + cross-track
    ordering), cross-track off pairing, hanging-on / unmatched-off drop,
    zero-duration span, sibling-helper isolation, and a to_bytes() /
    parse structural round trip.

Round 285 — synthesis profiling + run-segmented volume envelope (bit-identical, ~20 % faster)

  • New benches/synth_render.rs (harness = false) — repeatable
    SMF→PCM wall-clock harness: dense 8-channel / 32-voice score with
    pitch-bend sweeps + volume/pan CCs rendered through an in-memory
    looping SF2 bank. --corpus hashes (FNV-1a-64) the PCM for every
    in-tree fixture SMF through both the SF2 bank and the tone
    fallback; --spin SECS loops the render as a sampling-profiler
    target; default mode prints per-iteration wall time + output hash.
  • Profiling ranked Sf2Voice::render at ~89 % of the synthesis wall
    clock; within it the per-sample DAHDSR volume-envelope stage walk
    (an Option test + up to four stage comparisons + an f32 divide,
    serialised behind the phase walk) at ~31 % of total, ahead of
    sample fetch + linear interpolation (~15 %).
  • Sf2Voice::render now evaluates the volume envelope in
    stage-segmented runs (envelope_run) into a 256-entry stack
    buffer: delay / hold / sustain become slice fills, attack / decay /
    release become element-wise loops with no loop-carried dependency
    that the compiler vectorises. Every per-sample expression is kept
    verbatim from envelope_at, so the rendered PCM is bit-identical —
    corpus hashes are unchanged and the new
    envelope_run_matches_envelope_at_per_sample test pins
    to_bits() equality across stage boundaries, the release tail,
    and the elapsed-wrap fallback. Dense-score render: 80.2 ms →
    64.2 ms (-20 %) on an Apple-silicon dev box.

Round 275 — SmfFile::channel_pressures() — Dn-pp channel-voice mono-aftertouch iteration helper

  • New SmfFile::channel_pressures(&self) -> Vec<ChannelPressureEvent>
    surfaces every Dn pp Channel Pressure (mono aftertouch)
    channel-voice event on every track, pinned to the absolute tick at
    which it fires, in time order. Each entry is a
    ChannelPressureEvent { tick, track, channel, pressure } with the
    status nibble's low four bits decoded into the spec's 0..=15
    channel index and the single data byte pp (0..=127) carrying
    "the single greatest pressure value (of all the current depressed
    keys)" per the MIDI 1.0 Summary of MIDI Messages Table 1.
  • The new ChannelPressureEvent struct exposes channel() /
    pressure() accessors. The helper stays routing-agnostic — the
    pressure value's musical effect (volume / vibrato depth / filter
    cutoff) is left to the receiving instrument.
  • Only Dn is selected; polyphonic key pressure (An, per-key) keeps
    its own surface, and the neighbouring CC (Bn) / program (Cn) /
    pitch-bend (En) / note (8n / 9n) channel-voice events stay
    isolated. Per-track sequences are stably merged by absolute tick
    (track 0 before track 1 at the same tick), the same convention as
    every meta-event, SysEx, and channel-voice helper.
  • 8 new unit tests cover tick-zero decode, channel-index nibble,
    running-status chains (single-data-byte status), late-position
    absolute tick, stable same-tick cross-track sort, cross-track tick
    merge, cross-kind filtering, and a to_bytes() / parse round trip.

Round 267 — SmfFile::pitch_bends() — En-lsb-msb channel-voice pitch-bend iteration helper

  • New SmfFile::pitch_bends(&self) -> Vec<PitchBendEvent> surfaces
    every En lsb msb Pitch Bend channel-voice event on every track,
    pinned to the absolute tick at which it fires, in time order. Each
    entry is a PitchBendEvent { tick, track, channel, value } with the
    status nibble's low four bits decoded into the spec's 0..=15
    channel index and the two data bytes combined into the 14-bit code
    (msb << 7) | lsb, 0..=0x3FFF, no-bend centre 0x2000 (the parser
    assembles the value at decode time).
  • The new PitchBendEvent struct carries the same tick / track
    pair the existing channel-voice iteration helpers use plus the
    decoded channel and the assembled 14-bit value. The channel()
    / value() accessors return the fields with mnemonic names; a
    signed_value() -> i16 accessor returns the displacement from centre
    in -8192..=8191 (value as i32 - 0x2000, so 0x20000,
    0x0000-8192, 0x3FFF8191); an is_centre() predicate
    flags the no-bend position for bend-lane collapse / 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),
    which is intentionally left to the receiving application: the helper
    stays sensitivity-agnostic and surfaces the raw code so callers pick
    their own bend-range 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 existing iteration helper (control_changes(),
    program_changes(), the SysEx and meta-event families, …) and the
    scheduler's merged event list.
  • Companion to the wire-state primitive SmfFile::channel_snapshot_at,
    which folds the last pitch bend per channel into
    SmfChannelSnapshot::pitch_bend at the seek point. 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" —
    the typed accessor a DAW bend-lane editor or a glissando / vibrato
    curve renderer reads.
  • 10 new tests cover the empty-input case, the centre value at tick
    zero, the lsb / msb 14-bit combine at both extremes (0x0000
    signed -8192, 0x3FFF signed +8191), channel-index decode across
    the 0..=15 range, running-status reuse on the En two-data-byte
    status (three chained bends sharing one status byte), late-position
    absolute-tick tracking, stable-sort merge of same-tick events across
    tracks, time-ordered merge of different-tick events across tracks,
    filtering against the other six channel-voice status kinds (8n,
    9n, An, Bn, Cn, Dn), a cross-check against
    channel_snapshot_at confirming the snapshot's pitch_bend field
    tracks the iterator at each change point and on the silence between
    changes, and a to_bytes() / parse() round trip confirming the
    bend stream survives the mux-side writer.

Round 260 — SmfFile::control_changes() — Bn-cc-vv channel-voice continuous-controller / channel-mode iteration helper

  • New SmfFile::control_changes(&self) -> Vec<ControlChangeEvent>
    surfaces every Bn cc vv Control Change channel-voice event on
    every track, pinned to the absolute tick at which it fires, in time
    order. Each entry is a ControlChangeEvent { tick, track, channel, controller, value } with the status nibble's low four bits decoded
    into the spec's 0..=15 channel index and both data bytes cc /
    vv surfaced as raw 0..=127 payloads.
  • The new ControlChangeEvent struct carries the same tick /
    track pair the existing iteration helpers use plus the decoded
    channel, controller, and value bytes. The
    channel() / controller() / value() accessor methods return the
    same fields with mnemonic names so call sites driving a
    controller-automation view read cc.controller() / cc.value()
    rather than the bare field access. A is_channel_mode() predicate
    flags the channel-mode family (controller in 120..=127) — All
    Sound Off (120), Reset All Controllers (121), Local Control
    (122), All Notes Off (123), Omni Mode Off / On (124 / 125),
    Mono / Poly Mode On (126 / 127) — so a player reset-detector
    can route on the predicate without re-checking the controller
    range manually.
  • Resolution against a controller vocabulary (the MIDI 1.0
    Control Change Messages — Data Bytes table's 14-bit MSB / LSB
    pairing for controllers 0..=31 plus 32..=63, the on-off
    threshold value >= 64 for switch controllers 64..=69, the
    CC-6 / CC-38 Data Entry pump that drives RPN / NRPN parameter writes
    selected through CC-100 / CC-101 RPN and CC-98 / CC-99 NRPN pairs)
    is intentionally left to the receiving application: the helper
    stays controller-agnostic and surfaces the raw value byte so
    callers 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 existing iteration helper (program_changes(),
    sysex_events(), universal_sysex_events(), the meta-event
    family, …) and the scheduler's merged event list.
  • Companion to the wire-state primitive SmfFile::channel_snapshot_at,
    which folds the last value of the six snapshot-tracked
    controllers (Bank MSB / LSB, Modulation, Volume, Pan, Expression,
    Sustain) into the snapshot at the seek point. 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"
    — the typed accessor a DAW lane-editor or a CC-1 modulation-curve
    renderer reads.
  • 12 new tests cover the empty-input case, single-controller decode,
    the full 0..=127 value range, channel-index decode across the
    0..=15 range, running-status reuse on the Bn two-data-byte
    status (three chained CCs sharing one status byte), late-position
    absolute-tick tracking, stable-sort merge of same-tick events
    across tracks, time-ordered merge of different-tick events across
    tracks, filtering against the other six channel-voice status kinds
    (8n, 9n, An, Cn, Dn, En), the channel-mode family
    (120..=127) flagged by is_channel_mode() alongside a continuous
    CC-7 surface that doesn't, a cross-check against
    channel_snapshot_at confirming the snapshot's volume field
    tracks the iterator at each change point and on the silence
    between changes, and a to_bytes() / parse() round trip
    confirming the CC stream survives the mux-side writer. Total
    lib-test count: 508 (up from 496).

Round 254 — SmfFile::program_changes() — Cn-pp channel-voice patch-select iteration helper

  • New SmfFile::program_changes(&self) -> Vec<ProgramChangeEvent>
    surfaces every Cn pp Program Change channel-voice event on every
    track, pinned to the absolute tick at which it fires, in time order.
    Each entry is a ProgramChangeEvent { tick, track, channel, program }
    with the status nibble's low four bits decoded into the spec's
    0..=15 channel index and the single data byte pp surfaced as the
    0..=127 patch number.
  • The new ProgramChangeEvent struct carries the same tick / track
    pair the existing iteration helpers use plus the decoded channel and
    raw program bytes. channel() and program() accessor methods
    return the same fields with mnemonic names so call sites driving an
    instrument-list view read pc.channel() / pc.program() rather than
    the bare field access.
  • Resolution against a patch list (General MIDI 1 / 2, GS, XG, …) is
    intentionally left to the receiving application: the helper stays
    bank-agnostic and surfaces the raw program byte so callers can pick
    their own bank-select (CC 0 / CC 32) 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 existing iteration helper
    (sysex_events(), universal_sysex_events(), the meta-event family,
    …) and the scheduler's merged event list.
  • Companion to the wire-state primitive SmfFile::channel_snapshot_at,
    which folds the last Program Change per channel into
    SmfChannelSnapshot::program for 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" — the typed accessor a DAW track-inspector view (highlighting
    the bar each instrument enters) reads.
  • 10 new tests cover the empty-input case, single-patch decode, the
    full 0..=127 program range, channel-index decode across the
    0..=15 range, running-status reuse on the Cn single-data-byte
    status, late-position absolute-tick tracking, stable-sort merge of
    same-tick events across tracks, time-ordered merge of different-tick
    events across tracks, filtering against the other six channel-voice
    status kinds (8n, 9n, An, Bn, Dn, En), and a cross-check
    against channel_snapshot_at confirming the snapshot's program
    field tracks the iterator at each change point and on the silence
    between changes. Total lib-test count: 496 (up from 486).

Round 251 — SmfFile::universal_sysex_events() — Table-4-classified file-wide iteration helper

  • New SmfFile::universal_sysex_events(&self) -> Vec<UniversalSysExEvent>
    surfaces every Universal System Exclusive packet on every track —
    F0 7E … (Non-Real-Time) and F0 7F … (Real-Time) only — pinned
    to the absolute tick at which it fires, in time order, with the
    Table-4 classification already resolved. Each entry is a
    UniversalSysExEvent { tick, track, classification, data }.
  • The new UniversalSysExEvent struct carries the same tick /
    track pair the existing iteration helpers use, the parsed
    UniversalSysEx { realm, device_id, sub_id1 } classification
    the round-246 per-event classifier returns, and the verbatim
    payload bytes from the wire (leading <realm> byte through
    trailing 0xF7 when present) so callers reading Sub-ID #2-
    derived arguments — Master Volume's 14-bit value, MTC Full
    Message's hr / mn / se / fr quartet, MTS Single Note Tuning's
    note + tuning triple, … — don't have to re-walk
    SmfFile::sysex_events() alongside the typed list.
  • Manufacturer-prefixed F0 packets (Roland 0x41, Yamaha 0x43,
    any leading byte other than 0x7E / 0x7F) and F7
    continuation / escape packets are filtered out — callers
    interested in those route through SmfFile::sysex_events() and
    SysExEvent::manufacturer_id() directly. F0 packets truncated
    before the Sub-ID #1 byte (payload shorter than 3 bytes) are also
    filtered, matching the contract of the underlying
    SysExEvent::universal_classification() classifier so the two
    views agree on which packets are universal.
  • 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 by SmfFile::sysex_events() /
    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()
    and scheduler.rs §"merged event list, sorted by absolute tick".
  • The helper re-uses SysExEvent::universal_classification() for the
    per-packet decode so any future tweak to the Table 4 vocabulary
    lands in exactly one place; a regression test walks sysex_events()
    in parallel and confirms the typed view stays byte-for-byte aligned
    with the per-event classifier across a mixed packet sample.
  • 7 new unit tests cover the empty-file case, the manufacturer-
    prefixed + escape filter, both-realm classification, cross-track
    stable merging, same-tick stable-sort ordering (track 0 wins),
    truncated-universal-packet drop, and the per-event classifier
    parity check.

Round 246 — SysExEvent::universal_classification() — Table 4 Universal SysEx classifier

  • New SysExEvent::universal_classification(&self) -> Option<UniversalSysEx>
    classifies a Universal System Exclusive packet against Table 4 of the
    MIDI 1.0 Universal System Exclusive Messages document, returning a
    UniversalSysEx { realm, device_id, sub_id1 } for every F0 packet
    whose leading byte is 0x7E (Universal Non-Real-Time) or 0x7F
    (Universal Real-Time). The Sub-ID #1 byte is parsed against the
    Table 4 vocabulary into a UniversalSubId1 enum that names every
    category in the document; categories that branch on Sub-ID #2 carry
    a UniversalSubId2 payload that names every Sub-ID #2 value the
    document defines for the category-realm pair. Sub-ID #1 / Sub-ID #2
    bytes outside the Table 4 vocabulary the classifier knows about
    surface through UniversalSubId1::Other(raw) /
    UniversalSubId2::Other(raw) so callers with deeper, more recent
    vocabulary can still route the packet.
  • The classifier is realm-aware: the same (sub_id1, sub_id2) byte
    pair names different messages in the two realms (e.g. 0x09 0x01
    is GeneralMidi1SystemOn in Non-Real-Time and
    ControllerDestinationChannelPressure in Real-Time, per Table 4);
    the classifier decodes against the parsed realm before matching.
    Singleton Sub-ID #1 categories (0x7B End of File through 0x7F
    ACK) are singleton-shaped and report as unit variants of
    UniversalSubId1. F7 continuation / escape packets, manufacturer-
    prefixed F0 packets (any leading byte other than 0x7E / 0x7F),
    and F0 packets truncated before Sub-ID #1 (fewer than 3 payload
    bytes) return None so callers route them through
    SysExEvent::manufacturer_id or the raw SysExEvent::data instead.
  • The UniversalSubId1 enum surfaces every Sub-ID #1 byte the
    document defines: Sample Dump Header / Data Packet / Request (the
    three Non-Real-Time singletons at 0x01..=0x03), MIDI Time Code
    (Non-RT Setup at 0x04 / RT Quarter-Frame at 0x01), Sample Dump
    Extensions, General Information, File Dump, MIDI Tuning Standard,
    General MIDI / Controller Destination Setting, Downloadable Sounds /
    Key-Based Instrument Control, File Reference / Scalable Polyphony
    MIP, MIDI Visual Control / Mobile Phone Control, MIDI Capability
    Inquiry, MIDI Show Control, Notation Information, Device Control,
    Real-Time MTC Cueing — plus the five Non-Real-Time singletons (End
    of File, Wait, Cancel, NAK, ACK).
  • The UniversalSubId2 enum surfaces every Sub-ID #2 byte the
    document defines for those categories: 15 Non-Real-Time MTC Setup
    sub-categories (Special, Punch In / Out Points, Delete Punch In /
    Out Point, Event Start / Stop Point, the four "with additional
    info" variants, Cue Points, Delete Cue Point, Event Name with
    additional info), 7 Sample Dump Extension sub-categories (Loop
    Points / Sample Name transmission and request, Extended Dump
    Header, Extended Loop Points transmission and request), the
    Identity Request / Reply pair, File Dump Header / Data Packet /
    Request, 10 MTS sub-categories (Bulk Dump Request / Reply, Tuning
    Dump Request, Key-Based Tuning Dump, Scale/Octave Tuning Dump in 1-
    and 2-byte formats, Single Note Tuning Change with Bank Select,
    Scale/Octave Tuning in 1- and 2-byte formats, RT Single Note
    Tuning Change), GM 1 / GM Off / GM 2 System On, the DLS quartet
    (Turn DLS On / Off / Voice Allocation Off / On), the File
    Reference quartet (Open File, Select or Reselect Contents, Open
    File and Select Contents, Close File), MTC Full Message / User
    Bits, MSC Extensions, Notation Bar Number / Time Signature
    Immediate / Time Signature Delayed, the five Device Control
    variants (Master Volume / Balance / Fine Tuning / Coarse Tuning /
    Global Parameter Control), 10 RT MTC Cueing sub-categories (the
    Real-Time mirror of the Non-Real-Time MTC Setup family), the
    Controller Destination triple (Channel Pressure, Polyphonic Key
    Pressure, Control Change), Key-Based Instrument Control, Scalable
    Polyphony MIP Message, Mobile Phone Control Message.
  • 30 new unit tests cover: every Sub-ID #1 vocabulary slot, every
    Sub-ID #2 vocabulary slot across both realms, realm disambiguation
    for the shared 0x09 0x01 / 0x02 / 0x07 / 0x08 / 0x09
    byte pairs, the four "this is not a Universal packet" return-None
    paths (F7 continuation, manufacturer-prefixed F0, expanded
    three-byte manufacturer ID, payload shorter than 3 bytes), the
    device_id byte preservation across the 0x00..=0x7F range
    (specific-device vs broadcast 0x7F target), the Other(raw)
    fallback paths for unknown Sub-ID #1 and Sub-ID #2 values, and an
    end-to-end pass that parses an SMF carrying GM 1 System On +
    Master Volume + Master Fine Tuning, walks SmfFile::sysex_events(),
    classifies each entry, and confirms the realm split + Sub-ID #1 /
    Sub-ID #2 decoding all the way through the parser → iteration
    helper → classifier pipeline.
  • Builds on the existing SysExEvent surface (tick, track,
    is_escape, data, ends_with_eox, is_complete_message,
    manufacturer_id) — universal_classification is the realm- and
    category-decoded view of the same payload manufacturer_id returns
    the leading byte of, so a caller routing by either accessor stays
    in sync with the verbatim SysExEvent::data buffer the SMF
    parser produces.

Round 243 — SmfFile::sysex_events() (F0 / F7) iteration helper

  • New SmfFile::sysex_events(&self) -> Vec<SysExEvent> surfaces
    every System Exclusive event — both the F0 start and the F7
    continuation / escape flavours — as a SysExEvent { 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. Standard MIDI File Specification 1.0
    §"System Exclusive Events" defines F0 <varlen> <payload> (a
    complete-or-starting SysEx message; the trailing F7 end marker,
    when present, is included as the final byte of <payload>) and
    F7 <varlen> <payload> (a continuation packet for a previously-
    started F0 message or an arbitrary escape sequence whose
    payload is shipped verbatim to the wire). Both forms surface
    through the helper; the is_escape flag distinguishes them and
    the data payload is reproduced verbatim so a writer can
    round-trip the helper output through SmfFile::to_bytes without
    re-synthesising the SysEx framing.
  • SysExEvent::ends_with_eox() returns true when the payload
    terminates with the 0xF7 end marker; SysExEvent::is_complete_message()
    is sugar for !is_escape && ends_with_eox(), the common universal-
    SysEx case (GM-on F0 7E 7F 09 01 F7, Master Volume, Master
    Tuning) where a caller routes the whole packet in one step.
    SysExEvent::manufacturer_id() returns the leading byte of an
    F0 payload (the manufacturer ID, or 0x7E non-real-time /
    0x7F real-time for universal SysEx) and None for an F7
    packet or an empty payload; expanded three-byte IDs (0x00-
    prefixed) are surfaced as Some(0x00) and the caller inspects
    data[0..=2] for the full ID.
  • The FF 7F SequencerSpecific channel — surfaced through
    SmfFile::sequencer_specifics() — is not selected here; the
    two channels carry different semantics (SysEx travels to the
    MIDI wire; FF 7F is file-private metadata that does not) and a
    file may carry both an F0 7E 7F 09 01 F7 Universal Non-Real-Time
    GM-On packet on the conductor track and a private FF 7F
    plugin-state blob alongside it. The helper surfaces empty
    payloads (F0 00 / F7 00) as data.is_empty() rather than
    filtering them out — the spec permits a zero-length packet.
  • 9 new unit tests cover: empty case (no F0 / F7 in any track);
    universal GM-on at tick zero (F0 7E 7F 09 01 F7, complete
    message, manufacturer 0x7E); F0 without trailing F7 marking
    a multi-packet opener (Roland 0x41); F7 continuation pairing
    after an F0 opener (with EOX terminator on the continuation
    packet); empty payload F0 00 surfacing as data.is_empty();
    multi-track merge by absolute tick; stable sort keeping
    (track, in-track) order at the same tick; filter-purity against
    F0 + F7 alongside FF 03 / FF 01 / FF 21 / FF 20 / FF 51 / FF 7F / B0 / 90 / FF 2F plus a cross-check that
    sequencer_specifics() surfaces its single FF 7F entry
    untouched; and a parser → to_bytes → parser round-trip
    exercising the writer's F0 + F7 paths so the helper can't
    drift out of sync with the mux.
  • Surfaces the SysEx channel alongside the 15-helper meta-event
    family; the meta-event helper count itself stays at 15
    (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}), since SysEx is a wire-event
    family rather than a meta-event family.

Round 240 — SmfFile::channel_prefixes() (FF 20 01 cc) iteration helper

  • New SmfFile::channel_prefixes(&self) -> Vec<ChannelPrefixEvent>
    surfaces every FF 20 01 cc Channel Prefix meta event as a
    ChannelPrefixEvent { 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. FF 20 carries
    the channel-binding hint for non-channel events that follow on the
    same track: the single payload byte names the MIDI channel
    (0..=15) the following meta / sysex events should be associated
    with — text, lyric, marker, cue point, sysex — until another
    FF 20 arrives, the next channel-voice event arrives and
    supersedes the binding, or the track ends. 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 plus FF 21 port hints, but older files
    still emit it and a round-trip workflow must preserve it.
  • ChannelPrefixEvent::channel() returns the spec-clamped channel
    index as Option<u8>: Some(c) when c < 16, None otherwise.
    The raw byte stays available on the channel field so files with
    out-of-spec values (a single bit set in the high nibble, etc.)
    still round-trip; the helper returns None rather than mask the
    byte because masking would silently route to an unintended channel.
  • Only FF 20 is selected — the neighbouring FF 21 port-hint
    sibling (different routing semantics: per-track physical port
    assignment versus per-message channel override) stays on its own
    SmfFile::midi_ports() helper so callers reconstructing the
    channel association of surrounding non-channel events get a clean
    time-ordered list independent of the other meta streams.
  • 8 new unit tests cover: empty case (no FF 20 in any track);
    single binding at tick zero; full spec 0..=15 round trip (one
    case per cc); out-of-spec cc = 0x20 surfaces raw with
    channel() == None; multi-track merge by absolute tick; stable
    sort keeping (track, in-track) order at the same tick;
    filter-purity against FF 21 / FF 00 / FF 01 / FF 03 / FF 05 / FF 51 / FF 54 / FF 58 / FF 59 / FF 7F; and a parser → to_bytes
    → parser round-trip that exercises the writer's FF 20 path so
    the helper can't drift out of sync with the mux.
  • 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 234 — SmfFile::to_bytes() SMF mux-side writer

  • New SmfFile::to_bytes(&self) -> Result<Vec<u8>> serialises a
    parsed SMF back to a complete byte stream (MThd header + one
    MTrk chunk per Track), suitable to hand back to parse for
    a structural round-trip. Companion Track::to_bytes_chunk(&self) -> Result<Vec<u8>> emits one self-contained MTrk chunk so
    callers building a multi-track file from independent track
    sources can splice the chunks under a single MThd header.
  • Every channel-voice variant (NoteOn / NoteOff /
    PolyAftertouch / ControlChange / ProgramChange /
    ChannelAftertouch / PitchBend), every concrete meta variant
    (SequenceNumber / Text / ChannelPrefix / Port /
    EndOfTrack / Tempo / SmpteOffset / TimeSignature /
    KeySignature / SequencerSpecific / passthrough Unknown),
    and both sysex forms (F0 start, F7 continuation/escape) emit
    on the wire format the parser already accepts. Unknown with
    type_byte: 0x2F is rejected so the End-of-Track marker cannot
    be smuggled in past the placement check.
  • Output uses explicit status bytes throughout — the spec
    permits but does not require running-status compression, and
    the explicit form keeps the writer deterministic regardless of
    internal track ordering. A reader that does not honour running
    status can still consume the output unchanged.
  • New top-level MAX_VLQ_VALUE = 0x0FFF_FFFF constant — the
    largest VLQ-encodable value per the SMF spec's 4-byte cap (the
    same cap the parser's MAX_VLQ_BYTES = 4 enforces on the read
    side). Delta-times, meta payload lengths, and sysex payload
    lengths must all fit; the writer surfaces Error::InvalidData
    for anything larger.
  • Strict validation at encode-time, with descriptive
    Error::InvalidData messages that name the offending field /
    track / event index so caller-side debugging stays local:
    header.ntrks must match tracks.len(); each track must end
    with exactly one MetaEvent::EndOfTrack as its final event;
    channel-voice data bytes must have the high bit clear; pitch-bend
    values must fit 14 bits; KeySignature.mode must be 0 or 1;
    Tempo must fit 24 bits; SMPTE frames_per_second must be one
    of {24, 25, 29, 30}; TicksPerQuarter must be 1..=0x7FFF;
    Text.kind must be 0x01..=0x0F.
  • Lifts the SMF surface from read-only to read + write: the
    full event vocabulary the parser materialises (channel-voice,
    meta, sysex) now round-trips through to_bytes -> parse
    byte-for-byte. The 17 new writer tests cover spec-VLQ encoding
    (10 worked examples), every meta variant, every channel-voice
    variant, both sysex forms, a multi-track Format-1 file, the
    long-VLQ (4-byte) delta branch, all five validation rejections,
    and the per-track chunk helper.

Round 230 — SmfFile::midi_ports() (FF 21)

  • New smf::MidiPortEvent { tick, track, port } value type pinned to
    the absolute tick at which an FF 21 01 pp MIDI Port Meta-Event
    fires on its parent track. MidiPortEvent::port() returns the
    physical port byte (0..=127).
  • New SmfFile::midi_ports() -> Vec<MidiPortEvent> iterator helper
    that walks every track, sums the per-track cumulative deltas, and
    stably merges the matching FF 21 events across tracks (track 0
    before track 1 at the same tick) under the same merge rule the
    other 13 meta-event helpers and scheduler.rs §"merged event
    list, sorted by absolute tick" already use.
  • The FF 21 Meta-Event carries an unofficial routing hint: the
    single payload byte names the physical output port the
    surrounding channel-voice messages on this track should be
    dispatched through. The Standard MIDI File Specification 1.0 caps
    a single channel-voice stream at 16 channels (the four status
    nibbles 8x..Ex combined with the four channel bits x0..xF);
    the port hint lets a multi-port DAW back-end multiply that ceiling
    by however many physical outputs it wires up, by labelling each
    track with the port it routes to. The convention is one FF 21
    near the start of a track (delta zero, before the first
    channel-voice event), but the helper surfaces every occurrence
    rather than enforcing the placement rule so files that re-route
    mid-track still round-trip the hint.
  • Only FF 21 is selected — the neighbouring FF 20 channel-prefix
    hint (different routing semantics: per-message channel override
    versus per-track physical port assignment) stays on its own 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}).
  • 8 new unit tests covering: empty case, single hint at tick 0, full
    7-bit range (0x7F) round-trip, per-track routing in a format-1
    multi-port file, same-tick stable sort (track 0 before track 1),
    cross-track merge sorted by tick, late-position absolute-tick
    tracking after a channel-voice event, and filter exclusion against
    the surrounding FF 00 / FF 01 / FF 03 / FF 05 / FF 20 /
    FF 51 / FF 54 / FF 58 / FF 59 / FF 7F events (the
    FF 20 sibling test pins the channel-prefix-vs-port distinction).

Round 224 — SmfFile::sequence_numbers() (FF 00)

  • New smf::SequenceNumberEvent { tick, track, number } value type
    pinned to the absolute tick at which an FF 00 02 ssss Sequence
    Number Meta-Event fires on its parent track.
    SequenceNumberEvent::number() returns the 16-bit identifier (the
    ssss payload is decoded big-endian).
  • New SmfFile::sequence_numbers() -> Vec<SequenceNumberEvent>
    iterator helper that walks every track, sums the per-track
    cumulative deltas, and stably merges the matching FF 00 events
    across tracks (track 0 before track 1 at the same tick) under the
    same merge rule the other 12 meta-event helpers and scheduler.rs
    §"merged event list, sorted by absolute tick" already use.
  • The Standard MIDI File Specification 1.0 reserves the event for
    delta-time zero (the first event of a track) and uses it to label
    a format-2 pattern so it can be cued from a Song Select, 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. Format-1 / format-0 files conventionally place
    the event on track 0 to label the file as a whole.
  • 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}).
  • 8 new unit tests covering: empty case, single label at tick 0,
    big-endian decode (0x1234), full 0xFFFF round-trip, format-2
    per-pattern labels, same-tick stable sort (track 0 before track
    1), late-position absolute-tick tracking, and filter exclusion
    against the surrounding FF 01 / FF 03 / FF 05 / FF 51 /
    FF 54 / FF 58 / FF 59 / FF 7F events.

Round 219 — SmfFile::sequencer_specifics() (FF 7F)

  • New smf::SequencerSpecificEvent { tick, track, data } value type
    pinned to the absolute tick at which an FF 7F len data
    Sequencer-Specific Meta-Event fires on its parent track.
    SequencerSpecificEvent::data_bytes() borrows the raw payload.
  • New SmfFile::sequencer_specifics() -> Vec<SequencerSpecificEvent>
    iterator helper that walks every track, sums the per-track
    cumulative deltas, and stably merges the matching FF 7F events
    across tracks (track 0 before track 1 at the same tick) under the
    same merge rule the other 11 meta-event helpers and scheduler.rs
    §"merged event list, sorted by absolute tick" already use. Only
    FF 7F is selected — channel-message F0 / F7 SysEx events
    (which travel through the scheduler's SysEx pump rather than the
    meta-event family) stay out of the list.
  • FF 7F payloads are surfaced verbatim: the parser does not
    interpret the SysEx-style manufacturer-ID convention (where
    data[0], or data[0..=2] when data[0] == 0x00, holds the ID),
    so a caller routing by manufacturer can inspect
    SequencerSpecificEvent::data directly while a generic player can
    ignore per the SMF spec's "unknown meta events SHOULD be ignored"
    rule. Empty payloads (FF 7F 00) are surfaced as
    data.is_empty() rather than filtered out — the spec permits a
    zero-length blob.
  • 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}).
  • 8 new unit tests covering: empty case, single event at tick 0,
    zero-length payload, multiple within one track, cross-track merge
    sorted by tick, same-tick stable sort (track 0 before track 1),
    filter excluding text / tempo / SMPTE / time / key meta kinds,
    and absolute-tick tracking after channel-voice events.

Round 213 — SmfFile::channel_snapshot_at / channel_snapshots_at (channel-state seek primitive)

  • New smf::SmfChannelSnapshot { program, bank_msb, bank_lsb, volume, pan, expression, modulation, sustain, pitch_bend } capturing the
    on-the-wire channel state of one MIDI channel at one absolute SMF
    tick. Fields that no event has touched stay at the SMF + GM 1
    (RP-003) recommended defaults: volume = 100, pan = 64,
    expression = 127, modulation = 0, sustain = false,
    pitch_bend = 0x2000; program / bank_msb / bank_lsb stay
    None so a seek-time initialiser can skip emitting CCs the file
    never wrote.
  • New SmfFile::channel_snapshot_at(channel, tick) -> SmfChannelSnapshot and SmfFile::channel_snapshots_at(tick) -> [SmfChannelSnapshot; 16]. Each 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 tempo_map / time_signatures / key_signatures /
    the eight text-meta helpers / smpte_offsets and scheduler.rs
    §"merged event list, sorted by absolute tick" use.
  • The fold updates the snapshot for Program Change, Pitch Bend,
    and Control Change on the seven channel-state CCs (CC 0 Bank
    Select MSB, CC 1 Modulation Wheel, CC 7 Channel Volume, CC 10
    Pan, CC 11 Expression, CC 32 Bank Select LSB, CC 64 Sustain
    Pedal). Notes / poly + channel aftertouch are ignored — they
    affect voice state, not channel state — so the snapshot is
    cheap to compute for any tick without enumerating sounding
    notes. Sustain pedal decodes the CC 64 value with the spec
    threshold (value >= 64 = on, < 64 = off).
  • Events at exactly tick are included in the replay — the
    snapshot reflects state immediately after that tick fires.
    Channels >= 16 (not a legal MIDI channel) fall through to the
    default snapshot rather than panic / index-out.
  • Bulk accessor channel_snapshots_at pools every track's events
    into a single merge + replay pass so initialising all 16
    channels at a seek target is single-pass rather than 16-pass —
    the natural primitive for a DAW or player seeking into the
    middle of a file.
  • SmfChannelSnapshot::apply(&ChannelBody) is also exposed
    publicly so callers running custom replay (e.g. against a
    custom track ordering or a filtered event set) can reuse the
    same wire semantics.
  • This is the first SMF-file accessor that moves beyond the
    "iterate every meta event" lens introduced by tempo_map /
    time_signatures / key_signatures / the eight text-meta
    helpers / smpte_offsets. The iteration helpers answer "when
    does X fire?"; the snapshot answers "what is the wire state at
    T?" — the two are complementary primitives for SMF seeking and
    inspection.
  • 15 new dedicated tests: default snapshot when no events,
    Program Change at tick 0, the four CC dimensions
    (volume / pan / expression / modulation), CC 64 threshold at 63
    vs. 64, Bank Select MSB + LSB independence, 14-bit Pitch Bend
    decode, tick-filter inclusion of events at exact tick (90 →
    100 → 199 → 200 → ∞ walk), running-status Program Change
    series replayed, multi-channel independence, notes /
    aftertouch leave the snapshot at defaults, unknown CCs
    (CC 99 / CC 100) ignored, invalid channel returns default,
    bulk channel_snapshots_at returns 16 independent states
    consistent with per-channel calls, multi-track merge at same
    tick honours track 0 → track 1 stable order (last-writer-wins
    on the wire), public SmfChannelSnapshot::apply round-trips
    every event-kind.

Round 208 — SmfFile::smpte_offsets() iteration helper + FrameRate decoder (FF 54)

  • New smf::SmpteOffsetEvent { tick, track, hours_raw, minutes, seconds, frames, subframes } plus SmfFile::smpte_offsets() -> Vec<SmpteOffsetEvent>. Collects every SMPTE Offset meta event
    (FF 54 05 hr mn se fr ff, the wall-clock cue declaring when a
    track's first event is meant to fire) from every track, pins each
    one to the absolute tick of its parent track via cumulative
    TrackEvent::delta sums, then merges the per-track sequences
    with a stable sort by tick — track 0 wins over track 1 at the
    same tick, matching the same merge rule used by the ten existing
    text + rhythmic helpers (tempo_map / time_signatures /
    key_signatures / markers / lyrics / cue_points /
    track_names / instrument_names / texts / copyrights) and
    by scheduler.rs §"merged event list, sorted by absolute tick".
  • New smf::FrameRate enum (Fps24 / Fps25 / Fps30DropFrame
    / Fps30NonDrop) decoded from the packed hr byte per the
    MIDI Time Code spec, RP-004/008 §"HOURS COUNT": bits 5-6 hold
    the rate type (00=24fps, 01=25fps, 10=30fps drop-frame,
    11=30fps non-drop); bits 0-4 hold the hours count; bit 7 is
    reserved. FrameRate::from_hours_byte(hr) exposes the raw
    decode; frames_per_second() returns the nominal counter rate
    (drop-frame still numbers 30 frames per wall-second);
    is_drop_frame() distinguishes the two 30-Hz variants.
  • SmpteOffsetEvent::frame_rate() / hours_count() /
    seconds_total() surface the SMPTE-cueing semantics without
    forcing callers to re-mask the hr byte. seconds_total()
    returns the wall-clock offset as h*3600 + m*60 + s + (frames + subframes/100) / fps — drop-frame uses the same 30 Hz divisor
    since the counter compensates for the 29.97 Hz playback, so the
    helper stays rate-independent across the four MTC types (callers
    needing strict NTSC-accurate timing should re-derive from the
    tempo map).
  • Out-of-range values (hours > 23, minutes > 59, seconds > 59,
    frames above the rate's nominal count, subframes > 99) are
    preserved as-is rather than clamped — the helper surfaces the
    raw counter so callers can inspect malformed files.
  • Only FF 54 is selected; the rhythmic / text meta events stay
    uncontaminated (asserted by a dedicated cross-kind filter test).
  • 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 + SMPTE-cueing
    meta event the spec defines a per-event "when it fires" lens for.
  • 10 new dedicated tests: empty, single event with 24 fps decode,
    all-four-frame-rates decode at hours=1, multi-track merge sorted
    by tick, stable sort track-0-before-track-1 at same tick,
    filter excludes other rhythmic + text meta kinds (with
    sibling-helper uncontamination check), seconds_total() at 24
    fps with non-zero sub-frames, seconds_total() at 30-non-drop
    origin, absolute-tick accounting through running-status channel
    events, and FrameRate::from_hours_byte() bit-mask coverage
    (bit 7 reserved, bits 5-6 select the rate, bits 0-4 don't leak
    into the rate decode).

Round 202 — SmfFile::texts() + SmfFile::copyrights() iteration helpers (FF 01 + FF 02)

  • New smf::TextEvent { tick, track, text } plus
    SmfFile::texts() -> Vec<TextEvent>. Collects every free-form /
    general text meta event (FF 01 len text, the catch-all annotation
    kind used for production notes, "do not edit", version stamps,
    mix-engineer comments, …) from every track, pins each one to the
    absolute tick of its parent track via cumulative
    TrackEvent::delta sums, then merges the per-track sequences with
    a stable sort by tick — track 0 wins over track 1 at the same
    tick, matching the same merge rule used by the seven existing
    text-meta helpers (markers / lyrics / cue_points /
    track_names / instrument_names) and the rhythmic helpers
    (tempo_map / time_signatures / key_signatures) and by
    scheduler.rs §"merged event list, sorted by absolute tick".
  • New smf::CopyrightEvent { tick, track, text } plus
    SmfFile::copyrights() -> Vec<CopyrightEvent>. Collects every
    copyright-notice meta event (FF 02 len text) from every track
    under the same merge rule. The SMF specification recommends
    placing the notice on the first track at tick 0 so players can
    surface authorship without scanning the whole file; this helper
    surfaces every occurrence in time order regardless, so callers
    that only want the first notice can take .next() on the
    iterator while callers that want the full history can read the
    whole Vec.
  • Only FF 01 / FF 02 are selected by their respective helpers.
    The five sibling text-kind meta events (FF 03 track name,
    FF 04 instrument name, FF 05 lyric, FF 06 marker, FF 07
    cue point) are filtered out so callers populating the relevant
    metadata get a clean per-track stream without having to
    discriminate themselves. A dedicated round-trip test asserts
    that a single track may legally carry both FF 01 and FF 02
    and that the two helpers surface them independently — and the
    cross-kind filter tests now assert that all seven text-meta
    helpers stay uncontaminated when every text-kind event is
    present.
  • Lifts the SMF text-meta iterator family from 8 to 10
    helpers (SmfFile::{tempo_map,time_signatures,key_signatures, markers,lyrics,cue_points,track_names,instrument_names,texts, copyrights}), covering every FF 01..=07 text-flavour meta
    event the spec defines.
  • Same accessor shape as the other eight text-meta helpers:
    TextEvent::text_bytes() / CopyrightEvent::text_bytes() for
    the raw payload (encoding is spec-unspecified — historically
    Latin-1, modern files emit UTF-8), text_lossy() for a
    Cow<str> UTF-8 decode with U+FFFD substitutes for invalid
    sequences.
  • 19 new dedicated tests (10 for texts(), 9 for copyrights()):
    empty, single event, multiple within one track, multi-track
    merge sorted by tick, stable sort track-0-before-track-1 at
    same tick, filter excludes other text kinds (with sibling-
    helper uncontamination check), absolute-tick accounting
    through running-status channel events, text_lossy()
    invalid-UTF-8 substitution, and a cross-kind coexistence test
    proving FF 01 and FF 02 on the same track surface
    independently.

Round 196 — SmfFile::instrument_names() iteration helper (FF 04)

  • New smf::InstrumentNameEvent { tick, track, text } plus
    SmfFile::instrument_names() -> Vec<InstrumentNameEvent>. Collects
    every instrument-name meta event (FF 04 len text, the per-track
    voice / patch label distinct from the FF 03 track-list label) from
    every track, pins each one to the absolute tick of its parent track
    via cumulative TrackEvent::delta sums, then merges the per-track
    sequences with a stable sort by tick — track 0 wins over track 1
    at the same tick, matching the same merge rule used by
    SmfFile::track_names() / cue_points() / markers() / lyrics()
    / tempo_map() / time_signatures() / key_signatures() and by
    scheduler.rs §"merged event list, sorted by absolute tick".
  • Only FF 04 is selected. The other text-kind meta events
    (FF 01 general text, FF 02 copyright, FF 03 track name,
    FF 05 lyric, FF 06 marker, FF 07 cue point) are filtered out
    so callers populating per-track instrument metadata get a clean
    per-track stream without having to discriminate themselves. A
    dedicated round-trip test asserts that a single track may legally
    carry both FF 03 and FF 04 and that the two helpers surface
    them independently.
  • Lifts the SMF text-meta iterator 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 other six text-meta helpers:
    InstrumentNameEvent::text_bytes() for the raw payload (encoding
    is spec-unspecified — historically Latin-1, modern files emit
    UTF-8), text_lossy() for a Cow<str> UTF-8 decode with U+FFFD
    substitutes for invalid sequences.
  • 10 dedicated tests: empty, single event, per-track in format-1,
    multiple within one track, multi-track merge sorted by tick, stable
    sort track-0-before-track-1 at same tick, filter excludes other
    text kinds (including a sanity check that the sibling helpers stay
    uncontaminated), absolute-tick accounting through running-status
    channel events, coexistence with FF 03 on the same track, and
    text_lossy() invalid-UTF-8 substitution.