Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.5)
project(inspectrum CXX)
project(inspectrum VERSION 1.0.0 LANGUAGES CXX)
enable_testing()

add_subdirectory(src)
22 changes: 21 additions & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,19 @@ endif (NOT CMAKE_CXX_FLAGS)
# This only works in cmake >3.1
set(CMAKE_CXX_STANDARD 14)

list(APPEND inspectrum_sources
list(APPEND inspectrum_sources
abstractsamplesource.cpp
amplitudedemod.cpp
burstdetector.cpp
burstdock.cpp
compute/computation.cpp
compute/autocorrelationcomputation.cpp
compute/bandwidthcomputation.cpp
compute/centerfrequencycomputation.cpp
compute/modulationclassifier.cpp
compute/modulationcomputation.cpp
compute/powercomputation.cpp
compute/symbolratecomputation.cpp
cursor.cpp
cursors.cpp
main.cpp
Expand Down Expand Up @@ -65,6 +75,7 @@ include_directories(
)

add_executable(inspectrum ${EXE_ARGS} ${inspectrum_sources})
target_compile_definitions(inspectrum PRIVATE INSPECTRUM_VERSION="${PROJECT_VERSION}")

if (Qt6_FOUND)
target_link_libraries(inspectrum
Expand All @@ -80,6 +91,15 @@ else()
)
endif()

# Offline validation tool for the modulation classifier (not built by default)
add_executable(modulation_bench EXCLUDE_FROM_ALL
tools/modulation_bench.cpp
compute/modulationclassifier.cpp
fft.cpp
util.cpp
)
target_link_libraries(modulation_bench ${FFTW_LIBRARIES})

set(INSTALL_DEFAULT_BINDIR "bin" CACHE STRING "Appended to CMAKE_INSTALL_PREFIX")

install(TARGETS inspectrum RUNTIME DESTINATION ${INSTALL_DEFAULT_BINDIR})
Expand Down
123 changes: 123 additions & 0 deletions src/burstdetector.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright (C) 2026, Nicolas Guillaume <nicol@sguillau.me>
*
* This file is part of inspectrum.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#include "burstdetector.h"

#include <algorithm>
#include <cmath>
#include <utility>

BurstDetector::Result BurstDetector::detect(SampleSource<std::complex<float>> &src,
double sampleRate, const Params &params,
const std::function<bool(size_t, size_t)> &progress)
{
Result result;
const size_t total = src.count();
if (total == 0 || sampleRate <= 0.0)
return result;

// Envelope is block-averaged power; the block size grows with file size so
// the envelope stays bounded (~16M blocks max) even for 100GB+ captures.
size_t blockSize = 16;
const size_t maxBlocks = size_t(1) << 24;
while (total / blockSize > maxBlocks)
blockSize *= 2;

std::vector<float> blockPower;
blockPower.reserve((total + blockSize - 1) / blockSize);

const size_t chunkSize = std::max(blockSize, ((size_t(1) << 20) / blockSize) * blockSize);
for (size_t pos = 0; pos < total; pos += chunkSize) {
if (progress && !progress(pos, total)) {
result.cancelled = true;
return result;
}
size_t length = std::min(chunkSize, total - pos);
auto samples = src.getSamples(pos, length);
if (samples == nullptr)
break;
for (size_t i = 0; i < length; i += blockSize) {
size_t n = std::min(blockSize, length - i);
float acc = 0.0f;
for (size_t j = 0; j < n; j++)
acc += std::norm(samples[i + j]);
blockPower.push_back(acc / n);
}
}
if (progress)
progress(total, total);

if (blockPower.empty())
return result;

// Noise floor estimate: 20th percentile of the envelope. This assumes the
// channel is idle at least ~20% of the time; on a continuously occupied
// capture the threshold ends up above the signal and nothing is detected.
std::vector<float> sorted(blockPower);
size_t k = sorted.size() / 5;
std::nth_element(sorted.begin(), sorted.begin() + k, sorted.end());
float noiseFloor = std::max(sorted[k], 1e-20f);
result.noiseFloorDb = 10.0 * std::log10(noiseFloor);

const float threshold = noiseFloor * std::pow(10.0f, (float)params.thresholdDb / 10.0f);

const size_t minLenBlocks =
std::max<size_t>(1, std::llround(params.minLengthMs * 1e-3 * sampleRate / blockSize));
const size_t mergeGapBlocks =
(size_t)std::llround(params.mergeGapMs * 1e-3 * sampleRate / blockSize);

// Raw above-threshold intervals, in block indices
std::vector<std::pair<size_t, size_t>> intervals;
bool inBurst = false;
size_t burstStart = 0;
for (size_t i = 0; i < blockPower.size(); i++) {
bool above = blockPower[i] > threshold;
if (above && !inBurst) {
inBurst = true;
burstStart = i;
} else if (!above && inBurst) {
inBurst = false;
intervals.emplace_back(burstStart, i);
}
}
if (inBurst)
intervals.emplace_back(burstStart, blockPower.size());

// Merge intervals separated by short gaps, then drop the too-short ones
std::vector<std::pair<size_t, size_t>> merged;
for (const auto &interval : intervals) {
if (!merged.empty() && interval.first - merged.back().second <= mergeGapBlocks)
merged.back().second = interval.second;
else
merged.push_back(interval);
}

for (const auto &m : merged) {
if (m.second - m.first < minLenBlocks)
continue;
float peak = *std::max_element(blockPower.begin() + m.first, blockPower.begin() + m.second);
Burst burst;
burst.start = m.first * blockSize;
burst.end = std::min(m.second * blockSize, total);
burst.peakPowerDb = 10.0f * std::log10(std::max(peak, 1e-20f));
result.bursts.push_back(burst);
}

return result;
}
53 changes: 53 additions & 0 deletions src/burstdetector.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2026, Nicolas Guillaume <nicol@sguillau.me>
*
* This file is part of inspectrum.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#pragma once

#include <complex>
#include <functional>
#include <vector>

#include "samplesource.h"

struct Burst {
size_t start;
size_t end; // one past the last sample
float peakPowerDb;
};

class BurstDetector
{
public:
struct Params {
double thresholdDb = 8.0; // above the estimated noise floor
double minLengthMs = 0.1;
double mergeGapMs = 0.3;
};

struct Result {
std::vector<Burst> bursts;
double noiseFloorDb = 0.0;
bool cancelled = false;
};

// progress(done, total) is called periodically; returning false cancels the scan
static Result detect(SampleSource<std::complex<float>> &src, double sampleRate,
const Params &params,
const std::function<bool(size_t, size_t)> &progress = nullptr);
};
Loading