mirror of
https://github.com/AlexandreRouma/SDRPlusPlus.git
synced 2025-06-25 12:07:49 +02:00
Changed project structure
This commit is contained in:
40
sink_modules/audio_sink/CMakeLists.txt
Normal file
40
sink_modules/audio_sink/CMakeLists.txt
Normal file
@ -0,0 +1,40 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(audio_sink)
|
||||
|
||||
if (MSVC)
|
||||
add_compile_options(/O2 /Ob2 /std:c++17 /EHsc)
|
||||
elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
add_compile_options(-O3 -std=c++17 -Wno-unused-command-line-argument -undefined dynamic_lookup)
|
||||
else ()
|
||||
add_compile_options(-O3 -std=c++17)
|
||||
endif ()
|
||||
|
||||
file(GLOB SRC "src/*.cpp")
|
||||
|
||||
include_directories("src/")
|
||||
|
||||
add_library(audio_sink SHARED ${SRC})
|
||||
target_link_libraries(audio_sink PRIVATE sdrpp_core)
|
||||
set_target_properties(audio_sink PROPERTIES PREFIX "")
|
||||
|
||||
if (MSVC)
|
||||
# Lib path
|
||||
target_link_directories(audio_sink PUBLIC "C:/Program Files (x86)/RtAudio/lib")
|
||||
|
||||
# Misc headers
|
||||
target_include_directories(audio_sink PUBLIC "C:/Program Files (x86)/RtAudio/include/rtaudio")
|
||||
|
||||
target_link_libraries(audio_sink PUBLIC rtaudio)
|
||||
else (MSVC)
|
||||
find_package(PkgConfig)
|
||||
|
||||
pkg_check_modules(RTAUDIO REQUIRED rtaudio)
|
||||
|
||||
target_include_directories(audio_sink PUBLIC ${RTAUDIO_INCLUDE_DIRS})
|
||||
target_link_directories(audio_sink PUBLIC ${RTAUDIO_LIBRARY_DIRS})
|
||||
target_link_libraries(audio_sink PUBLIC ${RTAUDIO_LIBRARIES})
|
||||
|
||||
endif ()
|
||||
|
||||
# Install directives
|
||||
install(TARGETS audio_sink DESTINATION lib/sdrpp/plugins)
|
299
sink_modules/audio_sink/src/main.cpp
Normal file
299
sink_modules/audio_sink/src/main.cpp
Normal file
@ -0,0 +1,299 @@
|
||||
#include <imgui.h>
|
||||
#include <module.h>
|
||||
#include <gui/gui.h>
|
||||
#include <signal_path/signal_path.h>
|
||||
#include <signal_path/sink.h>
|
||||
#include <dsp/audio.h>
|
||||
#include <dsp/processing.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <RtAudio.h>
|
||||
#include <config.h>
|
||||
#include <options.h>
|
||||
|
||||
#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);
|
||||
|
||||
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);
|
||||
|
||||
int count = audio.getDeviceCount();
|
||||
RtAudio::DeviceInfo info;
|
||||
for (int i = 0; i < count; i++) {
|
||||
info = audio.getDeviceInfo(i);
|
||||
if (!info.probed) { continue; }
|
||||
if (info.outputChannels == 0) { continue; }
|
||||
if (info.isDefaultOutput) { defaultDevId = devList.size(); }
|
||||
devList.push_back(info);
|
||||
deviceIds.push_back(i);
|
||||
txtDevList += info.name;
|
||||
txtDevList += '\0';
|
||||
}
|
||||
|
||||
selectByName(device);
|
||||
}
|
||||
|
||||
~AudioSink() {
|
||||
|
||||
}
|
||||
|
||||
void start() {
|
||||
if (running) {
|
||||
return;
|
||||
}
|
||||
doStart();
|
||||
running = true;
|
||||
}
|
||||
|
||||
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::GetContentRegionAvailWidth();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void 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 ( RtAudioError& e ) {
|
||||
spdlog::error("Could not open audio device");
|
||||
return;
|
||||
}
|
||||
|
||||
spdlog::info("RtAudio stream open");
|
||||
}
|
||||
|
||||
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) { spdlog::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) { spdlog::error("NAN in audio data"); }
|
||||
// if (_this->stereoPacker.out.readBuf[i].l == INFINITY || _this->stereoPacker.out.readBuf[i].r == INFINITY) { spdlog::error("INFINITY in audio data"); }
|
||||
// if (_this->stereoPacker.out.readBuf[i].l == -INFINITY || _this->stereoPacker.out.readBuf[i].r == -INFINITY) { spdlog::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::StereoToMono s2m;
|
||||
dsp::Packer<float> monoPacker;
|
||||
dsp::Packer<dsp::stereo_t> stereoPacker;
|
||||
|
||||
std::string _streamName;
|
||||
|
||||
int srId = 0;
|
||||
int devCount;
|
||||
int devId = 0;
|
||||
bool running = false;
|
||||
|
||||
unsigned int defaultDevId = 0;
|
||||
|
||||
std::vector<RtAudio::DeviceInfo> devList;
|
||||
std::vector<unsigned int> deviceIds;
|
||||
std::string txtDevList;
|
||||
|
||||
std::vector<unsigned int> 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(options::opts.root + "/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();
|
||||
}
|
24
sink_modules/network_sink/CMakeLists.txt
Normal file
24
sink_modules/network_sink/CMakeLists.txt
Normal file
@ -0,0 +1,24 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(network_sink)
|
||||
|
||||
if (MSVC)
|
||||
add_compile_options(/O2 /Ob2 /std:c++17 /EHsc)
|
||||
elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
add_compile_options(-O3 -std=c++17 -Wno-unused-command-line-argument -undefined dynamic_lookup)
|
||||
else ()
|
||||
add_compile_options(-O3 -std=c++17)
|
||||
endif ()
|
||||
|
||||
file(GLOB SRC "src/*.cpp")
|
||||
|
||||
include_directories("src/")
|
||||
include_directories("../recorder/src")
|
||||
include_directories("../meteor_demodulator/src")
|
||||
include_directories("../radio/src")
|
||||
|
||||
add_library(network_sink SHARED ${SRC})
|
||||
target_link_libraries(network_sink PRIVATE sdrpp_core)
|
||||
set_target_properties(network_sink PROPERTIES PREFIX "")
|
||||
|
||||
# Install directives
|
||||
install(TARGETS network_sink DESTINATION lib/sdrpp/plugins)
|
356
sink_modules/network_sink/src/main.cpp
Normal file
356
sink_modules/network_sink/src/main.cpp
Normal file
@ -0,0 +1,356 @@
|
||||
#include <utils/networking.h>
|
||||
#include <imgui.h>
|
||||
#include <module.h>
|
||||
#include <gui/gui.h>
|
||||
#include <signal_path/signal_path.h>
|
||||
#include <signal_path/sink.h>
|
||||
#include <dsp/audio.h>
|
||||
#include <dsp/processing.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <config.h>
|
||||
#include <options.h>
|
||||
#include <gui/style.h>
|
||||
|
||||
#define CONCAT(a, b) ((std::string(a) + b).c_str())
|
||||
|
||||
SDRPP_MOD_INFO {
|
||||
/* Name: */ "network_sink",
|
||||
/* Description: */ "Network sink module for SDR++",
|
||||
/* Author: */ "Ryzerth",
|
||||
/* Version: */ 0, 1, 0,
|
||||
/* Max instances */ 1
|
||||
};
|
||||
|
||||
ConfigManager config;
|
||||
|
||||
enum {
|
||||
SINK_MODE_TCP,
|
||||
SINK_MODE_UDP
|
||||
};
|
||||
|
||||
const char* sinkModesTxt = "TCP\0UDP\0";
|
||||
|
||||
class NetworkSink : SinkManager::Sink {
|
||||
public:
|
||||
NetworkSink(SinkManager::Stream* stream, std::string streamName) {
|
||||
_stream = stream;
|
||||
_streamName = streamName;
|
||||
|
||||
// Load config
|
||||
config.acquire();
|
||||
if (!config.conf.contains(_streamName)) {
|
||||
config.conf[_streamName]["hostname"] = "localhost";
|
||||
config.conf[_streamName]["port"] = 7355;
|
||||
config.conf[_streamName]["protocol"] = SINK_MODE_UDP; // UDP
|
||||
config.conf[_streamName]["sampleRate"] = 48000.0;
|
||||
config.conf[_streamName]["stereo"] = false;
|
||||
config.conf[_streamName]["listening"] = false;
|
||||
}
|
||||
std::string host = config.conf[_streamName]["hostname"];
|
||||
strcpy(hostname, host.c_str());
|
||||
port = config.conf[_streamName]["port"];
|
||||
modeId = config.conf[_streamName]["protocol"];
|
||||
sampleRate = config.conf[_streamName]["sampleRate"];
|
||||
stereo = config.conf[_streamName]["stereo"];
|
||||
bool startNow = config.conf[_streamName]["listening"];
|
||||
config.release(true);
|
||||
|
||||
netBuf = new int16_t[STREAM_BUFFER_SIZE];
|
||||
|
||||
packer.init(_stream->sinkOut, 512);
|
||||
s2m.init(&packer.out);
|
||||
monoSink.init(&s2m.out, monoHandler, this);
|
||||
stereoSink.init(&packer.out, stereoHandler, this);
|
||||
|
||||
|
||||
// Create a list of sample rates
|
||||
for (int sr = 12000; sr < 200000; sr += 12000) {
|
||||
sampleRates.push_back(sr);
|
||||
}
|
||||
for (int sr = 11025; sr < 192000; sr += 11025) {
|
||||
sampleRates.push_back(sr);
|
||||
}
|
||||
|
||||
// Sort sample rate list
|
||||
std::sort(sampleRates.begin(), sampleRates.end(), [](double a, double b) { return (a < b); });
|
||||
|
||||
// Generate text list for UI
|
||||
char buffer[128];
|
||||
int id = 0;
|
||||
int _48kId;
|
||||
bool found = false;
|
||||
for (auto sr : sampleRates) {
|
||||
sprintf(buffer, "%d", (int)sr);
|
||||
sampleRatesTxt += buffer;
|
||||
sampleRatesTxt += '\0';
|
||||
if (sr == sampleRate) { srId = id; found = true; }
|
||||
if (sr == 48000.0) { _48kId = id; }
|
||||
id++;
|
||||
}
|
||||
if (!found) { srId = _48kId; sampleRate = 48000.0; }
|
||||
_stream->setSampleRate(sampleRate);
|
||||
|
||||
// Start if needed
|
||||
if (startNow) { startServer(); }
|
||||
}
|
||||
|
||||
~NetworkSink() {
|
||||
stopServer();
|
||||
delete[] netBuf;
|
||||
}
|
||||
|
||||
void start() {
|
||||
if (running) {
|
||||
return;
|
||||
}
|
||||
doStart();
|
||||
running = true;
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
doStop();
|
||||
running = false;
|
||||
}
|
||||
|
||||
void menuHandler() {
|
||||
float menuWidth = ImGui::GetContentRegionAvailWidth();
|
||||
|
||||
bool listening = (listener && listener->isListening()) || (conn && conn->isOpen());
|
||||
|
||||
if (listening) { style::beginDisabled(); }
|
||||
if (ImGui::InputText(CONCAT("##_network_sink_host_", _streamName), hostname, 1023)) {
|
||||
config.acquire();
|
||||
config.conf[_streamName]["hostname"] = hostname;
|
||||
config.release(true);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(menuWidth - ImGui::GetCursorPosX());
|
||||
if (ImGui::InputInt(CONCAT("##_network_sink_port_", _streamName), &port, 0, 0)) {
|
||||
config.acquire();
|
||||
config.conf[_streamName]["port"] = port;
|
||||
config.release(true);
|
||||
}
|
||||
|
||||
ImGui::LeftLabel("Protocol");
|
||||
ImGui::SetNextItemWidth(menuWidth - ImGui::GetCursorPosX());
|
||||
if (ImGui::Combo(CONCAT("##_network_sink_mode_", _streamName), &modeId, sinkModesTxt)) {
|
||||
config.acquire();
|
||||
config.conf[_streamName]["protocol"] = modeId;
|
||||
config.release(true);
|
||||
}
|
||||
|
||||
if (listening) { style::endDisabled(); }
|
||||
|
||||
ImGui::LeftLabel("Samplerate");
|
||||
ImGui::SetNextItemWidth(menuWidth - ImGui::GetCursorPosX());
|
||||
if (ImGui::Combo(CONCAT("##_network_sink_sr_", _streamName), &srId, sampleRatesTxt.c_str())) {
|
||||
sampleRate = sampleRates[srId];
|
||||
_stream->setSampleRate(sampleRate);
|
||||
packer.setSampleCount(sampleRate / 60);
|
||||
config.acquire();
|
||||
config.conf[_streamName]["sampleRate"] = sampleRate;
|
||||
config.release(true);
|
||||
}
|
||||
|
||||
if (ImGui::Checkbox(CONCAT("Stereo##_network_sink_stereo_", _streamName), &stereo)) {
|
||||
stop();
|
||||
start();
|
||||
config.acquire();
|
||||
config.conf[_streamName]["stereo"] = stereo;
|
||||
config.release(true);
|
||||
}
|
||||
|
||||
if (listening && ImGui::Button(CONCAT("Stop##_network_sink_stop_", _streamName), ImVec2(menuWidth, 0))) {
|
||||
stopServer();
|
||||
config.acquire();
|
||||
config.conf[_streamName]["listening"] = false;
|
||||
config.release(true);
|
||||
}
|
||||
else if (!listening && ImGui::Button(CONCAT("Start##_network_sink_stop_", _streamName), ImVec2(menuWidth, 0))) {
|
||||
startServer();
|
||||
config.acquire();
|
||||
config.conf[_streamName]["listening"] = true;
|
||||
config.release(true);
|
||||
}
|
||||
|
||||
ImGui::Text("Status:");
|
||||
ImGui::SameLine();
|
||||
if (conn && conn->isOpen()) {
|
||||
ImGui::TextColored(ImVec4(0.0, 1.0, 0.0, 1.0), (modeId == SINK_MODE_TCP) ? "Connected" : "Sending");
|
||||
}
|
||||
else if (listening) {
|
||||
ImGui::TextColored(ImVec4(1.0, 1.0, 0.0, 1.0), "Listening");
|
||||
}
|
||||
else {
|
||||
ImGui::Text("Idle");
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void doStart() {
|
||||
packer.start();
|
||||
if (stereo) {
|
||||
stereoSink.start();
|
||||
}
|
||||
else {
|
||||
spdlog::warn("Starting");
|
||||
s2m.start();
|
||||
monoSink.start();
|
||||
}
|
||||
}
|
||||
|
||||
void doStop() {
|
||||
packer.stop();
|
||||
s2m.stop();
|
||||
monoSink.stop();
|
||||
stereoSink.stop();
|
||||
}
|
||||
|
||||
void startServer() {
|
||||
if (modeId == SINK_MODE_TCP) {
|
||||
listener = net::listen(hostname, port);
|
||||
if (listener) {
|
||||
listener->acceptAsync(clientHandler, this);
|
||||
}
|
||||
}
|
||||
else {
|
||||
conn = net::openUDP("0.0.0.0", port, hostname, port, false);
|
||||
}
|
||||
}
|
||||
|
||||
void stopServer() {
|
||||
if (conn) { conn->close(); }
|
||||
if (listener) { listener->close(); }
|
||||
}
|
||||
|
||||
static void monoHandler(float* samples, int count, void* ctx) {
|
||||
NetworkSink* _this = (NetworkSink*)ctx;
|
||||
std::lock_guard lck(_this->connMtx);
|
||||
if (!_this->conn || !_this->conn->isOpen()) { return; }
|
||||
|
||||
volk_32f_s32f_convert_16i(_this->netBuf, (float*)samples, 32768.0f, count);
|
||||
|
||||
_this->conn->write(count*sizeof(int16_t), (uint8_t*)_this->netBuf);
|
||||
}
|
||||
|
||||
static void stereoHandler(dsp::stereo_t* samples, int count, void* ctx) {
|
||||
NetworkSink* _this = (NetworkSink*)ctx;
|
||||
std::lock_guard lck(_this->connMtx);
|
||||
if (!_this->conn || !_this->conn->isOpen()) { return; }
|
||||
|
||||
volk_32f_s32f_convert_16i(_this->netBuf, (float*)samples, 32768.0f, count*2);
|
||||
|
||||
_this->conn->write(count*2*sizeof(int16_t), (uint8_t*)_this->netBuf);
|
||||
}
|
||||
|
||||
static void clientHandler(net::Conn client, void* ctx) {
|
||||
NetworkSink* _this = (NetworkSink*)ctx;
|
||||
|
||||
{
|
||||
std::lock_guard lck(_this->connMtx);
|
||||
_this->conn = std::move(client);
|
||||
}
|
||||
|
||||
if (_this->conn) {
|
||||
_this->conn->waitForEnd();
|
||||
_this->conn->close();
|
||||
}
|
||||
else {
|
||||
|
||||
}
|
||||
|
||||
_this->listener->acceptAsync(clientHandler, _this);
|
||||
}
|
||||
|
||||
SinkManager::Stream* _stream;
|
||||
dsp::Packer<dsp::stereo_t> packer;
|
||||
dsp::StereoToMono s2m;
|
||||
dsp::HandlerSink<float> monoSink;
|
||||
dsp::HandlerSink<dsp::stereo_t> stereoSink;
|
||||
|
||||
std::string _streamName;
|
||||
|
||||
int srId = 0;
|
||||
bool running = false;
|
||||
|
||||
char hostname[1024];
|
||||
int port = 4242;
|
||||
|
||||
int modeId = 1;
|
||||
|
||||
std::vector<unsigned int> sampleRates;
|
||||
std::string sampleRatesTxt;
|
||||
unsigned int sampleRate = 48000;
|
||||
bool stereo = false;
|
||||
|
||||
int16_t* netBuf;
|
||||
|
||||
net::Listener listener;
|
||||
net::Conn conn;
|
||||
std::mutex connMtx;
|
||||
|
||||
};
|
||||
|
||||
class NetworkSinkModule : public ModuleManager::Instance {
|
||||
public:
|
||||
NetworkSinkModule(std::string name) {
|
||||
this->name = name;
|
||||
provider.create = create_sink;
|
||||
provider.ctx = this;
|
||||
|
||||
sigpath::sinkManager.registerSinkProvider("Network", provider);
|
||||
}
|
||||
|
||||
~NetworkSinkModule() {
|
||||
// Unregister sink, this will automatically stop and delete all instances of the audio sink
|
||||
sigpath::sinkManager.unregisterSinkProvider("Network");
|
||||
}
|
||||
|
||||
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 NetworkSink(stream, streamName));
|
||||
}
|
||||
|
||||
std::string name;
|
||||
bool enabled = true;
|
||||
SinkManager::SinkProvider provider;
|
||||
|
||||
};
|
||||
|
||||
MOD_EXPORT void _INIT_() {
|
||||
json def = json({});
|
||||
config.setPath(options::opts.root + "/network_sink_config.json");
|
||||
config.load(def);
|
||||
config.enableAutoSave();
|
||||
}
|
||||
|
||||
MOD_EXPORT void* _CREATE_INSTANCE_(std::string name) {
|
||||
NetworkSinkModule* instance = new NetworkSinkModule(name);
|
||||
return instance;
|
||||
}
|
||||
|
||||
MOD_EXPORT void _DELETE_INSTANCE_(void* instance) {
|
||||
delete (NetworkSinkModule*)instance;
|
||||
}
|
||||
|
||||
MOD_EXPORT void _END_() {
|
||||
config.disableAutoSave();
|
||||
config.save();
|
||||
}
|
37
sink_modules/new_portaudio_sink/CMakeLists.txt
Normal file
37
sink_modules/new_portaudio_sink/CMakeLists.txt
Normal file
@ -0,0 +1,37 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(new_portaudio_sink)
|
||||
|
||||
if (MSVC)
|
||||
add_compile_options(/O2 /Ob2 /std:c++17 /EHsc)
|
||||
elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
add_compile_options(-O3 -std=c++17 -Wno-unused-command-line-argument -undefined dynamic_lookup)
|
||||
else ()
|
||||
add_compile_options(-O3 -std=c++17 -fpermissive)
|
||||
endif (MSVC)
|
||||
|
||||
file(GLOB SRC "src/*.cpp")
|
||||
|
||||
include_directories("src/")
|
||||
|
||||
add_library(new_portaudio_sink SHARED ${SRC})
|
||||
target_link_libraries(new_portaudio_sink PRIVATE sdrpp_core)
|
||||
set_target_properties(new_portaudio_sink PROPERTIES PREFIX "")
|
||||
|
||||
if (MSVC)
|
||||
find_package(portaudio CONFIG REQUIRED)
|
||||
target_link_libraries(new_portaudio_sink PUBLIC portaudio)
|
||||
else (MSVC)
|
||||
find_package(PkgConfig)
|
||||
|
||||
pkg_check_modules(PORTAUDIO REQUIRED portaudio-2.0)
|
||||
|
||||
target_include_directories(new_portaudio_sink PUBLIC ${PORTAUDIO_INCLUDE_DIRS})
|
||||
|
||||
target_link_directories(new_portaudio_sink PUBLIC ${PORTAUDIO_LIBRARY_DIRS})
|
||||
|
||||
target_link_libraries(new_portaudio_sink PUBLIC ${PORTAUDIO_LIBRARIES})
|
||||
|
||||
endif (MSVC)
|
||||
|
||||
# Install directives
|
||||
install(TARGETS new_portaudio_sink DESTINATION lib/sdrpp/plugins)
|
445
sink_modules/new_portaudio_sink/src/main.cpp
Normal file
445
sink_modules/new_portaudio_sink/src/main.cpp
Normal file
@ -0,0 +1,445 @@
|
||||
#include <imgui.h>
|
||||
#include <module.h>
|
||||
#include <gui/gui.h>
|
||||
#include <signal_path/signal_path.h>
|
||||
#include <signal_path/sink.h>
|
||||
#include <portaudio.h>
|
||||
#include <dsp/audio.h>
|
||||
#include <dsp/processing.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <config.h>
|
||||
#include <algorithm>
|
||||
#include <options.h>
|
||||
|
||||
#define CONCAT(a, b) ((std::string(a) + b).c_str())
|
||||
|
||||
#define BLOCK_SIZE_DIVIDER 60
|
||||
#define AUDIO_LATENCY 1.0 / 60.0
|
||||
|
||||
SDRPP_MOD_INFO {
|
||||
/* Name: */ "new_portaudio_sink",
|
||||
/* Description: */ "Audio sink module for SDR++",
|
||||
/* Author: */ "Ryzerth;Maxime Biette",
|
||||
/* Version: */ 0, 1, 0,
|
||||
/* Max instances */ 1
|
||||
};
|
||||
|
||||
ConfigManager config;
|
||||
|
||||
class AudioSink : SinkManager::Sink {
|
||||
public:
|
||||
struct AudioDevice_t {
|
||||
const PaDeviceInfo* deviceInfo;
|
||||
const PaHostApiInfo* hostApiInfo;
|
||||
PaDeviceIndex id;
|
||||
int defaultSrId;
|
||||
PaStreamParameters outputParams;
|
||||
std::vector<double> sampleRates;
|
||||
std::string sampleRatesTxt;
|
||||
};
|
||||
|
||||
AudioSink(SinkManager::Stream* stream, std::string streamName) {
|
||||
_stream = stream;
|
||||
_streamName = streamName;
|
||||
|
||||
// Create config if it doesn't exist
|
||||
config.acquire();
|
||||
if (!config.conf.contains(_streamName)) {
|
||||
config.conf[_streamName]["device"] = "";
|
||||
config.conf[_streamName]["devices"] = json::object();
|
||||
}
|
||||
std::string selected = config.conf[_streamName]["device"];
|
||||
config.release(true);
|
||||
|
||||
// Register the play state handler
|
||||
playStateHandler.handler = playStateChangeHandler;
|
||||
playStateHandler.ctx = this;
|
||||
gui::mainWindow.onPlayStateChange.bindHandler(&playStateHandler);
|
||||
|
||||
// Initialize DSP blocks
|
||||
packer.init(_stream->sinkOut, 1024);
|
||||
s2m.init(&packer.out);
|
||||
|
||||
// Refresh devices and select the one from the config
|
||||
refreshDevices();
|
||||
selectDevByName(selected);
|
||||
}
|
||||
|
||||
~AudioSink() {
|
||||
stop();
|
||||
gui::mainWindow.onPlayStateChange.unbindHandler(&playStateHandler);
|
||||
}
|
||||
|
||||
void start() {
|
||||
if (running || selectedDevName.empty()) { return; }
|
||||
|
||||
// Get device and samplerate
|
||||
AudioDevice_t& dev = devices[deviceNames[devId]];
|
||||
double sampleRate = dev.sampleRates[srId];
|
||||
int blockSize = sampleRate / BLOCK_SIZE_DIVIDER;
|
||||
|
||||
// Set the SDR++ stream sample rate
|
||||
_stream->setSampleRate(sampleRate);
|
||||
|
||||
// Update the block size on the packer
|
||||
packer.setSampleCount(blockSize);
|
||||
|
||||
// Clear read stop signals
|
||||
packer.out.clearReadStop();
|
||||
s2m.out.clearReadStop();
|
||||
|
||||
// Open the stream
|
||||
PaError err;
|
||||
if (dev.deviceInfo->maxOutputChannels == 1) {
|
||||
packer.start();
|
||||
s2m.start();
|
||||
stereo = false;
|
||||
err = Pa_OpenStream(&devStream, NULL, &dev.outputParams, sampleRate, blockSize, paNoFlag, _mono_cb, this);
|
||||
}
|
||||
else {
|
||||
packer.start();
|
||||
stereo = true;
|
||||
err = Pa_OpenStream(&devStream, NULL, &dev.outputParams, sampleRate, blockSize, paNoFlag, _stereo_cb, this);
|
||||
}
|
||||
|
||||
// In case of error, abort
|
||||
if (err) {
|
||||
spdlog::error("PortAudio error {0}: {1}", err, Pa_GetErrorText(err));
|
||||
return;
|
||||
}
|
||||
|
||||
spdlog::info("Starting PortAudio stream at {0} S/s", sampleRate);
|
||||
|
||||
// Start stream
|
||||
Pa_StartStream(devStream);
|
||||
|
||||
running = true;
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (!running || selectedDevName.empty()) { return; }
|
||||
|
||||
// Send stop signal to the streams
|
||||
packer.out.stopReader();
|
||||
s2m.out.stopReader();
|
||||
|
||||
// Stop DSP
|
||||
packer.stop();
|
||||
s2m.stop();
|
||||
|
||||
// Stop stream
|
||||
Pa_AbortStream(devStream);
|
||||
|
||||
// Close the stream
|
||||
Pa_CloseStream(devStream);
|
||||
|
||||
running = false;
|
||||
}
|
||||
|
||||
void menuHandler() {
|
||||
float menuWidth = ImGui::GetContentRegionAvailWidth();
|
||||
|
||||
// Select device
|
||||
ImGui::SetNextItemWidth(menuWidth);
|
||||
if (ImGui::Combo("##audio_sink_dev_sel", &devId, deviceNamesTxt.c_str())) {
|
||||
selectDevByName(deviceNames[devId]);
|
||||
stop();
|
||||
start();
|
||||
if (selectedDevName != "") {
|
||||
config.acquire();
|
||||
config.conf[_streamName]["device"] = selectedDevName;
|
||||
config.release(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Select sample rate
|
||||
ImGui::SetNextItemWidth(menuWidth);
|
||||
if (ImGui::Combo("##audio_sink_sr_sel", &srId, selectedDev.sampleRatesTxt.c_str())) {
|
||||
stop();
|
||||
start();
|
||||
if (selectedDevName != "") {
|
||||
config.acquire();
|
||||
config.conf[_streamName]["devices"][selectedDevName] = selectedDev.sampleRates[srId];
|
||||
config.release(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int devId = 0;
|
||||
int srId = 0;
|
||||
bool stereo = false;
|
||||
|
||||
private:
|
||||
static void playStateChangeHandler(bool newState, void* ctx) {
|
||||
AudioSink* _this = (AudioSink*)ctx;
|
||||
|
||||
// Wake up reader to send nulls instead of data in preparation for shutoff
|
||||
if (newState) {
|
||||
if (_this->stereo) {
|
||||
_this->packer.out.stopReader();
|
||||
}
|
||||
else {
|
||||
_this->s2m.out.stopReader();
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (_this->stereo) {
|
||||
_this->packer.out.clearReadStop();
|
||||
}
|
||||
else {
|
||||
_this->s2m.out.clearReadStop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void refreshDevices() {
|
||||
// Clear current list
|
||||
devices.clear();
|
||||
deviceNames.clear();
|
||||
deviceNamesTxt.clear();
|
||||
|
||||
// Get number of devices
|
||||
int devCount = Pa_GetDeviceCount();
|
||||
PaStreamParameters outputParams;
|
||||
char buffer[128];
|
||||
|
||||
for (int i = 0; i < devCount; i++) {
|
||||
AudioDevice_t dev;
|
||||
|
||||
// Get device info
|
||||
dev.deviceInfo = Pa_GetDeviceInfo(i);
|
||||
dev.hostApiInfo = Pa_GetHostApiInfo(dev.deviceInfo->hostApi);
|
||||
dev.id = i;
|
||||
|
||||
// Check if device is usable
|
||||
if (dev.deviceInfo->maxOutputChannels == 0) { continue; }
|
||||
#ifdef _WIN32
|
||||
// On Windows, use only WASAPI
|
||||
if (dev.hostApiInfo->type == paMME || dev.hostApiInfo->type == paWDMKS) { continue; }
|
||||
#endif
|
||||
// Zero out output params
|
||||
dev.outputParams.device = i;
|
||||
dev.outputParams.sampleFormat = paFloat32;
|
||||
dev.outputParams.suggestedLatency = std::min<PaTime>(AUDIO_LATENCY, dev.deviceInfo->defaultLowOutputLatency);
|
||||
dev.outputParams.channelCount = std::min<int>(dev.deviceInfo->maxOutputChannels, 2);
|
||||
dev.outputParams.hostApiSpecificStreamInfo = NULL;
|
||||
|
||||
// List available sample rates
|
||||
for (int sr = 12000; sr < 200000; sr += 12000) {
|
||||
if (Pa_IsFormatSupported(NULL, &dev.outputParams, sr) != paFormatIsSupported) { continue; }
|
||||
dev.sampleRates.push_back(sr);
|
||||
}
|
||||
for (int sr = 11025; sr < 192000; sr += 11025) {
|
||||
if (Pa_IsFormatSupported(NULL, &dev.outputParams, sr) != paFormatIsSupported) { continue; }
|
||||
dev.sampleRates.push_back(sr);
|
||||
}
|
||||
|
||||
// If no sample rates are supported, cancel adding device
|
||||
if (dev.sampleRates.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sort sample rate list
|
||||
std::sort(dev.sampleRates.begin(), dev.sampleRates.end(), [](double a, double b) { return (a < b); });
|
||||
|
||||
// Generate text list for UI
|
||||
int srId = 0;
|
||||
int _48kId = -1;
|
||||
for (auto sr : dev.sampleRates) {
|
||||
sprintf(buffer, "%d", (int)sr);
|
||||
dev.sampleRatesTxt += buffer;
|
||||
dev.sampleRatesTxt += '\0';
|
||||
|
||||
// Save ID of the default sample rate and 48KHz
|
||||
if (sr == dev.deviceInfo->defaultSampleRate) { dev.defaultSrId = srId; }
|
||||
if (sr == 48000.0) { _48kId = srId; }
|
||||
srId++;
|
||||
}
|
||||
|
||||
// If a 48KHz option was found, use it instead of the default
|
||||
if (_48kId >= 0) { dev.defaultSrId = _48kId; }
|
||||
|
||||
std::string apiName = dev.hostApiInfo->name;
|
||||
|
||||
#ifdef _WIN32
|
||||
// Shorten the names on windows
|
||||
if (apiName.rfind("Windows ", 0) == 0) {
|
||||
apiName = apiName.substr(8);
|
||||
}
|
||||
#endif
|
||||
// Create device name and save to list
|
||||
sprintf(buffer, "[%s] %s", apiName.c_str(), dev.deviceInfo->name);
|
||||
devices[buffer] = dev;
|
||||
deviceNames.push_back(buffer);
|
||||
deviceNamesTxt += buffer;
|
||||
deviceNamesTxt += '\0';
|
||||
}
|
||||
}
|
||||
|
||||
void selectDefault() {
|
||||
if (devices.empty()) {
|
||||
selectedDevName = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// Search for the default device
|
||||
PaDeviceIndex defId = Pa_GetDefaultOutputDevice();
|
||||
for (auto const& [name, dev] : devices) {
|
||||
if (dev.id != defId) { continue; }
|
||||
selectDevByName(name);
|
||||
return;
|
||||
}
|
||||
|
||||
// If default not found, select first
|
||||
selectDevByName(deviceNames[0]);
|
||||
}
|
||||
|
||||
void selectDevByName(std::string name) {
|
||||
auto devIt = std::find(deviceNames.begin(), deviceNames.end(), name);
|
||||
if (devIt == deviceNames.end()) {
|
||||
selectDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the device name, device descriptor and device ID
|
||||
selectedDevName = name;
|
||||
selectedDev = devices[name];
|
||||
devId = std::distance(deviceNames.begin(), devIt);
|
||||
|
||||
// Load config
|
||||
config.acquire();
|
||||
if (!config.conf[_streamName]["devices"].contains(name)) {
|
||||
config.conf[_streamName]["devices"][name] = selectedDev.sampleRates[selectedDev.defaultSrId];
|
||||
}
|
||||
config.release(true);
|
||||
|
||||
// Find the sample rate ID, if not use default
|
||||
bool found = false;
|
||||
double selectedSr = config.conf[_streamName]["devices"][name];
|
||||
for (int i = 0; i < selectedDev.sampleRates.size(); i++) {
|
||||
if (selectedDev.sampleRates[i] != selectedSr) { continue; }
|
||||
srId = i;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
if (!found) {
|
||||
srId = selectedDev.defaultSrId;
|
||||
}
|
||||
}
|
||||
|
||||
static int _mono_cb(const void *input, void *output, unsigned long frameCount,
|
||||
const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void *userData) {
|
||||
AudioSink* _this = (AudioSink*)userData;
|
||||
|
||||
// For OSX, mute audio when not playing
|
||||
if (!gui::mainWindow.isPlaying()) {
|
||||
memset(output, 0, frameCount*sizeof(float));
|
||||
_this->s2m.out.flush();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Write to buffer
|
||||
_this->s2m.out.read();
|
||||
memcpy(output, _this->s2m.out.readBuf, frameCount * sizeof(float));
|
||||
_this->s2m.out.flush();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int _stereo_cb(const void *input, void *output, unsigned long frameCount,
|
||||
const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void *userData) {
|
||||
AudioSink* _this = (AudioSink*)userData;
|
||||
|
||||
// For OSX, mute audio when not playing
|
||||
if (!gui::mainWindow.isPlaying()) {
|
||||
memset(output, 0, frameCount*sizeof(dsp::stereo_t));
|
||||
_this->packer.out.flush();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Write to buffer
|
||||
_this->packer.out.read();
|
||||
memcpy(output, _this->packer.out.readBuf, frameCount * sizeof(dsp::stereo_t));
|
||||
_this->packer.out.flush();
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string _streamName;
|
||||
|
||||
bool running = false;
|
||||
std::map<std::string, AudioDevice_t> devices;
|
||||
std::vector<std::string> deviceNames;
|
||||
std::string deviceNamesTxt;
|
||||
|
||||
AudioDevice_t selectedDev;
|
||||
std::string selectedDevName;
|
||||
|
||||
SinkManager::Stream* _stream;
|
||||
dsp::Packer<dsp::stereo_t> packer;
|
||||
dsp::StereoToMono s2m;
|
||||
|
||||
PaStream *devStream;
|
||||
|
||||
EventHandler<bool> playStateHandler;
|
||||
};
|
||||
|
||||
class AudioSinkModule : public ModuleManager::Instance {
|
||||
public:
|
||||
AudioSinkModule(std::string name) {
|
||||
this->name = name;
|
||||
provider.create = create_sink;
|
||||
provider.ctx = this;
|
||||
|
||||
Pa_Initialize();
|
||||
|
||||
sigpath::sinkManager.registerSinkProvider("New Audio", provider);
|
||||
}
|
||||
|
||||
~AudioSinkModule() {
|
||||
sigpath::sinkManager.unregisterSinkProvider("New Audio");
|
||||
Pa_Terminate();
|
||||
}
|
||||
|
||||
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_() {
|
||||
config.setPath(options::opts.root + "/new_audio_sink_config.json");
|
||||
config.load(json::object());
|
||||
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();
|
||||
}
|
38
sink_modules/portaudio_sink/CMakeLists.txt
Normal file
38
sink_modules/portaudio_sink/CMakeLists.txt
Normal file
@ -0,0 +1,38 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(audio_sink)
|
||||
|
||||
if (MSVC)
|
||||
add_compile_options(/O2 /Ob2 /std:c++17 /EHsc)
|
||||
elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
add_compile_options(-O3 -std=c++17 -Wno-unused-command-line-argument -undefined dynamic_lookup)
|
||||
else ()
|
||||
add_compile_options(-O3 -std=c++17 -fpermissive)
|
||||
endif (MSVC)
|
||||
|
||||
|
||||
file(GLOB SRC "src/*.cpp")
|
||||
|
||||
include_directories("src/")
|
||||
|
||||
add_library(audio_sink SHARED ${SRC})
|
||||
target_link_libraries(audio_sink PRIVATE sdrpp_core)
|
||||
set_target_properties(audio_sink PROPERTIES PREFIX "")
|
||||
|
||||
if (MSVC)
|
||||
find_package(portaudio CONFIG REQUIRED)
|
||||
target_link_libraries(sdrpp_core PUBLIC portaudio)
|
||||
else (MSVC)
|
||||
find_package(PkgConfig)
|
||||
|
||||
pkg_check_modules(PORTAUDIO REQUIRED portaudio-2.0)
|
||||
|
||||
target_include_directories(sdrpp_core PUBLIC ${PORTAUDIO_INCLUDE_DIRS})
|
||||
|
||||
target_link_directories(sdrpp_core PUBLIC ${PORTAUDIO_LIBRARY_DIRS})
|
||||
|
||||
target_link_libraries(sdrpp_core PUBLIC ${PORTAUDIO_LIBRARIES})
|
||||
|
||||
endif (MSVC)
|
||||
|
||||
# Install directives
|
||||
install(TARGETS audio_sink DESTINATION lib/sdrpp/plugins)
|
341
sink_modules/portaudio_sink/src/main.cpp
Normal file
341
sink_modules/portaudio_sink/src/main.cpp
Normal file
@ -0,0 +1,341 @@
|
||||
#include <imgui.h>
|
||||
#include <module.h>
|
||||
#include <gui/gui.h>
|
||||
#include <signal_path/signal_path.h>
|
||||
#include <signal_path/sink.h>
|
||||
#include <portaudio.h>
|
||||
#include <dsp/audio.h>
|
||||
#include <dsp/processing.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#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
|
||||
};
|
||||
|
||||
class AudioSink : SinkManager::Sink {
|
||||
public:
|
||||
struct AudioDevice_t {
|
||||
std::string name;
|
||||
int index;
|
||||
int channels;
|
||||
int srId;
|
||||
std::vector<double> sampleRates;
|
||||
std::string txtSampleRates;
|
||||
};
|
||||
|
||||
AudioSink(SinkManager::Stream* stream, std::string streamName) {
|
||||
_stream = stream;
|
||||
_streamName = streamName;
|
||||
s2m.init(_stream->sinkOut);
|
||||
monoRB.init(&s2m.out);
|
||||
stereoRB.init(_stream->sinkOut);
|
||||
|
||||
// monoPacker.init(&s2m.out, 240);
|
||||
// stereoPacker.init(_stream->sinkOut, 240);
|
||||
|
||||
// Initialize PortAudio
|
||||
devCount = Pa_GetDeviceCount();
|
||||
devId = Pa_GetDefaultOutputDevice();
|
||||
const PaDeviceInfo *deviceInfo;
|
||||
PaStreamParameters outputParams;
|
||||
outputParams.sampleFormat = paFloat32;
|
||||
outputParams.hostApiSpecificStreamInfo = NULL;
|
||||
|
||||
// Gather hardware info
|
||||
for(int i = 0; i < devCount; i++) {
|
||||
deviceInfo = Pa_GetDeviceInfo(i);
|
||||
if (deviceInfo->maxOutputChannels < 1) {
|
||||
continue;
|
||||
}
|
||||
AudioDevice_t dev;
|
||||
dev.name = deviceInfo->name;
|
||||
dev.index = i;
|
||||
dev.channels = std::min<int>(deviceInfo->maxOutputChannels, 2);
|
||||
dev.sampleRates.clear();
|
||||
dev.txtSampleRates = "";
|
||||
for (int j = 0; j < 6; j++) {
|
||||
outputParams.channelCount = dev.channels;
|
||||
outputParams.device = dev.index;
|
||||
outputParams.suggestedLatency = deviceInfo->defaultLowOutputLatency;
|
||||
PaError err = Pa_IsFormatSupported(NULL, &outputParams, POSSIBLE_SAMP_RATE[j]);
|
||||
if (err != paFormatIsSupported) {
|
||||
continue;
|
||||
}
|
||||
dev.sampleRates.push_back(POSSIBLE_SAMP_RATE[j]);
|
||||
dev.txtSampleRates += std::to_string((int)POSSIBLE_SAMP_RATE[j]);
|
||||
dev.txtSampleRates += '\0';
|
||||
}
|
||||
if (dev.sampleRates.size() == 0) {
|
||||
continue;
|
||||
}
|
||||
if (i == devId) {
|
||||
devListId = devices.size();
|
||||
defaultDev = devListId;
|
||||
_stream->setSampleRate(dev.sampleRates[0]);
|
||||
}
|
||||
dev.srId = 0;
|
||||
|
||||
AudioDevice_t* _dev = new AudioDevice_t;
|
||||
*_dev = dev;
|
||||
devices.push_back(_dev);
|
||||
|
||||
deviceNames.push_back(deviceInfo->name);
|
||||
txtDevList += deviceInfo->name;
|
||||
txtDevList += '\0';
|
||||
}
|
||||
|
||||
// Load config from file
|
||||
}
|
||||
|
||||
~AudioSink() {
|
||||
for (auto const& dev : devices) {
|
||||
delete dev;
|
||||
}
|
||||
}
|
||||
|
||||
void start() {
|
||||
if (running) {
|
||||
return;
|
||||
}
|
||||
doStart();
|
||||
running = true;
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
doStop();
|
||||
running = false;
|
||||
}
|
||||
|
||||
void menuHandler() {
|
||||
float menuWidth = ImGui::GetContentRegionAvailWidth();
|
||||
|
||||
ImGui::SetNextItemWidth(menuWidth);
|
||||
if (ImGui::Combo(("##_audio_sink_dev_"+_streamName).c_str(), &devListId, txtDevList.c_str())) {
|
||||
// TODO: Load SR from config
|
||||
if (running) {
|
||||
doStop();
|
||||
doStart();
|
||||
}
|
||||
// TODO: Save to config
|
||||
}
|
||||
|
||||
AudioDevice_t* dev = devices[devListId];
|
||||
|
||||
ImGui::SetNextItemWidth(menuWidth);
|
||||
if (ImGui::Combo(("##_audio_sink_sr_"+_streamName).c_str(), &dev->srId, dev->txtSampleRates.c_str())) {
|
||||
_stream->setSampleRate(dev->sampleRates[dev->srId]);
|
||||
if (running) {
|
||||
doStop();
|
||||
doStart();
|
||||
}
|
||||
// TODO: Save to config
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void doStart() {
|
||||
const PaDeviceInfo *deviceInfo;
|
||||
AudioDevice_t* dev = devices[devListId];
|
||||
PaStreamParameters outputParams;
|
||||
deviceInfo = Pa_GetDeviceInfo(dev->index);
|
||||
outputParams.channelCount = 2;
|
||||
outputParams.sampleFormat = paFloat32;
|
||||
outputParams.hostApiSpecificStreamInfo = NULL;
|
||||
outputParams.device = dev->index;
|
||||
outputParams.suggestedLatency = Pa_GetDeviceInfo(outputParams.device)->defaultLowOutputLatency;
|
||||
PaError err;
|
||||
|
||||
float sampleRate = dev->sampleRates[dev->srId];
|
||||
int bufferSize = sampleRate / 60.0f;
|
||||
|
||||
if (dev->channels == 2) {
|
||||
stereoRB.data.setMaxLatency(bufferSize * 2);
|
||||
stereoRB.start();
|
||||
// stereoPacker.setSampleCount(bufferSize);
|
||||
// stereoPacker.start();
|
||||
err = Pa_OpenStream(&stream, NULL, &outputParams, sampleRate, paFramesPerBufferUnspecified, 0, _stereo_cb, this);
|
||||
//err = Pa_OpenStream(&stream, NULL, &outputParams, sampleRate, bufferSize, 0, _stereo_cb, this);
|
||||
}
|
||||
else {
|
||||
monoRB.data.setMaxLatency(bufferSize * 2);
|
||||
monoRB.start();
|
||||
// stereoPacker.setSampleCount(bufferSize);
|
||||
// monoPacker.start();
|
||||
err = Pa_OpenStream(&stream, NULL, &outputParams, sampleRate, paFramesPerBufferUnspecified, 0, _mono_cb, this);
|
||||
//err = Pa_OpenStream(&stream, NULL, &outputParams, sampleRate, bufferSize, 0, _mono_cb, this);
|
||||
}
|
||||
|
||||
if (err != 0) {
|
||||
spdlog::error("Error while opening audio stream: ({0}) => {1}", err, Pa_GetErrorText(err));
|
||||
return;
|
||||
}
|
||||
|
||||
err = Pa_StartStream(stream);
|
||||
if (err != 0) {
|
||||
spdlog::error("Error while starting audio stream: ({0}) => {1}", err, Pa_GetErrorText(err));
|
||||
return;
|
||||
}
|
||||
spdlog::info("Audio device open.");
|
||||
running = true;
|
||||
}
|
||||
|
||||
void doStop() {
|
||||
s2m.stop();
|
||||
monoRB.stop();
|
||||
stereoRB.stop();
|
||||
// monoPacker.stop();
|
||||
// stereoPacker.stop();
|
||||
monoRB.data.stopReader();
|
||||
stereoRB.data.stopReader();
|
||||
// monoPacker.out.stopReader();
|
||||
// stereoPacker.out.stopReader();
|
||||
Pa_StopStream(stream);
|
||||
Pa_CloseStream(stream);
|
||||
monoRB.data.clearReadStop();
|
||||
stereoRB.data.clearReadStop();
|
||||
// monoPacker.out.clearReadStop();
|
||||
// stereoPacker.out.clearWriteStop();
|
||||
}
|
||||
|
||||
static int _mono_cb(const void *input, void *output, unsigned long frameCount,
|
||||
const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void *userData) {
|
||||
AudioSink* _this = (AudioSink*)userData;
|
||||
if (!gui::mainWindow.isPlaying()) {
|
||||
memset(output, 0, frameCount*sizeof(float));
|
||||
return 0;
|
||||
}
|
||||
_this->monoRB.data.read((float*)output, frameCount);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int _stereo_cb(const void *input, void *output, unsigned long frameCount,
|
||||
const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void *userData) {
|
||||
AudioSink* _this = (AudioSink*)userData;
|
||||
if (!gui::mainWindow.isPlaying()) {
|
||||
memset(output, 0, frameCount*sizeof(dsp::stereo_t));
|
||||
return 0;
|
||||
}
|
||||
_this->stereoRB.data.read((dsp::stereo_t*)output, frameCount);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// static int _mono_cb(const void *input, void *output, unsigned long frameCount,
|
||||
// const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void *userData) {
|
||||
// AudioSink* _this = (AudioSink*)userData;
|
||||
// if (_this->monoPacker.out.read() < 0) { return 0; }
|
||||
// memcpy((float*)output, _this->monoPacker.out.readBuf, frameCount * sizeof(float));
|
||||
// _this->monoPacker.out.flush();
|
||||
// return 0;
|
||||
// }
|
||||
|
||||
// static int _stereo_cb(const void *input, void *output, unsigned long frameCount,
|
||||
// const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void *userData) {
|
||||
// AudioSink* _this = (AudioSink*)userData;
|
||||
// if (_this->stereoPacker.out.read() < 0) { spdlog::warn("CB killed"); return 0; }
|
||||
// memcpy((dsp::stereo_t*)output, _this->stereoPacker.out.readBuf, frameCount * sizeof(dsp::stereo_t));
|
||||
// _this->stereoPacker.out.flush();
|
||||
// return 0;
|
||||
// }
|
||||
|
||||
|
||||
SinkManager::Stream* _stream;
|
||||
dsp::StereoToMono s2m;
|
||||
dsp::RingBufferSink<float> monoRB;
|
||||
dsp::RingBufferSink<dsp::stereo_t> stereoRB;
|
||||
|
||||
// dsp::Packer<float> monoPacker;
|
||||
// dsp::Packer<dsp::stereo_t> stereoPacker;
|
||||
|
||||
std::string _streamName;
|
||||
PaStream *stream;
|
||||
|
||||
int srId = 0;
|
||||
int devCount;
|
||||
int devId = 0;
|
||||
int devListId = 0;
|
||||
int defaultDev = 0;
|
||||
bool running = false;
|
||||
|
||||
const double POSSIBLE_SAMP_RATE[6] = {
|
||||
48000.0f,
|
||||
44100.0f,
|
||||
24000.0f,
|
||||
22050.0f,
|
||||
12000.0f,
|
||||
11025.0f
|
||||
};
|
||||
|
||||
std::vector<AudioDevice_t*> devices;
|
||||
std::vector<std::string> deviceNames;
|
||||
std::string txtDevList;
|
||||
|
||||
};
|
||||
|
||||
class AudioSinkModule : public ModuleManager::Instance {
|
||||
public:
|
||||
AudioSinkModule(std::string name) {
|
||||
this->name = name;
|
||||
provider.create = create_sink;
|
||||
provider.ctx = this;
|
||||
|
||||
Pa_Initialize();
|
||||
|
||||
sigpath::sinkManager.registerSinkProvider("Audio", provider);
|
||||
}
|
||||
|
||||
~AudioSinkModule() {
|
||||
sigpath::sinkManager.unregisterSinkProvider("Audio");
|
||||
Pa_Terminate();
|
||||
}
|
||||
|
||||
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_() {
|
||||
// Nothing here
|
||||
// TODO: Do instancing here (in source modules as well) to prevent multiple loads
|
||||
}
|
||||
|
||||
MOD_EXPORT void* _CREATE_INSTANCE_(std::string name) {
|
||||
AudioSinkModule* instance = new AudioSinkModule(name);
|
||||
return instance;
|
||||
}
|
||||
|
||||
MOD_EXPORT void _DELETE_INSTANCE_() {
|
||||
|
||||
}
|
||||
|
||||
MOD_EXPORT void _END_() {
|
||||
|
||||
}
|
Reference in New Issue
Block a user