Skip to content

Commit

Permalink
Convolution editor
Browse files Browse the repository at this point in the history
  • Loading branch information
VoidXH committed Jul 10, 2024
1 parent d71f470 commit a99a272
Show file tree
Hide file tree
Showing 23 changed files with 395 additions and 53 deletions.
5 changes: 4 additions & 1 deletion Cavern.QuickEQ/Filters/GraphicEQ.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ public GraphicEQ(Equalizer equalizer, int sampleRate) : this(equalizer, sampleRa
/// <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)) => this.equalizer = equalizer;
base(equalizer.GetConvolution(sampleRate, filterLength)) {
this.equalizer = equalizer;
this.sampleRate = sampleRate;
}

/// <summary>
/// Parse a Graphic EQ line of Equalizer APO to a Cavern <see cref="GraphicEQ"/> filter.
Expand Down
2 changes: 1 addition & 1 deletion Cavern.QuickEQ/VerboseImpulseResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public float[] Response {
if (response != null) {
return response;
}
return response = Measurements.GetRealPart(ComplexResponse);
return response = Measurements.GetRealPart(ComplexResponse.IFFT());
}
}
float[] response;
Expand Down
7 changes: 1 addition & 6 deletions Cavern.WPF/Cavern.WPF.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,11 @@
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Controls\**" />
<EmbeddedResource Remove="Controls\**" />
<None Remove="Controls\**" />
<Page Remove="Controls\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Cavern.Format\Cavern.Format.csproj" />
<ProjectReference Include="..\Cavern\Cavern.csproj" />
<ProjectReference Include="..\Cavern.QuickEQ\Cavern.QuickEQ.csproj" />
</ItemGroup>
Expand Down
18 changes: 18 additions & 0 deletions Cavern.WPF/Cavern.WPF.csproj.user
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
<Compile Update="ChannelSelector.xaml.cs">
<SubType>Code</SubType>
</Compile>
<Compile Update="Controls\GraphRendererControl.xaml.cs">
<SubType>Code</SubType>
</Compile>
<Compile Update="ConvolutionEditor.xaml.cs">
<SubType>Code</SubType>
</Compile>
<Compile Update="EQEditor.xaml.cs">
<SubType>Code</SubType>
</Compile>
Expand All @@ -19,6 +25,12 @@
<Page Update="ChannelSelector.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Controls\GraphRendererControl.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="ConvolutionEditor.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="EQEditor.xaml">
<SubType>Designer</SubType>
</Page>
Expand All @@ -43,6 +55,12 @@
<Page Update="Resources\CommonStrings.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Resources\ConvolutionEditorStrings.hu-HU.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Resources\ConvolutionEditorStrings.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Resources\CrossoverStrings.hu-HU.xaml">
<SubType>Designer</SubType>
</Page>
Expand Down
16 changes: 16 additions & 0 deletions Cavern.WPF/Consts/Language.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ public static class Language {
/// </summary>
public static ResourceDictionary GetChannelSelectorStrings() => channelSelectorCache ??= GetFor("ChannelSelectorStrings");

/// <summary>
/// Get the <see cref="ConvolutionEditor"/>'s translation.
/// </summary>
public static ResourceDictionary GetConvolutionEditorStrings() => convolutionEditorCache ??= GetFor("ConvolutionEditorStrings");

/// <summary>
/// Get the translations related to crossover handling.
/// </summary>
Expand All @@ -34,6 +39,12 @@ public static class Language {
/// </summary>
public static ResourceDictionary GetEQEditorStrings() => eqEditorCache ??= GetFor("EQEditorStrings");

/// <summary>
/// Show an error <paramref name="message"/> with the title in the user's language.
/// </summary>
public static void Error(string message) =>
MessageBox.Show(message, (string)GetCommonStrings()["TErro"], MessageBoxButton.OK, MessageBoxImage.Error);

/// <summary>
/// Return a channel's name in the user's language or fall back to its short name.
/// </summary>
Expand Down Expand Up @@ -111,6 +122,11 @@ static ResourceDictionary GetFor(string resource) {
/// </summary>
static ResourceDictionary channelSelectorCache;

/// <summary>
/// The loaded translation of the <see cref="ConvolutionEditor"/> for reuse.
/// </summary>
static ResourceDictionary convolutionEditorCache;

/// <summary>
/// The loaded translation of crossover handling for reuse.
/// </summary>
Expand Down
9 changes: 9 additions & 0 deletions Cavern.WPF/Controls/GraphRendererControl.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<UserControl x:Class="Cavern.WPF.Controls.GraphRendererControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignWidth="800" d:DesignHeight="450">
<Image Name="image"/>
</UserControl>
92 changes: 92 additions & 0 deletions Cavern.WPF/Controls/GraphRendererControl.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System.Windows;
using System.Windows.Controls;

using Cavern.QuickEQ.Equalization;
using Cavern.QuickEQ.Graphing;
using Cavern.QuickEQ.Graphing.Overlays;
using Cavern.WPF.Utils;

namespace Cavern.WPF.Controls {
/// <summary>
/// Displays one or more <see cref="Equalizer"/> filters.
/// </summary>
public partial class GraphRendererControl : UserControl {
/// <summary>
/// The background of the displayed graph.
/// </summary>
public GraphOverlay Overlay { get; set; } = new LogScaleGrid(2, 1, 0xFF000000, 10);

/// <summary>
/// All displayed curves referencing the current <see cref="renderer"/>.
/// </summary>
readonly List<RenderedCurve> curves = [];

/// <summary>
/// Cavern's internal graph rendering engine.
/// </summary>
GraphRenderer renderer = new GraphRenderer(1, 1); // Placeholder for initialization, initial invalidation updates it

/// <summary>
/// Displays one or more <see cref="Equalizer"/> filters.
/// </summary>
public GraphRendererControl() => InitializeComponent();

/// <summary>
/// Add a curve with an ARGB color.
/// </summary>
/// <returns>Index of the curve that can be used in <see cref="Invalidate(int)"/>.</returns>
public int AddCurve(Equalizer curve, uint color) {
curves.Add(renderer.AddCurve(curve, color));
Invalidate();
return curves.Count - 1;
}

/// <summary>
/// Remove all displayed curves.
/// </summary>
public void Clear() {
curves.Clear();
renderer.Clear();
Invalidate();
}

/// <summary>
/// When a curve at a given <paramref name="index"/> has changed, update its drawn curve.
/// </summary>
public void Invalidate(int index) {
curves[index].Update(true);
InvalidateImage();
}

/// <summary>
/// Update all data related to the graph and redraw.
/// </summary>
public void Invalidate() {
for (int i = 0, c = curves.Count - 1; i <= c; i++) {
curves[i].Update(i == c);
}
InvalidateImage();
}

/// <summary>
/// Update the displayed graph when a curve was added, changed, or removed.
/// </summary>
public void InvalidateImage() => image.Source = renderer.Pixels.ToBitmap(renderer.Width, renderer.Height).ToImageSource();

/// <summary>
/// Keep the graph's size at the control resolution.
/// </summary>
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) {
base.OnRenderSizeChanged(sizeInfo);
renderer = new((int)(sizeInfo.NewSize.Width + .5), (int)(sizeInfo.NewSize.Height + .5)) {
DynamicRange = 50,
Peak = 25,
Overlay = Overlay
};
for (int i = 0, c = curves.Count; i < c; i++) {
curves[i] = renderer.AddCurve(curves[i].Curve, curves[i].Color);
}
Invalidate();
}
}
}
41 changes: 41 additions & 0 deletions Cavern.WPF/ConvolutionEditor.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<base:OkCancelDialog x:Class="Cavern.WPF.ConvolutionEditor"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:base="clr-namespace:Cavern.WPF.BaseClasses"
xmlns:cavern="clr-namespace:Cavern.WPF.Controls"
mc:Ignorable="d"
Title="{StaticResource Title}" Width="600" Height="300">
<Window.Resources>
<d:ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/CommonStrings.xaml"/>
<ResourceDictionary Source="Resources/ConvolutionEditorStrings.xaml"/>
</ResourceDictionary.MergedDictionaries>
</d:ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="200"/>
</Grid.ColumnDefinitions>
<cavern:GraphRendererControl x:Name="impulseDisplay"/>
<cavern:GraphRendererControl Grid.Row="1" x:Name="fftDisplay"/>
<Grid Grid.Column="1" Grid.RowSpan="2">
<Button Grid.Column="1" Margin="0,10,10,0" HorizontalAlignment="Right" VerticalAlignment="Top" Width="165" Height="20"
Content="{StaticResource BLoad}" Click="LoadFromFile"/>
<Button Grid.Column="1" Margin="0,0,95,10" HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="80" Height="20"
Content="{StaticResource BtnOk}" Click="OK"/>
<Button Grid.Column="1" Margin="0,0,10,10" HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="80" Height="20"
Content="{StaticResource BtnCa}" Click="Cancel"/>
<TextBlock x:Name="polarity" Margin="25,35,0,0" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<TextBlock x:Name="phaseDisplay" Margin="25,55,0,0" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<TextBlock x:Name="delay" Margin="25,75,0,0" HorizontalAlignment="Left" VerticalAlignment="Top"/>
</Grid>
</Grid>
</base:OkCancelDialog>
117 changes: 117 additions & 0 deletions Cavern.WPF/ConvolutionEditor.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using Microsoft.Win32;
using System.Windows;

using Cavern.QuickEQ;
using Cavern.QuickEQ.Equalization;
using Cavern.QuickEQ.Graphing.Overlays;
using Cavern.QuickEQ.Utilities;
using Cavern.Utilities;
using Cavern.WPF.BaseClasses;
using Cavern.Format;

namespace Cavern.WPF {
/// <summary>
/// Displays properties of a convolution's impulse response and allows loading a new set of samples.
/// </summary>
public partial class ConvolutionEditor : OkCancelDialog {
/// <summary>
/// Last displayed/loaded impulse response of a convolution filter.
/// </summary>
public float[] Impulse {
get => impulse;
set {
impulse = value;
Reset();
}
}
float[] impulse;

/// <summary>
/// The initial value of <see cref="impulse"/> as received in the constructor. When the editing is cancelled,
/// or no new convolution samples are loaded, <see cref="Impulse"/> will return its original reference.
/// </summary>
readonly float[] originalImpulse;

/// <summary>
/// Sample rate of the <see cref="impulse"/>.
/// </summary>
readonly int sampleRate;

/// <summary>
/// Source of language strings.
/// </summary>
readonly ResourceDictionary language = Consts.Language.GetConvolutionEditorStrings();

/// <summary>
/// Source of common language strings.
/// </summary>
readonly ResourceDictionary common = Consts.Language.GetCommonStrings();

/// <summary>
/// Displays properties of a convolution's impulse response and allows loading a new set of samples.
/// </summary>
public ConvolutionEditor(float[] impulse, int sampleRate) {
Resources.MergedDictionaries.Add(language);
Resources.MergedDictionaries.Add(common);
InitializeComponent();
impulseDisplay.Overlay = new Grid(2, 1, 0xFFAAAAAA, 10, 10);
fftDisplay.Overlay = new LogScaleGrid(2, 1, 0xFFAAAAAA, 10);
this.sampleRate = sampleRate;
Impulse = impulse;
originalImpulse = impulse;
}

/// <inheritdoc/>
protected override void Cancel(object _, RoutedEventArgs e) {
impulse = originalImpulse;
base.Cancel(_, e);
}

/// <summary>
/// Overwrite the convolution from an impulse response from a file, only allowing the system sample rate.
/// </summary>
void LoadFromFile(object _, RoutedEventArgs e) {
OpenFileDialog dialog = new OpenFileDialog() {
Filter = string.Format((string)common["ImFmt"], AudioReader.filter)
};
if (dialog.ShowDialog().Value) {
AudioReader file = AudioReader.Open(dialog.FileName);
file.ReadHeader();
if (file.SampleRate != sampleRate) {
Consts.Language.Error(string.Format((string)language["ESRat"], file.SampleRate, sampleRate));
} else if (file.ChannelCount != 1) {
Consts.Language.Error((string)language["EMono"]);
} else {
Impulse = file.Read();
}
}
}

/// <summary>
/// Reanalyze the <see cref="impulse"/> and redraw the layout.
/// </summary>
void Reset() {
impulseDisplay.Clear();
fftDisplay.Clear();
if (impulse == null) {
polarity.Text = string.Empty;
phaseDisplay.Text = string.Empty;
delay.Text = string.Empty;
return;
}

impulseDisplay.AddCurve(EQGenerator.FromGraph(impulse, 20, 20000), 0xFF0000FF);
Complex[] fft = impulse.FFT();
fftDisplay.AddCurve(EQGenerator.FromTransferFunction(fft, sampleRate), 0xFF00FF00);
float[] phase = Measurements.GetPhase(fft);
float[] phaseGraph = GraphUtils.ConvertToGraph(phase, 20, 20000, sampleRate, 1024);
WaveformUtils.Gain(phaseGraph, 25 / MathF.PI);
fftDisplay.AddCurve(EQGenerator.FromGraph(phaseGraph, 20, 20000), 0xFFFF0000);

VerboseImpulseResponse verbose = new(fft);
polarity.Text = string.Format((string)language["VPola"], verbose.Polarity ? '+' : '-');
phaseDisplay.Text = string.Format((string)language["VPhas"], (180 / Math.PI * verbose.Phase).ToString("0.00"));
delay.Text = string.Format((string)language["VDela"], verbose.Delay);
}
}
}
Loading

0 comments on commit a99a272

Please sign in to comment.