diff options
Diffstat (limited to 'media/audio/win/audio_unified_win.cc')
-rw-r--r-- | media/audio/win/audio_unified_win.cc | 984 |
1 files changed, 984 insertions, 0 deletions
diff --git a/media/audio/win/audio_unified_win.cc b/media/audio/win/audio_unified_win.cc new file mode 100644 index 0000000..901c8b8 --- /dev/null +++ b/media/audio/win/audio_unified_win.cc @@ -0,0 +1,984 @@ +// Copyright (c) 2012 The Chromium 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 "media/audio/win/audio_unified_win.h" + +#include <Functiondiscoverykeys_devpkey.h> + +#include "base/debug/trace_event.h" +#ifndef NDEBUG +#include "base/file_util.h" +#include "base/path_service.h" +#endif +#include "base/time/time.h" +#include "base/win/scoped_com_initializer.h" +#include "media/audio/win/audio_manager_win.h" +#include "media/audio/win/avrt_wrapper_win.h" +#include "media/audio/win/core_audio_util_win.h" + +using base::win::ScopedComPtr; +using base::win::ScopedCOMInitializer; +using base::win::ScopedCoMem; + +// Smoothing factor in exponential smoothing filter where 0 < alpha < 1. +// Larger values of alpha reduce the level of smoothing. +// See http://en.wikipedia.org/wiki/Exponential_smoothing for details. +static const double kAlpha = 0.1; + +// Compute a rate compensation which always attracts us back to a specified +// target level over a period of |kCorrectionTimeSeconds|. +static const double kCorrectionTimeSeconds = 0.1; + +#ifndef NDEBUG +// Max number of columns in the output text file |kUnifiedAudioDebugFileName|. +// See LogElementNames enumerator for details on what each column represents. +static const size_t kMaxNumSampleTypes = 4; + +static const size_t kMaxNumParams = 2; + +// Max number of rows in the output file |kUnifiedAudioDebugFileName|. +// Each row corresponds to one set of sample values for (approximately) the +// same time instant (stored in the first column). +static const size_t kMaxFileSamples = 10000; + +// Name of output debug file used for off-line analysis of measurements which +// can be utilized for performance tuning of this class. +static const char kUnifiedAudioDebugFileName[] = "unified_win_debug.txt"; + +// Name of output debug file used for off-line analysis of measurements. +// This file will contain a list of audio parameters. +static const char kUnifiedAudioParamsFileName[] = "unified_win_params.txt"; +#endif + +// Use the acquired IAudioClock interface to derive a time stamp of the audio +// sample which is currently playing through the speakers. +static double SpeakerStreamPosInMilliseconds(IAudioClock* clock) { + UINT64 device_frequency = 0, position = 0; + if (FAILED(clock->GetFrequency(&device_frequency)) || + FAILED(clock->GetPosition(&position, NULL))) { + return 0.0; + } + return base::Time::kMillisecondsPerSecond * + (static_cast<double>(position) / device_frequency); +} + +// Get a time stamp in milliseconds given number of audio frames in |num_frames| +// using the current sample rate |fs| as scale factor. +// Example: |num_frames| = 960 and |fs| = 48000 => 20 [ms]. +static double CurrentStreamPosInMilliseconds(UINT64 num_frames, DWORD fs) { + return base::Time::kMillisecondsPerSecond * + (static_cast<double>(num_frames) / fs); +} + +// Convert a timestamp in milliseconds to byte units given the audio format +// in |format|. +// Example: |ts_milliseconds| equals 10, sample rate is 48000 and frame size +// is 4 bytes per audio frame => 480 * 4 = 1920 [bytes]. +static int MillisecondsToBytes(double ts_milliseconds, + const WAVEFORMATPCMEX& format) { + double seconds = ts_milliseconds / base::Time::kMillisecondsPerSecond; + return static_cast<int>(seconds * format.Format.nSamplesPerSec * + format.Format.nBlockAlign + 0.5); +} + +// Convert frame count to milliseconds given the audio format in |format|. +static double FrameCountToMilliseconds(int num_frames, + const WAVEFORMATPCMEX& format) { + return (base::Time::kMillisecondsPerSecond * num_frames) / + static_cast<double>(format.Format.nSamplesPerSec); +} + +namespace media { + +WASAPIUnifiedStream::WASAPIUnifiedStream(AudioManagerWin* manager, + const AudioParameters& params, + const std::string& input_device_id) + : creating_thread_id_(base::PlatformThread::CurrentId()), + manager_(manager), + params_(params), + input_channels_(params.input_channels()), + output_channels_(params.channels()), + input_device_id_(input_device_id), + share_mode_(CoreAudioUtil::GetShareMode()), + opened_(false), + volume_(1.0), + output_buffer_size_frames_(0), + input_buffer_size_frames_(0), + endpoint_render_buffer_size_frames_(0), + endpoint_capture_buffer_size_frames_(0), + num_written_frames_(0), + total_delay_ms_(0.0), + total_delay_bytes_(0), + source_(NULL), + input_callback_received_(false), + io_sample_rate_ratio_(1), + target_fifo_frames_(0), + average_delta_(0), + fifo_rate_compensation_(1), + update_output_delay_(false), + capture_delay_ms_(0) { + TRACE_EVENT0("audio", "WASAPIUnifiedStream::WASAPIUnifiedStream"); + VLOG(1) << "WASAPIUnifiedStream::WASAPIUnifiedStream()"; + DCHECK(manager_); + + VLOG(1) << "Input channels : " << input_channels_; + VLOG(1) << "Output channels: " << output_channels_; + VLOG(1) << "Sample rate : " << params_.sample_rate(); + VLOG(1) << "Buffer size : " << params.frames_per_buffer(); + +#ifndef NDEBUG + input_time_stamps_.reset(new int64[kMaxFileSamples]); + num_frames_in_fifo_.reset(new int[kMaxFileSamples]); + resampler_margin_.reset(new int[kMaxFileSamples]); + fifo_rate_comps_.reset(new double[kMaxFileSamples]); + num_elements_.reset(new int[kMaxNumSampleTypes]); + std::fill(num_elements_.get(), num_elements_.get() + kMaxNumSampleTypes, 0); + input_params_.reset(new int[kMaxNumParams]); + output_params_.reset(new int[kMaxNumParams]); +#endif + + DVLOG_IF(1, share_mode_ == AUDCLNT_SHAREMODE_EXCLUSIVE) + << "Core Audio (WASAPI) EXCLUSIVE MODE is enabled."; + + // Load the Avrt DLL if not already loaded. Required to support MMCSS. + bool avrt_init = avrt::Initialize(); + DCHECK(avrt_init) << "Failed to load the avrt.dll"; + + // All events are auto-reset events and non-signaled initially. + + // Create the event which the audio engine will signal each time a buffer + // has been recorded. + capture_event_.Set(CreateEvent(NULL, FALSE, FALSE, NULL)); + + // Create the event which will be set in Stop() when straeming shall stop. + stop_streaming_event_.Set(CreateEvent(NULL, FALSE, FALSE, NULL)); +} + +WASAPIUnifiedStream::~WASAPIUnifiedStream() { + VLOG(1) << "WASAPIUnifiedStream::~WASAPIUnifiedStream()"; +#ifndef NDEBUG + base::FilePath data_file_name; + PathService::Get(base::DIR_EXE, &data_file_name); + data_file_name = data_file_name.AppendASCII(kUnifiedAudioDebugFileName); + data_file_ = base::OpenFile(data_file_name, "wt"); + DVLOG(1) << ">> Output file " << data_file_name.value() << " is created."; + + size_t n = 0; + size_t elements_to_write = *std::min_element( + num_elements_.get(), num_elements_.get() + kMaxNumSampleTypes); + while (n < elements_to_write) { + fprintf(data_file_, "%I64d %d %d %10.9f\n", + input_time_stamps_[n], + num_frames_in_fifo_[n], + resampler_margin_[n], + fifo_rate_comps_[n]); + ++n; + } + base::CloseFile(data_file_); + + base::FilePath param_file_name; + PathService::Get(base::DIR_EXE, ¶m_file_name); + param_file_name = param_file_name.AppendASCII(kUnifiedAudioParamsFileName); + param_file_ = base::OpenFile(param_file_name, "wt"); + DVLOG(1) << ">> Output file " << param_file_name.value() << " is created."; + fprintf(param_file_, "%d %d\n", input_params_[0], input_params_[1]); + fprintf(param_file_, "%d %d\n", output_params_[0], output_params_[1]); + base::CloseFile(param_file_); +#endif +} + +bool WASAPIUnifiedStream::Open() { + TRACE_EVENT0("audio", "WASAPIUnifiedStream::Open"); + DVLOG(1) << "WASAPIUnifiedStream::Open()"; + DCHECK_EQ(GetCurrentThreadId(), creating_thread_id_); + if (opened_) + return true; + + AudioParameters hw_output_params; + HRESULT hr = CoreAudioUtil::GetPreferredAudioParameters( + eRender, eConsole, &hw_output_params); + if (FAILED(hr)) { + LOG(ERROR) << "Failed to get preferred output audio parameters."; + return false; + } + + AudioParameters hw_input_params; + if (input_device_id_ == AudioManagerBase::kDefaultDeviceId) { + // Query native parameters for the default capture device. + hr = CoreAudioUtil::GetPreferredAudioParameters( + eCapture, eConsole, &hw_input_params); + } else { + // Query native parameters for the capture device given by + // |input_device_id_|. + hr = CoreAudioUtil::GetPreferredAudioParameters( + input_device_id_, &hw_input_params); + } + if (FAILED(hr)) { + LOG(ERROR) << "Failed to get preferred input audio parameters."; + return false; + } + + // It is currently only possible to open up the output audio device using + // the native number of channels. + if (output_channels_ != hw_output_params.channels()) { + LOG(ERROR) << "Audio device does not support requested output channels."; + return false; + } + + // It is currently only possible to open up the input audio device using + // the native number of channels. If the client asks for a higher channel + // count, we will do channel upmixing in this class. The most typical + // example is that the client provides stereo but the hardware can only be + // opened in mono mode. We will do mono to stereo conversion in this case. + if (input_channels_ < hw_input_params.channels()) { + LOG(ERROR) << "Audio device does not support requested input channels."; + return false; + } else if (input_channels_ > hw_input_params.channels()) { + ChannelLayout input_layout = + GuessChannelLayout(hw_input_params.channels()); + ChannelLayout output_layout = GuessChannelLayout(input_channels_); + channel_mixer_.reset(new ChannelMixer(input_layout, output_layout)); + DVLOG(1) << "Remixing input channel layout from " << input_layout + << " to " << output_layout << "; from " + << hw_input_params.channels() << " channels to " + << input_channels_; + } + + if (hw_output_params.sample_rate() != params_.sample_rate()) { + LOG(ERROR) << "Requested sample-rate: " << params_.sample_rate() + << " must match the hardware sample-rate: " + << hw_output_params.sample_rate(); + return false; + } + + if (hw_output_params.frames_per_buffer() != params_.frames_per_buffer()) { + LOG(ERROR) << "Requested buffer size: " << params_.frames_per_buffer() + << " must match the hardware buffer size: " + << hw_output_params.frames_per_buffer(); + return false; + } + + // Set up WAVEFORMATPCMEX structures for input and output given the specified + // audio parameters. + SetIOFormats(hw_input_params, params_); + + // Create the input and output busses. + input_bus_ = AudioBus::Create( + hw_input_params.channels(), input_buffer_size_frames_); + output_bus_ = AudioBus::Create(params_); + + // One extra bus is needed for the input channel mixing case. + if (channel_mixer_) { + DCHECK_LT(hw_input_params.channels(), input_channels_); + // The size of the |channel_bus_| must be the same as the size of the + // output bus to ensure that the channel manager can deal with both + // resampled and non-resampled data as input. + channel_bus_ = AudioBus::Create( + input_channels_, params_.frames_per_buffer()); + } + + // Check if FIFO and resampling is required to match the input rate to the + // output rate. If so, a special thread loop, optimized for this case, will + // be used. This mode is also called varispeed mode. + // Note that we can also use this mode when input and output rates are the + // same but native buffer sizes differ (can happen if two different audio + // devices are used). For this case, the resampler uses a target ratio of + // 1.0 but SetRatio is called to compensate for clock-drift. The FIFO is + // required to compensate for the difference in buffer sizes. + // TODO(henrika): we could perhaps improve the performance for the second + // case here by only using the FIFO and avoid resampling. Not sure how much + // that would give and we risk not compensation for clock drift. + if (hw_input_params.sample_rate() != params_.sample_rate() || + hw_input_params.frames_per_buffer() != params_.frames_per_buffer()) { + DoVarispeedInitialization(hw_input_params, params_); + } + + // Render side (event driven only in varispeed mode): + + ScopedComPtr<IAudioClient> audio_output_client = + CoreAudioUtil::CreateDefaultClient(eRender, eConsole); + if (!audio_output_client) + return false; + + if (!CoreAudioUtil::IsFormatSupported(audio_output_client, + share_mode_, + &output_format_)) { + return false; + } + + if (share_mode_ == AUDCLNT_SHAREMODE_SHARED) { + // The |render_event_| will be NULL unless varispeed mode is utilized. + hr = CoreAudioUtil::SharedModeInitialize( + audio_output_client, &output_format_, render_event_.Get(), + &endpoint_render_buffer_size_frames_); + } else { + // TODO(henrika): add support for AUDCLNT_SHAREMODE_EXCLUSIVE. + } + if (FAILED(hr)) + return false; + + ScopedComPtr<IAudioRenderClient> audio_render_client = + CoreAudioUtil::CreateRenderClient(audio_output_client); + if (!audio_render_client) + return false; + + // Capture side (always event driven but format depends on varispeed or not): + + ScopedComPtr<IAudioClient> audio_input_client; + if (input_device_id_ == AudioManagerBase::kDefaultDeviceId) { + audio_input_client = CoreAudioUtil::CreateDefaultClient(eCapture, eConsole); + } else { + ScopedComPtr<IMMDevice> audio_input_device( + CoreAudioUtil::CreateDevice(input_device_id_)); + audio_input_client = CoreAudioUtil::CreateClient(audio_input_device); + } + if (!audio_input_client) + return false; + + if (!CoreAudioUtil::IsFormatSupported(audio_input_client, + share_mode_, + &input_format_)) { + return false; + } + + if (share_mode_ == AUDCLNT_SHAREMODE_SHARED) { + // Include valid event handle for event-driven initialization. + // The input side is always event driven independent of if varispeed is + // used or not. + hr = CoreAudioUtil::SharedModeInitialize( + audio_input_client, &input_format_, capture_event_.Get(), + &endpoint_capture_buffer_size_frames_); + } else { + // TODO(henrika): add support for AUDCLNT_SHAREMODE_EXCLUSIVE. + } + if (FAILED(hr)) + return false; + + ScopedComPtr<IAudioCaptureClient> audio_capture_client = + CoreAudioUtil::CreateCaptureClient(audio_input_client); + if (!audio_capture_client) + return false; + + // Varispeed mode requires additional preparations. + if (VarispeedMode()) + ResetVarispeed(); + + // Store all valid COM interfaces. + audio_output_client_ = audio_output_client; + audio_render_client_ = audio_render_client; + audio_input_client_ = audio_input_client; + audio_capture_client_ = audio_capture_client; + + opened_ = true; + return SUCCEEDED(hr); +} + +void WASAPIUnifiedStream::Start(AudioSourceCallback* callback) { + TRACE_EVENT0("audio", "WASAPIUnifiedStream::Start"); + DVLOG(1) << "WASAPIUnifiedStream::Start()"; + DCHECK_EQ(GetCurrentThreadId(), creating_thread_id_); + CHECK(callback); + CHECK(opened_); + + if (audio_io_thread_) { + CHECK_EQ(callback, source_); + return; + } + + source_ = callback; + + if (VarispeedMode()) { + ResetVarispeed(); + fifo_rate_compensation_ = 1.0; + average_delta_ = 0.0; + input_callback_received_ = false; + update_output_delay_ = false; + } + + // Create and start the thread that will listen for capture events. + // We will also listen on render events on the same thread if varispeed + // mode is utilized. + audio_io_thread_.reset( + new base::DelegateSimpleThread(this, "wasapi_io_thread")); + audio_io_thread_->Start(); + if (!audio_io_thread_->HasBeenStarted()) { + DLOG(ERROR) << "Failed to start WASAPI IO thread."; + return; + } + + // Start input streaming data between the endpoint buffer and the audio + // engine. + HRESULT hr = audio_input_client_->Start(); + if (FAILED(hr)) { + StopAndJoinThread(hr); + return; + } + + // Ensure that the endpoint buffer is prepared with silence. + if (share_mode_ == AUDCLNT_SHAREMODE_SHARED) { + if (!CoreAudioUtil::FillRenderEndpointBufferWithSilence( + audio_output_client_, audio_render_client_)) { + DLOG(WARNING) << "Failed to prepare endpoint buffers with silence."; + return; + } + } + num_written_frames_ = endpoint_render_buffer_size_frames_; + + // Start output streaming data between the endpoint buffer and the audio + // engine. + hr = audio_output_client_->Start(); + if (FAILED(hr)) { + StopAndJoinThread(hr); + return; + } +} + +void WASAPIUnifiedStream::Stop() { + TRACE_EVENT0("audio", "WASAPIUnifiedStream::Stop"); + DVLOG(1) << "WASAPIUnifiedStream::Stop()"; + DCHECK_EQ(GetCurrentThreadId(), creating_thread_id_); + if (!audio_io_thread_) + return; + + // Stop input audio streaming. + HRESULT hr = audio_input_client_->Stop(); + if (FAILED(hr)) { + DLOG_IF(ERROR, hr != AUDCLNT_E_NOT_INITIALIZED) + << "Failed to stop input streaming: " << std::hex << hr; + } + + // Stop output audio streaming. + hr = audio_output_client_->Stop(); + if (FAILED(hr)) { + DLOG_IF(ERROR, hr != AUDCLNT_E_NOT_INITIALIZED) + << "Failed to stop output streaming: " << std::hex << hr; + } + + // Wait until the thread completes and perform cleanup. + SetEvent(stop_streaming_event_.Get()); + audio_io_thread_->Join(); + audio_io_thread_.reset(); + + // Ensure that we don't quit the main thread loop immediately next + // time Start() is called. + ResetEvent(stop_streaming_event_.Get()); + + // Clear source callback, it'll be set again on the next Start() call. + source_ = NULL; + + // Flush all pending data and reset the audio clock stream position to 0. + hr = audio_output_client_->Reset(); + if (FAILED(hr)) { + DLOG_IF(ERROR, hr != AUDCLNT_E_NOT_INITIALIZED) + << "Failed to reset output streaming: " << std::hex << hr; + } + + audio_input_client_->Reset(); + if (FAILED(hr)) { + DLOG_IF(ERROR, hr != AUDCLNT_E_NOT_INITIALIZED) + << "Failed to reset input streaming: " << std::hex << hr; + } + + // Extra safety check to ensure that the buffers are cleared. + // If the buffers are not cleared correctly, the next call to Start() + // would fail with AUDCLNT_E_BUFFER_ERROR at IAudioRenderClient::GetBuffer(). + // TODO(henrika): this check is is only needed for shared-mode streams. + UINT32 num_queued_frames = 0; + audio_output_client_->GetCurrentPadding(&num_queued_frames); + DCHECK_EQ(0u, num_queued_frames); +} + +void WASAPIUnifiedStream::Close() { + TRACE_EVENT0("audio", "WASAPIUnifiedStream::Close"); + DVLOG(1) << "WASAPIUnifiedStream::Close()"; + DCHECK_EQ(GetCurrentThreadId(), creating_thread_id_); + + // It is valid to call Close() before calling open or Start(). + // It is also valid to call Close() after Start() has been called. + Stop(); + + // Inform the audio manager that we have been closed. This will cause our + // destruction. + manager_->ReleaseOutputStream(this); +} + +void WASAPIUnifiedStream::SetVolume(double volume) { + DVLOG(1) << "SetVolume(volume=" << volume << ")"; + if (volume < 0 || volume > 1) + return; + volume_ = volume; +} + +void WASAPIUnifiedStream::GetVolume(double* volume) { + DVLOG(1) << "GetVolume()"; + *volume = static_cast<double>(volume_); +} + + +void WASAPIUnifiedStream::ProvideInput(int frame_delay, AudioBus* audio_bus) { + // TODO(henrika): utilize frame_delay? + // A non-zero framed delay means multiple callbacks were necessary to + // fulfill the requested number of frames. + if (frame_delay > 0) + DVLOG(3) << "frame_delay: " << frame_delay; + +#ifndef NDEBUG + resampler_margin_[num_elements_[RESAMPLER_MARGIN]] = + fifo_->frames() - audio_bus->frames(); + num_elements_[RESAMPLER_MARGIN]++; +#endif + + if (fifo_->frames() < audio_bus->frames()) { + DVLOG(ERROR) << "Not enough data in the FIFO (" + << fifo_->frames() << " < " << audio_bus->frames() << ")"; + audio_bus->Zero(); + return; + } + + fifo_->Consume(audio_bus, 0, audio_bus->frames()); +} + +void WASAPIUnifiedStream::SetIOFormats(const AudioParameters& input_params, + const AudioParameters& output_params) { + for (int n = 0; n < 2; ++n) { + const AudioParameters& params = (n == 0) ? input_params : output_params; + WAVEFORMATPCMEX* xformat = (n == 0) ? &input_format_ : &output_format_; + WAVEFORMATEX* format = &xformat->Format; + + // Begin with the WAVEFORMATEX structure that specifies the basic format. + format->wFormatTag = WAVE_FORMAT_EXTENSIBLE; + format->nChannels = params.channels(); + format->nSamplesPerSec = params.sample_rate(); + format->wBitsPerSample = params.bits_per_sample(); + format->nBlockAlign = (format->wBitsPerSample / 8) * format->nChannels; + format->nAvgBytesPerSec = format->nSamplesPerSec * format->nBlockAlign; + format->cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX); + + // Add the parts which are unique to WAVE_FORMAT_EXTENSIBLE. + // Note that we always open up using the native channel layout. + (*xformat).Samples.wValidBitsPerSample = format->wBitsPerSample; + (*xformat).dwChannelMask = + CoreAudioUtil::GetChannelConfig( + std::string(), n == 0 ? eCapture : eRender); + (*xformat).SubFormat = KSDATAFORMAT_SUBTYPE_PCM; + } + + input_buffer_size_frames_ = input_params.frames_per_buffer(); + output_buffer_size_frames_ = output_params.frames_per_buffer(); + VLOG(1) << "#audio frames per input buffer : " << input_buffer_size_frames_; + VLOG(1) << "#audio frames per output buffer: " << output_buffer_size_frames_; + +#ifndef NDEBUG + input_params_[0] = input_format_.Format.nSamplesPerSec; + input_params_[1] = input_buffer_size_frames_; + output_params_[0] = output_format_.Format.nSamplesPerSec; + output_params_[1] = output_buffer_size_frames_; +#endif +} + +void WASAPIUnifiedStream::DoVarispeedInitialization( + const AudioParameters& input_params, const AudioParameters& output_params) { + DVLOG(1) << "WASAPIUnifiedStream::DoVarispeedInitialization()"; + + // A FIFO is required in this mode for input to output buffering. + // Note that it will add some latency. + fifo_.reset(new AudioFifo(input_params.channels(), kFifoSize)); + VLOG(1) << "Using FIFO of size " << fifo_->max_frames() + << " (#channels=" << input_params.channels() << ")"; + + // Create the multi channel resampler using the initial sample rate ratio. + // We will call MultiChannelResampler::SetRatio() during runtime to + // allow arbitrary combinations of input and output devices running off + // different clocks and using different drivers, with potentially + // differing sample-rates. Note that the requested block size is given by + // the native input buffer size |input_buffer_size_frames_|. + io_sample_rate_ratio_ = input_params.sample_rate() / + static_cast<double>(output_params.sample_rate()); + DVLOG(2) << "io_sample_rate_ratio: " << io_sample_rate_ratio_; + resampler_.reset(new MultiChannelResampler( + input_params.channels(), io_sample_rate_ratio_, input_buffer_size_frames_, + base::Bind(&WASAPIUnifiedStream::ProvideInput, base::Unretained(this)))); + VLOG(1) << "Resampling from " << input_params.sample_rate() << " to " + << output_params.sample_rate(); + + // The optimal number of frames we'd like to keep in the FIFO at all times. + // The actual size will vary but the goal is to ensure that the average size + // is given by this value. + target_fifo_frames_ = kTargetFifoSafetyFactor * input_buffer_size_frames_; + VLOG(1) << "Target FIFO size: " << target_fifo_frames_; + + // Create the event which the audio engine will signal each time it + // wants an audio buffer to render. + render_event_.Set(CreateEvent(NULL, FALSE, FALSE, NULL)); + + // Allocate memory for temporary audio bus used to store resampled input + // audio. + resampled_bus_ = AudioBus::Create( + input_params.channels(), output_buffer_size_frames_); + + // Buffer initial silence corresponding to target I/O buffering. + ResetVarispeed(); +} + +void WASAPIUnifiedStream::ResetVarispeed() { + DCHECK(VarispeedMode()); + + // Buffer initial silence corresponding to target I/O buffering. + fifo_->Clear(); + scoped_ptr<AudioBus> silence = + AudioBus::Create(input_format_.Format.nChannels, + target_fifo_frames_); + silence->Zero(); + fifo_->Push(silence.get()); + resampler_->Flush(); +} + +void WASAPIUnifiedStream::Run() { + ScopedCOMInitializer com_init(ScopedCOMInitializer::kMTA); + + // Increase the thread priority. + audio_io_thread_->SetThreadPriority(base::kThreadPriority_RealtimeAudio); + + // Enable MMCSS to ensure that this thread receives prioritized access to + // CPU resources. + // TODO(henrika): investigate if it is possible to include these additional + // settings in SetThreadPriority() as well. + DWORD task_index = 0; + HANDLE mm_task = avrt::AvSetMmThreadCharacteristics(L"Pro Audio", + &task_index); + bool mmcss_is_ok = + (mm_task && avrt::AvSetMmThreadPriority(mm_task, AVRT_PRIORITY_CRITICAL)); + if (!mmcss_is_ok) { + // Failed to enable MMCSS on this thread. It is not fatal but can lead + // to reduced QoS at high load. + DWORD err = GetLastError(); + LOG(WARNING) << "Failed to enable MMCSS (error code=" << err << ")."; + } + + // The IAudioClock interface enables us to monitor a stream's data + // rate and the current position in the stream. Allocate it before we + // start spinning. + ScopedComPtr<IAudioClock> audio_output_clock; + HRESULT hr = audio_output_client_->GetService( + __uuidof(IAudioClock), audio_output_clock.ReceiveVoid()); + LOG_IF(WARNING, FAILED(hr)) << "Failed to create IAudioClock: " + << std::hex << hr; + + bool streaming = true; + bool error = false; + + HANDLE wait_array[3]; + size_t num_handles = 0; + wait_array[num_handles++] = stop_streaming_event_; + wait_array[num_handles++] = capture_event_; + if (render_event_) { + // One extra event handle is needed in varispeed mode. + wait_array[num_handles++] = render_event_; + } + + // Keep streaming audio until stop event is signaled. + // Capture events are always used but render events are only active in + // varispeed mode. + while (streaming && !error) { + // Wait for a close-down event, or a new capture event. + DWORD wait_result = WaitForMultipleObjects(num_handles, + wait_array, + FALSE, + INFINITE); + switch (wait_result) { + case WAIT_OBJECT_0 + 0: + // |stop_streaming_event_| has been set. + streaming = false; + break; + case WAIT_OBJECT_0 + 1: + // |capture_event_| has been set + if (VarispeedMode()) { + ProcessInputAudio(); + } else { + ProcessInputAudio(); + ProcessOutputAudio(audio_output_clock); + } + break; + case WAIT_OBJECT_0 + 2: + DCHECK(VarispeedMode()); + // |render_event_| has been set + ProcessOutputAudio(audio_output_clock); + break; + default: + error = true; + break; + } + } + + if (streaming && error) { + // Stop audio streaming since something has gone wrong in our main thread + // loop. Note that, we are still in a "started" state, hence a Stop() call + // is required to join the thread properly. + audio_input_client_->Stop(); + audio_output_client_->Stop(); + PLOG(ERROR) << "WASAPI streaming failed."; + } + + // Disable MMCSS. + if (mm_task && !avrt::AvRevertMmThreadCharacteristics(mm_task)) { + PLOG(WARNING) << "Failed to disable MMCSS"; + } +} + +void WASAPIUnifiedStream::ProcessInputAudio() { + TRACE_EVENT0("audio", "WASAPIUnifiedStream::ProcessInputAudio"); + + BYTE* data_ptr = NULL; + UINT32 num_captured_frames = 0; + DWORD flags = 0; + UINT64 device_position = 0; + UINT64 capture_time_stamp = 0; + + const int bytes_per_sample = input_format_.Format.wBitsPerSample >> 3; + + base::TimeTicks now_tick = base::TimeTicks::HighResNow(); + +#ifndef NDEBUG + if (VarispeedMode()) { + input_time_stamps_[num_elements_[INPUT_TIME_STAMP]] = + now_tick.ToInternalValue(); + num_elements_[INPUT_TIME_STAMP]++; + } +#endif + + // Retrieve the amount of data in the capture endpoint buffer. + // |endpoint_capture_time_stamp| is the value of the performance + // counter at the time that the audio endpoint device recorded + // the device position of the first audio frame in the data packet. + HRESULT hr = audio_capture_client_->GetBuffer(&data_ptr, + &num_captured_frames, + &flags, + &device_position, + &capture_time_stamp); + if (FAILED(hr)) { + DLOG(ERROR) << "Failed to get data from the capture buffer"; + return; + } + + if (hr == AUDCLNT_S_BUFFER_EMPTY) { + // The return coded is a success code but a new packet is *not* available + // and none of the output parameters in the GetBuffer() call contains valid + // values. Best we can do is to deliver silence and avoid setting + // |input_callback_received_| since this only seems to happen for the + // initial event(s) on some devices. + input_bus_->Zero(); + } else { + // Valid data has been recorded and it is now OK to set the flag which + // informs the render side that capturing has started. + input_callback_received_ = true; + } + + if (num_captured_frames != 0) { + if (flags & AUDCLNT_BUFFERFLAGS_SILENT) { + // Clear out the capture buffer since silence is reported. + input_bus_->Zero(); + } else { + // Store captured data in an audio bus after de-interleaving + // the data to match the audio bus structure. + input_bus_->FromInterleaved( + data_ptr, num_captured_frames, bytes_per_sample); + } + } + + hr = audio_capture_client_->ReleaseBuffer(num_captured_frames); + DLOG_IF(ERROR, FAILED(hr)) << "Failed to release capture buffer"; + + // Buffer input into FIFO if varispeed mode is used. The render event + // will drive resampling of this data to match the output side. + if (VarispeedMode()) { + int available_frames = fifo_->max_frames() - fifo_->frames(); + if (input_bus_->frames() <= available_frames) { + fifo_->Push(input_bus_.get()); + } +#ifndef NDEBUG + num_frames_in_fifo_[num_elements_[NUM_FRAMES_IN_FIFO]] = + fifo_->frames(); + num_elements_[NUM_FRAMES_IN_FIFO]++; +#endif + } + + // Save resource by not asking for new delay estimates each time. + // These estimates are fairly stable and it is perfectly safe to only + // sample at a rate of ~1Hz. + // TODO(henrika): we might have to increase the update rate in varispeed + // mode since the delay variations are higher in this mode. + if ((now_tick - last_delay_sample_time_).InMilliseconds() > + kTimeDiffInMillisecondsBetweenDelayMeasurements && + input_callback_received_) { + // Calculate the estimated capture delay, i.e., the latency between + // the recording time and the time we when we are notified about + // the recorded data. Note that the capture time stamp is given in + // 100-nanosecond (0.1 microseconds) units. + base::TimeDelta diff = + now_tick - base::TimeTicks::FromInternalValue(0.1 * capture_time_stamp); + capture_delay_ms_ = diff.InMillisecondsF(); + + last_delay_sample_time_ = now_tick; + update_output_delay_ = true; + } +} + +void WASAPIUnifiedStream::ProcessOutputAudio(IAudioClock* audio_output_clock) { + TRACE_EVENT0("audio", "WASAPIUnifiedStream::ProcessOutputAudio"); + + if (!input_callback_received_) { + if (share_mode_ == AUDCLNT_SHAREMODE_SHARED) { + if (!CoreAudioUtil::FillRenderEndpointBufferWithSilence( + audio_output_client_, audio_render_client_)) + DLOG(WARNING) << "Failed to prepare endpoint buffers with silence."; + } + return; + } + + // Rate adjusted resampling is required in varispeed mode. It means that + // recorded audio samples will be read from the FIFO, resampled to match the + // output sample-rate and then stored in |resampled_bus_|. + if (VarispeedMode()) { + // Calculate a varispeed rate scalar factor to compensate for drift between + // input and output. We use the actual number of frames still in the FIFO + // compared with the ideal value of |target_fifo_frames_|. + int delta = fifo_->frames() - target_fifo_frames_; + + // Average |delta| because it can jitter back/forth quite frequently + // by +/- the hardware buffer-size *if* the input and output callbacks are + // happening at almost exactly the same time. Also, if the input and output + // sample-rates are different then |delta| will jitter quite a bit due to + // the rate conversion happening in the varispeed, plus the jittering of + // the callbacks. The average value is what's important here. + // We use an exponential smoothing filter to reduce the variations. + average_delta_ += kAlpha * (delta - average_delta_); + + // Compute a rate compensation which always attracts us back to the + // |target_fifo_frames_| over a period of kCorrectionTimeSeconds. + double correction_time_frames = + kCorrectionTimeSeconds * output_format_.Format.nSamplesPerSec; + fifo_rate_compensation_ = + (correction_time_frames + average_delta_) / correction_time_frames; + +#ifndef NDEBUG + fifo_rate_comps_[num_elements_[RATE_COMPENSATION]] = + fifo_rate_compensation_; + num_elements_[RATE_COMPENSATION]++; +#endif + + // Adjust for FIFO drift. + const double new_ratio = io_sample_rate_ratio_ * fifo_rate_compensation_; + resampler_->SetRatio(new_ratio); + // Get resampled input audio from FIFO where the size is given by the + // output side. + resampler_->Resample(resampled_bus_->frames(), resampled_bus_.get()); + } + + // Derive a new total delay estimate if the capture side has set the + // |update_output_delay_| flag. + if (update_output_delay_) { + // Calculate the estimated render delay, i.e., the time difference + // between the time when data is added to the endpoint buffer and + // when the data is played out on the actual speaker. + const double stream_pos = CurrentStreamPosInMilliseconds( + num_written_frames_ + output_buffer_size_frames_, + output_format_.Format.nSamplesPerSec); + const double speaker_pos = + SpeakerStreamPosInMilliseconds(audio_output_clock); + const double render_delay_ms = stream_pos - speaker_pos; + const double fifo_delay_ms = VarispeedMode() ? + FrameCountToMilliseconds(target_fifo_frames_, input_format_) : 0; + + // Derive the total delay, i.e., the sum of the input and output + // delays. Also convert the value into byte units. An extra FIFO delay + // is added for varispeed usage cases. + total_delay_ms_ = VarispeedMode() ? + capture_delay_ms_ + render_delay_ms + fifo_delay_ms : + capture_delay_ms_ + render_delay_ms; + DVLOG(2) << "total_delay_ms : " << total_delay_ms_; + DVLOG(3) << " capture_delay_ms: " << capture_delay_ms_; + DVLOG(3) << " render_delay_ms : " << render_delay_ms; + DVLOG(3) << " fifo_delay_ms : " << fifo_delay_ms; + total_delay_bytes_ = MillisecondsToBytes(total_delay_ms_, output_format_); + + // Wait for new signal from the capture side. + update_output_delay_ = false; + } + + // Select source depending on if varispeed is utilized or not. + // Also, the source might be the output of a channel mixer if channel mixing + // is required to match the native input channels to the number of input + // channels used by the client (given by |input_channels_| in this case). + AudioBus* input_bus = VarispeedMode() ? + resampled_bus_.get() : input_bus_.get(); + if (channel_mixer_) { + DCHECK_EQ(input_bus->frames(), channel_bus_->frames()); + // Most common case is 1->2 channel upmixing. + channel_mixer_->Transform(input_bus, channel_bus_.get()); + // Use the output from the channel mixer as new input bus. + input_bus = channel_bus_.get(); + } + + // Prepare for rendering by calling OnMoreIOData(). + int frames_filled = source_->OnMoreIOData( + input_bus, + output_bus_.get(), + AudioBuffersState(0, total_delay_bytes_)); + DCHECK_EQ(frames_filled, output_bus_->frames()); + + // Keep track of number of rendered frames since we need it for + // our delay calculations. + num_written_frames_ += frames_filled; + + // Derive the the amount of available space in the endpoint buffer. + // Avoid render attempt if there is no room for a captured packet. + UINT32 num_queued_frames = 0; + audio_output_client_->GetCurrentPadding(&num_queued_frames); + if (endpoint_render_buffer_size_frames_ - num_queued_frames < + output_buffer_size_frames_) + return; + + // Grab all available space in the rendering endpoint buffer + // into which the client can write a data packet. + uint8* audio_data = NULL; + HRESULT hr = audio_render_client_->GetBuffer(output_buffer_size_frames_, + &audio_data); + if (FAILED(hr)) { + DLOG(ERROR) << "Failed to access render buffer"; + return; + } + + const int bytes_per_sample = output_format_.Format.wBitsPerSample >> 3; + + // Convert the audio bus content to interleaved integer data using + // |audio_data| as destination. + output_bus_->Scale(volume_); + output_bus_->ToInterleaved( + output_buffer_size_frames_, bytes_per_sample, audio_data); + + // Release the buffer space acquired in the GetBuffer() call. + audio_render_client_->ReleaseBuffer(output_buffer_size_frames_, 0); + DLOG_IF(ERROR, FAILED(hr)) << "Failed to release render buffer"; + + return; +} + +void WASAPIUnifiedStream::HandleError(HRESULT err) { + CHECK((started() && GetCurrentThreadId() == audio_io_thread_->tid()) || + (!started() && GetCurrentThreadId() == creating_thread_id_)); + NOTREACHED() << "Error code: " << std::hex << err; + if (source_) + source_->OnError(this); +} + +void WASAPIUnifiedStream::StopAndJoinThread(HRESULT err) { + CHECK(GetCurrentThreadId() == creating_thread_id_); + DCHECK(audio_io_thread_.get()); + SetEvent(stop_streaming_event_.Get()); + audio_io_thread_->Join(); + audio_io_thread_.reset(); + HandleError(err); +} + +} // namespace media |