diff --git a/CMakeLists.txt b/CMakeLists.txt index 18a3da1c..a9c67193 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,3 @@ -set(CMAKE_BUILD_TYPE "Debug") # TODO: Make sure this gets properly disabled when going back to normal builds - cmake_minimum_required(VERSION 3.13) project(sdrpp) diff --git a/core/src/gui/icons.cpp b/core/src/gui/icons.cpp index 2204903e..fba1c577 100644 --- a/core/src/gui/icons.cpp +++ b/core/src/gui/icons.cpp @@ -16,6 +16,7 @@ namespace icons { ImTextureID UNMUTED; ImTextureID NORMAL_TUNING; ImTextureID CENTER_TUNING; + ImTextureID ALIGN_CENTER; GLuint loadTexture(std::string path) { int w, h, n; @@ -45,6 +46,7 @@ namespace icons { UNMUTED = (ImTextureID)(uintptr_t)loadTexture(resDir + "/icons/unmuted.png"); NORMAL_TUNING = (ImTextureID)(uintptr_t)loadTexture(resDir + "/icons/normal_tuning.png"); CENTER_TUNING = (ImTextureID)(uintptr_t)loadTexture(resDir + "/icons/center_tuning.png"); + ALIGN_CENTER = (ImTextureID)(uintptr_t)loadTexture(resDir + "/icons/align_center.png"); return true; } diff --git a/core/src/gui/icons.h b/core/src/gui/icons.h index 4a9f7fce..8dca22e2 100644 --- a/core/src/gui/icons.h +++ b/core/src/gui/icons.h @@ -13,7 +13,8 @@ namespace icons { extern ImTextureID UNMUTED; extern ImTextureID NORMAL_TUNING; extern ImTextureID CENTER_TUNING; - + extern ImTextureID ALIGN_CENTER; + GLuint loadTexture(std::string path); bool load(std::string resDir); } \ No newline at end of file diff --git a/core/src/gui/menus/streams.cpp b/core/src/gui/menus/streams.cpp index 3e0eedf2..1ec586fc 100644 --- a/core/src/gui/menus/streams.cpp +++ b/core/src/gui/menus/streams.cpp @@ -3,20 +3,57 @@ #include #include #include +#include +#include #define CONCAT(a, b) ((std::string(a) + b).c_str()) namespace streamsmenu { std::vector sinksToBeRemoved; + std::recursive_mutex sinkTypesMtx; + OptionList sinkTypes; + + std::map selectedSinkTypeId; + std::map addSinkTypeId; + + void updateSinkTypeList(const std::string& removed = "") { + std::lock_guard lck1(sinkTypesMtx); + auto lck2 = sigpath::streamManager.getSinkTypesLock(); + const auto& types = sigpath::streamManager.getSinkTypes(); + sinkTypes.clear(); + for (const auto& type : types) { + if (type == removed) { continue; } + sinkTypes.define(type, type, type); + } + } + + void onSinkProviderRegistered(const std::string& type) { + // Update the list + updateSinkTypeList(); + + // Update the selected ID of each drop down + // TODO + } + + void onSinkProviderUnregister(const std::string& type) { + // Update the list + updateSinkTypeList(type); + + // Update the selected ID of each drop down + // TODO + } + void init() { - + sigpath::streamManager.onSinkProviderRegistered.bind(onSinkProviderRegistered); + sigpath::streamManager.onSinkProviderUnregister.bind(onSinkProviderUnregister); + updateSinkTypeList(); } void draw(void* ctx) { float menuWidth = ImGui::GetContentRegionAvail().x; auto lck = sigpath::streamManager.getStreamsLock(); - auto streams = sigpath::streamManager.getStreams(); + const auto& streams = sigpath::streamManager.getStreams(); int count = 0; int maxCount = streams.size(); @@ -26,35 +63,88 @@ namespace streamsmenu { ImGui::Text("%s", name.c_str()); // Display ever sink - if (ImGui::BeginTable(CONCAT("sdrpp_streams_tbl_", name), 1, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + if (ImGui::BeginTable(CONCAT("sdrpp_streams_tbl_", name), 1, ImGuiTableFlags_Borders)) { auto lck2 = stream->getSinksLock(); auto sinks = stream->getSinks(); for (auto& [id, sink] : sinks) { + std::string sid = sink->getStringID(); ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); + float tableWidth = ImGui::GetContentRegionAvail().x; + ImGui::Spacing(); + + // Sink type + + sink->showMenu(); + float vol = sink->getVolume(); + bool muted = sink->getMuted(); + float pan = sink->getPanning(); + bool linked = true; - // Volume slider + mute button - - + float height = ImGui::GetTextLineHeightWithSpacing() + 2; + ImGui::PushID(ImGui::GetID(("sdrpp_streams_center_btn_" + sid).c_str())); + if (ImGui::ImageButton(icons::ALIGN_CENTER, ImVec2(height, height), ImVec2(0, 0), ImVec2(1, 1), 0, ImVec4(0, 0, 0, 0), ImGui::GetStyleColorVec4(ImGuiCol_Text))) { + sink->setPanning(0.0f); + } + ImGui::PopID(); + ImGui::SameLine(); ImGui::FillWidth(); - if (ImGui::Button(CONCAT("Remove##sdrpp_streams_remove_type_", name))) { + if (ImGui::SliderFloat(CONCAT("##sdrpp_streams_pan_", sid), &pan, -1, 1, "")) { + sink->setPanning(pan); + } + + if (muted) { + ImGui::PushID(ImGui::GetID(("sdrpp_unmute_btn_" + sid).c_str())); + if (ImGui::ImageButton(icons::MUTED, ImVec2(height, height), ImVec2(0, 0), ImVec2(1, 1), 0, ImVec4(0, 0, 0, 0), ImGui::GetStyleColorVec4(ImGuiCol_Text))) { + sink->setMuted(false); + } + ImGui::PopID(); + } + else { + ImGui::PushID(ImGui::GetID(("sdrpp_mute_btn_" + sid).c_str())); + if (ImGui::ImageButton(icons::UNMUTED, ImVec2(height, height), ImVec2(0, 0), ImVec2(1, 1), 0, ImVec4(0, 0, 0, 0), ImGui::GetStyleColorVec4(ImGuiCol_Text))) { + sink->setMuted(true); + } + ImGui::PopID(); + } + ImGui::SameLine(); + ImGui::FillWidth(); + if (ImGui::SliderFloat(CONCAT("##sdrpp_streams_vol_", sid), &vol, 0, 1, "")) { + sink->setVolume(vol); + } + + + int startCur = ImGui::GetCursorPosX(); + if (ImGui::Checkbox(CONCAT("Link volume##sdrpp_streams_vol_", sid), &linked)) { + // TODO + } + ImGui::SameLine(); + if (ImGui::Button(CONCAT("Remove##sdrpp_streams_remove_type_", sid), ImVec2(tableWidth - (ImGui::GetCursorPosX() - startCur), 0))) { sinksToBeRemoved.push_back(id); } + ImGui::Spacing(); } lck2.unlock(); ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); + float tableWidth = ImGui::GetContentRegionAvail().x; + ImGui::Spacing(); int ssds = 0; - ImGui::Combo(CONCAT("##sdrpp_streams_add_type_", name), &ssds, "Test 1\0Test 2\0"); + int startCur = ImGui::GetCursorPosX(); + { + std::lock_guard lck(sinkTypesMtx); + ImGui::Combo(CONCAT("##sdrpp_streams_add_type_", name), &ssds, sinkTypes.txt); + } + ImGui::SameLine(); - ImGui::FillWidth(); - if (ImGui::Button(CONCAT("Add##sdrpp_streams_add_btn_", name))) { + if (ImGui::Button(CONCAT("Add##sdrpp_streams_add_btn_", name), ImVec2(tableWidth - (ImGui::GetCursorPosX() - startCur), 0))) { stream->addSink("Audio"); } + ImGui::Spacing(); ImGui::EndTable(); diff --git a/core/src/signal_path/stream.cpp b/core/src/signal_path/stream.cpp index e6e64735..8a969269 100644 --- a/core/src/signal_path/stream.cpp +++ b/core/src/signal_path/stream.cpp @@ -1,11 +1,12 @@ #include "stream.h" #include -Sink::Sink(SinkEntry* entry, dsp::stream* stream, const std::string& name, SinkID id) : +Sink::Sink(SinkEntry* entry, dsp::stream* stream, const std::string& name, SinkID id, const std::string& stringId) : entry(entry), stream(stream), streamName(name), - id(id) + id(id), + stringId(stringId) {} void Sink::showMenu() {} @@ -17,6 +18,12 @@ SinkEntry::SinkEntry(StreamManager* manager, AudioStream* parentStream, const st { this->type = type; this->inputSamplerate = inputSamplerate; + + // Generate string ID + stringId = parentStream->getName(); + char buf[16]; + sprintf(buf, "%d", (int)id); + stringId += buf; // Initialize DSP resamp.init(&input, inputSamplerate, inputSamplerate); @@ -44,7 +51,7 @@ void SinkEntry::setType(const std::string& type) { auto lck2 = manager->getSinkTypesLock(); // Get the provider or throw error - auto types = manager->getSinkTypes(); + const auto& types = manager->getSinkTypes(); if (std::find(types.begin(), types.end(), type) == types.end()) { this->type.clear(); throw SinkEntryCreateException("Invalid sink type"); @@ -53,7 +60,7 @@ void SinkEntry::setType(const std::string& type) { // Create sink this->type = type; provider = manager->providers[type]; - sink = provider->createSink(this, &volumeAdjust.out, parentStream->getName(), id); + sink = provider->createSink(this, &volumeAdjust.out, parentStream->getName(), id, stringId); } SinkID SinkEntry::getID() const { @@ -111,6 +118,15 @@ void SinkEntry::stopSink() { sink->stop(); } +std::lock_guard SinkEntry::getLock() { + return std::lock_guard(mtx); +} + +void SinkEntry::setSamplerate(double samplerate) { + std::lock_guard lck(mtx); + resamp.setOutSamplerate(samplerate); +} + void SinkEntry::startDSP() { std::lock_guard lck(mtx); resamp.start(); @@ -136,6 +152,10 @@ void SinkEntry::setInputSamplerate(double samplerate) { resamp.setInSamplerate(samplerate); } +std::string SinkEntry::getStringID() { + return stringId; +} + AudioStream::AudioStream(StreamManager* manager, const std::string& name, dsp::stream* stream, double samplerate) : manager(manager), name(name) @@ -449,7 +469,7 @@ void StreamManager::unregisterSinkProvider(SinkProvider* provider) { for (auto& [name, stream] : streams) { // Aquire lock on sink list auto sLock = stream->getSinksLock(); - auto sinks = stream->getSinks(); + const auto& sinks = stream->getSinks(); // Find all sinks with the type that is about to be removed std::vector toRemove; diff --git a/core/src/signal_path/stream.h b/core/src/signal_path/stream.h index 44fd7fca..6821e2df 100644 --- a/core/src/signal_path/stream.h +++ b/core/src/signal_path/stream.h @@ -19,7 +19,7 @@ class SinkEntry; class Sink { public: - Sink(SinkEntry* entry, dsp::stream* stream, const std::string& name, SinkID id); + Sink(SinkEntry* entry, dsp::stream* stream, const std::string& name, SinkID id, const std::string& stringId); virtual ~Sink() {} virtual void start() = 0; @@ -31,6 +31,7 @@ protected: dsp::stream* const stream; const std::string streamName; const SinkID id; + const std::string stringId; }; class SinkProvider { @@ -41,7 +42,7 @@ public: * @param name Name of the audio stream. * @param index Index of the sink in the menu. Should be use to keep settings. */ - virtual std::unique_ptr createSink(SinkEntry* entry, dsp::stream* stream, const std::string& name, SinkID index) = 0; + virtual std::unique_ptr createSink(SinkEntry* entry, dsp::stream* stream, const std::string& name, SinkID id, const std::string& stringId) = 0; /** * Destroy a sink instance. This function is so that the provide knows at all times how many instances there are. @@ -62,6 +63,7 @@ class StreamManager; // TODO: Would be cool to have data and audio sinks instead of just audio. class SinkEntry { friend AudioStream; + friend Sink; public: SinkEntry(StreamManager* manager, AudioStream* parentStream, const std::string& type, SinkID id, double inputSamplerate); @@ -124,6 +126,12 @@ public: */ void showMenu(); + /** + * Get the string form ID unique to both the sink and stream. Be used to reference settings. + * @return Unique string ID. + */ + std::string getStringID(); + // Emitted when the type of the sink was changed NewEvent onTypeChanged; // Emmited when volume of the sink was changed @@ -133,11 +141,17 @@ public: // Emitted when the panning of the sink was changed NewEvent onPanningChanged; + // TODO: Need to allow the sink to change the entry samplerate and start/stop the DSP + // This will also require allowing it to get a lock on the sink so others don't attempt to mess with it. + std::lock_guard getLock(); + void startDSP(); + void stopDSP(); + void setSamplerate(double samplerate); + private: void startSink(); void stopSink(); - void startDSP(); - void stopDSP(); + void destroy(bool forgetSettings); void setInputSamplerate(double samplerate); @@ -153,6 +167,8 @@ private: double inputSamplerate; AudioStream* const parentStream; StreamManager* const manager; + + std::string stringId; float volume = 1.0f; bool muted = false; diff --git a/decoder_modules/radio/src/radio_module.h b/decoder_modules/radio/src/radio_module.h index 057e4e55..b04a1468 100644 --- a/decoder_modules/radio/src/radio_module.h +++ b/decoder_modules/radio/src/radio_module.h @@ -446,6 +446,9 @@ private: afChain.disableAllBlocks([=](dsp::stream* out){ stream->setInput(out); }); } + // Update audo samplerate + stream->setSamplerate(selectedDemod->getAFSampleRate()); + // Start the IF chain ifChain.start(); diff --git a/root/res/icons/align_center.png b/root/res/icons/align_center.png new file mode 100644 index 00000000..ed9caa97 Binary files /dev/null and b/root/res/icons/align_center.png differ diff --git a/sink_modules/audio_sink/src/main.cpp b/sink_modules/audio_sink/src/main.cpp index 15b7c260..3a6bc434 100644 --- a/sink_modules/audio_sink/src/main.cpp +++ b/sink_modules/audio_sink/src/main.cpp @@ -2,10 +2,10 @@ #include #include #include -#include #include #include #include +#include #include #include #include @@ -22,26 +22,37 @@ SDRPP_MOD_INFO{ ConfigManager config; -class AudioSink : SinkManager::Sink { +bool operator==(const RtAudio::DeviceInfo& a, const RtAudio::DeviceInfo& b) { + return a.name == b.name; +} + +class AudioSink : public Sink { public: - AudioSink(SinkManager::Stream* stream, std::string streamName) { - _stream = stream; - _streamName = streamName; - s2m.init(_stream->sinkOut); + AudioSink(SinkEntry* entry, dsp::stream* stream, const std::string& name, SinkID id, const std::string& stringId) : + Sink(entry, stream, name, id, stringId) + { + s2m.init(stream); monoPacker.init(&s2m.out, 512); - stereoPacker.init(_stream->sinkOut, 512); + stereoPacker.init(stream, 512); - bool created = false; + // Convert config to the new format and get selected device + bool modified = 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); + // config.acquire(); + // if (config.conf.contains(streamName)) { + // if (!config.conf[streamName].is_array()) { + // json tmp = config.conf[streamName]; + // config.conf[streamName] = json::array(); + // config.conf[streamName][0] = tmp; + // modified = true; + // } + // if (config.conf[streamName].contains((int)id)) { + // device = config.conf[streamName][(int)id]["device"]; + // } + // } + // config.release(modified); + // List devices int count = audio.getDeviceCount(); RtAudio::DeviceInfo info; for (int i = 0; i < count; i++) { @@ -50,15 +61,13 @@ public: 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'; + devList.define(i, info.name, info); } catch (std::exception e) { flog::error("AudioSinkModule Error getting audio device info: {0}", e.what()); } } + selectByName(device); } @@ -92,79 +101,104 @@ public: } void selectById(int id) { + // Update 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); + selectedDevName = devList[id].name; - sampleRates = devList[id].sampleRates; - sampleRatesTxt = ""; + // List samplerates and select default SR char buf[256]; - bool found = false; - unsigned int defaultId = 0; + sampleRates.clear(); + const auto& srList = devList[id].sampleRates; unsigned int defaultSr = devList[id].preferredSampleRate; - for (int i = 0; i < sampleRates.size(); i++) { - if (sampleRates[i] == sampleRate) { - found = true; - srId = i; + for (auto& sr : srList) { + if (sr == defaultSr) { + srId = sampleRates.size(); + sampleRate = sr; } - if (sampleRates[i] == defaultSr) { - defaultId = i; - } - sprintf(buf, "%d", sampleRates[i]); - sampleRatesTxt += buf; - sampleRatesTxt += '\0'; - } - if (!found) { - sampleRate = defaultSr; - srId = defaultId; + sprintf(buf, "%d", sr); + sampleRates.define(sr, buf, sr); } + + // // Load config + // config.acquire(); + // if (config.conf[streamName][(int)id].contains(selectedDevName)) { + // unsigned int wantedSr = config.conf[streamName][id][selectedDevName]; + // if (sampleRates.keyExists(wantedSr)) { + // srId = sampleRates.keyId(wantedSr); + // sampleRate = sampleRates[srId]; + // } + // } + // config.release(); - _stream->setSampleRate(sampleRate); + // Lock the sink + auto lck = entry->getLock(); + // Stop the sink DSP + // TODO: Only if the sink DSP is running, otherwise you risk starting it when it shouldn't + entry->stopDSP(); + + // Stop the sink if (running) { doStop(); } + + // Update stream samplerate + entry->setSamplerate(sampleRate); + + // Start the DSP + entry->startDSP(); + + // Start the sink if (running) { doStart(); } } - void menuHandler() { + void showMenu() { float menuWidth = ImGui::GetContentRegionAvail().x; ImGui::SetNextItemWidth(menuWidth); - if (ImGui::Combo(("##_audio_sink_dev_" + _streamName).c_str(), &devId, txtDevList.c_str())) { + if (ImGui::Combo(("##_audio_sink_dev_" + stringId).c_str(), &devId, devList.txt)) { selectById(devId); - config.acquire(); - config.conf[_streamName]["device"] = devList[devId].name; - config.release(true); + // 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())) { + if (ImGui::Combo(("##_audio_sink_sr_" + stringId).c_str(), &srId, sampleRates.txt)) { sampleRate = sampleRates[srId]; - _stream->setSampleRate(sampleRate); - if (running) { - doStop(); - doStart(); - } - config.acquire(); - config.conf[_streamName]["devices"][devList[devId].name] = sampleRate; - config.release(true); + + // Lock the sink + auto lck = entry->getLock(); + + // Stop the sink DSP + // TODO: Only if the sink DSP is running, otherwise you risk starting it when it shouldn't + entry->stopDSP(); + + // Stop the sink + if (running) { doStop(); } + + // Update stream samplerate + entry->setSamplerate(sampleRate); + + // Start the DSP + entry->startDSP(); + + // Start the sink + if (running) { doStart(); } + + // config.acquire(); + // config.conf[streamName]["devices"][devList[devId].name] = sampleRate; + // config.release(true); } } private: bool doStart() { RtAudio::StreamParameters parameters; - parameters.deviceId = deviceIds[devId]; + parameters.deviceId = devList.key(devId); parameters.nChannels = 2; unsigned int bufferFrames = sampleRate / 60; RtAudio::StreamOptions opts; opts.flags = RTAUDIO_MINIMIZE_LATENCY; - opts.streamName = _streamName; + opts.streamName = streamName; try { audio.openStream(¶meters, NULL, RTAUDIO_FLOAT32, sampleRate, &bufferFrames, &callback, this, &opts); @@ -198,57 +232,39 @@ private: 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; + std::string selectedDevName; unsigned int defaultDevId = 0; - std::vector devList; - std::vector deviceIds; - std::string txtDevList; + OptionList devList; + OptionList sampleRates; - std::vector sampleRates; - std::string sampleRatesTxt; unsigned int sampleRate = 48000; RtAudio audio; }; -class AudioSinkModule : public ModuleManager::Instance { +class AudioSinkModule : public ModuleManager::Instance, public SinkProvider { public: AudioSinkModule(std::string name) { this->name = name; - provider.create = create_sink; - provider.ctx = this; - - sigpath::sinkManager.registerSinkProvider("Audio", provider); + sigpath::streamManager.registerSinkProvider("Audio", this); } ~AudioSinkModule() { - // Unregister sink, this will automatically stop and delete all instances of the audio sink - sigpath::sinkManager.unregisterSinkProvider("Audio"); + sigpath::streamManager.unregisterSinkProvider(this); } void postInit() {} @@ -265,14 +281,13 @@ public: return enabled; } -private: - static SinkManager::Sink* create_sink(SinkManager::Stream* stream, std::string streamName, void* ctx) { - return (SinkManager::Sink*)(new AudioSink(stream, streamName)); + std::unique_ptr createSink(SinkEntry* entry, dsp::stream* stream, const std::string& name, SinkID id, const std::string& stringId) { + return std::make_unique(entry, stream, name, id, stringId); } +private: std::string name; bool enabled = true; - SinkManager::SinkProvider provider; }; MOD_EXPORT void _INIT_() { @@ -294,4 +309,4 @@ MOD_EXPORT void _DELETE_INSTANCE_(void* instance) { MOD_EXPORT void _END_() { config.disableAutoSave(); config.save(); -} +} \ No newline at end of file