From adb28a1e6d037241c5e323e0d364b3dbfabd16ea Mon Sep 17 00:00:00 2001 From: VoidX Date: Sun, 22 Sep 2024 14:58:19 +0200 Subject: [PATCH] Support Sony ES series --- .../FilterSet/BaseClasses/FilterSet.cs | 49 +++++++++++---- .../FilterSet/BaseClasses/FilterSetTarget.cs | 5 ++ .../FilterSet/BaseClasses/IIRFilterSet.cs | 26 +------- .../BaseClasses/LimitedEqualizerFilterSet.cs | 62 +++++++++++++++++++ .../FilterSet/SonyESSeriesFilterSet.cs | 38 ++++++++++++ .../Equalization/Equalizer.Transform.cs | 5 ++ README.md | 2 +- docs/NuGet Readme.md | 2 +- 8 files changed, 150 insertions(+), 39 deletions(-) create mode 100644 Cavern.QuickEQ.Format/FilterSet/BaseClasses/LimitedEqualizerFilterSet.cs create mode 100644 Cavern.QuickEQ.Format/FilterSet/SonyESSeriesFilterSet.cs diff --git a/Cavern.QuickEQ.Format/FilterSet/BaseClasses/FilterSet.cs b/Cavern.QuickEQ.Format/FilterSet/BaseClasses/FilterSet.cs index aa8472f..a2cca4d 100644 --- a/Cavern.QuickEQ.Format/FilterSet/BaseClasses/FilterSet.cs +++ b/Cavern.QuickEQ.Format/FilterSet/BaseClasses/FilterSet.cs @@ -7,6 +7,7 @@ using Cavern.Channels; using Cavern.Filters; using Cavern.Format.Common; +using Cavern.Utilities; namespace Cavern.Format.FilterSet { /// @@ -62,6 +63,19 @@ public abstract class ChannelData { /// protected FilterSet(int sampleRate) => SampleRate = sampleRate; + /// + /// Convert a double to string with its maximum decimal places dependent on the base 10 logarithm. + /// + protected static string RangeDependentDecimals(double value) { + if (value < 100) { + return QMath.ToStringLimitDecimals(value, 2); + } else if (value < 1000) { + return QMath.ToStringLimitDecimals(value, 1); + } else { + return QMath.ToStringLimitDecimals(value, 0); + } + } + /// public abstract void Export(string path); @@ -91,6 +105,7 @@ public static FilterSet Create(FilterSetTarget device, int channels, int sampleR FilterSetTarget.AcurusMuse => new AcurusMuseFilterSet(channels, sampleRate), FilterSetTarget.Emotiva => new EmotivaFilterSet(channels, sampleRate), FilterSetTarget.MonolithHTP1 => new MonolithHTP1FilterSet(channels, sampleRate), + FilterSetTarget.SonyES => new SonyESSeriesFilterSet(channels, sampleRate), FilterSetTarget.StormAudio => new StormAudioFilterSet(channels, sampleRate), FilterSetTarget.TonewinnerAT => new TonewinnerATFilterSet(channels, sampleRate), FilterSetTarget.BehringerNX => new BehringerNXFilterSet(channels, sampleRate), @@ -131,6 +146,7 @@ public static FilterSet Create(FilterSetTarget device, ReferenceChannel[] channe FilterSetTarget.AcurusMuse => new AcurusMuseFilterSet(channels, sampleRate), FilterSetTarget.Emotiva => new EmotivaFilterSet(channels, sampleRate), FilterSetTarget.MonolithHTP1 => new MonolithHTP1FilterSet(channels, sampleRate), + FilterSetTarget.SonyES => new SonyESSeriesFilterSet(channels, sampleRate), FilterSetTarget.StormAudio => new StormAudioFilterSet(channels, sampleRate), FilterSetTarget.TonewinnerAT => new TonewinnerATFilterSet(channels, sampleRate), FilterSetTarget.BehringerNX => new BehringerNXFilterSet(channels, sampleRate), @@ -160,6 +176,23 @@ public static FilterSet Create(FilterSetTarget device, ReferenceChannel[] channe /// protected virtual string GetLabel(int channel) => Channels[channel].name ?? "CH" + (channel + 1); + /// + /// Insert channel header and basic information to a root file. + /// + /// Any information was exported. + protected bool RootFileChannelHeader(int channel, StringBuilder result) { + result.AppendLine(string.Empty); + string chName = GetLabel(channel); + result.AppendLine(chName); + result.AppendLine(new string('=', chName.Length)); + RootFileExtension(channel, result); + if (Channels[channel].delaySamples != 0) { + result.AppendLine("Delay: " + QMath.ToStringLimitDecimals(GetDelay(channel), 2)); + return true; + } + return false; + } + /// /// Add extra information for a channel that can't be part of the filter files to be written in the root file. /// @@ -204,21 +237,11 @@ protected void CreateRootFile(string path, string filterFileExtension) { string fileNameBase = Path.GetFileName(path); fileNameBase = fileNameBase[..fileNameBase.LastIndexOf('.')]; StringBuilder result = new StringBuilder(); - bool hasAnything = false, - hasDelays = false; + bool hasDelays = false; for (int i = 0, c = Channels.Length; i < c; i++) { - result.AppendLine(string.Empty); - result.AppendLine("Channel: " + GetLabel(i)); - if (Channels[i].delaySamples != 0) { - result.AppendLine("Delay: " + GetDelay(i).ToString("0.0 ms")); - hasAnything = true; - hasDelays = true; - } - int before = result.Length; - RootFileExtension(i, result); - hasAnything |= result.Length != before; + hasDelays |= RootFileChannelHeader(i, result); } - if (hasAnything) { + if (result.Length != 0) { File.WriteAllText(path, (hasDelays ? $"Set up levels and delays by this file. Load \"{fileNameBase} .{filterFileExtension}\" files as EQ." : $"Set up levels by this file. Load \"{fileNameBase} .{filterFileExtension}\" files as EQ.") + diff --git a/Cavern.QuickEQ.Format/FilterSet/BaseClasses/FilterSetTarget.cs b/Cavern.QuickEQ.Format/FilterSet/BaseClasses/FilterSetTarget.cs index a1abf03..1ce141c 100644 --- a/Cavern.QuickEQ.Format/FilterSet/BaseClasses/FilterSetTarget.cs +++ b/Cavern.QuickEQ.Format/FilterSet/BaseClasses/FilterSetTarget.cs @@ -84,6 +84,10 @@ public enum FilterSetTarget { /// MonolithHTP1, /// + /// Sony ES-series AVRs. + /// + SonyES, + /// /// StormAudio ISP processors. /// StormAudio, @@ -183,6 +187,7 @@ public static class FilterSetTargetExtensions { FilterSetTarget.AcurusMuse => "Acurus Muse", FilterSetTarget.Emotiva => "Emotiva", FilterSetTarget.MonolithHTP1 => "Monoprice Monolith HTP-1", + FilterSetTarget.SonyES => "Sony ES series", FilterSetTarget.StormAudio => "StormAudio", FilterSetTarget.TonewinnerAT => "Tonewinner AT series", FilterSetTarget.BehringerNX => "Behringer NX series", diff --git a/Cavern.QuickEQ.Format/FilterSet/BaseClasses/IIRFilterSet.cs b/Cavern.QuickEQ.Format/FilterSet/BaseClasses/IIRFilterSet.cs index 106e7aa..0212f30 100644 --- a/Cavern.QuickEQ.Format/FilterSet/BaseClasses/IIRFilterSet.cs +++ b/Cavern.QuickEQ.Format/FilterSet/BaseClasses/IIRFilterSet.cs @@ -78,19 +78,6 @@ public bool Equals(IIRChannelData other) => filters.Equals(other.filters) && gai /// public IIRFilterSet(ReferenceChannel[] channels, int sampleRate) : base(sampleRate) => Initialize(channels); - /// - /// Convert a double to string with its maximum decimal places dependent on the base 10 logarithm. - /// - static string RangeDependentDecimals(double value) { - if (value < 100) { - return QMath.ToStringLimitDecimals(value, 2); - } else if (value < 1000) { - return QMath.ToStringLimitDecimals(value, 1); - } else { - return QMath.ToStringLimitDecimals(value, 0); - } - } - /// /// If the filter set's band count is dependent on which channel is selected, use this function instead of . /// @@ -201,17 +188,8 @@ public override void Export(string path) { protected virtual string Export(bool gainOnly) { StringBuilder result = new StringBuilder("Set up the channels according to this configuration.").AppendLine(); for (int i = 0; i < Channels.Length; i++) { - IIRChannelData channelRef = (IIRChannelData)Channels[i]; - result.AppendLine(string.Empty); - string chName = GetLabel(i); - result.AppendLine(chName); - result.AppendLine(new string('=', chName.Length)); - RootFileExtension(i, result); - if (channelRef.delaySamples != 0) { - result.AppendLine("Delay: " + QMath.ToStringLimitDecimals(GetDelay(i), 2)); - } - - BiquadFilter[] bands = channelRef.filters; + RootFileChannelHeader(i, result); + BiquadFilter[] bands = ((IIRChannelData)Channels[i]).filters; if (gainOnly) { for (int j = 0; j < bands.Length; j++) { string gain = QMath.ToStringLimitDecimals(bands[j].Gain, 2); diff --git a/Cavern.QuickEQ.Format/FilterSet/BaseClasses/LimitedEqualizerFilterSet.cs b/Cavern.QuickEQ.Format/FilterSet/BaseClasses/LimitedEqualizerFilterSet.cs new file mode 100644 index 0000000..e2368e2 --- /dev/null +++ b/Cavern.QuickEQ.Format/FilterSet/BaseClasses/LimitedEqualizerFilterSet.cs @@ -0,0 +1,62 @@ +using System.IO; +using System.Text; + +using Cavern.Channels; +using Cavern.QuickEQ.Equalization; +using Cavern.Utilities; + +namespace Cavern.Format.FilterSet.BaseClasses { + /// + /// A fixed set of bands to sample from an for export into a single file. This is the recommended and fastest + /// approach of getting a filter set for incoherent fixed EQ bands, such as the . + /// + public abstract class LimitedEqualizerFilterSet : EqualizerFilterSet { + /// + /// All frequency bands that need to be set. + /// + protected abstract float[] Frequencies { get; } + + /// + /// Frequency bands for the LFE channel. + /// + protected abstract float[] LFEFrequencies { get; } + + /// + /// How much smoothing in octaves shall be applied on the results to have a precise enough averaged value at each used frequency. + /// + protected abstract float Smoothing { get; } + + /// + /// A fixed set of bands to sample from an for export into a single file. + /// + protected LimitedEqualizerFilterSet(int sampleRate) : base(sampleRate) { } + + /// + /// A fixed set of bands to sample from an for export into a single file. + /// + protected LimitedEqualizerFilterSet(int channels, int sampleRate) : base(channels, sampleRate) { } + + /// + /// A fixed set of bands to sample from an for export into a single file. + /// + protected LimitedEqualizerFilterSet(ReferenceChannel[] channels, int sampleRate) : base(channels, sampleRate) { } + + /// + /// Save the results of each channel to a single file. + /// + public override void Export(string path) { + StringBuilder result = new StringBuilder("Set up the channels according to this configuration.").AppendLine(); + for (int i = 0; i < Channels.Length; i++) { + RootFileChannelHeader(i, result); + Equalizer curve = (Equalizer)((EqualizerChannelData)Channels[i]).curve.Clone(); + curve.Smooth(Smoothing); + float[] freqs = Channels[i].reference != ReferenceChannel.ScreenLFE ? Frequencies : LFEFrequencies; + for (int j = 0; j < freqs.Length; j++) { + string gain = QMath.ToStringLimitDecimals(curve[freqs[j]], 2); + result.AppendLine($"{RangeDependentDecimals(freqs[j])} Hz:\t{gain} dB"); + } + } + File.WriteAllText(path, result.ToString()); + } + } +} \ No newline at end of file diff --git a/Cavern.QuickEQ.Format/FilterSet/SonyESSeriesFilterSet.cs b/Cavern.QuickEQ.Format/FilterSet/SonyESSeriesFilterSet.cs new file mode 100644 index 0000000..27f241e --- /dev/null +++ b/Cavern.QuickEQ.Format/FilterSet/SonyESSeriesFilterSet.cs @@ -0,0 +1,38 @@ +using Cavern.Channels; +using Cavern.Format.FilterSet.BaseClasses; + +namespace Cavern.Format.FilterSet { + /// + /// Banded filter set for Sony ES-series receivers. + /// + public class SonyESSeriesFilterSet : LimitedEqualizerFilterSet { + /// + protected override float[] Frequencies => frequencies; + + /// + protected override float[] LFEFrequencies => lfeFrequencies; + + /// + protected override float Smoothing => 1; + + /// + /// Banded filter set for Sony ES-series receivers. + /// + public SonyESSeriesFilterSet(int channels, int sampleRate) : base(channels, sampleRate) { } + + /// + /// Banded filter set for Sony ES-series receivers. + /// + public SonyESSeriesFilterSet(ReferenceChannel[] channels, int sampleRate) : base(channels, sampleRate) { } + + /// + /// All frequency bands that need to be set. + /// + static readonly float[] frequencies = { 47, 230, 470, 840, 1300, 2300, 3800, 5800, 9000, 14000 }; + + /// + /// All LFE frequency bands that need to be set. + /// + static readonly float[] lfeFrequencies = { 40, 60, 80, 90, 100, 120 }; + } +} \ No newline at end of file diff --git a/Cavern.QuickEQ/Equalization/Equalizer.Transform.cs b/Cavern.QuickEQ/Equalization/Equalizer.Transform.cs index 7d5023b..0800d50 100644 --- a/Cavern.QuickEQ/Equalization/Equalizer.Transform.cs +++ b/Cavern.QuickEQ/Equalization/Equalizer.Transform.cs @@ -214,6 +214,11 @@ public void Normalize(double startFreq, double endFreq) { Offset(total / (first - last)); } + /// + /// Set the RMS gain of the curve to 0 dB between frequency limits. + /// + public void NormalizeRMS(double startFreq, double endFreq) => Offset(-GetAverageLevel(startFreq, endFreq)); + /// /// Change the frequencies contained in this . /// diff --git a/README.md b/README.md index c0eb1b4..c822c57 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Cavernize. * Supported software/hardware for EQ/filter set export: * PC: Equalizer APO, CamillaDSP * DSP: MiniDSP 2x4 Advanced, MiniDSP 2x4 HD, MiniDSP DDRC-88A - * Processors: Acurus Muse, Emotiva, Monolith HTP-1, StormAudio, Tonewinner AT series + * Processors: Acurus Muse, Emotiva, Monolith HTP-1, Sony ES series, StormAudio, Tonewinner AT series * Amplifiers: Behringer NX series * Others: Audyssey MultEQ-X, Dirac Live, YPAO * Direction and distance virtualization for headphones diff --git a/docs/NuGet Readme.md b/docs/NuGet Readme.md index 4b1aba6..0c96b5f 100644 --- a/docs/NuGet Readme.md +++ b/docs/NuGet Readme.md @@ -23,7 +23,7 @@ self-calibration libraries built on the Cavern engine are also available. * Supported software/hardware for EQ/filter set export: * PC: Equalizer APO, CamillaDSP * DSP: MiniDSP 2x4 Advanced, MiniDSP 2x4 HD, MiniDSP DDRC-88A - * Processors: Acurus Muse, Emotiva, Monolith HTP-1, StormAudio, Tonewinner AT series + * Processors: Acurus Muse, Emotiva, Monolith HTP-1, Sony ES series, StormAudio, Tonewinner AT series * Amplifiers: Behringer NX series * Others: Audyssey MultEQ-X, Dirac Live, YPAO * Direction and distance virtualization for headphones