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

678
notes
View File

@@ -7,7 +7,7 @@
"steps": [
{
"active": true,
"script": "< < c5 c1 > m3 M3 m6 > note\n\"sine\" sound\n0.2 decay @",
"script": "0.2 0.9 rand gain 0.2 decay\n0.1 dur 1 4 rand fm 0.5 fmh\n\"tri\" < << c3 c2 >> 4 ! g3 > note sound @",
"source": null
},
{
@@ -25,6 +25,340 @@
"script": "",
"source": 0
},
{
"active": true,
"script": "0.2 0.9 rand gain 0.2 decay\n0.1 dur 1 4 rand fm 0.5 fmh\n0.5 0.9 rand verb \n< << c3 f3 >> 4 ! eb3 M3 > note \"tri\" sound @",
"source": null
},
{
"active": true,
"script": "",
"source": 0
},
{
"active": true,
"script": "",
"source": 0
},
{
"active": true,
"script": "",
"source": 0
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
}
],
"length": 8,
"speed": "Normal",
"name": null
},
{
"steps": [
{
"active": true,
"script": "\"ikick\" sound 2 gain @",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
}
],
"length": 4,
"speed": "Normal",
"name": null
},
{
"steps": [
{
"active": true,
"script": "\"hh\" sound 0.5 comb 400 combfreq @\n4 scale!\n{\n\"noise\" sound 0.05 dur 0.0 1.0 rand gain 0.05 decay 1 100 rand @\n} 2 div for",
"source": null
},
{
"active": true,
"script": "",
"source": 0
},
{
"active": true,
"script": "<< \"hh\" 4 ! \"crbongo\" >> sound @\n{ \"snare\" sound 0.2 dur 0.1 decay 200 freq @ } 2 every ?",
"source": null
},
{
"active": true,
"script": "",
"source": 0
},
{
"active": true,
"script": "",
@@ -174,7 +508,7 @@
"steps": [
{
"active": true,
"script": "",
"script": " {\n {\n | eb3 g3 bb3 c4 | note 0.1 decay 0.1 dur \"tri\" sound\n 0.5 fmh 0.5 delay 4.5 delaytime 0.75 delayfeedback\n 2 fm 0.9 verb 4 orbit @\n } 4 every ? \n} 4 stack for",
"source": null
},
{
@@ -333,341 +667,7 @@
"source": null
}
],
"length": 16,
"speed": "Normal",
"name": null
},
{
"steps": [
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
}
],
"length": 16,
"speed": "Normal",
"name": null
},
{
"steps": [
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
},
{
"active": true,
"script": "",
"source": null
}
],
"length": 16,
"length": 4,
"speed": "Normal",
"name": null
},
@@ -42834,6 +42834,8 @@
"name": null
}
],
"sample_paths": [],
"sample_paths": [
"/Users/bubo/Documents/samples"
],
"tempo": 110.0
}

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]