SpiralCloudOmega

cab-modeling

Cabinet impulse response capture, neural cab modeling, and real-time convolution processing

SpiralCloudOmega 1 1 Updated 7mo ago
GitHub

Install

npx skillscat add spiralcloudomega/devteam6/cab-modeling

Install via the SkillsCat registry.

SKILL.md

Cabinet & Impulse Response Modeling

Guitar cabinet modeling replaces physical speaker cabinets with digital impulse responses (IRs) or neural approximations, enabling studio-quality tone in real-time signal chains.

Convolution Fundamentals

Time-domain convolution (y[n] = Σ h[k]·x[n-k]) is O(N·M) and impractical for long IRs. Frequency-domain convolution via overlap-save converts this to O(N log N) using FFT: transform both signals, multiply spectra, inverse-transform. For a 2048-sample IR at 48 kHz, frequency-domain is roughly 10× faster than brute-force.

Impulse Response Capture Methodology

Exponential sine sweeps are preferred over linear sweeps because harmonic distortion products separate in time after deconvolution. The process: play a known stimulus through the cabinet/mic chain, record the result, deconvolve by inverse-filtering.

import numpy as np
from scipy.signal import fftconvolve

def generate_sweep(duration_s, fs, f_start=20.0, f_end=20000.0):
    t = np.linspace(0, duration_s, int(fs * duration_s), endpoint=False)
    L = duration_s / np.log(f_end / f_start)
    return np.sin(2 * np.pi * f_start * L * (np.exp(t / L) - 1)).astype(np.float32)

def deconvolve_ir(recorded, sweep, fs):
    inverse_sweep = sweep[::-1]
    t = np.linspace(0, len(sweep) / fs, len(sweep), endpoint=False)
    inverse_sweep *= np.exp(-t / (len(sweep) / fs / np.log(20000 / 20)))
    ir = fftconvolve(recorded, inverse_sweep, mode='full')
    return (ir / np.max(np.abs(ir)))[:int(fs * 0.1)]

Partitioned Convolution

Uniform partitioning splits the IR into equal-sized blocks matching the audio buffer, each processed via FFT multiply-accumulate. Non-uniform partitioning uses small partitions for early samples and larger ones for the tail — critical for sub-5 ms latency with long reverb IRs.

class PartitionedConvolver {
    std::vector<std::vector<std::complex<float>>> irPartitions;
    juce::dsp::FFT fft;
    int blockSize, fftSize;
public:
    PartitionedConvolver(const float* ir, int irLen, int block)
        : fft(static_cast<int>(std::log2(block * 2))),
          blockSize(block), fftSize(block * 2) {
        int numParts = (irLen + blockSize - 1) / blockSize;
        irPartitions.resize(numParts, std::vector<std::complex<float>>(fftSize));
        for (int p = 0; p < numParts; ++p) {
            std::vector<float> buf(fftSize, 0.0f);
            std::copy_n(ir + p * blockSize, std::min(blockSize, irLen - p * blockSize), buf.begin());
            fft.performRealOnlyForwardTransform(buf.data());
            std::memcpy(irPartitions[p].data(), buf.data(), fftSize * sizeof(float));
        }
    }
};

DynaCab — Neural Cabinet Modeling

DynaCab replaces static IRs with a neural network that interpolates between mic positions, distances, and speaker cone zones. A small MLP (~8K parameters) takes position coordinates as conditioning input and outputs filter coefficients, enabling smooth real-time morphing.

// DynaCab concept: position-conditioned neural IR generator
struct DynaCabModel {
    std::array<float, 3> micPosition; // normalized [on/off-axis, cone zone, distance]
    void generateIR(float* irOutput, int irLength) {
        float hidden[32];
        denseLayerReLU(micPosition.data(), 3, hidden, 32, weightsL1, biasL1);
        denseLayerReLU(hidden, 32, hidden, 32, weightsL2, biasL2);
        denseLayerReLU(hidden, 32, hidden, 32, weightsL3, biasL3);
        denseLayerTanh(hidden, 32, irOutput, irLength, weightsL4, biasL4);
    }
};

Minimum-Phase vs Mixed-Phase IRs

Minimum-phase IRs concentrate energy at the start and add zero latency — ideal for live guitar cab simulation. Mixed-phase (linear-phase) IRs preserve the original time-domain shape including pre-ring, better for mastering. Convert via cepstral method: FFT → log magnitude → IFFT → window causal half → FFT → exp → IFFT.

Stereo IRs and Mic Position Modeling

Capture stereo IRs with two mics: on-axis (bright, focused) and off-axis (darker, roomy). Typical positions — on-axis at cap edge, 45° off-axis at 6 inches, room mic at 3+ feet. Crossfade between channels with a single knob for panning control.

IR Length vs Quality Tradeoffs

Samples Duration @ 48 kHz Use Case CPU Cost
256 5.3 ms Tight cab, minimal color Very low
512 10.7 ms Standard guitar cab Low
1024 21.3 ms Full cab with room Moderate
2048 42.7 ms Cab + early reflections Higher

Guitar cabs rarely need more than 1024 samples. Reverb IRs may need 48000+ (1 second).

Real-Time JUCE Implementation

class CabSimProcessor : public juce::AudioProcessor {
    juce::dsp::Convolution convolution;
    void prepareToPlay(double sampleRate, int samplesPerBlock) override {
        juce::dsp::ProcessSpec spec{sampleRate,
            static_cast<juce::uint32>(samplesPerBlock), 2};
        convolution.prepare(spec);
        convolution.loadImpulseResponse(juce::File("/path/to/cab.wav"),
            juce::dsp::Convolution::Stereo::yes,
            juce::dsp::Convolution::Trim::yes, 1024);
    }
    void processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer&) override {
        juce::dsp::AudioBlock<float> block(buffer);
        convolution.process(juce::dsp::ProcessContextReplacing<float>(block));
    }
};

Anti-Patterns

  • Loading IRs on the audio thread — always load asynchronously; use background thread with lock-free handoff.
  • Time-domain convolution for IRs > 128 samples — switch to FFT-based partitioned convolution.
  • Ignoring sample rate mismatch — resample IRs to session rate before loading; mismatched rates shift frequency response.
  • Skipping normalization — peak-normalize deconvolved IRs to -1 dBFS before use.
  • Linear-phase IRs for live monitoring — pre-ring causes audible artifacts; use minimum-phase for real-time paths.

Checklist

  • IR capture uses exponential sweep with proper inverse filtering
  • Deconvolved IR is trimmed, normalized, and sample-rate matched
  • Convolution uses partitioned FFT (not brute-force) for IRs > 128 samples
  • IR loading happens off the audio thread with lock-free swap
  • Minimum-phase conversion applied for zero-latency monitoring paths
  • Stereo IR mic positions documented (angle, distance, axis)
  • IR length chosen appropriate to use case (cab ≤1024, reverb ≤48000+)
  • DynaCab model inference fits within real-time budget (< 1 ms per block)