From 487a2880b3579b86d0080453a93c6b2c3849e88a Mon Sep 17 00:00:00 2001 From: Adrian Cochrane Date: Mon, 6 Apr 2020 10:36:01 +1200 Subject: [PATCH] Embed espeak-ng --- rhapsode.cabal | 1 + src/main.c | 137 +++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 122 insertions(+), 16 deletions(-) diff --git a/rhapsode.cabal b/rhapsode.cabal index ef5e7f8..166c4c2 100644 --- a/rhapsode.cabal +++ b/rhapsode.cabal @@ -75,6 +75,7 @@ library executable rhapsode main-is: main.c ghc-options: -no-hs-main + extra-libraries: espeak-ng build-depends: base >=4.9 && <=4.12, rhapsode other-modules: Stub hs-source-dirs: src diff --git a/src/main.c b/src/main.c index afc5f5f..83a7cb0 100644 --- a/src/main.c +++ b/src/main.c @@ -5,7 +5,9 @@ #include #include "HsFFI.h" +#include +/* Exported Haskell functions/types */ struct session; struct session *c_newSession(); void c_freeSession(struct session*); @@ -20,13 +22,116 @@ 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"; @@ -35,26 +140,19 @@ int main(int argc, char **argv) { int c; opterr = 0; - while ((c = getopt(argc, argv, "xs::l::h")) != -1) { + 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 = optarg != NULL ? fopen(optarg, "w") : stdout; - if (fd_ssml == NULL) { - fprintf(stderr, "Failed to open file %s\n", optarg); - hs_exit(); - return -1; - } + fd_ssml = parse_opt_file("w"); break; case 'l': - fd_links = optarg != NULL ? fopen(optarg, "w") : stdout; - if (fd_links == NULL) { - fprintf(stderr, "Failed to open file %s\n", optarg); - hs_exit(); - return -1; - } + 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); @@ -68,28 +166,35 @@ int main(int argc, char **argv) { 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; + return c == 'h' ? 0 : 1; } } if (fd_ssml == stdout && fd_links == stdout) fd_links = stderr; - if (fd_ssml == NULL && fd_links == NULL) fd_ssml = stdout; + 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++) { - printf("%s\n", argv[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; } -- 2.30.2