#include "file_loader.h" #include #include #include #include #include #include #include #include #include #include #include #include #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 Id2FileMap_t; static Id2FileMap_t id2FileMap; static boost::object_pool 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 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 convert_mp3_to_linear(FileInfo_t *finfo, int8_t *data, size_t len) { std::vector 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(audio), reinterpret_cast(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 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(reinterpret_cast(buf), reinterpret_cast(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 } }