| // Copyright 2013 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. |
| // |
| // THREAD SAFETY |
| // |
| // AlsaPcmOutputStream object is *not* thread-safe and should only be used |
| // from the audio thread. We DCHECK on this assumption whenever we can. |
| // |
| // SEMANTICS OF Close() |
| // |
| // Close() is responsible for cleaning up any resources that were acquired after |
| // a successful Open(). Close() will nullify any scheduled outstanding runnable |
| // methods. |
| // |
| // |
| // SEMANTICS OF ERROR STATES |
| // |
| // The object has two distinct error states: |state_| == kInError |
| // and |stop_stream_|. The |stop_stream_| variable is used to indicate |
| // that the playback_handle should no longer be used either because of a |
| // hardware/low-level event. |
| // |
| // When |state_| == kInError, all public API functions will fail with an error |
| // (Start() will call the OnError() function on the callback immediately), or |
| // no-op themselves with the exception of Close(). Even if an error state has |
| // been entered, if Open() has previously returned successfully, Close() must be |
| // called to cleanup the ALSA devices and release resources. |
| // |
| // When |stop_stream_| is set, no more commands will be made against the |
| // ALSA device, and playback will effectively stop. From the client's point of |
| // view, it will seem that the device has just clogged and stopped requesting |
| // data. |
| |
| #include "media/audio/alsa/alsa_output.h" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <memory> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/logging.h" |
| #include "base/memory/free_deleter.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "base/time/default_tick_clock.h" |
| #include "base/trace_event/trace_event.h" |
| #include "media/audio/alsa/alsa_util.h" |
| #include "media/audio/alsa/alsa_wrapper.h" |
| #include "media/audio/alsa/audio_manager_alsa.h" |
| #include "media/base/audio_timestamp_helper.h" |
| #include "media/base/channel_mixer.h" |
| #include "media/base/data_buffer.h" |
| #include "media/base/seekable_buffer.h" |
| |
| namespace media { |
| |
| // Set to 0 during debugging if you want error messages due to underrun |
| // events or other recoverable errors. |
| #if defined(NDEBUG) |
| static const int kPcmRecoverIsSilent = 1; |
| #else |
| static const int kPcmRecoverIsSilent = 0; |
| #endif |
| |
| // The output channel layout if we set up downmixing for the kDefaultDevice |
| // device. |
| static const ChannelLayout kDefaultOutputChannelLayout = CHANNEL_LAYOUT_STEREO; |
| |
| // 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. nullptr 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 based on |
| // channel number. Fix API to pass that data down. |
| static const char* GuessSpecificDeviceName(uint32_t 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 nullptr; |
| } |
| } |
| |
| std::ostream& operator<<(std::ostream& os, |
| AlsaPcmOutputStream::InternalState state) { |
| switch (state) { |
| case AlsaPcmOutputStream::kInError: |
| os << "kInError"; |
| break; |
| case AlsaPcmOutputStream::kCreated: |
| os << "kCreated"; |
| break; |
| case AlsaPcmOutputStream::kIsOpened: |
| os << "kIsOpened"; |
| break; |
| case AlsaPcmOutputStream::kIsPlaying: |
| os << "kIsPlaying"; |
| break; |
| case AlsaPcmOutputStream::kIsStopped: |
| os << "kIsStopped"; |
| break; |
| case AlsaPcmOutputStream::kIsClosed: |
| os << "kIsClosed"; |
| break; |
| } |
| return os; |
| } |
| |
| static const SampleFormat kSampleFormat = kSampleFormatS16; |
| static const snd_pcm_format_t kAlsaSampleFormat = SND_PCM_FORMAT_S16; |
| |
| const char AlsaPcmOutputStream::kDefaultDevice[] = "default"; |
| const char AlsaPcmOutputStream::kAutoSelectDevice[] = ""; |
| const char AlsaPcmOutputStream::kPlugPrefix[] = "plug:"; |
| |
| // We use 40ms as our minimum required latency. If it is needed, we may be able |
| // to get it down to 20ms. |
| const uint32_t AlsaPcmOutputStream::kMinLatencyMicros = 40 * 1000; |
| |
| AlsaPcmOutputStream::AlsaPcmOutputStream(const std::string& device_name, |
| const AudioParameters& params, |
| AlsaWrapper* wrapper, |
| AudioManagerBase* manager) |
| : requested_device_name_(device_name), |
| pcm_format_(kAlsaSampleFormat), |
| channels_(params.channels()), |
| channel_layout_(params.channel_layout()), |
| sample_rate_(params.sample_rate()), |
| bytes_per_sample_(SampleFormatToBytesPerChannel(kSampleFormat)), |
| bytes_per_frame_(params.GetBytesPerFrame(kSampleFormat)), |
| packet_size_(params.GetBytesPerBuffer(kSampleFormat)), |
| latency_(std::max( |
| base::Microseconds(kMinLatencyMicros), |
| AudioTimestampHelper::FramesToTime(params.frames_per_buffer() * 2, |
| sample_rate_))), |
| bytes_per_output_frame_(bytes_per_frame_), |
| alsa_buffer_frames_(0), |
| stop_stream_(false), |
| wrapper_(wrapper), |
| manager_(manager), |
| task_runner_(base::ThreadTaskRunnerHandle::Get()), |
| playback_handle_(nullptr), |
| frames_per_packet_(packet_size_ / bytes_per_frame_), |
| state_(kCreated), |
| volume_(1.0f), |
| source_callback_(nullptr), |
| audio_bus_(AudioBus::Create(params)), |
| tick_clock_(base::DefaultTickClock::GetInstance()) { |
| DCHECK(manager_->GetTaskRunner()->BelongsToCurrentThread()); |
| DCHECK_EQ(audio_bus_->frames() * bytes_per_frame_, packet_size_); |
| |
| // Sanity check input values. |
| if (!params.IsValid()) { |
| LOG(WARNING) << "Unsupported audio parameters."; |
| TransitionTo(kInError); |
| } |
| } |
| |
| AlsaPcmOutputStream::~AlsaPcmOutputStream() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| InternalState current_state = state(); |
| DCHECK(current_state == kCreated || |
| current_state == kIsClosed || |
| current_state == kInError); |
| DCHECK(!playback_handle_); |
| } |
| |
| bool AlsaPcmOutputStream::Open() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (state() == kInError) |
| return false; |
| |
| if (!CanTransitionTo(kIsOpened)) { |
| NOTREACHED() << "Invalid state: " << state(); |
| 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 |
| // transition out from under us. |
| TransitionTo(kIsOpened); |
| |
| // Try to open the device. |
| if (requested_device_name_ == kAutoSelectDevice) { |
| playback_handle_ = AutoSelectDevice(latency_.InMicroseconds()); |
| if (playback_handle_) |
| DVLOG(1) << "Auto-selected device: " << device_name_; |
| } else { |
| device_name_ = requested_device_name_; |
| playback_handle_ = alsa_util::OpenPlaybackDevice( |
| wrapper_, device_name_.c_str(), channels_, sample_rate_, |
| pcm_format_, latency_.InMicroseconds()); |
| } |
| |
| // Finish initializing the stream if the device was opened successfully. |
| if (playback_handle_ == nullptr) { |
| stop_stream_ = true; |
| TransitionTo(kInError); |
| return false; |
| } |
| bytes_per_output_frame_ = |
| channel_mixer_ ? mixed_audio_bus_->channels() * bytes_per_sample_ |
| : bytes_per_frame_; |
| uint32_t output_packet_size = frames_per_packet_ * bytes_per_output_frame_; |
| buffer_ = std::make_unique<SeekableBuffer>(0, output_packet_size); |
| |
| // Get alsa buffer size. |
| snd_pcm_uframes_t buffer_size; |
| snd_pcm_uframes_t period_size; |
| int error = |
| wrapper_->PcmGetParams(playback_handle_, &buffer_size, &period_size); |
| if (error < 0) { |
| LOG(ERROR) << "Failed to get playback buffer size from ALSA: " |
| << wrapper_->StrError(error); |
| // Buffer size is at least twice of packet size. |
| alsa_buffer_frames_ = frames_per_packet_ * 2; |
| } else { |
| alsa_buffer_frames_ = buffer_size; |
| } |
| |
| return true; |
| } |
| |
| void AlsaPcmOutputStream::Close() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (state() != kIsClosed) |
| TransitionTo(kIsClosed); |
| |
| // Shutdown the audio device. |
| if (playback_handle_) { |
| if (alsa_util::CloseDevice(wrapper_, playback_handle_) < 0) { |
| LOG(WARNING) << "Unable to close audio device. Leaking handle."; |
| } |
| playback_handle_ = nullptr; |
| |
| // Release the buffer. |
| buffer_.reset(); |
| |
| // Signal anything that might already be scheduled to stop. |
| stop_stream_ = true; // Not necessary in production, but unit tests |
| // uses the flag to verify that stream was closed. |
| } |
| |
| weak_factory_.InvalidateWeakPtrs(); |
| |
| // Signal to the manager that we're closed and can be removed. |
| // Should be last call in the method as it deletes "this". |
| manager_->ReleaseOutputStream(this); |
| } |
| |
| void AlsaPcmOutputStream::Start(AudioSourceCallback* callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| CHECK(callback); |
| |
| if (stop_stream_) |
| return; |
| |
| // Only post the task if we can enter the playing state. |
| if (TransitionTo(kIsPlaying) != kIsPlaying) |
| return; |
| |
| // Before starting, the buffer might have audio from previous user of this |
| // device. |
| buffer_->Clear(); |
| |
| // When starting again, drop all packets in the device and prepare it again |
| // in case we are restarting from a pause state and need to flush old data. |
| int error = wrapper_->PcmDrop(playback_handle_); |
| if (error < 0 && error != -EAGAIN) { |
| LOG(ERROR) << "Failure clearing playback device (" |
| << wrapper_->PcmName(playback_handle_) << "): " |
| << wrapper_->StrError(error); |
| stop_stream_ = true; |
| return; |
| } |
| |
| error = wrapper_->PcmPrepare(playback_handle_); |
| if (error < 0 && error != -EAGAIN) { |
| LOG(ERROR) << "Failure preparing stream (" |
| << wrapper_->PcmName(playback_handle_) << "): " |
| << wrapper_->StrError(error); |
| stop_stream_ = true; |
| return; |
| } |
| |
| // Ensure the first buffer is silence to avoid startup glitches. |
| int buffer_size = GetAvailableFrames() * bytes_per_output_frame_; |
| scoped_refptr<DataBuffer> silent_packet = new DataBuffer(buffer_size); |
| silent_packet->set_data_size(buffer_size); |
| memset(silent_packet->writable_data(), 0, silent_packet->data_size()); |
| buffer_->Append(silent_packet); |
| WritePacket(); |
| |
| // Start the callback chain. |
| set_source_callback(callback); |
| WriteTask(); |
| } |
| |
| void AlsaPcmOutputStream::Stop() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Reset the callback, so that it is not called anymore. |
| set_source_callback(nullptr); |
| weak_factory_.InvalidateWeakPtrs(); |
| |
| TransitionTo(kIsStopped); |
| } |
| |
| // This stream is always used with sub second buffer sizes, where it's |
| // sufficient to simply always flush upon Start(). |
| void AlsaPcmOutputStream::Flush() {} |
| |
| void AlsaPcmOutputStream::SetVolume(double volume) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| volume_ = static_cast<float>(volume); |
| } |
| |
| void AlsaPcmOutputStream::GetVolume(double* volume) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| *volume = volume_; |
| } |
| |
| void AlsaPcmOutputStream::SetTickClockForTesting( |
| const base::TickClock* tick_clock) { |
| DCHECK(tick_clock); |
| tick_clock_ = tick_clock; |
| } |
| |
| void AlsaPcmOutputStream::BufferPacket(bool* source_exhausted) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // If stopped, simulate a 0-length packet. |
| if (stop_stream_) { |
| buffer_->Clear(); |
| *source_exhausted = true; |
| return; |
| } |
| |
| *source_exhausted = false; |
| |
| // Request more data only when we run out of data in the buffer, because |
| // WritePacket() consumes only the current chunk of data. |
| if (!buffer_->forward_bytes()) { |
| // Before making a request to source for data we need to determine the |
| // delay for the requested data to be played. |
| const base::TimeDelta delay = |
| AudioTimestampHelper::FramesToTime(GetCurrentDelay(), sample_rate_); |
| |
| scoped_refptr<DataBuffer> packet = new DataBuffer(packet_size_); |
| int frames_filled = |
| RunDataCallback(delay, tick_clock_->NowTicks(), audio_bus_.get()); |
| |
| size_t packet_size = frames_filled * bytes_per_frame_; |
| DCHECK_LE(packet_size, packet_size_); |
| |
| // TODO(dalecurtis): Channel downmixing, upmixing, should be done in mixer; |
| // volume adjust should use SSE optimized vector_fmul() prior to interleave. |
| AudioBus* output_bus = audio_bus_.get(); |
| ChannelLayout output_channel_layout = channel_layout_; |
| if (channel_mixer_) { |
| output_bus = mixed_audio_bus_.get(); |
| channel_mixer_->Transform(audio_bus_.get(), output_bus); |
| output_channel_layout = kDefaultOutputChannelLayout; |
| // Adjust packet size for downmix. |
| packet_size = packet_size / bytes_per_frame_ * bytes_per_output_frame_; |
| } |
| |
| // Reorder channels for 5.0, 5.1, and 7.1 to match ALSA's channel order, |
| // which has front center at channel index 4 and LFE at channel index 5. |
| // See http://ffmpeg.org/pipermail/ffmpeg-cvslog/2011-June/038454.html. |
| switch (output_channel_layout) { |
| case CHANNEL_LAYOUT_5_0: |
| case CHANNEL_LAYOUT_5_0_BACK: |
| output_bus->SwapChannels(2, 3); |
| output_bus->SwapChannels(3, 4); |
| break; |
| case CHANNEL_LAYOUT_5_1: |
| case CHANNEL_LAYOUT_5_1_BACK: |
| case CHANNEL_LAYOUT_7_1: |
| output_bus->SwapChannels(2, 4); |
| output_bus->SwapChannels(3, 5); |
| break; |
| default: |
| break; |
| } |
| |
| // Note: If this ever changes to output raw float the data must be clipped |
| // and sanitized since it may come from an untrusted source such as NaCl. |
| output_bus->Scale(volume_); |
| output_bus->ToInterleaved<SignedInt16SampleTypeTraits>( |
| frames_filled, reinterpret_cast<int16_t*>(packet->writable_data())); |
| |
| if (packet_size > 0) { |
| packet->set_data_size(packet_size); |
| // Add the packet to the buffer. |
| buffer_->Append(packet); |
| } else { |
| *source_exhausted = true; |
| } |
| } |
| } |
| |
| void AlsaPcmOutputStream::WritePacket() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // If the device is in error, just eat the bytes. |
| if (stop_stream_) { |
| buffer_->Clear(); |
| return; |
| } |
| |
| if (state() != kIsPlaying) |
| return; |
| |
| CHECK_EQ(buffer_->forward_bytes() % bytes_per_output_frame_, 0u); |
| |
| const uint8_t* buffer_data; |
| int buffer_size; |
| if (buffer_->GetCurrentChunk(&buffer_data, &buffer_size)) { |
| snd_pcm_sframes_t frames = std::min( |
| static_cast<snd_pcm_sframes_t>(buffer_size / bytes_per_output_frame_), |
| GetAvailableFrames()); |
| |
| if (!frames) |
| return; |
| |
| snd_pcm_sframes_t frames_written = |
| wrapper_->PcmWritei(playback_handle_, buffer_data, frames); |
| if (frames_written < 0) { |
| // Attempt once to immediately recover from EINTR, |
| // EPIPE (overrun/underrun), ESTRPIPE (stream suspended). WritePacket |
| // will eventually be called again, so eventual recovery will happen if |
| // muliple retries are required. |
| frames_written = wrapper_->PcmRecover(playback_handle_, |
| frames_written, |
| kPcmRecoverIsSilent); |
| if (frames_written < 0) { |
| if (frames_written != -EAGAIN) { |
| LOG(ERROR) << "Failed to write to pcm device: " |
| << wrapper_->StrError(frames_written); |
| RunErrorCallback(frames_written); |
| stop_stream_ = true; |
| } |
| } |
| } else { |
| DCHECK_EQ(frames_written, frames); |
| |
| // Seek forward in the buffer after we've written some data to ALSA. |
| buffer_->Seek(frames_written * bytes_per_output_frame_); |
| } |
| } else { |
| // If nothing left to write and playback hasn't started yet, start it now. |
| // This ensures that shorter sounds will still play. |
| if (playback_handle_ && |
| (wrapper_->PcmState(playback_handle_) == SND_PCM_STATE_PREPARED) && |
| GetCurrentDelay() > 0) { |
| wrapper_->PcmStart(playback_handle_); |
| } |
| } |
| } |
| |
| void AlsaPcmOutputStream::WriteTask() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (stop_stream_) |
| return; |
| |
| if (state() == kIsStopped) |
| return; |
| |
| bool source_exhausted; |
| BufferPacket(&source_exhausted); |
| WritePacket(); |
| |
| ScheduleNextWrite(source_exhausted); |
| } |
| |
| void AlsaPcmOutputStream::ScheduleNextWrite(bool source_exhausted) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (stop_stream_ || state() != kIsPlaying) |
| return; |
| |
| const uint32_t kTargetFramesAvailable = alsa_buffer_frames_ / 2; |
| uint32_t available_frames = GetAvailableFrames(); |
| |
| base::TimeDelta next_fill_time; |
| if (buffer_->forward_bytes() && available_frames) { |
| // If we've got data available and ALSA has room, deliver it immediately. |
| next_fill_time = base::TimeDelta(); |
| } else if (buffer_->forward_bytes()) { |
| // If we've got data available and no room, poll until room is available. |
| // Polling in this manner allows us to ensure a more consistent callback |
| // schedule. In testing this yields a variance of +/- 5ms versus the non- |
| // polling strategy which is around +/- 30ms and bimodal. |
| next_fill_time = base::Milliseconds(5); |
| } else if (available_frames < kTargetFramesAvailable) { |
| // Schedule the next write for the moment when the available buffer of the |
| // sound card hits |kTargetFramesAvailable|. |
| next_fill_time = AudioTimestampHelper::FramesToTime( |
| kTargetFramesAvailable - available_frames, sample_rate_); |
| } else if (!source_exhausted) { |
| // The sound card has |kTargetFramesAvailable| or more frames available. |
| // Invoke the next write immediately to avoid underrun. |
| next_fill_time = base::TimeDelta(); |
| } else { |
| // The sound card has frames available, but our source is exhausted, so |
| // avoid busy looping by delaying a bit. |
| next_fill_time = base::Milliseconds(10); |
| } |
| |
| task_runner_->PostDelayedTask(FROM_HERE, |
| base::BindOnce(&AlsaPcmOutputStream::WriteTask, |
| weak_factory_.GetWeakPtr()), |
| next_fill_time); |
| } |
| |
| std::string AlsaPcmOutputStream::FindDeviceForChannels(uint32_t 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(); |
| |
| std::string guessed_device; |
| void** hints = nullptr; |
| 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 != nullptr; hint_iter++) { |
| // Only examine devices that are output capable.. Valid values are |
| // "Input", "Output", and nullptr which means both input and output. |
| std::unique_ptr<char, base::FreeDeleter> io( |
| wrapper_->DeviceNameGetHint(*hint_iter, kIoHintName)); |
| if (io != nullptr && strcmp(io.get(), "Input") == 0) |
| continue; |
| |
| // Attempt to select the closest device for number of channels. |
| std::unique_ptr<char, base::FreeDeleter> name( |
| wrapper_->DeviceNameGetHint(*hint_iter, kNameHintName)); |
| if (strncmp(wanted_device, name.get(), strlen(wanted_device)) == 0) { |
| guessed_device = name.get(); |
| break; |
| } |
| } |
| |
| // Destroy the hint now that we're done with it. |
| wrapper_->DeviceNameFreeHint(hints); |
| hints = nullptr; |
| } else { |
| LOG(ERROR) << "Unable to get hints for devices: " |
| << wrapper_->StrError(error); |
| } |
| |
| return guessed_device; |
| } |
| |
| snd_pcm_sframes_t AlsaPcmOutputStream::GetCurrentDelay() { |
| snd_pcm_sframes_t delay = -1; |
| // Don't query ALSA's delay if we have underrun since it'll be jammed at some |
| // non-zero value and potentially even negative! |
| // |
| // Also, if we're in the prepared state, don't query because that seems to |
| // cause an I/O error when we do query the delay. |
| snd_pcm_state_t pcm_state = wrapper_->PcmState(playback_handle_); |
| if (pcm_state != SND_PCM_STATE_XRUN && |
| pcm_state != SND_PCM_STATE_PREPARED) { |
| int error = wrapper_->PcmDelay(playback_handle_, &delay); |
| if (error < 0) { |
| // Assume a delay of zero and attempt to recover the device. |
| delay = -1; |
| error = wrapper_->PcmRecover(playback_handle_, |
| error, |
| kPcmRecoverIsSilent); |
| if (error < 0) { |
| LOG(ERROR) << "Failed querying delay: " << wrapper_->StrError(error); |
| } |
| } |
| } |
| |
| // snd_pcm_delay() sometimes returns crazy values. In this case return delay |
| // of data we know currently is in ALSA's buffer. Note: When the underlying |
| // driver is PulseAudio based, certain configuration settings (e.g., tsched=1) |
| // will generate much larger delay values than |alsa_buffer_frames_|, so only |
| // clip if delay is truly crazy (> 10x expected). |
| if (delay < 0 || |
| static_cast<snd_pcm_uframes_t>(delay) > alsa_buffer_frames_ * 10) { |
| delay = alsa_buffer_frames_ - GetAvailableFrames(); |
| } |
| |
| if (delay < 0) { |
| delay = 0; |
| } |
| |
| return delay; |
| } |
| |
| snd_pcm_sframes_t AlsaPcmOutputStream::GetAvailableFrames() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (stop_stream_) |
| return 0; |
| |
| // Find the number of frames queued in the sound device. |
| snd_pcm_sframes_t available_frames = |
| wrapper_->PcmAvailUpdate(playback_handle_); |
| if (available_frames < 0) { |
| available_frames = wrapper_->PcmRecover(playback_handle_, |
| available_frames, |
| kPcmRecoverIsSilent); |
| } |
| if (available_frames < 0) { |
| LOG(ERROR) << "Failed querying available frames. Assuming 0: " |
| << wrapper_->StrError(available_frames); |
| return 0; |
| } |
| if (static_cast<uint32_t>(available_frames) > alsa_buffer_frames_ * 2) { |
| LOG(ERROR) << "ALSA returned " << available_frames << " of " |
| << alsa_buffer_frames_ << " frames available."; |
| return alsa_buffer_frames_; |
| } |
| |
| 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 in case ALSA can |
| // remap and do some software conversion to make it work. |
| // 3) If that fails, attempt the "plug:" version of the guessed name in |
| // case ALSA can remap and do some software conversion to make it work. |
| // 4) Fallback to kDefaultDevice. |
| // 5) If that fails too, try the "plug:" version of kDefaultDevice. |
| // 6) Give up. |
| snd_pcm_t* handle = nullptr; |
| device_name_ = FindDeviceForChannels(channels_); |
| |
| // Step 1. |
| if (!device_name_.empty()) { |
| if ((handle = alsa_util::OpenPlaybackDevice( |
| wrapper_, device_name_.c_str(), channels_, sample_rate_, |
| pcm_format_, latency)) != nullptr) { |
| return handle; |
| } |
| |
| // Step 2. |
| device_name_ = kPlugPrefix + device_name_; |
| if ((handle = alsa_util::OpenPlaybackDevice( |
| wrapper_, device_name_.c_str(), channels_, sample_rate_, |
| pcm_format_, latency)) != nullptr) { |
| return handle; |
| } |
| |
| // Step 3. |
| device_name_ = GuessSpecificDeviceName(channels_); |
| if (!device_name_.empty()) { |
| device_name_ = kPlugPrefix + device_name_; |
| if ((handle = alsa_util::OpenPlaybackDevice( |
| wrapper_, device_name_.c_str(), channels_, sample_rate_, |
| pcm_format_, latency)) != nullptr) { |
| 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 (3 channel to 8 channel), setup |
| // downmixing. |
| uint32_t default_channels = channels_; |
| if (default_channels > 2) { |
| channel_mixer_ = std::make_unique<ChannelMixer>( |
| channel_layout_, kDefaultOutputChannelLayout); |
| default_channels = 2; |
| mixed_audio_bus_ = AudioBus::Create( |
| default_channels, audio_bus_->frames()); |
| } |
| |
| // Step 4. |
| device_name_ = kDefaultDevice; |
| if ((handle = alsa_util::OpenPlaybackDevice( |
| wrapper_, device_name_.c_str(), default_channels, sample_rate_, |
| pcm_format_, latency)) != nullptr) { |
| return handle; |
| } |
| |
| // Step 5. |
| device_name_ = kPlugPrefix + device_name_; |
| if ((handle = alsa_util::OpenPlaybackDevice( |
| wrapper_, device_name_.c_str(), default_channels, sample_rate_, |
| pcm_format_, latency)) != nullptr) { |
| return handle; |
| } |
| |
| // Unable to open any device. |
| device_name_.clear(); |
| return nullptr; |
| } |
| |
| bool AlsaPcmOutputStream::CanTransitionTo(InternalState to) { |
| switch (state_) { |
| case kCreated: |
| return to == kIsOpened || to == kIsClosed || to == kInError; |
| |
| case kIsOpened: |
| return to == kIsPlaying || to == kIsStopped || |
| to == kIsClosed || to == kInError; |
| |
| case kIsPlaying: |
| return to == kIsPlaying || to == kIsStopped || |
| to == kIsClosed || to == kInError; |
| |
| case kIsStopped: |
| return to == kIsPlaying || to == kIsStopped || |
| to == kIsClosed || to == kInError; |
| |
| case kInError: |
| return to == kIsClosed || to == kInError; |
| |
| case kIsClosed: |
| default: |
| return false; |
| } |
| } |
| |
| AlsaPcmOutputStream::InternalState |
| AlsaPcmOutputStream::TransitionTo(InternalState to) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!CanTransitionTo(to)) { |
| NOTREACHED() << "Cannot transition from: " << state_ << " to: " << to; |
| state_ = kInError; |
| } else { |
| state_ = to; |
| } |
| return state_; |
| } |
| |
| AlsaPcmOutputStream::InternalState AlsaPcmOutputStream::state() { |
| return state_; |
| } |
| |
| int AlsaPcmOutputStream::RunDataCallback(base::TimeDelta delay, |
| base::TimeTicks delay_timestamp, |
| AudioBus* audio_bus) { |
| TRACE_EVENT0("audio", "AlsaPcmOutputStream::RunDataCallback"); |
| |
| if (source_callback_) |
| return source_callback_->OnMoreData(delay, delay_timestamp, 0, audio_bus); |
| |
| return 0; |
| } |
| |
| void AlsaPcmOutputStream::RunErrorCallback(int code) { |
| // TODO(dalecurtis): Consider sending a translated |code| value. |
| if (source_callback_) |
| source_callback_->OnError(AudioSourceCallback::ErrorType::kUnknown); |
| } |
| |
| // Changes the AudioSourceCallback to proxy calls to. Pass in nullptr to |
| // release ownership of the currently registered callback. |
| void AlsaPcmOutputStream::set_source_callback(AudioSourceCallback* callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| source_callback_ = callback; |
| } |
| |
| } // namespace media |