audioview/frontend-core/src/components/player.rs

157 lines
4.8 KiB
Rust

use crate::agents::audio_agent::{AudioAgent, AudioData};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{Device, Sample, SampleFormat, Stream, StreamConfig};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use yew::prelude::*;
use yew::services::ConsoleService;
pub enum Status {
Stopped,
Playing,
}
pub struct Player {
link: ComponentLink<Self>,
status: Status,
stream: Option<Stream>,
_audio_agent: Box<dyn Bridge<AudioAgent>>,
audio_data: Option<Arc<AudioData>>,
}
pub enum Msg {
Play,
AudioAgentMessage(Result<Arc<AudioData>, String>),
}
impl Component for Player {
type Message = Msg;
type Properties = ();
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
let cb = link.callback(Msg::AudioAgentMessage);
Self {
link,
status: Status::Stopped,
stream: None,
_audio_agent: AudioAgent::bridge(cb),
audio_data: None,
}
}
fn rendered(&mut self, _first_render: bool) {}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::Play => self.handle_play_button_clicked(),
Msg::AudioAgentMessage(Ok(audio_data)) => self.handle_samples_loaded(audio_data),
Msg::AudioAgentMessage(Err(err)) => self.handle_samples_loaded_error(&err),
}
}
fn change(&mut self, _: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
html! {
<div>
<button onclick=self.link.callback(move |_| Msg::Play)>{self.button_text()}</button>
</div>
}
}
}
impl Player {
fn handle_samples_loaded(&mut self, audio_data: Arc<AudioData>) -> ShouldRender {
ConsoleService::log("Player: samples loaded");
self.audio_data = Some(audio_data);
true
}
fn handle_samples_loaded_error(&mut self, err: &str) -> ShouldRender {
ConsoleService::log(&format!("Player: error loading samples: {:?}", err));
self.audio_data = None;
false
}
fn play(&mut self) {
if let Some(audio_data) = &self.audio_data {
let host = cpal::default_host();
let device = host.default_output_device().unwrap();
let config = device.default_output_config().unwrap();
ConsoleService::log(&format!("Using output config: {:?}", config));
let stream = match config.sample_format() {
SampleFormat::F32 => {
Player::run::<f32>(&device, &config.into(), audio_data.clone())
}
SampleFormat::I16 => {
Player::run::<i16>(&device, &config.into(), audio_data.clone())
}
SampleFormat::U16 => {
Player::run::<u16>(&device, &config.into(), audio_data.clone())
}
};
self.stream = Some(stream);
}
}
fn run<T>(device: &Device, config: &StreamConfig, audio_data: Arc<AudioData>) -> Stream
where
T: cpal::Sample,
{
let err_fn = |err| ConsoleService::warn(&format!("an error occurred on stream: {}", err));
let num_channels = audio_data.num_channels as usize;
// TODO: consider passing the link into the audio callback? Need to double check
// performance implications.
// https://discord.com/channels/701068342760570933/703449306497024049/753646647938121738
let idx = Arc::new(AtomicUsize::new(0));
let stream = device
.build_output_stream(
config,
move |output: &mut [T], _| {
let frames = output.chunks_mut(num_channels);
let mut idx = idx.fetch_add(frames.len(), Ordering::Relaxed);
for frame in output.chunks_mut(num_channels) {
for (j, sample) in frame.iter_mut().enumerate() {
let buffer = &audio_data.buffers[j];
let value: T = Sample::from::<f32>(&buffer[idx]);
*sample = value;
}
idx += 1;
}
},
err_fn,
)
.unwrap();
stream.play().unwrap();
stream
}
fn handle_play_button_clicked(&mut self) -> ShouldRender {
match self.status {
Status::Stopped => {
self.status = Status::Playing;
self.play();
}
Status::Playing => self.status = Status::Stopped,
}
true
}
fn button_text(&self) -> &str {
if let Status::Stopped = self.status {
"Play"
} else {
"Pause"
}
}
}