use crate::core::cell_note::CellNote;
use crate::core::effect::TrackEffect;
use crate::core::fixed::units::Volume;
use crate::core::pitch::Pitch;
use core::fmt::*;
use serde::{Deserialize, Serialize};
use alloc::vec::Vec;
#[derive(Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum CellEvent {
#[default]
None,
NoteOn { pitch: Pitch, velocity: Volume },
NoteOnGhost { pitch: Pitch, velocity: Volume },
InstrReset,
NoteOff { retrig: bool },
NoteCut,
NoteFade,
}
impl CellEvent {
#[inline]
pub fn from_columns(note: CellNote, instrument: Option<usize>, velocity: Volume) -> Self {
match (note, instrument) {
(CellNote::Empty, None) => Self::None,
(CellNote::Empty, Some(_)) => Self::InstrReset,
(CellNote::Play(p), Some(_)) => Self::NoteOn { pitch: p, velocity },
(CellNote::Play(p), None) => Self::NoteOnGhost { pitch: p, velocity },
(CellNote::KeyOff, None) => Self::NoteOff { retrig: false },
(CellNote::KeyOff, Some(_)) => Self::NoteOff { retrig: true },
(CellNote::NoteCut, _) => Self::NoteCut,
(CellNote::NoteFade, _) => Self::NoteFade,
}
}
#[inline]
pub fn pitch(&self) -> Option<Pitch> {
match *self {
Self::NoteOn { pitch, .. } | Self::NoteOnGhost { pitch, .. } => Some(pitch),
_ => None,
}
}
#[inline]
pub fn velocity(&self) -> Volume {
match *self {
Self::NoteOn { velocity, .. } | Self::NoteOnGhost { velocity, .. } => velocity,
_ => Volume::FULL,
}
}
#[inline]
pub fn has_instrument_column(&self) -> bool {
matches!(
self,
Self::NoteOn { .. } | Self::InstrReset | Self::NoteOff { retrig: true }
)
}
#[inline]
pub fn is_trigger(&self) -> bool {
matches!(self, Self::NoteOn { .. } | Self::NoteOnGhost { .. })
}
#[inline]
pub fn is_release(&self) -> bool {
matches!(self, Self::NoteOff { .. } | Self::NoteCut | Self::NoteFade)
}
#[inline]
pub fn to_cell_note(&self) -> CellNote {
match *self {
Self::None | Self::InstrReset => CellNote::Empty,
Self::NoteOn { pitch, .. } | Self::NoteOnGhost { pitch, .. } => CellNote::Play(pitch),
Self::NoteOff { .. } => CellNote::KeyOff,
Self::NoteCut => CellNote::NoteCut,
Self::NoteFade => CellNote::NoteFade,
}
}
}
impl Debug for CellEvent {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
match self {
Self::None => write!(f, "---"),
Self::NoteOn { pitch, .. } => write!(f, "{:?}", pitch),
Self::NoteOnGhost { pitch, .. } => write!(f, "{:?}g", pitch),
Self::InstrReset => write!(f, "i--"),
Self::NoteOff { retrig: true } => write!(f, "==!"),
Self::NoteOff { retrig: false } => write!(f, "==="),
Self::NoteCut => write!(f, "^^^"),
Self::NoteFade => write!(f, "~~~"),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
pub struct Cell {
pub event: CellEvent,
pub effects: Vec<TrackEffect>,
}
impl Cell {
#[inline]
pub fn from_columns(
note: CellNote,
instrument: Option<usize>,
velocity: Volume,
effects: Vec<TrackEffect>,
) -> Self {
Self {
event: CellEvent::from_columns(note, instrument, velocity),
effects,
}
}
#[inline]
pub fn pitch(&self) -> Option<Pitch> {
self.event.pitch()
}
#[inline]
pub fn velocity(&self) -> Volume {
self.event.velocity()
}
#[inline]
pub fn has_instrument_column(&self) -> bool {
self.event.has_instrument_column()
}
pub fn has_arpeggio(&self) -> bool {
self.effects
.iter()
.any(|effect| matches!(effect, TrackEffect::Arpeggio { half1: _, half2: _ }))
}
pub fn has_delay(&self) -> bool {
self.effects
.iter()
.any(|effect| matches!(effect, TrackEffect::NoteDelay(_)))
}
pub fn get_delay(&self) -> usize {
self.effects
.iter()
.find_map(|effect| {
if let TrackEffect::NoteDelay(delay) = effect {
Some(*delay)
} else {
None
}
})
.unwrap_or(0)
}
pub fn has_note_off(&self) -> bool {
let fx = self
.effects
.iter()
.any(|effect| matches!(effect, TrackEffect::NoteOff { tick: _, past: _ }));
fx || matches!(self.event, CellEvent::NoteOff { .. })
}
pub fn has_note_off_effect(&self) -> bool {
self.effects
.iter()
.any(|effect| matches!(effect, TrackEffect::NoteOff { tick: _, past: _ }))
}
pub fn has_note_off_at_tick_zero(&self) -> bool {
self.effects
.iter()
.any(|effect| matches!(effect, TrackEffect::NoteOff { tick: 0, past: _ }))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pitch() -> Pitch {
Pitch::C5
}
#[test]
fn from_columns_maps_every_arm() {
assert!(matches!(
CellEvent::from_columns(CellNote::Play(pitch()), Some(3), Volume::FULL),
CellEvent::NoteOn { .. }
));
assert!(matches!(
CellEvent::from_columns(CellNote::Play(pitch()), None, Volume::FULL),
CellEvent::NoteOnGhost { .. }
));
assert!(matches!(
CellEvent::from_columns(CellNote::Empty, Some(0), Volume::FULL),
CellEvent::InstrReset
));
assert!(matches!(
CellEvent::from_columns(CellNote::Empty, None, Volume::FULL),
CellEvent::None
));
assert_eq!(
CellEvent::from_columns(CellNote::KeyOff, None, Volume::FULL),
CellEvent::NoteOff { retrig: false }
);
assert_eq!(
CellEvent::from_columns(CellNote::KeyOff, Some(0), Volume::FULL),
CellEvent::NoteOff { retrig: true }
);
assert_eq!(
CellEvent::from_columns(CellNote::NoteCut, None, Volume::FULL),
CellEvent::NoteCut
);
assert_eq!(
CellEvent::from_columns(CellNote::NoteCut, Some(5), Volume::FULL),
CellEvent::NoteCut
);
assert_eq!(
CellEvent::from_columns(CellNote::NoteFade, None, Volume::FULL),
CellEvent::NoteFade
);
assert_eq!(
CellEvent::from_columns(CellNote::NoteFade, Some(5), Volume::FULL),
CellEvent::NoteFade
);
}
#[test]
fn pitch_velocity_extraction() {
let e = CellEvent::NoteOn {
pitch: pitch(),
velocity: Volume::FULL,
};
assert_eq!(e.pitch(), Some(pitch()));
assert_eq!(e.velocity(), Volume::FULL);
let e = CellEvent::NoteOnGhost {
pitch: pitch(),
velocity: Volume::SILENT,
};
assert_eq!(e.pitch(), Some(pitch()));
assert_eq!(e.velocity(), Volume::SILENT);
for e in [
CellEvent::None,
CellEvent::InstrReset,
CellEvent::NoteOff { retrig: false },
CellEvent::NoteOff { retrig: true },
CellEvent::NoteCut,
CellEvent::NoteFade,
] {
assert_eq!(e.pitch(), None);
assert_eq!(e.velocity(), Volume::FULL);
}
}
#[test]
fn has_instrument_column_truth_table() {
assert!(CellEvent::NoteOn {
pitch: pitch(),
velocity: Volume::FULL
}
.has_instrument_column());
assert!(CellEvent::InstrReset.has_instrument_column());
assert!(CellEvent::NoteOff { retrig: true }.has_instrument_column());
assert!(!CellEvent::NoteOnGhost {
pitch: pitch(),
velocity: Volume::FULL
}
.has_instrument_column());
assert!(!CellEvent::NoteOff { retrig: false }.has_instrument_column());
assert!(!CellEvent::None.has_instrument_column());
assert!(!CellEvent::NoteCut.has_instrument_column());
assert!(!CellEvent::NoteFade.has_instrument_column());
}
#[test]
fn to_cell_note_round_trip() {
assert_eq!(CellEvent::None.to_cell_note(), CellNote::Empty);
assert_eq!(CellEvent::InstrReset.to_cell_note(), CellNote::Empty);
assert_eq!(
CellEvent::NoteOn {
pitch: pitch(),
velocity: Volume::FULL
}
.to_cell_note(),
CellNote::Play(pitch())
);
assert_eq!(
CellEvent::NoteOff { retrig: false }.to_cell_note(),
CellNote::KeyOff
);
assert_eq!(
CellEvent::NoteOff { retrig: true }.to_cell_note(),
CellNote::KeyOff
);
assert_eq!(CellEvent::NoteCut.to_cell_note(), CellNote::NoteCut);
assert_eq!(CellEvent::NoteFade.to_cell_note(), CellNote::NoteFade);
}
}