mirror of
https://github.com/mackron/miniaudio.git
synced 2026-04-23 08:44:04 +02:00
Experimental optimzations to mono/stereo channel conversion.
This commit is contained in:
+52
@@ -864,6 +864,8 @@ struct ma_channel_router
|
|||||||
ma_channel_router_config config;
|
ma_channel_router_config config;
|
||||||
ma_bool32 isPassthrough : 1;
|
ma_bool32 isPassthrough : 1;
|
||||||
ma_bool32 isSimpleShuffle : 1;
|
ma_bool32 isSimpleShuffle : 1;
|
||||||
|
ma_bool32 isSimpleMonoExpansion : 1;
|
||||||
|
ma_bool32 isStereoToMono : 1;
|
||||||
ma_bool32 useSSE2 : 1;
|
ma_bool32 useSSE2 : 1;
|
||||||
ma_bool32 useAVX2 : 1;
|
ma_bool32 useAVX2 : 1;
|
||||||
ma_bool32 useAVX512 : 1;
|
ma_bool32 useAVX512 : 1;
|
||||||
@@ -29605,6 +29607,31 @@ ma_result ma_channel_router_init(const ma_channel_router_config* pConfig, ma_cha
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
We can use a simple case for expanding the mono channel. This will when expanding a mono input into any output so long
|
||||||
|
as no LFE is present in the output.
|
||||||
|
*/
|
||||||
|
if (!pRouter->isPassthrough) {
|
||||||
|
if (pRouter->config.channelsIn == 1 && pRouter->config.channelMapIn[0] == MA_CHANNEL_MONO) {
|
||||||
|
/* Optimal case if no LFE is in the output channel map. */
|
||||||
|
pRouter->isSimpleMonoExpansion = MA_TRUE;
|
||||||
|
if (ma_channel_map_contains_channel_position(pRouter->config.channelsOut, pRouter->config.channelMapOut, MA_CHANNEL_LFE)) {
|
||||||
|
pRouter->isSimpleMonoExpansion = MA_FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Another optimized case is stereo to mono. */
|
||||||
|
if (!pRouter->isPassthrough) {
|
||||||
|
if (pRouter->config.channelsOut == 1 && pRouter->config.channelMapOut[0] == MA_CHANNEL_MONO && pRouter->config.channelsIn == 2) {
|
||||||
|
/* Optimal case if no LFE is in the input channel map. */
|
||||||
|
pRouter->isStereoToMono = MA_TRUE;
|
||||||
|
if (ma_channel_map_contains_channel_position(pRouter->config.channelsIn, pRouter->config.channelMapIn, MA_CHANNEL_LFE)) {
|
||||||
|
pRouter->isStereoToMono = MA_FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Here is where we do a bit of pre-processing to know how each channel should be combined to make up the output. Rules:
|
Here is where we do a bit of pre-processing to know how each channel should be combined to make up the output. Rules:
|
||||||
|
|
||||||
@@ -29824,6 +29851,31 @@ void ma_channel_router__do_routing(ma_channel_router* pRouter, ma_uint64 frameCo
|
|||||||
iChannelOut = pRouter->shuffleTable[iChannelIn];
|
iChannelOut = pRouter->shuffleTable[iChannelIn];
|
||||||
ma_copy_memory_64(ppSamplesOut[iChannelOut], ppSamplesIn[iChannelIn], frameCount * sizeof(float));
|
ma_copy_memory_64(ppSamplesOut[iChannelOut], ppSamplesIn[iChannelIn], frameCount * sizeof(float));
|
||||||
}
|
}
|
||||||
|
} else if (pRouter->isSimpleMonoExpansion) {
|
||||||
|
/* Simple case for expanding from mono. */
|
||||||
|
if (pRouter->config.channelsOut == 2) {
|
||||||
|
ma_uint64 iFrame;
|
||||||
|
for (iFrame = 0; iFrame < frameCount; ++iFrame) {
|
||||||
|
ppSamplesOut[0][iFrame] = ppSamplesIn[0][iFrame];
|
||||||
|
ppSamplesOut[1][iFrame] = ppSamplesIn[0][iFrame];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (iChannelOut = 0; iChannelOut < pRouter->config.channelsOut; ++iChannelOut) {
|
||||||
|
ma_uint64 iFrame;
|
||||||
|
for (iFrame = 0; iFrame < frameCount; ++iFrame) {
|
||||||
|
ppSamplesOut[iChannelOut][iFrame] = ppSamplesIn[0][iFrame];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (pRouter->isStereoToMono) {
|
||||||
|
/* Simple case for going from stereo to mono. */
|
||||||
|
ma_assert(pRouter->config.channelsIn == 2);
|
||||||
|
ma_assert(pRouter->config.channelsOut == 1);
|
||||||
|
|
||||||
|
ma_uint64 iFrame;
|
||||||
|
for (iFrame = 0; iFrame < frameCount; ++iFrame) {
|
||||||
|
ppSamplesOut[0][iFrame] = (ppSamplesIn[0][iFrame] + ppSamplesIn[1][iFrame]) * 0.5f;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
/* This is the more complicated case. Each of the output channels is accumulated with 0 or more input channels. */
|
/* This is the more complicated case. Each of the output channels is accumulated with 0 or more input channels. */
|
||||||
|
|
||||||
|
|||||||
@@ -1672,6 +1672,222 @@ int do_channel_routing_tests()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
printf("Simple Mono Expansion (Mono -> Stereo)... ");
|
||||||
|
{
|
||||||
|
// The simple mono expansion case will be activated when a mono channel map is converted to anything without an LFE.
|
||||||
|
ma_channel_router_config routerConfig;
|
||||||
|
ma_zero_object(&routerConfig);
|
||||||
|
routerConfig.onReadDeinterleaved = channel_router_callback__passthrough_test;
|
||||||
|
routerConfig.pUserData = NULL;
|
||||||
|
routerConfig.mixingMode = ma_channel_mix_mode_planar_blend;
|
||||||
|
routerConfig.channelsIn = 1;
|
||||||
|
routerConfig.channelsOut = 2;
|
||||||
|
routerConfig.noSSE2 = MA_TRUE;
|
||||||
|
routerConfig.noAVX2 = MA_TRUE;
|
||||||
|
routerConfig.noAVX512 = MA_TRUE;
|
||||||
|
routerConfig.noNEON = MA_TRUE;
|
||||||
|
ma_get_standard_channel_map(ma_standard_channel_map_microsoft, routerConfig.channelsIn, routerConfig.channelMapIn);
|
||||||
|
ma_get_standard_channel_map(ma_standard_channel_map_microsoft, routerConfig.channelsOut, routerConfig.channelMapOut);
|
||||||
|
|
||||||
|
ma_channel_router router;
|
||||||
|
ma_result result = ma_channel_router_init(&routerConfig, &router);
|
||||||
|
if (result == MA_SUCCESS) {
|
||||||
|
if (router.isPassthrough) {
|
||||||
|
printf("Router incorrectly configured as a passthrough.\n");
|
||||||
|
hasError = MA_TRUE;
|
||||||
|
}
|
||||||
|
if (router.isSimpleShuffle) {
|
||||||
|
printf("Router incorrectly configured as a simple shuffle.\n");
|
||||||
|
hasError = MA_TRUE;
|
||||||
|
}
|
||||||
|
if (!router.isSimpleMonoExpansion) {
|
||||||
|
printf("Router not configured as simple mono expansion.\n");
|
||||||
|
hasError = MA_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expecting the weights to all be equal to 1 for each channel.
|
||||||
|
for (ma_uint32 iChannelIn = 0; iChannelIn < routerConfig.channelsIn; ++iChannelIn) {
|
||||||
|
for (ma_uint32 iChannelOut = 0; iChannelOut < routerConfig.channelsOut; ++iChannelOut) {
|
||||||
|
float expectedWeight = 1;
|
||||||
|
|
||||||
|
if (router.config.weights[iChannelIn][iChannelOut] != expectedWeight) {
|
||||||
|
printf("Failed. Channel weight incorrect: %f\n", expectedWeight);
|
||||||
|
hasError = MA_TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printf("Failed to init router.\n");
|
||||||
|
hasError = MA_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Here is where we check that the shuffle optimization works correctly. What we do is compare the output of the shuffle
|
||||||
|
// optimization with the non-shuffle output. We don't use a real sound here, but instead use values that makes it easier
|
||||||
|
// for us to check results. Each channel is given a value equal to it's index, plus 1.
|
||||||
|
float testData[MA_MAX_CHANNELS][100];
|
||||||
|
float* ppTestData[MA_MAX_CHANNELS];
|
||||||
|
for (ma_uint32 iChannel = 0; iChannel < routerConfig.channelsIn; ++iChannel) {
|
||||||
|
ppTestData[iChannel] = testData[iChannel];
|
||||||
|
for (ma_uint32 iFrame = 0; iFrame < 100; ++iFrame) {
|
||||||
|
ppTestData[iChannel][iFrame] = (float)(iChannel + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
routerConfig.pUserData = ppTestData;
|
||||||
|
ma_channel_router_init(&routerConfig, &router);
|
||||||
|
|
||||||
|
float outputA[MA_MAX_CHANNELS][100];
|
||||||
|
float outputB[MA_MAX_CHANNELS][100];
|
||||||
|
float* ppOutputA[MA_MAX_CHANNELS];
|
||||||
|
float* ppOutputB[MA_MAX_CHANNELS];
|
||||||
|
for (ma_uint32 iChannel = 0; iChannel < routerConfig.channelsOut; ++iChannel) {
|
||||||
|
ppOutputA[iChannel] = outputA[iChannel];
|
||||||
|
ppOutputB[iChannel] = outputB[iChannel];
|
||||||
|
}
|
||||||
|
|
||||||
|
// With optimizations.
|
||||||
|
ma_uint64 framesRead = ma_channel_router_read_deinterleaved(&router, 100, (void**)ppOutputA, router.config.pUserData);
|
||||||
|
if (framesRead != 100) {
|
||||||
|
printf("Returned frame count for optimized incorrect.");
|
||||||
|
hasError = MA_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without optimizations.
|
||||||
|
router.isPassthrough = MA_FALSE;
|
||||||
|
router.isSimpleShuffle = MA_FALSE;
|
||||||
|
framesRead = ma_channel_router_read_deinterleaved(&router, 100, (void**)ppOutputB, router.config.pUserData);
|
||||||
|
if (framesRead != 100) {
|
||||||
|
printf("Returned frame count for unoptimized path incorrect.");
|
||||||
|
hasError = MA_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare.
|
||||||
|
for (ma_uint32 iChannel = 0; iChannel < routerConfig.channelsOut; ++iChannel) {
|
||||||
|
for (ma_uint32 iFrame = 0; iFrame < 100; ++iFrame) {
|
||||||
|
if (ppOutputA[iChannel][iFrame] != ppOutputB[iChannel][iFrame]) {
|
||||||
|
printf("Sample incorrect [%d][%d]\n", iChannel, iFrame);
|
||||||
|
hasError = MA_TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!hasError) {
|
||||||
|
printf("PASSED\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Simple Stereo to Mono... ");
|
||||||
|
{
|
||||||
|
// The simple mono expansion case will be activated when a mono channel map is converted to anything without an LFE.
|
||||||
|
ma_channel_router_config routerConfig;
|
||||||
|
ma_zero_object(&routerConfig);
|
||||||
|
routerConfig.onReadDeinterleaved = channel_router_callback__passthrough_test;
|
||||||
|
routerConfig.pUserData = NULL;
|
||||||
|
routerConfig.mixingMode = ma_channel_mix_mode_planar_blend;
|
||||||
|
routerConfig.channelsIn = 2;
|
||||||
|
routerConfig.channelsOut = 1;
|
||||||
|
routerConfig.noSSE2 = MA_TRUE;
|
||||||
|
routerConfig.noAVX2 = MA_TRUE;
|
||||||
|
routerConfig.noAVX512 = MA_TRUE;
|
||||||
|
routerConfig.noNEON = MA_TRUE;
|
||||||
|
ma_get_standard_channel_map(ma_standard_channel_map_microsoft, routerConfig.channelsIn, routerConfig.channelMapIn);
|
||||||
|
ma_get_standard_channel_map(ma_standard_channel_map_microsoft, routerConfig.channelsOut, routerConfig.channelMapOut);
|
||||||
|
|
||||||
|
ma_channel_router router;
|
||||||
|
ma_result result = ma_channel_router_init(&routerConfig, &router);
|
||||||
|
if (result == MA_SUCCESS) {
|
||||||
|
if (router.isPassthrough) {
|
||||||
|
printf("Router incorrectly configured as a passthrough.\n");
|
||||||
|
hasError = MA_TRUE;
|
||||||
|
}
|
||||||
|
if (router.isSimpleShuffle) {
|
||||||
|
printf("Router incorrectly configured as a simple shuffle.\n");
|
||||||
|
hasError = MA_TRUE;
|
||||||
|
}
|
||||||
|
if (router.isSimpleMonoExpansion) {
|
||||||
|
printf("Router incorrectly configured as simple mono expansion.\n");
|
||||||
|
hasError = MA_TRUE;
|
||||||
|
}
|
||||||
|
if (!router.isStereoToMono) {
|
||||||
|
printf("Router not configured as stereo to mono.\n");
|
||||||
|
hasError = MA_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expecting the weights to all be equal to 1 for each channel.
|
||||||
|
for (ma_uint32 iChannelIn = 0; iChannelIn < routerConfig.channelsIn; ++iChannelIn) {
|
||||||
|
for (ma_uint32 iChannelOut = 0; iChannelOut < routerConfig.channelsOut; ++iChannelOut) {
|
||||||
|
float expectedWeight = 0.5f;
|
||||||
|
|
||||||
|
if (router.config.weights[iChannelIn][iChannelOut] != expectedWeight) {
|
||||||
|
printf("Failed. Channel weight incorrect: %f\n", expectedWeight);
|
||||||
|
hasError = MA_TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printf("Failed to init router.\n");
|
||||||
|
hasError = MA_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Here is where we check that the shuffle optimization works correctly. What we do is compare the output of the shuffle
|
||||||
|
// optimization with the non-shuffle output. We don't use a real sound here, but instead use values that makes it easier
|
||||||
|
// for us to check results. Each channel is given a value equal to it's index, plus 1.
|
||||||
|
float testData[MA_MAX_CHANNELS][100];
|
||||||
|
float* ppTestData[MA_MAX_CHANNELS];
|
||||||
|
for (ma_uint32 iChannel = 0; iChannel < routerConfig.channelsIn; ++iChannel) {
|
||||||
|
ppTestData[iChannel] = testData[iChannel];
|
||||||
|
for (ma_uint32 iFrame = 0; iFrame < 100; ++iFrame) {
|
||||||
|
ppTestData[iChannel][iFrame] = (float)(iChannel + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
routerConfig.pUserData = ppTestData;
|
||||||
|
ma_channel_router_init(&routerConfig, &router);
|
||||||
|
|
||||||
|
float outputA[MA_MAX_CHANNELS][100];
|
||||||
|
float outputB[MA_MAX_CHANNELS][100];
|
||||||
|
float* ppOutputA[MA_MAX_CHANNELS];
|
||||||
|
float* ppOutputB[MA_MAX_CHANNELS];
|
||||||
|
for (ma_uint32 iChannel = 0; iChannel < routerConfig.channelsOut; ++iChannel) {
|
||||||
|
ppOutputA[iChannel] = outputA[iChannel];
|
||||||
|
ppOutputB[iChannel] = outputB[iChannel];
|
||||||
|
}
|
||||||
|
|
||||||
|
// With optimizations.
|
||||||
|
ma_uint64 framesRead = ma_channel_router_read_deinterleaved(&router, 100, (void**)ppOutputA, router.config.pUserData);
|
||||||
|
if (framesRead != 100) {
|
||||||
|
printf("Returned frame count for optimized incorrect.");
|
||||||
|
hasError = MA_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without optimizations.
|
||||||
|
router.isPassthrough = MA_FALSE;
|
||||||
|
router.isSimpleShuffle = MA_FALSE;
|
||||||
|
framesRead = ma_channel_router_read_deinterleaved(&router, 100, (void**)ppOutputB, router.config.pUserData);
|
||||||
|
if (framesRead != 100) {
|
||||||
|
printf("Returned frame count for unoptimized path incorrect.");
|
||||||
|
hasError = MA_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare.
|
||||||
|
for (ma_uint32 iChannel = 0; iChannel < routerConfig.channelsOut; ++iChannel) {
|
||||||
|
for (ma_uint32 iFrame = 0; iFrame < 100; ++iFrame) {
|
||||||
|
if (ppOutputA[iChannel][iFrame] != ppOutputB[iChannel][iFrame]) {
|
||||||
|
printf("Sample incorrect [%d][%d]\n", iChannel, iFrame);
|
||||||
|
hasError = MA_TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!hasError) {
|
||||||
|
printf("PASSED\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
printf("Simple Conversion (Stereo -> 5.1)... ");
|
printf("Simple Conversion (Stereo -> 5.1)... ");
|
||||||
{
|
{
|
||||||
// This tests takes a Stereo to 5.1 conversion using the simple mixing mode. We should expect 0 and 1 (front/left, front/right) to have
|
// This tests takes a Stereo to 5.1 conversion using the simple mixing mode. We should expect 0 and 1 (front/left, front/right) to have
|
||||||
|
|||||||
+12
-12
@@ -295,12 +295,12 @@
|
|||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include="..\examples\simple_loopback.c">
|
<ClCompile Include="..\examples\simple_loopback.c">
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">false</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">false</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">false</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">true</ExcludedFromBuild>
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">false</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">true</ExcludedFromBuild>
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">false</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">false</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include="..\examples\simple_mixing.c">
|
<ClCompile Include="..\examples\simple_mixing.c">
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
|
||||||
@@ -383,12 +383,12 @@
|
|||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include="ma_test_0.c">
|
<ClCompile Include="ma_test_0.c">
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">false</ExcludedFromBuild>
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">true</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">false</ExcludedFromBuild>
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">false</ExcludedFromBuild>
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">true</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">false</ExcludedFromBuild>
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">false</ExcludedFromBuild>
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">false</ExcludedFromBuild>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include="ma_test_0.cpp">
|
<ClCompile Include="ma_test_0.cpp">
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
|
||||||
|
|||||||
Reference in New Issue
Block a user