├── .gitignore ├── rustfmt.toml ├── Cargo.toml ├── src ├── config_font.rs ├── config_keycode.rs ├── config_rgb.rs ├── remap.rs ├── value.rs ├── slip.rs ├── config_command.rs ├── audio.rs ├── cli.rs ├── config_joystick.rs ├── nav.rs ├── draw.rs ├── menu.rs ├── config.rs ├── display.rs ├── font.rs ├── main.rs ├── m8.rs ├── nav_page.rs ├── menu_tools.rs ├── app.rs └── nav_item.rs ├── LICENSE ├── rm8.json ├── rm8-qwerty.json ├── rm8-dvorak.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | rm8-*-archlinux* 4 | rm8-*-win64* 5 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | hard_tabs = true 3 | struct_field_align_threshold = 15 4 | struct_lit_width = 100 5 | struct_variant_width = 100 6 | use_small_heuristics = "Max" 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rm8" 3 | version = "0.5.3" 4 | edition = "2021" 5 | 6 | [profile.release] 7 | lto = true 8 | #strip = true 9 | panic = "abort" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | ctrlc = "3.2.1" 15 | sdl2 = "0.35.1" 16 | serde = "1.0.131" 17 | serde_derive = "1.0.131" 18 | serde_json = "1.0.72" 19 | serialport = "4.0.1" 20 | -------------------------------------------------------------------------------- /src/config_font.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] 4 | pub enum Font { 5 | Uppercase, 6 | UpperAltZero, 7 | Lowercase, 8 | LowerAltZero, 9 | } 10 | 11 | impl Font { 12 | pub const MAX_LENGTH: usize = 14; 13 | } 14 | 15 | impl TryFrom for Font { 16 | type Error = (); 17 | fn try_from(value: u8) -> Result { 18 | Ok(match value { 19 | 0 => Font::Uppercase, 20 | 1 => Font::UpperAltZero, 21 | 2 => Font::Lowercase, 22 | 3 => Font::LowerAltZero, 23 | _ => return Err(()), 24 | }) 25 | } 26 | } 27 | impl fmt::Display for Font { 28 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 29 | match self { 30 | Font::Uppercase => write!(f, "UPPERCASE"), 31 | Font::UpperAltZero => write!(f, "UPPER ALT.ZERO"), 32 | Font::Lowercase => write!(f, "LOWERCASE"), 33 | Font::LowerAltZero => write!(f, "LOWER ALT.ZERO"), 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/config_keycode.rs: -------------------------------------------------------------------------------- 1 | use sdl2::keyboard::Keycode as SdlKeycode; 2 | use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; 3 | 4 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 5 | pub struct Keycode(pub SdlKeycode); 6 | 7 | impl std::ops::Deref for Keycode { 8 | type Target = SdlKeycode; 9 | fn deref(&self) -> &Self::Target { 10 | &self.0 11 | } 12 | } 13 | 14 | impl From for Keycode { 15 | fn from(keycode: SdlKeycode) -> Self { 16 | Self(keycode) 17 | } 18 | } 19 | 20 | impl Serialize for Keycode { 21 | fn serialize(&self, serializer: S) -> Result 22 | where 23 | S: Serializer, 24 | { 25 | serializer.serialize_str(&self.0.name()) 26 | } 27 | } 28 | 29 | impl<'de> Deserialize<'de> for Keycode { 30 | fn deserialize(deserializer: D) -> Result 31 | where 32 | D: Deserializer<'de>, 33 | { 34 | let s = String::deserialize(deserializer)?; 35 | match SdlKeycode::from_name(&s) { 36 | Some(code) => Ok(Self(code)), 37 | None => Err(de::Error::custom(&format!("Invalid key: {}", s))), 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alex Boussinet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/config_rgb.rs: -------------------------------------------------------------------------------- 1 | use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; 2 | 3 | #[derive(Debug, Clone, Copy, PartialEq)] 4 | pub struct Rgb(pub u8, pub u8, pub u8); 5 | 6 | impl Rgb { 7 | pub fn from_tuple(rgb: (u8, u8, u8)) -> Self { 8 | Rgb(rgb.0, rgb.1, rgb.2) 9 | } 10 | 11 | pub fn rgb(&self) -> (u8, u8, u8) { 12 | (self.0, self.1, self.2) 13 | } 14 | } 15 | 16 | impl Serialize for Rgb { 17 | fn serialize(&self, serializer: S) -> Result 18 | where 19 | S: Serializer, 20 | { 21 | serializer.serialize_str(&format!("#{:02x}{:02x}{:02x}", self.0, self.1, self.2)) 22 | } 23 | } 24 | 25 | impl<'de> Deserialize<'de> for Rgb { 26 | fn deserialize(deserializer: D) -> Result 27 | where 28 | D: Deserializer<'de>, 29 | { 30 | let s = String::deserialize(deserializer)?; 31 | if s.starts_with('#') && s.len() == 7 { 32 | return Ok(Rgb( 33 | u8::from_str_radix(&s[1..3], 16).map_err(|_| { 34 | de::Error::custom(&format!("Invalid R component: {}", &s[1..3])) 35 | })?, 36 | u8::from_str_radix(&s[3..5], 16).map_err(|_| { 37 | de::Error::custom(&format!("Invalid G component: {}", &s[3..5])) 38 | })?, 39 | u8::from_str_radix(&s[5..7], 16).map_err(|_| { 40 | de::Error::custom(&format!("Invalid B component: {}", &s[5..7])) 41 | })?, 42 | )); 43 | } 44 | Err(de::Error::custom(&format!("Invalid RGB: {}", s))) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/remap.rs: -------------------------------------------------------------------------------- 1 | use crate::nav::{Input, Item, Navigation}; 2 | use sdl2::keyboard::Keycode; 3 | 4 | pub struct Remap { 5 | pos: usize, 6 | keys: Vec, 7 | } 8 | 9 | impl Remap { 10 | pub fn new(menu: &mut Navigation) -> Self { 11 | let mut keys = Vec::new(); 12 | for (i, item) in menu.page_mut().items_mut().enumerate() { 13 | if let Item::Input(_, Input::Key(k)) = item { 14 | if i == 0 { 15 | k.focus(); 16 | } 17 | keys.push(k.value()); 18 | } 19 | } 20 | menu.page_mut().reset_cursor(); 21 | menu.dirty(); 22 | Self { pos: usize::MAX, keys } 23 | } 24 | 25 | pub fn remap(&mut self, menu: &mut Navigation, key: Keycode) -> bool { 26 | if self.pos == usize::MAX { 27 | self.pos = 0; 28 | } else { 29 | let mut items = menu.page_mut().items_mut().skip(self.pos); 30 | if let Some(Item::Input(_, Input::Key(k))) = items.next() { 31 | if self.keys[..self.pos].contains(&key) { 32 | k.exists(); 33 | } else { 34 | k.unfocus(); 35 | k.set_value(key); 36 | self.pos += 1; 37 | if let Some(Item::Input(_, Input::Key(k))) = items.next() { 38 | k.focus(); 39 | } 40 | } 41 | } 42 | } 43 | menu.page_mut().reset_cursor(); 44 | menu.dirty(); 45 | self.pos >= self.keys.len() 46 | } 47 | 48 | pub fn abort(&mut self, menu: &mut Navigation) { 49 | for item in menu.page_mut().items_mut() { 50 | if let Item::Input(_, Input::Key(k)) = item { 51 | k.unfocus(); 52 | } 53 | } 54 | menu.page_mut().reset_cursor(); 55 | menu.dirty(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /rm8.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "fullscreen": false, 4 | "font": "Uppercase", 5 | "zoom": 4, 6 | "key_sensibility": 60, 7 | "show_fps": false, 8 | "fps": 60 9 | }, 10 | "theme": { 11 | "screen": "#000000", 12 | "text_default": "#8c8cba", 13 | "text_value": "#fafafa", 14 | "text_title": "#32ecff", 15 | "text_info": "#60608e", 16 | "cursor": "#32ecff", 17 | "octave_bg": "#0000ff", 18 | "octave_fg": "#ffffff", 19 | "velocity_bg": "#ff0000", 20 | "velocity_fg": "#ffffff" 21 | }, 22 | "m8": { 23 | "up": "Up", 24 | "down": "Down", 25 | "left": "Left", 26 | "right": "Right", 27 | "edit": "Left Ctrl", 28 | "option": "Left Alt", 29 | "shift": "Left Shift", 30 | "play": "Space" 31 | }, 32 | "rm8": { 33 | "keyjazz": "Return", 34 | "velocity_minus": "-", 35 | "velocity_plus": "=", 36 | "octave_minus": "[", 37 | "octave_plus": "]" 38 | }, 39 | "keyjazz": { 40 | "Z": 0, 41 | "S": 1, 42 | "X": 2, 43 | "D": 3, 44 | "C": 4, 45 | "V": 5, 46 | "G": 6, 47 | "B": 7, 48 | "H": 8, 49 | "N": 9, 50 | "J": 10, 51 | "M": 11, 52 | ",": 12, 53 | "Q": 12, 54 | "2": 13, 55 | "L": 13, 56 | ".": 14, 57 | "W": 14, 58 | ";": 15, 59 | "3": 15, 60 | "/": 16, 61 | "E": 16, 62 | "R": 17, 63 | "5": 18, 64 | "T": 19, 65 | "6": 20, 66 | "Y": 21, 67 | "7": 22, 68 | "U": 23, 69 | "I": 24, 70 | "9": 25, 71 | "O": 26, 72 | "0": 27, 73 | "P": 28 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /rm8-qwerty.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "fullscreen": false, 4 | "font": "Uppercase", 5 | "zoom": 4, 6 | "key_sensibility": 60, 7 | "show_fps": false, 8 | "fps": 60 9 | }, 10 | "theme": { 11 | "screen": "#000000", 12 | "text_default": "#8c8cba", 13 | "text_value": "#fafafa", 14 | "text_title": "#32ecff", 15 | "text_info": "#60608e", 16 | "cursor": "#32ecff", 17 | "octave_bg": "#0000ff", 18 | "octave_fg": "#ffffff", 19 | "velocity_bg": "#ff0000", 20 | "velocity_fg": "#ffffff" 21 | }, 22 | "m8": { 23 | "up": "Up", 24 | "down": "Down", 25 | "left": "Left", 26 | "right": "Right", 27 | "edit": "Left Ctrl", 28 | "option": "Left Alt", 29 | "shift": "Left Shift", 30 | "play": "Space" 31 | }, 32 | "rm8": { 33 | "keyjazz": "Return", 34 | "velocity_minus": "-", 35 | "velocity_plus": "=", 36 | "octave_minus": "[", 37 | "octave_plus": "]" 38 | }, 39 | "keyjazz": { 40 | "Z": 0, 41 | "S": 1, 42 | "X": 2, 43 | "D": 3, 44 | "C": 4, 45 | "V": 5, 46 | "G": 6, 47 | "B": 7, 48 | "H": 8, 49 | "N": 9, 50 | "J": 10, 51 | "M": 11, 52 | ",": 12, 53 | "Q": 12, 54 | "2": 13, 55 | "L": 13, 56 | ".": 14, 57 | "W": 14, 58 | ";": 15, 59 | "3": 15, 60 | "/": 16, 61 | "E": 16, 62 | "R": 17, 63 | "5": 18, 64 | "T": 19, 65 | "6": 20, 66 | "Y": 21, 67 | "7": 22, 68 | "U": 23, 69 | "I": 24, 70 | "9": 25, 71 | "O": 26, 72 | "0": 27, 73 | "P": 28 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /rm8-dvorak.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "fullscreen": false, 4 | "font": "Uppercase", 5 | "zoom": 4, 6 | "key_sensibility": 60, 7 | "show_fps": false, 8 | "fps": 60 9 | }, 10 | "theme": { 11 | "screen": "#000000", 12 | "text_default": "#8c8cba", 13 | "text_value": "#fafafa", 14 | "text_title": "#32ecff", 15 | "text_info": "#60608e", 16 | "cursor": "#32ecff", 17 | "octave_bg": "#0000ff", 18 | "octave_fg": "#ffffff", 19 | "velocity_bg": "#ff0000", 20 | "velocity_fg": "#ffffff" 21 | }, 22 | "m8": { 23 | "up": "Up", 24 | "down": "Down", 25 | "left": "Left", 26 | "right": "Right", 27 | "edit": "Left Ctrl", 28 | "option": "Left Alt", 29 | "shift": "Left Shift", 30 | "play": "Space" 31 | }, 32 | "rm8": { 33 | "keyjazz": "Return", 34 | "velocity_minus": "-", 35 | "velocity_plus": "\\", 36 | "octave_minus": "[", 37 | "octave_plus": "]" 38 | }, 39 | "keyjazz": { 40 | ";": 0, 41 | "O": 1, 42 | "Q": 2, 43 | "E": 3, 44 | "J": 4, 45 | "K": 5, 46 | "I": 6, 47 | "X": 7, 48 | "D": 8, 49 | "B": 9, 50 | "H": 10, 51 | "M": 11, 52 | "W": 12, 53 | "'": 12, 54 | "2": 13, 55 | "N": 13, 56 | "V": 14, 57 | ",": 14, 58 | "S": 15, 59 | "3": 15, 60 | "Z": 16, 61 | ".": 16, 62 | "P": 17, 63 | "5": 18, 64 | "Y": 19, 65 | "6": 20, 66 | "F": 21, 67 | "7": 22, 68 | "G": 23, 69 | "C": 24, 70 | "9": 25, 71 | "R": 26, 72 | "0": 27, 73 | "L": 28 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/value.rs: -------------------------------------------------------------------------------- 1 | pub struct Value { 2 | value: T, 3 | modified: bool, 4 | } 5 | 6 | impl std::ops::Deref for Value { 7 | type Target = T; 8 | fn deref(&self) -> &Self::Target { 9 | &self.value 10 | } 11 | } 12 | 13 | impl Value { 14 | pub fn new(value: T) -> Self { 15 | Self { value, modified: false } 16 | } 17 | 18 | pub fn set(&mut self, value: T) { 19 | if self.value != value { 20 | self.value = value; 21 | self.modified = true; 22 | } 23 | } 24 | 25 | pub fn set_changed(&mut self) { 26 | self.modified = true; 27 | } 28 | 29 | pub fn changed(&mut self) -> bool { 30 | if self.modified { 31 | self.modified = false; 32 | return true; 33 | } 34 | false 35 | } 36 | } 37 | 38 | impl Value { 39 | pub fn toggle(&mut self) { 40 | self.value = !self.value; 41 | self.modified = true; 42 | } 43 | } 44 | 45 | impl Value { 46 | pub fn set_bit(&mut self, mask: u8) { 47 | if self.value & mask == 0 { 48 | self.value |= mask; 49 | self.modified = true; 50 | } 51 | } 52 | 53 | pub fn clr_bit(&mut self, mask: u8) { 54 | if self.value & mask != 0 { 55 | self.value &= !mask; 56 | self.modified = true; 57 | } 58 | } 59 | 60 | pub fn tst_bit(&mut self, mask: u8) -> bool { 61 | self.value & mask != 0 62 | } 63 | 64 | pub fn add(&mut self, add: u8, max: u8) { 65 | if self.value < max { 66 | self.modified = true; 67 | if self.value as usize + add as usize > max as usize { 68 | self.value = max; 69 | } else { 70 | self.value += add; 71 | } 72 | } 73 | } 74 | 75 | pub fn sub(&mut self, sub: u8, min: u8) { 76 | if self.value > min { 77 | self.modified = true; 78 | if self.value as usize > min as usize + sub as usize { 79 | self.value -= sub; 80 | } else { 81 | self.value = min; 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/slip.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use serialport::SerialPort; 4 | 5 | const END: u8 = 0xc0; 6 | const ESC: u8 = 0xdb; 7 | const ESC_END: u8 = 0xdc; 8 | const ESC_ESC: u8 = 0xdd; 9 | 10 | enum SlipState { 11 | Normal, 12 | Escape, 13 | } 14 | 15 | pub struct Slip { 16 | state: SlipState, 17 | buf: [u8; N], 18 | rmax: usize, 19 | rpos: usize, 20 | wpos: usize, 21 | } 22 | 23 | impl Slip { 24 | pub fn new() -> Self { 25 | Self { state: SlipState::Normal, buf: [0; N], rmax: 0, rpos: 0, wpos: 0 } 26 | } 27 | 28 | fn push_byte(&mut self, byte: u8, buf: &mut [u8]) -> io::Result<()> { 29 | if self.wpos >= buf.len() { 30 | self.wpos = 0; 31 | return Err(io::Error::new(io::ErrorKind::InvalidData, "push_byte overflow")); 32 | } 33 | buf[self.wpos] = byte; 34 | self.wpos += 1; 35 | Ok(()) 36 | } 37 | 38 | pub fn read<'a>( 39 | &mut self, 40 | port: &mut Box, 41 | buf: &'a mut [u8], 42 | ) -> io::Result> { 43 | loop { 44 | if self.rpos >= self.rmax { 45 | self.rpos = 0; 46 | match port.read(&mut self.buf) { 47 | Ok(n) => self.rmax = n, 48 | Err(e) if e.kind() == io::ErrorKind::TimedOut => self.rmax = 0, 49 | Err(e) => return Err(e), 50 | } 51 | if self.rmax == 0 { 52 | return Ok(None); 53 | } 54 | } 55 | while self.rpos < self.rmax { 56 | let byte = self.buf[self.rpos]; 57 | match self.state { 58 | SlipState::Normal => match byte { 59 | END if self.wpos > 1 => { 60 | self.rpos += 1; 61 | let end = self.wpos; 62 | self.wpos = 0; 63 | return Ok(Some(&buf[..end])); 64 | } 65 | END => { 66 | return Err(io::Error::new(io::ErrorKind::InvalidData, "empty command")) 67 | } 68 | ESC => { 69 | self.state = SlipState::Escape; 70 | self.rpos += 1; 71 | continue; 72 | } 73 | _ => self.push_byte(byte, buf)?, 74 | }, 75 | SlipState::Escape => match byte { 76 | ESC_END => self.push_byte(END, buf)?, 77 | ESC_ESC => self.push_byte(ESC, buf)?, 78 | _ => { 79 | return Err(io::Error::new( 80 | io::ErrorKind::InvalidData, 81 | format!("invalid escape sequence: {:02x}", byte), 82 | )) 83 | } 84 | }, 85 | } 86 | self.state = SlipState::Normal; 87 | self.rpos += 1; 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/config_command.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] 4 | pub enum Command { 5 | None, 6 | Up, 7 | Down, 8 | Left, 9 | Right, 10 | Edit, 11 | r#Option, 12 | Shift, 13 | Play, 14 | VelocityMinus, 15 | VelocityPlus, 16 | OctaveMinus, 17 | OctavePlus, 18 | Keyjazz, 19 | Config, 20 | Escape, 21 | Fullscreen, 22 | Reset, 23 | ResetFull, 24 | } 25 | 26 | impl Default for Command { 27 | fn default() -> Self { 28 | Self::None 29 | } 30 | } 31 | 32 | impl Command { 33 | pub const MAX_LENGTH: usize = 9; 34 | 35 | pub fn is_none(&self) -> bool { 36 | self == &Self::None 37 | } 38 | } 39 | 40 | impl TryFrom for Command { 41 | type Error = (); 42 | fn try_from(value: u8) -> Result { 43 | Ok(match value { 44 | 0 => Command::None, 45 | 1 => Command::Up, 46 | 2 => Command::Down, 47 | 3 => Command::Left, 48 | 4 => Command::Right, 49 | 5 => Command::Edit, 50 | 6 => Command::r#Option, 51 | 7 => Command::Shift, 52 | 8 => Command::Play, 53 | 9 => Command::VelocityMinus, 54 | 10 => Command::VelocityPlus, 55 | 11 => Command::OctaveMinus, 56 | 12 => Command::OctavePlus, 57 | 13 => Command::Keyjazz, 58 | 14 => Command::Config, 59 | 15 => Command::Escape, 60 | 16 => Command::Fullscreen, 61 | 17 => Command::Reset, 62 | 18 => Command::ResetFull, 63 | _ => return Err(()), 64 | }) 65 | } 66 | } 67 | 68 | impl fmt::Display for Command { 69 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 70 | match *self { 71 | Command::None => write!(f, "{:1$}", " ", Command::MAX_LENGTH), 72 | Command::Up => write!(f, "UP"), 73 | Command::Down => write!(f, "DOWN"), 74 | Command::Left => write!(f, "LEFT"), 75 | Command::Right => write!(f, "RIGHT"), 76 | Command::Edit => write!(f, "EDIT"), 77 | Command::r#Option => write!(f, "OPTION"), 78 | Command::Shift => write!(f, "SHIFT"), 79 | Command::Play => write!(f, "PLAY"), 80 | Command::VelocityMinus => write!(f, "VELOCITY-"), 81 | Command::VelocityPlus => write!(f, "VELOCITY+"), 82 | Command::OctaveMinus => write!(f, "OCTAVE-"), 83 | Command::OctavePlus => write!(f, "OCTAVE+"), 84 | Command::Keyjazz => write!(f, "KEYJAZZ"), 85 | Command::Config => write!(f, "CONFIG"), 86 | Command::Escape => write!(f, "ESCAPE"), 87 | Command::Fullscreen => write!(f, "FULLSCREEN"), 88 | Command::Reset => write!(f, "RESET"), 89 | Command::ResetFull=> write!(f, "RESETFULL"), 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/audio.rs: -------------------------------------------------------------------------------- 1 | use sdl2::audio::{AudioCallback, AudioDevice, AudioSpecDesired}; 2 | 3 | use std::sync::mpsc; 4 | 5 | struct Capture { 6 | done_sender: mpsc::Sender>, 7 | size: usize, 8 | } 9 | 10 | impl AudioCallback for Capture { 11 | type Channel = i16; 12 | 13 | fn callback(&mut self, input: &mut [i16]) { 14 | for chunk in input.chunks(self.size) { 15 | self.done_sender.send(chunk.to_owned()).expect("could not send record buffer"); 16 | } 17 | } 18 | } 19 | 20 | struct Playback { 21 | done_receiver: mpsc::Receiver>, 22 | } 23 | 24 | impl AudioCallback for Playback { 25 | type Channel = i16; 26 | 27 | fn callback(&mut self, output: &mut [i16]) { 28 | if let Ok(vec) = self.done_receiver.recv() { 29 | output.copy_from_slice(&vec); 30 | } 31 | } 32 | } 33 | 34 | pub struct Audio { 35 | capture: AudioDevice, 36 | playback: AudioDevice, 37 | playing: bool, 38 | name: String, 39 | } 40 | 41 | impl Audio { 42 | fn real_open( 43 | audio: &sdl2::AudioSubsystem, 44 | device_name: String, 45 | samples: Option, 46 | ) -> Result { 47 | let spec = AudioSpecDesired { freq: Some(44100), channels: None, samples }; 48 | let (done_sender, done_receiver) = mpsc::channel(); 49 | let capture = audio.open_capture(Some(device_name.as_ref()), &spec, |spec| Capture { 50 | done_sender, 51 | size: samples.unwrap_or(spec.samples as u16) as usize * 2, 52 | })?; 53 | let playback = audio.open_playback(None, &spec, |_spec| Playback { done_receiver })?; 54 | 55 | Ok(Self { playing: false, capture, playback, name: device_name }) 56 | } 57 | 58 | pub fn open( 59 | audio: &sdl2::AudioSubsystem, 60 | name: Option, 61 | samples: Option, 62 | ) -> Result { 63 | if let Some(device_name) = name { 64 | Self::real_open(audio, device_name, samples) 65 | } else { 66 | for i in 0..audio.num_audio_capture_devices().unwrap_or(0) { 67 | if let Ok(device_name) = audio.audio_capture_device_name(i) { 68 | if device_name.starts_with("M8 Analog Stereo") { 69 | return Self::real_open(audio, device_name, samples); 70 | } 71 | } 72 | } 73 | Err("No M8 audio device found".to_string()) 74 | } 75 | } 76 | 77 | pub fn reopen( 78 | &mut self, 79 | audio: &sdl2::AudioSubsystem, 80 | name: String, 81 | samples: Option, 82 | ) -> Result<(), String> { 83 | *self = Self::real_open(audio, name, samples)?; 84 | Ok(()) 85 | } 86 | 87 | pub fn toggle(&mut self) { 88 | if self.playing { 89 | self.pause() 90 | } else { 91 | self.resume() 92 | } 93 | } 94 | 95 | pub fn pause(&mut self) { 96 | self.playing = false; 97 | self.capture.pause(); 98 | self.playback.pause(); 99 | } 100 | 101 | pub fn resume(&mut self) { 102 | self.playing = true; 103 | self.capture.resume(); 104 | self.playback.resume(); 105 | } 106 | 107 | pub fn name(&self) -> String { 108 | self.name.clone() 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use crate::config::Config; 4 | use crate::m8::M8; 5 | 6 | const USAGE: &str = "Usage rm8 [options] 7 | Available options: 8 | -help Display this help screen 9 | -version Display the version of the program 10 | -list List available M8 devices 11 | -noaudio Disable audio loopback mode 12 | -dev DEVICE Connect to the the given M8 device 13 | -cap DEVICE Connect the given capture device to the default playback device 14 | -smp SAMPLES Use the specified number of samples for audio processing 15 | -wc Write the default configuration to the standard output 16 | -wc FILE Write the default configuration to the given file 17 | -rc FILE Read the configuration from the given file"; 18 | 19 | pub fn handle_command_line( 20 | config: &mut Config, 21 | config_file: &mut Option, 22 | device: &mut Option, 23 | capture: &mut Option, 24 | samples: &mut Option, 25 | noaudio: &mut bool, 26 | ) -> Result { 27 | let mut args = env::args().skip(1); 28 | loop { 29 | match args.next().as_deref() { 30 | Some("-version") => { 31 | println!("rm8 v{}", env!("CARGO_PKG_VERSION")); 32 | return Ok(false); 33 | } 34 | Some("-help") => { 35 | eprintln!("{}", USAGE); 36 | return Ok(false); 37 | } 38 | Some("-list") => { 39 | let ports = M8::list_ports().map_err(|e| e.to_string())?; 40 | println!("{}", if ports.is_empty() { "No M8 found" } else { "M8 found:" }); 41 | for port in ports { 42 | println!("\t- {}", port); 43 | } 44 | return Ok(false); 45 | } 46 | Some("-wc") => match args.next() { 47 | Some(file) => { 48 | if let Err(e) = config.write(&file) { 49 | return Err(format!("Error: writing config to file {} ({})", &file, e)); 50 | } 51 | config_file.replace(file); 52 | return Ok(false); 53 | } 54 | None => match config.dump() { 55 | Ok(json) => { 56 | println!("{}", json); 57 | return Ok(false); 58 | } 59 | Err(e) => return Err(format!("Error: dumping config ({})", e)), 60 | }, 61 | }, 62 | Some("-rc") => match args.next() { 63 | Some(file) => { 64 | if let Err(e) = config.read(&file) { 65 | return Err(format!("Error: loading config file `{}` ({})", file, e)); 66 | } 67 | config_file.replace(file); 68 | } 69 | None => return Err("Error: missing config file argument".to_string()), 70 | }, 71 | Some("-noaudio") => { 72 | *noaudio = true; 73 | } 74 | Some("-dev") => match args.next() { 75 | Some(dev) => { 76 | device.replace(dev); 77 | } 78 | None => return Err("Error: missing device argument".to_string()), 79 | }, 80 | Some("-cap") => match args.next() { 81 | Some(cap) => { 82 | capture.replace(cap); 83 | } 84 | None => return Err("Error: missing capture argument".to_string()), 85 | }, 86 | Some("-smp") => match args.next() { 87 | Some(smp) => { 88 | let smp = 89 | smp.parse().map_err(|_| "Error: invalid samples argument".to_string())?; 90 | samples.replace(smp); 91 | } 92 | None => return Err("Error: missing samples argument".to_string()), 93 | }, 94 | Some(arg) => return Err(format!("Error: unknown argument: {}", arg)), 95 | None => break, 96 | } 97 | } 98 | Ok(true) 99 | } 100 | -------------------------------------------------------------------------------- /src/config_joystick.rs: -------------------------------------------------------------------------------- 1 | use serde::{ser::SerializeMap, Serializer}; 2 | use std::collections::HashMap; 3 | 4 | use crate::config::Command; 5 | 6 | #[derive(Debug, Clone, Default, Serialize, Deserialize)] 7 | pub struct HatConfig { 8 | #[serde(default, skip_serializing_if = "Command::is_none")] 9 | pub up: Command, 10 | #[serde(default, skip_serializing_if = "Command::is_none")] 11 | pub down: Command, 12 | #[serde(default, skip_serializing_if = "Command::is_none")] 13 | pub left: Command, 14 | #[serde(default, skip_serializing_if = "Command::is_none")] 15 | pub right: Command, 16 | #[serde(default, skip_serializing_if = "Command::is_none")] 17 | pub left_up: Command, 18 | #[serde(default, skip_serializing_if = "Command::is_none")] 19 | pub right_up: Command, 20 | #[serde(default, skip_serializing_if = "Command::is_none")] 21 | pub left_down: Command, 22 | #[serde(default, skip_serializing_if = "Command::is_none")] 23 | pub right_down: Command, 24 | } 25 | 26 | pub const DEFAULT_SENSIBILITY: usize = 20000; 27 | 28 | #[derive(Debug, Clone, Serialize, Deserialize)] 29 | pub struct Axis { 30 | #[serde(default, skip_serializing_if = "Command::is_none")] 31 | pub negative: Command, 32 | #[serde(default, skip_serializing_if = "Command::is_none")] 33 | pub positive: Command, 34 | #[serde(default)] 35 | pub sensibility: usize, 36 | } 37 | 38 | impl Axis { 39 | pub fn new(negative: Command, positive: Command, sensibility: usize) -> Self { 40 | Self { negative, positive, sensibility } 41 | } 42 | } 43 | 44 | impl Default for Axis { 45 | fn default() -> Self { 46 | Self { negative: Command::None, positive: Command::None, sensibility: DEFAULT_SENSIBILITY } 47 | } 48 | } 49 | 50 | fn serialize_buttons( 51 | buttons: &HashMap, 52 | serializer: S, 53 | ) -> Result { 54 | let mut map = serializer.serialize_map(None)?; 55 | for (k, v) in buttons.iter() { 56 | if v != &Command::None { 57 | map.serialize_entry(k, v)?; 58 | } 59 | } 60 | map.end() 61 | } 62 | 63 | fn serialize_axes( 64 | axes: &HashMap, 65 | serializer: S, 66 | ) -> Result { 67 | let mut map = serializer.serialize_map(None)?; 68 | for (k, v) in axes.iter() { 69 | if v.negative != Command::None || v.positive != Command::None || v.sensibility != 0 { 70 | map.serialize_entry(k, v)?; 71 | } 72 | } 73 | map.end() 74 | } 75 | 76 | fn buttons_empty(buttons: &HashMap) -> bool { 77 | for (_, v) in buttons.iter() { 78 | if v != &Command::None { 79 | return false; 80 | } 81 | } 82 | true 83 | } 84 | 85 | fn axes_empty(axes: &HashMap) -> bool { 86 | for (_, v) in axes.iter() { 87 | if v.negative != Command::None || v.positive != Command::None || v.sensibility != 0 { 88 | return false; 89 | } 90 | } 91 | true 92 | } 93 | 94 | pub fn serialize_joysticks( 95 | joysticks: &HashMap, 96 | serializer: S, 97 | ) -> Result { 98 | let mut map = serializer.serialize_map(None)?; 99 | for (k, v) in joysticks.iter() { 100 | if v.hats.is_some() || !buttons_empty(&v.buttons) || !axes_empty(&v.axes) { 101 | map.serialize_entry(k, v)?; 102 | } 103 | } 104 | map.end() 105 | } 106 | 107 | pub fn joysticks_empty(joystick: &HashMap) -> bool { 108 | for (_, v) in joystick.iter() { 109 | if v.hats.is_some() || !buttons_empty(&v.buttons) || !axes_empty(&v.axes) { 110 | return false; 111 | } 112 | } 113 | true 114 | } 115 | 116 | #[derive(Debug, Clone, Default, Serialize, Deserialize)] 117 | pub struct JoystickConfig { 118 | #[serde(default, skip_serializing_if = "buttons_empty", serialize_with = "serialize_buttons")] 119 | pub buttons: HashMap, 120 | #[serde(default, skip_serializing_if = "axes_empty", serialize_with = "serialize_axes")] 121 | pub axes: HashMap, 122 | #[serde(default, skip_serializing_if = "Option::is_none")] 123 | pub hats: Option>, 124 | } 125 | -------------------------------------------------------------------------------- /src/nav.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | draw::{Context, LINE_HEIGHT}, 3 | font, m8, 4 | }; 5 | 6 | pub use crate::{ 7 | nav_item::{Action, Direction, Edit, Input, Item}, 8 | nav_page::Page, 9 | }; 10 | 11 | const MENU_X: i32 = 288; 12 | const MENU_Y: i32 = 200; 13 | 14 | #[derive(Debug)] 15 | pub struct Navigation { 16 | pages: Vec, 17 | page: (usize, isize), 18 | changed: bool, 19 | } 20 | 21 | impl Navigation { 22 | pub fn new() -> Self { 23 | Self { pages: vec![], page: (0, 0), changed: true } 24 | } 25 | 26 | pub fn find(&self, short: char) -> Option<&Page> { 27 | for page in self.pages.iter() { 28 | if page.short_name() == short { 29 | return Some(page); 30 | } 31 | } 32 | None 33 | } 34 | 35 | pub fn find_mut(&mut self, short: char) -> Option<&mut Page> { 36 | for page in self.pages.iter_mut() { 37 | if page.short_name() == short { 38 | return Some(page); 39 | } 40 | } 41 | None 42 | } 43 | 44 | pub fn add_page(&mut self, mut page: Page) { 45 | page.reset_cursor(); 46 | self.pages.push(page); 47 | } 48 | 49 | pub fn page_mut(&mut self) -> &mut Page { 50 | self.pages[self.page.0].get_mut(self.page.1).unwrap() 51 | } 52 | 53 | pub fn page(&self) -> &Page { 54 | self.pages[self.page.0].get(self.page.1).unwrap() 55 | } 56 | 57 | pub fn main_page(&self) -> &Page { 58 | &self.pages[self.page.0] 59 | } 60 | 61 | pub fn edit_item(&mut self, edit: Edit) -> Action { 62 | self.page_mut().edit_item(edit) 63 | } 64 | 65 | pub fn replace(&mut self, short: char, mut page: Page) -> Page { 66 | for p in self.pages.iter_mut() { 67 | if p.short_name() == short { 68 | page.reset_cursor(); 69 | return std::mem::replace(p, page); 70 | } 71 | } 72 | page 73 | } 74 | 75 | pub fn navigate(&mut self, direction: Direction) { 76 | match direction { 77 | Direction::Left => { 78 | if self.page.0 > 0 { 79 | self.page.0 -= 1; 80 | self.page.1 = 0; 81 | self.changed = true; 82 | } 83 | } 84 | Direction::Right => { 85 | if self.page.0 + 1 < self.pages.len() { 86 | self.page.0 += 1; 87 | self.page.1 = 0; 88 | self.changed = true; 89 | } 90 | } 91 | Direction::Above => { 92 | if self.pages[self.page.0].get(self.page.1 + 1).is_some() { 93 | self.page.1 += 1; 94 | self.changed = true; 95 | } 96 | } 97 | Direction::Below => { 98 | if self.pages[self.page.0].get(self.page.1 - 1).is_some() { 99 | self.page.1 -= 1; 100 | self.changed = true; 101 | } 102 | } 103 | } 104 | } 105 | 106 | pub fn cursor_move(&mut self, direction: Direction) { 107 | self.page_mut().cursor_move(direction); 108 | } 109 | 110 | pub fn main_page_short_name(&self) -> char { 111 | self.pages[self.page.0].short_name() 112 | } 113 | 114 | pub fn main_page_reset(&mut self) { 115 | self.page.1 = 0 116 | } 117 | 118 | pub fn dirty(&mut self) { 119 | self.changed = true 120 | } 121 | 122 | fn draw_sub_menu( 123 | &self, 124 | ctx: &mut Context<'_, '_, '_>, 125 | sub: &[Page], 126 | x: i32, 127 | line_height: i32, 128 | selected: Option, 129 | ) -> Result<(), String> { 130 | let fg = ctx.theme.text_info; 131 | let hl = ctx.theme.text_title; 132 | let mut y = MENU_Y + line_height; 133 | for (i, s) in sub.iter().enumerate() { 134 | match selected { 135 | Some(sel) if sel == i => ctx.draw_char(s.short_name() as u8, x, y, hl, hl)?, 136 | _ => ctx.draw_char(s.short_name() as u8, x, y, fg, fg)?, 137 | } 138 | y += line_height; 139 | } 140 | Ok(()) 141 | } 142 | 143 | fn draw_menu(&self, ctx: &mut Context<'_, '_, '_>) -> Result<(), String> { 144 | let fg = ctx.theme.text_info; 145 | let hl = ctx.theme.text_title; 146 | let mut x = MENU_X; 147 | for (i, page) in self.pages.iter().enumerate() { 148 | let page_y = self.page.1.saturating_abs() as usize; 149 | let sel_above = 150 | if self.page.0 == i && self.page.1 > 0 { Some(page_y - 1) } else { None }; 151 | let sel_below = 152 | if self.page.0 == i && self.page.1 < 0 { Some(page_y - 1) } else { None }; 153 | self.draw_sub_menu(ctx, self.pages[i].above(), x, -(LINE_HEIGHT as i32), sel_above)?; 154 | self.draw_sub_menu(ctx, self.pages[i].below(), x, LINE_HEIGHT as i32, sel_below)?; 155 | if self.page.0 == i && self.page.1 == 0 { 156 | ctx.draw_char(page.short_name() as u8, x, MENU_Y, hl, hl)?; 157 | } else { 158 | ctx.draw_char(page.short_name() as u8, x, MENU_Y, fg, fg)?; 159 | } 160 | x += font::width(0); 161 | } 162 | Ok(()) 163 | } 164 | 165 | pub fn draw(&mut self, ctx: &mut Context<'_, '_, '_>) -> Result<(), String> { 166 | if self.changed { 167 | ctx.draw_rect((0, 0, m8::SCREEN_WIDTH, m8::SCREEN_HEIGHT), ctx.theme.screen)?; 168 | self.changed = false; 169 | } 170 | self.page_mut().draw(ctx)?; 171 | self.draw_menu(ctx) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/draw.rs: -------------------------------------------------------------------------------- 1 | use sdl2::{ 2 | rect, render, 3 | video::{FullscreenType, Window}, 4 | }; 5 | 6 | use crate::{ 7 | config::{Font, Rgb, ThemeConfig}, 8 | font, m8, 9 | }; 10 | 11 | pub const LINE_HEIGHT: i32 = 10; 12 | 13 | pub struct Context<'a, 'b, 'c> { 14 | pub canvas: &'b mut render::Canvas, 15 | pub font: &'a mut render::Texture<'c>, 16 | pub font_option: Font, 17 | pub theme: ThemeConfig, 18 | pub screen_bg: Option, 19 | } 20 | 21 | impl<'a, 'b, 'c> Context<'_, '_, '_> { 22 | pub fn clear(&mut self) -> Result<(), String> { 23 | self.draw_rect((0, 0, m8::SCREEN_WIDTH, m8::SCREEN_HEIGHT), self.theme.screen) 24 | } 25 | 26 | pub fn draw_str_centered(&mut self, s: &str, y: i32, fg: Rgb, bg: Rgb) -> Result<(), String> { 27 | let x = (m8::SCREEN_WIDTH as usize / 2) as i32 - font::width(s.len() / 2); 28 | self.draw_str(s, x, y, fg, bg) 29 | } 30 | 31 | pub fn draw_str(&mut self, s: &str, x: i32, y: i32, fg: Rgb, bg: Rgb) -> Result<(), String> { 32 | let mut x = x; 33 | for ch in s.chars() { 34 | self.draw_char(ch as u8, x, y, fg, bg)?; 35 | x += font::CHAR_WIDTH as i32; 36 | } 37 | Ok(()) 38 | } 39 | 40 | pub fn draw_char(&mut self, c: u8, x: i32, y: i32, fg: Rgb, bg: Rgb) -> Result<(), String> { 41 | let c = match self.font_option { 42 | Font::UpperAltZero if c == b'0' => 125, 43 | Font::LowerAltZero if c == b'0' => 123, 44 | Font::Uppercase | Font::UpperAltZero => { 45 | if (b'a'..=b'z').contains(&c) { 46 | c - 32 47 | } else { 48 | c 49 | } 50 | } 51 | Font::Lowercase | Font::LowerAltZero => { 52 | if (b'A'..=b'Z').contains(&c) { 53 | c + 32 54 | } else { 55 | c 56 | } 57 | } 58 | }; 59 | let row = c as i32 / font::CHARS_BY_ROW; 60 | let col = c as i32 % font::CHARS_BY_ROW; 61 | let src_rect = rect::Rect::new( 62 | col * font::CHAR_WIDTH, 63 | row * font::CHAR_HEIGHT, 64 | font::CHAR_WIDTH as u32, 65 | font::CHAR_HEIGHT as u32, 66 | ); 67 | let dst_rect = rect::Rect::new(x, y + 3, src_rect.w as u32, src_rect.h as u32); 68 | self.font.set_color_mod(fg.0, fg.1, fg.2); 69 | if fg != bg { 70 | let bg_rect = rect::Rect::new( 71 | x as i32 - 1, 72 | y as i32 + 2, 73 | font::CHAR_WIDTH as u32 - 1, 74 | font::CHAR_HEIGHT as u32 + 1, 75 | ); 76 | self.canvas.set_draw_color(bg.rgb()); 77 | self.canvas.fill_rect(bg_rect)?; 78 | } 79 | self.canvas.copy(self.font, src_rect, dst_rect) 80 | } 81 | 82 | pub fn draw_rect(&mut self, rect: (i32, i32, u32, u32), bg: Rgb) -> Result<(), String> { 83 | let r = rect::Rect::new(rect.0, rect.1, rect.2, rect.3); 84 | if rect.0 == 0 && rect.1 == 0 && rect.2 == m8::SCREEN_WIDTH && rect.3 == m8::SCREEN_HEIGHT { 85 | self.screen_bg = Some(bg); 86 | } 87 | self.canvas.set_draw_color(bg.rgb()); 88 | self.canvas.fill_rect(r) 89 | } 90 | 91 | pub fn draw_waveform(&mut self, data: &[u8], fg: (u8, u8, u8)) -> Result<(), String> { 92 | self.canvas.set_draw_color(self.theme.screen.rgb()); 93 | let rect = rect::Rect::new(0, 0, m8::SCREEN_WIDTH, m8::WAVEFORM_HEIGHT); 94 | self.canvas.fill_rect(rect)?; 95 | if data.is_empty() { 96 | return Ok(()); 97 | } 98 | self.canvas.set_draw_color(fg); 99 | 100 | let mut points = [rect::Point::new(0, 0); m8::SCREEN_WIDTH as usize]; 101 | for (i, p) in data.iter().enumerate() { 102 | points[i] = rect::Point::new(i as i32, *p as i32); 103 | } 104 | self.canvas.draw_points(points.as_ref()) 105 | } 106 | 107 | pub fn draw_octave(&mut self, octave: u8, show: bool) -> Result<(), String> { 108 | let x = m8::SCREEN_WIDTH as i32 - font::CHAR_WIDTH; 109 | let y = m8::SCREEN_HEIGHT as i32 - font::CHAR_HEIGHT; 110 | 111 | let rect = rect::Rect::new( 112 | x + 1, 113 | y - 1, 114 | font::CHAR_WIDTH as u32 - 1, 115 | font::CHAR_HEIGHT as u32 + 1, 116 | ); 117 | self.canvas.set_draw_color(self.theme.screen.rgb()); 118 | self.canvas.fill_rect(rect)?; 119 | 120 | if show { 121 | let c = if octave >= 9 { octave - 9 + b'A' } else { octave + b'1' }; 122 | let x = x - 1; 123 | let y = y - 3; 124 | let fg = self.theme.octave_fg; 125 | let bg = self.theme.octave_bg; 126 | self.draw_char(c, x + 3, y, fg, bg)?; 127 | } 128 | Ok(()) 129 | } 130 | 131 | pub fn draw_velocity(&mut self, velocity: u8, show: bool) -> Result<(), String> { 132 | let mut x = m8::SCREEN_WIDTH as i32 - font::CHAR_WIDTH * 3 + 2; 133 | let y = m8::SCREEN_HEIGHT as i32 - font::CHAR_HEIGHT; 134 | 135 | let rect = rect::Rect::new( 136 | x as i32 - 1, 137 | y as i32 - 1, 138 | font::CHAR_WIDTH as u32 * 2 - 2, 139 | font::CHAR_HEIGHT as u32 + 1, 140 | ); 141 | self.canvas.set_draw_color(self.theme.screen.rgb()); 142 | self.canvas.fill_rect(rect)?; 143 | 144 | if show { 145 | let (vh, vl) = (velocity >> 4, velocity & 0xf); 146 | let fg = self.theme.velocity_fg; 147 | let bg = self.theme.velocity_bg; 148 | let c1 = if vh > 9 { vh - 10 + b'A' } else { vh + b'0' }; 149 | self.draw_char(c1, x, y - 3, fg, bg)?; 150 | x += font::CHAR_WIDTH - 1; 151 | let c2 = if vl > 9 { vl - 10 + b'A' } else { vl + b'0' }; 152 | self.draw_char(c2, x, y - 3, fg, bg)?; 153 | } 154 | Ok(()) 155 | } 156 | } 157 | 158 | pub fn is_fullscreen(canvas: &render::Canvas) -> bool { 159 | !matches!(canvas.window().fullscreen_state(), FullscreenType::Off) 160 | } 161 | 162 | pub fn toggle_fullscreen(canvas: &mut render::Canvas) -> Result<(), String> { 163 | match canvas.window().fullscreen_state() { 164 | FullscreenType::Off => canvas.window_mut().set_fullscreen(FullscreenType::True), 165 | _ => canvas.window_mut().set_fullscreen(FullscreenType::Off), 166 | } 167 | } 168 | 169 | pub fn zoom_window(window: &mut Window, zoom: u32) { 170 | let (w, _) = window.size(); 171 | if m8::SCREEN_WIDTH * zoom != w { 172 | let _ = window.set_size(zoom * m8::SCREEN_WIDTH, zoom * m8::SCREEN_HEIGHT); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/menu.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | config::{Command, Config, DEFAULT_SENSIBILITY}, 3 | m8::M8, 4 | nav::{Navigation, Page}, 5 | }; 6 | 7 | pub fn build_menu(menu: &mut Navigation, m8: &M8, config: &Config) { 8 | let mut theme_page = Page::new("THEME", 'T'); 9 | theme_page.add_rgb("TEXT:DEFAULT", config.theme.text_default); 10 | theme_page.add_rgb("TEXT:VALUE", config.theme.text_value); 11 | theme_page.add_rgb("TEXT:TITLE", config.theme.text_title); 12 | theme_page.add_rgb("TEXT:INFO", config.theme.text_info); 13 | theme_page.add_rgb("CURSOR", config.theme.cursor); 14 | theme_page.add_rgb("SCREEN", config.theme.screen); 15 | theme_page.add_rgb("VELOCITY FG", config.theme.velocity_fg); 16 | theme_page.add_rgb("VELOCITY BG", config.theme.velocity_bg); 17 | theme_page.add_rgb("OCTAVE FG", config.theme.octave_fg); 18 | theme_page.add_rgb("OCTAVE BG", config.theme.octave_bg); 19 | theme_page.add_empty(); 20 | theme_page.add_action2("RESET", "SAVE"); 21 | 22 | let mut app_page = Page::new("CONFIG", 'C'); 23 | app_page.add_bool("FULLSCREEN", config.app.fullscreen); 24 | app_page.add_int("ZOOM", config.app.zoom as usize, 1, 9, 2); 25 | app_page.add_font("FONT", config.app.font); 26 | app_page.add_int("KEY SENS.", config.app.key_sensibility as usize, 60, 200, 10); 27 | app_page.add_bool("SHOW_FPS", config.app.show_fps); 28 | app_page.add_int("FPS", config.app.fps, 1, 200, 10); 29 | app_page.add_bool("RECONNECT", config.app.reconnect); 30 | app_page.add_device("DEVICE", m8.device_name()); 31 | app_page.add_audio("AUDIO", m8.capture_device_name()); 32 | app_page.add_empty(); 33 | app_page.add_action2("RESET", "SAVE"); 34 | app_page.add_page_above(theme_page); 35 | 36 | let mut rm8key_page = Page::new("RM8 KEYS", 'R'); 37 | rm8key_page.add_key("KEYJAZZ", *config.rm8.keyjazz); 38 | rm8key_page.add_key("VELOCITY-", *config.rm8.velocity_minus); 39 | rm8key_page.add_key("VELOCITY+", *config.rm8.velocity_plus); 40 | rm8key_page.add_key("OCTAVE-", *config.rm8.octave_minus); 41 | rm8key_page.add_key("OCTAVE+", *config.rm8.octave_plus); 42 | rm8key_page.add_empty(); 43 | rm8key_page.add_action3("REMAP", "RESET", "SAVE"); 44 | 45 | let mut m8key_page = Page::new("M8 KEYS", 'K'); 46 | m8key_page.add_key("UP", *config.m8.up); 47 | m8key_page.add_key("DOWN", *config.m8.down); 48 | m8key_page.add_key("LEFT", *config.m8.left); 49 | m8key_page.add_key("RIGHT", *config.m8.right); 50 | m8key_page.add_key("EDIT", *config.m8.edit); 51 | m8key_page.add_key("OPTION", *config.m8.option); 52 | m8key_page.add_key("SHIFT", *config.m8.shift); 53 | m8key_page.add_key("PLAY", *config.m8.play); 54 | m8key_page.add_empty(); 55 | m8key_page.add_action3("REMAP", "RESET", "SAVE"); 56 | m8key_page.add_page_below(rm8key_page); 57 | 58 | let mut empty_joystick_page = Page::new("JOYSTICK", 'J'); 59 | empty_joystick_page.add_info("N.JOYSTICKS", "0"); 60 | 61 | menu.add_page(app_page); 62 | menu.add_page(m8key_page); 63 | menu.add_page(empty_joystick_page); 64 | } 65 | 66 | pub fn build_joystick_page() -> Option { 67 | let mut axes_page = Page::new("AXES", 'A'); 68 | axes_page.add_title2("NEG.", "POS.", Command::MAX_LENGTH); 69 | axes_page.add_cmd2("AXIS 0", Command::None, Command::None); 70 | axes_page.add_int("AXIS 0 SENS.", DEFAULT_SENSIBILITY, 0, i16::MAX as usize, 100); 71 | axes_page.add_cmd2("AXIS 1", Command::None, Command::None); 72 | axes_page.add_int("AXIS 1 SENS.", DEFAULT_SENSIBILITY, 0, i16::MAX as usize, 100); 73 | axes_page.add_cmd2("AXIS 2", Command::None, Command::None); 74 | axes_page.add_int("AXIS 2 SENS.", DEFAULT_SENSIBILITY, 0, i16::MAX as usize, 100); 75 | axes_page.add_cmd2("AXIS 3", Command::None, Command::None); 76 | axes_page.add_int("AXIS 3 SENS.", DEFAULT_SENSIBILITY, 0, i16::MAX as usize, 100); 77 | axes_page.add_cmd2("AXIS 4", Command::None, Command::None); 78 | axes_page.add_int("AXIS 4 SENS.", DEFAULT_SENSIBILITY, 0, i16::MAX as usize, 100); 79 | axes_page.add_cmd2("AXIS 5", Command::None, Command::None); 80 | axes_page.add_int("AXIS 5 SENS.", DEFAULT_SENSIBILITY, 0, i16::MAX as usize, 100); 81 | axes_page.add_empty(); 82 | axes_page.add_action2("RESET", "SAVE"); 83 | 84 | let mut buttons_page = Page::new("BUTTONS", 'B'); 85 | buttons_page.add_cmd_label2("B.0", Command::None, "B.10", Command::None, 4); 86 | buttons_page.add_cmd_label2("B.1", Command::None, "B.11", Command::None, 4); 87 | buttons_page.add_cmd_label2("B.2", Command::None, "B.12", Command::None, 4); 88 | buttons_page.add_cmd_label2("B.3", Command::None, "B.13", Command::None, 4); 89 | buttons_page.add_cmd_label2("B.4", Command::None, "B.14", Command::None, 4); 90 | buttons_page.add_cmd_label2("B.5", Command::None, "B.15", Command::None, 4); 91 | buttons_page.add_cmd_label2("B.6", Command::None, "B.16", Command::None, 4); 92 | buttons_page.add_cmd_label2("B.7", Command::None, "B.17", Command::None, 4); 93 | buttons_page.add_cmd_label2("B.8", Command::None, "B.18", Command::None, 4); 94 | buttons_page.add_cmd_label2("B.9", Command::None, "B.19", Command::None, 4); 95 | buttons_page.add_empty(); 96 | buttons_page.add_action2("RESET", "SAVE"); 97 | 98 | let mut hats_page = Page::new("HAT", 'H'); 99 | hats_page.add_cmd("UP", Command::None); 100 | hats_page.add_cmd("DOWN", Command::None); 101 | hats_page.add_cmd("LEFT", Command::None); 102 | hats_page.add_cmd("RIGHT", Command::None); 103 | hats_page.add_cmd("UP LEFT", Command::None); 104 | hats_page.add_cmd("UP RIGHT", Command::None); 105 | hats_page.add_cmd("DOWN LEFT", Command::None); 106 | hats_page.add_cmd("DOWN RIGHT", Command::None); 107 | hats_page.add_empty(); 108 | hats_page.add_action2("RESET", "SAVE"); 109 | 110 | let mut joystick_page = Page::new("JOYSTICK", 'J'); 111 | joystick_page.add_info("N.JOYSTICKS", "0"); 112 | joystick_page.add_int("SEL.ID", 0, 0, 10, 1); 113 | joystick_page.add_empty(); 114 | joystick_page.add_title(""); 115 | joystick_page.add_text("GUID"); 116 | joystick_page.add_info("N.AXES", "0"); 117 | joystick_page.add_info("N.BUTTONS", "0"); 118 | joystick_page.add_info("N.HATS", "0"); 119 | joystick_page.add_empty(); 120 | joystick_page.add_action2("RESET", "SAVE"); 121 | joystick_page.add_page_above(axes_page); 122 | joystick_page.add_page_above(buttons_page); 123 | joystick_page.add_page_below(hats_page); 124 | Some(joystick_page) 125 | } 126 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use sdl2::keyboard::Keycode as SdlKeycode; 2 | use std::collections::HashMap; 3 | 4 | pub use crate::config_command::Command; 5 | pub use crate::config_font::Font; 6 | use crate::config_joystick::{joysticks_empty, serialize_joysticks}; 7 | pub use crate::config_joystick::{Axis, HatConfig, JoystickConfig, DEFAULT_SENSIBILITY}; 8 | pub use crate::config_keycode::Keycode; 9 | pub use crate::config_rgb::Rgb; 10 | 11 | #[derive(Debug, Clone, Copy, Serialize, Deserialize)] 12 | #[serde(default)] 13 | pub struct ThemeConfig { 14 | pub screen: Rgb, 15 | pub text_default: Rgb, 16 | pub text_value: Rgb, 17 | pub text_title: Rgb, 18 | pub text_info: Rgb, 19 | pub cursor: Rgb, 20 | pub octave_bg: Rgb, 21 | pub octave_fg: Rgb, 22 | pub velocity_bg: Rgb, 23 | pub velocity_fg: Rgb, 24 | } 25 | 26 | impl Default for ThemeConfig { 27 | fn default() -> Self { 28 | Self { 29 | octave_bg: Rgb(0, 0, 255), 30 | octave_fg: Rgb(255, 255, 255), 31 | velocity_bg: Rgb(255, 0, 0), 32 | velocity_fg: Rgb(255, 255, 255), 33 | screen: Rgb(0, 0, 0), 34 | text_default: Rgb(0x8c, 0x8c, 0xba), 35 | text_value: Rgb(0xfa, 0xfa, 0xfa), 36 | text_title: Rgb(0x32, 0xec, 0xff), 37 | text_info: Rgb(0x60, 0x60, 0x8e), 38 | cursor: Rgb(0x32, 0xec, 0xff), 39 | } 40 | } 41 | } 42 | 43 | #[derive(Debug, Clone, Serialize, Deserialize)] 44 | #[serde(default)] 45 | pub struct AppConfig { 46 | pub fullscreen: bool, 47 | pub font: Font, 48 | pub zoom: u32, 49 | pub key_sensibility: u64, 50 | pub fps: usize, 51 | pub show_fps: bool, 52 | pub reconnect: bool, 53 | } 54 | 55 | impl Default for AppConfig { 56 | fn default() -> Self { 57 | Self { 58 | fullscreen: false, 59 | font: Font::Uppercase, 60 | zoom: 4, 61 | key_sensibility: 60, 62 | fps: 60, 63 | show_fps: false, 64 | reconnect: false, 65 | } 66 | } 67 | } 68 | 69 | #[derive(Debug, Clone, Serialize, Deserialize)] 70 | #[serde(default)] 71 | pub struct M8KeyboardConfig { 72 | pub up: Keycode, 73 | pub down: Keycode, 74 | pub left: Keycode, 75 | pub right: Keycode, 76 | pub edit: Keycode, 77 | pub option: Keycode, 78 | pub shift: Keycode, 79 | pub play: Keycode, 80 | } 81 | 82 | impl Default for M8KeyboardConfig { 83 | fn default() -> Self { 84 | Self { 85 | up: SdlKeycode::Up.into(), 86 | down: SdlKeycode::Down.into(), 87 | left: SdlKeycode::Left.into(), 88 | right: SdlKeycode::Right.into(), 89 | edit: SdlKeycode::LCtrl.into(), 90 | option: SdlKeycode::LAlt.into(), 91 | shift: SdlKeycode::LShift.into(), 92 | play: SdlKeycode::Space.into(), 93 | } 94 | } 95 | } 96 | 97 | #[derive(Debug, Clone, Serialize, Deserialize)] 98 | #[serde(default)] 99 | pub struct RM8KeyboardConfig { 100 | pub keyjazz: Keycode, 101 | pub velocity_minus: Keycode, 102 | pub velocity_plus: Keycode, 103 | pub octave_minus: Keycode, 104 | pub octave_plus: Keycode, 105 | } 106 | 107 | impl Default for RM8KeyboardConfig { 108 | fn default() -> Self { 109 | Self { 110 | keyjazz: SdlKeycode::Return.into(), 111 | velocity_minus: SdlKeycode::Minus.into(), 112 | velocity_plus: SdlKeycode::Equals.into(), 113 | octave_minus: SdlKeycode::LeftBracket.into(), 114 | octave_plus: SdlKeycode::RightBracket.into(), 115 | } 116 | } 117 | } 118 | 119 | #[derive(Debug, Clone, Serialize, Deserialize)] 120 | pub struct Config { 121 | pub app: AppConfig, 122 | pub theme: ThemeConfig, 123 | pub m8: M8KeyboardConfig, 124 | pub rm8: RM8KeyboardConfig, 125 | #[serde( 126 | default, 127 | skip_serializing_if = "joysticks_empty", 128 | serialize_with = "serialize_joysticks" 129 | )] 130 | pub joysticks: HashMap, 131 | pub keyjazz: HashMap, 132 | #[serde(skip)] 133 | pub overlap: bool, 134 | } 135 | 136 | impl Default for Config { 137 | fn default() -> Self { 138 | Self { 139 | app: AppConfig::default(), 140 | theme: ThemeConfig::default(), 141 | m8: M8KeyboardConfig::default(), 142 | rm8: RM8KeyboardConfig::default(), 143 | keyjazz: HashMap::from([ 144 | (Keycode(SdlKeycode::Z), 0), 145 | (Keycode(SdlKeycode::S), 1), 146 | (Keycode(SdlKeycode::X), 2), 147 | (Keycode(SdlKeycode::D), 3), 148 | (Keycode(SdlKeycode::C), 4), 149 | (Keycode(SdlKeycode::V), 5), 150 | (Keycode(SdlKeycode::G), 6), 151 | (Keycode(SdlKeycode::B), 7), 152 | (Keycode(SdlKeycode::H), 8), 153 | (Keycode(SdlKeycode::N), 9), 154 | (Keycode(SdlKeycode::J), 10), 155 | (Keycode(SdlKeycode::M), 11), 156 | (Keycode(SdlKeycode::Comma), 12), 157 | (Keycode(SdlKeycode::L), 13), 158 | (Keycode(SdlKeycode::Period), 14), 159 | (Keycode(SdlKeycode::Semicolon), 15), 160 | (Keycode(SdlKeycode::Slash), 16), 161 | (Keycode(SdlKeycode::Q), 12), 162 | (Keycode(SdlKeycode::Num2), 13), 163 | (Keycode(SdlKeycode::W), 14), 164 | (Keycode(SdlKeycode::Num3), 15), 165 | (Keycode(SdlKeycode::E), 16), 166 | (Keycode(SdlKeycode::R), 17), 167 | (Keycode(SdlKeycode::Num5), 18), 168 | (Keycode(SdlKeycode::T), 19), 169 | (Keycode(SdlKeycode::Num6), 20), 170 | (Keycode(SdlKeycode::Y), 21), 171 | (Keycode(SdlKeycode::Num7), 22), 172 | (Keycode(SdlKeycode::U), 23), 173 | (Keycode(SdlKeycode::I), 24), 174 | (Keycode(SdlKeycode::Num9), 25), 175 | (Keycode(SdlKeycode::O), 26), 176 | (Keycode(SdlKeycode::Num0), 27), 177 | (Keycode(SdlKeycode::P), 28), 178 | ]), 179 | joysticks: HashMap::new(), 180 | overlap: false, 181 | } 182 | } 183 | } 184 | 185 | impl Config { 186 | pub fn read>(&mut self, file: T) -> Result<(), String> { 187 | let content = std::fs::read_to_string(file.as_ref()).map_err(|e| e.to_string())?; 188 | let config: Self = serde_json::from_str(&content).map_err(|e| e.to_string())?; 189 | *self = config; 190 | self.check_overlap(); 191 | Ok(()) 192 | } 193 | 194 | pub fn write>(&self, file: T) -> Result<(), String> { 195 | let config = self.dump()?; 196 | std::fs::write(file.as_ref(), config).map_err(|e| e.to_string()) 197 | } 198 | 199 | pub fn dump(&self) -> Result { 200 | serde_json::to_string_pretty(self).map_err(|e| e.to_string()) 201 | } 202 | 203 | fn check_overlap(&mut self) { 204 | self.overlap = self.keyjazz.contains_key(&self.m8.up) 205 | || self.keyjazz.contains_key(&self.m8.down) 206 | || self.keyjazz.contains_key(&self.m8.left) 207 | || self.keyjazz.contains_key(&self.m8.right) 208 | || self.keyjazz.contains_key(&self.m8.shift) 209 | || self.keyjazz.contains_key(&self.m8.play) 210 | || self.keyjazz.contains_key(&self.m8.edit) 211 | || self.keyjazz.contains_key(&self.m8.option) 212 | || self.keyjazz.contains_key(&self.rm8.keyjazz) 213 | || self.keyjazz.contains_key(&self.rm8.octave_minus) 214 | || self.keyjazz.contains_key(&self.rm8.octave_plus) 215 | || self.keyjazz.contains_key(&self.rm8.velocity_minus) 216 | || self.keyjazz.contains_key(&self.rm8.velocity_plus); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/display.rs: -------------------------------------------------------------------------------- 1 | use std::time; 2 | 3 | use sdl2::pixels; 4 | use sdl2::{rect, render, video}; 5 | 6 | use crate::config::Remap; 7 | use crate::font; 8 | use crate::m8; 9 | 10 | pub struct Display { 11 | pub bg: pixels::Color, 12 | pub ticks: time::Instant, 13 | } 14 | 15 | impl Display { 16 | pub fn new() -> Self { 17 | let now = time::Instant::now(); 18 | Self { bg: pixels::Color::RGB(0, 0, 0), ticks: now } 19 | } 20 | 21 | pub fn draw_waveform( 22 | &self, 23 | canvas: &mut render::Canvas, 24 | data: &[u8], 25 | fg: (u8, u8, u8), 26 | ) -> Result<(), String> { 27 | canvas.set_draw_color(self.bg); 28 | let rect = rect::Rect::new(0, 0, m8::SCREEN_WIDTH, m8::WAVEFORM_HEIGHT); 29 | canvas.fill_rect(rect)?; 30 | if data.is_empty() { 31 | return Ok(()); 32 | } 33 | canvas.set_draw_color(fg); 34 | 35 | let mut points = [rect::Point::new(0, 0); m8::SCREEN_WIDTH as usize]; 36 | for (i, p) in data.iter().enumerate() { 37 | points[i] = rect::Point::new(i as i32, *p as i32); 38 | } 39 | canvas.draw_points(points.as_ref()) 40 | } 41 | 42 | pub fn draw_rectangle( 43 | &mut self, 44 | canvas: &mut render::Canvas, 45 | x: u16, 46 | y: u16, 47 | w: u16, 48 | h: u16, 49 | bg: (u8, u8, u8), 50 | ) -> Result<(), String> { 51 | let rect = rect::Rect::new(x as i32, y as i32, w as u32, h as u32); 52 | if x == 0 && y == 0 && w == m8::SCREEN_WIDTH as u16 && h == m8::SCREEN_HEIGHT as u16 { 53 | self.bg = pixels::Color::RGB(bg.0, bg.1, bg.2); 54 | } 55 | canvas.set_draw_color(bg); 56 | canvas.fill_rect(rect) 57 | } 58 | 59 | pub fn draw_octave<'a>( 60 | &self, 61 | canvas: &mut render::Canvas, 62 | font: &mut render::Texture<'a>, 63 | octave: u8, 64 | show: bool, 65 | ) -> Result<(), String> { 66 | let x = m8::SCREEN_WIDTH - font::CHAR_WIDTH; 67 | let y = m8::SCREEN_HEIGHT - font::CHAR_HEIGHT; 68 | 69 | let rect = rect::Rect::new( 70 | x as i32 + 1, 71 | y as i32 - 1, 72 | font::CHAR_WIDTH - 1, 73 | font::CHAR_HEIGHT + 1, 74 | ); 75 | canvas.set_draw_color(self.bg); 76 | canvas.fill_rect(rect)?; 77 | 78 | if show { 79 | let c = if octave >= 9 { octave - 9 + b'A' } else { octave + b'1' }; 80 | let x = x as u16 - 1; 81 | let y = y as u16 - 3; 82 | let fg = (0xff, 0xff, 0xff); 83 | let bg = (0, 0, 0xff); 84 | self.draw_character(canvas, font, c, x + 3, y, fg, bg)?; 85 | } 86 | Ok(()) 87 | } 88 | 89 | pub fn draw_velocity<'a>( 90 | &self, 91 | canvas: &mut render::Canvas, 92 | font: &mut render::Texture<'a>, 93 | velocity: u8, 94 | show: bool, 95 | ) -> Result<(), String> { 96 | let mut x = m8::SCREEN_WIDTH - font::CHAR_WIDTH * 3 + 2; 97 | let y = m8::SCREEN_HEIGHT - font::CHAR_HEIGHT; 98 | 99 | let rect = rect::Rect::new( 100 | x as i32 - 1, 101 | y as i32 - 1, 102 | font::CHAR_WIDTH * 2 - 2, 103 | font::CHAR_HEIGHT + 1, 104 | ); 105 | canvas.set_draw_color(self.bg); 106 | canvas.fill_rect(rect)?; 107 | 108 | if show { 109 | let (vh, vl) = (velocity >> 4, velocity & 0xf); 110 | let fg = (0xff, 0xff, 0xff); 111 | let bg = (0xff, 0, 0); 112 | let c1 = if vh > 9 { vh - 10 + b'A' } else { vh + b'0' }; 113 | self.draw_character(canvas, font, c1, x as u16, y as u16 - 3, fg, bg)?; 114 | x += font::CHAR_WIDTH - 1; 115 | let c2 = if vl > 9 { vl - 10 + b'A' } else { vl + b'0' }; 116 | self.draw_character(canvas, font, c2, x as u16, y as u16 - 3, fg, bg)?; 117 | } 118 | Ok(()) 119 | } 120 | 121 | pub fn draw_string_centered<'a>( 122 | &self, 123 | canvas: &mut render::Canvas, 124 | font: &mut render::Texture<'a>, 125 | s: &str, 126 | y: u16, 127 | fg: (u8, u8, u8), 128 | bg: (u8, u8, u8), 129 | ) -> Result<(), String> { 130 | let x = m8::SCREEN_WIDTH as u16 / 2 - s.len() as u16 * font::CHAR_WIDTH as u16 / 2 + 1; 131 | self.draw_string(canvas, font, s, x, y, fg, bg) 132 | } 133 | 134 | #[allow(clippy::too_many_arguments)] 135 | pub fn draw_string<'a>( 136 | &self, 137 | canvas: &mut render::Canvas, 138 | font: &mut render::Texture<'a>, 139 | s: &str, 140 | x: u16, 141 | y: u16, 142 | fg: (u8, u8, u8), 143 | bg: (u8, u8, u8), 144 | ) -> Result<(), String> { 145 | let mut x = x; 146 | for ch in s.chars() { 147 | self.draw_character(canvas, font, ch as u8, x, y, fg, bg)?; 148 | x += font::CHAR_WIDTH as u16; 149 | } 150 | Ok(()) 151 | } 152 | 153 | #[allow(clippy::too_many_arguments)] 154 | pub fn draw_character<'a>( 155 | &self, 156 | canvas: &mut render::Canvas, 157 | font: &mut render::Texture<'a>, 158 | c: u8, 159 | x: u16, 160 | y: u16, 161 | fg: (u8, u8, u8), 162 | bg: (u8, u8, u8), 163 | ) -> Result<(), String> { 164 | let row = c as u32 / font::CHARS_BY_ROW; 165 | let col = c as u32 % font::CHARS_BY_ROW; 166 | let src_rect = rect::Rect::new( 167 | (col * font::CHAR_WIDTH) as i32, 168 | (row * font::CHAR_HEIGHT) as i32, 169 | font::CHAR_WIDTH, 170 | font::CHAR_HEIGHT, 171 | ); 172 | let dst_rect = 173 | rect::Rect::new(x as i32, y as i32 + 3, src_rect.w as u32, src_rect.h as u32); 174 | font.set_color_mod(fg.0, fg.1, fg.2); 175 | if fg != bg { 176 | let bg_rect = rect::Rect::new( 177 | x as i32 - 1, 178 | y as i32 + 2, 179 | font::CHAR_WIDTH - 1, 180 | font::CHAR_HEIGHT + 1, 181 | ); 182 | canvas.set_draw_color(bg); 183 | canvas.fill_rect(bg_rect)?; 184 | } 185 | canvas.copy(font, src_rect, dst_rect) 186 | } 187 | 188 | pub fn draw_mapping<'a>( 189 | &mut self, 190 | canvas: &mut render::Canvas, 191 | font: &mut render::Texture<'a>, 192 | remapping: &Remap, 193 | ) -> Result<(), String> { 194 | if remapping.init { 195 | canvas.set_draw_color(self.bg); 196 | canvas.clear(); 197 | let y1 = m8::SCREEN_HEIGHT / 3; 198 | let color1 = (0x32, 0xec, 0xff); 199 | self.draw_string_centered(canvas, font, "CONFIG", y1 as u16, color1, color1)?; 200 | let y2 = m8::SCREEN_HEIGHT / 4 * 3; 201 | let color2 = (0x32, 0xec, 0xff); 202 | self.draw_string_centered(canvas, font, "ESC = ABORT", y2 as u16, color2, color2)?; 203 | } 204 | 205 | let color = (0x60, 0x60, 0x8e); 206 | let y1 = m8::SCREEN_HEIGHT / 2 - font::CHAR_HEIGHT; 207 | let y2 = m8::SCREEN_HEIGHT / 2 + font::CHAR_HEIGHT; 208 | let rect = rect::Rect::new(0, y1 as i32 + 6, m8::SCREEN_WIDTH, y2 + font::CHAR_HEIGHT - y1); 209 | canvas.set_draw_color(self.bg); 210 | canvas.fill_rect(rect)?; 211 | if remapping.done() { 212 | self.draw_string_centered(canvas, font, "- DONE -", y1 as u16, color, color)?; 213 | } else { 214 | let map = remapping.current(); 215 | self.draw_string_centered(canvas, font, map, y1 as u16, color, color)?; 216 | } 217 | 218 | if remapping.exists { 219 | let color = (0xff, 0x30, 0x70); 220 | self.draw_string_centered(canvas, font, "MAPPING EXISTS!", y2 as u16, color, color)?; 221 | } 222 | Ok(()) 223 | } 224 | 225 | pub fn is_fullscreen(&self, canvas: &render::Canvas) -> bool { 226 | !matches!(canvas.window().fullscreen_state(), video::FullscreenType::Off) 227 | } 228 | 229 | pub fn toggle_fullscreen( 230 | &self, 231 | canvas: &mut render::Canvas, 232 | ) -> Result<(), String> { 233 | match canvas.window().fullscreen_state() { 234 | video::FullscreenType::Off => { 235 | canvas.window_mut().set_fullscreen(video::FullscreenType::True) 236 | } 237 | _ => canvas.window_mut().set_fullscreen(video::FullscreenType::Off), 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/font.rs: -------------------------------------------------------------------------------- 1 | use sdl2::{pixels, render, video}; 2 | use std::mem; 3 | 4 | pub fn init( 5 | creator: &render::TextureCreator, 6 | ) -> Result { 7 | let mut font = creator 8 | .create_texture_static(pixels::PixelFormatEnum::ARGB8888, WIDTH as u32, HEIGHT as u32) 9 | .map_err(|e| e.to_string())?; 10 | 11 | let mut pixels32 = 12 | unsafe { *mem::MaybeUninit::<[u32; (WIDTH * HEIGHT) as usize]>::uninit().as_mut_ptr() }; 13 | for (i, p) in DATA.iter().enumerate() { 14 | for j in 0..8 { 15 | pixels32[i * 8 + j] = if ((*p as usize) & (1 << j)) == 0 { u32::MAX } else { 0 } 16 | } 17 | } 18 | let pixels = unsafe { pixels32.align_to::().1 }; 19 | font.update(None, pixels, WIDTH as usize * mem::size_of::()).map_err(|e| e.to_string())?; 20 | font.set_blend_mode(render::BlendMode::Blend); 21 | Ok(font) 22 | } 23 | 24 | pub fn width(len: usize) -> i32 { 25 | len as i32 * CHAR_WIDTH + CHAR_WIDTH 26 | } 27 | 28 | pub const WIDTH: i32 = 128; 29 | pub const HEIGHT: i32 = 64; 30 | pub const CHARS_BY_ROW: i32 = 16; 31 | pub const CHARS_BY_COL: i32 = 8; 32 | pub const CHAR_WIDTH: i32 = WIDTH / CHARS_BY_ROW; 33 | pub const CHAR_HEIGHT: i32 = HEIGHT / CHARS_BY_COL; 34 | const DATA: &[u8] = &[ 35 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 36 | 0xff, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 37 | 0xff, 0xee, 0xee, 0xee, 0xe6, 0xfe, 0xee, 0xe6, 0xfe, 0xee, 0xe6, 0xfe, 0xee, 0xe6, 0xfe, 0xee, 38 | 0xff, 0xee, 0xee, 0xfe, 0xfe, 0xee, 0xfe, 0xfe, 0xee, 0xfe, 0xfe, 0xee, 0xfe, 0xfe, 0xee, 0xfe, 39 | 0xff, 0xea, 0xee, 0xee, 0xef, 0xef, 0xee, 0xef, 0xef, 0xee, 0xef, 0xef, 0xee, 0xef, 0xef, 0xee, 40 | 0xff, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 41 | 0xff, 0xee, 0xee, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 42 | 0xff, 0xe0, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 43 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 44 | 0xe0, 0xe0, 0xe2, 0xe0, 0xe0, 0xe2, 0xe0, 0xe0, 0xe2, 0xe0, 0xe0, 0xe2, 0xe0, 0xe0, 0xe2, 0xe0, 45 | 0xfe, 0xfe, 0xee, 0xee, 0xe6, 0xee, 0xfe, 0xfe, 0xee, 0xee, 0xe6, 0xee, 0xfe, 0xfe, 0xee, 0xee, 46 | 0xee, 0xee, 0xee, 0xff, 0xfe, 0xee, 0xee, 0xee, 0xee, 0xff, 0xfe, 0xee, 0xee, 0xee, 0xee, 0xff, 47 | 0xef, 0xef, 0xfe, 0xee, 0xef, 0xee, 0xef, 0xef, 0xfe, 0xee, 0xef, 0xfe, 0xef, 0xef, 0xfe, 0xee, 48 | 0xee, 0xee, 0xee, 0xee, 0xfe, 0xee, 0xee, 0xee, 0xee, 0xee, 0xfe, 0xee, 0xee, 0xee, 0xee, 0xee, 49 | 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 50 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 51 | 0xff, 0xfe, 0xf5, 0xff, 0xfb, 0xff, 0xf9, 0xfb, 0xef, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 52 | 0xff, 0xfe, 0xf5, 0xf5, 0xe0, 0xec, 0xf6, 0xfb, 0xf7, 0xfd, 0xf5, 0xff, 0xff, 0xff, 0xff, 0xef, 53 | 0xff, 0xfe, 0xff, 0xe0, 0xfa, 0xf4, 0xfa, 0xff, 0xf7, 0xfd, 0xfb, 0xfb, 0xff, 0xff, 0xff, 0xf7, 54 | 0xff, 0xfe, 0xff, 0xf5, 0xe0, 0xfb, 0xed, 0xff, 0xf7, 0xfd, 0xf5, 0xf1, 0xff, 0xf1, 0xff, 0xfb, 55 | 0xff, 0xfe, 0xff, 0xe0, 0xeb, 0xe5, 0xea, 0xff, 0xf7, 0xfd, 0xff, 0xfb, 0xff, 0xff, 0xff, 0xfd, 56 | 0xff, 0xff, 0xff, 0xf5, 0xe0, 0xe6, 0xf6, 0xff, 0xf7, 0xfd, 0xff, 0xff, 0xfb, 0xff, 0xff, 0xfe, 57 | 0xff, 0xfe, 0xff, 0xff, 0xfb, 0xff, 0xe9, 0xff, 0xef, 0xfe, 0xff, 0xff, 0xfb, 0xff, 0xfb, 0xfe, 58 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 59 | 0xe0, 0xf8, 0xe0, 0xe0, 0xee, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 60 | 0xee, 0xfb, 0xef, 0xef, 0xee, 0xfe, 0xfe, 0xef, 0xee, 0xee, 0xff, 0xff, 0xf7, 0xff, 0xfd, 0xee, 61 | 0xe6, 0xfb, 0xef, 0xef, 0xee, 0xfe, 0xfe, 0xef, 0xee, 0xee, 0xfd, 0xfd, 0xfb, 0xf1, 0xfb, 0xef, 62 | 0xea, 0xfb, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xf7, 0xe0, 0xe0, 0xff, 0xff, 0xfd, 0xff, 0xf7, 0xf7, 63 | 0xec, 0xfb, 0xfe, 0xef, 0xef, 0xef, 0xee, 0xfb, 0xee, 0xef, 0xff, 0xff, 0xfb, 0xf1, 0xfb, 0xfb, 64 | 0xee, 0xfb, 0xfe, 0xef, 0xef, 0xef, 0xee, 0xfb, 0xee, 0xef, 0xfd, 0xfd, 0xf7, 0xff, 0xfd, 0xff, 65 | 0xe0, 0xe0, 0xe0, 0xe0, 0xef, 0xe0, 0xe0, 0xfb, 0xe0, 0xef, 0xff, 0xfd, 0xff, 0xff, 0xff, 0xfb, 66 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 67 | 0xf1, 0xf1, 0xf0, 0xf1, 0xf0, 0xe0, 0xe0, 0xf1, 0xee, 0xe0, 0xef, 0xee, 0xfe, 0xee, 0xee, 0xf1, 68 | 0xee, 0xee, 0xee, 0xee, 0xee, 0xfe, 0xfe, 0xee, 0xee, 0xfb, 0xef, 0xf6, 0xfe, 0xe4, 0xec, 0xee, 69 | 0xe2, 0xee, 0xee, 0xfe, 0xee, 0xfe, 0xfe, 0xfe, 0xee, 0xfb, 0xef, 0xfa, 0xfe, 0xea, 0xea, 0xee, 70 | 0xea, 0xe0, 0xf0, 0xfe, 0xee, 0xf0, 0xf0, 0xfe, 0xe0, 0xfb, 0xef, 0xfc, 0xfe, 0xee, 0xe6, 0xee, 71 | 0xe2, 0xee, 0xee, 0xfe, 0xee, 0xfe, 0xfe, 0xe6, 0xee, 0xfb, 0xee, 0xfa, 0xfe, 0xee, 0xee, 0xee, 72 | 0xfe, 0xee, 0xee, 0xee, 0xee, 0xfe, 0xfe, 0xee, 0xee, 0xfb, 0xee, 0xf6, 0xfe, 0xee, 0xee, 0xee, 73 | 0xf1, 0xee, 0xf0, 0xf1, 0xf0, 0xe0, 0xfe, 0xf1, 0xee, 0xe0, 0xf1, 0xee, 0xe0, 0xee, 0xee, 0xf1, 74 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 75 | 0xf0, 0xf1, 0xf0, 0xe1, 0xe0, 0xee, 0xee, 0xee, 0xee, 0xee, 0xe0, 0xe7, 0xfe, 0xfc, 0xfb, 0xff, 76 | 0xee, 0xee, 0xee, 0xfe, 0xfb, 0xee, 0xee, 0xee, 0xee, 0xee, 0xef, 0xf7, 0xfe, 0xfd, 0xf5, 0xff, 77 | 0xee, 0xee, 0xee, 0xfe, 0xfb, 0xee, 0xee, 0xee, 0xf5, 0xee, 0xf7, 0xf7, 0xfd, 0xfd, 0xff, 0xff, 78 | 0xf0, 0xee, 0xf0, 0xf1, 0xfb, 0xee, 0xee, 0xee, 0xfb, 0xe1, 0xfb, 0xf7, 0xfb, 0xfd, 0xff, 0xff, 79 | 0xfe, 0xea, 0xf6, 0xef, 0xfb, 0xee, 0xf5, 0xea, 0xf5, 0xef, 0xfd, 0xf7, 0xf7, 0xfd, 0xff, 0xff, 80 | 0xfe, 0xf6, 0xee, 0xef, 0xfb, 0xee, 0xf5, 0xe4, 0xee, 0xef, 0xfe, 0xf7, 0xef, 0xfd, 0xff, 0xff, 81 | 0xfe, 0xe9, 0xee, 0xf0, 0xfb, 0xf1, 0xfb, 0xee, 0xee, 0xf0, 0xe0, 0xe7, 0xef, 0xfc, 0xff, 0xe0, 82 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 83 | 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 84 | 0xfe, 0xff, 0xfe, 0xff, 0xef, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 85 | 0xff, 0xe1, 0xf0, 0xe1, 0xe1, 0xf1, 0xfd, 0xe0, 0xfe, 0xfb, 0xef, 0xee, 0xfe, 0xe4, 0xf0, 0xf1, 86 | 0xff, 0xee, 0xee, 0xfe, 0xee, 0xee, 0xfd, 0xee, 0xf0, 0xfb, 0xef, 0xf6, 0xfe, 0xea, 0xee, 0xee, 87 | 0xff, 0xee, 0xee, 0xfe, 0xee, 0xe0, 0xe0, 0xe0, 0xee, 0xfb, 0xef, 0xf8, 0xfe, 0xea, 0xee, 0xee, 88 | 0xff, 0xee, 0xee, 0xfe, 0xee, 0xfe, 0xfd, 0xef, 0xee, 0xfb, 0xef, 0xf6, 0xfe, 0xea, 0xee, 0xee, 89 | 0xff, 0xe1, 0xf0, 0xe1, 0xe1, 0xe1, 0xfd, 0xf1, 0xee, 0xf1, 0xf0, 0xee, 0xe1, 0xea, 0xee, 0xf1, 90 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 91 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0xfe, 0xe0, 0xff, 0xff, 92 | 0xff, 0xff, 0xff, 0xff, 0xfd, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xee, 0xfe, 0xee, 0xed, 0xff, 93 | 0xe0, 0xe1, 0xe1, 0xe1, 0xe0, 0xee, 0xee, 0xea, 0xee, 0xee, 0xe0, 0xee, 0xfe, 0xee, 0xf2, 0xff, 94 | 0xee, 0xee, 0xfe, 0xfe, 0xfd, 0xee, 0xee, 0xea, 0xf5, 0xee, 0xf7, 0xee, 0xfe, 0xea, 0xff, 0xff, 95 | 0xee, 0xee, 0xfe, 0xe0, 0xfd, 0xee, 0xee, 0xea, 0xfb, 0xe1, 0xfb, 0xee, 0xfe, 0xee, 0xff, 0xff, 96 | 0xf0, 0xe0, 0xfe, 0xef, 0xfd, 0xee, 0xf5, 0xea, 0xf5, 0xef, 0xfd, 0xee, 0xfe, 0xee, 0xff, 0xff, 97 | 0xfe, 0xef, 0xfe, 0xf0, 0xe3, 0xf1, 0xfb, 0xf5, 0xee, 0xf0, 0xe0, 0xe0, 0xfe, 0xe0, 0xff, 0xff, 98 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 99 | ]; 100 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde_derive; 3 | 4 | use sdl2::{ 5 | event::Event, 6 | keyboard::{Keycode, Mod}, 7 | pixels::PixelFormatEnum, 8 | video, 9 | }; 10 | use std::sync::{ 11 | atomic::{self, AtomicBool}, 12 | Arc, 13 | }; 14 | 15 | mod app; 16 | mod audio; 17 | mod cli; 18 | mod config; 19 | mod config_command; 20 | mod config_font; 21 | mod config_joystick; 22 | mod config_keycode; 23 | mod config_rgb; 24 | mod draw; 25 | mod font; 26 | mod m8; 27 | mod menu; 28 | mod menu_tools; 29 | mod nav; 30 | mod nav_item; 31 | mod nav_page; 32 | mod remap; 33 | mod slip; 34 | mod value; 35 | 36 | use app::App; 37 | use config::Rgb; 38 | use m8::M8; 39 | 40 | fn main() -> Result<(), String> { 41 | let running = Arc::new(AtomicBool::new(true)); 42 | let ctrlc_running = running.clone(); 43 | let mut app = App::new(running); 44 | 45 | ctrlc::set_handler(move || ctrlc_running.store(false, atomic::Ordering::SeqCst)) 46 | .map_err(|e| e.to_string())?; 47 | 48 | // process command line arguments 49 | let mut device: Option = None; 50 | let mut config_file: Option = None; 51 | let mut capture: Option = None; 52 | let mut samples: Option = None; 53 | let mut noaudio = false; 54 | if !cli::handle_command_line( 55 | app.config_mut(), 56 | &mut config_file, 57 | &mut device, 58 | &mut capture, 59 | &mut samples, 60 | &mut noaudio, 61 | )? { 62 | return Ok(()); 63 | } 64 | 65 | // detect and connect to M8 66 | let mut m8 = match device { 67 | Some(dev) => M8::open(dev), 68 | None => M8::detect(), 69 | } 70 | .map_err(|e| e.to_string())?; 71 | m8.set_reconnect(app.config().app.reconnect); 72 | m8.enable_and_reset_display()?; 73 | m8.keyjazz.set(!app.config().overlap); 74 | 75 | app.build_menu(&m8); 76 | 77 | let sdl_context = sdl2::init()?; 78 | let joystick_subsystem = sdl_context.joystick()?; 79 | let video_subsystem = sdl_context.video()?; 80 | let audio_subsystem = sdl_context.audio()?; 81 | let zoom = app.config().app.zoom; 82 | let mut window = video_subsystem 83 | .window("rm8", m8::SCREEN_WIDTH * zoom, m8::SCREEN_HEIGHT * zoom) 84 | .position_centered() 85 | .opengl() 86 | .resizable() 87 | .build() 88 | .map_err(|e| e.to_string())?; 89 | window.set_fullscreen(if app.config().app.fullscreen { 90 | video::FullscreenType::True 91 | } else { 92 | video::FullscreenType::Off 93 | })?; 94 | 95 | if !noaudio { 96 | m8.connect_audio(audio::Audio::open(&audio_subsystem, capture, samples)?); 97 | } 98 | 99 | let mut canvas = window.into_canvas().accelerated().build().map_err(|e| e.to_string())?; 100 | canvas.set_logical_size(m8::SCREEN_WIDTH, m8::SCREEN_HEIGHT).map_err(|e| e.to_string())?; 101 | 102 | let creator = canvas.texture_creator(); 103 | let mut texture = creator 104 | .create_texture_target(PixelFormatEnum::ARGB8888, m8::SCREEN_WIDTH, m8::SCREEN_HEIGHT) 105 | .map_err(|e| e.to_string())?; 106 | 107 | let mut font = font::init(&creator)?; 108 | 109 | let mut event_pump = sdl_context.event_pump()?; 110 | while app.running() { 111 | for event in event_pump.poll_iter() { 112 | match event { 113 | Event::Quit { .. } => { 114 | app.quit(); 115 | continue; 116 | } 117 | Event::KeyDown { keycode: Some(keycode), keymod, repeat, .. } => { 118 | if keycode == Keycode::Escape { 119 | app.escape_command(&mut m8, &mut canvas)?; 120 | continue; 121 | } 122 | if repeat || app.remap_mode() { 123 | continue; 124 | } 125 | if keymod.intersects(Mod::LALTMOD | Mod::RALTMOD) { 126 | match keycode { 127 | Keycode::Return => { 128 | draw::toggle_fullscreen(&mut canvas)?; 129 | continue; 130 | } 131 | Keycode::C if !app.config_mode() => { 132 | app.start_config_mode(); 133 | m8.reset_display()?; 134 | continue; 135 | } 136 | Keycode::R if !app.config_mode() => { 137 | m8.reset(keymod.intersects(Mod::LSHIFTMOD | Mod::RSHIFTMOD))?; 138 | continue; 139 | } 140 | _ => {} 141 | } 142 | } 143 | 144 | if !app.config_mode() { 145 | let config = app.config(); 146 | if keycode == *config.rm8.keyjazz { 147 | m8.keyjazz.toggle(); 148 | } 149 | if !config.overlap || *m8.keyjazz { 150 | if let Some(n) = config.keyjazz.get(&keycode.into()) { 151 | m8.set_note(*n); 152 | } 153 | } 154 | } 155 | app.handle_key(&mut m8, keycode, keymod, false); 156 | } 157 | Event::KeyUp { keycode: Some(keycode), keymod, .. } => { 158 | if app.remap_mode() { 159 | if app.remap(keycode) { 160 | app.cancel_remap_mode(); 161 | app.action_modified(&mut canvas, &mut m8, &joystick_subsystem)?; 162 | } 163 | continue; 164 | } 165 | app.handle_key(&mut m8, keycode, keymod, true); 166 | } 167 | Event::JoyAxisMotion { which, axis_idx, value, .. } => { 168 | app.handle_cmd(&mut m8, app.axis_cmd(which, axis_idx, value)); 169 | } 170 | Event::JoyHatMotion { which, state, .. } => { 171 | app.handle_cmd(&mut m8, app.hat_cmd(which, state)); 172 | } 173 | Event::JoyButtonDown { which, button_idx, .. } => { 174 | app.handle_cmd(&mut m8, app.button_cmd(which, button_idx, false)); 175 | } 176 | Event::JoyButtonUp { which, button_idx, .. } => { 177 | app.handle_cmd(&mut m8, app.button_cmd(which, button_idx, true)); 178 | } 179 | Event::JoyDeviceAdded { which, .. } => { 180 | app.add_joystick(&joystick_subsystem, which); 181 | } 182 | Event::JoyDeviceRemoved { which, .. } => { 183 | app.rem_joystick(&joystick_subsystem, which) 184 | } 185 | _ => (), 186 | } 187 | } 188 | 189 | app.process_key(&mut m8); 190 | app.handle_defer(&mut m8, &mut canvas)?; 191 | if app.sync() { 192 | if app.config_mode() { 193 | app.process_action(&mut canvas, &mut m8, &joystick_subsystem, &config_file)?; 194 | 195 | canvas 196 | .with_texture_canvas(&mut texture, |target| { 197 | let config = app.config(); 198 | let ctx = &mut draw::Context { 199 | canvas: target, 200 | font: &mut font, 201 | theme: config.theme, 202 | font_option: config.app.font, 203 | screen_bg: None, 204 | }; 205 | let _ = app.render(ctx); 206 | let _ = app.render_fps(ctx); 207 | }) 208 | .map_err(|e| e.to_string())?; 209 | } else { 210 | if m8.note.changed() { 211 | m8.send_keyjazz()?; 212 | } 213 | if m8.keys.changed() { 214 | m8.send_keys()?; 215 | } 216 | 217 | canvas 218 | .with_texture_canvas(&mut texture, |target| { 219 | let config = app.config(); 220 | let ctx = &mut draw::Context { 221 | canvas: target, 222 | font: &mut font, 223 | theme: config.theme, 224 | font_option: config.app.font, 225 | screen_bg: None, 226 | }; 227 | while let Ok(Some(cmd)) = m8.read() { 228 | let _ = match cmd { 229 | m8::Command::Joypad { .. } => Ok(()), 230 | m8::Command::Waveform(fg, data) => ctx.draw_waveform(data, fg), 231 | m8::Command::Character(c, x, y, fg, bg) => ctx.draw_char( 232 | c, 233 | x as i32, 234 | y as i32, 235 | Rgb::from_tuple(fg), 236 | Rgb::from_tuple(bg), 237 | ), 238 | m8::Command::Rectangle(x, y, w, h, bg) => ctx.draw_rect( 239 | (x as i32, y as i32, w as u32, h as u32), 240 | Rgb::from_tuple(bg), 241 | ), 242 | }; 243 | } 244 | if m8.disconnected() { 245 | let _ = ctx.clear(); 246 | let fg = ctx.theme.text_info; 247 | let _ = ctx.draw_str_centered( 248 | "M8 LOST", 249 | m8::SCREEN_HEIGHT as i32 / 2, 250 | fg, 251 | fg, 252 | ); 253 | } 254 | let (kc, vc, oc) = 255 | (m8.keyjazz.changed(), m8.velocity.changed(), m8.octave.changed()); 256 | if kc || vc { 257 | let _ = ctx.draw_velocity(*m8.velocity, *m8.keyjazz); 258 | } 259 | if kc || oc { 260 | let _ = ctx.draw_octave(*m8.octave, *m8.keyjazz); 261 | } 262 | if let Some(bg) = ctx.screen_bg { 263 | app.config_mut().theme.screen = bg; 264 | } 265 | let _ = app.render_fps(ctx); 266 | }) 267 | .map_err(|e| e.to_string())?; 268 | } 269 | 270 | canvas.set_draw_color(app.config().theme.screen.rgb()); 271 | canvas.clear(); 272 | canvas.copy(&texture, None, None)?; 273 | canvas.present(); 274 | } 275 | } 276 | 277 | Ok(()) 278 | } 279 | -------------------------------------------------------------------------------- /src/m8.rs: -------------------------------------------------------------------------------- 1 | use serialport::{available_ports, ErrorKind, SerialPort, SerialPortType}; 2 | use std::{io, time::Duration}; 3 | 4 | use crate::{ 5 | audio::{self, Audio}, 6 | slip::Slip, 7 | value::Value, 8 | }; 9 | 10 | const VENDOR_ID: u16 = 0x16c0; 11 | const PRODUCT_ID: u16 = 0x048a; 12 | const JOYPYAD_CMD: u8 = 0xfb; 13 | const WAVEFORM_CMD: u8 = 0xfc; 14 | const CHARACTER_CMD: u8 = 0xfd; 15 | const RECTANGLE_CMD: u8 = 0xfe; 16 | pub const SCREEN_WIDTH: u32 = 320; 17 | pub const SCREEN_HEIGHT: u32 = 240; 18 | pub const WAVEFORM_HEIGHT: u32 = 22; 19 | const MIN_OCTAVE: u8 = 0; 20 | const MAX_OCTAVE: u8 = 10; 21 | const MIN_VELOCITY: u8 = 0; 22 | const MAX_VELOCITY: u8 = 127; 23 | pub const KEY_EDIT: u8 = 1; 24 | pub const KEY_OPTION: u8 = 1 << 1; 25 | pub const KEY_RIGHT: u8 = 1 << 2; 26 | pub const KEY_PLAY: u8 = 1 << 3; 27 | pub const KEY_SHIFT: u8 = 1 << 4; 28 | pub const KEY_DOWN: u8 = 1 << 5; 29 | pub const KEY_UP: u8 = 1 << 6; 30 | pub const KEY_LEFT: u8 = 1 << 7; 31 | pub const KEY_DIR: u8 = KEY_UP | KEY_DOWN | KEY_LEFT | KEY_RIGHT; 32 | 33 | pub enum Command<'a> { 34 | #[allow(dead_code)] 35 | Joypad(u8), 36 | Waveform((u8, u8, u8), &'a [u8]), 37 | Character(u8, u16, u16, (u8, u8, u8), (u8, u8, u8)), 38 | Rectangle(u16, u16, u16, u16, (u8, u8, u8)), 39 | } 40 | 41 | pub struct M8 { 42 | port: Box, 43 | buf: [u8; 324], 44 | reconnect: bool, 45 | lost: bool, 46 | slip: Slip<1024>, 47 | pub keyjazz: Value, 48 | pub note: Value, 49 | pub octave: Value, 50 | pub velocity: Value, 51 | pub keys: Value, 52 | audio: Option