Commit ebaa74d6 authored by David Reid's avatar David Reid

Improvements to channel conversion during spatialization.

This commit also fixes a bug where panning is incorrectly getting
applied to non-directional channels, such as mono and LFE channels.
parent 072efc6f
...@@ -3121,71 +3121,215 @@ static MA_INLINE float ma_apply_volume_unclipped_f32(float x, float volume) ...@@ -3121,71 +3121,215 @@ static MA_INLINE float ma_apply_volume_unclipped_f32(float x, float volume)
static void ma_convert_pcm_frames_format_and_channels(void* pDst, ma_format formatOut, ma_uint32 channelsOut, const ma_channel* pChannelMapOut, const void* pSrc, ma_format formatIn, ma_uint32 channelsIn, const ma_channel* pChannelMapIn, ma_uint64 frameCount, ma_dither_mode ditherMode)
static ma_result ma_channel_map_build_shuffle_table(const ma_channel* pChannelMapIn, ma_uint32 channelCountIn, const ma_channel* pChannelMapOut, ma_uint32 channelCountOut, ma_uint8* pShuffleTable)
{ {
MA_ASSERT(pDst != NULL); ma_uint32 iChannelIn;
MA_ASSERT(pSrc != NULL); ma_uint32 iChannelOut;
if (pShuffleTable == NULL || channelCountIn == 0 || channelCountIn > MA_MAX_CHANNELS || channelCountOut == 0 || channelCountOut > MA_MAX_CHANNELS) {
return MA_INVALID_ARGS;
}
/*
When building the shuffle table we just do a 1:1 mapping based on the first occurance of a channel. If the
input channel has more than one occurance of a channel position, the second one will be ignored.
*/
for (iChannelOut = 0; iChannelOut < channelCountOut; iChannelOut += 1) {
ma_channel channelOut;
/* Default to MA_CHANNEL_INDEX_NULL so that if a mapping is not found it'll be set appropriately. */
pShuffleTable[iChannelOut] = MA_CHANNEL_INDEX_NULL;
channelOut = ma_channel_map_get_channel(pChannelMapOut, channelCountOut, iChannelOut);
for (iChannelIn = 0; iChannelIn < channelCountIn; iChannelIn += 1) {
ma_channel channelIn;
channelIn = ma_channel_map_get_channel(pChannelMapIn, channelCountIn, iChannelIn);
if (channelOut == channelIn) {
pShuffleTable[iChannelOut] = (ma_uint8)iChannelIn;
break;
}
/*
Getting here means the channels don't exactly match, but we are going to support some
relaxed matching for practicality. If, for example, there are two stereo channel maps,
but one uses front left/right and the other uses side left/right, it makes logical
sense to just map these. The way we'll do it is we'll check if there is a logical
corresponding mapping, and if so, apply it, but we will *not* break from the loop,
thereby giving the loop a chance to find an exact match later which will take priority.
*/
switch (channelOut)
{
/* Left channels. */
case MA_CHANNEL_FRONT_LEFT:
case MA_CHANNEL_SIDE_LEFT:
{
switch (channelIn) {
case MA_CHANNEL_FRONT_LEFT:
case MA_CHANNEL_SIDE_LEFT:
{
pShuffleTable[iChannelOut] = (ma_uint8)iChannelIn;
} break;
}
} break;
/* Right channels. */
case MA_CHANNEL_FRONT_RIGHT:
case MA_CHANNEL_SIDE_RIGHT:
{
switch (channelIn) {
case MA_CHANNEL_FRONT_RIGHT:
case MA_CHANNEL_SIDE_RIGHT:
{
pShuffleTable[iChannelOut] = (ma_uint8)iChannelIn;
} break;
}
} break;
default: break;
}
}
}
if (channelsOut == channelsIn) { return MA_SUCCESS;
/* Only format conversion required. */ }
if (formatOut == formatIn) {
/* No data conversion required at all - just copy. */ static ma_result ma_channel_map_apply_shuffle_table_f32(float* pFramesOut, ma_uint32 channelsOut, const float* pFramesIn, ma_uint32 channelsIn, ma_uint64 frameCount, const ma_uint8* pShuffleTable)
if (pDst == pSrc) { {
/* No-op. */ ma_uint64 iFrame;
ma_uint32 iChannelOut;
if (pFramesOut == NULL || pFramesIn == NULL || channelsOut == 0 || channelsOut > MA_MAX_CHANNELS || pShuffleTable == NULL) {
return MA_INVALID_ARGS;
}
for (iFrame = 0; iFrame < frameCount; iFrame += 1) {
for (iChannelOut = 0; iChannelOut < channelsOut; iChannelOut += 1) {
ma_uint8 iChannelIn = pShuffleTable[iChannelOut];
if (iChannelIn < channelsIn) { /* For safety, and to deal with MA_CHANNEL_INDEX_NULL. */
pFramesOut[iChannelOut] = pFramesIn[iChannelIn];
} else { } else {
ma_copy_pcm_frames(pDst, pSrc, frameCount, formatOut, channelsOut); pFramesOut[iChannelOut] = 0;
} }
} else {
/* Simple format conversion. */
ma_convert_pcm_frames_format(pDst, formatOut, pSrc, formatIn, frameCount, channelsOut, ditherMode);
} }
} else {
/* TODO: This needs to be optimized. Too inefficient to be initializing a channel converter. */
/* Getting here means we require a channel converter. We do channel conversion in the input format, and then format convert as a post process step if required. */ pFramesOut += channelsOut;
ma_result result; pFramesIn += channelsIn;
ma_channel_converter_config channelConverterConfig; }
ma_channel_converter channelConverter;
channelConverterConfig = ma_channel_converter_config_init(formatIn, channelsIn, pChannelMapIn, channelsOut, pChannelMapOut, ma_channel_mix_mode_default); return MA_SUCCESS;
result = ma_channel_converter_init(&channelConverterConfig, &channelConverter); }
if (result != MA_SUCCESS) {
return; /* Failed to initialize channel converter for some reason. Should never fail. */ static ma_result ma_channel_map_apply_mono_out_f32(float* pFramesOut, const float* pFramesIn, const ma_channel* pChannelMapIn, ma_uint32 channelsIn, ma_uint64 frameCount)
{
ma_uint64 iFrame;
ma_uint32 iChannelIn;
ma_uint32 accumulationCount;
if (pFramesOut == NULL || pFramesIn == NULL || channelsIn == 0) {
return MA_INVALID_ARGS;
}
/* In this case the output stream needs to be the average of all channels, ignoring NONE. */
/* A quick pre-processing step to get the accumulation counter since we're ignoring NONE channels. */
accumulationCount = 0;
for (iChannelIn = 0; iChannelIn < channelsIn; iChannelIn += 1) {
if (ma_channel_map_get_channel(pChannelMapIn, channelsIn, iChannelIn) != MA_CHANNEL_NONE) {
accumulationCount += 1;
} }
}
/* If we don't require any format conversion we can output straight into the output buffer. Otherwise we need to use an intermediary. */ if (accumulationCount > 0) { /* <-- Prevent a division by zero. */
if (formatOut == formatIn) { for (iFrame = 0; iFrame < frameCount; iFrame += 1) {
/* No format conversion required. Output straight to the output buffer. */ float accumulation = 0;
ma_channel_converter_process_pcm_frames(&channelConverter, pDst, pSrc, frameCount);
} else {
/* Format conversion required. We need to use an intermediary buffer. */
ma_uint8 buffer[MA_DATA_CONVERTER_STACK_BUFFER_SIZE]; /* formatIn, channelsOut */
ma_uint32 bufferCap = sizeof(buffer) / ma_get_bytes_per_frame(formatIn, channelsOut);
ma_uint64 totalFramesProcessed = 0;
while (totalFramesProcessed < frameCount) {
ma_uint64 framesToProcess = frameCount - totalFramesProcessed;
if (framesToProcess > bufferCap) {
framesToProcess = bufferCap;
}
result = ma_channel_converter_process_pcm_frames(&channelConverter, buffer, ma_offset_ptr(pSrc, totalFramesProcessed * ma_get_bytes_per_frame(formatIn, channelsIn)), framesToProcess); for (iChannelIn = 0; iChannelIn < channelsIn; iChannelIn += 1) {
if (result != MA_SUCCESS) { ma_channel channelIn = ma_channel_map_get_channel(pChannelMapIn, channelsIn, iChannelIn);
break; if (channelIn != MA_CHANNEL_NONE) {
accumulation += pFramesIn[iChannelIn];
} }
}
pFramesOut[0] = accumulation / accumulationCount;
pFramesOut += 1;
pFramesIn += channelsIn;
}
} else {
ma_silence_pcm_frames(pFramesOut, frameCount, ma_format_f32, 1);
}
/* Channel conversion is done, now format conversion straight into the output buffer. */ return MA_SUCCESS;
ma_convert_pcm_frames_format(ma_offset_ptr(pDst, totalFramesProcessed * ma_get_bytes_per_frame(formatOut, channelsOut)), formatOut, buffer, formatIn, framesToProcess, channelsOut, ditherMode); }
totalFramesProcessed += framesToProcess; static ma_result ma_channel_map_apply_mono_in_f32(float* pFramesOut, const ma_channel* pChannelMapOut, ma_uint32 channelsOut, const float* pFramesIn, ma_uint64 frameCount)
{
ma_uint64 iFrame;
ma_uint32 iChannelOut;
if (pFramesOut == NULL || channelsOut == 0 || pFramesIn == NULL) {
return MA_INVALID_ARGS;
}
/* In this case we just copy the mono channel to each of the output channels, ignoring NONE. */
for (iFrame = 0; iFrame < frameCount; iFrame += 1) {
for (iChannelOut = 0; iChannelOut < channelsOut; iChannelOut += 1) {
ma_channel channelOut = ma_channel_map_get_channel(pChannelMapOut, channelsOut, iChannelOut);
if (channelOut != MA_CHANNEL_NONE) {
pFramesOut[iChannelOut] = pFramesIn[0];
} }
} }
pFramesOut += channelsOut;
pFramesIn += 1;
} }
return MA_SUCCESS;
} }
static void ma_convert_pcm_frames_channels_f32(float* pFramesOut, ma_uint32 channelsOut, const ma_channel* pChannelMapOut, const float* pFramesIn, ma_uint32 channelsIn, const ma_channel* pChannelMapIn, ma_uint64 frameCount) static void ma_channel_map_apply_f32(float* pFramesOut, const ma_channel* pChannelMapOut, ma_uint32 channelsOut, const float* pFramesIn, const ma_channel* pChannelMapIn, ma_uint32 channelsIn, ma_uint64 frameCount)
{ {
ma_convert_pcm_frames_format_and_channels(pFramesOut, ma_format_f32, channelsOut, pChannelMapOut, pFramesIn, ma_format_f32, channelsIn, pChannelMapIn, frameCount, ma_dither_mode_none); /* Optimized Path: Passthrough */
if (channelsOut == channelsIn && pChannelMapOut == pChannelMapIn) {
ma_copy_pcm_frames(pFramesOut, pFramesIn, frameCount, ma_format_f32, channelsOut);
return;
}
/* Special Path: Mono Output. */
if (channelsOut == 1 && (pChannelMapOut == NULL || pChannelMapOut[0] == MA_CHANNEL_MONO)) {
ma_channel_map_apply_mono_out_f32(pFramesOut, pFramesIn, pChannelMapIn, channelsIn, frameCount);
return;
}
/* Special Path: Mono Input. */
if (channelsIn == 1 && (pChannelMapIn == NULL || pChannelMapIn[0] == MA_CHANNEL_MONO)) {
ma_channel_map_apply_mono_in_f32(pFramesOut, pChannelMapOut, channelsOut, pFramesIn, frameCount);
return;
}
/*
For now, we're doing channel conversion a bit different to the standard channel converter. Here
we're only going to do a shuffle. We can't do this if the channel count exceeds the maximum
channel count or else we won't be able to fit it in a stack-allocated shuffle table.
*/
if (channelsOut <= MA_MAX_CHANNELS) {
ma_result result;
ma_channel shuffleTable[MA_MAX_CHANNELS];
result = ma_channel_map_build_shuffle_table(pChannelMapIn, channelsIn, pChannelMapOut, channelsOut, shuffleTable);
if (result != MA_SUCCESS) {
return;
}
result = ma_channel_map_apply_shuffle_table_f32(pFramesOut, channelsOut, pFramesIn, channelsIn, frameCount, shuffleTable);
if (result != MA_SUCCESS) {
return;
}
} else {
/* Fall back to silence. If you hit this, what are you doing with so many channels?! */
ma_silence_pcm_frames(pFramesOut, frameCount, ma_format_f32, channelsOut);
}
} }
...@@ -3380,81 +3524,6 @@ static ma_result ma_mix_pcm_frames(void* pDst, const void* pSrc, ma_uint64 frame ...@@ -3380,81 +3524,6 @@ static ma_result ma_mix_pcm_frames(void* pDst, const void* pSrc, ma_uint64 frame
#endif #endif
/* Not used right now, but leaving here for reference. */
#if 0
static ma_result ma_mix_pcm_frames_ex(void* pDst, ma_format formatOut, ma_uint32 channelsOut, const void* pSrc, ma_format formatIn, ma_uint32 channelsIn, ma_uint64 frameCount, float volume)
{
if (pDst == NULL || pSrc == NULL) {
return MA_INVALID_ARGS;
}
if (formatOut == formatIn && channelsOut == channelsIn) {
/* Fast path. */
return ma_mix_pcm_frames(pDst, pSrc, frameCount, formatOut, channelsOut, volume);
} else {
/* Slow path. Data conversion required. */
ma_uint8 buffer[MA_DATA_CONVERTER_STACK_BUFFER_SIZE];
ma_uint32 bufferCapInFrames = sizeof(buffer) / ma_get_bytes_per_frame(formatOut, channelsOut);
ma_uint64 totalFramesProcessed = 0;
/* */ void* pRunningDst = pDst;
const void* pRunningSrc = pSrc;
while (totalFramesProcessed < frameCount) {
ma_uint64 framesToProcess = frameCount - totalFramesProcessed;
if (framesToProcess > bufferCapInFrames) {
framesToProcess = bufferCapInFrames;
}
/* Conversion. */
ma_convert_pcm_frames_format_and_channels(buffer, formatOut, channelsOut, pRunningSrc, formatIn, channelsIn, framesToProcess, ma_dither_mode_none);
/* Mixing. */
ma_mix_pcm_frames(pRunningDst, buffer, framesToProcess, formatOut, channelsOut, volume);
totalFramesProcessed += framesToProcess;
pRunningDst = ma_offset_ptr(pRunningDst, framesToProcess * ma_get_accumulation_bytes_per_frame(formatOut, channelsOut));
pRunningSrc = ma_offset_ptr(pRunningSrc, framesToProcess * ma_get_bytes_per_frame(formatIn, channelsIn));
}
}
return MA_SUCCESS;
}
static void ma_convert_pcm_frames_channels_and_mix_f32(float* pFramesOut, ma_uint32 channelsOut, const float* pFramesIn, ma_uint32 channelsIn, ma_uint64 frameCount, float volume)
{
if (pFramesOut == NULL || pFramesIn == NULL) {
return;
}
if (channelsOut == channelsIn) {
/* Fast path. No channel conversion required. */
ma_mix_pcm_frames_f32(pFramesOut, pFramesIn, frameCount, channelsIn, volume);
} else {
/* Slow path. Channel conversion required. Needs to be done in two steps with an intermediary buffer. */
float temp[MA_DATA_CONVERTER_STACK_BUFFER_SIZE / sizeof(float)]; /* In output channels. */
ma_uint64 tempCapInFrames = ma_countof(temp) / channelsOut;
ma_uint64 totalFramesProcessed = 0;
while (totalFramesProcessed < frameCount) {
ma_uint64 framesToProcess;
framesToProcess = frameCount - totalFramesProcessed;
if (framesToProcess > tempCapInFrames) {
framesToProcess = tempCapInFrames;
}
/* Step 1: Convert channels. */
ma_convert_pcm_frames_channels_f32(temp, channelsOut, ma_offset_pcm_frames_const_ptr_f32(pFramesIn, totalFramesProcessed, channelsIn), channelsIn, framesToProcess);
/* Step 2: Mix. */
ma_mix_pcm_frames_f32(ma_offset_pcm_frames_ptr_f32(pFramesOut, totalFramesProcessed, channelsIn), temp, framesToProcess, channelsOut, volume);
totalFramesProcessed += framesToProcess;
}
}
}
#endif
MA_API ma_node_graph_config ma_node_graph_config_init(ma_uint32 channels) MA_API ma_node_graph_config ma_node_graph_config_init(ma_uint32 channels)
{ {
...@@ -10787,6 +10856,15 @@ static ma_vec3f g_maChannelDirections[MA_CHANNEL_POSITION_COUNT] = { ...@@ -10787,6 +10856,15 @@ static ma_vec3f g_maChannelDirections[MA_CHANNEL_POSITION_COUNT] = {
{ 0.0f, 0.0f, -1.0f } /* MA_CHANNEL_AUX_31 */ { 0.0f, 0.0f, -1.0f } /* MA_CHANNEL_AUX_31 */
}; };
static ma_vec3f ma_get_channel_direction(ma_channel channel)
{
if (channel >= MA_CHANNEL_POSITION_COUNT) {
return ma_vec3f_init_3f(0, 0, -1);
} else {
return g_maChannelDirections[channel];
}
}
static float ma_attenuation_inverse(float distance, float minDistance, float maxDistance, float rolloff) static float ma_attenuation_inverse(float distance, float minDistance, float maxDistance, float rolloff)
...@@ -11432,7 +11510,7 @@ MA_API ma_result ma_spatializer_process_pcm_frames(ma_spatializer* pSpatializer, ...@@ -11432,7 +11510,7 @@ MA_API ma_result ma_spatializer_process_pcm_frames(ma_spatializer* pSpatializer,
if (pSpatializer->config.channelsIn == pSpatializer->config.channelsOut) { if (pSpatializer->config.channelsIn == pSpatializer->config.channelsOut) {
ma_copy_pcm_frames(pFramesOut, pFramesIn, frameCount, ma_format_f32, pSpatializer->config.channelsIn); ma_copy_pcm_frames(pFramesOut, pFramesIn, frameCount, ma_format_f32, pSpatializer->config.channelsIn);
} else { } else {
ma_convert_pcm_frames_channels_f32((float*)pFramesOut, pSpatializer->config.channelsOut, pChannelMapOut, (const float*)pFramesIn, pSpatializer->config.channelsIn, pChannelMapIn, frameCount); /* Safe casts to float* because f32 is the only supported format. */ ma_channel_map_apply_f32((float*)pFramesOut, pChannelMapOut, pSpatializer->config.channelsOut, (const float*)pFramesIn, pChannelMapIn, pSpatializer->config.channelsIn, frameCount); /* Safe casts to float* because f32 is the only supported format. */
} }
/* /*
...@@ -11682,7 +11760,7 @@ MA_API ma_result ma_spatializer_process_pcm_frames(ma_spatializer* pSpatializer, ...@@ -11682,7 +11760,7 @@ MA_API ma_result ma_spatializer_process_pcm_frames(ma_spatializer* pSpatializer,
} }
/* Convert to our output channel count. */ /* Convert to our output channel count. */
ma_convert_pcm_frames_channels_f32((float*)pFramesOut, channelsOut, pChannelMapOut, (const float*)pFramesIn, channelsIn, pChannelMapIn, frameCount); ma_channel_map_apply_f32((float*)pFramesOut, pChannelMapOut, channelsOut, (const float*)pFramesIn, pChannelMapIn, channelsIn, frameCount);
/* /*
Calculate our per-channel gains. We do this based on the normalized relative position of the sound and it's Calculate our per-channel gains. We do this based on the normalized relative position of the sound and it's
...@@ -11696,7 +11774,15 @@ MA_API ma_result ma_spatializer_process_pcm_frames(ma_spatializer* pSpatializer, ...@@ -11696,7 +11774,15 @@ MA_API ma_result ma_spatializer_process_pcm_frames(ma_spatializer* pSpatializer,
unitPos.z *= distanceInv; unitPos.z *= distanceInv;
for (iChannel = 0; iChannel < channelsOut; iChannel += 1) { for (iChannel = 0; iChannel < channelsOut; iChannel += 1) {
float d = ma_vec3f_dot(unitPos, g_maChannelDirections[pChannelMapOut[iChannel]]); ma_channel channelOut;
float d;
channelOut = ma_channel_map_get_channel(pChannelMapOut, channelsOut, iChannel);
if (ma_is_spatial_channel_position(channelOut)) {
d = ma_vec3f_dot(unitPos, ma_get_channel_direction(channelOut));
} else {
d = 1; /* It's not a spatial channel so there's no real notion of direction. */
}
/* /*
In my testing, if the panning effect is too aggressive it makes spatialization feel uncomfortable. In my testing, if the panning effect is too aggressive it makes spatialization feel uncomfortable.
...@@ -12239,7 +12325,7 @@ static void ma_engine_node_process_pcm_frames__general(ma_engine_node* pEngineNo ...@@ -12239,7 +12325,7 @@ static void ma_engine_node_process_pcm_frames__general(ma_engine_node* pEngineNo
ma_copy_pcm_frames(pRunningFramesOut, pWorkingBuffer, framesJustProcessedOut, ma_format_f32, channelsOut); ma_copy_pcm_frames(pRunningFramesOut, pWorkingBuffer, framesJustProcessedOut, ma_format_f32, channelsOut);
} else { } else {
/* Channel conversion required. TODO: Add support for channel maps here. */ /* Channel conversion required. TODO: Add support for channel maps here. */
ma_convert_pcm_frames_channels_f32(pRunningFramesOut, channelsOut, NULL, pWorkingBuffer, channelsIn, NULL, framesJustProcessedOut); ma_channel_map_apply_f32(pRunningFramesOut, NULL, channelsOut, pWorkingBuffer, NULL, channelsIn, framesJustProcessedOut);
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment