├── screenshot.png ├── .gitignore ├── Marche_aux_Flambeaux.mid ├── cargo_watch.sh ├── .github └── workflows │ └── rust.yml ├── Cargo.toml ├── LICENSE ├── src ├── usage.rs ├── sdl_event_processor.rs ├── scroller.rs ├── time_controller.rs ├── main.rs ├── midi_sequencer.rs ├── midi_container.rs ├── draw_engine.rs ├── stderrlog.rs └── app_control.rs └── README.md /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gin66/rusthesia/HEAD/screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/* 2 | */target/* 3 | Cargo.lock 4 | */Cargo.lock 5 | *.mid 6 | *.mp4 7 | *.txt 8 | -------------------------------------------------------------------------------- /Marche_aux_Flambeaux.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gin66/rusthesia/HEAD/Marche_aux_Flambeaux.mid -------------------------------------------------------------------------------- /cargo_watch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cargo watch -w src -w Cargo.toml -w sdl2_timing -x build -x test -x doc 3 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Prepare 20 | run: sudo apt-get install librtaudio-dev libsdl2-2.0 cmake libfreetype6-dev libsdl2-dev libsdl2-gfx-dev libsdl2-ttf-dev libfontconfig1-dev 21 | - name: Build 22 | run: cargo build --verbose 23 | - name: Run tests 24 | run: cargo test --verbose 25 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rusthesia" 3 | version = "0.1.21" 4 | authors = ["Jochen Kiemes "] 5 | edition = "2018" 6 | license = "MIT" 7 | description = "Reads midi files and create piano notes waterfall." 8 | homepage = "https://github.com/gin66/rusthesia" 9 | repository = "https://github.com/gin66/rusthesia" 10 | keywords = ["music", "midi", "piano", "learning", "sdl2"] 11 | readme = "README.md" 12 | documentation = "https://docs.rs/releases/search?query=rusthesia" 13 | 14 | [dependencies] 15 | midir = "0.5" 16 | midly = "0.4" 17 | clap = "2.32" 18 | indoc = "0.3" 19 | font-kit = "0.5" 20 | piano_keyboard = "0.2" 21 | sdl2_timing = "0.2" 22 | 23 | # Dependencies for hacked stderrlog 24 | chrono = "0.4" 25 | termcolor = "1.1" 26 | thread_local = "1.0" 27 | 28 | [dependencies.sdl2] 29 | version = "0.33" 30 | default-features = false 31 | features = ["gfx","ttf"] 32 | 33 | [dependencies.log] 34 | version = "0.4" 35 | features = ["std"] 36 | 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 J.Kiemes 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/usage.rs: -------------------------------------------------------------------------------- 1 | use clap::crate_version; 2 | use clap::{App, Arg}; 3 | use indoc::indoc; 4 | 5 | pub fn usage() -> clap::ArgMatches<'static> { 6 | App::new("Rusthesia") 7 | .version(crate_version!()) 8 | //.author(crate_authors!("\n")) 9 | .about(indoc!( 10 | " 11 | Reads midi files and creates piano notes waterfall. 12 | 13 | Valid key commands, while playing: 14 | Transpose half tone lower 15 | Transpose half tone higher 16 | Go forward some time 17 | Go back some time 18 | <+> Faster 19 | <-> Slower 20 | Pause/continue playing 21 | 22 | Gestures: 23 | Two finger scrolling to move forward/backwards 24 | 25 | For playing midi without output, leave out '-s' option 26 | " 27 | )) 28 | .arg( 29 | Arg::with_name("transpose") 30 | .short("t") 31 | .long("transpose") 32 | .takes_value(true) 33 | .default_value("0") 34 | .help(indoc!( 35 | "Set number of note steps to transpose. 36 | For negative numbers use: -t=-12" 37 | )), 38 | ) 39 | .arg( 40 | Arg::with_name("play") 41 | .required_unless("list") 42 | .short("p") 43 | .long("play-tracks") 44 | .takes_value(true) 45 | .multiple(true) 46 | .help("Output these tracks as midi"), 47 | ) 48 | .arg( 49 | Arg::with_name("show") 50 | .short("s") 51 | .long("show-tracks") 52 | .takes_value(true) 53 | .multiple(true) 54 | .help("Show the tracks as falling notes"), 55 | ) 56 | .arg( 57 | Arg::with_name("list") 58 | .short("l") 59 | .long("list-tracks") 60 | .help("List the tracks in the midi file"), 61 | ) 62 | .arg( 63 | Arg::with_name("RD64") 64 | .long("rd64") 65 | .help("Select 64 key Piano like Roland RD-64"), 66 | ) 67 | .arg( 68 | Arg::with_name("MIDI") 69 | .help("Sets the midi file to use") 70 | .required(true) 71 | .index(1), 72 | ) 73 | .arg( 74 | Arg::with_name("debug") 75 | .takes_value(true) 76 | .multiple(true) 77 | .help("List of modules/targets to debug") 78 | .short("d"), 79 | ) 80 | .arg(Arg::with_name("verbose").multiple(true).short("v")) 81 | .arg( 82 | Arg::with_name("quiet") 83 | .short("q") 84 | .help("No logging output at all"), 85 | ) 86 | .get_matches() 87 | } 88 | -------------------------------------------------------------------------------- /src/sdl_event_processor.rs: -------------------------------------------------------------------------------- 1 | use log::*; 2 | 3 | use sdl2::event::Event; 4 | use sdl2::keyboard::Keycode; 5 | 6 | use crate::app_control::AppControl; 7 | 8 | pub fn process_event(event: Event, control: &mut AppControl) -> bool { 9 | match event { 10 | Event::Window { win_event, .. } => { 11 | trace!("Unprocessed window Event: {:?}", win_event); 12 | } 13 | Event::Quit { .. } 14 | | Event::KeyDown { 15 | keycode: Some(Keycode::Escape), 16 | .. 17 | } => return false, 18 | Event::KeyDown { 19 | keycode: Some(Keycode::Space), 20 | .. 21 | } => { 22 | control.toggle_play(); 23 | } 24 | Event::KeyDown { 25 | keycode: Some(Keycode::Plus), 26 | .. 27 | } => { 28 | control.modify_scaling(true); 29 | } 30 | Event::TextInput { text: ref key, .. } if key == &"+".to_string() => { 31 | control.modify_scaling(true); 32 | } 33 | Event::KeyDown { 34 | keycode: Some(Keycode::Minus), 35 | .. 36 | } => { 37 | control.modify_scaling(false); 38 | } 39 | Event::TextInput { text: ref key, .. } if key == &"-".to_string() => { 40 | control.modify_scaling(false); 41 | } 42 | Event::KeyDown { 43 | keycode: Some(Keycode::Up), 44 | .. 45 | } => { 46 | control.change_position(true); 47 | } 48 | Event::KeyDown { 49 | keycode: Some(Keycode::Down), 50 | .. 51 | } => { 52 | control.change_position(false); 53 | } 54 | Event::KeyDown { 55 | keycode: Some(Keycode::Left), 56 | .. 57 | } => { 58 | control.tune_up(false); 59 | } 60 | Event::KeyDown { 61 | keycode: Some(Keycode::Right), 62 | .. 63 | } => { 64 | control.tune_up(true); 65 | } 66 | Event::MultiGesture { 67 | timestamp: _timestamp, 68 | touch_id: _touch_id, 69 | x: _x, 70 | y, 71 | num_fingers, 72 | .. 73 | } => { 74 | //finger_msg = format!( 75 | // "t={} id={} fid={} x={:.2} y={:.2}", 76 | // timestamp, touch_id, num_fingers, x, y 77 | //); 78 | trace!("Finger {} {}", y, num_fingers); 79 | if num_fingers == 2 { 80 | control.two_finger_scroll_start(y); 81 | } 82 | } 83 | Event::FingerDown { 84 | timestamp: _timestamp, 85 | touch_id: _touch_id, 86 | finger_id: _finger_id, 87 | x: _x, 88 | y: _y, 89 | dx: _dx, 90 | dy: _dy, 91 | pressure: _pressure, 92 | } => { 93 | control.finger_touch(); 94 | } 95 | Event::FingerUp { 96 | timestamp: _timestamp, 97 | touch_id: _touch_id, 98 | finger_id: _finger_id, 99 | x: _x, 100 | y: _y, 101 | dx: _dx, 102 | dy: _dy, 103 | pressure: _pressure, 104 | } => { 105 | control.finger_up(); 106 | } 107 | Event::FingerMotion { 108 | timestamp: _timestamp, 109 | touch_id: _touch_id, 110 | finger_id: _finger_id, 111 | x: _x, 112 | y: _y, 113 | dx: _dx, 114 | dy: _dy, 115 | pressure: _pressure, 116 | } => { 117 | //finger_msg = format!("t={} id={} fid={} x={:.2} y={:.2} dx={:.2} dy={:.2}", 118 | // timestamp, touch_id, finger_id, 119 | // x,y,dx,dy); 120 | } 121 | _ => {} 122 | } 123 | true 124 | } 125 | -------------------------------------------------------------------------------- /src/scroller.rs: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/questions/4262221/duration-for-kinetic-scrolling-momentum-based-on-velocity 2 | // 3 | use std::time::Instant; 4 | 5 | use log::*; 6 | 7 | const SCROLL_TIME_MS: u32 = 1_950; 8 | const TIME_CONSTANT_MS: f32 = 10.0; 9 | 10 | enum ScrollerState { 11 | Inactive, 12 | Scrolling(Instant), 13 | FreeRunning(Instant), 14 | } 15 | 16 | pub struct Scroller { 17 | state: ScrollerState, 18 | time_ms: u32, 19 | start_y: f32, 20 | last_y: f32, 21 | scale_factor: f32, 22 | last_position: f32, 23 | amplitude: f32, 24 | } 25 | 26 | impl Scroller { 27 | pub fn new(scale_factor: f32) -> Scroller { 28 | Scroller { 29 | state: ScrollerState::Inactive, 30 | time_ms: 0, 31 | start_y: 0.0, 32 | last_y: 0.0, 33 | last_position: 0.0, 34 | amplitude: 0.0, 35 | scale_factor, 36 | } 37 | } 38 | pub fn stop(&mut self) -> bool { 39 | let scrolling = match self.state { 40 | ScrollerState::FreeRunning(_) => true, 41 | _ => false, 42 | }; 43 | self.state = ScrollerState::Inactive; 44 | scrolling 45 | } 46 | pub fn update_move(&mut self, y: f32) -> bool { 47 | let (state, moving) = match self.state { 48 | ScrollerState::FreeRunning(_) | ScrollerState::Inactive => { 49 | trace!("Update move"); 50 | self.start_y = y; 51 | self.last_y = y; 52 | self.time_ms = 0; 53 | self.last_position = y * self.scale_factor; 54 | self.amplitude = 0.0; 55 | (ScrollerState::Scrolling(Instant::now()), false) 56 | } 57 | ScrollerState::Scrolling(stamp) => { 58 | self.time_ms = stamp.elapsed().subsec_millis(); 59 | trace!("Update move"); 60 | self.last_y = y; 61 | let initial_velocity = (y - self.start_y) * 1000.0 / self.time_ms as f32; 62 | self.amplitude = initial_velocity * self.scale_factor; 63 | (ScrollerState::Scrolling(stamp), true) 64 | } 65 | }; 66 | self.state = state; 67 | moving 68 | } 69 | pub fn end_move(&mut self) { 70 | self.state = match self.state { 71 | ScrollerState::Inactive => ScrollerState::Inactive, 72 | ScrollerState::FreeRunning(_) | ScrollerState::Scrolling(_) => { 73 | self.time_ms = 0; 74 | ScrollerState::FreeRunning(Instant::now()) 75 | } 76 | } 77 | } 78 | pub fn update_position(&mut self) -> Option<(bool, f32)> { 79 | let (state, result) = match self.state { 80 | ScrollerState::Inactive => (ScrollerState::Inactive, None), 81 | ScrollerState::Scrolling(stamp) => { 82 | let new_position = self.last_y * self.scale_factor; 83 | let delta = new_position - self.last_position; 84 | self.last_position = new_position; 85 | trace!("Scroll delta = {}", delta); 86 | (ScrollerState::Scrolling(stamp), Some((false, delta))) 87 | } 88 | ScrollerState::FreeRunning(stamp) => { 89 | let dt_ms = stamp.elapsed().subsec_millis(); 90 | self.time_ms += dt_ms; 91 | let delta = self.amplitude / TIME_CONSTANT_MS; 92 | trace!("Freerunning delta = {}", delta); 93 | self.amplitude -= delta; 94 | if self.time_ms < SCROLL_TIME_MS { 95 | trace!("time = {} Scroll delta = {}", self.time_ms, delta); 96 | ( 97 | ScrollerState::FreeRunning(Instant::now()), 98 | Some((false, delta)), 99 | ) 100 | } else { 101 | (ScrollerState::Inactive, Some((true, delta))) 102 | } 103 | } 104 | }; 105 | self.state = state; 106 | result 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/time_controller.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex, MutexGuard}; 2 | use std::time::{Duration, Instant}; 3 | 4 | pub struct RefPosition { 5 | pos_us: i64, 6 | at_instant: Option, 7 | scaling_1000: u16, 8 | } 9 | impl RefPosition { 10 | pub fn set_pos_us(&mut self, pos_us: i64) { 11 | self.pos_us = pos_us; 12 | if self.at_instant.is_some() { 13 | self.at_instant = Some(Instant::now()); 14 | } 15 | } 16 | pub fn get_pos_us(&self) -> i64 { 17 | match self.at_instant.as_ref() { 18 | None => self.pos_us, 19 | Some(instant) => { 20 | let elapsed = instant.elapsed(); 21 | let mut elapsed_us = elapsed.subsec_micros() as i64; 22 | elapsed_us += elapsed.as_secs() as i64 * 1_000_000; 23 | let scaled_us = elapsed_us * self.scaling_1000 as i64 / 1000; 24 | self.pos_us + scaled_us 25 | } 26 | } 27 | } 28 | pub fn get_pos_us_after(&self, duration: Duration) -> i64 { 29 | match self.at_instant.as_ref() { 30 | None => self.pos_us, 31 | Some(instant) => { 32 | let elapsed = instant.elapsed() + duration; 33 | let mut elapsed_us = elapsed.subsec_micros() as i64; 34 | elapsed_us += elapsed.as_secs() as i64 * 1_000_000; 35 | let scaled_us = elapsed_us * self.scaling_1000 as i64 / 1000; 36 | self.pos_us + scaled_us 37 | } 38 | } 39 | } 40 | fn advance_to_now(&mut self) { 41 | let pos_us = self.get_pos_us(); 42 | self.set_pos_us(pos_us); 43 | } 44 | pub fn set_scaling_1000(&mut self, new_scale: u16) { 45 | self.advance_to_now(); 46 | self.scaling_1000 = new_scale; 47 | } 48 | pub fn start(&mut self) { 49 | self.at_instant = Some(Instant::now()); 50 | } 51 | #[allow(dead_code)] 52 | pub fn is_running(&self) -> bool { 53 | self.at_instant.is_some() 54 | } 55 | pub fn stop(&mut self) { 56 | self.advance_to_now(); 57 | self.at_instant = None; 58 | } 59 | pub fn ms_till_pos(&self, next_pos_us: i64) -> Option { 60 | let pos_us = self.get_pos_us(); 61 | if pos_us > next_pos_us { 62 | None 63 | } else { 64 | let rem_us = next_pos_us - pos_us; 65 | let scaled_ms = rem_us / self.scaling_1000 as i64; 66 | if scaled_ms == 0 { 67 | None 68 | } else { 69 | Some(scaled_ms as u32) 70 | } 71 | } 72 | } 73 | } 74 | 75 | pub trait TimeListenerTrait { 76 | fn get_locked(&self) -> Option>; 77 | fn get_pos_us(&self) -> i64 { 78 | self.get_locked().unwrap().get_pos_us() 79 | } 80 | fn get_pos_us_after(&self, duration: Duration) -> i64 { 81 | self.get_locked().unwrap().get_pos_us_after(duration) 82 | } 83 | fn is_running(&self) -> bool { 84 | self.get_locked().unwrap().is_running() 85 | } 86 | fn ms_till_pos(&self, next_pos_us: i64) -> Option { 87 | self.get_locked().unwrap().ms_till_pos(next_pos_us) 88 | } 89 | } 90 | 91 | #[derive(Clone)] 92 | pub struct TimeListener { 93 | ref_pos: Arc>, 94 | } 95 | impl TimeListenerTrait for TimeListener { 96 | fn get_locked(&self) -> Option> { 97 | self.ref_pos.lock().ok() 98 | } 99 | } 100 | 101 | pub struct TimeController { 102 | ref_pos: Arc>, 103 | } 104 | impl TimeController { 105 | pub fn new() -> TimeController { 106 | TimeController { 107 | ref_pos: Arc::new(Mutex::new(RefPosition { 108 | pos_us: 0, 109 | at_instant: None, 110 | scaling_1000: 1000, 111 | })), 112 | } 113 | } 114 | pub fn new_listener(&self) -> TimeListener { 115 | TimeListener { 116 | ref_pos: self.ref_pos.clone(), 117 | } 118 | } 119 | pub fn set_pos_us(&self, pos_us: i64) { 120 | self.get_locked().unwrap().set_pos_us(pos_us); 121 | } 122 | pub fn set_scaling_1000(&self, new_scale: u16) { 123 | self.get_locked().unwrap().set_scaling_1000(new_scale); 124 | } 125 | pub fn start(&self) { 126 | self.get_locked().unwrap().start(); 127 | } 128 | pub fn stop(&self) { 129 | self.get_locked().unwrap().stop(); 130 | } 131 | } 132 | impl TimeListenerTrait for TimeController { 133 | fn get_locked(&self) -> Option> { 134 | self.ref_pos.lock().ok() 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rusthesia 2 | 3 | ![Build](https://github.com/gin66/rusthesia/workflows/Rust/badge.svg) 4 | ![Rust](https://github.com/gin66/rusthesia/workflows/Rust/badge.svg) 5 | [![dependency status](https://deps.rs/repo/github/gin66/rusthesia/status.svg)](https://deps.rs/repo/github/gin66/rusthesia) 6 | 7 | Rusthesia is till now just a hack to play a midi file, created from Logic Pro/X, and display a window with falling notes down onto a piano. 8 | 9 | The midi file can be transposed in half notes in realtime by using shift left/right key. Playing can be paused by space. 10 | 11 | Please note: without the "-s" commandline option, no window will be shown ! 12 | 13 | ### Audio 14 | 15 | Synthesizer is not included. Instead midi messages will be sent via core audio. Logic Pro/X can be used for playing the midi, but has to be set up accordingly. 16 | 17 | No idea for other operation systems, if it works or how to do. 18 | 19 | ### Video 20 | 21 | Screen output uses sdl2. 22 | 23 | ### Screenshot 24 | 25 | ![Screenshot](screenshot.png) 26 | 27 | ## Preparation 28 | 29 | The sdl2 libraries need to be installed. On macos this can be done by: 30 | 31 | ``` 32 | brew install sdl2 sdl2_gfx sdl2_ttf 33 | ``` 34 | 35 | ## Installation 36 | 37 | ``` 38 | cargo install rusthesia 39 | ``` 40 | 41 | ## Usage 42 | 43 | For help just execute 44 | 45 | ``` 46 | rusthesia -h 47 | ``` 48 | 49 | As an example the midi-file 50 | [Marche_aux_Flambeaux.mid](http://www.mutopiaproject.org/cgibin/make-table.cgi?Instrument=Harmonium) 51 | is included. As per that website, this file is in the public domain. 52 | 53 | First list the available tracks: 54 | ``` 55 | > rusthesia Marche_aux_Flambeaux.mid -l 56 | Track 0: 57 | Text: Creator: GNU LilyPond 2.8.7 58 | Text: Generated automatically by: GNU LilyPond 2.8.7 59 | Text: at Mon Oct 16 20:41:39 2006 60 | Text: at Mon Oct 16 20:41:39 2006 61 | Track name: Track 0 62 | Track 1: 63 | Track name: upper 64 | Instrument name: accordion 65 | Track 2: 66 | Track name: lower 67 | Instrument name: accordion 68 | ``` 69 | 70 | For playing and displaying all tracks use: 71 | ``` 72 | > rusthesia Marche_aux_Flambeaux.mid -p 0 1 2 -s 0 1 2 73 | ``` 74 | 75 | In order to play the lower and show only the upper, use the following command: 76 | ``` 77 | > rusthesia Marche_aux_Flambeaux.mid -p 1 -s 2 78 | ``` 79 | 80 | Only use the midi player function without graphic output: 81 | ``` 82 | > rusthesia Marche_aux_Flambeaux.mid -p 1 83 | ``` 84 | 85 | To get info about the event loop in regard to timing debug flags can be added: 86 | ``` 87 | > rusthesia Marche_aux_Flambeaux.mid -p 1 -vvv -d eventloop 88 | ``` 89 | 90 | ## Todo 91 | 92 | Todo list is managed under [projects](https://github.com/gin66/rusthesia/projects) 93 | 94 | ## Synthesizer 95 | 96 | ### macos 97 | 98 | #### Logic Pro/X 99 | 100 | Have not been able to use Logic Pro/X as a synthesizer with channels assigned to different instruments. Still keep looking for the needed hidden feature. 101 | 102 | #### MainStage 103 | 104 | Works, but need to create a concert with keyboard per channel, which is not very convenient. 105 | 106 | #### Connected piano (Roland RD-64) with synthesizer 107 | 108 | As rusthesia offered to use my Roland RD-64 as output, I have given it a try. 109 | Very positively surprised, that it works pretty well. Even can play along by hand to the song. 110 | 111 | #### fluidsynth 112 | 113 | Good choice too is to use fluidsynth 114 | 115 | ```bash 116 | brew install fluid-synth 117 | ``` 118 | 119 | For playing need still a soundfont .sf2 file, which can be found in the internet. Start synthesizer with: 120 | 121 | ```bash 122 | fluidsynth your_soundfound.sf2 123 | ``` 124 | 125 | Now can start rusthesia and the midi-output should appear. 126 | 127 | ``` 128 | Available output ports: 129 | 0: IAC-Treiber IAC-Bus 1 130 | 1: FluidSynth virtual port (3776) 131 | ``` 132 | 133 | Just enter 1 for this case. 134 | 135 | ### Linux 136 | 137 | As per info from Samuel Da Mota, the code works on linux: 138 | 139 | > No need to change a single 140 | > line of code or configuration whatsoever. One only need to install the 141 | > libsdl2-dev and libsd2-gfx-dev packages for your project to build. And 142 | > then to install a system wide midi sequencer such as timidity and run 143 | > it (using timidity -iA) to get music being played. 144 | 145 | In the meantime have tried on deepin linux, which is IMHO based on Debian. 146 | This steps to be executed for compilation: 147 | 148 | ``` 149 | sudo apt install librtaudio-dev libsdl2-2.0 cmake libfreetype6-dev libsdl2-dev libsdl2-gfx-dev libsdl2-ttf-dev libfontconfig1-dev 150 | ``` 151 | 152 | Unfortunatly it does not work for these issues: 153 | 154 | * Long pause before windows comes up 155 | * Super slow 156 | * No output if using timidity -A 157 | 158 | Need further debugging. 159 | 160 | Performance measurement using: 161 | 162 | ``` 163 | cargo run --release Marche_aux_Flambeaux.mid -p 0 1 2 -s 0 1 2 -d eventloop -vvvv 164 | ```` 165 | 166 | without mouse movements/gestures yields: 167 | 168 | ``` 169 | Sleep time 1 ms - 1 times 170 | Sleep time 14 ms - 1 times 171 | Sleep time 15 ms - 1 times 172 | Sleep time 16 ms - 1 times 173 | Sleep time 17 ms - 1 times 174 | Sleep time 22 ms - 24 times 175 | Sleep time 23 ms - 81 times 176 | Sleep time 24 ms - 151 times 177 | Sleep time 25 ms - 285 times 178 | Sleep time 26 ms - 1736 times 179 | Sleep time 27 ms - 4152 times 180 | Sleep time 28 ms - 2 times 181 | Sleep time 37 ms - 1 times 182 | Sleep time 38 ms - 1 times 183 | Lost frames: 2 184 | min= 0us avg= 1us max= 64us control at loop start 185 | min= 0us avg= 1us max= 41us keyboard built 186 | min= 0us avg= 0us max= 1531us keyboard drawn 187 | min= 3us avg= 17us max= 77us canvas cleared 188 | min= 17us avg= 36us max= 173us copy keyboard to canvas 189 | min= 0us avg= 27us max= 24298us waterfall and pressed keys drawn 190 | min= 1us avg= 10us max= 597us event loop 191 | min= 1821us avg= 27083us max= 38550us sleep 192 | min= 11747us avg= 12834us max= 41421us canvas presented 193 | ``` 194 | 195 | Same for macos: 196 | ``` 197 | Sleep time 6 ms - 1 times 198 | Sleep time 18 ms - 1 times 199 | Sleep time 26 ms - 1 times 200 | Sleep time 27 ms - 4 times 201 | Sleep time 28 ms - 1 times 202 | Sleep time 29 ms - 4 times 203 | Sleep time 30 ms - 3 times 204 | Sleep time 31 ms - 2 times 205 | Sleep time 32 ms - 10 times 206 | Sleep time 33 ms - 9 times 207 | Sleep time 34 ms - 12 times 208 | Sleep time 35 ms - 75 times 209 | Sleep time 36 ms - 484 times 210 | Sleep time 37 ms - 4921 times 211 | Sleep time 38 ms - 1054 times 212 | Sleep time 39 ms - 1 times 213 | Lost frames: 2 214 | min= 2us avg= 4us max= 98us control at loop start 215 | min= 1us avg= 2us max= 115us keyboard built 216 | min= 0us avg= 0us max= 1355us keyboard drawn 217 | min= 7us avg= 11us max= 261us canvas cleared 218 | min= 9us avg= 15us max= 905us copy keyboard to canvas 219 | min= 0us avg= 43us max= 37208us waterfall and pressed keys drawn 220 | min= 36us avg= 149us max= 90750us event loop 221 | min= 7070us avg= 38221us max= 40092us sleep 222 | min= 1058us avg= 1574us max= 22518us canvas presented 223 | ``` 224 | 225 | On macos canvas present is on average 8 times faster. In addition on linux the waterfall has several flaws. 226 | Not sure if this is due to intel graphic driver or sdl library or ... 227 | 228 | Funnily on macos the event loop can be blocked by poll_event for a long time, which is weird. Luckily this appears to happen only for Window events, which are seldom. 229 | 230 | Update: 231 | After changing from Intel acceleration mode driver to Intel default driver the measurements are outperforming macos: 232 | 233 | ``` 234 | Sleep time 34 ms - 1 times 235 | Sleep time 37 ms - 1 times 236 | Sleep time 38 ms - 3 times 237 | Sleep time 39 ms - 6330 times 238 | Lost frames: 1 239 | min= 0us avg= 5us max= 58us control at loop start 240 | min= 0us avg= 2us max= 34us keyboard built 241 | min= 0us avg= 0us max= 1831us keyboard drawn 242 | min= 3us avg= 37us max= 393us canvas cleared 243 | min= 10us avg= 65us max= 251us copy keyboard to canvas 244 | min= 0us avg= 41us max= 44905us waterfall and pressed keys drawn 245 | min= 5us avg= 19us max= 326us event loop 246 | min= 34978us avg= 39624us max= 40005us sleep 247 | min= 47us avg= 207us max= 432us canvas presented 248 | ``` 249 | 250 | 251 | ## Data to Marche_aux_Flambeaux.mid 252 | 253 | * 115 bars 254 | * 4/4 bar 255 | * 110 beats per minute (as per Logic Pro/X) 256 | * duration 4 minutes 8 seconds 257 | * First note starts at bar 2.0 258 | * Last note ends at bar 114.3 259 | 260 | ## Solutions for known issues 261 | 262 | ### Linux 263 | 264 | If the application outputs lots of fontconfig errors, then there could be a libfontconfig mismatch. Please check, if pkg-config is able to find fontconfig: 265 | 266 | ``` 267 | pkg-config --list-all|grep fontconfig 268 | ``` 269 | 270 | If there is no output, then crate *servo-fontconfig-sys* will build its own version, which can be incompatible. Installation of libconfig1-dev has fixed this. 271 | 272 | ## License 273 | 274 | The attached LICENSE file defines the license for the code of this crate only - specifically before compiling or linking. The resulting binary after linking may be problematic in regard to license incompatibilities of included crates. 275 | 276 | From current point of view to be checked: 277 | BSD-3-Clause (1): simple-logging 278 | ISC (1): rdrand 279 | N/A (2): fuchsia-cprng 280 | Unlicense (1): midly 281 | 282 | AFAIK is incompatible: 283 | GPL-3.0-or-later (1): sdl2-unifont 284 | 285 | Consequently automated builds resulting in a public available binary cannot be set up for now. 286 | 287 | ## Final Words 288 | 289 | The application works, but still this is a quick hack and not polished for code review ;-) 290 | 291 | ## Alternatives 292 | 293 | * [Neothesia](https://github.com/PolyMeilex/Neothesia) 294 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::thread::sleep; 2 | use std::time::Duration; 3 | 4 | use log::*; 5 | 6 | use sdl2_timing::Sdl2Timing; 7 | 8 | //mod app; 9 | mod app_control; 10 | mod draw_engine; 11 | mod midi_container; 12 | mod midi_sequencer; 13 | mod scroller; 14 | mod sdl_event_processor; 15 | mod stderrlog; 16 | mod time_controller; 17 | mod usage; // Hacked version of stderrlog crate 18 | 19 | /// logging targets defined as abbreviated constants (and avoid typos in repeats) 20 | const EV: &str = &"eventloop"; 21 | const SDL: &str = &"sdl"; 22 | 23 | fn main() -> Result<(), Box> { 24 | let matches = usage::usage(); 25 | let mut control = app_control::AppControl::from_clap(matches); 26 | 27 | if let Some(modules) = control.is_debug() { 28 | stderrlog::new() 29 | .quiet(control.is_quiet()) 30 | .verbosity(control.verbosity()) 31 | .timestamp(stderrlog::Timestamp::Microsecond) 32 | .modules(modules.iter().cloned().collect::>()) 33 | .init() 34 | .unwrap(); 35 | } else { 36 | stderrlog::new() 37 | .quiet(control.is_quiet()) 38 | .verbosity(control.verbosity()) 39 | .timestamp(stderrlog::Timestamp::Microsecond) 40 | .init() 41 | .unwrap(); 42 | } 43 | 44 | log::set_max_level(match control.verbosity() { 45 | 0 => LevelFilter::Off, 46 | 1 => LevelFilter::Error, 47 | 2 => LevelFilter::Warn, 48 | 3 => LevelFilter::Info, 49 | 4 => LevelFilter::Debug, 50 | _ => LevelFilter::Trace, 51 | }); 52 | 53 | if control.list_command() { 54 | return midi_container::list_command(control.is_quiet(), &control.midi_fname()); 55 | } 56 | 57 | let only_midi_player = control.show_tracks().len() == 0; 58 | 59 | control.create_connected_sequencer(only_midi_player)?; 60 | if only_midi_player { 61 | let (_, play_events) = app_control::AppControl::read_midi_file( 62 | &control.midi_fname(), 63 | control.left_key(), 64 | control.right_key(), 65 | control.shift_key(), 66 | control.show_tracks().clone(), 67 | control.play_tracks().clone(), 68 | )?; 69 | control.play_midi_data(play_events); 70 | loop { 71 | sleep(Duration::from_millis(1000)); 72 | if control.seq_is_finished() { 73 | break; 74 | } 75 | } 76 | return Ok(()); 77 | } 78 | 79 | let nr_of_keys = control.right_key() - control.left_key() + 1; 80 | let sdl_context = sdl2::init().unwrap(); 81 | let video_subsystem = sdl_context.video().unwrap(); 82 | info!( 83 | target: SDL, 84 | "display driver: {:?}", 85 | video_subsystem.current_video_driver() 86 | ); 87 | let nr_displays = video_subsystem.num_video_displays()?; 88 | for i in 0..nr_displays { 89 | info!( 90 | target: SDL, 91 | "{}: Display Mode: {:?}", 92 | i, 93 | video_subsystem.current_display_mode(i) 94 | ); 95 | info!( 96 | target: SDL, 97 | "{}: dpi: {:?}", 98 | i, 99 | video_subsystem.display_dpi(i) 100 | ); 101 | } 102 | 103 | info!( 104 | target: SDL, 105 | "Screensaver: {:?}", 106 | video_subsystem.is_screen_saver_enabled() 107 | ); 108 | 109 | info!( 110 | target: SDL, 111 | "Swap interval: {:?}", 112 | video_subsystem.gl_get_swap_interval() 113 | ); 114 | info!( 115 | target: SDL, 116 | "{:?}", 117 | video_subsystem.gl_set_swap_interval(sdl2::video::SwapInterval::VSync) 118 | ); 119 | info!( 120 | target: SDL, 121 | "Swap interval: {:?}", 122 | video_subsystem.gl_get_swap_interval() 123 | ); 124 | 125 | let window = video_subsystem 126 | .window(&format!("Rusthesia: {}", control.midi_fname()), 800, 600) 127 | .position_centered() 128 | .resizable() 129 | .build() 130 | .unwrap(); 131 | info!(target: SDL, "Display Mode: {:?}", window.display_mode()); 132 | let mut st = Sdl2Timing::new_for(&video_subsystem, &window)?; 133 | 134 | //let window_context = window.context(); 135 | let mut canvas = sdl2::render::CanvasBuilder::new(window) 136 | .accelerated() 137 | .present_vsync() 138 | .build()?; 139 | let texture_creator = canvas.texture_creator(); 140 | let mut textures: Vec = vec![]; 141 | 142 | let mut event_pump = sdl_context.event_pump().unwrap(); 143 | 144 | let rows_per_s = 100; 145 | let waterfall_tex_height = 1000; 146 | 147 | 'running: loop { 148 | trace!(target: EV, "at loop start"); 149 | let bg_color = sdl2::pixels::Color::RGB(50, 50, 50); 150 | st.canvas_present_then_clear(&mut canvas, bg_color); 151 | 152 | if control.seq_is_finished() { 153 | break; 154 | } 155 | control.next_loop(); 156 | 157 | let rec = canvas.viewport(); 158 | let width = rec.width(); 159 | if control.need_redraw(width as u16) { 160 | textures.clear(); 161 | } 162 | st.sample("control at loop start"); 163 | 164 | trace!(target: EV, "before Eventloop"); 165 | loop { 166 | let rem_us = st.us_till_next_frame(); 167 | if rem_us > 5000 { 168 | if let Some(event) = event_pump.poll_event() { 169 | trace!("event received: {:?}", event); 170 | if !sdl_event_processor::process_event(event, &mut control) { 171 | break 'running; // Exit loop 172 | } 173 | continue; // next event 174 | } 175 | } 176 | break; 177 | } 178 | st.sample("event loop"); 179 | 180 | let waterfall_overlap = 2 * width / nr_of_keys as u32; // ensure even 181 | let waterfall_net_height = waterfall_tex_height - waterfall_overlap; 182 | 183 | if textures.len() == 0 { 184 | trace!("Create keyboard textures"); 185 | if let Some(keyboard) = control.get_keyboard() { 186 | // Texture 0 are for unpressed and 1 for pressed keys 187 | for pressed in vec![false, true].drain(..) { 188 | let mut texture = texture_creator 189 | .create_texture_target( 190 | texture_creator.default_pixel_format(), 191 | width, 192 | keyboard.height as u32, 193 | ) 194 | .unwrap(); 195 | canvas.with_texture_canvas(&mut texture, |tex_canvas| { 196 | draw_engine::draw_keyboard(keyboard, tex_canvas, pressed).ok(); 197 | })?; 198 | textures.push(texture); 199 | } 200 | st.sample("keyboard drawn"); 201 | } 202 | } 203 | 204 | if textures.len() == 0 { 205 | continue; 206 | } 207 | 208 | if let Some(keyboard) = control.get_keyboard() { 209 | // Copy keyboard with unpressed keys 210 | let dst_rec = sdl2::rect::Rect::new( 211 | 0, 212 | (rec.height() - keyboard.height as u32 - 1) as i32, 213 | width, 214 | keyboard.height as u32, 215 | ); 216 | canvas.copy(&textures[0], None, dst_rec)?; 217 | st.sample("copy keyboard to canvas"); 218 | } 219 | 220 | if control.show_events().is_some() { 221 | if textures.len() <= 2 { 222 | // Texture 2.. are for waterfall. 223 | // 224 | let maxtime_us = control.show_events().unwrap()[control.show_events_len() - 1].0; 225 | let rows = (maxtime_us * rows_per_s as u64 + 999_999) / 1_000_000; 226 | let nr_of_textures = 227 | ((rows + waterfall_net_height as u64 - 1) / waterfall_net_height as u64) as u32; 228 | trace!("Needed rows/textures: {}/{}", rows, nr_of_textures); 229 | for i in 0..nr_of_textures { 230 | let mut texture = texture_creator 231 | .create_texture_target( 232 | texture_creator.default_pixel_format(), 233 | width, 234 | waterfall_tex_height, 235 | ) 236 | .unwrap(); 237 | if let Some(keyboard) = control.get_keyboard() { 238 | canvas.with_texture_canvas(&mut texture, |tex_canvas| { 239 | draw_engine::draw_waterfall( 240 | keyboard, 241 | tex_canvas, 242 | i, 243 | i * waterfall_net_height, 244 | waterfall_net_height, 245 | waterfall_overlap, 246 | rows_per_s, 247 | &control.show_events().unwrap(), 248 | ); 249 | })?; 250 | } 251 | textures.push(texture); 252 | } 253 | } 254 | } 255 | st.sample("waterfall textures created and drawn"); 256 | 257 | let draw_commands = if control.show_events().is_some() { 258 | let rem_us = st.us_till_next_frame(); 259 | let pos_us = control.get_pos_us_after(rem_us); 260 | 261 | let mut draw_commands_1 = vec![]; 262 | if let Some(keyboard) = control.get_keyboard() { 263 | draw_commands_1 = draw_engine::get_pressed_key_rectangles( 264 | &keyboard, 265 | rec.height() - keyboard.height as u32 - 1, 266 | pos_us, 267 | &control.show_events().unwrap(), 268 | ); 269 | let mut draw_commands_2 = draw_engine::copy_waterfall_to_screen( 270 | textures.len() - 2, 271 | rec.width(), 272 | rec.height() - keyboard.height as u32, 273 | waterfall_net_height, 274 | waterfall_overlap, 275 | rows_per_s, 276 | pos_us, 277 | ); 278 | draw_commands_1.append(&mut draw_commands_2); 279 | } 280 | draw_commands_1 281 | } else { 282 | vec![] 283 | }; 284 | st.sample("waterfall and pressed keys commands generated"); 285 | 286 | trace!(target: EV, "before drawing to screen"); 287 | for cmd in draw_commands.into_iter() { 288 | match cmd { 289 | draw_engine::DrawCommand::CopyToScreen { 290 | src_texture, 291 | src_rect, 292 | dst_rect, 293 | } => { 294 | canvas.copy(&textures[src_texture], src_rect, dst_rect)?; 295 | } 296 | } 297 | } 298 | st.sample("waterfall and pressed keys drawn"); 299 | 300 | control.update_position_if_scrolling(); 301 | } 302 | sleep(Duration::from_millis(150)); 303 | 304 | st.output(); 305 | Ok(()) 306 | } 307 | -------------------------------------------------------------------------------- /src/midi_sequencer.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::io::{stdin, stdout, Write}; 3 | use std::sync::mpsc; 4 | use std::thread; 5 | use std::thread::sleep; 6 | use std::time::Duration; 7 | 8 | use log::*; 9 | use midir::MidiOutput; 10 | 11 | use crate::time_controller::{TimeController, TimeListener, TimeListenerTrait}; 12 | #[derive(Debug)] 13 | pub enum MidiEvent { 14 | NoteOn(u8, u8, u8), 15 | NoteOff(u8, u8, u8), 16 | Aftertouch(u8, u8, u8), 17 | Controller(u8, u8, u8), 18 | ChannelAftertouch(u8, u8), 19 | PitchBend(u8, u16), 20 | ProgramChange(u8, u8), 21 | } 22 | impl MidiEvent { 23 | pub fn as_raw( 24 | &self, 25 | trk_idx: usize, 26 | opt_key_pressed: Option<&mut HashSet<(usize, u8, u8)>>, 27 | ) -> Vec { 28 | match self { 29 | MidiEvent::NoteOn(channel, key, pressure) => { 30 | if let Some(key_pressed) = opt_key_pressed { 31 | key_pressed.insert((trk_idx, *channel, *key)); 32 | } 33 | vec![0x90 + channel, *key, *pressure] 34 | } 35 | MidiEvent::NoteOff(channel, key, pressure) => { 36 | if let Some(key_pressed) = opt_key_pressed { 37 | key_pressed.remove(&(trk_idx, *channel, *key)); 38 | } 39 | vec![0x80 + channel, *key, *pressure] 40 | } 41 | MidiEvent::Controller(channel, control, value) => { 42 | vec![0xb0 + channel, *control, *value] 43 | } 44 | MidiEvent::Aftertouch(channel, key, pressure) => vec![0xa0 + channel, *key, *pressure], 45 | MidiEvent::ChannelAftertouch(channel, pressure) => vec![0xd0 + channel, *pressure], 46 | MidiEvent::PitchBend(channel, change) => { 47 | vec![0xe0 + channel, (*change & 0x7f) as u8, (*change >> 7) as u8] 48 | } 49 | MidiEvent::ProgramChange(channel, program) => vec![0xc0 + channel, *program], 50 | } 51 | } 52 | } 53 | 54 | pub type RawMidiTuple = (u64, usize, MidiEvent); 55 | 56 | enum MidiSequencerCommand { 57 | Ping, 58 | Connect(usize), 59 | SetPosition(i64), 60 | SetEvents(Vec), 61 | Play(i64), 62 | Scale(u16), 63 | Stop, 64 | } 65 | 66 | enum SequencerState { 67 | Stopped, 68 | Playing, 69 | StartPlaying(i64), 70 | EOF, 71 | } 72 | 73 | struct MidiSequencerThread { 74 | control: mpsc::Receiver, 75 | events: Vec, 76 | time_control: TimeController, 77 | exit_on_eof: bool, 78 | } 79 | impl MidiSequencerThread { 80 | fn new( 81 | control: mpsc::Receiver, 82 | time_control: TimeController, 83 | exit_on_eof: bool, 84 | ) -> MidiSequencerThread { 85 | MidiSequencerThread { 86 | control, 87 | events: vec![], 88 | time_control, 89 | exit_on_eof, 90 | } 91 | } 92 | fn run(&mut self) { 93 | use crate::midi_sequencer::SequencerState::*; 94 | let mut opt_conn_out = None; 95 | let mut idx = 0; 96 | let mut state = EOF; 97 | let mut key_pressed = HashSet::new(); 98 | loop { 99 | state = match state { 100 | EOF => match self.control.recv() { 101 | Err(mpsc::RecvError) => break, 102 | Ok(MidiSequencerCommand::Connect(out_port)) => { 103 | trace!("Opening connection"); 104 | let midi_out = midir::MidiOutput::new("Rusthesia").unwrap(); 105 | opt_conn_out = Some(midi_out.connect(out_port, "rusthesia").unwrap()); 106 | trace!("Connection opened"); 107 | EOF 108 | } 109 | Ok(MidiSequencerCommand::Stop) | Ok(MidiSequencerCommand::Ping) => EOF, 110 | Ok(MidiSequencerCommand::Play(pos_us)) => StartPlaying(pos_us), 111 | Ok(MidiSequencerCommand::Scale(new_scaling)) => { 112 | self.time_control.set_scaling_1000(new_scaling); 113 | EOF 114 | } 115 | Ok(MidiSequencerCommand::SetPosition(pos_us)) => { 116 | self.time_control.set_pos_us(pos_us); 117 | EOF 118 | } 119 | Ok(MidiSequencerCommand::SetEvents(events)) => { 120 | self.events = events; 121 | Stopped 122 | } 123 | }, 124 | Stopped => match self.control.recv() { 125 | Err(mpsc::RecvError) => break, 126 | Ok(MidiSequencerCommand::Connect(_out_port)) => panic!("Not connected"), 127 | Ok(MidiSequencerCommand::Ping) => Stopped, 128 | Ok(MidiSequencerCommand::Play(pos_us)) => StartPlaying(pos_us), 129 | Ok(MidiSequencerCommand::Scale(new_scaling)) => { 130 | self.time_control.set_scaling_1000(new_scaling); 131 | Stopped 132 | } 133 | Ok(MidiSequencerCommand::SetPosition(pos_us)) => { 134 | self.time_control.set_pos_us(pos_us); 135 | Stopped 136 | } 137 | Ok(MidiSequencerCommand::SetEvents(events)) => { 138 | self.events = events; 139 | Stopped 140 | } 141 | Ok(MidiSequencerCommand::Stop) => Stopped, 142 | }, 143 | Playing => match self.control.try_recv() { 144 | Err(mpsc::TryRecvError::Disconnected) => break, 145 | Err(mpsc::TryRecvError::Empty) => Playing, 146 | Ok(MidiSequencerCommand::Connect(_out_port)) => panic!("Not connected"), 147 | Ok(MidiSequencerCommand::Ping) => Playing, 148 | Ok(MidiSequencerCommand::Play(pos_us)) => StartPlaying(pos_us), 149 | Ok(MidiSequencerCommand::Scale(new_scaling)) => { 150 | self.time_control.set_scaling_1000(new_scaling); 151 | Playing 152 | } 153 | Ok(MidiSequencerCommand::SetPosition(pos_us)) => StartPlaying(pos_us), 154 | Ok(MidiSequencerCommand::SetEvents(events)) => { 155 | self.events = events; 156 | StartPlaying(0) 157 | } 158 | Ok(MidiSequencerCommand::Stop) => { 159 | self.time_control.stop(); 160 | if let Some(ref mut conn_out) = opt_conn_out.as_mut() { 161 | for (trk_idx, channel, key) in key_pressed.drain() { 162 | let evt = MidiEvent::NoteOff(channel as u8, key, 0); 163 | let msg = evt.as_raw(trk_idx, None); 164 | conn_out.send(&msg).unwrap(); 165 | } 166 | } 167 | Stopped 168 | } 169 | }, 170 | StartPlaying(_) => panic!("StartPlaying should not be reachable here"), 171 | }; 172 | 173 | state = match state { 174 | Stopped => Stopped, 175 | EOF => EOF, 176 | StartPlaying(pos_us) => { 177 | idx = 0; 178 | self.time_control.set_pos_us(pos_us as i64); 179 | while idx < self.events.len() && pos_us >= self.events[idx].0 as i64 { 180 | idx += 1; 181 | } 182 | if idx >= self.events.len() { 183 | self.time_control.stop(); 184 | if self.exit_on_eof { 185 | break; 186 | } 187 | EOF 188 | } else { 189 | self.time_control.start(); 190 | Playing 191 | } 192 | } 193 | Playing => { 194 | let pos_us = self.time_control.get_pos_us(); 195 | if let Some(ref mut conn_out) = opt_conn_out.as_mut() { 196 | while idx < self.events.len() && pos_us >= self.events[idx].0 as i64 { 197 | let msg = self.events[idx] 198 | .2 199 | .as_raw(self.events[idx].1, Some(&mut key_pressed)); 200 | if msg.len() > 0 { 201 | conn_out.send(&msg).unwrap(); 202 | } 203 | idx += 1; 204 | } 205 | } 206 | if idx >= self.events.len() { 207 | self.time_control.stop(); 208 | if self.exit_on_eof { 209 | break; 210 | } 211 | EOF 212 | } else { 213 | let next_pos = self.events[idx].0 as i64; 214 | let opt_sleep_ms = self.time_control.ms_till_pos(next_pos); 215 | if let Some(sleep_ms) = opt_sleep_ms { 216 | let sleep_ms = sleep_ms.min(20); 217 | trace!("sleep {} ms", sleep_ms); 218 | sleep(Duration::from_millis(sleep_ms as u64)); 219 | } 220 | Playing 221 | } 222 | } 223 | } 224 | } 225 | if let Some(conn_out) = opt_conn_out { 226 | conn_out.close(); 227 | trace!("Connection closed"); 228 | } 229 | } 230 | } 231 | 232 | pub struct MidiSequencer { 233 | time_listener: TimeListener, 234 | control: mpsc::Sender, 235 | } 236 | 237 | impl MidiSequencer { 238 | pub fn new(exit_on_eof: bool) -> MidiSequencer { 239 | let (tx, rx) = mpsc::channel(); 240 | let controller = TimeController::new(); 241 | let time_listener = controller.new_listener(); 242 | thread::spawn(move || MidiSequencerThread::new(rx, controller, exit_on_eof).run()); 243 | MidiSequencer { 244 | control: tx, 245 | time_listener, 246 | } 247 | } 248 | pub fn get_new_listener(&self) -> TimeListener { 249 | self.time_listener.clone() 250 | } 251 | pub fn set_pos_us(&self, pos_us: i64) { 252 | self.control 253 | .send(MidiSequencerCommand::SetPosition(pos_us)) 254 | .ok(); 255 | } 256 | pub fn is_finished(&self) -> bool { 257 | self.control.send(MidiSequencerCommand::Ping).is_err() 258 | } 259 | pub fn set_midi_data(&self, events: Vec) { 260 | self.control 261 | .send(MidiSequencerCommand::SetEvents(events)) 262 | .ok(); 263 | } 264 | pub fn play(&self, pos_us: i64) { 265 | self.control.send(MidiSequencerCommand::Play(pos_us)).ok(); 266 | } 267 | pub fn set_scaling_1000(&self, new_scale: u16) { 268 | self.control 269 | .send(MidiSequencerCommand::Scale(new_scale)) 270 | .ok(); 271 | } 272 | pub fn stop(&self) { 273 | self.control.send(MidiSequencerCommand::Stop).ok(); 274 | } 275 | pub fn connect(&mut self) -> Result<(), Box> { 276 | trace!("output"); 277 | let midi_out = MidiOutput::new("Rusthesia")?; 278 | // Get an output port (read from console if multiple are available) 279 | let out_port = match midi_out.port_count() { 280 | 0 => return Err("no output port found".into()), 281 | 1 => { 282 | println!( 283 | "Choosing the only available output port: {}", 284 | midi_out.port_name(0).unwrap() 285 | ); 286 | 0 287 | } 288 | _ => { 289 | println!("\nAvailable output ports:"); 290 | for i in 0..midi_out.port_count() { 291 | println!("{}: {}", i, midi_out.port_name(i).unwrap()); 292 | } 293 | print!("Please select output port: "); 294 | stdout().flush()?; 295 | let mut input = String::new(); 296 | stdin().read_line(&mut input)?; 297 | input.trim().parse()? 298 | } 299 | }; 300 | drop(midi_out); 301 | self.control 302 | .send(MidiSequencerCommand::Connect(out_port)) 303 | .ok(); 304 | Ok(()) 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/midi_container.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | use std::io::{Error, ErrorKind}; 3 | use std::iter::Iterator; 4 | 5 | use log::*; 6 | 7 | pub struct TrackState<'m> { 8 | trk_number: usize, 9 | trk_iter: std::slice::Iter<'m, midly::Event<'m>>, 10 | time: u32, 11 | evt: Option<&'m midly::Event<'m>>, 12 | } 13 | impl<'m> TrackState<'m> { 14 | fn new(trk_number: usize, trk_iter: std::slice::Iter<'m, midly::Event<'m>>) -> TrackState<'m> { 15 | TrackState { 16 | trk_number, 17 | trk_iter, 18 | time: 0, 19 | evt: None, 20 | } 21 | } 22 | fn sort_key(&self) -> u32 { 23 | self.time 24 | } 25 | } 26 | impl<'m> std::cmp::PartialEq for TrackState<'m> { 27 | fn eq(&self, other: &Self) -> bool { 28 | self.sort_key() == other.sort_key() 29 | } 30 | } 31 | impl<'m> std::cmp::Eq for TrackState<'m> {} 32 | impl<'m> std::cmp::PartialOrd for TrackState<'m> { 33 | fn partial_cmp(&self, other: &Self) -> Option { 34 | Some(self.cmp(other)) 35 | } 36 | } 37 | impl<'m> std::cmp::Ord for TrackState<'m> { 38 | fn cmp(&self, other: &Self) -> Ordering { 39 | match (self.evt.is_some(), other.evt.is_some()) { 40 | (false, false) => Ordering::Equal, 41 | (false, true) => Ordering::Less, 42 | (true, false) => Ordering::Greater, 43 | (true, true) => match self.sort_key().cmp(&other.sort_key()) { 44 | Ordering::Equal => match self.evt.as_ref().unwrap().kind { 45 | midly::EventKind::Meta(_) => Ordering::Less, 46 | _ => match other.evt.as_ref().unwrap().kind { 47 | midly::EventKind::Meta(_) => Ordering::Greater, 48 | _ => Ordering::Equal, 49 | }, 50 | }, 51 | o => o, 52 | }, 53 | } 54 | } 55 | } 56 | 57 | pub struct MidiIterator<'m> { 58 | track_parsers: Vec>, 59 | } 60 | impl<'m> MidiIterator<'m> { 61 | pub fn new() -> MidiIterator<'m> { 62 | MidiIterator { 63 | track_parsers: vec![], 64 | } 65 | } 66 | pub fn add_track( 67 | &mut self, 68 | trk_number: usize, 69 | trk_iter: std::slice::Iter<'m, midly::Event<'m>>, 70 | ) { 71 | let ts = TrackState::new(trk_number, trk_iter); 72 | self.track_parsers.push(ts); 73 | } 74 | } 75 | impl<'m> Iterator for MidiIterator<'m> { 76 | type Item = (u64, usize, &'m midly::EventKind<'m>); 77 | fn next(&mut self) -> Option { 78 | loop { 79 | if self.track_parsers.len() == 0 { 80 | return None; 81 | } 82 | self.track_parsers.sort(); 83 | let mut p = self.track_parsers.remove(0); 84 | let trk_number = p.trk_number; 85 | let time = p.time; 86 | let opt_evt = p.evt.take(); 87 | if let Some(m) = p.trk_iter.next() { 88 | p.time += m.delta.as_int(); 89 | p.evt = Some(m); 90 | self.track_parsers.push(p); 91 | } 92 | if let Some(evt) = opt_evt { 93 | return Some((time as u64, trk_number, &evt.kind)); 94 | } 95 | } 96 | } 97 | } 98 | 99 | pub struct MidiTimedIterator<'m> { 100 | opt_midi_iter: Option>, 101 | timing: &'m midly::Timing, 102 | timebase: Option, 103 | current_time_us: u64, 104 | last_tick: u32, 105 | } 106 | impl<'m> MidiTimedIterator<'m> { 107 | fn update_timebase(&mut self, tempo: u32) { 108 | let ppqn = match self.timing { 109 | // http://www.onicos.com/staff/iz/formats/smf006.html 110 | // http://midiwonder.com/midifile.html 111 | // 112 | // tempo = 24ths of a microsecond per MIDI clock 113 | midly::Timing::Metrical(x) => x.as_int() as u32, 114 | midly::Timing::Timecode(_x, _y) => panic!("Timecode not implemented"), 115 | }; 116 | let bpm = 60_000_000 / tempo as u64; 117 | 118 | self.timebase = Some(60_000_000 / ppqn as u64 / bpm); 119 | } 120 | } 121 | impl<'m> Iterator for MidiTimedIterator<'m> { 122 | type Item = (u64, usize, &'m midly::EventKind<'m>); 123 | fn next(&mut self) -> Option { 124 | loop { 125 | if self.opt_midi_iter.is_none() { 126 | return None; 127 | } 128 | let opt_tuple = self.opt_midi_iter.as_mut().unwrap().next(); 129 | if let Some((time, trk, evt_kind)) = opt_tuple { 130 | let dt = time - self.last_tick as u64; 131 | if dt > 0 { 132 | self.last_tick = time as u32; 133 | self.current_time_us += dt as u64 * self.timebase.unwrap(); 134 | } 135 | match evt_kind { 136 | &midly::EventKind::Meta(midly::MetaMessage::Tempo(tmp)) => { 137 | self.update_timebase(tmp.as_int()); 138 | } 139 | _ => { 140 | return Some((self.current_time_us, trk, evt_kind)); 141 | } 142 | } 143 | } else { 144 | self.opt_midi_iter = None; 145 | } 146 | } 147 | } 148 | } 149 | 150 | impl<'m> MidiIterator<'m> { 151 | pub fn timed(self, timing: &'m midly::Timing) -> MidiTimedIterator<'m> { 152 | MidiTimedIterator { 153 | opt_midi_iter: Some(self), 154 | timing, 155 | timebase: None, 156 | current_time_us: 0, 157 | last_tick: 0, 158 | } 159 | } 160 | } 161 | 162 | pub struct MidiContainer<'m> { 163 | smf: midly::Smf<'m, Vec>>, 164 | } 165 | impl<'m> MidiContainer<'m> { 166 | pub fn from_buf(smf: &'m midly::Smf) -> Result, Error> { 167 | Ok(MidiContainer { smf: smf.clone() }) 168 | } 169 | pub fn iter(&'m self) -> MidiIterator<'m> { 170 | let mut mi = MidiIterator::new(); 171 | for (i, trk) in self.smf.tracks.iter().enumerate() { 172 | mi.add_track(i, trk.iter()); 173 | } 174 | mi 175 | } 176 | pub fn header(&'m self) -> &midly::Header { 177 | &self.smf.header 178 | } 179 | pub fn nr_of_tracks(&'m self) -> usize { 180 | self.smf.tracks.len() 181 | } 182 | } 183 | 184 | pub fn list_command(quiet: bool, midi_fname: &str) -> Result<(), Box> { 185 | let buf = std::fs::read(midi_fname)?; 186 | let smf_buf = 187 | midly::Smf::parse(&buf).map_err(|e| Error::new(ErrorKind::Other, format!("{:?}", e)))?; 188 | let container = MidiContainer::from_buf(&smf_buf)?; 189 | if !quiet { 190 | for _evt in container.iter() { 191 | //trace!("{:?}", evt); 192 | } 193 | for evt in container.iter().timed(&container.header().timing) { 194 | trace!("timed: {:?}", evt); 195 | } 196 | } 197 | for i in 0..container.nr_of_tracks() { 198 | println!("Track {}:", i); 199 | let mut used_channels = vec![false; 16]; 200 | for evt in container.iter().filter(|e| e.1 == i) { 201 | match evt.2 { 202 | midly::EventKind::Midi { 203 | channel: c, 204 | message: _m, 205 | } => { 206 | used_channels[c.as_int() as usize] = true; 207 | } 208 | midly::EventKind::SysEx(_) => (), 209 | midly::EventKind::Escape(_) => (), 210 | midly::EventKind::Meta(mm) => match mm { 211 | midly::MetaMessage::Text(raw) => { 212 | println!(" Text: {}", String::from_utf8_lossy(raw)); 213 | } 214 | midly::MetaMessage::ProgramName(raw) => { 215 | println!(" Program name: {}", String::from_utf8_lossy(raw)); 216 | } 217 | midly::MetaMessage::DeviceName(raw) => { 218 | println!(" Device name: {}", String::from_utf8_lossy(raw)); 219 | } 220 | midly::MetaMessage::InstrumentName(raw) => { 221 | println!(" Instrument name: {}", String::from_utf8_lossy(raw)); 222 | } 223 | midly::MetaMessage::TrackName(raw) => { 224 | println!(" Track name: {}", String::from_utf8_lossy(raw)); 225 | } 226 | midly::MetaMessage::MidiChannel(channel) => { 227 | println!(" Channel: {}", channel.as_int()); 228 | } 229 | midly::MetaMessage::Tempo(ms_per_beat) => { 230 | trace!(" Tempo: {:?}", ms_per_beat); 231 | } 232 | midly::MetaMessage::EndOfTrack => (), 233 | mm => warn!("Not treated meta message: {:?}", mm), 234 | }, 235 | } 236 | } 237 | println!( 238 | " Used channels: {:?}", 239 | used_channels 240 | .iter() 241 | .enumerate() 242 | .filter(|(_, v)| **v) 243 | .map(|(c, _)| c) 244 | .collect::>() 245 | ); 246 | } 247 | Ok(()) 248 | } 249 | 250 | #[cfg(test)] 251 | mod tests { 252 | use crate::midi_container; 253 | 254 | #[test] 255 | fn test_01() { 256 | let midi_fname = "Marche_aux_Flambeaux.mid"; 257 | let buf = std::fs::read(midi_fname).unwrap(); 258 | let smf_buf = midly::Smf::parse(&buf).unwrap(); 259 | let container = midi_container::MidiContainer::from_buf(&smf_buf).unwrap(); 260 | assert_eq!(container.nr_of_tracks(), 3); 261 | assert_eq!(container.iter().count(), 2423); 262 | //for evt in container.iter() { 263 | // println!("{:.unwrap()}", evt); 264 | //} 265 | } 266 | 267 | #[test] 268 | fn test_02() { 269 | let midi_fname = "Marche_aux_Flambeaux.mid"; 270 | let buf = std::fs::read(midi_fname).unwrap(); 271 | let smf_buf = midly::Smf::parse(&buf).unwrap(); 272 | let container = midi_container::MidiContainer::from_buf(&smf_buf).unwrap(); 273 | assert_eq!( 274 | container 275 | .iter() 276 | .filter(|(_time, track_id, _evt)| *track_id == 0) 277 | .count(), 278 | 6 279 | ); 280 | } 281 | 282 | #[test] 283 | fn test_03() { 284 | let midi_fname = "Marche_aux_Flambeaux.mid"; 285 | let buf = std::fs::read(midi_fname).unwrap(); 286 | let smf_buf = midly::Smf::parse(&buf).unwrap(); 287 | let container = midi_container::MidiContainer::from_buf(&smf_buf).unwrap(); 288 | assert_eq!( 289 | container 290 | .iter() 291 | .filter(|(_time, track_id, _evt)| *track_id == 1) 292 | .count(), 293 | 1679 294 | ); 295 | } 296 | 297 | #[test] 298 | fn test_04() { 299 | let midi_fname = "Marche_aux_Flambeaux.mid"; 300 | let buf = std::fs::read(midi_fname).unwrap(); 301 | let smf_buf = midly::Smf::parse(&buf).unwrap(); 302 | let container = midi_container::MidiContainer::from_buf(&smf_buf).unwrap(); 303 | assert_eq!( 304 | container 305 | .iter() 306 | .filter(|(_time, track_id, _evt)| *track_id == 2) 307 | .count(), 308 | 2423 - 6 - 1679 309 | ); 310 | } 311 | 312 | #[test] 313 | fn test_05() { 314 | let midi_fname = "Marche_aux_Flambeaux.mid"; 315 | let buf = std::fs::read(midi_fname).unwrap(); 316 | let smf_buf = midly::Smf::parse(&buf).unwrap(); 317 | let container = midi_container::MidiContainer::from_buf(&smf_buf).unwrap(); 318 | match container.header().timing { 319 | midly::Timing::Metrical(t) => assert_eq!(t.as_int(), 384), 320 | _ => panic!("wrong type"), 321 | } 322 | } 323 | 324 | #[test] 325 | fn test_06() { 326 | let midi_fname = "Marche_aux_Flambeaux.mid"; 327 | let buf = std::fs::read(midi_fname).unwrap(); 328 | let smf_buf = midly::Smf::parse(&buf).unwrap(); 329 | let container = midi_container::MidiContainer::from_buf(&smf_buf).unwrap(); 330 | let mut last_time = 0; 331 | for (time, _, _) in container.iter() { 332 | assert!(last_time <= time); 333 | last_time = time; 334 | } 335 | 336 | assert_eq!(last_time, 174720); 337 | } 338 | 339 | #[test] 340 | fn test_11() { 341 | let midi_fname = "Marche_aux_Flambeaux.mid"; 342 | let buf = std::fs::read(midi_fname).unwrap(); 343 | let smf_buf = midly::Smf::parse(&buf).unwrap(); 344 | let container = midi_container::MidiContainer::from_buf(&smf_buf).unwrap(); 345 | assert_eq!( 346 | container.iter().timed(&container.header().timing).count(), 347 | 2421 348 | ); // 2 Tempo events should be filtered 349 | } 350 | #[test] 351 | fn test_16() { 352 | let midi_fname = "Marche_aux_Flambeaux.mid"; 353 | let buf = std::fs::read(midi_fname).unwrap(); 354 | let smf_buf = midly::Smf::parse(&buf).unwrap(); 355 | let container = midi_container::MidiContainer::from_buf(&smf_buf).unwrap(); 356 | let mut last_time_us = 0; 357 | for (time_us, _, _) in container.iter().timed(&container.header().timing) { 358 | assert!(last_time_us <= time_us); 359 | last_time_us = time_us; 360 | } 361 | 362 | assert_eq!(last_time_us, 248_102_400); 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /src/draw_engine.rs: -------------------------------------------------------------------------------- 1 | //use font_kit; 2 | use log::*; 3 | use sdl2::gfx::primitives::DrawRenderer; 4 | use sdl2::pixels::Color; 5 | 6 | use piano_keyboard; 7 | 8 | use crate::midi_sequencer; 9 | 10 | #[derive(Debug, PartialEq)] 11 | pub enum DrawCommand { 12 | CopyToScreen { 13 | src_texture: usize, 14 | src_rect: sdl2::rect::Rect, 15 | dst_rect: sdl2::rect::Rect, 16 | }, 17 | } 18 | 19 | fn is_white(key: u8) -> bool { 20 | match key % 12 { 21 | 0 => true, 22 | 1 => false, 23 | 2 => true, 24 | 3 => false, 25 | 4 => true, 26 | 5 => true, 27 | 6 => false, 28 | 7 => true, 29 | 8 => false, 30 | 9 => true, 31 | 10 => false, 32 | 11 => true, 33 | _ => panic!("Cannot happen"), 34 | } 35 | } 36 | 37 | fn trk2col(trk: usize, key: u8) -> Color { 38 | match (trk % 2, is_white(key)) { 39 | (0, true) => Color::RGB(0, 255, 255), 40 | (0, false) => Color::RGB(0, 180, 180), 41 | (_, true) => Color::RGB(255, 0, 255), 42 | (_, false) => Color::RGB(180, 0, 180), 43 | } 44 | } 45 | 46 | pub fn draw_keyboard( 47 | keyboard: &piano_keyboard::Keyboard2d, 48 | canvas: &mut sdl2::render::Canvas, 49 | pressed: bool, 50 | ) -> Result<(), Box> { 51 | canvas.set_draw_color(sdl2::pixels::Color::RGB(100, 100, 100)); 52 | canvas.clear(); 53 | //let rec = canvas.viewport(); 54 | let (col_white, col_black) = if pressed { 55 | (Color::RGB(100, 255, 255), Color::RGB(50, 150, 150)) 56 | } else { 57 | (Color::RGB(200, 200, 200), Color::RGB(0, 0, 0)) 58 | }; 59 | 60 | for (col, rects) in vec![ 61 | (col_white, keyboard.white_keys(true)), 62 | (col_black, keyboard.black_keys()), 63 | ] 64 | .drain(..) 65 | { 66 | canvas.set_draw_color(col); 67 | for rect in rects.into_iter() { 68 | let rec = sdl2::rect::Rect::new( 69 | rect.x as i32, 70 | rect.y as i32, 71 | rect.width as u32, 72 | rect.height as u32, 73 | ); 74 | canvas.fill_rect(rec)?; 75 | } 76 | } 77 | Ok(()) 78 | } 79 | 80 | pub fn get_pressed_key_rectangles( 81 | keyboard: &piano_keyboard::Keyboard2d, 82 | height_offset: u32, 83 | pos_us: i64, 84 | show_events: &Vec<(u64, usize, midi_sequencer::MidiEvent)>, 85 | ) -> Vec { 86 | let nr_of_keys = keyboard.right_white_key - keyboard.left_white_key + 1; 87 | let mut pressed = vec![0; nr_of_keys as usize]; 88 | let left_key = keyboard.left_white_key; 89 | 90 | for (time, _, evt) in show_events.iter() { 91 | // TODO: This needs more work. Adjacent midi notes are shown continuously... 92 | if (*time as i64) > pos_us { 93 | break; 94 | } 95 | if (*time as i64) + 50_000 > pos_us { 96 | match evt { 97 | midi_sequencer::MidiEvent::NoteOn(_channel, key, pressure) => { 98 | pressed[(key - left_key) as usize] = *pressure 99 | } 100 | _ => (), 101 | } 102 | } else { 103 | match evt { 104 | midi_sequencer::MidiEvent::NoteOn(_channel, key, pressure) => { 105 | pressed[(key - left_key) as usize] = *pressure 106 | } 107 | midi_sequencer::MidiEvent::NoteOff(_channel, key, _) => { 108 | pressed[(key - left_key) as usize] = 0 109 | } 110 | _ => (), 111 | } 112 | } 113 | } 114 | 115 | let mut highlight = vec![]; 116 | for (el, is_pressed) in keyboard.iter().zip(pressed.iter()) { 117 | if *is_pressed > 0 { 118 | let rects = match *el { 119 | piano_keyboard::Element::WhiteKey { 120 | wide: ref r1, 121 | small: ref r2, 122 | blind: Some(ref r3), 123 | } => vec![r1, r2, r3], 124 | piano_keyboard::Element::WhiteKey { 125 | wide: ref r1, 126 | small: ref r2, 127 | blind: None, 128 | } => vec![r1, r2], 129 | piano_keyboard::Element::BlackKey(ref r1) => vec![r1], 130 | }; 131 | for r in rects.into_iter() { 132 | let src_rect = 133 | sdl2::rect::Rect::new(r.x as i32, r.y as i32, r.width as u32, r.height as u32); 134 | let dst_rect = sdl2::rect::Rect::new( 135 | r.x as i32, 136 | (r.y as u32 + height_offset) as i32, 137 | r.width as u32, 138 | r.height as u32, 139 | ); 140 | let cmd = DrawCommand::CopyToScreen { 141 | src_texture: 1, 142 | src_rect, 143 | dst_rect, 144 | }; 145 | highlight.push(cmd); 146 | } 147 | } 148 | } 149 | highlight 150 | } 151 | 152 | pub fn draw_waterfall( 153 | keyboard: &piano_keyboard::Keyboard2d, 154 | canvas: &mut sdl2::render::Canvas, 155 | i: u32, 156 | bottom_row: u32, 157 | net_rows: u32, 158 | overlap: u32, 159 | rows_per_s: u32, 160 | show_events: &Vec<(u64, usize, midi_sequencer::MidiEvent)>, 161 | ) { 162 | // The waterfall is flowing from top to bottom with SDL having origin top left. 163 | // Thus every texture has to fill from bottom to top. 164 | 165 | if false { 166 | let i = (i & 1) as u8 * 40; 167 | canvas.set_draw_color(sdl2::pixels::Color::RGB(100 + i, 100 + i, 100 + i)); 168 | } else { 169 | canvas.set_draw_color(sdl2::pixels::Color::RGB(100, 100, 100)); 170 | } 171 | canvas.clear(); 172 | 173 | let left_key = keyboard.left_white_key; 174 | let mut rect_templates: Vec = vec![]; 175 | for el in keyboard.iter() { 176 | let (x, width) = match *el { 177 | piano_keyboard::Element::WhiteKey { 178 | wide: _, 179 | small: ref r1, 180 | blind: Some(ref r2), 181 | } => (r1.x.min(r2.x), r1.width + r2.width), 182 | piano_keyboard::Element::WhiteKey { 183 | wide: _, 184 | small: ref r, 185 | blind: None, 186 | } 187 | | piano_keyboard::Element::BlackKey(ref r) => (r.x, r.width), 188 | }; 189 | rect_templates.push(sdl2::rect::Rect::new(x as i32, 0, width as u32, 0)); 190 | } 191 | 192 | for (i, _el) in keyboard.iter().enumerate() { 193 | let sel_key = left_key + i as u8; 194 | let mut opt_start = None; 195 | let mut opt_end = None; 196 | for (time, trk, evt) in show_events.iter() { 197 | let col = trk2col(*trk, sel_key); 198 | match evt { 199 | midi_sequencer::MidiEvent::NoteOn(_channel, key, pressure) 200 | if *key == sel_key && *pressure > 0 => 201 | { 202 | opt_start = Some((time * rows_per_s as u64 / 1_000_000) as u32); 203 | trace!("{}: {:?} {:?}", time, evt, opt_start); 204 | } 205 | midi_sequencer::MidiEvent::NoteOn(_channel, key, 0) 206 | | midi_sequencer::MidiEvent::NoteOff(_channel, key, _) 207 | if *key == sel_key => 208 | { 209 | opt_end = Some((time * rows_per_s as u64 / 1_000_000) as u32); 210 | trace!("{}: {:?} {:?}", time, evt, opt_end); 211 | } 212 | _ => continue, 213 | } 214 | match (opt_start, opt_end) { 215 | (Some(start), Some(end)) => { 216 | let top_row = bottom_row + net_rows + overlap - 1; 217 | if start > top_row { 218 | continue; 219 | } 220 | if end <= bottom_row { 221 | continue; 222 | } 223 | trace!("start/end = {}/{}", start, end); 224 | let start_row = start.max(bottom_row); 225 | let end_row = end.min(top_row); 226 | trace!("{} {}", start_row, end_row); 227 | let height = end_row - start_row + 1; 228 | let tex_y = top_row - end_row; // flip 229 | 230 | let mut rec = rect_templates[i].clone(); 231 | rec.set_y(tex_y as i32); 232 | rec.set_height(height); 233 | trace!("Need draw: {:?}", rec); 234 | let rounding = rec.width() as i16 / 2 - 1; 235 | // later change to draw two circles and a rectangle 236 | canvas 237 | .rounded_box( 238 | rec.left() as i16, 239 | rec.bottom() as i16 - rounding as i16 / 2 + 1, 240 | rec.right() as i16, 241 | rec.top() as i16 + rounding as i16 / 2 - 1, 242 | rounding, 243 | col, 244 | ) 245 | .unwrap(); 246 | opt_start = None; 247 | opt_end = None; 248 | } 249 | (None, Some(_)) => { 250 | opt_end = None; 251 | warn!("Note Off with Note On should not happen") 252 | } 253 | _ => (), 254 | } 255 | } 256 | } 257 | } 258 | 259 | pub fn copy_waterfall_to_screen( 260 | n: usize, 261 | wf_width: u32, 262 | wf_height: u32, 263 | net_rows: u32, 264 | overlap: u32, 265 | rows_per_s: u32, 266 | pos_us: i64, 267 | ) -> Vec { 268 | trace!( 269 | "copy_wf_to_screen: n={} wf_width={} wf_height={}", 270 | n, 271 | wf_width, 272 | wf_height 273 | ); 274 | trace!( 275 | " net_rows={} overlap={} rows_per_s={} pos_us={}", 276 | net_rows, 277 | overlap, 278 | rows_per_s, 279 | pos_us 280 | ); 281 | // if pos_us = 0, then first texture bottom need to reach keyboard 282 | // Thus 283 | // src_rect.x=net_rows+overlap-height,src_rect.height=height 284 | // dst_rect.x=0,dst_rect.height=height 285 | 286 | // rows to display 287 | // top/bottom as on display visible, with y(top) < y(bottom) 288 | let wf_row_top = pos_us * rows_per_s as i64 / 1_000_000; 289 | let wf_row_bottom = wf_row_top + wf_height as i64 - 1; 290 | 291 | let mut commands = vec![]; 292 | for i in 0..n { 293 | // Texture i covers these total rows 294 | let tex_row_top = (i as u32 * net_rows) as i64; 295 | let tex_row_bottom = tex_row_top + net_rows as i64 - 1; 296 | 297 | // The intersection with the canvas top/bottom row is the region to copy 298 | let copy_row_top = wf_row_top.max(tex_row_top); 299 | let copy_row_bottom = wf_row_bottom.min(tex_row_bottom); 300 | debug!( 301 | "Texture {}: Overlap texture {}-{}<>requested area {}-{} => {}-{}", 302 | i, 303 | tex_row_top, 304 | tex_row_bottom, 305 | wf_row_top, 306 | wf_row_bottom, 307 | copy_row_top, 308 | copy_row_bottom 309 | ); 310 | 311 | // If the intersection does not contain rows, continue with next texture 312 | if copy_row_top > copy_row_bottom { 313 | continue; 314 | } 315 | 316 | #[cfg(test)] 317 | println!( 318 | "Texture {}: Overlap texture {}-{}<>requested area {}-{} => {}-{}", 319 | i, 320 | tex_row_top, 321 | tex_row_bottom, 322 | wf_row_top, 323 | wf_row_bottom, 324 | copy_row_top, 325 | copy_row_bottom 326 | ); 327 | 328 | // The number of intersecting rows: 329 | let cp_height = (copy_row_bottom - copy_row_top + 1) as u32; 330 | 331 | // The distance from wf_row_bottom to copy_row_bottom 332 | // yields the y_shift 333 | let y_dst_bottom = wf_height as i64 - (wf_row_bottom - copy_row_bottom) - 1; 334 | let y_dst_top = (y_dst_bottom - (cp_height as i64 - 1)) as i32; 335 | let y_dst = wf_height as i32 - y_dst_top - cp_height as i32; 336 | #[cfg(test)] 337 | println!( 338 | "y_dst_bottom={} y_dst_top={} y_dst={} bottom={}", 339 | y_dst_bottom, 340 | y_dst_top, 341 | y_dst, 342 | wf_height - 1 343 | ); 344 | 345 | let y_src = (overlap + net_rows) as i32 346 | - cp_height as i32 347 | - (copy_row_top - tex_row_top).max(0) as i32; 348 | 349 | let src_rect = sdl2::rect::Rect::new(0, y_src, wf_width, cp_height); 350 | let dst_rect = sdl2::rect::Rect::new(0, y_dst, wf_width, cp_height); 351 | trace!(target: "copy_texture", "Copy {:?}->{:?}", src_rect, dst_rect); 352 | let cmd = DrawCommand::CopyToScreen { 353 | src_texture: i + 2, 354 | src_rect, 355 | dst_rect, 356 | }; 357 | commands.push(cmd); 358 | } 359 | commands 360 | } 361 | #[cfg(test)] 362 | mod tests { 363 | use crate::draw_engine; 364 | 365 | #[test] 366 | fn test_01() { 367 | let n = 28; 368 | let wf_width = 4096; 369 | let wf_height = 1515; 370 | let net_rows = 907; 371 | let overlap = 93; 372 | let rows_per_s = 100; 373 | let pos_us = 6199732; 374 | let mut cmds = draw_engine::copy_waterfall_to_screen( 375 | n, wf_width, wf_height, net_rows, overlap, rows_per_s, pos_us, 376 | ); 377 | assert_eq!(cmds.len(), 3); 378 | let mut dst_total_height = 0; 379 | let last = cmds.pop().unwrap(); 380 | match last { 381 | draw_engine::DrawCommand::CopyToScreen { 382 | src_texture, 383 | src_rect, 384 | dst_rect, 385 | } => { 386 | // sdl bottom is UNDER the rectangle.... 387 | assert_eq!(src_rect.height() as i32 + src_rect.top(), src_rect.bottom()); 388 | 389 | // Last texture is on top. So destination y must be 0 and source y max 390 | assert_eq!(src_texture, 4); 391 | assert_eq!(src_rect.left(), 0); 392 | assert_eq!(src_rect.width(), wf_width); 393 | assert_eq!(dst_rect.left(), 0); 394 | assert_eq!(dst_rect.width(), wf_width); 395 | assert_eq!(src_rect.height(), dst_rect.height()); 396 | assert_eq!((overlap + net_rows) as i32, src_rect.bottom()); 397 | dst_total_height += dst_rect.height(); 398 | assert_eq!(dst_rect.top(), 0); 399 | } 400 | } 401 | let second = cmds.pop().unwrap(); 402 | match second { 403 | draw_engine::DrawCommand::CopyToScreen { 404 | src_texture, 405 | src_rect, 406 | dst_rect, 407 | } => { 408 | // Middle texture is in the middle 409 | assert_eq!(src_texture, 3); 410 | assert_eq!(src_rect.left(), 0); 411 | assert_eq!(src_rect.width(), wf_width); 412 | assert_eq!(dst_rect.left(), 0); 413 | assert_eq!(dst_rect.width(), wf_width); 414 | assert_eq!(src_rect.height(), dst_rect.height()); 415 | dst_total_height += dst_rect.height(); 416 | } 417 | } 418 | let first = cmds.pop().unwrap(); 419 | match first { 420 | draw_engine::DrawCommand::CopyToScreen { 421 | src_texture, 422 | src_rect, 423 | dst_rect, 424 | } => { 425 | // First texture is at the bottom. So destination y must be max 426 | // and source y equal overlap 427 | assert_eq!(src_texture, 2); 428 | assert_eq!(src_rect.left(), 0); 429 | assert_eq!(src_rect.width(), wf_width); 430 | assert_eq!(dst_rect.left(), 0); 431 | assert_eq!(dst_rect.width(), wf_width); 432 | assert_eq!(src_rect.top(), overlap as i32); 433 | assert_eq!(src_rect.height(), dst_rect.height()); 434 | dst_total_height += dst_rect.height(); 435 | assert_eq!(dst_rect.bottom(), wf_height as i32); 436 | } 437 | } 438 | assert_eq!(dst_total_height, wf_height); 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /src/stderrlog.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2018 Doug Goldstein 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | #![doc(html_root_url = "https://docs.rs/stderrlog/0.4.1")] 10 | 11 | //! A simple logger to provide symantics similar to what is expected 12 | //! of most UNIX utilities by logging to stderr and the higher the 13 | //! verbosity the higher the log level. It supports the 14 | //! ability to provide timestamps at different granularities. As 15 | //! well as colorizing the different log levels. 16 | //! 17 | //! ## Simple Use Case 18 | //! 19 | //! ```rust 20 | //! #[macro_use] 21 | //! extern crate log; 22 | //! extern crate stderrlog; 23 | //! 24 | //! fn main() { 25 | //! stderrlog::new().module(module_path!()).init().unwrap(); 26 | //! 27 | //! error!("some failure"); 28 | //! 29 | //! // ... 30 | //! } 31 | //! ``` 32 | //! 33 | //! # StructOpt Example 34 | //! 35 | //! ``` 36 | //! #[macro_use] 37 | //! extern crate log; 38 | //! extern crate stderrlog; 39 | //! #[macro_use] 40 | //! extern crate structopt; 41 | //! 42 | //! use structopt::StructOpt; 43 | //! 44 | //! /// A StructOpt example 45 | //! #[derive(StructOpt, Debug)] 46 | //! #[structopt()] 47 | //! struct Opt { 48 | //! /// Silence all output 49 | //! #[structopt(short = "q", long = "quiet")] 50 | //! quiet: bool, 51 | //! /// Verbose mode (-v, -vv, -vvv, etc) 52 | //! #[structopt(short = "v", long = "verbose", parse(from_occurrences))] 53 | //! verbose: usize, 54 | //! /// Timestamp (sec, ms, ns, none) 55 | //! #[structopt(short = "t", long = "timestamp")] 56 | //! ts: Option, 57 | //! } 58 | //! 59 | //! fn main() { 60 | //! let opt = Opt::from_args(); 61 | //! 62 | //! stderrlog::new() 63 | //! .module(module_path!()) 64 | //! .quiet(opt.quiet) 65 | //! .verbosity(opt.verbose) 66 | //! .timestamp(opt.ts.unwrap_or(stderrlog::Timestamp::Off)) 67 | //! .init() 68 | //! .unwrap(); 69 | //! trace!("trace message"); 70 | //! debug!("debug message"); 71 | //! info!("info message"); 72 | //! warn!("warn message"); 73 | //! error!("error message"); 74 | //! } 75 | //! ``` 76 | //! 77 | //! ## docopt Example 78 | //! 79 | //! ```rust 80 | //! extern crate docopt; 81 | //! #[macro_use] 82 | //! extern crate log; 83 | //! extern crate rustc_serialize; 84 | //! extern crate stderrlog; 85 | //! 86 | //! use docopt::Docopt; 87 | //! 88 | //! const USAGE: &'static str = " 89 | //! Usage: program [-q] [-v...] 90 | //! "; 91 | //! 92 | //! #[derive(Debug, RustcDecodable)] 93 | //! struct Args { 94 | //! flag_q: bool, 95 | //! flag_v: usize, 96 | //! } 97 | //! 98 | //! fn main() { 99 | //! let args: Args = Docopt::new(USAGE) 100 | //! .and_then(|d| d.decode()) 101 | //! .unwrap_or_else(|e| e.exit()); 102 | //! 103 | //! stderrlog::new() 104 | //! .module(module_path!()) 105 | //! .quiet(args.flag_q) 106 | //! .timestamp(stderrlog::Timestamp::Second) 107 | //! .verbosity(args.flag_v) 108 | //! .init() 109 | //! .unwrap(); 110 | //! trace!("trace message"); 111 | //! debug!("debug message"); 112 | //! info!("info message"); 113 | //! warn!("warn message"); 114 | //! error!("error message"); 115 | //! 116 | //! // ... 117 | //! } 118 | //! ``` 119 | //! 120 | //! # clap Example 121 | //! 122 | //! ``` 123 | //! #[macro_use] 124 | //! extern crate clap; 125 | //! #[macro_use] 126 | //! extern crate log; 127 | //! extern crate stderrlog; 128 | //! 129 | //! use clap::{Arg, App}; 130 | //! use std::str::FromStr; 131 | //! 132 | //! fn main() { 133 | //! let m = App::new("stderrlog example") 134 | //! .version(crate_version!()) 135 | //! .arg(Arg::with_name("verbosity") 136 | //! .short("v") 137 | //! .multiple(true) 138 | //! .help("Increase message verbosity")) 139 | //! .arg(Arg::with_name("quiet") 140 | //! .short("q") 141 | //! .help("Silence all output")) 142 | //! .arg(Arg::with_name("timestamp") 143 | //! .short("t") 144 | //! .help("prepend log lines with a timestamp") 145 | //! .takes_value(true) 146 | //! .possible_values(&["none", "sec", "ms", "ns"])) 147 | //! .get_matches(); 148 | //! 149 | //! let verbose = m.occurrences_of("verbosity") as usize; 150 | //! let quiet = m.is_present("quiet"); 151 | //! let ts = m.value_of("timestamp").map(|v| { 152 | //! stderrlog::Timestamp::from_str(v).unwrap_or_else(|_| { 153 | //! clap::Error { 154 | //! message: "invalid value for 'timestamp'".into(), 155 | //! kind: clap::ErrorKind::InvalidValue, 156 | //! info: None, 157 | //! }.exit() 158 | //! }) 159 | //! }).unwrap_or(stderrlog::Timestamp::Off); 160 | //! 161 | //! stderrlog::new() 162 | //! .module(module_path!()) 163 | //! .quiet(quiet) 164 | //! .verbosity(verbose) 165 | //! .timestamp(ts) 166 | //! .init() 167 | //! .unwrap(); 168 | //! trace!("trace message"); 169 | //! debug!("debug message"); 170 | //! info!("info message"); 171 | //! warn!("warn message"); 172 | //! error!("error message"); 173 | //! } 174 | //! ``` 175 | //! 176 | //! ### `log` Compatibility 177 | //! 178 | //! The 0.3.x versions of `stderrlog` aim to provide compatibility with 179 | //! applications using `log` 0.3.x 180 | //! 181 | //! ### Rust Compatibility 182 | //! 183 | //! `stderrlog` is serious about backwards compat. `stderrlog` 184 | //! pins the minimum required version of Rust in the CI build. 185 | //! Bumping the minimum version of Rust is a minor breaking 186 | //! change and requires a minor version to be bumped. 187 | //! 188 | //! The minimum supported Rust version for this release is 1.16.0. 189 | //! 190 | //! ### Module Level Logging 191 | //! 192 | //! `stderrlog` has the ability to limit the components which can log. 193 | //! Many crates use [log](https://docs.rs/log/*/log/) but you may not 194 | //! want their output in your application. For example 195 | //! [hyper](https://docs.rs/hyper/*/hyper/) makes heavy use of log but 196 | //! when your application receives `-vvvvv` to enable the `trace!()` 197 | //! messages you don't want the output of `hyper`'s `trace!()` level. 198 | //! 199 | //! To support this `stderrlog` includes a `module()` method allowing 200 | //! you to specify the modules that are allowed to log. The examples 201 | //! above use the `module_path!()` macro to enable logging only for 202 | //! the binary itself but none of its dependencies. To enable logging 203 | //! from extra crates just add another call to `module()` with the 204 | //! name of the crate. To enable logging for only a module within 205 | //! that crate specifiy `crate::module` to `module()`. crates and 206 | //! modules will be named the same way would would include them in 207 | //! source code with `use` (e.g. `some-crate` would be `some_crate`). 208 | //! 209 | //! For a good example of how the module level logging works see the 210 | //! [large-example 211 | //! crate](https://github.com/cardoe/stderrlog-rs/tree/master/examples/large-example) 212 | //! under examples, you'll want to run the 213 | //! following binaries to see all the examples: 214 | //! 215 | //! - `cargo run --bin large-example --` 216 | //! - `cargo run --bin another --` 217 | //! - `cargo run --bin yet --` 218 | 219 | #![doc(html_root_url = "https://docs.rs/stderrlog/0.4.0")] 220 | 221 | extern crate chrono; 222 | extern crate log; 223 | extern crate termcolor; 224 | extern crate thread_local; 225 | 226 | use chrono::Local; 227 | use log::{Level, LevelFilter, Log, Metadata, Record}; 228 | use std::cell::RefCell; 229 | use std::fmt; 230 | use std::io::{self, Write}; 231 | use std::str::FromStr; 232 | use termcolor::{Color, ColorSpec, StandardStream, WriteColor}; 233 | 234 | pub use termcolor::ColorChoice; 235 | use thread_local::CachedThreadLocal; 236 | 237 | /// State of the timestampping in the logger. 238 | #[derive(Clone, Copy, Debug)] 239 | pub enum Timestamp { 240 | /// Disable timestamping of log messages 241 | Off, 242 | /// Timestamp with second granularity 243 | Second, 244 | /// Timestamp with millisecond granularity 245 | Millisecond, 246 | /// Timestamp with microsecond granularity 247 | Microsecond, 248 | /// Timestamp with nanosecond granularity 249 | Nanosecond, 250 | } 251 | 252 | /// Provides a quick conversion of the following: 253 | /// 254 | /// - "sec" -> `Timestamp::Second` 255 | /// - "ms" -> `Timestamp::Millisecond` 256 | /// - "us" -> `Timestamp::Microsecond` 257 | /// - "ns" -> `Timestamp::Nanosecond` 258 | /// - "none" | "off" -> `Timestamp::Off` 259 | /// 260 | /// This is provided as a helper for argument parsers 261 | impl FromStr for Timestamp { 262 | type Err = String; 263 | 264 | fn from_str(s: &str) -> Result { 265 | match s { 266 | "ns" => Ok(Timestamp::Nanosecond), 267 | "ms" => Ok(Timestamp::Millisecond), 268 | "us" => Ok(Timestamp::Microsecond), 269 | "sec" => Ok(Timestamp::Second), 270 | "none" | "off" => Ok(Timestamp::Off), 271 | _ => Err("invalid value".into()), 272 | } 273 | } 274 | } 275 | 276 | /// Data specific to this logger 277 | pub struct StdErrLog { 278 | verbosity: LevelFilter, 279 | quiet: bool, 280 | timestamp: Timestamp, 281 | modules: Vec, 282 | writer: CachedThreadLocal>>, 283 | color_choice: ColorChoice, 284 | } 285 | 286 | impl fmt::Debug for StdErrLog { 287 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 288 | f.debug_struct("StdErrLog") 289 | .field("verbosity", &self.verbosity) 290 | .field("quiet", &self.quiet) 291 | .field("timestamp", &self.timestamp) 292 | .field("modules", &self.modules) 293 | .field("writer", &"stderr") 294 | .field("color_choice", &self.color_choice) 295 | .finish() 296 | } 297 | } 298 | 299 | impl Clone for StdErrLog { 300 | fn clone(&self) -> StdErrLog { 301 | StdErrLog { 302 | modules: self.modules.clone(), 303 | writer: CachedThreadLocal::new(), 304 | ..*self 305 | } 306 | } 307 | } 308 | 309 | impl Log for StdErrLog { 310 | fn enabled(&self, metadata: &Metadata) -> bool { 311 | metadata.level() <= self.log_level_filter() && self.includes_module(metadata.target()) 312 | } 313 | 314 | fn log(&self, record: &Record) { 315 | // if logging isn't enabled for this level do a quick out 316 | if !self.enabled(record.metadata()) { 317 | return; 318 | } 319 | 320 | let writer = self.writer.get_or(|| { 321 | RefCell::new(io::LineWriter::new(StandardStream::stderr( 322 | self.color_choice, 323 | ))) 324 | }); 325 | let mut writer = writer.borrow_mut(); 326 | let color = match record.metadata().level() { 327 | Level::Error => Color::Red, 328 | Level::Warn => Color::Magenta, 329 | Level::Info => Color::Yellow, 330 | Level::Debug => Color::Cyan, 331 | Level::Trace => Color::Blue, 332 | }; 333 | { 334 | writer 335 | .get_mut() 336 | .set_color(ColorSpec::new().set_fg(Some(color))) 337 | .expect("failed to set color"); 338 | } 339 | match self.timestamp { 340 | Timestamp::Second => { 341 | let fmt = "%Y-%m-%dT%H:%M:%S%:z"; 342 | let _ = write!(writer, "{} - ", Local::now().format(fmt)); 343 | } 344 | Timestamp::Millisecond => { 345 | let fmt = "%Y-%m-%dT%H:%M:%S%.3f%:z"; 346 | let _ = write!(writer, "{} - ", Local::now().format(fmt)); 347 | } 348 | Timestamp::Microsecond => { 349 | let fmt = "%H:%M:%S%.6f"; //HACK 350 | let _ = write!(writer, "{}:", Local::now().format(fmt)); 351 | } 352 | Timestamp::Nanosecond => { 353 | let fmt = "%Y-%m-%dT%H:%M:%S%.9f%:z"; 354 | let _ = write!(writer, "{} - ", Local::now().format(fmt)); 355 | } 356 | Timestamp::Off => {} 357 | } 358 | let _ = writeln!( 359 | writer, 360 | "{}:{}:{}", 361 | record.level(), 362 | record.target(), 363 | record.args() 364 | ); 365 | { 366 | writer.get_mut().reset().expect("failed to reset the color"); 367 | } 368 | } 369 | 370 | fn flush(&self) { 371 | let writer = self.writer.get_or(|| { 372 | RefCell::new(io::LineWriter::new(StandardStream::stderr( 373 | self.color_choice, 374 | ))) 375 | }); 376 | let mut writer = writer.borrow_mut(); 377 | writer.flush().ok(); 378 | } 379 | } 380 | 381 | impl StdErrLog { 382 | /// creates a new stderr logger 383 | pub fn new() -> StdErrLog { 384 | StdErrLog { 385 | verbosity: LevelFilter::Error, 386 | quiet: false, 387 | timestamp: Timestamp::Off, 388 | modules: Vec::new(), 389 | writer: CachedThreadLocal::new(), 390 | color_choice: ColorChoice::Auto, 391 | } 392 | } 393 | 394 | /// Sets the verbosity level of messages that will be displayed 395 | pub fn verbosity(&mut self, verbosity: usize) -> &mut StdErrLog { 396 | let log_lvl = match verbosity { 397 | 0 => LevelFilter::Error, 398 | 1 => LevelFilter::Warn, 399 | 2 => LevelFilter::Info, 400 | 3 => LevelFilter::Debug, 401 | _ => LevelFilter::Trace, 402 | }; 403 | 404 | self.verbosity = log_lvl; 405 | self 406 | } 407 | 408 | /// silence all output, no matter the value of verbosity 409 | pub fn quiet(&mut self, quiet: bool) -> &mut StdErrLog { 410 | self.quiet = quiet; 411 | self 412 | } 413 | 414 | /// Enables or disables the use of timestamps in log messages 415 | pub fn timestamp(&mut self, timestamp: Timestamp) -> &mut StdErrLog { 416 | self.timestamp = timestamp; 417 | self 418 | } 419 | 420 | /// Enables or disables the use of color in log messages 421 | #[allow(dead_code)] 422 | pub fn color(&mut self, choice: ColorChoice) -> &mut StdErrLog { 423 | self.color_choice = choice; 424 | self 425 | } 426 | 427 | /// specify a module to allow to log to stderr 428 | pub fn module>(&mut self, module: T) -> &mut StdErrLog { 429 | self._module(module.into()) 430 | } 431 | 432 | fn _module(&mut self, module: String) -> &mut StdErrLog { 433 | // If Ok, the module was already found 434 | if let Err(i) = self.modules.binary_search(&module) { 435 | // If a super-module of the current module already exists, don't insert this module 436 | if i == 0 || !is_submodule(&self.modules[i - 1], &module) { 437 | // Remove any submodules of the module we're inserting 438 | let submodule_count = self.modules[i..] 439 | .iter() 440 | .take_while(|possible_submodule| is_submodule(&module, possible_submodule)) 441 | .count(); 442 | self.modules.drain(i..i + submodule_count); 443 | self.modules.insert(i, module); 444 | } 445 | } 446 | self 447 | } 448 | 449 | /// specifiy modules to allow to log to stderr 450 | pub fn modules, I: IntoIterator>( 451 | &mut self, 452 | modules: I, 453 | ) -> &mut StdErrLog { 454 | for module in modules { 455 | self.module(module); 456 | } 457 | self 458 | } 459 | 460 | fn log_level_filter(&self) -> LevelFilter { 461 | if self.quiet { 462 | LevelFilter::Off 463 | } else { 464 | self.verbosity 465 | } 466 | } 467 | 468 | fn includes_module(&self, module_path: &str) -> bool { 469 | // If modules is empty, include all module paths 470 | if self.modules.is_empty() { 471 | return true; 472 | } 473 | // if a prefix of module_path is in `self.modules`, it must 474 | // be located at the first location before 475 | // where module_path would be. 476 | match self 477 | .modules 478 | .binary_search_by(|module| module.as_str().cmp(&module_path)) 479 | { 480 | Ok(_) => { 481 | // Found exact module: return true 482 | true 483 | } 484 | Err(0) => { 485 | // if there's no item which would be located before module_path, no prefix is there 486 | false 487 | } 488 | Err(i) => is_submodule(&self.modules[i - 1], module_path), 489 | } 490 | } 491 | 492 | /// sets the the logger as active 493 | pub fn init(&self) -> Result<(), log::SetLoggerError> { 494 | log::set_max_level(self.log_level_filter()); 495 | log::set_boxed_logger(Box::new(self.clone())) 496 | } 497 | } 498 | 499 | impl Default for StdErrLog { 500 | fn default() -> Self { 501 | StdErrLog::new() 502 | } 503 | } 504 | 505 | /// creates a new stderr logger 506 | pub fn new() -> StdErrLog { 507 | StdErrLog::new() 508 | } 509 | 510 | fn is_submodule(parent: &str, possible_child: &str) -> bool { 511 | // Treat as bytes, because we'll be doing slicing, and we only care about ':' chars 512 | let parent = parent.as_bytes(); 513 | let possible_child = possible_child.as_bytes(); 514 | 515 | // a longer module path cannot be a parent of a shorter module path 516 | if parent.len() > possible_child.len() { 517 | return false; 518 | } 519 | 520 | // If the path up to the parent isn't the same as the child, 521 | if parent != &possible_child[..parent.len()] { 522 | return false; 523 | } 524 | 525 | // Either the path is exactly the same, or the sub module should have a "::" after 526 | // the length of the parent path. This prevents things like 'a::bad' being considered 527 | // a submodule of 'a::b' 528 | parent.len() == possible_child.len() 529 | || possible_child.get(parent.len()..parent.len() + 2) == Some(b"::") 530 | } 531 | 532 | #[cfg(test)] 533 | mod tests { 534 | use super::is_submodule; 535 | 536 | #[test] 537 | fn submodule() { 538 | assert!(is_submodule("a", "a::b::c::d")); 539 | assert!(is_submodule("a::b::c", "a::b::c::d")); 540 | assert!(is_submodule("a::b::c", "a::b::c")); 541 | assert!(!is_submodule("a::b::c", "a::bad::c")); 542 | assert!(!is_submodule("a::b::c", "a::b::cab")); 543 | assert!(!is_submodule("a::b::c", "a::b::cab::d")); 544 | assert!(!is_submodule("a::b::c", "a::b")); 545 | assert!(!is_submodule("a::b::c", "a::bad")); 546 | } 547 | 548 | #[test] 549 | fn test_default_level() { 550 | extern crate log; 551 | 552 | super::new().module(module_path!()).init().unwrap(); 553 | 554 | assert_eq!(log::Level::Error, log::max_level()) 555 | } 556 | } 557 | -------------------------------------------------------------------------------- /src/app_control.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Error, ErrorKind}; 2 | use std::sync::mpsc; 3 | use std::thread; 4 | use std::time::Duration; 5 | 6 | use clap::ArgMatches; 7 | use clap::{value_t, values_t}; 8 | use log::*; 9 | use midly; 10 | 11 | use crate::midi_container::MidiContainer; 12 | use crate::midi_sequencer::MidiEvent; 13 | use crate::midi_sequencer::MidiSequencer; 14 | use crate::midi_sequencer::RawMidiTuple; 15 | use crate::scroller::Scroller; 16 | use crate::time_controller::TimeListener; 17 | use crate::time_controller::TimeListenerTrait; 18 | 19 | const WK: &str = &"worker"; 20 | 21 | enum WorkerResult { 22 | EventsLoaded(Result<(Vec, Vec), std::io::Error>), 23 | KeyboardBuilt(Result), 24 | } 25 | 26 | #[derive(Debug)] 27 | enum AppState { 28 | Check, 29 | Wait, 30 | Running, 31 | } 32 | 33 | fn transposed_message( 34 | time_us: u64, 35 | trk: usize, 36 | channel: u8, 37 | message: &midly::MidiMessage, 38 | all: bool, 39 | shift_key: i8, 40 | left_key: u8, 41 | right_key: u8, 42 | ) -> Option<(u64, usize, MidiEvent)> { 43 | match (message, all) { 44 | (midly::MidiMessage::NoteOn { key, vel }, _) => { 45 | let shifted_key = key.as_int() as i16 + shift_key as i16; 46 | if shifted_key < left_key as i16 || shifted_key > right_key as i16 { 47 | None 48 | } else { 49 | Some(( 50 | time_us, 51 | trk, 52 | MidiEvent::NoteOn(channel, shifted_key as u8, vel.as_int()), 53 | )) 54 | } 55 | } 56 | (midly::MidiMessage::NoteOff { key, vel }, _) => { 57 | let shifted_key = key.as_int() as i16 + shift_key as i16; 58 | if shifted_key < left_key as i16 || shifted_key > right_key as i16 { 59 | None 60 | } else { 61 | Some(( 62 | time_us, 63 | trk, 64 | MidiEvent::NoteOff(channel, shifted_key as u8, vel.as_int()), 65 | )) 66 | } 67 | } 68 | (midly::MidiMessage::Aftertouch { key, vel }, true) => { 69 | let shifted_key = key.as_int() as i16 + shift_key as i16; 70 | if shifted_key < left_key as i16 || shifted_key > right_key as i16 { 71 | None 72 | } else { 73 | Some(( 74 | time_us, 75 | trk, 76 | MidiEvent::Aftertouch(channel, shifted_key as u8, vel.as_int()), 77 | )) 78 | } 79 | } 80 | (midly::MidiMessage::Controller { controller, value }, true) => Some(( 81 | time_us, 82 | trk, 83 | MidiEvent::Controller(channel, controller.as_int(), value.as_int()), 84 | )), 85 | (midly::MidiMessage::ChannelAftertouch { vel }, true) => Some(( 86 | time_us, 87 | trk, 88 | MidiEvent::ChannelAftertouch(channel, vel.as_int()), 89 | )), 90 | (midly::MidiMessage::PitchBend { bend }, true) => { 91 | Some((time_us, trk, MidiEvent::PitchBend(channel, bend.as_int()))) 92 | } 93 | (midly::MidiMessage::ProgramChange { program }, true) => Some(( 94 | time_us, 95 | trk, 96 | MidiEvent::ProgramChange(channel, program.as_int()), 97 | )), 98 | (_, false) => None, 99 | } 100 | } 101 | 102 | pub struct AppControl { 103 | state: Option, 104 | midi_fname: String, 105 | command_list_tracks: bool, 106 | quiet: bool, 107 | debug: Option>, 108 | verbose: usize, 109 | paused: bool, 110 | scale_1000: u16, 111 | pos_us: i64, 112 | left_key: u8, 113 | right_key: u8, 114 | shift_key: i8, 115 | width: Option, 116 | need_redraw_textures: bool, 117 | request_events: bool, 118 | request_keyboard: bool, 119 | show_tracks: Vec, 120 | play_tracks: Vec, 121 | show_events: Option>, 122 | sequencer: Option, 123 | scroller: Scroller, 124 | time_keeper: Option, 125 | rx: mpsc::Receiver, 126 | tx: mpsc::Sender, 127 | event_worker: Option>, 128 | keyboard_worker: Option>, 129 | keyboard: Option, 130 | } 131 | impl AppControl { 132 | pub fn from_clap(matches: ArgMatches) -> AppControl { 133 | let (tx, rx) = mpsc::channel(); 134 | let quiet = matches.is_present("quiet"); 135 | let debug = if matches.is_present("debug") { 136 | Some(values_t!(matches.values_of("debug"), String).unwrap_or_else(|_| vec![])) 137 | } else { 138 | None 139 | }; 140 | let verbose = matches.occurrences_of("verbose") as usize; 141 | let shift_key = value_t!(matches, "transpose", i8).unwrap_or_else(|e| e.exit()); 142 | let rd64 = matches.is_present("RD64"); 143 | let (left_key, right_key): (u8, u8) = if rd64 { 144 | // RD-64 is A1 to C7 145 | (21 + 12, 108 - 12) 146 | } else { 147 | // 88 note piano range from A0 to C8 148 | (21, 108) 149 | }; 150 | let midi_fname = matches.value_of("MIDI").unwrap().to_string(); 151 | let list_tracks = matches.is_present("list"); 152 | let show_tracks = values_t!(matches.values_of("show"), usize).unwrap_or_else(|_| vec![]); 153 | let play_tracks = values_t!(matches.values_of("play"), usize).unwrap_or_else(|e| e.exit()); 154 | let scroller = Scroller::new(5_000_000.0); 155 | AppControl { 156 | state: Some(AppState::Check), 157 | midi_fname, 158 | command_list_tracks: list_tracks, 159 | quiet, 160 | debug, 161 | verbose, 162 | paused: false, 163 | scale_1000: 1000, 164 | pos_us: 0, 165 | width: None, 166 | left_key, 167 | right_key, 168 | shift_key, 169 | request_events: true, 170 | request_keyboard: false, 171 | need_redraw_textures: false, 172 | show_tracks, 173 | play_tracks, 174 | show_events: None, 175 | sequencer: None, 176 | scroller, 177 | time_keeper: None, 178 | rx, 179 | tx, 180 | event_worker: None, 181 | keyboard_worker: None, 182 | keyboard: None, 183 | } 184 | } 185 | pub fn toggle_play(&mut self) { 186 | self.paused = !self.paused; 187 | if let Some(seq) = self.sequencer.take() { 188 | if self.paused { 189 | seq.stop(); 190 | } else { 191 | seq.play(self.pos_us); 192 | } 193 | self.sequencer = Some(seq); 194 | } 195 | } 196 | pub fn modify_scaling(&mut self, increase: bool) { 197 | if let Some(seq) = self.sequencer.take() { 198 | self.scale_1000 = if increase { 199 | 4000.min(self.scale_1000 + 50) 200 | } else { 201 | 250.max(self.scale_1000 - 50) 202 | }; 203 | info!("New scaling: {}", self.scale_1000); 204 | seq.set_scaling_1000(self.scale_1000); 205 | self.sequencer = Some(seq); 206 | } 207 | } 208 | pub fn change_position(&mut self, forward: bool) { 209 | if let Some(seq) = self.sequencer.take() { 210 | self.pos_us = if forward { 211 | self.pos_us + 5_000_000 212 | } else { 213 | (self.pos_us - 5_000_000).max(-3_000_000) 214 | }; 215 | if self.paused { 216 | seq.set_pos_us(self.pos_us); 217 | } else { 218 | seq.stop(); 219 | seq.play(self.pos_us); 220 | } 221 | self.sequencer = Some(seq); 222 | } 223 | } 224 | pub fn tune_up(&mut self, tune_up: bool) { 225 | self.shift_key = if tune_up { 226 | self.shift_key.min(126) + 1 227 | } else { 228 | self.shift_key.max(-126) - 1 229 | }; 230 | self.request_events = true; 231 | } 232 | pub fn two_finger_scroll_start(&mut self, y: f32) { 233 | if !self.scroller.update_move(y) { 234 | if let Some(seq) = self.sequencer.take() { 235 | seq.stop(); 236 | self.sequencer = Some(seq); 237 | } 238 | } 239 | } 240 | pub fn finger_touch(&mut self) { 241 | if self.scroller.stop() && !self.paused { 242 | if let Some(seq) = self.sequencer.take() { 243 | seq.play(self.pos_us); 244 | self.sequencer = Some(seq); 245 | } 246 | } 247 | } 248 | pub fn finger_up(&mut self) { 249 | self.scroller.end_move(); 250 | } 251 | pub fn update_position_if_scrolling(&mut self) { 252 | if let Some((is_end, delta)) = self.scroller.update_position() { 253 | if let Some(seq) = self.sequencer.take() { 254 | if is_end && !self.paused { 255 | seq.play(self.pos_us + delta as i64); 256 | } else { 257 | seq.set_pos_us(self.pos_us + delta as i64); 258 | } 259 | self.sequencer = Some(seq); 260 | self.pos_us = self.pos_us + delta as i64; 261 | } 262 | } 263 | } 264 | pub fn is_quiet(&self) -> bool { 265 | self.quiet 266 | } 267 | pub fn is_debug(&self) -> Option<&Vec> { 268 | self.debug.as_ref() 269 | } 270 | pub fn verbosity(&self) -> usize { 271 | self.verbose 272 | } 273 | pub fn shift_key(&self) -> i8 { 274 | self.shift_key 275 | } 276 | pub fn left_key(&self) -> u8 { 277 | self.left_key 278 | } 279 | pub fn right_key(&self) -> u8 { 280 | self.right_key 281 | } 282 | pub fn midi_fname(&self) -> &str { 283 | &self.midi_fname 284 | } 285 | pub fn list_command(&self) -> bool { 286 | self.command_list_tracks 287 | } 288 | pub fn show_tracks(&self) -> &Vec { 289 | &self.show_tracks 290 | } 291 | pub fn play_tracks(&self) -> &Vec { 292 | &self.play_tracks 293 | } 294 | pub fn seq_is_finished(&mut self) -> bool { 295 | if let Some(seq) = self.sequencer.take() { 296 | let finished = seq.is_finished(); 297 | self.sequencer = Some(seq); 298 | finished 299 | } else { 300 | true 301 | } 302 | } 303 | pub fn show_events(&self) -> Option<&Vec> { 304 | self.show_events.as_ref() 305 | } 306 | pub fn show_events_len(&self) -> usize { 307 | self.show_events 308 | .as_ref() 309 | .map(|events| events.len()) 310 | .unwrap_or(0) 311 | } 312 | pub fn create_connected_sequencer( 313 | &mut self, 314 | exit_on_eof: bool, 315 | ) -> Result<(), Box> { 316 | let mut sequencer = MidiSequencer::new(exit_on_eof); 317 | sequencer.connect()?; 318 | self.time_keeper = Some(sequencer.get_new_listener()); 319 | self.sequencer = Some(sequencer); 320 | Ok(()) 321 | } 322 | pub fn get_pos_us_after(&mut self, dt_us: u32) -> i64 { 323 | let rem_dur = Duration::new(0, dt_us * 1_000); 324 | self.time_keeper.as_ref().unwrap().get_pos_us_after(rem_dur) 325 | } 326 | pub fn get_keyboard(&self) -> Option<&piano_keyboard::Keyboard2d> { 327 | self.keyboard.as_ref() 328 | } 329 | fn build_keyboard( 330 | width: u16, 331 | left_key: u8, 332 | right_key: u8, 333 | ) -> Result { 334 | Ok(piano_keyboard::KeyboardBuilder::new() 335 | .set_width(width)? 336 | .white_black_gap_present(true) 337 | .set_most_left_right_white_keys(left_key, right_key)? 338 | .build2d()) 339 | } 340 | pub fn read_midi_file( 341 | midi_fname: &str, 342 | left_key: u8, 343 | right_key: u8, 344 | shift_key: i8, 345 | show_tracks: Vec, 346 | play_tracks: Vec, 347 | ) -> Result<(Vec, Vec), std::io::Error> { 348 | let buf = std::fs::read(midi_fname)?; 349 | let smf_buf = midly::Smf::parse(&buf) 350 | .map_err(|e| Error::new(ErrorKind::Other, format!("{:?}", e)))?; 351 | let container = MidiContainer::from_buf(&smf_buf)?; 352 | let show_events = container 353 | .iter() 354 | .timed(&container.header().timing) 355 | .filter(|(_time_us, trk, _evt)| show_tracks.contains(trk)) 356 | .filter_map(|(time_us, trk, evt)| match evt { 357 | midly::EventKind::Midi { channel, message } => transposed_message( 358 | time_us, 359 | trk, 360 | channel.as_int(), 361 | &message, 362 | false, 363 | shift_key, 364 | left_key, 365 | right_key, 366 | ), 367 | _ => None, 368 | }) 369 | .collect::>(); 370 | let play_events = container 371 | .iter() 372 | .timed(&container.header().timing) 373 | .filter(|(_time_us, trk, _evt)| play_tracks.contains(trk)) 374 | .filter_map(|(time_us, trk, evt)| match evt { 375 | midly::EventKind::Midi { channel, message } => transposed_message( 376 | time_us, 377 | trk, 378 | channel.as_int(), 379 | &message, 380 | true, 381 | shift_key, 382 | left_key, 383 | right_key, 384 | ), 385 | _ => None, 386 | }) 387 | .inspect(|e| trace!("{:?}", e)) 388 | .collect::>(); 389 | Ok((show_events, play_events)) 390 | } 391 | fn load_keyboard(&mut self) { 392 | trace!(target: WK, "Start thread for building keyboard"); 393 | if self.keyboard_worker.is_none() { 394 | let tx = self.tx.clone(); 395 | let left_key = self.left_key; 396 | let right_key = self.right_key; 397 | if let Some(width) = self.width { 398 | let jh = thread::spawn(move || { 399 | let res = AppControl::build_keyboard(width, left_key, right_key); 400 | let res = res 401 | .map_err(|err_str| std::io::Error::new(std::io::ErrorKind::Other, err_str)); 402 | trace!(target: WK, "Send keyboard to main"); 403 | tx.send(WorkerResult::KeyboardBuilt(res)).unwrap(); 404 | }); 405 | self.keyboard_worker = Some(jh); 406 | } 407 | } 408 | } 409 | fn load_event(&mut self) { 410 | trace!(target: WK, "Start thread for reading the midi file"); 411 | if self.event_worker.is_none() { 412 | let tx = self.tx.clone(); 413 | let midi_fname = self.midi_fname.clone(); 414 | let left_key = self.left_key; 415 | let right_key = self.right_key; 416 | let shift_key = self.shift_key; 417 | let show_tracks = self.show_tracks.clone(); 418 | let play_tracks = self.play_tracks.clone(); 419 | let jh = thread::spawn(move || { 420 | let res = AppControl::read_midi_file( 421 | &midi_fname, 422 | left_key, 423 | right_key, 424 | shift_key, 425 | show_tracks, 426 | play_tracks, 427 | ); 428 | trace!(target: WK, "Send events to main"); 429 | tx.send(WorkerResult::EventsLoaded(res)).unwrap(); 430 | }); 431 | self.event_worker = Some(jh); 432 | } 433 | } 434 | pub fn next_loop(&mut self) { 435 | if let Some(time_keeper) = self.time_keeper.as_ref() { 436 | self.pos_us = time_keeper.get_pos_us(); 437 | } 438 | let th_result = self.rx.try_recv(); 439 | match th_result { 440 | Ok(WorkerResult::EventsLoaded(Ok((show_events, play_events)))) => { 441 | trace!(target: WK, "Events loaded"); 442 | self.event_worker 443 | .take() 444 | .unwrap() 445 | .join() 446 | .expect("something went wrong with worker thread"); 447 | trace!(target: WK, "Join worker done"); 448 | self.show_events = Some(show_events); 449 | if let Some(seq) = self.sequencer.take() { 450 | seq.set_midi_data(play_events); 451 | seq.play(self.pos_us); 452 | self.sequencer = Some(seq); 453 | } 454 | self.need_redraw_textures = true; 455 | self.request_events = self.show_events.is_none(); 456 | } 457 | Ok(WorkerResult::KeyboardBuilt(Ok(keyboard))) => { 458 | trace!(target: WK, "Keyboard built"); 459 | self.keyboard_worker 460 | .take() 461 | .unwrap() 462 | .join() 463 | .expect("something went wrong with worker thread"); 464 | trace!(target: WK, "Join worker done"); 465 | self.keyboard = Some(keyboard); 466 | self.request_keyboard = self.keyboard.is_none(); 467 | self.need_redraw_textures = true; 468 | } 469 | Ok(WorkerResult::EventsLoaded(Err(_))) => (), 470 | Ok(WorkerResult::KeyboardBuilt(Err(_))) => (), 471 | Err(_) => (), 472 | }; 473 | debug!("AppState: {:?}", self.state); 474 | let s = match self.state.take() { 475 | Some(AppState::Check) => { 476 | if self.request_events { 477 | self.load_event(); 478 | } 479 | if self.request_keyboard { 480 | self.load_keyboard(); 481 | } 482 | AppState::Wait 483 | } 484 | Some(AppState::Wait) => { 485 | if self.event_worker.is_none() && self.keyboard_worker.is_none() { 486 | if let Some(seq) = self.sequencer.take() { 487 | seq.stop(); 488 | if !self.paused { 489 | seq.play(self.pos_us); 490 | } 491 | self.sequencer = Some(seq); 492 | } 493 | 494 | AppState::Running 495 | } else { 496 | AppState::Wait 497 | } 498 | } 499 | Some(AppState::Running) => { 500 | if self.request_events { 501 | self.load_event(); 502 | } 503 | if self.request_keyboard { 504 | self.load_keyboard(); 505 | } 506 | AppState::Running 507 | } 508 | None => panic!("should not happen"), 509 | }; 510 | self.state = Some(s); 511 | } 512 | pub fn need_redraw(&mut self, width: u16) -> bool { 513 | let mut need = self.need_redraw_textures; 514 | if self.width != Some(width) { 515 | self.width = Some(width); 516 | self.request_keyboard = true; 517 | need = true; 518 | } 519 | self.need_redraw_textures = false; 520 | need 521 | } 522 | pub fn play_midi_data(&mut self, play_events: Vec) { 523 | if let Some(seq) = self.sequencer.take() { 524 | seq.set_midi_data(play_events); 525 | seq.play(0); 526 | self.sequencer = Some(seq); 527 | } 528 | } 529 | } 530 | --------------------------------------------------------------------------------