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. ///