Fixes
This commit is contained in:
@@ -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,8 +28,8 @@ 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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user