Lots + MIDI implementation

This commit is contained in:
2026-01-31 23:13:51 +01:00
parent 0a4f1419eb
commit fed39c01e8
34 changed files with 4323 additions and 191 deletions

View File

@@ -5,6 +5,6 @@ mod types;
mod vm;
mod words;
pub use types::{Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Value, Variables};
pub use types::{CcMemory, Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Value, Variables};
pub use vm::Forth;
pub use words::{Word, WordCompile, WORDS};

View File

@@ -86,4 +86,11 @@ pub enum Op {
Generate,
GeomRange,
Times,
// MIDI
MidiEmit,
GetMidiCC,
MidiClock,
MidiStart,
MidiStop,
MidiContinue,
}

View File

@@ -29,6 +29,7 @@ pub struct StepContext {
pub speed: f64,
pub fill: bool,
pub nudge_secs: f64,
pub cc_memory: Option<CcMemory>,
}
impl StepContext {
@@ -41,6 +42,7 @@ pub type Variables = Arc<Mutex<HashMap<String, Value>>>;
pub type Dictionary = Arc<Mutex<HashMap<String, Vec<Op>>>>;
pub type Rng = Arc<Mutex<StdRng>>;
pub type Stack = Arc<Mutex<Vec<Value>>>;
pub type CcMemory = Arc<Mutex<[[u8; 128]; 16]>>;
pub(super) type CmdSnapshot<'a> = (Option<&'a Value>, &'a [(String, Value)]);
#[derive(Clone, Debug)]
@@ -124,6 +126,7 @@ pub(super) struct CmdRegister {
deltas: Vec<Value>,
}
impl CmdRegister {
pub(super) fn set_sound(&mut self, val: Value) {
self.sound = Some(val);

View File

@@ -790,6 +790,75 @@ impl Forth {
val *= ratio;
}
}
// MIDI operations
Op::MidiEmit => {
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
let get_int = |name: &str| -> Option<i64> {
params
.iter()
.rev()
.find(|(k, _)| k == name)
.and_then(|(_, v)| v.as_int().ok())
};
let get_float = |name: &str| -> Option<f64> {
params
.iter()
.rev()
.find(|(k, _)| k == name)
.and_then(|(_, v)| v.as_float().ok())
};
let chan = get_int("chan")
.map(|c| (c.clamp(1, 16) - 1) as u8)
.unwrap_or(0);
if let (Some(cc), Some(val)) = (get_int("ccnum"), get_int("ccout")) {
let cc = cc.clamp(0, 127) as u8;
let val = val.clamp(0, 127) as u8;
outputs.push(format!("/midi/cc/{cc}/{val}/chan/{chan}"));
} else if let Some(bend) = get_float("bend") {
let bend_clamped = bend.clamp(-1.0, 1.0);
let bend_14bit = ((bend_clamped + 1.0) * 8191.5) as u16;
outputs.push(format!("/midi/bend/{bend_14bit}/chan/{chan}"));
} else if let Some(pressure) = get_int("pressure") {
let pressure = pressure.clamp(0, 127) as u8;
outputs.push(format!("/midi/pressure/{pressure}/chan/{chan}"));
} else if let Some(program) = get_int("program") {
let program = program.clamp(0, 127) as u8;
outputs.push(format!("/midi/program/{program}/chan/{chan}"));
} else {
let note = get_int("note").unwrap_or(60).clamp(0, 127) as u8;
let velocity = get_int("velocity").unwrap_or(100).clamp(0, 127) as u8;
let dur = get_float("dur").unwrap_or(1.0);
let dur_secs = dur * ctx.step_duration();
outputs.push(format!("/midi/note/{note}/vel/{velocity}/chan/{chan}/dur/{dur_secs}"));
}
}
Op::MidiClock => {
outputs.push("/midi/clock".to_string());
}
Op::MidiStart => {
outputs.push("/midi/start".to_string());
}
Op::MidiStop => {
outputs.push("/midi/stop".to_string());
}
Op::MidiContinue => {
outputs.push("/midi/continue".to_string());
}
Op::GetMidiCC => {
let chan = stack.pop().ok_or("stack underflow")?.as_int()?;
let cc = stack.pop().ok_or("stack underflow")?.as_int()?;
let cc_clamped = (cc.clamp(0, 127)) as usize;
let chan_clamped = (chan.clamp(1, 16) - 1) as usize;
let val = ctx
.cc_memory
.as_ref()
.and_then(|mem| mem.lock().ok())
.map(|mem| mem[chan_clamped][cc_clamped])
.unwrap_or(0);
stack.push(Value::Int(val as i64, None));
}
}
pc += 1;
}

View File

@@ -2280,6 +2280,127 @@ pub const WORDS: &[Word] = &[
compile: Simple,
varargs: false,
},
// MIDI
Word {
name: "chan",
aliases: &[],
category: "MIDI",
stack: "(n --)",
desc: "Set MIDI channel 1-16",
example: "1 chan",
compile: Param,
varargs: false,
},
Word {
name: "ccnum",
aliases: &[],
category: "MIDI",
stack: "(n --)",
desc: "Set MIDI CC number 0-127",
example: "1 ccnum",
compile: Param,
varargs: false,
},
Word {
name: "ccout",
aliases: &[],
category: "MIDI",
stack: "(n --)",
desc: "Set MIDI CC output value 0-127",
example: "64 ccout",
compile: Param,
varargs: false,
},
Word {
name: "bend",
aliases: &[],
category: "MIDI",
stack: "(f --)",
desc: "Set pitch bend -1.0 to 1.0 (0 = center)",
example: "0.5 bend",
compile: Param,
varargs: false,
},
Word {
name: "pressure",
aliases: &[],
category: "MIDI",
stack: "(n --)",
desc: "Set channel pressure (aftertouch) 0-127",
example: "64 pressure",
compile: Param,
varargs: false,
},
Word {
name: "program",
aliases: &[],
category: "MIDI",
stack: "(n --)",
desc: "Set program change number 0-127",
example: "0 program",
compile: Param,
varargs: false,
},
Word {
name: "m.",
aliases: &[],
category: "MIDI",
stack: "(--)",
desc: "Emit MIDI message from params (note/cc/bend/pressure/program)",
example: "60 note 100 velocity 1 chan m.",
compile: Simple,
varargs: false,
},
Word {
name: "mclock",
aliases: &[],
category: "MIDI",
stack: "(--)",
desc: "Send MIDI clock pulse (24 per quarter note)",
example: "mclock",
compile: Simple,
varargs: false,
},
Word {
name: "mstart",
aliases: &[],
category: "MIDI",
stack: "(--)",
desc: "Send MIDI start message",
example: "mstart",
compile: Simple,
varargs: false,
},
Word {
name: "mstop",
aliases: &[],
category: "MIDI",
stack: "(--)",
desc: "Send MIDI stop message",
example: "mstop",
compile: Simple,
varargs: false,
},
Word {
name: "mcont",
aliases: &[],
category: "MIDI",
stack: "(--)",
desc: "Send MIDI continue message",
example: "mcont",
compile: Simple,
varargs: false,
},
Word {
name: "ccval",
aliases: &[],
category: "MIDI",
stack: "(cc chan -- val)",
desc: "Read CC value 0-127 from MIDI input",
example: "1 1 ccval",
compile: Simple,
varargs: false,
},
];
pub(super) fn simple_op(name: &str) -> Option<Op> {
@@ -2357,6 +2478,12 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
"gen" => Op::Generate,
"geom.." => Op::GeomRange,
"times" => Op::Times,
"m." => Op::MidiEmit,
"ccval" => Op::GetMidiCC,
"mclock" => Op::MidiClock,
"mstart" => Op::MidiStart,
"mstop" => Op::MidiStop,
"mcont" => Op::MidiContinue,
_ => return None,
})
}