use cpal::traits::{DeviceTrait, HostTrait}; use cpal::{Device, Host}; use log::{debug, error, info}; use minimp3::{Decoder, Error}; use regex::Regex; use std::fs::File; use std::io::Read; use std::io::{BufReader, Cursor}; use std::sync::mpsc::{self, Sender}; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, Mutex, }; use std::thread; #[derive(Clone)] pub enum SoundDecoder { Detect, Mp3Mini, Rodio, } pub enum Command { Play(String), PlayWhilePressing(String, String), Stop, } // todo: implement a device selection with this: pub enum DeviceSelection { SelectFirst, SelectById(usize), FindByPattern(String), } fn list_devices(host: &Host) -> Vec { let devices = host.output_devices().unwrap(); devices.map(|device| device.name().unwrap()).collect() } fn find_device_index_regex(devices: Vec, pattern: &str) -> Option { let re = Regex::new(pattern).unwrap(); devices.iter().position(|device| re.is_match(device)) } fn select_device(host: &Host, i: usize) -> Option { host.output_devices().unwrap().nth(i) } fn select_device_by_pattern(pattern: &str) -> Option { let host = cpal::default_host(); let devices = list_devices(&host); info!("Devices: {:?}", devices); if let Some(index) = find_device_index_regex(devices, pattern) { info!( "Selecting Device from pattern: \"{}\" at index {:?}", pattern, index ); return select_device(&host, index); } return None; } fn select_device_by_id(index: usize) -> Option { let host = cpal::default_host(); return select_device(&host, index); } pub type AudioThread = Sender; pub trait PlaybackAction { fn before_playback(&mut self); fn after_playback(&mut self); } pub struct PlaybackActionEvents { actions: Vec>, } impl PlaybackActionEvents { pub fn new() -> Self { PlaybackActionEvents { actions: Vec::new(), } } pub fn dispatch_before_playback(&mut self) { for action in self.actions.iter_mut() { action.before_playback(); } } pub fn dispatch_after_playback(&mut self) { for action in self.actions.iter_mut() { action.after_playback(); } } } pub struct AudioHandler { pub listener: AudioThread, events: Arc>, } impl AudioHandler { pub fn new(select_device: DeviceSelection, select_decoder: SoundDecoder) -> Self { let events = Arc::new(Mutex::new(PlaybackActionEvents::new())); let tx = audio_thread(select_device, select_decoder, events.clone()); Self { listener: tx, events, } } pub fn on_playback(&mut self, event: Box) { self.events.lock().unwrap().actions.push(event); } } pub mod example_handler { use enigo::*; pub struct PressMouseForward { enigo: Enigo, } impl PressMouseForward { pub fn new() -> Self { let enigo = Enigo::new(); PressMouseForward { enigo: enigo } } } impl super::PlaybackAction for PressMouseForward { fn before_playback(&mut self) { self.enigo.mouse_down(MouseButton::Forward); } fn after_playback(&mut self) { self.enigo.mouse_up(MouseButton::Forward); } } } pub fn audio_thread( select_device: DeviceSelection, select_decoder: SoundDecoder, events: Arc>, ) -> AudioThread { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let device = match select_device { DeviceSelection::SelectFirst => { select_device_by_id(0).expect("No Audio devices found.") } DeviceSelection::SelectById(id) => { select_device_by_id(id).expect("Audio device for ID not found.") } DeviceSelection::FindByPattern(pattern) => select_device_by_pattern(pattern.as_ref()) .expect("Pattern search for audio device yielded no results."), }; // Create a rodio Sink connected to our device. We need to keep stream and stream_handle alive during the thread. let (_stream, stream_handle) = rodio::OutputStream::try_from_device(&device).unwrap(); let new_sink = rodio::Sink::try_new(&stream_handle).unwrap(); let sink_mutex = Arc::new(Mutex::new(new_sink)); let is_button_thread_active = Arc::new(AtomicBool::new(false)); for command in rx { match command { Command::Play(file_name) => { if let Ok(sink) = sink_mutex.lock() { play_file(&sink, file_name, &select_decoder); } else { error!("Mutex Lock Failure while trying to play sound.") } } Command::PlayWhilePressing(file_name, press_button) => { if let Ok(sink) = sink_mutex.lock() { play_file(&sink, file_name, &select_decoder); } else { error!("Mutex Lock Failure while trying to play sound with callbacks."); } if !is_button_thread_active.load(Ordering::SeqCst) { let sink_mutex_clone = sink_mutex.clone(); let is_active_clone = is_button_thread_active.clone(); let events_clone = events.clone(); thread::spawn(move || { events_clone.lock().unwrap().dispatch_before_playback(); is_active_clone.store(true, Ordering::SeqCst); while sink_mutex_clone.lock().map_or(false, |sink| !sink.empty()) { thread::sleep(std::time::Duration::from_millis(100)); } events_clone.lock().unwrap().dispatch_after_playback(); is_active_clone.store(false, Ordering::SeqCst); }); } } Command::Stop => { if let Ok(sink) = sink_mutex.lock() { sink.stop(); } else { error!("Mutex Lock Failure while trying to stop sound."); } } } } }); tx } fn detect_decoder(file_name: &str, sound_decoder: &SoundDecoder) -> SoundDecoder { // TODO: File detection via ending or whitelisting? // This function MUST NOT return SoundDecoder::Detect match sound_decoder { SoundDecoder::Detect => SoundDecoder::Mp3Mini, other => other.clone(), } } fn play_file(sink: &rodio::Sink, file_name: String, sound_decoder: &SoundDecoder) { match detect_decoder(&file_name, sound_decoder) { SoundDecoder::Mp3Mini => { // MP3 Mini provides low latency playback let mut sound_file = File::open(file_name).unwrap(); let mut file_data = Vec::new(); sound_file.read_to_end(&mut file_data).unwrap(); // Iterate over the MP3 frames and play them let mut decoder = Decoder::new(Cursor::new(file_data)); loop { match decoder.next_frame() { Ok(frame) => { let source = rodio::buffer::SamplesBuffer::new( 2, frame.sample_rate.try_into().unwrap(), &*frame.data, ); sink.append(source); } Err(Error::Eof) => { debug!("EOF"); break; } Err(e) => { error!("{:?}", e); break; } } } } SoundDecoder::Rodio => { // Rodio currently supports Ogg, Mp3, WAV and Flac let file = File::open(&file_name).unwrap(); let source = rodio::Decoder::new(BufReader::new(file)).expect("Failed to open Decoder"); sink.append(source); //sink.sleep_until_end(); } SoundDecoder::Detect => error!("This should never happen"), } }