/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ // Disable symbol overrides so that we can use system headers. #define FORBIDDEN_SYMBOL_ALLOW_ALL #include "backends/text-to-speech/macosx/macosx-text-to-speech.h" #if defined(USE_TTS) && defined(MACOSX) #include "common/translation.h" #include #include #include @interface MacOSXTextToSpeechManagerDelegate : NSObject { MacOSXTextToSpeechManager *_ttsManager; BOOL _ignoreNextFinishedSpeaking; } - (id)initWithManager:(MacOSXTextToSpeechManager*)ttsManager; - (void)speechSynthesizer:(NSSpeechSynthesizer *)sender didFinishSpeaking:(BOOL)finishedSpeaking; - (void)ignoreNextFinishedSpeaking:(BOOL)ignore; @end @implementation MacOSXTextToSpeechManagerDelegate - (id)initWithManager:(MacOSXTextToSpeechManager*)ttsManager { self = [super init]; _ttsManager = ttsManager; _ignoreNextFinishedSpeaking = NO; return self; } - (void)speechSynthesizer:(NSSpeechSynthesizer *)sender didFinishSpeaking:(BOOL)finishedSpeaking { if (!_ignoreNextFinishedSpeaking) _ttsManager->startNextSpeech(); _ignoreNextFinishedSpeaking = NO; } - (void)ignoreNextFinishedSpeaking:(BOOL)ignore { _ignoreNextFinishedSpeaking = ignore; } @end NSSpeechSynthesizer *synthesizer; MacOSXTextToSpeechManagerDelegate *synthesizerDelegate; MacOSXTextToSpeechManager::MacOSXTextToSpeechManager() : Common::TextToSpeechManager(), _paused(false) { synthesizer = [[NSSpeechSynthesizer alloc] init]; synthesizerDelegate = [[MacOSXTextToSpeechManagerDelegate alloc] initWithManager:this]; [synthesizer setDelegate:synthesizerDelegate]; #ifdef USE_TRANSLATION setLanguage(TransMan.getCurrentLanguage()); #else setLanguage("en"); #endif } MacOSXTextToSpeechManager::~MacOSXTextToSpeechManager() { clearState(); [synthesizer release]; [synthesizerDelegate release]; } bool MacOSXTextToSpeechManager::say(const Common::U32String &text, Action action) { Common::String textToSpeak = text.encode(); if (isSpeaking()) { // Interruptions are done on word boundaries for nice transitions. // Should we interrupt immediately? if (action == DROP) return true; else if (action == INTERRUPT) { _messageQueue.clear(); [synthesizer stopSpeakingAtBoundary:NSSpeechWordBoundary]; } else if (action == INTERRUPT_NO_REPEAT) { // If the new speech is the one being currently said, continue that speech but clear the queue. // And otherwise both clear the queue and interrupt the current speech. _messageQueue.clear(); if (_currentSpeech == textToSpeak) return true; [synthesizer stopSpeakingAtBoundary:NSSpeechWordBoundary]; } else if (action == QUEUE_NO_REPEAT) { if (!_messageQueue.empty()) { if (_messageQueue.back() == textToSpeak) return true; } else if (_currentSpeech == textToSpeak) return true; } } _messageQueue.push(textToSpeak); if (!isSpeaking()) startNextSpeech(); return true; } bool MacOSXTextToSpeechManager::startNextSpeech() { _currentSpeech.clear(); if (_messageQueue.empty()) return false; Common::String textToSpeak; do { textToSpeak = _messageQueue.pop(); } while (textToSpeak.empty() && !_messageQueue.empty()); if (textToSpeak.empty()) return false; // Get current encoding CFStringEncoding stringEncoding = kCFStringEncodingUTF8; CFStringRef textNSString = CFStringCreateWithCString(NULL, textToSpeak.c_str(), stringEncoding); bool status = [synthesizer startSpeakingString:(NSString *)textNSString]; CFRelease(textNSString); if (status) _currentSpeech = textToSpeak; return status; } bool MacOSXTextToSpeechManager::stop() { _messageQueue.clear(); if (isSpeaking()) { _currentSpeech.clear(); // so that it immediately reports that it is no longer speaking // Stop as soon as possible // Also tell the MacOSXTextToSpeechManagerDelegate to ignore the next finishedSpeaking as // it has already been handled, but we might have started another speech by the time we // receive it, and we don't want to stop that one. [synthesizerDelegate ignoreNextFinishedSpeaking:YES]; [synthesizer stopSpeakingAtBoundary:NSSpeechImmediateBoundary]; } return true; } bool MacOSXTextToSpeechManager::pause() { // Pause on a word boundary as pausing/resuming in a middle of words is strange. [synthesizer pauseSpeakingAtBoundary:NSSpeechWordBoundary]; _paused = true; return true; } bool MacOSXTextToSpeechManager::resume() { _paused = false; [synthesizer continueSpeaking]; return true; } bool MacOSXTextToSpeechManager::isSpeaking() { // Because the NSSpeechSynthesizer is asynchronous, it doesn't start speaking immediately // and thus using [synthesizer isSpeaking] just after [synthesizer startSpeakingString:]] is // likely to return NO. So instead we check the _currentSpeech string (set when calling // startSpeakingString, and cleared when we receive the didFinishSpeaking message). //return [synthesizer isSpeaking]; return !_currentSpeech.empty(); } bool MacOSXTextToSpeechManager::isPaused() { // Because the NSSpeechSynthesizer is asynchronous, and because we pause at the end of a word // and not immediately, we cannot check the speech status as it is likely to not be paused yet // immediately after we requested the pause. So we keep our own flag. //NSDictionary *statusDict = (NSDictionary*) [synthesizer objectForProperty:NSSpeechStatusProperty error:nil]; //return [[statusDict objectForKey:NSSpeechStatusOutputBusy] boolValue] && [[statusDict objectForKey:NSSpeechStatusOutputPaused] boolValue]; return _paused; } bool MacOSXTextToSpeechManager::isReady() { // See comments in isSpeaking() and isPaused() //NSDictionary *statusDict = (NSDictionary*) [synthesizer objectForProperty:NSSpeechStatusProperty error:nil]; //return [[statusDict objectForKey:NSSpeechStatusOutputBusy] boolValue] == NO; return _currentSpeech.empty() && !_paused; } void MacOSXTextToSpeechManager::setVoice(unsigned index) { if (_ttsState->_availableVoices.empty()) return; assert(index < _ttsState->_availableVoices.size()); Common::TTSVoice voice = _ttsState->_availableVoices[index]; _ttsState->_activeVoice = index; [synthesizer setVoice:(NSString*)voice.getData()]; // Setting the voice reset the pitch and rate to the voice defaults. // Apply back the modifiers. int pitch = getPitch(), rate = getRate(); Common::TextToSpeechManager::setPitch(0); Common::TextToSpeechManager::setRate(0); setPitch(pitch); setRate(rate); } void MacOSXTextToSpeechManager::setRate(int rate) { int oldRate = getRate(); Common::TextToSpeechManager::setRate(rate); // The rate is a value between -100 and +100, with 0 being the default rate. // Convert this to a multiplier between 0.5 and 1.5. float oldRateMultiplier = 1.0f + oldRate / 200.0f; float ratehMultiplier = 1.0f + rate / 200.0f; synthesizer.rate = synthesizer.rate / oldRateMultiplier * ratehMultiplier; } void MacOSXTextToSpeechManager::setPitch(int pitch) { int oldPitch = getPitch(); Common::TextToSpeechManager::setPitch(pitch); // The pitch is a value between -100 and +100, with 0 being the default pitch. // Convert this to a multiplier between 0.5 and 1.5 on the default voice pitch. float oldPitchMultiplier = 1.0f + oldPitch / 200.0f; float pitchMultiplier = 1.0f + pitch / 200.0f; NSNumber *basePitchNumber = [synthesizer objectForProperty:NSSpeechPitchBaseProperty error:nil]; float basePitch = [basePitchNumber floatValue] / oldPitchMultiplier * pitchMultiplier; [synthesizer setObject:[NSNumber numberWithFloat:basePitch] forProperty:NSSpeechPitchBaseProperty error:nil]; } void MacOSXTextToSpeechManager::setVolume(unsigned volume) { Common::TextToSpeechManager::setVolume(volume); synthesizer.volume = volume / 100.0f; } void MacOSXTextToSpeechManager::setLanguage(Common::String language) { Common::TextToSpeechManager::setLanguage(language); updateVoices(); } int MacOSXTextToSpeechManager::getDefaultVoice() { if (_ttsState->_availableVoices.size() < 2) return 0; NSString *defaultVoice = [NSSpeechSynthesizer defaultVoice]; if (defaultVoice == nil) return 0; for (unsigned int i = 0 ; i < _ttsState->_availableVoices.size() ; ++i) { if ([defaultVoice isEqualToString:(NSString*)(_ttsState->_availableVoices[i].getData())]) return i; } return 0; } void MacOSXTextToSpeechManager::freeVoiceData(void *data) { NSString* voiceId = (NSString*)data; [voiceId release]; } void MacOSXTextToSpeechManager::updateVoices() { Common::String currentVoice; if (!_ttsState->_availableVoices.empty()) currentVoice = _ttsState->_availableVoices[_ttsState->_activeVoice].getDescription(); _ttsState->_availableVoices.clear(); int activeVoiceIndex = -1, defaultVoiceIndex = -1; Common::String lang = getLanguage(); NSArray *voices = [NSSpeechSynthesizer availableVoices]; NSString *defaultVoice = [NSSpeechSynthesizer defaultVoice]; int voiceIndex = 0; for (NSString *voiceId in voices) { NSDictionary *voiceAttr = [NSSpeechSynthesizer attributesForVoice:voiceId]; Common::String voiceLocale([[voiceAttr objectForKey:NSVoiceLocaleIdentifier] UTF8String]); if (voiceLocale.hasPrefix(lang)) { NSString *data = [[NSString alloc] initWithString:voiceId]; Common::String name([[voiceAttr objectForKey:NSVoiceName] UTF8String]); Common::TTSVoice::Gender gender = Common::TTSVoice::UNKNOWN_GENDER; NSString *voiceGender = [voiceAttr objectForKey:NSVoiceGender]; if (voiceGender != nil) { // This can be VoiceGenderMale, VoiceGenderFemale, VoiceGenderNeuter if ([voiceGender isEqualToString:@"VoiceGenderMale"]) gender = Common::TTSVoice::MALE; else if ([voiceGender isEqualToString:@"VoiceGenderFemale"]) gender = Common::TTSVoice::FEMALE; } Common::TTSVoice::Age age = Common::TTSVoice::UNKNOWN_AGE; NSNumber *voiceAge = [voiceAttr objectForKey:NSVoiceAge]; if (voiceAge != nil) { if ([voiceAge integerValue] < 18) age = Common::TTSVoice::CHILD; else age = Common::TTSVoice::ADULT; } Common::TTSVoice voice(gender, age, data, name); _ttsState->_availableVoices.push_back(voice); if (name == currentVoice) activeVoiceIndex = voiceIndex; if (defaultVoice != nil && [defaultVoice isEqualToString:voiceId]) defaultVoiceIndex = voiceIndex; ++voiceIndex; } } if (activeVoiceIndex == -1) activeVoiceIndex = defaultVoiceIndex == -1 ? 0 : defaultVoiceIndex; setVoice(activeVoiceIndex); } #endif