Compare commits
2 Commits
4743c33916
...
5a72e4cef4
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a72e4cef4 | |||
| 0097777449 |
@@ -1573,7 +1573,7 @@ impl Forth {
|
|||||||
} else {
|
} else {
|
||||||
let note = get_int("note").unwrap_or(60).clamp(0, 127) as u8;
|
let note = get_int("note").unwrap_or(60).clamp(0, 127) as u8;
|
||||||
let velocity =
|
let velocity =
|
||||||
get_int("velocity").unwrap_or(100).clamp(0, 127) as u8;
|
(get_float("velocity").unwrap_or(0.8) * 127.0).clamp(0.0, 127.0) as u8;
|
||||||
let dur = get_float("dur").unwrap_or(1.0);
|
let dur = get_float("dur").unwrap_or(1.0);
|
||||||
let dur_secs = dur * ctx.step_duration();
|
let dur_secs = dur * ctx.step_duration();
|
||||||
outputs.push(format!(
|
outputs.push(format!(
|
||||||
|
|||||||
@@ -28,14 +28,14 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
aliases: &[],
|
aliases: &[],
|
||||||
category: "Envelope",
|
category: "Envelope",
|
||||||
stack: "(v.. --)",
|
stack: "(v.. --)",
|
||||||
desc: "Set velocity",
|
desc: "Set velocity (0-1)",
|
||||||
example: "100 velocity",
|
example: "0.8 velocity",
|
||||||
compile: Param,
|
compile: Param,
|
||||||
varargs: true,
|
varargs: true,
|
||||||
},
|
},
|
||||||
Word {
|
Word {
|
||||||
name: "attack",
|
name: "attack",
|
||||||
aliases: &["att"],
|
aliases: &["att", "a"],
|
||||||
category: "Envelope",
|
category: "Envelope",
|
||||||
stack: "(v.. --)",
|
stack: "(v.. --)",
|
||||||
desc: "Set attack time",
|
desc: "Set attack time",
|
||||||
@@ -45,7 +45,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
},
|
},
|
||||||
Word {
|
Word {
|
||||||
name: "decay",
|
name: "decay",
|
||||||
aliases: &["dec"],
|
aliases: &["dec", "d"],
|
||||||
category: "Envelope",
|
category: "Envelope",
|
||||||
stack: "(v.. --)",
|
stack: "(v.. --)",
|
||||||
desc: "Set decay time",
|
desc: "Set decay time",
|
||||||
@@ -55,7 +55,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
},
|
},
|
||||||
Word {
|
Word {
|
||||||
name: "sustain",
|
name: "sustain",
|
||||||
aliases: &["sus"],
|
aliases: &["sus", "s"],
|
||||||
category: "Envelope",
|
category: "Envelope",
|
||||||
stack: "(v.. --)",
|
stack: "(v.. --)",
|
||||||
desc: "Set sustain level",
|
desc: "Set sustain level",
|
||||||
@@ -65,7 +65,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
},
|
},
|
||||||
Word {
|
Word {
|
||||||
name: "release",
|
name: "release",
|
||||||
aliases: &["rel"],
|
aliases: &["rel", "r"],
|
||||||
category: "Envelope",
|
category: "Envelope",
|
||||||
stack: "(v.. --)",
|
stack: "(v.. --)",
|
||||||
desc: "Set release time",
|
desc: "Set release time",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
// Sound
|
// Sound
|
||||||
Word {
|
Word {
|
||||||
name: "sound",
|
name: "sound",
|
||||||
aliases: &["s"],
|
aliases: &["snd"],
|
||||||
category: "Sound",
|
category: "Sound",
|
||||||
stack: "(name --)",
|
stack: "(name --)",
|
||||||
desc: "Begin sound command",
|
desc: "Begin sound command",
|
||||||
@@ -377,6 +377,16 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Param,
|
compile: Param,
|
||||||
varargs: true,
|
varargs: true,
|
||||||
},
|
},
|
||||||
|
Word {
|
||||||
|
name: "partials",
|
||||||
|
aliases: &[],
|
||||||
|
category: "Oscillator",
|
||||||
|
stack: "(v.. --)",
|
||||||
|
desc: "Set number of active harmonics (add source only)",
|
||||||
|
example: "16 partials",
|
||||||
|
compile: Param,
|
||||||
|
varargs: true,
|
||||||
|
},
|
||||||
Word {
|
Word {
|
||||||
name: "coarse",
|
name: "coarse",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
|
|||||||
@@ -137,10 +137,10 @@ impl<'a> SampleBrowser<'a> {
|
|||||||
|
|
||||||
let (icon, icon_color) = match entry.kind {
|
let (icon, icon_color) = match entry.kind {
|
||||||
TreeLineKind::Root { expanded: true } | TreeLineKind::Folder { expanded: true } => {
|
TreeLineKind::Root { expanded: true } | TreeLineKind::Folder { expanded: true } => {
|
||||||
("\u{25BC} ", colors.browser.folder_icon)
|
("\u{2212} ", colors.browser.folder_icon)
|
||||||
}
|
}
|
||||||
TreeLineKind::Root { expanded: false }
|
TreeLineKind::Root { expanded: false }
|
||||||
| TreeLineKind::Folder { expanded: false } => ("\u{25B6} ", colors.browser.folder_icon),
|
| TreeLineKind::Folder { expanded: false } => ("+ ", colors.browser.folder_icon),
|
||||||
TreeLineKind::File => ("\u{266A} ", colors.browser.file_icon),
|
TreeLineKind::File => ("\u{266A} ", colors.browser.file_icon),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -339,12 +339,6 @@ impl App {
|
|||||||
}
|
}
|
||||||
AppCommand::ToggleLiveKeysFill => self.live_keys.flip_fill(),
|
AppCommand::ToggleLiveKeysFill => self.live_keys.flip_fill(),
|
||||||
|
|
||||||
// Panel
|
|
||||||
AppCommand::ClosePanel => {
|
|
||||||
self.panel.visible = false;
|
|
||||||
self.panel.focus = crate::state::PanelFocus::Main;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Direct navigation (mouse)
|
// Direct navigation (mouse)
|
||||||
AppCommand::GoToStep(step) => {
|
AppCommand::GoToStep(step) => {
|
||||||
let len = self.current_edit_pattern().length;
|
let len = self.current_edit_pattern().length;
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ use crate::model::{self, Bank, Dictionary, Pattern, Rng, ScriptEngine, Variables
|
|||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
undo::UndoHistory, AudioSettings, EditorContext, LiveKeyState, Metrics, Modal,
|
undo::UndoHistory, AudioSettings, EditorContext, LiveKeyState, Metrics, Modal,
|
||||||
OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav, PlaybackState,
|
OptionsState, PatternField, PatternPropsField, PatternsNav, PlaybackState,
|
||||||
ProjectState, ScriptEditorState, UiState,
|
ProjectState, SampleBrowserState, ScriptEditorState, UiState,
|
||||||
};
|
};
|
||||||
|
|
||||||
static COMPLETION_CANDIDATES: LazyLock<Arc<[CompletionCandidate]>> = LazyLock::new(|| {
|
static COMPLETION_CANDIDATES: LazyLock<Arc<[CompletionCandidate]>> = LazyLock::new(|| {
|
||||||
@@ -66,7 +66,7 @@ pub struct App {
|
|||||||
|
|
||||||
pub audio: AudioSettings,
|
pub audio: AudioSettings,
|
||||||
pub options: OptionsState,
|
pub options: OptionsState,
|
||||||
pub panel: PanelState,
|
pub sample_browser: Option<SampleBrowserState>,
|
||||||
pub midi: MidiState,
|
pub midi: MidiState,
|
||||||
pub plugin_mode: bool,
|
pub plugin_mode: bool,
|
||||||
}
|
}
|
||||||
@@ -123,7 +123,7 @@ impl App {
|
|||||||
AudioSettings::default()
|
AudioSettings::default()
|
||||||
},
|
},
|
||||||
options: OptionsState::default(),
|
options: OptionsState::default(),
|
||||||
panel: PanelState::default(),
|
sample_browser: None,
|
||||||
midi: MidiState::new(),
|
midi: MidiState::new(),
|
||||||
plugin_mode,
|
plugin_mode,
|
||||||
}
|
}
|
||||||
@@ -213,6 +213,9 @@ impl App {
|
|||||||
if self.ui.modal != Modal::None {
|
if self.ui.modal != Modal::None {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if crate::model::onboarding::for_page(self.page).is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let name = self.page.name();
|
let name = self.page.name();
|
||||||
if self.ui.onboarding_dismissed.iter().any(|d| d == name) {
|
if self.ui.onboarding_dismissed.iter().any(|d| d == name) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -232,9 +232,6 @@ pub enum AppCommand {
|
|||||||
// Live keys
|
// Live keys
|
||||||
ToggleLiveKeysFill,
|
ToggleLiveKeysFill,
|
||||||
|
|
||||||
// Panel
|
|
||||||
ClosePanel,
|
|
||||||
|
|
||||||
// Direct navigation (mouse)
|
// Direct navigation (mouse)
|
||||||
GoToStep(usize),
|
GoToStep(usize),
|
||||||
PatternsSelectBank(usize),
|
PatternsSelectBank(usize),
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ use std::sync::atomic::Ordering;
|
|||||||
|
|
||||||
use super::{InputContext, InputResult};
|
use super::{InputContext, InputResult};
|
||||||
use crate::commands::AppCommand;
|
use crate::commands::AppCommand;
|
||||||
|
use crate::page::Page;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
ConfirmAction, CyclicEnum, EuclideanField, Modal, PanelFocus, PatternField, RenameTarget,
|
ConfirmAction, CyclicEnum, EuclideanField, Modal, PatternField, RenameTarget,
|
||||||
SampleBrowserState, SidePanel,
|
SampleBrowserState,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputResult {
|
pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputResult {
|
||||||
@@ -13,15 +14,11 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
|
|||||||
|
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Tab => {
|
KeyCode::Tab => {
|
||||||
if ctx.app.panel.visible {
|
if ctx.app.sample_browser.is_none() {
|
||||||
ctx.app.panel.visible = false;
|
ctx.app.sample_browser =
|
||||||
ctx.app.panel.focus = PanelFocus::Main;
|
Some(SampleBrowserState::new(&ctx.app.audio.config.sample_paths));
|
||||||
} else {
|
|
||||||
let state = SampleBrowserState::new(&ctx.app.audio.config.sample_paths);
|
|
||||||
ctx.app.panel.side = Some(SidePanel::SampleBrowser(state));
|
|
||||||
ctx.app.panel.visible = true;
|
|
||||||
ctx.app.panel.focus = PanelFocus::Side;
|
|
||||||
}
|
}
|
||||||
|
ctx.dispatch(AppCommand::GoToPage(Page::SampleExplorer));
|
||||||
}
|
}
|
||||||
KeyCode::Char('q') if !ctx.app.plugin_mode => {
|
KeyCode::Char('q') if !ctx.app.plugin_mode => {
|
||||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ mod main_page;
|
|||||||
mod modal;
|
mod modal;
|
||||||
mod mouse;
|
mod mouse;
|
||||||
pub(crate) mod options_page;
|
pub(crate) mod options_page;
|
||||||
mod panel;
|
|
||||||
mod patterns_page;
|
mod patterns_page;
|
||||||
|
mod sample_explorer;
|
||||||
mod script_page;
|
mod script_page;
|
||||||
|
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
@@ -22,7 +22,7 @@ use crate::app::App;
|
|||||||
use crate::commands::AppCommand;
|
use crate::commands::AppCommand;
|
||||||
use crate::engine::{AudioCommand, LinkState, SeqCommand, SequencerSnapshot};
|
use crate::engine::{AudioCommand, LinkState, SeqCommand, SequencerSnapshot};
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::state::{MinimapMode, Modal, PanelFocus};
|
use crate::state::{MinimapMode, Modal};
|
||||||
|
|
||||||
pub enum InputResult {
|
pub enum InputResult {
|
||||||
Continue,
|
Continue,
|
||||||
@@ -106,10 +106,6 @@ fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
return InputResult::Continue;
|
return InputResult::Continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.app.panel.visible && ctx.app.panel.focus == PanelFocus::Side {
|
|
||||||
return panel::handle_panel_input(ctx, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
if alt {
|
if alt {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Up => {
|
KeyCode::Up => {
|
||||||
@@ -200,6 +196,7 @@ fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
Page::Help => help_page::handle_help_page(ctx, key),
|
Page::Help => help_page::handle_help_page(ctx, key),
|
||||||
Page::Dict => help_page::handle_dict_page(ctx, key),
|
Page::Dict => help_page::handle_dict_page(ctx, key),
|
||||||
Page::Script => script_page::handle_script_page(ctx, key),
|
Page::Script => script_page::handle_script_page(ctx, key),
|
||||||
|
Page::SampleExplorer => sample_explorer::handle_sample_explorer(ctx, key),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -211,22 +211,16 @@ fn handle_scroll(ctx: &mut InputContext, col: u16, row: u16, term: Rect, up: boo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scroll over side panel area
|
match ctx.app.page {
|
||||||
if ctx.app.panel.visible && ctx.app.panel.side.is_some() {
|
Page::SampleExplorer => {
|
||||||
let (_main_area, side_area) = panel_split(body);
|
if let Some(state) = &mut ctx.app.sample_browser {
|
||||||
if contains(side_area, col, row) {
|
|
||||||
if let Some(crate::state::SidePanel::SampleBrowser(state)) = &mut ctx.app.panel.side {
|
|
||||||
if up {
|
if up {
|
||||||
state.move_up();
|
state.move_up();
|
||||||
} else {
|
} else {
|
||||||
state.move_down();
|
state.move_down();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
match ctx.app.page {
|
|
||||||
Page::Main => {
|
Page::Main => {
|
||||||
if up {
|
if up {
|
||||||
ctx.dispatch(AppCommand::StepUp);
|
ctx.dispatch(AppCommand::StepUp);
|
||||||
@@ -362,6 +356,7 @@ fn handle_footer_click(ctx: &mut InputContext, col: u16, row: u16, footer: Rect)
|
|||||||
Page::Help => " HELP ",
|
Page::Help => " HELP ",
|
||||||
Page::Dict => " DICT ",
|
Page::Dict => " DICT ",
|
||||||
Page::Script => " SCRIPT ",
|
Page::Script => " SCRIPT ",
|
||||||
|
Page::SampleExplorer => " SAMPLES ",
|
||||||
};
|
};
|
||||||
let badge_end = block_inner.x + badge_text.len() as u16;
|
let badge_end = block_inner.x + badge_text.len() as u16;
|
||||||
if col < badge_end {
|
if col < badge_end {
|
||||||
@@ -371,73 +366,20 @@ fn handle_footer_click(ctx: &mut InputContext, col: u16, row: u16, footer: Rect)
|
|||||||
|
|
||||||
// --- Body ---
|
// --- Body ---
|
||||||
|
|
||||||
fn panel_split(body: Rect) -> (Rect, Rect) {
|
|
||||||
if body.width >= 120 {
|
|
||||||
let panel_width = body.width * 35 / 100;
|
|
||||||
let [main, side] =
|
|
||||||
Layout::horizontal([Constraint::Fill(1), Constraint::Length(panel_width)])
|
|
||||||
.areas(body);
|
|
||||||
(main, side)
|
|
||||||
} else {
|
|
||||||
let panel_height = body.height * 40 / 100;
|
|
||||||
let [main, side] =
|
|
||||||
Layout::vertical([Constraint::Fill(1), Constraint::Length(panel_height)])
|
|
||||||
.areas(body);
|
|
||||||
(main, side)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_body_click(ctx: &mut InputContext, col: u16, row: u16, body: Rect, kind: ClickKind) {
|
fn handle_body_click(ctx: &mut InputContext, col: u16, row: u16, body: Rect, kind: ClickKind) {
|
||||||
use crate::state::PanelFocus;
|
if !contains(body, col, row) {
|
||||||
|
|
||||||
if ctx.app.panel.visible && ctx.app.panel.side.is_some() {
|
|
||||||
let (main_area, side_area) = panel_split(body);
|
|
||||||
|
|
||||||
if contains(side_area, col, row) {
|
|
||||||
ctx.app.panel.focus = PanelFocus::Side;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Click on main area: defocus panel
|
|
||||||
if contains(main_area, col, row) {
|
|
||||||
if kind == ClickKind::Double {
|
|
||||||
ctx.dispatch(AppCommand::ClosePanel);
|
|
||||||
} else {
|
|
||||||
ctx.app.panel.focus = PanelFocus::Main;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall through to page-specific handler with main_area
|
|
||||||
if !contains(main_area, col, row) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
match ctx.app.page {
|
match ctx.app.page {
|
||||||
Page::Main => handle_main_click(ctx, col, row, main_area, kind),
|
Page::Main => handle_main_click(ctx, col, row, body, kind),
|
||||||
Page::Patterns => handle_patterns_click(ctx, col, row, main_area, kind),
|
Page::Patterns => handle_patterns_click(ctx, col, row, body, kind),
|
||||||
Page::Help => handle_help_click(ctx, col, row, main_area),
|
Page::Help => handle_help_click(ctx, col, row, body),
|
||||||
Page::Dict => handle_dict_click(ctx, col, row, main_area),
|
Page::Dict => handle_dict_click(ctx, col, row, body),
|
||||||
Page::Options => handle_options_click(ctx, col, row, main_area),
|
Page::Options => handle_options_click(ctx, col, row, body),
|
||||||
Page::Engine => handle_engine_click(ctx, col, row, main_area, kind),
|
Page::Engine => handle_engine_click(ctx, col, row, body, kind),
|
||||||
Page::Script => handle_script_click(ctx, col, row, main_area),
|
Page::Script => handle_script_click(ctx, col, row, body),
|
||||||
}
|
Page::SampleExplorer => {}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let page_area = body;
|
|
||||||
|
|
||||||
if !contains(page_area, col, row) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
match ctx.app.page {
|
|
||||||
Page::Main => handle_main_click(ctx, col, row, page_area, kind),
|
|
||||||
Page::Patterns => handle_patterns_click(ctx, col, row, page_area, kind),
|
|
||||||
Page::Help => handle_help_click(ctx, col, row, page_area),
|
|
||||||
Page::Dict => handle_dict_click(ctx, col, row, page_area),
|
|
||||||
Page::Options => handle_options_click(ctx, col, row, page_area),
|
|
||||||
Page::Engine => handle_engine_click(ctx, col, row, page_area, kind),
|
|
||||||
Page::Script => handle_script_click(ctx, col, row, page_area),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -923,20 +865,7 @@ fn handle_script_editor_drag(ctx: &mut InputContext, col: u16, row: u16, term: R
|
|||||||
if ctx.app.script_editor.mouse_selecting {
|
if ctx.app.script_editor.mouse_selecting {
|
||||||
let padded = padded(term);
|
let padded = padded(term);
|
||||||
let (_header, body, _footer) = top_level_layout(padded);
|
let (_header, body, _footer) = top_level_layout(padded);
|
||||||
let page_area = if ctx.app.panel.visible && ctx.app.panel.side.is_some() {
|
handle_script_editor_mouse(ctx, col, row, body, true);
|
||||||
if body.width >= 120 {
|
|
||||||
let panel_width = body.width * 35 / 100;
|
|
||||||
Layout::horizontal([Constraint::Fill(1), Constraint::Length(panel_width)])
|
|
||||||
.split(body)[0]
|
|
||||||
} else {
|
|
||||||
let panel_height = body.height * 40 / 100;
|
|
||||||
Layout::vertical([Constraint::Fill(1), Constraint::Length(panel_height)])
|
|
||||||
.split(body)[0]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
body
|
|
||||||
};
|
|
||||||
handle_script_editor_mouse(ctx, col, row, page_area, true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
|||||||
use super::{InputContext, InputResult};
|
use super::{InputContext, InputResult};
|
||||||
use crate::commands::AppCommand;
|
use crate::commands::AppCommand;
|
||||||
use crate::engine::AudioCommand;
|
use crate::engine::AudioCommand;
|
||||||
use crate::state::SidePanel;
|
use crate::page::Page;
|
||||||
use cagire_ratatui::TreeLineKind;
|
use cagire_ratatui::TreeLineKind;
|
||||||
|
|
||||||
pub(super) fn handle_panel_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
pub(super) fn handle_sample_explorer(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||||
let state = match &mut ctx.app.panel.side {
|
let state = match &mut ctx.app.sample_browser {
|
||||||
Some(SidePanel::SampleBrowser(s)) => s,
|
Some(s) => s,
|
||||||
None => return InputResult::Continue,
|
None => return InputResult::Continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -86,11 +86,11 @@ pub(super) fn handle_panel_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
|||||||
if state.has_filter() {
|
if state.has_filter() {
|
||||||
state.clear_filter();
|
state.clear_filter();
|
||||||
} else {
|
} else {
|
||||||
ctx.dispatch(AppCommand::ClosePanel);
|
ctx.dispatch(AppCommand::GoToPage(Page::Main));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Tab => {
|
KeyCode::Tab => {
|
||||||
ctx.dispatch(AppCommand::ClosePanel);
|
ctx.dispatch(AppCommand::GoToPage(Page::Main));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ pub fn for_page(page: Page) -> &'static [(&'static str, &'static [(&'static str,
|
|||||||
match page {
|
match page {
|
||||||
Page::Main => &[
|
Page::Main => &[
|
||||||
(
|
(
|
||||||
"The step sequencer grid. Each cell is a Forth script that produces sound when evaluated. During playback, active steps run left-to-right, top-to-bottom. Toggle steps on/off with t to build your pattern. The left panel shows playing patterns, the right side shows VU meters.",
|
"The step sequencer grid. Each cell is a Forth script that produces sound when evaluated. During playback, active steps run left-to-right, top-to-bottom. Toggle steps on/off with t to build your pattern.",
|
||||||
&[
|
&[
|
||||||
("Arrows", "navigate grid"),
|
("Arrows", "navigate grid"),
|
||||||
("Space", "play / stop"),
|
("Space", "play / stop"),
|
||||||
@@ -101,5 +101,6 @@ pub fn for_page(page: Page) -> &'static [(&'static str, &'static [(&'static str,
|
|||||||
("Ctrl+S", "toggle stack preview"),
|
("Ctrl+S", "toggle stack preview"),
|
||||||
],
|
],
|
||||||
)],
|
)],
|
||||||
|
Page::SampleExplorer => &[],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/page.rs
19
src/page.rs
@@ -8,10 +8,11 @@ pub enum Page {
|
|||||||
Dict,
|
Dict,
|
||||||
Options,
|
Options,
|
||||||
Script,
|
Script,
|
||||||
|
SampleExplorer,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Page {
|
impl Page {
|
||||||
/// All pages for iteration (grid pages only — Script excluded)
|
/// All pages for iteration (grid pages only — Script and SampleExplorer excluded)
|
||||||
pub const ALL: &'static [Page] = &[
|
pub const ALL: &'static [Page] = &[
|
||||||
Page::Main,
|
Page::Main,
|
||||||
Page::Patterns,
|
Page::Patterns,
|
||||||
@@ -29,7 +30,7 @@ impl Page {
|
|||||||
/// col 0 col 1 col 2
|
/// col 0 col 1 col 2
|
||||||
/// row 0 Dict Patterns Options
|
/// row 0 Dict Patterns Options
|
||||||
/// row 1 Help Sequencer Engine
|
/// row 1 Help Sequencer Engine
|
||||||
/// Script lives outside the grid at (1, 2)
|
/// Script and SampleExplorer live outside the grid at (1, 2)
|
||||||
pub const fn grid_pos(self) -> (i8, i8) {
|
pub const fn grid_pos(self) -> (i8, i8) {
|
||||||
match self {
|
match self {
|
||||||
Page::Dict => (0, 0),
|
Page::Dict => (0, 0),
|
||||||
@@ -38,7 +39,7 @@ impl Page {
|
|||||||
Page::Main => (1, 1),
|
Page::Main => (1, 1),
|
||||||
Page::Options => (2, 0),
|
Page::Options => (2, 0),
|
||||||
Page::Engine => (2, 1),
|
Page::Engine => (2, 1),
|
||||||
Page::Script => (1, 2),
|
Page::Script | Page::SampleExplorer => (1, 2),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,11 +58,12 @@ impl Page {
|
|||||||
Page::Dict => "Dict",
|
Page::Dict => "Dict",
|
||||||
Page::Options => "Options",
|
Page::Options => "Options",
|
||||||
Page::Script => "Script",
|
Page::Script => "Script",
|
||||||
|
Page::SampleExplorer => "Samples",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn left(&mut self) {
|
pub fn left(&mut self) {
|
||||||
if *self == Page::Script {
|
if matches!(*self, Page::Script | Page::SampleExplorer) {
|
||||||
*self = Page::Help;
|
*self = Page::Help;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -76,7 +78,7 @@ impl Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn right(&mut self) {
|
pub fn right(&mut self) {
|
||||||
if *self == Page::Script {
|
if matches!(*self, Page::Script | Page::SampleExplorer) {
|
||||||
*self = Page::Engine;
|
*self = Page::Engine;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -91,7 +93,7 @@ impl Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn up(&mut self) {
|
pub fn up(&mut self) {
|
||||||
if *self == Page::Script {
|
if matches!(*self, Page::Script | Page::SampleExplorer) {
|
||||||
*self = Page::Main;
|
*self = Page::Main;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -115,13 +117,12 @@ impl Page {
|
|||||||
Page::Engine => Some(14), // "Introduction" (Audio Engine)
|
Page::Engine => Some(14), // "Introduction" (Audio Engine)
|
||||||
Page::Help => Some(0), // "Welcome"
|
Page::Help => Some(0), // "Welcome"
|
||||||
Page::Dict => Some(7), // "About Forth"
|
Page::Dict => Some(7), // "About Forth"
|
||||||
Page::Options => None,
|
Page::Options | Page::Script | Page::SampleExplorer => None,
|
||||||
Page::Script => None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this page appears in the navigation minimap grid.
|
/// Whether this page appears in the navigation minimap grid.
|
||||||
pub const fn visible_in_minimap(self) -> bool {
|
pub const fn visible_in_minimap(self) -> bool {
|
||||||
!matches!(self, Page::Script)
|
!matches!(self, Page::Script | Page::SampleExplorer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ pub mod file_browser;
|
|||||||
pub mod live_keys;
|
pub mod live_keys;
|
||||||
pub mod modal;
|
pub mod modal;
|
||||||
pub mod options;
|
pub mod options;
|
||||||
pub mod panel;
|
|
||||||
pub mod patterns_nav;
|
pub mod patterns_nav;
|
||||||
pub mod playback;
|
pub mod playback;
|
||||||
pub mod project;
|
pub mod project;
|
||||||
@@ -38,7 +37,6 @@ pub use editor::{
|
|||||||
pub use live_keys::LiveKeyState;
|
pub use live_keys::LiveKeyState;
|
||||||
pub use modal::{ConfirmAction, Modal, RenameTarget};
|
pub use modal::{ConfirmAction, Modal, RenameTarget};
|
||||||
pub use options::{OptionsFocus, OptionsState};
|
pub use options::{OptionsFocus, OptionsState};
|
||||||
pub use panel::{PanelFocus, PanelState, SidePanel};
|
|
||||||
pub use patterns_nav::{PatternsColumn, PatternsNav};
|
pub use patterns_nav::{PatternsColumn, PatternsNav};
|
||||||
pub use playback::{PlaybackState, StagedChange, StagedMuteChange, StagedPropChange};
|
pub use playback::{PlaybackState, StagedChange, StagedMuteChange, StagedPropChange};
|
||||||
pub use project::ProjectState;
|
pub use project::ProjectState;
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
use super::sample_browser::SampleBrowserState;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
|
||||||
pub enum PanelFocus {
|
|
||||||
#[default]
|
|
||||||
Main,
|
|
||||||
Side,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum SidePanel {
|
|
||||||
SampleBrowser(SampleBrowserState),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PanelState {
|
|
||||||
pub side: Option<SidePanel>,
|
|
||||||
pub focus: PanelFocus,
|
|
||||||
pub visible: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PanelState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
side: None,
|
|
||||||
focus: PanelFocus::Main,
|
|
||||||
visible: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,7 @@ pub mod main_view;
|
|||||||
pub mod options_view;
|
pub mod options_view;
|
||||||
pub mod patterns_view;
|
pub mod patterns_view;
|
||||||
mod render;
|
mod render;
|
||||||
|
pub mod sample_explorer_view;
|
||||||
pub mod script_view;
|
pub mod script_view;
|
||||||
pub mod title_view;
|
pub mod title_view;
|
||||||
|
|
||||||
|
|||||||
@@ -14,19 +14,18 @@ use crate::engine::{LinkState, SequencerSnapshot};
|
|||||||
use crate::model::{ExecutionTrace, SourceSpan};
|
use crate::model::{ExecutionTrace, SourceSpan};
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
EditorTarget, EuclideanField, FlashKind, Modal, PanelFocus, PatternField, RenameTarget,
|
EditorTarget, EuclideanField, FlashKind, Modal, PatternField, RenameTarget,
|
||||||
SidePanel,
|
|
||||||
};
|
};
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use crate::views::highlight::{self, highlight_line_with_runtime};
|
use crate::views::highlight::{self, highlight_line_with_runtime};
|
||||||
use crate::widgets::{
|
use crate::widgets::{
|
||||||
hint_line, render_props_form, render_search_bar, ConfirmModal, ModalFrame, NavMinimap, NavTile,
|
hint_line, render_props_form, render_search_bar, ConfirmModal, ModalFrame, NavMinimap, NavTile,
|
||||||
SampleBrowser, TextInputModal,
|
TextInputModal,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
dict_view, engine_view, help_view, main_view, options_view, patterns_view, script_view,
|
dict_view, engine_view, help_view, main_view, options_view, patterns_view,
|
||||||
title_view,
|
sample_explorer_view, script_view, title_view,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn clip_span(span: SourceSpan, line_start: usize, line_len: usize) -> Option<SourceSpan> {
|
fn clip_span(span: SourceSpan, line_start: usize, line_len: usize) -> Option<SourceSpan> {
|
||||||
@@ -164,28 +163,15 @@ pub fn render(
|
|||||||
render_header(frame, app, link, snapshot, header_area);
|
render_header(frame, app, link, snapshot, header_area);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (page_area, panel_area) = if app.panel.visible && app.panel.side.is_some() {
|
|
||||||
let panel_width = body_area.width * 35 / 100;
|
|
||||||
let [main, side] =
|
|
||||||
Layout::horizontal([Constraint::Fill(1), Constraint::Length(panel_width)])
|
|
||||||
.areas(body_area);
|
|
||||||
(main, Some(side))
|
|
||||||
} else {
|
|
||||||
(body_area, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
match app.page {
|
match app.page {
|
||||||
Page::Main => main_view::render(frame, app, snapshot, page_area),
|
Page::Main => main_view::render(frame, app, snapshot, body_area),
|
||||||
Page::Patterns => patterns_view::render(frame, app, snapshot, page_area),
|
Page::Patterns => patterns_view::render(frame, app, snapshot, body_area),
|
||||||
Page::Engine => engine_view::render(frame, app, link, page_area),
|
Page::Engine => engine_view::render(frame, app, link, body_area),
|
||||||
Page::Options => options_view::render(frame, app, page_area),
|
Page::Options => options_view::render(frame, app, body_area),
|
||||||
Page::Help => help_view::render(frame, app, page_area),
|
Page::Help => help_view::render(frame, app, body_area),
|
||||||
Page::Dict => dict_view::render(frame, app, page_area),
|
Page::Dict => dict_view::render(frame, app, body_area),
|
||||||
Page::Script => script_view::render(frame, app, snapshot, page_area),
|
Page::Script => script_view::render(frame, app, snapshot, body_area),
|
||||||
}
|
Page::SampleExplorer => sample_explorer_view::render(frame, app, body_area),
|
||||||
|
|
||||||
if let Some(side_area) = panel_area {
|
|
||||||
render_side_panel(frame, app, side_area);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !perf {
|
if !perf {
|
||||||
@@ -292,69 +278,6 @@ fn header_height(_width: u16) -> u16 {
|
|||||||
3
|
3
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_side_panel(frame: &mut Frame, app: &App, area: Rect) {
|
|
||||||
let focused = app.panel.focus == PanelFocus::Side;
|
|
||||||
match &app.panel.side {
|
|
||||||
Some(SidePanel::SampleBrowser(state)) => {
|
|
||||||
let [tree_area, preview_area] =
|
|
||||||
Layout::vertical([Constraint::Fill(1), Constraint::Length(6)]).areas(area);
|
|
||||||
|
|
||||||
// Compute visible height: tree_area minus borders (2), minus search bar (1) if shown
|
|
||||||
let mut vh = tree_area.height.saturating_sub(2) as usize;
|
|
||||||
if state.search_active || !state.search_query.is_empty() {
|
|
||||||
vh = vh.saturating_sub(1);
|
|
||||||
}
|
|
||||||
state.visible_height.set(vh);
|
|
||||||
|
|
||||||
let entries = state.entries();
|
|
||||||
SampleBrowser::new(&entries, state.cursor)
|
|
||||||
.scroll_offset(state.scroll_offset)
|
|
||||||
.search(&state.search_query, state.search_active)
|
|
||||||
.focused(focused)
|
|
||||||
.render(frame, tree_area);
|
|
||||||
|
|
||||||
if let Some(sample) = state
|
|
||||||
.sample_key()
|
|
||||||
.and_then(|key| app.audio.sample_registry.as_ref()?.get(&key))
|
|
||||||
.filter(|s| s.frame_count >= s.total_frames)
|
|
||||||
{
|
|
||||||
use crate::widgets::Waveform;
|
|
||||||
use std::cell::RefCell;
|
|
||||||
thread_local! {
|
|
||||||
static MONO_BUF: RefCell<Vec<f32>> = const { RefCell::new(Vec::new()) };
|
|
||||||
}
|
|
||||||
|
|
||||||
let [wave_area, info_area] =
|
|
||||||
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)])
|
|
||||||
.areas(preview_area);
|
|
||||||
|
|
||||||
MONO_BUF.with(|buf| {
|
|
||||||
let mut buf = buf.borrow_mut();
|
|
||||||
let channels = sample.channels as usize;
|
|
||||||
let frame_count = sample.frame_count as usize;
|
|
||||||
buf.clear();
|
|
||||||
buf.reserve(frame_count);
|
|
||||||
for i in 0..frame_count {
|
|
||||||
buf.push(sample.frames[i * channels]);
|
|
||||||
}
|
|
||||||
frame.render_widget(Waveform::new(&buf), wave_area);
|
|
||||||
});
|
|
||||||
|
|
||||||
let duration = sample.total_frames as f32 / app.audio.config.sample_rate;
|
|
||||||
let ch_label = if sample.channels == 1 {
|
|
||||||
"mono"
|
|
||||||
} else {
|
|
||||||
"stereo"
|
|
||||||
};
|
|
||||||
let info = Paragraph::new(format!(" {duration:.1}s · {ch_label}"))
|
|
||||||
.style(Style::new().fg(theme::get().ui.text_dim));
|
|
||||||
frame.render_widget(info, info_area);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_header(
|
fn render_header(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
app: &App,
|
app: &App,
|
||||||
@@ -527,6 +450,7 @@ fn render_footer(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, are
|
|||||||
Page::Help => " HELP ",
|
Page::Help => " HELP ",
|
||||||
Page::Dict => " DICT ",
|
Page::Dict => " DICT ",
|
||||||
Page::Script => " SCRIPT ",
|
Page::Script => " SCRIPT ",
|
||||||
|
Page::SampleExplorer => " SAMPLES ",
|
||||||
};
|
};
|
||||||
|
|
||||||
let content = if let Some(ref msg) = app.ui.status_message {
|
let content = if let Some(ref msg) = app.ui.status_message {
|
||||||
@@ -549,10 +473,10 @@ fn render_footer(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, are
|
|||||||
])
|
])
|
||||||
} else {
|
} else {
|
||||||
let bindings: Vec<(&str, &str)> = match app.page {
|
let bindings: Vec<(&str, &str)> = match app.page {
|
||||||
Page::Main if app.panel.visible && app.panel.focus == PanelFocus::Side => vec![
|
Page::SampleExplorer => vec![
|
||||||
("↑↓", "Navigate"),
|
("\u{2191}\u{2193}", "Navigate"),
|
||||||
("→", "Expand/Play"),
|
("\u{2192}", "Expand/Play"),
|
||||||
("←", "Collapse"),
|
("\u{2190}", "Collapse"),
|
||||||
("/", "Search"),
|
("/", "Search"),
|
||||||
("Tab", "Close"),
|
("Tab", "Close"),
|
||||||
],
|
],
|
||||||
|
|||||||
87
src/views/sample_explorer_view.rs
Normal file
87
src/views/sample_explorer_view.rs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
use ratatui::layout::{Constraint, Layout, Rect};
|
||||||
|
use ratatui::style::Style;
|
||||||
|
use ratatui::Frame;
|
||||||
|
|
||||||
|
use crate::app::App;
|
||||||
|
use crate::theme;
|
||||||
|
use crate::widgets::{SampleBrowser, Waveform};
|
||||||
|
|
||||||
|
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
|
||||||
|
render_browser(frame, app, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_browser(frame: &mut Frame, app: &App, area: Rect) {
|
||||||
|
let state = match &app.sample_browser {
|
||||||
|
Some(s) => s,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let [tree_area, preview_area] =
|
||||||
|
Layout::vertical([Constraint::Fill(1), Constraint::Length(6)]).areas(area);
|
||||||
|
|
||||||
|
let mut vh = tree_area.height.saturating_sub(2) as usize;
|
||||||
|
if state.search_active || !state.search_query.is_empty() {
|
||||||
|
vh = vh.saturating_sub(1);
|
||||||
|
}
|
||||||
|
state.visible_height.set(vh);
|
||||||
|
|
||||||
|
let entries = state.entries();
|
||||||
|
SampleBrowser::new(&entries, state.cursor)
|
||||||
|
.scroll_offset(state.scroll_offset)
|
||||||
|
.search(&state.search_query, state.search_active)
|
||||||
|
.focused(true)
|
||||||
|
.render(frame, tree_area);
|
||||||
|
|
||||||
|
render_waveform_preview(frame, app, state, preview_area);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_waveform_preview(
|
||||||
|
frame: &mut Frame,
|
||||||
|
app: &App,
|
||||||
|
state: &crate::state::SampleBrowserState,
|
||||||
|
area: Rect,
|
||||||
|
) {
|
||||||
|
use ratatui::text::{Line, Span};
|
||||||
|
use ratatui::widgets::Paragraph;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
let sample = match state
|
||||||
|
.sample_key()
|
||||||
|
.and_then(|key| app.audio.sample_registry.as_ref()?.get(&key))
|
||||||
|
.filter(|s| s.frame_count >= s.total_frames)
|
||||||
|
{
|
||||||
|
Some(s) => s,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static MONO_BUF: RefCell<Vec<f32>> = const { RefCell::new(Vec::new()) };
|
||||||
|
}
|
||||||
|
|
||||||
|
let [wave_area, info_area] =
|
||||||
|
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(area);
|
||||||
|
|
||||||
|
MONO_BUF.with(|buf| {
|
||||||
|
let mut buf = buf.borrow_mut();
|
||||||
|
let channels = sample.channels as usize;
|
||||||
|
let frame_count = sample.frame_count as usize;
|
||||||
|
buf.clear();
|
||||||
|
buf.reserve(frame_count);
|
||||||
|
for i in 0..frame_count {
|
||||||
|
buf.push(sample.frames[i * channels]);
|
||||||
|
}
|
||||||
|
frame.render_widget(Waveform::new(&buf), wave_area);
|
||||||
|
});
|
||||||
|
|
||||||
|
let duration = sample.total_frames as f32 / app.audio.config.sample_rate;
|
||||||
|
let ch_label = if sample.channels == 1 {
|
||||||
|
"mono"
|
||||||
|
} else {
|
||||||
|
"stereo"
|
||||||
|
};
|
||||||
|
let info = Paragraph::new(Line::from(Span::styled(
|
||||||
|
format!(" {duration:.1}s \u{00B7} {ch_label}"),
|
||||||
|
Style::new().fg(theme::get().ui.text_dim),
|
||||||
|
)));
|
||||||
|
frame.render_widget(info, info_area);
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ fn redefine_word_overwrites() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn word_with_param() {
|
fn word_with_param() {
|
||||||
let outputs = expect_outputs(": loud 0.9 gain ; \"kick\" s loud .", 1);
|
let outputs = expect_outputs(": loud 0.9 gain ; \"kick\" snd loud .", 1);
|
||||||
assert!(outputs[0].contains("gain/0.9"));
|
assert!(outputs[0].contains("gain/0.9"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ fn define_word_containing_quotation() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn define_word_with_sound() {
|
fn define_word_with_sound() {
|
||||||
let outputs = expect_outputs(": kick \"kick\" s . ; kick", 1);
|
let outputs = expect_outputs(": kick \"kick\" snd . ; kick", 1);
|
||||||
assert!(outputs[0].contains("sound/kick"));
|
assert!(outputs[0].contains("sound/kick"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ use cagire::forth::Value;
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_midi_channel_set() {
|
fn test_midi_channel_set() {
|
||||||
let outputs = expect_outputs("60 note 100 velocity 3 chan m.", 1);
|
let outputs = expect_outputs("60 note 0.8 velocity 3 chan m.", 1);
|
||||||
assert!(outputs[0].starts_with("/midi/note/60/vel/100/chan/2/dur/"));
|
assert!(outputs[0].starts_with("/midi/note/60/vel/101/chan/2/dur/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_midi_note_default_channel() {
|
fn test_midi_note_default_channel() {
|
||||||
let outputs = expect_outputs("72 note 80 velocity m.", 1);
|
let outputs = expect_outputs("72 note 0.6 velocity m.", 1);
|
||||||
assert!(outputs[0].starts_with("/midi/note/72/vel/80/chan/0/dur/"));
|
assert!(outputs[0].starts_with("/midi/note/72/vel/76/chan/0/dur/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -79,43 +79,43 @@ fn test_ccval_reads_from_cc_memory() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_midi_channel_clamping() {
|
fn test_midi_channel_clamping() {
|
||||||
// Channel should be clamped 1-16, then converted to 0-15 internally
|
// Channel should be clamped 1-16, then converted to 0-15 internally
|
||||||
let outputs = expect_outputs("60 note 100 velocity 0 chan m.", 1);
|
let outputs = expect_outputs("60 note 0.8 velocity 0 chan m.", 1);
|
||||||
assert!(outputs[0].contains("/chan/0")); // 0 clamped to 1, then -1 = 0
|
assert!(outputs[0].contains("/chan/0")); // 0 clamped to 1, then -1 = 0
|
||||||
|
|
||||||
let outputs = expect_outputs("60 note 100 velocity 17 chan m.", 1);
|
let outputs = expect_outputs("60 note 0.8 velocity 17 chan m.", 1);
|
||||||
assert!(outputs[0].contains("/chan/15")); // 17 clamped to 16, then -1 = 15
|
assert!(outputs[0].contains("/chan/15")); // 17 clamped to 16, then -1 = 15
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_midi_note_clamping() {
|
fn test_midi_note_clamping() {
|
||||||
let outputs = expect_outputs("-1 note 100 velocity m.", 1);
|
let outputs = expect_outputs("-1 note 0.8 velocity m.", 1);
|
||||||
assert!(outputs[0].contains("/note/0"));
|
assert!(outputs[0].contains("/note/0"));
|
||||||
|
|
||||||
let outputs = expect_outputs("200 note 100 velocity m.", 1);
|
let outputs = expect_outputs("200 note 0.8 velocity m.", 1);
|
||||||
assert!(outputs[0].contains("/note/127"));
|
assert!(outputs[0].contains("/note/127"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_midi_velocity_clamping() {
|
fn test_midi_velocity_clamping() {
|
||||||
let outputs = expect_outputs("60 note -10 velocity m.", 1);
|
let outputs = expect_outputs("60 note -0.1 velocity m.", 1);
|
||||||
assert!(outputs[0].contains("/vel/0"));
|
assert!(outputs[0].contains("/vel/0"));
|
||||||
|
|
||||||
let outputs = expect_outputs("60 note 200 velocity m.", 1);
|
let outputs = expect_outputs("60 note 2.0 velocity m.", 1);
|
||||||
assert!(outputs[0].contains("/vel/127"));
|
assert!(outputs[0].contains("/vel/127"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_midi_defaults() {
|
fn test_midi_defaults() {
|
||||||
// With only note specified, velocity defaults to 100 and channel to 0
|
// With only note specified, velocity defaults to 0.8 (101) and channel to 0
|
||||||
let outputs = expect_outputs("60 note m.", 1);
|
let outputs = expect_outputs("60 note m.", 1);
|
||||||
assert!(outputs[0].starts_with("/midi/note/60/vel/100/chan/0/dur/"));
|
assert!(outputs[0].starts_with("/midi/note/60/vel/101/chan/0/dur/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_midi_full_defaults() {
|
fn test_midi_full_defaults() {
|
||||||
// With nothing specified, defaults to note=60, velocity=100, channel=0
|
// With nothing specified, defaults to note=60, velocity=0.8 (101), channel=0
|
||||||
let outputs = expect_outputs("m.", 1);
|
let outputs = expect_outputs("m.", 1);
|
||||||
assert!(outputs[0].starts_with("/midi/note/60/vel/100/chan/0/dur/"));
|
assert!(outputs[0].starts_with("/midi/note/60/vel/101/chan/0/dur/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pitch bend tests
|
// Pitch bend tests
|
||||||
@@ -344,10 +344,10 @@ fn test_midi_polyphonic_notes() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_midi_polyphonic_notes_with_velocity() {
|
fn test_midi_polyphonic_notes_with_velocity() {
|
||||||
let outputs = expect_outputs("60 64 67 note 100 80 60 velocity m.", 3);
|
let outputs = expect_outputs("60 64 67 note 0.8 0.6 0.5 velocity m.", 3);
|
||||||
assert!(outputs[0].contains("/note/60/vel/100/"));
|
assert!(outputs[0].contains("/note/60/vel/101/"));
|
||||||
assert!(outputs[1].contains("/note/64/vel/80/"));
|
assert!(outputs[1].contains("/note/64/vel/76/"));
|
||||||
assert!(outputs[2].contains("/note/67/vel/60/"));
|
assert!(outputs[2].contains("/note/67/vel/63/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -59,20 +59,20 @@ fn nested_quotations() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quotation_with_param() {
|
fn quotation_with_param() {
|
||||||
let outputs = expect_outputs(r#""kick" s ( 2 distort ) 1 ? ."#, 1);
|
let outputs = expect_outputs(r#""kick" snd ( 2 distort ) 1 ? ."#, 1);
|
||||||
assert!(outputs[0].contains("distort/2"));
|
assert!(outputs[0].contains("distort/2"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quotation_skips_param() {
|
fn quotation_skips_param() {
|
||||||
let outputs = expect_outputs(r#""kick" s ( 2 distort ) 0 ? ."#, 1);
|
let outputs = expect_outputs(r#""kick" snd ( 2 distort ) 0 ? ."#, 1);
|
||||||
assert!(!outputs[0].contains("distort"));
|
assert!(!outputs[0].contains("distort"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quotation_with_emit() {
|
fn quotation_with_emit() {
|
||||||
// When true, . should fire
|
// When true, . should fire
|
||||||
let outputs = expect_outputs(r#""kick" s ( . ) 1 ?"#, 1);
|
let outputs = expect_outputs(r#""kick" snd ( . ) 1 ?"#, 1);
|
||||||
assert!(outputs[0].contains("kick"));
|
assert!(outputs[0].contains("kick"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ fn quotation_skips_emit() {
|
|||||||
// When false, . should not fire
|
// When false, . should not fire
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f
|
let outputs = f
|
||||||
.evaluate(r#""kick" s ( . ) 0 ?"#, &default_ctx())
|
.evaluate(r#""kick" snd ( . ) 0 ?"#, &default_ctx())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// No output since . was skipped and no implicit emit
|
// No output since . was skipped and no implicit emit
|
||||||
assert_eq!(outputs.len(), 0);
|
assert_eq!(outputs.len(), 0);
|
||||||
@@ -109,7 +109,7 @@ fn every_with_quotation_integration() {
|
|||||||
let ctx = ctx_with(|c| c.iter = iter);
|
let ctx = ctx_with(|c| c.iter = iter);
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f
|
let outputs = f
|
||||||
.evaluate(r#""kick" s ( 2 distort ) 2 every ."#, &ctx)
|
.evaluate(r#""kick" snd ( 2 distort ) 2 every ."#, &ctx)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
if iter % 2 == 0 {
|
if iter % 2 == 0 {
|
||||||
assert!(
|
assert!(
|
||||||
@@ -134,7 +134,7 @@ fn bjork_with_sound() {
|
|||||||
let ctx = ctx_with(|c| c.runs = 2); // position 2 is a hit for (3,8)
|
let ctx = ctx_with(|c| c.runs = 2); // position 2 is a hit for (3,8)
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f
|
let outputs = f
|
||||||
.evaluate(r#""kick" s ( 2 distort ) 3 8 bjork ."#, &ctx)
|
.evaluate(r#""kick" snd ( 2 distort ) 3 8 bjork ."#, &ctx)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(outputs[0].contains("distort/2"));
|
assert!(outputs[0].contains("distort/2"));
|
||||||
}
|
}
|
||||||
@@ -161,7 +161,7 @@ fn when_and_unless_complementary() {
|
|||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f
|
let outputs = f
|
||||||
.evaluate(
|
.evaluate(
|
||||||
r#""kick" s ( 2 distort ) iter 2 mod 0 = ? ( 4 distort ) iter 2 mod 0 = !? ."#,
|
r#""kick" snd ( 2 distort ) iter 2 mod 0 = ? ( 4 distort ) iter 2 mod 0 = !? ."#,
|
||||||
&ctx,
|
&ctx,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ fn basic_emit() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn alias_s() {
|
fn alias_s() {
|
||||||
let outputs = expect_outputs(r#""snare" s ."#, 1);
|
let outputs = expect_outputs(r#""snare" snd ."#, 1);
|
||||||
assert!(outputs[0].contains("sound/snare"));
|
assert!(outputs[0].contains("sound/snare"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn with_params() {
|
fn with_params() {
|
||||||
let outputs = expect_outputs(r#""kick" s 440 freq 0.5 gain ."#, 1);
|
let outputs = expect_outputs(r#""kick" snd 440 freq 0.5 gain ."#, 1);
|
||||||
assert!(outputs[0].contains("sound/kick"));
|
assert!(outputs[0].contains("sound/kick"));
|
||||||
assert!(outputs[0].contains("freq/440"));
|
assert!(outputs[0].contains("freq/440"));
|
||||||
assert!(outputs[0].contains("gain/0.5"));
|
assert!(outputs[0].contains("gain/0.5"));
|
||||||
@@ -22,13 +22,13 @@ fn with_params() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn auto_dur() {
|
fn auto_dur() {
|
||||||
let outputs = expect_outputs(r#""kick" s ."#, 1);
|
let outputs = expect_outputs(r#""kick" snd ."#, 1);
|
||||||
assert!(outputs[0].contains("dur/"));
|
assert!(outputs[0].contains("dur/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn auto_delaytime() {
|
fn auto_delaytime() {
|
||||||
let outputs = expect_outputs(r#""kick" s ."#, 1);
|
let outputs = expect_outputs(r#""kick" snd ."#, 1);
|
||||||
assert!(outputs[0].contains("delaytime/"));
|
assert!(outputs[0].contains("delaytime/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ fn emit_no_sound() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn multiple_emits() {
|
fn multiple_emits() {
|
||||||
let outputs = expect_outputs(r#""kick" s . "snare" s ."#, 2);
|
let outputs = expect_outputs(r#""kick" snd . "snare" snd ."#, 2);
|
||||||
assert!(outputs[0].contains("sound/kick"));
|
assert!(outputs[0].contains("sound/kick"));
|
||||||
assert!(outputs[1].contains("sound/snare"));
|
assert!(outputs[1].contains("sound/snare"));
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ fn multiple_emits() {
|
|||||||
fn envelope_params() {
|
fn envelope_params() {
|
||||||
// Values are tempo-scaled: 0.01 * step_duration(0.125) = 0.00125, etc.
|
// 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" snd 0.01 attack 0.1 decay 0.7 sustain 0.3 release ."#,
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
assert!(outputs[0].contains("attack/0.00125"));
|
assert!(outputs[0].contains("attack/0.00125"));
|
||||||
@@ -59,14 +59,14 @@ fn envelope_params() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn filter_params() {
|
fn filter_params() {
|
||||||
let outputs = expect_outputs(r#""synth" s 2000 lpf 0.5 lpq ."#, 1);
|
let outputs = expect_outputs(r#""synth" snd 2000 lpf 0.5 lpq ."#, 1);
|
||||||
assert!(outputs[0].contains("lpf/2000"));
|
assert!(outputs[0].contains("lpf/2000"));
|
||||||
assert!(outputs[0].contains("lpq/0.5"));
|
assert!(outputs[0].contains("lpq/0.5"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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" snd 0.01 0.1 0.5 0.3 adsr ."#, 1);
|
||||||
assert!(outputs[0].contains("attack/0.00125"));
|
assert!(outputs[0].contains("attack/0.00125"));
|
||||||
assert!(outputs[0].contains("decay/0.0125"));
|
assert!(outputs[0].contains("decay/0.0125"));
|
||||||
assert!(outputs[0].contains("sustain/0.5"));
|
assert!(outputs[0].contains("sustain/0.5"));
|
||||||
@@ -75,7 +75,7 @@ fn adsr_sets_all_envelope_params() {
|
|||||||
|
|
||||||
#[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" snd 0.01 0.1 ad ."#, 1);
|
||||||
assert!(outputs[0].contains("attack/0.00125"));
|
assert!(outputs[0].contains("attack/0.00125"));
|
||||||
assert!(outputs[0].contains("decay/0.0125"));
|
assert!(outputs[0].contains("decay/0.0125"));
|
||||||
assert!(outputs[0].contains("sustain/0"));
|
assert!(outputs[0].contains("sustain/0"));
|
||||||
@@ -83,7 +83,7 @@ fn ad_sets_attack_decay_sustain_zero() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bank_param() {
|
fn bank_param() {
|
||||||
let outputs = expect_outputs(r#""loop" s "a" bank ."#, 1);
|
let outputs = expect_outputs(r#""loop" snd "a" bank ."#, 1);
|
||||||
assert!(outputs[0].contains("sound/loop"));
|
assert!(outputs[0].contains("sound/loop"));
|
||||||
assert!(outputs[0].contains("bank/a"));
|
assert!(outputs[0].contains("bank/a"));
|
||||||
}
|
}
|
||||||
@@ -109,7 +109,7 @@ fn param_only_multiple_params() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn polyphonic_notes() {
|
fn polyphonic_notes() {
|
||||||
let outputs = expect_outputs(r#"60 64 67 note sine s ."#, 3);
|
let outputs = expect_outputs(r#"60 64 67 note sine snd ."#, 3);
|
||||||
assert!(outputs[0].contains("note/60"));
|
assert!(outputs[0].contains("note/60"));
|
||||||
assert!(outputs[1].contains("note/64"));
|
assert!(outputs[1].contains("note/64"));
|
||||||
assert!(outputs[2].contains("note/67"));
|
assert!(outputs[2].contains("note/67"));
|
||||||
@@ -117,14 +117,14 @@ fn polyphonic_notes() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn polyphonic_sounds() {
|
fn polyphonic_sounds() {
|
||||||
let outputs = expect_outputs(r#"440 freq kick hat s ."#, 2);
|
let outputs = expect_outputs(r#"440 freq kick hat snd ."#, 2);
|
||||||
assert!(outputs[0].contains("sound/kick"));
|
assert!(outputs[0].contains("sound/kick"));
|
||||||
assert!(outputs[1].contains("sound/hat"));
|
assert!(outputs[1].contains("sound/hat"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn polyphonic_cycling() {
|
fn polyphonic_cycling() {
|
||||||
let outputs = expect_outputs(r#"60 64 67 note 0.5 1.0 gain sine s ."#, 3);
|
let outputs = expect_outputs(r#"60 64 67 note 0.5 1.0 gain sine snd ."#, 3);
|
||||||
assert!(outputs[0].contains("note/60"));
|
assert!(outputs[0].contains("note/60"));
|
||||||
assert!(outputs[0].contains("gain/0.5"));
|
assert!(outputs[0].contains("gain/0.5"));
|
||||||
assert!(outputs[1].contains("note/64"));
|
assert!(outputs[1].contains("note/64"));
|
||||||
@@ -135,7 +135,7 @@ fn polyphonic_cycling() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn polyphonic_with_at() {
|
fn polyphonic_with_at() {
|
||||||
let outputs = expect_outputs(r#"0 0.5 at 60 64 note sine s ."#, 4);
|
let outputs = expect_outputs(r#"0 0.5 at 60 64 note sine snd ."#, 4);
|
||||||
assert_eq!(outputs.len(), 4);
|
assert_eq!(outputs.len(), 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ fn explicit_dur_zero_is_infinite() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn all_before_sounds() {
|
fn all_before_sounds() {
|
||||||
let outputs = expect_outputs(
|
let outputs = expect_outputs(
|
||||||
r#"500 lpf 0.5 verb all "kick" s 60 note . "hat" s 70 note ."#,
|
r#"500 lpf 0.5 verb all "kick" snd 60 note . "hat" snd 70 note ."#,
|
||||||
2,
|
2,
|
||||||
);
|
);
|
||||||
assert!(outputs[0].contains("sound/kick"));
|
assert!(outputs[0].contains("sound/kick"));
|
||||||
@@ -162,7 +162,7 @@ fn all_before_sounds() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn all_after_sounds() {
|
fn all_after_sounds() {
|
||||||
let outputs = expect_outputs(
|
let outputs = expect_outputs(
|
||||||
r#""kick" s 60 note . "hat" s 70 note . 500 lpf 0.5 verb all"#,
|
r#""kick" snd 60 note . "hat" snd 70 note . 500 lpf 0.5 verb all"#,
|
||||||
2,
|
2,
|
||||||
);
|
);
|
||||||
assert!(outputs[0].contains("sound/kick"));
|
assert!(outputs[0].contains("sound/kick"));
|
||||||
@@ -176,7 +176,7 @@ fn all_after_sounds() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn noall_clears_global_params() {
|
fn noall_clears_global_params() {
|
||||||
let outputs = expect_outputs(
|
let outputs = expect_outputs(
|
||||||
r#"500 lpf all "kick" s 60 note . noall "hat" s 70 note ."#,
|
r#"500 lpf all "kick" snd 60 note . noall "hat" snd 70 note ."#,
|
||||||
2,
|
2,
|
||||||
);
|
);
|
||||||
assert!(outputs[0].contains("lpf/500"));
|
assert!(outputs[0].contains("lpf/500"));
|
||||||
@@ -187,7 +187,7 @@ fn noall_clears_global_params() {
|
|||||||
fn all_with_tempo_scaled_params() {
|
fn all_with_tempo_scaled_params() {
|
||||||
// attack is tempo-scaled: 0.01 * step_duration(0.125) = 0.00125
|
// attack is tempo-scaled: 0.01 * step_duration(0.125) = 0.00125
|
||||||
let outputs = expect_outputs(
|
let outputs = expect_outputs(
|
||||||
r#"0.01 attack all "kick" s 60 note ."#,
|
r#"0.01 attack all "kick" snd 60 note ."#,
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
assert!(outputs[0].contains("attack/0.00125"));
|
assert!(outputs[0].contains("attack/0.00125"));
|
||||||
@@ -196,7 +196,7 @@ fn all_with_tempo_scaled_params() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn all_per_sound_override() {
|
fn all_per_sound_override() {
|
||||||
let outputs = expect_outputs(
|
let outputs = expect_outputs(
|
||||||
r#"500 lpf all "kick" s 2000 lpf . "hat" s ."#,
|
r#"500 lpf all "kick" snd 2000 lpf . "hat" snd ."#,
|
||||||
2,
|
2,
|
||||||
);
|
);
|
||||||
// kick has both global lpf=500 and per-sound lpf=2000; per-sound wins (comes last)
|
// kick has both global lpf=500 and per-sound lpf=2000; per-sound wins (comes last)
|
||||||
@@ -210,7 +210,7 @@ fn all_persists_across_evaluations() {
|
|||||||
let f = forth();
|
let f = forth();
|
||||||
let ctx = default_ctx();
|
let ctx = default_ctx();
|
||||||
f.evaluate(r#"500 lpf 0.5 verb all"#, &ctx).unwrap();
|
f.evaluate(r#"500 lpf 0.5 verb all"#, &ctx).unwrap();
|
||||||
let outputs = f.evaluate(r#""kick" s 60 note ."#, &ctx).unwrap();
|
let outputs = f.evaluate(r#""kick" snd 60 note ."#, &ctx).unwrap();
|
||||||
assert_eq!(outputs.len(), 1);
|
assert_eq!(outputs.len(), 1);
|
||||||
assert!(outputs[0].contains("lpf/500"), "global lpf missing: {}", outputs[0]);
|
assert!(outputs[0].contains("lpf/500"), "global lpf missing: {}", outputs[0]);
|
||||||
assert!(outputs[0].contains("verb/0.5"), "global verb missing: {}", outputs[0]);
|
assert!(outputs[0].contains("verb/0.5"), "global verb missing: {}", outputs[0]);
|
||||||
@@ -222,7 +222,7 @@ fn noall_clears_across_evaluations() {
|
|||||||
let ctx = default_ctx();
|
let ctx = default_ctx();
|
||||||
f.evaluate(r#"500 lpf all"#, &ctx).unwrap();
|
f.evaluate(r#"500 lpf all"#, &ctx).unwrap();
|
||||||
f.evaluate(r#"noall"#, &ctx).unwrap();
|
f.evaluate(r#"noall"#, &ctx).unwrap();
|
||||||
let outputs = f.evaluate(r#""kick" s 60 note ."#, &ctx).unwrap();
|
let outputs = f.evaluate(r#""kick" snd 60 note ."#, &ctx).unwrap();
|
||||||
assert_eq!(outputs.len(), 1);
|
assert_eq!(outputs.len(), 1);
|
||||||
assert!(!outputs[0].contains("lpf"), "lpf should be cleared: {}", outputs[0]);
|
assert!(!outputs[0].contains("lpf"), "lpf should be cleared: {}", outputs[0]);
|
||||||
}
|
}
|
||||||
@@ -251,20 +251,20 @@ fn all_replaces_previous_global() {
|
|||||||
let ctx = default_ctx();
|
let ctx = default_ctx();
|
||||||
f.evaluate(r#"500 lpf 0.5 verb all"#, &ctx).unwrap();
|
f.evaluate(r#"500 lpf 0.5 verb all"#, &ctx).unwrap();
|
||||||
f.evaluate(r#"2000 lpf all"#, &ctx).unwrap();
|
f.evaluate(r#"2000 lpf all"#, &ctx).unwrap();
|
||||||
let outputs = f.evaluate(r#""kick" s ."#, &ctx).unwrap();
|
let outputs = f.evaluate(r#""kick" snd ."#, &ctx).unwrap();
|
||||||
assert_eq!(outputs.len(), 1);
|
assert_eq!(outputs.len(), 1);
|
||||||
assert!(outputs[0].contains("lpf/2000"), "latest lpf should be 2000: {}", outputs[0]);
|
assert!(outputs[0].contains("lpf/2000"), "latest lpf should be 2000: {}", outputs[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn slice_param() {
|
fn slice_param() {
|
||||||
let outputs = expect_outputs(r#""break" s 8 slice ."#, 1);
|
let outputs = expect_outputs(r#""break" snd 8 slice ."#, 1);
|
||||||
assert!(outputs[0].contains("slice/8"));
|
assert!(outputs[0].contains("slice/8"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pick_param() {
|
fn pick_param() {
|
||||||
let outputs = expect_outputs(r#""break" s 8 slice 3 pick ."#, 1);
|
let outputs = expect_outputs(r#""break" snd 8 slice 3 pick ."#, 1);
|
||||||
assert!(outputs[0].contains("slice/8"));
|
assert!(outputs[0].contains("slice/8"));
|
||||||
assert!(outputs[0].contains("pick/3"));
|
assert!(outputs[0].contains("pick/3"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,14 +56,14 @@ fn stepdur_baseline() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn single_emit() {
|
fn single_emit() {
|
||||||
let outputs = expect_outputs(r#""kick" s ."#, 1);
|
let outputs = expect_outputs(r#""kick" snd ."#, 1);
|
||||||
let deltas = get_deltas(&outputs);
|
let deltas = get_deltas(&outputs);
|
||||||
assert!(approx_eq(deltas[0], 0.0), "single emit at start should have delta 0");
|
assert!(approx_eq(deltas[0], 0.0), "single emit at start should have delta 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn multiple_emits_all_at_zero() {
|
fn multiple_emits_all_at_zero() {
|
||||||
let outputs = expect_outputs(r#""kick" s . . . ."#, 4);
|
let outputs = expect_outputs(r#""kick" snd . . . ."#, 4);
|
||||||
let deltas = get_deltas(&outputs);
|
let deltas = get_deltas(&outputs);
|
||||||
for (i, delta) in deltas.iter().enumerate() {
|
for (i, delta) in deltas.iter().enumerate() {
|
||||||
assert!(approx_eq(*delta, 0.0), "emit {}: expected delta 0, got {}", i, delta);
|
assert!(approx_eq(*delta, 0.0), "emit {}: expected delta 0, got {}", i, delta);
|
||||||
@@ -72,7 +72,7 @@ fn multiple_emits_all_at_zero() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sound_persists() {
|
fn sound_persists() {
|
||||||
let outputs = expect_outputs(r#""kick" s . . "hat" s . ."#, 4);
|
let outputs = expect_outputs(r#""kick" snd . . "hat" snd . ."#, 4);
|
||||||
let sounds = get_sounds(&outputs);
|
let sounds = get_sounds(&outputs);
|
||||||
assert_eq!(sounds[0], "kick");
|
assert_eq!(sounds[0], "kick");
|
||||||
assert_eq!(sounds[1], "kick");
|
assert_eq!(sounds[1], "kick");
|
||||||
@@ -82,14 +82,14 @@ fn sound_persists() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn alternating_sounds() {
|
fn alternating_sounds() {
|
||||||
let outputs = expect_outputs(r#""kick" s . "snare" s . "kick" s . "snare" s ."#, 4);
|
let outputs = expect_outputs(r#""kick" snd . "snare" snd . "kick" snd . "snare" snd ."#, 4);
|
||||||
let sounds = get_sounds(&outputs);
|
let sounds = get_sounds(&outputs);
|
||||||
assert_eq!(sounds, vec!["kick", "snare", "kick", "snare"]);
|
assert_eq!(sounds, vec!["kick", "snare", "kick", "snare"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dur_is_step_duration() {
|
fn dur_is_step_duration() {
|
||||||
let outputs = expect_outputs(r#""kick" s ."#, 1);
|
let outputs = expect_outputs(r#""kick" snd ."#, 1);
|
||||||
let durs = get_durs(&outputs);
|
let durs = get_durs(&outputs);
|
||||||
assert!(approx_eq(durs[0], 0.5), "dur should be 4 * step_duration (0.5), got {}", durs[0]);
|
assert!(approx_eq(durs[0], 0.5), "dur should be 4 * step_duration (0.5), got {}", durs[0]);
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ fn cycle_picks_by_runs() {
|
|||||||
for runs in 0..4 {
|
for runs in 0..4 {
|
||||||
let ctx = ctx_with(|c| c.runs = runs);
|
let ctx = ctx_with(|c| c.runs = runs);
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f.evaluate(r#""kick" s ( . ) ( ) 2 cycle"#, &ctx).unwrap();
|
let outputs = f.evaluate(r#""kick" snd ( . ) ( ) 2 cycle"#, &ctx).unwrap();
|
||||||
if runs % 2 == 0 {
|
if runs % 2 == 0 {
|
||||||
assert_eq!(outputs.len(), 1, "runs={}: emit should be picked", runs);
|
assert_eq!(outputs.len(), 1, "runs={}: emit should be picked", runs);
|
||||||
} else {
|
} else {
|
||||||
@@ -113,7 +113,7 @@ fn pcycle_picks_by_iter() {
|
|||||||
for iter in 0..4 {
|
for iter in 0..4 {
|
||||||
let ctx = ctx_with(|c| c.iter = iter);
|
let ctx = ctx_with(|c| c.iter = iter);
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f.evaluate(r#""kick" s ( . ) ( ) 2 pcycle"#, &ctx).unwrap();
|
let outputs = f.evaluate(r#""kick" snd ( . ) ( ) 2 pcycle"#, &ctx).unwrap();
|
||||||
if iter % 2 == 0 {
|
if iter % 2 == 0 {
|
||||||
assert_eq!(outputs.len(), 1, "iter={}: emit should be picked", iter);
|
assert_eq!(outputs.len(), 1, "iter={}: emit should be picked", iter);
|
||||||
} else {
|
} else {
|
||||||
@@ -128,7 +128,7 @@ fn cycle_with_sounds() {
|
|||||||
let ctx = ctx_with(|c| c.runs = runs);
|
let ctx = ctx_with(|c| c.runs = runs);
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f.evaluate(
|
let outputs = f.evaluate(
|
||||||
r#"( "kick" s . ) ( "hat" s . ) ( "snare" s . ) 3 cycle"#,
|
r#"( "kick" snd . ) ( "hat" snd . ) ( "snare" snd . ) 3 cycle"#,
|
||||||
&ctx
|
&ctx
|
||||||
).unwrap();
|
).unwrap();
|
||||||
assert_eq!(outputs.len(), 1, "runs={}: expected 1 output", runs);
|
assert_eq!(outputs.len(), 1, "runs={}: expected 1 output", runs);
|
||||||
@@ -141,7 +141,7 @@ fn cycle_with_sounds() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn at_single_delta() {
|
fn at_single_delta() {
|
||||||
let outputs = expect_outputs(r#"0.5 at "kick" s ."#, 1);
|
let outputs = expect_outputs(r#"0.5 at "kick" snd ."#, 1);
|
||||||
let deltas = get_deltas(&outputs);
|
let deltas = get_deltas(&outputs);
|
||||||
let step_dur = 0.125;
|
let step_dur = 0.125;
|
||||||
assert!(approx_eq(deltas[0], 0.5 * step_dur), "expected delta at 0.5 of step, got {}", deltas[0]);
|
assert!(approx_eq(deltas[0], 0.5 * step_dur), "expected delta at 0.5 of step, got {}", deltas[0]);
|
||||||
@@ -149,7 +149,7 @@ fn at_single_delta() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn at_list_deltas() {
|
fn at_list_deltas() {
|
||||||
let outputs = expect_outputs(r#"0 0.5 at "kick" s ."#, 2);
|
let outputs = expect_outputs(r#"0 0.5 at "kick" snd ."#, 2);
|
||||||
let deltas = get_deltas(&outputs);
|
let deltas = get_deltas(&outputs);
|
||||||
let step_dur = 0.125;
|
let step_dur = 0.125;
|
||||||
assert!(approx_eq(deltas[0], 0.0), "expected delta 0, got {}", deltas[0]);
|
assert!(approx_eq(deltas[0], 0.0), "expected delta 0, got {}", deltas[0]);
|
||||||
@@ -158,7 +158,7 @@ fn at_list_deltas() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn at_three_deltas() {
|
fn at_three_deltas() {
|
||||||
let outputs = expect_outputs(r#"0 0.33 0.67 at "kick" s ."#, 3);
|
let outputs = expect_outputs(r#"0 0.33 0.67 at "kick" snd ."#, 3);
|
||||||
let deltas = get_deltas(&outputs);
|
let deltas = get_deltas(&outputs);
|
||||||
let step_dur = 0.125;
|
let step_dur = 0.125;
|
||||||
assert!(approx_eq(deltas[0], 0.0), "expected delta 0");
|
assert!(approx_eq(deltas[0], 0.0), "expected delta 0");
|
||||||
@@ -168,7 +168,7 @@ fn at_three_deltas() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn at_persists_across_emits() {
|
fn at_persists_across_emits() {
|
||||||
let outputs = expect_outputs(r#"0 0.5 at "kick" s . "hat" s ."#, 4);
|
let outputs = expect_outputs(r#"0 0.5 at "kick" snd . "hat" snd ."#, 4);
|
||||||
let sounds = get_sounds(&outputs);
|
let sounds = get_sounds(&outputs);
|
||||||
assert_eq!(sounds, vec!["kick", "kick", "hat", "hat"]);
|
assert_eq!(sounds, vec!["kick", "kick", "hat", "hat"]);
|
||||||
}
|
}
|
||||||
@@ -176,14 +176,14 @@ fn at_persists_across_emits() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn at_reset_with_zero() {
|
fn at_reset_with_zero() {
|
||||||
let outputs = expect_outputs(r#"0 0.5 at "kick" s . 0.0 at "hat" s ."#, 3);
|
let outputs = expect_outputs(r#"0 0.5 at "kick" snd . 0.0 at "hat" snd ."#, 3);
|
||||||
let sounds = get_sounds(&outputs);
|
let sounds = get_sounds(&outputs);
|
||||||
assert_eq!(sounds, vec!["kick", "kick", "hat"]);
|
assert_eq!(sounds, vec!["kick", "kick", "hat"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clear_resets_at_deltas() {
|
fn clear_resets_at_deltas() {
|
||||||
let outputs = expect_outputs(r#"0 0.5 at "kick" s . clear "hat" s ."#, 3);
|
let outputs = expect_outputs(r#"0 0.5 at "kick" snd . clear "hat" snd ."#, 3);
|
||||||
let sounds = get_sounds(&outputs);
|
let sounds = get_sounds(&outputs);
|
||||||
assert_eq!(sounds, vec!["kick", "kick", "hat"]);
|
assert_eq!(sounds, vec!["kick", "kick", "hat"]);
|
||||||
let deltas = get_deltas(&outputs);
|
let deltas = get_deltas(&outputs);
|
||||||
@@ -196,7 +196,7 @@ fn at_records_selected_spans() {
|
|||||||
|
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let mut trace = ExecutionTrace::default();
|
let mut trace = ExecutionTrace::default();
|
||||||
let script = r#"0 0.5 0.75 at "kick" s ."#;
|
let script = r#"0 0.5 0.75 at "kick" snd ."#;
|
||||||
f.evaluate_with_trace(script, &default_ctx(), &mut trace).unwrap();
|
f.evaluate_with_trace(script, &default_ctx(), &mut trace).unwrap();
|
||||||
|
|
||||||
// Should have 6 selected spans: 3 for at deltas + 3 for sound (one per emit)
|
// Should have 6 selected spans: 3 for at deltas + 3 for sound (one per emit)
|
||||||
@@ -226,7 +226,7 @@ fn get_gains(outputs: &[String]) -> Vec<f64> {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn arp_auto_subdivide() {
|
fn arp_auto_subdivide() {
|
||||||
let outputs = expect_outputs(r#"sine s c4 e4 g4 b4 arp note ."#, 4);
|
let outputs = expect_outputs(r#"sine snd c4 e4 g4 b4 arp note ."#, 4);
|
||||||
let notes = get_notes(&outputs);
|
let notes = get_notes(&outputs);
|
||||||
assert!(approx_eq(notes[0], 60.0));
|
assert!(approx_eq(notes[0], 60.0));
|
||||||
assert!(approx_eq(notes[1], 64.0));
|
assert!(approx_eq(notes[1], 64.0));
|
||||||
@@ -242,7 +242,7 @@ fn arp_auto_subdivide() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn arp_with_explicit_at() {
|
fn arp_with_explicit_at() {
|
||||||
let outputs = expect_outputs(r#"0 0.25 0.5 0.75 at sine s c4 e4 g4 b4 arp note ."#, 4);
|
let outputs = expect_outputs(r#"0 0.25 0.5 0.75 at sine snd c4 e4 g4 b4 arp note ."#, 4);
|
||||||
let notes = get_notes(&outputs);
|
let notes = get_notes(&outputs);
|
||||||
assert!(approx_eq(notes[0], 60.0));
|
assert!(approx_eq(notes[0], 60.0));
|
||||||
assert!(approx_eq(notes[1], 64.0));
|
assert!(approx_eq(notes[1], 64.0));
|
||||||
@@ -258,14 +258,14 @@ fn arp_with_explicit_at() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn arp_single_note() {
|
fn arp_single_note() {
|
||||||
let outputs = expect_outputs(r#"sine s c4 arp note ."#, 1);
|
let outputs = expect_outputs(r#"sine snd c4 arp note ."#, 1);
|
||||||
let notes = get_notes(&outputs);
|
let notes = get_notes(&outputs);
|
||||||
assert!(approx_eq(notes[0], 60.0));
|
assert!(approx_eq(notes[0], 60.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn arp_fewer_deltas_than_notes() {
|
fn arp_fewer_deltas_than_notes() {
|
||||||
let outputs = expect_outputs(r#"0 0.5 at sine s c4 e4 g4 b4 arp note ."#, 4);
|
let outputs = expect_outputs(r#"0 0.5 at sine snd c4 e4 g4 b4 arp note ."#, 4);
|
||||||
let notes = get_notes(&outputs);
|
let notes = get_notes(&outputs);
|
||||||
assert!(approx_eq(notes[0], 60.0));
|
assert!(approx_eq(notes[0], 60.0));
|
||||||
assert!(approx_eq(notes[1], 64.0));
|
assert!(approx_eq(notes[1], 64.0));
|
||||||
@@ -281,7 +281,7 @@ fn arp_fewer_deltas_than_notes() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn arp_fewer_notes_than_deltas() {
|
fn arp_fewer_notes_than_deltas() {
|
||||||
let outputs = expect_outputs(r#"0 0.25 0.5 0.75 at sine s c4 e4 arp note ."#, 4);
|
let outputs = expect_outputs(r#"0 0.25 0.5 0.75 at sine snd c4 e4 arp note ."#, 4);
|
||||||
let notes = get_notes(&outputs);
|
let notes = get_notes(&outputs);
|
||||||
assert!(approx_eq(notes[0], 60.0));
|
assert!(approx_eq(notes[0], 60.0));
|
||||||
assert!(approx_eq(notes[1], 64.0));
|
assert!(approx_eq(notes[1], 64.0));
|
||||||
@@ -291,7 +291,7 @@ fn arp_fewer_notes_than_deltas() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn arp_multiple_params() {
|
fn arp_multiple_params() {
|
||||||
let outputs = expect_outputs(r#"sine s c4 e4 g4 arp note 0.5 0.7 0.9 arp gain ."#, 3);
|
let outputs = expect_outputs(r#"sine snd c4 e4 g4 arp note 0.5 0.7 0.9 arp gain ."#, 3);
|
||||||
let notes = get_notes(&outputs);
|
let notes = get_notes(&outputs);
|
||||||
assert!(approx_eq(notes[0], 60.0));
|
assert!(approx_eq(notes[0], 60.0));
|
||||||
assert!(approx_eq(notes[1], 64.0));
|
assert!(approx_eq(notes[1], 64.0));
|
||||||
@@ -305,7 +305,7 @@ fn arp_multiple_params() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn arp_no_arp_unchanged() {
|
fn arp_no_arp_unchanged() {
|
||||||
// Standard CycleList without arp → cross-product (backward compat)
|
// Standard CycleList without arp → cross-product (backward compat)
|
||||||
let outputs = expect_outputs(r#"0 0.5 at sine s c4 e4 note ."#, 4);
|
let outputs = expect_outputs(r#"0 0.5 at sine snd c4 e4 note ."#, 4);
|
||||||
let notes = get_notes(&outputs);
|
let notes = get_notes(&outputs);
|
||||||
// Cross-product: each note at each delta
|
// Cross-product: each note at each delta
|
||||||
assert!(approx_eq(notes[0], 60.0));
|
assert!(approx_eq(notes[0], 60.0));
|
||||||
@@ -318,7 +318,7 @@ fn arp_no_arp_unchanged() {
|
|||||||
fn arp_mixed_cycle_and_arp() {
|
fn arp_mixed_cycle_and_arp() {
|
||||||
// CycleList sound (2) + ArpList note (3) → 3 arp × 2 poly = 6 voices
|
// CycleList sound (2) + ArpList note (3) → 3 arp × 2 poly = 6 voices
|
||||||
// Each arp step plays both sine and saw simultaneously (poly stacking)
|
// Each arp step plays both sine and saw simultaneously (poly stacking)
|
||||||
let outputs = expect_outputs(r#"sine saw s c4 e4 g4 arp note ."#, 6);
|
let outputs = expect_outputs(r#"sine saw snd c4 e4 g4 arp note ."#, 6);
|
||||||
let sounds = get_sounds(&outputs);
|
let sounds = get_sounds(&outputs);
|
||||||
// Arp step 0: poly 0=sine, poly 1=saw
|
// Arp step 0: poly 0=sine, poly 1=saw
|
||||||
assert_eq!(sounds[0], "sine");
|
assert_eq!(sounds[0], "sine");
|
||||||
@@ -346,7 +346,7 @@ fn every_offset_fires_at_offset() {
|
|||||||
for iter in 0..8 {
|
for iter in 0..8 {
|
||||||
let ctx = ctx_with(|c| c.iter = iter);
|
let ctx = ctx_with(|c| c.iter = iter);
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f.evaluate(r#""kick" s ( . ) 4 2 every+"#, &ctx).unwrap();
|
let outputs = f.evaluate(r#""kick" snd ( . ) 4 2 every+"#, &ctx).unwrap();
|
||||||
if iter % 4 == 2 {
|
if iter % 4 == 2 {
|
||||||
assert_eq!(outputs.len(), 1, "iter={}: should fire", iter);
|
assert_eq!(outputs.len(), 1, "iter={}: should fire", iter);
|
||||||
} else {
|
} else {
|
||||||
@@ -361,7 +361,7 @@ fn every_offset_wraps_large_offset() {
|
|||||||
for iter in 0..8 {
|
for iter in 0..8 {
|
||||||
let ctx = ctx_with(|c| c.iter = iter);
|
let ctx = ctx_with(|c| c.iter = iter);
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f.evaluate(r#""kick" s ( . ) 4 6 every+"#, &ctx).unwrap();
|
let outputs = f.evaluate(r#""kick" snd ( . ) 4 6 every+"#, &ctx).unwrap();
|
||||||
if iter % 4 == 2 {
|
if iter % 4 == 2 {
|
||||||
assert_eq!(outputs.len(), 1, "iter={}: should fire (wrapped offset)", iter);
|
assert_eq!(outputs.len(), 1, "iter={}: should fire (wrapped offset)", iter);
|
||||||
} else {
|
} else {
|
||||||
@@ -375,7 +375,7 @@ fn except_offset_inverse() {
|
|||||||
for iter in 0..8 {
|
for iter in 0..8 {
|
||||||
let ctx = ctx_with(|c| c.iter = iter);
|
let ctx = ctx_with(|c| c.iter = iter);
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f.evaluate(r#""kick" s ( . ) 4 2 except+"#, &ctx).unwrap();
|
let outputs = f.evaluate(r#""kick" snd ( . ) 4 2 except+"#, &ctx).unwrap();
|
||||||
if iter % 4 != 2 {
|
if iter % 4 != 2 {
|
||||||
assert_eq!(outputs.len(), 1, "iter={}: should fire", iter);
|
assert_eq!(outputs.len(), 1, "iter={}: should fire", iter);
|
||||||
} else {
|
} else {
|
||||||
@@ -389,8 +389,8 @@ fn every_offset_zero_is_same_as_every() {
|
|||||||
for iter in 0..8 {
|
for iter in 0..8 {
|
||||||
let ctx = ctx_with(|c| c.iter = iter);
|
let ctx = ctx_with(|c| c.iter = iter);
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let a = f.evaluate(r#""kick" s ( . ) 3 every"#, &ctx).unwrap();
|
let a = f.evaluate(r#""kick" snd ( . ) 3 every"#, &ctx).unwrap();
|
||||||
let b = f.evaluate(r#""kick" s ( . ) 3 0 every+"#, &ctx).unwrap();
|
let b = f.evaluate(r#""kick" snd ( . ) 3 0 every+"#, &ctx).unwrap();
|
||||||
assert_eq!(a.len(), b.len(), "iter={}: every and every+ 0 should match", iter);
|
assert_eq!(a.len(), b.len(), "iter={}: every and every+ 0 should match", iter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user