/********************************************************************** Audacity: A Digital Audio Editor VampEffect.cpp Chris Cannam, with heavy borrowing from LadspaEffect.cpp Vamp is an audio analysis and feature extraction plugin API. http://www.vamp-plugins.org/ **********************************************************************/ #include "../../Audacity.h" #if defined(USE_VAMP) #include "VampEffect.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../../widgets/valnum.h" enum { ID_Program = 10000, ID_Sliders = 11000, ID_Choices = 12000, ID_Texts = 13000, ID_Toggles = 14000, }; /////////////////////////////////////////////////////////////////////////////// // // VampEffect // /////////////////////////////////////////////////////////////////////////////// BEGIN_EVENT_TABLE(VampEffect, wxEvtHandler) EVT_SLIDER(wxID_ANY, VampEffect::OnSlider) EVT_TEXT(wxID_ANY, VampEffect::OnTextCtrl) EVT_CHECKBOX(wxID_ANY, VampEffect::OnCheckBox) EVT_CHOICE(wxID_ANY, VampEffect::OnChoice) END_EVENT_TABLE() VampEffect::VampEffect(Vamp::Plugin *plugin, const wxString & path, int output, bool hasParameters) : mPlugin(plugin), mPath(path), mOutput(output), mHasParameters(hasParameters), mRate(0) { mKey = mPath.BeforeLast(wxT('/')).ToUTF8(); mName = mPath.AfterLast(wxT('/')); mSliders = NULL; mFields = NULL; mLabels = NULL; mToggles = NULL; mChoices = NULL; mValues = NULL; } VampEffect::~VampEffect() { if (mPlugin) { delete mPlugin; } if (mValues) { delete [] mValues; } if (mSliders) { delete [] mSliders; } if (mFields) { delete [] mFields; } if (mLabels) { delete [] mLabels; } if (mToggles) { delete [] mToggles; } if (mChoices) { delete [] mChoices; } } // ============================================================================ // IdentInterface implementation // ============================================================================ wxString VampEffect::GetPath() { return mPath; } wxString VampEffect::GetSymbol() { return mName; } wxString VampEffect::GetName() { return GetSymbol(); } wxString VampEffect::GetVendor() { return wxString::FromUTF8(mPlugin->getMaker().c_str()); } wxString VampEffect::GetVersion() { return wxString::Format(wxT("%d"), mPlugin->getPluginVersion()); } wxString VampEffect::GetDescription() { return wxString::FromUTF8(mPlugin->getCopyright().c_str()); } // ============================================================================ // EffectIdentInterface implementation // ============================================================================ EffectType VampEffect::GetType() { return EffectTypeAnalyze; } wxString VampEffect::GetFamily() { return VAMPEFFECTS_FAMILY; } bool VampEffect::IsInteractive() { return mHasParameters; } bool VampEffect::IsDefault() { return false; } // EffectClientInterface implementation int VampEffect::GetAudioInCount() { return mPlugin->getMaxChannelCount(); } bool VampEffect::GetAutomationParameters(EffectAutomationParameters & parms) { for (size_t p = 0, cnt = mParameters.size(); p < cnt; p++) { wxString key = wxString::FromUTF8(mParameters[p].identifier.c_str()); float value = mPlugin->getParameter(mParameters[p].identifier); float lower = mParameters[p].minValue; float upper = mParameters[p].maxValue; if (mParameters[p].isQuantized && mParameters[p].quantizeStep == 1.0 && lower == 0.0 && upper == 1.0) { bool val = value > 0.5; parms.Write(key, val); } else if (mParameters[p].isQuantized && mParameters[p].quantizeStep == 1.0 && !mParameters[p].valueNames.empty()) { wxArrayString choices; int val = 0; for (size_t i = 0, cnt = mParameters[p].valueNames.size(); i < cnt; i++) { wxString choice = wxString::FromUTF8(mParameters[p].valueNames[i].c_str()); if (size_t(value - mParameters[p].minValue + 0.5) == i) { val = i; } choices.Add(choice); } parms.WriteEnum(key, val, choices); } else { parms.Write(key, value); } } return true; } bool VampEffect::SetAutomationParameters(EffectAutomationParameters & parms) { // First pass verifies values for (size_t p = 0, cnt = mParameters.size(); p < cnt; p++) { wxString key = wxString::FromUTF8(mParameters[p].identifier.c_str()); float lower = mParameters[p].minValue; float upper = mParameters[p].maxValue; bool good = false; if (mParameters[p].isQuantized && mParameters[p].quantizeStep == 1.0 && lower == 0.0 && upper == 1.0) { bool val; good = parms.Read(key, &val); } else if (mParameters[p].isQuantized && mParameters[p].quantizeStep == 1.0 && !mParameters[p].valueNames.empty()) { wxArrayString choices; int val; for (size_t i = 0, cnt = mParameters[p].valueNames.size(); i < cnt; i++) { wxString choice = wxString::FromUTF8(mParameters[p].valueNames[i].c_str()); choices.Add(choice); } good = parms.ReadEnum(key, &val, choices) && val != wxNOT_FOUND; } else { double val; good = parms.Read(key, &val) && val >= lower && val <= upper; } if (!good) { return false; } } // Second pass sets the variables for (size_t p = 0, cnt = mParameters.size(); p < cnt; p++) { wxString key = wxString::FromUTF8(mParameters[p].identifier.c_str()); float lower = mParameters[p].minValue; float upper = mParameters[p].maxValue; if (mParameters[p].isQuantized && mParameters[p].quantizeStep == 1.0 && lower == 0.0 && upper == 1.0) { bool val; parms.Read(key, &val); mPlugin->setParameter(mParameters[p].identifier, val ? upper : lower); } else if (mParameters[p].isQuantized && mParameters[p].quantizeStep == 1.0 && !mParameters[p].valueNames.empty()) { wxArrayString choices; int val; for (size_t i = 0, cnt = mParameters[p].valueNames.size(); i < cnt; i++) { wxString choice = wxString::FromUTF8(mParameters[p].valueNames[i].c_str()); choices.Add(choice); } parms.ReadEnum(key, &val, choices); mPlugin->setParameter(mParameters[p].identifier, (float) val); } else { double val; parms.Read(key, &val); if (mParameters[p].isQuantized) { float qs = mParameters[p].quantizeStep; if (qs != 0.0) { val = int((val - lower) / qs + 0.5) * qs + lower; } } mPlugin->setParameter(mParameters[p].identifier, val); } } return true; } bool VampEffect::Init() { TrackListOfKindIterator iter(Track::Wave, mTracks); WaveTrack *left = (WaveTrack *)iter.First(); mRate = 0.0; while (left) { if (mRate == 0.0) { mRate = left->GetRate(); } if (left->GetLinked()) { WaveTrack *right = (WaveTrack *)iter.Next(); if (left->GetRate() != right->GetRate()) { wxMessageBox(_("Sorry, Vamp Plug-ins cannot be run on stereo tracks where the individual channels of the track do not match.")); return false; } } left = (WaveTrack *)iter.Next(); } if (mRate <= 0.0) { mRate = mProjectRate; } // The plugin must be reloaded to allow changing parameters Vamp::HostExt::PluginLoader *loader = Vamp::HostExt::PluginLoader::getInstance(); if (mPlugin) { delete mPlugin; mPlugin = NULL; } mPlugin = loader->loadPlugin(mKey, mRate, Vamp::HostExt::PluginLoader::ADAPT_ALL); if (!mPlugin) { wxMessageBox(_("Sorry, failed to load Vamp Plug-in.")); return false; } return true; } bool VampEffect::Process() { if (!mPlugin) { return false; } TrackListOfKindIterator iter(Track::Wave, mTracks); int count = 0; WaveTrack *left = (WaveTrack *)iter.First(); bool multiple = false; int prevTrackChannels = 0; if (GetNumWaveGroups() > 1) { // if there is another track beyond this one and any linked one, // then we're processing more than one track. That means we // should use the originating track name in each new label // track's name, to make clear which is which multiple = true; } while (left) { sampleCount lstart, rstart = 0; sampleCount len; GetSamples(left, &lstart, &len); WaveTrack *right = NULL; int channels = 1; if (left->GetLinked()) { right = (WaveTrack *)iter.Next(); channels = 2; GetSamples(right, &rstart, &len); } size_t step = mPlugin->getPreferredStepSize(); size_t block = mPlugin->getPreferredBlockSize(); bool initialiseRequired = true; if (block == 0) { if (step != 0) { block = step; } else { block = 1024; } } if (step == 0) { step = block; } if (prevTrackChannels > 0) { // Plugin has already been initialised, so if the number of // channels remains the same, we only need to do a reset. // Otherwise we need to re-construct the whole plugin, // because a Vamp plugin can't be re-initialised. if (prevTrackChannels == channels) { mPlugin->reset(); initialiseRequired = false; } else { //!!! todo: retain parameters previously set Init(); } } if (initialiseRequired) { if (!mPlugin->initialise(channels, step, block)) { wxMessageBox(_("Sorry, Vamp Plug-in failed to initialize.")); return false; } } LabelTrack *ltrack = mFactory->NewLabelTrack(); if (!multiple) { ltrack->SetName(GetName()); } else { ltrack->SetName(wxString::Format(wxT("%s: %s"), left->GetName().c_str(), GetName().c_str())); } mTracks->Add(ltrack); float **data = new float *[channels]; // ANSWER-ME: Vigilant Sentry marks this as memory leak, var "data" not deleted. for (int c = 0; c < channels; ++c) { data[c] = new float[block]; } sampleCount originalLen = len; sampleCount ls = lstart; sampleCount rs = rstart; while (len) { int request = block; if (request > len) request = len; if (left) { left->Get((samplePtr)data[0], floatSample, ls, request); } if (right) { right->Get((samplePtr)data[1], floatSample, rs, request); } if (request < (int)block) { for (int c = 0; c < channels; ++c) { for (int i = request; i < (int)block; ++i) { data[c][i] = 0.f; } } } Vamp::RealTime timestamp = Vamp::RealTime::frame2RealTime(ls, (int)(mRate + 0.5)); Vamp::Plugin::FeatureSet features = mPlugin->process(data, timestamp); AddFeatures(ltrack, features); if (len > (int)step) { len -= step; } else { len = 0; } ls += step; rs += step; if (channels > 1) { if (TrackGroupProgress(count, (ls - lstart) / double(originalLen))) { return false; } } else { if (TrackProgress(count, (ls - lstart) / double(originalLen))) { return false; } } } Vamp::Plugin::FeatureSet features = mPlugin->getRemainingFeatures(); AddFeatures(ltrack, features); prevTrackChannels = channels; left = (WaveTrack *)iter.Next(); } return true; } void VampEffect::End() { delete mPlugin; mPlugin = 0; } void VampEffect::PopulateOrExchange(ShuttleGui & S) { Vamp::Plugin::ProgramList programs = mPlugin->getPrograms(); mParameters = mPlugin->getParameterDescriptors(); int count = mParameters.size(); mToggles = new wxCheckBox *[count]; mSliders = new wxSlider *[count]; mFields = new wxTextCtrl *[count]; mLabels = new wxStaticText *[count]; mChoices = new wxChoice *[count]; mValues = new float[count]; S.SetStyle(wxVSCROLL | wxTAB_TRAVERSAL); wxScrolledWindow *scroller = S.StartScroller(2); { S.StartStatic(_("Plugin Settings")); { S.StartMultiColumn(5, wxEXPAND); { S.SetStretchyCol(3); if (!programs.empty()) { wxString currentProgram = wxString::FromUTF8(mPlugin->getCurrentProgram().c_str()); wxArrayString choices; for (size_t i = 0, cnt = programs.size(); i < cnt; i++) { choices.Add(wxString::FromUTF8(programs[i].c_str())); } S.AddPrompt(_("Program")); S.Id(ID_Program); mProgram = S.AddChoice(wxT(""), currentProgram, &choices); mProgram->SetName(_("Program")); mProgram->SetSizeHints(-1, -1); wxSizer *s = mProgram->GetContainingSizer(); s->GetItem(mProgram)->SetFlag(wxEXPAND | wxALIGN_CENTER_VERTICAL | wxALL); S.AddSpace(1, 1); S.AddSpace(1, 1); S.AddSpace(1, 1); } for (int p = 0; p < count; p++) { wxString tip = wxString::FromUTF8(mParameters[p].description.c_str()); wxString unit = wxString::FromUTF8(mParameters[p].unit.c_str()); float value = mPlugin->getParameter(mParameters[p].identifier); mToggles[p] = NULL; mChoices[p] = NULL; mSliders[p] = NULL; mFields[p] = NULL; mValues[p] = 0.0; wxString labelText = wxString::FromUTF8(mParameters[p].name.c_str()); if (!unit.IsEmpty()) { labelText += wxT(" (") + unit + wxT(")"); } S.AddPrompt(labelText + wxT(":")); if (mParameters[p].isQuantized && mParameters[p].quantizeStep == 1.0 && mParameters[p].minValue == 0.0 && mParameters[p].maxValue == 1.0) { S.Id(ID_Toggles + p); mToggles[p] = S.AddCheckBox(wxT(""), value > 0.5 ? wxT("true") : wxT("false")); mToggles[p]->SetName(labelText); if (!tip.IsEmpty()) { mToggles[p]->SetToolTip(tip); } wxSizer *s = mToggles[p]->GetContainingSizer(); s->GetItem(mToggles[p])->SetFlag(wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL | wxALL); S.AddSpace(1, 1); S.AddSpace(1, 1); S.AddSpace(1, 1); } else if (mParameters[p].isQuantized && mParameters[p].quantizeStep == 1.0 && !mParameters[p].valueNames.empty()) { wxArrayString choices; wxString selected; for (size_t i = 0, cnt = mParameters[p].valueNames.size(); i < cnt; i++) { wxString choice = wxString::FromUTF8(mParameters[p].valueNames[i].c_str()); if (size_t(value - mParameters[p].minValue + 0.5) == i) { selected = choice; } choices.Add(choice); } S.Id(ID_Choices + p); mChoices[p] = S.AddChoice(wxT(""), selected, &choices); mChoices[p]->SetName(labelText); mChoices[p]->SetSizeHints(-1, -1); if (!tip.IsEmpty()) { mChoices[p]->SetToolTip(tip); } wxSizer *s = mChoices[p]->GetContainingSizer(); s->GetItem(mChoices[p])->SetFlag(wxEXPAND | wxALIGN_CENTER_VERTICAL | wxALL); S.AddSpace(1, 1); S.AddSpace(1, 1); S.AddSpace(1, 1); } else { mValues[p] = value; FloatingPointValidator vld(6, &mValues[p]); vld.SetRange(mParameters[p].minValue, mParameters[p].maxValue); float range = mParameters[p].maxValue - mParameters[p].minValue; int style = range < 10 ? NUM_VAL_THREE_TRAILING_ZEROES : range < 100 ? NUM_VAL_TWO_TRAILING_ZEROES : NUM_VAL_ONE_TRAILING_ZERO; vld.SetStyle(style); S.Id(ID_Texts + p); mFields[p] = S.AddTextBox(wxT(""), wxT(""), 12); mFields[p]->SetName(labelText); mFields[p]->SetValidator(vld); if (!tip.IsEmpty()) { mFields[p]->SetToolTip(tip); } wxSizer *s = mFields[p]->GetContainingSizer(); s->GetItem(mFields[p])->SetFlag(wxALIGN_CENTER_VERTICAL | wxALL); wxString str = Internat::ToDisplayString(mParameters[p].minValue); S.AddPrompt(str); S.SetStyle(wxSL_HORIZONTAL); S.Id(ID_Sliders + p); mSliders[p] = S.AddSlider(wxT(""), 0, 1000, 0); mSliders[p]->SetName(labelText); mSliders[p]->SetSizeHints(150, -1); if (!tip.IsEmpty()) { mSliders[p]->SetToolTip(tip); } str = Internat::ToDisplayString(mParameters[p].maxValue); S.AddUnits(str); } } } S.EndMultiColumn(); } S.EndStatic(); } S.EndScroller(); scroller->SetScrollRate(0, 20); return; } bool VampEffect::TransferDataToWindow() { if (!mUIParent->TransferDataToWindow()) { return false; } UpdateFromPlugin(); return true; } bool VampEffect::TransferDataFromWindow() { if (!mUIParent->Validate() || !mUIParent->TransferDataFromWindow()) { return false; } return true; } // VampEffect implementation void VampEffect::AddFeatures(LabelTrack *ltrack, Vamp::Plugin::FeatureSet &features) { for (Vamp::Plugin::FeatureList::iterator fli = features[mOutput].begin(); fli != features[mOutput].end(); ++fli) { Vamp::RealTime ftime0 = fli->timestamp; double ltime0 = ftime0.sec + (double(ftime0.nsec) / 1000000000.0); Vamp::RealTime ftime1 = ftime0; if (fli->hasDuration) ftime1 = ftime0 + fli->duration; double ltime1 = ftime1.sec + (double(ftime1.nsec) / 1000000000.0); wxString label = LAT1CTOWX(fli->label.c_str()); if (label == wxString()) { if (fli->values.empty()) { label = wxString::Format(LAT1CTOWX("%.3f"), ltime0); } else { label = wxString::Format(LAT1CTOWX("%.3f"), *fli->values.begin()); } } ltrack->AddLabel(SelectedRegion(ltime0, ltime1), label); } } void VampEffect::UpdateFromPlugin() { for (size_t p = 0, cnt = mParameters.size(); p < cnt; p++) { float value = mPlugin->getParameter(mParameters[p].identifier); if (mParameters[p].isQuantized && mParameters[p].quantizeStep == 1.0 && mParameters[p].minValue == 0.0 && mParameters[p].maxValue == 1.0) { mToggles[p]->SetValue(value > 0.5); } else if (mParameters[p].isQuantized && mParameters[p].quantizeStep == 1.0 && !mParameters[p].valueNames.empty()) { mChoices[p]->SetSelection(size_t(value - mParameters[p].minValue + 0.5)); } else { mValues[p] = value; mFields[p]->GetValidator()->TransferToWindow(); float lower = mParameters[p].minValue; float upper = mParameters[p].maxValue; float range = upper - lower; if (mParameters[p].isQuantized) { float qs = mParameters[p].quantizeStep; if (qs != 0.0) { value = int((value - lower) / qs + 0.5) * qs + lower; } } mSliders[p]->SetValue((int)(((value - lower) / range) * 1000.0 + 0.5)); } } } void VampEffect::OnCheckBox(wxCommandEvent &event) { int p = event.GetId() - ID_Toggles; mPlugin->setParameter(mParameters[p].identifier, mToggles[p]->GetValue()); } void VampEffect::OnChoice(wxCommandEvent & evt) { int p = evt.GetId(); // special value for programs if (p == ID_Program) { Vamp::Plugin::ProgramList programs = mPlugin->getPrograms(); mPlugin->selectProgram(programs[evt.GetInt()]); UpdateFromPlugin(); return; } mPlugin->setParameter(mParameters[p - ID_Choices].identifier, evt.GetInt()); } void VampEffect::OnSlider(wxCommandEvent & evt) { int p = evt.GetId() - ID_Sliders; float lower = mParameters[p].minValue; float upper = mParameters[p].maxValue; float range = upper - lower; float val = (evt.GetInt() / 1000.0) * range; if (mParameters[p].isQuantized) { float qs = mParameters[p].quantizeStep; if (qs != 0.0) { val = int(val / qs + 0.5) * qs; } } val += lower; mValues[p] = val; mFields[p]->GetValidator()->TransferToWindow(); mPlugin->setParameter(mParameters[p].identifier, val); } void VampEffect::OnTextCtrl(wxCommandEvent & evt) { int p = evt.GetId() - ID_Texts; mFields[p]->GetValidator()->TransferFromWindow(); float lower = mParameters[p].minValue; float upper = mParameters[p].maxValue; float range = upper - lower; float val = mValues[p]; if (mParameters[p].isQuantized) { float qs = mParameters[p].quantizeStep; if (qs != 0.0) { val = int((val - lower) / qs + 0.5) * qs + lower; } } mPlugin->setParameter(mParameters[p].identifier, val); mSliders[p]->SetValue((int)(((val - lower) / range) * 1000.0 + 0.5)); } #endif