pub const LANES: usize = 8;
pub const MAX_STEPS: usize = 32;
pub const SLOTS: usize = 8;
pub const MAX_CHAIN: usize = 16;
pub const MAX_PENDING_OFFS: usize = 32;
pub const STEP_COUNTS: [u8; 6] = [4, 8, 12, 16, 24, 32];
const MAX_STEP_SCAN: i64 = 64;
const MAX_SEGMENTS: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Rate {
Quarter,
Eighth,
#[default]
Sixteenth,
ThirtySecond,
EighthTriplet,
SixteenthTriplet,
}
impl Rate {
pub const ALL: [Rate; 6] = [
Self::Quarter,
Self::Eighth,
Self::Sixteenth,
Self::ThirtySecond,
Self::EighthTriplet,
Self::SixteenthTriplet,
];
#[must_use]
pub const fn ticks(self) -> i64 {
match self {
Self::Quarter => 960,
Self::Eighth => 480,
Self::Sixteenth => 240,
Self::ThirtySecond => 120,
Self::EighthTriplet => 320,
Self::SixteenthTriplet => 160,
}
}
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Quarter => "1/4",
Self::Eighth => "1/8",
Self::Sixteenth => "1/16",
Self::ThirtySecond => "1/32",
Self::EighthTriplet => "1/8T",
Self::SixteenthTriplet => "1/16T",
}
}
#[must_use]
pub const fn index(self) -> u8 {
match self {
Self::Quarter => 0,
Self::Eighth => 1,
Self::Sixteenth => 2,
Self::ThirtySecond => 3,
Self::EighthTriplet => 4,
Self::SixteenthTriplet => 5,
}
}
#[must_use]
pub fn from_index(index: u8) -> Self {
Self::ALL.get(index as usize).copied().unwrap_or_default()
}
#[must_use]
pub fn stepped(self, delta: i32) -> Self {
let target = (i32::from(self.index()) + delta).clamp(0, Self::ALL.len() as i32 - 1);
Self::ALL[target as usize]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SwitchQuant {
#[default]
PatternEnd,
Bar,
Beat,
Immediate,
}
impl SwitchQuant {
pub const ALL: [SwitchQuant; 4] = [Self::PatternEnd, Self::Bar, Self::Beat, Self::Immediate];
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::PatternEnd => "pattern",
Self::Bar => "bar",
Self::Beat => "beat",
Self::Immediate => "now",
}
}
#[must_use]
pub const fn index(self) -> u8 {
match self {
Self::PatternEnd => 0,
Self::Bar => 1,
Self::Beat => 2,
Self::Immediate => 3,
}
}
#[must_use]
pub fn from_index(index: u8) -> Self {
Self::ALL.get(index as usize).copied().unwrap_or_default()
}
#[must_use]
pub fn stepped(self, delta: i32) -> Self {
let target = (i32::from(self.index()) + delta).clamp(0, Self::ALL.len() as i32 - 1);
Self::ALL[target as usize]
}
#[must_use]
pub fn boundary(self, now: i64, pattern_ticks: i64) -> i64 {
let grid = match self {
Self::PatternEnd => pattern_ticks,
Self::Bar => crate::transport::Transport::PPQ * 4,
Self::Beat => crate::transport::Transport::PPQ,
Self::Immediate => return now,
};
if grid <= 0 {
return now;
}
(now + grid - 1).div_euclid(grid) * grid
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Mode {
#[default]
Chromatic,
Ionian,
Dorian,
Phrygian,
Lydian,
Mixolydian,
Aeolian,
Locrian,
}
const IONIAN: [i32; 7] = [0, 2, 4, 5, 7, 9, 11];
const IONIAN_TRIADS: [Chord; 7] = [
Chord::Maj,
Chord::Min,
Chord::Min,
Chord::Maj,
Chord::Maj,
Chord::Min,
Chord::Dim,
];
const IONIAN_SEVENTHS: [[i32; 4]; 7] = [
[0, 4, 7, 11], [0, 3, 7, 10], [0, 3, 7, 10], [0, 4, 7, 11], [0, 4, 7, 10], [0, 3, 7, 10], [0, 3, 6, 10], ];
impl Mode {
pub const ALL: [Mode; 8] = [
Self::Chromatic,
Self::Ionian,
Self::Dorian,
Self::Phrygian,
Self::Lydian,
Self::Mixolydian,
Self::Aeolian,
Self::Locrian,
];
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Chromatic => "chromatic",
Self::Ionian => "ionian",
Self::Dorian => "dorian",
Self::Phrygian => "phrygian",
Self::Lydian => "lydian",
Self::Mixolydian => "mixolydian",
Self::Aeolian => "aeolian",
Self::Locrian => "locrian",
}
}
#[must_use]
pub const fn index(self) -> u8 {
match self {
Self::Chromatic => 0,
Self::Ionian => 1,
Self::Dorian => 2,
Self::Phrygian => 3,
Self::Lydian => 4,
Self::Mixolydian => 5,
Self::Aeolian => 6,
Self::Locrian => 7,
}
}
#[must_use]
pub fn from_index(index: u8) -> Self {
Self::ALL.get(index as usize).copied().unwrap_or_default()
}
#[must_use]
pub fn stepped(self, delta: i32) -> Self {
let target = (i32::from(self.index()) + delta).clamp(0, Self::ALL.len() as i32 - 1);
Self::ALL[target as usize]
}
#[must_use]
pub const fn rotation(self) -> Option<usize> {
match self {
Self::Chromatic => None,
Self::Ionian => Some(0),
Self::Dorian => Some(1),
Self::Phrygian => Some(2),
Self::Lydian => Some(3),
Self::Mixolydian => Some(4),
Self::Aeolian => Some(5),
Self::Locrian => Some(6),
}
}
#[must_use]
pub fn scale(self) -> Option<[i32; 7]> {
let rot = self.rotation()?;
let base = IONIAN[rot];
let mut out = [0; 7];
for (i, slot) in out.iter_mut().enumerate() {
*slot = (IONIAN[(i + rot) % 7] - base).rem_euclid(12);
}
Some(out)
}
#[must_use]
pub fn degree_of(self, note: u8, tonic: u8) -> Option<usize> {
let scale = self.scale()?;
let pitch_class = (i32::from(note) - i32::from(tonic % 12)).rem_euclid(12);
scale.iter().position(|&s| s == pitch_class)
}
#[must_use]
pub fn walk(self, note: u8, tonic: u8, steps: i32) -> u8 {
let Some(scale) = self.scale() else {
return (i32::from(note) + steps).clamp(0, 127) as u8;
};
let tonic = i32::from(tonic % 12);
let relative = i32::from(note) - tonic;
let octave = relative.div_euclid(12);
let pitch_class = relative.rem_euclid(12);
let (degree, on_scale) = match scale.iter().position(|&s| s == pitch_class) {
Some(d) => (d as i32, true),
None => (scale.iter().filter(|&&s| s < pitch_class).count() as i32 - 1, false),
};
let target = degree + steps + i32::from(!on_scale && steps < 0);
let target_octave = octave + target.div_euclid(7);
let target_degree = target.rem_euclid(7) as usize;
(tonic + target_octave * 12 + scale[target_degree]).clamp(0, 127) as u8
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Chord {
#[default]
None,
Fifth,
Octave,
Diatonic,
Diatonic7,
Maj,
Min,
Dim,
Sus2,
Sus4,
Maj6,
Min6,
Dom7,
Min7,
Maj7,
Quartal,
}
impl Chord {
pub const ALL: [Chord; 16] = [
Self::None,
Self::Fifth,
Self::Octave,
Self::Diatonic,
Self::Diatonic7,
Self::Maj,
Self::Min,
Self::Dim,
Self::Sus2,
Self::Sus4,
Self::Maj6,
Self::Min6,
Self::Dom7,
Self::Min7,
Self::Maj7,
Self::Quartal,
];
#[must_use]
pub const fn index(self) -> u8 {
match self {
Self::None => 0,
Self::Fifth => 1,
Self::Octave => 2,
Self::Diatonic => 3,
Self::Diatonic7 => 4,
Self::Maj => 5,
Self::Min => 6,
Self::Dim => 7,
Self::Sus2 => 8,
Self::Sus4 => 9,
Self::Maj6 => 10,
Self::Min6 => 11,
Self::Dom7 => 12,
Self::Min7 => 13,
Self::Maj7 => 14,
Self::Quartal => 15,
}
}
#[must_use]
pub fn from_index(index: u8) -> Self {
Self::ALL.get(index as usize).copied().unwrap_or_default()
}
#[must_use]
pub fn stepped(self, delta: i32) -> Self {
let target = (i32::from(self.index()) + delta).clamp(0, Self::ALL.len() as i32 - 1);
Self::ALL[target as usize]
}
fn intervals(self, root: u8, mode: Mode, tonic: u8, out: &mut [i32; 4]) -> usize {
let fixed: &[i32] = match self {
Self::None => &[0],
Self::Fifth => &[0, 7],
Self::Octave => &[0, 12],
Self::Maj => &[0, 4, 7],
Self::Min => &[0, 3, 7],
Self::Dim => &[0, 3, 6],
Self::Sus2 => &[0, 2, 7],
Self::Sus4 => &[0, 5, 7],
Self::Maj6 => &[0, 4, 7, 9],
Self::Min6 => &[0, 3, 7, 9],
Self::Dom7 => &[0, 4, 7, 10],
Self::Min7 => &[0, 3, 7, 10],
Self::Maj7 => &[0, 4, 7, 11],
Self::Quartal => &[0, 5, 10],
Self::Diatonic | Self::Diatonic7 => {
let seventh = self == Self::Diatonic7;
let quality = mode
.degree_of(root, tonic)
.map(|degree| (degree + mode.rotation().unwrap_or(0)) % 7);
return match (quality, seventh) {
(Some(d), false) => IONIAN_TRIADS[d].intervals(root, mode, tonic, out),
(Some(d), true) => {
out.copy_from_slice(&IONIAN_SEVENTHS[d]);
4
}
(None, false) => Self::Maj.intervals(root, mode, tonic, out),
(None, true) => Self::Maj7.intervals(root, mode, tonic, out),
};
}
};
out[..fixed.len()].copy_from_slice(fixed);
fixed.len()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Voicing {
#[default]
Close,
Drop2,
First,
Second,
}
impl Voicing {
pub const ALL: [Voicing; 4] = [Self::Close, Self::Drop2, Self::First, Self::Second];
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Close => "close",
Self::Drop2 => "drop-2",
Self::First => "1st inv",
Self::Second => "2nd inv",
}
}
#[must_use]
pub const fn index(self) -> u8 {
match self {
Self::Close => 0,
Self::Drop2 => 1,
Self::First => 2,
Self::Second => 3,
}
}
#[must_use]
pub fn from_index(index: u8) -> Self {
Self::ALL.get(index as usize).copied().unwrap_or_default()
}
#[must_use]
pub fn stepped(self, delta: i32) -> Self {
let target = (i32::from(self.index()) + delta).clamp(0, Self::ALL.len() as i32 - 1);
Self::ALL[target as usize]
}
}
pub const MAX_CHORD_NOTES: usize = 5;
#[must_use]
pub fn chord_notes(
root: u8,
chord: Chord,
voicing: Voicing,
root_below: bool,
mode: Mode,
tonic: u8,
out: &mut [u8; MAX_CHORD_NOTES],
) -> usize {
let mut intervals = [0i32; 4];
let count = chord.intervals(root, mode, tonic, &mut intervals);
let mut voices = [0i32; MAX_CHORD_NOTES];
for (slot, interval) in voices.iter_mut().zip(&intervals[..count]) {
*slot = i32::from(root) + interval;
}
let mut len = count;
match voicing {
Voicing::Close => {}
Voicing::Drop2 => {
if len >= 2 {
voices[len - 2] -= 12;
}
}
Voicing::First => {
if len >= 2 {
voices[0] += 12;
}
}
Voicing::Second => {
if len >= 3 {
voices[0] += 12;
voices[1] += 12;
} else if len >= 2 {
voices[0] += 12;
}
}
}
if root_below && len < MAX_CHORD_NOTES {
voices[len] = i32::from(root) - 12;
len += 1;
}
for voice in &mut voices[..len] {
while *voice < 0 {
*voice += 12;
}
while *voice > 127 {
*voice -= 12;
}
}
voices[..len].sort_unstable();
let mut written = 0;
for i in 0..len {
if i > 0 && voices[i] == voices[i - 1] {
continue;
}
out[written] = voices[i] as u8;
written += 1;
}
written
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Step {
pub on: bool,
pub octave: u8,
pub key: u8,
pub chord: u8,
pub voicing: u8,
pub accent: bool,
pub gate: u8,
pub reserved: [u8; 2],
}
impl Step {
pub const TIE: u8 = 255;
pub const MIN_GATE: u8 = 5;
pub const MAX_GATE: u8 = 200;
pub const ROOT_BELOW: u8 = 0b0000_0100;
#[must_use]
pub const fn silent() -> Self {
Self {
on: false,
octave: 5,
key: 0,
chord: 0,
voicing: 0,
accent: false,
gate: 50,
reserved: [0; 2],
}
}
#[must_use]
pub fn root(self) -> u8 {
(u32::from(self.octave) * 12 + u32::from(self.key)).min(127) as u8
}
#[must_use]
pub fn chord_kind(self) -> Chord {
Chord::from_index(self.chord)
}
#[must_use]
pub fn voicing_kind(self) -> Voicing {
Voicing::from_index(self.voicing & 0b11)
}
#[must_use]
pub fn root_below(self) -> bool {
self.voicing & Self::ROOT_BELOW != 0
}
#[must_use]
pub fn gate_ticks(self, ticks_per_step: i64) -> Option<i64> {
if self.gate == Self::TIE {
return None;
}
let percent = i64::from(self.gate.clamp(Self::MIN_GATE, Self::MAX_GATE));
Some((ticks_per_step * percent / 100).max(1))
}
}
impl Default for Step {
fn default() -> Self {
Self::silent()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Lane {
pub note: u8,
pub muted: bool,
pub soloed: bool,
pub steps: [Step; MAX_STEPS],
}
impl Lane {
pub const FROM_STEP: u8 = 0xFF;
#[must_use]
pub const fn empty() -> Self {
Self {
note: Self::FROM_STEP,
muted: false,
soloed: false,
steps: [Step::silent(); MAX_STEPS],
}
}
#[must_use]
pub const fn drum(note: u8) -> Self {
Self { note, ..Self::empty() }
}
#[must_use]
pub const fn is_pitched(&self) -> bool {
self.note == Self::FROM_STEP
}
}
impl Default for Lane {
fn default() -> Self {
Self::empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ChainEntry {
pub slot: u8,
pub repeats: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PatternBlock {
pub steps: u8,
pub rate: Rate,
pub swing: u8,
pub base_vel: u8,
pub accent_vel: u8,
pub default_gate: u8,
pub mode: Mode,
pub tonic: u8,
pub lanes: [Lane; LANES],
pub playing: bool,
pub pending_slot: Option<u8>,
pub switch_quant: SwitchQuant,
pub chain: [ChainEntry; MAX_CHAIN],
pub chain_len: u8,
}
impl PatternBlock {
pub const SIZE: usize = std::mem::size_of::<Self>();
pub const MIN_SWING: u8 = 50;
pub const MAX_SWING: u8 = 75;
#[must_use]
pub const fn empty() -> Self {
Self {
steps: 16,
rate: Rate::Sixteenth,
swing: Self::MIN_SWING,
base_vel: 100,
accent_vel: 127,
default_gate: 50,
mode: Mode::Chromatic,
tonic: 0,
lanes: [Lane::empty(); LANES],
playing: false,
pending_slot: None,
switch_quant: SwitchQuant::PatternEnd,
chain: [ChainEntry { slot: 0, repeats: 1 }; MAX_CHAIN],
chain_len: 0,
}
}
#[must_use]
pub fn step_count(&self) -> usize {
(self.steps as usize).clamp(1, MAX_STEPS)
}
#[must_use]
pub fn ticks_per_step(&self) -> i64 {
self.rate.ticks()
}
#[must_use]
pub fn length_ticks(&self) -> i64 {
self.ticks_per_step() * self.step_count() as i64
}
#[must_use]
pub fn swing_offset(&self, step_index: usize) -> i64 {
if step_index % 2 == 0 {
return 0;
}
let swing = i64::from(self.swing.clamp(Self::MIN_SWING, Self::MAX_SWING));
(swing - i64::from(Self::MIN_SWING)) * 2 * self.ticks_per_step() / 100
}
fn max_swing_offset(&self) -> i64 {
let swing = i64::from(self.swing.clamp(Self::MIN_SWING, Self::MAX_SWING));
(swing - i64::from(Self::MIN_SWING)) * 2 * self.ticks_per_step() / 100
}
#[must_use]
pub fn onset(&self, origin: i64, index: i64) -> i64 {
let steps = self.step_count() as i64;
let in_pattern = index.rem_euclid(steps) as usize;
origin + index * self.ticks_per_step() + self.swing_offset(in_pattern)
}
#[must_use]
pub fn step_at(&self, origin: i64, tick: i64) -> usize {
let steps = self.step_count() as i64;
(tick - origin).div_euclid(self.ticks_per_step()).rem_euclid(steps) as usize
}
#[must_use]
pub fn lane_audible(&self, lane: usize) -> bool {
let Some(l) = self.lanes.get(lane) else { return false };
if l.muted {
return false;
}
let any_solo = self.lanes.iter().any(|l| l.soloed);
!any_solo || l.soloed
}
#[must_use]
pub fn chain_entries(&self) -> &[ChainEntry] {
&self.chain[..(self.chain_len as usize).min(MAX_CHAIN)]
}
}
impl Default for PatternBlock {
fn default() -> Self {
Self::empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PatternEvent {
pub tick: i64,
pub status: u8,
pub data1: u8,
pub data2: u8,
}
impl PatternEvent {
#[must_use]
pub const fn note_on(tick: i64, note: u8, velocity: u8) -> Self {
Self { tick, status: 0x90, data1: note, data2: velocity }
}
#[must_use]
pub const fn note_off(tick: i64, note: u8) -> Self {
Self { tick, status: 0x80, data1: note, data2: 0 }
}
#[must_use]
pub const fn is_note_on(&self) -> bool {
self.status == 0x90 && self.data2 > 0
}
}
pub trait EventSink {
fn accept(&mut self, event: PatternEvent) -> bool;
}
impl EventSink for Vec<PatternEvent> {
fn accept(&mut self, event: PatternEvent) -> bool {
self.push(event);
true
}
}
#[derive(Debug, Clone, Copy)]
pub struct PlaybackWindow {
from: i64,
to: i64,
position: i64,
ticks_per_sample: f64,
frames: u32,
continuous: bool,
}
impl PlaybackWindow {
pub const MAX_TICK_GAP: i64 = 1;
#[must_use]
pub fn for_block(
position: i64,
frames: u32,
ticks_per_sample: f64,
loop_region: Option<(i64, i64)>,
previous: Option<Self>,
) -> Self {
let span = (f64::from(frames) * ticks_per_sample) as i64;
let (from, continuous) = match (previous, loop_region) {
(Some(prev), Some((loop_start, _))) if position < prev.position => (loop_start, false),
(Some(prev), _) if prev.to <= position && position - prev.to <= Self::MAX_TICK_GAP => {
(prev.to, true)
}
_ => (position, false),
};
let mut to = position + span;
if let Some((_, loop_end)) = loop_region {
if loop_end > from {
to = to.min(loop_end);
}
}
Self {
from,
to: to.max(from),
position,
ticks_per_sample,
frames,
continuous,
}
}
#[must_use]
pub fn narrowed(&self, from: i64, to: i64) -> Self {
Self { from, to: to.max(from), ..*self }
}
#[must_use]
pub const fn from(&self) -> i64 {
self.from
}
#[must_use]
pub const fn to(&self) -> i64 {
self.to
}
#[must_use]
pub const fn is_continuous(&self) -> bool {
self.continuous
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.to <= self.from
}
#[must_use]
pub const fn contains(&self, tick: i64) -> bool {
tick >= self.from && tick < self.to
}
#[must_use]
pub fn sample_offset(&self, tick: i64) -> u32 {
let last = self.frames.saturating_sub(1);
let offset = tick - self.from;
if offset <= 0 || self.ticks_per_sample <= 0.0 {
return 0;
}
let samples = (offset as f64 / self.ticks_per_sample) as i64;
u32::try_from(samples).unwrap_or(last).min(last)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct PendingOff {
note: u8,
lane: u8,
due: Option<i64>,
}
#[derive(Debug, Clone, Copy)]
pub struct PendingOffs {
entries: [PendingOff; MAX_PENDING_OFFS],
len: usize,
}
impl PendingOffs {
#[must_use]
pub const fn new() -> Self {
Self {
entries: [PendingOff { note: 0, lane: 0, due: None }; MAX_PENDING_OFFS],
len: 0,
}
}
#[must_use]
pub const fn len(&self) -> usize {
self.len
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.len == 0
}
pub fn clear(&mut self) {
self.len = 0;
}
fn remove(&mut self, index: usize) -> PendingOff {
let gone = self.entries[index];
for i in index..self.len - 1 {
self.entries[i] = self.entries[i + 1];
}
self.len -= 1;
gone
}
pub fn hold(
&mut self,
lane: usize,
note: u8,
due: Option<i64>,
now: i64,
out: &mut impl EventSink,
) {
if self.len == MAX_PENDING_OFFS {
let oldest = self.remove(0);
out.accept(PatternEvent::note_off(now, oldest.note));
}
self.entries[self.len] = PendingOff { note, lane: lane as u8, due };
self.len += 1;
}
pub fn end_lane(&mut self, lane: usize, at: i64, out: &mut impl EventSink) {
let lane = lane as u8;
let mut i = 0;
while i < self.len {
if self.entries[i].lane == lane {
let gone = self.remove(i);
out.accept(PatternEvent::note_off(at, gone.note));
} else {
i += 1;
}
}
}
pub fn emit_due_before(&mut self, tick: i64, out: &mut impl EventSink) {
let mut i = 0;
while i < self.len {
match self.entries[i].due {
Some(due) if due < tick => {
let gone = self.remove(i);
out.accept(PatternEvent::note_off(due, gone.note));
}
_ => i += 1,
}
}
}
pub fn flush(&mut self, at: i64, out: &mut impl EventSink) {
for i in 0..self.len {
out.accept(PatternEvent::note_off(at, self.entries[i].note));
}
self.len = 0;
}
}
impl Default for PendingOffs {
fn default() -> Self {
Self::new()
}
}
pub fn generate(
block: &PatternBlock,
origin: i64,
from: i64,
to: i64,
pending: &mut PendingOffs,
out: &mut impl EventSink,
) {
if to <= from {
return;
}
let tps = block.ticks_per_step();
let steps = block.step_count() as i64;
let first = (from - origin - block.max_swing_offset()).div_euclid(tps);
let last = (to - origin).div_euclid(tps) + 1;
let last = last.min(first + MAX_STEP_SCAN);
let mut chord = [0u8; MAX_CHORD_NOTES];
for index in first..last {
let onset = block.onset(origin, index);
if onset < from || onset >= to {
continue;
}
pending.emit_due_before(onset, out);
let step_index = index.rem_euclid(steps) as usize;
for lane_index in 0..LANES {
if !block.lane_audible(lane_index) {
continue;
}
let lane = &block.lanes[lane_index];
let step = lane.steps[step_index];
if !step.on {
continue;
}
pending.end_lane(lane_index, onset, out);
let velocity = if step.accent { block.accent_vel } else { block.base_vel };
let velocity = velocity.clamp(1, 127);
let due = step.gate_ticks(tps).map(|len| onset + len);
let count = if lane.is_pitched() {
chord_notes(
step.root(),
step.chord_kind(),
step.voicing_kind(),
step.root_below(),
block.mode,
block.tonic,
&mut chord,
)
} else {
chord[0] = lane.note;
1
};
for ¬e in &chord[..count] {
if !out.accept(PatternEvent::note_on(onset, note, velocity)) {
return;
}
pending.hold(lane_index, note, due, onset, out);
}
}
}
pending.emit_due_before(to, out);
}
pub fn compile_cycle(block: &PatternBlock, origin: i64, out: &mut Vec<PatternEvent>) {
let length = block.length_ticks();
let mut pending = PendingOffs::new();
generate(block, origin, origin, origin + length, &mut pending, out);
pending.flush(origin + length, out);
out.sort_by_key(|e| e.tick);
}
#[derive(Debug, Clone, Copy)]
pub struct PatternPlayer {
slots: [PatternBlock; SLOTS],
live: u8,
playing: bool,
pending_slot: Option<u8>,
switch_quant: SwitchQuant,
chain: [ChainEntry; MAX_CHAIN],
chain_len: u8,
pending: PendingOffs,
active: bool,
step: u8,
}
impl PatternPlayer {
#[must_use]
pub fn new() -> Self {
Self {
slots: [PatternBlock::empty(); SLOTS],
live: 0,
playing: false,
pending_slot: None,
switch_quant: SwitchQuant::PatternEnd,
chain: [ChainEntry { slot: 0, repeats: 1 }; MAX_CHAIN],
chain_len: 0,
pending: PendingOffs::new(),
active: false,
step: 0,
}
}
pub fn apply(&mut self, slot: u8, block: PatternBlock) {
let slot = (slot as usize).min(SLOTS - 1);
self.slots[slot] = block;
self.playing = block.playing;
self.switch_quant = block.switch_quant;
self.chain = block.chain;
self.chain_len = block.chain_len;
self.pending_slot = block
.pending_slot
.filter(|&s| s != self.live && block.chain_len == 0);
}
#[must_use]
pub fn slot(&self, index: usize) -> &PatternBlock {
&self.slots[index.min(SLOTS - 1)]
}
#[must_use]
pub fn live_slot(&self) -> u8 {
self.live
}
#[must_use]
pub fn queued_slot(&self) -> Option<u8> {
self.pending_slot
}
#[must_use]
pub fn current_step(&self) -> u8 {
self.step
}
#[must_use]
pub fn is_playing(&self) -> bool {
self.playing
}
#[must_use]
pub fn held_notes(&self) -> usize {
self.pending.len()
}
pub fn silence(&mut self) {
self.pending.clear();
self.active = false;
}
#[must_use]
pub fn countdown(&self, now: i64) -> Option<(u8, i64)> {
let slot = self.pending_slot?;
let block = &self.slots[self.live as usize];
let at = self.switch_quant.boundary(now, block.length_ticks());
Some((slot, (at - now).div_euclid(block.ticks_per_step())))
}
fn locate(&self, tick: i64) -> (u8, i64, i64) {
if let Some(found) = self.chain_at(tick) {
return found;
}
let boundary = match self.pending_slot {
Some(_) => {
let block = &self.slots[self.live as usize];
self.switch_quant.boundary(tick, block.length_ticks())
}
None => i64::MAX,
};
(self.live, 0, boundary)
}
fn chain_at(&self, tick: i64) -> Option<(u8, i64, i64)> {
let entries = &self.chain[..(self.chain_len as usize).min(MAX_CHAIN)];
if entries.is_empty() {
return None;
}
let mut total = 0i64;
for entry in entries {
let slot = (entry.slot as usize).min(SLOTS - 1);
total += i64::from(entry.repeats.max(1)) * self.slots[slot].length_ticks();
}
if total <= 0 {
return None;
}
let base = tick.div_euclid(total) * total;
let mut offset = tick.rem_euclid(total);
let mut start = base;
for entry in entries {
let slot = (entry.slot as usize).min(SLOTS - 1);
let span = i64::from(entry.repeats.max(1)) * self.slots[slot].length_ticks();
if offset < span {
return Some((slot as u8, start, start + span));
}
offset -= span;
start += span;
}
None
}
pub fn render(
&mut self,
window: &PlaybackWindow,
transport_playing: bool,
out: &mut impl EventSink,
) {
if !transport_playing || !self.playing {
if self.active {
self.pending.flush(window.from(), out);
self.active = false;
}
return;
}
if !window.is_continuous() && self.active {
self.pending.flush(window.from(), out);
}
self.active = true;
let mut cursor = window.from();
for _ in 0..MAX_SEGMENTS {
if cursor >= window.to() {
break;
}
let (slot, origin, boundary) = self.locate(cursor);
if boundary <= cursor {
self.pending.flush(cursor, out);
self.switch_at(cursor);
continue;
}
self.live = slot;
let end = boundary.min(window.to());
let block = self.slots[slot as usize];
generate(&block, origin, cursor, end, &mut self.pending, out);
if boundary < window.to() {
self.pending.flush(boundary, out);
self.switch_at(boundary);
}
cursor = end;
}
let block = &self.slots[self.live as usize];
let origin = self.chain_at(window.from()).map_or(0, |(_, start, _)| start);
self.step = block.step_at(origin, window.from()) as u8;
}
fn switch_at(&mut self, boundary: i64) {
if self.chain_at(boundary).is_some() {
return;
}
if let Some(slot) = self.pending_slot.take() {
self.live = (slot as usize).min(SLOTS - 1) as u8;
}
}
}
impl Default for PatternPlayer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn drum_pattern(steps: u8) -> PatternBlock {
let mut block = PatternBlock::empty();
block.steps = steps;
block.playing = true;
block.lanes[0] = Lane::drum(36);
for step in &mut block.lanes[0].steps {
step.on = true;
}
block
}
fn melodic_pattern(on: &[usize]) -> PatternBlock {
let mut block = PatternBlock::empty();
block.playing = true;
for &index in on {
block.lanes[0].steps[index].on = true;
}
block
}
fn onsets(events: &[PatternEvent]) -> Vec<i64> {
events.iter().filter(|e| e.is_note_on()).map(|e| e.tick).collect()
}
fn run(block: &PatternBlock, from: i64, to: i64) -> Vec<PatternEvent> {
let mut out = Vec::new();
let mut pending = PendingOffs::new();
generate(block, 0, from, to, &mut pending, &mut out);
out
}
#[test]
fn the_block_is_the_size_it_is_supposed_to_be() {
assert_eq!(std::mem::size_of::<Step>(), 9);
assert_eq!(std::mem::size_of::<Lane>(), 3 + 32 * 9);
assert_eq!(std::mem::size_of::<PatternBlock>(), PatternBlock::SIZE);
assert_eq!(
PatternBlock::SIZE, 2_373,
"a pattern changed size; every queued SetPattern costs this many bytes"
);
assert_eq!(std::mem::align_of::<PatternBlock>(), 1);
}
#[test]
fn rate_ticks_are_the_960_ppq_table() {
assert_eq!(Rate::Quarter.ticks(), 960);
assert_eq!(Rate::Eighth.ticks(), 480);
assert_eq!(Rate::Sixteenth.ticks(), 240);
assert_eq!(Rate::ThirtySecond.ticks(), 120);
assert_eq!(Rate::EighthTriplet.ticks(), 320);
assert_eq!(Rate::SixteenthTriplet.ticks(), 160);
assert_eq!(Rate::EighthTriplet.ticks() * 3, Rate::Quarter.ticks());
assert_eq!(Rate::SixteenthTriplet.ticks() * 3, Rate::Eighth.ticks());
}
#[test]
fn straight_swing_moves_nothing() {
let block = drum_pattern(16);
assert_eq!(block.swing, PatternBlock::MIN_SWING);
for step in 0..16 {
assert_eq!(block.swing_offset(step), 0);
}
}
#[test]
fn full_swing_is_a_triplet_feel() {
let mut block = drum_pattern(16);
block.swing = 75;
assert_eq!(block.swing_offset(0), 0);
assert_eq!(block.swing_offset(1), block.ticks_per_step() / 2);
assert_eq!(block.swing_offset(2), 0);
assert_eq!(block.swing_offset(15), block.ticks_per_step() / 2);
}
#[test]
fn swing_is_exact_integer_ticks() {
let mut block = drum_pattern(16);
block.swing = 62;
assert_eq!(block.swing_offset(1), 57); block.rate = Rate::Eighth;
assert_eq!(block.swing_offset(1), 115); }
#[test]
fn swing_never_reorders_the_steps() {
for swing in PatternBlock::MIN_SWING..=PatternBlock::MAX_SWING {
let mut block = drum_pattern(16);
block.swing = swing;
let mut previous = i64::MIN;
for index in 0..32 {
let onset = block.onset(0, index);
assert!(onset > previous, "swing {swing} reordered step {index}");
previous = onset;
}
}
}
#[test]
fn starting_mid_pattern_fires_only_the_remaining_onsets() {
let block = drum_pattern(16);
let cycle = block.length_ticks();
assert_eq!(cycle, 3840);
let whole = onsets(&run(&block, 0, cycle));
assert_eq!(whole.len(), 16);
assert_eq!(whole[0], 0);
let late = onsets(&run(&block, 1200, cycle));
assert_eq!(late.len(), 11, "steps 5..=15 remain");
assert_eq!(late[0], 1200);
assert_eq!(late, whole[5..]);
}
#[test]
fn the_step_is_a_function_of_the_position() {
let block = drum_pattern(16);
assert_eq!(block.step_at(0, 0), 0);
assert_eq!(block.step_at(0, 239), 0);
assert_eq!(block.step_at(0, 240), 1);
assert_eq!(block.step_at(0, 3840), 0);
assert_eq!(block.step_at(0, 3840 * 4 + 720), 3);
}
#[test]
fn a_twelve_step_pattern_drifts_against_the_bar() {
let block = drum_pattern(12);
let bar = 3840;
assert_eq!(block.length_ticks(), 2880);
assert_eq!(block.step_at(0, 0), 0);
assert_eq!(block.step_at(0, bar), 4);
assert_eq!(block.step_at(0, bar * 2), 8);
assert_eq!(block.step_at(0, bar * 3), 0, "back in phase after three bars");
}
#[test]
fn a_shorter_pattern_masks_rather_than_truncates() {
let mut block = drum_pattern(32);
assert_eq!(onsets(&run(&block, 0, block.length_ticks())).len(), 32);
block.steps = 16;
let short = run(&block, 0, block.length_ticks());
assert_eq!(onsets(&short).len(), 16);
block.steps = 32;
assert_eq!(
onsets(&run(&block, 0, block.length_ticks())).len(),
32,
"the steps past 16 were cleared rather than masked"
);
}
#[test]
fn tiling_a_cycle_with_windows_fires_every_step_once() {
let block = drum_pattern(16);
let cycle = block.length_ticks();
for span in [1, 7, 240, 241, 1000] {
let mut all = Vec::new();
let mut pending = PendingOffs::new();
let mut from = 0;
while from < cycle {
let to = (from + span).min(cycle);
generate(&block, 0, from, to, &mut pending, &mut all);
from = to;
}
assert_eq!(
onsets(&all).len(),
16,
"span {span} produced the wrong number of onsets"
);
}
}
#[test]
fn a_gate_is_a_percentage_of_the_step() {
let step = Step { gate: 50, ..Step::silent() };
assert_eq!(step.gate_ticks(240), Some(120));
let step = Step { gate: 200, ..Step::silent() };
assert_eq!(step.gate_ticks(240), Some(480));
let step = Step { gate: 0, ..Step::silent() };
assert_eq!(step.gate_ticks(240), Some(12));
let step = Step { gate: Step::TIE, ..Step::silent() };
assert_eq!(step.gate_ticks(240), None, "a tie has no due tick");
}
#[test]
fn every_note_gets_an_off() {
let block = drum_pattern(16);
let events = run(&block, 0, block.length_ticks() + 240);
let ons = events.iter().filter(|e| e.is_note_on()).count();
let offs = events.iter().filter(|e| e.status == 0x80).count();
assert_eq!(ons, 17);
assert_eq!(offs, 17, "a note was left sounding");
}
#[test]
fn a_tie_holds_to_the_next_onset() {
let mut block = melodic_pattern(&[0, 4]);
block.lanes[0].steps[0].gate = Step::TIE;
let events = run(&block, 0, block.length_ticks());
let offs: Vec<i64> = events.iter().filter(|e| e.status == 0x80).map(|e| e.tick).collect();
assert_eq!(offs[0], 960, "the tie ended somewhere other than step 4");
let at_960: Vec<u8> = events.iter().filter(|e| e.tick == 960).map(|e| e.status).collect();
assert_eq!(at_960, vec![0x80, 0x90], "the off has to be pushed first");
}
#[test]
fn a_long_gate_is_cut_by_the_next_onset() {
let mut block = melodic_pattern(&[0, 1]);
block.lanes[0].steps[0].gate = 200;
let events = run(&block, 0, 960);
let at_240: Vec<u8> = events.iter().filter(|e| e.tick == 240).map(|e| e.status).collect();
assert_eq!(at_240, vec![0x80, 0x90]);
}
#[test]
fn the_pending_table_forces_off_the_oldest_on_overflow() {
let mut pending = PendingOffs::new();
let mut out = Vec::new();
for i in 0..MAX_PENDING_OFFS {
pending.hold(0, 40 + i as u8, None, 0, &mut out);
}
assert_eq!(pending.len(), MAX_PENDING_OFFS);
assert!(out.is_empty());
pending.hold(1, 99, None, 100, &mut out);
assert_eq!(out.len(), 1);
assert_eq!(out[0].data1, 40, "the oldest note was not the one forced off");
assert_eq!(out[0].tick, 100);
assert_eq!(pending.len(), MAX_PENDING_OFFS);
}
#[test]
fn a_flush_ends_everything_at_one_tick() {
let mut pending = PendingOffs::new();
let mut out = Vec::new();
pending.hold(0, 60, Some(500), 0, &mut out);
pending.hold(1, 64, None, 0, &mut out);
pending.flush(300, &mut out);
assert_eq!(out.len(), 2);
assert!(out.iter().all(|e| e.tick == 300 && e.status == 0x80));
assert!(pending.is_empty());
}
#[test]
fn a_muted_lane_is_silent_and_a_soloed_one_is_the_only_one() {
let mut block = drum_pattern(16);
block.lanes[1] = Lane::drum(42);
for step in &mut block.lanes[1].steps {
step.on = true;
}
assert_eq!(onsets(&run(&block, 0, 240)).len(), 2);
block.lanes[1].muted = true;
assert_eq!(onsets(&run(&block, 0, 240)).len(), 1);
block.lanes[1].muted = false;
block.lanes[1].soloed = true;
let solo = run(&block, 0, 240);
assert_eq!(onsets(&solo).len(), 1);
assert_eq!(solo[0].data1, 42);
}
#[test]
fn accent_picks_the_patterns_accent_velocity() {
let mut block = melodic_pattern(&[0, 1]);
block.lanes[0].steps[1].accent = true;
let events = run(&block, 0, 480);
let ons: Vec<u8> = events.iter().filter(|e| e.is_note_on()).map(|e| e.data2).collect();
assert_eq!(ons, vec![100, 127]);
}
fn notes_of(root: u8, chord: Chord, voicing: Voicing, below: bool, mode: Mode) -> Vec<u8> {
let mut out = [0u8; MAX_CHORD_NOTES];
let n = chord_notes(root, chord, voicing, below, mode, 0, &mut out);
out[..n].to_vec()
}
#[test]
fn the_chord_table_is_the_shapes_it_names() {
assert_eq!(notes_of(60, Chord::None, Voicing::Close, false, Mode::Chromatic), vec![60]);
assert_eq!(notes_of(60, Chord::Fifth, Voicing::Close, false, Mode::Chromatic), vec![60, 67]);
assert_eq!(notes_of(60, Chord::Octave, Voicing::Close, false, Mode::Chromatic), vec![60, 72]);
assert_eq!(notes_of(60, Chord::Maj, Voicing::Close, false, Mode::Chromatic), vec![60, 64, 67]);
assert_eq!(notes_of(60, Chord::Min, Voicing::Close, false, Mode::Chromatic), vec![60, 63, 67]);
assert_eq!(notes_of(60, Chord::Dim, Voicing::Close, false, Mode::Chromatic), vec![60, 63, 66]);
assert_eq!(notes_of(60, Chord::Sus2, Voicing::Close, false, Mode::Chromatic), vec![60, 62, 67]);
assert_eq!(notes_of(60, Chord::Sus4, Voicing::Close, false, Mode::Chromatic), vec![60, 65, 67]);
assert_eq!(notes_of(60, Chord::Maj6, Voicing::Close, false, Mode::Chromatic), vec![60, 64, 67, 69]);
assert_eq!(notes_of(60, Chord::Min6, Voicing::Close, false, Mode::Chromatic), vec![60, 63, 67, 69]);
assert_eq!(notes_of(60, Chord::Dom7, Voicing::Close, false, Mode::Chromatic), vec![60, 64, 67, 70]);
assert_eq!(notes_of(60, Chord::Min7, Voicing::Close, false, Mode::Chromatic), vec![60, 63, 67, 70]);
assert_eq!(notes_of(60, Chord::Maj7, Voicing::Close, false, Mode::Chromatic), vec![60, 64, 67, 71]);
assert_eq!(notes_of(60, Chord::Quartal, Voicing::Close, false, Mode::Chromatic), vec![60, 65, 70]);
}
#[test]
fn chord_identities_are_the_documented_order() {
let order = [
Chord::None, Chord::Fifth, Chord::Octave, Chord::Diatonic, Chord::Diatonic7,
Chord::Maj, Chord::Min, Chord::Dim, Chord::Sus2, Chord::Sus4, Chord::Maj6,
Chord::Min6, Chord::Dom7, Chord::Min7, Chord::Maj7, Chord::Quartal,
];
for (index, chord) in order.iter().enumerate() {
assert_eq!(chord.index() as usize, index);
assert_eq!(Chord::from_index(index as u8), *chord);
}
assert_eq!(Chord::from_index(200), Chord::None, "an unknown id is one note");
}
#[test]
fn drop_two_lowers_the_second_voice_from_the_top() {
assert_eq!(
notes_of(60, Chord::Maj, Voicing::Drop2, false, Mode::Chromatic),
vec![52, 60, 67]
);
assert_eq!(
notes_of(60, Chord::Maj7, Voicing::Drop2, false, Mode::Chromatic),
vec![55, 60, 64, 71]
);
}
#[test]
fn inversions_lift_the_bottom_voices() {
assert_eq!(
notes_of(60, Chord::Maj, Voicing::First, false, Mode::Chromatic),
vec![64, 67, 72]
);
assert_eq!(
notes_of(60, Chord::Maj, Voicing::Second, false, Mode::Chromatic),
vec![67, 72, 76]
);
}
#[test]
fn root_below_adds_the_bass_double() {
assert_eq!(
notes_of(60, Chord::Maj, Voicing::Close, true, Mode::Chromatic),
vec![48, 60, 64, 67]
);
}
#[test]
fn every_chord_and_voicing_is_playable() {
for &chord in &Chord::ALL {
for &voicing in &Voicing::ALL {
for below in [false, true] {
for &mode in &Mode::ALL {
for root in 24..=96u8 {
let notes = notes_of(root, chord, voicing, below, mode);
assert!(!notes.is_empty(), "{chord:?} produced nothing");
assert!(notes.len() <= MAX_CHORD_NOTES);
let mut seen = notes.clone();
seen.dedup();
assert_eq!(seen, notes, "{chord:?}/{voicing:?} doubled a note");
for window in notes.windows(2) {
assert!(window[0] < window[1], "not ascending");
}
}
}
}
}
}
}
#[test]
fn voicings_preserve_the_pitch_class_set() {
for &chord in &Chord::ALL {
for &mode in &Mode::ALL {
for root in 36..=84u8 {
let classes = |notes: Vec<u8>| {
let mut c: Vec<u8> = notes.iter().map(|n| n % 12).collect();
c.sort_unstable();
c.dedup();
c
};
let close = classes(notes_of(root, chord, Voicing::Close, false, mode));
for &voicing in &Voicing::ALL {
for below in [false, true] {
assert_eq!(
classes(notes_of(root, chord, voicing, below, mode)),
close,
"{chord:?} changed identity under {voicing:?} below={below}"
);
}
}
}
}
}
}
#[test]
fn diatonic_triads_have_the_textbook_qualities_in_every_mode() {
let expected: [(Mode, [Chord; 7]); 7] = [
(Mode::Ionian, [Chord::Maj, Chord::Min, Chord::Min, Chord::Maj, Chord::Maj, Chord::Min, Chord::Dim]),
(Mode::Dorian, [Chord::Min, Chord::Min, Chord::Maj, Chord::Maj, Chord::Min, Chord::Dim, Chord::Maj]),
(Mode::Phrygian, [Chord::Min, Chord::Maj, Chord::Maj, Chord::Min, Chord::Dim, Chord::Maj, Chord::Min]),
(Mode::Lydian, [Chord::Maj, Chord::Maj, Chord::Min, Chord::Dim, Chord::Maj, Chord::Min, Chord::Min]),
(Mode::Mixolydian, [Chord::Maj, Chord::Min, Chord::Dim, Chord::Maj, Chord::Min, Chord::Min, Chord::Maj]),
(Mode::Aeolian, [Chord::Min, Chord::Dim, Chord::Maj, Chord::Min, Chord::Min, Chord::Maj, Chord::Maj]),
(Mode::Locrian, [Chord::Dim, Chord::Maj, Chord::Min, Chord::Min, Chord::Maj, Chord::Maj, Chord::Min]),
];
for tonic in 0..12u8 {
for (mode, qualities) in &expected {
let scale = mode.scale().expect("a mode has a scale");
for (degree, &quality) in qualities.iter().enumerate() {
let root = 60 + i32::from(tonic) + scale[degree];
let root = root as u8;
let mut derived = [0u8; MAX_CHORD_NOTES];
let n = chord_notes(
root, Chord::Diatonic, Voicing::Close, false, *mode, tonic, &mut derived,
);
let mut explicit = [0u8; MAX_CHORD_NOTES];
let m = chord_notes(
root, quality, Voicing::Close, false, *mode, tonic, &mut explicit,
);
assert_eq!(
derived[..n],
explicit[..m],
"{mode:?} degree {} in tonic {tonic} should be {quality:?}",
degree + 1
);
}
}
}
}
#[test]
fn the_seventh_degree_is_half_diminished() {
let mut out = [0u8; MAX_CHORD_NOTES];
let n = chord_notes(71, Chord::Diatonic7, Voicing::Close, false, Mode::Ionian, 0, &mut out);
assert_eq!(&out[..n], &[71, 74, 77, 81], "B D F A is not m7♭5");
}
#[test]
fn the_diatonic_chords_collapse_under_chromatic() {
assert_eq!(
notes_of(60, Chord::Diatonic, Voicing::Close, false, Mode::Chromatic),
notes_of(60, Chord::Maj, Voicing::Close, false, Mode::Chromatic)
);
assert_eq!(
notes_of(60, Chord::Diatonic7, Voicing::Close, false, Mode::Chromatic),
notes_of(60, Chord::Maj7, Voicing::Close, false, Mode::Chromatic)
);
}
#[test]
fn a_borrowed_root_falls_back_to_major() {
assert_eq!(Mode::Ionian.degree_of(61, 0), None);
assert_eq!(
notes_of(61, Chord::Diatonic, Voicing::Close, false, Mode::Ionian),
vec![61, 65, 68]
);
}
#[test]
fn chromatic_walking_is_semitones() {
assert_eq!(Mode::Chromatic.walk(60, 0, 1), 61);
assert_eq!(Mode::Chromatic.walk(60, 0, -1), 59);
assert_eq!(Mode::Chromatic.walk(0, 0, -1), 0, "the bottom of the range holds");
assert_eq!(Mode::Chromatic.walk(127, 0, 1), 127);
}
#[test]
fn mode_walking_is_scale_degrees() {
let mut note = 60;
for expected in [62, 64, 65, 67, 69, 71, 72, 74] {
note = Mode::Ionian.walk(note, 0, 1);
assert_eq!(note, expected);
}
let mut note = 60;
for expected in [59, 57, 55, 53, 52, 50, 48] {
note = Mode::Ionian.walk(note, 0, -1);
assert_eq!(note, expected);
}
}
#[test]
fn walking_snaps_a_borrowed_note_onto_the_scale() {
assert_eq!(Mode::Ionian.walk(61, 0, 1), 62, "C# up lands on D");
assert_eq!(Mode::Ionian.walk(61, 0, -1), 60, "C# down lands on C");
}
#[test]
fn every_mode_walks_a_full_octave_in_seven_degrees() {
for &mode in &Mode::ALL {
if mode == Mode::Chromatic {
continue;
}
for tonic in 0..12u8 {
let start = 60 + tonic;
let start = mode.walk(start, tonic, 0);
let mut note = start;
for _ in 0..7 {
note = mode.walk(note, tonic, 1);
}
assert_eq!(note, start + 12, "{mode:?} in {tonic} did not close");
}
}
}
#[test]
fn switch_boundaries_are_the_next_grid_line() {
let pattern = 3840;
assert_eq!(SwitchQuant::Immediate.boundary(1234, pattern), 1234);
assert_eq!(SwitchQuant::Beat.boundary(1234, pattern), 1920);
assert_eq!(SwitchQuant::Bar.boundary(1234, pattern), 3840);
assert_eq!(SwitchQuant::PatternEnd.boundary(1234, pattern), 3840);
assert_eq!(SwitchQuant::PatternEnd.boundary(4000, 2880), 5760);
}
#[test]
fn a_boundary_already_reached_is_the_answer() {
assert_eq!(SwitchQuant::Bar.boundary(3840, 3840), 3840);
assert_eq!(SwitchQuant::Beat.boundary(960, 3840), 960);
assert_eq!(SwitchQuant::PatternEnd.boundary(0, 3840), 0);
}
const TPS: f64 = 120.0 * 960.0 / (60.0 * 44_100.0);
fn window(position: i64, frames: u32, previous: Option<PlaybackWindow>) -> PlaybackWindow {
PlaybackWindow::for_block(position, frames, TPS, None, previous)
}
#[test]
fn the_first_window_starts_where_the_transport_is() {
let w = window(1000, 512, None);
assert_eq!(w.from(), 1000);
assert!(!w.is_continuous(), "there is nothing for it to continue from");
}
#[test]
fn a_window_continues_from_the_last_one_across_a_rounding_gap() {
let first = window(0, 470, None);
let span = first.to();
let second = window(span + 1, 470, Some(first));
assert_eq!(second.from(), span, "a tick of song time was skipped");
assert!(second.is_continuous());
assert_eq!(second.to(), span + 1 + span);
}
#[test]
fn a_jump_breaks_continuity() {
let first = window(0, 512, None);
let jumped = window(100_000, 512, Some(first));
assert_eq!(jumped.from(), 100_000);
assert!(!jumped.is_continuous());
}
#[test]
fn a_loop_wrap_starts_the_window_at_the_loop_point() {
let previous = PlaybackWindow::for_block(3800, 512, TPS, Some((0, 3840)), None);
let wrapped = PlaybackWindow::for_block(3, 512, TPS, Some((0, 3840)), Some(previous));
assert_eq!(wrapped.from(), 0);
assert!(!wrapped.is_continuous());
}
#[test]
fn a_window_never_reaches_past_the_loop_end() {
let w = PlaybackWindow::for_block(3830, 4096, TPS, Some((0, 3840)), None);
assert_eq!(w.to(), 3840);
assert!(!w.contains(3840));
}
#[test]
fn sample_offsets_come_from_ticks_and_nothing_else() {
let w = window(1000, 512, None);
assert_eq!(w.sample_offset(1000), 0);
assert_eq!(w.sample_offset(999), 0, "before the window is the first sample");
assert_eq!(w.sample_offset(1000 + 22), (22.0 / TPS) as u32);
assert_eq!(w.sample_offset(i64::MAX), 511, "past the block is the last sample");
}
#[test]
fn a_zero_length_block_has_no_samples_to_land_on() {
let w = window(0, 0, None);
assert_eq!(w.sample_offset(1000), 0);
}
fn player_with(slot0: PatternBlock, slot1: PatternBlock) -> PatternPlayer {
let mut player = PatternPlayer::new();
player.apply(1, slot1);
player.apply(0, slot0);
player
}
fn run_player(
player: &mut PatternPlayer,
start: i64,
frames: u32,
until: i64,
) -> Vec<PatternEvent> {
let mut out = Vec::new();
let mut position = start;
let mut previous = None;
while position < until {
let w = window(position, frames, previous);
player.render(&w, true, &mut out);
position = w.to();
previous = Some(w);
}
out
}
fn tick_player(
player: &mut PatternPlayer,
position: i64,
frames: u32,
previous: Option<PlaybackWindow>,
) -> (PlaybackWindow, Vec<PatternEvent>) {
let w = window(position, frames, previous);
let mut out = Vec::new();
player.render(&w, true, &mut out);
(w, out)
}
#[test]
fn a_stopped_transport_produces_nothing_and_then_flushes_once() {
let mut player = player_with(drum_pattern(16), PatternBlock::empty());
let (w, events) = tick_player(&mut player, 0, 512, None);
assert!(!events.is_empty());
assert!(player.held_notes() > 0);
let mut out = Vec::new();
player.render(&w, false, &mut out);
assert_eq!(out.len(), 1, "the sounding note was not turned off");
assert_eq!(out[0].status, 0x80);
assert_eq!(player.held_notes(), 0);
let mut again = Vec::new();
player.render(&w, false, &mut again);
assert!(again.is_empty(), "the flush repeated");
}
#[test]
fn a_pattern_switch_ends_the_old_notes_before_starting_the_new_ones() {
let mut a = drum_pattern(16);
a.lanes[0].steps[15].gate = Step::TIE;
let mut b = drum_pattern(16);
b.lanes[0] = Lane::drum(42);
for step in &mut b.lanes[0].steps {
step.on = true;
}
let mut player = player_with(a, b);
let mut queue = a;
queue.pending_slot = Some(1);
player.apply(0, queue);
assert_eq!(player.countdown(3600), Some((1, 1)), "one step to go");
let out = run_player(&mut player, 3500, 512, 3900);
let at_boundary: Vec<(u8, u8)> = out
.iter()
.filter(|e| e.tick == 3840)
.map(|e| (e.status, e.data1))
.collect();
assert_eq!(
at_boundary,
vec![(0x80, 36), (0x90, 42)],
"the old note has to be ended before the new one starts"
);
assert_eq!(player.live_slot(), 1);
assert_eq!(player.queued_slot(), None);
}
#[test]
fn an_immediate_switch_takes_effect_at_the_start_of_the_block() {
let a = drum_pattern(16);
let mut b = drum_pattern(16);
b.lanes[0] = Lane::drum(42);
for step in &mut b.lanes[0].steps {
step.on = true;
}
let mut player = player_with(a, b);
let mut queued = a;
queued.pending_slot = Some(1);
queued.switch_quant = SwitchQuant::Immediate;
player.apply(0, queued);
let w = window(480, 512, None);
let mut out = Vec::new();
player.render(&w, true, &mut out);
assert_eq!(player.live_slot(), 1);
let first = out.iter().find(|e| e.is_note_on()).expect("a note");
assert_eq!(first.data1, 42, "the old pattern played after an immediate switch");
assert_eq!(first.tick, 480);
}
#[test]
fn a_beat_quantized_switch_splits_the_block_at_the_beat() {
let a = drum_pattern(16);
let mut b = drum_pattern(16);
b.lanes[0] = Lane::drum(42);
for step in &mut b.lanes[0].steps {
step.on = true;
}
let mut player = player_with(a, b);
let mut queued = a;
queued.pending_slot = Some(1);
queued.switch_quant = SwitchQuant::Beat;
player.apply(0, queued);
let out = run_player(&mut player, 700, 512, 1100);
let switched: Vec<(i64, u8)> = out
.iter()
.filter(|e| e.is_note_on())
.map(|e| (e.tick, e.data1))
.collect();
assert_eq!(
switched,
vec![(720, 36), (960, 42)],
"the switch did not land on the beat"
);
assert_eq!(player.live_slot(), 1);
}
#[test]
fn queueing_the_live_slot_does_nothing() {
let mut block = drum_pattern(16);
block.pending_slot = Some(0);
let player = player_with(block, PatternBlock::empty());
assert_eq!(player.queued_slot(), None);
assert_eq!(player.countdown(0), None);
}
#[test]
fn a_chain_is_derived_from_the_position() {
let a = drum_pattern(16);
let mut b = drum_pattern(16);
b.lanes[0] = Lane::drum(42);
for step in &mut b.lanes[0].steps {
step.on = true;
}
let mut chained = a;
chained.chain[0] = ChainEntry { slot: 0, repeats: 2 };
chained.chain[1] = ChainEntry { slot: 1, repeats: 1 };
chained.chain_len = 2;
let mut player = PatternPlayer::new();
player.apply(1, b);
player.apply(0, chained);
let cycle = 3840;
for (position, expected) in [
(0, 36),
(cycle, 36),
(cycle * 2, 42),
(cycle * 3, 36),
(cycle * 5, 42),
] {
let mut out = Vec::new();
let w = window(position, 512, None);
player.render(&w, true, &mut out);
let first = out.iter().find(|e| e.is_note_on()).expect("a note");
assert_eq!(first.data1, expected, "wrong chain entry at tick {position}");
}
}
#[test]
fn a_chain_advance_ends_the_notes_it_replaces() {
let mut a = drum_pattern(16);
a.lanes[0].steps[15].gate = Step::TIE;
let mut b = drum_pattern(16);
b.lanes[0] = Lane::drum(42);
b.lanes[0].steps[0].on = true;
let mut chained = a;
chained.chain[0] = ChainEntry { slot: 0, repeats: 1 };
chained.chain[1] = ChainEntry { slot: 1, repeats: 1 };
chained.chain_len = 2;
let mut player = PatternPlayer::new();
player.apply(1, b);
player.apply(0, chained);
let out = run_player(&mut player, 3500, 512, 3900);
let at_boundary: Vec<(u8, u8)> = out
.iter()
.filter(|e| e.tick == 3840)
.map(|e| (e.status, e.data1))
.collect();
assert_eq!(at_boundary, vec![(0x80, 36), (0x90, 42)]);
}
#[test]
fn a_bounced_cycle_is_tick_identical_to_live_playback() {
for swing in [50u8, 58, 62, 75] {
for rate in Rate::ALL {
let mut block = drum_pattern(16);
block.rate = rate;
block.swing = swing;
block.lanes[0].steps[3].gate = 150;
block.lanes[0].steps[7].gate = Step::TIE;
block.lanes[0].steps[9].accent = true;
let mut bounced = Vec::new();
compile_cycle(&block, 0, &mut bounced);
let cycle = block.length_ticks();
let mut live = Vec::new();
let mut pending = PendingOffs::new();
let mut from = 0;
while from < cycle {
let to = (from + 97).min(cycle);
generate(&block, 0, from, to, &mut pending, &mut live);
from = to;
}
pending.flush(cycle, &mut live);
live.sort_by_key(|e| e.tick);
let key = |e: &PatternEvent| (e.tick, e.status, e.data1, e.data2);
let bounced: Vec<_> = bounced.iter().map(key).collect();
let live: Vec<_> = live.iter().map(key).collect();
assert_eq!(bounced, live, "swing {swing} at {}", rate.label());
}
}
}
#[test]
fn rendering_a_pattern_does_not_allocate() {
let mut a = drum_pattern(16);
a.lanes[0].steps[15].gate = Step::TIE;
let mut b = drum_pattern(16);
b.lanes[0] = Lane::drum(42);
let mut player = Box::new(player_with(a, b));
let mut sink = Vec::with_capacity(1024);
let mut queued = a;
queued.pending_slot = Some(1);
let mut w = window(0, 512, None);
player.render(&w, true, &mut sink);
let allocations = crate::alloc_count::allocations_during(|| {
let mut position = 0;
for block in 0..64 {
w = window(position, 512, Some(w));
sink.clear();
player.render(&w, true, &mut sink);
if block == 8 {
player.apply(0, queued);
}
position = w.to();
}
});
assert_eq!(allocations, 0, "the pattern player reached the allocator");
}
#[test]
fn a_full_sink_stops_the_generator() {
struct Capped(Vec<PatternEvent>, usize);
impl EventSink for Capped {
fn accept(&mut self, event: PatternEvent) -> bool {
if self.0.len() >= self.1 {
return false;
}
self.0.push(event);
true
}
}
let block = drum_pattern(16);
let mut sink = Capped(Vec::new(), 3);
let mut pending = PendingOffs::new();
generate(&block, 0, 0, block.length_ticks(), &mut pending, &mut sink);
assert_eq!(sink.0.len(), 3, "the sink was written past its cap");
}
}