#include #include #include #include #include #include #include "HsFFI.h" #include #include /* 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 paragraph_no = 0, section_no = 0; int tablerow = 0, tablecol = 0, tableno = 0, in_table = 0; int capture_marks(short *wav, int numsamples, espeak_EVENT *events) { while (events->type != 0) { if (events->type == espeakEVENT_MARK) { in_table = 0; if (sscanf(events->id.name, "-rhaps-paragraph%i", ¶graph_no) == 1) {} else if (sscanf(events->id.name, "-rhaps-section%i", §ion_no) == 1) {} else if (sscanf(events->id.name, "-rhaps-tablecell%i:%ix%i", &tableno, &tablerow, &tablecol) == 3) {in_table = 1;} } 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 read_keyboard = 0; int speak_finalize() { while (read_keyboard) { char c = getc(stdin); if (c == '\033') { espeak_Cancel(); c = getc(stdin); if (c == 0 || c == -1 || c == '\033' || c == 'q') goto close; // skip [ switch (getc(stdin)) { case 'A': printf("🠕"); break; case 'B': printf("🠗"); break; case 'C': printf("âž”"); break; case 'D': printf("🠔"); break; } } } espeak_ng_STATUS result = espeak_ng_Synchronize(); if (result != ENS_OK) { espeak_ng_PrintStatusCodeMessage(result, stderr, context); return 4; } close: 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; } struct termios stored_settings; 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; tcgetattr(0, &stored_settings); int c; opterr = 0; while ((c = getopt(argc, argv, "xs::l::kw::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 'k': read_keyboard = 1; // Read input character by character, not line by line. struct termios new_settings = stored_settings; new_settings.c_lflag &= (~ICANON); new_settings.c_lflag &= (~ECHO); new_settings.c_cc[VTIME] = 1; new_settings.c_cc[VMIN] = 0; tcsetattr(0, TCSANOW, &new_settings); 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(); tcsetattr(0, TCSANOW, &stored_settings); return speak_err; }