| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "garnet/drivers/audio/virtual_audio/virtual_audio_stream.h" |
| |
| #include <ddk/debug.h> |
| #include <cmath> |
| |
| #include "garnet/drivers/audio/virtual_audio/virtual_audio_device_impl.h" |
| #include "garnet/drivers/audio/virtual_audio/virtual_audio_stream_in.h" |
| #include "garnet/drivers/audio/virtual_audio/virtual_audio_stream_out.h" |
| |
| namespace virtual_audio { |
| |
| constexpr bool kTestPosition = false; |
| |
| // static |
| fbl::RefPtr<VirtualAudioStream> VirtualAudioStream::CreateStream( |
| VirtualAudioDeviceImpl* owner, zx_device_t* devnode, bool is_input) { |
| if (is_input) { |
| return ::audio::SimpleAudioStream::Create<VirtualAudioStreamIn>(owner, |
| devnode); |
| } else { |
| return ::audio::SimpleAudioStream::Create<VirtualAudioStreamOut>(owner, |
| devnode); |
| } |
| } |
| |
| VirtualAudioStream::~VirtualAudioStream() { |
| ZX_DEBUG_ASSERT(domain_->deactivated()); |
| } |
| |
| zx_status_t VirtualAudioStream::Init() { |
| if (!strlcpy(device_name_, parent_->device_name_.c_str(), |
| sizeof(device_name_))) { |
| return ZX_ERR_INTERNAL; |
| } |
| |
| if (!strlcpy(mfr_name_, parent_->mfr_name_.c_str(), sizeof(mfr_name_))) { |
| return ZX_ERR_INTERNAL; |
| } |
| |
| if (!strlcpy(prod_name_, parent_->prod_name_.c_str(), sizeof(prod_name_))) { |
| return ZX_ERR_INTERNAL; |
| } |
| |
| memcpy(unique_id_.data, parent_->unique_id_, sizeof(unique_id_.data)); |
| |
| supported_formats_.reset(); |
| for (auto range : parent_->supported_formats_) { |
| supported_formats_.push_back(range); |
| } |
| |
| fifo_depth_ = parent_->fifo_depth_; |
| external_delay_nsec_ = parent_->external_delay_nsec_; |
| |
| max_buffer_frames_ = parent_->max_buffer_frames_; |
| min_buffer_frames_ = parent_->min_buffer_frames_; |
| modulo_buffer_frames_ = parent_->modulo_buffer_frames_; |
| |
| cur_gain_state_ = parent_->cur_gain_state_; |
| |
| audio_pd_notify_flags_t plug_flags = 0; |
| if (parent_->hardwired_) { |
| plug_flags |= AUDIO_PDNF_HARDWIRED; |
| } |
| if (parent_->async_plug_notify_) { |
| plug_flags |= AUDIO_PDNF_CAN_NOTIFY; |
| } |
| if (parent_->plugged_) { |
| plug_flags |= AUDIO_PDNF_PLUGGED; |
| } |
| SetInitialPlugState(plug_flags); |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t VirtualAudioStream::InitPost() { |
| plug_change_wakeup_ = dispatcher::WakeupEvent::Create(); |
| if (plug_change_wakeup_ == nullptr) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| dispatcher::WakeupEvent::ProcessHandler plug_wake_handler( |
| [this](dispatcher::WakeupEvent* event) -> zx_status_t { |
| OBTAIN_EXECUTION_DOMAIN_TOKEN(t, domain_); |
| HandlePlugChanges(); |
| return ZX_OK; |
| }); |
| zx_status_t status = |
| plug_change_wakeup_->Activate(domain_, std::move(plug_wake_handler)); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Plug WakeupEvent activate failed (%d)\n", status); |
| return status; |
| } |
| |
| gain_request_wakeup_ = dispatcher::WakeupEvent::Create(); |
| if (gain_request_wakeup_ == nullptr) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| dispatcher::WakeupEvent::ProcessHandler gain_wake_handler( |
| [this](dispatcher::WakeupEvent* event) -> zx_status_t { |
| OBTAIN_EXECUTION_DOMAIN_TOKEN(t, domain_); |
| HandleGainRequests(); |
| return ZX_OK; |
| }); |
| status = |
| gain_request_wakeup_->Activate(domain_, std::move(gain_wake_handler)); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "GetGain WakeupEvent activate failed (%d)\n", status); |
| return status; |
| } |
| |
| format_request_wakeup_ = dispatcher::WakeupEvent::Create(); |
| if (format_request_wakeup_ == nullptr) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| dispatcher::WakeupEvent::ProcessHandler format_wake_handler( |
| [this](dispatcher::WakeupEvent* event) -> zx_status_t { |
| OBTAIN_EXECUTION_DOMAIN_TOKEN(t, domain_); |
| HandleFormatRequests(); |
| return ZX_OK; |
| }); |
| status = |
| format_request_wakeup_->Activate(domain_, std::move(format_wake_handler)); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "GetFormat WakeupEvent activate failed (%d)\n", status); |
| return status; |
| } |
| |
| buffer_request_wakeup_ = dispatcher::WakeupEvent::Create(); |
| if (buffer_request_wakeup_ == nullptr) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| dispatcher::WakeupEvent::ProcessHandler buffer_wake_handler( |
| [this](dispatcher::WakeupEvent* event) -> zx_status_t { |
| OBTAIN_EXECUTION_DOMAIN_TOKEN(t, domain_); |
| HandleBufferRequests(); |
| return ZX_OK; |
| }); |
| status = |
| buffer_request_wakeup_->Activate(domain_, std::move(buffer_wake_handler)); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "GetBuffer WakeupEvent activate failed (%d)\n", status); |
| return status; |
| } |
| |
| position_request_wakeup_ = dispatcher::WakeupEvent::Create(); |
| if (position_request_wakeup_ == nullptr) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| dispatcher::WakeupEvent::ProcessHandler position_wake_handler( |
| [this](dispatcher::WakeupEvent* event) -> zx_status_t { |
| OBTAIN_EXECUTION_DOMAIN_TOKEN(t, domain_); |
| HandlePositionRequests(); |
| return ZX_OK; |
| }); |
| status = position_request_wakeup_->Activate(domain_, |
| std::move(position_wake_handler)); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "GetPosition WakeupEvent activate failed (%d)\n", status); |
| return status; |
| } |
| |
| notify_timer_ = dispatcher::Timer::Create(); |
| if (notify_timer_ == nullptr) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| dispatcher::Timer::ProcessHandler timer_handler( |
| [this](dispatcher::Timer* timer) -> zx_status_t { |
| OBTAIN_EXECUTION_DOMAIN_TOKEN(t, domain_); |
| return ProcessRingNotification(); |
| }); |
| status = notify_timer_->Activate(domain_, std::move(timer_handler)); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "PositionNotify Timer activate failed (%d)\n", status); |
| return status; |
| } |
| |
| return ZX_OK; |
| } |
| |
| void VirtualAudioStream::EnqueuePlugChange(bool plugged) { |
| { |
| fbl::AutoLock lock(&wakeup_queue_lock_); |
| PlugType plug_change = (plugged ? PlugType::Plug : PlugType::Unplug); |
| plug_queue_.push_back(plug_change); |
| } |
| |
| plug_change_wakeup_->Signal(); |
| } |
| |
| void VirtualAudioStream::EnqueueGainRequest( |
| fuchsia::virtualaudio::Device::GetGainCallback gain_callback) { |
| { |
| fbl::AutoLock lock(&wakeup_queue_lock_); |
| gain_queue_.push_back(std::move(gain_callback)); |
| } |
| |
| gain_request_wakeup_->Signal(); |
| } |
| |
| void VirtualAudioStream::EnqueueFormatRequest( |
| fuchsia::virtualaudio::Device::GetFormatCallback format_callback) { |
| { |
| fbl::AutoLock lock(&wakeup_queue_lock_); |
| format_queue_.push_back(std::move(format_callback)); |
| } |
| |
| format_request_wakeup_->Signal(); |
| } |
| |
| void VirtualAudioStream::EnqueueBufferRequest( |
| fuchsia::virtualaudio::Device::GetBufferCallback buffer_callback) { |
| { |
| fbl::AutoLock lock(&wakeup_queue_lock_); |
| buffer_queue_.push_back(std::move(buffer_callback)); |
| } |
| |
| buffer_request_wakeup_->Signal(); |
| } |
| |
| void VirtualAudioStream::EnqueuePositionRequest( |
| fuchsia::virtualaudio::Device::GetPositionCallback position_callback) { |
| { |
| fbl::AutoLock lock(&wakeup_queue_lock_); |
| position_queue_.push_back(std::move(position_callback)); |
| } |
| |
| position_request_wakeup_->Signal(); |
| } |
| |
| void VirtualAudioStream::HandlePlugChanges() { |
| while (true) { |
| PlugType plug_change; |
| |
| if (fbl::AutoLock lock(&wakeup_queue_lock_); !plug_queue_.empty()) { |
| plug_change = plug_queue_.front(); |
| plug_queue_.pop_front(); |
| } else { |
| break; |
| } |
| |
| HandlePlugChange(plug_change); |
| } |
| } |
| |
| void VirtualAudioStream::HandlePlugChange(PlugType plug_change) { |
| switch (plug_change) { |
| case PlugType::Plug: |
| SetPlugState(true); |
| break; |
| case PlugType::Unplug: |
| SetPlugState(false); |
| break; |
| // Intentionally omitting default, so new enums surface a logic error. |
| } |
| } |
| |
| void VirtualAudioStream::HandleGainRequests() { |
| while (true) { |
| bool current_mute, current_agc; |
| float current_gain_db; |
| fuchsia::virtualaudio::Device::GetGainCallback gain_callback; |
| |
| if (fbl::AutoLock lock(&wakeup_queue_lock_); !gain_queue_.empty()) { |
| current_mute = cur_gain_state_.cur_mute; |
| current_agc = cur_gain_state_.cur_agc; |
| current_gain_db = cur_gain_state_.cur_gain; |
| |
| gain_callback = std::move(gain_queue_.front()); |
| gain_queue_.pop_front(); |
| } else { |
| break; |
| } |
| |
| parent_->PostToDispatcher([gain_callback = std::move(gain_callback), |
| current_mute, current_agc, current_gain_db]() { |
| gain_callback(current_mute, current_agc, current_gain_db); |
| }); |
| } |
| } |
| |
| void VirtualAudioStream::HandleFormatRequests() { |
| while (true) { |
| uint32_t frames_per_second, sample_format, num_channels; |
| zx_duration_t external_delay; |
| fuchsia::virtualaudio::Device::GetFormatCallback format_callback; |
| |
| if (fbl::AutoLock lock(&wakeup_queue_lock_); !format_queue_.empty()) { |
| frames_per_second = frame_rate_; |
| sample_format = sample_format_; |
| num_channels = num_channels_; |
| external_delay = external_delay_nsec_; |
| |
| format_callback = std::move(format_queue_.front()); |
| format_queue_.pop_front(); |
| } else { |
| break; |
| } |
| |
| if (frames_per_second == 0) { |
| zxlogf(TRACE, "Format is not set - should not be calling GetFormat\n"); |
| return; |
| } |
| |
| parent_->PostToDispatcher([format_callback = std::move(format_callback), |
| frames_per_second, sample_format, num_channels, |
| external_delay]() { |
| format_callback(frames_per_second, sample_format, num_channels, |
| external_delay); |
| }); |
| } |
| } |
| |
| void VirtualAudioStream::HandleBufferRequests() { |
| while (true) { |
| zx_status_t status; |
| zx::vmo ring_buffer_vmo; |
| uint32_t num_ring_buffer_frames; |
| uint32_t notifications_per_ring; |
| fuchsia::virtualaudio::Device::GetBufferCallback buffer_callback; |
| |
| if (fbl::AutoLock lock(&wakeup_queue_lock_); !buffer_queue_.empty()) { |
| status = ring_buffer_vmo_.duplicate( |
| ZX_RIGHT_TRANSFER | ZX_RIGHT_READ | ZX_RIGHT_WRITE | ZX_RIGHT_MAP, |
| &ring_buffer_vmo); |
| num_ring_buffer_frames = num_ring_buffer_frames_; |
| notifications_per_ring = notifications_per_ring_; |
| |
| buffer_callback = std::move(buffer_queue_.front()); |
| buffer_queue_.pop_front(); |
| } else { |
| break; |
| } |
| |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s failed to duplicate VMO handle - %d\n", __func__, |
| status); |
| return; |
| } |
| if (!ring_buffer_vmo.is_valid()) { |
| zxlogf(TRACE, |
| "Buffer is not set - should not be retrieving ring buffer\n"); |
| return; |
| } |
| |
| parent_->PostToDispatcher([buffer_callback = std::move(buffer_callback), |
| rb_vmo = std::move(ring_buffer_vmo), |
| num_ring_buffer_frames, |
| notifications_per_ring]() mutable { |
| buffer_callback(std::move(rb_vmo), num_ring_buffer_frames, |
| notifications_per_ring); |
| }); |
| } |
| } |
| |
| void VirtualAudioStream::HandlePositionRequests() { |
| while (true) { |
| zx::time start_time; |
| uint32_t num_rb_frames, frame_size, frame_rate; |
| fuchsia::virtualaudio::Device::GetPositionCallback position_callback; |
| |
| if (fbl::AutoLock lock(&wakeup_queue_lock_); !position_queue_.empty()) { |
| start_time = start_time_; |
| num_rb_frames = num_ring_buffer_frames_; |
| frame_size = frame_size_; |
| frame_rate = frame_rate_; |
| |
| position_callback = std::move(position_queue_.front()); |
| position_queue_.pop_front(); |
| } else { |
| break; |
| } |
| |
| if (start_time.get() == 0) { |
| zxlogf(TRACE, |
| "Stream is not started -- should not be calling GetPosition\n"); |
| return; |
| } |
| |
| zx::time now = zx::clock::get_monotonic(); |
| zx::duration duration_ns = now - start_time; |
| uint64_t frames = (duration_ns.get() * frame_rate) / ZX_SEC(1); |
| uint32_t ring_buffer_position = (frames % num_rb_frames) * frame_size; |
| zx_time_t time_for_position = now.get(); |
| |
| parent_->PostToDispatcher([position_callback = std::move(position_callback), |
| ring_buffer_position, time_for_position]() { |
| position_callback(ring_buffer_position, time_for_position); |
| }); |
| } |
| } |
| |
| // Upon success, drivers should return a valid VMO with appropriate |
| // permissions (READ | MAP | TRANSFER for inputs, WRITE as well for outputs) |
| // as well as reporting the total number of usable frames in the ring. |
| // |
| // Format must already be set: a ring buffer channel (over which this command |
| // arrived) is provided as the return value from a successful SetFormat call. |
| zx_status_t VirtualAudioStream::GetBuffer( |
| const ::audio::audio_proto::RingBufGetBufferReq& req, |
| uint32_t* out_num_rb_frames, zx::vmo* out_buffer) { |
| if (req.notifications_per_ring > req.min_ring_buffer_frames) { |
| zxlogf(ERROR, "req.notifications_per_ring too big"); |
| return ZX_ERR_OUT_OF_RANGE; |
| } |
| if (req.min_ring_buffer_frames > max_buffer_frames_) { |
| zxlogf(ERROR, "req.min_ring_buffer_frames too big"); |
| return ZX_ERR_OUT_OF_RANGE; |
| } |
| |
| num_ring_buffer_frames_ = |
| std::max(min_buffer_frames_, |
| fbl::round_up<uint32_t, uint32_t>(req.min_ring_buffer_frames, |
| modulo_buffer_frames_)); |
| uint32_t ring_buffer_size = fbl::round_up<size_t, size_t>( |
| num_ring_buffer_frames_ * frame_size_, ZX_PAGE_SIZE); |
| |
| if (kTestPosition) { |
| zxlogf(TRACE, |
| "%s: cmd: %x, min_ring_buffer_frames: %u, notif_per_ring: %d. " |
| "Result: rb_frames: %u, buffer_size: %u\n", |
| __PRETTY_FUNCTION__, req.hdr.cmd, req.min_ring_buffer_frames, |
| req.notifications_per_ring, num_ring_buffer_frames_, |
| ring_buffer_size); |
| } |
| |
| if (ring_buffer_mapper_.start() != nullptr) { |
| ring_buffer_mapper_.Unmap(); |
| } |
| |
| zx_status_t status = ring_buffer_mapper_.CreateAndMap( |
| ring_buffer_size, ZX_VM_PERM_READ | ZX_VM_PERM_WRITE, nullptr, |
| &ring_buffer_vmo_, |
| ZX_RIGHT_READ | ZX_RIGHT_WRITE | ZX_RIGHT_MAP | ZX_RIGHT_DUPLICATE | |
| ZX_RIGHT_TRANSFER); |
| |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s failed to create ring buffer vmo - %d\n", __func__, |
| status); |
| return status; |
| } |
| status = ring_buffer_vmo_.duplicate( |
| ZX_RIGHT_TRANSFER | ZX_RIGHT_READ | ZX_RIGHT_WRITE | ZX_RIGHT_MAP, |
| out_buffer); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s failed to duplicate VMO handle for out param - %d\n", |
| __func__, status); |
| return status; |
| } |
| |
| notifications_per_ring_ = req.notifications_per_ring; |
| |
| if (notifications_per_ring_ == 0) { |
| us_per_notification_ = 0u; |
| } else { |
| us_per_notification_ = static_cast<uint32_t>( |
| (ZX_SEC(1) * num_ring_buffer_frames_) / |
| (ZX_USEC(1) * frame_rate_ * notifications_per_ring_)); |
| } |
| |
| if (kTestPosition) { |
| zxlogf(TRACE, "%s us_per_notification is %u\n", __PRETTY_FUNCTION__, |
| us_per_notification_); |
| } |
| |
| *out_num_rb_frames = num_ring_buffer_frames_; |
| |
| zx::vmo duplicate_vmo; |
| status = ring_buffer_vmo_.duplicate( |
| ZX_RIGHT_TRANSFER | ZX_RIGHT_READ | ZX_RIGHT_WRITE | ZX_RIGHT_MAP, |
| &duplicate_vmo); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s failed to duplicate VMO handle for notification - %d\n", |
| __func__, status); |
| return status; |
| } |
| parent_->NotifyBufferCreated(std::move(duplicate_vmo), |
| num_ring_buffer_frames_, |
| notifications_per_ring_); |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t VirtualAudioStream::ChangeFormat( |
| const ::audio::audio_proto::StreamSetFmtReq& req) { |
| // frame_size_ is already set, automatically |
| ZX_DEBUG_ASSERT(frame_size_); |
| |
| frame_rate_ = req.frames_per_second; |
| ZX_DEBUG_ASSERT(frame_rate_); |
| |
| sample_format_ = req.sample_format; |
| |
| num_channels_ = req.channels; |
| bytes_per_sec_ = frame_rate_ * frame_size_; |
| |
| // (Re)set external_delay_nsec_ and fifo_depth_ before leaving, if needed. |
| |
| parent_->NotifySetFormat(frame_rate_, sample_format_, num_channels_, |
| external_delay_nsec_); |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t VirtualAudioStream::SetGain( |
| const ::audio::audio_proto::SetGainReq& req) { |
| if (req.flags & AUDIO_SGF_GAIN_VALID) { |
| cur_gain_state_.cur_gain = |
| trunc(req.gain / cur_gain_state_.gain_step) * cur_gain_state_.gain_step; |
| } |
| |
| if (req.flags & AUDIO_SGF_MUTE_VALID) { |
| cur_gain_state_.cur_mute = req.flags & AUDIO_SGF_MUTE; |
| } |
| |
| if (req.flags & AUDIO_SGF_AGC_VALID) { |
| cur_gain_state_.cur_agc = req.flags & AUDIO_SGF_AGC; |
| } |
| |
| parent_->NotifySetGain(cur_gain_state_.cur_mute, cur_gain_state_.cur_agc, |
| cur_gain_state_.cur_gain); |
| |
| return ZX_OK; |
| } |
| |
| // Drivers *must* report the time at which the first frame will be clocked out |
| // on the CLOCK_MONOTONIC timeline, not including any external delay. |
| zx_status_t VirtualAudioStream::Start(uint64_t* out_start_time) { |
| start_time_ = zx::clock::get_monotonic() + |
| // Incorporate delay caused by fifo_depth_ |
| zx::duration((ZX_SEC(1) * fifo_depth_) / bytes_per_sec_); |
| |
| if (kTestPosition) { |
| zxlogf(TRACE, "%s at %ld, running at %d b/s\n", __PRETTY_FUNCTION__, |
| start_time_.get(), bytes_per_sec_); |
| } |
| |
| *out_start_time = start_time_.get(); |
| |
| // Set the timer here (if notifications are enabled). |
| if (us_per_notification_) { |
| notify_timer_->Arm(*out_start_time); |
| } |
| |
| parent_->NotifyStart(*out_start_time); |
| |
| return ZX_OK; |
| } |
| |
| // Timer handler for sending out position notifications |
| // TODO(mpuryear): Establish a notification cadence at the requested ring |
| // positions, such as 0, 1000, 0, 1000,... instead of 4, 1006, 8, 1010, 12,... |
| zx_status_t VirtualAudioStream::ProcessRingNotification() { |
| ZX_DEBUG_ASSERT(us_per_notification_ > 0); |
| |
| zx::time now = zx::clock::get_monotonic(); |
| |
| notify_timer_->Arm(now.get() + ZX_USEC(us_per_notification_)); |
| |
| ::audio::audio_proto::RingBufPositionNotify resp = {}; |
| resp.hdr.cmd = AUDIO_RB_POSITION_NOTIFY; |
| |
| zx::duration duration_ns = now - start_time_; |
| // TODO(mpuryear): use a proper Timeline object here. Reference MTWN-57. |
| uint64_t frames = (duration_ns.get() * frame_rate_) / ZX_SEC(1); |
| uint32_t ring_buffer_position = |
| (frames % num_ring_buffer_frames_) * frame_size_; |
| resp.ring_buffer_pos = ring_buffer_position; |
| |
| if (kTestPosition) { |
| zxlogf(TRACE, "%s at %08x, %ld\n", __PRETTY_FUNCTION__, |
| resp.ring_buffer_pos, now.get()); |
| } |
| |
| zx_status_t status = NotifyPosition(resp); |
| |
| parent_->NotifyPosition(ring_buffer_position, now.get()); |
| |
| return status; |
| } |
| |
| zx_status_t VirtualAudioStream::Stop() { |
| auto stop_time = zx::clock::get_monotonic(); |
| |
| if (kTestPosition) { |
| zxlogf(TRACE, "%s at %ld\n", __PRETTY_FUNCTION__, stop_time.get()); |
| } |
| |
| notify_timer_->Cancel(); |
| |
| zx::duration duration_ns = stop_time - start_time_; |
| uint64_t frames = (duration_ns.get() * frame_rate_) / ZX_SEC(1); |
| uint32_t ring_buf_position = (frames % num_ring_buffer_frames_) * frame_size_; |
| parent_->NotifyStop(stop_time.get(), ring_buf_position); |
| |
| start_time_ = zx::time(0); |
| |
| return ZX_OK; |
| } |
| |
| // Called by parent SimpleAudioStream::Shutdown, during DdkUnbind. |
| // If our parent is not shutting down, then someone else called our DdkUnbind |
| // (perhaps the DevHost is removing our driver), and we should let our parent |
| // know so that it does not later try to Unbind us. Knowing who started the |
| // unwinding allows this to proceed in an orderly way, in all cases. |
| void VirtualAudioStream::ShutdownHook() { |
| if (!shutdown_by_parent_) { |
| parent_->ClearStream(); |
| } |
| } |
| |
| } // namespace virtual_audio |