1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-09-18 17:10:55 +02:00

Bug 2334 - Mac: AU effects cannot Export or Import the effect's settings

This removes the ability to import/export multiple presets at
once and add the standard method used by the other plug-ings.

It also adds a workaround for the AU view not updating after
setting a new preset.  I will continue to look for the true
cause of this, but the workaround does correct the display
issue.
This commit is contained in:
Leland Lucius 2020-03-14 18:58:55 -05:00
parent 6cf635fdc1
commit 9cf80a41ee
4 changed files with 316 additions and 323 deletions

View File

@ -47,6 +47,7 @@ public:
void CreateCocoa();
void CreateGeneric();
void CocoaViewResized();
void ForceRedraw();
void OnSize(wxSizeEvent & evt);
@ -55,8 +56,6 @@ public:
void CreateCarbonOverlay();
void CarbonViewResized();
static pascal OSStatus ControlEventHandlerCallback(EventHandlerCallRef handler,
EventRef event,
void *data);
#endif
private:

View File

@ -23,7 +23,6 @@
#include "../../Audacity.h"
#include "AUControl.h"
#include <AudioUnit/AudioUnitProperties.h>
#include <AudioUnit/AUCocoaUIView.h>
#include <CoreAudioKit/CoreAudioKit.h>
@ -36,7 +35,10 @@
@interface AUView : NSView
{
@public
AUControl *mControl;
NSView *mView;
bool mRedraw;
}
@end
@ -52,12 +54,13 @@
}
}
- (instancetype)initWithControl:(AUControl *)control
- (instancetype)initWithControl:(AUControl *)control;
{
// Make sure a parameters were provided
NSParameterAssert(control);
mControl = control;
mRedraw = NO;
[super init];
@ -69,6 +72,30 @@
return NO;
}
- (void)viewWillDraw
{
// LLL: Hackage alert! I have absolutely no idea why the AudioUnitView doesn't
// get redrawn after the ClassInfo is updated, but this bit of malarkey
// gets around this issue.
//
// To see the problem, comment out the setFrameSize calls below, create/save
// a preset for the AUDelay effect with "Invert Feedback" checked, restore
// factory defaults and then reselect the save preset. The display will not
// update properly. But, resize the window and it does.
//
// Again, this is total hackage and I hope to find the real cause soon.
if (mRedraw)
{
NSRect viewRect = [mView frame];
NSRect bogusRect = {};
[mView setFrameSize:bogusRect.size];
[mView setFrameSize:viewRect.size];
mRedraw = NO;
}
[super viewWillDraw];
}
- (void)cocoaViewResized:(NSNotification *)notification
{
mControl->CocoaViewResized();
@ -190,6 +217,8 @@ bool AUControl::Create(wxWindow *parent, AudioComponent comp, AudioUnit unit, bo
return false;
}
((AUView *)mAUView)->mView = mView;
// wxWidgets takes ownership so safenew
SetPeer(safenew AUControlImpl(this, mAUView));
@ -481,6 +510,11 @@ void AUControl::CocoaViewResized()
GetEventHandler()->AddPendingEvent(wxSizeEvent(GetSize()));
}
void AUControl::ForceRedraw()
{
((AUView *)mAUView)->mRedraw = YES;
}
#if !defined(_LP64)
void AUControl::CreateCarbon()

View File

@ -72,6 +72,11 @@ struct CFReleaser
template <typename T>
using CFunique_ptr = std::unique_ptr<T, CFReleaser>;
// Uncomment to include parameter IDs in the final name. Only needed if it's
// discovered that many effects have duplicate names. It could even be done
// at runtime by scanning an effects parameters to determine if dups are present
// and, if so, enable the clump and parameter IDs.
#define USE_EXTENDED_NAMES
class ParameterInfo
{
public:
@ -91,7 +96,7 @@ public:
}
}
bool Get(AudioUnit mUnit, AudioUnitParameterID parmId)
bool Get(AudioUnit mUnit, AudioUnitParameterID parmID)
{
OSStatus result;
UInt32 dataSize;
@ -101,7 +106,7 @@ public:
result = AudioUnitGetProperty(mUnit,
kAudioUnitProperty_ParameterInfo,
kAudioUnitScope_Global,
parmId,
parmID,
&info,
&dataSize);
if (result != noErr)
@ -118,19 +123,36 @@ public:
name = wxString(info.name);
}
if (name.empty())
#if defined(USE_EXTENDED_NAMES)
// If the parameter has a non-empty name, then the final parameter name will
// be either:
//
// <parmID,ParameterName>
//
// or (if the name isn't available):
//
// <parmID>
if (!name.empty())
{
return false;
name.Replace(idBeg, wxT('_'));
name.Replace(idSep, wxT('_'));
name.Replace(idEnd, wxT('_'));
name.Append(idSep);
}
name = wxString::Format(wxT("%c%s%x%c"),
idBeg,
name,
parmID,
idEnd);
// If the parameter has a clumpID, then the final parameter name will be
// either:
//
// <clumpID,clumpName>ParameterName
// <clumpID,clumpName><parmID,ParameterName>
//
// or (if the clumpName isn't available):
//
// <clumpID>ParameterName
// <clumpID><parmID,ParameterName>
if (info.flags & kAudioUnitParameterFlag_HasClump)
{
wxString clumpName;
@ -148,21 +170,20 @@ public:
&dataSize);
if (result == noErr)
{
clumpName.Printf(wxT("%c%s"),
idSep,
wxCFStringRef::AsString(clumpInfo.outName));
clumpName.Replace(idEnd, wxT("_"));
clumpName = wxCFStringRef::AsString(clumpInfo.outName);
clumpName.Replace(idBeg, wxT('_'));
clumpName.Replace(idSep, wxT('_'));
clumpName.Replace(idEnd, wxT('_'));
clumpName.Append(idSep);
}
name.Replace(idBeg, wxT('_'));
name.Replace(idEnd, wxT('_'));
name = wxString::Format(wxT("%c%x%s%c%s"),
name = wxString::Format(wxT("%c%s%x%c%s"),
idBeg,
info.clumpID,
clumpName,
info.clumpID,
idEnd,
name);
}
#endif
return true;
}
@ -415,7 +436,6 @@ AudioComponent AudioUnitEffectsModule::FindAudioUnit(const PluginPath & path,
desc.componentFlagsMask = 0;
name = tokens.GetNextToken();
return AudioComponentFindNext(NULL, &desc);
}
@ -568,237 +588,6 @@ void AudioUnitEffectOptionsDialog::OnOk(wxCommandEvent & WXUNUSED(evt))
EndModal(wxID_OK);
}
///////////////////////////////////////////////////////////////////////////////
//
// AudioUnitEffectExportDialog
//
///////////////////////////////////////////////////////////////////////////////
class AudioUnitEffectExportDialog final : public wxDialogWrapper
{
public:
AudioUnitEffectExportDialog(wxWindow * parent, AudioUnitEffect *effect);
virtual ~AudioUnitEffectExportDialog();
void PopulateOrExchange(ShuttleGui & S);
wxString Export(const wxString & name);
void OnOk(wxCommandEvent & evt);
private:
wxWindow *mParent;
AudioUnitEffect *mEffect;
wxListCtrl *mList;
DECLARE_EVENT_TABLE()
};
BEGIN_EVENT_TABLE(AudioUnitEffectExportDialog, wxDialogWrapper)
EVT_BUTTON(wxID_OK, AudioUnitEffectExportDialog::OnOk)
END_EVENT_TABLE()
AudioUnitEffectExportDialog::AudioUnitEffectExportDialog(wxWindow * parent, AudioUnitEffect *effect)
: wxDialogWrapper(parent, wxID_ANY, XO("Export Audio Unit Presets"))
{
mEffect = effect;
ShuttleGui S(this, eIsCreating);
PopulateOrExchange(S);
}
AudioUnitEffectExportDialog::~AudioUnitEffectExportDialog()
{
}
void AudioUnitEffectExportDialog::PopulateOrExchange(ShuttleGui & S)
{
S.SetBorder(5);
S.StartHorizontalLay(wxEXPAND, 1);
{
S.StartVerticalLay(true);
{
S.StartStatic(XO("Presets (may select multiple)"));
{
mList = S.Style(wxLC_REPORT | wxLC_HRULES | wxLC_VRULES |
wxLC_NO_SORT_HEADER)
.AddListControlReportMode( { XO("Preset") } );
}
S.EndStatic();
}
S.EndVerticalLay();
}
S.EndHorizontalLay();
S.AddStandardButtons();
RegistryPaths presets;
mEffect->mHost->GetPrivateConfigSubgroups(mEffect->mHost->GetUserPresetsGroup(wxEmptyString), presets);
std::sort( presets.begin(), presets.end() );
for (size_t i = 0, cnt = presets.size(); i < cnt; i++)
{
mList->InsertItem(i, presets[i]);
}
// Set the list size...with a little extra for good measure
wxSize sz = mList->GetBestSize();
sz.x += 5;
sz.y += 5;
mList->SetMinSize(sz);
Layout();
Fit();
Center();
// Make the single column a reasonable size...not perfect but better
// than using wxLIST_AUTOSIZE.
sz = mList->GetSize();
mList->SetColumnWidth(0, sz.x - 10);
}
wxString AudioUnitEffectExportDialog::Export(const wxString & name)
{
RegistryPath group = mEffect->mHost->GetUserPresetsGroup(name);
// Make sure the user preset directory exists
wxString path;
path.Printf(wxT("%s/%s/%s/%s.aupreset"),
PRESET_USER_PATH,
mEffect->mVendor,
mEffect->mName,
name);
wxFileName fn(path);
fn.Normalize();
if (!fn.Mkdir(0755, wxPATH_MKDIR_FULL))
{
return _("Couldn't create the \"%s\" directory").Format(fn.GetPath());
}
// Create the file
const wxString fullPath{fn.GetFullPath()};
wxFFile f(fullPath, wxT("wb"));
if (!f.IsOpened())
{
return _("Couldn't open \"%s\"").Format(fullPath);
}
// Retrieve preset from config file
wxString parms;
if (!mEffect->mHost->GetPrivateConfig(group, PRESET_KEY, parms, wxEmptyString))
{
return _("Preset key \"%s\" not found in group \"%s\"").Format(PRESET_KEY, group);
}
// Decode it
wxMemoryBuffer buf = wxBase64Decode(parms);
size_t bufLen = buf.GetDataLen();
if (!bufLen)
{
return _("Failed to decode preset");
}
const uint8_t *bufPtr = (uint8_t *) buf.GetData();
// Determine if the data is binary or XML
bool isBin = (bufLen >= 6 && memcmp(bufPtr, "bplist", 6) == 0);
// Convert binary plist to XML
if (isBin)
{
// Create a CFData object that references the decoded preset
CFunique_ptr<const __CFData> data
{
CFDataCreateWithBytesNoCopy(kCFAllocatorDefault,
bufPtr,
bufLen,
kCFAllocatorNull)
};
if (!data)
{
return _("Failed to convert preset to internal data representation");
}
// Convert it back to a property list.
CFPropertyListRef content
{
CFPropertyListCreateWithData(kCFAllocatorDefault,
data.get(),
kCFPropertyListImmutable,
NULL,
NULL)
};
if (!content)
{
return _("Failed to create property list from preset data");
}
CFunique_ptr<char /* CFPropertyList */> ucontent { (char *) content };
// Serialize it as XML data
data.reset(CFPropertyListCreateData(kCFAllocatorDefault,
content,
kCFPropertyListXMLFormat_v1_0,
0,
NULL));
if (!data)
{
return _("Failed to convert property list to XML data");
}
// Nothing to do if we don't have any data
SInt32 length = CFDataGetLength(data.get());
if (!length)
{
return _("XML data is empty after conversion");
}
// Write XML data
if (f.Write(CFDataGetBytePtr(data.get()), length) != length || f.Error())
{
return _("Failed to write XML preset to \"%s\"").Format(fullPath);
}
}
else
{
// Write XML data
if (f.Write(bufPtr, bufLen) != bufLen || f.Error())
{
return _("Failed to write XML preset to \"%s\"").Format(fullPath);
}
}
f.Close();
return wxEmptyString;
}
void AudioUnitEffectExportDialog::OnOk(wxCommandEvent & evt)
{
evt.Skip();
// Export all selected presets
long sel = -1;
while ((sel = mList->GetNextItem(sel, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)) >= 0)
{
wxString name = mList->GetItemText(sel);
wxString msg = Export(name);
if (!msg.IsEmpty())
{
AudacityMessageBox(
XO("Could not export \"%s\" preset\n\n%s").Format( name, msg ),
XO("Export Audio Unit Presets"),
wxOK | wxCENTRE,
this);
return;
}
}
EndModal(wxID_OK);
}
///////////////////////////////////////////////////////////////////////////////
//
// AudioUnitEffectImportDialog
@ -1748,12 +1537,7 @@ bool AudioUnitEffect::SetAutomationParameters(CommandParameters & parms)
value,
0);
AudioUnitParameter aup = {};
aup.mAudioUnit = mUnit;
aup.mParameterID = array[i];
aup.mScope = kAudioUnitScope_Global;
aup.mElement = 0;
AUParameterListenerNotify(NULL, NULL, &aup);
Notify(mUnit, array[i]);
}
}
@ -1794,7 +1578,7 @@ bool AudioUnitEffect::LoadFactoryPreset(int id)
return false;
}
AUPreset *preset = (AUPreset *) CFArrayGetValueAtIndex(array, id);
const AUPreset *preset = (const AUPreset *) CFArrayGetValueAtIndex(array, id);
result = AudioUnitSetProperty(mUnit,
kAudioUnitProperty_PresentPreset,
@ -1804,12 +1588,8 @@ bool AudioUnitEffect::LoadFactoryPreset(int id)
sizeof(AUPreset));
if (result == noErr)
{
AudioUnitParameter aup;
aup.mAudioUnit = mUnit;
aup.mParameterID = kAUParameterListener_AnyParameter;
aup.mScope = kAudioUnitScope_Global;
aup.mElement = 0;
AUParameterListenerNotify(NULL, NULL, &aup);
// Notify interested parties of change and propagate to slaves
Notify(mUnit, kAUParameterListener_AnyParameter);
}
return result == noErr;
@ -1983,37 +1763,91 @@ bool AudioUnitEffect::CanExportPresets()
void AudioUnitEffect::ExportPresets()
{
RegistryPaths presets;
// Generate the user domain path
wxFileName fn;
fn.SetPath(PRESET_USER_PATH);
fn.AppendDir(mVendor);
fn.AppendDir(mName);
fn.Normalize();
FilePath path = fn.GetFullPath();
mHost->GetPrivateConfigSubgroups(mHost->GetUserPresetsGroup(wxEmptyString), presets);
if (presets.size())
{
AudioUnitEffectExportDialog dlg(mDialog, this);
dlg.ShowModal();
}
else
if (!fn.Mkdir(fn.GetFullPath(), 0755, wxPATH_MKDIR_FULL))
{
AudacityMessageBox(XO("No user presets to export."),
XO("Export Presets"),
wxOK | wxCENTRE,
mDialog);
wxLogError(_("Couldn't create the \"%s\" directory").Format(fn.GetPath()));
return;
}
// Ask the user for the name to use
//
// Passing a valid parent will cause some effects dialogs to malfunction
// upon returning from the FileNames::SelectFile().
path = FileNames::SelectFile(FileNames::Operation::_None,
XO("Export Audio Unit Preset As %s:").Format(fn.GetFullPath()),
fn.GetFullPath(),
wxEmptyString,
wxT("aupreset"),
{
{ XO("Standard Audio Unit preset file"), { wxT("aupreset") }, true },
},
wxFD_SAVE | wxFD_OVERWRITE_PROMPT | wxRESIZE_BORDER,
NULL);
// User canceled...
if (path.empty())
{
return;
}
wxString msg = Export(path);
if (!msg.IsEmpty())
{
AudacityMessageBox(
XO("Could not export \"%s\" preset\n\n%s").Format( path, msg ),
XO("Export Audio Unit Presets"),
wxOK | wxCENTRE,
mParent);
}
}
void AudioUnitEffect::ImportPresets()
{
AudioUnitEffectImportDialog dlg(mDialog, this);
if (dlg.HasPresets())
// Generate the user domain path
wxFileName fn;
fn.SetPath(PRESET_USER_PATH);
fn.AppendDir(mVendor);
fn.AppendDir(mName);
fn.Normalize();
FilePath path = fn.GetFullPath();
// Ask the user for the name to use
//
// Passing a valid parent will cause some effects dialogs to malfunction
// upon returning from the FileNames::SelectFile().
path = FileNames::SelectFile(FileNames::Operation::_None,
XO("Import Audio Unit Preset As %s:").Format(fn.GetFullPath()),
fn.GetFullPath(),
wxEmptyString,
wxT("aupreset"),
{
{ XO("Standard Audio Unit preset file"), { wxT("aupreset") }, true },
},
wxFD_OPEN | wxRESIZE_BORDER,
NULL);
// User canceled...
if (path.empty())
{
dlg.ShowModal();
return;
}
else
wxString msg = Import(path);
if (!msg.IsEmpty())
{
AudacityMessageBox(XO("No user or local presets to import."),
XO("Import Presets"),
wxOK | wxCENTRE,
mDialog);
AudacityMessageBox(
XO("Could not import \"%s\" preset\n\n%s").Format( path, msg ),
XO("Import Audio Unit Presets"),
wxOK | wxCENTRE,
mParent);
}
}
@ -2107,6 +1941,12 @@ bool AudioUnitEffect::LoadPreset(const RegistryPath & group)
}
CFunique_ptr<char /* CFPropertyList */> ucontent { (char *) content };
// See AUView::viewWillDraw
if (mpControl)
{
mpControl->ForceRedraw();
}
// Finally, update the properties and parameters
OSStatus result;
result = AudioUnitSetProperty(mUnit,
@ -2121,38 +1961,8 @@ bool AudioUnitEffect::LoadPreset(const RegistryPath & group)
return false;
}
// And notify any interested parties
AudioUnitParameter aup = {};
aup.mAudioUnit = mUnit;
aup.mParameterID = kAUParameterListener_AnyParameter;
aup.mScope = kAudioUnitScope_Global;
aup.mElement = 0;
AUParameterListenerNotify(NULL, NULL, &aup);
// Make sure all slaves get the new preset as well
for (size_t i = 0, cnt = mSlaves.size(); i < cnt; i++)
{
// Finally, update the properties and parameters
OSStatus result;
result = AudioUnitSetProperty(mSlaves[i]->mUnit,
kAudioUnitProperty_ClassInfo,
kAudioUnitScope_Global,
0,
&content,
sizeof(content));
if (result != noErr)
{
wxLogError(wxT("Failed to set slave class info for \"%s\" preset"), group);
}
// And notify any interested parties
AudioUnitParameter aup = {};
aup.mAudioUnit = mUnit;
aup.mParameterID = kAUParameterListener_AnyParameter;
aup.mScope = kAudioUnitScope_Global;
aup.mElement = 0;
AUParameterListenerNotify(NULL, NULL, &aup);
}
// Notify interested parties of change and propagate to slaves
Notify(mUnit, kAUParameterListener_AnyParameter);
return true;
}
@ -2382,12 +2192,7 @@ bool AudioUnitEffect::CopyParameters(AudioUnit srcUnit, AudioUnit dstUnit)
}
// Notify interested parties
AudioUnitParameter aup = {};
aup.mAudioUnit = dstUnit;
aup.mParameterID = kAUParameterListener_AnyParameter;
aup.mScope = kAudioUnitScope_Global;
aup.mElement = 0;
AUParameterListenerNotify(NULL, NULL, &aup);
Notify(dstUnit, kAUParameterListener_AnyParameter);
return true;
}
@ -2402,6 +2207,158 @@ void AudioUnitEffect::SetChannelCount(unsigned numChannels)
mNumChannels = numChannels;
}
wxString AudioUnitEffect::Export(const wxString & path)
{
// Create the file
wxFFile f(path, wxT("wb"));
if (!f.IsOpened())
{
return _("Couldn't open \"%s\"").Format(path);
}
// First set the name of the preset
wxCFStringRef cfname(wxFileName(path).GetName());
// Define the preset property
AUPreset preset;
preset.presetNumber = -1; // indicates user preset
preset.presetName = cfname;
// And set it in the audio unit
OSStatus result;
result = AudioUnitSetProperty(mUnit,
kAudioUnitProperty_PresentPreset,
kAudioUnitScope_Global,
0,
&preset,
sizeof(preset));
if (result != noErr)
{
return _("Failed to set preset name");
}
// Now retrieve the preset content
CFPropertyListRef content;
UInt32 size = sizeof(content);
result = AudioUnitGetProperty(mUnit,
kAudioUnitProperty_ClassInfo,
kAudioUnitScope_Global,
0,
&content,
&size);
CFunique_ptr<char /* CFPropertyList */> ucontent { (char *) content };
if (result != noErr)
{
return _("Failed to retrieve preset content");
}
// And convert it to serialized XML data
CFunique_ptr<const __CFData> data
{
CFPropertyListCreateData(kCFAllocatorDefault,
content,
kCFPropertyListXMLFormat_v1_0,
0,
NULL)
};
if (!data)
{
return _("Failed to convert property list to XML data");
}
// Nothing to do if we don't have any data
SInt32 length = CFDataGetLength(data.get());
if (!length)
{
return _("XML data is empty after conversion");
}
// Write XML data
if (f.Write(CFDataGetBytePtr(data.get()), length) != length || f.Error())
{
return _("Failed to write XML preset to \"%s\"").Format(path);
}
f.Close();
return wxEmptyString;
}
wxString AudioUnitEffect::Import(const wxString & path)
{
// Open the preset
wxFFile f(path, wxT("r"));
if (!f.IsOpened())
{
return _("Couldn't open \"%s\"").Format(path);
}
// Load it into the buffer
size_t len = f.Length();
wxMemoryBuffer buf(len);
if (f.Read(buf.GetData(), len) != len || f.Error())
{
return _("Unable to read the preset from \"%s\"").Format(path);
}
// Create a CFData object that references the decoded preset
CFunique_ptr<const __CFData> data
{
CFDataCreateWithBytesNoCopy(kCFAllocatorDefault,
(const UInt8 *) buf.GetData(),
len,
kCFAllocatorNull)
};
if (!data)
{
return _("Failed to convert preset to internal format");
}
// Convert it back to a property list.
CFPropertyListRef content
{
CFPropertyListCreateWithData(kCFAllocatorDefault,
data.get(),
kCFPropertyListImmutable,
NULL,
NULL)
};
if (!content)
{
return _("Failed to create property list for preset");
}
CFunique_ptr<char /* CFPropertyList */> ucontent { (char *) content };
// Finally, update the properties and parameters
OSStatus result;
result = AudioUnitSetProperty(mUnit,
kAudioUnitProperty_ClassInfo,
kAudioUnitScope_Global,
0,
&content,
sizeof(content));
if (result != noErr)
{
return _("Failed to set class info for \"%s\" preset");
}
// Notify interested parties of change and propagate to slaves
Notify(mUnit, kAUParameterListener_AnyParameter);
return wxEmptyString;
}
void AudioUnitEffect::Notify(AudioUnit unit, AudioUnitParameterID parm)
{
// Notify any interested parties
AudioUnitParameter aup = {};
aup.mAudioUnit = unit;
aup.mParameterID = parm;
aup.mScope = kAudioUnitScope_Global;
aup.mElement = 0;
AUParameterListenerNotify(NULL, NULL, &aup);
}
OSStatus AudioUnitEffect::Render(AudioUnitRenderActionFlags *inActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,

View File

@ -136,6 +136,9 @@ private:
bool SetRateAndChannels();
bool CopyParameters(AudioUnit srcUnit, AudioUnit dstUnit);
wxString Export(const wxString & path);
wxString Import(const wxString & path);
void Notify(AudioUnit unit, AudioUnitParameterID parm);
// Realtime
unsigned GetChannelCount();