// SPDX-License-Identifier: BSD-2-Clause // This code is part of the sfizz library and is licensed under a BSD 2-clause // license. You should have receive a LICENSE.md file along with the code. // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #include "SfizzVstController.h" #include "SfizzVstEditor.h" #include "SfizzVstParameters.h" #include "SfizzVstIDs.h" #include "plugin/InstrumentDescription.h" #include "base/source/fstreamer.h" #include "base/source/fstring.h" #include "base/source/updatehandler.h" #include #include #include #include enum { kProgramListID = 0 }; tresult PLUGIN_API SfizzVstControllerNoUi::initialize(FUnknown* context) { tresult result = EditControllerEx1::initialize(context); if (result != kResultTrue) return result; // initialize the update handler Steinberg::UpdateHandler::instance(); // initialize the thread checker threadChecker_ = Vst::ThreadChecker::create(); // create update objects queuedUpdates_ = Steinberg::owned(new QueuedUpdates); sfzUpdate_ = Steinberg::owned(new SfzUpdate); sfzDescriptionUpdate_ = Steinberg::owned(new SfzDescriptionUpdate); scalaUpdate_ = Steinberg::owned(new ScalaUpdate); playStateUpdate_ = Steinberg::owned(new PlayStateUpdate); // Unit addUnit(new Vst::Unit(Steinberg::String("Root"), Vst::kRootUnitId, Vst::kNoParentUnitId, kProgramListID)); // Parameters Vst::ParamID pid = 0; // Ordinary parameters parameters.addParameter( SfizzRange::getForParameter(kPidVolume).createParameter( Steinberg::String("Volume"), pid++, Steinberg::String("dB"), 0, Vst::ParameterInfo::kCanAutomate, Vst::kRootUnitId)); parameters.addParameter( SfizzRange::getForParameter(kPidNumVoices).createParameter( Steinberg::String("Polyphony"), pid++, nullptr, 0, Vst::ParameterInfo::kNoFlags, Vst::kRootUnitId)); parameters.addParameter( SfizzRange::getForParameter(kPidOversampling).createParameter( Steinberg::String("Oversampling"), pid++, nullptr, 0, Vst::ParameterInfo::kNoFlags, Vst::kRootUnitId)); parameters.addParameter( SfizzRange::getForParameter(kPidPreloadSize).createParameter( Steinberg::String("Preload size"), pid++, nullptr, 0, Vst::ParameterInfo::kNoFlags, Vst::kRootUnitId)); parameters.addParameter( SfizzRange::getForParameter(kPidScalaRootKey).createParameter( Steinberg::String("Scala root key"), pid++, nullptr, 0, Vst::ParameterInfo::kNoFlags, Vst::kRootUnitId)); parameters.addParameter( SfizzRange::getForParameter(kPidTuningFrequency).createParameter( Steinberg::String("Tuning frequency"), pid++, Steinberg::String("Hz"), 0, Vst::ParameterInfo::kNoFlags, Vst::kRootUnitId)); parameters.addParameter( SfizzRange::getForParameter(kPidStretchedTuning).createParameter( Steinberg::String("Stretched tuning"), pid++, nullptr, 0, Vst::ParameterInfo::kNoFlags, Vst::kRootUnitId)); parameters.addParameter( SfizzRange::getForParameter(kPidSampleQuality).createParameter( Steinberg::String("Sample quality"), pid++, nullptr, 0, Vst::ParameterInfo::kNoFlags, Vst::kRootUnitId)); parameters.addParameter( SfizzRange::getForParameter(kPidOscillatorQuality).createParameter( Steinberg::String("Oscillator quality"), pid++, nullptr, 0, Vst::ParameterInfo::kNoFlags, Vst::kRootUnitId)); parameters.addParameter( SfizzRange::getForParameter(kPidFreewheelingSampleQuality).createParameter( Steinberg::String("Freewheeling sample quality"), pid++, nullptr, 0, Vst::ParameterInfo::kNoFlags, Vst::kRootUnitId)); parameters.addParameter( SfizzRange::getForParameter(kPidFreewheelingOscillatorQuality).createParameter( Steinberg::String("Freewheeling oscillator quality"), pid++, nullptr, 0, Vst::ParameterInfo::kNoFlags, Vst::kRootUnitId)); parameters.addParameter( SfizzRange::getForParameter(kPidSustainCancelsRelease).createParameter( Steinberg::String("Sustain cancels release"), pid++, nullptr, 1, Vst::ParameterInfo::kNoFlags, Vst::kRootUnitId)); // MIDI special controllers parameters.addParameter( SfizzRange::getForParameter(kPidAftertouch).createParameter( Steinberg::String("Aftertouch"), pid++, nullptr, 0, Vst::ParameterInfo::kCanAutomate, Vst::kRootUnitId)); parameters.addParameter( SfizzRange::getForParameter(kPidPitchBend).createParameter( Steinberg::String("Pitch bend"), pid++, nullptr, 0, Vst::ParameterInfo::kCanAutomate, Vst::kRootUnitId)); // MIDI controllers for (unsigned i = 0; i < sfz::config::numCCs; ++i) { Steinberg::String title; Steinberg::String shortTitle; title.printf("Controller %u", i); shortTitle.printf("CC%u", i); parameters.addParameter( SfizzRange::getForParameter(kPidCC0 + i).createParameter( title, pid++, nullptr, 0, Vst::ParameterInfo::kCanAutomate, Vst::kRootUnitId, shortTitle)); } // Volume levels for (unsigned i = 0; i < 16; ++i) { Steinberg::String title; title.printf("Level %u", i); parameters.addParameter( SfizzRange::getForParameter(kPidLevel0 + i).createParameter( title, pid++, nullptr, 0, Vst::ParameterInfo::kIsReadOnly|Vst::ParameterInfo::kIsHidden, Vst::kRootUnitId)); } // Editor status parameters.addParameter( SfizzRange::getForParameter(kPidEditorOpen).createParameter( Steinberg::String("Editor open"), pid++, nullptr, 0, Vst::ParameterInfo::kIsReadOnly|Vst::ParameterInfo::kIsHidden, Vst::kRootUnitId)); // Initial MIDI mapping for (int32 i = 0; i < Vst::kCountCtrlNumber; ++i) { Vst::ParamID id = Vst::kNoParamId; switch (i) { case Vst::kAfterTouch: id = kPidAftertouch; break; case Vst::kPitchBend: id = kPidPitchBend; break; default: if (i < 128) id = kPidCC0 + i; break; } midiMapping_[i] = id; } // Program list IPtr list = Steinberg::owned( new Vst::ProgramListWithPitchNames(Steinberg::String("Programs"), kProgramListID, Vst::kRootUnitId)); list->addProgram(Steinberg::String("Default")); addProgramList(list); list->addRef(); // Use linear knobs setKnobMode(kLinearMode); return kResultTrue; } tresult PLUGIN_API SfizzVstControllerNoUi::terminate() { return EditControllerEx1::terminate(); } tresult PLUGIN_API SfizzVstControllerNoUi::getMidiControllerAssignment(int32 busIndex, int16 channel, Vst::CtrlNumber midiControllerNumber, Vst::ParamID& id) { if (midiControllerNumber < 0 || midiControllerNumber >= Vst::kCountCtrlNumber) { id = Vst::kNoParamId; return kResultFalse; } id = midiMapping_[midiControllerNumber]; if (id == Vst::kNoParamId) return kResultFalse; return kResultTrue; } int32 PLUGIN_API SfizzVstControllerNoUi::getKeyswitchCount(int32 busIndex, int16 channel) { (void)channel; if (busIndex != 0) return 0; return keyswitches_.size(); } tresult PLUGIN_API SfizzVstControllerNoUi::getKeyswitchInfo(int32 busIndex, int16 channel, int32 keySwitchIndex, Vst::KeyswitchInfo& info) { (void)channel; if (busIndex != 0) return kResultFalse; if (keySwitchIndex < 0 || keySwitchIndex >= keyswitches_.size()) return kResultFalse; info = keyswitches_[keySwitchIndex]; return kResultTrue; } tresult PLUGIN_API SfizzVstControllerNoUi::beginEditFromHost(Vst::ParamID paramID) { // Note(jpc) implementing this interface is a workaround to make // non-automatable parameters editable in Ardour (as of 6.6) (void)paramID; return kResultTrue; } tresult PLUGIN_API SfizzVstControllerNoUi::endEditFromHost(Vst::ParamID paramID) { (void)paramID; return kResultTrue; } tresult PLUGIN_API SfizzVstControllerNoUi::getParamStringByValue(Vst::ParamID tag, Vst::ParamValue valueNormalized, Vst::String128 string) { switch (tag) { case kPidOversampling: { const SfizzRange range = SfizzRange::getForParameter(tag); const int factorLog2 = static_cast(range.denormalize(valueNormalized)); Steinberg::String buf; buf.printf("%dX", 1 << factorLog2); buf.copyTo(string); return kResultTrue; } } return EditControllerEx1::getParamStringByValue(tag, valueNormalized, string); } tresult PLUGIN_API SfizzVstControllerNoUi::getParamValueByString(Vst::ParamID tag, Vst::TChar* string, Vst::ParamValue& valueNormalized) { switch (tag) { case kPidOversampling: { int32 factor; if (!Steinberg::String::scanInt32(string, factor, false)) factor = 1; const SfizzRange range = SfizzRange::getForParameter(tag); valueNormalized = range.normalize(integerLog2(factor)); return kResultTrue; } } return EditControllerEx1::getParamValueByString(tag, string, valueNormalized); } tresult SfizzVstControllerNoUi::setParam(Vst::ParamID tag, float value) { const SfizzRange range = SfizzRange::getForParameter(tag); return setParamNormalized(tag, range.normalize(value)); } tresult PLUGIN_API SfizzVstControllerNoUi::setComponentState(IBStream* stream) { SfizzVstState s; tresult r = s.load(stream); if (r != kResultTrue) return r; setParam(kPidVolume, s.volume); setParam(kPidNumVoices, s.numVoices); setParam(kPidOversampling, s.oversamplingLog2); setParam(kPidPreloadSize, s.preloadSize); setParam(kPidScalaRootKey, s.scalaRootKey); setParam(kPidTuningFrequency, s.tuningFrequency); setParam(kPidStretchedTuning, s.stretchedTuning); setParam(kPidSampleQuality, s.sampleQuality); setParam(kPidOscillatorQuality, s.oscillatorQuality); setParam(kPidFreewheelingSampleQuality, s.freewheelingSampleQuality); setParam(kPidFreewheelingOscillatorQuality, s.freewheelingOscillatorQuality); setParam(kPidSustainCancelsRelease, s.sustainCancelsRelease); uint32 ccLimit = uint32(std::min(s.controllers.size(), size_t(sfz::config::numCCs))); for (uint32 cc = 0; cc < ccLimit; ++cc) { if (absl::optional value = s.controllers[cc]) setParam(kPidCC0 + cc, *value); } sfzUpdate_->setPath(s.sfzFile); sfzUpdate_->deferUpdate(); scalaUpdate_->setPath(s.scalaFile); scalaUpdate_->deferUpdate(); return kResultTrue; } tresult SfizzVstControllerNoUi::notify(Vst::IMessage* message) { // Note: is expected to be called from the controller thread only tresult result = EditControllerEx1::notify(message); if (result != kResultFalse) return result; if (!threadChecker_->test()) { static std::atomic_bool warn_once_flag { false }; if (!warn_once_flag.exchange(true)) fprintf(stderr, "[sfizz] controller notification arrives from the wrong thread\n"); } const char* id = message->getMessageID(); /// if (!strcmp(id, SfzUpdate::getFClassID())) { if (!sfzUpdate_->convertFromMessage(*message)) { // assert(false); return kResultFalse; } // update the program name and notify std::string name = fs::u8path(sfzUpdate_->getPath()).filename().u8string(); if (absl::EndsWithIgnoreCase(name, ".sfz")) name.resize(name.size() - 4); setProgramName(kProgramListID, 0, Steinberg::String(name.c_str())); FUnknownPtr unitHandler(getComponentHandler()); if (unitHandler) unitHandler->notifyProgramListChange(kProgramListID, 0); // sfzUpdate_->deferUpdate(); } else if (!strcmp(id, SfzDescriptionUpdate::getFClassID())) { if (!sfzDescriptionUpdate_->convertFromMessage(*message)) { assert(false); return kResultFalse; } // parse the description blob const InstrumentDescription desc = parseDescriptionBlob( sfzDescriptionUpdate_->getDescription()); // update pitch names and notify Vst::ProgramListWithPitchNames* list = static_cast(getProgramList(kProgramListID)); for (int16 pitch = 0; pitch < 128; ++pitch) { Steinberg::String pitchName; if (desc.keyUsed.test(pitch) && !desc.keyLabel[pitch].empty()) pitchName = Steinberg::String(desc.keyLabel[pitch].c_str()); else if (desc.keyswitchUsed.test(pitch) && !desc.keyswitchLabel[pitch].empty()) pitchName = Steinberg::String(desc.keyswitchLabel[pitch].c_str()); list->setPitchName(0, pitch, pitchName); } FUnknownPtr unitHandler(getComponentHandler()); if (unitHandler) unitHandler->notifyProgramListChange(kProgramListID, 0); // update the key switches and notify size_t idKeyswitch = 0; for (int16 pitch = 0; pitch < 128; ++pitch) idKeyswitch += desc.keyswitchUsed.test(pitch); keyswitches_.resize(idKeyswitch); idKeyswitch = 0; for (int16 pitch = 0; pitch < 128; ++pitch) { if (!desc.keyswitchUsed.test(pitch)) continue; Vst::KeyswitchInfo info {}; info.typeId = Vst::kNoteOnKeyswitchTypeID; Steinberg::String(desc.keyswitchLabel[pitch].c_str()).copyTo(info.title); Steinberg::String(desc.keyswitchLabel[pitch].c_str()).copyTo(info.shortTitle); info.keyswitchMin = pitch; // TODO reexamine this when supporting keyswitch groups info.keyswitchMax = pitch; // TODO reexamine this when supporting keyswitch groups info.keyRemapped = pitch; info.unitId = Vst::kRootUnitId; info.flags = 0; keyswitches_[idKeyswitch++] = info; } if (Vst::IComponentHandler* componentHandler = getComponentHandler()) componentHandler->restartComponent(Vst::kKeyswitchChanged); // update the parameter titles and notify for (uint32 cc = 0; cc < sfz::config::numCCs; ++cc) { Vst::ParamID pid = kPidCC0 + cc; Vst::Parameter* param = getParameterObject(pid); Vst::ParameterInfo& info = param->getInfo(); Steinberg::String title; Steinberg::String shortTitle; if (!desc.ccLabel[cc].empty()) { title = desc.ccLabel[cc].c_str(); shortTitle = title; } else { title.printf("Controller %u", cc); shortTitle.printf("CC%u", cc); } title.copyTo(info.title); shortTitle.copyTo(info.shortTitle); } if (Vst::IComponentHandler* componentHandler = getComponentHandler()) componentHandler->restartComponent(Vst::kParamTitlesChanged); // sfzDescriptionUpdate_->deferUpdate(); } else if (!strcmp(id, ScalaUpdate::getFClassID())) { if (!scalaUpdate_->convertFromMessage(*message)) { assert(false); return kResultFalse; } scalaUpdate_->deferUpdate(); } else if (!strcmp(id, PlayStateUpdate::getFClassID())) { if (!playStateUpdate_->convertFromMessage(*message)) { assert(false); return kResultFalse; } playStateUpdate_->deferUpdate(); } else if (!strcmp(id, OSCUpdate::getFClassID())) { IPtr update = OSCUpdate::createFromMessage(*message); if (!update) { assert(false); return kResultFalse; } queuedUpdates_->enqueue(update); queuedUpdates_->deferUpdate(); } else if (!strcmp(id, NoteUpdate::getFClassID())) { IPtr update = NoteUpdate::createFromMessage(*message); if (!update) { assert(false); return kResultFalse; } queuedUpdates_->enqueue(update); queuedUpdates_->deferUpdate(); } else if (!strcmp(id, AutomationUpdate::getFClassID())) { IPtr update = AutomationUpdate::createFromMessage(*message); if (!update) { assert(false); return kResultFalse; } for (AutomationUpdate::Item item : update->getItems()) setParam(item.first, item.second); } return result; } // --- Controller with UI --- // IPlugView* PLUGIN_API SfizzVstController::createView(FIDString _name) { ConstString name(_name); fprintf(stderr, "[sfizz] about to create view: %s\n", _name); if (name != Vst::ViewType::kEditor) return nullptr; std::vector updates; updates.push_back(queuedUpdates_); updates.push_back(sfzUpdate_); updates.push_back(sfzDescriptionUpdate_); updates.push_back(scalaUpdate_); updates.push_back(playStateUpdate_); for (uint32 i = 0, n = parameters.getParameterCount(); i < n; ++i) updates.push_back(parameters.getParameterByIndex(i)); IPtr editor = Steinberg::owned( new SfizzVstEditor(this, absl::MakeSpan(updates))); editor->remember(); return editor; } FUnknown* SfizzVstController::createInstance(void*) { return static_cast(new SfizzVstController); } template <> FUnknown* createInstance(void* context) { return SfizzVstController::createInstance(context); } FUID SfizzVstController::cid = SfizzVstController_cid;