Skip to content

Releases: OxideAV/oxideav-midi

v0.0.4

15 Jun 05:11
c5caa30

Choose a tag to compare

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-...
Read more

v0.0.3

30 May 02:42
4731288

Choose a tag to compare

Other

  • SmfFile::track_names() iteration helper (FF 03)
  • SmfFile::cue_points() iteration helper (FF 07)
  • SmfFile::lyrics() iteration helper (FF 05)
  • SmfFile::markers() iteration helper (FF 06)
  • add cargo-fuzz harness over smf / sf2 / dls / sfz parsers (round 172)
  • fix parse_key integer overflow on huge octave magnitudes
  • add SmfFile::key_signatures() iteration helper
  • SmfFile::tempo_map() iteration helper (FF 51)
  • SmfFile::time_signatures() iteration helper (FF 58)

Round 192 — SmfFile::track_names() iteration helper (FF 03)

  • 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.

Round 186 — SmfFile::cue_points() iteration helper (FF 07)

  • 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.

Round 182 — SmfFile::lyrics() iteration helper (FF 05)

  • 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.

Round 176 — SmfFile::markers() iteration helper (FF 06)

  • 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 te...
Read more

v0.0.2

24 May 12:08
8c02671

Choose a tag to compare

Added

  • (midi) GM2 Global Parameter Control (Universal Real-Time SysEx 04 05)
  • (midi) Master Balance (Universal Real-Time SysEx 04 02)
  • (midi) Data Increment / Decrement (CC 96/97) per RP-018

Other

  • round 98 — MIDI Tuning Standard (MTS) microtuning
  • Round 95: SFZ-side filter envelope + fil_type + cutoff wiring
  • rewrite release-envelope comment to remove FluidSynth citation
  • EG2 + 2-pole resonant low-pass filter on the shared SamplePlayer (round 91)
  • DLS art1/art2 articulation interpretation (round 80)
  • round 75 — MPE + RPN 1/2/5 + CA-25 master tuning + master volume SysEx
  • registry calls: rename make_decoder/make_encoder → first_decoder/first_encoder

Round 114 — GM2 Global Parameter Control (Universal Real-Time SysEx 04 05)

  • New mixer::GmEffects carries the GM2 system-wide Reverb + Chorus
    parameters edited by the Global Parameter Control Universal
    Real-Time SysEx message (F0 7F <dev> 04 05 …, MMA CA-024). The two
    GM2-reserved slots are 0101 (Reverb) and 0102 (Chorus). Each raw
    7-bit parameter value is decoded to its engineering unit with the
    CA-024 "Recommended Practice for Reverb and Chorus Parameters (from
    General MIDI Level 2)" formulas:
    • Reverb pp=0 Type (select), pp=1 Time
      rt = exp((val − 40) · 0.025) s.
    • Chorus pp=0 Type (select), pp=1 Mod Rate mr = val · 0.122 Hz,
      pp=2 Mod Depth md = (val + 1) / 3.2 ms, pp=3 Feedback
      fb = val · 0.763 %, pp=4 Send-to-Reverb ctr = val · 0.787 %.
  • New Mixer::set_gm_reverb_param(pp, val) /
    Mixer::set_gm_chorus_param(pp, val) apply one parameter-value pair;
    unrecognised parameter ids are ignored per CA-024 ("only that
    parameter-value pair should be ignored"). Mixer::gm_effects()
    exposes the current state and Mixer::reset_gm_effects() restores
    the GM2 recommended initial defaults (Reverb Type 4 Large Hall,
    Chorus Type 2 Chorus 3, with the per-type table values).
  • The scheduler's Universal Real-Time dispatch now routes sub-ID#2
    05 (dispatch_global_parameter_control): it parses the Slot Path
    Length / Parameter-ID Width / Value Width header, requires the
    GM2-reserved slot path (length 1, Slot MSB 1), reads the MSB-first
    parameter ids and LSB-first values across the pair list to EOX, and
    routes each into the reverb/chorus setters by Slot LSB. Non-GM2 slot
    paths (Slot MSB ≠ 1 or length ≠ 1) are ignored.
  • GM 1 / GM 2 System On / GM System Off now also reset the GM2 effect
    parameters to their CA-024 defaults.
  • Tests: GM2 defaults; reverb type+time decode; all five chorus
    parameter decodes; non-GM2 slot ignored; unknown-parameter-in-a-pair
    ignored (rest applied); GM-on resets effects. (+7 lib tests.)
  • The decoded parameters are observable program state; a reverb/chorus
    DSP send is intentionally deferred to a later round.

Round 105 — Master Balance (Universal Real-Time SysEx 04 02)

  • New Mixer::set_master_balance_14(value) /
    Mixer::master_balance_14() carry the device-level Master Balance
    scalar from the MIDI 1.0 Detailed Specification v4.2.1 §"DEVICE
    CONTROL — MASTER VOLUME AND MASTER BALANCE" (p.57). The 14-bit
    value is stored verbatim with 0x0000 = hard left,
    0x2000 = centre, 0x3FFF = hard right; the setter clamps inputs
    above 0x3FFF to the spec maximum.
  • New Mixer::master_balance_gains() returns the per-side
    multipliers (left, right) that the mix loop folds into every
    voice's gain — (1.0, 1.0) at centre, (1.0, 0.0) at hard left,
    (0.0, 1.0) at hard right, and a linear ramp on the far side
    between centre and each extreme (the near side stays at unity).
    This is the textbook "balance between two sound sources" law M1
    v4.2.1 §"BALANCE" describes for CC 8, applied here as the
    device-level analog.
  • The stereo + mono branches of Mixer::mix_stereo now multiply
    every voice by these master-balance gains. The values are hoisted
    out of the per-slot loop, alongside the existing master-volume
    scalar, so the per-voice arithmetic gains a single extra f32
    multiply per side and the default 0x2000 setting produces an
    output buffer byte-identical to the pre-round-105 mix (asserted by
    the new master_balance_centre_matches_pre_balance_output test).
  • scheduler::dispatch_universal_sysex recognises 04 02 lsb msb
    in the Universal Real-Time area and forwards the combined 14-bit
    value via set_master_balance_14. GM 1 / GM 2 System On / GM
    System Off resets now also restore Master Balance to centre
    (0x2000), matching the rest of the master-state reset surface.
  • 12 new tests (9 mixer, 3 scheduler): default-centre +
    unity-gains, hard-left mutes right, hard-right mutes left,
    half-left/half-right ramp arithmetic, the clamp-above-14-bit
    guard, the per-side zeroing of the mix output at each extreme, the
    centre-equals-default-output regression, and the three scheduler
    SysEx routings (centre / hard left / hard right). The existing
    universal_gm_on_sysex_resets_state test gained an additional
    Master-Balance-set-then-reset assertion.

Round 102 — Data Increment / Data Decrement (RP-018)

  • New Mixer::data_inc_dec(channel, step) implements the Data Increment
    (CC 96) / Data Decrement (CC 97) response from the MMA Response to
    Data Inc/Dec Controllers
    recommended practice
    (docs/audio/midi/recommended-practices/rp18.pdf, RP-018). Per the
    spec the controller's value byte is don't care; the scheduler passes
    a fixed +1 step for CC 96 and -1 for CC 97. Each step adjusts the
    sub-field RP-018 prescribes for the currently-selected RPN:
    • RPN 0 (Pitch Bend Sensitivity): step the LSB (cents). Because
      the mixer stores the combined pitch_bend_range_cents
      (= semitones·100 + cents), ±1 performs the spec's
      "LSB-wraps-into-MSB at 100" carry automatically (RP-018 worked
      example: two CC 96 = +2 cents; 200 → 199 borrows down into 1
      semitone + 99 cents). Clamped to >= 1 so the range never reaches
      zero, and the live pitch bend is re-applied to held voices.
    • RPN 1 (Channel Fine Tuning): step the LSB of the 14-bit
      fine-tune accumulator; the cents view is re-derived and routed to
      held voices.
    • RPN 2 (Channel Coarse Tuning): step the MSB (= one semitone) per
      the 4.2-Addendum rule RP-018 cites, clamped to the CA-25 signed
      range −64..=+63.
    • RPN 5 (Modulation Depth Range, CA-26): step the cents field, the
      RP-018 default for future Registered Parameters, clamped to the
      existing 0..=2400 envelope.
    • RPN Null (0x3FFF) and any unmodelled / NRPN selection are a
      no-op, mirroring set_data_entry's null guard. NRPNs (CC 98/99) are
      not modelled, so a step issued under an NRPN selection does nothing.
  • scheduler::dispatch routes CC 96 → data_inc_dec(ch, 1) and CC 97 →
    data_inc_dec(ch, -1).
  • 11 new tests (10 mixer, 1 scheduler): RPN-0 cent-step +
    LSB-wraps-into-MSB carry, RPN-0 decrement borrow, value-byte-ignored
    contract, RPN-1 fine-tune LSB step, RPN-2 semitone step + signed-range
    clamp, RPN-5 cent step, RPN-Null no-op, RPN-0 clamp-above-zero, held-
    voice bend re-apply on range widen, and the scheduler CC 96/97 routing
    (with a deliberately nonsense data byte to prove it is ignored).

Round 98 — MIDI Tuning Standard (MTS) microtuning

  • New tuning module implements the retuning surface from the MMA
    MIDI Tuning Messages specification
    (docs/audio/midi/extensions/MIDI-Tuning-Updated-Specification.pdf,
    incorporating CA-020 / CA-021 / RP-020). TuningTable holds two
    layers of microtuning state — a global 128-entry key-based table and
    per-channel 12-entry scale/octave tables — both expressed as signed
    cents added to a key's 12-tone-equal-temperament pitch. Defaults to
    equal temperament everywhere, so a synth that never receives an MTS
    message renders bit-identically to the pre-MTS path.
  • Data-format decoders, each with worked-example unit tests against the
    spec's tables: freq_word_to_cents_offset (3-byte
    semitone + fraction14/16384 frequency word → cents offset from the
    addressed key, with the reserved 7F 7F 7F "no change" word
    returning None); scale_octave_1byte_to_cents (00 = -64 c,
    40 = 0 c, 7F = +63 c); scale_octave_2byte_to_cents (14-bit,
    0x0000 = -100 c, 0x2000 = 0 c, 0x3FFF = +100 c); and
    scale_octave_channel_mask (the ff gg hh 3-byte channel bitmap,
    with ff bits 2–6 reserved → must not light any channel).
  • Mixer carries a TuningTable; the per-key offset is folded into
    every voice-pitch composition site (note-on + the two
    set_pitch_bend re-apply paths). Drum channel (MIDI 10 = index 9) is
    exempt from retuning, matching the existing CA-25 master-tuning
    exemption. New public API: set_key_tuning_word,
    set_scale_octave_tuning, reset_tuning, tuning(). The real-time
    message forms re-apply pitch to sounding voices (live = true); the
    non-real-time "setup" forms update only the stored table.
  • scheduler::dispatch_universal_sysex now routes sub-ID#1 08 (MIDI
    Tuning Standard) in both the Universal Real-Time (7F) and
    Non-Real-Time (7E) areas: Single-Note Tuning Change (sub-ID#2
    02) and its bank form (07), and Scale/Octave Tuning 1-byte
    (08) and 2-byte (09) forms. Multi-change single-note messages
    (ll entries) and truncated/over-promised buffers are bounds-checked
    so malformed input cannot read past the payload. GM 1 / GM 2 System
    On / GM System Off now also reset MTS tuning to equal temperament.
  • 25 new tests (12 tuning unit, 7 mixer, 6 scheduler) covering
    the decoders, table summation, live vs. setup re-apply, drum-channel
    exemption, pitch-bend summation, channel-mask selection, GM reset,
    and truncated-message safety.

Round 95 — SFZ-side filter envelope + fil_type + cutoff wiring

  • New FilterType enum on instruments::sample_voice covers the six
    SFZ v1 fil_type values documented in
    `docs/a...
Read more

v0.0.1

05 May 06:16
ef97267

Choose a tag to compare

Other

  • SFZ + DLS voice generators (task #410)
  • DLS Level 1 + 2 RIFF reader (parse + dump bank)
  • SFZ text patch reader (load + dump regions)

Round 9 — SFZ + DLS voice generators (task #410)

  • Shared sample-playback voice (instruments::sample_voice). Mono
    in, mono out; the mixer handles stereo panning.
    Covers the DAHDSR amplitude envelope (delay / attack / hold / decay /
    sustain / release), four loop modes (NoLoop, OneShot,
    LoopContinuous, LoopSustain), pitch bend via the existing
    Voice::set_pitch_bend_cents hook, channel/poly aftertouch via
    Voice::set_pressure, exclusive-class drum cuts, and a sine vibrato
    LFO with rate / depth / start-delay.
  • Minimal RIFF/WAVE PCM decoder (instruments::wav_pcm). Decodes
    8-bit unsigned, 16-bit signed LE, 24-bit signed LE, 32-bit signed LE
    PCM, and 32-bit IEEE_FLOAT into mono f32. Stereo / multi-channel WAVs
    are mixed down to mono by averaging channels (round-2 voice
    generation will keep stereo intact).
  • SFZ voice generator. SfzInstrument::make_voice walks the
    flattened region table for the highest-priority match on (key, velocity), decodes the WAV bytes loaded by SfzInstrument::open,
    shifts pitch off pitch_keycenter + tune + transpose, and
    instantiates a SamplePlayer honoring the region's loop_* opcodes
    • an amplitude envelope from ampeg_delay/attack/hold/decay/sustain/ release + a vibrato LFO from lfo01_freq / lfo01_pitch /
      lfo01_delay (with vibrato_* aliases).
  • DLS Level 1 + 2 voice generator. DlsInstrument::make_voice
    picks the matching instrument by MIDI program (bank-MSB / LSB
    matching is round 2), picks a region by (key, velocity), resolves
    wlnk.table_indexptbl cue → wave-pool entry, decodes the PCM
    via wav_pcm::decode_pcm_bytes, and plays the sample through the
    shared SamplePlayer. Region-level wsmp overrides the wave-level
    default per the spec; WLOOP_TYPE_FORWARD (0) maps to
    LoopContinuous, WLOOP_TYPE_RELEASE (1) maps to LoopSustain.
    art1/art2 connection-block evaluation is round 2 — the parsed
    blocks remain on the bank.
  • InstrumentSource builder + MidiDecoder::with_instrument_source.
    Caller passes InstrumentSource::sf2(path) / sfz(path) / dls(path)
    / Tone and the decoder picks the right loader. Format detection is
    not by extension — the caller picks the variant.
  • Tests added: 13 net new lib-side (5 sample_voice, 4 wav_pcm, 3
    SFZ voice-generation, 2 DLS voice-generation, minus the 2 existing
    make_voice_returns_unsupported tests that were replaced/upgraded
    to actually exercise the round-1 voice path) + 3 integration
    (tests/voice_round_trip.rs) exercising end-to-end SFZ/SF2/DLS
    rendering through MidiDecoder with an RMS non-silence assertion.
    Total: 156 lib + 6 integration = 162 passing (was 143 + 3 = 146).

Round 8 — DLS Level 1 + 2 sample-loader (task #409)

  • DLS RIFF parser: walks the RIFF/DLS form and pulls the
    colh collection header, optional vers version stamp, ptbl
    pool table, lins instrument list, wvpl wave pool, and
    top-level INFO metadata into a fully-resolved
    DlsBank. Instruments → regions →
    wave-pool samples are cross-referenced; nothing references back
    into the source bytes.
  • Wave pool: every wave-list entry is parsed for its standard
    WAV fmt + data chunks plus the optional wsmp per-wave loop
    / pitch / gain header. Sample bytes are kept in their on-disk
    form (8-bit unsigned or 16-bit LE signed); decode is round-2.
  • Instrument table: each ins LIST surfaces its bank/program
    (decoded into bank_msb / bank_lsb / program_number and the
    is_drum() bit-31 helper), instrument name from a per-instrument
    INFO/INAM, and an instrument-level articulation list parsed
    from lart (DLS1) or lar2 (DLS2) sub-LISTs.
  • Regions: rgnh (key + velocity range, fusOptions, key group,
    optional DLS2 usLayer), wsmp (per-region overrides), wlnk
    (cue-table reference), and per-region articulation. DLS2 rgn2
    LISTs parse alongside DLS1 rgn and are flagged via
    DlsRegion::is_level2.
  • Articulation: art1-ck and art2-ck connection blocks
    (12-byte records: source / control / destination / transform /
    scale) parse into Vec<DlsArticulationBlock> tagged with
    DlsArtKind::{Art1, Art2} so the round-2 voice generator picks
    the right enum table (DLS1 spec page 43 / DLS2 spec tables 8-10).
    Connection enums are stored as raw u16s — no interpretation in
    round 1.
  • Magic-byte stub becomes real probe + parser: is_dls() and
    the new DlsInstrument::probe() honour the RIFF/DLS magic;
    DlsInstrument::open() and parse_bytes() plumb through to the
    full bank parser. make_voice() still returns
    Error::Unsupported (round-2 work, same shape as the SFZ
    followup).
  • Bounds + caps: every chunk length is checked against bytes
    remaining; pool-table, articulation, and wave-pool counts are
    capped at MAX_RECORDS (1 Mi); cumulative wave-data bytes capped
    at MAX_WAVE_BYTES (256 MiB).
  • Tests added: 13 lib-side (magic detection, minimal-DLS
    parse + wave pool + instrument + region + articulation, DLS2
    rgnh-with-usLayer, art2 block, wsmp loop record, error paths
    for non-DLS / truncated outer / non-DLS path, drum-bit decode,
    open round-trip through disk) + 1 integration smoke
    (tests/sfz_sf2_dls_smoke.rs) building a 2-region DLS in
    memory and dumping the instrument + region table. Total: 143
    lib + 3 integration = 146 passing (was 130 + 2 = 132).
  • Smoke test renamed from tests/sfz_sf2_smoke.rs to
    tests/sfz_sf2_dls_smoke.rs to reflect the wider coverage.

Round 7 — SFZ text patch reader (task #127)

  • SFZ parser: tokenises SFZ syntax (line // ... + block
    /* ... */ comments, <header> sections, name=value opcode
    pairs with space-bearing values like sample paths) and walks the
    full <control> / <global> / <master> / <group> / <region>
    hierarchy. Inheritance is flattened into one fully-resolved opcode
    map per region (global → master → group → region, later overrides
    earlier).
  • Strongly-typed region fields for the round-2 voice generator:
    sample_path, lokey / hikey / lovel / hivel,
    pitch_keycenter, key (sets lokey + hikey + pitch_keycenter),
    loop_start / loop_end, loop_mode (no_loop / one_shot /
    loop_continuous / loop_sustain), transpose, tune (alias
    pitch), volume, pan, trigger. Note names (C4, c#4,
    Db5, c-1) parse alongside decimal MIDI keys.
  • Sample loader: SfzInstrument::open resolves every sample=
    path against the SFZ file's directory + the active <control> default_path= opcode and reads the bytes off disk into
    region.sample_bytes. Missing or unreadable samples become a hard
    parse error so the caller learns at load time. parse_str skips
    the filesystem hooks for in-memory tests.
  • Preprocessor: #include is rejected with Error::Unsupported
    (round-1 reader doesn't follow includes); #define is stored
    verbatim in the surrounding scope's opcode map without macro
    expansion.
  • DLS reader status: docs-blocked. The new
    docs/audio/midi/instrument-formats/ directory contains the SFZ
    format docs (10 HTML files) plus the SoundFont 2.04 spec PDF, but
    no Microsoft DLS Level 1/2 specification. The DLS magic-byte stub
    remains in place; voice generation continues to return
    Error::Unsupported.
  • New tests: 23 added — 22 lib-side covering tokenisation, comment
    stripping, key-name parsing, header inheritance, group reset,
    control / default_path resolution, loop opcodes, opcode-map
    preservation, #include rejection, sample loading + missing-file
    handling, and a tutorial-shaped template smoke; 2 integration tests
    (tests/sfz_sf2_smoke.rs) that dump SFZ regions + an SF2 preset
    list via the public API. Total: 130 lib + 2 integration = 132
    passing (was 111).

Round 6 — SF2 polish (sm24 + stereo + mod-env + filter, task #139)

  • 24-bit sample storage (SF2 2.04+ sm24 chunk). PCM is now
    stored as Arc<[i32]> carrying signed 24-bit values in the lower
    24 bits. When sm24 is present its u8 lower bytes are combined
    with the 16-bit smpl upper bytes; otherwise the 16-bit value is
    widened by left-shift-8. Mismatched sm24 length is silently
    ignored per spec ("parsers must tolerate"). Voice fetch divides
    by 2^23 instead of 2^15.
  • Stereo SF2 zones. Sample headers tagged LEFT / RIGHT with
    a valid bidirectional sample_link are detected at resolve time
    and produce a stereo-aware Sf2Voice that holds two phase
    counters and writes distinct L/R via the new Voice::render_stereo
    hook. The mixer routes such voices through a balance law (cos/sin
    scaled to unity at centre) rather than its mono-pan path.
  • Modulation envelope (gens 25-30 — delay/attack/hold/decay/
    sustain/release). Same DAHDSR shape as the volume envelope but
    with 0..=1 sustain levels; release tracks the volume envelope's
    release_pos so a note-off cleanly tails both off together.
  • Mod-env routing (gens 7 + 11). modEnvToPitch adds the
    envelope-scaled cents offset to the live pitch-bend cents on every
    sample. modEnvToFilterFc modulates the biquad cutoff in cents.
  • Initial low-pass filter (gens 8/9). Direct-form-1 RBJ-cookbook
    biquad on the voice output. Cutoff in absolute cents (re. 8.176
    Hz), Q in centibels of resonance. Filter state is allocated only
    when the cutoff is below ~12 kHz or the mod-env routes meaningfully
    to it; bypass otherwise.
  • Exclusive class (gen 57). Note-on with the same non-zero class
    on the same channel hard-stops every prior voice in that class —
    used for hi-hat open/closed pairs in drum kits. Implemented in
    the mixer via a new Voice::exclusive_class hook.
  • **Pitch-wheel r...
Read more

v0.0.0

03 May 03:05

Choose a tag to compare

chore: Release package oxideav-midi version 0.0.0