2021-07-17 19:43:44 +02:00
|
|
|
#include <imgui.h>
|
|
|
|
#include <module.h>
|
|
|
|
#include <gui/gui.h>
|
|
|
|
#include <signal_path/signal_path.h>
|
|
|
|
#include <signal_path/sink.h>
|
|
|
|
#include <portaudio.h>
|
2022-06-17 17:34:23 +02:00
|
|
|
#include <dsp/buffer/packer.h>
|
|
|
|
#include <dsp/convert/stereo_to_mono.h>
|
2023-02-25 18:12:34 +01:00
|
|
|
#include <utils/flog.h>
|
2021-07-17 19:43:44 +02:00
|
|
|
#include <config.h>
|
|
|
|
#include <algorithm>
|
2022-02-24 21:24:46 +01:00
|
|
|
#include <core.h>
|
2021-07-17 19:43:44 +02:00
|
|
|
|
|
|
|
#define CONCAT(a, b) ((std::string(a) + b).c_str())
|
|
|
|
|
2021-12-19 22:11:44 +01:00
|
|
|
#define BLOCK_SIZE_DIVIDER 60
|
|
|
|
#define AUDIO_LATENCY 1.0 / 60.0
|
2021-07-17 19:43:44 +02:00
|
|
|
|
2021-12-19 22:11:44 +01:00
|
|
|
SDRPP_MOD_INFO{
|
2021-07-17 19:43:44 +02:00
|
|
|
/* 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;
|
|
|
|
};
|
2021-12-19 22:11:44 +01:00
|
|
|
|
2021-07-17 19:43:44 +02:00
|
|
|
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; }
|
2021-12-19 22:11:44 +01:00
|
|
|
|
2021-07-17 19:43:44 +02:00
|
|
|
// 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);
|
2021-12-19 22:11:44 +01:00
|
|
|
|
2021-07-17 19:43:44 +02:00
|
|
|
// 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) {
|
2023-02-25 18:12:34 +01:00
|
|
|
flog::error("PortAudio error {0}: {1}", err, Pa_GetErrorText(err));
|
2021-07-17 19:43:44 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-02-25 18:12:34 +01:00
|
|
|
flog::info("Starting PortAudio stream at {0} S/s", sampleRate);
|
2021-07-17 19:43:44 +02:00
|
|
|
|
|
|
|
// 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();
|
2021-12-19 22:11:44 +01:00
|
|
|
|
2021-07-17 19:43:44 +02:00
|
|
|
// Stop stream
|
|
|
|
Pa_AbortStream(devStream);
|
|
|
|
|
|
|
|
// Close the stream
|
|
|
|
Pa_CloseStream(devStream);
|
|
|
|
|
|
|
|
running = false;
|
|
|
|
}
|
2021-12-19 22:11:44 +01:00
|
|
|
|
2021-07-17 19:43:44 +02:00
|
|
|
void menuHandler() {
|
2022-02-14 23:33:52 +01:00
|
|
|
float menuWidth = ImGui::GetContentRegionAvail().x;
|
2021-07-17 19:43:44 +02:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-19 22:11:44 +01:00
|
|
|
static int _mono_cb(const void* input, void* output, unsigned long frameCount,
|
|
|
|
const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void* userData) {
|
2021-07-17 19:43:44 +02:00
|
|
|
AudioSink* _this = (AudioSink*)userData;
|
2021-12-19 22:11:44 +01:00
|
|
|
|
2021-07-17 19:43:44 +02:00
|
|
|
// For OSX, mute audio when not playing
|
2021-12-19 22:11:44 +01:00
|
|
|
if (!gui::mainWindow.isPlaying()) {
|
|
|
|
memset(output, 0, frameCount * sizeof(float));
|
2021-07-17 19:43:44 +02:00
|
|
|
_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;
|
|
|
|
}
|
|
|
|
|
2021-12-19 22:11:44 +01:00
|
|
|
static int _stereo_cb(const void* input, void* output, unsigned long frameCount,
|
|
|
|
const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void* userData) {
|
2021-07-17 19:43:44 +02:00
|
|
|
AudioSink* _this = (AudioSink*)userData;
|
2021-12-19 22:11:44 +01:00
|
|
|
|
2021-07-17 19:43:44 +02:00
|
|
|
// For OSX, mute audio when not playing
|
2021-12-19 22:11:44 +01:00
|
|
|
if (!gui::mainWindow.isPlaying()) {
|
|
|
|
memset(output, 0, frameCount * sizeof(dsp::stereo_t));
|
2021-07-17 19:43:44 +02:00
|
|
|
_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;
|
2022-06-17 17:34:23 +02:00
|
|
|
dsp::buffer::Packer<dsp::stereo_t> packer;
|
|
|
|
dsp::convert::StereoToMono s2m;
|
2021-07-17 19:43:44 +02:00
|
|
|
|
2021-12-19 22:11:44 +01:00
|
|
|
PaStream* devStream;
|
2021-07-17 19:43:44 +02:00
|
|
|
|
|
|
|
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() {
|
2021-07-26 15:54:33 +02:00
|
|
|
sigpath::sinkManager.unregisterSinkProvider("New Audio");
|
2021-07-17 19:43:44 +02:00
|
|
|
Pa_Terminate();
|
|
|
|
}
|
|
|
|
|
2021-07-26 03:11:51 +02:00
|
|
|
void postInit() {}
|
|
|
|
|
2021-07-17 19:43:44 +02:00
|
|
|
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_() {
|
2022-02-24 20:49:53 +01:00
|
|
|
config.setPath(core::args["root"].s() + "/new_audio_sink_config.json");
|
2021-07-17 19:43:44 +02:00
|
|
|
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();
|
|
|
|
}
|