SDRPlusPlus/misc_modules/recorder/src/main.cpp

407 wiersze
14 KiB
C++

#include <imgui.h>
#include <module.h>
#include <wav.h>
#include <dsp/types.h>
#include <dsp/stream.h>
#include <dsp/bench/peak_level_meter.h>
#include <dsp/sink/handler_sink.h>
#include <dsp/routing/splitter.h>
#include <dsp/audio/volume.h>
#include <thread>
#include <ctime>
#include <gui/gui.h>
#include <filesystem>
#include <signal_path/signal_path.h>
#include <config.h>
#include <gui/style.h>
#include <gui/widgets/volume_meter.h>
#include <regex>
#include <gui/widgets/folder_select.h>
#include <recorder_interface.h>
#include <core.h>
#include <utils/optionlist.h>
#include "wav.h"
#define CONCAT(a, b) ((std::string(a) + b).c_str())
SDRPP_MOD_INFO{
/* Name: */ "recorder",
/* Description: */ "Recorder module for SDR++",
/* Author: */ "Ryzerth",
/* Version: */ 0, 3, 0,
/* Max instances */ -1
};
ConfigManager config;
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();
if (config.conf[name].contains("mode")) {
recMode = config.conf[name]["mode"];
}
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("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();
// 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);
monoSink.init(NULL, monoHandler, this);
gui::menu.registerEntry(name, menuHandler, this);
}
~RecorderModule() {
stop();
deselectStream();
meter.stop();
gui::menu.removeEntry(name);
}
void postInit() {
}
void enable() {
enabled = true;
}
void disable() {
enabled = false;
}
bool isEnabled() {
return enabled;
}
void start() {
std::lock_guard<std::recursive_mutex> lck(recMtx);
if (recording) { return; }
// Configure the wav writer
if (recMode == RECORDER_MODE_AUDIO) {
samplerate = sigpath::sinkManager.getStreamSampleRate("Radio");
}
else {
samplerate = sigpath::iqFrontEnd.getSampleRate();
}
writer.setFormat(formats[formatId]);
writer.setChannels((recMode == RECORDER_MODE_AUDIO && !stereo) ? 1 : 2);
writer.setSampleDepth(sampleDepths[sampleDepthId]);
writer.setSamplerate(samplerate);
// Open file
std::string prefix = (recMode == RECORDER_MODE_AUDIO) ? "/audio_" : "/baseband_";
std::string expandedPath = expandString(folderSelect.path + genFileName(prefix, false));
if (!writer.open(expandedPath)) {
spdlog::error("Failed to open file for recording: {0}", expandedPath);
return;
}
// Open audio stream or baseband
// TODO: DO NOT HARDCODE THE STREAM NAME
if (recMode == RECORDER_MODE_AUDIO) {
// TODO: HAS TO BE DONE PROPERLY
stereoStream = sigpath::sinkManager.bindStream("Radio");
stereoSink.setInput(stereoStream);
stereoSink.start();
}
else {
// Create and bind IQ stream
basebandStream = new dsp::stream<dsp::complex_t>();
basebandSink.setInput(basebandStream);
basebandSink.start();
sigpath::iqFrontEnd.bindIQStream(basebandStream);
}
recording = true;
}
void stop() {
std::lock_guard<std::recursive_mutex> lck(recMtx);
if (!recording) { return; }
// Close audio stream or baseband
if (recMode == RECORDER_MODE_AUDIO) {
// TODO: HAS TO BE DONE PROPERLY
stereoSink.stop();
sigpath::sinkManager.unbindStream("Radio", stereoStream);
}
else {
// Unbind and destroy IQ stream
sigpath::iqFrontEnd.unbindIQStream(basebandStream);
basebandSink.stop();
delete basebandStream;
}
// Close file
writer.close();
recording = false;
}
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;
float menuWidth = ImGui::GetContentRegionAvail().x;
// Recording mode
if (_this->recording) { style::beginDisabled(); }
ImGui::BeginGroup();
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##_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("EndRecorderModeColumns##_", _this->name), false);
ImGui::EndGroup();
if (_this->recording) { style::endDisabled(); }
// Recording path
if (_this->folderSelect.render("##_recorder_fold_" + _this->name)) {
if (_this->folderSelect.pathIsValid()) {
config.acquire();
config.conf[_this->name]["recPath"] = _this->folderSelect.path;
config.release(true);
}
}
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);
}
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();audioStream
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_", _this->name), ImVec2(menuWidth, 0))) {
_this->stop();
}
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);
}
}
void updateAudioMeter(dsp::stereo_t& lvl) {
// Note: Yes, using the natural log is on purpose, it just gives a more beautiful result.
double frameTime = 1.0 / ImGui::GetIO().Framerate;
lvl.l = std::clamp<float>(lvl.l - (frameTime * 50.0), -90.0f, 10.0f);
lvl.r = std::clamp<float>(lvl.r - (frameTime * 50.0), -90.0f, 10.0f);
// 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 > lvl.l) { lvl.l = dbLvl.l; }
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 = "") {
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;
}
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;
std::string root;
OptionList<std::string, wav::Format> formats;
OptionList<int, wav::SampleDepth> sampleDepths;
FolderSelect folderSelect;
int recMode = RECORDER_MODE_AUDIO;
int formatId;
int sampleDepthId;
bool stereo = true;
std::string selectedStreamName = "";
float audioVolume = 1.0f;
bool ignoreSilence = false;
dsp::stereo_t audioLvl = { -100.0f, -100.0f };
bool recording = false;
wav::Writer writer;
std::recursive_mutex recMtx;
dsp::stream<dsp::complex_t>* basebandStream;
dsp::stream<dsp::stereo_t>* stereoStream;
dsp::sink::Handler<dsp::complex_t> basebandSink;
dsp::sink::Handler<dsp::stereo_t> stereoSink;
dsp::sink::Handler<float> monoSink;
dsp::stream<dsp::stereo_t>* audioStream = NULL;
dsp::routing::Splitter<dsp::stereo_t> splitter;
dsp::stream<dsp::stereo_t> meterStream;
dsp::bench::PeakLevelMeter<dsp::stereo_t> meter;
uint64_t samplerate = 48000;
};
MOD_EXPORT void _INIT_() {
// Create default recording directory
std::string root = (std::string)core::args["root"];
if (!std::filesystem::exists(root + "/recordings")) {
spdlog::warn("Recordings directory does not exist, creating it");
if (!std::filesystem::create_directory(root + "/recordings")) {
spdlog::error("Could not create recordings directory");
}
}
json def = json({});
config.setPath(root + "/recorder_config.json");
config.load(def);
config.enableAutoSave();
}
MOD_EXPORT ModuleManager::Instance* _CREATE_INSTANCE_(std::string name) {
return new RecorderModule(name);
}
MOD_EXPORT void _DELETE_INSTANCE_(ModuleManager::Instance* inst) {
delete (RecorderModule*)inst;
}
MOD_EXPORT void _END_() {
config.disableAutoSave();
config.save();
}