PlatformSpeechSynthesizerMac.mm   [plain text]

/*
* Copyright (C) 2013 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#import "config.h"
#import "PlatformSpeechSynthesizer.h"

#if ENABLE(SPEECH_SYNTHESIS) && PLATFORM(MAC)

#import "PlatformSpeechSynthesisUtterance.h"
#import "PlatformSpeechSynthesisVoice.h"
#import <AppKit/NSSpeechSynthesizer.h>
#import <pal/spi/mac/SpeechSynthesisSPI.h>
#import <wtf/RetainPtr.h>

@interface WebSpeechSynthesisWrapper : NSObject<NSSpeechSynthesizerDelegate>
{
WebCore::PlatformSpeechSynthesizer* m_synthesizerObject;
// Hold a Ref to the utterance so that it won't disappear until the synth is done with it.
WebCore::PlatformSpeechSynthesisUtterance* m_utterance;

RetainPtr<NSSpeechSynthesizer> m_synthesizer;
float m_basePitch;
}

- (WebSpeechSynthesisWrapper *)initWithSpeechSynthesizer:(WebCore::PlatformSpeechSynthesizer *)synthesizer;
- (void)speakUtterance:(WebCore::PlatformSpeechSynthesisUtterance *)utterance;

@end

@implementation WebSpeechSynthesisWrapper

- (WebSpeechSynthesisWrapper *)initWithSpeechSynthesizer:(WebCore::PlatformSpeechSynthesizer *)synthesizer
{
if (!(self = [super init]))
return nil;

m_synthesizerObject = synthesizer;
[self updateBasePitchForSynthesizer];
return self;
}

// NSSpeechSynthesizer expects a Words per Minute (WPM) rate. There is no preset default
// but they recommend that normal speaking is 180-220 WPM
- (float)convertRateToWPM:(float)rate
{
// We'll say 200 WPM is the default 1x value.
return 200.0f * rate;
}

- (float)convertPitchToNSSpeechValue:(float)pitch
{
// This allows the base pitch to range from 0% - 200% of the normal pitch.
return m_basePitch * pitch;
}

- (void)updateBasePitchForSynthesizer
{
// Reset the base pitch whenever we change voices, since the base pitch is different for each voice.
[m_synthesizer setObject:nil forProperty:NSSpeechResetProperty error:nil];
m_basePitch = [[m_synthesizer objectForProperty:NSSpeechPitchBaseProperty error:nil] floatValue];
}

- (void)speakUtterance:(WebCore::PlatformSpeechSynthesisUtterance *)utterance
{
// When speak is called we should not have an existing speech utterance outstanding.
ASSERT(!m_utterance);
ASSERT(utterance);

if (!m_synthesizer) {
m_synthesizer = adoptNS([[NSSpeechSynthesizer alloc] initWithVoice:nil]);
[m_synthesizer setDelegate:self];
}

// Find if we should use a specific voice based on the voiceURI in utterance.
// Otherwise, find the voice that matches the language. The Mac doesn't have a default voice per language, so the first
// one will have to do.

WebCore::PlatformSpeechSynthesisVoice* utteranceVoice = utterance->voice();
// If no voice was specified, try to match by language.
if (!utteranceVoice && !utterance->lang().isEmpty()) {
for (auto& voice : m_synthesizerObject->voiceList()) {
if (equalIgnoringASCIICase(utterance->lang(), voice->lang())) {
utteranceVoice = voice.get();
if (voice->isDefault())
break;
}
}
}

NSString *voiceURI = nil;
if (utteranceVoice)
voiceURI = utteranceVoice->voiceURI();
else
voiceURI = [NSSpeechSynthesizer defaultVoice];

// Don't set the voice unless necessary. There's a bug in NSSpeechSynthesizer such that
// setting the voice for the first time will cause the first speechDone callback to report it was unsuccessful.
BOOL updatePitch = NO;
if (![[m_synthesizer voice] isEqualToString:voiceURI]) {
[m_synthesizer setVoice:voiceURI];
// Reset the base pitch whenever we change voices.
updatePitch = YES;
}

if (m_basePitch == 0 || updatePitch)
[self updateBasePitchForSynthesizer];

[m_synthesizer setObject:[NSNumber numberWithFloat:[self convertPitchToNSSpeechValue:utterance->pitch()]] forProperty:NSSpeechPitchBaseProperty error:nil];
[m_synthesizer setRate:[self convertRateToWPM:utterance->rate()]];
[m_synthesizer setVolume:utterance->volume()];

m_utterance = utterance;
[m_synthesizer startSpeakingString:utterance->text()];
m_synthesizerObject->client()->didStartSpeaking(*m_utterance);
}

- (void)pause
{
if (!m_utterance)
return;

[m_synthesizer pauseSpeakingAtBoundary:NSSpeechImmediateBoundary];
m_synthesizerObject->client()->didPauseSpeaking(*m_utterance);
}

- (void)resume
{
if (!m_utterance)
return;

[m_synthesizer continueSpeaking];
m_synthesizerObject->client()->didResumeSpeaking(*m_utterance);
}

- (void)cancel
{
if (!m_utterance)
return;

[m_synthesizer stopSpeakingAtBoundary:NSSpeechImmediateBoundary];
m_synthesizerObject->client()->speakingErrorOccurred(*m_utterance);
m_utterance = 0;
}

- (void)invalidate
{
m_utterance = 0;
[m_synthesizer setDelegate:nil];
[m_synthesizer stopSpeakingAtBoundary:NSSpeechImmediateBoundary];
}

- (void)speechSynthesizer:(NSSpeechSynthesizer *)sender didFinishSpeaking:(BOOL)finishedSpeaking
{
if (!m_utterance)
return;

UNUSED_PARAM(sender);

// Clear the m_utterance variable in case finish speaking kicks off a new speaking job immediately.
WebCore::PlatformSpeechSynthesisUtterance* utterance = m_utterance;
m_utterance = 0;

if (finishedSpeaking)
m_synthesizerObject->client()->didFinishSpeaking(*utterance);
else
m_synthesizerObject->client()->speakingErrorOccurred(*utterance);
}

- (void)speechSynthesizer:(NSSpeechSynthesizer *)sender willSpeakWord:(NSRange)characterRange ofString:(NSString *)string
{
UNUSED_PARAM(sender);
UNUSED_PARAM(string);

if (!m_utterance)
return;

// Mac platform only supports word boundaries.
m_synthesizerObject->client()->boundaryEventOccurred(*m_utterance, WebCore::SpeechBoundary::SpeechWordBoundary, characterRange.location);
}

@end

namespace WebCore {

PlatformSpeechSynthesizer::PlatformSpeechSynthesizer(PlatformSpeechSynthesizerClient* client)
: m_speechSynthesizerClient(client)
{
}

PlatformSpeechSynthesizer::~PlatformSpeechSynthesizer()
{
[m_platformSpeechWrapper.get() invalidate];
}

static NSArray *speechSynthesisGetVoiceIdentifiers()
{
// Get all the voices offered by TTS.
// By default speech only returns "premium" voices, which does not include all the
// international voices. This allows us to offer speech synthesis for all supported languages.
return CFBridgingRelease(CopySpeechSynthesisVoicesForMode((__bridge CFArrayRef)@[ @"VoiceGroupDefault", @"VoiceGroupCompact" ]));
}

static NSString *speechSynthesisGetDefaultVoiceIdentifierForLocale(NSLocale *userLocale)
{
if (!userLocale)
return nil;

return (__bridge NSString *)GetIdentifierStringForPreferredVoiceInListWithLocale((__bridge CFArrayRef)speechSynthesisGetVoiceIdentifiers(), (__bridge CFLocaleRef)userLocale);
}

void PlatformSpeechSynthesizer::initializeVoiceList()
{
NSArray *availableVoices = speechSynthesisGetVoiceIdentifiers();
NSUInteger count = [availableVoices count];
for (NSUInteger k = 0; k < count; k++) {
NSString *voiceName = [availableVoices objectAtIndex:k];
NSDictionary *attributes = [NSSpeechSynthesizer attributesForVoice:voiceName];

NSString *voiceURI = [attributes objectForKey:NSVoiceIdentifier];
NSString *name = [attributes objectForKey:NSVoiceName];
NSString *language = [attributes objectForKey:NSVoiceLocaleIdentifier];
NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:language];
NSString *defaultVoiceURI = speechSynthesisGetDefaultVoiceIdentifierForLocale(locale);
[locale release];

// Change to BCP-47 format as defined by spec.
language = [language stringByReplacingOccurrencesOfString:@"_" withString:@"-"];

bool isDefault = [defaultVoiceURI isEqualToString:voiceURI];

m_voiceList.append(PlatformSpeechSynthesisVoice::create(voiceURI, name, language, true, isDefault));
}
}

void PlatformSpeechSynthesizer::pause()
{
[m_platformSpeechWrapper.get() pause];
}

void PlatformSpeechSynthesizer::resume()
{
[m_platformSpeechWrapper.get() resume];
}

void PlatformSpeechSynthesizer::speak(RefPtr<PlatformSpeechSynthesisUtterance>&& utterance)
{
if (!m_platformSpeechWrapper)
m_platformSpeechWrapper = adoptNS([[WebSpeechSynthesisWrapper alloc] initWithSpeechSynthesizer:this]);

[m_platformSpeechWrapper.get() speakUtterance:utterance.get()];
}

void PlatformSpeechSynthesizer::cancel()
{
[m_platformSpeechWrapper.get() cancel];
}

void PlatformSpeechSynthesizer::resetState()
{
[m_platformSpeechWrapper.get() cancel];
}

} // namespace WebCore

#endif // ENABLE(SPEECH_SYNTHESIS) && PLATFORM(MAC)