1153 lines
33 KiB
C++
1153 lines
33 KiB
C++
/*
|
|
* Copyright (C) 2016-2019 Robin Gareus <robin@gareus.org>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*/
|
|
|
|
#ifndef _GNU_SOURCE
|
|
#define _GNU_SOURCE
|
|
#endif
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <map>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include <math.h>
|
|
#include <pthread.h>
|
|
#include <stdlib.h>
|
|
|
|
#define AFS_URN "urn:ardour:a-fluidsynth"
|
|
|
|
#ifdef HAVE_LV2_1_10_0
|
|
#define x_forge_object lv2_atom_forge_object
|
|
#else
|
|
#define x_forge_object lv2_atom_forge_blank
|
|
#endif
|
|
|
|
#ifdef LV2_EXTENDED
|
|
#include "../../ardour/ardour/lv2_extensions.h"
|
|
#endif
|
|
|
|
#include "fluidsynth.h"
|
|
|
|
#ifdef HAVE_LV2_1_18_6
|
|
#include <lv2/atom/atom.h>
|
|
#include <lv2/atom/forge.h>
|
|
#include <lv2/atom/util.h>
|
|
#include <lv2/core/lv2.h>
|
|
#include <lv2/log/logger.h>
|
|
#include <lv2/midi/midi.h>
|
|
#include <lv2/patch/patch.h>
|
|
#include <lv2/state/state.h>
|
|
#include <lv2/urid/urid.h>
|
|
#include <lv2/worker/worker.h>
|
|
#else
|
|
#include <lv2/lv2plug.in/ns/ext/atom/atom.h>
|
|
#include <lv2/lv2plug.in/ns/ext/atom/forge.h>
|
|
#include <lv2/lv2plug.in/ns/ext/atom/util.h>
|
|
#include <lv2/lv2plug.in/ns/ext/log/logger.h>
|
|
#include <lv2/lv2plug.in/ns/ext/midi/midi.h>
|
|
#include <lv2/lv2plug.in/ns/ext/patch/patch.h>
|
|
#include <lv2/lv2plug.in/ns/ext/state/state.h>
|
|
#include <lv2/lv2plug.in/ns/ext/urid/urid.h>
|
|
#include <lv2/lv2plug.in/ns/ext/worker/worker.h>
|
|
#include <lv2/lv2plug.in/ns/lv2core/lv2.h>
|
|
#endif
|
|
|
|
enum {
|
|
FS_PORT_CONTROL = 0,
|
|
FS_PORT_NOTIFY,
|
|
FS_PORT_OUT_L,
|
|
FS_PORT_OUT_R,
|
|
FS_OUT_GAIN,
|
|
FS_REV_ENABLE,
|
|
FS_REV_ROOMSIZE,
|
|
FS_REV_DAMPING,
|
|
FS_REV_WIDTH,
|
|
FS_REV_LEVEL,
|
|
FS_CHR_ENABLE,
|
|
FS_CHR_N,
|
|
FS_CHR_SPEED,
|
|
FS_CHR_DEPTH,
|
|
FS_CHR_LEVEL,
|
|
FS_CHR_TYPE,
|
|
FS_PORT_ENABLE,
|
|
FS_PORT_LAST
|
|
};
|
|
|
|
enum {
|
|
CMD_APPLY = 0,
|
|
CMD_FREE = 1,
|
|
};
|
|
|
|
struct BankProgram {
|
|
BankProgram (const std::string& n, int b, int p)
|
|
: name (n)
|
|
, bank (b)
|
|
, program (p)
|
|
{}
|
|
|
|
BankProgram (const BankProgram& other)
|
|
: name (other.name)
|
|
, bank (other.bank)
|
|
, program (other.program)
|
|
{}
|
|
|
|
std::string name;
|
|
int bank;
|
|
int program;
|
|
};
|
|
|
|
typedef std::vector<BankProgram> BPList;
|
|
typedef std::map<int, BPList> BPMap;
|
|
typedef std::map<int, BankProgram> BPState;
|
|
|
|
typedef struct {
|
|
/* ports */
|
|
const LV2_Atom_Sequence* control;
|
|
LV2_Atom_Sequence* notify;
|
|
|
|
float* p_ports[FS_PORT_LAST];
|
|
float v_ports[FS_PORT_LAST];
|
|
|
|
/* fluid synth */
|
|
fluid_settings_t* settings;
|
|
fluid_synth_t* synth;
|
|
int synthId;
|
|
|
|
/* lv2 URIDs */
|
|
LV2_URID atom_Blank;
|
|
LV2_URID atom_Object;
|
|
LV2_URID atom_URID;
|
|
LV2_URID atom_Path;
|
|
LV2_URID atom_Vector;
|
|
LV2_URID atom_Double;
|
|
LV2_URID midi_MidiEvent;
|
|
LV2_URID patch_Get;
|
|
LV2_URID patch_Set;
|
|
LV2_URID patch_property;
|
|
LV2_URID patch_value;
|
|
LV2_URID state_Changed;
|
|
LV2_URID afs_sf2file;
|
|
LV2_URID afs_tuning;
|
|
|
|
/* lv2 extensions */
|
|
LV2_Log_Log* log;
|
|
LV2_Log_Logger logger;
|
|
LV2_Worker_Schedule* schedule;
|
|
LV2_Atom_Forge forge;
|
|
LV2_Atom_Forge_Frame frame;
|
|
|
|
#ifdef LV2_EXTENDED
|
|
LV2_Midnam* midnam;
|
|
LV2_BankPatch* bankpatch;
|
|
BPMap presets;
|
|
#endif
|
|
pthread_mutex_t bp_lock;
|
|
|
|
/* state */
|
|
bool panic;
|
|
bool initialized;
|
|
bool inform_ui;
|
|
bool send_bankpgm;
|
|
|
|
char current_sf2_file_path[1024];
|
|
char queue_sf2_file_path[1024];
|
|
bool reinit_in_progress; // set in run, cleared in work_response
|
|
bool queue_reinit; // set in restore, cleared in work_response
|
|
|
|
bool queue_retune;
|
|
double queue_tuning[128];
|
|
|
|
BankProgram program_state[16];
|
|
|
|
fluid_midi_event_t* fmidi_event;
|
|
|
|
} AFluidSynth;
|
|
|
|
/* *****************************************************************************
|
|
* helpers
|
|
*/
|
|
|
|
static bool
|
|
load_sf2 (AFluidSynth* self, const char* fn)
|
|
{
|
|
const int synth_id = fluid_synth_sfload (self->synth, fn, 1);
|
|
|
|
#ifdef LV2_EXTENDED
|
|
pthread_mutex_lock (&self->bp_lock);
|
|
self->presets.clear ();
|
|
pthread_mutex_unlock (&self->bp_lock);
|
|
#endif
|
|
|
|
if (synth_id == FLUID_FAILED) {
|
|
return false;
|
|
}
|
|
|
|
fluid_sfont_t* const sfont = fluid_synth_get_sfont_by_id (self->synth, synth_id);
|
|
if (!sfont) {
|
|
return false;
|
|
}
|
|
|
|
int chn;
|
|
fluid_preset_t* preset;
|
|
fluid_sfont_iteration_start (sfont);
|
|
pthread_mutex_lock (&self->bp_lock);
|
|
for (chn = 0; (preset = fluid_sfont_iteration_next (sfont)); ++chn) {
|
|
if (chn < 16) {
|
|
fluid_synth_program_select (self->synth, chn, synth_id,
|
|
fluid_preset_get_banknum (preset), fluid_preset_get_num (preset));
|
|
}
|
|
#ifndef LV2_EXTENDED
|
|
else {
|
|
break;
|
|
}
|
|
#else
|
|
self->presets[fluid_preset_get_banknum (preset)].push_back (
|
|
BankProgram (
|
|
fluid_preset_get_name (preset),
|
|
fluid_preset_get_banknum (preset),
|
|
fluid_preset_get_num (preset)));
|
|
#endif // LV2_EXTENDED
|
|
}
|
|
pthread_mutex_unlock (&self->bp_lock);
|
|
|
|
if (chn == 0) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static const LV2_Atom*
|
|
parse_patch_msg (AFluidSynth* self, const LV2_Atom_Object* obj)
|
|
{
|
|
const LV2_Atom* property = NULL;
|
|
const LV2_Atom* file_path = NULL;
|
|
|
|
if (obj->body.otype != self->patch_Set) {
|
|
return NULL;
|
|
}
|
|
|
|
lv2_atom_object_get (obj, self->patch_property, &property, 0);
|
|
if (!property || property->type != self->atom_URID) {
|
|
return NULL;
|
|
} else if (((const LV2_Atom_URID*)property)->body != self->afs_sf2file) {
|
|
return NULL;
|
|
}
|
|
|
|
lv2_atom_object_get (obj, self->patch_value, &file_path, 0);
|
|
if (!file_path || file_path->type != self->atom_Path) {
|
|
return NULL;
|
|
}
|
|
|
|
return file_path;
|
|
}
|
|
|
|
static void
|
|
inform_ui (AFluidSynth* self)
|
|
{
|
|
if (strlen (self->current_sf2_file_path) == 0) {
|
|
return;
|
|
}
|
|
|
|
LV2_Atom_Forge_Frame frame;
|
|
lv2_atom_forge_frame_time (&self->forge, 0);
|
|
x_forge_object (&self->forge, &frame, 1, self->patch_Set);
|
|
lv2_atom_forge_property_head (&self->forge, self->patch_property, 0);
|
|
lv2_atom_forge_urid (&self->forge, self->afs_sf2file);
|
|
lv2_atom_forge_property_head (&self->forge, self->patch_value, 0);
|
|
lv2_atom_forge_path (&self->forge, self->current_sf2_file_path, strlen (self->current_sf2_file_path));
|
|
|
|
lv2_atom_forge_pop (&self->forge, &frame);
|
|
}
|
|
|
|
static float
|
|
db_to_coeff (float db)
|
|
{
|
|
if (db <= -80) {
|
|
return 0;
|
|
} else if (db >= 20) {
|
|
return 10;
|
|
}
|
|
return powf (10.f, .05f * db);
|
|
}
|
|
|
|
static void
|
|
parse_mts (AFluidSynth* self, const uint8_t* data, uint32_t len)
|
|
{
|
|
assert (data[0] == 0xf0 && data[3] == 0x08 && len > 11);
|
|
if (data[4] == 0x01 && len == 408) {
|
|
/* bulk transfer
|
|
* 0xf0, 0x7e, -- non-realtime sysex
|
|
* 0xXX, -- target-id
|
|
* 0x08, 0x01, -- tuning, bulk dump reply
|
|
* 0x7X, -- tuning program number 0 to 127
|
|
* TEXT * 16 -- 16 chars name (zero padded)
|
|
* TUNE * 3 * 128 -- tuning for all notes (base, MSB, LSB)
|
|
* 0xXX -- checksum
|
|
* 0xf7 -- 408 bytes in total
|
|
*/
|
|
int prog = 0; // data[2]
|
|
int off = 22;
|
|
int key[128];
|
|
double pitch[128];
|
|
for (int i = 0; i < 128; ++i) {
|
|
const uint32_t note = data[off];
|
|
const uint32_t fract = (data[off + 1] << 7) | data[off + 2];
|
|
key[i] = i;
|
|
pitch[i] = note * 100.f + fract / 163.83;
|
|
off += 3;
|
|
}
|
|
if (data[off + 1] == 0xf7) {
|
|
int rv = fluid_synth_tune_notes (self->synth,
|
|
/* tuning bank */ 0,
|
|
/* tuning prog */ prog,
|
|
128, key, pitch,
|
|
/* apply */ 1);
|
|
if (rv == FLUID_OK) {
|
|
for (int c = 0; c < 16; ++c) {
|
|
fluid_synth_activate_tuning (self->synth, c, 0, prog, 0);
|
|
}
|
|
self->inform_ui = true; // StateChanged
|
|
}
|
|
}
|
|
} else if (data[4] == 0x02 && len == 12) {
|
|
/* single note tuning
|
|
* 0xf0, 0x7f, -- realtime sysex
|
|
* 0xXX, -- target-id
|
|
* 0x08, 0x02, -- tuning, note change request
|
|
* 0x7X, -- tuning program number 0 to 127
|
|
* 0x7X -- Note to re-tune
|
|
* base, -- semitone (MIDI note number to retune to, unit is 100 cents)
|
|
* cent_msb, -- MSB of fractional part (1/128 semitone = 100/128 cents = .78125 cent units)
|
|
* cent_lsb, -- LSB of fractional part (1/16384 semitone = 100/16384 cents = .0061 cent units
|
|
* 0xf7 -- 12 bytesin total
|
|
*/
|
|
const uint32_t note = data[8];
|
|
const uint32_t fract = (data[9] << 7) | data[10];
|
|
|
|
int prog = 0; // data[2]
|
|
int key = data[7];
|
|
double pitch = note * 100.f + fract / 163.83;
|
|
if (data[11] == 0xf7) {
|
|
int rv = fluid_synth_tune_notes (self->synth,
|
|
/* tuning bank */ 0,
|
|
/* tuning prog */ prog,
|
|
1, &key, &pitch,
|
|
/* apply */ 1);
|
|
if (rv == FLUID_OK) {
|
|
for (int c = 0; c < 16; ++c) {
|
|
fluid_synth_activate_tuning (self->synth, c, 0, prog, 0);
|
|
}
|
|
self->inform_ui = true; // StateChanged
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* *****************************************************************************
|
|
* LV2 Plugin
|
|
*/
|
|
|
|
static LV2_Handle
|
|
instantiate (const LV2_Descriptor* descriptor,
|
|
double rate,
|
|
const char* bundle_path,
|
|
const LV2_Feature* const* features)
|
|
{
|
|
AFluidSynth* self = (AFluidSynth*)calloc (1, sizeof (AFluidSynth));
|
|
|
|
if (!self) {
|
|
return NULL;
|
|
}
|
|
|
|
LV2_URID_Map* map = NULL;
|
|
|
|
for (int i = 0; features[i] != NULL; ++i) {
|
|
if (!strcmp (features[i]->URI, LV2_URID__map)) {
|
|
map = (LV2_URID_Map*)features[i]->data;
|
|
} else if (!strcmp (features[i]->URI, LV2_LOG__log)) {
|
|
self->log = (LV2_Log_Log*)features[i]->data;
|
|
} else if (!strcmp (features[i]->URI, LV2_WORKER__schedule)) {
|
|
self->schedule = (LV2_Worker_Schedule*)features[i]->data;
|
|
#ifdef LV2_EXTENDED
|
|
} else if (!strcmp (features[i]->URI, LV2_MIDNAM__update)) {
|
|
self->midnam = (LV2_Midnam*)features[i]->data;
|
|
} else if (!strcmp (features[i]->URI, LV2_BANKPATCH__notify)) {
|
|
self->bankpatch = (LV2_BankPatch*)features[i]->data;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
lv2_log_logger_init (&self->logger, map, self->log);
|
|
|
|
if (!map) {
|
|
lv2_log_error (&self->logger, "a-fluidsynth.lv2: Host does not support urid:map\n");
|
|
free (self);
|
|
return NULL;
|
|
}
|
|
|
|
if (!self->schedule) {
|
|
lv2_log_error (&self->logger, "a-fluidsynth.lv2: Host does not support worker:schedule\n");
|
|
free (self);
|
|
return NULL;
|
|
}
|
|
|
|
#ifdef LV2_EXTENDED
|
|
if (!self->midnam) {
|
|
lv2_log_warning (&self->logger, "a-fluidsynth.lv2: Host does not support midnam:update\n");
|
|
}
|
|
|
|
if (!self->bankpatch) {
|
|
lv2_log_warning (&self->logger, "a-fluidsynth.lv2: Host does not support bankpatch:notify\n");
|
|
}
|
|
#endif
|
|
|
|
/* initialize fluid synth */
|
|
self->settings = new_fluid_settings ();
|
|
|
|
if (!self->settings) {
|
|
lv2_log_error (&self->logger, "a-fluidsynth.lv2: cannot allocate Fluid Settings\n");
|
|
free (self);
|
|
return NULL;
|
|
}
|
|
|
|
fluid_settings_setnum (self->settings, "synth.sample-rate", rate);
|
|
fluid_settings_setint (self->settings, "synth.threadsafe-api", 0);
|
|
fluid_settings_setstr (self->settings, "synth.midi-bank-select", "mma");
|
|
|
|
self->synth = new_fluid_synth (self->settings);
|
|
|
|
if (!self->synth) {
|
|
lv2_log_error (&self->logger, "a-fluidsynth.lv2: cannot allocate Fluid Synth\n");
|
|
delete_fluid_settings (self->settings);
|
|
free (self);
|
|
return NULL;
|
|
}
|
|
|
|
fluid_synth_set_gain (self->synth, 1.0f);
|
|
fluid_synth_set_polyphony (self->synth, 256);
|
|
fluid_synth_set_sample_rate (self->synth, (float)rate);
|
|
|
|
fluid_synth_set_reverb_on (self->synth, 0);
|
|
fluid_synth_set_chorus_on (self->synth, 0);
|
|
|
|
self->fmidi_event = new_fluid_midi_event ();
|
|
|
|
if (!self->fmidi_event) {
|
|
lv2_log_error (&self->logger, "a-fluidsynth.lv2: cannot allocate Fluid Event\n");
|
|
delete_fluid_synth (self->synth);
|
|
delete_fluid_settings (self->settings);
|
|
free (self);
|
|
return NULL;
|
|
}
|
|
|
|
/* initialize plugin state */
|
|
|
|
pthread_mutex_init (&self->bp_lock, NULL);
|
|
#ifdef LV2_EXTENDED
|
|
self->presets = BPMap ();
|
|
#endif
|
|
self->panic = false;
|
|
self->inform_ui = false;
|
|
self->send_bankpgm = true;
|
|
self->initialized = false;
|
|
self->reinit_in_progress = false;
|
|
self->queue_reinit = false;
|
|
self->queue_retune = false;
|
|
for (int chn = 0; chn < 16; ++chn) {
|
|
self->program_state[chn].program = -1;
|
|
}
|
|
|
|
lv2_atom_forge_init (&self->forge, map);
|
|
|
|
/* map URIDs */
|
|
self->atom_Blank = map->map (map->handle, LV2_ATOM__Blank);
|
|
self->atom_Object = map->map (map->handle, LV2_ATOM__Object);
|
|
self->atom_Path = map->map (map->handle, LV2_ATOM__Path);
|
|
self->atom_Vector = map->map (map->handle, LV2_ATOM__Vector);
|
|
self->atom_Double = map->map (map->handle, LV2_ATOM__Double);
|
|
self->atom_URID = map->map (map->handle, LV2_ATOM__URID);
|
|
self->midi_MidiEvent = map->map (map->handle, LV2_MIDI__MidiEvent);
|
|
self->patch_Get = map->map (map->handle, LV2_PATCH__Get);
|
|
self->patch_Set = map->map (map->handle, LV2_PATCH__Set);
|
|
self->patch_property = map->map (map->handle, LV2_PATCH__property);
|
|
self->patch_value = map->map (map->handle, LV2_PATCH__value);
|
|
self->state_Changed = map->map (map->handle, "http://lv2plug.in/ns/ext/state#StateChanged");
|
|
self->afs_sf2file = map->map (map->handle, AFS_URN ":sf2file");
|
|
self->afs_tuning = map->map (map->handle, AFS_URN ":tuning");
|
|
|
|
return (LV2_Handle)self;
|
|
}
|
|
|
|
static void
|
|
connect_port (LV2_Handle instance,
|
|
uint32_t port,
|
|
void* data)
|
|
{
|
|
AFluidSynth* self = (AFluidSynth*)instance;
|
|
|
|
switch (port) {
|
|
case FS_PORT_CONTROL:
|
|
self->control = (const LV2_Atom_Sequence*)data;
|
|
break;
|
|
case FS_PORT_NOTIFY:
|
|
self->notify = (LV2_Atom_Sequence*)data;
|
|
break;
|
|
default:
|
|
if (port < FS_PORT_LAST) {
|
|
self->p_ports[port] = (float*)data;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
deactivate (LV2_Handle instance)
|
|
{
|
|
AFluidSynth* self = (AFluidSynth*)instance;
|
|
|
|
self->panic = true;
|
|
}
|
|
|
|
static void
|
|
run (LV2_Handle instance, uint32_t n_samples)
|
|
{
|
|
AFluidSynth* self = (AFluidSynth*)instance;
|
|
|
|
if (!self->control || !self->notify) {
|
|
return;
|
|
}
|
|
|
|
const uint32_t capacity = self->notify->atom.size;
|
|
lv2_atom_forge_set_buffer (&self->forge, (uint8_t*)self->notify, capacity);
|
|
lv2_atom_forge_sequence_head (&self->forge, &self->frame, 0);
|
|
|
|
const bool enabled = *self->p_ports[FS_PORT_ENABLE] > 0;
|
|
if (self->v_ports[FS_PORT_ENABLE] != *self->p_ports[FS_PORT_ENABLE]) {
|
|
if (self->initialized && !self->reinit_in_progress) {
|
|
fluid_synth_all_notes_off (self->synth, -1);
|
|
}
|
|
}
|
|
|
|
if (!self->initialized || self->reinit_in_progress) {
|
|
memset (self->p_ports[FS_PORT_OUT_L], 0, n_samples * sizeof (float));
|
|
memset (self->p_ports[FS_PORT_OUT_R], 0, n_samples * sizeof (float));
|
|
} else if (self->panic) {
|
|
fluid_synth_all_notes_off (self->synth, -1);
|
|
fluid_synth_all_sounds_off (self->synth, -1);
|
|
self->panic = false;
|
|
}
|
|
|
|
if (self->initialized && !self->reinit_in_progress) {
|
|
bool rev_change = false;
|
|
bool chr_change = false;
|
|
// TODO clamp values to ranges
|
|
if (self->v_ports[FS_OUT_GAIN] != *self->p_ports[FS_OUT_GAIN]) {
|
|
fluid_synth_set_gain (self->synth, db_to_coeff (*self->p_ports[FS_OUT_GAIN]));
|
|
}
|
|
if (self->v_ports[FS_REV_ENABLE] != *self->p_ports[FS_REV_ENABLE]) {
|
|
fluid_synth_set_reverb_on (self->synth, *self->p_ports[FS_REV_ENABLE] > 0 ? 1 : 0);
|
|
rev_change = true;
|
|
}
|
|
if (self->v_ports[FS_CHR_ENABLE] != *self->p_ports[FS_CHR_ENABLE]) {
|
|
fluid_synth_set_chorus_on (self->synth, *self->p_ports[FS_CHR_ENABLE] > 0 ? 1 : 0);
|
|
chr_change = true;
|
|
}
|
|
|
|
for (uint32_t p = FS_REV_ROOMSIZE; p <= FS_REV_LEVEL && !rev_change; ++p) {
|
|
if (self->v_ports[p] != *self->p_ports[p]) {
|
|
rev_change = true;
|
|
}
|
|
}
|
|
for (uint32_t p = FS_CHR_N; p <= FS_CHR_TYPE && !chr_change; ++p) {
|
|
if (self->v_ports[p] != *self->p_ports[p]) {
|
|
chr_change = true;
|
|
}
|
|
}
|
|
|
|
if (rev_change) {
|
|
fluid_synth_set_reverb (self->synth,
|
|
*self->p_ports[FS_REV_ROOMSIZE],
|
|
*self->p_ports[FS_REV_DAMPING],
|
|
*self->p_ports[FS_REV_WIDTH],
|
|
*self->p_ports[FS_REV_LEVEL]);
|
|
}
|
|
|
|
if (chr_change) {
|
|
fluid_synth_set_chorus (self->synth,
|
|
rintf (*self->p_ports[FS_CHR_N]),
|
|
db_to_coeff (*self->p_ports[FS_CHR_LEVEL]),
|
|
*self->p_ports[FS_CHR_SPEED],
|
|
*self->p_ports[FS_CHR_DEPTH],
|
|
(*self->p_ports[FS_CHR_TYPE] > 0) ? FLUID_CHORUS_MOD_SINE : FLUID_CHORUS_MOD_TRIANGLE);
|
|
}
|
|
for (uint32_t p = FS_OUT_GAIN; p < FS_PORT_LAST; ++p) {
|
|
self->v_ports[p] = *self->p_ports[p];
|
|
}
|
|
}
|
|
|
|
uint32_t offset = 0;
|
|
|
|
LV2_ATOM_SEQUENCE_FOREACH (self->control, ev)
|
|
{
|
|
const LV2_Atom_Object* obj = (LV2_Atom_Object*)&ev->body;
|
|
if (ev->body.type == self->atom_Blank || ev->body.type == self->atom_Object) {
|
|
if (obj->body.otype == self->patch_Get) {
|
|
self->inform_ui = false;
|
|
inform_ui (self);
|
|
} else if (obj->body.otype == self->patch_Set) {
|
|
const LV2_Atom* file_path = parse_patch_msg (self, obj);
|
|
if (file_path && !self->reinit_in_progress && !self->queue_reinit) {
|
|
const char* fn = (const char*)(file_path + 1);
|
|
strncpy (self->queue_sf2_file_path, fn, 1023);
|
|
self->queue_sf2_file_path[1023] = '\0';
|
|
self->reinit_in_progress = true;
|
|
int magic = 0x4711;
|
|
self->schedule->schedule_work (self->schedule->handle, sizeof (int), &magic);
|
|
}
|
|
}
|
|
} else if (ev->body.type == self->midi_MidiEvent) {
|
|
if (ev->time.frames >= n_samples || self->reinit_in_progress || !enabled) {
|
|
continue;
|
|
}
|
|
if (ev->body.size > 3) {
|
|
if (ev->body.size > 11) {
|
|
const uint8_t* const data = (const uint8_t*)(ev + 1);
|
|
if (data[0] == 0xf0 && (data[1] & 0x7e) == 0x7e && data[3] == 0x08) {
|
|
parse_mts (self, data, ev->body.size);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (ev->time.frames > offset) {
|
|
fluid_synth_write_float (
|
|
self->synth,
|
|
ev->time.frames - offset,
|
|
&self->p_ports[FS_PORT_OUT_L][offset], 0, 1,
|
|
&self->p_ports[FS_PORT_OUT_R][offset], 0, 1);
|
|
}
|
|
|
|
offset = ev->time.frames;
|
|
|
|
const uint8_t* const data = (const uint8_t*)(ev + 1);
|
|
fluid_midi_event_set_type (self->fmidi_event, data[0] & 0xf0);
|
|
fluid_midi_event_set_channel (self->fmidi_event, data[0] & 0x0f);
|
|
if (ev->body.size > 1) {
|
|
fluid_midi_event_set_key (self->fmidi_event, data[1]);
|
|
}
|
|
if (ev->body.size > 2) {
|
|
if (0xe0 /*PITCH_BEND*/ == fluid_midi_event_get_type (self->fmidi_event)) {
|
|
fluid_midi_event_set_value (self->fmidi_event, 0);
|
|
fluid_midi_event_set_pitch (self->fmidi_event, ((data[2] & 0x7f) << 7) | (data[1] & 0x7f));
|
|
} else {
|
|
fluid_midi_event_set_value (self->fmidi_event, data[2]);
|
|
}
|
|
if (0xb0 /* CC */ == fluid_midi_event_get_type (self->fmidi_event)) {
|
|
int chn = fluid_midi_event_get_channel (self->fmidi_event);
|
|
assert (chn >= 0 && chn < 16);
|
|
if (data[1] == 0x00) {
|
|
self->program_state[chn].bank &= 0x7f;
|
|
self->program_state[chn].bank |= (data[2] & 0x7f) << 7;
|
|
}
|
|
if (data[1] == 0x20) {
|
|
self->program_state[chn].bank &= 0x3F80;
|
|
self->program_state[chn].bank |= data[2] & 0x7f;
|
|
}
|
|
}
|
|
}
|
|
if (ev->body.size == 2 && 0xc0 /* Pgm */ == fluid_midi_event_get_type (self->fmidi_event)) {
|
|
int chn = fluid_midi_event_get_channel (self->fmidi_event);
|
|
assert (chn >= 0 && chn < 16);
|
|
self->program_state[chn].program = data[1];
|
|
#ifdef LV2_EXTENDED
|
|
if (self->bankpatch) {
|
|
self->bankpatch->notify (self->bankpatch->handle, chn,
|
|
self->program_state[chn].bank,
|
|
self->program_state[chn].program < 0 ? 255 : self->program_state[chn].program);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
fluid_synth_handle_midi_event (self->synth, self->fmidi_event);
|
|
}
|
|
}
|
|
|
|
if (self->queue_reinit && !self->reinit_in_progress) {
|
|
self->reinit_in_progress = true;
|
|
int magic = 0x4711;
|
|
self->schedule->schedule_work (self->schedule->handle, sizeof (int), &magic);
|
|
}
|
|
|
|
/* inform the GUI */
|
|
if (self->inform_ui) {
|
|
self->inform_ui = false;
|
|
|
|
/* emit stateChanged */
|
|
LV2_Atom_Forge_Frame frame;
|
|
lv2_atom_forge_frame_time (&self->forge, 0);
|
|
x_forge_object (&self->forge, &frame, 1, self->state_Changed);
|
|
lv2_atom_forge_pop (&self->forge, &frame);
|
|
|
|
/* send .sf2 filename */
|
|
inform_ui (self);
|
|
|
|
#ifdef LV2_EXTENDED
|
|
if (self->midnam)
|
|
self->midnam->update (self->midnam->handle);
|
|
#endif
|
|
}
|
|
|
|
#ifdef LV2_EXTENDED
|
|
if (self->send_bankpgm && self->bankpatch) {
|
|
self->send_bankpgm = false;
|
|
for (uint8_t chn = 0; chn < 16; ++chn) {
|
|
self->bankpatch->notify (self->bankpatch->handle, chn,
|
|
self->program_state[chn].bank,
|
|
self->program_state[chn].program < 0 ? 255 : self->program_state[chn].program);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (n_samples > offset && self->initialized && !self->reinit_in_progress) {
|
|
fluid_synth_write_float (
|
|
self->synth,
|
|
n_samples - offset,
|
|
&self->p_ports[FS_PORT_OUT_L][offset], 0, 1,
|
|
&self->p_ports[FS_PORT_OUT_R][offset], 0, 1);
|
|
}
|
|
}
|
|
|
|
static void
|
|
cleanup (LV2_Handle instance)
|
|
{
|
|
AFluidSynth* self = (AFluidSynth*)instance;
|
|
delete_fluid_synth (self->synth);
|
|
delete_fluid_settings (self->settings);
|
|
delete_fluid_midi_event (self->fmidi_event);
|
|
pthread_mutex_destroy (&self->bp_lock);
|
|
free (self);
|
|
}
|
|
|
|
/* *****************************************************************************
|
|
* LV2 Extensions
|
|
*/
|
|
|
|
static LV2_Worker_Status
|
|
work (LV2_Handle instance,
|
|
LV2_Worker_Respond_Function respond,
|
|
LV2_Worker_Respond_Handle handle,
|
|
uint32_t size,
|
|
const void* data)
|
|
{
|
|
AFluidSynth* self = (AFluidSynth*)instance;
|
|
|
|
if (size != sizeof (int)) {
|
|
return LV2_WORKER_ERR_UNKNOWN;
|
|
}
|
|
int magic = *((const int*)data);
|
|
if (magic != 0x4711) {
|
|
return LV2_WORKER_ERR_UNKNOWN;
|
|
}
|
|
|
|
self->initialized = load_sf2 (self, self->queue_sf2_file_path);
|
|
|
|
if (self->initialized) {
|
|
fluid_synth_all_notes_off (self->synth, -1);
|
|
fluid_synth_all_sounds_off (self->synth, -1);
|
|
self->panic = false;
|
|
// bootstrap synth engine.
|
|
float l[1024];
|
|
float r[1024];
|
|
fluid_synth_write_float (self->synth, 1024, l, 0, 1, r, 0, 1);
|
|
}
|
|
|
|
respond (handle, 1, "");
|
|
return LV2_WORKER_SUCCESS;
|
|
}
|
|
|
|
struct VectorOfDouble {
|
|
LV2_Atom_Vector_Body vb;
|
|
double pitch[128];
|
|
};
|
|
|
|
static LV2_Worker_Status
|
|
work_response (LV2_Handle instance,
|
|
uint32_t size,
|
|
const void* data)
|
|
{
|
|
AFluidSynth* self = (AFluidSynth*)instance;
|
|
|
|
if (self->initialized) {
|
|
strcpy (self->current_sf2_file_path, self->queue_sf2_file_path);
|
|
|
|
for (int chn = 0; chn < 16; ++chn) {
|
|
if (self->program_state[chn].program < 0) {
|
|
continue;
|
|
}
|
|
/* cannot direcly call fluid_channel_set_bank_msb/fluid_channel_set_bank_lsb, use CCs */
|
|
fluid_midi_event_set_type (self->fmidi_event, 0xb0 /* CC */);
|
|
fluid_midi_event_set_channel (self->fmidi_event, chn);
|
|
|
|
fluid_midi_event_set_control (self->fmidi_event, 0x00); // BANK_SELECT_MSB
|
|
fluid_midi_event_set_value (self->fmidi_event, (self->program_state[chn].bank >> 7) & 0x7f);
|
|
fluid_synth_handle_midi_event (self->synth, self->fmidi_event);
|
|
|
|
fluid_midi_event_set_control (self->fmidi_event, 0x20); // BANK_SELECT_LSB
|
|
fluid_midi_event_set_value (self->fmidi_event, self->program_state[chn].bank & 0x7f);
|
|
fluid_synth_handle_midi_event (self->synth, self->fmidi_event);
|
|
|
|
fluid_synth_program_change (self->synth, chn, self->program_state[chn].program);
|
|
}
|
|
|
|
for (int chn = 0; chn < 16; ++chn) {
|
|
int sfid = 0;
|
|
int bank = 0;
|
|
int program = -1;
|
|
if (FLUID_OK == fluid_synth_get_program (self->synth, chn, &sfid, &bank, &program)) {
|
|
self->program_state[chn].bank = bank;
|
|
self->program_state[chn].program = program;
|
|
}
|
|
}
|
|
if (self->queue_retune) {
|
|
int rv = fluid_synth_activate_key_tuning (self->synth,
|
|
/* tuning bank */ 0,
|
|
/* tuning prog */ 0,
|
|
"ACE", self->queue_tuning, 0);
|
|
if (rv == FLUID_OK) {
|
|
for (int c = 0; c < 16; ++c) {
|
|
fluid_synth_activate_tuning (self->synth, c, 0, 0, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
} else {
|
|
self->current_sf2_file_path[0] = 0;
|
|
}
|
|
|
|
self->reinit_in_progress = false;
|
|
self->inform_ui = true;
|
|
self->send_bankpgm = true;
|
|
self->queue_retune = false;
|
|
self->queue_reinit = false;
|
|
return LV2_WORKER_SUCCESS;
|
|
}
|
|
|
|
static LV2_State_Status
|
|
save (LV2_Handle instance,
|
|
LV2_State_Store_Function store,
|
|
LV2_State_Handle handle,
|
|
uint32_t flags,
|
|
const LV2_Feature* const* features)
|
|
{
|
|
AFluidSynth* self = (AFluidSynth*)instance;
|
|
|
|
if (strlen (self->current_sf2_file_path) == 0) {
|
|
return LV2_STATE_ERR_NO_PROPERTY;
|
|
}
|
|
|
|
LV2_State_Map_Path* map_path = NULL;
|
|
#ifdef LV2_STATE__freePath
|
|
LV2_State_Free_Path* free_path = NULL;
|
|
#endif
|
|
|
|
for (int i = 0; features[i]; ++i) {
|
|
if (!strcmp (features[i]->URI, LV2_STATE__mapPath)) {
|
|
map_path = (LV2_State_Map_Path*)features[i]->data;
|
|
}
|
|
#ifdef LV2_STATE__freePath
|
|
else if (!strcmp (features[i]->URI, LV2_STATE__freePath)) {
|
|
free_path = (LV2_State_Free_Path*)features[i]->data;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
if (!map_path) {
|
|
return LV2_STATE_ERR_NO_FEATURE;
|
|
}
|
|
|
|
char* apath = map_path->abstract_path (map_path->handle, self->current_sf2_file_path);
|
|
store (handle, self->afs_sf2file,
|
|
apath, strlen (apath) + 1,
|
|
self->atom_Path, LV2_STATE_IS_POD);
|
|
#ifdef LV2_STATE__freePath
|
|
if (free_path) {
|
|
free_path->free_path (free_path->handle, apath);
|
|
} else
|
|
#endif
|
|
{
|
|
#ifndef _WIN32 // https://github.com/drobilla/lilv/issues/14
|
|
free (apath);
|
|
#endif
|
|
}
|
|
|
|
int tbank, tprog;
|
|
fluid_synth_tuning_iteration_start (self->synth);
|
|
if (0 != fluid_synth_tuning_iteration_next (self->synth, &tbank, &tprog)) {
|
|
VectorOfDouble vod;
|
|
vod.vb.child_type = self->atom_Double;
|
|
vod.vb.child_size = sizeof (double);
|
|
fluid_synth_tuning_dump (self->synth, tbank, tprog, NULL, 0, vod.pitch);
|
|
store (handle, self->afs_tuning,
|
|
(void*)&vod, sizeof (LV2_Atom_Vector_Body) + 128 * sizeof (double),
|
|
self->atom_Vector,
|
|
LV2_STATE_IS_POD);
|
|
}
|
|
|
|
return LV2_STATE_SUCCESS;
|
|
}
|
|
|
|
static LV2_State_Status
|
|
restore (LV2_Handle instance,
|
|
LV2_State_Retrieve_Function retrieve,
|
|
LV2_State_Handle handle,
|
|
uint32_t flags,
|
|
const LV2_Feature* const* features)
|
|
{
|
|
AFluidSynth* self = (AFluidSynth*)instance;
|
|
if (self->reinit_in_progress || self->queue_reinit) {
|
|
lv2_log_warning (&self->logger, "a-fluidsynth.lv2: sf2 load already queued.\n");
|
|
return LV2_STATE_ERR_UNKNOWN;
|
|
}
|
|
|
|
LV2_State_Map_Path* map_path = NULL;
|
|
#ifdef LV2_STATE__freePath
|
|
LV2_State_Free_Path* free_path = NULL;
|
|
#endif
|
|
|
|
for (int i = 0; features[i]; ++i) {
|
|
if (!strcmp (features[i]->URI, LV2_STATE__mapPath)) {
|
|
map_path = (LV2_State_Map_Path*)features[i]->data;
|
|
}
|
|
#ifdef LV2_STATE__freePath
|
|
else if (!strcmp (features[i]->URI, LV2_STATE__freePath)) {
|
|
free_path = (LV2_State_Free_Path*)features[i]->data;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
if (!map_path) {
|
|
return LV2_STATE_ERR_NO_FEATURE;
|
|
}
|
|
|
|
size_t size;
|
|
uint32_t type;
|
|
uint32_t valflags;
|
|
|
|
const void* value = retrieve (handle, self->afs_sf2file, &size, &type, &valflags);
|
|
if (!value) {
|
|
return LV2_STATE_ERR_NO_PROPERTY;
|
|
}
|
|
|
|
char* apath = map_path->absolute_path (map_path->handle, (const char*)value);
|
|
strncpy (self->queue_sf2_file_path, apath, 1023);
|
|
self->queue_sf2_file_path[1023] = '\0';
|
|
self->queue_reinit = true;
|
|
#ifdef LV2_STATE__freePath
|
|
if (free_path) {
|
|
free_path->free_path (free_path->handle, apath);
|
|
} else
|
|
#endif
|
|
{
|
|
#ifndef _WIN32 // https://github.com/drobilla/lilv/issues/14
|
|
free (apath);
|
|
#endif
|
|
}
|
|
|
|
value = retrieve (handle, self->afs_tuning, &size, &type, &valflags);
|
|
if (value && size == sizeof (LV2_Atom_Vector_Body) + 128 * sizeof (double) && type == self->atom_Vector) {
|
|
memcpy (self->queue_tuning, LV2_ATOM_BODY (value), 128 * sizeof (double));
|
|
self->queue_retune = true;
|
|
}
|
|
|
|
return LV2_STATE_SUCCESS;
|
|
}
|
|
|
|
static std::string
|
|
xml_escape (const std::string& s)
|
|
{
|
|
std::string r (s);
|
|
std::replace (r.begin (), r.end (), '"', '\'');
|
|
size_t pos = 0;
|
|
while ((pos = r.find ("&", pos)) != std::string::npos) {
|
|
r.replace (pos, 1, "&");
|
|
pos += 4;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
#ifdef LV2_EXTENDED
|
|
static char*
|
|
mn_file (LV2_Handle instance)
|
|
{
|
|
AFluidSynth* self = (AFluidSynth*)instance;
|
|
char* rv = NULL;
|
|
char tmp[1024];
|
|
|
|
rv = (char*)calloc (1, sizeof (char));
|
|
|
|
#define pf(...) \
|
|
do { \
|
|
snprintf (tmp, sizeof (tmp), __VA_ARGS__); \
|
|
tmp[sizeof (tmp) - 1] = '\0'; \
|
|
rv = (char*)realloc (rv, strlen (rv) + strlen (tmp) + 1); \
|
|
strcat (rv, tmp); \
|
|
} while (0)
|
|
|
|
pf ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
|
"<!DOCTYPE MIDINameDocument PUBLIC \"-//MIDI Manufacturers Association//DTD MIDINameDocument 1.0//EN\" \"http://dev.midi.org/dtds/MIDINameDocument10.dtd\">\n"
|
|
"<MIDINameDocument>\n"
|
|
" <Author/>\n"
|
|
" <MasterDeviceNames>\n"
|
|
" <Manufacturer>Ardour Foundation</Manufacturer>\n"
|
|
" <Model>%s:%p</Model>\n",
|
|
AFS_URN, (void*)self);
|
|
|
|
pf (" <CustomDeviceMode Name=\"Default\">\n");
|
|
pf (" <ChannelNameSetAssignments>\n");
|
|
for (int c = 0; c < 16; ++c) {
|
|
pf (" <ChannelNameSetAssign Channel=\"%d\" NameSet=\"Presets\"/>\n", c + 1);
|
|
}
|
|
pf (" </ChannelNameSetAssignments>\n");
|
|
pf (" </CustomDeviceMode>\n");
|
|
|
|
// TODO collect used banks, std::set<> would be a nice here
|
|
|
|
pf (" <ChannelNameSet Name=\"Presets\">\n");
|
|
pf (" <AvailableForChannels>\n");
|
|
for (int c = 0; c < 16; ++c) {
|
|
pf (" <AvailableChannel Channel=\"%d\" Available=\"true\"/>\n", c + 1);
|
|
}
|
|
pf (" </AvailableForChannels>\n");
|
|
pf (" <UsesControlNameList Name=\"Controls\"/>\n");
|
|
|
|
int bn = 1;
|
|
pthread_mutex_lock (&self->bp_lock);
|
|
const BPMap& ps (self->presets);
|
|
pthread_mutex_unlock (&self->bp_lock);
|
|
|
|
for (BPMap::const_iterator i = ps.begin (); i != ps.end (); ++i, ++bn) {
|
|
pf (" <PatchBank Name=\"Patch Bank %d\">\n", i->first);
|
|
if (i->second.size () > 0) {
|
|
pf (" <MIDICommands>\n");
|
|
pf (" <ControlChange Control=\"0\" Value=\"%d\"/>\n", (i->first >> 7) & 127);
|
|
pf (" <ControlChange Control=\"32\" Value=\"%d\"/>\n", i->first & 127);
|
|
pf (" </MIDICommands>\n");
|
|
pf (" <PatchNameList>\n");
|
|
int n = 0;
|
|
for (BPList::const_iterator j = i->second.begin (); j != i->second.end (); ++j, ++n) {
|
|
pf (" <Patch Number=\"%d\" Name=\"%s\" ProgramChange=\"%d\"/>\n",
|
|
n, xml_escape (j->name).c_str (), j->program);
|
|
}
|
|
pf (" </PatchNameList>\n");
|
|
}
|
|
pf (" </PatchBank>\n");
|
|
}
|
|
pf (" </ChannelNameSet>\n");
|
|
|
|
pf (" <ControlNameList Name=\"Controls\">\n");
|
|
pf (" <Control Type=\"7bit\" Number=\"1\" Name=\"Modulation\"/>\n");
|
|
pf (" <Control Type=\"7bit\" Number=\"2\" Name=\"Breath\"/>\n");
|
|
pf (" <Control Type=\"7bit\" Number=\"5\" Name=\"Portamento Time\"/>\n");
|
|
pf (" <Control Type=\"7bit\" Number=\"7\" Name=\"Channel Volume\"/>\n");
|
|
pf (" <Control Type=\"7bit\" Number=\"8\" Name=\"Stereo Balance\"/>\n");
|
|
pf (" <Control Type=\"7bit\" Number=\"10\" Name=\"Pan\"/>\n");
|
|
pf (" <Control Type=\"7bit\" Number=\"11\" Name=\"Expression\"/>\n");
|
|
pf (" <Control Type=\"7bit\" Number=\"37\" Name=\"Portamento Time (Fine)\"/>\n");
|
|
pf (" <Control Type=\"7bit\" Number=\"64\" Name=\"Sustain On/Off\"/>\n");
|
|
pf (" <Control Type=\"7bit\" Number=\"65\" Name=\"Portamento On/Off\"/>\n");
|
|
pf (" <Control Type=\"7bit\" Number=\"66\" Name=\"Sostenuto On/Off\"/>\n");
|
|
pf (" <Control Type=\"7bit\" Number=\"68\" Name=\"Legato On/Off\"/>\n");
|
|
pf (" <Control Type=\"7bit\" Number=\"91\" Name=\"Reverb\"/>\n");
|
|
pf (" <Control Type=\"7bit\" Number=\"93\" Name=\"Chorus\"/>\n");
|
|
pf (" </ControlNameList>\n");
|
|
|
|
pf (
|
|
" </MasterDeviceNames>\n"
|
|
"</MIDINameDocument>");
|
|
|
|
//printf("-----\n%s\n------\n", rv);
|
|
return rv;
|
|
}
|
|
|
|
static char*
|
|
mn_model (LV2_Handle instance)
|
|
{
|
|
AFluidSynth* self = (AFluidSynth*)instance;
|
|
char* rv = (char*)malloc (64 * sizeof (char));
|
|
snprintf (rv, 64, "%s:%p", AFS_URN, (void*)self);
|
|
rv[63] = 0;
|
|
return rv;
|
|
}
|
|
|
|
static void
|
|
mn_free (char* v)
|
|
{
|
|
free (v);
|
|
}
|
|
#endif
|
|
|
|
static const void*
|
|
extension_data (const char* uri)
|
|
{
|
|
if (!strcmp (uri, LV2_WORKER__interface)) {
|
|
static const LV2_Worker_Interface worker = { work, work_response, NULL };
|
|
return &worker;
|
|
} else if (!strcmp (uri, LV2_STATE__interface)) {
|
|
static const LV2_State_Interface state = { save, restore };
|
|
return &state;
|
|
}
|
|
#ifdef LV2_EXTENDED
|
|
else if (!strcmp (uri, LV2_MIDNAM__interface)) {
|
|
static const LV2_Midnam_Interface midnam = { mn_file, mn_model, mn_free };
|
|
return &midnam;
|
|
}
|
|
#endif
|
|
return NULL;
|
|
}
|
|
|
|
static const LV2_Descriptor descriptor = {
|
|
AFS_URN,
|
|
instantiate,
|
|
connect_port,
|
|
NULL,
|
|
run,
|
|
deactivate,
|
|
cleanup,
|
|
extension_data
|
|
};
|
|
|
|
#undef LV2_SYMBOL_EXPORT
|
|
#ifdef _WIN32
|
|
# define LV2_SYMBOL_EXPORT __declspec(dllexport)
|
|
#else
|
|
# define LV2_SYMBOL_EXPORT __attribute__ ((visibility ("default")))
|
|
#endif
|
|
LV2_SYMBOL_EXPORT
|
|
const LV2_Descriptor*
|
|
lv2_descriptor (uint32_t index)
|
|
{
|
|
switch (index) {
|
|
case 0:
|
|
return &descriptor;
|
|
default:
|
|
return NULL;
|
|
}
|
|
}
|