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, status: Status, stream: Option, _audio_agent: Box>, audio_data: Option>, } pub enum Msg { Play, AudioAgentMessage(Result, String>), } impl Component for Player { type Message = Msg; type Properties = (); fn create(_: Self::Properties, link: ComponentLink) -> 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! {
} } } impl Player { fn handle_samples_loaded(&mut self, audio_data: Arc) -> 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::(&device, &config.into(), audio_data.clone()) } SampleFormat::I16 => { Player::run::(&device, &config.into(), audio_data.clone()) } SampleFormat::U16 => { Player::run::(&device, &config.into(), audio_data.clone()) } }; self.stream = Some(stream); } } fn run(device: &Device, config: &StreamConfig, audio_data: Arc) -> 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::(&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" } } }