Add an option to change the UHJ decoder method

For 2-channel UHJ, two decoding equations are provided in the original paper.
The alternative one is most often referenced for 2-channel UHJ decoding, but
the original/general one can also be used by assuming T is fully attenuated
(which the format allows for, as T can be variably attenuated by a factor
between 0 and 1 to deal with an imperfect transmission medium).

Neither method can be perfect for 2-channel UHJ, it's irrevocably lossy to the
original source, but my subjective testing indicates the general equation
produces less audibly errant results.
This commit is contained in:
Chris Robinson 2021-03-26 02:29:13 -07:00
parent d681573803
commit f045694ce0

View File

@ -130,8 +130,8 @@ struct UhjDecoder {
alignas(16) std::array<float,BufferLineSize + sFilterSize*2> mTemp{}; alignas(16) std::array<float,BufferLineSize + sFilterSize*2> mTemp{};
void decode(const float *RESTRICT InSamples, const al::span<FloatBufferLine> OutSamples, void decode(const float *RESTRICT InSamples, const size_t InChannels,
const size_t SamplesToDo); const al::span<FloatBufferLine> OutSamples, const size_t SamplesToDo);
void decode2(const float *RESTRICT InSamples, const al::span<FloatBufferLine,3> OutSamples, void decode2(const float *RESTRICT InSamples, const al::span<FloatBufferLine,3> OutSamples,
const size_t SamplesToDo); const size_t SamplesToDo);
@ -307,7 +307,7 @@ void allpass_process(al::span<float> dst, const float *RESTRICT src)
} }
/* Decoding 3- and 4-channel UHJ is done as: /* Decoding UHJ is done as:
* *
* S = Left + Right * S = Left + Right
* D = Left - Right * D = Left - Right
@ -317,7 +317,10 @@ void allpass_process(al::span<float> dst, const float *RESTRICT src)
* Y = 0.795954*D - 0.676406*T + j(0.186626*S) * Y = 0.795954*D - 0.676406*T + j(0.186626*S)
* Z = 1.023332*Q * Z = 1.023332*Q
* *
* where j is a +90 degree phase shift. 3-channel UHJ excludes Q/Z. * where j is a +90 degree phase shift. 3-channel UHJ excludes Q, while 2-
* channel excludes Q and T. The B-Format signal reconstructed from 2-channel
* UHJ should not be run through a normal B-Format decoder, as it needs
* different shelf filters.
* *
* NOTE: Some sources specify * NOTE: Some sources specify
* *
@ -377,13 +380,11 @@ void allpass_process(al::span<float> dst, const float *RESTRICT src)
* *
* Not halving produces a result matching the original input. * Not halving produces a result matching the original input.
*/ */
void UhjDecoder::decode(const float *RESTRICT InSamples, void UhjDecoder::decode(const float *RESTRICT InSamples, const size_t InChannels,
const al::span<FloatBufferLine> OutSamples, const size_t SamplesToDo) const al::span<FloatBufferLine> OutSamples, const size_t SamplesToDo)
{ {
ASSUME(SamplesToDo > 0); ASSUME(SamplesToDo > 0);
const size_t Channels{OutSamples.size()};
float *woutput{OutSamples[0].data()}; float *woutput{OutSamples[0].data()};
float *xoutput{OutSamples[1].data()}; float *xoutput{OutSamples[1].data()};
float *youtput{OutSamples[2].data()}; float *youtput{OutSamples[2].data()};
@ -394,27 +395,29 @@ void UhjDecoder::decode(const float *RESTRICT InSamples,
/* S = Left + Right */ /* S = Left + Right */
for(size_t i{0};i < SamplesToDo;++i) for(size_t i{0};i < SamplesToDo;++i)
mS[sFilterSize+i] = InSamples[i*Channels + 0] + InSamples[i*Channels + 1]; mS[sFilterSize+i] = InSamples[i*InChannels + 0] + InSamples[i*InChannels + 1];
/* D = Left - Right */ /* D = Left - Right */
for(size_t i{0};i < SamplesToDo;++i) for(size_t i{0};i < SamplesToDo;++i)
mD[sFilterSize+i] = InSamples[i*Channels + 0] - InSamples[i*Channels + 1]; mD[sFilterSize+i] = InSamples[i*InChannels + 0] - InSamples[i*InChannels + 1];
/* T */ if(InChannels > 2)
for(size_t i{0};i < SamplesToDo;++i) {
mT[sFilterSize+i] = InSamples[i*Channels + 2]; /* T */
for(size_t i{0};i < SamplesToDo;++i)
if(Channels > 3) mT[sFilterSize+i] = InSamples[i*InChannels + 2];
}
if(InChannels > 3)
{ {
/* Q */ /* Q */
for(size_t i{0};i < SamplesToDo;++i) for(size_t i{0};i < SamplesToDo;++i)
mQ[sFilterSize+i] = InSamples[i*Channels + 3]; mQ[sFilterSize+i] = InSamples[i*InChannels + 3];
} }
/* Precompute j(0.828347*D + 0.767835*T) and store in xoutput. */ /* Precompute j(0.828347*D + 0.767835*T) and store in xoutput. */
auto tmpiter = std::copy(mDTHistory.cbegin(), mDTHistory.cend(), mTemp.begin()); auto tmpiter = std::copy(mDTHistory.cbegin(), mDTHistory.cend(), mTemp.begin());
std::transform(mD.cbegin(), mD.cbegin()+SamplesToDo+sFilterSize, mT.cbegin(), tmpiter, std::transform(mD.cbegin(), mD.cbegin()+SamplesToDo+sFilterSize, mT.cbegin(), tmpiter,
[](const float D, const float T) noexcept { return 0.828347f*D + 0.767835f*T; }); [](const float d, const float t) noexcept { return 0.828347f*d + 0.767835f*t; });
std::copy_n(mTemp.cbegin()+SamplesToDo, mDTHistory.size(), mDTHistory.begin()); std::copy_n(mTemp.cbegin()+SamplesToDo, mDTHistory.size(), mDTHistory.begin());
allpass_process({xoutput, SamplesToDo}, mTemp.data()); allpass_process({xoutput, SamplesToDo}, mTemp.data());
@ -438,7 +441,7 @@ void UhjDecoder::decode(const float *RESTRICT InSamples,
youtput[i] = 0.795954f*mD[i] - 0.676406f*mT[i] + 0.186626f*youtput[i]; youtput[i] = 0.795954f*mD[i] - 0.676406f*mT[i] + 0.186626f*youtput[i];
} }
if(Channels > 3) if(OutSamples.size() > 3)
{ {
float *zoutput{OutSamples[3].data()}; float *zoutput{OutSamples[3].data()};
/* Z = 1.023332*Q */ /* Z = 1.023332*Q */
@ -452,12 +455,12 @@ void UhjDecoder::decode(const float *RESTRICT InSamples,
std::copy(mQ.begin()+SamplesToDo, mQ.begin()+SamplesToDo+sFilterSize, mQ.begin()); std::copy(mQ.begin()+SamplesToDo, mQ.begin()+SamplesToDo+sFilterSize, mQ.begin());
} }
/* There is a difference with decoding 2-channel UHJ compared to 3-channel, due /* This is an alternative equation for decoding 2-channel UHJ. Not sure what
* to 2-channel having lost some of the original signal. The B-Format signal * the intended benefit is over the above equation as this slightly reduces the
* reconstructed from 2-channel UHJ should not be run through a normal B-Format * amount of the original left response and has more of the phase-shifted
* decoder, as it needs different shelf filters. * forward response on the left response.
* *
* 2-channel UHJ decoding is done as: * This decoding is done as:
* *
* S = Left + Right * S = Left + Right
* D = Left - Right * D = Left - Right
@ -523,13 +526,32 @@ int main(int argc, char **argv)
{ {
if(argc < 2 || std::strcmp(argv[1], "-h") == 0 || std::strcmp(argv[1], "--help") == 0) if(argc < 2 || std::strcmp(argv[1], "-h") == 0 || std::strcmp(argv[1], "--help") == 0)
{ {
printf("Usage: %s <filename.wav...>\n", argv[0]); printf("Usage: %s <[options] filename.wav...>\n\n"
" Options:\n"
" --general Use the general equations for 2-channel UHJ (default).\n"
" --alternative Use the alternative equations for 2-channel UHJ.\n"
"\n"
"Note: When decoding 2-channel UHJ to an .amb file, the result should not use\n"
"the normal B-Format shelf filters! Only 3- and 4-channel UHJ can accurately\n"
"reconstruct the original B-Format signal.",
argv[0]);
return 1; return 1;
} }
size_t num_files{0}, num_decoded{0}; size_t num_files{0}, num_decoded{0};
bool use_general{true};
for(int fidx{1};fidx < argc;++fidx) for(int fidx{1};fidx < argc;++fidx)
{ {
if(std::strcmp(argv[fidx], "--general") == 0)
{
use_general = true;
continue;
}
if(std::strcmp(argv[fidx], "--alternative") == 0)
{
use_general = false;
continue;
}
++num_files; ++num_files;
SF_INFO ininfo{}; SF_INFO ininfo{};
SndFilePtr infile{sf_open(argv[fidx], SFM_READ, &ininfo)}; SndFilePtr infile{sf_open(argv[fidx], SFM_READ, &ininfo)};
@ -553,7 +575,8 @@ int main(int argc, char **argv)
fprintf(stderr, "%s is not a 2-, 3-, or 4-channel file\n", argv[fidx]); fprintf(stderr, "%s is not a 2-, 3-, or 4-channel file\n", argv[fidx]);
continue; continue;
} }
printf("Converting %s from %d-channel UHJ...\n", argv[fidx], ininfo.channels); printf("Converting %s from %d-channel UHJ%s...\n", argv[fidx], ininfo.channels,
(ininfo.channels == 2) ? use_general ? " (general)" : " (alternative)" : "");
std::string outname{argv[fidx]}; std::string outname{argv[fidx]};
auto lastslash = outname.find_last_of('/'); auto lastslash = outname.find_last_of('/');
@ -634,10 +657,10 @@ int main(int argc, char **argv)
} }
auto got = static_cast<size_t>(sgot); auto got = static_cast<size_t>(sgot);
if(ininfo.channels == 2) if(ininfo.channels > 2 || use_general)
decoder->decode(inmem.get(), static_cast<uint>(ininfo.channels), decmem, got);
else
decoder->decode2(inmem.get(), decmem, got); decoder->decode2(inmem.get(), decmem, got);
else if(ininfo.channels == 3 || ininfo.channels == 4)
decoder->decode(inmem.get(), decmem, got);
for(size_t i{0};i < got;++i) for(size_t i{0};i < got;++i)
{ {
for(size_t j{0};j < outchans;++j) for(size_t j{0};j < outchans;++j)