This commit is contained in:
2026-01-22 10:08:05 +01:00
parent 409e815414
commit 88b6f64a72
10 changed files with 43268 additions and 388 deletions

1008
notes

File diff suppressed because it is too large Load Diff

42841
something Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -31,7 +31,6 @@ pub struct App {
pub patterns_nav: PatternsNav, pub patterns_nav: PatternsNav,
pub metrics: Metrics, pub metrics: Metrics,
pub sample_pool_mb: f32,
pub script_engine: ScriptEngine, pub script_engine: ScriptEngine,
pub variables: Variables, pub variables: Variables,
pub rng: Rng, pub rng: Rng,
@@ -61,7 +60,6 @@ impl App {
patterns_nav: PatternsNav::default(), patterns_nav: PatternsNav::default(),
metrics: Metrics::default(), metrics: Metrics::default(),
sample_pool_mb: 0.0,
variables, variables,
rng, rng,
live_keys, live_keys,
@@ -280,7 +278,6 @@ impl App {
let ctx = StepContext { let ctx = StepContext {
step: step_idx, step: step_idx,
beat: link.beat(), beat: link.beat(),
bank,
pattern, pattern,
tempo: link.tempo(), tempo: link.tempo(),
phase: link.phase(), phase: link.phase(),
@@ -355,7 +352,6 @@ impl App {
let ctx = StepContext { let ctx = StepContext {
step: step_idx, step: step_idx,
beat: 0.0, beat: 0.0,
bank,
pattern, pattern,
tempo: link.tempo(), tempo: link.tempo(),
phase: 0.0, phase: 0.0,

View File

@@ -87,13 +87,13 @@ pub struct ActivePatternState {
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct SharedSequencerState { pub struct SharedSequencerState {
pub active_patterns: Vec<ActivePatternState>, pub active_patterns: Vec<ActivePatternState>,
pub pattern_traces: HashMap<PatternId, Vec<SourceSpan>>, pub step_traces: HashMap<(usize, usize, usize), Vec<SourceSpan>>,
pub event_count: usize, pub event_count: usize,
} }
pub struct SequencerSnapshot { pub struct SequencerSnapshot {
pub active_patterns: Vec<ActivePatternState>, pub active_patterns: Vec<ActivePatternState>,
pub pattern_traces: HashMap<PatternId, Vec<SourceSpan>>, pub step_traces: HashMap<(usize, usize, usize), Vec<SourceSpan>>,
pub event_count: usize, pub event_count: usize,
} }
@@ -118,8 +118,8 @@ impl SequencerSnapshot {
.map(|p| p.iter) .map(|p| p.iter)
} }
pub fn get_trace(&self, bank: usize, pattern: usize) -> Option<&Vec<SourceSpan>> { pub fn get_trace(&self, bank: usize, pattern: usize, step: usize) -> Option<&Vec<SourceSpan>> {
self.pattern_traces.get(&PatternId { bank, pattern }) self.step_traces.get(&(bank, pattern, step))
} }
} }
@@ -136,7 +136,7 @@ impl SequencerHandle {
let state = self.shared_state.lock().unwrap(); let state = self.shared_state.lock().unwrap();
SequencerSnapshot { SequencerSnapshot {
active_patterns: state.active_patterns.clone(), active_patterns: state.active_patterns.clone(),
pattern_traces: state.pattern_traces.clone(), step_traces: state.step_traces.clone(),
event_count: state.event_count, event_count: state.event_count,
} }
} }
@@ -284,6 +284,7 @@ impl RunsCounter {
} }
} }
#[allow(clippy::too_many_arguments)]
fn sequencer_loop( fn sequencer_loop(
cmd_rx: Receiver<SeqCommand>, cmd_rx: Receiver<SeqCommand>,
audio_tx: Sender<AudioCommand>, audio_tx: Sender<AudioCommand>,
@@ -301,7 +302,7 @@ fn sequencer_loop(
let mut audio_state = AudioState::new(); let mut audio_state = AudioState::new();
let mut pattern_cache = PatternCache::new(); let mut pattern_cache = PatternCache::new();
let mut runs_counter = RunsCounter::new(); let mut runs_counter = RunsCounter::new();
let mut pattern_traces: HashMap<PatternId, Vec<SourceSpan>> = HashMap::new(); let mut step_traces: HashMap<(usize, usize, usize), Vec<SourceSpan>> = HashMap::new();
let mut event_count: usize = 0; let mut event_count: usize = 0;
loop { loop {
@@ -360,13 +361,15 @@ fn sequencer_loop(
} }
for id in audio_state.pending_stops.drain(..) { for id in audio_state.pending_stops.drain(..) {
audio_state.active_patterns.remove(&id); audio_state.active_patterns.remove(&id);
pattern_traces.remove(&id); step_traces.retain(|&(bank, pattern, _), _| {
bank != id.bank || pattern != id.pattern
});
} }
} }
let prev_beat = audio_state.prev_beat; let prev_beat = audio_state.prev_beat;
for (id, active) in audio_state.active_patterns.iter_mut() { for (_id, active) in audio_state.active_patterns.iter_mut() {
let Some(pattern) = pattern_cache.get(active.bank, active.pattern) else { let Some(pattern) = pattern_cache.get(active.bank, active.pattern) else {
continue; continue;
}; };
@@ -391,7 +394,6 @@ fn sequencer_loop(
let ctx = StepContext { let ctx = StepContext {
step: step_idx, step: step_idx,
beat, beat,
bank: active.bank,
pattern: active.pattern, pattern: active.pattern,
tempo, tempo,
phase: beat % quantum, phase: beat % quantum,
@@ -406,8 +408,10 @@ fn sequencer_loop(
if let Ok(cmds) = if let Ok(cmds) =
script_engine.evaluate_with_trace(script, &ctx, &mut trace) script_engine.evaluate_with_trace(script, &ctx, &mut trace)
{ {
pattern_traces step_traces.insert(
.insert(*id, std::mem::take(&mut trace.selected_spans)); (active.bank, active.pattern, step_idx),
std::mem::take(&mut trace.selected_spans),
);
for cmd in cmds { for cmd in cmds {
match audio_tx.try_send(AudioCommand::Evaluate(cmd)) { match audio_tx.try_send(AudioCommand::Evaluate(cmd)) {
Ok(()) => { Ok(()) => {
@@ -450,7 +454,7 @@ fn sequencer_loop(
iter: a.iter, iter: a.iter,
}) })
.collect(); .collect();
state.pattern_traces = pattern_traces.clone(); state.step_traces = step_traces.clone();
state.event_count = event_count; state.event_count = event_count;
} }

View File

@@ -17,7 +17,6 @@ pub struct ExecutionTrace {
pub struct StepContext { pub struct StepContext {
pub step: usize, pub step: usize,
pub beat: f64, pub beat: f64,
pub bank: usize,
pub pattern: usize, pub pattern: usize,
pub tempo: f64, pub tempo: f64,
pub phase: f64, pub phase: f64,
@@ -1751,7 +1750,7 @@ fn parse_interval(name: &str) -> Option<i64> {
Some(simple) Some(simple)
} }
fn compile_word(name: &str, ops: &mut Vec<Op>) -> bool { fn compile_word(name: &str, span: Option<SourceSpan>, ops: &mut Vec<Op>) -> bool {
for word in WORDS { for word in WORDS {
if word.name == name { if word.name == name {
match &word.compile { match &word.compile {
@@ -1762,7 +1761,7 @@ fn compile_word(name: &str, ops: &mut Vec<Op>) -> bool {
} }
Context(ctx) => ops.push(Op::GetContext((*ctx).into())), Context(ctx) => ops.push(Op::GetContext((*ctx).into())),
Param => ops.push(Op::SetParam(name.into())), Param => ops.push(Op::SetParam(name.into())),
Alias(target) => return compile_word(target, ops), Alias(target) => return compile_word(target, span, ops),
Probability(p) => { Probability(p) => {
ops.push(Op::PushFloat(*p, None)); ops.push(Op::PushFloat(*p, None));
ops.push(Op::ChanceExec); ops.push(Op::ChanceExec);
@@ -1775,7 +1774,7 @@ fn compile_word(name: &str, ops: &mut Vec<Op>) -> bool {
// @varname - fetch variable // @varname - fetch variable
if let Some(var_name) = name.strip_prefix('@') { if let Some(var_name) = name.strip_prefix('@') {
if !var_name.is_empty() { if !var_name.is_empty() {
ops.push(Op::PushStr(var_name.to_string(), None)); ops.push(Op::PushStr(var_name.to_string(), span));
ops.push(Op::Get); ops.push(Op::Get);
return true; return true;
} }
@@ -1784,7 +1783,7 @@ fn compile_word(name: &str, ops: &mut Vec<Op>) -> bool {
// !varname - store into variable // !varname - store into variable
if let Some(var_name) = name.strip_prefix('!') { if let Some(var_name) = name.strip_prefix('!') {
if !var_name.is_empty() { if !var_name.is_empty() {
ops.push(Op::PushStr(var_name.to_string(), None)); ops.push(Op::PushStr(var_name.to_string(), span));
ops.push(Op::Set); ops.push(Op::Set);
return true; return true;
} }
@@ -1792,14 +1791,14 @@ fn compile_word(name: &str, ops: &mut Vec<Op>) -> bool {
// Note names: c4, c#4, cs4, eb4, etc. -> MIDI number // Note names: c4, c#4, cs4, eb4, etc. -> MIDI number
if let Some(midi) = parse_note_name(name) { if let Some(midi) = parse_note_name(name) {
ops.push(Op::PushInt(midi, None)); ops.push(Op::PushInt(midi, span));
return true; return true;
} }
// Intervals: m3, M3, P5, etc. -> dup top, add semitones (for chord building) // Intervals: m3, M3, P5, etc. -> dup top, add semitones (for chord building)
if let Some(semitones) = parse_interval(name) { if let Some(semitones) = parse_interval(name) {
ops.push(Op::Dup); ops.push(Op::Dup);
ops.push(Op::PushInt(semitones, None)); ops.push(Op::PushInt(semitones, span));
ops.push(Op::Add); ops.push(Op::Add);
return true; return true;
} }
@@ -1827,8 +1826,8 @@ enum Token {
Float(f64, SourceSpan), Float(f64, SourceSpan),
Str(String, SourceSpan), Str(String, SourceSpan),
Word(String, SourceSpan), Word(String, SourceSpan),
QuoteStart(SourceSpan), QuoteStart,
QuoteEnd(SourceSpan), QuoteEnd,
} }
fn tokenize(input: &str) -> Vec<Token> { fn tokenize(input: &str) -> Vec<Token> {
@@ -1869,22 +1868,14 @@ fn tokenize(input: &str) -> Vec<Token> {
} }
if c == '{' { if c == '{' {
let start = pos;
chars.next(); chars.next();
tokens.push(Token::QuoteStart(SourceSpan { tokens.push(Token::QuoteStart);
start,
end: start + 1,
}));
continue; continue;
} }
if c == '}' { if c == '}' {
let start = pos;
chars.next(); chars.next();
tokens.push(Token::QuoteEnd(SourceSpan { tokens.push(Token::QuoteEnd);
start,
end: start + 1,
}));
continue; continue;
} }
@@ -1923,15 +1914,15 @@ fn compile(tokens: &[Token]) -> Result<Vec<Op>, String> {
Token::Int(n, span) => ops.push(Op::PushInt(*n, Some(*span))), Token::Int(n, span) => ops.push(Op::PushInt(*n, Some(*span))),
Token::Float(f, span) => ops.push(Op::PushFloat(*f, Some(*span))), Token::Float(f, span) => ops.push(Op::PushFloat(*f, Some(*span))),
Token::Str(s, span) => ops.push(Op::PushStr(s.clone(), Some(*span))), Token::Str(s, span) => ops.push(Op::PushStr(s.clone(), Some(*span))),
Token::QuoteStart(_) => { Token::QuoteStart => {
let (quote_ops, consumed) = compile_quotation(&tokens[i + 1..])?; let (quote_ops, consumed) = compile_quotation(&tokens[i + 1..])?;
i += consumed; i += consumed;
ops.push(Op::Quotation(quote_ops)); ops.push(Op::Quotation(quote_ops));
} }
Token::QuoteEnd(_) => { Token::QuoteEnd => {
return Err("unexpected }".into()); return Err("unexpected }".into());
} }
Token::Word(w, _) => { Token::Word(w, span) => {
let word = w.as_str(); let word = w.as_str();
if word == "|" { if word == "|" {
if pipe_parity { if pipe_parity {
@@ -1952,7 +1943,7 @@ fn compile(tokens: &[Token]) -> Result<Vec<Op>, String> {
ops.push(Op::Branch(else_ops.len())); ops.push(Op::Branch(else_ops.len()));
ops.extend(else_ops); ops.extend(else_ops);
} }
} else if !compile_word(word, &mut ops) { } else if !compile_word(word, Some(*span), &mut ops) {
return Err(format!("unknown word: {word}")); return Err(format!("unknown word: {word}"));
} }
} }
@@ -1969,8 +1960,8 @@ fn compile_quotation(tokens: &[Token]) -> Result<(Vec<Op>, usize), String> {
for (i, tok) in tokens.iter().enumerate() { for (i, tok) in tokens.iter().enumerate() {
match tok { match tok {
Token::QuoteStart(_) => depth += 1, Token::QuoteStart => depth += 1,
Token::QuoteEnd(_) => { Token::QuoteEnd => {
depth -= 1; depth -= 1;
if depth == 0 { if depth == 0 {
end_pos = Some(i); end_pos = Some(i);
@@ -2039,10 +2030,12 @@ impl Forth {
} }
} }
#[allow(dead_code)]
pub fn stack(&self) -> Vec<Value> { pub fn stack(&self) -> Vec<Value> {
self.stack.lock().unwrap().clone() self.stack.lock().unwrap().clone()
} }
#[allow(dead_code)]
pub fn clear_stack(&self) { pub fn clear_stack(&self) {
self.stack.lock().unwrap().clear(); self.stack.lock().unwrap().clear();
} }
@@ -2104,6 +2097,7 @@ impl Forth {
Ok(outputs) Ok(outputs)
} }
#[allow(clippy::too_many_arguments)]
fn execute_ops( fn execute_ops(
&self, &self,
ops: &[Op], ops: &[Op],

View File

@@ -13,6 +13,9 @@ pub enum TokenKind {
Sound, Sound,
Param, Param,
Context, Context,
Note,
Interval,
Variable,
Default, Default,
} }
@@ -28,6 +31,9 @@ impl TokenKind {
TokenKind::Sound => Style::default().fg(Color::Rgb(100, 220, 200)), TokenKind::Sound => Style::default().fg(Color::Rgb(100, 220, 200)),
TokenKind::Param => Style::default().fg(Color::Rgb(180, 150, 220)), TokenKind::Param => Style::default().fg(Color::Rgb(180, 150, 220)),
TokenKind::Context => Style::default().fg(Color::Rgb(220, 180, 120)), TokenKind::Context => Style::default().fg(Color::Rgb(220, 180, 120)),
TokenKind::Note => Style::default().fg(Color::Rgb(120, 200, 160)),
TokenKind::Interval => Style::default().fg(Color::Rgb(160, 200, 120)),
TokenKind::Variable => Style::default().fg(Color::Rgb(200, 140, 180)),
TokenKind::Default => Style::default().fg(Color::Rgb(200, 200, 200)), TokenKind::Default => Style::default().fg(Color::Rgb(200, 200, 200)),
} }
} }
@@ -39,18 +45,21 @@ pub struct Token {
pub kind: TokenKind, pub kind: TokenKind,
} }
const STACK_OPS: &[&str] = &["dup", "drop", "swap", "over", "rot", "nip", "tuck"]; const STACK_OPS: &[&str] = &["dup", "dupn", "drop", "swap", "over", "rot", "nip", "tuck"];
const OPERATORS: &[&str] = &[ const OPERATORS: &[&str] = &[
"+", "-", "*", "/", "mod", "neg", "abs", "min", "max", "=", "<>", "<", ">", "<=", ">=", "and", "+", "-", "*", "/", "mod", "neg", "abs", "min", "max", "=", "<>", "<", ">", "<=", ">=", "and",
"or", "not", "or", "not", "ceil", "floor", "round", "mtof", "ftom",
]; ];
const KEYWORDS: &[&str] = &[ const KEYWORDS: &[&str] = &[
"if", "else", "then", "emit", "get", "set", "rand", "rrand", "seed", "cycle", "choose", "if", "else", "then", "emit", "rand", "rrand", "seed", "cycle", "choose", "chance", "[", "]",
"chance", "[", "]", "zoom", "scale!", "stack", "echo", "necho", "for", "div", "each", "at", "pop", "adsr", "ad",
"?", "!?", "<<", ">>", "|", "@", "!", "pcycle", "tempo!", "prob", "sometimes", "often",
"rarely", "almostAlways", "almostNever", "always", "never", "coin", "fill", "iter", "every",
"gt", "lt",
]; ];
const SOUND: &[&str] = &["sound", "s"]; const SOUND: &[&str] = &["sound", "s"];
const CONTEXT: &[&str] = &[ const CONTEXT: &[&str] = &[
"step", "beat", "bank", "pattern", "tempo", "phase", "slot", "runs", "step", "beat", "bank", "pattern", "tempo", "phase", "slot", "runs", "stepdur",
]; ];
const PARAMS: &[&str] = &[ const PARAMS: &[&str] = &[
"time", "time",
@@ -160,6 +169,28 @@ const PARAMS: &[&str] = &[
"cut", "cut",
"reset", "reset",
]; ];
const INTERVALS: &[&str] = &[
"P1", "unison", "m2", "M2", "m3", "M3", "P4", "aug4", "dim5", "tritone", "P5", "m6", "M6",
"m7", "M7", "P8", "oct", "m9", "M9", "m10", "M10", "P11", "aug11", "P12", "m13", "M13", "m14",
"M14", "P15",
];
fn is_note(word: &str) -> bool {
let bytes = word.as_bytes();
if bytes.len() < 2 {
return false;
}
if !matches!(bytes[0], b'a'..=b'g' | b'A'..=b'G') {
return false;
}
let rest = &bytes[1..];
let digits_start = if rest.first().is_some_and(|&b| b == b'#' || b == b's' || b == b'b') {
1
} else {
0
};
rest[digits_start..].iter().all(|&b| b.is_ascii_digit()) && digits_start < rest.len()
}
pub fn tokenize_line(line: &str) -> Vec<Token> { pub fn tokenize_line(line: &str) -> Vec<Token> {
let mut tokens = Vec::new(); let mut tokens = Vec::new();
@@ -252,6 +283,18 @@ fn classify_word(word: &str) -> TokenKind {
return TokenKind::Param; return TokenKind::Param;
} }
if INTERVALS.contains(&word) {
return TokenKind::Interval;
}
if is_note(word) {
return TokenKind::Note;
}
if word.len() > 1 && (word.starts_with('@') || word.starts_with('!')) {
return TokenKind::Variable;
}
TokenKind::Default TokenKind::Default
} }

View File

@@ -180,7 +180,7 @@ fn render_step_preview(frame: &mut Frame, app: &App, snapshot: &SequencerSnapsho
} }
let runtime_spans = if app.ui.runtime_highlight && app.playback.playing { let runtime_spans = if app.ui.runtime_highlight && app.playback.playing {
snapshot.get_trace(app.editor_ctx.bank, app.editor_ctx.pattern) snapshot.get_trace(app.editor_ctx.bank, app.editor_ctx.pattern, step_idx)
} else { } else {
None None
}; };

View File

@@ -362,7 +362,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
let (cursor_row, cursor_col) = app.editor_ctx.text.cursor(); let (cursor_row, cursor_col) = app.editor_ctx.text.cursor();
let runtime_spans = if app.ui.runtime_highlight && app.playback.playing { let runtime_spans = if app.ui.runtime_highlight && app.playback.playing {
snapshot.get_trace(app.editor_ctx.bank, app.editor_ctx.pattern) snapshot.get_trace(app.editor_ctx.bank, app.editor_ctx.pattern, app.editor_ctx.step)
} else { } else {
None None
}; };

View File

@@ -8,7 +8,6 @@ pub fn default_ctx() -> StepContext {
StepContext { StepContext {
step: 0, step: 0,
beat: 0.0, beat: 0.0,
bank: 0,
pattern: 0, pattern: 0,
tempo: 120.0, tempo: 120.0,
phase: 0.0, phase: 0.0,
@@ -127,6 +126,7 @@ pub fn expect_outputs(script: &str, count: usize) -> Vec<String> {
outputs outputs
} }
#[allow(dead_code)]
pub fn expect_output_contains(script: &str, substr: &str) { pub fn expect_output_contains(script: &str, substr: &str) {
let outputs = expect_outputs(script, 1); let outputs = expect_outputs(script, 1);
assert!( assert!(

View File

@@ -46,7 +46,7 @@ fn multiple_emits() {
#[test] #[test]
fn subdivide_each() { fn subdivide_each() {
let outputs = expect_outputs(r#""kick" s 4 div each"#, 4); let _outputs = expect_outputs(r#""kick" s 4 div each"#, 4);
} }
#[test] #[test]