281 lines
10 KiB
Rust
281 lines
10 KiB
Rust
//! Which notes an endpoint has left sounding.
|
|
//!
|
|
//! A note on with no matching note off is the failure a musician hears, and the one they cannot
|
|
//! fix from the application that caused it — the note is sounding on a synth that is no longer
|
|
//! being told anything. Everything that stops carrying MIDI has to stop the notes it started
|
|
//! first, and that means knowing which ones those are.
|
|
|
|
use crate::midi::{CC_ALL_NOTES_OFF, CC_ALL_SOUND_OFF, Channel, MidiMessage, silence_channel};
|
|
|
|
/// How many notes one channel can hold, which is the full seven-bit range.
|
|
const NOTES_PER_CHANNEL: u8 = 128;
|
|
|
|
/// The notes one endpoint currently has sounding, by channel.
|
|
///
|
|
/// Tracked per channel rather than silencing all sixteen, because a reset on a channel nothing
|
|
/// was playing still clears sustain and cuts sound for whatever else is driving that port.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct Sounding {
|
|
/// One bit per note, one word per channel.
|
|
notes: [u128; 16],
|
|
}
|
|
|
|
impl Sounding {
|
|
/// Creates a record with nothing sounding.
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Updates the record from one message on its way out of an endpoint.
|
|
pub fn record(&mut self, message: &MidiMessage) {
|
|
// A note on with zero velocity is a note off by convention, so `stops_note` is asked
|
|
// first: treating it as a start is the classic way to leave a note hanging forever.
|
|
if let Some((channel, note)) = message.stops_note() {
|
|
self.set(channel, note, false);
|
|
return;
|
|
}
|
|
if let MidiMessage::NoteOn { channel, note, .. } = message {
|
|
self.set(*channel, *note, true);
|
|
return;
|
|
}
|
|
// A reset the sender issued itself leaves the channel quiet, so silencing later has
|
|
// nothing left to say about it.
|
|
if let MidiMessage::ControlChange {
|
|
channel,
|
|
controller,
|
|
..
|
|
} = message
|
|
&& (*controller == CC_ALL_NOTES_OFF || *controller == CC_ALL_SOUND_OFF)
|
|
{
|
|
self.clear_channel(*channel);
|
|
}
|
|
}
|
|
|
|
/// Returns the messages that stop everything this endpoint has sounding.
|
|
///
|
|
/// Each played channel gets its pedal released, a note off for every note still sounding,
|
|
/// then the two broad resets. The note offs are for a synth that ignores all-notes-off, and
|
|
/// come after the pedal release so the pedal does not hold them.
|
|
///
|
|
/// Empty when nothing is sounding, which is how a caller avoids disturbing a port that was
|
|
/// never playing anything.
|
|
pub fn silence(&self) -> Vec<MidiMessage> {
|
|
self.silence_beside(0)
|
|
}
|
|
|
|
/// Returns the channels with anything sounding, one bit per channel.
|
|
pub fn channels(&self) -> u16 {
|
|
self.notes
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, word)| **word != 0)
|
|
.fold(0, |channels, (index, _)| channels | (1 << index))
|
|
}
|
|
|
|
/// Returns the messages that stop what this record has sounding, on a port others also play.
|
|
///
|
|
/// A channel in `shared`, one bit per channel as [`Sounding::channels`] gives them, has
|
|
/// another sender's notes sounding on it. It gets only this record's note offs: the pedal
|
|
/// release and the broad resets would stop the other sender's notes as well.
|
|
pub fn silence_beside(&self, shared: u16) -> Vec<MidiMessage> {
|
|
let mut messages = Vec::new();
|
|
for channel in Channel::all() {
|
|
let Some(word) = self
|
|
.notes
|
|
.get(usize::from(channel.index()))
|
|
.copied()
|
|
.filter(|word| *word != 0)
|
|
else {
|
|
continue;
|
|
};
|
|
let alone = shared & (1 << channel.index()) == 0;
|
|
let [pedal, notes_off, sound_off] = silence_channel(channel);
|
|
if alone {
|
|
messages.push(pedal);
|
|
}
|
|
messages.extend(
|
|
(0..NOTES_PER_CHANNEL)
|
|
.filter(|note| bit(*note).is_some_and(|bit| word & bit != 0))
|
|
.map(|note| MidiMessage::NoteOff {
|
|
channel,
|
|
note,
|
|
velocity: 0,
|
|
}),
|
|
);
|
|
if alone {
|
|
messages.push(notes_off);
|
|
messages.push(sound_off);
|
|
}
|
|
}
|
|
messages
|
|
}
|
|
|
|
/// Forgets everything, for an endpoint that is no longer able to sound anything.
|
|
pub fn clear(&mut self) {
|
|
self.notes = [0; 16];
|
|
}
|
|
|
|
/// Records one note as sounding or stopped.
|
|
fn set(&mut self, channel: Channel, note: u8, sounding: bool) {
|
|
let Some(word) = self.notes.get_mut(usize::from(channel.index())) else {
|
|
return;
|
|
};
|
|
let Some(bit) = bit(note) else {
|
|
return;
|
|
};
|
|
if sounding {
|
|
*word |= bit;
|
|
} else {
|
|
*word &= !bit;
|
|
}
|
|
}
|
|
|
|
/// Records a whole channel as quiet.
|
|
fn clear_channel(&mut self, channel: Channel) {
|
|
if let Some(word) = self.notes.get_mut(usize::from(channel.index())) {
|
|
*word = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns the bit standing for one note, or nothing for a value outside the seven-bit range.
|
|
fn bit(note: u8) -> Option<u128> {
|
|
(note < NOTES_PER_CHANNEL).then(|| 1u128 << note)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::midi::CC_SUSTAIN;
|
|
|
|
fn channel(index: u8) -> Channel {
|
|
Channel::new(index).expect("channel in range")
|
|
}
|
|
|
|
fn note_on(index: u8, note: u8, velocity: u8) -> MidiMessage {
|
|
MidiMessage::NoteOn {
|
|
channel: channel(index),
|
|
note,
|
|
velocity,
|
|
}
|
|
}
|
|
|
|
fn note_off(index: u8, note: u8) -> MidiMessage {
|
|
MidiMessage::NoteOff {
|
|
channel: channel(index),
|
|
note,
|
|
velocity: 0,
|
|
}
|
|
}
|
|
|
|
fn controller(index: u8, controller: u8) -> MidiMessage {
|
|
MidiMessage::ControlChange {
|
|
channel: channel(index),
|
|
controller,
|
|
value: 0,
|
|
}
|
|
}
|
|
|
|
/// Returns the full silence for one channel with the given notes held, in the order it is
|
|
/// sent: pedal release, a note off per note, all-notes-off, all-sound-off.
|
|
fn silenced(index: u8, notes: &[u8]) -> Vec<MidiMessage> {
|
|
let mut messages = vec![controller(index, CC_SUSTAIN)];
|
|
messages.extend(notes.iter().map(|note| note_off(index, *note)));
|
|
messages.push(controller(index, CC_ALL_NOTES_OFF));
|
|
messages.push(controller(index, CC_ALL_SOUND_OFF));
|
|
messages
|
|
}
|
|
|
|
/// Silencing stops exactly the notes a sender left sounding, on only the channels it played,
|
|
/// and leaves alone what another sender is playing.
|
|
///
|
|
/// The pedal is released first because a held sustain pedal keeps notes sounding through
|
|
/// all-notes-off, and each held note gets its own note off for a synth that ignores
|
|
/// all-notes-off. A channel nothing played is not reset, since a reset still clears sustain
|
|
/// for whatever else drives the port, and on a channel another sender is playing the pedal
|
|
/// release and broad resets would stop that sender's notes too, so only note offs go. A note
|
|
/// on with zero velocity is a note off (MIDI 1.0), and a note number outside the seven-bit
|
|
/// range is hostile input that must be ignored rather than index out of bounds.
|
|
#[test]
|
|
fn silencing_stops_exactly_what_was_left_sounding() {
|
|
let cases = [
|
|
(
|
|
"a released note",
|
|
vec![note_on(0, 60, 100), note_off(0, 60)],
|
|
vec![],
|
|
vec![],
|
|
),
|
|
(
|
|
"a note released by a note on with zero velocity",
|
|
vec![note_on(0, 60, 100), note_on(0, 60, 0)],
|
|
vec![],
|
|
vec![],
|
|
),
|
|
(
|
|
"the same note struck twice and released once",
|
|
vec![note_on(0, 60, 100), note_on(0, 60, 90), note_off(0, 60)],
|
|
vec![],
|
|
vec![],
|
|
),
|
|
(
|
|
"notes the sender silenced itself",
|
|
vec![
|
|
note_on(0, 60, 100),
|
|
note_on(0, 64, 100),
|
|
controller(0, CC_ALL_NOTES_OFF),
|
|
],
|
|
vec![],
|
|
vec![],
|
|
),
|
|
(
|
|
"notes out of the seven-bit range",
|
|
vec![note_on(0, 200, 100), note_off(0, 255)],
|
|
vec![],
|
|
vec![],
|
|
),
|
|
(
|
|
"held drum notes on channel 10 only, released by name in ascending order",
|
|
vec![
|
|
note_on(9, 82, 100),
|
|
note_on(9, 36, 100),
|
|
note_on(9, 40, 100),
|
|
note_off(9, 40),
|
|
],
|
|
vec![],
|
|
silenced(9, &[36, 82]),
|
|
),
|
|
(
|
|
"notes on three channels, each silenced in channel order",
|
|
vec![
|
|
note_on(15, 60, 100),
|
|
note_on(0, 60, 100),
|
|
note_on(9, 60, 100),
|
|
],
|
|
vec![],
|
|
[silenced(0, &[60]), silenced(9, &[60]), silenced(15, &[60])].concat(),
|
|
),
|
|
(
|
|
"a channel another sender is playing gets only note offs",
|
|
vec![note_on(0, 60, 100), note_on(9, 36, 100)],
|
|
vec![note_on(9, 38, 100)],
|
|
[silenced(0, &[60]), vec![note_off(9, 36)]].concat(),
|
|
),
|
|
];
|
|
for (case, played, beside, want) in cases {
|
|
let mut sounding = Sounding::new();
|
|
for message in &played {
|
|
sounding.record(message);
|
|
}
|
|
let mut other = Sounding::new();
|
|
for message in &beside {
|
|
other.record(message);
|
|
}
|
|
assert_eq!(
|
|
sounding.silence_beside(other.channels()),
|
|
want,
|
|
"{case}: silencing must stop what was left sounding and nothing else"
|
|
);
|
|
}
|
|
}
|
|
}
|