From 5f84ecc4de00eca6db2d5ccda4de8c93c51c7787 Mon Sep 17 00:00:00 2001 From: Ryzerth Date: Tue, 29 Jun 2021 18:14:26 +0200 Subject: [PATCH] Added audio fix for MacOS --- CMakeLists.txt | 5 + core/src/gui/main_window.cpp | 4 + core/src/gui/main_window.h | 2 + portaudio_sink/CMakeLists.txt | 35 ++++ portaudio_sink/src/main.cpp | 338 ++++++++++++++++++++++++++++++++++ 5 files changed, 384 insertions(+) create mode 100644 portaudio_sink/CMakeLists.txt create mode 100644 portaudio_sink/src/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2583478..51eaf88 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,7 @@ option(OPT_BUILD_PLUTOSDR_SOURCE "Build PlutoSDR Source Module (Depedencies: lib # Sinks option(OPT_BUILD_AUDIO_SINK "Build Audio Sink Module (Depedencies: rtaudio)" ON) +option(OPT_BUILD_PORTAUDIO_SINK "Build PortAudio Sink Module (Depedencies: portaudio)" OFF) # Decoders option(OPT_BUILD_FALCON9_DECODER "Build the falcon9 live decoder (Dependencies: ffplay)" OFF) @@ -100,6 +101,10 @@ if (OPT_BUILD_AUDIO_SINK) add_subdirectory("audio_sink") endif (OPT_BUILD_AUDIO_SINK) +if (OPT_BUILD_PORTAUDIO_SINK) +add_subdirectory("portaudio_sink") +endif (OPT_BUILD_PORTAUDIO_SINK) + # Decoders if (OPT_BUILD_FALCON9_DECODER) diff --git a/core/src/gui/main_window.cpp b/core/src/gui/main_window.cpp index 5718c6f..4a872d4 100644 --- a/core/src/gui/main_window.cpp +++ b/core/src/gui/main_window.cpp @@ -616,4 +616,8 @@ void MainWindow::setFFTSize(int size) { fft_in = (fftwf_complex*)fftwf_malloc(sizeof(fftwf_complex) * fftSize); fft_out = (fftwf_complex*)fftwf_malloc(sizeof(fftwf_complex) * fftSize); fftwPlan = fftwf_plan_dft_1d(fftSize, fft_in, fft_out, FFTW_FORWARD, FFTW_ESTIMATE); +} + +bool MainWindow::isPlaying() { + return playing; } \ No newline at end of file diff --git a/core/src/gui/main_window.h b/core/src/gui/main_window.h index 33a3cff..ab2f494 100644 --- a/core/src/gui/main_window.h +++ b/core/src/gui/main_window.h @@ -22,6 +22,8 @@ public: // TODO: Replace with it's own class void setVFO(double freq); + bool isPlaying(); + bool lockWaterfallControls = false; private: diff --git a/portaudio_sink/CMakeLists.txt b/portaudio_sink/CMakeLists.txt new file mode 100644 index 0000000..2dc4967 --- /dev/null +++ b/portaudio_sink/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.13) +project(audio_sink) + +if (MSVC) + set(CMAKE_CXX_FLAGS "-O2 /std:c++17 /EHsc") +else() + set(CMAKE_CXX_FLAGS "-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) \ No newline at end of file diff --git a/portaudio_sink/src/main.cpp b/portaudio_sink/src/main.cpp new file mode 100644 index 0000000..4d9b155 --- /dev/null +++ b/portaudio_sink/src/main.cpp @@ -0,0 +1,338 @@ +#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 +}; + +class AudioSink : SinkManager::Sink { +public: + struct AudioDevice_t { + std::string name; + int index; + int channels; + int srId; + std::vector 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(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 monoRB; + dsp::RingBufferSink stereoRB; + + // dsp::Packer monoPacker; + // dsp::Packer 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 devices; + std::vector 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() { + Pa_Terminate(); + } + + 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_() { + +} \ No newline at end of file