API Reference¶
Every public type and function in SignalCore, grouped by module. Defaults are
shown in parentheses. Everything lives in namespace signalcore; the examples
assume using namespace signalcore;.
The library follows the VoidLoop™ naming conventions: classes are PascalCase,
methods are camelCase, enums are enum class with PascalCase values, and macros
carry the SIGNALCORE_ prefix. The one exception is FFTSize, an unscoped enum
so its named sizes double as plain integers.
FFT (signalcore/FFT.h)¶
FFTSize¶
enum FFTSize : uint16_t lists the named transform lengths. It is an unscoped
enum, so each value also acts as a plain integer. Because of that,
FFT<FFTSize::N512>, FFT<512>, and FFT<FFT_LENGTH> (a macro set to 512) all
name the same type, and one integer can size both the template and a sample
array.
| Value | Samples |
|---|---|
FFTSize::N32 … FFTSize::N4096 |
32, 64, 128, 256, 512, 1024, 2048, 4096 |
A length does not have to use one of these names. Any power of two from 32 to
4096 is valid, and the names are just a convenience. The size must also stay
within SIGNALCORE_MAX_FFT_SIZE for the target board. Both rules are checked by
static_assert, so a bad size fails at compile time.
template<size_t FFT_LENGTH> class FFT¶
A self-contained FFT with statically allocated buffers. The template argument is
the transform length as an integer, written as FFT<512>, FFT<FFTSize::N512>,
or FFT<FFT_LENGTH> from a macro. FFT_LENGTH must be a power of two between 32
and 4096, and within SIGNALCORE_MAX_FFT_SIZE for the board. Construct one per
transform length.
| Method | Behavior |
|---|---|
FFT() |
Construct and initialize the backend for the chosen size. Window type defaults to None. |
operator float*() |
Implicit conversion to the result buffer. After calculate(), result[0 .. N/2] holds real magnitude bins. |
setWindowType(WindowType wt, uint32_t size = N) |
Select the analysis window and precompute its coefficients. size must match the sample count later passed to calculate(); values above N are clamped. Leave it at the default unless you deliberately transform partial (zero-padded) buffers. |
calculate(uint16_t* source, int size = N) |
Copy size samples from a uint16_t buffer (zero-padding to N) and run the forward transform. |
calculate(int16_t* source, int size = N) |
Same for a signed 16-bit buffer. |
calculate(float* source, int size = N) |
Same for a float buffer. |
forward(int size) |
Run the window, transform, magnitude, and scaling steps on the already-loaded sample buffer. Normally called for you by calculate(). |
postScaleDB(float floor = -100.0f, float maxScale = 32767.0f) |
Convert result[0 .. N/2] to dBFS in place using log10f, clamped to [floor, 10.0]. The default maxScale = 32767 suits 16-bit integer input; pass 1.0f for normalized synthesis, or (1 << bits) − 1 for ADC counts, or every reading is offset by a constant dB. |
postScaleDBFast(float floor = -100.0f, float maxScale = 32767.0f) |
Same conversion using a polynomial log10 approximation — faster where log10f is expensive, accurate to ~0.01 dB. |
peakBin(bool skipDC = true) |
Index of the dominant magnitude bin over result[0 .. N/2]. With skipDC (the default) the search starts at bin 1, so a DC offset can't win. Call before postScaleDB() if you also need the linear level. |
peakFrequency(float sampleRate, bool skipDC = true) |
Frequency of the dominant bin in Hz — peakBin(skipDC) * sampleRate / N. |
The result buffer is laid out as N/2 + 1 real bins: result[0] is DC,
result[N/2] is Nyquist, and the entries between are magnitudes for bins 1
through N/2 − 1. See FFT output format.
#define FFT_LENGTH 1024
FFT<FFT_LENGTH> fft; // FFT<1024> and FFT<FFTSize::N1024> are equivalent
fft.setWindowType(WindowType::BlackmanHarris);
fft.calculate(samples); // samples: float[FFT_LENGTH]
int peak = fft.peakBin(); // dominant bin, DC skipped
fft.postScaleDB(-100.0f, 1.0f); // magnitudes -> dBFS, after the linear read
Spectral helper functions¶
Free functions in signalcore, useful with or without the FFT class.
| Function | Returns / Behavior |
|---|---|
removeDC(float* samples, int size) |
Subtract the mean from samples in place (DC blocking before a transform). |
findPeak(float* data, int size, int start = 0) |
Index of the maximum element in data[start .. size-1], as an absolute index. Pass start = 1 to skip a spectrum's DC bin without slicing the pointer and re-adding the offset. |
mean(float* data, int size) |
Arithmetic mean of the buffer. |
signalToNoiseRatioDB(float signalAmplitude, float noiseAmplitude) |
20·log10(signal/noise) in dB; returns 0 when noiseAmplitude is 0. |
SINAD(float* data, int size) |
Signal-to-noise-and-distortion ratio in dB. Walks outward from the peak bin, accumulating signal power while bins stay at least twice the mean, then sums the remaining bins as noise. |
ENOB(float* data, int size) |
Effective number of bits, derived from SINAD via (SINAD − 1.76) / 6.02. |
log10fFast(float x) |
Fast log10 approximation (log2fApprox(x) · 0.30103). |
log2fApprox(float x) |
Polynomial log2 approximation, accurate to ~0.01 dB across the float range. |
Reading the result correctly
- Only
result[0 .. N/2]is valid (N/2 + 1bins). PassN/2 + 1as the size toSINAD,ENOB, andfindPeak— neverN; anything aboveresult[N/2]is stale intermediate data. For the dominant tone,peakBin()already scopes the search and skips DC for you. - The result buffer is reused in place as both the complex and magnitude
buffer.
operator float*()hands out the live buffer — copy it if you need to keep a frame. postScaleDB/SINAD/ENOBuse libmlog10f;postScaleDBFastandsignalToNoiseRatioDBuse thelog10fFastapproximation. Don't expect bit-identical dB between the fast and exact paths (~0.01 dB).- The size limit is compile-time:
FFT<4096>on AVR is astatic_assertfailure, not a runtime fallback.
DDS & Signal Generation (signalcore/DDS.h)¶
SignalShape¶
enum class SignalShape : uint8_t — Sine, Square, Triangle, Sawtooth,
Noise, None. Selects the waveform for a carrier or an AM source.
class SignalGenerator¶
High-level synthesizer: a carrier with optional amplitude modulation and gaussian noise. This is the path most sketches use.
| Method | Behavior |
|---|---|
SignalGenerator(int sampleRate) |
Construct at a virtual sample rate (samples per second). |
setCarrier(SignalShape shape, double frequency, double amplitude = 1.0, double phase = 0.0) |
Configure the carrier waveform, frequency (Hz), peak amplitude, and starting phase. |
setAM(SignalShape shape, double frequency, double amplitude) |
Apply amplitude modulation at frequency (Hz) with depth amplitude (0–100 %). |
setNoise(double amplitude) |
Add gaussian noise (Box–Muller) at level amplitude (0–100 %). Set to 0 to disable. |
nextSample(uint8_t resolution) |
Advance one sample and return it. resolution = 0 returns the raw bipolar float carrier (roughly ±amplitude). resolution = N offsets to unipolar and quantizes to an N-bit unsigned code in [0, 2^N) — e.g. 12 gives 0–4095, ready for a DAC — a completely different scale from the 0 case; excursions past the rails (AM overshoot, noise) saturate rather than wrap. |
modulateCarrier(float carrier) |
Apply the configured AM to a single carrier value. Called internally by nextSample(); exposed for custom synthesis loops. |
lockPhase(float x0, float x1, float x2) |
If the triplet straddles a local maximum (x1 is the peak), snap the carrier phase to π/2 and return true. Re-aligns a free-running oscillator to an external sync event. |
SignalGenerator sig(10000); // 10 kSPS
sig.setCarrier(SignalShape::Sine, 200.0); // 200 Hz tone
sig.setAM(SignalShape::Sine, 7.0, 30.0); // 30% AM at 7 Hz
sig.setNoise(3.0); // 3% gaussian noise
double s = sig.nextSample(0); // full-resolution float
class DDS¶
Low-level phase-accumulator oscillator over a pre-built int32_t wave table.
Table entries and tuning words are explicitly int32_t (not int), so the
module behaves identically on 16-bit-int AVR and on 32-bit boards.
| Method | Behavior |
|---|---|
DDS(int32_t* table, TableSize tableSize, uint32_t samplingFrequency) |
Construct over a wave table of the given size at a sampling frequency. |
nextSampleIncInt(int32_t tuningWord) |
Advance the accumulator by tuningWord and return the table sample (int32_t, full-scale ±INT32_MAX for DDSSineTable). |
nextSampleIncPhase(double phaseInc) |
Advance by a phase increment in radians. |
nextSampleAtFrequency(double frequency) |
Advance so the output runs at frequency (Hz) for the configured sampling frequency. |
getTableSize() |
Number of samples in the table. |
TableSize & DDSSineTable<>¶
enum class TableSize : uint8_t stores log2 of the sample count —
Samples64 … Samples65536 (values 6 … 16). tableSizeSamples(TableSize)
returns the actual sample count.
template<TableSize TS> struct DDSSineTable fills its sine table at
construction time and stores it in RAM as an ordinary member array —
SAMPLES × 4 bytes (Samples1024 = 4 KB); nothing is placed in flash. It
converts implicitly to int32_t*, so it can be handed straight to the DDS
constructor.
DDSSineTable<TableSize::Samples1024> table; // 4 KB of RAM
DDS osc(table, TableSize::Samples1024, 44100);
int32_t s = osc.nextSampleAtFrequency(440.0); // A4
Standalone table fillers¶
Free functions that fill caller-provided buffers.
| Function | Behavior |
|---|---|
fillSineTable(double sampleFrequency, double sineFrequency, int tableSize, int32_t* table, int bufferChannels = 1) |
Fill table with a sine at sineFrequency, duplicating each sample across interleaved channels when bufferChannels > 1. tableSize is the total entry count; every write stays within it. |
getTableSizeForFrequencyAtSampleRate(int sampleRate, int targetFrequency, int& tableSize, int& tablePeriods, double& actualFrequency) |
Find a table size near tableSize that fits an integer number of periods exactly, minimizing leakage when the table loops. Results returned by reference. |
createSquareSamples(uint16_t* data, int size, double periods, double amplitude) |
Fill data with periods cycles of a square wave scaled into uint16_t. |
createSineSamples(uint16_t* data, int size, double periods, double amp) |
Same for a sine wave, offset positive into uint16_t range. |
createSineSamples32(uint32_t* data, int size, double periods, double amp) |
Same into a uint32_t buffer. |
Driving the generator
- Call
setCarrier()before the firstnextSample(). The default carrier frequency is 0, so the phase never advances and the output stays flat. - AM and noise amplitudes are percentages (divided by 100 internally),
not linear gains —
setNoise(3.0)is 3 %, not 3×. DDSSineTable<>lives in RAM — the table is filled at construction time, so it can never move to flash.int32_t data[SAMPLES]atSamples65536is 256 KB — pick the smallest table that resolves your frequency.- The square waveform differs between helpers:
SignalGenerator's square carrier is −1/+1, butcreateSquareSamples()emits 0/1 (scaled toUINT16_MAX). Don't assume one range from the other. createSineSamples()andcreateSineSamples32()scale differently: the 16-bit version'sampscales the whole unipolar waveform, while the 32-bit version'sampscales only the AC swing around mid-scale.
Windows (signalcore/Windows.h)¶
WindowType¶
enum class WindowType : uint8_t — None, Rectangle, Hamming, Hann,
Triangle, Nuttall, Blackman, BlackmanNuttall, BlackmanHarris,
FlatTop, Welch. None and Rectangle apply no shaping.
| Function | Returns |
|---|---|
getWindowName(WindowType win, bool abbr = false) |
Full human-readable name (e.g. "Blackman-Harris"), or the short form when abbr = true. constexpr. |
getWindowNameAbbr(WindowType win) |
Short label for serial diagnostics (e.g. "Blk-Har"). constexpr. |
createWindowCoefficients(float* array, uint32_t size, WindowType windowType) |
Fill the first size/2 entries of array with the window's coefficients and return their sum. The FFT applies the window symmetrically, so only half the coefficients are stored. |
Most sketches never call createWindowCoefficients() directly — FFT::setWindowType()
does it for you. Call it when shaping a buffer outside the FFT class.
Note
createWindowCoefficients() writes only the first size/2 entries (the
window is symmetric; the FFT mirrors the index). The returned sum is the
amplitude-normalization divisor — it compensates the coherent gain lost to
the taper, so a windowed tone reports the same magnitude a rectangular one
would. It is not a power/noise-bandwidth normalization.
Trigger (signalcore/Trigger.h)¶
TriggerMode¶
enum class TriggerMode : uint8_t — None, RisingEdge, FallingEdge.
toString(TriggerMode) returns "Rising", "Falling", or "None".
class Trigger¶
Finds an edge in a sample buffer using Schmitt-trigger hysteresis and returns a pointer offset so the event sits at a chosen position in the output window — the standard way to stabilize an oscilloscope-style display.
| Member | Behavior |
|---|---|
Trigger(TriggerMode mode = RisingEdge, float threshold = TRIGGER_NO_THRESHOLD) |
Construct with a mode and optional fixed threshold. |
begin(TriggerMode mode, float threshold = TRIGGER_NO_THRESHOLD) |
Reconfigure mode and threshold and reset internal state. |
reset() |
Clear edge-detection state. |
calculateOffset(float* data, size_t outputSize, size_t inputCapacity) |
Search data for the trigger event and return a pointer to the start of an aligned outputSize window. Returns data unchanged if no trigger is found (including when the mode is None) — check valid() afterward. Relaxes its hysteresis margin over up to four passes before giving up. |
setThreshold(float threshold) |
Set a fixed trigger level. |
setPosition(float position) |
Where the event sits in the output window, 0.0–1.0 (default 0.5, centered). Clamped to range. |
setMinMax(float minValue, float maxValue) |
Supply the signal's min/max instead of having the trigger scan for them. |
active() |
true when the mode is not None. |
valid() |
true if the last calculateOffset() found an event. Always false after a call with mode None. |
mode() |
Current TriggerMode. |
When no threshold is set (TRIGGER_NO_THRESHOLD), the trigger auto-thresholds
at the signal midpoint. Two constants are exposed: TRIGGER_NO_THRESHOLD (the
"auto" sentinel) and NO_TRIGGER_FOUND (-1).
Trigger trig(TriggerMode::RisingEdge);
trig.setPosition(0.25f); // event 1/4 into the window
float* view = trig.calculateOffset(buffer, 480, 2048);
if (trig.valid()) { /* draw `view[0 .. 479]` */ }
Getting a stable trigger
inputCapacitymust exceedoutputSize— the search reserves room on both sides of the edge. Size the capture buffer to ~1.5× the output window.- Always gate on
valid(). A failed search returns the unaligned buffer at offset 0. min_/max_default to 0.0 / 3.3 (a 3.3 V ADC assumption). For other ranges, either leave the threshold unset socalculateOffsetscans for the extremes, or callsetMinMax()— otherwise the hysteresis margin is mis-scaled.- The hysteresis latch persists across calls. Call
reset()(orbegin()) when the signal source changes. - The trigger is not ring-buffer aware — linearize a wrapped capture before calling.
PeakHold (signalcore/PeakHold.h)¶
class PeakHoldProcessor¶
Per-element max-hold with exponential decay. The held value follows the input instantly when it rises and bleeds toward the input when it falls.
| Member | Behavior |
|---|---|
PeakHoldProcessor(size_t size, float decay = 0.95f, bool isActive = true) |
Construct for a vector of size elements. decay is the fraction of the previous peak kept each update (0.95 = keep 95 %, bleed 5 %). Allocates from the heap; copying is disabled (the copy constructor and copy assignment are deleted). |
update(float* samples, size_t inputSize) |
Update the held peaks from samples (processes min(inputSize, size) elements). |
update(float* samples) |
Convenience overload; samples must hold at least size() elements. |
reset() |
Reset every held value to −∞. Values read before the next update() are −∞. |
init(size_t newSize) |
Reallocate to a new size and reset. |
setDecay(float factor) / getDecay() |
Change or read the decay factor. |
operator[](size_t index) |
Read one held value. |
operator float*() |
Implicit conversion to the held-value buffer. |
size() |
Element count. |
bool active |
Public flag for sketch-side enable/disable; the library does not gate on it. |
PeakHoldProcessor hold(512, 0.98f); // slow decay
hold.update((float*)fft, 512); // max-hold over a spectrum
float* bars = hold;
MovingAverage (signalcore/MovingAverage.h)¶
class ExponentialMovingAverage¶
Per-element single-pole IIR smoother. sampleWeight (the smoothing factor often
called alpha) weights the newest sample: sampleWeight = 1.0 disables smoothing,
sampleWeight = 0.0 freezes the output, and small values smooth harder and
settle more slowly.
| Member | Behavior |
|---|---|
ExponentialMovingAverage(size_t size, float sampleWeight = 0.2f, bool isActive = true) |
Construct for a vector of size elements. Allocates from the heap; copying is disabled (the copy constructor and copy assignment are deleted). |
update(float* samples, size_t inputSize) |
Blend samples into the running average (processes min(inputSize, size) elements). The first call after construction, init(), or reset() copies the input directly, avoiding a long ramp-up from zero. |
update(float* samples) |
Convenience overload; samples must hold at least size() elements. |
reset() |
Re-seed: the next update() copies its input verbatim instead of blending. |
init(size_t newSize) |
Reallocate to a new size and re-seed on the next update(). |
setSampleWeight(float sampleWeight) / getSampleWeight() |
Set or read the smoothing weight. |
operator[](size_t index) |
Read one smoothed value. |
operator float*() |
Implicit conversion to the smoothed-value buffer. |
size() |
Element count. |
bool active |
Public enable/disable flag for sketch-side use. |
ExponentialMovingAverage ema(1, 0.2f); // smooth a single channel
float raw = analogRead(A0);
ema.update(&raw);
float smoothed = ema[0];
Sizing and tuning the smoothers
decay(peak-hold) andsampleWeight(EMA) are inverse conventions:decayis the fraction of the old value kept (higher = slower falloff);sampleWeightis the weight on the new sample (higher = faster response, less smoothing). Don't copy one constant into the other.- Both classes take an optional
inputSizeonupdate()and clamp tomin(inputSize, size). The single-argument overloads assumesamplesholds at leastsize()elements — use the two-argument form when the input length can vary. - Size both to
N/2 + 1when smoothing a full FFT result. - Both allocate from the heap in their constructor — construct once early in
setup()on RAM-tight boards (AVR) to avoid fragmentation. - The
activeflag is advisory: neither class checks it insideupdate(), so the caller must honor it.
Platform (signalcore/Platform.h)¶
Mostly compile-time machinery (see Configuration & Platforms), plus one runtime helper:
| Function | Behavior |
|---|---|
wrapPhase(double phase) |
Wrap a phase value into [0, 2π). Handles large positive or negative excursions (multi-period inputs), so phase accumulators can overshoot at high modulation rates without breaking. |
Created by VoidLoop · Founded by Gregory Kovacs · Written by Zachariah Magee