diff options
Diffstat (limited to 'utils')
-rw-r--r-- | utils/CIAIR.def | 4 | ||||
-rw-r--r-- | utils/IRC_1005.def | 4 | ||||
-rw-r--r-- | utils/MIT_KEMAR.def | 10 | ||||
-rw-r--r-- | utils/MIT_KEMAR_sofa.def | 51 | ||||
-rw-r--r-- | utils/SCUT_KEMAR.def | 48 | ||||
-rw-r--r-- | utils/alsoft-config/CMakeLists.txt | 4 | ||||
-rw-r--r-- | utils/alsoft-config/mainwindow.cpp | 653 | ||||
-rw-r--r-- | utils/alsoft-config/mainwindow.h | 1 | ||||
-rw-r--r-- | utils/alsoft-config/mainwindow.ui | 293 | ||||
-rw-r--r-- | utils/alsoft-config/verstr.cpp | 10 | ||||
-rw-r--r-- | utils/alsoft-config/verstr.h | 8 | ||||
-rw-r--r-- | utils/bsincgen.c | 404 | ||||
-rw-r--r-- | utils/makehrtf.c | 3455 | ||||
-rw-r--r-- | utils/makemhr/loaddef.cpp | 2033 | ||||
-rw-r--r-- | utils/makemhr/loaddef.h | 13 | ||||
-rw-r--r-- | utils/makemhr/loadsofa.cpp | 668 | ||||
-rw-r--r-- | utils/makemhr/loadsofa.h | 10 | ||||
-rw-r--r-- | utils/makemhr/makemhr.cpp | 1797 | ||||
-rw-r--r-- | utils/makemhr/makemhr.h | 128 | ||||
-rw-r--r-- | utils/openal-info.c | 8 | ||||
-rw-r--r-- | utils/sofa-info.cpp | 371 |
21 files changed, 5700 insertions, 4273 deletions
diff --git a/utils/CIAIR.def b/utils/CIAIR.def index 4876dc50..5fabdb3f 100644 --- a/utils/CIAIR.def +++ b/utils/CIAIR.def @@ -1,5 +1,5 @@ -# This is a makehrtf HRIR definition file. It is used to define the layout -# and source data to be processed into an OpenAL Soft compatible HRTF. +# This is a makemhr HRIR definition file. It is used to define the layout and +# source data to be processed into an OpenAL Soft compatible HRTF. # # This definition is used to transform the left and right ear HRIRs from a # data set used in several papers and articles by Fumitada Itakura, Kazuya diff --git a/utils/IRC_1005.def b/utils/IRC_1005.def index c2fd90b5..c91e7b4d 100644 --- a/utils/IRC_1005.def +++ b/utils/IRC_1005.def @@ -1,5 +1,5 @@ -# This is a makehrtf HRIR definition file. It is used to define the layout -# and source data to be processed into an OpenAL Soft compatible HRTF. +# This is a makemhr HRIR definition file. It is used to define the layout and +# source data to be processed into an OpenAL Soft compatible HRTF. # # This definition is used to transform the left and right ear HRIRs of any # raw data set from the IRCAM/AKG Listen HRTF database. diff --git a/utils/MIT_KEMAR.def b/utils/MIT_KEMAR.def index e6b0ddff..15036d9b 100644 --- a/utils/MIT_KEMAR.def +++ b/utils/MIT_KEMAR.def @@ -1,5 +1,5 @@ -# This is a makehrtf HRIR definition file. It is used to define the layout -# and source data to be processed into an OpenAL Soft compatible HRTF. +# This is a makemhr HRIR definition file. It is used to define the layout and +# source data to be processed into an OpenAL Soft compatible HRTF. # # This definition is used to transform the left ear HRIRs from the full set # of KEMAR HRIRs provided by Bill Gardner <[email protected]> and Keith @@ -33,9 +33,9 @@ type = mono points = 512 # The radius of the listener's head (measured ear-to-ear in meters). The -# makehrtf utility uses this value to rescale measured propagation delays -# when a custom head radius is specified on the command line. It is also -# used as the default radius when the spherical model is used to calculate an +# makemhr utility uses this value to rescale measured propagation delays when +# a custom head radius is specified on the command line. It is also used as +# the default radius when the spherical model is used to calculate an # approximate set of delays. This should match the data set as close as # possible for accurate rescaling when using the measured delays (the # default). At the moment, radius rescaling does not adjust HRIR coupling. diff --git a/utils/MIT_KEMAR_sofa.def b/utils/MIT_KEMAR_sofa.def new file mode 100644 index 00000000..5f25815f --- /dev/null +++ b/utils/MIT_KEMAR_sofa.def @@ -0,0 +1,51 @@ +# This is a makemhr HRIR definition file. It is used to define the layout and +# source data to be processed into an OpenAL Soft compatible HRTF. +# +# This definition is used to transform the SOFA packaged KEMAR HRIRs +# originally provided by Bill Gardner <[email protected]> and Keith Martin +# <[email protected]> of MIT Media Laboratory. +# +# The SOFA conversion is available from: +# +# http://sofacoustics.org/data/database/mit/ +# +# The original data is available from: +# +# http://sound.media.mit.edu/resources/KEMAR.html +# +# It is copyrighted 1994 by MIT Media Laboratory, and provided free of charge +# with no restrictions on use so long as the authors (above) are cited. + +# Sampling rate of the HRIR data (in hertz). +rate = 44100 + +# The SOFA file is stereo, but the original data was mono. Channels are just +# mirrored by azimuth; so save some memory by allowing OpenAL Soft to mirror +# them at run time. +type = mono + +points = 512 + +radius = 0.09 + +# The MIT set has only one field with a distance of 1.4m. +distance = 1.4 + +# The MIT set varies the number of azimuths for each elevation to maintain +# an average distance between them. +azimuths = 1, 12, 24, 36, 45, 56, 60, 72, 72, 72, 72, 72, 60, 56, 45, 36, 24, 12, 1 + +# Normally the dataset would be composed manually by listing all necessary +# 'sofa' sources with the appropriate radius, elevation, azimuth (counter- +# clockwise for SOFA files) and receiver arguments: +# +# [ 5, 0 ] = sofa (1.4, -40.0, 0.0 : 0) : "./mit_kemar_normal_pinna.sofa" +# [ 5, 1 ] = sofa (1.4, -40.0, 353.6 : 0) : "./mit_kemar_normal_pinna.sofa" +# [ 5, 2 ] = sofa (1.4, -40.0, 347.1 : 0) : "./mit_kemar_normal_pinna.sofa" +# [ 5, 3 ] = sofa (1.4, -40.0, 340.7 : 0) : "./mit_kemar_normal_pinna.sofa" +# ... +# +# If HRIR composition isn't necessary, it's easier to just use the following: + +[ * ] = sofa : "./mit_kemar_normal_pinna.sofa" mono + diff --git a/utils/SCUT_KEMAR.def b/utils/SCUT_KEMAR.def new file mode 100644 index 00000000..e5ae4ff8 --- /dev/null +++ b/utils/SCUT_KEMAR.def @@ -0,0 +1,48 @@ +# This is a makemhr HRIR definition file. It is used to define the layout and +# source data to be processed into an OpenAL Soft compatible HRTF. +# +# This definition is used to transform the near-field KEMAR HRIRs provided by +# Bosun Xie <[email protected]> of the South China University of +# Technology, Guangzhou, China; and converted from SCUT to SOFA format by +# Piotr Majdak <[email protected]> of the Acoustics Research Institute, +# Austrian Academy of Sciences. +# +# A copy of the data (SCUT_KEMAR_radius_all.sofa) is available from: +# +# http://sofacoustics.org/data/database/scut/SCUT_KEMAR_radius_all.sofa +# +# It is provided under the Creative Commons CC 3.0 BY-SA-NC license: +# +# https://creativecommons.org/licenses/by-nc-sa/3.0/ + +rate = 44100 + +# While the SOFA file is stereo, doubling the size of the data set will cause +# the utility to exhaust its address space if compiled 32-bit. Since the +# dummy head is symmetric, the same results (ignoring variations caused by +# measurement error) can be obtained using mono channel processing. +type = mono + +points = 512 + +radius = 0.09 + +# This data set has 10 fields ranging from 0.2m to 1m. The layout was +# obtained using the sofa-info utility. +distance = 0.2, 0.25, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0 + +azimuths = 1, 24, 36, 72, 72, 72, 72, 72, 72, 72, 36, 24, 1; + 1, 24, 36, 72, 72, 72, 72, 72, 72, 72, 36, 24, 1; + 1, 24, 36, 72, 72, 72, 72, 72, 72, 72, 36, 24, 1; + 1, 24, 36, 72, 72, 72, 72, 72, 72, 72, 36, 24, 1; + 1, 24, 36, 72, 72, 72, 72, 72, 72, 72, 36, 24, 1; + 1, 24, 36, 72, 72, 72, 72, 72, 72, 72, 36, 24, 1; + 1, 24, 36, 72, 72, 72, 72, 72, 72, 72, 36, 24, 1; + 1, 24, 36, 72, 72, 72, 72, 72, 72, 72, 36, 24, 1; + 1, 24, 36, 72, 72, 72, 72, 72, 72, 72, 36, 24, 1; + 1, 24, 36, 72, 72, 72, 72, 72, 72, 72, 36, 24, 1 + +# Given the above compatible layout, we can automatically process the entire +# data set. +[ * ] = sofa : "./SCUT_KEMAR_radius_all.sofa" mono + diff --git a/utils/alsoft-config/CMakeLists.txt b/utils/alsoft-config/CMakeLists.txt index 67cc44c7..68b9e9de 100644 --- a/utils/alsoft-config/CMakeLists.txt +++ b/utils/alsoft-config/CMakeLists.txt @@ -8,6 +8,8 @@ set(alsoft-config_SRCS main.cpp mainwindow.cpp mainwindow.h + verstr.cpp + verstr.h ) set(alsoft-config_UIS mainwindow.ui) set(alsoft-config_MOCS mainwindow.h) @@ -20,6 +22,7 @@ if(Qt5Widgets_FOUND AND NOT ALSOFT_NO_QT5) add_executable(alsoft-config ${alsoft-config_SRCS} ${UIS} ${RSCS} ${TRS} ${MOCS}) target_link_libraries(alsoft-config Qt5::Widgets) + target_include_directories(alsoft-config PRIVATE "${OpenAL_BINARY_DIR}") set_property(TARGET alsoft-config APPEND PROPERTY COMPILE_FLAGS ${EXTRA_CFLAGS}) set_target_properties(alsoft-config PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${OpenAL_BINARY_DIR}) if(TARGET build_version) @@ -43,6 +46,7 @@ else() add_executable(alsoft-config ${alsoft-config_SRCS} ${UIS} ${RSCS} ${TRS} ${MOCS}) target_link_libraries(alsoft-config ${QT_LIBRARIES}) + target_include_directories(alsoft-config PRIVATE "${OpenAL_BINARY_DIR}") set_property(TARGET alsoft-config APPEND PROPERTY COMPILE_FLAGS ${EXTRA_CFLAGS}) set_target_properties(alsoft-config PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${OpenAL_BINARY_DIR}) if(TARGET build_version) diff --git a/utils/alsoft-config/mainwindow.cpp b/utils/alsoft-config/mainwindow.cpp index 110fe4ed..fed9de59 100644 --- a/utils/alsoft-config/mainwindow.cpp +++ b/utils/alsoft-config/mainwindow.cpp @@ -1,7 +1,7 @@ #include "config.h" -#include "version.h" +#include "mainwindow.h" #include <iostream> #include <cmath> @@ -11,8 +11,13 @@ #include <QCloseEvent> #include <QSettings> #include <QtGlobal> -#include "mainwindow.h" #include "ui_mainwindow.h" +#include "verstr.h" + +#ifdef _WIN32 +#include <windows.h> +#include <shlobj.h> +#endif namespace { @@ -101,7 +106,9 @@ static const struct NameValuePair { { "Linear", "linear" }, { "Default (Linear)", "" }, { "Cubic Spline", "cubic" }, + { "11th order Sinc (fast)", "fast_bsinc12" }, { "11th order Sinc", "bsinc12" }, + { "23rd order Sinc (fast)", "fast_bsinc24" }, { "23rd order Sinc", "bsinc24" }, { "", "" } @@ -119,18 +126,32 @@ static const struct NameValuePair { { "", "" } }, ambiFormatList[] = { { "Default", "" }, - { "ACN + SN3D", "acn+sn3d" }, - { "ACN + N3D", "acn+n3d" }, + { "AmbiX (ACN, SN3D)", "ambix" }, + { "ACN, N3D", "acn+n3d" }, { "Furse-Malham", "fuma" }, { "", "" } +}, hrtfModeList[] = { + { "1st Order Ambisonic", "ambi1" }, + { "2nd Order Ambisonic", "ambi2" }, + { "Default (Full)", "" }, + { "Full", "full" }, + + { "", "" } }; static QString getDefaultConfigName() { #ifdef Q_OS_WIN32 static const char fname[] = "alsoft.ini"; - QByteArray base = qgetenv("AppData"); + auto get_appdata_path = []() noexcept -> QString + { + WCHAR buffer[MAX_PATH]; + if(SHGetSpecialFolderPathW(nullptr, buffer, CSIDL_APPDATA, FALSE) != FALSE) + return QString::fromWCharArray(buffer); + return QString(); + }; + QString base = get_appdata_path(); #else static const char fname[] = "alsoft.conf"; QByteArray base = qgetenv("XDG_CONFIG_HOME"); @@ -149,7 +170,14 @@ static QString getDefaultConfigName() static QString getBaseDataPath() { #ifdef Q_OS_WIN32 - QByteArray base = qgetenv("AppData"); + auto get_appdata_path = []() noexcept -> QString + { + WCHAR buffer[MAX_PATH]; + if(SHGetSpecialFolderPathW(nullptr, buffer, CSIDL_APPDATA, FALSE) != FALSE) + return QString::fromWCharArray(buffer); + return QString(); + }; + QString base = get_appdata_path(); #else QByteArray base = qgetenv("XDG_DATA_HOME"); if(base.isEmpty()) @@ -162,7 +190,7 @@ static QString getBaseDataPath() return base; } -static QStringList getAllDataPaths(QString append=QString()) +static QStringList getAllDataPaths(const QString &append) { QStringList list; list.append(getBaseDataPath()); @@ -196,7 +224,7 @@ static QString getValueFromName(const NameValuePair (&list)[N], const QString &s if(str == list[i].name) return list[i].value; } - return QString(); + return QString{}; } template<size_t N> @@ -207,7 +235,27 @@ static QString getNameFromValue(const NameValuePair (&list)[N], const QString &s if(str == list[i].value) return list[i].name; } - return QString(); + return QString{}; +} + + +Qt::CheckState getCheckState(const QVariant &var) +{ + if(var.isNull()) + return Qt::PartiallyChecked; + if(var.toBool()) + return Qt::Checked; + return Qt::Unchecked; +} + +QString getCheckValue(const QCheckBox *checkbox) +{ + const Qt::CheckState state{checkbox->checkState()}; + if(state == Qt::Checked) + return QString{"true"}; + if(state == Qt::Unchecked) + return QString{"false"}; + return QString{}; } } @@ -215,13 +263,13 @@ static QString getNameFromValue(const NameValuePair (&list)[N], const QString &s MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow), - mPeriodSizeValidator(NULL), - mPeriodCountValidator(NULL), - mSourceCountValidator(NULL), - mEffectSlotValidator(NULL), - mSourceSendValidator(NULL), - mSampleRateValidator(NULL), - mJackBufferValidator(NULL), + mPeriodSizeValidator(nullptr), + mPeriodCountValidator(nullptr), + mSourceCountValidator(nullptr), + mEffectSlotValidator(nullptr), + mSourceSendValidator(nullptr), + mSampleRateValidator(nullptr), + mJackBufferValidator(nullptr), mNeedsSave(false) { ui->setupUi(this); @@ -247,6 +295,9 @@ MainWindow::MainWindow(QWidget *parent) : } ui->resamplerSlider->setRange(0, count-1); + for(count = 0;hrtfModeList[count].name[0];count++) { + } + ui->hrtfmodeSlider->setRange(0, count-1); ui->hrtfStateComboBox->adjustSize(); #if !defined(HAVE_NEON) && !defined(HAVE_SSE) @@ -289,125 +340,131 @@ MainWindow::MainWindow(QWidget *parent) : #endif - mPeriodSizeValidator = new QIntValidator(64, 8192, this); + mPeriodSizeValidator = new QIntValidator{64, 8192, this}; ui->periodSizeEdit->setValidator(mPeriodSizeValidator); - mPeriodCountValidator = new QIntValidator(2, 16, this); + mPeriodCountValidator = new QIntValidator{2, 16, this}; ui->periodCountEdit->setValidator(mPeriodCountValidator); - mSourceCountValidator = new QIntValidator(0, 4096, this); + mSourceCountValidator = new QIntValidator{0, 4096, this}; ui->srcCountLineEdit->setValidator(mSourceCountValidator); - mEffectSlotValidator = new QIntValidator(0, 64, this); + mEffectSlotValidator = new QIntValidator{0, 64, this}; ui->effectSlotLineEdit->setValidator(mEffectSlotValidator); - mSourceSendValidator = new QIntValidator(0, 16, this); + mSourceSendValidator = new QIntValidator{0, 16, this}; ui->srcSendLineEdit->setValidator(mSourceSendValidator); - mSampleRateValidator = new QIntValidator(8000, 192000, this); + mSampleRateValidator = new QIntValidator{8000, 192000, this}; ui->sampleRateCombo->lineEdit()->setValidator(mSampleRateValidator); - mJackBufferValidator = new QIntValidator(0, 8192, this); + mJackBufferValidator = new QIntValidator{0, 8192, this}; ui->jackBufferSizeLine->setValidator(mJackBufferValidator); - connect(ui->actionLoad, SIGNAL(triggered()), this, SLOT(loadConfigFromFile())); - connect(ui->actionSave_As, SIGNAL(triggered()), this, SLOT(saveConfigAsFile())); - - connect(ui->actionAbout, SIGNAL(triggered()), this, SLOT(showAboutPage())); - - connect(ui->closeCancelButton, SIGNAL(clicked()), this, SLOT(cancelCloseAction())); - connect(ui->applyButton, SIGNAL(clicked()), this, SLOT(saveCurrentConfig())); - - connect(ui->channelConfigCombo, SIGNAL(currentIndexChanged(const QString&)), this, SLOT(enableApplyButton())); - connect(ui->sampleFormatCombo, SIGNAL(currentIndexChanged(const QString&)), this, SLOT(enableApplyButton())); - connect(ui->stereoModeCombo, SIGNAL(currentIndexChanged(const QString&)), this, SLOT(enableApplyButton())); - connect(ui->sampleRateCombo, SIGNAL(currentIndexChanged(const QString&)), this, SLOT(enableApplyButton())); - connect(ui->sampleRateCombo, SIGNAL(editTextChanged(const QString&)), this, SLOT(enableApplyButton())); - - connect(ui->resamplerSlider, SIGNAL(valueChanged(int)), this, SLOT(updateResamplerLabel(int))); - - connect(ui->periodSizeSlider, SIGNAL(valueChanged(int)), this, SLOT(updatePeriodSizeEdit(int))); - connect(ui->periodSizeEdit, SIGNAL(editingFinished()), this, SLOT(updatePeriodSizeSlider())); - connect(ui->periodCountSlider, SIGNAL(valueChanged(int)), this, SLOT(updatePeriodCountEdit(int))); - connect(ui->periodCountEdit, SIGNAL(editingFinished()), this, SLOT(updatePeriodCountSlider())); - - connect(ui->stereoEncodingComboBox, SIGNAL(currentIndexChanged(QString)), this, SLOT(enableApplyButton())); - connect(ui->ambiFormatComboBox, SIGNAL(currentIndexChanged(QString)), this, SLOT(enableApplyButton())); - connect(ui->outputLimiterCheckBox, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->outputDitherCheckBox, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - - connect(ui->decoderHQModeCheckBox, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->decoderDistCompCheckBox, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->decoderNFEffectsCheckBox, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->decoderNFRefDelaySpinBox, SIGNAL(valueChanged(double)), this, SLOT(enableApplyButton())); - connect(ui->decoderQuadLineEdit, SIGNAL(textChanged(QString)), this, SLOT(enableApplyButton())); - connect(ui->decoderQuadButton, SIGNAL(clicked()), this, SLOT(selectQuadDecoderFile())); - connect(ui->decoder51LineEdit, SIGNAL(textChanged(QString)), this, SLOT(enableApplyButton())); - connect(ui->decoder51Button, SIGNAL(clicked()), this, SLOT(select51DecoderFile())); - connect(ui->decoder61LineEdit, SIGNAL(textChanged(QString)), this, SLOT(enableApplyButton())); - connect(ui->decoder61Button, SIGNAL(clicked()), this, SLOT(select61DecoderFile())); - connect(ui->decoder71LineEdit, SIGNAL(textChanged(QString)), this, SLOT(enableApplyButton())); - connect(ui->decoder71Button, SIGNAL(clicked()), this, SLOT(select71DecoderFile())); - - connect(ui->preferredHrtfComboBox, SIGNAL(currentIndexChanged(const QString&)), this, SLOT(enableApplyButton())); - connect(ui->hrtfStateComboBox, SIGNAL(currentIndexChanged(const QString&)), this, SLOT(enableApplyButton())); - connect(ui->hrtfAddButton, SIGNAL(clicked()), this, SLOT(addHrtfFile())); - connect(ui->hrtfRemoveButton, SIGNAL(clicked()), this, SLOT(removeHrtfFile())); - connect(ui->hrtfFileList, SIGNAL(itemSelectionChanged()), this, SLOT(updateHrtfRemoveButton())); - connect(ui->defaultHrtfPathsCheckBox, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - - connect(ui->srcCountLineEdit, SIGNAL(editingFinished()), this, SLOT(enableApplyButton())); - connect(ui->srcSendLineEdit, SIGNAL(editingFinished()), this, SLOT(enableApplyButton())); - connect(ui->effectSlotLineEdit, SIGNAL(editingFinished()), this, SLOT(enableApplyButton())); - - connect(ui->enableSSECheckBox, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->enableSSE2CheckBox, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->enableSSE3CheckBox, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->enableSSE41CheckBox, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->enableNeonCheckBox, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); + connect(ui->actionLoad, &QAction::triggered, this, &MainWindow::loadConfigFromFile); + connect(ui->actionSave_As, &QAction::triggered, this, &MainWindow::saveConfigAsFile); + + connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::showAboutPage); + + connect(ui->closeCancelButton, &QPushButton::clicked, this, &MainWindow::cancelCloseAction); + connect(ui->applyButton, &QPushButton::clicked, this, &MainWindow::saveCurrentConfig); + + auto qcb_cicstr = static_cast<void(QComboBox::*)(const QString&)>(&QComboBox::currentIndexChanged); + connect(ui->channelConfigCombo, qcb_cicstr, this, &MainWindow::enableApplyButton); + connect(ui->sampleFormatCombo, qcb_cicstr, this, &MainWindow::enableApplyButton); + connect(ui->stereoModeCombo, qcb_cicstr, this, &MainWindow::enableApplyButton); + connect(ui->sampleRateCombo, qcb_cicstr, this, &MainWindow::enableApplyButton); + connect(ui->sampleRateCombo, &QComboBox::editTextChanged, this, &MainWindow::enableApplyButton); + + connect(ui->resamplerSlider, &QSlider::valueChanged, this, &MainWindow::updateResamplerLabel); + + connect(ui->periodSizeSlider, &QSlider::valueChanged, this, &MainWindow::updatePeriodSizeEdit); + connect(ui->periodSizeEdit, &QLineEdit::editingFinished, this, &MainWindow::updatePeriodSizeSlider); + connect(ui->periodCountSlider, &QSlider::valueChanged, this, &MainWindow::updatePeriodCountEdit); + connect(ui->periodCountEdit, &QLineEdit::editingFinished, this, &MainWindow::updatePeriodCountSlider); + + connect(ui->stereoEncodingComboBox, qcb_cicstr, this, &MainWindow::enableApplyButton); + connect(ui->ambiFormatComboBox, qcb_cicstr, this, &MainWindow::enableApplyButton); + connect(ui->outputLimiterCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->outputDitherCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + + connect(ui->decoderHQModeCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->decoderDistCompCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->decoderNFEffectsCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + auto qdsb_vcd = static_cast<void(QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged); + connect(ui->decoderNFRefDelaySpinBox, qdsb_vcd, this, &MainWindow::enableApplyButton); + connect(ui->decoderQuadLineEdit, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton); + connect(ui->decoderQuadButton, &QPushButton::clicked, this, &MainWindow::selectQuadDecoderFile); + connect(ui->decoder51LineEdit, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton); + connect(ui->decoder51Button, &QPushButton::clicked, this, &MainWindow::select51DecoderFile); + connect(ui->decoder61LineEdit, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton); + connect(ui->decoder61Button, &QPushButton::clicked, this, &MainWindow::select61DecoderFile); + connect(ui->decoder71LineEdit, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton); + connect(ui->decoder71Button, &QPushButton::clicked, this, &MainWindow::select71DecoderFile); + + connect(ui->preferredHrtfComboBox, qcb_cicstr, this, &MainWindow::enableApplyButton); + connect(ui->hrtfStateComboBox, qcb_cicstr, this, &MainWindow::enableApplyButton); + connect(ui->hrtfmodeSlider, &QSlider::valueChanged, this, &MainWindow::updateHrtfModeLabel); + + connect(ui->hrtfAddButton, &QPushButton::clicked, this, &MainWindow::addHrtfFile); + connect(ui->hrtfRemoveButton, &QPushButton::clicked, this, &MainWindow::removeHrtfFile); + connect(ui->hrtfFileList, &QListWidget::itemSelectionChanged, this, &MainWindow::updateHrtfRemoveButton); + connect(ui->defaultHrtfPathsCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + + connect(ui->srcCountLineEdit, &QLineEdit::editingFinished, this, &MainWindow::enableApplyButton); + connect(ui->srcSendLineEdit, &QLineEdit::editingFinished, this, &MainWindow::enableApplyButton); + connect(ui->effectSlotLineEdit, &QLineEdit::editingFinished, this, &MainWindow::enableApplyButton); + + connect(ui->enableSSECheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->enableSSE2CheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->enableSSE3CheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->enableSSE41CheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->enableNeonCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); ui->enabledBackendList->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->enabledBackendList, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showEnabledBackendMenu(QPoint))); + connect(ui->enabledBackendList, &QListWidget::customContextMenuRequested, this, &MainWindow::showEnabledBackendMenu); ui->disabledBackendList->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->disabledBackendList, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showDisabledBackendMenu(QPoint))); - connect(ui->backendCheckBox, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - - connect(ui->defaultReverbComboBox, SIGNAL(currentIndexChanged(const QString&)), this, SLOT(enableApplyButton())); - connect(ui->enableEaxReverbCheck, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->enableStdReverbCheck, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->enableAutowahCheck, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->enableChorusCheck, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->enableCompressorCheck, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->enableDistortionCheck, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->enableEchoCheck, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->enableEqualizerCheck, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->enableFlangerCheck, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->enableFrequencyShifterCheck, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->enableModulatorCheck, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->enableDedicatedCheck, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->enablePitchShifterCheck, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - - connect(ui->pulseAutospawnCheckBox, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->pulseAllowMovesCheckBox, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->pulseFixRateCheckBox, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - - connect(ui->jackAutospawnCheckBox, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->jackBufferSizeSlider, SIGNAL(valueChanged(int)), this, SLOT(updateJackBufferSizeEdit(int))); - connect(ui->jackBufferSizeLine, SIGNAL(editingFinished()), this, SLOT(updateJackBufferSizeSlider())); - - connect(ui->alsaDefaultDeviceLine, SIGNAL(textChanged(QString)), this, SLOT(enableApplyButton())); - connect(ui->alsaDefaultCaptureLine, SIGNAL(textChanged(QString)), this, SLOT(enableApplyButton())); - connect(ui->alsaResamplerCheckBox, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - connect(ui->alsaMmapCheckBox, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); - - connect(ui->ossDefaultDeviceLine, SIGNAL(textChanged(QString)), this, SLOT(enableApplyButton())); - connect(ui->ossPlaybackPushButton, SIGNAL(clicked(bool)), this, SLOT(selectOSSPlayback())); - connect(ui->ossDefaultCaptureLine, SIGNAL(textChanged(QString)), this, SLOT(enableApplyButton())); - connect(ui->ossCapturePushButton, SIGNAL(clicked(bool)), this, SLOT(selectOSSCapture())); - - connect(ui->solarisDefaultDeviceLine, SIGNAL(textChanged(QString)), this, SLOT(enableApplyButton())); - connect(ui->solarisPlaybackPushButton, SIGNAL(clicked(bool)), this, SLOT(selectSolarisPlayback())); - - connect(ui->waveOutputLine, SIGNAL(textChanged(QString)), this, SLOT(enableApplyButton())); - connect(ui->waveOutputButton, SIGNAL(clicked(bool)), this, SLOT(selectWaveOutput())); - connect(ui->waveBFormatCheckBox, SIGNAL(stateChanged(int)), this, SLOT(enableApplyButton())); + connect(ui->disabledBackendList, &QListWidget::customContextMenuRequested, this, &MainWindow::showDisabledBackendMenu); + connect(ui->backendCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + + connect(ui->defaultReverbComboBox, qcb_cicstr, this, &MainWindow::enableApplyButton); + connect(ui->enableEaxReverbCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->enableStdReverbCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->enableAutowahCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->enableChorusCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->enableCompressorCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->enableDistortionCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->enableEchoCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->enableEqualizerCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->enableFlangerCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->enableFrequencyShifterCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->enableModulatorCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->enableDedicatedCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->enablePitchShifterCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->enableVocalMorpherCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + + connect(ui->pulseAutospawnCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->pulseAllowMovesCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->pulseFixRateCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->pulseAdjLatencyCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + + connect(ui->jackAutospawnCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->jackBufferSizeSlider, &QSlider::valueChanged, this, &MainWindow::updateJackBufferSizeEdit); + connect(ui->jackBufferSizeLine, &QLineEdit::editingFinished, this, &MainWindow::updateJackBufferSizeSlider); + + connect(ui->alsaDefaultDeviceLine, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton); + connect(ui->alsaDefaultCaptureLine, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton); + connect(ui->alsaResamplerCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + connect(ui->alsaMmapCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); + + connect(ui->ossDefaultDeviceLine, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton); + connect(ui->ossPlaybackPushButton, &QPushButton::clicked, this, &MainWindow::selectOSSPlayback); + connect(ui->ossDefaultCaptureLine, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton); + connect(ui->ossCapturePushButton, &QPushButton::clicked, this, &MainWindow::selectOSSCapture); + + connect(ui->solarisDefaultDeviceLine, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton); + connect(ui->solarisPlaybackPushButton, &QPushButton::clicked, this, &MainWindow::selectSolarisPlayback); + + connect(ui->waveOutputLine, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton); + connect(ui->waveOutputButton, &QPushButton::clicked, this, &MainWindow::selectWaveOutput); + connect(ui->waveBFormatCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton); ui->backendListWidget->setCurrentRow(0); ui->tabWidget->setCurrentIndex(0); @@ -446,8 +503,7 @@ void MainWindow::closeEvent(QCloseEvent *event) { QMessageBox::StandardButton btn = QMessageBox::warning(this, tr("Apply changes?"), tr("Save changes before quitting?"), - QMessageBox::Save | QMessageBox::No | QMessageBox::Cancel - ); + QMessageBox::Save | QMessageBox::No | QMessageBox::Cancel); if(btn == QMessageBox::Save) saveCurrentConfig(); if(btn == QMessageBox::Cancel) @@ -467,9 +523,8 @@ void MainWindow::cancelCloseAction() void MainWindow::showAboutPage() { QMessageBox::information(this, tr("About"), - tr("OpenAL Soft Configuration Utility.\nBuilt for OpenAL Soft library version ")+ - (ALSOFT_VERSION "-" ALSOFT_GIT_COMMIT_HASH " (" ALSOFT_GIT_BRANCH " branch).") - ); + tr("OpenAL Soft Configuration Utility.\nBuilt for OpenAL Soft library version ") + + GetVersionString()); } @@ -486,17 +541,17 @@ QStringList MainWindow::collectHrtfs() { if(!fname.endsWith(".mhr", Qt::CaseInsensitive)) continue; - QString fullname = dir.absoluteFilePath(fname); + QString fullname{dir.absoluteFilePath(fname)}; if(processed.contains(fullname)) continue; processed.push_back(fullname); - QString name = fname.left(fname.length()-4); + QString name{fname.left(fname.length()-4)}; if(!ret.contains(name)) ret.push_back(name); else { - size_t i = 2; + size_t i{2}; do { QString s = name+" #"+QString::number(i); if(!ret.contains(s)) @@ -515,25 +570,25 @@ QStringList MainWindow::collectHrtfs() QStringList paths = getAllDataPaths("/openal/hrtf"); foreach(const QString &name, paths) { - QDir dir(name); - QStringList fnames = dir.entryList(QDir::Files | QDir::Readable, QDir::Name); + QDir dir{name}; + QStringList fnames{dir.entryList(QDir::Files | QDir::Readable, QDir::Name)}; foreach(const QString &fname, fnames) { if(!fname.endsWith(".mhr", Qt::CaseInsensitive)) continue; - QString fullname = dir.absoluteFilePath(fname); + QString fullname{dir.absoluteFilePath(fname)}; if(processed.contains(fullname)) continue; processed.push_back(fullname); - QString name = fname.left(fname.length()-4); + QString name{fname.left(fname.length()-4)}; if(!ret.contains(name)) ret.push_back(name); else { - size_t i = 2; + size_t i{2}; do { - QString s = name+" #"+QString::number(i); + QString s{name+" #"+QString::number(i)}; if(!ret.contains(s)) { ret.push_back(s); @@ -563,33 +618,33 @@ void MainWindow::loadConfigFromFile() void MainWindow::loadConfig(const QString &fname) { - QSettings settings(fname, QSettings::IniFormat); + QSettings settings{fname, QSettings::IniFormat}; QString sampletype = settings.value("sample-type").toString(); ui->sampleFormatCombo->setCurrentIndex(0); if(sampletype.isEmpty() == false) { - QString str = getNameFromValue(sampleTypeList, sampletype); + QString str{getNameFromValue(sampleTypeList, sampletype)}; if(!str.isEmpty()) { - int j = ui->sampleFormatCombo->findText(str); + const int j{ui->sampleFormatCombo->findText(str)}; if(j > 0) ui->sampleFormatCombo->setCurrentIndex(j); } } - QString channelconfig = settings.value("channels").toString(); + QString channelconfig{settings.value("channels").toString()}; ui->channelConfigCombo->setCurrentIndex(0); if(channelconfig.isEmpty() == false) { - QString str = getNameFromValue(speakerModeList, channelconfig); + QString str{getNameFromValue(speakerModeList, channelconfig)}; if(!str.isEmpty()) { - int j = ui->channelConfigCombo->findText(str); + const int j{ui->channelConfigCombo->findText(str)}; if(j > 0) ui->channelConfigCombo->setCurrentIndex(j); } } - QString srate = settings.value("frequency").toString(); + QString srate{settings.value("frequency").toString()}; if(srate.isEmpty()) ui->sampleRateCombo->setCurrentIndex(0); else @@ -608,11 +663,11 @@ void MainWindow::loadConfig(const QString &fname) QString resampler = settings.value("resampler").toString().trimmed(); ui->resamplerSlider->setValue(2); ui->resamplerLabel->setText(resamplerList[2].name); - /* The "cubic" and "sinc8" resamplers are no longer supported. Use "sinc4" + /* The "sinc4" and "sinc8" resamplers are no longer supported. Use "cubic" * as a fallback. */ - if(resampler == "cubic" || resampler == "sinc8") - resampler = "sinc4"; + if(resampler == "sinc4" || resampler == "sinc8") + resampler = "cubic"; /* The "bsinc" resampler name is an alias for "bsinc12". */ else if(resampler == "bsinc") resampler = "bsinc12"; @@ -630,15 +685,15 @@ void MainWindow::loadConfig(const QString &fname) ui->stereoModeCombo->setCurrentIndex(0); if(stereomode.isEmpty() == false) { - QString str = getNameFromValue(stereoModeList, stereomode); + QString str{getNameFromValue(stereoModeList, stereomode)}; if(!str.isEmpty()) { - int j = ui->stereoModeCombo->findText(str); + const int j{ui->stereoModeCombo->findText(str)}; if(j > 0) ui->stereoModeCombo->setCurrentIndex(j); } } - int periodsize = settings.value("period_size").toInt(); + int periodsize{settings.value("period_size").toInt()}; ui->periodSizeEdit->clear(); if(periodsize >= 64) { @@ -646,7 +701,7 @@ void MainWindow::loadConfig(const QString &fname) updatePeriodSizeSlider(); } - int periodcount = settings.value("periods").toInt(); + int periodcount{settings.value("periods").toInt()}; ui->periodCountEdit->clear(); if(periodcount >= 2) { @@ -654,51 +709,38 @@ void MainWindow::loadConfig(const QString &fname) updatePeriodCountSlider(); } - if(settings.value("output-limiter").isNull()) - ui->outputLimiterCheckBox->setCheckState(Qt::PartiallyChecked); - else - ui->outputLimiterCheckBox->setCheckState( - settings.value("output-limiter").toBool() ? Qt::Checked : Qt::Unchecked - ); - - if(settings.value("dither").isNull()) - ui->outputDitherCheckBox->setCheckState(Qt::PartiallyChecked); - else - ui->outputDitherCheckBox->setCheckState( - settings.value("dither").toBool() ? Qt::Checked : Qt::Unchecked - ); + ui->outputLimiterCheckBox->setCheckState(getCheckState(settings.value("output-limiter"))); + ui->outputDitherCheckBox->setCheckState(getCheckState(settings.value("dither"))); - QString stereopan = settings.value("stereo-encoding").toString(); + QString stereopan{settings.value("stereo-encoding").toString()}; ui->stereoEncodingComboBox->setCurrentIndex(0); if(stereopan.isEmpty() == false) { - QString str = getNameFromValue(stereoEncList, stereopan); + QString str{getNameFromValue(stereoEncList, stereopan)}; if(!str.isEmpty()) { - int j = ui->stereoEncodingComboBox->findText(str); + const int j{ui->stereoEncodingComboBox->findText(str)}; if(j > 0) ui->stereoEncodingComboBox->setCurrentIndex(j); } } - QString ambiformat = settings.value("ambi-format").toString(); + QString ambiformat{settings.value("ambi-format").toString()}; ui->ambiFormatComboBox->setCurrentIndex(0); if(ambiformat.isEmpty() == false) { - QString str = getNameFromValue(ambiFormatList, ambiformat); + QString str{getNameFromValue(ambiFormatList, ambiformat)}; if(!str.isEmpty()) { - int j = ui->ambiFormatComboBox->findText(str); + const int j{ui->ambiFormatComboBox->findText(str)}; if(j > 0) ui->ambiFormatComboBox->setCurrentIndex(j); } } - bool hqmode = settings.value("decoder/hq-mode", false).toBool(); + bool hqmode{settings.value("decoder/hq-mode", true).toBool()}; ui->decoderHQModeCheckBox->setChecked(hqmode); - bool distcomp = settings.value("decoder/distance-comp", true).toBool(); - ui->decoderDistCompCheckBox->setChecked(distcomp); - bool nfeffects = settings.value("decoder/nfc", true).toBool(); - ui->decoderNFEffectsCheckBox->setChecked(nfeffects); - double refdelay = settings.value("decoder/nfc-ref-delay", 0.0).toDouble(); + 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()}; ui->decoderNFRefDelaySpinBox->setValue(refdelay); ui->decoderQuadLineEdit->setText(settings.value("decoder/quad").toString()); @@ -706,22 +748,40 @@ void MainWindow::loadConfig(const QString &fname) ui->decoder61LineEdit->setText(settings.value("decoder/surround61").toString()); ui->decoder71LineEdit->setText(settings.value("decoder/surround71").toString()); - QStringList disabledCpuExts = settings.value("disable-cpu-exts").toStringList(); + QStringList disabledCpuExts{settings.value("disable-cpu-exts").toStringList()}; if(disabledCpuExts.size() == 1) disabledCpuExts = disabledCpuExts[0].split(QChar(',')); - for(QStringList::iterator iter = disabledCpuExts.begin();iter != disabledCpuExts.end();iter++) - *iter = iter->trimmed(); + for(QString &name : disabledCpuExts) + name = name.trimmed(); ui->enableSSECheckBox->setChecked(!disabledCpuExts.contains("sse", Qt::CaseInsensitive)); ui->enableSSE2CheckBox->setChecked(!disabledCpuExts.contains("sse2", Qt::CaseInsensitive)); ui->enableSSE3CheckBox->setChecked(!disabledCpuExts.contains("sse3", Qt::CaseInsensitive)); ui->enableSSE41CheckBox->setChecked(!disabledCpuExts.contains("sse4.1", Qt::CaseInsensitive)); ui->enableNeonCheckBox->setChecked(!disabledCpuExts.contains("neon", Qt::CaseInsensitive)); - QStringList hrtf_paths = settings.value("hrtf-paths").toStringList(); + 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") + hrtfmode = "ambi2"; + for(int i = 0;hrtfModeList[i].name[0];i++) + { + if(hrtfmode == hrtfModeList[i].value) + { + ui->hrtfmodeSlider->setValue(i); + ui->hrtfmodeLabel->setText(hrtfModeList[i].name); + break; + } + } + + QStringList hrtf_paths{settings.value("hrtf-paths").toStringList()}; if(hrtf_paths.size() == 1) hrtf_paths = hrtf_paths[0].split(QChar(',')); - for(QStringList::iterator iter = hrtf_paths.begin();iter != hrtf_paths.end();iter++) - *iter = iter->trimmed(); + for(QString &name : hrtf_paths) + name = name.trimmed(); if(!hrtf_paths.empty() && !hrtf_paths.back().isEmpty()) ui->defaultHrtfPathsCheckBox->setCheckState(Qt::Unchecked); else @@ -734,7 +794,7 @@ void MainWindow::loadConfig(const QString &fname) ui->hrtfFileList->addItems(hrtf_paths); updateHrtfRemoveButton(); - QString hrtfstate = settings.value("hrtf").toString().toLower(); + QString hrtfstate{settings.value("hrtf").toString().toLower()}; if(hrtfstate == "true") ui->hrtfStateComboBox->setCurrentIndex(1); else if(hrtfstate == "false") @@ -746,16 +806,16 @@ void MainWindow::loadConfig(const QString &fname) ui->preferredHrtfComboBox->addItem("- Any -"); if(ui->defaultHrtfPathsCheckBox->isChecked()) { - QStringList hrtfs = collectHrtfs(); + QStringList hrtfs{collectHrtfs()}; foreach(const QString &name, hrtfs) ui->preferredHrtfComboBox->addItem(name); } - QString defaulthrtf = settings.value("default-hrtf").toString(); + QString defaulthrtf{settings.value("default-hrtf").toString()}; ui->preferredHrtfComboBox->setCurrentIndex(0); if(defaulthrtf.isEmpty() == false) { - int i = ui->preferredHrtfComboBox->findText(defaulthrtf); + int i{ui->preferredHrtfComboBox->findText(defaulthrtf)}; if(i > 0) ui->preferredHrtfComboBox->setCurrentIndex(i); else @@ -769,23 +829,23 @@ void MainWindow::loadConfig(const QString &fname) ui->enabledBackendList->clear(); ui->disabledBackendList->clear(); - QStringList drivers = settings.value("drivers").toStringList(); + QStringList drivers{settings.value("drivers").toStringList()}; if(drivers.size() == 0) ui->backendCheckBox->setChecked(true); else { if(drivers.size() == 1) drivers = drivers[0].split(QChar(',')); - for(QStringList::iterator iter = drivers.begin();iter != drivers.end();iter++) + for(QString &name : drivers) { - *iter = iter->trimmed(); + name = name.trimmed(); /* Convert "mmdevapi" references to "wasapi" for backwards * compatibility. */ - if(*iter == "-mmdevapi") - *iter = "-wasapi"; - else if(*iter == "mmdevapi") - *iter = "wasapi"; + if(name == "-mmdevapi") + name = "-wasapi"; + else if(name == "mmdevapi") + name = "wasapi"; } bool lastWasEmpty = false; @@ -805,7 +865,7 @@ void MainWindow::loadConfig(const QString &fname) } else if(backend.size() > 1) { - QStringRef backendref = backend.rightRef(backend.size()-1); + QStringRef backendref{backend.rightRef(backend.size()-1)}; for(int j = 0;backendList[j].backend_name[0];j++) { if(backendref == backendList[j].backend_name) @@ -819,7 +879,7 @@ void MainWindow::loadConfig(const QString &fname) ui->backendCheckBox->setChecked(lastWasEmpty); } - QString defaultreverb = settings.value("default-reverb").toString().toLower(); + QString defaultreverb{settings.value("default-reverb").toString().toLower()}; ui->defaultReverbComboBox->setCurrentIndex(0); if(defaultreverb.isEmpty() == false) { @@ -833,11 +893,11 @@ void MainWindow::loadConfig(const QString &fname) } } - QStringList excludefx = settings.value("excludefx").toStringList(); + QStringList excludefx{settings.value("excludefx").toStringList()}; if(excludefx.size() == 1) excludefx = excludefx[0].split(QChar(',')); - for(QStringList::iterator iter = excludefx.begin();iter != excludefx.end();iter++) - *iter = iter->trimmed(); + for(QString &name : excludefx) + name = name.trimmed(); ui->enableEaxReverbCheck->setChecked(!excludefx.contains("eaxreverb", Qt::CaseInsensitive)); ui->enableStdReverbCheck->setChecked(!excludefx.contains("reverb", Qt::CaseInsensitive)); ui->enableAutowahCheck->setChecked(!excludefx.contains("autowah", Qt::CaseInsensitive)); @@ -851,19 +911,21 @@ void MainWindow::loadConfig(const QString &fname) ui->enableModulatorCheck->setChecked(!excludefx.contains("modulator", Qt::CaseInsensitive)); ui->enableDedicatedCheck->setChecked(!excludefx.contains("dedicated", Qt::CaseInsensitive)); ui->enablePitchShifterCheck->setChecked(!excludefx.contains("pshifter", Qt::CaseInsensitive)); + ui->enableVocalMorpherCheck->setChecked(!excludefx.contains("vmorpher", Qt::CaseInsensitive)); - ui->pulseAutospawnCheckBox->setChecked(settings.value("pulse/spawn-server", true).toBool()); - ui->pulseAllowMovesCheckBox->setChecked(settings.value("pulse/allow-moves", false).toBool()); - ui->pulseFixRateCheckBox->setChecked(settings.value("pulse/fix-rate", false).toBool()); + ui->pulseAutospawnCheckBox->setCheckState(getCheckState(settings.value("pulse/spawn-server"))); + ui->pulseAllowMovesCheckBox->setCheckState(getCheckState(settings.value("pulse/allow-moves"))); + ui->pulseFixRateCheckBox->setCheckState(getCheckState(settings.value("pulse/fix-rate"))); + ui->pulseAdjLatencyCheckBox->setCheckState(getCheckState(settings.value("pulse/adjust-latency"))); - ui->jackAutospawnCheckBox->setChecked(settings.value("jack/spawn-server", false).toBool()); + ui->jackAutospawnCheckBox->setCheckState(getCheckState(settings.value("jack/spawn-server"))); ui->jackBufferSizeLine->setText(settings.value("jack/buffer-size", QString()).toString()); updateJackBufferSizeSlider(); ui->alsaDefaultDeviceLine->setText(settings.value("alsa/device", QString()).toString()); ui->alsaDefaultCaptureLine->setText(settings.value("alsa/capture", QString()).toString()); - ui->alsaResamplerCheckBox->setChecked(settings.value("alsa/allow-resampler", false).toBool()); - ui->alsaMmapCheckBox->setChecked(settings.value("alsa/mmap", true).toBool()); + ui->alsaResamplerCheckBox->setCheckState(getCheckState(settings.value("alsa/allow-resampler"))); + ui->alsaMmapCheckBox->setCheckState(getCheckState(settings.value("alsa/mmap"))); ui->ossDefaultDeviceLine->setText(settings.value("oss/device", QString()).toString()); ui->ossDefaultCaptureLine->setText(settings.value("oss/capture", QString()).toString()); @@ -885,12 +947,12 @@ void MainWindow::saveCurrentConfig() ui->closeCancelButton->setText(tr("Close")); mNeedsSave = false; QMessageBox::information(this, tr("Information"), - tr("Applications using OpenAL need to be restarted for changes to take effect.")); + tr("Applications using OpenAL need to be restarted for changes to take effect.")); } void MainWindow::saveConfigAsFile() { - QString fname = QFileDialog::getOpenFileName(this, tr("Select Files")); + QString fname{QFileDialog::getOpenFileName(this, tr("Select Files"))}; if(fname.isEmpty() == false) { saveConfig(fname); @@ -901,13 +963,13 @@ void MainWindow::saveConfigAsFile() void MainWindow::saveConfig(const QString &fname) const { - QSettings settings(fname, QSettings::IniFormat); + QSettings settings{fname, QSettings::IniFormat}; /* HACK: Compound any stringlist values into a comma-separated string. */ - QStringList allkeys = settings.allKeys(); + QStringList allkeys{settings.allKeys()}; foreach(const QString &key, allkeys) { - QStringList vals = settings.value(key).toStringList(); + QStringList vals{settings.value(key).toStringList()}; if(vals.size() > 1) settings.setValue(key, vals.join(QChar(','))); } @@ -915,9 +977,9 @@ void MainWindow::saveConfig(const QString &fname) const settings.setValue("sample-type", getValueFromName(sampleTypeList, ui->sampleFormatCombo->currentText())); settings.setValue("channels", getValueFromName(speakerModeList, ui->channelConfigCombo->currentText())); - uint rate = ui->sampleRateCombo->currentText().toUInt(); - if(!(rate > 0)) - settings.setValue("frequency", QString()); + uint rate{ui->sampleRateCombo->currentText().toUInt()}; + if(rate <= 0) + settings.setValue("frequency", QString{}); else settings.setValue("frequency", rate); @@ -933,34 +995,17 @@ void MainWindow::saveConfig(const QString &fname) const settings.setValue("stereo-encoding", getValueFromName(stereoEncList, ui->stereoEncodingComboBox->currentText())); settings.setValue("ambi-format", getValueFromName(ambiFormatList, ui->ambiFormatComboBox->currentText())); - Qt::CheckState limiter = ui->outputLimiterCheckBox->checkState(); - if(limiter == Qt::PartiallyChecked) - settings.setValue("output-limiter", QString()); - else if(limiter == Qt::Checked) - settings.setValue("output-limiter", QString("true")); - else if(limiter == Qt::Unchecked) - settings.setValue("output-limiter", QString("false")); - - Qt::CheckState dither = ui->outputDitherCheckBox->checkState(); - if(dither == Qt::PartiallyChecked) - settings.setValue("dither", QString()); - else if(dither == Qt::Checked) - settings.setValue("dither", QString("true")); - else if(dither == Qt::Unchecked) - settings.setValue("dither", QString("false")); + 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/distance-comp", - ui->decoderDistCompCheckBox->isChecked() ? QString(/*"true"*/) : QString("false") - ); - settings.setValue("decoder/nfc", - ui->decoderNFEffectsCheckBox->isChecked() ? QString(/*"true"*/) : QString("false") + ui->decoderHQModeCheckBox->isChecked() ? QString{/*"true"*/} : QString{"false"} ); + settings.setValue("decoder/distance-comp", getCheckValue(ui->decoderDistCompCheckBox)); + settings.setValue("decoder/nfc", getCheckValue(ui->decoderNFEffectsCheckBox)); double refdelay = ui->decoderNFRefDelaySpinBox->value(); settings.setValue("decoder/nfc-ref-delay", - (refdelay > 0.0) ? QString::number(refdelay) : QString() + (refdelay > 0.0) ? QString::number(refdelay) : QString{} ); settings.setValue("decoder/quad", ui->decoderQuadLineEdit->text()); @@ -981,32 +1026,35 @@ void MainWindow::saveConfig(const QString &fname) const strlist.append("neon"); settings.setValue("disable-cpu-exts", strlist.join(QChar(','))); + settings.setValue("hrtf-mode", hrtfModeList[ui->hrtfmodeSlider->value()].value); + if(ui->hrtfStateComboBox->currentIndex() == 1) settings.setValue("hrtf", "true"); else if(ui->hrtfStateComboBox->currentIndex() == 2) settings.setValue("hrtf", "false"); else - settings.setValue("hrtf", QString()); + settings.setValue("hrtf", QString{}); if(ui->preferredHrtfComboBox->currentIndex() == 0) - settings.setValue("default-hrtf", QString()); + settings.setValue("default-hrtf", QString{}); else { - QString str = ui->preferredHrtfComboBox->currentText(); + QString str{ui->preferredHrtfComboBox->currentText()}; settings.setValue("default-hrtf", str); } strlist.clear(); + strlist.reserve(ui->hrtfFileList->count()); for(int i = 0;i < ui->hrtfFileList->count();i++) strlist.append(ui->hrtfFileList->item(i)->text()); if(!strlist.empty() && ui->defaultHrtfPathsCheckBox->isChecked()) - strlist.append(QString()); - settings.setValue("hrtf-paths", strlist.join(QChar(','))); + strlist.append(QString{}); + settings.setValue("hrtf-paths", strlist.join(QChar{','})); strlist.clear(); for(int i = 0;i < ui->enabledBackendList->count();i++) { - QString label = ui->enabledBackendList->item(i)->text(); + QString label{ui->enabledBackendList->item(i)->text()}; for(int j = 0;backendList[j].backend_name[0];j++) { if(label == backendList[j].full_string) @@ -1018,12 +1066,12 @@ void MainWindow::saveConfig(const QString &fname) const } for(int i = 0;i < ui->disabledBackendList->count();i++) { - QString label = ui->disabledBackendList->item(i)->text(); + QString label{ui->disabledBackendList->item(i)->text()}; for(int j = 0;backendList[j].backend_name[0];j++) { if(label == backendList[j].full_string) { - strlist.append(QChar('-')+QString(backendList[j].backend_name)); + strlist.append(QChar{'-'}+QString{backendList[j].backend_name}); break; } } @@ -1031,15 +1079,15 @@ void MainWindow::saveConfig(const QString &fname) const if(strlist.size() == 0 && !ui->backendCheckBox->isChecked()) strlist.append("-all"); else if(ui->backendCheckBox->isChecked()) - strlist.append(QString()); + strlist.append(QString{}); settings.setValue("drivers", strlist.join(QChar(','))); // TODO: Remove check when we can properly match global values. if(ui->defaultReverbComboBox->currentIndex() == 0) - settings.setValue("default-reverb", QString()); + settings.setValue("default-reverb", QString{}); else { - QString str = ui->defaultReverbComboBox->currentText().toLower(); + QString str{ui->defaultReverbComboBox->currentText().toLower()}; settings.setValue("default-reverb", str); } @@ -1070,31 +1118,22 @@ void MainWindow::saveConfig(const QString &fname) const strlist.append("dedicated"); if(!ui->enablePitchShifterCheck->isChecked()) strlist.append("pshifter"); - settings.setValue("excludefx", strlist.join(QChar(','))); + if(!ui->enableVocalMorpherCheck->isChecked()) + strlist.append("vmorpher"); + settings.setValue("excludefx", strlist.join(QChar{','})); - settings.setValue("pulse/spawn-server", - ui->pulseAutospawnCheckBox->isChecked() ? QString(/*"true"*/) : QString("false") - ); - settings.setValue("pulse/allow-moves", - ui->pulseAllowMovesCheckBox->isChecked() ? QString("true") : QString(/*"false"*/) - ); - settings.setValue("pulse/fix-rate", - ui->pulseFixRateCheckBox->isChecked() ? QString("true") : QString(/*"false"*/) - ); + settings.setValue("pulse/spawn-server", getCheckValue(ui->pulseAutospawnCheckBox)); + settings.setValue("pulse/allow-moves", getCheckValue(ui->pulseAllowMovesCheckBox)); + settings.setValue("pulse/fix-rate", getCheckValue(ui->pulseFixRateCheckBox)); + settings.setValue("pulse/adjust-latency", getCheckValue(ui->pulseAdjLatencyCheckBox)); - settings.setValue("jack/spawn-server", - ui->jackAutospawnCheckBox->isChecked() ? QString("true") : QString(/*"false"*/) - ); + settings.setValue("jack/spawn-server", getCheckValue(ui->jackAutospawnCheckBox)); settings.setValue("jack/buffer-size", ui->jackBufferSizeLine->text()); settings.setValue("alsa/device", ui->alsaDefaultDeviceLine->text()); settings.setValue("alsa/capture", ui->alsaDefaultCaptureLine->text()); - settings.setValue("alsa/allow-resampler", - ui->alsaResamplerCheckBox->isChecked() ? QString("true") : QString(/*"false"*/) - ); - settings.setValue("alsa/mmap", - ui->alsaMmapCheckBox->isChecked() ? QString(/*"true"*/) : QString("false") - ); + settings.setValue("alsa/allow-resampler", getCheckValue(ui->alsaResamplerCheckBox)); + settings.setValue("alsa/mmap", getCheckValue(ui->alsaMmapCheckBox)); settings.setValue("oss/device", ui->ossDefaultDeviceLine->text()); settings.setValue("oss/capture", ui->ossDefaultCaptureLine->text()); @@ -1103,7 +1142,7 @@ void MainWindow::saveConfig(const QString &fname) const settings.setValue("wave/file", ui->waveOutputLine->text()); settings.setValue("wave/bformat", - ui->waveBFormatCheckBox->isChecked() ? QString("true") : QString(/*"false"*/) + ui->waveBFormatCheckBox->isChecked() ? QString{"true"} : QString{/*"false"*/} ); /* Remove empty keys @@ -1112,8 +1151,8 @@ void MainWindow::saveConfig(const QString &fname) const allkeys = settings.allKeys(); foreach(const QString &key, allkeys) { - QString str = settings.value(key).toString(); - if(str == QString()) + QString str{settings.value(key).toString()}; + if(str == QString{}) settings.remove(key); } } @@ -1139,10 +1178,7 @@ void MainWindow::updatePeriodSizeEdit(int size) { ui->periodSizeEdit->clear(); if(size >= 64) - { - size = (size+32)&~0x3f; ui->periodSizeEdit->insert(QString::number(size)); - } enableApplyButton(); } @@ -1188,13 +1224,13 @@ void MainWindow::select71DecoderFile() { selectDecoderFile(ui->decoder71LineEdit, "Select 7.1 Surround Decoder");} void MainWindow::selectDecoderFile(QLineEdit *line, const char *caption) { - QString dir = line->text(); + QString dir{line->text()}; if(dir.isEmpty() || QDir::isRelativePath(dir)) { - QStringList paths = getAllDataPaths("/openal/presets"); + QStringList paths{getAllDataPaths("/openal/presets")}; while(!paths.isEmpty()) { - if(QDir(paths.last()).exists()) + if(QDir{paths.last()}.exists()) { dir = paths.last(); break; @@ -1202,9 +1238,8 @@ void MainWindow::selectDecoderFile(QLineEdit *line, const char *caption) paths.removeLast(); } } - QString fname = QFileDialog::getOpenFileName(this, tr(caption), - dir, tr("AmbDec Files (*.ambdec);;All Files (*.*)") - ); + QString fname{QFileDialog::getOpenFileName(this, tr(caption), + dir, tr("AmbDec Files (*.ambdec);;All Files (*.*)"))}; if(!fname.isEmpty()) { line->setText(fname); @@ -1223,16 +1258,23 @@ void MainWindow::updateJackBufferSizeEdit(int size) void MainWindow::updateJackBufferSizeSlider() { - int value = ui->jackBufferSizeLine->text().toInt(); - int pos = (int)floor(log2(value) + 0.5); + int value{ui->jackBufferSizeLine->text().toInt()}; + auto pos = static_cast<int>(floor(log2(value) + 0.5)); ui->jackBufferSizeSlider->setSliderPosition(pos); enableApplyButton(); } +void MainWindow::updateHrtfModeLabel(int num) +{ + ui->hrtfmodeLabel->setText(hrtfModeList[num].name); + enableApplyButton(); +} + + void MainWindow::addHrtfFile() { - QString path = QFileDialog::getExistingDirectory(this, tr("Select HRTF Path")); + QString path{QFileDialog::getExistingDirectory(this, tr("Select HRTF Path"))}; if(path.isEmpty() == false && !getAllDataPaths("/openal/hrtf").contains(path)) { ui->hrtfFileList->addItem(path); @@ -1242,7 +1284,7 @@ void MainWindow::addHrtfFile() void MainWindow::removeHrtfFile() { - QList<QListWidgetItem*> selected = ui->hrtfFileList->selectedItems(); + QList<QListWidgetItem*> selected{ui->hrtfFileList->selectedItems()}; if(!selected.isEmpty()) { foreach(QListWidgetItem *item, selected) @@ -1258,37 +1300,37 @@ void MainWindow::updateHrtfRemoveButton() void MainWindow::showEnabledBackendMenu(QPoint pt) { - QMap<QAction*,QString> actionMap; + QHash<QAction*,QString> actionMap; pt = ui->enabledBackendList->mapToGlobal(pt); QMenu ctxmenu; - QAction *removeAction = ctxmenu.addAction(QIcon::fromTheme("list-remove"), "Remove"); + QAction *removeAction{ctxmenu.addAction(QIcon::fromTheme("list-remove"), "Remove")}; if(ui->enabledBackendList->selectedItems().size() == 0) removeAction->setEnabled(false); ctxmenu.addSeparator(); for(size_t i = 0;backendList[i].backend_name[0];i++) { - QString backend = backendList[i].full_string; - QAction *action = ctxmenu.addAction(QString("Add ")+backend); + QString backend{backendList[i].full_string}; + QAction *action{ctxmenu.addAction(QString("Add ")+backend)}; actionMap[action] = backend; if(ui->enabledBackendList->findItems(backend, Qt::MatchFixedString).size() != 0 || ui->disabledBackendList->findItems(backend, Qt::MatchFixedString).size() != 0) action->setEnabled(false); } - QAction *gotAction = ctxmenu.exec(pt); + QAction *gotAction{ctxmenu.exec(pt)}; if(gotAction == removeAction) { - QList<QListWidgetItem*> selected = ui->enabledBackendList->selectedItems(); + QList<QListWidgetItem*> selected{ui->enabledBackendList->selectedItems()}; foreach(QListWidgetItem *item, selected) delete item; enableApplyButton(); } - else if(gotAction != NULL) + else if(gotAction != nullptr) { - QMap<QAction*,QString>::const_iterator iter = actionMap.find(gotAction); - if(iter != actionMap.end()) + auto iter = actionMap.constFind(gotAction); + if(iter != actionMap.cend()) ui->enabledBackendList->addItem(iter.value()); enableApplyButton(); } @@ -1296,37 +1338,37 @@ void MainWindow::showEnabledBackendMenu(QPoint pt) void MainWindow::showDisabledBackendMenu(QPoint pt) { - QMap<QAction*,QString> actionMap; + QHash<QAction*,QString> actionMap; pt = ui->disabledBackendList->mapToGlobal(pt); QMenu ctxmenu; - QAction *removeAction = ctxmenu.addAction(QIcon::fromTheme("list-remove"), "Remove"); + QAction *removeAction{ctxmenu.addAction(QIcon::fromTheme("list-remove"), "Remove")}; if(ui->disabledBackendList->selectedItems().size() == 0) removeAction->setEnabled(false); ctxmenu.addSeparator(); for(size_t i = 0;backendList[i].backend_name[0];i++) { - QString backend = backendList[i].full_string; - QAction *action = ctxmenu.addAction(QString("Add ")+backend); + QString backend{backendList[i].full_string}; + QAction *action{ctxmenu.addAction(QString("Add ")+backend)}; actionMap[action] = backend; if(ui->disabledBackendList->findItems(backend, Qt::MatchFixedString).size() != 0 || ui->enabledBackendList->findItems(backend, Qt::MatchFixedString).size() != 0) action->setEnabled(false); } - QAction *gotAction = ctxmenu.exec(pt); + QAction *gotAction{ctxmenu.exec(pt)}; if(gotAction == removeAction) { - QList<QListWidgetItem*> selected = ui->disabledBackendList->selectedItems(); + QList<QListWidgetItem*> selected{ui->disabledBackendList->selectedItems()}; foreach(QListWidgetItem *item, selected) delete item; enableApplyButton(); } - else if(gotAction != NULL) + else if(gotAction != nullptr) { - QMap<QAction*,QString>::const_iterator iter = actionMap.find(gotAction); - if(iter != actionMap.end()) + auto iter = actionMap.constFind(gotAction); + if(iter != actionMap.cend()) ui->disabledBackendList->addItem(iter.value()); enableApplyButton(); } @@ -1334,9 +1376,9 @@ void MainWindow::showDisabledBackendMenu(QPoint pt) void MainWindow::selectOSSPlayback() { - QString current = ui->ossDefaultDeviceLine->text(); + QString current{ui->ossDefaultDeviceLine->text()}; if(current.isEmpty()) current = ui->ossDefaultDeviceLine->placeholderText(); - QString fname = QFileDialog::getOpenFileName(this, tr("Select Playback Device"), current); + QString fname{QFileDialog::getOpenFileName(this, tr("Select Playback Device"), current)}; if(!fname.isEmpty()) { ui->ossDefaultDeviceLine->setText(fname); @@ -1346,9 +1388,9 @@ void MainWindow::selectOSSPlayback() void MainWindow::selectOSSCapture() { - QString current = ui->ossDefaultCaptureLine->text(); + QString current{ui->ossDefaultCaptureLine->text()}; if(current.isEmpty()) current = ui->ossDefaultCaptureLine->placeholderText(); - QString fname = QFileDialog::getOpenFileName(this, tr("Select Capture Device"), current); + QString fname{QFileDialog::getOpenFileName(this, tr("Select Capture Device"), current)}; if(!fname.isEmpty()) { ui->ossDefaultCaptureLine->setText(fname); @@ -1358,9 +1400,9 @@ void MainWindow::selectOSSCapture() void MainWindow::selectSolarisPlayback() { - QString current = ui->solarisDefaultDeviceLine->text(); + QString current{ui->solarisDefaultDeviceLine->text()}; if(current.isEmpty()) current = ui->solarisDefaultDeviceLine->placeholderText(); - QString fname = QFileDialog::getOpenFileName(this, tr("Select Playback Device"), current); + QString fname{QFileDialog::getOpenFileName(this, tr("Select Playback Device"), current)}; if(!fname.isEmpty()) { ui->solarisDefaultDeviceLine->setText(fname); @@ -1370,9 +1412,8 @@ void MainWindow::selectSolarisPlayback() void MainWindow::selectWaveOutput() { - QString fname = QFileDialog::getSaveFileName(this, tr("Select Wave File Output"), - ui->waveOutputLine->text(), tr("Wave Files (*.wav *.amb);;All Files (*.*)") - ); + QString fname{QFileDialog::getSaveFileName(this, tr("Select Wave File Output"), + ui->waveOutputLine->text(), tr("Wave Files (*.wav *.amb);;All Files (*.*)"))}; if(!fname.isEmpty()) { ui->waveOutputLine->setText(fname); diff --git a/utils/alsoft-config/mainwindow.h b/utils/alsoft-config/mainwindow.h index 8b763845..ca53582b 100644 --- a/utils/alsoft-config/mainwindow.h +++ b/utils/alsoft-config/mainwindow.h @@ -43,6 +43,7 @@ private slots: void updateJackBufferSizeEdit(int size); void updateJackBufferSizeSlider(); + void updateHrtfModeLabel(int num); void addHrtfFile(); void removeHrtfFile(); diff --git a/utils/alsoft-config/mainwindow.ui b/utils/alsoft-config/mainwindow.ui index 9c89cbc7..0506304e 100644 --- a/utils/alsoft-config/mainwindow.ui +++ b/utils/alsoft-config/mainwindow.ui @@ -7,7 +7,7 @@ <x>0</x> <y>0</y> <width>564</width> - <height>460</height> + <height>469</height> </rect> </property> <property name="minimumSize"> @@ -21,8 +21,7 @@ </property> <property name="windowIcon"> <iconset theme="preferences-desktop-sound"> - <normaloff/> - </iconset> + <normaloff>.</normaloff>.</iconset> </property> <widget class="QWidget" name="centralWidget"> <widget class="QPushButton" name="applyButton"> @@ -31,7 +30,7 @@ <x>470</x> <y>405</y> <width>81</width> - <height>21</height> + <height>31</height> </rect> </property> <property name="text"> @@ -39,8 +38,7 @@ </property> <property name="icon"> <iconset theme="dialog-ok-apply"> - <normaloff/> - </iconset> + <normaloff>.</normaloff>.</iconset> </property> </widget> <widget class="QTabWidget" name="tabWidget"> @@ -53,7 +51,7 @@ </rect> </property> <property name="currentIndex"> - <number>5</number> + <number>0</number> </property> <widget class="QWidget" name="tab_3"> <attribute name="title"> @@ -64,8 +62,8 @@ <rect> <x>110</x> <y>50</y> - <width>78</width> - <height>21</height> + <width>76</width> + <height>31</height> </rect> </property> <property name="toolTip"> @@ -82,7 +80,7 @@ float and converted to the output sample type as needed.</string> <x>0</x> <y>50</y> <width>101</width> - <height>21</height> + <height>31</height> </rect> </property> <property name="text"> @@ -98,7 +96,7 @@ float and converted to the output sample type as needed.</string> <x>0</x> <y>20</y> <width>101</width> - <height>21</height> + <height>31</height> </rect> </property> <property name="text"> @@ -113,8 +111,8 @@ float and converted to the output sample type as needed.</string> <rect> <x>110</x> <y>20</y> - <width>78</width> - <height>21</height> + <width>76</width> + <height>31</height> </rect> </property> <property name="toolTip"> @@ -131,8 +129,8 @@ to stereo output.</string> <rect> <x>380</x> <y>20</y> - <width>80</width> - <height>20</height> + <width>96</width> + <height>31</height> </rect> </property> <property name="toolTip"> @@ -194,7 +192,7 @@ to stereo output.</string> <x>290</x> <y>20</y> <width>81</width> - <height>21</height> + <height>31</height> </rect> </property> <property name="text"> @@ -210,7 +208,7 @@ to stereo output.</string> <x>290</x> <y>50</y> <width>81</width> - <height>21</height> + <height>31</height> </rect> </property> <property name="text"> @@ -225,8 +223,8 @@ to stereo output.</string> <rect> <x>380</x> <y>50</y> - <width>78</width> - <height>21</height> + <width>101</width> + <height>31</height> </rect> </property> <property name="toolTip"> @@ -256,7 +254,7 @@ otherwise be suitable for speakers.</string> <x>20</x> <y>30</y> <width>511</width> - <height>91</height> + <height>81</height> </rect> </property> <property name="title"> @@ -283,9 +281,9 @@ mixed and being heard.</string> <widget class="QLabel" name="label_11"> <property name="geometry"> <rect> - <x>20</x> + <x>60</x> <y>0</y> - <width>201</width> + <width>161</width> <height>21</height> </rect> </property> @@ -299,9 +297,9 @@ mixed and being heard.</string> <widget class="QSlider" name="periodCountSlider"> <property name="geometry"> <rect> - <x>80</x> + <x>99</x> <y>20</y> - <width>160</width> + <width>141</width> <height>21</height> </rect> </property> @@ -336,7 +334,7 @@ mixed and being heard.</string> <widget class="QLineEdit" name="periodCountEdit"> <property name="geometry"> <rect> - <x>20</x> + <x>40</x> <y>20</y> <width>51</width> <height>21</height> @@ -365,24 +363,24 @@ frames needed for each mixing update.</string> <rect> <x>60</x> <y>20</y> - <width>160</width> + <width>191</width> <height>21</height> </rect> </property> <property name="minimum"> - <number>0</number> + <number>63</number> </property> <property name="maximum"> <number>8192</number> </property> <property name="singleStep"> - <number>64</number> + <number>1</number> </property> <property name="pageStep"> <number>1024</number> </property> <property name="value"> - <number>0</number> + <number>63</number> </property> <property name="tracking"> <bool>true</bool> @@ -423,7 +421,7 @@ frames needed for each mixing update.</string> </rect> </property> <property name="placeholderText"> - <string>1024</string> + <string>20ms</string> </property> </widget> </widget> @@ -432,9 +430,9 @@ frames needed for each mixing update.</string> <property name="geometry"> <rect> <x>130</x> - <y>130</y> - <width>131</width> - <height>21</height> + <y>120</y> + <width>111</width> + <height>31</height> </rect> </property> <property name="toolTip"> @@ -451,9 +449,9 @@ receiver.</string> <property name="geometry"> <rect> <x>20</x> - <y>130</y> + <y>120</y> <width>101</width> - <height>21</height> + <height>31</height> </rect> </property> <property name="text"> @@ -466,23 +464,26 @@ receiver.</string> <widget class="QLabel" name="label_30"> <property name="geometry"> <rect> - <x>270</x> - <y>130</y> - <width>111</width> - <height>21</height> + <x>260</x> + <y>120</y> + <width>121</width> + <height>31</height> </rect> </property> <property name="text"> <string>Ambisonic Format:</string> </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> </widget> <widget class="QComboBox" name="ambiFormatComboBox"> <property name="geometry"> <rect> <x>390</x> - <y>130</y> + <y>120</y> <width>131</width> - <height>21</height> + <height>31</height> </rect> </property> </widget> @@ -533,7 +534,7 @@ quantization with low-level whitenoise.</string> <property name="geometry"> <rect> <x>60</x> - <y>80</y> + <y>90</y> <width>421</width> <height>81</height> </rect> @@ -611,7 +612,7 @@ quantization with low-level whitenoise.</string> <widget class="QCheckBox" name="decoderHQModeCheckBox"> <property name="geometry"> <rect> - <x>10</x> + <x>30</x> <y>20</y> <width>181</width> <height>21</height> @@ -635,7 +636,7 @@ appropriate speaker configuration you intend to use.</string> <widget class="QCheckBox" name="decoderDistCompCheckBox"> <property name="geometry"> <rect> - <x>10</x> + <x>30</x> <y>50</y> <width>181</width> <height>21</height> @@ -655,7 +656,7 @@ configuration file.</string> <property name="text"> <string>Distance Compensation:</string> </property> - <property name="checked"> + <property name="tristate"> <bool>true</bool> </property> </widget> @@ -834,7 +835,7 @@ configuration file.</string> <widget class="QCheckBox" name="decoderNFEffectsCheckBox"> <property name="geometry"> <rect> - <x>10</x> + <x>30</x> <y>80</y> <width>181</width> <height>21</height> @@ -847,8 +848,7 @@ creates a more realistic perception of sound distance. Note that the effect may be stronger or weaker than intended if the application doesn't use or specify an appropriate unit scale, or if incorrect speaker distances -are set in the decoder configuration file. Requires High -Quality Mode to be enabled.</string> +are set in the decoder configuration file.</string> </property> <property name="layoutDirection"> <enum>Qt::RightToLeft</enum> @@ -856,7 +856,7 @@ Quality Mode to be enabled.</string> <property name="text"> <string>Near-Field Effects:</string> </property> - <property name="checked"> + <property name="tristate"> <bool>true</bool> </property> </widget> @@ -885,7 +885,7 @@ normal output is created with no near-field simulation.</string> <rect> <x>20</x> <y>0</y> - <width>151</width> + <width>171</width> <height>21</height> </rect> </property> @@ -899,9 +899,9 @@ normal output is created with no near-field simulation.</string> <widget class="QDoubleSpinBox" name="decoderNFRefDelaySpinBox"> <property name="geometry"> <rect> - <x>180</x> + <x>200</x> <y>0</y> - <width>91</width> + <width>81</width> <height>21</height> </rect> </property> @@ -999,8 +999,7 @@ normal output is created with no near-field simulation.</string> </property> <property name="icon"> <iconset theme="list-add"> - <normaloff/> - </iconset> + <normaloff>.</normaloff>.</iconset> </property> <property name="flat"> <bool>false</bool> @@ -1040,8 +1039,7 @@ listed above.</string> </property> <property name="icon"> <iconset theme="list-remove"> - <normaloff/> - </iconset> + <normaloff>.</normaloff>.</iconset> </property> </widget> </widget> @@ -1049,10 +1047,10 @@ listed above.</string> <widget class="QLabel" name="label_16"> <property name="geometry"> <rect> - <x>40</x> - <y>50</y> + <x>50</x> + <y>60</y> <width>71</width> - <height>21</height> + <height>31</height> </rect> </property> <property name="text"> @@ -1066,9 +1064,9 @@ listed above.</string> <property name="geometry"> <rect> <x>130</x> - <y>50</y> + <y>60</y> <width>161</width> - <height>21</height> + <height>31</height> </rect> </property> <property name="toolTip"> @@ -1097,10 +1095,10 @@ application or system to determine if it should be used.</string> <widget class="QLabel" name="label_12"> <property name="geometry"> <rect> - <x>20</x> + <x>30</x> <y>20</y> <width>91</width> - <height>21</height> + <height>31</height> </rect> </property> <property name="text"> @@ -1116,13 +1114,84 @@ application or system to determine if it should be used.</string> <x>130</x> <y>20</y> <width>161</width> - <height>21</height> + <height>31</height> </rect> </property> <property name="toolTip"> <string>The default HRTF to use if the application doesn't request one.</string> </property> </widget> + <widget class="QGroupBox" name="groupBox_9"> + <property name="geometry"> + <rect> + <x>50</x> + <y>100</y> + <width>441</width> + <height>81</height> + </rect> + </property> + <property name="title"> + <string>HRTF Render Method</string> + </property> + <widget class="QLabel" name="label_31"> + <property name="geometry"> + <rect> + <x>20</x> + <y>30</y> + <width>51</width> + <height>21</height> + </rect> + </property> + <property name="text"> + <string>Speed</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + <widget class="QLabel" name="label_32"> + <property name="geometry"> + <rect> + <x>340</x> + <y>30</y> + <width>51</width> + <height>21</height> + </rect> + </property> + <property name="text"> + <string>Quality</string> + </property> + </widget> + <widget class="QSlider" name="hrtfmodeSlider"> + <property name="geometry"> + <rect> + <x>80</x> + <y>30</y> + <width>251</width> + <height>21</height> + </rect> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + <widget class="QLabel" name="hrtfmodeLabel"> + <property name="geometry"> + <rect> + <x>50</x> + <y>50</y> + <width>321</width> + <height>21</height> + </rect> + </property> + <property name="text"> + <string>Default</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </widget> </widget> <widget class="QWidget" name="tab"> <attribute name="title"> @@ -1185,6 +1254,9 @@ application or system to determine if it should be used.</string> <height>361</height> </rect> </property> + <property name="currentIndex"> + <number>0</number> + </property> <widget class="QWidget" name="page"> <widget class="QCheckBox" name="backendCheckBox"> <property name="geometry"> @@ -1279,7 +1351,7 @@ is not already running.</string> <property name="text"> <string>AutoSpawn Server</string> </property> - <property name="checked"> + <property name="tristate"> <bool>true</bool> </property> </widget> @@ -1301,6 +1373,9 @@ to match the new device.</string> <property name="text"> <string>Allow Moving Streams</string> </property> + <property name="tristate"> + <bool>true</bool> + </property> </widget> <widget class="QCheckBox" name="pulseFixRateCheckBox"> <property name="geometry"> @@ -1318,6 +1393,32 @@ rate to match the PulseAudio device.</string> <property name="text"> <string>Fix Sample Rate</string> </property> + <property name="tristate"> + <bool>true</bool> + </property> + </widget> + <widget class="QCheckBox" name="pulseAdjLatencyCheckBox"> + <property name="geometry"> + <rect> + <x>20</x> + <y>100</y> + <width>111</width> + <height>21</height> + </rect> + </property> + <property name="toolTip"> + <string>Attempts to adjust the overall latency of device +playback. Note that this may have adverse effects +on the resulting internal buffer sizes and mixing +updates, leading to performance problems and +drop-outs.</string> + </property> + <property name="text"> + <string>Adjust Latency</string> + </property> + <property name="tristate"> + <bool>true</bool> + </property> </widget> </widget> <widget class="QWidget" name="page_7"> @@ -1333,6 +1434,9 @@ rate to match the PulseAudio device.</string> <property name="text"> <string>AutoSpawn Server</string> </property> + <property name="tristate"> + <bool>true</bool> + </property> </widget> <widget class="QGroupBox" name="groupBox_7"> <property name="geometry"> @@ -1474,6 +1578,9 @@ resample pass on top of OpenAL's resampler.</string> <property name="text"> <string>Allow Resampler</string> </property> + <property name="tristate"> + <bool>true</bool> + </property> </widget> <widget class="QCheckBox" name="alsaMmapCheckBox"> <property name="geometry"> @@ -1492,7 +1599,7 @@ during updates.</string> <property name="text"> <string>MMap Buffer</string> </property> - <property name="checked"> + <property name="tristate"> <bool>true</bool> </property> </widget> @@ -2156,6 +2263,22 @@ added by the ALC_EXT_DEDICATED extension.</string> <bool>true</bool> </property> </widget> + <widget class="QCheckBox" name="enableVocalMorpherCheck"> + <property name="geometry"> + <rect> + <x>320</x> + <y>210</y> + <width>131</width> + <height>21</height> + </rect> + </property> + <property name="text"> + <string>Vocal morpher</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> </widget> <widget class="QLabel" name="label_13"> <property name="geometry"> @@ -2163,7 +2286,7 @@ added by the ALC_EXT_DEDICATED extension.</string> <x>10</x> <y>20</y> <width>141</width> - <height>21</height> + <height>31</height> </rect> </property> <property name="text"> @@ -2178,8 +2301,8 @@ added by the ALC_EXT_DEDICATED extension.</string> <rect> <x>160</x> <y>20</y> - <width>108</width> - <height>20</height> + <width>131</width> + <height>31</height> </rect> </property> <property name="sizeAdjustPolicy"> @@ -2329,7 +2452,7 @@ added by the ALC_EXT_DEDICATED extension.</string> <x>370</x> <y>405</y> <width>91</width> - <height>21</height> + <height>31</height> </rect> </property> <property name="text"> @@ -2337,8 +2460,7 @@ added by the ALC_EXT_DEDICATED extension.</string> </property> <property name="icon"> <iconset theme="window-close"> - <normaloff/> - </iconset> + <normaloff>.</normaloff>.</iconset> </property> </widget> </widget> @@ -2348,7 +2470,7 @@ added by the ALC_EXT_DEDICATED extension.</string> <x>0</x> <y>0</y> <width>564</width> - <height>21</height> + <height>29</height> </rect> </property> <widget class="QMenu" name="menuFile"> @@ -2372,8 +2494,7 @@ added by the ALC_EXT_DEDICATED extension.</string> <action name="actionQuit"> <property name="icon"> <iconset theme="application-exit"> - <normaloff/> - </iconset> + <normaloff>.</normaloff>.</iconset> </property> <property name="text"> <string>&Quit</string> @@ -2382,8 +2503,7 @@ added by the ALC_EXT_DEDICATED extension.</string> <action name="actionSave_As"> <property name="icon"> <iconset theme="document-save-as"> - <normaloff/> - </iconset> + <normaloff>.</normaloff>.</iconset> </property> <property name="text"> <string>Save &As...</string> @@ -2395,8 +2515,7 @@ added by the ALC_EXT_DEDICATED extension.</string> <action name="actionLoad"> <property name="icon"> <iconset theme="document-open"> - <normaloff/> - </iconset> + <normaloff>.</normaloff>.</iconset> </property> <property name="text"> <string>&Load...</string> @@ -2415,22 +2534,6 @@ added by the ALC_EXT_DEDICATED extension.</string> <resources/> <connections> <connection> - <sender>actionQuit</sender> - <signal>activated()</signal> - <receiver>MainWindow</receiver> - <slot>close()</slot> - <hints> - <hint type="sourcelabel"> - <x>-1</x> - <y>-1</y> - </hint> - <hint type="destinationlabel"> - <x>267</x> - <y>181</y> - </hint> - </hints> - </connection> - <connection> <sender>backendListWidget</sender> <signal>currentRowChanged(int)</signal> <receiver>backendStackedWidget</receiver> diff --git a/utils/alsoft-config/verstr.cpp b/utils/alsoft-config/verstr.cpp new file mode 100644 index 00000000..42b1aeac --- /dev/null +++ b/utils/alsoft-config/verstr.cpp @@ -0,0 +1,10 @@ + +#include "verstr.h" + +#include "version.h" + + +QString GetVersionString() +{ + return QStringLiteral(ALSOFT_VERSION "-" ALSOFT_GIT_COMMIT_HASH " (" ALSOFT_GIT_BRANCH " branch)."); +} diff --git a/utils/alsoft-config/verstr.h b/utils/alsoft-config/verstr.h new file mode 100644 index 00000000..73e3ecbd --- /dev/null +++ b/utils/alsoft-config/verstr.h @@ -0,0 +1,8 @@ +#ifndef VERSTR_H +#define VERSTR_H + +#include <QString> + +QString GetVersionString(); + +#endif /* VERSTR_H */ diff --git a/utils/bsincgen.c b/utils/bsincgen.c deleted file mode 100644 index 03421da9..00000000 --- a/utils/bsincgen.c +++ /dev/null @@ -1,404 +0,0 @@ -/*
- * Sinc interpolator coefficient and delta generator for the OpenAL Soft
- * cross platform audio library.
- *
- * Copyright (C) 2015 by Christopher Fitzgerald.
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Library General Public
- * License as published by the Free Software Foundation; either
- * version 2 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Library General Public License for more details.
- *
- * You should have received a copy of the GNU Library General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- * MA 02110-1301 USA
- *
- * Or visit: http://www.gnu.org/licenses/old-licenses/lgpl-2.0.html
- *
- * --------------------------------------------------------------------------
- *
- * This is a modified version of the bandlimited windowed sinc interpolator
- * algorithm presented here:
- *
- * Smith, J.O. "Windowed Sinc Interpolation", in
- * Physical Audio Signal Processing,
- * https://ccrma.stanford.edu/~jos/pasp/Windowed_Sinc_Interpolation.html,
- * online book,
- * accessed October 2012.
- */
-
-#define _UNICODE
-#include <stdio.h>
-#include <math.h>
-#include <string.h>
-#include <stdlib.h>
-
-#include "win_main_utf8.h"
-
-
-#ifndef M_PI
-#define M_PI (3.14159265358979323846)
-#endif
-
-#if defined(__ANDROID__) && !(defined(_ISOC99_SOURCE) || (defined(_POSIX_C_SOURCE) && _POSIX_C_SOURCE >= 200112L))
-#define log2(x) (log(x) / log(2.0))
-#endif
-
-// The number of distinct scale and phase intervals within the filter table.
-// Must be the same as in alu.h!
-#define BSINC_SCALE_COUNT (16)
-#define BSINC_PHASE_COUNT (16)
-
-/* 48 points includes the doubling for downsampling, so the maximum number of
- * base sample points is 24, which is 23rd order.
- */
-#define BSINC_POINTS_MAX (48)
-
-static double MinDouble(double a, double b)
-{ return (a <= b) ? a : b; }
-
-static double MaxDouble(double a, double b)
-{ return (a >= b) ? a : b; }
-
-/* NOTE: This is the normalized (instead of just sin(x)/x) cardinal sine
- * function.
- * 2 f_t sinc(2 f_t x)
- * f_t -- normalized transition frequency (0.5 is nyquist)
- * x -- sample index (-N to N)
- */
-static double Sinc(const double x)
-{
- if(fabs(x) < 1e-15)
- return 1.0;
- return sin(M_PI * x) / (M_PI * x);
-}
-
-static double BesselI_0(const double x)
-{
- double term, sum, last_sum, x2, y;
- int i;
-
- term = 1.0;
- sum = 1.0;
- x2 = x / 2.0;
- i = 1;
-
- do {
- y = x2 / i;
- i++;
- last_sum = sum;
- term *= y * y;
- sum += term;
- } while(sum != last_sum);
-
- return sum;
-}
-
-/* NOTE: k is assumed normalized (-1 to 1)
- * beta is equivalent to 2 alpha
- */
-static double Kaiser(const double b, const double k)
-{
- if(!(k >= -1.0 && k <= 1.0))
- return 0.0;
- return BesselI_0(b * sqrt(1.0 - k*k)) / BesselI_0(b);
-}
-
-/* Calculates the (normalized frequency) transition width of the Kaiser window.
- * Rejection is in dB.
- */
-static double CalcKaiserWidth(const double rejection, const int order)
-{
- double w_t = 2.0 * M_PI;
-
- if(rejection > 21.0)
- return (rejection - 7.95) / (order * 2.285 * w_t);
- /* This enforces a minimum rejection of just above 21.18dB */
- return 5.79 / (order * w_t);
-}
-
-static double CalcKaiserBeta(const double rejection)
-{
- if(rejection > 50.0)
- return 0.1102 * (rejection - 8.7);
- else if(rejection >= 21.0)
- return (0.5842 * pow(rejection - 21.0, 0.4)) +
- (0.07886 * (rejection - 21.0));
- return 0.0;
-}
-
-/* Generates the coefficient, delta, and index tables required by the bsinc resampler */
-static void BsiGenerateTables(FILE *output, const char *tabname, const double rejection, const int order)
-{
- static double filter[BSINC_SCALE_COUNT][BSINC_PHASE_COUNT + 1][BSINC_POINTS_MAX];
- static double scDeltas[BSINC_SCALE_COUNT][BSINC_PHASE_COUNT ][BSINC_POINTS_MAX];
- static double phDeltas[BSINC_SCALE_COUNT][BSINC_PHASE_COUNT + 1][BSINC_POINTS_MAX];
- static double spDeltas[BSINC_SCALE_COUNT][BSINC_PHASE_COUNT ][BSINC_POINTS_MAX];
- static int mt[BSINC_SCALE_COUNT];
- static double at[BSINC_SCALE_COUNT];
- const int num_points_min = order + 1;
- double width, beta, scaleBase, scaleRange;
- int si, pi, i;
-
- memset(filter, 0, sizeof(filter));
- memset(scDeltas, 0, sizeof(scDeltas));
- memset(phDeltas, 0, sizeof(phDeltas));
- memset(spDeltas, 0, sizeof(spDeltas));
-
- /* Calculate windowing parameters. The width describes the transition
- band, but it may vary due to the linear interpolation between scales
- of the filter.
- */
- width = CalcKaiserWidth(rejection, order);
- beta = CalcKaiserBeta(rejection);
- scaleBase = width / 2.0;
- scaleRange = 1.0 - scaleBase;
-
- // Determine filter scaling.
- for(si = 0; si < BSINC_SCALE_COUNT; si++)
- {
- const double scale = scaleBase + (scaleRange * si / (BSINC_SCALE_COUNT - 1));
- const double a = MinDouble(floor(num_points_min / (2.0 * scale)), num_points_min);
- const int m = 2 * (int)a;
-
- mt[si] = m;
- at[si] = a;
- }
-
- /* Calculate the Kaiser-windowed Sinc filter coefficients for each scale
- and phase.
- */
- for(si = 0; si < BSINC_SCALE_COUNT; si++)
- {
- const int m = mt[si];
- const int o = num_points_min - (m / 2);
- const int l = (m / 2) - 1;
- const double a = at[si];
- const double scale = scaleBase + (scaleRange * si / (BSINC_SCALE_COUNT - 1));
- const double cutoff = (0.5 * scale) - (scaleBase * MaxDouble(0.5, scale));
-
- for(pi = 0; pi <= BSINC_PHASE_COUNT; pi++)
- {
- const double phase = l + ((double)pi / BSINC_PHASE_COUNT);
-
- for(i = 0; i < m; i++)
- {
- const double x = i - phase;
- filter[si][pi][o + i] = Kaiser(beta, x / a) * 2.0 * cutoff * Sinc(2.0 * cutoff * x);
- }
- }
- }
-
- /* Linear interpolation between scales is simplified by pre-calculating
- the delta (b - a) in: x = a + f (b - a)
-
- Given a difference in points between scales, the destination points
- will be 0, thus: x = a + f (-a)
- */
- for(si = 0; si < (BSINC_SCALE_COUNT - 1); si++)
- {
- const int m = mt[si];
- const int o = num_points_min - (m / 2);
-
- for(pi = 0; pi < BSINC_PHASE_COUNT; pi++)
- {
- for(i = 0; i < m; i++)
- scDeltas[si][pi][o + i] = filter[si + 1][pi][o + i] - filter[si][pi][o + i];
- }
- }
-
- // Linear interpolation between phases is also simplified.
- for(si = 0; si < BSINC_SCALE_COUNT; si++)
- {
- const int m = mt[si];
- const int o = num_points_min - (m / 2);
-
- for(pi = 0; pi < BSINC_PHASE_COUNT; pi++)
- {
- for(i = 0; i < m; i++)
- phDeltas[si][pi][o + i] = filter[si][pi + 1][o + i] - filter[si][pi][o + i];
- }
- }
-
- /* This last simplification is done to complete the bilinear equation for
- the combination of scale and phase.
- */
- for(si = 0; si < (BSINC_SCALE_COUNT - 1); si++)
- {
- const int m = mt[si];
- const int o = num_points_min - (m / 2);
-
- for(pi = 0; pi < BSINC_PHASE_COUNT; pi++)
- {
- for(i = 0; i < m; i++)
- spDeltas[si][pi][o + i] = phDeltas[si + 1][pi][o + i] - phDeltas[si][pi][o + i];
- }
- }
-
- // Make sure the number of points is a multiple of 4 (for SIMD).
- for(si = 0; si < BSINC_SCALE_COUNT; si++)
- mt[si] = (mt[si]+3) & ~3;
-
- fprintf(output,
-"/* This %d%s order filter has a rejection of -%.0fdB, yielding a transition width\n"
-" * of ~%.3f (normalized frequency). Order increases when downsampling to a\n"
-" * limit of one octave, after which the quality of the filter (transition\n"
-" * width) suffers to reduce the CPU cost. The bandlimiting will cut all sound\n"
-" * after downsampling by ~%.2f octaves.\n"
-" */\n"
-"const BSincTable %s = {\n",
- order, (((order%100)/10) == 1) ? "th" :
- ((order%10) == 1) ? "st" :
- ((order%10) == 2) ? "nd" :
- ((order%10) == 3) ? "rd" : "th",
- rejection, width, log2(1.0/scaleBase), tabname);
-
- /* The scaleBase is calculated from the Kaiser window transition width.
- It represents the absolute limit to the filter before it fully cuts
- the signal. The limit in octaves can be calculated by taking the
- base-2 logarithm of its inverse: log_2(1 / scaleBase)
- */
- fprintf(output, " /* scaleBase */ %.9ef, /* scaleRange */ %.9ef,\n", scaleBase, 1.0 / scaleRange);
-
- fprintf(output, " /* m */ {");
- fprintf(output, " %d", mt[0]);
- for(si = 1; si < BSINC_SCALE_COUNT; si++)
- fprintf(output, ", %d", mt[si]);
- fprintf(output, " },\n");
-
- fprintf(output, " /* filterOffset */ {");
- fprintf(output, " %d", 0);
- i = mt[0]*4*BSINC_PHASE_COUNT;
- for(si = 1; si < BSINC_SCALE_COUNT; si++)
- {
- fprintf(output, ", %d", i);
- i += mt[si]*4*BSINC_PHASE_COUNT;
- }
-
- fprintf(output, " },\n");
-
- // Calculate the table size.
- i = 0;
- for(si = 0; si < BSINC_SCALE_COUNT; si++)
- i += 4 * BSINC_PHASE_COUNT * mt[si];
-
- fprintf(output, "\n /* Tab (%d entries) */ {\n", i);
- for(si = 0; si < BSINC_SCALE_COUNT; si++)
- {
- const int m = mt[si];
- const int o = num_points_min - (m / 2);
-
- for(pi = 0; pi < BSINC_PHASE_COUNT; pi++)
- {
- fprintf(output, " /* %2d,%2d (%d) */", si, pi, m);
- fprintf(output, "\n ");
- for(i = 0; i < m; i++)
- fprintf(output, " %+14.9ef,", filter[si][pi][o + i]);
- fprintf(output, "\n ");
- for(i = 0; i < m; i++)
- fprintf(output, " %+14.9ef,", scDeltas[si][pi][o + i]);
- fprintf(output, "\n ");
- for(i = 0; i < m; i++)
- fprintf(output, " %+14.9ef,", phDeltas[si][pi][o + i]);
- fprintf(output, "\n ");
- for(i = 0; i < m; i++)
- fprintf(output, " %+14.9ef,", spDeltas[si][pi][o + i]);
- fprintf(output, "\n");
- }
- }
- fprintf(output, " }\n};\n\n");
-}
-
-
-/* These methods generate a much simplified 4-point sinc interpolator using a
- * Kaiser window. This is much simpler to process at run-time, but has notably
- * more aliasing noise.
- */
-
-/* Same as in alu.h! */
-#define FRACTIONBITS (12)
-#define FRACTIONONE (1<<FRACTIONBITS)
-
-static void Sinc4GenerateTables(FILE *output, const double rejection)
-{
- static double filter[FRACTIONONE][4];
-
- const double width = CalcKaiserWidth(rejection, 3);
- const double beta = CalcKaiserBeta(rejection);
- const double scaleBase = width / 2.0;
- const double scaleRange = 1.0 - scaleBase;
- const double scale = scaleBase + scaleRange;
- const double a = MinDouble(4.0, floor(4.0 / (2.0*scale)));
- const int m = 2 * (int)a;
- const int l = (m/2) - 1;
- int pi;
- for(pi = 0;pi < FRACTIONONE;pi++)
- {
- const double phase = l + ((double)pi / FRACTIONONE);
- int i;
-
- for(i = 0;i < m;i++)
- {
- double x = i - phase;
- filter[pi][i] = Kaiser(beta, x / a) * Sinc(x);
- }
- }
-
- fprintf(output, "alignas(16) static const float sinc4Tab[FRACTIONONE][4] = {\n");
- for(pi = 0;pi < FRACTIONONE;pi++)
- fprintf(output, " { %+14.9ef, %+14.9ef, %+14.9ef, %+14.9ef },\n",
- filter[pi][0], filter[pi][1], filter[pi][2], filter[pi][3]);
- fprintf(output, "};\n\n");
-}
-
-
-int main(int argc, char *argv[])
-{
- FILE *output;
-
- if(argc > 2)
- {
- fprintf(stderr, "Usage: %s [output file]\n", argv[0]);
- return 1;
- }
-
- if(argc == 2)
- {
- output = fopen(argv[1], "wb");
- if(!output)
- {
- fprintf(stderr, "Failed to open %s for writing\n", argv[1]);
- return 1;
- }
- }
- else
- output = stdout;
-
- fprintf(output, "/* Generated by bsincgen, do not edit! */\n\n"
-"static_assert(BSINC_SCALE_COUNT == %d, \"Unexpected BSINC_SCALE_COUNT value!\");\n"
-"static_assert(BSINC_PHASE_COUNT == %d, \"Unexpected BSINC_PHASE_COUNT value!\");\n"
-"static_assert(FRACTIONONE == %d, \"Unexpected FRACTIONONE value!\");\n\n"
-"typedef struct BSincTable {\n"
-" const float scaleBase, scaleRange;\n"
-" const int m[BSINC_SCALE_COUNT];\n"
-" const int filterOffset[BSINC_SCALE_COUNT];\n"
-" alignas(16) const float Tab[];\n"
-"} BSincTable;\n\n", BSINC_SCALE_COUNT, BSINC_PHASE_COUNT, FRACTIONONE);
- /* A 23rd order filter with a -60dB drop at nyquist. */
- BsiGenerateTables(output, "bsinc24", 60.0, 23);
- /* An 11th order filter with a -60dB drop at nyquist. */
- BsiGenerateTables(output, "bsinc12", 60.0, 11);
- Sinc4GenerateTables(output, 60.0);
-
- if(output != stdout)
- fclose(output);
- output = NULL;
-
- return 0;
-}
diff --git a/utils/makehrtf.c b/utils/makehrtf.c deleted file mode 100644 index 0bd36849..00000000 --- a/utils/makehrtf.c +++ /dev/null @@ -1,3455 +0,0 @@ -/* - * HRTF utility for producing and demonstrating the process of creating an - * OpenAL Soft compatible HRIR data set. - * - * Copyright (C) 2011-2017 Christopher Fitzgerald - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Or visit: http://www.gnu.org/licenses/old-licenses/gpl-2.0.html - * - * -------------------------------------------------------------------------- - * - * A big thanks goes out to all those whose work done in the field of - * binaural sound synthesis using measured HRTFs makes this utility and the - * OpenAL Soft implementation possible. - * - * The algorithm for diffuse-field equalization was adapted from the work - * done by Rio Emmanuel and Larcher Veronique of IRCAM and Bill Gardner of - * MIT Media Laboratory. It operates as follows: - * - * 1. Take the FFT of each HRIR and only keep the magnitude responses. - * 2. Calculate the diffuse-field power-average of all HRIRs weighted by - * their contribution to the total surface area covered by their - * measurement. - * 3. Take the diffuse-field average and limit its magnitude range. - * 4. Equalize the responses by using the inverse of the diffuse-field - * average. - * 5. Reconstruct the minimum-phase responses. - * 5. Zero the DC component. - * 6. IFFT the result and truncate to the desired-length minimum-phase FIR. - * - * The spherical head algorithm for calculating propagation delay was adapted - * from the paper: - * - * Modeling Interaural Time Difference Assuming a Spherical Head - * Joel David Miller - * Music 150, Musical Acoustics, Stanford University - * December 2, 2001 - * - * The formulae for calculating the Kaiser window metrics are from the - * the textbook: - * - * Discrete-Time Signal Processing - * Alan V. Oppenheim and Ronald W. Schafer - * Prentice-Hall Signal Processing Series - * 1999 - */ - -#include "config.h" - -#define _UNICODE -#include <stdio.h> -#include <stdlib.h> -#include <stdarg.h> -#include <stddef.h> -#include <string.h> -#include <limits.h> -#include <ctype.h> -#include <math.h> -#ifdef HAVE_STRINGS_H -#include <strings.h> -#endif -#ifdef HAVE_GETOPT -#include <unistd.h> -#else -#include "getopt.h" -#endif - -#include "win_main_utf8.h" - -/* Define int64_t and uint64_t types */ -#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L -#include <inttypes.h> -#elif defined(_WIN32) && defined(__GNUC__) -#include <stdint.h> -#elif defined(_WIN32) -typedef __int64 int64_t; -typedef unsigned __int64 uint64_t; -#else -/* Fallback if nothing above works */ -#include <inttypes.h> -#endif - -#ifndef M_PI -#define M_PI (3.14159265358979323846) -#endif - -#ifndef HUGE_VAL -#define HUGE_VAL (1.0 / 0.0) -#endif - - -// The epsilon used to maintain signal stability. -#define EPSILON (1e-9) - -// Constants for accessing the token reader's ring buffer. -#define TR_RING_BITS (16) -#define TR_RING_SIZE (1 << TR_RING_BITS) -#define TR_RING_MASK (TR_RING_SIZE - 1) - -// The token reader's load interval in bytes. -#define TR_LOAD_SIZE (TR_RING_SIZE >> 2) - -// The maximum identifier length used when processing the data set -// definition. -#define MAX_IDENT_LEN (16) - -// The maximum path length used when processing filenames. -#define MAX_PATH_LEN (256) - -// The limits for the sample 'rate' metric in the data set definition and for -// resampling. -#define MIN_RATE (32000) -#define MAX_RATE (96000) - -// The limits for the HRIR 'points' metric in the data set definition. -#define MIN_POINTS (16) -#define MAX_POINTS (8192) - -// The limit to the number of 'distances' listed in the data set definition. -#define MAX_FD_COUNT (16) - -// The limits to the number of 'azimuths' listed in the data set definition. -#define MIN_EV_COUNT (5) -#define MAX_EV_COUNT (128) - -// The limits for each of the 'azimuths' listed in the data set definition. -#define MIN_AZ_COUNT (1) -#define MAX_AZ_COUNT (128) - -// The limits for the listener's head 'radius' in the data set definition. -#define MIN_RADIUS (0.05) -#define MAX_RADIUS (0.15) - -// The limits for the 'distance' from source to listener for each field in -// the definition file. -#define MIN_DISTANCE (0.05) -#define MAX_DISTANCE (2.50) - -// The maximum number of channels that can be addressed for a WAVE file -// source listed in the data set definition. -#define MAX_WAVE_CHANNELS (65535) - -// The limits to the byte size for a binary source listed in the definition -// file. -#define MIN_BIN_SIZE (2) -#define MAX_BIN_SIZE (4) - -// The minimum number of significant bits for binary sources listed in the -// data set definition. The maximum is calculated from the byte size. -#define MIN_BIN_BITS (16) - -// The limits to the number of significant bits for an ASCII source listed in -// the data set definition. -#define MIN_ASCII_BITS (16) -#define MAX_ASCII_BITS (32) - -// The limits to the FFT window size override on the command line. -#define MIN_FFTSIZE (65536) -#define MAX_FFTSIZE (131072) - -// The limits to the equalization range limit on the command line. -#define MIN_LIMIT (2.0) -#define MAX_LIMIT (120.0) - -// The limits to the truncation window size on the command line. -#define MIN_TRUNCSIZE (16) -#define MAX_TRUNCSIZE (512) - -// The limits to the custom head radius on the command line. -#define MIN_CUSTOM_RADIUS (0.05) -#define MAX_CUSTOM_RADIUS (0.15) - -// The truncation window size must be a multiple of the below value to allow -// for vectorized convolution. -#define MOD_TRUNCSIZE (8) - -// The defaults for the command line options. -#define DEFAULT_FFTSIZE (65536) -#define DEFAULT_EQUALIZE (1) -#define DEFAULT_SURFACE (1) -#define DEFAULT_LIMIT (24.0) -#define DEFAULT_TRUNCSIZE (32) -#define DEFAULT_HEAD_MODEL (HM_DATASET) -#define DEFAULT_CUSTOM_RADIUS (0.0) - -// The four-character-codes for RIFF/RIFX WAVE file chunks. -#define FOURCC_RIFF (0x46464952) // 'RIFF' -#define FOURCC_RIFX (0x58464952) // 'RIFX' -#define FOURCC_WAVE (0x45564157) // 'WAVE' -#define FOURCC_FMT (0x20746D66) // 'fmt ' -#define FOURCC_DATA (0x61746164) // 'data' -#define FOURCC_LIST (0x5453494C) // 'LIST' -#define FOURCC_WAVL (0x6C766177) // 'wavl' -#define FOURCC_SLNT (0x746E6C73) // 'slnt' - -// The supported wave formats. -#define WAVE_FORMAT_PCM (0x0001) -#define WAVE_FORMAT_IEEE_FLOAT (0x0003) -#define WAVE_FORMAT_EXTENSIBLE (0xFFFE) - -// The maximum propagation delay value supported by OpenAL Soft. -#define MAX_HRTD (63.0) - -// The OpenAL Soft HRTF format marker. It stands for minimum-phase head -// response protocol 02. -#define MHR_FORMAT ("MinPHR02") - -// Sample and channel type enum values. -typedef enum SampleTypeT { - ST_S16 = 0, - ST_S24 = 1 -} SampleTypeT; - -// Certain iterations rely on these integer enum values. -typedef enum ChannelTypeT { - CT_NONE = -1, - CT_MONO = 0, - CT_STEREO = 1 -} ChannelTypeT; - -// Byte order for the serialization routines. -typedef enum ByteOrderT { - BO_NONE, - BO_LITTLE, - BO_BIG -} ByteOrderT; - -// Source format for the references listed in the data set definition. -typedef enum SourceFormatT { - SF_NONE, - SF_WAVE, // RIFF/RIFX WAVE file. - SF_BIN_LE, // Little-endian binary file. - SF_BIN_BE, // Big-endian binary file. - SF_ASCII // ASCII text file. -} SourceFormatT; - -// Element types for the references listed in the data set definition. -typedef enum ElementTypeT { - ET_NONE, - ET_INT, // Integer elements. - ET_FP // Floating-point elements. -} ElementTypeT; - -// Head model used for calculating the impulse delays. -typedef enum HeadModelT { - HM_NONE, - HM_DATASET, // Measure the onset from the dataset. - HM_SPHERE // Calculate the onset using a spherical head model. -} HeadModelT; - -// Unsigned integer type. -typedef unsigned int uint; - -// Serialization types. The trailing digit indicates the number of bits. -typedef unsigned char uint8; -typedef int int32; -typedef unsigned int uint32; -typedef uint64_t uint64; - -// Token reader state for parsing the data set definition. -typedef struct TokenReaderT { - FILE *mFile; - const char *mName; - uint mLine; - uint mColumn; - char mRing[TR_RING_SIZE]; - size_t mIn; - size_t mOut; -} TokenReaderT; - -// Source reference state used when loading sources. -typedef struct SourceRefT { - SourceFormatT mFormat; - ElementTypeT mType; - uint mSize; - int mBits; - uint mChannel; - uint mSkip; - uint mOffset; - char mPath[MAX_PATH_LEN+1]; -} SourceRefT; - -// Structured HRIR storage for stereo azimuth pairs, elevations, and fields. -typedef struct HrirAzT { - double mAzimuth; - uint mIndex; - double mDelays[2]; - double *mIrs[2]; -} HrirAzT; - -typedef struct HrirEvT { - double mElevation; - uint mIrCount; - uint mAzCount; - HrirAzT *mAzs; -} HrirEvT; - -typedef struct HrirFdT { - double mDistance; - uint mIrCount; - uint mEvCount; - uint mEvStart; - HrirEvT *mEvs; -} HrirFdT; - -// The HRIR metrics and data set used when loading, processing, and storing -// the resulting HRTF. -typedef struct HrirDataT { - uint mIrRate; - SampleTypeT mSampleType; - ChannelTypeT mChannelType; - uint mIrPoints; - uint mFftSize; - uint mIrSize; - double mRadius; - uint mIrCount; - uint mFdCount; - HrirFdT *mFds; -} HrirDataT; - -// The resampler metrics and FIR filter. -typedef struct ResamplerT { - uint mP, mQ, mM, mL; - double *mF; -} ResamplerT; - - -/**************************************** - *** Complex number type and routines *** - ****************************************/ - -typedef struct { - double Real, Imag; -} Complex; - -static Complex MakeComplex(double r, double i) -{ - Complex c = { r, i }; - return c; -} - -static Complex c_add(Complex a, Complex b) -{ - Complex r; - r.Real = a.Real + b.Real; - r.Imag = a.Imag + b.Imag; - return r; -} - -static Complex c_sub(Complex a, Complex b) -{ - Complex r; - r.Real = a.Real - b.Real; - r.Imag = a.Imag - b.Imag; - return r; -} - -static Complex c_mul(Complex a, Complex b) -{ - Complex r; - r.Real = a.Real*b.Real - a.Imag*b.Imag; - r.Imag = a.Imag*b.Real + a.Real*b.Imag; - return r; -} - -static Complex c_muls(Complex a, double s) -{ - Complex r; - r.Real = a.Real * s; - r.Imag = a.Imag * s; - return r; -} - -static double c_abs(Complex a) -{ - return sqrt(a.Real*a.Real + a.Imag*a.Imag); -} - -static Complex c_exp(Complex a) -{ - Complex r; - double e = exp(a.Real); - r.Real = e * cos(a.Imag); - r.Imag = e * sin(a.Imag); - return r; -} - -/***************************** - *** Token reader routines *** - *****************************/ - -/* Whitespace is not significant. It can process tokens as identifiers, numbers - * (integer and floating-point), strings, and operators. Strings must be - * encapsulated by double-quotes and cannot span multiple lines. - */ - -// Setup the reader on the given file. The filename can be NULL if no error -// output is desired. -static void TrSetup(FILE *fp, const char *filename, TokenReaderT *tr) -{ - const char *name = NULL; - - if(filename) - { - const char *slash = strrchr(filename, '/'); - if(slash) - { - const char *bslash = strrchr(slash+1, '\\'); - if(bslash) name = bslash+1; - else name = slash+1; - } - else - { - const char *bslash = strrchr(filename, '\\'); - if(bslash) name = bslash+1; - else name = filename; - } - } - - tr->mFile = fp; - tr->mName = name; - tr->mLine = 1; - tr->mColumn = 1; - tr->mIn = 0; - tr->mOut = 0; -} - -// Prime the reader's ring buffer, and return a result indicating that there -// is text to process. -static int TrLoad(TokenReaderT *tr) -{ - size_t toLoad, in, count; - - toLoad = TR_RING_SIZE - (tr->mIn - tr->mOut); - if(toLoad >= TR_LOAD_SIZE && !feof(tr->mFile)) - { - // Load TR_LOAD_SIZE (or less if at the end of the file) per read. - toLoad = TR_LOAD_SIZE; - in = tr->mIn&TR_RING_MASK; - count = TR_RING_SIZE - in; - if(count < toLoad) - { - tr->mIn += fread(&tr->mRing[in], 1, count, tr->mFile); - tr->mIn += fread(&tr->mRing[0], 1, toLoad-count, tr->mFile); - } - else - tr->mIn += fread(&tr->mRing[in], 1, toLoad, tr->mFile); - - if(tr->mOut >= TR_RING_SIZE) - { - tr->mOut -= TR_RING_SIZE; - tr->mIn -= TR_RING_SIZE; - } - } - if(tr->mIn > tr->mOut) - return 1; - return 0; -} - -// Error display routine. Only displays when the base name is not NULL. -static void TrErrorVA(const TokenReaderT *tr, uint line, uint column, const char *format, va_list argPtr) -{ - if(!tr->mName) - return; - fprintf(stderr, "Error (%s:%u:%u): ", tr->mName, line, column); - vfprintf(stderr, format, argPtr); -} - -// Used to display an error at a saved line/column. -static void TrErrorAt(const TokenReaderT *tr, uint line, uint column, const char *format, ...) -{ - va_list argPtr; - - va_start(argPtr, format); - TrErrorVA(tr, line, column, format, argPtr); - va_end(argPtr); -} - -// Used to display an error at the current line/column. -static void TrError(const TokenReaderT *tr, const char *format, ...) -{ - va_list argPtr; - - va_start(argPtr, format); - TrErrorVA(tr, tr->mLine, tr->mColumn, format, argPtr); - va_end(argPtr); -} - -// Skips to the next line. -static void TrSkipLine(TokenReaderT *tr) -{ - char ch; - - while(TrLoad(tr)) - { - ch = tr->mRing[tr->mOut&TR_RING_MASK]; - tr->mOut++; - if(ch == '\n') - { - tr->mLine++; - tr->mColumn = 1; - break; - } - tr->mColumn ++; - } -} - -// Skips to the next token. -static int TrSkipWhitespace(TokenReaderT *tr) -{ - char ch; - - while(TrLoad(tr)) - { - ch = tr->mRing[tr->mOut&TR_RING_MASK]; - if(isspace(ch)) - { - tr->mOut++; - if(ch == '\n') - { - tr->mLine++; - tr->mColumn = 1; - } - else - tr->mColumn++; - } - else if(ch == '#') - TrSkipLine(tr); - else - return 1; - } - return 0; -} - -// Get the line and/or column of the next token (or the end of input). -static void TrIndication(TokenReaderT *tr, uint *line, uint *column) -{ - TrSkipWhitespace(tr); - if(line) *line = tr->mLine; - if(column) *column = tr->mColumn; -} - -// Checks to see if a token is (likely to be) an identifier. It does not -// display any errors and will not proceed to the next token. -static int TrIsIdent(TokenReaderT *tr) -{ - char ch; - - if(!TrSkipWhitespace(tr)) - return 0; - ch = tr->mRing[tr->mOut&TR_RING_MASK]; - return ch == '_' || isalpha(ch); -} - - -// Checks to see if a token is the given operator. It does not display any -// errors and will not proceed to the next token. -static int TrIsOperator(TokenReaderT *tr, const char *op) -{ - size_t out, len; - char ch; - - if(!TrSkipWhitespace(tr)) - return 0; - out = tr->mOut; - len = 0; - while(op[len] != '\0' && out < tr->mIn) - { - ch = tr->mRing[out&TR_RING_MASK]; - if(ch != op[len]) break; - len++; - out++; - } - if(op[len] == '\0') - return 1; - return 0; -} - -/* The TrRead*() routines obtain the value of a matching token type. They - * display type, form, and boundary errors and will proceed to the next - * token. - */ - -// Reads and validates an identifier token. -static int TrReadIdent(TokenReaderT *tr, const uint maxLen, char *ident) -{ - uint col, len; - char ch; - - col = tr->mColumn; - if(TrSkipWhitespace(tr)) - { - col = tr->mColumn; - ch = tr->mRing[tr->mOut&TR_RING_MASK]; - if(ch == '_' || isalpha(ch)) - { - len = 0; - do { - if(len < maxLen) - ident[len] = ch; - len++; - tr->mOut++; - if(!TrLoad(tr)) - break; - ch = tr->mRing[tr->mOut&TR_RING_MASK]; - } while(ch == '_' || isdigit(ch) || isalpha(ch)); - - tr->mColumn += len; - if(len < maxLen) - { - ident[len] = '\0'; - return 1; - } - TrErrorAt(tr, tr->mLine, col, "Identifier is too long.\n"); - return 0; - } - } - TrErrorAt(tr, tr->mLine, col, "Expected an identifier.\n"); - return 0; -} - -// Reads and validates (including bounds) an integer token. -static int TrReadInt(TokenReaderT *tr, const int loBound, const int hiBound, int *value) -{ - uint col, digis, len; - char ch, temp[64+1]; - - col = tr->mColumn; - if(TrSkipWhitespace(tr)) - { - col = tr->mColumn; - len = 0; - ch = tr->mRing[tr->mOut&TR_RING_MASK]; - if(ch == '+' || ch == '-') - { - temp[len] = ch; - len++; - tr->mOut++; - } - digis = 0; - while(TrLoad(tr)) - { - ch = tr->mRing[tr->mOut&TR_RING_MASK]; - if(!isdigit(ch)) break; - if(len < 64) - temp[len] = ch; - len++; - digis++; - tr->mOut++; - } - tr->mColumn += len; - if(digis > 0 && ch != '.' && !isalpha(ch)) - { - if(len > 64) - { - TrErrorAt(tr, tr->mLine, col, "Integer is too long."); - return 0; - } - temp[len] = '\0'; - *value = strtol(temp, NULL, 10); - if(*value < loBound || *value > hiBound) - { - TrErrorAt(tr, tr->mLine, col, "Expected a value from %d to %d.\n", loBound, hiBound); - return 0; - } - return 1; - } - } - TrErrorAt(tr, tr->mLine, col, "Expected an integer.\n"); - return 0; -} - -// Reads and validates (including bounds) a float token. -static int TrReadFloat(TokenReaderT *tr, const double loBound, const double hiBound, double *value) -{ - uint col, digis, len; - char ch, temp[64+1]; - - col = tr->mColumn; - if(TrSkipWhitespace(tr)) - { - col = tr->mColumn; - len = 0; - ch = tr->mRing[tr->mOut&TR_RING_MASK]; - if(ch == '+' || ch == '-') - { - temp[len] = ch; - len++; - tr->mOut++; - } - - digis = 0; - while(TrLoad(tr)) - { - ch = tr->mRing[tr->mOut&TR_RING_MASK]; - if(!isdigit(ch)) break; - if(len < 64) - temp[len] = ch; - len++; - digis++; - tr->mOut++; - } - if(ch == '.') - { - if(len < 64) - temp[len] = ch; - len++; - tr->mOut++; - } - while(TrLoad(tr)) - { - ch = tr->mRing[tr->mOut&TR_RING_MASK]; - if(!isdigit(ch)) break; - if(len < 64) - temp[len] = ch; - len++; - digis++; - tr->mOut++; - } - if(digis > 0) - { - if(ch == 'E' || ch == 'e') - { - if(len < 64) - temp[len] = ch; - len++; - digis = 0; - tr->mOut++; - if(ch == '+' || ch == '-') - { - if(len < 64) - temp[len] = ch; - len++; - tr->mOut++; - } - while(TrLoad(tr)) - { - ch = tr->mRing[tr->mOut&TR_RING_MASK]; - if(!isdigit(ch)) break; - if(len < 64) - temp[len] = ch; - len++; - digis++; - tr->mOut++; - } - } - tr->mColumn += len; - if(digis > 0 && ch != '.' && !isalpha(ch)) - { - if(len > 64) - { - TrErrorAt(tr, tr->mLine, col, "Float is too long."); - return 0; - } - temp[len] = '\0'; - *value = strtod(temp, NULL); - if(*value < loBound || *value > hiBound) - { - TrErrorAt(tr, tr->mLine, col, "Expected a value from %f to %f.\n", loBound, hiBound); - return 0; - } - return 1; - } - } - else - tr->mColumn += len; - } - TrErrorAt(tr, tr->mLine, col, "Expected a float.\n"); - return 0; -} - -// Reads and validates a string token. -static int TrReadString(TokenReaderT *tr, const uint maxLen, char *text) -{ - uint col, len; - char ch; - - col = tr->mColumn; - if(TrSkipWhitespace(tr)) - { - col = tr->mColumn; - ch = tr->mRing[tr->mOut&TR_RING_MASK]; - if(ch == '\"') - { - tr->mOut++; - len = 0; - while(TrLoad(tr)) - { - ch = tr->mRing[tr->mOut&TR_RING_MASK]; - tr->mOut++; - if(ch == '\"') - break; - if(ch == '\n') - { - TrErrorAt(tr, tr->mLine, col, "Unterminated string at end of line.\n"); - return 0; - } - if(len < maxLen) - text[len] = ch; - len++; - } - if(ch != '\"') - { - tr->mColumn += 1 + len; - TrErrorAt(tr, tr->mLine, col, "Unterminated string at end of input.\n"); - return 0; - } - tr->mColumn += 2 + len; - if(len > maxLen) - { - TrErrorAt(tr, tr->mLine, col, "String is too long.\n"); - return 0; - } - text[len] = '\0'; - return 1; - } - } - TrErrorAt(tr, tr->mLine, col, "Expected a string.\n"); - return 0; -} - -// Reads and validates the given operator. -static int TrReadOperator(TokenReaderT *tr, const char *op) -{ - uint col, len; - char ch; - - col = tr->mColumn; - if(TrSkipWhitespace(tr)) - { - col = tr->mColumn; - len = 0; - while(op[len] != '\0' && TrLoad(tr)) - { - ch = tr->mRing[tr->mOut&TR_RING_MASK]; - if(ch != op[len]) break; - len++; - tr->mOut++; - } - tr->mColumn += len; - if(op[len] == '\0') - return 1; - } - TrErrorAt(tr, tr->mLine, col, "Expected '%s' operator.\n", op); - return 0; -} - -/* Performs a string substitution. Any case-insensitive occurrences of the - * pattern string are replaced with the replacement string. The result is - * truncated if necessary. - */ -static int StrSubst(const char *in, const char *pat, const char *rep, const size_t maxLen, char *out) -{ - size_t inLen, patLen, repLen; - size_t si, di; - int truncated; - - inLen = strlen(in); - patLen = strlen(pat); - repLen = strlen(rep); - si = 0; - di = 0; - truncated = 0; - while(si < inLen && di < maxLen) - { - if(patLen <= inLen-si) - { - if(strncasecmp(&in[si], pat, patLen) == 0) - { - if(repLen > maxLen-di) - { - repLen = maxLen - di; - truncated = 1; - } - strncpy(&out[di], rep, repLen); - si += patLen; - di += repLen; - } - } - out[di] = in[si]; - si++; - di++; - } - if(si < inLen) - truncated = 1; - out[di] = '\0'; - return !truncated; -} - - -/********************* - *** Math routines *** - *********************/ - -// Provide missing math routines for MSVC versions < 1800 (Visual Studio 2013). -#if defined(_MSC_VER) && _MSC_VER < 1800 -static double round(double val) -{ - if(val < 0.0) - return ceil(val-0.5); - return floor(val+0.5); -} - -static double fmin(double a, double b) -{ - return (a<b) ? a : b; -} - -static double fmax(double a, double b) -{ - return (a>b) ? a : b; -} -#endif - -// Simple clamp routine. -static double Clamp(const double val, const double lower, const double upper) -{ - return fmin(fmax(val, lower), upper); -} - -// Performs linear interpolation. -static double Lerp(const double a, const double b, const double f) -{ - return a + f * (b - a); -} - -static inline uint dither_rng(uint *seed) -{ - *seed = *seed * 96314165 + 907633515; - return *seed; -} - -// Performs a triangular probability density function dither. The input samples -// should be normalized (-1 to +1). -static void TpdfDither(double *restrict out, const double *restrict in, const double scale, - const int count, const int step, uint *seed) -{ - static const double PRNG_SCALE = 1.0 / UINT_MAX; - uint prn0, prn1; - int i; - - for(i = 0;i < count;i++) - { - prn0 = dither_rng(seed); - prn1 = dither_rng(seed); - out[i*step] = round(in[i]*scale + (prn0*PRNG_SCALE - prn1*PRNG_SCALE)); - } -} - -// Allocates an array of doubles. -static double *CreateDoubles(size_t n) -{ - double *a; - - a = calloc(n?n:1, sizeof(*a)); - if(a == NULL) - { - fprintf(stderr, "Error: Out of memory.\n"); - exit(-1); - } - return a; -} - -// Allocates an array of complex numbers. -static Complex *CreateComplexes(size_t n) -{ - Complex *a; - - a = calloc(n?n:1, sizeof(*a)); - if(a == NULL) - { - fprintf(stderr, "Error: Out of memory.\n"); - exit(-1); - } - return a; -} - -/* Fast Fourier transform routines. The number of points must be a power of - * two. - */ - -// Performs bit-reversal ordering. -static void FftArrange(const uint n, Complex *inout) -{ - uint rk, k, m; - - // Handle in-place arrangement. - rk = 0; - for(k = 0;k < n;k++) - { - if(rk > k) - { - Complex temp = inout[rk]; - inout[rk] = inout[k]; - inout[k] = temp; - } - - m = n; - while(rk&(m >>= 1)) - rk &= ~m; - rk |= m; - } -} - -// Performs the summation. -static void FftSummation(const int n, const double s, Complex *cplx) -{ - double pi; - int m, m2; - int i, k, mk; - - pi = s * M_PI; - for(m = 1, m2 = 2;m < n; m <<= 1, m2 <<= 1) - { - // v = Complex (-2.0 * sin (0.5 * pi / m) * sin (0.5 * pi / m), -sin (pi / m)) - double sm = sin(0.5 * pi / m); - Complex v = MakeComplex(-2.0*sm*sm, -sin(pi / m)); - Complex w = MakeComplex(1.0, 0.0); - for(i = 0;i < m;i++) - { - for(k = i;k < n;k += m2) - { - Complex t; - mk = k + m; - t = c_mul(w, cplx[mk]); - cplx[mk] = c_sub(cplx[k], t); - cplx[k] = c_add(cplx[k], t); - } - w = c_add(w, c_mul(v, w)); - } - } -} - -// Performs a forward FFT. -static void FftForward(const uint n, Complex *inout) -{ - FftArrange(n, inout); - FftSummation(n, 1.0, inout); -} - -// Performs an inverse FFT. -static void FftInverse(const uint n, Complex *inout) -{ - double f; - uint i; - - FftArrange(n, inout); - FftSummation(n, -1.0, inout); - f = 1.0 / n; - for(i = 0;i < n;i++) - inout[i] = c_muls(inout[i], f); -} - -/* Calculate the complex helical sequence (or discrete-time analytical signal) - * of the given input using the Hilbert transform. Given the natural logarithm - * of a signal's magnitude response, the imaginary components can be used as - * the angles for minimum-phase reconstruction. - */ -static void Hilbert(const uint n, Complex *inout) -{ - uint i; - - // Handle in-place operation. - for(i = 0;i < n;i++) - inout[i].Imag = 0.0; - - FftInverse(n, inout); - for(i = 1;i < (n+1)/2;i++) - inout[i] = c_muls(inout[i], 2.0); - /* Increment i if n is even. */ - i += (n&1)^1; - for(;i < n;i++) - inout[i] = MakeComplex(0.0, 0.0); - FftForward(n, inout); -} - -/* Calculate the magnitude response of the given input. This is used in - * place of phase decomposition, since the phase residuals are discarded for - * minimum phase reconstruction. The mirrored half of the response is also - * discarded. - */ -static void MagnitudeResponse(const uint n, const Complex *in, double *out) -{ - const uint m = 1 + (n / 2); - uint i; - for(i = 0;i < m;i++) - out[i] = fmax(c_abs(in[i]), EPSILON); -} - -/* Apply a range limit (in dB) to the given magnitude response. This is used - * to adjust the effects of the diffuse-field average on the equalization - * process. - */ -static void LimitMagnitudeResponse(const uint n, const uint m, const double limit, const double *in, double *out) -{ - double halfLim; - uint i, lower, upper; - double ave; - - halfLim = limit / 2.0; - // Convert the response to dB. - for(i = 0;i < m;i++) - out[i] = 20.0 * log10(in[i]); - // Use six octaves to calculate the average magnitude of the signal. - lower = ((uint)ceil(n / pow(2.0, 8.0))) - 1; - upper = ((uint)floor(n / pow(2.0, 2.0))) - 1; - ave = 0.0; - for(i = lower;i <= upper;i++) - ave += out[i]; - ave /= upper - lower + 1; - // Keep the response within range of the average magnitude. - for(i = 0;i < m;i++) - out[i] = Clamp(out[i], ave - halfLim, ave + halfLim); - // Convert the response back to linear magnitude. - for(i = 0;i < m;i++) - out[i] = pow(10.0, out[i] / 20.0); -} - -/* Reconstructs the minimum-phase component for the given magnitude response - * of a signal. This is equivalent to phase recomposition, sans the missing - * residuals (which were discarded). The mirrored half of the response is - * reconstructed. - */ -static void MinimumPhase(const uint n, const double *in, Complex *out) -{ - const uint m = 1 + (n / 2); - double *mags; - uint i; - - mags = CreateDoubles(n); - for(i = 0;i < m;i++) - { - mags[i] = fmax(EPSILON, in[i]); - out[i] = MakeComplex(log(mags[i]), 0.0); - } - for(;i < n;i++) - { - mags[i] = mags[n - i]; - out[i] = out[n - i]; - } - Hilbert(n, out); - // Remove any DC offset the filter has. - mags[0] = EPSILON; - for(i = 0;i < n;i++) - { - Complex a = c_exp(MakeComplex(0.0, out[i].Imag)); - out[i] = c_mul(MakeComplex(mags[i], 0.0), a); - } - free(mags); -} - - -/*************************** - *** Resampler functions *** - ***************************/ - -/* This is the normalized cardinal sine (sinc) function. - * - * sinc(x) = { 1, x = 0 - * { sin(pi x) / (pi x), otherwise. - */ -static double Sinc(const double x) -{ - if(fabs(x) < EPSILON) - return 1.0; - return sin(M_PI * x) / (M_PI * x); -} - -/* The zero-order modified Bessel function of the first kind, used for the - * Kaiser window. - * - * I_0(x) = sum_{k=0}^inf (1 / k!)^2 (x / 2)^(2 k) - * = sum_{k=0}^inf ((x / 2)^k / k!)^2 - */ -static double BesselI_0(const double x) -{ - double term, sum, x2, y, last_sum; - int k; - - // Start at k=1 since k=0 is trivial. - term = 1.0; - sum = 1.0; - x2 = x/2.0; - k = 1; - - // Let the integration converge until the term of the sum is no longer - // significant. - do { - y = x2 / k; - k++; - last_sum = sum; - term *= y * y; - sum += term; - } while(sum != last_sum); - return sum; -} - -/* Calculate a Kaiser window from the given beta value and a normalized k - * [-1, 1]. - * - * w(k) = { I_0(B sqrt(1 - k^2)) / I_0(B), -1 <= k <= 1 - * { 0, elsewhere. - * - * Where k can be calculated as: - * - * k = i / l, where -l <= i <= l. - * - * or: - * - * k = 2 i / M - 1, where 0 <= i <= M. - */ -static double Kaiser(const double b, const double k) -{ - if(!(k >= -1.0 && k <= 1.0)) - return 0.0; - return BesselI_0(b * sqrt(1.0 - k*k)) / BesselI_0(b); -} - -// Calculates the greatest common divisor of a and b. -static uint Gcd(uint x, uint y) -{ - while(y > 0) - { - uint z = y; - y = x % y; - x = z; - } - return x; -} - -/* Calculates the size (order) of the Kaiser window. Rejection is in dB and - * the transition width is normalized frequency (0.5 is nyquist). - * - * M = { ceil((r - 7.95) / (2.285 2 pi f_t)), r > 21 - * { ceil(5.79 / 2 pi f_t), r <= 21. - * - */ -static uint CalcKaiserOrder(const double rejection, const double transition) -{ - double w_t = 2.0 * M_PI * transition; - if(rejection > 21.0) - return (uint)ceil((rejection - 7.95) / (2.285 * w_t)); - return (uint)ceil(5.79 / w_t); -} - -// Calculates the beta value of the Kaiser window. Rejection is in dB. -static double CalcKaiserBeta(const double rejection) -{ - if(rejection > 50.0) - return 0.1102 * (rejection - 8.7); - if(rejection >= 21.0) - return (0.5842 * pow(rejection - 21.0, 0.4)) + - (0.07886 * (rejection - 21.0)); - return 0.0; -} - -/* Calculates a point on the Kaiser-windowed sinc filter for the given half- - * width, beta, gain, and cutoff. The point is specified in non-normalized - * samples, from 0 to M, where M = (2 l + 1). - * - * w(k) 2 p f_t sinc(2 f_t x) - * - * x -- centered sample index (i - l) - * k -- normalized and centered window index (x / l) - * w(k) -- window function (Kaiser) - * p -- gain compensation factor when sampling - * f_t -- normalized center frequency (or cutoff; 0.5 is nyquist) - */ -static double SincFilter(const int l, const double b, const double gain, const double cutoff, const int i) -{ - return Kaiser(b, (double)(i - l) / l) * 2.0 * gain * cutoff * Sinc(2.0 * cutoff * (i - l)); -} - -/* This is a polyphase sinc-filtered resampler. - * - * Upsample Downsample - * - * p/q = 3/2 p/q = 3/5 - * - * M-+-+-+-> M-+-+-+-> - * -------------------+ ---------------------+ - * p s * f f f f|f| | p s * f f f f f | - * | 0 * 0 0 0|0|0 | | 0 * 0 0 0 0|0| | - * v 0 * 0 0|0|0 0 | v 0 * 0 0 0|0|0 | - * s * f|f|f f f | s * f f|f|f f | - * 0 * |0|0 0 0 0 | 0 * 0|0|0 0 0 | - * --------+=+--------+ 0 * |0|0 0 0 0 | - * d . d .|d|. d . d ----------+=+--------+ - * d . . . .|d|. . . . - * q-> - * q-+-+-+-> - * - * P_f(i,j) = q i mod p + pj - * P_s(i,j) = floor(q i / p) - j - * d[i=0..N-1] = sum_{j=0}^{floor((M - 1) / p)} { - * { f[P_f(i,j)] s[P_s(i,j)], P_f(i,j) < M - * { 0, P_f(i,j) >= M. } - */ - -// Calculate the resampling metrics and build the Kaiser-windowed sinc filter -// that's used to cut frequencies above the destination nyquist. -static void ResamplerSetup(ResamplerT *rs, const uint srcRate, const uint dstRate) -{ - double cutoff, width, beta; - uint gcd, l; - int i; - - gcd = Gcd(srcRate, dstRate); - rs->mP = dstRate / gcd; - rs->mQ = srcRate / gcd; - /* The cutoff is adjusted by half the transition width, so the transition - * ends before the nyquist (0.5). Both are scaled by the downsampling - * factor. - */ - if(rs->mP > rs->mQ) - { - cutoff = 0.475 / rs->mP; - width = 0.05 / rs->mP; - } - else - { - cutoff = 0.475 / rs->mQ; - width = 0.05 / rs->mQ; - } - // A rejection of -180 dB is used for the stop band. Round up when - // calculating the left offset to avoid increasing the transition width. - l = (CalcKaiserOrder(180.0, width)+1) / 2; - beta = CalcKaiserBeta(180.0); - rs->mM = l*2 + 1; - rs->mL = l; - rs->mF = CreateDoubles(rs->mM); - for(i = 0;i < ((int)rs->mM);i++) - rs->mF[i] = SincFilter((int)l, beta, rs->mP, cutoff, i); -} - -// Clean up after the resampler. -static void ResamplerClear(ResamplerT *rs) -{ - free(rs->mF); - rs->mF = NULL; -} - -// Perform the upsample-filter-downsample resampling operation using a -// polyphase filter implementation. -static void ResamplerRun(ResamplerT *rs, const uint inN, const double *in, const uint outN, double *out) -{ - const uint p = rs->mP, q = rs->mQ, m = rs->mM, l = rs->mL; - const double *f = rs->mF; - uint j_f, j_s; - double *work; - uint i; - - if(outN == 0) - return; - - // Handle in-place operation. - if(in == out) - work = CreateDoubles(outN); - else - work = out; - // Resample the input. - for(i = 0;i < outN;i++) - { - double r = 0.0; - // Input starts at l to compensate for the filter delay. This will - // drop any build-up from the first half of the filter. - j_f = (l + (q * i)) % p; - j_s = (l + (q * i)) / p; - while(j_f < m) - { - // Only take input when 0 <= j_s < inN. This single unsigned - // comparison catches both cases. - if(j_s < inN) - r += f[j_f] * in[j_s]; - j_f += p; - j_s--; - } - work[i] = r; - } - // Clean up after in-place operation. - if(work != out) - { - for(i = 0;i < outN;i++) - out[i] = work[i]; - free(work); - } -} - -/************************* - *** File source input *** - *************************/ - -// Read a binary value of the specified byte order and byte size from a file, -// storing it as a 32-bit unsigned integer. -static int ReadBin4(FILE *fp, const char *filename, const ByteOrderT order, const uint bytes, uint32 *out) -{ - uint8 in[4]; - uint32 accum; - uint i; - - if(fread(in, 1, bytes, fp) != bytes) - { - fprintf(stderr, "Error: Bad read from file '%s'.\n", filename); - return 0; - } - accum = 0; - switch(order) - { - case BO_LITTLE: - for(i = 0;i < bytes;i++) - accum = (accum<<8) | in[bytes - i - 1]; - break; - case BO_BIG: - for(i = 0;i < bytes;i++) - accum = (accum<<8) | in[i]; - break; - default: - break; - } - *out = accum; - return 1; -} - -// Read a binary value of the specified byte order from a file, storing it as -// a 64-bit unsigned integer. -static int ReadBin8(FILE *fp, const char *filename, const ByteOrderT order, uint64 *out) -{ - uint8 in [8]; - uint64 accum; - uint i; - - if(fread(in, 1, 8, fp) != 8) - { - fprintf(stderr, "Error: Bad read from file '%s'.\n", filename); - return 0; - } - accum = 0ULL; - switch(order) - { - case BO_LITTLE: - for(i = 0;i < 8;i++) - accum = (accum<<8) | in[8 - i - 1]; - break; - case BO_BIG: - for(i = 0;i < 8;i++) - accum = (accum<<8) | in[i]; - break; - default: - break; - } - *out = accum; - return 1; -} - -/* Read a binary value of the specified type, byte order, and byte size from - * a file, converting it to a double. For integer types, the significant - * bits are used to normalize the result. The sign of bits determines - * whether they are padded toward the MSB (negative) or LSB (positive). - * Floating-point types are not normalized. - */ -static int ReadBinAsDouble(FILE *fp, const char *filename, const ByteOrderT order, const ElementTypeT type, const uint bytes, const int bits, double *out) -{ - union { - uint32 ui; - int32 i; - float f; - } v4; - union { - uint64 ui; - double f; - } v8; - - *out = 0.0; - if(bytes > 4) - { - if(!ReadBin8(fp, filename, order, &v8.ui)) - return 0; - if(type == ET_FP) - *out = v8.f; - } - else - { - if(!ReadBin4(fp, filename, order, bytes, &v4.ui)) - return 0; - if(type == ET_FP) - *out = v4.f; - else - { - if(bits > 0) - v4.ui >>= (8*bytes) - ((uint)bits); - else - v4.ui &= (0xFFFFFFFF >> (32+bits)); - - if(v4.ui&(uint)(1<<(abs(bits)-1))) - v4.ui |= (0xFFFFFFFF << abs (bits)); - *out = v4.i / (double)(1<<(abs(bits)-1)); - } - } - return 1; -} - -/* Read an ascii value of the specified type from a file, converting it to a - * double. For integer types, the significant bits are used to normalize the - * result. The sign of the bits should always be positive. This also skips - * up to one separator character before the element itself. - */ -static int ReadAsciiAsDouble(TokenReaderT *tr, const char *filename, const ElementTypeT type, const uint bits, double *out) -{ - if(TrIsOperator(tr, ",")) - TrReadOperator(tr, ","); - else if(TrIsOperator(tr, ":")) - TrReadOperator(tr, ":"); - else if(TrIsOperator(tr, ";")) - TrReadOperator(tr, ";"); - else if(TrIsOperator(tr, "|")) - TrReadOperator(tr, "|"); - - if(type == ET_FP) - { - if(!TrReadFloat(tr, -HUGE_VAL, HUGE_VAL, out)) - { - fprintf(stderr, "Error: Bad read from file '%s'.\n", filename); - return 0; - } - } - else - { - int v; - if(!TrReadInt(tr, -(1<<(bits-1)), (1<<(bits-1))-1, &v)) - { - fprintf(stderr, "Error: Bad read from file '%s'.\n", filename); - return 0; - } - *out = v / (double)((1<<(bits-1))-1); - } - return 1; -} - -// Read the RIFF/RIFX WAVE format chunk from a file, validating it against -// the source parameters and data set metrics. -static int ReadWaveFormat(FILE *fp, const ByteOrderT order, const uint hrirRate, SourceRefT *src) -{ - uint32 fourCC, chunkSize; - uint32 format, channels, rate, dummy, block, size, bits; - - chunkSize = 0; - do { - if(chunkSize > 0) - fseek (fp, (long) chunkSize, SEEK_CUR); - if(!ReadBin4(fp, src->mPath, BO_LITTLE, 4, &fourCC) || - !ReadBin4(fp, src->mPath, order, 4, &chunkSize)) - return 0; - } while(fourCC != FOURCC_FMT); - if(!ReadBin4(fp, src->mPath, order, 2, &format) || - !ReadBin4(fp, src->mPath, order, 2, &channels) || - !ReadBin4(fp, src->mPath, order, 4, &rate) || - !ReadBin4(fp, src->mPath, order, 4, &dummy) || - !ReadBin4(fp, src->mPath, order, 2, &block)) - return 0; - block /= channels; - if(chunkSize > 14) - { - if(!ReadBin4(fp, src->mPath, order, 2, &size)) - return 0; - size /= 8; - if(block > size) - size = block; - } - else - size = block; - if(format == WAVE_FORMAT_EXTENSIBLE) - { - fseek(fp, 2, SEEK_CUR); - if(!ReadBin4(fp, src->mPath, order, 2, &bits)) - return 0; - if(bits == 0) - bits = 8 * size; - fseek(fp, 4, SEEK_CUR); - if(!ReadBin4(fp, src->mPath, order, 2, &format)) - return 0; - fseek(fp, (long)(chunkSize - 26), SEEK_CUR); - } - else - { - bits = 8 * size; - if(chunkSize > 14) - fseek(fp, (long)(chunkSize - 16), SEEK_CUR); - else - fseek(fp, (long)(chunkSize - 14), SEEK_CUR); - } - if(format != WAVE_FORMAT_PCM && format != WAVE_FORMAT_IEEE_FLOAT) - { - fprintf(stderr, "Error: Unsupported WAVE format in file '%s'.\n", src->mPath); - return 0; - } - if(src->mChannel >= channels) - { - fprintf(stderr, "Error: Missing source channel in WAVE file '%s'.\n", src->mPath); - return 0; - } - if(rate != hrirRate) - { - fprintf(stderr, "Error: Mismatched source sample rate in WAVE file '%s'.\n", src->mPath); - return 0; - } - if(format == WAVE_FORMAT_PCM) - { - if(size < 2 || size > 4) - { - fprintf(stderr, "Error: Unsupported sample size in WAVE file '%s'.\n", src->mPath); - return 0; - } - if(bits < 16 || bits > (8*size)) - { - fprintf (stderr, "Error: Bad significant bits in WAVE file '%s'.\n", src->mPath); - return 0; - } - src->mType = ET_INT; - } - else - { - if(size != 4 && size != 8) - { - fprintf(stderr, "Error: Unsupported sample size in WAVE file '%s'.\n", src->mPath); - return 0; - } - src->mType = ET_FP; - } - src->mSize = size; - src->mBits = (int)bits; - src->mSkip = channels; - return 1; -} - -// Read a RIFF/RIFX WAVE data chunk, converting all elements to doubles. -static int ReadWaveData(FILE *fp, const SourceRefT *src, const ByteOrderT order, const uint n, double *hrir) -{ - int pre, post, skip; - uint i; - - pre = (int)(src->mSize * src->mChannel); - post = (int)(src->mSize * (src->mSkip - src->mChannel - 1)); - skip = 0; - for(i = 0;i < n;i++) - { - skip += pre; - if(skip > 0) - fseek(fp, skip, SEEK_CUR); - if(!ReadBinAsDouble(fp, src->mPath, order, src->mType, src->mSize, src->mBits, &hrir[i])) - return 0; - skip = post; - } - if(skip > 0) - fseek(fp, skip, SEEK_CUR); - return 1; -} - -// Read the RIFF/RIFX WAVE list or data chunk, converting all elements to -// doubles. -static int ReadWaveList(FILE *fp, const SourceRefT *src, const ByteOrderT order, const uint n, double *hrir) -{ - uint32 fourCC, chunkSize, listSize, count; - uint block, skip, offset, i; - double lastSample; - - for(;;) - { - if(!ReadBin4(fp, src->mPath, BO_LITTLE, 4, &fourCC) || - !ReadBin4(fp, src->mPath, order, 4, &chunkSize)) - return 0; - - if(fourCC == FOURCC_DATA) - { - block = src->mSize * src->mSkip; - count = chunkSize / block; - if(count < (src->mOffset + n)) - { - fprintf(stderr, "Error: Bad read from file '%s'.\n", src->mPath); - return 0; - } - fseek(fp, (long)(src->mOffset * block), SEEK_CUR); - if(!ReadWaveData(fp, src, order, n, &hrir[0])) - return 0; - return 1; - } - else if(fourCC == FOURCC_LIST) - { - if(!ReadBin4(fp, src->mPath, BO_LITTLE, 4, &fourCC)) - return 0; - chunkSize -= 4; - if(fourCC == FOURCC_WAVL) - break; - } - if(chunkSize > 0) - fseek(fp, (long)chunkSize, SEEK_CUR); - } - listSize = chunkSize; - block = src->mSize * src->mSkip; - skip = src->mOffset; - offset = 0; - lastSample = 0.0; - while(offset < n && listSize > 8) - { - if(!ReadBin4(fp, src->mPath, BO_LITTLE, 4, &fourCC) || - !ReadBin4(fp, src->mPath, order, 4, &chunkSize)) - return 0; - listSize -= 8 + chunkSize; - if(fourCC == FOURCC_DATA) - { - count = chunkSize / block; - if(count > skip) - { - fseek(fp, (long)(skip * block), SEEK_CUR); - chunkSize -= skip * block; - count -= skip; - skip = 0; - if(count > (n - offset)) - count = n - offset; - if(!ReadWaveData(fp, src, order, count, &hrir[offset])) - return 0; - chunkSize -= count * block; - offset += count; - lastSample = hrir [offset - 1]; - } - else - { - skip -= count; - count = 0; - } - } - else if(fourCC == FOURCC_SLNT) - { - if(!ReadBin4(fp, src->mPath, order, 4, &count)) - return 0; - chunkSize -= 4; - if(count > skip) - { - count -= skip; - skip = 0; - if(count > (n - offset)) - count = n - offset; - for(i = 0; i < count; i ++) - hrir[offset + i] = lastSample; - offset += count; - } - else - { - skip -= count; - count = 0; - } - } - if(chunkSize > 0) - fseek(fp, (long)chunkSize, SEEK_CUR); - } - if(offset < n) - { - fprintf(stderr, "Error: Bad read from file '%s'.\n", src->mPath); - return 0; - } - return 1; -} - -// Load a source HRIR from a RIFF/RIFX WAVE file. -static int LoadWaveSource(FILE *fp, SourceRefT *src, const uint hrirRate, const uint n, double *hrir) -{ - uint32 fourCC, dummy; - ByteOrderT order; - - if(!ReadBin4(fp, src->mPath, BO_LITTLE, 4, &fourCC) || - !ReadBin4(fp, src->mPath, BO_LITTLE, 4, &dummy)) - return 0; - if(fourCC == FOURCC_RIFF) - order = BO_LITTLE; - else if(fourCC == FOURCC_RIFX) - order = BO_BIG; - else - { - fprintf(stderr, "Error: No RIFF/RIFX chunk in file '%s'.\n", src->mPath); - return 0; - } - - if(!ReadBin4(fp, src->mPath, BO_LITTLE, 4, &fourCC)) - return 0; - if(fourCC != FOURCC_WAVE) - { - fprintf(stderr, "Error: Not a RIFF/RIFX WAVE file '%s'.\n", src->mPath); - return 0; - } - if(!ReadWaveFormat(fp, order, hrirRate, src)) - return 0; - if(!ReadWaveList(fp, src, order, n, hrir)) - return 0; - return 1; -} - -// Load a source HRIR from a binary file. -static int LoadBinarySource(FILE *fp, const SourceRefT *src, const ByteOrderT order, const uint n, double *hrir) -{ - uint i; - - fseek(fp, (long)src->mOffset, SEEK_SET); - for(i = 0;i < n;i++) - { - if(!ReadBinAsDouble(fp, src->mPath, order, src->mType, src->mSize, src->mBits, &hrir[i])) - return 0; - if(src->mSkip > 0) - fseek(fp, (long)src->mSkip, SEEK_CUR); - } - return 1; -} - -// Load a source HRIR from an ASCII text file containing a list of elements -// separated by whitespace or common list operators (',', ';', ':', '|'). -static int LoadAsciiSource(FILE *fp, const SourceRefT *src, const uint n, double *hrir) -{ - TokenReaderT tr; - uint i, j; - double dummy; - - TrSetup(fp, NULL, &tr); - for(i = 0;i < src->mOffset;i++) - { - if(!ReadAsciiAsDouble(&tr, src->mPath, src->mType, (uint)src->mBits, &dummy)) - return 0; - } - for(i = 0;i < n;i++) - { - if(!ReadAsciiAsDouble(&tr, src->mPath, src->mType, (uint)src->mBits, &hrir[i])) - return 0; - for(j = 0;j < src->mSkip;j++) - { - if(!ReadAsciiAsDouble(&tr, src->mPath, src->mType, (uint)src->mBits, &dummy)) - return 0; - } - } - return 1; -} - -// Load a source HRIR from a supported file type. -static int LoadSource(SourceRefT *src, const uint hrirRate, const uint n, double *hrir) -{ - int result; - FILE *fp; - - if(src->mFormat == SF_ASCII) - fp = fopen(src->mPath, "r"); - else - fp = fopen(src->mPath, "rb"); - if(fp == NULL) - { - fprintf(stderr, "Error: Could not open source file '%s'.\n", src->mPath); - return 0; - } - if(src->mFormat == SF_WAVE) - result = LoadWaveSource(fp, src, hrirRate, n, hrir); - else if(src->mFormat == SF_BIN_LE) - result = LoadBinarySource(fp, src, BO_LITTLE, n, hrir); - else if(src->mFormat == SF_BIN_BE) - result = LoadBinarySource(fp, src, BO_BIG, n, hrir); - else - result = LoadAsciiSource(fp, src, n, hrir); - fclose(fp); - return result; -} - - -/*************************** - *** File storage output *** - ***************************/ - -// Write an ASCII string to a file. -static int WriteAscii(const char *out, FILE *fp, const char *filename) -{ - size_t len; - - len = strlen(out); - if(fwrite(out, 1, len, fp) != len) - { - fclose(fp); - fprintf(stderr, "Error: Bad write to file '%s'.\n", filename); - return 0; - } - return 1; -} - -// Write a binary value of the given byte order and byte size to a file, -// loading it from a 32-bit unsigned integer. -static int WriteBin4(const ByteOrderT order, const uint bytes, const uint32 in, FILE *fp, const char *filename) -{ - uint8 out[4]; - uint i; - - switch(order) - { - case BO_LITTLE: - for(i = 0;i < bytes;i++) - out[i] = (in>>(i*8)) & 0x000000FF; - break; - case BO_BIG: - for(i = 0;i < bytes;i++) - out[bytes - i - 1] = (in>>(i*8)) & 0x000000FF; - break; - default: - break; - } - if(fwrite(out, 1, bytes, fp) != bytes) - { - fprintf(stderr, "Error: Bad write to file '%s'.\n", filename); - return 0; - } - return 1; -} - -// Store the OpenAL Soft HRTF data set. -static int StoreMhr(const HrirDataT *hData, const char *filename) -{ - uint channels = (hData->mChannelType == CT_STEREO) ? 2 : 1; - uint n = hData->mIrPoints; - FILE *fp; - uint fi, ei, ai, i; - uint dither_seed = 22222; - - if((fp=fopen(filename, "wb")) == NULL) - { - fprintf(stderr, "Error: Could not open MHR file '%s'.\n", filename); - return 0; - } - if(!WriteAscii(MHR_FORMAT, fp, filename)) - return 0; - if(!WriteBin4(BO_LITTLE, 4, (uint32)hData->mIrRate, fp, filename)) - return 0; - if(!WriteBin4(BO_LITTLE, 1, (uint32)hData->mSampleType, fp, filename)) - return 0; - if(!WriteBin4(BO_LITTLE, 1, (uint32)hData->mChannelType, fp, filename)) - return 0; - if(!WriteBin4(BO_LITTLE, 1, (uint32)hData->mIrPoints, fp, filename)) - return 0; - if(!WriteBin4(BO_LITTLE, 1, (uint32)hData->mFdCount, fp, filename)) - return 0; - for(fi = 0;fi < hData->mFdCount;fi++) - { - if(!WriteBin4(BO_LITTLE, 2, (uint32)(1000.0 * hData->mFds[fi].mDistance), fp, filename)) - return 0; - if(!WriteBin4(BO_LITTLE, 1, (uint32)hData->mFds[fi].mEvCount, fp, filename)) - return 0; - for(ei = 0;ei < hData->mFds[fi].mEvCount;ei++) - { - if(!WriteBin4(BO_LITTLE, 1, (uint32)hData->mFds[fi].mEvs[ei].mAzCount, fp, filename)) - return 0; - } - } - - for(fi = 0;fi < hData->mFdCount;fi++) - { - const double scale = (hData->mSampleType == ST_S16) ? 32767.0 : - ((hData->mSampleType == ST_S24) ? 8388607.0 : 0.0); - const int bps = (hData->mSampleType == ST_S16) ? 2 : - ((hData->mSampleType == ST_S24) ? 3 : 0); - - for(ei = 0;ei < hData->mFds[fi].mEvCount;ei++) - { - for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) - { - HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; - double out[2 * MAX_TRUNCSIZE]; - - TpdfDither(out, azd->mIrs[0], scale, n, channels, &dither_seed); - if(hData->mChannelType == CT_STEREO) - TpdfDither(out+1, azd->mIrs[1], scale, n, channels, &dither_seed); - for(i = 0;i < (channels * n);i++) - { - int v = (int)Clamp(out[i], -scale-1.0, scale); - if(!WriteBin4(BO_LITTLE, bps, (uint32)v, fp, filename)) - return 0; - } - } - } - } - for(fi = 0;fi < hData->mFdCount;fi++) - { - for(ei = 0;ei < hData->mFds[fi].mEvCount;ei++) - { - for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) - { - HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; - int v = (int)fmin(round(hData->mIrRate * azd->mDelays[0]), MAX_HRTD); - - if(!WriteBin4(BO_LITTLE, 1, (uint32)v, fp, filename)) - return 0; - if(hData->mChannelType == CT_STEREO) - { - v = (int)fmin(round(hData->mIrRate * azd->mDelays[1]), MAX_HRTD); - - if(!WriteBin4(BO_LITTLE, 1, (uint32)v, fp, filename)) - return 0; - } - } - } - } - fclose(fp); - return 1; -} - - -/*********************** - *** HRTF processing *** - ***********************/ - -// Calculate the onset time of an HRIR and average it with any existing -// timing for its field, elevation, azimuth, and ear. -static double AverageHrirOnset(const uint rate, const uint n, const double *hrir, const double f, const double onset) -{ - double mag = 0.0; - uint i; - - for(i = 0;i < n;i++) - mag = fmax(fabs(hrir[i]), mag); - mag *= 0.15; - for(i = 0;i < n;i++) - { - if(fabs(hrir[i]) >= mag) - break; - } - return Lerp(onset, (double)i / rate, f); -} - -// Calculate the magnitude response of an HRIR and average it with any -// existing responses for its field, elevation, azimuth, and ear. -static void AverageHrirMagnitude(const uint points, const uint n, const double *hrir, const double f, double *mag) -{ - uint m = 1 + (n / 2), i; - Complex *h = CreateComplexes(n); - double *r = CreateDoubles(n); - - for(i = 0;i < points;i++) - h[i] = MakeComplex(hrir[i], 0.0); - for(;i < n;i++) - h[i] = MakeComplex(0.0, 0.0); - FftForward(n, h); - MagnitudeResponse(n, h, r); - for(i = 0;i < m;i++) - mag[i] = Lerp(mag[i], r[i], f); - free(r); - free(h); -} - -/* Calculate the contribution of each HRIR to the diffuse-field average based - * on the area of its surface patch. All patches are centered at the HRIR - * coordinates on the unit sphere and are measured by solid angle. - */ -static void CalculateDfWeights(const HrirDataT *hData, double *weights) -{ - double sum, evs, ev, upperEv, lowerEv, solidAngle; - uint fi, ei; - - sum = 0.0; - for(fi = 0;fi < hData->mFdCount;fi++) - { - evs = M_PI / 2.0 / (hData->mFds[fi].mEvCount - 1); - for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvCount;ei++) - { - // For each elevation, calculate the upper and lower limits of - // the patch band. - ev = hData->mFds[fi].mEvs[ei].mElevation; - lowerEv = fmax(-M_PI / 2.0, ev - evs); - upperEv = fmin(M_PI / 2.0, ev + evs); - // Calculate the area of the patch band. - solidAngle = 2.0 * M_PI * (sin(upperEv) - sin(lowerEv)); - // Each weight is the area of one patch. - weights[(fi * MAX_EV_COUNT) + ei] = solidAngle / hData->mFds[fi].mEvs[ei].mAzCount; - // Sum the total surface area covered by the HRIRs of all fields. - sum += solidAngle; - } - } - /* TODO: It may be interesting to experiment with how a volume-based - weighting performs compared to the existing distance-indepenent - surface patches. - */ - for(fi = 0;fi < hData->mFdCount;fi++) - { - // Normalize the weights given the total surface coverage for all - // fields. - for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvCount;ei++) - weights[(fi * MAX_EV_COUNT) + ei] /= sum; - } -} - -/* Calculate the diffuse-field average from the given magnitude responses of - * the HRIR set. Weighting can be applied to compensate for the varying - * surface area covered by each HRIR. The final average can then be limited - * by the specified magnitude range (in positive dB; 0.0 to skip). - */ -static void CalculateDiffuseFieldAverage(const HrirDataT *hData, const uint channels, const uint m, const int weighted, const double limit, double *dfa) -{ - double *weights = CreateDoubles(hData->mFdCount * MAX_EV_COUNT); - uint count, ti, fi, ei, i, ai; - - if(weighted) - { - // Use coverage weighting to calculate the average. - CalculateDfWeights(hData, weights); - } - else - { - double weight; - - // If coverage weighting is not used, the weights still need to be - // averaged by the number of existing HRIRs. - count = hData->mIrCount; - for(fi = 0;fi < hData->mFdCount;fi++) - { - for(ei = 0;ei < hData->mFds[fi].mEvStart;ei++) - count -= hData->mFds[fi].mEvs[ei].mAzCount; - } - weight = 1.0 / count; - - for(fi = 0;fi < hData->mFdCount;fi++) - { - for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvCount;ei++) - weights[(fi * MAX_EV_COUNT) + ei] = weight; - } - } - for(ti = 0;ti < channels;ti++) - { - for(i = 0;i < m;i++) - dfa[(ti * m) + i] = 0.0; - for(fi = 0;fi < hData->mFdCount;fi++) - { - for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvCount;ei++) - { - for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) - { - HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; - // Get the weight for this HRIR's contribution. - double weight = weights[(fi * MAX_EV_COUNT) + ei]; - - // Add this HRIR's weighted power average to the total. - for(i = 0;i < m;i++) - dfa[(ti * m) + i] += weight * azd->mIrs[ti][i] * azd->mIrs[ti][i]; - } - } - } - // Finish the average calculation and keep it from being too small. - for(i = 0;i < m;i++) - dfa[(ti * m) + i] = fmax(sqrt(dfa[(ti * m) + i]), EPSILON); - // Apply a limit to the magnitude range of the diffuse-field average - // if desired. - if(limit > 0.0) - LimitMagnitudeResponse(hData->mFftSize, m, limit, &dfa[ti * m], &dfa[ti * m]); - } - free(weights); -} - -// Perform diffuse-field equalization on the magnitude responses of the HRIR -// set using the given average response. -static void DiffuseFieldEqualize(const uint channels, const uint m, const double *dfa, const HrirDataT *hData) -{ - uint ti, fi, ei, ai, i; - - for(fi = 0;fi < hData->mFdCount;fi++) - { - for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvCount;ei++) - { - for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) - { - HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; - - for(ti = 0;ti < channels;ti++) - { - for(i = 0;i < m;i++) - azd->mIrs[ti][i] /= dfa[(ti * m) + i]; - } - } - } - } -} - -// Perform minimum-phase reconstruction using the magnitude responses of the -// HRIR set. -static void ReconstructHrirs(const HrirDataT *hData) -{ - uint channels = (hData->mChannelType == CT_STEREO) ? 2 : 1; - uint n = hData->mFftSize; - uint ti, fi, ei, ai, i; - Complex *h = CreateComplexes(n); - uint total, count, pcdone, lastpc; - - total = hData->mIrCount; - for(fi = 0;fi < hData->mFdCount;fi++) - { - for(ei = 0;ei < hData->mFds[fi].mEvStart;ei++) - total -= hData->mFds[fi].mEvs[ei].mAzCount; - } - total *= channels; - count = pcdone = lastpc = 0; - printf("%3d%% done.", pcdone); - fflush(stdout); - for(fi = 0;fi < hData->mFdCount;fi++) - { - for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvCount;ei++) - { - for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) - { - HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; - - for(ti = 0;ti < channels;ti++) - { - MinimumPhase(n, azd->mIrs[ti], h); - FftInverse(n, h); - for(i = 0;i < hData->mIrPoints;i++) - azd->mIrs[ti][i] = h[i].Real; - pcdone = ++count * 100 / total; - if(pcdone != lastpc) - { - lastpc = pcdone; - printf("\r%3d%% done.", pcdone); - fflush(stdout); - } - } - } - } - } - printf("\n"); - free(h); -} - -// Resamples the HRIRs for use at the given sampling rate. -static void ResampleHrirs(const uint rate, HrirDataT *hData) -{ - uint channels = (hData->mChannelType == CT_STEREO) ? 2 : 1; - uint n = hData->mIrPoints; - uint ti, fi, ei, ai; - ResamplerT rs; - - ResamplerSetup(&rs, hData->mIrRate, rate); - for(fi = 0;fi < hData->mFdCount;fi++) - { - for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvCount;ei++) - { - for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) - { - HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; - - for(ti = 0;ti < channels;ti++) - ResamplerRun(&rs, n, azd->mIrs[ti], n, azd->mIrs[ti]); - } - } - } - hData->mIrRate = rate; - ResamplerClear(&rs); -} - -/* Given field and elevation indices and an azimuth, calculate the indices of - * the two HRIRs that bound the coordinate along with a factor for - * calculating the continuous HRIR using interpolation. - */ -static void CalcAzIndices(const HrirDataT *hData, const uint fi, const uint ei, const double az, uint *a0, uint *a1, double *af) -{ - double f = (2.0*M_PI + az) * hData->mFds[fi].mEvs[ei].mAzCount / (2.0*M_PI); - uint i = (uint)f % hData->mFds[fi].mEvs[ei].mAzCount; - - f -= floor(f); - *a0 = i; - *a1 = (i + 1) % hData->mFds[fi].mEvs[ei].mAzCount; - *af = f; -} - -// Synthesize any missing onset timings at the bottom elevations of each -// field. This just blends between slightly exaggerated known onsets (not -// an accurate model). -static void SynthesizeOnsets(HrirDataT *hData) -{ - uint channels = (hData->mChannelType == CT_STEREO) ? 2 : 1; - uint ti, fi, oi, ai, ei, a0, a1; - double t, of, af; - - for(fi = 0;fi < hData->mFdCount;fi++) - { - if(hData->mFds[fi].mEvStart <= 0) - continue; - oi = hData->mFds[fi].mEvStart; - - for(ti = 0;ti < channels;ti++) - { - t = 0.0; - for(ai = 0;ai < hData->mFds[fi].mEvs[oi].mAzCount;ai++) - t += hData->mFds[fi].mEvs[oi].mAzs[ai].mDelays[ti]; - hData->mFds[fi].mEvs[0].mAzs[0].mDelays[ti] = 1.32e-4 + (t / hData->mFds[fi].mEvs[oi].mAzCount); - for(ei = 1;ei < hData->mFds[fi].mEvStart;ei++) - { - of = (double)ei / hData->mFds[fi].mEvStart; - for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) - { - CalcAzIndices(hData, fi, oi, hData->mFds[fi].mEvs[ei].mAzs[ai].mAzimuth, &a0, &a1, &af); - hData->mFds[fi].mEvs[ei].mAzs[ai].mDelays[ti] = Lerp( - hData->mFds[fi].mEvs[0].mAzs[0].mDelays[ti], - Lerp(hData->mFds[fi].mEvs[oi].mAzs[a0].mDelays[ti], - hData->mFds[fi].mEvs[oi].mAzs[a1].mDelays[ti], af), - of - ); - } - } - } - } -} - -/* Attempt to synthesize any missing HRIRs at the bottom elevations of each - * field. Right now this just blends the lowest elevation HRIRs together and - * applies some attenuation and high frequency damping. It is a simple, if - * inaccurate model. - */ -static void SynthesizeHrirs(HrirDataT *hData) -{ - uint channels = (hData->mChannelType == CT_STEREO) ? 2 : 1; - uint n = hData->mIrPoints; - uint ti, fi, ai, ei, i; - double lp[4], s0, s1; - double of, b; - uint a0, a1; - double af; - - for(fi = 0;fi < hData->mFdCount;fi++) - { - const uint oi = hData->mFds[fi].mEvStart; - if(oi <= 0) continue; - - for(ti = 0;ti < channels;ti++) - { - for(i = 0;i < n;i++) - hData->mFds[fi].mEvs[0].mAzs[0].mIrs[ti][i] = 0.0; - for(ai = 0;ai < hData->mFds[fi].mEvs[oi].mAzCount;ai++) - { - for(i = 0;i < n;i++) - hData->mFds[fi].mEvs[0].mAzs[0].mIrs[ti][i] += hData->mFds[fi].mEvs[oi].mAzs[ai].mIrs[ti][i] / - hData->mFds[fi].mEvs[oi].mAzCount; - } - for(ei = 1;ei < hData->mFds[fi].mEvStart;ei++) - { - of = (double)ei / hData->mFds[fi].mEvStart; - b = (1.0 - of) * (3.5e-6 * hData->mIrRate); - for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) - { - CalcAzIndices(hData, fi, oi, hData->mFds[fi].mEvs[ei].mAzs[ai].mAzimuth, &a0, &a1, &af); - lp[0] = 0.0; - lp[1] = 0.0; - lp[2] = 0.0; - lp[3] = 0.0; - for(i = 0;i < n;i++) - { - s0 = hData->mFds[fi].mEvs[0].mAzs[0].mIrs[ti][i]; - s1 = Lerp(hData->mFds[fi].mEvs[oi].mAzs[a0].mIrs[ti][i], - hData->mFds[fi].mEvs[oi].mAzs[a1].mIrs[ti][i], af); - s0 = Lerp(s0, s1, of); - lp[0] = Lerp(s0, lp[0], b); - lp[1] = Lerp(lp[0], lp[1], b); - lp[2] = Lerp(lp[1], lp[2], b); - lp[3] = Lerp(lp[2], lp[3], b); - hData->mFds[fi].mEvs[ei].mAzs[ai].mIrs[ti][i] = lp[3]; - } - } - } - b = 3.5e-6 * hData->mIrRate; - lp[0] = 0.0; - lp[1] = 0.0; - lp[2] = 0.0; - lp[3] = 0.0; - for(i = 0;i < n;i++) - { - s0 = hData->mFds[fi].mEvs[0].mAzs[0].mIrs[ti][i]; - lp[0] = Lerp(s0, lp[0], b); - lp[1] = Lerp(lp[0], lp[1], b); - lp[2] = Lerp(lp[1], lp[2], b); - lp[3] = Lerp(lp[2], lp[3], b); - hData->mFds[fi].mEvs[0].mAzs[0].mIrs[ti][i] = lp[3]; - } - } - hData->mFds[fi].mEvStart = 0; - } -} - -// The following routines assume a full set of HRIRs for all elevations. - -// Normalize the HRIR set and slightly attenuate the result. -static void NormalizeHrirs(const HrirDataT *hData) -{ - uint channels = (hData->mChannelType == CT_STEREO) ? 2 : 1; - uint n = hData->mIrPoints; - uint ti, fi, ei, ai, i; - double maxLevel = 0.0; - - for(fi = 0;fi < hData->mFdCount;fi++) - { - for(ei = 0;ei < hData->mFds[fi].mEvCount;ei++) - { - for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) - { - HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; - - for(ti = 0;ti < channels;ti++) - { - for(i = 0;i < n;i++) - maxLevel = fmax(fabs(azd->mIrs[ti][i]), maxLevel); - } - } - } - } - maxLevel = 1.01 * maxLevel; - for(fi = 0;fi < hData->mFdCount;fi++) - { - for(ei = 0;ei < hData->mFds[fi].mEvCount;ei++) - { - for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) - { - HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; - - for(ti = 0;ti < channels;ti++) - { - for(i = 0;i < n;i++) - azd->mIrs[ti][i] /= maxLevel; - } - } - } - } -} - -// Calculate the left-ear time delay using a spherical head model. -static double CalcLTD(const double ev, const double az, const double rad, const double dist) -{ - double azp, dlp, l, al; - - azp = asin(cos(ev) * sin(az)); - dlp = sqrt((dist*dist) + (rad*rad) + (2.0*dist*rad*sin(azp))); - l = sqrt((dist*dist) - (rad*rad)); - al = (0.5 * M_PI) + azp; - if(dlp > l) - dlp = l + (rad * (al - acos(rad / dist))); - return dlp / 343.3; -} - -// Calculate the effective head-related time delays for each minimum-phase -// HRIR. -static void CalculateHrtds(const HeadModelT model, const double radius, HrirDataT *hData) -{ - uint channels = (hData->mChannelType == CT_STEREO) ? 2 : 1; - double minHrtd = INFINITY, maxHrtd = -INFINITY; - uint ti, fi, ei, ai; - double t; - - if(model == HM_DATASET) - { - for(fi = 0;fi < hData->mFdCount;fi++) - { - for(ei = 0;ei < hData->mFds[fi].mEvCount;ei++) - { - for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) - { - HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; - - for(ti = 0;ti < channels;ti++) - { - t = azd->mDelays[ti] * radius / hData->mRadius; - azd->mDelays[ti] = t; - maxHrtd = fmax(t, maxHrtd); - minHrtd = fmin(t, minHrtd); - } - } - } - } - } - else - { - for(fi = 0;fi < hData->mFdCount;fi++) - { - for(ei = 0;ei < hData->mFds[fi].mEvCount;ei++) - { - HrirEvT *evd = &hData->mFds[fi].mEvs[ei]; - - for(ai = 0;ai < evd->mAzCount;ai++) - { - HrirAzT *azd = &evd->mAzs[ai]; - - for(ti = 0;ti < channels;ti++) - { - t = CalcLTD(evd->mElevation, azd->mAzimuth, radius, hData->mFds[fi].mDistance); - azd->mDelays[ti] = t; - maxHrtd = fmax(t, maxHrtd); - minHrtd = fmin(t, minHrtd); - } - } - } - } - } - for(fi = 0;fi < hData->mFdCount;fi++) - { - for(ei = 0;ei < hData->mFds[fi].mEvCount;ei++) - { - for(ti = 0;ti < channels;ti++) - { - for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) - hData->mFds[fi].mEvs[ei].mAzs[ai].mDelays[ti] -= minHrtd; - } - } - } -} - -// Clear the initial HRIR data state. -static void ResetHrirData(HrirDataT *hData) -{ - hData->mIrRate = 0; - hData->mSampleType = ST_S24; - hData->mChannelType = CT_NONE; - hData->mIrPoints = 0; - hData->mFftSize = 0; - hData->mIrSize = 0; - hData->mRadius = 0.0; - hData->mIrCount = 0; - hData->mFdCount = 0; - hData->mFds = NULL; -} - -// Allocate and configure dynamic HRIR structures. -static int PrepareHrirData(const uint fdCount, const double distances[MAX_FD_COUNT], const uint evCounts[MAX_FD_COUNT], const uint azCounts[MAX_FD_COUNT * MAX_EV_COUNT], HrirDataT *hData) -{ - uint evTotal = 0, azTotal = 0, fi, ei, ai; - - for(fi = 0;fi < fdCount;fi++) - { - evTotal += evCounts[fi]; - for(ei = 0;ei < evCounts[fi];ei++) - azTotal += azCounts[(fi * MAX_EV_COUNT) + ei]; - } - if(!fdCount || !evTotal || !azTotal) - return 0; - - hData->mFds = calloc(fdCount, sizeof(*hData->mFds)); - if(hData->mFds == NULL) - return 0; - hData->mFds[0].mEvs = calloc(evTotal, sizeof(*hData->mFds[0].mEvs)); - if(hData->mFds[0].mEvs == NULL) - return 0; - hData->mFds[0].mEvs[0].mAzs = calloc(azTotal, sizeof(*hData->mFds[0].mEvs[0].mAzs)); - if(hData->mFds[0].mEvs[0].mAzs == NULL) - return 0; - hData->mIrCount = azTotal; - hData->mFdCount = fdCount; - evTotal = 0; - azTotal = 0; - for(fi = 0;fi < fdCount;fi++) - { - hData->mFds[fi].mDistance = distances[fi]; - hData->mFds[fi].mEvCount = evCounts[fi]; - hData->mFds[fi].mEvStart = 0; - hData->mFds[fi].mEvs = &hData->mFds[0].mEvs[evTotal]; - evTotal += evCounts[fi]; - for(ei = 0;ei < evCounts[fi];ei++) - { - uint azCount = azCounts[(fi * MAX_EV_COUNT) + ei]; - - hData->mFds[fi].mIrCount += azCount; - hData->mFds[fi].mEvs[ei].mElevation = -M_PI / 2.0 + M_PI * ei / (evCounts[fi] - 1); - hData->mFds[fi].mEvs[ei].mIrCount += azCount; - hData->mFds[fi].mEvs[ei].mAzCount = azCount; - hData->mFds[fi].mEvs[ei].mAzs = &hData->mFds[0].mEvs[0].mAzs[azTotal]; - for(ai = 0;ai < azCount;ai++) - { - hData->mFds[fi].mEvs[ei].mAzs[ai].mAzimuth = 2.0 * M_PI * ai / azCount; - hData->mFds[fi].mEvs[ei].mAzs[ai].mIndex = azTotal + ai; - hData->mFds[fi].mEvs[ei].mAzs[ai].mDelays[0] = 0.0; - hData->mFds[fi].mEvs[ei].mAzs[ai].mDelays[1] = 0.0; - hData->mFds[fi].mEvs[ei].mAzs[ai].mIrs[0] = NULL; - hData->mFds[fi].mEvs[ei].mAzs[ai].mIrs[1] = NULL; - } - azTotal += azCount; - } - } - return 1; -} - -// Clean up HRIR data. -static void FreeHrirData(HrirDataT *hData) -{ - if(hData->mFds != NULL) - { - if(hData->mFds[0].mEvs != NULL) - { - if(hData->mFds[0].mEvs[0].mAzs) - { - free(hData->mFds[0].mEvs[0].mAzs[0].mIrs[0]); - free(hData->mFds[0].mEvs[0].mAzs); - } - free(hData->mFds[0].mEvs); - } - free(hData->mFds); - hData->mFds = NULL; - } -} - -// Match the channel type from a given identifier. -static ChannelTypeT MatchChannelType(const char *ident) -{ - if(strcasecmp(ident, "mono") == 0) - return CT_MONO; - if(strcasecmp(ident, "stereo") == 0) - return CT_STEREO; - return CT_NONE; -} - -// Process the data set definition to read and validate the data set metrics. -static int ProcessMetrics(TokenReaderT *tr, const uint fftSize, const uint truncSize, HrirDataT *hData) -{ - int hasRate = 0, hasType = 0, hasPoints = 0, hasRadius = 0; - int hasDistance = 0, hasAzimuths = 0; - char ident[MAX_IDENT_LEN+1]; - uint line, col; - double fpVal; - uint points; - int intVal; - double distances[MAX_FD_COUNT]; - uint fdCount = 0; - uint evCounts[MAX_FD_COUNT]; - uint *azCounts = calloc(MAX_FD_COUNT * MAX_EV_COUNT, sizeof(*azCounts)); - - if(azCounts == NULL) - { - fprintf(stderr, "Error: Out of memory.\n"); - exit(-1); - } - TrIndication(tr, &line, &col); - while(TrIsIdent(tr)) - { - TrIndication(tr, &line, &col); - if(!TrReadIdent(tr, MAX_IDENT_LEN, ident)) - goto error; - if(strcasecmp(ident, "rate") == 0) - { - if(hasRate) - { - TrErrorAt(tr, line, col, "Redefinition of 'rate'.\n"); - goto error; - } - if(!TrReadOperator(tr, "=")) - goto error; - if(!TrReadInt(tr, MIN_RATE, MAX_RATE, &intVal)) - goto error; - hData->mIrRate = (uint)intVal; - hasRate = 1; - } - else if(strcasecmp(ident, "type") == 0) - { - char type[MAX_IDENT_LEN+1]; - - if(hasType) - { - TrErrorAt(tr, line, col, "Redefinition of 'type'.\n"); - goto error; - } - if(!TrReadOperator(tr, "=")) - goto error; - - if(!TrReadIdent(tr, MAX_IDENT_LEN, type)) - goto error; - hData->mChannelType = MatchChannelType(type); - if(hData->mChannelType == CT_NONE) - { - TrErrorAt(tr, line, col, "Expected a channel type.\n"); - goto error; - } - hasType = 1; - } - else if(strcasecmp(ident, "points") == 0) - { - if(hasPoints) - { - TrErrorAt(tr, line, col, "Redefinition of 'points'.\n"); - goto error; - } - if(!TrReadOperator(tr, "=")) - goto error; - TrIndication(tr, &line, &col); - if(!TrReadInt(tr, MIN_POINTS, MAX_POINTS, &intVal)) - goto error; - points = (uint)intVal; - if(fftSize > 0 && points > fftSize) - { - TrErrorAt(tr, line, col, "Value exceeds the overridden FFT size.\n"); - goto error; - } - if(points < truncSize) - { - TrErrorAt(tr, line, col, "Value is below the truncation size.\n"); - goto error; - } - hData->mIrPoints = points; - if(fftSize <= 0) - { - hData->mFftSize = DEFAULT_FFTSIZE; - hData->mIrSize = 1 + (DEFAULT_FFTSIZE / 2); - } - else - { - hData->mFftSize = fftSize; - hData->mIrSize = 1 + (fftSize / 2); - if(points > hData->mIrSize) - hData->mIrSize = points; - } - hasPoints = 1; - } - else if(strcasecmp(ident, "radius") == 0) - { - if(hasRadius) - { - TrErrorAt(tr, line, col, "Redefinition of 'radius'.\n"); - goto error; - } - if(!TrReadOperator(tr, "=")) - goto error; - if(!TrReadFloat(tr, MIN_RADIUS, MAX_RADIUS, &fpVal)) - goto error; - hData->mRadius = fpVal; - hasRadius = 1; - } - else if(strcasecmp(ident, "distance") == 0) - { - uint count = 0; - - if(hasDistance) - { - TrErrorAt(tr, line, col, "Redefinition of 'distance'.\n"); - goto error; - } - if(!TrReadOperator(tr, "=")) - goto error; - - for(;;) - { - if(!TrReadFloat(tr, MIN_DISTANCE, MAX_DISTANCE, &fpVal)) - goto error; - if(count > 0 && fpVal <= distances[count - 1]) - { - TrError(tr, "Distances are not ascending.\n"); - goto error; - } - distances[count++] = fpVal; - if(!TrIsOperator(tr, ",")) - break; - if(count >= MAX_FD_COUNT) - { - TrError(tr, "Exceeded the maximum of %d fields.\n", MAX_FD_COUNT); - goto error; - } - TrReadOperator(tr, ","); - } - if(fdCount != 0 && count != fdCount) - { - TrError(tr, "Did not match the specified number of %d fields.\n", fdCount); - goto error; - } - fdCount = count; - hasDistance = 1; - } - else if(strcasecmp(ident, "azimuths") == 0) - { - uint count = 0; - - if(hasAzimuths) - { - TrErrorAt(tr, line, col, "Redefinition of 'azimuths'.\n"); - goto error; - } - if(!TrReadOperator(tr, "=")) - goto error; - - evCounts[0] = 0; - for(;;) - { - if(!TrReadInt(tr, MIN_AZ_COUNT, MAX_AZ_COUNT, &intVal)) - goto error; - azCounts[(count * MAX_EV_COUNT) + evCounts[count]++] = (uint)intVal; - if(TrIsOperator(tr, ",")) - { - if(evCounts[count] >= MAX_EV_COUNT) - { - TrError(tr, "Exceeded the maximum of %d elevations.\n", MAX_EV_COUNT); - goto error; - } - TrReadOperator(tr, ","); - } - else - { - if(evCounts[count] < MIN_EV_COUNT) - { - TrErrorAt(tr, line, col, "Did not reach the minimum of %d azimuth counts.\n", MIN_EV_COUNT); - goto error; - } - if(azCounts[count * MAX_EV_COUNT] != 1 || azCounts[(count * MAX_EV_COUNT) + evCounts[count] - 1] != 1) - { - TrError(tr, "Poles are not singular for field %d.\n", count - 1); - goto error; - } - count++; - if(TrIsOperator(tr, ";")) - { - if(count >= MAX_FD_COUNT) - { - TrError(tr, "Exceeded the maximum number of %d fields.\n", MAX_FD_COUNT); - goto error; - } - evCounts[count] = 0; - TrReadOperator(tr, ";"); - } - else - { - break; - } - } - } - if(fdCount != 0 && count != fdCount) - { - TrError(tr, "Did not match the specified number of %d fields.\n", fdCount); - goto error; - } - fdCount = count; - hasAzimuths = 1; - } - else - { - TrErrorAt(tr, line, col, "Expected a metric name.\n"); - goto error; - } - TrSkipWhitespace(tr); - } - if(!(hasRate && hasPoints && hasRadius && hasDistance && hasAzimuths)) - { - TrErrorAt(tr, line, col, "Expected a metric name.\n"); - goto error; - } - if(distances[0] < hData->mRadius) - { - TrError(tr, "Distance cannot start below head radius.\n"); - goto error; - } - if(hData->mChannelType == CT_NONE) - hData->mChannelType = CT_MONO; - if(!PrepareHrirData(fdCount, distances, evCounts, azCounts, hData)) - { - fprintf(stderr, "Error: Out of memory.\n"); - exit(-1); - } - free(azCounts); - return 1; - -error: - free(azCounts); - return 0; -} - -// Parse an index triplet from the data set definition. -static int ReadIndexTriplet(TokenReaderT *tr, const HrirDataT *hData, uint *fi, uint *ei, uint *ai) -{ - int intVal; - - if(hData->mFdCount > 1) - { - if(!TrReadInt(tr, 0, (int)hData->mFdCount - 1, &intVal)) - return 0; - *fi = (uint)intVal; - if(!TrReadOperator(tr, ",")) - return 0; - } - else - { - *fi = 0; - } - if(!TrReadInt(tr, 0, (int)hData->mFds[*fi].mEvCount - 1, &intVal)) - return 0; - *ei = (uint)intVal; - if(!TrReadOperator(tr, ",")) - return 0; - if(!TrReadInt(tr, 0, (int)hData->mFds[*fi].mEvs[*ei].mAzCount - 1, &intVal)) - return 0; - *ai = (uint)intVal; - return 1; -} - -// Match the source format from a given identifier. -static SourceFormatT MatchSourceFormat(const char *ident) -{ - if(strcasecmp(ident, "wave") == 0) - return SF_WAVE; - if(strcasecmp(ident, "bin_le") == 0) - return SF_BIN_LE; - if(strcasecmp(ident, "bin_be") == 0) - return SF_BIN_BE; - if(strcasecmp(ident, "ascii") == 0) - return SF_ASCII; - return SF_NONE; -} - -// Match the source element type from a given identifier. -static ElementTypeT MatchElementType(const char *ident) -{ - if(strcasecmp(ident, "int") == 0) - return ET_INT; - if(strcasecmp(ident, "fp") == 0) - return ET_FP; - return ET_NONE; -} - -// Parse and validate a source reference from the data set definition. -static int ReadSourceRef(TokenReaderT *tr, SourceRefT *src) -{ - char ident[MAX_IDENT_LEN+1]; - uint line, col; - int intVal; - - TrIndication(tr, &line, &col); - if(!TrReadIdent(tr, MAX_IDENT_LEN, ident)) - return 0; - src->mFormat = MatchSourceFormat(ident); - if(src->mFormat == SF_NONE) - { - TrErrorAt(tr, line, col, "Expected a source format.\n"); - return 0; - } - if(!TrReadOperator(tr, "(")) - return 0; - if(src->mFormat == SF_WAVE) - { - if(!TrReadInt(tr, 0, MAX_WAVE_CHANNELS, &intVal)) - return 0; - src->mType = ET_NONE; - src->mSize = 0; - src->mBits = 0; - src->mChannel = (uint)intVal; - src->mSkip = 0; - } - else - { - TrIndication(tr, &line, &col); - if(!TrReadIdent(tr, MAX_IDENT_LEN, ident)) - return 0; - src->mType = MatchElementType(ident); - if(src->mType == ET_NONE) - { - TrErrorAt(tr, line, col, "Expected a source element type.\n"); - return 0; - } - if(src->mFormat == SF_BIN_LE || src->mFormat == SF_BIN_BE) - { - if(!TrReadOperator(tr, ",")) - return 0; - if(src->mType == ET_INT) - { - if(!TrReadInt(tr, MIN_BIN_SIZE, MAX_BIN_SIZE, &intVal)) - return 0; - src->mSize = (uint)intVal; - if(!TrIsOperator(tr, ",")) - src->mBits = (int)(8*src->mSize); - else - { - TrReadOperator(tr, ","); - TrIndication(tr, &line, &col); - if(!TrReadInt(tr, -2147483647-1, 2147483647, &intVal)) - return 0; - if(abs(intVal) < MIN_BIN_BITS || (uint)abs(intVal) > (8*src->mSize)) - { - TrErrorAt(tr, line, col, "Expected a value of (+/-) %d to %d.\n", MIN_BIN_BITS, 8*src->mSize); - return 0; - } - src->mBits = intVal; - } - } - else - { - TrIndication(tr, &line, &col); - if(!TrReadInt(tr, -2147483647-1, 2147483647, &intVal)) - return 0; - if(intVal != 4 && intVal != 8) - { - TrErrorAt(tr, line, col, "Expected a value of 4 or 8.\n"); - return 0; - } - src->mSize = (uint)intVal; - src->mBits = 0; - } - } - else if(src->mFormat == SF_ASCII && src->mType == ET_INT) - { - if(!TrReadOperator(tr, ",")) - return 0; - if(!TrReadInt(tr, MIN_ASCII_BITS, MAX_ASCII_BITS, &intVal)) - return 0; - src->mSize = 0; - src->mBits = intVal; - } - else - { - src->mSize = 0; - src->mBits = 0; - } - - if(!TrIsOperator(tr, ";")) - src->mSkip = 0; - else - { - TrReadOperator(tr, ";"); - if(!TrReadInt(tr, 0, 0x7FFFFFFF, &intVal)) - return 0; - src->mSkip = (uint)intVal; - } - } - if(!TrReadOperator(tr, ")")) - return 0; - if(TrIsOperator(tr, "@")) - { - TrReadOperator(tr, "@"); - if(!TrReadInt(tr, 0, 0x7FFFFFFF, &intVal)) - return 0; - src->mOffset = (uint)intVal; - } - else - src->mOffset = 0; - if(!TrReadOperator(tr, ":")) - return 0; - if(!TrReadString(tr, MAX_PATH_LEN, src->mPath)) - return 0; - return 1; -} - -// Match the target ear (index) from a given identifier. -static int MatchTargetEar(const char *ident) -{ - if(strcasecmp(ident, "left") == 0) - return 0; - if(strcasecmp(ident, "right") == 0) - return 1; - return -1; -} - -// Process the list of sources in the data set definition. -static int ProcessSources(const HeadModelT model, TokenReaderT *tr, HrirDataT *hData) -{ - uint channels = (hData->mChannelType == CT_STEREO) ? 2 : 1; - double *hrirs = CreateDoubles(channels * hData->mIrCount * hData->mIrSize); - double *hrir = CreateDoubles(hData->mIrPoints); - uint line, col, fi, ei, ai, ti; - int count; - - printf("Loading sources..."); - fflush(stdout); - count = 0; - while(TrIsOperator(tr, "[")) - { - double factor[2] = { 1.0, 1.0 }; - - TrIndication(tr, &line, &col); - TrReadOperator(tr, "["); - if(!ReadIndexTriplet(tr, hData, &fi, &ei, &ai)) - goto error; - if(!TrReadOperator(tr, "]")) - goto error; - HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; - - if(azd->mIrs[0] != NULL) - { - TrErrorAt(tr, line, col, "Redefinition of source.\n"); - goto error; - } - if(!TrReadOperator(tr, "=")) - goto error; - - for(;;) - { - SourceRefT src; - uint ti = 0; - - if(!ReadSourceRef(tr, &src)) - goto error; - - // TODO: Would be nice to display 'x of y files', but that would - // require preparing the source refs first to get a total count - // before loading them. - ++count; - printf("\rLoading sources... %d file%s", count, (count==1)?"":"s"); - fflush(stdout); - - if(!LoadSource(&src, hData->mIrRate, hData->mIrPoints, hrir)) - goto error; - - if(hData->mChannelType == CT_STEREO) - { - char ident[MAX_IDENT_LEN+1]; - - if(!TrReadIdent(tr, MAX_IDENT_LEN, ident)) - goto error; - ti = MatchTargetEar(ident); - if((int)ti < 0) - { - TrErrorAt(tr, line, col, "Expected a target ear.\n"); - goto error; - } - } - azd->mIrs[ti] = &hrirs[hData->mIrSize * (ti * hData->mIrCount + azd->mIndex)]; - if(model == HM_DATASET) - azd->mDelays[ti] = AverageHrirOnset(hData->mIrRate, hData->mIrPoints, hrir, 1.0 / factor[ti], azd->mDelays[ti]); - AverageHrirMagnitude(hData->mIrPoints, hData->mFftSize, hrir, 1.0 / factor[ti], azd->mIrs[ti]); - factor[ti] += 1.0; - if(!TrIsOperator(tr, "+")) - break; - TrReadOperator(tr, "+"); - } - if(hData->mChannelType == CT_STEREO) - { - if(azd->mIrs[0] == NULL) - { - TrErrorAt(tr, line, col, "Missing left ear source reference(s).\n"); - goto error; - } - else if(azd->mIrs[1] == NULL) - { - TrErrorAt(tr, line, col, "Missing right ear source reference(s).\n"); - goto error; - } - } - } - printf("\n"); - for(fi = 0;fi < hData->mFdCount;fi++) - { - for(ei = 0;ei < hData->mFds[fi].mEvCount;ei++) - { - for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) - { - HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; - - if(azd->mIrs[0] != NULL) - break; - } - if(ai < hData->mFds[fi].mEvs[ei].mAzCount) - break; - } - if(ei >= hData->mFds[fi].mEvCount) - { - TrError(tr, "Missing source references [ %d, *, * ].\n", fi); - goto error; - } - hData->mFds[fi].mEvStart = ei; - for(;ei < hData->mFds[fi].mEvCount;ei++) - { - for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) - { - HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; - - if(azd->mIrs[0] == NULL) - { - TrError(tr, "Missing source reference [ %d, %d, %d ].\n", fi, ei, ai); - goto error; - } - } - } - } - for(ti = 0;ti < channels;ti++) - { - for(fi = 0;fi < hData->mFdCount;fi++) - { - for(ei = 0;ei < hData->mFds[fi].mEvCount;ei++) - { - for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) - { - HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; - - azd->mIrs[ti] = &hrirs[hData->mIrSize * (ti * hData->mIrCount + azd->mIndex)]; - } - } - } - } - if(!TrLoad(tr)) - { - free(hrir); - return 1; - } - TrError(tr, "Errant data at end of source list.\n"); - -error: - free(hrir); - return 0; -} - -/* Parse the data set definition and process the source data, storing the - * resulting data set as desired. If the input name is NULL it will read - * from standard input. - */ -static int ProcessDefinition(const char *inName, const uint outRate, const uint fftSize, const int equalize, const int surface, const double limit, const uint truncSize, const HeadModelT model, const double radius, const char *outName) -{ - char rateStr[8+1], expName[MAX_PATH_LEN]; - TokenReaderT tr; - HrirDataT hData; - FILE *fp; - int ret; - - ResetHrirData(&hData); - fprintf(stdout, "Reading HRIR definition from %s...\n", inName?inName:"stdin"); - if(inName != NULL) - { - fp = fopen(inName, "r"); - if(fp == NULL) - { - fprintf(stderr, "Error: Could not open definition file '%s'\n", inName); - return 0; - } - TrSetup(fp, inName, &tr); - } - else - { - fp = stdin; - TrSetup(fp, "<stdin>", &tr); - } - if(!ProcessMetrics(&tr, fftSize, truncSize, &hData)) - { - if(inName != NULL) - fclose(fp); - return 0; - } - if(!ProcessSources(model, &tr, &hData)) - { - FreeHrirData(&hData); - if(inName != NULL) - fclose(fp); - return 0; - } - if(fp != stdin) - fclose(fp); - if(equalize) - { - uint c = (hData.mChannelType == CT_STEREO) ? 2 : 1; - uint m = 1 + hData.mFftSize / 2; - double *dfa = CreateDoubles(c * m); - - fprintf(stdout, "Calculating diffuse-field average...\n"); - CalculateDiffuseFieldAverage(&hData, c, m, surface, limit, dfa); - fprintf(stdout, "Performing diffuse-field equalization...\n"); - DiffuseFieldEqualize(c, m, dfa, &hData); - free(dfa); - } - fprintf(stdout, "Performing minimum phase reconstruction...\n"); - ReconstructHrirs(&hData); - if(outRate != 0 && outRate != hData.mIrRate) - { - fprintf(stdout, "Resampling HRIRs...\n"); - ResampleHrirs(outRate, &hData); - } - fprintf(stdout, "Truncating minimum-phase HRIRs...\n"); - hData.mIrPoints = truncSize; - fprintf(stdout, "Synthesizing missing elevations...\n"); - if(model == HM_DATASET) - SynthesizeOnsets(&hData); - SynthesizeHrirs(&hData); - fprintf(stdout, "Normalizing final HRIRs...\n"); - NormalizeHrirs(&hData); - fprintf(stdout, "Calculating impulse delays...\n"); - CalculateHrtds(model, (radius > DEFAULT_CUSTOM_RADIUS) ? radius : hData.mRadius, &hData); - snprintf(rateStr, 8, "%u", hData.mIrRate); - StrSubst(outName, "%r", rateStr, MAX_PATH_LEN, expName); - fprintf(stdout, "Creating MHR data set %s...\n", expName); - ret = StoreMhr(&hData, expName); - - FreeHrirData(&hData); - return ret; -} - -static void PrintHelp(const char *argv0, FILE *ofile) -{ - fprintf(ofile, "Usage: %s [<option>...]\n\n", argv0); - fprintf(ofile, "Options:\n"); - fprintf(ofile, " -m Ignored for compatibility.\n"); - fprintf(ofile, " -r <rate> Change the data set sample rate to the specified value and\n"); - fprintf(ofile, " resample the HRIRs accordingly.\n"); - fprintf(ofile, " -f <points> Override the FFT window size (default: %u).\n", DEFAULT_FFTSIZE); - fprintf(ofile, " -e {on|off} Toggle diffuse-field equalization (default: %s).\n", (DEFAULT_EQUALIZE ? "on" : "off")); - fprintf(ofile, " -s {on|off} Toggle surface-weighted diffuse-field average (default: %s).\n", (DEFAULT_SURFACE ? "on" : "off")); - fprintf(ofile, " -l {<dB>|none} Specify a limit to the magnitude range of the diffuse-field\n"); - fprintf(ofile, " average (default: %.2f).\n", DEFAULT_LIMIT); - fprintf(ofile, " -w <points> Specify the size of the truncation window that's applied\n"); - fprintf(ofile, " after minimum-phase reconstruction (default: %u).\n", DEFAULT_TRUNCSIZE); - fprintf(ofile, " -d {dataset| Specify the model used for calculating the head-delay timing\n"); - fprintf(ofile, " sphere} values (default: %s).\n", ((DEFAULT_HEAD_MODEL == HM_DATASET) ? "dataset" : "sphere")); - fprintf(ofile, " -c <size> Use a customized head radius measured ear-to-ear in meters.\n"); - fprintf(ofile, " -i <filename> Specify an HRIR definition file to use (defaults to stdin).\n"); - fprintf(ofile, " -o <filename> Specify an output file. Use of '%%r' will be substituted with\n"); - fprintf(ofile, " the data set sample rate.\n"); -} - -// Standard command line dispatch. -int main(int argc, char *argv[]) -{ - const char *inName = NULL, *outName = NULL; - uint outRate, fftSize; - int equalize, surface; - char *end = NULL; - HeadModelT model; - uint truncSize; - double radius; - double limit; - int opt; - - GET_UNICODE_ARGS(&argc, &argv); - - if(argc < 2) - { - fprintf(stdout, "HRTF Processing and Composition Utility\n\n"); - PrintHelp(argv[0], stdout); - exit(EXIT_SUCCESS); - } - - outName = "./oalsoft_hrtf_%r.mhr"; - outRate = 0; - fftSize = 0; - equalize = DEFAULT_EQUALIZE; - surface = DEFAULT_SURFACE; - limit = DEFAULT_LIMIT; - truncSize = DEFAULT_TRUNCSIZE; - model = DEFAULT_HEAD_MODEL; - radius = DEFAULT_CUSTOM_RADIUS; - - while((opt=getopt(argc, argv, "mr:f:e:s:l:w:d:c:e:i:o:h")) != -1) - { - switch(opt) - { - case 'm': - fprintf(stderr, "Ignoring unused command '-m'.\n"); - break; - - case 'r': - outRate = strtoul(optarg, &end, 10); - if(end[0] != '\0' || outRate < MIN_RATE || outRate > MAX_RATE) - { - fprintf(stderr, "Error: Got unexpected value \"%s\" for option -%c, expected between %u to %u.\n", optarg, opt, MIN_RATE, MAX_RATE); - exit(EXIT_FAILURE); - } - break; - - case 'f': - fftSize = strtoul(optarg, &end, 10); - if(end[0] != '\0' || (fftSize&(fftSize-1)) || fftSize < MIN_FFTSIZE || fftSize > MAX_FFTSIZE) - { - fprintf(stderr, "Error: Got unexpected value \"%s\" for option -%c, expected a power-of-two between %u to %u.\n", optarg, opt, MIN_FFTSIZE, MAX_FFTSIZE); - exit(EXIT_FAILURE); - } - break; - - case 'e': - if(strcmp(optarg, "on") == 0) - equalize = 1; - else if(strcmp(optarg, "off") == 0) - equalize = 0; - else - { - fprintf(stderr, "Error: Got unexpected value \"%s\" for option -%c, expected on or off.\n", optarg, opt); - exit(EXIT_FAILURE); - } - break; - - case 's': - if(strcmp(optarg, "on") == 0) - surface = 1; - else if(strcmp(optarg, "off") == 0) - surface = 0; - else - { - fprintf(stderr, "Error: Got unexpected value \"%s\" for option -%c, expected on or off.\n", optarg, opt); - exit(EXIT_FAILURE); - } - break; - - case 'l': - if(strcmp(optarg, "none") == 0) - limit = 0.0; - else - { - limit = strtod(optarg, &end); - if(end[0] != '\0' || limit < MIN_LIMIT || limit > MAX_LIMIT) - { - fprintf(stderr, "Error: Got unexpected value \"%s\" for option -%c, expected between %.0f to %.0f.\n", optarg, opt, MIN_LIMIT, MAX_LIMIT); - exit(EXIT_FAILURE); - } - } - break; - - case 'w': - truncSize = strtoul(optarg, &end, 10); - if(end[0] != '\0' || truncSize < MIN_TRUNCSIZE || truncSize > MAX_TRUNCSIZE || (truncSize%MOD_TRUNCSIZE)) - { - fprintf(stderr, "Error: Got unexpected value \"%s\" for option -%c, expected multiple of %u between %u to %u.\n", optarg, opt, MOD_TRUNCSIZE, MIN_TRUNCSIZE, MAX_TRUNCSIZE); - exit(EXIT_FAILURE); - } - break; - - case 'd': - if(strcmp(optarg, "dataset") == 0) - model = HM_DATASET; - else if(strcmp(optarg, "sphere") == 0) - model = HM_SPHERE; - else - { - fprintf(stderr, "Error: Got unexpected value \"%s\" for option -%c, expected dataset or sphere.\n", optarg, opt); - exit(EXIT_FAILURE); - } - break; - - case 'c': - radius = strtod(optarg, &end); - if(end[0] != '\0' || radius < MIN_CUSTOM_RADIUS || radius > MAX_CUSTOM_RADIUS) - { - fprintf(stderr, "Error: Got unexpected value \"%s\" for option -%c, expected between %.2f to %.2f.\n", optarg, opt, MIN_CUSTOM_RADIUS, MAX_CUSTOM_RADIUS); - exit(EXIT_FAILURE); - } - break; - - case 'i': - inName = optarg; - break; - - case 'o': - outName = optarg; - break; - - case 'h': - PrintHelp(argv[0], stdout); - exit(EXIT_SUCCESS); - - default: /* '?' */ - PrintHelp(argv[0], stderr); - exit(EXIT_FAILURE); - } - } - - if(!ProcessDefinition(inName, outRate, fftSize, equalize, surface, limit, - truncSize, model, radius, outName)) - return -1; - fprintf(stdout, "Operation completed.\n"); - - return EXIT_SUCCESS; -} diff --git a/utils/makemhr/loaddef.cpp b/utils/makemhr/loaddef.cpp new file mode 100644 index 00000000..619f5104 --- /dev/null +++ b/utils/makemhr/loaddef.cpp @@ -0,0 +1,2033 @@ +/* + * HRTF utility for producing and demonstrating the process of creating an + * OpenAL Soft compatible HRIR data set. + * + * Copyright (C) 2011-2019 Christopher Fitzgerald + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Or visit: http://www.gnu.org/licenses/old-licenses/gpl-2.0.html + */ + +#include "loaddef.h" + +#include <algorithm> +#include <cctype> +#include <cmath> +#include <cstring> +#include <cstdarg> +#include <cstdio> +#include <cstdlib> +#include <iterator> +#include <limits> +#include <memory> +#include <vector> + +#include "alfstream.h" +#include "alstring.h" +#include "makemhr.h" + +#include "mysofa.h" + +// Constants for accessing the token reader's ring buffer. +#define TR_RING_BITS (16) +#define TR_RING_SIZE (1 << TR_RING_BITS) +#define TR_RING_MASK (TR_RING_SIZE - 1) + +// The token reader's load interval in bytes. +#define TR_LOAD_SIZE (TR_RING_SIZE >> 2) + +// Token reader state for parsing the data set definition. +struct TokenReaderT { + std::istream &mIStream; + const char *mName{}; + uint mLine{}; + uint mColumn{}; + char mRing[TR_RING_SIZE]{}; + std::streamsize mIn{}; + std::streamsize mOut{}; + + TokenReaderT(std::istream &istream) noexcept : mIStream{istream} { } + TokenReaderT(const TokenReaderT&) = default; +}; + + +// The maximum identifier length used when processing the data set +// definition. +#define MAX_IDENT_LEN (16) + +// The limits for the listener's head 'radius' in the data set definition. +#define MIN_RADIUS (0.05) +#define MAX_RADIUS (0.15) + +// The maximum number of channels that can be addressed for a WAVE file +// source listed in the data set definition. +#define MAX_WAVE_CHANNELS (65535) + +// The limits to the byte size for a binary source listed in the definition +// file. +#define MIN_BIN_SIZE (2) +#define MAX_BIN_SIZE (4) + +// The minimum number of significant bits for binary sources listed in the +// data set definition. The maximum is calculated from the byte size. +#define MIN_BIN_BITS (16) + +// The limits to the number of significant bits for an ASCII source listed in +// the data set definition. +#define MIN_ASCII_BITS (16) +#define MAX_ASCII_BITS (32) + +// The four-character-codes for RIFF/RIFX WAVE file chunks. +#define FOURCC_RIFF (0x46464952) // 'RIFF' +#define FOURCC_RIFX (0x58464952) // 'RIFX' +#define FOURCC_WAVE (0x45564157) // 'WAVE' +#define FOURCC_FMT (0x20746D66) // 'fmt ' +#define FOURCC_DATA (0x61746164) // 'data' +#define FOURCC_LIST (0x5453494C) // 'LIST' +#define FOURCC_WAVL (0x6C766177) // 'wavl' +#define FOURCC_SLNT (0x746E6C73) // 'slnt' + +// The supported wave formats. +#define WAVE_FORMAT_PCM (0x0001) +#define WAVE_FORMAT_IEEE_FLOAT (0x0003) +#define WAVE_FORMAT_EXTENSIBLE (0xFFFE) + + +enum ByteOrderT { + BO_NONE, + BO_LITTLE, + BO_BIG +}; + +// Source format for the references listed in the data set definition. +enum SourceFormatT { + SF_NONE, + SF_ASCII, // ASCII text file. + SF_BIN_LE, // Little-endian binary file. + SF_BIN_BE, // Big-endian binary file. + SF_WAVE, // RIFF/RIFX WAVE file. + SF_SOFA // Spatially Oriented Format for Accoustics (SOFA) file. +}; + +// Element types for the references listed in the data set definition. +enum ElementTypeT { + ET_NONE, + ET_INT, // Integer elements. + ET_FP // Floating-point elements. +}; + +// Source reference state used when loading sources. +struct SourceRefT { + SourceFormatT mFormat; + ElementTypeT mType; + uint mSize; + int mBits; + uint mChannel; + double mAzimuth; + double mElevation; + double mRadius; + uint mSkip; + uint mOffset; + char mPath[MAX_PATH_LEN+1]; +}; + + +/* Whitespace is not significant. It can process tokens as identifiers, numbers + * (integer and floating-point), strings, and operators. Strings must be + * encapsulated by double-quotes and cannot span multiple lines. + */ + +// Setup the reader on the given file. The filename can be NULL if no error +// output is desired. +static void TrSetup(const char *startbytes, std::streamsize startbytecount, const char *filename, + TokenReaderT *tr) +{ + const char *name = nullptr; + + if(filename) + { + const char *slash = strrchr(filename, '/'); + if(slash) + { + const char *bslash = strrchr(slash+1, '\\'); + if(bslash) name = bslash+1; + else name = slash+1; + } + else + { + const char *bslash = strrchr(filename, '\\'); + if(bslash) name = bslash+1; + else name = filename; + } + } + + tr->mName = name; + tr->mLine = 1; + tr->mColumn = 1; + tr->mIn = 0; + tr->mOut = 0; + + if(startbytecount > 0) + { + std::copy_n(startbytes, startbytecount, std::begin(tr->mRing)); + tr->mIn += startbytecount; + } +} + +// Prime the reader's ring buffer, and return a result indicating that there +// is text to process. +static int TrLoad(TokenReaderT *tr) +{ + std::istream &istream = tr->mIStream; + + std::streamsize toLoad{TR_RING_SIZE - static_cast<std::streamsize>(tr->mIn - tr->mOut)}; + if(toLoad >= TR_LOAD_SIZE && istream.good()) + { + // Load TR_LOAD_SIZE (or less if at the end of the file) per read. + toLoad = TR_LOAD_SIZE; + std::streamsize in{tr->mIn&TR_RING_MASK}; + std::streamsize count{TR_RING_SIZE - in}; + if(count < toLoad) + { + istream.read(&tr->mRing[in], count); + tr->mIn += istream.gcount(); + istream.read(&tr->mRing[0], toLoad-count); + tr->mIn += istream.gcount(); + } + else + { + istream.read(&tr->mRing[in], toLoad); + tr->mIn += istream.gcount(); + } + + if(tr->mOut >= TR_RING_SIZE) + { + tr->mOut -= TR_RING_SIZE; + tr->mIn -= TR_RING_SIZE; + } + } + if(tr->mIn > tr->mOut) + return 1; + return 0; +} + +// Error display routine. Only displays when the base name is not NULL. +static void TrErrorVA(const TokenReaderT *tr, uint line, uint column, const char *format, va_list argPtr) +{ + if(!tr->mName) + return; + fprintf(stderr, "\nError (%s:%u:%u): ", tr->mName, line, column); + vfprintf(stderr, format, argPtr); +} + +// Used to display an error at a saved line/column. +static void TrErrorAt(const TokenReaderT *tr, uint line, uint column, const char *format, ...) +{ + va_list argPtr; + + va_start(argPtr, format); + TrErrorVA(tr, line, column, format, argPtr); + va_end(argPtr); +} + +// Used to display an error at the current line/column. +static void TrError(const TokenReaderT *tr, const char *format, ...) +{ + va_list argPtr; + + va_start(argPtr, format); + TrErrorVA(tr, tr->mLine, tr->mColumn, format, argPtr); + va_end(argPtr); +} + +// Skips to the next line. +static void TrSkipLine(TokenReaderT *tr) +{ + char ch; + + while(TrLoad(tr)) + { + ch = tr->mRing[tr->mOut&TR_RING_MASK]; + tr->mOut++; + if(ch == '\n') + { + tr->mLine++; + tr->mColumn = 1; + break; + } + tr->mColumn ++; + } +} + +// Skips to the next token. +static int TrSkipWhitespace(TokenReaderT *tr) +{ + while(TrLoad(tr)) + { + char ch{tr->mRing[tr->mOut&TR_RING_MASK]}; + if(isspace(ch)) + { + tr->mOut++; + if(ch == '\n') + { + tr->mLine++; + tr->mColumn = 1; + } + else + tr->mColumn++; + } + else if(ch == '#') + TrSkipLine(tr); + else + return 1; + } + return 0; +} + +// Get the line and/or column of the next token (or the end of input). +static void TrIndication(TokenReaderT *tr, uint *line, uint *column) +{ + TrSkipWhitespace(tr); + if(line) *line = tr->mLine; + if(column) *column = tr->mColumn; +} + +// Checks to see if a token is (likely to be) an identifier. It does not +// display any errors and will not proceed to the next token. +static int TrIsIdent(TokenReaderT *tr) +{ + if(!TrSkipWhitespace(tr)) + return 0; + char ch{tr->mRing[tr->mOut&TR_RING_MASK]}; + return ch == '_' || isalpha(ch); +} + + +// Checks to see if a token is the given operator. It does not display any +// errors and will not proceed to the next token. +static int TrIsOperator(TokenReaderT *tr, const char *op) +{ + std::streamsize out; + size_t len; + char ch; + + if(!TrSkipWhitespace(tr)) + return 0; + out = tr->mOut; + len = 0; + while(op[len] != '\0' && out < tr->mIn) + { + ch = tr->mRing[out&TR_RING_MASK]; + if(ch != op[len]) break; + len++; + out++; + } + if(op[len] == '\0') + return 1; + return 0; +} + +/* The TrRead*() routines obtain the value of a matching token type. They + * display type, form, and boundary errors and will proceed to the next + * token. + */ + +// Reads and validates an identifier token. +static int TrReadIdent(TokenReaderT *tr, const uint maxLen, char *ident) +{ + uint col, len; + char ch; + + col = tr->mColumn; + if(TrSkipWhitespace(tr)) + { + col = tr->mColumn; + ch = tr->mRing[tr->mOut&TR_RING_MASK]; + if(ch == '_' || isalpha(ch)) + { + len = 0; + do { + if(len < maxLen) + ident[len] = ch; + len++; + tr->mOut++; + if(!TrLoad(tr)) + break; + ch = tr->mRing[tr->mOut&TR_RING_MASK]; + } while(ch == '_' || isdigit(ch) || isalpha(ch)); + + tr->mColumn += len; + if(len < maxLen) + { + ident[len] = '\0'; + return 1; + } + TrErrorAt(tr, tr->mLine, col, "Identifier is too long.\n"); + return 0; + } + } + TrErrorAt(tr, tr->mLine, col, "Expected an identifier.\n"); + return 0; +} + +// Reads and validates (including bounds) an integer token. +static int TrReadInt(TokenReaderT *tr, const int loBound, const int hiBound, int *value) +{ + uint col, digis, len; + char ch, temp[64+1]; + + col = tr->mColumn; + if(TrSkipWhitespace(tr)) + { + col = tr->mColumn; + len = 0; + ch = tr->mRing[tr->mOut&TR_RING_MASK]; + if(ch == '+' || ch == '-') + { + temp[len] = ch; + len++; + tr->mOut++; + } + digis = 0; + while(TrLoad(tr)) + { + ch = tr->mRing[tr->mOut&TR_RING_MASK]; + if(!isdigit(ch)) break; + if(len < 64) + temp[len] = ch; + len++; + digis++; + tr->mOut++; + } + tr->mColumn += len; + if(digis > 0 && ch != '.' && !isalpha(ch)) + { + if(len > 64) + { + TrErrorAt(tr, tr->mLine, col, "Integer is too long."); + return 0; + } + temp[len] = '\0'; + *value = static_cast<int>(strtol(temp, nullptr, 10)); + if(*value < loBound || *value > hiBound) + { + TrErrorAt(tr, tr->mLine, col, "Expected a value from %d to %d.\n", loBound, hiBound); + return 0; + } + return 1; + } + } + TrErrorAt(tr, tr->mLine, col, "Expected an integer.\n"); + return 0; +} + +// Reads and validates (including bounds) a float token. +static int TrReadFloat(TokenReaderT *tr, const double loBound, const double hiBound, double *value) +{ + uint col, digis, len; + char ch, temp[64+1]; + + col = tr->mColumn; + if(TrSkipWhitespace(tr)) + { + col = tr->mColumn; + len = 0; + ch = tr->mRing[tr->mOut&TR_RING_MASK]; + if(ch == '+' || ch == '-') + { + temp[len] = ch; + len++; + tr->mOut++; + } + + digis = 0; + while(TrLoad(tr)) + { + ch = tr->mRing[tr->mOut&TR_RING_MASK]; + if(!isdigit(ch)) break; + if(len < 64) + temp[len] = ch; + len++; + digis++; + tr->mOut++; + } + if(ch == '.') + { + if(len < 64) + temp[len] = ch; + len++; + tr->mOut++; + } + while(TrLoad(tr)) + { + ch = tr->mRing[tr->mOut&TR_RING_MASK]; + if(!isdigit(ch)) break; + if(len < 64) + temp[len] = ch; + len++; + digis++; + tr->mOut++; + } + if(digis > 0) + { + if(ch == 'E' || ch == 'e') + { + if(len < 64) + temp[len] = ch; + len++; + digis = 0; + tr->mOut++; + if(ch == '+' || ch == '-') + { + if(len < 64) + temp[len] = ch; + len++; + tr->mOut++; + } + while(TrLoad(tr)) + { + ch = tr->mRing[tr->mOut&TR_RING_MASK]; + if(!isdigit(ch)) break; + if(len < 64) + temp[len] = ch; + len++; + digis++; + tr->mOut++; + } + } + tr->mColumn += len; + if(digis > 0 && ch != '.' && !isalpha(ch)) + { + if(len > 64) + { + TrErrorAt(tr, tr->mLine, col, "Float is too long."); + return 0; + } + temp[len] = '\0'; + *value = strtod(temp, nullptr); + if(*value < loBound || *value > hiBound) + { + TrErrorAt(tr, tr->mLine, col, "Expected a value from %f to %f.\n", loBound, hiBound); + return 0; + } + return 1; + } + } + else + tr->mColumn += len; + } + TrErrorAt(tr, tr->mLine, col, "Expected a float.\n"); + return 0; +} + +// Reads and validates a string token. +static int TrReadString(TokenReaderT *tr, const uint maxLen, char *text) +{ + uint col, len; + char ch; + + col = tr->mColumn; + if(TrSkipWhitespace(tr)) + { + col = tr->mColumn; + ch = tr->mRing[tr->mOut&TR_RING_MASK]; + if(ch == '\"') + { + tr->mOut++; + len = 0; + while(TrLoad(tr)) + { + ch = tr->mRing[tr->mOut&TR_RING_MASK]; + tr->mOut++; + if(ch == '\"') + break; + if(ch == '\n') + { + TrErrorAt(tr, tr->mLine, col, "Unterminated string at end of line.\n"); + return 0; + } + if(len < maxLen) + text[len] = ch; + len++; + } + if(ch != '\"') + { + tr->mColumn += 1 + len; + TrErrorAt(tr, tr->mLine, col, "Unterminated string at end of input.\n"); + return 0; + } + tr->mColumn += 2 + len; + if(len > maxLen) + { + TrErrorAt(tr, tr->mLine, col, "String is too long.\n"); + return 0; + } + text[len] = '\0'; + return 1; + } + } + TrErrorAt(tr, tr->mLine, col, "Expected a string.\n"); + return 0; +} + +// Reads and validates the given operator. +static int TrReadOperator(TokenReaderT *tr, const char *op) +{ + uint col, len; + char ch; + + col = tr->mColumn; + if(TrSkipWhitespace(tr)) + { + col = tr->mColumn; + len = 0; + while(op[len] != '\0' && TrLoad(tr)) + { + ch = tr->mRing[tr->mOut&TR_RING_MASK]; + if(ch != op[len]) break; + len++; + tr->mOut++; + } + tr->mColumn += len; + if(op[len] == '\0') + return 1; + } + TrErrorAt(tr, tr->mLine, col, "Expected '%s' operator.\n", op); + return 0; +} + + +/************************* + *** File source input *** + *************************/ + +// Read a binary value of the specified byte order and byte size from a file, +// storing it as a 32-bit unsigned integer. +static int ReadBin4(std::istream &istream, const char *filename, const ByteOrderT order, const uint bytes, uint32_t *out) +{ + uint8_t in[4]; + istream.read(reinterpret_cast<char*>(in), static_cast<int>(bytes)); + if(istream.gcount() != bytes) + { + fprintf(stderr, "\nError: Bad read from file '%s'.\n", filename); + return 0; + } + uint32_t accum{0}; + switch(order) + { + case BO_LITTLE: + for(uint i = 0;i < bytes;i++) + accum = (accum<<8) | in[bytes - i - 1]; + break; + case BO_BIG: + for(uint i = 0;i < bytes;i++) + accum = (accum<<8) | in[i]; + break; + default: + break; + } + *out = accum; + return 1; +} + +// Read a binary value of the specified byte order from a file, storing it as +// a 64-bit unsigned integer. +static int ReadBin8(std::istream &istream, const char *filename, const ByteOrderT order, uint64_t *out) +{ + uint8_t in[8]; + uint64_t accum; + uint i; + + istream.read(reinterpret_cast<char*>(in), 8); + if(istream.gcount() != 8) + { + fprintf(stderr, "\nError: Bad read from file '%s'.\n", filename); + return 0; + } + accum = 0; + switch(order) + { + case BO_LITTLE: + for(i = 0;i < 8;i++) + accum = (accum<<8) | in[8 - i - 1]; + break; + case BO_BIG: + for(i = 0;i < 8;i++) + accum = (accum<<8) | in[i]; + break; + default: + break; + } + *out = accum; + return 1; +} + +/* Read a binary value of the specified type, byte order, and byte size from + * a file, converting it to a double. For integer types, the significant + * bits are used to normalize the result. The sign of bits determines + * whether they are padded toward the MSB (negative) or LSB (positive). + * Floating-point types are not normalized. + */ +static int ReadBinAsDouble(std::istream &istream, const char *filename, const ByteOrderT order, + const ElementTypeT type, const uint bytes, const int bits, double *out) +{ + union { + uint32_t ui; + int32_t i; + float f; + } v4; + union { + uint64_t ui; + double f; + } v8; + + *out = 0.0; + if(bytes > 4) + { + if(!ReadBin8(istream, filename, order, &v8.ui)) + return 0; + if(type == ET_FP) + *out = v8.f; + } + else + { + if(!ReadBin4(istream, filename, order, bytes, &v4.ui)) + return 0; + if(type == ET_FP) + *out = v4.f; + else + { + if(bits > 0) + v4.ui >>= (8*bytes) - (static_cast<uint>(bits)); + else + v4.ui &= (0xFFFFFFFF >> (32+bits)); + + if(v4.ui&static_cast<uint>(1<<(std::abs(bits)-1))) + v4.ui |= (0xFFFFFFFF << std::abs(bits)); + *out = v4.i / static_cast<double>(1<<(std::abs(bits)-1)); + } + } + return 1; +} + +/* Read an ascii value of the specified type from a file, converting it to a + * double. For integer types, the significant bits are used to normalize the + * result. The sign of the bits should always be positive. This also skips + * up to one separator character before the element itself. + */ +static int ReadAsciiAsDouble(TokenReaderT *tr, const char *filename, const ElementTypeT type, const uint bits, double *out) +{ + if(TrIsOperator(tr, ",")) + TrReadOperator(tr, ","); + else if(TrIsOperator(tr, ":")) + TrReadOperator(tr, ":"); + else if(TrIsOperator(tr, ";")) + TrReadOperator(tr, ";"); + else if(TrIsOperator(tr, "|")) + TrReadOperator(tr, "|"); + + if(type == ET_FP) + { + if(!TrReadFloat(tr, -std::numeric_limits<double>::infinity(), + std::numeric_limits<double>::infinity(), out)) + { + fprintf(stderr, "\nError: Bad read from file '%s'.\n", filename); + return 0; + } + } + else + { + int v; + if(!TrReadInt(tr, -(1<<(bits-1)), (1<<(bits-1))-1, &v)) + { + fprintf(stderr, "\nError: Bad read from file '%s'.\n", filename); + return 0; + } + *out = v / static_cast<double>((1<<(bits-1))-1); + } + return 1; +} + +// Read the RIFF/RIFX WAVE format chunk from a file, validating it against +// the source parameters and data set metrics. +static int ReadWaveFormat(std::istream &istream, const ByteOrderT order, const uint hrirRate, + SourceRefT *src) +{ + uint32_t fourCC, chunkSize; + uint32_t format, channels, rate, dummy, block, size, bits; + + chunkSize = 0; + do { + if(chunkSize > 0) + istream.seekg(static_cast<int>(chunkSize), std::ios::cur); + if(!ReadBin4(istream, src->mPath, BO_LITTLE, 4, &fourCC) + || !ReadBin4(istream, src->mPath, order, 4, &chunkSize)) + return 0; + } while(fourCC != FOURCC_FMT); + if(!ReadBin4(istream, src->mPath, order, 2, &format) + || !ReadBin4(istream, src->mPath, order, 2, &channels) + || !ReadBin4(istream, src->mPath, order, 4, &rate) + || !ReadBin4(istream, src->mPath, order, 4, &dummy) + || !ReadBin4(istream, src->mPath, order, 2, &block)) + return 0; + block /= channels; + if(chunkSize > 14) + { + if(!ReadBin4(istream, src->mPath, order, 2, &size)) + return 0; + size /= 8; + if(block > size) + size = block; + } + else + size = block; + if(format == WAVE_FORMAT_EXTENSIBLE) + { + istream.seekg(2, std::ios::cur); + if(!ReadBin4(istream, src->mPath, order, 2, &bits)) + return 0; + if(bits == 0) + bits = 8 * size; + istream.seekg(4, std::ios::cur); + if(!ReadBin4(istream, src->mPath, order, 2, &format)) + return 0; + istream.seekg(static_cast<int>(chunkSize - 26), std::ios::cur); + } + else + { + bits = 8 * size; + if(chunkSize > 14) + istream.seekg(static_cast<int>(chunkSize - 16), std::ios::cur); + else + istream.seekg(static_cast<int>(chunkSize - 14), std::ios::cur); + } + if(format != WAVE_FORMAT_PCM && format != WAVE_FORMAT_IEEE_FLOAT) + { + fprintf(stderr, "\nError: Unsupported WAVE format in file '%s'.\n", src->mPath); + return 0; + } + if(src->mChannel >= channels) + { + fprintf(stderr, "\nError: Missing source channel in WAVE file '%s'.\n", src->mPath); + return 0; + } + if(rate != hrirRate) + { + fprintf(stderr, "\nError: Mismatched source sample rate in WAVE file '%s'.\n", src->mPath); + return 0; + } + if(format == WAVE_FORMAT_PCM) + { + if(size < 2 || size > 4) + { + fprintf(stderr, "\nError: Unsupported sample size in WAVE file '%s'.\n", src->mPath); + return 0; + } + if(bits < 16 || bits > (8*size)) + { + fprintf(stderr, "\nError: Bad significant bits in WAVE file '%s'.\n", src->mPath); + return 0; + } + src->mType = ET_INT; + } + else + { + if(size != 4 && size != 8) + { + fprintf(stderr, "\nError: Unsupported sample size in WAVE file '%s'.\n", src->mPath); + return 0; + } + src->mType = ET_FP; + } + src->mSize = size; + src->mBits = static_cast<int>(bits); + src->mSkip = channels; + return 1; +} + +// Read a RIFF/RIFX WAVE data chunk, converting all elements to doubles. +static int ReadWaveData(std::istream &istream, const SourceRefT *src, const ByteOrderT order, + const uint n, double *hrir) +{ + int pre, post, skip; + uint i; + + pre = static_cast<int>(src->mSize * src->mChannel); + post = static_cast<int>(src->mSize * (src->mSkip - src->mChannel - 1)); + skip = 0; + for(i = 0;i < n;i++) + { + skip += pre; + if(skip > 0) + istream.seekg(skip, std::ios::cur); + if(!ReadBinAsDouble(istream, src->mPath, order, src->mType, src->mSize, src->mBits, &hrir[i])) + return 0; + skip = post; + } + if(skip > 0) + istream.seekg(skip, std::ios::cur); + return 1; +} + +// Read the RIFF/RIFX WAVE list or data chunk, converting all elements to +// doubles. +static int ReadWaveList(std::istream &istream, const SourceRefT *src, const ByteOrderT order, + const uint n, double *hrir) +{ + uint32_t fourCC, chunkSize, listSize, count; + uint block, skip, offset, i; + double lastSample; + + for(;;) + { + if(!ReadBin4(istream, src->mPath, BO_LITTLE, 4, &fourCC) + || !ReadBin4(istream, src->mPath, order, 4, &chunkSize)) + return 0; + + if(fourCC == FOURCC_DATA) + { + block = src->mSize * src->mSkip; + count = chunkSize / block; + if(count < (src->mOffset + n)) + { + fprintf(stderr, "\nError: Bad read from file '%s'.\n", src->mPath); + return 0; + } + istream.seekg(static_cast<long>(src->mOffset * block), std::ios::cur); + if(!ReadWaveData(istream, src, order, n, &hrir[0])) + return 0; + return 1; + } + else if(fourCC == FOURCC_LIST) + { + if(!ReadBin4(istream, src->mPath, BO_LITTLE, 4, &fourCC)) + return 0; + chunkSize -= 4; + if(fourCC == FOURCC_WAVL) + break; + } + if(chunkSize > 0) + istream.seekg(static_cast<long>(chunkSize), std::ios::cur); + } + listSize = chunkSize; + block = src->mSize * src->mSkip; + skip = src->mOffset; + offset = 0; + lastSample = 0.0; + while(offset < n && listSize > 8) + { + if(!ReadBin4(istream, src->mPath, BO_LITTLE, 4, &fourCC) + || !ReadBin4(istream, src->mPath, order, 4, &chunkSize)) + return 0; + listSize -= 8 + chunkSize; + if(fourCC == FOURCC_DATA) + { + count = chunkSize / block; + if(count > skip) + { + istream.seekg(static_cast<long>(skip * block), std::ios::cur); + chunkSize -= skip * block; + count -= skip; + skip = 0; + if(count > (n - offset)) + count = n - offset; + if(!ReadWaveData(istream, src, order, count, &hrir[offset])) + return 0; + chunkSize -= count * block; + offset += count; + lastSample = hrir[offset - 1]; + } + else + { + skip -= count; + count = 0; + } + } + else if(fourCC == FOURCC_SLNT) + { + if(!ReadBin4(istream, src->mPath, order, 4, &count)) + return 0; + chunkSize -= 4; + if(count > skip) + { + count -= skip; + skip = 0; + if(count > (n - offset)) + count = n - offset; + for(i = 0; i < count; i ++) + hrir[offset + i] = lastSample; + offset += count; + } + else + { + skip -= count; + count = 0; + } + } + if(chunkSize > 0) + istream.seekg(static_cast<long>(chunkSize), std::ios::cur); + } + if(offset < n) + { + fprintf(stderr, "\nError: Bad read from file '%s'.\n", src->mPath); + return 0; + } + return 1; +} + +// Load a source HRIR from an ASCII text file containing a list of elements +// separated by whitespace or common list operators (',', ';', ':', '|'). +static int LoadAsciiSource(std::istream &istream, const SourceRefT *src, + const uint n, double *hrir) +{ + TokenReaderT tr{istream}; + uint i, j; + double dummy; + + TrSetup(nullptr, 0, nullptr, &tr); + for(i = 0;i < src->mOffset;i++) + { + if(!ReadAsciiAsDouble(&tr, src->mPath, src->mType, static_cast<uint>(src->mBits), &dummy)) + return 0; + } + for(i = 0;i < n;i++) + { + if(!ReadAsciiAsDouble(&tr, src->mPath, src->mType, static_cast<uint>(src->mBits), &hrir[i])) + return 0; + for(j = 0;j < src->mSkip;j++) + { + if(!ReadAsciiAsDouble(&tr, src->mPath, src->mType, static_cast<uint>(src->mBits), &dummy)) + return 0; + } + } + return 1; +} + +// Load a source HRIR from a binary file. +static int LoadBinarySource(std::istream &istream, const SourceRefT *src, const ByteOrderT order, + const uint n, double *hrir) +{ + istream.seekg(static_cast<long>(src->mOffset), std::ios::beg); + for(uint i{0};i < n;i++) + { + if(!ReadBinAsDouble(istream, src->mPath, order, src->mType, src->mSize, src->mBits, &hrir[i])) + return 0; + if(src->mSkip > 0) + istream.seekg(static_cast<long>(src->mSkip), std::ios::cur); + } + return 1; +} + +// Load a source HRIR from a RIFF/RIFX WAVE file. +static int LoadWaveSource(std::istream &istream, SourceRefT *src, const uint hrirRate, + const uint n, double *hrir) +{ + uint32_t fourCC, dummy; + ByteOrderT order; + + if(!ReadBin4(istream, src->mPath, BO_LITTLE, 4, &fourCC) + || !ReadBin4(istream, src->mPath, BO_LITTLE, 4, &dummy)) + return 0; + if(fourCC == FOURCC_RIFF) + order = BO_LITTLE; + else if(fourCC == FOURCC_RIFX) + order = BO_BIG; + else + { + fprintf(stderr, "\nError: No RIFF/RIFX chunk in file '%s'.\n", src->mPath); + return 0; + } + + if(!ReadBin4(istream, src->mPath, BO_LITTLE, 4, &fourCC)) + return 0; + if(fourCC != FOURCC_WAVE) + { + fprintf(stderr, "\nError: Not a RIFF/RIFX WAVE file '%s'.\n", src->mPath); + return 0; + } + if(!ReadWaveFormat(istream, order, hrirRate, src)) + return 0; + if(!ReadWaveList(istream, src, order, n, hrir)) + return 0; + return 1; +} + + + +// Load a Spatially Oriented Format for Accoustics (SOFA) file. +static MYSOFA_EASY* LoadSofaFile(SourceRefT *src, const uint hrirRate, const uint n) +{ + struct MYSOFA_EASY *sofa{mysofa_cache_lookup(src->mPath, static_cast<float>(hrirRate))}; + if(sofa) return sofa; + + sofa = static_cast<MYSOFA_EASY*>(calloc(1, sizeof(*sofa))); + if(sofa == nullptr) + { + fprintf(stderr, "\nError: Out of memory.\n"); + return nullptr; + } + sofa->lookup = nullptr; + sofa->neighborhood = nullptr; + + int err; + sofa->hrtf = mysofa_load(src->mPath, &err); + if(!sofa->hrtf) + { + mysofa_close(sofa); + fprintf(stderr, "\nError: Could not load source file '%s'.\n", src->mPath); + return nullptr; + } + /* NOTE: Some valid SOFA files are failing this check. */ + err = mysofa_check(sofa->hrtf); + if(err != MYSOFA_OK) + fprintf(stderr, "\nWarning: Supposedly malformed source file '%s'.\n", src->mPath); + if((src->mOffset + n) > sofa->hrtf->N) + { + mysofa_close(sofa); + fprintf(stderr, "\nError: Not enough samples in SOFA file '%s'.\n", src->mPath); + return nullptr; + } + if(src->mChannel >= sofa->hrtf->R) + { + mysofa_close(sofa); + fprintf(stderr, "\nError: Missing source receiver in SOFA file '%s'.\n", src->mPath); + return nullptr; + } + mysofa_tocartesian(sofa->hrtf); + sofa->lookup = mysofa_lookup_init(sofa->hrtf); + if(sofa->lookup == nullptr) + { + mysofa_close(sofa); + fprintf(stderr, "\nError: Out of memory.\n"); + return nullptr; + } + return mysofa_cache_store(sofa, src->mPath, static_cast<float>(hrirRate)); +} + +// Copies the HRIR data from a particular SOFA measurement. +static void ExtractSofaHrir(const MYSOFA_EASY *sofa, const uint index, const uint channel, const uint offset, const uint n, double *hrir) +{ + for(uint i{0u};i < n;i++) + hrir[i] = sofa->hrtf->DataIR.values[(index*sofa->hrtf->R + channel)*sofa->hrtf->N + offset + i]; +} + +// Load a source HRIR from a Spatially Oriented Format for Accoustics (SOFA) +// file. +static int LoadSofaSource(SourceRefT *src, const uint hrirRate, const uint n, double *hrir) +{ + struct MYSOFA_EASY *sofa; + float target[3]; + int nearest; + float *coords; + + sofa = LoadSofaFile(src, hrirRate, n); + if(sofa == nullptr) + return 0; + + /* NOTE: At some point it may be benficial or necessary to consider the + various coordinate systems, listener/source orientations, and + direciontal vectors defined in the SOFA file. + */ + target[0] = static_cast<float>(src->mAzimuth); + target[1] = static_cast<float>(src->mElevation); + target[2] = static_cast<float>(src->mRadius); + mysofa_s2c(target); + + nearest = mysofa_lookup(sofa->lookup, target); + if(nearest < 0) + { + fprintf(stderr, "\nError: Lookup failed in source file '%s'.\n", src->mPath); + return 0; + } + + coords = &sofa->hrtf->SourcePosition.values[3 * nearest]; + if(std::abs(coords[0] - target[0]) > 0.001 || std::abs(coords[1] - target[1]) > 0.001 || std::abs(coords[2] - target[2]) > 0.001) + { + fprintf(stderr, "\nError: No impulse response at coordinates (%.3fr, %.1fev, %.1faz) in file '%s'.\n", src->mRadius, src->mElevation, src->mAzimuth, src->mPath); + target[0] = coords[0]; + target[1] = coords[1]; + target[2] = coords[2]; + mysofa_c2s(target); + fprintf(stderr, " Nearest candidate at (%.3fr, %.1fev, %.1faz).\n", target[2], target[1], target[0]); + return 0; + } + + ExtractSofaHrir(sofa, static_cast<uint>(nearest), src->mChannel, src->mOffset, n, hrir); + + return 1; +} + +// Load a source HRIR from a supported file type. +static int LoadSource(SourceRefT *src, const uint hrirRate, const uint n, double *hrir) +{ + std::unique_ptr<al::ifstream> istream; + if(src->mFormat != SF_SOFA) + { + if(src->mFormat == SF_ASCII) + istream.reset(new al::ifstream{src->mPath}); + else + istream.reset(new al::ifstream{src->mPath, std::ios::binary}); + if(!istream->good()) + { + fprintf(stderr, "\nError: Could not open source file '%s'.\n", src->mPath); + return 0; + } + } + int result{0}; + switch(src->mFormat) + { + case SF_ASCII: + result = LoadAsciiSource(*istream, src, n, hrir); + break; + case SF_BIN_LE: + result = LoadBinarySource(*istream, src, BO_LITTLE, n, hrir); + break; + case SF_BIN_BE: + result = LoadBinarySource(*istream, src, BO_BIG, n, hrir); + break; + case SF_WAVE: + result = LoadWaveSource(*istream, src, hrirRate, n, hrir); + break; + case SF_SOFA: + result = LoadSofaSource(src, hrirRate, n, hrir); + break; + case SF_NONE: + break; + } + return result; +} + + +// Match the channel type from a given identifier. +static ChannelTypeT MatchChannelType(const char *ident) +{ + if(al::strcasecmp(ident, "mono") == 0) + return CT_MONO; + if(al::strcasecmp(ident, "stereo") == 0) + return CT_STEREO; + return CT_NONE; +} + + +// Process the data set definition to read and validate the data set metrics. +static int ProcessMetrics(TokenReaderT *tr, const uint fftSize, const uint truncSize, const ChannelModeT chanMode, HrirDataT *hData) +{ + int hasRate = 0, hasType = 0, hasPoints = 0, hasRadius = 0; + int hasDistance = 0, hasAzimuths = 0; + char ident[MAX_IDENT_LEN+1]; + uint line, col; + double fpVal; + uint points; + int intVal; + double distances[MAX_FD_COUNT]; + uint fdCount = 0; + uint evCounts[MAX_FD_COUNT]; + std::vector<uint> azCounts(MAX_FD_COUNT * MAX_EV_COUNT); + + TrIndication(tr, &line, &col); + while(TrIsIdent(tr)) + { + TrIndication(tr, &line, &col); + if(!TrReadIdent(tr, MAX_IDENT_LEN, ident)) + return 0; + if(al::strcasecmp(ident, "rate") == 0) + { + if(hasRate) + { + TrErrorAt(tr, line, col, "Redefinition of 'rate'.\n"); + return 0; + } + if(!TrReadOperator(tr, "=")) + return 0; + if(!TrReadInt(tr, MIN_RATE, MAX_RATE, &intVal)) + return 0; + hData->mIrRate = static_cast<uint>(intVal); + hasRate = 1; + } + else if(al::strcasecmp(ident, "type") == 0) + { + char type[MAX_IDENT_LEN+1]; + + if(hasType) + { + TrErrorAt(tr, line, col, "Redefinition of 'type'.\n"); + return 0; + } + if(!TrReadOperator(tr, "=")) + return 0; + + if(!TrReadIdent(tr, MAX_IDENT_LEN, type)) + return 0; + hData->mChannelType = MatchChannelType(type); + if(hData->mChannelType == CT_NONE) + { + TrErrorAt(tr, line, col, "Expected a channel type.\n"); + return 0; + } + else if(hData->mChannelType == CT_STEREO) + { + if(chanMode == CM_ForceMono) + hData->mChannelType = CT_MONO; + } + hasType = 1; + } + else if(al::strcasecmp(ident, "points") == 0) + { + if(hasPoints) + { + TrErrorAt(tr, line, col, "Redefinition of 'points'.\n"); + return 0; + } + if(!TrReadOperator(tr, "=")) + return 0; + TrIndication(tr, &line, &col); + if(!TrReadInt(tr, MIN_POINTS, MAX_POINTS, &intVal)) + return 0; + points = static_cast<uint>(intVal); + if(fftSize > 0 && points > fftSize) + { + TrErrorAt(tr, line, col, "Value exceeds the overridden FFT size.\n"); + return 0; + } + if(points < truncSize) + { + TrErrorAt(tr, line, col, "Value is below the truncation size.\n"); + return 0; + } + hData->mIrPoints = points; + hData->mFftSize = fftSize; + hData->mIrSize = 1 + (fftSize / 2); + if(points > hData->mIrSize) + hData->mIrSize = points; + hasPoints = 1; + } + else if(al::strcasecmp(ident, "radius") == 0) + { + if(hasRadius) + { + TrErrorAt(tr, line, col, "Redefinition of 'radius'.\n"); + return 0; + } + if(!TrReadOperator(tr, "=")) + return 0; + if(!TrReadFloat(tr, MIN_RADIUS, MAX_RADIUS, &fpVal)) + return 0; + hData->mRadius = fpVal; + hasRadius = 1; + } + else if(al::strcasecmp(ident, "distance") == 0) + { + uint count = 0; + + if(hasDistance) + { + TrErrorAt(tr, line, col, "Redefinition of 'distance'.\n"); + return 0; + } + if(!TrReadOperator(tr, "=")) + return 0; + + for(;;) + { + if(!TrReadFloat(tr, MIN_DISTANCE, MAX_DISTANCE, &fpVal)) + return 0; + if(count > 0 && fpVal <= distances[count - 1]) + { + TrError(tr, "Distances are not ascending.\n"); + return 0; + } + distances[count++] = fpVal; + if(!TrIsOperator(tr, ",")) + break; + if(count >= MAX_FD_COUNT) + { + TrError(tr, "Exceeded the maximum of %d fields.\n", MAX_FD_COUNT); + return 0; + } + TrReadOperator(tr, ","); + } + if(fdCount != 0 && count != fdCount) + { + TrError(tr, "Did not match the specified number of %d fields.\n", fdCount); + return 0; + } + fdCount = count; + hasDistance = 1; + } + else if(al::strcasecmp(ident, "azimuths") == 0) + { + uint count = 0; + + if(hasAzimuths) + { + TrErrorAt(tr, line, col, "Redefinition of 'azimuths'.\n"); + return 0; + } + if(!TrReadOperator(tr, "=")) + return 0; + + evCounts[0] = 0; + for(;;) + { + if(!TrReadInt(tr, MIN_AZ_COUNT, MAX_AZ_COUNT, &intVal)) + return 0; + azCounts[(count * MAX_EV_COUNT) + evCounts[count]++] = static_cast<uint>(intVal); + if(TrIsOperator(tr, ",")) + { + if(evCounts[count] >= MAX_EV_COUNT) + { + TrError(tr, "Exceeded the maximum of %d elevations.\n", MAX_EV_COUNT); + return 0; + } + TrReadOperator(tr, ","); + } + else + { + if(evCounts[count] < MIN_EV_COUNT) + { + TrErrorAt(tr, line, col, "Did not reach the minimum of %d azimuth counts.\n", MIN_EV_COUNT); + return 0; + } + if(azCounts[count * MAX_EV_COUNT] != 1 || azCounts[(count * MAX_EV_COUNT) + evCounts[count] - 1] != 1) + { + TrError(tr, "Poles are not singular for field %d.\n", count - 1); + return 0; + } + count++; + if(!TrIsOperator(tr, ";")) + break; + + if(count >= MAX_FD_COUNT) + { + TrError(tr, "Exceeded the maximum number of %d fields.\n", MAX_FD_COUNT); + return 0; + } + evCounts[count] = 0; + TrReadOperator(tr, ";"); + } + } + if(fdCount != 0 && count != fdCount) + { + TrError(tr, "Did not match the specified number of %d fields.\n", fdCount); + return 0; + } + fdCount = count; + hasAzimuths = 1; + } + else + { + TrErrorAt(tr, line, col, "Expected a metric name.\n"); + return 0; + } + TrSkipWhitespace(tr); + } + if(!(hasRate && hasPoints && hasRadius && hasDistance && hasAzimuths)) + { + TrErrorAt(tr, line, col, "Expected a metric name.\n"); + return 0; + } + if(distances[0] < hData->mRadius) + { + TrError(tr, "Distance cannot start below head radius.\n"); + return 0; + } + if(hData->mChannelType == CT_NONE) + hData->mChannelType = CT_MONO; + if(!PrepareHrirData(fdCount, distances, evCounts, azCounts.data(), hData)) + { + fprintf(stderr, "Error: Out of memory.\n"); + exit(-1); + } + return 1; +} + +// Parse an index triplet from the data set definition. +static int ReadIndexTriplet(TokenReaderT *tr, const HrirDataT *hData, uint *fi, uint *ei, uint *ai) +{ + int intVal; + + if(hData->mFdCount > 1) + { + if(!TrReadInt(tr, 0, static_cast<int>(hData->mFdCount) - 1, &intVal)) + return 0; + *fi = static_cast<uint>(intVal); + if(!TrReadOperator(tr, ",")) + return 0; + } + else + { + *fi = 0; + } + if(!TrReadInt(tr, 0, static_cast<int>(hData->mFds[*fi].mEvCount) - 1, &intVal)) + return 0; + *ei = static_cast<uint>(intVal); + if(!TrReadOperator(tr, ",")) + return 0; + if(!TrReadInt(tr, 0, static_cast<int>(hData->mFds[*fi].mEvs[*ei].mAzCount) - 1, &intVal)) + return 0; + *ai = static_cast<uint>(intVal); + return 1; +} + +// Match the source format from a given identifier. +static SourceFormatT MatchSourceFormat(const char *ident) +{ + if(al::strcasecmp(ident, "ascii") == 0) + return SF_ASCII; + if(al::strcasecmp(ident, "bin_le") == 0) + return SF_BIN_LE; + if(al::strcasecmp(ident, "bin_be") == 0) + return SF_BIN_BE; + if(al::strcasecmp(ident, "wave") == 0) + return SF_WAVE; + if(al::strcasecmp(ident, "sofa") == 0) + return SF_SOFA; + return SF_NONE; +} + +// Match the source element type from a given identifier. +static ElementTypeT MatchElementType(const char *ident) +{ + if(al::strcasecmp(ident, "int") == 0) + return ET_INT; + if(al::strcasecmp(ident, "fp") == 0) + return ET_FP; + return ET_NONE; +} + +// Parse and validate a source reference from the data set definition. +static int ReadSourceRef(TokenReaderT *tr, SourceRefT *src) +{ + char ident[MAX_IDENT_LEN+1]; + uint line, col; + double fpVal; + int intVal; + + TrIndication(tr, &line, &col); + if(!TrReadIdent(tr, MAX_IDENT_LEN, ident)) + return 0; + src->mFormat = MatchSourceFormat(ident); + if(src->mFormat == SF_NONE) + { + TrErrorAt(tr, line, col, "Expected a source format.\n"); + return 0; + } + if(!TrReadOperator(tr, "(")) + return 0; + if(src->mFormat == SF_SOFA) + { + if(!TrReadFloat(tr, MIN_DISTANCE, MAX_DISTANCE, &fpVal)) + return 0; + src->mRadius = fpVal; + if(!TrReadOperator(tr, ",")) + return 0; + if(!TrReadFloat(tr, -90.0, 90.0, &fpVal)) + return 0; + src->mElevation = fpVal; + if(!TrReadOperator(tr, ",")) + return 0; + if(!TrReadFloat(tr, -360.0, 360.0, &fpVal)) + return 0; + src->mAzimuth = fpVal; + if(!TrReadOperator(tr, ":")) + return 0; + if(!TrReadInt(tr, 0, MAX_WAVE_CHANNELS, &intVal)) + return 0; + src->mType = ET_NONE; + src->mSize = 0; + src->mBits = 0; + src->mChannel = static_cast<uint>(intVal); + src->mSkip = 0; + } + else if(src->mFormat == SF_WAVE) + { + if(!TrReadInt(tr, 0, MAX_WAVE_CHANNELS, &intVal)) + return 0; + src->mType = ET_NONE; + src->mSize = 0; + src->mBits = 0; + src->mChannel = static_cast<uint>(intVal); + src->mSkip = 0; + } + else + { + TrIndication(tr, &line, &col); + if(!TrReadIdent(tr, MAX_IDENT_LEN, ident)) + return 0; + src->mType = MatchElementType(ident); + if(src->mType == ET_NONE) + { + TrErrorAt(tr, line, col, "Expected a source element type.\n"); + return 0; + } + if(src->mFormat == SF_BIN_LE || src->mFormat == SF_BIN_BE) + { + if(!TrReadOperator(tr, ",")) + return 0; + if(src->mType == ET_INT) + { + if(!TrReadInt(tr, MIN_BIN_SIZE, MAX_BIN_SIZE, &intVal)) + return 0; + src->mSize = static_cast<uint>(intVal); + if(!TrIsOperator(tr, ",")) + src->mBits = static_cast<int>(8*src->mSize); + else + { + TrReadOperator(tr, ","); + TrIndication(tr, &line, &col); + if(!TrReadInt(tr, -2147483647-1, 2147483647, &intVal)) + return 0; + if(std::abs(intVal) < MIN_BIN_BITS || static_cast<uint>(std::abs(intVal)) > (8*src->mSize)) + { + TrErrorAt(tr, line, col, "Expected a value of (+/-) %d to %d.\n", MIN_BIN_BITS, 8*src->mSize); + return 0; + } + src->mBits = intVal; + } + } + else + { + TrIndication(tr, &line, &col); + if(!TrReadInt(tr, -2147483647-1, 2147483647, &intVal)) + return 0; + if(intVal != 4 && intVal != 8) + { + TrErrorAt(tr, line, col, "Expected a value of 4 or 8.\n"); + return 0; + } + src->mSize = static_cast<uint>(intVal); + src->mBits = 0; + } + } + else if(src->mFormat == SF_ASCII && src->mType == ET_INT) + { + if(!TrReadOperator(tr, ",")) + return 0; + if(!TrReadInt(tr, MIN_ASCII_BITS, MAX_ASCII_BITS, &intVal)) + return 0; + src->mSize = 0; + src->mBits = intVal; + } + else + { + src->mSize = 0; + src->mBits = 0; + } + + if(!TrIsOperator(tr, ";")) + src->mSkip = 0; + else + { + TrReadOperator(tr, ";"); + if(!TrReadInt(tr, 0, 0x7FFFFFFF, &intVal)) + return 0; + src->mSkip = static_cast<uint>(intVal); + } + } + if(!TrReadOperator(tr, ")")) + return 0; + if(TrIsOperator(tr, "@")) + { + TrReadOperator(tr, "@"); + if(!TrReadInt(tr, 0, 0x7FFFFFFF, &intVal)) + return 0; + src->mOffset = static_cast<uint>(intVal); + } + else + src->mOffset = 0; + if(!TrReadOperator(tr, ":")) + return 0; + if(!TrReadString(tr, MAX_PATH_LEN, src->mPath)) + return 0; + return 1; +} + +// Parse and validate a SOFA source reference from the data set definition. +static int ReadSofaRef(TokenReaderT *tr, SourceRefT *src) +{ + char ident[MAX_IDENT_LEN+1]; + uint line, col; + int intVal; + + TrIndication(tr, &line, &col); + if(!TrReadIdent(tr, MAX_IDENT_LEN, ident)) + return 0; + src->mFormat = MatchSourceFormat(ident); + if(src->mFormat != SF_SOFA) + { + TrErrorAt(tr, line, col, "Expected the SOFA source format.\n"); + return 0; + } + + src->mType = ET_NONE; + src->mSize = 0; + src->mBits = 0; + src->mChannel = 0; + src->mSkip = 0; + + if(TrIsOperator(tr, "@")) + { + TrReadOperator(tr, "@"); + if(!TrReadInt(tr, 0, 0x7FFFFFFF, &intVal)) + return 0; + src->mOffset = static_cast<uint>(intVal); + } + else + src->mOffset = 0; + if(!TrReadOperator(tr, ":")) + return 0; + if(!TrReadString(tr, MAX_PATH_LEN, src->mPath)) + return 0; + return 1; +} + +// Match the target ear (index) from a given identifier. +static int MatchTargetEar(const char *ident) +{ + if(al::strcasecmp(ident, "left") == 0) + return 0; + if(al::strcasecmp(ident, "right") == 0) + return 1; + return -1; +} + +// Calculate the onset time of an HRIR and average it with any existing +// timing for its field, elevation, azimuth, and ear. +static double AverageHrirOnset(const uint rate, const uint n, const double *hrir, const double f, const double onset) +{ + std::vector<double> upsampled(10 * n); + { + ResamplerT rs; + ResamplerSetup(&rs, rate, 10 * rate); + ResamplerRun(&rs, n, hrir, 10 * n, upsampled.data()); + } + + double mag{0.0}; + for(uint i{0u};i < 10*n;i++) + mag = std::max(std::abs(upsampled[i]), mag); + + mag *= 0.15; + uint i{0u}; + for(;i < 10*n;i++) + { + if(std::abs(upsampled[i]) >= mag) + break; + } + return Lerp(onset, static_cast<double>(i) / (10*rate), f); +} + +// Calculate the magnitude response of an HRIR and average it with any +// existing responses for its field, elevation, azimuth, and ear. +static void AverageHrirMagnitude(const uint points, const uint n, const double *hrir, const double f, double *mag) +{ + uint m = 1 + (n / 2), i; + std::vector<complex_d> h(n); + std::vector<double> r(n); + + for(i = 0;i < points;i++) + h[i] = complex_d{hrir[i], 0.0}; + for(;i < n;i++) + h[i] = complex_d{0.0, 0.0}; + FftForward(n, h.data()); + MagnitudeResponse(n, h.data(), r.data()); + for(i = 0;i < m;i++) + mag[i] = Lerp(mag[i], r[i], f); +} + +// Process the list of sources in the data set definition. +static int ProcessSources(TokenReaderT *tr, HrirDataT *hData) +{ + uint channels = (hData->mChannelType == CT_STEREO) ? 2 : 1; + hData->mHrirsBase.resize(channels * hData->mIrCount * hData->mIrSize); + double *hrirs = hData->mHrirsBase.data(); + std::vector<double> hrir(hData->mIrPoints); + uint line, col, fi, ei, ai; + int count; + + printf("Loading sources..."); + fflush(stdout); + count = 0; + while(TrIsOperator(tr, "[")) + { + double factor[2]{ 1.0, 1.0 }; + + TrIndication(tr, &line, &col); + TrReadOperator(tr, "["); + + if(TrIsOperator(tr, "*")) + { + SourceRefT src; + struct MYSOFA_EASY *sofa; + uint si; + + TrReadOperator(tr, "*"); + if(!TrReadOperator(tr, "]") || !TrReadOperator(tr, "=")) + return 0; + + TrIndication(tr, &line, &col); + if(!ReadSofaRef(tr, &src)) + return 0; + + if(hData->mChannelType == CT_STEREO) + { + char type[MAX_IDENT_LEN+1]; + ChannelTypeT channelType; + + if(!TrReadIdent(tr, MAX_IDENT_LEN, type)) + return 0; + + channelType = MatchChannelType(type); + + switch(channelType) + { + case CT_NONE: + TrErrorAt(tr, line, col, "Expected a channel type.\n"); + return 0; + case CT_MONO: + src.mChannel = 0; + break; + case CT_STEREO: + src.mChannel = 1; + break; + } + } + else + { + char type[MAX_IDENT_LEN+1]; + ChannelTypeT channelType; + + if(!TrReadIdent(tr, MAX_IDENT_LEN, type)) + return 0; + + channelType = MatchChannelType(type); + if(channelType != CT_MONO) + { + TrErrorAt(tr, line, col, "Expected a mono channel type.\n"); + return 0; + } + src.mChannel = 0; + } + + sofa = LoadSofaFile(&src, hData->mIrRate, hData->mIrPoints); + if(!sofa) return 0; + + for(si = 0;si < sofa->hrtf->M;si++) + { + printf("\rLoading sources... %d of %d", si+1, sofa->hrtf->M); + fflush(stdout); + + float aer[3] = { + sofa->hrtf->SourcePosition.values[3*si], + sofa->hrtf->SourcePosition.values[3*si + 1], + sofa->hrtf->SourcePosition.values[3*si + 2] + }; + mysofa_c2s(aer); + + if(std::fabs(aer[1]) >= 89.999f) + aer[0] = 0.0f; + else + aer[0] = std::fmod(360.0f - aer[0], 360.0f); + + for(fi = 0;fi < hData->mFdCount;fi++) + { + double delta = aer[2] - hData->mFds[fi].mDistance; + if(std::abs(delta) < 0.001) break; + } + if(fi >= hData->mFdCount) + continue; + + double ef{(90.0 + aer[1]) / 180.0 * (hData->mFds[fi].mEvCount - 1)}; + ei = static_cast<uint>(std::round(ef)); + ef = (ef - ei) * 180.0 / (hData->mFds[fi].mEvCount - 1); + if(std::abs(ef) >= 0.1) + continue; + + double af{aer[0] / 360.0 * hData->mFds[fi].mEvs[ei].mAzCount}; + ai = static_cast<uint>(std::round(af)); + af = (af - ai) * 360.0 / hData->mFds[fi].mEvs[ei].mAzCount; + ai = ai % hData->mFds[fi].mEvs[ei].mAzCount; + if(std::abs(af) >= 0.1) + continue; + + HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; + if(azd->mIrs[0] != nullptr) + { + TrErrorAt(tr, line, col, "Redefinition of source [ %d, %d, %d ].\n", fi, ei, ai); + return 0; + } + + ExtractSofaHrir(sofa, si, 0, src.mOffset, hData->mIrPoints, hrir.data()); + azd->mIrs[0] = &hrirs[hData->mIrSize * azd->mIndex]; + azd->mDelays[0] = AverageHrirOnset(hData->mIrRate, hData->mIrPoints, hrir.data(), 1.0, azd->mDelays[0]); + AverageHrirMagnitude(hData->mIrPoints, hData->mFftSize, hrir.data(), 1.0, azd->mIrs[0]); + + if(src.mChannel == 1) + { + ExtractSofaHrir(sofa, si, 1, src.mOffset, hData->mIrPoints, hrir.data()); + azd->mIrs[1] = &hrirs[hData->mIrSize * (hData->mIrCount + azd->mIndex)]; + azd->mDelays[1] = AverageHrirOnset(hData->mIrRate, hData->mIrPoints, hrir.data(), 1.0, azd->mDelays[1]); + AverageHrirMagnitude(hData->mIrPoints, hData->mFftSize, hrir.data(), 1.0, azd->mIrs[1]); + } + + // TODO: Since some SOFA files contain minimum phase HRIRs, + // it would be beneficial to check for per-measurement delays + // (when available) to reconstruct the HRTDs. + } + + continue; + } + + if(!ReadIndexTriplet(tr, hData, &fi, &ei, &ai)) + return 0; + if(!TrReadOperator(tr, "]")) + return 0; + HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; + + if(azd->mIrs[0] != nullptr) + { + TrErrorAt(tr, line, col, "Redefinition of source.\n"); + return 0; + } + if(!TrReadOperator(tr, "=")) + return 0; + + for(;;) + { + SourceRefT src; + + if(!ReadSourceRef(tr, &src)) + return 0; + + // TODO: Would be nice to display 'x of y files', but that would + // require preparing the source refs first to get a total count + // before loading them. + ++count; + printf("\rLoading sources... %d file%s", count, (count==1)?"":"s"); + fflush(stdout); + + if(!LoadSource(&src, hData->mIrRate, hData->mIrPoints, hrir.data())) + return 0; + + uint ti{0}; + if(hData->mChannelType == CT_STEREO) + { + char ident[MAX_IDENT_LEN+1]; + + if(!TrReadIdent(tr, MAX_IDENT_LEN, ident)) + return 0; + ti = static_cast<uint>(MatchTargetEar(ident)); + if(static_cast<int>(ti) < 0) + { + TrErrorAt(tr, line, col, "Expected a target ear.\n"); + return 0; + } + } + azd->mIrs[ti] = &hrirs[hData->mIrSize * (ti * hData->mIrCount + azd->mIndex)]; + azd->mDelays[ti] = AverageHrirOnset(hData->mIrRate, hData->mIrPoints, hrir.data(), 1.0 / factor[ti], azd->mDelays[ti]); + AverageHrirMagnitude(hData->mIrPoints, hData->mFftSize, hrir.data(), 1.0 / factor[ti], azd->mIrs[ti]); + factor[ti] += 1.0; + if(!TrIsOperator(tr, "+")) + break; + TrReadOperator(tr, "+"); + } + if(hData->mChannelType == CT_STEREO) + { + if(azd->mIrs[0] == nullptr) + { + TrErrorAt(tr, line, col, "Missing left ear source reference(s).\n"); + return 0; + } + else if(azd->mIrs[1] == nullptr) + { + TrErrorAt(tr, line, col, "Missing right ear source reference(s).\n"); + return 0; + } + } + } + printf("\n"); + for(fi = 0;fi < hData->mFdCount;fi++) + { + for(ei = 0;ei < hData->mFds[fi].mEvCount;ei++) + { + for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) + { + HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; + if(azd->mIrs[0] != nullptr) + break; + } + if(ai < hData->mFds[fi].mEvs[ei].mAzCount) + break; + } + if(ei >= hData->mFds[fi].mEvCount) + { + TrError(tr, "Missing source references [ %d, *, * ].\n", fi); + return 0; + } + hData->mFds[fi].mEvStart = ei; + for(;ei < hData->mFds[fi].mEvCount;ei++) + { + for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) + { + HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; + + if(azd->mIrs[0] == nullptr) + { + TrError(tr, "Missing source reference [ %d, %d, %d ].\n", fi, ei, ai); + return 0; + } + } + } + } + for(uint ti{0};ti < channels;ti++) + { + for(fi = 0;fi < hData->mFdCount;fi++) + { + for(ei = 0;ei < hData->mFds[fi].mEvCount;ei++) + { + for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) + { + HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; + + azd->mIrs[ti] = &hrirs[hData->mIrSize * (ti * hData->mIrCount + azd->mIndex)]; + } + } + } + } + if(!TrLoad(tr)) + { + mysofa_cache_release_all(); + return 1; + } + + TrError(tr, "Errant data at end of source list.\n"); + mysofa_cache_release_all(); + return 0; +} + + +bool LoadDefInput(std::istream &istream, const char *startbytes, std::streamsize startbytecount, + const char *filename, const uint fftSize, const uint truncSize, const ChannelModeT chanMode, + HrirDataT *hData) +{ + TokenReaderT tr{istream}; + + TrSetup(startbytes, startbytecount, filename, &tr); + if(!ProcessMetrics(&tr, fftSize, truncSize, chanMode, hData) + || !ProcessSources(&tr, hData)) + return false; + + return true; +} diff --git a/utils/makemhr/loaddef.h b/utils/makemhr/loaddef.h new file mode 100644 index 00000000..34fbb832 --- /dev/null +++ b/utils/makemhr/loaddef.h @@ -0,0 +1,13 @@ +#ifndef LOADDEF_H +#define LOADDEF_H + +#include <istream> + +#include "makemhr.h" + + +bool LoadDefInput(std::istream &istream, const char *startbytes, std::streamsize startbytecount, + const char *filename, const uint fftSize, const uint truncSize, const ChannelModeT chanMode, + HrirDataT *hData); + +#endif /* LOADDEF_H */ diff --git a/utils/makemhr/loadsofa.cpp b/utils/makemhr/loadsofa.cpp new file mode 100644 index 00000000..c91613c8 --- /dev/null +++ b/utils/makemhr/loadsofa.cpp @@ -0,0 +1,668 @@ +/* + * HRTF utility for producing and demonstrating the process of creating an + * OpenAL Soft compatible HRIR data set. + * + * Copyright (C) 2018-2019 Christopher Fitzgerald + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Or visit: http://www.gnu.org/licenses/old-licenses/gpl-2.0.html + */ + +#include "loadsofa.h" + +#include <algorithm> +#include <array> +#include <cmath> +#include <cstdio> +#include <iterator> +#include <memory> +#include <numeric> +#include <string> +#include <vector> + +#include "makemhr.h" + +#include "mysofa.h" + + +using double3 = std::array<double,3>; + +static const char *SofaErrorStr(int err) +{ + switch(err) + { + case MYSOFA_OK: return "OK"; + case MYSOFA_INVALID_FORMAT: return "Invalid format"; + case MYSOFA_UNSUPPORTED_FORMAT: return "Unsupported format"; + case MYSOFA_INTERNAL_ERROR: return "Internal error"; + case MYSOFA_NO_MEMORY: return "Out of memory"; + case MYSOFA_READ_ERROR: return "Read error"; + } + return "Unknown"; +} + + +/* Produces a sorted array of unique elements from a particular axis of the + * triplets array. The filters are used to focus on particular coordinates + * of other axes as necessary. The epsilons are used to constrain the + * equality of unique elements. + */ +static uint GetUniquelySortedElems(const uint m, const double3 *aers, const uint axis, + const double *const (&filters)[3], const double (&epsilons)[3], double *elems) +{ + uint count{0u}; + for(uint i{0u};i < m;++i) + { + const double elem{aers[i][axis]}; + + uint j; + for(j = 0;j < 3;j++) + { + if(filters[j] && std::fabs(aers[i][j] - *filters[j]) > epsilons[j]) + break; + } + if(j < 3) + continue; + + for(j = 0;j < count;j++) + { + const double delta{elem - elems[j]}; + + if(delta > epsilons[axis]) + continue; + + if(delta >= -epsilons[axis]) + break; + + for(uint k{count};k > j;k--) + elems[k] = elems[k - 1]; + + elems[j] = elem; + count++; + break; + } + + if(j >= count) + elems[count++] = elem; + } + + return count; +} + +/* Given a list of elements, this will produce the smallest step size that + * can uniformly cover a fair portion of the list. Ideally this will be over + * half, but in degenerate cases this can fall to a minimum of 5 (the lower + * limit on elevations necessary to build a layout). + */ +static double GetUniformStepSize(const double epsilon, const uint m, const double *elems) +{ + auto steps = std::vector<double>(m, 0.0); + auto counts = std::vector<uint>(m, 0u); + uint count{0u}; + + for(uint stride{1u};stride < m/2;stride++) + { + for(uint i{0u};i < m-stride;i++) + { + const double step{elems[i + stride] - elems[i]}; + + uint j; + for(j = 0;j < count;j++) + { + if(std::fabs(step - steps[j]) < epsilon) + { + counts[j]++; + break; + } + } + + if(j >= count) + { + steps[j] = step; + counts[j] = 1; + count++; + } + } + + for(uint i{1u};i < count;i++) + { + if(counts[i] > counts[0]) + { + steps[0] = steps[i]; + counts[0] = counts[i]; + } + } + + count = 1; + + if(counts[0] > m/2) + break; + } + + if(counts[0] > 255) + { + uint i{2u}; + while(counts[0]/i > 255 && (counts[0]%i) != 0) + ++i; + counts[0] /= i; + steps[0] *= i; + } + if(counts[0] > 5) + return steps[0]; + return 0.0; +} + +/* Attempts to produce a compatible layout. Most data sets tend to be + * uniform and have the same major axis as used by OpenAL Soft's HRTF model. + * This will remove outliers and produce a maximally dense layout when + * possible. Those sets that contain purely random measurements or use + * different major axes will fail. + */ +static bool PrepareLayout(const uint m, const float *xyzs, HrirDataT *hData) +{ + auto aers = std::vector<double3>(m, double3{}); + auto elems = std::vector<double>(m, 0.0); + + for(uint i{0u};i < m;++i) + { + float aer[3]{xyzs[i*3], xyzs[i*3 + 1], xyzs[i*3 + 2]}; + mysofa_c2s(&aer[0]); + aers[i][0] = aer[0]; + aers[i][1] = aer[1]; + aers[i][2] = aer[2]; + } + + const uint fdCount{GetUniquelySortedElems(m, aers.data(), 2, { nullptr, nullptr, nullptr }, + { 0.1, 0.1, 0.001 }, elems.data())}; + if(fdCount > MAX_FD_COUNT) + { + fprintf(stdout, "Incompatible layout (inumerable radii).\n"); + return false; + } + + double distances[MAX_FD_COUNT]{}; + uint evCounts[MAX_FD_COUNT]{}; + auto azCounts = std::vector<uint>(MAX_FD_COUNT*MAX_EV_COUNT, 0u); + for(uint fi{0u};fi < fdCount;fi++) + { + distances[fi] = elems[fi]; + if(fi > 0 && distances[fi] <= distances[fi-1]) + { + fprintf(stderr, "Distances must increase.\n"); + return 0; + } + } + if(distances[0] < hData->mRadius) + { + fprintf(stderr, "Distance cannot start below head radius.\n"); + return 0; + } + + for(uint fi{0u};fi < fdCount;fi++) + { + const double dist{distances[fi]}; + uint evCount{GetUniquelySortedElems(m, aers.data(), 1, { nullptr, nullptr, &dist }, + { 0.1, 0.1, 0.001 }, elems.data())}; + + if(evCount > MAX_EV_COUNT) + { + fprintf(stderr, "Incompatible layout (innumerable elevations).\n"); + return false; + } + + double step{GetUniformStepSize(0.1, evCount, elems.data())}; + if(step <= 0.0) + { + fprintf(stderr, "Incompatible layout (non-uniform elevations).\n"); + return false; + } + + uint evStart{0u}; + for(uint ei{0u};ei < evCount;ei++) + { + double ev{90.0 + elems[ei]}; + double eif{std::round(ev / step)}; + const uint ei_start{static_cast<uint>(eif)}; + + if(std::fabs(eif - static_cast<double>(ei_start)) < (0.1/step)) + { + evStart = ei_start; + break; + } + } + + evCount = static_cast<uint>(std::round(180.0 / step)) + 1; + if(evCount < 5) + { + fprintf(stderr, "Incompatible layout (too few uniform elevations).\n"); + return false; + } + + evCounts[fi] = evCount; + + for(uint ei{evStart};ei < evCount;ei++) + { + const double ev{-90.0 + ei*180.0/(evCount - 1)}; + const uint azCount{GetUniquelySortedElems(m, aers.data(), 0, { nullptr, &ev, &dist }, + { 0.1, 0.1, 0.001 }, elems.data())}; + + if(ei > 0 && ei < (evCount - 1)) + { + step = GetUniformStepSize(0.1, azCount, elems.data()); + if(step <= 0.0) + { + fprintf(stderr, "Incompatible layout (non-uniform azimuths).\n"); + return false; + } + + azCounts[fi*MAX_EV_COUNT + ei] = static_cast<uint>(std::round(360.0 / step)); + if(azCounts[fi*MAX_EV_COUNT + ei] > MAX_AZ_COUNT) + { + fprintf(stderr, + "Incompatible layout (too many azimuths on elev=%f, rad=%f, %u > %u).\n", + ev, dist, azCounts[fi*MAX_EV_COUNT + ei], MAX_AZ_COUNT); + return false; + } + } + else if(azCount != 1) + { + fprintf(stderr, "Incompatible layout (non-singular poles).\n"); + return false; + } + else + { + azCounts[fi*MAX_EV_COUNT + ei] = 1; + } + } + + for(uint ei{0u};ei < evStart;ei++) + azCounts[fi*MAX_EV_COUNT + ei] = azCounts[fi*MAX_EV_COUNT + evCount - ei - 1]; + } + return PrepareHrirData(fdCount, distances, evCounts, azCounts.data(), hData) != 0; +} + + +bool PrepareSampleRate(MYSOFA_HRTF *sofaHrtf, HrirDataT *hData) +{ + const char *srate_dim{nullptr}; + const char *srate_units{nullptr}; + MYSOFA_ARRAY *srate_array{&sofaHrtf->DataSamplingRate}; + MYSOFA_ATTRIBUTE *srate_attrs{srate_array->attributes}; + while(srate_attrs) + { + if(std::string{"DIMENSION_LIST"} == srate_attrs->name) + { + if(srate_dim) + { + fprintf(stderr, "Duplicate SampleRate.DIMENSION_LIST\n"); + return false; + } + srate_dim = srate_attrs->value; + } + else if(std::string{"Units"} == srate_attrs->name) + { + if(srate_units) + { + fprintf(stderr, "Duplicate SampleRate.Units\n"); + return false; + } + srate_units = srate_attrs->value; + } + else + fprintf(stderr, "Unexpected sample rate attribute: %s = %s\n", srate_attrs->name, + srate_attrs->value); + srate_attrs = srate_attrs->next; + } + if(!srate_dim) + { + fprintf(stderr, "Missing sample rate dimensions\n"); + return false; + } + if(srate_dim != std::string{"I"}) + { + fprintf(stderr, "Unsupported sample rate dimensions: %s\n", srate_dim); + return false; + } + if(!srate_units) + { + fprintf(stderr, "Missing sample rate unit type\n"); + return false; + } + if(srate_units != std::string{"hertz"}) + { + fprintf(stderr, "Unsupported sample rate unit type: %s\n", srate_units); + return false; + } + /* I dimensions guarantees 1 element, so just extract it. */ + hData->mIrRate = static_cast<uint>(srate_array->values[0] + 0.5f); + if(hData->mIrRate < MIN_RATE || hData->mIrRate > MAX_RATE) + { + fprintf(stderr, "Sample rate out of range: %u (expected %u to %u)", hData->mIrRate, + MIN_RATE, MAX_RATE); + return false; + } + return true; +} + +bool PrepareDelay(MYSOFA_HRTF *sofaHrtf, HrirDataT *hData) +{ + const char *delay_dim{nullptr}; + MYSOFA_ARRAY *delay_array{&sofaHrtf->DataDelay}; + MYSOFA_ATTRIBUTE *delay_attrs{delay_array->attributes}; + while(delay_attrs) + { + if(std::string{"DIMENSION_LIST"} == delay_attrs->name) + { + if(delay_dim) + { + fprintf(stderr, "Duplicate Delay.DIMENSION_LIST\n"); + return false; + } + delay_dim = delay_attrs->value; + } + else + fprintf(stderr, "Unexpected delay attribute: %s = %s\n", delay_attrs->name, + delay_attrs->value); + delay_attrs = delay_attrs->next; + } + if(!delay_dim) + { + fprintf(stderr, "Missing delay dimensions\n"); + /*return false;*/ + } + else if(delay_dim != std::string{"I,R"}) + { + fprintf(stderr, "Unsupported delay dimensions: %s\n", delay_dim); + return false; + } + else if(hData->mChannelType == CT_STEREO) + { + /* I,R is 1xChannelCount. Makemhr currently removes any delay constant, + * so we can ignore this as long as it's equal. + */ + if(delay_array->values[0] != delay_array->values[1]) + { + fprintf(stderr, "Mismatched delays not supported: %f, %f\n", delay_array->values[0], + delay_array->values[1]); + return false; + } + } + return true; +} + +bool CheckIrData(MYSOFA_HRTF *sofaHrtf) +{ + const char *ir_dim{nullptr}; + MYSOFA_ARRAY *ir_array{&sofaHrtf->DataIR}; + MYSOFA_ATTRIBUTE *ir_attrs{ir_array->attributes}; + while(ir_attrs) + { + if(std::string{"DIMENSION_LIST"} == ir_attrs->name) + { + if(ir_dim) + { + fprintf(stderr, "Duplicate IR.DIMENSION_LIST\n"); + return false; + } + ir_dim = ir_attrs->value; + } + else + fprintf(stderr, "Unexpected IR attribute: %s = %s\n", ir_attrs->name, + ir_attrs->value); + ir_attrs = ir_attrs->next; + } + if(!ir_dim) + { + fprintf(stderr, "Missing IR dimensions\n"); + return false; + } + if(ir_dim != std::string{"M,R,N"}) + { + fprintf(stderr, "Unsupported IR dimensions: %s\n", ir_dim); + return false; + } + return true; +} + + +/* Calculate the onset time of a HRIR. */ +static double CalcHrirOnset(const uint rate, const uint n, std::vector<double> &upsampled, + const double *hrir) +{ + { + ResamplerT rs; + ResamplerSetup(&rs, rate, 10 * rate); + ResamplerRun(&rs, n, hrir, 10 * n, upsampled.data()); + } + + double mag{std::accumulate(upsampled.cbegin(), upsampled.cend(), double{0.0}, + [](const double magnitude, const double sample) -> double + { return std::max(magnitude, std::abs(sample)); })}; + + mag *= 0.15; + auto iter = std::find_if(upsampled.cbegin(), upsampled.cend(), + [mag](const double sample) -> bool { return (std::abs(sample) >= mag); }); + return static_cast<double>(std::distance(upsampled.cbegin(), iter)) / (10.0*rate); +} + +/* Calculate the magnitude response of a HRIR. */ +static void CalcHrirMagnitude(const uint points, const uint n, std::vector<complex_d> &h, + const double *hrir, double *mag) +{ + auto iter = std::copy_n(hrir, points, h.begin()); + std::fill(iter, h.end(), complex_d{0.0, 0.0}); + + FftForward(n, h.data()); + MagnitudeResponse(n, h.data(), mag); +} + +static bool LoadResponses(MYSOFA_HRTF *sofaHrtf, HrirDataT *hData) +{ + const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u}; + hData->mHrirsBase.resize(channels * hData->mIrCount * hData->mIrSize); + double *hrirs = hData->mHrirsBase.data(); + + /* Temporary buffers used to calculate the IR's onset and frequency + * magnitudes. + */ + auto upsampled = std::vector<double>(10 * hData->mIrPoints); + auto htemp = std::vector<complex_d>(hData->mFftSize); + auto hrir = std::vector<double>(hData->mFftSize); + + for(uint si{0u};si < sofaHrtf->M;si++) + { + printf("\rLoading HRIRs... %d of %d", si+1, sofaHrtf->M); + fflush(stdout); + + float aer[3]{ + sofaHrtf->SourcePosition.values[3*si], + sofaHrtf->SourcePosition.values[3*si + 1], + sofaHrtf->SourcePosition.values[3*si + 2] + }; + mysofa_c2s(aer); + + if(std::abs(aer[1]) >= 89.999f) + aer[0] = 0.0f; + else + aer[0] = std::fmod(360.0f - aer[0], 360.0f); + + auto field = std::find_if(hData->mFds.cbegin(), hData->mFds.cend(), + [&aer](const HrirFdT &fld) -> bool + { + double delta = aer[2] - fld.mDistance; + return (std::abs(delta) < 0.001); + }); + if(field == hData->mFds.cend()) + continue; + + double ef{(90.0+aer[1]) / 180.0 * (field->mEvCount-1)}; + auto ei = static_cast<int>(std::round(ef)); + ef = (ef-ei) * 180.0 / (field->mEvCount-1); + if(std::abs(ef) >= 0.1) continue; + + double af{aer[0] / 360.0 * field->mEvs[ei].mAzCount}; + auto ai = static_cast<int>(std::round(af)); + af = (af-ai) * 360.0 / field->mEvs[ei].mAzCount; + ai %= field->mEvs[ei].mAzCount; + if(std::abs(af) >= 0.1) continue; + + HrirAzT *azd = &field->mEvs[ei].mAzs[ai]; + if(azd->mIrs[0] != nullptr) + { + fprintf(stderr, "Multiple measurements near [ a=%f, e=%f, r=%f ].\n", + aer[0], aer[1], aer[2]); + return false; + } + + for(uint ti{0u};ti < channels;++ti) + { + std::copy_n(&sofaHrtf->DataIR.values[(si*sofaHrtf->R + ti)*sofaHrtf->N], + hData->mIrPoints, hrir.begin()); + azd->mIrs[ti] = &hrirs[hData->mIrSize * (hData->mIrCount*ti + azd->mIndex)]; + azd->mDelays[ti] = CalcHrirOnset(hData->mIrRate, hData->mIrPoints, upsampled, + hrir.data()); + CalcHrirMagnitude(hData->mIrPoints, hData->mFftSize, htemp, hrir.data(), + azd->mIrs[ti]); + } + + // TODO: Since some SOFA files contain minimum phase HRIRs, + // it would be beneficial to check for per-measurement delays + // (when available) to reconstruct the HRTDs. + } + printf("\n"); + return true; +} + +struct MySofaHrtfDeleter { + void operator()(MYSOFA_HRTF *ptr) { mysofa_free(ptr); } +}; +using MySofaHrtfPtr = std::unique_ptr<MYSOFA_HRTF,MySofaHrtfDeleter>; + +bool LoadSofaFile(const char *filename, const uint fftSize, const uint truncSize, + const ChannelModeT chanMode, HrirDataT *hData) +{ + int err; + MySofaHrtfPtr sofaHrtf{mysofa_load(filename, &err)}; + if(!sofaHrtf) + { + fprintf(stdout, "Error: Could not load %s: %s\n", filename, SofaErrorStr(err)); + return false; + } + + /* NOTE: Some valid SOFA files are failing this check. */ + err = mysofa_check(sofaHrtf.get()); + if(err != MYSOFA_OK) + fprintf(stderr, "Warning: Supposedly malformed source file '%s' (%s).\n", filename, + SofaErrorStr(err)); + + mysofa_tocartesian(sofaHrtf.get()); + + /* Make sure emitter and receiver counts are sane. */ + if(sofaHrtf->E != 1) + { + fprintf(stderr, "%u emitters not supported\n", sofaHrtf->E); + return false; + } + if(sofaHrtf->R > 2 || sofaHrtf->R < 1) + { + fprintf(stderr, "%u receivers not supported\n", sofaHrtf->R); + return false; + } + /* Assume R=2 is a stereo measurement, and R=1 is mono left-ear-only. */ + if(sofaHrtf->R == 2 && chanMode == CM_AllowStereo) + hData->mChannelType = CT_STEREO; + else + hData->mChannelType = CT_MONO; + + /* Check and set the FFT and IR size. */ + if(sofaHrtf->N > fftSize) + { + fprintf(stderr, "Sample points exceeds the FFT size.\n"); + return false; + } + if(sofaHrtf->N < truncSize) + { + fprintf(stderr, "Sample points is below the truncation size.\n"); + return false; + } + hData->mIrPoints = sofaHrtf->N; + hData->mFftSize = fftSize; + hData->mIrSize = std::max(1u + (fftSize/2u), sofaHrtf->N); + + /* Assume a default head radius of 9cm. */ + hData->mRadius = 0.09; + + if(!PrepareSampleRate(sofaHrtf.get(), hData) || !PrepareDelay(sofaHrtf.get(), hData) + || !CheckIrData(sofaHrtf.get())) + return false; + if(!PrepareLayout(sofaHrtf->M, sofaHrtf->SourcePosition.values, hData)) + return false; + + if(!LoadResponses(sofaHrtf.get(), hData)) + return false; + sofaHrtf = nullptr; + + for(uint fi{0u};fi < hData->mFdCount;fi++) + { + uint ei{0u}; + for(;ei < hData->mFds[fi].mEvCount;ei++) + { + uint ai{0u}; + for(;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) + { + HrirAzT &azd = hData->mFds[fi].mEvs[ei].mAzs[ai]; + if(azd.mIrs[0] != nullptr) break; + } + if(ai < hData->mFds[fi].mEvs[ei].mAzCount) + break; + } + if(ei >= hData->mFds[fi].mEvCount) + { + fprintf(stderr, "Missing source references [ %d, *, * ].\n", fi); + return false; + } + hData->mFds[fi].mEvStart = ei; + for(;ei < hData->mFds[fi].mEvCount;ei++) + { + for(uint ai{0u};ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) + { + HrirAzT &azd = hData->mFds[fi].mEvs[ei].mAzs[ai]; + if(azd.mIrs[0] == nullptr) + { + fprintf(stderr, "Missing source reference [ %d, %d, %d ].\n", fi, ei, ai); + return false; + } + } + } + } + + const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u}; + double *hrirs = hData->mHrirsBase.data(); + for(uint fi{0u};fi < hData->mFdCount;fi++) + { + for(uint ei{0u};ei < hData->mFds[fi].mEvCount;ei++) + { + for(uint ai{0u};ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) + { + HrirAzT &azd = hData->mFds[fi].mEvs[ei].mAzs[ai]; + for(uint ti{0u};ti < channels;ti++) + azd.mIrs[ti] = &hrirs[hData->mIrSize * (hData->mIrCount*ti + azd.mIndex)]; + } + } + } + + return true; +} diff --git a/utils/makemhr/loadsofa.h b/utils/makemhr/loadsofa.h new file mode 100644 index 00000000..93bf1704 --- /dev/null +++ b/utils/makemhr/loadsofa.h @@ -0,0 +1,10 @@ +#ifndef LOADSOFA_H +#define LOADSOFA_H + +#include "makemhr.h" + + +bool LoadSofaFile(const char *filename, const uint fftSize, const uint truncSize, + const ChannelModeT chanMode, HrirDataT *hData); + +#endif /* LOADSOFA_H */ diff --git a/utils/makemhr/makemhr.cpp b/utils/makemhr/makemhr.cpp new file mode 100644 index 00000000..1e28ca4b --- /dev/null +++ b/utils/makemhr/makemhr.cpp @@ -0,0 +1,1797 @@ +/* + * HRTF utility for producing and demonstrating the process of creating an + * OpenAL Soft compatible HRIR data set. + * + * Copyright (C) 2011-2019 Christopher Fitzgerald + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Or visit: http://www.gnu.org/licenses/old-licenses/gpl-2.0.html + * + * -------------------------------------------------------------------------- + * + * A big thanks goes out to all those whose work done in the field of + * binaural sound synthesis using measured HRTFs makes this utility and the + * OpenAL Soft implementation possible. + * + * The algorithm for diffuse-field equalization was adapted from the work + * done by Rio Emmanuel and Larcher Veronique of IRCAM and Bill Gardner of + * MIT Media Laboratory. It operates as follows: + * + * 1. Take the FFT of each HRIR and only keep the magnitude responses. + * 2. Calculate the diffuse-field power-average of all HRIRs weighted by + * their contribution to the total surface area covered by their + * measurement. This has since been modified to use coverage volume for + * multi-field HRIR data sets. + * 3. Take the diffuse-field average and limit its magnitude range. + * 4. Equalize the responses by using the inverse of the diffuse-field + * average. + * 5. Reconstruct the minimum-phase responses. + * 5. Zero the DC component. + * 6. IFFT the result and truncate to the desired-length minimum-phase FIR. + * + * The spherical head algorithm for calculating propagation delay was adapted + * from the paper: + * + * Modeling Interaural Time Difference Assuming a Spherical Head + * Joel David Miller + * Music 150, Musical Acoustics, Stanford University + * December 2, 2001 + * + * The formulae for calculating the Kaiser window metrics are from the + * the textbook: + * + * Discrete-Time Signal Processing + * Alan V. Oppenheim and Ronald W. Schafer + * Prentice-Hall Signal Processing Series + * 1999 + */ + +#define _UNICODE +#include "config.h" + +#include "makemhr.h" + +#include <algorithm> +#include <atomic> +#include <chrono> +#include <cmath> +#include <complex> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <functional> +#include <iostream> +#include <limits> +#include <memory> +#include <numeric> +#include <thread> +#include <utility> +#include <vector> + +#ifdef HAVE_GETOPT +#include <unistd.h> +#else +#include "../getopt.h" +#endif + +#include "alfstream.h" +#include "alstring.h" +#include "loaddef.h" +#include "loadsofa.h" + +#include "win_main_utf8.h" + + +namespace { + +using namespace std::placeholders; + +} // namespace + +#ifndef M_PI +#define M_PI (3.14159265358979323846) +#endif + + +// Head model used for calculating the impulse delays. +enum HeadModelT { + HM_NONE, + HM_DATASET, // Measure the onset from the dataset. + HM_SPHERE // Calculate the onset using a spherical head model. +}; + + +// The epsilon used to maintain signal stability. +#define EPSILON (1e-9) + +// The limits to the FFT window size override on the command line. +#define MIN_FFTSIZE (65536) +#define MAX_FFTSIZE (131072) + +// The limits to the equalization range limit on the command line. +#define MIN_LIMIT (2.0) +#define MAX_LIMIT (120.0) + +// The limits to the truncation window size on the command line. +#define MIN_TRUNCSIZE (16) +#define MAX_TRUNCSIZE (512) + +// The limits to the custom head radius on the command line. +#define MIN_CUSTOM_RADIUS (0.05) +#define MAX_CUSTOM_RADIUS (0.15) + +// The truncation window size must be a multiple of the below value to allow +// for vectorized convolution. +#define MOD_TRUNCSIZE (8) + +// The defaults for the command line options. +#define DEFAULT_FFTSIZE (65536) +#define DEFAULT_EQUALIZE (1) +#define DEFAULT_SURFACE (1) +#define DEFAULT_LIMIT (24.0) +#define DEFAULT_TRUNCSIZE (32) +#define DEFAULT_HEAD_MODEL (HM_DATASET) +#define DEFAULT_CUSTOM_RADIUS (0.0) + +// The maximum propagation delay value supported by OpenAL Soft. +#define MAX_HRTD (63.0) + +// The OpenAL Soft HRTF format marker. It stands for minimum-phase head +// response protocol 02. +#define MHR_FORMAT ("MinPHR02") + +/* Channel index enums. Mono uses LeftChannel only. */ +enum ChannelIndex : uint { + LeftChannel = 0u, + RightChannel = 1u +}; + + +/* Performs a string substitution. Any case-insensitive occurrences of the + * pattern string are replaced with the replacement string. The result is + * truncated if necessary. + */ +static int StrSubst(const char *in, const char *pat, const char *rep, const size_t maxLen, char *out) +{ + size_t inLen, patLen, repLen; + size_t si, di; + int truncated; + + inLen = strlen(in); + patLen = strlen(pat); + repLen = strlen(rep); + si = 0; + di = 0; + truncated = 0; + while(si < inLen && di < maxLen) + { + if(patLen <= inLen-si) + { + if(al::strncasecmp(&in[si], pat, patLen) == 0) + { + if(repLen > maxLen-di) + { + repLen = maxLen - di; + truncated = 1; + } + strncpy(&out[di], rep, repLen); + si += patLen; + di += repLen; + } + } + out[di] = in[si]; + si++; + di++; + } + if(si < inLen) + truncated = 1; + out[di] = '\0'; + return !truncated; +} + + +/********************* + *** Math routines *** + *********************/ + +// Simple clamp routine. +static double Clamp(const double val, const double lower, const double upper) +{ + return std::min(std::max(val, lower), upper); +} + +static inline uint dither_rng(uint *seed) +{ + *seed = *seed * 96314165 + 907633515; + return *seed; +} + +// Performs a triangular probability density function dither. The input samples +// should be normalized (-1 to +1). +static void TpdfDither(double *RESTRICT out, const double *RESTRICT in, const double scale, + const uint count, const uint step, uint *seed) +{ + static constexpr double PRNG_SCALE = 1.0 / std::numeric_limits<uint>::max(); + + for(uint i{0};i < count;i++) + { + uint prn0{dither_rng(seed)}; + uint prn1{dither_rng(seed)}; + *out = std::round(*(in++)*scale + (prn0*PRNG_SCALE - prn1*PRNG_SCALE)); + out += step; + } +} + +/* Fast Fourier transform routines. The number of points must be a power of + * two. + */ + +// Performs bit-reversal ordering. +static void FftArrange(const uint n, complex_d *inout) +{ + // Handle in-place arrangement. + uint rk{0u}; + for(uint k{0u};k < n;k++) + { + if(rk > k) + std::swap(inout[rk], inout[k]); + + uint m{n}; + while(rk&(m >>= 1)) + rk &= ~m; + rk |= m; + } +} + +// Performs the summation. +static void FftSummation(const uint n, const double s, complex_d *cplx) +{ + double pi; + uint m, m2; + uint i, k, mk; + + pi = s * M_PI; + for(m = 1, m2 = 2;m < n; m <<= 1, m2 <<= 1) + { + // v = Complex (-2.0 * sin (0.5 * pi / m) * sin (0.5 * pi / m), -sin (pi / m)) + double sm = std::sin(0.5 * pi / m); + auto v = complex_d{-2.0*sm*sm, -std::sin(pi / m)}; + auto w = complex_d{1.0, 0.0}; + for(i = 0;i < m;i++) + { + for(k = i;k < n;k += m2) + { + mk = k + m; + auto t = w * cplx[mk]; + cplx[mk] = cplx[k] - t; + cplx[k] = cplx[k] + t; + } + w += v*w; + } + } +} + +// Performs a forward FFT. +void FftForward(const uint n, complex_d *inout) +{ + FftArrange(n, inout); + FftSummation(n, 1.0, inout); +} + +// Performs an inverse FFT. +void FftInverse(const uint n, complex_d *inout) +{ + FftArrange(n, inout); + FftSummation(n, -1.0, inout); + double f{1.0 / n}; + for(uint i{0};i < n;i++) + inout[i] *= f; +} + +/* Calculate the complex helical sequence (or discrete-time analytical signal) + * of the given input using the Hilbert transform. Given the natural logarithm + * of a signal's magnitude response, the imaginary components can be used as + * the angles for minimum-phase reconstruction. + */ +static void Hilbert(const uint n, complex_d *inout) +{ + uint i; + + // Handle in-place operation. + for(i = 0;i < n;i++) + inout[i].imag(0.0); + + FftInverse(n, inout); + for(i = 1;i < (n+1)/2;i++) + inout[i] *= 2.0; + /* Increment i if n is even. */ + i += (n&1)^1; + for(;i < n;i++) + inout[i] = complex_d{0.0, 0.0}; + FftForward(n, inout); +} + +/* Calculate the magnitude response of the given input. This is used in + * place of phase decomposition, since the phase residuals are discarded for + * minimum phase reconstruction. The mirrored half of the response is also + * discarded. + */ +void MagnitudeResponse(const uint n, const complex_d *in, double *out) +{ + const uint m = 1 + (n / 2); + uint i; + for(i = 0;i < m;i++) + out[i] = std::max(std::abs(in[i]), EPSILON); +} + +/* Apply a range limit (in dB) to the given magnitude response. This is used + * to adjust the effects of the diffuse-field average on the equalization + * process. + */ +static void LimitMagnitudeResponse(const uint n, const uint m, const double limit, const double *in, double *out) +{ + double halfLim; + uint i, lower, upper; + double ave; + + halfLim = limit / 2.0; + // Convert the response to dB. + for(i = 0;i < m;i++) + out[i] = 20.0 * std::log10(in[i]); + // Use six octaves to calculate the average magnitude of the signal. + lower = (static_cast<uint>(std::ceil(n / std::pow(2.0, 8.0)))) - 1; + upper = (static_cast<uint>(std::floor(n / std::pow(2.0, 2.0)))) - 1; + ave = 0.0; + for(i = lower;i <= upper;i++) + ave += out[i]; + ave /= upper - lower + 1; + // Keep the response within range of the average magnitude. + for(i = 0;i < m;i++) + out[i] = Clamp(out[i], ave - halfLim, ave + halfLim); + // Convert the response back to linear magnitude. + for(i = 0;i < m;i++) + out[i] = std::pow(10.0, out[i] / 20.0); +} + +/* Reconstructs the minimum-phase component for the given magnitude response + * of a signal. This is equivalent to phase recomposition, sans the missing + * residuals (which were discarded). The mirrored half of the response is + * reconstructed. + */ +static void MinimumPhase(const uint n, const double *in, complex_d *out) +{ + const uint m = 1 + (n / 2); + std::vector<double> mags(n); + + uint i; + for(i = 0;i < m;i++) + { + mags[i] = std::max(EPSILON, in[i]); + out[i] = complex_d{std::log(mags[i]), 0.0}; + } + for(;i < n;i++) + { + mags[i] = mags[n - i]; + out[i] = out[n - i]; + } + Hilbert(n, out); + // Remove any DC offset the filter has. + mags[0] = EPSILON; + for(i = 0;i < n;i++) + { + auto a = std::exp(complex_d{0.0, out[i].imag()}); + out[i] = complex_d{mags[i], 0.0} * a; + } +} + + +/*************************** + *** Resampler functions *** + ***************************/ + +/* This is the normalized cardinal sine (sinc) function. + * + * sinc(x) = { 1, x = 0 + * { sin(pi x) / (pi x), otherwise. + */ +static double Sinc(const double x) +{ + if(std::abs(x) < EPSILON) + return 1.0; + return std::sin(M_PI * x) / (M_PI * x); +} + +/* The zero-order modified Bessel function of the first kind, used for the + * Kaiser window. + * + * I_0(x) = sum_{k=0}^inf (1 / k!)^2 (x / 2)^(2 k) + * = sum_{k=0}^inf ((x / 2)^k / k!)^2 + */ +static double BesselI_0(const double x) +{ + double term, sum, x2, y, last_sum; + int k; + + // Start at k=1 since k=0 is trivial. + term = 1.0; + sum = 1.0; + x2 = x/2.0; + k = 1; + + // Let the integration converge until the term of the sum is no longer + // significant. + do { + y = x2 / k; + k++; + last_sum = sum; + term *= y * y; + sum += term; + } while(sum != last_sum); + return sum; +} + +/* Calculate a Kaiser window from the given beta value and a normalized k + * [-1, 1]. + * + * w(k) = { I_0(B sqrt(1 - k^2)) / I_0(B), -1 <= k <= 1 + * { 0, elsewhere. + * + * Where k can be calculated as: + * + * k = i / l, where -l <= i <= l. + * + * or: + * + * k = 2 i / M - 1, where 0 <= i <= M. + */ +static double Kaiser(const double b, const double k) +{ + if(!(k >= -1.0 && k <= 1.0)) + return 0.0; + return BesselI_0(b * std::sqrt(1.0 - k*k)) / BesselI_0(b); +} + +// Calculates the greatest common divisor of a and b. +static uint Gcd(uint x, uint y) +{ + while(y > 0) + { + uint z{y}; + y = x % y; + x = z; + } + return x; +} + +/* Calculates the size (order) of the Kaiser window. Rejection is in dB and + * the transition width is normalized frequency (0.5 is nyquist). + * + * M = { ceil((r - 7.95) / (2.285 2 pi f_t)), r > 21 + * { ceil(5.79 / 2 pi f_t), r <= 21. + * + */ +static uint CalcKaiserOrder(const double rejection, const double transition) +{ + double w_t = 2.0 * M_PI * transition; + if(rejection > 21.0) + return static_cast<uint>(std::ceil((rejection - 7.95) / (2.285 * w_t))); + return static_cast<uint>(std::ceil(5.79 / w_t)); +} + +// Calculates the beta value of the Kaiser window. Rejection is in dB. +static double CalcKaiserBeta(const double rejection) +{ + if(rejection > 50.0) + return 0.1102 * (rejection - 8.7); + if(rejection >= 21.0) + return (0.5842 * std::pow(rejection - 21.0, 0.4)) + + (0.07886 * (rejection - 21.0)); + return 0.0; +} + +/* Calculates a point on the Kaiser-windowed sinc filter for the given half- + * width, beta, gain, and cutoff. The point is specified in non-normalized + * samples, from 0 to M, where M = (2 l + 1). + * + * w(k) 2 p f_t sinc(2 f_t x) + * + * x -- centered sample index (i - l) + * k -- normalized and centered window index (x / l) + * w(k) -- window function (Kaiser) + * p -- gain compensation factor when sampling + * f_t -- normalized center frequency (or cutoff; 0.5 is nyquist) + */ +static double SincFilter(const uint l, const double b, const double gain, const double cutoff, const uint i) +{ + return Kaiser(b, static_cast<double>(i - l) / l) * 2.0 * gain * cutoff * Sinc(2.0 * cutoff * (i - l)); +} + +/* This is a polyphase sinc-filtered resampler. + * + * Upsample Downsample + * + * p/q = 3/2 p/q = 3/5 + * + * M-+-+-+-> M-+-+-+-> + * -------------------+ ---------------------+ + * p s * f f f f|f| | p s * f f f f f | + * | 0 * 0 0 0|0|0 | | 0 * 0 0 0 0|0| | + * v 0 * 0 0|0|0 0 | v 0 * 0 0 0|0|0 | + * s * f|f|f f f | s * f f|f|f f | + * 0 * |0|0 0 0 0 | 0 * 0|0|0 0 0 | + * --------+=+--------+ 0 * |0|0 0 0 0 | + * d . d .|d|. d . d ----------+=+--------+ + * d . . . .|d|. . . . + * q-> + * q-+-+-+-> + * + * P_f(i,j) = q i mod p + pj + * P_s(i,j) = floor(q i / p) - j + * d[i=0..N-1] = sum_{j=0}^{floor((M - 1) / p)} { + * { f[P_f(i,j)] s[P_s(i,j)], P_f(i,j) < M + * { 0, P_f(i,j) >= M. } + */ + +// Calculate the resampling metrics and build the Kaiser-windowed sinc filter +// that's used to cut frequencies above the destination nyquist. +void ResamplerSetup(ResamplerT *rs, const uint srcRate, const uint dstRate) +{ + const uint gcd{Gcd(srcRate, dstRate)}; + rs->mP = dstRate / gcd; + rs->mQ = srcRate / gcd; + + /* The cutoff is adjusted by half the transition width, so the transition + * ends before the nyquist (0.5). Both are scaled by the downsampling + * factor. + */ + double cutoff, width; + if(rs->mP > rs->mQ) + { + cutoff = 0.475 / rs->mP; + width = 0.05 / rs->mP; + } + else + { + cutoff = 0.475 / rs->mQ; + width = 0.05 / rs->mQ; + } + // A rejection of -180 dB is used for the stop band. Round up when + // calculating the left offset to avoid increasing the transition width. + const uint l{(CalcKaiserOrder(180.0, width)+1) / 2}; + const double beta{CalcKaiserBeta(180.0)}; + rs->mM = l*2 + 1; + rs->mL = l; + rs->mF.resize(rs->mM); + for(uint i{0};i < rs->mM;i++) + rs->mF[i] = SincFilter(l, beta, rs->mP, cutoff, i); +} + +// Perform the upsample-filter-downsample resampling operation using a +// polyphase filter implementation. +void ResamplerRun(ResamplerT *rs, const uint inN, const double *in, const uint outN, double *out) +{ + const uint p = rs->mP, q = rs->mQ, m = rs->mM, l = rs->mL; + std::vector<double> workspace; + const double *f = rs->mF.data(); + uint j_f, j_s; + double *work; + uint i; + + if(outN == 0) + return; + + // Handle in-place operation. + if(in == out) + { + workspace.resize(outN); + work = workspace.data(); + } + else + work = out; + // Resample the input. + for(i = 0;i < outN;i++) + { + double r = 0.0; + // Input starts at l to compensate for the filter delay. This will + // drop any build-up from the first half of the filter. + j_f = (l + (q * i)) % p; + j_s = (l + (q * i)) / p; + while(j_f < m) + { + // Only take input when 0 <= j_s < inN. This single unsigned + // comparison catches both cases. + if(j_s < inN) + r += f[j_f] * in[j_s]; + j_f += p; + j_s--; + } + work[i] = r; + } + // Clean up after in-place operation. + if(work != out) + { + for(i = 0;i < outN;i++) + out[i] = work[i]; + } +} + + +/*************************** + *** File storage output *** + ***************************/ + +// Write an ASCII string to a file. +static int WriteAscii(const char *out, FILE *fp, const char *filename) +{ + size_t len; + + len = strlen(out); + if(fwrite(out, 1, len, fp) != len) + { + fclose(fp); + fprintf(stderr, "\nError: Bad write to file '%s'.\n", filename); + return 0; + } + return 1; +} + +// Write a binary value of the given byte order and byte size to a file, +// loading it from a 32-bit unsigned integer. +static int WriteBin4(const uint bytes, const uint32_t in, FILE *fp, const char *filename) +{ + uint8_t out[4]; + uint i; + + for(i = 0;i < bytes;i++) + out[i] = (in>>(i*8)) & 0x000000FF; + + if(fwrite(out, 1, bytes, fp) != bytes) + { + fprintf(stderr, "\nError: Bad write to file '%s'.\n", filename); + return 0; + } + return 1; +} + +// Store the OpenAL Soft HRTF data set. +static int StoreMhr(const HrirDataT *hData, const char *filename) +{ + uint channels = (hData->mChannelType == CT_STEREO) ? 2 : 1; + uint n = hData->mIrPoints; + FILE *fp; + uint fi, ei, ai, i; + uint dither_seed = 22222; + + if((fp=fopen(filename, "wb")) == nullptr) + { + fprintf(stderr, "\nError: Could not open MHR file '%s'.\n", filename); + return 0; + } + if(!WriteAscii(MHR_FORMAT, fp, filename)) + return 0; + if(!WriteBin4(4, hData->mIrRate, fp, filename)) + return 0; + if(!WriteBin4(1, static_cast<uint32_t>(hData->mSampleType), fp, filename)) + return 0; + if(!WriteBin4(1, static_cast<uint32_t>(hData->mChannelType), fp, filename)) + return 0; + if(!WriteBin4(1, hData->mIrPoints, fp, filename)) + return 0; + if(!WriteBin4(1, hData->mFdCount, fp, filename)) + return 0; + for(fi = 0;fi < hData->mFdCount;fi++) + { + auto fdist = static_cast<uint32_t>(std::round(1000.0 * hData->mFds[fi].mDistance)); + if(!WriteBin4(2, fdist, fp, filename)) + return 0; + if(!WriteBin4(1, hData->mFds[fi].mEvCount, fp, filename)) + return 0; + for(ei = 0;ei < hData->mFds[fi].mEvCount;ei++) + { + if(!WriteBin4(1, hData->mFds[fi].mEvs[ei].mAzCount, fp, filename)) + return 0; + } + } + + for(fi = 0;fi < hData->mFdCount;fi++) + { + const double scale = (hData->mSampleType == ST_S16) ? 32767.0 : + ((hData->mSampleType == ST_S24) ? 8388607.0 : 0.0); + const uint bps = (hData->mSampleType == ST_S16) ? 2 : + ((hData->mSampleType == ST_S24) ? 3 : 0); + + for(ei = 0;ei < hData->mFds[fi].mEvCount;ei++) + { + for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) + { + HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; + double out[2 * MAX_TRUNCSIZE]; + + TpdfDither(out, azd->mIrs[0], scale, n, channels, &dither_seed); + if(hData->mChannelType == CT_STEREO) + TpdfDither(out+1, azd->mIrs[1], scale, n, channels, &dither_seed); + for(i = 0;i < (channels * n);i++) + { + int v = static_cast<int>(Clamp(out[i], -scale-1.0, scale)); + if(!WriteBin4(bps, static_cast<uint32_t>(v), fp, filename)) + return 0; + } + } + } + } + for(fi = 0;fi < hData->mFdCount;fi++) + { + for(ei = 0;ei < hData->mFds[fi].mEvCount;ei++) + { + for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) + { + const HrirAzT &azd = hData->mFds[fi].mEvs[ei].mAzs[ai]; + int v = static_cast<int>(std::min(std::round(hData->mIrRate * azd.mDelays[0]), MAX_HRTD)); + + if(!WriteBin4(1, static_cast<uint32_t>(v), fp, filename)) + return 0; + if(hData->mChannelType == CT_STEREO) + { + v = static_cast<int>(std::min(std::round(hData->mIrRate * azd.mDelays[1]), MAX_HRTD)); + + if(!WriteBin4(1, static_cast<uint32_t>(v), fp, filename)) + return 0; + } + } + } + } + fclose(fp); + return 1; +} + + +/*********************** + *** HRTF processing *** + ***********************/ + +/* Balances the maximum HRIR magnitudes of multi-field data sets by + * independently normalizing each field in relation to the overall maximum. + * This is done to ignore distance attenuation. + */ +static void BalanceFieldMagnitudes(const HrirDataT *hData, const uint channels, const uint m) +{ + double maxMags[MAX_FD_COUNT]; + uint fi, ei, ai, ti, i; + + double maxMag{0.0}; + for(fi = 0;fi < hData->mFdCount;fi++) + { + maxMags[fi] = 0.0; + + for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvCount;ei++) + { + for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) + { + HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; + for(ti = 0;ti < channels;ti++) + { + for(i = 0;i < m;i++) + maxMags[fi] = std::max(azd->mIrs[ti][i], maxMags[fi]); + } + } + } + + maxMag = std::max(maxMags[fi], maxMag); + } + + for(fi = 0;fi < hData->mFdCount;fi++) + { + const double magFactor{maxMag / maxMags[fi]}; + + for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvCount;ei++) + { + for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) + { + HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; + for(ti = 0;ti < channels;ti++) + { + for(i = 0;i < m;i++) + azd->mIrs[ti][i] *= magFactor; + } + } + } + } +} + +/* Calculate the contribution of each HRIR to the diffuse-field average based + * on its coverage volume. All volumes are centered at the spherical HRIR + * coordinates and measured by extruded solid angle. + */ +static void CalculateDfWeights(const HrirDataT *hData, double *weights) +{ + double sum, innerRa, outerRa, evs, ev, upperEv, lowerEv; + double solidAngle, solidVolume; + uint fi, ei; + + sum = 0.0; + // The head radius acts as the limit for the inner radius. + innerRa = hData->mRadius; + for(fi = 0;fi < hData->mFdCount;fi++) + { + // Each volume ends half way between progressive field measurements. + if((fi + 1) < hData->mFdCount) + outerRa = 0.5f * (hData->mFds[fi].mDistance + hData->mFds[fi + 1].mDistance); + // The final volume has its limit extended to some practical value. + // This is done to emphasize the far-field responses in the average. + else + outerRa = 10.0f; + + evs = M_PI / 2.0 / (hData->mFds[fi].mEvCount - 1); + for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvCount;ei++) + { + // For each elevation, calculate the upper and lower limits of + // the patch band. + ev = hData->mFds[fi].mEvs[ei].mElevation; + lowerEv = std::max(-M_PI / 2.0, ev - evs); + upperEv = std::min(M_PI / 2.0, ev + evs); + // Calculate the surface area of the patch band. + solidAngle = 2.0 * M_PI * (std::sin(upperEv) - std::sin(lowerEv)); + // Then the volume of the extruded patch band. + solidVolume = solidAngle * (std::pow(outerRa, 3.0) - std::pow(innerRa, 3.0)) / 3.0; + // Each weight is the volume of one extruded patch. + weights[(fi * MAX_EV_COUNT) + ei] = solidVolume / hData->mFds[fi].mEvs[ei].mAzCount; + // Sum the total coverage volume of the HRIRs for all fields. + sum += solidAngle; + } + + innerRa = outerRa; + } + + for(fi = 0;fi < hData->mFdCount;fi++) + { + // Normalize the weights given the total surface coverage for all + // fields. + for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvCount;ei++) + weights[(fi * MAX_EV_COUNT) + ei] /= sum; + } +} + +/* Calculate the diffuse-field average from the given magnitude responses of + * the HRIR set. Weighting can be applied to compensate for the varying + * coverage of each HRIR. The final average can then be limited by the + * specified magnitude range (in positive dB; 0.0 to skip). + */ +static void CalculateDiffuseFieldAverage(const HrirDataT *hData, const uint channels, const uint m, const int weighted, const double limit, double *dfa) +{ + std::vector<double> weights(hData->mFdCount * MAX_EV_COUNT); + uint count, ti, fi, ei, i, ai; + + if(weighted) + { + // Use coverage weighting to calculate the average. + CalculateDfWeights(hData, weights.data()); + } + else + { + double weight; + + // If coverage weighting is not used, the weights still need to be + // averaged by the number of existing HRIRs. + count = hData->mIrCount; + for(fi = 0;fi < hData->mFdCount;fi++) + { + for(ei = 0;ei < hData->mFds[fi].mEvStart;ei++) + count -= hData->mFds[fi].mEvs[ei].mAzCount; + } + weight = 1.0 / count; + + for(fi = 0;fi < hData->mFdCount;fi++) + { + for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvCount;ei++) + weights[(fi * MAX_EV_COUNT) + ei] = weight; + } + } + for(ti = 0;ti < channels;ti++) + { + for(i = 0;i < m;i++) + dfa[(ti * m) + i] = 0.0; + for(fi = 0;fi < hData->mFdCount;fi++) + { + for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvCount;ei++) + { + for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) + { + HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; + // Get the weight for this HRIR's contribution. + double weight = weights[(fi * MAX_EV_COUNT) + ei]; + + // Add this HRIR's weighted power average to the total. + for(i = 0;i < m;i++) + dfa[(ti * m) + i] += weight * azd->mIrs[ti][i] * azd->mIrs[ti][i]; + } + } + } + // Finish the average calculation and keep it from being too small. + for(i = 0;i < m;i++) + dfa[(ti * m) + i] = std::max(sqrt(dfa[(ti * m) + i]), EPSILON); + // Apply a limit to the magnitude range of the diffuse-field average + // if desired. + if(limit > 0.0) + LimitMagnitudeResponse(hData->mFftSize, m, limit, &dfa[ti * m], &dfa[ti * m]); + } +} + +// Perform diffuse-field equalization on the magnitude responses of the HRIR +// set using the given average response. +static void DiffuseFieldEqualize(const uint channels, const uint m, const double *dfa, const HrirDataT *hData) +{ + uint ti, fi, ei, ai, i; + + for(fi = 0;fi < hData->mFdCount;fi++) + { + for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvCount;ei++) + { + for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) + { + HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; + + for(ti = 0;ti < channels;ti++) + { + for(i = 0;i < m;i++) + azd->mIrs[ti][i] /= dfa[(ti * m) + i]; + } + } + } + } +} + +/* Perform minimum-phase reconstruction using the magnitude responses of the + * HRIR set. Work is delegated to this struct, which runs asynchronously on one + * or more threads (sharing the same reconstructor object). + */ +struct HrirReconstructor { + std::vector<double*> mIrs; + std::atomic<size_t> mCurrent; + std::atomic<size_t> mDone; + uint mFftSize; + uint mIrPoints; + + void Worker() + { + auto h = std::vector<complex_d>(mFftSize); + + while(1) + { + /* Load the current index to process. */ + size_t idx{mCurrent.load()}; + do { + /* If the index is at the end, we're done. */ + if(idx >= mIrs.size()) + return; + /* Otherwise, increment the current index atomically so other + * threads know to go to the next one. If this call fails, the + * current index was just changed by another thread and the new + * value is loaded into idx, which we'll recheck. + */ + } while(!mCurrent.compare_exchange_weak(idx, idx+1, std::memory_order_relaxed)); + + /* Now do the reconstruction, and apply the inverse FFT to get the + * time-domain response. + */ + MinimumPhase(mFftSize, mIrs[idx], h.data()); + FftInverse(mFftSize, h.data()); + for(uint i{0u};i < mIrPoints;++i) + mIrs[idx][i] = h[i].real(); + + /* Increment the number of IRs done. */ + mDone.fetch_add(1); + } + } +}; + +static void ReconstructHrirs(const HrirDataT *hData) +{ + const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u}; + + /* Count the number of IRs to process (excluding elevations that will be + * synthesized later). + */ + size_t total{hData->mIrCount}; + for(uint fi{0u};fi < hData->mFdCount;fi++) + { + for(uint ei{0u};ei < hData->mFds[fi].mEvStart;ei++) + total -= hData->mFds[fi].mEvs[ei].mAzCount; + } + total *= channels; + + /* Set up the reconstructor with the needed size info and pointers to the + * IRs to process. + */ + HrirReconstructor reconstructor; + reconstructor.mIrs.reserve(total); + reconstructor.mCurrent.store(0, std::memory_order_relaxed); + reconstructor.mDone.store(0, std::memory_order_relaxed); + reconstructor.mFftSize = hData->mFftSize; + reconstructor.mIrPoints = hData->mIrPoints; + for(uint fi{0u};fi < hData->mFdCount;fi++) + { + const HrirFdT &field = hData->mFds[fi]; + for(uint ei{field.mEvStart};ei < field.mEvCount;ei++) + { + const HrirEvT &elev = field.mEvs[ei]; + for(uint ai{0u};ai < elev.mAzCount;ai++) + { + const HrirAzT &azd = elev.mAzs[ai]; + for(uint ti{0u};ti < channels;ti++) + reconstructor.mIrs.push_back(azd.mIrs[ti]); + } + } + } + + /* Launch two threads to work on reconstruction. */ + std::thread thrd1{std::mem_fn(&HrirReconstructor::Worker), &reconstructor}; + std::thread thrd2{std::mem_fn(&HrirReconstructor::Worker), &reconstructor}; + + /* Keep track of the number of IRs done, periodically reporting it. */ + size_t count; + while((count=reconstructor.mDone.load()) != total) + { + size_t pcdone{count * 100 / total}; + + printf("\r%3zu%% done (%zu of %zu)", pcdone, count, total); + fflush(stdout); + + std::this_thread::sleep_for(std::chrono::milliseconds{50}); + } + size_t pcdone{count * 100 / total}; + printf("\r%3zu%% done (%zu of %zu)\n", pcdone, count, total); + + if(thrd2.joinable()) thrd2.join(); + if(thrd1.joinable()) thrd1.join(); +} + +// Resamples the HRIRs for use at the given sampling rate. +static void ResampleHrirs(const uint rate, HrirDataT *hData) +{ + uint channels = (hData->mChannelType == CT_STEREO) ? 2 : 1; + uint n = hData->mIrPoints; + uint ti, fi, ei, ai; + ResamplerT rs; + + ResamplerSetup(&rs, hData->mIrRate, rate); + for(fi = 0;fi < hData->mFdCount;fi++) + { + for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvCount;ei++) + { + for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) + { + HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; + for(ti = 0;ti < channels;ti++) + ResamplerRun(&rs, n, azd->mIrs[ti], n, azd->mIrs[ti]); + } + } + } + hData->mIrRate = rate; +} + +/* Given field and elevation indices and an azimuth, calculate the indices of + * the two HRIRs that bound the coordinate along with a factor for + * calculating the continuous HRIR using interpolation. + */ +static void CalcAzIndices(const HrirFdT &field, const uint ei, const double az, uint *a0, uint *a1, double *af) +{ + double f{(2.0*M_PI + az) * field.mEvs[ei].mAzCount / (2.0*M_PI)}; + uint i{static_cast<uint>(f) % field.mEvs[ei].mAzCount}; + + f -= std::floor(f); + *a0 = i; + *a1 = (i + 1) % field.mEvs[ei].mAzCount; + *af = f; +} + +/* Synthesize any missing onset timings at the bottom elevations of each field. + * This just mirrors some top elevations for the bottom, and blends the + * remaining elevations (not an accurate model). + */ +static void SynthesizeOnsets(HrirDataT *hData) +{ + const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u}; + + auto proc_field = [channels](HrirFdT &field) -> void + { + /* Get the starting elevation from the measurements, and use it as the + * upper elevation limit for what needs to be calculated. + */ + const uint upperElevReal{field.mEvStart}; + if(upperElevReal <= 0) return; + + /* Get the lowest half of the missing elevations' delays by mirroring + * the top elevation delays. The responses are on a spherical grid + * centered between the ears, so these should align. + */ + uint ei{}; + if(channels > 1) + { + /* Take the polar opposite position of the desired measurement and + * swap the ears. + */ + field.mEvs[0].mAzs[0].mDelays[0] = field.mEvs[field.mEvCount-1].mAzs[0].mDelays[1]; + field.mEvs[0].mAzs[0].mDelays[1] = field.mEvs[field.mEvCount-1].mAzs[0].mDelays[0]; + for(ei = 1u;ei < (upperElevReal+1)/2;++ei) + { + const uint topElev{field.mEvCount-ei-1}; + + for(uint ai{0u};ai < field.mEvs[ei].mAzCount;ai++) + { + uint a0, a1; + double af; + + /* Rotate this current azimuth by a half-circle, and lookup + * the mirrored elevation to find the indices for the polar + * opposite position (may need blending). + */ + const double az{field.mEvs[ei].mAzs[ai].mAzimuth + M_PI}; + CalcAzIndices(field, topElev, az, &a0, &a1, &af); + + /* Blend the delays, and again, swap the ears. */ + field.mEvs[ei].mAzs[ai].mDelays[0] = Lerp( + field.mEvs[topElev].mAzs[a0].mDelays[1], + field.mEvs[topElev].mAzs[a1].mDelays[1], af); + field.mEvs[ei].mAzs[ai].mDelays[1] = Lerp( + field.mEvs[topElev].mAzs[a0].mDelays[0], + field.mEvs[topElev].mAzs[a1].mDelays[0], af); + } + } + } + else + { + field.mEvs[0].mAzs[0].mDelays[0] = field.mEvs[field.mEvCount-1].mAzs[0].mDelays[0]; + for(ei = 1u;ei < (upperElevReal+1)/2;++ei) + { + const uint topElev{field.mEvCount-ei-1}; + + for(uint ai{0u};ai < field.mEvs[ei].mAzCount;ai++) + { + uint a0, a1; + double af; + + /* For mono data sets, mirror the azimuth front<->back + * since the other ear is a mirror of what we have (e.g. + * the left ear's back-left is simulated with the right + * ear's front-right, which uses the left ear's front-left + * measurement). + */ + double az{field.mEvs[ei].mAzs[ai].mAzimuth}; + if(az <= M_PI) az = M_PI - az; + else az = (M_PI*2.0)-az + M_PI; + CalcAzIndices(field, topElev, az, &a0, &a1, &af); + + field.mEvs[ei].mAzs[ai].mDelays[0] = Lerp( + field.mEvs[topElev].mAzs[a0].mDelays[0], + field.mEvs[topElev].mAzs[a1].mDelays[0], af); + } + } + } + /* Record the lowest elevation filled in with the mirrored top. */ + const uint lowerElevFake{ei-1u}; + + /* Fill in the remaining delays using bilinear interpolation. This + * helps smooth the transition back to the real delays. + */ + for(;ei < upperElevReal;++ei) + { + const double ef{(field.mEvs[upperElevReal].mElevation - field.mEvs[ei].mElevation) / + (field.mEvs[upperElevReal].mElevation - field.mEvs[lowerElevFake].mElevation)}; + + for(uint ai{0u};ai < field.mEvs[ei].mAzCount;ai++) + { + uint a0, a1, a2, a3; + double af0, af1; + + double az{field.mEvs[ei].mAzs[ai].mAzimuth}; + CalcAzIndices(field, upperElevReal, az, &a0, &a1, &af0); + CalcAzIndices(field, lowerElevFake, az, &a2, &a3, &af1); + double blend[4]{ + (1.0-ef) * (1.0-af0), + (1.0-ef) * ( af0), + ( ef) * (1.0-af1), + ( ef) * ( af1) + }; + + for(uint ti{0u};ti < channels;ti++) + { + field.mEvs[ei].mAzs[ai].mDelays[ti] = + field.mEvs[upperElevReal].mAzs[a0].mDelays[ti]*blend[0] + + field.mEvs[upperElevReal].mAzs[a1].mDelays[ti]*blend[1] + + field.mEvs[lowerElevFake].mAzs[a2].mDelays[ti]*blend[2] + + field.mEvs[lowerElevFake].mAzs[a3].mDelays[ti]*blend[3]; + } + } + } + }; + std::for_each(hData->mFds.begin(), hData->mFds.begin()+hData->mFdCount, proc_field); +} + +/* Attempt to synthesize any missing HRIRs at the bottom elevations of each + * field. Right now this just blends the lowest elevation HRIRs together and + * applies some attenuation and high frequency damping. It is a simple, if + * inaccurate model. + */ +static void SynthesizeHrirs(HrirDataT *hData) +{ + const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u}; + const uint irSize{hData->mIrPoints}; + const double beta{3.5e-6 * hData->mIrRate}; + + auto proc_field = [channels,irSize,beta](HrirFdT &field) -> void + { + const uint oi{field.mEvStart}; + if(oi <= 0) return; + + for(uint ti{0u};ti < channels;ti++) + { + for(uint i{0u};i < irSize;i++) + field.mEvs[0].mAzs[0].mIrs[ti][i] = 0.0; + /* Blend the lowest defined elevation's responses for an average + * -90 degree elevation response. + */ + double blend_count{0.0}; + for(uint ai{0u};ai < field.mEvs[oi].mAzCount;ai++) + { + /* Only include the left responses for the left ear, and the + * right responses for the right ear. This removes the cross- + * talk that shouldn't exist for the -90 degree elevation + * response (and would be mistimed anyway). NOTE: Azimuth goes + * from 0...2pi rather than -pi...+pi (0 in front, clockwise). + */ + if(std::abs(field.mEvs[oi].mAzs[ai].mAzimuth) < EPSILON || + (ti == LeftChannel && field.mEvs[oi].mAzs[ai].mAzimuth > M_PI-EPSILON) || + (ti == RightChannel && field.mEvs[oi].mAzs[ai].mAzimuth < M_PI+EPSILON)) + { + for(uint i{0u};i < irSize;i++) + field.mEvs[0].mAzs[0].mIrs[ti][i] += field.mEvs[oi].mAzs[ai].mIrs[ti][i]; + blend_count += 1.0; + } + } + if(blend_count > 0.0) + { + for(uint i{0u};i < irSize;i++) + field.mEvs[0].mAzs[0].mIrs[ti][i] /= blend_count; + } + + for(uint ei{1u};ei < field.mEvStart;ei++) + { + const double of{static_cast<double>(ei) / field.mEvStart}; + const double b{(1.0 - of) * beta}; + for(uint ai{0u};ai < field.mEvs[ei].mAzCount;ai++) + { + uint a0, a1; + double af; + + CalcAzIndices(field, oi, field.mEvs[ei].mAzs[ai].mAzimuth, &a0, &a1, &af); + double lp[4]{}; + for(uint i{0u};i < irSize;i++) + { + /* Blend the two defined HRIRs closest to this azimuth, + * then blend that with the synthesized -90 elevation. + */ + const double s1{Lerp(field.mEvs[oi].mAzs[a0].mIrs[ti][i], + field.mEvs[oi].mAzs[a1].mIrs[ti][i], af)}; + const double s0{Lerp(field.mEvs[0].mAzs[0].mIrs[ti][i], s1, of)}; + /* Apply a low-pass to simulate body occlusion. */ + lp[0] = Lerp(s0, lp[0], b); + lp[1] = Lerp(lp[0], lp[1], b); + lp[2] = Lerp(lp[1], lp[2], b); + lp[3] = Lerp(lp[2], lp[3], b); + field.mEvs[ei].mAzs[ai].mIrs[ti][i] = lp[3]; + } + } + } + const double b{beta}; + double lp[4]{}; + for(uint i{0u};i < irSize;i++) + { + const double s0{field.mEvs[0].mAzs[0].mIrs[ti][i]}; + lp[0] = Lerp(s0, lp[0], b); + lp[1] = Lerp(lp[0], lp[1], b); + lp[2] = Lerp(lp[1], lp[2], b); + lp[3] = Lerp(lp[2], lp[3], b); + field.mEvs[0].mAzs[0].mIrs[ti][i] = lp[3]; + } + } + field.mEvStart = 0; + }; + std::for_each(hData->mFds.begin(), hData->mFds.begin()+hData->mFdCount, proc_field); +} + +// The following routines assume a full set of HRIRs for all elevations. + +// Normalize the HRIR set and slightly attenuate the result. +static void NormalizeHrirs(HrirDataT *hData) +{ + const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u}; + const uint irSize{hData->mIrPoints}; + + /* Find the maximum amplitude and RMS out of all the IRs. */ + struct LevelPair { double amp, rms; }; + auto proc0_field = [channels,irSize](const LevelPair levels0, const HrirFdT &field) -> LevelPair + { + auto proc_elev = [channels,irSize](const LevelPair levels1, const HrirEvT &elev) -> LevelPair + { + auto proc_azi = [channels,irSize](const LevelPair levels2, const HrirAzT &azi) -> LevelPair + { + auto proc_channel = [irSize](const LevelPair levels3, const double *ir) -> LevelPair + { + /* Calculate the peak amplitude and RMS of this IR. */ + auto current = std::accumulate(ir, ir+irSize, LevelPair{0.0, 0.0}, + [](const LevelPair cur, const double impulse) -> LevelPair + { + return {std::max(std::abs(impulse), cur.amp), + cur.rms + impulse*impulse}; + }); + current.rms = std::sqrt(current.rms / irSize); + + /* Accumulate levels by taking the maximum amplitude and RMS. */ + return LevelPair{std::max(current.amp, levels3.amp), + std::max(current.rms, levels3.rms)}; + }; + return std::accumulate(azi.mIrs, azi.mIrs+channels, levels2, proc_channel); + }; + return std::accumulate(elev.mAzs, elev.mAzs+elev.mAzCount, levels1, proc_azi); + }; + return std::accumulate(field.mEvs, field.mEvs+field.mEvCount, levels0, proc_elev); + }; + const auto maxlev = std::accumulate(hData->mFds.begin(), hData->mFds.begin()+hData->mFdCount, + LevelPair{0.0, 0.0}, proc0_field); + + /* Normalize using the maximum RMS of the HRIRs. The RMS measure for the + * non-filtered signal is of an impulse with equal length (to the filter): + * + * rms_impulse = sqrt(sum([ 1^2, 0^2, 0^2, ... ]) / n) + * = sqrt(1 / n) + * + * This helps keep a more consistent volume between the non-filtered signal + * and various data sets. + */ + double factor{std::sqrt(1.0 / irSize) / maxlev.rms}; + + /* Also ensure the samples themselves won't clip. */ + factor = std::min(factor, 0.99/maxlev.amp); + + /* Now scale all IRs by the given factor. */ + auto proc1_field = [channels,irSize,factor](HrirFdT &field) -> void + { + auto proc_elev = [channels,irSize,factor](HrirEvT &elev) -> void + { + auto proc_azi = [channels,irSize,factor](HrirAzT &azi) -> void + { + auto proc_channel = [irSize,factor](double *ir) -> void + { + std::transform(ir, ir+irSize, ir, + std::bind(std::multiplies<double>{}, _1, factor)); + }; + std::for_each(azi.mIrs, azi.mIrs+channels, proc_channel); + }; + std::for_each(elev.mAzs, elev.mAzs+elev.mAzCount, proc_azi); + }; + std::for_each(field.mEvs, field.mEvs+field.mEvCount, proc_elev); + }; + std::for_each(hData->mFds.begin(), hData->mFds.begin()+hData->mFdCount, proc1_field); +} + +// Calculate the left-ear time delay using a spherical head model. +static double CalcLTD(const double ev, const double az, const double rad, const double dist) +{ + double azp, dlp, l, al; + + azp = std::asin(std::cos(ev) * std::sin(az)); + dlp = std::sqrt((dist*dist) + (rad*rad) + (2.0*dist*rad*sin(azp))); + l = std::sqrt((dist*dist) - (rad*rad)); + al = (0.5 * M_PI) + azp; + if(dlp > l) + dlp = l + (rad * (al - std::acos(rad / dist))); + return dlp / 343.3; +} + +// Calculate the effective head-related time delays for each minimum-phase +// HRIR. This is done per-field since distance delay is ignored. +static void CalculateHrtds(const HeadModelT model, const double radius, HrirDataT *hData) +{ + uint channels = (hData->mChannelType == CT_STEREO) ? 2 : 1; + double customRatio{radius / hData->mRadius}; + uint ti, fi, ei, ai; + + if(model == HM_SPHERE) + { + for(fi = 0;fi < hData->mFdCount;fi++) + { + for(ei = 0;ei < hData->mFds[fi].mEvCount;ei++) + { + HrirEvT *evd = &hData->mFds[fi].mEvs[ei]; + + for(ai = 0;ai < evd->mAzCount;ai++) + { + HrirAzT *azd = &evd->mAzs[ai]; + + for(ti = 0;ti < channels;ti++) + azd->mDelays[ti] = CalcLTD(evd->mElevation, azd->mAzimuth, radius, hData->mFds[fi].mDistance); + } + } + } + } + else if(customRatio != 1.0) + { + for(fi = 0;fi < hData->mFdCount;fi++) + { + for(ei = 0;ei < hData->mFds[fi].mEvCount;ei++) + { + HrirEvT *evd = &hData->mFds[fi].mEvs[ei]; + + for(ai = 0;ai < evd->mAzCount;ai++) + { + HrirAzT *azd = &evd->mAzs[ai]; + for(ti = 0;ti < channels;ti++) + azd->mDelays[ti] *= customRatio; + } + } + } + } + + for(fi = 0;fi < hData->mFdCount;fi++) + { + double minHrtd{std::numeric_limits<double>::infinity()}; + for(ei = 0;ei < hData->mFds[fi].mEvCount;ei++) + { + for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) + { + HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; + + for(ti = 0;ti < channels;ti++) + minHrtd = std::min(azd->mDelays[ti], minHrtd); + } + } + + for(ei = 0;ei < hData->mFds[fi].mEvCount;ei++) + { + for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzCount;ai++) + { + HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai]; + + for(ti = 0;ti < channels;ti++) + azd->mDelays[ti] -= minHrtd; + } + } + } +} + +// Allocate and configure dynamic HRIR structures. +int PrepareHrirData(const uint fdCount, const double (&distances)[MAX_FD_COUNT], + const uint (&evCounts)[MAX_FD_COUNT], const uint azCounts[MAX_FD_COUNT * MAX_EV_COUNT], + HrirDataT *hData) +{ + uint evTotal = 0, azTotal = 0, fi, ei, ai; + + for(fi = 0;fi < fdCount;fi++) + { + evTotal += evCounts[fi]; + for(ei = 0;ei < evCounts[fi];ei++) + azTotal += azCounts[(fi * MAX_EV_COUNT) + ei]; + } + if(!fdCount || !evTotal || !azTotal) + return 0; + + hData->mEvsBase.resize(evTotal); + hData->mAzsBase.resize(azTotal); + hData->mFds.resize(fdCount); + hData->mIrCount = azTotal; + hData->mFdCount = fdCount; + evTotal = 0; + azTotal = 0; + for(fi = 0;fi < fdCount;fi++) + { + hData->mFds[fi].mDistance = distances[fi]; + hData->mFds[fi].mEvCount = evCounts[fi]; + hData->mFds[fi].mEvStart = 0; + hData->mFds[fi].mEvs = &hData->mEvsBase[evTotal]; + evTotal += evCounts[fi]; + for(ei = 0;ei < evCounts[fi];ei++) + { + uint azCount = azCounts[(fi * MAX_EV_COUNT) + ei]; + + hData->mFds[fi].mIrCount += azCount; + hData->mFds[fi].mEvs[ei].mElevation = -M_PI / 2.0 + M_PI * ei / (evCounts[fi] - 1); + hData->mFds[fi].mEvs[ei].mIrCount += azCount; + hData->mFds[fi].mEvs[ei].mAzCount = azCount; + hData->mFds[fi].mEvs[ei].mAzs = &hData->mAzsBase[azTotal]; + for(ai = 0;ai < azCount;ai++) + { + hData->mFds[fi].mEvs[ei].mAzs[ai].mAzimuth = 2.0 * M_PI * ai / azCount; + hData->mFds[fi].mEvs[ei].mAzs[ai].mIndex = azTotal + ai; + hData->mFds[fi].mEvs[ei].mAzs[ai].mDelays[0] = 0.0; + hData->mFds[fi].mEvs[ei].mAzs[ai].mDelays[1] = 0.0; + hData->mFds[fi].mEvs[ei].mAzs[ai].mIrs[0] = nullptr; + hData->mFds[fi].mEvs[ei].mAzs[ai].mIrs[1] = nullptr; + } + azTotal += azCount; + } + } + return 1; +} + + +/* Parse the data set definition and process the source data, storing the + * resulting data set as desired. If the input name is NULL it will read + * from standard input. + */ +static int ProcessDefinition(const char *inName, const uint outRate, const ChannelModeT chanMode, + const uint fftSize, const int equalize, const int surface, const double limit, + const uint truncSize, const HeadModelT model, const double radius, const char *outName) +{ + char rateStr[8+1], expName[MAX_PATH_LEN]; + HrirDataT hData; + + if(!inName) + { + inName = "stdin"; + fprintf(stdout, "Reading HRIR definition from %s...\n", inName); + if(!LoadDefInput(std::cin, nullptr, 0, inName, fftSize, truncSize, chanMode, &hData)) + return 0; + } + else + { + std::unique_ptr<al::ifstream> input{new al::ifstream{inName}}; + if(!input->is_open()) + { + fprintf(stderr, "Error: Could not open input file '%s'\n", inName); + return 0; + } + + char startbytes[4]{}; + input->read(startbytes, sizeof(startbytes)); + std::streamsize startbytecount{input->gcount()}; + if(startbytecount != sizeof(startbytes) || !input->good()) + { + fprintf(stderr, "Error: Could not read input file '%s'\n", inName); + return 0; + } + + if(startbytes[0] == '\x89' && startbytes[1] == 'H' && startbytes[2] == 'D' + && startbytes[3] == 'F') + { + input = nullptr; + fprintf(stdout, "Reading HRTF data from %s...\n", inName); + if(!LoadSofaFile(inName, fftSize, truncSize, chanMode, &hData)) + return 0; + } + else + { + fprintf(stdout, "Reading HRIR definition from %s...\n", inName); + if(!LoadDefInput(*input, startbytes, startbytecount, inName, fftSize, truncSize, chanMode, &hData)) + return 0; + } + } + + if(equalize) + { + uint c{(hData.mChannelType == CT_STEREO) ? 2u : 1u}; + uint m{hData.mFftSize/2u + 1u}; + auto dfa = std::vector<double>(c * m); + + if(hData.mFdCount > 1) + { + fprintf(stdout, "Balancing field magnitudes...\n"); + BalanceFieldMagnitudes(&hData, c, m); + } + fprintf(stdout, "Calculating diffuse-field average...\n"); + CalculateDiffuseFieldAverage(&hData, c, m, surface, limit, dfa.data()); + fprintf(stdout, "Performing diffuse-field equalization...\n"); + DiffuseFieldEqualize(c, m, dfa.data(), &hData); + } + fprintf(stdout, "Performing minimum phase reconstruction...\n"); + ReconstructHrirs(&hData); + if(outRate != 0 && outRate != hData.mIrRate) + { + fprintf(stdout, "Resampling HRIRs...\n"); + ResampleHrirs(outRate, &hData); + } + fprintf(stdout, "Truncating minimum-phase HRIRs...\n"); + hData.mIrPoints = truncSize; + fprintf(stdout, "Synthesizing missing elevations...\n"); + if(model == HM_DATASET) + SynthesizeOnsets(&hData); + SynthesizeHrirs(&hData); + fprintf(stdout, "Normalizing final HRIRs...\n"); + NormalizeHrirs(&hData); + fprintf(stdout, "Calculating impulse delays...\n"); + CalculateHrtds(model, (radius > DEFAULT_CUSTOM_RADIUS) ? radius : hData.mRadius, &hData); + snprintf(rateStr, sizeof(rateStr), "%u", hData.mIrRate); + StrSubst(outName, "%r", rateStr, sizeof(expName), expName); + fprintf(stdout, "Creating MHR data set %s...\n", expName); + return StoreMhr(&hData, expName); +} + +static void PrintHelp(const char *argv0, FILE *ofile) +{ + fprintf(ofile, "Usage: %s [<option>...]\n\n", argv0); + fprintf(ofile, "Options:\n"); + fprintf(ofile, " -r <rate> Change the data set sample rate to the specified value and\n"); + fprintf(ofile, " resample the HRIRs accordingly.\n"); + fprintf(ofile, " -m Change the data set to mono, mirroring the left ear for the\n"); + fprintf(ofile, " right ear.\n"); + fprintf(ofile, " -f <points> Override the FFT window size (default: %u).\n", DEFAULT_FFTSIZE); + fprintf(ofile, " -e {on|off} Toggle diffuse-field equalization (default: %s).\n", (DEFAULT_EQUALIZE ? "on" : "off")); + fprintf(ofile, " -s {on|off} Toggle surface-weighted diffuse-field average (default: %s).\n", (DEFAULT_SURFACE ? "on" : "off")); + fprintf(ofile, " -l {<dB>|none} Specify a limit to the magnitude range of the diffuse-field\n"); + fprintf(ofile, " average (default: %.2f).\n", DEFAULT_LIMIT); + fprintf(ofile, " -w <points> Specify the size of the truncation window that's applied\n"); + fprintf(ofile, " after minimum-phase reconstruction (default: %u).\n", DEFAULT_TRUNCSIZE); + fprintf(ofile, " -d {dataset| Specify the model used for calculating the head-delay timing\n"); + fprintf(ofile, " sphere} values (default: %s).\n", ((DEFAULT_HEAD_MODEL == HM_DATASET) ? "dataset" : "sphere")); + fprintf(ofile, " -c <radius> Use a customized head radius measured to-ear in meters.\n"); + fprintf(ofile, " -i <filename> Specify an HRIR definition file to use (defaults to stdin).\n"); + fprintf(ofile, " -o <filename> Specify an output file. Use of '%%r' will be substituted with\n"); + fprintf(ofile, " the data set sample rate.\n"); +} + +// Standard command line dispatch. +int main(int argc, char *argv[]) +{ + const char *inName = nullptr, *outName = nullptr; + uint outRate, fftSize; + int equalize, surface; + char *end = nullptr; + ChannelModeT chanMode; + HeadModelT model; + uint truncSize; + double radius; + double limit; + int opt; + + GET_UNICODE_ARGS(&argc, &argv); + + if(argc < 2) + { + fprintf(stdout, "HRTF Processing and Composition Utility\n\n"); + PrintHelp(argv[0], stdout); + exit(EXIT_SUCCESS); + } + + outName = "./oalsoft_hrtf_%r.mhr"; + outRate = 0; + chanMode = CM_AllowStereo; + fftSize = DEFAULT_FFTSIZE; + equalize = DEFAULT_EQUALIZE; + surface = DEFAULT_SURFACE; + limit = DEFAULT_LIMIT; + truncSize = DEFAULT_TRUNCSIZE; + model = DEFAULT_HEAD_MODEL; + radius = DEFAULT_CUSTOM_RADIUS; + + while((opt=getopt(argc, argv, "r:mf:e:s:l:w:d:c:e:i:o:h")) != -1) + { + switch(opt) + { + case 'r': + outRate = static_cast<uint>(strtoul(optarg, &end, 10)); + if(end[0] != '\0' || outRate < MIN_RATE || outRate > MAX_RATE) + { + fprintf(stderr, "\nError: Got unexpected value \"%s\" for option -%c, expected between %u to %u.\n", optarg, opt, MIN_RATE, MAX_RATE); + exit(EXIT_FAILURE); + } + break; + + case 'm': + chanMode = CM_ForceMono; + break; + + case 'f': + fftSize = static_cast<uint>(strtoul(optarg, &end, 10)); + if(end[0] != '\0' || (fftSize&(fftSize-1)) || fftSize < MIN_FFTSIZE || fftSize > MAX_FFTSIZE) + { + fprintf(stderr, "\nError: Got unexpected value \"%s\" for option -%c, expected a power-of-two between %u to %u.\n", optarg, opt, MIN_FFTSIZE, MAX_FFTSIZE); + exit(EXIT_FAILURE); + } + break; + + case 'e': + if(strcmp(optarg, "on") == 0) + equalize = 1; + else if(strcmp(optarg, "off") == 0) + equalize = 0; + else + { + fprintf(stderr, "\nError: Got unexpected value \"%s\" for option -%c, expected on or off.\n", optarg, opt); + exit(EXIT_FAILURE); + } + break; + + case 's': + if(strcmp(optarg, "on") == 0) + surface = 1; + else if(strcmp(optarg, "off") == 0) + surface = 0; + else + { + fprintf(stderr, "\nError: Got unexpected value \"%s\" for option -%c, expected on or off.\n", optarg, opt); + exit(EXIT_FAILURE); + } + break; + + case 'l': + if(strcmp(optarg, "none") == 0) + limit = 0.0; + else + { + limit = strtod(optarg, &end); + if(end[0] != '\0' || limit < MIN_LIMIT || limit > MAX_LIMIT) + { + fprintf(stderr, "\nError: Got unexpected value \"%s\" for option -%c, expected between %.0f to %.0f.\n", optarg, opt, MIN_LIMIT, MAX_LIMIT); + exit(EXIT_FAILURE); + } + } + break; + + case 'w': + truncSize = static_cast<uint>(strtoul(optarg, &end, 10)); + if(end[0] != '\0' || truncSize < MIN_TRUNCSIZE || truncSize > MAX_TRUNCSIZE || (truncSize%MOD_TRUNCSIZE)) + { + fprintf(stderr, "\nError: Got unexpected value \"%s\" for option -%c, expected multiple of %u between %u to %u.\n", optarg, opt, MOD_TRUNCSIZE, MIN_TRUNCSIZE, MAX_TRUNCSIZE); + exit(EXIT_FAILURE); + } + break; + + case 'd': + if(strcmp(optarg, "dataset") == 0) + model = HM_DATASET; + else if(strcmp(optarg, "sphere") == 0) + model = HM_SPHERE; + else + { + fprintf(stderr, "\nError: Got unexpected value \"%s\" for option -%c, expected dataset or sphere.\n", optarg, opt); + exit(EXIT_FAILURE); + } + break; + + case 'c': + radius = strtod(optarg, &end); + if(end[0] != '\0' || radius < MIN_CUSTOM_RADIUS || radius > MAX_CUSTOM_RADIUS) + { + fprintf(stderr, "\nError: Got unexpected value \"%s\" for option -%c, expected between %.2f to %.2f.\n", optarg, opt, MIN_CUSTOM_RADIUS, MAX_CUSTOM_RADIUS); + exit(EXIT_FAILURE); + } + break; + + case 'i': + inName = optarg; + break; + + case 'o': + outName = optarg; + break; + + case 'h': + PrintHelp(argv[0], stdout); + exit(EXIT_SUCCESS); + + default: /* '?' */ + PrintHelp(argv[0], stderr); + exit(EXIT_FAILURE); + } + } + + int ret = ProcessDefinition(inName, outRate, chanMode, fftSize, equalize, surface, limit, + truncSize, model, radius, outName); + if(!ret) return -1; + fprintf(stdout, "Operation completed.\n"); + + return EXIT_SUCCESS; +} diff --git a/utils/makemhr/makemhr.h b/utils/makemhr/makemhr.h new file mode 100644 index 00000000..ba19efbe --- /dev/null +++ b/utils/makemhr/makemhr.h @@ -0,0 +1,128 @@ +#ifndef MAKEMHR_H +#define MAKEMHR_H + +#include <vector> +#include <complex> + + +// The maximum path length used when processing filenames. +#define MAX_PATH_LEN (256) + +// The limit to the number of 'distances' listed in the data set definition. +// Must be less than 256 +#define MAX_FD_COUNT (16) + +// The limits to the number of 'elevations' listed in the data set definition. +// Must be less than 256. +#define MIN_EV_COUNT (5) +#define MAX_EV_COUNT (181) + +// The limits for each of the 'azimuths' listed in the data set definition. +// Must be less than 256. +#define MIN_AZ_COUNT (1) +#define MAX_AZ_COUNT (255) + +// The limits for the 'distance' from source to listener for each field in +// the definition file. +#define MIN_DISTANCE (0.05) +#define MAX_DISTANCE (2.50) + +// The limits for the sample 'rate' metric in the data set definition and for +// resampling. +#define MIN_RATE (32000) +#define MAX_RATE (96000) + +// The limits for the HRIR 'points' metric in the data set definition. +#define MIN_POINTS (16) +#define MAX_POINTS (8192) + + +using uint = unsigned int; + +/* Complex double type. */ +using complex_d = std::complex<double>; + + +enum ChannelModeT : bool { + CM_AllowStereo = false, + CM_ForceMono = true +}; + +// Sample and channel type enum values. +enum SampleTypeT { + ST_S16 = 0, + ST_S24 = 1 +}; + +// Certain iterations rely on these integer enum values. +enum ChannelTypeT { + CT_NONE = -1, + CT_MONO = 0, + CT_STEREO = 1 +}; + +// Structured HRIR storage for stereo azimuth pairs, elevations, and fields. +struct HrirAzT { + double mAzimuth{0.0}; + uint mIndex{0u}; + double mDelays[2]{0.0, 0.0}; + double *mIrs[2]{nullptr, nullptr}; +}; + +struct HrirEvT { + double mElevation{0.0}; + uint mIrCount{0u}; + uint mAzCount{0u}; + HrirAzT *mAzs{nullptr}; +}; + +struct HrirFdT { + double mDistance{0.0}; + uint mIrCount{0u}; + uint mEvCount{0u}; + uint mEvStart{0u}; + HrirEvT *mEvs{nullptr}; +}; + +// The HRIR metrics and data set used when loading, processing, and storing +// the resulting HRTF. +struct HrirDataT { + uint mIrRate{0u}; + SampleTypeT mSampleType{ST_S24}; + ChannelTypeT mChannelType{CT_NONE}; + uint mIrPoints{0u}; + uint mFftSize{0u}; + uint mIrSize{0u}; + double mRadius{0.0}; + uint mIrCount{0u}; + uint mFdCount{0u}; + + std::vector<double> mHrirsBase; + std::vector<HrirEvT> mEvsBase; + std::vector<HrirAzT> mAzsBase; + + std::vector<HrirFdT> mFds; +}; + + +int PrepareHrirData(const uint fdCount, const double (&distances)[MAX_FD_COUNT], const uint (&evCounts)[MAX_FD_COUNT], const uint azCounts[MAX_FD_COUNT * MAX_EV_COUNT], HrirDataT *hData); +void MagnitudeResponse(const uint n, const complex_d *in, double *out); +void FftForward(const uint n, complex_d *inout); +void FftInverse(const uint n, complex_d *inout); + + +// The resampler metrics and FIR filter. +struct ResamplerT { + uint mP, mQ, mM, mL; + std::vector<double> mF; +}; + +void ResamplerSetup(ResamplerT *rs, const uint srcRate, const uint dstRate); +void ResamplerRun(ResamplerT *rs, const uint inN, const double *in, const uint outN, double *out); + + +// Performs linear interpolation. +inline double Lerp(const double a, const double b, const double f) +{ return a + f * (b - a); } + +#endif /* MAKEMHR_H */ diff --git a/utils/openal-info.c b/utils/openal-info.c index 12dc6311..cdce77e0 100644 --- a/utils/openal-info.c +++ b/utils/openal-info.c @@ -53,7 +53,7 @@ static WCHAR *FromUTF8(const char *str) if((len=MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0)) > 0) { - out = calloc(sizeof(WCHAR), len); + out = calloc(sizeof(WCHAR), (unsigned int)(len)); MultiByteToWideChar(CP_UTF8, 0, str, -1, out, len); } return out; @@ -124,7 +124,7 @@ static void printList(const char *list, char separator) next = strchr(list, separator); if(next) { - len = next-list; + len = (size_t)(next-list); do { next++; } while(*next == separator); @@ -223,7 +223,7 @@ static void printHRTFInfo(ALCdevice *device) return; } - alcGetStringiSOFT = alcGetProcAddress(device, "alcGetStringiSOFT"); + alcGetStringiSOFT = (LPALCGETSTRINGISOFT)alcGetProcAddress(device, "alcGetStringiSOFT"); alcGetIntegerv(device, ALC_NUM_HRTF_SPECIFIERS_SOFT, 1, &num_hrtfs); if(!num_hrtfs) @@ -263,7 +263,7 @@ static void printResamplerInfo(void) return; } - alGetStringiSOFT = alGetProcAddress("alGetStringiSOFT"); + alGetStringiSOFT = (LPALGETSTRINGISOFT)alGetProcAddress("alGetStringiSOFT"); num_resamplers = alGetInteger(AL_NUM_RESAMPLERS_SOFT); def_resampler = alGetInteger(AL_DEFAULT_RESAMPLER_SOFT); diff --git a/utils/sofa-info.cpp b/utils/sofa-info.cpp new file mode 100644 index 00000000..bc5b709a --- /dev/null +++ b/utils/sofa-info.cpp @@ -0,0 +1,371 @@ +/* + * SOFA info utility for inspecting SOFA file metrics and determining HRTF + * utility compatible layouts. + * + * Copyright (C) 2018-2019 Christopher Fitzgerald + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Or visit: http://www.gnu.org/licenses/old-licenses/gpl-2.0.html + */ + +#include <stdio.h> + +#include <array> +#include <cmath> +#include <memory> +#include <vector> + +#include <mysofa.h> + +#include "win_main_utf8.h" + + +using uint = unsigned int; +using double3 = std::array<double,3>; + +struct MySofaDeleter { + void operator()(MYSOFA_HRTF *sofa) { mysofa_free(sofa); } +}; +using MySofaHrtfPtr = std::unique_ptr<MYSOFA_HRTF,MySofaDeleter>; + +// Per-field measurement info. +struct HrirFdT { + double mDistance{0.0}; + uint mEvCount{0u}; + uint mEvStart{0u}; + std::vector<uint> mAzCounts; +}; + +static const char *SofaErrorStr(int err) +{ + switch(err) + { + case MYSOFA_OK: return "OK"; + case MYSOFA_INVALID_FORMAT: return "Invalid format"; + case MYSOFA_UNSUPPORTED_FORMAT: return "Unsupported format"; + case MYSOFA_INTERNAL_ERROR: return "Internal error"; + case MYSOFA_NO_MEMORY: return "Out of memory"; + case MYSOFA_READ_ERROR: return "Read error"; + } + return "Unknown"; +} + +static void PrintSofaAttributes(const char *prefix, struct MYSOFA_ATTRIBUTE *attribute) +{ + while(attribute) + { + fprintf(stdout, "%s.%s: %s\n", prefix, attribute->name, attribute->value); + attribute = attribute->next; + } +} + +static void PrintSofaArray(const char *prefix, struct MYSOFA_ARRAY *array) +{ + PrintSofaAttributes(prefix, array->attributes); + + for(uint i{0u};i < array->elements;i++) + fprintf(stdout, "%s[%u]: %.6f\n", prefix, i, array->values[i]); +} + +/* Produces a sorted array of unique elements from a particular axis of the + * triplets array. The filters are used to focus on particular coordinates + * of other axes as necessary. The epsilons are used to constrain the + * equality of unique elements. + */ +static uint GetUniquelySortedElems(const uint m, const double3 *aers, const uint axis, + const double *const (&filters)[3], const double (&epsilons)[3], double *elems) +{ + uint count{0u}; + for(uint i{0u};i < m;++i) + { + const double elem{aers[i][axis]}; + + uint j; + for(j = 0;j < 3;j++) + { + if(filters[j] && std::fabs(aers[i][j] - *filters[j]) > epsilons[j]) + break; + } + if(j < 3) + continue; + + for(j = 0;j < count;j++) + { + const double delta{elem - elems[j]}; + + if(delta > epsilons[axis]) + continue; + + if(delta >= -epsilons[axis]) + break; + + for(uint k{count};k > j;k--) + elems[k] = elems[k - 1]; + + elems[j] = elem; + count++; + break; + } + + if(j >= count) + elems[count++] = elem; + } + + return count; +} + +/* Given a list of elements, this will produce the smallest step size that + * can uniformly cover a fair portion of the list. Ideally this will be over + * half, but in degenerate cases this can fall to a minimum of 5 (the lower + * limit on elevations necessary to build a layout). + */ +static double GetUniformStepSize(const double epsilon, const uint m, const double *elems) +{ + auto steps = std::vector<double>(m, 0.0); + auto counts = std::vector<uint>(m, 0u); + uint count{0u}; + + for(uint stride{1u};stride < m/2;stride++) + { + for(uint i{0u};i < m-stride;i++) + { + const double step{elems[i + stride] - elems[i]}; + + uint j; + for(j = 0;j < count;j++) + { + if(std::fabs(step - steps[j]) < epsilon) + { + counts[j]++; + break; + } + } + + if(j >= count) + { + steps[j] = step; + counts[j] = 1; + count++; + } + } + + for(uint i{1u};i < count;i++) + { + if(counts[i] > counts[0]) + { + steps[0] = steps[i]; + counts[0] = counts[i]; + } + } + + count = 1; + + if(counts[0] > m/2) + break; + } + + if(counts[0] > 255) + { + uint i{2u}; + while(counts[0]/i > 255 && (counts[0]%i) != 0) + ++i; + counts[0] /= i; + steps[0] *= i; + } + if(counts[0] > 5) + return steps[0]; + return 0.0; +} + +/* Attempts to produce a compatible layout. Most data sets tend to be + * uniform and have the same major axis as used by OpenAL Soft's HRTF model. + * This will remove outliers and produce a maximally dense layout when + * possible. Those sets that contain purely random measurements or use + * different major axes will fail. + */ +static void PrintCompatibleLayout(const uint m, const float *xyzs) +{ + auto aers = std::vector<double3>(m, double3{}); + auto elems = std::vector<double>(m, {}); + + fprintf(stdout, "\n"); + + for(uint i{0u};i < m;++i) + { + float aer[3]{xyzs[i*3], xyzs[i*3 + 1], xyzs[i*3 + 2]}; + mysofa_c2s(&aer[0]); + aers[i][0] = aer[0]; + aers[i][1] = aer[1]; + aers[i][2] = aer[2]; + } + + uint fdCount{GetUniquelySortedElems(m, aers.data(), 2, { nullptr, nullptr, nullptr }, + { 0.1, 0.1, 0.001 }, elems.data())}; + if(fdCount > (m / 3)) + { + fprintf(stdout, "Incompatible layout (inumerable radii).\n"); + return; + } + + std::vector<HrirFdT> fds(fdCount); + for(uint fi{0u};fi < fdCount;fi++) + fds[fi].mDistance = elems[fi]; + + for(uint fi{0u};fi < fdCount;fi++) + { + const double dist{fds[fi].mDistance}; + uint evCount{GetUniquelySortedElems(m, aers.data(), 1, { nullptr, nullptr, &dist }, + { 0.1, 0.1, 0.001 }, elems.data())}; + + if(evCount > (m / 3)) + { + fprintf(stdout, "Incompatible layout (innumerable elevations).\n"); + return; + } + + double step{GetUniformStepSize(0.1, evCount, elems.data())}; + if(step <= 0.0) + { + fprintf(stdout, "Incompatible layout (non-uniform elevations).\n"); + return; + } + + uint evStart{0u}; + for(uint ei{0u};ei < evCount;ei++) + { + double ev{90.0 + elems[ei]}; + double eif{std::round(ev / step)}; + const uint ev_start{static_cast<uint>(eif)}; + + if(std::fabs(eif - static_cast<double>(ev_start)) < (0.1/step)) + { + evStart = ev_start; + break; + } + } + + evCount = static_cast<uint>(std::round(180.0 / step)) + 1; + if(evCount < 5) + { + fprintf(stdout, "Incompatible layout (too few uniform elevations).\n"); + return; + } + + fds[fi].mEvCount = evCount; + fds[fi].mEvStart = evStart; + fds[fi].mAzCounts.resize(evCount); + auto &azCounts = fds[fi].mAzCounts; + + for(uint ei{evStart};ei < evCount;ei++) + { + double ev{-90.0 + static_cast<double>(ei)*180.0/static_cast<double>(evCount - 1)}; + uint azCount{GetUniquelySortedElems(m, aers.data(), 0, { nullptr, &ev, &dist }, + { 0.1, 0.1, 0.001 }, elems.data())}; + + if(azCount > (m / 3)) + { + fprintf(stdout, "Incompatible layout (innumerable azimuths).\n"); + return; + } + + if(ei > 0 && ei < (evCount - 1)) + { + step = GetUniformStepSize(0.1, azCount, elems.data()); + if(step <= 0.0) + { + fprintf(stdout, "Incompatible layout (non-uniform azimuths).\n"); + return; + } + + azCounts[ei] = static_cast<uint>(std::round(360.0f / step)); + } + else if(azCount != 1) + { + fprintf(stdout, "Incompatible layout (non-singular poles).\n"); + return; + } + else + { + azCounts[ei] = 1; + } + } + + for(uint ei{0u};ei < evStart;ei++) + azCounts[ei] = azCounts[evCount - ei - 1]; + } + + fprintf(stdout, "Compatible Layout:\n\ndistance = %.3f", fds[0].mDistance); + + for(uint fi{1u};fi < fdCount;fi++) + fprintf(stdout, ", %.3f", fds[fi].mDistance); + + fprintf(stdout, "\nazimuths = "); + for(uint fi{0u};fi < fdCount;fi++) + { + for(uint ei{0u};ei < fds[fi].mEvCount;ei++) + fprintf(stdout, "%d%s", fds[fi].mAzCounts[ei], + (ei < (fds[fi].mEvCount - 1)) ? ", " : + (fi < (fdCount - 1)) ? ";\n " : "\n"); + } +} + +// Load and inspect the given SOFA file. +static void SofaInfo(const char *filename) +{ + int err; + MySofaHrtfPtr sofa{mysofa_load(filename, &err)}; + if(!sofa) + { + fprintf(stdout, "Error: Could not load source file '%s'.\n", filename); + return; + } + + /* NOTE: Some valid SOFA files are failing this check. */ + err = mysofa_check(sofa.get()); + if(err != MYSOFA_OK) + fprintf(stdout, "Warning: Supposedly malformed source file '%s' (%s).\n", filename, + SofaErrorStr(err)); + + mysofa_tocartesian(sofa.get()); + + PrintSofaAttributes("Info", sofa->attributes); + + fprintf(stdout, "Measurements: %u\n", sofa->M); + fprintf(stdout, "Receivers: %u\n", sofa->R); + fprintf(stdout, "Emitters: %u\n", sofa->E); + fprintf(stdout, "Samples: %u\n", sofa->N); + + PrintSofaArray("SampleRate", &sofa->DataSamplingRate); + PrintSofaArray("DataDelay", &sofa->DataDelay); + + PrintCompatibleLayout(sofa->M, sofa->SourcePosition.values); +} + +int main(int argc, char *argv[]) +{ + GET_UNICODE_ARGS(&argc, &argv); + + if(argc != 2) + { + fprintf(stdout, "Usage: %s <sofa-file>\n", argv[0]); + return 0; + } + + SofaInfo(argv[1]); + + return 0; +} + |