Releases: OxideAV/oxideav-midi
Releases · OxideAV/oxideav-midi
v0.0.4
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>: everyNotespan
sounding at the absolutetick— the piano-roll / seek companion to
notes(), and the note-level analogue of the channel-state
channel_snapshot_atprimitive. 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 aschannel_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 bynotes()— cannot be reported.
The result preserves thenotes()(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 ppPolyphonic Key Pressure (per-key aftertouch) channel-voice
event as aPolyAftertouchEvent { 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. Anwas the only channel-voice status nibble without a dedicated
typed extraction helper. Distinct from Channel Pressure (Dn,
surfaced bychannel_pressures()) —Ancarries a per-keykkbyte,
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 oneNote { 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: a9n key 0is treated as a Note Off and closes the
earliest open note of that pitch (FIFO), withoff_velocity == 0.
An explicit8n key off_velcarries 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. Noteaccessors: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 ato_bytes()/
parsestructural 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.--corpushashes (FNV-1a-64) the PCM for every
in-tree fixture SMF through both the SF2 bank and the tone
fallback;--spin SECSloops the render as a sampling-profiler
target; default mode prints per-iteration wall time + output hash. - Profiling ranked
Sf2Voice::renderat ~89 % of the synthesis wall
clock; within it the per-sample DAHDSR volume-envelope stage walk
(anOptiontest + 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::rendernow 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 fromenvelope_at, so the rendered PCM is bit-identical —
corpus hashes are unchanged and the new
envelope_run_matches_envelope_at_per_sampletest pins
to_bits()equality across stage boundaries, the release tail,
and theelapsed-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 everyDn ppChannel 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's0..=15
channel index and the single data bytepp(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
ChannelPressureEventstruct exposeschannel()/
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
Dnis 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 ato_bytes()/parseround trip.
Round 267 — SmfFile::pitch_bends() — En-lsb-msb channel-voice pitch-bend iteration helper
- New
SmfFile::pitch_bends(&self) -> Vec<PitchBendEvent>surfaces
everyEn lsb msbPitch Bend channel-voice event on every track,
pinned to the absolute tick at which it fires, in time order. Each
entry is aPitchBendEvent { tick, track, channel, value }with the
status nibble's low four bits decoded into the spec's0..=15
channel index and the two data bytes combined into the 14-bit code
(msb << 7) | lsb,0..=0x3FFF, no-bend centre0x2000(the parser
assembles the value at decode time). - The new
PitchBendEventstruct carries the sametick/track
pair the existing channel-voice iteration helpers use plus the
decodedchanneland the assembled 14-...
v0.0.3
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 cumulativeTrackEvent::deltasums, then merges the
per-track sequences with a stable sort bytick— track 0 wins
over track 1 at the same tick, matching the same merge rule used
bySmfFile::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 03is selected. Other text-kind meta events
(FF 01general text,FF 02copyright,FF 04instrument
name,FF 05lyric,FF 06marker,FF 07cue 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 03per 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 onTrackNameEvent::track,
while callers tracking renames over time read the fullVec. On
a format-0 file the single track'sFF 03is conventionally read
as the sequence title. TrackNameEvent::text_bytes()borrows the rawtextpayload
unchanged (the SMF spec leaves the encoding unspecified —
historically Latin-1, modern DAWs emit UTF-8).text_lossy()
returnsCow<str>usingString::from_utf8_lossy, so invalid
UTF-8 surfaces asU+FFFDreplacement 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 existingMAX_EVENTS_PER_FILEcap; the helper does not
introduce a new allocation ceiling. - 6 new unit tests in
src/smf.rs::testscover: empty input,
single name at tick 0, per-track names on a format-1 two-track
file (Drums/Bass), twoFF 03events on one track in time
order (Introat tick 0,Mainat tick 480), stable sort at the
same tick across two tracks, filtering against the full
text-meta neighbourhood (FF 01general text,FF 02copyright,
FF 04instrument name,FF 05lyric,FF 06marker,FF 07
cue — with cross-checks that the marker / lyric / cue helpers
stay uncontaminated), andtext_lossy()resilience against
non-UTF-8 bytes. Brings the in-crate unit suite from 333 to 339
unit tests, all passing undercargo test. - Docstring cross-links:
SmfFile::lyrics()and
SmfFile::cue_points()now point atSmfFile::track_namesin
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::deltasums, then merges the per-track sequences
with a stable sort bytick— 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 byscheduler.rs§"merged event
list, sorted by absolute tick". - Only
FF 07is selected. Other text-kind meta events
(FF 01general text,FF 02copyright,FF 03track name,
FF 04instrument name,FF 05lyric,FF 06marker, …) 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 rawtextpayload unchanged
(the SMF spec leaves the encoding unspecified — historically
Latin-1, modern editors emit UTF-8).text_lossy()returns
Cow<str>usingString::from_utf8_lossy, so invalid UTF-8
surfaces asU+FFFDreplacement 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 existingMAX_EVENTS_PER_FILEcap; the helper does
not introduce a new allocation ceiling. - 8 new unit tests in
src/smf.rs::testscover: 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 03track name,FF 05lyric,FF 06marker —
with a cross-check that the marker and lyric helpers stay
uncontaminated), absolute-tick accounting through running-status
channel events, andtext_lossy()resilience against non-UTF-8
bytes. Brings the in-crate suite from 323 to 331 unit tests, all
passing undercargo test -p oxideav-midi. - Docstring cross-links:
SmfFile::markers()andSmfFile::lyrics()
now point atSmfFile::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.karsyllable convention)
from every track, pins each one to the absolute tick of its
parent track via cumulativeTrackEvent::deltasums, then merges
the per-track sequences with a stable sort bytick— track 0
wins over track 1 at the same tick, matching the same merge rule
used bySmfFile::markers()/SmfFile::tempo_map()/
SmfFile::time_signatures()/SmfFile::key_signatures()and
byscheduler.rs§"merged event list, sorted by absolute tick". - Only
FF 05is selected. Other text-kind meta events
(FF 01general text,FF 02copyright,FF 03track name,
FF 04instrument name,FF 06marker,FF 07cue point, …)
are filtered out so karaoke callers iterating syllables don't
have to discriminate themselves. LyricEvent::text_bytes()borrows the rawtextpayload
unchanged (the SMF spec leaves the encoding unspecified —
historically Latin-1, modern files emit UTF-8).text_lossy()
returnsCow<str>usingString::from_utf8_lossy, so invalid
UTF-8 surfaces asU+FFFDreplacement 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 existingMAX_EVENTS_PER_FILEcap; the helper
does not introduce a new allocation ceiling. - 8 new unit tests in
src/smf.rs::testscover: empty input,
single syllable at tick 0, four-syllable in-order sequence
(the "Twinkle, Twinkle".karshape), multi-track merge order,
stable sort at the same tick, filtering against neighbouring
text kinds (FF 03track name,FF 06marker), 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 cumulativeTrackEvent::deltasums, then merges
the per-track sequences with a stable sort bytick— track 0
wins over track 1 at the same tick, matching the same merge
rule used bySmfFile::tempo_map()/
SmfFile::time_signatures()/SmfFile::key_signatures()and
byscheduler.rs§"merged event list, sorted by absolute tick". - Only
FF 06is selected. Other text-kind meta events
(FF 03track name,FF 05lyric,FF 07cue point, …)
are filtered out so callers iterating section labels don't have
to discriminate themselves. MarkerEvent::text_bytes()borrows the rawtextpayload
unchanged (the SMF spec leaves the encoding unspecified —
historically Latin-1, modern DAWs emit UTF-8).text_lossy()
returnsCow<str>usingString::from_utf8_lossy, so invalid
UTF-8 surfaces asU+FFFDreplacement 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 existingMAX_EVENTS_PER_FILEcap; the helper
does not introduce a new allocation ceiling. - 8 new unit tests in
src/smf.rs::testscover: 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...
v0.0.2
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::GmEffectscarries 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 are0101(Reverb) and0102(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=0Type (select),pp=1Time
rt = exp((val − 40) · 0.025)s. - Chorus
pp=0Type (select),pp=1Mod Ratemr = val · 0.122Hz,
pp=2Mod Depthmd = (val + 1) / 3.2ms,pp=3Feedback
fb = val · 0.763%,pp=4Send-to-Reverbctr = val · 0.787%.
- Reverb
- 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 andMixer::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 with0x0000= hard left,
0x2000= centre,0x3FFF= hard right; the setter clamps inputs
above0x3FFFto 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_stereonow 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 extraf32
multiply per side and the default0x2000setting produces an
output buffer byte-identical to the pre-round-105 mix (asserted by
the newmaster_balance_centre_matches_pre_balance_outputtest). scheduler::dispatch_universal_sysexrecognises04 02 lsb msb
in the Universal Real-Time area and forwards the combined 14-bit
value viaset_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, 3scheduler): 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_statetest 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+1step for CC 96 and-1for 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 combinedpitch_bend_range_cents
(= semitones·100 + cents),±1performs 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>= 1so 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, mirroringset_data_entry's null guard. NRPNs (CC 98/99) are
not modelled, so a step issued under an NRPN selection does nothing.
- RPN 0 (Pitch Bend Sensitivity): step the LSB (cents). Because
scheduler::dispatchroutes CC 96 →data_inc_dec(ch, 1)and CC 97 →
data_inc_dec(ch, -1).- 11 new tests (10
mixer, 1scheduler): 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
tuningmodule 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).TuningTableholds 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/16384frequency word → cents offset from the
addressed key, with the reserved7F 7F 7F"no change" word
returningNone);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(theff gg hh3-byte channel bitmap,
withffbits 2–6 reserved → must not light any channel). Mixercarries aTuningTable; the per-key offset is folded into
every voice-pitch composition site (note-on + the two
set_pitch_bendre-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_sysexnow routes sub-ID#108(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
(llentries) 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
tuningunit, 7mixer, 6scheduler) 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
FilterTypeenum oninstruments::sample_voicecovers the six
SFZ v1fil_typevalues documented in
`docs/a...
v0.0.1
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; themixerhandles 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_centshook, 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_voicewalks the
flattened region table for the highest-priority match on(key, velocity), decodes the WAV bytes loaded bySfzInstrument::open,
shifts pitch offpitch_keycenter+tune+transpose, and
instantiates aSamplePlayerhonoring the region'sloop_*opcodes- an amplitude envelope from
ampeg_delay/attack/hold/decay/sustain/ release+ a vibrato LFO fromlfo01_freq/lfo01_pitch/
lfo01_delay(withvibrato_*aliases).
- an amplitude envelope from
- 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_index→ptblcue → wave-pool entry, decodes the PCM
viawav_pcm::decode_pcm_bytes, and plays the sample through the
sharedSamplePlayer. Region-levelwsmpoverrides the wave-level
default per the spec;WLOOP_TYPE_FORWARD(0) maps to
LoopContinuous,WLOOP_TYPE_RELEASE(1) maps toLoopSustain.
art1/art2connection-block evaluation is round 2 — the parsed
blocks remain on the bank. InstrumentSourcebuilder +MidiDecoder::with_instrument_source.
Caller passesInstrumentSource::sf2(path)/sfz(path)/dls(path)
/Toneand 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_unsupportedtests 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 throughMidiDecoderwith 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/DLSform and pulls the
colhcollection header, optionalversversion stamp,ptbl
pool table,linsinstrument list,wvplwave pool, and
top-levelINFOmetadata into a fully-resolved
DlsBank. Instruments → regions →
wave-pool samples are cross-referenced; nothing references back
into the source bytes. - Wave pool: every
wave-listentry is parsed for its standard
WAVfmt+datachunks plus the optionalwsmpper-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
insLIST surfaces its bank/program
(decoded intobank_msb/bank_lsb/program_numberand the
is_drum()bit-31 helper), instrument name from a per-instrument
INFO/INAM, and an instrument-level articulation list parsed
fromlart(DLS1) orlar2(DLS2) sub-LISTs. - Regions:
rgnh(key + velocity range, fusOptions, key group,
optional DLS2usLayer),wsmp(per-region overrides),wlnk
(cue-table reference), and per-region articulation. DLS2rgn2
LISTs parse alongside DLS1rgnand are flagged via
DlsRegion::is_level2. - Articulation:
art1-ckandart2-ckconnection blocks
(12-byte records: source / control / destination / transform /
scale) parse intoVec<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 rawu16s — no interpretation in
round 1. - Magic-byte stub becomes real probe + parser:
is_dls()and
the newDlsInstrument::probe()honour theRIFF/DLSmagic;
DlsInstrument::open()andparse_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 atMAX_RECORDS(1 Mi); cumulative wave-data bytes capped
atMAX_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.rsto
tests/sfz_sf2_dls_smoke.rsto reflect the wider coverage.
Round 7 — SFZ text patch reader (task #127)
- SFZ parser: tokenises SFZ syntax (line
// ...+ block
/* ... */comments,<header>sections,name=valueopcode
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::openresolves everysample=
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_strskips
the filesystem hooks for in-memory tests. - Preprocessor:
#includeis rejected withError::Unsupported
(round-1 reader doesn't follow includes);#defineis 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,#includerejection, 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+
sm24chunk). PCM is now
stored asArc<[i32]>carrying signed 24-bit values in the lower
24 bits. Whensm24is present its u8 lower bytes are combined
with the 16-bitsmplupper 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/RIGHTwith
a valid bidirectionalsample_linkare detected atresolvetime
and produce a stereo-awareSf2Voicethat holds two phase
counters and writes distinct L/R via the newVoice::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
with0..=1sustain levels; release tracks the volume envelope's
release_pos so a note-off cleanly tails both off together. - Mod-env routing (gens 7 + 11).
modEnvToPitchadds the
envelope-scaled cents offset to the live pitch-bend cents on every
sample.modEnvToFilterFcmodulates 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 newVoice::exclusive_classhook. - **Pitch-wheel r...
v0.0.0
chore: Release package oxideav-midi version 0.0.0