// Copyright (c) 2011 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 <string> #include <vector> #include "base/float_util.h" #include "base/json/json_writer.h" #include "base/message_loop.h" #include "base/values.h" #include "chrome/browser/extensions/extension_event_router.h" #include "chrome/browser/extensions/extension_service.h" #include "chrome/browser/extensions/extension_tts_api.h" #include "chrome/browser/profiles/profile.h" namespace util = extension_tts_api_util; namespace { const char kSpeechInterruptedError[] = "Utterance interrupted."; const char kSpeechRemovedFromQueueError[] = "Utterance removed from queue."; const int kSpeechCheckDelayIntervalMs = 100; }; namespace events { const char kOnSpeak[] = "experimental.tts.onSpeak"; const char kOnStop[] = "experimental.tts.onStop"; }; // namespace events // // ExtensionTtsPlatformImpl // std::string ExtensionTtsPlatformImpl::error() { return error_; } void ExtensionTtsPlatformImpl::clear_error() { error_ = std::string(); } void ExtensionTtsPlatformImpl::set_error(const std::string& error) { error_ = error; } // // Utterance // // static int Utterance::next_utterance_id_ = 0; Utterance::Utterance(Profile* profile, const std::string& text, DictionaryValue* options, Task* completion_task) : profile_(profile), id_(next_utterance_id_++), text_(text), rate_(-1.0), pitch_(-1.0), volume_(-1.0), can_enqueue_(false), completion_task_(completion_task) { if (!options) { // Use all default options. options_.reset(new DictionaryValue()); return; } options_.reset(options->DeepCopy()); if (options->HasKey(util::kVoiceNameKey)) options->GetString(util::kVoiceNameKey, &voice_name_); if (options->HasKey(util::kLocaleKey)) options->GetString(util::kLocaleKey, &locale_); if (options->HasKey(util::kGenderKey)) options->GetString(util::kGenderKey, &gender_); if (util::ReadNumberByKey(options, util::kRateKey, &rate_)) { if (!base::IsFinite(rate_) || rate_ < 0.0 || rate_ > 1.0) rate_ = -1.0; } if (util::ReadNumberByKey(options, util::kPitchKey, &pitch_)) { if (!base::IsFinite(pitch_) || pitch_ < 0.0 || pitch_ > 1.0) pitch_ = -1.0; } if (util::ReadNumberByKey(options, util::kVolumeKey, &volume_)) { if (!base::IsFinite(volume_) || volume_ < 0.0 || volume_ > 1.0) volume_ = -1.0; } if (options->HasKey(util::kEnqueueKey)) options->GetBoolean(util::kEnqueueKey, &can_enqueue_); } Utterance::~Utterance() { DCHECK_EQ(completion_task_, static_cast<Task *>(NULL)); } void Utterance::FinishAndDestroy() { completion_task_->Run(); completion_task_ = NULL; delete this; } // // ExtensionTtsController // // static ExtensionTtsController* ExtensionTtsController::GetInstance() { return Singleton<ExtensionTtsController>::get(); } ExtensionTtsController::ExtensionTtsController() : ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)), current_utterance_(NULL), platform_impl_(NULL) { } ExtensionTtsController::~ExtensionTtsController() { FinishCurrentUtterance(); ClearUtteranceQueue(); } void ExtensionTtsController::SpeakOrEnqueue(Utterance* utterance) { if (IsSpeaking() && utterance->can_enqueue()) { utterance_queue_.push(utterance); } else { Stop(); SpeakNow(utterance); } } std::string ExtensionTtsController::GetMatchingExtensionId( Utterance* utterance) { ExtensionService* service = utterance->profile()->GetExtensionService(); DCHECK(service); ExtensionEventRouter* event_router = utterance->profile()->GetExtensionEventRouter(); DCHECK(event_router); const ExtensionList* extensions = service->extensions(); ExtensionList::const_iterator iter; for (iter = extensions->begin(); iter != extensions->end(); ++iter) { const Extension* extension = *iter; if (!event_router->ExtensionHasEventListener( extension->id(), events::kOnSpeak) || !event_router->ExtensionHasEventListener( extension->id(), events::kOnStop)) { continue; } const std::vector<Extension::TtsVoice>& tts_voices = extension->tts_voices(); for (size_t i = 0; i < tts_voices.size(); ++i) { const Extension::TtsVoice& voice = tts_voices[i]; if (!voice.voice_name.empty() && !utterance->voice_name().empty() && voice.voice_name != utterance->voice_name()) { continue; } if (!voice.locale.empty() && !utterance->locale().empty() && voice.locale != utterance->locale()) { continue; } if (!voice.gender.empty() && !utterance->gender().empty() && voice.gender != utterance->gender()) { continue; } return extension->id(); } } return std::string(); } void ExtensionTtsController::SpeakNow(Utterance* utterance) { std::string extension_id = GetMatchingExtensionId(utterance); if (!extension_id.empty()) { current_utterance_ = utterance; utterance->set_extension_id(extension_id); ListValue args; args.Set(0, Value::CreateStringValue(utterance->text())); // Pass through all options to the speech engine, except for // "enqueue", which the speech engine doesn't need to handle. DictionaryValue* options = static_cast<DictionaryValue*>( utterance->options()->DeepCopy()); if (options->HasKey(util::kEnqueueKey)) options->Remove(util::kEnqueueKey, NULL); args.Set(1, options); args.Set(2, Value::CreateIntegerValue(utterance->id())); std::string json_args; base::JSONWriter::Write(&args, false, &json_args); utterance->profile()->GetExtensionEventRouter()->DispatchEventToExtension( extension_id, events::kOnSpeak, json_args, utterance->profile(), GURL()); return; } GetPlatformImpl()->clear_error(); bool success = GetPlatformImpl()->Speak( utterance->text(), utterance->locale(), utterance->gender(), utterance->rate(), utterance->pitch(), utterance->volume()); if (!success) { utterance->set_error(GetPlatformImpl()->error()); utterance->FinishAndDestroy(); return; } current_utterance_ = utterance; // Check to see if it's still speaking; finish the utterance if not and // start polling if so. Checking immediately helps to avoid flaky unit // tests by forcing them to set expectations for IsSpeaking. CheckSpeechStatus(); } void ExtensionTtsController::Stop() { if (current_utterance_ && !current_utterance_->extension_id().empty()) { current_utterance_->profile()->GetExtensionEventRouter()-> DispatchEventToExtension( current_utterance_->extension_id(), events::kOnStop, "[]", current_utterance_->profile(), GURL()); } else { GetPlatformImpl()->clear_error(); GetPlatformImpl()->StopSpeaking(); } if (current_utterance_) current_utterance_->set_error(kSpeechInterruptedError); FinishCurrentUtterance(); ClearUtteranceQueue(); } void ExtensionTtsController::OnSpeechFinished( int request_id, const std::string& error_message) { // We may sometimes receive completion callbacks "late", after we've // already finished the utterance (for example because another utterance // interrupted or we got a call to Stop). It's also possible that a buggy // extension has called this more than once. In either case it's safe to // just ignore this call. if (!current_utterance_ || request_id != current_utterance_->id()) return; current_utterance_->set_error(error_message); FinishCurrentUtterance(); SpeakNextUtterance(); } bool ExtensionTtsController::IsSpeaking() const { return current_utterance_ != NULL; } void ExtensionTtsController::FinishCurrentUtterance() { if (current_utterance_) { current_utterance_->FinishAndDestroy(); current_utterance_ = NULL; } } void ExtensionTtsController::SpeakNextUtterance() { // Start speaking the next utterance in the queue. Keep trying in case // one fails but there are still more in the queue to try. while (!utterance_queue_.empty() && !current_utterance_) { Utterance* utterance = utterance_queue_.front(); utterance_queue_.pop(); SpeakNow(utterance); } } void ExtensionTtsController::ClearUtteranceQueue() { while (!utterance_queue_.empty()) { Utterance* utterance = utterance_queue_.front(); utterance_queue_.pop(); utterance->set_error(kSpeechRemovedFromQueueError); utterance->FinishAndDestroy(); } } void ExtensionTtsController::CheckSpeechStatus() { if (!current_utterance_) return; if (!current_utterance_->extension_id().empty()) return; if (GetPlatformImpl()->IsSpeaking() == false) { FinishCurrentUtterance(); SpeakNextUtterance(); } // If we're still speaking something (either the prevoius utterance or // a new utterance), keep calling this method after another delay. // TODO(dmazzoni): get rid of this as soon as all platform implementations // provide completion callbacks rather than only supporting polling. if (current_utterance_ && current_utterance_->extension_id().empty()) { MessageLoop::current()->PostDelayedTask( FROM_HERE, method_factory_.NewRunnableMethod( &ExtensionTtsController::CheckSpeechStatus), kSpeechCheckDelayIntervalMs); } } void ExtensionTtsController::SetPlatformImpl( ExtensionTtsPlatformImpl* platform_impl) { platform_impl_ = platform_impl; } ExtensionTtsPlatformImpl* ExtensionTtsController::GetPlatformImpl() { if (!platform_impl_) platform_impl_ = ExtensionTtsPlatformImpl::GetInstance(); return platform_impl_; } // // Extension API functions // bool ExtensionTtsSpeakFunction::RunImpl() { std::string text; EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &text)); DictionaryValue* options = NULL; if (args_->GetSize() >= 2) EXTENSION_FUNCTION_VALIDATE(args_->GetDictionary(1, &options)); Task* completion_task = NewRunnableMethod( this, &ExtensionTtsSpeakFunction::SpeechFinished); utterance_ = new Utterance(profile(), text, options, completion_task); AddRef(); // Balanced in SpeechFinished(). ExtensionTtsController::GetInstance()->SpeakOrEnqueue(utterance_); return true; } void ExtensionTtsSpeakFunction::SpeechFinished() { error_ = utterance_->error(); bool success = error_.empty(); SendResponse(success); Release(); // Balanced in RunImpl(). } bool ExtensionTtsStopSpeakingFunction::RunImpl() { ExtensionTtsController::GetInstance()->Stop(); return true; } bool ExtensionTtsIsSpeakingFunction::RunImpl() { result_.reset(Value::CreateBooleanValue( ExtensionTtsController::GetInstance()->IsSpeaking())); return true; } bool ExtensionTtsSpeakCompletedFunction::RunImpl() { int request_id; std::string error_message; EXTENSION_FUNCTION_VALIDATE(args_->GetInteger(0, &request_id)); if (args_->GetSize() >= 2) EXTENSION_FUNCTION_VALIDATE(args_->GetString(1, &error_message)); ExtensionTtsController::GetInstance()->OnSpeechFinished( request_id, error_message); return true; }