119 lines
3.4 KiB
Rust
119 lines
3.4 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::Cursor;
|
|
use std::io::Read;
|
|
use std::sync::mpsc::{self, Sender};
|
|
use std::thread;
|
|
|
|
pub enum Command {
|
|
Play(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 fn audio_thread(select_device: DeviceSelection) -> AudioThread {
|
|
let (tx, rx) = mpsc::channel();
|
|
|
|
thread::spawn(move || {
|
|
let device = match select_device {
|
|
DeviceSelection::SelectFirst => select_device_by_id(0).unwrap(),
|
|
DeviceSelection::SelectById(id) => select_device_by_id(id).unwrap(),
|
|
DeviceSelection::FindByPattern(pattern) => {
|
|
select_device_by_pattern(pattern.as_ref()).unwrap()
|
|
}
|
|
};
|
|
|
|
// Create a rodio Sink connected to our device
|
|
let (_stream, stream_handle) = rodio::OutputStream::try_from_device(&device).unwrap();
|
|
let sink = rodio::Sink::try_new(&stream_handle).unwrap();
|
|
|
|
for command in rx {
|
|
match command {
|
|
Command::Play(file_name) => {
|
|
play_file(&sink, file_name);
|
|
}
|
|
Command::Stop => {
|
|
sink.stop();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
tx
|
|
}
|
|
|
|
fn play_file(sink: &rodio::Sink, file_name: String) {
|
|
let mut mp3_file = File::open(file_name).unwrap();
|
|
let mut mp3_data = Vec::new();
|
|
mp3_file.read_to_end(&mut mp3_data).unwrap();
|
|
|
|
// Iterate over the MP3 frames and play them
|
|
let mut decoder = Decoder::new(Cursor::new(mp3_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;
|
|
}
|
|
}
|
|
}
|
|
}
|