diff --git a/microwave/src/app/input/mod.rs b/microwave/src/app/input/mod.rs index 18f59e60..39abcf16 100644 --- a/microwave/src/app/input/mod.rs +++ b/microwave/src/app/input/mod.rs @@ -1,3 +1,5 @@ +mod hex_layout; + use std::collections::HashSet; use bevy::{ @@ -12,16 +14,15 @@ use bevy::{ use tune::pitch::{Pitch, Ratio}; use crate::{ - app::model::{PianoEngineResource, ViewModel}, + app::{ + resources::{HudStackResource, MainViewResource, PianoEngineResource}, + VirtualKeyboardResource, + }, control::LiveParameter, piano::{Event, Location, PianoEngine, SourceId}, PhysicalKeyboardLayout, }; -use super::{Toggle, VirtualKeyboardLayout}; - -mod hex_layout; - pub struct InputPlugin; impl Plugin for InputPlugin { @@ -32,9 +33,10 @@ impl Plugin for InputPlugin { fn handle_input_event( engine: Res, + mut hud_stack: ResMut, physical_layout: Res, - mut virtual_layout: ResMut>, - mut view_model: ResMut, + mut virtual_keyboard: ResMut, + mut main_view: ResMut, windows: Query<&Window>, key_code: Res>, mut keyboard_inputs: EventReader, @@ -60,7 +62,7 @@ fn handle_input_event( handle_scan_code_event( &engine.0, &physical_layout, - virtual_layout.curr_option(), + &virtual_keyboard, mod_pressed, keyboard_input.key_code, keyboard_input.state, @@ -70,8 +72,8 @@ fn handle_input_event( if keyboard_input.state.is_pressed() { handle_key_event( &engine.0, - &mut virtual_layout, - &mut view_model, + &mut hud_stack, + &mut virtual_keyboard, &keyboard_input.logical_key, alt_pressed, ); @@ -79,26 +81,26 @@ fn handle_input_event( } for mouse_button_input in mouse_button_inputs.read() { - handle_mouse_button_event(&engine.0, window, &view_model, *mouse_button_input); + handle_mouse_button_event(&engine.0, window, &main_view, *mouse_button_input); } if !mouse_motions.is_empty() { - handle_mouse_motion_event(&engine.0, window, &view_model); + handle_mouse_motion_event(&engine.0, window, &main_view); } for mouse_wheel in mouse_wheels.read() { - handle_mouse_wheel_event(alt_pressed, &mut view_model, *mouse_wheel); + handle_mouse_wheel_event(alt_pressed, &mut main_view, *mouse_wheel); } for touch_input in touch_inputs.read() { - handle_touch_event(&engine.0, window, &view_model, *touch_input); + handle_touch_event(&engine.0, window, &main_view, *touch_input); } } fn handle_scan_code_event( engine: &PianoEngine, physical_layout: &PhysicalKeyboardLayout, - virtual_layout: &VirtualKeyboardLayout, + virtual_keyboard: &VirtualKeyboardResource, mod_pressed: bool, key_code: KeyCode, button_state: ButtonState, @@ -109,7 +111,7 @@ fn handle_scan_code_event( if let Some(key_coord) = hex_layout::location_of_key(physical_layout, key_code) { let (x, y) = key_coord; - let degree = virtual_layout.keyboard.get_key(x.into(), y.into()); + let degree = virtual_keyboard.get_key(x.into(), y.into()); let event = match button_state { ButtonState::Pressed => { @@ -122,40 +124,61 @@ fn handle_scan_code_event( } } +pub enum HudMode { + Keyboard, +} + fn handle_key_event( engine: &PianoEngine, - virtual_layout: &mut ResMut>, - view_settings: &mut ResMut, + hud_stack: &mut ResMut, + virtual_keyboard: &mut ResMut, logical_key: &Key, alt_pressed: bool, ) { - match logical_key { - Key::Character(character) => match character.to_uppercase().as_str() { - "E" if alt_pressed => engine.toggle_envelope_type(), - "K" if alt_pressed => view_settings.on_screen_keyboards.toggle_next(), - "L" if alt_pressed => engine.toggle_parameter(LiveParameter::Legato), - "O" if alt_pressed => engine.toggle_synth_mode(), - "T" if alt_pressed => engine.toggle_tuning_mode(), - "Y" if alt_pressed => virtual_layout.toggle_next(), - _ => {} - }, - Key::F1 => engine.toggle_parameter(LiveParameter::Sound1), - Key::F2 => engine.toggle_parameter(LiveParameter::Sound2), - Key::F3 => engine.toggle_parameter(LiveParameter::Sound3), - Key::F4 => engine.toggle_parameter(LiveParameter::Sound4), - Key::F5 => engine.toggle_parameter(LiveParameter::Sound5), - Key::F6 => engine.toggle_parameter(LiveParameter::Sound6), - Key::F7 => engine.toggle_parameter(LiveParameter::Sound7), - Key::F8 => engine.toggle_parameter(LiveParameter::Sound8), - Key::F9 => engine.toggle_parameter(LiveParameter::Sound9), - Key::F10 => engine.toggle_parameter(LiveParameter::Sound10), - Key::Space => engine.toggle_parameter(LiveParameter::Foot), - Key::ArrowUp if !alt_pressed => engine.dec_program(), - Key::ArrowDown if !alt_pressed => engine.inc_program(), - Key::ArrowLeft if alt_pressed => engine.change_ref_note_by(-1), - Key::ArrowRight if alt_pressed => engine.change_ref_note_by(1), - Key::ArrowLeft if !alt_pressed => engine.change_root_offset_by(-1), - Key::ArrowRight if !alt_pressed => engine.change_root_offset_by(1), + match (logical_key, alt_pressed) { + (Key::F1, false) => engine.toggle_parameter(LiveParameter::Sound1), + (Key::F2, false) => engine.toggle_parameter(LiveParameter::Sound2), + (Key::F3, false) => engine.toggle_parameter(LiveParameter::Sound3), + (Key::F4, false) => engine.toggle_parameter(LiveParameter::Sound4), + (Key::F5, false) => engine.toggle_parameter(LiveParameter::Sound5), + (Key::F6, false) => engine.toggle_parameter(LiveParameter::Sound6), + (Key::F7, false) => engine.toggle_parameter(LiveParameter::Sound7), + (Key::F8, false) => engine.toggle_parameter(LiveParameter::Sound8), + (Key::F9, false) => engine.toggle_parameter(LiveParameter::Sound9), + (Key::F10, false) => engine.toggle_parameter(LiveParameter::Sound10), + (Key::Space, false) => engine.toggle_parameter(LiveParameter::Foot), + (Key::ArrowUp, true) => engine.dec_backend(), + (Key::ArrowDown, true) => engine.inc_backend(), + (Key::ArrowUp, false) => engine.dec_program(), + (Key::ArrowDown, false) => engine.inc_program(), + (Key::ArrowLeft, true) => engine.change_ref_note_by(-1), + (Key::ArrowRight, true) => engine.change_ref_note_by(1), + (Key::ArrowLeft, false) => engine.change_root_offset_by(-1), + (Key::ArrowRight, false) => engine.change_root_offset_by(1), + (Key::Character(character), true) => { + let character = &character.to_uppercase(); + match hud_stack.top() { + None => match &**character { + "E" if alt_pressed => engine.toggle_envelope_type(), + "K" => hud_stack.push(HudMode::Keyboard), + "L" => engine.toggle_parameter(LiveParameter::Legato), + "T" => engine.toggle_tuning_mode(), + _ => {} + }, + Some(HudMode::Keyboard) => { + match &**character { + "C" => virtual_keyboard.compression.toggle_next(), + "K" => virtual_keyboard.on_screen_keyboard.toggle_next(), + "L" => virtual_keyboard.layout.toggle_next(), + "S" => virtual_keyboard.scale.toggle_next(), + _ => {} + }; + } + } + } + (Key::Escape, false) => { + hud_stack.pop(); + } _ => {} } } @@ -163,7 +186,7 @@ fn handle_key_event( fn handle_mouse_button_event( engine: &PianoEngine, window: &Window, - view_model: &ViewModel, + main_view: &MainViewResource, mouse_button_input: MouseButtonInput, ) { if mouse_button_input.button == MouseButton::Left { @@ -173,7 +196,7 @@ fn handle_mouse_button_event( handle_position_event( engine, window, - view_model, + main_view, cursor_position, SourceId::Mouse, |location| Event::Pressed(SourceId::Mouse, location, 100), @@ -185,12 +208,12 @@ fn handle_mouse_button_event( } } -fn handle_mouse_motion_event(engine: &PianoEngine, window: &Window, view_model: &ViewModel) { +fn handle_mouse_motion_event(engine: &PianoEngine, window: &Window, main_view: &MainViewResource) { if let Some(cursor_position) = window.cursor_position() { handle_position_event( engine, window, - view_model, + main_view, cursor_position, SourceId::Mouse, |location| Event::Moved(SourceId::Mouse, location), @@ -200,7 +223,7 @@ fn handle_mouse_motion_event(engine: &PianoEngine, window: &Window, view_model: fn handle_mouse_wheel_event( alt_pressed: bool, - view_model: &mut ResMut, + main_view: &mut ResMut, mouse_wheel: MouseWheel, ) { let unit_factor = match mouse_wheel.unit { @@ -216,16 +239,16 @@ fn handle_mouse_wheel_event( } if x_delta.abs() > y_delta.abs() { - let shift_ratio = view_model.pitch_range().repeated(-x_delta / 500.0); - view_model.viewport_left = view_model.viewport_left * shift_ratio; - view_model.viewport_right = view_model.viewport_right * shift_ratio; + let shift_ratio = main_view.pitch_range().repeated(-x_delta / 500.0); + main_view.viewport_left = main_view.viewport_left * shift_ratio; + main_view.viewport_right = main_view.viewport_right * shift_ratio; } else { let zoom_ratio = Ratio::from_semitones(y_delta / 10.0); - view_model.viewport_left = view_model.viewport_left * zoom_ratio; - view_model.viewport_right = view_model.viewport_right / zoom_ratio; + main_view.viewport_left = main_view.viewport_left * zoom_ratio; + main_view.viewport_right = main_view.viewport_right / zoom_ratio; } - let mut target_pitch_range = view_model.pitch_range(); + let mut target_pitch_range = main_view.pitch_range(); let min_pitch = Pitch::from_hz(20.0); let max_pitch = Pitch::from_hz(20000.0); @@ -236,41 +259,41 @@ fn handle_mouse_wheel_event( let x = target_pitch_range .stretched_by(min_allowed_pitch_range.inv()) .divided_into_equal_steps(2.0); - view_model.viewport_left = view_model.viewport_left * x; - view_model.viewport_right = view_model.viewport_right / x; + main_view.viewport_left = main_view.viewport_left * x; + main_view.viewport_right = main_view.viewport_right / x; } if target_pitch_range > max_allowed_pitch_range { target_pitch_range = max_allowed_pitch_range; } - if view_model.viewport_left < min_pitch { - view_model.viewport_left = min_pitch; - view_model.viewport_right = min_pitch * target_pitch_range; + if main_view.viewport_left < min_pitch { + main_view.viewport_left = min_pitch; + main_view.viewport_right = min_pitch * target_pitch_range; } - if view_model.viewport_right > max_pitch { - view_model.viewport_left = max_pitch / target_pitch_range; - view_model.viewport_right = max_pitch; + if main_view.viewport_right > max_pitch { + main_view.viewport_left = max_pitch / target_pitch_range; + main_view.viewport_right = max_pitch; } } fn handle_touch_event( engine: &PianoEngine, window: &Window, - view_model: &ViewModel, + main_view: &MainViewResource, event: TouchInput, ) { let id = SourceId::Touchpad(event.id); match event.phase { TouchPhase::Started => { - handle_position_event(engine, window, view_model, event.position, id, |location| { + handle_position_event(engine, window, main_view, event.position, id, |location| { Event::Pressed(id, location, 100) }) } TouchPhase::Moved => { - handle_position_event(engine, window, view_model, event.position, id, |location| { + handle_position_event(engine, window, main_view, event.position, id, |location| { Event::Moved(id, location) }); } @@ -281,7 +304,7 @@ fn handle_touch_event( fn handle_position_event( engine: &PianoEngine, window: &Window, - view_model: &ViewModel, + main_view: &MainViewResource, position: Vec2, id: SourceId, to_event: impl Fn(Location) -> Event, @@ -289,15 +312,15 @@ fn handle_position_event( let x_normalized = f64::from(position.x / window.width()); let y_normalized = 1.0 - f64::from(position.y / window.height()).max(0.0).min(1.0); - let keyboard_range = view_model.pitch_range(); - let pitch = view_model.viewport_left * keyboard_range.repeated(x_normalized); + let keyboard_range = main_view.pitch_range(); + let pitch = main_view.viewport_left * keyboard_range.repeated(x_normalized); engine.handle_event(to_event(Location::Pitch(pitch))); match id { SourceId::Mouse => engine.set_parameter(LiveParameter::Breath, y_normalized), - SourceId::Touchpad(_) => { + SourceId::Touchpad(..) => { engine.set_key_pressure(id, y_normalized); } - SourceId::Keyboard(_, _) | SourceId::Midi(_) => unreachable!(), + SourceId::Keyboard(..) | SourceId::Midi(..) => unreachable!(), } } diff --git a/microwave/src/app/mod.rs b/microwave/src/app/mod.rs index 74ca344f..b115f82b 100644 --- a/microwave/src/app/mod.rs +++ b/microwave/src/app/mod.rs @@ -1,35 +1,31 @@ -use std::{any::Any, fmt, sync::Arc}; +mod input; +mod resources; +mod view; + +use std::{any::Any, fmt, slice, sync::Arc}; use bevy::{prelude::*, window::PresentMode}; use clap::ValueEnum; use flume::Receiver; -use tune::{ - layout::IsomorphicKeyboard, - note::NoteLetter, - pitch::{Pitched, Ratio}, - scala::Scl, -}; - -use crate::piano::{PianoEngine, PianoEngineState}; +use input::InputPlugin; +use tune::{note::NoteLetter, pitch::Pitched, scala::Scl}; +use view::ViewPlugin; -use self::{ - input::InputPlugin, - model::{ - BackendInfoResource, OnScreenKeyboards, PianoEngineResource, PianoEngineStateResource, - ViewModel, +use crate::{ + app::resources::{ + BackendInfoResource, HudStackResource, MainViewResource, PianoEngineResource, + PianoEngineStateResource, }, - view::ViewPlugin, + piano::{PianoEngine, PianoEngineState}, }; -mod input; -mod model; -mod view; +pub use resources::virtual_keyboard::VirtualKeyboardResource; pub fn start( engine: Arc, engine_state: PianoEngineState, physical_layout: PhysicalKeyboardLayout, - virtual_layouts: Vec, + virtual_keyboard: VirtualKeyboardResource, odd_limit: u16, info_updates: Receiver, resources: Vec>, @@ -51,22 +47,14 @@ pub fn start( ViewPlugin, )) .insert_resource(physical_layout) - .insert_resource(Toggle::from(virtual_layouts)) + .insert_resource(virtual_keyboard) .insert_resource(PianoEngineResource(engine)) .insert_resource(PianoEngineStateResource(engine_state)) .insert_resource(BackendInfoResource(info_updates)) - .insert_resource(ViewModel { + .insert_resource(HudStackResource::default()) + .insert_resource(MainViewResource { viewport_left: NoteLetter::Fsh.in_octave(2).pitch(), viewport_right: NoteLetter::Ash.in_octave(5).pitch(), - on_screen_keyboards: vec![ - OnScreenKeyboards::Isomorphic, - OnScreenKeyboards::Scale, - OnScreenKeyboards::Reference, - OnScreenKeyboards::IsomorphicAndReference, - OnScreenKeyboards::ScaleAndReference, - OnScreenKeyboards::None, - ] - .into(), reference_scl: Scl::builder().push_cents(100.0).build().unwrap(), odd_limit, }) @@ -84,15 +72,6 @@ pub enum PhysicalKeyboardLayout { Iso, } -pub struct VirtualKeyboardLayout { - pub description: String, - pub keyboard: IsomorphicKeyboard, - pub num_primary_steps: u16, - pub num_secondary_steps: u16, - pub period: Ratio, - pub colors: Vec, -} - #[derive(Resource)] pub struct DynBackendInfo(pub Box); @@ -105,16 +84,42 @@ pub trait BackendInfo: Sync + Send + 'static { #[derive(Resource)] pub struct Toggle { options: Vec, - curr_option: usize, + curr_index: usize, } impl Toggle { + pub fn curr_index(&self) -> usize { + self.curr_index + } + pub fn toggle_next(&mut self) { - self.curr_option = (self.curr_option + 1) % self.options.len(); + self.curr_index = (self.curr_index + 1) % self.options.len(); + } + + pub fn inc(&mut self) { + self.curr_index = (self.curr_index.saturating_add(1)).min(self.options.len() - 1); + } + + pub fn dec(&mut self) { + self.curr_index = self.curr_index.saturating_sub(1); } pub fn curr_option(&self) -> &T { - &self.options[self.curr_option] + &self.options[self.curr_index] + } + + pub fn curr_option_mut(&mut self) -> &mut T { + &mut self.options[self.curr_index] + } +} + +impl<'a, T> IntoIterator for &'a mut Toggle { + type Item = &'a mut T; + + type IntoIter = slice::IterMut<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + self.options.iter_mut() } } @@ -122,7 +127,7 @@ impl From> for Toggle { fn from(options: Vec) -> Self { Toggle { options, - curr_option: 0, + curr_index: 0, } } } diff --git a/microwave/src/app/model.rs b/microwave/src/app/resources/mod.rs similarity index 62% rename from microwave/src/app/model.rs rename to microwave/src/app/resources/mod.rs index b10ea667..efbbc64a 100644 --- a/microwave/src/app/model.rs +++ b/microwave/src/app/resources/mod.rs @@ -1,3 +1,5 @@ +pub mod virtual_keyboard; + use std::sync::Arc; use bevy::prelude::Resource; @@ -7,9 +9,10 @@ use tune::{ scala::Scl, }; -use crate::piano::{PianoEngine, PianoEngineState}; - -use super::{DynBackendInfo, Toggle}; +use crate::{ + app::{input::HudMode, DynBackendInfo}, + piano::{PianoEngine, PianoEngineState}, +}; #[derive(Resource)] pub struct PianoEngineResource(pub Arc); @@ -21,25 +24,14 @@ pub struct PianoEngineStateResource(pub PianoEngineState); pub struct BackendInfoResource(pub Receiver); #[derive(Resource)] -pub struct ViewModel { +pub struct MainViewResource { pub viewport_left: Pitch, pub viewport_right: Pitch, - pub on_screen_keyboards: Toggle, pub reference_scl: Scl, pub odd_limit: u16, } -#[derive(Clone, Copy)] -pub enum OnScreenKeyboards { - Isomorphic, - Scale, - Reference, - IsomorphicAndReference, - ScaleAndReference, - None, -} - -impl ViewModel { +impl MainViewResource { pub fn pitch_range(&self) -> Ratio { Ratio::between_pitches(self.viewport_left, self.viewport_right) } @@ -50,3 +42,20 @@ impl ViewModel { - 0.5 } } + +#[derive(Default, Resource)] +pub struct HudStackResource(Vec); + +impl HudStackResource { + pub fn push(&mut self, mode: HudMode) { + self.0.push(mode); + } + + pub fn pop(&mut self) -> Option { + self.0.pop() + } + + pub fn top(&self) -> Option<&HudMode> { + self.0.last() + } +} diff --git a/microwave/src/app/resources/virtual_keyboard.rs b/microwave/src/app/resources/virtual_keyboard.rs new file mode 100644 index 00000000..197afbff --- /dev/null +++ b/microwave/src/app/resources/virtual_keyboard.rs @@ -0,0 +1,280 @@ +use std::{ + cmp::Ordering, + fmt::{self, Display}, + sync::Arc, +}; + +use bevy::prelude::*; +use tune::{ + layout::{IsomorphicKeyboard, IsomorphicLayout}, + pitch::Ratio, + scala::Scl, +}; + +use crate::{app::Toggle, CustomKeyboardOptions}; + +#[derive(Resource)] +pub struct VirtualKeyboardResource { + pub on_screen_keyboard: Toggle, + pub scale: Toggle, + pub layout: Toggle>>, + pub compression: Toggle, + avg_step_size: Ratio, +} + +#[derive(Clone, Copy)] +pub enum OnScreenKeyboards { + Isomorphic, + Scale, + Reference, + IsomorphicAndReference, + ScaleAndReference, + None, +} + +impl Display for OnScreenKeyboards { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Isomorphic => write!(f, "Isomorphic"), + Self::Scale => write!(f, "Scale"), + Self::Reference => write!(f, "Reference"), + Self::IsomorphicAndReference => write!(f, "Isomorphic + Reference"), + Self::ScaleAndReference => write!(f, "Scale + Reference"), + Self::None => write!(f, "OFF"), + } + } +} + +pub struct VirtualKeyboardScale { + layout: Arc, + colors: Vec, +} + +pub struct VirtualKeyboardLayout { + scale_name: String, + keyboard: IsomorphicKeyboard, + orig_keyboard: IsomorphicKeyboard, + steps: (u16, u16), +} + +#[derive(Debug)] +pub enum Compression { + None, + Compressed, + Expanded, +} + +impl VirtualKeyboardResource { + pub fn new(scl: &Scl, options: CustomKeyboardOptions) -> VirtualKeyboardResource { + let on_screen_keyboards = vec![ + OnScreenKeyboards::Isomorphic, + OnScreenKeyboards::Scale, + OnScreenKeyboards::Reference, + OnScreenKeyboards::IsomorphicAndReference, + OnScreenKeyboards::ScaleAndReference, + OnScreenKeyboards::None, + ]; + + let avg_step_size = if scl.period().is_negligible() { + Ratio::from_octaves(1) + } else { + scl.period() + } + .divided_into_equal_steps(scl.num_items()); + + let mut scales = Vec::new(); + let mut layouts = vec![None]; + + IsomorphicLayout::find_by_step_size(avg_step_size) + .into_iter() + .map(|isomorphic_layout| { + let scale_name = format!( + "{} | {}{}", + isomorphic_layout.notation(), + isomorphic_layout.get_scale_name(), + isomorphic_layout + .alt_tritave() + .then_some(" | b-val") + .unwrap_or_default(), + ); + + let mos = isomorphic_layout.mos(); + let orig_keyboard = IsomorphicKeyboard { + primary_step: mos.primary_step(), + secondary_step: mos.secondary_step(), + }; + + ( + VirtualKeyboardLayout { + scale_name, + keyboard: orig_keyboard.clone().coprime(), + orig_keyboard, + steps: (mos.num_primary_steps(), mos.num_secondary_steps()), + }, + generate_colors(&isomorphic_layout), + ) + }) + .chain({ + let keyboard = IsomorphicKeyboard { + primary_step: options.primary_step_width, + secondary_step: options.secondary_step_width, + }; + + [( + VirtualKeyboardLayout { + scale_name: options.layout_name, + keyboard: keyboard.clone(), + orig_keyboard: keyboard, + steps: (options.num_primary_steps, options.num_secondary_steps), + }, + options.colors.0, + )] + }) + .for_each(|(layout, colors)| { + let layout = Arc::new(layout); + + scales.push(VirtualKeyboardScale { + layout: layout.clone(), + colors, + }); + layouts.push(Some(layout)); + }); + + let compressions = vec![ + Compression::None, + Compression::Compressed, + Compression::Expanded, + ]; + + VirtualKeyboardResource { + on_screen_keyboard: on_screen_keyboards.into(), + scale: scales.into(), + layout: layouts.into(), + compression: compressions.into(), + avg_step_size, + } + } + + pub fn scale_name(&self) -> &str { + &self.scale.curr_option().layout.scale_name + } + + pub fn colors(&self) -> &[Color] { + &self.scale.curr_option().colors + } + + pub fn scale_step_sizes(&self) -> (i32, i32, i32) { + let scale_steps = &self.scale.curr_option().layout.orig_keyboard; + let primary_step = i32::from(scale_steps.primary_step); + let secondary_step = i32::from(scale_steps.secondary_step); + (primary_step, secondary_step, primary_step - secondary_step) + } + + pub fn layout_name(&self) -> &str { + self.layout + .curr_option() + .as_ref() + .map(|layout| &*layout.scale_name) + .unwrap_or("Automatic") + } + + pub fn layout_step_counts(&self) -> (u16, u16) { + self.curr_layout().steps + } + + pub fn layout_step_sizes(&self) -> (i32, i32, i32) { + let layout_steps = &self.curr_layout().keyboard; + let primary_step = i32::from(layout_steps.primary_step); + let secondary_step = i32::from(layout_steps.secondary_step); + let secondary_step = match self.compression.curr_option() { + Compression::None => secondary_step, + Compression::Compressed => secondary_step + primary_step, + Compression::Expanded => secondary_step - primary_step, + }; + (primary_step, secondary_step, primary_step - secondary_step) + } + + fn curr_layout(&self) -> &VirtualKeyboardLayout { + self.layout + .curr_option() + .as_ref() + .unwrap_or_else(|| &self.scale.curr_option().layout) + } + + pub fn get_key(&self, num_primary_steps: i16, num_secondary_steps: i16) -> i32 { + let num_primary_steps = match self.compression.curr_option() { + Compression::None => num_primary_steps, + Compression::Compressed => num_primary_steps + num_secondary_steps, + Compression::Expanded => num_primary_steps - num_secondary_steps, + }; + + self.curr_layout() + .keyboard + .get_key(num_primary_steps, num_secondary_steps) + } + + pub fn period(&self) -> Ratio { + let (num_primary_steps, num_secondary_steps) = self.layout_step_counts(); + let (primary_step, secondary_step, ..) = self.layout_step_sizes(); + self.avg_step_size.repeated( + i32::from(num_primary_steps) * primary_step + + i32::from(num_secondary_steps) * secondary_step, + ) + } +} + +fn generate_colors(layout: &IsomorphicLayout) -> Vec { + let color_indexes = layout.get_colors(); + + let colors = [ + SHARP_COLOR, + FLAT_COLOR, + DOUBLE_SHARP_COLOR, + DOUBLE_FLAT_COLOR, + TRIPLE_SHARP_COLOR, + TRIPLE_FLAT_COLOR, + ]; + + (0..layout.pergen().period()) + .map(|index| { + const CYCLE_DARKNESS_FACTOR: f32 = 0.5; + + let generation = layout.pergen().get_generation(index); + let degree = generation.degree; + let color_index = color_indexes[usize::from(degree)]; + + // The shade logic combines two requirements: + // - High contrast in the sharp (north-east) direction => Alternation + // - High contrast in the secondary (south-east) direction => Exception to the alternation rule for the middle cycle + let cycle_darkness = match (generation.cycle.unwrap_or_default() * 2 + 1) + .cmp(&layout.pergen().num_cycles()) + { + Ordering::Less => { + CYCLE_DARKNESS_FACTOR * f32::from(generation.cycle.unwrap_or_default() % 2 != 0) + } + Ordering::Equal => CYCLE_DARKNESS_FACTOR / 2.0, + Ordering::Greater => { + CYCLE_DARKNESS_FACTOR + * f32::from( + (layout.pergen().num_cycles() - generation.cycle.unwrap_or_default()) + % 2 + != 0, + ) + } + }; + + (match color_index { + 0 => NATURAL_COLOR, + x => colors[(x - 1) % colors.len()], + }) * (1.0 - cycle_darkness) + }) + .collect() +} + +const NATURAL_COLOR: Color = Color::WHITE; +const SHARP_COLOR: Color = Color::rgb(0.5, 0.0, 1.0); +const FLAT_COLOR: Color = Color::rgb(0.5, 1.0, 0.5); +const DOUBLE_SHARP_COLOR: Color = Color::rgb(0.5, 0.5, 1.0); +const DOUBLE_FLAT_COLOR: Color = Color::rgb(0.0, 0.5, 0.5); +const TRIPLE_SHARP_COLOR: Color = Color::rgb(0.5, 0.0, 0.5); +const TRIPLE_FLAT_COLOR: Color = Color::rgb(1.0, 0.0, 0.5); diff --git a/microwave/src/app/view/mod.rs b/microwave/src/app/view/mod.rs index 5c35fdc5..89352bef 100644 --- a/microwave/src/app/view/mod.rs +++ b/microwave/src/app/view/mod.rs @@ -13,7 +13,15 @@ use tune::{math, note::Note, pitch::Ratio, scala::KbmRoot, tuning::Scale}; use tune_cli::shared::midi::TuningMethod; use crate::{ - app::model::OnScreenKeyboards, + app::{ + input::HudMode, + resources::{ + virtual_keyboard::OnScreenKeyboards, BackendInfoResource, HudStackResource, + MainViewResource, PianoEngineResource, PianoEngineStateResource, + }, + view::on_screen_keyboard::{KeyboardCreator, OnScreenKeyboard}, + BackendInfo, DynBackendInfo, VirtualKeyboardResource, + }, control::LiveParameter, fluid::{FluidError, FluidInfo}, midi::{MidiOutError, MidiOutInfo}, @@ -23,14 +31,7 @@ use crate::{ tunable, }; -use self::keyboard::{KeyboardCreator, OnScreenKeyboard}; - -use super::{ - model::{BackendInfoResource, PianoEngineResource, PianoEngineStateResource, ViewModel}, - BackendInfo, DynBackendInfo, Toggle, VirtualKeyboardLayout, -}; - -mod keyboard; +mod on_screen_keyboard; const SCENE_HEIGHT_2D: f32 = 1.0 / 2.0; // Designed for 2:1 viewport ratio const SCENE_BOTTOM_2D: f32 = -SCENE_HEIGHT_2D / 2.0; @@ -136,8 +137,8 @@ fn process_updates( mut materials: ResMut>, engine: Res, mut state: ResMut, - virtual_layout: Res>, - view_model: Res, + virtual_keyboard: Res, + main_view: Res, keyboards: Query<(Entity, &mut OnScreenKeyboard)>, mut keys: Query<&mut Transform>, grid_lines: Query>, @@ -149,7 +150,7 @@ fn process_updates( engine.0.capture_state(&mut state.0); let scene_rerender_required = - state.0.tuning_updated || virtual_layout.is_changed() || view_model.is_changed(); + state.0.tuning_updated || virtual_keyboard.is_changed() || main_view.is_changed(); let pitch_lines_rerender_required = state.0.keys_updated || scene_rerender_required; if scene_rerender_required { @@ -163,8 +164,8 @@ fn process_updates( &mut meshes, &mut materials, &state.0, - virtual_layout.curr_option(), - &view_model, + &virtual_keyboard, + &main_view, ); // Remove old grid lines @@ -177,7 +178,7 @@ fn process_updates( &mut meshes, &mut materials, &state.0, - &view_model, + &main_view, ); } @@ -192,7 +193,7 @@ fn process_updates( &mut meshes, &mut color_materials, &state.0, - &view_model, + &main_view, &font.0, ); } @@ -203,8 +204,8 @@ fn create_keyboards( meshes: &mut Assets, materials: &mut Assets, state: &PianoEngineState, - virtual_layout: &VirtualKeyboardLayout, - view_model: &ViewModel, + virtual_keyboard: &VirtualKeyboardResource, + main_view: &MainViewResource, ) { fn get_12edo_key_color(key: i32) -> Color { if [1, 3, 6, 8, 10].contains(&key.rem_euclid(12)) { @@ -217,7 +218,7 @@ fn create_keyboards( let kbm_root = state.kbm.kbm_root(); let (reference_keyboard_location, scale_keyboard_location, keyboard_location) = - match view_model.on_screen_keyboards.curr_option() { + match virtual_keyboard.on_screen_keyboard.curr_option() { OnScreenKeyboards::Isomorphic => (None, None, Some(1.0 / 3.0)), OnScreenKeyboards::Scale => (None, Some(1.0 / 3.0), None), OnScreenKeyboards::Reference => (Some(1.0 / 3.0), None, None), @@ -230,7 +231,7 @@ fn create_keyboards( commands, meshes, materials, - view_model, + main_view, height: SCENE_HEIGHT_3D / 3.0 * KEYBOARD_VERT_FILL, width: 1.0, }; @@ -238,7 +239,7 @@ fn create_keyboards( if let Some(reference_keyboard_location) = reference_keyboard_location { creator.create_linear( ( - view_model.reference_scl.clone(), + main_view.reference_scl.clone(), KbmRoot::from(Note::from_piano_key(kbm_root.ref_key)), ), |key| get_12edo_key_color(key + kbm_root.ref_key.midi_number()), @@ -246,29 +247,21 @@ fn create_keyboards( ); } + let colors = &virtual_keyboard.colors(); + if let Some(scale_keyboard_location) = scale_keyboard_location { creator.create_linear( (state.scl.clone(), kbm_root), - |key| { - virtual_layout.colors[usize::from(math::i32_rem_u( - key, - u16::try_from(virtual_layout.colors.len()).unwrap(), - ))] - }, + |key| colors[usize::from(math::i32_rem_u(key, u16::try_from(colors.len()).unwrap()))], scale_keyboard_location * SCENE_HEIGHT_3D, ); } if let Some(keyboard_location) = keyboard_location { creator.create_isomorphic( - virtual_layout, + virtual_keyboard, (state.scl.clone(), kbm_root), - |key| { - virtual_layout.colors[usize::from(math::i32_rem_u( - key, - u16::try_from(virtual_layout.colors.len()).unwrap(), - ))] - }, + |key| colors[usize::from(math::i32_rem_u(key, u16::try_from(colors.len()).unwrap()))], keyboard_location * SCENE_HEIGHT_3D, ); } @@ -309,7 +302,7 @@ fn create_grid_lines( meshes: &mut Assets, materials: &mut Assets, state: &PianoEngineState, - view_model: &ViewModel, + main_view: &MainViewResource, ) { let line_mesh = meshes.add({ let mut mesh = Mesh::new(PrimitiveTopology::LineStrip, default()); @@ -326,7 +319,7 @@ fn create_grid_lines( let mut scale_grid = commands.spawn((GridLines, SpatialBundle::default())); let tuning = (&state.scl, state.kbm.kbm_root()); - for (degree, pitch_coord) in iterate_grid_coords(view_model, &tuning) { + for (degree, pitch_coord) in iterate_grid_coords(main_view, &tuning) { let line_color = match degree { 0 => Color::SALMON, _ => Color::GRAY, @@ -355,7 +348,7 @@ fn create_pitch_lines_and_deviation_markers( meshes: &mut Assets, color_materials: &mut Assets, state: &PianoEngineState, - view_model: &ViewModel, + main_view: &MainViewResource, font: &Handle, ) { const LINE_HEIGHT: f32 = SCENE_HEIGHT_2D / 24.0; @@ -377,7 +370,7 @@ fn create_pitch_lines_and_deviation_markers( let square_mesh = meshes.add(Rectangle::default()); - let octave_range = view_model.pitch_range().as_octaves(); + let octave_range = main_view.pitch_range().as_octaves(); let mut freqs_hz = state .pressed_keys @@ -389,7 +382,7 @@ fn create_pitch_lines_and_deviation_markers( let mut curr_slice_window = freqs_hz.as_slice(); while let Some((second, others)) = curr_slice_window.split_last() { - let pitch_coord = view_model.hor_world_coord(*second) as f32; + let pitch_coord = main_view.hor_world_coord(*second) as f32; scale_grid_canvas.with_children(|commands| { commands.spawn(MaterialMesh2dBundle { @@ -423,7 +416,7 @@ fn create_pitch_lines_and_deviation_markers( for first in others.iter() { let approximation = - Ratio::between_pitches(*first, *second).nearest_fraction(view_model.odd_limit); + Ratio::between_pitches(*first, *second).nearest_fraction(main_view.odd_limit); let width = (approximation.deviation.as_octaves() / octave_range) as f32; @@ -475,14 +468,14 @@ fn create_pitch_lines_and_deviation_markers( } fn iterate_grid_coords<'a>( - view_model: &'a ViewModel, + main_view: &'a MainViewResource, tuning: &'a impl Scale, ) -> impl Iterator + 'a { - tunable::range(tuning, view_model.viewport_left, view_model.viewport_right).map( + tunable::range(tuning, main_view.viewport_left, main_view.viewport_right).map( move |key_degree| { ( key_degree, - view_model.hor_world_coord(tuning.sorted_pitch_of(key_degree)) as f32, + main_view.hor_world_coord(tuning.sorted_pitch_of(key_degree)) as f32, ) }, ) @@ -546,15 +539,16 @@ fn update_hud( mut hud_texts: Query<&mut Text, With>, font: Res, state: Res, - virtual_layout: Res>, - view_model: Res, + hud_stack: Res, + virtual_keyboard: Res, + main_view: Res, ) { for info_update in info_updates.0.try_iter() { *info = info_update; } for mut hud_text in &mut hud_texts { hud_text.sections[0] = TextSection::new( - create_hud_text(&state.0, &virtual_layout, &view_model, &info), + create_hud_text(&state.0, &hud_stack, &virtual_keyboard, &main_view, &info), TextStyle { font: font.0.clone(), font_size: FONT_RESOLUTION, @@ -566,85 +560,110 @@ fn update_hud( fn create_hud_text( state: &PianoEngineState, - virtual_layout: &Toggle, - view_model: &ViewModel, + hud_stack: &HudStackResource, + virtual_keyboard: &VirtualKeyboardResource, + main_view: &MainViewResource, info: &DynBackendInfo, ) -> String { let mut hud_text = String::new(); - writeln!( - hud_text, - "Scale: {scale}\n\ - Reference note [Alt+Left/Right]: {ref_note}\n\ - Scale offset [Left/Right]: {offset:+}\n\ - Output target [Alt+O]: {target}", - scale = state.scl.description(), - ref_note = state.kbm.kbm_root().ref_key.midi_number(), - offset = state.kbm.kbm_root().root_offset, - target = info.0.description(), - ) - .unwrap(); - - info.0.write_info(&mut hud_text).unwrap(); - - let effects = [ - LiveParameter::Sound1, - LiveParameter::Sound2, - LiveParameter::Sound3, - LiveParameter::Sound4, - LiveParameter::Sound5, - LiveParameter::Sound6, - LiveParameter::Sound7, - LiveParameter::Sound8, - LiveParameter::Sound9, - LiveParameter::Sound10, - ] - .into_iter() - .enumerate() - .filter(|&(_, p)| state.storage.is_active(p)) - .map(|(i, p)| format!("{} (cc {})", i + 1, state.mapper.get_ccn(p).unwrap())) - .collect::>(); - - writeln!( - hud_text, - "Tuning mode [Alt+T]: {tuning_mode:?}\n\ - Legato [Alt+L]: {legato}\n\ - Effects [F1-F10]: {effects}\n\ - Recording [Space]: {recording}\n\ - On-screen keyboard [Alt+K]: {keyboard}\n\ - Keyboard layout [Alt+Y]: {layout}\n\ - Range [Alt+/Scroll]: {from:.0}..{to:.0} Hz", - tuning_mode = state.tuning_mode, - legato = if state.storage.is_active(LiveParameter::Legato) { - format!( - "ON (cc {})", - state.mapper.get_ccn(LiveParameter::Legato).unwrap() + match hud_stack.top() { + None => { + writeln!( + hud_text, + "Scale: {}\n\ + \n\ + [Alt+Left/Right] Reference note: {}\n\ + [Left/Right] Scale offset: {:+}\n\ + [Alt+Up/Down] Output target: {}", + state.scl.description(), + state.kbm.kbm_root().ref_key.midi_number(), + state.kbm.kbm_root().root_offset, + info.0.description(), ) - } else { - "OFF".to_owned() - }, - effects = effects.join(", "), - recording = if state.storage.is_active(LiveParameter::Foot) { - format!( - "ON (cc {})", - state.mapper.get_ccn(LiveParameter::Foot).unwrap() + .unwrap(); + + info.0.write_info(&mut hud_text).unwrap(); + + let effects = [ + LiveParameter::Sound1, + LiveParameter::Sound2, + LiveParameter::Sound3, + LiveParameter::Sound4, + LiveParameter::Sound5, + LiveParameter::Sound6, + LiveParameter::Sound7, + LiveParameter::Sound8, + LiveParameter::Sound9, + LiveParameter::Sound10, + ] + .into_iter() + .enumerate() + .filter(|&(_, p)| state.storage.is_active(p)) + .map(|(i, p)| format!("{} (cc {})", i + 1, state.mapper.get_ccn(p).unwrap())) + .collect::>() + .join(", "); + + writeln!( + hud_text, + "[Alt+T] Tuning mode: {:?}\n\ + [Alt+L] Legato: {}\n\ + [F1-F10] Effects: {}\n\ + [Space] Recording: {}\n\ + [Alt+K] Keyboard settings ...\n\ + [(Alt+)Scroll] Range: {:.0}..{:.0} Hz", + state.tuning_mode, + if state.storage.is_active(LiveParameter::Legato) { + format!( + "ON (cc {})", + state.mapper.get_ccn(LiveParameter::Legato).unwrap() + ) + } else { + "OFF".to_owned() + }, + effects, + if state.storage.is_active(LiveParameter::Foot) { + format!( + "ON (cc {})", + state.mapper.get_ccn(LiveParameter::Foot).unwrap() + ) + } else { + "OFF".to_owned() + }, + main_view.viewport_left.as_hz(), + main_view.viewport_right.as_hz(), ) - } else { - "OFF".to_owned() - }, - keyboard = match view_model.on_screen_keyboards.curr_option() { - OnScreenKeyboards::Isomorphic => "Isomorphic", - OnScreenKeyboards::Scale => "Scale", - OnScreenKeyboards::Reference => "Reference", - OnScreenKeyboards::IsomorphicAndReference => "Isomorphic + Reference", - OnScreenKeyboards::ScaleAndReference => "Scale + Reference", - OnScreenKeyboards::None => "OFF", - }, - layout = virtual_layout.curr_option().description, - from = view_model.viewport_left.as_hz(), - to = view_model.viewport_right.as_hz(), - ) - .unwrap(); + .unwrap(); + } + Some(HudMode::Keyboard) => { + let scale_steps = virtual_keyboard.scale_step_sizes(); + let layout_steps = virtual_keyboard.layout_step_sizes(); + + writeln!( + hud_text, + "MOS scale: primary_step = {} | secondary_step = {} | sharpness = {}\n\ + Isomorphic layout: east = {} | south-east = {} | north-east = {}\n\ + \n\ + [Alt+K] On-screen keyboards: {}\n\ + [Alt+S] Scale: {}\n\ + [Alt+L] Layout: {}\n\ + [Alt+C] Compression: {:?}\n\ + \n\ + [Esc] Back", + scale_steps.0, + scale_steps.1, + scale_steps.2, + layout_steps.0, + layout_steps.1, + layout_steps.2, + virtual_keyboard.on_screen_keyboard.curr_option(), + virtual_keyboard.scale_name(), + virtual_keyboard.layout_name(), + virtual_keyboard.compression.curr_option() + ) + .unwrap(); + } + } hud_text } @@ -663,8 +682,8 @@ impl BackendInfo for MagnetronInfo { fn write_info(&self, target: &mut String) -> fmt::Result { writeln!( target, - "Waveform [Up/Down]: {waveform_number} - {waveform_name}\n\ - Envelope [Alt+E]: {envelope_name}{is_default_indicator}", + "[Up/Down] Waveform: {waveform_number} - {waveform_name}\n\ + [Alt+E] Envelope: {envelope_name}{is_default_indicator}", waveform_number = self.waveform_number, waveform_name = self.waveform_name, envelope_name = self.envelope_name, @@ -692,7 +711,7 @@ impl BackendInfo for FluidInfo { target, "Soundfont File: {soundfont_file}\n\ Tuning method: {tuning_method}\n\ - Program [Up/Down]: {program_number} - {program_name}", + [Up/Down] Program: {program_number} - {program_name}", soundfont_file = self.soundfont_location, program_number = self .program @@ -742,7 +761,7 @@ impl BackendInfo for MidiOutInfo { target, "Device: {device}\n\ Tuning method: {tuning_method}\n\ - Program [Up/Down]: {program_number}", + [Up/Down] Program: {program_number}", device = self.device, program_number = self.program_number, ) diff --git a/microwave/src/app/view/keyboard.rs b/microwave/src/app/view/on_screen_keyboard.rs similarity index 93% rename from microwave/src/app/view/keyboard.rs rename to microwave/src/app/view/on_screen_keyboard.rs index a27a5421..e10959a1 100644 --- a/microwave/src/app/view/keyboard.rs +++ b/microwave/src/app/view/on_screen_keyboard.rs @@ -10,9 +10,7 @@ use tune::{ tuning::Scale, }; -use crate::app::model::ViewModel; - -use super::VirtualKeyboardLayout; +use crate::app::resources::{virtual_keyboard::VirtualKeyboardResource, MainViewResource}; #[derive(Component)] pub struct OnScreenKeyboard { @@ -70,7 +68,7 @@ pub struct KeyboardCreator<'a, 'w, 's> { pub commands: &'a mut Commands<'w, 's>, pub meshes: &'a mut Assets, pub materials: &'a mut Assets, - pub view_model: &'a ViewModel, + pub main_view: &'a MainViewResource, pub height: f32, pub width: f32, } @@ -96,7 +94,7 @@ impl KeyboardCreator<'_, '_, '_> { let mut left; let (mut mid, mut right) = default(); - for (iterated_key, grid_coord) in super::iterate_grid_coords(self.view_model, &tuning) { + for (iterated_key, grid_coord) in super::iterate_grid_coords(self.main_view, &tuning) { (left, mid, right) = (mid, right, Some(grid_coord * self.width)); if let (Some(left), Some(mid), Some(right)) = (left, mid, right) { @@ -149,7 +147,7 @@ impl KeyboardCreator<'_, '_, '_> { pub fn create_isomorphic( &mut self, - virtual_layout: &VirtualKeyboardLayout, + virtual_keyboard: &VirtualKeyboardResource, tuning: (Scl, KbmRoot), get_key_color: impl Fn(i32) -> Color, vertical_position: f32, @@ -161,22 +159,23 @@ impl KeyboardCreator<'_, '_, '_> { let primary_step = Vec2::new(1.0, 0.0); // Hexagonal east direction let secondary_step = Vec2::new(0.5, -0.5 * 3f32.sqrt()); // Hexagonal south-east direction - let geometric_period = f32::from(virtual_layout.num_primary_steps) * primary_step - + f32::from(virtual_layout.num_secondary_steps) * secondary_step; + let (num_primary_steps, num_secondary_steps) = virtual_keyboard.layout_step_counts(); + let geometric_period = f32::from(num_primary_steps) * primary_step + + f32::from(num_secondary_steps) * secondary_step; let board_angle = geometric_period.angle_between(Vec2::X); let board_rotation = Mat2::from_angle(board_angle); - let key_stride = virtual_layout - .period + let key_stride = virtual_keyboard + .period() .divided_into_equal_steps(geometric_period.length()) - .num_equal_steps_of_size(self.view_model.pitch_range()) as f32; + .num_equal_steps_of_size(self.main_view.pitch_range()) as f32; let primary_stride_2d = key_stride * (board_rotation * primary_step); let secondary_stride_2d = key_stride * (board_rotation * secondary_step); let (x_range, y_range) = self.get_bounding_box(); - let offset = self.view_model.hor_world_coord(tuning.1.ref_pitch) as f32; + let offset = self.main_view.hor_world_coord(tuning.1.ref_pitch) as f32; let (p_range, s_range) = ortho_bounding_box_to_hex_bounding_box( primary_stride_2d, @@ -236,7 +235,7 @@ impl KeyboardCreator<'_, '_, '_> { continue; } - let key_degree = virtual_layout.keyboard.get_key(p, s) - tuning.1.root_offset; + let key_degree = virtual_keyboard.get_key(p, s) - tuning.1.root_offset; let key_color = get_key_color(key_degree); let transform = Transform::from_translation(translation) diff --git a/microwave/src/backend.rs b/microwave/src/backend.rs index f611e6db..9509dddf 100644 --- a/microwave/src/backend.rs +++ b/microwave/src/backend.rs @@ -5,7 +5,8 @@ use tune::{ scala::{KbmRoot, Scl}, }; -pub type Backends = Vec>>; +pub type DynBackend = Box>; +pub type Backends = Vec>; pub trait Backend: Send { fn note_input(&self) -> NoteInput; diff --git a/microwave/src/main.rs b/microwave/src/main.rs index adedce65..eda78862 100644 --- a/microwave/src/main.rs +++ b/microwave/src/main.rs @@ -18,10 +18,10 @@ mod synth; mod test; mod tunable; -use std::{cmp::Ordering, collections::HashMap, path::PathBuf, str::FromStr}; +use std::{collections::HashMap, path::PathBuf, str::FromStr}; use ::magnetron::automation::AutomationFactory; -use app::{PhysicalKeyboardLayout, VirtualKeyboardLayout}; +use app::{PhysicalKeyboardLayout, VirtualKeyboardResource}; use async_std::task; use bevy::render::color::Color; use clap::{builder::ValueParserFactory, Parser}; @@ -29,7 +29,6 @@ use control::{LiveParameter, LiveParameterMapper, LiveParameterStorage, Paramete use piano::PianoEngine; use profile::MicrowaveProfile; use tune::{ - layout::{IsomorphicKeyboard, IsomorphicLayout}, note::NoteLetter, pitch::Ratio, scala::{Kbm, Scl}, @@ -113,7 +112,7 @@ struct RunOptions { program_number: u8, #[command(flatten)] - virtual_layout: VirtualKeyboardOptions, + custom_keyboard: CustomKeyboardOptions, /// Physical keyboard layout. /// [ansi] Large backspace key, horizontal enter key, large left shift key. @@ -233,7 +232,11 @@ struct AudioOptions { } #[derive(Parser)] -struct VirtualKeyboardOptions { +struct CustomKeyboardOptions { + /// Name of the custom isometric layout + #[arg(long = "cust-layout", default_value = "PC Keyboard")] + layout_name: String, + /// Primary step width (east direction) of the custom isometric layout (computer keyboard and on-screen keyboard) #[arg(long = "p-step", default_value = "4", value_parser = u16::value_parser().range(1..100))] primary_step_width: u16, @@ -246,7 +249,7 @@ struct VirtualKeyboardOptions { #[arg(long = "p-steps", default_value = "1", value_parser = u16::value_parser().range(1..100))] num_primary_steps: u16, - /// Number of secondary steps (south-east direction) of the isometric layout (on-screen keyboard) + /// Number of secondary steps (south-east direction) of the custom isometric layout (on-screen keyboard) #[arg(long = "s-steps", default_value = "0", value_parser = u16::value_parser().range(0..100))] num_secondary_steps: u16, @@ -365,7 +368,7 @@ impl RunOptions { .unwrap() }); - let virtual_layouts = self.virtual_layout.find_layouts(&scl); + let virtual_keyboard = VirtualKeyboardResource::new(&scl, self.custom_keyboard); let profile = MicrowaveProfile::load(&self.profile_location).await?; @@ -452,7 +455,7 @@ impl RunOptions { engine, engine_state, self.physical_layout, - virtual_layouts, + virtual_keyboard, self.odd_limit, info_recv, resources, @@ -462,139 +465,6 @@ impl RunOptions { } } -impl VirtualKeyboardOptions { - fn find_layouts(self, scl: &Scl) -> Vec { - let average_step_size = if scl.period().is_negligible() { - Ratio::from_octaves(1) - } else { - scl.period() - } - .divided_into_equal_steps(scl.num_items()); - - IsomorphicLayout::find_by_step_size(average_step_size) - .iter() - .map(|layout| { - let keyboard = layout.get_keyboard(); - - let period = average_step_size.repeated( - u32::from(layout.mos().num_primary_steps()) * u32::from(keyboard.primary_step) - + u32::from(layout.mos().num_secondary_steps()) - * u32::from(keyboard.secondary_step), - ); - - let description = format!( - "{} | {}{} | p = {} | s = {} | # = {}", - layout.notation(), - layout.get_scale_name(), - layout - .alt_tritave() - .then_some(" | b-val") - .unwrap_or_default(), - keyboard.primary_step, - keyboard.secondary_step, - layout.mos().sharpness() - ); - - VirtualKeyboardLayout { - description, - keyboard, - num_primary_steps: layout.mos().num_primary_steps(), - num_secondary_steps: layout.mos().num_secondary_steps(), - period, - colors: generate_colors(layout), - } - }) - .chain([{ - let keyboard = IsomorphicKeyboard { - primary_step: self.primary_step_width, - secondary_step: self.secondary_step_width, - }; - - let period = average_step_size.repeated( - i32::from(self.num_primary_steps) * i32::from(self.primary_step_width) - + i32::from(self.num_secondary_steps) - * i32::from(self.secondary_step_width), - ); - - VirtualKeyboardLayout { - description: "Custom".to_owned(), - keyboard, - num_primary_steps: self.num_primary_steps, - num_secondary_steps: self.num_secondary_steps, - period, - colors: self.colors.0, - } - }]) - .collect() - } -} - -fn generate_colors(layout: &IsomorphicLayout) -> Vec { - let color_indexes = layout.get_colors(); - - let colors = match layout.mos().sharpness() >= 0 { - true => [ - SHARP_COLOR, - FLAT_COLOR, - DOUBLE_SHARP_COLOR, - DOUBLE_FLAT_COLOR, - TRIPLE_SHARP_COLOR, - TRIPLE_FLAT_COLOR, - ], - false => [ - FLAT_COLOR, - SHARP_COLOR, - DOUBLE_FLAT_COLOR, - DOUBLE_SHARP_COLOR, - TRIPLE_FLAT_COLOR, - TRIPLE_SHARP_COLOR, - ], - }; - - (0..layout.pergen().period()) - .map(|index| { - const CYCLE_DARKNESS_FACTOR: f32 = 0.5; - - let generation = layout.pergen().get_generation(index); - let degree = generation.degree; - let color_index = color_indexes[usize::from(degree)]; - - // The shade logic combines two requirements: - // - High contrast in the sharp (north-east) direction => Alternation - // - High contrast in the secondary (south-east) direction => Exception to the alternation rule for the middle cycle - let cycle_darkness = match (generation.cycle.unwrap_or_default() * 2 + 1) - .cmp(&layout.pergen().num_cycles()) - { - Ordering::Less => { - CYCLE_DARKNESS_FACTOR * f32::from(generation.cycle.unwrap_or_default() % 2 != 0) - } - Ordering::Equal => CYCLE_DARKNESS_FACTOR / 2.0, - Ordering::Greater => { - CYCLE_DARKNESS_FACTOR - * f32::from( - (layout.pergen().num_cycles() - generation.cycle.unwrap_or_default()) - % 2 - != 0, - ) - } - }; - - (match color_index { - 0 => NATURAL_COLOR, - x => colors[(x - 1) % colors.len()], - }) * (1.0 - cycle_darkness) - }) - .collect() -} - -const NATURAL_COLOR: Color = Color::WHITE; -const SHARP_COLOR: Color = Color::rgb(0.5, 0.0, 1.0); -const FLAT_COLOR: Color = Color::rgb(0.5, 1.0, 0.5); -const DOUBLE_SHARP_COLOR: Color = Color::rgb(0.5, 0.5, 1.0); -const DOUBLE_FLAT_COLOR: Color = Color::rgb(0.0, 0.5, 0.5); -const TRIPLE_SHARP_COLOR: Color = Color::rgb(0.5, 0.0, 0.5); -const TRIPLE_FLAT_COLOR: Color = Color::rgb(1.0, 0.0, 0.5); - impl ControlChangeOptions { fn to_parameter_mapper(&self) -> LiveParameterMapper { let mut mapper = LiveParameterMapper::new(); diff --git a/microwave/src/piano.rs b/microwave/src/piano.rs index 8bf624be..bfb46465 100644 --- a/microwave/src/piano.rs +++ b/microwave/src/piano.rs @@ -15,7 +15,8 @@ use tune::{ use tune_cli::shared::midi::MultiChannelOffset; use crate::{ - backend::{Backend, Backends, NoteInput}, + app::Toggle, + backend::{Backends, DynBackend, NoteInput}, control::{LiveParameter, LiveParameterMapper, LiveParameterStorage, ParameterValue}, }; @@ -25,7 +26,6 @@ pub struct PianoEngine { #[derive(Clone)] pub struct PianoEngineState { - pub curr_backend: usize, pub scl: Scl, pub kbm: Kbm, pub tuning_mode: TuningMode, @@ -58,7 +58,7 @@ pub struct KeyInfo { struct PianoEngineModel { state: PianoEngineState, - backends: Backends, + backends: Toggle>, storage_updates: Sender, } @@ -86,7 +86,6 @@ impl PianoEngine { storage_updates: Sender, ) -> (Arc, PianoEngineState) { let state = PianoEngineState { - curr_backend: 0, scl, kbm, tuning_mode: TuningMode::Fixed, @@ -99,7 +98,7 @@ impl PianoEngine { let mut model = PianoEngineModel { state: state.clone(), - backends, + backends: backends.into(), storage_updates, }; @@ -142,10 +141,15 @@ impl PianoEngine { backend.send_status(); } - pub fn toggle_synth_mode(&self) { + pub fn inc_backend(&self) { let mut model = self.lock_model(); - model.curr_backend += 1; - model.curr_backend %= model.backends.len(); + model.backends.inc(); + model.backend_mut().send_status(); + } + + pub fn dec_backend(&self) { + let mut model = self.lock_model(); + model.backends.dec(); model.backend_mut().send_status(); } @@ -252,8 +256,8 @@ impl PianoEngineModel { match event { Event::Pressed(id, location, velocity) => { let (degree, pitch) = self.degree_and_pitch(location); - let curr_backend = self.curr_backend; - for (backend_id, backend) in self.backends.iter_mut().enumerate() { + let curr_backend = self.backends.curr_index(); + for (backend_id, backend) in self.backends.into_iter().enumerate() { let is_curr_backend = backend_id == curr_backend; if backend.note_input() == NoteInput::Background || is_curr_backend { backend.start(id, degree, pitch, velocity); @@ -268,7 +272,7 @@ impl PianoEngineModel { Event::Moved(id, location) => { if self.storage.is_active(LiveParameter::Legato) { let (degree, pitch) = self.degree_and_pitch(location); - for (backend_id, backend) in self.backends.iter_mut().enumerate() { + for (backend_id, backend) in self.backends.into_iter().enumerate() { if let Some(key_info) = self.state.pressed_keys.get_mut(&(id, backend_id)) { backend.update_pitch(id, degree, pitch, 100); if let (true, Some(key_info)) = (backend.has_legato(), key_info) { @@ -280,7 +284,7 @@ impl PianoEngineModel { } } Event::Released(id, velocity) => { - for (backend_id, backend) in self.backends.iter_mut().enumerate() { + for (backend_id, backend) in self.backends.into_iter().enumerate() { backend.stop(id, velocity); self.state.pressed_keys.remove(&(id, backend_id)); self.state.keys_updated = true; @@ -391,8 +395,7 @@ pub enum SourceId { } impl PianoEngineModel { - pub fn backend_mut(&mut self) -> &mut dyn Backend { - let curr_backend = self.curr_backend; - self.backends[curr_backend].as_mut() + pub fn backend_mut(&mut self) -> &mut DynBackend { + self.backends.curr_option_mut() } } diff --git a/tune-cli/src/est.rs b/tune-cli/src/est.rs index 6ea75dae..97b0aeb3 100644 --- a/tune-cli/src/est.rs +++ b/tune-cli/src/est.rs @@ -220,8 +220,6 @@ impl<'a, 'b> EstPrinter<'a, 'b> { ))?; self.print_newline()?; - let keyboard = layout.get_keyboard(); - self.app.writeln("---- Note names ----")?; self.print_newline()?; @@ -237,6 +235,7 @@ impl<'a, 'b> EstPrinter<'a, 'b> { self.app.writeln("---- Keyboard layout ----")?; self.print_newline()?; + let keyboard = layout.get_keyboard(); for y in -5i16..=5 { for x in 0..10 { self.app.write(format_args!( diff --git a/tune-cli/src/midi.rs b/tune-cli/src/midi.rs index 92bd1983..67474bd8 100644 --- a/tune-cli/src/midi.rs +++ b/tune-cli/src/midi.rs @@ -62,7 +62,7 @@ impl MidiSource { } pub struct MultiChannelOffset { - offset: i32, + pub offset: i32, } impl MultiChannelOffset { @@ -270,10 +270,10 @@ pub fn start_in_connect_loop( start_connect_loop( fuzzy_port_name, move || MidiInput::new(&client_name), - move |driver, port, name| { + move |driver, port| { driver.connect( port, - name, + "MIDI-in", { let callback = callback.clone(); move |_, message, _| (callback.try_lock().unwrap())(message) @@ -296,13 +296,13 @@ pub fn connect_to_out_device( let (port_name, port) = find_port_by_name(&midi_output, fuzzy_port_name)?; - Ok((port_name, midi_output.connect(&port, "MIDI in")?)) + Ok((port_name, midi_output.connect(&port, "MIDI-out")?)) } fn start_connect_loop( fuzzy_port_name: String, mut driver_factory: impl FnMut() -> Result + SendTask + 'static, - mut connect: impl FnMut(D, &D::Port, &str) -> Result> + SendTask + 'static, + mut connect: impl FnMut(D, &D::Port) -> Result> + SendTask + 'static, mut disconnect: impl FnMut(C) + SendTask + 'static, mut report_status: impl FnMut(String) + SendTask + 'static, ) where @@ -337,7 +337,7 @@ fn start_connect_loop( } if let Ok((name, port)) = find_port_by_name(&driver, &fuzzy_port_name) { - match connect(driver, &port, &name) { + match connect(driver, &port) { Ok(conn) => { report_status(format!("Connected to {name}")); port_name_conn = Some((port, name, conn));