blob: 97c3e99de1e939337b479023f680a7e49dafe0cb [file] [log] [blame] [edit]
/*
* BRLTTY - A background process providing access to the console screen (when in
* text mode) for a blind person using a refreshable braille display.
*
* Copyright (C) 1995-2023 by The BRLTTY Developers.
*
* BRLTTY comes with ABSOLUTELY NO WARRANTY.
*
* This is free software, placed under the terms of the
* GNU Lesser General Public License, as published by the Free Software
* Foundation; either version 2.1 of the License, or (at your option) any
* later version. Please see the file LICENSE-LGPL for details.
*
* Web Page: http://brltty.app/
*
* This software is maintained by Dave Mielke <dave@mielke.cc>.
*/
/* Theta/speech.c - Speech library
* For the Theta text to speech package
* Maintained by Dave Mielke <dave@mielke.cc>
*/
#include "prologue.h"
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <signal.h>
#include <sys/wait.h>
#include "log.h"
#include "parse.h"
typedef enum {
PARM_AGE,
PARM_GENDER,
PARM_LANGUAGE,
PARM_NAME,
PARM_PITCH
} DriverParameter;
#define SPKPARMS "age", "gender", "language", "name", "pitch"
#include "spk_driver.h"
struct theta_sfx_block_ops { /* used but not defined in header */
char dummy;
};
#include <theta.h>
static cst_voice *voice = NULL;
static pid_t child = -1;
static int pipeDescriptors[2];
static const int *pipeOutput = &pipeDescriptors[0];
static const int *pipeInput = &pipeDescriptors[1];
static void
initializeTheta (void) {
static int initialized = 0;
if (!initialized) {
{
const char *directory = THETA_ROOT "/voices";
setenv("THETA_VOXPATH", directory, 0);
}
setenv("THETA_HOME", THETA_ROOT, 0);
theta_init(NULL);
initialized = 1;
}
}
static void
loadVoice (theta_voice_desc *descriptor) {
if ((voice = theta_load_voice(descriptor))) {
logMessage(LOG_INFO, "Voice: %s(%s,%d)",
theta_voice_human(voice),
theta_voice_gender(voice),
theta_voice_age(voice));
} else {
logMessage(LOG_WARNING, "Voice load error: %s [%s]",
descriptor->human, descriptor->voxname);
}
}
static int
doChild (void) {
FILE *stream = fdopen(*pipeOutput, "r");
char buffer[0X400];
char *line;
while ((line = fgets(buffer, sizeof(buffer), stream))) {
theta_tts(line, voice);
}
return 0;
}
static void
spk_say (SpeechSynthesizer *spk, const unsigned char *buffer, size_t length, size_t count, const unsigned char *attributes) {
if (voice) {
if (child != -1) goto ready;
if (pipe(pipeDescriptors) != -1) {
if ((child = fork()) == -1) {
logSystemError("fork");
} else if (child == 0) {
_exit(doChild());
} else
ready: {
unsigned char text[length + 1];
memcpy(text, buffer, length);
text[length] = '\n';
write(*pipeInput, text, sizeof(text));
return;
}
close(*pipeInput);
close(*pipeOutput);
} else {
logSystemError("pipe");
}
}
}
static void
spk_mute (SpeechSynthesizer *spk) {
if (child != -1) {
close(*pipeInput);
close(*pipeOutput);
kill(child, SIGKILL);
waitpid(child, NULL, 0);
child = -1;
}
}
static void
spk_setVolume (SpeechSynthesizer *spk, unsigned char setting) {
theta_set_rescale(voice, getFloatSpeechVolume(setting), NULL);
}
static void
spk_setRate (SpeechSynthesizer *spk, unsigned char setting) {
theta_set_rate_stretch(voice, 1.0/getFloatSpeechRate(setting), NULL);
}
static int
spk_construct (SpeechSynthesizer *spk, char **parameters) {
theta_voice_search criteria;
memset(&criteria, 0, sizeof(criteria));
initializeTheta();
spk->setVolume = spk_setVolume;
spk->setRate = spk_setRate;
if (*parameters[PARM_GENDER]) {
const char *const choices[] = {"male", "female", "neuter", NULL};
unsigned int choice;
if (validateChoice(&choice, parameters[PARM_GENDER], choices)) {
criteria.gender = (char *)choices[choice];
} else {
logMessage(LOG_WARNING, "%s: %s", "invalid gender specification", parameters[PARM_GENDER]);
}
}
if (*parameters[PARM_LANGUAGE]) criteria.lang = parameters[PARM_LANGUAGE];
if (*parameters[PARM_AGE]) {
const char *word = parameters[PARM_AGE];
int value;
int younger;
static const int minimumAge = 1;
static const int maximumAge = 99;
if ((younger = word[0] == '-') && word[1]) ++word;
if (validateInteger(&value, word, &minimumAge, &maximumAge)) {
if (younger) value = -value;
criteria.age = value;
} else {
logMessage(LOG_WARNING, "%s: %s", "invalid age specification", word);
}
}
{
const char *name = parameters[PARM_NAME];
if (name && (*name == '/')) {
theta_voice_desc *descriptor = theta_try_voxdir(name, &criteria);
if (descriptor) {
loadVoice(descriptor);
theta_free_voice_desc(descriptor);
}
} else {
theta_voice_desc *descriptors = theta_enum_voices(theta_voxpath, &criteria);
if (descriptors) {
theta_voice_desc *descriptor;
for (descriptor=descriptors; descriptor; descriptor=descriptor->next) {
if (*name)
if (strcasecmp(name, descriptor->human) != 0)
continue;
loadVoice(descriptor);
if (voice) break;
}
theta_free_voicelist(descriptors);
}
}
}
if (voice) {
{
float pitch = 0.0;
static const float minimumPitch = -2.0;
static const float maximumPitch = 2.0;
if (validateFloat(&pitch, parameters[PARM_PITCH], &minimumPitch, &maximumPitch)) {
theta_set_pitch_shift(voice, pitch, NULL);
} else {
logMessage(LOG_WARNING, "%s: %s", "invalid pitch shift specification", parameters[PARM_PITCH]);
}
}
logMessage(LOG_INFO, "Theta Engine: version %s", theta_version);
return 1;
}
logMessage(LOG_WARNING, "No voices found.");
return 0;
}
static void
spk_destruct (SpeechSynthesizer *spk) {
spk_mute(spk);
if (voice) {
theta_unload_voice(voice);
voice = NULL;
}
}