Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions OpenUtau.Core/SignalChain/Effects/DeEsser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System;

namespace OpenUtau.Core.SignalChain.Effects {
/// <summary>
/// Isolates high-frequency sibilance using a sidechain high-pass filter,
/// dynamically ducking those frequencies when they exceed a threshold.
/// </summary>
public class DeEsser {
private int channels;
private float thresholdLinear;
private float[] envelopes;

// High-pass filter variables for the sidechain
private float hp_b0, hp_b1, hp_b2, hp_a1, hp_a2;
private float[] hp_x1, hp_x2, hp_y1, hp_y2;

public bool IsBypassed => thresholdLinear >= 1.0f;

public DeEsser(int sampleRate, int channels) {
this.channels = channels;
envelopes = new float[channels];
hp_x1 = new float[channels]; hp_x2 = new float[channels];
hp_y1 = new float[channels]; hp_y2 = new float[channels];
}

public void Configure(double freq, double thresholdDb, int sampleRate) {
this.thresholdLinear = (float)Math.Pow(10, thresholdDb / 20.0);
if (IsBypassed) return;

// High-pass filter coefficients
double w0 = 2 * Math.PI * freq / sampleRate;
double alpha = Math.Sin(w0) / 2 * 0.707;
double a0_c = 1 + alpha;

hp_b0 = (float)((1 + Math.Cos(w0)) / 2 / a0_c);
hp_b1 = (float)(-(1 + Math.Cos(w0)) / a0_c);
hp_b2 = (float)((1 + Math.Cos(w0)) / 2 / a0_c);
hp_a1 = (float)(-2 * Math.Cos(w0) / a0_c);
hp_a2 = (float)((1 - alpha) / a0_c);
}

public void Process(float[] buffer, int offset, int count) {
if (IsBypassed) return;

float attack = 0.05f;
float release = 0.005f;

for (int i = offset; i < offset + count; i++) {
int ch = i % channels;
float x = buffer[i];

// Isolate the "Ess" frequencies
float det = hp_b0 * x + hp_b1 * hp_x1[ch] + hp_b2 * hp_x2[ch] - hp_a1 * hp_y1[ch] - hp_a2 * hp_y2[ch];
hp_x2[ch] = hp_x1[ch]; hp_x1[ch] = x;
hp_y2[ch] = hp_y1[ch]; hp_y1[ch] = det;

// Read the volume of the "Ess"
float detAbs = Math.Abs(det);
if (detAbs > envelopes[ch]) envelopes[ch] += attack * (detAbs - envelopes[ch]);
else envelopes[ch] += release * (detAbs - envelopes[ch]);

// Calculate gain reduction if Sibilance is too loud
float gain = 1.0f;
if (envelopes[ch] > thresholdLinear) {
gain = thresholdLinear / envelopes[ch];
gain = Math.Max(gain, 0.1f);
}

// Subtract the over-loud sibilance from the main signal
float duckingAmount = 1.0f - gain;
buffer[i] = x - (det * duckingAmount);
}
}
}
}
58 changes: 58 additions & 0 deletions OpenUtau.Core/SignalChain/Effects/DeThumper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;

namespace OpenUtau.Core.SignalChain.Effects {
/// <summary>
/// A low-shelf biquad filter designed to reduce low-frequency rumble, plosives, and mud.
/// </summary>
public class DeThumper {
private int channels;
private float reduction;
private float b0, b1, b2, a1, a2;
private float[] x1, x2, y1, y2;

public bool IsBypassed => reduction >= -0.01f;

public DeThumper(int sampleRate, int channels) {
this.channels = channels;
x1 = new float[channels]; x2 = new float[channels];
y1 = new float[channels]; y2 = new float[channels];
}

public void Configure(double freq, double reductionDb, int sampleRate) {
this.reduction = (float)reductionDb;
if (IsBypassed) return;

// Low-shelf Biquad Filter Coefficients
double A = Math.Pow(10, reductionDb / 40.0);
double w0 = 2 * Math.PI * freq / sampleRate;
double alpha = Math.Sin(w0) / 2.0 * Math.Sqrt(2) / 2.0;

double b0_c = A * ((A + 1) - (A - 1) * Math.Cos(w0) + 2 * Math.Sqrt(A) * alpha);
double b1_c = 2 * A * ((A - 1) - (A + 1) * Math.Cos(w0));
double b2_c = A * ((A + 1) - (A - 1) * Math.Cos(w0) - 2 * Math.Sqrt(A) * alpha);
double a0_c = (A + 1) + (A - 1) * Math.Cos(w0) + 2 * Math.Sqrt(A) * alpha;
double a1_c = -2 * ((A - 1) + (A + 1) * Math.Cos(w0));
double a2_c = (A + 1) + (A - 1) * Math.Cos(w0) - 2 * Math.Sqrt(A) * alpha;

b0 = (float)(b0_c / a0_c);
b1 = (float)(b1_c / a0_c);
b2 = (float)(b2_c / a0_c);
a1 = (float)(a1_c / a0_c);
a2 = (float)(a2_c / a0_c);
}

public void Process(float[] buffer, int offset, int count) {
if (IsBypassed) return;
for (int i = offset; i < offset + count; i++) {
int ch = i % channels;
float x = buffer[i];
float y = b0 * x + b1 * x1[ch] + b2 * x2[ch] - a1 * y1[ch] - a2 * y2[ch];

x2[ch] = x1[ch]; x1[ch] = x;
y2[ch] = y1[ch]; y1[ch] = y;

buffer[i] = y;
}
}
}
}
33 changes: 33 additions & 0 deletions OpenUtau.Core/SignalChain/Effects/Saturation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;

namespace OpenUtau.Core.SignalChain.Effects {
/// <summary>
/// Applies soft clipping via a hyperbolic tangent (Tanh) function to introduce
/// harmonic distortion, thickening the vocal and simulating analog warmth.
/// </summary>
public class Saturation {
private float drive;
private float mix;

public bool IsBypassed => mix <= 0.001f || drive <= 0.001f;

public void Configure(double driveDb, double mixFactor) {
this.drive = (float)driveDb;
this.mix = (float)mixFactor;
}

public void Process(float[] buffer, int offset, int count) {
if (IsBypassed) return;

// Map 0-10 Drive to a gain multiplier
float gain = 1f + (drive * 0.4f);
float invGain = 1f / (float)Math.Tanh(gain); // Gain compensation

for (int i = offset; i < offset + count; i++) {
float dry = buffer[i];
float wet = (float)Math.Tanh(dry * gain) * invGain;
buffer[i] = dry + (wet - dry) * mix;
}
}
}
}
78 changes: 39 additions & 39 deletions OpenUtau.Core/SignalChain/MixFxSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,94 +3,94 @@
using OpenUtau.Core.Ustx;

namespace OpenUtau.Core.SignalChain {
/// <summary>
/// ISignalSource wrapper that applies the user-configured post-FX chain
/// (3-band EQ -> compressor -> reverb). When all effects bypass, the
/// wrapper still adds a small amount of work (one memcpy + a few branches);
/// callers that want literal zero overhead should check
/// <see cref="IsAnythingEnabled"/> and skip wrapping entirely.
///
/// The wrapper is stateful (filter state, envelope follower, reverb
/// buffers) and must be constructed fresh per playback session.
/// </summary>
public class MixFxSource : ISignalSource {
public const int SampleRate = 44100;
public const int Channels = 2;

private readonly ISignalSource source;
private readonly DeThumper deThumper;
private readonly BiquadEQ eq;
private readonly DeEsser deEsser;
private readonly SimpleCompressor comp;
private readonly Saturation saturation;
private readonly Freeverb reverb;

// Scratch buffer. The signal chain in MasterAdapter passes in a
// zeroed buffer and we mix into it; we need a private writeable copy
// because the inner source uses additive mixing.
private float[] scratch;

public MixFxSource(ISignalSource source,
BiquadEQ eq, SimpleCompressor comp, Freeverb reverb) {
DeThumper deThumper, BiquadEQ eq, DeEsser deEsser,
SimpleCompressor comp, Saturation saturation, Freeverb reverb) {
this.source = source;
this.deThumper = deThumper;
this.eq = eq;
this.deEsser = deEsser;
this.comp = comp;
this.saturation = saturation;
this.reverb = reverb;
}

public bool IsReady(int position, int count) => source.IsReady(position, count);

public int Mix(int position, float[] buffer, int index, int count) {
// Allocate / grow scratch as needed. In the common case the
// playback buffer size is constant so this is allocated once.
if (scratch == null || scratch.Length < count) {
scratch = new float[count];
}
Array.Clear(scratch, 0, count);
int ret = source.Mix(position, scratch, 0, count);

// Apply effects in series. Each effect short-circuits internally
// when its parameters are at unity so individually-disabled stages
// cost effectively nothing.
// Apply effects in series.
// Typical Chain: Thump -> EQ -> Esser -> Comp -> Saturation -> Reverb
deThumper.Process(scratch, 0, count);
eq.Process(scratch, 0, count);
deEsser.Process(scratch, 0, count);
comp.Process(scratch, 0, count);
saturation.Process(scratch, 0, count);
reverb.Process(scratch, 0, count);

// Additive mix into output (matches Fader / WaveMix convention).
for (int i = 0; i < count; i++) {
buffer[index + i] += scratch[i];
}
return ret;
}

/// <summary>True iff at least one effect would change the signal.</summary>
public bool IsAnythingEnabled => !eq.IsBypassed || !comp.IsBypassed || !reverb.IsBypassed;
public bool IsAnythingEnabled =>
!deThumper.IsBypassed || !eq.IsBypassed || !deEsser.IsBypassed ||
!comp.IsBypassed || !saturation.IsBypassed || !reverb.IsBypassed;

/// <summary>
/// Per-track wrapper. Returns the inner source unchanged when the
/// track has no FX configured or has Enabled = false.
/// </summary>
public static ISignalSource WrapWith(ISignalSource inner, UMixFx fx) {
if (fx == null || !fx.Enabled) {
return inner;
}
if (fx == null || !fx.Enabled) return inner;

// 1. De-Thumper
var deThumper = new DeThumper(SampleRate, Channels);
deThumper.Configure(fx.DeThumperFreq, fx.DeThumperReductionDb, SampleRate);

// 2. EQ
var eq = new BiquadEQ(SampleRate, Channels);
eq.Configure(fx.EqLowDb, fx.EqMidFreq, 0.707, fx.EqMidDb, fx.EqHighDb);

// 3. De-Esser
var deEsser = new DeEsser(SampleRate, Channels);
deEsser.Configure(fx.DeEsserFreq, fx.DeEsserThresholdDb, SampleRate);

// 4. Compressor
var comp = new SimpleCompressor(SampleRate, Channels);
FxPresets.CompParams cParams = FxPresets.Comp.TryGetValue(fx.CompPreset ?? FxPresets.Off, out var cp)
? cp
: FxPresets.Comp[FxPresets.Off];
comp.Configure(fx.CompThresholdDb, fx.CompRatio,
cParams.AttackMs, cParams.ReleaseMs, fx.CompMakeupDb);
? cp : FxPresets.Comp[FxPresets.Off];
comp.Configure(fx.CompThresholdDb, fx.CompRatio, cParams.AttackMs, cParams.ReleaseMs, fx.CompMakeupDb);

// 5. Saturation
var saturation = new Saturation();
saturation.Configure(fx.SaturationDrive, fx.SaturationMix);

// 6. Reverb
var reverb = new Freeverb(SampleRate, Channels);
FxPresets.ReverbParams rParams = FxPresets.Reverb.TryGetValue(fx.ReverbPreset ?? FxPresets.Off, out var rp)
? rp
: FxPresets.Reverb[FxPresets.Off];
? rp : FxPresets.Reverb[FxPresets.Off];
double userWet = Math.Clamp(fx.ReverbWet, 0.0, 2.0);
reverb.Configure(fx.ReverbSize, fx.ReverbDamp, rParams.Width,
rParams.Wet * userWet, rParams.Dry, fx.ReverbPreDelayMs);
reverb.Configure(fx.ReverbSize, fx.ReverbDamp, rParams.Width, rParams.Wet * userWet, rParams.Dry, fx.ReverbPreDelayMs);

var wrapper = new MixFxSource(inner, eq, comp, reverb);
var wrapper = new MixFxSource(inner, deThumper, eq, deEsser, comp, saturation, reverb);
return wrapper.IsAnythingEnabled ? wrapper : inner;
}
}
}
}
33 changes: 28 additions & 5 deletions OpenUtau.Core/Ustx/UMixFx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,38 @@ public class UMixFx {
public string EqPreset { get; set; } = "vocal_air";
public string CompPreset { get; set; } = "gentle";
public string ReverbPreset { get; set; } = "small_room";
public string DeEsserPreset { get; set; } = "standard";
public string DeThumperPreset { get; set; } = "standard";
public string SaturationPreset { get; set; } = "standard";

// EQ
public double EqLowDb { get; set; } = 0.0;
public double EqMidFreq { get; set; } = 3000.0;
public double EqMidDb { get; set; } = 1.5;
public double EqHighDb { get; set; } = 3.0;

// Compressor
public double CompThresholdDb { get; set; } = -18.0;
public double CompRatio { get; set; } = 2.0;
public double CompMakeupDb { get; set; } = 2.5;

// Reverb
public double ReverbSize { get; set; } = 0.30;
public double ReverbDamp { get; set; } = 0.7;
public double ReverbWet { get; set; } = 1.0;
// Default 0 (not 12) so legacy ustx files without this field deserialize
// to the same audio they previously rendered. The recommended-rack
// builder and reverb preset loader assign explicit non-zero values
// for new projects.
public double ReverbPreDelayMs { get; set; } = 0.0;

// De-esser
public double DeEsserFreq { get; set; } = 6000.0;
public double DeEsserThresholdDb { get; set; } = -20.0;

// De-thumper
public double DeThumperFreq { get; set; } = 80.0;
public double DeThumperReductionDb { get; set; } = -6.0;

// Saturation
public double SaturationDrive { get; set; } = 0.0;
public double SaturationMix { get; set; } = 0.0;

public UMixFx Clone() {
return new UMixFx {
Expand All @@ -48,7 +62,16 @@ public UMixFx Clone() {
ReverbDamp = ReverbDamp,
ReverbWet = ReverbWet,
ReverbPreDelayMs = ReverbPreDelayMs,
DeEsserFreq = DeEsserFreq,
DeEsserThresholdDb = DeEsserThresholdDb,
DeThumperFreq = DeThumperFreq,
DeThumperReductionDb = DeThumperReductionDb,
SaturationDrive = SaturationDrive,
SaturationMix = SaturationMix,
DeEsserPreset = DeEsserPreset,
DeThumperPreset = DeThumperPreset,
SaturationPreset = SaturationPreset,
};
}
}
}
}
Loading
Loading