Skip to content

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::N32FFTSize::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 + 1 bins). Pass N/2 + 1 as the size to SINAD, ENOB, and findPeak — never N; anything above result[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 / ENOB use libm log10f; postScaleDBFast and signalToNoiseRatioDB use the log10fFast approximation. 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 a static_assert failure, not a runtime fallback.

DDS & Signal Generation (signalcore/DDS.h)

SignalShape

enum class SignalShape : uint8_tSine, 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 — Samples64Samples65536 (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 first nextSample(). 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] at Samples65536 is 256 KB — pick the smallest table that resolves your frequency.
  • The square waveform differs between helpers: SignalGenerator's square carrier is −1/+1, but createSquareSamples() emits 0/1 (scaled to UINT16_MAX). Don't assume one range from the other.
  • createSineSamples() and createSineSamples32() scale differently: the 16-bit version's amp scales the whole unipolar waveform, while the 32-bit version's amp scales only the AC swing around mid-scale.

Windows (signalcore/Windows.h)

WindowType

enum class WindowType : uint8_tNone, 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_tNone, 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.01.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

  • inputCapacity must exceed outputSize — 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 so calculateOffset scans for the extremes, or call setMinMax() — otherwise the hysteresis margin is mis-scaled.
  • The hysteresis latch persists across calls. Call reset() (or begin()) 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) and sampleWeight (EMA) are inverse conventions: decay is the fraction of the old value kept (higher = slower falloff); sampleWeight is the weight on the new sample (higher = faster response, less smoothing). Don't copy one constant into the other.
  • Both classes take an optional inputSize on update() and clamp to min(inputSize, size). The single-argument overloads assume samples holds at least size() elements — use the two-argument form when the input length can vary.
  • Size both to N/2 + 1 when 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 active flag is advisory: neither class checks it inside update(), 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