diff options
author | ajwong@chromium.org <ajwong@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-10-16 18:41:56 +0000 |
---|---|---|
committer | ajwong@chromium.org <ajwong@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-10-16 18:41:56 +0000 |
commit | 713b99db0f33455c019968d6ff890e9830ebd4f0 (patch) | |
tree | eefc378f9c6a469a25ad1d1653fac72e906bb6a1 /media/audio | |
parent | 4576ba04a833ea439f6dd034eeced33b24c898e9 (diff) | |
download | chromium_src-713b99db0f33455c019968d6ff890e9830ebd4f0.zip chromium_src-713b99db0f33455c019968d6ff890e9830ebd4f0.tar.gz chromium_src-713b99db0f33455c019968d6ff890e9830ebd4f0.tar.bz2 |
Move Alsa device opening into the audio thread, and add in support for multi-channel audio.
Moving the device opening into the audio thread will prevent browser hangs for badly behaving alsa implementations (like pulseaudio) that can hang snd_pcm_open if pulseaudiod is wedged, even if SND_PCM_NONBLOCK is requested.
For multi-channel audio, device enumeration has been added to try and find a multi-channel device with a stable channel mapping. According to http://0pointer.de/blog/projects/guide-to-sound-apis.html, default should only be used with mono and stereo stream because the channel ordering is not defined by Alsa. To get a well-defined channel ordering, one must use one of the surround40, surround51, etc., device names. However, these device names do not always allow multiple opens, so a fallback scheme is implemented to use default if necessary.
BUG=20945,17703
TEST=listened with built-in soundcard and USB soundcard with various other audio programs running.
Review URL: http://codereview.chromium.org/275022
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@29299 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'media/audio')
-rw-r--r-- | media/audio/linux/alsa_output.cc | 388 | ||||
-rw-r--r-- | media/audio/linux/alsa_output.h | 40 | ||||
-rw-r--r-- | media/audio/linux/alsa_output_unittest.cc | 310 | ||||
-rw-r--r-- | media/audio/linux/alsa_wrapper.cc | 12 | ||||
-rw-r--r-- | media/audio/linux/alsa_wrapper.h | 4 | ||||
-rw-r--r-- | media/audio/linux/audio_manager_linux.cc | 18 | ||||
-rw-r--r-- | media/audio/mac/audio_output_mac.cc | 3 |
7 files changed, 656 insertions, 119 deletions
diff --git a/media/audio/linux/alsa_output.cc b/media/audio/linux/alsa_output.cc index 9f3498e..53dc14b 100644 --- a/media/audio/linux/alsa_output.cc +++ b/media/audio/linux/alsa_output.cc @@ -102,13 +102,20 @@ static const int kPcmRecoverIsSilent = 0; #endif const char AlsaPcmOutputStream::kDefaultDevice[] = "default"; +const char AlsaPcmOutputStream::kAutoSelectDevice[] = ""; +const char AlsaPcmOutputStream::kPlugPrefix[] = "plug:"; + +// Since we expect to only be able to wake up with a resolution of +// kSleepErrorMilliseconds, double that for our minimum required latency. +const int AlsaPcmOutputStream::kMinLatencyMicros = + kSleepErrorMilliseconds * 2 * 1000; namespace { snd_pcm_format_t BitsToFormat(char bits_per_sample) { switch (bits_per_sample) { case 8: - return SND_PCM_FORMAT_S8; + return SND_PCM_FORMAT_U8; case 16: return SND_PCM_FORMAT_S16; @@ -124,8 +131,79 @@ snd_pcm_format_t BitsToFormat(char bits_per_sample) { } } +// While the "default" device may support multi-channel audio, in Alsa, only +// the device names surround40, surround41, surround50, etc, have a defined +// channel mapping according to Lennart: +// +// http://0pointer.de/blog/projects/guide-to-sound-apis.html +// +// This function makes a best guess at the specific > 2 channel device name +// based on the number of channels requested. NULL is returned if no device +// can be found to match the channel numbers. In this case, using +// kDefaultDevice is probably the best bet. +// +// A five channel source is assumed to be surround50 instead of surround41 +// (which is also 5 channels). +// +// TODO(ajwong): The source data should have enough info to tell us if we want +// surround41 versus surround51, etc., instead of needing us to guess base don +// channel number. Fix API to pass that data down. +const char* GuessSpecificDeviceName(int channels) { + switch (channels) { + case 8: + return "surround71"; + + case 7: + return "surround70"; + + case 6: + return "surround51"; + + case 5: + return "surround50"; + + case 4: + return "surround40"; + + default: + return NULL; + } +} + +// Reorder PCM from AAC layout to Alsa layout. +// TODO(fbarchard): Switch layout when ffmpeg is updated. +template<class Format> +static void Swizzle50Layout(Format* b, size_t filled) { + static const int kNumSurroundChannels = 5; + Format aac[kNumSurroundChannels]; + for (size_t i = 0; i < filled; i += sizeof(aac), b += kNumSurroundChannels) { + memcpy(aac, b, sizeof(aac)); + b[0] = aac[1]; // L + b[1] = aac[2]; // R + b[2] = aac[3]; // Ls + b[3] = aac[4]; // Rs + b[4] = aac[0]; // C + } +} + +template<class Format> +static void Swizzle51Layout(Format* b, size_t filled) { + static const int kNumSurroundChannels = 6; + Format aac[kNumSurroundChannels]; + for (size_t i = 0; i < filled; i += sizeof(aac), b += kNumSurroundChannels) { + memcpy(aac, b, sizeof(aac)); + b[0] = aac[1]; // L + b[1] = aac[2]; // R + b[2] = aac[3]; // Ls + b[3] = aac[4]; // Rs + b[4] = aac[0]; // C + b[5] = aac[5]; // LFE + } +} + } // namespace +// Not in an anonymous so that it can be a friend to AlsaPcmOutputStream. std::ostream& operator<<(std::ostream& os, AlsaPcmOutputStream::InternalState state) { switch (state) { @@ -160,12 +238,16 @@ AlsaPcmOutputStream::AlsaPcmOutputStream(const std::string& device_name, AudioManagerLinux* manager, MessageLoop* message_loop) : shared_data_(MessageLoop::current()), - device_name_(device_name), + requested_device_name_(device_name), pcm_format_(BitsToFormat(bits_per_sample)), channels_(channels), sample_rate_(sample_rate), bytes_per_sample_(bits_per_sample / 8), bytes_per_frame_(channels_ * bits_per_sample / 8), + should_downmix_(false), + latency_micros_(0), + micros_per_packet_(0), + bytes_per_output_frame_(bytes_per_frame_), stop_stream_(false), wrapper_(wrapper), manager_(manager), @@ -175,16 +257,6 @@ AlsaPcmOutputStream::AlsaPcmOutputStream(const std::string& device_name, message_loop_(message_loop) { // Sanity check input values. - // - // TODO(scherkus): ALSA works fine if you pass in multichannel audio, however - // it seems to be mapped to the wrong channels. We may have to do some - // channel swizzling from decoder output to ALSA's preferred multichannel - // format. - if (channels_ != 1 && channels_ != 2) { - LOG(WARNING) << "Only 1 and 2 channel audio is supported right now."; - shared_data_.TransitionTo(kInError); - } - if (AudioManager::AUDIO_PCM_LINEAR != format) { LOG(WARNING) << "Only linear PCM supported."; shared_data_.TransitionTo(kInError); @@ -222,36 +294,6 @@ bool AlsaPcmOutputStream::Open(size_t packet_size) { return false; } - // Try to open the device. - snd_pcm_t* handle = NULL; - int error = wrapper_->PcmOpen(&handle, device_name_.c_str(), - SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK); - if (error < 0) { - LOG(ERROR) << "Cannot open audio device (" << device_name_ << "): " - << wrapper_->StrError(error); - return false; - } - - // Configure the device for software resampling, and add enough buffer for - // two audio packets. - int micros_per_packet = - FramesToMicros(packet_size / bytes_per_frame_, sample_rate_); - if ((error = wrapper_->PcmSetParams(handle, - pcm_format_, - SND_PCM_ACCESS_RW_INTERLEAVED, - channels_, - sample_rate_, - 1, // soft_resample -- let ALSA resample - micros_per_packet * 2)) < 0) { - LOG(ERROR) << "Unable to set PCM parameters for (" << device_name_ - << "): " << wrapper_->StrError(error); - if (!CloseDevice(handle)) { - // TODO(ajwong): Retry on certain errors? - LOG(WARNING) << "Unable to close audio device. Leaking handle."; - } - return false; - } - // We do not need to check if the transition was successful because // CanTransitionTo() was checked above, and it is assumed that this // object's public API is only called on one thread so the state cannot @@ -259,8 +301,7 @@ bool AlsaPcmOutputStream::Open(size_t packet_size) { shared_data_.TransitionTo(kIsOpened); message_loop_->PostTask( FROM_HERE, - NewRunnableMethod(this, &AlsaPcmOutputStream::FinishOpen, - handle, packet_size)); + NewRunnableMethod(this, &AlsaPcmOutputStream::OpenTask, packet_size)); return true; } @@ -321,18 +362,45 @@ void AlsaPcmOutputStream::GetVolume(double* left_level, double* right_level) { *left_level = *right_level = shared_data_.volume(); } -void AlsaPcmOutputStream::FinishOpen(snd_pcm_t* playback_handle, - size_t packet_size) { +void AlsaPcmOutputStream::OpenTask(size_t packet_size) { DCHECK_EQ(MessageLoop::current(), message_loop_); - playback_handle_ = playback_handle; - packet_.reset(new Packet(packet_size)); + // Initialize the configuration variables. frames_per_packet_ = packet_size / bytes_per_frame_; + + // Try to open the device. + micros_per_packet_ = + FramesToMicros(packet_size / bytes_per_frame_, sample_rate_); + latency_micros_ = std::max(AlsaPcmOutputStream::kMinLatencyMicros, + micros_per_packet_ * 2); + if (requested_device_name_ == kAutoSelectDevice) { + playback_handle_ = AutoSelectDevice(latency_micros_); + if (playback_handle_) { + LOG(INFO) << "Auto-selected device: " << device_name_; + } + } else { + device_name_ = requested_device_name_; + playback_handle_ = OpenDevice(device_name_, channels_, latency_micros_); + } + + // Finish initializing the stream if the device was opened successfully. + if (playback_handle_ == NULL) { + stop_stream_ = true; + } else { + packet_.reset(new Packet(packet_size)); + if (should_downmix_) { + bytes_per_output_frame_ = 2 * bytes_per_sample_; + } + } } void AlsaPcmOutputStream::StartTask() { DCHECK_EQ(MessageLoop::current(), message_loop_); + if (stop_stream_) { + return; + } + // When starting again, drop all packets in the device and prepare it again // incase we are restarting from a pause state and need to flush old data. int error = wrapper_->PcmDrop(playback_handle_); @@ -353,14 +421,15 @@ void AlsaPcmOutputStream::StartTask() { return; } - // Do a best-effort write of 2 packets to pre-roll. + // Do a best-effort pre-roll to fill the buffer. Use integer rounding to find + // the maximum number of full packets that can fit into the buffer. // - // TODO(ajwong): Make this track with the us_latency set in Open(). - // Also handle EAGAIN. - BufferPacket(packet_.get()); - WritePacket(packet_.get()); - BufferPacket(packet_.get()); - WritePacket(packet_.get()); + // TODO(ajwong): Handle EAGAIN. + const int num_preroll = latency_micros_ / micros_per_packet_; + for (int i = 0; i < num_preroll; ++i) { + BufferPacket(packet_.get()); + WritePacket(packet_.get()); + } ScheduleNextWrite(packet_.get()); } @@ -410,7 +479,7 @@ void AlsaPcmOutputStream::BufferPacket(Packet* packet) { // the playback and report an error. delay = 0; } else { - delay *= bytes_per_frame_; + delay *= bytes_per_output_frame_; } packet->used = 0; @@ -425,18 +494,62 @@ void AlsaPcmOutputStream::BufferPacket(Packet* packet) { DCHECK(packet->size % bytes_per_frame_ == 0); packet->size = (packet->size / bytes_per_frame_) * bytes_per_frame_; - media::AdjustVolume(packet->buffer.get(), - packet->size, - channels_, - bytes_per_sample_, - shared_data_.volume()); + if (should_downmix_) { + if (media::FoldChannels(packet->buffer.get(), + packet->size, + channels_, + bytes_per_sample_, + shared_data_.volume())) { + // Adjust packet size for downmix. + packet->size = + packet->size / bytes_per_frame_ * bytes_per_output_frame_; + } else { + LOG(ERROR) << "Folding failed"; + } + } else { + // TODO(ajwong): Handle other channel orderings. + + // Handle channel order for 5.0 audio. + if (channels_ == 5) { + if (bytes_per_sample_ == 1) { + Swizzle50Layout(reinterpret_cast<uint8*>(packet->buffer.get()), + packet->size); + } else if (bytes_per_sample_ == 2) { + Swizzle50Layout(reinterpret_cast<int16*>(packet->buffer.get()), + packet->size); + } else if (bytes_per_sample_ == 4) { + Swizzle50Layout(reinterpret_cast<int32*>(packet->buffer.get()), + packet->size); + } + } + + // Handle channel order for 5.1 audio. + if (channels_ == 6) { + if (bytes_per_sample_ == 1) { + Swizzle51Layout(reinterpret_cast<uint8*>(packet->buffer.get()), + packet->size); + } else if (bytes_per_sample_ == 2) { + Swizzle51Layout(reinterpret_cast<int16*>(packet->buffer.get()), + packet->size); + } else if (bytes_per_sample_ == 4) { + Swizzle51Layout(reinterpret_cast<int32*>(packet->buffer.get()), + packet->size); + } + } + + media::AdjustVolume(packet->buffer.get(), + packet->size, + channels_, + bytes_per_sample_, + shared_data_.volume()); + } } } void AlsaPcmOutputStream::WritePacket(Packet* packet) { DCHECK_EQ(MessageLoop::current(), message_loop_); - CHECK(packet->size % bytes_per_frame_ == 0); + CHECK(packet->size % bytes_per_output_frame_ == 0); // If the device is in error, just eat the bytes. if (stop_stream_) { @@ -446,7 +559,7 @@ void AlsaPcmOutputStream::WritePacket(Packet* packet) { if (packet->used < packet->size) { char* buffer_pos = packet->buffer.get() + packet->used; - snd_pcm_sframes_t frames = FramesInPacket(*packet, bytes_per_frame_); + snd_pcm_sframes_t frames = FramesInPacket(*packet, bytes_per_output_frame_); DCHECK_GT(frames, 0); @@ -472,7 +585,7 @@ void AlsaPcmOutputStream::WritePacket(Packet* packet) { stop_stream_ = true; } } else { - packet->used += frames_written * bytes_per_frame_; + packet->used += frames_written * bytes_per_output_frame_; } } } @@ -498,10 +611,12 @@ void AlsaPcmOutputStream::ScheduleNextWrite(Packet* current_packet) { } // Calculate when we should have enough buffer for another packet of data. - int frames_leftover = FramesInPacket(*current_packet, bytes_per_frame_); - int frames_needed = - frames_leftover > 0 ? frames_leftover : frames_per_packet_; - int frames_until_empty_enough = frames_needed - GetAvailableFrames(); + // Make sure to take into consideration down-mixing. + int frames_leftover = + FramesInPacket(*current_packet, bytes_per_output_frame_); + int frames_avail_wanted = + (frames_leftover > 0) ? frames_leftover : frames_per_packet_; + int frames_until_empty_enough = frames_avail_wanted - GetAvailableFrames(); int next_fill_time_ms = FramesToMillis(frames_until_empty_enough, sample_rate_); @@ -545,6 +660,89 @@ int64 AlsaPcmOutputStream::FramesToMillis(int frames, int sample_rate) { return frames * base::Time::kMillisecondsPerSecond / sample_rate; } +std::string AlsaPcmOutputStream::FindDeviceForChannels(int channels) { + // Constants specified by the ALSA API for device hints. + static const int kGetAllDevices = -1; + static const char kPcmInterfaceName[] = "pcm"; + static const char kIoHintName[] = "IOID"; + static const char kNameHintName[] = "NAME"; + + const char* wanted_device = GuessSpecificDeviceName(channels); + if (!wanted_device) { + return ""; + } + + std::string guessed_device; + void** hints = NULL; + int error = wrapper_->DeviceNameHint(kGetAllDevices, + kPcmInterfaceName, + &hints); + if (error == 0) { + // NOTE: Do not early return from inside this if statement. The + // hints above need to be freed. + for (void** hint_iter = hints; *hint_iter != NULL; hint_iter++) { + // Only examine devices that are output capable.. Valid values are + // "Input", "Output", and NULL which means both input and output. + scoped_ptr_malloc<char> io( + wrapper_->DeviceNameGetHint(*hint_iter, kIoHintName)); + if (io != NULL && strcmp(io.get(), "Input") == 0) + continue; + + // Attempt to select the closest device for number of channels. + scoped_ptr_malloc<char> name( + wrapper_->DeviceNameGetHint(*hint_iter, kNameHintName)); + if (strncmp(wanted_device, name.get(), strlen(wanted_device)) == 0) { + guessed_device = name.get(); + break; + } + } + + // Destory the hint now that we're done with it. + wrapper_->DeviceNameFreeHint(hints); + hints = NULL; + } else { + LOG(ERROR) << "Unable to get hints for devices: " + << wrapper_->StrError(error); + } + + return guessed_device; +} + +snd_pcm_t* AlsaPcmOutputStream::OpenDevice(const std::string& device_name, + int channels, + unsigned int latency) { + snd_pcm_t* handle = NULL; + int error = wrapper_->PcmOpen(&handle, device_name.c_str(), + SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK); + if (error < 0) { + LOG(ERROR) << "Cannot open audio device (" << device_name << "): " + << wrapper_->StrError(error); + return NULL; + } + + // Configure the device for software resampling. + if ((error = wrapper_->PcmSetParams(handle, + pcm_format_, + SND_PCM_ACCESS_RW_INTERLEAVED, + channels, + sample_rate_, + 1, // soft_resample -- let ALSA resample + latency)) < 0) { + LOG(ERROR) << "Unable to set PCM parameters for (" << device_name + << "): " << wrapper_->StrError(error) + << " -- Format: " << pcm_format_ + << " Channels: " << channels + << " Latency (us): " << latency; + if (!CloseDevice(handle)) { + // TODO(ajwong): Retry on certain errors? + LOG(WARNING) << "Unable to close audio device. Leaking handle."; + } + return NULL; + } + + return handle; +} + bool AlsaPcmOutputStream::CloseDevice(snd_pcm_t* handle) { int error = wrapper_->PcmClose(handle); if (error < 0) { @@ -580,6 +778,60 @@ snd_pcm_sframes_t AlsaPcmOutputStream::GetAvailableFrames() { return available_frames; } +snd_pcm_t* AlsaPcmOutputStream::AutoSelectDevice(unsigned int latency) { + // For auto-selection: + // 1) Attempt to open a device that best matches the number of channels + // requested. + // 2) If that fails, attempt the "plug:" version of it incase ALSA can + // remap do some software conversion to make it work. + // 3) Fallback to kDefaultDevice. + // 4) If that fails too, try the "plug:" version of kDefaultDevice. + // 5) Give up. + snd_pcm_t* handle = NULL; + device_name_ = FindDeviceForChannels(channels_); + + // Step 1. + if (!device_name_.empty()) { + if ((handle = OpenDevice(device_name_, channels_, latency)) != NULL) { + return handle; + } + + // Step 2. + device_name_ = kPlugPrefix + device_name_; + if ((handle = OpenDevice(device_name_, channels_, latency)) != NULL) { + return handle; + } + } + + // For the kDefaultDevice device, we can only reliably depend on 2-channel + // output to have the correct ordering according to Lennart. For the channel + // formats that we know how to downmix from (5 channel to 6 channel), setup + // downmixing. + // + // TODO(ajwong): We need a SupportsFolding() function. + int default_channels = channels_; + if (default_channels >= 5 && default_channels <= 6) { + should_downmix_ = true; + default_channels = 2; + } + + // Step 3. + device_name_ = kDefaultDevice; + if ((handle = OpenDevice(device_name_, default_channels, latency)) != NULL) { + return handle; + } + + // Step 4. + device_name_ = kPlugPrefix + device_name_; + if ((handle = OpenDevice(device_name_, default_channels, latency)) != NULL) { + return handle; + } + + // Unable to open any device. + device_name_.clear(); + return NULL; +} + AudioManagerLinux* AlsaPcmOutputStream::manager() { DCHECK_EQ(MessageLoop::current(), client_thread_loop_); return manager_; diff --git a/media/audio/linux/alsa_output.h b/media/audio/linux/alsa_output.h index 409b158..5bdaa1b 100644 --- a/media/audio/linux/alsa_output.h +++ b/media/audio/linux/alsa_output.h @@ -46,18 +46,27 @@ class AlsaPcmOutputStream : public AudioOutputStream, public base::RefCountedThreadSafe<AlsaPcmOutputStream> { public: - // Set to "default" which should avoid locking the sound device and allow - // ALSA to multiplex sound from different processes that want to write PCM - // data. + // String for the generic "default" ALSA device that has the highest + // compatibility and chance of working. static const char kDefaultDevice[]; + // Pass this to the AlsaPcmOutputStream if you want to attempt auto-selection + // of the audio device. + static const char kAutoSelectDevice[]; + + // Prefix for device names to enable ALSA library resampling. + static const char kPlugPrefix[]; + + // The minimum latency that is accepted by the device. + static const int kMinLatencyMicros; + // Create a PCM Output stream for the ALSA device identified by // |device_name|. The AlsaPcmOutputStream uses |wrapper| to communicate with // the alsa libraries, allowing for dependency injection during testing. All // requesting of data, and writing to the alsa device will be done on // |message_loop|. // - // If unsure of what to use for |device_name|, use |kDefaultDevice|. + // If unsure of what to use for |device_name|, use |kAutoSelectDevice|. AlsaPcmOutputStream(const std::string& device_name, AudioManager::Format format, int channels, @@ -78,10 +87,14 @@ class AlsaPcmOutputStream : private: friend class AlsaPcmOutputStreamTest; + FRIEND_TEST(AlsaPcmOutputStreamTest, AutoSelectDevice_DeviceSelect); + FRIEND_TEST(AlsaPcmOutputStreamTest, AutoSelectDevice_FallbackDevices); + FRIEND_TEST(AlsaPcmOutputStreamTest, AutoSelectDevice_HintFail); FRIEND_TEST(AlsaPcmOutputStreamTest, BufferPacket); FRIEND_TEST(AlsaPcmOutputStreamTest, BufferPacket_StopStream); FRIEND_TEST(AlsaPcmOutputStreamTest, BufferPacket_UnfinishedPacket); FRIEND_TEST(AlsaPcmOutputStreamTest, ConstructedState); + FRIEND_TEST(AlsaPcmOutputStreamTest, LatencyFloor); FRIEND_TEST(AlsaPcmOutputStreamTest, OpenClose); FRIEND_TEST(AlsaPcmOutputStreamTest, PcmOpenFailed); FRIEND_TEST(AlsaPcmOutputStreamTest, PcmSetParamsFailed); @@ -121,7 +134,7 @@ class AlsaPcmOutputStream : friend std::ostream& ::operator<<(std::ostream& os, InternalState); // Various tasks that complete actions started in the public API. - void FinishOpen(snd_pcm_t* playback_handle, size_t packet_size); + void OpenTask(size_t packet_size); void StartTask(); void CloseTask(); @@ -137,9 +150,17 @@ class AlsaPcmOutputStream : int bytes_per_frame); static int64 FramesToMicros(int frames, int sample_rate); static int64 FramesToMillis(int frames, int sample_rate); + std::string FindDeviceForChannels(int channels); + snd_pcm_t* OpenDevice(const std::string& device_name, + int channels, + unsigned int latency); bool CloseDevice(snd_pcm_t* handle); snd_pcm_sframes_t GetAvailableFrames(); + // Attempts to find the best matching linux audio device for the given number + // of channels. This function will set |device_name_| and |should_downmix_|. + snd_pcm_t* AutoSelectDevice(unsigned int latency); + // Thread-asserting accessors for member variables. AudioManagerLinux* manager(); @@ -192,13 +213,20 @@ class AlsaPcmOutputStream : // Configuration constants from the constructor. Referenceable by all threads // since they are constants. - const std::string device_name_; + const std::string requested_device_name_; const snd_pcm_format_t pcm_format_; const int channels_; const int sample_rate_; const int bytes_per_sample_; const int bytes_per_frame_; + // Device configuration data. Populated after OpenTask() completes. + std::string device_name_; + bool should_downmix_; + int latency_micros_; + int micros_per_packet_; + int bytes_per_output_frame_; + // Flag indicating the code should stop reading from the data source or // writing to the ALSA device. This is set because the device has entered // an unrecoverable error state, or the ClosedTask() has executed. diff --git a/media/audio/linux/alsa_output_unittest.cc b/media/audio/linux/alsa_output_unittest.cc index e20526e..72c4c90 100644 --- a/media/audio/linux/alsa_output_unittest.cc +++ b/media/audio/linux/alsa_output_unittest.cc @@ -3,6 +3,7 @@ // found in the LICENSE file. #include "base/logging.h" +#include "base/string_util.h" #include "media/audio/linux/alsa_output.h" #include "media/audio/linux/alsa_wrapper.h" #include "media/audio/linux/audio_manager_linux.h" @@ -12,13 +13,24 @@ using testing::_; using testing::DoAll; using testing::Eq; +using testing::InSequence; +using testing::Invoke; +using testing::Mock; +using testing::MockFunction; using testing::Return; using testing::SetArgumentPointee; using testing::StrictMock; using testing::StrEq; +using testing::Unused; class MockAlsaWrapper : public AlsaWrapper { public: + MOCK_METHOD3(DeviceNameHint, int(int card, + const char* iface, + void*** hints)); + MOCK_METHOD2(DeviceNameGetHint, char*(const void* hint, const char* id)); + MOCK_METHOD1(DeviceNameFreeHint, int(void** hints)); + MOCK_METHOD4(PcmOpen, int(snd_pcm_t** handle, const char* name, snd_pcm_stream_t stream, int mode)); MOCK_METHOD1(PcmClose, int(snd_pcm_t* handle)); @@ -64,15 +76,7 @@ class AlsaPcmOutputStreamTest : public testing::Test { protected: AlsaPcmOutputStreamTest() : packet_(kTestPacketSize + 1) { - test_stream_ = new AlsaPcmOutputStream(kTestDeviceName, - kTestFormat, - kTestChannels, - kTestSampleRate, - kTestBitsPerSample, - &mock_alsa_wrapper_, - &mock_manager_, - &message_loop_); - + test_stream_ = CreateStreamWithChannels(kTestChannels); packet_.size = kTestPacketSize; } @@ -80,6 +84,27 @@ class AlsaPcmOutputStreamTest : public testing::Test { test_stream_ = NULL; } + AlsaPcmOutputStream* CreateStreamWithChannels(int channels) { + return new AlsaPcmOutputStream(kTestDeviceName, + kTestFormat, + channels, + kTestSampleRate, + kTestBitsPerSample, + &mock_alsa_wrapper_, + &mock_manager_, + &message_loop_); + } + + // Helper function to malloc the string returned by DeviceNameHint for NAME. + static char* EchoHint(const void* name, Unused) { + return strdup(static_cast<const char*>(name)); + } + + // Helper function to malloc the string returned by DeviceNameHint for IOID. + static char* OutputHint(Unused, Unused) { + return strdup("Output"); + } + static const int kTestChannels; static const int kTestSampleRate; static const int kTestBitsPerSample; @@ -92,6 +117,15 @@ class AlsaPcmOutputStreamTest : public testing::Test { static const int kTestFailedErrno; static snd_pcm_t* const kFakeHandle; + // Used to simulate DeviceNameHint. + static char kSurround40[]; + static char kSurround41[]; + static char kSurround50[]; + static char kSurround51[]; + static char kSurround70[]; + static char kSurround71[]; + static void* kFakeHints[]; + StrictMock<MockAlsaWrapper> mock_alsa_wrapper_; StrictMock<MockAudioManagerLinux> mock_manager_; MessageLoop message_loop_; @@ -113,7 +147,7 @@ const AudioManager::Format AlsaPcmOutputStreamTest::kTestFormat = AudioManager::AUDIO_PCM_LINEAR; const char AlsaPcmOutputStreamTest::kTestDeviceName[] = "TestDevice"; const char AlsaPcmOutputStreamTest::kDummyMessage[] = "dummy"; -const int AlsaPcmOutputStreamTest::kTestFramesPerPacket = 100; +const int AlsaPcmOutputStreamTest::kTestFramesPerPacket = 1000; const size_t AlsaPcmOutputStreamTest::kTestPacketSize = AlsaPcmOutputStreamTest::kTestFramesPerPacket * AlsaPcmOutputStreamTest::kTestBytesPerFrame; @@ -121,32 +155,28 @@ const int AlsaPcmOutputStreamTest::kTestFailedErrno = -EACCES; snd_pcm_t* const AlsaPcmOutputStreamTest::kFakeHandle = reinterpret_cast<snd_pcm_t*>(1); +char AlsaPcmOutputStreamTest::kSurround40[] = "surround40:CARD=foo,DEV=0"; +char AlsaPcmOutputStreamTest::kSurround41[] = "surround41:CARD=foo,DEV=0"; +char AlsaPcmOutputStreamTest::kSurround50[] = "surround50:CARD=foo,DEV=0"; +char AlsaPcmOutputStreamTest::kSurround51[] = "surround51:CARD=foo,DEV=0"; +char AlsaPcmOutputStreamTest::kSurround70[] = "surround70:CARD=foo,DEV=0"; +char AlsaPcmOutputStreamTest::kSurround71[] = "surround71:CARD=foo,DEV=0"; +void* AlsaPcmOutputStreamTest::kFakeHints[] = { + kSurround40, kSurround41, kSurround50, kSurround51, + kSurround70, kSurround71, NULL }; + TEST_F(AlsaPcmOutputStreamTest, ConstructedState) { EXPECT_EQ(AlsaPcmOutputStream::kCreated, test_stream_->shared_data_.state()); // Should support mono. - test_stream_ = new AlsaPcmOutputStream(kTestDeviceName, - kTestFormat, - 1, // Channels. - kTestSampleRate, - kTestBitsPerSample, - &mock_alsa_wrapper_, - &mock_manager_, - &message_loop_); + test_stream_ = CreateStreamWithChannels(1); EXPECT_EQ(AlsaPcmOutputStream::kCreated, test_stream_->shared_data_.state()); - // Should not support multi-channel. - test_stream_ = new AlsaPcmOutputStream(kTestDeviceName, - kTestFormat, - 3, // Channels. - kTestSampleRate, - kTestBitsPerSample, - &mock_alsa_wrapper_, - &mock_manager_, - &message_loop_); - EXPECT_EQ(AlsaPcmOutputStream::kInError, + // Should support multi-channel. + test_stream_ = CreateStreamWithChannels(3); + EXPECT_EQ(AlsaPcmOutputStream::kCreated, test_stream_->shared_data_.state()); // Bad bits per sample. @@ -174,6 +204,68 @@ TEST_F(AlsaPcmOutputStreamTest, ConstructedState) { test_stream_->shared_data_.state()); } +TEST_F(AlsaPcmOutputStreamTest, LatencyFloor) { + const double kMicrosPerFrame = + static_cast<double>(1000000) / kTestSampleRate; + const double kPacketFramesInMinLatency = + AlsaPcmOutputStream::kMinLatencyMicros / kMicrosPerFrame / 2.0; + const int kMinLatencyPacketSize = + static_cast<int>(kPacketFramesInMinLatency * kTestBytesPerFrame); + + // Test that packets which would cause a latency under less than + // AlsaPcmOutputStream::kMinLatencyMicros will get clipped to + // AlsaPcmOutputStream::kMinLatencyMicros, + EXPECT_CALL(mock_alsa_wrapper_, PcmOpen(_, _, _, _)) + .WillOnce(DoAll(SetArgumentPointee<0>(kFakeHandle), + Return(0))); + EXPECT_CALL(mock_alsa_wrapper_, + PcmSetParams(_, _, _, _, _, _, + AlsaPcmOutputStream::kMinLatencyMicros)) + .WillOnce(Return(0)); + + ASSERT_TRUE(test_stream_->Open(kMinLatencyPacketSize)); + message_loop_.RunAllPending(); + + // Now close it and test that everything was released. + EXPECT_CALL(mock_alsa_wrapper_, PcmClose(kFakeHandle)) .WillOnce(Return(0)); + EXPECT_CALL(mock_manager_, ReleaseStream(test_stream_.get())); + test_stream_->Close(); + message_loop_.RunAllPending(); + + Mock::VerifyAndClear(&mock_alsa_wrapper_); + Mock::VerifyAndClear(&mock_manager_); + + // Test that having more packets ends up with a latency based on packet size. + const int kOverMinLatencyPacketSize = + (kPacketFramesInMinLatency + 1) * kTestBytesPerFrame; + int64 expected_micros = 2 * + AlsaPcmOutputStream::FramesToMicros( + kOverMinLatencyPacketSize / kTestBytesPerFrame, + kTestSampleRate); + + // Recreate the stream to reset the state. + test_stream_ = CreateStreamWithChannels(kTestChannels); + + EXPECT_CALL(mock_alsa_wrapper_, PcmOpen(_, _, _, _)) + .WillOnce(DoAll(SetArgumentPointee<0>(kFakeHandle), Return(0))); + EXPECT_CALL(mock_alsa_wrapper_, + PcmSetParams(_, _, _, _, _, _, expected_micros)) + .WillOnce(Return(0)); + + ASSERT_TRUE(test_stream_->Open(kOverMinLatencyPacketSize)); + message_loop_.RunAllPending(); + + // Now close it and test that everything was released. + EXPECT_CALL(mock_alsa_wrapper_, PcmClose(kFakeHandle)) + .WillOnce(Return(0)); + EXPECT_CALL(mock_manager_, ReleaseStream(test_stream_.get())); + test_stream_->Close(); + message_loop_.RunAllPending(); + + Mock::VerifyAndClear(&mock_alsa_wrapper_); + Mock::VerifyAndClear(&mock_manager_); +} + TEST_F(AlsaPcmOutputStreamTest, OpenClose) { int64 expected_micros = 2 * AlsaPcmOutputStream::FramesToMicros(kTestPacketSize / kTestBytesPerFrame, @@ -189,7 +281,7 @@ TEST_F(AlsaPcmOutputStreamTest, OpenClose) { Return(0))); EXPECT_CALL(mock_alsa_wrapper_, PcmSetParams(kFakeHandle, - SND_PCM_FORMAT_S8, + SND_PCM_FORMAT_U8, SND_PCM_ACCESS_RW_INTERLEAVED, kTestChannels, kTestSampleRate, @@ -226,11 +318,24 @@ TEST_F(AlsaPcmOutputStreamTest, PcmOpenFailed) { EXPECT_CALL(mock_alsa_wrapper_, StrError(kTestFailedErrno)) .WillOnce(Return(kDummyMessage)); - // If open fails, the stream stays in kCreated because it has effectively had - // no changes. - ASSERT_FALSE(test_stream_->Open(kTestPacketSize)); - EXPECT_EQ(AlsaPcmOutputStream::kCreated, + // Open still succeeds since PcmOpen is delegated to another thread. + ASSERT_TRUE(test_stream_->Open(kTestPacketSize)); + ASSERT_EQ(AlsaPcmOutputStream::kIsOpened, test_stream_->shared_data_.state()); + ASSERT_FALSE(test_stream_->stop_stream_); + message_loop_.RunAllPending(); + + // Ensure internal state is set for a no-op stream if PcmOpen() failes. + EXPECT_EQ(AlsaPcmOutputStream::kIsOpened, + test_stream_->shared_data_.state()); + EXPECT_TRUE(test_stream_->stop_stream_); + EXPECT_TRUE(test_stream_->playback_handle_ == NULL); + EXPECT_FALSE(test_stream_->packet_.get()); + + // Close the stream since we opened it to make destruction happy. + EXPECT_CALL(mock_manager_, ReleaseStream(test_stream_.get())); + test_stream_->Close(); + message_loop_.RunAllPending(); } TEST_F(AlsaPcmOutputStreamTest, PcmSetParamsFailed) { @@ -246,9 +351,23 @@ TEST_F(AlsaPcmOutputStreamTest, PcmSetParamsFailed) { // If open fails, the stream stays in kCreated because it has effectively had // no changes. - ASSERT_FALSE(test_stream_->Open(kTestPacketSize)); - EXPECT_EQ(AlsaPcmOutputStream::kCreated, + ASSERT_TRUE(test_stream_->Open(kTestPacketSize)); + EXPECT_EQ(AlsaPcmOutputStream::kIsOpened, + test_stream_->shared_data_.state()); + ASSERT_FALSE(test_stream_->stop_stream_); + message_loop_.RunAllPending(); + + // Ensure internal state is set for a no-op stream if PcmSetParams() failes. + EXPECT_EQ(AlsaPcmOutputStream::kIsOpened, test_stream_->shared_data_.state()); + EXPECT_TRUE(test_stream_->stop_stream_); + EXPECT_TRUE(test_stream_->playback_handle_ == NULL); + EXPECT_FALSE(test_stream_->packet_.get()); + + // Close the stream since we opened it to make destruction happy. + EXPECT_CALL(mock_manager_, ReleaseStream(test_stream_.get())); + test_stream_->Close(); + message_loop_.RunAllPending(); } TEST_F(AlsaPcmOutputStreamTest, StartStop) { @@ -386,6 +505,127 @@ TEST_F(AlsaPcmOutputStreamTest, BufferPacket_UnfinishedPacket) { EXPECT_EQ(kTestPacketSize, packet_.size); } +TEST_F(AlsaPcmOutputStreamTest, AutoSelectDevice_DeviceSelect) { + // Try channels from 1 -> 9. and see that we get the more specific surroundXX + // device opened for channels 4-8. For all other channels, the device should + // default to |AlsaPcmOutputStream::kDefaultDevice|. We should also not + // downmix any channel in this case because downmixing is only defined for + // channels 4-8, which we are guaranteeing to work. + // + // Note that the loop starts at "1", so the first parameter is ignored in + // these arrays. + const char* kExpectedDeviceName[] = { NULL, + AlsaPcmOutputStream::kDefaultDevice, + AlsaPcmOutputStream::kDefaultDevice, + AlsaPcmOutputStream::kDefaultDevice, + kSurround40, kSurround50, kSurround51, + kSurround70, kSurround71, + AlsaPcmOutputStream::kDefaultDevice }; + bool kExpectedDownmix[] = { false, false, false, false, false, false, + false, false, false, false }; + + for (int i = 1; i <= 9; ++i) { + SCOPED_TRACE(StringPrintf("Attempting %d Channel", i)); + + // Hints will only be grabbed for channel numbers that have non-default + // devices associated with them. + if (kExpectedDeviceName[i] != AlsaPcmOutputStream::kDefaultDevice) { + // The DeviceNameHint and DeviceNameFreeHint need to be paired to avoid a + // memory leak. + EXPECT_CALL(mock_alsa_wrapper_, DeviceNameHint(_, _, _)) + .WillOnce(DoAll(SetArgumentPointee<2>(&kFakeHints[0]), Return(0))); + EXPECT_CALL(mock_alsa_wrapper_, DeviceNameFreeHint(&kFakeHints[0])) + .Times(1); + } + + EXPECT_CALL(mock_alsa_wrapper_, + PcmOpen(_, StrEq(kExpectedDeviceName[i]), _, _)) + .WillOnce(DoAll(SetArgumentPointee<0>(kFakeHandle), Return(0))); + EXPECT_CALL(mock_alsa_wrapper_, + PcmSetParams(kFakeHandle, _, _, i, _, _, _)) + .WillOnce(Return(0)); + + // The parameters are specified by ALSA documentation, and are in constants + // in the implementation files. + EXPECT_CALL(mock_alsa_wrapper_, DeviceNameGetHint(_, StrEq("IOID"))) + .WillRepeatedly(Invoke(OutputHint)); + EXPECT_CALL(mock_alsa_wrapper_, DeviceNameGetHint(_, StrEq("NAME"))) + .WillRepeatedly(Invoke(EchoHint)); + + + test_stream_ = CreateStreamWithChannels(i); + EXPECT_TRUE(test_stream_->AutoSelectDevice(i)); + EXPECT_EQ(kExpectedDownmix[i], test_stream_->should_downmix_); + + Mock::VerifyAndClearExpectations(&mock_alsa_wrapper_); + Mock::VerifyAndClearExpectations(&mock_manager_); + } +} + +TEST_F(AlsaPcmOutputStreamTest, AutoSelectDevice_FallbackDevices) { + using std::string; + + // If there are problems opening a multi-channel device, it the fallbacks + // operations should be as follows. Assume the multi-channel device name is + // surround50: + // + // 1) Try open "surround50" + // 2) Try open "plug:surround50". + // 3) Try open "default". + // 4) Try open "plug:default". + // 5) Give up trying to open. + // + const string first_try = kSurround50; + const string second_try = string(AlsaPcmOutputStream::kPlugPrefix) + + kSurround50; + const string third_try = AlsaPcmOutputStream::kDefaultDevice; + const string fourth_try = string(AlsaPcmOutputStream::kPlugPrefix) + + AlsaPcmOutputStream::kDefaultDevice; + + EXPECT_CALL(mock_alsa_wrapper_, DeviceNameHint(_, _, _)) + .WillOnce(DoAll(SetArgumentPointee<2>(&kFakeHints[0]), Return(0))); + EXPECT_CALL(mock_alsa_wrapper_, DeviceNameFreeHint(&kFakeHints[0])) + .Times(1); + EXPECT_CALL(mock_alsa_wrapper_, DeviceNameGetHint(_, StrEq("IOID"))) + .WillRepeatedly(Invoke(OutputHint)); + EXPECT_CALL(mock_alsa_wrapper_, DeviceNameGetHint(_, StrEq("NAME"))) + .WillRepeatedly(Invoke(EchoHint)); + EXPECT_CALL(mock_alsa_wrapper_, StrError(kTestFailedErrno)) + .WillRepeatedly(Return(kDummyMessage)); + + InSequence s; + EXPECT_CALL(mock_alsa_wrapper_, PcmOpen(_, StrEq(first_try.c_str()), _, _)) + .WillOnce(Return(kTestFailedErrno)); + EXPECT_CALL(mock_alsa_wrapper_, PcmOpen(_, StrEq(second_try.c_str()), _, _)) + .WillOnce(Return(kTestFailedErrno)); + EXPECT_CALL(mock_alsa_wrapper_, PcmOpen(_, StrEq(third_try.c_str()), _, _)) + .WillOnce(Return(kTestFailedErrno)); + EXPECT_CALL(mock_alsa_wrapper_, PcmOpen(_, StrEq(fourth_try.c_str()), _, _)) + .WillOnce(Return(kTestFailedErrno)); + + test_stream_ = CreateStreamWithChannels(5); + EXPECT_FALSE(test_stream_->AutoSelectDevice(5)); +} + +TEST_F(AlsaPcmOutputStreamTest, AutoSelectDevice_HintFail) { + // Should get |kDefaultDevice|, and force a 2-channel downmix on a failure to + // enumerate devices. + EXPECT_CALL(mock_alsa_wrapper_, DeviceNameHint(_, _, _)) + .WillRepeatedly(Return(kTestFailedErrno)); + EXPECT_CALL(mock_alsa_wrapper_, + PcmOpen(_, StrEq(AlsaPcmOutputStream::kDefaultDevice), _, _)) + .WillOnce(DoAll(SetArgumentPointee<0>(kFakeHandle), Return(0))); + EXPECT_CALL(mock_alsa_wrapper_, + PcmSetParams(kFakeHandle, _, _, 2, _, _, _)) + .WillOnce(Return(0)); + EXPECT_CALL(mock_alsa_wrapper_, StrError(kTestFailedErrno)) + .WillOnce(Return(kDummyMessage)); + + test_stream_ = CreateStreamWithChannels(5); + EXPECT_TRUE(test_stream_->AutoSelectDevice(5)); + EXPECT_TRUE(test_stream_->should_downmix_); +} + TEST_F(AlsaPcmOutputStreamTest, BufferPacket_StopStream) { test_stream_->stop_stream_ = true; test_stream_->BufferPacket(&packet_); diff --git a/media/audio/linux/alsa_wrapper.cc b/media/audio/linux/alsa_wrapper.cc index 8463686..303a1f1 100644 --- a/media/audio/linux/alsa_wrapper.cc +++ b/media/audio/linux/alsa_wrapper.cc @@ -17,6 +17,18 @@ int AlsaWrapper::PcmOpen(snd_pcm_t** handle, const char* name, return snd_pcm_open(handle, name, stream, mode); } +int AlsaWrapper::DeviceNameHint(int card, const char* iface, void*** hints) { + return snd_device_name_hint(card, iface, hints); +} + +char* AlsaWrapper::DeviceNameGetHint(const void* hint, const char* id) { + return snd_device_name_get_hint(hint, id); +} + +int AlsaWrapper::DeviceNameFreeHint(void** hints) { + return snd_device_name_free_hint(hints); +} + int AlsaWrapper::PcmClose(snd_pcm_t* handle) { return snd_pcm_close(handle); } diff --git a/media/audio/linux/alsa_wrapper.h b/media/audio/linux/alsa_wrapper.h index d7686dd..5ab3c84 100644 --- a/media/audio/linux/alsa_wrapper.h +++ b/media/audio/linux/alsa_wrapper.h @@ -15,6 +15,10 @@ class AlsaWrapper { AlsaWrapper(); virtual ~AlsaWrapper(); + virtual int DeviceNameHint(int card, const char* iface, void*** hints); + virtual char* DeviceNameGetHint(const void* hint, const char* id); + virtual int DeviceNameFreeHint(void** hints); + virtual int PcmOpen(snd_pcm_t** handle, const char* name, snd_pcm_stream_t stream, int mode); virtual int PcmClose(snd_pcm_t* handle); diff --git a/media/audio/linux/audio_manager_linux.cc b/media/audio/linux/audio_manager_linux.cc index 0c4db6a..9d395e8 100644 --- a/media/audio/linux/audio_manager_linux.cc +++ b/media/audio/linux/audio_manager_linux.cc @@ -5,10 +5,13 @@ #include "media/audio/linux/audio_manager_linux.h" #include "base/at_exit.h" +#include "base/command_line.h" #include "base/logging.h" #include "media/audio/fake_audio_output_stream.h" #include "media/audio/linux/alsa_output.h" #include "media/audio/linux/alsa_wrapper.h" +#include "media/base/media_switches.h" + namespace { AudioManagerLinux* g_audio_manager = NULL; @@ -34,15 +37,14 @@ AudioOutputStream* AudioManagerLinux::MakeAudioStream(Format format, return NULL; } - // TODO(ajwong): Do we want to be able to configure the device? default - // should work correctly for all mono/stereo, but not surround, which needs - // surround40, surround51, etc. - // - // http://0pointer.de/blog/projects/guide-to-sound-apis.html + std::string device_name = AlsaPcmOutputStream::kAutoSelectDevice; + if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kAlsaDevice)) { + device_name = CommandLine::ForCurrentProcess()->GetSwitchValueASCII( + switches::kAlsaDevice); + } AlsaPcmOutputStream* stream = - new AlsaPcmOutputStream(AlsaPcmOutputStream::kDefaultDevice, - format, channels, sample_rate, bits_per_sample, - wrapper_.get(), this, + new AlsaPcmOutputStream(device_name, format, channels, sample_rate, + bits_per_sample, wrapper_.get(), this, audio_thread_.message_loop()); AutoLock l(lock_); diff --git a/media/audio/mac/audio_output_mac.cc b/media/audio/mac/audio_output_mac.cc index a8f34f3..42e80a1 100644 --- a/media/audio/mac/audio_output_mac.cc +++ b/media/audio/mac/audio_output_mac.cc @@ -171,7 +171,7 @@ void PCMQueueOutAudioOutputStream::GetVolume(double* left_level, // TODO(fbarchard): Switch layout when ffmpeg is updated. namespace { template<class Format> -static void SwizzleLayout(Format *b, size_t filled) { +static void SwizzleLayout(Format* b, size_t filled) { static const int kNumSurroundChannels = 6; Format aac[kNumSurroundChannels]; for (size_t i = 0; i < filled; i += sizeof(aac), b += kNumSurroundChannels) { @@ -274,4 +274,3 @@ void PCMQueueOutAudioOutputStream::Start(AudioSourceCallback* callback) { return; } } - |