269 lines
8.6 KiB
Rust
269 lines
8.6 KiB
Rust
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<String> {
|
|
let devices = host.output_devices().unwrap();
|
|
devices.map(|device| device.name().unwrap()).collect()
|
|
}
|
|
|
|
fn find_device_index_regex(devices: Vec<String>, pattern: &str) -> Option<usize> {
|
|
let re = Regex::new(pattern).unwrap();
|
|
devices.iter().position(|device| re.is_match(device))
|
|
}
|
|
|
|
fn select_device(host: &Host, i: usize) -> Option<Device> {
|
|
host.output_devices().unwrap().nth(i)
|
|
}
|
|
|
|
fn select_device_by_pattern(pattern: &str) -> Option<Device> {
|
|
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<Device> {
|
|
let host = cpal::default_host();
|
|
return select_device(&host, index);
|
|
}
|
|
|
|
pub type AudioThread = Sender<Command>;
|
|
|
|
pub trait PlaybackAction {
|
|
fn before_playback(&mut self);
|
|
fn after_playback(&mut self);
|
|
}
|
|
|
|
pub struct PlaybackActionEvents {
|
|
actions: Vec<Box<dyn PlaybackAction + Send>>,
|
|
}
|
|
|
|
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<Mutex<PlaybackActionEvents>>,
|
|
}
|
|
|
|
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<dyn PlaybackAction + Send>) {
|
|
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<Mutex<PlaybackActionEvents>>,
|
|
) -> 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"),
|
|
}
|
|
}
|