diff options
Diffstat (limited to 'examples/stm32f4')
| -rw-r--r-- | examples/stm32f4/Cargo.toml | 1 | ||||
| -rw-r--r-- | examples/stm32f4/src/bin/usb_uac_speaker.rs | 375 |
2 files changed, 376 insertions, 0 deletions
diff --git a/examples/stm32f4/Cargo.toml b/examples/stm32f4/Cargo.toml index 75e315e82..435b0b43c 100644 --- a/examples/stm32f4/Cargo.toml +++ b/examples/stm32f4/Cargo.toml | |||
| @@ -27,6 +27,7 @@ embedded-io-async = { version = "0.6.1" } | |||
| 27 | panic-probe = { version = "0.3", features = ["print-defmt"] } | 27 | panic-probe = { version = "0.3", features = ["print-defmt"] } |
| 28 | futures-util = { version = "0.3.30", default-features = false } | 28 | futures-util = { version = "0.3.30", default-features = false } |
| 29 | heapless = { version = "0.8", default-features = false } | 29 | heapless = { version = "0.8", default-features = false } |
| 30 | critical-section = "1.1" | ||
| 30 | nb = "1.0.0" | 31 | nb = "1.0.0" |
| 31 | embedded-storage = "0.3.1" | 32 | embedded-storage = "0.3.1" |
| 32 | micromath = "2.0.0" | 33 | micromath = "2.0.0" |
diff --git a/examples/stm32f4/src/bin/usb_uac_speaker.rs b/examples/stm32f4/src/bin/usb_uac_speaker.rs new file mode 100644 index 000000000..77c693ace --- /dev/null +++ b/examples/stm32f4/src/bin/usb_uac_speaker.rs | |||
| @@ -0,0 +1,375 @@ | |||
| 1 | #![no_std] | ||
| 2 | #![no_main] | ||
| 3 | |||
| 4 | use core::cell::RefCell; | ||
| 5 | |||
| 6 | use defmt::{panic, *}; | ||
| 7 | use embassy_executor::Spawner; | ||
| 8 | use embassy_stm32::time::Hertz; | ||
| 9 | use embassy_stm32::{bind_interrupts, interrupt, peripherals, timer, usb, Config}; | ||
| 10 | use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex}; | ||
| 11 | use embassy_sync::blocking_mutex::Mutex; | ||
| 12 | use embassy_sync::signal::Signal; | ||
| 13 | use embassy_sync::zerocopy_channel; | ||
| 14 | use embassy_usb::class::uac1; | ||
| 15 | use embassy_usb::class::uac1::speaker::{self, Speaker}; | ||
| 16 | use embassy_usb::driver::EndpointError; | ||
| 17 | use heapless::Vec; | ||
| 18 | use micromath::F32Ext; | ||
| 19 | use static_cell::StaticCell; | ||
| 20 | use {defmt_rtt as _, panic_probe as _}; | ||
| 21 | |||
| 22 | bind_interrupts!(struct Irqs { | ||
| 23 | OTG_FS => usb::InterruptHandler<peripherals::USB_OTG_FS>; | ||
| 24 | }); | ||
| 25 | |||
| 26 | static TIMER: Mutex<CriticalSectionRawMutex, RefCell<Option<timer::low_level::Timer<peripherals::TIM2>>>> = | ||
| 27 | Mutex::new(RefCell::new(None)); | ||
| 28 | |||
| 29 | // A counter signal that is written by the feedback timer, once every `FEEDBACK_REFRESH_PERIOD`. | ||
| 30 | // At that point, a feedback value is sent to the host. | ||
| 31 | pub static FEEDBACK_SIGNAL: Signal<CriticalSectionRawMutex, u32> = Signal::new(); | ||
| 32 | |||
| 33 | // Stereo input | ||
| 34 | pub const INPUT_CHANNEL_COUNT: usize = 2; | ||
| 35 | |||
| 36 | // This example uses a fixed sample rate of 48 kHz. | ||
| 37 | pub const SAMPLE_RATE_HZ: u32 = 48_000; | ||
| 38 | pub const FEEDBACK_COUNTER_TICK_RATE: u32 = 42_000_000; | ||
| 39 | |||
| 40 | // Use 32 bit samples, which allow for a lot of (software) volume adjustment without degradation of quality. | ||
| 41 | pub const SAMPLE_WIDTH: uac1::SampleWidth = uac1::SampleWidth::Width4Byte; | ||
| 42 | pub const SAMPLE_WIDTH_BIT: usize = SAMPLE_WIDTH.in_bit(); | ||
| 43 | pub const SAMPLE_SIZE: usize = SAMPLE_WIDTH as usize; | ||
| 44 | pub const SAMPLE_SIZE_PER_S: usize = (SAMPLE_RATE_HZ as usize) * INPUT_CHANNEL_COUNT * SAMPLE_SIZE; | ||
| 45 | |||
| 46 | // Size of audio samples per 1 ms - for the full-speed USB frame period of 1 ms. | ||
| 47 | pub const USB_FRAME_SIZE: usize = SAMPLE_SIZE_PER_S.div_ceil(1000); | ||
| 48 | |||
| 49 | // Select front left and right audio channels. | ||
| 50 | pub const AUDIO_CHANNELS: [uac1::Channel; INPUT_CHANNEL_COUNT] = [uac1::Channel::LeftFront, uac1::Channel::RightFront]; | ||
| 51 | |||
| 52 | // Factor of two as a margin for feedback (this is an excessive amount) | ||
| 53 | pub const USB_MAX_PACKET_SIZE: usize = 2 * USB_FRAME_SIZE; | ||
| 54 | pub const USB_MAX_SAMPLE_COUNT: usize = USB_MAX_PACKET_SIZE / SAMPLE_SIZE; | ||
| 55 | |||
| 56 | // The data type that is exchanged via the zero-copy channel (a sample vector). | ||
| 57 | pub type SampleBlock = Vec<u32, USB_MAX_SAMPLE_COUNT>; | ||
| 58 | |||
| 59 | // Feedback is provided in 10.14 format for full-speed endpoints. | ||
| 60 | pub const FEEDBACK_REFRESH_PERIOD: uac1::FeedbackRefresh = uac1::FeedbackRefresh::Period8Frames; | ||
| 61 | const FEEDBACK_SHIFT: usize = 14; | ||
| 62 | |||
| 63 | const TICKS_PER_SAMPLE: f32 = (FEEDBACK_COUNTER_TICK_RATE as f32) / (SAMPLE_RATE_HZ as f32); | ||
| 64 | |||
| 65 | struct Disconnected {} | ||
| 66 | |||
| 67 | impl From<EndpointError> for Disconnected { | ||
| 68 | fn from(val: EndpointError) -> Self { | ||
| 69 | match val { | ||
| 70 | EndpointError::BufferOverflow => panic!("Buffer overflow"), | ||
| 71 | EndpointError::Disabled => Disconnected {}, | ||
| 72 | } | ||
| 73 | } | ||
| 74 | } | ||
| 75 | |||
| 76 | /// Sends feedback messages to the host. | ||
| 77 | async fn feedback_handler<'d, T: usb::Instance + 'd>( | ||
| 78 | feedback: &mut speaker::Feedback<'d, usb::Driver<'d, T>>, | ||
| 79 | feedback_factor: f32, | ||
| 80 | ) -> Result<(), Disconnected> { | ||
| 81 | let mut packet: Vec<u8, 4> = Vec::new(); | ||
| 82 | |||
| 83 | // Collects the fractional component of the feedback value that is lost by rounding. | ||
| 84 | let mut rest = 0.0_f32; | ||
| 85 | |||
| 86 | loop { | ||
| 87 | let counter = FEEDBACK_SIGNAL.wait().await; | ||
| 88 | |||
| 89 | packet.clear(); | ||
| 90 | |||
| 91 | let raw_value = counter as f32 * feedback_factor + rest; | ||
| 92 | let value = raw_value.round(); | ||
| 93 | rest = raw_value - value; | ||
| 94 | |||
| 95 | let value = value as u32; | ||
| 96 | packet.push(value as u8).unwrap(); | ||
| 97 | packet.push((value >> 8) as u8).unwrap(); | ||
| 98 | packet.push((value >> 16) as u8).unwrap(); | ||
| 99 | |||
| 100 | feedback.write_packet(&packet).await?; | ||
| 101 | } | ||
| 102 | } | ||
| 103 | |||
| 104 | /// Handles streaming of audio data from the host. | ||
| 105 | async fn stream_handler<'d, T: usb::Instance + 'd>( | ||
| 106 | stream: &mut speaker::Stream<'d, usb::Driver<'d, T>>, | ||
| 107 | sender: &mut zerocopy_channel::Sender<'static, NoopRawMutex, SampleBlock>, | ||
| 108 | ) -> Result<(), Disconnected> { | ||
| 109 | loop { | ||
| 110 | let mut usb_data = [0u8; USB_MAX_PACKET_SIZE]; | ||
| 111 | let data_size = stream.read_packet(&mut usb_data).await?; | ||
| 112 | |||
| 113 | let word_count = data_size / SAMPLE_SIZE; | ||
| 114 | |||
| 115 | if word_count * SAMPLE_SIZE == data_size { | ||
| 116 | // Obtain a buffer from the channel | ||
| 117 | let samples = sender.send().await; | ||
| 118 | samples.clear(); | ||
| 119 | |||
| 120 | for w in 0..word_count { | ||
| 121 | let byte_offset = w * SAMPLE_SIZE; | ||
| 122 | let sample = u32::from_le_bytes(usb_data[byte_offset..byte_offset + SAMPLE_SIZE].try_into().unwrap()); | ||
| 123 | |||
| 124 | // Fill the sample buffer with data. | ||
| 125 | samples.push(sample).unwrap(); | ||
| 126 | } | ||
| 127 | |||
| 128 | sender.send_done(); | ||
| 129 | } else { | ||
| 130 | debug!("Invalid USB buffer size of {}, skipped.", data_size); | ||
| 131 | } | ||
| 132 | } | ||
| 133 | } | ||
| 134 | |||
| 135 | /// Receives audio samples from the USB streaming task and can play them back. | ||
| 136 | #[embassy_executor::task] | ||
| 137 | async fn audio_receiver_task(mut usb_audio_receiver: zerocopy_channel::Receiver<'static, NoopRawMutex, SampleBlock>) { | ||
| 138 | loop { | ||
| 139 | let _samples = usb_audio_receiver.receive().await; | ||
| 140 | // Use the samples, for example play back via the SAI peripheral. | ||
| 141 | |||
| 142 | // Notify the channel that the buffer is now ready to be reused | ||
| 143 | usb_audio_receiver.receive_done(); | ||
| 144 | } | ||
| 145 | } | ||
| 146 | |||
| 147 | /// Receives audio samples from the host. | ||
| 148 | #[embassy_executor::task] | ||
| 149 | async fn usb_streaming_task( | ||
| 150 | mut stream: speaker::Stream<'static, usb::Driver<'static, peripherals::USB_OTG_FS>>, | ||
| 151 | mut sender: zerocopy_channel::Sender<'static, NoopRawMutex, SampleBlock>, | ||
| 152 | ) { | ||
| 153 | loop { | ||
| 154 | stream.wait_connection().await; | ||
| 155 | _ = stream_handler(&mut stream, &mut sender).await; | ||
| 156 | } | ||
| 157 | } | ||
| 158 | |||
| 159 | /// Sends sample rate feedback to the host. | ||
| 160 | /// | ||
| 161 | /// The `feedback_factor` scales the feedback timer's counter value so that the result is the number of samples that | ||
| 162 | /// this device played back or "consumed" during one SOF period (1 ms) - in 10.14 format. | ||
| 163 | /// | ||
| 164 | /// Ideally, the `feedback_factor` that is calculated below would be an integer for avoiding numerical errors. | ||
| 165 | /// This is achieved by having `TICKS_PER_SAMPLE` be a power of two. For audio applications at a sample rate of 48 kHz, | ||
| 166 | /// 24.576 MHz would be one such option. | ||
| 167 | /// | ||
| 168 | /// A good choice for the STM32F4, which also has to generate a 48 MHz clock from its HSE (e.g. running at 8 MHz) | ||
| 169 | /// for USB, is to clock the feedback timer from the MCLK output of the SAI peripheral. The SAI peripheral then uses an | ||
| 170 | /// external clock. In that case, wiring the MCLK output to the timer clock input is required. | ||
| 171 | /// | ||
| 172 | /// This simple example just uses the internal clocks for supplying the feedback timer, | ||
| 173 | /// and does not even set up a SAI peripheral. | ||
| 174 | #[embassy_executor::task] | ||
| 175 | async fn usb_feedback_task(mut feedback: speaker::Feedback<'static, usb::Driver<'static, peripherals::USB_OTG_FS>>) { | ||
| 176 | let feedback_factor = | ||
| 177 | ((1 << FEEDBACK_SHIFT) as f32 / TICKS_PER_SAMPLE) / FEEDBACK_REFRESH_PERIOD.frame_count() as f32; | ||
| 178 | |||
| 179 | // Should be 2.3405714285714287... | ||
| 180 | info!("Using a feedback factor of {}.", feedback_factor); | ||
| 181 | |||
| 182 | loop { | ||
| 183 | feedback.wait_connection().await; | ||
| 184 | _ = feedback_handler(&mut feedback, feedback_factor).await; | ||
| 185 | } | ||
| 186 | } | ||
| 187 | |||
| 188 | #[embassy_executor::task] | ||
| 189 | async fn usb_task(mut usb_device: embassy_usb::UsbDevice<'static, usb::Driver<'static, peripherals::USB_OTG_FS>>) { | ||
| 190 | usb_device.run().await; | ||
| 191 | } | ||
| 192 | |||
| 193 | /// Checks for changes on the control monitor of the class. | ||
| 194 | /// | ||
| 195 | /// In this case, monitor changes of volume or mute state. | ||
| 196 | #[embassy_executor::task] | ||
| 197 | async fn usb_control_task(control_monitor: speaker::ControlMonitor<'static>) { | ||
| 198 | loop { | ||
| 199 | control_monitor.changed().await; | ||
| 200 | |||
| 201 | for channel in AUDIO_CHANNELS { | ||
| 202 | let volume = control_monitor.volume(channel).unwrap(); | ||
| 203 | info!("Volume changed to {} on channel {}.", volume, channel); | ||
| 204 | } | ||
| 205 | } | ||
| 206 | } | ||
| 207 | |||
| 208 | /// Feedback value measurement and calculation | ||
| 209 | /// | ||
| 210 | /// Used for measuring/calculating the number of samples that were received from the host during the | ||
| 211 | /// `FEEDBACK_REFRESH_PERIOD`. | ||
| 212 | /// | ||
| 213 | /// Configured in this example with | ||
| 214 | /// - a refresh period of 8 ms, and | ||
| 215 | /// - a tick rate of 42 MHz. | ||
| 216 | /// | ||
| 217 | /// This gives an (ideal) counter value of 336.000 for every update of the `FEEDBACK_SIGNAL`. | ||
| 218 | #[interrupt] | ||
| 219 | fn TIM2() { | ||
| 220 | static mut LAST_TICKS: u32 = 0; | ||
| 221 | static mut FRAME_COUNT: usize = 0; | ||
| 222 | |||
| 223 | critical_section::with(|cs| { | ||
| 224 | // Read timer counter. | ||
| 225 | let ticks = TIMER.borrow(cs).borrow().as_ref().unwrap().regs_gp32().cnt().read(); | ||
| 226 | |||
| 227 | // Clear trigger interrupt flag. | ||
| 228 | TIMER | ||
| 229 | .borrow(cs) | ||
| 230 | .borrow_mut() | ||
| 231 | .as_mut() | ||
| 232 | .unwrap() | ||
| 233 | .regs_gp32() | ||
| 234 | .sr() | ||
| 235 | .modify(|r| r.set_tif(false)); | ||
| 236 | |||
| 237 | // Count up frames and emit a signal, when the refresh period is reached (here, every 8 ms). | ||
| 238 | *FRAME_COUNT += 1; | ||
| 239 | if *FRAME_COUNT >= FEEDBACK_REFRESH_PERIOD.frame_count() { | ||
| 240 | *FRAME_COUNT = 0; | ||
| 241 | FEEDBACK_SIGNAL.signal(ticks.wrapping_sub(*LAST_TICKS)); | ||
| 242 | *LAST_TICKS = ticks; | ||
| 243 | } | ||
| 244 | }); | ||
| 245 | } | ||
| 246 | |||
| 247 | // If you are trying this and your USB device doesn't connect, the most | ||
| 248 | // common issues are the RCC config and vbus_detection | ||
| 249 | // | ||
| 250 | // See https://embassy.dev/book/#_the_usb_examples_are_not_working_on_my_board_is_there_anything_else_i_need_to_configure | ||
| 251 | // for more information. | ||
| 252 | #[embassy_executor::main] | ||
| 253 | async fn main(spawner: Spawner) { | ||
| 254 | info!("Hello World!"); | ||
| 255 | |||
| 256 | let mut config = Config::default(); | ||
| 257 | { | ||
| 258 | use embassy_stm32::rcc::*; | ||
| 259 | config.rcc.hse = Some(Hse { | ||
| 260 | freq: Hertz(8_000_000), | ||
| 261 | mode: HseMode::Bypass, | ||
| 262 | }); | ||
| 263 | config.rcc.pll_src = PllSource::HSE; | ||
| 264 | config.rcc.pll = Some(Pll { | ||
| 265 | prediv: PllPreDiv::DIV4, | ||
| 266 | mul: PllMul::MUL168, | ||
| 267 | divp: Some(PllPDiv::DIV2), // ((8 MHz / 4) * 168) / 2 = 168 Mhz. | ||
| 268 | divq: Some(PllQDiv::DIV7), // ((8 MHz / 4) * 168) / 7 = 48 Mhz. | ||
| 269 | divr: None, | ||
| 270 | }); | ||
| 271 | config.rcc.ahb_pre = AHBPrescaler::DIV1; | ||
| 272 | config.rcc.apb1_pre = APBPrescaler::DIV4; | ||
| 273 | config.rcc.apb2_pre = APBPrescaler::DIV2; | ||
| 274 | config.rcc.sys = Sysclk::PLL1_P; | ||
| 275 | config.rcc.mux.clk48sel = mux::Clk48sel::PLL1_Q; | ||
| 276 | } | ||
| 277 | let p = embassy_stm32::init(config); | ||
| 278 | |||
| 279 | // Configure all required buffers in a static way. | ||
| 280 | debug!("USB packet size is {} byte", USB_MAX_PACKET_SIZE); | ||
| 281 | static CONFIG_DESCRIPTOR: StaticCell<[u8; 256]> = StaticCell::new(); | ||
| 282 | let config_descriptor = CONFIG_DESCRIPTOR.init([0; 256]); | ||
| 283 | |||
| 284 | static BOS_DESCRIPTOR: StaticCell<[u8; 32]> = StaticCell::new(); | ||
| 285 | let bos_descriptor = BOS_DESCRIPTOR.init([0; 32]); | ||
| 286 | |||
| 287 | const CONTROL_BUF_SIZE: usize = 64; | ||
| 288 | static CONTROL_BUF: StaticCell<[u8; CONTROL_BUF_SIZE]> = StaticCell::new(); | ||
| 289 | let control_buf = CONTROL_BUF.init([0; CONTROL_BUF_SIZE]); | ||
| 290 | |||
| 291 | const FEEDBACK_BUF_SIZE: usize = 4; | ||
| 292 | static EP_OUT_BUFFER: StaticCell<[u8; FEEDBACK_BUF_SIZE + CONTROL_BUF_SIZE + USB_MAX_PACKET_SIZE]> = | ||
| 293 | StaticCell::new(); | ||
| 294 | let ep_out_buffer = EP_OUT_BUFFER.init([0u8; FEEDBACK_BUF_SIZE + CONTROL_BUF_SIZE + USB_MAX_PACKET_SIZE]); | ||
| 295 | |||
| 296 | static STATE: StaticCell<speaker::State> = StaticCell::new(); | ||
| 297 | let state = STATE.init(speaker::State::new()); | ||
| 298 | |||
| 299 | // Create the driver, from the HAL. | ||
| 300 | let mut usb_config = usb::Config::default(); | ||
| 301 | |||
| 302 | // Do not enable vbus_detection. This is a safe default that works in all boards. | ||
| 303 | // However, if your USB device is self-powered (can stay powered on if USB is unplugged), you need | ||
| 304 | // to enable vbus_detection to comply with the USB spec. If you enable it, the board | ||
| 305 | // has to support it or USB won't work at all. See docs on `vbus_detection` for details. | ||
| 306 | usb_config.vbus_detection = false; | ||
| 307 | |||
| 308 | let usb_driver = usb::Driver::new_fs(p.USB_OTG_FS, Irqs, p.PA12, p.PA11, ep_out_buffer, usb_config); | ||
| 309 | |||
| 310 | // Basic USB device configuration | ||
| 311 | let mut config = embassy_usb::Config::new(0xc0de, 0xcafe); | ||
| 312 | config.manufacturer = Some("Embassy"); | ||
| 313 | config.product = Some("USB-audio-speaker example"); | ||
| 314 | config.serial_number = Some("12345678"); | ||
| 315 | |||
| 316 | // Required for windows compatibility. | ||
| 317 | // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/1.9.1/kconfig/CONFIG_CDC_ACM_IAD.html#help | ||
| 318 | config.device_class = 0xEF; | ||
| 319 | config.device_sub_class = 0x02; | ||
| 320 | config.device_protocol = 0x01; | ||
| 321 | config.composite_with_iads = true; | ||
| 322 | |||
| 323 | let mut builder = embassy_usb::Builder::new( | ||
| 324 | usb_driver, | ||
| 325 | config, | ||
| 326 | config_descriptor, | ||
| 327 | bos_descriptor, | ||
| 328 | &mut [], // no msos descriptors | ||
| 329 | control_buf, | ||
| 330 | ); | ||
| 331 | |||
| 332 | // Create the UAC1 Speaker class components | ||
| 333 | let (stream, feedback, control_monitor) = Speaker::new( | ||
| 334 | &mut builder, | ||
| 335 | state, | ||
| 336 | USB_MAX_PACKET_SIZE as u16, | ||
| 337 | uac1::SampleWidth::Width4Byte, | ||
| 338 | &[SAMPLE_RATE_HZ], | ||
| 339 | &AUDIO_CHANNELS, | ||
| 340 | FEEDBACK_REFRESH_PERIOD, | ||
| 341 | ); | ||
| 342 | |||
| 343 | // Create the USB device | ||
| 344 | let usb_device = builder.build(); | ||
| 345 | |||
| 346 | // Establish a zero-copy channel for transferring received audio samples between tasks | ||
| 347 | static SAMPLE_BLOCKS: StaticCell<[SampleBlock; 2]> = StaticCell::new(); | ||
| 348 | let sample_blocks = SAMPLE_BLOCKS.init([Vec::new(), Vec::new()]); | ||
| 349 | |||
| 350 | static CHANNEL: StaticCell<zerocopy_channel::Channel<'_, NoopRawMutex, SampleBlock>> = StaticCell::new(); | ||
| 351 | let channel = CHANNEL.init(zerocopy_channel::Channel::new(sample_blocks)); | ||
| 352 | let (sender, receiver) = channel.split(); | ||
| 353 | |||
| 354 | // Run a timer for counting between SOF interrupts. | ||
| 355 | let mut tim2 = timer::low_level::Timer::new(p.TIM2); | ||
| 356 | tim2.set_tick_freq(Hertz(FEEDBACK_COUNTER_TICK_RATE)); | ||
| 357 | tim2.set_trigger_source(timer::low_level::TriggerSource::ITR1); // The USB SOF signal. | ||
| 358 | tim2.set_slave_mode(timer::low_level::SlaveMode::TRIGGER_MODE); | ||
| 359 | tim2.regs_gp16().dier().modify(|r| r.set_tie(true)); // Enable the trigger interrupt. | ||
| 360 | tim2.start(); | ||
| 361 | |||
| 362 | TIMER.lock(|p| p.borrow_mut().replace(tim2)); | ||
| 363 | |||
| 364 | // Unmask the TIM2 interrupt. | ||
| 365 | unsafe { | ||
| 366 | cortex_m::peripheral::NVIC::unmask(interrupt::TIM2); | ||
| 367 | } | ||
| 368 | |||
| 369 | // Launch USB audio tasks. | ||
| 370 | unwrap!(spawner.spawn(usb_control_task(control_monitor))); | ||
| 371 | unwrap!(spawner.spawn(usb_streaming_task(stream, sender))); | ||
| 372 | unwrap!(spawner.spawn(usb_feedback_task(feedback))); | ||
| 373 | unwrap!(spawner.spawn(usb_task(usb_device))); | ||
| 374 | unwrap!(spawner.spawn(audio_receiver_task(receiver))); | ||
| 375 | } | ||
