VST3: Plugin discovery
This commit is contained in:
parent
84a86fa21a
commit
2a9795113b
|
@ -42,10 +42,15 @@ namespace ARDOUR {
|
|||
|
||||
class Plugin;
|
||||
|
||||
#ifdef VST3_SUPPORT
|
||||
struct VST3Info;
|
||||
#endif
|
||||
|
||||
class LIBARDOUR_API PluginManager : public boost::noncopyable {
|
||||
public:
|
||||
static PluginManager& instance();
|
||||
static std::string scanner_bin_path;
|
||||
static std::string vst3_scanner_bin_path;
|
||||
|
||||
~PluginManager ();
|
||||
|
||||
|
@ -65,6 +70,8 @@ public:
|
|||
void clear_vst_blacklist ();
|
||||
void clear_au_cache ();
|
||||
void clear_au_blacklist ();
|
||||
void clear_vst3_cache ();
|
||||
void clear_vst3_blacklist ();
|
||||
|
||||
const std::string get_default_windows_vst_path() const { return windows_vst_path; }
|
||||
const std::string get_default_lxvst_path() const { return lxvst_path; }
|
||||
|
@ -77,8 +84,8 @@ public:
|
|||
*/
|
||||
static std::string plugin_type_name (const PluginType, bool short_name = true);
|
||||
|
||||
bool cancelled () { return _cancel_scan; }
|
||||
bool no_timeout () { return _cancel_timeout; }
|
||||
bool cancelled () const { return _cancel_scan; }
|
||||
bool no_timeout () const { return _cancel_timeout; }
|
||||
|
||||
void reset_stats ();
|
||||
void stats_use_plugin (PluginInfoPtr const&);
|
||||
|
@ -228,6 +235,8 @@ private:
|
|||
void detect_name_ambiguities (ARDOUR::PluginInfoList*);
|
||||
void detect_type_ambiguities (ARDOUR::PluginInfoList&);
|
||||
|
||||
void conceal_duplicates (ARDOUR::PluginInfoList*, ARDOUR::PluginInfoList*);
|
||||
|
||||
void load_statuses ();
|
||||
void load_tags ();
|
||||
void load_stats ();
|
||||
|
@ -261,6 +270,10 @@ private:
|
|||
|
||||
int vst3_discover_from_path (std::string const& path, bool cache_only = false);
|
||||
int vst3_discover (std::string const& path, bool cache_only = false);
|
||||
#ifdef VST3_SUPPORT
|
||||
void vst3_plugin (std::string const& module_path, VST3Info const&);
|
||||
bool run_vst3_scanner_app (std::string bundle_path) const;
|
||||
#endif
|
||||
|
||||
int lxvst_discover_from_path (std::string path, bool cache_only = false);
|
||||
int lxvst_discover (std::string path, bool cache_only = false);
|
||||
|
|
|
@ -227,6 +227,7 @@ CONFIG_VARIABLE (bool, use_macvst, "use-macvst", true)
|
|||
CONFIG_VARIABLE (bool, discover_vst_on_start, "discover-vst-on-start", false)
|
||||
CONFIG_VARIABLE (bool, verbose_plugin_scan, "verbose-plugin-scan", false)
|
||||
CONFIG_VARIABLE (bool, conceal_lv1_if_lv2_exists, "conceal-lv1-if-lv2-exists", true)
|
||||
CONFIG_VARIABLE (bool, conceal_vst2_if_vst3_exists, "conceal-vst2-if-vst3-exists", true)
|
||||
CONFIG_VARIABLE (int, vst_scan_timeout, "vst-scan-timeout", 1200) /* deciseconds, per plugin, <= 0 no timeout */
|
||||
CONFIG_VARIABLE (bool, discover_audio_units, "discover-audio-units", false)
|
||||
CONFIG_VARIABLE (bool, ask_replace_instrument, "ask-replace-instrument", true)
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright (C) 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 _ardour_vst3_scan_h_
|
||||
#define _ardour_vst3_scan_h_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <boost/shared_ptr.hpp>
|
||||
|
||||
#include "pbd/xml++.h"
|
||||
|
||||
#include "ardour/libardour_visibility.h"
|
||||
|
||||
namespace ARDOUR {
|
||||
class VST3PluginModule;
|
||||
}
|
||||
|
||||
namespace ARDOUR {
|
||||
|
||||
struct VST3Info {
|
||||
VST3Info ()
|
||||
: index (0)
|
||||
, n_inputs (0)
|
||||
, n_outputs (0)
|
||||
, n_aux_inputs (0)
|
||||
, n_aux_outputs (0)
|
||||
, n_midi_inputs (0)
|
||||
, n_midi_outputs (0)
|
||||
{}
|
||||
|
||||
VST3Info (XMLNode const&);
|
||||
XMLNode& state () const;
|
||||
|
||||
int index;
|
||||
std::string uid;
|
||||
std::string name;
|
||||
std::string vendor;
|
||||
std::string category;
|
||||
std::string version;
|
||||
std::string sdk_version;
|
||||
std::string url;
|
||||
std::string email;
|
||||
|
||||
int n_inputs;
|
||||
int n_outputs;
|
||||
int n_aux_inputs;
|
||||
int n_aux_outputs;
|
||||
int n_midi_inputs;
|
||||
int n_midi_outputs;
|
||||
};
|
||||
|
||||
LIBARDOUR_API extern bool
|
||||
discover_vst3 (boost::shared_ptr<VST3PluginModule>,
|
||||
std::vector<VST3Info>&);
|
||||
|
||||
LIBARDOUR_API extern std::string
|
||||
module_path_vst3 (std::string const& path);
|
||||
|
||||
LIBARDOUR_API extern std::string
|
||||
vst3_cache_file (std::string const& module_path);
|
||||
|
||||
LIBARDOUR_API extern std::string
|
||||
vst3_valid_cache_file (std::string const& module_path, bool verbose = false);
|
||||
|
||||
LIBARDOUR_API extern bool
|
||||
vst3_scan_and_cache (std::string const& module_path, std::string const& bundle_path, boost::function<void (std::string const&, VST3Info const&)> cb);
|
||||
|
||||
} // namespace ARDOUR
|
||||
|
||||
#endif
|
|
@ -79,7 +79,6 @@
|
|||
#include <glibmm/miscutils.h>
|
||||
#include <glibmm/pattern.h>
|
||||
#include <glibmm/fileutils.h>
|
||||
#include <glibmm/miscutils.h>
|
||||
|
||||
#include "pbd/convert.h"
|
||||
#include "pbd/file_utils.h"
|
||||
|
@ -115,9 +114,10 @@
|
|||
#endif
|
||||
|
||||
#ifdef VST3_SUPPORT
|
||||
#include "pbd/basename.h"
|
||||
#include "ardour/system_exec.h"
|
||||
#include "ardour/vst3_module.h"
|
||||
#include "ardour/vst3_plugin.h"
|
||||
#include "ardour/vst3_scan.h"
|
||||
#endif
|
||||
|
||||
#include "pbd/error.h"
|
||||
|
@ -133,6 +133,7 @@ using namespace std;
|
|||
|
||||
PluginManager* PluginManager::_instance = 0;
|
||||
std::string PluginManager::scanner_bin_path = "";
|
||||
std::string PluginManager::vst3_scanner_bin_path = "";
|
||||
|
||||
PluginManager&
|
||||
PluginManager::instance()
|
||||
|
@ -158,7 +159,7 @@ PluginManager::PluginManager ()
|
|||
char* s;
|
||||
string lrdf_path;
|
||||
|
||||
#if defined WINDOWS_VST_SUPPORT || defined LXVST_SUPPORT || defined MACVST_SUPPORT
|
||||
#if defined WINDOWS_VST_SUPPORT || defined LXVST_SUPPORT || defined MACVST_SUPPORT || defined VST3_SUPPORT
|
||||
// source-tree (ardev, etc)
|
||||
PBD::Searchpath vstsp(Glib::build_filename(ARDOUR::ardour_dll_directory(), "fst"));
|
||||
|
||||
|
@ -170,6 +171,7 @@ PluginManager::PluginManager ()
|
|||
vstsp += ARDOUR::ardour_dll_directory();
|
||||
#endif
|
||||
|
||||
#if defined WINDOWS_VST_SUPPORT || defined LXVST_SUPPORT || defined MACVST_SUPPORT
|
||||
if (!PBD::find_file (vstsp,
|
||||
#ifdef PLATFORM_WINDOWS
|
||||
#ifdef DEBUGGABLE_SCANNER_APP
|
||||
|
@ -187,7 +189,20 @@ PluginManager::PluginManager ()
|
|||
, scanner_bin_path)) {
|
||||
PBD::warning << "VST scanner app (ardour-vst-scanner) not found in path " << vstsp.to_string() << endmsg;
|
||||
}
|
||||
#endif // VST2
|
||||
|
||||
#ifdef VST3_SUPPORT
|
||||
if (!PBD::find_file (vstsp,
|
||||
#ifdef PLATFORM_WINDOWS
|
||||
"ardour-vst3-scanner.exe"
|
||||
#else
|
||||
"ardour-vst3-scanner"
|
||||
#endif
|
||||
, vst3_scanner_bin_path)) {
|
||||
PBD::warning << "VST3 scanner app (ardour-vst3-scanner) not found in path " << vstsp.to_string() << endmsg;
|
||||
}
|
||||
#endif // VST3_SUPPORT
|
||||
#endif // any VST
|
||||
|
||||
load_statuses ();
|
||||
load_tags ();
|
||||
|
@ -349,6 +364,25 @@ PluginManager::detect_type_ambiguities (PluginInfoList& pil)
|
|||
}
|
||||
}
|
||||
|
||||
void
|
||||
PluginManager::conceal_duplicates (ARDOUR::PluginInfoList* old, ARDOUR::PluginInfoList* nu)
|
||||
{
|
||||
if (!old) {
|
||||
return;
|
||||
}
|
||||
for (PluginInfoList::const_iterator i = old->begin(); i != old->end(); ++i) {
|
||||
for (PluginInfoList::const_iterator j = nu->begin(); j != nu->end(); ++j) {
|
||||
if ((*i)->creator == (*j)->creator && (*i)->name == (*j)->name) {
|
||||
PluginStatus ps ((*i)->type, (*i)->unique_id, Concealed);
|
||||
if (find (statuses.begin(), statuses.end(), ps) == statuses.end()) {
|
||||
statuses.erase (ps);
|
||||
statuses.insert (ps);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
PluginManager::refresh (bool cache_only)
|
||||
{
|
||||
|
@ -369,25 +403,10 @@ PluginManager::refresh (bool cache_only)
|
|||
BootMessage (_("Scanning LV2 Plugins"));
|
||||
lv2_refresh ();
|
||||
|
||||
if (Config->get_conceal_lv1_if_lv2_exists()) {
|
||||
for (PluginInfoList::const_iterator i = _ladspa_plugin_info->begin(); i != _ladspa_plugin_info->end(); ++i) {
|
||||
for (PluginInfoList::const_iterator j = _lv2_plugin_info->begin(); j != _lv2_plugin_info->end(); ++j) {
|
||||
if ((*i)->creator == (*j)->creator && (*i)->name == (*j)->name) {
|
||||
PluginStatus ps (LADSPA, (*i)->unique_id, Concealed);
|
||||
if (find (statuses.begin(), statuses.end(), ps) == statuses.end()) {
|
||||
statuses.erase (ps);
|
||||
statuses.insert (ps);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (PluginStatusList::iterator i = statuses.begin(); i != statuses.end();) {
|
||||
PluginStatusList::iterator j = i++;
|
||||
if ((*j).status == Concealed) {
|
||||
statuses.erase (j);
|
||||
}
|
||||
}
|
||||
bool conceal_lv1 = Config->get_conceal_lv1_if_lv2_exists();
|
||||
|
||||
if (conceal_lv1) {
|
||||
conceal_duplicates (_ladspa_plugin_info, _lv2_plugin_info);
|
||||
}
|
||||
|
||||
#ifdef WINDOWS_VST_SUPPORT
|
||||
|
@ -428,20 +447,16 @@ PluginManager::refresh (bool cache_only)
|
|||
#endif //Native Mac VST SUPPORT
|
||||
|
||||
#if (defined WINDOWS_VST_SUPPORT || defined LXVST_SUPPORT || defined MACVST_SUPPORT)
|
||||
if (!cache_only) {
|
||||
string fn = Glib::build_filename (ARDOUR::user_cache_directory(), VST_BLACKLIST);
|
||||
if (Glib::file_test (fn, Glib::FILE_TEST_EXISTS)) {
|
||||
gchar *bl = NULL;
|
||||
if (g_file_get_contents(fn.c_str (), &bl, NULL, NULL)) {
|
||||
if (Config->get_verbose_plugin_scan()) {
|
||||
PBD::info << _("VST Blacklist: ") << fn << "\n" << bl << "-----" << endmsg;
|
||||
} else {
|
||||
PBD::info << _("VST Blacklist:") << "\n" << bl << "-----" << endmsg;
|
||||
}
|
||||
g_free (bl);
|
||||
}
|
||||
if (!cache_only) {
|
||||
string fn = Glib::build_filename (ARDOUR::user_cache_directory(), VST_BLACKLIST);
|
||||
if (Glib::file_test (fn, Glib::FILE_TEST_EXISTS)) {
|
||||
try {
|
||||
std::string bl = Glib::file_get_contents (fn);
|
||||
PBD::info << _("VST 2 Blacklist: ") << "\n" << bl << "-----" << endmsg;
|
||||
} catch (Glib::FileError const& err) {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef VST3_SUPPORT
|
||||
|
@ -451,6 +466,25 @@ PluginManager::refresh (bool cache_only)
|
|||
BootMessage (_("Discovering VST3 Plugins"));
|
||||
}
|
||||
vst3_refresh (cache_only);
|
||||
|
||||
if (!cache_only) {
|
||||
string fn = Glib::build_filename (ARDOUR::user_cache_directory(), "vst3_blacklist.txt");
|
||||
if (Glib::file_test (fn, Glib::FILE_TEST_EXISTS)) {
|
||||
try {
|
||||
std::string bl = Glib::file_get_contents (fn);
|
||||
PBD::info << _("VST 3 Blacklist: ") << "\n" << bl << "-----" << endmsg;
|
||||
} catch (Glib::FileError const& err) {
|
||||
}
|
||||
}
|
||||
}
|
||||
bool conceal_vst2 = Config->get_conceal_vst2_if_vst3_exists();
|
||||
if (conceal_vst2) {
|
||||
conceal_duplicates (_windows_vst_plugin_info, _vst3_plugin_info);
|
||||
conceal_duplicates (_lxvst_plugin_info, _vst3_plugin_info);
|
||||
conceal_duplicates (_mac_vst_plugin_info, _vst3_plugin_info);
|
||||
}
|
||||
#else
|
||||
bool conceal_vst2 = false;
|
||||
#endif
|
||||
|
||||
#ifdef AUDIOUNIT_SUPPORT
|
||||
|
@ -462,6 +496,22 @@ PluginManager::refresh (bool cache_only)
|
|||
au_refresh (cache_only);
|
||||
#endif
|
||||
|
||||
/* unset concealed plugins */
|
||||
if (!conceal_lv1 || !conceal_vst2) {
|
||||
for (PluginStatusList::iterator i = statuses.begin(); i != statuses.end();) {
|
||||
PluginStatusList::iterator j = i++;
|
||||
if ((*j).status != Concealed) {
|
||||
continue;
|
||||
}
|
||||
if (!conceal_lv1 && (*j).type == LADSPA) {
|
||||
statuses.erase (j);
|
||||
}
|
||||
if (!conceal_vst2 && ((*j).type == Windows_VST || (*j).type == LXVST || (*j).type == MacVST)) {
|
||||
statuses.erase (j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BootMessage (_("Plugin Scan Complete..."));
|
||||
PluginListChanged (); /* EMIT SIGNAL */
|
||||
PluginScanMessage(X_("closeme"), "", false);
|
||||
|
@ -1447,7 +1497,88 @@ PluginManager::lxvst_discover (string path, bool cache_only)
|
|||
|
||||
#endif // LXVST_SUPPORT
|
||||
|
||||
void
|
||||
PluginManager::clear_vst3_cache ()
|
||||
{
|
||||
#ifdef VST3_SUPPORT
|
||||
string dn = Glib::build_filename (ARDOUR::user_cache_directory(), "vst");
|
||||
vector<string> v3i_files;
|
||||
find_files_matching_regex (v3i_files, dn, "\\.v3i$", false);
|
||||
for (vector<string>::iterator i = v3i_files.begin(); i != v3i_files.end (); ++i) {
|
||||
::g_unlink(i->c_str());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
PluginManager::clear_vst3_blacklist ()
|
||||
{
|
||||
string fn = Glib::build_filename (ARDOUR::user_cache_directory (), "vst3_blacklist.txt");
|
||||
if (Glib::file_test (fn, Glib::FILE_TEST_EXISTS)) {
|
||||
::g_unlink(fn.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef VST3_SUPPORT
|
||||
|
||||
static void vst3_blacklist (string const& module_path)
|
||||
{
|
||||
string fn = Glib::build_filename (ARDOUR::user_cache_directory (), "vst3_blacklist.txt");
|
||||
FILE* f = NULL;
|
||||
if (! (f = g_fopen (fn.c_str (), "a"))) {
|
||||
PBD::error << string_compose (_("Cannot write to VST3 blacklist file '%1'"), fn) << endmsg;
|
||||
return;
|
||||
}
|
||||
fprintf (f, "%s\n", module_path.c_str ());
|
||||
::fclose (f);
|
||||
}
|
||||
|
||||
static void vst3_whitelist (string module_path)
|
||||
{
|
||||
string fn = Glib::build_filename (ARDOUR::user_cache_directory (), "vst3_blacklist.txt");
|
||||
if (!Glib::file_test (fn, Glib::FILE_TEST_EXISTS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string bl;
|
||||
try {
|
||||
bl = Glib::file_get_contents (fn);
|
||||
} catch (Glib::FileError const& err) {
|
||||
return;
|
||||
}
|
||||
::g_unlink (fn.c_str ());
|
||||
|
||||
module_path += "\n"; // add separator
|
||||
const size_t rpl = bl.find (module_path);
|
||||
if (rpl != string::npos) {
|
||||
bl.replace (rpl, module_path.size (), "");
|
||||
}
|
||||
if (bl.empty ()) {
|
||||
return;
|
||||
}
|
||||
Glib::file_set_contents (fn, bl);
|
||||
}
|
||||
|
||||
static bool vst3_is_blacklisted (string const& module_path)
|
||||
{
|
||||
string fn = Glib::build_filename (ARDOUR::user_cache_directory (), "vst3_blacklist.txt");
|
||||
if (!Glib::file_test (fn, Glib::FILE_TEST_EXISTS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string bl;
|
||||
try {
|
||||
bl = Glib::file_get_contents (fn);
|
||||
} catch (Glib::FileError const& err) {
|
||||
return false;
|
||||
}
|
||||
return bl.find (module_path + "\n") != string::npos;
|
||||
}
|
||||
|
||||
static bool vst3_filter (const string& str, void*)
|
||||
{
|
||||
return str[0] != '.' && (str.length() > 4 && str.find (".vst3") == (str.length() - 5));
|
||||
}
|
||||
|
||||
void
|
||||
PluginManager::vst3_refresh (bool cache_only)
|
||||
|
@ -1468,11 +1599,6 @@ PluginManager::vst3_refresh (bool cache_only)
|
|||
#endif
|
||||
}
|
||||
|
||||
static bool vst3_filter (const string& str, void*)
|
||||
{
|
||||
return str[0] != '.' && (str.length() > 4 && str.find (".vst3") == (str.length() - 5));
|
||||
}
|
||||
|
||||
int
|
||||
PluginManager::vst3_discover_from_path (string const& path, bool cache_only)
|
||||
{
|
||||
|
@ -1496,63 +1622,181 @@ PluginManager::vst3_discover_from_path (string const& path, bool cache_only)
|
|||
return cancelled() ? -1 : 0;
|
||||
}
|
||||
|
||||
static std::string vst3_bindir () {
|
||||
#ifdef __APPLE__
|
||||
return "MacOS";
|
||||
#elif defined PLATFORM_WINDOWS
|
||||
# if defined __x86_64__ || defined _M_X64
|
||||
return "x86_64-win";
|
||||
# else
|
||||
return "x86-win";
|
||||
# endif
|
||||
#else // Linux
|
||||
# if defined __x86_64__ || defined _M_X64
|
||||
return "x86_64-linux";
|
||||
# elif defined __i386__ || defined _M_IX86
|
||||
return "i386-linux";
|
||||
#endif
|
||||
// ARM, PPC ?
|
||||
#endif
|
||||
return "";
|
||||
}
|
||||
void
|
||||
PluginManager::vst3_plugin (string const& module_path, VST3Info const& i)
|
||||
{
|
||||
PluginInfoPtr info (new VST3PluginInfo ());
|
||||
|
||||
static std::string vst3_suffix () {
|
||||
#ifdef __APPLE__
|
||||
return "";
|
||||
#elif defined PLATFORM_WINDOWS
|
||||
return ".vst3";
|
||||
#else // Linux
|
||||
return ".so";
|
||||
#endif
|
||||
info->path = module_path;
|
||||
info->index = i.index;
|
||||
info->unique_id = i.uid;
|
||||
info->name = i.name;
|
||||
info->category = i.category; // TODO process split at "|" -> tags
|
||||
info->creator = i.vendor;
|
||||
info->n_inputs = ChanCount();
|
||||
info->n_outputs = ChanCount();
|
||||
|
||||
info->n_inputs.set_audio (i.n_inputs + i.n_aux_inputs);
|
||||
info->n_inputs.set_midi (i.n_midi_inputs);
|
||||
|
||||
info->n_outputs.set_audio (i.n_outputs + i.n_aux_outputs);
|
||||
info->n_outputs.set_midi (i.n_midi_outputs);
|
||||
|
||||
_vst3_plugin_info->push_back (info);
|
||||
|
||||
if (!info->category.empty ()) {
|
||||
set_tags (info->type, info->unique_id, info->category, info->name, FromPlug);
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
PluginManager::vst3_discover (string const& path, bool cache_only)
|
||||
{
|
||||
string module_path;
|
||||
if (!Glib::file_test (path, Glib::FILE_TEST_IS_DIR)) {
|
||||
module_path = path;
|
||||
} else {
|
||||
module_path = Glib::build_filename (path, "Contents",
|
||||
vst3_bindir (), PBD::basename_nosuffix (path) + vst3_suffix ());
|
||||
}
|
||||
if (!Glib::file_test (module_path, Glib::FILE_TEST_IS_REGULAR)) {
|
||||
cerr << "VST3 not a valid bundle: '" << module_path << "'\n";
|
||||
string module_path = module_path_vst3 (path);
|
||||
if (module_path.empty ()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (vst3_is_blacklisted (module_path)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
ARDOUR::PluginScanMessage(_("VST3"), module_path, !(cache_only || cancelled()));
|
||||
DEBUG_TRACE (DEBUG::PluginManager, string_compose ("VST3: discover %1 (%2)\n", path, module_path));
|
||||
|
||||
try {
|
||||
boost::shared_ptr<VST3PluginModule> m = VST3PluginModule::load (module_path);
|
||||
} catch (...) {
|
||||
DEBUG_TRACE (DEBUG::PluginManager, string_compose ("Cannot load VST3 at '%1'\n", path));
|
||||
if (!cache_only && vst3_scanner_bin_path.empty ()) {
|
||||
/* direct scan in the host's process */
|
||||
vst3_blacklist (module_path);
|
||||
|
||||
if (! vst3_scan_and_cache (module_path, path, sigc::mem_fun (*this, &PluginManager::vst3_plugin))) {
|
||||
DEBUG_TRACE (DEBUG::PluginManager, string_compose ("Cannot load VST3 at '%1'\n", path));
|
||||
return -1;
|
||||
}
|
||||
|
||||
vst3_whitelist (module_path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
string cache_file = vst3_valid_cache_file (module_path);
|
||||
|
||||
bool run_scan = false;
|
||||
|
||||
XMLTree tree;
|
||||
if (cache_file.empty ()) {
|
||||
run_scan = true;
|
||||
} else if (tree.read (cache_file)) {
|
||||
/* valid cache file was found, now check version */
|
||||
int cf_version = 0;
|
||||
if (!tree.root()->get_property ("version", cf_version) || cf_version < 1) {
|
||||
run_scan = true;
|
||||
}
|
||||
} else {
|
||||
/* failed to parse XML */
|
||||
run_scan = true;
|
||||
}
|
||||
|
||||
if (!cache_only && run_scan) {
|
||||
/* re/generate cache file */
|
||||
vst3_blacklist (module_path);
|
||||
if (!run_vst3_scanner_app (path)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cache_file = vst3_valid_cache_file (module_path);
|
||||
|
||||
if (cache_file.empty ()) {
|
||||
return -1;
|
||||
}
|
||||
/* re-read cache file */
|
||||
if (!tree.read (cache_file)) {
|
||||
error << string_compose (_("Cannot parse VST3 cache file '%1' for plugin '%2'"), cache_file, module_path) << endmsg;
|
||||
return -1;
|
||||
}
|
||||
run_scan = false; // mark as scanned
|
||||
}
|
||||
|
||||
if (cache_file.empty () || run_scan) {
|
||||
/* cache file does not exist and cache_only == true,
|
||||
* or cache file is invalid (scan needed)
|
||||
*/
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::string module;
|
||||
if (!tree.root()->get_property ("module", module) || module != module_path) {
|
||||
error << string_compose (_("Invalid VST3 cache file '%1' for plugin '%2'"), cache_file, module_path) << endmsg;
|
||||
return -1;
|
||||
}
|
||||
|
||||
vst3_whitelist (module_path);
|
||||
|
||||
for (XMLNodeConstIterator i = tree.root()->children().begin(); i != tree.root()->children().end(); ++i) {
|
||||
try {
|
||||
VST3Info nfo (**i);
|
||||
vst3_plugin (module_path, nfo);
|
||||
} catch (...) {
|
||||
error << string_compose (_("Corrupt VST3 cache file '%1' for plugin '%2'"), cache_file, module_path) << endmsg;
|
||||
DEBUG_TRACE (DEBUG::PluginManager, string_compose ("Cannot load VST3 at '%1'\n", path));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void vst3_scanner_log (std::string msg, std::string bundle_path)
|
||||
{
|
||||
PBD::error << string_compose ("VST<%1> scanner: %2", bundle_path, msg) << endmsg;
|
||||
}
|
||||
|
||||
bool
|
||||
PluginManager::run_vst3_scanner_app (std::string bundle_path) const
|
||||
{
|
||||
char **argp= (char**) calloc (5, sizeof (char*));
|
||||
argp[0] = strdup (vst3_scanner_bin_path.c_str ());
|
||||
argp[1] = strdup ("-q");
|
||||
argp[2] = strdup ("-f");
|
||||
argp[3] = strdup (bundle_path.c_str ());
|
||||
argp[4] = 0;
|
||||
|
||||
ARDOUR::SystemExec scanner (vst3_scanner_bin_path, argp);
|
||||
PBD::ScopedConnection c;
|
||||
scanner.ReadStdout.connect_same_thread (c, boost::bind (&vst3_scanner_log, _1, bundle_path));
|
||||
|
||||
if (scanner.start (ARDOUR::SystemExec::MergeWithStdin)) {
|
||||
PBD::error << string_compose (_("Cannot launch VST scanner app '%1': %2"), scanner_bin_path, strerror (errno)) << endmsg;
|
||||
return false;
|
||||
}
|
||||
|
||||
int timeout = Config->get_vst_scan_timeout(); // deciseconds
|
||||
bool notime = (timeout <= 0);
|
||||
|
||||
ARDOUR::PluginScanTimeout (timeout);
|
||||
while (scanner.is_running () && (notime || timeout > 0)) {
|
||||
|
||||
if (!notime && !no_timeout ()) {
|
||||
if (timeout % 5 == 0) {
|
||||
ARDOUR::PluginScanTimeout (timeout);
|
||||
}
|
||||
--timeout;
|
||||
}
|
||||
|
||||
ARDOUR::GUIIdle ();
|
||||
Glib::usleep (100000);
|
||||
|
||||
if (cancelled () || (!notime && timeout == 0)) {
|
||||
scanner.terminate ();
|
||||
/* may be partially written */
|
||||
std::string module_path = module_path_vst3 (bundle_path);
|
||||
if (!module_path.empty ()) {
|
||||
g_unlink (vst3_cache_file (module_path).c_str ());
|
||||
}
|
||||
vst3_whitelist (module_path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // VST3_SUPPORT
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,414 @@
|
|||
/*
|
||||
* Copyright (C) 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.
|
||||
*/
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "pbd/gstdio_compat.h"
|
||||
#include <glibmm/fileutils.h>
|
||||
#include <glibmm/miscutils.h>
|
||||
|
||||
#ifdef COMPILER_MSVC
|
||||
#include <sys/utime.h>
|
||||
#else
|
||||
#include <utime.h>
|
||||
#endif
|
||||
|
||||
#include "vst3/vst3.h"
|
||||
|
||||
#include "pbd/basename.h"
|
||||
#include "pbd/error.h"
|
||||
#include "pbd/failed_constructor.h"
|
||||
|
||||
#include "ardour/filesystem_paths.h"
|
||||
#include "ardour/vst3_module.h"
|
||||
#include "ardour/vst3_host.h"
|
||||
#include "ardour/vst3_scan.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace Steinberg;
|
||||
|
||||
static int32
|
||||
count_channels (Vst::IComponent* c, Vst::MediaType media, Vst::BusDirection dir, Vst::BusType type)
|
||||
{
|
||||
/* see also libs/ardour/vst3_plugin.cc VST3PI::count_channels */
|
||||
int32 n_busses = c->getBusCount (media, dir);
|
||||
int32 n_channels = 0;
|
||||
for (int32 i = 0; i < n_busses; ++i) {
|
||||
Vst::BusInfo bus;
|
||||
if (c->getBusInfo (media, dir, i, bus) == kResultTrue && bus.busType == type) {
|
||||
#if 1
|
||||
if ((type == Vst::kMain && i != 0) || (type == Vst::kAux && i != 1)) {
|
||||
/* For now allow we only support one main bus, and one aux-bus.
|
||||
* Also an aux-bus by itself is currently N/A.
|
||||
*/
|
||||
std::cerr << "VST3: Ignored extra bus\n";
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
if (media == Vst::kEvent) {
|
||||
/* MIDI Channel count -> MIDI input */
|
||||
return std::min<int32> (1, bus.channelCount);
|
||||
} else {
|
||||
n_channels += bus.channelCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
return n_channels;
|
||||
}
|
||||
|
||||
bool
|
||||
ARDOUR::discover_vst3 (boost::shared_ptr<VST3PluginModule> m, std::vector<VST3Info>& rv)
|
||||
{
|
||||
using namespace std;
|
||||
|
||||
GetFactoryProc fp = (GetFactoryProc)m->fn_ptr ("GetPluginFactory");
|
||||
|
||||
if (!fp) {
|
||||
cerr << "Failed to find 'GetPluginFactory' function\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
IPluginFactory* factory = fp ();
|
||||
|
||||
if (!factory) {
|
||||
cerr << "Failed to get VST3 plug-in factory\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
PFactoryInfo fi;
|
||||
if (factory->getFactoryInfo (&fi) != kResultTrue) {
|
||||
cerr << "Failed to get VST3 factory info\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
//cout << "FactoryInfo: '" << fi.vendor << "' '" << fi.url << "' '" << fi.email << "'" << "\n";
|
||||
|
||||
IPluginFactory2* factory2 = FUnknownPtr<IPluginFactory2> (factory);
|
||||
|
||||
int32 class_cnt = factory->countClasses ();
|
||||
for (int32 i = 0; i < class_cnt; ++i) {
|
||||
PClassInfo ci;
|
||||
if (factory->getClassInfo (i, &ci) == kResultTrue) {
|
||||
if (strcmp (ci.category, kVstAudioEffectClass)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
VST3Info nfo;
|
||||
TUID uid;
|
||||
|
||||
//cout << "FOUND: " << i << " '" << ci.name << "' '" << ci.category << "'" << "\n";
|
||||
|
||||
/* pre-fill with factory settings */
|
||||
nfo.vendor = strlen (fi.vendor) == 0 ? "Unknown" : fi.vendor;
|
||||
nfo.url = fi.url;
|
||||
nfo.email = fi.email;
|
||||
|
||||
PClassInfo2 ci2;
|
||||
if (factory2 && factory2->getClassInfo2 (i, &ci2) == kResultTrue) {
|
||||
memcpy (uid, ci2.cid, sizeof (TUID));
|
||||
nfo.name = ci2.name;
|
||||
if (strlen (ci2.vendor) > 0) {
|
||||
nfo.vendor = ci2.vendor;
|
||||
}
|
||||
nfo.category = ci2.subCategories;
|
||||
nfo.version = ci2.version;
|
||||
nfo.sdk_version = ci2.sdkVersion;
|
||||
} else {
|
||||
memcpy (uid, ci.cid, sizeof (TUID));
|
||||
nfo.name = ci.name;
|
||||
nfo.version = "0.0.0";
|
||||
nfo.sdk_version = "VST 3";
|
||||
}
|
||||
|
||||
nfo.index = i;
|
||||
{
|
||||
char suid[33] = "";
|
||||
FUID::fromTUID (uid).toString (suid);
|
||||
nfo.uid = suid;
|
||||
}
|
||||
|
||||
Vst::IComponent* component;
|
||||
if (factory->createInstance (uid, Vst::IComponent::iid, (void**)&component) != kResultTrue) {
|
||||
cerr << "Failed to create VST3 component\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO init params
|
||||
|
||||
if (component->initialize (HostApplication::getHostContext ()) != kResultOk) {
|
||||
cerr << "Failed to initialize VST3 component\n";
|
||||
//component->terminate();
|
||||
continue;
|
||||
}
|
||||
|
||||
FUnknownPtr<Vst::IAudioProcessor> processor;
|
||||
if (!(processor = FUnknownPtr<Vst::IAudioProcessor> (component))) {
|
||||
cerr << "VST3: No valid processor";
|
||||
//controller->terminate();
|
||||
component->terminate ();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (processor->canProcessSampleSize (Vst::kSample32) != kResultTrue) {
|
||||
cerr << "VST3: Cannot process 32bit float";
|
||||
component->terminate ();
|
||||
continue;
|
||||
}
|
||||
|
||||
nfo.n_inputs = count_channels (component, Vst::kAudio, Vst::kInput, Vst::kMain);
|
||||
nfo.n_aux_inputs = count_channels (component, Vst::kAudio, Vst::kInput, Vst::kAux);
|
||||
nfo.n_outputs = count_channels (component, Vst::kAudio, Vst::kOutput, Vst::kMain);
|
||||
nfo.n_aux_outputs = count_channels (component, Vst::kAudio, Vst::kOutput, Vst::kAux);
|
||||
nfo.n_midi_inputs = count_channels (component, Vst::kEvent, Vst::kInput, Vst::kMain);
|
||||
nfo.n_midi_outputs = count_channels (component, Vst::kEvent, Vst::kOutput, Vst::kMain);
|
||||
|
||||
//controller->terminate();
|
||||
component->terminate ();
|
||||
rv.push_back (nfo);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::string vst3_bindir () {
|
||||
#ifdef __APPLE__
|
||||
return "MacOS";
|
||||
#elif defined PLATFORM_WINDOWS
|
||||
# if defined __x86_64__ || defined _M_X64
|
||||
return "x86_64-win";
|
||||
# else
|
||||
return "x86-win";
|
||||
# endif
|
||||
#else // Linux
|
||||
# if defined __x86_64__ || defined _M_X64
|
||||
return "x86_64-linux";
|
||||
# elif defined __i386__ || defined _M_IX86
|
||||
return "i386-linux";
|
||||
# elif defined __aarch64__
|
||||
return "aarch64-linux";
|
||||
# elif defined __arm__ && defined ARM_NEON_SUPPORT
|
||||
return "armv7hl-linux";
|
||||
# elif defined __arm__
|
||||
return "armv7l-linux";
|
||||
#else
|
||||
// other ARM, linux-PPC ?
|
||||
return "*-linux"; // XXX
|
||||
#endif
|
||||
#endif
|
||||
return "";
|
||||
}
|
||||
|
||||
static std::string vst3_suffix () {
|
||||
#ifdef __APPLE__
|
||||
return "";
|
||||
#elif defined PLATFORM_WINDOWS
|
||||
return ".vst3";
|
||||
#else // Linux
|
||||
return ".so";
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string
|
||||
ARDOUR::module_path_vst3 (string const& path)
|
||||
{
|
||||
string module_path;
|
||||
|
||||
if (!Glib::file_test (path, Glib::FILE_TEST_IS_DIR)) {
|
||||
module_path = path;
|
||||
} else {
|
||||
module_path = Glib::build_filename (path, "Contents",
|
||||
vst3_bindir (), PBD::basename_nosuffix (path) + vst3_suffix ());
|
||||
}
|
||||
if (!Glib::file_test (module_path, Glib::FILE_TEST_IS_REGULAR)) {
|
||||
cerr << "VST3 not a valid bundle: '" << module_path << "'\n";
|
||||
return "";
|
||||
}
|
||||
return module_path;
|
||||
}
|
||||
|
||||
static string
|
||||
vst3_info_cache_dir ()
|
||||
{
|
||||
string dir = Glib::build_filename (ARDOUR::user_cache_directory (), "vst");
|
||||
/* if the directory doesn't exist, try to create it */
|
||||
if (!Glib::file_test (dir, Glib::FILE_TEST_IS_DIR)) {
|
||||
if (g_mkdir (dir.c_str (), 0700)) {
|
||||
PBD::fatal << "Cannot create VST info folder '" << dir << "'" << endmsg;
|
||||
}
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
#include "sha1.c"
|
||||
|
||||
string
|
||||
ARDOUR::vst3_cache_file (std::string const& module_path)
|
||||
{
|
||||
char hash[41];
|
||||
Sha1Digest s;
|
||||
sha1_init (&s);
|
||||
sha1_write (&s, (const uint8_t *) module_path.c_str(), module_path.size());
|
||||
sha1_result_hash (&s, hash);
|
||||
return Glib::build_filename (vst3_info_cache_dir (), std::string (hash) + std::string (".v3i"));
|
||||
}
|
||||
|
||||
string
|
||||
ARDOUR::vst3_valid_cache_file (std::string const& module_path, bool verbose)
|
||||
{
|
||||
string const cache_file = ARDOUR::vst3_cache_file (module_path);
|
||||
if (!Glib::file_test (cache_file, Glib::FileTest (Glib::FILE_TEST_EXISTS | Glib::FILE_TEST_IS_REGULAR))) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
PBD::info << "Found cache file: '" << cache_file <<"'" << endmsg;
|
||||
}
|
||||
|
||||
GStatBuf sb_vst;
|
||||
GStatBuf sb_v3i;
|
||||
|
||||
if (g_stat (module_path.c_str(), &sb_vst) == 0 && g_stat (cache_file.c_str (), &sb_v3i) == 0) {
|
||||
if (sb_vst.st_mtime < sb_v3i.st_mtime) {
|
||||
/* plugin is older than cache file */
|
||||
if (verbose) {
|
||||
PBD::info << "Cache file is up-to-date." << endmsg;
|
||||
}
|
||||
return cache_file;
|
||||
} else if (verbose) {
|
||||
PBD::info << "Stale cache." << endmsg;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
static void
|
||||
touch_cachefile (std::string const& module_path, std::string const& cache_file)
|
||||
{
|
||||
GStatBuf sb_vst;
|
||||
GStatBuf sb_v3i;
|
||||
if (g_stat (module_path.c_str(), &sb_vst) == 0 && g_stat (cache_file.c_str (), &sb_v3i) == 0) {
|
||||
struct utimbuf utb;
|
||||
utb.actime = sb_v3i.st_atime;
|
||||
utb.modtime = std::max (sb_vst.st_mtime, sb_v3i.st_mtime);
|
||||
g_utime (cache_file.c_str (), &utb);
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
vst3_save_cache_file (std::string const& module_path, XMLNode* root)
|
||||
{
|
||||
string const cache_file = ARDOUR::vst3_cache_file (module_path);
|
||||
|
||||
XMLTree tree;
|
||||
tree.set_root (root);
|
||||
if (!tree.write (cache_file)) {
|
||||
PBD::error << "Could not save VST3 plugin cache to: " << cache_file << endmsg;
|
||||
return false;
|
||||
} else {
|
||||
touch_cachefile (module_path, cache_file);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
ARDOUR::vst3_scan_and_cache (std::string const& module_path, std::string const& bundle_path, boost::function<void (std::string const&, VST3Info const&)> cb)
|
||||
{
|
||||
XMLNode* root = new XMLNode ("VST3Cache");
|
||||
root->set_property ("version", 1);
|
||||
root->set_property ("bundle", bundle_path);
|
||||
root->set_property ("module", module_path);
|
||||
|
||||
try {
|
||||
boost::shared_ptr<VST3PluginModule> m = VST3PluginModule::load (module_path);
|
||||
std::vector<VST3Info> nfo;
|
||||
discover_vst3 (m, nfo);
|
||||
for (std::vector<VST3Info>::const_iterator i = nfo.begin(); i != nfo.end(); ++i) {
|
||||
cb (module_path, *i);
|
||||
root->add_child_nocopy (i->state ());
|
||||
}
|
||||
|
||||
} catch (...) {
|
||||
cerr << "Cannot load VST3 module: '" << module_path << "'\n";
|
||||
delete root;
|
||||
return false;
|
||||
}
|
||||
|
||||
return vst3_save_cache_file (module_path, root);
|
||||
}
|
||||
|
||||
|
||||
using namespace ARDOUR;
|
||||
|
||||
VST3Info::VST3Info (XMLNode const& node)
|
||||
: index (0)
|
||||
, n_inputs (0)
|
||||
, n_outputs (0)
|
||||
, n_aux_inputs (0)
|
||||
, n_aux_outputs (0)
|
||||
, n_midi_inputs (0)
|
||||
, n_midi_outputs (0)
|
||||
{
|
||||
bool err = false;
|
||||
|
||||
if (node.name() != "VST3Info") {
|
||||
throw failed_constructor ();
|
||||
}
|
||||
err |= !node.get_property ("index", index);
|
||||
err |= !node.get_property ("uid", uid);
|
||||
err |= !node.get_property ("name", name);
|
||||
err |= !node.get_property ("vendor", vendor);
|
||||
err |= !node.get_property ("category", category);
|
||||
err |= !node.get_property ("version", version);
|
||||
err |= !node.get_property ("sdk-version", sdk_version);
|
||||
err |= !node.get_property ("url", url);
|
||||
err |= !node.get_property ("email", email);
|
||||
|
||||
err |= !node.get_property ("n_inputs", n_inputs);
|
||||
err |= !node.get_property ("n_outputs", n_outputs);
|
||||
err |= !node.get_property ("n_aux_inputs", n_aux_inputs);
|
||||
err |= !node.get_property ("n_aux_outputs", n_aux_outputs);
|
||||
err |= !node.get_property ("n_midi_inputs", n_midi_inputs);
|
||||
err |= !node.get_property ("n_midi_outputs", n_midi_outputs);
|
||||
|
||||
if (err) {
|
||||
throw failed_constructor ();
|
||||
}
|
||||
}
|
||||
|
||||
XMLNode&
|
||||
VST3Info::state () const
|
||||
{
|
||||
XMLNode* node = new XMLNode("VST3Info");
|
||||
node->set_property ("index", index);
|
||||
node->set_property ("uid", uid);
|
||||
node->set_property ("name", name);
|
||||
node->set_property ("vendor", vendor);
|
||||
node->set_property ("category", category);
|
||||
node->set_property ("version", version);
|
||||
node->set_property ("sdk-version", sdk_version);
|
||||
node->set_property ("url", url);
|
||||
node->set_property ("email", email);
|
||||
|
||||
node->set_property ("n_inputs", n_inputs);
|
||||
node->set_property ("n_outputs", n_outputs);
|
||||
node->set_property ("n_aux_inputs", n_aux_inputs);
|
||||
node->set_property ("n_aux_outputs", n_aux_outputs);
|
||||
node->set_property ("n_midi_inputs", n_midi_inputs);
|
||||
node->set_property ("n_midi_outputs", n_midi_outputs);
|
||||
return *node;
|
||||
}
|
|
@ -460,7 +460,7 @@ def build(bld):
|
|||
obj.defines += [ 'MACVST_SUPPORT' ]
|
||||
|
||||
if bld.is_defined('VST3_SUPPORT'):
|
||||
obj.source += [ 'vst3_plugin.cc', 'vst3_module.cc', 'vst3_host.cc' ]
|
||||
obj.source += [ 'vst3_plugin.cc', 'vst3_module.cc', 'vst3_host.cc', 'vst3_scan.cc' ]
|
||||
obj.defines += [ 'VST3_SUPPORT' ]
|
||||
|
||||
if bld.is_defined('HAVE_COREAUDIO'):
|
||||
|
|
Loading…
Reference in New Issue