JUCE LV2 Plugin Wrapper

64 posts / 0 new
Last post
falkTX
falkTX's picture
Offline
Last seen: 4 hours 43 min ago
Joined: 4 Jun 2011 - 16:15
JUCE LV2 Plugin Wrapper

Hi there.

EDIT:
We're in the process of integrating the wrapper into official Juce code.
The latest version of the wrapper is here:
https://github.com/falkTX/DISTRHO/tree/master/libs/juce-2.0/source/modules/juce_audio_plugin_client/LV2

For recent discussion, skip to page 4: http://www.rawmaterialsoftware.com/viewtopic.php?f=8&t=7494&start=45#p62609

----------------------------------------------------------------------------------------
old stuff follows:

I want to jump in JUCE development and contribute with a LV2 plugin wrapper for JUCE.
(I'll probably base this on the VST wrapper code, and borrow some from the unofficial DSSI wrapper attempt)

The most complicated thing will be to generate RDF data on-the-fly. Calf plugins do this, so I'll borrow some code.

Let's review the extensions needed:
- URI Map (for events)
- Events (for MIDI)
- MIDI
- UI
- External UI (I can't see Suil or any non-JUCE host supporting JUCE UIs natively)
- Data Access
- Instance Access
And some to handle Chunk data (JUCE XML dump of plugin state)
^ These extensions will provide all the functionality we need.
I'm not sure how presets work in LV2 currently.

But I have some questions regarding JUCE:
- Can a JUCE plugin change Audio and MIDI ports or is it static?
(this is not possible in LV2)
- Can a JUCE plugin add new/remove parameters?
(this will require lv2dynparam extension, which complicates things a bit and most hosts don't support it)
- Does JUCE support multi-plugins per binary?
(afaik, it doesn't. less work for me!)

Edited by: falkTX on 28 Apr 2013 - 01:48
jpo
Offline
Last seen: 3 days 4 hours ago
Joined: 20 Mar 2008 - 13:45
Re: JUCE LV2 Plugin Wrapper

I've been told that Dave Robillard is very open for supporting plugins in suil that just expose an X11 windows for their interface (instead of only GTK or Qt interfaces) so I think you should consider not taking the 'externalui' extension road ! Maybe you should contact him.

falkTX
falkTX's picture
Offline
Last seen: 4 hours 43 min ago
Joined: 4 Jun 2011 - 16:15
Re: JUCE LV2 Plugin Wrapper

jpo wrote:
I've been told that Dave Robillard is very open for supporting plugins in suil that just expose an X11 windows for their interface (instead of only GTK or Qt interfaces) so I think you should consider not taking the 'externalui' extension road ! Maybe you should contact him.

I decided to use both UIs - JUCE native UI and external UI.
It's not that hard to code...

I'm still not sure if Drobilla will be ok with a JUCE UI. Last time I tried to ask him, he just left the IRC room...
Still, Suil is linux only, while LV2 is not. A (future) Windows host may want to use JUCE LV2s, and Suil won't be there to help, so external UI makes sense.

falkTX
falkTX's picture
Offline
Last seen: 4 hours 43 min ago
Joined: 4 Jun 2011 - 16:15
Re: JUCE LV2 Plugin Wrapper

Good News!

Auto-generating turtle files work!
Here is the output of the JUCE demo plugin, converted to ttl for LV2:

manifest.ttl:

@prefix lv2:  <http://lv2plug.in/ns/lv2core#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

<urn:Raw_Material_Software:Juce_Demo_Plugin:1.0.0>
    a lv2:Plugin ;
    lv2:binary <Juce_Demo_Plugin.so> ;
    rdfs:seeAlso <Juce_Demo_Plugin.ttl> .

Juce_Demo_Plugin.ttl:

@prefix doap:  <http://usefulinc.com/ns/doap#> .
@prefix lv2:   <http://lv2plug.in/ns/lv2core#> .
@prefix lv2ev: <http://lv2plug.in/ns/ext/event#> .
@prefix lv2ui: <http://lv2plug.in/ns/extensions/ui#> .

<urn:Raw_Material_Software:Juce_Demo_Plugin:JUCE-Native-UI>
    a lv2ui:JUCEUI ;
    lv2ui:binary <Raw_Material_Software.so> .
<urn:Raw_Material_Software:Juce_Demo_Plugin:JUCE-External-UI>
    a uiext:external ;
    lv2ui:binary <Raw_Material_Software.so> .

<urn:Raw_Material_Software:Juce_Demo_Plugin:1.0.0>
    a lv2:Plugin ;

    lv2:port [
      a lv2:InputPort, lv2ev:EventPort;
      lv2ev:supportsEvent <http://lv2plug.in/ns/ext/midi#MidiEvent> ;
      lv2:index 0;
      lv2:symbol "midi_in";
      lv2:name "MIDI Input";
    ] ;
    lv2:port [
      a lv2:OutputPort, lv2ev:EventPort;
      lv2ev:supportsEvent <http://lv2plug.in/ns/ext/midi#MidiEvent> ;
      lv2:index 1;
      lv2:symbol "midi_out";
      lv2:name "MIDI Output";
    ] ;

    lv2:port [
      a lv2:InputPort, lv2:AudioPort;
      lv2:index 2;
      lv2:symbol "audio_in_0";
      lv2:name "Audio Input 0";
    ],
    [
      a lv2:InputPort, lv2:AudioPort;
      lv2:index 3;
      lv2:symbol "audio_in_1";
      lv2:name "Audio Input 1";
    ] ;
    lv2:port [
      a lv2:OutputPort, lv2:AudioPort;
      lv2:index 4;
      lv2:symbol "audio_out_0";
      lv2:name "Audio Output 0";
    ],
    [
      a lv2:OutputPort, lv2:AudioPort;
      lv2:index 5;
      lv2:symbol "audio_out_1";
      lv2:name "Audio Output 1";
    ] ;

    lv2:port [
      a lv2:InputPort;
      a lv2:ControlPort;
      lv2:index 6;
      lv2:symbol gain";
      lv2:name gain;
      lv2:default 1.0;
      lv2:minimum 0.0;
      lv2:maximum 1.0;
    ],
    [
      a lv2:InputPort;
      a lv2:ControlPort;
      lv2:index 7;
      lv2:symbol delay";
      lv2:name delay;
      lv2:default 0.5;
      lv2:minimum 0.0;
      lv2:maximum 1.0;
    ] ;

    doap:name "Juce Demo Plugin" ;
    doap:creator "Raw Material Software" .

There a few things missing (presets and units), but I'll get there later.

My current code requires some changes to JUCE plugins though, in JucePluginCharacteristics.h, I added2 more fields:

#define JucePlugin_LV2Includes          "PluginProcessor.h"
#define JucePlugin_LV2ClassName         JuceDemoPluginAudioProcessor

This is required to build the *.ttl files, otherwise we would need to compile the plugin binary first, and somehow extract the info from it.
I would like some opinions in here though...

Here's my ttl-generator code so far:

/*
 * LV2 ttl generator for JUCE Plugins
 */

#include <fstream>
#include <iostream>
#include <stdint.h>

#include "JuceHeader.h"
#include "JucePluginCharacteristics.h"

#include JucePlugin_LV2Includes

// These are dummy values!
enum FakePlugCategory
{
    kPlugCategUnknown,
    kPlugCategEffect,
    kPlugCategSynth,
    kPlugCategAnalysis,
    kPlugCategMastering,
    kPlugCategSpacializer,
    kPlugCategRoomFx,
    kPlugSurroundFx,
    kPlugCategRestoration,
    kPlugCategOfflineProcess,
    kPlugCategGenerator
};

String name_to_symbol(String Name)
{
    String Symbol = Name.trimStart().trimEnd().replace(" ", "_").toLowerCase();

    for (int i=0; i < Symbol.length(); i++) {
        if (std::isalpha(Symbol[i]) || std::isdigit(Symbol[i]) || Symbol[i] == '_') {
            // nothing
        } else {
            Symbol[i] == '_';
        }
    }
    return Symbol;
}

String float_to_string(float value)
{
    if (value < 0.0f || value > 1.0f) {
        std::cerr << "WARNING - Parameter uses out-of-bounds default value -> " << value << std::endl;
    }
    String string(value);
    if (!string.contains(".")) {
        string += ".0";
    }
    return string;
}

String get_uri()
{
    return String("urn:" JucePlugin_Manufacturer ":" JucePlugin_Name ":" JucePlugin_VersionString).replace(" ", "_");
}

String get_juce_ui_uri()
{
    return String("urn:" JucePlugin_Manufacturer ":" JucePlugin_Name ":JUCE-Native-UI").replace(" ", "_");
}

String get_external_ui_uri()
{
    return String("urn:" JucePlugin_Manufacturer ":" JucePlugin_Name ":JUCE-External-UI").replace(" ", "_");
}

String get_binary_name()
{
    return String(JucePlugin_Name).replace(" ", "_");
}

String get_plugin_type()
{
    String ptype;

    switch (JucePlugin_VSTCategory) {
    case kPlugCategSynth:
        ptype += "lv2:InstrumentPlugin";
        break;
    case kPlugCategAnalysis:
        ptype += "lv2:AnalyserPlugin";
        break;
    case kPlugCategMastering:
        ptype += "lv2:DynamicsPlugin";
        break;
    case kPlugCategSpacializer:
        ptype += "lv2:SpatialPlugin";
        break;
    case kPlugCategRoomFx:
        ptype += "lv2:ModulatorPlugin";
        break;
    case kPlugCategRestoration:
        ptype += "lv2:UtilityPlugin";
        break;
    case kPlugCategGenerator:
        ptype += "lv2:GeneratorPlugin";
        break;
    }

    if (ptype.isNotEmpty()) {
        ptype += ", ";
    }

    ptype += "lv2:Plugin";
    return ptype;
}

String get_manifest_ttl(String URI, String Binary)
{
    String manifest;
    manifest += "@prefix lv2:  <http://lv2plug.in/ns/lv2core#> .\n";
    manifest += "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n";
    manifest += "\n";
    manifest += "<" + URI + ">\n";
    manifest += "    a lv2:Plugin ;\n";
    manifest += "    lv2:binary <" + Binary + ".so> ;\n";
    manifest += "    rdfs:seeAlso <" + Binary +".ttl> .\n";
    return manifest;
}

String get_plugin_ttl(String URI, String Binary)
{
    // Testing, need another way to do this!!
    JucePlugin_LV2ClassName* JucePlugin = new JucePlugin_LV2ClassName();

    String plugin;
    plugin += "@prefix doap:  <http://usefulinc.com/ns/doap#> .\n";
    //plugin += "@prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .\n";
    //plugin += "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n";
    plugin += "@prefix lv2:   <http://lv2plug.in/ns/lv2core#> .\n";
    plugin += "@prefix lv2ev: <http://lv2plug.in/ns/ext/event#> .\n";
    plugin += "@prefix lv2ui: <http://lv2plug.in/ns/extensions/ui#> .\n";
    plugin += "\n";

    if (JucePlugin->hasEditor()) {
        plugin += "<" + get_juce_ui_uri() + ">\n";
        plugin += "    a lv2ui:JUCEUI ;\n";
        plugin += "    lv2ui:binary <" + Binary + ".so> .\n";
        plugin += "<" + get_external_ui_uri() + ">\n";
        plugin += "    a uiext:external ;\n";
        plugin += "    lv2ui:binary <" + Binary + ".so> .\n";
        plugin += "\n";
    }

    plugin += "<" + URI + ">\n";
    plugin += "    a " + get_plugin_type() + " ;\n";
    plugin += "\n";

    uint32_t i, port_index = 0;

#if JucePlugin_WantsMidiInput
    plugin += "    lv2:port [\n";
    plugin += "      a lv2:InputPort, lv2ev:EventPort;\n";
    plugin += "      lv2ev:supportsEvent <http://lv2plug.in/ns/ext/midi#MidiEvent> ;\n";
    plugin += "      lv2:index " + String(port_index) + ";\n";
    plugin += "      lv2:symbol \"midi_in\";\n";
    plugin += "      lv2:name \"MIDI Input\";\n";
    plugin += "    ] ;\n";
    port_index++;
#endif

#if JucePlugin_ProducesMidiOutput
    plugin += "    lv2:port [\n";
    plugin += "      a lv2:OutputPort, lv2ev:EventPort;\n";
    plugin += "      lv2ev:supportsEvent <http://lv2plug.in/ns/ext/midi#MidiEvent> ;\n";
    plugin += "      lv2:index " + String(port_index) + ";\n";
    plugin += "      lv2:symbol \"midi_out\";\n";
    plugin += "      lv2:name \"MIDI Output\";\n";
    plugin += "    ] ;\n";
    port_index++;
#endif

#if JucePlugin_WantsMidiInput || JucePlugin_ProducesMidiOutput
    plugin += "\n";
#endif

    for (i=0; i<JucePlugin_MaxNumInputChannels; i++) {
        if (i == 0) {
            plugin += "    lv2:port [\n";
        } else {
            plugin += "    [\n";
        }

        plugin += "      a lv2:InputPort, lv2:AudioPort;\n";
        //plugin += "      lv2:datatype lv2:float;\n";
        plugin += "      lv2:index " + String(port_index) + ";\n";
        plugin += "      lv2:symbol \"audio_in_" + String(i) + "\";\n";
        plugin += "      lv2:name \"Audio Input " + String(i) + "\";\n";

        if (i == JucePlugin_MaxNumInputChannels-1) {
            plugin += "    ] ;\n";
        } else {
            plugin += "    ],\n";
        }

        port_index++;
    }

    for (i=0; i<JucePlugin_MaxNumOutputChannels; i++) {
        if (i == 0) {
            plugin += "    lv2:port [\n";
        } else {
            plugin += "    [\n";
        }

        plugin += "      a lv2:OutputPort, lv2:AudioPort;\n";
        //plugin += "      lv2:datatype lv2:float;\n";
        plugin += "      lv2:index " + String(port_index) + ";\n";
        plugin += "      lv2:symbol \"audio_out_" + String(i) + "\";\n";
        plugin += "      lv2:name \"Audio Output " + String(i) + "\";\n";

        if (i == JucePlugin_MaxNumOutputChannels-1) {
            plugin += "    ] ;\n";
        } else {
            plugin += "    ],\n";
        }

        port_index++;
    }

#if JucePlugin_MaxNumInputChannels > 0 || JucePlugin_MaxNumOutputChannels > 0
    plugin += "\n";
#endif

    for (i=0; i < JucePlugin->getNumParameters(); i++) {
        if (i == 0) {
            plugin += "    lv2:port [\n";
        } else {
            plugin += "    [\n";
        }

        plugin += "      a lv2:InputPort;\n";
        plugin += "      a lv2:ControlPort;\n";
        //plugin += "      lv2:datatype lv2:float;\n";
        plugin += "      lv2:index " + String(port_index) + ";\n";
        plugin += "      lv2:symbol " + name_to_symbol(JucePlugin->getParameterName(i)) + "\";\n";
        plugin += "      lv2:name " + JucePlugin->getParameterName(i) + ";\n";
        plugin += "      lv2:default " + float_to_string(JucePlugin->getParameter(i)) + ";\n";
        plugin += "      lv2:minimum 0.0;\n";
        plugin += "      lv2:maximum 1.0;\n";
        // TODO - units

        if (i == JucePlugin_MaxNumOutputChannels-1) {
            plugin += "    ] ;\n";
        } else {
            plugin += "    ],\n";
        }

        port_index++;
    }

    if (JucePlugin->getNumParameters() > 0) {
        plugin += "\n";
    }

    plugin += "    doap:name \"" + String(JucePlugin_Name) + "\" ;\n";
    plugin += "    doap:creator \"" + String(JucePlugin_Manufacturer) + "\" .\n";

    delete JucePlugin;
    return plugin;
}

int main(int argc, char *argv[])
{
    String URI = get_uri();
    String Binary = get_binary_name();
    String BinaryTTL = Binary + ".ttl";

    std::cout << "Writing manifest.ttl...";
    std::fstream manifest("manifest.ttl", std::ios::out);
    manifest << get_manifest_ttl(URI, Binary) << std::endl;
    manifest.close();
    std::cout << " done!" << std::endl;

    std::cout << "Writing " << BinaryTTL;
    std::fstream plugin(BinaryTTL.toUTF8(), std::ios::out);
    plugin << get_plugin_ttl(URI, Binary) << std::endl;
    plugin.close();
    std::cout << " done!" << std::endl;

    return 0;
}

I'll keep working on this and keep you guys posted.

jpo
Offline
Last seen: 3 days 4 hours ago
Joined: 20 Mar 2008 - 13:45
Re: JUCE LV2 Plugin Wrapper

Quote:
Last time I tried to ask him, he just left the IRC room

ahah well my information was not first hand, so maybe it was a bit over optimistic :)

Anyway, great work !

falkTX
falkTX's picture
Offline
Last seen: 4 hours 43 min ago
Joined: 4 Jun 2011 - 16:15
Re: JUCE LV2 Plugin Wrapper

Good News!

Plugin processing (Effects) are working fine.
To do is plugin UIs (JUCE and external), MIDI and chunks.

I've created a git repo for this:
http://repo.or.cz/w/juce-lv2.git

Please follow the updates there.
I'll post a screenshot here once I've got plugin UIs working.

falkTX
falkTX's picture
Offline
Last seen: 4 hours 43 min ago
Joined: 4 Jun 2011 - 16:15
Re: JUCE LV2 Plugin Wrapper

Simple rdf generation and processing already works, but things got complicated when I added some GUI functions...

Can someone clarify me what it's the purpose of all the 'mmLock'? (I suppose it's to wait until processing occurs, then do the GUI stuff?)

And is MessageSharedThread really needed on Linux?
I'm not sure what it does, but I assume it handles multiple instances of the same plugin?

Any help is appreciated, thanks!

jpo
Offline
Last seen: 3 days 4 hours ago
Joined: 20 Mar 2008 - 13:45
Re: JUCE LV2 Plugin Wrapper

take what I say with a grain of salt, maybe Jules will correct me, but as far as I know:

mmLock is a lock to prevent race conditions between the juce message thread, and other threads (the host will call your lv2 callbacks from a thread which will never be the juce message thread so you have to take care of any potential race condition)

The shared message thread stuff is quite specific to linux / x11 , it is used by juce for its event loop. It is shared by all instances of your plugin loaded in the host application (save some ressources, and allow them to communicate).

falkTX
falkTX's picture
Offline
Last seen: 4 hours 43 min ago
Joined: 4 Jun 2011 - 16:15
Re: JUCE LV2 Plugin Wrapper

jpo wrote:
take what I say with a grain of salt, maybe Jules will correct me

ok, until he posts here, help me just a bit here

jpo wrote:
mmLock is a lock to prevent race conditions between the juce message thread, and other threads (the host will call your lv2 callbacks from a thread which will never be the juce message thread so you have to take care of any potential race condition)

So I should just basically add it before any GUI call (like changing parameters) ?
GUI -> Host should be safe I guess, right?

jpo wrote:
The shared message thread stuff is quite specific to linux / x11 , it is used by juce for its event loop. It is shared by all instances of your plugin loaded in the host application (save some ressources, and allow them to communicate).

But why only Linux needs/have this...? Is it really required?

After a small testing, I realized that I should probably do initializejuce_GUI as soon as the DLL loads (as done with the VST wrapper).
This is a little bad for gui-less plugins...

jpo
Offline
Last seen: 3 days 4 hours ago
Joined: 20 Mar 2008 - 13:45
Re: JUCE LV2 Plugin Wrapper

well I don't recall the details but I think nothing will work if you don't have the sharedmessagethread stuff running. On macos, juce uses the host event loop, on windows it uses whatever thread is used when instanciating the plugin but on linux there is no convention for that, so juce has to create its own thread for sending / receiving its internal messages and X11 messages.

initialisejuce_gui should work fine when no X11 display is available

I think your reference should be the vst wrapper, which works pretty well on linux. The dssi wrapper was basically a stripped down version with gui and win32/macos removed.

falkTX
falkTX's picture
Offline
Last seen: 4 hours 43 min ago
Joined: 4 Jun 2011 - 16:15
Re: JUCE LV2 Plugin Wrapper

jpo wrote:
well I don't recall the details but I think nothing will work if you don't have the sharedmessagethread stuff running. On macos, juce uses the host event loop, on windows it uses whatever thread is used when instanciating the plugin but on linux there is no convention for that, so juce has to create its own thread for sending / receiving its internal messages and X11 messages.

Thanks for the clarification, that makes sense.

jpo wrote:
initialisejuce_gui should work fine when no X11 display is available

Cool, then I'll initialize it as soon as the plugin loads

jpo wrote:
I think your reference should be the vst wrapper, which works pretty well on linux. The dssi wrapper was basically a stripped down version with gui and win32/macos removed.

The dssi wrapper is useful cause DSSI has some similarities to LV2 (but not in the GUI stuff though).
I'll try to make this as close to the VST wrapper as possible, just to be safe.

Pages