From 0c8f6ab836902c1ee70342dac2a76c23e65d06be Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Sun, 25 Sep 2022 01:01:07 +0200 Subject: [PATCH 01/12] new recorder stuff --- core/src/dsp/multirate/rational_resampler.h | 2 +- misc_modules/recorder/src/main.cpp | 628 +++++++------------- misc_modules/recorder/src/wav.h | 118 ++-- 3 files changed, 284 insertions(+), 464 deletions(-) diff --git a/core/src/dsp/multirate/rational_resampler.h b/core/src/dsp/multirate/rational_resampler.h index f23fb9c9..c8235652 100644 --- a/core/src/dsp/multirate/rational_resampler.h +++ b/core/src/dsp/multirate/rational_resampler.h @@ -171,4 +171,4 @@ namespace dsp::multirate { double _outSamplerate; Mode mode; }; -} +} \ No newline at end of file diff --git a/misc_modules/recorder/src/main.cpp b/misc_modules/recorder/src/main.cpp index 50201c77..4ed8e0b5 100644 --- a/misc_modules/recorder/src/main.cpp +++ b/misc_modules/recorder/src/main.cpp @@ -19,125 +19,77 @@ #include #include #include +#include +#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 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(); + basebandSink.setInput(basebandStream); + basebandSink.start(); + sigpath::iqFrontEnd.bindIQStream(basebandStream); + } + + recording = true; + } + + void stop() { + std::lock_guard 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 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(lvlL - (frameTime * 50.0), -90.0f, 10.0f); - lvlR = std::clamp(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(lvl.l - (frameTime * 50.0), -90.0f, 10.0f); + lvl.r = std::clamp(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(*_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(_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 dummyStream; - - std::mutex recMtx; - - FolderSelect folderSelect; - - // Audio path - dsp::stream* audioInput = NULL; - dsp::audio::Volume vol; - dsp::routing::Splitter audioSplit; - dsp::stream meterStream; - dsp::bench::PeakLevelMeter meter; - dsp::stream audioHandlerStream; - dsp::sink::Handler audioHandler; - WavWriter* audioWriter; - - std::vector streamNames; - std::string streamNamesTxt; - int streamId = 0; - std::string selectedStreamName = ""; std::string root; - // Baseband path - dsp::stream basebandStream; - dsp::sink::Handler basebandHandler; - WavWriter* basebandWriter; - - uint64_t samplesWritten; - int16_t* wavSampleBuf; - - EventHandler streamRegisteredHandler; - EventHandler streamUnregisterHandler; - EventHandler streamUnregisteredHandler; + OptionList formats; + OptionList 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* basebandStream; + dsp::stream* stereoStream; + dsp::sink::Handler basebandSink; + dsp::sink::Handler stereoSink; + dsp::sink::Handler 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(); } \ No newline at end of file diff --git a/misc_modules/recorder/src/wav.h b/misc_modules/recorder/src/wav.h index f17ed312..7823b5d5 100644 --- a/misc_modules/recorder/src/wav.h +++ b/misc_modules/recorder/src/wav.h @@ -1,66 +1,74 @@ #pragma once -#include +#include #include +#include +#include -#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; -}; \ No newline at end of file + 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; + }; +} From 5b8b344142cf3663b6e03c11fdd9efb313d980f9 Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Sun, 25 Sep 2022 01:01:43 +0200 Subject: [PATCH 02/12] added missing files --- misc_modules/recorder/src/wav.cpp | 185 ++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 misc_modules/recorder/src/wav.cpp diff --git a/misc_modules/recorder/src/wav.cpp b/misc_modules/recorder/src/wav.cpp new file mode 100644 index 00000000..2cd47243 --- /dev/null +++ b/misc_modules/recorder/src/wav.cpp @@ -0,0 +1,185 @@ +#include "wav.h" +#include +#include +#include +#include + +namespace wav { + const char* RIFF_SIGNATURE = "RIFF"; + 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; + + Writer::Writer(int channels, uint64_t samplerate, Format format, SampleDepth depth) { + // 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; + _depth = depth; + + // Initialize header with constants + memcpy(hdr.signature, RIFF_SIGNATURE, 4); + memcpy(hdr.fileType, WAVE_FILE_TYPE, 4); + memcpy(hdr.formatMarker, FORMAT_MARKER, 4); + hdr.formatHeaderLength = FORMAT_HEADER_LEN; + hdr.sampleType = SAMPLE_TYPE_PCM; + memcpy(hdr.dataMarker, DATA_MARKER, 4); + } + + Writer::~Writer() { close(); } + + bool Writer::open(std::string path) { + std::lock_guard lck(mtx); + // Close previous file + if (_isOpen) { close(); } + + // Reset work values + samplesWritten = 0; + + // Precompute sizes and allocate buffers + bytesPerSamp = (_depth / 8) * _channels; + switch (_depth) { + case SAMP_DEPTH_8BIT: + buf8 = dsp::buffer::alloc(STREAM_BUFFER_SIZE * _channels); + break; + case SAMP_DEPTH_16BIT: + buf16 = dsp::buffer::alloc(STREAM_BUFFER_SIZE * _channels); + break; + case SAMP_DEPTH_32BIT: + buf32 = dsp::buffer::alloc(STREAM_BUFFER_SIZE * _channels); + break; + default: + return false; + break; + } + + // Open new file + file.open(path, std::ios::out | std::ios::binary); + if (!file.is_open()) { return false; } + + // Skip header, it'll be written when finalizing the file + uint8_t dummy[sizeof(Header)]; + memset(dummy, 0, sizeof(dummy)); + file.write((char*)dummy, sizeof(dummy)); + + _isOpen = true; + return true; + } + + bool Writer::isOpen() { + std::lock_guard lck(mtx); + return _isOpen; + } + + void Writer::close() { + std::lock_guard lck(mtx); + // Do nothing if the file is not open + if (!_isOpen) { return; } + + // Finilize wav + finalize(); + + // Close the file + file.close(); + + // Destroy buffers + switch (_depth) { + case SAMP_DEPTH_8BIT: + dsp::buffer::free(buf8); + break; + case SAMP_DEPTH_16BIT: + dsp::buffer::free(buf16); + break; + case SAMP_DEPTH_32BIT: + dsp::buffer::free(buf32); + break; + default: + break; + } + + // Mark as closed + _isOpen = false; + } + + void Writer::setChannels(int channels) { + std::lock_guard lck(mtx); + // Do not allow settings to change while open + if (_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 lck(mtx); + // Do not allow settings to change while open + if (_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 lck(mtx); + // Do not allow settings to change while open + if (_isOpen) { throw std::runtime_error("Cannot change parameters while file is open"); } + _format = format; + } + + void Writer::setSampleDepth(SampleDepth depth) { + std::lock_guard lck(mtx); + // Do not allow settings to change while open + if (_isOpen) { throw std::runtime_error("Cannot change parameters while file is open"); } + _depth = depth; + } + + void Writer::write(float* samples, int count) { + std::lock_guard lck(mtx); + if (!_isOpen) { return; } + + // Select different writer function depending on the chose depth + int tcount; + switch (_depth) { + case SAMP_DEPTH_8BIT: + tcount = count * _channels; + // Volk doesn't support unsigned ints :/ + for (int i = 0; i < tcount; i++) { + buf8[i] = (samples[i] * 127.0f) + 128.0f; + } + file.write((char*)buf8, count * bytesPerSamp); + break; + case SAMP_DEPTH_16BIT: + volk_32f_s32f_convert_16i(buf16, samples, 32767.0f, count * _channels); + file.write((char*)buf16, count * bytesPerSamp); + break; + case SAMP_DEPTH_32BIT: + volk_32f_s32f_convert_32i(buf32, samples, 2147483647.0f, count * _channels); + file.write((char*)buf32, count * bytesPerSamp); + break; + default: + break; + } + + // Increment sample counter + samplesWritten += count; + } + + void Writer::finalize() { + file.seekp(file.beg); + hdr.channelCount = _channels; + hdr.sampleRate = _samplerate; + hdr.bitDepth = _depth; + hdr.bytesPerSample = bytesPerSamp; + hdr.bytesPerSecond = bytesPerSamp * _samplerate; + hdr.dataSize = samplesWritten * bytesPerSamp; + hdr.fileSize = hdr.dataSize + sizeof(hdr) - 8; + file.write((char*)&hdr, sizeof(hdr)); + } +} \ No newline at end of file From 3421aae9a262dce6fd242eb6978dcd30f895b28e Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Mon, 26 Sep 2022 14:57:06 +0200 Subject: [PATCH 03/12] more work on recorder --- misc_modules/recorder/src/main.cpp | 32 +++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/misc_modules/recorder/src/main.cpp b/misc_modules/recorder/src/main.cpp index 4ed8e0b5..77bec880 100644 --- a/misc_modules/recorder/src/main.cpp +++ b/misc_modules/recorder/src/main.cpp @@ -76,6 +76,12 @@ public: } config.release(); + // Init audio path + splitter.init(NULL); + splitter.bindStream(&meterStream); + meter.init(&meterStream); + meter.start(); + // Init sinks basebandSink.init(NULL, complexHandler, this); stereoSink.init(NULL, stereoHandler, this); @@ -85,6 +91,9 @@ public: } ~RecorderModule() { + stop(); + deselectStream(); + meter.stop(); gui::menu.removeEntry(name); } @@ -170,6 +179,22 @@ public: recording = false; } + void selectStream(std::string name) { + deselectStream(); + audioStream = sigpath::sinkManager.bindStream(name); + if (!audioStream) { return; } + splitter.setInput(audioStream); + splitter.start(); + } + + void deselectStream() { + if (selectedStreamName.empty() || !audioStream) { return; } + splitter.stop(); + sigpath::sinkManager.unbindStream(selectedStreamName, audioStream); + selectedStreamName = ""; + audioStream = NULL; + } + private: static void menuHandler(void* ctx) { RecorderModule* _this = (RecorderModule*)ctx; @@ -241,7 +266,7 @@ private: if (_this->recording) { style::beginDisabled(); } if (ImGui::Checkbox(CONCAT("Stereo##_recorder_stereo_", _this->name), &_this->stereo)) { - config.acquire(); + config.acquire();audioStream config.conf[_this->name]["stereo"] = _this->stereo; config.release(true); } @@ -344,6 +369,11 @@ private: dsp::sink::Handler stereoSink; dsp::sink::Handler monoSink; + dsp::stream* audioStream = NULL; + dsp::routing::Splitter splitter; + dsp::stream meterStream; + dsp::bench::PeakLevelMeter meter; + uint64_t samplerate = 48000; }; From 869b3e0abde6e3e18d8b48c866ad9ee1f3e2e4f8 Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Mon, 26 Sep 2022 15:08:01 +0200 Subject: [PATCH 04/12] thread safety + stop recorder on audio stream deselect --- misc_modules/recorder/src/main.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/misc_modules/recorder/src/main.cpp b/misc_modules/recorder/src/main.cpp index 77bec880..a0a30988 100644 --- a/misc_modules/recorder/src/main.cpp +++ b/misc_modules/recorder/src/main.cpp @@ -180,6 +180,7 @@ public: } void selectStream(std::string name) { + std::lock_guard lck(recMtx); deselectStream(); audioStream = sigpath::sinkManager.bindStream(name); if (!audioStream) { return; } @@ -188,7 +189,9 @@ public: } void deselectStream() { + std::lock_guard lck(recMtx); if (selectedStreamName.empty() || !audioStream) { return; } + if (recording && recMode == RECORDER_MODE_AUDIO) { stop(); } splitter.stop(); sigpath::sinkManager.unbindStream(selectedStreamName, audioStream); selectedStreamName = ""; From f4bd483410003a96eaaa84742211e9bc9dd89d72 Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Mon, 26 Sep 2022 17:04:24 +0200 Subject: [PATCH 05/12] more work --- misc_modules/recorder/src/main.cpp | 36 +++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/misc_modules/recorder/src/main.cpp b/misc_modules/recorder/src/main.cpp index a0a30988..04dcf1c5 100644 --- a/misc_modules/recorder/src/main.cpp +++ b/misc_modules/recorder/src/main.cpp @@ -77,10 +77,10 @@ public: config.release(); // Init audio path - splitter.init(NULL); + volume.init(NULL, audioVolume, false); + splitter.init(&volume.out); splitter.bindStream(&meterStream); meter.init(&meterStream); - meter.start(); // Init sinks basebandSink.init(NULL, complexHandler, this); @@ -98,7 +98,7 @@ public: } void postInit() { - + selectStream("Radio"); } void enable() { @@ -184,15 +184,16 @@ public: deselectStream(); audioStream = sigpath::sinkManager.bindStream(name); if (!audioStream) { return; } - splitter.setInput(audioStream); - splitter.start(); + selectedStreamName = name; + volume.setInput(audioStream); + startAudioPath(); } void deselectStream() { std::lock_guard lck(recMtx); if (selectedStreamName.empty() || !audioStream) { return; } if (recording && recMode == RECORDER_MODE_AUDIO) { stop(); } - splitter.stop(); + stopAudioPath(); sigpath::sinkManager.unbindStream(selectedStreamName, audioStream); selectedStreamName = ""; audioStream = NULL; @@ -260,16 +261,16 @@ private: ImGui::FillWidth(); if (ImGui::SliderFloat(CONCAT("##_recorder_vol_", _this->name), &_this->audioVolume, 0, 1, "")) { // TODO: ADD VOLUME CONTROL - //_this->vol.setVolume(_this->audioVolume); + _this->volume.setVolume(_this->audioVolume); config.acquire(); config.conf[_this->name]["audioVolume"] = _this->audioVolume; config.release(true); } - ImGui::PopItemWidth(); + //ImGui::PopItemWidth(); if (_this->recording) { style::beginDisabled(); } if (ImGui::Checkbox(CONCAT("Stereo##_recorder_stereo_", _this->name), &_this->stereo)) { - config.acquire();audioStream + config.acquire(); config.conf[_this->name]["stereo"] = _this->stereo; config.release(true); } @@ -302,14 +303,26 @@ private: } } + void startAudioPath() { + volume.start(); + splitter.start(); + meter.start(); + } + + void stopAudioPath() { + volume.stop(); + splitter.stop(); + meter.stop(); + } + 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(lvl.l - (frameTime * 50.0), -90.0f, 10.0f); lvl.r = std::clamp(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 rawLvl = meter.getLevel(); + meter.resetLevel(); dsp::stereo_t dbLvl = { 10.0f * logf(rawLvl.l), 10.0f * logf(rawLvl.r) }; if (dbLvl.l > lvl.l) { lvl.l = dbLvl.l; } if (dbLvl.r > lvl.r) { lvl.r = dbLvl.r; } @@ -373,6 +386,7 @@ private: dsp::sink::Handler monoSink; dsp::stream* audioStream = NULL; + dsp::audio::Volume volume; dsp::routing::Splitter splitter; dsp::stream meterStream; dsp::bench::PeakLevelMeter meter; From 34b0577f3b54098e8b12de0c658b6c8481f3fc69 Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Tue, 11 Oct 2022 17:40:38 +0200 Subject: [PATCH 06/12] more work on the new recorder --- misc_modules/recorder/src/main.cpp | 42 +++++++++++++++--------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/misc_modules/recorder/src/main.cpp b/misc_modules/recorder/src/main.cpp index 04dcf1c5..9d06bafc 100644 --- a/misc_modules/recorder/src/main.cpp +++ b/misc_modules/recorder/src/main.cpp @@ -98,7 +98,7 @@ public: } void postInit() { - selectStream("Radio"); + selectStream(selectedStreamName); } void enable() { @@ -179,26 +179,6 @@ public: recording = false; } - void selectStream(std::string name) { - std::lock_guard lck(recMtx); - deselectStream(); - audioStream = sigpath::sinkManager.bindStream(name); - if (!audioStream) { return; } - selectedStreamName = name; - volume.setInput(audioStream); - startAudioPath(); - } - - void deselectStream() { - std::lock_guard lck(recMtx); - if (selectedStreamName.empty() || !audioStream) { return; } - if (recording && recMode == RECORDER_MODE_AUDIO) { stop(); } - stopAudioPath(); - sigpath::sinkManager.unbindStream(selectedStreamName, audioStream); - selectedStreamName = ""; - audioStream = NULL; - } - private: static void menuHandler(void* ctx) { RecorderModule* _this = (RecorderModule*)ctx; @@ -303,6 +283,26 @@ private: } } + void selectStream(std::string name) { + std::lock_guard lck(recMtx); + deselectStream(); + audioStream = sigpath::sinkManager.bindStream(name); + if (!audioStream) { return; } + selectedStreamName = name; + volume.setInput(audioStream); + startAudioPath(); + } + + void deselectStream() { + std::lock_guard lck(recMtx); + if (selectedStreamName.empty() || !audioStream) { return; } + if (recording && recMode == RECORDER_MODE_AUDIO) { stop(); } + stopAudioPath(); + sigpath::sinkManager.unbindStream(selectedStreamName, audioStream); + selectedStreamName = ""; + audioStream = NULL; + } + void startAudioPath() { volume.start(); splitter.start(); From f97ca9ac057b08bb029189a039dfacf6f7339260 Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Wed, 23 Nov 2022 10:22:29 +0100 Subject: [PATCH 07/12] almost done with the new recorder --- core/src/utils/optionlist.h | 4 ++ misc_modules/recorder/src/main.cpp | 90 ++++++++++++++++++++++++++---- 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/core/src/utils/optionlist.h b/core/src/utils/optionlist.h index 97f71da3..06e18ce9 100644 --- a/core/src/utils/optionlist.h +++ b/core/src/utils/optionlist.h @@ -50,6 +50,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; diff --git a/misc_modules/recorder/src/main.cpp b/misc_modules/recorder/src/main.cpp index 9d06bafc..597beaf3 100644 --- a/misc_modules/recorder/src/main.cpp +++ b/misc_modules/recorder/src/main.cpp @@ -42,7 +42,7 @@ public: // Define option lists formats.define("WAV", wav::FORMAT_WAV); - formats.define("RF64", wav::FORMAT_RF64); + // formats.define("RF64", wav::FORMAT_RF64); // Disabled for now 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); @@ -98,6 +98,22 @@ public: } void postInit() { + // 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); } @@ -138,10 +154,8 @@ public: } // 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"); + stereoStream = sigpath::sinkManager.bindStream(selectedStreamName); stereoSink.setInput(stereoStream); stereoSink.start(); } @@ -164,7 +178,7 @@ public: if (recMode == RECORDER_MODE_AUDIO) { // TODO: HAS TO BE DONE PROPERLY stereoSink.stop(); - sigpath::sinkManager.unbindStream("Radio", stereoStream); + sigpath::sinkManager.unbindStream(selectedStreamName, stereoStream); } else { // Unbind and destroy IQ stream @@ -231,7 +245,16 @@ private: } // Show additional audio options - if (_this->recMode == RECORDER_MODE_AUDIO) { + 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); @@ -240,13 +263,11 @@ private: ImGui::FillWidth(); if (ImGui::SliderFloat(CONCAT("##_recorder_vol_", _this->name), &_this->audioVolume, 0, 1, "")) { - // TODO: ADD VOLUME CONTROL _this->volume.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)) { @@ -286,20 +307,34 @@ private: void selectStream(std::string name) { std::lock_guard lck(recMtx); deselectStream(); + + if (audioStreams.empty()) { + selectedStreamName.clear(); + return; + } + else if (!audioStreams.keyExists(name)) { + selectStream(audioStreams.key(0)); + return; + } + audioStream = sigpath::sinkManager.bindStream(name); if (!audioStream) { return; } selectedStreamName = name; + streamId = audioStreams.keyId(name); volume.setInput(audioStream); startAudioPath(); } void deselectStream() { std::lock_guard lck(recMtx); - if (selectedStreamName.empty() || !audioStream) { return; } + if (selectedStreamName.empty() || !audioStream) { + selectedStreamName.clear(); + return; + } if (recording && recMode == RECORDER_MODE_AUDIO) { stop(); } stopAudioPath(); sigpath::sinkManager.unbindStream(selectedStreamName, audioStream); - selectedStreamName = ""; + selectedStreamName.clear(); audioStream = NULL; } @@ -315,6 +350,36 @@ private: 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; @@ -385,6 +450,8 @@ private: dsp::sink::Handler stereoSink; dsp::sink::Handler monoSink; + OptionList audioStreams; + int streamId = 0; dsp::stream* audioStream = NULL; dsp::audio::Volume volume; dsp::routing::Splitter splitter; @@ -393,6 +460,9 @@ private: uint64_t samplerate = 48000; + EventHandler onStreamRegisteredHandler; + EventHandler onStreamUnregisterHandler; + }; MOD_EXPORT void _INIT_() { From d069fb3af8fc68c31ceaee7d98a9c6c41df9fd34 Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Fri, 25 Nov 2022 20:32:12 +0100 Subject: [PATCH 08/12] more fixes --- misc_modules/recorder/src/main.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/misc_modules/recorder/src/main.cpp b/misc_modules/recorder/src/main.cpp index 597beaf3..eee452c3 100644 --- a/misc_modules/recorder/src/main.cpp +++ b/misc_modules/recorder/src/main.cpp @@ -135,7 +135,8 @@ public: // Configure the wav writer if (recMode == RECORDER_MODE_AUDIO) { - samplerate = sigpath::sinkManager.getStreamSampleRate("Radio"); + if (selectedStreamName.empty()) { return; } + samplerate = sigpath::sinkManager.getStreamSampleRate(selectedStreamName); } else { samplerate = sigpath::iqFrontEnd.getSampleRate(); @@ -155,6 +156,7 @@ public: // Open audio stream or baseband if (recMode == RECORDER_MODE_AUDIO) { + // TODO: Select the stereo to mono converter if needed stereoStream = sigpath::sinkManager.bindStream(selectedStreamName); stereoSink.setInput(stereoStream); stereoSink.start(); From 5c690c97533290f001e873071d597422b016a356 Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Tue, 6 Dec 2022 14:52:42 +0100 Subject: [PATCH 09/12] more work --- misc_modules/recorder/src/main.cpp | 25 ++++----- misc_modules/recorder/src/riff.cpp | 86 ++++++++++++++++++++++++++++++ misc_modules/recorder/src/riff.h | 41 ++++++++++++++ misc_modules/recorder/src/wav.cpp | 85 ++++++++++++++++------------- misc_modules/recorder/src/wav.h | 28 ++++++---- 5 files changed, 206 insertions(+), 59 deletions(-) create mode 100644 misc_modules/recorder/src/riff.cpp create mode 100644 misc_modules/recorder/src/riff.h diff --git a/misc_modules/recorder/src/main.cpp b/misc_modules/recorder/src/main.cpp index eee452c3..25287518 100644 --- a/misc_modules/recorder/src/main.cpp +++ b/misc_modules/recorder/src/main.cpp @@ -43,13 +43,14 @@ public: // Define option lists formats.define("WAV", wav::FORMAT_WAV); // formats.define("RF64", wav::FORMAT_RF64); // Disabled for now - 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); + 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 formatId = formats.valueId(wav::FORMAT_WAV); - sampleDepthId = sampleDepths.valueId(wav::SAMP_DEPTH_16BIT); + sampleTypeId = sampleTypes.valueId(wav::SAMP_TYPE_INT16); // Load config config.acquire(); @@ -62,8 +63,8 @@ public: 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("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"]; @@ -143,7 +144,7 @@ public: } writer.setFormat(formats[formatId]); writer.setChannels((recMode == RECORDER_MODE_AUDIO && !stereo) ? 1 : 2); - writer.setSampleDepth(sampleDepths[sampleDepthId]); + writer.setSampleType(sampleTypes[sampleTypeId]); writer.setSamplerate(samplerate); // Open file @@ -238,11 +239,11 @@ private: config.release(true); } - ImGui::LeftLabel("Sample depth"); + ImGui::LeftLabel("Sample type"); ImGui::FillWidth(); - if (ImGui::Combo(CONCAT("##_recorder_bits_", _this->name), &_this->sampleDepthId, _this->sampleDepths.txt)) { + if (ImGui::Combo(CONCAT("##_recorder_st_", _this->name), &_this->sampleTypeId, _this->sampleTypes.txt)) { config.acquire(); - config.conf[_this->name]["sampleDepth"] = _this->sampleDepths.key(_this->sampleDepthId); + config.conf[_this->name]["sampleType"] = _this->sampleTypes.key(_this->sampleTypeId); config.release(true); } @@ -431,12 +432,12 @@ private: std::string root; OptionList formats; - OptionList sampleDepths; + OptionList sampleTypes; FolderSelect folderSelect; int recMode = RECORDER_MODE_AUDIO; int formatId; - int sampleDepthId; + int sampleTypeId; bool stereo = true; std::string selectedStreamName = ""; float audioVolume = 1.0f; diff --git a/misc_modules/recorder/src/riff.cpp b/misc_modules/recorder/src/riff.cpp new file mode 100644 index 00000000..4c1fab75 --- /dev/null +++ b/misc_modules/recorder/src/riff.cpp @@ -0,0 +1,86 @@ +#include "riff.h" +#include + +namespace riff { + bool Writer::open(std::string path, char form[4]) { + // TODO: Open file + + beginRIFF(form); + + return false; + } + + bool Writer::isOpen() { + + return false; + } + + void Writer::close() { + if (!isOpen()) { return; } + + endRIFF(); + + file.close(); + } + + void Writer::beginChunk(char id[4]) { + // 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() { + 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(); + file.seekp(desc.pos + 4); + 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(void* data, size_t len) { + 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(char form[4]) { + 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"); + write(form, sizeof(form)); + } + + void Writer::endRIFF() { + if (!chunks.empty()) { + throw std::runtime_error("No chunk to end"); + } + if (memcmp(chunks.top().hdr.id, "RIFF", 4)) { + throw std::runtime_error("Top chunk not RIFF chunk"); + } + endChunk(); + } +} \ No newline at end of file diff --git a/misc_modules/recorder/src/riff.h b/misc_modules/recorder/src/riff.h new file mode 100644 index 00000000..6bfbaaf3 --- /dev/null +++ b/misc_modules/recorder/src/riff.h @@ -0,0 +1,41 @@ +#pragma once +#include +#include +#include +#include + +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, char form[4]); + bool isOpen(); + void close(); + + void beginList(); + void endList(); + + void beginChunk(char id[4]); + void endChunk(); + + void write(void* data, size_t len); + + private: + void beginRIFF(char form[4]); + void endRIFF(); + + std::ofstream file; + std::stack chunks; + }; +} \ No newline at end of file diff --git a/misc_modules/recorder/src/wav.cpp b/misc_modules/recorder/src/wav.cpp index 2cd47243..5a6a38a1 100644 --- a/misc_modules/recorder/src/wav.cpp +++ b/misc_modules/recorder/src/wav.cpp @@ -3,6 +3,7 @@ #include #include #include +#include namespace wav { const char* RIFF_SIGNATURE = "RIFF"; @@ -11,8 +12,15 @@ namespace wav { const char* DATA_MARKER = "data"; const uint32_t FORMAT_HEADER_LEN = 16; const uint16_t SAMPLE_TYPE_PCM = 1; + + std::map 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, SampleDepth depth) { + 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"); } @@ -21,14 +29,13 @@ namespace wav { _channels = channels; _samplerate = samplerate; _format = format; - _depth = depth; + _type = type; // Initialize header with constants memcpy(hdr.signature, RIFF_SIGNATURE, 4); memcpy(hdr.fileType, WAVE_FILE_TYPE, 4); memcpy(hdr.formatMarker, FORMAT_MARKER, 4); hdr.formatHeaderLength = FORMAT_HEADER_LEN; - hdr.sampleType = SAMPLE_TYPE_PCM; memcpy(hdr.dataMarker, DATA_MARKER, 4); } @@ -43,16 +50,19 @@ namespace wav { samplesWritten = 0; // Precompute sizes and allocate buffers - bytesPerSamp = (_depth / 8) * _channels; - switch (_depth) { - case SAMP_DEPTH_8BIT: - buf8 = dsp::buffer::alloc(STREAM_BUFFER_SIZE * _channels); + // TODO: Get number of bits for each sample type + bytesPerSamp = (SAMP_BITS[_type] / 8) * _channels; // THIS IS WRONG + switch (_type) { + case SAMP_TYPE_UINT8: + bufU8 = dsp::buffer::alloc(STREAM_BUFFER_SIZE * _channels); break; - case SAMP_DEPTH_16BIT: - buf16 = dsp::buffer::alloc(STREAM_BUFFER_SIZE * _channels); + case SAMP_TYPE_INT16: + bufI16 = dsp::buffer::alloc(STREAM_BUFFER_SIZE * _channels); break; - case SAMP_DEPTH_32BIT: - buf32 = dsp::buffer::alloc(STREAM_BUFFER_SIZE * _channels); + case SAMP_TYPE_INT32: + bufI32 = dsp::buffer::alloc(STREAM_BUFFER_SIZE * _channels); + break; + case SAMP_TYPE_FLOAT32: break; default: return false; @@ -89,18 +99,17 @@ namespace wav { file.close(); // Destroy buffers - switch (_depth) { - case SAMP_DEPTH_8BIT: - dsp::buffer::free(buf8); - break; - case SAMP_DEPTH_16BIT: - dsp::buffer::free(buf16); - break; - case SAMP_DEPTH_32BIT: - dsp::buffer::free(buf32); - break; - default: - break; + if (bufU8) { + dsp::buffer::free(bufU8); + bufU8 = NULL; + } + if (bufI16) { + dsp::buffer::free(bufI16); + bufI16 = NULL; + } + if (bufI32) { + dsp::buffer::free(bufI32); + bufI32 = NULL; } // Mark as closed @@ -133,11 +142,11 @@ namespace wav { _format = format; } - void Writer::setSampleDepth(SampleDepth depth) { + void Writer::setSampleType(SampleType type) { std::lock_guard lck(mtx); // Do not allow settings to change while open if (_isOpen) { throw std::runtime_error("Cannot change parameters while file is open"); } - _depth = depth; + _type = type; } void Writer::write(float* samples, int count) { @@ -146,22 +155,25 @@ namespace wav { // Select different writer function depending on the chose depth int tcount; - switch (_depth) { - case SAMP_DEPTH_8BIT: + switch (_type) { + case SAMP_TYPE_UINT8: tcount = count * _channels; // Volk doesn't support unsigned ints :/ for (int i = 0; i < tcount; i++) { - buf8[i] = (samples[i] * 127.0f) + 128.0f; + bufU8[i] = (samples[i] * 127.0f) + 128.0f; } - file.write((char*)buf8, count * bytesPerSamp); + file.write((char*)bufU8, count * bytesPerSamp); break; - case SAMP_DEPTH_16BIT: - volk_32f_s32f_convert_16i(buf16, samples, 32767.0f, count * _channels); - file.write((char*)buf16, count * bytesPerSamp); + case SAMP_TYPE_INT16: + volk_32f_s32f_convert_16i(bufI16, samples, 32767.0f, count * _channels); + file.write((char*)bufI16, count * bytesPerSamp); break; - case SAMP_DEPTH_32BIT: - volk_32f_s32f_convert_32i(buf32, samples, 2147483647.0f, count * _channels); - file.write((char*)buf32, count * bytesPerSamp); + case SAMP_TYPE_INT32: + volk_32f_s32f_convert_32i(bufI32, samples, 2147483647.0f, count * _channels); + file.write((char*)bufI32, count * bytesPerSamp); + break; + case SAMP_TYPE_FLOAT32: + file.write((char*)samples, count * bytesPerSamp); break; default: break; @@ -173,9 +185,10 @@ namespace wav { void Writer::finalize() { file.seekp(file.beg); + hdr.codec = (_type == SAMP_TYPE_FLOAT32) ? CODEC_FLOAT : CODEC_PCM; hdr.channelCount = _channels; hdr.sampleRate = _samplerate; - hdr.bitDepth = _depth; + hdr.bitDepth = SAMP_BITS[_type]; hdr.bytesPerSample = bytesPerSamp; hdr.bytesPerSecond = bytesPerSamp * _samplerate; hdr.dataSize = samplesWritten * bytesPerSamp; diff --git a/misc_modules/recorder/src/wav.h b/misc_modules/recorder/src/wav.h index 7823b5d5..f18e7097 100644 --- a/misc_modules/recorder/src/wav.h +++ b/misc_modules/recorder/src/wav.h @@ -12,7 +12,7 @@ namespace wav { char fileType[4]; // "WAVE" char formatMarker[4]; // "fmt " uint32_t formatHeaderLength; // Always 16 - uint16_t sampleType; // PCM (1) + uint16_t codec; // PCM (1) uint16_t channelCount; uint32_t sampleRate; uint32_t bytesPerSecond; @@ -28,15 +28,21 @@ namespace wav { FORMAT_RF64 }; - enum SampleDepth { - SAMP_DEPTH_8BIT = 8, - SAMP_DEPTH_16BIT = 16, - SAMP_DEPTH_32BIT = 32 + 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, SampleDepth depth = SAMP_DEPTH_16BIT); + Writer(int channels = 2, uint64_t samplerate = 48000, Format format = FORMAT_WAV, SampleType type = SAMP_TYPE_INT16); ~Writer(); bool open(std::string path); @@ -46,7 +52,7 @@ namespace wav { void setChannels(int channels); void setSamplerate(uint64_t samplerate); void setFormat(Format format); - void setSampleDepth(SampleDepth depth); + void setSampleType(SampleType type); size_t getSamplesWritten() { return samplesWritten; } @@ -63,12 +69,12 @@ namespace wav { int _channels; uint64_t _samplerate; Format _format; - SampleDepth _depth; + SampleType _type; size_t bytesPerSamp; - int8_t* buf8; - int16_t* buf16; - int32_t* buf32; + uint8_t* bufU8 = NULL; + int16_t* bufI16 = NULL; + int32_t* bufI32 = NULL; size_t samplesWritten = 0; }; } From 8771e4bf0904809d1a05989bfbe82b200c7e1209 Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Sun, 1 Jan 2023 03:20:49 +0100 Subject: [PATCH 10/12] final fixes --- misc_modules/recorder/src/main.cpp | 42 ++++++++----- misc_modules/recorder/src/riff.cpp | 77 +++++++++++++++++++----- misc_modules/recorder/src/riff.h | 12 ++-- misc_modules/recorder/src/wav.cpp | 94 +++++++++++++----------------- misc_modules/recorder/src/wav.h | 19 ++---- 5 files changed, 140 insertions(+), 104 deletions(-) diff --git a/misc_modules/recorder/src/main.cpp b/misc_modules/recorder/src/main.cpp index 25287518..238c83a3 100644 --- a/misc_modules/recorder/src/main.cpp +++ b/misc_modules/recorder/src/main.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -41,15 +42,15 @@ public: root = (std::string)core::args["root"]; // Define option lists - formats.define("WAV", wav::FORMAT_WAV); - // formats.define("RF64", wav::FORMAT_RF64); // Disabled for now + 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 - formatId = formats.valueId(wav::FORMAT_WAV); + containerId = containers.valueId(wav::FORMAT_WAV); sampleTypeId = sampleTypes.valueId(wav::SAMP_TYPE_INT16); // Load config @@ -60,8 +61,8 @@ public: if (config.conf[name].contains("recPath")) { folderSelect.setPath(config.conf[name]["recPath"]); } - if (config.conf[name].contains("format") && formats.keyExists(config.conf[name]["format"])) { - formatId = formats.keyId(config.conf[name]["format"]); + if (config.conf[name].contains("format") && containers.keyExists(config.conf[name]["format"])) { + containerId = containers.keyId(config.conf[name]["format"]); } if (config.conf[name].contains("sampleType") && sampleTypes.keyExists(config.conf[name]["sampleType"])) { sampleTypeId = sampleTypes.keyId(config.conf[name]["sampleType"]); @@ -82,11 +83,12 @@ public: splitter.init(&volume.out); splitter.bindStream(&meterStream); meter.init(&meterStream); + s2m.init(NULL); // Init sinks basebandSink.init(NULL, complexHandler, this); stereoSink.init(NULL, stereoHandler, this); - monoSink.init(NULL, monoHandler, this); + monoSink.init(&s2m.out, monoHandler, this); gui::menu.registerEntry(name, menuHandler, this); } @@ -142,7 +144,7 @@ public: else { samplerate = sigpath::iqFrontEnd.getSampleRate(); } - writer.setFormat(formats[formatId]); + writer.setFormat(containers[containerId]); writer.setChannels((recMode == RECORDER_MODE_AUDIO && !stereo) ? 1 : 2); writer.setSampleType(sampleTypes[sampleTypeId]); writer.setSamplerate(samplerate); @@ -159,8 +161,15 @@ public: if (recMode == RECORDER_MODE_AUDIO) { // TODO: Select the stereo to mono converter if needed stereoStream = sigpath::sinkManager.bindStream(selectedStreamName); - stereoSink.setInput(stereoStream); - stereoSink.start(); + if (stereo) { + stereoSink.setInput(stereoStream); + stereoSink.start(); + } + else { + s2m.setInput(stereoStream); + s2m.start(); + monoSink.start(); + } } else { // Create and bind IQ stream @@ -179,8 +188,10 @@ public: // Close audio stream or baseband if (recMode == RECORDER_MODE_AUDIO) { - // TODO: HAS TO BE DONE PROPERLY + // 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 { @@ -231,11 +242,11 @@ private: } } - ImGui::LeftLabel("WAV Format"); + ImGui::LeftLabel("Container"); ImGui::FillWidth(); - if (ImGui::Combo(CONCAT("##_recorder_wav_fmt_", _this->name), &_this->formatId, _this->formats.txt)) { + if (ImGui::Combo(CONCAT("##_recorder_container_", _this->name), &_this->containerId, _this->containers.txt)) { config.acquire(); - config.conf[_this->name]["format"] = _this->formats.key(_this->formatId); + config.conf[_this->name]["container"] = _this->containers.key(_this->containerId); config.release(true); } @@ -431,12 +442,12 @@ private: bool enabled = true; std::string root; - OptionList formats; + OptionList containers; OptionList sampleTypes; FolderSelect folderSelect; int recMode = RECORDER_MODE_AUDIO; - int formatId; + int containerId; int sampleTypeId; bool stereo = true; std::string selectedStreamName = ""; @@ -460,6 +471,7 @@ private: dsp::routing::Splitter splitter; dsp::stream meterStream; dsp::bench::PeakLevelMeter meter; + dsp::convert::StereoToMono s2m; uint64_t samplerate = 48000; diff --git a/misc_modules/recorder/src/riff.cpp b/misc_modules/recorder/src/riff.cpp index 4c1fab75..c59f2760 100644 --- a/misc_modules/recorder/src/riff.cpp +++ b/misc_modules/recorder/src/riff.cpp @@ -2,28 +2,64 @@ #include namespace riff { - bool Writer::open(std::string path, char form[4]) { - // TODO: Open file + 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 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 false; + return true; } bool Writer::isOpen() { - - return false; + std::lock_guard lck(mtx); + return file.is_open(); } void Writer::close() { + std::lock_guard lck(mtx); + if (!isOpen()) { return; } + // Finalize RIFF chunk endRIFF(); + // Close file file.close(); } - void Writer::beginChunk(char id[4]) { + void Writer::beginList(const char id[4]) { + std::lock_guard 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 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 lck(mtx); + // Create and write header ChunkDesc desc; desc.pos = file.tellp(); @@ -36,7 +72,9 @@ namespace riff { } void Writer::endChunk() { - if (!chunks.empty()) { + std::lock_guard lck(mtx); + + if (chunks.empty()) { throw std::runtime_error("No chunk to end"); } @@ -46,7 +84,9 @@ namespace riff { // Write size auto pos = file.tellp(); - file.seekp(desc.pos + 4); + auto npos = desc.pos; + npos += 4; + file.seekp(npos); file.write((char*)&desc.hdr.size, sizeof(desc.hdr.size)); file.seekp(pos); @@ -56,31 +96,38 @@ namespace riff { } } - void Writer::write(void* data, size_t len) { - if (!chunks.empty()) { + void Writer::write(const uint8_t* data, size_t len) { + std::lock_guard 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(char form[4]) { + void Writer::beginRIFF(const char form[4]) { + std::lock_guard 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"); - write(form, sizeof(form)); + beginChunk(RIFF_SIGNATURE); + write((uint8_t*)form, RIFF_LABEL_SIZE); } void Writer::endRIFF() { - if (!chunks.empty()) { + std::lock_guard lck(mtx); + + if (chunks.empty()) { throw std::runtime_error("No chunk to end"); } - if (memcmp(chunks.top().hdr.id, "RIFF", 4)) { + if (memcmp(chunks.top().hdr.id, RIFF_SIGNATURE, RIFF_LABEL_SIZE)) { throw std::runtime_error("Top chunk not RIFF chunk"); } + endChunk(); } } \ No newline at end of file diff --git a/misc_modules/recorder/src/riff.h b/misc_modules/recorder/src/riff.h index 6bfbaaf3..e47ccf03 100644 --- a/misc_modules/recorder/src/riff.h +++ b/misc_modules/recorder/src/riff.h @@ -1,4 +1,5 @@ #pragma once +#include #include #include #include @@ -19,22 +20,23 @@ namespace riff { class Writer { public: - bool open(std::string path, char form[4]); + bool open(std::string path, const char form[4]); bool isOpen(); void close(); - void beginList(); + void beginList(const char id[4]); void endList(); - void beginChunk(char id[4]); + void beginChunk(const char id[4]); void endChunk(); - void write(void* data, size_t len); + void write(const uint8_t* data, size_t len); private: - void beginRIFF(char form[4]); + void beginRIFF(const char form[4]); void endRIFF(); + std::recursive_mutex mtx; std::ofstream file; std::stack chunks; }; diff --git a/misc_modules/recorder/src/wav.cpp b/misc_modules/recorder/src/wav.cpp index 5a6a38a1..bd545755 100644 --- a/misc_modules/recorder/src/wav.cpp +++ b/misc_modules/recorder/src/wav.cpp @@ -6,7 +6,6 @@ #include namespace wav { - const char* RIFF_SIGNATURE = "RIFF"; const char* WAVE_FILE_TYPE = "WAVE"; const char* FORMAT_MARKER = "fmt "; const char* DATA_MARKER = "data"; @@ -30,13 +29,6 @@ namespace wav { _samplerate = samplerate; _format = format; _type = type; - - // Initialize header with constants - memcpy(hdr.signature, RIFF_SIGNATURE, 4); - memcpy(hdr.fileType, WAVE_FILE_TYPE, 4); - memcpy(hdr.formatMarker, FORMAT_MARKER, 4); - hdr.formatHeaderLength = FORMAT_HEADER_LEN; - memcpy(hdr.dataMarker, DATA_MARKER, 4); } Writer::~Writer() { close(); } @@ -44,14 +36,21 @@ namespace wav { bool Writer::open(std::string path) { std::lock_guard lck(mtx); // Close previous file - if (_isOpen) { close(); } + 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 - // TODO: Get number of bits for each sample type - bytesPerSamp = (SAMP_BITS[_type] / 8) * _channels; // THIS IS WRONG switch (_type) { case SAMP_TYPE_UINT8: bufU8 = dsp::buffer::alloc(STREAM_BUFFER_SIZE * _channels); @@ -69,36 +68,37 @@ namespace wav { break; } - // Open new file - file.open(path, std::ios::out | std::ios::binary); - if (!file.is_open()) { return false; } + // Open file + if (!rw.open(path, WAVE_FILE_TYPE)) { return false; } - // Skip header, it'll be written when finalizing the file - uint8_t dummy[sizeof(Header)]; - memset(dummy, 0, sizeof(dummy)); - file.write((char*)dummy, sizeof(dummy)); + // Write format chunk + rw.beginChunk(FORMAT_MARKER); + rw.write((uint8_t*)&hdr, sizeof(FormatHeader)); + rw.endChunk(); - _isOpen = true; + // Begin data chunk + rw.beginChunk(DATA_MARKER); + return true; } bool Writer::isOpen() { std::lock_guard lck(mtx); - return _isOpen; + return rw.isOpen(); } void Writer::close() { std::lock_guard lck(mtx); // Do nothing if the file is not open - if (!_isOpen) { return; } + if (!rw.isOpen()) { return; } - // Finilize wav - finalize(); + // Finish data chunk + rw.endChunk(); // Close the file - file.close(); + rw.close(); - // Destroy buffers + // Free buffers if (bufU8) { dsp::buffer::free(bufU8); bufU8 = NULL; @@ -111,15 +111,12 @@ namespace wav { dsp::buffer::free(bufI32); bufI32 = NULL; } - - // Mark as closed - _isOpen = false; } void Writer::setChannels(int channels) { std::lock_guard lck(mtx); // Do not allow settings to change while open - if (_isOpen) { throw std::runtime_error("Cannot change parameters while file is 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"); } @@ -129,7 +126,7 @@ namespace wav { void Writer::setSamplerate(uint64_t samplerate) { std::lock_guard lck(mtx); // Do not allow settings to change while open - if (_isOpen) { throw std::runtime_error("Cannot change parameters while file is 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"); } @@ -138,42 +135,42 @@ namespace wav { void Writer::setFormat(Format format) { std::lock_guard lck(mtx); // Do not allow settings to change while open - if (_isOpen) { throw std::runtime_error("Cannot change parameters while file is 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 lck(mtx); // Do not allow settings to change while open - if (_isOpen) { throw std::runtime_error("Cannot change parameters while file is 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 lck(mtx); - if (!_isOpen) { return; } + if (!rw.isOpen()) { return; } // Select different writer function depending on the chose depth - int tcount; + int tcount = count * _channels; + int tbytes = count * bytesPerSamp; switch (_type) { case SAMP_TYPE_UINT8: - tcount = count * _channels; - // Volk doesn't support unsigned ints :/ + // Volk doesn't support unsigned ints yet :/ for (int i = 0; i < tcount; i++) { bufU8[i] = (samples[i] * 127.0f) + 128.0f; } - file.write((char*)bufU8, count * bytesPerSamp); + rw.write(bufU8, tbytes); break; case SAMP_TYPE_INT16: - volk_32f_s32f_convert_16i(bufI16, samples, 32767.0f, count * _channels); - file.write((char*)bufI16, count * bytesPerSamp); + 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, count * _channels); - file.write((char*)bufI32, count * bytesPerSamp); + volk_32f_s32f_convert_32i(bufI32, samples, 2147483647.0f, tcount); + rw.write((uint8_t*)bufI32, tbytes); break; case SAMP_TYPE_FLOAT32: - file.write((char*)samples, count * bytesPerSamp); + rw.write((uint8_t*)samples, tbytes); break; default: break; @@ -182,17 +179,4 @@ namespace wav { // Increment sample counter samplesWritten += count; } - - void Writer::finalize() { - file.seekp(file.beg); - 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; - hdr.dataSize = samplesWritten * bytesPerSamp; - hdr.fileSize = hdr.dataSize + sizeof(hdr) - 8; - file.write((char*)&hdr, sizeof(hdr)); - } } \ No newline at end of file diff --git a/misc_modules/recorder/src/wav.h b/misc_modules/recorder/src/wav.h index f18e7097..c9a95bdb 100644 --- a/misc_modules/recorder/src/wav.h +++ b/misc_modules/recorder/src/wav.h @@ -3,23 +3,17 @@ #include #include #include +#include "riff.h" 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 codec; // PCM (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) @@ -59,12 +53,9 @@ namespace wav { void write(float* samples, int count); private: - void finalize(); - std::recursive_mutex mtx; - std::ofstream file; - Header hdr; - bool _isOpen = false; + FormatHeader hdr; + riff::Writer rw; int _channels; uint64_t _samplerate; From 385b34a0a15aef6857cc5ba42b6d4b51b8bd7cd4 Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Mon, 2 Jan 2023 01:24:30 +0100 Subject: [PATCH 11/12] bugfix --- misc_modules/recorder/src/main.cpp | 92 ++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 12 deletions(-) diff --git a/misc_modules/recorder/src/main.cpp b/misc_modules/recorder/src/main.cpp index 238c83a3..62cf656d 100644 --- a/misc_modules/recorder/src/main.cpp +++ b/misc_modules/recorder/src/main.cpp @@ -40,6 +40,7 @@ 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); @@ -61,8 +62,8 @@ public: if (config.conf[name].contains("recPath")) { folderSelect.setPath(config.conf[name]["recPath"]); } - if (config.conf[name].contains("format") && containers.keyExists(config.conf[name]["format"])) { - containerId = containers.keyId(config.conf[name]["format"]); + if (config.conf[name].contains("container") && containers.keyExists(config.conf[name]["container"])) { + containerId = containers.keyId(config.conf[name]["container"]); } if (config.conf[name].contains("sampleType") && sampleTypes.keyExists(config.conf[name]["sampleType"])) { sampleTypeId = sampleTypes.keyId(config.conf[name]["sampleType"]); @@ -76,6 +77,13 @@ public: 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 @@ -91,13 +99,18 @@ public: monoSink.init(&s2m.out, monoHandler, this); gui::menu.registerEntry(name, menuHandler, this); + core::modComManager.registerInterface("recorder", name, moduleInterfaceHandler, this); } ~RecorderModule() { + std::lock_guard lck(recMtx); + core::modComManager.unregisterInterface(name); + gui::menu.removeEntry(name); stop(); deselectStream(); + sigpath::sinkManager.onStreamRegistered.unbindHandler(&onStreamRegisteredHandler); + sigpath::sinkManager.onStreamUnregister.unbindHandler(&onStreamUnregisterHandler); meter.stop(); - gui::menu.removeEntry(name); } void postInit() { @@ -150,8 +163,10 @@ public: writer.setSamplerate(samplerate); // Open file - std::string prefix = (recMode == RECORDER_MODE_AUDIO) ? "/audio_" : "/baseband_"; - std::string expandedPath = expandString(folderSelect.path + genFileName(prefix, false)); + 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; @@ -242,6 +257,14 @@ private: } } + 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); + } + ImGui::LeftLabel("Container"); ImGui::FillWidth(); if (ImGui::Combo(CONCAT("##_recorder_container_", _this->name), &_this->containerId, _this->containers.txt)) { @@ -399,7 +422,6 @@ private: double frameTime = 1.0 / ImGui::GetIO().Framerate; lvl.l = std::clamp(lvl.l - (frameTime * 50.0), -90.0f, 10.0f); lvl.r = std::clamp(lvl.r - (frameTime * 50.0), -90.0f, 10.0f); - // TODO: FINISH METER dsp::stereo_t rawLvl = meter.getLevel(); meter.resetLevel(); dsp::stereo_t dbLvl = { 10.0f * logf(rawLvl.l), 10.0f * logf(rawLvl.r) }; @@ -407,19 +429,44 @@ private: if (dbLvl.r > lvl.r) { lvl.r = dbLvl.r; } } - // TODO: REPLACE WITH SOMETHING CLEAN - std::string genFileName(std::string prefix, bool isVfo, std::string name = "") { + 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 (isVfo && gui::waterfall.vfos.find(name) != gui::waterfall.vfos.end()) { + if (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; + + // 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; } + std::string expandString(std::string input) { input = std::regex_replace(input, std::regex("%ROOT%"), root); return std::regex_replace(input, std::regex("//"), "/"); @@ -438,9 +485,30 @@ private: _this->writer.write(data, 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(*_in, 0, 1); + } + else if (code == RECORDER_IFACE_CMD_START) { + if (!_this->recording) { _this->start(); } + } + else if (code == RECORDER_IFACE_CMD_STOP) { + if (_this->recording) { _this->stop(); } + } + } + std::string name; bool enabled = true; std::string root; + char nameTemplate[1024]; OptionList containers; OptionList sampleTypes; From 5773419257a31421a88fceff8ee826101e9bd939 Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Mon, 2 Jan 2023 02:09:41 +0100 Subject: [PATCH 12/12] added missing include --- misc_modules/recorder/src/riff.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/misc_modules/recorder/src/riff.cpp b/misc_modules/recorder/src/riff.cpp index c59f2760..9074badc 100644 --- a/misc_modules/recorder/src/riff.cpp +++ b/misc_modules/recorder/src/riff.cpp @@ -1,4 +1,5 @@ #include "riff.h" +#include #include namespace riff {