Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Contribute ChannelMixerSampleProvider - arbitrary mixing of channels in a stream #982

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions NAudio.Core/Wave/SampleProviders/ChannelMixMatrix.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System;

namespace NAudio.Core.Wave.SampleProviders
{
/// <summary>
/// Defines common channel mixing matrixes for use with <see cref="ChannelMixerSampleProvider"/>.
/// </summary>
public static class ChannelMixMatrix
{
/// <summary>
/// Converts a mono source to 2-channel source by copying the input to both outputs.
/// </summary>
public static readonly float[,] MonoToStereo = new float[,]
{
{ 1.0f, 1.0f }
};

/// <summary>
/// Converts a 2-channel source to a mono source by mixing the channels together using equal weight.
/// </summary>
public static readonly float[,] StereoToMono = new float[,]
{
{ 0.5f },
{ 0.5f },
};

/// <summary>
/// Modifies a 2-channel stream by selecting only the first channel. The output is 2-channel.
/// </summary>
public static readonly float[,] StereoLeft = new float[,]
{
{ 1.0f, 0.0f },
{ 0.0f, 0.0f },
};

/// <summary>
/// Modifies a 2-channel stream by selecting only the second channel. The output is 2-channel.
/// </summary>
public static readonly float[,] StereoRight = new float[,]
{
{ 0.0f, 0.0f },
{ 0.0f, 1.0f },
};

/// <summary>
/// Converts a 2-channel source to a canonical 5.1 output. The output has 6 channels:
/// FrontLeft, FrontRight, Center, Sub, RearLeft, RearRight, in that order.
/// </summary>
/// <remarks>
/// The matrix is designed so that the aggregate output volume from the 6-channel output
/// is the same output volume that would've occurred if the original input was applied to
/// two channels; if the original audio would've produced 200W of output spread across 2
/// speakers, the transformed output would also produce 200W of output, instead spread
/// across 6 speakers.
///
/// This can be noted by the fact that no column in the matrix sums to 1.0. The loudest
/// channel is the center channel, receiving a sum of 0.444 of its inputs (0.222 from left,
/// 0.222 from right).
///
/// If you would like a matrix that maximizes output volume, scale the matrix by a factor of
/// 2.25. One might do this to preserve entropy during processing, with a final gain
/// reduction step at the output to maintain intended power output.
/// </remarks>
public static readonly float[,] StereoTo5_1 = new float[,]
{
// Output Channels:
//FL FR Centr Subwf RL RR
{0.314f, 0.000f, 0.222f, 0.031f, 0.268f, 0.164f}, // Left Input
{0.000f, 0.314f, 0.222f, 0.031f, 0.164f, 0.268f} // Right Input
};
}
}
146 changes: 146 additions & 0 deletions NAudio.Core/Wave/SampleProviders/ChannelMixerSampleProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using System;
using NAudio.Utils;
using NAudio.Wave;

namespace NAudio.Core.Wave.SampleProviders
{
/// <summary>
/// Produces an output stream by mixing together the channels of a single source using an
/// arbitrary mixing matrix, where the number of output channels is determined by the number of
/// columns in the matrix.
/// </summary>
/// <remarks>See <see cref="ChannelMixMatrix"/> for some pre-defined matrixes.</remarks>
public class ChannelMixerSampleProvider : ISampleProvider
{
private readonly ISampleProvider source;
private readonly float[,] matrix;

private readonly int inputChannels;
private readonly int outputChannels;

private float[] sourceBuffer;

/// <summary>
/// Initializes a new instance of the <see cref="ChannelMixer"/> class. The number of output
/// channels from the mixer depends on the structure of the provided matrix.
/// </summary>
/// <param name="source">The provider to read from.</param>
/// <param name="matrix">
/// Specifies the matrix that converts input samples to output samples. The values of the
/// matrix should be between 0.0f and 1.0f. The number of rows in the matrix must match the
/// number of input channels from <paramref name="source"/>. The number of columns in the matrix
/// determines the number of output channels.
/// </param>
/// <exception cref="ArgumentNullException">Occurs if any argument is null.</exception>
/// <exception cref="ArgumentException">Occurs if the matrix is not 2-dimensional.</exception>
public ChannelMixerSampleProvider( ISampleProvider source, float[,] matrix )
{
if( source == null )
throw new ArgumentNullException( nameof( source ) );

if( matrix == null )
throw new ArgumentNullException( nameof( matrix ) );

this.source = source;
this.matrix = matrix;
this.inputChannels = matrix.GetLength( 0 );
this.outputChannels = matrix.GetLength( 1 );

if( this.inputChannels != source.WaveFormat.Channels )
{
throw new ArgumentException(
"The number of channels in the source do not match the number of input elements in the matrix."
);
}

this.WaveFormat = WaveFormat.CreateIeeeFloatWaveFormat(
this.source.WaveFormat.SampleRate,
this.outputChannels
);
}

/// <summary>
/// Gets the WaveFormat of the output from the mixer. The encoding is always <see
/// cref="WaveFormatEncoding.IeeeFloat"/>. The number of channels present in the <see
/// cref="WaveFormat"/> depends on the channel mixing matrix in use.
/// </summary>
public WaveFormat WaveFormat { get; private set; }

/// <summary>
/// Reads samples from the mixer into the provided buffer.
/// </summary>
/// <param name="buffer">A buffer to which to write output samples.</param>
/// <param name="start">The first index in 'buffer' to write to.</param>
/// <param name="numSamples">
/// The maximum number of samples to write to the buffer. Note that fewer than this number
/// of samples may be returned.
/// </param>
/// <returns>The total number of samples that were obtained.</returns>
public int Read( float[] buffer, int start, int numSamples )
{
// 1. Figure out how many samples to read from the source to satisfy the caller.
int numBlocks = numSamples / this.outputChannels;
int numSourceSamples = numBlocks * this.inputChannels;

this.sourceBuffer = BufferHelpers.Ensure( this.sourceBuffer, numSourceSamples );

// 2. Read from the source and figure out how much we got.
numSourceSamples = source.Read( this.sourceBuffer, 0, numSourceSamples );
numBlocks = numSourceSamples / this.inputChannels;

// 3. Build a view over the input and output float arrays that will view just one block.
var sourceBlock = new FloatSpan( this.sourceBuffer );
var destBlock = new FloatSpan( buffer );

for( int i = 0; i < numBlocks; i++ )
{
// 4. Update which block we're looking at.
sourceBlock.Start = this.inputChannels * i;
destBlock.Start = this.outputChannels * i + start;

// 5. Transform one block from input to output.
TransformBlock( sourceBlock, destBlock );
}

return numBlocks * this.outputChannels;
}

private void TransformBlock( FloatSpan sourceBlock, FloatSpan destBlock )
{
float value;
for( int outCh = 0; outCh < this.outputChannels; outCh++ )
{
value = 0;

for( int inCh = 0; inCh < this.inputChannels; inCh++ )
{
value += sourceBlock[inCh] * matrix[inCh, outCh];
}

destBlock[outCh] = value;
}
}

/// <summary>
/// Makes it easier to do the index math when transforming blocks.
/// </summary>
private struct FloatSpan
{
public float[] Floats;

public int Start;

public FloatSpan( float[] floats )
{
this.Floats = floats;
this.Start = 0;
}

public float this[int index]
{
get => Floats[index + Start];
set => Floats[index + Start] = value;
}
}
}
}