diff --git a/OpenUtau.Core/SignalChain/Effects/DeEsser.cs b/OpenUtau.Core/SignalChain/Effects/DeEsser.cs
new file mode 100644
index 000000000..5cd0df0d9
--- /dev/null
+++ b/OpenUtau.Core/SignalChain/Effects/DeEsser.cs
@@ -0,0 +1,75 @@
+using System;
+
+namespace OpenUtau.Core.SignalChain.Effects {
+ ///
+ /// Isolates high-frequency sibilance using a sidechain high-pass filter,
+ /// dynamically ducking those frequencies when they exceed a threshold.
+ ///
+ 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/OpenUtau.Core/SignalChain/Effects/DeThumper.cs b/OpenUtau.Core/SignalChain/Effects/DeThumper.cs
new file mode 100644
index 000000000..b48a3664e
--- /dev/null
+++ b/OpenUtau.Core/SignalChain/Effects/DeThumper.cs
@@ -0,0 +1,58 @@
+using System;
+
+namespace OpenUtau.Core.SignalChain.Effects {
+ ///
+ /// A low-shelf biquad filter designed to reduce low-frequency rumble, plosives, and mud.
+ ///
+ 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;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/OpenUtau.Core/SignalChain/Effects/Saturation.cs b/OpenUtau.Core/SignalChain/Effects/Saturation.cs
new file mode 100644
index 000000000..c2f36c5b5
--- /dev/null
+++ b/OpenUtau.Core/SignalChain/Effects/Saturation.cs
@@ -0,0 +1,33 @@
+using System;
+
+namespace OpenUtau.Core.SignalChain.Effects {
+ ///
+ /// Applies soft clipping via a hyperbolic tangent (Tanh) function to introduce
+ /// harmonic distortion, thickening the vocal and simulating analog warmth.
+ ///
+ 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;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/OpenUtau.Core/SignalChain/MixFxSource.cs b/OpenUtau.Core/SignalChain/MixFxSource.cs
index 71a6cbbc7..4e9b464af 100644
--- a/OpenUtau.Core/SignalChain/MixFxSource.cs
+++ b/OpenUtau.Core/SignalChain/MixFxSource.cs
@@ -3,94 +3,94 @@
using OpenUtau.Core.Ustx;
namespace OpenUtau.Core.SignalChain {
- ///
- /// 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
- /// and skip wrapping entirely.
- ///
- /// The wrapper is stateful (filter state, envelope follower, reverb
- /// buffers) and must be constructed fresh per playback session.
- ///
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;
}
- /// True iff at least one effect would change the signal.
- public bool IsAnythingEnabled => !eq.IsBypassed || !comp.IsBypassed || !reverb.IsBypassed;
+ public bool IsAnythingEnabled =>
+ !deThumper.IsBypassed || !eq.IsBypassed || !deEsser.IsBypassed ||
+ !comp.IsBypassed || !saturation.IsBypassed || !reverb.IsBypassed;
- ///
- /// Per-track wrapper. Returns the inner source unchanged when the
- /// track has no FX configured or has Enabled = false.
- ///
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;
}
}
-}
+}
\ No newline at end of file
diff --git a/OpenUtau.Core/Ustx/UMixFx.cs b/OpenUtau.Core/Ustx/UMixFx.cs
index e476d9d18..96f531c93 100644
--- a/OpenUtau.Core/Ustx/UMixFx.cs
+++ b/OpenUtau.Core/Ustx/UMixFx.cs
@@ -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 {
@@ -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,
};
}
}
-}
+}
\ No newline at end of file
diff --git a/OpenUtau/ViewModels/MixFxViewModel.cs b/OpenUtau/ViewModels/MixFxViewModel.cs
index b3f96204c..a4449c1b1 100644
--- a/OpenUtau/ViewModels/MixFxViewModel.cs
+++ b/OpenUtau/ViewModels/MixFxViewModel.cs
@@ -33,6 +33,11 @@ public PresetOption(string key, string label) {
public List EqPresets { get; }
public List CompPresets { get; }
public List ReverbPresets { get; }
+
+ // New Effect Presets
+ public List DeEsserPresets { get; }
+ public List DeThumperPresets { get; }
+ public List SaturationPresets { get; }
public ObservableCollection UserPresets { get; }
@@ -44,22 +49,42 @@ public PresetOption(string key, string label) {
[Reactive] public PresetOption? SelectedEq { get; set; }
[Reactive] public PresetOption? SelectedComp { get; set; }
[Reactive] public PresetOption? SelectedReverb { get; set; }
+
+ [Reactive] public PresetOption? SelectedDeEsser { get; set; }
+ [Reactive] public PresetOption? SelectedDeThumper { get; set; }
+ [Reactive] public PresetOption? SelectedSaturation { get; set; }
+
[Reactive] public Preferences.MixFxUserPreset? SelectedUserPreset { get; set; }
+ // EQ
[Reactive] public double EqLowDb { get; set; }
[Reactive] public double EqMidFreq { get; set; }
[Reactive] public double EqMidDb { get; set; }
[Reactive] public double EqHighDb { get; set; }
+ // Compressor
[Reactive] public double CompThresholdDb { get; set; }
[Reactive] public double CompRatio { get; set; }
[Reactive] public double CompMakeupDb { get; set; }
+ // Reverb
[Reactive] public double ReverbSize { get; set; }
[Reactive] public double ReverbDamp { get; set; }
[Reactive] public double ReverbWet { get; set; }
[Reactive] public double ReverbPreDelayMs { get; set; }
+ // De-esser
+ [Reactive] public double DeEsserFreq { get; set; }
+ [Reactive] public double DeEsserThresholdDb { get; set; }
+
+ // De-thumper
+ [Reactive] public double DeThumperFreq { get; set; }
+ [Reactive] public double DeThumperReductionDb { get; set; }
+
+ // Saturation
+ [Reactive] public double SaturationDrive { get; set; }
+ [Reactive] public double SaturationMix { get; set; }
+
[Reactive] public bool ApplyOnExportMixdown { get; set; }
// True when the currently-selected library entry is user-deletable
@@ -92,6 +117,15 @@ public MixFxViewModel(UTrack? track) {
.Select(k => new PresetOption(k, PrettyLabel(k)))
.ToList();
+ // Build preset lists for new effects
+ var newEffectPresets = new List {
+ new PresetOption(FxPresets.Off, PrettyLabel(FxPresets.Off)),
+ new PresetOption("standard", "Standard")
+ };
+ DeEsserPresets = newEffectPresets.ToList();
+ DeThumperPresets = newEffectPresets.ToList();
+ SaturationPresets = newEffectPresets.ToList();
+
defaultPreset = new Preferences.MixFxUserPreset {
Name = ThemeManager.GetString("mixfx.library.default"),
Fx = BuildDefaultFx(),
@@ -101,40 +135,65 @@ public MixFxViewModel(UTrack? track) {
UserPresets.Add(p);
}
+ // --- BUG FIX: Move Subscriptions Here ---
+ // Picking a preset reloads its parameters into the sliders.
+ // Subscribing first ensures that when we load the track state below, the suspendBindings flag prevents overwriting.
+ this.WhenAnyValue(x => x.SelectedEq).Subscribe(opt => { if (opt != null) LoadEqPreset(opt.Key); });
+ this.WhenAnyValue(x => x.SelectedComp).Subscribe(opt => { if (opt != null) LoadCompPreset(opt.Key); });
+ this.WhenAnyValue(x => x.SelectedReverb).Subscribe(opt => { if (opt != null) LoadReverbPreset(opt.Key); });
+
+ this.WhenAnyValue(x => x.SelectedDeEsser).Subscribe(opt => { if (opt != null) LoadDeEsserPreset(opt.Key); });
+ this.WhenAnyValue(x => x.SelectedDeThumper).Subscribe(opt => { if (opt != null) LoadDeThumperPreset(opt.Key); });
+ this.WhenAnyValue(x => x.SelectedSaturation).Subscribe(opt => { if (opt != null) LoadSaturationPreset(opt.Key); });
+
+ this.WhenAnyValue(x => x.SelectedUserPreset).Subscribe(p => { if (p != null) LoadUserPreset(p); });
+
+ this.WhenAnyValue(x => x.SelectedUserPreset)
+ .Select(p => p != null && !ReferenceEquals(p, defaultPreset))
+ .ToProperty(this, x => x.CanDeleteSelectedPreset, out canDeleteSelectedPreset);
+
// Seed dialog state from track's existing FX, or sensible defaults.
var fx = track?.MixFx ?? new UMixFx();
suspendBindings = true;
try {
Enabled = track?.MixFx?.Enabled ?? false;
+
+ // Load Preset States
SelectedEq = FindOrFirst(EqPresets, fx.EqPreset);
SelectedComp = FindOrFirst(CompPresets, fx.CompPreset);
SelectedReverb = FindOrFirst(ReverbPresets, fx.ReverbPreset);
+ SelectedDeEsser = FindOrFirst(DeEsserPresets, fx.DeEsserPreset);
+ SelectedDeThumper = FindOrFirst(DeThumperPresets, fx.DeThumperPreset);
+ SelectedSaturation = FindOrFirst(SaturationPresets, fx.SaturationPreset);
+
EqLowDb = fx.EqLowDb;
EqMidFreq = fx.EqMidFreq;
EqMidDb = fx.EqMidDb;
EqHighDb = fx.EqHighDb;
+
CompThresholdDb = fx.CompThresholdDb;
CompRatio = fx.CompRatio;
CompMakeupDb = fx.CompMakeupDb;
+
ReverbSize = fx.ReverbSize;
ReverbDamp = fx.ReverbDamp;
ReverbWet = fx.ReverbWet;
ReverbPreDelayMs = fx.ReverbPreDelayMs;
+
+ DeEsserFreq = fx.DeEsserFreq;
+ DeEsserThresholdDb = fx.DeEsserThresholdDb;
+
+ DeThumperFreq = fx.DeThumperFreq;
+ DeThumperReductionDb = fx.DeThumperReductionDb;
+
+ SaturationDrive = fx.SaturationDrive;
+ SaturationMix = fx.SaturationMix;
+
ApplyOnExportMixdown = Preferences.Default.MixFxApplyOnExportMixdown;
} finally {
suspendBindings = false;
}
- // Picking a preset reloads its parameters into the sliders.
- this.WhenAnyValue(x => x.SelectedEq).Subscribe(opt => { if (opt != null) LoadEqPreset(opt.Key); });
- this.WhenAnyValue(x => x.SelectedComp).Subscribe(opt => { if (opt != null) LoadCompPreset(opt.Key); });
- this.WhenAnyValue(x => x.SelectedReverb).Subscribe(opt => { if (opt != null) LoadReverbPreset(opt.Key); });
- this.WhenAnyValue(x => x.SelectedUserPreset).Subscribe(p => { if (p != null) LoadUserPreset(p); });
-
- this.WhenAnyValue(x => x.SelectedUserPreset)
- .Select(p => p != null && !ReferenceEquals(p, defaultPreset))
- .ToProperty(this, x => x.CanDeleteSelectedPreset, out canDeleteSelectedPreset);
-
ApplyRecommendedCommand = ReactiveCommand.Create(ApplyRecommended);
SaveUserPresetCommand = ReactiveCommand.CreateFromTask(SaveUserPresetAsync);
DeleteUserPresetCommand = ReactiveCommand.Create(DeleteUserPreset);
@@ -166,6 +225,13 @@ private static UMixFx BuildDefaultFx() {
ReverbPreset = "small_room",
ReverbSize = r.RoomSize, ReverbDamp = r.Damp, ReverbWet = 1.0,
ReverbPreDelayMs = r.PreDelayMs,
+
+ DeEsserPreset = FxPresets.Off,
+ DeThumperPreset = FxPresets.Off,
+ SaturationPreset = FxPresets.Off,
+ DeEsserFreq = 6000.0, DeEsserThresholdDb = 0.0,
+ DeThumperFreq = 80.0, DeThumperReductionDb = 0.0,
+ SaturationDrive = 0.0, SaturationMix = 0.0
};
}
@@ -208,19 +274,72 @@ private void LoadReverbPreset(string key) {
}
}
+ private void LoadDeEsserPreset(string key) {
+ if (suspendBindings) return;
+ suspendBindings = true;
+ try {
+ if (key == FxPresets.Off) {
+ DeEsserThresholdDb = 0.0; // 0 Threshold = Bypassed
+ } else {
+ DeEsserFreq = 6000.0;
+ DeEsserThresholdDb = -20.0;
+ }
+ } finally { suspendBindings = false; }
+ }
+
+ private void LoadDeThumperPreset(string key) {
+ if (suspendBindings) return;
+ suspendBindings = true;
+ try {
+ if (key == FxPresets.Off) {
+ DeThumperReductionDb = 0.0; // 0 Reduction = Bypassed
+ } else {
+ DeThumperFreq = 80.0;
+ DeThumperReductionDb = -6.0;
+ }
+ } finally { suspendBindings = false; }
+ }
+
+ private void LoadSaturationPreset(string key) {
+ if (suspendBindings) return;
+ suspendBindings = true;
+ try {
+ if (key == FxPresets.Off) {
+ SaturationDrive = 0.0; // 0 Mix/Drive = Bypassed
+ SaturationMix = 0.0;
+ } else {
+ SaturationDrive = 4.0;
+ SaturationMix = 0.5;
+ }
+ } finally { suspendBindings = false; }
+ }
+
private void LoadUserPreset(Preferences.MixFxUserPreset p) {
+ if (suspendBindings) return;
if (p == null || p.Fx == null) return;
var fx = p.Fx;
suspendBindings = true;
try {
Enabled = fx.Enabled || Enabled;
+
SelectedEq = FindOrFirst(EqPresets, fx.EqPreset);
SelectedComp = FindOrFirst(CompPresets, fx.CompPreset);
SelectedReverb = FindOrFirst(ReverbPresets, fx.ReverbPreset);
+ SelectedDeEsser = FindOrFirst(DeEsserPresets, fx.DeEsserPreset);
+ SelectedDeThumper = FindOrFirst(DeThumperPresets, fx.DeThumperPreset);
+ SelectedSaturation = FindOrFirst(SaturationPresets, fx.SaturationPreset);
+
EqLowDb = fx.EqLowDb; EqMidFreq = fx.EqMidFreq; EqMidDb = fx.EqMidDb; EqHighDb = fx.EqHighDb;
CompThresholdDb = fx.CompThresholdDb; CompRatio = fx.CompRatio; CompMakeupDb = fx.CompMakeupDb;
ReverbSize = fx.ReverbSize; ReverbDamp = fx.ReverbDamp; ReverbWet = fx.ReverbWet;
ReverbPreDelayMs = fx.ReverbPreDelayMs;
+
+ DeEsserFreq = fx.DeEsserFreq;
+ DeEsserThresholdDb = fx.DeEsserThresholdDb;
+ DeThumperFreq = fx.DeThumperFreq;
+ DeThumperReductionDb = fx.DeThumperReductionDb;
+ SaturationDrive = fx.SaturationDrive;
+ SaturationMix = fx.SaturationMix;
} finally {
suspendBindings = false;
}
@@ -279,10 +398,21 @@ public UMixFx BuildUMixFx() {
EqPreset = SelectedEq?.Key ?? FxPresets.Off,
CompPreset = SelectedComp?.Key ?? FxPresets.Off,
ReverbPreset = SelectedReverb?.Key ?? FxPresets.Off,
+ DeEsserPreset = SelectedDeEsser?.Key ?? FxPresets.Off,
+ DeThumperPreset = SelectedDeThumper?.Key ?? FxPresets.Off,
+ SaturationPreset = SelectedSaturation?.Key ?? FxPresets.Off,
+
EqLowDb = EqLowDb, EqMidFreq = EqMidFreq, EqMidDb = EqMidDb, EqHighDb = EqHighDb,
CompThresholdDb = CompThresholdDb, CompRatio = CompRatio, CompMakeupDb = CompMakeupDb,
ReverbSize = ReverbSize, ReverbDamp = ReverbDamp, ReverbWet = ReverbWet,
ReverbPreDelayMs = ReverbPreDelayMs,
+
+ DeEsserFreq = DeEsserFreq,
+ DeEsserThresholdDb = DeEsserThresholdDb,
+ DeThumperFreq = DeThumperFreq,
+ DeThumperReductionDb = DeThumperReductionDb,
+ SaturationDrive = SaturationDrive,
+ SaturationMix = SaturationMix
};
}
diff --git a/OpenUtau/Views/MixFxDialog.axaml b/OpenUtau/Views/MixFxDialog.axaml
index 1c6f90957..38888cc94 100644
--- a/OpenUtau/Views/MixFxDialog.axaml
+++ b/OpenUtau/Views/MixFxDialog.axaml
@@ -6,7 +6,7 @@
x:Class="OpenUtau.App.Views.MixFxDialog"
Icon="/Assets/open-utau.ico"
Title="{DynamicResource mixfx.caption}"
- Width="640" Height="540"
+ Width="640" SizeToContent="Height"
WindowStartupLocation="CenterOwner"
CanResize="False">
@@ -26,10 +26,10 @@