diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml index cb79742b..ce255d00 100644 --- a/.github/workflows/build_all.yml +++ b/.github/workflows/build_all.yml @@ -116,7 +116,7 @@ jobs: - name: Prepare CMake working-directory: ${{runner.workspace}}/build - run: cmake $GITHUB_WORKSPACE -DOPT_BUILD_PLUTOSDR_SOURCE=ON -DOPT_BUILD_SOAPY_SOURCE=OFF -DOPT_BUILD_BLADERF_SOURCE=ON -DOPT_BUILD_SDRPLAY_SOURCE=ON -DOPT_BUILD_LIMESDR_SOURCE=ON -DOPT_BUILD_AUDIO_SINK=OFF -DOPT_BUILD_PORTAUDIO_SINK=ON -DOPT_BUILD_NEW_PORTAUDIO_SINK=ON -DOPT_BUILD_M17_DECODER=ON -DUSE_BUNDLE_DEFAULTS=ON -DCMAKE_BUILD_TYPE=Release + run: cmake $GITHUB_WORKSPACE -DOPT_BUILD_PLUTOSDR_SOURCE=ON -DOPT_BUILD_SOAPY_SOURCE=OFF -DOPT_BUILD_BLADERF_SOURCE=ON -DOPT_BUILD_SDRPLAY_SOURCE=ON -DOPT_BUILD_LIMESDR_SOURCE=ON -DOPT_BUILD_AUDIO_SINK=OFF -DOPT_BUILD_PORTAUDIO_SINK=ON -DOPT_BUILD_NEW_PORTAUDIO_SINK=ON -DOPT_BUILD_M17_DECODER=ON -DOPT_BUILD_AUDIO_SOURCE=OFF -DUSE_BUNDLE_DEFAULTS=ON -DCMAKE_BUILD_TYPE=Release - name: Build working-directory: ${{runner.workspace}}/build diff --git a/CMakeLists.txt b/CMakeLists.txt index e45ed5af..c9c3f816 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ option(OPT_OVERRIDE_STD_FILESYSTEM "Use a local version of std::filesystem on sy # Sources option(OPT_BUILD_AIRSPY_SOURCE "Build Airspy Source Module (Dependencies: libairspy)" ON) option(OPT_BUILD_AIRSPYHF_SOURCE "Build Airspy HF+ Source Module (Dependencies: libairspyhf)" ON) +option(OPT_BUILD_AUDIO_SOURCE "Build Audio Source Module (Dependencies: rtaudio)" ON) option(OPT_BUILD_BLADERF_SOURCE "Build BladeRF Source Module (Dependencies: libbladeRF)" OFF) option(OPT_BUILD_FILE_SOURCE "Wav file source" ON) option(OPT_BUILD_HACKRF_SOURCE "Build HackRF Source Module (Dependencies: libhackrf)" ON) @@ -116,6 +117,10 @@ if (OPT_BUILD_AIRSPYHF_SOURCE) add_subdirectory("source_modules/airspyhf_source") endif (OPT_BUILD_AIRSPYHF_SOURCE) +if (OPT_BUILD_AUDIO_SOURCE) +add_subdirectory("source_modules/audio_source") +endif (OPT_BUILD_AUDIO_SOURCE) + if (OPT_BUILD_BLADERF_SOURCE) add_subdirectory("source_modules/bladerf_source") endif (OPT_BUILD_BLADERF_SOURCE) diff --git a/android/app/build.gradle b/android/app/build.gradle index 19bc3771..f50f220d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -14,7 +14,7 @@ android { externalNativeBuild { cmake { - arguments "-DOPT_BACKEND_GLFW=OFF", "-DOPT_BACKEND_ANDROID=ON", "-DOPT_BUILD_SOAPY_SOURCE=OFF", "-DOPT_BUILD_ANDROID_AUDIO_SINK=ON", "-DOPT_BUILD_AUDIO_SINK=OFF", "-DOPT_BUILD_DISCORD_PRESENCE=OFF", "-DOPT_BUILD_M17_DECODER=ON", "-DOPT_BUILD_PLUTOSDR_SOURCE=ON" + arguments "-DOPT_BACKEND_GLFW=OFF", "-DOPT_BACKEND_ANDROID=ON", "-DOPT_BUILD_SOAPY_SOURCE=OFF", "-DOPT_BUILD_ANDROID_AUDIO_SINK=ON", "-DOPT_BUILD_AUDIO_SINK=OFF", "-DOPT_BUILD_DISCORD_PRESENCE=OFF", "-DOPT_BUILD_M17_DECODER=ON", "-DOPT_BUILD_PLUTOSDR_SOURCE=ON", "-DOPT_BUILD_AUDIO_SOURCE=OFF" } } } diff --git a/core/src/core.cpp b/core/src/core.cpp index b54cf31a..23d5a2ab 100644 --- a/core/src/core.cpp +++ b/core/src/core.cpp @@ -163,6 +163,8 @@ int sdrpp_main(int argc, char* argv[]) { defConfig["moduleInstances"]["Airspy Source"]["enabled"] = true; defConfig["moduleInstances"]["AirspyHF+ Source"]["module"] = "airspyhf_source"; defConfig["moduleInstances"]["AirspyHF+ Source"]["enabled"] = true; + defConfig["moduleInstances"]["Audio Source"]["module"] = "audio_source"; + defConfig["moduleInstances"]["Audio Source"]["enabled"] = true; defConfig["moduleInstances"]["BladeRF Source"]["module"] = "bladerf_source"; defConfig["moduleInstances"]["BladeRF Source"]["enabled"] = true; defConfig["moduleInstances"]["File Source"]["module"] = "file_source"; diff --git a/source_modules/audio_source/CMakeLists.txt b/source_modules/audio_source/CMakeLists.txt new file mode 100644 index 00000000..afaf0929 --- /dev/null +++ b/source_modules/audio_source/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.13) +project(audio_source) + +file(GLOB SRC "src/*.cpp") + +include(${SDRPP_MODULE_CMAKE}) + +if (MSVC) + # Lib path + target_link_directories(audio_source PRIVATE "C:/Program Files (x86)/RtAudio/lib") + + # Misc headers + target_include_directories(audio_source PRIVATE "C:/Program Files (x86)/RtAudio/include/rtaudio") + + target_link_libraries(audio_source PRIVATE rtaudio) +else (MSVC) + find_package(PkgConfig) + + pkg_check_modules(RTAUDIO REQUIRED rtaudio) + + target_include_directories(audio_source PRIVATE ${RTAUDIO_INCLUDE_DIRS}) + target_link_directories(audio_source PRIVATE ${RTAUDIO_LIBRARY_DIRS}) + target_link_libraries(audio_source PRIVATE ${RTAUDIO_LIBRARIES}) + +endif () \ No newline at end of file diff --git a/source_modules/audio_source/src/main.cpp b/source_modules/audio_source/src/main.cpp new file mode 100644 index 00000000..d332d193 --- /dev/null +++ b/source_modules/audio_source/src/main.cpp @@ -0,0 +1,293 @@ +#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_source", + /* Description: */ "Audio source module for SDR++", + /* Author: */ "Ryzerth", + /* Version: */ 0, 1, 0, + /* Max instances */ 1 +}; + +ConfigManager config; + +struct DeviceInfo { + RtAudio::DeviceInfo info; + int id; + bool operator==(const struct DeviceInfo& other) { + return other.id == id; + } +}; + +class AudioSourceModule : public ModuleManager::Instance { +public: + AudioSourceModule(std::string name) { + this->name = name; + + sampleRate = 48000.0; + + handler.ctx = this; + handler.selectHandler = menuSelected; + handler.deselectHandler = menuDeselected; + handler.menuHandler = menuHandler; + handler.startHandler = start; + handler.stopHandler = stop; + handler.tuneHandler = tune; + handler.stream = &stream; + + // Refresh devices + refresh(); + + // Select device + std::string device = ""; + config.acquire(); + if (config.conf.contains("device")) { + device = config.conf["device"]; + } + config.release(); + select(device); + + sigpath::sourceManager.registerSource("Audio", &handler); + } + + ~AudioSourceModule() { + stop(this); + sigpath::sourceManager.unregisterSource("Audio"); + } + + void postInit() {} + + void enable() { + enabled = true; + } + + void disable() { + enabled = false; + } + + bool isEnabled() { + return enabled; + } + + void refresh() { + devices.clear(); + + int count = audio.getDeviceCount(); + for (int i = 0; i < count; i++) { + try { + // Get info + auto info = audio.getDeviceInfo(i); + + // Check that it has a stereo input + if (info.probed && info.inputChannels < 2) { continue; } + + // Save info + DeviceInfo dinfo = { info, i }; + devices.define(info.name, info.name, dinfo); + } + catch (std::exception e) { + spdlog::error("Error getting audio device info: {0}", e.what()); + } + } + } + + void select(std::string name) { + if (devices.empty()) { + selectedDevice.clear(); + return; + } + + // Check that such a device exist. If not select first + if (!devices.keyExists(name)) { + select(devices.key(0)); + return; + } + + // Get device info + devId = devices.keyId(name); + auto info = devices.value(devId).info; + selectedDevice = name; + + // List samplerates and save ID of the preference one + sampleRates.clear(); + for (const auto& sr : info.sampleRates) { + std::string name = getBandwdithScaled(sr); + sampleRates.define(sr, name, sr); + if (sr == info.preferredSampleRate) { + srId = sampleRates.valueId(sr); + } + } + + // Load samplerate from config + config.acquire(); + if (config.conf["devices"][selectedDevice].contains("sampleRate")) { + sampleRate = config.conf["devices"][selectedDevice]["sampleRate"]; + if (sampleRates.keyExists(sampleRate)) { + srId = sampleRates.keyId(sampleRate); + } + } + config.release(); + + // Update samplerate from ID + sampleRate = sampleRates[srId]; + core::setInputSampleRate(sampleRate); + } + +private: + std::string getBandwdithScaled(double bw) { + char buf[1024]; + if (bw >= 1000000.0) { + sprintf(buf, "%.1lfMHz", bw / 1000000.0); + } + else if (bw >= 1000.0) { + sprintf(buf, "%.1lfKHz", bw / 1000.0); + } + else { + sprintf(buf, "%.1lfHz", bw); + } + return std::string(buf); + } + + static void menuSelected(void* ctx) { + AudioSourceModule* _this = (AudioSourceModule*)ctx; + core::setInputSampleRate(_this->sampleRate); + spdlog::info("AudioSourceModule '{0}': Menu Select!", _this->name); + } + + static void menuDeselected(void* ctx) { + AudioSourceModule* _this = (AudioSourceModule*)ctx; + spdlog::info("AudioSourceModule '{0}': Menu Deselect!", _this->name); + } + + static void start(void* ctx) { + AudioSourceModule* _this = (AudioSourceModule*)ctx; + if (_this->running) { return; } + + // Stream options + RtAudio::StreamParameters parameters; + parameters.deviceId = _this->devices[_this->devId].id; + parameters.nChannels = 2; + unsigned int bufferFrames = _this->sampleRate / 200; + RtAudio::StreamOptions opts; + opts.flags = RTAUDIO_MINIMIZE_LATENCY; + opts.streamName = "SDR++ Audio Source"; + + // Open and start stream + try { + _this->audio.openStream(NULL, ¶meters, RTAUDIO_FLOAT32, _this->sampleRate, &bufferFrames, callback, _this, &opts); + _this->audio.startStream(); + _this->running = true; + } + catch (std::exception e) { + spdlog::error("Error opening audio device: {0}", e.what()); + } + + spdlog::info("AudioSourceModule '{0}': Start!", _this->name); + } + + static void stop(void* ctx) { + AudioSourceModule* _this = (AudioSourceModule*)ctx; + if (!_this->running) { return; } + _this->running = false; + + _this->audio.stopStream(); + _this->audio.closeStream(); + + spdlog::info("AudioSourceModule '{0}': Stop!", _this->name); + } + + static void tune(double freq, void* ctx) { + // Not possible + } + + static void menuHandler(void* ctx) { + AudioSourceModule* _this = (AudioSourceModule*)ctx; + + if (_this->running) { SmGui::BeginDisabled(); } + + SmGui::FillWidth(); + SmGui::ForceSync(); + if (SmGui::Combo(CONCAT("##_audio_dev_sel_", _this->name), &_this->devId, _this->devices.txt)) { + std::string dev = _this->devices.key(_this->devId); + _this->select(dev); + config.acquire(); + config.conf["device"] = dev; + config.release(true); + } + + if (SmGui::Combo(CONCAT("##_audio_sr_sel_", _this->name), &_this->srId, _this->sampleRates.txt)) { + _this->sampleRate = _this->sampleRates[_this->srId]; + core::setInputSampleRate(_this->sampleRate); + if (!_this->selectedDevice.empty()) { + config.acquire(); + config.conf["devices"][_this->selectedDevice]["sampleRate"] = _this->sampleRate; + config.release(true); + } + } + + SmGui::SameLine(); + SmGui::FillWidth(); + SmGui::ForceSync(); + if (SmGui::Button(CONCAT("Refresh##_audio_refr_", _this->name))) { + _this->refresh(); + _this->select(_this->selectedDevice); + } + + if (_this->running) { SmGui::EndDisabled(); } + } + + static int callback(void* outputBuffer, void* inputBuffer, unsigned int nBufferFrames, double streamTime, RtAudioStreamStatus status, void* userData) { + AudioSourceModule* _this = (AudioSourceModule*)userData; + memcpy(_this->stream.writeBuf, inputBuffer, nBufferFrames * sizeof(dsp::complex_t)); + _this->stream.swap(nBufferFrames); + return 0; + } + + std::string name; + bool enabled = true; + dsp::stream stream; + double sampleRate; + SourceManager::SourceHandler handler; + bool running = false; + + OptionList devices; + OptionList sampleRates; + std::string selectedDevice = ""; + int devId = 0; + int srId = 0; + + RtAudio audio; +}; + +MOD_EXPORT void _INIT_() { + json def = json({}); + def["devices"] = json({}); + def["device"] = ""; + config.setPath(core::args["root"].s() + "/audio_source_config.json"); + config.load(def); + config.enableAutoSave(); +} + +MOD_EXPORT ModuleManager::Instance* _CREATE_INSTANCE_(std::string name) { + return new AudioSourceModule(name); +} + +MOD_EXPORT void _DELETE_INSTANCE_(ModuleManager::Instance* instance) { + delete (AudioSourceModule*)instance; +} + +MOD_EXPORT void _END_() { + config.disableAutoSave(); + config.save(); +} \ No newline at end of file