| // 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. |
| |
| #include "media/audio/alsa/alsa_input.h" |
| |
| #include <stddef.h> |
| |
| #include "base/bind.h" |
| #include "base/cxx17_backports.h" |
| #include "base/location.h" |
| #include "base/logging.h" |
| #include "base/single_thread_task_runner.h" |
| #include "media/audio/alsa/alsa_output.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/audio/audio_manager.h" |
| |
| namespace media { |
| |
| static const SampleFormat kSampleFormat = kSampleFormatS16; |
| static const snd_pcm_format_t kAlsaSampleFormat = SND_PCM_FORMAT_S16; |
| |
| static const int kNumPacketsInRingBuffer = 3; |
| |
| static const char kDefaultDevice1[] = "default"; |
| static const char kDefaultDevice2[] = "plug:default"; |
| |
| const char AlsaPcmInputStream::kAutoSelectDevice[] = ""; |
| |
| AlsaPcmInputStream::AlsaPcmInputStream(AudioManagerBase* 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.GetBytesPerBuffer(kSampleFormat)), |
| wrapper_(wrapper), |
| buffer_duration_(base::Microseconds( |
| params.frames_per_buffer() * base::Time::kMicrosecondsPerSecond / |
| static_cast<float>(params.sample_rate()))), |
| callback_(nullptr), |
| device_handle_(nullptr), |
| mixer_handle_(nullptr), |
| mixer_element_handle_(nullptr), |
| read_callback_behind_schedule_(false), |
| audio_bus_(AudioBus::Create(params)), |
| capture_thread_("AlsaInput"), |
| running_(false) {} |
| |
| AlsaPcmInputStream::~AlsaPcmInputStream() = default; |
| |
| AudioInputStream::OpenOutcome AlsaPcmInputStream::Open() { |
| if (device_handle_) |
| return OpenOutcome::kAlreadyOpen; |
| |
| uint32_t packet_us = buffer_duration_.InMicroseconds(); |
| uint32_t buffer_us = packet_us * kNumPacketsInRingBuffer; |
| |
| // Use the same minimum required latency as output. |
| buffer_us = std::max(buffer_us, AlsaPcmOutputStream::kMinLatencyMicros); |
| |
| if (device_name_ == kAutoSelectDevice) { |
| const char* device_names[] = { kDefaultDevice1, kDefaultDevice2 }; |
| for (size_t i = 0; i < base::size(device_names); ++i) { |
| device_handle_ = alsa_util::OpenCaptureDevice( |
| wrapper_, device_names[i], params_.channels(), params_.sample_rate(), |
| kAlsaSampleFormat, buffer_us, packet_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(), kAlsaSampleFormat, buffer_us, packet_us); |
| } |
| |
| if (device_handle_) { |
| audio_buffer_.reset(new uint8_t[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_ != nullptr ? OpenOutcome::kSuccess |
| : OpenOutcome::kFailed; |
| } |
| |
| void AlsaPcmInputStream::Start(AudioInputCallback* callback) { |
| DCHECK(!callback_ && callback); |
| callback_ = callback; |
| StartAgc(); |
| 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_ = nullptr; |
| } else { |
| base::Thread::Options options; |
| options.priority = base::ThreadPriority::REALTIME_AUDIO; |
| CHECK(capture_thread_.StartWithOptions(std::move(options))); |
| |
| // We start reading data half |buffer_duration_| 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 = buffer_duration_ + buffer_duration_ / 2; |
| next_read_time_ = base::TimeTicks::Now() + delay; |
| running_ = true; |
| capture_thread_.task_runner()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&AlsaPcmInputStream::ReadAudio, base::Unretained(this)), |
| delay); |
| } |
| } |
| |
| bool AlsaPcmInputStream::Recover(int original_error) { |
| DCHECK(capture_thread_.task_runner()->BelongsToCurrentThread()); |
| 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; |
| } |
| |
| void AlsaPcmInputStream::StopRunningOnCaptureThread() { |
| DCHECK(capture_thread_.IsRunning()); |
| if (!capture_thread_.task_runner()->BelongsToCurrentThread()) { |
| capture_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&AlsaPcmInputStream::StopRunningOnCaptureThread, |
| base::Unretained(this))); |
| return; |
| } |
| running_ = false; |
| } |
| |
| void AlsaPcmInputStream::ReadAudio() { |
| DCHECK(capture_thread_.task_runner()->BelongsToCurrentThread()); |
| DCHECK(callback_); |
| if (!running_) |
| return; |
| |
| 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::TimeTicks::Now(); |
| read_callback_behind_schedule_ = false; |
| } |
| |
| base::TimeDelta next_check_time = buffer_duration_ / 2; |
| capture_thread_.task_runner()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&AlsaPcmInputStream::ReadAudio, base::Unretained(this)), |
| next_check_time); |
| return; |
| } |
| |
| // 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. |
| double normalized_volume = 0.0; |
| GetAgcVolume(&normalized_volume); |
| |
| int num_buffers = frames / params_.frames_per_buffer(); |
| while (num_buffers--) { |
| int frames_read = wrapper_->PcmReadi(device_handle_, audio_buffer_.get(), |
| params_.frames_per_buffer()); |
| if (frames_read == params_.frames_per_buffer()) { |
| audio_bus_->FromInterleaved<SignedInt16SampleTypeTraits>( |
| reinterpret_cast<int16_t*>(audio_buffer_.get()), |
| audio_bus_->frames()); |
| |
| // TODO(dalecurtis): This should probably use snd_pcm_htimestamp() so that |
| // we can have |capture_time| directly instead of computing it as |
| // Now() - available frames. |
| snd_pcm_sframes_t avail_frames = wrapper_->PcmAvailUpdate(device_handle_); |
| if (avail_frames < 0) { |
| LOG(WARNING) << "PcmAvailUpdate(): " |
| << wrapper_->StrError(avail_frames); |
| avail_frames = 0; // Error getting number of avail frames, set it to 0 |
| } |
| base::TimeDelta hardware_delay = base::Seconds( |
| avail_frames / static_cast<double>(params_.sample_rate())); |
| |
| callback_->OnData(audio_bus_.get(), |
| base::TimeTicks::Now() - hardware_delay, |
| normalized_volume); |
| } else if (frames_read < 0) { |
| bool success = Recover(frames_read); |
| LOG(WARNING) << "PcmReadi failed with error " |
| << wrapper_->StrError(frames_read) << ". " |
| << (success ? "Successfully" : "Unsuccessfully") |
| << " recovered."; |
| } else { |
| LOG(WARNING) << "PcmReadi returning less than expected frames: " |
| << frames_read << " vs. " << params_.frames_per_buffer() |
| << ". Dropping this buffer."; |
| } |
| } |
| |
| next_read_time_ += buffer_duration_; |
| base::TimeDelta delay = next_read_time_ - base::TimeTicks::Now(); |
| if (delay < base::TimeDelta()) { |
| DVLOG(1) << "Audio read callback behind schedule by " |
| << (buffer_duration_ - delay).InMicroseconds() |
| << " (us)."; |
| // 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(); |
| } |
| |
| capture_thread_.task_runner()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&AlsaPcmInputStream::ReadAudio, base::Unretained(this)), |
| delay); |
| } |
| |
| void AlsaPcmInputStream::Stop() { |
| if (!device_handle_ || !callback_) |
| return; |
| |
| StopAgc(); |
| |
| StopRunningOnCaptureThread(); |
| capture_thread_.Stop(); |
| int error = wrapper_->PcmDrop(device_handle_); |
| if (error < 0) |
| HandleError("PcmDrop", error); |
| |
| callback_ = nullptr; |
| } |
| |
| void AlsaPcmInputStream::Close() { |
| if (device_handle_) { |
| Stop(); |
| 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_ = nullptr; |
| mixer_handle_ = nullptr; |
| mixer_element_handle_ = nullptr; |
| } |
| |
| 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); |
| } |
| |
| bool AlsaPcmInputStream::IsMuted() { |
| return false; |
| } |
| |
| void AlsaPcmInputStream::SetOutputDeviceForAec( |
| const std::string& output_device_id) { |
| // Not supported. Do nothing. |
| } |
| |
| void AlsaPcmInputStream::HandleError(const char* method, int error) { |
| LOG(WARNING) << method << ": " << wrapper_->StrError(error); |
| if (callback_) |
| callback_->OnError(); |
| } |
| |
| } // namespace media |