Skip to content

Commit

Permalink
feat(amir): Build AMIR files from SOFA files.
Browse files Browse the repository at this point in the history
Signed-off-by: Axel Nana <[email protected]>
  • Loading branch information
na2axl committed Oct 3, 2024
1 parent 5de0b6c commit 8d177ae
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 83 deletions.
6 changes: 6 additions & 0 deletions include/SparkyStudios/Audio/Amplitude/HRTF/HRIRSphere.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ namespace SparkyStudios::Audio::Amplitude
*/
eHRIRSphereDatasetModel_SADIE = 2,

/**
* @brief The HRIR sphere uses data from a SOFA (Spatially Oriented Format for Acoustics) file.
* (https://www.sofaconventions.org/).
*/
eHRIRSphereDatasetModel_SOFA = 3,

/**
* @brief Invalid HRIR sphere dataset model.
*/
Expand Down
5 changes: 4 additions & 1 deletion tools/amir/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ cmake_minimum_required(VERSION 3.20)

project(amir)

find_package(mysofa CONFIG REQUIRED)

add_executable(amir main.cpp convhull_3d.h)

target_link_libraries(amir
Static
PRIVATE
Static mysofa::mysofa-static
)

add_dependencies(amir
Expand Down
245 changes: 164 additions & 81 deletions tools/amir/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

#include <SparkyStudios/Audio/Amplitude/Amplitude.h>

#include <mysofa.h>

#include <Core/Codecs/WAV/Codec.h>
#include <DSP/Filters/BiquadResonantFilter.h>
#include <Utils/Utils.h>
Expand All @@ -35,7 +37,7 @@ struct ProcessingState
bool enabled = false;
AmUInt32 targetSampleRate = 44100;
} resampling;
HRIRSphereDatasetModel datasetModel = eHRIRSphereDatasetModel_IRCAM;
HRIRSphereDatasetModel datasetModel = eHRIRSphereDatasetModel_SOFA;
};

static constexpr AmUInt32 kCurrentVersion = 1;
Expand Down Expand Up @@ -262,7 +264,41 @@ int parseFileName_SADIE(const AmOsString& fileName, SphericalPosition& position)
return EXIT_SUCCESS;
}

static int process(const AmOsString& inFileName, const AmOsString& outFileName, const ProcessingState& state)
void processVertex(
const AudioBuffer& buffer, const AmVec3& position, AmUInt32 irLength, AmReal32 sampleRate, bool mirror, HRIRSphereVertex& vertex)
{
vertex.m_Position = position;
vertex.m_LeftIR.resize(irLength);
vertex.m_RightIR.resize(irLength);

const auto& leftChannel = buffer[0];
const auto& rightChannel = buffer[1];

std::memcpy(vertex.m_LeftIR.data(), !mirror ? leftChannel.begin() : rightChannel.begin(), irLength * sizeof(AmReal32));
std::memcpy(vertex.m_RightIR.data(), !mirror ? rightChannel.begin() : leftChannel.begin(), irLength * sizeof(AmReal32));
}

void resampleIR(const ProcessingState& state, AudioBuffer& buffer, AmUInt32& sampleRate, AmUInt64& irLength)
{
if (!state.resampling.enabled)
return;

auto* resampler = Resampler::Construct("default");
resampler->Initialize(2, sampleRate, state.resampling.targetSampleRate);

auto resampledTotalFrames = resampler->GetExpectedOutputFrames(irLength);
AudioBuffer resampledBuffer(resampledTotalFrames, 2);

resampler->Process(buffer, irLength, resampledBuffer, resampledTotalFrames);

irLength = resampledTotalFrames;
sampleRate = state.resampling.targetSampleRate;

buffer = resampledBuffer;
Resampler::Destruct("default", resampler);
}

int process(const AmOsString& inFileName, const AmOsString& outFileName, const ProcessingState& state)
{
const std::filesystem::path datasetPath(inFileName);
const std::filesystem::path packagePath(outFileName);
Expand All @@ -273,85 +309,77 @@ static int process(const AmOsString& inFileName, const AmOsString& outFileName,
return EXIT_FAILURE;
}

if (!is_directory(datasetPath))
{
log(stderr, "The path " AM_OS_CHAR_FMT " is not a directory.\n", datasetPath.native().c_str());
return EXIT_FAILURE;
}

if (state.datasetModel >= eHRIRSphereDatasetModel_Invalid)
{
log(stderr, "Unsupported dataset model.\n");
return EXIT_FAILURE;
}

DiskFile packageFile(absolute(packagePath), eFOM_WRITE);

AmUInt32 sampleRate = 0;
AmUInt32 irLength = 0;

std::set<std::filesystem::path> sorted_by_name;
AmUInt64 irLength = 0;

std::vector<HRIRSphereVertex> vertices;
std::vector<AmUInt32> indices;

for (const auto& file : std::filesystem::recursive_directory_iterator(datasetPath))
{
if (file.is_directory())
continue;
DiskFile packageFile(absolute(packagePath), eFOM_WRITE);

// Avoid known bad files
if (state.datasetModel != eHRIRSphereDatasetModel_SOFA)
{
if (!is_directory(datasetPath))
{
if (file.path().filename() == AM_OS_STRING(".DS_Store"))
continue;
log(stderr, "The path " AM_OS_CHAR_FMT " is not a directory.\n", datasetPath.native().c_str());
return EXIT_FAILURE;
}

sorted_by_name.insert(file);
}
std::set<std::filesystem::path> sorted_by_name;

AmUniquePtr<MemoryPoolKind::Default, Codec> wavCodec(amnew(WAVCodec));
for (const auto& file : std::filesystem::recursive_directory_iterator(datasetPath))
{
if (file.is_directory())
continue;

std::vector<AmVec3> positions;
// Avoid known bad files
{
if (file.path().filename() == AM_OS_STRING(".DS_Store"))
continue;
}

for (const auto& entry : sorted_by_name)
{
const auto& path = entry.native();
sorted_by_name.insert(file);
}

if (state.verbose)
log(stdout, "Processing %s.\n", path.c_str());
AmUniquePtr<MemoryPoolKind::Default, Codec> wavCodec(amnew(WAVCodec));

SphericalPosition spherical;
std::vector<AmVec3> positions;

if (state.datasetModel == eHRIRSphereDatasetModel_IRCAM &&
parseFileName_IRCAM(entry.filename().native(), spherical) == EXIT_FAILURE)
for (const auto& entry : sorted_by_name)
{
log(stderr, "\tInvalid file name: %s.\n", path.c_str());
return EXIT_FAILURE;
}
const auto& path = entry.native();

if (state.datasetModel == eHRIRSphereDatasetModel_MIT && parseFileName_MIT(entry.filename().native(), spherical) == EXIT_FAILURE)
{
log(stderr, "\tInvalid file name: %s.\n", path.c_str());
return EXIT_FAILURE;
}
if (state.verbose)
log(stdout, "Processing %s.\n", path.c_str());

if (state.datasetModel == eHRIRSphereDatasetModel_SADIE &&
parseFileName_SADIE(entry.filename().native(), spherical) == EXIT_FAILURE)
{
log(stderr, "\tInvalid file name: %s.\n", path.c_str());
return EXIT_FAILURE;
}
SphericalPosition spherical;

const AmUInt32 max = state.datasetModel == eHRIRSphereDatasetModel_MIT ? 2 : 1;
for (AmUInt32 i = 0; i < max; ++i)
{
spherical.SetAzimuth(spherical.GetAzimuth() * (i * -2.0f + 1.0f));
const AmVec3 position = spherical.ToCartesian();
if (state.datasetModel == eHRIRSphereDatasetModel_IRCAM &&
parseFileName_IRCAM(entry.filename().native(), spherical) == EXIT_FAILURE)
{
log(stderr, "\tInvalid file name: %s.\n", path.c_str());
return EXIT_FAILURE;
}

if (const auto& it = std::find(positions.begin(), positions.end(), position); it != positions.end())
continue; // Do not duplicate borders
if (state.datasetModel == eHRIRSphereDatasetModel_MIT &&
parseFileName_MIT(entry.filename().native(), spherical) == EXIT_FAILURE)
{
log(stderr, "\tInvalid file name: %s.\n", path.c_str());
return EXIT_FAILURE;
}

positions.push_back(position);
if (state.datasetModel == eHRIRSphereDatasetModel_SADIE &&
parseFileName_SADIE(entry.filename().native(), spherical) == EXIT_FAILURE)
{
log(stderr, "\tInvalid file name: %s.\n", path.c_str());
return EXIT_FAILURE;
}

auto* decoder = wavCodec->CreateDecoder();
std::shared_ptr<File> file = std::make_shared<DiskFile>(absolute(entry));
Expand Down Expand Up @@ -379,46 +407,95 @@ static int process(const AmOsString& inFileName, const AmOsString& outFileName,
AudioBuffer buffer(totalFrames, 2);
decoder->Load(&buffer);

if (state.resampling.enabled)
resampleIR(state, buffer, sampleRate, irLength);

const AmUInt32 max = state.datasetModel == eHRIRSphereDatasetModel_MIT ? 2 : 1;
for (AmUInt32 i = 0; i < max; ++i)
{
auto* resampler = Resampler::Construct("default");
resampler->Initialize(2, sampleRate, state.resampling.targetSampleRate);
spherical.SetAzimuth(spherical.GetAzimuth() * (i * -2.0f + 1.0f));
const AmVec3 position = spherical.ToCartesian();

auto resampledTotalFrames = resampler->GetExpectedOutputFrames(totalFrames);
AudioBuffer resampledBuffer(resampledTotalFrames, 2);
if (const auto& it = std::find(positions.begin(), positions.end(), position); it != positions.end())
continue; // Do not duplicate borders

resampler->Process(buffer, totalFrames, resampledBuffer, resampledTotalFrames);
positions.push_back(position);

totalFrames = resampledTotalFrames;
sampleRate = state.resampling.targetSampleRate;
HRIRSphereVertex vertex;
processVertex(buffer, position, irLength, sampleRate, i != 0, vertex);
estimateITD(vertex, irLength, sampleRate);

buffer = resampledBuffer;
Resampler::Destruct("default", resampler);
vertices.push_back(vertex);

log(stdout, "\tProcessed %s -> {%f, %f, %f}.\n", path.c_str(), vertex.m_Position.X, vertex.m_Position.Y,
vertex.m_Position.Z);
}

HRIRSphereVertex vertex;
vertex.m_Position = position;
vertex.m_LeftIR.resize(totalFrames);
vertex.m_RightIR.resize(totalFrames);
buffer.Clear();
wavCodec->DestroyDecoder(decoder);
}
}
else
{
AmInt32 err = 0;
MYSOFA_HRTF* hrtf = nullptr;
MYSOFA_ATTRIBUTE* tmp_a = nullptr;

const auto& leftChannel = buffer[0];
const auto& rightChannel = buffer[1];
hrtf = mysofa_load(datasetPath.string().c_str(), &err);

for (AmUInt32 j = 0; j < totalFrames; ++j)
switch (err)
{
case MYSOFA_OK:
{
vertex.m_LeftIR[j] = i == 0 ? leftChannel[j] : rightChannel[j];
vertex.m_RightIR[j] = i == 0 ? rightChannel[j] : leftChannel[j];
}
if (state.resampling.enabled)
mysofa_resample(hrtf, state.resampling.targetSampleRate);

estimateITD(vertex, totalFrames, sampleRate);
vertices.reserve(hrtf->M);

vertices.push_back(vertex);
irLength = hrtf->N;
sampleRate = hrtf->DataSamplingRate.values[0];

buffer.Clear();
wavCodec->DestroyDecoder(decoder);
if (hrtf->R != 2)
{
log(stderr, "Unsupported number of channels: %d. Only 2 channels is supported.\n", hrtf->R);
return EXIT_FAILURE;
}

const AmUInt32 bufferSize = hrtf->N * hrtf->R;

const AmVec3 listenerForward =
AM_V3(hrtf->ListenerView.values[0], hrtf->ListenerView.values[1], hrtf->ListenerView.values[2]);
const AmVec3 listenerUp = AM_V3(hrtf->ListenerUp.values[0], hrtf->ListenerUp.values[1], hrtf->ListenerUp.values[2]);

AudioBuffer buffer(hrtf->N, hrtf->R);

for (AmUInt32 i = 0; i < hrtf->M; ++i)
{
std::memcpy(buffer.GetData().GetBuffer(), hrtf->DataIR.values + i * bufferSize, bufferSize * sizeof(AmReal32));

const AmString type = mysofa_getAttribute(hrtf->SourcePosition.attributes, "Type");

AmReal32* rawPosition = hrtf->SourcePosition.values + i * 3;

log(stdout, "\tProcessed %s -> {%f, %f, %f}.\n", path.c_str(), vertex.m_Position.X, vertex.m_Position.Y, vertex.m_Position.Z);
if (type == "spherical")
mysofa_s2c(rawPosition);

AmVec3 position = AM_V3(rawPosition[0], rawPosition[1], rawPosition[2]);

HRIRSphereVertex vertex;
processVertex(buffer, position, irLength, sampleRate, false, vertex);
estimateITD(vertex, irLength, sampleRate);

vertices.push_back(vertex);

buffer.Clear();

log(stdout, "Processed SOFA measurement %u -> {%f, %f, %f}.\n", i, rawPosition[0], rawPosition[1], rawPosition[2]);
}
break;
}
}

mysofa_free(hrtf);
}

log(stdout, "Building mesh...\n");
Expand Down Expand Up @@ -580,10 +657,14 @@ int main(int argc, char* argv[])
log(stdout, " -[rR] freq: \tResample HRIR data to the target frequency.\n");
log(stdout, " -[mM]: \tThe dataset model to use.\n");
log(stdout, " \tThe default value is 0. The available values are:\n");
log(stdout, " 0: \tIRCAN (LISTEN) dataset (http://recherche.ircam.fr/equipes/salles/listen/download.html).\n");
log(stdout, " 0: \tIRCAM (LISTEN) dataset (http://recherche.ircam.fr/equipes/salles/listen/download.html).\n");
log(stdout, " 1: \tMIT (KEMAR) dataset (http://sound.media.mit.edu/resources/KEMAR.html).\n");
log(stdout, " 2: \tSADIE II dataset (https://www.york.ac.uk/sadie-project/database.html).\n");
log(stdout, " 3: \tSOFA file (https://www.sofaconventions.org).\n");
log(stdout, "\n");
log(stdout, "Example: amir -m 1 /path/to/mit/dataset/ output_package.amir\n");
log(stdout, "Example:\n");
log(stdout, "\tamir -m 1 /path/to/mit/dataset/ output_asset.amir\n");
log(stdout, "\tamir -m 3 /path/to/mit/file.sofa output_asset.amir\n");
log(stdout, "\n");
// clang-format on

Expand All @@ -596,5 +677,7 @@ int main(int argc, char* argv[])

Engine::UnregisterDefaultPlugins();

MemoryManager::Deinitialize();

return res;
}
6 changes: 5 additions & 1 deletion vcpkg.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/master/docs/vcpkg.schema.json",
"builtin-baseline": "198d68dbcc6c907cb3d0b9b1d93c3df6ecf93c62",
"builtin-baseline": "76d153790caf0592fce8fc4484aa6db18c7d00d4",
"name": "amplitude-audio-sdk",
"description": "A powerful and cross-platform audio engine, optimized for games.",
"version": "1.0",
Expand Down Expand Up @@ -38,6 +38,10 @@
{
"name": "miniaudio",
"version>=": "0.11.21"
},
{
"name": "libmysofa",
"version>=": "1.3.2"
}
]
}

0 comments on commit 8d177ae

Please sign in to comment.