// 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/pulse/pulse_util.h" #include #include #include #include "base/files/file_path.h" #include "base/logging.h" #include "base/macros.h" #include "base/memory/ptr_util.h" #include "base/notreached.h" #include "base/synchronization/waitable_event.h" #include "build/branding_buildflags.h" #include "media/audio/audio_device_description.h" #include "media/base/audio_timestamp_helper.h" #if defined(DLOPEN_PULSEAUDIO) #include "media/audio/pulse/pulse_stubs.h" using media_audio_pulse::kModulePulse; using media_audio_pulse::InitializeStubs; using media_audio_pulse::StubPathMap; #endif // defined(DLOPEN_PULSEAUDIO) namespace media { namespace pulse { namespace { #if BUILDFLAG(GOOGLE_CHROME_BRANDING) constexpr char kBrowserDisplayName[] = "google-chrome"; #define PRODUCT_STRING "Google Chrome" #else constexpr char kBrowserDisplayName[] = "chromium-browser"; #define PRODUCT_STRING "Chromium" #endif #if defined(DLOPEN_PULSEAUDIO) static const base::FilePath::CharType kPulseLib[] = FILE_PATH_LITERAL("libpulse.so.0"); #endif void DestroyMainloop(pa_threaded_mainloop* mainloop) { pa_threaded_mainloop_stop(mainloop); pa_threaded_mainloop_free(mainloop); } void DestroyContext(pa_context* context) { pa_context_set_state_callback(context, nullptr, nullptr); pa_context_disconnect(context); pa_context_unref(context); } pa_channel_position ChromiumToPAChannelPosition(Channels channel) { switch (channel) { // PulseAudio does not differentiate between left/right and // stereo-left/stereo-right, both translate to front-left/front-right. case LEFT: return PA_CHANNEL_POSITION_FRONT_LEFT; case RIGHT: return PA_CHANNEL_POSITION_FRONT_RIGHT; case CENTER: return PA_CHANNEL_POSITION_FRONT_CENTER; case LFE: return PA_CHANNEL_POSITION_LFE; case BACK_LEFT: return PA_CHANNEL_POSITION_REAR_LEFT; case BACK_RIGHT: return PA_CHANNEL_POSITION_REAR_RIGHT; case LEFT_OF_CENTER: return PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER; case RIGHT_OF_CENTER: return PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER; case BACK_CENTER: return PA_CHANNEL_POSITION_REAR_CENTER; case SIDE_LEFT: return PA_CHANNEL_POSITION_SIDE_LEFT; case SIDE_RIGHT: return PA_CHANNEL_POSITION_SIDE_RIGHT; default: NOTREACHED() << "Invalid channel: " << channel; return PA_CHANNEL_POSITION_INVALID; } } class ScopedPropertyList { public: ScopedPropertyList() : property_list_(pa_proplist_new()) {} ~ScopedPropertyList() { pa_proplist_free(property_list_); } pa_proplist* get() const { return property_list_; } private: pa_proplist* property_list_; DISALLOW_COPY_AND_ASSIGN(ScopedPropertyList); }; struct InputBusData { InputBusData(pa_threaded_mainloop* loop, const std::string& name) : loop_(loop), name_(name), bus_() {} pa_threaded_mainloop* const loop_; const std::string& name_; std::string bus_; }; struct OutputBusData { OutputBusData(pa_threaded_mainloop* loop, const std::string& bus) : loop_(loop), name_(), bus_(bus) {} pa_threaded_mainloop* const loop_; std::string name_; const std::string& bus_; }; void InputBusCallback(pa_context* context, const pa_source_info* info, int error, void* user_data) { InputBusData* data = static_cast(user_data); if (error) { // We have checked all the devices now. pa_threaded_mainloop_signal(data->loop_, 0); return; } if (strcmp(info->name, data->name_.c_str()) == 0 && pa_proplist_contains(info->proplist, PA_PROP_DEVICE_BUS)) { data->bus_ = pa_proplist_gets(info->proplist, PA_PROP_DEVICE_BUS); } } void OutputBusCallback(pa_context* context, const pa_sink_info* info, int error, void* user_data) { OutputBusData* data = static_cast(user_data); if (error) { // We have checked all the devices now. pa_threaded_mainloop_signal(data->loop_, 0); return; } if (pa_proplist_contains(info->proplist, PA_PROP_DEVICE_BUS) && strcmp(pa_proplist_gets(info->proplist, PA_PROP_DEVICE_BUS), data->bus_.c_str()) == 0) { data->name_ = info->name; } } struct DefaultDevicesData { explicit DefaultDevicesData(pa_threaded_mainloop* loop) : loop_(loop) {} std::string input_; std::string output_; pa_threaded_mainloop* const loop_; }; void GetDefaultDeviceIdCallback(pa_context* c, const pa_server_info* info, void* userdata) { DefaultDevicesData* data = static_cast(userdata); if (info->default_source_name) data->input_ = info->default_source_name; if (info->default_sink_name) data->output_ = info->default_sink_name; pa_threaded_mainloop_signal(data->loop_, 0); } struct ContextStartupData { base::WaitableEvent* context_wait; pa_threaded_mainloop* pa_mainloop; }; void SignalReadyOrErrorStateCallback(pa_context* context, void* context_data) { auto context_state = pa_context_get_state(context); auto* data = static_cast(context_data); if (!PA_CONTEXT_IS_GOOD(context_state) || context_state == PA_CONTEXT_READY) data->context_wait->Signal(); pa_threaded_mainloop_signal(data->pa_mainloop, 0); } } // namespace bool InitPulse(pa_threaded_mainloop** mainloop, pa_context** context) { #if defined(DLOPEN_PULSEAUDIO) StubPathMap paths; // Check if the pulse library is available. paths[kModulePulse].push_back(kPulseLib); if (!InitializeStubs(paths)) { VLOG(1) << "Failed on loading the Pulse library and symbols"; return false; } #endif // defined(DLOPEN_PULSEAUDIO) // The setup order below follows the pattern used by pa_simple_new(): // https://github.com/pulseaudio/pulseaudio/blob/master/src/pulse/simple.c // Create a mainloop API and connect to the default server. // The mainloop is the internal asynchronous API event loop. pa_threaded_mainloop* pa_mainloop = pa_threaded_mainloop_new(); if (!pa_mainloop) return false; pa_mainloop_api* pa_mainloop_api = pa_threaded_mainloop_get_api(pa_mainloop); pa_context* pa_context = pa_context_new(pa_mainloop_api, PRODUCT_STRING " input"); if (!pa_context) { pa_threaded_mainloop_free(pa_mainloop); return false; } // We can't rely on pa_threaded_mainloop_wait() for PulseAudio startup since // it can hang indefinitely. Instead we use a WaitableEvent to time out the // startup process if it takes too long. base::WaitableEvent context_wait; ContextStartupData data = {&context_wait, pa_mainloop}; pa_context_set_state_callback(pa_context, &SignalReadyOrErrorStateCallback, &data); if (pa_context_connect(pa_context, nullptr, PA_CONTEXT_NOAUTOSPAWN, nullptr)) { VLOG(1) << "Failed to connect to the context. Error: " << pa_strerror(pa_context_errno(pa_context)); DestroyContext(pa_context); pa_threaded_mainloop_free(pa_mainloop); return false; } // Lock the event loop object, effectively blocking the event loop thread // from processing events. This is necessary. auto mainloop_lock = std::make_unique(pa_mainloop); // Start the threaded mainloop after everything has been configured. if (pa_threaded_mainloop_start(pa_mainloop)) { DestroyContext(pa_context); mainloop_lock.reset(); DestroyMainloop(pa_mainloop); return false; } // Don't hold the mainloop lock while waiting for the context to become ready, // or we'll never complete since PulseAudio can't continue working. mainloop_lock.reset(); // Wait for up to 5 seconds for pa_context to become ready. We'll be signaled // by the SignalReadyOrErrorStateCallback that we setup above. // // We've chosen a timeout value of 5 seconds because this can be executed at // browser startup (other times it's during audio process startup). In the // normal case, this should only take ~50ms, but we've seen some test bots // hang indefinitely when the pulse daemon can't be started. constexpr base::TimeDelta kStartupTimeout = base::TimeDelta::FromSeconds(5); const bool was_signaled = context_wait.TimedWait(kStartupTimeout); // Require the mainloop lock before checking the context state. mainloop_lock = std::make_unique(pa_mainloop); auto context_state = pa_context_get_state(pa_context); if (context_state != PA_CONTEXT_READY) { if (!was_signaled) VLOG(1) << "Timed out trying to connect to PulseAudio."; else VLOG(1) << "Failed to connect to PulseAudio: " << context_state; DestroyContext(pa_context); mainloop_lock.reset(); DestroyMainloop(pa_mainloop); return false; } // Replace our function local state callback with a global appropriate one. pa_context_set_state_callback(pa_context, &pulse::ContextStateCallback, pa_mainloop); *mainloop = pa_mainloop; *context = pa_context; return true; } void DestroyPulse(pa_threaded_mainloop* mainloop, pa_context* context) { DCHECK(mainloop); DCHECK(context); { AutoPulseLock auto_lock(mainloop); DestroyContext(context); } DestroyMainloop(mainloop); } // static, pa_stream_success_cb_t void StreamSuccessCallback(pa_stream* s, int error, void* mainloop) { pa_threaded_mainloop* pa_mainloop = static_cast(mainloop); pa_threaded_mainloop_signal(pa_mainloop, 0); } // |pa_context| and |pa_stream| state changed cb. void ContextStateCallback(pa_context* context, void* mainloop) { pa_threaded_mainloop* pa_mainloop = static_cast(mainloop); pa_threaded_mainloop_signal(pa_mainloop, 0); } pa_channel_map ChannelLayoutToPAChannelMap(ChannelLayout channel_layout) { pa_channel_map channel_map; if (channel_layout == CHANNEL_LAYOUT_MONO) { // CHANNEL_LAYOUT_MONO only specifies audio on the C channel, but we // want PulseAudio to play single-channel audio on more than just that. pa_channel_map_init_mono(&channel_map); } else { pa_channel_map_init(&channel_map); channel_map.channels = ChannelLayoutToChannelCount(channel_layout); for (Channels ch = LEFT; ch <= CHANNELS_MAX; ch = static_cast(ch + 1)) { int channel_index = ChannelOrder(channel_layout, ch); if (channel_index < 0) continue; channel_map.map[channel_index] = ChromiumToPAChannelPosition(ch); } } return channel_map; } bool WaitForOperationCompletion(pa_threaded_mainloop* mainloop, pa_operation* operation, pa_context* optional_context, pa_stream* optional_stream) { if (!operation) { LOG(ERROR) << "pa_operation is nullptr."; return false; } while (pa_operation_get_state(operation) == PA_OPERATION_RUNNING) { if (optional_context) { pa_context_state_t context_state = pa_context_get_state(optional_context); if (!PA_CONTEXT_IS_GOOD(context_state)) { LOG(ERROR) << "pa_context went bad while waiting: state=" << context_state << ", error=" << pa_strerror(pa_context_errno(optional_context)); pa_operation_cancel(operation); pa_operation_unref(operation); return false; } } if (optional_stream) { pa_stream_state_t stream_state = pa_stream_get_state(optional_stream); if (!PA_STREAM_IS_GOOD(stream_state)) { LOG(ERROR) << "pa_stream went bad while waiting: " << stream_state; pa_operation_cancel(operation); pa_operation_unref(operation); return false; } } pa_threaded_mainloop_wait(mainloop); } pa_operation_unref(operation); return true; } base::TimeDelta GetHardwareLatency(pa_stream* stream) { DCHECK(stream); int negative = 0; pa_usec_t latency_micros = 0; if (pa_stream_get_latency(stream, &latency_micros, &negative) != 0) return base::TimeDelta(); if (negative) return base::TimeDelta(); return base::TimeDelta::FromMicroseconds(latency_micros); } // Helper macro for CreateInput/OutputStream() to avoid code spam and // string bloat. #define RETURN_ON_FAILURE(expression, message) do { \ if (!(expression)) { \ DLOG(ERROR) << message; \ return false; \ } \ } while (0) bool CreateInputStream(pa_threaded_mainloop* mainloop, pa_context* context, pa_stream** stream, const AudioParameters& params, const std::string& device_id, pa_stream_notify_cb_t stream_callback, void* user_data) { DCHECK(mainloop); DCHECK(context); // Set sample specifications. pa_sample_spec sample_specifications; // FIXME: This should be PA_SAMPLE_FLOAT32, but there is more work needed in // PulseAudioInputStream to support this. static_assert(kInputSampleFormat == kSampleFormatS16, "Only 16-bit input supported."); sample_specifications.format = PA_SAMPLE_S16LE; sample_specifications.rate = params.sample_rate(); sample_specifications.channels = params.channels(); // Get channel mapping and open recording stream. pa_channel_map source_channel_map = ChannelLayoutToPAChannelMap( params.channel_layout()); pa_channel_map* map = (source_channel_map.channels != 0) ? &source_channel_map : nullptr; // Create a new recording stream and // tells PulseAudio what the stream icon should be. ScopedPropertyList property_list; pa_proplist_sets(property_list.get(), PA_PROP_APPLICATION_ICON_NAME, kBrowserDisplayName); *stream = pa_stream_new_with_proplist(context, "RecordStream", &sample_specifications, map, property_list.get()); RETURN_ON_FAILURE(*stream, "failed to create PA recording stream"); pa_stream_set_state_callback(*stream, stream_callback, user_data); // Set server-side capture buffer metrics. Detailed documentation on what // values should be chosen can be found at // freedesktop.org/software/pulseaudio/doxygen/structpa__buffer__attr.html. pa_buffer_attr buffer_attributes; const unsigned int buffer_size = params.GetBytesPerBuffer(kInputSampleFormat); buffer_attributes.maxlength = static_cast(-1); buffer_attributes.tlength = buffer_size; buffer_attributes.minreq = buffer_size; buffer_attributes.prebuf = static_cast(-1); buffer_attributes.fragsize = buffer_size; int flags = PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_ADJUST_LATENCY | PA_STREAM_START_CORKED; RETURN_ON_FAILURE( pa_stream_connect_record( *stream, device_id == AudioDeviceDescription::kDefaultDeviceId ? nullptr : device_id.c_str(), &buffer_attributes, static_cast(flags)) == 0, "pa_stream_connect_record FAILED "); // Wait for the stream to be ready. while (true) { pa_stream_state_t stream_state = pa_stream_get_state(*stream); RETURN_ON_FAILURE( PA_STREAM_IS_GOOD(stream_state), "Invalid PulseAudio stream state"); if (stream_state == PA_STREAM_READY) break; pa_threaded_mainloop_wait(mainloop); } return true; } bool CreateOutputStream(pa_threaded_mainloop** mainloop, pa_context** context, pa_stream** stream, const AudioParameters& params, const std::string& device_id, const std::string& app_name, pa_stream_notify_cb_t stream_callback, pa_stream_request_cb_t write_callback, void* user_data) { DCHECK(!*mainloop); DCHECK(!*context); *mainloop = pa_threaded_mainloop_new(); RETURN_ON_FAILURE(*mainloop, "Failed to create PulseAudio main loop."); pa_mainloop_api* pa_mainloop_api = pa_threaded_mainloop_get_api(*mainloop); *context = pa_context_new( pa_mainloop_api, app_name.empty() ? PRODUCT_STRING : app_name.c_str()); RETURN_ON_FAILURE(*context, "Failed to create PulseAudio context."); // A state callback must be set before calling pa_threaded_mainloop_lock() or // pa_threaded_mainloop_wait() calls may lead to dead lock. pa_context_set_state_callback(*context, &ContextStateCallback, *mainloop); // Lock the main loop while setting up the context. Failure to do so may lead // to crashes as the PulseAudio thread tries to run before things are ready. AutoPulseLock auto_lock(*mainloop); RETURN_ON_FAILURE(pa_threaded_mainloop_start(*mainloop) == 0, "Failed to start PulseAudio main loop."); RETURN_ON_FAILURE(pa_context_connect(*context, nullptr, PA_CONTEXT_NOAUTOSPAWN, nullptr) == 0, "Failed to connect PulseAudio context."); // Wait until |pa_context_| is ready. pa_threaded_mainloop_wait() must be // called after pa_context_get_state() in case the context is already ready, // otherwise pa_threaded_mainloop_wait() will hang indefinitely. while (true) { pa_context_state_t context_state = pa_context_get_state(*context); RETURN_ON_FAILURE(PA_CONTEXT_IS_GOOD(context_state), "Invalid PulseAudio context state."); if (context_state == PA_CONTEXT_READY) break; pa_threaded_mainloop_wait(*mainloop); } // Set sample specifications. pa_sample_spec sample_specifications; sample_specifications.format = PA_SAMPLE_FLOAT32; sample_specifications.rate = params.sample_rate(); sample_specifications.channels = params.channels(); // Get channel mapping. pa_channel_map* map = nullptr; pa_channel_map source_channel_map = ChannelLayoutToPAChannelMap( params.channel_layout()); if (source_channel_map.channels != 0) { // The source data uses a supported channel map so we will use it rather // than the default channel map (nullptr). map = &source_channel_map; } // Open playback stream and // tell PulseAudio what the stream icon should be. ScopedPropertyList property_list; pa_proplist_sets(property_list.get(), PA_PROP_APPLICATION_ICON_NAME, kBrowserDisplayName); *stream = pa_stream_new_with_proplist( *context, "Playback", &sample_specifications, map, property_list.get()); RETURN_ON_FAILURE(*stream, "failed to create PA playback stream"); pa_stream_set_state_callback(*stream, stream_callback, user_data); // Even though we start the stream corked above, PulseAudio will issue one // stream request after setup. write_callback() must fulfill the write. pa_stream_set_write_callback(*stream, write_callback, user_data); // Pulse is very finicky with the small buffer sizes used by Chrome. The // settings below are mostly found through trial and error. Essentially we // want Pulse to auto size its internal buffers, but call us back nearly every // |minreq| bytes. |tlength| should be a multiple of |minreq|; too low and // Pulse will issue callbacks way too fast, too high and we don't get // callbacks frequently enough. // // Setting |minreq| to the exact buffer size leads to more callbacks than // necessary, so we've clipped it to half the buffer size. Regardless of the // requested amount, we'll always fill |params.GetBytesPerBuffer()| though. size_t buffer_size = params.GetBytesPerBuffer(kSampleFormatF32); pa_buffer_attr pa_buffer_attributes; pa_buffer_attributes.maxlength = static_cast(-1); pa_buffer_attributes.minreq = buffer_size / 2; pa_buffer_attributes.prebuf = static_cast(-1); pa_buffer_attributes.tlength = buffer_size * 3; pa_buffer_attributes.fragsize = static_cast(-1); // Connect playback stream. Like pa_buffer_attr, the pa_stream_flags have a // huge impact on the performance of the stream and were chosen through trial // and error. RETURN_ON_FAILURE( pa_stream_connect_playback( *stream, device_id == AudioDeviceDescription::kDefaultDeviceId ? nullptr : device_id.c_str(), &pa_buffer_attributes, static_cast( PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_ADJUST_LATENCY | PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_NOT_MONOTONIC | PA_STREAM_START_CORKED), nullptr, nullptr) == 0, "pa_stream_connect_playback FAILED "); // Wait for the stream to be ready. while (true) { pa_stream_state_t stream_state = pa_stream_get_state(*stream); RETURN_ON_FAILURE( PA_STREAM_IS_GOOD(stream_state), "Invalid PulseAudio stream state"); if (stream_state == PA_STREAM_READY) break; pa_threaded_mainloop_wait(*mainloop); } return true; } std::string GetBusOfInput(pa_threaded_mainloop* mainloop, pa_context* context, const std::string& name) { DCHECK(mainloop); DCHECK(context); AutoPulseLock auto_lock(mainloop); InputBusData data(mainloop, name); pa_operation* operation = pa_context_get_source_info_list(context, InputBusCallback, &data); WaitForOperationCompletion(mainloop, operation, context); return data.bus_; } std::string GetOutputCorrespondingTo(pa_threaded_mainloop* mainloop, pa_context* context, const std::string& bus) { DCHECK(mainloop); DCHECK(context); AutoPulseLock auto_lock(mainloop); OutputBusData data(mainloop, bus); pa_operation* operation = pa_context_get_sink_info_list(context, OutputBusCallback, &data); WaitForOperationCompletion(mainloop, operation, context); return data.name_; } std::string GetRealDefaultDeviceId(pa_threaded_mainloop* mainloop, pa_context* context, RequestType type) { DCHECK(mainloop); DCHECK(context); AutoPulseLock auto_lock(mainloop); DefaultDevicesData data(mainloop); pa_operation* operation = pa_context_get_server_info(context, &GetDefaultDeviceIdCallback, &data); WaitForOperationCompletion(mainloop, operation, context); return (type == RequestType::INPUT) ? data.input_ : data.output_; } #undef RETURN_ON_FAILURE } // namespace pulse } // namespace media