diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..826d493 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +dev/* +build/* +# Temporary ignore!! +docs/* + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..297673e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.14 FATAL_ERROR) + +project(radio-sdr) + +file(GLOB_RECURSE SOURCES "src/*.cpp") +file(GLOB_RECURSE HEADERS "src/*.hpp") + +add_executable(main ${SOURCES} ${HEADERS}) + +set(CMAKE_CXX_FLAGS_DEBUG "-g -Og -ggdb") +set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG") + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +#============================== +# CPM - Cmake Package Manager | +#============================== +include(src/get_CPM.cmake) + +CPMAddPackage("gh:libsdl-org/SDL#release-3.2.10") + +CPMAddPackage(gh:ocornut/imgui@1.91.9b) +add_library(imgui STATIC + ${imgui_SOURCE_DIR}/imgui.cpp + ${imgui_SOURCE_DIR}/imgui_demo.cpp # optionally comment this out + ${imgui_SOURCE_DIR}/imgui_draw.cpp + ${imgui_SOURCE_DIR}/imgui_widgets.cpp + ${imgui_SOURCE_DIR}/imgui_tables.cpp +) +message(STATUS "imgui_SOURCE_DIR: ${imgui_SOURCE_DIR}") +target_include_directories(imgui INTERFACE ${imgui_SOURCE_DIR}) +target_compile_definitions(imgui PUBLIC -DIMGUI_DISABLE_OBSOLETE_FUNCTIONS) # optional imgui setting + +target_link_libraries(main SDL3 imgui) diff --git a/README.md b/README.md index 78fd9ff..c7a6392 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -# radio \ No newline at end of file +# radio-sdr - An exploration of low-level radio modulation, raw filetype parsing and displaying data + +# FOREWORD diff --git a/src/fft.cpp b/src/fft.cpp new file mode 100644 index 0000000..6845bb6 --- /dev/null +++ b/src/fft.cpp @@ -0,0 +1,31 @@ +#include "fft.hpp" +using namespace std; + +#include + +void fft(vector &a, bool invert) { + assert(a.size() > 0 && (a.size() & (a.size() - 1)) == 0); // Check power of 2 + int n = a.size(); + if (n == 1) + return; + + vector a0(n / 2), a1(n / 2); + for (int i = 0; 2 * i < n; i++) { + a0[i] = a[2*i]; + a1[i] = a[2*i+1]; + } + fft(a0, invert); + fft(a1, invert); + + double ang = 2 * PI / n * (invert ? -1 : 1); + cd w(1), wn(cos(ang), sin(ang)); + for (int i = 0; 2 * i < n; i++) { + a[i] = a0[i] + w * a1[i]; + a[i + n/2] = a0[i] - w * a1[i]; + if (invert) { + a[i] /= 2; + a[i + n/2] /= 2; + } + w *= wn; + } +} \ No newline at end of file diff --git a/src/fft.hpp b/src/fft.hpp new file mode 100644 index 0000000..e1a9189 --- /dev/null +++ b/src/fft.hpp @@ -0,0 +1,8 @@ +#include +#include +#include + +using cd = std::complex; +const double PI = acos(-1); + +void fft(std::vector &a, bool invert); \ No newline at end of file diff --git a/src/get_CPM.cmake b/src/get_CPM.cmake new file mode 100644 index 0000000..5dd4171 --- /dev/null +++ b/src/get_CPM.cmake @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: MIT +# +# SPDX-FileCopyrightText: Copyright (c) 2019-2023 Lars Melchior and contributors + +set(CPM_DOWNLOAD_VERSION 0.42.0) +set(CPM_HASH_SUM "2020b4fc42dba44817983e06342e682ecfc3d2f484a581f11cc5731fbe4dce8a") + +if(CPM_SOURCE_CACHE) + set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +elseif(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +else() + set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +endif() + +# Expand relative path. This is important if the provided path contains a tilde (~) +get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE) + +file(DOWNLOAD + https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake + ${CPM_DOWNLOAD_LOCATION} EXPECTED_HASH SHA256=${CPM_HASH_SUM} +) + +include(${CPM_DOWNLOAD_LOCATION}) \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..795458c --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,67 @@ +#include "sources/wav-baseband.hpp" +#include "fft.hpp" + +#include + +//GUI +#include +#include +#include +#include + +int GUIMain() { + if(!SDL_Init(SDL_INIT_VIDEO)) { + return 1; + } + + SDL_WindowFlags window_flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY; + SDL_Window *window = SDL_CreateWindow("Title", 1280, 720, window_flags); + if(window == NULL) { + return 1; + } + SDL_Renderer *renderer = SDL_CreateRenderer(window, NULL); + if(renderer == NULL) { + return 1; + } + + SDL_SetRenderVSync(renderer, 1); + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); (void)io; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + + ImGui::StyleColorsDark(); + ImGui_ImplSDL3_InitForSDLRenderer(window, renderer); + ImGui_ImplSDLRenderer3_Init(renderer); + + return 0; +} + +int main() { + int res2 = GUIMain(); + + return res2; + RFSourceWAV src; + + bool res = src.Load("./dev/test2.wav"); + std::vector samples; + for (int i = 0; i != 10000 * 2; i++) { + std::vector sampleFrame = src.GetSamples(i * pow(2,11), pow(2,11)); + fft(sampleFrame, false); + samples.insert(samples.end(), sampleFrame.begin(), sampleFrame.end()); + } + std::ofstream out; + out.open("dev/out1.csv"); + out << "Magnitude\n"; + + for (size_t i = 0; i < samples.size(); i++) { + int mag = static_cast(std::abs(samples[i])); + out << mag << "\n"; + } + + out.close(); + + return res; +} + diff --git a/src/sources/base.hpp b/src/sources/base.hpp new file mode 100644 index 0000000..9b37699 --- /dev/null +++ b/src/sources/base.hpp @@ -0,0 +1,19 @@ +#include +#include +#include +#include + +using cd = std::complex; + +class RFSourceBase { + public: + bool live; // TRUE - Live RF stream FALSE - Recorded file + + // Fundamental metadata. In Hz + long bandwidth; + long samplerate; + long centerFreq; + + bool Load(std::string sourceID); + std::vector GetSamples(uintmax_t indx, int n); +}; \ No newline at end of file diff --git a/src/sources/wav-baseband.cpp b/src/sources/wav-baseband.cpp new file mode 100644 index 0000000..dcdb26f --- /dev/null +++ b/src/sources/wav-baseband.cpp @@ -0,0 +1,106 @@ +#include "wav-baseband.hpp" + +#include +#include +#include + +bool RFSourceWAV::Load(std::string filename) { + file.open(filename.c_str(), std::ios::in | std::ios::binary); + if (!file || !file.is_open()) {return false;} + + std::cout << header.samplerate << std::endl; + + char buff[sizeof(header)]; + file.read(buff, sizeof(header)); + + memcpy(&header, buff, sizeof(header)); + + // Assumptions about the format which if not true require refactoring of the code + if (std::strncmp(header.Magic, "RIFF", 4) != 0 || + std::strncmp(header.RIFFType, "WAV", 4) != 0 || + std::strncmp(header.FMTMARK, "fmt ", 4) != 0 || + std::strncmp(header.DATAMARK, "data", 4) != 0 || + header.fmtType != 1 || + header.chnNum != 2) + { + // TODO: figure out a DEBUG/ERROR system for more precise data than just the failure + return false; + } + + this->samplerate = header.samplerate; + + return false; +} + +// converts the `char`(1byte) raw sampleData into correct bitsPerSample based values +int64_t convertSample(const char *sampleData, uint_fast16_t bitsPerSample) { + assert(bitsPerSample > 0 && (bitsPerSample & (bitsPerSample - 1)) == 0); // Check power of 2 + assert(bitsPerSample <= 64); + + if (bitsPerSample == 8) { + // 8-bit WAV audio is typically stored as unsigned values. + // Convert to signed: 0-255 (unsigned) -> -128 to 127. + uint8_t raw = *(reinterpret_cast(sampleData)); + return static_cast(static_cast(raw) - 128); + } else if (bitsPerSample == 16) { + int16_t sample = static_cast( + (static_cast(static_cast(sampleData[1])) << 8) | + static_cast(sampleData[0]) + ); + return static_cast(sample); + } else if (bitsPerSample == 32) { + int32_t sample = static_cast( + (static_cast(static_cast(sampleData[3])) << 24) | + (static_cast(static_cast(sampleData[2])) << 16) | + (static_cast(static_cast(sampleData[1])) << 8) | + static_cast(sampleData[0]) + ); + return static_cast(sample); + } else if (bitsPerSample == 64 && sizeof(size_t) >= 8) { + int64_t sample = static_cast( + (static_cast(static_cast(sampleData[7])) << 56) | + (static_cast(static_cast(sampleData[6])) << 48) | + (static_cast(static_cast(sampleData[5])) << 40) | + (static_cast(static_cast(sampleData[4])) << 32) | + (static_cast(static_cast(sampleData[3])) << 24) | + (static_cast(static_cast(sampleData[2])) << 16) | + (static_cast(static_cast(sampleData[1])) << 8) | + static_cast(sampleData[0]) + ); + return static_cast(sample); + } else { + return 0; + } +} + +std::vector RFSourceWAV::GetSamples(uintmax_t indx, int n) { + // an I/Q PCM stream MUST have 2 channels but setting value here for readability + int8_t chnNum = 2; + assert (chnNum == header.chnNum); + + std::vector samples; + if (!file || !file.is_open()) {return samples;} + + std::vector buf((header.bitsPerSample / 8) * chnNum * n); + uintmax_t offset = sizeof(header) + indx * header.blockAlign; + if (offset + buf.size() > header.dataLen + sizeof(header)) {return samples;} + + file.seekg(offset); + if (!file) {return samples;} + file.read(buf.data(), buf.size()); + + for (size_t i = 0; i < buf.size(); i += header.blockAlign) { + int sampleI = convertSample(&buf.data()[i], header.bitsPerSample); + int sampleQ = convertSample(&buf.data()[i + (header.bitsPerSample / 8)], + header.bitsPerSample); + samples.push_back(cd(sampleI, sampleQ)); + } + + return samples; +} + +RFSourceWAV::~RFSourceWAV() { + if(file){ + if(file.is_open()) {file.close();} + } +} \ No newline at end of file diff --git a/src/sources/wav-baseband.hpp b/src/sources/wav-baseband.hpp new file mode 100644 index 0000000..d605afc --- /dev/null +++ b/src/sources/wav-baseband.hpp @@ -0,0 +1,35 @@ +#include "base.hpp" + +#include + +#pragma pack(push, 1) +struct WAVHeader { + char Magic[4]; // Always "RIFF". Magic value for all RIFF-Based formats! + uint32_t Size; + char RIFFType[4]; // Always "WAV" + char FMTMARK[4]; + uint32_t fmtChunkLen; // Always 16 + uint16_t fmtType; // 1 - PCM + uint16_t chnNum; + uint32_t samplerate; + uint32_t byteRate; // samplerate * bitsPerSample * chnNum / 8 + uint16_t blockAlign; // bitsPerSample * chnNum / 8 + uint16_t bitsPerSample; + char DATAMARK[4]; + uint32_t dataLen; +}; +#pragma pack(pop) + +class RFSourceWAV : RFSourceBase { + public: + bool Load(std::string filename); + // Get `n` samples at index `indx` for both I and Q channels from the WAV file and return an + // FFT-friendly vector + std::vector GetSamples(uintmax_t indx, int n); + ~RFSourceWAV(); + + private: + std::ifstream file; + WAVHeader header; +}; +