diff --git a/Cargo.lock b/Cargo.lock index 661278a..756f8dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -174,6 +174,31 @@ dependencies = [ "windows", ] +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi", +] + [[package]] name = "cty" version = "0.2.2" @@ -333,6 +358,18 @@ dependencies = [ "cc", ] +[[package]] +name = "mio" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + [[package]] name = "ndk" version = "0.6.0" @@ -684,6 +721,36 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +[[package]] +name = "signal-hook" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + [[package]] name = "slice-deque" version = "0.3.0" @@ -705,6 +772,7 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" name = "snoud" version = "0.1.0" dependencies = [ + "crossterm", "rodio", ] @@ -814,6 +882,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" version = "0.2.83" diff --git a/Cargo.toml b/Cargo.toml index fb5abd2..e3299b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,4 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +crossterm = "0.25.0" rodio = { version = "0.16.0", default_features = false, features = ["mp3"] } diff --git a/src/main.rs b/src/main.rs index a67ad45..642cb20 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,97 +1,121 @@ -use rodio::source::{Repeat, Source}; -use rodio::{Decoder, OutputStream}; -use std::fs::File; -use std::time::Duration; +use crossterm::{ + cursor::MoveTo, + event::{self, Event, KeyCode}, + execute, + terminal::{self, Clear, ClearType}, +}; +use rodio::{source::Source, OutputStream, OutputStreamHandle}; +use std::{ + io::stdout, + sync::{Arc, Mutex}, + time::Duration, +}; -type Volume = f32; +mod sound; +use sound::Snoud; fn main() { - let files: Vec = vec![ - "sound/rain.mp3".into(), - "sound/thunder.mp3".into(), - "sound/wind.mp3".into(), - ]; - let source = Snoud::new(&files); - - let (_stream, stream_handle) = OutputStream::try_default().unwrap(); - - stream_handle.play_raw(source.convert_samples()).unwrap(); - - // let sink = Sink::try_new(&stream_handle).unwrap(); - // sink.append(source); - // sink.play(); - loop { - std::thread::sleep(Duration::from_millis(200)); - } + App::new().run(); } -struct Snoud { - channels: Vec, - sample_rate: u32, +struct UIChannel { + name: String, + volume: f32, + internal_volume: Arc>, } -struct SoundChannel { - source: Repeat>, - paused: bool, - volume: Volume, +struct App { + channels: Vec, + selected_channel: usize, + _stream: OutputStream, + stream_handle: OutputStreamHandle, + quit: bool, } -impl SoundChannel { - fn new(name: &String) -> Self { - let file = File::open(name).expect("File not found"); - let source = Decoder::new(file) - .expect("Could not decode file") - .repeat_infinite(); +impl App { + fn new() -> Self { + let (_stream, stream_handle) = OutputStream::try_default() // + .expect("Failed to create output stream"); Self { - source, - paused: false, - volume: 1.0, + channels: Vec::new(), + selected_channel: 0, + _stream, + stream_handle, + quit: false, } } -} -impl Iterator for Snoud { - type Item = i16; + fn run(&mut self) { + // TODO scan directory instead + let files: Vec = vec![ + "sound/rain.mp3".into(), + "sound/thunder.mp3".into(), + "sound/wind.mp3".into(), + ]; - fn next(&mut self) -> Option { - let mut out: Self::Item = 0; - for c in &mut self.channels { - if c.paused { - continue; + let mut snoud = Snoud::new(); + for filename in files { + let internal_volume = snoud.add_channel(&filename); + let ui_channel = UIChannel { + name: filename, + volume: 1.0, + internal_volume, + }; + self.channels.push(ui_channel); + } + + self.stream_handle + .play_raw(snoud.convert_samples()) + .unwrap(); + + terminal::enable_raw_mode().unwrap(); + while !self.quit { + self.render(); + self.input(); + } + terminal::disable_raw_mode().unwrap(); + } + + fn render(&mut self) { + execute!(stdout(), Clear(ClearType::All), MoveTo(0, 0)).unwrap(); + + terminal::disable_raw_mode().unwrap(); + println!("Snoud!!"); + println!("--------"); + for (i, channel) in self.channels.iter().enumerate() { + println!( + "{}{}:\n {:.0}", + if i == self.selected_channel { ">" } else { " " }, + &channel.name, + (channel.volume * 100.0) + ); + } + terminal::enable_raw_mode().unwrap(); + } + + fn input(&mut self) { + if !event::poll(Duration::from_millis(50)).unwrap() { + return; + } + + let event = if let Ok(Event::Key(keyevent)) = event::read() { + keyevent + } else { + return; + }; + + match event.code { + KeyCode::Char('q') => self.quit = true, + KeyCode::Up => { + self.selected_channel = match self.selected_channel { + 0 => self.channels.len() - 1, + n => n - 1, + }; } - let mut sample: Self::Item = c.source.next().unwrap(); - sample = (f32::from(sample) * c.volume) as Self::Item; - out = out.saturating_add(sample); - } - Some(out) - } -} - -impl Source for Snoud { - fn channels(&self) -> u16 { - 2 - } - - fn sample_rate(&self) -> u32 { - self.sample_rate - } - - fn current_frame_len(&self) -> Option { - None - } - - fn total_duration(&self) -> Option { - None - } -} - -impl Snoud { - fn new(filenames: &[String]) -> Self { - let channels = filenames.iter().map(SoundChannel::new).collect(); - - Self { - sample_rate: 48000, - channels, + KeyCode::Down => { + self.selected_channel = (self.selected_channel + 1) % self.channels.len(); + } + _ => (), } } } diff --git a/src/sound.rs b/src/sound.rs new file mode 100644 index 0000000..732a987 --- /dev/null +++ b/src/sound.rs @@ -0,0 +1,81 @@ +use rodio::source::{Repeat, Source}; +use rodio::Decoder; +use std::fs::File; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +pub struct Snoud { + channels: Vec, + sample_rate: u32, +} + +struct SoundChannel { + source: Repeat>, + paused: bool, + volume: f32, + volume_sync: Arc>, +} + +impl SoundChannel { + fn new(name: &str) -> Self { + let file = File::open(name).expect("File not found"); + let source = Decoder::new(file) + .expect("Could not decode file") + .repeat_infinite(); + Self { + source, + paused: false, + volume: 1.0, + volume_sync: Arc::new(Mutex::new(1.0)), + } + } +} + +impl Iterator for Snoud { + type Item = i16; + + fn next(&mut self) -> Option { + let mut out: Self::Item = 0; + for c in &mut self.channels { + if c.paused { + continue; + } + let mut sample: Self::Item = c.source.next().unwrap(); + sample = (f32::from(sample) * c.volume) as Self::Item; + out = out.saturating_add(sample); + } + Some(out) + } +} + +impl Source for Snoud { + fn channels(&self) -> u16 { + 2 + } + fn sample_rate(&self) -> u32 { + self.sample_rate + } + fn current_frame_len(&self) -> Option { + None + } + fn total_duration(&self) -> Option { + None + } +} + +impl Snoud { + pub fn new(/* filenames: &[String] */) -> Self { + // let channels = filenames.iter().map(SoundChannel::new).collect(); + Self { + sample_rate: 48000, + channels: Vec::new(), + } + } + + pub fn add_channel(&mut self, filename: &str) -> Arc> { + let new = SoundChannel::new(filename); + let volume_sync = new.volume_sync.clone(); + self.channels.push(new); + volume_sync + } +}