| // 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/linux/alsa_input.h" |
| |
| #include "base/basictypes.h" |
| #include "base/bind.h" |
| #include "base/logging.h" |
| #include "base/message_loop.h" |
| #include "base/time.h" |
| #include "media/audio/audio_manager.h" |
| #include "media/audio/linux/alsa_output.h" |
| #include "media/audio/linux/alsa_util.h" |
| #include "media/audio/linux/alsa_wrapper.h" |
| #include "media/audio/linux/audio_manager_linux.h" |
| |
| namespace media { |
| |
| static const int kNumPacketsInRingBuffer = 3; |
| |
| static const char kDefaultDevice1[] = "default"; |
| static const char kDefaultDevice2[] = "plug:default"; |
| |
| const char* AlsaPcmInputStream::kAutoSelectDevice = ""; |
| |
| AlsaPcmInputStream::AlsaPcmInputStream(AudioManagerLinux* audio_manager, |
| const std::string& device_name, |
| const AudioParameters& params, |
| AlsaWrapper* wrapper) |
| : audio_manager_(audio_manager), |
| device_name_(device_name), |
| params_(params), |
| bytes_per_buffer_(params.frames_per_buffer() * |
| (params.channels() * params.bits_per_sample()) / 8), |
| wrapper_(wrapper), |
| buffer_duration_ms_( |
| (params.frames_per_buffer() * base::Time::kMillisecondsPerSecond) / |
| params.sample_rate()), |
| callback_(NULL), |
| device_handle_(NULL), |
| mixer_handle_(NULL), |
| mixer_element_handle_(NULL), |
| ALLOW_THIS_IN_INITIALIZER_LIST(weak_factory_(this)), |
| read_callback_behind_schedule_(false) { |
| } |
| |
| AlsaPcmInputStream::~AlsaPcmInputStream() {} |
| |
| bool AlsaPcmInputStream::Open() { |
| if (device_handle_) |
| return false; // Already open. |
| |
| snd_pcm_format_t pcm_format = alsa_util::BitsToFormat( |
| params_.bits_per_sample()); |
| if (pcm_format == SND_PCM_FORMAT_UNKNOWN) { |
| LOG(WARNING) << "Unsupported bits per sample: " |
| << params_.bits_per_sample(); |
| return false; |
| } |
| |
| uint32 latency_us = buffer_duration_ms_ * kNumPacketsInRingBuffer * |
| base::Time::kMicrosecondsPerMillisecond; |
| |
| // Use the same minimum required latency as output. |
| latency_us = std::max(latency_us, AlsaPcmOutputStream::kMinLatencyMicros); |
| |
| if (device_name_ == kAutoSelectDevice) { |
| const char* device_names[] = { kDefaultDevice1, kDefaultDevice2 }; |
| for (size_t i = 0; i < arraysize(device_names); ++i) { |
| device_handle_ = alsa_util::OpenCaptureDevice( |
| wrapper_, device_names[i], params_.channels(), |
| params_.sample_rate(), pcm_format, latency_us); |
| |
| if (device_handle_) { |
| device_name_ = device_names[i]; |
| break; |
| } |
| } |
| } else { |
| device_handle_ = alsa_util::OpenCaptureDevice(wrapper_, |
| device_name_.c_str(), |
| params_.channels(), |
| params_.sample_rate(), |
| pcm_format, latency_us); |
| } |
| |
| if (device_handle_) { |
| audio_buffer_.reset(new uint8[bytes_per_buffer_]); |
| |
| // Open the microphone mixer. |
| mixer_handle_ = alsa_util::OpenMixer(wrapper_, device_name_); |
| if (mixer_handle_) { |
| mixer_element_handle_ = alsa_util::LoadCaptureMixerElement( |
| wrapper_, mixer_handle_); |
| } |
| } |
| |
| return device_handle_ != NULL; |
| } |
| |
| void AlsaPcmInputStream::Start(AudioInputCallback* callback) { |
| DCHECK(!callback_ && callback); |
| callback_ = callback; |
| int error = wrapper_->PcmPrepare(device_handle_); |
| if (error < 0) { |
| HandleError("PcmPrepare", error); |
| } else { |
| error = wrapper_->PcmStart(device_handle_); |
| if (error < 0) |
| HandleError("PcmStart", error); |
| } |
| |
| if (error < 0) { |
| callback_ = NULL; |
| } else { |
| // We start reading data half |buffer_duration_ms_| later than when the |
| // buffer might have got filled, to accommodate some delays in the audio |
| // driver. This could also give us a smooth read sequence going forward. |
| base::TimeDelta delay = base::TimeDelta::FromMilliseconds( |
| buffer_duration_ms_ + buffer_duration_ms_ / 2); |
| next_read_time_ = base::Time::Now() + delay; |
| MessageLoop::current()->PostDelayedTask( |
| FROM_HERE, |
| base::Bind(&AlsaPcmInputStream::ReadAudio, weak_factory_.GetWeakPtr()), |
| delay); |
| |
| audio_manager_->IncreaseActiveInputStreamCount(); |
| } |
| } |
| |
| bool AlsaPcmInputStream::Recover(int original_error) { |
| int error = wrapper_->PcmRecover(device_handle_, original_error, 1); |
| if (error < 0) { |
| // Docs say snd_pcm_recover returns the original error if it is not one |
| // of the recoverable ones, so this log message will probably contain the |
| // same error twice. |
| LOG(WARNING) << "Unable to recover from \"" |
| << wrapper_->StrError(original_error) << "\": " |
| << wrapper_->StrError(error); |
| return false; |
| } |
| |
| if (original_error == -EPIPE) { // Buffer underrun/overrun. |
| // For capture streams we have to repeat the explicit start() to get |
| // data flowing again. |
| error = wrapper_->PcmStart(device_handle_); |
| if (error < 0) { |
| HandleError("PcmStart", error); |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| snd_pcm_sframes_t AlsaPcmInputStream::GetCurrentDelay() { |
| snd_pcm_sframes_t delay = -1; |
| |
| int error = wrapper_->PcmDelay(device_handle_, &delay); |
| if (error < 0) |
| Recover(error); |
| |
| // snd_pcm_delay() may not work in the beginning of the stream. In this case |
| // return delay of data we know currently is in the ALSA's buffer. |
| if (delay < 0) |
| delay = wrapper_->PcmAvailUpdate(device_handle_); |
| |
| return delay; |
| } |
| |
| void AlsaPcmInputStream::ReadAudio() { |
| DCHECK(callback_); |
| |
| snd_pcm_sframes_t frames = wrapper_->PcmAvailUpdate(device_handle_); |
| if (frames < 0) { // Potentially recoverable error? |
| LOG(WARNING) << "PcmAvailUpdate(): " << wrapper_->StrError(frames); |
| Recover(frames); |
| } |
| |
| if (frames < params_.frames_per_buffer()) { |
| // Not enough data yet or error happened. In both cases wait for a very |
| // small duration before checking again. |
| // Even Though read callback was behind schedule, there is no data, so |
| // reset the next_read_time_. |
| if (read_callback_behind_schedule_) { |
| next_read_time_ = base::Time::Now(); |
| read_callback_behind_schedule_ = false; |
| } |
| |
| base::TimeDelta next_check_time = base::TimeDelta::FromMilliseconds( |
| buffer_duration_ms_ / 2); |
| MessageLoop::current()->PostDelayedTask( |
| FROM_HERE, |
| base::Bind(&AlsaPcmInputStream::ReadAudio, weak_factory_.GetWeakPtr()), |
| next_check_time); |
| return; |
| } |
| |
| int num_buffers = frames / params_.frames_per_buffer(); |
| uint32 hardware_delay_bytes = |
| static_cast<uint32>(GetCurrentDelay() * params_.GetBytesPerFrame()); |
| double normalized_volume = 0.0; |
| |
| // Update the AGC volume level once every second. Note that, |volume| is |
| // also updated each time SetVolume() is called through IPC by the |
| // render-side AGC. |
| QueryAgcVolume(&normalized_volume); |
| |
| while (num_buffers--) { |
| int frames_read = wrapper_->PcmReadi(device_handle_, audio_buffer_.get(), |
| params_.frames_per_buffer()); |
| if (frames_read == params_.frames_per_buffer()) { |
| callback_->OnData(this, audio_buffer_.get(), bytes_per_buffer_, |
| hardware_delay_bytes, normalized_volume); |
| } else { |
| LOG(WARNING) << "PcmReadi returning less than expected frames: " |
| << frames_read << " vs. " << params_.frames_per_buffer() |
| << ". Dropping this buffer."; |
| } |
| } |
| |
| next_read_time_ += base::TimeDelta::FromMilliseconds(buffer_duration_ms_); |
| base::TimeDelta delay = next_read_time_ - base::Time::Now(); |
| if (delay < base::TimeDelta()) { |
| LOG(WARNING) << "Audio read callback behind schedule by " |
| << (buffer_duration_ms_ - delay.InMilliseconds()) |
| << " (ms)."; |
| // Read callback is behind schedule. Assuming there is data pending in |
| // the soundcard, invoke the read callback immediate in order to catch up. |
| read_callback_behind_schedule_ = true; |
| delay = base::TimeDelta(); |
| } |
| |
| MessageLoop::current()->PostDelayedTask( |
| FROM_HERE, |
| base::Bind(&AlsaPcmInputStream::ReadAudio, weak_factory_.GetWeakPtr()), |
| delay); |
| } |
| |
| void AlsaPcmInputStream::Stop() { |
| if (!device_handle_ || !callback_) |
| return; |
| |
| // Stop is always called before Close. In case of error, this will be |
| // also called when closing the input controller. |
| audio_manager_->DecreaseActiveInputStreamCount(); |
| |
| weak_factory_.InvalidateWeakPtrs(); // Cancel the next scheduled read. |
| int error = wrapper_->PcmDrop(device_handle_); |
| if (error < 0) |
| HandleError("PcmDrop", error); |
| } |
| |
| void AlsaPcmInputStream::Close() { |
| if (device_handle_) { |
| weak_factory_.InvalidateWeakPtrs(); // Cancel the next scheduled read. |
| int error = alsa_util::CloseDevice(wrapper_, device_handle_); |
| if (error < 0) |
| HandleError("PcmClose", error); |
| |
| if (mixer_handle_) |
| alsa_util::CloseMixer(wrapper_, mixer_handle_, device_name_); |
| |
| audio_buffer_.reset(); |
| device_handle_ = NULL; |
| mixer_handle_ = NULL; |
| mixer_element_handle_ = NULL; |
| |
| if (callback_) |
| callback_->OnClose(this); |
| } |
| |
| audio_manager_->ReleaseInputStream(this); |
| } |
| |
| double AlsaPcmInputStream::GetMaxVolume() { |
| if (!mixer_handle_ || !mixer_element_handle_) { |
| DLOG(WARNING) << "GetMaxVolume is not supported for " << device_name_; |
| return 0.0; |
| } |
| |
| if (!wrapper_->MixerSelemHasCaptureVolume(mixer_element_handle_)) { |
| DLOG(WARNING) << "Unsupported microphone volume for " << device_name_; |
| return 0.0; |
| } |
| |
| long min = 0; |
| long max = 0; |
| if (wrapper_->MixerSelemGetCaptureVolumeRange(mixer_element_handle_, |
| &min, |
| &max)) { |
| DLOG(WARNING) << "Unsupported max microphone volume for " << device_name_; |
| return 0.0; |
| } |
| DCHECK(min == 0); |
| DCHECK(max > 0); |
| |
| return static_cast<double>(max); |
| } |
| |
| void AlsaPcmInputStream::SetVolume(double volume) { |
| if (!mixer_handle_ || !mixer_element_handle_) { |
| DLOG(WARNING) << "SetVolume is not supported for " << device_name_; |
| return; |
| } |
| |
| int error = wrapper_->MixerSelemSetCaptureVolumeAll( |
| mixer_element_handle_, static_cast<long>(volume)); |
| if (error < 0) { |
| DLOG(WARNING) << "Unable to set volume for " << device_name_; |
| } |
| |
| // Update the AGC volume level based on the last setting above. Note that, |
| // the volume-level resolution is not infinite and it is therefore not |
| // possible to assume that the volume provided as input parameter can be |
| // used directly. Instead, a new query to the audio hardware is required. |
| // This method does nothing if AGC is disabled. |
| UpdateAgcVolume(); |
| } |
| |
| double AlsaPcmInputStream::GetVolume() { |
| if (!mixer_handle_ || !mixer_element_handle_) { |
| DLOG(WARNING) << "GetVolume is not supported for " << device_name_; |
| return 0.0; |
| } |
| |
| long current_volume = 0; |
| int error = wrapper_->MixerSelemGetCaptureVolume( |
| mixer_element_handle_, static_cast<snd_mixer_selem_channel_id_t>(0), |
| ¤t_volume); |
| if (error < 0) { |
| DLOG(WARNING) << "Unable to get volume for " << device_name_; |
| return 0.0; |
| } |
| |
| return static_cast<double>(current_volume); |
| } |
| |
| void AlsaPcmInputStream::HandleError(const char* method, int error) { |
| LOG(WARNING) << method << ": " << wrapper_->StrError(error); |
| callback_->OnError(this, error); |
| } |
| |
| } // namespace media |