#include #include #include #include #include #include #include "audio.hxx" #include "opus.h" #include "opus_defines.h" #include "rtp.hxx" #include "server.hxx" #define OPUS_FRAME_SIZE 3840 std::string getline(); Server::Server(std::pair local_socket, std::pair remote_socket, AudioDevice dev, asio::io_context &io) : local_address(local_socket.first), device(std::move(dev)), rtp(local_socket, remote_socket, io) { this->logger = spdlog::get("audpipe"); int error = OPUS_OK; this->opusenc = opus_encoder_create(48000, 2, OPUS_APPLICATION_VOIP, &error); if (error != OPUS_OK) { auto error_msg = std::format("Failed to create opus encoder. Reason: {}", opus_strerror(error)); this->logger->critical(error_msg); throw std::runtime_error(error_msg); } opus_encoder_ctl(this->opusenc, OPUS_SET_BITRATE(192000)); opus_encoder_ctl(this->opusenc, OPUS_SET_COMPLEXITY(10)); opus_encoder_ctl(this->opusenc, OPUS_SET_DTX(1)); opus_encoder_ctl( this->opusenc, OPUS_SET_VBR( 1)); // TODO: Encryption needs to be handled specially with VBR on. opus_encoder_ctl(this->opusenc, OPUS_SET_VBR_CONSTRAINT(0)); opus_encoder_ctl(this->opusenc, OPUS_SET_INBAND_FEC(0)); opus_encoder_ctl(this->opusenc, OPUS_SET_PACKET_LOSS_PERC(0)); opus_encoder_ctl(this->opusenc, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_FULLBAND)); opus_encoder_ctl(this->opusenc, OPUS_SET_SIGNAL(OPUS_SIGNAL_VOICE)); this->opus_enc_outbuf = std::vector(); this->opus_enc_outbuf.resize(OPUS_FRAME_SIZE); this->opus_enc_outbuf_size = 0; this->pcm_stereo_buf.resize(960 * 2); auto &audio = AudioBackend::instance(); audio.set_data_callback([&](const uint8_t *data, size_t len) { this->bytes_to_opus(data, len); uint8_t *opus_data = static_cast(this->opus_enc_outbuf.data()); size_t opus_data_len = this->opus_enc_outbuf_size; if (opus_data_len == 0) { return; } // TODO: Make this check for errors/fail etc. For now just hand it off. this->rtp.write_frames(opus_data, opus_data_len); }); } Server::~Server() { stop(); if (this->opusenc != nullptr) { opus_encoder_destroy(this->opusenc); this->opusenc = nullptr; } } void Server::run() { this->running = true; try { AudioBackend::instance().record(this->device); } catch (...) { // We trust upstream code to log before throwing so just ignore the // exception. this->running = false; throw; } this->running = false; } void Server::stop() { this->running = false; this->rtp.stop(); AudioBackend::instance().stop_recording(); } AudioDevice Server::choose_device_interactive(std::vector inputs) { for (int i = 0; i < inputs.size(); i++) { std::cout << std::format("{})\t{}", i + 1, inputs.at(i).description) << std::endl; } std::cout << "Choose a device to record from (1): " << std::flush; int idx = 1; std::istringstream iss(getline()); if (!(iss >> idx) || (iss >> std::ws, !iss.eof()) || idx <= 0 || idx > inputs.size()) { auto logger = spdlog::get("audpipe"); logger->debug("Invalid input, defaulting to 1."); idx = 1; } AudioDevice dev = inputs.at(idx - 1); return dev; } // Source - https://stackoverflow.com/a/546470 // Posted by Johannes Schaub - litb, modified by community. See post 'Timeline' // for change history Retrieved 2026-03-13, License - CC BY-SA 3.0 std::string getline() { std::string str; std::getline(std::cin, str); return str; } // Populates opus_enc_outbuf. void Server::bytes_to_opus(const uint8_t *data, size_t len) { // 960 samples/channel is 20 ms at 48 kHz. const size_t max_bytes = OPUS_FRAME_SIZE; this->opus_enc_outbuf_size = 0; if (data == nullptr || len < sizeof(opus_int16)) { return; } // Clear the outbuf. memset(this->opus_enc_outbuf.data(), 0, this->opus_enc_outbuf.size()); const auto *in = reinterpret_cast(data); const size_t samples_total = len / sizeof(opus_int16); const opus_int16 *encode_in = nullptr; if (this->device.channels == 1) { if (samples_total < 960) { return; } // Upmix mono capture to stereo expected by encoder. for (size_t i = 0; i < 960; i++) { auto sample = in[i]; this->pcm_stereo_buf[2 * i] = sample; this->pcm_stereo_buf[2 * i + 1] = sample; } encode_in = this->pcm_stereo_buf.data(); } else { if (samples_total < 1920) { return; } encode_in = in; } auto res = opus_encode(this->opusenc, encode_in, 960, this->opus_enc_outbuf.data(), max_bytes); // TODO: Recover from this. if (res < 0) { auto error_msg = std::format("Failed to encode opus frame. Reason: {}", opus_strerror(res)); this->logger->critical(error_msg); throw std::runtime_error(error_msg); } // Else we have the size of the output frame. I would rather not reallocate // the block of memory, seems easier to just store pointer and length. this->opus_enc_outbuf_size = res; this->logger->debug("Got opus frame with size {}", res); }