use std::{path::Path, sync::Arc, thread::JoinHandle, time::Duration};
use cpal::{
Stream, StreamConfig,
traits::{DeviceTrait, HostTrait, StreamTrait},
};
use crossbeam::channel;
use crate::{
BUFFER_MS, CHANNEL_COUNT, SAMPLE_TAP_CAPACITY, SEEK_FADE_MS,
engine::command::SeekPosition,
error::{Result, VoxError},
};
mod command;
mod decoder;
mod resampler;
mod state;
mod tap;
pub(crate) use command::VoxCommand;
pub(crate) use decoder::VoxDecoder;
pub(crate) use resampler::VoxResampler;
pub(crate) use state::SharedState;
pub(crate) use tap::SampleTap;
pub struct Vox {
state: Arc<SharedState>,
commands: channel::Sender<VoxCommand>,
sps: f64,
tap: SampleTap,
_stream: Stream,
_decoder_thread: JoinHandle<()>,
}
impl Vox {
pub fn new() -> Result<Self> {
let host = cpal::default_host();
let device = host
.default_output_device()
.ok_or_else(|| VoxError::Output("No output device recognized!".into()))?;
let config = device
.default_output_config()
.map_err(|e| VoxError::Output(e.to_string()))?;
let output_rate = config.sample_rate() as f32;
let output_channels = config.channels() as usize;
let stream_config: StreamConfig = config.into();
let buffer_size = (output_rate as usize * output_channels * BUFFER_MS) / 1000;
let (producer, mut consumer) = rtrb::RingBuffer::new(buffer_size);
let tap = SampleTap::new(SAMPLE_TAP_CAPACITY);
let tap_input = tap.clone();
let state = Arc::new(SharedState::default());
let state_clone = Arc::clone(&state);
let mut was_seeking = false;
let fade_total_samples = (output_rate as usize * output_channels * SEEK_FADE_MS) / 1000;
let mut fade_samples_remaining: usize = 0;
let stream = device
.build_output_stream(
&stream_config,
move |data: &mut [f32], _| {
let is_seeking = state_clone.is_seeking();
let is_inactive =
state_clone.is_paused() || !state_clone.is_active() || is_seeking;
if is_inactive {
while consumer.pop().is_ok() {}
data.fill(0.0);
was_seeking = is_seeking;
return;
}
if was_seeking && !is_seeking {
fade_samples_remaining = fade_total_samples;
}
was_seeking = is_seeking;
let mut samples_consumed = 0u64;
for sample in data.iter_mut() {
match consumer.pop() {
Ok(mut s) => {
if fade_samples_remaining > 0 {
let progress = 1.0
- (fade_samples_remaining as f32
/ fade_total_samples as f32);
s *= progress;
fade_samples_remaining -= 1;
}
*sample = s;
samples_consumed += 1;
}
Err(_) => *sample = 0.0,
}
}
if samples_consumed > 0 {
state_clone.add_samples(samples_consumed);
}
tap_input.push(data);
},
|err| eprintln!("Stream error: {}", err),
None,
)
.expect("Failed to create stream");
let (tx, rx) = channel::bounded(CHANNEL_COUNT);
let decoder_thread = command::spawn(
rx,
producer,
Arc::clone(&state),
output_rate as u32,
output_channels,
);
stream.play().map_err(|e| VoxError::Output(e.to_string()))?;
Ok(Self {
state: state,
commands: tx,
sps: output_rate as f64 * output_channels as f64,
_stream: stream,
_decoder_thread: decoder_thread,
tap,
})
}
pub fn play<P: AsRef<Path>>(&mut self, p: P) -> Result<()> {
let path = p.as_ref();
if !path.exists() {
let s = path.to_string_lossy().to_string();
return Err(VoxError::FileOpen(s));
}
self.state.start_seek();
self.state.reset_samples();
self.state.set_active(true);
self.commands
.send(VoxCommand::Play(path.to_string_lossy().to_string()))
.map_err(|_| VoxError::Output("Channel closed".into()))
}
pub fn set_next<P: AsRef<Path>>(&mut self, p: P) -> Result<()> {
let path = p.as_ref();
if !path.exists() {
return Err(VoxError::FileOpen(path.to_string_lossy().to_string()));
}
let _ = self
.commands
.try_send(VoxCommand::QueueNext(path.to_string_lossy().to_string()));
Ok(())
}
pub fn seek_to(&mut self, pos: f64) -> Result<()> {
if !self.state.is_active() {
return Ok(());
}
self.state.start_seek();
self.commands
.send(VoxCommand::Seek(SeekPosition::Absolute(pos)))
.map_err(|e| VoxError::Seek(e.to_string()))?;
Ok(())
}
pub fn seek_relative(&mut self, increment: f64) -> Result<()> {
if !self.state.is_active() {
return Ok(());
}
self.state.start_seek();
self.commands
.send(VoxCommand::Seek(SeekPosition::Relative(increment)))
.map_err(|e| VoxError::Seek(e.to_string()))?;
Ok(())
}
pub fn toggle_playback(&self) {
self.state.toggle_playback();
}
pub fn is_paused(&self) -> bool {
self.state.is_paused()
}
pub fn is_active(&self) -> bool {
self.state.is_active()
}
pub fn pause(&self) {
self.state.set_paused(true)
}
pub fn resume(&self) {
self.state.set_paused(false);
}
pub fn stop(&self) -> Result<()> {
self.commands
.send(VoxCommand::Stop)
.map_err(|_| VoxError::Output("Channel closed".into()))
}
pub fn position(&self) -> Duration {
Duration::from_secs_f64(self.state.get_samples() as f64 / self.sps)
}
pub fn duration(&self) -> Duration {
Duration::from_secs_f64(self.state.get_duration_secs())
}
pub fn get_latest_samples(&self, amount: usize) -> Vec<f32> {
self.tap.get_latest(amount)
}
pub fn track_ended(&self) -> bool {
self.state.take_track_ended()
}
}
impl Drop for Vox {
fn drop(&mut self) {
let _ = self.commands.send(VoxCommand::Shutdown);
}
}