#include #include #include #include #include #include #include #include #include #include #include #define CONCAT(a, b) ((std::string(a) + b).c_str()) SDRPP_MOD_INFO{ /* Name: */ "audio_sink", /* Description: */ "Audio sink module for SDR++", /* Author: */ "Ryzerth", /* Version: */ 0, 1, 0, /* Max instances */ 1 }; ConfigManager config; class AudioSink : SinkManager::Sink { public: AudioSink(SinkManager::Stream* stream, std::string streamName) { _stream = stream; _streamName = streamName; s2m.init(_stream->sinkOut); monoPacker.init(&s2m.out, 512); stereoPacker.init(_stream->sinkOut, 512); #if RTAUDIO_VERSION_MAJOR >= 6 audio.setErrorCallback(&reportErrorsAsException); #endif bool created = false; std::string device = ""; config.acquire(); if (!config.conf.contains(_streamName)) { created = true; config.conf[_streamName]["device"] = ""; config.conf[_streamName]["devices"] = json({}); } device = config.conf[_streamName]["device"]; config.release(created); RtAudio::DeviceInfo info; #if RTAUDIO_VERSION_MAJOR >= 6 for (int i : audio.getDeviceIds()) { #else int count = audio.getDeviceCount(); for (int i = 0; i < count; i++) { #endif try { info = audio.getDeviceInfo(i); if (info.outputChannels == 0) { continue; } if (info.isDefaultOutput) { defaultDevId = devList.size(); } devList.push_back(info); deviceIds.push_back(i); txtDevList += info.name; txtDevList += '\0'; } catch (const std::exception& e) { flog::error("AudioSinkModule Error getting audio device info: id={0}: {1}", i, e.what()); } } selectByName(device); } ~AudioSink() { stop(); } void start() { if (running) { return; } running = doStart(); } void stop() { if (!running) { return; } doStop(); running = false; } void selectFirst() { selectById(defaultDevId); } void selectByName(std::string name) { for (int i = 0; i < devList.size(); i++) { if (devList[i].name == name) { selectById(i); return; } } selectFirst(); } void selectById(int id) { devId = id; bool created = false; config.acquire(); if (!config.conf[_streamName]["devices"].contains(devList[id].name)) { created = true; config.conf[_streamName]["devices"][devList[id].name] = devList[id].preferredSampleRate; } sampleRate = config.conf[_streamName]["devices"][devList[id].name]; config.release(created); sampleRates = devList[id].sampleRates; sampleRatesTxt = ""; char buf[256]; bool found = false; unsigned int defaultId = 0; unsigned int defaultSr = devList[id].preferredSampleRate; for (int i = 0; i < sampleRates.size(); i++) { if (sampleRates[i] == sampleRate) { found = true; srId = i; } if (sampleRates[i] == defaultSr) { defaultId = i; } sprintf(buf, "%d", sampleRates[i]); sampleRatesTxt += buf; sampleRatesTxt += '\0'; } if (!found) { sampleRate = defaultSr; srId = defaultId; } _stream->setSampleRate(sampleRate); if (running) { doStop(); } if (running) { doStart(); } } void menuHandler() { float menuWidth = ImGui::GetContentRegionAvail().x; ImGui::SetNextItemWidth(menuWidth); if (ImGui::Combo(("##_audio_sink_dev_" + _streamName).c_str(), &devId, txtDevList.c_str())) { selectById(devId); config.acquire(); config.conf[_streamName]["device"] = devList[devId].name; config.release(true); } ImGui::SetNextItemWidth(menuWidth); if (ImGui::Combo(("##_audio_sink_sr_" + _streamName).c_str(), &srId, sampleRatesTxt.c_str())) { sampleRate = sampleRates[srId]; _stream->setSampleRate(sampleRate); if (running) { doStop(); doStart(); } config.acquire(); config.conf[_streamName]["devices"][devList[devId].name] = sampleRate; config.release(true); } } #if RTAUDIO_VERSION_MAJOR >= 6 static void reportErrorsAsException(RtAudioErrorType type, const std::string& errorText) { switch (type) { case RtAudioErrorType::RTAUDIO_NO_ERROR: return; case RtAudioErrorType::RTAUDIO_WARNING: case RtAudioErrorType::RTAUDIO_NO_DEVICES_FOUND: case RtAudioErrorType::RTAUDIO_DEVICE_DISCONNECT: flog::warn("AudioSink: {0} ({1})", errorText, (int)type); break; default: throw std::runtime_error(errorText); } } #endif private: bool doStart() { RtAudio::StreamParameters parameters; parameters.deviceId = deviceIds[devId]; parameters.nChannels = 2; unsigned int bufferFrames = sampleRate / 60; RtAudio::StreamOptions opts; opts.flags = RTAUDIO_MINIMIZE_LATENCY; opts.streamName = _streamName; try { audio.openStream(¶meters, NULL, RTAUDIO_FLOAT32, sampleRate, &bufferFrames, &callback, this, &opts); stereoPacker.setSampleCount(bufferFrames); audio.startStream(); stereoPacker.start(); } catch (const std::exception& e) { flog::error("Could not open audio device {0}", e.what()); return false; } flog::info("RtAudio stream open"); return true; } void doStop() { s2m.stop(); monoPacker.stop(); stereoPacker.stop(); monoPacker.out.stopReader(); stereoPacker.out.stopReader(); audio.stopStream(); audio.closeStream(); monoPacker.out.clearReadStop(); stereoPacker.out.clearReadStop(); } static int callback(void* outputBuffer, void* inputBuffer, unsigned int nBufferFrames, double streamTime, RtAudioStreamStatus status, void* userData) { AudioSink* _this = (AudioSink*)userData; int count = _this->stereoPacker.out.read(); if (count < 0) { return 0; } // For debug purposes only... // if (nBufferFrames != count) { flog::warn("Buffer size mismatch, wanted {0}, was asked for {1}", count, nBufferFrames); } // for (int i = 0; i < count; i++) { // if (_this->stereoPacker.out.readBuf[i].l == NAN || _this->stereoPacker.out.readBuf[i].r == NAN) { flog::error("NAN in audio data"); } // if (_this->stereoPacker.out.readBuf[i].l == INFINITY || _this->stereoPacker.out.readBuf[i].r == INFINITY) { flog::error("INFINITY in audio data"); } // if (_this->stereoPacker.out.readBuf[i].l == -INFINITY || _this->stereoPacker.out.readBuf[i].r == -INFINITY) { flog::error("-INFINITY in audio data"); } // } memcpy(outputBuffer, _this->stereoPacker.out.readBuf, nBufferFrames * sizeof(dsp::stereo_t)); _this->stereoPacker.out.flush(); return 0; } SinkManager::Stream* _stream; dsp::convert::StereoToMono s2m; dsp::buffer::Packer monoPacker; dsp::buffer::Packer stereoPacker; std::string _streamName; int srId = 0; int devCount; int devId = 0; bool running = false; unsigned int defaultDevId = 0; std::vector devList; std::vector deviceIds; std::string txtDevList; std::vector sampleRates; std::string sampleRatesTxt; unsigned int sampleRate = 48000; RtAudio audio; }; class AudioSinkModule : public ModuleManager::Instance { public: AudioSinkModule(std::string name) { this->name = name; provider.create = create_sink; provider.ctx = this; sigpath::sinkManager.registerSinkProvider("Audio", provider); } ~AudioSinkModule() { // Unregister sink, this will automatically stop and delete all instances of the audio sink sigpath::sinkManager.unregisterSinkProvider("Audio"); } void postInit() {} void enable() { enabled = true; } void disable() { enabled = false; } bool isEnabled() { return enabled; } private: static SinkManager::Sink* create_sink(SinkManager::Stream* stream, std::string streamName, void* ctx) { return (SinkManager::Sink*)(new AudioSink(stream, streamName)); } std::string name; bool enabled = true; SinkManager::SinkProvider provider; }; MOD_EXPORT void _INIT_() { json def = json({}); config.setPath(core::args["root"].s() + "/audio_sink_config.json"); config.load(def); config.enableAutoSave(); } MOD_EXPORT void* _CREATE_INSTANCE_(std::string name) { AudioSinkModule* instance = new AudioSinkModule(name); return instance; } MOD_EXPORT void _DELETE_INSTANCE_(void* instance) { delete (AudioSinkModule*)instance; } MOD_EXPORT void _END_() { config.disableAutoSave(); config.save(); }