Skip to content

Commit e3a830d

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 e3a830d

File tree

3 files changed

+388
-8
lines changed

3 files changed

+388
-8
lines changed

NeuralAmpModeler/NeuralAmpModeler.cpp

Lines changed: 249 additions & 5 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

@@ -697,6 +713,18 @@ std::string NeuralAmpModeler::_StageModel(const WDL_String& modelPath)
697713
temp->Reset(GetSampleRate(), GetBlockSize());
698714
mStagedModel = std::move(temp);
699715
mNAMPath = modelPath;
716+
717+
// Read file data for embedding in session
718+
mNAMData.clear();
719+
std::ifstream file(dspPath, std::ios::binary | std::ios::ate);
720+
if (file.is_open())
721+
{
722+
std::streamsize size = file.tellg();
723+
file.seekg(0, std::ios::beg);
724+
mNAMData.resize(static_cast<size_t>(size));
725+
file.read(reinterpret_cast<char*>(mNAMData.data()), size);
726+
}
727+
700728
SendControlMsgFromDelegate(kCtrlTagModelFileBrowser, kMsgTagLoadedModel, mNAMPath.GetLength(), mNAMPath.Get());
701729
}
702730
catch (std::runtime_error& e)
@@ -721,6 +749,7 @@ dsp::wav::LoadReturnCode NeuralAmpModeler::_StageIR(const WDL_String& irPath)
721749
// path and the model got caught on opposite sides of the fence...
722750
WDL_String previousIRPath = mIRPath;
723751
const double sampleRate = GetSampleRate();
752+
724753
dsp::wav::LoadReturnCode wavState = dsp::wav::LoadReturnCode::ERROR_OTHER;
725754
try
726755
{
@@ -738,6 +767,19 @@ dsp::wav::LoadReturnCode NeuralAmpModeler::_StageIR(const WDL_String& irPath)
738767
if (wavState == dsp::wav::LoadReturnCode::SUCCESS)
739768
{
740769
mIRPath = irPath;
770+
771+
// Read file data for embedding in session
772+
mIRData.clear();
773+
auto irPathU8 = std::filesystem::u8path(irPath.Get());
774+
std::ifstream file(irPathU8, std::ios::binary | std::ios::ate);
775+
if (file.is_open())
776+
{
777+
std::streamsize size = file.tellg();
778+
file.seekg(0, std::ios::beg);
779+
mIRData.resize(static_cast<size_t>(size));
780+
file.read(reinterpret_cast<char*>(mIRData.data()), size);
781+
}
782+
741783
SendControlMsgFromDelegate(kCtrlTagIRFileBrowser, kMsgTagLoadedIR, mIRPath.GetLength(), mIRPath.Get());
742784
}
743785
else
@@ -911,5 +953,207 @@ void NeuralAmpModeler::_UpdateMeters(sample** inputPointer, sample** outputPoint
911953
mOutputSender.ProcessBlock(outputPointer, (int)nFrames, kCtrlTagOutputMeter, nChansHack);
912954
}
913955

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