#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include "HsFFI.h"
#include <espeak-ng/espeak_ng.h>
#include <sndfile.h>
/* Exported Haskell functions/types */
struct session;
struct session *c_newSession();
void c_freeSession(struct session*);
struct page;
struct page *c_initialReferer();
void *c_fetchURL(struct session*, char*, struct page*, char*);
//struct page **c_fetchURLs(struct session*, struct page*, char**); // FIXME segfaults.
void c_freePage(struct page*);
char *c_renderDoc(struct session*, struct page*, _Bool);
char **c_extractLinks(struct page*);
char **c_docLinksAndRendering(struct session*, struct page*, _Bool); // FIXME segfaults.
/* espeak-ng integration. Based on the espeak-ng command source code. */
SNDFILE *fd_wav = NULL;
char *path_wav = NULL;
static int samplerate;
espeak_ng_ERROR_CONTEXT context;
int choose_format(char *path) {
SF_FORMAT_INFO format_info;
int k, count;
sf_command(fd_wav, SFC_GET_FORMAT_MAJOR_COUNT, &count, sizeof (int));
for (k = 0; k < count; k++) {
format_info.format = k;
sf_command(fd_wav, SFC_GET_FORMAT_MAJOR, &format_info, sizeof (format_info));
char *suffix = path + strlen(path) - strlen(format_info.extension);
if (strcmp(path, format_info.extension) == 0) return format_info.format;
}
return SF_FORMAT_WAV;
}
#define BUFFER_LEN 1024
#define MAX_CHANNELS 6
// FIXME convert samplerate
int read_mono(SNDFILE *fd, SF_INFO *info, short *out) {
sf_count_t count = sf_read_short(fd, out, BUFFER_LEN);
if (info->channels == 1) return count;
int i = 0;
for (int j = 0; j < count; j += info->channels) out[i++] = out[j];
return i;
}
int capture_marks(short *wav, int numsamples, espeak_EVENT *events) {
while (events->type != 0) {
if (events->type == espeakEVENT_MARK) printf("%s:\n", events->id.name);
events++;
}
return 0;
}
int save_audio(short *wav, int numsamples, espeak_EVENT *events) {
capture_marks(wav, numsamples, events);
if (wav == NULL) return 0;
while (events->type != 0) {
if (events->type == espeakEVENT_SAMPLERATE) samplerate = events->id.number;
events++;
}
if (fd_wav == NULL) {
SF_INFO info;
info.samplerate = samplerate;
info.channels = 1;
info.format = choose_format(path_wav) | SF_FORMAT_PCM_16 | SF_ENDIAN_LITTLE;
fd_wav = sf_open(path_wav, SFM_WRITE, &info);
}
if (numsamples > 0) sf_writef_short(fd_wav, wav, numsamples);
return 0;
}
int speak_initialize() {
espeak_ng_InitializePath(NULL);
context = NULL;
espeak_ng_STATUS result = espeak_ng_Initialize(&context);
if (result != ENS_OK) {
espeak_ng_PrintStatusCodeMessage(result, stderr, context);
espeak_ng_ClearErrorContext(&context);
return 2;
}
if (path_wav != NULL) {
result = espeak_ng_InitializeOutput(ENOUTPUT_MODE_SYNCHRONOUS, 0, NULL);
espeak_SetSynthCallback(save_audio);
} else {
result = espeak_ng_InitializeOutput(ENOUTPUT_MODE_SPEAK_AUDIO, 0, NULL);
espeak_SetSynthCallback(capture_marks);
}
if (result != ENS_OK) {
espeak_ng_PrintStatusCodeMessage(result, stderr, context);
return 3;
}
samplerate = espeak_ng_GetSampleRate();
return 0;
}
void speak(char *ssml) {
int flags = espeakCHARS_AUTO | espeakPHONEMES | espeakENDPAUSE | espeakCHARS_UTF8 | espeakSSML;
espeak_Synth(ssml, strlen(ssml)+1, 0, POS_CHARACTER, 0, flags, NULL, NULL);
}
int speak_finalize() {
espeak_ng_STATUS result = espeak_ng_Synchronize();
if (result != ENS_OK) {
espeak_ng_PrintStatusCodeMessage(result, stderr, context);
return 4;
}
if (path_wav != NULL) sf_close(fd_wav);
espeak_ng_Terminate();
return 0;
}
/* Main driver */
void write_links(FILE *dest, char **links) {
for (int i = 0; strcmp(links[i], " ") != 0; i++) {
fprintf(dest, "%s%c", links[i], (i % 3) == 2 ? '\n' : '\t');
}
}
FILE *parse_opt_file() {
FILE *ret = optarg != NULL ? fopen(optarg, "w") : stdout;
if (ret == NULL) {
fprintf(stderr, "Failed to open file %s\n", optarg);
hs_exit();
exit(-1);
}
return ret;
}
int main(int argc, char **argv) {
int speak_err = 0;
hs_init(&argc, &argv);
char *mimes = "text/html text/xml application/xml application/xhtml+xml text/plain";
FILE *fd_ssml = NULL;
FILE *fd_links = NULL;
int use_espeak = 0;
int c;
opterr = 0;
while ((c = getopt(argc, argv, "xs::l::w::h")) != -1) {
switch (c) {
case 'x':
mimes = "text/xml application/xml application/xhtml+xml text/html text/plain";
break;
case 's':
fd_ssml = parse_opt_file();
break;
case 'l':
fd_links = parse_opt_file();
break;
case 'w':
use_espeak = 1;
path_wav = optarg;
break;
case '?':
fprintf(stderr, "Invalid flag %c\n\n", optopt);
case 'h':
fprintf(stderr, "USAGE: rhapsode [FLAGS] URL...\n");
fprintf(stderr, "\t-x\tX(HT)ML\tIndicates to expect an X(HT)ML file.\n");
fprintf(stderr, "\t-s\tsilent/SSML\tWrites SSML to the specified file or stdout.\n");
fprintf(stderr, "\t\t\thttps://xkcd.com/1692/\n");
fprintf(stderr, "\t-l\tlinks\tWrite extracted links to specifed file or stdout as TSV.\n");
fprintf(stderr, "\t-h\thelp\tOutputs this usage information to stderr.\n");
fprintf(stderr, "\t\t\tIf both -s & -l are enabled without an argument, writes to stderr instead.\n");
hs_exit();
return c == 'h' ? 0 : 1;
}
}
if (fd_ssml == stdout && fd_links == stdout) fd_links = stderr;
if (fd_ssml == NULL && fd_links == NULL && !use_espeak) use_espeak = 1;
struct session *session = c_newSession();
struct page *referer = c_initialReferer();
if (use_espeak) speak_err = speak_initialize();
for (int i = optind; i < argc; i++) {
if (use_espeak && speak_err == 0) speak(argv[i]);
else printf("%s\n", argv[i]);
struct page *page = c_fetchURL(session, mimes, referer, argv[i]);
char *ssml = c_renderDoc(session, page, use_espeak);
char **links = c_extractLinks(page);
if (fd_ssml != NULL) fprintf(fd_ssml, "%s\n", ssml);
if (fd_links != NULL) write_links(fd_links, links);
if (use_espeak & speak_err == 0) speak(ssml);
c_freePage(page);
}
if (use_espeak & speak_err == 0) speak_err = speak_finalize();
c_freePage(referer);
c_freeSession(session);
hs_exit();
return speak_err;
}