Compare commits
3 Commits
61daa9d79d
...
a3a39ea28e
| Author | SHA1 | Date | |
|---|---|---|---|
| a3a39ea28e | |||
| 574625735b | |||
| 40c509e295 |
@@ -95,7 +95,6 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
|||||||
let mut ops = Vec::new();
|
let mut ops = Vec::new();
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
let mut list_depth: usize = 0;
|
let mut list_depth: usize = 0;
|
||||||
let mut pipe_parity = false;
|
|
||||||
|
|
||||||
while i < tokens.len() {
|
while i < tokens.len() {
|
||||||
match &tokens[i] {
|
match &tokens[i] {
|
||||||
@@ -131,13 +130,6 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
|||||||
ops.push(Op::Branch(else_ops.len()));
|
ops.push(Op::Branch(else_ops.len()));
|
||||||
ops.extend(else_ops);
|
ops.extend(else_ops);
|
||||||
}
|
}
|
||||||
} else if word == "|" {
|
|
||||||
if pipe_parity {
|
|
||||||
ops.push(Op::InternalCycleEnd);
|
|
||||||
} else {
|
|
||||||
ops.push(Op::ListStart);
|
|
||||||
}
|
|
||||||
pipe_parity = !pipe_parity;
|
|
||||||
} else if is_list_start(word) {
|
} else if is_list_start(word) {
|
||||||
ops.push(Op::ListStart);
|
ops.push(Op::ListStart);
|
||||||
list_depth += 1;
|
list_depth += 1;
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ mod vm;
|
|||||||
mod words;
|
mod words;
|
||||||
|
|
||||||
pub use types::{Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Value, Variables};
|
pub use types::{Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Value, Variables};
|
||||||
pub use vm::{EmissionCounter, Forth};
|
pub use vm::Forth;
|
||||||
pub use words::{Word, WordCompile, WORDS};
|
pub use words::{Word, WordCompile, WORDS};
|
||||||
|
|||||||
@@ -84,9 +84,9 @@ pub enum Op {
|
|||||||
Loop,
|
Loop,
|
||||||
Degree(&'static [i64]),
|
Degree(&'static [i64]),
|
||||||
Oct,
|
Oct,
|
||||||
InternalCycleEnd,
|
|
||||||
DivStart,
|
DivStart,
|
||||||
DivEnd,
|
DivEnd,
|
||||||
StackStart,
|
StackStart,
|
||||||
EmitN,
|
EmitN,
|
||||||
|
ClearCmd,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ pub enum Value {
|
|||||||
Str(String, Option<SourceSpan>),
|
Str(String, Option<SourceSpan>),
|
||||||
Marker,
|
Marker,
|
||||||
Quotation(Vec<Op>, Option<SourceSpan>),
|
Quotation(Vec<Op>, Option<SourceSpan>),
|
||||||
Alternator(Vec<Value>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Value {
|
impl PartialEq for Value {
|
||||||
@@ -59,7 +58,6 @@ impl PartialEq for Value {
|
|||||||
(Value::Str(a, _), Value::Str(b, _)) => a == b,
|
(Value::Str(a, _), Value::Str(b, _)) => a == b,
|
||||||
(Value::Marker, Value::Marker) => true,
|
(Value::Marker, Value::Marker) => true,
|
||||||
(Value::Quotation(a, _), Value::Quotation(b, _)) => a == b,
|
(Value::Quotation(a, _), Value::Quotation(b, _)) => a == b,
|
||||||
(Value::Alternator(a), Value::Alternator(b)) => a == b,
|
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,7 +94,6 @@ impl Value {
|
|||||||
Value::Str(s, _) => !s.is_empty(),
|
Value::Str(s, _) => !s.is_empty(),
|
||||||
Value::Marker => false,
|
Value::Marker => false,
|
||||||
Value::Quotation(..) => true,
|
Value::Quotation(..) => true,
|
||||||
Value::Alternator(items) => !items.is_empty(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,14 +108,13 @@ impl Value {
|
|||||||
Value::Str(s, _) => s.clone(),
|
Value::Str(s, _) => s.clone(),
|
||||||
Value::Marker => String::new(),
|
Value::Marker => String::new(),
|
||||||
Value::Quotation(..) => String::new(),
|
Value::Quotation(..) => String::new(),
|
||||||
Value::Alternator(_) => String::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn span(&self) -> Option<SourceSpan> {
|
pub(super) fn span(&self) -> Option<SourceSpan> {
|
||||||
match self {
|
match self {
|
||||||
Value::Int(_, s) | Value::Float(_, s) | Value::Str(_, s) => *s,
|
Value::Int(_, s) | Value::Float(_, s) | Value::Str(_, s) | Value::Quotation(_, s) => *s,
|
||||||
Value::Marker | Value::Quotation(..) | Value::Alternator(_) => None,
|
Value::Marker => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,7 +135,14 @@ impl CmdRegister {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn snapshot(&self) -> Option<(Value, Vec<(String, Value)>)> {
|
pub(super) fn snapshot(&self) -> Option<(Value, Vec<(String, Value)>)> {
|
||||||
self.sound.as_ref().map(|s| (s.clone(), self.params.clone()))
|
self.sound
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| (s.clone(), self.params.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn clear(&mut self) {
|
||||||
|
self.sound = None;
|
||||||
|
self.params.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1222,6 +1222,54 @@ pub const WORDS: &[Word] = &[
|
|||||||
example: "0.3 bpr",
|
example: "0.3 bpr",
|
||||||
compile: Param,
|
compile: Param,
|
||||||
},
|
},
|
||||||
|
Word {
|
||||||
|
name: "llpf",
|
||||||
|
category: "Ladder Filter",
|
||||||
|
stack: "(f --)",
|
||||||
|
desc: "Set ladder lowpass frequency",
|
||||||
|
example: "2000 llpf",
|
||||||
|
compile: Param,
|
||||||
|
},
|
||||||
|
Word {
|
||||||
|
name: "llpq",
|
||||||
|
category: "Ladder Filter",
|
||||||
|
stack: "(f --)",
|
||||||
|
desc: "Set ladder lowpass resonance",
|
||||||
|
example: "0.5 llpq",
|
||||||
|
compile: Param,
|
||||||
|
},
|
||||||
|
Word {
|
||||||
|
name: "lhpf",
|
||||||
|
category: "Ladder Filter",
|
||||||
|
stack: "(f --)",
|
||||||
|
desc: "Set ladder highpass frequency",
|
||||||
|
example: "100 lhpf",
|
||||||
|
compile: Param,
|
||||||
|
},
|
||||||
|
Word {
|
||||||
|
name: "lhpq",
|
||||||
|
category: "Ladder Filter",
|
||||||
|
stack: "(f --)",
|
||||||
|
desc: "Set ladder highpass resonance",
|
||||||
|
example: "0.5 lhpq",
|
||||||
|
compile: Param,
|
||||||
|
},
|
||||||
|
Word {
|
||||||
|
name: "lbpf",
|
||||||
|
category: "Ladder Filter",
|
||||||
|
stack: "(f --)",
|
||||||
|
desc: "Set ladder bandpass frequency",
|
||||||
|
example: "1000 lbpf",
|
||||||
|
compile: Param,
|
||||||
|
},
|
||||||
|
Word {
|
||||||
|
name: "lbpq",
|
||||||
|
category: "Ladder Filter",
|
||||||
|
stack: "(f --)",
|
||||||
|
desc: "Set ladder bandpass resonance",
|
||||||
|
example: "0.5 lbpq",
|
||||||
|
compile: Param,
|
||||||
|
},
|
||||||
Word {
|
Word {
|
||||||
name: "ftype",
|
name: "ftype",
|
||||||
category: "Filter",
|
category: "Filter",
|
||||||
@@ -1486,6 +1534,54 @@ pub const WORDS: &[Word] = &[
|
|||||||
example: "0.02 chorusdelay",
|
example: "0.02 chorusdelay",
|
||||||
compile: Param,
|
compile: Param,
|
||||||
},
|
},
|
||||||
|
Word {
|
||||||
|
name: "eqlo",
|
||||||
|
category: "EQ",
|
||||||
|
stack: "(f --)",
|
||||||
|
desc: "Set low shelf gain (dB)",
|
||||||
|
example: "3 eqlo",
|
||||||
|
compile: Param,
|
||||||
|
},
|
||||||
|
Word {
|
||||||
|
name: "eqmid",
|
||||||
|
category: "EQ",
|
||||||
|
stack: "(f --)",
|
||||||
|
desc: "Set mid peak gain (dB)",
|
||||||
|
example: "-2 eqmid",
|
||||||
|
compile: Param,
|
||||||
|
},
|
||||||
|
Word {
|
||||||
|
name: "eqhi",
|
||||||
|
category: "EQ",
|
||||||
|
stack: "(f --)",
|
||||||
|
desc: "Set high shelf gain (dB)",
|
||||||
|
example: "1 eqhi",
|
||||||
|
compile: Param,
|
||||||
|
},
|
||||||
|
Word {
|
||||||
|
name: "tilt",
|
||||||
|
category: "EQ",
|
||||||
|
stack: "(f --)",
|
||||||
|
desc: "Set tilt EQ (-1 dark, 1 bright)",
|
||||||
|
example: "-0.5 tilt",
|
||||||
|
compile: Param,
|
||||||
|
},
|
||||||
|
Word {
|
||||||
|
name: "width",
|
||||||
|
category: "Stereo",
|
||||||
|
stack: "(f --)",
|
||||||
|
desc: "Set stereo width (0 mono, 1 normal, 2 wide)",
|
||||||
|
example: "0 width",
|
||||||
|
compile: Param,
|
||||||
|
},
|
||||||
|
Word {
|
||||||
|
name: "haas",
|
||||||
|
category: "Stereo",
|
||||||
|
stack: "(f --)",
|
||||||
|
desc: "Set Haas delay in ms (spatial placement)",
|
||||||
|
example: "8 haas",
|
||||||
|
compile: Param,
|
||||||
|
},
|
||||||
Word {
|
Word {
|
||||||
name: "comb",
|
name: "comb",
|
||||||
category: "Filter",
|
category: "Filter",
|
||||||
@@ -1718,6 +1814,14 @@ pub const WORDS: &[Word] = &[
|
|||||||
example: "1 reset",
|
example: "1 reset",
|
||||||
compile: Param,
|
compile: Param,
|
||||||
},
|
},
|
||||||
|
Word {
|
||||||
|
name: "clear",
|
||||||
|
category: "Sound",
|
||||||
|
stack: "(--)",
|
||||||
|
desc: "Clear sound register (sound and all params)",
|
||||||
|
example: "\"kick\" s 0.5 gain . clear \"hat\" s .",
|
||||||
|
compile: Simple,
|
||||||
|
},
|
||||||
// Quotation execution
|
// Quotation execution
|
||||||
Word {
|
Word {
|
||||||
name: "apply",
|
name: "apply",
|
||||||
@@ -1823,6 +1927,7 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
"stack" => Op::StackStart,
|
"stack" => Op::StackStart,
|
||||||
"~" => Op::DivEnd,
|
"~" => Op::DivEnd,
|
||||||
".!" => Op::EmitN,
|
".!" => Op::EmitN,
|
||||||
|
"clear" => Op::ClearCmd,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1902,11 +2007,28 @@ fn parse_interval(name: &str) -> Option<i64> {
|
|||||||
Some(simple)
|
Some(simple)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn compile_word(name: &str, span: Option<SourceSpan>, ops: &mut Vec<Op>, dict: &Dictionary) -> bool {
|
pub(super) fn compile_word(
|
||||||
|
name: &str,
|
||||||
|
span: Option<SourceSpan>,
|
||||||
|
ops: &mut Vec<Op>,
|
||||||
|
dict: &Dictionary,
|
||||||
|
) -> bool {
|
||||||
match name {
|
match name {
|
||||||
"linramp" => { ops.push(Op::PushFloat(1.0, span)); ops.push(Op::Ramp); return true; }
|
"linramp" => {
|
||||||
"expramp" => { ops.push(Op::PushFloat(3.0, span)); ops.push(Op::Ramp); return true; }
|
ops.push(Op::PushFloat(1.0, span));
|
||||||
"logramp" => { ops.push(Op::PushFloat(0.3, span)); ops.push(Op::Ramp); return true; }
|
ops.push(Op::Ramp);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
"expramp" => {
|
||||||
|
ops.push(Op::PushFloat(3.0, span));
|
||||||
|
ops.push(Op::Ramp);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
"logramp" => {
|
||||||
|
ops.push(Op::PushFloat(0.3, span));
|
||||||
|
ops.push(Op::Ramp);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,11 +35,13 @@ impl From<&Project> for ProjectFile {
|
|||||||
|
|
||||||
impl From<ProjectFile> for Project {
|
impl From<ProjectFile> for Project {
|
||||||
fn from(file: ProjectFile) -> Self {
|
fn from(file: ProjectFile) -> Self {
|
||||||
Self {
|
let mut project = Self {
|
||||||
banks: file.banks,
|
banks: file.banks,
|
||||||
sample_paths: file.sample_paths,
|
sample_paths: file.sample_paths,
|
||||||
tempo: file.tempo,
|
tempo: file.tempo,
|
||||||
}
|
};
|
||||||
|
project.normalize();
|
||||||
|
project
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -282,4 +282,14 @@ impl Project {
|
|||||||
pub fn pattern_at_mut(&mut self, bank: usize, pattern: usize) -> &mut Pattern {
|
pub fn pattern_at_mut(&mut self, bank: usize, pattern: usize) -> &mut Pattern {
|
||||||
&mut self.banks[bank].patterns[pattern]
|
&mut self.banks[bank].patterns[pattern]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn normalize(&mut self) {
|
||||||
|
self.banks.resize_with(MAX_BANKS, Bank::default);
|
||||||
|
for bank in &mut self.banks {
|
||||||
|
bank.patterns.resize_with(MAX_PATTERNS, Pattern::default);
|
||||||
|
for pattern in &mut bank.patterns {
|
||||||
|
pattern.steps.resize_with(MAX_STEPS, Step::default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
rand = "0.8"
|
||||||
ratatui = "0.29"
|
ratatui = "0.29"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
tui-textarea = { version = "0.7", features = ["search"] }
|
tui-textarea = { version = "0.7", features = ["search"] }
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ mod modal;
|
|||||||
mod nav_minimap;
|
mod nav_minimap;
|
||||||
mod sample_browser;
|
mod sample_browser;
|
||||||
mod scope;
|
mod scope;
|
||||||
|
mod sparkles;
|
||||||
mod spectrum;
|
mod spectrum;
|
||||||
mod text_input;
|
mod text_input;
|
||||||
mod vu_meter;
|
mod vu_meter;
|
||||||
@@ -18,6 +19,7 @@ pub use modal::ModalFrame;
|
|||||||
pub use nav_minimap::{NavMinimap, NavTile};
|
pub use nav_minimap::{NavMinimap, NavTile};
|
||||||
pub use sample_browser::{SampleBrowser, TreeLine, TreeLineKind};
|
pub use sample_browser::{SampleBrowser, TreeLine, TreeLineKind};
|
||||||
pub use scope::{Orientation, Scope};
|
pub use scope::{Orientation, Scope};
|
||||||
|
pub use sparkles::Sparkles;
|
||||||
pub use spectrum::Spectrum;
|
pub use spectrum::Spectrum;
|
||||||
pub use text_input::TextInputModal;
|
pub use text_input::TextInputModal;
|
||||||
pub use vu_meter::VuMeter;
|
pub use vu_meter::VuMeter;
|
||||||
|
|||||||
65
crates/ratatui/src/sparkles.rs
Normal file
65
crates/ratatui/src/sparkles.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
use rand::Rng;
|
||||||
|
use ratatui::buffer::Buffer;
|
||||||
|
use ratatui::layout::Rect;
|
||||||
|
use ratatui::style::{Color, Style};
|
||||||
|
use ratatui::widgets::Widget;
|
||||||
|
|
||||||
|
const CHARS: &[char] = &['·', '✦', '✧', '°', '•', '+', '⋆', '*'];
|
||||||
|
const COLORS: &[(u8, u8, u8)] = &[
|
||||||
|
(200, 220, 255),
|
||||||
|
(255, 200, 150),
|
||||||
|
(150, 255, 200),
|
||||||
|
(255, 150, 200),
|
||||||
|
(200, 150, 255),
|
||||||
|
];
|
||||||
|
|
||||||
|
struct Sparkle {
|
||||||
|
x: u16,
|
||||||
|
y: u16,
|
||||||
|
char_idx: usize,
|
||||||
|
life: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Sparkles {
|
||||||
|
sparkles: Vec<Sparkle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sparkles {
|
||||||
|
pub fn tick(&mut self, area: Rect) {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
for _ in 0..3 {
|
||||||
|
if rng.gen_bool(0.6) {
|
||||||
|
self.sparkles.push(Sparkle {
|
||||||
|
x: rng.gen_range(0..area.width),
|
||||||
|
y: rng.gen_range(0..area.height),
|
||||||
|
char_idx: rng.gen_range(0..CHARS.len()),
|
||||||
|
life: rng.gen_range(15..40),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.sparkles
|
||||||
|
.iter_mut()
|
||||||
|
.for_each(|s| s.life = s.life.saturating_sub(1));
|
||||||
|
self.sparkles.retain(|s| s.life > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for &Sparkles {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
for sparkle in &self.sparkles {
|
||||||
|
let color = COLORS[sparkle.char_idx % COLORS.len()];
|
||||||
|
let intensity = (sparkle.life as f32 / 30.0).min(1.0);
|
||||||
|
let r = (color.0 as f32 * intensity) as u8;
|
||||||
|
let g = (color.1 as f32 * intensity) as u8;
|
||||||
|
let b = (color.2 as f32 * intensity) as u8;
|
||||||
|
|
||||||
|
if sparkle.x < area.width && sparkle.y < area.height {
|
||||||
|
let x = area.x + sparkle.x;
|
||||||
|
let y = area.y + sparkle.y;
|
||||||
|
let ch = CHARS[sparkle.char_idx];
|
||||||
|
buf[(x, y)].set_char(ch).set_style(Style::new().fg(Color::Rgb(r, g, b)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/app.rs
40
src/app.rs
@@ -53,7 +53,8 @@ impl App {
|
|||||||
let variables = Arc::new(Mutex::new(HashMap::new()));
|
let variables = 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(0)));
|
let rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0)));
|
||||||
let script_engine = ScriptEngine::new(Arc::clone(&variables), Arc::clone(&dict), Arc::clone(&rng));
|
let script_engine =
|
||||||
|
ScriptEngine::new(Arc::clone(&variables), Arc::clone(&dict), Arc::clone(&rng));
|
||||||
let live_keys = Arc::new(LiveKeyState::new());
|
let live_keys = Arc::new(LiveKeyState::new());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -251,7 +252,9 @@ impl App {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
self.editor_ctx.editor.set_candidates(candidates);
|
self.editor_ctx.editor.set_candidates(candidates);
|
||||||
self.editor_ctx.editor.set_completion_enabled(self.ui.show_completion);
|
self.editor_ctx
|
||||||
|
.editor
|
||||||
|
.set_completion_enabled(self.ui.show_completion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,7 +336,8 @@ impl App {
|
|||||||
{
|
{
|
||||||
step.command = None;
|
step.command = None;
|
||||||
}
|
}
|
||||||
self.ui.flash(&format!("Script error: {e}"), 300, FlashKind::Error);
|
self.ui
|
||||||
|
.flash(&format!("Script error: {e}"), 300, FlashKind::Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -409,21 +413,14 @@ impl App {
|
|||||||
let is_playing = snapshot.is_playing(bank, pattern);
|
let is_playing = snapshot.is_playing(bank, pattern);
|
||||||
let pattern_data = self.project_state.project.pattern_at(bank, pattern);
|
let pattern_data = self.project_state.project.pattern_at(bank, pattern);
|
||||||
|
|
||||||
let existing = self
|
let existing = self.playback.staged_changes.iter().position(|c| {
|
||||||
.playback
|
c.change.pattern_id().bank == bank && c.change.pattern_id().pattern == pattern
|
||||||
.staged_changes
|
});
|
||||||
.iter()
|
|
||||||
.position(|c| {
|
|
||||||
c.change.pattern_id().bank == bank && c.change.pattern_id().pattern == pattern
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(idx) = existing {
|
if let Some(idx) = existing {
|
||||||
self.playback.staged_changes.remove(idx);
|
self.playback.staged_changes.remove(idx);
|
||||||
self.ui.set_status(format!(
|
self.ui
|
||||||
"B{:02}:P{:02} unstaged",
|
.set_status(format!("B{:02}:P{:02} unstaged", bank + 1, pattern + 1));
|
||||||
bank + 1,
|
|
||||||
pattern + 1
|
|
||||||
));
|
|
||||||
} else if is_playing {
|
} else if is_playing {
|
||||||
self.playback.staged_changes.push(StagedChange {
|
self.playback.staged_changes.push(StagedChange {
|
||||||
change: PatternChange::Stop { bank, pattern },
|
change: PatternChange::Stop { bank, pattern },
|
||||||
@@ -467,7 +464,8 @@ impl App {
|
|||||||
}
|
}
|
||||||
let count = self.playback.staged_changes.len();
|
let count = self.playback.staged_changes.len();
|
||||||
self.playback.staged_changes.clear();
|
self.playback.staged_changes.clear();
|
||||||
self.ui.set_status(format!("Cleared {count} staged changes"));
|
self.ui
|
||||||
|
.set_status(format!("Cleared {count} staged changes"));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_edit_pattern(&mut self, pattern: usize) {
|
pub fn select_edit_pattern(&mut self, pattern: usize) {
|
||||||
@@ -705,8 +703,11 @@ impl App {
|
|||||||
}
|
}
|
||||||
self.project_state.mark_dirty(bank, pattern);
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
self.ui
|
self.ui.flash(
|
||||||
.flash(&format!("Linked to step {:02}", copied.step + 1), 150, FlashKind::Success);
|
&format!("Linked to step {:02}", copied.step + 1),
|
||||||
|
150,
|
||||||
|
FlashKind::Success,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn harden_step(&mut self) {
|
pub fn harden_step(&mut self) {
|
||||||
@@ -896,7 +897,8 @@ impl App {
|
|||||||
// If current step is a shallow copy, navigate to source step
|
// If current step is a shallow copy, navigate to source step
|
||||||
let pattern = &self.project_state.project.banks[self.editor_ctx.bank].patterns
|
let pattern = &self.project_state.project.banks[self.editor_ctx.bank].patterns
|
||||||
[self.editor_ctx.pattern];
|
[self.editor_ctx.pattern];
|
||||||
if let Some(source) = pattern.steps[self.editor_ctx.step].source {
|
if let Some(source) = pattern.step(self.editor_ctx.step).and_then(|s| s.source)
|
||||||
|
{
|
||||||
self.editor_ctx.step = source;
|
self.editor_ctx.step = source;
|
||||||
}
|
}
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
|
|||||||
@@ -160,16 +160,6 @@ pub struct AnalysisHandle {
|
|||||||
thread: Option<JoinHandle<()>>,
|
thread: Option<JoinHandle<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnalysisHandle {
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn shutdown(mut self) {
|
|
||||||
self.running.store(false, Ordering::SeqCst);
|
|
||||||
if let Some(t) = self.thread.take() {
|
|
||||||
let _ = t.join();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for AnalysisHandle {
|
impl Drop for AnalysisHandle {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.running.store(false, Ordering::SeqCst);
|
self.running.store(false, Ordering::SeqCst);
|
||||||
|
|||||||
@@ -210,7 +210,10 @@ fn main() -> io::Result<()> {
|
|||||||
app.flush_queued_changes(&sequencer.cmd_tx);
|
app.flush_queued_changes(&sequencer.cmd_tx);
|
||||||
app.flush_dirty_patterns(&sequencer.cmd_tx);
|
app.flush_dirty_patterns(&sequencer.cmd_tx);
|
||||||
|
|
||||||
terminal.draw(|frame| views::render(frame, &mut app, &link, &seq_snapshot))?;
|
if app.ui.show_title {
|
||||||
|
app.ui.sparkles.tick(terminal.get_frame().area());
|
||||||
|
}
|
||||||
|
terminal.draw(|frame| views::render(frame, &app, &link, &seq_snapshot))?;
|
||||||
|
|
||||||
if event::poll(Duration::from_millis(app.audio.config.refresh_rate.millis()))? {
|
if event::poll(Duration::from_millis(app.audio.config.refresh_rate.millis()))? {
|
||||||
match event::read()? {
|
match event::read()? {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use cagire_ratatui::Sparkles;
|
||||||
|
|
||||||
use crate::state::Modal;
|
use crate::state::Modal;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
@@ -10,13 +12,6 @@ pub enum FlashKind {
|
|||||||
Info,
|
Info,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Sparkle {
|
|
||||||
pub x: u16,
|
|
||||||
pub y: u16,
|
|
||||||
pub char_idx: usize,
|
|
||||||
pub life: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum DictFocus {
|
pub enum DictFocus {
|
||||||
#[default]
|
#[default]
|
||||||
@@ -25,7 +20,7 @@ pub enum DictFocus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct UiState {
|
pub struct UiState {
|
||||||
pub sparkles: Vec<Sparkle>,
|
pub sparkles: Sparkles,
|
||||||
pub status_message: Option<String>,
|
pub status_message: Option<String>,
|
||||||
pub flash_until: Option<Instant>,
|
pub flash_until: Option<Instant>,
|
||||||
pub flash_kind: FlashKind,
|
pub flash_kind: FlashKind,
|
||||||
@@ -46,7 +41,7 @@ pub struct UiState {
|
|||||||
impl Default for UiState {
|
impl Default for UiState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
sparkles: Vec::new(),
|
sparkles: Sparkles::default(),
|
||||||
status_message: None,
|
status_message: None,
|
||||||
flash_until: None,
|
flash_until: None,
|
||||||
flash_kind: FlashKind::Success,
|
flash_kind: FlashKind::Success,
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ fn render_devices(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
let input_focused = section_focused && app.audio.device_kind == DeviceKind::Input;
|
let input_focused = section_focused && app.audio.device_kind == DeviceKind::Input;
|
||||||
|
|
||||||
render_device_column(
|
render_device_column(
|
||||||
frame, app, output_col,
|
frame, output_col,
|
||||||
"Output", &app.audio.output_devices,
|
"Output", &app.audio.output_devices,
|
||||||
app.audio.current_output_device_index(),
|
app.audio.current_output_device_index(),
|
||||||
app.audio.output_list.cursor,
|
app.audio.output_list.cursor,
|
||||||
@@ -172,7 +172,7 @@ fn render_devices(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
frame.render_widget(Paragraph::new(sep_lines), separator);
|
frame.render_widget(Paragraph::new(sep_lines), separator);
|
||||||
|
|
||||||
render_device_column(
|
render_device_column(
|
||||||
frame, app, input_col,
|
frame, input_col,
|
||||||
"Input", &app.audio.input_devices,
|
"Input", &app.audio.input_devices,
|
||||||
app.audio.current_input_device_index(),
|
app.audio.current_input_device_index(),
|
||||||
app.audio.input_list.cursor,
|
app.audio.input_list.cursor,
|
||||||
@@ -184,7 +184,6 @@ fn render_devices(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
|
|
||||||
fn render_device_column(
|
fn render_device_column(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
_app: &App,
|
|
||||||
area: Rect,
|
area: Rect,
|
||||||
label: &str,
|
label: &str,
|
||||||
devices: &[doux::audio::AudioDeviceInfo],
|
devices: &[doux::audio::AudioDeviceInfo],
|
||||||
|
|||||||
@@ -51,11 +51,57 @@ const OPERATORS: &[&str] = &[
|
|||||||
"or", "not", "ceil", "floor", "round", "mtof", "ftom",
|
"or", "not", "ceil", "floor", "round", "mtof", "ftom",
|
||||||
];
|
];
|
||||||
const KEYWORDS: &[&str] = &[
|
const KEYWORDS: &[&str] = &[
|
||||||
"if", "else", "then", "emit", "rand", "rrand", "seed", "cycle", "choose", "chance", "[", "]",
|
"if",
|
||||||
"zoom", "scale!", "stack", "echo", "necho", "for", "div", "each", "at", "pop", "adsr", "ad",
|
"else",
|
||||||
"?", "!?", "<<", ">>", "|", "@", "!", "pcycle", "tempo!", "prob", "sometimes", "often",
|
"then",
|
||||||
"rarely", "almostAlways", "almostNever", "always", "never", "coin", "fill", "iter", "every",
|
"emit",
|
||||||
"gt", "lt", ":", ";", "apply",
|
"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",
|
||||||
|
":",
|
||||||
|
";",
|
||||||
|
"apply",
|
||||||
|
"clear",
|
||||||
];
|
];
|
||||||
const SOUND: &[&str] = &["sound", "s"];
|
const SOUND: &[&str] = &["sound", "s"];
|
||||||
const CONTEXT: &[&str] = &[
|
const CONTEXT: &[&str] = &[
|
||||||
@@ -168,6 +214,23 @@ const PARAMS: &[&str] = &[
|
|||||||
"n",
|
"n",
|
||||||
"cut",
|
"cut",
|
||||||
"reset",
|
"reset",
|
||||||
|
"eqlo",
|
||||||
|
"eqmid",
|
||||||
|
"eqhi",
|
||||||
|
"tilt",
|
||||||
|
"width",
|
||||||
|
"haas",
|
||||||
|
"llpf",
|
||||||
|
"llpq",
|
||||||
|
"lhpf",
|
||||||
|
"lhpq",
|
||||||
|
"lbpf",
|
||||||
|
"lbpq",
|
||||||
|
"sub",
|
||||||
|
"suboct",
|
||||||
|
"subwave",
|
||||||
|
"bank",
|
||||||
|
"loop",
|
||||||
];
|
];
|
||||||
const INTERVALS: &[&str] = &[
|
const INTERVALS: &[&str] = &[
|
||||||
"P1", "unison", "m2", "M2", "m3", "M3", "P4", "aug4", "dim5", "tritone", "P5", "m6", "M6",
|
"P1", "unison", "m2", "M2", "m3", "M3", "P4", "aug4", "dim5", "tritone", "P5", "m6", "M6",
|
||||||
@@ -176,34 +239,27 @@ const INTERVALS: &[&str] = &[
|
|||||||
];
|
];
|
||||||
|
|
||||||
const NOTES: &[&str] = &[
|
const NOTES: &[&str] = &[
|
||||||
"c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9",
|
"c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "d0", "d1", "d2", "d3", "d4", "d5",
|
||||||
"d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9",
|
"d6", "d7", "d8", "d9", "e0", "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "f0", "f1",
|
||||||
"e0", "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9",
|
"f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "g0", "g1", "g2", "g3", "g4", "g5", "g6", "g7",
|
||||||
"f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9",
|
"g8", "g9", "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "b0", "b1", "b2", "b3",
|
||||||
"g0", "g1", "g2", "g3", "g4", "g5", "g6", "g7", "g8", "g9",
|
"b4", "b5", "b6", "b7", "b8", "b9", "cs0", "cs1", "cs2", "cs3", "cs4", "cs5", "cs6", "cs7",
|
||||||
"a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9",
|
"cs8", "cs9", "ds0", "ds1", "ds2", "ds3", "ds4", "ds5", "ds6", "ds7", "ds8", "ds9", "es0",
|
||||||
"b0", "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9",
|
"es1", "es2", "es3", "es4", "es5", "es6", "es7", "es8", "es9", "fs0", "fs1", "fs2", "fs3",
|
||||||
"cs0", "cs1", "cs2", "cs3", "cs4", "cs5", "cs6", "cs7", "cs8", "cs9",
|
"fs4", "fs5", "fs6", "fs7", "fs8", "fs9", "gs0", "gs1", "gs2", "gs3", "gs4", "gs5", "gs6",
|
||||||
"ds0", "ds1", "ds2", "ds3", "ds4", "ds5", "ds6", "ds7", "ds8", "ds9",
|
"gs7", "gs8", "gs9", "as0", "as1", "as2", "as3", "as4", "as5", "as6", "as7", "as8", "as9",
|
||||||
"es0", "es1", "es2", "es3", "es4", "es5", "es6", "es7", "es8", "es9",
|
"bs0", "bs1", "bs2", "bs3", "bs4", "bs5", "bs6", "bs7", "bs8", "bs9", "cb0", "cb1", "cb2",
|
||||||
"fs0", "fs1", "fs2", "fs3", "fs4", "fs5", "fs6", "fs7", "fs8", "fs9",
|
"cb3", "cb4", "cb5", "cb6", "cb7", "cb8", "cb9", "db0", "db1", "db2", "db3", "db4", "db5",
|
||||||
"gs0", "gs1", "gs2", "gs3", "gs4", "gs5", "gs6", "gs7", "gs8", "gs9",
|
"db6", "db7", "db8", "db9", "eb0", "eb1", "eb2", "eb3", "eb4", "eb5", "eb6", "eb7", "eb8",
|
||||||
"as0", "as1", "as2", "as3", "as4", "as5", "as6", "as7", "as8", "as9",
|
"eb9", "fb0", "fb1", "fb2", "fb3", "fb4", "fb5", "fb6", "fb7", "fb8", "fb9", "gb0", "gb1",
|
||||||
"bs0", "bs1", "bs2", "bs3", "bs4", "bs5", "bs6", "bs7", "bs8", "bs9",
|
"gb2", "gb3", "gb4", "gb5", "gb6", "gb7", "gb8", "gb9", "ab0", "ab1", "ab2", "ab3", "ab4",
|
||||||
"cb0", "cb1", "cb2", "cb3", "cb4", "cb5", "cb6", "cb7", "cb8", "cb9",
|
"ab5", "ab6", "ab7", "ab8", "ab9", "bb0", "bb1", "bb2", "bb3", "bb4", "bb5", "bb6", "bb7",
|
||||||
"db0", "db1", "db2", "db3", "db4", "db5", "db6", "db7", "db8", "db9",
|
"bb8", "bb9", "c#0", "c#1", "c#2", "c#3", "c#4", "c#5", "c#6", "c#7", "c#8", "c#9", "d#0",
|
||||||
"eb0", "eb1", "eb2", "eb3", "eb4", "eb5", "eb6", "eb7", "eb8", "eb9",
|
"d#1", "d#2", "d#3", "d#4", "d#5", "d#6", "d#7", "d#8", "d#9", "e#0", "e#1", "e#2", "e#3",
|
||||||
"fb0", "fb1", "fb2", "fb3", "fb4", "fb5", "fb6", "fb7", "fb8", "fb9",
|
"e#4", "e#5", "e#6", "e#7", "e#8", "e#9", "f#0", "f#1", "f#2", "f#3", "f#4", "f#5", "f#6",
|
||||||
"gb0", "gb1", "gb2", "gb3", "gb4", "gb5", "gb6", "gb7", "gb8", "gb9",
|
"f#7", "f#8", "f#9", "g#0", "g#1", "g#2", "g#3", "g#4", "g#5", "g#6", "g#7", "g#8", "g#9",
|
||||||
"ab0", "ab1", "ab2", "ab3", "ab4", "ab5", "ab6", "ab7", "ab8", "ab9",
|
"a#0", "a#1", "a#2", "a#3", "a#4", "a#5", "a#6", "a#7", "a#8", "a#9", "b#0", "b#1", "b#2",
|
||||||
"bb0", "bb1", "bb2", "bb3", "bb4", "bb5", "bb6", "bb7", "bb8", "bb9",
|
"b#3", "b#4", "b#5", "b#6", "b#7", "b#8", "b#9",
|
||||||
"c#0", "c#1", "c#2", "c#3", "c#4", "c#5", "c#6", "c#7", "c#8", "c#9",
|
|
||||||
"d#0", "d#1", "d#2", "d#3", "d#4", "d#5", "d#6", "d#7", "d#8", "d#9",
|
|
||||||
"e#0", "e#1", "e#2", "e#3", "e#4", "e#5", "e#6", "e#7", "e#8", "e#9",
|
|
||||||
"f#0", "f#1", "f#2", "f#3", "f#4", "f#5", "f#6", "f#7", "f#8", "f#9",
|
|
||||||
"g#0", "g#1", "g#2", "g#3", "g#4", "g#5", "g#6", "g#7", "g#8", "g#9",
|
|
||||||
"a#0", "a#1", "a#2", "a#3", "a#4", "a#5", "a#6", "a#7", "a#8", "a#9",
|
|
||||||
"b#0", "b#1", "b#2", "b#3", "b#4", "b#5", "b#6", "b#7", "b#8", "b#9",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn tokenize_line(line: &str) -> Vec<Token> {
|
pub fn tokenize_line(line: &str) -> Vec<Token> {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use crate::app::App;
|
|||||||
use crate::engine::SequencerSnapshot;
|
use crate::engine::SequencerSnapshot;
|
||||||
use crate::widgets::{Orientation, Scope, Spectrum, VuMeter};
|
use crate::widgets::{Orientation, Scope, Spectrum, VuMeter};
|
||||||
|
|
||||||
pub fn render(frame: &mut Frame, app: &mut App, snapshot: &SequencerSnapshot, area: Rect) {
|
pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
||||||
let [left_area, _spacer, vu_area] = Layout::horizontal([
|
let [left_area, _spacer, vu_area] = Layout::horizontal([
|
||||||
Constraint::Fill(1),
|
Constraint::Fill(1),
|
||||||
Constraint::Length(2),
|
Constraint::Length(2),
|
||||||
|
|||||||
@@ -28,14 +28,14 @@ fn adjust_spans_for_line(spans: &[SourceSpan], line_start: usize, line_len: usiz
|
|||||||
}).collect()
|
}).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(frame: &mut Frame, app: &mut App, link: &LinkState, snapshot: &SequencerSnapshot) {
|
pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &SequencerSnapshot) {
|
||||||
let term = frame.area();
|
let term = frame.area();
|
||||||
let blank = " ".repeat(term.width as usize);
|
let blank = " ".repeat(term.width as usize);
|
||||||
let lines: Vec<Line> = (0..term.height).map(|_| Line::raw(&blank)).collect();
|
let lines: Vec<Line> = (0..term.height).map(|_| Line::raw(&blank)).collect();
|
||||||
frame.render_widget(Paragraph::new(lines), term);
|
frame.render_widget(Paragraph::new(lines), term);
|
||||||
|
|
||||||
if app.ui.show_title {
|
if app.ui.show_title {
|
||||||
title_view::render(frame, term, &mut app.ui);
|
title_view::render(frame, term, &app.ui);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use rand::Rng;
|
|
||||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Color, Style, Stylize};
|
use ratatui::style::{Color, Style, Stylize};
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
@@ -6,56 +5,11 @@ use ratatui::widgets::Paragraph;
|
|||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
use tui_big_text::{BigText, PixelSize};
|
use tui_big_text::{BigText, PixelSize};
|
||||||
|
|
||||||
use crate::state::ui::{Sparkle, UiState};
|
use crate::state::ui::UiState;
|
||||||
|
|
||||||
const SPARKLE_CHARS: &[char] = &['·', '✦', '✧', '°', '•', '+', '⋆', '*'];
|
pub fn render(frame: &mut Frame, area: Rect, ui: &UiState) {
|
||||||
const SPARKLE_COLORS: &[(u8, u8, u8)] = &[
|
frame.render_widget(&ui.sparkles, area);
|
||||||
(200, 220, 255),
|
|
||||||
(255, 200, 150),
|
|
||||||
(150, 255, 200),
|
|
||||||
(255, 150, 200),
|
|
||||||
(200, 150, 255),
|
|
||||||
];
|
|
||||||
|
|
||||||
pub fn render(frame: &mut Frame, area: Rect, ui: &mut UiState) {
|
|
||||||
let mut rng = rand::thread_rng();
|
|
||||||
|
|
||||||
// Spawn new sparkles
|
|
||||||
for _ in 0..3 {
|
|
||||||
if rng.gen_bool(0.6) {
|
|
||||||
let x = rng.gen_range(0..area.width);
|
|
||||||
let y = rng.gen_range(0..area.height);
|
|
||||||
ui.sparkles.push(Sparkle {
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
char_idx: rng.gen_range(0..SPARKLE_CHARS.len()),
|
|
||||||
life: rng.gen_range(15..40),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Age and remove dead sparkles
|
|
||||||
ui.sparkles.iter_mut().for_each(|s| s.life = s.life.saturating_sub(1));
|
|
||||||
ui.sparkles.retain(|s| s.life > 0);
|
|
||||||
|
|
||||||
// Render sparkles
|
|
||||||
for sparkle in &ui.sparkles {
|
|
||||||
let color = SPARKLE_COLORS[sparkle.char_idx % SPARKLE_COLORS.len()];
|
|
||||||
let intensity = (sparkle.life as f32 / 30.0).min(1.0);
|
|
||||||
let r = (color.0 as f32 * intensity) as u8;
|
|
||||||
let g = (color.1 as f32 * intensity) as u8;
|
|
||||||
let b = (color.2 as f32 * intensity) as u8;
|
|
||||||
|
|
||||||
let ch = SPARKLE_CHARS[sparkle.char_idx];
|
|
||||||
let span = Span::styled(ch.to_string(), Style::new().fg(Color::Rgb(r, g, b)));
|
|
||||||
let para = Paragraph::new(Line::from(span));
|
|
||||||
let sparkle_area = Rect::new(sparkle.x, sparkle.y, 1, 1);
|
|
||||||
if sparkle_area.x < area.width && sparkle_area.y < area.height {
|
|
||||||
frame.render_widget(para, sparkle_area);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main content
|
|
||||||
let author_style = Style::new().fg(Color::Rgb(180, 140, 200));
|
let author_style = Style::new().fg(Color::Rgb(180, 140, 200));
|
||||||
let link_style = Style::new().fg(Color::Rgb(120, 200, 180));
|
let link_style = Style::new().fg(Color::Rgb(120, 200, 180));
|
||||||
let license_style = Style::new().fg(Color::Rgb(200, 160, 100));
|
let license_style = Style::new().fg(Color::Rgb(200, 160, 100));
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use rand::rngs::StdRng;
|
use rand::rngs::StdRng;
|
||||||
use rand::SeedableRng;
|
use rand::SeedableRng;
|
||||||
use cagire::forth::{Dictionary, EmissionCounter, Forth, Rng, StepContext, Value, Variables};
|
use cagire::forth::{Dictionary, Forth, Rng, StepContext, Value, Variables};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
@@ -46,14 +46,6 @@ pub fn forth_seeded(seed: u64) -> Forth {
|
|||||||
Forth::new(new_vars(), new_dict(), seeded_rng(seed))
|
Forth::new(new_vars(), new_dict(), seeded_rng(seed))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_emission_counter() -> EmissionCounter {
|
|
||||||
Arc::new(Mutex::new(0))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn forth_with_counter(counter: EmissionCounter) -> Forth {
|
|
||||||
Forth::new_with_counter(new_vars(), new_dict(), seeded_rng(42), counter)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run(script: &str) -> Forth {
|
pub fn run(script: &str) -> Forth {
|
||||||
let f = forth();
|
let f = forth();
|
||||||
f.evaluate(script, &default_ctx()).unwrap();
|
f.evaluate(script, &default_ctx()).unwrap();
|
||||||
@@ -139,13 +131,3 @@ pub fn expect_outputs(script: &str, count: usize) -> Vec<String> {
|
|||||||
outputs
|
outputs
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn expect_output_contains(script: &str, substr: &str) {
|
|
||||||
let outputs = expect_outputs(script, 1);
|
|
||||||
assert!(
|
|
||||||
outputs[0].contains(substr),
|
|
||||||
"output '{}' does not contain '{}'",
|
|
||||||
outputs[0],
|
|
||||||
substr
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -46,14 +46,15 @@ fn multiple_emits() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn envelope_params() {
|
fn envelope_params() {
|
||||||
|
// Values are tempo-scaled: 0.01 * step_duration(0.125) = 0.00125, etc.
|
||||||
let outputs = expect_outputs(
|
let outputs = expect_outputs(
|
||||||
r#""synth" s 0.01 attack 0.1 decay 0.7 sustain 0.3 release ."#,
|
r#""synth" s 0.01 attack 0.1 decay 0.7 sustain 0.3 release ."#,
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
assert!(outputs[0].contains("attack/0.01"));
|
assert!(outputs[0].contains("attack/0.00125"));
|
||||||
assert!(outputs[0].contains("decay/0.1"));
|
assert!(outputs[0].contains("decay/0.0125"));
|
||||||
assert!(outputs[0].contains("sustain/0.7"));
|
assert!(outputs[0].contains("sustain/0.7"));
|
||||||
assert!(outputs[0].contains("release/0.3"));
|
assert!(outputs[0].contains("release/0.0375"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -66,17 +67,17 @@ fn filter_params() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn adsr_sets_all_envelope_params() {
|
fn adsr_sets_all_envelope_params() {
|
||||||
let outputs = expect_outputs(r#""synth" s 0.01 0.1 0.5 0.3 adsr ."#, 1);
|
let outputs = expect_outputs(r#""synth" s 0.01 0.1 0.5 0.3 adsr ."#, 1);
|
||||||
assert!(outputs[0].contains("attack/0.01"));
|
assert!(outputs[0].contains("attack/0.00125"));
|
||||||
assert!(outputs[0].contains("decay/0.1"));
|
assert!(outputs[0].contains("decay/0.0125"));
|
||||||
assert!(outputs[0].contains("sustain/0.5"));
|
assert!(outputs[0].contains("sustain/0.5"));
|
||||||
assert!(outputs[0].contains("release/0.3"));
|
assert!(outputs[0].contains("release/0.0375"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ad_sets_attack_decay_sustain_zero() {
|
fn ad_sets_attack_decay_sustain_zero() {
|
||||||
let outputs = expect_outputs(r#""synth" s 0.01 0.1 ad ."#, 1);
|
let outputs = expect_outputs(r#""synth" s 0.01 0.1 ad ."#, 1);
|
||||||
assert!(outputs[0].contains("attack/0.01"));
|
assert!(outputs[0].contains("attack/0.00125"));
|
||||||
assert!(outputs[0].contains("decay/0.1"));
|
assert!(outputs[0].contains("decay/0.0125"));
|
||||||
assert!(outputs[0].contains("sustain/0"));
|
assert!(outputs[0].contains("sustain/0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
use super::harness::*;
|
use super::harness::*;
|
||||||
#[allow(unused_imports)]
|
|
||||||
use super::harness::{forth_with_counter, new_emission_counter};
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
fn parse_params(output: &str) -> HashMap<String, f64> {
|
fn parse_params(output: &str) -> HashMap<String, f64> {
|
||||||
@@ -245,51 +243,6 @@ fn dot_with_silence() {
|
|||||||
assert!(approx_eq(deltas[1], 2.0 * step));
|
assert!(approx_eq(deltas[1], 2.0 * step));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn internal_alternation_basic() {
|
|
||||||
let outputs = expect_outputs(r#"| "kick" "snare" | s . . . ."#, 4);
|
|
||||||
let sounds = get_sounds(&outputs);
|
|
||||||
assert_eq!(sounds, vec!["kick", "snare", "kick", "snare"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn internal_alternation_three_sounds() {
|
|
||||||
let outputs = expect_outputs(r#"| "kick" "snare" "hat" | s . . . . . ."#, 6);
|
|
||||||
let sounds = get_sounds(&outputs);
|
|
||||||
assert_eq!(sounds, vec!["kick", "snare", "hat", "kick", "snare", "hat"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn internal_alternation_single_item() {
|
|
||||||
let outputs = expect_outputs(r#"| "kick" | s . . . ."#, 4);
|
|
||||||
let sounds = get_sounds(&outputs);
|
|
||||||
assert_eq!(sounds, vec!["kick", "kick", "kick", "kick"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn internal_alternation_with_params() {
|
|
||||||
let outputs = expect_outputs(r#"| 0.5 0.9 | gain "kick" s . ."#, 2);
|
|
||||||
fn parse_gain(output: &str) -> f64 {
|
|
||||||
let parts: Vec<&str> = output.trim_start_matches('/').split('/').collect();
|
|
||||||
for i in 0..parts.len() - 1 {
|
|
||||||
if parts[i] == "gain" {
|
|
||||||
return parts[i + 1].parse().unwrap_or(0.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
0.0
|
|
||||||
}
|
|
||||||
let gains: Vec<f64> = outputs.iter().map(|o| parse_gain(o)).collect();
|
|
||||||
assert!(approx_eq(gains[0], 0.5), "first gain should be 0.5, got {}", gains[0]);
|
|
||||||
assert!(approx_eq(gains[1], 0.9), "second gain should be 0.9, got {}", gains[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn internal_alternation_empty_error() {
|
|
||||||
let f = forth();
|
|
||||||
let result = f.evaluate(r#"| | . ."#, &default_ctx());
|
|
||||||
assert!(result.is_err(), "empty internal cycle should error");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn div_basic_subdivision() {
|
fn div_basic_subdivision() {
|
||||||
let outputs = expect_outputs(r#"div "kick" s . "hat" s . ~"#, 2);
|
let outputs = expect_outputs(r#"div "kick" s . "hat" s . ~"#, 2);
|
||||||
@@ -355,40 +308,6 @@ fn unmatched_scope_terminator_error() {
|
|||||||
assert!(result.is_err(), "unmatched ~ should error");
|
assert!(result.is_err(), "unmatched ~ should error");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alternator_with_scale() {
|
|
||||||
let outputs = expect_outputs(r#""sine" s | 0 1 2 3 | mixolydian note . . . ."#, 4);
|
|
||||||
fn parse_note(output: &str) -> i64 {
|
|
||||||
let parts: Vec<&str> = output.trim_start_matches('/').split('/').collect();
|
|
||||||
for i in 0..parts.len() - 1 {
|
|
||||||
if parts[i] == "note" {
|
|
||||||
return parts[i + 1].parse().unwrap_or(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
0
|
|
||||||
}
|
|
||||||
let notes: Vec<i64> = outputs.iter().map(|o| parse_note(o)).collect();
|
|
||||||
// mixolydian from C4: 0->60, 1->62, 2->64, 3->65
|
|
||||||
assert_eq!(notes, vec![60, 62, 64, 65]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alternator_with_arithmetic() {
|
|
||||||
let outputs = expect_outputs(r#""sine" s | 100 200 | 2 * freq . ."#, 2);
|
|
||||||
fn parse_freq(output: &str) -> f64 {
|
|
||||||
let parts: Vec<&str> = output.trim_start_matches('/').split('/').collect();
|
|
||||||
for i in 0..parts.len() - 1 {
|
|
||||||
if parts[i] == "freq" {
|
|
||||||
return parts[i + 1].parse().unwrap_or(0.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
0.0
|
|
||||||
}
|
|
||||||
let freqs: Vec<f64> = outputs.iter().map(|o| parse_freq(o)).collect();
|
|
||||||
assert!(approx_eq(freqs[0], 200.0), "first freq: expected 200, got {}", freqs[0]);
|
|
||||||
assert!(approx_eq(freqs[1], 400.0), "second freq: expected 400, got {}", freqs[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stack_superposes_sounds() {
|
fn stack_superposes_sounds() {
|
||||||
let outputs = expect_outputs(r#"stack "kick" s . "hat" s . ~"#, 2);
|
let outputs = expect_outputs(r#"stack "kick" s . "hat" s . ~"#, 2);
|
||||||
@@ -452,13 +371,6 @@ fn emit_n_basic() {
|
|||||||
assert_eq!(sounds, vec!["kick", "kick", "kick", "kick"]);
|
assert_eq!(sounds, vec!["kick", "kick", "kick", "kick"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn emit_n_with_alternator() {
|
|
||||||
let outputs = expect_outputs(r#"| "kick" "snare" | s 4 .!"#, 4);
|
|
||||||
let sounds = get_sounds(&outputs);
|
|
||||||
assert_eq!(sounds, vec!["kick", "snare", "kick", "snare"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn emit_n_zero() {
|
fn emit_n_zero() {
|
||||||
let outputs = expect_outputs(r#""kick" s 0 .!"#, 0);
|
let outputs = expect_outputs(r#""kick" s 0 .!"#, 0);
|
||||||
@@ -472,62 +384,3 @@ fn emit_n_negative_error() {
|
|||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn persistent_counter_across_evaluations() {
|
|
||||||
let counter = new_emission_counter();
|
|
||||||
let ctx = default_ctx();
|
|
||||||
|
|
||||||
// First evaluation: kick, snare, kick, snare
|
|
||||||
let f1 = forth_with_counter(counter.clone());
|
|
||||||
let outputs1 = f1.evaluate(r#"| "kick" "snare" | s . ."#, &ctx).unwrap();
|
|
||||||
let sounds1 = get_sounds(&outputs1);
|
|
||||||
assert_eq!(sounds1, vec!["kick", "snare"]);
|
|
||||||
|
|
||||||
// Second evaluation: continues from where we left off
|
|
||||||
let f2 = forth_with_counter(counter.clone());
|
|
||||||
let outputs2 = f2.evaluate(r#"| "kick" "snare" | s . ."#, &ctx).unwrap();
|
|
||||||
let sounds2 = get_sounds(&outputs2);
|
|
||||||
assert_eq!(sounds2, vec!["kick", "snare"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn persistent_counter_three_item_cycle() {
|
|
||||||
let counter = new_emission_counter();
|
|
||||||
let ctx = default_ctx();
|
|
||||||
|
|
||||||
// First eval: kick, snare
|
|
||||||
let f1 = forth_with_counter(counter.clone());
|
|
||||||
let outputs1 = f1.evaluate(r#"| "kick" "snare" "hat" | s . ."#, &ctx).unwrap();
|
|
||||||
let sounds1 = get_sounds(&outputs1);
|
|
||||||
assert_eq!(sounds1, vec!["kick", "snare"]);
|
|
||||||
|
|
||||||
// Second eval: continues from hat (index 2)
|
|
||||||
let f2 = forth_with_counter(counter.clone());
|
|
||||||
let outputs2 = f2.evaluate(r#"| "kick" "snare" "hat" | s . ."#, &ctx).unwrap();
|
|
||||||
let sounds2 = get_sounds(&outputs2);
|
|
||||||
assert_eq!(sounds2, vec!["hat", "kick"]);
|
|
||||||
|
|
||||||
// Third eval: snare, hat
|
|
||||||
let f3 = forth_with_counter(counter.clone());
|
|
||||||
let outputs3 = f3.evaluate(r#"| "kick" "snare" "hat" | s . ."#, &ctx).unwrap();
|
|
||||||
let sounds3 = get_sounds(&outputs3);
|
|
||||||
assert_eq!(sounds3, vec!["snare", "hat"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn emit_n_with_persistent_counter() {
|
|
||||||
let counter = new_emission_counter();
|
|
||||||
let ctx = default_ctx();
|
|
||||||
|
|
||||||
// First eval: 3 emits from a 4-item cycle
|
|
||||||
let f1 = forth_with_counter(counter.clone());
|
|
||||||
let outputs1 = f1.evaluate(r#"| "a" "b" "c" "d" | s 3 .!"#, &ctx).unwrap();
|
|
||||||
let sounds1 = get_sounds(&outputs1);
|
|
||||||
assert_eq!(sounds1, vec!["a", "b", "c"]);
|
|
||||||
|
|
||||||
// Second eval: continues from d
|
|
||||||
let f2 = forth_with_counter(counter.clone());
|
|
||||||
let outputs2 = f2.evaluate(r#"| "a" "b" "c" "d" | s 3 .!"#, &ctx).unwrap();
|
|
||||||
let sounds2 = get_sounds(&outputs2);
|
|
||||||
assert_eq!(sounds2, vec!["d", "a", "b"]);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user