/* 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 2 * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ // 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; } - (id)initWithManager:(MacOSXTextToSpeechManager*)ttsManager; - (void)speechSynthesizer:(NSSpeechSynthesizer *)sender didFinishSpeaking:(BOOL)finishedSpeaking; @end @implementation MacOSXTextToSpeechManagerDelegate - (id)initWithManager:(MacOSXTextToSpeechManager*)ttsManager { self = [super init]; _ttsManager = ttsManager; return self; } - (void)speechSynthesizer:(NSSpeechSynthesizer *)sender didFinishSpeaking:(BOOL)finishedSpeaking { _ttsManager->startNextSpeech(); } @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(Common::String text, Action action, Common::String encoding) { 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 == text) return true; [synthesizer stopSpeakingAtBoundary:NSSpeechWordBoundary]; } else if (action == QUEUE_NO_REPEAT) { if (!_messageQueue.empty()) { if (_messageQueue.back().text == text) return true; } else if (_currentSpeech == text) return true; } } if (encoding.empty()) { #ifdef USE_TRANSLATION encoding = TransMan.getCurrentCharset(); #endif } _messageQueue.push(SpeechText(text, encoding)); if (!isSpeaking()) startNextSpeech(); return true; } bool MacOSXTextToSpeechManager::startNextSpeech() { _currentSpeech.clear(); if (_messageQueue.empty()) return false; SpeechText text = _messageQueue.pop(); // Get current encoding CFStringEncoding stringEncoding = kCFStringEncodingASCII; if (!text.encoding.empty()) { CFStringRef encStr = CFStringCreateWithCString(NULL, text.encoding.c_str(), kCFStringEncodingASCII); stringEncoding = CFStringConvertIANACharSetNameToEncoding(encStr); CFRelease(encStr); } CFStringRef textNSString = CFStringCreateWithCString(NULL, text.text.c_str(), stringEncoding); bool status = [synthesizer startSpeakingString:(NSString *)textNSString]; CFRelease(textNSString); if (status) _currentSpeech = text.text; return status; } bool MacOSXTextToSpeechManager::stop() { _messageQueue.clear(); _currentSpeech.clear(); // so that it immediately reports that it is no longer speeking // Stop as soon as possible [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 speeking 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(); } 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