use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use cpal::{
SampleFormat, SupportedBufferSize,
traits::{DeviceTrait, HostTrait, StreamTrait},
};
use serde::de::DeserializeOwned;
use symphonia::default::{get_probe, register_enabled_codecs};
use symphonia_adapter_libopus::OpusDecoder;
use symphonia_core::{
audio::SampleBuffer,
codecs::{CodecRegistry, DecoderOptions},
errors::Error as SymphoniaError,
formats::FormatOptions,
io::{MediaSourceStream, ReadOnlySource},
meta::MetadataOptions,
probe::Hint,
};
use tauri::{AppHandle, Runtime, plugin::PluginApi};
use crate::{
Error, Result,
models::{
AudioDevice, AudioDeviceList, BufferSizeRange, LoadedAudio, PlaybackProgress, StreamConfig,
},
playback::{PlaybackState, render_samples, seek_position_samples},
};
pub fn init<R: Runtime, C: DeserializeOwned>(
app: &AppHandle<R>,
_api: PluginApi<R, C>,
) -> Result<Audioplayer<R>> {
Ok(Audioplayer {
_app: app.clone(),
state: Arc::new(Mutex::new(PlayerState::default())),
})
}
struct PlayerState {
selected_input_id: Option<String>,
selected_output_id: Option<String>,
loaded_audio: Option<DecodedAudio>,
playback: Option<Arc<Mutex<PlaybackState>>>,
output_stream: Option<cpal::Stream>,
volume: f32,
}
impl Default for PlayerState {
fn default() -> Self {
Self {
selected_input_id: None,
selected_output_id: None,
loaded_audio: None,
playback: None,
output_stream: None,
volume: 1.0,
}
}
}
#[derive(Clone)]
struct DecodedAudio {
samples: Arc<Vec<f32>>,
channels: usize,
sample_rate: u32,
}
pub struct Audioplayer<R: Runtime> {
_app: AppHandle<R>,
state: Arc<Mutex<PlayerState>>,
}
impl<R: Runtime> Audioplayer<R> {
pub fn list_input_devices(&self) -> Result<AudioDeviceList> {
let host = cpal::default_host();
let default_id = default_device_id(host.default_input_device());
let devices = host
.input_devices()
.map_err(Error::message)?
.map(input_device_details)
.collect();
Ok(AudioDeviceList {
devices,
default_id,
})
}
pub fn list_output_devices(&self) -> Result<AudioDeviceList> {
let host = cpal::default_host();
let default_id = default_device_id(host.default_output_device());
let devices = host
.output_devices()
.map_err(Error::message)?
.map(output_device_details)
.collect();
Ok(AudioDeviceList {
devices,
default_id,
})
}
pub fn select_input_device(&self, id: &str) -> Result<AudioDevice> {
let host = cpal::default_host();
let device = find_device(&host, id, DeviceDirection::Input)?;
let details = input_device_details(device);
self.state.lock().map_err(Error::message)?.selected_input_id = Some(id.to_owned());
Ok(details)
}
pub fn select_output_device(&self, id: &str) -> Result<AudioDevice> {
let host = cpal::default_host();
let device = find_device(&host, id, DeviceDirection::Output)?;
let details = output_device_details(device);
let mut state = self.state.lock().map_err(Error::message)?;
state.output_stream = None;
state.playback = None;
state.selected_output_id = Some(id.to_owned());
Ok(details)
}
pub fn load_audio(
&self,
url: &str,
headers: Option<&HashMap<String, String>>,
) -> Result<LoadedAudio> {
let output_id = {
let mut state = self.state.lock().map_err(Error::message)?;
state.output_stream = None;
state.playback = None;
state.selected_output_id.clone().ok_or_else(|| {
Error::Message("Select an output device before loading audio".into())
})?
};
let host = cpal::default_host();
find_device(&host, &output_id, DeviceDirection::Output)?;
let audio = download_and_decode_audio(url, headers)?;
let duration_seconds =
audio.samples.len() as f64 / (audio.channels as f64 * audio.sample_rate as f64);
let details = LoadedAudio {
channels: audio.channels,
sample_rate: audio.sample_rate,
duration_seconds,
};
self.state.lock().map_err(Error::message)?.loaded_audio = Some(audio);
Ok(details)
}
pub fn play_audio(&self) -> Result<()> {
let mut state = self.state.lock().map_err(Error::message)?;
if let (Some(stream), Some(playback)) = (&state.output_stream, &state.playback) {
let mut playback = playback.lock().map_err(Error::message)?;
if playback.position_samples >= playback.samples.len() {
playback.position_samples = 0;
}
playback.playing = true;
stream.play().map_err(Error::message)?;
return Ok(());
}
let output_id = state
.selected_output_id
.clone()
.ok_or_else(|| Error::Message("Select an output device before playing audio".into()))?;
let audio = state
.loaded_audio
.clone()
.ok_or_else(|| Error::Message("Load audio before playing it".into()))?;
let host = cpal::default_host();
let device = find_device(&host, &output_id, DeviceDirection::Output)?;
let config = output_stream_config(&device)?;
if config.sample_rate() != audio.sample_rate {
return Err(Error::Message(format!(
"The selected output uses {} Hz but the loaded audio uses {} Hz. Resampling is not implemented yet.",
config.sample_rate(),
audio.sample_rate,
)));
}
let playback = Arc::new(Mutex::new(PlaybackState::new(
audio.samples,
audio.channels,
)));
playback.lock().map_err(Error::message)?.volume = state.volume;
let stream = create_output_stream(&device, config, Arc::clone(&playback))?;
stream.play().map_err(Error::message)?;
state.playback = Some(playback);
state.output_stream = Some(stream);
Ok(())
}
pub fn pause_audio(&self) -> Result<()> {
let state = self.state.lock().map_err(Error::message)?;
let stream = state
.output_stream
.as_ref()
.ok_or_else(|| Error::Message("No audio stream is playing".into()))?;
let playback = state
.playback
.as_ref()
.ok_or_else(|| Error::Message("No loaded playback state is available".into()))?;
playback.lock().map_err(Error::message)?.playing = false;
stream.pause().map_err(Error::message)
}
pub fn stop_audio(&self) -> Result<()> {
let state = self.state.lock().map_err(Error::message)?;
let Some(playback) = state.playback.as_ref() else {
return state
.loaded_audio
.as_ref()
.map(|_| ())
.ok_or_else(|| Error::Message("Load audio before stopping it".into()));
};
let mut playback = playback.lock().map_err(Error::message)?;
playback.playing = false;
playback.position_samples = 0;
if let Some(stream) = state.output_stream.as_ref() {
stream.pause().map_err(Error::message)?;
}
Ok(())
}
pub fn set_volume(&self, volume: f32) -> Result<()> {
if !volume.is_finite() || !(0.0..=1.0).contains(&volume) {
return Err(Error::Message(
"Volume must be a finite value between 0.0 and 1.0".into(),
));
}
let mut state = self.state.lock().map_err(Error::message)?;
state.volume = volume;
if let Some(playback) = &state.playback {
playback.lock().map_err(Error::message)?.volume = volume;
}
Ok(())
}
pub fn mute(&self) -> Result<()> {
self.set_volume(0.0)
}
pub fn playback_progress(&self) -> Result<PlaybackProgress> {
let state = self.state.lock().map_err(Error::message)?;
playback_progress_for_state(&state)
}
pub fn seek_audio(&self, position_seconds: f64) -> Result<PlaybackProgress> {
if !position_seconds.is_finite() || position_seconds < 0.0 {
return Err(Error::Message(
"The requested playback position must be a positive number".into(),
));
}
let state = self.state.lock().map_err(Error::message)?;
let audio = state
.loaded_audio
.as_ref()
.ok_or_else(|| Error::Message("Load audio before seeking it".into()))?;
let playback = state
.playback
.as_ref()
.ok_or_else(|| Error::Message("Start playback before seeking audio".into()))?;
let mut playback = playback.lock().map_err(Error::message)?;
playback.position_samples = seek_position_samples(
position_seconds,
audio.sample_rate,
audio.channels,
audio.samples.len(),
)
.ok_or_else(|| {
Error::Message("The loaded audio has an invalid stream configuration".into())
})?;
let duration_seconds =
audio.samples.len() as f64 / (audio.channels as f64 * audio.sample_rate as f64);
Ok(PlaybackProgress {
position_seconds: (playback.position_samples as f64
/ (audio.channels as f64 * audio.sample_rate as f64))
.min(duration_seconds),
duration_seconds,
is_playing: playback.playing,
})
}
}
fn playback_progress_for_state(state: &PlayerState) -> Result<PlaybackProgress> {
let audio = state
.loaded_audio
.as_ref()
.ok_or_else(|| Error::Message("Load audio before requesting its progress".into()))?;
let duration_seconds =
audio.samples.len() as f64 / (audio.channels as f64 * audio.sample_rate as f64);
let (position_seconds, is_playing) = match state.playback.as_ref() {
Some(playback) => {
let playback = playback.lock().map_err(Error::message)?;
(
(playback.position_samples as f64
/ (playback.source_channels as f64 * audio.sample_rate as f64))
.min(duration_seconds),
playback.playing,
)
}
None => (0.0, false),
};
Ok(PlaybackProgress {
position_seconds,
duration_seconds,
is_playing,
})
}
enum DeviceDirection {
Input,
Output,
}
fn find_device(host: &cpal::Host, id: &str, direction: DeviceDirection) -> Result<cpal::Device> {
let device_id = id.parse::<cpal::DeviceId>().map_err(Error::message)?;
let device = host
.device_by_id(&device_id)
.ok_or_else(|| Error::Message(format!("Audio device not found: {id}")))?;
let supports_direction = match direction {
DeviceDirection::Input => device.supports_input(),
DeviceDirection::Output => device.supports_output(),
};
supports_direction.then_some(device).ok_or_else(|| {
Error::Message(format!(
"Audio device does not support the requested direction: {id}"
))
})
}
fn default_device_id(device: Option<cpal::Device>) -> String {
device
.and_then(|device| device.id().ok())
.map(|id| id.to_string())
.unwrap_or_default()
}
fn input_device_details(device: cpal::Device) -> AudioDevice {
let id = device.id().map(|id| id.to_string()).unwrap_or_default();
let name = device.to_string();
match device.supported_input_configs() {
Ok(configs) => AudioDevice {
id,
name,
selected_stream_config: config_range_selector(configs).map(stream_config_details),
error: None,
},
Err(error) => AudioDevice {
id,
name,
selected_stream_config: None,
error: Some(error.to_string()),
},
}
}
fn output_device_details(device: cpal::Device) -> AudioDevice {
let id = device.id().map(|id| id.to_string()).unwrap_or_default();
let name = device.to_string();
match device.supported_output_configs() {
Ok(configs) => AudioDevice {
id,
name,
selected_stream_config: config_range_selector(configs).map(stream_config_details),
error: None,
},
Err(error) => AudioDevice {
id,
name,
selected_stream_config: None,
error: Some(error.to_string()),
},
}
}
fn config_range_selector(
ranges: impl IntoIterator<Item = cpal::SupportedStreamConfigRange>,
) -> Option<cpal::SupportedStreamConfig> {
ranges
.into_iter()
.max_by(|left, right| left.cmp_default_heuristics(right))
.map(|range| {
range
.try_with_standard_sample_rate()
.unwrap_or_else(|| range.with_max_sample_rate())
})
}
fn stream_config_details(config: cpal::SupportedStreamConfig) -> StreamConfig {
let buffer_size = match config.buffer_size() {
SupportedBufferSize::Range { min, max } => Some(BufferSizeRange {
min_frames: *min,
max_frames: *max,
}),
SupportedBufferSize::Unknown => None,
};
StreamConfig {
channels: config.channels(),
sample_format: format!("{:?}", config.sample_format()),
sample_rate: config.sample_rate(),
buffer_size,
}
}
fn output_stream_config(device: &cpal::Device) -> Result<cpal::SupportedStreamConfig> {
device
.supported_output_configs()
.map_err(Error::message)?
.into_iter()
.filter(|range| range.sample_format() == SampleFormat::F32)
.max_by(|left, right| left.cmp_default_heuristics(right))
.map(|range| {
range
.try_with_standard_sample_rate()
.unwrap_or_else(|| range.with_max_sample_rate())
})
.ok_or_else(|| Error::Message("The selected output has no F32 stream configuration".into()))
}
fn create_output_stream(
device: &cpal::Device,
config: cpal::SupportedStreamConfig,
playback: Arc<Mutex<PlaybackState>>,
) -> Result<cpal::Stream> {
let stream_config: cpal::StreamConfig = config.into();
let output_channels = stream_config.channels as usize;
let error_callback = |error| eprintln!("Audio output stream error: {error}");
device
.build_output_stream(
stream_config,
move |data: &mut [f32], _| render_samples(data, output_channels, &playback),
error_callback,
None,
)
.map_err(Error::message)
}
fn download_and_decode_audio(
url: &str,
headers: Option<&HashMap<String, String>>,
) -> Result<DecodedAudio> {
let mut request = ureq::get(url);
if let Some(headers) = headers {
for (name, value) in headers {
request = request.header(name.as_str(), value.as_str());
}
}
let response = request.call().map_err(Error::message)?;
let source = MediaSourceStream::new(
Box::new(ReadOnlySource::new(response.into_body().into_reader())),
Default::default(),
);
let mut hint = Hint::new();
if let Some(extension) = url.split('?').next().and_then(|url| url.rsplit('.').next()) {
hint.with_extension(extension);
}
let mut format = get_probe()
.format(
&hint,
source,
&FormatOptions::default(),
&MetadataOptions::default(),
)
.map_err(Error::message)?
.format;
let track = format
.default_track()
.ok_or_else(|| Error::Message("The audio file has no default audio track".into()))?;
let track_id = track.id;
let codec_params = track.codec_params.clone();
let channels = codec_params
.channels
.as_ref()
.ok_or_else(|| Error::Message("The audio track has no channel count".into()))?
.count();
let sample_rate = codec_params
.sample_rate
.ok_or_else(|| Error::Message("The audio track has no sample rate".into()))?;
let mut codecs = CodecRegistry::new();
register_enabled_codecs(&mut codecs);
codecs.register_all::<OpusDecoder>();
let mut decoder = codecs
.make(&codec_params, &DecoderOptions::default())
.map_err(Error::message)?;
let mut samples = Vec::new();
loop {
let packet = match format.next_packet() {
Ok(packet) => packet,
Err(SymphoniaError::IoError(_)) => break,
Err(error) => return Err(Error::message(error)),
};
if packet.track_id() != track_id {
continue;
}
match decoder.decode(&packet) {
Ok(decoded) => {
let mut decoded_samples =
SampleBuffer::<f32>::new(decoded.capacity() as u64, *decoded.spec());
decoded_samples.copy_interleaved_ref(decoded);
samples.extend_from_slice(decoded_samples.samples());
}
Err(SymphoniaError::DecodeError(_)) => continue,
Err(error) => return Err(Error::message(error)),
}
}
Ok(DecodedAudio {
samples: Arc::new(samples),
channels,
sample_rate,
})
}