Bug 1003452 - Implement OSX backend for WebSpeech Synthesis. r=eeejay
authorMakoto Kato <m_kato@ga2.so-net.ne.jp>
Thu, 15 Oct 2015 23:51:49 +0900
changeset 303543 26c7f2d75c75769c2e615ccb3708809fc3faf46f
parent 303542 921efb579415a7dc78bcdde4c9d51c3221e66890
child 303544 0aec32dbd6bebe993b7c2aaede649d41592f8b4c
push id1001
push userraliiev@mozilla.com
push dateMon, 18 Jan 2016 19:06:03 +0000
treeherdermozilla-release@8b89261f3ac4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerseeejay
bugs1003452
milestone44.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1003452 - Implement OSX backend for WebSpeech Synthesis. r=eeejay
dom/media/webspeech/synth/cocoa/OSXSpeechSynthesizerModule.cpp
dom/media/webspeech/synth/cocoa/OSXSpeechSynthesizerService.h
dom/media/webspeech/synth/cocoa/OSXSpeechSynthesizerService.mm
dom/media/webspeech/synth/cocoa/moz.build
dom/media/webspeech/synth/moz.build
new file mode 100644
--- /dev/null
+++ b/dom/media/webspeech/synth/cocoa/OSXSpeechSynthesizerModule.cpp
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/ModuleUtils.h"
+#include "nsIClassInfoImpl.h"
+
+#include "OSXSpeechSynthesizerService.h"
+
+using namespace mozilla::dom;
+
+#define OSXSPEECHSYNTHESIZERSERVICE_CID \
+  {0x914e73b4, 0x6337, 0x4bef, {0x97, 0xf3, 0x4d, 0x06, 0x9e, 0x05, 0x3a, 0x12}}
+
+#define OSXSPEECHSYNTHESIZERSERVICE_CONTRACTID "@mozilla.org/synthsystem;1"
+
+// Defines OSXSpeechSynthesizerServiceConstructor
+NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(OSXSpeechSynthesizerService,
+                                         OSXSpeechSynthesizerService::GetInstanceForService)
+
+// Defines kSAPISERVICE_CID
+NS_DEFINE_NAMED_CID(OSXSPEECHSYNTHESIZERSERVICE_CID);
+
+static const mozilla::Module::CIDEntry kCIDs[] = {
+  { &kOSXSPEECHSYNTHESIZERSERVICE_CID, true, nullptr, OSXSpeechSynthesizerServiceConstructor },
+  { nullptr }
+};
+
+static const mozilla::Module::ContractIDEntry kContracts[] = {
+  { OSXSPEECHSYNTHESIZERSERVICE_CONTRACTID, &kOSXSPEECHSYNTHESIZERSERVICE_CID },
+  { nullptr }
+};
+
+static const mozilla::Module::CategoryEntry kCategories[] = {
+  { "profile-after-change", "Sapi Speech Synth", OSXSPEECHSYNTHESIZERSERVICE_CONTRACTID },
+  { nullptr }
+};
+
+static void
+UnloadOSXSpeechSynthesizerModule()
+{
+  OSXSpeechSynthesizerService::Shutdown();
+}
+
+static const mozilla::Module kModule = {
+  mozilla::Module::kVersion,
+  kCIDs,
+  kContracts,
+  kCategories,
+  nullptr,
+  nullptr,
+  UnloadOSXSpeechSynthesizerModule
+};
+
+NSMODULE_DEFN(osxsynth) = &kModule;
new file mode 100644
--- /dev/null
+++ b/dom/media/webspeech/synth/cocoa/OSXSpeechSynthesizerService.h
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_OsxSpeechSynthesizerService_h
+#define mozilla_dom_OsxSpeechSynthesizerService_h
+
+#include "nsAutoPtr.h"
+#include "nsISpeechService.h"
+#include "nsIObserver.h"
+#include "mozilla/StaticPtr.h"
+
+namespace mozilla {
+namespace dom {
+
+class OSXSpeechSynthesizerService final : public nsISpeechService
+                                        , public nsIObserver
+{
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSISPEECHSERVICE
+  NS_DECL_NSIOBSERVER
+
+  bool Init();
+
+  static OSXSpeechSynthesizerService* GetInstance();
+  static already_AddRefed<OSXSpeechSynthesizerService> GetInstanceForService();
+  static void Shutdown();
+
+private:
+  OSXSpeechSynthesizerService();
+  virtual ~OSXSpeechSynthesizerService();
+
+  bool RegisterVoices();
+
+  bool mInitialized;
+  static mozilla::StaticRefPtr<OSXSpeechSynthesizerService> sSingleton;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
new file mode 100644
--- /dev/null
+++ b/dom/media/webspeech/synth/cocoa/OSXSpeechSynthesizerService.mm
@@ -0,0 +1,384 @@
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sw=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.h"
+#include "nsServiceManagerUtils.h"
+#include "nsObjCExceptions.h"
+#include "nsCocoaUtils.h"
+#include "nsThreadUtils.h"
+#include "mozilla/dom/nsSynthVoiceRegistry.h"
+#include "mozilla/dom/nsSpeechTask.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Assertions.h"
+#include "OSXSpeechSynthesizerService.h"
+
+#import <Cocoa/Cocoa.h>
+
+using namespace mozilla;
+
+class SpeechTaskCallback final : public nsISpeechTaskCallback
+{
+public:
+  SpeechTaskCallback(nsISpeechTask* aTask, NSSpeechSynthesizer* aSynth)
+    : mTask(aTask)
+    , mSpeechSynthesizer(aSynth)
+  {
+    mStartingTime = TimeStamp::Now();
+  }
+
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(SpeechTaskCallback, nsISpeechTaskCallback)
+
+  NS_DECL_NSISPEECHTASKCALLBACK
+
+  void OnWillSpeakWord(uint32_t aIndex);
+  void OnError(uint32_t aIndex);
+  void OnDidFinishSpeaking();
+
+private:
+  virtual ~SpeechTaskCallback()
+  {
+    [mSpeechSynthesizer release];
+  }
+
+  float GetTimeDurationFromStart();
+
+  nsCOMPtr<nsISpeechTask> mTask;
+  NSSpeechSynthesizer* mSpeechSynthesizer;
+  TimeStamp mStartingTime;
+  uint32_t mCurrentIndex;
+};
+
+NS_IMPL_CYCLE_COLLECTION(SpeechTaskCallback, mTask);
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SpeechTaskCallback)
+  NS_INTERFACE_MAP_ENTRY(nsISpeechTaskCallback)
+  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISpeechTaskCallback)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(SpeechTaskCallback)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(SpeechTaskCallback)
+
+NS_IMETHODIMP
+SpeechTaskCallback::OnCancel()
+{
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
+
+  [mSpeechSynthesizer stopSpeaking];
+  return NS_OK;
+
+  NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
+}
+
+NS_IMETHODIMP
+SpeechTaskCallback::OnPause()
+{
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
+
+  [mSpeechSynthesizer pauseSpeakingAtBoundary:NSSpeechImmediateBoundary];
+  mTask->DispatchPause(GetTimeDurationFromStart(), mCurrentIndex);
+  return NS_OK;
+
+  NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
+}
+
+NS_IMETHODIMP
+SpeechTaskCallback::OnResume()
+{
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
+
+  [mSpeechSynthesizer continueSpeaking];
+  mTask->DispatchResume(GetTimeDurationFromStart(), mCurrentIndex);
+  return NS_OK;
+
+  NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
+}
+
+NS_IMETHODIMP
+SpeechTaskCallback::OnVolumeChanged(float aVolume)
+{
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
+
+  [mSpeechSynthesizer setObject:[NSNumber numberWithFloat:aVolume]
+                    forProperty:NSSpeechVolumeProperty error:nil];
+  return NS_OK;
+
+  NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
+}
+
+float
+SpeechTaskCallback::GetTimeDurationFromStart()
+{
+  TimeDuration duration = TimeStamp::Now() - mStartingTime;
+  return duration.ToMilliseconds();
+}
+
+void
+SpeechTaskCallback::OnWillSpeakWord(uint32_t aIndex)
+{
+  mCurrentIndex = aIndex;
+  mTask->DispatchBoundary(NS_LITERAL_STRING("word"),
+                          GetTimeDurationFromStart(), mCurrentIndex);
+}
+
+void
+SpeechTaskCallback::OnError(uint32_t aIndex)
+{
+  mTask->DispatchError(GetTimeDurationFromStart(), aIndex);
+}
+
+void
+SpeechTaskCallback::OnDidFinishSpeaking()
+{
+  mTask->DispatchEnd(GetTimeDurationFromStart(), mCurrentIndex);
+  // no longer needed
+  mTask = nullptr;
+}
+
+@interface SpeechDelegate : NSObject<NSSpeechSynthesizerDelegate>
+{
+@private
+  SpeechTaskCallback* mCallback;
+}
+
+  - (id)initWithCallback:(SpeechTaskCallback*)aCallback;
+@end
+
+@implementation SpeechDelegate
+- (id)initWithCallback:(SpeechTaskCallback*)aCallback
+{
+  [super init];
+  mCallback = aCallback;
+  return self;
+}
+
+- (void)speechSynthesizer:(NSSpeechSynthesizer *)aSender
+            willSpeakWord:(NSRange)aRange ofString:(NSString*)aString
+{
+  mCallback->OnWillSpeakWord(aRange.location);
+}
+
+- (void)speechSynthesizer:(NSSpeechSynthesizer *)aSender
+        didFinishSpeaking:(BOOL)aFinishedSpeaking
+{
+  mCallback->OnDidFinishSpeaking();
+}
+
+- (void)speechSynthesizer:(NSSpeechSynthesizer*)aSender
+ didEncounterErrorAtIndex:(NSUInteger)aCharacterIndex
+                 ofString:(NSString*)aString
+                  message:(NSString*)aMessage
+{
+  mCallback->OnError(aCharacterIndex);
+}
+@end
+
+namespace mozilla {
+namespace dom {
+
+class RegisterVoicesRunnable final : public nsRunnable
+{
+public:
+  explicit RegisterVoicesRunnable(OSXSpeechSynthesizerService* aSpeechService)
+    : mSpeechService(aSpeechService)
+  {
+  }
+
+  NS_IMETHOD Run();
+
+private:
+  ~RegisterVoicesRunnable()
+  {
+  }
+
+  RefPtr<OSXSpeechSynthesizerService> mSpeechService;
+};
+
+NS_IMETHODIMP
+RegisterVoicesRunnable::Run()
+{
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
+
+  nsresult rv;
+  nsCOMPtr<nsISynthVoiceRegistry> registry =
+    do_GetService(NS_SYNTHVOICEREGISTRY_CONTRACTID, &rv);
+  if (!registry) {
+    return rv;
+  }
+
+  NSArray* voices = [NSSpeechSynthesizer availableVoices];
+  NSString* defaultVoice = [NSSpeechSynthesizer defaultVoice];
+
+  for (NSString* voice in voices) {
+    NSDictionary* attr = [NSSpeechSynthesizer attributesForVoice:voice];
+
+    nsAutoString identifier;
+    nsCocoaUtils::GetStringForNSString([attr objectForKey:NSVoiceIdentifier],
+                                       identifier);
+
+    nsAutoString name;
+    nsCocoaUtils::GetStringForNSString([attr objectForKey:NSVoiceName], name);
+
+    nsAutoString locale;
+    nsCocoaUtils::GetStringForNSString(
+      [attr objectForKey:NSVoiceLocaleIdentifier], locale);
+    locale.ReplaceChar('_', '-');
+
+    nsAutoString uri;
+    uri.AssignLiteral("urn:moz-tts:osx:");
+    uri.Append(identifier);
+    rv = registry->AddVoice(mSpeechService, uri, name, locale, true, false);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      continue;
+    }
+
+    if ([voice isEqualToString:defaultVoice]) {
+      registry->SetDefaultVoice(uri, true);
+    }
+  }
+
+  return NS_OK;
+
+  NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
+}
+
+StaticRefPtr<OSXSpeechSynthesizerService> OSXSpeechSynthesizerService::sSingleton;
+
+NS_INTERFACE_MAP_BEGIN(OSXSpeechSynthesizerService)
+  NS_INTERFACE_MAP_ENTRY(nsISpeechService)
+  NS_INTERFACE_MAP_ENTRY(nsIObserver)
+  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISpeechService)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_ADDREF(OSXSpeechSynthesizerService)
+NS_IMPL_RELEASE(OSXSpeechSynthesizerService)
+
+OSXSpeechSynthesizerService::OSXSpeechSynthesizerService()
+  : mInitialized(false)
+{
+}
+
+OSXSpeechSynthesizerService::~OSXSpeechSynthesizerService()
+{
+}
+
+bool
+OSXSpeechSynthesizerService::Init()
+{
+  if (Preferences::GetBool("media.webspeech.synth.test") ||
+      !Preferences::GetBool("media.webspeech.synth.enabled")) {
+    // When test is enabled, we shouldn't add OS backend (Bug 1160844)
+    return false;
+  }
+
+  // Get all the voices and register in the SynthVoiceRegistry
+  nsCOMPtr<nsIRunnable> runnable = new RegisterVoicesRunnable(this);
+  NS_DispatchToMainThread(runnable);
+
+  mInitialized = true;
+  return true;
+}
+
+NS_IMETHODIMP
+OSXSpeechSynthesizerService::Speak(const nsAString& aText,
+                                   const nsAString& aUri,
+                                   float aVolume,
+                                   float aRate,
+                                   float aPitch,
+                                   nsISpeechTask* aTask)
+{
+  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
+
+  MOZ_ASSERT(StringBeginsWith(aUri, NS_LITERAL_STRING("urn:moz-tts:osx:")),
+             "OSXSpeechSynthesizerService doesn't allow this voice URI");
+
+  NSSpeechSynthesizer* synth = [[NSSpeechSynthesizer alloc] init];
+  // strlen("urn:moz-tts:osx:") == 16
+  NSString* identifier = nsCocoaUtils::ToNSString(Substring(aUri, 16));
+  [synth setVoice:identifier];
+
+  // default rate is 180-220
+  [synth setObject:[NSNumber numberWithInt:aRate * 200]
+         forProperty:NSSpeechRateProperty error:nil];
+  // volume allows 0.0-1.0
+  [synth setObject:[NSNumber numberWithFloat:aVolume]
+         forProperty:NSSpeechVolumeProperty error:nil];
+  // Use default pitch value to calculate this
+  NSNumber* defaultPitch =
+    [synth objectForProperty:NSSpeechPitchBaseProperty error:nil];
+  if (defaultPitch) {
+    int newPitch = [defaultPitch intValue] * (aPitch / 2 + 0.5);
+    [synth setObject:[NSNumber numberWithInt:newPitch]
+           forProperty:NSSpeechPitchBaseProperty error:nil];
+  }
+
+  RefPtr<SpeechTaskCallback> callback = new SpeechTaskCallback(aTask, synth);
+  nsresult rv = aTask->Setup(callback, 0, 0, 0);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  SpeechDelegate* delegate = [[SpeechDelegate alloc] initWithCallback:callback];
+  [synth setDelegate:delegate];
+  [delegate release ];
+
+  NSString* text = nsCocoaUtils::ToNSString(aText);
+  BOOL success = [synth startSpeakingString:text];
+  NS_ENSURE_TRUE(success, NS_ERROR_FAILURE);
+
+  aTask->DispatchStart();
+  return NS_OK;
+
+  NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
+}
+
+NS_IMETHODIMP
+OSXSpeechSynthesizerService::GetServiceType(SpeechServiceType* aServiceType)
+{
+  *aServiceType = nsISpeechService::SERVICETYPE_INDIRECT_AUDIO;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+OSXSpeechSynthesizerService::Observe(nsISupports* aSubject, const char* aTopic,
+                                     const char16_t* aData)
+{
+  if (!strcmp(aTopic, "profile-after-change")) {
+    Init();
+  }
+  return NS_OK;
+}
+
+OSXSpeechSynthesizerService*
+OSXSpeechSynthesizerService::GetInstance()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  if (XRE_GetProcessType() != GeckoProcessType_Default) {
+    return nullptr;
+  }
+
+  if (!sSingleton) {
+    sSingleton = new OSXSpeechSynthesizerService();
+  }
+  return sSingleton;
+}
+
+already_AddRefed<OSXSpeechSynthesizerService>
+OSXSpeechSynthesizerService::GetInstanceForService()
+{
+  RefPtr<OSXSpeechSynthesizerService> speechService = GetInstance();
+  return speechService.forget();
+}
+
+void
+OSXSpeechSynthesizerService::Shutdown()
+{
+  if (!sSingleton) {
+    return;
+  }
+  sSingleton = nullptr;
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/media/webspeech/synth/cocoa/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+SOURCES += [
+  'OSXSpeechSynthesizerModule.cpp',
+  'OSXSpeechSynthesizerService.mm'
+]
+
+FINAL_LIBRARY = 'xul'
--- a/dom/media/webspeech/synth/moz.build
+++ b/dom/media/webspeech/synth/moz.build
@@ -41,16 +41,19 @@ if CONFIG['MOZ_WEBSPEECH']:
             'test/nsFakeSynthServices.cpp'
         ]
 
     DIRS = []
 
     if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
         DIRS += ['windows']
 
+    if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa':
+        DIRS += ['cocoa']
+
     if CONFIG['MOZ_SYNTH_SPEECHD']:
         DIRS += ['speechd']
 
     if CONFIG['MOZ_SYNTH_PICO']:
         DIRS += ['pico']
 
 IPDL_SOURCES += [
     'ipc/PSpeechSynthesis.ipdl',