mirror of
https://github.com/AlexandreRouma/SDRPlusPlus.git
synced 2025-04-07 07:45:40 +02:00
commit
637d683c0a
@ -52,6 +52,10 @@ public:
|
|||||||
return keys.size();
|
return keys.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool empty() {
|
||||||
|
return keys.empty();
|
||||||
|
}
|
||||||
|
|
||||||
bool keyExists(K key) {
|
bool keyExists(K key) {
|
||||||
if (std::find(keys.begin(), keys.end(), key) != keys.end()) { return true; }
|
if (std::find(keys.begin(), keys.end(), key) != keys.end()) { return true; }
|
||||||
return false;
|
return false;
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
#include <dsp/sink/handler_sink.h>
|
#include <dsp/sink/handler_sink.h>
|
||||||
#include <dsp/routing/splitter.h>
|
#include <dsp/routing/splitter.h>
|
||||||
#include <dsp/audio/volume.h>
|
#include <dsp/audio/volume.h>
|
||||||
|
#include <dsp/convert/stereo_to_mono.h>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <gui/gui.h>
|
#include <gui/gui.h>
|
||||||
@ -19,125 +20,117 @@
|
|||||||
#include <gui/widgets/folder_select.h>
|
#include <gui/widgets/folder_select.h>
|
||||||
#include <recorder_interface.h>
|
#include <recorder_interface.h>
|
||||||
#include <core.h>
|
#include <core.h>
|
||||||
|
#include <utils/optionlist.h>
|
||||||
|
#include "wav.h"
|
||||||
|
|
||||||
#define CONCAT(a, b) ((std::string(a) + b).c_str())
|
#define CONCAT(a, b) ((std::string(a) + b).c_str())
|
||||||
|
|
||||||
SDRPP_MOD_INFO{
|
SDRPP_MOD_INFO{
|
||||||
/* Name: */ "recorder",
|
/* Name: */ "recorder",
|
||||||
/* Description: */ "Recorder module for SDR++",
|
/* Description: */ "Recorder module for SDR++",
|
||||||
/* Author: */ "Ryzerth",
|
/* Author: */ "Ryzerth",
|
||||||
/* Version: */ 0, 2, 0,
|
/* Version: */ 0, 3, 0,
|
||||||
/* Max instances */ -1
|
/* Max instances */ -1
|
||||||
};
|
};
|
||||||
|
|
||||||
ConfigManager config;
|
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 {
|
class RecorderModule : public ModuleManager::Instance {
|
||||||
public:
|
public:
|
||||||
RecorderModule(std::string name) : folderSelect("%ROOT%/recordings") {
|
RecorderModule(std::string name) : folderSelect("%ROOT%/recordings") {
|
||||||
this->name = name;
|
this->name = name;
|
||||||
|
|
||||||
root = (std::string)core::args["root"];
|
root = (std::string)core::args["root"];
|
||||||
|
strcpy(nameTemplate, "$t_$f_$h-$m-$s_$d-$M-$y");
|
||||||
|
|
||||||
|
// Define option lists
|
||||||
|
containers.define("WAV", wav::FORMAT_WAV);
|
||||||
|
// containers.define("RF64", wav::FORMAT_RF64); // Disabled for now
|
||||||
|
sampleTypes.define(wav::SAMP_TYPE_UINT8, "Uint8", wav::SAMP_TYPE_UINT8);
|
||||||
|
sampleTypes.define(wav::SAMP_TYPE_INT16, "Int16", wav::SAMP_TYPE_INT16);
|
||||||
|
sampleTypes.define(wav::SAMP_TYPE_INT32, "Int32", wav::SAMP_TYPE_INT32);
|
||||||
|
sampleTypes.define(wav::SAMP_TYPE_FLOAT32, "Float32", wav::SAMP_TYPE_FLOAT32);
|
||||||
|
|
||||||
|
// Load default config for option lists
|
||||||
|
containerId = containers.valueId(wav::FORMAT_WAV);
|
||||||
|
sampleTypeId = sampleTypes.valueId(wav::SAMP_TYPE_INT16);
|
||||||
|
|
||||||
// Load config
|
// Load config
|
||||||
config.acquire();
|
config.acquire();
|
||||||
bool created = false;
|
if (config.conf[name].contains("mode")) {
|
||||||
|
recMode = config.conf[name]["mode"];
|
||||||
// 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("recPath")) {
|
||||||
if (!config.conf[name].contains("audioVolume")) {
|
folderSelect.setPath(config.conf[name]["recPath"]);
|
||||||
config.conf[name]["audioVolume"] = 1.0;
|
|
||||||
}
|
}
|
||||||
if (!config.conf[name].contains("ignoreSilence")) {
|
if (config.conf[name].contains("container") && containers.keyExists(config.conf[name]["container"])) {
|
||||||
config.conf[name]["ignoreSilence"] = false;
|
containerId = containers.keyId(config.conf[name]["container"]);
|
||||||
}
|
}
|
||||||
|
if (config.conf[name].contains("sampleType") && sampleTypes.keyExists(config.conf[name]["sampleType"])) {
|
||||||
recMode = config.conf[name]["mode"];
|
sampleTypeId = sampleTypes.keyId(config.conf[name]["sampleType"]);
|
||||||
folderSelect.setPath(config.conf[name]["recPath"]);
|
}
|
||||||
selectedStreamName = config.conf[name]["audioStream"];
|
if (config.conf[name].contains("audioStream")) {
|
||||||
audioVolume = config.conf[name]["audioVolume"];
|
selectedStreamName = config.conf[name]["audioStream"];
|
||||||
ignoreSilence = config.conf[name]["ignoreSilence"];
|
}
|
||||||
config.release(created);
|
if (config.conf[name].contains("audioVolume")) {
|
||||||
|
audioVolume = config.conf[name]["audioVolume"];
|
||||||
|
}
|
||||||
|
if (config.conf[name].contains("ignoreSilence")) {
|
||||||
|
ignoreSilence = config.conf[name]["ignoreSilence"];
|
||||||
|
}
|
||||||
|
if (config.conf[name].contains("nameTemplate")) {
|
||||||
|
std::string _nameTemplate = config.conf[name]["nameTemplate"];
|
||||||
|
if (_nameTemplate.length() > sizeof(nameTemplate)-1) {
|
||||||
|
_nameTemplate = _nameTemplate.substr(0, sizeof(nameTemplate)-1);
|
||||||
|
}
|
||||||
|
strcpy(nameTemplate, _nameTemplate.c_str());
|
||||||
|
}
|
||||||
|
config.release();
|
||||||
|
|
||||||
// Init audio path
|
// Init audio path
|
||||||
vol.init(&dummyStream, audioVolume, false);
|
volume.init(NULL, audioVolume, false);
|
||||||
audioSplit.init(&vol.out);
|
splitter.init(&volume.out);
|
||||||
audioSplit.bindStream(&meterStream);
|
splitter.bindStream(&meterStream);
|
||||||
meter.init(&meterStream);
|
meter.init(&meterStream);
|
||||||
audioHandler.init(&audioHandlerStream, _audioHandler, this);
|
s2m.init(NULL);
|
||||||
|
|
||||||
vol.start();
|
// Init sinks
|
||||||
audioSplit.start();
|
basebandSink.init(NULL, complexHandler, this);
|
||||||
meter.start();
|
stereoSink.init(NULL, stereoHandler, this);
|
||||||
|
monoSink.init(&s2m.out, monoHandler, this);
|
||||||
// Init baseband path
|
|
||||||
basebandHandler.init(&basebandStream, _basebandHandler, this);
|
|
||||||
|
|
||||||
wavSampleBuf = new int16_t[2 * STREAM_BUFFER_SIZE];
|
|
||||||
|
|
||||||
gui::menu.registerEntry(name, menuHandler, this);
|
gui::menu.registerEntry(name, menuHandler, this);
|
||||||
core::modComManager.registerInterface("recorder", name, moduleInterfaceHandler, 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() {
|
~RecorderModule() {
|
||||||
std::lock_guard lck(recMtx);
|
std::lock_guard<std::recursive_mutex> lck(recMtx);
|
||||||
gui::menu.removeEntry(name);
|
|
||||||
core::modComManager.unregisterInterface(name);
|
core::modComManager.unregisterInterface(name);
|
||||||
|
gui::menu.removeEntry(name);
|
||||||
// Stop recording
|
stop();
|
||||||
if (recording) { stopRecording(); }
|
deselectStream();
|
||||||
|
sigpath::sinkManager.onStreamRegistered.unbindHandler(&onStreamRegisteredHandler);
|
||||||
vol.setInput(&dummyStream);
|
sigpath::sinkManager.onStreamUnregister.unbindHandler(&onStreamUnregisterHandler);
|
||||||
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();
|
meter.stop();
|
||||||
|
|
||||||
delete[] wavSampleBuf;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void postInit() {
|
void postInit() {
|
||||||
refreshStreams();
|
// Enumerate streams
|
||||||
if (selectedStreamName == "") {
|
audioStreams.clear();
|
||||||
selectStream(streamNames[0]);
|
auto names = sigpath::sinkManager.getStreamNames();
|
||||||
}
|
for (const auto& name : names) {
|
||||||
else {
|
audioStreams.define(name, name, name);
|
||||||
selectStream(selectedStreamName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bind stream register/unregister handlers
|
||||||
|
onStreamRegisteredHandler.ctx = this;
|
||||||
|
onStreamRegisteredHandler.handler = streamRegisteredHandler;
|
||||||
|
sigpath::sinkManager.onStreamRegistered.bindHandler(&onStreamRegisteredHandler);
|
||||||
|
onStreamUnregisterHandler.ctx = this;
|
||||||
|
onStreamUnregisterHandler.handler = streamUnregisterHandler;
|
||||||
|
sigpath::sinkManager.onStreamUnregister.bindHandler(&onStreamUnregisterHandler);
|
||||||
|
|
||||||
|
// Select the stream
|
||||||
|
selectStream(selectedStreamName);
|
||||||
}
|
}
|
||||||
|
|
||||||
void enable() {
|
void enable() {
|
||||||
@ -152,70 +145,106 @@ public:
|
|||||||
return enabled;
|
return enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void start() {
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(recMtx);
|
||||||
|
if (recording) { return; }
|
||||||
|
|
||||||
|
// Configure the wav writer
|
||||||
|
if (recMode == RECORDER_MODE_AUDIO) {
|
||||||
|
if (selectedStreamName.empty()) { return; }
|
||||||
|
samplerate = sigpath::sinkManager.getStreamSampleRate(selectedStreamName);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
samplerate = sigpath::iqFrontEnd.getSampleRate();
|
||||||
|
}
|
||||||
|
writer.setFormat(containers[containerId]);
|
||||||
|
writer.setChannels((recMode == RECORDER_MODE_AUDIO && !stereo) ? 1 : 2);
|
||||||
|
writer.setSampleType(sampleTypes[sampleTypeId]);
|
||||||
|
writer.setSamplerate(samplerate);
|
||||||
|
|
||||||
|
// Open file
|
||||||
|
std::string type = (recMode == RECORDER_MODE_AUDIO) ? "audio" : "baseband";
|
||||||
|
std::string vfoName = (recMode == RECORDER_MODE_AUDIO) ? gui::waterfall.selectedVFO : "";
|
||||||
|
std::string extension = ".wav";
|
||||||
|
std::string expandedPath = expandString(folderSelect.path + "/" + genFileName(nameTemplate, type, vfoName) + extension);
|
||||||
|
if (!writer.open(expandedPath)) {
|
||||||
|
spdlog::error("Failed to open file for recording: {0}", expandedPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open audio stream or baseband
|
||||||
|
if (recMode == RECORDER_MODE_AUDIO) {
|
||||||
|
// TODO: Select the stereo to mono converter if needed
|
||||||
|
stereoStream = sigpath::sinkManager.bindStream(selectedStreamName);
|
||||||
|
if (stereo) {
|
||||||
|
stereoSink.setInput(stereoStream);
|
||||||
|
stereoSink.start();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
s2m.setInput(stereoStream);
|
||||||
|
s2m.start();
|
||||||
|
monoSink.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Create and bind IQ stream
|
||||||
|
basebandStream = new dsp::stream<dsp::complex_t>();
|
||||||
|
basebandSink.setInput(basebandStream);
|
||||||
|
basebandSink.start();
|
||||||
|
sigpath::iqFrontEnd.bindIQStream(basebandStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
recording = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(recMtx);
|
||||||
|
if (!recording) { return; }
|
||||||
|
|
||||||
|
// Close audio stream or baseband
|
||||||
|
if (recMode == RECORDER_MODE_AUDIO) {
|
||||||
|
// NOTE: Has to be done before the unbind since the stream is deleted...
|
||||||
|
monoSink.stop();
|
||||||
|
stereoSink.stop();
|
||||||
|
s2m.stop();
|
||||||
|
sigpath::sinkManager.unbindStream(selectedStreamName, stereoStream);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Unbind and destroy IQ stream
|
||||||
|
sigpath::iqFrontEnd.unbindIQStream(basebandStream);
|
||||||
|
basebandSink.stop();
|
||||||
|
delete basebandStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close file
|
||||||
|
writer.close();
|
||||||
|
|
||||||
|
recording = false;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void refreshStreams() {
|
|
||||||
std::vector<std::string> names = sigpath::sinkManager.getStreamNames();
|
|
||||||
|
|
||||||
streamNames.clear();
|
|
||||||
streamNamesTxt = "";
|
|
||||||
|
|
||||||
// If there are no stream, cancel
|
|
||||||
if (names.size() == 0) { return; }
|
|
||||||
|
|
||||||
// List streams
|
|
||||||
for (auto const& name : names) {
|
|
||||||
streamNames.push_back(name);
|
|
||||||
streamNamesTxt += name;
|
|
||||||
streamNamesTxt += '\0';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void selectStream(std::string name) {
|
|
||||||
if (streamNames.empty()) {
|
|
||||||
selectedStreamName = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto it = std::find(streamNames.begin(), streamNames.end(), name);
|
|
||||||
if (it == streamNames.end()) {
|
|
||||||
selectStream(streamNames[0]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
streamId = std::distance(streamNames.begin(), it);
|
|
||||||
|
|
||||||
vol.stop();
|
|
||||||
if (audioInput != NULL) { sigpath::sinkManager.unbindStream(selectedStreamName, audioInput); }
|
|
||||||
audioInput = sigpath::sinkManager.bindStream(name);
|
|
||||||
if (audioInput == NULL) {
|
|
||||||
selectedStreamName = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
selectedStreamName = name;
|
|
||||||
vol.setInput(audioInput);
|
|
||||||
vol.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void menuHandler(void* ctx) {
|
static void menuHandler(void* ctx) {
|
||||||
RecorderModule* _this = (RecorderModule*)ctx;
|
RecorderModule* _this = (RecorderModule*)ctx;
|
||||||
float menuColumnWidth = ImGui::GetContentRegionAvail().x;
|
float menuWidth = ImGui::GetContentRegionAvail().x;
|
||||||
|
|
||||||
// Recording mode
|
// Recording mode
|
||||||
if (_this->recording) { style::beginDisabled(); }
|
if (_this->recording) { style::beginDisabled(); }
|
||||||
ImGui::BeginGroup();
|
ImGui::BeginGroup();
|
||||||
ImGui::Columns(2, CONCAT("AirspyGainModeColumns##_", _this->name), false);
|
ImGui::Columns(2, CONCAT("RecorderModeColumns##_", _this->name), false);
|
||||||
if (ImGui::RadioButton(CONCAT("Baseband##_recmode_", _this->name), _this->recMode == RECORDER_MODE_BASEBAND)) {
|
if (ImGui::RadioButton(CONCAT("Baseband##_recorder_mode_", _this->name), _this->recMode == RECORDER_MODE_BASEBAND)) {
|
||||||
_this->recMode = RECORDER_MODE_BASEBAND;
|
_this->recMode = RECORDER_MODE_BASEBAND;
|
||||||
config.acquire();
|
config.acquire();
|
||||||
config.conf[_this->name]["mode"] = _this->recMode;
|
config.conf[_this->name]["mode"] = _this->recMode;
|
||||||
config.release(true);
|
config.release(true);
|
||||||
}
|
}
|
||||||
ImGui::NextColumn();
|
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;
|
_this->recMode = RECORDER_MODE_AUDIO;
|
||||||
config.acquire();
|
config.acquire();
|
||||||
config.conf[_this->name]["mode"] = _this->recMode;
|
config.conf[_this->name]["mode"] = _this->recMode;
|
||||||
config.release(true);
|
config.release(true);
|
||||||
}
|
}
|
||||||
ImGui::Columns(1, CONCAT("EndAirspyGainModeColumns##_", _this->name), false);
|
ImGui::Columns(1, CONCAT("EndRecorderModeColumns##_", _this->name), false);
|
||||||
ImGui::EndGroup();
|
ImGui::EndGroup();
|
||||||
if (_this->recording) { style::endDisabled(); }
|
if (_this->recording) { style::endDisabled(); }
|
||||||
|
|
||||||
@ -228,116 +257,232 @@ private:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mode specific menu
|
ImGui::LeftLabel("Name template");
|
||||||
if (_this->recMode == RECORDER_MODE_AUDIO) {
|
ImGui::FillWidth();
|
||||||
_this->audioMenu(menuColumnWidth);
|
if (ImGui::InputText(CONCAT("##_recorder_name_template_", _this->name), _this->nameTemplate, 1023)) {
|
||||||
|
config.acquire();
|
||||||
|
config.conf[_this->name]["nameTemplate"] = _this->nameTemplate;
|
||||||
|
config.release(true);
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
_this->basebandMenu(menuColumnWidth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void basebandMenu(float menuColumnWidth) {
|
ImGui::LeftLabel("Container");
|
||||||
if (!folderSelect.pathIsValid()) { style::beginDisabled(); }
|
ImGui::FillWidth();
|
||||||
if (!recording) {
|
if (ImGui::Combo(CONCAT("##_recorder_container_", _this->name), &_this->containerId, _this->containers.txt)) {
|
||||||
if (ImGui::Button(CONCAT("Record##_recorder_rec_", name), ImVec2(menuColumnWidth, 0))) {
|
config.acquire();
|
||||||
std::lock_guard lck(recMtx);
|
config.conf[_this->name]["container"] = _this->containers.key(_this->containerId);
|
||||||
startRecording();
|
config.release(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::LeftLabel("Sample type");
|
||||||
|
ImGui::FillWidth();
|
||||||
|
if (ImGui::Combo(CONCAT("##_recorder_st_", _this->name), &_this->sampleTypeId, _this->sampleTypes.txt)) {
|
||||||
|
config.acquire();
|
||||||
|
config.conf[_this->name]["sampleType"] = _this->sampleTypes.key(_this->sampleTypeId);
|
||||||
|
config.release(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show additional audio options
|
||||||
|
if (_this->recMode == RECORDER_MODE_AUDIO) {
|
||||||
|
ImGui::LeftLabel("Stream");
|
||||||
|
ImGui::FillWidth();
|
||||||
|
if (ImGui::Combo(CONCAT("##_recorder_stream_", _this->name), &_this->streamId, _this->audioStreams.txt)) {
|
||||||
|
_this->selectStream(_this->audioStreams.value(_this->streamId));
|
||||||
|
config.acquire();
|
||||||
|
config.conf[_this->name]["audioStream"] = _this->audioStreams.key(_this->streamId);
|
||||||
|
config.release(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
_this->updateAudioMeter(_this->audioLvl);
|
||||||
|
ImGui::FillWidth();
|
||||||
|
ImGui::VolumeMeter(_this->audioLvl.l, _this->audioLvl.l, -60, 10);
|
||||||
|
ImGui::FillWidth();
|
||||||
|
ImGui::VolumeMeter(_this->audioLvl.r, _this->audioLvl.r, -60, 10);
|
||||||
|
|
||||||
|
ImGui::FillWidth();
|
||||||
|
if (ImGui::SliderFloat(CONCAT("##_recorder_vol_", _this->name), &_this->audioVolume, 0, 1, "")) {
|
||||||
|
_this->volume.setVolume(_this->audioVolume);
|
||||||
|
config.acquire();
|
||||||
|
config.conf[_this->name]["audioVolume"] = _this->audioVolume;
|
||||||
|
config.release(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_this->recording) { style::beginDisabled(); }
|
||||||
|
if (ImGui::Checkbox(CONCAT("Stereo##_recorder_stereo_", _this->name), &_this->stereo)) {
|
||||||
|
config.acquire();
|
||||||
|
config.conf[_this->name]["stereo"] = _this->stereo;
|
||||||
|
config.release(true);
|
||||||
|
}
|
||||||
|
if (_this->recording) { style::endDisabled(); }
|
||||||
|
|
||||||
|
if (ImGui::Checkbox(CONCAT("Ignore silence##_recorder_ignore_silence_", _this->name), &_this->ignoreSilence)) {
|
||||||
|
config.acquire();
|
||||||
|
config.conf[_this->name]["ignoreSilence"] = _this->ignoreSilence;
|
||||||
|
config.release(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record button
|
||||||
|
bool canRecord = _this->folderSelect.pathIsValid();
|
||||||
|
if (_this->recMode == RECORDER_MODE_AUDIO) { canRecord &= !_this->selectedStreamName.empty(); }
|
||||||
|
if (!_this->recording) {
|
||||||
|
if (ImGui::Button(CONCAT("Record##_recorder_rec_", _this->name), ImVec2(menuWidth, 0))) {
|
||||||
|
_this->start();
|
||||||
}
|
}
|
||||||
ImGui::TextColored(ImGui::GetStyleColorVec4(ImGuiCol_Text), "Idle --:--:--");
|
ImGui::TextColored(ImGui::GetStyleColorVec4(ImGuiCol_Text), "Idle --:--:--");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (ImGui::Button(CONCAT("Stop##_recorder_rec_", name), ImVec2(menuColumnWidth, 0))) {
|
if (ImGui::Button(CONCAT("Stop##_recorder_rec_", _this->name), ImVec2(menuWidth, 0))) {
|
||||||
std::lock_guard lck(recMtx);
|
_this->stop();
|
||||||
stopRecording();
|
|
||||||
}
|
}
|
||||||
uint64_t seconds = samplesWritten / (uint64_t)sampleRate;
|
uint64_t seconds = _this->writer.getSamplesWritten() / _this->samplerate;
|
||||||
time_t diff = seconds;
|
time_t diff = seconds;
|
||||||
tm* dtm = gmtime(&diff);
|
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);
|
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) {
|
void selectStream(std::string name) {
|
||||||
ImGui::PushItemWidth(menuColumnWidth);
|
std::lock_guard<std::recursive_mutex> lck(recMtx);
|
||||||
|
deselectStream();
|
||||||
|
|
||||||
if (streamNames.size() == 0) {
|
if (audioStreams.empty()) {
|
||||||
|
selectedStreamName.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (!audioStreams.keyExists(name)) {
|
||||||
|
selectStream(audioStreams.key(0));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recording) { style::beginDisabled(); }
|
audioStream = sigpath::sinkManager.bindStream(name);
|
||||||
if (ImGui::Combo(CONCAT("##_recorder_strm_", name), &streamId, streamNamesTxt.c_str())) {
|
if (!audioStream) { return; }
|
||||||
selectStream(streamNames[streamId]);
|
selectedStreamName = name;
|
||||||
config.acquire();
|
streamId = audioStreams.keyId(name);
|
||||||
config.conf[name]["audioStream"] = streamNames[streamId];
|
volume.setInput(audioStream);
|
||||||
config.release(true);
|
startAudioPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
void deselectStream() {
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(recMtx);
|
||||||
|
if (selectedStreamName.empty() || !audioStream) {
|
||||||
|
selectedStreamName.clear();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (recording) { style::endDisabled(); }
|
if (recording && recMode == RECORDER_MODE_AUDIO) { stop(); }
|
||||||
|
stopAudioPath();
|
||||||
|
sigpath::sinkManager.unbindStream(selectedStreamName, audioStream);
|
||||||
|
selectedStreamName.clear();
|
||||||
|
audioStream = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
double frameTime = 1.0 / ImGui::GetIO().Framerate;
|
void startAudioPath() {
|
||||||
lvlL = std::clamp<float>(lvlL - (frameTime * 50.0), -90.0f, 10.0f);
|
volume.start();
|
||||||
lvlR = std::clamp<float>(lvlR - (frameTime * 50.0), -90.0f, 10.0f);
|
splitter.start();
|
||||||
|
meter.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void stopAudioPath() {
|
||||||
|
volume.stop();
|
||||||
|
splitter.stop();
|
||||||
|
meter.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void streamRegisteredHandler(std::string name, void* ctx) {
|
||||||
|
RecorderModule* _this = (RecorderModule*)ctx;
|
||||||
|
|
||||||
|
// Add new stream to the list
|
||||||
|
_this->audioStreams.define(name, name, name);
|
||||||
|
|
||||||
|
// If no stream is selected, select new stream. If not, update the menu ID.
|
||||||
|
if (_this->selectedStreamName.empty()) {
|
||||||
|
_this->selectStream(name);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_this->streamId = _this->audioStreams.keyId(_this->selectedStreamName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void streamUnregisterHandler(std::string name, void* ctx) {
|
||||||
|
RecorderModule* _this = (RecorderModule*)ctx;
|
||||||
|
|
||||||
|
// Remove stream from list
|
||||||
|
_this->audioStreams.undefineKey(name);
|
||||||
|
|
||||||
|
// If the stream is in used, deselect it and reselect default. Otherwise, update ID.
|
||||||
|
if (_this->selectedStreamName == name) {
|
||||||
|
_this->selectStream("");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_this->streamId = _this->audioStreams.keyId(_this->selectedStreamName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateAudioMeter(dsp::stereo_t& lvl) {
|
||||||
// Note: Yes, using the natural log is on purpose, it just gives a more beautiful result.
|
// Note: Yes, using the natural log is on purpose, it just gives a more beautiful result.
|
||||||
|
double frameTime = 1.0 / ImGui::GetIO().Framerate;
|
||||||
|
lvl.l = std::clamp<float>(lvl.l - (frameTime * 50.0), -90.0f, 10.0f);
|
||||||
|
lvl.r = std::clamp<float>(lvl.r - (frameTime * 50.0), -90.0f, 10.0f);
|
||||||
dsp::stereo_t rawLvl = meter.getLevel();
|
dsp::stereo_t rawLvl = meter.getLevel();
|
||||||
meter.resetLevel();
|
meter.resetLevel();
|
||||||
dsp::stereo_t dbLvl = { 10.0f * logf(rawLvl.l), 10.0f * logf(rawLvl.r) };
|
dsp::stereo_t dbLvl = { 10.0f * logf(rawLvl.l), 10.0f * logf(rawLvl.r) };
|
||||||
if (dbLvl.l > lvlL) { lvlL = dbLvl.l; }
|
if (dbLvl.l > lvl.l) { lvl.l = dbLvl.l; }
|
||||||
if (dbLvl.r > lvlR) { lvlR = dbLvl.r; }
|
if (dbLvl.r > lvl.r) { lvl.r = 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(); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void _audioHandler(dsp::stereo_t* data, int count, void* ctx) {
|
std::string genFileName(std::string templ, std::string type, std::string name) {
|
||||||
RecorderModule* _this = (RecorderModule*)ctx;
|
// Get data
|
||||||
if (_this->ignoreSilence && data[0].l == 0.0f && data[0].r == 0.0f) {
|
time_t now = time(0);
|
||||||
return;
|
tm* ltm = localtime(&now);
|
||||||
|
char buf[1024];
|
||||||
|
double freq = gui::waterfall.getCenterFrequency();
|
||||||
|
if (gui::waterfall.vfos.find(name) != gui::waterfall.vfos.end()) {
|
||||||
|
freq += gui::waterfall.vfos[name]->generalOffset;
|
||||||
}
|
}
|
||||||
volk_32f_s32f_convert_16i(_this->wavSampleBuf, (float*)data, 32767.0f, count * 2);
|
|
||||||
_this->audioWriter->writeSamples(_this->wavSampleBuf, count * 2 * sizeof(int16_t));
|
// Format to string
|
||||||
_this->samplesWritten += count;
|
char freqStr[128];
|
||||||
|
char hourStr[128];
|
||||||
|
char minStr[128];
|
||||||
|
char secStr[128];
|
||||||
|
char dayStr[128];
|
||||||
|
char monStr[128];
|
||||||
|
char yearStr[128];
|
||||||
|
sprintf(freqStr, "%.0lfHz", freq);
|
||||||
|
sprintf(hourStr, "%02d", ltm->tm_hour);
|
||||||
|
sprintf(minStr, "%02d", ltm->tm_min);
|
||||||
|
sprintf(secStr, "%02d", ltm->tm_sec);
|
||||||
|
sprintf(dayStr, "%02d", ltm->tm_mday);
|
||||||
|
sprintf(monStr, "%02d", ltm->tm_mon + 1);
|
||||||
|
sprintf(yearStr, "%02d", ltm->tm_year + 1900);
|
||||||
|
|
||||||
|
// Replace in template
|
||||||
|
templ = std::regex_replace(templ, std::regex("\\$t"), type);
|
||||||
|
templ = std::regex_replace(templ, std::regex("\\$f"), freqStr);
|
||||||
|
templ = std::regex_replace(templ, std::regex("\\$h"), hourStr);
|
||||||
|
templ = std::regex_replace(templ, std::regex("\\$m"), minStr);
|
||||||
|
templ = std::regex_replace(templ, std::regex("\\$s"), secStr);
|
||||||
|
templ = std::regex_replace(templ, std::regex("\\$d"), dayStr);
|
||||||
|
templ = std::regex_replace(templ, std::regex("\\$M"), monStr);
|
||||||
|
templ = std::regex_replace(templ, std::regex("\\$y"), yearStr);
|
||||||
|
return templ;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void _basebandHandler(dsp::complex_t* data, int count, void* ctx) {
|
std::string expandString(std::string input) {
|
||||||
|
input = std::regex_replace(input, std::regex("%ROOT%"), root);
|
||||||
|
return std::regex_replace(input, std::regex("//"), "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void complexHandler(dsp::complex_t* data, int count, void* ctx) {
|
||||||
|
monoHandler((float*)data, count, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void stereoHandler(dsp::stereo_t* data, int count, void* ctx) {
|
||||||
|
monoHandler((float*)data, count, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void monoHandler(float* data, int count, void* ctx) {
|
||||||
RecorderModule* _this = (RecorderModule*)ctx;
|
RecorderModule* _this = (RecorderModule*)ctx;
|
||||||
volk_32f_s32f_convert_16i(_this->wavSampleBuf, (float*)data, 32767.0f, count * 2);
|
_this->writer.write(data, count);
|
||||||
_this->basebandWriter->writeSamples(_this->wavSampleBuf, count * 2 * sizeof(int16_t));
|
|
||||||
_this->samplesWritten += count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void moduleInterfaceHandler(int code, void* in, void* out, void* ctx) {
|
static void moduleInterfaceHandler(int code, void* in, void* out, void* ctx) {
|
||||||
@ -353,187 +498,54 @@ private:
|
|||||||
_this->recMode = std::clamp<int>(*_in, 0, 1);
|
_this->recMode = std::clamp<int>(*_in, 0, 1);
|
||||||
}
|
}
|
||||||
else if (code == RECORDER_IFACE_CMD_START) {
|
else if (code == RECORDER_IFACE_CMD_START) {
|
||||||
if (!_this->recording) { _this->startRecording(); }
|
if (!_this->recording) { _this->start(); }
|
||||||
}
|
}
|
||||||
else if (code == RECORDER_IFACE_CMD_STOP) {
|
else if (code == RECORDER_IFACE_CMD_STOP) {
|
||||||
if (_this->recording) { _this->stopRecording(); }
|
if (_this->recording) { _this->stop(); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void startRecording() {
|
|
||||||
if (recMode == RECORDER_MODE_BASEBAND) {
|
|
||||||
samplesWritten = 0;
|
|
||||||
std::string expandedPath = expandString(folderSelect.path + genFileName("/baseband_", false));
|
|
||||||
sampleRate = sigpath::iqFrontEnd.getSampleRate();
|
|
||||||
basebandWriter = new WavWriter(expandedPath, 16, 2, sigpath::iqFrontEnd.getSampleRate());
|
|
||||||
if (basebandWriter->isOpen()) {
|
|
||||||
basebandHandler.start();
|
|
||||||
sigpath::iqFrontEnd.bindIQStream(&basebandStream);
|
|
||||||
recording = true;
|
|
||||||
spdlog::info("Recording to '{0}'", expandedPath);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
spdlog::error("Could not create '{0}'", expandedPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (recMode == RECORDER_MODE_AUDIO) {
|
|
||||||
if (selectedStreamName.empty()) {
|
|
||||||
spdlog::error("Cannot record with no selected stream");
|
|
||||||
}
|
|
||||||
samplesWritten = 0;
|
|
||||||
std::string expandedPath = expandString(folderSelect.path + genFileName("/audio_", true, selectedStreamName));
|
|
||||||
sampleRate = sigpath::sinkManager.getStreamSampleRate(selectedStreamName);
|
|
||||||
audioWriter = new WavWriter(expandedPath, 16, 2, sigpath::sinkManager.getStreamSampleRate(selectedStreamName));
|
|
||||||
if (audioWriter->isOpen()) {
|
|
||||||
recording = true;
|
|
||||||
audioHandler.start();
|
|
||||||
audioSplit.bindStream(&audioHandlerStream);
|
|
||||||
spdlog::info("Recording to '{0}'", expandedPath);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
spdlog::error("Could not create '{0}'", expandedPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void stopRecording() {
|
|
||||||
if (recMode == 0) {
|
|
||||||
recording = false;
|
|
||||||
sigpath::iqFrontEnd.unbindIQStream(&basebandStream);
|
|
||||||
basebandHandler.stop();
|
|
||||||
basebandWriter->close();
|
|
||||||
delete basebandWriter;
|
|
||||||
}
|
|
||||||
else if (recMode == 1) {
|
|
||||||
recording = false;
|
|
||||||
audioSplit.unbindStream(&audioHandlerStream);
|
|
||||||
audioHandler.stop();
|
|
||||||
audioWriter->close();
|
|
||||||
delete audioWriter;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void onStreamRegistered(std::string name, void* ctx) {
|
|
||||||
RecorderModule* _this = (RecorderModule*)ctx;
|
|
||||||
_this->refreshStreams();
|
|
||||||
|
|
||||||
if (_this->streamNames.empty()) {
|
|
||||||
_this->selectedStreamName = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_this->selectedStreamName.empty()) {
|
|
||||||
_this->selectStream(_this->streamNames[0]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reselect stream in UI to make sure the ID is correct
|
|
||||||
int id = 0;
|
|
||||||
for (auto& str : _this->streamNames) {
|
|
||||||
if (str == _this->selectedStreamName) {
|
|
||||||
_this->streamId = id;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
id++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void onStreamUnregister(std::string name, void* ctx) {
|
|
||||||
RecorderModule* _this = (RecorderModule*)ctx;
|
|
||||||
if (name != _this->selectedStreamName) { return; }
|
|
||||||
if (_this->recording) { _this->stopRecording(); }
|
|
||||||
if (_this->audioInput != NULL) {
|
|
||||||
_this->vol.setInput(&_this->dummyStream);
|
|
||||||
sigpath::sinkManager.unbindStream(_this->selectedStreamName, _this->audioInput);
|
|
||||||
_this->audioInput = NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void onStreamUnregistered(std::string name, void* ctx) {
|
|
||||||
RecorderModule* _this = (RecorderModule*)ctx;
|
|
||||||
_this->refreshStreams();
|
|
||||||
|
|
||||||
if (_this->streamNames.empty()) {
|
|
||||||
_this->selectedStreamName = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If current stream was deleted, reselect steam completely
|
|
||||||
if (name == _this->selectedStreamName) {
|
|
||||||
_this->streamId = std::clamp<int>(_this->streamId, 0, _this->streamNames.size() - 1);
|
|
||||||
_this->selectStream(_this->streamNames[_this->streamId]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reselect stream in UI to make sure the ID is correct
|
|
||||||
int id = 0;
|
|
||||||
for (auto& str : _this->streamNames) {
|
|
||||||
if (str == _this->selectedStreamName) {
|
|
||||||
_this->streamId = id;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
id++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string expandString(std::string input) {
|
|
||||||
input = std::regex_replace(input, std::regex("%ROOT%"), root);
|
|
||||||
return std::regex_replace(input, std::regex("//"), "/");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
std::string name;
|
std::string name;
|
||||||
bool enabled = true;
|
bool enabled = true;
|
||||||
|
std::string root;
|
||||||
|
char nameTemplate[1024];
|
||||||
|
|
||||||
int recMode = 1;
|
OptionList<std::string, wav::Format> containers;
|
||||||
bool recording = false;
|
OptionList<int, wav::SampleType> sampleTypes;
|
||||||
|
|
||||||
float audioVolume = 1.0f;
|
|
||||||
|
|
||||||
double sampleRate = 48000;
|
|
||||||
|
|
||||||
float lvlL = -90.0f;
|
|
||||||
float lvlR = -90.0f;
|
|
||||||
|
|
||||||
dsp::stream<dsp::stereo_t> dummyStream;
|
|
||||||
|
|
||||||
std::mutex recMtx;
|
|
||||||
|
|
||||||
FolderSelect folderSelect;
|
FolderSelect folderSelect;
|
||||||
|
|
||||||
// Audio path
|
int recMode = RECORDER_MODE_AUDIO;
|
||||||
dsp::stream<dsp::stereo_t>* audioInput = NULL;
|
int containerId;
|
||||||
dsp::audio::Volume vol;
|
int sampleTypeId;
|
||||||
dsp::routing::Splitter<dsp::stereo_t> audioSplit;
|
bool stereo = true;
|
||||||
|
std::string selectedStreamName = "";
|
||||||
|
float audioVolume = 1.0f;
|
||||||
|
bool ignoreSilence = false;
|
||||||
|
dsp::stereo_t audioLvl = { -100.0f, -100.0f };
|
||||||
|
|
||||||
|
bool recording = false;
|
||||||
|
wav::Writer writer;
|
||||||
|
std::recursive_mutex recMtx;
|
||||||
|
dsp::stream<dsp::complex_t>* basebandStream;
|
||||||
|
dsp::stream<dsp::stereo_t>* stereoStream;
|
||||||
|
dsp::sink::Handler<dsp::complex_t> basebandSink;
|
||||||
|
dsp::sink::Handler<dsp::stereo_t> stereoSink;
|
||||||
|
dsp::sink::Handler<float> monoSink;
|
||||||
|
|
||||||
|
OptionList<std::string, std::string> audioStreams;
|
||||||
|
int streamId = 0;
|
||||||
|
dsp::stream<dsp::stereo_t>* audioStream = NULL;
|
||||||
|
dsp::audio::Volume volume;
|
||||||
|
dsp::routing::Splitter<dsp::stereo_t> splitter;
|
||||||
dsp::stream<dsp::stereo_t> meterStream;
|
dsp::stream<dsp::stereo_t> meterStream;
|
||||||
dsp::bench::PeakLevelMeter<dsp::stereo_t> meter;
|
dsp::bench::PeakLevelMeter<dsp::stereo_t> meter;
|
||||||
dsp::stream<dsp::stereo_t> audioHandlerStream;
|
dsp::convert::StereoToMono s2m;
|
||||||
dsp::sink::Handler<dsp::stereo_t> audioHandler;
|
|
||||||
WavWriter* audioWriter;
|
|
||||||
|
|
||||||
std::vector<std::string> streamNames;
|
uint64_t samplerate = 48000;
|
||||||
std::string streamNamesTxt;
|
|
||||||
int streamId = 0;
|
|
||||||
std::string selectedStreamName = "";
|
|
||||||
std::string root;
|
|
||||||
|
|
||||||
// Baseband path
|
EventHandler<std::string> onStreamRegisteredHandler;
|
||||||
dsp::stream<dsp::complex_t> basebandStream;
|
EventHandler<std::string> onStreamUnregisterHandler;
|
||||||
dsp::sink::Handler<dsp::complex_t> basebandHandler;
|
|
||||||
WavWriter* basebandWriter;
|
|
||||||
|
|
||||||
uint64_t samplesWritten;
|
|
||||||
int16_t* wavSampleBuf;
|
|
||||||
|
|
||||||
EventHandler<std::string> streamRegisteredHandler;
|
|
||||||
EventHandler<std::string> streamUnregisterHandler;
|
|
||||||
EventHandler<std::string> streamUnregisteredHandler;
|
|
||||||
|
|
||||||
bool ignoreSilence = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct RecorderContext_t {
|
|
||||||
std::string name;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
MOD_EXPORT void _INIT_() {
|
MOD_EXPORT void _INIT_() {
|
||||||
@ -559,7 +571,7 @@ MOD_EXPORT void _DELETE_INSTANCE_(ModuleManager::Instance* inst) {
|
|||||||
delete (RecorderModule*)inst;
|
delete (RecorderModule*)inst;
|
||||||
}
|
}
|
||||||
|
|
||||||
MOD_EXPORT void _END_(RecorderContext_t* ctx) {
|
MOD_EXPORT void _END_() {
|
||||||
config.disableAutoSave();
|
config.disableAutoSave();
|
||||||
config.save();
|
config.save();
|
||||||
}
|
}
|
134
misc_modules/recorder/src/riff.cpp
Normal file
134
misc_modules/recorder/src/riff.cpp
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
#include "riff.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace riff {
|
||||||
|
const char* RIFF_SIGNATURE = "RIFF";
|
||||||
|
const char* LIST_SIGNATURE = "LIST";
|
||||||
|
const size_t RIFF_LABEL_SIZE = 4;
|
||||||
|
|
||||||
|
bool Writer::open(std::string path, const char form[4]) {
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||||
|
|
||||||
|
// Open file
|
||||||
|
file = std::ofstream(path, std::ios::out | std::ios::binary);
|
||||||
|
if (!file.is_open()) { return false; }
|
||||||
|
|
||||||
|
// Begin RIFF chunk
|
||||||
|
beginRIFF(form);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Writer::isOpen() {
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||||
|
return file.is_open();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Writer::close() {
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||||
|
|
||||||
|
if (!isOpen()) { return; }
|
||||||
|
|
||||||
|
// Finalize RIFF chunk
|
||||||
|
endRIFF();
|
||||||
|
|
||||||
|
// Close file
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Writer::beginList(const char id[4]) {
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||||
|
|
||||||
|
// Create chunk with the LIST ID and write id
|
||||||
|
beginChunk(LIST_SIGNATURE);
|
||||||
|
write((uint8_t*)id, RIFF_LABEL_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Writer::endList() {
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||||
|
|
||||||
|
if (chunks.empty()) {
|
||||||
|
throw std::runtime_error("No chunk to end");
|
||||||
|
}
|
||||||
|
if (memcmp(chunks.top().hdr.id, LIST_SIGNATURE, RIFF_LABEL_SIZE)) {
|
||||||
|
throw std::runtime_error("Top chunk not LIST chunk");
|
||||||
|
}
|
||||||
|
|
||||||
|
endChunk();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Writer::beginChunk(const char id[4]) {
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||||
|
|
||||||
|
// Create and write header
|
||||||
|
ChunkDesc desc;
|
||||||
|
desc.pos = file.tellp();
|
||||||
|
memcpy(desc.hdr.id, id, sizeof(desc.hdr.id));
|
||||||
|
desc.hdr.size = 0;
|
||||||
|
file.write((char*)&desc.hdr, sizeof(ChunkHeader));
|
||||||
|
|
||||||
|
// Save descriptor
|
||||||
|
chunks.push(desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Writer::endChunk() {
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||||
|
|
||||||
|
if (chunks.empty()) {
|
||||||
|
throw std::runtime_error("No chunk to end");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get descriptor
|
||||||
|
ChunkDesc desc = chunks.top();
|
||||||
|
chunks.pop();
|
||||||
|
|
||||||
|
// Write size
|
||||||
|
auto pos = file.tellp();
|
||||||
|
auto npos = desc.pos;
|
||||||
|
npos += 4;
|
||||||
|
file.seekp(npos);
|
||||||
|
file.write((char*)&desc.hdr.size, sizeof(desc.hdr.size));
|
||||||
|
file.seekp(pos);
|
||||||
|
|
||||||
|
// If parent chunk, increment its size
|
||||||
|
if (!chunks.empty()) {
|
||||||
|
chunks.top().hdr.size += desc.hdr.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Writer::write(const uint8_t* data, size_t len) {
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||||
|
|
||||||
|
if (chunks.empty()) {
|
||||||
|
throw std::runtime_error("No chunk to write into");
|
||||||
|
}
|
||||||
|
file.write((char*)data, len);
|
||||||
|
chunks.top().hdr.size += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Writer::beginRIFF(const char form[4]) {
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||||
|
|
||||||
|
if (!chunks.empty()) {
|
||||||
|
throw std::runtime_error("Can't create RIFF chunk on an existing RIFF file");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create chunk with RIFF ID and write form
|
||||||
|
beginChunk(RIFF_SIGNATURE);
|
||||||
|
write((uint8_t*)form, RIFF_LABEL_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Writer::endRIFF() {
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||||
|
|
||||||
|
if (chunks.empty()) {
|
||||||
|
throw std::runtime_error("No chunk to end");
|
||||||
|
}
|
||||||
|
if (memcmp(chunks.top().hdr.id, RIFF_SIGNATURE, RIFF_LABEL_SIZE)) {
|
||||||
|
throw std::runtime_error("Top chunk not RIFF chunk");
|
||||||
|
}
|
||||||
|
|
||||||
|
endChunk();
|
||||||
|
}
|
||||||
|
}
|
43
misc_modules/recorder/src/riff.h
Normal file
43
misc_modules/recorder/src/riff.h
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <mutex>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
#include <stack>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
namespace riff {
|
||||||
|
#pragma pack(push, 1)
|
||||||
|
struct ChunkHeader {
|
||||||
|
char id[4];
|
||||||
|
uint32_t size;
|
||||||
|
};
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
struct ChunkDesc {
|
||||||
|
ChunkHeader hdr;
|
||||||
|
std::streampos pos;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Writer {
|
||||||
|
public:
|
||||||
|
bool open(std::string path, const char form[4]);
|
||||||
|
bool isOpen();
|
||||||
|
void close();
|
||||||
|
|
||||||
|
void beginList(const char id[4]);
|
||||||
|
void endList();
|
||||||
|
|
||||||
|
void beginChunk(const char id[4]);
|
||||||
|
void endChunk();
|
||||||
|
|
||||||
|
void write(const uint8_t* data, size_t len);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void beginRIFF(const char form[4]);
|
||||||
|
void endRIFF();
|
||||||
|
|
||||||
|
std::recursive_mutex mtx;
|
||||||
|
std::ofstream file;
|
||||||
|
std::stack<ChunkDesc> chunks;
|
||||||
|
};
|
||||||
|
}
|
182
misc_modules/recorder/src/wav.cpp
Normal file
182
misc_modules/recorder/src/wav.cpp
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
#include "wav.h"
|
||||||
|
#include <volk/volk.h>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <dsp/buffer/buffer.h>
|
||||||
|
#include <dsp/stream.h>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
namespace wav {
|
||||||
|
const char* WAVE_FILE_TYPE = "WAVE";
|
||||||
|
const char* FORMAT_MARKER = "fmt ";
|
||||||
|
const char* DATA_MARKER = "data";
|
||||||
|
const uint32_t FORMAT_HEADER_LEN = 16;
|
||||||
|
const uint16_t SAMPLE_TYPE_PCM = 1;
|
||||||
|
|
||||||
|
std::map<SampleType, int> SAMP_BITS = {
|
||||||
|
{ SAMP_TYPE_UINT8, 8 },
|
||||||
|
{ SAMP_TYPE_INT16, 16 },
|
||||||
|
{ SAMP_TYPE_INT32, 32 },
|
||||||
|
{ SAMP_TYPE_FLOAT32, 32 }
|
||||||
|
};
|
||||||
|
|
||||||
|
Writer::Writer(int channels, uint64_t samplerate, Format format, SampleType type) {
|
||||||
|
// Validate channels and samplerate
|
||||||
|
if (channels < 1) { throw std::runtime_error("Channel count must be greater or equal to 1"); }
|
||||||
|
if (!samplerate) { throw std::runtime_error("Samplerate must be non-zero"); }
|
||||||
|
|
||||||
|
// Initialize variables
|
||||||
|
_channels = channels;
|
||||||
|
_samplerate = samplerate;
|
||||||
|
_format = format;
|
||||||
|
_type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
Writer::~Writer() { close(); }
|
||||||
|
|
||||||
|
bool Writer::open(std::string path) {
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||||
|
// Close previous file
|
||||||
|
if (rw.isOpen()) { close(); }
|
||||||
|
|
||||||
|
// Reset work values
|
||||||
|
samplesWritten = 0;
|
||||||
|
|
||||||
|
// Fill header
|
||||||
|
bytesPerSamp = (SAMP_BITS[_type] / 8) * _channels;
|
||||||
|
hdr.codec = (_type == SAMP_TYPE_FLOAT32) ? CODEC_FLOAT : CODEC_PCM;
|
||||||
|
hdr.channelCount = _channels;
|
||||||
|
hdr.sampleRate = _samplerate;
|
||||||
|
hdr.bitDepth = SAMP_BITS[_type];
|
||||||
|
hdr.bytesPerSample = bytesPerSamp;
|
||||||
|
hdr.bytesPerSecond = bytesPerSamp * _samplerate;
|
||||||
|
|
||||||
|
// Precompute sizes and allocate buffers
|
||||||
|
switch (_type) {
|
||||||
|
case SAMP_TYPE_UINT8:
|
||||||
|
bufU8 = dsp::buffer::alloc<uint8_t>(STREAM_BUFFER_SIZE * _channels);
|
||||||
|
break;
|
||||||
|
case SAMP_TYPE_INT16:
|
||||||
|
bufI16 = dsp::buffer::alloc<int16_t>(STREAM_BUFFER_SIZE * _channels);
|
||||||
|
break;
|
||||||
|
case SAMP_TYPE_INT32:
|
||||||
|
bufI32 = dsp::buffer::alloc<int32_t>(STREAM_BUFFER_SIZE * _channels);
|
||||||
|
break;
|
||||||
|
case SAMP_TYPE_FLOAT32:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open file
|
||||||
|
if (!rw.open(path, WAVE_FILE_TYPE)) { return false; }
|
||||||
|
|
||||||
|
// Write format chunk
|
||||||
|
rw.beginChunk(FORMAT_MARKER);
|
||||||
|
rw.write((uint8_t*)&hdr, sizeof(FormatHeader));
|
||||||
|
rw.endChunk();
|
||||||
|
|
||||||
|
// Begin data chunk
|
||||||
|
rw.beginChunk(DATA_MARKER);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Writer::isOpen() {
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||||
|
return rw.isOpen();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Writer::close() {
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||||
|
// Do nothing if the file is not open
|
||||||
|
if (!rw.isOpen()) { return; }
|
||||||
|
|
||||||
|
// Finish data chunk
|
||||||
|
rw.endChunk();
|
||||||
|
|
||||||
|
// Close the file
|
||||||
|
rw.close();
|
||||||
|
|
||||||
|
// Free buffers
|
||||||
|
if (bufU8) {
|
||||||
|
dsp::buffer::free(bufU8);
|
||||||
|
bufU8 = NULL;
|
||||||
|
}
|
||||||
|
if (bufI16) {
|
||||||
|
dsp::buffer::free(bufI16);
|
||||||
|
bufI16 = NULL;
|
||||||
|
}
|
||||||
|
if (bufI32) {
|
||||||
|
dsp::buffer::free(bufI32);
|
||||||
|
bufI32 = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Writer::setChannels(int channels) {
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||||
|
// Do not allow settings to change while open
|
||||||
|
if (rw.isOpen()) { throw std::runtime_error("Cannot change parameters while file is open"); }
|
||||||
|
|
||||||
|
// Validate channel count
|
||||||
|
if (channels < 1) { throw std::runtime_error("Channel count must be greater or equal to 1"); }
|
||||||
|
_channels = channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Writer::setSamplerate(uint64_t samplerate) {
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||||
|
// Do not allow settings to change while open
|
||||||
|
if (rw.isOpen()) { throw std::runtime_error("Cannot change parameters while file is open"); }
|
||||||
|
|
||||||
|
// Validate samplerate
|
||||||
|
if (!samplerate) { throw std::runtime_error("Samplerate must be non-zero"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
void Writer::setFormat(Format format) {
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||||
|
// Do not allow settings to change while open
|
||||||
|
if (rw.isOpen()) { throw std::runtime_error("Cannot change parameters while file is open"); }
|
||||||
|
_format = format;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Writer::setSampleType(SampleType type) {
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||||
|
// Do not allow settings to change while open
|
||||||
|
if (rw.isOpen()) { throw std::runtime_error("Cannot change parameters while file is open"); }
|
||||||
|
_type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Writer::write(float* samples, int count) {
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||||
|
if (!rw.isOpen()) { return; }
|
||||||
|
|
||||||
|
// Select different writer function depending on the chose depth
|
||||||
|
int tcount = count * _channels;
|
||||||
|
int tbytes = count * bytesPerSamp;
|
||||||
|
switch (_type) {
|
||||||
|
case SAMP_TYPE_UINT8:
|
||||||
|
// Volk doesn't support unsigned ints yet :/
|
||||||
|
for (int i = 0; i < tcount; i++) {
|
||||||
|
bufU8[i] = (samples[i] * 127.0f) + 128.0f;
|
||||||
|
}
|
||||||
|
rw.write(bufU8, tbytes);
|
||||||
|
break;
|
||||||
|
case SAMP_TYPE_INT16:
|
||||||
|
volk_32f_s32f_convert_16i(bufI16, samples, 32767.0f, tcount);
|
||||||
|
rw.write((uint8_t*)bufI16, tbytes);
|
||||||
|
break;
|
||||||
|
case SAMP_TYPE_INT32:
|
||||||
|
volk_32f_s32f_convert_32i(bufI32, samples, 2147483647.0f, tcount);
|
||||||
|
rw.write((uint8_t*)bufI32, tbytes);
|
||||||
|
break;
|
||||||
|
case SAMP_TYPE_FLOAT32:
|
||||||
|
rw.write((uint8_t*)samples, tbytes);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment sample counter
|
||||||
|
samplesWritten += count;
|
||||||
|
}
|
||||||
|
}
|
@ -1,66 +1,71 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <stdint.h>
|
#include <string>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <mutex>
|
||||||
|
#include "riff.h"
|
||||||
|
|
||||||
#define WAV_SIGNATURE "RIFF"
|
namespace wav {
|
||||||
#define WAV_TYPE "WAVE"
|
#pragma pack(push, 1)
|
||||||
#define WAV_FORMAT_MARK "fmt "
|
struct FormatHeader {
|
||||||
#define WAV_DATA_MARK "data"
|
uint16_t codec;
|
||||||
#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)
|
|
||||||
uint16_t channelCount;
|
uint16_t channelCount;
|
||||||
uint32_t sampleRate;
|
uint32_t sampleRate;
|
||||||
uint32_t bytesPerSecond;
|
uint32_t bytesPerSecond;
|
||||||
uint16_t bytesPerSample;
|
uint16_t bytesPerSample;
|
||||||
uint16_t bitDepth;
|
uint16_t bitDepth;
|
||||||
char dataMarker[4]; // "data"
|
};
|
||||||
uint32_t dataSize;
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
enum Format {
|
||||||
|
FORMAT_WAV,
|
||||||
|
FORMAT_RF64
|
||||||
};
|
};
|
||||||
|
|
||||||
std::ofstream file;
|
enum SampleType {
|
||||||
size_t bytesWritten = 0;
|
SAMP_TYPE_UINT8,
|
||||||
WavHeader_t hdr;
|
SAMP_TYPE_INT16,
|
||||||
};
|
SAMP_TYPE_INT32,
|
||||||
|
SAMP_TYPE_FLOAT32
|
||||||
|
};
|
||||||
|
|
||||||
|
enum Codec {
|
||||||
|
CODEC_PCM = 1,
|
||||||
|
CODEC_FLOAT = 3
|
||||||
|
};
|
||||||
|
|
||||||
|
class Writer {
|
||||||
|
public:
|
||||||
|
Writer(int channels = 2, uint64_t samplerate = 48000, Format format = FORMAT_WAV, SampleType type = SAMP_TYPE_INT16);
|
||||||
|
~Writer();
|
||||||
|
|
||||||
|
bool open(std::string path);
|
||||||
|
bool isOpen();
|
||||||
|
void close();
|
||||||
|
|
||||||
|
void setChannels(int channels);
|
||||||
|
void setSamplerate(uint64_t samplerate);
|
||||||
|
void setFormat(Format format);
|
||||||
|
void setSampleType(SampleType type);
|
||||||
|
|
||||||
|
size_t getSamplesWritten() { return samplesWritten; }
|
||||||
|
|
||||||
|
void write(float* samples, int count);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::recursive_mutex mtx;
|
||||||
|
FormatHeader hdr;
|
||||||
|
riff::Writer rw;
|
||||||
|
|
||||||
|
int _channels;
|
||||||
|
uint64_t _samplerate;
|
||||||
|
Format _format;
|
||||||
|
SampleType _type;
|
||||||
|
size_t bytesPerSamp;
|
||||||
|
|
||||||
|
uint8_t* bufU8 = NULL;
|
||||||
|
int16_t* bufI16 = NULL;
|
||||||
|
int32_t* bufI32 = NULL;
|
||||||
|
size_t samplesWritten = 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user