blob: 0b0719b7e1ca3c02e575f73182132383b45feada [file]
// Copyright 2020 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 "chrome/services/speech/speech_recognition_recognizer_impl.h"
#include <string>
#include <utility>
#include "base/bind.h"
#include "base/containers/span.h"
#include "base/files/file_util.h"
#include "base/metrics/histogram_functions.h"
#include "chrome/services/speech/soda/soda_client.h"
#include "google_apis/google_api_keys.h"
#include "media/base/audio_buffer.h"
#include "media/base/audio_sample_types.h"
#include "media/base/audio_timestamp_helper.h"
#include "media/base/bind_to_current_loop.h"
#include "media/base/limits.h"
#include "media/base/media_switches.h"
#include "media/mojo/common/media_type_converters.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
namespace speech {
constexpr char kInvalidAudioDataError[] = "Invalid audio data received.";
// static
const char
SpeechRecognitionRecognizerImpl::kCaptionBubbleVisibleHistogramName[] =
"Accessibility.LiveCaption.Duration.CaptionBubbleVisible";
// static
const char
SpeechRecognitionRecognizerImpl::kCaptionBubbleHiddenHistogramName[] =
"Accessibility.LiveCaption.Duration.CaptionBubbleHidden";
namespace {
// Callback executed by the SODA library on a speech recognition event. The
// callback handle is a void pointer to the SpeechRecognitionRecognizerImpl that
// owns the SODA instance. SpeechRecognitionRecognizerImpl owns the SodaClient
// which owns the instance of SODA and their sequential destruction order
// ensures that this callback will never be called with an invalid callback
// handle to the SpeechRecognitionRecognizerImpl.
void RecognitionCallback(const char* result,
const bool is_final,
void* callback_handle) {
DCHECK(callback_handle);
static_cast<SpeechRecognitionRecognizerImpl*>(callback_handle)
->recognition_event_callback()
.Run(std::string(result), is_final);
}
} // namespace
SpeechRecognitionRecognizerImpl::~SpeechRecognitionRecognizerImpl() {
RecordDuration();
}
void SpeechRecognitionRecognizerImpl::Create(
mojo::PendingReceiver<media::mojom::SpeechRecognitionRecognizer> receiver,
mojo::PendingRemote<media::mojom::SpeechRecognitionRecognizerClient> remote,
base::WeakPtr<SpeechRecognitionServiceImpl> speech_recognition_service_impl,
const base::FilePath& binary_path,
const base::FilePath& config_path) {
mojo::MakeSelfOwnedReceiver(
std::make_unique<SpeechRecognitionRecognizerImpl>(
std::move(remote), std::move(speech_recognition_service_impl),
binary_path, config_path),
std::move(receiver));
}
bool SpeechRecognitionRecognizerImpl::IsMultichannelSupported() {
if (base::FeatureList::IsEnabled(media::kUseSodaForLiveCaption)) {
return true;
} else {
return false;
}
}
void SpeechRecognitionRecognizerImpl::OnRecognitionEvent(
const std::string& result,
const bool is_final) {
client_remote_->OnSpeechRecognitionRecognitionEvent(
media::mojom::SpeechRecognitionResult::New(result, is_final));
}
SpeechRecognitionRecognizerImpl::SpeechRecognitionRecognizerImpl(
mojo::PendingRemote<media::mojom::SpeechRecognitionRecognizerClient> remote,
base::WeakPtr<SpeechRecognitionServiceImpl> speech_recognition_service_impl,
const base::FilePath& binary_path,
const base::FilePath& config_path)
: client_remote_(std::move(remote)), config_path_(config_path) {
recognition_event_callback_ = media::BindToCurrentLoop(
base::BindRepeating(&SpeechRecognitionRecognizerImpl::OnRecognitionEvent,
weak_factory_.GetWeakPtr()));
enable_soda_ = base::FeatureList::IsEnabled(media::kUseSodaForLiveCaption);
if (enable_soda_) {
DCHECK(base::PathExists(binary_path));
soda_client_ = std::make_unique<soda::SodaClient>(binary_path);
} else {
cloud_client_ = std::make_unique<CloudSpeechRecognitionClient>(
recognition_event_callback(),
std::move(speech_recognition_service_impl));
}
}
void SpeechRecognitionRecognizerImpl::SendAudioToSpeechRecognitionService(
media::mojom::AudioDataS16Ptr buffer) {
int channel_count = buffer->channel_count;
int frame_count = buffer->frame_count;
int sample_rate = buffer->sample_rate;
size_t num_samples = 0;
size_t buffer_size = 0;
// Update watch time durations.
base::TimeDelta duration =
media::AudioTimestampHelper::FramesToTime(frame_count, sample_rate);
if (!caption_bubble_closed_) {
caption_bubble_visible_duration_ += duration;
} else {
caption_bubble_hidden_duration_ += duration;
}
// Verify the channel count.
if (channel_count <= 0 || channel_count > media::limits::kMaxChannels) {
mojo::ReportBadMessage(kInvalidAudioDataError);
return;
}
// Verify and calculate the number of samples.
if (sample_rate <= 0 || frame_count <= 0 ||
!base::CheckMul(frame_count, channel_count).AssignIfValid(&num_samples) ||
num_samples != buffer->data.size()) {
mojo::ReportBadMessage(kInvalidAudioDataError);
return;
}
// Verify and calculate the buffer size.
if (!base::CheckMul(buffer->data.size(), sizeof(buffer->data[0]))
.AssignIfValid(&buffer_size)) {
mojo::ReportBadMessage(kInvalidAudioDataError);
return;
}
if (enable_soda_) {
DCHECK(soda_client_);
DCHECK(base::PathExists(config_path_));
if (!soda_client_->IsInitialized() ||
soda_client_->DidAudioPropertyChange(sample_rate, channel_count)) {
// Initialize the SODA instance.
auto api_key = google_apis::GetSodaAPIKey();
std::string language_pack_directory = config_path_.AsUTF8Unsafe();
SodaConfig config;
config.channel_count = channel_count;
config.sample_rate = sample_rate;
config.language_pack_directory = language_pack_directory.c_str();
config.callback = RecognitionCallback;
config.callback_handle = this;
config.api_key = api_key.c_str();
soda_client_->Reset(config);
}
soda_client_->AddAudio(reinterpret_cast<char*>(buffer->data.data()),
buffer_size);
} else {
DCHECK(cloud_client_);
if (!cloud_client_->IsInitialized() ||
cloud_client_->DidAudioPropertyChange(sample_rate, channel_count)) {
// Initialize the stream.
CloudSpeechConfig config;
config.sample_rate = sample_rate;
config.channel_count = channel_count;
config.language_code = "en-US";
cloud_client_->Initialize(config);
}
cloud_client_->AddAudio(base::span<const char>(
reinterpret_cast<char*>(buffer->data.data()), buffer_size));
}
}
void SpeechRecognitionRecognizerImpl::OnCaptionBubbleClosed() {
caption_bubble_closed_ = true;
}
void SpeechRecognitionRecognizerImpl::AudioReceivedAfterBubbleClosed(
base::TimeDelta duration) {
caption_bubble_hidden_duration_ += duration;
}
void SpeechRecognitionRecognizerImpl::RecordDuration() {
if (caption_bubble_visible_duration_ > base::TimeDelta()) {
base::UmaHistogramMediumTimes(kCaptionBubbleVisibleHistogramName,
caption_bubble_visible_duration_);
}
if (caption_bubble_hidden_duration_ > base::TimeDelta()) {
base::UmaHistogramMediumTimes(kCaptionBubbleHiddenHistogramName,
caption_bubble_hidden_duration_);
}
}
} // namespace speech