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
This commit is contained in:
Solomon Peachy 2026-08-09 08:00:50 -04:00
parent eecd4ec98b
commit 21e95258f2

View file

@ -939,17 +939,20 @@ void Config::testTts()
} }
QString filename; QString filename;
QTemporaryFile file(this); QTemporaryDir* tempDir = nullptr;
// keep filename empty if the TTS can do speaking for itself. // keep filename empty if the TTS can do speaking for itself.
if(!(tts->capabilities() & TTSBase::CanSpeak)) { if(!(tts->capabilities() & TTSBase::CanSpeak)) {
file.open(); // SAPI's SpFileStream requires a filename with a wave extension.
filename = file.fileName(); // Give the engine a path that does not exist yet, matching normal
file.close(); // 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) if(tts->voice(tr("Rockbox Utility Voice Test"),filename,&errstr) == FatalError)
{ {
tts->stop(); tts->stop();
delete tempDir;
QMessageBox::warning(this,tr("Could not voice test string."), QMessageBox::warning(this,tr("Could not voice test string."),
tr("Could not voice test string.\n") + errstr tr("Could not voice test string.\n") + errstr
+ tr("\nPlease configure TTS engine.")); + tr("\nPlease configure TTS engine."));
@ -957,17 +960,28 @@ void Config::testTts()
return; return;
} }
tts->stop(); 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); ui.testTTS->setEnabled(true);
delete tts; /* Config objects are never deleted (in fact, they are delete tts; /* Config objects are never deleted (in fact, they are
leaked..), so we can't rely on QObject, since that would leaked..), so we can't rely on QObject, since that would
delete the TTSBase instance on application exit */ 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 #endif
} }