diff --git a/Cavern.Format/Common/_Exceptions.cs b/Cavern.Format/Common/_Exceptions.cs
index 882ba64b..e8b7280d 100644
--- a/Cavern.Format/Common/_Exceptions.cs
+++ b/Cavern.Format/Common/_Exceptions.cs
@@ -27,6 +27,15 @@ public class CorruptionException : Exception {
public CorruptionException(string location) : base(string.Format(message, location)) { }
}
+ ///
+ /// Tells if an operation can only handle complex numbers of which a single component is set.
+ ///
+ public class ComplexNumberFilledException : Exception {
+ const string message = "This operation can only handle complex numbers of which a single component is set.";
+
+ public ComplexNumberFilledException() : base(message) { }
+ }
+
///
/// Tells if the decoder ran into a predefined error code that is found in the decoder's documentation.
///
diff --git a/Cavern.Format/MatrixMixing/MatrixMixer.cs b/Cavern.Format/MatrixMixing/MatrixMixer.cs
new file mode 100644
index 00000000..ed481f25
--- /dev/null
+++ b/Cavern.Format/MatrixMixing/MatrixMixer.cs
@@ -0,0 +1,96 @@
+using Cavern.Filters;
+using Cavern.Format.Common;
+using Cavern.Utilities;
+
+namespace Cavern.Format.MatrixMixing {
+ ///
+ /// Encodes frames of multichannel audio data to less channels, then back.
+ ///
+ public class MatrixMixer {
+ ///
+ /// For each encoded channel, the contribution of each source channel.
+ ///
+ readonly Filter[][] encoders;
+
+ ///
+ /// For each decoded channel, the contribution of each encoded channel.
+ ///
+ readonly Filter[][] decoders;
+
+ ///
+ /// Encodes frames of multichannel audio data to less channels, then back.
+ /// The complex numbers in the matrices can be one of 4:
+ /// - 0: no mixing will happen.
+ /// - Real: mixed with this gain.
+ /// - Positive imaginary: mixed with this gain and a 90-degree phase shift.
+ /// - Negative imaginary: mixed with this gain and a -90-degree phase shift.
+ ///
+ /// For each encoded channel, the contribution of each source channel
+ /// For each decoded channel, the contribution of each encoded channel
+ /// Length of the filters
+ public MatrixMixer(Complex[][] encodingMatrix, Complex[][] decodingMatrix, int blockSize) {
+ encoders = ConvertMatrixToFilters(encodingMatrix, blockSize, true);
+ decoders = ConvertMatrixToFilters(decodingMatrix, blockSize, false);
+ }
+
+ ///
+ /// Create the or .
+ ///
+ /// Input encoding or decoding matrix
+ /// Length of the filters
+ /// Create an encoder instead of a decoder
+ static Filter[][] ConvertMatrixToFilters(Complex[][] matrix, int blockSize, bool forward) {
+ Filter[][] result = new Filter[matrix.Length][];
+ for (int i = 0; i < matrix.Length; i++) {
+ Complex[] source = matrix[i];
+ Filter[] target = result[i] = new Filter[source.Length];
+ for (int j = 0; j < source.Length; j++) {
+ if (source[j].Real != 0 && source[j].Imaginary == 0) {
+ target[j] = new Gain(source[j].Real);
+ } else if (source[j].Real == 0 && source[j].Imaginary != 0) {
+ bool actualForward = forward;
+ if (source[j].Imaginary < 0) {
+ actualForward = !actualForward;
+ source[j].Imaginary = -source[j].Imaginary;
+ }
+ target[j] = new ComplexFilter(
+ new PhaseShifter(blockSize, actualForward),
+ new Gain(actualForward ? source[j].Imaginary : -source[j].Imaginary)
+ );
+ } else if (source[j].Real != 0 && source[j].Imaginary != 0) {
+ throw new ComplexNumberFilledException();
+ }
+ }
+ }
+ return result;
+ }
+
+ ///
+ /// Perform encoding or decoding using the transform matrix of the desired transformation.
+ ///
+ static void Process(MultichannelWaveform source, MultichannelWaveform target, Filter[][] transform) {
+ float[] working = new float[source.Length];
+ for (int t = 0; t < target.Channels; t++) {
+ target[t].Clear();
+ Filter[] channelCoding = transform[t];
+ for (int s = 0; s < source.Channels; s++) {
+ if (channelCoding[s] != null) {
+ source[s].CopyTo(working);
+ channelCoding[s].Process(working);
+ WaveformUtils.Mix(working, target[t]);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Encode the to the using the encoding matrix.
+ ///
+ public void Encode(MultichannelWaveform source, MultichannelWaveform target) => Process(source, target, encoders);
+
+ ///
+ /// Decode the to the using the decoding matrix.
+ ///
+ public void Decode(MultichannelWaveform source, MultichannelWaveform target) => Process(source, target, decoders);
+ }
+}
\ No newline at end of file
diff --git a/Cavern/Filters/FastConvolver.cs b/Cavern/Filters/FastConvolver.cs
index 03c18e60..815e8ae0 100644
--- a/Cavern/Filters/FastConvolver.cs
+++ b/Cavern/Filters/FastConvolver.cs
@@ -90,10 +90,38 @@ public int Delay {
///
int delay;
+ ///
+ /// Constructs an optimized convolution with no delay.
+ ///
+ /// Transfer function of the desired filter
+ public FastConvolver(Complex[] filter) : this(filter, 0) { }
+
+ ///
+ /// Constructs an optimized convolution with added delay.
+ ///
+ /// Transfer function of the desired filter
+ /// Added filter delay to the impulse, in samples
+ public FastConvolver(Complex[] filter, int delay) {
+ this.filter = filter;
+ present = new Complex[filter.Length];
+ cache = CreateCache(filter.Length);
+ Delay = delay;
+ }
+
+ ///
+ /// Constructs an optimized convolution with added delay and sets the sample rate.
+ ///
+ /// Transfer function of the desired filter
+ /// Sample rate of the response
+ /// Added filter delay to the impulse, in samples
+ public FastConvolver(Complex[] filter, int sampleRate, int delay) : this(filter, delay) => SampleRate = sampleRate;
+
///
/// Constructs an optimized convolution with no delay.
///
/// Impulse response of the desired filter
+ /// This constructor transforms the to Fourier space. If you have a transfer function available, use
+ /// for optimal performance.
public FastConvolver(float[] impulse) : this(impulse, 0) { }
///
@@ -101,6 +129,8 @@ public FastConvolver(float[] impulse) : this(impulse, 0) { }
///
/// Impulse response of the desired filter
/// Added filter delay to the impulse, in samples
+ /// This constructor transforms the to Fourier space. If you have a transfer function available, use
+ /// for optimal performance.
public FastConvolver(float[] impulse, int delay) {
this.delay = delay;
Impulse = impulse;
@@ -112,6 +142,8 @@ public FastConvolver(float[] impulse, int delay) {
/// Impulse response of the desired filter
/// Sample rate of the response
/// Added filter delay to the impulse, in samples
+ /// This constructor transforms the to Fourier space. If you have a transfer function available, use
+ /// for optimal performance.
public FastConvolver(float[] impulse, int sampleRate, int delay) : this(impulse, delay) => SampleRate = sampleRate;
///
diff --git a/Cavern/Filters/PhaseShifter.cs b/Cavern/Filters/PhaseShifter.cs
index c51d3ba3..02cbddf9 100644
--- a/Cavern/Filters/PhaseShifter.cs
+++ b/Cavern/Filters/PhaseShifter.cs
@@ -2,27 +2,36 @@
namespace Cavern.Filters {
///
- /// Performs a Hilbert transform for a 90-degree phase shift.
+ /// Performs a Hilbert transform for a 90 or -90-degree phase shift.
///
/// This filter is based on the .
public class PhaseShifter : FastConvolver {
///
- /// Creates a phase shifter for a given block size.
+ /// Creates a 90-degree phase shifter for a given block size.
///
- public PhaseShifter(int blockSize) : base(GenerateFilter(blockSize)) { }
+ /// Length of the filter
+ public PhaseShifter(int blockSize) : this(blockSize, true) { }
+
+ ///
+ /// Creates a phase shift in a given direction.
+ ///
+ /// Length of the filter
+ /// True for a 90-degree phase shift, false for a -90-degree phase shift
+ public PhaseShifter(int blockSize, bool forward) : base(GenerateFilter(blockSize, forward)) { }
///
/// Generate the Hilbert transform's impulse response for a given block size.
///
- static float[] GenerateFilter(int blockSize) {
+ static float[] GenerateFilter(int blockSize, bool forward) {
float[] result = new float[blockSize];
int half = blockSize / 2;
+ float dir = forward ? 1 : -1;
for (int i = half--; i < blockSize; i++) {
- result[i] = 1 / ((i - half) * MathF.PI);
+ result[i] = dir / ((i - half) * MathF.PI);
}
- ++half;
+ half++;
for (int i = 0; i < half; i++) {
- result[i] = 1 / ((-half + i) * MathF.PI);
+ result[i] = dir / ((-half + i) * MathF.PI);
}
return result;
}
diff --git a/Cavern/Waveforms/MultichannelWaveform.cs b/Cavern/Waveforms/MultichannelWaveform.cs
index d0f4ce17..b3ce6175 100644
--- a/Cavern/Waveforms/MultichannelWaveform.cs
+++ b/Cavern/Waveforms/MultichannelWaveform.cs
@@ -24,6 +24,14 @@ public int Channels {
get => signals.Length;
}
+ ///
+ /// The length of a single channel's waveform.
+ ///
+ public int Length {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => signals[0].Length;
+ }
+
///
/// Each channel's waveform.
///