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/audio/midi/instrument-formats/sfz-legacy.html"Filter type"
table:lpf_1p/hpf_1p(one-pole, 6 dB/oct, resonance ignored)
andlpf_2p/hpf_2p/bpf_2p/brf_2p(two-pole, 12 dB/oct).
Default isTwoPoleLowPass, which preserves round-91 SF2 / DLS
behaviour exactly.FilterType::parse_sfz()honours the
case-insensitive opcode string + falls back tolpf_2pon unknown
values per the SFZ default convention. -
FilterParamsgains akind: FilterTypefield. SF2 / DLS construct
the struct without overriding it (they inheritTwoPoleLowPass); the
instruments::articulation::Articulation::filter()helper also pins
the kind explicitly to make the SF2/DLS commitment to a single shape
permanent against any futureDefaultflip. -
SamplePlayer::update_filter_coeffsswitches onfilter_kind. The
one-pole arms computetan(ω/2)-prewarped bilinear coefficients with
b2 = a2 = 0so the same direct-form-1tick()math still applies
(no per-sample branch). The 2-pole arms keep the round-91 RBJ-cookbook
denominators (a0 = 1 + α,a1 = -2·cos(ω),a2 = 1 - α) and switch
numerators per shape: low-pass(1-cos)/2, 1-cos, (1-cos)/2,
high-pass(1+cos)/2, -(1+cos), (1+cos)/2, band-pass
sin/2, 0, -sin/2, band-reject1, -2cos, 1. All derivations are
the project's own RBJ math (bilinear transform of the analog
prototypes) — SF2 §8.1.3 + §9.7 + SFZ-legacyfil_typerow only
specify the response shape and slope, not the discrete realisation. -
SamplePlayer::new'sneeds_filtergate now respects non-low-pass
shapes: afil_typeofhpf_*/bpf_2p/brf_2pis not "open"
at the SF2 13500-cents sentinel (it would still attenuate the audible
band), so the biquad allocation also fires whenever
cfg.filter.kindis anything other than the two low-pass variants. -
instruments::sfz::build_filter_from_opcodes()parsescutoff=
(Hz, with acents = 1200·log2(fc_hz / 8.176)bridge into SF2
absolute cents — clamped into the §8.1.3 useful range1500..=13500),
resonance=(dB → centibels atcb = dB · 10, clamped to the
SFZ-spec range0..=40 dB), andfil_type=(via
FilterType::parse_sfz). Aliases (cutoff2,resonance2,
filtype) are honoured per the opcode index. -
instruments::sfz::build_mod_env_from_opcodes()parses the SFZ v1
fileg_*Envelope-Generator opcodes (fileg_delay/
fileg_attack/fileg_hold/fileg_decay/fileg_sustain/
fileg_release+fileg_depthfor the routing depth) and their
SFZ v2fil_*aliases (fil_delayetc.).fileg_sustainis in
percent and maps to the SamplePlayer's0..=1fraction;
fileg_depthis clamped into the documented-12000..=12000
range and dropped intoModEnvParams::to_filter_cents. -
instruments::sfz::build_config_for_regioncalls the two helpers
instead of plumbingDefault::default()formod_env/filter.
Regions withoutfileg_*/fil_type/cutoff/resonance
opcodes still render bit-identically to the round-91 path — the
SF2 "filter open" sentinel (13500 cents) + the inertModEnvParams
default fall straight throughbuild_filter_from_opcodes()/
build_mod_env_from_opcodes(). -
18 new tests in the crate:
instruments::sample_voice::tests::high_pass_attenuates_below_cutoffinstruments::sample_voice::tests::band_pass_peaks_at_cutoffinstruments::sample_voice::tests::band_reject_kills_signal_at_cutoffinstruments::sample_voice::tests::one_pole_low_pass_attenuates_high_frequenciesinstruments::sfz::tests::hz_to_filter_cents_round_trips_known_valuesinstruments::sfz::tests::build_filter_from_opcodes_honours_cutoff_resonance_filtypeinstruments::sfz::tests::build_filter_from_opcodes_defaults_to_open_lpf_2pinstruments::sfz::tests::build_filter_from_opcodes_clamps_resonance_into_rangeinstruments::sfz::tests::filter_type_parse_covers_all_sfz_v1_valuesinstruments::sfz::tests::build_mod_env_from_opcodes_honours_fileg_setinstruments::sfz::tests::build_mod_env_from_opcodes_aliases_v2_namesinstruments::sfz::tests::build_mod_env_default_is_inertinstruments::sfz::tests::build_mod_env_clamps_depthinstruments::sfz::tests::full_template_smoke_drops_cutoff_into_sample_playerinstruments::sfz::tests::sfz_with_low_cutoff_attenuates_high_frequenciesinstruments::sfz::tests::sfz_high_pass_inverts_attenuationinstruments::sfz::tests::sfz_fileg_sweeps_cutoff_open_during_attackinstruments::sfz::tests::build_config_for_region_default_filter_open
All 228 lib tests + 14 integration tests pass (242 total, 0 ignored).
-
Sources:
docs/audio/midi/instrument-formats/sfz-opcodes-index.html
(Aria opcode reference:cutoff,resonance,fil_type,fileg_*fil_*aliases with documented defaults / ranges / SFZ-version
tags),docs/audio/midi/instrument-formats/sfz-legacy.html
("Filter type" table enumerating the sixfil_typevalues + the
"filter disabled" semantics for an absentcutoffopcode),
docs/audio/midi/instrument-formats/sf2-spec-2.04.pdf§8.1.3
(initialFilterFc/initialFilterQunit conventions reused
unchanged from round 91).
Round 91 — EG2 + 2-pole resonant low-pass filter on the shared SamplePlayer
- New
ModEnvParams+FilterParamsstructs on
instruments::sample_voice.ModEnvParamsmirrors the existing
EnvelopeParamsDAHDSR shape but treatssustain_levelas the
post-decrease linear fraction per SF2 v2.04 §8.1.3sustainModEnv
("decrease in level expressed in 0.1 % units", with 0 = peak and 1000
= silence) and carriesto_filter_cents— the per-mod-env-unit
contribution to the filter cutoff per SF2 gen 11modEnvToFilterFc.
FilterParamscarriescutoff_cents(SF2 absolute cents re. 8.176
Hz; default 13500 = "filter open" sentinel from §8.1.3 gen 8) and
q_centibels(0 = Butterworth, per §8.1.3 gen 9). SamplePlayer::newallocates a per-voice 2-pole resonant low-pass
biquad only when the bank actually wants the filter (cutoff <
13000 cents or mod-env-to-filter routing depth > 200 cents).
Otherwise the per-sample biquad work is skipped entirely so
unconfigured SFZ / DLS regions render bit-identically to the
round-80 pre-filter path.- Biquad coefficients computed from the SF2 §8.1.3 cents reference
fc_hz = 8.176 * 2^(cents/1200)(clamped to the spec useful range
1500..=13500, then clipped at0.99 * Nyquist) and the RBJ
cookbook low-pass formulas:a0 = 1 + α,a1 = -2·cos(ω),
a2 = 1 - α,b0 = b2 = (1 - cos(ω))/2,b1 = 1 - cos(ω), where
α = sin(ω)/(2Q). Q (centibels) → linearq_lin = √(½) · 10^(cb/200)
with a0.1..=16.0clamp so runaway resonance can't produce NaN
coefficients. The RBJ derivation is the project's own clean-room
math (bilinear transform of the analog 2-pole low-pass
H(s) = ω₀² / (s² + (ω₀/Q)s + ω₀²)) — SF2 §8.1.3 explicitly
leaves the filter implementation to the renderer per §9.7. SamplePlayer::renderevaluates the mod-env DAHDSR each sample,
addsmod_env_level * mod_env_to_filter_centsto the initial
cutoff, and lazily recomputes biquad coefficients only when the
live cutoff drifts > 50 cents from the last computed value (cheap
perceptual gate that keeps the inner loop multiplication-only
during the delay / hold / sustain phases when the mod-env is
constant). The filter sits between sample fetch and amplitude
envelope, so EG1 amplitude shaping is post-filter as expected.SamplePlayer::releasenow captures the EG2 release-start level
separately from the EG1 release-start level so the filter cutoff
tails off from wherever the mod-env was at note-off, not from
peak — a discontinuity-free release for both amplitude and filter.- New
Articulation::mod_env() -> ModEnvParams+
Articulation::filter() -> FilterParamshelpers on
instruments::articulation.mod_env()converts the DLS EG2
destinations (CONN_DST_EG2_DELAYTIME/_ATTACKTIME/
_HOLDTIME/_DECAYTIME/_SUSTAINLEVEL/_RELEASETIME
plus theSRC_EG2 → DST_FILTER_CUTOFFrouting depth) into the
SamplePlayer-sideModEnvParams;filter()maps
CONN_DST_FILTER_CUTOFF/CONN_DST_FILTER_QintoFilterParams,
falling back to the SF2 "filter open" sentinel (13500 cents) when
the region carries no filter blocks. instruments::dls::build_dls_configplumbsart.mod_env()+
art.filter()into theSamplePlayerConfig. A DLS bank with no
art2filter blocks produces the same audio it did pre-round-91;
a bank with anart2SRC_EG2 → DST_FILTER_CUTOFFblock sweeps
the cutoff exactly as the spec describes.instruments::sfz::build_config_for_regionpopulates the new
fields withDefault::default()— SFZfileg_*/fil_type/
cutoffopcodes are not yet plumbed; SFZ banks render
bit-identically to the round-80 path until a future round wires
the SFZ filter opcodes.- 12 new tests across the crate:
instruments::sample_voice::tests::default_filter_is_inert_no_biquad_allocatedinstruments::sample_voice::tests::low_cutoff_filter_attenuates_high_frequenciesinstruments::sample_voice::tests::mod_env_dahdsr_shape_matches_specinstruments::sample_voice::tests::eg2_filter_sweep_changes_spectrum_over_noteinstruments::sample_voice::tests::high_q_filter_resonates_at_cutoffinstruments::sample_voice::tests::release_captures_mod_env_levelinstruments::articulation::tests::default_mod_env_is_inertinstruments::articulation::tests::default_filter_is_open_sentinelinstruments::articulation::tests::eg2_to_filter_routing_lands_on_mod_envinstruments::articulation::tests::filter_cutoff_override_lands_on_filterinstruments::articulation::tests::eg2_attack_time_lands_on_mod_env_attacktests::dls_articulation::dls_articulation_eg2_sweeps_filter_cutoff_open
(integration: builds a DLS bank withSRC_EG2 → DST_FILTER_CUTOFF- slow EG2 attack, renders through the full
MidiDecoder, asserts
the late-window RMS is > 1.6× the early-window RMS).
- slow EG2 attack, renders through the full
- Test count moves from 212 → 224.
- Spec backing: SF2 v2.04 §8.1.3 generators 8 + 9 + 11 + 25–30
(docs/audio/midi/instrument-formats/sf2-spec-2.04.pdf); DLS Level
2.2 v1.0 Amendment 2 §1.5.2 + Tables 5–6 + 8–10 + 1.13 + 1.14
(docs/audio/midi/instrument-formats/dls2amd2(all)a(pub).pdf); DLS
Level 1 v1.1b "Device Architecture" + "Articulation"
(docs/audio/midi/instrument-formats/dls1v11b.pdf).
Round 80 — DLS art1 / art2 articulation interpretation
- New module
instruments::articulationinterpreting DLS Level 1 and
Level 2 connection blocks at voice-build time. Backed by MMA DLS
Level 1 v1.1b (docs/audio/midi/instrument-formats/dls1v11b.pdf,
Table 1 + 2 of the Device Architecture section) and MMA DLS Level 2.2
v1.0 Amendment 2 (docs/audio/midi/instrument-formats/dls2amd2(all)a(pub).pdf,
Tables 5–10). - Named constants for every
CONN_SRC_*/CONN_DST_*/CONN_TRN_*
enum from DLS2 Tables 8 + 9 + 10, plusABSOLUTE_ZEROsentinel,
exported frominstruments::articulationso callers can build
blocks programmatically (used by the new
crates/oxideav-midi/tests/dls_articulation.rsintegration test). - New
Articulation::evaluate(region_blocks, instrument_blocks) -> Articulationwalks the region-level then instrument-level
connection lists, overlaying every recognised connection on top of
the spec defaults (DLS2 Tables 5 + 6). Per the DLS spec, a region
block overrides the corresponding instrument-level block. - Supported
SRC_NONE → DST_xconnections (the "absolute default
override" branch): Vol EG (EG1) delay / attack / hold / decay /
sustain / release; Mod EG (EG2) delay / attack / hold / decay /
sustain / release (raw, surfaced for a later round); modulator LFO
frequency + start delay; vibrato LFO frequency + start delay;
filter cutoff (DST_FILTER_CUTOFF) and Q; tuning (DST_PITCH);
per-region gain (DST_GAIN); pan (DST_PAN). - Supported modulator routings:
SRC_LFO → DST_PITCH(vibrato depth
on DLS1-style banks where LFO and vibrato share a source);
SRC_LFO → DST_GAIN(tremolo depth);SRC_VIBRATO → DST_PITCH
(dedicated DLS2 vibrato depth — wins overSRC_LFO → DST_PITCH
when both are present);SRC_EG2 → DST_PITCHandSRC_EG2 → DST_FILTER_CUTOFF(mod-env routings, raw);SRC_KEYONVELOCITY → DST_EG1_ATTACKTIME(velocity-dependent attack, raw). DlsInstrument::make_voicenow calls
Articulation::evaluate(®ion.articulation, &inst.articulation)
and folds the resulting [EnvelopeParams] + [VibratoParams] +
tuning cents + gain multiplier into theSamplePlayerConfig. An
emptylartlist still falls back to the SamplePlayer defaults so
banks with no articulation are bit-identical to round-75 output.- Unit conversions: time-cents → seconds (DLS §1.14.3, clamped at
60 s), absolute-pitch → cents (DLS §1.14.1, clamped at ±14 400),
absolute-pitch → Hz (LFO frequency, clamped at 50 Hz), gain → linear
amplitude (DLS §1.14.4, clamped at -96..+48 dB), sustain-percent
→ 0..=1, pan-percent → ±50. - New integration test
tests/dls_articulation.rsexercises the full
SMF → scheduler → DLS bank → articulation → SamplePlayer → PCM path:
aSRC_NONE → DST_PITCH @ +1200 centsblock produces an audibly
different rendering than the same bank with nolartlist (avg
pointwise PCM diff > 50 LSB); aSRC_NONE → DST_EG1_RELEASETIME
block at +2 s sustains a louder release tail than the default 100 ms. - 9 new unit tests in
instruments::articulation: spec-default
fallback when both lists are empty; region overrides instrument;
instrument-level fallback when region is silent; tuning cents
conversion; LFO → pitch routes to the vibrato depth; DLS2 vibrato
source takes precedence over the mod LFO source; gain destination
attenuates correctly (-6 dB → 0.5012 linear); ABSOLUTE_ZERO sentinel
is skipped without overriding the default; unrecognised connections
are dropped silently.
Total test count: 199 lib + 13 integration = 212 (up from 190 + 11 in
round 75).
Round 75 — MPE + RPN expansion + Master Tuning / Master Volume SysEx
- MIDI Polyphonic Expression (MPE) v1.1 end-to-end
(docs/audio/midi/extensions/M1-100-UM_v1-1_MIDI_Polyphonic_Expression_Specification.pdf):
the MPE Configuration Message (RPN 0x0006 on channel 0 = Lower
Manager / channel 15 = Upper Manager) configures one or two zones
via [Mixer::set_mpe_zone]. Per §2.2.5 the receiver sets Manager
Channel PB Sensitivity to 2 semitones and every Member Channel to
48 semitones at MCM time. Per §2.2.7 Polyphonic Key Pressure on a
Member is silently dropped (the spec says it "shall not be sent").
Per Appendix C, Member Channel Pitch Bend sums in cents with the
Manager Channel's bend before reaching the held voice. Per §2.2.3,
a zone reconfiguration stops every Sounding Note on the affected
channels and resets their controllers. New types
[Mixer::MpeZone] / [Mixer::MpeRole] / [Mixer::MpeZoneKind]
surface the zone topology to callers. - CC #74 ("third dimension of control") routed through the new
[Voice::set_timbre] hook. MPE Manager broadcasts to every voice
in the zone; Member only reaches its own voice. Non-MPE channels
route plainly. - RPN 1 (Channel Fine Tuning) — 14-bit data-entry value maps
linearly to ±100 cents (centre 0x40/0x00). Stored as both the raw
accumulator and the derived cents view onChannelStateso an
MSB-then-LSB sequence composes bit-exact. - RPN 2 (Channel Coarse Tuning) — CC 6 MSB sets signed semitone
offset centred on 0x40 (-64..=+63). CC 38 LSB ignored per spec
("the LSB is always 0"). - RPN 5 (Modulation Depth Range) per
docs/audio/midi/recommended-practices/ca26-RPN05-Modulation-Depth-Range.pdf:
CC 6 sets whole-cent range, CC 38 sets fractional cents (0..=99).
Default 50 cents matches the GM 2 recommended practice. Clamped
to ≤ 2400 cents (±2 octaves) so a stray CC 6 = 127 can't pop the
timbre out of audibility. - CC 1 (Modulation Wheel) routed via
[Mixer::set_mod_wheel] → [Voice::set_mod_depth_cents] using
the channel's RPN-5 range. Applied to every held voice plus
picked up at note-on for new voices. - Universal Real-Time SysEx — Master Volume (
F0 7F <dev> 04 01 lsb msb F7): the 14-bit value applies as a multiplicative global
gain at mix time. - Universal Real-Time SysEx — Master Fine Tuning / Master Coarse
Tuning per
docs/audio/midi/recommended-practices/ca25-Master-Fine-Coarse-Tuning-SysEx-Message.pdf:
Fine is ±100 cents centred on 0x40/0x00 (formula
100/8192 × (value - 0x2000)); Coarse is a signed semitone count
centred on 0x40 with the LSB always 0 per spec. Both sum with the
per-channel RPN-1 / RPN-2 tuning + the live pitch bend into the
effective cents pushed to each voice. Drum channel (MIDI 10 =
index 9) is exempt from all tuning per CA-25's "MUST NOT result in
MIDI note-shifting" clause. - Universal Non-Real-Time SysEx — GM 1 / GM 2 System On + GM
System Off (sub-IDs09 01/09 02/09 03) reset the
mixer's master state to GM defaults: master volume → max, master
fine / coarse tuning → centre, all sounding notes off. - Voice trait extension:
set_mod_depth_cents+set_timbre
default-no-op methods so existing voices keep working unchanged. - Per-channel state expansion:
ChannelStategains
mod_wheel,mod_depth_range_cents,channel_fine_tune_cents,
channel_fine_tune_raw_14,channel_coarse_tune_semitones, and
mpe_rolefields. All zero / centre by default so existing tests
see unchanged routing. - Tests added: 38 new — 27 lib-side mixer (RPN 1 / 2 / 5
data-entry, channel + master fine/coarse routing, drum channel
exemption, master volume scaling, mod-wheel routing, CC 74
routing, MPE zone assignment + role tagging + PB sensitivity
defaults + zero-members deactivate + Upper zone, Member + Manager
PB combining, Manager CC 74 broadcast, Member CC 74 isolation,
Polyphonic Key Pressure dropped on Member, Member + Manager
pressure combining, zone-conflict resolution, MCM via the data-
entry pathway, MCM on a non-Manager channel ignored), 6 lib-side
scheduler (CC 1 / CC 74 / Master Volume / Master Fine / Master
Coarse / GM-on routing + MCM via SMF), and 5 new integration
(tests/mpe_and_master_tuning.rs) checking the public
MidiDecodersurface. Total: 189 lib + 11 integration = 200
passing (was 156 + 6 = 162).
Wall respected: every change in this round used only
docs/audio/midi/extensions/M1-100-UM_v1-1_MIDI_Polyphonic_Expression_Specification.pdf
(pages 1–28), docs/audio/midi/recommended-practices/ca25-Master-Fine-Coarse-Tuning-SysEx-Message.pdf,
docs/audio/midi/recommended-practices/ca26-RPN05-Modulation-Depth-Range.pdf,
docs/audio/midi/midi-1.0/Universal-System-Exclusive-Messages.pdf,
plus the in-tree crate sources + oxideav-core's public API. No
external library source consulted, paraphrased, or cross-checked.