Skip to content

Commit

Permalink
Editability extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
VoidXH committed Jul 2, 2024
1 parent 1496046 commit 8afdf95
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 30 deletions.
22 changes: 17 additions & 5 deletions Cavern.QuickEQ/Filters/GraphicEQ.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,36 @@ public class GraphicEQ : FastConvolver {
/// Copy of the equalizer curve for further alignment.
/// </summary>
/// <remarks>Changing the bands on this <see cref="Equalizer"/> does not result in the recomputation of the convolution filter,
/// please recreate the filter instead.</remarks>
public Equalizer Equalizer { get; }
/// please use the setter instead.</remarks>
public Equalizer Equalizer {
get => equalizer;
set {
Impulse = value.GetConvolution(sampleRate, Length);
equalizer = value;
}
}
Equalizer equalizer;

/// <summary>
/// Sample rate at which this EQ is converted to a minimum-phase FIR filter.
/// </summary>
readonly int sampleRate;

/// <summary>
/// Convert an <paramref name="equalizer"/> to a 65536-sample convolution filter.
/// </summary>
/// <param name="equalizer">Desired frequency response change</param>
/// <param name="sampleRate">Sample rate of the filter</param>
/// <param name="sampleRate">Sample rate at which this EQ is converted to a minimum-phase FIR filter</param>
public GraphicEQ(Equalizer equalizer, int sampleRate) : this(equalizer, sampleRate, 65536) { }

/// <summary>
/// Convert an <paramref name="equalizer"/> to a convolution filter.
/// </summary>
/// <param name="equalizer">Desired frequency response change</param>
/// <param name="sampleRate">Sample rate of the filter</param>
/// <param name="sampleRate">Sample rate at which this EQ is converted to a minimum-phase FIR filter</param>
/// <param name="filterLength">Number of samples in the generated convolution filter, must be a power of 2</param>
public GraphicEQ(Equalizer equalizer, int sampleRate, int filterLength) :
base(equalizer.GetConvolution(sampleRate, filterLength)) => Equalizer = equalizer;
base(equalizer.GetConvolution(sampleRate, filterLength)) => this.equalizer = equalizer;

/// <summary>
/// Parse a Graphic EQ line of Equalizer APO to a Cavern <see cref="GraphicEQ"/> filter.
Expand Down
56 changes: 35 additions & 21 deletions Cavern/Filters/FastConvolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,25 @@ public partial class FastConvolver : Filter, IDisposable, ILocalizableToString {
/// <summary>
/// Get a clone of the <see cref="filter"/>'s impulse response.
/// </summary>
public float[] Impulse => Measurements.GetRealPartHalf(filter.IFFT(cache)); // The constructor doubles the length
public float[] Impulse {
get => Measurements.GetRealPartHalf(filter.IFFT(cache)); // The setter doubles the length
protected set {
Dispose();
if (CavernAmp.Available && CavernAmp.IsMono()) { // CavernAmp only improves performance when the runtime has no SIMD
native = CavernAmp.FastConvolver_Create(value, delay);
return;
}
int fftSize = 2 << QMath.Log2Ceil(value.Length); // Zero padding for the falloff to have space
cache = CreateCache(fftSize);
filter = new Complex[fftSize];
for (int sample = 0; sample < value.Length; sample++) {
filter[sample].Real = value[sample];
}
filter.InPlaceFFT(cache);
present = new Complex[fftSize];
Delay = delay;
}
}

/// <summary>
/// Number of samples in the impulse response.
Expand All @@ -25,37 +43,43 @@ public partial class FastConvolver : Filter, IDisposable, ILocalizableToString {
/// <summary>
/// Added filter delay to the impulse, in samples.
/// </summary>
public int Delay => delay;
public int Delay {
get => delay;
set {
future = new float[Length + value];
delay = value;
}
}

/// <summary>
/// CavernAmp instance of a FastConvolver.
/// </summary>
readonly IntPtr native;
IntPtr native;

/// <summary>
/// Created convolution filter in Fourier-space.
/// </summary>
readonly Complex[] filter;
Complex[] filter;

/// <summary>
/// Cache to perform the FFT in.
/// </summary>
readonly Complex[] present;
Complex[] present;

/// <summary>
/// Overlap samples from previous runs.
/// </summary>
readonly float[] future;
float[] future;

/// <summary>
/// FFT optimization.
/// </summary>
readonly FFTCache cache;
FFTCache cache;

/// <summary>
/// Added filter delay to the impulse, in samples.
/// </summary>
readonly int delay;
int delay;

/// <summary>
/// Constructs an optimized convolution with no delay.
Expand All @@ -69,20 +93,8 @@ public FastConvolver(float[] impulse) : this(impulse, 0) { }
/// <param name="impulse">Impulse response of the desired filter</param>
/// <param name="delay">Added filter delay to the impulse, in samples</param>
public FastConvolver(float[] impulse, int delay) {
if (CavernAmp.Available && CavernAmp.IsMono()) { // CavernAmp only improves performance when the runtime has no SIMD
native = CavernAmp.FastConvolver_Create(impulse, delay);
return;
}
int fftSize = 2 << QMath.Log2Ceil(impulse.Length); // Zero padding for the falloff to have space
cache = CreateCache(fftSize);
filter = new Complex[fftSize];
for (int sample = 0; sample < impulse.Length; sample++) {
filter[sample].Real = impulse[sample];
}
filter.InPlaceFFT(cache);
present = new Complex[fftSize];
future = new float[fftSize + delay];
this.delay = delay;
Impulse = impulse;
}

/// <summary>
Expand Down Expand Up @@ -131,6 +143,8 @@ public override void Process(float[] samples, int channel, int channels) {
public void Dispose() {
if (native != IntPtr.Zero) {
CavernAmp.FastConvolver_Dispose(native);
} else {
cache?.Dispose();
}
}

Expand Down
17 changes: 16 additions & 1 deletion CavernSamples/FilterStudio/MainWindow.Graph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

using Cavern.Filters;
using Cavern.Filters.Utilities;
using Cavern.QuickEQ.Equalization;
using Cavern.Utilities;
using VoidX.WPF;

Expand Down Expand Up @@ -122,7 +123,13 @@ void GraphLeftClick(object _) {
}

selectedNode.Text = node.LabelText;
properties.ItemsSource = new ObjectToDataGrid(node.Filter.Filter, FilterPropertyChanged, e => Error(e.Message));
if (properties.ItemsSource is ObjectToDataGrid old) {
properties.BeginningEdit -= old.BeginningEdit;
}
ObjectToDataGrid propertySource = new ObjectToDataGrid(node.Filter.Filter, FilterPropertyChanged, e => Error(e.Message),
(typeof(Equalizer), EditEqualizer));
properties.ItemsSource = propertySource;
properties.BeginningEdit += propertySource.BeginningEdit;
}

/// <summary>
Expand Down Expand Up @@ -188,5 +195,13 @@ void ReloadGraph() {
graph.Graph = newGraph;
}
}

/// <summary>
/// Open the editor for an existing <see cref="Equalizer"/>.
/// </summary>
void EditEqualizer(object filter) {
Equalizer equalizer = (Equalizer)filter;
// TODO: editor
}
}
}
68 changes: 65 additions & 3 deletions CavernSamples/VoidX.WPF/ObjectToDataGrid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Windows.Controls;

using FilterStudio;

Expand All @@ -20,6 +22,16 @@ public class PropertyDisplay(object source, PropertyInfo property, Action succes
/// </summary>
public string Property { get; } = property.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName ?? property.Name;

/// <summary>
/// The object to edit this <see cref="property"/> of.
/// </summary>
protected readonly object source = source;

/// <summary>
/// The property of the <see cref="source"/> to edit.
/// </summary>
protected readonly PropertyInfo property = property;

/// <summary>
/// Current value of the property as a string.
/// </summary>
Expand All @@ -39,7 +51,7 @@ public string Value {
}
}
}
string value = property.GetValue(source, null)?.ToString() ?? "null";
protected string value = property.GetValue(source, null)?.ToString() ?? "null";

/// <summary>
/// Quick access to the parsers of supported types.
Expand All @@ -63,6 +75,36 @@ public string Value {
};
}

/// <summary>
/// An editable property that uses an external handler or dialog to update the <paramref name="property"/>
/// of the <paramref name="source"/>.
/// </summary>
public class ExternallyEditablePropertyDisplay : PropertyDisplay {
/// <summary>
/// Passes the value of the property in the source object for editing externally.
/// </summary>
readonly Action<object> editCallback;

/// <summary>
/// An editable property that uses an external handler or dialog to update the <paramref name="property"/>
/// of the <paramref name="source"/>.
/// </summary>
/// <param name="source">The object to edit this <paramref name="property"/> of</param>
/// <param name="property">The property of the <paramref name="source"/> to edit</param>
/// <param name="editCallback">Passes the value of the <paramref name="property"/> in the <paramref name="source"/> object
/// for editing externally</param>
public ExternallyEditablePropertyDisplay(object source, PropertyInfo property, Action<object> editCallback) :
base(source, property, null, null) {
this.editCallback = editCallback;
value = "...";
}

/// <summary>
/// Call the <see cref="editCallback"/> with the property value to edit.
/// </summary>
public void Edit() => editCallback(property.GetValue(source));
}

/// <summary>
/// Makes the properties of classes available for editing as key-value string pairs.
/// </summary>
Expand All @@ -73,14 +115,34 @@ public class ObjectToDataGrid : List<PropertyDisplay> {
/// <param name="source">The object to edit</param>
/// <param name="successCallback">Call this function when a property was changed</param>
/// <param name="failureCallback">Call this function when a property wasn't changed</param>
public ObjectToDataGrid(object source, Action successCallback, Action<Exception> failureCallback) {
/// <param name="customFields">For these types, ... will be displayed for value, and an action calls back with the value
/// for modification</param>
public ObjectToDataGrid(object source, Action successCallback, Action<Exception> failureCallback,
params(Type type, Action<object> editor)[] customFields) {
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));
(Type type, Action<object> editor) = customFields.FirstOrDefault(x => x.type == property.PropertyType);
if (editor == null) {
Add(new PropertyDisplay(source, property, successCallback, failureCallback));
} else {
Add(new ExternallyEditablePropertyDisplay(source, property, editor));
}
}
}
}

/// <summary>
/// Attach this function to the <see cref="DataGrid"/>'s corresponding event to support
/// <see cref="ExternallyEditablePropertyDisplay"/>s.
/// </summary>
public void BeginningEdit(object _, DataGridBeginningEditEventArgs e) {
if (this[e.Row.GetIndex()] is ExternallyEditablePropertyDisplay editable) {
editable.Edit();
e.EditingEventArgs.Handled = true;
e.Cancel = true;
}
}
}
}

0 comments on commit 8afdf95

Please sign in to comment.