use cpal::traits::{DeviceTrait, HostTrait}; use cpal::{Device, Host}; use log::{debug, error, info}; 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, Rodio, } pub enum Command { Exit, 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 fn exit(&mut self) { self.events.lock().unwrap().actions.clear(); self.listener .send(Command::Exit) .expect("Error sending Exit Command."); } } pub mod example_handler { use enigo::*; pub struct PressMouseForward { enigo: Enigo, } impl PressMouseForward { pub fn new() -> Self { let enigo_settings = Settings::default(); let enigo = Enigo::new(&enigo_settings).unwrap(); PressMouseForward { enigo: enigo } } } impl super::PlaybackAction for PressMouseForward { fn before_playback(&mut self) { self.enigo .button(Button::Forward, Direction::Press) .unwrap(); } fn after_playback(&mut self) { self.enigo .button(Button::Forward, Direction::Release) .unwrap(); } } } 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::Exit => { eprintln!("Exiting Soundloop."); if let Ok(sink) = sink_mutex.lock() { sink.stop(); } drop(stream_handle); break; } 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."); } } } } eprintln!("Exiting audio_thread"); }); 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::Rodio, // Mp3Mini seems bugged. other => other.clone(), } } fn play_file(sink: &rodio::Sink, file_name: String, sound_decoder: &SoundDecoder) { match detect_decoder(&file_name, sound_decoder) { 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"), } }