mirror of
https://github.com/jambonz/freeswitch-modules.git
synced 2026-01-25 02:08:27 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da2a21f852 | |||
| 9874bf9ae0 | |||
| 33fee93ec7 | |||
| 41aebafd1c | |||
| cfe9cad816 | |||
| 91ea9f3f73 | |||
| a873f074d9 | |||
| bd69d476e7 | |||
| fea51d5ecf | |||
| 83a2d1d730 |
@@ -480,7 +480,7 @@ AudioPipe::AudioPipe(const char* uuid, const char* host, unsigned int port, cons
|
||||
}
|
||||
AudioPipe::~AudioPipe() {
|
||||
if (m_audio_buffer) delete [] m_audio_buffer;
|
||||
if (m_recv_buf) delete [] m_recv_buf;
|
||||
if (m_recv_buf) free(m_recv_buf);
|
||||
}
|
||||
|
||||
void AudioPipe::connect(void) {
|
||||
|
||||
@@ -59,7 +59,7 @@ extern "C" {
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
fullDirPath = std::string(baseDir) + "jambonz-tts-cache-files";
|
||||
fullDirPath = std::string(baseDir) + "tts-cache-files";
|
||||
|
||||
// Create the directory with read, write, and execute permissions for everyone
|
||||
mode_t oldMask = umask(0);
|
||||
@@ -135,23 +135,12 @@ extern "C" {
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
if (a->session_id) {
|
||||
if (a->rate != 8000 /*Hz*/) {
|
||||
int err;
|
||||
switch_codec_implementation_t read_impl;
|
||||
|
||||
/* lock and unlock session */
|
||||
switch_core_session_t *psession = switch_core_session_locate(a->session_id);
|
||||
switch_core_session_get_read_impl(psession, &read_impl);
|
||||
switch_core_session_rwunlock(psession);
|
||||
|
||||
uint32_t samples_per_second = !strcasecmp(read_impl.iananame, "g722") ? read_impl.actual_samples_per_second : read_impl.samples_per_second;
|
||||
a->samples_rate = samples_per_second;
|
||||
if (samples_per_second != 8000 /*Hz*/) {
|
||||
a->resampler = speex_resampler_init(1, 8000, samples_per_second, SWITCH_RESAMPLE_QUALITY, &err);
|
||||
if (0 != err) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error initializing resampler: %s.\n", speex_resampler_strerror(err));
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
a->resampler = speex_resampler_init(1, 8000, a->rate, SWITCH_RESAMPLE_QUALITY, &err);
|
||||
if (0 != err) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error initializing resampler: %s.\n", speex_resampler_strerror(err));
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,72 +168,77 @@ extern "C" {
|
||||
speechConfig->SetEndpointId(a->endpointId);
|
||||
}
|
||||
|
||||
auto speechSynthesizer = SpeechSynthesizer::FromConfig(speechConfig);
|
||||
try {
|
||||
auto speechSynthesizer = SpeechSynthesizer::FromConfig(speechConfig);
|
||||
|
||||
speechSynthesizer->SynthesisStarted += [a](const SpeechSynthesisEventArgs& e) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "azure_speech_feed_tts SynthesisStarted\n");
|
||||
};
|
||||
speechSynthesizer->SynthesisStarted += [a](const SpeechSynthesisEventArgs& e) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "azure_speech_feed_tts SynthesisStarted\n");
|
||||
};
|
||||
|
||||
speechSynthesizer->Synthesizing += [a](const SpeechSynthesisEventArgs& e) {
|
||||
if (a->flushed) return;
|
||||
bool fireEvent = false;
|
||||
CircularBuffer_t *cBuffer = (CircularBuffer_t *) a->circularBuffer;
|
||||
speechSynthesizer->Synthesizing += [a](const SpeechSynthesisEventArgs& e) {
|
||||
if (a->flushed) return;
|
||||
bool fireEvent = false;
|
||||
CircularBuffer_t *cBuffer = (CircularBuffer_t *) a->circularBuffer;
|
||||
|
||||
auto audioData = e.Result->GetAudioData();
|
||||
if (a->file) {
|
||||
fwrite(audioData->data(), 1, audioData->size(), a->file);
|
||||
}
|
||||
auto audioData = e.Result->GetAudioData();
|
||||
if (a->file) {
|
||||
fwrite(audioData->data(), 1, audioData->size(), a->file);
|
||||
}
|
||||
|
||||
/**
|
||||
* this sort of reinterpretation can be dangerous as a general rule, but in this case we know that the data
|
||||
* is 16-bit PCM, so it's safe to do this and its much faster than copying the data byte by byte
|
||||
*/
|
||||
const uint16_t* begin = reinterpret_cast<const uint16_t*>(audioData->data());
|
||||
const uint16_t* end = reinterpret_cast<const uint16_t*>(audioData->data() + audioData->size());
|
||||
/**
|
||||
* this sort of reinterpretation can be dangerous as a general rule, but in this case we know that the data
|
||||
* is 16-bit PCM, so it's safe to do this and its much faster than copying the data byte by byte
|
||||
*/
|
||||
const uint16_t* begin = reinterpret_cast<const uint16_t*>(audioData->data());
|
||||
const uint16_t* end = reinterpret_cast<const uint16_t*>(audioData->data() + audioData->size());
|
||||
|
||||
/* lock as briefly as possible */
|
||||
switch_mutex_lock(a->mutex);
|
||||
if (cBuffer->capacity() - cBuffer->size() < audioData->size()) {
|
||||
cBuffer->set_capacity(cBuffer->size() + std::max( audioData->size(), (size_t)BUFFER_SIZE));
|
||||
}
|
||||
cBuffer->insert(cBuffer->end(), begin, end);
|
||||
switch_mutex_unlock(a->mutex);
|
||||
/* lock as briefly as possible */
|
||||
switch_mutex_lock(a->mutex);
|
||||
if (cBuffer->capacity() - cBuffer->size() < audioData->size()) {
|
||||
cBuffer->set_capacity(cBuffer->size() + std::max( audioData->size(), (size_t)BUFFER_SIZE));
|
||||
}
|
||||
cBuffer->insert(cBuffer->end(), begin, end);
|
||||
switch_mutex_unlock(a->mutex);
|
||||
|
||||
if (0 == a->reads++) {
|
||||
fireEvent = true;
|
||||
}
|
||||
if (0 == a->reads++) {
|
||||
fireEvent = true;
|
||||
}
|
||||
|
||||
if (fireEvent && a->session_id) {
|
||||
auto endTime = std::chrono::high_resolution_clock::now();
|
||||
auto startTime = *static_cast<std::chrono::time_point<std::chrono::high_resolution_clock>*>(a->startTime);
|
||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);
|
||||
auto time_to_first_byte_ms = std::to_string(duration.count());
|
||||
switch_core_session_t* session = switch_core_session_locate(a->session_id);
|
||||
if (session) {
|
||||
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||
switch_core_session_rwunlock(session);
|
||||
if (channel) {
|
||||
switch_event_t *event;
|
||||
if (switch_event_create(&event, SWITCH_EVENT_PLAYBACK_START) == SWITCH_STATUS_SUCCESS) {
|
||||
switch_channel_event_set_data(channel, event);
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "Playback-File-Type", "tts_stream");
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_time_to_first_byte_ms", time_to_first_byte_ms.c_str());
|
||||
if (a->cache_filename) {
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_cache_filename", a->cache_filename);
|
||||
if (fireEvent && a->session_id) {
|
||||
auto endTime = std::chrono::high_resolution_clock::now();
|
||||
auto startTime = *static_cast<std::chrono::time_point<std::chrono::high_resolution_clock>*>(a->startTime);
|
||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);
|
||||
auto time_to_first_byte_ms = std::to_string(duration.count());
|
||||
switch_core_session_t* session = switch_core_session_locate(a->session_id);
|
||||
if (session) {
|
||||
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||
switch_core_session_rwunlock(session);
|
||||
if (channel) {
|
||||
switch_event_t *event;
|
||||
if (switch_event_create(&event, SWITCH_EVENT_PLAYBACK_START) == SWITCH_STATUS_SUCCESS) {
|
||||
switch_channel_event_set_data(channel, event);
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "Playback-File-Type", "tts_stream");
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_time_to_first_byte_ms", time_to_first_byte_ms.c_str());
|
||||
if (a->cache_filename) {
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_cache_filename", a->cache_filename);
|
||||
}
|
||||
switch_event_fire(&event);
|
||||
} else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "speechSynthesizer->Synthesizing: failed to create event\n");
|
||||
}
|
||||
switch_event_fire(&event);
|
||||
} else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "speechSynthesizer->Synthesizing: failed to create event\n");
|
||||
}else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "speechSynthesizer->Synthesizing: channel not found\n");
|
||||
}
|
||||
}else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "speechSynthesizer->Synthesizing: channel not found\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
std::thread(start_synthesis, speechSynthesizer, text, a).detach();
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "azure_speech_feed_tts sent synthesize request\n");
|
||||
std::thread(start_synthesis, speechSynthesizer, text, a).detach();
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "azure_speech_feed_tts sent synthesize request\n");
|
||||
} catch (const std::exception& e) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "mod_azure_tts: Exception: %s\n", e.what());
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -271,9 +265,7 @@ extern "C" {
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
// azure returned 8000hz 16 bit data, we have to take enough data based on call sample rate.
|
||||
size_t size = a->samples_rate ?
|
||||
std::min((*datalen/(2 * a->samples_rate / 8000)), bufSize) :
|
||||
std::min((*datalen/2), bufSize);
|
||||
size_t size = std::min((*datalen/(2 * a->rate / 8000)), bufSize);
|
||||
pcm_data.insert(pcm_data.end(), cBuffer->begin(), cBuffer->begin() + size);
|
||||
cBuffer->erase(cBuffer->begin(), cBuffer->begin() + size);
|
||||
switch_mutex_unlock(a->mutex);
|
||||
|
||||
@@ -82,7 +82,6 @@ static switch_status_t a_speech_feed_tts(switch_speech_handle_t *sh, char *text,
|
||||
a->draining = 0;
|
||||
a->reads = 0;
|
||||
a->flushed = 0;
|
||||
a->samples_rate = 0;
|
||||
|
||||
return azure_speech_feed_tts(a, text, flags);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ typedef struct azure_data {
|
||||
int reads;
|
||||
int cache_audio;
|
||||
int flushed;
|
||||
uint32_t samples_rate;
|
||||
|
||||
void *startTime;
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
include $(top_srcdir)/build/modmake.rulesam
|
||||
MODNAME=mod_custom_tts
|
||||
|
||||
mod_LTLIBRARIES = mod_custom_tts.la
|
||||
mod_custom_tts_la_SOURCES = mod_custom_tts.c custom_glue.cpp
|
||||
mod_custom_tts_la_CFLAGS = $(AM_CFLAGS)
|
||||
mod_custom_tts_la_LIBADD = $(switch_builddir)/libfreeswitch.la
|
||||
mod_custom_tts_la_LDFLAGS = -avoid-version -module -no-undefined -shared -lstdc++ -lboost_system -lboost_thread -lmpg123
|
||||
@@ -1,3 +1,3 @@
|
||||
# mod_google_tts
|
||||
# mod_custom_tts
|
||||
|
||||
A Freeswitch module that allows speak text to speech audio from Google stream.
|
||||
A Freeswitch module that allows speak text to speech audio from custom vendor stream.
|
||||
@@ -0,0 +1,960 @@
|
||||
#include "mod_custom_tts.h"
|
||||
#include <switch.h>
|
||||
#include <switch_json.h>
|
||||
#include <curl/curl.h>
|
||||
#include <cstdlib>
|
||||
|
||||
#include <boost/circular_buffer.hpp>
|
||||
#include <boost/thread.hpp>
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include <boost/pool/object_pool.hpp>
|
||||
#include <boost/bind/bind.hpp>
|
||||
#include <boost/tokenizer.hpp>
|
||||
#include <boost/foreach.hpp>
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/assign/list_of.hpp>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
#include "mpg123.h"
|
||||
|
||||
#define BUFFER_GROW_SIZE (8192)
|
||||
#define MP3_DCACHE 8192 * 2
|
||||
|
||||
typedef boost::circular_buffer<uint16_t> CircularBuffer_t;
|
||||
/* Global information, common to all connections */
|
||||
typedef struct
|
||||
{
|
||||
CURLM *multi;
|
||||
int still_running;
|
||||
} GlobalInfo_t;
|
||||
static GlobalInfo_t global;
|
||||
|
||||
/* Information associated with a specific easy handle */
|
||||
typedef struct
|
||||
{
|
||||
CURL *easy;
|
||||
custom_t* custom;
|
||||
char* body;
|
||||
struct curl_slist *hdr_list;
|
||||
GlobalInfo_t *global;
|
||||
mpg123_handle *mh;
|
||||
char error[CURL_ERROR_SIZE];
|
||||
FILE* file;
|
||||
std::chrono::time_point<std::chrono::high_resolution_clock> startTime;
|
||||
bool flushed;
|
||||
} ConnInfo_t;
|
||||
|
||||
|
||||
static boost::object_pool<ConnInfo_t> pool ;
|
||||
static std::map<curl_socket_t, boost::asio::ip::tcp::socket *> socket_map;
|
||||
static boost::asio::io_service io_service;
|
||||
static boost::asio::deadline_timer timer(io_service);
|
||||
static std::string fullDirPath;
|
||||
static std::thread worker_thread;
|
||||
|
||||
std::string secondsToMillisecondsString(double seconds) {
|
||||
// Convert to milliseconds
|
||||
double milliseconds = seconds * 1000.0;
|
||||
|
||||
// Truncate to remove fractional part
|
||||
long milliseconds_long = static_cast<long>(milliseconds);
|
||||
|
||||
// Convert to string
|
||||
return std::to_string(milliseconds_long);
|
||||
}
|
||||
|
||||
static CURL* createEasyHandle(void) {
|
||||
CURL* easy = curl_easy_init();
|
||||
if(!easy) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "curl_easy_init() failed!\n");
|
||||
return nullptr ;
|
||||
}
|
||||
|
||||
curl_easy_setopt(easy, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(easy, CURLOPT_USERAGENT, "jambonz/0.8.5");
|
||||
|
||||
// set connect timeout to 3 seconds and total timeout to 109 seconds
|
||||
curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, 3000L);
|
||||
curl_easy_setopt(easy, CURLOPT_TIMEOUT, 10L);
|
||||
|
||||
return easy ;
|
||||
}
|
||||
|
||||
static void cleanupConn(ConnInfo_t *conn) {
|
||||
auto c = conn->custom;
|
||||
|
||||
if (conn->mh) {
|
||||
mpg123_close(conn->mh);
|
||||
mpg123_delete(conn->mh);
|
||||
}
|
||||
|
||||
if( conn->hdr_list ) {
|
||||
curl_slist_free_all(conn->hdr_list);
|
||||
conn->hdr_list = nullptr ;
|
||||
}
|
||||
curl_easy_cleanup(conn->easy);
|
||||
|
||||
if (conn->file) {
|
||||
if (fclose(conn->file) != 0) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "cleanupConn: error closing audio cache file\n");
|
||||
}
|
||||
conn->file = nullptr ;
|
||||
}
|
||||
|
||||
c->conn = nullptr ;
|
||||
c->draining = 1;
|
||||
|
||||
memset(conn, 0, sizeof(ConnInfo_t));
|
||||
pool.destroy(conn) ;
|
||||
}
|
||||
|
||||
/* Check for completed transfers, and remove their easy handles */
|
||||
void check_multi_info(GlobalInfo_t *g) {
|
||||
CURLMsg *msg;
|
||||
int msgs_left;
|
||||
ConnInfo_t *conn;
|
||||
CURL *easy;
|
||||
CURLcode res;
|
||||
|
||||
while((msg = curl_multi_info_read(g->multi, &msgs_left))) {
|
||||
if(msg->msg == CURLMSG_DONE) {
|
||||
long response_code;
|
||||
double namelookup=0, connect=0, total=0 ;
|
||||
char *ct = NULL ;
|
||||
|
||||
easy = msg->easy_handle;
|
||||
res = msg->data.result;
|
||||
curl_easy_getinfo(easy, CURLINFO_PRIVATE, &conn);
|
||||
curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &response_code);
|
||||
curl_easy_getinfo(easy, CURLINFO_CONTENT_TYPE, &ct);
|
||||
|
||||
curl_easy_getinfo(easy, CURLINFO_NAMELOOKUP_TIME, &namelookup);
|
||||
curl_easy_getinfo(easy, CURLINFO_CONNECT_TIME, &connect);
|
||||
curl_easy_getinfo(easy, CURLINFO_TOTAL_TIME, &total);
|
||||
|
||||
auto c = conn->custom;
|
||||
c->response_code = response_code;
|
||||
if (ct) c->ct = strdup(ct);
|
||||
|
||||
std::string name_lookup_ms = secondsToMillisecondsString(namelookup);
|
||||
std::string connect_ms = secondsToMillisecondsString(connect);
|
||||
std::string final_response_time_ms = secondsToMillisecondsString(total);
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG,
|
||||
"mod_custom_tts: response: %ld, content-type %s,"
|
||||
"dns(ms): %" CURL_FORMAT_CURL_OFF_T ".%06ld, "
|
||||
"connect(ms): %" CURL_FORMAT_CURL_OFF_T ".%06ld, "
|
||||
"total(ms): %" CURL_FORMAT_CURL_OFF_T ".%06ld\n",
|
||||
response_code, ct,
|
||||
(long)(namelookup), (long)(fmod(namelookup, 1.0) * 1000000),
|
||||
(long)(connect), (long)(fmod(connect, 1.0) * 1000000),
|
||||
(long)(total), (long)(fmod(total, 1.0) * 1000000));
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "name lookup time: %s\n", name_lookup_ms.c_str());
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "connect time: %s\n", connect_ms.c_str());
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "final response time: %s\n", final_response_time_ms.c_str());
|
||||
|
||||
c->name_lookup_time_ms = strdup(name_lookup_ms.c_str());
|
||||
c->connect_time_ms = strdup(connect_ms.c_str());
|
||||
c->final_response_time_ms = strdup(final_response_time_ms.c_str());
|
||||
|
||||
curl_multi_remove_handle(g->multi, easy);
|
||||
cleanupConn(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int mcode_test(const char *where, CURLMcode code) {
|
||||
if(CURLM_OK != code) {
|
||||
const char *s;
|
||||
switch(code) {
|
||||
case CURLM_CALL_MULTI_PERFORM:
|
||||
s = "CURLM_CALL_MULTI_PERFORM";
|
||||
break;
|
||||
case CURLM_BAD_HANDLE:
|
||||
s = "CURLM_BAD_HANDLE";
|
||||
break;
|
||||
case CURLM_BAD_EASY_HANDLE:
|
||||
s = "CURLM_BAD_EASY_HANDLE";
|
||||
break;
|
||||
case CURLM_OUT_OF_MEMORY:
|
||||
s = "CURLM_OUT_OF_MEMORY";
|
||||
break;
|
||||
case CURLM_INTERNAL_ERROR:
|
||||
s = "CURLM_INTERNAL_ERROR";
|
||||
break;
|
||||
case CURLM_UNKNOWN_OPTION:
|
||||
s = "CURLM_UNKNOWN_OPTION";
|
||||
break;
|
||||
case CURLM_LAST:
|
||||
s = "CURLM_LAST";
|
||||
break;
|
||||
default:
|
||||
s = "CURLM_unknown";
|
||||
break;
|
||||
case CURLM_BAD_SOCKET:
|
||||
s = "CURLM_BAD_SOCKET";
|
||||
break;
|
||||
}
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "mcode_test ERROR: %s returns %s:%d\n", where, s, code);
|
||||
|
||||
return -1;
|
||||
}
|
||||
return 0 ;
|
||||
}
|
||||
|
||||
static void remsock(int *f, GlobalInfo_t *g) {
|
||||
if(f) {
|
||||
free(f);
|
||||
f = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* Called by asio when there is an action on a socket */
|
||||
static void event_cb(GlobalInfo_t *g, curl_socket_t s, int action, const boost::system::error_code & error, int *fdp) {
|
||||
int f = *fdp;
|
||||
|
||||
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "event_cb socket %#X has action %d\n", s, action) ;
|
||||
|
||||
// Socket already POOL REMOVED.
|
||||
if (f == CURL_POLL_REMOVE) {
|
||||
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "event_cb socket %#X removed\n", s);
|
||||
remsock(fdp, g);
|
||||
return;
|
||||
}
|
||||
|
||||
if(socket_map.find(s) == socket_map.end()) {
|
||||
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "event_cb: socket %#X already closed\n, s");
|
||||
return;
|
||||
}
|
||||
|
||||
/* make sure the event matches what are wanted */
|
||||
if(f == action || f == CURL_POLL_INOUT) {
|
||||
if(error) {
|
||||
action = CURL_CSELECT_ERR;
|
||||
}
|
||||
CURLMcode rc = curl_multi_socket_action(g->multi, s, action, &g->still_running);
|
||||
|
||||
mcode_test("event_cb: curl_multi_socket_action", rc);
|
||||
check_multi_info(g);
|
||||
|
||||
if(g->still_running <= 0) {
|
||||
timer.cancel();
|
||||
}
|
||||
|
||||
/* keep on watching.
|
||||
* the socket may have been closed and/or fdp may have been changed
|
||||
* in curl_multi_socket_action(), so check them both */
|
||||
if(!error && socket_map.find(s) != socket_map.end() &&
|
||||
(f == action || f == CURL_POLL_INOUT)) {
|
||||
boost::asio::ip::tcp::socket *tcp_socket = socket_map.find(s)->second;
|
||||
|
||||
if(action == CURL_POLL_IN) {
|
||||
tcp_socket->async_read_some(boost::asio::null_buffers(),
|
||||
boost::bind(&event_cb, g, s,
|
||||
action, boost::placeholders::_1, fdp));
|
||||
}
|
||||
if(action == CURL_POLL_OUT) {
|
||||
tcp_socket->async_write_some(boost::asio::null_buffers(),
|
||||
boost::bind(&event_cb, g, s,
|
||||
action, boost::placeholders::_1, fdp));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* socket functions */
|
||||
static void setsock(int *fdp, curl_socket_t s, CURL *e, int act, int oldact, GlobalInfo_t *g) {
|
||||
std::map<curl_socket_t, boost::asio::ip::tcp::socket *>::iterator it = socket_map.find(s);
|
||||
|
||||
if(it == socket_map.end()) {
|
||||
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "setsock: socket %#X not found\n, s");
|
||||
return;
|
||||
}
|
||||
|
||||
boost::asio::ip::tcp::socket * tcp_socket = it->second;
|
||||
|
||||
*fdp = act;
|
||||
|
||||
if(act == CURL_POLL_IN) {
|
||||
if(oldact != CURL_POLL_IN && oldact != CURL_POLL_INOUT) {
|
||||
tcp_socket->async_read_some(boost::asio::null_buffers(),
|
||||
boost::bind(&event_cb, g, s,
|
||||
CURL_POLL_IN, boost::placeholders::_1, fdp));
|
||||
}
|
||||
}
|
||||
else if(act == CURL_POLL_OUT) {
|
||||
if(oldact != CURL_POLL_OUT && oldact != CURL_POLL_INOUT) {
|
||||
tcp_socket->async_write_some(boost::asio::null_buffers(),
|
||||
boost::bind(&event_cb, g, s,
|
||||
CURL_POLL_OUT, boost::placeholders::_1, fdp));
|
||||
}
|
||||
}
|
||||
else if(act == CURL_POLL_INOUT) {
|
||||
if(oldact != CURL_POLL_IN && oldact != CURL_POLL_INOUT) {
|
||||
tcp_socket->async_read_some(boost::asio::null_buffers(),
|
||||
boost::bind(&event_cb, g, s,
|
||||
CURL_POLL_IN, boost::placeholders::_1, fdp));
|
||||
}
|
||||
if(oldact != CURL_POLL_OUT && oldact != CURL_POLL_INOUT) {
|
||||
tcp_socket->async_write_some(boost::asio::null_buffers(),
|
||||
boost::bind(&event_cb, g, s,
|
||||
CURL_POLL_OUT, boost::placeholders::_1, fdp));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void addsock(curl_socket_t s, CURL *easy, int action, GlobalInfo_t *g) {
|
||||
/* fdp is used to store current action */
|
||||
int *fdp = (int *) calloc(sizeof(int), 1);
|
||||
|
||||
setsock(fdp, s, easy, action, 0, g);
|
||||
curl_multi_assign(g->multi, s, fdp);
|
||||
}
|
||||
|
||||
static int sock_cb(CURL *e, curl_socket_t s, int what, void *cbp, void *sockp) {
|
||||
GlobalInfo_t *g = &global;
|
||||
|
||||
int *actionp = (int *) sockp;
|
||||
static const char *whatstr[] = { "none", "IN", "OUT", "INOUT", "REMOVE"};
|
||||
|
||||
if(what == CURL_POLL_REMOVE) {
|
||||
*actionp = what;
|
||||
}
|
||||
else {
|
||||
if(!actionp) {
|
||||
addsock(s, e, what, g);
|
||||
}
|
||||
else {
|
||||
setsock(actionp, s, e, what, *actionp, g);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void threadFunc() {
|
||||
/* to make sure the event loop doesn't terminate when there is no work to do */
|
||||
io_service.reset() ;
|
||||
boost::asio::io_service::work work(io_service);
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "mod_custom_tts threadFunc - starting\n");
|
||||
|
||||
for(;;) {
|
||||
|
||||
try {
|
||||
io_service.run() ;
|
||||
break ;
|
||||
}
|
||||
catch( std::exception& e) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "mod_custom_tts threadFunc - Error: %s\n", e.what());
|
||||
}
|
||||
}
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "mod_custom_tts threadFunc - ending\n");
|
||||
}
|
||||
|
||||
|
||||
/* Called by asio when our timeout expires */
|
||||
static void timer_cb(const boost::system::error_code & error, GlobalInfo_t *g)
|
||||
{
|
||||
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "timer_cb\n");
|
||||
|
||||
if(!error) {
|
||||
CURLMcode rc = curl_multi_socket_action(g->multi, CURL_SOCKET_TIMEOUT, 0, &g->still_running);
|
||||
mcode_test("timer_cb: curl_multi_socket_action", rc);
|
||||
check_multi_info(g);
|
||||
}
|
||||
}
|
||||
|
||||
int multi_timer_cb(CURLM *multi, long timeout_ms, GlobalInfo_t *g) {
|
||||
|
||||
/* cancel running timer */
|
||||
timer.cancel();
|
||||
|
||||
if(timeout_ms >= 0) {
|
||||
// from libcurl 7.88.1-10+deb12u4 does not allow call curl_multi_socket_action or curl_multi_perform in curl_multi callback directly
|
||||
timer.expires_from_now(boost::posix_time::millisec(timeout_ms ? timeout_ms : 1));
|
||||
timer.async_wait(boost::bind(&timer_cb, boost::placeholders::_1, g));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static std::vector<uint16_t> convert_mp3_to_linear(ConnInfo_t *conn, uint8_t *data, size_t len) {
|
||||
std::vector<uint16_t> linear_data;
|
||||
int eof = 0;
|
||||
int mp3err = 0;
|
||||
unsigned char decode_buf[MP3_DCACHE];
|
||||
|
||||
if(mpg123_feed(conn->mh, data, len) == MPG123_OK) {
|
||||
while(!eof) {
|
||||
size_t usedlen = 0;
|
||||
off_t frame_offset;
|
||||
unsigned char* audio;
|
||||
|
||||
int decode_status = mpg123_decode_frame(conn->mh, &frame_offset, &audio, &usedlen);
|
||||
|
||||
switch(decode_status) {
|
||||
case MPG123_NEW_FORMAT:
|
||||
continue;
|
||||
|
||||
case MPG123_OK:
|
||||
for(size_t i = 0; i < usedlen; i += 2) {
|
||||
uint16_t value = reinterpret_cast<uint16_t*>(audio)[i / 2];
|
||||
linear_data.push_back(value);
|
||||
}
|
||||
break;
|
||||
|
||||
case MPG123_DONE:
|
||||
case MPG123_NEED_MORE:
|
||||
eof = 1;
|
||||
break;
|
||||
|
||||
case MPG123_ERR:
|
||||
default:
|
||||
if(++mp3err >= 5) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Decoder Error!\n");
|
||||
eof = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (eof)
|
||||
break;
|
||||
|
||||
mp3err = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return linear_data;
|
||||
}
|
||||
/* CURLOPT_WRITEFUNCTION */
|
||||
static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
|
||||
bool fireEvent = false;
|
||||
uint8_t *data = (uint8_t *) ptr;
|
||||
size_t bytes_received = size * nmemb;
|
||||
auto c = conn->custom;
|
||||
CircularBuffer_t *cBuffer = (CircularBuffer_t *) c->circularBuffer;
|
||||
std::vector<uint16_t> pcm_data;
|
||||
|
||||
if (conn->flushed || cBuffer == nullptr) {
|
||||
/* this will abort the transfer */
|
||||
return 0;
|
||||
}
|
||||
{
|
||||
switch_mutex_lock(c->mutex);
|
||||
|
||||
if (c->response_code > 0 && c->response_code != 200) {
|
||||
std::string body((char *) ptr, bytes_received);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: received body %s\n", body.c_str());
|
||||
c->err_msg = strdup(body.c_str());
|
||||
switch_mutex_unlock(c->mutex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* cache file will stay in the mp3 format for size (smaller) and simplicity */
|
||||
if (conn->file) fwrite(data, sizeof(uint8_t), bytes_received, conn->file);
|
||||
|
||||
pcm_data = convert_mp3_to_linear(conn, data, bytes_received);
|
||||
size_t bytesResampled = pcm_data.size() * sizeof(uint16_t);
|
||||
|
||||
// Resize the buffer if necessary
|
||||
if (cBuffer->capacity() - cBuffer->size() < (bytesResampled / sizeof(uint16_t))) {
|
||||
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "write_cb growing buffer\n");
|
||||
|
||||
//TODO: if buffer exceeds some max size, return CURL_WRITEFUNC_ERROR to abort the transfer
|
||||
cBuffer->set_capacity(cBuffer->size() + std::max((bytesResampled / sizeof(uint16_t)), (size_t)BUFFER_GROW_SIZE));
|
||||
}
|
||||
|
||||
/* Push the data into the buffer */
|
||||
cBuffer->insert(cBuffer->end(), pcm_data.data(), pcm_data.data() + pcm_data.size());
|
||||
|
||||
if (0 == c->reads++) {
|
||||
fireEvent = true;
|
||||
}
|
||||
switch_mutex_unlock(c->mutex);
|
||||
}
|
||||
if (fireEvent && c->session_id) {
|
||||
auto endTime = std::chrono::high_resolution_clock::now();
|
||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - conn->startTime);
|
||||
auto time_to_first_byte_ms = std::to_string(duration.count());
|
||||
switch_core_session_t* session = switch_core_session_locate(c->session_id);
|
||||
if (session) {
|
||||
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||
switch_core_session_rwunlock(session);
|
||||
if (channel) {
|
||||
switch_event_t *event;
|
||||
if (switch_event_create(&event, SWITCH_EVENT_PLAYBACK_START) == SWITCH_STATUS_SUCCESS) {
|
||||
switch_channel_event_set_data(channel, event);
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "write_cb: firing playback-started\n");
|
||||
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "Playback-File-Type", "tts_stream");
|
||||
if (c->name_lookup_time_ms) {
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_custom_name_lookup_time_ms", c->name_lookup_time_ms);
|
||||
}
|
||||
if (c->connect_time_ms) {
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_custom_connect_time_ms", c->connect_time_ms);
|
||||
}
|
||||
if (c->final_response_time_ms) {
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_custom_final_response_time_ms", c->final_response_time_ms);
|
||||
}
|
||||
if (c->voice_name) {
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_custom_voice_name", c->voice_name);
|
||||
}
|
||||
if (c->cache_filename) {
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_cache_filename", c->cache_filename);
|
||||
}
|
||||
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_time_to_first_byte_ms", time_to_first_byte_ms.c_str());
|
||||
switch_event_fire(&event);
|
||||
}
|
||||
else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: failed to create event\n");
|
||||
}
|
||||
}
|
||||
else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: channel not found\n");
|
||||
}
|
||||
}
|
||||
else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: session %s not found\n", c->session_id);
|
||||
}
|
||||
}
|
||||
return bytes_received;
|
||||
}
|
||||
|
||||
static bool parseHeader(const std::string& str, std::string& header, std::string& value) {
|
||||
std::vector<std::string> parts;
|
||||
boost::split(parts, str, boost::is_any_of(":"), boost::token_compress_on);
|
||||
|
||||
if (parts.size() != 2)
|
||||
return false;
|
||||
|
||||
header = boost::trim_copy(parts[0]);
|
||||
value = boost::trim_copy(parts[1]);
|
||||
return true;
|
||||
}
|
||||
|
||||
static int extract_response_code(const std::string& input) {
|
||||
std::size_t space_pos = input.find(' ');
|
||||
if (space_pos == std::string::npos) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Invalid HTTP response format %s\n", input.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::size_t code_start_pos = space_pos + 1;
|
||||
std::size_t code_end_pos = input.find(' ', code_start_pos);
|
||||
if (code_end_pos == std::string::npos) {
|
||||
code_end_pos = input.length();
|
||||
}
|
||||
|
||||
std::string code_str = input.substr(code_start_pos, code_end_pos - code_start_pos);
|
||||
int response_code = std::stoi(code_str);
|
||||
return response_code;
|
||||
}
|
||||
|
||||
static size_t header_callback(char *buffer, size_t size, size_t nitems, ConnInfo_t *conn) {
|
||||
size_t bytes_received = size * nitems;
|
||||
const std::string prefix = "HTTP/";
|
||||
custom_t* c = conn->custom;
|
||||
std::string header, value;
|
||||
std::string input(buffer, bytes_received);
|
||||
if (parseHeader(input, header, value)) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "recv header: %s with value %s\n", header.c_str(), value.c_str());
|
||||
}
|
||||
else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "header_callback: %s\n", input.c_str());
|
||||
if (input.rfind(prefix, 0) == 0) {
|
||||
try {
|
||||
c->response_code = extract_response_code(input);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "header_callback: parsed response code: %ld\n", c->response_code);
|
||||
} catch (const std::invalid_argument& e) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "header_callback: invalid response code %s\n", input.substr(prefix.length()).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
return bytes_received;
|
||||
}
|
||||
|
||||
/* CURLOPT_OPENSOCKETFUNCTION */
|
||||
static curl_socket_t opensocket(void *clientp, curlsocktype purpose, struct curl_sockaddr *address) {
|
||||
curl_socket_t sockfd = CURL_SOCKET_BAD;
|
||||
|
||||
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "opensocket: %d\n", purpose);
|
||||
/* restrict to IPv4 */
|
||||
if(purpose == CURLSOCKTYPE_IPCXN && address->family == AF_INET) {
|
||||
/* create a tcp socket object */
|
||||
boost::asio::ip::tcp::socket *tcp_socket = new boost::asio::ip::tcp::socket(io_service);
|
||||
|
||||
/* open it and get the native handle*/
|
||||
boost::system::error_code ec;
|
||||
tcp_socket->open(boost::asio::ip::tcp::v4(), ec);
|
||||
|
||||
if(ec) {
|
||||
/* An error occurred */
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Couldn't open socket [%ld][%s]\n", ec, ec.message().c_str());
|
||||
}
|
||||
else {
|
||||
sockfd = tcp_socket->native_handle();
|
||||
|
||||
/* save it for monitoring */
|
||||
socket_map.insert(std::pair<curl_socket_t, boost::asio::ip::tcp::socket *>(sockfd, tcp_socket));
|
||||
}
|
||||
}
|
||||
return sockfd;
|
||||
}
|
||||
|
||||
/* CURLOPT_CLOSESOCKETFUNCTION */
|
||||
static int close_socket(void *clientp, curl_socket_t item) {
|
||||
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "close_socket : %#X\n", item);
|
||||
|
||||
std::map<curl_socket_t, boost::asio::ip::tcp::socket *>::iterator it = socket_map.find(item);
|
||||
if(it != socket_map.end()) {
|
||||
delete it->second;
|
||||
socket_map.erase(it);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
extern "C" {
|
||||
switch_status_t custom_speech_load() {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "custom_speech_loading..\n");
|
||||
memset(&global, 0, sizeof(GlobalInfo_t));
|
||||
global.multi = curl_multi_init();
|
||||
|
||||
if (!global.multi) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "custom_speech_load curl_multi_init() failed, exiting!\n");
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
curl_multi_setopt(global.multi, CURLMOPT_SOCKETFUNCTION, sock_cb);
|
||||
curl_multi_setopt(global.multi, CURLMOPT_SOCKETDATA, &global);
|
||||
curl_multi_setopt(global.multi, CURLMOPT_TIMERFUNCTION, multi_timer_cb);
|
||||
curl_multi_setopt(global.multi, CURLMOPT_TIMERDATA, &global);
|
||||
curl_multi_setopt(global.multi, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
|
||||
|
||||
/* create temp folder for cache files */
|
||||
const char* baseDir = std::getenv("JAMBONZ_TMP_CACHE_FOLDER");
|
||||
if (!baseDir) {
|
||||
baseDir = "/tmp/";
|
||||
}
|
||||
if (strcmp(baseDir, "/") == 0) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to create folder %s\n", baseDir);
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
fullDirPath = std::string(baseDir) + "tts-cache-files";
|
||||
|
||||
// Create the directory with read, write, and execute permissions for everyone
|
||||
mode_t oldMask = umask(0);
|
||||
int result = mkdir(fullDirPath.c_str(), S_IRWXU | S_IRWXG | S_IRWXO);
|
||||
umask(oldMask);
|
||||
if (result != 0) {
|
||||
if (errno != EEXIST) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to create folder %s\n", fullDirPath.c_str());
|
||||
fullDirPath = "";
|
||||
}
|
||||
else switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "folder %s already exists\n", fullDirPath.c_str());
|
||||
}
|
||||
else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "created folder %s\n", fullDirPath.c_str());
|
||||
}
|
||||
// init mgp123
|
||||
if (mpg123_init() != MPG123_OK) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to initiate MPG123");
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
/* start worker thread that handles transfers*/
|
||||
std::thread t(threadFunc) ;
|
||||
worker_thread.swap( t ) ;
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "custom_speech_loaded..\n");
|
||||
|
||||
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
switch_status_t custom_speech_unload() {
|
||||
/* stop the ASIO IO service */
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "custom_speech_unload: stopping io service\n");
|
||||
io_service.stop();
|
||||
|
||||
/* Join the worker thread */
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "custom_speech_unload: wait for worker thread to complete\n");
|
||||
if (worker_thread.joinable()) {
|
||||
worker_thread.join();
|
||||
}
|
||||
|
||||
/* cleanup curl multi handle*/
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "custom_speech_unload: release curl multi\n");
|
||||
curl_multi_cleanup(global.multi);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "custom_speech_unload: completed\n");
|
||||
|
||||
mpg123_exit();
|
||||
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
switch_status_t custom_speech_open(custom_t* custom) {
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
switch_status_t custom_speech_feed_tts(custom_t* c, char* text, switch_speech_flag_t *flags) {
|
||||
CURLMcode rc;
|
||||
|
||||
const int MAX_CHARS = 20;
|
||||
char tempText[MAX_CHARS + 4]; // +4 for the ellipsis and null terminator
|
||||
|
||||
if (strlen(text) > MAX_CHARS) {
|
||||
strncpy(tempText, text, MAX_CHARS);
|
||||
strcpy(tempText + MAX_CHARS, "...");
|
||||
} else {
|
||||
strcpy(tempText, text);
|
||||
}
|
||||
|
||||
/* open cache file */
|
||||
if (c->cache_audio && fullDirPath.length() > 0) {
|
||||
switch_uuid_t uuid;
|
||||
char uuid_str[SWITCH_UUID_FORMATTED_LENGTH + 1];
|
||||
char outfile[512] = "";
|
||||
int fd;
|
||||
|
||||
switch_uuid_get(&uuid);
|
||||
switch_uuid_format(uuid_str, &uuid);
|
||||
|
||||
switch_snprintf(outfile, sizeof(outfile), "%s%s%s.mp3", fullDirPath.c_str(), SWITCH_PATH_SEPARATOR, uuid_str);
|
||||
c->cache_filename = strdup(outfile);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "writing audio cache file to %s\n", c->cache_filename);
|
||||
|
||||
mode_t oldMask = umask(0);
|
||||
fd = open(outfile, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
|
||||
umask(oldMask);
|
||||
if (fd == -1 ) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error opening cache file %s: %s\n", outfile, strerror(errno));
|
||||
}
|
||||
else {
|
||||
c->file = fdopen(fd, "wb");
|
||||
if (!c->file) {
|
||||
close(fd);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error opening cache file %s: %s\n", outfile, strerror(errno));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!c->custom_tts_url) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "custom_speech_feed_tts: no custom_tts_url provided\n");
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
/* format url*/
|
||||
std::string url = c->custom_tts_url;
|
||||
|
||||
/* create the JSON body */
|
||||
cJSON * jResult = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(jResult, "text", text);
|
||||
cJSON_AddStringToObject(jResult, "type", std::strncmp(text, "<speak", 6) == 0 ? "ssml" : "text");
|
||||
if (c->language) {
|
||||
cJSON_AddStringToObject(jResult, "language", c->language);
|
||||
}
|
||||
if (c->voice_name) {
|
||||
cJSON_AddStringToObject(jResult, "voice", c->voice_name);
|
||||
}
|
||||
char *json = cJSON_PrintUnformatted(jResult);
|
||||
|
||||
cJSON_Delete(jResult);
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "custom_speech_feed_tts: [%s] [%s]\n", url.c_str(), tempText);
|
||||
|
||||
ConnInfo_t *conn = pool.malloc() ;
|
||||
|
||||
// COnfigure MPG123
|
||||
int mhError = 0;
|
||||
mpg123_handle *mh = mpg123_new("auto", &mhError);
|
||||
if (!mh) {
|
||||
const char *mhErr = mpg123_plain_strerror(mhError);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error allocating mpg123 handle! %s\n", switch_str_nil(mhErr));
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
if (mpg123_open_feed(mh) != MPG123_OK) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error mpg123_open_feed!\n");
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
if (mpg123_format_all(mh) != MPG123_OK) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error mpg123_format_all!\n");
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
if (mpg123_param(mh, MPG123_FLAGS, MPG123_MONO_MIX, 0) != MPG123_OK) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error forcing single channel!\n");
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
CURL* easy = createEasyHandle();
|
||||
c->conn = (void *) conn ;
|
||||
conn->custom = c;
|
||||
conn->easy = easy;
|
||||
conn->mh = mh;
|
||||
conn->global = &global;
|
||||
conn->hdr_list = NULL ;
|
||||
conn->file = c->file;
|
||||
conn->body = json;
|
||||
conn->flushed = false;
|
||||
|
||||
|
||||
c->circularBuffer = (void *) new CircularBuffer_t(8192);
|
||||
|
||||
if (mpg123_param(mh, MPG123_FORCE_RATE, c->rate /*Hz*/, 0) != MPG123_OK) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error mpg123_param!\n");
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
curl_easy_setopt(easy, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_cb);
|
||||
curl_easy_setopt(easy, CURLOPT_WRITEDATA, conn);
|
||||
curl_easy_setopt(easy, CURLOPT_ERRORBUFFER, conn->error);
|
||||
curl_easy_setopt(easy, CURLOPT_PRIVATE, conn);
|
||||
curl_easy_setopt(easy, CURLOPT_VERBOSE, 0L);
|
||||
curl_easy_setopt(easy, CURLOPT_NOPROGRESS, 1L);
|
||||
curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, header_callback);
|
||||
curl_easy_setopt(easy, CURLOPT_HEADERDATA, conn);
|
||||
|
||||
/* call this function to get a socket */
|
||||
curl_easy_setopt(easy, CURLOPT_OPENSOCKETFUNCTION, opensocket);
|
||||
|
||||
/* call this function to close a socket */
|
||||
curl_easy_setopt(easy, CURLOPT_CLOSESOCKETFUNCTION, close_socket);
|
||||
|
||||
if (c->auth_token) {
|
||||
std::ostringstream api_key_stream;
|
||||
api_key_stream << "Authorization: Bearer " << c->auth_token;
|
||||
conn->hdr_list = curl_slist_append(conn->hdr_list, api_key_stream.str().c_str());
|
||||
}
|
||||
conn->hdr_list = curl_slist_append(conn->hdr_list, "Accept: audio/mpeg");
|
||||
conn->hdr_list = curl_slist_append(conn->hdr_list, "Content-Type: application/json");
|
||||
curl_easy_setopt(easy, CURLOPT_HTTPHEADER, conn->hdr_list);
|
||||
|
||||
curl_easy_setopt(easy, CURLOPT_POSTFIELDS, conn->body);
|
||||
//curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE, body.length());
|
||||
|
||||
curl_easy_setopt(easy, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
|
||||
|
||||
rc = curl_multi_add_handle(global.multi, conn->easy);
|
||||
mcode_test("new_conn: curl_multi_add_handle", rc);
|
||||
|
||||
/* start a timer to measure the duration until we receive first byte of audio */
|
||||
conn->startTime = std::chrono::high_resolution_clock::now();
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "custom_speech_feed_tts: called curl_multi_add_handle\n");
|
||||
|
||||
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
switch_status_t custom_speech_read_tts(custom_t* c, void *data, size_t *datalen, switch_speech_flag_t *flags) {
|
||||
CircularBuffer_t *cBuffer = (CircularBuffer_t *) c->circularBuffer;
|
||||
std::vector<uint16_t> pcm_data;
|
||||
|
||||
{
|
||||
switch_mutex_lock(c->mutex);
|
||||
ConnInfo_t *conn = (ConnInfo_t *) c->conn;
|
||||
if (c->response_code > 0 && c->response_code != 200) {
|
||||
switch_mutex_unlock(c->mutex);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "custom_speech_read_tts, returning failure\n") ;
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
if (conn && conn->flushed) {
|
||||
switch_mutex_unlock(c->mutex);
|
||||
return SWITCH_STATUS_BREAK;
|
||||
}
|
||||
if (cBuffer->empty()) {
|
||||
if (c->draining) {
|
||||
switch_mutex_unlock(c->mutex);
|
||||
return SWITCH_STATUS_BREAK;
|
||||
}
|
||||
/* no audio available yet so send silence */
|
||||
memset(data, 255, *datalen);
|
||||
switch_mutex_unlock(c->mutex);
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
size_t size = std::min((*datalen/2), cBuffer->size());
|
||||
pcm_data.insert(pcm_data.end(), cBuffer->begin(), cBuffer->begin() + size);
|
||||
cBuffer->erase(cBuffer->begin(), cBuffer->begin() + size);
|
||||
switch_mutex_unlock(c->mutex);
|
||||
}
|
||||
|
||||
memcpy(data, pcm_data.data(), pcm_data.size() * sizeof(uint16_t));
|
||||
*datalen = pcm_data.size() * sizeof(uint16_t);
|
||||
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
switch_status_t custom_speech_flush_tts(custom_t* c) {
|
||||
bool download_complete = c->response_code == 200;
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "custom_speech_flush_tts, download complete? %s\n", download_complete ? "yes" : "no") ;
|
||||
ConnInfo_t *conn = (ConnInfo_t *) c->conn;
|
||||
CircularBuffer_t *cBuffer = (CircularBuffer_t *) c->circularBuffer;
|
||||
delete cBuffer;
|
||||
c->circularBuffer = nullptr ;
|
||||
|
||||
if (conn) {
|
||||
conn->flushed = true;
|
||||
if (!download_complete) {
|
||||
if (conn->file) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "closing audio cache file %s because download was interrupted\n", c->cache_filename);
|
||||
if (fclose(conn->file) != 0) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "error closing audio cache file\n");
|
||||
}
|
||||
conn->file = nullptr ;
|
||||
}
|
||||
|
||||
if (c->cache_filename) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "removing audio cache file %s because download was interrupted\n", c->cache_filename);
|
||||
if (unlink(c->cache_filename) != 0) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "cleanupConn: error removing audio cache file %s: %d:%s\n",
|
||||
c->cache_filename, errno, strerror(errno));
|
||||
}
|
||||
free(c->cache_filename);
|
||||
c->cache_filename = nullptr ;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (c->session_id) {
|
||||
switch_core_session_t* session = switch_core_session_locate(c->session_id);
|
||||
if (session) {
|
||||
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||
switch_core_session_rwunlock(session);
|
||||
if (channel) {
|
||||
switch_event_t *event;
|
||||
if (switch_event_create(&event, SWITCH_EVENT_PLAYBACK_STOP) == SWITCH_STATUS_SUCCESS) {
|
||||
switch_channel_event_set_data(channel, event);
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "Playback-File-Type", "tts_stream");
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_custom_response_code", std::to_string(c->response_code).c_str());
|
||||
if (c->cache_filename && c->response_code == 200) {
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_cache_filename", c->cache_filename);
|
||||
}
|
||||
if (c->response_code != 200 && c->err_msg) {
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_error", c->err_msg);
|
||||
}
|
||||
switch_event_fire(&event);
|
||||
}
|
||||
else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: failed to create event\n");
|
||||
}
|
||||
}
|
||||
else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: channel not found\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
switch_status_t custom_speech_close(custom_t* c) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "custom_speech_close\n") ;
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#ifndef __CUSTOM_GLUE_H__
|
||||
#define __CUSTOM_GLUE_H__
|
||||
|
||||
switch_status_t custom_speech_load();
|
||||
switch_status_t custom_speech_open(custom_t* custom);
|
||||
switch_status_t custom_speech_feed_tts(custom_t* custom, char* text, switch_speech_flag_t *flags);
|
||||
switch_status_t custom_speech_read_tts(custom_t* custom, void *data, size_t *datalen, switch_speech_flag_t *flags);
|
||||
switch_status_t custom_speech_flush_tts(custom_t* custom);
|
||||
switch_status_t custom_speech_close(custom_t* custom);
|
||||
switch_status_t custom_speech_unload();
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,158 @@
|
||||
#include "mod_custom_tts.h"
|
||||
#include "custom_glue.h"
|
||||
|
||||
SWITCH_MODULE_LOAD_FUNCTION(mod_custom_tts_load);
|
||||
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_custom_tts_shutdown);
|
||||
SWITCH_MODULE_DEFINITION(mod_custom_tts, mod_custom_tts_load, mod_custom_tts_shutdown, NULL);
|
||||
|
||||
static void clearCustomVendor(custom_t* c, int freeAll) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "clearCustomVendor\n");
|
||||
if (c->auth_token) free(c->auth_token);
|
||||
if (c->custom_tts_url) free(c->custom_tts_url);
|
||||
if (c->language) free(c->language);
|
||||
if (c->ct) free(c->ct);
|
||||
if (c->err_msg) free(c->err_msg);
|
||||
if (c->name_lookup_time_ms) free(c->name_lookup_time_ms);
|
||||
if (c->connect_time_ms) free(c->connect_time_ms);
|
||||
if (c->final_response_time_ms) free(c->final_response_time_ms);
|
||||
if (c->cache_filename) free(c->cache_filename);
|
||||
|
||||
|
||||
c->auth_token = NULL;
|
||||
c->custom_tts_url = NULL;
|
||||
c->language = NULL;
|
||||
c->ct = NULL;
|
||||
c->err_msg = NULL;
|
||||
c->name_lookup_time_ms = NULL;
|
||||
c->connect_time_ms = NULL;
|
||||
c->final_response_time_ms = NULL;
|
||||
c->cache_filename = NULL;
|
||||
|
||||
if (freeAll) {
|
||||
if (c->voice_name) free(c->voice_name);
|
||||
if (c->session_id) free(c->session_id);
|
||||
c->voice_name = NULL;
|
||||
c->session_id = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static custom_t * createOrRetrievePrivateData(switch_speech_handle_t *sh) {
|
||||
custom_t *c = (custom_t *) sh->private_info;
|
||||
if (!c) {
|
||||
c = switch_core_alloc(sh->memory_pool, sizeof(*c));
|
||||
sh->private_info = c;
|
||||
memset(c, 0, sizeof(*c));
|
||||
switch_mutex_init(&c->mutex, SWITCH_MUTEX_NESTED, sh->memory_pool);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "allocated custom_t\n");
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
switch_status_t w_speech_open(switch_speech_handle_t *sh, const char *voice_name, int rate, int channels, switch_speech_flag_t *flags)
|
||||
{
|
||||
custom_t *c = createOrRetrievePrivateData(sh);
|
||||
c->voice_name = strdup(voice_name);
|
||||
c->rate = rate;
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "w_speech_open voice: %s, rate %d, channels %d\n", voice_name, rate, channels);
|
||||
return custom_speech_open(c);
|
||||
}
|
||||
|
||||
static switch_status_t w_speech_close(switch_speech_handle_t *sh, switch_speech_flag_t *flags)
|
||||
{
|
||||
switch_status_t rc;
|
||||
custom_t *c = createOrRetrievePrivateData(sh);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "w_speech_close\n");
|
||||
|
||||
switch_mutex_destroy(c->mutex);
|
||||
|
||||
rc = custom_speech_close(c);
|
||||
clearCustomVendor(c, 1);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Freeswitch will call this function to feed us text to speak
|
||||
*/
|
||||
static switch_status_t w_speech_feed_tts(switch_speech_handle_t *sh, char *text, switch_speech_flag_t *flags)
|
||||
{
|
||||
custom_t *c = createOrRetrievePrivateData(sh);
|
||||
c->draining = 0;
|
||||
c->reads = 0;
|
||||
c->response_code = 0;
|
||||
c->err_msg = NULL;
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "w_speech_feed_tts\n");
|
||||
|
||||
return custom_speech_feed_tts(c, text, flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Freeswitch calls periodically to get some rendered audio in L16 format. We can provide up to 8k of audio at a time.
|
||||
*/
|
||||
static switch_status_t w_speech_read_tts(switch_speech_handle_t *sh, void *data, size_t *datalen, switch_speech_flag_t *flags)
|
||||
{
|
||||
custom_t *c = createOrRetrievePrivateData(sh);
|
||||
return custom_speech_read_tts(c, data, datalen, flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called at the end, not sure exactly what we need to do here..
|
||||
*/
|
||||
static void w_speech_flush_tts(switch_speech_handle_t *sh)
|
||||
{
|
||||
custom_t *c = createOrRetrievePrivateData(sh);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "w_speech_flush_tts\n");
|
||||
custom_speech_flush_tts(c);
|
||||
|
||||
clearCustomVendor(c, 0);
|
||||
}
|
||||
|
||||
static void w_text_param_tts(switch_speech_handle_t *sh, char *param, const char *val)
|
||||
{
|
||||
custom_t *c = createOrRetrievePrivateData(sh);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "w_text_param_tts: %s=%s\n", param, val);
|
||||
if (0 == strcmp(param, "auth_token")) {
|
||||
if (c->auth_token) free(c->auth_token);
|
||||
c->auth_token = strdup(val);
|
||||
} else if (0 == strcmp(param, "custom_tts_url")) {
|
||||
if (c->custom_tts_url) free(c->custom_tts_url);
|
||||
c->custom_tts_url = strdup(val);
|
||||
} else if (0 == strcmp(param, "language")) {
|
||||
if (c->language) free(c->language);
|
||||
c->language = strdup(val);
|
||||
} else if (0 == strcmp(param, "session-uuid")) {
|
||||
if (c->session_id) free(c->session_id);
|
||||
c->session_id = strdup(val);
|
||||
} else if (0 == strcmp(param, "write_cache_file") && switch_true(val)) {
|
||||
c->cache_audio = 1;
|
||||
}
|
||||
}
|
||||
static void w_numeric_param_tts(switch_speech_handle_t *sh, char *param, int val)
|
||||
{
|
||||
}
|
||||
static void w_float_param_tts(switch_speech_handle_t *sh, char *param, double val)
|
||||
{
|
||||
}
|
||||
|
||||
SWITCH_MODULE_LOAD_FUNCTION(mod_custom_tts_load)
|
||||
{
|
||||
switch_speech_interface_t *speech_interface;
|
||||
|
||||
*module_interface = switch_loadable_module_create_module_interface(pool, modname);
|
||||
speech_interface = switch_loadable_module_create_interface(*module_interface, SWITCH_SPEECH_INTERFACE);
|
||||
speech_interface->interface_name = "custom";
|
||||
speech_interface->speech_open = w_speech_open;
|
||||
speech_interface->speech_close = w_speech_close;
|
||||
speech_interface->speech_feed_tts = w_speech_feed_tts;
|
||||
speech_interface->speech_read_tts = w_speech_read_tts;
|
||||
speech_interface->speech_flush_tts = w_speech_flush_tts;
|
||||
speech_interface->speech_text_param_tts = w_text_param_tts;
|
||||
speech_interface->speech_numeric_param_tts = w_numeric_param_tts;
|
||||
speech_interface->speech_float_param_tts = w_float_param_tts;
|
||||
return custom_speech_load();
|
||||
}
|
||||
|
||||
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_custom_tts_shutdown)
|
||||
{
|
||||
return custom_speech_unload();
|
||||
}
|
||||
@@ -1,34 +1,31 @@
|
||||
#ifndef __MOD_GOOGLE_TTS_H__
|
||||
#define __MOD_GOOGLE_TTS_H__
|
||||
#ifndef __MOD_CUSTOM_TTS_H__
|
||||
#define __MOD_CUSTOM_TTS_H__
|
||||
|
||||
#include <switch.h>
|
||||
#include <speex/speex_resampler.h>
|
||||
|
||||
typedef struct google_data {
|
||||
typedef struct custom_data {
|
||||
char *voice_name;
|
||||
char *model;
|
||||
char *reported_usage;
|
||||
char *auth_token;
|
||||
char *custom_tts_url;
|
||||
char *language;
|
||||
char *gender;
|
||||
|
||||
/* result data */
|
||||
long response_code;
|
||||
char *session_id;
|
||||
char *cache_filename;
|
||||
char *ct;
|
||||
char *name_lookup_time_ms;
|
||||
char *connect_time_ms;
|
||||
char *final_response_time_ms;
|
||||
char *err_msg;
|
||||
char *cache_filename;
|
||||
char *session_id;
|
||||
|
||||
int rate;
|
||||
int draining;
|
||||
int reads;
|
||||
int cache_audio;
|
||||
int flushed;
|
||||
|
||||
void *startTime;
|
||||
|
||||
FILE *file;
|
||||
SpeexResamplerState *resampler;
|
||||
void *conn;
|
||||
void *circularBuffer;
|
||||
switch_mutex_t *mutex;
|
||||
} google_t;
|
||||
|
||||
FILE *file;
|
||||
} custom_t;
|
||||
#endif
|
||||
@@ -385,7 +385,7 @@ static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
|
||||
auto d = conn->deepgram;
|
||||
CircularBuffer_t *cBuffer = (CircularBuffer_t *) d->circularBuffer;
|
||||
|
||||
if (conn->flushed) {
|
||||
if (conn->flushed || cBuffer == nullptr) {
|
||||
/* this will abort the transfer */
|
||||
return 0;
|
||||
}
|
||||
@@ -641,7 +641,7 @@ extern "C" {
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
fullDirPath = std::string(baseDir) + "jambonz-tts-cache-files";
|
||||
fullDirPath = std::string(baseDir) + "tts-cache-files";
|
||||
|
||||
// Create the directory with read, write, and execute permissions for everyone
|
||||
mode_t oldMask = umask(0);
|
||||
@@ -764,7 +764,9 @@ extern "C" {
|
||||
conn->file = d->file;
|
||||
conn->body = json;
|
||||
conn->flushed = false;
|
||||
|
||||
conn->has_last_byte = false;
|
||||
conn->last_byte = 0;
|
||||
|
||||
|
||||
d->circularBuffer = (void *) new CircularBuffer_t(BUFFER_GROW_SIZE);
|
||||
// Always use deepgram at rate 8000 for helping cache audio from jambonz.
|
||||
|
||||
@@ -4,6 +4,60 @@
|
||||
#include <switch_json.h>
|
||||
#include <map>
|
||||
|
||||
|
||||
switch_status_t custom_vendor_parse_text(const std::map<std::string, std::string>& params, const std::string& text,
|
||||
std::string& url, std::string& body, std::vector<std::string>& headers) {
|
||||
std::string auth_token;
|
||||
std::string voice_name;
|
||||
std::string custom_tts_url;
|
||||
std::string language;
|
||||
|
||||
for (const auto& pair : params) {
|
||||
if (pair.first == "auth_token") {
|
||||
auth_token = pair.second;
|
||||
} else if (pair.first == "voice") {
|
||||
voice_name = pair.second;
|
||||
} else if (pair.first == "custom_tts_url") {
|
||||
custom_tts_url = pair.second;
|
||||
} else if (pair.first == "language") {
|
||||
language = pair.second;
|
||||
}
|
||||
}
|
||||
|
||||
if (custom_tts_url.empty()) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "custom_vendor_parse_text: no custom_tts_url provided\n");
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
url = custom_tts_url;
|
||||
|
||||
/* create the JSON body */
|
||||
cJSON * jResult = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(jResult, "text", text.c_str());
|
||||
cJSON_AddStringToObject(jResult, "type", text.substr(0, 6) == "<speak" ? "ssml" : "text");
|
||||
cJSON_AddNumberToObject(jResult, "samplingRate", 8000);
|
||||
if (!voice_name.empty()) {
|
||||
cJSON_AddStringToObject(jResult, "voice", voice_name.c_str());
|
||||
}
|
||||
if (!language.empty()) {
|
||||
cJSON_AddStringToObject(jResult, "language", language.c_str());
|
||||
}
|
||||
char* _body = cJSON_PrintUnformatted(jResult);
|
||||
body = _body;
|
||||
|
||||
cJSON_Delete(jResult);
|
||||
free(_body);
|
||||
|
||||
// Create headers
|
||||
if (!auth_token.empty()) {
|
||||
headers.push_back("Authorization: Bearer " + auth_token);
|
||||
}
|
||||
headers.push_back("Accept: audio/mp3");
|
||||
headers.push_back("Content-Type: application/json");
|
||||
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
switch_status_t rimelabs_parse_text(const std::map<std::string, std::string>& params, const std::string& text,
|
||||
std::string& url, std::string& body, std::vector<std::string>& headers) {
|
||||
std::string api_key;
|
||||
@@ -476,7 +530,9 @@ switch_status_t tts_vendor_parse_text(const std::string& say, std::string& url,
|
||||
return playht_parse_text(params, text, url, body, headers);
|
||||
} else if (params["vendor"] == "rimelabs") {
|
||||
return rimelabs_parse_text(params, text, url, body, headers);
|
||||
} else {
|
||||
} else if (params["vendor"] == "custom") {
|
||||
return custom_vendor_parse_text(params, text, url, body, headers);
|
||||
} else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "tts_vendor_parse_text: There is no available parser for vendor %s\n", params["vendor"]);
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
@@ -445,7 +445,7 @@ static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
|
||||
CircularBuffer_t *cBuffer = (CircularBuffer_t *) el->circularBuffer;
|
||||
std::vector<uint16_t> pcm_data;
|
||||
|
||||
if (conn->flushed) {
|
||||
if (conn->flushed || cBuffer == nullptr) {
|
||||
/* this will abort the transfer */
|
||||
return 0;
|
||||
}
|
||||
@@ -688,7 +688,7 @@ extern "C" {
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
fullDirPath = std::string(baseDir) + "jambonz-tts-cache-files";
|
||||
fullDirPath = std::string(baseDir) + "tts-cache-files";
|
||||
|
||||
// Create the directory with read, write, and execute permissions for everyone
|
||||
mode_t oldMask = umask(0);
|
||||
@@ -841,7 +841,6 @@ extern "C" {
|
||||
CURL* easy = createEasyHandle();
|
||||
|
||||
el->conn = (void *) conn ;
|
||||
el->sample_rate = 0;
|
||||
conn->elevenlabs = el;
|
||||
conn->easy = easy;
|
||||
conn->global = &global;
|
||||
@@ -852,20 +851,12 @@ extern "C" {
|
||||
|
||||
el->circularBuffer = (void *) new CircularBuffer_t(8192);
|
||||
|
||||
if (el->session_id) {
|
||||
if (el->rate != 8000 /*Hz*/) {
|
||||
int err;
|
||||
switch_codec_implementation_t read_impl;
|
||||
switch_core_session_t *psession = switch_core_session_locate(el->session_id);
|
||||
switch_core_session_get_read_impl(psession, &read_impl);
|
||||
uint32_t samples_per_second = !strcasecmp(read_impl.iananame, "g722") ? read_impl.actual_samples_per_second : read_impl.samples_per_second;
|
||||
el->sample_rate = samples_per_second;
|
||||
// elevenlabs output is PCMU 8000
|
||||
if (samples_per_second != 8000 /*Hz*/) {
|
||||
el->resampler = speex_resampler_init(1, 8000, samples_per_second, SWITCH_RESAMPLE_QUALITY, &err);
|
||||
if (0 != err) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error initializing resampler: %s.\n", speex_resampler_strerror(err));
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
el->resampler = speex_resampler_init(1, 8000, el->rate, SWITCH_RESAMPLE_QUALITY, &err);
|
||||
if (0 != err) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error initializing resampler: %s.\n", speex_resampler_strerror(err));
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -939,9 +930,7 @@ extern "C" {
|
||||
switch_mutex_unlock(el->mutex);
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
size_t size = el->sample_rate ?
|
||||
std::min((*datalen/(2 * el->sample_rate / 8000)), cBuffer->size()) :
|
||||
std::min((*datalen/2), cBuffer->size());
|
||||
size_t size = std::min((*datalen/(2 * el->rate / 8000)), cBuffer->size());
|
||||
pcm_data.insert(pcm_data.end(), cBuffer->begin(), cBuffer->begin() + size);
|
||||
cBuffer->erase(cBuffer->begin(), cBuffer->begin() + size);
|
||||
switch_mutex_unlock(el->mutex);
|
||||
|
||||
@@ -31,7 +31,6 @@ struct elevenlabs_data {
|
||||
char *cache_filename;
|
||||
|
||||
int rate;
|
||||
uint32_t sample_rate;
|
||||
|
||||
void *conn;
|
||||
FILE *file;
|
||||
|
||||
@@ -236,6 +236,7 @@ static void *SWITCH_THREAD_FUNC grpc_read_thread(switch_thread_t *thread, void *
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "grpc_read_thread: error %s (%d)\n", status.message().c_str(), status.code()) ;
|
||||
cJSON* json = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(json, "type", "error");
|
||||
cJSON_AddStringToObject(json, "error_cause", "stream_response");
|
||||
cJSON_AddStringToObject(json, "error", status.message().c_str());
|
||||
char* jsonString = cJSON_PrintUnformatted(json);
|
||||
cb->responseHandler(session, jsonString, cb->bugname);
|
||||
@@ -339,9 +340,10 @@ static void *SWITCH_THREAD_FUNC grpc_read_thread(switch_thread_t *thread, void *
|
||||
cb->responseHandler(session, "no_audio", cb->bugname);
|
||||
}
|
||||
}
|
||||
else {
|
||||
else if (status.error_code() != 0) {
|
||||
cJSON* json = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(json, "type", "error");
|
||||
cJSON_AddStringToObject(json, "error_cause", "stream_close");
|
||||
cJSON_AddItemToObject(json, "error_code", cJSON_CreateNumber(status.error_code()));
|
||||
cJSON_AddStringToObject(json, "error_message", status.error_message().c_str());
|
||||
char* jsonString = cJSON_PrintUnformatted(json);
|
||||
|
||||
@@ -349,6 +349,7 @@ static void *SWITCH_THREAD_FUNC grpc_read_thread(switch_thread_t *thread, void *
|
||||
else if (status.error_code() != 0) {
|
||||
cJSON* json = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(json, "type", "error");
|
||||
cJSON_AddStringToObject(json, "error_cause", "stream_close");
|
||||
cJSON_AddItemToObject(json, "error_code", cJSON_CreateNumber(status.error_code()));
|
||||
cJSON_AddStringToObject(json, "error_message", status.error_message().c_str());
|
||||
char* jsonString = cJSON_PrintUnformatted(json);
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
include $(top_srcdir)/build/modmake.rulesam
|
||||
MODNAME=mod_google_tts
|
||||
|
||||
mod_LTLIBRARIES = mod_google_tts.la
|
||||
mod_google_tts_la_SOURCES = mod_google_tts.c google_glue.cpp
|
||||
mod_google_tts_la_CFLAGS = $(AM_CFLAGS)
|
||||
mod_google_tts_la_CXXFLAGS = -I $(top_srcdir)/libs/googleapis/gens $(AM_CXXFLAGS) -std=c++17
|
||||
mod_google_tts_la_LIBADD = $(switch_builddir)/libfreeswitch.la
|
||||
mod_google_tts_la_LDFLAGS = -avoid-version -module -no-undefined -shared `pkg-config --libs grpc++ grpc`
|
||||
@@ -1,405 +0,0 @@
|
||||
#include "mod_google_tts.h"
|
||||
#include <switch.h>
|
||||
|
||||
#include <boost/circular_buffer.hpp>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#include "google/cloud/texttospeech/v1/cloud_tts.grpc.pb.h"
|
||||
#include <grpc++/grpc++.h>
|
||||
|
||||
using google::cloud::texttospeech::v1::SynthesizeSpeechRequest;
|
||||
using google::cloud::texttospeech::v1::SynthesizeSpeechResponse;
|
||||
using google::cloud::texttospeech::v1::TextToSpeech;
|
||||
using google::cloud::texttospeech::v1::Voice;
|
||||
using google::cloud::texttospeech::v1::SsmlVoiceGender;
|
||||
using google::cloud::texttospeech::v1::SsmlVoiceGender_Name;
|
||||
using google::cloud::texttospeech::v1::SynthesisInput;
|
||||
using google::cloud::texttospeech::v1::AudioEncoding;
|
||||
|
||||
#define BUFFER_SIZE 8129
|
||||
|
||||
typedef boost::circular_buffer<uint16_t> CircularBuffer_t;
|
||||
|
||||
static std::string fullDirPath;
|
||||
|
||||
static std::shared_ptr<grpc::Channel> create_grpc_channel(switch_channel_t *channel) {
|
||||
const char* google_uri = "texttospeech.googleapis.com";
|
||||
const char* var;
|
||||
if (var = switch_channel_get_variable(channel, "GOOGLE_TTS_APPLICATION_CREDENTIALS")) {
|
||||
auto channelCreds = grpc::SslCredentials(grpc::SslCredentialsOptions());
|
||||
auto callCreds = grpc::ServiceAccountJWTAccessCredentials(var);
|
||||
auto creds = grpc::CompositeChannelCredentials(channelCreds, callCreds);
|
||||
return grpc::CreateChannel(google_uri, creds);
|
||||
}
|
||||
else {
|
||||
auto creds = grpc::GoogleDefaultCredentials();
|
||||
return grpc::CreateChannel(google_uri, creds);
|
||||
}
|
||||
}
|
||||
|
||||
static void start_synthesis(const char* text, google_t* g) {
|
||||
try {
|
||||
SynthesizeSpeechRequest request;
|
||||
SynthesizeSpeechResponse response;
|
||||
grpc::ClientContext context;
|
||||
auto input = request.mutable_input();
|
||||
auto voice = request.mutable_voice();
|
||||
auto custom_voice = voice->mutable_custom_voice();
|
||||
auto audio_config = request.mutable_audio_config();
|
||||
/* lock and unlock session */
|
||||
switch_core_session_t *psession = switch_core_session_locate(g->session_id);
|
||||
switch_channel_t *swChannel = switch_core_session_get_channel(psession);
|
||||
switch_core_session_rwunlock(psession);
|
||||
auto channel = create_grpc_channel(swChannel);
|
||||
auto stub = TextToSpeech::NewStub(channel);
|
||||
|
||||
if (strstr(text, "<speak") == text) {
|
||||
input->set_ssml(text);
|
||||
}
|
||||
else {
|
||||
input->set_text(text);
|
||||
}
|
||||
if (g->gender) {
|
||||
if (strcmp(g->gender, "MALE") == 0) {
|
||||
voice->set_ssml_gender(google::cloud::texttospeech::v1::SsmlVoiceGender::MALE);
|
||||
} else if (strcmp(g->gender, "FEMALE") == 0) {
|
||||
voice->set_ssml_gender(google::cloud::texttospeech::v1::SsmlVoiceGender::FEMALE);
|
||||
} else if (strcmp(g->gender, "NEUTRAL") == 0) {
|
||||
voice->set_ssml_gender(google::cloud::texttospeech::v1::SsmlVoiceGender::NEUTRAL);
|
||||
} else {
|
||||
voice->set_ssml_gender(google::cloud::texttospeech::v1::SsmlVoiceGender::SSML_VOICE_GENDER_UNSPECIFIED);
|
||||
}
|
||||
}
|
||||
|
||||
if (g->model) {
|
||||
custom_voice->set_model(g->model);
|
||||
if (strcmp(g->reported_usage, "OFFLINE") == 0) {
|
||||
custom_voice->set_reported_usage(google::cloud::texttospeech::v1::CustomVoiceParams_ReportedUsage::CustomVoiceParams_ReportedUsage_OFFLINE);
|
||||
} else if (strcmp(g->reported_usage, "REALTIME") == 0) {
|
||||
custom_voice->set_reported_usage(google::cloud::texttospeech::v1::CustomVoiceParams_ReportedUsage::CustomVoiceParams_ReportedUsage_REALTIME);
|
||||
} else {
|
||||
custom_voice->set_reported_usage(google::cloud::texttospeech::v1::CustomVoiceParams_ReportedUsage::CustomVoiceParams_ReportedUsage_REPORTED_USAGE_UNSPECIFIED);
|
||||
}
|
||||
} else {
|
||||
voice->set_name(g->voice_name);
|
||||
}
|
||||
voice->set_language_code(g->language);
|
||||
audio_config->set_audio_encoding(AudioEncoding::LINEAR16);
|
||||
audio_config->set_sample_rate_hertz(8000);
|
||||
grpc::Status status = stub->SynthesizeSpeech(&context, request, &response);
|
||||
if (!status.ok()) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR,
|
||||
"start_synthesis: error synthesizing speech: %s: details: %s\n",
|
||||
status.error_message().c_str(), status.error_details().c_str());
|
||||
return;
|
||||
}
|
||||
g->response_code = 200;
|
||||
auto audioData = response.audio_content();
|
||||
if (g->flushed) return;
|
||||
bool fireEvent = false;
|
||||
CircularBuffer_t *cBuffer = (CircularBuffer_t *) g->circularBuffer;
|
||||
if (g->file) {
|
||||
// google return linear16 wav data that contains 44 bytes wav header, let remove them
|
||||
fwrite(audioData.data() + 44, 1, audioData.size() - 44, g->file);
|
||||
}
|
||||
|
||||
/**
|
||||
* this sort of reinterpretation can be dangerous as a general rule, but in this case we know that the data
|
||||
* is 16-bit PCM, so it's safe to do this and its much faster than copying the data byte by byte
|
||||
*/
|
||||
// google return linear16 wav data that contains 44 bytes wav header, let remove them
|
||||
const uint16_t* begin = reinterpret_cast<const uint16_t*>(audioData.data() + 44);
|
||||
const uint16_t* end = reinterpret_cast<const uint16_t*>(audioData.data() + audioData.size());
|
||||
|
||||
/* lock as briefly as possible */
|
||||
switch_mutex_lock(g->mutex);
|
||||
if (cBuffer->capacity() - cBuffer->size() < audioData.size()) {
|
||||
cBuffer->set_capacity(cBuffer->size() + std::max( audioData.size(), (size_t)BUFFER_SIZE));
|
||||
}
|
||||
cBuffer->insert(cBuffer->end(), begin, end);
|
||||
switch_mutex_unlock(g->mutex);
|
||||
|
||||
if (0 == g->reads++) {
|
||||
fireEvent = true;
|
||||
}
|
||||
|
||||
if (fireEvent && g->session_id) {
|
||||
auto endTime = std::chrono::high_resolution_clock::now();
|
||||
auto startTime = *static_cast<std::chrono::time_point<std::chrono::high_resolution_clock>*>(g->startTime);
|
||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);
|
||||
auto time_to_first_byte_ms = std::to_string(duration.count());
|
||||
switch_core_session_t* session = switch_core_session_locate(g->session_id);
|
||||
if (session) {
|
||||
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||
switch_core_session_rwunlock(session);
|
||||
if (channel) {
|
||||
switch_event_t *event;
|
||||
if (switch_event_create(&event, SWITCH_EVENT_PLAYBACK_START) == SWITCH_STATUS_SUCCESS) {
|
||||
switch_channel_event_set_data(channel, event);
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "Playback-File-Type", "tts_stream");
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_time_to_first_byte_ms", time_to_first_byte_ms.c_str());
|
||||
if (g->cache_filename) {
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_cache_filename", g->cache_filename);
|
||||
}
|
||||
switch_event_fire(&event);
|
||||
} else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "mod_google_tts start_synthesis: failed to create event\n");
|
||||
}
|
||||
}else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "mod_google_tts start_synthesis: channel not found\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
g->response_code = 500;
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "mod_google_tts: Exception in start_synthesis %s\n", e.what());
|
||||
}
|
||||
g->draining = 1;
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
switch_status_t google_speech_load() {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "google_speech_loading..\n");
|
||||
|
||||
/* create temp folder for cache files */
|
||||
const char* baseDir = std::getenv("JAMBONZ_TMP_CACHE_FOLDER");
|
||||
if (!baseDir) {
|
||||
baseDir = "/tmp/";
|
||||
}
|
||||
if (strcmp(baseDir, "/") == 0) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to create folder %s\n", baseDir);
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
fullDirPath = std::string(baseDir) + "jambonz-tts-cache-files";
|
||||
|
||||
// Create the directory with read, write, and execute permissions for everyone
|
||||
mode_t oldMask = umask(0);
|
||||
int result = mkdir(fullDirPath.c_str(), S_IRWXU | S_IRWXG | S_IRWXO);
|
||||
umask(oldMask);
|
||||
if (result != 0) {
|
||||
if (errno != EEXIST) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to create folder %s\n", fullDirPath.c_str());
|
||||
fullDirPath = "";
|
||||
}
|
||||
else switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "folder %s already exists\n", fullDirPath.c_str());
|
||||
}
|
||||
else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "created folder %s\n", fullDirPath.c_str());
|
||||
}
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "google_speech_loaded..\n");
|
||||
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
switch_status_t google_speech_open(google_t* google) {
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
switch_status_t google_speech_feed_tts(google_t* g, char* text, switch_speech_flag_t *flags) {
|
||||
const int MAX_CHARS = 20;
|
||||
char tempText[MAX_CHARS + 4]; // +4 for the ellipsis and null terminator
|
||||
|
||||
if (strlen(text) > MAX_CHARS) {
|
||||
strncpy(tempText, text, MAX_CHARS);
|
||||
strcpy(tempText + MAX_CHARS, "...");
|
||||
} else {
|
||||
strcpy(tempText, text);
|
||||
}
|
||||
|
||||
/* open cache file */
|
||||
if (g->cache_audio && fullDirPath.length() > 0) {
|
||||
switch_uuid_t uuid;
|
||||
char uuid_str[SWITCH_UUID_FORMATTED_LENGTH + 1];
|
||||
char outfile[512] = "";
|
||||
int fd;
|
||||
|
||||
switch_uuid_get(&uuid);
|
||||
switch_uuid_format(uuid_str, &uuid);
|
||||
|
||||
switch_snprintf(outfile, sizeof(outfile), "%s%s%s.r8", fullDirPath.c_str(), SWITCH_PATH_SEPARATOR, uuid_str);
|
||||
g->cache_filename = strdup(outfile);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "writing audio cache file to %s\n", g->cache_filename);
|
||||
|
||||
mode_t oldMask = umask(0);
|
||||
fd = open(outfile, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
|
||||
umask(oldMask);
|
||||
if (fd == -1 ) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error opening cache file %s: %s\n", outfile, strerror(errno));
|
||||
}
|
||||
else {
|
||||
g->file = fdopen(fd, "wb");
|
||||
if (!g->file) {
|
||||
close(fd);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error opening cache file %s: %s\n", outfile, strerror(errno));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!g->language) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "google_speech_feed_tts: no language provided\n");
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
if (!g->voice_name && !g->model) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "google_speech_feed_tts: no voice_name or model provided\n");
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
if (g->rate != 8000 /*Hz*/) {
|
||||
int err;
|
||||
g->resampler = speex_resampler_init(1, 8000, g->rate, SWITCH_RESAMPLE_QUALITY, &err);
|
||||
if (0 != err) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error initializing resampler: %s.\n", speex_resampler_strerror(err));
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
std::chrono::time_point<std::chrono::high_resolution_clock>* ptr = new std::chrono::time_point<std::chrono::high_resolution_clock>(std::chrono::high_resolution_clock::now());
|
||||
g->startTime = ptr;
|
||||
|
||||
g->circularBuffer = (void *) new CircularBuffer_t(BUFFER_SIZE);
|
||||
|
||||
std::thread(start_synthesis, text, g).detach();
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "google_speech_feed_tts sent synthesize request\n");
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
switch_status_t google_speech_read_tts(google_t* g, void *data, size_t *datalen, switch_speech_flag_t *flags) {
|
||||
CircularBuffer_t *cBuffer = (CircularBuffer_t *) g->circularBuffer;
|
||||
std::vector<uint16_t> pcm_data;
|
||||
|
||||
if (g->response_code > 0 && g->response_code != 200) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "google_speech_read_tts, returning failure\n") ;
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
if (g->flushed) {
|
||||
return SWITCH_STATUS_BREAK;
|
||||
}
|
||||
switch_mutex_lock(g->mutex);
|
||||
size_t bufSize = cBuffer->size();
|
||||
if (cBuffer->empty()) {
|
||||
switch_mutex_unlock(g->mutex);
|
||||
if (g->draining) {
|
||||
return SWITCH_STATUS_BREAK;
|
||||
}
|
||||
/* no audio available yet so send silence */
|
||||
memset(data, 255, *datalen);
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
// google returned 8000hz 16 bit data, we have to take enough data based on call sample rate.
|
||||
size_t size = std::min((*datalen/(2 * g->rate / 8000)), bufSize);
|
||||
pcm_data.insert(pcm_data.end(), cBuffer->begin(), cBuffer->begin() + size);
|
||||
cBuffer->erase(cBuffer->begin(), cBuffer->begin() + size);
|
||||
switch_mutex_unlock(g->mutex);
|
||||
|
||||
size_t data_size = pcm_data.size();
|
||||
|
||||
if (g->resampler) {
|
||||
std::vector<int16_t> in(pcm_data.begin(), pcm_data.end());
|
||||
|
||||
std::vector<int16_t> out((*datalen));
|
||||
spx_uint32_t in_len = data_size;
|
||||
spx_uint32_t out_len = out.size();
|
||||
|
||||
speex_resampler_process_interleaved_int(g->resampler, in.data(), &in_len, out.data(), &out_len);
|
||||
|
||||
if (out_len > out.size()) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_CRIT, "Resampler output exceeded maximum buffer size!\n");
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
memcpy(data, out.data(), out_len * sizeof(int16_t));
|
||||
*datalen = out_len * sizeof(int16_t);
|
||||
} else {
|
||||
memcpy(data, pcm_data.data(), data_size * sizeof(int16_t));
|
||||
*datalen = data_size * sizeof(int16_t);
|
||||
}
|
||||
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
switch_status_t google_speech_flush_tts(google_t* g) {
|
||||
bool download_complete = g->response_code == 200;
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "google_speech_flush_tts, download complete? %s\n", download_complete ? "yes" : "no") ;
|
||||
|
||||
CircularBuffer_t *cBuffer = (CircularBuffer_t *) g->circularBuffer;
|
||||
delete cBuffer;
|
||||
g->circularBuffer = nullptr ;
|
||||
delete static_cast<std::chrono::time_point<std::chrono::high_resolution_clock>*>(g->startTime);
|
||||
g->startTime = nullptr;
|
||||
|
||||
g->flushed = 1;
|
||||
if (!download_complete) {
|
||||
if (g->file) {
|
||||
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "closing audio cache file %s because download was interrupted\n", g->cache_filename);
|
||||
if (fclose(g->file) != 0) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "error closing audio cache file\n");
|
||||
}
|
||||
g->file = nullptr ;
|
||||
}
|
||||
|
||||
if (g->cache_filename) {
|
||||
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "removing audio cache file %s because download was interrupted\n", g->cache_filename);
|
||||
if (unlink(g->cache_filename) != 0) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "cleanupConn: error removing audio cache file %s: %d:%s\n",
|
||||
g->cache_filename, errno, strerror(errno));
|
||||
}
|
||||
free(g->cache_filename);
|
||||
g->cache_filename = nullptr ;
|
||||
}
|
||||
}
|
||||
if (g->session_id) {
|
||||
switch_core_session_t* session = switch_core_session_locate(g->session_id);
|
||||
if (session) {
|
||||
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||
|
||||
/* unlock as quickly as possible */
|
||||
switch_core_session_rwunlock(session);
|
||||
if (channel) {
|
||||
switch_event_t *event;
|
||||
if (switch_event_create(&event, SWITCH_EVENT_PLAYBACK_STOP) == SWITCH_STATUS_SUCCESS) {
|
||||
switch_channel_event_set_data(channel, event);
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "Playback-File-Type", "tts_stream");
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_google_response_code", std::to_string(g->response_code).c_str());
|
||||
if (g->cache_filename && download_complete) {
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_cache_filename", g->cache_filename);
|
||||
}
|
||||
if (!download_complete && g->err_msg) {
|
||||
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "variable_tts_error", g->err_msg);
|
||||
}
|
||||
switch_event_fire(&event);
|
||||
}
|
||||
else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: failed to create event\n");
|
||||
}
|
||||
}
|
||||
else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: channel not found\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
switch_status_t google_speech_close(google_t* g) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "google_speech_close\n") ;
|
||||
if (g->resampler) {
|
||||
speex_resampler_destroy(g->resampler);
|
||||
}
|
||||
|
||||
g->resampler = NULL;
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
switch_status_t google_speech_unload() {
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
#ifndef __GOOGLE_TTS_GLUE_H__
|
||||
#define __GOOGLE_TTS_GLUE_H__
|
||||
|
||||
switch_status_t google_speech_load();
|
||||
switch_status_t google_speech_open(google_t* google);
|
||||
switch_status_t google_speech_feed_tts(google_t* google, char* text, switch_speech_flag_t *flags);
|
||||
switch_status_t google_speech_read_tts(google_t* google, void *data, size_t *datalen, switch_speech_flag_t *flags);
|
||||
switch_status_t google_speech_flush_tts(google_t* google);
|
||||
switch_status_t google_speech_close(google_t* google);
|
||||
switch_status_t google_speech_unload();
|
||||
|
||||
#endif
|
||||
@@ -1,154 +0,0 @@
|
||||
#include "mod_google_tts.h"
|
||||
#include "google_glue.h"
|
||||
|
||||
SWITCH_MODULE_LOAD_FUNCTION(mod_google_tts_load);
|
||||
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_google_tts_shutdown);
|
||||
SWITCH_MODULE_DEFINITION(mod_google_tts, mod_google_tts_load, mod_google_tts_shutdown, NULL);
|
||||
|
||||
static void clearGoogle(google_t* g, int freeAll) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "clearGoogle\n");
|
||||
|
||||
if (g->cache_filename) free(g->cache_filename);
|
||||
if (g->model) free(g->model);
|
||||
if (g->reported_usage) free(g->reported_usage);
|
||||
if (g->gender) free(g->gender);
|
||||
if (g->language) free(g->language);
|
||||
if (g->err_msg) free(g->err_msg);
|
||||
|
||||
g->cache_filename = NULL;
|
||||
g->model = NULL;
|
||||
g->reported_usage = NULL;
|
||||
g->gender = NULL;
|
||||
g->language = NULL;
|
||||
g->err_msg = NULL;
|
||||
|
||||
|
||||
if (freeAll) {
|
||||
if (g->voice_name) free(g->voice_name);
|
||||
if (g->session_id) free(g->session_id);
|
||||
g->voice_name = NULL;
|
||||
g->session_id = NULL;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static google_t * createOrRetrievePrivateData(switch_speech_handle_t *sh) {
|
||||
google_t *g = (google_t *) sh->private_info;
|
||||
if (!g) {
|
||||
g = switch_core_alloc(sh->memory_pool, sizeof(*g));
|
||||
sh->private_info = g;
|
||||
memset(g, 0, sizeof(*g));
|
||||
switch_mutex_init(&g->mutex, SWITCH_MUTEX_NESTED, sh->memory_pool);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "allocated google_t\n");
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
switch_status_t g_speech_open(switch_speech_handle_t *sh, const char *voice_name, int rate, int channels, switch_speech_flag_t *flags)
|
||||
{
|
||||
google_t *g = createOrRetrievePrivateData(sh);
|
||||
g->voice_name = strdup(voice_name);
|
||||
g->rate = rate;
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "g_speech_open voice: %s, rate %d, channels %d\n", voice_name, rate, channels);
|
||||
return google_speech_open(g);
|
||||
}
|
||||
|
||||
static switch_status_t g_speech_close(switch_speech_handle_t *sh, switch_speech_flag_t *flags)
|
||||
{
|
||||
switch_status_t rc;
|
||||
google_t *g = createOrRetrievePrivateData(sh);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "g_speech_close\n");
|
||||
|
||||
switch_mutex_destroy(g->mutex);
|
||||
|
||||
rc = google_speech_close(g);
|
||||
clearGoogle(g, 1);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Freeswitch will call this function to feed us text to speak
|
||||
*/
|
||||
static switch_status_t g_speech_feed_tts(switch_speech_handle_t *sh, char *text, switch_speech_flag_t *flags)
|
||||
{
|
||||
google_t *g = createOrRetrievePrivateData(sh);
|
||||
g->draining = 0;
|
||||
g->reads = 0;
|
||||
g->flushed = 0;
|
||||
|
||||
return google_speech_feed_tts(g, text, flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Freeswitch calls periodically to get some rendered audio in L16 format. We can provide up to 8k of audio at a time.
|
||||
*/
|
||||
static switch_status_t g_speech_read_tts(switch_speech_handle_t *sh, void *data, size_t *datalen, switch_speech_flag_t *flags)
|
||||
{
|
||||
google_t *g = createOrRetrievePrivateData(sh);
|
||||
return google_speech_read_tts(g, data, datalen, flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called at the end, not sure exactly what we need to do here..
|
||||
*/
|
||||
static void g_speech_flush_tts(switch_speech_handle_t *sh)
|
||||
{
|
||||
google_t *g = createOrRetrievePrivateData(sh);
|
||||
google_speech_flush_tts(g);
|
||||
|
||||
clearGoogle(g, 0);
|
||||
}
|
||||
|
||||
static void g_text_param_tts(switch_speech_handle_t *sh, char *param, const char *val)
|
||||
{
|
||||
google_t *g = createOrRetrievePrivateData(sh);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "g_text_param_tts: %s=%s\n", param, val);
|
||||
if (0 == strcmp(param, "voice")) {
|
||||
if (g->voice_name) free(g->voice_name);
|
||||
g->voice_name = strdup(val);
|
||||
} else if (0 == strcmp(param, "model")) {
|
||||
if (g->model) free(g->model);
|
||||
g->model = strdup(val);
|
||||
} else if (0 == strcmp(param, "reported_usage")) {
|
||||
if (g->reported_usage) free(g->reported_usage);
|
||||
g->reported_usage = strdup(val);
|
||||
} else if (0 == strcmp(param, "language")) {
|
||||
if (g->language) free(g->language);
|
||||
g->language = strdup(val);
|
||||
} else if (0 == strcmp(param, "session-uuid")) {
|
||||
if (g->session_id) free(g->session_id);
|
||||
g->session_id = strdup(val);
|
||||
} else if (0 == strcmp(param, "write_cache_file") && switch_true(val)) {
|
||||
g->cache_audio = 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void g_numeric_param_tts(switch_speech_handle_t *sh, char *param, int val)
|
||||
{
|
||||
}
|
||||
static void g_float_param_tts(switch_speech_handle_t *sh, char *param, double val)
|
||||
{
|
||||
}
|
||||
|
||||
SWITCH_MODULE_LOAD_FUNCTION(mod_google_tts_load)
|
||||
{
|
||||
switch_speech_interface_t *speech_interface;
|
||||
|
||||
*module_interface = switch_loadable_module_create_module_interface(pool, modname);
|
||||
speech_interface = switch_loadable_module_create_interface(*module_interface, SWITCH_SPEECH_INTERFACE);
|
||||
speech_interface->interface_name = "google";
|
||||
speech_interface->speech_open = g_speech_open;
|
||||
speech_interface->speech_close = g_speech_close;
|
||||
speech_interface->speech_feed_tts = g_speech_feed_tts;
|
||||
speech_interface->speech_read_tts = g_speech_read_tts;
|
||||
speech_interface->speech_flush_tts = g_speech_flush_tts;
|
||||
speech_interface->speech_text_param_tts = g_text_param_tts;
|
||||
speech_interface->speech_numeric_param_tts = g_numeric_param_tts;
|
||||
speech_interface->speech_float_param_tts = g_float_param_tts;
|
||||
return google_speech_load();
|
||||
}
|
||||
|
||||
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_google_tts_shutdown)
|
||||
{
|
||||
return google_speech_unload();
|
||||
}
|
||||
@@ -436,7 +436,7 @@ static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
|
||||
CircularBuffer_t *cBuffer = (CircularBuffer_t *) p->circularBuffer;
|
||||
std::vector<uint16_t> pcm_data;
|
||||
|
||||
if (conn->flushed) {
|
||||
if (conn->flushed || cBuffer == nullptr) {
|
||||
/* this will abort the transfer */
|
||||
return 0;
|
||||
}
|
||||
@@ -648,7 +648,7 @@ extern "C" {
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
fullDirPath = std::string(baseDir) + "jambonz-tts-cache-files";
|
||||
fullDirPath = std::string(baseDir) + "tts-cache-files";
|
||||
|
||||
// Create the directory with read, write, and execute permissions for everyone
|
||||
mode_t oldMask = umask(0);
|
||||
@@ -838,17 +838,9 @@ extern "C" {
|
||||
|
||||
p->circularBuffer = (void *) new CircularBuffer_t(8192);
|
||||
|
||||
if (p->session_id) {
|
||||
int err;
|
||||
switch_codec_implementation_t read_impl;
|
||||
switch_core_session_t *psession = switch_core_session_locate(p->session_id);
|
||||
switch_core_session_get_read_impl(psession, &read_impl);
|
||||
switch_core_session_rwunlock(psession);
|
||||
uint32_t samples_per_second = !strcasecmp(read_impl.iananame, "g722") ? read_impl.actual_samples_per_second : read_impl.samples_per_second;
|
||||
if (mpg123_param(mh, MPG123_FORCE_RATE, samples_per_second /*Hz*/, 0) != MPG123_OK) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error mpg123_param!\n");
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
if (mpg123_param(mh, MPG123_FORCE_RATE, p->rate /*Hz*/, 0) != MPG123_OK) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error mpg123_param!\n");
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
std::ostringstream api_key_stream;
|
||||
|
||||
@@ -386,7 +386,7 @@ static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
|
||||
auto d = conn->rimelabs;
|
||||
CircularBuffer_t *cBuffer = (CircularBuffer_t *) d->circularBuffer;
|
||||
|
||||
if (conn->flushed) {
|
||||
if (conn->flushed || cBuffer == nullptr) {
|
||||
/* this will abort the transfer */
|
||||
return 0;
|
||||
}
|
||||
@@ -394,6 +394,8 @@ static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
|
||||
std::unique_ptr<uint8_t[]> combinedData;
|
||||
|
||||
if (conn->has_last_byte) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "rimelabs write_cb: processing last byte from previous read\n");
|
||||
|
||||
conn->has_last_byte = false; // We'll handle the last_byte now, so toggle the flag off
|
||||
|
||||
// Allocate memory for the new data array
|
||||
@@ -415,6 +417,8 @@ static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
|
||||
|
||||
// If we now have an odd total, save the last byte for next time
|
||||
if ((total_bytes_to_process % sizeof(int16_t)) != 0) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "rimelabs write_cb: got odd number of bytes %d\n", total_bytes_to_process);
|
||||
|
||||
conn->last_byte = data[total_bytes_to_process - 1];
|
||||
conn->has_last_byte = true;
|
||||
total_bytes_to_process--;
|
||||
@@ -626,7 +630,7 @@ extern "C" {
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
fullDirPath = std::string(baseDir) + "jambonz-tts-cache-files";
|
||||
fullDirPath = std::string(baseDir) + "tts-cache-files";
|
||||
|
||||
// Create the directory with read, write, and execute permissions for everyone
|
||||
mode_t oldMask = umask(0);
|
||||
@@ -758,6 +762,8 @@ extern "C" {
|
||||
conn->file = d->file;
|
||||
conn->body = json;
|
||||
conn->flushed = false;
|
||||
conn->has_last_byte = false;
|
||||
conn->last_byte = 0;
|
||||
|
||||
|
||||
d->circularBuffer = (void *) new CircularBuffer_t(BUFFER_GROW_SIZE);
|
||||
|
||||
@@ -436,7 +436,7 @@ static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
|
||||
CircularBuffer_t *cBuffer = (CircularBuffer_t *) w->circularBuffer;
|
||||
std::vector<uint16_t> pcm_data;
|
||||
|
||||
if (conn->flushed) {
|
||||
if (conn->flushed || cBuffer == nullptr) {
|
||||
/* this will abort the transfer */
|
||||
return 0;
|
||||
}
|
||||
@@ -671,7 +671,7 @@ extern "C" {
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
fullDirPath = std::string(baseDir) + "jambonz-tts-cache-files";
|
||||
fullDirPath = std::string(baseDir) + "tts-cache-files";
|
||||
|
||||
// Create the directory with read, write, and execute permissions for everyone
|
||||
mode_t oldMask = umask(0);
|
||||
|
||||
Reference in New Issue
Block a user