// 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 "base/logging.h" #include "base/time/time.h" #include "media/audio/audio_manager_base.h" #include "media/audio/audio_parameters.h" namespace media { namespace pulse { namespace { 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; } } } // namespace // static, pa_stream_success_cb_t void StreamSuccessCallback(pa_stream* s, int error, void* mainloop) { pa_threaded_mainloop* pa_mainloop = static_cast<pa_threaded_mainloop*>(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<pa_threaded_mainloop*>(mainloop); pa_threaded_mainloop_signal(pa_mainloop, 0); } pa_sample_format_t BitsToPASampleFormat(int bits_per_sample) { switch (bits_per_sample) { case 8: return PA_SAMPLE_U8; case 16: return PA_SAMPLE_S16LE; case 24: return PA_SAMPLE_S24LE; case 32: return PA_SAMPLE_S32LE; default: NOTREACHED() << "Invalid bits per sample: " << bits_per_sample; return PA_SAMPLE_INVALID; } } pa_channel_map ChannelLayoutToPAChannelMap(ChannelLayout channel_layout) { pa_channel_map channel_map; pa_channel_map_init(&channel_map); channel_map.channels = ChannelLayoutToChannelCount(channel_layout); for (Channels ch = LEFT; ch <= CHANNELS_MAX; ch = static_cast<Channels>(ch + 1)) { int channel_index = ChannelOrder(channel_layout, ch); if (channel_index < 0) continue; channel_map.map[channel_index] = ChromiumToPAChannelPosition(ch); } return channel_map; } void WaitForOperationCompletion(pa_threaded_mainloop* pa_mainloop, pa_operation* operation) { if (!operation) { DLOG(WARNING) << "Operation is NULL"; return; } while (pa_operation_get_state(operation) == PA_OPERATION_RUNNING) pa_threaded_mainloop_wait(pa_mainloop); pa_operation_unref(operation); } int GetHardwareLatencyInBytes(pa_stream* stream, int sample_rate, int bytes_per_frame) { DCHECK(stream); int negative = 0; pa_usec_t latency_micros = 0; if (pa_stream_get_latency(stream, &latency_micros, &negative) != 0) return 0; if (negative) return 0; return latency_micros * sample_rate * bytes_per_frame / base::Time::kMicrosecondsPerSecond; } // 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; sample_specifications.format = BitsToPASampleFormat( params.bits_per_sample()); 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 : NULL; // Create a new recording stream. *stream = pa_stream_new(context, "RecordStream", &sample_specifications, map); 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(); buffer_attributes.maxlength = static_cast<uint32_t>(-1); buffer_attributes.tlength = buffer_size; buffer_attributes.minreq = buffer_size; buffer_attributes.prebuf = static_cast<uint32_t>(-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 == AudioManagerBase::kDefaultDeviceId ? NULL : device_id.c_str(), &buffer_attributes, static_cast<pa_stream_flags_t>(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, 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, "Chromium"); 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, NULL, PA_CONTEXT_NOAUTOSPAWN, NULL) == 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 = BitsToPASampleFormat( params.bits_per_sample()); sample_specifications.rate = params.sample_rate(); sample_specifications.channels = params.channels(); // Get channel mapping and open playback stream. pa_channel_map* map = NULL; 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 (NULL). map = &source_channel_map; } *stream = pa_stream_new(*context, "Playback", &sample_specifications, map); 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. pa_buffer_attr pa_buffer_attributes; pa_buffer_attributes.maxlength = static_cast<uint32_t>(-1); pa_buffer_attributes.minreq = params.GetBytesPerBuffer() / 2; pa_buffer_attributes.prebuf = static_cast<uint32_t>(-1); pa_buffer_attributes.tlength = params.GetBytesPerBuffer() * 3; pa_buffer_attributes.fragsize = static_cast<uint32_t>(-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 == AudioManagerBase::kDefaultDeviceId ? NULL : device_id.c_str(), &pa_buffer_attributes, static_cast<pa_stream_flags_t>( PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_ADJUST_LATENCY | PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_NOT_MONOTONIC | PA_STREAM_START_CORKED), NULL, NULL) == 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; } #undef RETURN_ON_FAILURE } // namespace pulse } // namespace media