mirror of
				https://github.com/AlexandreRouma/SDRPlusPlus.git
				synced 2025-10-31 17:08:13 +01:00 
			
		
		
		
	new recorder stuff
This commit is contained in:
		| @@ -171,4 +171,4 @@ namespace dsp::multirate { | ||||
|         double _outSamplerate; | ||||
|         Mode mode; | ||||
|     }; | ||||
| } | ||||
| } | ||||
| @@ -19,125 +19,77 @@ | ||||
| #include <gui/widgets/folder_select.h> | ||||
| #include <recorder_interface.h> | ||||
| #include <core.h> | ||||
| #include <utils/optionlist.h> | ||||
| #include "wav.h" | ||||
|  | ||||
| #define CONCAT(a, b) ((std::string(a) + b).c_str()) | ||||
|  | ||||
| SDRPP_MOD_INFO{ | ||||
|     /* Name:            */ "recorder", | ||||
|     /* Description:     */ "Recorder module for SDR++", | ||||
|     /* Author:          */ "Ryzerth", | ||||
|     /* Version:         */ 0, 2, 0, | ||||
|     /* Version:         */ 0, 3, 0, | ||||
|     /* Max instances    */ -1 | ||||
| }; | ||||
|  | ||||
| ConfigManager config; | ||||
|  | ||||
| std::string genFileName(std::string prefix, bool isVfo, std::string name = "") { | ||||
|     time_t now = time(0); | ||||
|     tm* ltm = localtime(&now); | ||||
|     char buf[1024]; | ||||
|     double freq = gui::waterfall.getCenterFrequency(); | ||||
|     ; | ||||
|     if (isVfo && gui::waterfall.vfos.find(name) != gui::waterfall.vfos.end()) { | ||||
|         freq += gui::waterfall.vfos[name]->generalOffset; | ||||
|     } | ||||
|     sprintf(buf, "%.0lfHz_%02d-%02d-%02d_%02d-%02d-%02d.wav", freq, ltm->tm_hour, ltm->tm_min, ltm->tm_sec, ltm->tm_mday, ltm->tm_mon + 1, ltm->tm_year + 1900); | ||||
|     return prefix + buf; | ||||
| } | ||||
|  | ||||
| class RecorderModule : public ModuleManager::Instance { | ||||
| public: | ||||
|     RecorderModule(std::string name) : folderSelect("%ROOT%/recordings") { | ||||
|         this->name = name; | ||||
|  | ||||
|         root = (std::string)core::args["root"]; | ||||
|  | ||||
|         // Define option lists | ||||
|         formats.define("WAV", wav::FORMAT_WAV); | ||||
|         formats.define("RF64", wav::FORMAT_RF64); | ||||
|         sampleDepths.define(wav::SAMP_DEPTH_8BIT, "8-Bit", wav::SAMP_DEPTH_8BIT); | ||||
|         sampleDepths.define(wav::SAMP_DEPTH_16BIT, "16-Bit", wav::SAMP_DEPTH_16BIT); | ||||
|         sampleDepths.define(wav::SAMP_DEPTH_32BIT, "32-Bit", wav::SAMP_DEPTH_32BIT); | ||||
|  | ||||
|         // Load default config for option lists | ||||
|         formatId = formats.valueId(wav::FORMAT_WAV); | ||||
|         sampleDepthId = sampleDepths.valueId(wav::SAMP_DEPTH_16BIT); | ||||
|  | ||||
|         // Load config | ||||
|         config.acquire(); | ||||
|         bool created = false; | ||||
|  | ||||
|         // Create config if it doesn't exist | ||||
|         if (!config.conf.contains(name)) { | ||||
|             config.conf[name]["mode"] = RECORDER_MODE_AUDIO; | ||||
|             config.conf[name]["recPath"] = "%ROOT%/recordings"; | ||||
|             config.conf[name]["audioStream"] = "Radio"; | ||||
|             config.conf[name]["audioVolume"] = 1.0; | ||||
|             created = true; | ||||
|         if (config.conf[name].contains("mode")) { | ||||
|             recMode = config.conf[name]["mode"]; | ||||
|         } | ||||
|  | ||||
|         if (!config.conf[name].contains("audioVolume")) { | ||||
|             config.conf[name]["audioVolume"] = 1.0; | ||||
|         if (config.conf[name].contains("recPath")) { | ||||
|             folderSelect.setPath(config.conf[name]["recPath"]); | ||||
|         } | ||||
|         if (!config.conf[name].contains("ignoreSilence")) { | ||||
|             config.conf[name]["ignoreSilence"] = false; | ||||
|         if (config.conf[name].contains("format") && formats.keyExists(config.conf[name]["format"])) { | ||||
|             formatId = formats.keyId(config.conf[name]["format"]); | ||||
|         } | ||||
|         if (config.conf[name].contains("sampleDepth") && sampleDepths.keyExists(config.conf[name]["sampleDepth"])) { | ||||
|             sampleDepthId = sampleDepths.keyId(config.conf[name]["sampleDepth"]); | ||||
|         } | ||||
|         if (config.conf[name].contains("audioStream")) { | ||||
|             selectedStreamName = config.conf[name]["audioStream"]; | ||||
|         } | ||||
|         if (config.conf[name].contains("audioVolume")) { | ||||
|             audioVolume = config.conf[name]["audioVolume"]; | ||||
|         } | ||||
|         if (config.conf[name].contains("ignoreSilence")) { | ||||
|             ignoreSilence = config.conf[name]["ignoreSilence"]; | ||||
|         } | ||||
|         config.release(); | ||||
|  | ||||
|         recMode = config.conf[name]["mode"]; | ||||
|         folderSelect.setPath(config.conf[name]["recPath"]); | ||||
|         selectedStreamName = config.conf[name]["audioStream"]; | ||||
|         audioVolume = config.conf[name]["audioVolume"]; | ||||
|         ignoreSilence = config.conf[name]["ignoreSilence"]; | ||||
|         config.release(created); | ||||
|  | ||||
|         // Init audio path | ||||
|         vol.init(&dummyStream, audioVolume, false); | ||||
|         audioSplit.init(&vol.out); | ||||
|         audioSplit.bindStream(&meterStream); | ||||
|         meter.init(&meterStream); | ||||
|         audioHandler.init(&audioHandlerStream, _audioHandler, this); | ||||
|  | ||||
|         vol.start(); | ||||
|         audioSplit.start(); | ||||
|         meter.start(); | ||||
|  | ||||
|         // Init baseband path | ||||
|         basebandHandler.init(&basebandStream, _basebandHandler, this); | ||||
|  | ||||
|         wavSampleBuf = new int16_t[2 * STREAM_BUFFER_SIZE]; | ||||
|         // Init sinks | ||||
|         basebandSink.init(NULL, complexHandler, this); | ||||
|         stereoSink.init(NULL, stereoHandler, this); | ||||
|         monoSink.init(NULL, monoHandler, this); | ||||
|  | ||||
|         gui::menu.registerEntry(name, menuHandler, this); | ||||
|         core::modComManager.registerInterface("recorder", name, moduleInterfaceHandler, this); | ||||
|  | ||||
|         streamRegisteredHandler.handler = onStreamRegistered; | ||||
|         streamRegisteredHandler.ctx = this; | ||||
|         streamUnregisterHandler.handler = onStreamUnregister; | ||||
|         streamUnregisterHandler.ctx = this; | ||||
|         streamUnregisteredHandler.handler = onStreamUnregistered; | ||||
|         streamUnregisteredHandler.ctx = this; | ||||
|         sigpath::sinkManager.onStreamRegistered.bindHandler(&streamRegisteredHandler); | ||||
|         sigpath::sinkManager.onStreamUnregister.bindHandler(&streamUnregisterHandler); | ||||
|         sigpath::sinkManager.onStreamUnregistered.bindHandler(&streamUnregisteredHandler); | ||||
|     } | ||||
|  | ||||
|     ~RecorderModule() { | ||||
|         std::lock_guard lck(recMtx); | ||||
|         gui::menu.removeEntry(name); | ||||
|         core::modComManager.unregisterInterface(name); | ||||
|  | ||||
|         // Stop recording | ||||
|         if (recording) { stopRecording(); } | ||||
|  | ||||
|         vol.setInput(&dummyStream); | ||||
|         if (audioInput != NULL) { sigpath::sinkManager.unbindStream(selectedStreamName, audioInput); } | ||||
|  | ||||
|         sigpath::sinkManager.onStreamRegistered.unbindHandler(&streamRegisteredHandler); | ||||
|         sigpath::sinkManager.onStreamUnregister.unbindHandler(&streamUnregisterHandler); | ||||
|         sigpath::sinkManager.onStreamUnregistered.unbindHandler(&streamUnregisteredHandler); | ||||
|  | ||||
|         vol.stop(); | ||||
|         audioSplit.stop(); | ||||
|         meter.stop(); | ||||
|  | ||||
|         delete[] wavSampleBuf; | ||||
|     } | ||||
|  | ||||
|     void postInit() { | ||||
|         refreshStreams(); | ||||
|         if (selectedStreamName == "") { | ||||
|             selectStream(streamNames[0]); | ||||
|         } | ||||
|         else { | ||||
|             selectStream(selectedStreamName); | ||||
|         } | ||||
|          | ||||
|     } | ||||
|  | ||||
|     void enable() { | ||||
| @@ -152,70 +104,95 @@ public: | ||||
|         return enabled; | ||||
|     } | ||||
|  | ||||
|     void start() { | ||||
|         std::lock_guard<std::recursive_mutex> lck(recMtx); | ||||
|         if (recording) { return; } | ||||
|  | ||||
|         // Configure the wav writer | ||||
|         if (recMode == RECORDER_MODE_AUDIO) { | ||||
|             samplerate = sigpath::sinkManager.getStreamSampleRate("Radio"); | ||||
|         } | ||||
|         else { | ||||
|             samplerate = sigpath::iqFrontEnd.getSampleRate(); | ||||
|         } | ||||
|         writer.setFormat(formats[formatId]); | ||||
|         writer.setChannels((recMode == RECORDER_MODE_AUDIO && !stereo) ? 1 : 2); | ||||
|         writer.setSampleDepth(sampleDepths[sampleDepthId]); | ||||
|         writer.setSamplerate(samplerate); | ||||
|  | ||||
|         // Open file | ||||
|         std::string prefix = (recMode == RECORDER_MODE_AUDIO) ? "/audio_" : "/baseband_"; | ||||
|         std::string expandedPath = expandString(folderSelect.path + genFileName(prefix, false)); | ||||
|         if (!writer.open(expandedPath)) { | ||||
|             spdlog::error("Failed to open file for recording: {0}", expandedPath); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Open audio stream or baseband | ||||
|         // TODO: DO NOT HARDCODE THE STREAM NAME | ||||
|         if (recMode == RECORDER_MODE_AUDIO) { | ||||
|             // TODO: HAS TO BE DONE PROPERLY | ||||
|             stereoStream = sigpath::sinkManager.bindStream("Radio"); | ||||
|             stereoSink.setInput(stereoStream); | ||||
|             stereoSink.start(); | ||||
|         } | ||||
|         else { | ||||
|             // Create and bind IQ stream | ||||
|             basebandStream = new dsp::stream<dsp::complex_t>(); | ||||
|             basebandSink.setInput(basebandStream); | ||||
|             basebandSink.start(); | ||||
|             sigpath::iqFrontEnd.bindIQStream(basebandStream); | ||||
|         } | ||||
|  | ||||
|         recording = true; | ||||
|     } | ||||
|  | ||||
|     void stop() { | ||||
|         std::lock_guard<std::recursive_mutex> lck(recMtx); | ||||
|         if (!recording) { return; } | ||||
|  | ||||
|         // Close audio stream or baseband | ||||
|         if (recMode == RECORDER_MODE_AUDIO) { | ||||
|             // TODO: HAS TO BE DONE PROPERLY | ||||
|             stereoSink.stop(); | ||||
|             sigpath::sinkManager.unbindStream("Radio", stereoStream); | ||||
|         } | ||||
|         else { | ||||
|             // Unbind and destroy IQ stream | ||||
|             sigpath::iqFrontEnd.unbindIQStream(basebandStream); | ||||
|             basebandSink.stop(); | ||||
|             delete basebandStream; | ||||
|         } | ||||
|  | ||||
|         // Close file | ||||
|         writer.close(); | ||||
|          | ||||
|         recording = false; | ||||
|     } | ||||
|  | ||||
| private: | ||||
|     void refreshStreams() { | ||||
|         std::vector<std::string> names = sigpath::sinkManager.getStreamNames(); | ||||
|  | ||||
|         streamNames.clear(); | ||||
|         streamNamesTxt = ""; | ||||
|  | ||||
|         // If there are no stream, cancel | ||||
|         if (names.size() == 0) { return; } | ||||
|  | ||||
|         // List streams | ||||
|         for (auto const& name : names) { | ||||
|             streamNames.push_back(name); | ||||
|             streamNamesTxt += name; | ||||
|             streamNamesTxt += '\0'; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     void selectStream(std::string name) { | ||||
|         if (streamNames.empty()) { | ||||
|             selectedStreamName = ""; | ||||
|             return; | ||||
|         } | ||||
|         auto it = std::find(streamNames.begin(), streamNames.end(), name); | ||||
|         if (it == streamNames.end()) { | ||||
|             selectStream(streamNames[0]); | ||||
|             return; | ||||
|         } | ||||
|         streamId = std::distance(streamNames.begin(), it); | ||||
|  | ||||
|         vol.stop(); | ||||
|         if (audioInput != NULL) { sigpath::sinkManager.unbindStream(selectedStreamName, audioInput); } | ||||
|         audioInput = sigpath::sinkManager.bindStream(name); | ||||
|         if (audioInput == NULL) { | ||||
|             selectedStreamName = ""; | ||||
|             return; | ||||
|         } | ||||
|         selectedStreamName = name; | ||||
|         vol.setInput(audioInput); | ||||
|         vol.start(); | ||||
|     } | ||||
|  | ||||
|     static void menuHandler(void* ctx) { | ||||
|         RecorderModule* _this = (RecorderModule*)ctx; | ||||
|         float menuColumnWidth = ImGui::GetContentRegionAvail().x; | ||||
|         float menuWidth = ImGui::GetContentRegionAvail().x; | ||||
|  | ||||
|         // Recording mode | ||||
|         if (_this->recording) { style::beginDisabled(); } | ||||
|         ImGui::BeginGroup(); | ||||
|         ImGui::Columns(2, CONCAT("AirspyGainModeColumns##_", _this->name), false); | ||||
|         if (ImGui::RadioButton(CONCAT("Baseband##_recmode_", _this->name), _this->recMode == RECORDER_MODE_BASEBAND)) { | ||||
|         ImGui::Columns(2, CONCAT("RecorderModeColumns##_", _this->name), false); | ||||
|         if (ImGui::RadioButton(CONCAT("Baseband##_recorder_mode_", _this->name), _this->recMode == RECORDER_MODE_BASEBAND)) { | ||||
|             _this->recMode = RECORDER_MODE_BASEBAND; | ||||
|             config.acquire(); | ||||
|             config.conf[_this->name]["mode"] = _this->recMode; | ||||
|             config.release(true); | ||||
|         } | ||||
|         ImGui::NextColumn(); | ||||
|         if (ImGui::RadioButton(CONCAT("Audio##_recmode_", _this->name), _this->recMode == RECORDER_MODE_AUDIO)) { | ||||
|         if (ImGui::RadioButton(CONCAT("Audio##_recorder_mode_", _this->name), _this->recMode == RECORDER_MODE_AUDIO)) { | ||||
|             _this->recMode = RECORDER_MODE_AUDIO; | ||||
|             config.acquire(); | ||||
|             config.conf[_this->name]["mode"] = _this->recMode; | ||||
|             config.release(true); | ||||
|         } | ||||
|         ImGui::Columns(1, CONCAT("EndAirspyGainModeColumns##_", _this->name), false); | ||||
|         ImGui::Columns(1, CONCAT("EndRecorderModeColumns##_", _this->name), false); | ||||
|         ImGui::EndGroup(); | ||||
|         if (_this->recording) { style::endDisabled(); } | ||||
|  | ||||
| @@ -228,312 +205,147 @@ private: | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Mode specific menu | ||||
|         if (_this->recMode == RECORDER_MODE_AUDIO) { | ||||
|             _this->audioMenu(menuColumnWidth); | ||||
|         ImGui::LeftLabel("WAV Format"); | ||||
|         ImGui::FillWidth(); | ||||
|         if (ImGui::Combo(CONCAT("##_recorder_wav_fmt_", _this->name), &_this->formatId, _this->formats.txt)) { | ||||
|             config.acquire(); | ||||
|             config.conf[_this->name]["format"] = _this->formats.key(_this->formatId); | ||||
|             config.release(true); | ||||
|         } | ||||
|         else { | ||||
|             _this->basebandMenu(menuColumnWidth); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     void basebandMenu(float menuColumnWidth) { | ||||
|         if (!folderSelect.pathIsValid()) { style::beginDisabled(); } | ||||
|         if (!recording) { | ||||
|             if (ImGui::Button(CONCAT("Record##_recorder_rec_", name), ImVec2(menuColumnWidth, 0))) { | ||||
|                 std::lock_guard lck(recMtx); | ||||
|                 startRecording(); | ||||
|         ImGui::LeftLabel("Sample depth"); | ||||
|         ImGui::FillWidth(); | ||||
|         if (ImGui::Combo(CONCAT("##_recorder_bits_", _this->name), &_this->sampleDepthId, _this->sampleDepths.txt)) { | ||||
|             config.acquire(); | ||||
|             config.conf[_this->name]["sampleDepth"] = _this->sampleDepths.key(_this->sampleDepthId); | ||||
|             config.release(true); | ||||
|         } | ||||
|  | ||||
|         // Show additional audio options | ||||
|         if (_this->recMode == RECORDER_MODE_AUDIO) {             | ||||
|             _this->updateAudioMeter(_this->audioLvl); | ||||
|             ImGui::FillWidth(); | ||||
|             ImGui::VolumeMeter(_this->audioLvl.l, _this->audioLvl.l, -60, 10); | ||||
|             ImGui::FillWidth(); | ||||
|             ImGui::VolumeMeter(_this->audioLvl.r, _this->audioLvl.r, -60, 10); | ||||
|  | ||||
|             ImGui::FillWidth(); | ||||
|             if (ImGui::SliderFloat(CONCAT("##_recorder_vol_", _this->name), &_this->audioVolume, 0, 1, "")) { | ||||
|                 // TODO: ADD VOLUME CONTROL | ||||
|                 //_this->vol.setVolume(_this->audioVolume); | ||||
|                 config.acquire(); | ||||
|                 config.conf[_this->name]["audioVolume"] = _this->audioVolume; | ||||
|                 config.release(true); | ||||
|             } | ||||
|             ImGui::PopItemWidth(); | ||||
|  | ||||
|             if (_this->recording) { style::beginDisabled(); } | ||||
|             if (ImGui::Checkbox(CONCAT("Stereo##_recorder_stereo_", _this->name), &_this->stereo)) { | ||||
|                 config.acquire(); | ||||
|                 config.conf[_this->name]["stereo"] = _this->stereo; | ||||
|                 config.release(true); | ||||
|             } | ||||
|             if (_this->recording) { style::endDisabled(); } | ||||
|  | ||||
|             if (ImGui::Checkbox(CONCAT("Ignore silence##_recorder_ignore_silence_", _this->name), &_this->ignoreSilence)) { | ||||
|                 config.acquire(); | ||||
|                 config.conf[_this->name]["ignoreSilence"] = _this->ignoreSilence; | ||||
|                 config.release(true); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Record button | ||||
|         bool canRecord = _this->folderSelect.pathIsValid(); | ||||
|         if (_this->recMode == RECORDER_MODE_AUDIO) { canRecord &= !_this->selectedStreamName.empty(); } | ||||
|         if (!_this->recording) { | ||||
|             if (ImGui::Button(CONCAT("Record##_recorder_rec_", _this->name), ImVec2(menuWidth, 0))) { | ||||
|                 _this->start(); | ||||
|             } | ||||
|             ImGui::TextColored(ImGui::GetStyleColorVec4(ImGuiCol_Text), "Idle --:--:--"); | ||||
|         } | ||||
|         else { | ||||
|             if (ImGui::Button(CONCAT("Stop##_recorder_rec_", name), ImVec2(menuColumnWidth, 0))) { | ||||
|                 std::lock_guard lck(recMtx); | ||||
|                 stopRecording(); | ||||
|             if (ImGui::Button(CONCAT("Stop##_recorder_rec_", _this->name), ImVec2(menuWidth, 0))) { | ||||
|                 _this->stop(); | ||||
|             } | ||||
|             uint64_t seconds = samplesWritten / (uint64_t)sampleRate; | ||||
|             uint64_t seconds = _this->writer.getSamplesWritten() / _this->samplerate; | ||||
|             time_t diff = seconds; | ||||
|             tm* dtm = gmtime(&diff); | ||||
|             ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Recording %02d:%02d:%02d", dtm->tm_hour, dtm->tm_min, dtm->tm_sec); | ||||
|         } | ||||
|         if (!folderSelect.pathIsValid()) { style::endDisabled(); } | ||||
|     } | ||||
|  | ||||
|     void audioMenu(float menuColumnWidth) { | ||||
|         ImGui::PushItemWidth(menuColumnWidth); | ||||
|  | ||||
|         if (streamNames.size() == 0) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (recording) { style::beginDisabled(); } | ||||
|         if (ImGui::Combo(CONCAT("##_recorder_strm_", name), &streamId, streamNamesTxt.c_str())) { | ||||
|             selectStream(streamNames[streamId]); | ||||
|             config.acquire(); | ||||
|             config.conf[name]["audioStream"] = streamNames[streamId]; | ||||
|             config.release(true); | ||||
|         } | ||||
|         if (recording) { style::endDisabled(); } | ||||
|  | ||||
|         double frameTime = 1.0 / ImGui::GetIO().Framerate; | ||||
|         lvlL = std::clamp<float>(lvlL - (frameTime * 50.0), -90.0f, 10.0f); | ||||
|         lvlR = std::clamp<float>(lvlR - (frameTime * 50.0), -90.0f, 10.0f); | ||||
|  | ||||
|     void updateAudioMeter(dsp::stereo_t& lvl) { | ||||
|         // Note: Yes, using the natural log is on purpose, it just gives a more beautiful result. | ||||
|         dsp::stereo_t rawLvl = meter.getLevel(); | ||||
|         meter.resetLevel(); | ||||
|         double frameTime = 1.0 / ImGui::GetIO().Framerate; | ||||
|         lvl.l = std::clamp<float>(lvl.l - (frameTime * 50.0), -90.0f, 10.0f); | ||||
|         lvl.r = std::clamp<float>(lvl.r - (frameTime * 50.0), -90.0f, 10.0f); | ||||
|         // TODO: FINISH METER | ||||
|         dsp::stereo_t rawLvl = {0.0f,0.0f};//meter.getLevel(); | ||||
|         //meter.resetLevel(); | ||||
|         dsp::stereo_t dbLvl = { 10.0f * logf(rawLvl.l), 10.0f * logf(rawLvl.r) }; | ||||
|         if (dbLvl.l > lvlL) { lvlL = dbLvl.l; } | ||||
|         if (dbLvl.r > lvlR) { lvlR = dbLvl.r; } | ||||
|         ImGui::VolumeMeter(lvlL, lvlL, -60, 10); | ||||
|         ImGui::VolumeMeter(lvlR, lvlR, -60, 10); | ||||
|  | ||||
|         if (ImGui::SliderFloat(CONCAT("##_recorder_vol_", name), &audioVolume, 0, 1, "")) { | ||||
|             vol.setVolume(audioVolume); | ||||
|             config.acquire(); | ||||
|             config.conf[name]["audioVolume"] = audioVolume; | ||||
|             config.release(true); | ||||
|         } | ||||
|         ImGui::PopItemWidth(); | ||||
|  | ||||
|         if (ImGui::Checkbox(CONCAT("Ignore silence##_recorder_ing_silence_", name), &ignoreSilence)) { | ||||
|             config.acquire(); | ||||
|             config.conf[name]["ignoreSilence"] = ignoreSilence; | ||||
|             config.release(true); | ||||
|         } | ||||
|  | ||||
|         if (!folderSelect.pathIsValid() || selectedStreamName == "") { style::beginDisabled(); } | ||||
|         if (!recording) { | ||||
|             if (ImGui::Button(CONCAT("Record##_recorder_rec_", name), ImVec2(menuColumnWidth, 0))) { | ||||
|                 std::lock_guard lck(recMtx); | ||||
|                 startRecording(); | ||||
|             } | ||||
|             ImGui::TextColored(ImGui::GetStyleColorVec4(ImGuiCol_Text), "Idle --:--:--"); | ||||
|         } | ||||
|         else { | ||||
|             if (ImGui::Button(CONCAT("Stop##_recorder_rec_", name), ImVec2(menuColumnWidth, 0))) { | ||||
|                 std::lock_guard lck(recMtx); | ||||
|                 stopRecording(); | ||||
|             } | ||||
|             uint64_t seconds = samplesWritten / (uint64_t)sampleRate; | ||||
|             time_t diff = seconds; | ||||
|             tm* dtm = gmtime(&diff); | ||||
|             ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Recording %02d:%02d:%02d", dtm->tm_hour, dtm->tm_min, dtm->tm_sec); | ||||
|         } | ||||
|         if (!folderSelect.pathIsValid() || selectedStreamName == "") { style::endDisabled(); } | ||||
|         if (dbLvl.l > lvl.l) { lvl.l = dbLvl.l; } | ||||
|         if (dbLvl.r > lvl.r) { lvl.r = dbLvl.r; } | ||||
|     } | ||||
|  | ||||
|     static void _audioHandler(dsp::stereo_t* data, int count, void* ctx) { | ||||
|         RecorderModule* _this = (RecorderModule*)ctx; | ||||
|         if (_this->ignoreSilence && data[0].l == 0.0f && data[0].r == 0.0f) { | ||||
|             return; | ||||
|     // TODO: REPLACE WITH SOMETHING CLEAN | ||||
|     std::string genFileName(std::string prefix, bool isVfo, std::string name = "") { | ||||
|         time_t now = time(0); | ||||
|         tm* ltm = localtime(&now); | ||||
|         char buf[1024]; | ||||
|         double freq = gui::waterfall.getCenterFrequency(); | ||||
|         ; | ||||
|         if (isVfo && gui::waterfall.vfos.find(name) != gui::waterfall.vfos.end()) { | ||||
|             freq += gui::waterfall.vfos[name]->generalOffset; | ||||
|         } | ||||
|         volk_32f_s32f_convert_16i(_this->wavSampleBuf, (float*)data, 32767.0f, count * 2); | ||||
|         _this->audioWriter->writeSamples(_this->wavSampleBuf, count * 2 * sizeof(int16_t)); | ||||
|         _this->samplesWritten += count; | ||||
|         sprintf(buf, "%.0lfHz_%02d-%02d-%02d_%02d-%02d-%02d.wav", freq, ltm->tm_hour, ltm->tm_min, ltm->tm_sec, ltm->tm_mday, ltm->tm_mon + 1, ltm->tm_year + 1900); | ||||
|         return prefix + buf; | ||||
|     } | ||||
|  | ||||
|     static void _basebandHandler(dsp::complex_t* data, int count, void* ctx) { | ||||
|         RecorderModule* _this = (RecorderModule*)ctx; | ||||
|         volk_32f_s32f_convert_16i(_this->wavSampleBuf, (float*)data, 32767.0f, count * 2); | ||||
|         _this->basebandWriter->writeSamples(_this->wavSampleBuf, count * 2 * sizeof(int16_t)); | ||||
|         _this->samplesWritten += count; | ||||
|     } | ||||
|  | ||||
|     static void moduleInterfaceHandler(int code, void* in, void* out, void* ctx) { | ||||
|         RecorderModule* _this = (RecorderModule*)ctx; | ||||
|         std::lock_guard lck(_this->recMtx); | ||||
|         if (code == RECORDER_IFACE_CMD_GET_MODE) { | ||||
|             int* _out = (int*)out; | ||||
|             *_out = _this->recMode; | ||||
|         } | ||||
|         else if (code == RECORDER_IFACE_CMD_SET_MODE) { | ||||
|             if (_this->recording) { return; } | ||||
|             int* _in = (int*)in; | ||||
|             _this->recMode = std::clamp<int>(*_in, 0, 1); | ||||
|         } | ||||
|         else if (code == RECORDER_IFACE_CMD_START) { | ||||
|             if (!_this->recording) { _this->startRecording(); } | ||||
|         } | ||||
|         else if (code == RECORDER_IFACE_CMD_STOP) { | ||||
|             if (_this->recording) { _this->stopRecording(); } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     void startRecording() { | ||||
|         if (recMode == RECORDER_MODE_BASEBAND) { | ||||
|             samplesWritten = 0; | ||||
|             std::string expandedPath = expandString(folderSelect.path + genFileName("/baseband_", false)); | ||||
|             sampleRate = sigpath::iqFrontEnd.getSampleRate(); | ||||
|             basebandWriter = new WavWriter(expandedPath, 16, 2, sigpath::iqFrontEnd.getSampleRate()); | ||||
|             if (basebandWriter->isOpen()) { | ||||
|                 basebandHandler.start(); | ||||
|                 sigpath::iqFrontEnd.bindIQStream(&basebandStream); | ||||
|                 recording = true; | ||||
|                 spdlog::info("Recording to '{0}'", expandedPath); | ||||
|             } | ||||
|             else { | ||||
|                 spdlog::error("Could not create '{0}'", expandedPath); | ||||
|             } | ||||
|         } | ||||
|         else if (recMode == RECORDER_MODE_AUDIO) { | ||||
|             if (selectedStreamName.empty()) { | ||||
|                 spdlog::error("Cannot record with no selected stream"); | ||||
|             } | ||||
|             samplesWritten = 0; | ||||
|             std::string expandedPath = expandString(folderSelect.path + genFileName("/audio_", true, selectedStreamName)); | ||||
|             sampleRate = sigpath::sinkManager.getStreamSampleRate(selectedStreamName); | ||||
|             audioWriter = new WavWriter(expandedPath, 16, 2, sigpath::sinkManager.getStreamSampleRate(selectedStreamName)); | ||||
|             if (audioWriter->isOpen()) { | ||||
|                 recording = true; | ||||
|                 audioHandler.start(); | ||||
|                 audioSplit.bindStream(&audioHandlerStream); | ||||
|                 spdlog::info("Recording to '{0}'", expandedPath); | ||||
|             } | ||||
|             else { | ||||
|                 spdlog::error("Could not create '{0}'", expandedPath); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     void stopRecording() { | ||||
|         if (recMode == 0) { | ||||
|             recording = false; | ||||
|             sigpath::iqFrontEnd.unbindIQStream(&basebandStream); | ||||
|             basebandHandler.stop(); | ||||
|             basebandWriter->close(); | ||||
|             delete basebandWriter; | ||||
|         } | ||||
|         else if (recMode == 1) { | ||||
|             recording = false; | ||||
|             audioSplit.unbindStream(&audioHandlerStream); | ||||
|             audioHandler.stop(); | ||||
|             audioWriter->close(); | ||||
|             delete audioWriter; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static void onStreamRegistered(std::string name, void* ctx) { | ||||
|         RecorderModule* _this = (RecorderModule*)ctx; | ||||
|         _this->refreshStreams(); | ||||
|  | ||||
|         if (_this->streamNames.empty()) { | ||||
|             _this->selectedStreamName = ""; | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (_this->selectedStreamName.empty()) { | ||||
|             _this->selectStream(_this->streamNames[0]); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Reselect stream in UI to make sure the ID is correct | ||||
|         int id = 0; | ||||
|         for (auto& str : _this->streamNames) { | ||||
|             if (str == _this->selectedStreamName) { | ||||
|                 _this->streamId = id; | ||||
|                 break; | ||||
|             } | ||||
|             id++; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static void onStreamUnregister(std::string name, void* ctx) { | ||||
|         RecorderModule* _this = (RecorderModule*)ctx; | ||||
|         if (name != _this->selectedStreamName) { return; } | ||||
|         if (_this->recording) { _this->stopRecording(); } | ||||
|         if (_this->audioInput != NULL) { | ||||
|             _this->vol.setInput(&_this->dummyStream); | ||||
|             sigpath::sinkManager.unbindStream(_this->selectedStreamName, _this->audioInput); | ||||
|             _this->audioInput = NULL; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static void onStreamUnregistered(std::string name, void* ctx) { | ||||
|         RecorderModule* _this = (RecorderModule*)ctx; | ||||
|         _this->refreshStreams(); | ||||
|  | ||||
|         if (_this->streamNames.empty()) { | ||||
|             _this->selectedStreamName = ""; | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // If current stream was deleted, reselect steam completely | ||||
|         if (name == _this->selectedStreamName) { | ||||
|             _this->streamId = std::clamp<int>(_this->streamId, 0, _this->streamNames.size() - 1); | ||||
|             _this->selectStream(_this->streamNames[_this->streamId]); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Reselect stream in UI to make sure the ID is correct | ||||
|         int id = 0; | ||||
|         for (auto& str : _this->streamNames) { | ||||
|             if (str == _this->selectedStreamName) { | ||||
|                 _this->streamId = id; | ||||
|                 break; | ||||
|             } | ||||
|             id++; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     std::string expandString(std::string input) { | ||||
|         input = std::regex_replace(input, std::regex("%ROOT%"), root); | ||||
|         return std::regex_replace(input, std::regex("//"), "/"); | ||||
|     } | ||||
|  | ||||
|     static void complexHandler(dsp::complex_t* data, int count, void* ctx) { | ||||
|         monoHandler((float*)data, count, ctx); | ||||
|     } | ||||
|  | ||||
|     static void stereoHandler(dsp::stereo_t* data, int count, void* ctx) { | ||||
|         monoHandler((float*)data, count, ctx); | ||||
|     } | ||||
|  | ||||
|     static void monoHandler(float* data, int count, void* ctx) { | ||||
|         RecorderModule* _this = (RecorderModule*)ctx; | ||||
|         _this->writer.write(data, count); | ||||
|     } | ||||
|  | ||||
|     std::string name; | ||||
|     bool enabled = true; | ||||
|  | ||||
|     int recMode = 1; | ||||
|     bool recording = false; | ||||
|  | ||||
|     float audioVolume = 1.0f; | ||||
|  | ||||
|     double sampleRate = 48000; | ||||
|  | ||||
|     float lvlL = -90.0f; | ||||
|     float lvlR = -90.0f; | ||||
|  | ||||
|     dsp::stream<dsp::stereo_t> dummyStream; | ||||
|  | ||||
|     std::mutex recMtx; | ||||
|  | ||||
|     FolderSelect folderSelect; | ||||
|  | ||||
|     // Audio path | ||||
|     dsp::stream<dsp::stereo_t>* audioInput = NULL; | ||||
|     dsp::audio::Volume vol; | ||||
|     dsp::routing::Splitter<dsp::stereo_t> audioSplit; | ||||
|     dsp::stream<dsp::stereo_t> meterStream; | ||||
|     dsp::bench::PeakLevelMeter<dsp::stereo_t> meter; | ||||
|     dsp::stream<dsp::stereo_t> audioHandlerStream; | ||||
|     dsp::sink::Handler<dsp::stereo_t> audioHandler; | ||||
|     WavWriter* audioWriter; | ||||
|  | ||||
|     std::vector<std::string> streamNames; | ||||
|     std::string streamNamesTxt; | ||||
|     int streamId = 0; | ||||
|     std::string selectedStreamName = ""; | ||||
|     std::string root; | ||||
|  | ||||
|     // Baseband path | ||||
|     dsp::stream<dsp::complex_t> basebandStream; | ||||
|     dsp::sink::Handler<dsp::complex_t> basebandHandler; | ||||
|     WavWriter* basebandWriter; | ||||
|  | ||||
|     uint64_t samplesWritten; | ||||
|     int16_t* wavSampleBuf; | ||||
|  | ||||
|     EventHandler<std::string> streamRegisteredHandler; | ||||
|     EventHandler<std::string> streamUnregisterHandler; | ||||
|     EventHandler<std::string> streamUnregisteredHandler; | ||||
|     OptionList<std::string, wav::Format> formats; | ||||
|     OptionList<int, wav::SampleDepth> sampleDepths; | ||||
|     FolderSelect folderSelect; | ||||
|  | ||||
|     int recMode = RECORDER_MODE_AUDIO; | ||||
|     int formatId; | ||||
|     int sampleDepthId; | ||||
|     bool stereo = true; | ||||
|     std::string selectedStreamName = ""; | ||||
|     float audioVolume = 1.0f; | ||||
|     bool ignoreSilence = false; | ||||
| }; | ||||
|     dsp::stereo_t audioLvl = { -100.0f, -100.0f }; | ||||
|  | ||||
|     bool recording = false; | ||||
|     wav::Writer writer; | ||||
|     std::recursive_mutex recMtx; | ||||
|     dsp::stream<dsp::complex_t>* basebandStream; | ||||
|     dsp::stream<dsp::stereo_t>* stereoStream; | ||||
|     dsp::sink::Handler<dsp::complex_t> basebandSink; | ||||
|     dsp::sink::Handler<dsp::stereo_t> stereoSink; | ||||
|     dsp::sink::Handler<float> monoSink; | ||||
|  | ||||
|     uint64_t samplerate = 48000; | ||||
|  | ||||
| struct RecorderContext_t { | ||||
|     std::string name; | ||||
| }; | ||||
|  | ||||
| MOD_EXPORT void _INIT_() { | ||||
| @@ -559,7 +371,7 @@ MOD_EXPORT void _DELETE_INSTANCE_(ModuleManager::Instance* inst) { | ||||
|     delete (RecorderModule*)inst; | ||||
| } | ||||
|  | ||||
| MOD_EXPORT void _END_(RecorderContext_t* ctx) { | ||||
| MOD_EXPORT void _END_() { | ||||
|     config.disableAutoSave(); | ||||
|     config.save(); | ||||
| } | ||||
| @@ -1,66 +1,74 @@ | ||||
| #pragma once | ||||
| #include <stdint.h> | ||||
| #include <string> | ||||
| #include <fstream> | ||||
| #include <stdint.h> | ||||
| #include <mutex> | ||||
|  | ||||
| #define WAV_SIGNATURE       "RIFF" | ||||
| #define WAV_TYPE            "WAVE" | ||||
| #define WAV_FORMAT_MARK     "fmt " | ||||
| #define WAV_DATA_MARK       "data" | ||||
| #define WAV_SAMPLE_TYPE_PCM 1 | ||||
|  | ||||
| class WavWriter { | ||||
| public: | ||||
|     WavWriter(std::string path, uint16_t bitDepth, uint16_t channelCount, uint32_t sampleRate) { | ||||
|         file = std::ofstream(path.c_str(), std::ios::binary); | ||||
|         memcpy(hdr.signature, WAV_SIGNATURE, 4); | ||||
|         memcpy(hdr.fileType, WAV_TYPE, 4); | ||||
|         memcpy(hdr.formatMarker, WAV_FORMAT_MARK, 4); | ||||
|         memcpy(hdr.dataMarker, WAV_DATA_MARK, 4); | ||||
|         hdr.formatHeaderLength = 16; | ||||
|         hdr.sampleType = WAV_SAMPLE_TYPE_PCM; | ||||
|         hdr.channelCount = channelCount; | ||||
|         hdr.sampleRate = sampleRate; | ||||
|         hdr.bytesPerSecond = (bitDepth / 8) * channelCount * sampleRate; | ||||
|         hdr.bytesPerSample = (bitDepth / 8) * channelCount; | ||||
|         hdr.bitDepth = bitDepth; | ||||
|         file.write((char*)&hdr, sizeof(WavHeader_t)); | ||||
|     } | ||||
|  | ||||
|     bool isOpen() { | ||||
|         return file.is_open(); | ||||
|     } | ||||
|  | ||||
|     void writeSamples(void* data, size_t size) { | ||||
|         file.write((char*)data, size); | ||||
|         bytesWritten += size; | ||||
|     } | ||||
|  | ||||
|     void close() { | ||||
|         hdr.fileSize = bytesWritten + sizeof(WavHeader_t) - 8; | ||||
|         hdr.dataSize = bytesWritten; | ||||
|         file.seekp(0); | ||||
|         file.write((char*)&hdr, sizeof(WavHeader_t)); | ||||
|         file.close(); | ||||
|     } | ||||
|  | ||||
| private: | ||||
|     struct WavHeader_t { | ||||
|         char signature[4];           // "RIFF" | ||||
|         uint32_t fileSize;           // data bytes + sizeof(WavHeader_t) - 8 | ||||
|         char fileType[4];            // "WAVE" | ||||
|         char formatMarker[4];        // "fmt " | ||||
|         uint32_t formatHeaderLength; // Always 16 | ||||
|         uint16_t sampleType;         // PCM (1) | ||||
| namespace wav {     | ||||
|     #pragma pack(push, 1) | ||||
|     struct Header { | ||||
|         char signature[4];              // "RIFF" | ||||
|         uint32_t fileSize;              // data bytes + sizeof(WavHeader_t) - 8 | ||||
|         char fileType[4];               // "WAVE" | ||||
|         char formatMarker[4];           // "fmt " | ||||
|         uint32_t formatHeaderLength;    // Always 16 | ||||
|         uint16_t sampleType;            // PCM (1) | ||||
|         uint16_t channelCount; | ||||
|         uint32_t sampleRate; | ||||
|         uint32_t bytesPerSecond; | ||||
|         uint16_t bytesPerSample; | ||||
|         uint16_t bitDepth; | ||||
|         char dataMarker[4]; // "data" | ||||
|         char dataMarker[4];             // "data" | ||||
|         uint32_t dataSize; | ||||
|     }; | ||||
|     #pragma pack(pop) | ||||
|  | ||||
|     std::ofstream file; | ||||
|     size_t bytesWritten = 0; | ||||
|     WavHeader_t hdr; | ||||
| }; | ||||
|     enum Format { | ||||
|         FORMAT_WAV, | ||||
|         FORMAT_RF64 | ||||
|     }; | ||||
|  | ||||
|     enum SampleDepth { | ||||
|         SAMP_DEPTH_8BIT     = 8, | ||||
|         SAMP_DEPTH_16BIT    = 16, | ||||
|         SAMP_DEPTH_32BIT    = 32 | ||||
|     }; | ||||
|  | ||||
|     class Writer { | ||||
|     public: | ||||
|         Writer(int channels = 2, uint64_t samplerate = 48000, Format format = FORMAT_WAV, SampleDepth depth = SAMP_DEPTH_16BIT); | ||||
|         ~Writer(); | ||||
|  | ||||
|         bool open(std::string path); | ||||
|         bool isOpen(); | ||||
|         void close(); | ||||
|  | ||||
|         void setChannels(int channels); | ||||
|         void setSamplerate(uint64_t samplerate); | ||||
|         void setFormat(Format format); | ||||
|         void setSampleDepth(SampleDepth depth); | ||||
|  | ||||
|         size_t getSamplesWritten() { return samplesWritten; } | ||||
|  | ||||
|         void write(float* samples, int count); | ||||
|  | ||||
|     private: | ||||
|         void finalize(); | ||||
|          | ||||
|         std::recursive_mutex mtx; | ||||
|         std::ofstream file; | ||||
|         Header hdr; | ||||
|         bool _isOpen = false; | ||||
|  | ||||
|         int _channels; | ||||
|         uint64_t _samplerate; | ||||
|         Format _format; | ||||
|         SampleDepth _depth; | ||||
|         size_t bytesPerSamp; | ||||
|  | ||||
|         int8_t* buf8; | ||||
|         int16_t* buf16; | ||||
|         int32_t* buf32; | ||||
|         size_t samplesWritten = 0; | ||||
|     }; | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user