* BugFix: Correct convexDecomp compilation by setting the LINUX flag when necessary.

* BugFix: Update OpenAL to correct a compilation error on Linux.
This commit is contained in:
Robert MacGregor 2022-05-30 16:32:45 -04:00
parent e071f1d901
commit 7380161054
234 changed files with 30864 additions and 7523 deletions

View file

@ -28,6 +28,9 @@ static const struct {
#ifdef HAVE_JACK
{ "jack", "JACK" },
#endif
#ifdef HAVE_PIPEWIRE
{ "pipewire", "PipeWire" },
#endif
#ifdef HAVE_PULSEAUDIO
{ "pulse", "PulseAudio" },
#endif
@ -80,8 +83,7 @@ static const struct NameValuePair {
{ "Mono", "mono" },
{ "Stereo", "stereo" },
{ "Quadraphonic", "quad" },
{ "5.1 Surround (Side)", "surround51" },
{ "5.1 Surround (Rear)", "surround51rear" },
{ "5.1 Surround", "surround51" },
{ "6.1 Surround", "surround61" },
{ "7.1 Surround", "surround71" },
@ -122,18 +124,21 @@ static const struct NameValuePair {
{ "Default", "" },
{ "Pan Pot", "panpot" },
{ "UHJ", "uhj" },
{ "Binaural", "hrtf" },
{ "", "" }
}, ambiFormatList[] = {
{ "Default", "" },
{ "AmbiX (ACN, SN3D)", "ambix" },
{ "ACN, N3D", "acn+n3d" },
{ "Furse-Malham", "fuma" },
{ "ACN, N3D", "acn+n3d" },
{ "ACN, FuMa", "acn+fuma" },
{ "", "" }
}, hrtfModeList[] = {
{ "1st Order Ambisonic", "ambi1" },
{ "2nd Order Ambisonic", "ambi2" },
{ "3rd Order Ambisonic", "ambi3" },
{ "Default (Full)", "" },
{ "Full", "full" },
@ -200,7 +205,11 @@ static QStringList getAllDataPaths(const QString &append)
QString paths = qgetenv("XDG_DATA_DIRS");
if(paths.isEmpty())
paths = "/usr/local/share/:/usr/share/";
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
list += paths.split(QChar(':'), Qt::SkipEmptyParts);
#else
list += paths.split(QChar(':'), QString::SkipEmptyParts);
#endif
#endif
QStringList::iterator iter = list.begin();
while(iter != list.end())
@ -445,8 +454,11 @@ MainWindow::MainWindow(QWidget *parent) :
connect(ui->pulseFixRateCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
connect(ui->pulseAdjLatencyCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
connect(ui->pwireAssumeAudioCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
connect(ui->jackAutospawnCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
connect(ui->jackConnectPortsCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
connect(ui->jackRtMixCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
connect(ui->jackBufferSizeSlider, &QSlider::valueChanged, this, &MainWindow::updateJackBufferSizeEdit);
connect(ui->jackBufferSizeLine, &QLineEdit::editingFinished, this, &MainWindow::updateJackBufferSizeSlider);
@ -635,6 +647,8 @@ void MainWindow::loadConfig(const QString &fname)
ui->channelConfigCombo->setCurrentIndex(0);
if(channelconfig.isEmpty() == false)
{
if(channelconfig == "surround51rear")
channelconfig = "surround51";
QString str{getNameFromValue(speakerModeList, channelconfig)};
if(!str.isEmpty())
{
@ -735,8 +749,7 @@ void MainWindow::loadConfig(const QString &fname)
}
}
bool hqmode{settings.value("decoder/hq-mode", true).toBool()};
ui->decoderHQModeCheckBox->setChecked(hqmode);
ui->decoderHQModeCheckBox->setChecked(getCheckState(settings.value("decoder/hq-mode")));
ui->decoderDistCompCheckBox->setCheckState(getCheckState(settings.value("decoder/distance-comp")));
ui->decoderNFEffectsCheckBox->setCheckState(getCheckState(settings.value("decoder/nfc")));
double refdelay{settings.value("decoder/nfc-ref-delay", 0.0).toDouble()};
@ -760,11 +773,9 @@ void MainWindow::loadConfig(const QString &fname)
QString hrtfmode{settings.value("hrtf-mode").toString().trimmed()};
ui->hrtfmodeSlider->setValue(2);
ui->hrtfmodeLabel->setText(hrtfModeList[2].name);
/* The "basic" mode name is no longer supported, and "ambi3" is temporarily
* disabled. Use "ambi2" instead.
*/
if(hrtfmode == "basic" || hrtfmode == "ambi3")
ui->hrtfmodeLabel->setText(hrtfModeList[3].name);
/* The "basic" mode name is no longer supported. Use "ambi2" instead. */
if(hrtfmode == "basic")
hrtfmode = "ambi2";
for(int i = 0;hrtfModeList[i].name[0];i++)
{
@ -917,8 +928,12 @@ void MainWindow::loadConfig(const QString &fname)
ui->pulseFixRateCheckBox->setCheckState(getCheckState(settings.value("pulse/fix-rate")));
ui->pulseAdjLatencyCheckBox->setCheckState(getCheckState(settings.value("pulse/adjust-latency")));
ui->pwireAssumeAudioCheckBox->setCheckState(settings.value("pipewire/assume-audio").toBool()
? Qt::Checked : Qt::Unchecked);
ui->jackAutospawnCheckBox->setCheckState(getCheckState(settings.value("jack/spawn-server")));
ui->jackConnectPortsCheckBox->setCheckState(getCheckState(settings.value("jack/connect-ports")));
ui->jackRtMixCheckBox->setCheckState(getCheckState(settings.value("jack/rt-mix")));
ui->jackBufferSizeLine->setText(settings.value("jack/buffer-size", QString()).toString());
updateJackBufferSizeSlider();
@ -998,9 +1013,7 @@ void MainWindow::saveConfig(const QString &fname) const
settings.setValue("output-limiter", getCheckValue(ui->outputLimiterCheckBox));
settings.setValue("dither", getCheckValue(ui->outputDitherCheckBox));
settings.setValue("decoder/hq-mode",
ui->decoderHQModeCheckBox->isChecked() ? QString{/*"true"*/} : QString{"false"}
);
settings.setValue("decoder/hq-mode", getCheckValue(ui->decoderHQModeCheckBox));
settings.setValue("decoder/distance-comp", getCheckValue(ui->decoderDistCompCheckBox));
settings.setValue("decoder/nfc", getCheckValue(ui->decoderNFEffectsCheckBox));
double refdelay = ui->decoderNFRefDelaySpinBox->value();
@ -1127,8 +1140,12 @@ void MainWindow::saveConfig(const QString &fname) const
settings.setValue("pulse/fix-rate", getCheckValue(ui->pulseFixRateCheckBox));
settings.setValue("pulse/adjust-latency", getCheckValue(ui->pulseAdjLatencyCheckBox));
settings.setValue("pipewire/assume-audio", ui->pwireAssumeAudioCheckBox->isChecked()
? QString{"true"} : QString{/*"false"*/});
settings.setValue("jack/spawn-server", getCheckValue(ui->jackAutospawnCheckBox));
settings.setValue("jack/connect-ports", getCheckValue(ui->jackConnectPortsCheckBox));
settings.setValue("jack/rt-mix", getCheckValue(ui->jackRtMixCheckBox));
settings.setValue("jack/buffer-size", ui->jackBufferSizeLine->text());
settings.setValue("alsa/device", ui->alsaDefaultDeviceLine->text());

View file

@ -62,7 +62,7 @@
<rect>
<x>110</x>
<y>50</y>
<width>76</width>
<width>80</width>
<height>31</height>
</rect>
</property>
@ -111,7 +111,7 @@ float and converted to the output sample type as needed.</string>
<rect>
<x>110</x>
<y>20</y>
<width>76</width>
<width>80</width>
<height>31</height>
</rect>
</property>
@ -129,7 +129,7 @@ to stereo output.</string>
<rect>
<x>380</x>
<y>20</y>
<width>96</width>
<width>100</width>
<height>31</height>
</rect>
</property>
@ -437,12 +437,15 @@ frames needed for each mixing update.</string>
</property>
<property name="toolTip">
<string>Pan Pot uses standard amplitude panning (aka
pair-wise, stereo pair, etc) between -30 and +30
degrees, while UHJ creates a stereo-compatible
two-channel UHJ mix, which encodes some
surround sound information into stereo output
that can be decoded with a surround sound
receiver.</string>
pair-wise, stereo pair, etc).
UHJ creates a stereo-compatible two-channel
UHJ mix, which encodes some surround sound
information into stereo output that can be
decoded with a surround sound receiver.
Binaural applies HRTF filters to create a sense
of 3D space with headphones.</string>
</property>
</widget>
<widget class="QLabel" name="label_19">
@ -632,6 +635,9 @@ appropriate speaker configuration you intend to use.</string>
<property name="text">
<string>High Quality Mode:</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
<widget class="QCheckBox" name="decoderDistCompCheckBox">
<property name="geometry">
@ -1219,6 +1225,11 @@ application or system to determine if it should be used.</string>
<string>PulseAudio</string>
</property>
</item>
<item>
<property name="text">
<string>PipeWire</string>
</property>
</item>
<item>
<property name="text">
<string>JACK</string>
@ -1421,6 +1432,26 @@ drop-outs.</string>
</property>
</widget>
</widget>
<widget class="QWidget" name="page_8">
<widget class="QCheckBox" name="pwireAssumeAudioCheckBox">
<property name="geometry">
<rect>
<x>20</x>
<y>10</y>
<width>161</width>
<height>21</height>
</rect>
</property>
<property name="toolTip">
<string>Assumes PipeWire has support for audio, allowing
the backend to initialize even when no audio devices
are reported.</string>
</property>
<property name="text">
<string>Assume audio support</string>
</property>
</widget>
</widget>
<widget class="QWidget" name="page_7">
<widget class="QCheckBox" name="jackAutospawnCheckBox">
<property name="geometry">
@ -1442,7 +1473,7 @@ drop-outs.</string>
<property name="geometry">
<rect>
<x>10</x>
<y>70</y>
<y>110</y>
<width>401</width>
<height>80</height>
</rect>
@ -1450,7 +1481,8 @@ drop-outs.</string>
<property name="toolTip">
<string>The update buffer size, in samples, that the backend
will keep buffered to handle the server's real-time
processing requests. Must be a power of 2.</string>
processing requests. Must be a power of 2. Ignored
when Real-time Mixing is used.</string>
</property>
<property name="title">
<string>Buffer Size</string>
@ -1516,6 +1548,30 @@ processing requests. Must be a power of 2.</string>
<bool>true</bool>
</property>
</widget>
<widget class="QCheckBox" name="jackRtMixCheckBox">
<property name="geometry">
<rect>
<x>20</x>
<y>70</y>
<width>141</width>
<height>21</height>
</rect>
</property>
<property name="toolTip">
<string>Renders samples directly in the real-time
processing callback. This allows for lower
latency and less overall CPU utilization, but
can increase the risk of underruns when
increasing the amount of processing the
mixer needs to do.</string>
</property>
<property name="text">
<string>Real-time Mixing</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</widget>
<widget class="QWidget" name="page_3">
<widget class="QLabel" name="label_17">
@ -2317,7 +2373,7 @@ added by the ALC_EXT_DEDICATED extension.</string>
<rect>
<x>160</x>
<y>20</y>
<width>131</width>
<width>135</width>
<height>31</height>
</rect>
</property>

View file

@ -34,6 +34,7 @@
#include <memory>
#include <numeric>
#include <string>
#include <thread>
#include <vector>
#include "makemhr.h"

View file

@ -22,6 +22,7 @@
* THE SOFTWARE.
*/
#include <assert.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
@ -33,83 +34,17 @@
#include "win_main_utf8.h"
#ifndef ALC_ENUMERATE_ALL_EXT
#define ALC_DEFAULT_ALL_DEVICES_SPECIFIER 0x1012
#define ALC_ALL_DEVICES_SPECIFIER 0x1013
/* C doesn't allow casting between function and non-function pointer types, so
* with C99 we need to use a union to reinterpret the pointer type. Pre-C99
* still needs to use a normal cast and live with the warning (C++ is fine with
* a regular reinterpret_cast).
*/
#if __STDC_VERSION__ >= 199901L
#define FUNCTION_CAST(T, ptr) (union{void *p; T f;}){ptr}.f
#else
#define FUNCTION_CAST(T, ptr) (T)(ptr)
#endif
#ifndef ALC_EXT_EFX
#define ALC_EFX_MAJOR_VERSION 0x20001
#define ALC_EFX_MINOR_VERSION 0x20002
#define ALC_MAX_AUXILIARY_SENDS 0x20003
#endif
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
static WCHAR *FromUTF8(const char *str)
{
WCHAR *out = NULL;
int len;
if((len=MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0)) > 0)
{
out = calloc(sizeof(WCHAR), (unsigned int)(len));
MultiByteToWideChar(CP_UTF8, 0, str, -1, out, len);
}
return out;
}
/* Override printf, fprintf, and fwrite so we can print UTF-8 strings. */
static void al_fprintf(FILE *file, const char *fmt, ...)
{
char str[1024];
WCHAR *wstr;
va_list ap;
va_start(ap, fmt);
vsnprintf(str, sizeof(str), fmt, ap);
va_end(ap);
str[sizeof(str)-1] = 0;
wstr = FromUTF8(str);
if(!wstr)
fprintf(file, "<UTF-8 error> %s", str);
else
fprintf(file, "%ls", wstr);
free(wstr);
}
#define fprintf al_fprintf
#define printf(...) al_fprintf(stdout, __VA_ARGS__)
static size_t al_fwrite(const void *ptr, size_t size, size_t nmemb, FILE *file)
{
char str[1024];
WCHAR *wstr;
size_t len;
len = size * nmemb;
if(len > sizeof(str)-1)
len = sizeof(str)-1;
memcpy(str, ptr, len);
str[len] = 0;
wstr = FromUTF8(str);
if(!wstr)
fprintf(file, "<UTF-8 error> %s", str);
else
fprintf(file, "%ls", wstr);
free(wstr);
return len / size;
}
#define fwrite al_fwrite
#endif
#define MAX_WIDTH 80
static void printList(const char *list, char separator)
@ -227,7 +162,8 @@ static void printHRTFInfo(ALCdevice *device)
return;
}
alcGetStringiSOFT = (LPALCGETSTRINGISOFT)alcGetProcAddress(device, "alcGetStringiSOFT");
alcGetStringiSOFT = FUNCTION_CAST(LPALCGETSTRINGISOFT,
alcGetProcAddress(device, "alcGetStringiSOFT"));
alcGetIntegerv(device, ALC_NUM_HRTF_SPECIFIERS_SOFT, 1, &num_hrtfs);
if(!num_hrtfs)
@ -245,6 +181,34 @@ static void printHRTFInfo(ALCdevice *device)
checkALCErrors(device);
}
static void printModeInfo(ALCdevice *device)
{
if(alcIsExtensionPresent(device, "ALC_SOFT_output_mode"))
{
const char *modename = "(error)";
ALCenum mode = 0;
alcGetIntegerv(device, ALC_OUTPUT_MODE_SOFT, 1, &mode);
checkALCErrors(device);
switch(mode)
{
case ALC_ANY_SOFT: modename = "Unknown / unspecified"; break;
case ALC_MONO_SOFT: modename = "Mono"; break;
case ALC_STEREO_SOFT: modename = "Stereo (unspecified encoding)"; break;
case ALC_STEREO_BASIC_SOFT: modename = "Stereo (basic)"; break;
case ALC_STEREO_UHJ_SOFT: modename = "Stereo (UHJ)"; break;
case ALC_STEREO_HRTF_SOFT: modename = "Stereo (HRTF)"; break;
case ALC_QUAD_SOFT: modename = "Quadraphonic"; break;
case ALC_SURROUND_5_1_SOFT: modename = "5.1 Surround"; break;
case ALC_SURROUND_6_1_SOFT: modename = "6.1 Surround"; break;
case ALC_SURROUND_7_1_SOFT: modename = "7.1 Surround"; break;
}
printf("Output channel mode: %s\n", modename);
}
else
printf("Output mode extension not available\n");
}
static void printALInfo(void)
{
printf("OpenAL vendor string: %s\n", alGetString(AL_VENDOR));
@ -267,7 +231,7 @@ static void printResamplerInfo(void)
return;
}
alGetStringiSOFT = (LPALGETSTRINGISOFT)alGetProcAddress("alGetStringiSOFT");
alGetStringiSOFT = FUNCTION_CAST(LPALGETSTRINGISOFT, alGetProcAddress("alGetStringiSOFT"));
num_resamplers = alGetInteger(AL_NUM_RESAMPLERS_SOFT);
def_resampler = alGetInteger(AL_DEFAULT_RESAMPLER_SOFT);
@ -289,26 +253,35 @@ static void printResamplerInfo(void)
static void printEFXInfo(ALCdevice *device)
{
ALCint major, minor, sends;
static const ALchar filters[][32] = {
"AL_FILTER_LOWPASS", "AL_FILTER_HIGHPASS", "AL_FILTER_BANDPASS", ""
static LPALGENFILTERS palGenFilters;
static LPALDELETEFILTERS palDeleteFilters;
static LPALFILTERI palFilteri;
static LPALGENEFFECTS palGenEffects;
static LPALDELETEEFFECTS palDeleteEffects;
static LPALEFFECTI palEffecti;
static const ALint filters[] = {
AL_FILTER_LOWPASS, AL_FILTER_HIGHPASS, AL_FILTER_BANDPASS,
AL_FILTER_NULL
};
char filterNames[] = "Low-pass,High-pass,Band-pass,";
static const ALchar effects[][32] = {
"AL_EFFECT_EAXREVERB", "AL_EFFECT_REVERB", "AL_EFFECT_CHORUS",
"AL_EFFECT_DISTORTION", "AL_EFFECT_ECHO", "AL_EFFECT_FLANGER",
"AL_EFFECT_FREQUENCY_SHIFTER", "AL_EFFECT_VOCAL_MORPHER",
"AL_EFFECT_PITCH_SHIFTER", "AL_EFFECT_RING_MODULATOR",
"AL_EFFECT_AUTOWAH", "AL_EFFECT_COMPRESSOR", "AL_EFFECT_EQUALIZER", ""
static const ALint effects[] = {
AL_EFFECT_EAXREVERB, AL_EFFECT_REVERB, AL_EFFECT_CHORUS,
AL_EFFECT_DISTORTION, AL_EFFECT_ECHO, AL_EFFECT_FLANGER,
AL_EFFECT_FREQUENCY_SHIFTER, AL_EFFECT_VOCAL_MORPHER,
AL_EFFECT_PITCH_SHIFTER, AL_EFFECT_RING_MODULATOR,
AL_EFFECT_AUTOWAH, AL_EFFECT_COMPRESSOR, AL_EFFECT_EQUALIZER,
AL_EFFECT_NULL
};
static const ALchar dedeffects[][64] = {
"AL_EFFECT_DEDICATED_DIALOGUE",
"AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT", ""
static const ALint dedeffects[] = {
AL_EFFECT_DEDICATED_DIALOGUE, AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT,
AL_EFFECT_NULL
};
char effectNames[] = "EAX Reverb,Reverb,Chorus,Distortion,Echo,Flanger,"
"Frequency Shifter,Vocal Morpher,Pitch Shifter,"
"Ring Modulator,Autowah,Compressor,Equalizer,"
"Dedicated Dialog,Dedicated LFE,";
"Frequency Shifter,Vocal Morpher,Pitch Shifter,Ring Modulator,Autowah,"
"Compressor,Equalizer,Dedicated Dialog,Dedicated LFE,";
ALCint major, minor, sends;
ALuint object;
char *current;
int i;
@ -318,6 +291,13 @@ static void printEFXInfo(ALCdevice *device)
return;
}
palGenFilters = FUNCTION_CAST(LPALGENFILTERS, alGetProcAddress("alGenFilters"));
palDeleteFilters = FUNCTION_CAST(LPALDELETEFILTERS, alGetProcAddress("alDeleteFilters"));
palFilteri = FUNCTION_CAST(LPALFILTERI, alGetProcAddress("alFilteri"));
palGenEffects = FUNCTION_CAST(LPALGENEFFECTS, alGetProcAddress("alGenEffects"));
palDeleteEffects = FUNCTION_CAST(LPALDELETEEFFECTS, alGetProcAddress("alDeleteEffects"));
palEffecti = FUNCTION_CAST(LPALEFFECTI, alGetProcAddress("alEffecti"));
alcGetIntegerv(device, ALC_EFX_MAJOR_VERSION, 1, &major);
alcGetIntegerv(device, ALC_EFX_MINOR_VERSION, 1, &minor);
if(checkALCErrors(device) == ALC_NO_ERROR)
@ -326,14 +306,17 @@ static void printEFXInfo(ALCdevice *device)
if(checkALCErrors(device) == ALC_NO_ERROR)
printf("Max auxiliary sends: %d\n", sends);
palGenFilters(1, &object);
checkALErrors();
current = filterNames;
for(i = 0;filters[i][0];i++)
for(i = 0;filters[i] != AL_FILTER_NULL;i++)
{
char *next = strchr(current, ',');
ALenum val;
assert(next != NULL);
val = alGetEnumValue(filters[i]);
if(alGetError() != AL_NO_ERROR || val == 0 || val == -1)
palFilteri(object, AL_FILTER_TYPE, filters[i]);
if(alGetError() != AL_NO_ERROR)
memmove(current, next+1, strlen(next));
else
current = next+1;
@ -341,27 +324,31 @@ static void printEFXInfo(ALCdevice *device)
printf("Supported filters:");
printList(filterNames, ',');
palDeleteFilters(1, &object);
palGenEffects(1, &object);
checkALErrors();
current = effectNames;
for(i = 0;effects[i][0];i++)
for(i = 0;effects[i] != AL_EFFECT_NULL;i++)
{
char *next = strchr(current, ',');
ALenum val;
assert(next != NULL);
val = alGetEnumValue(effects[i]);
if(alGetError() != AL_NO_ERROR || val == 0 || val == -1)
palEffecti(object, AL_EFFECT_TYPE, effects[i]);
if(alGetError() != AL_NO_ERROR)
memmove(current, next+1, strlen(next));
else
current = next+1;
}
if(alcIsExtensionPresent(device, "ALC_EXT_DEDICATED"))
{
for(i = 0;dedeffects[i][0];i++)
for(i = 0;dedeffects[i] != AL_EFFECT_NULL;i++)
{
char *next = strchr(current, ',');
ALenum val;
assert(next != NULL);
val = alGetEnumValue(dedeffects[i]);
if(alGetError() != AL_NO_ERROR || val == 0 || val == -1)
palEffecti(object, AL_EFFECT_TYPE, dedeffects[i]);
if(alGetError() != AL_NO_ERROR)
memmove(current, next+1, strlen(next));
else
current = next+1;
@ -369,14 +356,18 @@ static void printEFXInfo(ALCdevice *device)
}
else
{
for(i = 0;dedeffects[i][0];i++)
for(i = 0;dedeffects[i] != AL_EFFECT_NULL;i++)
{
char *next = strchr(current, ',');
assert(next != NULL);
memmove(current, next+1, strlen(next));
}
}
printf("Supported effects:");
printList(effectNames, ',');
palDeleteEffects(1, &object);
checkALErrors();
}
int main(int argc, char *argv[])
@ -384,6 +375,11 @@ int main(int argc, char *argv[])
ALCdevice *device;
ALCcontext *context;
#ifdef _WIN32
/* OpenAL Soft gives UTF-8 strings, so set the console to expect that. */
SetConsoleOutputCP(CP_UTF8);
#endif
if(argc > 1 && (strcmp(argv[1], "--help") == 0 ||
strcmp(argv[1], "-h") == 0))
{
@ -429,6 +425,7 @@ int main(int argc, char *argv[])
return 1;
}
printModeInfo(device);
printALInfo();
printResamplerInfo();
printEFXInfo(device);

View file

@ -0,0 +1,538 @@
/*
* 2-channel UHJ Decoder
*
* Copyright (c) Chris Robinson <chris.kcat@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include "config.h"
#include <array>
#include <complex>
#include <cstring>
#include <memory>
#include <stddef.h>
#include <string>
#include <utility>
#include <vector>
#include "albit.h"
#include "albyte.h"
#include "alcomplex.h"
#include "almalloc.h"
#include "alnumbers.h"
#include "alspan.h"
#include "vector.h"
#include "opthelpers.h"
#include "phase_shifter.h"
#include "sndfile.h"
#include "win_main_utf8.h"
struct FileDeleter {
void operator()(FILE *file) { fclose(file); }
};
using FilePtr = std::unique_ptr<FILE,FileDeleter>;
struct SndFileDeleter {
void operator()(SNDFILE *sndfile) { sf_close(sndfile); }
};
using SndFilePtr = std::unique_ptr<SNDFILE,SndFileDeleter>;
using ubyte = unsigned char;
using ushort = unsigned short;
using uint = unsigned int;
using complex_d = std::complex<double>;
using byte4 = std::array<al::byte,4>;
constexpr ubyte SUBTYPE_BFORMAT_FLOAT[]{
0x03, 0x00, 0x00, 0x00, 0x21, 0x07, 0xd3, 0x11, 0x86, 0x44, 0xc8, 0xc1,
0xca, 0x00, 0x00, 0x00
};
void fwrite16le(ushort val, FILE *f)
{
ubyte data[2]{ static_cast<ubyte>(val&0xff), static_cast<ubyte>((val>>8)&0xff) };
fwrite(data, 1, 2, f);
}
void fwrite32le(uint val, FILE *f)
{
ubyte data[4]{ static_cast<ubyte>(val&0xff), static_cast<ubyte>((val>>8)&0xff),
static_cast<ubyte>((val>>16)&0xff), static_cast<ubyte>((val>>24)&0xff) };
fwrite(data, 1, 4, f);
}
template<al::endian = al::endian::native>
byte4 f32AsLEBytes(const float &value) = delete;
template<>
byte4 f32AsLEBytes<al::endian::little>(const float &value)
{
byte4 ret{};
std::memcpy(ret.data(), &value, 4);
return ret;
}
template<>
byte4 f32AsLEBytes<al::endian::big>(const float &value)
{
byte4 ret{};
std::memcpy(ret.data(), &value, 4);
std::swap(ret[0], ret[3]);
std::swap(ret[1], ret[2]);
return ret;
}
constexpr uint BufferLineSize{1024};
using FloatBufferLine = std::array<float,BufferLineSize>;
using FloatBufferSpan = al::span<float,BufferLineSize>;
struct UhjDecoder {
constexpr static size_t sFilterDelay{1024};
alignas(16) std::array<float,BufferLineSize+sFilterDelay> mS{};
alignas(16) std::array<float,BufferLineSize+sFilterDelay> mD{};
alignas(16) std::array<float,BufferLineSize+sFilterDelay> mT{};
alignas(16) std::array<float,BufferLineSize+sFilterDelay> mQ{};
/* History for the FIR filter. */
alignas(16) std::array<float,sFilterDelay-1> mDTHistory{};
alignas(16) std::array<float,sFilterDelay-1> mSHistory{};
alignas(16) std::array<float,BufferLineSize + sFilterDelay*2> mTemp{};
void decode(const float *RESTRICT InSamples, const size_t InChannels,
const al::span<FloatBufferLine> OutSamples, const size_t SamplesToDo);
void decode2(const float *RESTRICT InSamples, const al::span<FloatBufferLine,3> OutSamples,
const size_t SamplesToDo);
DEF_NEWDEL(UhjDecoder)
};
const PhaseShifterT<UhjDecoder::sFilterDelay*2> PShift{};
/* Decoding UHJ is done as:
*
* S = Left + Right
* D = Left - Right
*
* W = 0.981532*S + 0.197484*j(0.828331*D + 0.767820*T)
* X = 0.418496*S - j(0.828331*D + 0.767820*T)
* Y = 0.795968*D - 0.676392*T + j(0.186633*S)
* Z = 1.023332*Q
*
* 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
*
* S = (Left + Right)/2
* D = (Left - Right)/2
*
* However, this is incorrect. It's halving Left and Right even though they
* were already halved during encoding, causing S and D to be half what they
* initially were at the encoding stage. This division is not present in
* Gerzon's original paper for deriving Sigma (S) or Delta (D) from the L and R
* signals. As proof, taking Y for example:
*
* Y = 0.795968*D - 0.676392*T + j(0.186633*S)
*
* * Plug in the encoding parameters, using ? as a placeholder for whether S
* and D should receive an extra 0.5 factor
* Y = 0.795968*(j(-0.3420201*W + 0.5098604*X) + 0.6554516*Y)*? -
* 0.676392*(j(-0.1432*W + 0.6512*X) - 0.7071068*Y) +
* 0.186633*j(0.9396926*W + 0.1855740*X)*?
*
* * Move common factors in
* Y = (j(-0.3420201*0.795968*?*W + 0.5098604*0.795968*?*X) + 0.6554516*0.795968*?*Y) -
* (j(-0.1432*0.676392*W + 0.6512*0.676392*X) - 0.7071068*0.676392*Y) +
* j(0.9396926*0.186633*?*W + 0.1855740*0.186633*?*X)
*
* * Clean up extraneous groupings
* Y = j(-0.3420201*0.795968*?*W + 0.5098604*0.795968*?*X) + 0.6554516*0.795968*?*Y -
* j(-0.1432*0.676392*W + 0.6512*0.676392*X) + 0.7071068*0.676392*Y +
* j*(0.9396926*0.186633*?*W + 0.1855740*0.186633*?*X)
*
* * Move phase shifts together and combine them
* Y = j(-0.3420201*0.795968*?*W + 0.5098604*0.795968*?*X - -0.1432*0.676392*W -
* 0.6512*0.676392*X + 0.9396926*0.186633*?*W + 0.1855740*0.186633*?*X) +
* 0.6554516*0.795968*?*Y + 0.7071068*0.676392*Y
*
* * Reorder terms
* Y = j(-0.3420201*0.795968*?*W + 0.1432*0.676392*W + 0.9396926*0.186633*?*W +
* 0.5098604*0.795968*?*X + -0.6512*0.676392*X + 0.1855740*0.186633*?*X) +
* 0.7071068*0.676392*Y + 0.6554516*0.795968*?*Y
*
* * Move common factors out
* Y = j((-0.3420201*0.795968*? + 0.1432*0.676392 + 0.9396926*0.186633*?)*W +
* ( 0.5098604*0.795968*? + -0.6512*0.676392 + 0.1855740*0.186633*?)*X) +
* (0.7071068*0.676392 + 0.6554516*0.795968*?)*Y
*
* * Result w/ 0.5 factor:
* -0.3420201*0.795968*0.5 + 0.1432*0.676392 + 0.9396926*0.186633*0.5 = 0.04843*W
* 0.5098604*0.795968*0.5 + -0.6512*0.676392 + 0.1855740*0.186633*0.5 = -0.22023*X
* 0.7071068*0.676392 + 0.6554516*0.795968*0.5 = 0.73914*Y
* -> Y = j(0.04843*W + -0.22023*X) + 0.73914*Y
*
* * Result w/o 0.5 factor:
* -0.3420201*0.795968 + 0.1432*0.676392 + 0.9396926*0.186633 = 0.00000*W
* 0.5098604*0.795968 + -0.6512*0.676392 + 0.1855740*0.186633 = 0.00000*X
* 0.7071068*0.676392 + 0.6554516*0.795968 = 1.00000*Y
* -> Y = j(0.00000*W + 0.00000*X) + 1.00000*Y
*
* Not halving produces a result matching the original input.
*/
void UhjDecoder::decode(const float *RESTRICT InSamples, const size_t InChannels,
const al::span<FloatBufferLine> OutSamples, const size_t SamplesToDo)
{
ASSUME(SamplesToDo > 0);
float *woutput{OutSamples[0].data()};
float *xoutput{OutSamples[1].data()};
float *youtput{OutSamples[2].data()};
/* Add a delay to the input channels, to align it with the all-passed
* signal.
*/
/* S = Left + Right */
for(size_t i{0};i < SamplesToDo;++i)
mS[sFilterDelay+i] = InSamples[i*InChannels + 0] + InSamples[i*InChannels + 1];
/* D = Left - Right */
for(size_t i{0};i < SamplesToDo;++i)
mD[sFilterDelay+i] = InSamples[i*InChannels + 0] - InSamples[i*InChannels + 1];
if(InChannels > 2)
{
/* T */
for(size_t i{0};i < SamplesToDo;++i)
mT[sFilterDelay+i] = InSamples[i*InChannels + 2];
}
if(InChannels > 3)
{
/* Q */
for(size_t i{0};i < SamplesToDo;++i)
mQ[sFilterDelay+i] = InSamples[i*InChannels + 3];
}
/* Precompute j(0.828331*D + 0.767820*T) and store in xoutput. */
auto tmpiter = std::copy(mDTHistory.cbegin(), mDTHistory.cend(), mTemp.begin());
std::transform(mD.cbegin(), mD.cbegin()+SamplesToDo+sFilterDelay, mT.cbegin(), tmpiter,
[](const float d, const float t) noexcept { return 0.828331f*d + 0.767820f*t; });
std::copy_n(mTemp.cbegin()+SamplesToDo, mDTHistory.size(), mDTHistory.begin());
PShift.process({xoutput, SamplesToDo}, mTemp.data());
for(size_t i{0};i < SamplesToDo;++i)
{
/* W = 0.981532*S + 0.197484*j(0.828331*D + 0.767820*T) */
woutput[i] = 0.981532f*mS[i] + 0.197484f*xoutput[i];
/* X = 0.418496*S - j(0.828331*D + 0.767820*T) */
xoutput[i] = 0.418496f*mS[i] - xoutput[i];
}
/* Precompute j*S and store in youtput. */
tmpiter = std::copy(mSHistory.cbegin(), mSHistory.cend(), mTemp.begin());
std::copy_n(mS.cbegin(), SamplesToDo+sFilterDelay, tmpiter);
std::copy_n(mTemp.cbegin()+SamplesToDo, mSHistory.size(), mSHistory.begin());
PShift.process({youtput, SamplesToDo}, mTemp.data());
for(size_t i{0};i < SamplesToDo;++i)
{
/* Y = 0.795968*D - 0.676392*T + j(0.186633*S) */
youtput[i] = 0.795968f*mD[i] - 0.676392f*mT[i] + 0.186633f*youtput[i];
}
if(OutSamples.size() > 3)
{
float *zoutput{OutSamples[3].data()};
/* Z = 1.023332*Q */
for(size_t i{0};i < SamplesToDo;++i)
zoutput[i] = 1.023332f*mQ[i];
}
std::copy(mS.begin()+SamplesToDo, mS.begin()+SamplesToDo+sFilterDelay, mS.begin());
std::copy(mD.begin()+SamplesToDo, mD.begin()+SamplesToDo+sFilterDelay, mD.begin());
std::copy(mT.begin()+SamplesToDo, mT.begin()+SamplesToDo+sFilterDelay, mT.begin());
std::copy(mQ.begin()+SamplesToDo, mQ.begin()+SamplesToDo+sFilterDelay, mQ.begin());
}
/* This is an alternative equation for decoding 2-channel UHJ. Not sure what
* the intended benefit is over the above equation as this slightly reduces the
* amount of the original left response and has more of the phase-shifted
* forward response on the left response.
*
* This decoding is done as:
*
* S = Left + Right
* D = Left - Right
*
* W = 0.981530*S + j*0.163585*D
* X = 0.418504*S - j*0.828347*D
* Y = 0.762956*D + j*0.384230*S
*
* where j is a +90 degree phase shift.
*
* NOTE: As above, S and D should not be halved. The only consequence of
* halving here is merely a -6dB reduction in output, but it's still incorrect.
*/
void UhjDecoder::decode2(const float *RESTRICT InSamples,
const al::span<FloatBufferLine,3> OutSamples, const size_t SamplesToDo)
{
ASSUME(SamplesToDo > 0);
float *woutput{OutSamples[0].data()};
float *xoutput{OutSamples[1].data()};
float *youtput{OutSamples[2].data()};
/* S = Left + Right */
for(size_t i{0};i < SamplesToDo;++i)
mS[sFilterDelay+i] = InSamples[i*2 + 0] + InSamples[i*2 + 1];
/* D = Left - Right */
for(size_t i{0};i < SamplesToDo;++i)
mD[sFilterDelay+i] = InSamples[i*2 + 0] - InSamples[i*2 + 1];
/* Precompute j*D and store in xoutput. */
auto tmpiter = std::copy(mDTHistory.cbegin(), mDTHistory.cend(), mTemp.begin());
std::copy_n(mD.cbegin(), SamplesToDo+sFilterDelay, tmpiter);
std::copy_n(mTemp.cbegin()+SamplesToDo, mDTHistory.size(), mDTHistory.begin());
PShift.process({xoutput, SamplesToDo}, mTemp.data());
for(size_t i{0};i < SamplesToDo;++i)
{
/* W = 0.981530*S + j*0.163585*D */
woutput[i] = 0.981530f*mS[i] + 0.163585f*xoutput[i];
/* X = 0.418504*S - j*0.828347*D */
xoutput[i] = 0.418504f*mS[i] - 0.828347f*xoutput[i];
}
/* Precompute j*S and store in youtput. */
tmpiter = std::copy(mSHistory.cbegin(), mSHistory.cend(), mTemp.begin());
std::copy_n(mS.cbegin(), SamplesToDo+sFilterDelay, tmpiter);
std::copy_n(mTemp.cbegin()+SamplesToDo, mSHistory.size(), mSHistory.begin());
PShift.process({youtput, SamplesToDo}, mTemp.data());
for(size_t i{0};i < SamplesToDo;++i)
{
/* Y = 0.762956*D + j*0.384230*S */
youtput[i] = 0.762956f*mD[i] + 0.384230f*youtput[i];
}
std::copy(mS.begin()+SamplesToDo, mS.begin()+SamplesToDo+sFilterDelay, mS.begin());
std::copy(mD.begin()+SamplesToDo, mD.begin()+SamplesToDo+sFilterDelay, mD.begin());
}
int main(int argc, char **argv)
{
if(argc < 2 || std::strcmp(argv[1], "-h") == 0 || std::strcmp(argv[1], "--help") == 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;
}
size_t num_files{0}, num_decoded{0};
bool use_general{true};
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;
SF_INFO ininfo{};
SndFilePtr infile{sf_open(argv[fidx], SFM_READ, &ininfo)};
if(!infile)
{
fprintf(stderr, "Failed to open %s\n", argv[fidx]);
continue;
}
if(sf_command(infile.get(), SFC_WAVEX_GET_AMBISONIC, NULL, 0) == SF_AMBISONIC_B_FORMAT)
{
fprintf(stderr, "%s is already B-Format\n", argv[fidx]);
continue;
}
uint outchans{};
if(ininfo.channels == 2)
outchans = 3;
else if(ininfo.channels == 3 || ininfo.channels == 4)
outchans = static_cast<uint>(ininfo.channels);
else
{
fprintf(stderr, "%s is not a 2-, 3-, or 4-channel file\n", argv[fidx]);
continue;
}
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]};
auto lastslash = outname.find_last_of('/');
if(lastslash != std::string::npos)
outname.erase(0, lastslash+1);
auto lastdot = outname.find_last_of('.');
if(lastdot != std::string::npos)
outname.resize(lastdot+1);
outname += "amb";
FilePtr outfile{fopen(outname.c_str(), "wb")};
if(!outfile)
{
fprintf(stderr, "Failed to create %s\n", outname.c_str());
continue;
}
fputs("RIFF", outfile.get());
fwrite32le(0xFFFFFFFF, outfile.get()); // 'RIFF' header len; filled in at close
fputs("WAVE", outfile.get());
fputs("fmt ", outfile.get());
fwrite32le(40, outfile.get()); // 'fmt ' header len; 40 bytes for EXTENSIBLE
// 16-bit val, format type id (extensible: 0xFFFE)
fwrite16le(0xFFFE, outfile.get());
// 16-bit val, channel count
fwrite16le(static_cast<ushort>(outchans), outfile.get());
// 32-bit val, frequency
fwrite32le(static_cast<uint>(ininfo.samplerate), outfile.get());
// 32-bit val, bytes per second
fwrite32le(static_cast<uint>(ininfo.samplerate)*sizeof(float)*outchans, outfile.get());
// 16-bit val, frame size
fwrite16le(static_cast<ushort>(sizeof(float)*outchans), outfile.get());
// 16-bit val, bits per sample
fwrite16le(static_cast<ushort>(sizeof(float)*8), outfile.get());
// 16-bit val, extra byte count
fwrite16le(22, outfile.get());
// 16-bit val, valid bits per sample
fwrite16le(static_cast<ushort>(sizeof(float)*8), outfile.get());
// 32-bit val, channel mask
fwrite32le(0, outfile.get());
// 16 byte GUID, sub-type format
fwrite(SUBTYPE_BFORMAT_FLOAT, 1, 16, outfile.get());
fputs("data", outfile.get());
fwrite32le(0xFFFFFFFF, outfile.get()); // 'data' header len; filled in at close
if(ferror(outfile.get()))
{
fprintf(stderr, "Error writing wave file header: %s (%d)\n", strerror(errno), errno);
continue;
}
auto DataStart = ftell(outfile.get());
auto decoder = std::make_unique<UhjDecoder>();
auto inmem = std::make_unique<float[]>(BufferLineSize*static_cast<uint>(ininfo.channels));
auto decmem = al::vector<std::array<float,BufferLineSize>, 16>(outchans);
auto outmem = std::make_unique<byte4[]>(BufferLineSize*outchans);
/* A number of initial samples need to be skipped to cut the lead-in
* from the all-pass filter delay. The same number of samples need to
* be fed through the decoder after reaching the end of the input file
* to ensure none of the original input is lost.
*/
size_t LeadIn{UhjDecoder::sFilterDelay};
sf_count_t LeadOut{UhjDecoder::sFilterDelay};
while(LeadOut > 0)
{
sf_count_t sgot{sf_readf_float(infile.get(), inmem.get(), BufferLineSize)};
sgot = std::max<sf_count_t>(sgot, 0);
if(sgot < BufferLineSize)
{
const sf_count_t remaining{std::min(BufferLineSize - sgot, LeadOut)};
std::fill_n(inmem.get() + sgot*ininfo.channels, remaining*ininfo.channels, 0.0f);
sgot += remaining;
LeadOut -= remaining;
}
auto got = static_cast<size_t>(sgot);
if(ininfo.channels > 2 || use_general)
decoder->decode(inmem.get(), static_cast<uint>(ininfo.channels), decmem, got);
else
decoder->decode2(inmem.get(), decmem, got);
if(LeadIn >= got)
{
LeadIn -= got;
continue;
}
got -= LeadIn;
for(size_t i{0};i < got;++i)
{
/* Attenuate by -3dB for FuMa output levels. */
constexpr auto inv_sqrt2 = static_cast<float>(1.0/al::numbers::sqrt2);
for(size_t j{0};j < outchans;++j)
outmem[i*outchans + j] = f32AsLEBytes(decmem[j][LeadIn+i] * inv_sqrt2);
}
LeadIn = 0;
size_t wrote{fwrite(outmem.get(), sizeof(byte4)*outchans, got, outfile.get())};
if(wrote < got)
{
fprintf(stderr, "Error writing wave data: %s (%d)\n", strerror(errno), errno);
break;
}
}
auto DataEnd = ftell(outfile.get());
if(DataEnd > DataStart)
{
long dataLen{DataEnd - DataStart};
if(fseek(outfile.get(), 4, SEEK_SET) == 0)
fwrite32le(static_cast<uint>(DataEnd-8), outfile.get()); // 'WAVE' header len
if(fseek(outfile.get(), DataStart-4, SEEK_SET) == 0)
fwrite32le(static_cast<uint>(dataLen), outfile.get()); // 'data' header len
}
fflush(outfile.get());
++num_decoded;
}
if(num_decoded == 0)
fprintf(stderr, "Failed to decode any input files\n");
else if(num_decoded < num_files)
fprintf(stderr, "Decoded %zu of %zu files\n", num_decoded, num_files);
else
printf("Decoded %zu file%s\n", num_decoded, (num_decoded==1)?"":"s");
return 0;
}

View file

@ -0,0 +1,507 @@
/*
* 2-channel UHJ Encoder
*
* Copyright (c) Chris Robinson <chris.kcat@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include "config.h"
#include <array>
#include <cstring>
#include <inttypes.h>
#include <memory>
#include <stddef.h>
#include <string>
#include <utility>
#include <vector>
#include "almalloc.h"
#include "alnumbers.h"
#include "alspan.h"
#include "opthelpers.h"
#include "phase_shifter.h"
#include "vector.h"
#include "sndfile.h"
#include "win_main_utf8.h"
namespace {
struct SndFileDeleter {
void operator()(SNDFILE *sndfile) { sf_close(sndfile); }
};
using SndFilePtr = std::unique_ptr<SNDFILE,SndFileDeleter>;
using uint = unsigned int;
constexpr uint BufferLineSize{1024};
using FloatBufferLine = std::array<float,BufferLineSize>;
using FloatBufferSpan = al::span<float,BufferLineSize>;
struct UhjEncoder {
constexpr static size_t sFilterDelay{1024};
/* Delays and processing storage for the unfiltered signal. */
alignas(16) std::array<float,BufferLineSize+sFilterDelay> mS{};
alignas(16) std::array<float,BufferLineSize+sFilterDelay> mD{};
alignas(16) std::array<float,BufferLineSize+sFilterDelay> mT{};
alignas(16) std::array<float,BufferLineSize+sFilterDelay> mQ{};
/* History for the FIR filter. */
alignas(16) std::array<float,sFilterDelay*2 - 1> mWXHistory1{};
alignas(16) std::array<float,sFilterDelay*2 - 1> mWXHistory2{};
alignas(16) std::array<float,BufferLineSize + sFilterDelay*2> mTemp{};
void encode(const al::span<FloatBufferLine> OutSamples,
const al::span<FloatBufferLine,4> InSamples, const size_t SamplesToDo);
DEF_NEWDEL(UhjEncoder)
};
const PhaseShifterT<UhjEncoder::sFilterDelay*2> PShift{};
/* Encoding UHJ from B-Format is done as:
*
* S = 0.9396926*W + 0.1855740*X
* D = j(-0.3420201*W + 0.5098604*X) + 0.6554516*Y
*
* Left = (S + D)/2.0
* Right = (S - D)/2.0
* T = j(-0.1432*W + 0.6512*X) - 0.7071068*Y
* Q = 0.9772*Z
*
* where j is a wide-band +90 degree phase shift. T is excluded from 2-channel
* output, and Q is excluded from 2- and 3-channel output.
*/
void UhjEncoder::encode(const al::span<FloatBufferLine> OutSamples,
const al::span<FloatBufferLine,4> InSamples, const size_t SamplesToDo)
{
const float *RESTRICT winput{al::assume_aligned<16>(InSamples[0].data())};
const float *RESTRICT xinput{al::assume_aligned<16>(InSamples[1].data())};
const float *RESTRICT yinput{al::assume_aligned<16>(InSamples[2].data())};
const float *RESTRICT zinput{al::assume_aligned<16>(InSamples[3].data())};
/* Combine the previously delayed S/D signal with the input. */
/* S = 0.9396926*W + 0.1855740*X */
auto miditer = mS.begin() + sFilterDelay;
std::transform(winput, winput+SamplesToDo, xinput, miditer,
[](const float w, const float x) noexcept -> float
{ return 0.9396926f*w + 0.1855740f*x; });
/* D = 0.6554516*Y */
auto sideiter = mD.begin() + sFilterDelay;
std::transform(yinput, yinput+SamplesToDo, sideiter,
[](const float y) noexcept -> float { return 0.6554516f*y; });
/* D += j(-0.3420201*W + 0.5098604*X) */
auto tmpiter = std::copy(mWXHistory1.cbegin(), mWXHistory1.cend(), mTemp.begin());
std::transform(winput, winput+SamplesToDo, xinput, tmpiter,
[](const float w, const float x) noexcept -> float
{ return -0.3420201f*w + 0.5098604f*x; });
std::copy_n(mTemp.cbegin()+SamplesToDo, mWXHistory1.size(), mWXHistory1.begin());
PShift.processAccum({mD.data(), SamplesToDo}, mTemp.data());
/* Left = (S + D)/2.0 */
float *RESTRICT left{al::assume_aligned<16>(OutSamples[0].data())};
for(size_t i{0};i < SamplesToDo;i++)
left[i] = (mS[i] + mD[i]) * 0.5f;
/* Right = (S - D)/2.0 */
float *RESTRICT right{al::assume_aligned<16>(OutSamples[1].data())};
for(size_t i{0};i < SamplesToDo;i++)
right[i] = (mS[i] - mD[i]) * 0.5f;
if(OutSamples.size() > 2)
{
/* T = -0.7071068*Y */
sideiter = mT.begin() + sFilterDelay;
std::transform(yinput, yinput+SamplesToDo, sideiter,
[](const float y) noexcept -> float { return -0.7071068f*y; });
/* T += j(-0.1432*W + 0.6512*X) */
tmpiter = std::copy(mWXHistory2.cbegin(), mWXHistory2.cend(), mTemp.begin());
std::transform(winput, winput+SamplesToDo, xinput, tmpiter,
[](const float w, const float x) noexcept -> float
{ return -0.1432f*w + 0.6512f*x; });
std::copy_n(mTemp.cbegin()+SamplesToDo, mWXHistory2.size(), mWXHistory2.begin());
PShift.processAccum({mT.data(), SamplesToDo}, mTemp.data());
float *RESTRICT t{al::assume_aligned<16>(OutSamples[2].data())};
for(size_t i{0};i < SamplesToDo;i++)
t[i] = mT[i];
}
if(OutSamples.size() > 3)
{
/* Q = 0.9772*Z */
sideiter = mQ.begin() + sFilterDelay;
std::transform(zinput, zinput+SamplesToDo, sideiter,
[](const float z) noexcept -> float { return 0.9772f*z; });
float *RESTRICT q{al::assume_aligned<16>(OutSamples[3].data())};
for(size_t i{0};i < SamplesToDo;i++)
q[i] = mQ[i];
}
/* Copy the future samples to the front for next time. */
std::copy(mS.cbegin()+SamplesToDo, mS.cbegin()+SamplesToDo+sFilterDelay, mS.begin());
std::copy(mD.cbegin()+SamplesToDo, mD.cbegin()+SamplesToDo+sFilterDelay, mD.begin());
std::copy(mT.cbegin()+SamplesToDo, mT.cbegin()+SamplesToDo+sFilterDelay, mT.begin());
std::copy(mQ.cbegin()+SamplesToDo, mQ.cbegin()+SamplesToDo+sFilterDelay, mQ.begin());
}
struct SpeakerPos {
int mChannelID;
float mAzimuth;
float mElevation;
};
/* Azimuth is counter-clockwise. */
constexpr SpeakerPos StereoMap[2]{
{ SF_CHANNEL_MAP_LEFT, 30.0f, 0.0f },
{ SF_CHANNEL_MAP_RIGHT, -30.0f, 0.0f },
}, QuadMap[4]{
{ SF_CHANNEL_MAP_LEFT, 45.0f, 0.0f },
{ SF_CHANNEL_MAP_RIGHT, -45.0f, 0.0f },
{ SF_CHANNEL_MAP_REAR_LEFT, 135.0f, 0.0f },
{ SF_CHANNEL_MAP_REAR_RIGHT, -135.0f, 0.0f },
}, X51Map[6]{
{ SF_CHANNEL_MAP_LEFT, 30.0f, 0.0f },
{ SF_CHANNEL_MAP_RIGHT, -30.0f, 0.0f },
{ SF_CHANNEL_MAP_CENTER, 0.0f, 0.0f },
{ SF_CHANNEL_MAP_LFE, 0.0f, 0.0f },
{ SF_CHANNEL_MAP_SIDE_LEFT, 110.0f, 0.0f },
{ SF_CHANNEL_MAP_SIDE_RIGHT, -110.0f, 0.0f },
}, X51RearMap[6]{
{ SF_CHANNEL_MAP_LEFT, 30.0f, 0.0f },
{ SF_CHANNEL_MAP_RIGHT, -30.0f, 0.0f },
{ SF_CHANNEL_MAP_CENTER, 0.0f, 0.0f },
{ SF_CHANNEL_MAP_LFE, 0.0f, 0.0f },
{ SF_CHANNEL_MAP_REAR_LEFT, 110.0f, 0.0f },
{ SF_CHANNEL_MAP_REAR_RIGHT, -110.0f, 0.0f },
}, X71Map[8]{
{ SF_CHANNEL_MAP_LEFT, 30.0f, 0.0f },
{ SF_CHANNEL_MAP_RIGHT, -30.0f, 0.0f },
{ SF_CHANNEL_MAP_CENTER, 0.0f, 0.0f },
{ SF_CHANNEL_MAP_LFE, 0.0f, 0.0f },
{ SF_CHANNEL_MAP_REAR_LEFT, 150.0f, 0.0f },
{ SF_CHANNEL_MAP_REAR_RIGHT, -150.0f, 0.0f },
{ SF_CHANNEL_MAP_SIDE_LEFT, 90.0f, 0.0f },
{ SF_CHANNEL_MAP_SIDE_RIGHT, -90.0f, 0.0f },
}, X714Map[12]{
{ SF_CHANNEL_MAP_LEFT, 30.0f, 0.0f },
{ SF_CHANNEL_MAP_RIGHT, -30.0f, 0.0f },
{ SF_CHANNEL_MAP_CENTER, 0.0f, 0.0f },
{ SF_CHANNEL_MAP_LFE, 0.0f, 0.0f },
{ SF_CHANNEL_MAP_REAR_LEFT, 150.0f, 0.0f },
{ SF_CHANNEL_MAP_REAR_RIGHT, -150.0f, 0.0f },
{ SF_CHANNEL_MAP_SIDE_LEFT, 90.0f, 0.0f },
{ SF_CHANNEL_MAP_SIDE_RIGHT, -90.0f, 0.0f },
{ SF_CHANNEL_MAP_TOP_FRONT_LEFT, 45.0f, 35.0f },
{ SF_CHANNEL_MAP_TOP_FRONT_RIGHT, -45.0f, 35.0f },
{ SF_CHANNEL_MAP_TOP_REAR_LEFT, 135.0f, 35.0f },
{ SF_CHANNEL_MAP_TOP_REAR_RIGHT, -135.0f, 35.0f },
};
constexpr auto GenCoeffs(double x /*+front*/, double y /*+left*/, double z /*+up*/) noexcept
{
/* Coefficients are +3dB of FuMa. */
return std::array<float,4>{{
1.0f,
static_cast<float>(al::numbers::sqrt2 * x),
static_cast<float>(al::numbers::sqrt2 * y),
static_cast<float>(al::numbers::sqrt2 * z)
}};
}
} // namespace
int main(int argc, char **argv)
{
if(argc < 2 || std::strcmp(argv[1], "-h") == 0 || std::strcmp(argv[1], "--help") == 0)
{
printf("Usage: %s <infile...>\n\n", argv[0]);
return 1;
}
uint uhjchans{2};
size_t num_files{0}, num_encoded{0};
for(int fidx{1};fidx < argc;++fidx)
{
if(strcmp(argv[fidx], "-bhj") == 0)
{
uhjchans = 2;
continue;
}
if(strcmp(argv[fidx], "-thj") == 0)
{
uhjchans = 3;
continue;
}
if(strcmp(argv[fidx], "-phj") == 0)
{
uhjchans = 4;
continue;
}
++num_files;
std::string outname{argv[fidx]};
size_t lastslash{outname.find_last_of('/')};
if(lastslash != std::string::npos)
outname.erase(0, lastslash+1);
size_t extpos{outname.find_last_of('.')};
if(extpos != std::string::npos)
outname.resize(extpos);
outname += ".uhj.flac";
SF_INFO ininfo{};
SndFilePtr infile{sf_open(argv[fidx], SFM_READ, &ininfo)};
if(!infile)
{
fprintf(stderr, "Failed to open %s\n", argv[fidx]);
continue;
}
printf("Converting %s to %s...\n", argv[fidx], outname.c_str());
/* Work out the channel map, preferably using the actual channel map
* from the file/format, but falling back to assuming WFX order.
*
* TODO: Map indices when the channel order differs from the virtual
* speaker position maps.
*/
al::span<const SpeakerPos> spkrs;
auto chanmap = std::vector<int>(static_cast<uint>(ininfo.channels), SF_CHANNEL_MAP_INVALID);
if(sf_command(infile.get(), SFC_GET_CHANNEL_MAP_INFO, chanmap.data(),
ininfo.channels*int{sizeof(int)}) == SF_TRUE)
{
static const std::array<int,2> stereomap{{SF_CHANNEL_MAP_LEFT, SF_CHANNEL_MAP_RIGHT}};
static const std::array<int,4> quadmap{{SF_CHANNEL_MAP_LEFT, SF_CHANNEL_MAP_RIGHT,
SF_CHANNEL_MAP_REAR_LEFT, SF_CHANNEL_MAP_REAR_RIGHT}};
static const std::array<int,6> x51map{{SF_CHANNEL_MAP_LEFT, SF_CHANNEL_MAP_RIGHT,
SF_CHANNEL_MAP_CENTER, SF_CHANNEL_MAP_LFE,
SF_CHANNEL_MAP_SIDE_LEFT, SF_CHANNEL_MAP_SIDE_RIGHT}};
static const std::array<int,6> x51rearmap{{SF_CHANNEL_MAP_LEFT, SF_CHANNEL_MAP_RIGHT,
SF_CHANNEL_MAP_CENTER, SF_CHANNEL_MAP_LFE,
SF_CHANNEL_MAP_REAR_LEFT, SF_CHANNEL_MAP_REAR_RIGHT}};
static const std::array<int,8> x71map{{SF_CHANNEL_MAP_LEFT, SF_CHANNEL_MAP_RIGHT,
SF_CHANNEL_MAP_CENTER, SF_CHANNEL_MAP_LFE,
SF_CHANNEL_MAP_REAR_LEFT, SF_CHANNEL_MAP_REAR_RIGHT,
SF_CHANNEL_MAP_SIDE_LEFT, SF_CHANNEL_MAP_SIDE_RIGHT}};
static const std::array<int,12> x714map{{SF_CHANNEL_MAP_LEFT, SF_CHANNEL_MAP_RIGHT,
SF_CHANNEL_MAP_CENTER, SF_CHANNEL_MAP_LFE,
SF_CHANNEL_MAP_REAR_LEFT, SF_CHANNEL_MAP_REAR_RIGHT,
SF_CHANNEL_MAP_SIDE_LEFT, SF_CHANNEL_MAP_SIDE_RIGHT,
SF_CHANNEL_MAP_TOP_FRONT_LEFT, SF_CHANNEL_MAP_TOP_FRONT_RIGHT,
SF_CHANNEL_MAP_TOP_REAR_LEFT, SF_CHANNEL_MAP_TOP_REAR_RIGHT}};
static const std::array<int,3> ambi2dmap{{SF_CHANNEL_MAP_AMBISONIC_B_W,
SF_CHANNEL_MAP_AMBISONIC_B_X, SF_CHANNEL_MAP_AMBISONIC_B_Y}};
static const std::array<int,4> ambi3dmap{{SF_CHANNEL_MAP_AMBISONIC_B_W,
SF_CHANNEL_MAP_AMBISONIC_B_X, SF_CHANNEL_MAP_AMBISONIC_B_Y,
SF_CHANNEL_MAP_AMBISONIC_B_Z}};
auto match_chanmap = [](const al::span<int> a, const al::span<const int> b) -> bool
{
return a.size() == b.size()
&& std::mismatch(a.begin(), a.end(), b.begin(), b.end()).first == a.end();
};
if(match_chanmap(chanmap, stereomap))
spkrs = StereoMap;
else if(match_chanmap(chanmap, quadmap))
spkrs = QuadMap;
else if(match_chanmap(chanmap, x51map))
spkrs = X51Map;
else if(match_chanmap(chanmap, x51rearmap))
spkrs = X51RearMap;
else if(match_chanmap(chanmap, x71map))
spkrs = X71Map;
else if(match_chanmap(chanmap, x714map))
spkrs = X714Map;
else if(match_chanmap(chanmap, ambi2dmap) || match_chanmap(chanmap, ambi3dmap))
{
/* Do nothing. */
}
else
{
std::string mapstr;
if(chanmap.size() > 0)
{
mapstr = std::to_string(chanmap[0]);
for(int idx : al::span<int>{chanmap}.subspan<1>())
{
mapstr += ',';
mapstr += std::to_string(idx);
}
}
fprintf(stderr, " ... %zu channels not supported (map: %s)\n", chanmap.size(),
mapstr.c_str());
continue;
}
}
else if(ininfo.channels == 2)
{
fprintf(stderr, " ... assuming WFX order stereo\n");
spkrs = StereoMap;
}
else if(ininfo.channels == 6)
{
fprintf(stderr, " ... assuming WFX order 5.1\n");
spkrs = X51Map;
}
else if(ininfo.channels == 8)
{
fprintf(stderr, " ... assuming WFX order 7.1\n");
spkrs = X71Map;
}
else
{
fprintf(stderr, " ... unmapped %d-channel audio not supported\n", ininfo.channels);
continue;
}
SF_INFO outinfo{};
outinfo.frames = ininfo.frames;
outinfo.samplerate = ininfo.samplerate;
outinfo.channels = static_cast<int>(uhjchans);
outinfo.format = SF_FORMAT_PCM_24 | SF_FORMAT_FLAC;
SndFilePtr outfile{sf_open(outname.c_str(), SFM_WRITE, &outinfo)};
if(!outfile)
{
fprintf(stderr, " ... failed to create %s\n", outname.c_str());
continue;
}
auto encoder = std::make_unique<UhjEncoder>();
auto splbuf = al::vector<FloatBufferLine, 16>(static_cast<uint>(9+ininfo.channels)+uhjchans);
auto ambmem = al::span<FloatBufferLine,4>{&splbuf[0], 4};
auto encmem = al::span<FloatBufferLine,4>{&splbuf[4], 4};
auto srcmem = al::span<float,BufferLineSize>{splbuf[8].data(), BufferLineSize};
auto outmem = al::span<float>{splbuf[9].data(), BufferLineSize*uhjchans};
/* A number of initial samples need to be skipped to cut the lead-in
* from the all-pass filter delay. The same number of samples need to
* be fed through the encoder after reaching the end of the input file
* to ensure none of the original input is lost.
*/
size_t total_wrote{0};
size_t LeadIn{UhjEncoder::sFilterDelay};
sf_count_t LeadOut{UhjEncoder::sFilterDelay};
while(LeadIn > 0 || LeadOut > 0)
{
auto inmem = outmem.data() + outmem.size();
auto sgot = sf_readf_float(infile.get(), inmem, BufferLineSize);
sgot = std::max<sf_count_t>(sgot, 0);
if(sgot < BufferLineSize)
{
const sf_count_t remaining{std::min(BufferLineSize - sgot, LeadOut)};
std::fill_n(inmem + sgot*ininfo.channels, remaining*ininfo.channels, 0.0f);
sgot += remaining;
LeadOut -= remaining;
}
for(auto&& buf : ambmem)
buf.fill(0.0f);
auto got = static_cast<size_t>(sgot);
if(spkrs.empty())
{
/* B-Format is already in the correct order. It just needs a
* +3dB boost.
*/
constexpr float scale{al::numbers::sqrt2_v<float>};
const size_t chans{std::min<size_t>(static_cast<uint>(ininfo.channels), 4u)};
for(size_t c{0};c < chans;++c)
{
for(size_t i{0};i < got;++i)
ambmem[c][i] = inmem[i*static_cast<uint>(ininfo.channels)] * scale;
++inmem;
}
}
else for(auto&& spkr : spkrs)
{
/* Skip LFE. Or mix directly into W? Or W+X? */
if(spkr.mChannelID == SF_CHANNEL_MAP_LFE)
{
++inmem;
continue;
}
for(size_t i{0};i < got;++i)
srcmem[i] = inmem[i * static_cast<uint>(ininfo.channels)];
++inmem;
constexpr auto Deg2Rad = al::numbers::pi / 180.0;
const auto coeffs = GenCoeffs(
std::cos(spkr.mAzimuth*Deg2Rad) * std::cos(spkr.mElevation*Deg2Rad),
std::sin(spkr.mAzimuth*Deg2Rad) * std::cos(spkr.mElevation*Deg2Rad),
std::sin(spkr.mElevation*Deg2Rad));
for(size_t c{0};c < 4;++c)
{
for(size_t i{0};i < got;++i)
ambmem[c][i] += srcmem[i] * coeffs[c];
}
}
encoder->encode(encmem.subspan(0, uhjchans), ambmem, got);
if(LeadIn >= got)
{
LeadIn -= got;
continue;
}
got -= LeadIn;
for(size_t c{0};c < uhjchans;++c)
{
constexpr float max_val{8388607.0f / 8388608.0f};
auto clamp = [](float v, float mn, float mx) noexcept
{ return std::min(std::max(v, mn), mx); };
for(size_t i{0};i < got;++i)
outmem[i*uhjchans + c] = clamp(encmem[c][LeadIn+i], -1.0f, max_val);
}
LeadIn = 0;
sf_count_t wrote{sf_writef_float(outfile.get(), outmem.data(),
static_cast<sf_count_t>(got))};
if(wrote < 0)
fprintf(stderr, " ... failed to write samples: %d\n", sf_error(outfile.get()));
else
total_wrote += static_cast<size_t>(wrote);
}
printf(" ... wrote %zu samples (%" PRId64 ").\n", total_wrote, int64_t{ininfo.frames});
++num_encoded;
}
if(num_encoded == 0)
fprintf(stderr, "Failed to encode any input files\n");
else if(num_encoded < num_files)
fprintf(stderr, "Encoded %zu of %zu files\n", num_encoded, num_files);
else
printf("Encoded %s%zu file%s\n", (num_encoded > 1) ? "all " : "", num_encoded,
(num_encoded == 1) ? "" : "s");
return 0;
}