forked from len0rd/rockbox
rbutil: Merge rbutil with utils folder.
rbutil uses several components from the utils folder, and can be considered part of utils too. Having it in a separate folder is an arbitrary split that doesn't help anymore these days, so merge them. This also allows other utils to easily use libtools.make without the need to navigate to a different folder. Change-Id: I3fc2f4de19e3e776553efb5dea5f779dfec0dc21
This commit is contained in:
parent
6c6f0757d7
commit
c876d3bbef
494 changed files with 13 additions and 13 deletions
443
utils/rbutilqt/base/ttscarbon.cpp
Normal file
443
utils/rbutilqt/base/ttscarbon.cpp
Normal file
|
|
@ -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(QString text, 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;
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue