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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

- Added a `TuningMap` class (`midi/yup_TuningMap.h`): maps MIDI note numbers to frequencies under an arbitrary scale and key map, loading Scala `.scl` scale files and `.kbm` key map files via `loadScale()` / `loadKeyMap()` (which return a `yup::Result` and keep the previous tuning when a file fails to parse). `isNoteActive()` reports the notes a key map asks to retune, taken from the range in its header unless the file carries `< first last` lines, which declare it instead

- `FFTProcessor` is now templated on the sample type - `FFTProcessor<float>` (the default) or `FFTProcessor<double>` - and every backend (PFFFT, Apple vDSP, Intel IPP, FFTW3 and the Ooura fallback, which now ships both a `float` and a `double` implementation) gained a native double-precision path. References to the nested scaling enum need qualifying, e.g. `FFTProcessor<float>::FFTScaling::asymmetric`

### Graphics

- `Image::getWidth()` and `Image::getHeight()` now return 0 on an invalid image instead of asserting and dereferencing null. Other accessors and pixel access still assert, as documented
Expand Down Expand Up @@ -164,6 +166,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Fixed `CodeEditor` reshaping (tokenizing, laying out and re-tessellating) the entire document on every single edit, making typing in a large file cost seconds per keystroke (measured: 2s for one backspace in a large file, mostly `StyledText::update()`). `styledText` now only ever holds the currently visible lines rather than the whole document; scrolling reshapes just the newly-visible range. Selection, search highlights, the caret, and Up/Down arrow navigation were adjusted to work correctly when their target is outside the currently-shaped range (falling back to an exact document-position computation rather than depending on `styledText`). Components that never call `setSize()`/`setBounds()` on their `CodeEditor` (as none of its unit tests do) keep shaping the whole document, since there's no meaningful "visible range" to restrict to without a real size.
- `CodeEditor` now renders through a `CodeEditorScheme` (new `code/yup_CodeEditorScheme.h`): every color — background, gutter, caret, current line, selection, search highlight, breakpoint and the per-token syntax colors — is stored keyed by `Identifier` (`CodeEditorScheme::setColor` / `getColor`, string constants in `CodeEditorScheme::ColorId`) and switched with `CodeEditor::setScheme`. Built-in well-known schemes are provided via `CodeEditorScheme::getBuiltIn`: `monokai`, `alabaster`, `oneDark`, `solarizedDark` and `solarizedLight`. The editor's painting moved into the theme (Themes v1) as a registered `CodeEditor` component style, and a vertical auto-hide `ScrollBar` now appears when the document overflows the viewport. The `CodeEditor` demo gained a scheme dropdown.

### Audio GUI (`yup_audio_gui`)

- `SpectrogramComponent` now keeps its waterfall history on the GPU: a precompiled `.ysl` shader bundle (embedded in `yup_SpectrogramComponentShader.inc`, built with the `yup_shader_bundler` host tool) drives a single fullscreen-triangle `GpuRenderPass` (see `GpuPipeline`) that scrolls the previous frame down by the pending rows and writes the new rows with the color map applied entirely on the GPU, uploading only the raw magnitudes as a uniform buffer - no per-paint CPU pixel upload, no GPU texture creation, and no 2D canvas flush (if the bundle cannot be compiled no waterfall is rendered). Pending FFT rows are always consumed (applied or dropped) so the update queue can never accumulate. The log-frequency → FFT-bin mapping is precomputed once per configuration instead of recomputed with pow/log per row, and the frequency grid (lines + labels) is cached in an offscreen canvas and only re-rendered when the frequency range or size changes. The component now requires a GPU render context (the CPU `Image` fallback was removed). The component's per-frame `refreshDisplay` hook processes pending FFT rows, and the history is presented at a fractional vertical offset that advances at the FFT row rate, so the waterfall scrolls smoothly between rows instead of jumping a row per update; the offset is clamped to a single row so bursts of FFT rows can never push the waterfall off-screen. The scroll speed is adjustable via the new `setScrollSpeed()` multiplier (1.0 = realtime, 0.0 = paused).
- `SpectrogramComponent` waterfall failures (shader bundle load, pipeline compile, and GPU pass encode/draw) are now reported via `Logger::outputDebugString` in all build configurations instead of silently dropping pending rows, and the waterfall texture's render resolution is exposed as the new `defaultSpectrogramRenderWidth` constant (2x the frequency-bin count - `getSpectrogramImage()` returns that full-resolution image).
- Fixed `SpectrumAnalyzerState` never flagging FFT data as ready after a single bulk `pushSamples()`: the readiness check ran before the scoped FIFO write had committed (the `AbstractFifo::ScopedWrite` commits in its destructor), so `isFFTDataReady()` stayed false until a second push arrived. `pushSample()`/`pushSamples()` now commit the write before checking, so a pushed window is immediately available to `SpectrogramComponent::refreshDisplay()` instead of leaving the backlog untouched.
- `SpectrumAnalyzerComponent` and `SpectrogramComponent` no longer snap every display point to its nearest FFT bin, which rendered identical levels (a flat staircase) for all the points sharing one bin. The new `SpectrumBinMapping` helper (`displays/yup_SpectrumBinMapping.h`) holds the shared log-frequency → fractional FFT bin mapping and evaluates levels continuously: the three bins surrounding a fractional position are parabolically interpolated in the amplitude decibel domain, evaluated at that position rather than at the vertex of the parabola, with a monotone linear fallback where the neighbours are not concave so a steep bin pair cannot undershoot. Display bands are aggregated over their fractional edges - `peak` for the peak/RMS level modes, `sum` (the levels integrated across the band width, in bin units) for `powerDecibels` and `mean` for `powerSpectralDensity` - so bins entering or leaving a band no longer step the displayed level. In the power modes this makes the band power an integral over the band's bandwidth instead of a sum over the integer bins its edges happen to touch
- `SpectrumAnalyzerComponent` builds its spectrum outline once per pixel column, interpolating between the 512 smoothed display points, so the curve stays continuous at any component width and at HiDPI scales instead of following the 512-point polyline verbatim. The private `computeSpectrumPath (Path, …)` became `createSpectrumPath (const Rectangle<float>&, bool)` returning a `Path`, removing its reliance on `Path` sharing its `rive::rcp<RiveRenderPath>` between copies

#### Layout

Expand Down
58 changes: 22 additions & 36 deletions docs/dsp/frequency.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ implementation the module can fall back on.

## FFTProcessor

`FFTProcessor` is a multi-backend, float-only FFT engine with a unified
interface. The best available backend is selected **at compile time**, in this
priority order:
`FFTProcessor<SampleType>` is a multi-backend FFT engine with a unified
interface, available in `float` and `double` precision. The best available
backend is selected **at compile time**, in this priority order:

1. **PFFFT** (`YUP_FFT_USING_PFFFT`)
2. **Apple vDSP** (`YUP_FFT_USING_VDSP`, via the `Accelerate` framework)
Expand All @@ -20,12 +20,26 @@ The engine is non-copyable and move-only; `getBackendName()` reports which
backend is active (`"PFFFT"`, `"Apple vDSP"`, `"Intel IPP"`, `"FFTW3"`,
`"Ooura FFT"`, or `"Unknown"`).

### Precision

The processing precision is the template argument — `FFTProcessor<float>`
(the default) or `FFTProcessor<double>`. Each backend uses its native
double-precision path where the underlying library exposes one: PFFFT
(`pffft_` / `pffftd_`), Apple vDSP (`vDSP_…` / `vDSP_…D`), Intel IPP
(`_32f` / `_64f`) and FFTW3 (`fftwf_` / `fftw_`). The Ooura fallback ships both
precisions of its transform routines.

```cpp
FFTProcessor<float> fftFloat (512); // fastest
FFTProcessor<double> fftDouble (512); // higher precision
```

### Supported sizes and layout

FFT sizes are powers of two in `[64, 65536]`. Buffers are **interleaved
complex pairs** — `[re0, im0, re1, im1, ...]` — so an N-point complex spectrum
occupies `2 * N` floats. The engine handles backend-specific packed layouts
(e.g. PFFFT's `[DC, Nyquist, re1, im1, ...]`, Ooura's real-DFT packing)
occupies `2 * N` sample values. The engine handles backend-specific packed
layouts (e.g. PFFFT's `[DC, Nyquist, re1, im1, ...]`, Ooura's real-DFT packing)
internally, presenting the same interleaved format to the caller for every
backend.

Expand All @@ -40,16 +54,16 @@ backend.
| `asymmetric` | inverse scaled by `1/N`, forward unscaled |

```cpp
FFTProcessor fft (512);
FFTProcessor<float> fft (512);
std::vector<float> realInput (512), complexOutput (1024);

fft.performRealFFTForward (realInput.data(), complexOutput.data()); // R → C, 512 reals → 1024 floats
fft.performRealFFTForward (realInput.data(), complexOutput.data()); // R → C, 512 reals → 1024 samples
fft.performRealFFTInverse (complexOutput.data(), realInput.data()); // C → R

fft.performComplexFFTForward (complexInput, complexOutput); // C → C
fft.performComplexFFTInverse (complexInput, complexOutput);

fft.setScaling (FFTProcessor::FFTScaling::unitary);
fft.setScaling (FFTProcessor<float>::FFTScaling::unitary);
fft.setSize (1024); // re-initialize for a new power-of-two size
```

Expand Down Expand Up @@ -96,34 +110,6 @@ Key methods:
data).
- `reset()` — clears the FIFO and the ready flag.

## OouraFFT8g

`yup_OouraFFT8g.h` exposes Takuya Ooura's classic **FFT8g** suite: single-
dimension, power-of-two, split-radix, decimation-in-frequency, in-place,
table-based transforms (public-domain ISC license, © 1996–2001 Ooura). These
are the primitives used by the Ooura backend of `FFTProcessor`, and are also
available directly:

- `cdft (n, isgn, a, ip, w)` — complex DFT; `n = 2 × (#complex points)`;
`isgn = 1` forward, `-1` inverse; in-place.
- `rdft (n, isgn, a, ip, w)` — real DFT; packed output
`[DC, Nyquist, Re1, Im1, Re2, Im2, ...]`; in-place.
- `ddct` / `ddst` — discrete cosine / sine transforms.
- `dfct` / `dfst` — cosine / sine transforms of a real DFT, needing an extra
scratch buffer `t`.

The work areas follow Ooura's original contract: `ip[0]` must be `0` on first
use (initialization flag), `ip` needs `2 + sqrt(n/2)` ints, and `w` needs
`n/2` floats:

```cpp
std::vector<float> a (1024); // 512 complex points
std::vector<int> ip (2 + int (std::sqrt (512)));
std::vector<float> w (512);
ip[0] = 0; // first call only
yup::cdft (1024, 1, a.data(), ip.data(), w.data()); // forward complex FFT, in-place
```

## Related

- [Windowing](math.md) — pair `FFTProcessor` with `WindowFunctions` for
Expand Down
2 changes: 1 addition & 1 deletion examples/audiograph/source/nodes/AnalyzerNodes.h
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ class SpectrumAnalyzerDisplayComponent final
SpectrumAnalyzerProcessor& processor;
yup::Color accentColor;
int fftSize = 0;
yup::FFTProcessor fftProcessor;
yup::FFTProcessor<float> fftProcessor;
std::vector<float> fftInput;
std::vector<float> fftOutput;
std::vector<float> window;
Expand Down
Loading
Loading