Skip to content

Commit

Permalink
Configuration file project sample rate
Browse files Browse the repository at this point in the history
  • Loading branch information
VoidXH committed Jul 17, 2024
1 parent 6471536 commit d186099
Show file tree
Hide file tree
Showing 26 changed files with 363 additions and 77 deletions.
14 changes: 13 additions & 1 deletion Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,18 @@ public void ClearSplitPoint(int index) {
}
}

/// <summary>
/// Merge the filters of the split point at the given <paramref name="index"/> and the next one.
/// </summary>
public void MergeSplitPointWithNext(int index) {
FilterGraphNode[] roots = SplitPoints[index + 1].roots;
for (int j = 0; j < roots.Length; j++) {
roots[j].Parents[0].DetachFromGraph(); // Output of the split at the index
roots[j].DetachFromGraph(); // Input of the split at index + 1
}
splitPoints.RemoveAt(index + 1);
}

/// <summary>
/// Remove any splits, leave only one continuous graph of filters.
/// </summary>
Expand Down Expand Up @@ -270,7 +282,7 @@ protected void FinishEmpty(ReferenceChannel[] channels) {
channelIndex = GetChannelIndex(output.Channel);
} else if (source.Parents.Count == 0) { // Entry node
channelIndex = GetChannelIndex(((InputChannel)source.Filter).Channel);
} else { // TODO: greedily keep direct paths on the same channel index, don't have all filters on separate nodes
} else {
channelIndex = --lowestChannel;
}
result[i] = (source, channelIndex);
Expand Down
19 changes: 19 additions & 0 deletions Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFileType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Cavern.Format.ConfigurationFile {
/// <summary>
/// Supported configuration file formats.
/// </summary>
public enum ConfigurationFileType {
/// <summary>
/// Cavern Filter Studio configuration file.
/// </summary>
CavernFilterStudio,
/// <summary>
/// Convolution Box Format configuration file.
/// </summary>
ConvolutionBoxFormat,
/// <summary>
/// Equalizer APO configuration file.
/// </summary>
EqualizerAPO,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@ namespace Cavern.Format.ConfigurationFile {
/// A file format only supporting matrix mixing and convolution filters.
/// </summary>
public sealed class ConvolutionBoxFormatConfigurationFile : ConfigurationFile {
/// <summary>
/// CBFM marker.
/// </summary>
public static readonly int syncWord = 0x4D464243;

/// <inheritdoc/>
public override string FileExtension => "cbf";

/// <summary>
/// Sample rate of the convolution filters used.
/// </summary>
readonly int sampleRate;
public int SampleRate { get; private set; }

/// <summary>
/// Convert an<paramref name="other"/> configuration file to Convolution Box Format with the default convoltion length of
Expand All @@ -34,7 +39,7 @@ public ConvolutionBoxFormatConfigurationFile(ConfigurationFile other, int sample
/// <paramref name="convolutionLength"/>.
/// </summary>
public ConvolutionBoxFormatConfigurationFile(ConfigurationFile other, int sampleRate, int convolutionLength) : base(other) {
this.sampleRate = sampleRate;
SampleRate = sampleRate;
MergeSplitPoints();
Optimize();
SplitPoints[0].roots.ConvertToConvolution(convolutionLength);
Expand All @@ -44,7 +49,7 @@ public ConvolutionBoxFormatConfigurationFile(ConfigurationFile other, int sample
/// Create an empty file for a custom layout.
/// </summary>
public ConvolutionBoxFormatConfigurationFile(string name, int sampleRate, ReferenceChannel[] inputs) : base(name, inputs) {
this.sampleRate = sampleRate;
SampleRate = sampleRate;
FinishEmpty(inputs);
}

Expand All @@ -55,7 +60,7 @@ public ConvolutionBoxFormatConfigurationFile(string path) : base(Path.GetFileNam
// Dirty way of reading the sample rate after parsing
using FileStream stream = File.OpenRead(path);
stream.Position = 4;
sampleRate = stream.ReadInt32();
SampleRate = stream.ReadInt32();
Optimize();
}

Expand All @@ -65,7 +70,7 @@ public ConvolutionBoxFormatConfigurationFile(string path) : base(Path.GetFileNam
/// <remarks>Merge nodes can be created, calling <see cref="ConfigurationFile.Optimize"/> is recommended.</remarks>
static (string, FilterGraphNode)[] Parse(string path) {
using FileStream stream = File.OpenRead(path);
if (stream.ReadInt32() != magicNumber) {
if (stream.ReadInt32() != syncWord) {
throw new SyncException();
}
stream.Position = 8; // Sample rate is read in the constructor
Expand Down Expand Up @@ -120,7 +125,6 @@ FilterGraphNode GetChannel(int index) { // Get an actual channel's last node
inputChannels.Sort((a, b) => a.index.CompareTo(b.index));
foreach (KeyValuePair<int, FilterGraphNode> node in lastNodes) {
if (node.Key >= 0) {
// TODO: many points make input or output channels from channels, create them from names instead
OutputChannel outputFilter = new OutputChannel(((InputChannel)inputChannels[node.Key].root.Filter).ChannelName);
if (node.Value.Filter == null && node.Value.Children.Count == 0) { // Only overwrite dead ends with outputs
node.Value.Filter = outputFilter;
Expand Down Expand Up @@ -158,8 +162,8 @@ public override void Export(string path) {
}

using FileStream file = File.Open(path, FileMode.Create);
file.WriteAny(magicNumber);
file.WriteAny(sampleRate);
file.WriteAny(syncWord);
file.WriteAny(SampleRate);
int count = entries.Count;
file.WriteAny(count);
for (int i = 0; i < count; i++) {
Expand All @@ -168,7 +172,7 @@ public override void Export(string path) {
}

/// <summary>
/// Throw an <see cref="UnsupportedFilterException"/> if it's not a convolution or a merge point.
/// Throw an <see cref="UnsupportedFilterForExportException"/> if it's not a convolution or a merge point.
/// </summary>
void ValidateForExport() {
foreach (FilterGraphNode node in SplitPoints[0].roots.MapGraph()) {
Expand All @@ -177,10 +181,5 @@ void ValidateForExport() {
}
}
}

/// <summary>
/// CBFM marker.
/// </summary>
const int magicNumber = 0x4D464243;
}
}
19 changes: 17 additions & 2 deletions Cavern.QuickEQ.Format/ConfigurationFile/_Exceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,30 @@
using Cavern.Filters;

namespace Cavern.Format.ConfigurationFile {
/// <summary>
/// Thrown when an unsupported filter would be exported to a <see cref="ConfigurationFile"/>.
/// </summary>
public abstract class UnsupportedFilterForExportException : Exception {
/// <summary>
/// The filter not supported by Equalizer APO.
/// </summary>
public Filter Filter { get; }

/// <summary>
/// Thrown when an unsupported filter would be exported to a <see cref="ConfigurationFile"/>.
/// </summary>
public UnsupportedFilterForExportException(string message, Filter filter) : base(message) => Filter = filter;
}

/// <summary>
/// Thrown when an unsupported filter would be exported for Equalizer APO.
/// </summary>
public class NotEqualizerAPOFilterException : Exception {
public class NotEqualizerAPOFilterException : UnsupportedFilterForExportException {
const string message = "Equalizer APO does not support the following filter: ";

/// <summary>
/// Thrown when an unsupported filter would be exported for Equalizer APO.
/// </summary>
public NotEqualizerAPOFilterException(Filter filter) : base(message + filter) { }
public NotEqualizerAPOFilterException(Filter filter) : base(message + filter, filter) { }
}
}
2 changes: 1 addition & 1 deletion Cavern.QuickEQ/Utilities/GraphUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public static float[] ConvertToGraph(Complex[] response, double startFreq, doubl
/// for example the result of <see cref="Measurements.GetSpectrum(Complex[])"/>.</remarks>
public static float[] ConvertToGraph(float[] response, double startFreq, double endFreq, int sampleRate, int resultSize) {
float[] graph = new float[resultSize];
double step = Math.Pow(10, (Math.Log10(endFreq) - Math.Log10(startFreq)) / (graph.Length - 1)),
double step = Math.Pow(10, (Math.Log10(endFreq) - Math.Log10(startFreq)) / graph.Length),
positioner = response.Length * 2 / (double)sampleRate;
for (int i = 0; i < graph.Length; ++i) {
graph[i] = response[(int)(startFreq * positioner)];
Expand Down
7 changes: 4 additions & 3 deletions Cavern.WPF/ConvolutionEditor.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,14 @@ void Reset() {
return;
}

impulseDisplay.AddCurve(EQGenerator.FromGraph(impulse, 20, 20000), 0xFF0000FF);
int maxFreq = Math.Min(sampleRate >> 1, 20000);
impulseDisplay.AddCurve(EQGenerator.FromGraph(impulse, 20, maxFreq), 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);
float[] phaseGraph = GraphUtils.ConvertToGraph(phase, 20, maxFreq, sampleRate, 1024);
WaveformUtils.Gain(phaseGraph, 25 / MathF.PI);
fftDisplay.AddCurve(EQGenerator.FromGraph(phaseGraph, 20, 20000), 0xFFFF0000);
fftDisplay.AddCurve(EQGenerator.FromGraph(phaseGraph, 20, maxFreq), 0xFFFF0000);

VerboseImpulseResponse verbose = new(fft);
polarity.Text = string.Format((string)language["VPola"], verbose.Polarity ? '+' : '-');
Expand Down
17 changes: 12 additions & 5 deletions Cavern/Filters/BiquadFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;

using Cavern.Filters.Interfaces;
using Cavern.Filters.Utilities;
Expand All @@ -12,11 +13,17 @@ namespace Cavern.Filters {
/// <summary>
/// Simple first-order biquad filter.
/// </summary>
public abstract class BiquadFilter : Filter, IEqualizerAPOFilter, ILocalizableToString {
/// <summary>
/// Sample rate of the filter.
/// </summary>
public int SampleRate { get; protected set; }
public abstract class BiquadFilter : Filter, IEqualizerAPOFilter, ISampleRateDependentFilter, ILocalizableToString {
/// <inheritdoc/>
[IgnoreDataMember]
public int SampleRate {
get => sampleRate;
set {
sampleRate = value;
Reset(value, q, gain);
}
}
int sampleRate;

/// <summary>
/// Center frequency (-3 dB point) of the filter.
Expand Down
15 changes: 10 additions & 5 deletions Cavern/Filters/Cavernize.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
using System;
using System.ComponentModel;
using System.Runtime.Serialization;

using Cavern.Filters.Interfaces;

namespace Cavern.Filters {
/// <summary>
/// Separates ground and height data for a channel of a regular surround mix.
/// </summary>
public class Cavernize : Filter {
public class Cavernize : Filter, ISampleRateDependentFilter {
/// <inheritdoc/>
public override bool LinearTimeInvariant => false;

/// <summary>
/// Sample rate of the system this filter is used on.
/// </summary>
public int SampleRate => crossover.SampleRate;
/// <inheritdoc/>
[IgnoreDataMember]
public int SampleRate {
get => crossover.SampleRate;
set => crossover.SampleRate = value;
}

/// <summary>
/// Height separation effect strength.
Expand Down
24 changes: 17 additions & 7 deletions Cavern/Filters/Crossover.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
using System;
using System.ComponentModel;
using System.Runtime.Serialization;

using Cavern.Filters.Interfaces;

namespace Cavern.Filters {
/// <summary>
/// Simple variable-order crossover.
/// </summary>
public class Crossover : Filter {
/// <summary>
/// Cached filter sample rate.
/// </summary>
public int SampleRate { get; }
public class Crossover : Filter, ISampleRateDependentFilter {
/// <inheritdoc/>
[IgnoreDataMember]
public int SampleRate {
get => lowpasses[0].SampleRate;
set {
for (int i = 0; i < lowpasses.Length; i++) {
lowpasses[i].SampleRate = value;
highpasses[i].SampleRate = value;
}
}
}

/// <summary>
/// Crossover frequency.
Expand All @@ -18,7 +28,7 @@ public class Crossover : Filter {
public double Frequency {
get => lowpasses[0].CenterFreq;
set {
for (int i = 0; i < lowpasses.Length; ++i) {
for (int i = 0; i < lowpasses.Length; i++) {
lowpasses[i].CenterFreq = highpasses[i].CenterFreq = value;
}
}
Expand Down Expand Up @@ -102,7 +112,7 @@ public override void Process(float[] samples, int channel, int channels) {
}
Array.Copy(samples, LowOutput, sampleCount);
Array.Copy(samples, HighOutput, sampleCount);
for (int i = 0; i < lowpasses.Length; ++i) {
for (int i = 0; i < lowpasses.Length; i++) {
lowpasses[i].Process(LowOutput, channel, channels);
highpasses[i].Process(HighOutput, channel, channels);
}
Expand Down
28 changes: 15 additions & 13 deletions Cavern/Filters/Delay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;

using Cavern.Filters.Interfaces;
using Cavern.Utilities;
Expand All @@ -11,7 +12,13 @@ namespace Cavern.Filters {
/// <summary>
/// Delays the audio.
/// </summary>
public class Delay : Filter, IEqualizerAPOFilter, ILocalizableToString {
public class Delay : Filter, IEqualizerAPOFilter, ISampleRateDependentFilter, ILocalizableToString {
/// <summary>
/// If the filter was set up with a time delay, this is the sample rate used to calculate the delay in samples.
/// </summary>
[IgnoreDataMember]
public int SampleRate { get; set; }

/// <summary>
/// Delay in samples.
/// </summary>
Expand All @@ -35,17 +42,17 @@ public double DelayMs {
if (!double.IsNaN(delayMs)) {
return delayMs;
}
if (sampleRate == 0) {
if (SampleRate == 0) {
throw new SampleRateNotSetException();
}
return DelaySamples / (double)sampleRate * 1000;
return DelaySamples / (double)SampleRate * 1000;
}

set {
if (sampleRate == 0) {
if (SampleRate == 0) {
throw new SampleRateNotSetException();
}
DelaySamples = (int)Math.Round(value * sampleRate * .001);
DelaySamples = (int)Math.Round(value * SampleRate * .001);
delayMs = value;
}
}
Expand All @@ -60,11 +67,6 @@ public double DelayMs {
/// </summary>
readonly float[][] cache = new float[2][];

/// <summary>
/// If the filter was set up with a time delay, this is the sample rate that was used for it.
/// </summary>
int sampleRate;

/// <summary>
/// The used cache (0 or 1).
/// </summary>
Expand All @@ -87,7 +89,7 @@ public Delay(int samples) {
/// Create a delay for a given length in seconds.
/// </summary>
public Delay(double time, int sampleRate) {
this.sampleRate = sampleRate;
this.SampleRate = sampleRate;
delayMs = time;
RecreateCaches((int)(time * sampleRate * .001 + .5));
}
Expand All @@ -109,7 +111,7 @@ public static Delay FromEqualizerAPO(string[] splitLine, int sampleRate) {
return splitLine[2].ToLowerInvariant() switch {
"ms" => new Delay(delay, sampleRate),
"samples" => new Delay((int)delay) {
sampleRate = sampleRate
SampleRate = sampleRate
},
_ => throw new ArgumentOutOfRangeException(splitLine[0]),
};
Expand Down Expand Up @@ -145,7 +147,7 @@ public override void Process(float[] samples) {
}

/// <inheritdoc/>
public override object Clone() => double.IsNaN(delayMs) ? new Delay(DelaySamples) : new Delay(DelayMs, sampleRate);
public override object Clone() => double.IsNaN(delayMs) ? new Delay(DelaySamples) : new Delay(DelayMs, SampleRate);

/// <inheritdoc/>
public override string ToString() {
Expand Down
Loading

0 comments on commit d186099

Please sign in to comment.