Skip to content

Commit 4996eaf

Browse files
committed
Add support for embedding NAM models and IR files in session data
This enables DAW sessions to be fully portable by storing the actual NAM model and IR file data in the session, not just file paths. Changes: - SerializeState: Embed NAM/IR file data in session chunk - UnserializeState: Load from embedded data if file path not found - New _StageModelFromData/_StageIRFromData functions - Version bump to 0.7.13 with backward-compatible serialization Behavior: - Prefers loading from file path if available (for easy model updates) - Falls back to embedded data if file is missing (for portability) - Fully backward compatible with older session formats
1 parent 512f5c6 commit 4996eaf

3 files changed

Lines changed: 392 additions & 10 deletions

File tree

NeuralAmpModeler/NeuralAmpModeler.cpp

Lines changed: 253 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include <algorithm> // std::clamp, std::min
22
#include <cmath> // pow
33
#include <filesystem>
4+
#include <fstream> // std::ifstream for file reading
45
#include <iostream>
56
#include <utility>
67

@@ -74,7 +75,6 @@ const bool kDefaultCalibrateInput = false;
7475
const std::string kInputCalibrationLevelParamName = "InputCalibrationLevel";
7576
const double kDefaultInputCalibrationLevel = 12.0;
7677

77-
7878
NeuralAmpModeler::NeuralAmpModeler(const InstanceInfo& info)
7979
: Plugin(info, MakeConfig(kNumParams, kNumPresets))
8080
{
@@ -183,7 +183,6 @@ NeuralAmpModeler::NeuralAmpModeler(const InstanceInfo& info)
183183
{
184184
// Sets mNAMPath and mStagedNAM
185185
const std::string msg = _StageModel(fileName);
186-
// TODO error messages like the IR loader.
187186
if (msg.size())
188187
{
189188
std::stringstream ss;
@@ -407,16 +406,33 @@ void NeuralAmpModeler::OnIdle()
407406

408407
bool NeuralAmpModeler::SerializeState(IByteChunk& chunk) const
409408
{
410-
// If this isn't here when unserializing, then we know we're dealing with something before v0.8.0.
409+
// If this isn't here when unserializing, then we know we're dealing with something before v0.7.13.
411410
WDL_String header("###NeuralAmpModeler###"); // Don't change this!
412411
chunk.PutStr(header.Get());
413412
// Plugin version, so we can load legacy serialized states in the future!
414413
WDL_String version(PLUG_VERSION_STR);
415414
chunk.PutStr(version.Get());
416-
// Model directory (don't serialize the model itself; we'll just load it again
417-
// when we unserialize)
415+
416+
// Serialize file paths for backward compatibility
418417
chunk.PutStr(mNAMPath.Get());
419418
chunk.PutStr(mIRPath.Get());
419+
420+
// Embed the actual file data for portability
421+
// Data was read when model/IR was loaded
422+
int namDataSize = static_cast<int>(mNAMData.size());
423+
chunk.Put(&namDataSize);
424+
if (namDataSize > 0)
425+
{
426+
chunk.PutBytes(mNAMData.data(), namDataSize);
427+
}
428+
429+
int irDataSize = static_cast<int>(mIRData.size());
430+
chunk.Put(&irDataSize);
431+
if (irDataSize > 0)
432+
{
433+
chunk.PutBytes(mIRData.data(), irDataSize);
434+
}
435+
420436
return SerializeParams(chunk);
421437
}
422438

@@ -689,14 +705,28 @@ void NeuralAmpModeler::_SetOutputGain()
689705
std::string NeuralAmpModeler::_StageModel(const WDL_String& modelPath)
690706
{
691707
WDL_String previousNAMPath = mNAMPath;
708+
const double sampleRate = GetSampleRate();
709+
692710
try
693711
{
694712
auto dspPath = std::filesystem::u8path(modelPath.Get());
695713
std::unique_ptr<nam::DSP> model = nam::get_dsp(dspPath);
696-
std::unique_ptr<ResamplingNAM> temp = std::make_unique<ResamplingNAM>(std::move(model), GetSampleRate());
697-
temp->Reset(GetSampleRate(), GetBlockSize());
714+
std::unique_ptr<ResamplingNAM> temp = std::make_unique<ResamplingNAM>(std::move(model), sampleRate);
715+
temp->Reset(sampleRate, GetBlockSize());
698716
mStagedModel = std::move(temp);
699717
mNAMPath = modelPath;
718+
719+
// Read file data for embedding in session
720+
mNAMData.clear();
721+
std::ifstream file(dspPath, std::ios::binary | std::ios::ate);
722+
if (file.is_open())
723+
{
724+
std::streamsize size = file.tellg();
725+
file.seekg(0, std::ios::beg);
726+
mNAMData.resize(static_cast<size_t>(size));
727+
file.read(reinterpret_cast<char*>(mNAMData.data()), size);
728+
}
729+
700730
SendControlMsgFromDelegate(kCtrlTagModelFileBrowser, kMsgTagLoadedModel, mNAMPath.GetLength(), mNAMPath.Get());
701731
}
702732
catch (std::runtime_error& e)
@@ -721,6 +751,7 @@ dsp::wav::LoadReturnCode NeuralAmpModeler::_StageIR(const WDL_String& irPath)
721751
// path and the model got caught on opposite sides of the fence...
722752
WDL_String previousIRPath = mIRPath;
723753
const double sampleRate = GetSampleRate();
754+
724755
dsp::wav::LoadReturnCode wavState = dsp::wav::LoadReturnCode::ERROR_OTHER;
725756
try
726757
{
@@ -738,6 +769,19 @@ dsp::wav::LoadReturnCode NeuralAmpModeler::_StageIR(const WDL_String& irPath)
738769
if (wavState == dsp::wav::LoadReturnCode::SUCCESS)
739770
{
740771
mIRPath = irPath;
772+
773+
// Read file data for embedding in session
774+
mIRData.clear();
775+
auto irPathU8 = std::filesystem::u8path(irPath.Get());
776+
std::ifstream file(irPathU8, std::ios::binary | std::ios::ate);
777+
if (file.is_open())
778+
{
779+
std::streamsize size = file.tellg();
780+
file.seekg(0, std::ios::beg);
781+
mIRData.resize(static_cast<size_t>(size));
782+
file.read(reinterpret_cast<char*>(mIRData.data()), size);
783+
}
784+
741785
SendControlMsgFromDelegate(kCtrlTagIRFileBrowser, kMsgTagLoadedIR, mIRPath.GetLength(), mIRPath.Get());
742786
}
743787
else
@@ -911,5 +955,207 @@ void NeuralAmpModeler::_UpdateMeters(sample** inputPointer, sample** outputPoint
911955
mOutputSender.ProcessBlock(outputPointer, (int)nFrames, kCtrlTagOutputMeter, nChansHack);
912956
}
913957

958+
std::string NeuralAmpModeler::_StageModelFromData(const std::vector<uint8_t>& data, const WDL_String& originalPath)
959+
{
960+
WDL_String previousNAMPath = mNAMPath;
961+
const double sampleRate = GetSampleRate();
962+
963+
try
964+
{
965+
// Parse the JSON from memory
966+
std::string jsonStr(data.begin(), data.end());
967+
nlohmann::json j = nlohmann::json::parse(jsonStr);
968+
969+
// Build dspData structure
970+
nam::dspData dspData;
971+
dspData.version = j["version"];
972+
dspData.architecture = j["architecture"];
973+
dspData.config = j["config"];
974+
dspData.metadata = j["metadata"];
975+
976+
// Extract weights
977+
if (j.find("weights") != j.end())
978+
{
979+
dspData.weights = j["weights"].get<std::vector<float>>();
980+
}
981+
982+
// Extract sample rate
983+
if (j.find("sample_rate") != j.end())
984+
dspData.expected_sample_rate = j["sample_rate"];
985+
else
986+
dspData.expected_sample_rate = -1.0;
987+
988+
// Create DSP from dspData
989+
std::unique_ptr<nam::DSP> model = nam::get_dsp(dspData);
990+
std::unique_ptr<ResamplingNAM> temp = std::make_unique<ResamplingNAM>(std::move(model), sampleRate);
991+
temp->Reset(sampleRate, GetBlockSize());
992+
mStagedModel = std::move(temp);
993+
mNAMPath = originalPath;
994+
mNAMData = data; // Store the embedded data
995+
SendControlMsgFromDelegate(kCtrlTagModelFileBrowser, kMsgTagLoadedModel, mNAMPath.GetLength(), mNAMPath.Get());
996+
}
997+
catch (std::exception& e)
998+
{
999+
SendControlMsgFromDelegate(kCtrlTagModelFileBrowser, kMsgTagLoadFailed);
1000+
1001+
if (mStagedModel != nullptr)
1002+
{
1003+
mStagedModel = nullptr;
1004+
}
1005+
mNAMPath = previousNAMPath;
1006+
std::cerr << "Failed to read DSP module from embedded data" << std::endl;
1007+
std::cerr << e.what() << std::endl;
1008+
return e.what();
1009+
}
1010+
return "";
1011+
}
1012+
1013+
dsp::wav::LoadReturnCode NeuralAmpModeler::_StageIRFromData(const std::vector<uint8_t>& data,
1014+
const WDL_String& originalPath)
1015+
{
1016+
WDL_String previousIRPath = mIRPath;
1017+
const double sampleRate = GetSampleRate();
1018+
1019+
dsp::wav::LoadReturnCode wavState = dsp::wav::LoadReturnCode::ERROR_OTHER;
1020+
1021+
try
1022+
{
1023+
// Parse WAV from memory
1024+
std::vector<float> audio;
1025+
double wavSampleRate = 0.0;
1026+
1027+
// Basic WAV parser for in-memory data
1028+
// WAV format: RIFF header (12 bytes) + fmt chunk + data chunk
1029+
if (data.size() < 44) // Minimum WAV file size
1030+
{
1031+
throw std::runtime_error("IR data too small to be valid WAV");
1032+
}
1033+
1034+
// Check RIFF header
1035+
if (data[0] != 'R' || data[1] != 'I' || data[2] != 'F' || data[3] != 'F')
1036+
{
1037+
throw std::runtime_error("Invalid WAV format - missing RIFF header");
1038+
}
1039+
1040+
// Check WAVE format
1041+
if (data[8] != 'W' || data[9] != 'A' || data[10] != 'V' || data[11] != 'E')
1042+
{
1043+
throw std::runtime_error("Invalid WAV format - not a WAVE file");
1044+
}
1045+
1046+
// Find fmt chunk
1047+
size_t pos = 12;
1048+
uint16_t audioFormat = 0;
1049+
uint16_t numChannels = 0;
1050+
uint32_t sampleRateInt = 0;
1051+
uint16_t bitsPerSample = 0;
1052+
1053+
while (pos < data.size() - 8)
1054+
{
1055+
std::string chunkID(data.begin() + pos, data.begin() + pos + 4);
1056+
uint32_t chunkSize = *reinterpret_cast<const uint32_t*>(&data[pos + 4]);
1057+
1058+
if (chunkID == "fmt ")
1059+
{
1060+
audioFormat = *reinterpret_cast<const uint16_t*>(&data[pos + 8]);
1061+
numChannels = *reinterpret_cast<const uint16_t*>(&data[pos + 10]);
1062+
sampleRateInt = *reinterpret_cast<const uint32_t*>(&data[pos + 12]);
1063+
bitsPerSample = *reinterpret_cast<const uint16_t*>(&data[pos + 22]);
1064+
wavSampleRate = static_cast<double>(sampleRateInt);
1065+
}
1066+
else if (chunkID == "data")
1067+
{
1068+
// Found data chunk
1069+
size_t dataStart = pos + 8;
1070+
size_t numSamples = chunkSize / (bitsPerSample / 8);
1071+
1072+
audio.resize(numSamples);
1073+
1074+
// Convert based on bits per sample
1075+
if (bitsPerSample == 16 && audioFormat == 1) // PCM 16-bit
1076+
{
1077+
for (size_t i = 0; i < numSamples; i++)
1078+
{
1079+
int16_t sample = *reinterpret_cast<const int16_t*>(&data[dataStart + i * 2]);
1080+
audio[i] = sample / 32768.0f;
1081+
}
1082+
}
1083+
else if (bitsPerSample == 24 && audioFormat == 1) // PCM 24-bit
1084+
{
1085+
for (size_t i = 0; i < numSamples; i++)
1086+
{
1087+
int32_t sample = 0;
1088+
sample |= static_cast<int32_t>(data[dataStart + i * 3]);
1089+
sample |= static_cast<int32_t>(data[dataStart + i * 3 + 1]) << 8;
1090+
sample |= static_cast<int32_t>(data[dataStart + i * 3 + 2]) << 16;
1091+
if (sample & 0x800000)
1092+
sample |= 0xFF000000; // Sign extend
1093+
audio[i] = sample / 8388608.0f;
1094+
}
1095+
}
1096+
else if (bitsPerSample == 32 && audioFormat == 3) // IEEE float 32-bit
1097+
{
1098+
for (size_t i = 0; i < numSamples; i++)
1099+
{
1100+
audio[i] = *reinterpret_cast<const float*>(&data[dataStart + i * 4]);
1101+
}
1102+
}
1103+
else
1104+
{
1105+
throw std::runtime_error("Unsupported WAV format");
1106+
}
1107+
1108+
break;
1109+
}
1110+
1111+
pos += 8 + chunkSize;
1112+
}
1113+
1114+
if (audio.empty())
1115+
{
1116+
throw std::runtime_error("No audio data found in WAV");
1117+
}
1118+
1119+
// Layer 9: Validate that fmt chunk was actually found and sample rate is valid
1120+
// WAV files can have missing fmt chunks or chunks in wrong order
1121+
if (wavSampleRate <= 0.0 || wavSampleRate != wavSampleRate)
1122+
{
1123+
throw std::runtime_error("Invalid or missing sample rate in WAV fmt chunk");
1124+
}
1125+
1126+
// Create IR from the loaded data
1127+
dsp::ImpulseResponse::IRData irData;
1128+
irData.mRawAudio = audio;
1129+
irData.mRawAudioSampleRate = wavSampleRate;
1130+
1131+
mStagedIR = std::make_unique<dsp::ImpulseResponse>(irData, sampleRate);
1132+
wavState = dsp::wav::LoadReturnCode::SUCCESS;
1133+
}
1134+
catch (std::exception& e)
1135+
{
1136+
wavState = dsp::wav::LoadReturnCode::ERROR_OTHER;
1137+
std::cerr << "Failed to load IR from embedded data:" << std::endl;
1138+
std::cerr << e.what() << std::endl;
1139+
}
1140+
1141+
if (wavState == dsp::wav::LoadReturnCode::SUCCESS)
1142+
{
1143+
mIRPath = originalPath;
1144+
mIRData = data; // Store the embedded data
1145+
SendControlMsgFromDelegate(kCtrlTagIRFileBrowser, kMsgTagLoadedIR, mIRPath.GetLength(), mIRPath.Get());
1146+
}
1147+
else
1148+
{
1149+
if (mStagedIR != nullptr)
1150+
{
1151+
mStagedIR = nullptr;
1152+
}
1153+
mIRPath = previousIRPath;
1154+
SendControlMsgFromDelegate(kCtrlTagIRFileBrowser, kMsgTagLoadFailed);
1155+
}
1156+
1157+
return wavState;
1158+
}
1159+
9141160
// HACK
9151161
#include "Unserialization.cpp"

NeuralAmpModeler/NeuralAmpModeler.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,10 +220,14 @@ class NeuralAmpModeler final : public iplug::Plugin
220220
// Loads a NAM model and stores it to mStagedNAM
221221
// Returns an empty string on success, or an error message on failure.
222222
std::string _StageModel(const WDL_String& dspFile);
223+
// Loads a NAM model from embedded binary data
224+
std::string _StageModelFromData(const std::vector<uint8_t>& data, const WDL_String& originalPath);
223225
// Loads an IR and stores it to mStagedIR.
224226
// Return status code so that error messages can be relayed if
225227
// it wasn't successful.
226228
dsp::wav::LoadReturnCode _StageIR(const WDL_String& irPath);
229+
// Loads an IR from embedded binary data
230+
dsp::wav::LoadReturnCode _StageIRFromData(const std::vector<uint8_t>& data, const WDL_String& originalPath);
227231

228232
bool _HaveModel() const { return this->mModel != nullptr; };
229233
// Prepare the input & output buffers
@@ -307,6 +311,10 @@ class NeuralAmpModeler final : public iplug::Plugin
307311
// Path to IR (.wav file)
308312
WDL_String mIRPath;
309313

314+
// Embedded file data for portability (stored with DAW session)
315+
std::vector<uint8_t> mNAMData;
316+
std::vector<uint8_t> mIRData;
317+
310318
WDL_String mHighLightColor{PluginColors::NAM_THEMECOLOR.ToColorCode()};
311319

312320
std::unordered_map<std::string, double> mNAMParams = {{"Input", 0.0}, {"Output", 0.0}};

0 commit comments

Comments
 (0)