Feat: all and noall words
This commit is contained in:
@@ -126,6 +126,9 @@ pub enum Op {
|
||||
ModSlide(u8),
|
||||
ModRnd(u8),
|
||||
ModEnv,
|
||||
// Global params
|
||||
EmitAll,
|
||||
ClearGlobal,
|
||||
// MIDI
|
||||
MidiEmit,
|
||||
GetMidiCC,
|
||||
|
||||
@@ -160,6 +160,7 @@ pub(super) struct CmdRegister {
|
||||
sound: Option<Value>,
|
||||
params: Vec<(&'static str, Value)>,
|
||||
deltas: Vec<Value>,
|
||||
global_params: Vec<(&'static str, Value)>,
|
||||
}
|
||||
|
||||
impl CmdRegister {
|
||||
@@ -168,6 +169,7 @@ impl CmdRegister {
|
||||
sound: None,
|
||||
params: Vec::with_capacity(16),
|
||||
deltas: Vec::with_capacity(4),
|
||||
global_params: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +205,28 @@ impl CmdRegister {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn global_params(&self) -> &[(&'static str, Value)] {
|
||||
&self.global_params
|
||||
}
|
||||
|
||||
pub(super) fn commit_global(&mut self) {
|
||||
self.global_params.append(&mut self.params);
|
||||
self.sound = None;
|
||||
self.deltas.clear();
|
||||
}
|
||||
|
||||
pub(super) fn clear_global(&mut self) {
|
||||
self.global_params.clear();
|
||||
}
|
||||
|
||||
pub fn set_global(&mut self, params: Vec<(&'static str, Value)>) {
|
||||
self.global_params = params;
|
||||
}
|
||||
|
||||
pub fn take_global(&mut self) -> Vec<(&'static str, Value)> {
|
||||
std::mem::take(&mut self.global_params)
|
||||
}
|
||||
|
||||
pub(super) fn clear(&mut self) {
|
||||
self.sound = None;
|
||||
self.params.clear();
|
||||
|
||||
@@ -19,6 +19,7 @@ pub struct Forth {
|
||||
vars: Variables,
|
||||
dict: Dictionary,
|
||||
rng: Rng,
|
||||
global_params: Mutex<Vec<(&'static str, Value)>>,
|
||||
}
|
||||
|
||||
impl Forth {
|
||||
@@ -28,6 +29,7 @@ impl Forth {
|
||||
vars,
|
||||
dict,
|
||||
rng,
|
||||
global_params: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +41,10 @@ impl Forth {
|
||||
self.stack.lock().clear();
|
||||
}
|
||||
|
||||
pub fn clear_global_params(&self) {
|
||||
self.global_params.lock().clear();
|
||||
}
|
||||
|
||||
pub fn evaluate(&self, script: &str, ctx: &StepContext) -> Result<Vec<String>, String> {
|
||||
let (outputs, var_writes) = self.evaluate_impl(script, ctx, None)?;
|
||||
self.apply_var_writes(var_writes);
|
||||
@@ -102,6 +108,8 @@ impl Forth {
|
||||
let vars_snapshot = self.vars.load_full();
|
||||
let mut var_writes: HashMap<String, Value> = HashMap::new();
|
||||
|
||||
cmd.set_global(self.global_params.lock().clone());
|
||||
|
||||
self.execute_ops(
|
||||
ops,
|
||||
ctx,
|
||||
@@ -113,6 +121,8 @@ impl Forth {
|
||||
&mut var_writes,
|
||||
)?;
|
||||
|
||||
*self.global_params.lock() = cmd.take_global();
|
||||
|
||||
Ok((outputs, var_writes))
|
||||
}
|
||||
|
||||
@@ -214,8 +224,9 @@ impl Forth {
|
||||
_ => 1,
|
||||
};
|
||||
let param_max = cmd
|
||||
.params()
|
||||
.global_params()
|
||||
.iter()
|
||||
.chain(cmd.params().iter())
|
||||
.map(|(_, v)| match v {
|
||||
Value::CycleList(items) => items.len(),
|
||||
_ => 1,
|
||||
@@ -227,7 +238,8 @@ impl Forth {
|
||||
|
||||
let has_arp_list = |cmd: &CmdRegister| -> bool {
|
||||
matches!(cmd.sound(), Some(Value::ArpList(_)))
|
||||
|| cmd.params().iter().any(|(_, v)| matches!(v, Value::ArpList(_)))
|
||||
|| cmd.global_params().iter().chain(cmd.params().iter())
|
||||
.any(|(_, v)| matches!(v, Value::ArpList(_)))
|
||||
};
|
||||
|
||||
let compute_arp_count = |cmd: &CmdRegister| -> usize {
|
||||
@@ -253,15 +265,21 @@ impl Forth {
|
||||
delta_secs: f64,
|
||||
outputs: &mut Vec<String>|
|
||||
-> Result<Option<Value>, String> {
|
||||
let (sound_opt, params) = cmd.snapshot().ok_or("nothing to emit")?;
|
||||
let has_sound = cmd.sound().is_some();
|
||||
let has_params = !cmd.params().is_empty();
|
||||
let has_global = !cmd.global_params().is_empty();
|
||||
if !has_sound && !has_params && !has_global {
|
||||
return Err("nothing to emit".into());
|
||||
}
|
||||
let resolved_sound_val =
|
||||
sound_opt.map(|sv| resolve_value(sv, arp_idx, poly_idx));
|
||||
cmd.sound().map(|sv| resolve_value(sv, arp_idx, poly_idx));
|
||||
let sound_str = match &resolved_sound_val {
|
||||
Some(v) => Some(v.as_str()?.to_string()),
|
||||
None => None,
|
||||
};
|
||||
let resolved_params: Vec<(&str, String)> = params
|
||||
let resolved_params: Vec<(&str, String)> = cmd.global_params()
|
||||
.iter()
|
||||
.chain(cmd.params().iter())
|
||||
.map(|(k, v)| {
|
||||
let resolved = resolve_value(v, arp_idx, poly_idx);
|
||||
if let Value::CycleList(_) | Value::ArpList(_) = v {
|
||||
@@ -1194,6 +1212,37 @@ impl Forth {
|
||||
cmd.clear();
|
||||
}
|
||||
|
||||
Op::EmitAll => {
|
||||
// Retroactive: patch existing sound outputs with current params
|
||||
if !cmd.params().is_empty() {
|
||||
let step_duration = ctx.step_duration();
|
||||
for output in outputs.iter_mut() {
|
||||
if output.starts_with("/sound/") {
|
||||
use std::fmt::Write;
|
||||
for (k, v) in cmd.params() {
|
||||
let val_str = v.to_param_string();
|
||||
if !output.ends_with('/') {
|
||||
output.push('/');
|
||||
}
|
||||
if is_tempo_scaled_param(k) {
|
||||
if let Ok(val) = val_str.parse::<f64>() {
|
||||
let _ = write!(output, "{k}/{}", val * step_duration);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let _ = write!(output, "{k}/{val_str}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Prospective: store for future emits
|
||||
cmd.commit_global();
|
||||
}
|
||||
|
||||
Op::ClearGlobal => {
|
||||
cmd.clear_global();
|
||||
}
|
||||
|
||||
Op::IntRange => {
|
||||
let end = pop_int(stack)?;
|
||||
let start = pop_int(stack)?;
|
||||
|
||||
@@ -94,6 +94,8 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
||||
"loop" => Op::Loop,
|
||||
"oct" => Op::Oct,
|
||||
"clear" => Op::ClearCmd,
|
||||
"all" => Op::EmitAll,
|
||||
"noall" => Op::ClearGlobal,
|
||||
".." => Op::IntRange,
|
||||
".," => Op::StepRange,
|
||||
"gen" => Op::Generate,
|
||||
|
||||
@@ -43,6 +43,26 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "all",
|
||||
aliases: &[],
|
||||
category: "Sound",
|
||||
stack: "(--)",
|
||||
desc: "Apply current params to all sounds",
|
||||
example: "500 lpf 0.5 verb all",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "noall",
|
||||
aliases: &[],
|
||||
category: "Sound",
|
||||
stack: "(--)",
|
||||
desc: "Clear global params",
|
||||
example: "noall",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
// Sample
|
||||
Word {
|
||||
name: "bank",
|
||||
|
||||
@@ -35,6 +35,41 @@ saw s
|
||||
|
||||
Parameters can appear in any order. They accumulate until you emit. You can clear the register using the `clear` word.
|
||||
|
||||
## Global Parameters
|
||||
|
||||
Use `all` to apply parameters globally. Global parameters persist across all patterns and steps until cleared with `noall`. They work both prospectively (before sounds) and retroactively (after sounds):
|
||||
|
||||
```forth
|
||||
;; Prospective: set params before emitting
|
||||
500 lpf 0.5 verb all
|
||||
kick s 60 note . ;; gets lpf=500 verb=0.5
|
||||
hat s 70 note . ;; gets lpf=500 verb=0.5
|
||||
```
|
||||
|
||||
```forth
|
||||
;; Retroactive: patch already-emitted sounds
|
||||
kick s 60 note .
|
||||
hat s 70 note .
|
||||
500 lpf 0.5 verb all ;; both outputs get lpf and verb
|
||||
```
|
||||
|
||||
Per-sound parameters override global ones:
|
||||
|
||||
```forth
|
||||
500 lpf all
|
||||
kick s 2000 lpf . ;; lpf=2000 (per-sound wins)
|
||||
hat s . ;; lpf=500 (global)
|
||||
```
|
||||
|
||||
Use `noall` to clear global parameters:
|
||||
|
||||
```forth
|
||||
500 lpf all
|
||||
kick s . ;; gets lpf
|
||||
noall
|
||||
hat s . ;; no lpf
|
||||
```
|
||||
|
||||
## Controlling Existing Voices
|
||||
|
||||
You can emit without a sound name. In this case, no new voice is created. Instead, the parameters are sent to control an existing voice. Use `voice` with an ID to target a specific voice:
|
||||
|
||||
@@ -686,6 +686,7 @@ impl SequencerState {
|
||||
self.variables.store(Arc::new(HashMap::new()));
|
||||
self.dict.lock().clear();
|
||||
self.speed_overrides.clear();
|
||||
self.script_engine.clear_global_params();
|
||||
}
|
||||
SeqCommand::Shutdown => {}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ impl ScriptEngine {
|
||||
self.forth.evaluate_with_trace(script, ctx, trace)
|
||||
}
|
||||
|
||||
pub fn clear_global_params(&self) {
|
||||
self.forth.clear_global_params();
|
||||
}
|
||||
|
||||
pub fn stack(&self) -> Vec<Value> {
|
||||
self.forth.stack()
|
||||
}
|
||||
|
||||
@@ -144,3 +144,96 @@ fn explicit_dur_zero_is_infinite() {
|
||||
let outputs = expect_outputs("880 freq 0 dur .", 1);
|
||||
assert!(outputs[0].contains("dur/0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_before_sounds() {
|
||||
let outputs = expect_outputs(
|
||||
r#"500 lpf 0.5 verb all "kick" s 60 note . "hat" s 70 note ."#,
|
||||
2,
|
||||
);
|
||||
assert!(outputs[0].contains("sound/kick"));
|
||||
assert!(outputs[0].contains("lpf/500"));
|
||||
assert!(outputs[0].contains("verb/0.5"));
|
||||
assert!(outputs[1].contains("sound/hat"));
|
||||
assert!(outputs[1].contains("lpf/500"));
|
||||
assert!(outputs[1].contains("verb/0.5"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_after_sounds() {
|
||||
let outputs = expect_outputs(
|
||||
r#""kick" s 60 note . "hat" s 70 note . 500 lpf 0.5 verb all"#,
|
||||
2,
|
||||
);
|
||||
assert!(outputs[0].contains("sound/kick"));
|
||||
assert!(outputs[0].contains("lpf/500"));
|
||||
assert!(outputs[0].contains("verb/0.5"));
|
||||
assert!(outputs[1].contains("sound/hat"));
|
||||
assert!(outputs[1].contains("lpf/500"));
|
||||
assert!(outputs[1].contains("verb/0.5"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noall_clears_global_params() {
|
||||
let outputs = expect_outputs(
|
||||
r#"500 lpf all "kick" s 60 note . noall "hat" s 70 note ."#,
|
||||
2,
|
||||
);
|
||||
assert!(outputs[0].contains("lpf/500"));
|
||||
assert!(!outputs[1].contains("lpf/500"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_with_tempo_scaled_params() {
|
||||
// attack is tempo-scaled: 0.01 * step_duration(0.125) = 0.00125
|
||||
let outputs = expect_outputs(
|
||||
r#"0.01 attack all "kick" s 60 note ."#,
|
||||
1,
|
||||
);
|
||||
assert!(outputs[0].contains("attack/0.00125"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_per_sound_override() {
|
||||
let outputs = expect_outputs(
|
||||
r#"500 lpf all "kick" s 2000 lpf . "hat" s ."#,
|
||||
2,
|
||||
);
|
||||
// kick has both global lpf=500 and per-sound lpf=2000; per-sound wins (comes last)
|
||||
assert!(outputs[0].contains("lpf/2000"));
|
||||
// hat only has global lpf=500
|
||||
assert!(outputs[1].contains("lpf/500"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_persists_across_evaluations() {
|
||||
let f = forth();
|
||||
let ctx = default_ctx();
|
||||
f.evaluate(r#"500 lpf 0.5 verb all"#, &ctx).unwrap();
|
||||
let outputs = f.evaluate(r#""kick" s 60 note ."#, &ctx).unwrap();
|
||||
assert_eq!(outputs.len(), 1);
|
||||
assert!(outputs[0].contains("lpf/500"), "global lpf missing: {}", outputs[0]);
|
||||
assert!(outputs[0].contains("verb/0.5"), "global verb missing: {}", outputs[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noall_clears_across_evaluations() {
|
||||
let f = forth();
|
||||
let ctx = default_ctx();
|
||||
f.evaluate(r#"500 lpf all"#, &ctx).unwrap();
|
||||
f.evaluate(r#"noall"#, &ctx).unwrap();
|
||||
let outputs = f.evaluate(r#""kick" s 60 note ."#, &ctx).unwrap();
|
||||
assert_eq!(outputs.len(), 1);
|
||||
assert!(!outputs[0].contains("lpf"), "lpf should be cleared: {}", outputs[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_replaces_previous_global() {
|
||||
let f = forth();
|
||||
let ctx = default_ctx();
|
||||
f.evaluate(r#"500 lpf 0.5 verb all"#, &ctx).unwrap();
|
||||
f.evaluate(r#"2000 lpf all"#, &ctx).unwrap();
|
||||
let outputs = f.evaluate(r#""kick" s ."#, &ctx).unwrap();
|
||||
assert_eq!(outputs.len(), 1);
|
||||
assert!(outputs[0].contains("lpf/2000"), "latest lpf should be 2000: {}", outputs[0]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user