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