Files
InstaShadow/Source/TransformerSaturation.cpp
hariel1985 a587a43ff9 Initial release — InstaShadow mastering compressor v1.0
Dual-stage compressor (optical + VCA) with output transformer saturation.
- Port-Hamiltonian T4B opto-cell model with implicit trapezoidal integration
- Feed-forward VCA with 7 ratios, 6 attack/release presets, Dual release mode
- 3 transformer types (Nickel/Iron/Steel) with 4x oversampled waveshaping
- Analog-style needle VU meters, horizontal GR meters
- Sidechain HPF, stereo link, independent section bypass
- Full state save/restore, CI/CD for Windows/macOS/Linux
2026-03-27 16:03:24 +01:00

109 sor
3.6 KiB
C++

#include "TransformerSaturation.h"
TransformerSaturation::TransformerSaturation() {}
void TransformerSaturation::prepare (double sampleRate, int samplesPerBlock)
{
sr = sampleRate;
oversampler.initProcessing ((size_t) samplesPerBlock);
// Iron tonestack: very subtle low shelf +0.2dB at 110Hz
auto ironCoeffs = juce::dsp::IIR::Coefficients<float>::makeLowShelf (
sampleRate, 110.0f, 0.707f, juce::Decibels::decibelsToGain (0.2f));
ironBoostL.coefficients = ironCoeffs;
ironBoostR.coefficients = ironCoeffs;
// Steel tonestack: subtle low shelf +0.4dB at 40Hz
auto steelCoeffs = juce::dsp::IIR::Coefficients<float>::makeLowShelf (
sampleRate, 40.0f, 0.707f, juce::Decibels::decibelsToGain (0.4f));
steelBoostL.coefficients = steelCoeffs;
steelBoostR.coefficients = steelCoeffs;
}
void TransformerSaturation::reset()
{
oversampler.reset();
ironBoostL.reset(); ironBoostR.reset();
steelBoostL.reset(); steelBoostR.reset();
}
void TransformerSaturation::processBlock (juce::AudioBuffer<float>& buffer, Type type)
{
if (type == Off) return;
const int numChannels = std::min (buffer.getNumChannels(), 2);
const int numSamples = buffer.getNumSamples();
// Select parameters per type:
// drive: how hard the tanh clips (1.0 = no effect)
// even: 2nd harmonic amount (asymmetric warmth)
// odd: 3rd harmonic amount (edge/presence)
// mix: wet/dry blend (keeps effect very subtle)
float drive, even, odd, mix;
switch (type)
{
case Nickel:
drive = 1.05f; even = 0.002f; odd = 0.0005f; mix = 0.3f;
break;
case Iron:
drive = 1.15f; even = 0.008f; odd = 0.002f; mix = 0.5f;
break;
case Steel:
drive = 1.3f; even = 0.012f; odd = 0.008f; mix = 0.6f;
break;
default:
return;
}
// 4x upsample
juce::dsp::AudioBlock<float> block (buffer);
auto oversampledBlock = oversampler.processSamplesUp (block);
const int osNumSamples = (int) oversampledBlock.getNumSamples();
const int osNumChannels = std::min ((int) oversampledBlock.getNumChannels(), 2);
// Apply waveshaping at oversampled rate
for (int ch = 0; ch < osNumChannels; ++ch)
{
float* data = oversampledBlock.getChannelPointer ((size_t) ch);
for (int i = 0; i < osNumSamples; ++i)
{
float x = data[i];
float dry = x;
// Soft-clip: tanh(drive*x)/drive — unity gain at DC
float shaped = std::tanh (drive * x) / drive;
// Add subtle harmonic content
shaped += even * x * std::abs (x); // 2nd harmonic (even-order)
shaped -= odd * x * x * x; // 3rd harmonic (odd-order)
// Blend dry/wet to keep the effect subtle
data[i] = dry + (shaped - dry) * mix;
}
}
// 4x downsample
oversampler.processSamplesDown (block);
// Post-saturation tonestack (very subtle EQ coloration)
if (type == Iron)
{
for (int i = 0; i < numSamples; ++i)
{
if (numChannels > 0) buffer.setSample (0, i, ironBoostL.processSample (buffer.getSample (0, i)));
if (numChannels > 1) buffer.setSample (1, i, ironBoostR.processSample (buffer.getSample (1, i)));
}
}
else if (type == Steel)
{
for (int i = 0; i < numSamples; ++i)
{
if (numChannels > 0) buffer.setSample (0, i, steelBoostL.processSample (buffer.getSample (0, i)));
if (numChannels > 1) buffer.setSample (1, i, steelBoostR.processSample (buffer.getSample (1, i)));
}
}
}