lofi.h
Degradation on purpose: bit depth, sample rate, wavefolding.
#include <sonore/lofi.h>- 4types
- 1other declaration
- Guides: Getting started, MIDI and MPE
On this page
Design notes
Three tiny nonlinearities and one wrapper, and the wrapper is the point. The quantiser and the folder are the most violently aliasing shapes a plugin reaches for -- a fold turns one sine into dozens of harmonics, most of them above Nyquist -- and a model asked for "a bitcrusher" writes the one-liner and ships the aliasing with it. Oversampled<> pairs any per-sample shaper with the SDK's half-band cascade so the folded product lands where the filter removes it, and reports the latency the host has to be told about.
The DECIMATOR is deliberately not oversampled, and the reason is worth stating: its aliasing is the effect. A sample-and-hold at 8 kHz is supposed to sound like an 8 kHz converter, images and all; filtering them off would leave a dull lowpass and no lo-fi. Wrap it anyway if that is the sound you want -- nothing prevents it -- but it is not the default for a reason.
Included by dsp.h.
Types 4
Word-length reduction. bits may be fractional -- 3.5 bits is a perfectly good step size, and sweeping the control through integers only would step. At 24 bits the step is below float's own resolution and this is transparent.
Sample-rate reduction by sample-and-hold, to any rate, including one that does not divide the host's. A phase accumulator decides when to take a new sample, so 11.025 kHz inside 48 kHz holds for 4.35 samples on average rather than snapping to 4 or 5 and sounding like a different rate.
Triangle wavefolding: what goes past +/-1 is reflected back, as many times as it takes. Pushing a sine into it adds harmonics that a clipper cannot -- a clipper flattens the top of the wave, a folder puts a new wave on it -- and the timbre keeps changing with drive long after a clipper has become a square.
fold() is the closed form of "reflect until inside": the input mapped onto a triangle wave of period 4, which is one fmod and one abs per sample instead of a loop that runs longer the harder it is driven.
bias shifts the input before folding. A symmetric fold makes odd harmonics only; a little bias breaks the symmetry and lets the even ones in.
template <typename Shaper, int Stages = 2, typename Filter = HalfBandFilter<33>> class Oversampled
Any per-sample shaper, run at 2^Stages times the rate.
The wrapper owns the shaper (reach it through shaper() to set its controls) and the half-band cascade from resample.h. The shaper must be a plain float process(float); its own state, if any, advances at the OVERSAMPLED rate, which is right for a memoryless shape and something to know about for one with a time constant.
latencySamples() is the cascade's delay at the base rate, an integer by construction (see resample.h for why 32x is the ceiling), and it is the number the plugin must report. The IIR flavour has none, at the cost of phase linearity -- OversampledIir<> for when a parallel path does not need to null.
Aliases 1
- using
template <typename Shaper, int Stages = 2> using OversampledIir = Oversampled<Shaper, Stages, PolyphaseIirHalfBand<9>>Oversampled with the polyphase IIR half-band: no latency, at the cost of phase linearity, so a dry/wet mix with it does not null.lofi.h:216