| Lift freeq's AV media plane out of sleek 90f8b89 nandi 19d ago | 1 | use std::{cell::Cell, rc::Rc}; |
| 2 | |
| 3 | use cpal::{ |
| 4 | traits::{DeviceTrait, HostTrait, StreamTrait}, |
| 5 | Stream, |
| 6 | }; |
| 7 | use wasm_bindgen::prelude::*; |
| 8 | use web_sys::console; |
| 9 | |
| 10 | // This is like the `main` function, except for JavaScript. |
| 11 | #[wasm_bindgen(start)] |
| 12 | pub fn main_js() -> Result<(), JsValue> { |
| 13 | // This provides better error messages in debug mode. |
| 14 | // It's disabled in release mode, so it doesn't bloat up the file size. |
| 15 | #[cfg(debug_assertions)] |
| 16 | console_error_panic_hook::set_once(); |
| 17 | |
| 18 | let document = gloo::utils::document(); |
| 19 | let play_button = document.get_element_by_id("play").unwrap(); |
| 20 | let stop_button = document.get_element_by_id("stop").unwrap(); |
| 21 | |
| 22 | // stream needs to be referenced from the "play" and "stop" closures |
| 23 | let stream = Rc::new(Cell::new(None)); |
| 24 | |
| 25 | // set up play button |
| 26 | { |
| 27 | let stream = stream.clone(); |
| 28 | let closure = Closure::<dyn FnMut(_)>::new(move |_event: web_sys::MouseEvent| { |
| 29 | stream.set(Some(beep())); |
| 30 | }); |
| 31 | play_button |
| 32 | .add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref())?; |
| 33 | closure.forget(); |
| 34 | } |
| 35 | |
| 36 | // set up stop button |
| 37 | { |
| 38 | let closure = Closure::<dyn FnMut(_)>::new(move |_event: web_sys::MouseEvent| { |
| 39 | // stop the stream by dropping it |
| 40 | stream.take(); |
| 41 | }); |
| 42 | stop_button |
| 43 | .add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref())?; |
| 44 | closure.forget(); |
| 45 | } |
| 46 | |
| 47 | Ok(()) |
| 48 | } |
| 49 | |
| 50 | fn beep() -> Stream { |
| 51 | let host = |
| 52 | cpal::host_from_id(cpal::HostId::AudioWorklet).expect("AudioWorklet host not available"); |
| 53 | |
| 54 | let device = host |
| 55 | .default_output_device() |
| 56 | .expect("failed to find a default output device"); |
| 57 | let config = device.default_output_config().unwrap(); |
| 58 | |
| 59 | match config.sample_format() { |
| 60 | cpal::SampleFormat::F32 => run::<f32>(&device, config.into()), |
| 61 | cpal::SampleFormat::I16 => run::<i16>(&device, config.into()), |
| 62 | cpal::SampleFormat::U16 => run::<u16>(&device, config.into()), |
| 63 | _ => panic!("unsupported sample format"), |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | fn run<T>(device: &cpal::Device, config: cpal::StreamConfig) -> Stream |
| 68 | where |
| 69 | T: cpal::Sample + cpal::SizedSample + cpal::FromSample<f32>, |
| 70 | { |
| 71 | let sample_rate = config.sample_rate as f32; |
| 72 | let channels = config.channels as usize; |
| 73 | |
| 74 | // Produce a sinusoid of maximum amplitude. |
| 75 | let mut sample_clock = 0f32; |
| 76 | let mut next_value = move || { |
| 77 | sample_clock = (sample_clock + 1.0) % sample_rate; |
| 78 | (sample_clock * 440.0 * 2.0 * std::f32::consts::PI / sample_rate).sin() |
| 79 | }; |
| 80 | |
| 81 | let err_fn = |err| console::error_1(&format!("an error occurred on stream: {}", err).into()); |
| 82 | |
| 83 | let stream = device |
| 84 | .build_output_stream( |
| 85 | config, |
| 86 | move |data: &mut [T], _| write_data(data, channels, &mut next_value), |
| 87 | err_fn, |
| 88 | None, |
| 89 | ) |
| 90 | .unwrap(); |
| 91 | stream.play().unwrap(); |
| 92 | stream |
| 93 | } |
| 94 | |
| 95 | fn write_data<T>(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> f32) |
| 96 | where |
| 97 | T: cpal::Sample + cpal::FromSample<f32>, |
| 98 | { |
| 99 | for frame in output.chunks_mut(channels) { |
| 100 | let sample = next_sample(); |
| 101 | let value = T::from_sample::<f32>(sample); |
| 102 | for sample in frame.iter_mut() { |
| 103 | *sample = value; |
| 104 | } |
| 105 | } |
| 106 | } |