From d3c0faae40f8776377dfaddb5aec4a2ab9649a83 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 16 Aug 2019 11:02:19 -0400 Subject: [PATCH] Support DAW state in the patch (#1032) Some features of the synth - notably, zoom, MPE Enablement, and Tuning - are features of the DAW environment you are working in and were not persisted. This fixes that by adding a dawExtraState section to the streaming protocol which is only populated and read at DAW time not at general patch time. Closes #890 Closes #914 CLoses #915 Mostly wraps up #828 --- src/au/aulayer.cpp | 10 ++ src/common/SurgePatch.cpp | 181 +++++++++++++++++++++++++++++- src/common/SurgeStorage.h | 17 +++ src/common/SurgeSynthesizer.h | 20 ++++ src/common/Tunings.cpp | 40 ++++--- src/common/Tunings.h | 1 + src/common/gui/SurgeGUIEditor.cpp | 8 ++ src/common/gui/SurgeGUIEditor.h | 14 +++ src/vst2/Vst2PluginInstance.cpp | 8 ++ src/vst3/SurgeVst3Processor.cpp | 8 ++ 10 files changed, 289 insertions(+), 18 deletions(-) diff --git a/src/au/aulayer.cpp b/src/au/aulayer.cpp index 1723be16d29..20dc393c14a 100644 --- a/src/au/aulayer.cpp +++ b/src/au/aulayer.cpp @@ -525,6 +525,10 @@ ComponentResult aulayer::RestoreState(CFPropertyListRef plist) p = CFDataGetBytePtr(data); size_t psize = CFDataGetLength(data); plugin_instance->loadRaw(p, psize, false); + + plugin_instance->loadFromDawExtraState(); + if( editor_instance ) + editor_instance->loadFromDAWExtraState(plugin_instance); } return noErr; } @@ -545,11 +549,17 @@ ComponentResult aulayer::SaveState(CFPropertyListRef* plist) CFMutableDictionaryRef dict = (CFMutableDictionaryRef)*plist; void* data; + + plugin_instance->populateDawExtraState(); + if( editor_instance ) + editor_instance->populateDawExtraState(plugin_instance); + CFIndex size = plugin_instance->saveRaw(&data); CFDataRef dataref = CFDataCreateWithBytesNoCopy(NULL, (const UInt8*)data, size, kCFAllocatorNull); CFDictionarySetValue(dict, rawchunkname, dataref); CFRelease(dataref); + return noErr; } diff --git a/src/common/SurgePatch.cpp b/src/common/SurgePatch.cpp index a78dd33e8a7..40eb9b800df 100644 --- a/src/common/SurgePatch.cpp +++ b/src/common/SurgePatch.cpp @@ -42,11 +42,15 @@ const int gui_mid_topbar_y = 17; // 3 -> 4 comb+/- combined into 1 filtertype (subtype 0,0->0 0,1->1 1,0->2 1,1->3 ) // 4 -> 5 stereo filterconf now have seperate pan controls // 5 -> 6 new filter sound in v1.2 (same parameters, but different sound & changed resonance -// response). 6 -> 7 custom controller state now stored (in seq. recall) 7 -> 8 larger resonance -// range (old filters are set to subtype 1), pan2 -> width 8 -> 9 now 8 controls (offset ids larger -// than ctrl7 by +1), custom controllers have names (guess for pre-rev9 patches) 9 -> 10 added -// character parameter -const int ff_revision = 10; +// response). +// 6 -> 7 custom controller state now stored (in seq. recall) +// 7 -> 8 larger resonance +// range (old filters are set to subtype 1), pan2 -> width +// 8 -> 9 now 8 controls (offset ids larger +// than ctrl7 by +1), custom controllers have names (guess for pre-rev9 patches) +// 9 -> 10 added character parameter +// 10 -> 11 (1.6.2 release) added DAW Extra State +const int ff_revision = 11; SurgePatch::SurgePatch(SurgeStorage* storage) { @@ -732,6 +736,96 @@ struct patch_header }; #pragma pack(pop) + +// BASE 64 SUPPORT. THANKS https://renenyffenegger.ch/notes/development/Base64/Encoding-and-decoding-base-64-with-cpp +static const std::string base64_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + + +static inline bool is_base64(unsigned char c) { + return (isalnum(c) || (c == '+') || (c == '/')); +} + +std::string base64_encode(unsigned char const* bytes_to_encode, unsigned int in_len) { + std::string ret; + int i = 0; + int j = 0; + unsigned char char_array_3[3]; + unsigned char char_array_4[4]; + + while (in_len--) { + char_array_3[i++] = *(bytes_to_encode++); + if (i == 3) { + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for(i = 0; (i <4) ; i++) + ret += base64_chars[char_array_4[i]]; + i = 0; + } + } + + if (i) + { + for(j = i; j < 3; j++) + char_array_3[j] = '\0'; + + char_array_4[0] = ( char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + + for (j = 0; (j < i + 1); j++) + ret += base64_chars[char_array_4[j]]; + + while((i++ < 3)) + ret += '='; + + } + + return ret; +} + +std::string base64_decode(std::string const& encoded_string) { + int in_len = encoded_string.size(); + int i = 0; + int j = 0; + int in_ = 0; + unsigned char char_array_4[4], char_array_3[3]; + std::string ret; + + while (in_len-- && ( encoded_string[in_] != '=') && is_base64(encoded_string[in_])) { + char_array_4[i++] = encoded_string[in_]; in_++; + if (i ==4) { + for (i = 0; i <4; i++) + char_array_4[i] = base64_chars.find(char_array_4[i]); + + char_array_3[0] = ( char_array_4[0] << 2 ) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; + + for (i = 0; (i < 3); i++) + ret += char_array_3[i]; + i = 0; + } + } + + if (i) { + for (j = 0; j < i; j++) + char_array_4[j] = base64_chars.find(char_array_4[j]); + + char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + + for (j = 0; (j < i - 1); j++) ret += char_array_3[j]; + } + + return ret; +} + void SurgePatch::load_patch(const void* data, int datasize, bool preset) { if (datasize <= 4) @@ -878,6 +972,7 @@ void SurgePatch::load_xml(const void* data, int datasize, bool is_preset) char* temp = (char*)malloc(datasize + 1); memcpy(temp, data, datasize); *(temp + datasize) = 0; + // std::cout << "XML DOC is " << temp << std::endl; doc.Parse(temp, nullptr, TIXML_ENCODING_LEGACY); free(temp); } @@ -1259,6 +1354,53 @@ void SurgePatch::load_xml(const void* data, int datasize, bool is_preset) } p = TINYXML_SAFE_TO_ELEMENT(p->NextSibling("entry")); } + + dawExtraState.isPopulated = false; + TiXmlElement *de = TINYXML_SAFE_TO_ELEMENT(patch->FirstChild("dawExtraState")); + if( de ) + { + int pop; + if( de->QueryIntAttribute("populated", &pop) == TIXML_SUCCESS ) + { + dawExtraState.isPopulated = (pop != 0); + } + + if( dawExtraState.isPopulated ) + { + int ival; + TiXmlElement *p; + p = TINYXML_SAFE_TO_ELEMENT(de->FirstChild("instanceZoomFactor")); + if( p && + p->QueryIntAttribute("v",&ival) == TIXML_SUCCESS) + dawExtraState.instanceZoomFactor = ival; + + p = TINYXML_SAFE_TO_ELEMENT(de->FirstChild("mpeEnabled")); + if( p && + p->QueryIntAttribute("v",&ival) == TIXML_SUCCESS) + dawExtraState.mpeEnabled = ival; + + p = TINYXML_SAFE_TO_ELEMENT(de->FirstChild("hasTuning")); + if( p && + p->QueryIntAttribute("v",&ival) == TIXML_SUCCESS) + { + dawExtraState.hasTuning = (ival != 0); + } + + const char* td; + if( dawExtraState.hasTuning ) + { + p = TINYXML_SAFE_TO_ELEMENT(de->FirstChild("tuningContents")); + if( p && + (td = p->Attribute("v") )) + { + auto tc = base64_decode(td); + dawExtraState.tuningContents = tc; + } + } + + } + } + if (!is_preset) { TiXmlElement* mw = TINYXML_SAFE_TO_ELEMENT(patch->FirstChild("modwheel")); @@ -1437,6 +1579,35 @@ unsigned int SurgePatch::save_xml(void** data) // allocates mem, must be freed b patch.InsertEndChild(mw); } + TiXmlElement dawExtraXML("dawExtraState"); + dawExtraXML.SetAttribute( "populated", dawExtraState.isPopulated ? 1 : 0 ); + + if( dawExtraState.isPopulated ) + { + TiXmlElement izf("instanceZoomFactor"); + izf.SetAttribute( "v", dawExtraState.instanceZoomFactor ); + dawExtraXML.InsertEndChild(izf); + + TiXmlElement mpe("mpeEnabled"); + mpe.SetAttribute("v", dawExtraState.mpeEnabled ? 1 : 0 ); + dawExtraXML.InsertEndChild(mpe); + + TiXmlElement tun("hasTuning"); + tun.SetAttribute("v", dawExtraState.hasTuning ? 1 : 0 ); + dawExtraXML.InsertEndChild(tun); + + /* + ** we really want a cdata here but TIXML is ambiguous whether + ** it does the right thing when I read the code, and is kinda crufty + ** so just protect ourselves with a base 64 encoding. + */ + TiXmlElement tnc("tuningContents"); + tnc.SetAttribute("v", base64_encode( (unsigned const char *)dawExtraState.tuningContents.c_str(), + dawExtraState.tuningContents.size() ).c_str() ); + dawExtraXML.InsertEndChild(tnc); + } + patch.InsertEndChild(dawExtraXML); + doc.InsertEndChild(decl); doc.InsertEndChild(patch); diff --git a/src/common/SurgeStorage.h b/src/common/SurgeStorage.h index 81a77a7ada5..8f6db5372c4 100644 --- a/src/common/SurgeStorage.h +++ b/src/common/SurgeStorage.h @@ -387,6 +387,21 @@ struct StepSequencerStorage unsigned int trigmask; }; +/* +** There are a collection of things we want your DAW to save about your particular instance +** but don't want saved in your patch. So have this extra structure in the patch which we +** can activate/populate from the DAW hosts. See #915 +*/ +struct DAWExtraStateStorage +{ + bool isPopulated = false; + + int instanceZoomFactor = -1; + bool mpeEnabled = false; + bool hasTuning = false; + std::string tuningContents = ""; +}; + class SurgeStorage; class SurgePatch @@ -423,6 +438,8 @@ class SurgePatch StepSequencerStorage stepsequences[2][n_lfos]; + DAWExtraStateStorage dawExtraState; + std::vector param_ptr; std::vector easy_params_id; diff --git a/src/common/SurgeSynthesizer.h b/src/common/SurgeSynthesizer.h index 7b7570e9be7..3732d04980a 100644 --- a/src/common/SurgeSynthesizer.h +++ b/src/common/SurgeSynthesizer.h @@ -167,6 +167,26 @@ class alignas(16) SurgeSynthesizer float vu_peak[8]; + void populateDawExtraState() { + storage.getPatch().dawExtraState.isPopulated = true; + storage.getPatch().dawExtraState.mpeEnabled = mpeEnabled; + storage.getPatch().dawExtraState.hasTuning = !storage.isStandardTuning; + if( ! storage.isStandardTuning ) + storage.getPatch().dawExtraState.tuningContents = storage.currentScale.rawText; + else + storage.getPatch().dawExtraState.tuningContents = ""; + } + void loadFromDawExtraState() { + if( ! storage.getPatch().dawExtraState.isPopulated ) + return; + mpeEnabled = storage.getPatch().dawExtraState.mpeEnabled; + if( storage.getPatch().dawExtraState.hasTuning ) + { + auto sc = Surge::Storage::parseSCLData(storage.getPatch().dawExtraState.tuningContents ); + storage.retuneToScale(sc); + } + } + public: int CC0, PCH, patchid; float masterfade = 0; diff --git a/src/common/Tunings.cpp b/src/common/Tunings.cpp index 9a6dc24059a..c94977cd925 100644 --- a/src/common/Tunings.cpp +++ b/src/common/Tunings.cpp @@ -31,21 +31,13 @@ should give a read error and be rejected. */ -Surge::Storage::Scale Surge::Storage::readSCLFile(std::string fname) +Surge::Storage::Scale scaleFromStream(std::istream &inf) { - std::ifstream inf; - inf.open(fname); - if (!inf.is_open()) - { - return Scale(); - } - std::string line; const int read_header = 0, read_count = 1, read_note = 2; int state = read_header; - Scale res; - res.name = fname; + Surge::Storage::Scale res; std::ostringstream rawOSS; while (std::getline(inf, line)) { @@ -65,16 +57,16 @@ Surge::Storage::Scale Surge::Storage::readSCLFile(std::string fname) state = read_note; break; case read_note: - Tone t; + Surge::Storage::Tone t; t.stringRep = line; if (line.find(".") != std::string::npos) { - t.type = Tone::kToneCents; + t.type = Surge::Storage::Tone::kToneCents; t.cents = atof(line.c_str()); } else { - t.type = Tone::kToneRatio; + t.type = Surge::Storage::Tone::kToneRatio; auto slashPos = line.find("/"); if (slashPos == std::string::npos) { @@ -103,6 +95,28 @@ Surge::Storage::Scale Surge::Storage::readSCLFile(std::string fname) return res; } +Surge::Storage::Scale Surge::Storage::readSCLFile(std::string fname) +{ + std::ifstream inf; + inf.open(fname); + if (!inf.is_open()) + { + return Scale(); + } + + auto res = scaleFromStream(inf); + res.name = fname; + return res; +} + +Surge::Storage::Scale Surge::Storage::parseSCLData(const std::string &d) +{ + std::istringstream iss(d); + auto res = scaleFromStream(iss); + res.name = "Scale from Patch"; + return res; +} + std::ostream& Surge::Storage::operator<<(std::ostream& os, const Surge::Storage::Tone& t) { os << (t.type == Tone::kToneCents ? "cents" : "ratio") << " "; diff --git a/src/common/Tunings.h b/src/common/Tunings.h index 4360d3cb1d0..4b7eb3fdd4a 100644 --- a/src/common/Tunings.h +++ b/src/common/Tunings.h @@ -45,5 +45,6 @@ std::ostream& operator<<(std::ostream& os, const Tone& sc); std::ostream& operator<<(std::ostream& os, const Scale& sc); Scale readSCLFile(std::string fname); +Scale parseSCLData(const std::string &sclContents); } // namespace Storage } // namespace Surge diff --git a/src/common/gui/SurgeGUIEditor.cpp b/src/common/gui/SurgeGUIEditor.cpp index d8b2cc09c5d..71e5b0799d4 100644 --- a/src/common/gui/SurgeGUIEditor.cpp +++ b/src/common/gui/SurgeGUIEditor.cpp @@ -97,6 +97,14 @@ SurgeGUIEditor::SurgeGUIEditor(void* effect, SurgeSynthesizer* synth) : super(ef // init the size of the plugin int userDefaultZoomFactor = Surge::Storage::getUserDefaultValue(&(synth->storage), "defaultZoom", 100); float zf = userDefaultZoomFactor / 100.0; + + if( synth->storage.getPatch().dawExtraState.isPopulated && + synth->storage.getPatch().dawExtraState.instanceZoomFactor > 0 + ) + { + // If I restore state before I am constructed I need to do this + zf = synth->storage.getPatch().dawExtraState.instanceZoomFactor / 100.0; + } rect.left = 0; rect.top = 0; diff --git a/src/common/gui/SurgeGUIEditor.h b/src/common/gui/SurgeGUIEditor.h index d24b770d83d..c81a92614c0 100644 --- a/src/common/gui/SurgeGUIEditor.h +++ b/src/common/gui/SurgeGUIEditor.h @@ -148,6 +148,20 @@ class SurgeGUIEditor : public EditorType, public VSTGUI::IControlListener, publi bool zoomEnabled = true; public: + + void populateDawExtraState(SurgeSynthesizer *synth) { + synth->storage.getPatch().dawExtraState.isPopulated = true; + synth->storage.getPatch().dawExtraState.instanceZoomFactor = zoomFactor; + } + void loadFromDAWExtraState(SurgeSynthesizer *synth) { + if( synth->storage.getPatch().dawExtraState.isPopulated ) + { + auto sz = synth->storage.getPatch().dawExtraState.instanceZoomFactor; + if( sz > 0 ) + setZoomFactor(sz); + } + } + void setZoomCallback(std::function< void(SurgeGUIEditor *) > f) { zoom_callback = f; setZoomFactor(getZoomFactor()); // notify the new callback diff --git a/src/vst2/Vst2PluginInstance.cpp b/src/vst2/Vst2PluginInstance.cpp index 11ba6062077..75805b9033e 100644 --- a/src/vst2/Vst2PluginInstance.cpp +++ b/src/vst2/Vst2PluginInstance.cpp @@ -511,6 +511,10 @@ VstInt32 Vst2PluginInstance::getChunk(void** data, bool isPreset) if (!tryInit()) return 0; + _instance->populateDawExtraState(); + if( editor ) + ((SurgeGUIEditor *)editor)->populateDawExtraState(_instance); + return _instance->saveRaw(data); //#endif } @@ -525,6 +529,10 @@ VstInt32 Vst2PluginInstance::setChunk(void* data, VstInt32 byteSize, bool isPres _instance->loadRaw(data, byteSize, false); + _instance->loadFromDawExtraState(); + if( editor ) + ((SurgeGUIEditor *)editor)->loadFromDAWExtraState(_instance); + return 1; } diff --git a/src/vst3/SurgeVst3Processor.cpp b/src/vst3/SurgeVst3Processor.cpp index b6c42f03479..dd713b1ab18 100644 --- a/src/vst3/SurgeVst3Processor.cpp +++ b/src/vst3/SurgeVst3Processor.cpp @@ -159,6 +159,10 @@ tresult PLUGIN_API SurgeVst3Processor::getState(IBStream* state) CHECK_INITIALIZED void* data = nullptr; // surgeInstance keeps its data in an auto-ptr so we don't need to free it + surgeInstance->populateDawExtraState(); + for( auto e : viewsSet ) + e->populateDawExtraState(surgeInstance.get()); + unsigned int stateSize = surgeInstance->saveRaw(&data); state->write(data, stateSize); @@ -179,6 +183,10 @@ tresult PLUGIN_API SurgeVst3Processor::setState(IBStream* state) if (result == kResultOk) { surgeInstance->loadRaw(data, numBytes, false); + surgeInstance->loadFromDawExtraState(); + for( auto e : viewsSet ) + e->loadFromDAWExtraState(surgeInstance.get()); + } free(data);