/* * Copyright (C) 2021 Robin Gareus * * 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 #include #include #include #include #include "pbd/gstdio_compat.h" #include "CAAudioUnit.h" #include "CAAUParameter.h" #include #include #include #include #ifdef WITH_CARBON #include #endif #ifdef COREAUDIO105 #define ArdourComponent Component #define ArdourDescription ComponentDescription #define ArdourFindNext FindNextComponent #else #define ArdourComponent AudioComponent #define ArdourDescription AudioComponentDescription #define ArdourFindNext AudioComponentFindNext #endif #include #include #include "pbd/basename.h" #include "pbd/error.h" #include "pbd/failed_constructor.h" #include "pbd/whitespace.h" #include "ardour/filesystem_paths.h" #include "ardour/auv2_scan.h" #include "pbd/i18n.h" using namespace std; using namespace PBD; void ARDOUR::auv2_blacklist (std::string const& id) { string fn = Glib::build_filename (ARDOUR::user_cache_directory(), "au_blacklist.txt"); FILE* f = NULL; if (! (f = g_fopen (fn.c_str (), "a"))) { error << "Cannot append to AU blacklist for '" << id <<"'\n"; return; } assert (id.find ("\n") == string::npos); fprintf (f, "%s\n", id.c_str ()); ::fclose (f); } void ARDOUR::auv2_whitelist (std::string id) { string fn = Glib::build_filename (ARDOUR::user_cache_directory(), "au_blacklist.txt"); if (!Glib::file_test (fn, Glib::FILE_TEST_EXISTS)) { warning << _("Expected AUv2 Blacklist file does not exist.") << endmsg; return; } std::string bl; try { bl = Glib::file_get_contents (fn); } catch (Glib::FileError const& err) { return; } ::g_unlink (fn.c_str ()); assert (id.find("\n") == string::npos); id += "\n"; // add separator const size_t rpl = bl.find (id); if (rpl != string::npos) { bl.replace (rpl, id.size (), ""); } if (bl.empty ()) { return; } Glib::file_set_contents (fn, bl); } bool ARDOUR::auv2_is_blacklisted (std::string const& id) { string fn = Glib::build_filename (ARDOUR::user_cache_directory(), "au_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 (id + "\n") != string::npos; } /* ****************************************************************************/ ARDOUR::AUv2DescStr::AUv2DescStr (std::string const& desc) { if (desc.empty ()) { return; } if (desc.size () == 14 && desc[4] == '-' && desc[9] == '-') { type = desc.substr (0,4); subt = desc.substr (5,4); manu = desc.substr (10,4); } if (!valid ()) { type = ""; subt = ""; manu = ""; } } std::string ARDOUR::AUv2DescStr::to_s () const { return type + "-" + subt + "-" + manu; } bool ARDOUR::AUv2DescStr::valid () const { return type.size () == 4 && subt.size() == 4 && manu.size() == 4; } CAComponentDescription ARDOUR::AUv2DescStr::desc () const { CFStringRef s_type = CFStringCreateWithCString (kCFAllocatorDefault, type.c_str(), kCFStringEncodingUTF8); CFStringRef s_subt = CFStringCreateWithCString (kCFAllocatorDefault, subt.c_str(), kCFStringEncodingUTF8); CFStringRef s_manu = CFStringCreateWithCString (kCFAllocatorDefault, manu.c_str(), kCFStringEncodingUTF8); OSType t = UTGetOSTypeFromString (s_type); OSType s = UTGetOSTypeFromString (s_subt); OSType m = UTGetOSTypeFromString (s_manu); CAComponentDescription desc (t, s, m); CFRelease (s_type); CFRelease (s_subt); CFRelease (s_manu); return desc; } /* ****************************************************************************/ /* copied from ardour/utils.cc */ static string CFStringRefToStdString(CFStringRef stringRef) { CFIndex size = CFStringGetMaximumSizeForEncoding(CFStringGetLength(stringRef) , kCFStringEncodingUTF8); char *buf = new char[size]; std::string result; if(CFStringGetCString(stringRef, buf, size, kCFStringEncodingUTF8)) { result = buf; } delete [] buf; return result; } std::string ARDOUR::auv2_stringify_descriptor (CAComponentDescription const& desc) { CFStringRef s_type = UTCreateStringForOSType (desc.Type ()); CFStringRef s_subt = UTCreateStringForOSType (desc.SubType ()); CFStringRef s_manu = UTCreateStringForOSType (desc.Manu ()); ARDOUR::AUv2DescStr foo; if (s_type && s_subt && s_manu) { char tmp[5]; CFStringGetCString (s_type, tmp, 5, kCFStringEncodingUTF8); foo.type = tmp; CFStringGetCString (s_subt, tmp, 5, kCFStringEncodingUTF8); foo.subt = tmp; CFStringGetCString (s_manu, tmp, 5, kCFStringEncodingUTF8); foo.manu = tmp; } else { assert (0); } CFRelease (s_type); CFRelease (s_subt); CFRelease (s_manu); return foo.to_s (); } static void #ifdef COREAUDIO105 get_names (CAComponentDescription& comp_desc, std::string& name, std::string& maker) #else get_names (ArdourComponent& comp, std::string& name, std::string& maker) #endif { CFStringRef itemName = NULL; // Marc Poirier-style item name #ifdef COREAUDIO105 CAComponent auComponent (comp_desc); if (auComponent.IsValid()) { CAComponentDescription dummydesc; Handle nameHandle = NewHandle(sizeof(void*)); if (nameHandle != NULL) { OSErr err = GetComponentInfo(auComponent.Comp(), &dummydesc, nameHandle, NULL, NULL); if (err == noErr) { ConstStr255Param nameString = (ConstStr255Param) (*nameHandle); if (nameString != NULL) { itemName = CFStringCreateWithPascalString(kCFAllocatorDefault, nameString, CFStringGetSystemEncoding()); } } DisposeHandle(nameHandle); } } #else assert (comp); AudioComponentCopyName (comp, &itemName); #endif // if Marc-style fails, do the original way if (itemName == NULL) { #ifndef COREAUDIO105 CAComponentDescription comp_desc; AudioComponentGetDescription (comp, &comp_desc); #endif CFStringRef compTypeString = UTCreateStringForOSType(comp_desc.componentType); CFStringRef compSubTypeString = UTCreateStringForOSType(comp_desc.componentSubType); CFStringRef compManufacturerString = UTCreateStringForOSType(comp_desc.componentManufacturer); itemName = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("%@ - %@ - %@"), compTypeString, compManufacturerString, compSubTypeString); if (compTypeString != NULL) CFRelease(compTypeString); if (compSubTypeString != NULL) CFRelease(compSubTypeString); if (compManufacturerString != NULL) CFRelease(compManufacturerString); } string str = CFStringRefToStdString(itemName); string::size_type colon = str.find (':'); if (colon) { name = str.substr (colon+1); maker = str.substr (0, colon); strip_whitespace_edges (maker); strip_whitespace_edges (name); } else { name = str; maker = "unknown"; strip_whitespace_edges (name); } #if 0 if (itemName != NULL) { CFRelease(itemName); } #endif } static void auv2_plugin_info (ArdourComponent& comp, CAComponentDescription& desc, std::vector& rv, bool verbose) { ARDOUR::AUv2Info info; switch (desc.Type()) { case kAudioUnitType_Panner: case kAudioUnitType_OfflineEffect: case kAudioUnitType_FormatConverter: // return; case kAudioUnitType_Output: info.category = _("Output"); break; case kAudioUnitType_MusicDevice: info.category = _("Instrument"); info.n_midi_inputs = 1; break; case kAudioUnitType_MusicEffect: info.category = _("Effect"); info.n_midi_inputs = 1; break; case kAudioUnitType_Effect: info.category = _("Effect"); break; case kAudioUnitType_Mixer: info.category = _("Mixer"); break; case kAudioUnitType_Generator: info.category = _("Generator"); break; default: info.category = _("(Unknown)"); break; } info.id = ARDOUR::auv2_stringify_descriptor (desc); #ifdef COREAUDIO105 get_names (desc, info.name, info.creator); #else get_names (comp, info.name, info.creator); #endif CAComponent cacomp (desc); UInt32 version; #ifdef COREAUDIO105 if (cacomp.GetResourceVersion (version) != noErr) #else if (cacomp.GetVersion (version) != noErr) #endif { info.version = 0; } info.version = version; info.max_outputs = 0; /// DEBUG_TRACE (DEBUG::AudioUnitConfig, "get AU channel info\n"); CAAudioUnit unit; AUChannelInfo* channel_info; UInt32 cnt; int ret; try { if (CAAudioUnit::Open (cacomp, unit) != noErr) { return; } } catch (...) { warning << string_compose (_("Could not load AU plugin %1 - ignored"), info.name) << endmsg; return; } if ((ret = unit.GetChannelInfo (&channel_info, cnt)) < 0) { return; } if (ret > 0) { /* AU is expected to deal with same channel valance in and out */ info.io_configs.push_back (pair (-1, -1)); } else { /* CAAudioUnit::GetChannelInfo silently merges bus formats * check if this was the case and if so, add * bus configs as incremental options. */ Boolean* isWritable = 0; UInt32 dataSize = 0; OSStatus result = AudioUnitGetPropertyInfo (unit.AU(), kAudioUnitProperty_SupportedNumChannels, kAudioUnitScope_Global, 0, &dataSize, isWritable); if (result != noErr && (cacomp.Desc().IsGenerator() || cacomp.Desc().IsMusicDevice())) { /* incrementally add busses */ int in = 0; int out = 0; for (uint32_t n = 0; n < cnt; ++n) { in += channel_info[n].inChannels; out += channel_info[n].outChannels; info.io_configs.push_back (pair (in, out)); } } else { /* store each configuration */ for (uint32_t n = 0; n < cnt; ++n) { info.io_configs.push_back (pair (channel_info[n].inChannels, channel_info[n].outChannels)); } } free (channel_info); } /* here we have to map apple's wildcard system to a simple pair * of values. in ::can_do() we use the whole system, but here * we need a single pair of values. XXX probably means we should * remove any use of these values. * * for now, if the plugin provides a wildcard, treat it as 1. we really * don't care much, because whether we can handle an i/o configuration * depends upon ::configure_variable_io(), not these counts. * * they exist because other parts of ardour try to present i/o configuration * info to the user, which should perhaps be revisited. */ const vector >& ioc (info.io_configs); for (vector >::const_iterator i = ioc.begin(); i != ioc.end(); ++i) { int32_t possible_out = i->second; if (possible_out < 0) { continue; } else if (possible_out > info.max_outputs) { info.max_outputs = possible_out; } } int32_t possible_in = ioc.front().first; int32_t possible_out = ioc.front().second; if (possible_in > 0) { info.n_inputs = possible_in; } else { info.n_inputs = 1; } if (possible_out > 0) { info.n_outputs = possible_out; } else { info.n_outputs = 1; } //DEBUG_TRACE (DEBUG::AudioUnitConfig, string_compose ("detected AU %1 with %2 i/o configurations - %3\n", info->name.c_str(), info->cache.io_configs.size(), info->unique_id)); rv.push_back (info); } /* ****************************************************************************/ static bool discover_auv2 (CAComponentDescription& desc, std::vector& rv, bool verbose) { ArdourComponent comp = ArdourFindNext (NULL, &desc); if (comp == NULL) { error << ("AU was not found.") << endmsg; return false; } while (comp != NULL) { CAComponentDescription temp; #ifdef COREAUDIO105 GetComponentInfo (comp, &temp, NULL, NULL, NULL); #else AudioComponentGetDescription (comp, &temp); #endif info << ("Component loaded") << endmsg; assert (temp.componentType == desc.componentType); assert (temp.componentSubType == desc.componentSubType); assert (temp.componentManufacturer == desc.componentManufacturer); auv2_plugin_info (comp, desc, rv, verbose); comp = ArdourFindNext (comp, &desc); assert (comp == NULL); } return true; } static string auv2_info_cache_dir () { string dir = Glib::build_filename (ARDOUR::user_cache_directory (), "auv2"); /* 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 AudioUnit cache folder '" << dir << "'" << endmsg; } } return dir; } #include "sha1.c" string ARDOUR::auv2_cache_file (CAComponentDescription const& desc) { std::string id = auv2_stringify_descriptor (desc); char hash[41]; Sha1Digest s; sha1_init (&s); sha1_write (&s, (const uint8_t *) id.c_str(), id.size()); sha1_result_hash (&s, hash); return Glib::build_filename (auv2_info_cache_dir (), std::string (hash) + std::string (".a2i")); } string ARDOUR::auv2_valid_cache_file (CAComponentDescription const& desc, bool verbose, bool* is_new) { string const cache_file = ARDOUR::auv2_cache_file (desc); if (!Glib::file_test (cache_file, Glib::FileTest (Glib::FILE_TEST_EXISTS | Glib::FILE_TEST_IS_REGULAR))) { if (is_new) { *is_new = true; } return ""; } if (is_new) { *is_new = false; } if (verbose) { info << "Found cache file: '" << cache_file <<"'" << endmsg; } // TODO check version return cache_file; } static bool auv2_save_cache_file (CAComponentDescription& desc, XMLNode* root, bool verbose) { string const cache_file = ARDOUR::auv2_cache_file (desc); XMLTree tree; tree.set_root (root); if (!tree.write (cache_file)) { error << "Could not save AUv2 plugin cache to: " << cache_file << endmsg; return false; } if (verbose) { root->dump (std::cout, "\t"); } return true; } bool ARDOUR::auv2_scan_and_cache (CAComponentDescription& desc, boost::function cb, bool verbose) { XMLNode* root = new XMLNode ("AUv2Cache"); root->set_property ("version", 2); try { std::vector nfo; if (!discover_auv2 (desc, nfo, verbose)) { delete root; return false; } if (nfo.empty ()) { cerr << "No plugins matching ID: '" << auv2_stringify_descriptor (desc) << "'\n"; delete root; return false; } for (std::vector::const_iterator i = nfo.begin(); i != nfo.end(); ++i) { cb (desc, *i); root->add_child_nocopy (i->state ()); } } catch (...) { cerr << "Cannot load AudioUnit plugin: '" << auv2_stringify_descriptor (desc) << "'\n"; delete root; return false; } return auv2_save_cache_file (desc, root, verbose); } /* ****************************************************************************/ static void index_components (std::vector& rv, CAComponentDescription &desc) { ArdourComponent comp = 0; do { CAComponentDescription temp; comp = ArdourFindNext (comp, &desc); if (!comp) { break; } #ifdef COREAUDIO105 GetComponentInfo (comp, &temp, NULL, NULL, NULL); #else AudioComponentGetDescription (comp, &temp); #endif CAComponent cacomp (desc); UInt32 version; #ifdef COREAUDIO105 if (cacomp.GetResourceVersion (version) != noErr) #else if (cacomp.GetVersion (version) != noErr) #endif { version = 0; //continue; } switch (temp.Type()) { case kAudioUnitType_Panner: case kAudioUnitType_OfflineEffect: case kAudioUnitType_FormatConverter: continue; default: break; } CFStringRef s_type = UTCreateStringForOSType (temp.componentType); CFStringRef s_subt = UTCreateStringForOSType (temp.componentSubType); CFStringRef s_manu = UTCreateStringForOSType (temp.componentManufacturer); if (s_type && s_subt && s_manu) { ARDOUR::AUv2DescStr foo; char tmp[5]; CFStringGetCString (s_type, tmp, 5, kCFStringEncodingUTF8); foo.type = tmp; CFStringGetCString (s_subt, tmp, 5, kCFStringEncodingUTF8); foo.subt = tmp; CFStringGetCString (s_manu, tmp, 5, kCFStringEncodingUTF8); foo.manu = tmp; // TODO add version rv.push_back (foo); } CFRelease (s_type); CFRelease (s_subt); CFRelease (s_manu); } while (true); } void ARDOUR::auv2_list_plugins (std::vector& rv) { CAComponentDescription desc; desc.componentFlags = 0; desc.componentFlagsMask = 0; desc.componentSubType = 0; desc.componentManufacturer = 0; desc.componentType = kAudioUnitType_Effect; index_components (rv, desc); desc.componentType = kAudioUnitType_MusicEffect; index_components (rv, desc); desc.componentType = kAudioUnitType_Generator; index_components (rv, desc); desc.componentType = kAudioUnitType_MusicDevice; index_components (rv, desc); } /* ****************************************************************************/ using namespace ARDOUR; AUv2Info::AUv2Info (XMLNode const& node) : version (0) , n_inputs (0) , n_outputs (0) , n_midi_inputs (0) , n_midi_outputs (0) , max_outputs (0) { bool err = false; if (node.name() != "AUv2Info") { throw failed_constructor (); } err |= !node.get_property ("id", id); err |= !node.get_property ("name", name); err |= !node.get_property ("creator", creator); err |= !node.get_property ("category", category); err |= !node.get_property ("version", version); err |= !node.get_property ("n_inputs", n_inputs); err |= !node.get_property ("n_outputs", n_outputs); err |= !node.get_property ("n_midi_inputs", n_midi_inputs); err |= !node.get_property ("n_midi_outputs", n_midi_outputs); err |= !node.get_property ("max_outputs", max_outputs); const XMLNodeList children = node.children(); for (XMLNodeConstIterator i = children.begin(); i != children.end(); ++i) { if ((*i)->name() != X_("io_config")) { continue; } int32_t in; int32_t out; if ((*i)->get_property (X_("in"), in) && (*i)->get_property (X_("out"), out)) { io_configs.push_back (std::pair (in, out)); } } if (err) { throw failed_constructor (); } } XMLNode& AUv2Info::state () const { XMLNode* node = new XMLNode("AUv2Info"); node->set_property ("id", id); node->set_property ("name", name); node->set_property ("creator", creator); node->set_property ("category", category); node->set_property ("version", version); node->set_property ("n_inputs", n_inputs); node->set_property ("n_outputs", n_outputs); node->set_property ("n_midi_inputs", n_midi_inputs); node->set_property ("n_midi_outputs", n_midi_outputs); node->set_property ("max_outputs", max_outputs); for (vector >::const_iterator i = io_configs.begin(); i != io_configs.end(); ++i) { XMLNode* child = new XMLNode (X_("io_config")); child->set_property (X_("in"), i->first); child->set_property (X_("out"), i->second); node->add_child_nocopy (*child); } return *node; }