/**
 * @file minidsp.h
 * @brief A mini library of DSP (Digital Signal Processing) routines.
 *
 * This header declares functions for:
 *   - Basic signal measurements (energy, power, entropy)
 *   - Signal analysis (RMS, zero-crossing rate, autocorrelation, peak detection, F0 estimation, mixing)
 *   - Simple effects (delay/echo, tremolo, and comb-filter reverb)
 *   - Signal scaling and gain adjustment
 *   - Math utilities (Bessel I₀, normalized sinc)
 *   - Window generation (Hanning, Hamming, Blackman, rectangular, Kaiser)
 *   - Signal generators (sine, white noise, impulse, chirp, square, sawtooth, Shepard tone)
 *   - FIR filtering, convolution, and lowpass FIR design (time-domain and FFT overlap-add)
 *   - Sample rate conversion (polyphase sinc resampler)
 *   - FFT-based magnitude spectrum, power spectral density, STFT, mel filterbanks, MFCCs, and brickwall lowpass filtering
 *   - DTMF tone detection (ITU-T Q.24) and generation
 *   - Generalized Cross-Correlation (GCC-PHAT) for delay estimation
 *   - Spectrogram text art (synthesise audio that displays text in a spectrogram)
 *   - Audio steganography (hide, detect, and recover secret messages or binary data via LSB, frequency-band, or spectrogram-text encoding)
 *
 * These are the kinds of building blocks you'd use in an audio processing
 * pipeline -- for example, estimating which direction a sound came from
 * using a pair of microphones.
 */
#ifndef MINIDSP_H
#define MINIDSP_H

/* Version macros — injected by Makefile via -D flags.
 * Defaults here allow the header to compile standalone. */
#ifndef MINIDSP_VERSION_MAJOR
#define MINIDSP_VERSION_MAJOR 0
#endif
#ifndef MINIDSP_VERSION_MINOR
#define MINIDSP_VERSION_MINOR 0
#endif
#ifndef MINIDSP_VERSION_PATCH
#define MINIDSP_VERSION_PATCH 0
#endif
#ifndef MINIDSP_VERSION
#define MINIDSP_VERSION "0.0.0"
#endif

#include <string.h>
#include <assert.h>
#include <stdlib.h>
#include <math.h>
#include <stdbool.h>

/* M_PI is not guaranteed by the C standard.  Define it if the system didn't. */
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
#include <float.h>
#include <complex.h>
#include <fftw3.h>

/* -----------------------------------------------------------------------
 * Basic signal measurement functions
 * -----------------------------------------------------------------------*/

/**
 * Compute the dot product of two vectors.
 * The dot product is the sum of element-wise products: a[0]*b[0] + a[1]*b[1] + ...
 */
double MD_dot(const double *a, const double *b, unsigned N);

/**
 * Compute the normalized entropy of a distribution.
 * Returns a value between 0.0 (all energy concentrated in one bin)
 * and 1.0 (energy spread equally across all bins).
 *
 * @param clip  If true, ignore negative values. If false, square all values first.
 */
double MD_entropy(const double *a, unsigned N, bool clip);

/** Compute signal energy: sum of squared samples. */
double MD_energy(const double *a, unsigned N);

/** Compute signal power: energy divided by the number of samples. */
double MD_power(const double *a, unsigned N);

/** Compute signal power in decibels: 10 * log10(power). */
double MD_power_db(const double *a, unsigned N);

/* -----------------------------------------------------------------------
 * Signal analysis
 * -----------------------------------------------------------------------*/

/**
 * Compute the root mean square (RMS) of a signal.
 *
 * RMS is the standard measure of signal "loudness":
 *
 * \f[
 * \mathrm{RMS} = \sqrt{\frac{1}{N}\sum_{n=0}^{N-1} x[n]^2}
 * \f]
 *
 * Equivalently, `sqrt(MD_power(a, N))`.  A unit-amplitude sine wave
 * has RMS = 1/sqrt(2) ~ 0.707.  A DC signal of value c has RMS = |c|.
 *
 * @param a  Input signal of length N.
 * @param N  Number of samples.  Must be > 0.
 * @return   RMS value (always >= 0).
 *
 * @code
 * double sig[1024];
 * MD_sine_wave(sig, 1024, 1.0, 440.0, 44100.0);
 * double rms = MD_rms(sig, 1024);  // ~ 0.707
 * @endcode
 */
double MD_rms(const double *a, unsigned N);

/**
 * Compute the zero-crossing rate of a signal.
 *
 * The zero-crossing rate counts how often the signal changes sign,
 * normalised by the number of adjacent pairs:
 *
 * \f[
 * \mathrm{ZCR} = \frac{1}{N-1}\sum_{n=1}^{N-1}
 *     \mathbf{1}\!\bigl[\mathrm{sgn}(x[n]) \ne \mathrm{sgn}(x[n-1])\bigr]
 * \f]
 *
 * Returns a value in [0.0, 1.0].  High ZCR indicates noise or
 * high-frequency content; low ZCR indicates tonal or low-frequency
 * content.  Zero is treated as non-negative (standard convention).
 *
 * @param a  Input signal of length N.
 * @param N  Number of samples.  Must be > 1.
 * @return   Zero-crossing rate in [0.0, 1.0].
 *
 * @code
 * double sig[4096];
 * MD_sine_wave(sig, 4096, 1.0, 1000.0, 16000.0);
 * double zcr = MD_zero_crossing_rate(sig, 4096);
 * // zcr ~ 2 * 1000 / 16000 = 0.125
 * @endcode
 */
double MD_zero_crossing_rate(const double *a, unsigned N);

/**
 * Compute the normalised autocorrelation of a signal.
 *
 * The autocorrelation measures the similarity between a signal and a
 * delayed copy of itself.  This function computes the normalised
 * (biased) autocorrelation for lags 0 through max_lag-1:
 *
 * \f[
 * R[\tau] = \frac{1}{R[0]} \sum_{n=0}^{N-1-\tau} x[n]\,x[n+\tau]
 * \f]
 *
 * where \f$R[0] = \sum x[n]^2\f$ is the signal energy.
 * The output satisfies out[0] = 1.0 and |out[tau]| <= 1.0.
 * A silent signal (all zeros) produces all-zero output.
 *
 * @param a        Input signal of length N.
 * @param N        Number of samples.  Must be > 0.
 * @param out      Output array of length max_lag (caller-allocated).
 * @param max_lag  Number of lag values to compute.  Must be > 0 and < N.
 *
 * @code
 * double sig[1024];
 * MD_sine_wave(sig, 1024, 1.0, 100.0, 1000.0);
 * double acf[50];
 * MD_autocorrelation(sig, 1024, acf, 50);
 * // acf[0] = 1.0, acf[10] ~ 1.0 (lag = one period of 100 Hz at 1 kHz)
 * @endcode
 */
void MD_autocorrelation(const double *a, unsigned N,
                        double *out, unsigned max_lag);

/**
 * Detect peaks (local maxima) in a signal.
 *
 * A sample a[i] is a peak if it is strictly greater than both
 * immediate neighbours and above the given threshold.  The
 * min_distance parameter suppresses nearby secondary peaks:
 * after accepting a peak at index i, any candidate within
 * min_distance indices is skipped.
 *
 * Peaks are found left-to-right (deterministic tie-breaking).
 * Endpoint samples (i=0, i=N-1) are never peaks because they
 * lack two neighbours.
 *
 * @param a             Input signal of length N.
 * @param N             Number of samples.
 * @param threshold     Minimum value for a peak.
 * @param min_distance  Minimum index gap between accepted peaks (>= 1).
 * @param peaks_out     Output array of peak indices (caller-allocated,
 *                      worst case N elements).
 * @param num_peaks_out Receives the number of peaks found.
 *
 * @code
 * double sig[] = {0, 1, 3, 1, 0, 2, 5, 2, 0};
 * unsigned peaks[9], n;
 * MD_peak_detect(sig, 9, 0.0, 1, peaks, &n);
 * // n = 2, peaks = {2, 6}  (values 3 and 5)
 * @endcode
 */
void MD_peak_detect(const double *a, unsigned N, double threshold,
                    unsigned min_distance, unsigned *peaks_out,
                    unsigned *num_peaks_out);

/**
 * Estimate the fundamental frequency (F0) using autocorrelation.
 *
 * This method searches for the strongest local peak in the normalised
 * autocorrelation sequence over lags corresponding to the requested
 * frequency range:
 *
 * \f[
 * f_0 = \frac{f_s}{\tau_\text{peak}}
 * \f]
 *
 * where \f$\tau_\text{peak}\f$ is the selected lag and \f$f_s\f$ is the
 * sample rate.
 *
 * @param signal       Input signal of length N.
 * @param N            Number of samples (must be >= 2).
 * @param sample_rate  Sampling rate in Hz (must be > 0).
 * @param min_freq_hz  Minimum search frequency in Hz (must be > 0).
 * @param max_freq_hz  Maximum search frequency in Hz (must be > min_freq_hz).
 * @return             Estimated F0 in Hz, or 0.0 if no reliable F0 is found.
 *
 * @note Invalid API usage is guarded by assertions.  A return value of
 *       0.0 means the call was valid but no usable pitch peak was found
 *       (for example, silence or an unvoiced/noisy frame).
 *
 * @code
 * double f0 = MD_f0_autocorrelation(frame, frame_len, 16000.0, 80.0, 400.0);
 * @endcode
 */
double MD_f0_autocorrelation(const double *signal, unsigned N,
                             double sample_rate,
                             double min_freq_hz, double max_freq_hz);

/**
 * Estimate the fundamental frequency (F0) using FFT peak picking.
 *
 * The signal is internally Hann-windowed, transformed with a one-sided FFT,
 * then the dominant magnitude peak in the requested frequency range is mapped
 * back to Hz:
 *
 * \f[
 * f_0 = \frac{k_\text{peak} f_s}{N}
 * \f]
 *
 * where \f$k_\text{peak}\f$ is the selected FFT bin.
 *
 * This method is simple and fast but generally less robust than
 * MD_f0_autocorrelation() for noisy or strongly harmonic signals.
 *
 * @param signal       Input signal of length N.
 * @param N            Number of samples (must be >= 2).
 * @param sample_rate  Sampling rate in Hz (must be > 0).
 * @param min_freq_hz  Minimum search frequency in Hz (must be > 0).
 * @param max_freq_hz  Maximum search frequency in Hz (must be > min_freq_hz).
 * @return             Estimated F0 in Hz, or 0.0 if no reliable F0 is found.
 *
 * @note This function uses the internal FFT cache.  Call MD_shutdown()
 *       when done with FFT-based APIs.
 * @note Invalid API usage is guarded by assertions.  A return value of
 *       0.0 means the call was valid but no usable pitch peak was found.
 *
 * @code
 * double f0 = MD_f0_fft(frame, frame_len, 16000.0, 80.0, 400.0);
 * @endcode
 */
double MD_f0_fft(const double *signal, unsigned N,
                 double sample_rate,
                 double min_freq_hz, double max_freq_hz);

/**
 * Mix (weighted sum) two signals.
 *
 * Computes the element-wise weighted sum:
 *
 * \f[
 * \mathrm{out}[n] = w_a \cdot a[n] + w_b \cdot b[n]
 * \f]
 *
 * In-place safe: the output buffer may alias either input.
 *
 * @param a    First input signal of length N.
 * @param b    Second input signal of length N.
 * @param out  Output signal of length N (caller-allocated).
 * @param N    Number of samples.
 * @param w_a  Weight for signal a.
 * @param w_b  Weight for signal b.
 *
 * @code
 * double sine[1024], noise[1024], mix[1024];
 * MD_sine_wave(sine, 1024, 1.0, 440.0, 44100.0);
 * MD_white_noise(noise, 1024, 0.1, 42);
 * MD_mix(sine, noise, mix, 1024, 0.8, 0.2);
 * @endcode
 */
void MD_mix(const double *a, const double *b, double *out,
            unsigned N, double w_a, double w_b);

/* -----------------------------------------------------------------------
 * Simple effects
 * -----------------------------------------------------------------------*/

/**
 * Delay line / echo effect using a circular buffer with feedback.
 *
 * Let D = delay_samples. The internal delay state follows:
 *
 * \f[
 * s[n] = x[n] + feedback \cdot s[n-D]
 * \f]
 *
 * and output is:
 *
 * \f[
 * y[n] = dry \cdot x[n] + wet \cdot s[n-D]
 * \f]
 *
 * This creates repeating echoes decaying geometrically when
 * \f$|feedback| < 1\f$.  In-place safe: @p out may alias @p in.
 *
 * @param in             Input signal of length N.
 * @param out            Output signal of length N (caller-allocated).
 * @param N              Number of samples.
 * @param delay_samples  Delay length in samples (must be > 0).
 * @param feedback       Echo feedback gain (must satisfy |feedback| < 1).
 * @param dry            Dry (original) mix weight.
 * @param wet            Wet (delayed) mix weight.
 */
void MD_delay_echo(const double *in, double *out, unsigned N,
                   unsigned delay_samples, double feedback,
                   double dry, double wet);

/**
 * Tremolo effect (amplitude modulation by a sinusoidal LFO).
 *
 * The modulation gain is:
 *
 * \f[
 * g[n] = (1 - depth) + depth \cdot \frac{1 + \sin(2\pi f_{LFO} n / f_s)}{2}
 * \f]
 *
 * so \f$g[n]\f$ ranges from \f$1-depth\f$ to \f$1\f$.
 *
 * Output:
 *
 * \f[
 * y[n] = g[n] \cdot x[n]
 * \f]
 *
 * In-place safe: @p out may alias @p in.
 *
 * @param in           Input signal of length N.
 * @param out          Output signal of length N (caller-allocated).
 * @param N            Number of samples.
 * @param rate_hz      LFO rate in Hz (must be >= 0).
 * @param depth        Modulation depth in [0, 1].
 * @param sample_rate  Sampling rate in Hz (must be > 0).
 */
void MD_tremolo(const double *in, double *out, unsigned N,
                double rate_hz, double depth, double sample_rate);

/**
 * Comb-filter reverb (feedback comb filter with dry/wet mix).
 *
 * Internal comb section:
 *
 * \f[
 * c[n] = x[n] + feedback \cdot c[n-D]
 * \f]
 *
 * Final output:
 *
 * \f[
 * y[n] = dry \cdot x[n] + wet \cdot c[n]
 * \f]
 *
 * where \f$D\f$ is @p delay_samples.  In-place safe: @p out may alias @p in.
 *
 * @param in             Input signal of length N.
 * @param out            Output signal of length N (caller-allocated).
 * @param N              Number of samples.
 * @param delay_samples  Comb delay in samples (must be > 0).
 * @param feedback       Feedback gain (must satisfy |feedback| < 1).
 * @param dry            Dry (original) mix weight.
 * @param wet            Wet (comb output) mix weight.
 */
void MD_comb_reverb(const double *in, double *out, unsigned N,
                    unsigned delay_samples, double feedback,
                    double dry, double wet);

/* -----------------------------------------------------------------------
 * FIR filters / convolution
 * -----------------------------------------------------------------------*/

/**
 * Compute the output length of a full linear convolution.
 *
 * For input length N and kernel length M, full convolution length is N+M-1.
 */
unsigned MD_convolution_num_samples(unsigned signal_len, unsigned kernel_len);

/**
 * Time-domain full linear convolution (direct sum-of-products).
 *
 * Computes:
 *   out[n] = sum_{k=0}^{kernel_len-1} signal[n-k] * kernel[k]
 * with out-of-range signal samples treated as zero.
 *
 * @param signal      Input signal of length signal_len.
 * @param signal_len  Number of input samples (must be > 0).
 * @param kernel      FIR kernel of length kernel_len.
 * @param kernel_len  Number of FIR taps (must be > 0).
 * @param out         Output buffer of length signal_len + kernel_len - 1.
 */
void MD_convolution_time(const double *signal, unsigned signal_len,
                         const double *kernel, unsigned kernel_len,
                         double *out);

/**
 * Causal moving-average FIR filter with zero-padded startup.
 *
 * Computes:
 *   out[n] = (1/window_len) * sum_{k=0}^{window_len-1} signal[n-k]
 * where out-of-range samples (n-k < 0) are treated as zero.
 *
 * @param signal      Input signal of length signal_len.
 * @param signal_len  Number of input samples (must be > 0).
 * @param window_len  Moving-average window length (must be > 0).
 * @param out         Output buffer of length signal_len.
 */
void MD_moving_average(const double *signal, unsigned signal_len,
                       unsigned window_len, double *out);

/**
 * Apply a causal FIR filter with arbitrary coefficients.
 *
 * Computes:
 *   out[n] = sum_{k=0}^{num_taps-1} coeffs[k] * signal[n-k]
 * with out-of-range signal samples treated as zero.
 *
 * @param signal      Input signal of length signal_len.
 * @param signal_len  Number of input samples (must be > 0).
 * @param coeffs      FIR coefficients of length num_taps.
 * @param num_taps    Number of FIR taps (must be > 0).
 * @param out         Output buffer of length signal_len.
 */
void MD_fir_filter(const double *signal, unsigned signal_len,
                   const double *coeffs, unsigned num_taps,
                   double *out);

/**
 * Full linear convolution using FFT overlap-add (offline).
 *
 * Produces the same output as MD_convolution_time() but is faster for
 * longer kernels by processing blocks in the frequency domain.
 *
 * @param signal      Input signal of length signal_len.
 * @param signal_len  Number of input samples (must be > 0).
 * @param kernel      FIR kernel of length kernel_len.
 * @param kernel_len  Number of FIR taps (must be > 0).
 * @param out         Output buffer of length signal_len + kernel_len - 1.
 */
void MD_convolution_fft_ola(const double *signal, unsigned signal_len,
                            const double *kernel, unsigned kernel_len,
                            double *out);

/**
 * Design a Kaiser-windowed sinc lowpass FIR filter.
 *
 * Generates a linear-phase lowpass filter with the specified cutoff
 * frequency and Kaiser window shape.  The coefficients are normalized
 * to unity DC gain (they sum to 1.0).
 *
 * \f[
 *   h[i] = 2\,f_c\;\mathrm{sinc}\!\bigl(2\,f_c\,(i - (N-1)/2)\bigr)
 *          \;\cdot\; w_{\mathrm{Kaiser}}[i]
 * \f]
 * where \f$f_c = \mathrm{cutoff\_freq} / \mathrm{sample\_rate}\f$.
 *
 * @param coeffs       Output buffer of length num_taps (caller-allocated).
 * @param num_taps     Filter length (must be > 0).
 * @param cutoff_freq  -6 dB cutoff frequency in Hz (must be > 0
 *                     and < sample_rate / 2).
 * @param sample_rate  Sampling rate in Hz (must be > 0).
 * @param kaiser_beta  Kaiser window \f$\beta\f$ parameter (e.g. 10.0).
 *
 * @code
 * double h[65];
 * MD_design_lowpass_fir(h, 65, 4000.0, 48000.0, 10.0);  // 4 kHz LPF
 * @endcode
 */
void MD_design_lowpass_fir(double *coeffs, unsigned num_taps,
                           double cutoff_freq, double sample_rate,
                           double kaiser_beta);

/* -----------------------------------------------------------------------
 * Signal scaling and conditioning
 * -----------------------------------------------------------------------*/

/**
 * Map a single value from one range to another.
 * Example: MD_scale(5, 0, 10, 0, 100) returns 50.
 */
double MD_scale(double in,
                double oldmin, double oldmax,
                double newmin, double newmax);

/** Map every element of a vector from one range to another. */
void MD_scale_vec(double *in, double *out, unsigned N,
                  double oldmin, double oldmax,
                  double newmin, double newmax);

/**
 * Fit values within [newmin, newmax].
 * If all values already fit, they are copied unchanged.
 * Otherwise the entire vector is rescaled.
 */
void MD_fit_within_range(double *in, double *out, unsigned N,
                         double newmin, double newmax);

/**
 * Automatic Gain Control: scale a signal so its power matches
 * the requested dB level, then clip to [-1, 1].
 */
void MD_adjust_dblevel(const double *in, double *out,
                       unsigned N, double dblevel);

/* -----------------------------------------------------------------------
 * FFT / Spectrum Analysis
 * -----------------------------------------------------------------------*/

/**
 * Compute the magnitude spectrum of a real-valued signal.
 *
 * Given a signal of length N, this function computes the FFT and returns
 * the magnitude |X(k)| for each frequency bin.  Because the input is
 * real-valued, the FFT output is conjugate-symmetric, so only the first
 * N/2 + 1 bins are unique (from DC through Nyquist).
 *
 * To convert bin index k to a frequency in Hz:
 *   freq_k = k * sample_rate / N
 *
 * The output is **not** normalised by N -- divide each value by N to get
 * the "standard" DFT magnitude, or by N/2 (except DC and Nyquist) for
 * single-sided amplitude.
 *
 * @param signal   Input signal of length N.
 * @param N        Number of samples in the signal.
 * @param mag_out  Output array, must be pre-allocated to at least N/2 + 1
 *                 doubles.  On return, mag_out[k] = |X(k)|.
 *
 * @note The caller must allocate mag_out.  The required size is
 *       (N / 2 + 1) * sizeof(double).
 *
 * Example:
 * @code
 *   double signal[1024];
 *   // ... fill signal with audio samples ...
 *
 *   unsigned num_bins = 1024 / 2 + 1;  // = 513
 *   double *mag = malloc(num_bins * sizeof(double));
 *   MD_magnitude_spectrum(signal, 1024, mag);
 *
 *   // mag[0]   = DC component magnitude
 *   // mag[k]   = magnitude at frequency k * sample_rate / 1024
 *   // mag[512] = Nyquist frequency magnitude
 *
 *   free(mag);
 *   MD_shutdown();  // free cached FFT plans when done
 * @endcode
 */
void MD_magnitude_spectrum(const double *signal, unsigned N, double *mag_out);

/**
 * Compute the power spectral density (PSD) of a real-valued signal.
 *
 * The PSD describes how a signal's power is distributed across frequencies.
 * While the magnitude spectrum tells you the *amplitude* at each frequency,
 * the PSD tells you the *power* -- useful for noise analysis, SNR estimation,
 * and comparing signals of different lengths.
 *
 * This function computes the "periodogram" estimator:
 *
 *   PSD[k] = |X(k)|^2 / N  =  (Re(X(k))^2 + Im(X(k))^2) / N
 *
 * where X(k) is the DFT of the input signal (unnormalised, as computed by FFTW).
 *
 * **Relationship to the magnitude spectrum:**
 *   PSD[k] = |X(k)|^2 / N  =  (magnitude[k])^2 / N
 *
 * **Parseval's theorem (energy conservation):**
 *   The one-sided PSD sums to the total signal energy:
 *   PSD[0] + 2 * sum(PSD[1..N/2-1]) + PSD[N/2] = sum(x[n]^2)
 *
 * @param signal   Input signal of length N.
 * @param N        Number of samples in the signal (must be >= 2).
 * @param psd_out  Output array, must be pre-allocated to at least N/2 + 1
 *                 doubles.  On return, psd_out[k] = |X(k)|^2 / N.
 *
 * @note The caller must allocate psd_out.  The required size is
 *       (N / 2 + 1) * sizeof(double).
 *
 * Example:
 * @code
 *   double signal[1024];
 *   // ... fill signal with audio samples ...
 *
 *   unsigned num_bins = 1024 / 2 + 1;  // = 513
 *   double *psd = malloc(num_bins * sizeof(double));
 *   MD_power_spectral_density(signal, 1024, psd);
 *
 *   // psd[0]   = DC power
 *   // psd[k]   = power at frequency k * sample_rate / 1024
 *   // psd[512] = Nyquist frequency power
 *
 *   free(psd);
 *   MD_shutdown();  // free cached FFT plans when done
 * @endcode
 */
void MD_power_spectral_density(const double *signal, unsigned N, double *psd_out);

/**
 * @brief Compute the one-sided phase spectrum of a real signal.
 *
 * Returns the instantaneous phase angle \f$\phi(k) = \arg X(k)\f$ for each
 * DFT bin using an unnormalised real-to-complex FFT (FFTW r2c).  The phase
 * is expressed in **radians** in the range \f$[-\pi,\,\pi]\f$.
 *
 * For a real signal of length \f$N\f$, only the first \f$N/2+1\f$ bins carry
 * unique information (bins \f$N/2+1\ldots N-1\f$ are conjugate-symmetric
 * mirrors).  Accordingly, @p phase_out must be pre-allocated to hold at least
 * \f$N/2+1\f$ doubles.
 *
 * **Interpretation:**
 * - A pure cosine at an integer bin \f$k_0\f$ (exact period in \f$N\f$
 *   samples) produces \f$\phi(k_0) = 0\f$.
 * - A pure sine at the same bin produces \f$\phi(k_0) = -\pi/2\f$.
 * - A time-delayed signal exhibits **linear phase**: \f$\phi(k) = -2\pi k d/N\f$,
 *   where \f$d\f$ is the delay in samples.
 *
 * @note Phase values at bins where the magnitude is near zero are numerically
 *       unreliable.  Always examine MD_magnitude_spectrum() alongside the
 *       phase spectrum to identify significant bins.
 *
 * @param[in]  signal    Input signal of length @p N.
 * @param[in]  N         Signal length (must be >= 2).
 * @param[out] phase_out Pre-allocated array of at least @p N/2+1 doubles
 *                       that receives the phase in radians.
 *
 * **Example**
 * @code
 * unsigned N = 1024;
 * double *sig = malloc(N * sizeof(double));
 * // ... fill sig ...
 * unsigned num_bins = N / 2 + 1;
 * double *phase = malloc(num_bins * sizeof(double));
 * MD_phase_spectrum(sig, N, phase);
 * // phase[k] in [-M_PI, M_PI]
 * free(sig);
 * free(phase);
 * MD_shutdown();
 * @endcode
 *
 * @see MD_magnitude_spectrum(), MD_power_spectral_density(), MD_shutdown()
 */
void MD_phase_spectrum(const double *signal, unsigned N, double *phase_out);

/**
 * Compute the number of STFT frames for the given signal length and parameters.
 *
 * The formula is:
 *   num_frames = (signal_len - N) / hop + 1  when signal_len >= N
 *   num_frames = 0                            when signal_len < N
 *
 * Use this function to size the output buffer before calling MD_stft().
 *
 * @param signal_len  Total number of samples in the signal.
 * @param N           FFT window size (samples per frame).
 * @param hop         Hop size (samples between successive frame starts).
 * @return            Number of complete frames that fit in the signal.
 *
 * Example:
 * @code
 *   // 1 s of audio at 16 kHz, 32 ms window, 8 ms hop
 *   unsigned signal_len = 16000;
 *   unsigned N   = 512;   // 32 ms at 16 kHz
 *   unsigned hop = 128;   // 8 ms (75% overlap)
 *   unsigned num_frames = MD_stft_num_frames(signal_len, N, hop);
 *   // num_frames = (16000 - 512) / 128 + 1 = 121
 * @endcode
 */
unsigned MD_stft_num_frames(unsigned signal_len, unsigned N, unsigned hop);

/**
 * Compute the Short-Time Fourier Transform (STFT) of a real-valued signal.
 *
 * The STFT slides a Hanning-windowed FFT over the signal in steps of
 * @p hop samples, producing a time-frequency magnitude matrix.
 *
 * For each frame @p f starting at sample @p f * hop, the function:
 *   1. Multiplies the frame by a Hanning window.
 *   2. Computes the real-to-complex FFT using FFTW.
 *   3. Stores the magnitudes |X(k)| for bins 0..N/2 in the output.
 *
 * The STFT formula for frame @p f and bin @p k is:
 *
 *   X_f(k) = SUM_{n=0}^{N-1}  w[n] * x[f*hop + n] * e^{-j2pi*k*n/N}
 *
 *   mag_out[f * (N/2+1) + k] = |X_f(k)|
 *
 * where @p w[n] is the Hanning window.
 *
 * The output is **not** normalised by N -- divide each value by N to get
 * the "standard" DFT magnitude, consistent with MD_magnitude_spectrum().
 *
 * The STFT reuses the same cached FFT plan as MD_magnitude_spectrum() and
 * MD_power_spectral_density().  Only the Hanning window buffer is separate.
 *
 * @param signal      Input signal.
 * @param signal_len  Total number of samples in the signal.
 * @param N           FFT window size (must be >= 2).
 * @param hop         Hop size in samples (must be >= 1).
 * @param mag_out     Output array (row-major).  Must be pre-allocated to at
 *                    least MD_stft_num_frames(signal_len, N, hop) * (N/2+1)
 *                    doubles.  On return, mag_out[f*(N/2+1) + k] = |X_f(k)|.
 *
 * @note  If signal_len < N, the function returns immediately without writing
 *        any output (zero frames fit).  Use MD_stft_num_frames() to check
 *        in advance.
 *
 * @note  The caller must allocate mag_out.  A typical pattern:
 * @code
 *   unsigned N   = 512;
 *   unsigned hop = 128;
 *   unsigned num_frames = MD_stft_num_frames(signal_len, N, hop);
 *   unsigned num_bins   = N / 2 + 1;
 *
 *   double *mag_out = malloc(num_frames * num_bins * sizeof(double));
 *   MD_stft(signal, signal_len, N, hop, mag_out);
 *
 *   // mag_out[f * num_bins + k] = |X_f(k)|
 *   // Convert bin k to Hz: freq_hz = k * sample_rate / N
 *   // Convert frame f to seconds: time_s = (double)(f * hop) / sample_rate
 *
 *   free(mag_out);
 *   MD_shutdown();  // free cached FFT plans when done
 * @endcode
 */
void MD_stft(const double *signal, unsigned signal_len,
             unsigned N, unsigned hop,
             double *mag_out);

/**
 * Build a mel-spaced triangular filterbank matrix.
 *
 * This function creates @p num_mels triangular filters over one-sided FFT bins
 * (0..N/2), using the HTK mel mapping:
 *
 *   mel(f) = 2595 * log10(1 + f / 700)
 *
 * The requested frequency range [@p min_freq_hz, @p max_freq_hz] is clamped at
 * runtime to [0, sample_rate/2].  If the clamped range is empty, all output
 * weights are zero.
 *
 * Output layout is row-major:
 *   filterbank_out[m * (N/2 + 1) + k]
 * where @p m is the mel band index and @p k is the FFT bin index.
 *
 * @param N              FFT size (must be >= 2).
 * @param sample_rate    Sampling rate in Hz (must be > 0).
 * @param num_mels       Number of mel filters (must be > 0).
 * @param min_freq_hz    Requested lower frequency bound in Hz.
 * @param max_freq_hz    Requested upper frequency bound in Hz (must be > min_freq_hz).
 * @param filterbank_out Output matrix, caller-allocated, length
 *                       num_mels * (N/2 + 1) doubles.
 */
void MD_mel_filterbank(unsigned N, double sample_rate,
                       unsigned num_mels,
                       double min_freq_hz, double max_freq_hz,
                       double *filterbank_out);

/**
 * Compute mel-band energies from a single frame.
 *
 * Processing steps:
 *   1) Apply an internal Hann window.
 *   2) Compute one-sided PSD bins via FFT: |X(k)|^2 / N.
 *   3) Apply mel filterbank weights and sum per band.
 *
 * This function uses the same internal FFT cache as MD_stft() and spectrum APIs.
 *
 * @param signal       Input frame of length N.
 * @param N            Frame length / FFT size (must be >= 2).
 * @param sample_rate  Sampling rate in Hz (must be > 0).
 * @param num_mels     Number of mel bands (must be > 0).
 * @param min_freq_hz  Requested lower frequency bound in Hz.
 * @param max_freq_hz  Requested upper frequency bound in Hz (must be > min_freq_hz).
 * @param mel_out      Output mel energies, caller-allocated length num_mels.
 */
void MD_mel_energies(const double *signal, unsigned N,
                     double sample_rate, unsigned num_mels,
                     double min_freq_hz, double max_freq_hz,
                     double *mel_out);

/**
 * Compute mel-frequency cepstral coefficients (MFCCs) from a single frame.
 *
 * Conventions used by this API:
 *   - HTK mel mapping for filter placement.
 *   - Internal Hann windowing and one-sided PSD mel energies.
 *   - Natural-log compression: log(max(E_mel, 1e-12)).
 *   - DCT-II with HTK-C0 profile:
 *       c0 uses sqrt(1/M) normalization,
 *       c_n (n>0) uses sqrt(2/M),
 *     where M is num_mels.
 *   - Coefficient 0 (C0) is written to mfcc_out[0].
 *
 * @param signal       Input frame of length N.
 * @param N            Frame length / FFT size (must be >= 2).
 * @param sample_rate  Sampling rate in Hz (must be > 0).
 * @param num_mels     Number of mel bands (must be > 0).
 * @param num_coeffs   Number of cepstral coefficients to output
 *                     (must be in [1, num_mels]).
 * @param min_freq_hz  Requested lower frequency bound in Hz.
 * @param max_freq_hz  Requested upper frequency bound in Hz (must be > min_freq_hz).
 * @param mfcc_out     Output array, caller-allocated length num_coeffs.
 */
void MD_mfcc(const double *signal, unsigned N,
             double sample_rate,
             unsigned num_mels, unsigned num_coeffs,
             double min_freq_hz, double max_freq_hz,
             double *mfcc_out);

/* -----------------------------------------------------------------------
 * FFT-based filtering
 * -----------------------------------------------------------------------*/

/**
 * Apply a brickwall lowpass filter to a signal in-place.
 *
 * Performs an FFT, zeroes all frequency bins above the cutoff frequency,
 * and inverse-FFTs the result.  This is a "brickwall" filter: it has zero
 * transition bandwidth, meaning content at or below the cutoff is preserved
 * exactly and content above the cutoff is eliminated completely.
 *
 * The filter operation in the frequency domain:
 *
 * \f[
 *   X'(k) = \begin{cases}
 *     X(k) & \text{if } k \leq \lfloor f_c \cdot N / f_s \rfloor \\
 *     0    & \text{otherwise}
 *   \end{cases}
 * \f]
 *
 * where \f$X(k)\f$ is the DFT of the input, \f$f_c\f$ is the cutoff
 * frequency, \f$N\f$ is the signal length, and \f$f_s\f$ is the sample rate.
 *
 * @note Uses one-off FFTW plans (not the cached spectrum analysis plans).
 *       Suitable for offline/batch processing.  Gibbs ringing occurs at the
 *       cutoff frequency; this is negligible when the cutoff is far from any
 *       frequency band of interest.
 *
 * @param signal      Signal buffer, modified in-place (must not be NULL).
 * @param len         Number of samples (must be > 0).
 * @param cutoff_hz   Cutoff frequency in Hz (must be in (0, sample_rate/2)).
 * @param sample_rate Sample rate in Hz (must be > 0).
 *
 * @code
 * // Remove all content above 8 kHz from a 48 kHz signal
 * double buf[48000];
 * MD_sine_wave(buf, 48000, 1.0, 440.0, 48000.0);
 * MD_lowpass_brickwall(buf, 48000, 8000.0, 48000.0);
 * @endcode
 */
void MD_lowpass_brickwall(double *signal, unsigned len,
                          double cutoff_hz, double sample_rate);

/* -----------------------------------------------------------------------
 * Math utilities
 * -----------------------------------------------------------------------*/

/**
 * Zeroth-order modified Bessel function of the first kind, \f$I_0(x)\f$.
 *
 * Computed via the power series:
 * \f[
 *   I_0(x) = \sum_{k=0}^{\infty} \left[\frac{(x/2)^k}{k!}\right]^2
 * \f]
 * Converges until the term is below \f$10^{-15}\f$ relative tolerance.
 *
 * @param x  Input value (any real number).
 * @return   \f$I_0(x)\f$.  Always \f$\geq 1\f$ since \f$I_0(0)=1\f$.
 *
 * @code
 * double val = MD_bessel_i0(5.0);  // ≈ 27.2399
 * @endcode
 */
double MD_bessel_i0(double x);

/**
 * Normalized sinc function: \f$\mathrm{sinc}(x) = \sin(\pi x)/(\pi x)\f$.
 *
 * \f[
 *   \mathrm{sinc}(x) = \begin{cases}
 *     1 & \text{if } |x| < 10^{-12} \\
 *     \dfrac{\sin(\pi x)}{\pi x} & \text{otherwise}
 *   \end{cases}
 * \f]
 *
 * @param x  Input value.
 * @return   sinc(x).  Equals 1 at zero, 0 at nonzero integers.
 *
 * @code
 * double v = MD_sinc(0.5);  // ≈ 0.6366 (2/π)
 * @endcode
 */
double MD_sinc(double x);

/* -----------------------------------------------------------------------
 * Window generation
 * -----------------------------------------------------------------------*/

/**
 * Generate a Hanning (Hann) window of length n.
 * Tapers to zero at both ends and is a common default for FFT analysis.
 */
void MD_Gen_Hann_Win(double *out, unsigned n);

/**
 * Generate a Hamming window of length n.
 * Similar to Hanning, but with a slightly lower first sidelobe.
 */
void MD_Gen_Hamming_Win(double *out, unsigned n);

/**
 * Generate a Blackman window of length n.
 * Much lower sidelobes than Hanning/Hamming, with a wider main lobe.
 */
void MD_Gen_Blackman_Win(double *out, unsigned n);

/**
 * Generate a rectangular window of length n (all ones).
 * Useful as a baseline reference (equivalent to no tapering).
 */
void MD_Gen_Rect_Win(double *out, unsigned n);

/**
 * Generate a Kaiser window of length n with shape parameter beta.
 *
 * \f[
 *   w[i] = \frac{I_0\!\left(\beta\,\sqrt{1 - \left(\frac{2i}{n-1}-1\right)^2}\right)}
 *               {I_0(\beta)}
 * \f]
 *
 * The \f$\beta\f$ parameter controls the sidelobe/mainlobe tradeoff:
 *   - \f$\beta \approx 5\f$  : ~45 dB stopband attenuation (fast/draft)
 *   - \f$\beta \approx 10\f$ : ~100 dB stopband attenuation (high quality)
 *   - \f$\beta \approx 14\f$ : ~120 dB stopband attenuation (mastering)
 *
 * @param out   Output buffer of length n (caller-allocated).
 * @param n     Window length.  Must be > 0.  For n == 1, outputs 1.0.
 * @param beta  Shape parameter (> 0 for useful windows).
 *
 * @code
 * double win[256];
 * MD_Gen_Kaiser_Win(win, 256, 10.0);  // high-quality Kaiser window
 * @endcode
 */
void MD_Gen_Kaiser_Win(double *out, unsigned n, double beta);

/* -----------------------------------------------------------------------
 * Signal generators
 * -----------------------------------------------------------------------*/

/**
 * Generate a sine wave.
 *
 * Fills `output[i] = amplitude * sin(2π * freq * i / sample_rate)`
 * for i in [0, N).
 *
 * This is the simplest test signal in DSP — a pure tone at a single
 * frequency.  Use it to verify filter responses, check FFT bin
 * alignment, or provide a clean input for any processing chain.
 *
 * @param output      Output buffer of length N (caller-allocated).
 * @param N           Number of samples to generate.  Must be > 0.
 * @param amplitude   Peak amplitude of the sine wave (e.g. 1.0).
 * @param freq        Frequency in Hz.
 * @param sample_rate Sampling rate in Hz.  Must be > 0.
 *
 * @code
 * double sig[1024];
 * MD_sine_wave(sig, 1024, 1.0, 440.0, 44100.0);  // 440 Hz A4 tone
 * @endcode
 */
void MD_sine_wave(double *output, unsigned N, double amplitude,
                  double freq, double sample_rate);

/**
 * Generate Gaussian white noise.
 *
 * Fills `output` with normally distributed random samples (mean 0,
 * standard deviation `amplitude`) using the Box-Muller transform.
 * White noise has equal energy at all frequencies -- its power
 * spectral density is approximately flat.
 *
 * Use white noise to test filters, measure impulse responses, or
 * as an additive noise source for SNR experiments.
 *
 * @param output    Output buffer of length N (caller-allocated).
 * @param N         Number of samples to generate.  Must be > 0.
 * @param amplitude Standard deviation of the noise (e.g. 1.0).
 * @param seed      Seed for the random number generator.  Using the
 *                  same seed produces the same output sequence, which
 *                  is useful for reproducible tests.
 *
 * @code
 * double noise[4096];
 * MD_white_noise(noise, 4096, 1.0, 42);  // reproducible Gaussian noise
 * @endcode
 */
void MD_white_noise(double *output, unsigned N, double amplitude,
                    unsigned seed);

/**
 * Generate a discrete impulse (Kronecker delta).
 *
 * Fills the output buffer with zeros except at @p position, where the
 * value is set to @p amplitude.  A unit impulse (amplitude 1.0 at
 * position 0) is the identity element of convolution and has a
 * perfectly flat magnitude spectrum.
 *
 * Common uses:
 *   - Measure a system's impulse response by feeding it through a filter.
 *   - Verify that MD_magnitude_spectrum() returns a flat spectrum.
 *   - Create delayed spikes for testing convolution and delay estimation.
 *
 * @param output    Output buffer of length N (caller-allocated).
 * @param N         Number of samples to generate.  Must be > 0.
 * @param amplitude Value of the impulse spike (e.g. 1.0 for unit impulse).
 * @param position  Sample index of the spike (0-based).  Must be < N.
 *
 * @code
 * double sig[1024];
 * MD_impulse(sig, 1024, 1.0, 0);  // unit impulse at sample 0
 * @endcode
 */
void MD_impulse(double *output, unsigned N, double amplitude, unsigned position);

/**
 * Generate a linear chirp (swept sine with linearly increasing frequency).
 *
 * The instantaneous frequency sweeps linearly from @p f_start to @p f_end
 * over N samples:
 *
 *   f(t) = f_start + (f_end - f_start) * t / T
 *
 * where T = (N-1) / sample_rate is the sweep duration.  The output is:
 *
 *   output[i] = amplitude * sin(2*pi * (f_start*t + 0.5*chirp_rate*t^2))
 *
 * A linear chirp is the standard test signal for spectrograms -- its
 * instantaneous frequency traces a straight diagonal line in the
 * time-frequency plane.
 *
 * @param output      Output buffer of length N (caller-allocated).
 * @param N           Number of samples to generate.  Must be > 0.
 * @param amplitude   Peak amplitude of the chirp (e.g. 1.0).
 * @param f_start     Starting frequency in Hz.
 * @param f_end       Ending frequency in Hz.
 * @param sample_rate Sampling rate in Hz.  Must be > 0.
 *
 * @code
 * double sig[16000];
 * // 1-second linear chirp from 200 Hz to 4000 Hz at 16 kHz sample rate
 * MD_chirp_linear(sig, 16000, 1.0, 200.0, 4000.0, 16000.0);
 * @endcode
 */
void MD_chirp_linear(double *output, unsigned N, double amplitude,
                     double f_start, double f_end, double sample_rate);

/**
 * Generate a logarithmic chirp (swept sine with exponentially increasing
 * frequency).
 *
 * The instantaneous frequency sweeps exponentially from @p f_start to
 * @p f_end over N samples:
 *
 *   f(t) = f_start * (f_end / f_start)^(t / T)
 *
 * where T = (N-1) / sample_rate is the sweep duration.  The output is:
 *
 *   output[i] = amplitude * sin(2*pi * f_start * T * (r^(t/T) - 1) / ln(r))
 *
 * where r = f_end / f_start.
 *
 * A logarithmic chirp spends equal time per octave, making it ideal for
 * measuring systems whose behaviour is best described on a log-frequency
 * axis (e.g. audio equaliser response).
 *
 * @param output      Output buffer of length N (caller-allocated).
 * @param N           Number of samples to generate.  Must be > 0.
 * @param amplitude   Peak amplitude of the chirp (e.g. 1.0).
 * @param f_start     Starting frequency in Hz.  Must be > 0.
 * @param f_end       Ending frequency in Hz.  Must be > 0 and != f_start.
 * @param sample_rate Sampling rate in Hz.  Must be > 0.
 *
 * @code
 * double sig[44100];
 * // 1-second log chirp from 20 Hz to 20 kHz at 44.1 kHz sample rate
 * MD_chirp_log(sig, 44100, 1.0, 20.0, 20000.0, 44100.0);
 * @endcode
 */
void MD_chirp_log(double *output, unsigned N, double amplitude,
                  double f_start, double f_end, double sample_rate);

/**
 * Generate a square wave.
 *
 * Fills the output buffer with a bipolar square wave that alternates
 * between +amplitude and −amplitude at the given frequency:
 *
 * \f[
 * x[n] = \begin{cases}
 *   +A  & 0 < \phi < \pi \\
 *   -A  & \pi < \phi < 2\pi \\
 *    0  & \phi = 0 \text{ or } \phi = \pi
 * \end{cases}
 * \f]
 *
 * where \f$\phi = 2\pi f n / f_s \pmod{2\pi}\f$.
 *
 * A square wave contains only odd harmonics (1f, 3f, 5f, …) whose
 * amplitudes decay as 1/k — a classic demonstration of the Fourier
 * series and the Gibbs phenomenon.
 *
 * @param output      Output buffer of length N (caller-allocated).
 * @param N           Number of samples to generate.  Must be > 0.
 * @param amplitude   Peak amplitude of the square wave (e.g. 1.0).
 * @param freq        Frequency in Hz.
 * @param sample_rate Sampling rate in Hz.  Must be > 0.
 *
 * @code
 * double sig[1024];
 * MD_square_wave(sig, 1024, 1.0, 440.0, 44100.0);
 * @endcode
 */
void MD_square_wave(double *output, unsigned N, double amplitude,
                    double freq, double sample_rate);

/**
 * Generate a sawtooth wave.
 *
 * Fills the output buffer with a sawtooth wave that ramps linearly
 * from −amplitude to +amplitude over each period:
 *
 * \f[
 * x[n] = A \left(\frac{\phi}{\pi} - 1\right)
 * \f]
 *
 * where \f$\phi = 2\pi f n / f_s \pmod{2\pi}\f$.
 *
 * A sawtooth wave contains all integer harmonics (1f, 2f, 3f, …)
 * whose amplitudes decay as 1/k — useful for demonstrating richer
 * harmonic content compared to the square wave's odd-only series.
 *
 * @param output      Output buffer of length N (caller-allocated).
 * @param N           Number of samples to generate.  Must be > 0.
 * @param amplitude   Peak amplitude of the sawtooth wave (e.g. 1.0).
 * @param freq        Frequency in Hz.
 * @param sample_rate Sampling rate in Hz.  Must be > 0.
 *
 * @code
 * double sig[1024];
 * MD_sawtooth_wave(sig, 1024, 1.0, 440.0, 44100.0);
 * @endcode
 */
void MD_sawtooth_wave(double *output, unsigned N, double amplitude,
                      double freq, double sample_rate);

/**
 * @brief Generate a Shepard tone — the auditory illusion of endlessly
 *        rising or falling pitch.
 *
 * A [Shepard tone](https://en.wikipedia.org/wiki/Shepard_tone) superimposes
 * several sine waves spaced one octave apart.  Each tone glides continuously
 * upward (or downward) in pitch while a Gaussian spectral envelope — fixed
 * in log-frequency space — fades tones in at one end and out at the other.
 * The result is a sound that seems to rise (or fall) forever without ever
 * actually leaving its frequency range.
 *
 * **Signal model.**  At time \f$t = n / f_s\f$ the output sample is:
 *
 * \f[
 *   x[n] \;=\; A_\mathrm{norm}\!\sum_k
 *     \underbrace{
 *       \exp\!\Bigl(-\frac{d_k(t)^2}{2\sigma^2}\Bigr)
 *     }_{\text{Gaussian envelope}}
 *     \;\sin\!\bigl(\varphi_k(n)\bigr)
 * \f]
 *
 * where the octave distance from the Gaussian centre is
 *
 * \f[
 *   d_k(t) = k - c + R\,t, \qquad
 *   c = \frac{L-1}{2}, \qquad
 *   \sigma = \frac{L}{4}
 * \f]
 *
 * and the instantaneous frequency of layer \f$k\f$ is
 * \f$f_k(t) = f_\text{base}\cdot 2^{d_k(t)}\f$.
 * The phase \f$\varphi_k\f$ is accumulated sample-by-sample from
 * \f$f_k\f$ so that each tone glides smoothly.
 * \f$R\f$ is @p rate_octaves_per_sec and \f$L\f$ is @p num_octaves.
 * \f$A_\mathrm{norm}\f$ scales the peak to @p amplitude.
 *
 * The sum ranges over all integer indices \f$k\f$ for which
 * \f$|d_k(t)|\f$ is within \f$5\sigma\f$ at some point during the
 * signal, ensuring enough layers are present to sustain the illusion
 * for the full duration.
 *
 * @param output               Output buffer of length @p N (caller-allocated).
 * @param N                    Number of samples to generate.  Must be > 0.
 * @param amplitude            Peak amplitude of the output signal.
 * @param base_freq            Centre frequency of the Gaussian envelope in Hz.
 *                             Typical values: 200–600 Hz.
 * @param sample_rate          Sampling rate in Hz.  Must be > 0.
 * @param rate_octaves_per_sec Glissando rate in octaves per second.
 *                             Positive = rising, negative = falling, 0 = static chord.
 * @param num_octaves          Number of audible octave layers (width of the
 *                             Gaussian bell).  Typical values: 6–10.
 *                             Must be > 0.
 *
 * @code
 * // 5 seconds of endlessly rising Shepard tone at 44.1 kHz
 * unsigned N = 5 * 44100;
 * double *sig = malloc(N * sizeof(double));
 * MD_shepard_tone(sig, N, 0.8, 440.0, 44100.0, 0.5, 8);
 * // sig[] now sounds like it rises forever
 * free(sig);
 * @endcode
 */
void MD_shepard_tone(double *output, unsigned N, double amplitude,
                     double base_freq, double sample_rate,
                     double rate_octaves_per_sec, unsigned num_octaves);

/* -----------------------------------------------------------------------
 * DTMF tone detection and generation
 * -----------------------------------------------------------------------
 *
 * Dual-Tone Multi-Frequency (DTMF) signalling encodes each keypad
 * button as a pair of sinusoids -- one from a low-frequency "row" group
 * and one from a high-frequency "column" group:
 *
 *              1209 Hz   1336 Hz   1477 Hz   1633 Hz
 *   697 Hz        1         2         3         A
 *   770 Hz        4         5         6         B
 *   852 Hz        7         8         9         C
 *   941 Hz        *         0         #         D
 *
 * Detection uses frame-based FFT magnitude analysis with a state machine
 * that enforces ITU-T Q.24 minimum timing constraints (40 ms tone-on,
 * 40 ms inter-digit pause).
 */

/** A single detected DTMF tone with timing information. */
typedef struct {
    char   digit;     /**< Decoded digit: '0'--'9', 'A'--'D', '*', or '#'. */
    double start_s;   /**< Tone onset time in seconds. */
    double end_s;     /**< Tone offset time in seconds. */
} MD_DTMFTone;

/**
 * Detect DTMF tones in an audio signal.
 *
 * Slides a Hanning-windowed FFT frame across the signal and checks each
 * frame for a valid row + column frequency pair.  A tone is reported only
 * after it persists for at least 40 ms (ITU-T Q.24), and a new tone is
 * not accepted until at least 40 ms of silence separates it from the
 * previous one.
 *
 * The FFT size is the largest power of two whose window fits within
 * 35 ms, keeping it shorter than the 40 ms Q.24 minimum pause so the
 * state machine can resolve inter-digit gaps.  The hop size is N/4
 * (75 % overlap).
 *
 * @param signal      Audio samples (mono).
 * @param signal_len  Number of samples.  Must be > 0.
 * @param sample_rate Sampling rate in Hz.  Must be >= 4000
 *                    (Nyquist for the highest DTMF frequency, 1633 Hz).
 * @param tones_out   Output array for detected tones (caller-allocated).
 * @param max_tones   Size of @p tones_out.  Must be > 0.
 * @return            Number of tones detected (<= @p max_tones).
 *
 * @code
 * // Generate a short DTMF sequence and detect it
 * const char *digits = "5551234";
 * unsigned len = MD_dtmf_signal_length(7, 8000.0, 70, 70);
 * double *sig = malloc(len * sizeof(double));
 * MD_dtmf_generate(sig, digits, 8000.0, 70, 70);
 *
 * MD_DTMFTone tones[16];
 * unsigned n = MD_dtmf_detect(sig, len, 8000.0, tones, 16);
 * for (unsigned i = 0; i < n; i++)
 *     printf("%c  %.3f--%.3f s\n", tones[i].digit,
 *            tones[i].start_s, tones[i].end_s);
 * free(sig);
 * @endcode
 */
unsigned MD_dtmf_detect(const double *signal, unsigned signal_len,
                        double sample_rate,
                        MD_DTMFTone *tones_out, unsigned max_tones);

/**
 * Generate a DTMF tone sequence.
 *
 * Each digit is rendered as the sum of its row and column sinusoids,
 * each at amplitude 0.5 (peak sum = 1.0).  Digits are separated by
 * silent gaps.  Use MD_dtmf_signal_length() to pre-compute the required
 * output buffer size.
 *
 * Timing constraints follow ITU-T Q.24:
 *   - @p tone_ms >= 40  (minimum recognisable tone)
 *   - @p pause_ms >= 40 (minimum inter-digit pause)
 *
 * @param output      Output buffer (caller-allocated, length from
 *                    MD_dtmf_signal_length()).
 * @param digits      Null-terminated string of valid DTMF characters:
 *                    '0'--'9', 'A'--'D' (case-insensitive), '*', '#'.
 *                    An invalid character triggers an assertion failure.
 * @param sample_rate Sampling rate in Hz.  Must be > 0.
 * @param tone_ms     Duration of each tone in milliseconds (>= 40).
 * @param pause_ms    Duration of silence between tones in ms (>= 40).
 *
 * @code
 * unsigned len = MD_dtmf_signal_length(3, 8000.0, 70, 70);
 * double *sig = malloc(len * sizeof(double));
 * MD_dtmf_generate(sig, "911", 8000.0, 70, 70);
 * // sig now contains 3 tones of 70 ms each with 70 ms gaps
 * free(sig);
 * @endcode
 */
void MD_dtmf_generate(double *output, const char *digits,
                      double sample_rate,
                      unsigned tone_ms, unsigned pause_ms);

/**
 * Calculate the number of samples needed for MD_dtmf_generate().
 *
 * \f[
 * N = D \cdot \left\lfloor \frac{t_{\text{tone}} \cdot f_s}{1000} \right\rfloor
 *   + (D - 1) \cdot \left\lfloor \frac{t_{\text{pause}} \cdot f_s}{1000} \right\rfloor
 * \f]
 *
 * where \f$D\f$ is the number of digits.
 *
 * @param num_digits  Number of digits in the sequence.
 * @param sample_rate Sampling rate in Hz.  Must be > 0.
 * @param tone_ms     Tone duration in milliseconds.
 * @param pause_ms    Pause duration in milliseconds.
 * @return            Total number of samples (0 if num_digits == 0).
 */
unsigned MD_dtmf_signal_length(unsigned num_digits, double sample_rate,
                               unsigned tone_ms, unsigned pause_ms);

/* -----------------------------------------------------------------------
 * Resource cleanup
 * -----------------------------------------------------------------------*/

/** Free all internally cached FFT plans and buffers. Call when done. */
void MD_shutdown(void);

/* -----------------------------------------------------------------------
 * Generalized Cross-Correlation (GCC) for delay estimation
 * -----------------------------------------------------------------------
 *
 * Given two microphone signals that captured the same sound source,
 * GCC-PHAT estimates how many samples one signal is delayed relative
 * to the other.  This is the basis of acoustic source localisation.
 *
 * The algorithm:
 *   1. FFT both signals.
 *   2. Multiply one spectrum by the conjugate of the other (cross-spectrum).
 *   3. Apply a weighting (PHAT normalises by magnitude, sharpening the peak).
 *   4. Inverse-FFT back to the time domain.
 *   5. The position of the peak tells you the delay in samples.
 */

/** Weighting types for Generalized Cross-Correlation. */
enum MD_GCC_WEIGHTING_TYPE {
    SIMP, /**< Simple 1/N weighting (basic cross-correlation) */
    PHAT  /**< Phase Transform weighting (sharper peaks, more robust to noise) */
};

/**
 * Estimate delays between a reference signal and M-1 other signals.
 *
 * @param sigs        Array of M pointers to signals (sigs[0] is the reference).
 * @param M           Number of signals.
 * @param N           Length of each signal (all must be the same length).
 * @param margin      Search +/- this many samples around zero-lag.
 * @param weightfunc  SIMP or PHAT (see ::MD_GCC_WEIGHTING_TYPE).
 * @param outdelays   Output array of M-1 delay values (must be pre-allocated).
 */
void MD_get_multiple_delays(const double **sigs, unsigned M, unsigned N,
                            unsigned margin, int weightfunc,
                            int *outdelays);

/**
 * Estimate the delay between two signals.
 *
 * @param siga        First signal.
 * @param sigb        Second signal.
 * @param N           Length of both signals.
 * @param ent         If non-null, receives the normalised entropy of the
 *                    correlation peak region (closer to 1.0 = less trustworthy).
 * @param margin      Search +/- this many samples around zero-lag.
 * @param weightfunc  SIMP or PHAT.
 * @return            Delay in samples (positive = sigb lags siga).
 */
int MD_get_delay(const double *siga, const double *sigb, unsigned N,
                 double *ent, unsigned margin, int weightfunc);

/**
 * Compute the full generalized cross-correlation between two signals.
 *
 * @param siga        First signal.
 * @param sigb        Second signal.
 * @param N           Length of both signals.
 * @param lagvals     Output array of N doubles (pre-allocated).
 *                    The zero-lag value is at index ceil(N/2).
 * @param weightfunc  SIMP or PHAT.
 */
void MD_gcc(const double *siga, const double *sigb, unsigned N,
            double *lagvals, int weightfunc);

/* -----------------------------------------------------------------------
 * Spectrogram text art
 * -----------------------------------------------------------------------*/

/**
 * @brief Synthesise audio that displays readable text in a spectrogram.
 *
 * Given a short string, the function rasterises it with a built-in 5x7
 * bitmap font.  Each bitmap column becomes a time slice; each "on" pixel
 * becomes a sine wave at the corresponding frequency between @p freq_lo
 * and @p freq_hi.  A 3 ms raised-cosine crossfade suppresses clicks at
 * column boundaries.  The output is normalised to 0.9 peak amplitude.
 *
 * @param output       Output buffer (caller-allocated).
 * @param max_len      Size of @p output in samples (must be >= returned value).
 * @param text         Printable ASCII string to render (must be non-empty).
 * @param freq_lo      Lowest frequency in Hz (bottom of text).
 * @param freq_hi      Highest frequency in Hz (top of text, must be < Nyquist).
 * @param duration_sec Total duration in seconds.
 * @param sample_rate  Sample rate in Hz.
 * @return             Number of samples written to @p output.
 *
 * @code
 * double buf[64000];
 * unsigned n = MD_spectrogram_text(buf, 64000, "HELLO",
 *                                  200.0, 7500.0, 2.0, 16000.0);
 * // buf[0..n-1] now contains synthesised audio; view its spectrogram
 * // to see "HELLO" spelled out in the frequency domain.
 * @endcode
 */
unsigned MD_spectrogram_text(double *output, unsigned max_len,
                             const char *text,
                             double freq_lo, double freq_hi,
                             double duration_sec, double sample_rate);

/* -----------------------------------------------------------------------
 * Audio steganography
 * -----------------------------------------------------------------------*/

/** Steganography method: least-significant-bit encoding. */
#define MD_STEG_LSB       0
/** Steganography method: near-ultrasonic frequency-band modulation (BFSK). */
#define MD_STEG_FREQ_BAND 1
/** Steganography method: hybrid LSB + spectrogram text art.
 *
 *  Encodes the message via LSB (machine-readable round-trip) and
 *  simultaneously renders it as text in a spectrogram using sine tones
 *  in the 18--23.5 kHz ultrasonic band (human-readable visual watermark).
 *
 *  The host signal is automatically upsampled to 48 kHz if its sample
 *  rate is below 48 kHz, so the output buffer must be sized for
 *  @c MD_resample_output_len(signal_len, sample_rate, 48000.0) samples
 *  when the input rate is less than 48 kHz.
 *
 *  Visual capacity is limited by host duration: each character occupies
 *  240 ms (8 bitmap columns at 30 ms each).  Messages longer than the
 *  visual capacity are truncated for the spectrogram art, but the full
 *  message is always recoverable via the LSB data channel. */
#define MD_STEG_SPECTEXT  2

/** Payload type flag: text (null-terminated string). */
#define MD_STEG_TYPE_TEXT   0
/** Payload type flag: binary (raw byte buffer). */
#define MD_STEG_TYPE_BINARY 1

/**
 * @brief Compute the maximum message length (in bytes) that can be hidden.
 *
 * @param signal_len  Length of the host signal in samples.
 * @param sample_rate Sample rate in Hz.
 * @param method      MD_STEG_LSB, MD_STEG_FREQ_BAND, or MD_STEG_SPECTEXT.
 * @return            Maximum message bytes that fit (excluding null terminator).
 */
unsigned MD_steg_capacity(unsigned signal_len, double sample_rate, int method);

/**
 * @brief Encode a secret message into a host audio signal.
 *
 * The host signal is copied to @p output, then the message is embedded
 * using the selected method.  A 32-bit length header is prepended so
 * that MD_steg_decode() can recover the message without knowing its
 * length in advance.
 *
 * - **MD_STEG_LSB** — flips the least-significant bit of a 16-bit PCM
 *   representation of each sample.  Distortion is at most ±1/32768
 *   (≈ −90 dB).  Works at any sample rate.
 *
 * - **MD_STEG_FREQ_BAND** — adds a low-amplitude BFSK tone in the
 *   near-ultrasonic band (18.5 / 19.5 kHz).  Requires
 *   @p sample_rate >= 40000 Hz so the carriers remain below Nyquist.
 *
 * - **MD_STEG_SPECTEXT** — hybrid: LSB data + spectrogram text art in the
 *   18--23.5 kHz band.  Auto-upsamples to 48 kHz; output buffer must be
 *   sized for the 48 kHz signal length when input rate < 48 kHz.
 *
 * @param host        Input host signal (not modified).
 * @param output      Output stego signal (caller-allocated; for MD_STEG_SPECTEXT
 *                    with sample_rate < 48 kHz, must hold
 *                    MD_resample_output_len(signal_len, sample_rate, 48000) samples).
 * @param signal_len  Length of host and output in samples.
 * @param sample_rate Sample rate in Hz.
 * @param message     Null-terminated message string to hide.
 * @param method      MD_STEG_LSB, MD_STEG_FREQ_BAND, or MD_STEG_SPECTEXT.
 * @return            Number of message bytes encoded (0 on failure).
 *
 * @code
 * double host[44100], stego[44100];
 * MD_sine_wave(host, 44100, 0.8, 440.0, 44100.0);
 * unsigned n = MD_steg_encode(host, stego, 44100, 44100.0,
 *                             "secret", MD_STEG_LSB);
 * @endcode
 */
unsigned MD_steg_encode(const double *host, double *output,
                        unsigned signal_len, double sample_rate,
                        const char *message, int method);

/**
 * @brief Decode a secret message from a stego audio signal.
 *
 * Reads the 32-bit length header, then extracts message bytes using
 * the same method that was used for encoding.
 *
 * @param stego       The stego signal containing the hidden message.
 * @param signal_len  Length of the stego signal in samples.
 * @param sample_rate Sample rate in Hz.
 * @param message_out Output buffer for the decoded message (caller-allocated).
 * @param max_msg_len Size of @p message_out buffer (including null terminator).
 * @param method      MD_STEG_LSB, MD_STEG_FREQ_BAND, or MD_STEG_SPECTEXT.
 * @return            Number of message bytes decoded (0 if none found).
 *
 * @code
 * char recovered[256];
 * unsigned n = MD_steg_decode(stego, 44100, 44100.0,
 *                             recovered, 256, MD_STEG_LSB);
 * printf("Hidden message: %s\n", recovered);
 * @endcode
 */
unsigned MD_steg_decode(const double *stego, unsigned signal_len,
                        double sample_rate,
                        char *message_out, unsigned max_msg_len,
                        int method);

/**
 * @brief Encode arbitrary binary data into a host audio signal.
 *
 * Works like MD_steg_encode() but accepts a raw byte buffer instead of a
 * null-terminated string, so it can embed data containing @c 0x00 bytes
 * (e.g. images, compressed archives, cryptographic keys).
 *
 * @param host        Input host signal (not modified).
 * @param output      Output stego signal (caller-allocated, same length).
 * @param signal_len  Length of host and output in samples.
 * @param sample_rate Sample rate in Hz.
 * @param data        Pointer to the binary data to hide.
 * @param data_len    Length of @p data in bytes.
 * @param method      MD_STEG_LSB, MD_STEG_FREQ_BAND, or MD_STEG_SPECTEXT.
 * @return            Number of data bytes encoded (0 on failure).
 *
 * @code
 * double host[44100], stego[44100];
 * MD_sine_wave(host, 44100, 0.8, 440.0, 44100.0);
 * unsigned char png_data[332];
 * // ... read PNG file into png_data ...
 * unsigned n = MD_steg_encode_bytes(host, stego, 44100, 44100.0,
 *                                   png_data, 332, MD_STEG_LSB);
 * @endcode
 */
unsigned MD_steg_encode_bytes(const double *host, double *output,
                              unsigned signal_len, double sample_rate,
                              const unsigned char *data, unsigned data_len,
                              int method);

/**
 * @brief Decode binary data from a stego audio signal.
 *
 * Works like MD_steg_decode() but writes raw bytes without null termination,
 * suitable for recovering binary payloads.
 *
 * @param stego       The stego signal containing the hidden data.
 * @param signal_len  Length of the stego signal in samples.
 * @param sample_rate Sample rate in Hz.
 * @param data_out    Output buffer for the decoded bytes (caller-allocated).
 * @param max_len     Maximum number of bytes to write to @p data_out.
 * @param method      MD_STEG_LSB, MD_STEG_FREQ_BAND, or MD_STEG_SPECTEXT.
 * @return            Number of data bytes decoded (0 if none found).
 *
 * @code
 * unsigned char recovered[1024];
 * unsigned n = MD_steg_decode_bytes(stego, 44100, 44100.0,
 *                                   recovered, 1024, MD_STEG_LSB);
 * // recovered[0..n-1] contains the original binary data
 * @endcode
 */
unsigned MD_steg_decode_bytes(const double *stego, unsigned signal_len,
                              double sample_rate,
                              unsigned char *data_out, unsigned max_len,
                              int method);

/**
 * @brief Detect which steganography method (if any) was used to encode a signal.
 *
 * Probes the signal for a valid steganographic header using both LSB and
 * frequency-band (BFSK) methods.  If a valid header is found, returns the
 * method identifier and optionally the payload type (text or binary).
 *
 * BFSK detection is only attempted when @p sample_rate >= 40000 Hz.
 * If both methods appear valid, BFSK is preferred (lower false-positive rate).
 *
 * @param signal           The signal to inspect.
 * @param signal_len       Length of the signal in samples.
 * @param sample_rate      Sample rate in Hz.
 * @param payload_type_out If non-null, receives MD_STEG_TYPE_TEXT or
 *                         MD_STEG_TYPE_BINARY when a method is detected.
 * @return                 MD_STEG_LSB, MD_STEG_FREQ_BAND, MD_STEG_SPECTEXT,
 *                         or -1 if no steganographic content is detected.
 *
 * @code
 * int payload_type;
 * int method = MD_steg_detect(signal, signal_len, 44100.0, &payload_type);
 * if (method >= 0) {
 *     printf("Detected %s payload (%s)\n",
 *            payload_type == MD_STEG_TYPE_BINARY ? "binary" : "text",
 *            method == MD_STEG_LSB ? "LSB" : "BFSK");
 * }
 * @endcode
 */
int MD_steg_detect(const double *signal, unsigned signal_len,
                   double sample_rate, int *payload_type_out);

/* -----------------------------------------------------------------------
 * Sample rate conversion
 * -----------------------------------------------------------------------*/

/**
 * Compute the output buffer size needed for resampling.
 *
 * Returns \f$\lceil \mathrm{input\_len} \times
 * \mathrm{out\_rate} / \mathrm{in\_rate} \rceil\f$.
 *
 * @param input_len  Number of input samples (must be > 0).
 * @param in_rate    Input sample rate in Hz (must be > 0).
 * @param out_rate   Output sample rate in Hz (must be > 0).
 * @return           Required output buffer length.
 *
 * @code
 * unsigned n = MD_resample_output_len(44100, 44100.0, 48000.0);  // 48000
 * @endcode
 */
unsigned MD_resample_output_len(unsigned input_len,
                                double in_rate, double out_rate);

/**
 * Resample a signal from one sample rate to another using polyphase
 * sinc interpolation.
 *
 * Uses a fixed table of 512 sub-phases with linear interpolation and
 * a Kaiser-windowed sinc anti-aliasing filter.  Anti-aliasing cutoff
 * is automatically set to \f$\min(\mathrm{in\_rate}, \mathrm{out\_rate})/2\f$.
 *
 * @param input              Input signal of length input_len.
 * @param input_len          Number of input samples (must be > 0).
 * @param output             Output buffer (caller-allocated).  Use
 *                           MD_resample_output_len() to size it.
 * @param max_output_len     Capacity of output buffer.  Must be >=
 *                           MD_resample_output_len(input_len, in_rate, out_rate).
 * @param in_rate            Input sample rate in Hz (must be > 0).
 * @param out_rate           Output sample rate in Hz (must be > 0).
 * @param num_zero_crossings Number of sinc zero-crossings per side.
 *                           Higher = better quality.  Typical: 32.
 * @param kaiser_beta        Kaiser window shape parameter.
 *                           Typical: 10.0 for ~100 dB stopband.
 * @return                   Number of samples written to output.
 *
 * @code
 * double in[44100], out[48000];
 * MD_sine_wave(in, 44100, 1.0, 440.0, 44100.0);
 * unsigned n = MD_resample(in, 44100, out, 48000,
 *                          44100.0, 48000.0, 32, 10.0);
 * @endcode
 */
unsigned MD_resample(const double *input, unsigned input_len,
                     double *output, unsigned max_output_len,
                     double in_rate, double out_rate,
                     unsigned num_zero_crossings, double kaiser_beta);

#endif /* MINIDSP_H */
