You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
New smf::TrackNameEvent { tick, track, text } plus SmfFile::track_names() -> Vec<TrackNameEvent>. Collects every
track-name meta event (FF 03 len text, the DAW-track-list
convention from the Standard MIDI File 1.0 specification) 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::cue_points() / SmfFile::markers() / SmfFile::lyrics() / SmfFile::tempo_map() / SmfFile::time_signatures() / SmfFile::key_signatures() and by scheduler.rs §"merged event list, sorted by absolute tick".
Only FF 03 is selected. Other text-kind meta events
(FF 01 general text, FF 02 copyright, FF 04 instrument
name, FF 05 lyric, FF 06 marker, FF 07 cue point) are
filtered out so callers populating a DAW track-list label don't
have to discriminate themselves.
Authoring tools conventionally emit at most one FF 03 per track
at tick 0, but the spec does not constrain count or placement.
The helper surfaces every occurrence so callers that only want
the first name per track can collect into a HashMap<usize, TrackNameEvent> keyed on TrackNameEvent::track,
while callers tracking renames over time read the full Vec. On
a format-0 file the single track's FF 03 is conventionally read
as the sequence title.
TrackNameEvent::text_bytes() borrows the raw text payload
unchanged (the SMF spec leaves the encoding unspecified —
historically Latin-1, modern DAWs emit UTF-8). text_lossy()
returns Cow<str> using String::from_utf8_lossy, so invalid
UTF-8 surfaces as U+FFFD replacement characters rather than
panicking — convenient default for callers that only need the
human-readable track label.
Cost is linear in the total event count and bounded above by the
parser's existing MAX_EVENTS_PER_FILE cap; the helper does not
introduce a new allocation ceiling.
6 new unit tests in src/smf.rs::tests cover: empty input,
single name at tick 0, per-track names on a format-1 two-track
file (Drums / Bass), two FF 03 events on one track in time
order (Intro at tick 0, Main at tick 480), stable sort at the
same tick across two tracks, filtering against the full
text-meta neighbourhood (FF 01 general text, FF 02 copyright, FF 04 instrument name, FF 05 lyric, FF 06 marker, FF 07
cue — with cross-checks that the marker / lyric / cue helpers
stay uncontaminated), and text_lossy() resilience against
non-UTF-8 bytes. Brings the in-crate unit suite from 333 to 339
unit tests, all passing under cargo test.
Docstring cross-links: SmfFile::lyrics() and SmfFile::cue_points() now point at SmfFile::track_names in
their "distinct from" enumerations, so the doc graph between the
six text-meta helpers stays bidirectionally connected.
New smf::CueEvent { tick, track, text } plus SmfFile::cue_points() -> Vec<CueEvent>. Collects every cue-point
meta event (FF 07 len text, the Standard MIDI File 1.0
film-score / theatrical sync convention) 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::markers() / SmfFile::lyrics() / SmfFile::tempo_map() / SmfFile::time_signatures() / SmfFile::key_signatures() and by scheduler.rs §"merged event
list, sorted by absolute tick".
Only FF 07 is selected. Other text-kind meta events
(FF 01 general text, FF 02 copyright, FF 03 track name, FF 04 instrument name, FF 05 lyric, FF 06 marker, …) are
filtered out so callers driving external synchronisation (scene
change, SFX trigger, video cue) don't have to discriminate
themselves.
CueEvent::text_bytes() borrows the raw text payload unchanged
(the SMF spec leaves the encoding unspecified — historically
Latin-1, modern editors emit UTF-8). text_lossy() returns Cow<str> using String::from_utf8_lossy, so invalid UTF-8
surfaces as U+FFFD replacement characters rather than panicking
— convenient default for callers that only need the human-readable
cue name.
Cost is linear in the total event count and bounded above by
the parser's existing MAX_EVENTS_PER_FILE cap; the helper does
not introduce a new allocation ceiling.
8 new unit tests in src/smf.rs::tests cover: empty input,
single cue at tick 0, three-cue in-order sequence (the
Intro / SceneA / SceneB shape), multi-track merge order,
stable sort at the same tick, filtering against neighbouring
text kinds (FF 03 track name, FF 05 lyric, FF 06 marker —
with a cross-check that the marker and lyric helpers stay
uncontaminated), absolute-tick accounting through running-status
channel events, and text_lossy() resilience against non-UTF-8
bytes. Brings the in-crate suite from 323 to 331 unit tests, all
passing under cargo test -p oxideav-midi.
Docstring cross-links: SmfFile::markers() and SmfFile::lyrics()
now point at SmfFile::cue_points() for the film-score sync
stream so callers searching either doc find the cue companion.
New smf::LyricEvent { tick, track, text } plus SmfFile::lyrics() -> Vec<LyricEvent>. Collects every lyric meta
event (FF 05 len text, the karaoke .kar syllable convention)
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::markers() / SmfFile::tempo_map() / SmfFile::time_signatures() / SmfFile::key_signatures() and
by scheduler.rs §"merged event list, sorted by absolute tick".
Only FF 05 is selected. Other text-kind meta events
(FF 01 general text, FF 02 copyright, FF 03 track name, FF 04 instrument name, FF 06 marker, FF 07 cue point, …)
are filtered out so karaoke callers iterating syllables don't
have to discriminate themselves.
LyricEvent::text_bytes() borrows the raw text payload
unchanged (the SMF spec leaves the encoding unspecified —
historically Latin-1, modern files emit UTF-8). text_lossy()
returns Cow<str> using String::from_utf8_lossy, so invalid
UTF-8 surfaces as U+FFFD replacement characters rather than
panicking — convenient default for callers that only need the
human-readable text.
Cost is linear in the total event count and bounded above by
the parser's existing MAX_EVENTS_PER_FILE cap; the helper
does not introduce a new allocation ceiling.
8 new unit tests in src/smf.rs::tests cover: empty input,
single syllable at tick 0, four-syllable in-order sequence
(the "Twinkle, Twinkle" .kar shape), multi-track merge order,
stable sort at the same tick, filtering against neighbouring
text kinds (FF 03 track name, FF 06 marker), absolute-tick
accounting through running-status channel events, and text_lossy() resilience against non-UTF-8 bytes. Brings the
in-crate suite from 315 to 323 unit tests, all passing under cargo test -p oxideav-midi.
Docstring cross-link: SmfFile::markers() now points at SmfFile::lyrics() for the karaoke syllable stream so callers
searching the marker docs find the lyric companion.
New smf::MarkerEvent { tick, track, text } plus SmfFile::markers() -> Vec<MarkerEvent>. Collects every marker
meta event (FF 06 len text, the DAW song-section convention)
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::tempo_map() / SmfFile::time_signatures() / SmfFile::key_signatures() and
by scheduler.rs §"merged event list, sorted by absolute tick".
Only FF 06 is selected. Other text-kind meta events
(FF 03 track name, FF 05 lyric, FF 07 cue point, …)
are filtered out so callers iterating section labels don't have
to discriminate themselves.
MarkerEvent::text_bytes() borrows the raw text payload
unchanged (the SMF spec leaves the encoding unspecified —
historically Latin-1, modern DAWs emit UTF-8). text_lossy()
returns Cow<str> using String::from_utf8_lossy, so invalid
UTF-8 surfaces as U+FFFD replacement characters rather than
panicking — convenient default for callers that only need the
human-readable label.
Cost is linear in the total event count and bounded above by
the parser's existing MAX_EVENTS_PER_FILE cap; the helper
does not introduce a new allocation ceiling.
8 new unit tests in src/smf.rs::tests cover: empty input,
single marker at tick 0, multiple per-track markers in order,
multi-track merge order, stable sort at the same tick,
filtering against neighbouring text kinds (FF 03 / FF 05),
absolute-tick accounting through running-status channel
events, and text_lossy() resilience against non-UTF-8 bytes.
Brings the in-crate suite from 307 to 315 unit tests, all
passing under cargo test -p oxideav-midi.
Round 172 — cargo-fuzz harness over every attacker-facing parser
New fuzz/ crate (its own [workspace] so it doesn't drag the
umbrella) with four libfuzzer-sys targets covering every parser
that takes attacker-controlled bytes end-to-end:
smf exercises oxideav_midi::smf::parse plus the three public
iteration helpers (tempo_map, time_signatures, key_signatures) on every successful parse, so the
cumulative-tick accounting + meta-event extraction paths cover
fuzz-discovered shapes too.
sf2 exercises instruments::sf2::Sf2Bank::parse (the full RIFF/sfbk walker + LIST INFO / LIST sdta / LIST pdta
cross-link resolution).
sfz exercises instruments::sfz::parse_str (the comment
stripper + tokenizer + <global> / <master> / <group> / <region> header walker + opcode flattening + every typed
field parser).
Each target asserts the contract every parser advertises: arbitrary
bytes return a Result, with no panic / OOM / integer overflow
(debug) / out-of-bounds index on any path. The return value is
intentionally discarded.
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
fixed crash stays under perpetual fuzzer pressure.
Fuzz-discovered bug fix: sfz::parse_key (note-name → MIDI key
conversion) used to panic with attempt to multiply with overflow
in a debug build when handed an octave whose magnitude approached i32::MAX (e.g. lokey=C-2011420400, the libfuzzer crash sample).
The (octave + 1) * 12 + note_idx + accidental chain now uses checked_add / checked_mul and falls out to None on overflow,
matching the existing xyz / c100 / c-100 rejection paths.
New parse_key_octave_extremes_do_not_overflow lib test pins the
fix against C-2011420400, i32::MAX, i32::MIN, the g
note-name variant, and the previously-tested moderate
out-of-MIDI-range pair (c100 / c-100).
Initial 4×~45 s runs cleared 30+ million inputs across smf /
sf2 / dls and 2 M inputs across sfz with zero remaining
crashes. The harness can run indefinitely; CI does not gate on
fuzz time.
New smf::KeySignatureChange { tick, track, sharps_flats, mode }
pins one decoded FF 59 02 sf mi Key Signature meta event to the
absolute tick (cumulative delta-sum) at which it fires on its parent
track. sharps_flats is the signed -7..=+7 accidental count
(negative = flats); mode is 0 major or 1 minor. KeySignatureChange::tonic_name() returns the tonic spelling
("C", "F#", "Bb", …) and name() returns the full key name
("C major", "A minor", …). Both helpers consult a 15-entry
lookup keyed by sf + 7 and return None for out-of-range sf
or any mode other than 0 / 1, so junk payloads stay observable
but never panic. is_major() / is_minor() mirror the mode bit.
New SmfFile::key_signatures() walks every track, sums per-track
deltas into absolute ticks, collects every MetaEvent::KeySignature, and returns the merged stream sorted by
tick. The sort is stable so two changes at the same tick keep the
per-track insertion order — track 0 wins over track 1 at the same
tick, matching the scheduler's merge convention and the existing tempo_map / time_signatures helpers.
8 new lib tests (smf::tests): empty-when-no-meta-event;
single-change-at-tick-zero (C major, all four fields + the two
display helpers); three changes within one track (C major →
A major → C minor); merge across two tracks sorted by tick; stable
sort keeps track 0 before track 1 at the same tick; absolute-tick
accounting after running-status channel events; the full 30-entry
circle-of-fifths name table (both modes, every sf in -7..=+7);
out-of-range sf / unknown mode produce None.
New smf::TempoChange { tick, track, microseconds_per_quarter_note, bpm } pins one decoded FF 51 03 tt tt tt Set Tempo meta event to
the absolute tick (cumulative delta-sum) at which it fires on its
parent track. bpm is pre-computed as 60_000_000.0 / microseconds_per_quarter_note; microseconds_per_quarter_note == 0 maps to f64::INFINITY so a
degenerate payload can't divide-by-zero. TempoChange::new is the
public constructor that does the pre-computation.
New SmfFile::tempo_map() walks every track, sums per-track deltas
into absolute ticks, collects every MetaEvent::Tempo, and returns
the merged stream sorted by tick. The sort is stable so two changes
at the same tick keep the per-track insertion order — track 0 wins
over track 1 at the same tick, matching the scheduler's merge
convention and the existing SmfFile::time_signatures() helper.
7 new lib tests (smf::tests): empty-when-no-meta-event;
single-change-at-tick-zero (with the BPM cross-check); three
changes within one track; merge across two tracks sorted by tick;
stable sort keeps track 0 before track 1 at the same tick;
absolute-tick accounting after running-status channel events;
zero µs/qn maps to +INF BPM without panic.
New smf::TimeSignatureChange { tick, track, numerator, denominator_pow2, clocks_per_click, notated_32nd_per_quarter }
pins one decoded FF 58 04 nn dd cc bb meta event to the absolute
tick (cumulative delta-sum) at which it fires on its parent track. TimeSignatureChange::denominator() returns 1 << dd, saturated
at u32::MAX so a spec-illegal dd >= 32 can't overflow the
shift.
New SmfFile::time_signatures() walks every track, sums per-track
deltas into absolute ticks, collects every MetaEvent::TimeSignature, and returns the merged stream sorted
by tick. The sort is stable so two changes at the same tick keep
the per-track insertion order — track 0 wins over track 1 at the
same tick, matching the scheduler's merge convention.
7 new lib tests (smf::tests): empty-when-no-meta-event;
single-change-at-tick-zero (all six fields); three changes within
one track; merge across two tracks sorted by tick; stable sort
keeps track 0 before track 1 at the same tick; absolute-tick
accounting after running-status channel events; denominator
saturates on a pathological dd >= 32.