diff --git a/modules/recorder/CMakeLists.txt b/modules/recorder/CMakeLists.txt new file mode 100644 index 00000000..e55e9f0d --- /dev/null +++ b/modules/recorder/CMakeLists.txt @@ -0,0 +1,55 @@ +cmake_minimum_required(VERSION 3.9) +project(recorder) + +if (MSVC) + set(CMAKE_CXX_FLAGS "-O2 /std:c++17") + link_directories(recorder "C:/Program Files/PothosSDR/lib/") + include_directories(recorder "C:/Program Files/PothosSDR/include/volk/") + include_directories(recorder "C:/Program Files/PothosSDR/include/") +else() + set(CMAKE_CXX_FLAGS "-O3 -std=c++17 -fsanitize=address -g") + include_directories(recorder "/usr/include/volk") + link_libraries(pthread) + link_libraries(GL) + link_libraries(GLEW) + link_libraries(glfw) + link_libraries(fftw3) + link_libraries(fftw3f) + link_libraries(portaudio) + link_libraries(X11) + link_libraries(Xxf86vm) +endif (MSVC) + +link_libraries(volk) +link_libraries(SoapySDR) + +# Main code +include_directories(recorder "src/") +include_directories(recorder "../../src/") +include_directories(recorder "../../src/imgui") +file(GLOB SRC "src/*.cpp") +file(GLOB IMGUI "../../src/imgui/*.cpp") +add_library(recorder SHARED ${SRC} ${IMGUI}) +set_target_properties(recorder PROPERTIES OUTPUT_NAME recorder) + +if (MSVC) + # Glew + find_package(GLEW REQUIRED) + target_link_libraries(recorder PRIVATE GLEW::GLEW) + + # GLFW3 + find_package(glfw3 CONFIG REQUIRED) + target_link_libraries(recorder PRIVATE glfw) + + # FFTW3 + find_package(FFTW3 CONFIG REQUIRED) + target_link_libraries(recorder PRIVATE FFTW3::fftw3) + find_package(FFTW3f CONFIG REQUIRED) + target_link_libraries(recorder PRIVATE FFTW3::fftw3f) + + # PortAudio + find_package(portaudio CONFIG REQUIRED) + target_link_libraries(recorder PRIVATE portaudio portaudio_static) +endif (MSVC) + +# cmake .. "-DCMAKE_TOOLCHAIN_FILE=C:/Users/Alex/vcpkg/scripts/buildsystems/vcpkg.cmake" -G "Visual Studio 15 2017 Win64" \ No newline at end of file diff --git a/modules/recorder/src/main.cpp b/modules/recorder/src/main.cpp new file mode 100644 index 00000000..7c4f1757 --- /dev/null +++ b/modules/recorder/src/main.cpp @@ -0,0 +1,138 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#define CONCAT(a, b) ((std::string(a) + b).c_str()) + +mod::API_t* API; + +struct RecorderContext_t { + std::string name; + dsp::stream* stream; + WavWriter* writer; + std::thread workerThread; + bool recording; + time_t startTime; + std::string lastNameList; + std::string selectedStreamName; + int selectedStreamId; +}; + +void _writeWorker(RecorderContext_t* ctx) { + dsp::StereoFloat_t* floatBuf = new dsp::StereoFloat_t[1024]; + int16_t* sampleBuf = new int16_t[2048]; + while (true) { + if (ctx->stream->read(floatBuf, 1024) < 0) { + break; + } + for (int i = 0; i < 1024; i++) { + sampleBuf[(i * 2) + 0] = floatBuf[i].l * 0x7FFF; + sampleBuf[(i * 2) + 1] = floatBuf[i].r * 0x7FFF; + } + ctx->writer->writeSamples(sampleBuf, 2048 * sizeof(int16_t)); + } + delete[] floatBuf; + delete[] sampleBuf; +} + +std::string genFileName(std::string prefix) { + time_t now = time(0); + tm *ltm = localtime(&now); + char buf[1024]; + sprintf(buf, "%02d-%02d-%02d_%02d-%02d-%02d.wav", ltm->tm_hour, ltm->tm_min, ltm->tm_sec, ltm->tm_mday, ltm->tm_mon + 1, ltm->tm_year + 1900); + return prefix + buf; +} + +void streamRemovedHandler(void* ctx) { + +} + +void sampleRateChanged(void* ctx, float sampleRate, int blockSize) { + +} + +MOD_EXPORT void* _INIT_(mod::API_t* _API, ImGuiContext* imctx, std::string _name) { + API = _API; + RecorderContext_t* ctx = new RecorderContext_t; + ctx->recording = false; + ImGui::SetCurrentContext(imctx); + return ctx; +} + +MOD_EXPORT void _NEW_FRAME_(RecorderContext_t* ctx) { + +} + +MOD_EXPORT void _DRAW_MENU_(RecorderContext_t* ctx) { + float menuColumnWidth = ImGui::GetContentRegionAvailWidth(); + + std::vector streamNames = API->getStreamNameList(); + std::string nameList; + for (std::string name : streamNames) { + nameList += name; + nameList += '\0'; + } + + if (ctx->lastNameList != nameList) { + ctx->lastNameList = nameList; + + } + + ImGui::PushItemWidth(menuColumnWidth); + if (!ctx->recording) { + ImGui::Combo(CONCAT("##_strea_select_", ctx->name), &ctx->selectedStreamId, nameList.c_str()); + } + else { + ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.44f, 0.44f, 0.44f, 0.15f)); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.20f, 0.21f, 0.22f, 0.30f)); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.00f, 1.00f, 1.00f, 0.65f)); + ImGui::Combo(CONCAT("##_strea_select_", ctx->name), &ctx->selectedStreamId, nameList.c_str()); + ImGui::PopItemFlag(); + ImGui::PopStyleColor(3); + } + + if (!ctx->recording) { + if (ImGui::Button("Record", ImVec2(menuColumnWidth, 0))) { + ctx->writer = new WavWriter("recordings/" + genFileName("audio_"), 16, 2, 48000); + ctx->stream = API->bindToStreamStereo("Radio", streamRemovedHandler, sampleRateChanged, ctx); + ctx->workerThread = std::thread(_writeWorker, ctx); + ctx->recording = true; + ctx->startTime = time(0); + } + ImGui::TextColored(ImGui::GetStyleColorVec4(ImGuiCol_Text), "Idle --:--:--"); + } + else { + if (ImGui::Button("Stop", ImVec2(menuColumnWidth, 0))) { + ctx->stream->stopReader(); + ctx->workerThread.join(); + ctx->stream->clearReadStop(); + API->unbindFromStreamStereo("Radio", ctx->stream); + ctx->writer->close(); + delete ctx->writer; + ctx->recording = false; + } + time_t diff = time(0) - ctx->startTime; + 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); + } +} + +MOD_EXPORT void _HANDLE_EVENT_(RecorderContext_t* ctx, int eventId) { + // INSTEAD OF EVENTS, REGISTER HANDLER WHEN CREATING VFO + if (eventId == mod::EVENT_STREAM_PARAM_CHANGED) { + + } + else if (eventId == mod::EVENT_SELECTED_VFO_CHANGED) { + + } +} + +MOD_EXPORT void _STOP_(RecorderContext_t* ctx) { + +} \ No newline at end of file diff --git a/modules/recorder/src/wav.h b/modules/recorder/src/wav.h new file mode 100644 index 00000000..7bf9ac8a --- /dev/null +++ b/modules/recorder/src/wav.h @@ -0,0 +1,62 @@ +#pragma once +#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)); + } + + 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; + uint32_t sampleRate; + uint32_t bytesPerSecond; + uint16_t bytesPerSample; + uint16_t bitDepth; + char dataMarker[4]; // "data" + uint32_t dataSize; + }; + + std::ofstream file; + size_t bytesWritten = 0; + WavHeader_t hdr; +}; \ No newline at end of file diff --git a/src/audio.cpp b/src/audio.cpp index de96637b..65eb3c45 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -108,14 +108,24 @@ namespace audio { bstr.sampleRateChangeHandler = sampleRateChangeHandler; if (astr->type == STREAM_TYPE_MONO) { bstr.monoStream = new dsp::stream(astr->blockSize * 2); + astr->monoDynSplit->stop(); astr->monoDynSplit->bind(bstr.monoStream); + if (astr->running) { + astr->monoDynSplit->start(); + } + astr->boundStreams.push_back(bstr); return bstr.monoStream; } bstr.stereoStream = new dsp::stream(astr->blockSize * 2); bstr.s2m = new dsp::StereoToMono(bstr.stereoStream, astr->blockSize * 2); bstr.monoStream = &bstr.s2m->output; + astr->stereoDynSplit->stop(); astr->stereoDynSplit->bind(bstr.stereoStream); + if (astr->running) { + astr->stereoDynSplit->start(); + } bstr.s2m->start(); + astr->boundStreams.push_back(bstr); return bstr.monoStream; } @@ -128,14 +138,24 @@ namespace audio { bstr.sampleRateChangeHandler = sampleRateChangeHandler; if (astr->type == STREAM_TYPE_STEREO) { bstr.stereoStream = new dsp::stream(astr->blockSize * 2); + astr->stereoDynSplit->stop(); astr->stereoDynSplit->bind(bstr.stereoStream); + if (astr->running) { + astr->stereoDynSplit->start(); + } + astr->boundStreams.push_back(bstr); return bstr.stereoStream; } bstr.monoStream = new dsp::stream(astr->blockSize * 2); bstr.m2s = new dsp::MonoToStereo(bstr.monoStream, astr->blockSize * 2); bstr.stereoStream = &bstr.m2s->output; + astr->monoDynSplit->stop(); astr->monoDynSplit->bind(bstr.monoStream); + if (astr->running) { + astr->monoDynSplit->start(); + } bstr.m2s->start(); + astr->boundStreams.push_back(bstr); return bstr.stereoStream; } @@ -179,8 +199,19 @@ namespace audio { continue; } if (astr->type == STREAM_TYPE_STEREO) { + astr->stereoDynSplit->stop(); + astr->stereoDynSplit->unbind(bstr.stereoStream); + if (astr->running) { + astr->stereoDynSplit->start(); + } bstr.s2m->stop(); delete bstr.s2m; + return; + } + astr->monoDynSplit->stop(); + astr->monoDynSplit->unbind(bstr.monoStream); + if (astr->running) { + astr->monoDynSplit->start(); } delete stream; return; @@ -195,8 +226,19 @@ namespace audio { continue; } if (astr->type == STREAM_TYPE_MONO) { - bstr.s2m->stop(); + astr->monoDynSplit->stop(); + astr->monoDynSplit->unbind(bstr.monoStream); + if (astr->running) { + astr->monoDynSplit->start(); + } + bstr.m2s->stop(); delete bstr.m2s; + return; + } + astr->stereoDynSplit->stop(); + astr->stereoDynSplit->unbind(bstr.stereoStream); + if (astr->running) { + astr->stereoDynSplit->start(); } delete stream; return; @@ -259,5 +301,13 @@ namespace audio { astr->audio->setDevice(deviceId); setSampleRate(name, sampleRate); } + + std::vector getStreamNameList() { + std::vector list; + for (auto [name, stream] : streams) { + list.push_back(name); + } + return list; + } }; diff --git a/src/audio.h b/src/audio.h index c9de6c3c..8f8fe9a8 100644 --- a/src/audio.h +++ b/src/audio.h @@ -59,5 +59,6 @@ namespace audio { std::string getNameFromVFO(std::string vfoName); void setSampleRate(std::string name, float sampleRate); void setAudioDevice(std::string name, int deviceId, float sampleRate); + std::vector getStreamNameList(); }; diff --git a/src/dsp/routing.h b/src/dsp/routing.h index b8be1913..5c4a955c 100644 --- a/src/dsp/routing.h +++ b/src/dsp/routing.h @@ -3,6 +3,7 @@ #include #include #include +#include namespace dsp { class Splitter { @@ -144,7 +145,7 @@ namespace dsp { for (int i = 0; i < outputCount; i++) { if (outputs[i] == stream) { outputs.erase(outputs.begin() + i); - break; + return; } } } @@ -179,11 +180,13 @@ namespace dsp { MonoToStereo(stream* input, int bufferSize) { _in = input; _bufferSize = bufferSize; + output.init(bufferSize * 2); } void init(stream* input, int bufferSize) { _in = input; _bufferSize = bufferSize; + output.init(bufferSize * 2); } void start() { diff --git a/src/main.cpp b/src/main.cpp index 33a96f66..1ef2450f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -123,6 +123,9 @@ int main() { ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); + + + int wwidth, wheight; glfwGetWindowSize(window, &wwidth, &wheight); ImGui::SetNextWindowPos(ImVec2(0, 0)); diff --git a/src/main_window.cpp b/src/main_window.cpp index 47131243..4ae7d31e 100644 --- a/src/main_window.cpp +++ b/src/main_window.cpp @@ -204,10 +204,10 @@ void windowInit() { // DSB / CW and RAW modes; // Write a recorder // Adjustable "snap to grid" for each VFO - // Bring VFO to a visible plane when changing sample rate if it's smaller - // Fix invalid values on the min/max sliders + // Bring VFO to a visible place when changing sample rate if it's smaller // Possibility to resize waterfall and menu // Have a proper root directory + // Switch to double for all frequecies and bandwidth // Update UI settings fftMin = config::config["min"]; @@ -672,10 +672,6 @@ void drawWindow() { ImGui::Spacing(); } - if (ImGui::CollapsingHeader("Recording")) { - ImGui::Spacing(); - } - if(ImGui::CollapsingHeader("Debug")) { ImGui::Text("Frame time: %.3f ms/frame", 1000.0f / ImGui::GetIO().Framerate); ImGui::Text("Framerate: %.1f FPS", ImGui::GetIO().Framerate); diff --git a/src/module.cpp b/src/module.cpp index ef0fe3e9..2ea8be18 100644 --- a/src/module.cpp +++ b/src/module.cpp @@ -42,6 +42,7 @@ namespace mod { API.setBlockSize = audio::setBlockSize; API.unbindFromStreamMono = audio::unbindFromStreamMono; API.unbindFromStreamStereo = audio::unbindFromStreamStereo; + API.getStreamNameList = audio::getStreamNameList; } void loadModule(std::string path, std::string name) { diff --git a/src/module.h b/src/module.h index 2bde0855..d5b16193 100644 --- a/src/module.h +++ b/src/module.h @@ -44,6 +44,7 @@ namespace mod { void (*setBlockSize)(std::string name, int blockSize); void (*unbindFromStreamMono)(std::string name, dsp::stream* stream); void (*unbindFromStreamStereo)(std::string name, dsp::stream* stream); + std::vector (*getStreamNameList)(void); enum { REF_LOWER, diff --git a/src/styles.cpp b/src/style.cpp similarity index 100% rename from src/styles.cpp rename to src/style.cpp