Fix Patch/Bank import-export issues from TODO.md
- patchImportXml: handle the legacy single-patch format (JSYNTH_PATCH root) as its own top-level branch instead of incorrectly nested inside the modern per-child loop, which checked/decoded the outer xml on every iteration instead of the loop variable xml2. Switched its CC-table import to import_midi_cc, matching the convention already used (and exercised) by bankDecodeXml_legacy. loadPatchFromFile's .xmp branch now also accepts legacy-rooted files. - setCurrentProgramStateInformation: fix a related bug found in passing - it was passing patchImportXml an XML node one level too deep, making the VST2 "copy plugin state to another track" feature a complete no-op. - Fix the fundamentally broken fxb/fxp magic-number check: it compared raw file bytes (as text, e.g. "CcnK") against JUCE's int-to-decimal- text String constructor (e.g. "1130589771"), which can never match. This meant the entire opaque .fxb/.fxp load path was dead code. Fixed using ByteOrder::bigEndianInt, matching the pattern JUCE's own VST3 wrapper uses for the identical struct. "Regular" (non-chunk) .fxb/.fxp is still unsupported, but now shows an AlertWindow explaining why instead of silently doing nothing. - Implement .fxp/.fxb saving using the same opaque-chunk format the load path expects, reusing getCurrentProgramStateInformation/ getStateInformation for the payload. - Deduplicate patchDecodeXml/patchDecodeXml_legacy (the latter now just delegates to the former). Verified with clean debug/release builds and a standalone round-trip test of the binary format logic (magic detection, byte order, size/payload round-trip, safe rejection of truncated/bogus files). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011dhtwRLARk4eiPngcQykLJ
This commit is contained in:
+106
-36
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
#include <math.h>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
|
||||
#include "PluginProcessor.h"
|
||||
#include "JaySynthAudioProcessorEditor.h"
|
||||
@@ -843,20 +844,9 @@ void JaySynthAudioProcessor::patchDecodeXml(const XmlElement *xml, JaySynthSound
|
||||
|
||||
void JaySynthAudioProcessor::patchDecodeXml_legacy(const XmlElement& xml, JaySynthSound *patch)
|
||||
{
|
||||
// ok, now pull out our parameters..
|
||||
patch->setName(xml.getStringAttribute("NAME"));
|
||||
for (int i=0; i < SYNTH_NUM_PARAMS; i++)
|
||||
{
|
||||
synth_float_t value = (synth_float_t) xml.getDoubleAttribute (getParameterNameXML(i), patch->getParameter(i));
|
||||
if (i==SYNTH_PARAM_TUNE)
|
||||
{
|
||||
if (value > 400.0)
|
||||
{
|
||||
value = 1200*log(value/440)/log(2.0);
|
||||
}
|
||||
}
|
||||
patch->setParameter(i, value);
|
||||
}
|
||||
// Legacy format uses the same per-parameter attribute layout as the
|
||||
// current format, just addressed via a reference instead of a pointer.
|
||||
patchDecodeXml(&xml, patch);
|
||||
}
|
||||
|
||||
void JaySynthAudioProcessor::bankEncodeXml(XmlElement *xml)
|
||||
@@ -921,18 +911,28 @@ void JaySynthAudioProcessor::loadBankFromFile (const File &file)
|
||||
return;
|
||||
|
||||
fxBank *bank = (fxBank*)mem.getData();
|
||||
if (String((const char*)&bank->chunkMagic) == String(cMagic))
|
||||
// fxb/fxp magic numbers and sizes are stored big-endian on disk (see
|
||||
// vstfxstore.h); bigEndianInt() reads them correctly regardless of
|
||||
// host byte order.
|
||||
if (ByteOrder::bigEndianInt(&bank->chunkMagic) == (uint32_t)cMagic)
|
||||
{
|
||||
bool is_regular = String((const char*)&bank->fxMagic) == String(bankMagic);
|
||||
bool is_opaque = String((const char*)&bank->fxMagic) == String(chunkBankMagic);
|
||||
bool is_regular = ByteOrder::bigEndianInt(&bank->fxMagic) == (uint32_t)bankMagic;
|
||||
bool is_opaque = ByteOrder::bigEndianInt(&bank->fxMagic) == (uint32_t)chunkBankMagic;
|
||||
if (is_opaque)
|
||||
{
|
||||
uint32_t size = ByteOrder::swap((uint32_t)bank->content.data.size);
|
||||
uint32_t size = ByteOrder::bigEndianInt(&bank->content.data.size);
|
||||
const size_t chunkOffset = offsetof(fxBank, content.data.chunk);
|
||||
if (chunkOffset > mem.getSize() || (size_t)size > mem.getSize() - chunkOffset)
|
||||
return;
|
||||
setStateInformation(bank->content.data.chunk, (int)size);
|
||||
}
|
||||
else if (is_regular)
|
||||
{
|
||||
AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, "Load bank",
|
||||
"This .fxb file uses the \"regular\" (non-chunk) VST2 bank format, "
|
||||
"which JaySynth doesn't support. Only chunk-based .fxb files "
|
||||
"(as saved by JaySynth or other JUCE-based plugins) can be loaded.");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (String(".xmb") == ext)
|
||||
@@ -1026,24 +1026,34 @@ void JaySynthAudioProcessor::loadPatchFromFile (const File &file, int index)
|
||||
return;
|
||||
|
||||
fxProgram *program = (fxProgram*)mem.getData();
|
||||
if (String((const char*)&program->chunkMagic) == String(cMagic))
|
||||
// fxb/fxp magic numbers and sizes are stored big-endian on disk (see
|
||||
// vstfxstore.h); bigEndianInt() reads them correctly regardless of
|
||||
// host byte order.
|
||||
if (ByteOrder::bigEndianInt(&program->chunkMagic) == (uint32_t)cMagic)
|
||||
{
|
||||
bool is_regular = String((const char*)&program->fxMagic) == String(fMagic);
|
||||
bool is_opaque = String((const char*)&program->fxMagic) == String(chunkPresetMagic);
|
||||
bool is_regular = ByteOrder::bigEndianInt(&program->fxMagic) == (uint32_t)fMagic;
|
||||
bool is_opaque = ByteOrder::bigEndianInt(&program->fxMagic) == (uint32_t)chunkPresetMagic;
|
||||
if (is_opaque)
|
||||
{
|
||||
uint32_t size = ByteOrder::swap((uint32_t)program->content.data.size);
|
||||
uint32_t size = ByteOrder::bigEndianInt(&program->content.data.size);
|
||||
const size_t chunkOffset = offsetof(fxProgram, content.data.chunk);
|
||||
if (chunkOffset > mem.getSize() || (size_t)size > mem.getSize() - chunkOffset)
|
||||
return;
|
||||
setCurrentProgramStateInformation(program->content.data.chunk, (int)size);
|
||||
}
|
||||
else if (is_regular)
|
||||
{
|
||||
AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, "Load patch",
|
||||
"This .fxp file uses the \"regular\" (non-chunk) VST2 patch format, "
|
||||
"which JaySynth doesn't support. Only chunk-based .fxp files "
|
||||
"(as saved by JaySynth or other JUCE-based plugins) can be loaded.");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (String(".xmp") == ext)
|
||||
{
|
||||
ScopedPointer<XmlElement> xml = XmlDocument::parse(file);
|
||||
if (xml != 0 && xml->hasTagName ("JSynth"))
|
||||
if (xml != 0 && (xml->hasTagName ("JSynth") || xml->hasTagName ("JSYNTH_PATCH")))
|
||||
{
|
||||
patchImportXml(xml, currpatch);
|
||||
}
|
||||
@@ -1052,6 +1062,21 @@ void JaySynthAudioProcessor::loadPatchFromFile (const File &file, int index)
|
||||
|
||||
void JaySynthAudioProcessor::patchImportXml (const XmlElement *xml, JaySynthSound *patch)
|
||||
{
|
||||
// Legacy single-patch format: parameters are attributes directly on the
|
||||
// root element, not nested under a "Patch" child - handle it separately
|
||||
// rather than inside the per-child loop below (xml has no children with
|
||||
// tag "Patch"/"VelKey"/"MidiCC" in this format, so it doesn't fit that
|
||||
// loop's shape). Mirrors how bankDecodeXml_legacy decodes each legacy
|
||||
// bank entry.
|
||||
if (xml->hasTagName ("JSYNTH_PATCH"))
|
||||
{
|
||||
patchDecodeXml_legacy(*xml, patch);
|
||||
XmlElement *pXML_MidiCc = xml->getChildByName ("JSYNTH_MIDICONTROLLER_TABLE");
|
||||
((JaySynthMidiCC*)patch->getMidiCC())->import_midi_cc(pXML_MidiCc);
|
||||
setCurrentProgram(getCurrentProgram());
|
||||
return;
|
||||
}
|
||||
|
||||
XmlElement *xml2 = xml->getFirstChildElement();
|
||||
while(xml2)
|
||||
{
|
||||
@@ -1064,20 +1089,13 @@ void JaySynthAudioProcessor::patchImportXml (const XmlElement *xml, JaySynthSoun
|
||||
((JaySynthMidiCC*)patch->getMidiCC())->import_midi_cc_legacy(pXML_MidiCc);
|
||||
}
|
||||
}
|
||||
else if (xml->hasTagName ("JSYNTH_PATCH"))
|
||||
{
|
||||
patchDecodeXml_legacy(*xml, patch);
|
||||
XmlElement *pXML_MidiCc = xml->getChildByName ("JSYNTH_MIDICONTROLLER_TABLE");
|
||||
((JaySynthMidiCC*)patch->getMidiCC())->import_midi_cc_legacy(pXML_MidiCc);
|
||||
|
||||
}
|
||||
else if (xml2->hasTagName ("VelKey"))
|
||||
{
|
||||
((JaySynthMidiCC*)patch->getMidiCC())->import_midi_velkey(xml->getChildByName ("VelKey"));
|
||||
((JaySynthMidiCC*)patch->getMidiCC())->import_midi_velkey(xml2);
|
||||
}
|
||||
else if (xml2->hasTagName ("MidiCC"))
|
||||
{
|
||||
((JaySynthMidiCC*)patch->getMidiCC())->import_midi_cc(xml->getChildByName ("MidiCC"));
|
||||
((JaySynthMidiCC*)patch->getMidiCC())->import_midi_cc(xml2);
|
||||
}
|
||||
xml2 = xml2->getNextElement();
|
||||
};
|
||||
@@ -1111,8 +1129,10 @@ void JaySynthAudioProcessor::setCurrentProgramStateInformation (const void* data
|
||||
{
|
||||
if (xml->hasTagName ("JSynth"))
|
||||
{
|
||||
XmlElement *xml2 = xml->getFirstChildElement();
|
||||
patchImportXml(xml2, currpatch);
|
||||
// xml is already the "JSynth" container (Patch/VelKey/MidiCC
|
||||
// children) - patchImportXml expects that container, not one of
|
||||
// its children.
|
||||
patchImportXml(xml, currpatch);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1122,7 +1142,34 @@ void JaySynthAudioProcessor::savePatchToFile (const File &file, int index)
|
||||
String ext = file.getFileExtension();
|
||||
if (String(".fxp") == ext)
|
||||
{
|
||||
// ToDo: implement fxp save
|
||||
commitEditBuffer();
|
||||
|
||||
// Reuse the same opaque chunk format used for the host's own VST2
|
||||
// "current program" state (and expected back by the .fxp opaque-chunk
|
||||
// branch of loadPatchFromFile).
|
||||
MemoryBlock chunk;
|
||||
getCurrentProgramStateInformation(chunk);
|
||||
|
||||
const size_t headerSize = offsetof(fxProgram, content) + sizeof(VstInt32);
|
||||
MemoryBlock mem(headerSize + chunk.getSize(), true);
|
||||
fxProgram *program = (fxProgram*)mem.getData();
|
||||
|
||||
program->chunkMagic = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)cMagic);
|
||||
program->byteSize = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)(mem.getSize() - 8));
|
||||
program->fxMagic = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)chunkPresetMagic);
|
||||
program->version = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)1);
|
||||
program->fxID = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)JucePlugin_VSTUniqueID);
|
||||
program->fxVersion = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)JucePlugin_VersionCode);
|
||||
program->numParams = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)SYNTH_NUM_PARAMS);
|
||||
|
||||
const String name = currpatch->getName();
|
||||
memcpy(program->prgName, name.toRawUTF8(),
|
||||
jmin(sizeof(program->prgName) - 1, (size_t)name.getNumBytesAsUTF8()));
|
||||
|
||||
program->content.data.size = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)chunk.getSize());
|
||||
memcpy(program->content.data.chunk, chunk.getData(), chunk.getSize());
|
||||
|
||||
file.replaceWithData(mem.getData(), mem.getSize());
|
||||
}
|
||||
else if (String(".xmp") == ext)
|
||||
{
|
||||
@@ -1144,7 +1191,30 @@ void JaySynthAudioProcessor::saveBankToFile (const File &file)
|
||||
String ext = file.getFileExtension();
|
||||
if (String(".fxb") == ext)
|
||||
{
|
||||
// ToDo: implement fxp save
|
||||
commitEditBuffer();
|
||||
|
||||
// Reuse the same opaque chunk format used for the host's own VST2
|
||||
// bank state (and expected back by the .fxb opaque-chunk branch of
|
||||
// loadBankFromFile).
|
||||
MemoryBlock chunk;
|
||||
getStateInformation(chunk);
|
||||
|
||||
const size_t headerSize = offsetof(fxBank, content) + sizeof(VstInt32);
|
||||
MemoryBlock mem(headerSize + chunk.getSize(), true);
|
||||
fxBank *bank = (fxBank*)mem.getData();
|
||||
|
||||
bank->chunkMagic = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)cMagic);
|
||||
bank->byteSize = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)(mem.getSize() - 8));
|
||||
bank->fxMagic = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)chunkBankMagic);
|
||||
bank->version = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)1);
|
||||
bank->fxID = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)JucePlugin_VSTUniqueID);
|
||||
bank->fxVersion = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)JucePlugin_VersionCode);
|
||||
bank->numPrograms = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)NUM_PROGRAMS);
|
||||
|
||||
bank->content.data.size = (VstInt32)ByteOrder::swapIfLittleEndian((uint32_t)chunk.getSize());
|
||||
memcpy(bank->content.data.chunk, chunk.getData(), chunk.getSize());
|
||||
|
||||
file.replaceWithData(mem.getData(), mem.getSize());
|
||||
}
|
||||
else if (String(".xmb") == ext)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user