summaryrefslogtreecommitdiffstats
path: root/utils/rbutilqt/base/ttscarbon.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'utils/rbutilqt/base/ttscarbon.cpp')
-rw-r--r--utils/rbutilqt/base/ttscarbon.cpp443
1 files changed, 443 insertions, 0 deletions
diff --git a/utils/rbutilqt/base/ttscarbon.cpp b/utils/rbutilqt/base/ttscarbon.cpp
new file mode 100644
index 0000000000..6b20de764f
--- /dev/null
+++ b/utils/rbutilqt/base/ttscarbon.cpp
@@ -0,0 +1,443 @@
+/***************************************************************************
+ * __________ __ ___.
+ * Open \______ \ ____ ____ | | _\_ |__ _______ ___
+ * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
+ * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
+ * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
+ * \/ \/ \/ \/ \/
+ *
+ * Copyright (C) 2010 by Dominik Riebeling
+ *
+ * All files in this archive are subject to the GNU General Public License.
+ * See the file COPYING in the source tree root for full license agreement.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ****************************************************************************/
+
+#include <QtCore>
+#include "ttsbase.h"
+#include "ttscarbon.h"
+#include "encttssettings.h"
+#include "rbsettings.h"
+
+#include <CoreFoundation/CoreFoundation.h>
+#include <ApplicationServices/ApplicationServices.h>
+#include <Carbon/Carbon.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <inttypes.h>
+#include "Logger.h"
+
+TTSCarbon::TTSCarbon(QObject* parent) : TTSBase(parent)
+{
+}
+
+TTSBase::Capabilities TTSCarbon::capabilities()
+{
+ return TTSBase::CanSpeak;
+}
+
+bool TTSCarbon::configOk()
+{
+ return true;
+}
+
+
+bool TTSCarbon::start(QString *errStr)
+{
+ (void)errStr;
+ VoiceSpec vspec;
+ VoiceSpec* vspecref = NULL;
+ VoiceDescription vdesc;
+ OSErr error;
+ QString selectedVoice
+ = RbSettings::subValue("carbon", RbSettings::TtsVoice).toString();
+ SInt16 numVoices;
+ SInt16 voiceIndex;
+ error = CountVoices(&numVoices);
+ for(voiceIndex = 1; voiceIndex < numVoices; ++voiceIndex) {
+ error = GetIndVoice(voiceIndex, &vspec);
+ error = GetVoiceDescription(&vspec, &vdesc, sizeof(vdesc));
+ // name is pascal string, i.e. the first byte is the length.
+ QString name = QString::fromLocal8Bit((const char*)&vdesc.name[1],
+ vdesc.name[0]);
+ if(name == selectedVoice) {
+ vspecref = &vspec;
+ if(vdesc.script != -1)
+ m_voiceScript = (CFStringBuiltInEncodings)vdesc.script;
+ else
+ m_voiceScript = (CFStringBuiltInEncodings)vdesc.reserved[0];
+ break;
+ }
+ }
+ if(voiceIndex == numVoices) {
+ // voice not found. Add user notification here and proceed with
+ // system default voice.
+ LOG_WARNING() << "Selected voice not found, using system default!";
+ GetVoiceDescription(&vspec, &vdesc, sizeof(vdesc));
+ if(vdesc.script != -1)
+ m_voiceScript = (CFStringBuiltInEncodings)vdesc.script;
+ else
+ m_voiceScript = (CFStringBuiltInEncodings)vdesc.reserved[0];
+ }
+
+ error = NewSpeechChannel(vspecref, &m_channel);
+ //SetSpeechInfo(channel, soSpeechDoneCallBack, speechDone);
+ Fixed rate = (Fixed)(0x10000 * RbSettings::subValue("carbon",
+ RbSettings::TtsSpeed).toInt());
+ if(rate != 0)
+ SetSpeechRate(m_channel, rate);
+
+ Fixed pitch = (Fixed)(0x10000 * RbSettings::subValue("carbon",
+ RbSettings::TtsPitch).toInt());
+ if(pitch != 0)
+ SetSpeechPitch(m_channel, pitch);
+
+ return (error == 0) ? true : false;
+}
+
+
+bool TTSCarbon::stop(void)
+{
+ DisposeSpeechChannel(m_channel);
+ return true;
+}
+
+
+void TTSCarbon::generateSettings(void)
+{
+ QStringList voiceNames;
+ QString systemVoice;
+ SInt16 numVoices;
+ OSErr error;
+ VoiceSpec vspec;
+ VoiceDescription vdesc;
+
+ // get system voice
+ error = GetVoiceDescription(NULL, &vdesc, sizeof(vdesc));
+ systemVoice
+ = QString::fromLocal8Bit((const char*)&vdesc.name[1], vdesc.name[0]);
+ // get list of all voices
+ CountVoices(&numVoices);
+ for(SInt16 i = 1; i < numVoices; ++i) {
+ error = GetIndVoice(i, &vspec);
+ error = GetVoiceDescription(&vspec, &vdesc, sizeof(vdesc));
+ // name is pascal string, i.e. the first byte is the length.
+ QString name
+ = QString::fromLocal8Bit((const char*)&vdesc.name[1], vdesc.name[0]);
+ voiceNames.append(name.trimmed());
+ }
+ // voice
+ EncTtsSetting* setting;
+ QString voice
+ = RbSettings::subValue("carbon", RbSettings::TtsVoice).toString();
+ if(voice.isEmpty())
+ voice = systemVoice;
+ setting = new EncTtsSetting(this, EncTtsSetting::eSTRINGLIST,
+ tr("Voice:"), voice, voiceNames, EncTtsSetting::eNOBTN);
+ insertSetting(ConfigVoice, setting);
+
+ // speed
+ int speed = RbSettings::subValue("carbon", RbSettings::TtsSpeed).toInt();
+ setting = new EncTtsSetting(this, EncTtsSetting::eINT,
+ tr("Speed (words/min):"), speed, 80, 500,
+ EncTtsSetting::eNOBTN);
+ insertSetting(ConfigSpeed, setting);
+
+ // pitch
+ int pitch = RbSettings::subValue("carbon", RbSettings::TtsPitch).toInt();
+ setting = new EncTtsSetting(this, EncTtsSetting::eINT,
+ tr("Pitch (0 for default):"), pitch, 0, 65,
+ EncTtsSetting::eNOBTN);
+ insertSetting(ConfigPitch, setting);
+
+}
+
+
+void TTSCarbon::saveSettings(void)
+{
+ // save settings in user config
+ RbSettings::setSubValue("carbon", RbSettings::TtsVoice,
+ getSetting(ConfigVoice)->current().toString());
+ RbSettings::setSubValue("carbon", RbSettings::TtsSpeed,
+ getSetting(ConfigSpeed)->current().toInt());
+ RbSettings::setSubValue("carbon", RbSettings::TtsPitch,
+ getSetting(ConfigPitch)->current().toInt());
+ RbSettings::sync();
+}
+
+
+/** @brief create wav file from text using the selected TTS voice.
+ */
+TTSStatus TTSCarbon::voice(const QString& text, const QString& wavfile, QString* errStr)
+{
+ TTSStatus status = NoError;
+ OSErr error;
+
+ char* tmpfile = NULL;
+ if(!wavfile.isEmpty()) {
+ QString aifffile = wavfile + ".aiff";
+ // FIXME: find out why we need to do this.
+ // Create a local copy of the temporary file filename.
+ // Not doing so causes weird issues (path contains trailing spaces)
+ unsigned int len = aifffile.size() + 1;
+ tmpfile = (char*)malloc(len * sizeof(char));
+ strncpy(tmpfile, aifffile.toLocal8Bit().constData(), len);
+ CFStringRef tmpfileref = CFStringCreateWithCString(kCFAllocatorDefault,
+ tmpfile, kCFStringEncodingUTF8);
+ CFURLRef urlref = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
+ tmpfileref, kCFURLPOSIXPathStyle, false);
+ SetSpeechInfo(m_channel, soOutputToFileWithCFURL, urlref);
+ }
+
+ // speak it.
+ // Convert the string to the encoding requested by the voice. Do this
+ // via CFString, as this allows to directly use the destination encoding
+ // as CFString uses the same values as the voice.
+
+ // allocate enough space to allow storing the string in a 2 byte encoding
+ unsigned int textlen = 2 * text.length() + 1;
+ char* textbuf = (char*)calloc(textlen, sizeof(char));
+ char* utf8data = (char*)text.toUtf8().constData();
+ int utf8bytes = text.toUtf8().size();
+ CFStringRef cfstring = CFStringCreateWithBytes(kCFAllocatorDefault,
+ (UInt8*)utf8data, utf8bytes,
+ kCFStringEncodingUTF8, (Boolean)false);
+ CFIndex usedBuf = 0;
+ CFRange range;
+ range.location = 0; // character in string to start.
+ range.length = text.length(); // number of _characters_ in string
+ // FIXME: check if converting between encodings was lossless.
+ CFStringGetBytes(cfstring, range, m_voiceScript, ' ',
+ false, (UInt8*)textbuf, textlen, &usedBuf);
+
+ error = SpeakText(m_channel, textbuf, (unsigned long)usedBuf);
+ while(SpeechBusy()) {
+ // FIXME: add small delay here to make calls less frequent
+ QCoreApplication::processEvents();
+ }
+ if(error != 0) {
+ *errStr = tr("Could not voice string");
+ status = FatalError;
+ }
+ free(textbuf);
+ CFRelease(cfstring);
+
+ if(!wavfile.isEmpty()) {
+ // convert the temporary aiff file to wav
+ if(status == NoError
+ && convertAiffToWav(tmpfile, wavfile.toLocal8Bit().constData()) != 0) {
+ *errStr = tr("Could not convert intermediate file");
+ status = FatalError;
+ }
+ // remove temporary aiff file
+ unlink(tmpfile);
+ free(tmpfile);
+ }
+
+ return status;
+}
+
+
+unsigned long TTSCarbon::be2u32(unsigned char* buf)
+{
+ return (buf[0]&0xff)<<24 | (buf[1]&0xff)<<16 | (buf[2]&0xff)<<8 | (buf[3]&0xff);
+}
+
+
+unsigned long TTSCarbon::be2u16(unsigned char* buf)
+{
+ return (buf[1]&0xff) | (buf[0]&0xff)<<8;
+}
+
+
+unsigned char* TTSCarbon::u32tobuf(unsigned char* buf, uint32_t val)
+{
+ buf[0] = val & 0xff;
+ buf[1] = (val>> 8) & 0xff;
+ buf[2] = (val>>16) & 0xff;
+ buf[3] = (val>>24) & 0xff;
+ return buf;
+}
+
+
+unsigned char* TTSCarbon::u16tobuf(unsigned char* buf, uint16_t val)
+{
+ buf[0] = val & 0xff;
+ buf[1] = (val>> 8) & 0xff;
+ return buf;
+}
+
+
+/** @brief convert 80 bit extended ("long double") to int.
+ * This is simplified to handle the usual audio sample rates. Everything else
+ * might break. If the value isn't supported it will return 0.
+ * Conversion taken from Rockbox aiff codec.
+ */
+unsigned int TTSCarbon::extended2int(unsigned char* buf)
+{
+ unsigned int result = 0;
+ /* value negative? */
+ if(buf[0] & 0x80)
+ return 0;
+ /* check exponent. Int can handle up to 2^31. */
+ int exponent = buf[0] << 8 | buf[1];
+ if(exponent < 0x4000 || exponent > (0x4000 + 30))
+ return 0;
+ result = ((buf[2]<<24) | (buf[3]<<16) | (buf[4]<<8) | buf[5]) + 1;
+ result >>= (16 + 14 - buf[1]);
+ return result;
+}
+
+
+/** @brief Convert aiff file to wav. Returns 0 on success.
+ */
+int TTSCarbon::convertAiffToWav(const char* aiff, const char* wav)
+{
+ struct commchunk {
+ unsigned long chunksize;
+ unsigned short channels;
+ unsigned long frames;
+ unsigned short size;
+ int rate;
+ };
+
+ struct ssndchunk {
+ unsigned long chunksize;
+ unsigned long offset;
+ unsigned long blocksize;
+ };
+
+ FILE* in;
+ FILE* out;
+ unsigned char obuf[4];
+ unsigned char* buf;
+ /* minimum file size for a valid aiff file is 46 bytes:
+ * - FORM chunk: 12 bytes
+ * - COMM chunk: 18 bytes
+ * - SSND chunk: 16 bytes (with no actual data)
+ */
+ struct stat filestat;
+ stat(aiff, &filestat);
+ if(filestat.st_size < 46)
+ return -1;
+ /* read input file into memory */
+ buf = (unsigned char*)malloc(filestat.st_size * sizeof(unsigned char));
+ if(!buf) /* error out if malloc() failed */
+ return -1;
+ in = fopen(aiff, "rb");
+ if(fread(buf, 1, filestat.st_size, in) < filestat.st_size) {
+ printf("could not read file: not enought bytes read\n");
+ fclose(in);
+ free(buf);
+ return -1;
+ }
+ fclose(in);
+
+ /* check input file format */
+ if(memcmp(buf, "FORM", 4) | memcmp(&buf[8], "AIFF", 4)) {
+ printf("No valid AIFF header found.\n");
+ free(buf);
+ return -1;
+ }
+ /* read COMM chunk */
+ unsigned char* commstart = &buf[12];
+ struct commchunk comm;
+ if(memcmp(commstart, "COMM", 4)) {
+ printf("COMM chunk not at beginning.\n");
+ free(buf);
+ return -1;
+ }
+ comm.chunksize = be2u32(&commstart[4]);
+ comm.channels = be2u16(&commstart[8]);
+ comm.frames = be2u32(&commstart[10]);
+ comm.size = be2u16(&commstart[14]);
+ comm.rate = extended2int(&commstart[16]);
+
+ /* find SSND as next chunk */
+ unsigned char* ssndstart = commstart + 8 + comm.chunksize;
+ while(memcmp(ssndstart, "SSND", 4) && ssndstart < (buf + filestat.st_size)) {
+ printf("Skipping chunk.\n");
+ ssndstart += be2u32(&ssndstart[4]) + 8;
+ }
+ if(ssndstart > (buf + filestat.st_size)) {
+ free(buf);
+ return -1;
+ }
+
+ struct ssndchunk ssnd;
+ ssnd.chunksize = be2u32(&ssndstart[4]);
+ ssnd.offset = be2u32(&ssndstart[8]);
+ ssnd.blocksize = be2u32(&ssndstart[12]);
+
+ /* Calculate the total length of the resulting RIFF chunk.
+ * The length is given by frames * samples * bytes/sample.
+ * We need to add:
+ * - 16 bytes: fmt chunk header
+ * - 8 bytes: data chunk header
+ * - 4 bytes: wave chunk identifier
+ */
+ out = fopen(wav, "wb+");
+
+ /* write the wav header */
+ unsigned short blocksize = comm.channels * (comm.size >> 3);
+ unsigned long rifflen = blocksize * comm.frames + 28;
+ fwrite("RIFF", 1, 4, out);
+ fwrite(u32tobuf(obuf, rifflen), 1, 4, out);
+ fwrite("WAVE", 1, 4, out);
+
+ /* write the fmt chunk and chunk size (always 16) */
+ /* write fmt chunk header:
+ * header, size (always 0x10, format code (always 0x0001)
+ */
+ fwrite("fmt \x10\x00\x00\x00\x01\x00", 1, 10, out);
+ /* number of channels (2 bytes) */
+ fwrite(u16tobuf(obuf, comm.channels), 1, 2, out);
+ /* sampling rate (4 bytes) */
+ fwrite(u32tobuf(obuf, comm.rate), 1, 4, out);
+
+ /* data rate, i.e. bytes/sec */
+ fwrite(u32tobuf(obuf, comm.rate * blocksize), 1, 4, out);
+
+ /* data block size */
+ fwrite(u16tobuf(obuf, blocksize), 1, 2, out);
+
+ /* bits per sample */
+ fwrite(u16tobuf(obuf, comm.size), 1, 2, out);
+
+ /* write the data chunk */
+ /* chunk id */
+ fwrite("data", 1, 4, out);
+ /* chunk size: 4 bytes. */
+ unsigned long cs = blocksize * comm.frames;
+ fwrite(u32tobuf(obuf, cs), 1, 4, out);
+
+ /* write data */
+ unsigned char* data = ssndstart;
+ unsigned long pos = ssnd.chunksize;
+ /* byteswap if samples are 16 bit */
+ if(comm.size == 16) {
+ while(pos) {
+ obuf[1] = *data++ & 0xff;
+ obuf[0] = *data++ & 0xff;
+ fwrite(obuf, 1, 2, out);
+ pos -= 2;
+ }
+ }
+ /* 8 bit samples have need no conversion so we can bulk copy.
+ * Everything that is not 16 bit is considered 8. */
+ else {
+ fwrite(data, 1, pos, out);
+ }
+ /* number of bytes has to be even, even if chunksize is not. */
+ if(cs % 2) {
+ fwrite(obuf, 1, 1, out);
+ }
+
+ fclose(out);
+ free(buf);
+ return 0;
+}
+