Compare commits

...

3 Commits

Author SHA1 Message Date
Dave Horton 74bfc3152f fix build errors 2024-03-23 20:23:13 -04:00
Dave Horton 9cdc5fdfca Feat/mod dub v2 (#22)
* support tts elevenlabs to mod_dub

Signed-off-by: Quan HL <quan.luuhoang8@gmail.com>

* wip

Signed-off-by: Quan HL <quan.luuhoang8@gmail.com>

* wip

* wip

* fix aws race condition when 2 start transcribes are sent at the same instant

* wip

* wip

* wip

* allow queue play on track

* wip

Signed-off-by: Quan HL <quan.luuhoang8@gmail.com>

* revert change for aws transcribe

Signed-off-by: Quan HL <quan.luuhoang8@gmail.com>

* fix type

* wip

* wip

* rename parameters

* rename paramters

* wip

* wip

* wip

* wip

* wip

* bug: there exists scenarios where callback is not defined

* wip

* wip

* revert unintended changes to mod_google_transcribe

* fix bugs w/ streaming tts simplified arg parsing to use freeswitch conventions

---------

Signed-off-by: Quan HL <quan.luuhoang8@gmail.com>
Co-authored-by: Quan HL <quan.luuhoang8@gmail.com>
2024-03-23 15:37:27 -04:00
Andrew Golledge 4e57f73c7e Add Support for Google Cloud Speech-To-Text V2 library in mod_google_transcribe (#23)
* Introduce Google Speech-To-Text V2 library

* Add sign-off to previous commit

Signed-off-by: Andrew Golledge <andreas.golledge@gmail.com>

---------

Signed-off-by: Andrew Golledge <andreas.golledge@gmail.com>
2024-03-23 12:02:48 -04:00
32 changed files with 3204 additions and 2392 deletions
+9 -1
View File
@@ -2,9 +2,17 @@ include $(top_srcdir)/build/modmake.rulesam
MODNAME=mod_dub
mod_LTLIBRARIES = mod_dub.la
mod_dub_la_SOURCES = file_loader.cpp audio_downloader.cpp mod_dub.c dub_glue.cpp
mod_dub_la_SOURCES = ap_file.cpp ap_http.cpp track.cpp dub_glue.cpp tts_vendor_parser.cpp mod_dub.c vector_math.cpp mpg_decode.cpp
mod_dub_la_CFLAGS = $(AM_CFLAGS)
mod_dub_la_CXXFLAGS = $(AM_CXXFLAGS) -std=c++17
if USE_AVX2
mod_dub_la_CXXFLAGS += -mavx2 -DUSE_AVX2
else
if USE_SSE2
mod_dub_la_CXXFLAGS += -msse2 -DUSE_SSE2
endif
endif
mod_dub_la_LIBADD = $(switch_builddir)/libfreeswitch.la
mod_dub_la_LDFLAGS = -avoid-version -module -no-undefined -shared `pkg-config --libs boost` -lstdc++ -lmpg123
+41
View File
@@ -0,0 +1,41 @@
#ifndef __AP_H__
#define __AP_H__
#include <mutex>
#include <functional>
#include "common.h"
class AudioProducer {
public:
AudioProducer(
std::mutex& mutex,
CircularBuffer_t& circularBuffer,
int sampleRate
) : _mutex(mutex), _buffer(circularBuffer), _sampleRate(sampleRate), _notified(false), _loop(false), _gain(0) {}
virtual ~AudioProducer() {}
virtual void notifyDone(bool error, const std::string& errorMsg) {
if (!_notified) {
_notified = true;
if (_callback) _callback(error, errorMsg);
}
}
virtual void start(std::function<void(bool, const std::string&)> callback) = 0;
virtual void stop() = 0;
bool isLoopedAudio() const { return _loop; }
protected:
std::mutex& _mutex;
CircularBuffer_t& _buffer;
int _sampleRate;
int _gain;
bool _loop;
std::function<void(bool, const std::string&)> _callback;
bool _notified;
};
#endif
+196
View File
@@ -0,0 +1,196 @@
#include "switch.h"
#include "ap_file.h"
#include "mpg_decode.h"
#define INIT_BUFFER_SIZE (80000)
#define BUFFER_GROW_SIZE (80000)
#define BUFFER_THROTTLE_LOW (40000)
#define BUFFER_THROTTLE_HIGH (160000)
bool AudioProducerFile::initialized = false;
boost::asio::io_service AudioProducerFile::io_service;
std::thread AudioProducerFile::worker_thread;
void AudioProducerFile::threadFunc() {
io_service.reset() ;
boost::asio::io_service::work work(io_service);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "file_loader threadFunc - starting\n");
for(;;) {
try {
io_service.run() ;
break ;
}
catch( std::exception& e) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "file_loader threadFunc - Error: %s\n", e.what());
}
}
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "file_loader threadFunc - ending\n");
}
void AudioProducerFile::_init() {
if (!initialized) {
initialized = true;
if (mpg123_init() != MPG123_OK) {
throw std::runtime_error("AudioProducerFile::AudioProducerFile: failed to initiate MPG123");
return ;
}
/* start worker thread */
std::thread t(threadFunc) ;
worker_thread.swap( t ) ;
}
}
void AudioProducerFile::_deinit() {
if (initialized) {
initialized = false;
io_service.stop();
if (worker_thread.joinable()) {
worker_thread.join();
}
mpg123_exit();
}
}
AudioProducerFile::AudioProducerFile(
std::mutex& mutex,
CircularBuffer_t& circularBuffer,
int sampleRate
) : AudioProducer(mutex, circularBuffer, sampleRate), _timer(io_service), _mh(nullptr) {
AudioProducerFile::_init();
}
AudioProducerFile::~AudioProducerFile() {
reset();
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "AudioProducerFile::~AudioProducerFile %p\n", (void *)this);
}
void AudioProducerFile::queueFileAudio(const std::string& path, int gain, bool loop) {
_path = path;
_gain = gain;
_loop = loop;
/* we only handle mp3 or r8 files atm */
size_t pos = path.find_last_of('.');
if (pos == std::string::npos) {
throw std::runtime_error("file " + path + " has no extension");
}
auto filetype = path.substr(pos + 1);
if (0 == filetype.compare("mp3")) _type = FILE_TYPE_MP3;
else if (0 == filetype.compare("r8")) _type = FILE_TYPE_R8;
else throw std::runtime_error("file " + path + " has unsupported extension " + filetype);
}
void AudioProducerFile::start(std::function<void(bool, const std::string&)> callback) {
int mhError = 0;
_callback = callback;
/* allocate handle for mpeg decoding */
_mh = mpg123_new("auto", &mhError);
if (!_mh) {
const char *mhErr = mpg123_plain_strerror(mhError);
throw std::runtime_error("Error allocating mpg123 handle! " + std::string(mhErr));
}
if (mpg123_open_feed(_mh) != MPG123_OK) throw std::runtime_error("Error mpg123_open_feed!");
if (mpg123_format_all(_mh) != MPG123_OK) throw std::runtime_error("Error mpg123_format_all!");
if (mpg123_param(_mh, MPG123_FORCE_RATE, _sampleRate, 0) != MPG123_OK) throw std::runtime_error("Error forcing resample to 8k!");
if (mpg123_param(_mh, MPG123_FLAGS, MPG123_MONO_MIX, 0) != MPG123_OK) throw std::runtime_error("Error forcing single channel!");
_fp = fopen(_path.c_str(), "rb");
if (!_fp) throw std::runtime_error("Error opening file " + _path);
_status = Status_t::STATUS_AWAITING_RESTART;
/* do the initial read in the worker thread so we don't block here */
_timer.expires_from_now(boost::posix_time::millisec(1));
_timer.async_wait(boost::bind(&AudioProducerFile::read_cb, this, boost::placeholders::_1));
}
void AudioProducerFile::read_cb(const boost::system::error_code& error) {
if (_status == Status_t::STATUS_STOPPED) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: session gone\n");
return;
}
if (_status == Status_t::STATUS_AWAITING_RESTART) {
_status = Status_t::STATUS_IN_PROGRESS;
}
if (!error) {
size_t size = _buffer.size();
if (size < BUFFER_THROTTLE_LOW) {
std::vector<int16_t> pcm_data;
int8_t buf[INIT_BUFFER_SIZE];
size_t bytesRead = ::fread(buf, sizeof(int8_t), INIT_BUFFER_SIZE, _fp);
if (bytesRead <= 0) {
if (::feof(_fp)) switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %p eof\n", (void *) this);
else if (::ferror(_fp)) switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "read_cb: %p error reading file\n", (void *) this);
else switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "read_cb: %p unknown error reading file\n", (void *) this);
_status = Status_t::STATUS_COMPLETE;
return;
}
if (_type == FileType_t::FILE_TYPE_MP3) pcm_data = convert_mp3_to_linear(_mh, _gain, buf, bytesRead);
else pcm_data = std::vector<int16_t>(reinterpret_cast<int16_t*>(buf), reinterpret_cast<int16_t*>(buf) + bytesRead / 2);
{
std::lock_guard<std::mutex> lock(_mutex);
// Resize the buffer if necessary
size_t capacity = _buffer.capacity();
if (capacity - size < pcm_data.size()) {
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "write_cb %p growing buffer, size now %ld\n", (void *) this, size);
_buffer.set_capacity(size + std::max(pcm_data.size(), (size_t)BUFFER_GROW_SIZE));
}
/* Push the data into the buffer */
_buffer.insert(_buffer.end(), pcm_data.data(), pcm_data.data() + pcm_data.size());
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %p wrote data, buffer size is now %ld\n", (void *) this, _buffer.size());
}
if (bytesRead < INIT_BUFFER_SIZE) {
_status = Status_t::STATUS_COMPLETE;
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %u reached end of file, status is %s\n", (void *) this, status2String(_status));
}
}
if (_status == Status_t::STATUS_COMPLETE) {
cleanup(Status_t::STATUS_COMPLETE);
}
else {
// read more in 2 seconds
_timer.expires_from_now(boost::posix_time::millisec(2000));
_timer.async_wait(boost::bind(&AudioProducerFile::read_cb, this, boost::placeholders::_1));
}
} else {
cleanup(Status_t::STATUS_FAILED, error.message());
}
}
void AudioProducerFile::stop() {
cleanup(Status_t::STATUS_STOPPED, "");
}
void AudioProducerFile::reset() {
if (_fp) {
fclose(_fp);
_fp = nullptr;
}
if (_mh) {
mpg123_close(_mh);
mpg123_delete(_mh);
_mh = nullptr;
}
_timer.cancel();
_status = Status_t::STATUS_NONE;
}
void AudioProducerFile::cleanup(Status_t status, std::string errMsg) {
reset();
_status = status;
notifyDone(status != Status_t::STATUS_COMPLETE, errMsg);
}
+89
View File
@@ -0,0 +1,89 @@
#ifndef __AP_FILE_H__
#define __AP_FILE_H__
#include <thread>
#include <boost/asio.hpp>
#include <boost/bind/bind.hpp>
#include <mpg123.h>
#include "ap.h"
class AudioProducerFile : public AudioProducer {
public:
typedef enum
{
STATUS_NONE = 0,
STATUS_FAILED,
STATUS_IN_PROGRESS,
STATUS_PAUSED,
STATUS_COMPLETE,
STATUS_AWAITING_RESTART,
STATUS_STOPPED
} Status_t;
typedef enum {
FILE_TYPE_MP3 = 0,
FILE_TYPE_R8
} FileType_t;
const char* status2String(Status_t status)
{
static const char* statusStrings[] = {
"STATUS_NONE",
"STATUS_FAILED",
"STATUS_IN_PROGRESS",
"STATUS_PAUSED",
"STATUS_COMPLETE",
"STATUS_AWAITING_RESTART",
"STATUS_STOPPED"
};
if (status >= 0 && status < sizeof(statusStrings) / sizeof(statusStrings[0]))
{
return statusStrings[status];
}
else
{
return "UNKNOWN_STATUS";
}
}
AudioProducerFile(
std::mutex& mutex,
CircularBuffer_t& circularBuffer,
int sampleRate
);
virtual ~AudioProducerFile();
virtual void start(std::function<void(bool, const std::string&)> callback);
virtual void stop();
void cleanup(Status_t status, std::string errMsg = "");
void reset();
void queueFileAudio(const std::string& path, int gain = 0, bool loop = false);
void read_cb(const boost::system::error_code& error);
static bool initialized;
static std::thread worker_thread;
static boost::asio::io_service io_service;
static void threadFunc();
private:
static void _init();
static void _deinit();
void stop_file_load();
std::string _path;
Status_t _status;
FileType_t _type;
mpg123_handle *_mh;
boost::asio::deadline_timer _timer;
FILE* _fp;
};
#endif
+660
View File
@@ -0,0 +1,660 @@
#include <functional>
#include <mpg123.h>
#include "switch.h"
#include "ap_http.h"
#include "mpg_decode.h"
#include <boost/asio/ssl.hpp>
#include <boost/pool/object_pool.hpp>
#include <boost/bind/bind.hpp>
#include <boost/tokenizer.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/assign/list_of.hpp>
#define BUFFER_GROW_SIZE (80000)
#define BUFFER_THROTTLE_LOW (40000)
#define BUFFER_THROTTLE_HIGH (160000)
bool AudioProducerHttp::initialized = false;
boost::asio::io_service AudioProducerHttp::io_service;
std::thread AudioProducerHttp::worker_thread;
GlobalInfo_t AudioProducerHttp::global;
std::map<curl_socket_t, boost::asio::ip::tcp::socket *> AudioProducerHttp::socket_map;
boost::asio::deadline_timer AudioProducerHttp::multi_timer(AudioProducerHttp::io_service);
void AudioProducerHttp::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, "ap_http threadFunc - starting\n");
for(;;) {
try {
io_service.run() ;
break ;
}
catch( std::exception& e) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "ap_http threadFunc - Error: %s\n", e.what());
}
}
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "ap_http threadFunc - ending\n");
}
void AudioProducerHttp::_init() {
if (!initialized) {
initialized = true;
if (mpg123_init() != MPG123_OK) {
throw std::runtime_error("AudioProducerFile::AudioProducerFile: failed to initiate MPG123");
return ;
}
memset(&global, 0, sizeof(GlobalInfo_t));
global.multi = curl_multi_init();
if (!global.multi) {
throw std::runtime_error("AudioProducerHttp::_init: failed to initiate CURL multi");
return ;
}
curl_multi_setopt(global.multi, CURLMOPT_SOCKETFUNCTION, &AudioProducerHttp::sock_cb);
curl_multi_setopt(global.multi, CURLMOPT_SOCKETDATA, &global);
curl_multi_setopt(global.multi, CURLMOPT_TIMERFUNCTION, &AudioProducerHttp::multi_timer_cb);
curl_multi_setopt(global.multi, CURLMOPT_TIMERDATA, &global);
curl_multi_setopt(global.multi, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
/* start worker thread */
std::thread t(threadFunc) ;
worker_thread.swap( t ) ;
}
}
void AudioProducerHttp::_deinit() {
if (initialized) {
initialized = false;
io_service.stop();
if (worker_thread.joinable()) {
worker_thread.join();
}
/* cleanup curl multi handle*/
curl_multi_cleanup(global.multi);
mpg123_exit();
}
}
AudioProducerHttp::AudioProducerHttp(
std::mutex& mutex,
CircularBuffer_t& circularBuffer,
int sampleRate
) : AudioProducer(mutex, circularBuffer, sampleRate), _status(Status_t::STATUS_NONE), _mh(nullptr), _easy(nullptr),
_error{0}, _response_code(0), _timer(io_service) {
AudioProducerHttp::_init();
}
AudioProducerHttp::~AudioProducerHttp() {
reset();
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "AudioProducerFile::~AudioProducerFile %p\n", (void *)this);
}
void AudioProducerHttp::start(std::function<void(bool, const std::string&)> callback) {
int mhError = 0;
_callback = callback;
memset(_error, 0, sizeof(_error));
/* allocate handle for mpeg decoding */
_mh = mpg123_new("auto", &mhError);
if (!_mh) {
const char *mhErr = mpg123_plain_strerror(mhError);
throw std::runtime_error("Error allocating mpg123 handle! " + std::string(mhErr));
}
if (mpg123_open_feed(_mh) != MPG123_OK) throw std::runtime_error("Error mpg123_open_feed!\n");
if (mpg123_format_all(_mh) != MPG123_OK) throw std::runtime_error("Error mpg123_format_all!\n");
if (mpg123_param(_mh, MPG123_FORCE_RATE, _sampleRate, 0) != MPG123_OK) throw std::runtime_error("Error forcing resample to 8k!\n");
if (mpg123_param(_mh, MPG123_FLAGS, MPG123_MONO_MIX, 0) != MPG123_OK) throw std::runtime_error("Error forcing single channel!\n");
_easy = createEasyHandle();
if (!_easy) throw std::runtime_error("Error creating easy handle!\n");
curl_easy_setopt(_easy, CURLOPT_WRITEFUNCTION, &AudioProducerHttp::static_write_cb);
curl_easy_setopt(_easy, CURLOPT_URL, _url.c_str());
curl_easy_setopt(_easy, CURLOPT_HTTPGET, _method == HttpMethod_t::HTTP_METHOD_GET ? 1L : 0L);
curl_easy_setopt(_easy, CURLOPT_WRITEDATA, this);
curl_easy_setopt(_easy, CURLOPT_ERRORBUFFER, _error);
curl_easy_setopt(_easy, CURLOPT_PRIVATE, this);
curl_easy_setopt(_easy, CURLOPT_VERBOSE, 0L);
curl_easy_setopt(_easy, CURLOPT_NOPROGRESS, 1L);
curl_easy_setopt(_easy, CURLOPT_HEADERFUNCTION, &AudioProducerHttp::static_header_callback);
curl_easy_setopt(_easy, CURLOPT_HEADERDATA, this);
/* call this function to get a socket */
curl_easy_setopt(_easy, CURLOPT_OPENSOCKETFUNCTION, &AudioProducerHttp::open_socket);
/* call this function to close a socket */
curl_easy_setopt(_easy, CURLOPT_CLOSESOCKETFUNCTION, &AudioProducerHttp::close_socket);
curl_easy_setopt(_easy, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
/* keep the speed down so we don't have to buffer large amounts*/
curl_easy_setopt(_easy, CURLOPT_MAX_RECV_SPEED_LARGE, (curl_off_t)31415);
/*Add request body*/
if (!_body.empty()) curl_easy_setopt(_easy, CURLOPT_POSTFIELDS, _body.c_str());
/*Add request headers*/
struct curl_slist *hdr_list = nullptr;
for(const auto& header : _headers) {
hdr_list = curl_slist_append(hdr_list, header.c_str());
}
if (hdr_list) curl_easy_setopt(_easy, CURLOPT_HTTPHEADER, hdr_list);
_status = Status_t::STATUS_AWAITING_RESTART;
_timer.expires_from_now(boost::posix_time::millisec(1));
_timer.async_wait(boost::bind(&AudioProducerHttp::addCurlHandle, this, boost::placeholders::_1));
}
void AudioProducerHttp::queueHttpGetAudio(const std::string& url, int gain, bool loop) {
_method = HttpMethod_t::HTTP_METHOD_GET;
_url = url;
_gain = gain;
_loop = loop;
}
void AudioProducerHttp::queueHttpPostAudio(const std::string& url, int gain, bool loop) {
_method = HttpMethod_t::HTTP_METHOD_POST;
_url = url;
_gain = gain;
_loop = loop;
}
void AudioProducerHttp::queueHttpPostAudio(const std::string& url, const std::string& body, std::vector<std::string>& headers, int gain, bool loop) {
_method = HttpMethod_t::HTTP_METHOD_POST;
_url = url;
_body = body;
_headers = headers;
_gain = gain;
_loop = loop;
}
void AudioProducerHttp::addCurlHandle(const boost::system::error_code& error) {
if (_status == Status_t::STATUS_AWAITING_RESTART) {
auto rc = curl_multi_add_handle(global.multi, _easy);
if (mcode_test("new_conn: curl_multi_add_handle", rc) < 0) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "AudioProducerHttp::addCurlHandle: Error adding easy handle to multi handle\n");
}
else {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "AudioProducerHttp::addCurlHandle retrieving from %s\n", _url.c_str());
}
}
}
CURL* AudioProducerHttp::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 no total timeout as files could be large
curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, 3000L);
curl_easy_setopt(easy, CURLOPT_TIMEOUT, 0L); // no timeout
return easy ;
}
size_t AudioProducerHttp::static_write_cb(char* ptr, size_t size, size_t nmemb, void* userdata) {
return static_cast<AudioProducerHttp*>(userdata)->write_cb(ptr, size, nmemb);
}
size_t AudioProducerHttp::write_cb(void *ptr, size_t size, size_t nmemb) {
int8_t *data = (int8_t *) ptr;
size_t bytes_received = size * nmemb;
std::vector<int16_t> pcm_data;
if (_status == Status_t::STATUS_STOPPING || _status == Status_t::STATUS_STOPPED) {
_timer.cancel();
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG,
// "AudioProducerHttp::write_cb: aborting transfer, status %s, mutex %p, buffer %p\n", status2String(_status));
/* this will abort the transfer */
return 0;
}
if (_response_code > 0 && _response_code != 200) {
std::string body((char *) ptr, bytes_received);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "AudioProducerHttp::write_cb: received body %s\n", body.c_str());
_err_msg = body;
_status = Status_t::STATUS_FAILED;
return 0;
}
/* throttle after reaching high water mark */
size_t bufSize = _buffer.size();
if (bufSize > BUFFER_THROTTLE_HIGH) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "AudioProducerHttp::write_cb: throttling download, buffer size is %ld\n", _buffer.size());
// check back in 2 seconds
_timer.expires_from_now(boost::posix_time::millisec(2000));
_timer.async_wait(boost::bind(&AudioProducerHttp::throttling_cb, this, boost::placeholders::_1));
_status = Status_t::STATUS_DOWNLOAD_PAUSED;
return CURL_WRITEFUNC_PAUSE;
}
pcm_data = convert_mp3_to_linear(_mh, _gain, data, bytes_received);
size_t samples = pcm_data.size();
std::lock_guard<std::mutex> lock(_mutex);
// Resize the buffer if necessary
if (_buffer.capacity() - bufSize < samples) {
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "AudioProducerHttp::write_cb growing buffer, size now %ld\n", _buffer.size());
//TODO: if buffer exceeds some max size, return CURL_WRITEFUNC_ERROR to abort the transfer
_buffer.set_capacity(_buffer.size() + std::max(samples, (size_t)BUFFER_GROW_SIZE));
}
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "AudioProducerHttp::write_cb: writing %ld samples to buffer\n", pcm_data.size());
/* Push the data into the buffer */
_buffer.insert(_buffer.end(), pcm_data.data(), pcm_data.data() + pcm_data.size());
return bytes_received;
}
size_t AudioProducerHttp::static_header_callback(char *buffer, size_t size, size_t nitems, void* userdata) {
return static_cast<AudioProducerHttp*>(userdata)->header_callback(buffer, size, nitems);
}
size_t AudioProducerHttp::header_callback(char *buffer, size_t size, size_t nitems) {
size_t bytes_received = size * nitems;
const std::string prefix = "HTTP/";
std::string header, value;
std::string input(buffer, bytes_received);
if (parseHeader(input, header, value)) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "header_callback: %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 {
_response_code = extract_response_code(input);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "header_callback: parsed response code: %ld\n", _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;
}
void AudioProducerHttp::throttling_cb(const boost::system::error_code& error) {
if (_status == Status_t::STATUS_STOPPING) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "throttling_cb: session gone, resume download so we can complete\n");
curl_easy_pause(_easy, CURLPAUSE_CONT);
return;
}
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "throttling_cb: status is %s\n", status2String(_status));
if (!error) {
auto size = _buffer.size();
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "throttling_cb: size is now %ld\n", size);
if (size < BUFFER_THROTTLE_LOW) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "throttling_cb: resuming download\n");
curl_easy_pause(_easy, CURLPAUSE_CONT);
return;
}
// check back in 2 seconds
_timer.expires_from_now(boost::posix_time::millisec(2000));
_timer.async_wait(boost::bind(&AudioProducerHttp::throttling_cb, this, boost::placeholders::_1));
} else if (125 == error.value()) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "throttling_cb: timer canceled\n");
}
else {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "throttling_cb: error (%d): %s\n", error.value(), error.message().c_str());
}
}
int AudioProducerHttp::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;
}
void AudioProducerHttp::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 AudioProducerHttp::multi_timer_cb(CURLM *multi, long timeout_ms, GlobalInfo_t *g) {
/* cancel running timer */
multi_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
multi_timer.expires_from_now(boost::posix_time::millisec(timeout_ms ? timeout_ms : 1));
multi_timer.async_wait(boost::bind(&timer_cb, boost::placeholders::_1, g));
}
return 0;
}
curl_socket_t AudioProducerHttp::open_socket(void *clientp, curlsocktype purpose, struct curl_sockaddr *address) {
curl_socket_t sockfd = CURL_SOCKET_BAD;
/* 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;
}
int AudioProducerHttp::close_socket(void *clientp, curl_socket_t 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;
}
int AudioProducerHttp::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 ;
}
bool AudioProducerHttp::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;
}
int AudioProducerHttp::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;
}
void AudioProducerHttp::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()) {
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));
}
}
}
/* Called by asio when there is an action on a socket */
void AudioProducerHttp::event_cb(GlobalInfo_t *g, curl_socket_t s, int action, const boost::system::error_code & error, int *fdp) {
int f = *fdp;
// Socket already POOL REMOVED.
if (f == CURL_POLL_REMOVE) {
remsock(fdp, g);
return;
}
if(socket_map.find(s) == socket_map.end()) {
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) {
multi_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));
}
}
}
}
void AudioProducerHttp::remsock(int *f, GlobalInfo_t *g) {
if(f) {
free(f);
f = NULL;
}
}
void AudioProducerHttp::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);
}
/* Check for completed transfers, and remove their easy handles */
void AudioProducerHttp::check_multi_info(GlobalInfo_t *g) {
CURLMsg *msg;
int msgs_left;
AudioProducerHttp *ap;
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, &ap);
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);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "curl done, response code %d, status %s\n", response_code, status2String(ap->getStatus()));
bool restart = ap->isLoopedAudio() && ap->getStatus() != Status_t::STATUS_STOPPING && response_code == 200;
if (restart) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "restarting looped audio\n");
ap->getTimer().expires_from_now(boost::posix_time::millisec(1000));
ap->getTimer().async_wait(boost::bind(&AudioProducerHttp::static_restart_cb, boost::placeholders::_1, ap));
}
else {
ap->cleanup(Status_t::STATUS_DOWNLOAD_COMPLETE, (int) response_code);
}
}
}
}
void AudioProducerHttp::static_restart_cb(const boost::system::error_code& error, void* userdata) {
static_cast<AudioProducerHttp*>(userdata)->restart_cb(error);
}
void AudioProducerHttp::restart_cb(const boost::system::error_code& error) {
if (_status == Status_t::STATUS_STOPPING) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "restart_cb: session gone\n");
return;
}
if (!error) {
reset();
start(_callback);
}
else if (error.value() == boost::asio::error::operation_aborted) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "restart_cb: %s, cancelling retrieve\n", error.message().c_str());
}
else {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "restart_cb: error (%d): %s\n", error.value(), error.message().c_str());
}
}
void AudioProducerHttp::stop() {
cleanup(Status_t::STATUS_STOPPED, 0);
return;
}
void AudioProducerHttp::reset() {
if (_easy) {
curl_multi_remove_handle(global.multi, _easy);
curl_easy_cleanup(_easy);
_easy = nullptr;
}
if (_mh) {
mpg123_close(_mh);
mpg123_delete(_mh);
_mh = nullptr;
}
_err_msg.clear();
_response_code = 0;
_timer.cancel();
_status = Status_t::STATUS_NONE;
}
void AudioProducerHttp::cleanup(Status_t status, int response_code) {
std::string errMsg = response_code > 200 ? "http response: " + std::to_string(response_code) : "";
reset();
_status = status;
notifyDone(!errMsg.empty(), errMsg);
}
+134
View File
@@ -0,0 +1,134 @@
#ifndef __AP_HTTP_H__
#define __AP_HTTP_H__
#include "ap.h"
#include <curl/curl.h>
#include <mpg123.h>
#include <boost/asio.hpp>
typedef struct
{
CURLM *multi;
int still_running;
} GlobalInfo_t;
class AudioProducerHttp : public AudioProducer {
public:
typedef enum
{
STATUS_NONE = 0,
STATUS_FAILED,
STATUS_DOWNLOAD_IN_PROGRESS,
STATUS_DOWNLOAD_PAUSED,
STATUS_DOWNLOAD_COMPLETE,
STATUS_AWAITING_RESTART,
STATUS_STOPPING,
STATUS_STOPPED
} Status_t;
typedef enum {
HTTP_METHOD_GET = 0,
HTTP_METHOD_POST
} HttpMethod_t;
static const char* status2String(Status_t status) {
static const char* statusStrings[] = {
"STATUS_NONE",
"STATUS_FAILED",
"STATUS_DOWNLOAD_IN_PROGRESS",
"STATUS_DOWNLOAD_PAUSED",
"STATUS_DOWNLOAD_COMPLETE",
"STATUS_AWAITING_RESTART",
"STATUS_STOPPED"
};
if (status >= 0 && status < sizeof(statusStrings) / sizeof(statusStrings[0]))
{
return statusStrings[status];
}
else
{
return "UNKNOWN_STATUS";
}
}
AudioProducerHttp(
std::mutex& mutex,
CircularBuffer_t& circularBuffer,
int sampleRate
);
virtual ~AudioProducerHttp();
virtual void start(std::function<void(bool, const std::string&)> callback);
void addCurlHandle(const boost::system::error_code& error);
virtual void stop();
void cleanup(Status_t status, int response_code);
void reset();
void queueHttpGetAudio(const std::string& url, int gain = 0, bool loop = false);
void queueHttpPostAudio(const std::string& url, int gain = 0, bool loop = false);
void queueHttpPostAudio(const std::string& url, const std::string& body, std::vector<std::string>& headers, int gain = 0, bool loop = false);
Status_t getStatus() const { return _status; }
void setStatus(Status_t status) { _status = status; }
boost::asio::deadline_timer& getTimer() { return _timer; }
static bool initialized;
static std::thread worker_thread;
static boost::asio::io_service io_service;
static void threadFunc();
static std::map<curl_socket_t, boost::asio::ip::tcp::socket *> socket_map;
static boost::asio::deadline_timer multi_timer;
static GlobalInfo_t global;
static int sock_cb(CURL *e, curl_socket_t s, int what, void *cbp, void *sockp);
static int multi_timer_cb(CURLM *multi, long timeout_ms, GlobalInfo_t *g);
static void timer_cb(const boost::system::error_code & error, GlobalInfo_t *g);
static void addsock(curl_socket_t s, CURL *easy, int action, GlobalInfo_t *g);
static void setsock(int *fdp, curl_socket_t s, CURL *e, int act, int oldact, GlobalInfo_t *g);
static int mcode_test(const char *where, CURLMcode code);
static void check_multi_info(GlobalInfo_t *g);
static void event_cb(GlobalInfo_t *g, curl_socket_t s, int action, const boost::system::error_code & error, int *fdp);
static void remsock(int *f, GlobalInfo_t *g);
static size_t static_write_cb(char* ptr, size_t size, size_t nmemb, void* userdata);
size_t write_cb(void *ptr, size_t size, size_t nmemb);
static size_t static_header_callback(char *buffer, size_t size, size_t nitems, void* userdata);
size_t header_callback(char *buffer, size_t size, size_t nitems);
void throttling_cb(const boost::system::error_code& error);
static void static_restart_cb(const boost::system::error_code& error, void* userdata);
void restart_cb(const boost::system::error_code& error);
static int close_socket(void *clientp, curl_socket_t item);
static curl_socket_t open_socket(void *clientp, curlsocktype purpose, struct curl_sockaddr *address);
private:
static void _init();
static void _deinit();
CURL* createEasyHandle();
bool parseHeader(const std::string& str, std::string& header, std::string& value);
int extract_response_code(const std::string& input);
HttpMethod_t _method;
std::string _url;
std::string _body;
std::vector<std::string> _headers;
Status_t _status;
mpg123_handle *_mh;
CURL *_easy;
char _error[CURL_ERROR_SIZE]; // curl error buffer
std::string _err_msg;
int _response_code;
boost::asio::deadline_timer _timer;
};
#endif
-885
View File
@@ -1,885 +0,0 @@
#include "audio_downloader.h"
#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 <map>
#include <mpg123.h>
#include <curl/curl.h>
#define BUFFER_GROW_SIZE (80000)
#define BUFFER_THROTTLE_LOW (40000)
#define BUFFER_THROTTLE_HIGH (160000)
static uint16_t currDownloadId = 0;
typedef struct
{
CURLM *multi;
int still_running;
} GlobalInfo_t;
static GlobalInfo_t global;
typedef enum
{
STATUS_NONE = 0,
STATUS_FAILED,
STATUS_DOWNLOAD_IN_PROGRESS,
STATUS_DOWNLOAD_PAUSED,
STATUS_DOWNLOAD_COMPLETE,
STATUS_AWAITING_RESTART,
STATUS_STOPPING,
STATUS_STOPPED
} Status_t;
static const char* status2String(Status_t status)
{
static const char* statusStrings[] = {
"STATUS_NONE",
"STATUS_FAILED",
"STATUS_DOWNLOAD_IN_PROGRESS",
"STATUS_DOWNLOAD_PAUSED",
"STATUS_DOWNLOAD_COMPLETE",
"STATUS_AWAITING_RESTART",
"STATUS_STOPPING",
"STATUS_STOPPED"
};
if (status >= 0 && status < sizeof(statusStrings) / sizeof(statusStrings[0]))
{
return statusStrings[status];
}
else
{
return "UNKNOWN_STATUS";
}
}
typedef struct
{
GlobalInfo_t *global;
CURL *easy;
switch_mutex_t* mutex;
CircularBuffer_t* buffer;
mpg123_handle *mh;
char error[CURL_ERROR_SIZE]; // curl error buffer
char *err_msg; // http server error message
char* url;
bool loop;
int rate;
boost::asio::deadline_timer *timer;
Status_t status;
downloadId_t id;
int response_code;
int gain;
} ConnInfo_t;
typedef std::map<int32_t, ConnInfo_t *> Id2ConnMap_t;
static Id2ConnMap_t id2ConnMap;
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;
/* forward declarations */
static ConnInfo_t* createDownloader(const char *url, int rate, int loop, int gain, mpg123_handle *mhm, switch_mutex_t *mutex, CircularBuffer_t *buffer);
static CURL* createEasyHandle(void);
static void destroyConnection(ConnInfo_t *conn);
static void check_multi_info(GlobalInfo_t *g) ;
static int mcode_test(const char *where, CURLMcode code);
static void event_cb(GlobalInfo_t *g, curl_socket_t s, int action, const boost::system::error_code & error, int *fdp);
static void setsock(int *fdp, curl_socket_t s, CURL *e, int act, int oldact, GlobalInfo_t *g);
static void addsock(curl_socket_t s, CURL *easy, int action, GlobalInfo_t *g);
static int sock_cb(CURL *e, curl_socket_t s, int what, void *cbp, void *sockp);
static void threadFunc();
static void timer_cb(const boost::system::error_code & error, GlobalInfo_t *g);
static int multi_timer_cb(CURLM *multi, long timeout_ms, GlobalInfo_t *g);
static std::vector<int16_t> convert_mp3_to_linear(ConnInfo_t *conn, int8_t *data, size_t len);
static void throttling_cb(const boost::system::error_code& error, ConnInfo_t* conn) ;
static void restart_cb(const boost::system::error_code& error, ConnInfo_t* conn);
static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn);
static bool parseHeader(const std::string& str, std::string& header, std::string& value) ;
static int extract_response_code(const std::string& input) ;
static size_t header_callback(char *buffer, size_t size, size_t nitems, ConnInfo_t *conn);
static curl_socket_t opensocket(void *clientp, curlsocktype purpose, struct curl_sockaddr *address);
static int close_socket(void *clientp, curl_socket_t item);
/* apis */
extern "C" {
switch_status_t init_audio_downloader() {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "init_audio_downloader 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, "init_audio_downloader 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);
if (mpg123_init() != MPG123_OK) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "init_audio_downloader: failed to initiate MPG123");
return SWITCH_STATUS_FALSE;
}
/* start worker thread */
std::thread t(threadFunc) ;
worker_thread.swap( t ) ;
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "init_audio_downloader: loaded\n");
return SWITCH_STATUS_SUCCESS;
}
switch_status_t deinit_audio_downloader() {
/* stop the ASIO IO service */
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "deinit_audio_downloader: stopping io service\n");
io_service.stop();
/* Join the worker thread */
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "deinit_audio_downloader: 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, "deinit_audio_downloader: release curl multi\n");
curl_multi_cleanup(global.multi);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "deinit_audio_downloader: completed\n");
mpg123_exit();
return SWITCH_STATUS_SUCCESS;
}
downloadId_t start_audio_download(const char* url, int rate, int loop, int gain, switch_mutex_t* mutex, CircularBuffer_t* buffer) {
int mhError = 0;
/* allocate handle for mpeg decoding */
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 INVALID_DOWNLOAD_ID;
}
if (mpg123_open_feed(mh) != MPG123_OK) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error mpg123_open_feed!\n");
return INVALID_DOWNLOAD_ID;
}
if (mpg123_format_all(mh) != MPG123_OK) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error mpg123_format_all!\n");
return INVALID_DOWNLOAD_ID;
}
if (mpg123_param(mh, MPG123_FORCE_RATE, rate, 0) != MPG123_OK) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error forcing resample to 8k!\n");
return INVALID_DOWNLOAD_ID;
}
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 INVALID_DOWNLOAD_ID;
}
ConnInfo_t* conn = createDownloader(url, rate, loop, gain, mh, mutex, buffer);
if (!conn) {
return INVALID_DOWNLOAD_ID;
}
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO,
"start_audio_download: starting download %d\n", conn->id);
return conn->id;
}
switch_status_t stop_audio_download(int id) {
auto it = id2ConnMap.find(id);
if (it == id2ConnMap.end()) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "stop_audio_download: id %d has already completed\n", id);
return SWITCH_STATUS_FALSE;
}
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO,
"stop_audio_download: stopping download %d, status %s\n", id, status2String(it->second->status));
ConnInfo_t *conn = it->second;
auto status = conn->status;
/* past this point I shall not access either the mutex or the buffer provided */
conn->mutex = nullptr;
conn->buffer = nullptr;
/* if download is in progress set status to cancel it during next call back */
if (status == Status_t::STATUS_DOWNLOAD_PAUSED) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "stop_audio_download: resuming download %d so we can cancel it\n", id);
conn->status = Status_t::STATUS_STOPPING;
curl_easy_pause(conn->easy, CURLPAUSE_CONT);
}
if (status != Status_t::STATUS_DOWNLOAD_IN_PROGRESS) {
destroyConnection(conn);
}
conn->status = Status_t::STATUS_STOPPING;
return SWITCH_STATUS_SUCCESS;
}
}
/* internal */
ConnInfo_t* createDownloader(const char *url, int rate, int loop, int gain, mpg123_handle *mh, switch_mutex_t *mutex, CircularBuffer_t *buffer) {
ConnInfo_t *conn = pool.malloc() ;
CURL* easy = createEasyHandle();
if (!easy || !conn) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "createDownloader: failed to allocate memory\n");
return nullptr;
}
memset(conn, 0, sizeof(ConnInfo_t));
conn->easy = easy;
conn->mutex = mutex;
conn->buffer = buffer;
conn->mh = mh;
conn->loop = loop;
conn->gain = gain;
conn->rate = rate;
conn->url = strdup(url);
conn->global = &global;
conn->status = Status_t::STATUS_NONE;
conn->timer = new boost::asio::deadline_timer(io_service);
downloadId_t id = ++currDownloadId;
if (id == 0) id++;
id2ConnMap[id] = conn;
conn->id = id;
curl_easy_setopt(easy, CURLOPT_URL, url);
curl_easy_setopt(easy, CURLOPT_HTTPGET, 1L);
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);
curl_easy_setopt(easy, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
/* keep the speed down so we don't have to buffer large amounts*/
curl_easy_setopt(easy, CURLOPT_MAX_RECV_SPEED_LARGE, (curl_off_t)31415);
auto rc = curl_multi_add_handle(global.multi, conn->easy);
if (mcode_test("new_conn: curl_multi_add_handle", rc) < 0) {
return nullptr;
}
conn->status = Status_t::STATUS_DOWNLOAD_IN_PROGRESS;
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "createDownloader: launched request, gain %d\n", conn->gain);
return conn;
}
void destroyConnection(ConnInfo_t *conn) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "destroyConnection\n");
/* clean up the curl handle*/
curl_multi_remove_handle(conn->global, conn->easy);
curl_easy_cleanup(conn->easy);
/* clear asio resources and free resources */
if (conn->timer) {
conn->timer->cancel();
delete conn->timer;
}
if (conn->err_msg) {
free(conn->err_msg);
}
/* free mp3 decoder */
if (conn->mh) {
mpg123_close(conn->mh);
mpg123_delete(conn->mh);
}
if (conn->url) {
free(conn->url);
}
if (conn->mutex) switch_mutex_lock(conn->mutex);
id2ConnMap.erase(conn->id);
if (conn->mutex) switch_mutex_unlock(conn->mutex);
memset(conn, 0, sizeof(ConnInfo_t));
pool.destroy(conn) ;
}
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 no total timeout as files could be large
curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, 3000L);
curl_easy_setopt(easy, CURLOPT_TIMEOUT, 0L); // no timeout
return easy ;
}
/* 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);
downloadId_t id = conn->id;
auto mutex = conn->mutex;
auto buffer = conn->buffer;
auto rate = conn->rate;
auto loop = conn->loop;
auto gain = conn->gain;
auto oldId = conn->id;
bool restart = conn->loop && conn->status != Status_t::STATUS_STOPPING && response_code == 200;
conn->response_code = response_code;
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "curl done, response code %d, status %s\n", response_code, status2String(conn->status));
conn->status = Status_t::STATUS_DOWNLOAD_COMPLETE;
curl_multi_remove_handle(g->multi, easy);
if (restart) {
conn->status = Status_t::STATUS_AWAITING_RESTART;
conn->timer->expires_from_now(boost::posix_time::millisec(1000));
conn->timer->async_wait(boost::bind(&restart_cb, boost::placeholders::_1, conn));
//TODO: this seems to not be working from this callback; maybe start it from a timer callback?
}
else {
destroyConnection(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 ;
}
void remsock(int *f, GlobalInfo_t *g) {
if(f) {
free(f);
f = NULL;
}
}
/* Called by asio when there is an action on a socket */
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 */
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));
}
}
}
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);
}
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;
}
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_dub threadFunc - starting\n");
for(;;) {
try {
io_service.run() ;
break ;
}
catch( std::exception& e) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "mod_dub threadFunc - Error: %s\n", e.what());
}
}
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "mod_dub threadFunc - ending\n");
}
/* Called by asio when our timeout expires */
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;
}
std::vector<int16_t> convert_mp3_to_linear(ConnInfo_t *conn, int8_t *data, size_t len) {
std::vector<int16_t> linear_data;
int eof = 0;
int mp3err = 0;
if(mpg123_feed(conn->mh, (const unsigned char*) 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:
{
size_t samples = usedlen / sizeof(int16_t);
linear_data.insert(linear_data.end(), reinterpret_cast<int16_t*>(audio), reinterpret_cast<int16_t*>(audio) + samples);
}
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;
}
if (conn->gain != 0) {
switch_change_sln_volume_granular(linear_data.data(), linear_data.size(), conn->gain);
}
}
return linear_data;
}
void restart_cb(const boost::system::error_code& error, ConnInfo_t* conn) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "restart_cb status is %s\n", status2String(conn->status));
if (conn->status == Status_t::STATUS_AWAITING_RESTART) {
auto url = strdup(conn->url);
auto rate = conn->rate;
auto loop = conn->loop;
auto gain = conn->gain;
auto mutex = conn->mutex;
auto buffer = conn->buffer;
auto oldId = conn->id;
destroyConnection(conn);
downloadId_t id = start_audio_download(url, rate, loop, gain, mutex, buffer);
/* re-use id since caller is tracking that id */
auto * newConnection = id2ConnMap[id];
id2ConnMap[oldId] = newConnection;
id2ConnMap.erase(id);
free(url);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "restarted looped download\n");
}
}
void throttling_cb(const boost::system::error_code& error, ConnInfo_t* conn) {
if (conn->status == Status_t::STATUS_STOPPING || !conn->mutex || !conn->buffer) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "throttling_cb: session gone, resume download so we can complete\n");
curl_easy_pause(conn->easy, CURLPAUSE_CONT);
return;
}
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "throttling_cb: status is %s\n", status2String(conn->status));
switch_mutex_lock(conn->mutex);
if (!error) {
auto size = conn->buffer->size();
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "throttling_cb: size is now %ld\n", size);
if (size < BUFFER_THROTTLE_LOW) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "throttling_cb: resuming download\n");
curl_easy_pause(conn->easy, CURLPAUSE_CONT);
switch_mutex_unlock(conn->mutex);
return;
}
// check back in 2 seconds
conn->timer->expires_from_now(boost::posix_time::millisec(2000));
conn->timer->async_wait(boost::bind(&throttling_cb, boost::placeholders::_1, conn));
} else {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "throttling_cb: error (%d): %s\n", error.value(), error.message().c_str());
// Handle any errors
}
switch_mutex_unlock(conn->mutex);
}
/* CURLOPT_WRITEFUNCTION - here is where we receive the data */
size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
int8_t *data = (int8_t *) ptr;
size_t bytes_received = size * nmemb;
std::vector<int16_t> pcm_data;
if (conn->status == Status_t::STATUS_STOPPING || conn->status == Status_t::STATUS_STOPPED || !conn->mutex || !conn->buffer) {
if (conn->timer) conn->timer->cancel();
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG,
"write_cb: aborting transfer, status %s, mutex %p, buffer %p\n", status2String(conn->status), conn->mutex, conn->buffer);
/* this will abort the transfer */
return 0;
}
{
switch_mutex_lock(conn->mutex);
if (conn->response_code > 0 && conn->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());
conn->err_msg = strdup(body.c_str());
conn->status = Status_t::STATUS_FAILED;
switch_mutex_unlock(conn->mutex);
return 0;
}
/* throttle after reaching high water mark */
if (conn->buffer->size() > BUFFER_THROTTLE_HIGH) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "write_cb: throttling download, buffer size is %ld\n", conn->buffer->size());
// check back in 2 seconds
conn->timer->expires_from_now(boost::posix_time::millisec(2000));
conn->timer->async_wait(boost::bind(&throttling_cb, boost::placeholders::_1, conn));
conn->status = Status_t::STATUS_DOWNLOAD_PAUSED;
switch_mutex_unlock(conn->mutex);
return CURL_WRITEFUNC_PAUSE;
}
pcm_data = convert_mp3_to_linear(conn, data, bytes_received);
size_t bytesResampled = pcm_data.size() * sizeof(int16_t);
// Resize the buffer if necessary
if (conn->buffer->capacity() - conn->buffer->size() < (bytesResampled / sizeof(int16_t))) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "write_cb growing buffer, size now %ld\n", conn->buffer->size());
//TODO: if buffer exceeds some max size, return CURL_WRITEFUNC_ERROR to abort the transfer
conn->buffer->set_capacity(conn->buffer->size() + std::max((bytesResampled / sizeof(int16_t)), (size_t)BUFFER_GROW_SIZE));
}
/* Push the data into the buffer */
conn->buffer->insert(conn->buffer->end(), pcm_data.data(), pcm_data.data() + pcm_data.size());
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "write_cb: wrote data, buffer size is now %ld\n", conn->buffer->size());
switch_mutex_unlock(conn->mutex);
}
return bytes_received;
}
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;
}
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;
}
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/";
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, "recv header: %s\n", input.c_str());
if (input.rfind(prefix, 0) == 0) {
try {
conn->response_code = extract_response_code(input);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "parsed response code: %ld\n", conn->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 */
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 */
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;
}
-18
View File
@@ -1,18 +0,0 @@
#ifndef __AUDIO_DOWNLOADER_H__
#define __AUDIO_DOWNLOADER_H__
#include <switch.h>
#include "common.h"
extern "C" {
switch_status_t init_audio_downloader();
switch_status_t deinit_audio_downloader();
int start_audio_download(const char* url, int rate, int loop, int gain, switch_mutex_t* mutex, CircularBuffer_t* buffer);
switch_status_t stop_audio_download(int id);
}
#endif
-3
View File
@@ -3,10 +3,7 @@
#include <boost/circular_buffer.hpp>
typedef boost::circular_buffer<int16_t> CircularBuffer_t;
typedef int32_t downloadId_t;
#define INVALID_DOWNLOAD_ID (-1)
#endif
+117 -130
View File
@@ -1,8 +1,9 @@
#include "mod_dub.h"
#include "audio_downloader.h"
#include "file_loader.h"
#include "tts_vendor_parser.h"
#include "track.h"
#include "vector_math.h"
#include <string>
#include <queue>
#include <switch.h>
@@ -15,124 +16,111 @@ typedef boost::circular_buffer<int16_t> CircularBuffer_t;
extern "C" {
void init_dub_track(dub_track_t *track, char* trackName, int sampleRate) {
track->state = DUB_TRACK_STATE_READY;
track->trackName = strdup(trackName);
track->sampleRate = sampleRate;
track->circularBuffer = new CircularBuffer_t(INIT_BUFFER_SIZE);
Track* find_track_by_name(void** tracks, const std::string& trackName) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "find_track_by_name: searching for %s\n", trackName.c_str());
for (int i = 0; i < MAX_DUB_TRACKS; i++) {
Track* track = static_cast<Track*>(tracks[i]);
std::string name = track ? track->getTrackName() : "null";
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "find_track_by_name: offset %d: %s\n", i, name.c_str());
if (track && 0 == track->getTrackName().compare(trackName)) {
return track;
}
}
return nullptr;
}
switch_status_t silence_dub_track(dub_track_t *track) {
assert(track);
switch (track->generator) {
case DUB_GENERATOR_TYPE_HTTP:
stop_audio_download(track->generatorId);
break;
case DUB_GENERATOR_TYPE_FILE:
stop_file_load(track->generatorId);
break;
case DUB_GENERATOR_TYPE_TTS:
//TODO
break;
}
CircularBuffer_t* buffer = reinterpret_cast<CircularBuffer_t*>(track->circularBuffer);
buffer->clear();
track->state = DUB_TRACK_STATE_READY;
track->generator = DUB_GENERATOR_TYPE_UNKNOWN;
track->generatorId = 0;
return SWITCH_STATUS_SUCCESS;
}
switch_status_t remove_dub_track(dub_track_t *track) {
assert(track);
switch (track->generator) {
case DUB_GENERATOR_TYPE_HTTP:
stop_audio_download(track->generatorId);
break;
case DUB_GENERATOR_TYPE_FILE:
stop_file_load(track->generatorId);
break;
case DUB_GENERATOR_TYPE_TTS:
//TODO
break;
}
CircularBuffer_t* buffer = reinterpret_cast<CircularBuffer_t*>(track->circularBuffer);
if (buffer) {
delete buffer;
}
if (track->trackName) {
free(track->trackName);
}
memset(track, 0, sizeof(dub_track_t));
return SWITCH_STATUS_SUCCESS;
}
switch_status_t play_dub_track(dub_track_t *track, switch_mutex_t *mutex, char* url, int loop, int gain) {
bool isHttp = strncmp(url, "http", 4) == 0;
if (track->state != DUB_TRACK_STATE_READY) {
silence_dub_track(track);
}
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "play_dub_track: starting %s download: %s\n", (isHttp ? "HTTP" : "file"), url);
int id = isHttp ?
start_audio_download(url, track->sampleRate, loop, gain, mutex, (CircularBuffer_t*) track->circularBuffer) :
start_file_load(url, track->sampleRate, loop, gain, mutex, (CircularBuffer_t*) track->circularBuffer);
if (id == INVALID_DOWNLOAD_ID) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "play_dub_track: failed to start audio download\n");
switch_status_t add_track(struct cap_cb* cb, char* trackName, int sampleRate) {
Track* existingTrack = find_track_by_name(cb->tracks, trackName);
if (existingTrack) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "add_track: track %s already exists\n", trackName);
return SWITCH_STATUS_FALSE;
}
track->state = DUB_TRACK_STATE_ACTIVE;
track->generatorId = id;
track->generator = isHttp ? DUB_GENERATOR_TYPE_HTTP : DUB_GENERATOR_TYPE_FILE;
track->gain = gain;
for (int i = 0; i < MAX_DUB_TRACKS; i++) {
if (!cb->tracks[i]) {
cb->tracks[i] = new Track(trackName, sampleRate);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "add_track: added track %s at offset %d\n", trackName, i);
return SWITCH_STATUS_SUCCESS;
}
}
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "add_track: no room for track %s\n", trackName);
return SWITCH_STATUS_FALSE;
}
switch_status_t silence_dub_track(struct cap_cb* cb, char* trackName) {
Track* track = find_track_by_name(cb->tracks, trackName);
if (track) {
track->removeAllAudio();
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "silence_dub_track: silenced track %s\n", trackName);
return SWITCH_STATUS_SUCCESS;
}
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "silence_dub_track: track %s not found\n", trackName);
return SWITCH_STATUS_FALSE;
}
switch_status_t remove_dub_track(struct cap_cb* cb, char* trackName) {
for (int i = 0; i < MAX_DUB_TRACKS; i++) {
Track* track = static_cast<Track*>(cb->tracks[i]);
if (track && track->getTrackName() == trackName) {
track->removeAllAudio();
delete track;
cb->tracks[i] = nullptr;
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "remove_dub_track: removed track %s\n", trackName);
return SWITCH_STATUS_SUCCESS;
}
}
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "remove_dub_track: track %s not found\n", trackName);
return SWITCH_STATUS_FALSE;
}
switch_status_t play_dub_track(struct cap_cb* cb, char* trackName, char* url, int loop, int gain) {
bool isHttp = strncmp(url, "http", 4) == 0;
Track* track = find_track_by_name(cb->tracks, trackName);
if (!track) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "play_dub_track: track %s not found\n", trackName);
return SWITCH_STATUS_FALSE;
}
if (isHttp) {
track->queueHttpGetAudio(url, gain, loop);
}
else {
track->queueFileAudio(url, gain, loop);
}
return SWITCH_STATUS_SUCCESS;
}
switch_status_t say_dub_track(dub_track_t *track, switch_mutex_t *mutex, char* text, int gain) {
if (track->state != DUB_TRACK_STATE_READY) {
silence_dub_track(track); // wait...shouldnt we queue says?
}
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "say_dub_track: starting TTS\n");
/**
* TODO:
* This is not implemented yet. We can play TTS using using the playOnSay function
* because jambonz can generate local audio files using TTS vendors.
* However, we should probably at least implement support for elevenlabs streaming api
* here because it is so much faster.
*
*/
switch_status_t say_dub_track(struct cap_cb* cb, char* trackName, char* text, int gain) {
std::vector<std::string> headers;
std::string url, body;
Track* track = find_track_by_name(cb->tracks, trackName);
/*
track->state = DUB_TRACK_STATE_ACTIVE;
track->generatorId = id;
track->generator = DUB_GENERATOR_TYPE_TTS;
track->gain = gain;
*/
if (!track) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "play_dub_track: track %s not found\n", trackName);
return SWITCH_STATUS_FALSE;
}
if (tts_vendor_parse_text(text, url, body, headers) != SWITCH_STATUS_SUCCESS) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "say_dub_track: failed to parse text\n");
return SWITCH_STATUS_FALSE;
}
track->queueHttpPostAudio(url, body, headers, gain);
return SWITCH_STATUS_SUCCESS;
}
/* module load and unload */
switch_status_t dub_init() {
switch_status_t status;
status = init_audio_downloader();
if (status == SWITCH_STATUS_SUCCESS) {
status = init_file_loader();
}
return status;
return SWITCH_STATUS_SUCCESS;
}
switch_status_t dub_cleanup() {
switch_status_t status;
status = deinit_audio_downloader();
if (status == SWITCH_STATUS_SUCCESS) {
status = deinit_file_loader();
}
return status;
return SWITCH_STATUS_SUCCESS;
}
switch_status_t dub_session_cleanup(switch_core_session_t *session, int channelIsClosing, switch_media_bug_t *bug) {
@@ -144,17 +132,18 @@ extern "C" {
if (!switch_channel_get_private(channel, MY_BUG_NAME)) {
// race condition
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "%s Bug is not attached (race).\n", switch_channel_get_name(channel));
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "%s Bug is not attached (race).\n", switch_channel_get_name(channel));
switch_mutex_unlock(cb->mutex);
return SWITCH_STATUS_FALSE;
}
switch_channel_set_private(channel, MY_BUG_NAME, NULL);
for (int i = 0; i < MAX_DUB_TRACKS; i++) {
dub_track_t* track = &cb->tracks[i];
if (track->state != DUB_TRACK_STATE_INACTIVE) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "dub_session_cleanup: cleared track %d:%s\n", i, track->trackName);
remove_dub_track(track);
Track* track = static_cast<Track*>(cb->tracks[i]);
if (track) {
track->removeAllAudio();
delete track;
cb->tracks[i] = nullptr;
}
}
@@ -162,7 +151,7 @@ extern "C" {
switch_core_media_bug_remove(session, &bug);
}
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "dub_session_cleanup: removed bug and cleared tracks\n");
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "dub_session_cleanup: removed bug and cleared tracks\n");
switch_mutex_unlock(cb->mutex);
}
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "%s dub_session_cleanup: Bug is not attached.\n", switch_channel_get_name(channel));
@@ -175,12 +164,17 @@ extern "C" {
if (switch_mutex_trylock(cb->mutex) == SWITCH_STATUS_SUCCESS) {
/* check if any tracks are actively pushing audio */
int trackCount = 0;
/* check if any tracks have audio to contribute */
std::vector<Track*> activeTracks;
activeTracks.reserve(MAX_DUB_TRACKS);
for (int i = 0; i < MAX_DUB_TRACKS; i++) {
if (cb->tracks[i].state == DUB_TRACK_STATE_ACTIVE) trackCount++;
if (cb->tracks[i]) {
auto track = static_cast<Track*>(cb->tracks[i]);
if (track->hasAudio_NoLock()) activeTracks.push_back(static_cast<Track*>(cb->tracks[i]));
}
}
if (trackCount == 0 && cb->gain == 0) {
if (activeTracks.size() == 0 && cb->gain == 0) {
switch_mutex_unlock(cb->mutex);
return SWITCH_TRUE;
}
@@ -189,31 +183,24 @@ extern "C" {
int16_t *fp = reinterpret_cast<int16_t*>(rframe->data);
rframe->channels = 1;
rframe->datalen = rframe->samples * rframe->channels * sizeof(int16_t);
rframe->datalen = rframe->samples * sizeof(int16_t);
/* apply gain to audio in main channel if requested*/
if (cb->gain != 0) {
switch_change_sln_volume_granular(fp, rframe->samples, cb->gain);
vector_change_sln_volume_granular(fp, rframe->samples, cb->gain);
}
/* now mux in the data from tracks */
for (int i = 0; i < rframe->samples; i++) {
int16_t input = fp[i];
int16_t value = input;
for (int j = 0; j < MAX_DUB_TRACKS; j++) {
dub_track_t* track = &cb->tracks[j];
if (track->state == DUB_TRACK_STATE_ACTIVE) {
CircularBuffer_t* buffer = reinterpret_cast<CircularBuffer_t*>(track->circularBuffer);
if (buffer && !buffer->empty()) {
int16_t sample = buffer->front();
buffer->pop_front();
value += sample;
}
}
for (auto track : activeTracks) {
int16_t data[SWITCH_RECOMMENDED_BUFFER_SIZE];
memset(data, 0, sizeof(data));
auto samples = track->retrieveAndClearAudio(data, rframe->samples);
if (samples > 0) {
vector_add(fp, data, rframe->samples);
}
switch_normalize_to_16bit(value);
fp[i] = (int16_t) value;
}
vector_normalize(fp, rframe->samples);
switch_core_media_bug_set_write_replace_frame(bug, rframe);
switch_mutex_unlock(cb->mutex);
}
+5 -5
View File
@@ -4,11 +4,11 @@
switch_status_t dub_init();
switch_status_t dub_cleanup();
void init_dub_track(dub_track_t *track, char* trackName, int sampleRate);
switch_status_t silence_dub_track(dub_track_t *track);
switch_status_t remove_dub_track(dub_track_t *track);
switch_status_t play_dub_track(dub_track_t *track, switch_mutex_t *mutex, char* url, int loop, int gain);
switch_status_t say_dub_track(dub_track_t *track, switch_mutex_t *mutex, char* text, int gain);
switch_status_t add_track(struct cap_cb* cb, char* trackName, int sampleRate);
switch_status_t silence_dub_track(struct cap_cb* cb, char* trackName);
switch_status_t remove_dub_track(struct cap_cb* cb, char* trackName);
switch_status_t play_dub_track(struct cap_cb* cb, char* trackName, char* url, int loop, int gain);
switch_status_t say_dub_track(struct cap_cb* cb, char* trackName, char* text, int gain);
switch_status_t dub_session_cleanup(switch_core_session_t *session, int channelIsClosing, switch_media_bug_t *bug);
switch_bool_t dub_speech_frame(switch_media_bug_t *bug, void* user_data);
-437
View File
@@ -1,437 +0,0 @@
#include "file_loader.h"
#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 <map>
#include <mpg123.h>
#define INIT_BUFFER_SIZE (80000)
#define BUFFER_GROW_SIZE (80000)
#define BUFFER_THROTTLE_LOW (40000)
#define BUFFER_THROTTLE_HIGH (160000)
static uint16_t currDownloadId = 0;
typedef enum
{
STATUS_NONE = 0,
STATUS_FAILED,
STATUS_FILE_IN_PROGRESS,
STATUS_FILE_PAUSED,
STATUS_FILE_COMPLETE,
STATUS_AWAITING_RESTART,
STATUS_STOPPING,
STATUS_STOPPED
} Status_t;
typedef enum {
FILE_TYPE_MP3 = 0,
FILE_TYPE_R8
} FileType_t;
static const char* status2String(Status_t status)
{
static const char* statusStrings[] = {
"STATUS_NONE",
"STATUS_FAILED",
"STATUS_FILE_IN_PROGRESS",
"STATUS_FILE_PAUSED",
"STATUS_FILE_COMPLETE",
"STATUS_AWAITING_RESTART",
"STATUS_STOPPING",
"STATUS_STOPPED"
};
if (status >= 0 && status < sizeof(statusStrings) / sizeof(statusStrings[0]))
{
return statusStrings[status];
}
else
{
return "UNKNOWN_STATUS";
}
}
typedef struct
{
switch_mutex_t* mutex;
CircularBuffer_t* buffer;
mpg123_handle *mh;
FILE* fp;
char* path;
bool loop;
int rate;
boost::asio::deadline_timer *timer;
Status_t status;
FileType_t type;
downloadId_t id;
int gain;
} FileInfo_t;
typedef std::map<int32_t, FileInfo_t *> Id2FileMap_t;
static Id2FileMap_t id2FileMap;
static boost::object_pool<FileInfo_t> pool ;
static boost::asio::io_service io_service;
static std::thread worker_thread;
/* forward declarations */
static FileInfo_t* createFileLoader(const char *path, int rate, int loop, int gain, mpg123_handle *mhm, switch_mutex_t *mutex, CircularBuffer_t *buffer);
static void destroyFileInfo(FileInfo_t *finfo);
static void threadFunc();
static std::vector<int16_t> convert_mp3_to_linear(FileInfo_t *file, int8_t *data, size_t len);
static void read_cb(const boost::system::error_code& error, FileInfo_t* finfo) ;
/* apis */
extern "C" {
switch_status_t init_file_loader() {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "init_file_loader loading..\n");
if (mpg123_init() != MPG123_OK) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "init_file_loader: failed to initiate MPG123");
return SWITCH_STATUS_FALSE;
}
/* start worker thread */
std::thread t(threadFunc) ;
worker_thread.swap( t ) ;
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "init_file_loader: loaded\n");
return SWITCH_STATUS_SUCCESS;
}
switch_status_t deinit_file_loader() {
/* stop the ASIO IO service */
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "deinit_file_loader: stopping io service\n");
io_service.stop();
/* Join the worker thread */
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "deinit_file_loader: wait for worker thread to complete\n");
if (worker_thread.joinable()) {
worker_thread.join();
}
mpg123_exit();
return SWITCH_STATUS_SUCCESS;
}
downloadId_t start_file_load(const char* path, int rate, int loop, int gain, switch_mutex_t* mutex, CircularBuffer_t* buffer) {
int mhError = 0;
/* we only handle mp3 or r8 files atm */
const char *ext = strrchr(path, '.');
if (!ext) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "start_file_load: file %s has no extension\n", path);
return INVALID_DOWNLOAD_ID;
}
ext++;
if (0 != strcmp(ext, "mp3") && 0 != strcmp(ext, "r8")) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "start_file_load: file %s has unsupported extension %s\n", path, ext);
return INVALID_DOWNLOAD_ID;
}
/* allocate handle for mpeg decoding */
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 INVALID_DOWNLOAD_ID;
}
if (mpg123_open_feed(mh) != MPG123_OK) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error mpg123_open_feed!\n");
return INVALID_DOWNLOAD_ID;
}
if (mpg123_format_all(mh) != MPG123_OK) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error mpg123_format_all!\n");
return INVALID_DOWNLOAD_ID;
}
if (mpg123_param(mh, MPG123_FORCE_RATE, rate, 0) != MPG123_OK) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error forcing resample to 8k!\n");
return INVALID_DOWNLOAD_ID;
}
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 INVALID_DOWNLOAD_ID;
}
FileInfo_t* finfo = createFileLoader(path, rate, loop, gain, mh, mutex, buffer);
if (!finfo) {
return INVALID_DOWNLOAD_ID;
}
/* do the initial read in the worker thread so we don't block here */
finfo->timer->expires_from_now(boost::posix_time::millisec(1));
finfo->timer->async_wait(boost::bind(&read_cb, boost::placeholders::_1, finfo));
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO,
"start_file_load: starting load %d\n", finfo->id);
return finfo->id;
}
switch_status_t stop_file_load(int id) {
auto it = id2FileMap.find(id);
if (it == id2FileMap.end()) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "stop_file_load: id %d has already completed\n", id);
return SWITCH_STATUS_FALSE;
}
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO,
"stop_audio_download: stopping download %d, status %s\n", id, status2String(it->second->status));
FileInfo_t *finfo = it->second;
auto status = finfo->status;
/* past this point I shall not access either the mutex or the buffer provided */
finfo->mutex = nullptr;
finfo->buffer = nullptr;
destroyFileInfo(finfo);
finfo->status = Status_t::STATUS_STOPPED;
return SWITCH_STATUS_SUCCESS;
}
}
/* internal */
FileInfo_t* createFileLoader(const char *path, int rate, int loop, int gain, mpg123_handle *mh, switch_mutex_t *mutex, CircularBuffer_t *buffer) {
FileInfo_t *finfo = pool.malloc() ;
const char *ext = strrchr(path, '.');
memset(finfo, 0, sizeof(FileInfo_t));
finfo->mutex = mutex;
finfo->buffer = buffer;
finfo->mh = mh;
finfo->loop = loop;
finfo->gain = gain;
finfo->rate = rate;
finfo->path = strdup(path);
finfo->status = Status_t::STATUS_NONE;
finfo->timer = new boost::asio::deadline_timer(io_service);
if (0 == strcmp(ext, "mp3")) finfo->type = FileType_t::FILE_TYPE_MP3;
else if (0 == strcmp(ext, "r8")) finfo->type = FileType_t::FILE_TYPE_R8;
downloadId_t id = ++currDownloadId;
if (id == 0) id++;
id2FileMap[id] = finfo;
finfo->id = id;
finfo->status = Status_t::STATUS_AWAITING_RESTART;
finfo->fp = fopen(finfo->path, "rb");
if (finfo->fp == NULL) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "createFileLoader: failed to open file %s\n", finfo->path);
destroyFileInfo(finfo);
return nullptr;
}
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG,
"createFileLoader: launched request, loop %s, gain %d\n", (finfo->loop ? "yes": "no"), finfo->gain);
return finfo;
}
void destroyFileInfo(FileInfo_t *finfo) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "destroyFileInfo\n");
/* clear asio resources and free resources */
if (finfo->timer) {
finfo->timer->cancel();
delete finfo->timer;
}
/* free mp3 decoder */
if (finfo->mh) {
mpg123_close(finfo->mh);
mpg123_delete(finfo->mh);
}
if (finfo->path) {
free(finfo->path);
}
if (finfo->mutex) switch_mutex_lock(finfo->mutex);
id2FileMap.erase(finfo->id);
if (finfo->mutex) switch_mutex_unlock(finfo->mutex);
memset(finfo, 0, sizeof(FileInfo_t));
pool.destroy(finfo) ;
}
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, "file_loader threadFunc - starting\n");
for(;;) {
try {
io_service.run() ;
break ;
}
catch( std::exception& e) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "file_loader threadFunc - Error: %s\n", e.what());
}
}
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "file_loader threadFunc - ending\n");
}
std::vector<int16_t> convert_mp3_to_linear(FileInfo_t *finfo, int8_t *data, size_t len) {
std::vector<int16_t> linear_data;
int eof = 0;
int mp3err = 0;
if(mpg123_feed(finfo->mh, (const unsigned char*) data, len) == MPG123_OK) {
while(!eof) {
size_t usedlen = 0;
off_t frame_offset;
unsigned char* audio;
int decode_status = mpg123_decode_frame(finfo->mh, &frame_offset, &audio, &usedlen);
switch(decode_status) {
case MPG123_NEW_FORMAT:
continue;
case MPG123_OK:
{
size_t samples = usedlen / sizeof(int16_t);
linear_data.insert(linear_data.end(), reinterpret_cast<int16_t*>(audio), reinterpret_cast<int16_t*>(audio) + samples);
}
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;
}
if (finfo->gain != 0) {
switch_change_sln_volume_granular(linear_data.data(), linear_data.size(), finfo->gain);
}
}
return linear_data;
}
void read_cb(const boost::system::error_code& error, FileInfo_t* finfo) {
if (finfo->status == Status_t::STATUS_STOPPING || !finfo->mutex || !finfo->buffer) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %u session gone\n", finfo->id);
return;
}
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %u status is %s\n", finfo->id, status2String(finfo->status));
if (finfo->status == Status_t::STATUS_AWAITING_RESTART) {
finfo->status = Status_t::STATUS_FILE_IN_PROGRESS;
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %u starting initial read of file\n", finfo->id);
}
if (!error) {
size_t size = 0;
switch_mutex_lock(finfo->mutex);
size = finfo->buffer->size();
switch_mutex_unlock(finfo->mutex);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %u size is now %ld\n", finfo->id, size);
if (size < BUFFER_THROTTLE_LOW) {
std::vector<int16_t> pcm_data;
int8_t buf[INIT_BUFFER_SIZE];
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %u reading data\n", finfo->id);
size_t bytesRead = ::fread(buf, sizeof(int8_t), INIT_BUFFER_SIZE, finfo->fp);
if (bytesRead <= 0) {
if (::feof(finfo->fp)) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %u eof\n", finfo->id);
}
else if (::ferror(finfo->fp)) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "read_cb: %u error reading file\n", finfo->id);
}
else {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "read_cb: %u unknown error reading file\n", finfo->id);
}
finfo->status = Status_t::STATUS_FILE_COMPLETE;
return;
}
if (finfo->type == FileType_t::FILE_TYPE_MP3) {
pcm_data = convert_mp3_to_linear(finfo, buf, bytesRead);
} else {
pcm_data = std::vector<int16_t>(reinterpret_cast<int16_t*>(buf), reinterpret_cast<int16_t*>(buf) + bytesRead / 2);
}
switch_mutex_lock(finfo->mutex);
// Resize the buffer if necessary
if (finfo->buffer->capacity() - finfo->buffer->size() < pcm_data.size()) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "write_cb %u growing buffer, size now %ld\n", finfo->id, finfo->buffer->size());
finfo->buffer->set_capacity(finfo->buffer->size() + std::max(pcm_data.size(), (size_t)BUFFER_GROW_SIZE));
}
/* Push the data into the buffer */
finfo->buffer->insert(finfo->buffer->end(), pcm_data.data(), pcm_data.data() + pcm_data.size());
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %u wrote data, buffer size is now %ld\n", finfo->id, finfo->buffer->size());
switch_mutex_unlock(finfo->mutex);
if (bytesRead < INIT_BUFFER_SIZE) {
finfo->status = Status_t::STATUS_FILE_COMPLETE;
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %u reached end of file, status is %s\n", finfo->id, status2String(finfo->status));
}
}
if (finfo->status == Status_t::STATUS_FILE_COMPLETE && finfo->loop) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %u looping\n", finfo->id);
::fseek(finfo->fp, 0, SEEK_SET);
finfo->status = Status_t::STATUS_AWAITING_RESTART;
}
if (finfo->status != Status_t::STATUS_FILE_COMPLETE) {
// read more in 2 seconds
finfo->timer->expires_from_now(boost::posix_time::millisec(2000));
finfo->timer->async_wait(boost::bind(&read_cb, boost::placeholders::_1, finfo));
}
else {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %u file complete, status %s loop %s\n",
finfo->id, status2String(finfo->status), (finfo->loop ? "yes" : "no"));
}
} else {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "read_cb: %u error (%d): %s\n", finfo->id, error.value(), error.message().c_str());
// Handle any errors
}
}
-18
View File
@@ -1,18 +0,0 @@
#ifndef __FILE_LOADER_H__
#define __FILE_LOADER_H__
#include <switch.h>
#include "common.h"
extern "C" {
switch_status_t init_file_loader();
switch_status_t deinit_file_loader();
int start_file_load(const char* path, int rate, int loop, int gain, switch_mutex_t* mutex, CircularBuffer_t* buffer);
switch_status_t stop_file_load(int id);
}
#endif
+31 -83
View File
@@ -8,6 +8,7 @@
#include <switch.h>
#include <switch_curl.h>
#include "dub_glue.h"
#include <string.h>
/* Prototypes */
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_dub_shutdown);
@@ -88,7 +89,6 @@ static switch_status_t dub_add_track(switch_core_session_t *session, char* track
switch_channel_t *channel = switch_core_session_get_channel(session);
switch_media_bug_t *bug = NULL;
struct cap_cb *cb = NULL;
int offset = 0;
int samples_per_second;
switch_codec_implementation_t write_impl = { 0 };
@@ -102,36 +102,32 @@ static switch_status_t dub_add_track(switch_core_session_t *session, char* track
cb =(struct cap_cb *) switch_core_session_alloc(session, sizeof(struct cap_cb));
memset(cb, 0, sizeof(struct cap_cb));
switch_mutex_init(&cb->mutex, SWITCH_MUTEX_NESTED, switch_core_session_get_pool(session));
offset = 0;
}
else {
/* retrieve the bug and search for an empty track */
cb = (struct cap_cb *) switch_core_media_bug_get_user_data(bug);
while (offset < MAX_DUB_TRACKS && cb->tracks[offset].state != DUB_TRACK_STATE_INACTIVE) {
offset++;
}
if (offset == MAX_DUB_TRACKS) {
/* all tracks are in use */
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "dub_add_track: no available tracks\n");
return SWITCH_STATUS_FALSE;
}
}
switch_core_session_get_write_impl(session, &write_impl);
samples_per_second = !strcasecmp(write_impl.iananame, "g722") ? write_impl.actual_samples_per_second : write_impl.samples_per_second;
init_dub_track(&cb->tracks[offset], trackName, samples_per_second);
switch_mutex_lock(cb->mutex);
if (add_track(cb, trackName, samples_per_second) != SWITCH_STATUS_SUCCESS) {
switch_mutex_unlock(cb->mutex);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "dub_add_track: error adding track %s\n", trackName);
return SWITCH_STATUS_FALSE;
}
if (!bug) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "dub_add_track: adding bug for track %s\n", trackName);
if (switch_core_media_bug_add(session, MY_BUG_NAME, NULL, capture_callback, (void *) cb, 0, SMBF_WRITE_REPLACE, &bug) != SWITCH_STATUS_SUCCESS) {
switch_mutex_unlock(cb->mutex);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "dub_add_track: error adding bug!\n");
return SWITCH_STATUS_FALSE;
}
switch_channel_set_private(channel, MY_BUG_NAME, bug);
}
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "dub_add_track: added track %s at offset %d\n", trackName, offset);
switch_mutex_unlock(cb->mutex);
return SWITCH_STATUS_SUCCESS;
}
@@ -140,39 +136,17 @@ static switch_status_t dub_remove_track(switch_core_session_t *session, char* tr
switch_channel_t *channel = switch_core_session_get_channel(session);
switch_media_bug_t *bug = (switch_media_bug_t*) switch_channel_get_private(channel, MY_BUG_NAME);
switch_status_t status = SWITCH_STATUS_FALSE;
dub_track_t *track = NULL;
if (bug) {
struct cap_cb *cb =(struct cap_cb *) switch_core_media_bug_get_user_data(bug);
switch_mutex_lock(cb->mutex);
for (int i = 0; i < MAX_DUB_TRACKS && track == NULL; i++) {
if (cb->tracks[i].state != DUB_TRACK_STATE_INACTIVE && strcmp(cb->tracks[i].trackName, trackName) == 0) {
track = &cb->tracks[i];
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "dub_remove_track: removing track %s at offset %d\n", trackName, i);
break;
}
}
if (track) {
int count = 0;
remove_dub_track(track);
/* check if this is the last bug */
for (int i = 0; i < MAX_DUB_TRACKS; i++) {
if (cb->tracks[i].state != DUB_TRACK_STATE_INACTIVE) count++;
}
if (count == 0) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "dub_remove_track: removing bug after removing last track\n");
dub_session_cleanup(session, 0, bug);
}
status = SWITCH_STATUS_SUCCESS;
}
else {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "dub_remove_track: track %s not found\n", trackName);
}
status = remove_dub_track(cb, trackName);
switch_mutex_unlock(cb->mutex);
if (status != SWITCH_STATUS_SUCCESS) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "dub_remove_track: error removing track %s\n", trackName);
return SWITCH_STATUS_FALSE;
}
}
else {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "dub_remove_track: bug not found\n");
@@ -185,24 +159,14 @@ static switch_status_t dub_silence_track(switch_core_session_t *session, char* t
switch_channel_t *channel = switch_core_session_get_channel(session);
switch_media_bug_t *bug = (switch_media_bug_t*) switch_channel_get_private(channel, MY_BUG_NAME);
switch_status_t status = SWITCH_STATUS_FALSE;
dub_track_t *track = NULL;
if (bug) {
struct cap_cb *cb =(struct cap_cb *) switch_core_media_bug_get_user_data(bug);
switch_mutex_lock(cb->mutex);
for (int i = 0; i < MAX_DUB_TRACKS; i++) {
if (cb->tracks[i].state != DUB_TRACK_STATE_INACTIVE && strcmp(cb->tracks[i].trackName, trackName) == 0) {
track = &cb->tracks[i];
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "dub_silence_track: silencing track %s at offset %d\n", trackName, i);
break;
}
status = silence_dub_track(cb, trackName);
if (status != SWITCH_STATUS_SUCCESS) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "dub_silence_track: error silencing track %s\n", trackName);
return SWITCH_STATUS_FALSE;
}
if (track) {
silence_dub_track(track);
status = SWITCH_STATUS_SUCCESS;
}
switch_mutex_unlock(cb->mutex);
}
else {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "dub_silence_track: bug not found\n");
@@ -215,24 +179,14 @@ static switch_status_t dub_play_on_track(switch_core_session_t *session, char* t
switch_channel_t *channel = switch_core_session_get_channel(session);
switch_media_bug_t *bug = (switch_media_bug_t*) switch_channel_get_private(channel, MY_BUG_NAME);
switch_status_t status = SWITCH_STATUS_FALSE;
dub_track_t *track = NULL;
if (bug) {
struct cap_cb *cb =(struct cap_cb *) switch_core_media_bug_get_user_data(bug);
switch_mutex_lock(cb->mutex);
for (int i = 0; i < MAX_DUB_TRACKS; i++) {
if (cb->tracks[i].state != DUB_TRACK_STATE_INACTIVE && strcmp(cb->tracks[i].trackName, trackName) == 0) {
track = &cb->tracks[i];
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO,
"dub_play_on_track: playing %s on track %s at offset %d with gain %d\n", url, trackName, i, gain);
break;
}
status = play_dub_track(cb, trackName, url, loop, gain);
if (status != SWITCH_STATUS_SUCCESS) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "dub_play_on_track: error playing %s on track %s\n", url, trackName);
return SWITCH_STATUS_FALSE;
}
if (track) {
status = play_dub_track(track, cb->mutex, url, loop, gain);
}
switch_mutex_unlock(cb->mutex);
}
return status;
}
@@ -241,37 +195,30 @@ static switch_status_t dub_say_on_track(switch_core_session_t *session, char* tr
switch_channel_t *channel = switch_core_session_get_channel(session);
switch_media_bug_t *bug = (switch_media_bug_t*) switch_channel_get_private(channel, MY_BUG_NAME);
switch_status_t status = SWITCH_STATUS_FALSE;
dub_track_t *track = NULL;
if (bug) {
struct cap_cb *cb =(struct cap_cb *) switch_core_media_bug_get_user_data(bug);
switch_mutex_lock(cb->mutex);
for (int i = 0; i < MAX_DUB_TRACKS; i++) {
if (cb->tracks[i].state != DUB_TRACK_STATE_INACTIVE && strcmp(cb->tracks[i].trackName, trackName) == 0) {
track = &cb->tracks[i];
break;
}
status = say_dub_track(cb, trackName, text, gain);
if (status != SWITCH_STATUS_SUCCESS) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "dub_say_on_track: error saying %s on track %s\n", text, trackName);
return SWITCH_STATUS_FALSE;
}
if (track) {
status = say_dub_track(track, cb->mutex, text, gain);
}
switch_mutex_unlock(cb->mutex);
}
return status;
}
#define DUB_API_SYNTAX "<uuid> [addTrack|removeTrack|silenceTrack|playOnTrack|sayOnTrack|setGain] track [url|text|gain] [gain] [loop]"
#define MAX_PARAMS 6
SWITCH_STANDARD_API(dub_function)
{
char *mycmd = NULL, *argv[6] = { 0 };
char *mycmd = NULL, *argv[MAX_PARAMS] = { 0 };
int argc = 0;
int error_written = 0;
switch_status_t status = SWITCH_STATUS_FALSE;
if (!zstr(cmd) && (mycmd = strdup(cmd))) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "dub_function: %s\n", mycmd);
argc = switch_separate_string(mycmd, ' ', argv, (sizeof(argv) / sizeof(argv[0])));
argc = switch_separate_string(mycmd, ' ', argv, (sizeof(argv) / sizeof(argv[0])));
}
if (zstr(cmd) || argc < 3 || zstr(argv[0]) || zstr(argv[1]) || zstr(argv[2])) {
@@ -302,6 +249,7 @@ SWITCH_STANDARD_API(dub_function)
}
else if (0 == strcmp(action, "playOnTrack")) {
if (argc < 4) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "playOnTrack requires a url\n");
stream->write_function(stream, "-USAGE: %s\n", DUB_API_SYNTAX);
error_written = 1;
}
+1 -32
View File
@@ -17,42 +17,11 @@
/* per-channel data */
typedef void (*responseHandler_t)(switch_core_session_t* session, const char* json, const char* bugname, const char* details);
typedef enum {
DUB_TRACK_STATE_INACTIVE = 0,
DUB_TRACK_STATE_READY,
DUB_TRACK_STATE_ACTIVE,
DUB_TRACK_STATE_PAUSED
} dub_state_t;
typedef enum {
DUB_GENERATOR_TYPE_UNKNOWN = 0,
DUB_GENERATOR_TYPE_HTTP,
DUB_GENERATOR_TYPE_FILE,
DUB_GENERATOR_TYPE_TTS
} dub_generator_t;
typedef enum {
DUB_TRACK_EVENT_PLAY = 0,
DUB_TRACK_EVENT_STOP,
DUB_TRACK_EVENT_PAUSE,
DUB_TRACK_EVENT_RESUME
} dub_event_t;
typedef struct dub_track {
dub_state_t state;
dub_generator_t generator;
char* trackName;
int sampleRate;
int gain;
void* circularBuffer;
int generatorId;
} dub_track_t;
struct cap_cb {
switch_mutex_t *mutex;
int gain;
dub_track_t tracks[MAX_DUB_TRACKS];
void *tracks[MAX_DUB_TRACKS];
};
+58
View File
@@ -0,0 +1,58 @@
#include "mpg_decode.h"
#include "vector_math.h"
std::vector<int16_t> convert_mp3_to_linear(mpg123_handle *mh, int gain, int8_t *data, size_t len) {
std::vector<int16_t> linear_data;
int eof = 0;
int mp3err = 0;
if(mpg123_feed(mh, (const unsigned char*) data, len) == MPG123_OK) {
while(!eof) {
size_t usedlen = 0;
off_t frame_offset;
unsigned char* audio;
int decode_status = mpg123_decode_frame(mh, &frame_offset, &audio, &usedlen);
switch(decode_status) {
case MPG123_NEW_FORMAT:
continue;
case MPG123_OK:
{
size_t samples = usedlen / sizeof(int16_t);
linear_data.insert(linear_data.end(), reinterpret_cast<int16_t*>(audio), reinterpret_cast<int16_t*>(audio) + samples);
}
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;
}
if (gain != 0) {
vector_change_sln_volume_granular(linear_data.data(), linear_data.size(), gain);
}
}
else {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error feeding data to mpg123\n");
}
return linear_data;
}
+11
View File
@@ -0,0 +1,11 @@
#ifndef MPG_DECODE_H
#define MPG_DECODE_H
#include <vector>
#include <mpg123.h>
#include "switch.h"
std::vector<int16_t> convert_mp3_to_linear(mpg123_handle *mh, int gain, int8_t *data, size_t len);
#endif
+143
View File
@@ -0,0 +1,143 @@
#include "track.h"
#include "ap_file.h"
#include "ap_http.h"
#include "switch.h"
#define INIT_BUFFER_SIZE (80000)
Track::Track(const std::string& trackName, int sampleRate) : _trackName(trackName), _sampleRate(sampleRate),
_buffer(INIT_BUFFER_SIZE), _stopping(false)
{
}
Track::~Track() {
removeAllAudio();
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "Track::~Track: track %s\n", _trackName.c_str());
}
/**
* @brief called when an audio producer has finished retrieving the audio.
* If we have another producer queued, then start it.
*
* @param hasError
* @param errMsg
*/
void Track::onPlayDone(bool hasError, const std::string& errMsg) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "Track::onPlayDone %s\n", _trackName.c_str());
if (!_stopping) {
if (hasError) switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "onPlayDone: error: %s\n", errMsg.c_str());
std::lock_guard<std::mutex> lock(_mutex);
_apQueue.pop();
if (!_apQueue.empty()) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "onPlayDone: starting queued audio on track %s\n", _trackName.c_str());
_apQueue.front()->start(std::bind(&Track::onPlayDone, this, std::placeholders::_1, std::placeholders::_2));
}
}
else {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "onPlayDone: track %s stopping\n", _trackName.c_str());
}
}
void Track::queueFileAudio(const std::string& path, int gain, bool loop) {
bool startIt = false;
if (_stopping) return;
auto ap = std::make_shared<AudioProducerFile>(_mutex, _buffer, _sampleRate);
ap->queueFileAudio(path, gain, loop);
{
std::lock_guard<std::mutex> lock(_mutex);
_apQueue.push(ap);
startIt = _apQueue.size() == 1;
}
if (startIt) {
try {
ap->start(std::bind(&Track::onPlayDone, this, std::placeholders::_1, std::placeholders::_2));
} catch (std::exception& e) {
onPlayDone(true, e.what());
}
}
}
void Track::queueHttpGetAudio(const std::string& url, int gain, bool loop) {
bool startIt = false;
if (_stopping) return;
auto ap = std::make_shared<AudioProducerHttp>(_mutex, _buffer, _sampleRate);
ap->queueHttpGetAudio(url, gain, loop);
{
std::lock_guard<std::mutex> lock(_mutex);
_apQueue.push(ap);
startIt = _apQueue.size() == 1;
}
if (startIt) {
try {
ap->start(std::bind(&Track::onPlayDone, this, std::placeholders::_1, std::placeholders::_2));
} catch (std::exception& e) {
onPlayDone(true, e.what());
}
}
}
void Track::queueHttpPostAudio(const std::string& url, int gain, bool loop) {
bool startIt = false;
if (_stopping) return;
auto ap = std::make_shared<AudioProducerHttp>(_mutex, _buffer, _sampleRate);
ap->queueHttpPostAudio(url, gain, loop);
{
std::lock_guard<std::mutex> lock(_mutex);
_apQueue.push(ap);
startIt = _apQueue.size() == 1;
}
if (startIt) {
try {
ap->start(std::bind(&Track::onPlayDone, this, std::placeholders::_1, std::placeholders::_2));
} catch (std::exception& e) {
onPlayDone(true, e.what());
}
}
}
void Track::queueHttpPostAudio(const std::string& url, const std::string& body, std::vector<std::string>& headers, int gain, bool loop) {
bool startIt = false;
if (_stopping) return;
auto ap = std::make_shared<AudioProducerHttp>(_mutex, _buffer, _sampleRate);
ap->queueHttpPostAudio(url, body, headers, gain, loop);
{
std::lock_guard<std::mutex> lock(_mutex);
_apQueue.push(ap);
startIt = _apQueue.size() == 1;
}
if (startIt) {
try {
ap->start(std::bind(&Track::onPlayDone, this, std::placeholders::_1, std::placeholders::_2));
} catch (std::exception& e) {
onPlayDone(true, e.what());
}
}
}
bool Track::hasAudio() {
if (_stopping) return false;
std::lock_guard<std::mutex> lock(_mutex);
return hasAudio_NoLock();
}
void Track::removeAllAudio() {
_stopping = true;
std::queue<std::shared_ptr<AudioProducer>> apQueueCopy;
{
std::lock_guard<std::mutex> lock(_mutex);
apQueueCopy = _apQueue;
_apQueue = std::queue<std::shared_ptr<AudioProducer>>();
}
while (!apQueueCopy.empty()) {
auto ap = apQueueCopy.front();
apQueueCopy.pop();
ap->stop();
}
}
+52
View File
@@ -0,0 +1,52 @@
#ifndef __TRACK_H__
#define __TRACK_H__
#include <mutex>
#include <queue>
#include "common.h"
#include "ap.h"
class Track {
public:
Track(const std::string& trackName, int sampleRate);
~Track();
/* audio production methods */
void queueHttpGetAudio(const std::string& url, int gain = 0, bool loop = false);
void queueHttpPostAudio(const std::string& url, int gain = 0, bool loop = false);
void queueHttpPostAudio(const std::string& url, const std::string& body, std::vector<std::string>& headers, int gain = 0, bool loop = false);
void queueFileAudio(const std::string& path, int gain = 0, bool loop = false);
void removeAllAudio();
void onPlayDone(bool hasError, const std::string& errMsg);
std::string& getTrackName() { return _trackName; }
/* audio playout methods */
bool hasAudio();
inline bool hasAudio_NoLock() const {
return !_stopping && !_buffer.empty();
}
int retrieveAndClearAudio(int16_t* buf, int desiredSamples) {
std::lock_guard<std::mutex> lock(_mutex);
int samplesToCopy = std::min(static_cast<int>(_buffer.size()), desiredSamples);
std::copy_n(_buffer.begin(), samplesToCopy, buf);
_buffer.erase(_buffer.begin(), _buffer.begin() + samplesToCopy);
return samplesToCopy;
}
private:
std::string _trackName;
int _sampleRate;
std::mutex _mutex;
CircularBuffer_t _buffer;
std::queue<std::shared_ptr<AudioProducer>> _apQueue;
bool _stopping;
};
#endif
+118
View File
@@ -0,0 +1,118 @@
#include "tts_vendor_parser.h"
#include <sstream>
#include <switch_json.h>
#include <map>
switch_status_t elevenlabs_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;
std::string voice_name;
std::string model_id;
std::string similarity_boost;
std::string stability;
std::string style;
std::string use_speaker_boost;
std::string optimize_streaming_latency;
for (const auto& pair : params) {
if (pair.first == "api_key") {
api_key = pair.second;
} else if (pair.first == "voice") {
voice_name = pair.second;
} else if (pair.first == "model_id") {
model_id = pair.second;
} else if (pair.first == "similarity_boost") {
similarity_boost = pair.second;
} else if (pair.first == "stability") {
stability = pair.second;
} else if (pair.first == "style") {
style = pair.second;
} else if (pair.first == "use_speaker_boost") {
use_speaker_boost = pair.second;
} else if (pair.first == "modeloptimize_streaming_latency_id") {
optimize_streaming_latency = pair.second;
}
}
if (api_key.empty()) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "elevenlabs_parse_text: no api_key provided\n");
return SWITCH_STATUS_FALSE;
}
if (model_id.empty()) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "elevenlabs_parse_text: no model_id provided\n");
return SWITCH_STATUS_FALSE;
}
if (optimize_streaming_latency.empty()) {
optimize_streaming_latency = "2";
}
// URL
std::ostringstream url_stream;
url_stream << "https://api.elevenlabs.io/v1/text-to-speech/" << voice_name << "/stream?";
url_stream << "optimize_streaming_latency=" << optimize_streaming_latency << "&output_format=mp3_44100_128";
url = url_stream.str();
/* create the JSON body */
cJSON * jResult = cJSON_CreateObject();
cJSON_AddStringToObject(jResult, "model_id", model_id.c_str());
cJSON_AddStringToObject(jResult, "text", text.c_str());
if (!similarity_boost.empty() || !style.empty() || !use_speaker_boost.empty() || !stability.empty()) {
cJSON * jVoiceSettings = cJSON_CreateObject();
cJSON_AddItemToObject(jResult, "voice_settings", jVoiceSettings);
if (!similarity_boost.empty()) {
cJSON_AddStringToObject(jVoiceSettings, "similarity_boost", similarity_boost.c_str());
}
if (!style.empty()) {
cJSON_AddStringToObject(jVoiceSettings, "style", style.c_str());
}
if (!use_speaker_boost.empty()) {
cJSON_AddStringToObject(jVoiceSettings, "use_speaker_boost", use_speaker_boost.c_str());
}
if (!stability.empty()) {
cJSON_AddStringToObject(jVoiceSettings, "stability", stability.c_str());
}
}
char* _body = cJSON_PrintUnformatted(jResult);
body = _body;
cJSON_Delete(jResult);
free(_body);
// Create headers
headers.push_back("xi-api-key: " + api_key);
headers.push_back("Content-Type: application/json");
return SWITCH_STATUS_SUCCESS;
}
switch_status_t tts_vendor_parse_text(const std::string& say, std::string& url, std::string& body, std::vector<std::string>& headers) {
size_t start = say.find("{") + 1;
size_t end = say.find("}");
std::string text = say.substr(end + 1);
std::string params_string = say.substr(start, end - start);
std::istringstream ss(params_string);
std::map<std::string, std::string> params;
while (ss.good()) {
std::string substr;
getline(ss, substr, ',');
substr.erase(0, substr.find_first_not_of(' '));
size_t equal_pos = substr.find("=");
std::string key = substr.substr(0, equal_pos);
std::string value = substr.substr(equal_pos + 1, substr.size());
params[key] = value;
}
if (params["vendor"] == "elevenlabs") {
return elevenlabs_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 text\n");
return SWITCH_STATUS_FALSE;
}
}
+12
View File
@@ -0,0 +1,12 @@
#ifndef __MOD_DUB_TTS_VENDOR_PARSER_H__
#define __MOD_DUB_TTS_VENDOR_PARSER_H__
#include <string>
#include <vector>
#include <switch.h>
#include "common.h"
switch_status_t tts_vendor_parse_text(const std::string& say, std::string& url, std::string& body, std::vector<std::string>& headers);
#endif
+211
View File
@@ -0,0 +1,211 @@
#include "vector_math.h"
#include <assert.h>
#include <string.h>
#define GRANULAR_VOLUME_MAX (50)
#define SMAX 32767
#define SMIN (-32768)
#define normalize_to_16bit_basic(n) if (n > SMAX) n = SMAX; else if (n < SMIN) n = SMIN;
#define normalize_volume_granular(x) if (x > GRANULAR_VOLUME_MAX) x = GRANULAR_VOLUME_MAX; if (x < -GRANULAR_VOLUME_MAX) x = -GRANULAR_VOLUME_MAX;
#ifdef __cplusplus
extern "C" {
#endif
#if defined(USE_AVX2)
#include <immintrin.h>
#pragma message("Using AVX2 SIMD.")
void vector_add(int16_t* a, int16_t* b, size_t len) {
size_t i = 0;
for (; i + 15 < len; i += 16) {
__m256i va = _mm256_loadu_si256((const __m256i*)(a + i));
__m256i vb = _mm256_loadu_si256((const __m256i*)(b + i));
__m256i vc = _mm256_add_epi16(va, vb);
_mm256_storeu_si256((__m256i*)(a + i), vc);
}
for (; i < len; ++i) {
a[i] += b[i];
}
}
void vector_normalize(int16_t* a, size_t len) {
__m256i max_val = _mm256_set1_epi16(SMAX);
__m256i min_val = _mm256_set1_epi16(SMIN);
size_t i = 0;
for (; i + 15 < len; i += 16) {
__m256i values = _mm256_loadu_si256((__m256i*)(a + i));
__m256i gt_max = _mm256_cmpgt_epi16(values, max_val);
__m256i lt_min = _mm256_cmpgt_epi16(min_val, values);
values = _mm256_blendv_epi8(values, max_val, gt_max);
values = _mm256_blendv_epi8(values, min_val, lt_min);
_mm256_storeu_si256((__m256i*)(a + i), values);
}
// Process remaining elements
for (; i < len; ++i) {
if (a[i] > SMAX) a[i] = SMAX;
else if (a[i] < SMIN) a[i] = SMIN;
}
}
typedef union {
int16_t* data;
__m256i* fp_avx2;
} vector_data_t;
void vector_change_sln_volume_granular(int16_t* data, uint32_t samples, int32_t vol) {
float newrate = 0;
static const float pos[GRANULAR_VOLUME_MAX] = {
1.122018, 1.258925, 1.412538, 1.584893, 1.778279, 1.995262, 2.238721, 2.511887, 2.818383, 3.162278,
3.548134, 3.981072, 4.466835, 5.011872, 5.623413, 6.309574, 7.079458, 7.943282, 8.912509, 10.000000,
11.220183, 12.589254, 14.125375, 15.848933, 17.782795, 19.952621, 22.387213, 25.118862, 28.183832, 31.622776,
35.481335, 39.810719, 44.668358, 50.118729, 56.234131, 63.095726, 70.794586, 79.432816, 89.125107, 100.000000,
112.201836, 125.892517, 141.253784, 158.489334, 177.827942, 199.526215, 223.872070, 251.188705, 281.838318, 316.227753
};
static const float neg[GRANULAR_VOLUME_MAX] = {
0.891251, 0.794328, 0.707946, 0.630957, 0.562341, 0.501187, 0.446684, 0.398107, 0.354813, 0.316228,
0.281838, 0.251189, 0.223872, 0.199526, 0.177828, 0.158489, 0.141254, 0.125893, 0.112202, 0.100000,
0.089125, 0.079433, 0.070795, 0.063096, 0.056234, 0.050119, 0.044668, 0.039811, 0.035481, 0.031623,
0.028184, 0.025119, 0.022387, 0.019953, 0.017783, 0.015849, 0.014125, 0.012589, 0.011220, 0.010000,
0.008913, 0.007943, 0.007079, 0.006310, 0.005623, 0.005012, 0.004467, 0.003981, 0.003548, 0.000000 // NOTE mapped -50 dB ratio to total silence instead of 0.003162
};
const float* chart;
uint32_t i = abs(vol) - 1;
if (vol == 0) return;
normalize_volume_granular(vol);
chart = vol > 0 ? pos : neg;
newrate = chart[i];
if (newrate) {
__m256 scale_factor_reg = _mm256_set1_ps(newrate);
uint32_t processed_samples = samples - (samples % 8); // Ensure we process only multiples of 8
for (uint32_t i = 0; i < processed_samples; i += 8) {
__m128i data_ = _mm_loadu_si128((__m128i*)(data + i));
__m256i data_32 = _mm256_cvtepi16_epi32(data_);
__m256 data_float = _mm256_cvtepi32_ps(data_32);
__m256 result = _mm256_mul_ps(data_float, scale_factor_reg);
__m256i result_32 = _mm256_cvtps_epi32(result);
// Handle saturation
__m256i min_val = _mm256_set1_epi32(SMIN);
__m256i max_val = _mm256_set1_epi32(SMAX);
result_32 = _mm256_min_epi32(result_32, max_val);
result_32 = _mm256_max_epi32(result_32, min_val);
__m128i result_16 = _mm_packs_epi32(_mm256_castsi256_si128(result_32), _mm256_extractf128_si256(result_32, 1));
_mm_storeu_si128((__m128i*)(data + i), result_16);
}
// Process any remaining samples
for (uint32_t i = processed_samples; i < samples; i++) {
int32_t tmp = (int32_t)(data[i] * newrate);
tmp = tmp > SMAX ? SMAX : (tmp < SMIN ? SMIN : tmp);
data[i] = (int16_t)tmp;
}
}
}
#elif defined(USE_SSE2)
#include <emmintrin.h>
#pragma message("Using SSE2 SIMD.")
void vector_add(int16_t* a, int16_t* b, size_t len) {
size_t i = 0;
for (; i + 7 < len; i += 8) {
__m128i va = _mm_loadu_si128((const __m128i*)(a + i));
__m128i vb = _mm_loadu_si128((const __m128i*)(b + i));
__m128i vc = _mm_add_epi16(va, vb);
_mm_storeu_si128((__m128i*)(a + i), vc);
}
for (; i < len; ++i) {
a[i] += b[i];
}
}
void vector_normalize(int16_t* a, size_t len) {
__m128i max_val = _mm_set1_epi16(SMAX);
__m128i min_val = _mm_set1_epi16(SMIN);
size_t i = 0;
for (; i + 7 < len; i += 8) {
__m128i values = _mm_loadu_si128((__m128i*)(a + i));
__m128i gt_max = _mm_cmpgt_epi16(values, max_val);
__m128i lt_min = _mm_cmpgt_epi16(min_val, values);
__m128i max_masked = _mm_and_si128(gt_max, max_val);
__m128i min_masked = _mm_and_si128(lt_min, min_val);
__m128i other_masked = _mm_andnot_si128(_mm_or_si128(gt_max, lt_min), values);
values = _mm_or_si128(_mm_or_si128(max_masked, min_masked), other_masked);
_mm_storeu_si128((__m128i*)(a + i), values);
}
// Process remaining elements
for (; i < len; ++i) {
if (a[i] > SMAX) a[i] = SMAX;
else if (a[i] < SMIN) a[i] = SMIN;
}
}
typedef union {
int16_t* data;
__m128i* fp_sse2;
} vector_data_t;
#else
void vector_add(int16_t* a, int16_t* b, size_t len) {
for (size_t i = 0; i < len; i++) {
a[i] += b[i];
}
}
void vector_normalize(int16_t* a, size_t len) {
for (size_t i = 0; i < len; i++) {
normalize_to_16bit_basic(a[i]);
}
}
void vector_change_sln_volume_granular(int16_t* data, uint32_t samples, int32_t vol) {
float newrate = 0;
static const float pos[GRANULAR_VOLUME_MAX] = {
1.122018, 1.258925, 1.412538, 1.584893, 1.778279, 1.995262, 2.238721, 2.511887, 2.818383, 3.162278,
3.548134, 3.981072, 4.466835, 5.011872, 5.623413, 6.309574, 7.079458, 7.943282, 8.912509, 10.000000,
11.220183, 12.589254, 14.125375, 15.848933, 17.782795, 19.952621, 22.387213, 25.118862, 28.183832, 31.622776,
35.481335, 39.810719, 44.668358, 50.118729, 56.234131, 63.095726, 70.794586, 79.432816, 89.125107, 100.000000,
112.201836, 125.892517, 141.253784, 158.489334, 177.827942, 199.526215, 223.872070, 251.188705, 281.838318, 316.227753
};
static const float neg[GRANULAR_VOLUME_MAX] = {
0.891251, 0.794328, 0.707946, 0.630957, 0.562341, 0.501187, 0.446684, 0.398107, 0.354813, 0.316228,
0.281838, 0.251189, 0.223872, 0.199526, 0.177828, 0.158489, 0.141254, 0.125893, 0.112202, 0.100000,
0.089125, 0.079433, 0.070795, 0.063096, 0.056234, 0.050119, 0.044668, 0.039811, 0.035481, 0.031623,
0.028184, 0.025119, 0.022387, 0.019953, 0.017783, 0.015849, 0.014125, 0.012589, 0.011220, 0.010000,
0.008913, 0.007943, 0.007079, 0.006310, 0.005623, 0.005012, 0.004467, 0.003981, 0.003548, 0.000000 // NOTE mapped -50 dB ratio to total silence instead of 0.003162
};
const float* chart;
uint32_t i;
if (vol == 0) return;
normalize_volume_granular(vol);
chart = vol > 0 ? pos : neg;
i = abs(vol) - 1;
assert(i < GRANULAR_VOLUME_MAX);
newrate = chart[i];
if (newrate) {
int32_t tmp;
uint32_t x;
int16_t *fp = data;
for (x = 0; x < samples; x++) {
tmp = (int32_t) (fp[x] * newrate);
normalize_to_16bit_basic(tmp);
fp[x] = (int16_t) tmp;
}
}
}
#endif
#ifdef __cplusplus
}
#endif
+20
View File
@@ -0,0 +1,20 @@
#ifndef VECTOR_MATH_H
#define VECTOR_MATH_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
void vector_add(int16_t* a, int16_t* b, size_t len);
void vector_normalize(int16_t* a, size_t len);
void vector_change_sln_volume_granular(int16_t* data, uint32_t samples, int32_t vol);
#ifdef __cplusplus
}
#endif
#endif
+1 -1
View File
@@ -2,7 +2,7 @@ include $(top_srcdir)/build/modmake.rulesam
MODNAME=mod_google_transcribe
mod_LTLIBRARIES = mod_google_transcribe.la
mod_google_transcribe_la_SOURCES = mod_google_transcribe.c google_glue.cpp
mod_google_transcribe_la_SOURCES = mod_google_transcribe.c google_glue.cpp google_glue_v1.cpp google_glue_v2.cpp
mod_google_transcribe_la_CFLAGS = $(AM_CFLAGS)
mod_google_transcribe_la_CXXFLAGS = -I $(top_srcdir)/libs/googleapis/gens $(AM_CXXFLAGS) -std=c++17
+248
View File
@@ -0,0 +1,248 @@
#ifndef __GENERIC_GOOGLE_GLUE_H__
#define __GENERIC_GOOGLE_GLUE_H__
#include <switch_json.h>
template<typename Streamer>
switch_bool_t google_speech_frame(switch_media_bug_t *bug, void* user_data) {
switch_core_session_t *session = switch_core_media_bug_get_session(bug);
struct cap_cb *cb = (struct cap_cb *) user_data;
if (cb->streamer && (!cb->wants_single_utterance || !cb->got_end_of_utterance)) {
Streamer* streamer = (Streamer *) cb->streamer;
uint8_t data[SWITCH_RECOMMENDED_BUFFER_SIZE];
switch_frame_t frame = {};
frame.data = data;
frame.buflen = SWITCH_RECOMMENDED_BUFFER_SIZE;
if (switch_mutex_trylock(cb->mutex) == SWITCH_STATUS_SUCCESS) {
while (switch_core_media_bug_read(bug, &frame, SWITCH_TRUE) == SWITCH_STATUS_SUCCESS && !switch_test_flag((&frame), SFF_CNG)) {
if (frame.datalen) {
if (cb->vad && !streamer->isConnected()) {
switch_vad_state_t state = switch_vad_process(cb->vad, (int16_t*) frame.data, frame.samples);
if (state == SWITCH_VAD_STATE_START_TALKING) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "detected speech, connect to google speech now\n");
streamer->connect();
cb->responseHandler(session, "vad_detected", cb->bugname);
}
}
if (cb->resampler) {
spx_int16_t out[SWITCH_RECOMMENDED_BUFFER_SIZE];
spx_uint32_t out_len = SWITCH_RECOMMENDED_BUFFER_SIZE;
spx_uint32_t in_len = frame.samples;
size_t written;
speex_resampler_process_interleaved_int(cb->resampler,
(const spx_int16_t *) frame.data,
(spx_uint32_t *) &in_len,
&out[0],
&out_len);
streamer->write( &out[0], sizeof(spx_int16_t) * out_len);
}
else {
streamer->write( frame.data, sizeof(spx_int16_t) * frame.samples);
}
}
}
switch_mutex_unlock(cb->mutex);
}
}
return SWITCH_TRUE;
}
template<typename Streamer>
switch_status_t google_speech_session_init(switch_core_session_t *session, responseHandler_t responseHandler,
switch_thread_start_t func, uint32_t to_rate, uint32_t samples_per_second, uint32_t channels, char* lang,
int interim, char *bugname, int single_utterance, int separate_recognition, int max_alternatives,
int profanity_filter, int word_time_offset, int punctuation, const char* model, int enhanced,
const char* hints, char* play_file, void **ppUserData) {
switch_channel_t *channel = switch_core_session_get_channel(session);
auto read_codec = switch_core_session_get_read_codec(session);
uint32_t sampleRate = read_codec->implementation->actual_samples_per_second;
struct cap_cb *cb;
int err;
cb =(struct cap_cb *) switch_core_session_alloc(session, sizeof(*cb));
strncpy(cb->sessionId, switch_core_session_get_uuid(session), MAX_SESSION_ID);
strncpy(cb->bugname, bugname, MAX_BUG_LEN);
cb->got_end_of_utterance = 0;
cb->wants_single_utterance = single_utterance;
if (play_file != NULL){
cb->play_file = 1;
}
switch_mutex_init(&cb->mutex, SWITCH_MUTEX_NESTED, switch_core_session_get_pool(session));
if (sampleRate != to_rate) {
cb->resampler = speex_resampler_init(channels, sampleRate, to_rate, SWITCH_RESAMPLE_QUALITY, &err);
if (0 != err) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "%s: Error initializing resampler: %s.\n",
switch_channel_get_name(channel), speex_resampler_strerror(err));
return SWITCH_STATUS_FALSE;
}
} else {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "%s: no resampling needed for this call\n", switch_channel_get_name(channel));
}
cb->responseHandler = responseHandler;
// allocate vad if we are delaying connecting to the recognizer until we detect speech
if (switch_channel_var_true(channel, "START_RECOGNIZING_ON_VAD")) {
cb->vad = switch_vad_init(sampleRate, channels);
if (cb->vad) {
const char* var;
int mode = 2;
int silence_ms = 150;
int voice_ms = 250;
int debug = 0;
if (var = switch_channel_get_variable(channel, "RECOGNIZER_VAD_MODE")) {
mode = atoi(var);
}
if (var = switch_channel_get_variable(channel, "RECOGNIZER_VAD_SILENCE_MS")) {
silence_ms = atoi(var);
}
if (var = switch_channel_get_variable(channel, "RECOGNIZER_VAD_VOICE_MS")) {
voice_ms = atoi(var);
}
if (var = switch_channel_get_variable(channel, "RECOGNIZER_VAD_VOICE_MS")) {
voice_ms = atoi(var);
}
switch_vad_set_mode(cb->vad, mode);
switch_vad_set_param(cb->vad, "silence_ms", silence_ms);
switch_vad_set_param(cb->vad, "voice_ms", voice_ms);
switch_vad_set_param(cb->vad, "debug", debug);
}
}
Streamer *streamer = NULL;
try {
streamer = new Streamer(session, channels, lang, interim, to_rate, sampleRate, single_utterance, separate_recognition, max_alternatives,
profanity_filter, word_time_offset, punctuation, model, enhanced, hints);
cb->streamer = streamer;
} catch (std::exception& e) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "%s: Error initializing gstreamer: %s.\n",
switch_channel_get_name(channel), e.what());
return SWITCH_STATUS_FALSE;
}
if (!cb->vad) streamer->connect();
// create the read thread
switch_threadattr_t *thd_attr = NULL;
switch_memory_pool_t *pool = switch_core_session_get_pool(session);
switch_threadattr_create(&thd_attr, pool);
switch_threadattr_stacksize_set(thd_attr, SWITCH_THREAD_STACKSIZE);
switch_thread_create(&cb->thread, thd_attr, func, cb, pool);
*ppUserData = cb;
return SWITCH_STATUS_SUCCESS;
}
template<typename Streamer>
switch_status_t google_speech_session_cleanup(switch_core_session_t *session, int channelIsClosing, switch_media_bug_t *bug) {
switch_channel_t *channel = switch_core_session_get_channel(session);
if (bug) {
struct cap_cb *cb = (struct cap_cb *) switch_core_media_bug_get_user_data(bug);
switch_mutex_lock(cb->mutex);
if (!switch_channel_get_private(channel, cb->bugname)) {
// race condition
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "%s Bug is not attached (race).\n", switch_channel_get_name(channel));
switch_mutex_unlock(cb->mutex);
return SWITCH_STATUS_FALSE;
}
switch_channel_set_private(channel, cb->bugname, NULL);
// stop playback if available
if (cb->play_file == 1){
if (switch_channel_test_flag(channel, CF_BROADCAST)) {
switch_channel_stop_broadcast(channel);
} else {
switch_channel_set_flag_value(channel, CF_BREAK, 1);
}
}
// close connection and get final responses
Streamer* streamer = (Streamer *) cb->streamer;
if (streamer) {
streamer->writesDone();
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "google_speech_session_cleanup: GStreamer (%p) waiting for read thread to complete\n", (void*)streamer);
switch_status_t st;
switch_thread_join(&st, cb->thread);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "google_speech_session_cleanup: GStreamer (%p) read thread completed\n", (void*)streamer);
delete streamer;
cb->streamer = NULL;
}
if (cb->resampler) {
speex_resampler_destroy(cb->resampler);
}
if (cb->vad) {
switch_vad_destroy(&cb->vad);
cb->vad = nullptr;
}
if (!channelIsClosing) {
switch_core_media_bug_remove(session, &bug);
}
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "google_speech_session_cleanup: Closed stream\n");
switch_mutex_unlock(cb->mutex);
return SWITCH_STATUS_SUCCESS;
}
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "%s Bug is not attached.\n", switch_channel_get_name(channel));
return SWITCH_STATUS_FALSE;
}
template<typename PhraseSet>
void google_speech_configure_grammar_hints(switch_core_session_t *session, switch_channel_t *channel, const char* hints, PhraseSet* phrase_set) {
float boost = -1;
// get boost setting for the phrase set in its entirety
if (switch_true(switch_channel_get_variable(channel, "GOOGLE_SPEECH_HINTS_BOOST"))) {
boost = (float) atof(switch_channel_get_variable(channel, "GOOGLE_SPEECH_HINTS_BOOST"));
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "boost value: %f\n", boost);
phrase_set->set_boost(boost);
}
// hints are either a simple comma-separated list of phrases, or a json array of objects
// containing a phrase and a boost value
auto *jHint = cJSON_Parse((char *) hints);
if (jHint) {
int i = 0;
cJSON *jPhrase = NULL;
cJSON_ArrayForEach(jPhrase, jHint) {
cJSON *jItem = cJSON_GetObjectItem(jPhrase, "phrase");
if (jItem) {
auto* phrase = phrase_set->add_phrases();
phrase->set_value(cJSON_GetStringValue(jItem));
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "phrase: %s\n", phrase->value().c_str());
if (cJSON_GetObjectItem(jPhrase, "boost")) {
phrase->set_boost((float) cJSON_GetObjectItem(jPhrase, "boost")->valuedouble);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "boost value: %f\n", phrase->boost());
}
i++;
}
}
cJSON_Delete(jHint);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "added %d hints\n", i);
}
else {
char *phrases[500] = { 0 };
int argc = switch_separate_string((char *) hints, ',', phrases, 500);
for (int i = 0; i < argc; i++) {
auto* phrase = phrase_set->add_phrases();
phrase->set_value(phrases[i]);
}
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "added %d hints\n", argc);
}
}
#endif
+19 -719
View File
@@ -1,727 +1,27 @@
#include <cstdlib>
#include <algorithm>
#include <future>
#include <switch.h>
#include <switch_json.h>
#include <grpc++/grpc++.h>
#include "google/cloud/speech/v1p1beta1/cloud_speech.grpc.pb.h"
#include <switch_json.h>
#include <unistd.h>
#include "mod_google_transcribe.h"
#include "simple_buffer.h"
using google::cloud::speech::v1p1beta1::RecognitionConfig;
using google::cloud::speech::v1p1beta1::Speech;
using google::cloud::speech::v1p1beta1::SpeechContext;
using google::cloud::speech::v1p1beta1::StreamingRecognizeRequest;
using google::cloud::speech::v1p1beta1::StreamingRecognizeResponse;
using google::cloud::speech::v1p1beta1::SpeakerDiarizationConfig;
using google::cloud::speech::v1p1beta1::SpeechAdaptation;
using google::cloud::speech::v1p1beta1::PhraseSet;
using google::cloud::speech::v1p1beta1::PhraseSet_Phrase;
using google::cloud::speech::v1p1beta1::RecognitionMetadata;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_InteractionType_DISCUSSION;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_InteractionType_PRESENTATION;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_InteractionType_PHONE_CALL;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_InteractionType_VOICEMAIL;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_InteractionType_PROFESSIONALLY_PRODUCED;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_InteractionType_VOICE_SEARCH;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_InteractionType_VOICE_COMMAND;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_InteractionType_DICTATION;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_MicrophoneDistance_NEARFIELD;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_MicrophoneDistance_MIDFIELD;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_MicrophoneDistance_FARFIELD;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_OriginalMediaType_AUDIO;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_OriginalMediaType_VIDEO;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_RecordingDeviceType_SMARTPHONE;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_RecordingDeviceType_PC;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_RecordingDeviceType_PHONE_LINE;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_RecordingDeviceType_VEHICLE;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_RecordingDeviceType_OTHER_OUTDOOR_DEVICE;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_RecordingDeviceType_OTHER_INDOOR_DEVICE;
using google::cloud::speech::v1p1beta1::StreamingRecognizeResponse_SpeechEventType_END_OF_SINGLE_UTTERANCE;
using google::rpc::Status;
#define CHUNKSIZE (320)
namespace {
int case_insensitive_match(std::string s1, std::string s2) {
std::transform(s1.begin(), s1.end(), s1.begin(), ::tolower);
std::transform(s2.begin(), s2.end(), s2.begin(), ::tolower);
if(s1.compare(s2) == 0)
return 1; //The strings are same
return 0; //not matched
}
}
class GStreamer;
class GStreamer {
public:
GStreamer(
switch_core_session_t *session,
uint32_t channels,
char* lang,
int interim,
uint32_t config_sample_rate,
uint32_t samples_per_second,
int single_utterance,
int separate_recognition,
int max_alternatives,
int profanity_filter,
int word_time_offset,
int punctuation,
const char* model,
int enhanced,
const char* hints) : m_session(session), m_writesDone(false), m_connected(false),
m_audioBuffer(CHUNKSIZE, 15) {
const char* var;
const char* google_uri;
switch_channel_t *channel = switch_core_session_get_channel(session);
if (!(google_uri = switch_channel_get_variable(channel, "GOOGLE_SPEECH_TO_TEXT_URI"))) {
google_uri = "speech.googleapis.com";
}
if (var = switch_channel_get_variable(channel, "GOOGLE_APPLICATION_CREDENTIALS")) {
auto channelCreds = grpc::SslCredentials(grpc::SslCredentialsOptions());
auto callCreds = grpc::ServiceAccountJWTAccessCredentials(var);
auto creds = grpc::CompositeChannelCredentials(channelCreds, callCreds);
m_channel = grpc::CreateChannel(google_uri, creds);
}
else {
auto creds = grpc::GoogleDefaultCredentials();
m_channel = grpc::CreateChannel(google_uri, creds);
}
m_stub = Speech::NewStub(m_channel);
auto* streaming_config = m_request.mutable_streaming_config();
RecognitionConfig* config = streaming_config->mutable_config();
streaming_config->set_interim_results(interim);
if (single_utterance == 1) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "enable_single_utterance\n");
streaming_config->set_single_utterance(true);
}
else {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "enable_single_utterance is FALSE\n");
streaming_config->set_single_utterance(false);
}
config->set_language_code(lang);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "transcribe language %s \n", lang);
config->set_sample_rate_hertz(config_sample_rate);
config->set_encoding(RecognitionConfig::LINEAR16);
// the rest of config comes from channel vars
// number of channels in the audio stream (default: 1)
if (channels > 1) {
config->set_audio_channel_count(channels);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "audio_channel_count %d\n", channels);
// transcribe each separately?
if (separate_recognition == 1) {
config->set_enable_separate_recognition_per_channel(true);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "enable_separate_recognition_per_channel on\n");
}
}
// max alternatives
if (max_alternatives > 1) {
config->set_max_alternatives(max_alternatives);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "max_alternatives %d\n", max_alternatives);
}
// profanity filter
if (profanity_filter == 1) {
config->set_profanity_filter(true);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "profanity_filter\n");
}
// enable word offsets
if (word_time_offset == 1) {
config->set_enable_word_time_offsets(true);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "enable_word_time_offsets\n");
}
// enable automatic punctuation
if (punctuation == 1) {
config->set_enable_automatic_punctuation(true);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "enable_automatic_punctuation\n");
}
else {
config->set_enable_automatic_punctuation(false);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "disable_automatic_punctuation\n");
}
// speech model
if (model != NULL) {
config->set_model(model);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "speech model %s\n", model);
}
// use enhanced model
if (enhanced == 1) {
config->set_use_enhanced(true);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "use_enhanced\n");
}
// hints
if (hints != NULL) {
auto* adaptation = config->mutable_adaptation();
auto* phrase_set = adaptation->add_phrase_sets();
auto *context = config->add_speech_contexts();
float boost = -1;
// get boost setting for the phrase set in its entirety
if (switch_true(switch_channel_get_variable(channel, "GOOGLE_SPEECH_HINTS_BOOST"))) {
boost = (float) atof(switch_channel_get_variable(channel, "GOOGLE_SPEECH_HINTS_BOOST"));
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "boost value: %f\n", boost);
phrase_set->set_boost(boost);
}
// hints are either a simple comma-separated list of phrases, or a json array of objects
// containing a phrase and a boost value
auto *jHint = cJSON_Parse((char *) hints);
if (jHint) {
int i = 0;
cJSON *jPhrase = NULL;
cJSON_ArrayForEach(jPhrase, jHint) {
auto* phrase = phrase_set->add_phrases();
cJSON *jItem = cJSON_GetObjectItem(jPhrase, "phrase");
if (jItem) {
phrase->set_value(cJSON_GetStringValue(jItem));
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "phrase: %s\n", phrase->value().c_str());
if (cJSON_GetObjectItem(jPhrase, "boost")) {
phrase->set_boost((float) cJSON_GetObjectItem(jPhrase, "boost")->valuedouble);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "boost value: %f\n", phrase->boost());
}
i++;
}
}
cJSON_Delete(jHint);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "added %d hints\n", i);
}
else {
char *phrases[500] = { 0 };
int argc = switch_separate_string((char *) hints, ',', phrases, 500);
for (int i = 0; i < argc; i++) {
auto* phrase = phrase_set->add_phrases();
phrase->set_value(phrases[i]);
}
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "added %d hints\n", argc);
}
}
// alternative language
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES")) {
char *alt_langs[3] = { 0 };
int argc = switch_separate_string((char *) var, ',', alt_langs, 3);
for (int i = 0; i < argc; i++) {
config->add_alternative_language_codes(alt_langs[i]);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "added alternative lang %s\n", alt_langs[i]);
}
}
// speaker diarization
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_SPEAKER_DIARIZATION")) {
auto* diarization_config = config->mutable_diarization_config();
diarization_config->set_enable_speaker_diarization(true);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "enabling speaker diarization\n", var);
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_SPEAKER_DIARIZATION_MIN_SPEAKER_COUNT")) {
int count = std::max(atoi(var), 1);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "setting min speaker count to %d\n", count);
diarization_config->set_min_speaker_count(count);
}
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_SPEAKER_DIARIZATION_MAX_SPEAKER_COUNT")) {
int count = std::max(atoi(var), 2);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "setting max speaker count to %d\n", count);
diarization_config->set_max_speaker_count(count);
}
}
// recognition metadata
auto* metadata = config->mutable_metadata();
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_METADATA_INTERACTION_TYPE")) {
if (case_insensitive_match("discussion", var)) metadata->set_interaction_type(RecognitionMetadata_InteractionType_DISCUSSION);
if (case_insensitive_match("presentation", var)) metadata->set_interaction_type(RecognitionMetadata_InteractionType_PRESENTATION);
if (case_insensitive_match("phone_call", var)) metadata->set_interaction_type(RecognitionMetadata_InteractionType_PHONE_CALL);
if (case_insensitive_match("voicemail", var)) metadata->set_interaction_type(RecognitionMetadata_InteractionType_VOICEMAIL);
if (case_insensitive_match("professionally_produced", var)) metadata->set_interaction_type(RecognitionMetadata_InteractionType_PROFESSIONALLY_PRODUCED);
if (case_insensitive_match("voice_search", var)) metadata->set_interaction_type(RecognitionMetadata_InteractionType_VOICE_SEARCH);
if (case_insensitive_match("voice_command", var)) metadata->set_interaction_type(RecognitionMetadata_InteractionType_VOICE_COMMAND);
if (case_insensitive_match("dictation", var)) metadata->set_interaction_type(RecognitionMetadata_InteractionType_DICTATION);
}
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE")) {
metadata->set_industry_naics_code_of_audio(atoi(var));
}
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_METADATA_MICROPHONE_DISTANCE")) {
if (case_insensitive_match("nearfield", var)) metadata->set_microphone_distance(RecognitionMetadata_MicrophoneDistance_NEARFIELD);
if (case_insensitive_match("midfield", var)) metadata->set_microphone_distance(RecognitionMetadata_MicrophoneDistance_MIDFIELD);
if (case_insensitive_match("farfield", var)) metadata->set_microphone_distance(RecognitionMetadata_MicrophoneDistance_FARFIELD);
}
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_METADATA_ORIGINAL_MEDIA_TYPE")) {
if (case_insensitive_match("audio", var)) metadata->set_original_media_type(RecognitionMetadata_OriginalMediaType_AUDIO);
if (case_insensitive_match("video", var)) metadata->set_original_media_type(RecognitionMetadata_OriginalMediaType_VIDEO);
}
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_METADATA_RECORDING_DEVICE_TYPE")) {
if (case_insensitive_match("smartphone", var)) metadata->set_recording_device_type(RecognitionMetadata_RecordingDeviceType_SMARTPHONE);
if (case_insensitive_match("pc", var)) metadata->set_recording_device_type(RecognitionMetadata_RecordingDeviceType_PC);
if (case_insensitive_match("phone_line", var)) metadata->set_recording_device_type(RecognitionMetadata_RecordingDeviceType_PHONE_LINE);
if (case_insensitive_match("vehicle", var)) metadata->set_recording_device_type(RecognitionMetadata_RecordingDeviceType_VEHICLE);
if (case_insensitive_match("other_outdoor_device", var)) metadata->set_recording_device_type(RecognitionMetadata_RecordingDeviceType_OTHER_OUTDOOR_DEVICE);
if (case_insensitive_match("other_indoor_device", var)) metadata->set_recording_device_type(RecognitionMetadata_RecordingDeviceType_OTHER_INDOOR_DEVICE);
}
}
~GStreamer() {
//switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_INFO, "GStreamer::~GStreamer - deleting channel and stub: %p\n", (void*)this);
}
void connect() {
assert(!m_connected);
// Begin a stream.
m_streamer = m_stub->StreamingRecognize(&m_context);
m_connected = true;
// read thread is waiting on this
m_promise.set_value();
// Write the first request, containing the config only.
m_streamer->Write(m_request);
// send any buffered audio
int nFrames = m_audioBuffer.getNumItems();
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "GStreamer %p got stream ready, %d buffered frames\n", this, nFrames);
if (nFrames) {
char *p;
do {
p = m_audioBuffer.getNextChunk();
if (p) {
write(p, CHUNKSIZE);
}
} while (p);
}
}
bool write(void* data, uint32_t datalen) {
if (!m_connected) {
if (datalen % CHUNKSIZE == 0) {
m_audioBuffer.add(data, datalen);
}
return true;
}
m_request.set_audio_content(data, datalen);
bool ok = m_streamer->Write(m_request);
return ok;
}
uint32_t nextMessageSize(void) {
uint32_t size = 0;
m_streamer->NextMessageSize(&size);
return size;
}
bool read(StreamingRecognizeResponse* response) {
return m_streamer->Read(response);
}
grpc::Status finish() {
return m_streamer->Finish();
}
void writesDone() {
// grpc crashes if we call this twice on a stream
if (!m_connected) {
cancelConnect();
}
else if (!m_writesDone) {
m_streamer->WritesDone();
m_writesDone = true;
}
}
bool waitForConnect() {
std::shared_future<void> sf(m_promise.get_future());
sf.wait();
return m_connected;
}
void cancelConnect() {
assert(!m_connected);
m_promise.set_value();
}
bool isConnected() {
return m_connected;
}
private:
switch_core_session_t* m_session;
grpc::ClientContext m_context;
std::shared_ptr<grpc::Channel> m_channel;
std::unique_ptr<Speech::Stub> m_stub;
std::unique_ptr< grpc::ClientReaderWriterInterface<StreamingRecognizeRequest, StreamingRecognizeResponse> > m_streamer;
StreamingRecognizeRequest m_request;
bool m_writesDone;
bool m_connected;
std::promise<void> m_promise;
SimpleBuffer m_audioBuffer;
};
static void *SWITCH_THREAD_FUNC grpc_read_thread(switch_thread_t *thread, void *obj) {
static int count;
struct cap_cb *cb = (struct cap_cb *) obj;
GStreamer* streamer = (GStreamer *) cb->streamer;
bool connected = streamer->waitForConnect();
if (!connected) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "google transcribe grpc read thread exiting since we didnt connect\n") ;
return nullptr;
}
// Read responses.
StreamingRecognizeResponse response;
while (streamer->read(&response)) { // Returns false when no more to read.
switch_core_session_t* session = switch_core_session_locate(cb->sessionId);
if (!session) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "grpc_read_thread: session %s is gone!\n", cb->sessionId) ;
return nullptr;
}
count++;
auto speech_event_type = response.speech_event_type();
if (response.has_error()) {
Status status = response.error();
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", status.message().c_str());
char* jsonString = cJSON_PrintUnformatted(json);
cb->responseHandler(session, jsonString, cb->bugname);
free(jsonString);
cJSON_Delete(json);
}
if (cb->play_file == 1){
cb->responseHandler(session, "play_interrupt", cb->bugname);
}
for (int r = 0; r < response.results_size(); ++r) {
auto result = response.results(r);
cJSON * jResult = cJSON_CreateObject();
cJSON * jAlternatives = cJSON_CreateArray();
cJSON * jStability = cJSON_CreateNumber(result.stability());
cJSON * jIsFinal = cJSON_CreateBool(result.is_final());
cJSON * jLanguageCode = cJSON_CreateString(result.language_code().c_str());
cJSON * jChannelTag = cJSON_CreateNumber(result.channel_tag());
auto duration = result.result_end_time();
int32_t seconds = duration.seconds();
int64_t nanos = duration.nanos();
int span = (int) trunc(seconds * 1000. + ((float) nanos / 1000000.));
cJSON * jResultEndTime = cJSON_CreateNumber(span);
cJSON_AddItemToObject(jResult, "stability", jStability);
cJSON_AddItemToObject(jResult, "is_final", jIsFinal);
cJSON_AddItemToObject(jResult, "alternatives", jAlternatives);
cJSON_AddItemToObject(jResult, "language_code", jLanguageCode);
cJSON_AddItemToObject(jResult, "channel_tag", jChannelTag);
cJSON_AddItemToObject(jResult, "result_end_time", jResultEndTime);
for (int a = 0; a < result.alternatives_size(); ++a) {
auto alternative = result.alternatives(a);
cJSON* jAlt = cJSON_CreateObject();
cJSON* jConfidence = cJSON_CreateNumber(alternative.confidence());
cJSON* jTranscript = cJSON_CreateString(alternative.transcript().c_str());
cJSON_AddItemToObject(jAlt, "confidence", jConfidence);
cJSON_AddItemToObject(jAlt, "transcript", jTranscript);
if (alternative.words_size() > 0) {
cJSON * jWords = cJSON_CreateArray();
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "grpc_read_thread: %d words\n", alternative.words_size()) ;
for (int b = 0; b < alternative.words_size(); b++) {
auto words = alternative.words(b);
cJSON* jWord = cJSON_CreateObject();
cJSON_AddItemToObject(jWord, "word", cJSON_CreateString(words.word().c_str()));
if (words.has_start_time()) {
cJSON_AddItemToObject(jWord, "start_time", cJSON_CreateNumber(words.start_time().seconds()));
}
if (words.has_end_time()) {
cJSON_AddItemToObject(jWord, "end_time", cJSON_CreateNumber(words.end_time().seconds()));
}
int speaker_tag = words.speaker_tag();
if (speaker_tag > 0) {
cJSON_AddItemToObject(jWord, "speaker_tag", cJSON_CreateNumber(speaker_tag));
}
float confidence = words.confidence();
if (confidence > 0.0) {
cJSON_AddItemToObject(jWord, "confidence", cJSON_CreateNumber(confidence));
}
cJSON_AddItemToArray(jWords, jWord);
}
cJSON_AddItemToObject(jAlt, "words", jWords);
}
cJSON_AddItemToArray(jAlternatives, jAlt);
}
char* json = cJSON_PrintUnformatted(jResult);
cb->responseHandler(session, (const char *) json, cb->bugname);
free(json);
cJSON_Delete(jResult);
}
if (speech_event_type == StreamingRecognizeResponse_SpeechEventType_END_OF_SINGLE_UTTERANCE) {
// we only get this when we have requested it, and recognition stops after we get this
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "grpc_read_thread: got end_of_utterance\n") ;
cb->got_end_of_utterance = 1;
cb->responseHandler(session, "end_of_utterance", cb->bugname);
if (cb->wants_single_utterance) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "grpc_read_thread: sending writesDone because we want only a single utterance\n") ;
streamer->writesDone();
}
}
switch_core_session_rwunlock(session);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "grpc_read_thread: got %d responses\n", response.results_size());
}
{
switch_core_session_t* session = switch_core_session_locate(cb->sessionId);
if (session) {
grpc::Status status = streamer->finish();
if (11 == status.error_code()) {
if (std::string::npos != status.error_message().find("Exceeded maximum allowed stream duration")) {
cb->responseHandler(session, "max_duration_exceeded", cb->bugname);
}
else {
cb->responseHandler(session, "no_audio", cb->bugname);
}
}
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "grpc_read_thread: finish() status %s (%d)\n", status.error_message().c_str(), status.error_code()) ;
switch_core_session_rwunlock(session);
}
}
return nullptr;
}
#include "google_glue.h"
#include "generic_google_glue.h"
extern "C" {
switch_status_t google_speech_init() {
const char* gcsServiceKeyFile = std::getenv("GOOGLE_APPLICATION_CREDENTIALS");
if (gcsServiceKeyFile) {
try {
auto creds = grpc::GoogleDefaultCredentials();
} catch (const std::exception& e) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_CRIT,
"Error initializing google api with provided credentials in %s: %s\n", gcsServiceKeyFile, e.what());
return SWITCH_STATUS_FALSE;
}
}
return SWITCH_STATUS_SUCCESS;
}
switch_status_t google_speech_init() {
const char* gcsServiceKeyFile = std::getenv("GOOGLE_APPLICATION_CREDENTIALS");
if (gcsServiceKeyFile) {
try {
auto creds = grpc::GoogleDefaultCredentials();
} catch (const std::exception& e) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_CRIT,
"Error initializing google api with provided credentials in %s: %s\n", gcsServiceKeyFile, e.what());
return SWITCH_STATUS_FALSE;
}
}
return SWITCH_STATUS_SUCCESS;
}
switch_status_t google_speech_cleanup() {
return SWITCH_STATUS_SUCCESS;
}
switch_status_t google_speech_session_init(switch_core_session_t *session, responseHandler_t responseHandler,
uint32_t to_rate, uint32_t samples_per_second, uint32_t channels, char* lang, int interim, char *bugname,
int single_utterance, int separate_recognition, int max_alternatives, int profanity_filter, int word_time_offset,
int punctuation, const char* model, int enhanced, const char* hints, char* play_file, void **ppUserData) {
switch_channel_t *channel = switch_core_session_get_channel(session);
auto read_codec = switch_core_session_get_read_codec(session);
uint32_t sampleRate = read_codec->implementation->actual_samples_per_second;
struct cap_cb *cb;
int err;
cb =(struct cap_cb *) switch_core_session_alloc(session, sizeof(*cb));
strncpy(cb->sessionId, switch_core_session_get_uuid(session), MAX_SESSION_ID);
strncpy(cb->bugname, bugname, MAX_BUG_LEN);
cb->got_end_of_utterance = 0;
cb->wants_single_utterance = single_utterance;
if (play_file != NULL){
cb->play_file = 1;
}
switch_mutex_init(&cb->mutex, SWITCH_MUTEX_NESTED, switch_core_session_get_pool(session));
if (sampleRate != to_rate) {
cb->resampler = speex_resampler_init(channels, sampleRate, to_rate, SWITCH_RESAMPLE_QUALITY, &err);
if (0 != err) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "%s: Error initializing resampler: %s.\n",
switch_channel_get_name(channel), speex_resampler_strerror(err));
return SWITCH_STATUS_FALSE;
}
} else {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "%s: no resampling needed for this call\n", switch_channel_get_name(channel));
}
cb->responseHandler = responseHandler;
// allocate vad if we are delaying connecting to the recognizer until we detect speech
if (switch_channel_var_true(channel, "START_RECOGNIZING_ON_VAD")) {
cb->vad = switch_vad_init(sampleRate, channels);
if (cb->vad) {
const char* var;
int mode = 2;
int silence_ms = 150;
int voice_ms = 250;
int debug = 0;
if (var = switch_channel_get_variable(channel, "RECOGNIZER_VAD_MODE")) {
mode = atoi(var);
}
if (var = switch_channel_get_variable(channel, "RECOGNIZER_VAD_SILENCE_MS")) {
silence_ms = atoi(var);
}
if (var = switch_channel_get_variable(channel, "RECOGNIZER_VAD_VOICE_MS")) {
voice_ms = atoi(var);
}
if (var = switch_channel_get_variable(channel, "RECOGNIZER_VAD_VOICE_MS")) {
voice_ms = atoi(var);
}
switch_vad_set_mode(cb->vad, mode);
switch_vad_set_param(cb->vad, "silence_ms", silence_ms);
switch_vad_set_param(cb->vad, "voice_ms", voice_ms);
switch_vad_set_param(cb->vad, "debug", debug);
}
}
GStreamer *streamer = NULL;
try {
streamer = new GStreamer(session, channels, lang, interim, to_rate, sampleRate, single_utterance, separate_recognition, max_alternatives,
profanity_filter, word_time_offset, punctuation, model, enhanced, hints);
cb->streamer = streamer;
} catch (std::exception& e) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "%s: Error initializing gstreamer: %s.\n",
switch_channel_get_name(channel), e.what());
return SWITCH_STATUS_FALSE;
}
if (!cb->vad) streamer->connect();
// create the read thread
switch_threadattr_t *thd_attr = NULL;
switch_memory_pool_t *pool = switch_core_session_get_pool(session);
switch_threadattr_create(&thd_attr, pool);
switch_threadattr_stacksize_set(thd_attr, SWITCH_THREAD_STACKSIZE);
switch_thread_create(&cb->thread, thd_attr, grpc_read_thread, cb, pool);
*ppUserData = cb;
return SWITCH_STATUS_SUCCESS;
}
switch_status_t google_speech_session_cleanup(switch_core_session_t *session, int channelIsClosing, switch_media_bug_t *bug) {
switch_channel_t *channel = switch_core_session_get_channel(session);
if (bug) {
struct cap_cb *cb = (struct cap_cb *) switch_core_media_bug_get_user_data(bug);
switch_mutex_lock(cb->mutex);
if (!switch_channel_get_private(channel, cb->bugname)) {
// race condition
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "%s Bug is not attached (race).\n", switch_channel_get_name(channel));
switch_mutex_unlock(cb->mutex);
return SWITCH_STATUS_FALSE;
}
switch_channel_set_private(channel, cb->bugname, NULL);
// stop playback if available
if (cb->play_file == 1){
if (switch_channel_test_flag(channel, CF_BROADCAST)) {
switch_channel_stop_broadcast(channel);
} else {
switch_channel_set_flag_value(channel, CF_BREAK, 1);
}
}
// close connection and get final responses
GStreamer* streamer = (GStreamer *) cb->streamer;
if (streamer) {
streamer->writesDone();
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "google_speech_session_cleanup: GStreamer (%p) waiting for read thread to complete\n", (void*)streamer);
switch_status_t st;
switch_thread_join(&st, cb->thread);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "google_speech_session_cleanup: GStreamer (%p) read thread completed\n", (void*)streamer);
delete streamer;
cb->streamer = NULL;
}
if (cb->resampler) {
speex_resampler_destroy(cb->resampler);
}
if (cb->vad) {
switch_vad_destroy(&cb->vad);
cb->vad = nullptr;
}
if (!channelIsClosing) {
switch_core_media_bug_remove(session, &bug);
}
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "google_speech_session_cleanup: Closed stream\n");
switch_mutex_unlock(cb->mutex);
return SWITCH_STATUS_SUCCESS;
}
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "%s Bug is not attached.\n", switch_channel_get_name(channel));
return SWITCH_STATUS_FALSE;
}
switch_bool_t google_speech_frame(switch_media_bug_t *bug, void* user_data) {
switch_core_session_t *session = switch_core_media_bug_get_session(bug);
struct cap_cb *cb = (struct cap_cb *) user_data;
if (cb->streamer && (!cb->wants_single_utterance || !cb->got_end_of_utterance)) {
GStreamer* streamer = (GStreamer *) cb->streamer;
uint8_t data[SWITCH_RECOMMENDED_BUFFER_SIZE];
switch_frame_t frame = {};
frame.data = data;
frame.buflen = SWITCH_RECOMMENDED_BUFFER_SIZE;
if (switch_mutex_trylock(cb->mutex) == SWITCH_STATUS_SUCCESS) {
while (switch_core_media_bug_read(bug, &frame, SWITCH_TRUE) == SWITCH_STATUS_SUCCESS && !switch_test_flag((&frame), SFF_CNG)) {
if (frame.datalen) {
if (cb->vad && !streamer->isConnected()) {
switch_vad_state_t state = switch_vad_process(cb->vad, (int16_t*) frame.data, frame.samples);
if (state == SWITCH_VAD_STATE_START_TALKING) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "detected speech, connect to google speech now\n");
streamer->connect();
cb->responseHandler(session, "vad_detected", cb->bugname);
}
}
if (cb->resampler) {
spx_int16_t out[SWITCH_RECOMMENDED_BUFFER_SIZE];
spx_uint32_t out_len = SWITCH_RECOMMENDED_BUFFER_SIZE;
spx_uint32_t in_len = frame.samples;
size_t written;
speex_resampler_process_interleaved_int(cb->resampler,
(const spx_int16_t *) frame.data,
(spx_uint32_t *) &in_len,
&out[0],
&out_len);
streamer->write( &out[0], sizeof(spx_int16_t) * out_len);
}
else {
streamer->write( frame.data, sizeof(spx_int16_t) * frame.samples);
}
}
}
switch_mutex_unlock(cb->mutex);
}
}
return SWITCH_TRUE;
}
switch_status_t google_speech_cleanup() {
return SWITCH_STATUS_SUCCESS;
}
}
+19 -4
View File
@@ -1,13 +1,28 @@
#ifndef __GOOGLE_GLUE_H__
#define __GOOGLE_GLUE_H__
#ifdef __cplusplus
extern "C" {
#endif
switch_status_t google_speech_init();
switch_status_t google_speech_cleanup();
switch_status_t google_speech_session_init(switch_core_session_t *session, responseHandler_t responseHandler,
switch_status_t google_speech_session_init_v1(switch_core_session_t *session, responseHandler_t responseHandler,
uint32_t to_rate, uint32_t samples_per_second, uint32_t channels, char* lang, int interim, char *bugname, int single_utterence,
int separate_recognition, int max_alternatives, int profinity_filter, int word_time_offset, int punctuation, const char* model, int enhanced,
int separate_recognition, int max_alternatives, int profanity_filter, int word_time_offset, int punctuation, const char* model, int enhanced,
const char* hints, char* play_file, void **ppUserData);
switch_status_t google_speech_session_cleanup(switch_core_session_t *session, int channelIsClosing, switch_media_bug_t *bug);
switch_bool_t google_speech_frame(switch_media_bug_t *bug, void* user_data);
switch_status_t google_speech_session_cleanup_v1(switch_core_session_t *session, int channelIsClosing, switch_media_bug_t *bug);
switch_status_t google_speech_session_init_v2(switch_core_session_t *session, responseHandler_t responseHandler,
uint32_t to_rate, uint32_t samples_per_second, uint32_t channels, char* lang, int interim, char *bugname, int single_utterence,
int separate_recognition, int max_alternatives, int profanity_filter, int word_time_offset, int punctuation, const char* model, int enhanced,
const char* hints, char* play_file, void **ppUserData);
switch_status_t google_speech_session_cleanup_v2(switch_core_session_t *session, int channelIsClosing, switch_media_bug_t *bug);
switch_bool_t google_speech_frame_v1(switch_media_bug_t *bug, void* user_data);
switch_bool_t google_speech_frame_v2(switch_media_bug_t *bug, void* user_data);
#ifdef __cplusplus
}
#endif
#endif
+381
View File
@@ -0,0 +1,381 @@
#include <switch.h>
#include <switch_json.h>
#include <grpc++/grpc++.h>
#include "mod_google_transcribe.h"
#include "gstreamer.h"
#include "generic_google_glue.h"
#include "google/cloud/speech/v1p1beta1/cloud_speech.grpc.pb.h"
using google::cloud::speech::v1p1beta1::RecognitionConfig;
using google::cloud::speech::v1p1beta1::Speech;
using google::cloud::speech::v1p1beta1::SpeechContext;
using google::cloud::speech::v1p1beta1::StreamingRecognizeRequest;
using google::cloud::speech::v1p1beta1::StreamingRecognizeResponse;
using google::cloud::speech::v1p1beta1::SpeakerDiarizationConfig;
using google::cloud::speech::v1p1beta1::SpeechAdaptation;
using google::cloud::speech::v1p1beta1::PhraseSet;
using google::cloud::speech::v1p1beta1::PhraseSet_Phrase;
using google::cloud::speech::v1p1beta1::RecognitionMetadata;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_InteractionType_DISCUSSION;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_InteractionType_PRESENTATION;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_InteractionType_PHONE_CALL;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_InteractionType_VOICEMAIL;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_InteractionType_PROFESSIONALLY_PRODUCED;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_InteractionType_VOICE_SEARCH;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_InteractionType_VOICE_COMMAND;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_InteractionType_DICTATION;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_MicrophoneDistance_NEARFIELD;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_MicrophoneDistance_MIDFIELD;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_MicrophoneDistance_FARFIELD;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_OriginalMediaType_AUDIO;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_OriginalMediaType_VIDEO;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_RecordingDeviceType_SMARTPHONE;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_RecordingDeviceType_PC;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_RecordingDeviceType_PHONE_LINE;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_RecordingDeviceType_VEHICLE;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_RecordingDeviceType_OTHER_OUTDOOR_DEVICE;
using google::cloud::speech::v1p1beta1::RecognitionMetadata_RecordingDeviceType_OTHER_INDOOR_DEVICE;
using google::cloud::speech::v1p1beta1::StreamingRecognizeResponse_SpeechEventType_END_OF_SINGLE_UTTERANCE;
using google::rpc::Status;
typedef GStreamer<StreamingRecognizeRequest, StreamingRecognizeResponse, Speech::Stub> GStreamer_V1;
template<>
GStreamer<StreamingRecognizeRequest, StreamingRecognizeResponse, Speech::Stub>::GStreamer(
switch_core_session_t *session,
uint32_t channels,
char* lang,
int interim,
uint32_t config_sample_rate,
uint32_t samples_per_second,
int single_utterance,
int separate_recognition,
int max_alternatives,
int profanity_filter,
int word_time_offset,
int punctuation,
const char* model,
int enhanced,
const char* hints) : m_session(session), m_writesDone(false), m_connected(false),
m_audioBuffer(CHUNKSIZE, 15) {
switch_channel_t *channel = switch_core_session_get_channel(session);
m_channel = create_grpc_channel(channel);
m_stub = Speech::NewStub(m_channel);
auto* streaming_config = m_request.mutable_streaming_config();
RecognitionConfig* config = streaming_config->mutable_config();
streaming_config->set_interim_results(interim);
if (single_utterance == 1) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "enable_single_utterance\n");
streaming_config->set_single_utterance(true);
}
else {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "enable_single_utterance is FALSE\n");
streaming_config->set_single_utterance(false);
}
config->set_language_code(lang);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "transcribe language %s \n", lang);
config->set_sample_rate_hertz(config_sample_rate);
config->set_encoding(RecognitionConfig::LINEAR16);
// the rest of config comes from channel vars
// number of channels in the audio stream (default: 1)
if (channels > 1) {
config->set_audio_channel_count(channels);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "audio_channel_count %d\n", channels);
// transcribe each separately?
if (separate_recognition == 1) {
config->set_enable_separate_recognition_per_channel(true);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "enable_separate_recognition_per_channel on\n");
}
}
// max alternatives
if (max_alternatives > 1) {
config->set_max_alternatives(max_alternatives);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "max_alternatives %d\n", max_alternatives);
}
// profanity filter
if (profanity_filter == 1) {
config->set_profanity_filter(true);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "profanity_filter\n");
}
// enable word offsets
if (word_time_offset == 1) {
config->set_enable_word_time_offsets(true);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "enable_word_time_offsets\n");
}
// enable automatic punctuation
if (punctuation == 1) {
config->set_enable_automatic_punctuation(true);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "enable_automatic_punctuation\n");
}
else {
config->set_enable_automatic_punctuation(false);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "disable_automatic_punctuation\n");
}
// speech model
if (model != NULL) {
config->set_model(model);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "speech model %s\n", model);
}
// use enhanced model
if (enhanced == 1) {
config->set_use_enhanced(true);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "use_enhanced\n");
}
// hints
if (hints != NULL) {
auto* adaptation = config->mutable_adaptation();
auto* phrase_set = adaptation->add_phrase_sets();
google_speech_configure_grammar_hints(m_session, channel, hints, phrase_set);
}
const char* var;
// alternative language
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES")) {
char *alt_langs[3] = { 0 };
int argc = switch_separate_string((char *) var, ',', alt_langs, 3);
for (int i = 0; i < argc; i++) {
config->add_alternative_language_codes(alt_langs[i]);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "added alternative lang %s\n", alt_langs[i]);
}
}
// speaker diarization
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_SPEAKER_DIARIZATION")) {
auto* diarization_config = config->mutable_diarization_config();
diarization_config->set_enable_speaker_diarization(true);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "enabling speaker diarization\n", var);
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_SPEAKER_DIARIZATION_MIN_SPEAKER_COUNT")) {
int count = std::max(atoi(var), 1);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "setting min speaker count to %d\n", count);
diarization_config->set_min_speaker_count(count);
}
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_SPEAKER_DIARIZATION_MAX_SPEAKER_COUNT")) {
int count = std::max(atoi(var), 2);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "setting max speaker count to %d\n", count);
diarization_config->set_max_speaker_count(count);
}
}
// recognition metadata
auto* metadata = config->mutable_metadata();
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_METADATA_INTERACTION_TYPE")) {
if (case_insensitive_match("discussion", var)) metadata->set_interaction_type(RecognitionMetadata_InteractionType_DISCUSSION);
if (case_insensitive_match("presentation", var)) metadata->set_interaction_type(RecognitionMetadata_InteractionType_PRESENTATION);
if (case_insensitive_match("phone_call", var)) metadata->set_interaction_type(RecognitionMetadata_InteractionType_PHONE_CALL);
if (case_insensitive_match("voicemail", var)) metadata->set_interaction_type(RecognitionMetadata_InteractionType_VOICEMAIL);
if (case_insensitive_match("professionally_produced", var)) metadata->set_interaction_type(RecognitionMetadata_InteractionType_PROFESSIONALLY_PRODUCED);
if (case_insensitive_match("voice_search", var)) metadata->set_interaction_type(RecognitionMetadata_InteractionType_VOICE_SEARCH);
if (case_insensitive_match("voice_command", var)) metadata->set_interaction_type(RecognitionMetadata_InteractionType_VOICE_COMMAND);
if (case_insensitive_match("dictation", var)) metadata->set_interaction_type(RecognitionMetadata_InteractionType_DICTATION);
}
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE")) {
metadata->set_industry_naics_code_of_audio(atoi(var));
}
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_METADATA_MICROPHONE_DISTANCE")) {
if (case_insensitive_match("nearfield", var)) metadata->set_microphone_distance(RecognitionMetadata_MicrophoneDistance_NEARFIELD);
if (case_insensitive_match("midfield", var)) metadata->set_microphone_distance(RecognitionMetadata_MicrophoneDistance_MIDFIELD);
if (case_insensitive_match("farfield", var)) metadata->set_microphone_distance(RecognitionMetadata_MicrophoneDistance_FARFIELD);
}
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_METADATA_ORIGINAL_MEDIA_TYPE")) {
if (case_insensitive_match("audio", var)) metadata->set_original_media_type(RecognitionMetadata_OriginalMediaType_AUDIO);
if (case_insensitive_match("video", var)) metadata->set_original_media_type(RecognitionMetadata_OriginalMediaType_VIDEO);
}
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_METADATA_RECORDING_DEVICE_TYPE")) {
if (case_insensitive_match("smartphone", var)) metadata->set_recording_device_type(RecognitionMetadata_RecordingDeviceType_SMARTPHONE);
if (case_insensitive_match("pc", var)) metadata->set_recording_device_type(RecognitionMetadata_RecordingDeviceType_PC);
if (case_insensitive_match("phone_line", var)) metadata->set_recording_device_type(RecognitionMetadata_RecordingDeviceType_PHONE_LINE);
if (case_insensitive_match("vehicle", var)) metadata->set_recording_device_type(RecognitionMetadata_RecordingDeviceType_VEHICLE);
if (case_insensitive_match("other_outdoor_device", var)) metadata->set_recording_device_type(RecognitionMetadata_RecordingDeviceType_OTHER_OUTDOOR_DEVICE);
if (case_insensitive_match("other_indoor_device", var)) metadata->set_recording_device_type(RecognitionMetadata_RecordingDeviceType_OTHER_INDOOR_DEVICE);
}
}
static void *SWITCH_THREAD_FUNC grpc_read_thread(switch_thread_t *thread, void *obj) {
static int count;
struct cap_cb *cb = (struct cap_cb *) obj;
GStreamer_V1* streamer = (GStreamer_V1 *) cb->streamer;
bool connected = streamer->waitForConnect();
if (!connected) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "google transcribe grpc read thread exiting since we didn't connect\n") ;
return nullptr;
}
// Read responses.
StreamingRecognizeResponse response;
while (streamer->read(&response)) { // Returns false when no more to read.
switch_core_session_t* session = switch_core_session_locate(cb->sessionId);
if (!session) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "grpc_read_thread: session %s is gone!\n", cb->sessionId) ;
return nullptr;
}
count++;
auto speech_event_type = response.speech_event_type();
if (response.has_error()) {
Status status = response.error();
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", status.message().c_str());
char* jsonString = cJSON_PrintUnformatted(json);
cb->responseHandler(session, jsonString, cb->bugname);
free(jsonString);
cJSON_Delete(json);
}
if (cb->play_file == 1) {
cb->responseHandler(session, "play_interrupt", cb->bugname);
}
for (int r = 0; r < response.results_size(); ++r) {
auto result = response.results(r);
cJSON * jResult = cJSON_CreateObject();
cJSON * jAlternatives = cJSON_CreateArray();
cJSON * jStability = cJSON_CreateNumber(result.stability());
cJSON * jIsFinal = cJSON_CreateBool(result.is_final());
cJSON * jLanguageCode = cJSON_CreateString(result.language_code().c_str());
cJSON * jChannelTag = cJSON_CreateNumber(result.channel_tag());
auto duration = result.result_end_time();
int32_t seconds = duration.seconds();
int64_t nanos = duration.nanos();
int span = (int) trunc(seconds * 1000. + ((float) nanos / 1000000.));
cJSON * jResultEndTime = cJSON_CreateNumber(span);
cJSON_AddItemToObject(jResult, "stability", jStability);
cJSON_AddItemToObject(jResult, "is_final", jIsFinal);
cJSON_AddItemToObject(jResult, "alternatives", jAlternatives);
cJSON_AddItemToObject(jResult, "language_code", jLanguageCode);
cJSON_AddItemToObject(jResult, "channel_tag", jChannelTag);
cJSON_AddItemToObject(jResult, "result_end_time", jResultEndTime);
for (int a = 0; a < result.alternatives_size(); ++a) {
auto alternative = result.alternatives(a);
cJSON* jAlt = cJSON_CreateObject();
cJSON* jConfidence = cJSON_CreateNumber(alternative.confidence());
cJSON* jTranscript = cJSON_CreateString(alternative.transcript().c_str());
cJSON_AddItemToObject(jAlt, "confidence", jConfidence);
cJSON_AddItemToObject(jAlt, "transcript", jTranscript);
if (alternative.words_size() > 0) {
cJSON * jWords = cJSON_CreateArray();
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "grpc_read_thread: %d words\n", alternative.words_size()) ;
for (int b = 0; b < alternative.words_size(); b++) {
auto words = alternative.words(b);
cJSON* jWord = cJSON_CreateObject();
cJSON_AddItemToObject(jWord, "word", cJSON_CreateString(words.word().c_str()));
if (words.has_start_time()) {
cJSON_AddItemToObject(jWord, "start_time", cJSON_CreateNumber(words.start_time().seconds()));
}
if (words.has_end_time()) {
cJSON_AddItemToObject(jWord, "end_time", cJSON_CreateNumber(words.end_time().seconds()));
}
int speaker_tag = words.speaker_tag();
if (speaker_tag > 0) {
cJSON_AddItemToObject(jWord, "speaker_tag", cJSON_CreateNumber(speaker_tag));
}
float confidence = words.confidence();
if (confidence > 0.0) {
cJSON_AddItemToObject(jWord, "confidence", cJSON_CreateNumber(confidence));
}
cJSON_AddItemToArray(jWords, jWord);
}
cJSON_AddItemToObject(jAlt, "words", jWords);
}
cJSON_AddItemToArray(jAlternatives, jAlt);
}
char* json = cJSON_PrintUnformatted(jResult);
cb->responseHandler(session, (const char *) json, cb->bugname);
free(json);
cJSON_Delete(jResult);
}
if (speech_event_type == StreamingRecognizeResponse_SpeechEventType_END_OF_SINGLE_UTTERANCE) {
// we only get this when we have requested it, and recognition stops after we get this
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "grpc_read_thread: got end_of_utterance\n") ;
cb->got_end_of_utterance = 1;
cb->responseHandler(session, "end_of_utterance", cb->bugname);
if (cb->wants_single_utterance) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "grpc_read_thread: sending writesDone because we want only a single utterance\n") ;
streamer->writesDone();
}
}
switch_core_session_rwunlock(session);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "grpc_read_thread: got %d responses\n", response.results_size());
}
{
switch_core_session_t* session = switch_core_session_locate(cb->sessionId);
if (session) {
grpc::Status status = streamer->finish();
if (11 == status.error_code()) {
if (std::string::npos != status.error_message().find("Exceeded maximum allowed stream duration")) {
cb->responseHandler(session, "max_duration_exceeded", cb->bugname);
}
else {
cb->responseHandler(session, "no_audio", cb->bugname);
}
}
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "grpc_read_thread: finish() status %s (%d)\n", status.error_message().c_str(), status.error_code()) ;
switch_core_session_rwunlock(session);
}
}
return nullptr;
}
template<>
bool GStreamer<StreamingRecognizeRequest, StreamingRecognizeResponse, Speech::Stub>::write(void* data, uint32_t datalen) {
if (!m_connected) {
if (datalen % CHUNKSIZE == 0) {
m_audioBuffer.add(data, datalen);
}
return true;
}
m_request.set_audio_content(data, datalen);
bool ok = m_streamer->Write(m_request);
return ok;
}
extern "C" {
switch_status_t google_speech_session_cleanup_v1(switch_core_session_t *session, int channelIsClosing, switch_media_bug_t *bug) {
return google_speech_session_cleanup<GStreamer_V1>(session, channelIsClosing, bug);
}
switch_bool_t google_speech_frame_v1(switch_media_bug_t *bug, void* user_data) {
return google_speech_frame<GStreamer_V1>(bug, user_data);
}
switch_status_t google_speech_session_init_v1(switch_core_session_t *session, responseHandler_t responseHandler,
uint32_t to_rate, uint32_t samples_per_second, uint32_t channels, char* lang, int interim, char *bugname, int single_utterance,
int separate_recognition, int max_alternatives, int profanity_filter, int word_time_offset, int punctuation, const char* model, int enhanced,
const char* hints, char* play_file, void **ppUserData) {
return google_speech_session_init<GStreamer_V1>(session, responseHandler, grpc_read_thread, to_rate, samples_per_second, channels,
lang, interim, bugname, single_utterance, separate_recognition, max_alternatives, profanity_filter,
word_time_offset, punctuation, model, enhanced, hints, play_file, ppUserData);
}
}
+339
View File
@@ -0,0 +1,339 @@
#include <switch.h>
#include <switch_json.h>
#include <grpc++/grpc++.h>
#include "mod_google_transcribe.h"
#include "gstreamer.h"
#include "generic_google_glue.h"
#include "google/cloud/speech/v2/cloud_speech.grpc.pb.h"
using google::cloud::speech::v2::RecognitionConfig;
using google::cloud::speech::v2::Speech;
using google::cloud::speech::v2::StreamingRecognizeRequest;
using google::cloud::speech::v2::StreamingRecognizeResponse;
using google::cloud::speech::v2::SpeakerDiarizationConfig;
using google::cloud::speech::v2::SpeechAdaptation;
using google::cloud::speech::v2::SpeechRecognitionAlternative;
using google::cloud::speech::v2::PhraseSet;
using google::cloud::speech::v2::PhraseSet_Phrase;
using google::cloud::speech::v2::StreamingRecognizeResponse_SpeechEventType_END_OF_SINGLE_UTTERANCE;
using google::cloud::speech::v2::ExplicitDecodingConfig_AudioEncoding_LINEAR16;
using google::cloud::speech::v2::RecognitionFeatures_MultiChannelMode_SEPARATE_RECOGNITION_PER_CHANNEL;
using google::cloud::speech::v2::SpeechAdaptation_AdaptationPhraseSet;
using google::rpc::Status;
typedef GStreamer<StreamingRecognizeRequest, StreamingRecognizeResponse, Speech::Stub> GStreamer_V2;
template<>
GStreamer<StreamingRecognizeRequest, StreamingRecognizeResponse, Speech::Stub>::GStreamer(
switch_core_session_t *session,
uint32_t channels,
char* lang,
int interim,
uint32_t config_sample_rate,
uint32_t samples_per_second,
int single_utterance,
int separate_recognition,
int max_alternatives,
int profanity_filter,
int word_time_offset,
int punctuation,
const char* model,
int enhanced,
const char* hints) : m_session(session), m_writesDone(false), m_connected(false),
m_audioBuffer(CHUNKSIZE, 15) {
switch_channel_t *channel = switch_core_session_get_channel(session);
m_channel = create_grpc_channel(channel);
m_stub = Speech::NewStub(m_channel);
auto streaming_config = m_request.mutable_streaming_config();
const char* var;
// The parent of the recognizer must still be provided even if the wildcard
// recognizer is used rather than a pre-prepared recognizer.
std::string recognizer;
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_RECOGNIZER_PARENT")) {
recognizer = var;
recognizer += "/recognizers/";
} else {
throw std::runtime_error("The v2 Speech-To-Text library requires GOOGLE_SPEECH_RECOGNIZER_PARENT to be set");
}
// Use the recognizer specified in the variable or just use the wildcard if this is not set.
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_RECOGNIZER_ID")) {
recognizer += var;
} else {
recognizer += "_";
RecognitionConfig* config = streaming_config->mutable_config();
config->add_language_codes(lang);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "transcribe language %s\n", lang);
// alternative language
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES")) {
char *alt_langs[3] = { 0 };
int argc = switch_separate_string((char *) var, ',', alt_langs, 3);
for (int i = 0; i < argc; i++) {
config->add_language_codes(alt_langs[i]);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "added alternative lang %s\n", alt_langs[i]);
}
}
config->mutable_explicit_decoding_config()->set_sample_rate_hertz(config_sample_rate);
config->mutable_explicit_decoding_config()->set_encoding(ExplicitDecodingConfig_AudioEncoding_LINEAR16);
// number of channels in the audio stream (default: 1)
// N.B. It is essential to set this configuration value in v2 even if it doesn't deviate from the default.
config->mutable_explicit_decoding_config()->set_audio_channel_count(channels);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "audio_channel_count %d\n", channels);
if (channels > 1) {
// transcribe each separately?
if (separate_recognition == 1) {
config->mutable_features()->set_multi_channel_mode(RecognitionFeatures_MultiChannelMode_SEPARATE_RECOGNITION_PER_CHANNEL);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "enable_separate_recognition_per_channel on\n");
}
}
// max alternatives
if (max_alternatives > 1) {
config->mutable_features()->set_max_alternatives(max_alternatives);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "max_alternatives %d\n", max_alternatives);
}
// profanity filter
if (profanity_filter == 1) {
config->mutable_features()->set_profanity_filter(true);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "profanity_filter\n");
}
// enable word offsets
if (word_time_offset == 1) {
config->mutable_features()->set_enable_word_time_offsets(true);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "enable_word_time_offsets\n");
}
// enable automatic punctuation
if (punctuation == 1) {
config->mutable_features()->set_enable_automatic_punctuation(true);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "enable_automatic_punctuation\n");
}
else {
config->mutable_features()->set_enable_automatic_punctuation(false);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "disable_automatic_punctuation\n");
}
// speech model
if (model != NULL) {
config->set_model(model);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "speech model %s\n", model);
}
// hints
if (hints != NULL) {
auto* adaptation = config->mutable_adaptation();
auto* phrase_set = adaptation->add_phrase_sets()->mutable_inline_phrase_set();
google_speech_configure_grammar_hints(m_session, channel, hints, phrase_set);
}
// the rest of config comes from channel vars
// speaker diarization
// N.B. At the moment there does not seem to be any combination of model, language and location which supports diarization for STT v2.
// See https://stackoverflow.com/questions/76779418/speaker-diarization-is-disabled-even-for-supported-languages-in-google-speech-to
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_SPEAKER_DIARIZATION")) {
auto* diarization_config = config->mutable_features()->mutable_diarization_config();
// There is no enable function in v2
// diarization_config->set_enable_speaker_diarization(true);
// switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "enabling speaker diarization\n", var);
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_SPEAKER_DIARIZATION_MIN_SPEAKER_COUNT")) {
int count = std::max(atoi(var), 1);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "setting min speaker count to %d\n", count);
diarization_config->set_min_speaker_count(count);
}
if (var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_SPEAKER_DIARIZATION_MAX_SPEAKER_COUNT")) {
int count = std::max(atoi(var), 2);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "setting max speaker count to %d\n", count);
diarization_config->set_max_speaker_count(count);
}
}
}
m_request.set_recognizer(recognizer);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_DEBUG, "using recognizer: %s\n", recognizer.c_str());
// This must be set whether a recognizer id is provided orr not, because it cannot be configured as part of a recognizer.
if (interim > 0) {
streaming_config->mutable_streaming_features()->set_interim_results(interim > 0);
}
}
static void *SWITCH_THREAD_FUNC grpc_read_thread(switch_thread_t *thread, void *obj) {
static int count;
struct cap_cb *cb = (struct cap_cb *) obj;
GStreamer_V2* streamer = (GStreamer_V2 *) cb->streamer;
bool connected = streamer->waitForConnect();
if (!connected) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "google transcribe grpc read thread exiting since we didn't connect\n") ;
return nullptr;
}
// Read responses.
StreamingRecognizeResponse response;
while (streamer->read(&response)) { // Returns false when no more to read.
switch_core_session_t* session = switch_core_session_locate(cb->sessionId);
if (!session) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "grpc_read_thread: session %s is gone!\n", cb->sessionId) ;
return nullptr;
}
count++;
if (cb->play_file == 1){
cb->responseHandler(session, "play_interrupt", cb->bugname);
}
for (int r = 0; r < response.results_size(); ++r) {
auto result = response.results(r);
cJSON * jResult = cJSON_CreateObject();
cJSON * jAlternatives = cJSON_CreateArray();
cJSON * jStability = cJSON_CreateNumber(result.stability());
cJSON * jIsFinal = cJSON_CreateBool(result.is_final());
cJSON * jLanguageCode = cJSON_CreateString(result.language_code().c_str());
cJSON * jChannelTag = cJSON_CreateNumber(result.channel_tag());
auto duration = result.result_end_offset();
int32_t seconds = duration.seconds();
int64_t nanos = duration.nanos();
int span = (int) trunc(seconds * 1000. + ((float) nanos / 1000000.));
cJSON * jResultEndTime = cJSON_CreateNumber(span);
cJSON_AddItemToObject(jResult, "stability", jStability);
cJSON_AddItemToObject(jResult, "is_final", jIsFinal);
cJSON_AddItemToObject(jResult, "alternatives", jAlternatives);
cJSON_AddItemToObject(jResult, "language_code", jLanguageCode);
cJSON_AddItemToObject(jResult, "channel_tag", jChannelTag);
cJSON_AddItemToObject(jResult, "result_end_time", jResultEndTime);
if (result.alternatives_size() == 0) {
SpeechRecognitionAlternative alternative;
alternative.set_confidence(0.0);
alternative.set_transcript("");
*result.add_alternatives() = alternative;
}
for (int a = 0; a < result.alternatives_size(); ++a) {
auto alternative = result.alternatives(a);
cJSON* jAlt = cJSON_CreateObject();
cJSON* jConfidence = cJSON_CreateNumber(alternative.confidence());
cJSON* jTranscript = cJSON_CreateString(alternative.transcript().c_str());
cJSON_AddItemToObject(jAlt, "confidence", jConfidence);
cJSON_AddItemToObject(jAlt, "transcript", jTranscript);
if (alternative.words_size() > 0) {
cJSON * jWords = cJSON_CreateArray();
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "grpc_read_thread: %d words\n", alternative.words_size()) ;
for (int b = 0; b < alternative.words_size(); b++) {
auto words = alternative.words(b);
cJSON* jWord = cJSON_CreateObject();
cJSON_AddItemToObject(jWord, "word", cJSON_CreateString(words.word().c_str()));
if (words.has_start_offset()) {
cJSON_AddItemToObject(jWord, "start_offset", cJSON_CreateNumber(words.start_offset().seconds()));
}
if (words.has_end_offset()) {
cJSON_AddItemToObject(jWord, "end_offset", cJSON_CreateNumber(words.end_offset().seconds()));
}
auto speaker_label = words.speaker_label();
if (speaker_label.size() > 0) {
cJSON_AddItemToObject(jWord, "speaker_label", cJSON_CreateString(speaker_label.c_str()));
}
float confidence = words.confidence();
if (confidence > 0.0) {
cJSON_AddItemToObject(jWord, "confidence", cJSON_CreateNumber(confidence));
}
cJSON_AddItemToArray(jWords, jWord);
}
cJSON_AddItemToObject(jAlt, "words", jWords);
}
cJSON_AddItemToArray(jAlternatives, jAlt);
}
char* json = cJSON_PrintUnformatted(jResult);
cb->responseHandler(session, (const char *) json, cb->bugname);
free(json);
cJSON_Delete(jResult);
}
auto speech_event_type = response.speech_event_type();
if (speech_event_type == StreamingRecognizeResponse_SpeechEventType_END_OF_SINGLE_UTTERANCE) {
// we only get this when we have requested it, and recognition stops after we get this
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "grpc_read_thread: got end_of_utterance\n") ;
cb->got_end_of_utterance = 1;
cb->responseHandler(session, "end_of_utterance", cb->bugname);
if (cb->wants_single_utterance) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "grpc_read_thread: sending writesDone because we want only a single utterance\n") ;
streamer->writesDone();
}
}
switch_core_session_rwunlock(session);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "grpc_read_thread: got %d responses\n", response.results_size());
}
{
switch_core_session_t* session = switch_core_session_locate(cb->sessionId);
if (session) {
grpc::Status status = streamer->finish();
// TODO: This works on the same principle as that used in the v1 equivalent, in that we search for the textual
// error message to determine whether the cause of the problem is the expiration of the session.
// It would be better if we could find a more reliable way of detecting this.
if (10 == status.error_code()) {
if (std::string::npos != status.error_message().find("Max duration of 5 minutes reached")) {
cb->responseHandler(session, "max_duration_exceeded", cb->bugname);
}
else {
cb->responseHandler(session, "no_audio", cb->bugname);
}
}
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "grpc_read_thread: finish() status %s (%d)\n", status.error_message().c_str(), status.error_code()) ;
switch_core_session_rwunlock(session);
}
}
return nullptr;
}
template <>
bool GStreamer<StreamingRecognizeRequest, StreamingRecognizeResponse, Speech::Stub>::write(void* data, uint32_t datalen) {
if (!m_connected) {
if (datalen % CHUNKSIZE == 0) {
m_audioBuffer.add(data, datalen);
}
return true;
}
m_request.clear_streaming_config();
m_request.set_audio(data, datalen);
bool ok = m_streamer->Write(m_request);
return ok;
}
extern "C" {
switch_status_t google_speech_session_cleanup_v2(switch_core_session_t *session, int channelIsClosing, switch_media_bug_t *bug) {
return google_speech_session_cleanup<GStreamer_V2>(session, channelIsClosing, bug);
}
switch_bool_t google_speech_frame_v2(switch_media_bug_t *bug, void* user_data) {
return google_speech_frame<GStreamer_V2>(bug, user_data);
}
switch_status_t google_speech_session_init_v2(switch_core_session_t *session, responseHandler_t responseHandler,
uint32_t to_rate, uint32_t samples_per_second, uint32_t channels, char* lang, int interim, char *bugname, int single_utterance,
int separate_recognition, int max_alternatives, int profanity_filter, int word_time_offset, int punctuation, const char* model, int enhanced,
const char* hints, char* play_file, void **ppUserData) {
return google_speech_session_init<GStreamer_V2>(session, responseHandler, grpc_read_thread, to_rate, samples_per_second, channels,
lang, interim, bugname, single_utterance, separate_recognition, max_alternatives, profanity_filter,
word_time_offset, punctuation, model, enhanced, hints, play_file, ppUserData);
}
}
+147
View File
@@ -0,0 +1,147 @@
#include <cstdlib>
#include <algorithm>
#include <future>
#include <switch.h>
#include <switch_json.h>
#include <grpc++/grpc++.h>
#include <grpcpp/impl/codegen/sync_stream.h>
#include "mod_google_transcribe.h"
#include "simple_buffer.h"
#define CHUNKSIZE (320)
namespace {
int case_insensitive_match(std::string s1, std::string s2) {
std::transform(s1.begin(), s1.end(), s1.begin(), ::tolower);
std::transform(s2.begin(), s2.end(), s2.begin(), ::tolower);
if(s1.compare(s2) == 0)
return 1; //The strings are same
return 0; //not matched
}
}
template <typename Request, typename Response, typename Stub>
class GStreamer {
public:
GStreamer(
switch_core_session_t *session,
uint32_t channels,
char* lang,
int interim,
uint32_t config_sample_rate,
uint32_t samples_per_second,
int single_utterance,
int separate_recognition,
int max_alternatives,
int profanity_filter,
int word_time_offset,
int punctuation,
const char* model,
int enhanced,
const char* hints);
~GStreamer() {
//switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(m_session), SWITCH_LOG_INFO, "GStreamer::~GStreamer - deleting channel and stub: %p\n", (void*)this);
}
bool write(void* data, uint32_t datalen);
void connect() {
assert(!m_connected);
// Begin a stream.
m_streamer = m_stub->StreamingRecognize(&m_context);
m_connected = true;
// read thread is waiting on this
m_promise.set_value();
// Write the first request, containing the config only.
m_streamer->Write(m_request);
// send any buffered audio
int nFrames = m_audioBuffer.getNumItems();
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "GStreamer %p got stream ready, %d buffered frames\n", this, nFrames);
if (nFrames) {
char *p;
do {
p = m_audioBuffer.getNextChunk();
if (p) {
write(p, CHUNKSIZE);
}
} while (p);
}
}
uint32_t nextMessageSize(void) {
uint32_t size = 0;
m_streamer->NextMessageSize(&size);
return size;
}
bool read(Response* response) {
return m_streamer->Read(response);
}
grpc::Status finish() {
return m_streamer->Finish();
}
void writesDone() {
// grpc crashes if we call this twice on a stream
if (!m_connected) {
cancelConnect();
}
else if (!m_writesDone) {
m_streamer->WritesDone();
m_writesDone = true;
}
}
bool waitForConnect() {
std::shared_future<void> sf(m_promise.get_future());
sf.wait();
return m_connected;
}
void cancelConnect() {
assert(!m_connected);
m_promise.set_value();
}
bool isConnected() {
return m_connected;
}
private:
std::shared_ptr<grpc::Channel> create_grpc_channel(switch_channel_t *channel) {
const char* google_uri;
if (!(google_uri = switch_channel_get_variable(channel, "GOOGLE_SPEECH_TO_TEXT_URI"))) {
google_uri = "speech.googleapis.com";
}
const char* var;
if (var = switch_channel_get_variable(channel, "GOOGLE_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);
}
}
switch_core_session_t* m_session;
grpc::ClientContext m_context;
std::shared_ptr<grpc::Channel> m_channel;
std::unique_ptr<Stub> m_stub;
std::unique_ptr< grpc::ClientReaderWriterInterface<Request, Response> > m_streamer;
Request m_request;
bool m_writesDone;
bool m_connected;
std::promise<void> m_promise;
SimpleBuffer m_audioBuffer;
};
+138 -56
View File
@@ -10,6 +10,13 @@
static const uint32_t DEFAULT_SAMPLE_RATE = 8000;
/* Callback Type Definitions */
typedef switch_status_t (*speech_cleanup_callback_t) (switch_core_session_t *, int, switch_media_bug_t *);
typedef switch_bool_t (*speech_frame_callback_t) (switch_media_bug_t *, void *);
typedef switch_status_t (*speech_init_callback_t) (switch_core_session_t *, responseHandler_t,
uint32_t, uint32_t, uint32_t, char *, int, char *, int, int, int, int, int, int, const char *,
int, const char *, char *, void **);
/* Prototypes */
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_transcribe_shutdown);
SWITCH_MODULE_RUNTIME_FUNCTION(mod_transcribe_runtime);
@@ -17,7 +24,42 @@ SWITCH_MODULE_LOAD_FUNCTION(mod_transcribe_load);
SWITCH_MODULE_DEFINITION(mod_google_transcribe, mod_transcribe_load, mod_transcribe_shutdown, NULL);
static switch_status_t do_stop(switch_core_session_t *session, char* bugname);
static switch_bool_t capture_callback_v1(switch_media_bug_t *bug, void *user_data, switch_abc_type_t type);
static switch_bool_t capture_callback_v2(switch_media_bug_t *bug, void *user_data, switch_abc_type_t type);
static switch_status_t do_stop(switch_core_session_t *session, char* bugname, speech_cleanup_callback_t cleanup_callback);
static switch_media_bug_callback_t get_bug_callback_from_version(GoogleCloudServiceVersion version) {
switch (version) {
case GoogleCloudServiceVersion_v1:
return capture_callback_v1;
case GoogleCloudServiceVersion_v2:
return capture_callback_v2;
default:
return capture_callback_v1;
}
}
static speech_cleanup_callback_t get_cleanup_callback_from_version(GoogleCloudServiceVersion version) {
switch (version) {
case GoogleCloudServiceVersion_v1:
return google_speech_session_cleanup_v1;
case GoogleCloudServiceVersion_v2:
return google_speech_session_cleanup_v2;
default:
return google_speech_session_cleanup_v1;
}
}
static speech_init_callback_t get_init_callback_from_version(GoogleCloudServiceVersion version) {
switch (version) {
case GoogleCloudServiceVersion_v1:
return google_speech_session_init_v1;
case GoogleCloudServiceVersion_v2:
return google_speech_session_init_v2;
default:
return google_speech_session_init_v1;
}
}
static void responseHandler(switch_core_session_t* session, const char * json, const char* bugname) {
@@ -69,19 +111,19 @@ static void responseHandler(switch_core_session_t* session, const char * json, c
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "transcription-vendor", "google");
}
else {
int error = 0;
cJSON* jMessage = cJSON_Parse(json);
if (jMessage) {
const char* type = cJSON_GetStringValue(cJSON_GetObjectItem(jMessage, "type"));
if (type && 0 == strcmp(type, "error")) {
error = 1;
switch_event_create_subclass(&event, SWITCH_EVENT_CUSTOM, TRANSCRIBE_EVENT_ERROR);
}
cJSON_Delete(jMessage);
}
if (!error) {
switch_event_create_subclass(&event, SWITCH_EVENT_CUSTOM, TRANSCRIBE_EVENT_RESULTS);
}
int error = 0;
cJSON* jMessage = cJSON_Parse(json);
if (jMessage) {
const char* type = cJSON_GetStringValue(cJSON_GetObjectItem(jMessage, "type"));
if (type && 0 == strcmp(type, "error")) {
error = 1;
switch_event_create_subclass(&event, SWITCH_EVENT_CUSTOM, TRANSCRIBE_EVENT_ERROR);
}
cJSON_Delete(jMessage);
}
if (!error) {
switch_event_create_subclass(&event, SWITCH_EVENT_CUSTOM, TRANSCRIBE_EVENT_RESULTS);
}
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "%s json payload: %s.\n", bugname ? bugname : "google_transcribe", json);
switch_channel_event_set_data(channel, event);
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "transcription-vendor", "google");
@@ -91,29 +133,29 @@ static void responseHandler(switch_core_session_t* session, const char * json, c
switch_event_fire(&event);
}
static switch_bool_t capture_callback(switch_media_bug_t *bug, void *user_data, switch_abc_type_t type)
static switch_bool_t capture_callback(switch_media_bug_t *bug, void *user_data, switch_abc_type_t type,
speech_frame_callback_t frame_callback, speech_cleanup_callback_t cleanup_callback)
{
switch_core_session_t *session = switch_core_media_bug_get_session(bug);
struct cap_cb* cb = (struct cap_cb*) switch_core_media_bug_get_user_data(bug);
switch (type) {
case SWITCH_ABC_TYPE_INIT:
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Got SWITCH_ABC_TYPE_INIT.\n");
responseHandler(session, "start_of_transcript", cb->bugname);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Got SWITCH_ABC_TYPE_INIT.\n");
responseHandler(session, "start_of_transcript", cb->bugname);
break;
case SWITCH_ABC_TYPE_CLOSE:
{
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Got SWITCH_ABC_TYPE_CLOSE, calling google_speech_session_cleanup.\n");
responseHandler(session, "end_of_transcript", cb->bugname);
google_speech_session_cleanup(session, 1, bug);
cleanup_callback(session, 1, bug);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Finished SWITCH_ABC_TYPE_CLOSE.\n");
}
break;
case SWITCH_ABC_TYPE_READ:
return google_speech_frame(bug, user_data);
return frame_callback(bug, user_data);
break;
case SWITCH_ABC_TYPE_WRITE:
@@ -124,19 +166,27 @@ static switch_bool_t capture_callback(switch_media_bug_t *bug, void *user_data,
return SWITCH_TRUE;
}
static switch_bool_t capture_callback_v1(switch_media_bug_t *bug, void *user_data, switch_abc_type_t type) {
return capture_callback(bug, user_data, type, google_speech_frame_v1, google_speech_session_cleanup_v1);
}
static switch_bool_t capture_callback_v2(switch_media_bug_t *bug, void *user_data, switch_abc_type_t type) {
return capture_callback(bug, user_data, type, google_speech_frame_v2, google_speech_session_cleanup_v2);
}
static switch_status_t transcribe_input_callback(switch_core_session_t *session, void *input, switch_input_type_t input_type, void *data, unsigned int len){
if (input_type == SWITCH_INPUT_TYPE_EVENT) {
switch_event_t *event;
event = (switch_event_t *)input;
if (event->event_id == SWITCH_EVENT_DETECTED_SPEECH) {
return SWITCH_STATUS_BREAK;
}
switch_event_t *event;
event = (switch_event_t *)input;
if (event->event_id == SWITCH_EVENT_DETECTED_SPEECH) {
return SWITCH_STATUS_BREAK;
}
}
return SWITCH_STATUS_SUCCESS;
}
static switch_status_t do_stop(switch_core_session_t *session, char *bugname)
static switch_status_t do_stop(switch_core_session_t *session, char *bugname, speech_cleanup_callback_t cleanup_callback)
{
switch_status_t status = SWITCH_STATUS_SUCCESS;
switch_channel_t *channel = switch_core_session_get_channel(session);
@@ -144,7 +194,7 @@ static switch_status_t do_stop(switch_core_session_t *session, char *bugname)
if (bug) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Received user command command, calling google_speech_session_cleanup (possibly to stop prev transcribe)\n");
status = google_speech_session_cleanup(session, 0, bug);
status = cleanup_callback(session, 0, bug);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "stopped transcription.\n");
}
@@ -153,7 +203,8 @@ static switch_status_t do_stop(switch_core_session_t *session, char *bugname)
static switch_status_t start_capture2(switch_core_session_t *session, switch_media_bug_flag_t flags,
uint32_t sample_rate, char* lang, int interim, int single_utterance, int separate_recognition, int max_alternatives,
int profinity_filter, int word_time_offset, int punctuation, const char* model, int enhanced, const char* hints, char* play_file)
int profanity_filter, int word_time_offset, int punctuation, const char* model, int enhanced, const char* hints,
char* play_file, GoogleCloudServiceVersion version)
{
switch_channel_t *channel = switch_core_session_get_channel(session);
switch_media_bug_t *bug;
@@ -165,7 +216,7 @@ static switch_status_t start_capture2(switch_core_session_t *session, switch_med
if (switch_channel_get_private(channel, MY_BUG_NAME)) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "removing bug from previous transcribe\n");
do_stop(session, MY_BUG_NAME);
do_stop(session, MY_BUG_NAME, get_cleanup_callback_from_version(version));
}
switch_core_session_get_read_impl(session, &read_impl);
@@ -175,13 +226,15 @@ static switch_status_t start_capture2(switch_core_session_t *session, switch_med
}
samples_per_second = !strcasecmp(read_impl.iananame, "g722") ? read_impl.actual_samples_per_second : read_impl.samples_per_second;
status = get_init_callback_from_version(version)(session, responseHandler, sample_rate, samples_per_second, flags & SMBF_STEREO ? 2 : 1, lang, interim, MY_BUG_NAME, single_utterance,
separate_recognition, max_alternatives, profanity_filter, word_time_offset, punctuation, model, enhanced, hints, play_file, &pUserData);
if (SWITCH_STATUS_FALSE == google_speech_session_init(session, responseHandler, sample_rate, samples_per_second, flags & SMBF_STEREO ? 2 : 1, lang, interim, MY_BUG_NAME, single_utterance,
separate_recognition, max_alternatives, profinity_filter, word_time_offset, punctuation, model, enhanced, hints, play_file, &pUserData)) {
if (SWITCH_STATUS_FALSE == status) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error initializing google speech session.\n");
return SWITCH_STATUS_FALSE;
}
if ((status = switch_core_media_bug_add(session, "google_transcribe", NULL, capture_callback, pUserData, 0, flags, &bug)) != SWITCH_STATUS_SUCCESS) {
if ((status = switch_core_media_bug_add(session, "google_transcribe", NULL, get_bug_callback_from_version(version), pUserData, 0, flags, &bug)) != SWITCH_STATUS_SUCCESS) {
return status;
}
@@ -197,22 +250,23 @@ static switch_status_t start_capture2(switch_core_session_t *session, switch_med
}
static switch_status_t start_capture(switch_core_session_t *session, switch_media_bug_flag_t flags,
char* lang, int interim, char* bugname)
char* lang, int interim, char* bugname, GoogleCloudServiceVersion version)
{
switch_channel_t *channel = switch_core_session_get_channel(session);
switch_media_bug_t *bug;
switch_status_t status;
switch_codec_implementation_t read_impl = { 0 };
void *pUserData;
uint32_t sample_rate = DEFAULT_SAMPLE_RATE;
uint32_t samples_per_second;
int single_utterance = 0, separate_recognition = 0, max_alternatives = 0, profanity_filter = 0, word_time_offset = 0, punctuation = 0, enhanced = 0;
const char* hints = NULL;
const char* model = NULL;
const char* model = NULL;
const char* var;
if (switch_channel_get_private(channel, bugname)) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "removing bug from previous transcribe\n");
do_stop(session, bugname);
do_stop(session, bugname, get_cleanup_callback_from_version(version));
}
if (switch_true(switch_channel_get_variable(channel, "GOOGLE_SPEECH_SINGLE_UTTERANCE"))) {
@@ -229,6 +283,11 @@ static switch_status_t start_capture(switch_core_session_t *session, switch_medi
max_alternatives = atoi(var);
}
// sample rate
if ((var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_SAMPLE_RATE"))) {
sample_rate = atoi(var);
}
// profanity filter
if (switch_true(switch_channel_get_variable(channel, "GOOGLE_SPEECH_PROFANITY_FILTER"))) {
profanity_filter = 1;
@@ -266,14 +325,16 @@ static switch_status_t start_capture(switch_core_session_t *session, switch_medi
}
samples_per_second = !strcasecmp(read_impl.iananame, "g722") ? read_impl.actual_samples_per_second : read_impl.samples_per_second;
status = SWITCH_STATUS_FALSE;
status = get_init_callback_from_version(version)(session, responseHandler, sample_rate, samples_per_second, flags & SMBF_STEREO ? 2 : 1, lang, interim, bugname, single_utterance,
separate_recognition, max_alternatives, profanity_filter, word_time_offset, punctuation, model, enhanced, hints, NULL, &pUserData);
if (SWITCH_STATUS_FALSE == google_speech_session_init(session, responseHandler, DEFAULT_SAMPLE_RATE, samples_per_second, flags & SMBF_STEREO ? 2 : 1, lang, interim, bugname, single_utterance,
separate_recognition, max_alternatives, profanity_filter, word_time_offset, punctuation, model, enhanced, hints, NULL, &pUserData)) {
if (SWITCH_STATUS_FALSE == status) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error initializing google speech session.\n");
return SWITCH_STATUS_FALSE;
}
if ((status = switch_core_media_bug_add(session, bugname, NULL, capture_callback, pUserData, 0, flags, &bug)) != SWITCH_STATUS_SUCCESS) {
if ((status = switch_core_media_bug_add(session, bugname, NULL, get_bug_callback_from_version(version), pUserData, 0, flags, &bug)) != SWITCH_STATUS_SUCCESS) {
return status;
}
@@ -283,7 +344,7 @@ static switch_status_t start_capture(switch_core_session_t *session, switch_medi
}
// #define TRANSCRIBE_API_SYNTAX "<uuid> [start|stop] [lang-code] [interim] [single-utterance](bool) [seperate-recognition](bool) [max-alternatives](int) [profinity-filter](bool) [word-time](bool) [punctuation](bool) [model](string) [enhanced](true) [hints](string without space) [play-file]"
#define TRANSCRIBE2_API_SYNTAX "<uuid> [start|stop] [lang-code] [interim] [single-utterance] [seperate-recognition] [max-alternatives] [profinity-filter] [word-time] [punctuation] [sample-rate] [model] [enhanced] [hints] [play-file]"
#define TRANSCRIBE2_API_SYNTAX "<uuid> [start|stop] [lang-code] [interim] [single-utterance] [separate-recognition] [max-alternatives] [profanity-filter] [word-time] [punctuation] [sample-rate] [model] [enhanced] [hints] [play-file]"
SWITCH_STANDARD_API(transcribe2_function)
{
char *mycmd = NULL, *argv[20] = { 0 };
@@ -295,6 +356,9 @@ SWITCH_STANDARD_API(transcribe2_function)
switch_status_t status = SWITCH_STATUS_FALSE;
switch_media_bug_flag_t flags = SMBF_READ_STREAM /* | SMBF_WRITE_STREAM | SMBF_READ_PING */;
switch_channel_t *channel;
const char* var;
GoogleCloudServiceVersion version = GoogleCloudServiceVersion_v1;
if (!zstr(cmd) && (mycmd = strdup(cmd))) {
argc = switch_separate_string(mycmd, ' ', argv, (sizeof(argv) / sizeof(argv[0])));
@@ -311,16 +375,24 @@ SWITCH_STANDARD_API(transcribe2_function)
switch_core_session_t *lsession = NULL;
if ((lsession = switch_core_session_locate(argv[0]))) {
channel = switch_core_session_get_channel(lsession);
if ((var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_CLOUD_SERVICES_VERSION"))) {
if (!strcasecmp(var, "v1"))
version = GoogleCloudServiceVersion_v1;
else if (!strcasecmp(var, "v2"))
version = GoogleCloudServiceVersion_v2;
}
if (!strcasecmp(argv[1], "stop")) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "stop transcribing\n");
status = do_stop(lsession, MY_BUG_NAME);
status = do_stop(lsession, MY_BUG_NAME, get_cleanup_callback_from_version(version));
} else if (!strcasecmp(argv[1], "start")) {
char* lang = argv[2];
int interim = argc > 3 && !strcmp(argv[3], "true");
char* lang = argv[2];
int interim = argc > 3 && !strcmp(argv[3], "true");
int single_utterance = !strcmp(argv[4], "true"); // single-utterance
int separate_recognition = !strcmp(argv[5], "true"); // sepreate-recognition
int separate_recognition = !strcmp(argv[5], "true"); // separate-recognition
int max_alternatives = atoi(argv[6]); // max-alternatives
int profinity_filter = !strcmp(argv[7], "true"); // profinity-filter
int profanity_filter = !strcmp(argv[7], "true"); // profanity-filter
int word_time_offset = !strcmp(argv[8], "true"); // word-time
int punctuation = !strcmp(argv[9], "true"); //punctuation
if (argc > 10) {
@@ -336,9 +408,9 @@ SWITCH_STANDARD_API(transcribe2_function)
if (argc > 14){
play_file = argv[14];
}
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "start transcribing %s %s\n", lang, interim ? "interim": "complete");
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "start transcribing %s %s\n", lang, interim ? "interim": "complete");
status = start_capture2(lsession, flags, sample_rate, lang, interim, single_utterance, separate_recognition,max_alternatives,
profinity_filter, word_time_offset, punctuation, model, enhanced, hints, play_file);
profanity_filter, word_time_offset, punctuation, model, enhanced, hints, play_file, version);
}
switch_core_session_rwunlock(lsession);
}
@@ -363,6 +435,9 @@ SWITCH_STANDARD_API(transcribe_function)
int argc = 0;
switch_status_t status = SWITCH_STATUS_FALSE;
switch_media_bug_flag_t flags = SMBF_READ_STREAM /* | SMBF_WRITE_STREAM | SMBF_READ_PING */;
switch_channel_t *channel;
const char* var;
GoogleCloudServiceVersion version = GoogleCloudServiceVersion_v1;
if (!zstr(cmd) && (mycmd = strdup(cmd))) {
argc = switch_separate_string(mycmd, ' ', argv, (sizeof(argv) / sizeof(argv[0])));
@@ -379,20 +454,28 @@ SWITCH_STANDARD_API(transcribe_function)
switch_core_session_t *lsession = NULL;
if ((lsession = switch_core_session_locate(argv[0]))) {
channel = switch_core_session_get_channel(lsession);
if ((var = switch_channel_get_variable(channel, "GOOGLE_SPEECH_CLOUD_SERVICES_VERSION"))) {
if (!strcasecmp(var, "v1"))
version = GoogleCloudServiceVersion_v1;
else if (!strcasecmp(var, "v2"))
version = GoogleCloudServiceVersion_v2;
}
if (!strcasecmp(argv[1], "stop")) {
char *bugname = argc > 2 ? argv[2] : MY_BUG_NAME;
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "stop transcribing\n");
status = do_stop(lsession, bugname);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "stop transcribing\n");
status = do_stop(lsession, bugname, get_cleanup_callback_from_version(version));
} else if (!strcasecmp(argv[1], "start")) {
char* lang = argv[2];
int interim = argc > 3 && !strcmp(argv[3], "interim");
char* lang = argv[2];
int interim = argc > 3 && !strcmp(argv[3], "interim");
char *bugname = argc > 5 ? argv[5] : MY_BUG_NAME;
if (argc > 4 && !strcmp(argv[4], "stereo")) {
flags |= SMBF_WRITE_STREAM ;
flags |= SMBF_STEREO;
flags |= SMBF_WRITE_STREAM ;
flags |= SMBF_STEREO;
}
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "%s start transcribing %s %s\n", bugname, lang, interim ? "interim": "complete");
status = start_capture(lsession, flags, lang, interim, bugname);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "%s start transcribing %s %s\n", bugname, lang, interim ? "interim": "complete");
status = start_capture(lsession, flags, lang, interim, bugname, version);
}
switch_core_session_rwunlock(lsession);
}
@@ -481,4 +564,3 @@ SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_transcribe_shutdown)
switch_event_free_subclass(TRANSCRIBE_EVENT_PLAY_INTERRUPT);
return SWITCH_STATUS_SUCCESS;
}
@@ -37,6 +37,10 @@ struct cap_cb {
#else
/* per-channel data */
typedef void (*responseHandler_t)(switch_core_session_t* session, const char* json, const char* bugname);
typedef enum GoogleCloudServiceVersion {
GoogleCloudServiceVersion_v1,
GoogleCloudServiceVersion_v2
} GoogleCloudServiceVersion;
struct cap_cb {
switch_mutex_t *mutex;