From 21e95258f2d3f7c8c62cd01872afe9926301bf8d Mon Sep 17 00:00:00 2001 From: Solomon Peachy Date: Sun, 9 Aug 2026 08:00:50 -0400 Subject: [PATCH] FS#13977 - rbutil: fix SAPI5 TTS test output and playback (Alessio Lenzi) After the SAPI5 error handling added for FS#13972, the Test TTS button can fail with SAPI error 5 even though the engine and voice are configured correctly. The test also destroys its QSoundEffect and temporary file before asynchronous playback can complete. Config::testTts() used a QTemporaryFile with no .wav extension. TTSSapi::voice() removes the requested output before synthesis so that a stale file cannot be mistaken for successful output. SAPI SpFileStream was therefore asked to create an extensionless output file and returned error 5 (invalid procedure call or argument). After successful synthesis, QSoundEffect was allocated on the stack, its loop count was set to zero, and the temporary file was removed when testTts() returned. This does not allow asynchronous playback to complete reliably. * create a temporary directory and requests an initially nonexistent tts-test.wav inside it; * keep the directory and generated wave file alive for playback; * keep QSoundEffect alive until playback finishes or fails; * request one playback and clean up all temporary data afterward. Change-Id: I8e482ab846e6445889118e121025ec48d3776d6b --- utils/rbutilqt/configure.cpp | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/utils/rbutilqt/configure.cpp b/utils/rbutilqt/configure.cpp index 7b6d634c3f..d8a2a5a4c2 100644 --- a/utils/rbutilqt/configure.cpp +++ b/utils/rbutilqt/configure.cpp @@ -939,17 +939,20 @@ void Config::testTts() } QString filename; - QTemporaryFile file(this); + QTemporaryDir* tempDir = nullptr; // keep filename empty if the TTS can do speaking for itself. if(!(tts->capabilities() & TTSBase::CanSpeak)) { - file.open(); - filename = file.fileName(); - file.close(); + // SAPI's SpFileStream requires a filename with a wave extension. + // Give the engine a path that does not exist yet, matching normal + // voice-file generation, and keep its directory until playback ends. + tempDir = new QTemporaryDir(); + filename = tempDir->filePath("tts-test.wav"); } if(tts->voice(tr("Rockbox Utility Voice Test"),filename,&errstr) == FatalError) { tts->stop(); + delete tempDir; QMessageBox::warning(this,tr("Could not voice test string."), tr("Could not voice test string.\n") + errstr + tr("\nPlease configure TTS engine.")); @@ -957,17 +960,28 @@ void Config::testTts() return; } tts->stop(); - if(!filename.isEmpty()) { - QSoundEffect effect; - effect.setSource(QUrl::fromLocalFile(filename)); - effect.setLoopCount(0); - effect.setVolume(1.0f); - effect.play(); - } ui.testTTS->setEnabled(true); delete tts; /* Config objects are never deleted (in fact, they are leaked..), so we can't rely on QObject, since that would delete the TTSBase instance on application exit */ + if(tempDir != nullptr) { + QSoundEffect* effect = new QSoundEffect(this); + connect(effect, &QObject::destroyed, [tempDir]() { + delete tempDir; + }); + connect(effect, &QSoundEffect::playingChanged, effect, [effect]() { + if(!effect->isPlaying()) + effect->deleteLater(); + }); + connect(effect, &QSoundEffect::statusChanged, effect, [effect]() { + if(effect->status() == QSoundEffect::Error) + effect->deleteLater(); + }); + effect->setSource(QUrl::fromLocalFile(filename)); + effect->setLoopCount(1); + effect->setVolume(1.0f); + effect->play(); + } #endif }