//
//  LADSPAInfo.C - Class for indexing information on LADSPA Plugins
//
//  Copyleft (C) 2002  Mike Rawes <myk@waxfrenzy.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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//

// #include <config.h>

#include <vector>
#include <string>
#include <list>
#include <map>
#include <iostream>
#include <sstream>
#include <algorithm>
#include <stdio.h>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <dlfcn.h>

#include <ladspa.h>

#define HAVE_LIBLRDF 1
#ifdef HAVE_LIBLRDF
#include <lrdf.h>
#endif

#include "LADSPAInfo.h"

using namespace std;

LADSPAInfo::LADSPAInfo(bool override,
                       const char *path_list)
{
	if (strlen(path_list) > 0) {
		m_ExtraPaths = strdup(path_list);
	} else {
		m_ExtraPaths = NULL;
	}
	m_LADSPAPathOverride = override;

	RescanPlugins();
}

LADSPAInfo::~LADSPAInfo()
{
	CleanUp();
}

void
LADSPAInfo::RescanPlugins(void)
{
// Clear out what we've got
	CleanUp();

	if (!m_LADSPAPathOverride) {
	// Get $LADPSA_PATH, if available
		char *ladspa_path = getenv("LADSPA_PATH");
		if (ladspa_path) {
			ScanPathList(ladspa_path, &LADSPAInfo::ExaminePluginLibrary);

		} else {

			cerr << "WARNING: LADSPA_PATH environment variable not set" << endl;
			cerr << "         Assuming /usr/lib/ladspa:/usr/local/lib/ladspa" << endl;

			ScanPathList("/usr/lib/ladspa:/usr/local/lib/ladspa", &LADSPAInfo::ExaminePluginLibrary);
		}
	}

// Check any supplied extra paths
	if (m_ExtraPaths) {
		ScanPathList(m_ExtraPaths, &LADSPAInfo::ExaminePluginLibrary);
	}

// Do we have any plugins now?
	if (m_Plugins.size() == 0) {
		cerr << "WARNING: No plugins found" << endl;
	} else {
		cerr << m_Plugins.size() << " plugins found in " << m_Libraries.size() << " libraries" << endl;

#ifdef HAVE_LIBLRDF
	// Got some plugins. Now search for RDF data
		lrdf_init();

		char *rdf_path = getenv("LADSPA_RDF_PATH");

		if (rdf_path) {
		// Examine rdf info
			ScanPathList(rdf_path, &LADSPAInfo::ExamineRDFFile);

		} else {
			cerr << "WARNING: LADSPA_RDF_PATH environment variable not set" << endl;
			cerr << "         Assuming /usr/share/ladspa/rdf:/usr/local/share/ladspa/rdf" << endl;

		// Examine rdf info
			ScanPathList("/usr/share/ladspa/rdf:/usr/local/share/ladspa/rdf", &LADSPAInfo::ExamineRDFFile);
		}
		MetadataRDFDescend(LADSPA_BASE "Plugin", 0);

	// See which plugins were not added to an rdf group, and add them
	// all into the top level 'LADSPA' one
		list<unsigned long> rdf_p;

	// Get indices of plugins added to groups
		for (vector<RDFURIInfo>::iterator ri = m_RDFURIs.begin(); ri != m_RDFURIs.end(); ri++) {
			rdf_p.insert(rdf_p.begin(), ri->Plugins.begin(), ri->Plugins.end());
		}

	// Add all uncategorized plugins to top level group, subclassed by their
	// library's basename.
		rdf_p.unique();
		rdf_p.sort();
		unsigned long last_p = 0;
		for (list<unsigned long>::iterator p = rdf_p.begin(); p != rdf_p.end(); p++) {
			if ((*p - last_p) > 1) {
				for (unsigned long i = last_p + 1; i < *p; i++) {
				// URI 0 is top-level "LADSPA" group
					m_RDFURIs[0].Plugins.push_back(i);
				}
			}
			last_p = *p;
		}
		while (++last_p < m_Plugins.size()) {
		// URI 0 is top-level "LADSPA" group
			m_RDFURIs[0].Plugins.push_back(last_p);
		}

		lrdf_cleanup();
#else
	// No RDF. Add all plugins to top-level group
		RDFURIInfo ri;

		ri.URI = "";
		ri.Label = "LADSPA";

		m_RDFURIs.push_back(ri);
		m_RDFLabelLookup["LADSPA"] = 0;

		for (unsigned long i = 0; i < m_Plugins.size(); i++) {
		// Add plugin index
			m_RDFURIs[0].Plugins.push_back(i);
		}
#endif
	}
}

void
LADSPAInfo::UnloadAllLibraries(void)
{
// Blank descriptors
	for (vector<PluginInfo>::iterator i = m_Plugins.begin();
		i != m_Plugins.end(); i++) {
		if (i->Descriptor) i->Descriptor = NULL;
	}
// Unload DLLs,
	for (vector<LibraryInfo>::iterator i = m_Libraries.begin();
		i != m_Libraries.end(); i++) {
		if (i->Handle) {
			dlclose(i->Handle);
			i->Handle = NULL;
		}
		i->RefCount = 0;
	}
}

const LADSPA_Descriptor *
LADSPAInfo::GetDescriptorByID(unsigned long unique_id)
{
	if (m_IDLookup.find(unique_id) == m_IDLookup.end()) {
		cerr << "LADSPA Plugin ID " << unique_id << " not found!" << endl;
		return NULL;
	}

// Got plugin index
	unsigned long plugin_index = m_IDLookup[unique_id];

	PluginInfo *pi = &(m_Plugins[plugin_index]);
	LibraryInfo *li = &(m_Libraries[pi->LibraryIndex]);

	if (!(pi->Descriptor)) {
		LADSPA_Descriptor_Function desc_func = GetDescriptorFunctionForLibrary(pi->LibraryIndex);
		if (desc_func) pi->Descriptor = desc_func(pi->Index);
	}

	if (pi->Descriptor) {

	// Success, so increment ref counter for library
		li->RefCount++;
	}

	return pi->Descriptor;
}

void
LADSPAInfo::DiscardDescriptorByID(unsigned long unique_id)
{
	if (m_IDLookup.find(unique_id) == m_IDLookup.end()) {
		cerr << "LADSPA Plugin ID " << unique_id << " not found!" << endl;
	} else {

	// Get plugin index
		unsigned long plugin_index = m_IDLookup[unique_id];

		PluginInfo *pi = &(m_Plugins[plugin_index]);
		LibraryInfo *li = &(m_Libraries[pi->LibraryIndex]);

		pi->Descriptor = NULL;

	// Decrement reference counter for library, and unload if last
		if (li->RefCount > 0) {
			li->RefCount--;
			if (li->RefCount == 0) {

			// Unload library
				dlclose(li->Handle);
				li->Handle = NULL;
			}
		}
	}
}

// ****************************************************************************
// **                      SSM Specific Functions                            **
// ****************************************************************************

unsigned long
LADSPAInfo::GetIDFromFilenameAndLabel(std::string filename,
                                      std::string label)
{
	bool library_loaded = false;

	if (m_FilenameLookup.find(filename) == m_FilenameLookup.end()) {
		cerr << "LADSPA Library " << filename << " not found!" << endl;
		return 0;
	}

	unsigned long library_index = m_FilenameLookup[filename];

	if (!(m_Libraries[library_index].Handle)) library_loaded = true;

	LADSPA_Descriptor_Function desc_func = GetDescriptorFunctionForLibrary(library_index);

	if (!desc_func) {
		return 0;
	}

// Search for label in library
	const LADSPA_Descriptor *desc;
	for (unsigned long i = 0; (desc = desc_func(i)) != NULL; i++) {
		string l = desc->Label;
		if (l == label) {

		// If we had to load the library, unload it
			unsigned long id = desc->UniqueID;
			if (library_loaded) {
				dlclose(m_Libraries[library_index].Handle);
				m_Libraries[library_index].Handle = NULL;
			}
			return id;
		}
	}

	cerr << "Plugin " << label << " not found in library " << filename << endl;
	return 0;
}

const vector<LADSPAInfo::PluginEntry>
LADSPAInfo::GetMenuList(void)
{
	m_SSMMenuList.clear();

	DescendGroup("", "LADSPA", 1);

	return m_SSMMenuList;
}

const vector<LADSPAInfo::PluginInfo>
LADSPAInfo::GetPluginInfo(void)
{
    return m_Plugins;
}

unsigned long
LADSPAInfo::GetPluginListEntryByID(unsigned long unique_id)
{
	unsigned long j = 0;
	for (vector<PluginEntry>::iterator i = m_SSMMenuList.begin();
		i != m_SSMMenuList.end(); i++, j++) {
		if (i->UniqueID == unique_id) return j;
	}
	return m_SSMMenuList.size();
}

// ****************************************************************************
// **                     Private Member Functions                           **
// ****************************************************************************

// Build a list of plugins by group, suitable for SSM LADSPA Plugin drop-down
// The top-level "LADSPA" group is not included

void
LADSPAInfo::DescendGroup(string prefix,
                         const string group,
                         unsigned int depth)
{
	list<string> groups = GetSubGroups(group);

	if (prefix.length() > 0) {
	// Add an explicit '/' as we're creating sub-menus from groups
		prefix += "/";
	}

	for (list<string>::iterator g = groups.begin(); g != groups.end(); g++) {
		string name;

		// Escape '/' and '|' characters
		size_t x = g->find_first_of("/|");
		if (x == string::npos) {
			name = *g;
		} else {
                    size_t last_x = 0;
			while (x < string::npos) {
				name += g->substr(last_x, x - last_x) + '\\' + (*g)[x];
				last_x = x + 1;
				x = g->find_first_of("/|", x + 1);
			}
			name += g->substr(last_x, x - last_x);
		}

		DescendGroup(prefix + name, *g, depth + 1);
	}
	if (m_RDFLabelLookup.find(group) != m_RDFLabelLookup.end()) {
		unsigned long uri_index = m_RDFLabelLookup[group];

	// Create group for unclassified plugins
		if (prefix.length() == 0) {
			prefix = "Unclassified/";
			depth = depth + 1;
		}

	// Temporary list (for sorting the plugins by name)
		list<PluginEntry> plugins;

		for (vector<unsigned long>::iterator p = m_RDFURIs[uri_index].Plugins.begin();
			p != m_RDFURIs[uri_index].Plugins.end(); p++) {

			PluginInfo *pi = &(m_Plugins[*p]);
			string name;

		// Escape '/' and '|' characters
			size_t x = pi->Name.find_first_of("/|");
			if (x == string::npos) {
				name = pi->Name;
			} else {
				size_t last_x = 0;
				while (x < string::npos) {
					name += pi->Name.substr(last_x, x - last_x) + '\\' + pi->Name[x];
					last_x = x + 1;
					x = pi->Name.find_first_of("/|", x + 1);
				}
				name += pi->Name.substr(last_x, x - last_x);
			}

			PluginEntry pe;

			pe.Depth = depth;
			pe.UniqueID = pi->UniqueID;
			pe.Name = name;
                        pe.Category = prefix;
                        pe.Category = pe.Category.substr(0, pe.Category.size()-1);

                  
			plugins.push_back(pe);
		}
		plugins.sort();

	// Deal with duplicates by numbering them
		for (list<PluginEntry>::iterator i = plugins.begin();
			i != plugins.end(); ) {
			string name = i->Name;

			i++;
			unsigned long n = 2;
			while ((i != plugins.end()) && (i->Name == name)) {
				stringstream s;
				s << n;
				i->Name = name + " (" + s.str() + ")";
				n++;
				i++;
			}
		}

	// Add all ordered entries to the Menu List
	// This ensures that plugins appear after groups
		for (list<PluginEntry>::iterator p = plugins.begin(); p != plugins.end(); p++) {
			m_SSMMenuList.push_back(*p);
		}
	}
}

// Get list of groups that are within given group. The root group is
// always "LADSPA"
list<string>
LADSPAInfo::GetSubGroups(const string group)
{
	list<string> groups;
	unsigned long uri_index;

	if (m_RDFLabelLookup.find(group) == m_RDFLabelLookup.end()) {
		return groups;
	} else {
		uri_index = m_RDFLabelLookup[group];
	}

	for (vector<unsigned long>::iterator sg = m_RDFURIs[uri_index].Children.begin();
		sg != m_RDFURIs[uri_index].Children.end(); sg++) {
		groups.push_back(m_RDFURIs[*sg].Label);
	}

	groups.sort();

	return groups;
}

// Unload any loaded DLLs and clear vectors etc
void
LADSPAInfo::CleanUp(void)
{
	m_MaxInputPortCount = 0;

	m_IDLookup.clear();
	m_Plugins.clear();

// Unload loaded dlls
	for (vector<LibraryInfo>::iterator i = m_Libraries.begin();
		i != m_Libraries.end(); i++) {
		if (i->Handle) dlclose(i->Handle);
	}

	m_Libraries.clear();
	m_Paths.clear();

	m_RDFURILookup.clear();
	m_RDFURIs.clear();

	if (m_ExtraPaths) {
		free(m_ExtraPaths);
		m_ExtraPaths = NULL;
	}
}

// Given a colon-separated list of paths, examine the contents of each
// path, examining any regular files using the given member function,
// which currently can be:
//
//   ExaminePluginLibrary - add plugin library info from plugins
//   ExamineRDFFile       - add plugin information from .rdf/.rdfs files
void
LADSPAInfo::ScanPathList(const char *path_list,
                         void (LADSPAInfo::*ExamineFunc)(const string,
                                                         const string))
{
	const char *start;
	const char *end;
	int extra;
	char *path;
	string basename;
	DIR *dp;
	struct dirent *ep;
	struct stat sb;

// This does the same kind of thing as strtok, but strtok won't
// like the const
	start = path_list;
	while (*start != '\0') {
		while (*start == ':') start++;
		end = start;
		while (*end != ':' && *end != '\0') end++;

		if (end - start > 0) {
			extra = (*(end - 1) == '/') ? 0 : 1;
			path = (char *)malloc(end - start + 1 + extra);
			if (path) {
				strncpy(path, start, end - start);
				if (extra == 1) path[end - start] = '/';
				path[end - start + extra] = '\0';

				dp = opendir(path);
				if (!dp) {
					cerr << "WARNING: Could not open path " << path << endl;
				} else {
					while ((ep = readdir(dp))) {

					// Stat file to get type
						basename = ep->d_name;
						if (!stat((path + basename).c_str(), &sb)) {

						// We only want regular files
							if (S_ISREG(sb.st_mode)) (*this.*ExamineFunc)(path, basename);
						}
					}
					closedir(dp);
				}
				free(path);
			}
		}
		start = end;
	}
}

// Check given file is a valid LADSPA Plugin library
//
// If so, add path, library and plugin info
// to the m_Paths, m_Libraries and m_Plugins vectors.
//
void
LADSPAInfo::ExaminePluginLibrary(const string path,
                                 const string basename)
{
	void *handle;
	LADSPA_Descriptor_Function desc_func;
	const LADSPA_Descriptor *desc;
	string fullpath = path + basename;

// We're not executing any code, so be lazy about resolving symbols
	handle = dlopen(fullpath.c_str(), RTLD_LAZY);

	if (!handle) {
		cerr << "WARNING: File " << fullpath
			<< " could not be examined" << endl;
		cerr << "dlerror() output:" << endl;
		cerr << dlerror() << endl;
	} else {

	// It's a DLL, so now see if it's a LADSPA plugin library
		desc_func = (LADSPA_Descriptor_Function)dlsym(handle,
													"ladspa_descriptor");
		if (!desc_func) {

		// Is DLL, but not a LADSPA one
			cerr << "WARNING: DLL " << fullpath
				<< " has no ladspa_descriptor function" << endl;
			cerr << "dlerror() output:" << endl;
			cerr << dlerror() << endl;
		} else {

		// Got ladspa_descriptor, so we can now get plugin info
			bool library_added = false;
			unsigned long i = 0;
			desc = desc_func(i);
			while (desc) {

			// First, check that it's not a dupe
				if (m_IDLookup.find(desc->UniqueID) != m_IDLookup.end()) {
					unsigned long plugin_index = m_IDLookup[desc->UniqueID];
					unsigned long library_index = m_Plugins[plugin_index].LibraryIndex;
					unsigned long path_index = m_Libraries[library_index].PathIndex;

					cerr << "WARNING: Duplicated Plugin ID ("
						<< desc->UniqueID << ") found:" << endl;

					cerr << "  Plugin " << m_Plugins[plugin_index].Index
						<< " in library: " << m_Paths[path_index]
						<< m_Libraries[library_index].Basename
						<< " [First instance found]" << endl;
					cerr << "  Plugin " << i << " in library: " << fullpath
						<< " [Duplicate not added]" << endl;
				} else {
					if (CheckPlugin(desc)) {

					// Add path if not already added
						unsigned long path_index;
						vector<string>::iterator p = find(m_Paths.begin(), m_Paths.end(), path);
						if (p == m_Paths.end()) {
							path_index = m_Paths.size();
							m_Paths.push_back(path);
						} else {
							path_index = p - m_Paths.begin();
						}

					// Add library info if not already added
						if (!library_added) {
							LibraryInfo li;
							li.PathIndex = path_index;
							li.Basename = basename;
							li.RefCount = 0;
							li.Handle = NULL;
							m_Libraries.push_back(li);

							library_added = true;
						}

                                            
					// Add plugin info
						PluginInfo pi;
						pi.LibraryIndex = m_Libraries.size() - 1;
						pi.Index = i;
						pi.UniqueID = desc->UniqueID;
						pi.Label = desc->Label;
						pi.Name = desc->Name;
						pi.Descriptor = NULL;
                                                pi.Maker = desc->Maker;
                                                pi.AudioInputs = 0;
                                                pi.AudioOutputs = 0;
                                                
					// Find number of input ports
						unsigned long in_port_count = 0;
						for (unsigned long p = 0; p < desc->PortCount; p++) {
                                                    if (LADSPA_IS_PORT_INPUT(desc->PortDescriptors[p])) {
                                                        in_port_count++;
                                                        if ( LADSPA_IS_PORT_AUDIO(desc->PortDescriptors[p] ) )
                                                            pi.AudioInputs++;
                                                    }
						}
                                                for (unsigned long p = 0; p < desc->PortCount; p++) {
                                                    if (LADSPA_IS_PORT_OUTPUT(desc->PortDescriptors[p])) {
                                                        
                                                        if ( LADSPA_IS_PORT_AUDIO(desc->PortDescriptors[p] ) )
                                                            pi.AudioOutputs++;
                                                    }
						}
					
						if (in_port_count > m_MaxInputPortCount) {
                                                    m_MaxInputPortCount = in_port_count;
                                                    
						}

						m_Plugins.push_back(pi);

					// Add to index
						m_IDLookup[desc->UniqueID] = m_Plugins.size() - 1;

					} else {
						cerr << "WARNING: Plugin " << desc->UniqueID << " not added" << endl;
					}
				}

                                
				desc = desc_func(++i);
			}
		}
		dlclose(handle);
	}
}

#ifdef HAVE_LIBLRDF
// Examine given RDF plugin meta-data file
void
LADSPAInfo::ExamineRDFFile(const std::string path,
                           const std::string basename)
{
	string fileuri = "file://" + path + basename;

	if (lrdf_read_file(fileuri.c_str())) {
		cerr << "WARNING: File " << path + basename << " could not be parsed [Ignored]" << endl;
	}
}

// Recursively add rdf information for plugins that have been
// found from scanning LADSPA_PATH
void
LADSPAInfo::MetadataRDFDescend(const char * uri,
                               unsigned long parent)
{
	unsigned long this_uri_index;

// Check URI not already added
	if (m_RDFURILookup.find(uri) == m_RDFURILookup.end()) {

	// Not found
		RDFURIInfo ri;

		ri.URI = uri;

		if (ri.URI == LADSPA_BASE "Plugin") {

		// Add top level group as "LADSPA"
		// This will always happen, even if there are no .rdf files read by liblrdf
		// or if there is no liblrdf support
			ri.Label = "LADSPA";
		} else {
			char * label = lrdf_get_label(uri);
			if (label) {
				ri.Label = label;
			} else {
				ri.Label = "(No label)";
			}
		}

	// Add any instances found
		lrdf_uris * instances = lrdf_get_instances(uri);
		if (instances) {
			for (unsigned long j = 0; j < instances->count; j++) {
				unsigned long uid = lrdf_get_uid(instances->items[j]);
				if (m_IDLookup.find(uid) != m_IDLookup.end()) {
					ri.Plugins.push_back(m_IDLookup[uid]);
				}
			}
		}

		lrdf_free_uris(instances);

		m_RDFURIs.push_back(ri);
		this_uri_index = m_RDFURIs.size() - 1;

		m_RDFURILookup[ri.URI] = this_uri_index;
		m_RDFLabelLookup[ri.Label] = this_uri_index;

	} else {

	// Already added
		this_uri_index = m_RDFURILookup[uri];
	}

// Only add parent - child info if this uri is NOT the first (root) uri
	if (this_uri_index > 0) {
		m_RDFURIs[this_uri_index].Parents.push_back(parent);
		m_RDFURIs[parent].Children.push_back(this_uri_index);
	}

	lrdf_uris * uris = lrdf_get_subclasses(uri);

	if (uris) {
		for (unsigned long i = 0; i < uris->count; i++) {
			MetadataRDFDescend(uris->items[i], this_uri_index);
		}
	}

	lrdf_free_uris(uris);
}
#endif

bool
LADSPAInfo::CheckPlugin(const LADSPA_Descriptor *desc)
{
#define test(t, m) { \
	if (!(t)) { \
		cerr << m << endl; \
		return false; \
	} \
}
	test(desc->instantiate, "WARNING: Plugin has no instatiate function");
	test(desc->connect_port, "WARNING: Warning: Plugin has no connect_port funciton");
	test(desc->run, "WARNING: Plugin has no run function");
	test(!(desc->run_adding != 0 && desc->set_run_adding_gain == 0),
			"WARNING: Plugin has run_adding but no set_run_adding_gain");
	test(!(desc->run_adding == 0 && desc->set_run_adding_gain != 0),
			"WARNING: Plugin has set_run_adding_gain but no run_adding");
	test(desc->cleanup, "WARNING: Plugin has no cleanup function");
	test(!LADSPA_IS_INPLACE_BROKEN(desc->Properties),
			"WARNING: Plugin cannot use in place processing");
	test(desc->PortCount, "WARNING: Plugin has no ports");
        test(desc->Name, "WARNING: Plugin has no name" );

        if ( strcmp( desc->Maker, "Tom Szilagyi" ) )
        {
            /* The author of the TAP plugins has a very narrow
             * interpretation of the HARD RT hint which differs from
             * most everyone else, so we are left in the unfortunate
             * position of having to ignore the hint for TAP plugins
             * and consider them all RT safe */
            test(LADSPA_IS_HARD_RT_CAPABLE( desc->Properties ), "WARNING: Plugin is not RT capable" );
        }

	return true;
}

LADSPA_Descriptor_Function
LADSPAInfo::GetDescriptorFunctionForLibrary(unsigned long library_index)
{
	LibraryInfo *li = &(m_Libraries[library_index]);

	if (!(li->Handle)) {

	// Need full path
		string fullpath = m_Paths[li->PathIndex];
		fullpath.append(li->Basename);

	// Immediate symbol resolution, as plugin code is likely to be executed
		li->Handle = dlopen(fullpath.c_str(), RTLD_NOW);
		if (!(li->Handle)) {

		// Plugin library changed since last path scan
			cerr << "WARNING: Plugin library " << fullpath << " cannot be loaded" << endl;
			cerr << "Rescan of plugins recommended" << endl;
			cerr << "dlerror() output:" << endl;
			cerr << dlerror() << endl;
			return NULL;
		}
	}

// Got handle so now verify that it's a LADSPA plugin library
	const LADSPA_Descriptor_Function desc_func = (LADSPA_Descriptor_Function)dlsym(li->Handle,
																				"ladspa_descriptor");
	if (!desc_func) {

	// Is DLL, but not a LADSPA one (changed since last path scan?)
		cerr << "WARNING: DLL " << m_Paths[li->PathIndex] << li->Basename
			<< " has no ladspa_descriptor function" << endl;
		cerr << "Rescan of plugins recommended" << endl;
		cerr << "dlerror() output:" << endl;
		cerr << dlerror() << endl;

	// Unload library
		dlclose(li->Handle);
		return NULL;
	}

	return desc_func;
}
