std1.0.0-beta-7 SafeC docs

Source dsp/sample_filter.h

View in the repository

#pragma once
// SafeC Standard Library — streaming DSP as composable traits.
//
// std/dsp's streaming filters share one shape: process() one sample,
// process_block() a buffer (out may alias in), reset() the state to
// silence. The traits below name each capability on its own and compose
// into the roles generic code asks for (trait composition: 'trait
// SampleFilter : SampleProcessor + BlockProcessor + Resettable {}' requires
// all three). The generic helpers after them are written once and ask only
// for what they use; each call is resolved at compile time (no dispatch at
// run time, so a generic wrapper around a hand-tuned method costs nothing).
//
//   SampleProcessor  process(x): one sample in, one out
//   BlockProcessor   process_block(in, out)  (slices)
//   Resettable       reset(): back to silence
//   SampleFilter     SampleProcessor + BlockProcessor + Resettable:
//                      double  FirFilter, IirFilter, Biquad, CombFF, CombFB
//                      float   FirFilter32, IirFilter32, Biquad32,
//                              CombFF32, CombFB32
//                      Q8.24   FFirFilter, FIirFilter, FBiquad,
//                              FCombFF, FCombFB
//                      and FilterChain<S, A, B> of any two of one type
//   SampleGenerator  next(): one sample out, no input: KarplusStrong,
//                    KarplusStrong32, FKarplusStrong
//
// One set of traits covers all three sample types. A trait is satisfied by
// method name, so the signatures below are written with double, and a
// float or Q8.24 (Fixed) filter conforms the same way; the helpers take
// the sample type as a parameter of their own (S), inferred from the
// buffers. Your own filter works with them (and with any
// generic<F: SampleFilter> code) as soon as it has the methods.
//
//   struct Biquad hp = Biquad::highpass(48000.0, 80.0, 0.7071);
//   struct Biquad lp = Biquad::lowpass(48000.0, 8000.0, 0.7071);
//   struct FilterChain<double, struct Biquad, struct Biquad> band =
//       FilterChain::of(hp, lp);
//   band.process_block(in, out);                     // band-limit a block (slices)
//
//   double h[64];
//   filter_impulse_response(&fir, h[0..64]);         // h[i] = taps, then 0
//
// The per-type process_block bodies with a fast path of their own (FIR's
// sliding dot products, the biquad's look-ahead recurrence) stay hand-
// written; the others are filter_process_each over process().
#include <std/dsp/fixed.h>
#include <std/traits.h>   // Resettable

namespace std {

trait SampleProcessor {
    double process(double x);
}

trait BlockProcessor {
    // out[i] = process(in[i]) for i < min(in.len, out.len); out may be in.
    void process_block([]const double in, []double out);
}

trait SampleFilter : SampleProcessor + BlockProcessor + Resettable {}

trait SampleGenerator {
    // The next output sample (no input: the generator runs on its state).
    double next();
}

// ── Sample constants ──────────────────────────────────────────────────────────
// 1.0 as a sample of type S: FIXED_ONE for Q8.24, 1.0 for double/float.
// The test tells Fixed (an int newtype, where 0.5 truncates to 0) from the
// floating types; the optimizer folds it. (It is a plain 'if': 'if const'
// doesn't fold a cast to a newtype yet, so it can't make that test.)

generic<S>
inline S sample_one() {
    if ((S)0.5 == (S)0) { return (S)FIXED_ONE; }
    return (S)1.0;
}

// ── Per-sample block loop ─────────────────────────────────────────────────────
// out[i] = f.process(in[i]), one sample at a time (in == out is fine): the
// process_block of a filter with no faster block form.

generic<F: SampleProcessor, S>
void filter_process_each_n_(&F f, const &S in, &S out, unsigned long n) {
    unsigned long i = 0UL;
    while (i < n) {
        S xi;
        unsafe { xi = in[i]; }
        S yi = f.process(xi);
        unsafe { out[i] = yi; }
        i = i + 1UL;
    }
}

generic<F: SampleProcessor, S>
void filter_process_each(&F f, []const S in, []S out) {
    unsigned long n = in.len < out.len ? in.len : out.len;
    if (n == 0UL) return;
    unsafe { filter_process_each_n_(f, (const &S)in.ptr, (&S)out.ptr, n); }
}

// ── Impulse and step response ─────────────────────────────────────────────────
// The first n samples of f's response to a unit impulse (1, 0, 0, ...) or
// a unit step (1, 1, 1, ...), from a reset state. f is left reset, ready
// for real input. For a FIR the impulse response is its taps.

generic<F: SampleProcessor + Resettable, S>
void filter_impulse_response_n_(&F f, &S out, unsigned long n) {
    S one = sample_one();
    f.reset();
    unsigned long i = 0UL;
    while (i < n) {
        S yi = f.process(i == 0UL ? one : (S)0);
        unsafe { out[i] = yi; }
        i = i + 1UL;
    }
    f.reset();
}

generic<F: SampleProcessor + Resettable, S>
void filter_impulse_response(&F f, []S out) {
    if (out.len == 0UL) return;
    unsafe { filter_impulse_response_n_(f, (&S)out.ptr, out.len); }
}

generic<F: SampleProcessor + Resettable, S>
void filter_step_response_n_(&F f, &S out, unsigned long n) {
    S one = sample_one();
    f.reset();
    unsigned long i = 0UL;
    while (i < n) {
        S yi = f.process(one);
        unsafe { out[i] = yi; }
        i = i + 1UL;
    }
    f.reset();
}

generic<F: SampleProcessor + Resettable, S>
void filter_step_response(&F f, []S out) {
    if (out.len == 0UL) return;
    unsafe { filter_step_response_n_(f, (&S)out.ptr, out.len); }
}

// ── Generators ────────────────────────────────────────────────────────────────
// out[i] = g.next() for i < n.

generic<G: SampleGenerator, S>
void generate_block_n_(&G g, &S out, unsigned long n) {
    unsigned long i = 0UL;
    while (i < n) {
        S yi = g.next();
        unsafe { out[i] = yi; }
        i = i + 1UL;
    }
}

generic<G: SampleGenerator, S>
void generate_block(&G g, []S out) {
    if (out.len == 0UL) return;
    unsafe { generate_block_n_(g, (&S)out.ptr, out.len); }
}

// ── Cascade ───────────────────────────────────────────────────────────────────
// Two filters in series, itself a SampleFilter: process(x) is
// second(first(x)), and process_block runs each stage over the whole block
// with its own process_block (so each keeps its fast path), using 'out' as
// the intermediate buffer (no scratch; out may alias in). S is the sample
// type (double, float or Fixed). Chains nest, so any number of stages is a
// chain of chains:
//
//   struct FilterChain<double, struct Biquad, struct Biquad> band =
//       FilterChain::of(hp, lp);
//   struct FilterChain<double, struct FilterChain<double, struct Biquad,
//       struct Biquad>, struct FirFilter> eq = FilterChain::of(band, fir);
//
// of() copies the stages into the chain; reach them as .first / .second.
// A stage that keeps its history in your buffer (FIR, IIR, comb) still
// uses that buffer, so don't run the original alongside the chain's copy.

generic<S, A: SampleFilter, B: SampleFilter>
struct FilterChain {
    A first;
    B second;

    S    process(S x);
    void reset();
    void process_block([]const S in, []S out);

    static struct FilterChain<S, A, B> of(A first, B second);
};

generic<S, A: SampleFilter, B: SampleFilter>
inline S FilterChain::process(S x) {
    return self.second.process(self.first.process(x));
}

generic<S, A: SampleFilter, B: SampleFilter>
void FilterChain::reset() {
    self.first.reset();
    self.second.reset();
}

generic<S, A: SampleFilter, B: SampleFilter>
void FilterChain::process_block([]const S in, []S out) {
    self.first.process_block(in, out);
    self.second.process_block(out, out);
}

generic<S, A: SampleFilter, B: SampleFilter>
struct FilterChain<S, A, B> FilterChain::of(A first, B second) {
    struct FilterChain<S, A, B> c;
    c.first = first;
    c.second = second;
    return c;
}

} // namespace std