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:
@@ -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,76 +991,45 @@ 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)
|
||||
{
|
||||
self.audio_state.pending_stops.push(PendingPattern {
|
||||
id: source,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
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));
|
||||
self.audio_state.pending_starts.push(PendingPattern {
|
||||
id: target,
|
||||
quantization: quant,
|
||||
sync_mode: sync,
|
||||
});
|
||||
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: *completed_id,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: 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: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user