diff options
Diffstat (limited to 'rbutil/rbutilqt/tts.cpp')
-rw-r--r-- | rbutil/rbutilqt/tts.cpp | 277 |
1 files changed, 271 insertions, 6 deletions
diff --git a/rbutil/rbutilqt/tts.cpp b/rbutil/rbutilqt/tts.cpp index 252608f53e..48555cc114 100644 --- a/rbutil/rbutilqt/tts.cpp +++ b/rbutil/rbutilqt/tts.cpp | |||
@@ -33,7 +33,9 @@ void TTSBase::initTTSList() | |||
33 | #if defined(Q_OS_WIN) | 33 | #if defined(Q_OS_WIN) |
34 | ttsList["sapi"] = "Sapi TTS Engine"; | 34 | ttsList["sapi"] = "Sapi TTS Engine"; |
35 | #endif | 35 | #endif |
36 | 36 | #if defined(Q_OS_LINUX) | |
37 | ttsList["festival"] = "Festival TTS Engine"; | ||
38 | #endif | ||
37 | } | 39 | } |
38 | 40 | ||
39 | // function to get a specific encoder | 41 | // function to get a specific encoder |
@@ -44,6 +46,7 @@ TTSBase* TTSBase::getTTS(QString ttsName) | |||
44 | return ttsCache.value(ttsName); | 46 | return ttsCache.value(ttsName); |
45 | 47 | ||
46 | TTSBase* tts; | 48 | TTSBase* tts; |
49 | #if defined(Q_OS_WIN) | ||
47 | if(ttsName == "sapi") | 50 | if(ttsName == "sapi") |
48 | { | 51 | { |
49 | tts = new TTSSapi(); | 52 | tts = new TTSSapi(); |
@@ -51,6 +54,17 @@ TTSBase* TTSBase::getTTS(QString ttsName) | |||
51 | return tts; | 54 | return tts; |
52 | } | 55 | } |
53 | else | 56 | else |
57 | #endif | ||
58 | #if defined(Q_OS_LINUX) | ||
59 | if (ttsName == "festival") | ||
60 | { | ||
61 | tts = new TTSFestival(); | ||
62 | ttsCache[ttsName] = tts; | ||
63 | return tts; | ||
64 | } | ||
65 | else | ||
66 | #endif | ||
67 | if (true) // fix for OS other than WIN or LINUX | ||
54 | { | 68 | { |
55 | tts = new TTSExes(ttsName); | 69 | tts = new TTSExes(ttsName); |
56 | ttsCache[ttsName] = tts; | 70 | ttsCache[ttsName] = tts; |
@@ -92,7 +106,7 @@ TTSExes::TTSExes(QString name) : TTSBase() | |||
92 | m_name = name; | 106 | m_name = name; |
93 | 107 | ||
94 | m_TemplateMap["espeak"] = "\"%exe\" %options -w \"%wavfile\" \"%text\""; | 108 | m_TemplateMap["espeak"] = "\"%exe\" %options -w \"%wavfile\" \"%text\""; |
95 | m_TemplateMap["flite"] = "\"%exe\" %options -o \"%wavfile\" \"%text\""; | 109 | m_TemplateMap["flite"] = "\"%exe\" %options -o \"%wavfile\" -t \"%text\""; |
96 | m_TemplateMap["swift"] = "\"%exe\" %options -o \"%wavfile\" \"%text\""; | 110 | m_TemplateMap["swift"] = "\"%exe\" %options -o \"%wavfile\" \"%text\""; |
97 | 111 | ||
98 | } | 112 | } |
@@ -153,8 +167,9 @@ bool TTSExes::start(QString *errStr) | |||
153 | } | 167 | } |
154 | } | 168 | } |
155 | 169 | ||
156 | bool TTSExes::voice(QString text,QString wavfile) | 170 | TTSStatus TTSExes::voice(QString text,QString wavfile, QString *errStr) |
157 | { | 171 | { |
172 | (void) errStr; | ||
158 | QString execstring = m_TTSTemplate; | 173 | QString execstring = m_TTSTemplate; |
159 | 174 | ||
160 | execstring.replace("%exe",m_TTSexec); | 175 | execstring.replace("%exe",m_TTSexec); |
@@ -163,7 +178,7 @@ bool TTSExes::voice(QString text,QString wavfile) | |||
163 | execstring.replace("%text",text); | 178 | execstring.replace("%text",text); |
164 | //qDebug() << "voicing" << execstring; | 179 | //qDebug() << "voicing" << execstring; |
165 | QProcess::execute(execstring); | 180 | QProcess::execute(execstring); |
166 | return true; | 181 | return NoError; |
167 | 182 | ||
168 | } | 183 | } |
169 | 184 | ||
@@ -304,15 +319,16 @@ QStringList TTSSapi::getVoiceList(QString language) | |||
304 | 319 | ||
305 | 320 | ||
306 | 321 | ||
307 | bool TTSSapi::voice(QString text,QString wavfile) | 322 | TTSStatus TTSSapi::voice(QString text,QString wavfile, QString *errStr) |
308 | { | 323 | { |
324 | (void) errStr; | ||
309 | QString query = "SPEAK\t"+wavfile+"\t"+text+"\r\n"; | 325 | QString query = "SPEAK\t"+wavfile+"\t"+text+"\r\n"; |
310 | qDebug() << "voicing" << query; | 326 | qDebug() << "voicing" << query; |
311 | *voicestream << query; | 327 | *voicestream << query; |
312 | *voicestream << "SYNC\tbla\r\n"; | 328 | *voicestream << "SYNC\tbla\r\n"; |
313 | voicestream->flush(); | 329 | voicestream->flush(); |
314 | voicescript->waitForReadyRead(); | 330 | voicescript->waitForReadyRead(); |
315 | return true; | 331 | return NoError; |
316 | } | 332 | } |
317 | 333 | ||
318 | bool TTSSapi::stop() | 334 | bool TTSSapi::stop() |
@@ -349,5 +365,254 @@ bool TTSSapi::configOk() | |||
349 | return false; | 365 | return false; |
350 | return true; | 366 | return true; |
351 | } | 367 | } |
368 | /********************************************************************** | ||
369 | * TSSFestival - client-server wrapper | ||
370 | **********************************************************************/ | ||
371 | TTSFestival::~TTSFestival() | ||
372 | { | ||
373 | stop(); | ||
374 | } | ||
375 | |||
376 | void TTSFestival::startServer() | ||
377 | { | ||
378 | if(!configOk()) | ||
379 | return; | ||
380 | |||
381 | QStringList paths = settings->ttsPath("festival").split(":"); | ||
382 | |||
383 | serverProcess.start(QString("%1 --server").arg(paths[0])); | ||
384 | serverProcess.waitForStarted(); | ||
385 | |||
386 | queryServer("(getpid)"); | ||
387 | if(serverProcess.state() == QProcess::Running) | ||
388 | qDebug() << "Festival is up and running"; | ||
389 | else | ||
390 | qDebug() << "Festival failed to start"; | ||
391 | } | ||
392 | |||
393 | void TTSFestival::ensureServerRunning() | ||
394 | { | ||
395 | if(serverProcess.state() != QProcess::Running) | ||
396 | { | ||
397 | // least common denominator for all the server startup code paths | ||
398 | QProgressDialog progressDialog(tr(""), tr(""), 0, 0); | ||
399 | progressDialog.setWindowTitle(tr("Starting festival")); | ||
400 | progressDialog.setModal(true); | ||
401 | progressDialog.setLabel(0); | ||
402 | progressDialog.setCancelButton(0); | ||
403 | progressDialog.show(); | ||
404 | |||
405 | QApplication::processEvents(); // actually show the dialog | ||
406 | |||
407 | startServer(); | ||
408 | } | ||
409 | } | ||
410 | |||
411 | bool TTSFestival::start(QString* errStr) | ||
412 | { | ||
413 | (void) errStr; | ||
414 | ensureServerRunning(); | ||
415 | if (!settings->ttsVoice("festival").isEmpty()) | ||
416 | queryServer(QString("(voice.select '%1)").arg(settings->ttsVoice("festival"))); | ||
417 | |||
418 | return true; | ||
419 | } | ||
352 | 420 | ||
421 | bool TTSFestival::stop() | ||
422 | { | ||
423 | serverProcess.terminate(); | ||
424 | serverProcess.kill(); | ||
425 | |||
426 | return true; | ||
427 | } | ||
428 | |||
429 | TTSStatus TTSFestival::voice(QString text, QString wavfile, QString* errStr) | ||
430 | { | ||
431 | qDebug() << text << "->" << wavfile; | ||
432 | |||
433 | QStringList paths = settings->ttsPath("festival").split(":"); | ||
434 | QString cmd = QString("%1 --server localhost --otype riff --ttw --withlisp --output \"%2\" - ").arg(paths[1]).arg(wavfile); | ||
435 | qDebug() << cmd; | ||
436 | |||
437 | QProcess clientProcess; | ||
438 | clientProcess.start(cmd); | ||
439 | clientProcess.write(QString("%1.\n").arg(text).toAscii()); | ||
440 | clientProcess.waitForBytesWritten(); | ||
441 | clientProcess.closeWriteChannel(); | ||
442 | clientProcess.waitForReadyRead(); | ||
443 | QString response = clientProcess.readAll(); | ||
444 | response = response.trimmed(); | ||
445 | if(!response.contains("Utterance")) | ||
446 | { | ||
447 | qDebug() << "Could not voice string: " << response; | ||
448 | *errStr = tr("engine could not voice string"); | ||
449 | return Warning; | ||
450 | /* do not stop the voicing process because of a single string | ||
451 | TODO: needs proper settings */ | ||
452 | } | ||
453 | clientProcess.closeReadChannel(QProcess::StandardError); | ||
454 | clientProcess.closeReadChannel(QProcess::StandardOutput); | ||
455 | clientProcess.terminate(); | ||
456 | clientProcess.kill(); | ||
457 | |||
458 | return NoError; | ||
459 | } | ||
460 | |||
461 | bool TTSFestival::configOk() | ||
462 | { | ||
463 | QStringList paths = settings->ttsPath("festival").split(":"); | ||
464 | if(paths.size() != 2) | ||
465 | return false; | ||
466 | bool ret = QFileInfo(paths[0]).isExecutable() && | ||
467 | QFileInfo(paths[1]).isExecutable(); | ||
468 | if(settings->ttsVoice("festival").size() > 0 && voices.size() > 0) | ||
469 | ret = ret && (voices.indexOf(settings->ttsVoice("festival")) != -1); | ||
470 | return ret; | ||
471 | } | ||
472 | |||
473 | void TTSFestival::showCfg() | ||
474 | { | ||
475 | #ifndef CONSOLE | ||
476 | TTSFestivalGui gui(this); | ||
477 | #endif | ||
478 | gui.setCfg(settings); | ||
479 | gui.showCfg(); | ||
480 | } | ||
481 | |||
482 | QStringList TTSFestival::getVoiceList() | ||
483 | { | ||
484 | if(!configOk()) | ||
485 | return QStringList(); | ||
486 | |||
487 | if(voices.size() > 0) | ||
488 | { | ||
489 | qDebug() << "Using voice cache"; | ||
490 | return voices; | ||
491 | } | ||
492 | QString response = queryServer("(voice.list)"); | ||
493 | |||
494 | // get the 2nd line. It should be (<voice_name>, <voice_name>) | ||
495 | response = response.mid(response.indexOf('\n') + 1, -1); | ||
496 | response = response.left(response.indexOf('\n')).trimmed(); | ||
497 | |||
498 | voices = response.mid(1, response.size()-2).split(' '); | ||
499 | |||
500 | voices.sort(); | ||
501 | if (voices.size() == 1 && voices[0].size() == 0) | ||
502 | voices.removeAt(0); | ||
503 | if (voices.size() > 0) | ||
504 | qDebug() << "Voices: " << voices; | ||
505 | else | ||
506 | qDebug() << "No voices."; | ||
507 | return voices; | ||
508 | } | ||
509 | |||
510 | QString TTSFestival::getVoiceInfo(QString voice) | ||
511 | { | ||
512 | if(!configOk()) | ||
513 | return ""; | ||
514 | |||
515 | if(!getVoiceList().contains(voice)) | ||
516 | return ""; | ||
517 | |||
518 | if(voiceDescriptions.contains(voice)) | ||
519 | return voiceDescriptions[voice]; | ||
520 | |||
521 | QString response = queryServer(QString("(voice.description '%1)").arg(voice), 3000); | ||
522 | |||
523 | if (response == "") | ||
524 | { | ||
525 | voiceDescriptions[voice]=tr("No description available"); | ||
526 | } | ||
527 | else | ||
528 | { | ||
529 | response = response.remove(QRegExp("(description \"*\")", Qt::CaseInsensitive, QRegExp::Wildcard)); | ||
530 | qDebug() << "voiceInfo w/o descr: " << response; | ||
531 | response = response.remove(')'); | ||
532 | QStringList responseLines = response.split('(', QString::SkipEmptyParts); | ||
533 | responseLines.removeAt(0); // the voice name itself | ||
534 | |||
535 | QString description; | ||
536 | foreach(QString line, responseLines) | ||
537 | { | ||
538 | line = line.remove('('); | ||
539 | line = line.simplified(); | ||
540 | |||
541 | line[0] = line[0].toUpper(); // capitalize the key | ||
542 | |||
543 | int firstSpace = line.indexOf(' '); | ||
544 | if (firstSpace > 0) | ||
545 | { | ||
546 | line = line.insert(firstSpace, ':'); // add a colon between the key and the value | ||
547 | line[firstSpace+2] = line[firstSpace+2].toUpper(); // capitalize the value | ||
548 | } | ||
549 | |||
550 | description += line + "\n"; | ||
551 | } | ||
552 | voiceDescriptions[voice] = description.trimmed(); | ||
553 | } | ||
554 | return voiceDescriptions[voice]; | ||
555 | } | ||
556 | |||
557 | QString TTSFestival::queryServer(QString query, int timeout) | ||
558 | { | ||
559 | if(!configOk()) | ||
560 | return ""; | ||
561 | |||
562 | ensureServerRunning(); | ||
563 | |||
564 | qDebug() << "queryServer with " << query; | ||
565 | QString response; | ||
566 | |||
567 | QDateTime endTime; | ||
568 | if(timeout > 0) | ||
569 | endTime = QDateTime::currentDateTime().addMSecs(timeout); | ||
570 | |||
571 | /* Festival is *extremely* unreliable. Although at this | ||
572 | * point we are sure that SIOD is accepting commands, | ||
573 | * we might end up with an empty response. Hence, the loop. | ||
574 | */ | ||
575 | while(true) | ||
576 | { | ||
577 | QApplication::processEvents(QEventLoop::AllEvents, 50); | ||
578 | QTcpSocket socket; | ||
579 | |||
580 | socket.connectToHost("localhost", 1314); | ||
581 | socket.waitForConnected(); | ||
582 | |||
583 | if(socket.state() == QAbstractSocket::ConnectedState) | ||
584 | { | ||
585 | socket.write(QString("%1\n").arg(query).toAscii()); | ||
586 | socket.waitForBytesWritten(); | ||
587 | socket.waitForReadyRead(); | ||
588 | |||
589 | response = socket.readAll().trimmed(); | ||
590 | |||
591 | if (response != "LP" && response != "") | ||
592 | break; | ||
593 | } | ||
594 | socket.abort(); | ||
595 | socket.disconnectFromHost(); | ||
596 | |||
597 | if(timeout > 0 && QDateTime::currentDateTime() >= endTime) | ||
598 | return ""; | ||
599 | |||
600 | /* make sure we wait a little as we don't want to flood the server with requests */ | ||
601 | QDateTime tmpEndTime = QDateTime::currentDateTime().addMSecs(500); | ||
602 | while(QDateTime::currentDateTime() < tmpEndTime) | ||
603 | QApplication::processEvents(QEventLoop::AllEvents); | ||
604 | } | ||
605 | if(response == "nil") | ||
606 | return ""; | ||
607 | |||
608 | QStringList lines = response.split('\n'); | ||
609 | if(lines.size() > 2) | ||
610 | { | ||
611 | lines.removeFirst(); | ||
612 | lines.removeLast(); | ||
613 | } | ||
614 | else | ||
615 | qDebug() << "Response too short: " << response; | ||
616 | return lines.join("\n"); | ||
617 | } | ||
353 | 618 | ||