mirror of
https://github.com/jambonz/freeswitch-modules.git
synced 2026-01-25 02:08:27 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74bfc3152f | |||
| 9cdc5fdfca | |||
| 4e57f73c7e | |||
| 4925d31f95 |
@@ -7,4 +7,4 @@ mod_aws_lex_la_CFLAGS = $(AM_CFLAGS)
|
||||
mod_aws_lex_la_CXXFLAGS = $(AM_CXXFLAGS) -std=c++11 -I${switch_srcdir}/libs/aws-sdk-cpp/aws-cpp-sdk-core/include -I${switch_srcdir}/libs/aws-sdk-cpp/aws-cpp-sdk-lexv2-runtime/include -I${switch_srcdir}/libs/aws-sdk-cpp/build/.deps/install/include
|
||||
|
||||
mod_aws_lex_la_LIBADD = $(switch_builddir)/libfreeswitch.la
|
||||
mod_aws_lex_la_LDFLAGS = -avoid-version -module -no-undefined -L${switch_srcdir}/libs/aws-sdk-cpp/build/.deps/install/lib -L${switch_srcdir}/libs/aws-sdk-cpp/build/aws-cpp-sdk-core -L${switch_srcdir}/libs/aws-sdk-cpp/build/aws-cpp-sdk-lexv2-runtime -laws-cpp-sdk-lexv2-runtime -laws-cpp-sdk-core -laws-c-event-stream -laws-checksums -laws-c-common -lpthread -lcurl -lcrypto -lssl -lz
|
||||
mod_aws_lex_la_LDFLAGS = -avoid-version -module -no-undefined -L/usr/local/lib -L${switch_srcdir}/libs/aws-sdk-cpp/build/.deps/install/lib -L${switch_srcdir}/libs/aws-sdk-cpp/build/aws-cpp-sdk-core -L${switch_srcdir}/libs/aws-sdk-cpp/build/aws-cpp-sdk-lexv2-runtime -laws-cpp-sdk-lexv2-runtime -laws-cpp-sdk-core -laws-c-event-stream -laws-checksums -laws-c-common -lpthread -lcurl -lcrypto -lssl -lz
|
||||
@@ -7,4 +7,4 @@ mod_aws_transcribe_la_CFLAGS = $(AM_CFLAGS)
|
||||
mod_aws_transcribe_la_CXXFLAGS = $(AM_CXXFLAGS) -std=c++11 -I${switch_srcdir}/libs/aws-sdk-cpp/aws-cpp-sdk-core/include -I${switch_srcdir}/libs/aws-sdk-cpp/aws-cpp-sdk-transcribestreaming/include -I${switch_srcdir}/libs/aws-sdk-cpp/build/.deps/install/include
|
||||
|
||||
mod_aws_transcribe_la_LIBADD = $(switch_builddir)/libfreeswitch.la
|
||||
mod_aws_transcribe_la_LDFLAGS = -avoid-version -module -no-undefined -L${switch_srcdir}/libs/aws-sdk-cpp/build/.deps/install/lib -L${switch_srcdir}/libs/aws-sdk-cpp/build/aws-cpp-sdk-core -L${switch_srcdir}/libs/aws-sdk-cpp/build/aws-cpp-sdk-transcribestreaming -laws-cpp-sdk-transcribestreaming -laws-cpp-sdk-core -laws-c-event-stream -laws-checksums -laws-c-common -lpthread -lcurl -lcrypto -lssl -lz
|
||||
mod_aws_transcribe_la_LDFLAGS = -avoid-version -module -no-undefined -L/usr/local/lib -L${switch_srcdir}/libs/aws-sdk-cpp/build/.deps/install/lib -L${switch_srcdir}/libs/aws-sdk-cpp/build/aws-cpp-sdk-core -L${switch_srcdir}/libs/aws-sdk-cpp/build/aws-cpp-sdk-transcribestreaming -laws-cpp-sdk-transcribestreaming -laws-cpp-sdk-core -laws-c-event-stream -laws-checksums -laws-c-common -laws-crt-cpp -lpthread -lcurl -lcrypto -lssl -lz
|
||||
+9
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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];
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user