[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