diff --git a/Cavern.QuickEQ.Format/FilterSet/MonolithHTP1FilterSet.cs b/Cavern.QuickEQ.Format/FilterSet/MonolithHTP1FilterSet.cs index 8017ee81..6a8aa8b3 100644 --- a/Cavern.QuickEQ.Format/FilterSet/MonolithHTP1FilterSet.cs +++ b/Cavern.QuickEQ.Format/FilterSet/MonolithHTP1FilterSet.cs @@ -70,7 +70,7 @@ public override void Export(string path) { /// /// The corresponding JSON label for each supported channel. /// - readonly static Dictionary channelLabels = new Dictionary { + static readonly Dictionary channelLabels = new Dictionary { [ReferenceChannel.FrontLeft] = "lf", [ReferenceChannel.FrontRight] = "rf", [ReferenceChannel.FrontCenter] = "c", diff --git a/Cavern/Filters/BiquadFilter.cs b/Cavern/Filters/BiquadFilter.cs index 8f3135cf..94cc004d 100644 --- a/Cavern/Filters/BiquadFilter.cs +++ b/Cavern/Filters/BiquadFilter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Runtime.CompilerServices; using Cavern.Filters.Interfaces; @@ -18,6 +19,7 @@ public abstract class BiquadFilter : Filter, ICloneable, IEqualizerAPOFilter { /// /// Center frequency (-3 dB point) of the filter. /// + [DisplayName("Center frequency (Hz)")] public double CenterFreq { get => centerFreq; set => Reset(value, q, gain); @@ -26,6 +28,7 @@ public double CenterFreq { /// /// Q-factor of the filter. /// + [DisplayName("Q-factor")] public double Q { get => q; set => Reset(centerFreq, value, gain); @@ -34,6 +37,7 @@ public double Q { /// /// Gain of the filter in decibels. /// + [DisplayName("Gain (dB)")] public double Gain { get => gain; set => Reset(centerFreq, q, value); diff --git a/Cavern/Filters/BypassFilter.cs b/Cavern/Filters/BypassFilter.cs index 0c4c1219..b9c7cbdf 100644 --- a/Cavern/Filters/BypassFilter.cs +++ b/Cavern/Filters/BypassFilter.cs @@ -6,13 +6,13 @@ public class BypassFilter : Filter { /// /// Name of this filter node. /// - readonly string name; + public string Name { get; set; } /// /// A filter that doesn't do anything. Used to display empty filter nodes with custom names, like the beginning of virtual channels. /// /// Name of this filter node - public BypassFilter(string name) => this.name = name; + public BypassFilter(string name) => Name = name; /// public override void Process(float[] samples) { @@ -25,6 +25,6 @@ public override void Process(float[] samples, int channel, int channels) { } /// - public override string ToString() => name; + public override string ToString() => Name; } } \ No newline at end of file diff --git a/Cavern/Filters/Cavernize.cs b/Cavern/Filters/Cavernize.cs index c9df12b9..19a3bd62 100644 --- a/Cavern/Filters/Cavernize.cs +++ b/Cavern/Filters/Cavernize.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; namespace Cavern.Filters { /// @@ -8,6 +9,7 @@ public class Cavernize : Filter { /// /// Height separation effect strength. /// + [DisplayName("Effect (ratio)")] public float Effect { get; set; } = .75f; /// @@ -16,11 +18,13 @@ public class Cavernize : Filter { /// /// The default value is calculated with 0.8 smoothness, with an update rate of 240 at /// 48 kHz sampling. + [DisplayName("Smoothing factor (ratio)")] public float SmoothFactor { get; set; } = .0229349384f; /// /// Keep all frequencies below this on the ground. /// + [DisplayName("Ground crossover (Hz)")] public double GroundCrossover { get => crossover.Frequency; set => crossover.Frequency = value; diff --git a/Cavern/Filters/Comb.cs b/Cavern/Filters/Comb.cs index 3173aff0..4422ec3c 100644 --- a/Cavern/Filters/Comb.cs +++ b/Cavern/Filters/Comb.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; namespace Cavern.Filters { /// @@ -9,11 +10,13 @@ public class Comb : Filter { /// /// Wet mix multiplier. /// + [DisplayName("Alpha (ratio)")] public double Alpha { get; set; } /// /// Delay in samples. /// + [DisplayName("K (samples)")] public int K { get => delay.DelaySamples; set => delay.DelaySamples = value; @@ -22,6 +25,7 @@ public int K { /// /// First minimum point. /// + [DisplayName("Frequency (Hz)")] public double Frequency { get => sampleRate * .5 / K; set => K = (int)(.5 / (value / sampleRate) + 1); @@ -66,9 +70,7 @@ public Comb(int sampleRate, double frequency, double alpha) { delay = new Delay((int)(.5 / (frequency / sampleRate) + 1)); } - /// - /// Apply comb on an array of samples. One filter should be applied to only one continuous stream of samples. - /// + /// public override void Process(float[] samples) { if (cache.Length != samples.Length) { cache = new float[samples.Length]; @@ -77,7 +79,7 @@ public override void Process(float[] samples) { delay.Process(cache); float alpha = (float)Alpha, divisor = 1 / (1 + alpha); - for (int sample = 0; sample < samples.Length; ++sample) { + for (int sample = 0; sample < samples.Length; sample++) { samples[sample] = (samples[sample] + cache[sample] * alpha) * divisor; } } diff --git a/Cavern/Filters/Crossover.cs b/Cavern/Filters/Crossover.cs index 3360ca88..ce6dc512 100644 --- a/Cavern/Filters/Crossover.cs +++ b/Cavern/Filters/Crossover.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; namespace Cavern.Filters { /// @@ -13,6 +14,7 @@ public class Crossover : Filter { /// /// Crossover frequency. /// + [DisplayName("Frequency (Hz)")] public double Frequency { get => lowpasses[0].CenterFreq; set { @@ -27,6 +29,7 @@ public double Frequency { /// /// A value of 2 is recommended for notch prevention when mixing /// and back together. + [DisplayName("Order")] public int Order { get => lowpasses.Length; set => RecreateFilters(lowpasses[0].CenterFreq, value); diff --git a/Cavern/Filters/DebugCrossover.cs b/Cavern/Filters/DebugCrossover.cs index 453b5f98..75390dca 100644 --- a/Cavern/Filters/DebugCrossover.cs +++ b/Cavern/Filters/DebugCrossover.cs @@ -18,9 +18,7 @@ public DebugCrossover(int sampleRate, double frequency) : base(sampleRate, frequ /// Number of filters per pass, 2 is recommended for mixing notch prevention public DebugCrossover(int sampleRate, double frequency, int order) : base(sampleRate, frequency, order) { } - /// - /// Apply crossover on an array of samples. One filter should be applied to only one continuous stream of samples. - /// + /// public override void Process(float[] samples) { base.Process(samples); for (int i = 0; i < samples.Length; ++i) { @@ -28,12 +26,7 @@ public override void Process(float[] samples) { } } - /// - /// Apply crossover on an array of samples. One filter should be applied to only one continuous stream of samples. - /// - /// Input samples - /// Channel to filter - /// Total channels + /// public override void Process(float[] samples, int channel, int channels) { base.Process(samples, channel, channels); for (int sample = channel; sample < samples.Length; sample += channels) { diff --git a/Cavern/Filters/Delay.cs b/Cavern/Filters/Delay.cs index c6495c70..839227cb 100644 --- a/Cavern/Filters/Delay.cs +++ b/Cavern/Filters/Delay.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.Runtime.CompilerServices; @@ -13,15 +14,46 @@ public class Delay : Filter, IEqualizerAPOFilter { /// /// Delay in samples. /// + [DisplayName("Delay (samples)")] public int DelaySamples { get => cache[0].Length; set { if (cache[0].Length != value) { RecreateCaches(value); + delayMs = double.NaN; } } } + /// + /// Delay in milliseconds. + /// + [DisplayName("Delay (ms)")] + public double DelayMs { + get { + if (!double.IsNaN(delayMs)) { + return delayMs; + } + if (sampleRate == 0) { + throw new SampleRateNotSetException(); + } + return DelaySamples / (double)sampleRate * 1000; + } + + set { + if (sampleRate == 0) { + throw new SampleRateNotSetException(); + } + DelaySamples = (int)Math.Round(value * sampleRate * .001); + delayMs = value; + } + } + + /// + /// When the filter was created with a precise delay that is not a round value in samples, display this instead. + /// + double delayMs; + /// /// Cached samples for the next block. Alternates between two arrays to prevent memory allocation. /// @@ -45,14 +77,18 @@ void RecreateCaches(int size) { /// /// Create a delay for a given length in samples. /// - public Delay(int samples) => RecreateCaches(samples); + public Delay(int samples) { + delayMs = double.NaN; + RecreateCaches(samples); + } /// /// Create a delay for a given length in seconds. /// public Delay(double time, int sampleRate) { this.sampleRate = sampleRate; - RecreateCaches((int)(time * sampleRate + .5f)); + delayMs = time; + RecreateCaches((int)(time * sampleRate * .001 + .5)); } /// @@ -110,7 +146,7 @@ public override string ToString() { if (sampleRate == 0) { return $"Delay: {DelaySamples} samples"; } else { - string delay = ((double)DelaySamples / sampleRate).ToString(CultureInfo.InvariantCulture); + string delay = DelayMs.ToString(CultureInfo.InvariantCulture); return $"Delay: {delay} ms"; } } diff --git a/Cavern/Filters/Filter.cs b/Cavern/Filters/Filter.cs index 64323f15..beac83e6 100644 --- a/Cavern/Filters/Filter.cs +++ b/Cavern/Filters/Filter.cs @@ -11,6 +11,7 @@ public abstract class Filter { /// /// Apply this filter on an array of samples. One filter should be applied to only one continuous stream of samples. /// + /// Input samples public virtual void Process(float[] samples) => Process(samples, 0, 1); /// diff --git a/Cavern/Filters/Gain.cs b/Cavern/Filters/Gain.cs index c122d103..076d378c 100644 --- a/Cavern/Filters/Gain.cs +++ b/Cavern/Filters/Gain.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using Cavern.Filters.Interfaces; @@ -12,6 +13,7 @@ public class Gain : Filter, IEqualizerAPOFilter { /// /// Filter gain in decibels. /// + [DisplayName("Gain (dB)")] public double GainValue { get => 20 * Math.Log10(gainValue); set => gainValue = (float)Math.Pow(10, value * .05); @@ -28,21 +30,14 @@ public double GainValue { /// Filter gain in decibels public Gain(double gain) => GainValue = gain; - /// - /// Apply gain on an array of samples. This filter can be used on multiple streams. - /// + /// public override void Process(float[] samples) { - for (int sample = 0; sample < samples.Length; ++sample) { + for (int sample = 0; sample < samples.Length; sample++) { samples[sample] *= gainValue; } } - /// - /// Apply gain on an array of samples. This filter can be used on multiple streams. - /// - /// Input samples - /// Channel to filter - /// Total channels + /// public override void Process(float[] samples, int channel, int channels) { for (int sample = channel; sample < samples.Length; sample += channels) { samples[sample] *= gainValue; @@ -50,10 +45,7 @@ public override void Process(float[] samples, int channel, int channels) { } /// - public override string ToString() { - double rounded = (int)(GainValue * 100 + .5) * .01; - return $"Gain: {rounded.ToString(CultureInfo.InvariantCulture)} dB"; - } + public override string ToString() => $"Gain: {GainValue.ToString("0.00", CultureInfo.InvariantCulture)} dB"; /// public void ExportToEqualizerAPO(List wipConfig) => diff --git a/Cavern/Filters/PhaseSwappableBiquadFilter.cs b/Cavern/Filters/PhaseSwappableBiquadFilter.cs index 1a19a2d5..64269661 100644 --- a/Cavern/Filters/PhaseSwappableBiquadFilter.cs +++ b/Cavern/Filters/PhaseSwappableBiquadFilter.cs @@ -1,4 +1,6 @@ -namespace Cavern.Filters { +using System.ComponentModel; + +namespace Cavern.Filters { /// /// Simple first-order biquad filter with the option to invert the phase response. /// @@ -6,6 +8,7 @@ public abstract class PhaseSwappableBiquadFilter : BiquadFilter { /// /// Biquad filters usually achieve their effect by delaying lower frequencies. Phase swapping delays higher frequencies. /// + [DisplayName("Phase-swapped")] public bool PhaseSwapped { get => phaseSwapped; set { diff --git a/Cavern/Filters/SpikeConvolver.cs b/Cavern/Filters/SpikeConvolver.cs index adcfd3ee..c4005519 100644 --- a/Cavern/Filters/SpikeConvolver.cs +++ b/Cavern/Filters/SpikeConvolver.cs @@ -44,9 +44,7 @@ public static float[] SpikeConvolve(float[] impulse, float[] samples, int delay) return convolved; } - /// - /// Apply convolution on an array of samples. One filter should be applied to only one continuous stream of samples. - /// + /// public override void Process(float[] samples) { float[] convolved; if (delay == 0) { diff --git a/Cavern/Filters/_Exceptions.cs b/Cavern/Filters/_Exceptions.cs new file mode 100644 index 00000000..011733ef --- /dev/null +++ b/Cavern/Filters/_Exceptions.cs @@ -0,0 +1,15 @@ +using System; + +namespace Cavern.Filters { + /// + /// Tells if a property can only be used when the filter was created with a set sample rate. + /// + public class SampleRateNotSetException : Exception { + const string message = "This property can only be used when the filter was created with a set sample rate."; + + /// + /// Tells if a property can only be used when the filter was created with a set sample rate. + /// + public SampleRateNotSetException() : base(message) { } + } +} \ No newline at end of file diff --git a/CavernSamples/FilterStudio/Consts/_Exceptions.cs b/CavernSamples/FilterStudio/Consts/_Exceptions.cs new file mode 100644 index 00000000..c9275d17 --- /dev/null +++ b/CavernSamples/FilterStudio/Consts/_Exceptions.cs @@ -0,0 +1,15 @@ +using System; + +namespace FilterStudio { + /// + /// Tells if value can't be edited for a filter, because the type is not currently supported for parsing. + /// + public class UneditableTypeException : Exception { + const string message = "This value can't be edited, because the type is not currently supported for parsing."; + + /// + /// Tells if value can't be edited for a filter, because the type is not currently supported for parsing. + /// + public UneditableTypeException() : base(message) { } + } +} \ No newline at end of file diff --git a/CavernSamples/FilterStudio/MainWindow.xaml b/CavernSamples/FilterStudio/MainWindow.xaml index dfea4751..10944311 100644 --- a/CavernSamples/FilterStudio/MainWindow.xaml +++ b/CavernSamples/FilterStudio/MainWindow.xaml @@ -26,7 +26,7 @@ - + diff --git a/CavernSamples/FilterStudio/MainWindow.xaml.cs b/CavernSamples/FilterStudio/MainWindow.xaml.cs index 6fdeb274..f3311212 100644 --- a/CavernSamples/FilterStudio/MainWindow.xaml.cs +++ b/CavernSamples/FilterStudio/MainWindow.xaml.cs @@ -1,12 +1,15 @@ using Microsoft.Win32; +using System; using System.Linq; using System.Windows; using System.Windows.Input; using System.Windows.Media; using Cavern; +using Cavern.Filters; using Cavern.Filters.Utilities; using Cavern.Format.ConfigurationFile; +using VoidX.WPF; using FilterStudio.Graphs; @@ -47,11 +50,14 @@ public MainWindow() { /// void OnNodeSelected() { StyledNode node = SelectedFilter; - if (node == null) { + if (node == null || node.Filter == null) { selectedNode.Text = (string)language["NNode"]; + properties.ItemsSource = Array.Empty(); return; } + selectedNode.Text = node.LabelText; + properties.ItemsSource = new ObjectToDataGrid(node.Filter.Filter, FilterPropertyChanged, e => Error(e.Message)); } /// @@ -62,6 +68,22 @@ void ReloadGraph() { OnNodeSelected(); } + /// + /// Update the name of a filter when any property of it was modified. + /// + void FilterPropertyChanged() { + StyledNode node = SelectedFilter; + Filter modified = node?.Filter?.Filter; + if (modified != null) { + string newDisplayName = modified.ToString(); + if (node.LabelText != newDisplayName) { + node.LabelText = newDisplayName; + selectedNode.Text = node.LabelText; + ReloadGraph(); + } + } + } + /// /// Open a configuration file of known formats. /// diff --git a/CavernSamples/FilterStudio/VoidX.WPF/ObjectToDataGrid.cs b/CavernSamples/FilterStudio/VoidX.WPF/ObjectToDataGrid.cs new file mode 100644 index 00000000..8fd9fcc2 --- /dev/null +++ b/CavernSamples/FilterStudio/VoidX.WPF/ObjectToDataGrid.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Reflection; + +using FilterStudio; + +namespace VoidX.WPF { + /// + /// Displays a as a row of a DataGrid. + /// + /// The object to edit this of + /// The property of the to edit + /// Call this function when the was changed + /// Call this function when the wasn't changed + public class PropertyDisplay(object source, PropertyInfo property, Action successCallback, Action failureCallback) { + /// + /// Name of the property, or the display name if a is present. + /// + public string Property { get; } = property.GetCustomAttribute()?.DisplayName ?? property.Name; + + /// + /// Current value of the property as a string. + /// + public string Value { + get => value; + set { + this.value = value; + try { + if (typeParsers.TryGetValue(property.PropertyType, out var parser)) { + property.SetValue(source, parser(value)); + } else { + throw new UneditableTypeException(); + } + successCallback(); + } catch (Exception e) { + failureCallback(e); + } + } + } + string value = property.GetValue(source, null)?.ToString() ?? "null"; + + /// + /// Quick access to the parsers of supported types. + /// + static readonly Dictionary> typeParsers = new() { + { typeof(bool), value => bool.Parse(value) }, + { typeof(byte), value => byte.Parse(value) }, + { typeof(char), value => char.Parse(value) }, + { typeof(DateTime), value => DateTime.Parse(value) }, + { typeof(decimal), value => decimal.Parse(value.Replace(',', '.'), CultureInfo.InvariantCulture) }, + { typeof(double), value => double.Parse(value.Replace(',', '.'), CultureInfo.InvariantCulture) }, + { typeof(float), value => float.Parse(value.Replace(',', '.'), CultureInfo.InvariantCulture) }, + { typeof(int), value => int.Parse(value) }, + { typeof(long), value => long.Parse(value) }, + { typeof(sbyte), value => sbyte.Parse(value) }, + { typeof(short), value => short.Parse(value) }, + { typeof(string), value => value }, + { typeof(uint), value => uint.Parse(value) }, + { typeof(ulong), value => ulong.Parse(value) }, + { typeof(ushort), value => ushort.Parse(value) } + }; + } + + /// + /// Makes the properties of classes available for editing as key-value string pairs. + /// + public class ObjectToDataGrid : List { + /// + /// Makes the properties of classes available for editing as key-value string pairs. + /// + /// The object to edit + /// Call this function when a property was changed + /// Call this function when a property wasn't changed + public ObjectToDataGrid(object source, Action successCallback, Action failureCallback) { + PropertyInfo[] properties = source.GetType().GetProperties(); + for (int i = 0; i < properties.Length; i++) { + PropertyInfo property = properties[i]; + if (property.SetMethod != null && property.SetMethod.IsPublic) { + Add(new PropertyDisplay(source, property, successCallback, failureCallback)); + } + } + } + } +} \ No newline at end of file diff --git a/CavernSamples/_Common/Styles.xaml b/CavernSamples/_Common/Styles.xaml index 57663033..01371a4b 100644 --- a/CavernSamples/_Common/Styles.xaml +++ b/CavernSamples/_Common/Styles.xaml @@ -11,6 +11,25 @@ + + + +