Line data Source code
1 : /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 : /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 : /* This Source Code Form is subject to the terms of the Mozilla Public
4 : * License, v. 2.0. If a copy of the MPL was not distributed with this
5 : * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 :
7 : #include "SpeechDispatcherService.h"
8 :
9 : #include "mozilla/dom/nsSpeechTask.h"
10 : #include "mozilla/dom/nsSynthVoiceRegistry.h"
11 : #include "mozilla/Preferences.h"
12 : #include "nsEscape.h"
13 : #include "nsISupports.h"
14 : #include "nsPrintfCString.h"
15 : #include "nsReadableUtils.h"
16 : #include "nsServiceManagerUtils.h"
17 : #include "nsThreadUtils.h"
18 : #include "prlink.h"
19 :
20 : #include <math.h>
21 : #include <stdlib.h>
22 :
23 : #define URI_PREFIX "urn:moz-tts:speechd:"
24 :
25 : #define MAX_RATE static_cast<float>(2.5)
26 : #define MIN_RATE static_cast<float>(0.5)
27 :
28 : // Some structures for libspeechd
29 : typedef enum {
30 : SPD_EVENT_BEGIN,
31 : SPD_EVENT_END,
32 : SPD_EVENT_INDEX_MARK,
33 : SPD_EVENT_CANCEL,
34 : SPD_EVENT_PAUSE,
35 : SPD_EVENT_RESUME
36 : } SPDNotificationType;
37 :
38 : typedef enum {
39 : SPD_BEGIN = 1,
40 : SPD_END = 2,
41 : SPD_INDEX_MARKS = 4,
42 : SPD_CANCEL = 8,
43 : SPD_PAUSE = 16,
44 : SPD_RESUME = 32,
45 :
46 : SPD_ALL = 0x3f
47 : } SPDNotification;
48 :
49 : typedef enum {
50 : SPD_MODE_SINGLE = 0,
51 : SPD_MODE_THREADED = 1
52 : } SPDConnectionMode;
53 :
54 : typedef void (*SPDCallback) (size_t msg_id, size_t client_id,
55 : SPDNotificationType state);
56 :
57 : typedef void (*SPDCallbackIM) (size_t msg_id, size_t client_id,
58 : SPDNotificationType state, char* index_mark);
59 :
60 : struct SPDConnection
61 : {
62 : SPDCallback callback_begin;
63 : SPDCallback callback_end;
64 : SPDCallback callback_cancel;
65 : SPDCallback callback_pause;
66 : SPDCallback callback_resume;
67 : SPDCallbackIM callback_im;
68 :
69 : /* partial, more private fields in structure */
70 : };
71 :
72 : struct SPDVoice
73 : {
74 : char* name;
75 : char* language;
76 : char* variant;
77 : };
78 :
79 : typedef enum {
80 : SPD_IMPORTANT = 1,
81 : SPD_MESSAGE = 2,
82 : SPD_TEXT = 3,
83 : SPD_NOTIFICATION = 4,
84 : SPD_PROGRESS = 5
85 : } SPDPriority;
86 :
87 : #define SPEECHD_FUNCTIONS \
88 : FUNC(spd_open, SPDConnection*, (const char*, const char*, const char*, SPDConnectionMode)) \
89 : FUNC(spd_close, void, (SPDConnection*)) \
90 : FUNC(spd_list_synthesis_voices, SPDVoice**, (SPDConnection*)) \
91 : FUNC(spd_say, int, (SPDConnection*, SPDPriority, const char*)) \
92 : FUNC(spd_cancel, int, (SPDConnection*)) \
93 : FUNC(spd_set_volume, int, (SPDConnection*, int)) \
94 : FUNC(spd_set_voice_rate, int, (SPDConnection*, int)) \
95 : FUNC(spd_set_voice_pitch, int, (SPDConnection*, int)) \
96 : FUNC(spd_set_synthesis_voice, int, (SPDConnection*, const char*)) \
97 : FUNC(spd_set_notification_on, int, (SPDConnection*, SPDNotification))
98 :
99 : #define FUNC(name, type, params) \
100 : typedef type (*_##name##_fn) params; \
101 : static _##name##_fn _##name;
102 :
103 : SPEECHD_FUNCTIONS
104 :
105 : #undef FUNC
106 :
107 : #define spd_open _spd_open
108 : #define spd_close _spd_close
109 : #define spd_list_synthesis_voices _spd_list_synthesis_voices
110 : #define spd_say _spd_say
111 : #define spd_cancel _spd_cancel
112 : #define spd_set_volume _spd_set_volume
113 : #define spd_set_voice_rate _spd_set_voice_rate
114 : #define spd_set_voice_pitch _spd_set_voice_pitch
115 : #define spd_set_synthesis_voice _spd_set_synthesis_voice
116 : #define spd_set_notification_on _spd_set_notification_on
117 :
118 : static PRLibrary* speechdLib = nullptr;
119 :
120 : typedef void (*nsSpeechDispatcherFunc)();
121 : struct nsSpeechDispatcherDynamicFunction
122 : {
123 : const char* functionName;
124 : nsSpeechDispatcherFunc* function;
125 : };
126 :
127 : namespace mozilla {
128 : namespace dom {
129 :
130 3 : StaticRefPtr<SpeechDispatcherService> SpeechDispatcherService::sSingleton;
131 :
132 : class SpeechDispatcherVoice
133 : {
134 : public:
135 :
136 0 : SpeechDispatcherVoice(const nsAString& aName, const nsAString& aLanguage)
137 0 : : mName(aName), mLanguage(aLanguage) {}
138 :
139 0 : NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SpeechDispatcherVoice)
140 :
141 : // Voice name
142 : nsString mName;
143 :
144 : // Voice language, in BCP-47 syntax
145 : nsString mLanguage;
146 :
147 : private:
148 0 : ~SpeechDispatcherVoice() {}
149 : };
150 :
151 :
152 : class SpeechDispatcherCallback final : public nsISpeechTaskCallback
153 : {
154 : public:
155 0 : SpeechDispatcherCallback(nsISpeechTask* aTask, SpeechDispatcherService* aService)
156 0 : : mTask(aTask)
157 0 : , mService(aService) {}
158 :
159 : NS_DECL_CYCLE_COLLECTING_ISUPPORTS
160 0 : NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(SpeechDispatcherCallback, nsISpeechTaskCallback)
161 :
162 : NS_DECL_NSISPEECHTASKCALLBACK
163 :
164 : bool OnSpeechEvent(SPDNotificationType state);
165 :
166 : private:
167 0 : ~SpeechDispatcherCallback() { }
168 :
169 : // This pointer is used to dispatch events
170 : nsCOMPtr<nsISpeechTask> mTask;
171 :
172 : // By holding a strong reference to the service we guarantee that it won't be
173 : // destroyed before this runnable.
174 : RefPtr<SpeechDispatcherService> mService;
175 :
176 : TimeStamp mStartTime;
177 : };
178 :
179 0 : NS_IMPL_CYCLE_COLLECTION(SpeechDispatcherCallback, mTask);
180 :
181 0 : NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SpeechDispatcherCallback)
182 0 : NS_INTERFACE_MAP_ENTRY(nsISpeechTaskCallback)
183 0 : NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISpeechTaskCallback)
184 0 : NS_INTERFACE_MAP_END
185 :
186 0 : NS_IMPL_CYCLE_COLLECTING_ADDREF(SpeechDispatcherCallback)
187 0 : NS_IMPL_CYCLE_COLLECTING_RELEASE(SpeechDispatcherCallback)
188 :
189 : NS_IMETHODIMP
190 0 : SpeechDispatcherCallback::OnPause()
191 : {
192 : // XXX: Speech dispatcher does not pause immediately, but waits for the speech
193 : // to reach an index mark so that it could resume from that offset.
194 : // There is no support for word or sentence boundaries, so index marks would
195 : // only occur in explicit SSML marks, and we don't support that yet.
196 : // What in actuality happens, is that if you call spd_pause(), it will speak
197 : // the utterance in its entirety, dispatch an end event, and then put speechd
198 : // in a 'paused' state. Since it is after the utterance ended, we don't get
199 : // that state change, and our speech api is in an unrecoverable state.
200 : // So, since it is useless anyway, I am not implementing pause.
201 0 : return NS_OK;
202 : }
203 :
204 : NS_IMETHODIMP
205 0 : SpeechDispatcherCallback::OnResume()
206 : {
207 : // XXX: Unsupported, see OnPause().
208 0 : return NS_OK;
209 : }
210 :
211 : NS_IMETHODIMP
212 0 : SpeechDispatcherCallback::OnCancel()
213 : {
214 0 : if (spd_cancel(mService->mSpeechdClient) < 0) {
215 0 : return NS_ERROR_FAILURE;
216 : }
217 :
218 0 : return NS_OK;
219 : }
220 :
221 : NS_IMETHODIMP
222 0 : SpeechDispatcherCallback::OnVolumeChanged(float aVolume)
223 : {
224 : // XXX: This currently does not change the volume mid-utterance, but it
225 : // doesn't do anything bad either. So we could put this here with the hopes
226 : // that speechd supports this in the future.
227 0 : if (spd_set_volume(mService->mSpeechdClient, static_cast<int>(aVolume * 100)) < 0) {
228 0 : return NS_ERROR_FAILURE;
229 : }
230 :
231 0 : return NS_OK;
232 : }
233 :
234 : bool
235 0 : SpeechDispatcherCallback::OnSpeechEvent(SPDNotificationType state)
236 : {
237 0 : bool remove = false;
238 :
239 0 : switch (state) {
240 : case SPD_EVENT_BEGIN:
241 0 : mStartTime = TimeStamp::Now();
242 0 : mTask->DispatchStart();
243 0 : break;
244 :
245 : case SPD_EVENT_PAUSE:
246 0 : mTask->DispatchPause((TimeStamp::Now() - mStartTime).ToSeconds(), 0);
247 0 : break;
248 :
249 : case SPD_EVENT_RESUME:
250 0 : mTask->DispatchResume((TimeStamp::Now() - mStartTime).ToSeconds(), 0);
251 0 : break;
252 :
253 : case SPD_EVENT_CANCEL:
254 : case SPD_EVENT_END:
255 0 : mTask->DispatchEnd((TimeStamp::Now() - mStartTime).ToSeconds(), 0);
256 0 : remove = true;
257 0 : break;
258 :
259 : case SPD_EVENT_INDEX_MARK:
260 : // Not yet supported
261 0 : break;
262 :
263 : default:
264 0 : break;
265 : }
266 :
267 0 : return remove;
268 : }
269 :
270 : static void
271 0 : speechd_cb(size_t msg_id, size_t client_id, SPDNotificationType state)
272 : {
273 0 : SpeechDispatcherService* service = SpeechDispatcherService::GetInstance(false);
274 :
275 0 : if (service) {
276 0 : NS_DispatchToMainThread(NewRunnableMethod<uint32_t, SPDNotificationType>(
277 : "dom::SpeechDispatcherService::EventNotify",
278 : service,
279 : &SpeechDispatcherService::EventNotify,
280 0 : static_cast<uint32_t>(msg_id),
281 0 : state));
282 : }
283 0 : }
284 :
285 :
286 0 : NS_INTERFACE_MAP_BEGIN(SpeechDispatcherService)
287 0 : NS_INTERFACE_MAP_ENTRY(nsISpeechService)
288 0 : NS_INTERFACE_MAP_ENTRY(nsIObserver)
289 0 : NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
290 0 : NS_INTERFACE_MAP_END
291 :
292 0 : NS_IMPL_ADDREF(SpeechDispatcherService)
293 0 : NS_IMPL_RELEASE(SpeechDispatcherService)
294 :
295 0 : SpeechDispatcherService::SpeechDispatcherService()
296 : : mInitialized(false)
297 0 : , mSpeechdClient(nullptr)
298 : {
299 0 : }
300 :
301 : void
302 0 : SpeechDispatcherService::Init()
303 : {
304 0 : if (!Preferences::GetBool("media.webspeech.synth.enabled") ||
305 0 : Preferences::GetBool("media.webspeech.synth.test")) {
306 0 : return;
307 : }
308 :
309 : // While speech dispatcher has a "threaded" mode, only spd_say() is async.
310 : // Since synchronous socket i/o could impact startup time, we do
311 : // initialization in a separate thread.
312 0 : DebugOnly<nsresult> rv = NS_NewNamedThread("speechd init",
313 0 : getter_AddRefs(mInitThread));
314 0 : MOZ_ASSERT(NS_SUCCEEDED(rv));
315 0 : rv = mInitThread->Dispatch(
316 0 : NewRunnableMethod("dom::SpeechDispatcherService::Setup",
317 : this,
318 : &SpeechDispatcherService::Setup),
319 0 : NS_DISPATCH_NORMAL);
320 0 : MOZ_ASSERT(NS_SUCCEEDED(rv));
321 : }
322 :
323 0 : SpeechDispatcherService::~SpeechDispatcherService()
324 : {
325 0 : if (mInitThread) {
326 0 : mInitThread->Shutdown();
327 : }
328 :
329 0 : if (mSpeechdClient) {
330 0 : spd_close(mSpeechdClient);
331 : }
332 0 : }
333 :
334 : void
335 0 : SpeechDispatcherService::Setup()
336 : {
337 : #define FUNC(name, type, params) { #name, (nsSpeechDispatcherFunc *)&_##name },
338 : static const nsSpeechDispatcherDynamicFunction kSpeechDispatcherSymbols[] = {
339 : SPEECHD_FUNCTIONS
340 : };
341 : #undef FUNC
342 :
343 0 : MOZ_ASSERT(!mInitialized);
344 :
345 0 : speechdLib = PR_LoadLibrary("libspeechd.so.2");
346 :
347 0 : if (!speechdLib) {
348 0 : NS_WARNING("Failed to load speechd library");
349 0 : return;
350 : }
351 :
352 0 : if (!PR_FindFunctionSymbol(speechdLib, "spd_get_volume")) {
353 : // There is no version getter function, so we rely on a symbol that was
354 : // introduced in release 0.8.2 in order to check for ABI compatibility.
355 0 : NS_WARNING("Unsupported version of speechd detected");
356 0 : return;
357 : }
358 :
359 0 : for (uint32_t i = 0; i < ArrayLength(kSpeechDispatcherSymbols); i++) {
360 0 : *kSpeechDispatcherSymbols[i].function =
361 0 : PR_FindFunctionSymbol(speechdLib, kSpeechDispatcherSymbols[i].functionName);
362 :
363 0 : if (!*kSpeechDispatcherSymbols[i].function) {
364 0 : NS_WARNING(nsPrintfCString("Failed to find speechd symbol for'%s'",
365 0 : kSpeechDispatcherSymbols[i].functionName).get());
366 0 : return;
367 : }
368 : }
369 :
370 0 : mSpeechdClient = spd_open("firefox", "web speech api", "who", SPD_MODE_THREADED);
371 0 : if (!mSpeechdClient) {
372 0 : NS_WARNING("Failed to call spd_open");
373 0 : return;
374 : }
375 :
376 : // Get all the voices from sapi and register in the SynthVoiceRegistry
377 0 : SPDVoice** list = spd_list_synthesis_voices(mSpeechdClient);
378 :
379 0 : mSpeechdClient->callback_begin = speechd_cb;
380 0 : mSpeechdClient->callback_end = speechd_cb;
381 0 : mSpeechdClient->callback_cancel = speechd_cb;
382 0 : mSpeechdClient->callback_pause = speechd_cb;
383 0 : mSpeechdClient->callback_resume = speechd_cb;
384 :
385 0 : spd_set_notification_on(mSpeechdClient, SPD_BEGIN);
386 0 : spd_set_notification_on(mSpeechdClient, SPD_END);
387 0 : spd_set_notification_on(mSpeechdClient, SPD_CANCEL);
388 :
389 0 : if (list != NULL) {
390 0 : for (int i = 0; list[i]; i++) {
391 0 : nsAutoString uri;
392 :
393 0 : uri.AssignLiteral(URI_PREFIX);
394 0 : nsAutoCString name;
395 0 : NS_EscapeURL(list[i]->name, -1, esc_OnlyNonASCII | esc_AlwaysCopy, name);
396 0 : uri.Append(NS_ConvertUTF8toUTF16(name));;
397 0 : uri.AppendLiteral("?");
398 :
399 0 : nsAutoCString lang(list[i]->language);
400 :
401 0 : if (strcmp(list[i]->variant, "none") != 0) {
402 : // In speech dispatcher, the variant will usually be the locale subtag
403 : // with another, non-standard suptag after it. We keep the first one
404 : // and convert it to uppercase.
405 0 : const char* v = list[i]->variant;
406 0 : const char* hyphen = strchr(v, '-');
407 0 : nsDependentCSubstring variant(v, hyphen ? hyphen - v : strlen(v));
408 0 : ToUpperCase(variant);
409 :
410 : // eSpeak uses UK which is not a valid region subtag in BCP47.
411 0 : if (variant.Equals("UK")) {
412 0 : variant.AssignLiteral("GB");
413 : }
414 :
415 0 : lang.AppendLiteral("-");
416 0 : lang.Append(variant);
417 : }
418 :
419 0 : uri.Append(NS_ConvertUTF8toUTF16(lang));
420 :
421 0 : mVoices.Put(uri, new SpeechDispatcherVoice(
422 0 : NS_ConvertUTF8toUTF16(list[i]->name),
423 0 : NS_ConvertUTF8toUTF16(lang)));
424 : }
425 : }
426 :
427 0 : NS_DispatchToMainThread(
428 0 : NewRunnableMethod("dom::SpeechDispatcherService::RegisterVoices",
429 : this,
430 0 : &SpeechDispatcherService::RegisterVoices));
431 :
432 : //mInitialized = true;
433 : }
434 :
435 : // private methods
436 :
437 : void
438 0 : SpeechDispatcherService::RegisterVoices()
439 : {
440 0 : RefPtr<nsSynthVoiceRegistry> registry = nsSynthVoiceRegistry::GetInstance();
441 0 : for (auto iter = mVoices.Iter(); !iter.Done(); iter.Next()) {
442 0 : RefPtr<SpeechDispatcherVoice>& voice = iter.Data();
443 :
444 : // This service can only speak one utterance at a time, so we set
445 : // aQueuesUtterances to true in order to track global state and schedule
446 : // access to this service.
447 : DebugOnly<nsresult> rv =
448 0 : registry->AddVoice(this, iter.Key(), voice->mName, voice->mLanguage,
449 0 : voice->mName.EqualsLiteral("default"), true);
450 :
451 0 : NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to add voice");
452 : }
453 :
454 0 : mInitThread->Shutdown();
455 0 : mInitThread = nullptr;
456 :
457 0 : mInitialized = true;
458 :
459 0 : registry->NotifyVoicesChanged();
460 0 : }
461 :
462 : // nsIObserver
463 :
464 : NS_IMETHODIMP
465 0 : SpeechDispatcherService::Observe(nsISupports* aSubject, const char* aTopic,
466 : const char16_t* aData)
467 : {
468 0 : return NS_OK;
469 : }
470 :
471 : // nsISpeechService
472 :
473 : // TODO: Support SSML
474 : NS_IMETHODIMP
475 0 : SpeechDispatcherService::Speak(const nsAString& aText, const nsAString& aUri,
476 : float aVolume, float aRate, float aPitch,
477 : nsISpeechTask* aTask)
478 : {
479 0 : if (NS_WARN_IF(!mInitialized)) {
480 0 : return NS_ERROR_NOT_AVAILABLE;
481 : }
482 :
483 : RefPtr<SpeechDispatcherCallback> callback =
484 0 : new SpeechDispatcherCallback(aTask, this);
485 :
486 0 : bool found = false;
487 0 : SpeechDispatcherVoice* voice = mVoices.GetWeak(aUri, &found);
488 :
489 0 : if(NS_WARN_IF(!(found))) {
490 0 : return NS_ERROR_NOT_AVAILABLE;
491 : }
492 :
493 0 : spd_set_synthesis_voice(mSpeechdClient,
494 0 : NS_ConvertUTF16toUTF8(voice->mName).get());
495 :
496 : // We provide a volume of 0.0 to 1.0, speech-dispatcher expects 0 - 100.
497 0 : spd_set_volume(mSpeechdClient, static_cast<int>(aVolume * 100));
498 :
499 : // aRate is a value of 0.1 (0.1x) to 10 (10x) with 1 (1x) being normal rate.
500 : // speechd expects -100 to 100 with 0 being normal rate.
501 0 : float rate = 0;
502 0 : if (aRate > 1) {
503 : // Each step to 100 is logarithmically distributed up to 2.5x.
504 0 : rate = log10(std::min(aRate, MAX_RATE)) / log10(MAX_RATE) * 100;
505 0 : } else if (aRate < 1) {
506 : // Each step to -100 is logarithmically distributed down to 0.5x.
507 0 : rate = log10(std::max(aRate, MIN_RATE)) / log10(MIN_RATE) * -100;
508 : }
509 :
510 0 : spd_set_voice_rate(mSpeechdClient, static_cast<int>(rate));
511 :
512 : // We provide a pitch of 0 to 2 with 1 being the default.
513 : // speech-dispatcher expects -100 to 100 with 0 being default.
514 0 : spd_set_voice_pitch(mSpeechdClient, static_cast<int>((aPitch - 1) * 100));
515 :
516 : // The last three parameters don't matter for an indirect service
517 0 : nsresult rv = aTask->Setup(callback, 0, 0, 0);
518 :
519 0 : if (NS_FAILED(rv)) {
520 0 : return rv;
521 : }
522 :
523 0 : if (aText.Length()) {
524 0 : int msg_id = spd_say(
525 0 : mSpeechdClient, SPD_MESSAGE, NS_ConvertUTF16toUTF8(aText).get());
526 :
527 0 : if (msg_id < 0) {
528 0 : return NS_ERROR_FAILURE;
529 : }
530 :
531 0 : mCallbacks.Put(msg_id, callback);
532 : } else {
533 : // Speech dispatcher does not work well with empty strings.
534 : // In that case, don't send empty string to speechd,
535 : // and just emulate a speechd start and end event.
536 0 : NS_DispatchToMainThread(NewRunnableMethod<SPDNotificationType>(
537 : "dom::SpeechDispatcherCallback::OnSpeechEvent",
538 : callback,
539 : &SpeechDispatcherCallback::OnSpeechEvent,
540 0 : SPD_EVENT_BEGIN));
541 :
542 0 : NS_DispatchToMainThread(NewRunnableMethod<SPDNotificationType>(
543 : "dom::SpeechDispatcherCallback::OnSpeechEvent",
544 : callback,
545 : &SpeechDispatcherCallback::OnSpeechEvent,
546 0 : SPD_EVENT_END));
547 : }
548 :
549 0 : return NS_OK;
550 : }
551 :
552 : NS_IMETHODIMP
553 0 : SpeechDispatcherService::GetServiceType(SpeechServiceType* aServiceType)
554 : {
555 0 : *aServiceType = nsISpeechService::SERVICETYPE_INDIRECT_AUDIO;
556 0 : return NS_OK;
557 : }
558 :
559 : SpeechDispatcherService*
560 0 : SpeechDispatcherService::GetInstance(bool create)
561 : {
562 0 : if (XRE_GetProcessType() != GeckoProcessType_Default) {
563 0 : MOZ_ASSERT(false,
564 : "SpeechDispatcherService can only be started on main gecko process");
565 : return nullptr;
566 : }
567 :
568 0 : if (!sSingleton && create) {
569 0 : sSingleton = new SpeechDispatcherService();
570 0 : sSingleton->Init();
571 : }
572 :
573 0 : return sSingleton;
574 : }
575 :
576 : already_AddRefed<SpeechDispatcherService>
577 0 : SpeechDispatcherService::GetInstanceForService()
578 : {
579 0 : MOZ_ASSERT(NS_IsMainThread());
580 0 : RefPtr<SpeechDispatcherService> sapiService = GetInstance();
581 0 : return sapiService.forget();
582 : }
583 :
584 : void
585 0 : SpeechDispatcherService::EventNotify(uint32_t aMsgId, uint32_t aState)
586 : {
587 0 : SpeechDispatcherCallback* callback = mCallbacks.GetWeak(aMsgId);
588 :
589 0 : if (callback) {
590 0 : if (callback->OnSpeechEvent((SPDNotificationType)aState)) {
591 0 : mCallbacks.Remove(aMsgId);
592 : }
593 : }
594 0 : }
595 :
596 : void
597 0 : SpeechDispatcherService::Shutdown()
598 : {
599 0 : if (!sSingleton) {
600 0 : return;
601 : }
602 :
603 0 : sSingleton = nullptr;
604 : }
605 :
606 : } // namespace dom
607 9 : } // namespace mozilla
|