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

View File

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

View File

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

View File

@@ -13,6 +13,9 @@ pub enum TokenKind {
Sound,
Param,
Context,
Note,
Interval,
Variable,
Default,
}
@@ -28,6 +31,9 @@ impl TokenKind {
TokenKind::Sound => Style::default().fg(Color::Rgb(100, 220, 200)),
TokenKind::Param => Style::default().fg(Color::Rgb(180, 150, 220)),
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)),
}
}
@@ -39,18 +45,21 @@ pub struct Token {
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] = &[
"+", "-", "*", "/", "mod", "neg", "abs", "min", "max", "=", "<>", "<", ">", "<=", ">=", "and",
"or", "not",
"or", "not", "ceil", "floor", "round", "mtof", "ftom",
];
const KEYWORDS: &[&str] = &[
"if", "else", "then", "emit", "get", "set", "rand", "rrand", "seed", "cycle", "choose",
"chance", "[", "]",
"if", "else", "then", "emit", "rand", "rrand", "seed", "cycle", "choose", "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 CONTEXT: &[&str] = &[
"step", "beat", "bank", "pattern", "tempo", "phase", "slot", "runs",
"step", "beat", "bank", "pattern", "tempo", "phase", "slot", "runs", "stepdur",
];
const PARAMS: &[&str] = &[
"time",
@@ -160,6 +169,28 @@ const PARAMS: &[&str] = &[
"cut",
"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> {
let mut tokens = Vec::new();
@@ -252,6 +283,18 @@ fn classify_word(word: &str) -> TokenKind {
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
}

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 {
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 {
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 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 {
None
};

View File

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

View File

@@ -46,7 +46,7 @@ fn multiple_emits() {
#[test]
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]