Skip to content

Commit

Permalink
Generic matrix mixing with phase shift support
Browse files Browse the repository at this point in the history
  • Loading branch information
VoidXH committed Jan 5, 2025
1 parent e6eaf7d commit 20ba934
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 7 deletions.
9 changes: 9 additions & 0 deletions Cavern.Format/Common/_Exceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ public class CorruptionException : Exception {
public CorruptionException(string location) : base(string.Format(message, location)) { }
}

/// <summary>
/// Tells if an operation can only handle complex numbers of which a single component is set.
/// </summary>
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) { }
}

/// <summary>
/// Tells if the decoder ran into a predefined error code that is found in the decoder's documentation.
/// </summary>
Expand Down
96 changes: 96 additions & 0 deletions Cavern.Format/MatrixMixing/MatrixMixer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using Cavern.Filters;
using Cavern.Format.Common;
using Cavern.Utilities;

namespace Cavern.Format.MatrixMixing {
/// <summary>
/// Encodes frames of multichannel audio data to less channels, then back.
/// </summary>
public class MatrixMixer {
/// <summary>
/// For each encoded channel, the contribution of each source channel.
/// </summary>
readonly Filter[][] encoders;

/// <summary>
/// For each decoded channel, the contribution of each encoded channel.
/// </summary>
readonly Filter[][] decoders;

/// <summary>
/// Encodes frames of multichannel audio data to less channels, then back.
/// The complex numbers in the matrices can be one of 4:<br />
/// - 0: no mixing will happen.<br />
/// - Real: mixed with this gain.<br />
/// - Positive imaginary: mixed with this gain and a 90-degree phase shift.<br />
/// - Negative imaginary: mixed with this gain and a -90-degree phase shift.
/// </summary>
/// <param name="encodingMatrix">For each encoded channel, the contribution of each source channel</param>
/// <param name="decodingMatrix">For each decoded channel, the contribution of each encoded channel</param>
/// <param name="blockSize">Length of the filters</param>
public MatrixMixer(Complex[][] encodingMatrix, Complex[][] decodingMatrix, int blockSize) {
encoders = ConvertMatrixToFilters(encodingMatrix, blockSize, true);
decoders = ConvertMatrixToFilters(decodingMatrix, blockSize, false);
}

/// <summary>
/// Create the <see cref="encoders"/> or <see cref="decoders"/>.
/// </summary>
/// <param name="matrix">Input encoding or decoding matrix</param>
/// <param name="blockSize">Length of the filters</param>
/// <param name="forward">Create an encoder instead of a decoder</param>
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;
}

/// <summary>
/// Perform encoding or decoding using the transform matrix of the desired transformation.
/// </summary>
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]);
}
}
}
}

/// <summary>
/// Encode the <paramref name="source"/> to the <paramref name="target"/> using the encoding matrix.
/// </summary>
public void Encode(MultichannelWaveform source, MultichannelWaveform target) => Process(source, target, encoders);

/// <summary>
/// Decode the <paramref name="source"/> to the <paramref name="target"/> using the decoding matrix.
/// </summary>
public void Decode(MultichannelWaveform source, MultichannelWaveform target) => Process(source, target, decoders);
}
}
32 changes: 32 additions & 0 deletions Cavern/Filters/FastConvolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,47 @@ public int Delay {
/// </summary>
int delay;

/// <summary>
/// Constructs an optimized convolution with no delay.
/// </summary>
/// <param name="filter">Transfer function of the desired filter</param>
public FastConvolver(Complex[] filter) : this(filter, 0) { }

/// <summary>
/// Constructs an optimized convolution with added delay.
/// </summary>
/// <param name="filter">Transfer function of the desired filter</param>
/// <param name="delay">Added filter delay to the impulse, in samples</param>
public FastConvolver(Complex[] filter, int delay) {
this.filter = filter;
present = new Complex[filter.Length];
cache = CreateCache(filter.Length);
Delay = delay;
}

/// <summary>
/// Constructs an optimized convolution with added delay and sets the sample rate.
/// </summary>
/// <param name="impulse">Transfer function of the desired filter</param>
/// <param name="sampleRate">Sample rate of the <paramref name="impulse"/> response</param>
/// <param name="delay">Added filter delay to the impulse, in samples</param>
public FastConvolver(Complex[] filter, int sampleRate, int delay) : this(filter, delay) => SampleRate = sampleRate;

/// <summary>
/// Constructs an optimized convolution with no delay.
/// </summary>
/// <param name="impulse">Impulse response of the desired filter</param>
/// <remarks>This constructor transforms the <paramref name="impulse"/> to Fourier space. If you have a transfer function available, use
/// <see cref="FastConvolver(Complex[])"/> for optimal performance.</remarks>
public FastConvolver(float[] impulse) : this(impulse, 0) { }

/// <summary>
/// Constructs an optimized convolution with added delay.
/// </summary>
/// <param name="impulse">Impulse response of the desired filter</param>
/// <param name="delay">Added filter delay to the impulse, in samples</param>
/// <remarks>This constructor transforms the <paramref name="impulse"/> to Fourier space. If you have a transfer function available, use
/// <see cref="FastConvolver(Complex[], int)"/> for optimal performance.</remarks>
public FastConvolver(float[] impulse, int delay) {
this.delay = delay;
Impulse = impulse;
Expand All @@ -112,6 +142,8 @@ public FastConvolver(float[] impulse, int delay) {
/// <param name="impulse">Impulse response of the desired filter</param>
/// <param name="sampleRate">Sample rate of the <paramref name="impulse"/> response</param>
/// <param name="delay">Added filter delay to the impulse, in samples</param>
/// <remarks>This constructor transforms the <paramref name="impulse"/> to Fourier space. If you have a transfer function available, use
/// <see cref="FastConvolver(Complex[], int, int)"/> for optimal performance.</remarks>
public FastConvolver(float[] impulse, int sampleRate, int delay) : this(impulse, delay) => SampleRate = sampleRate;

/// <summary>
Expand Down
23 changes: 16 additions & 7 deletions Cavern/Filters/PhaseShifter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,36 @@

namespace Cavern.Filters {
/// <summary>
/// Performs a Hilbert transform for a 90-degree phase shift.
/// Performs a Hilbert transform for a 90 or -90-degree phase shift.
/// </summary>
/// <remarks>This filter is based on the <see cref="FastConvolver"/>.</remarks>
public class PhaseShifter : FastConvolver {
/// <summary>
/// Creates a phase shifter for a given block size.
/// Creates a 90-degree phase shifter for a given block size.
/// </summary>
public PhaseShifter(int blockSize) : base(GenerateFilter(blockSize)) { }
/// <param name="blockSize">Length of the filter</param>
public PhaseShifter(int blockSize) : this(blockSize, true) { }

/// <summary>
/// Creates a phase shift in a given direction.
/// </summary>
/// <param name="blockSize">Length of the filter</param>
/// <param name="forward">True for a 90-degree phase shift, false for a -90-degree phase shift</param>
public PhaseShifter(int blockSize, bool forward) : base(GenerateFilter(blockSize, forward)) { }

/// <summary>
/// Generate the Hilbert transform's impulse response for a given block size.
/// </summary>
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;
}
Expand Down
8 changes: 8 additions & 0 deletions Cavern/Waveforms/MultichannelWaveform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ public int Channels {
get => signals.Length;
}

/// <summary>
/// The length of a single channel's waveform.
/// </summary>
public int Length {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => signals[0].Length;
}

/// <summary>
/// Each channel's waveform.
/// </summary>
Expand Down

0 comments on commit 20ba934

Please sign in to comment.