Skip to content
Merged
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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ 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`

- Fixed the PFFFT-backed `FFTProcessor` requiring its input and output buffers to be SIMD aligned: the transforms are now staged through buffers owned by the PFFFT backend and allocated with PFFFT's own aligned allocator, so the public API accepts buffers with any alignment (the double-precision real transform previously hit PFFFT's `VALIGNED` assertion when handed a plain `std::vector<double>`)

- Fixed the double-precision Ooura FFT translation unit only building on GCC/Clang: it declared every internal helper (`makewt`, `cftfsub`, `bitrv2`, ...) inside the body of the functions that call them, and a block-scope declaration inside `namespace yup` declares a *global* function, so `yup::cdft` referenced a `::makewt` that no one defined and the Windows link failed with 30 unresolved externals. The declarations now sit at namespace scope, matching `yup_OouraFFT8g_float.cpp`

### 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 @@ -148,6 +154,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### UI

- New `ComponentNative::RepaintMode` (`ComponentNative::Options::withRepaintMode`) selects how a window turns its accumulated dirty rectangles into repaint work. The default, `RepaintMode::disjointRegions`, repaints each dirty rectangle in isolation: a parent shared by several dirty rectangles is painted once, clipped to those rectangles, so components lying between two distant dirty rectangles are no longer repainted. `RepaintMode::boundingBox` keeps the previous behaviour of collapsing every dirty rectangle into one bounding box (repainting everything in between) and remains available as a fallback.
- A `paint()` that throws no longer leaves the graphics frame open. `SDLComponentNative::renderFrame()` called `context->begin()` inside its render lambda but `context->end()` only after that lambda returned, so an exception from user paint code skipped the `end()`/`tick()` pair and the next frame began on a context that had never been flushed. The pair now runs from a scope guard
- GUI: multitouch input on any platform with touch hardware (mobile, Emscripten in a mobile browser, and desktop touchscreens). Every finger is delivered through the existing mouse callbacks (`mouseDown` / `mouseDrag` / `mouseUp`, left button held): the first finger behaves exactly like a mouse, each additional finger arrives in parallel with a stable, dense finger index exposed as `MouseEvent::isTouch()` / `MouseEvent::getTouchIndex()`, plus touch pressure as `MouseEvent::getPressure()` (0.0-1.0). Each finger is hit-tested independently, keeps its index for the whole contact, and has its own double-click detection. SDL's synthetic touch-to-mouse events are disabled so every touch is delivered exactly once.
- New "Touch Trails" example in `examples/graphics`: draws one hue-spaced colored trail per pointer (each finger gets its own color via `MouseEvent::getTouchIndex()`, with a mouse as a single pointer on desktop). Points carry an age in fade-timer ticks - not the wall clock - and their size and opacity decay with that age, so late frames can't make trails jump or flicker; the fade timer also runs while a pointer is down, dissolving the trail until only the pressed point remains, and once the finger lifts that point fades out too.
Expand All @@ -164,6 +171,14 @@ 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 slides the newest row into place over one row period; that offset is pulled back with a fixed time constant instead of being clamped, so the motion never stalls at the edge of its window. The scroll speed is adjustable via the new `setScrollSpeed()` multiplier (1.0 = realtime, 0.0 = paused).
- `SpectrogramComponent`'s waterfall scroll is now independent of the frame rate. The component requests a repaint on every frame while the waterfall is live (through the existing `refreshDisplay` hook) instead of only when an FFT row arrives, so the sub-row offset above is actually rendered - previously the repaint cadence matched the row arrival cadence exactly, which made the display advance one whole row per repaint (one pixel, for a component whose height matches the history). A frame that produced more FFT rows than one GPU pass can write (a pass writes `defaultSpectrogramMagnitudes / defaultSpectrogramWidth` rows) now drains them over several passes instead of dropping the surplus, which is what happened whenever the display ran below the FFT row rate: at 30 fps half the history of a 2048/1024 configuration was silently lost. `setScrollSpeed (0.0)` now truly freezes the waterfall (pending rows are discarded rather than written, so the content no longer keeps sliding down while paused), and the animation stops requesting repaints once the analysis stalls instead of spinning on a frozen frame
- `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 Expand Up @@ -360,6 +375,7 @@ The `FlexBox` and `Grid` containers landed in this cycle (they were previously l

### Bug Fixes

- SDL windowing: partial repaints now grow the dirty area by half a pixel before rounding it out to whole pixels. Rive applies rectangular clips as anti-aliased coverage rather than a pixel-exact scissor, so geometry touching a component's clip edge could bleed a tiny coverage into the adjacent pixel row; with a preserved render target that row was never redrawn and the bleed accumulated into a persistent line just outside components that repaint continuously (visible around the `SpectrogramComponent` at 1x scale). The extra border lets the parent repaint those pixels every frame.
- AUv2 wrapper: an input bus that is not fed during a render cycle is now presented to the processor as a null-channel view. `buildInputBusViews` filled the per-bus channel pointers only for the channels it actually received, leaving the rest at whatever the previous render had stored there, so a sidechain input the host stopped feeding (inactive element or a failed `PullInput`) kept pointing at that element's stale audio instead of reading as silent - contradicting the comment on `pullAuxiliaryInputElements` and the `AudioBusBufferView` "null for an inactive or silent bus" contract. The input path now clears each bus's slots first, exactly as `buildOutputBusViews` already did for outputs; the AUv3 wrapper never had the problem because it maps input views onto its own scratch buffers
- `MessageManager` on Apple platforms: `runDispatchLoop()` and `runDispatchLoopUntil()` now wrap their loop body in `YUP_TRY`/`YUP_CATCH_EXCEPTION`, matching the generic implementations in `yup_MessageManager.cpp` that `#if ! (YUP_MAC || YUP_IOS || YUP_WASM)` compiles out on these platforms. The `.mm` replacements previously caught only `NSException`, a disjoint set from `std::exception`, so a C++ exception thrown by a message callback escaped the dispatch loop instead of reaching `YUPApplicationBase::sendUnhandledException()` — which made `unhandledException()` unreachable on macOS and iOS, and killed the application on the first failure.
- The SDL render thread now routes exceptions from `renderFrame()` through `YUP_CATCH_EXCEPTION` as well. It never passes through a dispatch loop, so an exception escaping `paint()` reached `Thread::threadEntryPoint()`, which only asserts — rendering then stopped permanently with no diagnostic in release builds.
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
18 changes: 18 additions & 0 deletions docs/ui/component-basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,24 @@ comp.repaint (10, 10, 100, 100); // x, y, w, h overload
this in debug builds.
```

### Multiple dirty areas (`RepaintMode`)

When several disjoint areas of a window are dirty in the same frame, the framework
repaints each dirty rectangle in isolation: a parent shared by several dirty
rectangles is painted once, clipped to those rectangles, and only the parts of the
hierarchy that overlap a dirty rectangle are repainted. Anything between two distant
dirty rectangles is left untouched, so it is not redrawn even though it did not
change. This is the default (`RepaintMode::disjointRegions`).

The older behaviour — collapsing every dirty rectangle into a single bounding box,
which repaints everything in between — remains available as a safe fallback:

```cpp
ComponentNative::Options opts;
opts.withRepaintMode (ComponentNative::RepaintMode::boundingBox);
rootComponent.addToDesktop (opts);
```

### Optimising paint with opacity

If a component is fully opaque (covers its background entirely), inform the
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