diff options
Diffstat (limited to 'rbutil/rbutilqt/base/ttscarbon.cpp')
-rw-r--r-- | rbutil/rbutilqt/base/ttscarbon.cpp | 405 |
1 files changed, 405 insertions, 0 deletions
diff --git a/rbutil/rbutilqt/base/ttscarbon.cpp b/rbutil/rbutilqt/base/ttscarbon.cpp new file mode 100644 index 0000000000..b8259a374b --- /dev/null +++ b/rbutil/rbutilqt/base/ttscarbon.cpp | |||
@@ -0,0 +1,405 @@ | |||
1 | /*************************************************************************** | ||
2 | * __________ __ ___. | ||
3 | * Open \______ \ ____ ____ | | _\_ |__ _______ ___ | ||
4 | * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / | ||
5 | * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < | ||
6 | * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ | ||
7 | * \/ \/ \/ \/ \/ | ||
8 | * | ||
9 | * Copyright (C) 2010 by Dominik Riebeling | ||
10 | * $Id$ | ||
11 | * | ||
12 | * All files in this archive are subject to the GNU General Public License. | ||
13 | * See the file COPYING in the source tree root for full license agreement. | ||
14 | * | ||
15 | * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY | ||
16 | * KIND, either express or implied. | ||
17 | * | ||
18 | ****************************************************************************/ | ||
19 | |||
20 | #include <QtCore> | ||
21 | #include "ttsbase.h" | ||
22 | #include "ttscarbon.h" | ||
23 | #include "encttssettings.h" | ||
24 | #include "rbsettings.h" | ||
25 | |||
26 | #include <CoreFoundation/CoreFoundation.h> | ||
27 | #include <Carbon/Carbon.h> | ||
28 | #include <unistd.h> | ||
29 | #include <sys/stat.h> | ||
30 | #include <inttypes.h> | ||
31 | |||
32 | TTSCarbon::TTSCarbon(QObject* parent) : TTSBase(parent) | ||
33 | { | ||
34 | } | ||
35 | |||
36 | |||
37 | bool TTSCarbon::configOk() | ||
38 | { | ||
39 | return true; | ||
40 | } | ||
41 | |||
42 | |||
43 | bool TTSCarbon::start(QString *errStr) | ||
44 | { | ||
45 | (void)errStr; | ||
46 | VoiceSpec vspec; | ||
47 | VoiceSpec* vspecref; | ||
48 | VoiceDescription vdesc; | ||
49 | OSErr error; | ||
50 | QString selectedVoice | ||
51 | = RbSettings::subValue("carbon", RbSettings::TtsVoice).toString(); | ||
52 | SInt16 numVoices; | ||
53 | SInt16 voiceIndex; | ||
54 | error = CountVoices(&numVoices); | ||
55 | for(voiceIndex = 1; voiceIndex < numVoices; ++voiceIndex) { | ||
56 | error = GetIndVoice(voiceIndex, &vspec); | ||
57 | error = GetVoiceDescription(&vspec, &vdesc, sizeof(vdesc)); | ||
58 | // name is pascal string, i.e. the first byte is the length. | ||
59 | QString name = QString::fromLocal8Bit((const char*)&vdesc.name[1], | ||
60 | vdesc.name[0]); | ||
61 | if(name == selectedVoice) { | ||
62 | vspecref = &vspec; | ||
63 | if(vdesc.script != -1) | ||
64 | m_voiceScript = (CFStringBuiltInEncodings)vdesc.script; | ||
65 | else | ||
66 | m_voiceScript = (CFStringBuiltInEncodings)vdesc.reserved[0]; | ||
67 | break; | ||
68 | } | ||
69 | } | ||
70 | if(voiceIndex == numVoices) { | ||
71 | // voice not found. Add user notification here and proceed with | ||
72 | // system default voice. | ||
73 | qDebug() << "selected voice not found, using system default!"; | ||
74 | vspecref = NULL; | ||
75 | GetVoiceDescription(&vspec, &vdesc, sizeof(vdesc)); | ||
76 | if(vdesc.script != -1) | ||
77 | m_voiceScript = (CFStringBuiltInEncodings)vdesc.script; | ||
78 | else | ||
79 | m_voiceScript = (CFStringBuiltInEncodings)vdesc.reserved[0]; | ||
80 | } | ||
81 | |||
82 | error = NewSpeechChannel(vspecref, &m_channel); | ||
83 | //SetSpeechInfo(channel, soSpeechDoneCallBack, speechDone); | ||
84 | return (error == 0) ? true : false; | ||
85 | } | ||
86 | |||
87 | |||
88 | bool TTSCarbon::stop(void) | ||
89 | { | ||
90 | DisposeSpeechChannel(m_channel); | ||
91 | return true; | ||
92 | } | ||
93 | |||
94 | |||
95 | void TTSCarbon::generateSettings(void) | ||
96 | { | ||
97 | QStringList voiceNames; | ||
98 | QString systemVoice; | ||
99 | SInt16 numVoices; | ||
100 | OSErr error; | ||
101 | VoiceSpec vspec; | ||
102 | VoiceDescription vdesc; | ||
103 | |||
104 | // get system voice | ||
105 | error = GetVoiceDescription(NULL, &vdesc, sizeof(vdesc)); | ||
106 | systemVoice | ||
107 | = QString::fromLocal8Bit((const char*)&vdesc.name[1], vdesc.name[0]); | ||
108 | // get list of all voices | ||
109 | CountVoices(&numVoices); | ||
110 | for(SInt16 i = 1; i < numVoices; ++i) { | ||
111 | error = GetIndVoice(i, &vspec); | ||
112 | error = GetVoiceDescription(&vspec, &vdesc, sizeof(vdesc)); | ||
113 | // name is pascal string, i.e. the first byte is the length. | ||
114 | QString name | ||
115 | = QString::fromLocal8Bit((const char*)&vdesc.name[1], vdesc.name[0]); | ||
116 | voiceNames.append(name.trimmed()); | ||
117 | } | ||
118 | // voice | ||
119 | EncTtsSetting* setting; | ||
120 | QString voice | ||
121 | = RbSettings::subValue("carbon", RbSettings::TtsVoice).toString(); | ||
122 | if(voice.isEmpty()) | ||
123 | voice = systemVoice; | ||
124 | setting = new EncTtsSetting(this, EncTtsSetting::eSTRINGLIST, | ||
125 | tr("Voice:"), voice, voiceNames, EncTtsSetting::eNOBTN); | ||
126 | insertSetting(ConfigVoice, setting); | ||
127 | |||
128 | } | ||
129 | |||
130 | |||
131 | void TTSCarbon::saveSettings(void) | ||
132 | { | ||
133 | // save settings in user config | ||
134 | RbSettings::setSubValue("carbon", RbSettings::TtsVoice, | ||
135 | getSetting(ConfigVoice)->current().toString()); | ||
136 | RbSettings::sync(); | ||
137 | } | ||
138 | |||
139 | |||
140 | /** @brief create wav file from text using the selected TTS voice. | ||
141 | */ | ||
142 | TTSStatus TTSCarbon::voice(QString text, QString wavfile, QString* errStr) | ||
143 | { | ||
144 | TTSStatus status = NoError; | ||
145 | OSErr error; | ||
146 | |||
147 | QString aifffile = wavfile + ".aiff"; | ||
148 | // FIXME: find out why we need to do this. | ||
149 | // Create a local copy of the temporary file filename. | ||
150 | // Not doing so causes weird issues (path contains trailing spaces) | ||
151 | unsigned int len = aifffile.size() + 1; | ||
152 | char* tmpfile = (char*)malloc(len * sizeof(char)); | ||
153 | strncpy(tmpfile, aifffile.toLocal8Bit().constData(), len); | ||
154 | CFStringRef tmpfileref = CFStringCreateWithCString(kCFAllocatorDefault, | ||
155 | tmpfile, kCFStringEncodingUTF8); | ||
156 | CFURLRef urlref = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, | ||
157 | tmpfileref, kCFURLPOSIXPathStyle, false); | ||
158 | SetSpeechInfo(m_channel, soOutputToFileWithCFURL, urlref); | ||
159 | |||
160 | // speak it. | ||
161 | // Convert the string to the encoding requested by the voice. Do this | ||
162 | // via CFString, as this allows to directly use the destination encoding | ||
163 | // as CFString uses the same values as the voice. | ||
164 | |||
165 | // allocate enough space to allow storing the string in a 2 byte encoding | ||
166 | unsigned int textlen = 2 * text.length() + 1; | ||
167 | char* textbuf = (char*)calloc(textlen, sizeof(char)); | ||
168 | char* utf8data = (char*)text.toUtf8().constData(); | ||
169 | int utf8bytes = text.toUtf8().size(); | ||
170 | CFStringRef cfstring = CFStringCreateWithBytes(kCFAllocatorDefault, | ||
171 | (UInt8*)utf8data, utf8bytes, | ||
172 | kCFStringEncodingUTF8, (Boolean)false); | ||
173 | CFIndex usedBuf = 0; | ||
174 | CFRange range; | ||
175 | range.location = 0; // character in string to start. | ||
176 | range.length = text.length(); // number of _characters_ in string | ||
177 | // FIXME: check if converting between encodings was lossless. | ||
178 | CFStringGetBytes(cfstring, range, m_voiceScript, ' ', | ||
179 | false, (UInt8*)textbuf, textlen, &usedBuf); | ||
180 | |||
181 | error = SpeakText(m_channel, textbuf, (unsigned long)usedBuf); | ||
182 | while(SpeechBusy()) { | ||
183 | // FIXME: add small delay here to make calls less frequent | ||
184 | QCoreApplication::processEvents(); | ||
185 | } | ||
186 | if(error != 0) { | ||
187 | *errStr = tr("Could not voice string"); | ||
188 | status = FatalError; | ||
189 | } | ||
190 | free(textbuf); | ||
191 | CFRelease(cfstring); | ||
192 | |||
193 | // convert the temporary aiff file to wav | ||
194 | if(status == NoError | ||
195 | && convertAiffToWav(tmpfile, wavfile.toLocal8Bit().constData()) != 0) { | ||
196 | *errStr = tr("Could not convert intermediate file"); | ||
197 | status = FatalError; | ||
198 | } | ||
199 | // remove temporary aiff file | ||
200 | unlink(tmpfile); | ||
201 | free(tmpfile); | ||
202 | |||
203 | return status; | ||
204 | } | ||
205 | |||
206 | |||
207 | unsigned long TTSCarbon::be2u32(unsigned char* buf) | ||
208 | { | ||
209 | return (buf[0]&0xff)<<24 | (buf[1]&0xff)<<16 | (buf[2]&0xff)<<8 | (buf[3]&0xff); | ||
210 | } | ||
211 | |||
212 | |||
213 | unsigned long TTSCarbon::be2u16(unsigned char* buf) | ||
214 | { | ||
215 | return buf[1]&0xff | (buf[0]&0xff)<<8; | ||
216 | } | ||
217 | |||
218 | |||
219 | unsigned char* TTSCarbon::u32tobuf(unsigned char* buf, uint32_t val) | ||
220 | { | ||
221 | buf[0] = val & 0xff; | ||
222 | buf[1] = (val>> 8) & 0xff; | ||
223 | buf[2] = (val>>16) & 0xff; | ||
224 | buf[3] = (val>>24) & 0xff; | ||
225 | return buf; | ||
226 | } | ||
227 | |||
228 | |||
229 | unsigned char* TTSCarbon::u16tobuf(unsigned char* buf, uint16_t val) | ||
230 | { | ||
231 | buf[0] = val & 0xff; | ||
232 | buf[1] = (val>> 8) & 0xff; | ||
233 | return buf; | ||
234 | } | ||
235 | |||
236 | |||
237 | /** @brief convert 80 bit extended ("long double") to int. | ||
238 | * This is simplified to handle the usual audio sample rates. Everything else | ||
239 | * might break. If the value isn't supported it will return 0. | ||
240 | * Conversion taken from Rockbox aiff codec. | ||
241 | */ | ||
242 | unsigned int TTSCarbon::extended2int(unsigned char* buf) | ||
243 | { | ||
244 | unsigned int result = 0; | ||
245 | /* value negative? */ | ||
246 | if(buf[0] & 0x80) | ||
247 | return 0; | ||
248 | /* check exponent. Int can handle up to 2^31. */ | ||
249 | int exponent = buf[0] << 8 | buf[1]; | ||
250 | if(exponent < 0x4000 || exponent > (0x4000 + 30)) | ||
251 | return 0; | ||
252 | result = ((buf[2]<<24) | (buf[3]<<16) | (buf[4]<<8) | buf[5]) + 1; | ||
253 | result >>= (16 + 14 - buf[1]); | ||
254 | return result; | ||
255 | } | ||
256 | |||
257 | |||
258 | /** @brief Convert aiff file to wav. Returns 0 on success. | ||
259 | */ | ||
260 | int TTSCarbon::convertAiffToWav(const char* aiff, const char* wav) | ||
261 | { | ||
262 | struct commchunk { | ||
263 | unsigned long chunksize; | ||
264 | unsigned short channels; | ||
265 | unsigned long frames; | ||
266 | unsigned short size; | ||
267 | int rate; | ||
268 | }; | ||
269 | |||
270 | struct ssndchunk { | ||
271 | unsigned long chunksize; | ||
272 | unsigned long offset; | ||
273 | unsigned long blocksize; | ||
274 | }; | ||
275 | |||
276 | FILE* in; | ||
277 | FILE* out; | ||
278 | unsigned char obuf[4]; | ||
279 | unsigned char* buf; | ||
280 | /* minimum file size for a valid aiff file is 46 bytes: | ||
281 | * - FORM chunk: 12 bytes | ||
282 | * - COMM chunk: 18 bytes | ||
283 | * - SSND chunk: 16 bytes (with no actual data) | ||
284 | */ | ||
285 | struct stat filestat; | ||
286 | stat(aiff, &filestat); | ||
287 | if(filestat.st_size < 46) | ||
288 | return -1; | ||
289 | /* read input file into memory */ | ||
290 | buf = (unsigned char*)malloc(filestat.st_size * sizeof(unsigned char)); | ||
291 | if(!buf) /* error out if malloc() failed */ | ||
292 | return -1; | ||
293 | in = fopen(aiff, "rb"); | ||
294 | if(fread(buf, 1, filestat.st_size, in) < filestat.st_size) { | ||
295 | printf("could not read file: not enought bytes read\n"); | ||
296 | fclose(in); | ||
297 | return -1; | ||
298 | } | ||
299 | fclose(in); | ||
300 | |||
301 | /* check input file format */ | ||
302 | if(memcmp(buf, "FORM", 4) | memcmp(&buf[8], "AIFF", 4)) { | ||
303 | printf("No valid AIFF header found.\n"); | ||
304 | free(buf); | ||
305 | return -1; | ||
306 | } | ||
307 | /* read COMM chunk */ | ||
308 | unsigned char* commstart = &buf[12]; | ||
309 | struct commchunk comm; | ||
310 | if(memcmp(commstart, "COMM", 4)) { | ||
311 | printf("COMM chunk not at beginning.\n"); | ||
312 | free(buf); | ||
313 | return -1; | ||
314 | } | ||
315 | comm.chunksize = be2u32(&commstart[4]); | ||
316 | comm.channels = be2u16(&commstart[8]); | ||
317 | comm.frames = be2u32(&commstart[10]); | ||
318 | comm.size = be2u16(&commstart[14]); | ||
319 | comm.rate = extended2int(&commstart[16]); | ||
320 | |||
321 | /* find SSND as next chunk */ | ||
322 | unsigned char* ssndstart = commstart + 8 + comm.chunksize; | ||
323 | while(memcmp(ssndstart, "SSND", 4) && ssndstart < (buf + filestat.st_size)) { | ||
324 | printf("Skipping chunk.\n"); | ||
325 | ssndstart += be2u32(&ssndstart[4]) + 8; | ||
326 | } | ||
327 | if(ssndstart > (buf + filestat.st_size)) { | ||
328 | free(buf); | ||
329 | return -1; | ||
330 | } | ||
331 | |||
332 | struct ssndchunk ssnd; | ||
333 | ssnd.chunksize = be2u32(&ssndstart[4]); | ||
334 | ssnd.offset = be2u32(&ssndstart[8]); | ||
335 | ssnd.blocksize = be2u32(&ssndstart[12]); | ||
336 | |||
337 | /* Calculate the total length of the resulting RIFF chunk. | ||
338 | * The length is given by frames * samples * bytes/sample. | ||
339 | * We need to add: | ||
340 | * - 16 bytes: fmt chunk header | ||
341 | * - 8 bytes: data chunk header | ||
342 | * - 4 bytes: wave chunk identifier | ||
343 | */ | ||
344 | out = fopen(wav, "wb+"); | ||
345 | |||
346 | /* write the wav header */ | ||
347 | unsigned short blocksize = comm.channels * (comm.size >> 3); | ||
348 | unsigned long rifflen = blocksize * comm.frames + 28; | ||
349 | fwrite("RIFF", 1, 4, out); | ||
350 | fwrite(u32tobuf(obuf, rifflen), 1, 4, out); | ||
351 | fwrite("WAVE", 1, 4, out); | ||
352 | |||
353 | /* write the fmt chunk and chunk size (always 16) */ | ||
354 | /* write fmt chunk header: | ||
355 | * header, size (always 0x10, format code (always 0x0001) | ||
356 | */ | ||
357 | fwrite("fmt \x10\x00\x00\x00\x01\x00", 1, 10, out); | ||
358 | /* number of channels (2 bytes) */ | ||
359 | fwrite(u16tobuf(obuf, comm.channels), 1, 2, out); | ||
360 | /* sampling rate (4 bytes) */ | ||
361 | fwrite(u32tobuf(obuf, comm.rate), 1, 4, out); | ||
362 | |||
363 | /* data rate, i.e. bytes/sec */ | ||
364 | fwrite(u32tobuf(obuf, comm.rate * blocksize), 1, 4, out); | ||
365 | |||
366 | /* data block size */ | ||
367 | fwrite(u16tobuf(obuf, blocksize), 1, 2, out); | ||
368 | |||
369 | /* bits per sample */ | ||
370 | fwrite(u16tobuf(obuf, comm.size), 1, 2, out); | ||
371 | |||
372 | /* write the data chunk */ | ||
373 | /* chunk id */ | ||
374 | fwrite("data", 1, 4, out); | ||
375 | /* chunk size: 4 bytes. */ | ||
376 | unsigned long cs = blocksize * comm.frames; | ||
377 | fwrite(u32tobuf(obuf, cs), 1, 4, out); | ||
378 | |||
379 | /* write data */ | ||
380 | unsigned char* data = ssndstart; | ||
381 | unsigned long pos = ssnd.chunksize; | ||
382 | /* byteswap if samples are 16 bit */ | ||
383 | if(comm.size == 16) { | ||
384 | while(pos) { | ||
385 | obuf[1] = *data++ & 0xff; | ||
386 | obuf[0] = *data++ & 0xff; | ||
387 | fwrite(obuf, 1, 2, out); | ||
388 | pos -= 2; | ||
389 | } | ||
390 | } | ||
391 | /* 8 bit samples have need no conversion so we can bulk copy. | ||
392 | * Everything that is not 16 bit is considered 8. */ | ||
393 | else { | ||
394 | fwrite(data, 1, pos, out); | ||
395 | } | ||
396 | /* number of bytes has to be even, even if chunksize is not. */ | ||
397 | if(cs % 2) { | ||
398 | fwrite(obuf, 1, 1, out); | ||
399 | } | ||
400 | |||
401 | fclose(out); | ||
402 | free(buf); | ||
403 | return 0; | ||
404 | } | ||
405 | |||