JUCE LV2 Plugin Wrapper

Discuss issues relating to audio plugins

JUCE LV2 Plugin Wrapper

Postby falkTX » Thu Jul 07, 2011 5:38 pm

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/ ... client/LV2

For recent discussion, skip to page 4: 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!)
Last edited by falkTX on Sun Apr 28, 2013 1:48 am, edited 2 times in total.
falkTX
JUCE UberWeenie
 
Posts: 117
Joined: Sat Jun 04, 2011 4:15 pm

Re: JUCE LV2 Plugin Wrapper

Postby jpo » Wed Jul 13, 2011 11:41 pm

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.
jpo
JUCE UberWeenie
 
Posts: 336
Joined: Thu Mar 20, 2008 2:45 pm

Re: JUCE LV2 Plugin Wrapper

Postby falkTX » Thu Jul 14, 2011 1:24 am

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
JUCE UberWeenie
 
Posts: 117
Joined: Sat Jun 04, 2011 4:15 pm

Re: JUCE LV2 Plugin Wrapper

Postby falkTX » Thu Jul 14, 2011 1:39 am

Good News!

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

manifest.ttl:
Code: Select all
@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:
Code: Select all
@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:
Code: Select all
#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:
Code: Select all
/*
* 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.
falkTX
JUCE UberWeenie
 
Posts: 117
Joined: Sat Jun 04, 2011 4:15 pm

Re: JUCE LV2 Plugin Wrapper

Postby jpo » Thu Jul 14, 2011 9:35 am

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 !
jpo
JUCE UberWeenie
 
Posts: 336
Joined: Thu Mar 20, 2008 2:45 pm

Re: JUCE LV2 Plugin Wrapper

Postby falkTX » Sat Jul 16, 2011 3:48 pm

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
JUCE UberWeenie
 
Posts: 117
Joined: Sat Jun 04, 2011 4:15 pm

Re: JUCE LV2 Plugin Wrapper

Postby falkTX » Thu Jul 28, 2011 11:12 am

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!
falkTX
JUCE UberWeenie
 
Posts: 117
Joined: Sat Jun 04, 2011 4:15 pm

Re: JUCE LV2 Plugin Wrapper

Postby jpo » Thu Jul 28, 2011 1:36 pm

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).
jpo
JUCE UberWeenie
 
Posts: 336
Joined: Thu Mar 20, 2008 2:45 pm

Re: JUCE LV2 Plugin Wrapper

Postby falkTX » Thu Jul 28, 2011 2:57 pm

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...
falkTX
JUCE UberWeenie
 
Posts: 117
Joined: Sat Jun 04, 2011 4:15 pm

Re: JUCE LV2 Plugin Wrapper

Postby jpo » Fri Jul 29, 2011 8:10 am

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.
jpo
JUCE UberWeenie
 
Posts: 336
Joined: Thu Mar 20, 2008 2:45 pm

Re: JUCE LV2 Plugin Wrapper

Postby falkTX » Fri Jul 29, 2011 10:22 am

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.
falkTX
JUCE UberWeenie
 
Posts: 117
Joined: Sat Jun 04, 2011 4:15 pm

Re: JUCE LV2 Plugin Wrapper

Postby falkTX » Mon Aug 08, 2011 11:41 am

ok, there is progress.

Plugin processing already works fine (code heavily based on the VST wrapper), including MIDI input and output.

The UI is a bit more complicated, but at least it's now being shown/hidden on demand (via External UI).
JUCE UI is ok, since we just need to pass the our JUCE Component pointer, but for external UI we need to create our own window.
(Sadly, there's not a single JUCE app that loads LV2... :( )

Here's a mandatory screenshot - Ardour using JUCE Demo and TAL-Reverb-II plugins, with their UI shown via external UI.
Image
falkTX
JUCE UberWeenie
 
Posts: 117
Joined: Sat Jun 04, 2011 4:15 pm

Re: JUCE LV2 Plugin Wrapper

Postby falkTX » Sat Sep 17, 2011 6:12 pm

it's working!!!

here's a testing 64bit plugin you can try:
http://kxstudio.sourceforge.net/tmp/EQinox.lv2.7z

Known issues:
- external UI does not close
- no chunk save support (only port values for now)
falkTX
JUCE UberWeenie
 
Posts: 117
Joined: Sat Jun 04, 2011 4:15 pm

Re: JUCE LV2 Plugin Wrapper

Postby jules » Sat Sep 17, 2011 6:14 pm

cool! Nice work!
User avatar
jules
Fearless Leader
 
Posts: 17192
Joined: Mon Sep 06, 2004 9:03 am
Location: London, UK

Re: JUCE LV2 Plugin Wrapper

Postby falkTX » Sat Sep 17, 2011 6:21 pm

jules wrote:cool! Nice work!


I'm very close to finish this baby, just need an extension for chunk stuff, and some listener for the close button.

Jules, would you consider adding this to the source tree when ready?
falkTX
JUCE UberWeenie
 
Posts: 117
Joined: Sat Jun 04, 2011 4:15 pm

Next

Return to Audio Plugins

Who is online

Users browsing this forum: Bing [Bot], Buncker and 2 guests