Compare commits
4 Commits
6d71c64a34
...
dacc9bd6be
| Author | SHA1 | Date | |
|---|---|---|---|
| dacc9bd6be | |||
| bfd52c0053 | |||
| 12172ce1e8 | |||
| 1513d80a8d |
@@ -14,4 +14,4 @@ pub const MAX_STEPS: usize = 1024;
|
||||
pub const DEFAULT_LENGTH: usize = 16;
|
||||
|
||||
pub use file::{load, load_str, save, FileError};
|
||||
pub use project::{Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project, Step, SyncMode};
|
||||
pub use project::{Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project, Step};
|
||||
|
||||
@@ -206,39 +206,6 @@ impl LaunchQuantization {
|
||||
}
|
||||
}
|
||||
|
||||
/// How a pattern synchronizes when launched: restart or phase-lock.
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||
pub enum SyncMode {
|
||||
#[default]
|
||||
Reset,
|
||||
PhaseLock,
|
||||
}
|
||||
|
||||
impl SyncMode {
|
||||
/// Human-readable label for display.
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Reset => "Reset",
|
||||
Self::PhaseLock => "Phase-Lock",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn short_label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Reset => "Rst",
|
||||
Self::PhaseLock => "Plk",
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle between Reset and PhaseLock.
|
||||
pub fn toggle(&self) -> Self {
|
||||
match self {
|
||||
Self::Reset => Self::PhaseLock,
|
||||
Self::PhaseLock => Self::Reset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What happens when a pattern finishes: loop, stop, or chain to another.
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||
pub enum FollowUp {
|
||||
@@ -315,7 +282,7 @@ impl Default for Step {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sequence of steps with playback settings (speed, quantization, sync, follow-up).
|
||||
/// Sequence of steps with playback settings (speed, quantization, follow-up).
|
||||
#[derive(Clone)]
|
||||
pub struct Pattern {
|
||||
pub steps: Vec<Step>,
|
||||
@@ -324,7 +291,6 @@ pub struct Pattern {
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub quantization: LaunchQuantization,
|
||||
pub sync_mode: SyncMode,
|
||||
pub follow_up: FollowUp,
|
||||
}
|
||||
|
||||
@@ -361,8 +327,6 @@ struct SparsePattern {
|
||||
description: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "is_default_quantization")]
|
||||
quantization: LaunchQuantization,
|
||||
#[serde(default, skip_serializing_if = "is_default_sync_mode")]
|
||||
sync_mode: SyncMode,
|
||||
#[serde(default, skip_serializing_if = "is_default_follow_up")]
|
||||
follow_up: FollowUp,
|
||||
}
|
||||
@@ -371,10 +335,6 @@ fn is_default_quantization(q: &LaunchQuantization) -> bool {
|
||||
*q == LaunchQuantization::default()
|
||||
}
|
||||
|
||||
fn is_default_sync_mode(s: &SyncMode) -> bool {
|
||||
*s == SyncMode::default()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LegacyPattern {
|
||||
steps: Vec<Step>,
|
||||
@@ -388,8 +348,6 @@ struct LegacyPattern {
|
||||
#[serde(default)]
|
||||
quantization: LaunchQuantization,
|
||||
#[serde(default)]
|
||||
sync_mode: SyncMode,
|
||||
#[serde(default)]
|
||||
follow_up: FollowUp,
|
||||
}
|
||||
|
||||
@@ -416,7 +374,6 @@ impl Serialize for Pattern {
|
||||
name: self.name.clone(),
|
||||
description: self.description.clone(),
|
||||
quantization: self.quantization,
|
||||
sync_mode: self.sync_mode,
|
||||
follow_up: self.follow_up,
|
||||
};
|
||||
sparse.serialize(serializer)
|
||||
@@ -452,7 +409,6 @@ impl<'de> Deserialize<'de> for Pattern {
|
||||
name: sparse.name,
|
||||
description: sparse.description,
|
||||
quantization: sparse.quantization,
|
||||
sync_mode: sparse.sync_mode,
|
||||
follow_up: sparse.follow_up,
|
||||
})
|
||||
}
|
||||
@@ -463,7 +419,6 @@ impl<'de> Deserialize<'de> for Pattern {
|
||||
name: legacy.name,
|
||||
description: legacy.description,
|
||||
quantization: legacy.quantization,
|
||||
sync_mode: legacy.sync_mode,
|
||||
follow_up: legacy.follow_up,
|
||||
}),
|
||||
}
|
||||
@@ -479,7 +434,6 @@ impl Default for Pattern {
|
||||
name: None,
|
||||
description: None,
|
||||
quantization: LaunchQuantization::default(),
|
||||
sync_mode: SyncMode::default(),
|
||||
follow_up: FollowUp::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ Each pattern is an independent sequence of steps with its own properties:
|
||||
| Length | Steps before the pattern loops (`1`-`1024`) | `16` |
|
||||
| Speed | Playback rate (`1/8x` to `8x`) | `1x` |
|
||||
| Quantization | When the pattern launches | `Bar` |
|
||||
| Sync Mode | Reset or Phase-Lock on re-trigger | `Reset` |
|
||||
| Follow Up | What happens when the pattern finishes an iteration | `Loop` |
|
||||
|
||||
Press `e` in the patterns view to edit these settings. After editing properties, you will have to hit the `c` key to _launch_ these changes. More about that later!
|
||||
|
||||
@@ -33,7 +33,7 @@ You can also arm mute/solo changes:
|
||||
- Press `Shift+m` to clear all mutes
|
||||
- Press `Shift+x` to clear all solos
|
||||
|
||||
A pattern might not start immediately depending on the sync mode you have chosen.
|
||||
A pattern might not start immediately depending on its quantization setting.
|
||||
It might wait for the next beat/bar boundary.
|
||||
|
||||
## Status Indicators
|
||||
@@ -63,9 +63,4 @@ Launched changes don't execute immediately. They wait for a quantization boundar
|
||||
|
||||
Edit quantization in pattern properties (press `e` on a pattern).
|
||||
|
||||
## Sync Mode
|
||||
|
||||
When a pattern starts, its playback position depends on sync mode:
|
||||
|
||||
- **Reset**: Always start at step 0
|
||||
- **Phase-Lock**: Start at the current beat-aligned position (stays in sync with other patterns)
|
||||
Patterns always start at a beat-aligned position (phase-lock), staying in sync with other running patterns.
|
||||
|
||||
@@ -219,7 +219,6 @@ impl Plugin for CagirePlugin {
|
||||
source: s.source,
|
||||
})
|
||||
.collect(),
|
||||
sync_mode: pat.sync_mode,
|
||||
follow_up: pat.follow_up,
|
||||
};
|
||||
let _ = self.bridge.cmd_tx.send(SeqCommand::PatternUpdate {
|
||||
|
||||
@@ -199,7 +199,6 @@ impl App {
|
||||
length,
|
||||
speed,
|
||||
quantization,
|
||||
sync_mode,
|
||||
follow_up,
|
||||
} => {
|
||||
self.playback.staged_prop_changes.insert(
|
||||
@@ -210,7 +209,6 @@ impl App {
|
||||
length,
|
||||
speed,
|
||||
quantization,
|
||||
sync_mode,
|
||||
follow_up,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -203,7 +203,6 @@ impl App {
|
||||
length: pat.length.to_string(),
|
||||
speed: pat.speed,
|
||||
quantization: pat.quantization,
|
||||
sync_mode: pat.sync_mode,
|
||||
follow_up: pat.follow_up,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -138,7 +138,6 @@ impl App {
|
||||
self.playback.queued_changes.push(StagedChange {
|
||||
change: PatternChange::Start { bank, pattern },
|
||||
quantization: crate::model::LaunchQuantization::Immediate,
|
||||
sync_mode: crate::model::SyncMode::PhaseLock,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ impl App {
|
||||
bank,
|
||||
pattern,
|
||||
quantization: staged.quantization,
|
||||
sync_mode: staged.sync_mode,
|
||||
});
|
||||
}
|
||||
PatternChange::Stop { bank, pattern } => {
|
||||
@@ -68,7 +67,6 @@ impl App {
|
||||
source: s.source,
|
||||
})
|
||||
.collect(),
|
||||
sync_mode: pat.sync_mode,
|
||||
follow_up: pat.follow_up,
|
||||
};
|
||||
let _ = cmd_tx.send(SeqCommand::PatternUpdate {
|
||||
|
||||
@@ -29,7 +29,6 @@ impl App {
|
||||
self.playback.staged_changes.push(StagedChange {
|
||||
change: PatternChange::Stop { bank, pattern },
|
||||
quantization: pattern_data.quantization,
|
||||
sync_mode: pattern_data.sync_mode,
|
||||
});
|
||||
self.ui
|
||||
.set_status(format!("{} armed to stop", bp_label(bank, pattern)));
|
||||
@@ -37,7 +36,6 @@ impl App {
|
||||
self.playback.staged_changes.push(StagedChange {
|
||||
change: PatternChange::Start { bank, pattern },
|
||||
quantization: pattern_data.quantization,
|
||||
sync_mode: pattern_data.sync_mode,
|
||||
});
|
||||
self.ui
|
||||
.set_status(format!("{} armed to play", bp_label(bank, pattern)));
|
||||
@@ -84,7 +82,6 @@ impl App {
|
||||
}
|
||||
pat.speed = props.speed;
|
||||
pat.quantization = props.quantization;
|
||||
pat.sync_mode = props.sync_mode;
|
||||
pat.follow_up = props.follow_up;
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed, SyncMode};
|
||||
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed};
|
||||
use crate::page::Page;
|
||||
use crate::state::{ColorScheme, DeviceKind, Modal, OptionsFocus, PatternField, ScriptField, SettingKind};
|
||||
|
||||
@@ -169,7 +169,6 @@ pub enum AppCommand {
|
||||
length: Option<usize>,
|
||||
speed: PatternSpeed,
|
||||
quantization: LaunchQuantization,
|
||||
sync_mode: SyncMode,
|
||||
follow_up: FollowUp,
|
||||
},
|
||||
|
||||
|
||||
@@ -81,6 +81,20 @@ impl LinkState {
|
||||
self.link.commit_app_session_state(&state);
|
||||
}
|
||||
|
||||
pub fn start_playing(&self, beat: f64, time: i64, quantum: f64) {
|
||||
let mut state = SessionState::new();
|
||||
self.link.capture_app_session_state(&mut state);
|
||||
state.set_is_playing_and_request_beat_at_time(true, time, beat, quantum);
|
||||
self.link.commit_app_session_state(&state);
|
||||
}
|
||||
|
||||
pub fn stop_playing(&self, time: i64) {
|
||||
let mut state = SessionState::new();
|
||||
self.link.capture_app_session_state(&mut state);
|
||||
state.set_is_playing(false, time);
|
||||
self.link.commit_app_session_state(&state);
|
||||
}
|
||||
|
||||
pub fn capture_app_state(&self) -> SessionState {
|
||||
let mut state = SessionState::new();
|
||||
self.link.capture_app_session_state(&mut state);
|
||||
|
||||
@@ -5,7 +5,7 @@ pub mod realtime;
|
||||
pub mod sequencer;
|
||||
mod timing;
|
||||
|
||||
pub use timing::{substeps_in_window, StepTiming, SyncTime};
|
||||
pub use timing::{next_boundary, substeps_in_window, SyncTime};
|
||||
|
||||
pub use audio::{preload_sample_heads, AnalysisHandle, ScopeBuffer, SpectrumBuffer};
|
||||
|
||||
|
||||
@@ -14,11 +14,17 @@ use std::thread::{self, JoinHandle};
|
||||
|
||||
use super::dispatcher::{dispatcher_loop, MidiDispatch, TimedMidiCommand};
|
||||
use super::realtime::set_realtime_priority;
|
||||
use super::{substeps_in_window, LinkState, StepTiming, SyncTime};
|
||||
use super::{next_boundary, substeps_in_window, LinkState, SyncTime};
|
||||
use crate::model::{
|
||||
CcAccess, Dictionary, ExecutionTrace, Rng, ScriptEngine, StepContext, Value, Variables,
|
||||
};
|
||||
use crate::model::{FollowUp, LaunchQuantization, SyncMode, MAX_BANKS, MAX_PATTERNS};
|
||||
use crate::model::{FollowUp, LaunchQuantization, MAX_BANKS, MAX_PATTERNS};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum SyncMode {
|
||||
Reset,
|
||||
PhaseLock,
|
||||
}
|
||||
use crate::state::LiveKeyState;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
@@ -114,7 +120,6 @@ pub enum SeqCommand {
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
quantization: LaunchQuantization,
|
||||
sync_mode: SyncMode,
|
||||
},
|
||||
PatternStop {
|
||||
bank: usize,
|
||||
@@ -141,7 +146,6 @@ pub struct PatternSnapshot {
|
||||
pub speed: crate::model::PatternSpeed,
|
||||
pub length: usize,
|
||||
pub steps: Vec<StepSnapshot>,
|
||||
pub sync_mode: SyncMode,
|
||||
pub follow_up: FollowUp,
|
||||
}
|
||||
|
||||
@@ -299,18 +303,24 @@ struct ActivePattern {
|
||||
step_index: usize,
|
||||
iter: usize,
|
||||
last_step_beat: f64,
|
||||
origin_beat: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct PendingPattern {
|
||||
id: PatternId,
|
||||
quantization: LaunchQuantization,
|
||||
target_beat: Option<f64>,
|
||||
sync_mode: SyncMode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum PlayState {
|
||||
Idle { pause_beat: Option<f64> },
|
||||
Playing { frontier: f64 },
|
||||
}
|
||||
|
||||
struct AudioState {
|
||||
prev_beat: f64,
|
||||
pause_beat: Option<f64>,
|
||||
play_state: PlayState,
|
||||
active_patterns: HashMap<PatternId, ActivePattern>,
|
||||
pending_starts: Vec<PendingPattern>,
|
||||
pending_stops: Vec<PendingPattern>,
|
||||
@@ -320,8 +330,7 @@ struct AudioState {
|
||||
impl AudioState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
prev_beat: -1.0,
|
||||
pause_beat: None,
|
||||
play_state: PlayState::Idle { pause_beat: None },
|
||||
active_patterns: HashMap::new(),
|
||||
pending_starts: Vec::new(),
|
||||
pending_stops: Vec::new(),
|
||||
@@ -474,22 +483,6 @@ impl PatternSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_quantization_boundary(
|
||||
quantization: LaunchQuantization,
|
||||
beat: f64,
|
||||
prev_beat: f64,
|
||||
quantum: f64,
|
||||
) -> bool {
|
||||
match quantization {
|
||||
LaunchQuantization::Immediate => prev_beat >= 0.0,
|
||||
LaunchQuantization::Beat => StepTiming::NextBeat.crossed(prev_beat, beat, quantum),
|
||||
LaunchQuantization::Bar => StepTiming::NextBar.crossed(prev_beat, beat, quantum),
|
||||
LaunchQuantization::Bars2 => StepTiming::NextBar.crossed(prev_beat, beat, quantum * 2.0),
|
||||
LaunchQuantization::Bars4 => StepTiming::NextBar.crossed(prev_beat, beat, quantum * 4.0),
|
||||
LaunchQuantization::Bars8 => StepTiming::NextBar.crossed(prev_beat, beat, quantum * 8.0),
|
||||
}
|
||||
}
|
||||
|
||||
type StepKey = (usize, usize, usize);
|
||||
|
||||
struct RunsCounter {
|
||||
@@ -582,7 +575,7 @@ pub struct SequencerState {
|
||||
script_text: String,
|
||||
script_speed: crate::model::PatternSpeed,
|
||||
script_length: usize,
|
||||
script_frontier: f64,
|
||||
script_frontier: Option<f64>,
|
||||
script_step: usize,
|
||||
script_trace: Option<ExecutionTrace>,
|
||||
print_output: Option<String>,
|
||||
@@ -621,7 +614,7 @@ impl SequencerState {
|
||||
script_text: String::new(),
|
||||
script_speed: crate::model::PatternSpeed::default(),
|
||||
script_length: 16,
|
||||
script_frontier: -1.0,
|
||||
script_frontier: None,
|
||||
script_step: 0,
|
||||
script_trace: None,
|
||||
print_output: None,
|
||||
@@ -639,7 +632,7 @@ impl SequencerState {
|
||||
false
|
||||
}
|
||||
|
||||
fn process_commands(&mut self, commands: Vec<SeqCommand>) {
|
||||
fn process_commands(&mut self, commands: Vec<SeqCommand>, quantum: f64) {
|
||||
for cmd in commands {
|
||||
match cmd {
|
||||
SeqCommand::PatternUpdate {
|
||||
@@ -660,15 +653,15 @@ impl SequencerState {
|
||||
bank,
|
||||
pattern,
|
||||
quantization,
|
||||
sync_mode,
|
||||
} => {
|
||||
let id = PatternId { bank, pattern };
|
||||
self.audio_state.pending_stops.retain(|p| p.id != id);
|
||||
if !self.audio_state.pending_starts.iter().any(|p| p.id == id) {
|
||||
let target_beat = next_boundary(self.last_beat, quantization, quantum);
|
||||
self.audio_state.pending_starts.push(PendingPattern {
|
||||
id,
|
||||
quantization,
|
||||
sync_mode,
|
||||
target_beat,
|
||||
sync_mode: SyncMode::PhaseLock,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -680,9 +673,10 @@ impl SequencerState {
|
||||
let id = PatternId { bank, pattern };
|
||||
self.audio_state.pending_starts.retain(|p| p.id != id);
|
||||
if !self.audio_state.pending_stops.iter().any(|p| p.id == id) {
|
||||
let target_beat = next_boundary(self.last_beat, quantization, quantum);
|
||||
self.audio_state.pending_stops.push(PendingPattern {
|
||||
id,
|
||||
quantization,
|
||||
target_beat,
|
||||
sync_mode: SyncMode::Reset,
|
||||
});
|
||||
}
|
||||
@@ -722,7 +716,8 @@ impl SequencerState {
|
||||
self.audio_state.active_patterns.clear();
|
||||
self.audio_state.pending_starts.clear();
|
||||
self.audio_state.pending_stops.clear();
|
||||
self.audio_state.pause_beat = None;
|
||||
self.audio_state.play_state = PlayState::Idle { pause_beat: None };
|
||||
self.script_frontier = None;
|
||||
self.step_traces = Arc::new(HashMap::new());
|
||||
self.runs_counter.counts.clear();
|
||||
self.audio_state.flush_midi_notes = true;
|
||||
@@ -732,9 +727,8 @@ impl SequencerState {
|
||||
active.step_index = 0;
|
||||
active.iter = 0;
|
||||
}
|
||||
self.audio_state.prev_beat = -1.0;
|
||||
self.audio_state.pause_beat = None;
|
||||
self.script_frontier = -1.0;
|
||||
self.audio_state.play_state = PlayState::Idle { pause_beat: None };
|
||||
self.script_frontier = None;
|
||||
self.script_step = 0;
|
||||
self.script_trace = None;
|
||||
self.variables.store(Arc::new(HashMap::new()));
|
||||
@@ -758,30 +752,26 @@ impl SequencerState {
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, input: TickInput) -> TickOutput {
|
||||
self.process_commands(input.commands);
|
||||
self.last_tempo = input.tempo;
|
||||
self.last_beat = input.beat;
|
||||
self.last_playing = input.playing;
|
||||
self.process_commands(input.commands, input.quantum);
|
||||
|
||||
if !input.playing {
|
||||
return self.tick_paused();
|
||||
}
|
||||
|
||||
let frontier = self.audio_state.prev_beat;
|
||||
let lookahead_end = input.lookahead_end;
|
||||
let resuming = frontier < 0.0;
|
||||
|
||||
let boundary_frontier = if resuming {
|
||||
self.audio_state.pause_beat.take().unwrap_or(input.beat)
|
||||
} else {
|
||||
frontier
|
||||
let (frontier, resuming) = match self.audio_state.play_state {
|
||||
PlayState::Playing { frontier } => (frontier, false),
|
||||
PlayState::Idle { pause_beat } => (pause_beat.unwrap_or(input.beat), true),
|
||||
};
|
||||
let lookahead_end = input.lookahead_end;
|
||||
|
||||
self.activate_pending(lookahead_end, boundary_frontier, input.quantum);
|
||||
self.deactivate_pending(lookahead_end, boundary_frontier, input.quantum);
|
||||
self.activate_pending(frontier, lookahead_end);
|
||||
self.deactivate_pending(frontier, lookahead_end);
|
||||
|
||||
if resuming {
|
||||
self.realign_phaselock_patterns(lookahead_end);
|
||||
self.reset_origins_on_resume(lookahead_end);
|
||||
}
|
||||
|
||||
let steps = self.execute_steps(
|
||||
@@ -818,7 +808,7 @@ impl SequencerState {
|
||||
let new_tempo = self.read_tempo_variable(steps.any_step_fired);
|
||||
self.apply_follow_ups();
|
||||
|
||||
self.audio_state.prev_beat = lookahead_end;
|
||||
self.audio_state.play_state = PlayState::Playing { frontier: lookahead_end };
|
||||
|
||||
let flush = std::mem::take(&mut self.audio_state.flush_midi_notes);
|
||||
TickOutput {
|
||||
@@ -840,11 +830,12 @@ impl SequencerState {
|
||||
self.pattern_cache.set(key.0, key.1, snapshot);
|
||||
}
|
||||
}
|
||||
if self.audio_state.prev_beat >= 0.0 {
|
||||
self.audio_state.pause_beat = Some(self.audio_state.prev_beat);
|
||||
}
|
||||
self.audio_state.prev_beat = -1.0;
|
||||
self.script_frontier = -1.0;
|
||||
let pause_beat = match self.audio_state.play_state {
|
||||
PlayState::Playing { frontier } => Some(frontier),
|
||||
PlayState::Idle { pause_beat } => pause_beat,
|
||||
};
|
||||
self.audio_state.play_state = PlayState::Idle { pause_beat };
|
||||
self.script_frontier = None;
|
||||
self.script_step = 0;
|
||||
self.script_trace = None;
|
||||
self.print_output = None;
|
||||
@@ -858,35 +849,46 @@ impl SequencerState {
|
||||
}
|
||||
}
|
||||
|
||||
fn realign_phaselock_patterns(&mut self, beat: f64) {
|
||||
for (id, active) in &mut self.audio_state.active_patterns {
|
||||
let Some(pattern) = self.pattern_cache.get(id.bank, id.pattern) else {
|
||||
continue;
|
||||
};
|
||||
if pattern.sync_mode != SyncMode::PhaseLock {
|
||||
continue;
|
||||
}
|
||||
let speed_mult = pattern.speed.multiplier();
|
||||
let subs_per_beat = 4.0 * speed_mult;
|
||||
let step = (beat * subs_per_beat).floor() as usize + 1;
|
||||
active.step_index = step % pattern.length;
|
||||
fn pause_beat(&self) -> Option<f64> {
|
||||
match self.audio_state.play_state {
|
||||
PlayState::Idle { pause_beat } => pause_beat,
|
||||
PlayState::Playing { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn activate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) {
|
||||
fn reset_origins_on_resume(&mut self, lookahead_end: f64) {
|
||||
for (id, active) in &mut self.audio_state.active_patterns {
|
||||
active.origin_beat = lookahead_end;
|
||||
let Some(pattern) = self.pattern_cache.get(id.bank, id.pattern) else {
|
||||
continue;
|
||||
};
|
||||
let subs_per_beat = 4.0 * pattern.speed.multiplier();
|
||||
let step = (lookahead_end * subs_per_beat).floor() as usize + 1;
|
||||
active.step_index = step % pattern.length;
|
||||
}
|
||||
self.script_frontier = Some(lookahead_end);
|
||||
}
|
||||
|
||||
fn activate_pending(&mut self, frontier: f64, lookahead_end: f64) {
|
||||
self.buf_activated.clear();
|
||||
for pending in &self.audio_state.pending_starts {
|
||||
if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) {
|
||||
let should_activate = match pending.target_beat {
|
||||
None => true,
|
||||
Some(target) => target <= lookahead_end,
|
||||
};
|
||||
if should_activate {
|
||||
let origin_beat = match pending.target_beat {
|
||||
Some(t) if t > frontier => t - 1e-9,
|
||||
_ => frontier,
|
||||
};
|
||||
let start_step = match pending.sync_mode {
|
||||
SyncMode::Reset => 0,
|
||||
SyncMode::PhaseLock => {
|
||||
if let Some(pat) =
|
||||
self.pattern_cache.get(pending.id.bank, pending.id.pattern)
|
||||
{
|
||||
let speed_mult = pat.speed.multiplier();
|
||||
let subs_per_beat = 4.0 * speed_mult;
|
||||
let first_sub = (prev_beat * subs_per_beat).floor() as usize + 1;
|
||||
first_sub % pat.length
|
||||
let subs_per_beat = 4.0 * pat.speed.multiplier();
|
||||
(origin_beat * subs_per_beat).floor() as usize % pat.length
|
||||
} else {
|
||||
0
|
||||
}
|
||||
@@ -901,7 +903,8 @@ impl SequencerState {
|
||||
pattern: pending.id.pattern,
|
||||
step_index: start_step,
|
||||
iter: 0,
|
||||
last_step_beat: beat,
|
||||
last_step_beat: lookahead_end,
|
||||
origin_beat,
|
||||
},
|
||||
);
|
||||
self.buf_activated.push(pending.id);
|
||||
@@ -913,15 +916,18 @@ impl SequencerState {
|
||||
.retain(|p| !activated.contains(&p.id));
|
||||
}
|
||||
|
||||
fn deactivate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) {
|
||||
fn deactivate_pending(&mut self, _frontier: f64, lookahead_end: f64) {
|
||||
self.buf_stopped.clear();
|
||||
for pending in &self.audio_state.pending_stops {
|
||||
if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) {
|
||||
let should_deactivate = match pending.target_beat {
|
||||
None => true,
|
||||
Some(target) => target <= lookahead_end,
|
||||
};
|
||||
if should_deactivate {
|
||||
self.audio_state.active_patterns.remove(&pending.id);
|
||||
Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| {
|
||||
bank != pending.id.bank || pattern != pending.id.pattern
|
||||
});
|
||||
// Flush pending update so cache stays current for future launches
|
||||
let key = (pending.id.bank, pending.id.pattern);
|
||||
if let Some(snapshot) = self.pending_updates.remove(&key) {
|
||||
self.pattern_cache.set(key.0, key.1, snapshot);
|
||||
@@ -981,7 +987,8 @@ impl SequencerState {
|
||||
.copied()
|
||||
.unwrap_or_else(|| pattern.speed.multiplier());
|
||||
|
||||
let step_beats = substeps_in_window(frontier, lookahead_end, speed_mult);
|
||||
let pattern_frontier = frontier.max(active.origin_beat);
|
||||
let step_beats = substeps_in_window(pattern_frontier, lookahead_end, speed_mult);
|
||||
|
||||
for step_beat in step_beats {
|
||||
result.any_step_fired = true;
|
||||
@@ -1116,11 +1123,7 @@ impl SequencerState {
|
||||
return;
|
||||
}
|
||||
|
||||
let script_frontier = if self.script_frontier < 0.0 {
|
||||
frontier
|
||||
} else {
|
||||
self.script_frontier
|
||||
};
|
||||
let script_frontier = self.script_frontier.unwrap_or(frontier);
|
||||
|
||||
let speed_mult = self.script_speed.multiplier();
|
||||
let fire_beats = substeps_in_window(script_frontier, lookahead_end, speed_mult);
|
||||
@@ -1187,7 +1190,7 @@ impl SequencerState {
|
||||
self.script_step += 1;
|
||||
}
|
||||
|
||||
self.script_frontier = lookahead_end;
|
||||
self.script_frontier = Some(lookahead_end);
|
||||
}
|
||||
|
||||
fn read_tempo_variable(&self, any_step_fired: bool) -> Option<f64> {
|
||||
@@ -1220,21 +1223,21 @@ impl SequencerState {
|
||||
FollowUp::Stop => {
|
||||
self.audio_state.pending_stops.push(PendingPattern {
|
||||
id: *completed_id,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
target_beat: None,
|
||||
sync_mode: SyncMode::Reset,
|
||||
});
|
||||
}
|
||||
FollowUp::Chain { bank, pattern } => {
|
||||
self.audio_state.pending_stops.push(PendingPattern {
|
||||
id: *completed_id,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
target_beat: None,
|
||||
sync_mode: SyncMode::Reset,
|
||||
});
|
||||
let target = PatternId { bank, pattern };
|
||||
if !self.audio_state.pending_starts.iter().any(|p| p.id == target) {
|
||||
self.audio_state.pending_starts.push(PendingPattern {
|
||||
id: target,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
target_beat: None,
|
||||
sync_mode: SyncMode::Reset,
|
||||
});
|
||||
}
|
||||
@@ -1325,9 +1328,19 @@ fn sequencer_loop(
|
||||
|
||||
let state = link.capture_app_state();
|
||||
let current_time_us = link.clock_micros() as SyncTime;
|
||||
let beat = state.beat_at_time(current_time_us as i64, quantum);
|
||||
let mut beat = state.beat_at_time(current_time_us as i64, quantum);
|
||||
let tempo = state.tempo();
|
||||
|
||||
let is_playing = playing.load(Ordering::Relaxed);
|
||||
if is_playing && !seq_state.last_playing {
|
||||
let anchor_beat = seq_state.pause_beat().unwrap_or(0.0);
|
||||
link.start_playing(anchor_beat, current_time_us as i64, quantum);
|
||||
let state = link.capture_app_state();
|
||||
beat = state.beat_at_time(current_time_us as i64, quantum);
|
||||
} else if !is_playing && seq_state.last_playing {
|
||||
link.stop_playing(current_time_us as i64);
|
||||
}
|
||||
|
||||
let lookahead_beats = if tempo > 0.0 {
|
||||
lookahead_secs * tempo / 60.0
|
||||
} else {
|
||||
@@ -1339,7 +1352,7 @@ fn sequencer_loop(
|
||||
let audio_samples = audio_sample_pos.load(Ordering::Acquire);
|
||||
let input = TickInput {
|
||||
commands,
|
||||
playing: playing.load(Ordering::Relaxed),
|
||||
playing: is_playing,
|
||||
beat,
|
||||
lookahead_end,
|
||||
tempo,
|
||||
@@ -1550,7 +1563,6 @@ mod tests {
|
||||
source: None,
|
||||
})
|
||||
.collect(),
|
||||
sync_mode: SyncMode::Reset,
|
||||
follow_up: FollowUp::default(),
|
||||
}
|
||||
}
|
||||
@@ -1621,7 +1633,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
],
|
||||
1.0,
|
||||
@@ -1649,7 +1660,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
1.0,
|
||||
));
|
||||
@@ -1697,7 +1707,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
0.5,
|
||||
));
|
||||
@@ -1706,43 +1715,32 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quantization_boundaries() {
|
||||
assert!(check_quantization_boundary(
|
||||
LaunchQuantization::Immediate,
|
||||
1.5,
|
||||
1.0,
|
||||
4.0
|
||||
));
|
||||
assert!(check_quantization_boundary(
|
||||
LaunchQuantization::Beat,
|
||||
2.0,
|
||||
1.9,
|
||||
4.0
|
||||
));
|
||||
assert!(!check_quantization_boundary(
|
||||
LaunchQuantization::Beat,
|
||||
1.5,
|
||||
1.2,
|
||||
4.0
|
||||
));
|
||||
assert!(check_quantization_boundary(
|
||||
LaunchQuantization::Bar,
|
||||
4.0,
|
||||
3.9,
|
||||
4.0
|
||||
));
|
||||
assert!(!check_quantization_boundary(
|
||||
LaunchQuantization::Bar,
|
||||
3.5,
|
||||
3.2,
|
||||
4.0
|
||||
));
|
||||
assert!(!check_quantization_boundary(
|
||||
LaunchQuantization::Immediate,
|
||||
1.0,
|
||||
-1.0,
|
||||
4.0
|
||||
));
|
||||
fn test_next_boundary() {
|
||||
use super::super::next_boundary;
|
||||
|
||||
// Immediate returns None
|
||||
assert_eq!(next_boundary(1.5, LaunchQuantization::Immediate, 4.0), None);
|
||||
|
||||
// Beat: next integer beat
|
||||
assert_eq!(next_boundary(1.5, LaunchQuantization::Beat, 4.0), Some(2.0));
|
||||
assert_eq!(next_boundary(1.9, LaunchQuantization::Beat, 4.0), Some(2.0));
|
||||
// On exact beat boundary, targets next beat
|
||||
assert_eq!(next_boundary(2.0, LaunchQuantization::Beat, 4.0), Some(3.0));
|
||||
|
||||
// Bar (quantum=4): next multiple of 4
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bar, 4.0), Some(4.0));
|
||||
assert_eq!(next_boundary(3.9, LaunchQuantization::Bar, 4.0), Some(4.0));
|
||||
// On exact bar boundary, targets next bar
|
||||
assert_eq!(next_boundary(4.0, LaunchQuantization::Bar, 4.0), Some(8.0));
|
||||
|
||||
// Bars2 (quantum=4): next multiple of 8
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bars2, 4.0), Some(8.0));
|
||||
|
||||
// Bars4 (quantum=4): next multiple of 16
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bars4, 4.0), Some(16.0));
|
||||
|
||||
// Bars8 (quantum=4): next multiple of 32
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bars8, 4.0), Some(32.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1763,7 +1761,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
0.5,
|
||||
));
|
||||
@@ -1808,7 +1805,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
0.5,
|
||||
));
|
||||
@@ -1844,7 +1840,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
SeqCommand::PatternStop {
|
||||
bank: 0,
|
||||
@@ -1879,13 +1874,11 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 1,
|
||||
quantization: LaunchQuantization::Beat,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
],
|
||||
0.0,
|
||||
@@ -1921,7 +1914,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
0.5,
|
||||
));
|
||||
@@ -1990,7 +1982,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
..tick_at(1.0, false)
|
||||
});
|
||||
@@ -2019,7 +2010,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
1.0,
|
||||
));
|
||||
@@ -2049,13 +2039,11 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
],
|
||||
0.0,
|
||||
@@ -2088,7 +2076,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
0.5,
|
||||
));
|
||||
@@ -2115,7 +2102,6 @@ mod tests {
|
||||
source: None,
|
||||
})
|
||||
.collect(),
|
||||
sync_mode: SyncMode::Reset,
|
||||
follow_up: FollowUp::default(),
|
||||
}
|
||||
}
|
||||
@@ -2138,7 +2124,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
0.5,
|
||||
));
|
||||
@@ -2186,13 +2171,11 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 1,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
],
|
||||
0.5,
|
||||
@@ -2227,7 +2210,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
3.5,
|
||||
));
|
||||
@@ -2266,7 +2248,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
..tick_at(2.0, false)
|
||||
});
|
||||
@@ -2306,13 +2287,11 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 1,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
],
|
||||
3.5,
|
||||
@@ -2344,7 +2323,6 @@ mod tests {
|
||||
source: None,
|
||||
})
|
||||
.collect(),
|
||||
sync_mode: SyncMode::PhaseLock,
|
||||
follow_up: FollowUp::default(),
|
||||
}
|
||||
}
|
||||
@@ -2368,7 +2346,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
sync_mode: SyncMode::PhaseLock,
|
||||
}],
|
||||
3.5,
|
||||
));
|
||||
@@ -2410,7 +2387,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
1.0,
|
||||
));
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
use crate::model::LaunchQuantization;
|
||||
|
||||
/// Microsecond-precision timestamp for audio synchronization.
|
||||
pub type SyncTime = u64;
|
||||
|
||||
/// Timing boundary types for step and pattern scheduling.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StepTiming {
|
||||
/// Fire when a beat boundary is crossed.
|
||||
NextBeat,
|
||||
/// Fire when a bar/quantum boundary is crossed.
|
||||
NextBar,
|
||||
}
|
||||
|
||||
impl StepTiming {
|
||||
/// Returns true if the boundary was crossed between prev_beat and curr_beat.
|
||||
pub fn crossed(&self, prev_beat: f64, curr_beat: f64, quantum: f64) -> bool {
|
||||
if prev_beat < 0.0 {
|
||||
return false;
|
||||
/// Compute the exact next quantization boundary beat after `current_beat`.
|
||||
/// Returns `None` for Immediate (activate now), `Some(beat)` for all others.
|
||||
pub fn next_boundary(current_beat: f64, quantization: LaunchQuantization, quantum: f64) -> Option<f64> {
|
||||
match quantization {
|
||||
LaunchQuantization::Immediate => None,
|
||||
LaunchQuantization::Beat => Some(current_beat.floor() + 1.0),
|
||||
LaunchQuantization::Bar => {
|
||||
Some((current_beat / quantum).floor() * quantum + quantum)
|
||||
}
|
||||
match self {
|
||||
Self::NextBeat => prev_beat.floor() as i64 != curr_beat.floor() as i64,
|
||||
Self::NextBar => {
|
||||
(prev_beat / quantum).floor() as i64 != (curr_beat / quantum).floor() as i64
|
||||
LaunchQuantization::Bars2 => {
|
||||
let p = quantum * 2.0;
|
||||
Some((current_beat / p).floor() * p + p)
|
||||
}
|
||||
LaunchQuantization::Bars4 => {
|
||||
let p = quantum * 4.0;
|
||||
Some((current_beat / p).floor() * p + p)
|
||||
}
|
||||
LaunchQuantization::Bars8 => {
|
||||
let p = quantum * 8.0;
|
||||
Some((current_beat / p).floor() * p + p)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +31,7 @@ impl StepTiming {
|
||||
/// Each entry is the exact beat at which that substep fires.
|
||||
/// Clamped to 64 results max to prevent runaway.
|
||||
pub fn substeps_in_window(frontier: f64, end: f64, speed: f64) -> Vec<f64> {
|
||||
if frontier < 0.0 || end <= frontier || speed <= 0.0 {
|
||||
if end <= frontier || speed <= 0.0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let substeps_per_beat = 4.0 * speed;
|
||||
@@ -55,9 +57,6 @@ mod tests {
|
||||
}
|
||||
|
||||
fn substeps_crossed(prev_beat: f64, curr_beat: f64, speed: f64) -> usize {
|
||||
if prev_beat < 0.0 {
|
||||
return 0;
|
||||
}
|
||||
let prev_substep = (prev_beat * 4.0 * speed).floor() as i64;
|
||||
let curr_substep = (curr_beat * 4.0 * speed).floor() as i64;
|
||||
(curr_substep - prev_substep).clamp(0, 16) as usize
|
||||
@@ -88,23 +87,15 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_step_timing_beat_crossed() {
|
||||
// Crossing from beat 0 to beat 1
|
||||
assert!(StepTiming::NextBeat.crossed(0.9, 1.1, 4.0));
|
||||
// Not crossing (both in same beat)
|
||||
assert!(!StepTiming::NextBeat.crossed(0.5, 0.9, 4.0));
|
||||
// Negative prev_beat returns false
|
||||
assert!(!StepTiming::NextBeat.crossed(-1.0, 1.0, 4.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_step_timing_bar_crossed() {
|
||||
// Crossing from bar 0 to bar 1 (quantum=4)
|
||||
assert!(StepTiming::NextBar.crossed(3.9, 4.1, 4.0));
|
||||
// Not crossing (both in same bar)
|
||||
assert!(!StepTiming::NextBar.crossed(2.0, 3.0, 4.0));
|
||||
// Crossing with different quantum
|
||||
assert!(StepTiming::NextBar.crossed(7.9, 8.1, 8.0));
|
||||
fn test_next_boundary() {
|
||||
assert_eq!(next_boundary(1.5, LaunchQuantization::Immediate, 4.0), None);
|
||||
assert_eq!(next_boundary(1.5, LaunchQuantization::Beat, 4.0), Some(2.0));
|
||||
assert_eq!(next_boundary(2.0, LaunchQuantization::Beat, 4.0), Some(3.0));
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bar, 4.0), Some(4.0));
|
||||
assert_eq!(next_boundary(4.0, LaunchQuantization::Bar, 4.0), Some(8.0));
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bars2, 4.0), Some(8.0));
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bars4, 4.0), Some(16.0));
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bars8, 4.0), Some(32.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -126,9 +117,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_substeps_crossed_negative_prev() {
|
||||
// Negative prev_beat returns 0
|
||||
assert_eq!(substeps_crossed(-1.0, 0.5, 1.0), 0);
|
||||
fn test_substeps_crossed_same_position() {
|
||||
// Same position returns 0
|
||||
assert_eq!(substeps_crossed(0.5, 0.5, 1.0), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -205,8 +196,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_substeps_in_window_negative_frontier() {
|
||||
let result = substeps_in_window(-1.0, 0.5, 1.0);
|
||||
fn test_substeps_in_window_reversed() {
|
||||
// end <= frontier returns empty
|
||||
let result = substeps_in_window(0.5, 0.3, 1.0);
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,8 +83,7 @@ pub fn init(args: InitArgs) -> Init {
|
||||
for (bank, pattern) in playing {
|
||||
app.playback.queued_changes.push(StagedChange {
|
||||
change: PatternChange::Start { bank, pattern },
|
||||
quantization: model::LaunchQuantization::Immediate,
|
||||
sync_mode: model::SyncMode::PhaseLock,
|
||||
quantization: model::LaunchQuantization::Bar,
|
||||
});
|
||||
}
|
||||
app.ui.set_status(format!("Demo: {}", demo.name));
|
||||
@@ -96,8 +95,7 @@ pub fn init(args: InitArgs) -> Init {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
},
|
||||
quantization: model::LaunchQuantization::Immediate,
|
||||
sync_mode: model::SyncMode::PhaseLock,
|
||||
quantization: model::LaunchQuantization::Bar,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -461,7 +461,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
length,
|
||||
speed,
|
||||
quantization,
|
||||
sync_mode,
|
||||
follow_up,
|
||||
} => {
|
||||
let (bank, pattern) = (*bank, *pattern);
|
||||
@@ -472,7 +471,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
KeyCode::Left => match field {
|
||||
PatternPropsField::Speed => *speed = speed.prev(),
|
||||
PatternPropsField::Quantization => *quantization = quantization.prev(),
|
||||
PatternPropsField::SyncMode => *sync_mode = sync_mode.toggle(),
|
||||
PatternPropsField::FollowUp => *follow_up = follow_up.prev_mode(),
|
||||
PatternPropsField::ChainBank => {
|
||||
if let FollowUp::Chain { bank: b, .. } = follow_up {
|
||||
@@ -489,7 +487,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
KeyCode::Right => match field {
|
||||
PatternPropsField::Speed => *speed = speed.next(),
|
||||
PatternPropsField::Quantization => *quantization = quantization.next(),
|
||||
PatternPropsField::SyncMode => *sync_mode = sync_mode.toggle(),
|
||||
PatternPropsField::FollowUp => *follow_up = follow_up.next_mode(),
|
||||
PatternPropsField::ChainBank => {
|
||||
if let FollowUp::Chain { bank: b, .. } = follow_up {
|
||||
@@ -535,7 +532,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
let length_val = length.parse().ok();
|
||||
let speed_val = *speed;
|
||||
let quant_val = *quantization;
|
||||
let sync_val = *sync_mode;
|
||||
let follow_up_val = *follow_up;
|
||||
ctx.dispatch(AppCommand::StagePatternProps {
|
||||
bank,
|
||||
@@ -545,7 +541,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
length: length_val,
|
||||
speed: speed_val,
|
||||
quantization: quant_val,
|
||||
sync_mode: sync_val,
|
||||
follow_up: follow_up_val,
|
||||
});
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
|
||||
@@ -11,7 +11,7 @@ pub use cagire_forth::{
|
||||
};
|
||||
pub use cagire_project::{
|
||||
load, load_str, save, share, Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed,
|
||||
Project, SyncMode, MAX_BANKS, MAX_PATTERNS,
|
||||
Project, MAX_BANKS, MAX_PATTERNS,
|
||||
};
|
||||
pub use script::ScriptEngine;
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ pub enum PatternPropsField {
|
||||
Length,
|
||||
Speed,
|
||||
Quantization,
|
||||
SyncMode,
|
||||
FollowUp,
|
||||
ChainBank,
|
||||
ChainPattern,
|
||||
@@ -44,8 +43,7 @@ impl PatternPropsField {
|
||||
Self::Description => Self::Length,
|
||||
Self::Length => Self::Speed,
|
||||
Self::Speed => Self::Quantization,
|
||||
Self::Quantization => Self::SyncMode,
|
||||
Self::SyncMode => Self::FollowUp,
|
||||
Self::Quantization => Self::FollowUp,
|
||||
Self::FollowUp if follow_up_is_chain => Self::ChainBank,
|
||||
Self::FollowUp => Self::FollowUp,
|
||||
Self::ChainBank => Self::ChainPattern,
|
||||
@@ -60,8 +58,7 @@ impl PatternPropsField {
|
||||
Self::Length => Self::Description,
|
||||
Self::Speed => Self::Length,
|
||||
Self::Quantization => Self::Speed,
|
||||
Self::SyncMode => Self::Quantization,
|
||||
Self::FollowUp => Self::SyncMode,
|
||||
Self::FollowUp => Self::Quantization,
|
||||
Self::ChainBank => Self::FollowUp,
|
||||
Self::ChainPattern if follow_up_is_chain => Self::ChainBank,
|
||||
Self::ChainPattern => Self::FollowUp,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::model::{self, FollowUp, LaunchQuantization, PatternSpeed, SyncMode};
|
||||
use crate::model::{self, FollowUp, LaunchQuantization, PatternSpeed};
|
||||
use crate::state::editor::{EuclideanField, PatternField, PatternPropsField, ScriptField};
|
||||
use crate::state::file_browser::FileBrowserState;
|
||||
|
||||
@@ -85,7 +85,6 @@ pub enum Modal {
|
||||
length: String,
|
||||
speed: PatternSpeed,
|
||||
quantization: LaunchQuantization,
|
||||
sync_mode: SyncMode,
|
||||
follow_up: FollowUp,
|
||||
},
|
||||
KeybindingsHelp {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use crate::engine::PatternChange;
|
||||
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed, SyncMode};
|
||||
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct StagedChange {
|
||||
pub change: PatternChange,
|
||||
pub quantization: LaunchQuantization,
|
||||
pub sync_mode: SyncMode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
@@ -21,7 +20,6 @@ pub struct StagedPropChange {
|
||||
pub length: Option<usize>,
|
||||
pub speed: PatternSpeed,
|
||||
pub quantization: LaunchQuantization,
|
||||
pub sync_mode: SyncMode,
|
||||
pub follow_up: FollowUp,
|
||||
}
|
||||
|
||||
|
||||
@@ -507,11 +507,7 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
||||
};
|
||||
let props_indicator = if has_staged_props { "~" } else { "" };
|
||||
let quant_sync = if is_selected {
|
||||
format!(
|
||||
"{}:{} ",
|
||||
pattern.quantization.short_label(),
|
||||
pattern.sync_mode.short_label()
|
||||
)
|
||||
format!("{} ", pattern.quantization.short_label())
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
@@ -755,8 +751,6 @@ fn render_properties(
|
||||
let steps_label = format!("{}/{}", content_count, pattern.length);
|
||||
let speed_label = pattern.speed.label();
|
||||
let quant_label = pattern.quantization.label();
|
||||
let sync_label = pattern.sync_mode.label();
|
||||
|
||||
let label_style = Style::new().fg(theme.ui.text_muted);
|
||||
let value_style = Style::new().fg(theme.ui.text_primary);
|
||||
|
||||
@@ -781,10 +775,6 @@ fn render_properties(
|
||||
Span::styled(" Quant ", label_style),
|
||||
Span::styled(quant_label, value_style),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" Sync ", label_style),
|
||||
Span::styled(sync_label, value_style),
|
||||
]),
|
||||
];
|
||||
|
||||
if pattern.follow_up != FollowUp::Loop {
|
||||
|
||||
@@ -737,7 +737,6 @@ fn render_modal(
|
||||
length,
|
||||
speed,
|
||||
quantization,
|
||||
sync_mode,
|
||||
follow_up,
|
||||
} => {
|
||||
use crate::model::FollowUp;
|
||||
@@ -766,7 +765,6 @@ fn render_modal(
|
||||
("Length", length.clone(), *field == PatternPropsField::Length),
|
||||
("Speed", speed_label, *field == PatternPropsField::Speed),
|
||||
("Quantization", quantization.label().to_string(), *field == PatternPropsField::Quantization),
|
||||
("Sync Mode", sync_mode.label().to_string(), *field == PatternPropsField::SyncMode),
|
||||
("Follow Up", follow_up_label, *field == PatternPropsField::FollowUp),
|
||||
];
|
||||
if is_chain {
|
||||
|
||||
Reference in New Issue
Block a user