Try to optimize
This commit is contained in:
@@ -83,4 +83,7 @@ pub enum Op {
|
|||||||
ClearCmd,
|
ClearCmd,
|
||||||
SetSpeed,
|
SetSpeed,
|
||||||
At,
|
At,
|
||||||
|
IntRange,
|
||||||
|
Generate,
|
||||||
|
GeomRange,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,10 +140,8 @@ impl CmdRegister {
|
|||||||
&self.deltas
|
&self.deltas
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn snapshot(&self) -> Option<(Value, Vec<(String, Value)>)> {
|
pub(super) fn snapshot(&self) -> Option<(&Value, &[(String, Value)])> {
|
||||||
self.sound
|
self.sound.as_ref().map(|s| (s, self.params.as_slice()))
|
||||||
.as_ref()
|
|
||||||
.map(|s| (s.clone(), self.params.clone()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn clear(&mut self) {
|
pub(super) fn clear(&mut self) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use rand::rngs::StdRng;
|
use rand::rngs::StdRng;
|
||||||
use rand::{Rng as RngTrait, SeedableRng};
|
use rand::{Rng as RngTrait, SeedableRng};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use super::compiler::compile_script;
|
use super::compiler::compile_script;
|
||||||
use super::ops::Op;
|
use super::ops::Op;
|
||||||
@@ -147,13 +148,11 @@ impl Forth {
|
|||||||
|
|
||||||
let emit_with_cycling = |cmd: &CmdRegister, emit_idx: usize, delta_secs: f64, outputs: &mut Vec<String>| -> Result<Option<Value>, String> {
|
let emit_with_cycling = |cmd: &CmdRegister, emit_idx: usize, delta_secs: f64, outputs: &mut Vec<String>| -> Result<Option<Value>, String> {
|
||||||
let (sound_val, params) = cmd.snapshot().ok_or("no sound set")?;
|
let (sound_val, params) = cmd.snapshot().ok_or("no sound set")?;
|
||||||
let resolved_sound_val = resolve_cycling(&sound_val, emit_idx);
|
let resolved_sound_val = resolve_cycling(sound_val, emit_idx);
|
||||||
// Note: sound span is recorded by Op::Emit, not here
|
|
||||||
let sound = resolved_sound_val.as_str()?.to_string();
|
let sound = resolved_sound_val.as_str()?.to_string();
|
||||||
let resolved_params: Vec<(String, String)> =
|
let resolved_params: Vec<(String, String)> =
|
||||||
params.iter().map(|(k, v)| {
|
params.iter().map(|(k, v)| {
|
||||||
let resolved = resolve_cycling(v, emit_idx);
|
let resolved = resolve_cycling(v, emit_idx);
|
||||||
// Record selected span for params if they came from a CycleList
|
|
||||||
if let Value::CycleList(_) = v {
|
if let Value::CycleList(_) = v {
|
||||||
if let Some(span) = resolved.span() {
|
if let Some(span) = resolved.span() {
|
||||||
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
@@ -164,7 +163,7 @@ impl Forth {
|
|||||||
(k.clone(), resolved.to_param_string())
|
(k.clone(), resolved.to_param_string())
|
||||||
}).collect();
|
}).collect();
|
||||||
emit_output(&sound, &resolved_params, ctx.step_duration(), delta_secs, outputs);
|
emit_output(&sound, &resolved_params, ctx.step_duration(), delta_secs, outputs);
|
||||||
Ok(Some(resolved_sound_val))
|
Ok(Some(resolved_sound_val.into_owned()))
|
||||||
};
|
};
|
||||||
|
|
||||||
while pc < ops.len() {
|
while pc < ops.len() {
|
||||||
@@ -630,8 +629,15 @@ impl Forth {
|
|||||||
let top = stack.pop().ok_or("stack underflow")?;
|
let top = stack.pop().ok_or("stack underflow")?;
|
||||||
let deltas = match &top {
|
let deltas = match &top {
|
||||||
Value::Float(..) => vec![top],
|
Value::Float(..) => vec![top],
|
||||||
Value::Int(n, _) if *n > 0 && stack.len() >= *n as usize => {
|
Value::Int(n, _) => {
|
||||||
let count = *n as usize;
|
let count = *n as usize;
|
||||||
|
if stack.len() < count {
|
||||||
|
return Err(format!(
|
||||||
|
"at: stack underflow, expected {} values but got {}",
|
||||||
|
count,
|
||||||
|
stack.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
let mut vals = Vec::with_capacity(count);
|
let mut vals = Vec::with_capacity(count);
|
||||||
for _ in 0..count {
|
for _ in 0..count {
|
||||||
vals.push(stack.pop().ok_or("stack underflow")?);
|
vals.push(stack.pop().ok_or("stack underflow")?);
|
||||||
@@ -639,8 +645,7 @@ impl Forth {
|
|||||||
vals.reverse();
|
vals.reverse();
|
||||||
vals
|
vals
|
||||||
}
|
}
|
||||||
Value::Int(..) => vec![top],
|
_ => return Err("at expects float or int count".into()),
|
||||||
_ => return Err("at expects number or list".into()),
|
|
||||||
};
|
};
|
||||||
cmd.set_deltas(deltas);
|
cmd.set_deltas(deltas);
|
||||||
}
|
}
|
||||||
@@ -709,6 +714,50 @@ impl Forth {
|
|||||||
emit_with_cycling(cmd, i, ctx.nudge_secs, outputs)?;
|
emit_with_cycling(cmd, i, ctx.nudge_secs, outputs)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Op::IntRange => {
|
||||||
|
let end = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||||
|
let start = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||||
|
if start <= end {
|
||||||
|
for i in start..=end {
|
||||||
|
stack.push(Value::Int(i, None));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for i in (end..=start).rev() {
|
||||||
|
stack.push(Value::Int(i, None));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Generate => {
|
||||||
|
let count = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||||
|
let quot = stack.pop().ok_or("stack underflow")?;
|
||||||
|
if count < 0 {
|
||||||
|
return Err("gen count must be >= 0".into());
|
||||||
|
}
|
||||||
|
let mut results = Vec::with_capacity(count as usize);
|
||||||
|
for _ in 0..count {
|
||||||
|
run_quotation(quot.clone(), stack, outputs, cmd)?;
|
||||||
|
results.push(stack.pop().ok_or("gen: quotation must produce a value")?);
|
||||||
|
}
|
||||||
|
for val in results {
|
||||||
|
stack.push(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::GeomRange => {
|
||||||
|
let count = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||||
|
let ratio = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
let start = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
if count < 0 {
|
||||||
|
return Err("geom.. count must be >= 0".into());
|
||||||
|
}
|
||||||
|
let mut val = start;
|
||||||
|
for _ in 0..count {
|
||||||
|
stack.push(float_to_value(val));
|
||||||
|
val *= ratio;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pc += 1;
|
pc += 1;
|
||||||
}
|
}
|
||||||
@@ -717,31 +766,34 @@ impl Forth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const TEMPO_SCALED_PARAMS: &[&str] = &[
|
fn is_tempo_scaled_param(name: &str) -> bool {
|
||||||
"attack",
|
matches!(
|
||||||
"decay",
|
name,
|
||||||
"release",
|
"attack"
|
||||||
"lpa",
|
| "decay"
|
||||||
"lpd",
|
| "release"
|
||||||
"lpr",
|
| "lpa"
|
||||||
"hpa",
|
| "lpd"
|
||||||
"hpd",
|
| "lpr"
|
||||||
"hpr",
|
| "hpa"
|
||||||
"bpa",
|
| "hpd"
|
||||||
"bpd",
|
| "hpr"
|
||||||
"bpr",
|
| "bpa"
|
||||||
"patt",
|
| "bpd"
|
||||||
"pdec",
|
| "bpr"
|
||||||
"prel",
|
| "patt"
|
||||||
"fma",
|
| "pdec"
|
||||||
"fmd",
|
| "prel"
|
||||||
"fmr",
|
| "fma"
|
||||||
"glide",
|
| "fmd"
|
||||||
"verbdecay",
|
| "fmr"
|
||||||
"verbpredelay",
|
| "glide"
|
||||||
"chorusdelay",
|
| "verbdecay"
|
||||||
"duration",
|
| "verbpredelay"
|
||||||
];
|
| "chorusdelay"
|
||||||
|
| "duration"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn emit_output(
|
fn emit_output(
|
||||||
sound: &str,
|
sound: &str,
|
||||||
@@ -765,7 +817,7 @@ fn emit_output(
|
|||||||
pairs.push(("delaytime".into(), step_duration.to_string()));
|
pairs.push(("delaytime".into(), step_duration.to_string()));
|
||||||
}
|
}
|
||||||
for pair in &mut pairs {
|
for pair in &mut pairs {
|
||||||
if TEMPO_SCALED_PARAMS.contains(&pair.0.as_str()) {
|
if is_tempo_scaled_param(&pair.0) {
|
||||||
if let Ok(val) = pair.1.parse::<f64>() {
|
if let Ok(val) = pair.1.parse::<f64>() {
|
||||||
pair.1 = (val * step_duration).to_string();
|
pair.1 = (val * step_duration).to_string();
|
||||||
}
|
}
|
||||||
@@ -853,11 +905,11 @@ fn format_cmd(pairs: &[(String, String)]) -> String {
|
|||||||
format!("/{}", parts.join("/"))
|
format!("/{}", parts.join("/"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_cycling(val: &Value, emit_idx: usize) -> Value {
|
fn resolve_cycling(val: &Value, emit_idx: usize) -> Cow<'_, Value> {
|
||||||
match val {
|
match val {
|
||||||
Value::CycleList(items) if !items.is_empty() => {
|
Value::CycleList(items) if !items.is_empty() => {
|
||||||
items[emit_idx % items.len()].clone()
|
Cow::Owned(items[emit_idx % items.len()].clone())
|
||||||
}
|
}
|
||||||
other => other.clone(),
|
other => Cow::Borrowed(other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,11 @@ use ratatui::buffer::Buffer;
|
|||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::Color;
|
use ratatui::style::Color;
|
||||||
use ratatui::widgets::Widget;
|
use ratatui::widgets::Widget;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub enum Orientation {
|
pub enum Orientation {
|
||||||
@@ -58,7 +63,11 @@ fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, g
|
|||||||
let fine_width = width * 2;
|
let fine_width = width * 2;
|
||||||
let fine_height = height * 4;
|
let fine_height = height * 4;
|
||||||
|
|
||||||
let mut patterns = vec![0u8; width * height];
|
PATTERNS.with(|p| {
|
||||||
|
let mut patterns = p.borrow_mut();
|
||||||
|
let size = width * height;
|
||||||
|
patterns.clear();
|
||||||
|
patterns.resize(size, 0);
|
||||||
|
|
||||||
for fine_x in 0..fine_width {
|
for fine_x in 0..fine_width {
|
||||||
let sample_idx = (fine_x * data.len()) / fine_width;
|
let sample_idx = (fine_x * data.len()) / fine_width;
|
||||||
@@ -98,6 +107,7 @@ fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, g
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gain: f32) {
|
fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gain: f32) {
|
||||||
@@ -106,7 +116,11 @@ fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gai
|
|||||||
let fine_width = width * 2;
|
let fine_width = width * 2;
|
||||||
let fine_height = height * 4;
|
let fine_height = height * 4;
|
||||||
|
|
||||||
let mut patterns = vec![0u8; width * height];
|
PATTERNS.with(|p| {
|
||||||
|
let mut patterns = p.borrow_mut();
|
||||||
|
let size = width * height;
|
||||||
|
patterns.clear();
|
||||||
|
patterns.resize(size, 0);
|
||||||
|
|
||||||
for fine_y in 0..fine_height {
|
for fine_y in 0..fine_height {
|
||||||
let sample_idx = (fine_y * data.len()) / fine_height;
|
let sample_idx = (fine_y * data.len()) / fine_height;
|
||||||
@@ -146,4 +160,5 @@ fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gai
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ struct SpectrumAnalyzer {
|
|||||||
fft: Arc<dyn rustfft::Fft<f32>>,
|
fft: Arc<dyn rustfft::Fft<f32>>,
|
||||||
window: [f32; FFT_SIZE],
|
window: [f32; FFT_SIZE],
|
||||||
scratch: Vec<Complex<f32>>,
|
scratch: Vec<Complex<f32>>,
|
||||||
|
fft_buf: Vec<Complex<f32>>,
|
||||||
band_edges: [usize; NUM_BANDS + 1],
|
band_edges: [usize; NUM_BANDS + 1],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +115,7 @@ impl SpectrumAnalyzer {
|
|||||||
fft,
|
fft,
|
||||||
window,
|
window,
|
||||||
scratch: vec![Complex::default(); scratch_len],
|
scratch: vec![Complex::default(); scratch_len],
|
||||||
|
fft_buf: vec![Complex::default(); FFT_SIZE],
|
||||||
band_edges,
|
band_edges,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,20 +132,19 @@ impl SpectrumAnalyzer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run_fft(&mut self, output: &SpectrumBuffer) {
|
fn run_fft(&mut self, output: &SpectrumBuffer) {
|
||||||
let mut buf: Vec<Complex<f32>> = (0..FFT_SIZE)
|
for i in 0..FFT_SIZE {
|
||||||
.map(|i| {
|
|
||||||
let idx = (self.pos + i) % FFT_SIZE;
|
let idx = (self.pos + i) % FFT_SIZE;
|
||||||
Complex::new(self.ring[idx] * self.window[i], 0.0)
|
self.fft_buf[i] = Complex::new(self.ring[idx] * self.window[i], 0.0);
|
||||||
})
|
}
|
||||||
.collect();
|
|
||||||
|
|
||||||
self.fft.process_with_scratch(&mut buf, &mut self.scratch);
|
self.fft
|
||||||
|
.process_with_scratch(&mut self.fft_buf, &mut self.scratch);
|
||||||
|
|
||||||
let mut bands = [0.0f32; NUM_BANDS];
|
let mut bands = [0.0f32; NUM_BANDS];
|
||||||
for (band, mag) in bands.iter_mut().enumerate() {
|
for (band, mag) in bands.iter_mut().enumerate() {
|
||||||
let lo = self.band_edges[band];
|
let lo = self.band_edges[band];
|
||||||
let hi = self.band_edges[band + 1].max(lo + 1);
|
let hi = self.band_edges[band + 1].max(lo + 1);
|
||||||
let sum: f32 = buf[lo..hi].iter().map(|c| c.norm()).sum();
|
let sum: f32 = self.fft_buf[lo..hi].iter().map(|c| c.norm()).sum();
|
||||||
let avg = sum / (hi - lo) as f32;
|
let avg = sum / (hi - lo) as f32;
|
||||||
let amplitude = avg / (FFT_SIZE as f32 / 2.0);
|
let amplitude = avg / (FFT_SIZE as f32 / 2.0);
|
||||||
let db = 20.0 * amplitude.max(1e-10).log10();
|
let db = 20.0 * amplitude.max(1e-10).log10();
|
||||||
|
|||||||
@@ -93,17 +93,19 @@ pub struct ActivePatternState {
|
|||||||
pub iter: usize,
|
pub iter: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type StepTracesMap = HashMap<(usize, usize, usize), ExecutionTrace>;
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct SharedSequencerState {
|
pub struct SharedSequencerState {
|
||||||
pub active_patterns: Vec<ActivePatternState>,
|
pub active_patterns: Vec<ActivePatternState>,
|
||||||
pub step_traces: HashMap<(usize, usize, usize), ExecutionTrace>,
|
pub step_traces: Arc<StepTracesMap>,
|
||||||
pub event_count: usize,
|
pub event_count: usize,
|
||||||
pub dropped_events: usize,
|
pub dropped_events: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SequencerSnapshot {
|
pub struct SequencerSnapshot {
|
||||||
pub active_patterns: Vec<ActivePatternState>,
|
pub active_patterns: Vec<ActivePatternState>,
|
||||||
pub step_traces: HashMap<(usize, usize, usize), ExecutionTrace>,
|
step_traces: Arc<StepTracesMap>,
|
||||||
pub event_count: usize,
|
pub event_count: usize,
|
||||||
pub dropped_events: usize,
|
pub dropped_events: usize,
|
||||||
}
|
}
|
||||||
@@ -146,7 +148,7 @@ impl SequencerHandle {
|
|||||||
let state = self.shared_state.load();
|
let state = self.shared_state.load();
|
||||||
SequencerSnapshot {
|
SequencerSnapshot {
|
||||||
active_patterns: state.active_patterns.clone(),
|
active_patterns: state.active_patterns.clone(),
|
||||||
step_traces: state.step_traces.clone(),
|
step_traces: Arc::clone(&state.step_traces),
|
||||||
event_count: state.event_count,
|
event_count: state.event_count,
|
||||||
dropped_events: state.dropped_events,
|
dropped_events: state.dropped_events,
|
||||||
}
|
}
|
||||||
@@ -366,7 +368,6 @@ pub(crate) struct TickOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct StepResult {
|
struct StepResult {
|
||||||
audio_commands: Vec<String>,
|
|
||||||
completed_iterations: Vec<PatternId>,
|
completed_iterations: Vec<PatternId>,
|
||||||
any_step_fired: bool,
|
any_step_fired: bool,
|
||||||
}
|
}
|
||||||
@@ -384,15 +385,44 @@ fn parse_chain_target(s: &str) -> Option<PatternId> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct KeyCache {
|
||||||
|
speed_keys: [[String; MAX_PATTERNS]; MAX_BANKS],
|
||||||
|
chain_keys: [[String; MAX_PATTERNS]; MAX_BANKS],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyCache {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
speed_keys: std::array::from_fn(|bank| {
|
||||||
|
std::array::from_fn(|pattern| format!("__speed_{bank}_{pattern}__"))
|
||||||
|
}),
|
||||||
|
chain_keys: std::array::from_fn(|bank| {
|
||||||
|
std::array::from_fn(|pattern| format!("__chain_{bank}_{pattern}__"))
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn speed_key(&self, bank: usize, pattern: usize) -> &str {
|
||||||
|
&self.speed_keys[bank][pattern]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chain_key(&self, bank: usize, pattern: usize) -> &str {
|
||||||
|
&self.chain_keys[bank][pattern]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct SequencerState {
|
pub(crate) struct SequencerState {
|
||||||
audio_state: AudioState,
|
audio_state: AudioState,
|
||||||
pattern_cache: PatternCache,
|
pattern_cache: PatternCache,
|
||||||
runs_counter: RunsCounter,
|
runs_counter: RunsCounter,
|
||||||
step_traces: HashMap<(usize, usize, usize), ExecutionTrace>,
|
step_traces: Arc<StepTracesMap>,
|
||||||
event_count: usize,
|
event_count: usize,
|
||||||
dropped_events: usize,
|
dropped_events: usize,
|
||||||
script_engine: ScriptEngine,
|
script_engine: ScriptEngine,
|
||||||
variables: Variables,
|
variables: Variables,
|
||||||
|
speed_overrides: HashMap<(usize, usize), f64>,
|
||||||
|
key_cache: KeyCache,
|
||||||
|
buf_audio_commands: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SequencerState {
|
impl SequencerState {
|
||||||
@@ -406,11 +436,14 @@ impl SequencerState {
|
|||||||
audio_state: AudioState::new(),
|
audio_state: AudioState::new(),
|
||||||
pattern_cache: PatternCache::new(),
|
pattern_cache: PatternCache::new(),
|
||||||
runs_counter: RunsCounter::new(),
|
runs_counter: RunsCounter::new(),
|
||||||
step_traces: HashMap::new(),
|
step_traces: Arc::new(HashMap::new()),
|
||||||
event_count: 0,
|
event_count: 0,
|
||||||
dropped_events: 0,
|
dropped_events: 0,
|
||||||
script_engine,
|
script_engine,
|
||||||
variables,
|
variables,
|
||||||
|
speed_overrides: HashMap::new(),
|
||||||
|
key_cache: KeyCache::new(),
|
||||||
|
buf_audio_commands: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,7 +492,7 @@ impl SequencerState {
|
|||||||
self.audio_state.active_patterns.clear();
|
self.audio_state.active_patterns.clear();
|
||||||
self.audio_state.pending_starts.clear();
|
self.audio_state.pending_starts.clear();
|
||||||
self.audio_state.pending_stops.clear();
|
self.audio_state.pending_stops.clear();
|
||||||
self.step_traces.clear();
|
Arc::make_mut(&mut self.step_traces).clear();
|
||||||
self.runs_counter.counts.clear();
|
self.runs_counter.counts.clear();
|
||||||
}
|
}
|
||||||
SeqCommand::Shutdown => {}
|
SeqCommand::Shutdown => {}
|
||||||
@@ -491,7 +524,7 @@ impl SequencerState {
|
|||||||
self.audio_state.prev_beat = beat;
|
self.audio_state.prev_beat = beat;
|
||||||
|
|
||||||
TickOutput {
|
TickOutput {
|
||||||
audio_commands: steps.audio_commands,
|
audio_commands: std::mem::take(&mut self.buf_audio_commands),
|
||||||
new_tempo: vars.new_tempo,
|
new_tempo: vars.new_tempo,
|
||||||
shared_state: self.build_shared_state(),
|
shared_state: self.build_shared_state(),
|
||||||
}
|
}
|
||||||
@@ -500,13 +533,14 @@ impl SequencerState {
|
|||||||
fn tick_paused(&mut self) -> TickOutput {
|
fn tick_paused(&mut self) -> TickOutput {
|
||||||
for pending in self.audio_state.pending_stops.drain(..) {
|
for pending in self.audio_state.pending_stops.drain(..) {
|
||||||
self.audio_state.active_patterns.remove(&pending.id);
|
self.audio_state.active_patterns.remove(&pending.id);
|
||||||
self.step_traces.retain(|&(bank, pattern, _), _| {
|
Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| {
|
||||||
bank != pending.id.bank || pattern != pending.id.pattern
|
bank != pending.id.bank || pattern != pending.id.pattern
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
self.audio_state.pending_starts.clear();
|
self.audio_state.pending_starts.clear();
|
||||||
|
self.buf_audio_commands.clear();
|
||||||
TickOutput {
|
TickOutput {
|
||||||
audio_commands: Vec::new(),
|
audio_commands: std::mem::take(&mut self.buf_audio_commands),
|
||||||
new_tempo: None,
|
new_tempo: None,
|
||||||
shared_state: self.build_shared_state(),
|
shared_state: self.build_shared_state(),
|
||||||
}
|
}
|
||||||
@@ -547,7 +581,7 @@ impl SequencerState {
|
|||||||
for pending in &self.audio_state.pending_stops {
|
for pending in &self.audio_state.pending_stops {
|
||||||
if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) {
|
if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) {
|
||||||
self.audio_state.active_patterns.remove(&pending.id);
|
self.audio_state.active_patterns.remove(&pending.id);
|
||||||
self.step_traces.retain(|&(bank, pattern, _), _| {
|
Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| {
|
||||||
bank != pending.id.bank || pattern != pending.id.pattern
|
bank != pending.id.bank || pattern != pending.id.pattern
|
||||||
});
|
});
|
||||||
stopped.push(pending.id);
|
stopped.push(pending.id);
|
||||||
@@ -565,32 +599,29 @@ impl SequencerState {
|
|||||||
fill: bool,
|
fill: bool,
|
||||||
nudge_secs: f64,
|
nudge_secs: f64,
|
||||||
) -> StepResult {
|
) -> StepResult {
|
||||||
|
self.buf_audio_commands.clear();
|
||||||
let mut result = StepResult {
|
let mut result = StepResult {
|
||||||
audio_commands: Vec::new(),
|
|
||||||
completed_iterations: Vec::new(),
|
completed_iterations: Vec::new(),
|
||||||
any_step_fired: false,
|
any_step_fired: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let speed_overrides: HashMap<(usize, usize), f64> = {
|
self.speed_overrides.clear();
|
||||||
|
{
|
||||||
let vars = self.variables.lock().unwrap();
|
let vars = self.variables.lock().unwrap();
|
||||||
self.audio_state
|
for id in self.audio_state.active_patterns.keys() {
|
||||||
.active_patterns
|
let key = self.key_cache.speed_key(id.bank, id.pattern);
|
||||||
.keys()
|
if let Some(v) = vars.get(key).and_then(|v| v.as_float().ok()) {
|
||||||
.filter_map(|id| {
|
self.speed_overrides.insert((id.bank, id.pattern), v);
|
||||||
let key = format!("__speed_{}_{}__", id.bank, id.pattern);
|
}
|
||||||
vars.get(&key)
|
}
|
||||||
.and_then(|v| v.as_float().ok())
|
}
|
||||||
.map(|v| ((id.bank, id.pattern), v))
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
for (_id, active) in self.audio_state.active_patterns.iter_mut() {
|
for (_id, active) in self.audio_state.active_patterns.iter_mut() {
|
||||||
let Some(pattern) = self.pattern_cache.get(active.bank, active.pattern) else {
|
let Some(pattern) = self.pattern_cache.get(active.bank, active.pattern) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let speed_mult = speed_overrides
|
let speed_mult = self.speed_overrides
|
||||||
.get(&(active.bank, active.pattern))
|
.get(&(active.bank, active.pattern))
|
||||||
.copied()
|
.copied()
|
||||||
.unwrap_or_else(|| pattern.speed.multiplier());
|
.unwrap_or_else(|| pattern.speed.multiplier());
|
||||||
@@ -634,13 +665,13 @@ impl SequencerState {
|
|||||||
.script_engine
|
.script_engine
|
||||||
.evaluate_with_trace(script, &ctx, &mut trace)
|
.evaluate_with_trace(script, &ctx, &mut trace)
|
||||||
{
|
{
|
||||||
self.step_traces.insert(
|
Arc::make_mut(&mut self.step_traces).insert(
|
||||||
(active.bank, active.pattern, source_idx),
|
(active.bank, active.pattern, source_idx),
|
||||||
std::mem::take(&mut trace),
|
std::mem::take(&mut trace),
|
||||||
);
|
);
|
||||||
for cmd in cmds {
|
for cmd in cmds {
|
||||||
self.event_count += 1;
|
self.event_count += 1;
|
||||||
result.audio_commands.push(cmd);
|
self.buf_audio_commands.push(cmd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -681,18 +712,18 @@ impl SequencerState {
|
|||||||
|
|
||||||
let mut chain_transitions = Vec::new();
|
let mut chain_transitions = Vec::new();
|
||||||
for id in completed {
|
for id in completed {
|
||||||
let chain_key = format!("__chain_{}_{}__", id.bank, id.pattern);
|
let chain_key = self.key_cache.chain_key(id.bank, id.pattern);
|
||||||
if let Some(Value::Str(s, _)) = vars.get(&chain_key) {
|
if let Some(Value::Str(s, _)) = vars.get(chain_key) {
|
||||||
if let Some(target) = parse_chain_target(s) {
|
if let Some(target) = parse_chain_target(s) {
|
||||||
chain_transitions.push((*id, target));
|
chain_transitions.push((*id, target));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
vars.remove(&chain_key);
|
vars.remove(chain_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
for id in stopped {
|
for id in stopped {
|
||||||
let chain_key = format!("__chain_{}_{}__", id.bank, id.pattern);
|
let chain_key = self.key_cache.chain_key(id.bank, id.pattern);
|
||||||
vars.remove(&chain_key);
|
vars.remove(chain_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
VariableReads {
|
VariableReads {
|
||||||
@@ -738,7 +769,7 @@ impl SequencerState {
|
|||||||
iter: a.iter,
|
iter: a.iter,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
step_traces: self.step_traces.clone(),
|
step_traces: Arc::clone(&self.step_traces),
|
||||||
event_count: self.event_count,
|
event_count: self.event_count,
|
||||||
dropped_events: self.dropped_events,
|
dropped_events: self.dropped_events,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -476,7 +476,7 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
KeyCode::Char('p') if ctrl => {
|
KeyCode::Char('p') if ctrl => {
|
||||||
editor.search_prev();
|
editor.search_prev();
|
||||||
}
|
}
|
||||||
KeyCode::Char('k') if ctrl => {
|
KeyCode::Char('s') if ctrl => {
|
||||||
ctx.app.editor_ctx.show_stack = !ctx.app.editor_ctx.show_stack;
|
ctx.app.editor_ctx.show_stack = !ctx.app.editor_ctx.show_stack;
|
||||||
}
|
}
|
||||||
KeyCode::Char('a') if ctrl => {
|
KeyCode::Char('a') if ctrl => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
use std::ops::RangeInclusive;
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
use cagire_ratatui::Editor;
|
use cagire_ratatui::Editor;
|
||||||
@@ -55,6 +56,14 @@ pub struct EditorContext {
|
|||||||
pub selection_anchor: Option<usize>,
|
pub selection_anchor: Option<usize>,
|
||||||
pub copied_steps: Option<CopiedSteps>,
|
pub copied_steps: Option<CopiedSteps>,
|
||||||
pub show_stack: bool,
|
pub show_stack: bool,
|
||||||
|
pub stack_cache: RefCell<Option<StackCache>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct StackCache {
|
||||||
|
pub cursor_line: usize,
|
||||||
|
pub lines_hash: u64,
|
||||||
|
pub result: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -96,6 +105,7 @@ impl Default for EditorContext {
|
|||||||
selection_anchor: None,
|
selection_anchor: None,
|
||||||
copied_steps: None,
|
copied_steps: None,
|
||||||
show_stack: false,
|
show_stack: false,
|
||||||
|
stack_cache: RefCell::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ pub mod ui;
|
|||||||
|
|
||||||
pub use audio::{AudioSettings, DeviceKind, EngineSection, Metrics, SettingKind};
|
pub use audio::{AudioSettings, DeviceKind, EngineSection, Metrics, SettingKind};
|
||||||
pub use options::{OptionsFocus, OptionsState};
|
pub use options::{OptionsFocus, OptionsState};
|
||||||
pub use editor::{CopiedStepData, CopiedSteps, EditorContext, Focus, PatternField, PatternPropsField};
|
pub use editor::{CopiedStepData, CopiedSteps, EditorContext, Focus, PatternField, PatternPropsField, StackCache};
|
||||||
pub use live_keys::LiveKeyState;
|
pub use live_keys::LiveKeyState;
|
||||||
pub use modal::Modal;
|
pub use modal::Modal;
|
||||||
pub use panel::{PanelFocus, PanelState, SidePanel};
|
pub use panel::{PanelFocus, PanelState, SidePanel};
|
||||||
|
|||||||
@@ -16,39 +16,90 @@ pub enum TokenKind {
|
|||||||
Note,
|
Note,
|
||||||
Interval,
|
Interval,
|
||||||
Variable,
|
Variable,
|
||||||
|
Emit,
|
||||||
|
Vary,
|
||||||
|
Generator,
|
||||||
Default,
|
Default,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TokenKind {
|
impl TokenKind {
|
||||||
pub fn style(self) -> Style {
|
pub fn style(self) -> Style {
|
||||||
match self {
|
match self {
|
||||||
TokenKind::Number => Style::default().fg(Color::Rgb(255, 180, 100)),
|
TokenKind::Emit => Style::default()
|
||||||
TokenKind::String => Style::default().fg(Color::Rgb(150, 220, 150)),
|
.fg(Color::Rgb(255, 255, 255))
|
||||||
TokenKind::Comment => Style::default().fg(Color::Rgb(100, 100, 100)),
|
.bg(Color::Rgb(140, 50, 50))
|
||||||
TokenKind::Keyword => Style::default().fg(Color::Rgb(220, 120, 220)),
|
.add_modifier(Modifier::BOLD),
|
||||||
TokenKind::StackOp => Style::default().fg(Color::Rgb(120, 180, 220)),
|
TokenKind::Number => Style::default()
|
||||||
TokenKind::Operator => Style::default().fg(Color::Rgb(200, 200, 130)),
|
.fg(Color::Rgb(255, 200, 120))
|
||||||
TokenKind::Sound => Style::default().fg(Color::Rgb(100, 220, 200)),
|
.bg(Color::Rgb(60, 40, 15)),
|
||||||
TokenKind::Param => Style::default().fg(Color::Rgb(180, 150, 220)),
|
TokenKind::String => Style::default()
|
||||||
TokenKind::Context => Style::default().fg(Color::Rgb(220, 180, 120)),
|
.fg(Color::Rgb(150, 230, 150))
|
||||||
TokenKind::Note => Style::default().fg(Color::Rgb(120, 200, 160)),
|
.bg(Color::Rgb(20, 55, 20)),
|
||||||
TokenKind::Interval => Style::default().fg(Color::Rgb(160, 200, 120)),
|
TokenKind::Comment => Style::default()
|
||||||
TokenKind::Variable => Style::default().fg(Color::Rgb(200, 140, 180)),
|
.fg(Color::Rgb(100, 100, 100))
|
||||||
TokenKind::Default => Style::default().fg(Color::Rgb(200, 200, 200)),
|
.bg(Color::Rgb(18, 18, 18)),
|
||||||
|
TokenKind::Keyword => Style::default()
|
||||||
|
.fg(Color::Rgb(230, 130, 230))
|
||||||
|
.bg(Color::Rgb(55, 25, 55)),
|
||||||
|
TokenKind::StackOp => Style::default()
|
||||||
|
.fg(Color::Rgb(130, 190, 240))
|
||||||
|
.bg(Color::Rgb(20, 40, 70)),
|
||||||
|
TokenKind::Operator => Style::default()
|
||||||
|
.fg(Color::Rgb(220, 220, 140))
|
||||||
|
.bg(Color::Rgb(45, 45, 20)),
|
||||||
|
TokenKind::Sound => Style::default()
|
||||||
|
.fg(Color::Rgb(100, 240, 220))
|
||||||
|
.bg(Color::Rgb(15, 60, 55)),
|
||||||
|
TokenKind::Param => Style::default()
|
||||||
|
.fg(Color::Rgb(190, 160, 240))
|
||||||
|
.bg(Color::Rgb(45, 30, 70)),
|
||||||
|
TokenKind::Context => Style::default()
|
||||||
|
.fg(Color::Rgb(240, 190, 120))
|
||||||
|
.bg(Color::Rgb(60, 45, 20)),
|
||||||
|
TokenKind::Note => Style::default()
|
||||||
|
.fg(Color::Rgb(120, 220, 170))
|
||||||
|
.bg(Color::Rgb(20, 55, 40)),
|
||||||
|
TokenKind::Interval => Style::default()
|
||||||
|
.fg(Color::Rgb(170, 220, 120))
|
||||||
|
.bg(Color::Rgb(35, 55, 20)),
|
||||||
|
TokenKind::Variable => Style::default()
|
||||||
|
.fg(Color::Rgb(220, 150, 190))
|
||||||
|
.bg(Color::Rgb(60, 30, 50)),
|
||||||
|
TokenKind::Vary => Style::default()
|
||||||
|
.fg(Color::Rgb(230, 230, 100))
|
||||||
|
.bg(Color::Rgb(55, 55, 15)),
|
||||||
|
TokenKind::Generator => Style::default()
|
||||||
|
.fg(Color::Rgb(100, 220, 180))
|
||||||
|
.bg(Color::Rgb(15, 55, 45)),
|
||||||
|
TokenKind::Default => Style::default()
|
||||||
|
.fg(Color::Rgb(160, 160, 160))
|
||||||
|
.bg(Color::Rgb(25, 25, 25)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn gap_style() -> Style {
|
||||||
|
Style::default().bg(Color::Rgb(25, 25, 25))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Token {
|
pub struct Token {
|
||||||
pub start: usize,
|
pub start: usize,
|
||||||
pub end: usize,
|
pub end: usize,
|
||||||
pub kind: TokenKind,
|
pub kind: TokenKind,
|
||||||
|
pub varargs: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lookup_word_kind(word: &str) -> Option<(TokenKind, bool)> {
|
||||||
|
if word == "." {
|
||||||
|
return Some((TokenKind::Emit, false));
|
||||||
|
}
|
||||||
|
if word == ".!" {
|
||||||
|
return Some((TokenKind::Emit, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lookup_word_kind(word: &str) -> Option<TokenKind> {
|
|
||||||
for w in WORDS {
|
for w in WORDS {
|
||||||
if w.name == word || w.aliases.contains(&word) {
|
if w.name == word || w.aliases.contains(&word) {
|
||||||
return Some(match &w.compile {
|
let kind = match &w.compile {
|
||||||
WordCompile::Param => TokenKind::Param,
|
WordCompile::Param => TokenKind::Param,
|
||||||
WordCompile::Context(_) => TokenKind::Context,
|
WordCompile::Context(_) => TokenKind::Context,
|
||||||
_ => match w.category {
|
_ => match w.category {
|
||||||
@@ -58,9 +109,12 @@ fn lookup_word_kind(word: &str) -> Option<TokenKind> {
|
|||||||
TokenKind::Operator
|
TokenKind::Operator
|
||||||
}
|
}
|
||||||
"Sound" => TokenKind::Sound,
|
"Sound" => TokenKind::Sound,
|
||||||
|
"Randomness" | "Probability" | "Selection" => TokenKind::Vary,
|
||||||
|
"Generator" => TokenKind::Generator,
|
||||||
_ => TokenKind::Keyword,
|
_ => TokenKind::Keyword,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
return Some((kind, w.varargs));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@@ -98,11 +152,11 @@ pub fn tokenize_line(line: &str) -> Vec<Token> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c == ';' && chars.peek().map(|(_, ch)| *ch) == Some(';') {
|
if c == ';' && chars.peek().map(|(_, ch)| *ch) == Some(';') {
|
||||||
// ;; starts a comment to end of line
|
|
||||||
tokens.push(Token {
|
tokens.push(Token {
|
||||||
start,
|
start,
|
||||||
end: line.len(),
|
end: line.len(),
|
||||||
kind: TokenKind::Comment,
|
kind: TokenKind::Comment,
|
||||||
|
varargs: false,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -119,6 +173,7 @@ pub fn tokenize_line(line: &str) -> Vec<Token> {
|
|||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
kind: TokenKind::String,
|
kind: TokenKind::String,
|
||||||
|
varargs: false,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -133,35 +188,35 @@ pub fn tokenize_line(line: &str) -> Vec<Token> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let word = &line[start..end];
|
let word = &line[start..end];
|
||||||
let kind = classify_word(word);
|
let (kind, varargs) = classify_word(word);
|
||||||
tokens.push(Token { start, end, kind });
|
tokens.push(Token { start, end, kind, varargs });
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens
|
tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
fn classify_word(word: &str) -> TokenKind {
|
fn classify_word(word: &str) -> (TokenKind, bool) {
|
||||||
if word.parse::<f64>().is_ok() || word.parse::<i64>().is_ok() {
|
if word.parse::<f64>().is_ok() || word.parse::<i64>().is_ok() {
|
||||||
return TokenKind::Number;
|
return (TokenKind::Number, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(kind) = lookup_word_kind(word) {
|
if let Some((kind, varargs)) = lookup_word_kind(word) {
|
||||||
return kind;
|
return (kind, varargs);
|
||||||
}
|
}
|
||||||
|
|
||||||
if INTERVALS.contains(&word) {
|
if INTERVALS.contains(&word) {
|
||||||
return TokenKind::Interval;
|
return (TokenKind::Interval, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_note(&word.to_ascii_lowercase()) {
|
if is_note(&word.to_ascii_lowercase()) {
|
||||||
return TokenKind::Note;
|
return (TokenKind::Note, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if word.len() > 1 && (word.starts_with('@') || word.starts_with('!')) {
|
if word.len() > 1 && (word.starts_with('@') || word.starts_with('!')) {
|
||||||
return TokenKind::Variable;
|
return (TokenKind::Variable, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
TokenKind::Default
|
(TokenKind::Default, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn highlight_line(line: &str) -> Vec<(Style, String)> {
|
pub fn highlight_line(line: &str) -> Vec<(Style, String)> {
|
||||||
@@ -179,13 +234,11 @@ pub fn highlight_line_with_runtime(
|
|||||||
|
|
||||||
let executed_bg = Color::Rgb(40, 35, 50);
|
let executed_bg = Color::Rgb(40, 35, 50);
|
||||||
let selected_bg = Color::Rgb(80, 60, 20);
|
let selected_bg = Color::Rgb(80, 60, 20);
|
||||||
|
let gap_style = TokenKind::gap_style();
|
||||||
|
|
||||||
for token in tokens {
|
for token in tokens {
|
||||||
if token.start > last_end {
|
if token.start > last_end {
|
||||||
result.push((
|
result.push((gap_style, line[last_end..token.start].to_string()));
|
||||||
TokenKind::Default.style(),
|
|
||||||
line[last_end..token.start].to_string(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_selected = selected_spans
|
let is_selected = selected_spans
|
||||||
@@ -196,6 +249,9 @@ pub fn highlight_line_with_runtime(
|
|||||||
.any(|span| overlaps(token.start, token.end, span.start, span.end));
|
.any(|span| overlaps(token.start, token.end, span.start, span.end));
|
||||||
|
|
||||||
let mut style = token.kind.style();
|
let mut style = token.kind.style();
|
||||||
|
if token.varargs {
|
||||||
|
style = style.add_modifier(Modifier::UNDERLINED);
|
||||||
|
}
|
||||||
if is_selected {
|
if is_selected {
|
||||||
style = style.bg(selected_bg).add_modifier(Modifier::BOLD);
|
style = style.bg(selected_bg).add_modifier(Modifier::BOLD);
|
||||||
} else if is_executed {
|
} else if is_executed {
|
||||||
@@ -207,7 +263,7 @@ pub fn highlight_line_with_runtime(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if last_end < line.len() {
|
if last_end < line.len() {
|
||||||
result.push((TokenKind::Default.style(), line[last_end..].to_string()));
|
result.push((gap_style, line[last_end..].to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
@@ -15,7 +17,7 @@ use crate::app::App;
|
|||||||
use crate::engine::{LinkState, SequencerSnapshot};
|
use crate::engine::{LinkState, SequencerSnapshot};
|
||||||
use crate::model::{SourceSpan, StepContext, Value};
|
use crate::model::{SourceSpan, StepContext, Value};
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::state::{FlashKind, Modal, PanelFocus, PatternField, SidePanel};
|
use crate::state::{FlashKind, Modal, PanelFocus, PatternField, SidePanel, StackCache};
|
||||||
use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime};
|
use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime};
|
||||||
use crate::widgets::{
|
use crate::widgets::{
|
||||||
ConfirmModal, ModalFrame, NavMinimap, NavTile, SampleBrowser, TextInputModal,
|
ConfirmModal, ModalFrame, NavMinimap, NavTile, SampleBrowser, TextInputModal,
|
||||||
@@ -25,15 +27,30 @@ use super::{
|
|||||||
dict_view, engine_view, help_view, main_view, options_view, patterns_view, title_view,
|
dict_view, engine_view, help_view, main_view, options_view, patterns_view, title_view,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn compute_stack_display(lines: &[String], editor: &cagire_ratatui::Editor) -> String {
|
fn compute_stack_display(lines: &[String], editor: &cagire_ratatui::Editor, cache: &std::cell::RefCell<Option<StackCache>>) -> String {
|
||||||
let cursor_line = editor.cursor().0;
|
let cursor_line = editor.cursor().0;
|
||||||
|
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
for (i, line) in lines.iter().enumerate() {
|
||||||
|
if i > cursor_line {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
line.hash(&mut hasher);
|
||||||
|
}
|
||||||
|
let lines_hash = hasher.finish();
|
||||||
|
|
||||||
|
if let Some(ref c) = *cache.borrow() {
|
||||||
|
if c.cursor_line == cursor_line && c.lines_hash == lines_hash {
|
||||||
|
return c.result.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let partial: Vec<&str> = lines.iter().take(cursor_line + 1).map(|s| s.as_str()).collect();
|
let partial: Vec<&str> = lines.iter().take(cursor_line + 1).map(|s| s.as_str()).collect();
|
||||||
let script = partial.join("\n");
|
let script = partial.join("\n");
|
||||||
|
|
||||||
if script.trim().is_empty() {
|
let result = if script.trim().is_empty() {
|
||||||
return "Stack: []".to_string();
|
"Stack: []".to_string()
|
||||||
}
|
} else {
|
||||||
|
|
||||||
let vars = Arc::new(Mutex::new(HashMap::new()));
|
let vars = Arc::new(Mutex::new(HashMap::new()));
|
||||||
let dict = Arc::new(Mutex::new(HashMap::new()));
|
let dict = Arc::new(Mutex::new(HashMap::new()));
|
||||||
let rng = Arc::new(Mutex::new(StdRng::seed_from_u64(42)));
|
let rng = Arc::new(Mutex::new(StdRng::seed_from_u64(42)));
|
||||||
@@ -62,6 +79,15 @@ fn compute_stack_display(lines: &[String], editor: &cagire_ratatui::Editor) -> S
|
|||||||
}
|
}
|
||||||
Err(e) => format!("Error: {e}"),
|
Err(e) => format!("Error: {e}"),
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
*cache.borrow_mut() = Some(StackCache {
|
||||||
|
cursor_line,
|
||||||
|
lines_hash,
|
||||||
|
result: result.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_value(v: &Value) -> String {
|
fn format_value(v: &Value) -> String {
|
||||||
@@ -740,13 +766,13 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
|||||||
]);
|
]);
|
||||||
frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area);
|
frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area);
|
||||||
} else if app.editor_ctx.show_stack {
|
} else if app.editor_ctx.show_stack {
|
||||||
let stack_text = compute_stack_display(text_lines, &app.editor_ctx.editor);
|
let stack_text = compute_stack_display(text_lines, &app.editor_ctx.editor, &app.editor_ctx.stack_cache);
|
||||||
let hint = Line::from(vec![
|
let hint = Line::from(vec![
|
||||||
Span::styled("Esc", key),
|
Span::styled("Esc", key),
|
||||||
Span::styled(" save ", dim),
|
Span::styled(" save ", dim),
|
||||||
Span::styled("C-e", key),
|
Span::styled("C-e", key),
|
||||||
Span::styled(" eval ", dim),
|
Span::styled(" eval ", dim),
|
||||||
Span::styled("C-k", key),
|
Span::styled("C-s", key),
|
||||||
Span::styled(" hide", dim),
|
Span::styled(" hide", dim),
|
||||||
]);
|
]);
|
||||||
let [hint_left, stack_right] = Layout::horizontal([
|
let [hint_left, stack_right] = Layout::horizontal([
|
||||||
@@ -767,7 +793,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
|||||||
Span::styled(" eval ", dim),
|
Span::styled(" eval ", dim),
|
||||||
Span::styled("C-f", key),
|
Span::styled("C-f", key),
|
||||||
Span::styled(" find ", dim),
|
Span::styled(" find ", dim),
|
||||||
Span::styled("C-k", key),
|
Span::styled("C-s", key),
|
||||||
Span::styled(" stack ", dim),
|
Span::styled(" stack ", dim),
|
||||||
Span::styled("C-u", key),
|
Span::styled("C-u", key),
|
||||||
Span::styled("/", dim),
|
Span::styled("/", dim),
|
||||||
|
|||||||
@@ -48,3 +48,6 @@ mod list_words;
|
|||||||
|
|
||||||
#[path = "forth/ramps.rs"]
|
#[path = "forth/ramps.rs"]
|
||||||
mod ramps;
|
mod ramps;
|
||||||
|
|
||||||
|
#[path = "forth/generator.rs"]
|
||||||
|
mod generator;
|
||||||
|
|||||||
104
tests/forth/generator.rs
Normal file
104
tests/forth/generator.rs
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
use super::harness::*;
|
||||||
|
use cagire::forth::Value;
|
||||||
|
|
||||||
|
fn int(n: i64) -> Value {
|
||||||
|
Value::Int(n, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn range_ascending() {
|
||||||
|
expect_stack("1 4 ..", &[int(1), int(2), int(3), int(4)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn range_descending() {
|
||||||
|
expect_stack("4 1 ..", &[int(4), int(3), int(2), int(1)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn range_single() {
|
||||||
|
expect_stack("3 3 ..", &[int(3)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn range_negative() {
|
||||||
|
expect_stack("-2 1 ..", &[int(-2), int(-1), int(0), int(1)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn range_underflow() {
|
||||||
|
expect_error("1 ..", "stack underflow");
|
||||||
|
expect_error("..", "stack underflow");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gen_basic() {
|
||||||
|
expect_stack("{ 42 } 3 gen", &[int(42), int(42), int(42)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gen_with_computation() {
|
||||||
|
// Each iteration: dup current value, add 1, result is new value
|
||||||
|
// 0 → dup(0,0) 1+(0,1) → pop 1, stack [0]
|
||||||
|
// 0 → dup(0,0) 1+(0,1) → pop 1, stack [0]
|
||||||
|
// So we get [0, 1, 1, 1] - the 0 stays, we collect three 1s
|
||||||
|
expect_stack("0 { dup 1 + } 3 gen", &[int(0), int(1), int(1), int(1)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gen_chained() {
|
||||||
|
// Start with 1, each iteration: dup, multiply by 2
|
||||||
|
// 1 → dup(1,1) 2*(1,2) → pop 2, stack [1]
|
||||||
|
// 1 → dup(1,1) 2*(1,2) → pop 2, stack [1]
|
||||||
|
expect_stack("1 { dup 2 * } 3 gen", &[int(1), int(2), int(2), int(2)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gen_zero() {
|
||||||
|
expect_stack("{ 1 } 0 gen", &[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gen_underflow() {
|
||||||
|
expect_error("3 gen", "stack underflow");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gen_not_a_number() {
|
||||||
|
expect_error("{ 1 } gen", "expected number");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gen_negative() {
|
||||||
|
expect_error("{ 1 } -1 gen", "gen count must be >= 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gen_empty_quot_error() {
|
||||||
|
expect_error("{ } 3 gen", "quotation must produce");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn geom_growing() {
|
||||||
|
expect_stack("1 2 4 geom..", &[int(1), int(2), int(4), int(8)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn geom_shrinking() {
|
||||||
|
expect_stack("8 0.5 4 geom..", &[int(8), int(4), int(2), int(1)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn geom_single() {
|
||||||
|
expect_stack("5 2 1 geom..", &[int(5)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn geom_zero_count() {
|
||||||
|
expect_stack("1 2 0 geom..", &[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn geom_underflow() {
|
||||||
|
expect_error("1 2 geom..", "stack underflow");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user