From 0c8f6ab836902c1ee70342dac2a76c23e65d06be Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Sun, 25 Sep 2022 01:01:07 +0200 Subject: [PATCH] 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; + }; +}