| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "media/audio/cras/cras_input.h" |
| |
| #include <inttypes.h> |
| #include <math.h> |
| |
| #include <algorithm> |
| #include <ctime> |
| |
| #include "base/files/file_util.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/time/time.h" |
| #include "media/audio/audio_device_description.h" |
| #include "media/audio/cras/audio_manager_cras_base.h" |
| #include "media/base/audio_timestamp_helper.h" |
| |
| namespace media { |
| |
| namespace { |
| |
| // Used to log errors in `CrasInputStream::Open`. |
| enum class StreamOpenResult { |
| kCallbackOpenSuccess = 0, |
| kCallbackOpenClientAlreadyOpen = 1, |
| kCallbackOpenUnsupportedAudioFrequency = 2, |
| kCallbackOpenUnsupportedAudioFormat = 3, |
| kCallbackOpenCrasClientCreationFailed = 4, |
| kCallbackOpenCannotConnectToCrasClient = 5, |
| kCallbackOpenCannotRunCrasClient = 6, |
| kCallbackOpenCannotSynchronizeData = 7, |
| kCallbackOpenCannotFindLoopbackDevice = 8, |
| kMaxValue = kCallbackOpenCannotFindLoopbackDevice |
| }; |
| |
| // Used to log errors in `CrasInputStream::Start`. |
| enum class StreamStartResult { |
| kCallbackStartSuccess = 0, |
| kCallbackStartErrorCreatingStreamParameters = 1, |
| kCallbackStartErrorSettingUpStreamParameters = 2, |
| kCallbackStartErrorSettingUpChannelLayout = 3, |
| kCallbackStartFailedAddingStream = 4, |
| kMaxValue = kCallbackStartFailedAddingStream |
| }; |
| |
| void ReportStreamOpenResult(StreamOpenResult result) { |
| base::UmaHistogramEnumeration("Media.Audio.CrasInputStreamOpenSuccess", |
| result); |
| } |
| |
| void ReportStreamStartResult(StreamStartResult result) { |
| base::UmaHistogramEnumeration("Media.Audio.CrasInputStreamStartSuccess", |
| result); |
| } |
| |
| void ReportNotifyStreamErrors(int err) { |
| base::UmaHistogramSparse("Media.Audio.CrasInputStreamNotifyStreamError", err); |
| } |
| |
| } // namespace |
| |
| CrasInputStream::CrasInputStream(const AudioParameters& params, |
| AudioManagerCrasBase* manager, |
| const std::string& device_id, |
| const AudioManager::LogCallback& log_callback) |
| : audio_manager_(manager), |
| params_(params), |
| is_loopback_(AudioDeviceDescription::IsLoopbackDevice(device_id)), |
| is_loopback_without_chrome_( |
| device_id == AudioDeviceDescription::kLoopbackWithoutChromeId), |
| mute_system_audio_(device_id == |
| AudioDeviceDescription::kLoopbackWithMuteDeviceId), |
| #if DCHECK_IS_ON() |
| recording_enabled_(false), |
| #endif |
| glitch_reporter_(SystemGlitchReporter::StreamType::kCapture), |
| log_callback_(std::move(log_callback)), |
| peak_detector_(base::BindRepeating(&AudioManager::TraceAmplitudePeak, |
| base::Unretained(audio_manager_), |
| /*trace_start=*/true)) { |
| DCHECK(audio_manager_); |
| audio_bus_ = AudioBus::Create(params_); |
| if (!audio_manager_->IsDefault(device_id, true)) { |
| uint64_t cras_node_id; |
| base::StringToUint64(device_id, &cras_node_id); |
| pin_device_ = dev_index_of(cras_node_id); |
| } |
| } |
| |
| CrasInputStream::~CrasInputStream() { |
| DCHECK(!client_); |
| } |
| |
| AudioInputStream::OpenOutcome CrasInputStream::Open() { |
| if (client_) { |
| NOTREACHED() << "CrasInputStream already open"; |
| ReportStreamOpenResult(StreamOpenResult::kCallbackOpenClientAlreadyOpen); |
| return OpenOutcome::kAlreadyOpen; |
| } |
| |
| // Sanity check input values. |
| if (params_.sample_rate() <= 0) { |
| DLOG(WARNING) << "Unsupported audio frequency."; |
| ReportStreamOpenResult( |
| StreamOpenResult::kCallbackOpenUnsupportedAudioFrequency); |
| return OpenOutcome::kFailed; |
| } |
| |
| if (AudioParameters::AUDIO_PCM_LINEAR != params_.format() && |
| AudioParameters::AUDIO_PCM_LOW_LATENCY != params_.format()) { |
| DLOG(WARNING) << "Unsupported audio format."; |
| ReportStreamOpenResult( |
| StreamOpenResult::kCallbackOpenUnsupportedAudioFormat); |
| return OpenOutcome::kFailed; |
| } |
| |
| // Create the client and connect to the CRAS server. |
| client_ = libcras_client_create(); |
| if (!client_) { |
| DLOG(WARNING) << "Couldn't create CRAS client.\n"; |
| ReportStreamOpenResult( |
| StreamOpenResult::kCallbackOpenCrasClientCreationFailed); |
| client_ = NULL; |
| return OpenOutcome::kFailed; |
| } |
| |
| if (libcras_client_connect(client_)) { |
| DLOG(WARNING) << "Couldn't connect CRAS client.\n"; |
| ReportStreamOpenResult( |
| StreamOpenResult::kCallbackOpenCannotConnectToCrasClient); |
| libcras_client_destroy(client_); |
| client_ = NULL; |
| return OpenOutcome::kFailed; |
| } |
| |
| // Then start running the client. |
| if (libcras_client_run_thread(client_)) { |
| DLOG(WARNING) << "Couldn't run CRAS client.\n"; |
| ReportStreamOpenResult(StreamOpenResult::kCallbackOpenCannotRunCrasClient); |
| libcras_client_destroy(client_); |
| client_ = NULL; |
| return OpenOutcome::kFailed; |
| } |
| |
| if (is_loopback_) { |
| if (libcras_client_connected_wait(client_) < 0) { |
| DLOG(WARNING) << "Couldn't synchronize data."; |
| // TODO(chinyue): Add a DestroyClientOnError method to de-duplicate the |
| // cleanup code. |
| ReportStreamOpenResult( |
| StreamOpenResult::kCallbackOpenCannotSynchronizeData); |
| libcras_client_destroy(client_); |
| client_ = NULL; |
| return OpenOutcome::kFailed; |
| } |
| |
| int rc; |
| if (is_loopback_without_chrome_) { |
| uint32_t client_types = 0; |
| client_types |= 1 << CRAS_CLIENT_TYPE_CHROME; |
| client_types |= 1 << CRAS_CLIENT_TYPE_LACROS; |
| client_types = ~client_types; |
| rc = pin_device_ = libcras_client_get_floop_dev_idx_by_client_types( |
| client_, client_types); |
| } else { |
| rc = libcras_client_get_loopback_dev_idx(client_, &pin_device_); |
| } |
| if (rc < 0) { |
| DLOG(WARNING) << "Couldn't find CRAS loopback device " |
| << (is_loopback_without_chrome_ ? " for flexible loopback." |
| : " for full loopback."); |
| ReportStreamOpenResult( |
| StreamOpenResult::kCallbackOpenCannotFindLoopbackDevice); |
| libcras_client_destroy(client_); |
| client_ = NULL; |
| return OpenOutcome::kFailed; |
| } |
| } |
| |
| ReportStreamOpenResult(StreamOpenResult::kCallbackOpenSuccess); |
| return OpenOutcome::kSuccess; |
| } |
| |
| void CrasInputStream::Close() { |
| Stop(); |
| |
| if (client_) { |
| libcras_client_stop(client_); |
| libcras_client_destroy(client_); |
| client_ = NULL; |
| } |
| |
| // Signal to the manager that we're closed and can be removed. |
| // Should be last call in the method as it deletes "this". |
| audio_manager_->ReleaseInputStream(this); |
| } |
| |
| inline bool CrasInputStream::UseCrasAec() const { |
| return params_.effects() & AudioParameters::ECHO_CANCELLER; |
| } |
| |
| inline bool CrasInputStream::UseCrasNs() const { |
| return params_.effects() & AudioParameters::NOISE_SUPPRESSION; |
| } |
| |
| inline bool CrasInputStream::UseCrasAgc() const { |
| return params_.effects() & AudioParameters::AUTOMATIC_GAIN_CONTROL; |
| } |
| |
| inline bool CrasInputStream::DspBasedAecIsAllowed() const { |
| return params_.effects() & AudioParameters::ALLOW_DSP_ECHO_CANCELLER; |
| } |
| |
| inline bool CrasInputStream::DspBasedNsIsAllowed() const { |
| return params_.effects() & AudioParameters::ALLOW_DSP_NOISE_SUPPRESSION; |
| } |
| |
| inline bool CrasInputStream::DspBasedAgcIsAllowed() const { |
| return params_.effects() & AudioParameters::ALLOW_DSP_AUTOMATIC_GAIN_CONTROL; |
| } |
| |
| void CrasInputStream::Start(AudioInputCallback* callback) { |
| DCHECK(client_); |
| DCHECK(callback); |
| |
| // Channel map to CRAS_CHANNEL, values in the same order of |
| // corresponding source in Chromium defined Channels. |
| static const int kChannelMap[] = { |
| CRAS_CH_FL, CRAS_CH_FR, CRAS_CH_FC, CRAS_CH_LFE, CRAS_CH_RL, CRAS_CH_RR, |
| CRAS_CH_FLC, CRAS_CH_FRC, CRAS_CH_RC, CRAS_CH_SL, CRAS_CH_SR}; |
| static_assert(std::size(kChannelMap) == CHANNELS_MAX + 1, |
| "kChannelMap array size should match"); |
| |
| // If already playing, stop before re-starting. |
| if (started_) { |
| return; |
| } |
| |
| StartAgc(); |
| |
| callback_ = callback; |
| |
| CRAS_STREAM_TYPE type = CRAS_STREAM_TYPE_DEFAULT; |
| uint32_t flags = 0; |
| if (params_.effects() & AudioParameters::PlatformEffectsMask::HOTWORD) { |
| flags = HOTWORD_STREAM; |
| type = CRAS_STREAM_TYPE_SPEECH_RECOGNITION; |
| } |
| |
| unsigned int frames_per_packet = params_.frames_per_buffer(); |
| struct libcras_stream_params* stream_params = libcras_stream_params_create(); |
| if (!stream_params) { |
| DLOG(ERROR) << "Error creating stream params"; |
| ReportStreamStartResult( |
| StreamStartResult::kCallbackStartErrorCreatingStreamParameters); |
| callback_->OnError(); |
| callback_ = NULL; |
| return; |
| } |
| |
| int rc = libcras_stream_params_set( |
| stream_params, stream_direction_, frames_per_packet, frames_per_packet, |
| type, audio_manager_->GetClientType(), flags, this, |
| CrasInputStream::SamplesReady, CrasInputStream::StreamError, |
| params_.sample_rate(), SND_PCM_FORMAT_S16, params_.channels()); |
| |
| if (rc) { |
| DLOG(WARNING) << "Error setting up stream parameters."; |
| ReportStreamStartResult( |
| StreamStartResult::kCallbackStartErrorSettingUpStreamParameters); |
| callback_->OnError(); |
| callback_ = NULL; |
| libcras_stream_params_destroy(stream_params); |
| return; |
| } |
| |
| // Initialize channel layout to all -1 to indicate that none of |
| // the channels is set in the layout. |
| int8_t layout[CRAS_CH_MAX]; |
| for (size_t i = 0; i < std::size(layout); ++i) { |
| layout[i] = -1; |
| } |
| |
| // Converts to CRAS defined channels. ChannelOrder will return -1 |
| // for channels that are not present in params_.channel_layout(). |
| for (size_t i = 0; i < std::size(kChannelMap); ++i) { |
| layout[kChannelMap[i]] = |
| ChannelOrder(params_.channel_layout(), static_cast<Channels>(i)); |
| } |
| |
| rc = libcras_stream_params_set_channel_layout(stream_params, CRAS_CH_MAX, |
| layout); |
| if (rc) { |
| DLOG(WARNING) << "Error setting up the channel layout."; |
| ReportStreamStartResult( |
| StreamStartResult::kCallbackStartErrorSettingUpChannelLayout); |
| callback_->OnError(); |
| callback_ = NULL; |
| libcras_stream_params_destroy(stream_params); |
| return; |
| } |
| |
| if (UseCrasAec()) { |
| libcras_stream_params_enable_aec(stream_params); |
| } |
| |
| if (UseCrasNs()) { |
| libcras_stream_params_enable_ns(stream_params); |
| } |
| |
| if (UseCrasAgc()) { |
| libcras_stream_params_enable_agc(stream_params); |
| } |
| |
| if (DspBasedAecIsAllowed()) { |
| libcras_stream_params_allow_aec_on_dsp(stream_params); |
| } |
| |
| if (DspBasedNsIsAllowed()) { |
| libcras_stream_params_allow_ns_on_dsp(stream_params); |
| } |
| |
| if (DspBasedAgcIsAllowed()) { |
| libcras_stream_params_allow_agc_on_dsp(stream_params); |
| } |
| |
| // Adding the stream will start the audio callbacks. |
| if (libcras_client_add_pinned_stream(client_, pin_device_, &stream_id_, |
| stream_params)) { |
| DLOG(WARNING) << "Failed to add the stream."; |
| ReportStreamStartResult( |
| StreamStartResult::kCallbackStartFailedAddingStream); |
| callback_->OnError(); |
| callback_ = NULL; |
| } |
| |
| // Mute system audio if requested. |
| if (mute_system_audio_) { |
| int muted; |
| libcras_client_get_system_muted(client_, &muted); |
| if (!muted) { |
| libcras_client_set_system_mute(client_, 1); |
| } |
| mute_done_ = true; |
| } |
| |
| // Done with config params. |
| libcras_stream_params_destroy(stream_params); |
| |
| started_ = true; |
| |
| audio_manager_->RegisterSystemAecDumpSource(this); |
| |
| ReportStreamStartResult(StreamStartResult::kCallbackStartSuccess); |
| } |
| |
| void CrasInputStream::Stop() { |
| if (!client_) { |
| return; |
| } |
| |
| if (!callback_ || !started_) { |
| return; |
| } |
| |
| audio_manager_->DeregisterSystemAecDumpSource(this); |
| |
| if (mute_system_audio_ && mute_done_) { |
| libcras_client_set_system_mute(client_, 0); |
| mute_done_ = false; |
| } |
| |
| StopAgc(); |
| |
| // Removing the stream from the client stops audio. |
| libcras_client_rm_stream(client_, stream_id_); |
| |
| ReportAndResetStats(); |
| |
| started_ = false; |
| callback_ = NULL; |
| } |
| |
| // Static callback asking for samples. Run on high priority thread. |
| int CrasInputStream::SamplesReady(struct libcras_stream_cb_data* data) { |
| unsigned int frames; |
| uint8_t* buf; |
| struct timespec latency; |
| void* usr_arg; |
| uint32_t overrun_frames = 0; |
| struct timespec dropped_samples_duration_ts; |
| base::TimeDelta dropped_samples_duration; |
| |
| libcras_stream_cb_data_get_frames(data, &frames); |
| libcras_stream_cb_data_get_buf(data, &buf); |
| libcras_stream_cb_data_get_latency(data, &latency); |
| libcras_stream_cb_data_get_usr_arg(data, &usr_arg); |
| CrasInputStream* me = static_cast<CrasInputStream*>(usr_arg); |
| me->ReadAudio(frames, buf, &latency); |
| // Audio glitches are checked every callback. |
| libcras_stream_cb_data_get_overrun_frames(data, &overrun_frames); |
| libcras_stream_cb_data_get_dropped_samples_duration( |
| data, &dropped_samples_duration_ts); |
| dropped_samples_duration = |
| base::TimeDelta::FromTimeSpec(dropped_samples_duration_ts); |
| me->CalculateAudioGlitches(overrun_frames, dropped_samples_duration); |
| return frames; |
| } |
| |
| // Static callback for stream errors. |
| int CrasInputStream::StreamError(cras_client* client, |
| cras_stream_id_t stream_id, |
| int err, |
| void* arg) { |
| CrasInputStream* me = static_cast<CrasInputStream*>(arg); |
| me->NotifyStreamError(err); |
| return 0; |
| } |
| |
| void CrasInputStream::ReadAudio(size_t frames, |
| uint8_t* buffer, |
| const timespec* latency_ts) { |
| DCHECK(callback_); |
| |
| // 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); |
| |
| const base::TimeDelta delay = |
| std::max(base::TimeDelta::FromTimeSpec(*latency_ts), base::TimeDelta()); |
| |
| // The delay says how long ago the capture was, so we subtract the delay from |
| // Now() to find the capture time. |
| const base::TimeTicks capture_time = base::TimeTicks::Now() - delay; |
| |
| audio_bus_->FromInterleaved<SignedInt16SampleTypeTraits>( |
| reinterpret_cast<int16_t*>(buffer), audio_bus_->frames()); |
| |
| peak_detector_.FindPeak(audio_bus_.get()); |
| |
| callback_->OnData(audio_bus_.get(), capture_time, normalized_volume, {}); |
| } |
| |
| void CrasInputStream::NotifyStreamError(int err) { |
| ReportNotifyStreamErrors(err); |
| if (callback_) { |
| callback_->OnError(); |
| } |
| } |
| |
| double CrasInputStream::GetMaxVolume() { |
| return 1.0f; |
| } |
| |
| void CrasInputStream::SetVolume(double volume) { |
| DCHECK(client_); |
| |
| // Set the volume ratio to CRAS's softare and stream specific gain. |
| input_volume_ = volume; |
| libcras_client_set_stream_volume(client_, stream_id_, input_volume_); |
| |
| // 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 CrasInputStream::GetVolume() { |
| if (!client_) { |
| return 0.0; |
| } |
| |
| return input_volume_; |
| } |
| |
| bool CrasInputStream::IsMuted() { |
| int muted = 0; |
| libcras_client_get_system_capture_muted(client_, &muted); |
| return static_cast<bool>(muted); |
| } |
| |
| void CrasInputStream::SetOutputDeviceForAec( |
| const std::string& output_device_id) { |
| DCHECK(client_); |
| |
| int echo_ref_id; |
| |
| // Default device means to just use the system default output as AEC |
| // reference. CRAS server side requires passing NO_DEVICE in that case. |
| if (AudioDeviceDescription::IsDefaultDevice(output_device_id)) { |
| echo_ref_id = NO_DEVICE; |
| } else { |
| uint64_t cras_node_id; |
| base::StringToUint64(output_device_id, &cras_node_id); |
| echo_ref_id = dev_index_of(cras_node_id); |
| } |
| libcras_client_set_aec_ref(client_, stream_id_, echo_ref_id); |
| } |
| |
| void CrasInputStream::StartAecdump(base::File file) { |
| FILE* stream = base::FileToFILE(std::move(file), "w"); |
| if (!client_) { |
| return; |
| } |
| #if DCHECK_IS_ON() |
| DCHECK(!recording_enabled_); |
| recording_enabled_ = true; |
| #endif |
| |
| libcras_client_set_aec_dump(client_, stream_id_, /*start=*/1, fileno(stream)); |
| } |
| |
| void CrasInputStream::StopAecdump() { |
| if (!client_) { |
| return; |
| } |
| #if DCHECK_IS_ON() |
| DCHECK(recording_enabled_); |
| recording_enabled_ = false; |
| #endif |
| libcras_client_set_aec_dump(client_, stream_id_, /*start=*/0, /*fd=*/-1); |
| } |
| |
| void CrasInputStream::ReportAndResetStats() { |
| SystemGlitchReporter::Stats stats = |
| glitch_reporter_.GetLongTermStatsAndReset(); |
| |
| std::string log_message = base::StringPrintf( |
| "CRAS in: (num_glitches_detected=[%d], cumulative_audio_lost=[%" PRId64 |
| " ms],largest_glitch=[%" PRId64 " ms])", |
| stats.glitches_detected, stats.total_glitch_duration.InMilliseconds(), |
| stats.largest_glitch_duration.InMilliseconds()); |
| |
| log_callback_.Run(log_message); |
| if (stats.glitches_detected != 0) { |
| DLOG(WARNING) << log_message; |
| } |
| last_overrun_frames_ = 0; |
| last_dropped_samples_duration_ = base::TimeDelta(); |
| } |
| |
| void CrasInputStream::CalculateAudioGlitches( |
| uint32_t overrun_frames, |
| base::TimeDelta dropped_samples_duration) { |
| // |overrun_frames| obtained from callback is the cumulative value of the |
| // overwritten frames of the whole stream. Calculate the overrun frames this |
| // callback and convert it to base::TimeDelta. |
| DCHECK_GE(overrun_frames, last_overrun_frames_); |
| uint32_t overrun_frames_this_callback = overrun_frames - last_overrun_frames_; |
| base::TimeDelta overrun_glitch_duration = AudioTimestampHelper::FramesToTime( |
| overrun_frames_this_callback, params_.sample_rate()); |
| |
| // |dropped_samples_duration| obtained from callback is the cumulative value |
| // of the dropped audio samples of the whole stream. Calculate the dropped |
| // audio sample duration this callback. |
| DCHECK_GE(dropped_samples_duration, last_dropped_samples_duration_); |
| base::TimeDelta dropped_samples_glitch_duration = |
| dropped_samples_duration - last_dropped_samples_duration_; |
| |
| glitch_reporter_.UpdateStats(overrun_glitch_duration + |
| dropped_samples_glitch_duration); |
| last_overrun_frames_ = overrun_frames; |
| last_dropped_samples_duration_ = dropped_samples_duration; |
| } |
| |
| } // namespace media |