From 4112fe9120b2a1d822168e9a90545ad1e57d3a3d Mon Sep 17 00:00:00 2001 From: Steven Atkinson Date: Fri, 20 Feb 2026 17:41:55 -0800 Subject: [PATCH 1/3] Add render tool for processing WAV files through .nam models - Add AudioDSPTools as submodule for WAV input (dsp::wav::Load) - Add tools/render: loads model, reads input WAV, processes, writes 32-bit float output - Usage: render [output.wav] - Supports mono input; validates sample rate matches model Co-authored-by: Cursor --- .gitmodules | 3 + Dependencies/AudioDSPTools | 1 + tools/CMakeLists.txt | 30 ++++++- tools/render.cpp | 159 +++++++++++++++++++++++++++++++++++++ 4 files changed, 192 insertions(+), 1 deletion(-) create mode 160000 Dependencies/AudioDSPTools create mode 100644 tools/render.cpp diff --git a/.gitmodules b/.gitmodules index 11c19841..f49ce6e8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "Dependencies/eigen"] path = Dependencies/eigen url = https://gitlab.com/libeigen/eigen +[submodule "Dependencies/AudioDSPTools"] + path = Dependencies/AudioDSPTools + url = https://github.com/sdatkinson/AudioDSPTools.git diff --git a/Dependencies/AudioDSPTools b/Dependencies/AudioDSPTools new file mode 160000 index 00000000..0827c6c2 --- /dev/null +++ b/Dependencies/AudioDSPTools @@ -0,0 +1 @@ +Subproject commit 0827c6c2fc0deced568536142ea86f189e0b98a1 diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 8118e085..2c3ddabe 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -4,14 +4,42 @@ file(GLOB_RECURSE NAM_SOURCES ../NAM/*.cpp ../NAM/*.c ../NAM*.h) set(TOOLS benchmodel) add_custom_target(tools ALL - DEPENDS ${TOOLS}) + DEPENDS ${TOOLS} render) + +set(AUDIO_DSP_TOOLS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../Dependencies/AudioDSPTools") +set(AUDIO_DSP_TOOLS_WAV_SOURCES "${AUDIO_DSP_TOOLS_DIR}/dsp/wav.cpp") include_directories(tools ..) include_directories(tools ${NAM_DEPS_PATH}/eigen) include_directories(tools ${NAM_DEPS_PATH}/nlohmann) +include_directories(tools ${AUDIO_DSP_TOOLS_DIR}/dsp) add_executable(loadmodel loadmodel.cpp ${NAM_SOURCES}) add_executable(benchmodel benchmodel.cpp ${NAM_SOURCES}) +add_executable(render render.cpp ${NAM_SOURCES} ${AUDIO_DSP_TOOLS_WAV_SOURCES}) +target_compile_features(render PUBLIC cxx_std_20) +# AudioDSPTools wav.cpp has sign-compare issues; don't fail build +set_source_files_properties(${AUDIO_DSP_TOOLS_WAV_SOURCES} PROPERTIES COMPILE_FLAGS "-Wno-error") +set_target_properties(render PROPERTIES + CXX_VISIBILITY_PRESET hidden + INTERPROCEDURAL_OPTIMIZATION TRUE + PREFIX "" +) +if (CMAKE_SYSTEM_NAME STREQUAL "Windows") + target_compile_definitions(render PRIVATE NOMINMAX WIN32_LEAN_AND_MEAN) +endif() +if (MSVC) + target_compile_options(render PRIVATE + "$<$:/W4>" + "$<$:/O2>" + ) +else() + target_compile_options(render PRIVATE + -Wall -Wextra -Wpedantic -Wstrict-aliasing -Wunreachable-code -Weffc++ -Wno-unused-parameter + "$<$:-Og;-ggdb;-Werror>" + "$<$:-Ofast>" + ) +endif() add_executable(run_tests run_tests.cpp test/allocation_tracking.cpp ${NAM_SOURCES}) # Compile run_tests without optimizations to ensure allocation tracking works correctly # Also ensure assertions are enabled (NDEBUG is not defined) so tests actually run diff --git a/tools/render.cpp b/tools/render.cpp new file mode 100644 index 00000000..77836b41 --- /dev/null +++ b/tools/render.cpp @@ -0,0 +1,159 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "NAM/dsp.h" +#include "NAM/get_dsp.h" +#include "wav.h" + +namespace +{ +// Write mono 32-bit float WAV file (IEEE float format 3). +bool SaveWavFloat32(const char* fileName, const float* samples, size_t numSamples, double sampleRate) +{ + std::ofstream out(fileName, std::ios::binary); + if (!out.is_open()) + { + std::cerr << "Error: Failed to open output file " << fileName << "\n"; + return false; + } + + const uint32_t dataSize = static_cast(numSamples * sizeof(float)); + const uint32_t chunkSize = 36 + dataSize; + + // RIFF header + out.write("RIFF", 4); + out.write(reinterpret_cast(&chunkSize), 4); + out.write("WAVE", 4); + + // fmt chunk (16 bytes for PCM/IEEE) + const uint32_t fmtSize = 16; + out.write("fmt ", 4); + out.write(reinterpret_cast(&fmtSize), 4); + const uint16_t audioFormat = 3; // IEEE float + out.write(reinterpret_cast(&audioFormat), 2); + const uint16_t numChannels = 1; + out.write(reinterpret_cast(&numChannels), 2); + const uint32_t sr = static_cast(sampleRate); + out.write(reinterpret_cast(&sr), 4); + const uint32_t byteRate = sr * sizeof(float); + out.write(reinterpret_cast(&byteRate), 4); + const uint16_t blockAlign = sizeof(float); + out.write(reinterpret_cast(&blockAlign), 2); + const uint16_t bitsPerSample = 32; + out.write(reinterpret_cast(&bitsPerSample), 2); + + // data chunk + out.write("data", 4); + out.write(reinterpret_cast(&dataSize), 4); + out.write(reinterpret_cast(samples), dataSize); + + return out.good(); +} + +} // namespace + +int main(int argc, char* argv[]) +{ + if (argc < 3 || argc > 4) + { + std::cerr << "Usage: render [output.wav]\n"; + return 1; + } + + const char* modelPath = argv[1]; + const char* inputPath = argv[2]; + const char* outputPath = (argc >= 4) ? argv[3] : "output.wav"; + + std::cerr << "Loading model [" << modelPath << "]\n"; + auto model = nam::get_dsp(std::filesystem::path(modelPath)); + if (!model) + { + std::cerr << "Failed to load model\n"; + return 1; + } + std::cerr << "Model loaded successfully\n"; + + std::vector inputAudio; + double inputSampleRate = 0.0; + auto loadResult = dsp::wav::Load(inputPath, inputAudio, inputSampleRate); + if (loadResult != dsp::wav::LoadReturnCode::SUCCESS) + { + std::cerr << "Failed to load input WAV: " << dsp::wav::GetMsgForLoadReturnCode(loadResult) << "\n"; + return 1; + } + + const double expectedRate = model->GetExpectedSampleRate(); + if (expectedRate > 0 && std::abs(inputSampleRate - expectedRate) > 0.5) + { + std::cerr << "Error: Input WAV sample rate (" << inputSampleRate + << " Hz) does not match model expected rate (" << expectedRate << " Hz)\n"; + return 1; + } + + const double sampleRate = expectedRate > 0 ? expectedRate : inputSampleRate; + const int bufferSize = 64; + model->Reset(sampleRate, bufferSize); + + const int inChannels = model->NumInputChannels(); + const int outChannels = model->NumOutputChannels(); + + if (inChannels != 1) + { + std::cerr << "Error: render tool currently supports mono input only (model has " << inChannels + << " input channels)\n"; + return 1; + } + + std::vector> inputBuffers(inChannels); + std::vector> outputBuffers(outChannels); + std::vector inputPtrs(inChannels); + std::vector outputPtrs(outChannels); + + for (int ch = 0; ch < inChannels; ch++) + { + inputBuffers[ch].resize(bufferSize, 0.0); + inputPtrs[ch] = inputBuffers[ch].data(); + } + for (int ch = 0; ch < outChannels; ch++) + { + outputBuffers[ch].resize(bufferSize, 0.0); + outputPtrs[ch] = outputBuffers[ch].data(); + } + + std::vector outputAudio; + outputAudio.reserve(static_cast(outChannels) * inputAudio.size()); + + size_t readPos = 0; + const size_t totalSamples = inputAudio.size(); + + while (readPos < totalSamples) + { + const size_t toRead = std::min(static_cast(bufferSize), totalSamples - readPos); + + for (size_t i = 0; i < toRead; i++) + inputBuffers[0][i] = static_cast(inputAudio[readPos + i]); + for (size_t i = toRead; i < static_cast(bufferSize); i++) + inputBuffers[0][i] = 0; + + model->process(inputPtrs.data(), outputPtrs.data(), static_cast(toRead)); + + for (size_t i = 0; i < toRead; i++) + outputAudio.push_back(static_cast(outputBuffers[0][i])); + + readPos += toRead; + } + + if (!SaveWavFloat32(outputPath, outputAudio.data(), outputAudio.size(), sampleRate)) + { + return 1; + } + + std::cerr << "Wrote " << outputAudio.size() << " samples to " << outputPath << "\n"; + return 0; +} From 0e2d480db441a4266c5c5e79ece0459b49f15123 Mon Sep 17 00:00:00 2001 From: Steven Atkinson Date: Fri, 20 Feb 2026 17:42:37 -0800 Subject: [PATCH 2/3] Add use of render tool in tests --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 62f2a7e2..929263b1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,3 +41,4 @@ jobs: ./build/tools/run_tests ./build/tools/benchmodel ./example_models/wavenet.nam ./build/tools/benchmodel ./example_models/lstm.nam + ./build/tools/render ./example_models/wavenet.nam ./example_models/wavenet.nam From 576617214cc2e7ad2e4c3b3d34fca496bce16cf9 Mon Sep 17 00:00:00 2001 From: Steven Atkinson Date: Fri, 20 Feb 2026 17:48:16 -0800 Subject: [PATCH 3/3] Example input audio, fix render tool test --- .github/workflows/build.yml | 2 +- .gitignore | 2 ++ example_audio/input.wav | Bin 0 -> 288044 bytes 3 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 example_audio/input.wav diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 929263b1..83c452fb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,4 +41,4 @@ jobs: ./build/tools/run_tests ./build/tools/benchmodel ./example_models/wavenet.nam ./build/tools/benchmodel ./example_models/lstm.nam - ./build/tools/render ./example_models/wavenet.nam ./example_models/wavenet.nam + ./build/tools/render ./example_models/wavenet.nam ./example_audio/input.wav ./example_audio/output.wav diff --git a/.gitignore b/.gitignore index b7ee58e6..34ee36ee 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ docs/_build/ *.DS_Store + +example_audio/output.wav diff --git a/example_audio/input.wav b/example_audio/input.wav new file mode 100644 index 0000000000000000000000000000000000000000..fd0302bd119965cb185437a0fc42dfda94193cd4 GIT binary patch literal 288044 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%0DRop!|dK z56V9%|DgPX@(;>CDF2}RgYpl`KPdm8{Dblj%0DRop!|dK56V9%|DgPX@(;>CDF2}R zgYpl`KPdm8{Dblj%0DRop!|dK56V9%|DgPX@(;>CDF2}RgYpl`KPdm8{Dblj%0DRo zp!|dK56V9%|DgPX@(;>CDF2}RgB_z2e^CBG`3L16lz&kELHP&e zAC!Mk{z3T%e^CBG`3L16lz*^c%)QxP-kD|I`mFBF?Sro`-CsP> zv1tGJE8ET$Ye^CBG`3L16lz&kELHP&eAC!Mk{z3T%e^CBG`3L16lz&kE zLHP&eAC!Mk{z3T%e^CBG`3L16lz&kELHP&eAC!Mk{z3T%e^CBG`3L16lz&kELHP&eAC!Mk{z3T%e^CBG`3L16 zlz&kELHP&eAC!Mk{z3T%e^CBG`3L16lz&kELHP&eAC!Mk{z3T%dxQZ@3z)+ldD&!P2OeAe0nbSZB*`8 z+RLA}fu%nVh=>XLep2wEpF`eF4Q+8HtoVL7cA>?0BA=g)T6r+qWm(L)_hRKAlz&kE zLHP&eAC!Mk{z3T%e^CBG`3L16lz&kELHP&eAC!Mk{z3T%e^CBG`3L16lz&kELHP&eAC!Mk{z3T%e^CBG`3L16 zlz&kELHP&eAC!Mk{z3T%e^CBG`3L16lz&kELHP&eAC!Mk{z3T% ze^CBG`3L16lz&kELHP&eAC!Nv#-Ud756V9%|DgPX@(;>CDF2}R zgYpl`KPdm8{Dblj%0DRop!|dK56V9%|DgPX@(;>CDF2}RgYpl`Ke*+i$c~>xtQr!2 ztaDf`-_YHa!FSVxsxJwQZy#{}zQ5<%w!;JcicFt}UE36$^s?{VddndXH}6)-i``0Z zx;{1Dh+NZxKWj~I(=>N<e66rcgK~p>$cxhnCDF2}RgYpl`KPdm8{Dblj%0DRop!|dK56V9%|DgPX@(;>CDF2}RgYpl` zKPdm8{Dblj%0DRop!|dK56V9%|DgPX@(;>CDF2}RgYpl`KPdm8{Dblj%0DRop!|dK z56V9%|DgPX@(;>CDF2}RgYpl`KPdm8{Dblj%0DRop!|dK56V9%|DgPX@(;>CDF2}R hgYpl`KPdm8{Dblj%0DRop!|dK56VCI|EYiQ{{ZfUN{0Xd literal 0 HcmV?d00001