How to share data between instances
If you want your plugins to communicate you can achieve it in various ways. We'll show two possible solutions tested in our projects:
- Using built-in "model" mechanism (GUI-side);
- Using shared memory (DSP side, C/C++);
Using built-in "model" mechanism
Every Blue Cat Audio plugin has a feature that allows sharing data between instances of the same plugin (on the GUI side). It's not documented and is sometimes tricky to use in Plug'n Script, because the "model" works on the plugin level and in Plug'n Script we work with different scripts doing different jobs, so it's like having many different plugins. So it's a little tricky. But with some effort you can make it work and it can do some amazing things.
Maybe you've noticed that in the Plug'n Script skin you can select the default editor for editing script, kuiml or opening the log files. And when you change it, it applies to all instances of Plug'n Script. If you take a look inside the skin, the name of the editor is kept in the string, for example: "global.edit_script_application". If you try displaying it and changing it manually, you'll see that it updates immediately in all instances of the plugin.
- <TEXT value="global.edit_script_application:" h_align="left" opacity="0.5" />
- <TEXT_EDIT_BOX string_id="global.edit_script_application" width="300" change_on_type="true" />
So this is definitely something we can use to communicate between instances.
Model folder location
The file where this string is declared is called "global.model" and is located inside the Plug'n Script plugin in the "Model" folder.
The path on Mac for PnS VST3 version is (use 'Show Package Contents'):
/Library/Audio/Plug-Ins/VST3/BC Plug'n Script VST3.vst3/Contents/Resources/BC Plug'n Script VST3 data/Model/
On Windows the location is similar:
C:\Program Files\Common Files\VST3\BC Plug'n Script VST3 data\Model\
* For each version of the Plug'n Script (VST2, VST3, AU, AAX, Standalone) there's a dedicated "Model" folder, so find the right one for the version you're using.
Let's add a new param into the global.model file:
- <?xml version="1.0" encoding="utf-8" ?>
- <MODEL id="global">
- <!-- these strings are declared by Blue Cat Audio -->
- <INCLUDE_ONCE file="global.$_SYSTEM_TYPE_$.inc"/>
- <STRING id="edit_script_application" default="$DEFAULT_SCRIPT_EDITOR$" exposed="true" persistent="true"/>
- <STRING id="edit_kuiml_application" default="$DEFAULT_SCRIPT_EDITOR$" exposed="true" persistent="true"/>
- <STRING id="open_log_application" default="$DEFAULT_SCRIPT_EDITOR$" exposed="true" persistent="true"/>
- <!-- we add another global param -->
- <PARAM id="common_gain" min="-20" max="20" default="0" unit="dB" exposed="true" persistent="true" />
- </MODEL>
After we've updated the model we have to reload the plugin (sometimes it's easier to reload the whole project or restart the host).
After reloading the plugin we can access this parameter in our skin like this:
- <PARAM_TEXT_CONTROL param_id="global.common_gain" />
Note that this will be available in the main skin (the .xml file located in the Skins folder). But if you don't use it in the main skin it will be "stripped" and will not be visible in the "subskin" (.kuiml file). To prevent stripping this parameter we can add this line to the main skin:
- <REQUIRED_OBJECTS object_ids="global.common_gain" />
Or we could also use a mask like this (but it can be slow if we have a lot of objects):
- <REQUIRED_OBJECTS object_ids="global.*" />
Now we can use this shared global parameter in our plugin in .kuiml as well.
- <TEXT value="Shared gain" />
- <PNS_BLACK_VINTAGE_B_KNOB param_id="global.common_gain" />
- <PARAM_TEXT_CONTROL param_id="global.common_gain" />
Note that we've declared this parameter with persistent="true" - this makes it restore its state even when the project is reloaded. Using exposed="true" is required for it to be seen in the main skin and not only in the model itself.
Three models
If you examine other Blue Cat Audio plugins you'll see that there can be up to three different models in Model folder.
- shared.model - parameters and strings declared inside are shared between instances, so it's like a communication layer
- global.model - almost like shared.model, but it also can keep values persistent between sessions - useful as a place for global preferences
- model.model - parameters and strings are not shared, so here you can keep things that can be different in each instance
To get some ideas on how you can use these models, take a look at Blue Cat's Gain Suite. Install the plugin and examine its "Model" folder. You'll see how parameters can be linked, unlinked, so that plugin instances are grouped. Also many other BCA plugins use this "Model" thing, so check them out to learn.
Advanced possibilities
This built-in "model" allows you to also run actions via ACTION_TRIGGER, use TIMER, execute scripts and do a lot of other stuff that you usually can't do when the GUI is closed. So even if GUI is closed and DSP is bypassed, you can still do some work in the Model. Though it's not recommended to do heavy processing here, because the model thread is shared between all instances of a plugin.
Exporting
The default skin for Plug'n Script doesn't support exporting model files (when building a plugin). So you can either manually copy the Model folder into your exported plugin, or use LM Skin. When you change your model files it detects that and shows export model options in the Export window.
You can also use a "trick" to keep model files separated for each particular script and for proper exporting. Say your main script is called "comp.cxx". You can create "comp.model", "comp.model.global" and "comp.model.shared" in the same folder where the script is. Then in the PnS "Model" folder create additional "global-custom.model", "model-custom.model" and "shared-custom.model" files.
In the global.model file keep all the code that is related to all scripts, and in the end of it add an include:
- <INCLUDE_ONCE ignore_missing="true" file="global-custom.model" />
And in "global-custom.model" include the actual file in the script's folder:
- <INCLUDE_ONCE ignore_missing="true" file="/PNS/Scripts/Comp/comp.model.global" />
The same you can do for model.model and for shared.model.
After the export LM Skin renames the comp.model.global from the script's folder to global-custom.model and puts it into the Model folder of the exported plugin (together with global.model - remember that global.model includes this global-custom.model). The same happens for the shared.model and for model.model. So in the exported plugin you have the model files required for this particular plugin, and your model files are easy to access and edit.
Downsides of using a model
The main problems are that it requires reloading the plugin after update and that it is very tricky to debug. If there's an error in the model, it just will not load (without any error messages) and then you usually get a lot of errors in the main skin that can't find some model objects. For getting some messages from the model you can create a string in the model and display it in the main skin, so that you can have some feedback while developing/debugging. Also for linking model params to DSP params you have to deeply understand parameters mapping and how they differ inside PnS and when exported.
Also the communication is not instant. It usually takes from 10 milliseconds for parameters to update. Also remember that the parameters are shared only between instances of the same plugin (and within the same DAW/project/plugin format).
In general a "model" is a very powerful gem in Plug'n Script and other Blue Cat Audio plugins that can help you in various ways, mostly related to the GUI-side of the plugin.
Using shared memory (DSP side)
If you've already switched to C/C++ (from AngelScript), you can try various ways to access memory. Scripts written in AngelScript don't have a direct access to memory, so the only way to commucate with the "outer world" seems to be reading and writing to files. (We also did some experiments opening virtual COM-ports as files, but we wouldn't recommend it as a reliable option).
In C/C++ you can making a variable "static" hoping that it will be shared between instances. But each time PnS plugin loads your compiled code it's renamed and opened as a new library, so "static" will not work as expected.
One of the options that Blue Cat Audio recommends is making another dynamic library that you can load, and use it as a place for sharing data. We tried that and it works, but there's another way we've found that doesn't require building additional libraries - opening virtual files in shared memory.
Opening a virtual file in shared memory
In this demo we've created a virtual file in shared memory (allocated some space there), got an address of the first byte of that shared memory and used it to share a simple value. See more comments and explanations in the code.
Here we share the routines that can be used to access that shared memory on both Mac and Windows.
Contents of the main dsp file (test_shared_mem.cpp)
- // include required stuff
- #include <cmath>
- #include <string>
- #include <vector>
- // include factory Blue Cat Audio libraries
- #include "../library/dspapi.h"
- #include "../library/cpphelpers.h"
- // include LetiMix library to add params in a cleaner way
- // see https://pns.letimix.com/how-to/add-params-more-conveniently
- #include "../library/params.h"
- // include shared memory functions (see below)
- #include "shared_memory.h"
- // script name
- DSP_EXPORT string name="Shared memory";
- // placeholders for input and output params indexes
- int GAIN, SHARED_GAIN;
- // to prevent adding params twice
- bool params_were_initialized = false;
- // initialize (usually done once on plugin loading)
- if (!params_were_initialized) {
- // add input params
- GAIN = ip("Local", "dB", -20, 20, 0, ".2");
- // add output params
- SHARED_GAIN = op("Shared", "dB", -20, 20, 0, ".2");
- }
- // try to initialize shared memory (see shared_memory.h)
- if (initSharedMemory() == false) {
- print("Shared memory unavailable!");
- return false;
- }
- return true;
- }
- ///////////////////////////////////////////////
- // now functions executed when the plugin is running
- // read input param changes
- double gain = IP[GAIN];
- if (sh != nullptr) {
- // set value in shared memory
- sh->gain = gain;
- // print("updated to " + std::to_string(sh->gain));
- }
- }
- // output values
- if (sh != nullptr) {
- // output value from shared memory
- OP[SHARED_GAIN] = sh->gain;
- }
- }
- ///////////////////////////////////////////////
- // when the plugin is unloading
- // shutdown shared memory (see shared_memory.h)
- shutDownSharedMemory();
- }
Contents of the shared_memory.h
- // THIS IS A DEMO LIBRARY FOR USING SHARED MEMORY
- // Jan 2023 // Ilya Orlov // support@letimix.com
- // Down below be add a function called 'sharedMem_getAddr'
- // implemented for both Mac and Windows machines.
- // This function gets a 'virtual file name' and
- // size in bytes (the amount of shared memory you need).
- // On success it returns a pointer to start of that shared memory block.
- // You can use this memory the way you want,
- // for example use that start address (memAddr) + offset to read or write
- // using something like memset or memmove
- // But it's convenient to make a "structure" and place it
- // in the beginning of that shared memory block
- // so you can later read and write easily like this
- // sh->gain = 0.5; or double freq = sh->freq;
- struct sharedParamsStruct{
- double gain = 0;
- double freq = 0;
- double some_data[512];
- };
- // this will be a pointer to that sharedParamsStruct in shared memory
- // so we could use it like sh->gain = 10;
- sharedParamsStruct * sh = nullptr;
- // variables for shared memory allocation
- int memSize = 0; // will get the value after allocation
- void* memAddr = nullptr; // will get the value after allocation
- ////////////////////////
- // SOME HELPERS WE USE
- // PnS print implementation with std::string
- void print(std::string s){
- if (s.length() > 0)
- print(s.c_str());
- }
- /////////////////////////////////////////////////////////////
- // NOW LET'S IMPLEMENT A FUNCTION TO GET THAT SHARED MEMORY
- /////////////////////////
- // The implementation is different on Windows and Mac, but the result
- // is the same - you get an address in shared memory where
- // you can read and write.
- // Notice that this address will be different for each plugin instance.
- // Because it's not the "real" address, it's translated by the OS.
- // But it works like you're accessing the same memory.
- ////////////////////////
- // for Mac
- #if defined(__clang__)
- // MAC
- #include <sys/mman.h>
- #include <sys/fcntl.h>
- #include <errno.h>
- #include <unistd.h>
- int fdMapFile = -1;
- void* pMemAddr = nullptr;
- size_t _memSize = 0;
- // returns pointer to shared memory addess
- void* sharedMem_getAddr(const char* virtualFileName, int & sizeToAllocate){
- // get page size (or granularity, which is bigger) on current system
- double pageSize = 0.0 + sysconf(_SC_PAGE_SIZE);
- // update sizeToAllocate
- sizeToAllocate = pageSize*ceil((0.0 + sizeToAllocate)/pageSize);
- // get shared memory file descriptor
- fdMapFile = shm_open(virtualFileName, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
- if (fdMapFile == -1) {
- print("shm_open error");
- pMemAddr = nullptr;
- return pMemAddr;
- }
- // extend shared memory object as by default it's initialized with size 0
- ftruncate(fdMapFile, sizeToAllocate);
- // map shared memory to process address space
- pMemAddr = mmap(NULL, sizeToAllocate, PROT_WRITE|PROT_READ, MAP_SHARED, fdMapFile, 0);
- if (pMemAddr == MAP_FAILED) {
- print("mmap error. sizeToAllocate: " + std::to_string(sizeToAllocate));
- pMemAddr = nullptr;
- return pMemAddr;
- } else {
- _memSize = sizeToAllocate;
- }
- return pMemAddr;
- }
- // on closing
- void sharedMem_onShutdown(){
- if (pMemAddr != nullptr) {
- munmap(pMemAddr, _memSize);
- }
- if (fdMapFile >-1) {
- close(fdMapFile);
- }
- // we could also delete the virtual file, but we don't have to
- // shm_unlink(virtualFileName);
- }
- #else
- // WINDOWS
- #define WIN32_LEAN_AND_MEAN 1
- #include <windows.h>
- HANDLE hMapFile = 0;
- void* pMemAddr = nullptr;
- // returns pointer to shared memory address
- void* sharedMem_getAddr(const char* baseFileName, int & sizeToAllocate){
- // define real size to allocate depending on page size
- // get page size (or granularity, which is bigger) on current system
- double pageSize = 0;
- SYSTEM_INFO system_info;
- GetSystemInfo(&system_info);
- pageSize = system_info.dwPageSize;
- if (system_info.dwAllocationGranularity > pageSize) {
- pageSize = system_info.dwAllocationGranularity;
- }
- // update sizeToAllocate
- sizeToAllocate = pageSize*ceil((0.0 + sizeToAllocate)/pageSize);
- std::string virtualFileName = std::string("Local\\") + baseFileName;
- // get handle to virtual file
- hMapFile = OpenFileMapping( FILE_MAP_ALL_ACCESS, FALSE, virtualFileName.c_str());
- if (hMapFile == nullptr) {
- hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, sizeToAllocate, virtualFileName.c_str());
- }
- if (hMapFile == nullptr) {
- print("Could not create file mapping object. Error: "+std::to_string(GetLastError()));
- pMemAddr = nullptr;
- return pMemAddr;
- }
- // map view of virtual file in memory
- pMemAddr = (void*) MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, sizeToAllocate);
- if (pMemAddr == nullptr) {
- print("Could not map view of file. Error: "+std::to_string(GetLastError()));
- CloseHandle(hMapFile);
- pMemAddr = nullptr;
- return pMemAddr;
- }
- return pMemAddr;
- }
- // on closing script
- void sharedMem_onShutdown(){
- UnmapViewOfFile(pMemAddr);
- CloseHandle(hMapFile);
- }
- #endif
- ////////////////////////
- // initialize shared memory
- bool initSharedMemory() {
- // prevent double initialization of shared memory
- if (sh == nullptr) {
- // for the 'virtual file name' you may also want to add
- // name of the current app (DAW), current userid and other stuff
- // this way you'll have different 'shared memories'
- // when using plugin in different DAWs on the same computer
- // or when using multiple user accounts on the same machine
- std::string virtualFileName = std::string("my_virtual_file");
- // how much memory we need
- int extra_memory = 16384; // in addition to sizeof sharedParamsStruct
- int memSizeToRequest = sizeof(sharedParamsStruct) + extra_memory;
- // ask for memory
- memSize = memSizeToRequest;
- memAddr = sharedMem_getAddr(virtualFileName.c_str(), memSize);
- // if we got a memory address, it's a success
- if (memAddr != nullptr) {
- // place the "sh" pointer at the start of that memory
- sh = (sharedParamsStruct *) memAddr;
- } else {
- sh = nullptr;
- }
- // output some debug info
- print("Virtual file name: '" + virtualFileName + "'; requested: " + std::to_string(memSizeToRequest) + "; allocated: "+ std::to_string(memSize) + " bytes");
- }
- if (sh == nullptr)
- return false;
- else
- return true;
- }
- // on unloading the plugin
- void shutDownSharedMemory(){
- // release what we're using
- sharedMem_onShutdown();
- }
Sending audio using shared memory
One of the things you can do using shared memory is send audio data from one plugin to another. At first it seems like an easy task - just copy samples from the current block in the sender to the shared memory, and in receiver copy these samples from shared memory to the current block samples.
It would actually work in some DAWs, but in other DAWs it will not, because the block size in sender and receiver can be different, and block size can also change during automation and in other situations. So the solution is to have a buffer in shared memory long enough for the largest possible block size (or maybe x2 or x3 of that) and just to copy samples to the end when you have them in the Sender. And in Receiver keep track of the last reading position in the buffer and read as many samples as you need for the current block.
When you get to the end of the buffer, jump to the start: this cyclic writing works much faster than moving the whole buffer down in memory to add another block at the end - don't do it, it's slow and hard on CPU.
If in Receiver you need to add a delay to compensate for the delay between Sender and Receiver, you can either make the shared memory buffer long enough, or add another (larger) buffer in the receiver that can enlarge depending on the required delay.
Also be aware of threading problems, which can occure when sending audio from one channel to another, and the channels are unrelated. DAW can process them on different threads (and on different CPU cores), so that in Sender you can have already say 7 blocks processed, and in Receiver only 1, or vice versa (which is worse). In that case you may also need to add a delay in Receiver so that it always has enough audio to read.
In general sending audio is an interesting and sometimes challenging topic, feel free to share your thoughts and experience in the comments.