Skip to main content

phosphor_app/sequencer/
compile.rs

1//! Bounce: a pattern, or a whole chain, written out as a clip.
2//!
3//! # Why it is one function call
4//!
5//! The bounce does not re-implement the sequencer. It calls
6//! [`phosphor_core::pattern::compile_cycle`], which is the generator the audio
7//! thread runs, with a `Vec` for a sink instead of a track's event queue.
8//! Swing, gates, ties, accents, chords and lane mutes are therefore not "the
9//! same as" live playback — they *are* live playback, and there is no second
10//! copy of the arithmetic to drift.
11//!
12//! # Where it lands
13//!
14//! At the next free bar at or after the playhead. Two clips overlapping on
15//! one track is a position the rest of the application has no meaning for, so
16//! the bounce looks for a gap rather than making one; the caller is told which
17//! bar it chose so the status line can say so.
18//!
19//! # And it stops the pattern
20//!
21//! A bounced clip and the pattern that produced it play the same notes at the
22//! same ticks, so leaving both running is a doubled part — every note a flam
23//! against itself, which sounds wrong in a way that is hard to attribute.
24//! [`Bounce::stops_playback`] says so, and the caller acts on it.
25
26use phosphor_core::clip::{ClipEvent, MidiClip, NoteSnapshot};
27use phosphor_core::pattern::compile_cycle;
28use phosphor_core::transport::Transport;
29
30use super::SequencerState;
31use crate::state::Clip;
32
33/// One bar in ticks, 4/4 — the grid a bounce lands on.
34pub const TICKS_PER_BAR: i64 = Transport::PPQ * 4;
35
36/// `ticks` rounded up to a whole number of bars.
37///
38/// Written out rather than `i64::div_ceil`, which is still unstable at this
39/// project's minimum supported Rust version.
40fn bars_covering(ticks: i64) -> i64 {
41    (ticks + TICKS_PER_BAR - 1).div_euclid(TICKS_PER_BAR)
42}
43
44/// The first bar line at or after `tick`.
45fn bar_at_or_after(tick: i64) -> i64 {
46    bars_covering(tick.max(0)) * TICKS_PER_BAR
47}
48
49/// What a bounce produced.
50#[derive(Debug, Clone, PartialEq)]
51pub struct Bounce {
52    /// Where on the timeline it goes.
53    pub start_tick: i64,
54    /// How long it is.
55    pub length_ticks: i64,
56    /// The notes, in tick order, relative to `start_tick`.
57    pub events: Vec<ClipEvent>,
58    /// Whether the sequencer on this track was running and now has to stop.
59    pub stops_playback: bool,
60}
61
62impl Bounce {
63    /// The bar number a player would call this, counting from one.
64    #[must_use]
65    pub fn bar(&self) -> i64 {
66        self.start_tick / TICKS_PER_BAR + 1
67    }
68
69    /// How many bars long it is, rounded up — a 12-step pattern is not a
70    /// whole number of them.
71    #[must_use]
72    pub fn bars(&self) -> i64 {
73        bars_covering(self.length_ticks).max(1)
74    }
75
76    /// The notes as the piano roll holds them.
77    #[must_use]
78    pub fn notes(&self) -> Vec<NoteSnapshot> {
79        let clip = MidiClip::new(self.start_tick, self.length_ticks, self.events.clone());
80        phosphor_core::clip::ClipSnapshot::from_clip(0, 0, &clip).notes
81    }
82}
83
84/// Compile the pattern under the editor, one time through.
85#[must_use]
86pub fn bounce_pattern(state: &SequencerState, playhead: i64, clips: &[Clip]) -> Option<Bounce> {
87    let block = state.block(state.selected_slot() as usize);
88    let mut events = Vec::new();
89    compile_cycle(&block, 0, &mut events);
90    finish(state, events, block.length_ticks(), playhead, clips)
91}
92
93/// Compile the whole chain, repeats expanded, one time through.
94///
95/// Falls back to the pattern under the editor when there is no chain, so that
96/// the command means something on every track.
97#[must_use]
98pub fn bounce_chain(state: &SequencerState, playhead: i64, clips: &[Clip]) -> Option<Bounce> {
99    if !state.is_chained() {
100        return bounce_pattern(state, playhead, clips);
101    }
102
103    let mut events = Vec::new();
104    let mut origin = 0i64;
105    for entry in state.chain() {
106        let block = state.block(entry.slot as usize);
107        // Each repeat is compiled at its own origin rather than the whole
108        // entry at once, because that is what the audio thread does with it:
109        // a pattern that repeats starts again from step zero.
110        for _ in 0..entry.repeats.max(1) {
111            compile_cycle(&block, origin, &mut events);
112            origin += block.length_ticks();
113        }
114    }
115    events.sort_by_key(|e| e.tick);
116    finish(state, events, origin, playhead, clips)
117}
118
119fn finish(
120    state: &SequencerState,
121    events: Vec<phosphor_core::pattern::PatternEvent>,
122    length_ticks: i64,
123    playhead: i64,
124    clips: &[Clip],
125) -> Option<Bounce> {
126    if events.is_empty() || length_ticks <= 0 {
127        return None;
128    }
129    Some(Bounce {
130        start_tick: next_free_bar(clips, playhead, length_ticks),
131        length_ticks,
132        events: events
133            .into_iter()
134            .map(|e| ClipEvent { tick: e.tick, status: e.status, data1: e.data1, data2: e.data2 })
135            .collect(),
136        stops_playback: state.is_playing(),
137    })
138}
139
140/// The first bar line at or after `playhead` where a clip `length` ticks long
141/// fits between the clips already on the track.
142///
143/// Bar-aligned because a bounce is a bar of music and a player is going to
144/// want it lined up with the rest of them; searched rather than assumed
145/// because writing a clip on top of another one produces a track state
146/// nothing else in the application knows how to draw or play.
147#[must_use]
148pub fn next_free_bar(clips: &[Clip], playhead: i64, length: i64) -> i64 {
149    let length = length.max(1);
150    let mut start = bar_at_or_after(playhead);
151
152    // Bounded: each step past an occupied bar moves the candidate to the end
153    // of the clip that blocked it, so the search visits each clip once at
154    // most, and the `+1` guarantees forward progress even on a clip of no
155    // length.
156    for _ in 0..=clips.len() {
157        let end = start + length;
158        let blocker = clips
159            .iter()
160            .filter(|c| c.start_tick < end && c.start_tick + c.length_ticks.max(1) > start)
161            .map(|c| c.start_tick + c.length_ticks.max(1))
162            .max();
163        match blocker {
164            Some(after) => start = bar_at_or_after(after.max(start + 1)),
165            None => return start,
166        }
167    }
168    start
169}
170
171#[cfg(test)]
172mod tests {
173    use super::super::ops::{dispatch, SeqOp};
174    use super::super::tests::drum_track;
175    use super::*;
176    use crate::state::TrackState;
177
178    fn clip(start_tick: i64, length_ticks: i64) -> Clip {
179        Clip {
180            number: 1,
181            width: 4,
182            has_content: true,
183            start_tick,
184            length_ticks,
185            notes: Vec::new(),
186            hidden_notes: Vec::new(),
187        }
188    }
189
190    fn four_on_the_floor() -> TrackState {
191        let mut track = drum_track();
192        for step in [0usize, 4, 8, 12] {
193            dispatch(&mut track, SeqOp::SelectStep(step as u8));
194            dispatch(&mut track, SeqOp::ToggleStep);
195        }
196        track
197    }
198
199    /// One time through, and every hit accounted for at the tick the pattern
200    /// would have played it.
201    #[test]
202    fn a_bounce_is_one_cycle_of_the_pattern() {
203        let track = four_on_the_floor();
204        let bounce = bounce_pattern(track.sequencer.as_ref().unwrap(), 0, &[]).unwrap();
205
206        assert_eq!(bounce.length_ticks, 3840);
207        assert_eq!(bounce.bars(), 1);
208        assert_eq!(bounce.bar(), 1);
209        let ons: Vec<i64> = bounce
210            .events
211            .iter()
212            .filter(|e| e.status == 0x90 && e.data2 > 0)
213            .map(|e| e.tick)
214            .collect();
215        assert_eq!(ons, vec![0, 960, 1920, 2880]);
216        // Every note ends.
217        assert_eq!(bounce.events.iter().filter(|e| e.status == 0x80).count(), 4);
218        assert_eq!(bounce.notes().len(), 4);
219    }
220
221    /// Swing is not applied again by the bounce; it comes out of the same
222    /// generator, so the offsets are the pattern's own.
223    #[test]
224    fn a_bounce_carries_the_patterns_swing() {
225        let mut track = drum_track();
226        for step in 0..8u8 {
227            dispatch(&mut track, SeqOp::SelectStep(step));
228            dispatch(&mut track, SeqOp::ToggleStep);
229        }
230        dispatch(&mut track, SeqOp::NudgeSwing(12)); // 62%
231
232        let bounce = bounce_pattern(track.sequencer.as_ref().unwrap(), 0, &[]).unwrap();
233        let ons: Vec<i64> = bounce
234            .events
235            .iter()
236            .filter(|e| e.status == 0x90 && e.data2 > 0)
237            .map(|e| e.tick)
238            .collect();
239        // Odd steps 57 ticks late: (62 - 50) * 2 * 240 / 100.
240        assert_eq!(ons, vec![0, 297, 480, 777, 960, 1257, 1440, 1737]);
241    }
242
243    /// A chain bounces as it plays: entries in order, repeats expanded, each
244    /// time through starting from step zero.
245    #[test]
246    fn a_chain_bounces_with_its_repeats_expanded() {
247        let mut track = four_on_the_floor();
248        dispatch(&mut track, SeqOp::SelectSlot(1));
249        dispatch(&mut track, SeqOp::SelectStep(2));
250        dispatch(&mut track, SeqOp::ToggleStep);
251        dispatch(&mut track, SeqOp::PushChainEntry { slot: 0, repeats: 2 });
252        dispatch(&mut track, SeqOp::PushChainEntry { slot: 1, repeats: 1 });
253
254        let state = track.sequencer.as_ref().unwrap();
255        let bounce = bounce_chain(state, 0, &[]).unwrap();
256        assert_eq!(bounce.length_ticks, 3840 * 3);
257        assert_eq!(bounce.bars(), 3);
258
259        let ons: Vec<i64> = bounce
260            .events
261            .iter()
262            .filter(|e| e.status == 0x90 && e.data2 > 0)
263            .map(|e| e.tick)
264            .collect();
265        assert_eq!(
266            ons,
267            vec![0, 960, 1920, 2880, 3840, 4800, 5760, 6720, 7680 + 480],
268            "two times through A, then one of B"
269        );
270    }
271
272    /// With no chain, the chain bounce is the pattern bounce — the command
273    /// has to mean something on every track.
274    #[test]
275    fn bouncing_a_chain_that_is_not_there_bounces_the_pattern() {
276        let track = four_on_the_floor();
277        let state = track.sequencer.as_ref().unwrap();
278        assert_eq!(bounce_chain(state, 0, &[]), bounce_pattern(state, 0, &[]));
279    }
280
281    /// An empty pattern produces nothing rather than an empty clip nobody
282    /// asked for.
283    #[test]
284    fn an_empty_pattern_bounces_to_nothing() {
285        let track = drum_track();
286        assert!(bounce_pattern(track.sequencer.as_ref().unwrap(), 0, &[]).is_none());
287    }
288
289    /// A running sequencer has to stop, because the clip and the pattern
290    /// would otherwise play the same notes at the same ticks.
291    #[test]
292    fn a_bounce_says_when_it_has_to_stop_the_pattern() {
293        // A fresh sequencer runs by default, so bouncing it stops playback.
294        let mut track = four_on_the_floor();
295        let state = track.sequencer.as_ref().unwrap();
296        assert!(bounce_pattern(state, 0, &[]).unwrap().stops_playback);
297
298        dispatch(&mut track, SeqOp::SetPlaying(false));
299        let state = track.sequencer.as_ref().unwrap();
300        assert!(!bounce_pattern(state, 0, &[]).unwrap().stops_playback);
301    }
302
303    // ── Placement ──
304
305    #[test]
306    fn a_bounce_lands_on_a_bar_line_at_or_after_the_playhead() {
307        assert_eq!(next_free_bar(&[], 0, 3840), 0);
308        assert_eq!(next_free_bar(&[], 1, 3840), 3840);
309        assert_eq!(next_free_bar(&[], 3840, 3840), 3840);
310        assert_eq!(next_free_bar(&[], 3841, 3840), 7680);
311        assert_eq!(next_free_bar(&[], -500, 3840), 0);
312    }
313
314    /// Never on top of a clip that is already there: two overlapping clips on
315    /// one track is a position the rest of the application has no meaning
316    /// for.
317    #[test]
318    fn a_bounce_never_lands_on_a_clip_that_is_already_there() {
319        let occupied = [clip(0, 3840), clip(3840, 3840)];
320        assert_eq!(next_free_bar(&occupied, 0, 3840), 7680);
321
322        // A gap that is big enough gets used.
323        let gap = [clip(0, 3840), clip(7680, 3840)];
324        assert_eq!(next_free_bar(&gap, 0, 3840), 3840);
325
326        // A gap that is not big enough does not.
327        assert_eq!(next_free_bar(&gap, 0, 3840 * 2), 11_520);
328    }
329
330    /// The search terminates whatever it is given, including clips of no
331    /// length and clips out of order.
332    #[test]
333    fn the_search_for_a_free_bar_terminates() {
334        let awkward = [clip(7680, 0), clip(0, 1), clip(3840, 100_000), clip(0, 3840)];
335        let found = next_free_bar(&awkward, 0, 3840);
336        assert_eq!(found % TICKS_PER_BAR, 0);
337        assert!(found >= 103_840);
338    }
339}