Feat: introduce follow up actions
Some checks failed
Deploy Website / deploy (push) Failing after 4m49s
Some checks failed
Deploy Website / deploy (push) Failing after 4m49s
This commit is contained in:
@@ -94,7 +94,6 @@ pub enum Op {
|
||||
Triangle,
|
||||
Range,
|
||||
Perlin,
|
||||
Chain,
|
||||
Loop,
|
||||
Degree(&'static [i64]),
|
||||
Oct,
|
||||
|
||||
@@ -59,7 +59,6 @@ pub struct StepContext<'a> {
|
||||
pub nudge_secs: f64,
|
||||
pub cc_access: Option<&'a dyn CcAccess>,
|
||||
pub speed_key: &'a str,
|
||||
pub chain_key: &'a str,
|
||||
pub mouse_x: f64,
|
||||
pub mouse_y: f64,
|
||||
pub mouse_down: f64,
|
||||
|
||||
@@ -992,26 +992,6 @@ impl Forth {
|
||||
.insert(ctx.speed_key.to_string(), Value::Float(clamped, None));
|
||||
}
|
||||
|
||||
Op::Chain => {
|
||||
let pattern = pop_int(stack)? - 1;
|
||||
let bank = pop_int(stack)? - 1;
|
||||
if bank < 0 || pattern < 0 {
|
||||
return Err("chain: bank and pattern must be >= 1".into());
|
||||
}
|
||||
if bank as usize == ctx.bank && pattern as usize == ctx.pattern {
|
||||
// chaining to self is a no-op
|
||||
} else {
|
||||
use std::fmt::Write;
|
||||
let mut val = String::with_capacity(8);
|
||||
let _ = write!(&mut val, "{bank}:{pattern}");
|
||||
var_writes_cell
|
||||
.borrow_mut()
|
||||
.as_mut()
|
||||
.expect("var_writes taken")
|
||||
.insert(ctx.chain_key.to_string(), Value::Str(Arc::from(val), None));
|
||||
}
|
||||
}
|
||||
|
||||
Op::Loop => {
|
||||
let beats = pop_float(stack)?;
|
||||
if ctx.tempo == 0.0 || ctx.speed == 0.0 {
|
||||
|
||||
@@ -89,7 +89,6 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
||||
"triangle" => Op::Triangle,
|
||||
"range" => Op::Range,
|
||||
"perlin" => Op::Perlin,
|
||||
"chain" => Op::Chain,
|
||||
"loop" => Op::Loop,
|
||||
"oct" => Op::Oct,
|
||||
"clear" => Op::ClearCmd,
|
||||
|
||||
@@ -254,16 +254,6 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "chain",
|
||||
aliases: &[],
|
||||
category: "Time",
|
||||
stack: "(bank pattern --)",
|
||||
desc: "Chain to bank/pattern (1-indexed) when current pattern ends",
|
||||
example: "1 4 chain",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "at",
|
||||
aliases: &[],
|
||||
|
||||
@@ -7,4 +7,4 @@ pub const MAX_STEPS: usize = 1024;
|
||||
pub const DEFAULT_LENGTH: usize = 16;
|
||||
|
||||
pub use file::{load, save, FileError};
|
||||
pub use project::{Bank, LaunchQuantization, Pattern, PatternSpeed, Project, Step, SyncMode};
|
||||
pub use project::{Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project, Step, SyncMode};
|
||||
|
||||
@@ -206,6 +206,44 @@ impl SyncMode {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||
pub enum FollowUp {
|
||||
#[default]
|
||||
Loop,
|
||||
Stop,
|
||||
Chain { bank: usize, pattern: usize },
|
||||
}
|
||||
|
||||
impl FollowUp {
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Loop => "Loop",
|
||||
Self::Stop => "Stop",
|
||||
Self::Chain { .. } => "Chain",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_mode(&self) -> Self {
|
||||
match self {
|
||||
Self::Loop => Self::Stop,
|
||||
Self::Stop => Self::Chain { bank: 0, pattern: 0 },
|
||||
Self::Chain { .. } => Self::Loop,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prev_mode(&self) -> Self {
|
||||
match self {
|
||||
Self::Loop => Self::Chain { bank: 0, pattern: 0 },
|
||||
Self::Stop => Self::Loop,
|
||||
Self::Chain { .. } => Self::Stop,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_default_follow_up(f: &FollowUp) -> bool {
|
||||
*f == FollowUp::default()
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Step {
|
||||
pub active: bool,
|
||||
@@ -245,6 +283,7 @@ pub struct Pattern {
|
||||
pub name: Option<String>,
|
||||
pub quantization: LaunchQuantization,
|
||||
pub sync_mode: SyncMode,
|
||||
pub follow_up: FollowUp,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -280,6 +319,8 @@ struct SparsePattern {
|
||||
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,
|
||||
}
|
||||
|
||||
fn is_default_quantization(q: &LaunchQuantization) -> bool {
|
||||
@@ -302,6 +343,8 @@ struct LegacyPattern {
|
||||
quantization: LaunchQuantization,
|
||||
#[serde(default)]
|
||||
sync_mode: SyncMode,
|
||||
#[serde(default)]
|
||||
follow_up: FollowUp,
|
||||
}
|
||||
|
||||
impl Serialize for Pattern {
|
||||
@@ -327,6 +370,7 @@ impl Serialize for Pattern {
|
||||
name: self.name.clone(),
|
||||
quantization: self.quantization,
|
||||
sync_mode: self.sync_mode,
|
||||
follow_up: self.follow_up,
|
||||
};
|
||||
sparse.serialize(serializer)
|
||||
}
|
||||
@@ -361,6 +405,7 @@ impl<'de> Deserialize<'de> for Pattern {
|
||||
name: sparse.name,
|
||||
quantization: sparse.quantization,
|
||||
sync_mode: sparse.sync_mode,
|
||||
follow_up: sparse.follow_up,
|
||||
})
|
||||
}
|
||||
PatternFormat::Legacy(legacy) => Ok(Pattern {
|
||||
@@ -370,6 +415,7 @@ impl<'de> Deserialize<'de> for Pattern {
|
||||
name: legacy.name,
|
||||
quantization: legacy.quantization,
|
||||
sync_mode: legacy.sync_mode,
|
||||
follow_up: legacy.follow_up,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -384,6 +430,7 @@ impl Default for Pattern {
|
||||
name: None,
|
||||
quantization: LaunchQuantization::default(),
|
||||
sync_mode: SyncMode::default(),
|
||||
follow_up: FollowUp::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,18 @@ Each pattern is an independent sequence of steps with its own properties:
|
||||
| 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.
|
||||
|
||||
### Follow Up
|
||||
|
||||
The follow-up action determines what happens when a pattern reaches the end of its steps:
|
||||
|
||||
- **Loop** — the pattern repeats indefinitely. This is the default behavior.
|
||||
- **Stop** — the pattern plays once and stops.
|
||||
- **Chain** — the pattern plays once, then starts another pattern. Use `Left`/`Right` to set the target bank and pattern in the edit view.
|
||||
|
||||
## Patterns View
|
||||
|
||||
Access the patterns view with `F2` (or `Ctrl+Up` from the sequencer). The view shows all banks and patterns in a grid. Indicators show pattern state:
|
||||
|
||||
@@ -217,6 +217,7 @@ impl Plugin for CagirePlugin {
|
||||
.collect(),
|
||||
quantization: pat.quantization,
|
||||
sync_mode: pat.sync_mode,
|
||||
follow_up: pat.follow_up,
|
||||
};
|
||||
let _ = self.bridge.cmd_tx.send(SeqCommand::PatternUpdate {
|
||||
bank: bank_idx,
|
||||
|
||||
@@ -180,6 +180,7 @@ impl App {
|
||||
speed,
|
||||
quantization,
|
||||
sync_mode,
|
||||
follow_up,
|
||||
} => {
|
||||
self.playback.staged_prop_changes.insert(
|
||||
(bank, pattern),
|
||||
@@ -189,6 +190,7 @@ impl App {
|
||||
speed,
|
||||
quantization,
|
||||
sync_mode,
|
||||
follow_up,
|
||||
},
|
||||
);
|
||||
self.ui.set_status(format!(
|
||||
|
||||
@@ -189,6 +189,7 @@ impl App {
|
||||
speed: pat.speed,
|
||||
quantization: pat.quantization,
|
||||
sync_mode: pat.sync_mode,
|
||||
follow_up: pat.follow_up,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ impl App {
|
||||
nudge_secs: 0.0,
|
||||
cc_access: None,
|
||||
speed_key: "",
|
||||
chain_key: "",
|
||||
mouse_x: 0.5,
|
||||
mouse_y: 0.5,
|
||||
mouse_down: 0.0,
|
||||
|
||||
@@ -54,6 +54,7 @@ impl App {
|
||||
.collect(),
|
||||
quantization: pat.quantization,
|
||||
sync_mode: pat.sync_mode,
|
||||
follow_up: pat.follow_up,
|
||||
};
|
||||
let _ = cmd_tx.send(SeqCommand::PatternUpdate {
|
||||
bank,
|
||||
|
||||
@@ -85,6 +85,7 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::model::{LaunchQuantization, PatternSpeed, SyncMode};
|
||||
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed, SyncMode};
|
||||
use crate::page::Page;
|
||||
use crate::state::{ColorScheme, DeviceKind, EngineSection, Modal, OptionsFocus, PatternField, SettingKind};
|
||||
|
||||
@@ -144,6 +144,7 @@ pub enum AppCommand {
|
||||
speed: PatternSpeed,
|
||||
quantization: LaunchQuantization,
|
||||
sync_mode: SyncMode,
|
||||
follow_up: FollowUp,
|
||||
},
|
||||
|
||||
// Page navigation
|
||||
|
||||
@@ -16,7 +16,7 @@ use super::{substeps_in_window, LinkState, StepTiming, SyncTime};
|
||||
use crate::model::{
|
||||
CcAccess, Dictionary, ExecutionTrace, Rng, ScriptEngine, StepContext, Value, Variables,
|
||||
};
|
||||
use crate::model::{LaunchQuantization, SyncMode, MAX_BANKS, MAX_PATTERNS};
|
||||
use crate::model::{FollowUp, LaunchQuantization, SyncMode, MAX_BANKS, MAX_PATTERNS};
|
||||
use crate::state::LiveKeyState;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
@@ -134,6 +134,7 @@ pub struct PatternSnapshot {
|
||||
pub steps: Vec<StepSnapshot>,
|
||||
pub quantization: LaunchQuantization,
|
||||
pub sync_mode: SyncMode,
|
||||
pub follow_up: FollowUp,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -523,33 +524,6 @@ struct StepResult {
|
||||
any_step_fired: bool,
|
||||
}
|
||||
|
||||
struct VariableReads {
|
||||
new_tempo: Option<f64>,
|
||||
chain_transitions: Vec<(PatternId, PatternId)>,
|
||||
}
|
||||
|
||||
fn parse_chain_target(s: &str) -> Option<PatternId> {
|
||||
let (bank, pattern) = s.split_once(':')?;
|
||||
Some(PatternId {
|
||||
bank: bank.parse().ok()?,
|
||||
pattern: pattern.parse().ok()?,
|
||||
})
|
||||
}
|
||||
|
||||
struct KeyBuf {
|
||||
speed: String,
|
||||
chain: String,
|
||||
}
|
||||
|
||||
impl KeyBuf {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
speed: String::with_capacity(24),
|
||||
chain: String::with_capacity(24),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_speed_key(buf: &mut String, bank: usize, pattern: usize) -> &str {
|
||||
use std::fmt::Write;
|
||||
buf.clear();
|
||||
@@ -557,13 +531,6 @@ fn format_speed_key(buf: &mut String, bank: usize, pattern: usize) -> &str {
|
||||
buf
|
||||
}
|
||||
|
||||
fn format_chain_key(buf: &mut String, bank: usize, pattern: usize) -> &str {
|
||||
use std::fmt::Write;
|
||||
buf.clear();
|
||||
write!(buf, "__chain_{bank}_{pattern}__").unwrap();
|
||||
buf
|
||||
}
|
||||
|
||||
pub struct SequencerState {
|
||||
audio_state: AudioState,
|
||||
pattern_cache: PatternCache,
|
||||
@@ -575,7 +542,7 @@ pub struct SequencerState {
|
||||
variables: Variables,
|
||||
dict: Dictionary,
|
||||
speed_overrides: HashMap<(usize, usize), f64>,
|
||||
key_buf: KeyBuf,
|
||||
speed_key_buf: String,
|
||||
buf_audio_commands: Vec<TimestampedCommand>,
|
||||
buf_activated: Vec<PatternId>,
|
||||
buf_stopped: Vec<PatternId>,
|
||||
@@ -606,7 +573,7 @@ impl SequencerState {
|
||||
variables,
|
||||
dict,
|
||||
speed_overrides: HashMap::with_capacity(MAX_PATTERNS),
|
||||
key_buf: KeyBuf::new(),
|
||||
speed_key_buf: String::with_capacity(24),
|
||||
buf_audio_commands: Vec::with_capacity(32),
|
||||
buf_activated: Vec::with_capacity(16),
|
||||
buf_stopped: Vec::with_capacity(16),
|
||||
@@ -757,15 +724,15 @@ impl SequencerState {
|
||||
input.mouse_down,
|
||||
);
|
||||
|
||||
let vars = self.read_variables(&self.buf_completed_iterations, steps.any_step_fired);
|
||||
self.apply_chain_transitions(vars.chain_transitions);
|
||||
let new_tempo = self.read_tempo_variable(steps.any_step_fired);
|
||||
self.apply_follow_ups();
|
||||
|
||||
self.audio_state.prev_beat = lookahead_end;
|
||||
|
||||
let flush = std::mem::take(&mut self.audio_state.flush_midi_notes);
|
||||
TickOutput {
|
||||
audio_commands: std::mem::take(&mut self.buf_audio_commands),
|
||||
new_tempo: vars.new_tempo,
|
||||
new_tempo,
|
||||
shared_state: self.build_shared_state(),
|
||||
flush_midi_notes: flush,
|
||||
}
|
||||
@@ -896,7 +863,7 @@ impl SequencerState {
|
||||
{
|
||||
let vars = self.variables.load_full();
|
||||
for id in self.audio_state.active_patterns.keys() {
|
||||
let key = format_speed_key(&mut self.key_buf.speed, id.bank, id.pattern);
|
||||
let key = format_speed_key(&mut self.speed_key_buf, id.bank, id.pattern);
|
||||
if let Some(v) = vars.get(key).and_then(|v: &Value| v.as_float().ok()) {
|
||||
self.speed_overrides.insert((id.bank, id.pattern), v);
|
||||
}
|
||||
@@ -947,8 +914,7 @@ impl SequencerState {
|
||||
active.pattern,
|
||||
source_idx,
|
||||
);
|
||||
let speed_key = format_speed_key(&mut self.key_buf.speed, active.bank, active.pattern);
|
||||
let chain_key = format_chain_key(&mut self.key_buf.chain, active.bank, active.pattern);
|
||||
let speed_key = format_speed_key(&mut self.speed_key_buf, active.bank, active.pattern);
|
||||
let ctx = StepContext {
|
||||
step: step_idx,
|
||||
beat: step_beat,
|
||||
@@ -964,7 +930,6 @@ impl SequencerState {
|
||||
nudge_secs,
|
||||
cc_access: self.cc_access.as_deref(),
|
||||
speed_key,
|
||||
chain_key,
|
||||
mouse_x,
|
||||
mouse_y,
|
||||
mouse_down,
|
||||
@@ -1016,14 +981,9 @@ impl SequencerState {
|
||||
result
|
||||
}
|
||||
|
||||
fn read_variables(&self, completed: &[PatternId], any_step_fired: bool) -> VariableReads {
|
||||
let stopped = &self.buf_stopped;
|
||||
let needs_access = !completed.is_empty() || !stopped.is_empty() || any_step_fired;
|
||||
if !needs_access {
|
||||
return VariableReads {
|
||||
new_tempo: None,
|
||||
chain_transitions: Vec::new(),
|
||||
};
|
||||
fn read_tempo_variable(&self, any_step_fired: bool) -> Option<f64> {
|
||||
if !any_step_fired {
|
||||
return None;
|
||||
}
|
||||
|
||||
let vars = self.variables.load_full();
|
||||
@@ -1031,79 +991,48 @@ impl SequencerState {
|
||||
.get("__tempo__")
|
||||
.and_then(|v: &Value| v.as_float().ok());
|
||||
|
||||
let mut chain_transitions = Vec::new();
|
||||
let mut buf = String::with_capacity(24);
|
||||
for id in completed {
|
||||
let chain_key = format_chain_key(&mut buf, id.bank, id.pattern);
|
||||
if let Some(Value::Str(s, _)) = vars.get(chain_key) {
|
||||
if let Some(target) = parse_chain_target(s) {
|
||||
chain_transitions.push((*id, target));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove consumed variables (tempo and chain keys)
|
||||
let mut needs_removal = new_tempo.is_some();
|
||||
if !needs_removal {
|
||||
for id in completed.iter().chain(stopped.iter()) {
|
||||
if vars.contains_key(format_chain_key(&mut buf, id.bank, id.pattern)) {
|
||||
needs_removal = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if needs_removal {
|
||||
if new_tempo.is_some() {
|
||||
let mut new_vars = (*vars).clone();
|
||||
new_vars.remove("__tempo__");
|
||||
for id in completed {
|
||||
new_vars.remove(format_chain_key(&mut buf, id.bank, id.pattern));
|
||||
}
|
||||
for id in stopped {
|
||||
new_vars.remove(format_chain_key(&mut buf, id.bank, id.pattern));
|
||||
}
|
||||
self.variables.store(Arc::new(new_vars));
|
||||
}
|
||||
|
||||
VariableReads {
|
||||
new_tempo,
|
||||
chain_transitions,
|
||||
}
|
||||
new_tempo
|
||||
}
|
||||
|
||||
fn apply_chain_transitions(&mut self, transitions: Vec<(PatternId, PatternId)>) {
|
||||
for (source, target) in transitions {
|
||||
if !self
|
||||
.audio_state
|
||||
.pending_stops
|
||||
.iter()
|
||||
.any(|p| p.id == source)
|
||||
{
|
||||
fn apply_follow_ups(&mut self) {
|
||||
for completed_id in &self.buf_completed_iterations {
|
||||
let Some(pattern) = self.pattern_cache.get(completed_id.bank, completed_id.pattern) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match pattern.follow_up {
|
||||
FollowUp::Loop => {}
|
||||
FollowUp::Stop => {
|
||||
self.audio_state.pending_stops.push(PendingPattern {
|
||||
id: source,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
id: *completed_id,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
});
|
||||
}
|
||||
if !self
|
||||
.audio_state
|
||||
.pending_starts
|
||||
.iter()
|
||||
.any(|p| p.id == target)
|
||||
{
|
||||
let (quant, sync) = self
|
||||
.pattern_cache
|
||||
.get(target.bank, target.pattern)
|
||||
.map(|p| (p.quantization, p.sync_mode))
|
||||
.unwrap_or((LaunchQuantization::Bar, SyncMode::Reset));
|
||||
FollowUp::Chain { bank, pattern } => {
|
||||
self.audio_state.pending_stops.push(PendingPattern {
|
||||
id: *completed_id,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
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: quant,
|
||||
sync_mode: sync,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_shared_state(&self) -> SharedSequencerState {
|
||||
SharedSequencerState {
|
||||
@@ -1412,6 +1341,7 @@ mod tests {
|
||||
.collect(),
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
follow_up: FollowUp::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1528,68 +1458,6 @@ mod tests {
|
||||
assert!(!state.audio_state.active_patterns.contains_key(&pid(0, 0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chain_requires_active_source() {
|
||||
let mut state = make_state();
|
||||
|
||||
// Set up: pattern 0 (length 1) chains to pattern 1
|
||||
state.tick(tick_with(
|
||||
vec![
|
||||
SeqCommand::PatternUpdate {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
data: simple_pattern(1),
|
||||
},
|
||||
SeqCommand::PatternUpdate {
|
||||
bank: 0,
|
||||
pattern: 1,
|
||||
data: simple_pattern(4),
|
||||
},
|
||||
],
|
||||
0.0,
|
||||
));
|
||||
|
||||
// Start pattern 0
|
||||
state.tick(tick_with(
|
||||
vec![SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
0.5,
|
||||
));
|
||||
|
||||
// Set chain variable
|
||||
{
|
||||
let mut vars = (**state.variables.load()).clone();
|
||||
vars.insert(
|
||||
"__chain_0_0__".to_string(),
|
||||
Value::Str(std::sync::Arc::from("0:1"), None),
|
||||
);
|
||||
state.variables.store(Arc::new(vars));
|
||||
}
|
||||
|
||||
// Pattern 0 completes iteration AND gets stopped immediately in the same tick.
|
||||
// The stop removes it from active_patterns before chain evaluation,
|
||||
// so the chain guard (active_patterns.contains_key) blocks the transition.
|
||||
let output = state.tick(tick_with(
|
||||
vec![SeqCommand::PatternStop {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
}],
|
||||
1.0,
|
||||
));
|
||||
|
||||
assert!(output.shared_state.active_patterns.is_empty());
|
||||
assert!(!state
|
||||
.audio_state
|
||||
.pending_starts
|
||||
.iter()
|
||||
.any(|p| p.id == pid(0, 1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern_start_cancels_pending_stop() {
|
||||
let mut state = make_state();
|
||||
@@ -1779,67 +1647,6 @@ mod tests {
|
||||
assert!(!state.audio_state.active_patterns.contains_key(&pid(0, 0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stop_during_iteration_blocks_chain() {
|
||||
let mut state = make_state();
|
||||
|
||||
state.tick(tick_with(
|
||||
vec![
|
||||
SeqCommand::PatternUpdate {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
data: simple_pattern(1),
|
||||
},
|
||||
SeqCommand::PatternUpdate {
|
||||
bank: 0,
|
||||
pattern: 1,
|
||||
data: simple_pattern(4),
|
||||
},
|
||||
SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
],
|
||||
0.0,
|
||||
));
|
||||
|
||||
// Pattern 0 is now pending (will activate next tick when prev_beat >= 0)
|
||||
// Advance so it activates
|
||||
state.tick(tick_at(0.5, true));
|
||||
assert!(state.audio_state.active_patterns.contains_key(&pid(0, 0)));
|
||||
|
||||
// Set chain: 0:0 -> 0:1
|
||||
{
|
||||
let mut vars = (**state.variables.load()).clone();
|
||||
vars.insert(
|
||||
"__chain_0_0__".to_string(),
|
||||
Value::Str(std::sync::Arc::from("0:1"), None),
|
||||
);
|
||||
state.variables.store(Arc::new(vars));
|
||||
}
|
||||
|
||||
// Pattern 0 (length 1) completes iteration at beat=1.0 AND
|
||||
// an immediate stop removes it from active_patterns first.
|
||||
// Chain guard should block transition to pattern 1.
|
||||
state.tick(tick_with(
|
||||
vec![SeqCommand::PatternStop {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
}],
|
||||
1.0,
|
||||
));
|
||||
|
||||
assert!(!state.audio_state.active_patterns.contains_key(&pid(0, 1)));
|
||||
assert!(!state
|
||||
.audio_state
|
||||
.pending_starts
|
||||
.iter()
|
||||
.any(|p| p.id == pid(0, 1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_patterns_independent_quantization() {
|
||||
let mut state = make_state();
|
||||
@@ -2100,6 +1907,7 @@ mod tests {
|
||||
.collect(),
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
follow_up: FollowUp::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
use super::{InputContext, InputResult};
|
||||
use crate::commands::AppCommand;
|
||||
use crate::engine::SeqCommand;
|
||||
use crate::model::PatternSpeed;
|
||||
use crate::model::{FollowUp, PatternSpeed};
|
||||
use crate::state::{
|
||||
ConfirmAction, EditorTarget, EuclideanField, Modal, PatternField,
|
||||
PatternPropsField, RenameTarget,
|
||||
@@ -377,21 +377,45 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
speed,
|
||||
quantization,
|
||||
sync_mode,
|
||||
follow_up,
|
||||
} => {
|
||||
let (bank, pattern) = (*bank, *pattern);
|
||||
let is_chain = matches!(follow_up, FollowUp::Chain { .. });
|
||||
match key.code {
|
||||
KeyCode::Up => *field = field.prev(),
|
||||
KeyCode::Down | KeyCode::Tab => *field = field.next(),
|
||||
KeyCode::Up => *field = field.prev(is_chain),
|
||||
KeyCode::Down | KeyCode::Tab => *field = field.next(is_chain),
|
||||
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 {
|
||||
*b = b.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
PatternPropsField::ChainPattern => {
|
||||
if let FollowUp::Chain { pattern: p, .. } = follow_up {
|
||||
*p = p.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
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 {
|
||||
*b = (*b + 1).min(31);
|
||||
}
|
||||
}
|
||||
PatternPropsField::ChainPattern => {
|
||||
if let FollowUp::Chain { pattern: p, .. } = follow_up {
|
||||
*p = (*p + 1).min(31);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Char(c) => match field {
|
||||
@@ -418,6 +442,7 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
let speed_val = *speed;
|
||||
let quant_val = *quantization;
|
||||
let sync_val = *sync_mode;
|
||||
let follow_up_val = *follow_up;
|
||||
ctx.dispatch(AppCommand::StagePatternProps {
|
||||
bank,
|
||||
pattern,
|
||||
@@ -426,6 +451,7 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
speed: speed_val,
|
||||
quantization: quant_val,
|
||||
sync_mode: sync_val,
|
||||
follow_up: follow_up_val,
|
||||
});
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ pub use cagire_forth::{
|
||||
Variables, Word, WordCompile, WORDS,
|
||||
};
|
||||
pub use cagire_project::{
|
||||
load, save, Bank, LaunchQuantization, Pattern, PatternSpeed, Project, SyncMode, MAX_BANKS,
|
||||
MAX_PATTERNS,
|
||||
load, save, Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project, SyncMode,
|
||||
MAX_BANKS, MAX_PATTERNS,
|
||||
};
|
||||
pub use script::ScriptEngine;
|
||||
|
||||
@@ -60,7 +60,6 @@ pub fn update_cache(editor_ctx: &EditorContext) {
|
||||
nudge_secs: 0.0,
|
||||
cc_access: None,
|
||||
speed_key: "",
|
||||
chain_key: "",
|
||||
mouse_x: 0.5,
|
||||
mouse_y: 0.5,
|
||||
mouse_down: 0.0,
|
||||
|
||||
@@ -24,26 +24,37 @@ pub enum PatternPropsField {
|
||||
Speed,
|
||||
Quantization,
|
||||
SyncMode,
|
||||
FollowUp,
|
||||
ChainBank,
|
||||
ChainPattern,
|
||||
}
|
||||
|
||||
impl PatternPropsField {
|
||||
pub fn next(&self) -> Self {
|
||||
pub fn next(&self, follow_up_is_chain: bool) -> Self {
|
||||
match self {
|
||||
Self::Name => Self::Length,
|
||||
Self::Length => Self::Speed,
|
||||
Self::Speed => Self::Quantization,
|
||||
Self::Quantization => Self::SyncMode,
|
||||
Self::SyncMode => Self::SyncMode,
|
||||
Self::SyncMode => Self::FollowUp,
|
||||
Self::FollowUp if follow_up_is_chain => Self::ChainBank,
|
||||
Self::FollowUp => Self::FollowUp,
|
||||
Self::ChainBank => Self::ChainPattern,
|
||||
Self::ChainPattern => Self::ChainPattern,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prev(&self) -> Self {
|
||||
pub fn prev(&self, follow_up_is_chain: bool) -> Self {
|
||||
match self {
|
||||
Self::Name => Self::Name,
|
||||
Self::Length => Self::Name,
|
||||
Self::Speed => Self::Length,
|
||||
Self::Quantization => Self::Speed,
|
||||
Self::SyncMode => Self::Quantization,
|
||||
Self::FollowUp => Self::SyncMode,
|
||||
Self::ChainBank => Self::FollowUp,
|
||||
Self::ChainPattern if follow_up_is_chain => Self::ChainBank,
|
||||
Self::ChainPattern => Self::FollowUp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::model::{LaunchQuantization, PatternSpeed, SyncMode};
|
||||
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed, SyncMode};
|
||||
use crate::state::editor::{EuclideanField, PatternField, PatternPropsField};
|
||||
use crate::state::file_browser::FileBrowserState;
|
||||
|
||||
@@ -77,6 +77,7 @@ pub enum Modal {
|
||||
speed: PatternSpeed,
|
||||
quantization: LaunchQuantization,
|
||||
sync_mode: SyncMode,
|
||||
follow_up: FollowUp,
|
||||
},
|
||||
KeybindingsHelp {
|
||||
scroll: usize,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::engine::PatternChange;
|
||||
use crate::model::{LaunchQuantization, PatternSpeed, SyncMode};
|
||||
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed, SyncMode};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -21,6 +21,7 @@ pub struct StagedPropChange {
|
||||
pub speed: PatternSpeed,
|
||||
pub quantization: LaunchQuantization,
|
||||
pub sync_mode: SyncMode,
|
||||
pub follow_up: FollowUp,
|
||||
}
|
||||
|
||||
pub struct PlaybackState {
|
||||
|
||||
@@ -716,6 +716,8 @@ fn render_properties(
|
||||
bank: usize,
|
||||
pattern_idx: usize,
|
||||
) {
|
||||
use cagire_project::FollowUp;
|
||||
|
||||
let theme = theme::get();
|
||||
let pattern = &app.project_state.project.banks[bank].patterns[pattern_idx];
|
||||
|
||||
@@ -729,7 +731,7 @@ fn render_properties(
|
||||
let label_style = Style::new().fg(theme.ui.text_muted);
|
||||
let value_style = Style::new().fg(theme.ui.text_primary);
|
||||
|
||||
let rows: Vec<Line> = vec![
|
||||
let mut rows: Vec<Line> = vec![
|
||||
Line::from(vec![
|
||||
Span::styled(" Name ", label_style),
|
||||
Span::styled(name, value_style),
|
||||
@@ -752,5 +754,17 @@ fn render_properties(
|
||||
]),
|
||||
];
|
||||
|
||||
if pattern.follow_up != FollowUp::Loop {
|
||||
let follow_label = match pattern.follow_up {
|
||||
FollowUp::Loop => unreachable!(),
|
||||
FollowUp::Stop => "Stop".to_string(),
|
||||
FollowUp::Chain { bank: b, pattern: p } => format!("Chain B{:02}:P{:02}", b + 1, p + 1),
|
||||
};
|
||||
rows.push(Line::from(vec![
|
||||
Span::styled(" After ", label_style),
|
||||
Span::styled(follow_label, value_style),
|
||||
]));
|
||||
}
|
||||
|
||||
frame.render_widget(Paragraph::new(rows), area);
|
||||
}
|
||||
|
||||
@@ -609,37 +609,45 @@ fn render_modal(
|
||||
speed,
|
||||
quantization,
|
||||
sync_mode,
|
||||
follow_up,
|
||||
} => {
|
||||
use crate::model::FollowUp;
|
||||
use crate::state::PatternPropsField;
|
||||
|
||||
let is_chain = matches!(follow_up, FollowUp::Chain { .. });
|
||||
let modal_height = if is_chain { 16 } else { 14 };
|
||||
|
||||
let inner = ModalFrame::new(&format!(" Pattern B{:02}:P{:02} ", bank + 1, pattern + 1))
|
||||
.width(50)
|
||||
.height(12)
|
||||
.height(modal_height)
|
||||
.border_color(theme.modal.input)
|
||||
.render_centered(frame, term);
|
||||
|
||||
let speed_label = speed.label();
|
||||
let fields: Vec<(&str, &str, bool)> = vec![
|
||||
("Name", name.as_str(), *field == PatternPropsField::Name),
|
||||
(
|
||||
"Length",
|
||||
length.as_str(),
|
||||
*field == PatternPropsField::Length,
|
||||
),
|
||||
("Speed", &speed_label, *field == PatternPropsField::Speed),
|
||||
(
|
||||
"Quantization",
|
||||
quantization.label(),
|
||||
*field == PatternPropsField::Quantization,
|
||||
),
|
||||
(
|
||||
"Sync Mode",
|
||||
sync_mode.label(),
|
||||
*field == PatternPropsField::SyncMode,
|
||||
),
|
||||
let follow_up_label = match follow_up {
|
||||
FollowUp::Loop => "Loop".to_string(),
|
||||
FollowUp::Stop => "Stop".to_string(),
|
||||
FollowUp::Chain { bank: b, pattern: p } => {
|
||||
format!("Chain B{:02}:P{:02}", b + 1, p + 1)
|
||||
}
|
||||
};
|
||||
let mut fields: Vec<(&str, String, bool)> = vec![
|
||||
("Name", name.clone(), *field == PatternPropsField::Name),
|
||||
("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 {
|
||||
if let FollowUp::Chain { bank: b, pattern: p } = follow_up {
|
||||
fields.push((" Bank", format!("{:02}", b + 1), *field == PatternPropsField::ChainBank));
|
||||
fields.push((" Pattern", format!("{:02}", p + 1), *field == PatternPropsField::ChainPattern));
|
||||
}
|
||||
}
|
||||
|
||||
render_props_form(frame, inner, &fields);
|
||||
let fields_ref: Vec<(&str, &str, bool)> = fields.iter().map(|(l, v, s)| (*l, v.as_str(), *s)).collect();
|
||||
render_props_form(frame, inner, &fields_ref);
|
||||
|
||||
let hint_area = Rect::new(inner.x, inner.y + inner.height - 1, inner.width, 1);
|
||||
let hints = hint_line(&[
|
||||
|
||||
@@ -22,7 +22,6 @@ pub fn default_ctx() -> StepContext<'static> {
|
||||
nudge_secs: 0.0,
|
||||
cc_access: None,
|
||||
speed_key: "__speed_0_0__",
|
||||
chain_key: "__chain_0_0__",
|
||||
mouse_x: 0.5,
|
||||
mouse_y: 0.5,
|
||||
mouse_down: 0.0,
|
||||
|
||||
Reference in New Issue
Block a user