mirror of
				https://github.com/AlexandreRouma/SDRPlusPlus.git
				synced 2025-10-31 08:58:13 +01:00 
			
		
		
		
	| @@ -52,6 +52,10 @@ public: | ||||
|         return keys.size(); | ||||
|     } | ||||
|  | ||||
|     bool empty() { | ||||
|         return keys.empty(); | ||||
|     } | ||||
|  | ||||
|     bool keyExists(K key) { | ||||
|         if (std::find(keys.begin(), keys.end(), key) != keys.end()) { return true; } | ||||
|         return false; | ||||
|   | ||||
| @@ -7,6 +7,7 @@ | ||||
| #include <dsp/sink/handler_sink.h> | ||||
| #include <dsp/routing/splitter.h> | ||||
| #include <dsp/audio/volume.h> | ||||
| #include <dsp/convert/stereo_to_mono.h> | ||||
| #include <thread> | ||||
| #include <ctime> | ||||
| #include <gui/gui.h> | ||||
| @@ -19,125 +20,117 @@ | ||||
| #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"]; | ||||
|         strcpy(nameTemplate, "$t_$f_$h-$m-$s_$d-$M-$y"); | ||||
|  | ||||
|         // Define option lists | ||||
|         containers.define("WAV", wav::FORMAT_WAV); | ||||
|         // containers.define("RF64", wav::FORMAT_RF64); // Disabled for now | ||||
|         sampleTypes.define(wav::SAMP_TYPE_UINT8, "Uint8", wav::SAMP_TYPE_UINT8); | ||||
|         sampleTypes.define(wav::SAMP_TYPE_INT16, "Int16", wav::SAMP_TYPE_INT16); | ||||
|         sampleTypes.define(wav::SAMP_TYPE_INT32, "Int32", wav::SAMP_TYPE_INT32); | ||||
|         sampleTypes.define(wav::SAMP_TYPE_FLOAT32, "Float32", wav::SAMP_TYPE_FLOAT32); | ||||
|  | ||||
|         // Load default config for option lists | ||||
|         containerId = containers.valueId(wav::FORMAT_WAV); | ||||
|         sampleTypeId = sampleTypes.valueId(wav::SAMP_TYPE_INT16); | ||||
|  | ||||
|         // 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("container") && containers.keyExists(config.conf[name]["container"])) { | ||||
|             containerId = containers.keyId(config.conf[name]["container"]); | ||||
|         } | ||||
|  | ||||
|         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); | ||||
|         if (config.conf[name].contains("sampleType") && sampleTypes.keyExists(config.conf[name]["sampleType"])) { | ||||
|             sampleTypeId = sampleTypes.keyId(config.conf[name]["sampleType"]); | ||||
|         } | ||||
|         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"]; | ||||
|         } | ||||
|         if (config.conf[name].contains("nameTemplate")) { | ||||
|             std::string _nameTemplate = config.conf[name]["nameTemplate"]; | ||||
|             if (_nameTemplate.length() > sizeof(nameTemplate)-1) { | ||||
|                 _nameTemplate = _nameTemplate.substr(0, sizeof(nameTemplate)-1); | ||||
|             } | ||||
|             strcpy(nameTemplate, _nameTemplate.c_str()); | ||||
|         } | ||||
|         config.release(); | ||||
|  | ||||
|         // Init audio path | ||||
|         vol.init(&dummyStream, audioVolume, false); | ||||
|         audioSplit.init(&vol.out); | ||||
|         audioSplit.bindStream(&meterStream); | ||||
|         volume.init(NULL, audioVolume, false); | ||||
|         splitter.init(&volume.out); | ||||
|         splitter.bindStream(&meterStream); | ||||
|         meter.init(&meterStream); | ||||
|         audioHandler.init(&audioHandlerStream, _audioHandler, this); | ||||
|         s2m.init(NULL); | ||||
|  | ||||
|         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(&s2m.out, 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); | ||||
|         std::lock_guard<std::recursive_mutex> lck(recMtx); | ||||
|         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(); | ||||
|         gui::menu.removeEntry(name); | ||||
|         stop(); | ||||
|         deselectStream(); | ||||
|         sigpath::sinkManager.onStreamRegistered.unbindHandler(&onStreamRegisteredHandler); | ||||
|         sigpath::sinkManager.onStreamUnregister.unbindHandler(&onStreamUnregisterHandler); | ||||
|         meter.stop(); | ||||
|  | ||||
|         delete[] wavSampleBuf; | ||||
|     } | ||||
|  | ||||
|     void postInit() { | ||||
|         refreshStreams(); | ||||
|         if (selectedStreamName == "") { | ||||
|             selectStream(streamNames[0]); | ||||
|         } | ||||
|         else { | ||||
|             selectStream(selectedStreamName); | ||||
|         // Enumerate streams | ||||
|         audioStreams.clear(); | ||||
|         auto names = sigpath::sinkManager.getStreamNames(); | ||||
|         for (const auto& name : names) { | ||||
|             audioStreams.define(name, name, name); | ||||
|         } | ||||
|  | ||||
|         // Bind stream register/unregister handlers | ||||
|         onStreamRegisteredHandler.ctx = this; | ||||
|         onStreamRegisteredHandler.handler = streamRegisteredHandler; | ||||
|         sigpath::sinkManager.onStreamRegistered.bindHandler(&onStreamRegisteredHandler); | ||||
|         onStreamUnregisterHandler.ctx = this; | ||||
|         onStreamUnregisterHandler.handler = streamUnregisterHandler; | ||||
|         sigpath::sinkManager.onStreamUnregister.bindHandler(&onStreamUnregisterHandler); | ||||
|  | ||||
|         // Select the stream | ||||
|         selectStream(selectedStreamName); | ||||
|     } | ||||
|  | ||||
|     void enable() { | ||||
| @@ -152,70 +145,106 @@ 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) { | ||||
|             if (selectedStreamName.empty()) { return; } | ||||
|             samplerate = sigpath::sinkManager.getStreamSampleRate(selectedStreamName); | ||||
|         } | ||||
|         else { | ||||
|             samplerate = sigpath::iqFrontEnd.getSampleRate(); | ||||
|         } | ||||
|         writer.setFormat(containers[containerId]); | ||||
|         writer.setChannels((recMode == RECORDER_MODE_AUDIO && !stereo) ? 1 : 2); | ||||
|         writer.setSampleType(sampleTypes[sampleTypeId]); | ||||
|         writer.setSamplerate(samplerate); | ||||
|  | ||||
|         // Open file | ||||
|         std::string type = (recMode == RECORDER_MODE_AUDIO) ? "audio" : "baseband"; | ||||
|         std::string vfoName = (recMode == RECORDER_MODE_AUDIO) ? gui::waterfall.selectedVFO : ""; | ||||
|         std::string extension = ".wav"; | ||||
|         std::string expandedPath = expandString(folderSelect.path + "/" + genFileName(nameTemplate, type, vfoName) + extension); | ||||
|         if (!writer.open(expandedPath)) { | ||||
|             spdlog::error("Failed to open file for recording: {0}", expandedPath); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Open audio stream or baseband | ||||
|         if (recMode == RECORDER_MODE_AUDIO) { | ||||
|             // TODO: Select the stereo to mono converter if needed | ||||
|             stereoStream = sigpath::sinkManager.bindStream(selectedStreamName); | ||||
|             if (stereo) { | ||||
|                 stereoSink.setInput(stereoStream); | ||||
|                 stereoSink.start(); | ||||
|             } | ||||
|             else { | ||||
|                 s2m.setInput(stereoStream); | ||||
|                 s2m.start(); | ||||
|                 monoSink.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) { | ||||
|             // NOTE: Has to be done before the unbind since the stream is deleted... | ||||
|             monoSink.stop(); | ||||
|             stereoSink.stop(); | ||||
|             s2m.stop(); | ||||
|             sigpath::sinkManager.unbindStream(selectedStreamName, 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,116 +257,232 @@ private: | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Mode specific menu | ||||
|         if (_this->recMode == RECORDER_MODE_AUDIO) { | ||||
|             _this->audioMenu(menuColumnWidth); | ||||
|         ImGui::LeftLabel("Name template"); | ||||
|         ImGui::FillWidth(); | ||||
|         if (ImGui::InputText(CONCAT("##_recorder_name_template_", _this->name), _this->nameTemplate, 1023)) { | ||||
|             config.acquire(); | ||||
|             config.conf[_this->name]["nameTemplate"] = _this->nameTemplate; | ||||
|             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("Container"); | ||||
|         ImGui::FillWidth(); | ||||
|         if (ImGui::Combo(CONCAT("##_recorder_container_", _this->name), &_this->containerId, _this->containers.txt)) { | ||||
|             config.acquire(); | ||||
|             config.conf[_this->name]["container"] = _this->containers.key(_this->containerId); | ||||
|             config.release(true); | ||||
|         } | ||||
|  | ||||
|         ImGui::LeftLabel("Sample type"); | ||||
|         ImGui::FillWidth(); | ||||
|         if (ImGui::Combo(CONCAT("##_recorder_st_", _this->name), &_this->sampleTypeId, _this->sampleTypes.txt)) { | ||||
|             config.acquire(); | ||||
|             config.conf[_this->name]["sampleType"] = _this->sampleTypes.key(_this->sampleTypeId); | ||||
|             config.release(true); | ||||
|         } | ||||
|  | ||||
|         // Show additional audio options | ||||
|         if (_this->recMode == RECORDER_MODE_AUDIO) { | ||||
|             ImGui::LeftLabel("Stream"); | ||||
|             ImGui::FillWidth(); | ||||
|             if (ImGui::Combo(CONCAT("##_recorder_stream_", _this->name), &_this->streamId, _this->audioStreams.txt)) { | ||||
|                 _this->selectStream(_this->audioStreams.value(_this->streamId)); | ||||
|                 config.acquire(); | ||||
|                 config.conf[_this->name]["audioStream"] = _this->audioStreams.key(_this->streamId); | ||||
|                 config.release(true); | ||||
|             } | ||||
|  | ||||
|             _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, "")) { | ||||
|                 _this->volume.setVolume(_this->audioVolume); | ||||
|                 config.acquire(); | ||||
|                 config.conf[_this->name]["audioVolume"] = _this->audioVolume; | ||||
|                 config.release(true); | ||||
|             } | ||||
|  | ||||
|             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); | ||||
|     void selectStream(std::string name) { | ||||
|         std::lock_guard<std::recursive_mutex> lck(recMtx); | ||||
|         deselectStream(); | ||||
|  | ||||
|         if (streamNames.size() == 0) { | ||||
|         if (audioStreams.empty()) { | ||||
|             selectedStreamName.clear(); | ||||
|             return; | ||||
|         } | ||||
|         else if (!audioStreams.keyExists(name)) { | ||||
|             selectStream(audioStreams.key(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); | ||||
|         audioStream = sigpath::sinkManager.bindStream(name); | ||||
|         if (!audioStream) { return; } | ||||
|         selectedStreamName = name; | ||||
|         streamId = audioStreams.keyId(name); | ||||
|         volume.setInput(audioStream); | ||||
|         startAudioPath(); | ||||
|     } | ||||
|  | ||||
|     void deselectStream() { | ||||
|         std::lock_guard<std::recursive_mutex> lck(recMtx); | ||||
|         if (selectedStreamName.empty() || !audioStream) { | ||||
|             selectedStreamName.clear(); | ||||
|             return; | ||||
|         } | ||||
|         if (recording) { style::endDisabled(); } | ||||
|         if (recording && recMode == RECORDER_MODE_AUDIO) { stop(); } | ||||
|         stopAudioPath(); | ||||
|         sigpath::sinkManager.unbindStream(selectedStreamName, audioStream); | ||||
|         selectedStreamName.clear(); | ||||
|         audioStream = NULL; | ||||
|     } | ||||
|  | ||||
|         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 startAudioPath() { | ||||
|         volume.start(); | ||||
|         splitter.start(); | ||||
|         meter.start(); | ||||
|     } | ||||
|  | ||||
|     void stopAudioPath() { | ||||
|         volume.stop(); | ||||
|         splitter.stop(); | ||||
|         meter.stop(); | ||||
|     } | ||||
|  | ||||
|     static void streamRegisteredHandler(std::string name, void* ctx) { | ||||
|         RecorderModule* _this = (RecorderModule*)ctx; | ||||
|  | ||||
|         // Add new stream to the list | ||||
|         _this->audioStreams.define(name, name, name); | ||||
|  | ||||
|         // If no stream is selected, select new stream. If not, update the menu ID.  | ||||
|         if (_this->selectedStreamName.empty()) { | ||||
|             _this->selectStream(name); | ||||
|         } | ||||
|         else { | ||||
|             _this->streamId = _this->audioStreams.keyId(_this->selectedStreamName); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static void streamUnregisterHandler(std::string name, void* ctx) { | ||||
|         RecorderModule* _this = (RecorderModule*)ctx; | ||||
|  | ||||
|         // Remove stream from list | ||||
|         _this->audioStreams.undefineKey(name); | ||||
|  | ||||
|         // If the stream is in used, deselect it and reselect default. Otherwise, update ID. | ||||
|         if (_this->selectedStreamName == name) { | ||||
|             _this->selectStream(""); | ||||
|         } | ||||
|         else { | ||||
|             _this->streamId = _this->audioStreams.keyId(_this->selectedStreamName); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     void updateAudioMeter(dsp::stereo_t& lvl) { | ||||
|         // Note: Yes, using the natural log is on purpose, it just gives a more beautiful result. | ||||
|         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); | ||||
|         dsp::stereo_t rawLvl = 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; | ||||
|     std::string genFileName(std::string templ, std::string type, std::string name) { | ||||
|         // Get data | ||||
|         time_t now = time(0); | ||||
|         tm* ltm = localtime(&now); | ||||
|         char buf[1024]; | ||||
|         double freq = gui::waterfall.getCenterFrequency(); | ||||
|         if (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; | ||||
|  | ||||
|         // Format to string | ||||
|         char freqStr[128]; | ||||
|         char hourStr[128]; | ||||
|         char minStr[128]; | ||||
|         char secStr[128]; | ||||
|         char dayStr[128]; | ||||
|         char monStr[128]; | ||||
|         char yearStr[128]; | ||||
|         sprintf(freqStr, "%.0lfHz", freq); | ||||
|         sprintf(hourStr, "%02d", ltm->tm_hour); | ||||
|         sprintf(minStr, "%02d", ltm->tm_min); | ||||
|         sprintf(secStr, "%02d", ltm->tm_sec); | ||||
|         sprintf(dayStr, "%02d", ltm->tm_mday); | ||||
|         sprintf(monStr, "%02d", ltm->tm_mon + 1); | ||||
|         sprintf(yearStr, "%02d", ltm->tm_year + 1900); | ||||
|  | ||||
|         // Replace in template | ||||
|         templ = std::regex_replace(templ, std::regex("\\$t"), type); | ||||
|         templ = std::regex_replace(templ, std::regex("\\$f"), freqStr); | ||||
|         templ = std::regex_replace(templ, std::regex("\\$h"), hourStr); | ||||
|         templ = std::regex_replace(templ, std::regex("\\$m"), minStr); | ||||
|         templ = std::regex_replace(templ, std::regex("\\$s"), secStr); | ||||
|         templ = std::regex_replace(templ, std::regex("\\$d"), dayStr); | ||||
|         templ = std::regex_replace(templ, std::regex("\\$M"), monStr); | ||||
|         templ = std::regex_replace(templ, std::regex("\\$y"), yearStr); | ||||
|         return templ; | ||||
|     } | ||||
|  | ||||
|     static void _basebandHandler(dsp::complex_t* data, int count, void* ctx) { | ||||
|     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; | ||||
|         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; | ||||
|         _this->writer.write(data, count); | ||||
|     } | ||||
|  | ||||
|     static void moduleInterfaceHandler(int code, void* in, void* out, void* ctx) { | ||||
| @@ -353,187 +498,54 @@ private: | ||||
|             _this->recMode = std::clamp<int>(*_in, 0, 1); | ||||
|         } | ||||
|         else if (code == RECORDER_IFACE_CMD_START) { | ||||
|             if (!_this->recording) { _this->startRecording(); } | ||||
|             if (!_this->recording) { _this->start(); } | ||||
|         } | ||||
|         else if (code == RECORDER_IFACE_CMD_STOP) { | ||||
|             if (_this->recording) { _this->stopRecording(); } | ||||
|             if (_this->recording) { _this->stop(); } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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("//"), "/"); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     std::string name; | ||||
|     bool enabled = true; | ||||
|     std::string root; | ||||
|     char nameTemplate[1024]; | ||||
|  | ||||
|     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; | ||||
|  | ||||
|     OptionList<std::string, wav::Format> containers; | ||||
|     OptionList<int, wav::SampleType> sampleTypes; | ||||
|     FolderSelect folderSelect; | ||||
|  | ||||
|     // Audio path | ||||
|     dsp::stream<dsp::stereo_t>* audioInput = NULL; | ||||
|     dsp::audio::Volume vol; | ||||
|     dsp::routing::Splitter<dsp::stereo_t> audioSplit; | ||||
|     int recMode = RECORDER_MODE_AUDIO; | ||||
|     int containerId; | ||||
|     int sampleTypeId; | ||||
|     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; | ||||
|  | ||||
|     OptionList<std::string, std::string> audioStreams; | ||||
|     int streamId = 0; | ||||
|     dsp::stream<dsp::stereo_t>* audioStream = NULL; | ||||
|     dsp::audio::Volume volume; | ||||
|     dsp::routing::Splitter<dsp::stereo_t> splitter; | ||||
|     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; | ||||
|     dsp::convert::StereoToMono s2m; | ||||
|  | ||||
|     std::vector<std::string> streamNames; | ||||
|     std::string streamNamesTxt; | ||||
|     int streamId = 0; | ||||
|     std::string selectedStreamName = ""; | ||||
|     std::string root; | ||||
|     uint64_t samplerate = 48000; | ||||
|  | ||||
|     // Baseband path | ||||
|     dsp::stream<dsp::complex_t> basebandStream; | ||||
|     dsp::sink::Handler<dsp::complex_t> basebandHandler; | ||||
|     WavWriter* basebandWriter; | ||||
|     EventHandler<std::string> onStreamRegisteredHandler; | ||||
|     EventHandler<std::string> onStreamUnregisterHandler; | ||||
|  | ||||
|     uint64_t samplesWritten; | ||||
|     int16_t* wavSampleBuf; | ||||
|  | ||||
|     EventHandler<std::string> streamRegisteredHandler; | ||||
|     EventHandler<std::string> streamUnregisterHandler; | ||||
|     EventHandler<std::string> streamUnregisteredHandler; | ||||
|  | ||||
|     bool ignoreSilence = false; | ||||
| }; | ||||
|  | ||||
| struct RecorderContext_t { | ||||
|     std::string name; | ||||
| }; | ||||
|  | ||||
| MOD_EXPORT void _INIT_() { | ||||
| @@ -559,7 +571,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(); | ||||
| } | ||||
							
								
								
									
										134
									
								
								misc_modules/recorder/src/riff.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								misc_modules/recorder/src/riff.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,134 @@ | ||||
| #include "riff.h" | ||||
| #include <string.h> | ||||
| #include <stdexcept> | ||||
|  | ||||
| namespace riff { | ||||
|     const char* RIFF_SIGNATURE      = "RIFF"; | ||||
|     const char* LIST_SIGNATURE      = "LIST"; | ||||
|     const size_t RIFF_LABEL_SIZE    = 4; | ||||
|  | ||||
|     bool Writer::open(std::string path, const char form[4]) { | ||||
|         std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|  | ||||
|         // Open file | ||||
|         file = std::ofstream(path, std::ios::out | std::ios::binary); | ||||
|         if (!file.is_open()) { return false; } | ||||
|  | ||||
|         // Begin RIFF chunk | ||||
|         beginRIFF(form); | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     bool Writer::isOpen() { | ||||
|         std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|         return file.is_open(); | ||||
|     } | ||||
|  | ||||
|     void Writer::close() { | ||||
|         std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|  | ||||
|         if (!isOpen()) { return; } | ||||
|  | ||||
|         // Finalize RIFF chunk | ||||
|         endRIFF(); | ||||
|  | ||||
|         // Close file | ||||
|         file.close(); | ||||
|     } | ||||
|  | ||||
|     void Writer::beginList(const char id[4]) { | ||||
|         std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|  | ||||
|         // Create chunk with the LIST ID and write id | ||||
|         beginChunk(LIST_SIGNATURE); | ||||
|         write((uint8_t*)id, RIFF_LABEL_SIZE); | ||||
|     } | ||||
|  | ||||
|     void Writer::endList() { | ||||
|         std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|  | ||||
|         if (chunks.empty()) { | ||||
|             throw std::runtime_error("No chunk to end"); | ||||
|         } | ||||
|         if (memcmp(chunks.top().hdr.id, LIST_SIGNATURE, RIFF_LABEL_SIZE)) { | ||||
|             throw std::runtime_error("Top chunk not LIST chunk"); | ||||
|         } | ||||
|  | ||||
|         endChunk(); | ||||
|     } | ||||
|  | ||||
|     void Writer::beginChunk(const char id[4]) { | ||||
|         std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|  | ||||
|         // Create and write header | ||||
|         ChunkDesc desc; | ||||
|         desc.pos = file.tellp(); | ||||
|         memcpy(desc.hdr.id, id, sizeof(desc.hdr.id)); | ||||
|         desc.hdr.size = 0; | ||||
|         file.write((char*)&desc.hdr, sizeof(ChunkHeader)); | ||||
|  | ||||
|         // Save descriptor | ||||
|         chunks.push(desc); | ||||
|     } | ||||
|  | ||||
|     void Writer::endChunk() { | ||||
|         std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|  | ||||
|         if (chunks.empty()) { | ||||
|             throw std::runtime_error("No chunk to end"); | ||||
|         } | ||||
|  | ||||
|         // Get descriptor | ||||
|         ChunkDesc desc = chunks.top(); | ||||
|         chunks.pop(); | ||||
|  | ||||
|         // Write size | ||||
|         auto pos = file.tellp(); | ||||
|         auto npos = desc.pos; | ||||
|         npos += 4; | ||||
|         file.seekp(npos); | ||||
|         file.write((char*)&desc.hdr.size, sizeof(desc.hdr.size)); | ||||
|         file.seekp(pos); | ||||
|  | ||||
|         // If parent chunk, increment its size | ||||
|         if (!chunks.empty()) { | ||||
|             chunks.top().hdr.size += desc.hdr.size; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     void Writer::write(const uint8_t* data, size_t len) { | ||||
|         std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|  | ||||
|         if (chunks.empty()) { | ||||
|             throw std::runtime_error("No chunk to write into"); | ||||
|         } | ||||
|         file.write((char*)data, len); | ||||
|         chunks.top().hdr.size += len; | ||||
|     } | ||||
|  | ||||
|     void Writer::beginRIFF(const char form[4]) { | ||||
|         std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|  | ||||
|         if (!chunks.empty()) { | ||||
|             throw std::runtime_error("Can't create RIFF chunk on an existing RIFF file"); | ||||
|         } | ||||
|  | ||||
|         // Create chunk with RIFF ID and write form | ||||
|         beginChunk(RIFF_SIGNATURE); | ||||
|         write((uint8_t*)form, RIFF_LABEL_SIZE); | ||||
|     } | ||||
|  | ||||
|     void Writer::endRIFF() { | ||||
|         std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|  | ||||
|         if (chunks.empty()) { | ||||
|             throw std::runtime_error("No chunk to end"); | ||||
|         } | ||||
|         if (memcmp(chunks.top().hdr.id, RIFF_SIGNATURE, RIFF_LABEL_SIZE)) { | ||||
|             throw std::runtime_error("Top chunk not RIFF chunk"); | ||||
|         } | ||||
|  | ||||
|         endChunk(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										43
									
								
								misc_modules/recorder/src/riff.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								misc_modules/recorder/src/riff.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| #pragma once | ||||
| #include <mutex> | ||||
| #include <fstream> | ||||
| #include <string> | ||||
| #include <stack> | ||||
| #include <stdint.h> | ||||
|  | ||||
| namespace riff { | ||||
| #pragma pack(push, 1) | ||||
|     struct ChunkHeader { | ||||
|         char id[4]; | ||||
|         uint32_t size; | ||||
|     }; | ||||
| #pragma pack(pop) | ||||
|  | ||||
|     struct ChunkDesc { | ||||
|         ChunkHeader hdr; | ||||
|         std::streampos pos; | ||||
|     }; | ||||
|  | ||||
|     class Writer { | ||||
|     public: | ||||
|         bool open(std::string path, const char form[4]); | ||||
|         bool isOpen(); | ||||
|         void close(); | ||||
|  | ||||
|         void beginList(const char id[4]); | ||||
|         void endList(); | ||||
|  | ||||
|         void beginChunk(const char id[4]); | ||||
|         void endChunk(); | ||||
|  | ||||
|         void write(const uint8_t* data, size_t len); | ||||
|  | ||||
|     private: | ||||
|         void beginRIFF(const char form[4]); | ||||
|         void endRIFF(); | ||||
|  | ||||
|         std::recursive_mutex mtx; | ||||
|         std::ofstream file; | ||||
|         std::stack<ChunkDesc> chunks; | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										182
									
								
								misc_modules/recorder/src/wav.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								misc_modules/recorder/src/wav.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,182 @@ | ||||
| #include "wav.h" | ||||
| #include <volk/volk.h> | ||||
| #include <stdexcept> | ||||
| #include <dsp/buffer/buffer.h> | ||||
| #include <dsp/stream.h> | ||||
| #include <map> | ||||
|  | ||||
| namespace wav { | ||||
|     const char* WAVE_FILE_TYPE          = "WAVE"; | ||||
|     const char* FORMAT_MARKER           = "fmt "; | ||||
|     const char* DATA_MARKER             = "data"; | ||||
|     const uint32_t FORMAT_HEADER_LEN    = 16; | ||||
|     const uint16_t SAMPLE_TYPE_PCM      = 1; | ||||
|  | ||||
|     std::map<SampleType, int> SAMP_BITS = { | ||||
|         { SAMP_TYPE_UINT8, 8 }, | ||||
|         { SAMP_TYPE_INT16, 16 }, | ||||
|         { SAMP_TYPE_INT32, 32 }, | ||||
|         { SAMP_TYPE_FLOAT32, 32 } | ||||
|     }; | ||||
|      | ||||
|     Writer::Writer(int channels, uint64_t samplerate, Format format, SampleType type) { | ||||
|         // Validate channels and samplerate | ||||
|         if (channels < 1) { throw std::runtime_error("Channel count must be greater or equal to 1"); } | ||||
|         if (!samplerate) { throw std::runtime_error("Samplerate must be non-zero"); } | ||||
|          | ||||
|         // Initialize variables | ||||
|         _channels = channels; | ||||
|         _samplerate = samplerate; | ||||
|         _format = format; | ||||
|         _type = type; | ||||
|     } | ||||
|  | ||||
|     Writer::~Writer() { close(); } | ||||
|  | ||||
|     bool Writer::open(std::string path) { | ||||
|         std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|         // Close previous file | ||||
|         if (rw.isOpen()) { close(); } | ||||
|  | ||||
|         // Reset work values | ||||
|         samplesWritten = 0; | ||||
|  | ||||
|         // Fill header | ||||
|         bytesPerSamp = (SAMP_BITS[_type] / 8) * _channels; | ||||
|         hdr.codec = (_type == SAMP_TYPE_FLOAT32) ? CODEC_FLOAT : CODEC_PCM; | ||||
|         hdr.channelCount = _channels; | ||||
|         hdr.sampleRate = _samplerate; | ||||
|         hdr.bitDepth = SAMP_BITS[_type]; | ||||
|         hdr.bytesPerSample = bytesPerSamp; | ||||
|         hdr.bytesPerSecond = bytesPerSamp * _samplerate; | ||||
|  | ||||
|         // Precompute sizes and allocate buffers | ||||
|         switch (_type) { | ||||
|         case SAMP_TYPE_UINT8: | ||||
|             bufU8 = dsp::buffer::alloc<uint8_t>(STREAM_BUFFER_SIZE * _channels); | ||||
|             break; | ||||
|         case SAMP_TYPE_INT16: | ||||
|             bufI16 = dsp::buffer::alloc<int16_t>(STREAM_BUFFER_SIZE * _channels); | ||||
|             break; | ||||
|         case SAMP_TYPE_INT32: | ||||
|             bufI32 = dsp::buffer::alloc<int32_t>(STREAM_BUFFER_SIZE * _channels); | ||||
|             break; | ||||
|         case SAMP_TYPE_FLOAT32: | ||||
|             break; | ||||
|         default: | ||||
|             return false; | ||||
|             break; | ||||
|         } | ||||
|  | ||||
|         // Open file | ||||
|         if (!rw.open(path, WAVE_FILE_TYPE)) { return false; } | ||||
|  | ||||
|         // Write format chunk | ||||
|         rw.beginChunk(FORMAT_MARKER); | ||||
|         rw.write((uint8_t*)&hdr, sizeof(FormatHeader)); | ||||
|         rw.endChunk(); | ||||
|  | ||||
|         // Begin data chunk | ||||
|         rw.beginChunk(DATA_MARKER); | ||||
|          | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     bool Writer::isOpen() { | ||||
|         std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|         return rw.isOpen(); | ||||
|     } | ||||
|  | ||||
|     void Writer::close() { | ||||
|         std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|         // Do nothing if the file is not open | ||||
|         if (!rw.isOpen()) { return; } | ||||
|  | ||||
|         // Finish data chunk | ||||
|         rw.endChunk(); | ||||
|  | ||||
|         // Close the file | ||||
|         rw.close(); | ||||
|  | ||||
|         // Free buffers | ||||
|         if (bufU8) { | ||||
|             dsp::buffer::free(bufU8); | ||||
|             bufU8 = NULL; | ||||
|         } | ||||
|         if (bufI16) { | ||||
|             dsp::buffer::free(bufI16); | ||||
|             bufI16 = NULL; | ||||
|         } | ||||
|         if (bufI32) { | ||||
|             dsp::buffer::free(bufI32); | ||||
|             bufI32 = NULL; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     void Writer::setChannels(int channels) { | ||||
|         std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|         // Do not allow settings to change while open | ||||
|         if (rw.isOpen()) { throw std::runtime_error("Cannot change parameters while file is open"); } | ||||
|  | ||||
|         // Validate channel count | ||||
|         if (channels < 1) { throw std::runtime_error("Channel count must be greater or equal to 1"); } | ||||
|         _channels = channels; | ||||
|     } | ||||
|  | ||||
|     void Writer::setSamplerate(uint64_t samplerate) { | ||||
|         std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|         // Do not allow settings to change while open | ||||
|         if (rw.isOpen()) { throw std::runtime_error("Cannot change parameters while file is open"); } | ||||
|  | ||||
|         // Validate samplerate | ||||
|         if (!samplerate) { throw std::runtime_error("Samplerate must be non-zero"); } | ||||
|     } | ||||
|  | ||||
|     void Writer::setFormat(Format format) { | ||||
|         std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|         // Do not allow settings to change while open | ||||
|         if (rw.isOpen()) { throw std::runtime_error("Cannot change parameters while file is open"); } | ||||
|         _format = format; | ||||
|     } | ||||
|  | ||||
|     void Writer::setSampleType(SampleType type) { | ||||
|         std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|         // Do not allow settings to change while open | ||||
|         if (rw.isOpen()) { throw std::runtime_error("Cannot change parameters while file is open"); } | ||||
|         _type = type; | ||||
|     } | ||||
|  | ||||
|     void Writer::write(float* samples, int count) { | ||||
|         std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|         if (!rw.isOpen()) { return; } | ||||
|          | ||||
|         // Select different writer function depending on the chose depth | ||||
|         int tcount = count * _channels; | ||||
|         int tbytes = count * bytesPerSamp; | ||||
|         switch (_type) { | ||||
|         case SAMP_TYPE_UINT8: | ||||
|             // Volk doesn't support unsigned ints yet :/ | ||||
|             for (int i = 0; i < tcount; i++) { | ||||
|                 bufU8[i] = (samples[i] * 127.0f) + 128.0f; | ||||
|             } | ||||
|             rw.write(bufU8, tbytes); | ||||
|             break; | ||||
|         case SAMP_TYPE_INT16: | ||||
|             volk_32f_s32f_convert_16i(bufI16, samples, 32767.0f, tcount); | ||||
|             rw.write((uint8_t*)bufI16, tbytes); | ||||
|             break; | ||||
|         case SAMP_TYPE_INT32: | ||||
|             volk_32f_s32f_convert_32i(bufI32, samples, 2147483647.0f, tcount); | ||||
|             rw.write((uint8_t*)bufI32, tbytes); | ||||
|             break; | ||||
|         case SAMP_TYPE_FLOAT32: | ||||
|             rw.write((uint8_t*)samples, tbytes); | ||||
|             break; | ||||
|         default: | ||||
|             break; | ||||
|         } | ||||
|  | ||||
|         // Increment sample counter | ||||
|         samplesWritten += count; | ||||
|     } | ||||
| } | ||||
| @@ -1,66 +1,71 @@ | ||||
| #pragma once | ||||
| #include <stdint.h> | ||||
| #include <string> | ||||
| #include <fstream> | ||||
| #include <stdint.h> | ||||
| #include <mutex> | ||||
| #include "riff.h" | ||||
|  | ||||
| #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 FormatHeader { | ||||
|         uint16_t codec; | ||||
|         uint16_t channelCount; | ||||
|         uint32_t sampleRate; | ||||
|         uint32_t bytesPerSecond; | ||||
|         uint16_t bytesPerSample; | ||||
|         uint16_t bitDepth; | ||||
|         char dataMarker[4]; // "data" | ||||
|         uint32_t dataSize; | ||||
|     }; | ||||
|     #pragma pack(pop) | ||||
|  | ||||
|     enum Format { | ||||
|         FORMAT_WAV, | ||||
|         FORMAT_RF64 | ||||
|     }; | ||||
|  | ||||
|     std::ofstream file; | ||||
|     size_t bytesWritten = 0; | ||||
|     WavHeader_t hdr; | ||||
| }; | ||||
|     enum SampleType { | ||||
|         SAMP_TYPE_UINT8, | ||||
|         SAMP_TYPE_INT16, | ||||
|         SAMP_TYPE_INT32, | ||||
|         SAMP_TYPE_FLOAT32 | ||||
|     }; | ||||
|  | ||||
|     enum Codec { | ||||
|         CODEC_PCM   = 1, | ||||
|         CODEC_FLOAT = 3 | ||||
|     }; | ||||
|  | ||||
|     class Writer { | ||||
|     public: | ||||
|         Writer(int channels = 2, uint64_t samplerate = 48000, Format format = FORMAT_WAV, SampleType type = SAMP_TYPE_INT16); | ||||
|         ~Writer(); | ||||
|  | ||||
|         bool open(std::string path); | ||||
|         bool isOpen(); | ||||
|         void close(); | ||||
|  | ||||
|         void setChannels(int channels); | ||||
|         void setSamplerate(uint64_t samplerate); | ||||
|         void setFormat(Format format); | ||||
|         void setSampleType(SampleType type); | ||||
|  | ||||
|         size_t getSamplesWritten() { return samplesWritten; } | ||||
|  | ||||
|         void write(float* samples, int count); | ||||
|  | ||||
|     private: | ||||
|         std::recursive_mutex mtx; | ||||
|         FormatHeader hdr; | ||||
|         riff::Writer rw; | ||||
|  | ||||
|         int _channels; | ||||
|         uint64_t _samplerate; | ||||
|         Format _format; | ||||
|         SampleType _type; | ||||
|         size_t bytesPerSamp; | ||||
|  | ||||
|         uint8_t* bufU8 = NULL; | ||||
|         int16_t* bufI16 = NULL; | ||||
|         int32_t* bufI32 = NULL; | ||||
|         size_t samplesWritten = 0; | ||||
|     }; | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user