early skeleton

This commit is contained in:
bytequill 2025-06-29 09:49:55 +02:00
parent dbfddda58a
commit ecd259f629
Signed by: bytequill
GPG Key ID: 1D929B3AA9873122
10 changed files with 332 additions and 1 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
dev/*
build/*
# Temporary ignore!!
docs/*

34
CMakeLists.txt Normal file
View File

@ -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)

View File

@ -1 +1,3 @@
# radio
# radio-sdr - An exploration of low-level radio modulation, raw filetype parsing and displaying data
# FOREWORD

31
src/fft.cpp Normal file
View File

@ -0,0 +1,31 @@
#include "fft.hpp"
using namespace std;
#include <cassert>
void fft(vector<cd> &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<cd> 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;
}
}

8
src/fft.hpp Normal file
View File

@ -0,0 +1,8 @@
#include <cmath>
#include <complex>
#include <vector>
using cd = std::complex<double>;
const double PI = acos(-1);
void fft(std::vector<cd> &a, bool invert);

24
src/get_CPM.cmake Normal file
View File

@ -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})

67
src/main.cpp Normal file
View File

@ -0,0 +1,67 @@
#include "sources/wav-baseband.hpp"
#include "fft.hpp"
#include <fstream>
//GUI
#include <SDL3/SDL.h>
#include <imgui.h>
#include <backends/imgui_impl_sdl3.h>
#include <backends/imgui_impl_sdlrenderer3.h>
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<cd> samples;
for (int i = 0; i != 10000 * 2; i++) {
std::vector<cd> 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<int>(std::abs(samples[i]));
out << mag << "\n";
}
out.close();
return res;
}

19
src/sources/base.hpp Normal file
View File

@ -0,0 +1,19 @@
#include <cstdint>
#include <string>
#include <complex>
#include <vector>
using cd = std::complex<double>;
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<cd> GetSamples(uintmax_t indx, int n);
};

View File

@ -0,0 +1,106 @@
#include "wav-baseband.hpp"
#include <cassert>
#include <iostream>
#include <cstring>
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<const uint8_t*>(sampleData));
return static_cast<int64_t>(static_cast<int16_t>(raw) - 128);
} else if (bitsPerSample == 16) {
int16_t sample = static_cast<int16_t>(
(static_cast<uint16_t>(static_cast<uint8_t>(sampleData[1])) << 8) |
static_cast<uint8_t>(sampleData[0])
);
return static_cast<int64_t>(sample);
} else if (bitsPerSample == 32) {
int32_t sample = static_cast<int32_t>(
(static_cast<uint32_t>(static_cast<uint8_t>(sampleData[3])) << 24) |
(static_cast<uint32_t>(static_cast<uint8_t>(sampleData[2])) << 16) |
(static_cast<uint32_t>(static_cast<uint8_t>(sampleData[1])) << 8) |
static_cast<uint8_t>(sampleData[0])
);
return static_cast<int64_t>(sample);
} else if (bitsPerSample == 64 && sizeof(size_t) >= 8) {
int64_t sample = static_cast<int64_t>(
(static_cast<uint64_t>(static_cast<uint8_t>(sampleData[7])) << 56) |
(static_cast<uint64_t>(static_cast<uint8_t>(sampleData[6])) << 48) |
(static_cast<uint64_t>(static_cast<uint8_t>(sampleData[5])) << 40) |
(static_cast<uint64_t>(static_cast<uint8_t>(sampleData[4])) << 32) |
(static_cast<uint64_t>(static_cast<uint8_t>(sampleData[3])) << 24) |
(static_cast<uint64_t>(static_cast<uint8_t>(sampleData[2])) << 16) |
(static_cast<uint64_t>(static_cast<uint8_t>(sampleData[1])) << 8) |
static_cast<uint8_t>(sampleData[0])
);
return static_cast<int64_t>(sample);
} else {
return 0;
}
}
std::vector<cd> 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<cd> samples;
if (!file || !file.is_open()) {return samples;}
std::vector<char> 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();}
}
}

View File

@ -0,0 +1,35 @@
#include "base.hpp"
#include <fstream>
#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<cd> GetSamples(uintmax_t indx, int n);
~RFSourceWAV();
private:
std::ifstream file;
WAVHeader header;
};