#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include "HsFFI.h"
#include <espeak-ng/espeak_ng.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*);
char **c_extractLinks(struct page*);
char **c_docLinksAndRendering(struct session*, struct page*); // FIXME segfaults.
/* espeak-ng integration. Based on the espeak-ng command source code. */
FILE *fd_wav = NULL;
static int samplerate;
espeak_ng_ERROR_CONTEXT context;
void write4b(int value) {
// Write 4 bytes to a file, least significant first
int ix;
for (ix = 0; ix < 4; ix++) {
fputc(value & 0xff, fd_wav);
value = value >> 8;
}
}
int wrote_header = 0;
void write_header() {
static unsigned char wave_hdr[44] = {
'R', 'I', 'F', 'F', 0x24, 0xf0, 0xff, 0x7f, 'W', 'A', 'V', 'E', 'f', 'm', 't', ' ',
0x10, 0, 0, 0, 1, 0, 1, 0, 9, 0x3d, 0, 0, 0x12, 0x7a, 0, 0,
2, 0, 0x10, 0, 'd', 'a', 't', 'a', 0x00, 0xf0, 0xff, 0x7f
};
fwrite(wave_hdr, 1, 24, fd_wav);
write4b(samplerate);
write4b(samplerate * 2);
fwrite(&wave_hdr[32], 1, 12, fd_wav);
}
void close_wav_file() {
fflush(fd_wav);
unsigned int pos = ftell(fd_wav);
if (fseek(fd_wav, 4, SEEK_SET) != -1) write4b(pos - 8);
if (fseek(fd_wav, 40, SEEK_SET) != -1) write4b(pos - 44);
fclose(fd_wav);
}
int synth_callback(short *wav, int numsamples, espeak_EVENT *events) {
if (wav == NULL) return 0;
while (events->type != 0) {
if (events->type == espeakEVENT_SAMPLERATE) samplerate = events->id.number;
events++;
}
if (!wrote_header) write_header();
if (numsamples > 0) fwrite(wav, numsamples*2, 1, fd_wav);
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 (fd_wav != stdout) {
result = espeak_ng_InitializeOutput(ENOUTPUT_MODE_SYNCHRONOUS, 0, NULL);
espeak_SetSynthCallback(synth_callback);
} else {
result = espeak_ng_InitializeOutput(ENOUTPUT_MODE_SPEAK_AUDIO, 0, NULL);
}
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 (fd_wav != stdout) close_wav_file();
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(char *mode) {
FILE *ret = optarg != NULL ? fopen(optarg, mode) : 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 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("w");
break;
case 'l':
fd_links = parse_opt_file("w");
break;
case 'w':
fd_wav = parse_opt_file("wb");
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 && fd_wav == NULL) fd_wav = stdout;
struct session *session = c_newSession();
struct page *referer = c_initialReferer();
if (fd_wav != NULL) speak_err = speak_initialize();
for (int i = optind; i < argc; i++) {
if (fd_wav != NULL && 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);
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 (fd_wav != NULL& speak_err == 0) speak(ssml);
c_freePage(page);
}
if (fd_wav != NULL & speak_err == 0) speak_err = speak_finalize();
c_freePage(referer);
c_freeSession(session);
hs_exit();
return speak_err;
}