| /* |
| * module_process.c - Processing loop of output modules. |
| * |
| * Copyright (C) 2020-2023, 2025 Samuel Thibault <samuel.thibault@ens-lyon.org> |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY Samuel Thibault AND CONTRIBUTORS ``AS IS'' AND |
| * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE |
| * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS |
| * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) |
| * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
| * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY |
| * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF |
| * SUCH DAMAGE. |
| */ |
| |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <stdarg.h> |
| #include <unistd.h> |
| #include <pthread.h> |
| |
| #include <spd_audio.h> |
| #include "spd_module_main.h" |
| |
| pthread_mutex_t module_stdout_mutex = PTHREAD_MUTEX_INITIALIZER; |
| |
| static int module_should_stop; |
| |
| /* This sends some text to the server, taking the mutex to avoid intermixing |
| * between multi-line answers and asynchronous sends. */ |
| void module_send(const char *format, ...) |
| { |
| va_list ap; |
| va_start(ap, format); |
| pthread_mutex_lock(&module_stdout_mutex); |
| vprintf(format, ap); |
| pthread_mutex_unlock(&module_stdout_mutex); |
| va_end(ap); |
| fflush(stdout); |
| } |
| |
| /* Whether we will send the audio to the server */ |
| static int audio_server; |
| |
| void module_audio_set_server(void) |
| { |
| audio_server = 1; |
| } |
| |
| static int module_audio_set_through_server(const char *cur_item, const char *cur_value) { |
| if (strcmp(cur_item, "audio_output_method") != 0) |
| /* We only support the audio output method parameter */ |
| return -1; |
| |
| if (strcmp(cur_value, "server") != 0) |
| /* We only support server audio output method */ |
| return -1; |
| |
| return 0; |
| } |
| |
| static void module_tts_output_send_server(const AudioTrack *track, AudioFormat format) |
| { |
| const char *p, *end; |
| size_t size = track->num_channels * track->num_samples * track->bits / 8; |
| |
| pthread_mutex_lock(&module_stdout_mutex); |
| printf("705-bits=%d\n", track->bits); |
| printf("705-num_channels=%d\n", track->num_channels); |
| printf("705-sample_rate=%d\n", track->sample_rate); |
| printf("705-num_samples=%d\n", track->num_samples); |
| printf("705-big_endian=%d\n", format); |
| |
| printf("705-AUDIO"); |
| putc(0, stdout); |
| |
| p = (const char *) track->samples; |
| end = p + size; |
| |
| /* HDLC escaping: prefix NL and escapes with escape, and invert their bit 5. */ |
| const char escape = 0x7d; |
| const char invert = 1<<5; |
| |
| while (p < end) { |
| const char *stop, *nl, *next; |
| |
| stop = memchr(p, escape, end - p); |
| nl = memchr(p, '\n', end - p); |
| |
| if (nl && (!stop || nl < stop)) |
| stop = nl; |
| |
| if (stop) { |
| next = stop+1; |
| } else { |
| next = stop = end; |
| } |
| |
| fwrite(p, 1, stop - p, stdout); |
| |
| /* Escape NL or escape */ |
| if (stop < end) { |
| putc(escape, stdout); |
| putc((*stop) ^ invert, stdout); |
| } |
| |
| p = next; |
| } |
| putc('\n', stdout); |
| printf("705 AUDIO\n"); |
| |
| pthread_mutex_unlock(&module_stdout_mutex); |
| fflush(stdout); |
| } |
| |
| /* Arbitrary chunk size in bytes, large enough to get efficient transfer |
| * but small enough to be reactive. */ |
| #define MAX_CHUNK 10000 |
| void module_tts_output_server(const AudioTrack *track, AudioFormat format) |
| { |
| AudioTrack mytrack = *track; |
| size_t sample_size = track->num_channels * track->bits / 8; |
| int samplepos = 0; |
| int num_samples; |
| |
| while (samplepos < track->num_samples) { |
| if (module_should_stop) |
| /* We are requested to stop, ignore the rest of audio */ |
| break; |
| |
| num_samples = MAX_CHUNK / sample_size; |
| if (num_samples > track->num_samples - samplepos) |
| num_samples = track->num_samples - samplepos; |
| |
| mytrack.num_samples = num_samples; |
| mytrack.samples = (void*) track->samples + samplepos * sample_size; |
| samplepos += num_samples; |
| |
| module_tts_output_send_server(&mytrack, format); |
| |
| module_process(STDIN_FILENO, 0); |
| } |
| } |
| |
| /* |
| * This only parses the SSIP protocol from the server, and calls the |
| * corresponding functions provided by the module or by module_utils.c |
| */ |
| |
| #define print(fmt, ...) module_send(fmt "\n", ## __VA_ARGS__) |
| |
| #define BAD_SYNTAX "302 ERROR BAD SYNTAX" |
| #define BAD_PARAM "303 ERROR INVALID PARAMETER OR VALUE" |
| #define BAD_MULTILINE "305 DATA MORE THAN ONE LINE" |
| |
| #define bad_syntax() print(BAD_SYNTAX) |
| #define bad_param() print(BAD_PARAM) |
| #define bad_multiline() print(BAD_MULTILINE) |
| |
| #define bad_internal() print("401 ERROR INTERNAL") |
| #define bad_memory() print("402 ERROR OUT OF MEMORY") |
| |
| /* some text |
| * at will |
| * . |
| */ |
| static void cmd_speak(int fd, SPDMessageType msgtype) |
| { |
| size_t text_allocated = 128, new_allocated; |
| char *text = malloc(text_allocated), *new_text; |
| size_t text_len = 0; |
| size_t len; |
| int ret; |
| int nlines = 0; |
| |
| print("202 OK RECEIVING MESSAGE"); |
| |
| while (1) { |
| char *line = module_readline(fd, 1); |
| int offset = 0; |
| if (!line) { |
| /* EOF */ |
| free(text); |
| return; |
| } |
| |
| if (!strcmp(line, ".\n")) { |
| /* Replace \n at the end with a \0 */ |
| if (text_len) { |
| text_len--; |
| text[text_len] = 0; |
| } |
| free(line); |
| break; |
| } |
| |
| if (line[0] == '.') { |
| offset++; |
| line++; |
| } |
| |
| nlines++; |
| len = strlen(line); |
| |
| new_allocated = text_allocated; |
| while (text_len + len > new_allocated) |
| new_allocated *= 2; |
| |
| if (new_allocated > text_allocated) { |
| new_text = realloc(text, new_allocated); |
| if (!new_text) { |
| free(line); |
| free(text); |
| bad_internal(); |
| return; |
| } |
| text = new_text; |
| text_allocated = new_allocated; |
| } |
| memcpy(text + text_len, line, len); |
| text_len += len; |
| free(line - offset); |
| } |
| |
| if (!text_len) { |
| free(text); |
| print("301 ERROR CANT SPEAK"); |
| return; |
| } |
| |
| if (msgtype != SPD_MSGTYPE_TEXT && nlines > 1) { |
| free(text); |
| print("305 DATA MORE THAN ONE LINE"); |
| return; |
| } |
| |
| if (msgtype == SPD_MSGTYPE_KEY || msgtype == SPD_MSGTYPE_CHAR) { |
| if (!strcmp(text, "space")) { |
| free(text); |
| text = strdup(" "); |
| text_len = 1; |
| } |
| } |
| |
| module_should_stop = 0; |
| |
| #pragma weak module_speak_sync |
| #pragma weak module_speak |
| if (module_speak_sync) { |
| module_speak_sync(text, text_len, msgtype); |
| } else { |
| pthread_mutex_lock(&module_stdout_mutex); |
| ret = module_speak(text, text_len, msgtype); |
| if (ret > 0) |
| printf("200 OK SPEAKING\n"); |
| else |
| printf("301 ERROR CANT SPEAK\n"); |
| fflush(stdout); |
| pthread_mutex_unlock(&module_stdout_mutex); |
| } |
| free(text); |
| } |
| |
| static void cmd_speak_text(int fd) |
| { |
| return cmd_speak(fd, SPD_MSGTYPE_TEXT); |
| } |
| |
| static void cmd_speak_sound_icon(int fd) |
| { |
| return cmd_speak(fd, SPD_MSGTYPE_SOUND_ICON); |
| } |
| |
| static void cmd_speak_char(int fd) |
| { |
| return cmd_speak(fd, SPD_MSGTYPE_CHAR); |
| } |
| |
| static void cmd_speak_key(int fd) |
| { |
| return cmd_speak(fd, SPD_MSGTYPE_KEY); |
| } |
| |
| void module_speak_ok(void) |
| { |
| print("200 OK SPEAKING"); |
| } |
| |
| void module_speak_error(void) |
| { |
| print("301 ERROR CANT SPEAK"); |
| } |
| |
| static void cmd_stop(void) |
| { |
| module_should_stop = 1; |
| module_stop(); |
| } |
| |
| static void cmd_pause(void) |
| { |
| module_should_stop = 1; |
| module_pause(); |
| } |
| |
| static void cmd_list_voices(char *line) |
| { |
| SPDVoice **voices, **voice; |
| int one = 0;; |
| char *dumb; |
| char *requested_language; |
| char *requested_variant = NULL; |
| char *save = NULL; |
| |
| voices = module_list_voices(); |
| if (!voices) { |
| print("304 CANT LIST VOICES"); |
| return; |
| } |
| |
| dumb = strtok_r(line, " \n", &save); |
| if (strcmp(dumb, "LIST")) { |
| bad_internal(); |
| return; |
| } |
| dumb = strtok_r(NULL, " \n", &save); |
| if (strcmp(dumb, "VOICES")) { |
| bad_internal(); |
| return; |
| } |
| requested_language = strtok_r(NULL, " \n", &save); |
| if (requested_language) |
| requested_variant = strtok_r(NULL, " \n", &save); |
| |
| pthread_mutex_lock(&module_stdout_mutex); |
| for (voice = voices; *voice; voice++) { |
| const char *name = (*voice)->name; |
| const char *language = (*voice)->language; |
| const char *variant = (*voice)->variant; |
| |
| if (!name) |
| /* Ok, skip this */ |
| continue; |
| |
| if (!language) |
| language = "none"; |
| if (!variant) |
| variant = "none"; |
| |
| if (requested_language) { |
| if (strcasecmp(requested_language, language)) |
| { |
| /* Not exactly the requested locale, but maybe the language? */ |
| const char *dash = strchr(language, '-'); |
| size_t langlen = dash - language; |
| |
| if (strlen(requested_language) != langlen |
| || strncasecmp(requested_language, language, langlen)) |
| /* Not the requested language */ |
| continue; |
| } |
| if (requested_variant) |
| if (strcasecmp(requested_variant, variant)) |
| /* Not the requested variant, skip */ |
| continue; |
| } |
| |
| one = 1; |
| |
| printf("200-%s\t%s\t%s\n", name, language, variant); |
| } |
| if (one) |
| printf("200 OK VOICE LIST SENT\n"); |
| else |
| printf("304 CANT LIST VOICES\n"); |
| pthread_mutex_unlock(&module_stdout_mutex); |
| fflush(stdout); |
| } |
| |
| /* FOO1=bar1 |
| * FOO2=bar2 |
| * . |
| */ |
| static int cmd_params(int fd, int ack, const char *type, int (*set)(const char *var, const char *val)) |
| { |
| char *var, *val, *save; |
| const char *err = NULL; |
| |
| print("%u OK RECEIVING %sSETTINGS", ack, type); |
| |
| while (1) { |
| char *line = module_readline(fd, 1); |
| if (!line) |
| /* EOF */ |
| return -1; |
| |
| if (!strcmp(line, ".\n")) { |
| free(line); |
| |
| if (!err) |
| return 0; |
| print("%s", err); |
| return -1; |
| } |
| |
| save = NULL; |
| var = strtok_r(line, "=", &save); |
| if (!var) { |
| err = BAD_SYNTAX; |
| free(line); |
| continue; |
| } |
| |
| val = strtok_r(NULL, "\n", &save); |
| if (!val) { |
| err = BAD_SYNTAX; |
| free(line); |
| continue; |
| } |
| |
| if (set(var, val) != 0) |
| err = BAD_PARAM; |
| |
| free(line); |
| } |
| } |
| |
| static void cmd_set(int fd) |
| { |
| if (cmd_params(fd, 203, "", module_set) != 0) |
| return; |
| |
| print("203 OK SETTINGS RECEIVED"); |
| } |
| |
| static void cmd_audio(int fd) |
| { |
| char *status = NULL; |
| int ret; |
| |
| if (audio_server) { |
| if (cmd_params(fd, 207, "AUDIO ", module_audio_set_through_server) != 0) |
| return; |
| |
| ret = 0; |
| } else { |
| if (cmd_params(fd, 207, "AUDIO ", module_audio_set) != 0) |
| return; |
| |
| ret = module_audio_init(&status); |
| } |
| |
| if (!ret) |
| print("203 OK AUDIO INITIALIZED"); |
| else |
| print("300-%s\n300 MODULE ERROR", status); |
| |
| free(status); |
| } |
| |
| static void cmd_loglevel(int fd) |
| { |
| if (cmd_params(fd, 207, "LOGLEVEL ", module_loglevel_set) != 0) |
| return; |
| print("203 OK LOGLEVEL SET"); |
| } |
| |
| /* DEBUG ON /some/file |
| * or |
| * DEBUG OFF |
| */ |
| static void cmd_debug(char *line) |
| { |
| char *debug, *on, *file = NULL, *save = NULL; |
| int enable = 0; |
| |
| debug = strtok_r(line, " \n", &save); |
| if (!debug) { |
| bad_syntax(); |
| return; |
| } |
| if (strcmp(debug, "DEBUG")) { |
| bad_internal(); |
| return; |
| } |
| |
| on = strtok_r(NULL, " \n", &save); |
| if (!on) { |
| bad_syntax(); |
| return; |
| } |
| |
| if (!strcmp(on, "ON")) { |
| enable = 1; |
| file = strtok_r(NULL, " \n", &save); |
| if (!file) { |
| bad_syntax(); |
| return; |
| } |
| } else if (strcmp(on, "OFF")) { |
| bad_syntax(); |
| return; |
| } |
| |
| if (module_debug(enable, file) != 0) |
| print("303 CANT OPEN CUSTOM DEBUG FILE"); |
| else |
| print("200 OK DEBUGGING %s", on); |
| } |
| |
| static void cmd_quit(void) |
| { |
| module_close(); |
| |
| print("210 OK QUIT"); |
| } |
| |
| int module_process(int fd, int block) |
| { |
| while (1) { |
| char *line = module_readline(fd, block); |
| if (line == NULL) |
| return -1; |
| |
| if (!strcmp(line, "SPEAK\n")) |
| cmd_speak_text(fd); |
| |
| else if (!strcmp(line, "SOUND_ICON\n")) |
| cmd_speak_sound_icon(fd); |
| |
| else if (!strcmp(line, "CHAR\n")) |
| cmd_speak_char(fd); |
| |
| else if (!strcmp(line, "KEY\n")) |
| cmd_speak_key(fd); |
| |
| else if (!strcmp(line, "STOP\n")) |
| cmd_stop(); |
| |
| else if (!strcmp(line, "PAUSE\n")) |
| cmd_pause(); |
| |
| else if (!strncmp(line, "LIST VOICES", 11)) |
| cmd_list_voices(line); |
| |
| else if (!strcmp(line, "SET\n")) |
| cmd_set(fd); |
| |
| else if (!strcmp(line, "AUDIO\n")) |
| cmd_audio(fd); |
| |
| else if (!strcmp(line, "LOGLEVEL\n")) |
| cmd_loglevel(fd); |
| |
| else if (!strncmp(line, "DEBUG", 5)) |
| cmd_debug(line); |
| |
| else if (!strcmp(line, "QUIT\n")) { |
| free(line); |
| cmd_quit(); |
| fflush(stdout); |
| return 0; |
| } |
| |
| else |
| print("300 ERR UNKNOWN COMMAND"); |
| |
| free(line); |
| fflush(stdout); |
| } |
| } |
| |
| /* Report index */ |
| void module_report_index_mark(const char *mark) |
| { |
| if (!mark) |
| return; |
| |
| print("700-%s\n700 INDEX MARK", mark); |
| } |
| |
| /* Report speak start */ |
| void module_report_event_begin(void) |
| { |
| print("701 BEGIN"); |
| } |
| |
| /* Report speak end */ |
| void module_report_event_end(void) |
| { |
| print("702 END"); |
| } |
| |
| /* Report speak stop */ |
| void module_report_event_stop(void) |
| { |
| print("703 STOP"); |
| } |
| |
| /* Report speak pause */ |
| void module_report_event_pause(void) |
| { |
| print("704 PAUSE"); |
| } |
| |
| /* Report sound icon */ |
| void module_report_icon(const char *icon) |
| { |
| if (!icon) |
| return; |
| print("706-%s\n706 ICON", icon); |
| } |