DSSI plugin wrapper


#1

I have written a quite complete DSSI wrapper. DSSI ( http://dssi.sourceforge.net/ ) is a plugin format, mostly for Linux, supported by a few host such as Renoise 2.6, Rosegarden, QTractor, Jost, etc . It is built as an extension of LADSPA, adding support for midi input, GUI handling, and programs.

The wrapper code is based on the juce vst wrapper, with its non-linux parts striped. The code is really simple, there are no tricks, except for … the gui. DSSI allows plugin guis, but they must be launched by an external process, and communicate with the host via OSC messages. All of this is quite a pain to deal with.

If your plugin is foo, it is installed as

$DSSI_PATH/foo.so <- the plugin code
$DSSI_PATH/foo/foo_gui <- the plugin gui application

Of course, the typical Juce plugin does not fit well into this model… So I have just created a dumb “external gui” program that all requests to the plugin itself, telling it to show or hide its editor component (in a top-level window). This is of course twisting the dssi spec a little bit since the purpose of the separate plugin gui process was to control the plugin while it runs on a remote computer.

The code below requires an OSC library – it may be liblo ( http://liblo.sourceforge.net ) or oscpkt ( http://gruntthepeon.free.fr/oscpkt ). If you want to use liblo, just #define USING_LIBLO

Now there is something missing in the DSSI specs, and I became aware of that fact only after I had completed everything else in that wrapper… It renders this wrapper almost useless for me. The thing missing is support for opaque “chunks” for saving the plugin state (i.e. AudioProcessor::getStateInformation for juce plugins). DSSI only supports recalling programs, and setting individual parameter values, so if your plugin makes use of chunks to save its state, that wrapper won’t allow DSSI hosts to fully save / restore your plugin state.

Here is the code for the wrapper, let’s name it dssi_wrapper.cc :

[code]/*
DSSI Plugin wrapper.

Recall to : disable XInitThreads in
doPlatformSpecificInitialisation/doPlatformSpecificShutdown with
also the signal redirection stuff, and the X11 io error stuff (this
is done in juce-git since 2010/10/28).

Recall also to export only a few symbols, in order to avoid
potential clashes with the host symbols. The best is to use a linker
script:

g++ -shared -Wl,version-script=linux_dssi_symbols.map

with linux_dssi_symbols.map:
{
global:
ladspa_descriptor;
dssi_descriptor;
dssi_gui_main;
local: *;
};

And build your sources with -fvisibility=hidden. The best is also to
check that nm -D plugin.so does not list a ton of exported symbols
that may clash, such as zlib functions / png stuff or md5_init …
Stuff from libstdc++ should be safe, I think.

IMPORTANT: make sure that the .so file used for the VST plugin and
the DSSI are not symlinks to the same physical file, or that will
cause issues (if a host allows VST and DSSI to be loaded at the same
time, such as renoise, then in that case both vst and dssi will
share the same juce environment, but they both use a different
message thread, and they both call ShutdownJuce_GUI() without caring
for each other so horrible crashes happen).

*/

#include
#if defined(TARGET_LINUX)
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#undef KeyPress
#endif

#include “dssi.h”
#include “ladspa.h”
#include <alsa/asoundlib.h>
#include “JucePluginCharacteristics.h”
#include <juce.h>

//#define USING_LIBLO
#ifdef USING_LIBLO
#include <lo/lo.h>
#else
#define OSCPKT_OSTREAM_OUTPUT
#include “oscpkt/oscpkt.hh”
#include “oscpkt/udp.hh”
#endif

/* conveniency functions for debugging …*/
inline std::ostream &operator<<(std::ostream &os, const juce::String &s) {
os << s.toUTF8();
return os;
}

BEGIN_JUCE_NAMESPACE
extern Display* display;
extern bool juce_postMessageToSystemQueue (void* message);
END_JUCE_NAMESPACE
class DssiSharedMessageThread : public Thread
{
public:
DssiSharedMessageThread()
: Thread (JUCE_T(“DssiMessageThread”)),
initialised (false)
{
startThread (7);
while (! initialised)
sleep (1);
}

~DssiSharedMessageThread()
{
signalThreadShouldExit();
JUCEApplication::quit();
waitForThreadToExit (5000);
clearSingletonInstance();
}

void run()
{
initialiseJuce_GUI();
initialised = true;
MessageManager::getInstance()->setCurrentThreadAsMessageThread();

while ((! threadShouldExit()) && MessageManager::getInstance()->runDispatchLoopUntil (250))
  {
  }

}

juce_DeclareSingleton (DssiSharedMessageThread, false)

private:
bool initialised;
};

juce_ImplementSingleton (DssiSharedMessageThread);

class JuceDSSIWrapper;

class DssiEditorCompWrapper : public DocumentWindow
{
JuceDSSIWrapper* wrapper;

public:
DssiEditorCompWrapper (JuceDSSIWrapper* const wrapper_,
AudioProcessorEditor* const editor, const String &title,
int xpos, int ypos)
: DocumentWindow(title, Colours::white, DocumentWindow::allButtons, false)
{
wrapper = wrapper_;
setOpaque (true);
setTitleBarHeight(0);
setUsingNativeTitleBar(true);
editor->setOpaque (true);
setDropShadowEnabled(false);
setContentComponent(editor, true, true);

if (xpos != -10000 && ypos != -10000) {
  setTopLeftPosition(xpos, ypos-20 /* position bug? */ );
} else setCentreRelative(.5f, .5f);

Component::addToDesktop(getDesktopWindowStyleFlags());
setVisible(true);

}

AudioProcessorEditor* getEditorComp() const
{
return dynamic_cast <AudioProcessorEditor*> (getContentComponent());
}

BorderSize getBorderThickness() const { return BorderSize(0); }
BorderSize getContentComponentBorder() const { return BorderSize(0); }

void closeButtonPressed();

juce_UseDebuggingNewOperator
};

struct OscAction : public juce::Message {
String msg, arg;
};
#ifdef USING_LIBLO
class DssiMinimalOscServer : public Thread {
lo_server serv;
MessageListener *listener;
public:
DssiMinimalOscServer() : Thread(JUCE_T(“osc”)), listener(0) {}
~DssiMinimalOscServer() {
jassert(!isThreadRunning());
stopServer();
}
void setListener(MessageListener *l) { listener = l; }
void sendMessageTo(const String &osc_url, const String &message_path,
const String &arg1 = String::empty, const String &arg2 = String::empty) {
char *url_hostname = lo_url_get_hostname(osc_url.toUTF8());
char *url_port = lo_url_get_port(osc_url.toUTF8());
char *url_path = lo_url_get_path(osc_url.toUTF8());

String path; 
path << url_path << message_path; path = path.replace("//","/");
lo_address hostaddr = lo_address_new(url_hostname, url_port);

if (!arg1.isNotEmpty()) {
  lo_send(hostaddr, path.toUTF8(), "");
} else if (!arg2.isNotEmpty()) {
  lo_send(hostaddr, path.toUTF8(), "s", arg1.toUTF8());
} else {
  lo_send(hostaddr, path.toUTF8(), "ss", arg1.toUTF8(), arg2.toUTF8());
}
lo_address_free(hostaddr);
free(url_hostname);
free(url_port);
free(url_path);

}
void startServer() {
if (isThreadRunning()) return;
serv = lo_server_new(NULL, NULL);
lo_server_add_method(serv, NULL, NULL, &osc_callback, this);
startThread();
}
void stopServer() {
if (!isThreadRunning()) return;
signalThreadShouldExit();
stopThread(1500);
lo_server_free(serv);
}
String getOscUrl() {
char *s = lo_server_get_url(serv);
String url; url << s; free(s);
return url;
}
void run() {
while (!threadShouldExit()) {
lo_server_recv_noblock(serv, 50);
}
}
static int osc_callback(const char *path, const char *types,
lo_arg **argv, int argc, lo_message , void *user_data) {
OscAction a = new OscAction;
a->msg << path;
if (argc > 0 && types[0] == ‘s’) {
a->arg << &argv[0]->s;
}
((DssiMinimalOscServer
)user_data)->listener->postMessage(a);
return 0;
}
};
#else
// ad-hoc server that implements only what we need for dssi…
class DssiMinimalOscServer : public Thread {
oscpkt::UdpSocket serv;
MessageListener *listener;
public:
DssiMinimalOscServer() : Thread(JUCE_T(“osc”)), listener(0) {}
void setListener(MessageListener *l) { listener = l; }
void sendMessageTo(const String &osc_url, const String &message_path,
const String &arg1 = String::empty, const String &arg2 = String::empty) {
oscpkt::Url url(osc_url.toUTF8());
if (!url.isOk()) return;

oscpkt::PacketWriter pw;
oscpkt::Message msg;
std::string path = url.path; 
if (!path.empty() && path[path.size()-1] == '/') path.resize(path.size()-1);
path += message_path.toUTF8();

msg.init(path);
if (arg1.isNotEmpty()) msg.pushStr(arg1.toUTF8());
if (arg2.isNotEmpty()) msg.pushStr(arg2.toUTF8());
pw.addMessage(msg);

oscpkt::UdpSocket sock; sock.connectTo(url.hostname, url.port);
bool ok = sock.sendPacket(pw.packetData(), pw.packetSize());
if (!ok) {
  cerr << "Could not send " << msg.addressPattern() << " message to " 
       << url.hostname << ":" << url.port << " '" << sock.errorMessage() << "'\n";
}

}
void startServer() {
if (isThreadRunning()) return;
serv.bindTo(0 /* any port /);
if (!serv.isOk()) {
cerr << "cannot start osc server: " << serv.errorMessage() << “\n”; return;
}
startThread();
}
void stopServer() {
if (!isThreadRunning()) return;
signalThreadShouldExit();
stopThread(1500);
serv.close();
}
String getOscUrl() {
String s; s << (“osc.udp://” + serv.localHostNameWithPort() + “/”).c_str();
return s;
}
void run() {
while (!threadShouldExit() && serv.isOk()) {
if (serv.receiveNextPacket(50 /
timeout, in ms */)) {
oscpkt::PacketReader pr(serv.packetData(), serv.packetSize());
if (pr.isOk()) {
oscpkt::Message *msg;
while ((msg = pr.popMessage())) {
OscAction *a = new OscAction;
a->msg << msg->addressPattern().c_str();
if (msg->arg().nbArgRemaining() && msg->arg().isStr()) {
std::string s; msg->arg().popStr(s);
a->arg << s.c_str();
}
listener->postMessage(a);
}
}
}
}
}
};
#endif

static Array<void*> activePlugins;

extern AudioProcessor* JUCE_CALLTYPE createPluginFilter();

static LADSPA_Descriptor *ladspa_desc = 0;
static DSSI_Descriptor *dssi_desc = 0;

struct JuceDSSIWrapper : public Timer, public MessageListener {

static LADSPA_Descriptor *getLadspaDescriptor() {
if (!ladspa_desc) initialiseDescriptors();
return ladspa_desc;
}

static DSSI_Descriptor *getDssiDescriptor() {
if (!dssi_desc) initialiseDescriptors();
return dssi_desc;
}

static void initialiseDescriptors();

static void destroyDescriptors() {
if (ladspa_desc) {
for (size_t i=0; i < ladspa_desc->PortCount; ++i) {
free((void*)ladspa_desc->PortNames[i]);
}
delete[] ladspa_desc->PortDescriptors;
delete[] ladspa_desc->PortNames;
delete[] ladspa_desc->PortRangeHints;
delete ladspa_desc;
ladspa_desc = 0;
}
if (dssi_desc) {
delete dssi_desc;
dssi_desc = 0;
}
}

static void callbackCleanup(LADSPA_Handle instance) {
{
MessageManagerLock mmLock;
delete (JuceDSSIWrapper*)instance;
}
if (activePlugins.size() == 0) {
DssiSharedMessageThread::deleteInstance();
shutdownJuce_GUI();
}
}

static LADSPA_Handle callbackInstantiate(const LADSPA_Descriptor *,
unsigned long s_rate) {
if (activePlugins.size() == 0) {
DssiSharedMessageThread::getInstance();
}
MessageManagerLock mmLock;
return new JuceDSSIWrapper(s_rate);
}

static void callbackConnectPort(LADSPA_Handle instance, unsigned long port,
LADSPA_Data * data) {
MessageManagerLock mmLock;
((JuceDSSIWrapper*)instance)->connectPort(port, data);
}

static void callbackActivate(LADSPA_Handle instance) {
MessageManagerLock mmLock;
((JuceDSSIWrapper*)instance)->activate();
}

static void callbackDeactivate(LADSPA_Handle instance) {
MessageManagerLock mmLock;
((JuceDSSIWrapper*)instance)->deactivate();
}

static void callbackRunAsEffect(LADSPA_Handle instance,
unsigned long sample_count) {
((JuceDSSIWrapper*)instance)->run(sample_count, 0, 0);
}

static void callbackRun(LADSPA_Handle instance, unsigned long sample_count,
snd_seq_event_t events, unsigned long event_count) {
((JuceDSSIWrapper
)instance)->run(sample_count, events, event_count);
}

static char* callbackConfigure(LADSPA_Handle instance,
const char *key, const char value) {
MessageManagerLock mmLock;
return ((JuceDSSIWrapper
)instance)->configure(key, value);
}

static const DSSI_Program_Descriptor callbackGetProgram(LADSPA_Handle instance,
unsigned long index) {
MessageManagerLock mmLock;
return ((JuceDSSIWrapper
)instance)->getProgram(index);
}

static void callbackSelectProgram(LADSPA_Handle instance,
unsigned long bank,
unsigned long program) {
MessageManagerLock mmLock;
return ((JuceDSSIWrapper*)instance)->selectProgram(bank, program);
}

private:
double sample_rate;
MidiBuffer midi_buffer;

float *output_port[JucePlugin_MaxNumOutputChannels];

#define UNSET_PARAMETER_VALUE 1e10
Array<float*> param_port;
Array param_saved; // value of the parameters saved at previous callback, to detect param value change

AudioProcessor *filter;

DssiEditorCompWrapper* editorComp;
bool shouldDeleteEditor;
bool hasShutdown;

int x_editor, y_editor;

DssiMinimalOscServer osc_server; // used only for comminucation with the gui
String gui_osc_url;

public:
JuceDSSIWrapper(unsigned long s_rate) {
editorComp = 0;
hasShutdown = false;
shouldDeleteEditor = false;
x_editor = y_editor = -10000;
osc_server.setListener(this);

for (int c=0; c < JucePlugin_MaxNumOutputChannels; ++c) output_port[c] = 0;
sample_rate = s_rate;
filter = createPluginFilter();
param_port.insertMultiple(0, 0, filter->getNumParameters());
param_saved.insertMultiple(0, UNSET_PARAMETER_VALUE, filter->getNumParameters());
activePlugins.add (this);
startTimer (1000);

}

~JuceDSSIWrapper() {
osc_server.stopServer();
stopTimer();
deleteEditor(false);
hasShutdown = true;
deleteAndZero(filter);
jassert (activePlugins.contains (this));
activePlugins.removeValue (this);
}

AudioProcessor *getFilter() { return filter; }

void connectPort(unsigned long port, LADSPA_Data data) {
if (port < JucePlugin_MaxNumOutputChannels) {
output_port[port] = data;
} else {
int param = (int)port - JucePlugin_MaxNumOutputChannels;
if (param < param_port.size()) {
param_port.set(param, (float
)data);
param_saved.set(param, UNSET_PARAMETER_VALUE);
}
}
}

void activate() {
unsigned block_size = 512;
filter->setNonRealtime(false);
filter->setPlayConfigDetails (0, JucePlugin_MaxNumOutputChannels,
sample_rate, block_size);
filter->prepareToPlay(sample_rate, block_size);
updateParameters();
midi_buffer.clear();
snd_midi_event_new(sizeof midi_parser_buffer, &midi_parser);
}

void deactivate() {
filter->releaseResources();
midi_buffer.clear();
snd_midi_event_free(midi_parser);
}

DSSI_Program_Descriptor latest_program_descriptor;
std::string latest_program_descriptor_name;
const DSSI_Program_Descriptor *getProgram(unsigned long index) {
if (index < (unsigned long)filter->getNumPrograms()) {
latest_program_descriptor.Bank = 0;
latest_program_descriptor.Program = index;
latest_program_descriptor_name = filter->getProgramName((int)index).toUTF8();
latest_program_descriptor.Name = latest_program_descriptor_name.c_str();
return &latest_program_descriptor;
}
return 0;
}

void selectProgram(unsigned long bank, unsigned long program) {
if (bank == 0) filter->setCurrentProgram((int)program);
updateParameters();
}

// update the port values from the plugin parameter values
void updateParameters() {
for (int i=0; i < param_port.size(); ++i) {
if (param_port[i]) {
float v = filter->getParameter(i);
*param_port[i] = v;
param_saved.set(i,v);
}
}
}

void run(unsigned long sample_count, snd_seq_event_t events, unsigned long event_count) {
/
handle incoming midi events */
if (event_count) {
for (size_t i=0; i < event_count; ++i) {
const int num_bytes = snd_midi_event_decode(midi_parser, midi_parser_buffer, sizeof midi_parser_buffer, &events[i]);
snd_midi_event_reset_decode(midi_parser);
if (num_bytes) {
midi_buffer.addEvent(midi_parser_buffer, num_bytes, events[i].time.tick);
}
}
}

/* handle parameter changes initiated by the host */
for (int i=0; i < param_port.size(); ++i) {
  if (param_port[i]) {
    if (param_saved[i] != *param_port[i]) {
      filter->setParameter(i, *param_port[i]);
    }
  }
}

{
  const ScopedLock sl (filter->getCallbackLock());
  if (filter->isSuspended()) {
    for (int i = 0; i < JucePlugin_MaxNumOutputChannels; ++i)
      zeromem (output_port[i], sizeof (float) * sample_count);
  } else {
    AudioSampleBuffer chans (output_port, JucePlugin_MaxNumOutputChannels, sample_count);
    filter->processBlock (chans, midi_buffer);
  }
}

/* read back parameter values */
updateParameters();

if (!midi_buffer.isEmpty()) { midi_buffer.clear(); }

}

struct FakeGuiConnectMessage : public Message {
String arg;
FakeGuiConnectMessage(const String &s) { arg = s; }
~FakeGuiConnectMessage() throw() {}
};

char *configure(const char *key, const char *value) {
if (strcmp(key, “guiVisible”) == 0) {
postMessage(new FakeGuiConnectMessage(String(value)));
}
return 0;
}

void handleMessage(const Message &msg) {
const FakeGuiConnectMessage fmsg;
if ((fmsg = dynamic_cast<const FakeGuiConnectMessage
>(&msg))) {
bool show = fmsg->arg.isNotEmpty();
if (show) {
StringArray arg; arg.addTokens(fmsg->arg, JUCE_T("|"), JUCE_T(""));
if (arg.size() == 2) {
gui_osc_url = arg[0];
String window_title = arg[1];

      /* only 1 gui will be opened at once, request for new guis will automatically close the older ones */
      deleteEditor (true);
      createEditorComp(window_title);
    }
  } else {
    deleteEditor (true);
  }
}

const OscAction *osc;
if ((osc = dynamic_cast<const OscAction*>(&msg))) {
  if (osc->msg == "/internal_gui_hide") {
    deleteEditor(true);
  } else if (osc->msg == "/exiting") {
    deleteEditor(true);
    gui_osc_url = String::empty;
  }
}

}

static AudioProcessor initialiseAndCreateFilter() {
initialiseJuce_GUI();
AudioProcessor
filter = createPluginFilter();
return filter;
}

void createEditorComp(const String &title);
void deleteEditor (bool canDeleteLaterIfModal);

void notifyRemoteProcess(bool b) {
if (b) {
osc_server.startServer();
osc_server.sendMessageTo(gui_osc_url, JUCE_T("/internal_gui_status"), osc_server.getOscUrl());
} else {
osc_server.sendMessageTo(gui_osc_url, JUCE_T("/internal_gui_status"), JUCE_T(""));
osc_server.stopServer();
}
}

void timerCallback() {
if (shouldDeleteEditor) {
shouldDeleteEditor = false;
deleteEditor (true);
}
if (osc_server.isThreadRunning() && gui_osc_url.isNotEmpty()) {
// perdiodically ping the gui process, so that it now it has not been abandonned as an orphan process…
notifyRemoteProcess(editorComp !=0 );
}
}

snd_midi_event_t* midi_parser;
uint8_t midi_parser_buffer[16384];

}; // end of class JuceDSSIWrapper

void DssiEditorCompWrapper::closeButtonPressed() {
wrapper->deleteEditor(true);
}

void JuceDSSIWrapper::createEditorComp(const String &title) {
if (hasShutdown || filter == 0)
return;

if (editorComp == 0) {
AudioProcessorEditor* const ed = filter->createEditorIfNeeded();
if (ed) {
editorComp = new DssiEditorCompWrapper(this, ed, title, x_editor, y_editor);
notifyRemoteProcess(true);
}
}
shouldDeleteEditor = false;
}

void JuceDSSIWrapper::deleteEditor (bool canDeleteLaterIfModal)
{
PopupMenu::dismissAllActiveMenus();

if (editorComp != 0) {
Component* const modalComponent = Component::getCurrentlyModalComponent();
if (modalComponent != 0) {
modalComponent->exitModalState (0);

  if (canDeleteLaterIfModal) {
    shouldDeleteEditor = true;
    return;
  }
}

filter->editorBeingDeleted (editorComp->getEditorComp());    
x_editor = editorComp->getX();
y_editor = editorComp->getY();
deleteAndZero (editorComp);

notifyRemoteProcess(false);

// there's some kind of component currently modal, but the host
// is trying to delete our plugin. You should try to avoid this happening..
jassert (Component::getCurrentlyModalComponent() == 0);

}
}

void JuceDSSIWrapper::initialiseDescriptors() {
initialiseJuce_GUI();
AudioProcessor *plugin = createPluginFilter();

char **port_names;
LADSPA_PortDescriptor *port_descriptors;
LADSPA_PortRangeHint *port_range_hints;

ladspa_desc = new LADSPA_Descriptor; assert(ladspa_desc);
ladspa_desc->UniqueID = JucePlugin_VSTUniqueID; // not used by dssi hosts anyway…
ladspa_desc->Label = “Main”; // must not contain white spaces
ladspa_desc->Properties = LADSPA_PROPERTY_REALTIME; //LADSPA_PROPERTY_HARD_RT_CAPABLE;
ladspa_desc->Name = JucePlugin_Name " DSSI Synth";
ladspa_desc->Maker = JucePlugin_Manufacturer;
ladspa_desc->Copyright = “Copyright © " JucePlugin_Manufacturer " 2010”;
ladspa_desc->PortCount = JucePlugin_MaxNumOutputChannels + plugin->getNumParameters();

port_descriptors = new LADSPA_PortDescriptor[ladspa_desc->PortCount];
memset(port_descriptors, 0, sizeof(LADSPA_PortDescriptor)*ladspa_desc->PortCount);
ladspa_desc->PortDescriptors = port_descriptors;

port_range_hints = new LADSPA_PortRangeHint[ladspa_desc->PortCount];
memset(port_range_hints, 0, sizeof(LADSPA_PortRangeHint)*ladspa_desc->PortCount);
ladspa_desc->PortRangeHints = port_range_hints;

port_names = new char *[ladspa_desc->PortCount];
ladspa_desc->PortNames = port_names;

unsigned long port = 0;
for (int channel=0; channel < JucePlugin_MaxNumOutputChannels; ++channel, ++port) {
char s[100]; snprintf(s, 100, “Output%d”, channel+1);
port_names[port] = strdup(s);

port_descriptors[port] = LADSPA_PORT_OUTPUT|LADSPA_PORT_AUDIO;
port_range_hints[port].HintDescriptor = 0;

}
for (int param=0; param < plugin->getNumParameters(); ++param, ++port) {
port_names[port] = strdup(plugin->getParameterName(param).toUTF8());
port_descriptors[port] = LADSPA_PORT_INPUT | LADSPA_PORT_CONTROL;
port_range_hints[port].HintDescriptor = LADSPA_HINT_BOUNDED_BELOW | LADSPA_HINT_BOUNDED_ABOVE;
port_range_hints[port].LowerBound = 0;
port_range_hints[port].UpperBound = 1;
}
jassert(port == ladspa_desc->PortCount);

ladspa_desc->activate = &callbackActivate;
ladspa_desc->cleanup = &callbackCleanup;
ladspa_desc->connect_port = &callbackConnectPort;
ladspa_desc->deactivate = &callbackDeactivate;
ladspa_desc->instantiate = &callbackInstantiate;
ladspa_desc->run = &callbackRunAsEffect;
ladspa_desc->run_adding = NULL;
ladspa_desc->set_run_adding_gain = NULL;

dssi_desc = new DSSI_Descriptor;
dssi_desc->DSSI_API_Version = 1;
dssi_desc->LADSPA_Plugin = ladspa_desc;
dssi_desc->configure = &callbackConfigure;
dssi_desc->get_program = callbackGetProgram;
dssi_desc->get_midi_controller_for_port = NULL;
dssi_desc->select_program = callbackSelectProgram;
dssi_desc->run_synth = &callbackRun;
dssi_desc->run_synth_adding = NULL;
dssi_desc->run_multiple_synths = NULL;
dssi_desc->run_multiple_synths_adding = NULL;

delete plugin;
shutdownJuce_GUI();
}

attribute((destructor)) void dssi_destructor()
{
jassert(activePlugins.size() == 0);
JuceDSSIWrapper::destroyDescriptors();
}

extern “C” attribute ((visibility(“default”))) const LADSPA_Descriptor *ladspa_descriptor(unsigned long index)
{
return (index == 0 ? JuceDSSIWrapper::getLadspaDescriptor() : 0);
}

extern “C” attribute ((visibility(“default”))) const DSSI_Descriptor *dssi_descriptor(unsigned long index)
{
return (index == 0 ? JuceDSSIWrapper::getDssiDescriptor() : 0);
}

/* ---------- the fake gui process starts below ---------- */

struct FakeExternalGUI : public MessageListener, public Timer {
String window_title;
String host_osc_url;
String plugin_osc_url;
DssiMinimalOscServer osc_server;
juce::Time time_last_ping;

FakeExternalGUI() { osc_server.setListener(this); startTimer(1000); }
~FakeExternalGUI() { osc_server.stopServer(); }

// notify the plugin via the host, using the ‘/configure’ callback
void show(bool do_show) {
String conf;
if (do_show) {
conf << osc_server.getOscUrl() << “|” << window_title;
}
osc_server.sendMessageTo(host_osc_url, “/configure”, “guiVisible”, conf);
if (!do_show && plugin_osc_url.isNotEmpty())
osc_server.sendMessageTo(plugin_osc_url, “/internal_gui_hide”, “0”);
}

void quit() {
MessageManager::getInstance()->stopDispatchLoop();
}

void init(const char *host_osc_url_, const char *plugin_so_name,
const char *label, const char *friendlyname) {
(void)plugin_so_name;
host_osc_url << host_osc_url_;
window_title << label << " - " << friendlyname;
osc_server.startServer();
osc_server.sendMessageTo(host_osc_url, “/update”, osc_server.getOscUrl() + “dssi”);
}

void handleMessage(const Message &msg) {
const OscAction osc;
if ((osc = dynamic_cast<const OscAction
>(&msg))) {
if (osc->msg == “/dssi/hide”) show(false);
else if (osc->msg == “/dssi/show”) show(true);
else if (osc->msg == “/dssi/quit”) quit();
else if (osc->msg == “/internal_gui_status”) {
plugin_osc_url = osc->arg;
time_last_ping = juce::Time::getCurrentTime();
if (!plugin_osc_url.isNotEmpty()) quit();
}
}
}

void timerCallback() {
juce::Time t = juce::Time::getCurrentTime();
if (plugin_osc_url.isNotEmpty() && (t-time_last_ping ).inMilliseconds() > 5000) {
/* no ping for 5 seconds, the fake gui process kills itself… */
quit();
}
}

void exiting() {
osc_server.sendMessageTo(host_osc_url, “/exiting”);
if (plugin_osc_url) osc_server.sendMessageTo(plugin_osc_url, “/exiting”);
}
};

FakeExternalGUI *fake = 0;

void handle_sigterm(int) {
static int count = 0;
if (count++ == 0) {
fake->quit();
} else exit(1);
}

extern “C” attribute ((visibility(“default”))) int dssi_gui_main(const char *osc_host_url, const char *plugin_so_name,
const char *label, const char *friendlyname) {
initialiseJuce_GUI();
signal(SIGTERM, &handle_sigterm);

fake = new FakeExternalGUI();
fake->init(osc_host_url, plugin_so_name, label, friendlyname);

MessageManager::getInstance()->runDispatchLoop();
//fake->run_();
fake->exiting();
deleteAndZero(fake);
shutdownJuce_GUI();
return 0;
}
[/code]

And here is the code for the fake gui program, it is just dlopen-ing the plugin gui, and call the dssi_gui_main function inside the dssi plugin – this is just to avoid duplicating juce code in the plugin and in the fake gui application. Let’s call it dssi_wrapper_gui.c , because it is not even c++ :

[code]#include <dlfcn.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include “JucePluginCharacteristics.h”

typedef int (*DSSI_Gui_Main)(const char *, const char *, const char *, const char *);

int main(int argc, char **argv) {
if (argc != 5) {
printf(“This application is intended to display the GUI for the DSSI plugin %s\n”, JucePlugin_Name);
printf(“Usage: %s <plugin.so> \n”, argv[0]);
return 1;
}

const char *osc_host_url = argv[1];
const char *plugin_so_name = argv[2];
const char *label = argv[3];
const char *friendlyname = argv[4];

char so_path[512]; strncpy(so_path, argv[0], 512); so_path[511] = 0;
int len = strlen(so_path);
while (len && so_path[len-1] != ‘/’) --len; if (len) --len;
so_path[len] = 0; strncat(so_path, “.so”, 512); so_path[511] = 0;

printf(“len=%d so_path: %s\nso: %s self: %s\n”, len, so_path, argv[2], argv[0]);

//char b[1024]; getcwd(b, 1024);
//printf(“cwd: %s\n”, b);

void *handle = dlopen(so_path, RTLD_NOW);
if (!handle) {
fprintf(stderr, “error: %s\nWe need to be able to locate the original %s file”, dlerror(), plugin_so_name);
return 2;
}

DSSI_Gui_Main dssi_gui_main = (DSSI_Gui_Main)dlsym(handle, “dssi_gui_main”);
if (!dssi_gui_main) {
fprintf(stderr, “Invalid %s : could not locate dssi_gui_main\n”, plugin_so_name);
return 3;
}
return dssi_gui_main(osc_host_url, plugin_so_name, label, friendlyname);
}
[/code]


Ladspa?
#2

Nice bit of hackery there! Annoying about the lack of chunk saving… Do DSSI plugins use tricks to work around that, or are do they all just use parameters?


#3

No there’s no tricks. I’ve discussed a bit with kraken about that, dssi-vst (the wrapper for windows vst), which is by far the dssi plugin that would benefit most from chunk support, does have an optional patch in the dssi-vst-0.9.2.tar.gz archive ( http://code.breakfastquay.com/projects/dssi-vst/repository/entry/ ), but it is useless as long as it does not make its way into the official dssi spec.


#4

Sorry to revive an old thread, but we can actually save custom data without the “unofficial getCustomData()” patch.

I’ve been working on a plugin host myself this past days (http://repo.or.cz/w/cadence.git), and I found that many DSSI plugins do save custom data.
2 examples are FluidSynth and Xsynth.

The FluidSynth GUI has 2 parameters and a filename, but if you turn off the GUI you get no control ports whatsoever.
The trick is to use DSSI’s configure(), which sends data from the OSC GUI to the plugin (and host, if supported).
FluidSynth sends/receives 'control ’ and 'load ', so we can actually save custom data (2 parameter values and a filename in this case).

Xsynth uses “patchesX” in configure, which save/loads the current patch data.
This one is quite big, but nonetheless works, which proves we can send large data between OSC “safely”.