From 2281c475401954ab9ab268ffac740a6da678cb2f Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Sat, 27 Jun 2026 14:49:22 +0800 Subject: [PATCH] more track fx --- OpenUtau.Core/SignalChain/Effects/DeEsser.cs | 75 +++++++++ .../SignalChain/Effects/DeThumper.cs | 58 +++++++ .../SignalChain/Effects/Saturation.cs | 33 ++++ OpenUtau.Core/SignalChain/MixFxSource.cs | 78 ++++----- OpenUtau.Core/Ustx/UMixFx.cs | 33 +++- OpenUtau/ViewModels/MixFxViewModel.cs | 150 ++++++++++++++++-- OpenUtau/Views/MixFxDialog.axaml | 109 +++++++++---- OpenUtau/Views/MixFxDialog.axaml.cs | 18 +++ 8 files changed, 469 insertions(+), 85 deletions(-) create mode 100644 OpenUtau.Core/SignalChain/Effects/DeEsser.cs create mode 100644 OpenUtau.Core/SignalChain/Effects/DeThumper.cs create mode 100644 OpenUtau.Core/SignalChain/Effects/Saturation.cs 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 @@