use phosphor_core::clip::{ClipEvent, MidiClip, NoteSnapshot};
use phosphor_core::pattern::compile_cycle;
use phosphor_core::transport::Transport;
use super::SequencerState;
use crate::state::Clip;
pub const TICKS_PER_BAR: i64 = Transport::PPQ * 4;
fn bars_covering(ticks: i64) -> i64 {
(ticks + TICKS_PER_BAR - 1).div_euclid(TICKS_PER_BAR)
}
fn bar_at_or_after(tick: i64) -> i64 {
bars_covering(tick.max(0)) * TICKS_PER_BAR
}
#[derive(Debug, Clone, PartialEq)]
pub struct Bounce {
pub start_tick: i64,
pub length_ticks: i64,
pub events: Vec<ClipEvent>,
pub stops_playback: bool,
}
impl Bounce {
#[must_use]
pub fn bar(&self) -> i64 {
self.start_tick / TICKS_PER_BAR + 1
}
#[must_use]
pub fn bars(&self) -> i64 {
bars_covering(self.length_ticks).max(1)
}
#[must_use]
pub fn notes(&self) -> Vec<NoteSnapshot> {
let clip = MidiClip::new(self.start_tick, self.length_ticks, self.events.clone());
phosphor_core::clip::ClipSnapshot::from_clip(0, 0, &clip).notes
}
}
#[must_use]
pub fn bounce_pattern(state: &SequencerState, playhead: i64, clips: &[Clip]) -> Option<Bounce> {
let block = state.block(state.selected_slot() as usize);
let mut events = Vec::new();
compile_cycle(&block, 0, &mut events);
finish(state, events, block.length_ticks(), playhead, clips)
}
#[must_use]
pub fn bounce_chain(state: &SequencerState, playhead: i64, clips: &[Clip]) -> Option<Bounce> {
if !state.is_chained() {
return bounce_pattern(state, playhead, clips);
}
let mut events = Vec::new();
let mut origin = 0i64;
for entry in state.chain() {
let block = state.block(entry.slot as usize);
for _ in 0..entry.repeats.max(1) {
compile_cycle(&block, origin, &mut events);
origin += block.length_ticks();
}
}
events.sort_by_key(|e| e.tick);
finish(state, events, origin, playhead, clips)
}
fn finish(
state: &SequencerState,
events: Vec<phosphor_core::pattern::PatternEvent>,
length_ticks: i64,
playhead: i64,
clips: &[Clip],
) -> Option<Bounce> {
if events.is_empty() || length_ticks <= 0 {
return None;
}
Some(Bounce {
start_tick: next_free_bar(clips, playhead, length_ticks),
length_ticks,
events: events
.into_iter()
.map(|e| ClipEvent { tick: e.tick, status: e.status, data1: e.data1, data2: e.data2 })
.collect(),
stops_playback: state.is_playing(),
})
}
#[must_use]
pub fn next_free_bar(clips: &[Clip], playhead: i64, length: i64) -> i64 {
let length = length.max(1);
let mut start = bar_at_or_after(playhead);
for _ in 0..=clips.len() {
let end = start + length;
let blocker = clips
.iter()
.filter(|c| c.start_tick < end && c.start_tick + c.length_ticks.max(1) > start)
.map(|c| c.start_tick + c.length_ticks.max(1))
.max();
match blocker {
Some(after) => start = bar_at_or_after(after.max(start + 1)),
None => return start,
}
}
start
}
#[cfg(test)]
mod tests {
use super::super::ops::{dispatch, SeqOp};
use super::super::tests::drum_track;
use super::*;
use crate::state::TrackState;
fn clip(start_tick: i64, length_ticks: i64) -> Clip {
Clip {
number: 1,
width: 4,
has_content: true,
start_tick,
length_ticks,
notes: Vec::new(),
hidden_notes: Vec::new(),
}
}
fn four_on_the_floor() -> TrackState {
let mut track = drum_track();
for step in [0usize, 4, 8, 12] {
dispatch(&mut track, SeqOp::SelectStep(step as u8));
dispatch(&mut track, SeqOp::ToggleStep);
}
track
}
#[test]
fn a_bounce_is_one_cycle_of_the_pattern() {
let track = four_on_the_floor();
let bounce = bounce_pattern(track.sequencer.as_ref().unwrap(), 0, &[]).unwrap();
assert_eq!(bounce.length_ticks, 3840);
assert_eq!(bounce.bars(), 1);
assert_eq!(bounce.bar(), 1);
let ons: Vec<i64> = bounce
.events
.iter()
.filter(|e| e.status == 0x90 && e.data2 > 0)
.map(|e| e.tick)
.collect();
assert_eq!(ons, vec![0, 960, 1920, 2880]);
assert_eq!(bounce.events.iter().filter(|e| e.status == 0x80).count(), 4);
assert_eq!(bounce.notes().len(), 4);
}
#[test]
fn a_bounce_carries_the_patterns_swing() {
let mut track = drum_track();
for step in 0..8u8 {
dispatch(&mut track, SeqOp::SelectStep(step));
dispatch(&mut track, SeqOp::ToggleStep);
}
dispatch(&mut track, SeqOp::NudgeSwing(12));
let bounce = bounce_pattern(track.sequencer.as_ref().unwrap(), 0, &[]).unwrap();
let ons: Vec<i64> = bounce
.events
.iter()
.filter(|e| e.status == 0x90 && e.data2 > 0)
.map(|e| e.tick)
.collect();
assert_eq!(ons, vec![0, 297, 480, 777, 960, 1257, 1440, 1737]);
}
#[test]
fn a_chain_bounces_with_its_repeats_expanded() {
let mut track = four_on_the_floor();
dispatch(&mut track, SeqOp::SelectSlot(1));
dispatch(&mut track, SeqOp::SelectStep(2));
dispatch(&mut track, SeqOp::ToggleStep);
dispatch(&mut track, SeqOp::PushChainEntry { slot: 0, repeats: 2 });
dispatch(&mut track, SeqOp::PushChainEntry { slot: 1, repeats: 1 });
let state = track.sequencer.as_ref().unwrap();
let bounce = bounce_chain(state, 0, &[]).unwrap();
assert_eq!(bounce.length_ticks, 3840 * 3);
assert_eq!(bounce.bars(), 3);
let ons: Vec<i64> = bounce
.events
.iter()
.filter(|e| e.status == 0x90 && e.data2 > 0)
.map(|e| e.tick)
.collect();
assert_eq!(
ons,
vec![0, 960, 1920, 2880, 3840, 4800, 5760, 6720, 7680 + 480],
"two times through A, then one of B"
);
}
#[test]
fn bouncing_a_chain_that_is_not_there_bounces_the_pattern() {
let track = four_on_the_floor();
let state = track.sequencer.as_ref().unwrap();
assert_eq!(bounce_chain(state, 0, &[]), bounce_pattern(state, 0, &[]));
}
#[test]
fn an_empty_pattern_bounces_to_nothing() {
let track = drum_track();
assert!(bounce_pattern(track.sequencer.as_ref().unwrap(), 0, &[]).is_none());
}
#[test]
fn a_bounce_says_when_it_has_to_stop_the_pattern() {
let mut track = four_on_the_floor();
let state = track.sequencer.as_ref().unwrap();
assert!(!bounce_pattern(state, 0, &[]).unwrap().stops_playback);
dispatch(&mut track, SeqOp::SetPlaying(true));
let state = track.sequencer.as_ref().unwrap();
assert!(bounce_pattern(state, 0, &[]).unwrap().stops_playback);
}
#[test]
fn a_bounce_lands_on_a_bar_line_at_or_after_the_playhead() {
assert_eq!(next_free_bar(&[], 0, 3840), 0);
assert_eq!(next_free_bar(&[], 1, 3840), 3840);
assert_eq!(next_free_bar(&[], 3840, 3840), 3840);
assert_eq!(next_free_bar(&[], 3841, 3840), 7680);
assert_eq!(next_free_bar(&[], -500, 3840), 0);
}
#[test]
fn a_bounce_never_lands_on_a_clip_that_is_already_there() {
let occupied = [clip(0, 3840), clip(3840, 3840)];
assert_eq!(next_free_bar(&occupied, 0, 3840), 7680);
let gap = [clip(0, 3840), clip(7680, 3840)];
assert_eq!(next_free_bar(&gap, 0, 3840), 3840);
assert_eq!(next_free_bar(&gap, 0, 3840 * 2), 11_520);
}
#[test]
fn the_search_for_a_free_bar_terminates() {
let awkward = [clip(7680, 0), clip(0, 1), clip(3840, 100_000), clip(0, 3840)];
let found = next_free_bar(&awkward, 0, 3840);
assert_eq!(found % TICKS_PER_BAR, 0);
assert!(found >= 103_840);
}
}