[Scummvm-git-logs] scummvm master -> ef57720515088d1dd61ac46b34683caef09777c1
criezy
noreply at scummvm.org
Mon Feb 2 20:33:17 UTC 2026
This automated email contains information about 6 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
28cbd667d5 TTS: MACOS, IOS: Implement Text to Speech using AVSpeechSynthesizer
263814d0da CREATE_PROJECT: Add AVFAudio framework to Xcode project
b30eaf2903 TTS: MACOS, IOS: Add version checks for voice gender API
de24d902e1 CREATE_PROJECT: Use legacy TTS API for macOS Xcode project not on Apple Silicon
58fa5b5524 DOC: IOS: TTS can now be enabled in iOS builds
ef57720515 NEWS: Mention support for TTS on iOS
Commit: 28cbd667d57533618ee89eb7142e9c4ff26f0016
https://github.com/scummvm/scummvm/commit/28cbd667d57533618ee89eb7142e9c4ff26f0016
Author: Thierry Crozat (criezy at scummvm.org)
Date: 2026-02-02T20:33:05Z
Commit Message:
TTS: MACOS, IOS: Implement Text to Speech using AVSpeechSynthesizer
The NSSpeechSynthesizer API has been deprecated in recent macOS versions.
This commits adds an implementation using the newer AVSpeechSynthesizer,
available since macOS 10.14. The new implementation is only used when
building for Apple Silicon. In theory we could also use it when building
for 64 bit Intel macs and targetting macOS 10.14 or above, but there is
not much benefit in doing so.
As a bonus it should also work on iOS.
Changed paths:
A backends/text-to-speech/avfaudio/avfaudio-text-to-speech.h
A backends/text-to-speech/avfaudio/avfaudio-text-to-speech.mm
backends/module.mk
backends/platform/ios7/ios7_osys_main.cpp
backends/platform/sdl/macosx/macosx.cpp
configure
ports.mk
diff --git a/backends/module.mk b/backends/module.mk
index 2dd0b82f6c5..d1ae9063d30 100644
--- a/backends/module.mk
+++ b/backends/module.mk
@@ -104,7 +104,7 @@ MODULE_OBJS += \
fs/emscripten/emscripten-fs-factory.o \
fs/emscripten/emscripten-posix-fs.o \
fs/emscripten/http-fs.o \
- midi/webmidi.o
+ midi/webmidi.o
ifdef USE_CLOUD
MODULE_OBJS += \
fs/emscripten/cloud-fs.o
@@ -299,8 +299,13 @@ MODULE_OBJS += \
taskbar/macosx/macosx-taskbar.o
ifdef USE_TTS
+ifdef USE_NS_SPEECH_SYNTHESIZER
MODULE_OBJS += \
text-to-speech/macosx/macosx-text-to-speech.o
+else
+MODULE_OBJS += \
+ text-to-speech/avfaudio/avfaudio-text-to-speech.o
+endif
endif
ifdef SDL_BACKEND
@@ -436,6 +441,12 @@ MODULE_OBJS += \
mutex/pthread/pthread-mutex.o \
graphics/ios/ios-graphics.o \
graphics/ios/renderbuffer.o
+
+ifdef USE_TTS
+MODULE_OBJS += \
+ text-to-speech/avfaudio/avfaudio-text-to-speech.o
+endif
+
endif
ifeq ($(BACKEND),maemo)
diff --git a/backends/platform/ios7/ios7_osys_main.cpp b/backends/platform/ios7/ios7_osys_main.cpp
index f69eec2616d..ad3a5d967bf 100644
--- a/backends/platform/ios7/ios7_osys_main.cpp
+++ b/backends/platform/ios7/ios7_osys_main.cpp
@@ -53,6 +53,7 @@
#include "backends/mutex/pthread/pthread-mutex.h"
#include "backends/fs/chroot/chroot-fs-factory.h"
#include "backends/fs/posix/posix-fs.h"
+#include "backends/text-to-speech/avfaudio/avfaudio-text-to-speech.h"
#include "audio/mixer.h"
#include "audio/mixer_intern.h"
@@ -160,6 +161,11 @@ void OSystem_iOS7::initBackend() {
_graphicsManager = new iOSGraphicsManager();
+#ifdef USE_TTS
+ // Initialize Text to Speech manager
+ _textToSpeechManager = new AVFAudioTextToSpeechManager();
+#endif
+
setupMixer();
setTimerCallback(&OSystem_iOS7::timerHandler, 10);
diff --git a/backends/platform/sdl/macosx/macosx.cpp b/backends/platform/sdl/macosx/macosx.cpp
index 4b653dd7255..38362237573 100644
--- a/backends/platform/sdl/macosx/macosx.cpp
+++ b/backends/platform/sdl/macosx/macosx.cpp
@@ -33,7 +33,11 @@
#include "backends/platform/sdl/macosx/macosx-window.h"
#include "backends/updates/macosx/macosx-updates.h"
#include "backends/taskbar/macosx/macosx-taskbar.h"
+#ifdef USE_NS_SPEECH_SYNTHESIZER
#include "backends/text-to-speech/macosx/macosx-text-to-speech.h"
+#else
+#include "backends/text-to-speech/avfaudio/avfaudio-text-to-speech.h"
+#endif
#include "backends/dialogs/macosx/macosx-dialogs.h"
#include "backends/platform/sdl/macosx/macosx_wrapper.h"
#include "backends/fs/posix/posix-fs.h"
@@ -114,7 +118,11 @@ void OSystem_MacOSX::initBackend() {
#ifdef USE_TTS
// Initialize Text to Speech manager
+#ifdef USE_NS_SPEECH_SYNTHESIZER
_textToSpeechManager = new MacOSXTextToSpeechManager();
+#else
+ _textToSpeechManager = new AVFAudioTextToSpeechManager();
+#endif
#endif
// Migrate savepath.
diff --git a/backends/text-to-speech/avfaudio/avfaudio-text-to-speech.h b/backends/text-to-speech/avfaudio/avfaudio-text-to-speech.h
new file mode 100644
index 00000000000..7c4cadfd542
--- /dev/null
+++ b/backends/text-to-speech/avfaudio/avfaudio-text-to-speech.h
@@ -0,0 +1,68 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef BACKENDS_TEXT_TO_SPEECH_AVFAUDIO_H
+#define BACKENDS_TEXT_TO_SPEECH_AVFAUDIO_H
+
+#include "common/scummsys.h"
+
+#if defined(USE_TTS) && (defined(MACOSX) || defined(IPHONE))
+
+#include "common/text-to-speech.h"
+#include "common/queue.h"
+#include "common/ustr.h"
+
+class AVFAudioTextToSpeechManager : public Common::TextToSpeechManager {
+public:
+ AVFAudioTextToSpeechManager();
+ ~AVFAudioTextToSpeechManager() override;
+
+ bool say(const Common::U32String &str, Action action) override;
+
+ bool stop() override;
+ bool pause() override;
+ bool resume() override;
+
+ bool isSpeaking() override;
+ bool isPaused() override;
+ bool isReady() override;
+
+ void setLanguage(Common::String language) override;
+ void setVoice(unsigned index) override;
+ int getDefaultVoice() override;
+ void freeVoiceData(void *data) override;
+
+ bool startNextSpeech();
+
+private:
+ void updateVoices() override;
+
+ Common::Queue<Common::String> _messageQueue;
+ Common::String _currentSpeech;
+ bool _paused;
+ bool _interruptRequested;
+};
+
+#endif
+
+#endif // BACKENDS_TEXT_TO_SPEECH_AVFAUDIO_H
+
+
diff --git a/backends/text-to-speech/avfaudio/avfaudio-text-to-speech.mm b/backends/text-to-speech/avfaudio/avfaudio-text-to-speech.mm
new file mode 100644
index 00000000000..a5541d41702
--- /dev/null
+++ b/backends/text-to-speech/avfaudio/avfaudio-text-to-speech.mm
@@ -0,0 +1,297 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Disable symbol overrides so that we can use system headers.
+#define FORBIDDEN_SYMBOL_ALLOW_ALL
+
+#include "backends/text-to-speech/avfaudio/avfaudio-text-to-speech.h"
+
+#if defined(USE_TTS) && (defined(MACOSX) || defined(IPHONE))
+#include "common/translation.h"
+#include <Foundation/NSString.h>
+#include <AVFoundation/AVFoundation.h>
+
+ at interface AVFAudioTextToSpeechManagerDelegate : NSObject<AVSpeechSynthesizerDelegate> {
+ AVFAudioTextToSpeechManager *_ttsManager;
+ BOOL _ignoreNextFinishedSpeaking;
+}
+- (id)initWithManager:(AVFAudioTextToSpeechManager*)ttsManager;
+- (void)speechSynthesizer:(AVSpeechSynthesizer *)sender didFinishSpeechUtterance:(AVSpeechUtterance *)utterance;
+- (void)speechSynthesizer:(AVSpeechSynthesizer *)sender didCancelSpeechUtterance:(AVSpeechUtterance *)utterance;
+- (void)ignoreNextFinishedSpeaking:(BOOL)ignore;
+ at end
+
+ at implementation AVFAudioTextToSpeechManagerDelegate
+- (id)initWithManager:(AVFAudioTextToSpeechManager*)ttsManager {
+ self = [super init];
+ _ttsManager = ttsManager;
+ _ignoreNextFinishedSpeaking = NO;
+ return self;
+}
+
+- (void)speechSynthesizer:(AVSpeechSynthesizer *)sender didFinishSpeechUtterance:(AVSpeechUtterance *) utterance {
+ if (!_ignoreNextFinishedSpeaking)
+ _ttsManager->startNextSpeech();
+ _ignoreNextFinishedSpeaking = NO;
+}
+
+- (void)speechSynthesizer:(AVSpeechSynthesizer *)sender didCancelSpeechUtterance:(AVSpeechUtterance *) utterance {
+ if (!_ignoreNextFinishedSpeaking)
+ _ttsManager->startNextSpeech();
+ _ignoreNextFinishedSpeaking = NO;
+}
+
+- (void)ignoreNextFinishedSpeaking:(BOOL)ignore {
+ _ignoreNextFinishedSpeaking = ignore;
+}
+ at end
+
+AVSpeechSynthesizer *synthesizer;
+AVFAudioTextToSpeechManagerDelegate *synthesizerDelegate;
+
+AVFAudioTextToSpeechManager::AVFAudioTextToSpeechManager() : Common::TextToSpeechManager(), _paused(false), _interruptRequested(false) {
+ synthesizer = [[AVSpeechSynthesizer alloc] init];
+ synthesizerDelegate = [[AVFAudioTextToSpeechManagerDelegate alloc] initWithManager:this];
+ [synthesizer setDelegate:synthesizerDelegate];
+
+#ifdef USE_TRANSLATION
+ setLanguage(TransMan.getCurrentLanguage());
+#else
+ setLanguage("en");
+#endif
+}
+
+AVFAudioTextToSpeechManager::~AVFAudioTextToSpeechManager() {
+ clearState();
+
+ [synthesizer release];
+ [synthesizerDelegate release];
+}
+
+bool AVFAudioTextToSpeechManager::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();
+ // If an interrupt was already requested but not yet processed do not request it again as it may end up
+ // interrpting the next speech.
+ if (!_interruptRequested) {
+ _interruptRequested = true;
+ [synthesizer stopSpeakingAtBoundary:AVSpeechBoundaryWord];
+ }
+ } 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;
+ // If an interrupt was already requested but not yet processed do not request it again as it may end up
+ // interrpting the next speech.
+ if (!_interruptRequested) {
+ _interruptRequested = true;
+ [synthesizer stopSpeakingAtBoundary:AVSpeechBoundaryWord];
+ }
+ } else if (action == QUEUE_NO_REPEAT) {
+ if (!_messageQueue.empty()) {
+ if (_messageQueue.back() == textToSpeak)
+ return true;
+ } else if (_currentSpeech == textToSpeak)
+ return true;
+ }
+ }
+
+ // We can queue utterances in the AVSpeechSynthesizer, however if we did that we could not
+ // unqueue them without also stopping the current utterance. That means we could not implement
+ // INTERRUPT_NO_REPEAT when the new text is the same as the current one, which we thus want to
+ // continue, and the queue has additionaltext we want to drop.
+ // So use our own queue and start each utterance manually.
+ _messageQueue.push(textToSpeak);
+ if (!isSpeaking())
+ startNextSpeech();
+ return true;
+}
+
+bool AVFAudioTextToSpeechManager::startNextSpeech() {
+ _currentSpeech.clear();
+ _interruptRequested = false;
+ if (_messageQueue.empty())
+ return false;
+
+ Common::String textToSpeak;
+ do {
+ textToSpeak = _messageQueue.pop();
+ } while (textToSpeak.empty() && !_messageQueue.empty());
+ if (textToSpeak.empty())
+ return false;
+
+ CFStringRef textNSString = CFStringCreateWithCString(NULL, textToSpeak.c_str(), kCFStringEncodingUTF8);
+ AVSpeechUtterance *utterance = [[AVSpeechUtterance alloc]initWithString:(NSString*)textNSString];
+ CFRelease(textNSString);
+ if (!_ttsState->_availableVoices.empty())
+ utterance.voice = [AVSpeechSynthesisVoice voiceWithIdentifier:(NSString *)(getVoice().getData())];
+ // 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 ratehMultiplier = 1.0f + getRate() / 200.0f;
+ utterance.rate = AVSpeechUtteranceDefaultSpeechRate * ratehMultiplier;
+ // 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.
+ utterance.pitchMultiplier = 1.0f + getPitch() / 200.0f;
+ utterance.volume = getVolume() / 100.0f;
+ //utterance.postUtteranceDelay = 0.1f;
+
+ [synthesizer speakUtterance:utterance];
+ [utterance release];
+ _currentSpeech = textToSpeak;
+
+ return true;
+}
+
+bool AVFAudioTextToSpeechManager::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 AVFAudioTextToSpeechManagerDelegate 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:AVSpeechBoundaryImmediate];
+ }
+ return true;
+}
+
+bool AVFAudioTextToSpeechManager::pause() {
+ // Pause on a word boundary as pausing/resuming in a middle of words is strange.
+ [synthesizer pauseSpeakingAtBoundary:AVSpeechBoundaryWord];
+ _paused = true;
+ return true;
+}
+
+bool AVFAudioTextToSpeechManager::resume() {
+ _paused = false;
+ [synthesizer continueSpeaking];
+ return true;
+}
+
+bool AVFAudioTextToSpeechManager::isSpeaking() {
+ // Because the AVSpeechSynthesizer 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 AVFAudioTextToSpeechManager::isPaused() {
+ // Because the AVSpeechSynthesizer 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.
+ //return [synthesizer isPaused];
+ return _paused;
+}
+
+bool AVFAudioTextToSpeechManager::isReady() {
+ // See comments in isSpeaking() and isPaused()
+ return _currentSpeech.empty() && !_paused;
+}
+
+void AVFAudioTextToSpeechManager::setLanguage(Common::String language) {
+ Common::TextToSpeechManager::setLanguage(language);
+ updateVoices();
+}
+
+void AVFAudioTextToSpeechManager::setVoice(unsigned index) {
+ if (_ttsState->_availableVoices.empty())
+ return;
+ assert(index < _ttsState->_availableVoices.size());
+ _ttsState->_activeVoice = index;
+}
+
+int AVFAudioTextToSpeechManager::getDefaultVoice() {
+ if (_ttsState->_availableVoices.size() < 2)
+ return 0;
+
+ Common::String lang = getLanguage();
+ CFStringRef langNSString = CFStringCreateWithCString(NULL, lang.c_str(), kCFStringEncodingUTF8);
+ AVSpeechSynthesisVoice *defaultVoice = [AVSpeechSynthesisVoice voiceWithLanguage:(NSString *)langNSString];
+ CFRelease(langNSString);
+
+ if (defaultVoice == nil)
+ return 0;
+ for (unsigned int i = 0 ; i < _ttsState->_availableVoices.size() ; ++i) {
+ if ([defaultVoice.identifier isEqualToString:(NSString*)(_ttsState->_availableVoices[i].getData())])
+ return i;
+ }
+ return 0;
+}
+
+void AVFAudioTextToSpeechManager::freeVoiceData(void *data) {
+ NSString* voiceId = (NSString*)data;
+ [voiceId release];
+}
+
+void AVFAudioTextToSpeechManager::updateVoices() {
+ Common::String currentVoice;
+ if (!_ttsState->_availableVoices.empty())
+ currentVoice = _ttsState->_availableVoices[_ttsState->_activeVoice].getDescription();
+ _ttsState->_availableVoices.clear();
+ int activeVoiceIndex = -1;
+
+ Common::String lang = getLanguage();
+ NSArray<AVSpeechSynthesisVoice *> *voices = [AVSpeechSynthesisVoice speechVoices];
+ int voiceIndex = 0;
+ for (AVSpeechSynthesisVoice *voice in voices) {
+ Common::String voiceLocale([voice.language UTF8String]);
+ if (voiceLocale.hasPrefix(lang)) {
+ NSString *data = [[NSString alloc] initWithString:voice.identifier];
+ Common::String name([voice.name UTF8String]);
+ Common::TTSVoice::Gender gender = Common::TTSVoice::UNKNOWN_GENDER;
+ switch (voice.gender) {
+ case AVSpeechSynthesisVoiceGenderMale:
+ gender = Common::TTSVoice::MALE;
+ break;
+ case AVSpeechSynthesisVoiceGenderFemale:
+ gender = Common::TTSVoice::FEMALE;
+ break;
+ case AVSpeechSynthesisVoiceGenderUnspecified:
+ gender = Common::TTSVoice::UNKNOWN_GENDER;
+ break;
+ }
+ Common::TTSVoice::Age age = Common::TTSVoice::UNKNOWN_AGE;
+ Common::TTSVoice ttsVoice(gender, age, data, name);
+ _ttsState->_availableVoices.push_back(ttsVoice);
+ if (name == currentVoice)
+ activeVoiceIndex = voiceIndex;
+ ++voiceIndex;
+ }
+ }
+
+ if (activeVoiceIndex == -1)
+ activeVoiceIndex = getDefaultVoice();
+ setVoice(activeVoiceIndex);
+}
+
+#endif
diff --git a/configure b/configure
index 0c97cea0cfb..5795a2dc3fd 100755
--- a/configure
+++ b/configure
@@ -192,6 +192,7 @@ _updates=no
_libunity=auto
_dialogs=auto
_tts=auto
+_osx_tts_backend=auto
_gtk=auto
_fribidi=auto
_discord=auto
@@ -3229,6 +3230,10 @@ EOF
fi
fi
+ # The TTS backend can use either NSSpeechSynthesizer or AVSpeechSynthesizer. The latter
+ # is only available since macOS 10.14, while the former is available in older versions
+ # but deprecated in macOS 12. This coincides with the switch to Apple Silicon CPUs.
+ _osx_tts_backend="NSSpeechSynthesizer"
case $_host_cpu in
powerpc*)
# We use -force_cpusubtype_ALL to ensure the binary runs on every
@@ -3245,6 +3250,7 @@ EOF
;;
aarch64)
add_line_to_config_mk 'MACOSX_ARM64 = 1'
+ _osx_tts_backend="AVSpeechSynthesizer"
;;
esac
@@ -5381,17 +5387,38 @@ EOF
cc_check -lspeechd && _tts=yes
;;
darwin*)
- # Check the API is available. The most recent API we need is for the NSSpeechSynthesizerDelegate protocol
- cat > $TMPC << EOF
+ if test "$_osx_tts_backend" = "NSSpeechSynthesizer" ; then
+ # Check older NSSpeechSynthesizer API.
+ cat > $TMPC << EOF
#include <AppKit/NSSpeechSynthesizer.h>
@interface SpeechDelegate : NSObject<NSSpeechSynthesizerDelegate> {
}
@end
int main(void) { return 0; }
+EOF
+ cc_check -ObjC++ -lobjc && _tts=yes
+ else
+ # Check the newer AVSpeechSynthesizer API is available
+ cat > $TMPC << EOF
+#import <AVFoundation/AVFoundation.h>
+ at interface SpeechDelegate : NSObject<AVSpeechSynthesizerDelegate> {
+}
+ at end
+int main(void) { return 0; }
+EOF
+ cc_check -ObjC++ -lobjc && _tts=yes
+ fi
+ ;;
+ iphoneos)
+ cat > $TMPC << EOF
+#import <AVFoundation/AVFoundation.h>
+ at interface SpeechDelegate : NSObject<AVSpeechSynthesizerDelegate> {
+}
+ at end
+int main(void) { return 0; }
EOF
cc_check -ObjC++ -lobjc && _tts=yes
;;
-
emscripten)
# Emscripten has the "Web Speech API" available
_tts=yes
@@ -7251,7 +7278,18 @@ else
append_var LIBS '-lsapi -lole32'
;;
darwin*)
- echo "osx"
+ if test "$_osx_tts_backend" = "NSSpeechSynthesizer" ; then
+ echo "osx (NSSpeechSynthesizer)"
+ _tts=yes
+ define_in_config_if_yes $_tts 'USE_NS_SPEECH_SYNTHESIZER'
+ else
+ echo "osx (AVSpeechSynthesizer)"
+ _tts=yes
+ append_var LIBS "-framework AVFoundation"
+ fi
+ ;;
+ iphoneos)
+ echo "ios (AVSpeechSynthesizer)"
_tts=yes
;;
emscripten)
diff --git a/ports.mk b/ports.mk
index af53e1787c5..caed8d7ab81 100644
--- a/ports.mk
+++ b/ports.mk
@@ -519,6 +519,12 @@ OSX_STATIC_LIBS += $(STATICLIBPATH)/lib/libintl.a
endif
endif
+ifdef USE_TTS
+ifndef USE_NS_SPEECH_SYNTHESIZER
+OSX_STATIC_LIBS += -framework AVFoundation
+endif
+endif
+
ifneq ($(BACKEND), ios7)
OSX_STATIC_LIBS += -lreadline
endif
Commit: 263814d0da833c222b66f8258e0f5445b670b987
https://github.com/scummvm/scummvm/commit/263814d0da833c222b66f8258e0f5445b670b987
Author: Lars Sundström (l.sundstrom at gmail.com)
Date: 2026-02-02T20:33:05Z
Commit Message:
CREATE_PROJECT: Add AVFAudio framework to Xcode project
The new TTS backend utilises the new AVFAudio framework.
Changed paths:
devtools/create_project/xcode.cpp
diff --git a/devtools/create_project/xcode.cpp b/devtools/create_project/xcode.cpp
index 45732b97105..828275ec6dd 100644
--- a/devtools/create_project/xcode.cpp
+++ b/devtools/create_project/xcode.cpp
@@ -440,6 +440,7 @@ void XcodeProvider::setupFrameworksBuildPhase(const BuildSetup &setup) {
DEF_SYSFRAMEWORK("ApplicationServices");
DEF_SYSFRAMEWORK("AudioToolbox");
DEF_SYSFRAMEWORK("AudioUnit");
+ DEF_SYSFRAMEWORK("AVFAudio");
DEF_SYSFRAMEWORK("Carbon");
DEF_SYSFRAMEWORK("Cocoa");
DEF_SYSFRAMEWORK("CoreAudio");
@@ -602,6 +603,7 @@ void XcodeProvider::setupFrameworksBuildPhase(const BuildSetup &setup) {
frameworks_osx.push_back("CoreFoundation.framework");
frameworks_osx.push_back("Foundation.framework");
frameworks_osx.push_back("AudioToolbox.framework");
+ frameworks_osx.push_back("AVFAudio.framework");
frameworks_osx.push_back("CoreMIDI.framework");
frameworks_osx.push_back("CoreAudio.framework");
frameworks_osx.push_back("QuartzCore.framework");
@@ -727,6 +729,7 @@ void XcodeProvider::setupFrameworksBuildPhase(const BuildSetup &setup) {
frameworks_iOS.push_back("UIKit.framework");
frameworks_iOS.push_back("SystemConfiguration.framework");
frameworks_iOS.push_back("AudioToolbox.framework");
+ frameworks_iOS.push_back("AVFAudio.framework");
frameworks_iOS.push_back("QuartzCore.framework");
frameworks_iOS.push_back("OpenGLES.framework");
@@ -846,6 +849,7 @@ void XcodeProvider::setupFrameworksBuildPhase(const BuildSetup &setup) {
frameworks_tvOS.push_back("UIKit.framework");
frameworks_tvOS.push_back("SystemConfiguration.framework");
frameworks_tvOS.push_back("AudioToolbox.framework");
+ frameworks_tvOS.push_back("AVFAudio.framework");
frameworks_tvOS.push_back("QuartzCore.framework");
frameworks_tvOS.push_back("OpenGLES.framework");
Commit: b30eaf2903b63953d29f6062ef3343d37e61099a
https://github.com/scummvm/scummvm/commit/b30eaf2903b63953d29f6062ef3343d37e61099a
Author: Thierry Crozat (criezy at scummvm.org)
Date: 2026-02-02T20:33:05Z
Commit Message:
TTS: MACOS, IOS: Add version checks for voice gender API
The AVSpeechSynthesisVoice.gender is only available since iOS 13 and
macOS 10.15. Add a SDK version check (for compilation) and runtime
version check for iOS so that the code compiles with older SDK and
runs on older systems even if compiled with recent enough SDK.
On macOS the runtime check is here as well, but there is no SDK check
as normally when compiler with older SDK we use the older
NSSpeechSynthetiser API anyway.
Changed paths:
backends/text-to-speech/avfaudio/avfaudio-text-to-speech.mm
diff --git a/backends/text-to-speech/avfaudio/avfaudio-text-to-speech.mm b/backends/text-to-speech/avfaudio/avfaudio-text-to-speech.mm
index a5541d41702..0686e31898d 100644
--- a/backends/text-to-speech/avfaudio/avfaudio-text-to-speech.mm
+++ b/backends/text-to-speech/avfaudio/avfaudio-text-to-speech.mm
@@ -269,17 +269,21 @@ void AVFAudioTextToSpeechManager::updateVoices() {
NSString *data = [[NSString alloc] initWithString:voice.identifier];
Common::String name([voice.name UTF8String]);
Common::TTSVoice::Gender gender = Common::TTSVoice::UNKNOWN_GENDER;
- switch (voice.gender) {
- case AVSpeechSynthesisVoiceGenderMale:
- gender = Common::TTSVoice::MALE;
- break;
- case AVSpeechSynthesisVoiceGenderFemale:
- gender = Common::TTSVoice::FEMALE;
- break;
- case AVSpeechSynthesisVoiceGenderUnspecified:
- gender = Common::TTSVoice::UNKNOWN_GENDER;
- break;
+#if defined(__IPHONE_13_0) || defined(MACOSX)
+ if (@available(iOS 13, macOS 10.15, *)) {
+ switch (voice.gender) {
+ case AVSpeechSynthesisVoiceGenderMale:
+ gender = Common::TTSVoice::MALE;
+ break;
+ case AVSpeechSynthesisVoiceGenderFemale:
+ gender = Common::TTSVoice::FEMALE;
+ break;
+ case AVSpeechSynthesisVoiceGenderUnspecified:
+ gender = Common::TTSVoice::UNKNOWN_GENDER;
+ break;
+ }
}
+#endif
Common::TTSVoice::Age age = Common::TTSVoice::UNKNOWN_AGE;
Common::TTSVoice ttsVoice(gender, age, data, name);
_ttsState->_availableVoices.push_back(ttsVoice);
Commit: de24d902e106e4d01a5419f27551a6004921bf19
https://github.com/scummvm/scummvm/commit/de24d902e106e4d01a5419f27551a6004921bf19
Author: Thierry Crozat (criezy at scummvm.org)
Date: 2026-02-02T20:33:05Z
Commit Message:
CREATE_PROJECT: Use legacy TTS API for macOS Xcode project not on Apple Silicon
This commit assumes that the architecture on which we build
create_project is also the one we will target for the ScummVM
build. This should be the case in most cases.
Changed paths:
devtools/create_project/create_project.cpp
devtools/create_project/xcode.cpp
diff --git a/devtools/create_project/create_project.cpp b/devtools/create_project/create_project.cpp
index b9ff27b7a00..038f1896b7d 100644
--- a/devtools/create_project/create_project.cpp
+++ b/devtools/create_project/create_project.cpp
@@ -459,6 +459,15 @@ int main(int argc, char *argv[]) {
setup.defines.push_back("SCUMMVM_NEON");
} else {
setup.defines.push_back("MACOSX");
+ // We have two TTS backends, one that is deprecated in macOS 11 and a newer one
+ // that requires macOS 10.14 minimum. Use the new one when compiling for ARM, and
+ // otherwise (PPC, Intel) use the old one. We assume the current arch to compile
+ // create_project is also the one we will be compiling ScummVM for.
+#if !defined(__aarch64__)
+ if (getFeatureBuildState("tts", setup.features)) {
+ setup.defines.push_back("USE_NS_SPEECH_SYNTHESIZER");
+ }
+#endif
}
} else if (projectType == kProjectMSVC || projectType == kProjectCodeBlocks) {
setup.defines.push_back("WIN32");
diff --git a/devtools/create_project/xcode.cpp b/devtools/create_project/xcode.cpp
index 828275ec6dd..d5b8830c8f7 100644
--- a/devtools/create_project/xcode.cpp
+++ b/devtools/create_project/xcode.cpp
@@ -603,7 +603,6 @@ void XcodeProvider::setupFrameworksBuildPhase(const BuildSetup &setup) {
frameworks_osx.push_back("CoreFoundation.framework");
frameworks_osx.push_back("Foundation.framework");
frameworks_osx.push_back("AudioToolbox.framework");
- frameworks_osx.push_back("AVFAudio.framework");
frameworks_osx.push_back("CoreMIDI.framework");
frameworks_osx.push_back("CoreAudio.framework");
frameworks_osx.push_back("QuartzCore.framework");
@@ -614,6 +613,11 @@ void XcodeProvider::setupFrameworksBuildPhase(const BuildSetup &setup) {
frameworks_osx.push_back("OpenGL.framework");
frameworks_osx.push_back("AudioUnit.framework");
+ if (CONTAINS_DEFINE(setup.defines, "USE_TTS") &&
+ !CONTAINS_DEFINE(setup.defines, "USE_NS_SPEECH_SYNTHESIZER")) {
+ frameworks_osx.push_back("AVFAudio.framework");
+ }
+
if (CONTAINS_DEFINE(setup.defines, "USE_FAAD")) {
frameworks_osx.push_back(getLibString("faad", setup.useXCFramework));
}
@@ -729,10 +733,13 @@ void XcodeProvider::setupFrameworksBuildPhase(const BuildSetup &setup) {
frameworks_iOS.push_back("UIKit.framework");
frameworks_iOS.push_back("SystemConfiguration.framework");
frameworks_iOS.push_back("AudioToolbox.framework");
- frameworks_iOS.push_back("AVFAudio.framework");
frameworks_iOS.push_back("QuartzCore.framework");
frameworks_iOS.push_back("OpenGLES.framework");
+ if (CONTAINS_DEFINE(setup.defines, "USE_TTS")) {
+ frameworks_iOS.push_back("AVFAudio.framework");
+ }
+
if (CONTAINS_DEFINE(setup.defines, "USE_FAAD")) {
frameworks_iOS.push_back(getLibString("faad", setup.useXCFramework));
}
@@ -849,10 +856,12 @@ void XcodeProvider::setupFrameworksBuildPhase(const BuildSetup &setup) {
frameworks_tvOS.push_back("UIKit.framework");
frameworks_tvOS.push_back("SystemConfiguration.framework");
frameworks_tvOS.push_back("AudioToolbox.framework");
- frameworks_tvOS.push_back("AVFAudio.framework");
frameworks_tvOS.push_back("QuartzCore.framework");
frameworks_tvOS.push_back("OpenGLES.framework");
+ if (CONTAINS_DEFINE(setup.defines, "USE_TTS")) {
+ frameworks_tvOS.push_back("AVFAudio.framework");
+ }
if (CONTAINS_DEFINE(setup.defines, "USE_FAAD")) {
frameworks_tvOS.push_back(getLibString("faad", setup.useXCFramework));
}
Commit: 58fa5b55248343539b643f829878d612e2a9d7e9
https://github.com/scummvm/scummvm/commit/58fa5b55248343539b643f829878d612e2a9d7e9
Author: Thierry Crozat (criezy at scummvm.org)
Date: 2026-02-02T20:33:05Z
Commit Message:
DOC: IOS: TTS can now be enabled in iOS builds
Changed paths:
doc/docportal/other_platforms/ios_build.rst
diff --git a/doc/docportal/other_platforms/ios_build.rst b/doc/docportal/other_platforms/ios_build.rst
index ffa6006f1f0..53fc34d6b3a 100644
--- a/doc/docportal/other_platforms/ios_build.rst
+++ b/doc/docportal/other_platforms/ios_build.rst
@@ -66,7 +66,7 @@ It's time to generate the Xcode project. Run the following on the command line:
.. code::
- ../scummvm/devtools/create_project/xcode/build/Release/create_project ../scummvm --xcode --ios --use-xcframework --enable-faad --enable-gif --enable-mikmod --enable-vpx --enable-mpc --enable-a52 --disable-taskbar --disable-tts
+ ../scummvm/devtools/create_project/xcode/build/Release/create_project ../scummvm --xcode --ios --use-xcframework --enable-faad --enable-gif --enable-mikmod --enable-vpx --enable-mpc --enable-a52 --disable-taskbar
The resulting directory structure looks like this:
Commit: ef57720515088d1dd61ac46b34683caef09777c1
https://github.com/scummvm/scummvm/commit/ef57720515088d1dd61ac46b34683caef09777c1
Author: Thierry Crozat (criezy at scummvm.org)
Date: 2026-02-02T20:33:05Z
Commit Message:
NEWS: Mention support for TTS on iOS
Changed paths:
NEWS.md
diff --git a/NEWS.md b/NEWS.md
index 148d9ea80ec..3679e947d72 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -25,6 +25,8 @@ For a more comprehensive changelog of the latest experimental code, see:
- Numerous visual fixes.
- Implemented mouse scrolling of text window.
+ iOS port:
+ - Added support for Text-to-Speech.
#### 2026.1.0 "Like a version" (2026-01-31)
More information about the Scummvm-git-logs
mailing list