From 5912b703c6fc1062b0dbf56fed543e2b0d9dbe54 Mon Sep 17 00:00:00 2001 From: Paul MARTIN Date: Sat, 9 May 2026 22:57:32 +0200 Subject: [PATCH 1/9] [audioplugins] revamp framework module: decouple, version, state lifecycle Decouple the audioplugins module from the audio module so it can host generic plugin discovery for other apps (e.g. Audacity), and add a versioned cache schema with app-registered migrations. This commit is the framework-side half; it is paired with a main-repo commit that wires the MuseScore-side migrations and bumps the muse/ submodule pointer. Decoupling: - Plugin-shaped types (AudioResourceMeta, AudioResourceAttributes, AudioResourceId, etc.) live in muse::audioplugins:: instead of muse::audio::. Audio keeps `using` aliases for source compat. - audioplugins::AudioResourceType becomes an opaque std::string. Apps register their own plugin format identifiers; the audio module keeps an engine-internal enum and converts at the boundary via resourceTypeFromString() / resourceTypeName(). - Runtime-only attributes (skipped on save, re-injected on load) are app-registered via IAudioPluginsConfiguration::setRuntimeAttributeDefaults instead of hard-coded to audio::PLAYBACK_SETUP_DATA_ATTRIBUTE. - HAS_NATIVE_EDITOR_SUPPORT_ATTRIBUTE and CATEGORIES_ATTRIBUTE move out of audioplugins (now framework-pure) into audio. meta.hasNativeEditorSupport() is replaced by a free function audio::hasNativeEditorSupport(meta). - AudioPluginType, IAudioPluginTypeDetector and AudioPluginInfo.type are dropped from the framework. The Instrument/Fx classification was runtime-only, never persisted, and MuseScore-shaped. The VST module gains its own vst::PluginType and computes the category from meta on demand. Cache schema (version field added; bare-array legacy treated as v0): - v0 -> v1: hasNativeEditorSupport moves from a top-level meta field into meta.attributes ("true"/"false" strings). - v1 -> v2: the boolean `enabled` flag becomes a `state` string. The state lifecycle has four values: * Discovered: scanner found the file but validation hasn't run yet * Validated: validation succeeded; usable * Missing: file no longer found at the previously known path * Error: validation failed (errorCode carries detail) - Both migrations are registered MuseScore-side via the new IKnownAudioPluginsMigrationRegister. Scanner behaviour: - scanPlugins() now reports rediscoveredPluginIds (entries that were Missing and have come back) alongside missingPluginIds. - updatePluginsRegistry() uses the new IKnownAudioPluginsRegister::setPluginsState() to mark removed entries as Missing instead of erasing them, and rediscovered ones back to Validated. Already-Missing entries stay Missing without churn. unregisterPlugins() is kept for explicit UI-driven removal. Typed attribute accessors: - boolAttribute() / intAttribute() free helpers in muse::audioplugins encode the on-disk "true"/"1" -> bool and digit-string -> int conventions in one place, so callers don't reimplement them. Storage stays as map; no JSON / RPC / file-format change. Tests cover migration chaining, legacy v0 array load, the v0->v1 and v1->v2 transitions, and the Missing/Rediscovered scanner transitions. --- framework/audio/common/audiotypes.h | 116 +++-- framework/audio/common/audioutils.h | 24 +- framework/audio/common/rpc/rpcpacker.h | 4 +- .../internal/audioengineconfiguration.cpp | 3 +- .../engine/internal/enginerpccontroller.cpp | 4 +- .../synthesizers/fluidsynth/fluidresolver.cpp | 6 +- framework/audio/tests/rpcpacker_tests.cpp | 11 +- framework/audioplugins/CMakeLists.txt | 4 +- framework/audioplugins/audiopluginsmodule.cpp | 4 +- framework/audioplugins/audiopluginstypes.h | 143 +++++- .../audioplugins/iaudiopluginmetareader.h | 6 +- .../audioplugins/iaudiopluginsconfiguration.h | 9 + .../iknownaudiopluginsmigrationregister.h | 46 ++ .../audioplugins/iknownaudiopluginsregister.h | 14 +- .../internal/audiopluginsconfiguration.cpp | 10 + .../internal/audiopluginsconfiguration.h | 6 + .../knownaudiopluginsmigrationregister.cpp | 104 ++++ ...h => knownaudiopluginsmigrationregister.h} | 26 +- .../internal/knownaudiopluginsregister.cpp | 151 ++++-- .../internal/knownaudiopluginsregister.h | 14 +- .../internal/registeraudiopluginsscenario.cpp | 173 +++++-- .../internal/registeraudiopluginsscenario.h | 10 +- .../iregisteraudiopluginsscenario.h | 20 +- framework/audioplugins/tests/CMakeLists.txt | 3 +- .../tests/audiopluginsutilstest.cpp | 52 -- ...knownaudiopluginsmigrationregistertest.cpp | 312 ++++++++++++ .../tests/knownaudiopluginsregistertest.cpp | 240 +++++++-- .../tests/mocks/audiopluginmetareadermock.h | 4 +- .../mocks/audiopluginsconfigurationmock.h | 3 + .../knownaudiopluginsmigrationregistermock.h | 35 ++ .../mocks/knownaudiopluginsregistermock.h | 10 +- .../registeraudiopluginsscenariotest.cpp | 457 +++++++++++++----- .../internal/musesamplerresolver.cpp | 2 +- framework/vst/internal/fx/vstfxprocessor.cpp | 2 +- .../vst/internal/synth/vstsynthesiser.cpp | 2 +- framework/vst/internal/vstaudioclient.cpp | 8 +- framework/vst/internal/vstaudioclient.h | 6 +- .../vst/internal/vstmodulesrepository.cpp | 13 +- framework/vst/internal/vstmodulesrepository.h | 3 +- .../vst/internal/vstpluginmetareader.cpp | 6 +- framework/vst/vsttypes.h | 6 + 41 files changed, 1637 insertions(+), 435 deletions(-) create mode 100644 framework/audioplugins/iknownaudiopluginsmigrationregister.h create mode 100644 framework/audioplugins/internal/knownaudiopluginsmigrationregister.cpp rename framework/audioplugins/internal/{audiopluginsutils.h => knownaudiopluginsmigrationregister.h} (55%) create mode 100644 framework/audioplugins/tests/knownaudiopluginsmigrationregistertest.cpp create mode 100644 framework/audioplugins/tests/mocks/knownaudiopluginsmigrationregistermock.h diff --git a/framework/audio/common/audiotypes.h b/framework/audio/common/audiotypes.h index c91ec5ed04..6ffc653d2b 100644 --- a/framework/audio/common/audiotypes.h +++ b/framework/audio/common/audiotypes.h @@ -37,6 +37,8 @@ #include "mpe/events.h" +#include "audioplugins/audiopluginstypes.h" + #include "log.h" namespace muse::audio { @@ -168,15 +170,21 @@ struct AudioEngineConfig { }; using AudioSourceName = std::string; -using AudioResourceId = std::string; -using AudioResourceIdList = std::vector; -using AudioResourceVendor = std::string; -using AudioResourceAttributes = std::map; using AudioUnitConfig = std::map; -static const String PLAYBACK_SETUP_DATA_ATTRIBUTE(u"playbackSetupData"); -static const String CATEGORIES_ATTRIBUTE(u"categories"); - +using AudioResourceId = muse::audioplugins::AudioResourceId; +using AudioResourceIdList = muse::audioplugins::AudioResourceIdList; +using AudioResourceVendor = muse::audioplugins::AudioResourceVendor; +using AudioResourceAttributes = muse::audioplugins::AudioResourceAttributes; +using AudioResourceMeta = muse::audioplugins::AudioResourceMeta; +using AudioResourceMetaList = muse::audioplugins::AudioResourceMetaList; +using AudioResourceMetaSet = muse::audioplugins::AudioResourceMetaSet; + +// audio::AudioResourceType is an audio-engine-internal enum used to dispatch +// synth/fx routing. It is intentionally kept separate from the framework's +// opaque audioplugins::AudioResourceType (a std::string identifier persisted +// by the audioplugins module). Map between them at the boundary via +// resourceTypeFromString() / resourceTypeName(). enum class AudioResourceType { Undefined = -1, FluidSoundfont, @@ -188,6 +196,45 @@ enum class AudioResourceType { NyquistPlugin, }; +static const std::map RESOURCE_TYPE_NAMES = { + { AudioResourceType::FluidSoundfont, "FluidSoundfont" }, + { AudioResourceType::VstPlugin, "VstPlugin" }, + { AudioResourceType::NativeEffect, "NativeEffect" }, + { AudioResourceType::MuseSamplerSoundPack, "MuseSamplerSoundPack" }, + { AudioResourceType::Lv2Plugin, "Lv2Plugin" }, + { AudioResourceType::AudioUnit, "AudioUnit" }, + { AudioResourceType::NyquistPlugin, "NyquistPlugin" }, +}; + +inline const std::string& resourceTypeName(AudioResourceType type) +{ + auto it = RESOURCE_TYPE_NAMES.find(type); + if (it != RESOURCE_TYPE_NAMES.end()) { + return it->second; + } + static const std::string empty; + return empty; +} + +inline AudioResourceType resourceTypeFromString(const std::string& name) +{ + for (const auto& kv : RESOURCE_TYPE_NAMES) { + if (kv.second == name) { + return kv.first; + } + } + return AudioResourceType::Undefined; +} + +static const String PLAYBACK_SETUP_DATA_ATTRIBUTE(u"playbackSetupData"); +static const String CATEGORIES_ATTRIBUTE(u"categories"); +static const String HAS_NATIVE_EDITOR_SUPPORT_ATTRIBUTE(u"hasNativeEditorSupport"); + +inline bool hasNativeEditorSupport(const AudioResourceMeta& meta) +{ + return muse::audioplugins::boolAttribute(meta, HAS_NATIVE_EDITOR_SUPPORT_ATTRIBUTE); +} + static const std::map RESOURCE_TYPE_MAP = { { AudioResourceType::Undefined, "undefined" }, { AudioResourceType::MuseSamplerSoundPack, "muse_sampler_sound_pack" }, @@ -199,55 +246,6 @@ static const std::map RESOURCE_TYPE_MAP = { { AudioResourceType::NyquistPlugin, "nyquist_plugin" }, }; -struct AudioResourceMeta { - AudioResourceId id; - AudioResourceVendor vendor; - AudioResourceAttributes attributes; - AudioResourceType type = AudioResourceType::Undefined; - bool hasNativeEditorSupport = false; - - const String& attributeVal(const String& key) const - { - auto search = attributes.find(key); - if (search != attributes.cend()) { - return search->second; - } - - static String empty; - return empty; - } - - bool isValid() const - { - return !id.empty() - && !vendor.empty() - && type != AudioResourceType::Undefined; - } - - bool operator==(const AudioResourceMeta& other) const - { - return id == other.id - && vendor == other.vendor - && type == other.type - && hasNativeEditorSupport == other.hasNativeEditorSupport - && attributes == other.attributes; - } - - bool operator!=(const AudioResourceMeta& other) const - { - return !(*this == other); - } - - bool operator<(const AudioResourceMeta& other) const - { - return id < other.id - || vendor < other.vendor; - } -}; - -using AudioResourceMetaList = std::vector; -using AudioResourceMetaSet = std::set; - static const AudioResourceId MUSE_REVERB_ID("Muse Reverb"); enum class AudioFxType { @@ -305,7 +303,7 @@ struct AudioFxParams { AudioFxType type() const { - switch (resourceMeta.type) { + switch (resourceTypeFromString(resourceMeta.type)) { case AudioResourceType::VstPlugin: return AudioFxType::VstFx; case AudioResourceType::NativeEffect: return AudioFxType::MuseFx; case AudioResourceType::AudioUnit: @@ -384,9 +382,9 @@ enum class AudioSourceType { MuseSampler }; -inline AudioSourceType sourceTypeFromResourceType(AudioResourceType type) +inline AudioSourceType sourceTypeFromResourceType(const muse::audioplugins::AudioResourceType& metaType) { - switch (type) { + switch (resourceTypeFromString(metaType)) { case AudioResourceType::FluidSoundfont: return AudioSourceType::Fluid; case AudioResourceType::VstPlugin: return AudioSourceType::Vsti; case AudioResourceType::MuseSamplerSoundPack: return AudioSourceType::MuseSampler; diff --git a/framework/audio/common/audioutils.h b/framework/audio/common/audioutils.h index 21129a0dab..4656f590b7 100644 --- a/framework/audio/common/audioutils.h +++ b/framework/audio/common/audioutils.h @@ -30,15 +30,16 @@ inline AudioResourceMeta makeReverbMeta() { AudioResourceMeta meta; meta.id = MUSE_REVERB_ID; - meta.type = AudioResourceType::NativeEffect; + meta.type = "NativeEffect"; meta.vendor = "Muse"; - meta.hasNativeEditorSupport = true; + meta.attributes.emplace(HAS_NATIVE_EDITOR_SUPPORT_ATTRIBUTE, u"true"); return meta; } -inline String audioResourceTypeToString(const AudioResourceType& type) +inline String audioResourceTypeToString(const muse::audioplugins::AudioResourceType& metaType) { + AudioResourceType type = resourceTypeFromString(metaType); auto search = RESOURCE_TYPE_MAP.find(type); if (search != RESOURCE_TYPE_MAP.end()) { @@ -54,7 +55,7 @@ inline String audioSourceName(const AudioInputParams& params) return params.resourceMeta.attributeVal(u"museName"); } - if (params.resourceMeta.type == audio::AudioResourceType::FluidSoundfont) { + if (params.resourceMeta.type == "FluidSoundfont") { const String& presetName = params.resourceMeta.attributeVal(synth::PRESET_NAME_ATTRIBUTE); if (!presetName.empty()) { return presetName; @@ -84,7 +85,7 @@ inline String audioSourceCategoryName(const AudioInputParams& params) return params.resourceMeta.attributeVal(u"museCategory"); } - if (params.resourceMeta.type == audio::AudioResourceType::FluidSoundfont) { + if (params.resourceMeta.type == "FluidSoundfont") { return params.resourceMeta.attributeVal(synth::SOUNDFONT_NAME_ATTRIBUTE); } @@ -109,17 +110,6 @@ inline AudioFxCategories audioFxCategoriesFromString(const String& str) inline bool isOnlineAudioResource(const AudioResourceMeta& meta) { - const String& attr = meta.attributeVal(u"isOnline"); - if (attr.empty()) { - return false; - } - - bool ok = true; - const int val = attr.toInt(&ok); - if (!ok) { - return false; - } - - return val == 1; + return muse::audioplugins::boolAttribute(meta, u"isOnline"); } } diff --git a/framework/audio/common/rpc/rpcpacker.h b/framework/audio/common/rpc/rpcpacker.h index b724fe4d83..de5f43bc2a 100644 --- a/framework/audio/common/rpc/rpcpacker.h +++ b/framework/audio/common/rpc/rpcpacker.h @@ -188,12 +188,12 @@ inline void unpack_custom(muse::msgpack::UnPacker& p, muse::audio::AudioFxCatego inline void pack_custom(muse::msgpack::Packer& p, const muse::audio::AudioResourceMeta& value) { - p.process(value.id, value.type, value.vendor, value.attributes, value.hasNativeEditorSupport); + p.process(value.id, value.type, value.vendor, value.attributes); } inline void unpack_custom(muse::msgpack::UnPacker& p, muse::audio::AudioResourceMeta& value) { - p.process(value.id, value.type, value.vendor, value.attributes, value.hasNativeEditorSupport); + p.process(value.id, value.type, value.vendor, value.attributes); } inline void pack_custom(muse::msgpack::Packer& p, const muse::audio::AudioFxParams& value) diff --git a/framework/audio/engine/internal/audioengineconfiguration.cpp b/framework/audio/engine/internal/audioengineconfiguration.cpp index d6a289fef1..e486c7826b 100644 --- a/framework/audio/engine/internal/audioengineconfiguration.cpp +++ b/framework/audio/engine/internal/audioengineconfiguration.cpp @@ -41,8 +41,7 @@ static const AudioResourceMeta DEFAULT_AUDIO_RESOURCE_META = { DEFAULT_SOUND_FONT_NAME, "Fluid", DEFAULT_AUDIO_RESOURCE_ATTRIBUTES, - AudioResourceType::FluidSoundfont, - false /*hasNativeEditor*/ }; + "FluidSoundfont" }; void AudioEngineConfiguration::setConfig(const AudioEngineConfig& conf) { diff --git a/framework/audio/engine/internal/enginerpccontroller.cpp b/framework/audio/engine/internal/enginerpccontroller.cpp index 468a1c26b6..a8ce339e10 100644 --- a/framework/audio/engine/internal/enginerpccontroller.cpp +++ b/framework/audio/engine/internal/enginerpccontroller.cpp @@ -225,10 +225,10 @@ void EngineRpcController::init() } }; - AudioResourceType resourceType = params.source.resourceMeta.type; + const std::string& resourceType = params.source.resourceMeta.type; // Not Fluid - if (resourceType != AudioResourceType::FluidSoundfont) { + if (resourceType != "FluidSoundfont") { addTrackAndSendResponse(msg, trackName, playbackData, params); return make_response_delayed(msg); } diff --git a/framework/audio/engine/internal/synthesizers/fluidsynth/fluidresolver.cpp b/framework/audio/engine/internal/synthesizers/fluidsynth/fluidresolver.cpp index 42e93214ae..714f06ca02 100644 --- a/framework/audio/engine/internal/synthesizers/fluidsynth/fluidresolver.cpp +++ b/framework/audio/engine/internal/synthesizers/fluidsynth/fluidresolver.cpp @@ -113,13 +113,12 @@ void FluidResolver::refresh() AudioResourceMeta chooseAutomaticMeta; chooseAutomaticMeta.id = id; - chooseAutomaticMeta.type = AudioResourceType::FluidSoundfont; + chooseAutomaticMeta.type = "FluidSoundfont"; chooseAutomaticMeta.vendor = FLUID_VENDOR_NAME; chooseAutomaticMeta.attributes = { { PLAYBACK_SETUP_DATA_ATTRIBUTE, muse::mpe::GENERIC_SETUP_DATA_STRING }, { SOUNDFONT_NAME_ATTRIBUTE, String::fromStdString(soundFont.name) } }; - chooseAutomaticMeta.hasNativeEditorSupport = false; m_resourcesCache.emplace(id, SoundFontResource { soundFont.path, std::nullopt, std::move(chooseAutomaticMeta) }); } @@ -129,7 +128,7 @@ void FluidResolver::refresh() AudioResourceMeta meta; meta.id = id; - meta.type = AudioResourceType::FluidSoundfont; + meta.type = "FluidSoundfont"; meta.vendor = FLUID_VENDOR_NAME; meta.attributes = { { PLAYBACK_SETUP_DATA_ATTRIBUTE, muse::mpe::GENERIC_SETUP_DATA_STRING }, @@ -138,7 +137,6 @@ void FluidResolver::refresh() { PRESET_BANK_ATTRIBUTE, String::number(preset.program.bank) }, { PRESET_PROGRAM_ATTRIBUTE, String::number(preset.program.program) }, }; - meta.hasNativeEditorSupport = false; m_resourcesCache.emplace(id, SoundFontResource { soundFont.path, preset.program, std::move(meta) }); } diff --git a/framework/audio/tests/rpcpacker_tests.cpp b/framework/audio/tests/rpcpacker_tests.cpp index 72d37e38fe..24ba9d2723 100644 --- a/framework/audio/tests/rpcpacker_tests.cpp +++ b/framework/audio/tests/rpcpacker_tests.cpp @@ -90,17 +90,16 @@ TEST_F(Audio_RpcPackerTests, AudioResourceMeta) { AudioResourceMeta origin; origin.id = "1234"; - origin.type = AudioResourceType::NativeEffect; + origin.type = "NativeEffect"; origin.vendor = "muse"; origin.attributes.insert({ u"key", u"val" }); - origin.hasNativeEditorSupport = true; + origin.attributes.insert({ HAS_NATIVE_EDITOR_SUPPORT_ATTRIBUTE, u"true" }); KNOWN_FIELDS(origin, origin.id, origin.type, origin.vendor, - origin.attributes, - origin.hasNativeEditorSupport); + origin.attributes); ByteArray data = rpc::RpcPacker::pack(origin); @@ -112,7 +111,7 @@ TEST_F(Audio_RpcPackerTests, AudioResourceMeta) EXPECT_TRUE(origin.type == unpacked.type); EXPECT_TRUE(origin.vendor == unpacked.vendor); EXPECT_TRUE(origin.attributes == unpacked.attributes); - EXPECT_TRUE(origin.hasNativeEditorSupport == unpacked.hasNativeEditorSupport); + EXPECT_TRUE(hasNativeEditorSupport(origin) == hasNativeEditorSupport(unpacked)); } TEST_F(Audio_RpcPackerTests, AudioResourceMetaList) @@ -124,7 +123,7 @@ TEST_F(Audio_RpcPackerTests, AudioResourceMetaList) for (int i = 0; i < count; ++i) { AudioResourceMeta meta; meta.id = std::to_string(1234567); - meta.type = AudioResourceType::MuseSamplerSoundPack; + meta.type = "MuseSamplerSoundPack"; meta.vendor = "MuseSounds"; meta.attributes = { { u"playbackSetupData", u"instrumentSoundId" }, diff --git a/framework/audioplugins/CMakeLists.txt b/framework/audioplugins/CMakeLists.txt index c0dca622b0..0c3a22b3f5 100644 --- a/framework/audioplugins/CMakeLists.txt +++ b/framework/audioplugins/CMakeLists.txt @@ -27,17 +27,19 @@ target_sources(muse_audioplugins PRIVATE audiopluginserrors.h iaudiopluginsconfiguration.h iknownaudiopluginsregister.h + iknownaudiopluginsmigrationregister.h iaudiopluginsscanner.h iaudiopluginsscannerregister.h iaudiopluginmetareader.h iaudiopluginmetareaderregister.h iregisteraudiopluginsscenario.h - internal/audiopluginsutils.h internal/audiopluginsconfiguration.cpp internal/audiopluginsconfiguration.h internal/knownaudiopluginsregister.cpp internal/knownaudiopluginsregister.h + internal/knownaudiopluginsmigrationregister.cpp + internal/knownaudiopluginsmigrationregister.h internal/audiopluginsscannerregister.cpp internal/audiopluginsscannerregister.h internal/audiopluginmetareaderregister.cpp diff --git a/framework/audioplugins/audiopluginsmodule.cpp b/framework/audioplugins/audiopluginsmodule.cpp index 0f758f60f9..a9b951856d 100644 --- a/framework/audioplugins/audiopluginsmodule.cpp +++ b/framework/audioplugins/audiopluginsmodule.cpp @@ -23,6 +23,7 @@ #include "internal/audiopluginsconfiguration.h" #include "internal/knownaudiopluginsregister.h" +#include "internal/knownaudiopluginsmigrationregister.h" #include "internal/audiopluginsscannerregister.h" #include "internal/audiopluginmetareaderregister.h" #include "internal/registeraudiopluginsscenario.h" @@ -33,7 +34,7 @@ using namespace muse; using namespace muse::modularity; using namespace muse::audioplugins; -static const std::string mname("vst"); +static const std::string mname("audio_plugins"); std::string AudioPluginsModule::moduleName() const { @@ -45,6 +46,7 @@ void AudioPluginsModule::registerExports() m_configuration = std::make_shared(globalCtx()); globalIoc()->registerExport(moduleName(), m_configuration); + globalIoc()->registerExport(moduleName(), std::make_shared()); globalIoc()->registerExport(moduleName(), std::make_shared()); globalIoc()->registerExport(moduleName(), std::make_shared()); globalIoc()->registerExport(moduleName(), std::make_shared()); diff --git a/framework/audioplugins/audiopluginstypes.h b/framework/audioplugins/audiopluginstypes.h index 355fde48ed..a1881a4fd7 100644 --- a/framework/audioplugins/audiopluginstypes.h +++ b/framework/audioplugins/audiopluginstypes.h @@ -21,21 +21,150 @@ */ #pragma once +#include +#include +#include +#include + #include "global/io/path.h" -#include "audio/common/audiotypes.h" +#include "global/types/string.h" namespace muse::audioplugins { -enum class AudioPluginType { +using AudioResourceId = std::string; +using AudioResourceIdList = std::vector; +using AudioResourceVendor = std::string; +using AudioResourceAttributes = std::map; + +// Opaque app-defined plugin format identifier (e.g. "VstPlugin", "Lv2Plugin", +// or any string the embedding app cares about). The framework persists it as-is +// and never inspects the value. Apps map between this string and their own +// engine-side type concepts at the boundary. +using AudioResourceType = std::string; + +struct AudioResourceMeta { + AudioResourceId id; + AudioResourceVendor vendor; + AudioResourceAttributes attributes; + AudioResourceType type; + + const String& attributeVal(const String& key) const + { + auto search = attributes.find(key); + if (search != attributes.cend()) { + return search->second; + } + + static String empty; + return empty; + } + + bool isValid() const + { + return !id.empty() + && !vendor.empty() + && !type.empty(); + } + + bool operator==(const AudioResourceMeta& other) const + { + return id == other.id + && vendor == other.vendor + && type == other.type + && attributes == other.attributes; + } + + bool operator!=(const AudioResourceMeta& other) const + { + return !(*this == other); + } + + bool operator<(const AudioResourceMeta& other) const + { + return id < other.id + || vendor < other.vendor; + } +}; + +using AudioResourceMetaList = std::vector; +using AudioResourceMetaSet = std::set; + +// Typed accessors over the string-encoded attributes map. Cheap alternative to +// holding a typed variant in the storage itself; encodes the on-disk convention +// ("true"/"1" → true) in one place so callers don't reimplement it. +inline bool boolAttribute(const AudioResourceMeta& meta, const String& key, bool fallback = false) +{ + const String& v = meta.attributeVal(key); + if (v.empty()) { + return fallback; + } + return v == u"true" || v == u"1"; +} + +inline int intAttribute(const AudioResourceMeta& meta, const String& key, int fallback = 0) +{ + const String& v = meta.attributeVal(key); + if (v.empty()) { + return fallback; + } + bool ok = true; + int n = v.toInt(&ok); + return ok ? n : fallback; +} + +// Lifecycle state of a plugin entry in the cache. +// Discovered: scanner found the file at `path`; validation has not yet completed. +// (Reserved: no producer in the framework yet — apps that want a +// pre-validation insertion flow can transition to this state.) +// Validated: validation succeeded; the plugin is usable. +// Missing: `path` was known previously but the scanner no longer finds it. +// Error: validation failed; `errorCode` carries the details. +enum class AudioPluginState { Undefined = -1, - Instrument, - Fx, + Discovered, + Validated, + Missing, + Error, }; +namespace detail { +inline const std::map& audioPluginStateNames() +{ + static const std::map NAMES = { + { AudioPluginState::Undefined, "Undefined" }, + { AudioPluginState::Discovered, "Discovered" }, + { AudioPluginState::Validated, "Validated" }, + { AudioPluginState::Missing, "Missing" }, + { AudioPluginState::Error, "Error" }, + }; + return NAMES; +} +} + +inline const std::string& audioPluginStateName(AudioPluginState state) +{ + const auto& names = detail::audioPluginStateNames(); + auto it = names.find(state); + if (it != names.end()) { + return it->second; + } + static const std::string empty; + return empty; +} + +inline AudioPluginState audioPluginStateFromName(const std::string& name) +{ + for (const auto& kv : detail::audioPluginStateNames()) { + if (kv.second == name) { + return kv.first; + } + } + return AudioPluginState::Undefined; +} + struct AudioPluginInfo { - AudioPluginType type = AudioPluginType::Undefined; - audio::AudioResourceMeta meta; + AudioResourceMeta meta; io::path_t path; - bool enabled = false; + AudioPluginState state = AudioPluginState::Undefined; int errorCode = 0; }; diff --git a/framework/audioplugins/iaudiopluginmetareader.h b/framework/audioplugins/iaudiopluginmetareader.h index ddd212ff6b..c1a1fb2dfc 100644 --- a/framework/audioplugins/iaudiopluginmetareader.h +++ b/framework/audioplugins/iaudiopluginmetareader.h @@ -24,7 +24,7 @@ #include "global/types/retval.h" #include "global/io/path.h" -#include "audio/common/audiotypes.h" +#include "audiopluginstypes.h" namespace muse::audioplugins { class IAudioPluginMetaReader @@ -32,9 +32,9 @@ class IAudioPluginMetaReader public: virtual ~IAudioPluginMetaReader() = default; - virtual audio::AudioResourceType metaType() const = 0; + virtual AudioResourceType metaType() const = 0; virtual bool canReadMeta(const io::path_t& pluginPath) const = 0; - virtual RetVal readMeta(const io::path_t& pluginPath) const = 0; + virtual RetVal readMeta(const io::path_t& pluginPath) const = 0; }; using IAudioPluginMetaReaderPtr = std::shared_ptr; diff --git a/framework/audioplugins/iaudiopluginsconfiguration.h b/framework/audioplugins/iaudiopluginsconfiguration.h index 7c7d83df9a..f94e6579a9 100644 --- a/framework/audioplugins/iaudiopluginsconfiguration.h +++ b/framework/audioplugins/iaudiopluginsconfiguration.h @@ -25,6 +25,8 @@ #include "global/io/path.h" +#include "audiopluginstypes.h" + namespace muse::audioplugins { class IAudioPluginsConfiguration : MODULE_GLOBAL_INTERFACE { @@ -34,5 +36,12 @@ class IAudioPluginsConfiguration : MODULE_GLOBAL_INTERFACE virtual ~IAudioPluginsConfiguration() = default; virtual io::path_t knownAudioPluginsFilePath() const = 0; + + // Attributes the framework treats as runtime-only: never persisted on save, + // and re-injected (with the supplied default value) on load. The framework + // is otherwise oblivious to their meaning. Apps register their own runtime + // attributes (e.g. MuseScore registers playbackSetupData) at startup. + virtual const AudioResourceAttributes& runtimeAttributeDefaults() const = 0; + virtual void setRuntimeAttributeDefaults(const AudioResourceAttributes& defaults) = 0; }; } diff --git a/framework/audioplugins/iknownaudiopluginsmigrationregister.h b/framework/audioplugins/iknownaudiopluginsmigrationregister.h new file mode 100644 index 0000000000..3ff2d075ae --- /dev/null +++ b/framework/audioplugins/iknownaudiopluginsmigrationregister.h @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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, see . + */ +#pragma once + +#include + +#include "modularity/imoduleinterface.h" + +#include "global/serialization/json.h" +#include "global/types/ret.h" + +namespace muse::audioplugins { +inline constexpr int CURRENT_KNOWN_AUDIO_PLUGINS_VERSION = 3; + +class IKnownAudioPluginsMigrationRegister : MODULE_GLOBAL_INTERFACE +{ + INTERFACE_ID(IKnownAudioPluginsMigrationRegister) + +public: + virtual ~IKnownAudioPluginsMigrationRegister() = default; + + using PluginsMigration = std::function; + + virtual void registerMigration(int fromVersion, PluginsMigration cb) = 0; + virtual Ret migrate(int fromVersion, int toVersion, JsonArray& plugins) const = 0; +}; +} diff --git a/framework/audioplugins/iknownaudiopluginsregister.h b/framework/audioplugins/iknownaudiopluginsregister.h index ee90080626..3f110ee922 100644 --- a/framework/audioplugins/iknownaudiopluginsregister.h +++ b/framework/audioplugins/iknownaudiopluginsregister.h @@ -44,12 +44,20 @@ class IKnownAudioPluginsRegister : MODULE_GLOBAL_INTERFACE virtual AudioPluginInfoList pluginInfoList(PluginInfoAccepted accepted = PluginInfoAccepted()) const = 0; virtual muse::async::Notification pluginInfoListChanged() const = 0; - virtual const io::path_t& pluginPath(const audio::AudioResourceId& resourceId) const = 0; + virtual const io::path_t& pluginPath(const AudioResourceId& resourceId) const = 0; virtual bool exists(const io::path_t& pluginPath) const = 0; - virtual bool exists(const audio::AudioResourceId& resourceId) const = 0; + virtual bool exists(const AudioResourceId& resourceId) const = 0; virtual Ret registerPlugins(const AudioPluginInfoList& list) = 0; - virtual Ret unregisterPlugins(const audio::AudioResourceIdList& resourceIds) = 0; + virtual Ret unregisterPlugins(const AudioResourceIdList& resourceIds) = 0; + + virtual Ret setPluginsState(const AudioResourceIdList& resourceIds, AudioPluginState state) = 0; + + // Erase every entry whose `path` matches. Used to clear a Discovered + // placeholder before its (re)validation result is written, so a + // multi-effect plugin's real-id entries can replace the basename-id + // placeholder without orphaning it. + virtual Ret removePluginsAtPath(const io::path_t& path) = 0; }; } diff --git a/framework/audioplugins/internal/audiopluginsconfiguration.cpp b/framework/audioplugins/internal/audiopluginsconfiguration.cpp index 07494234a1..a2acb7ebd2 100644 --- a/framework/audioplugins/internal/audiopluginsconfiguration.cpp +++ b/framework/audioplugins/internal/audiopluginsconfiguration.cpp @@ -28,3 +28,13 @@ io::path_t AudioPluginsConfiguration::knownAudioPluginsFilePath() const { return globalConfiguration()->userAppDataPath() + "/known_audio_plugins.json"; } + +const AudioResourceAttributes& AudioPluginsConfiguration::runtimeAttributeDefaults() const +{ + return m_runtimeAttributeDefaults; +} + +void AudioPluginsConfiguration::setRuntimeAttributeDefaults(const AudioResourceAttributes& defaults) +{ + m_runtimeAttributeDefaults = defaults; +} diff --git a/framework/audioplugins/internal/audiopluginsconfiguration.h b/framework/audioplugins/internal/audiopluginsconfiguration.h index be721c131e..5d27a12df9 100644 --- a/framework/audioplugins/internal/audiopluginsconfiguration.h +++ b/framework/audioplugins/internal/audiopluginsconfiguration.h @@ -37,5 +37,11 @@ class AudioPluginsConfiguration : public IAudioPluginsConfiguration, public muse : Contextable(iocCtx) {} io::path_t knownAudioPluginsFilePath() const override; + + const AudioResourceAttributes& runtimeAttributeDefaults() const override; + void setRuntimeAttributeDefaults(const AudioResourceAttributes& defaults) override; + +private: + AudioResourceAttributes m_runtimeAttributeDefaults; }; } diff --git a/framework/audioplugins/internal/knownaudiopluginsmigrationregister.cpp b/framework/audioplugins/internal/knownaudiopluginsmigrationregister.cpp new file mode 100644 index 0000000000..43c46f2585 --- /dev/null +++ b/framework/audioplugins/internal/knownaudiopluginsmigrationregister.cpp @@ -0,0 +1,104 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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, see . + */ + +#include "knownaudiopluginsmigrationregister.h" + +#include "log.h" + +using namespace muse; +using namespace muse::audioplugins; + +KnownAudioPluginsMigrationRegister::KnownAudioPluginsMigrationRegister() +{ + // v0 -> v1: structural change — the envelope `{version, plugins}` is + // introduced. The bare-array → envelope wrapping is handled at load() + // time; this callback is a no-op that just marks the version transition. + registerMigration(0, [](const JsonArray& plugins) { + return plugins; + }); + + // v1 -> v2: `enabled` boolean replaced by `state` string. Owned by the + // framework because AudioPluginState is a framework enum. + registerMigration(1, [](const JsonArray& plugins) { + JsonArray out; + for (size_t i = 0; i < plugins.size(); ++i) { + JsonObject obj = plugins.at(i).toObject(); + const bool enabled = obj.value("enabled").toBool(); + obj.set("state", audioPluginStateName(enabled ? AudioPluginState::Validated + : AudioPluginState::Error)); + + JsonObject rebuilt; + for (const std::string& k : obj.keys()) { + if (k == "enabled") { + continue; + } + rebuilt.set(k, obj.value(k)); + } + out << rebuilt; + } + return out; + }); + + // v2 -> v3: an app-specific step (MuseScore lifts `hasNativeEditorSupport` + // into meta.attributes). The framework registers a no-op default so a v2 + // cache can always reach CURRENT_KNOWN_AUDIO_PLUGINS_VERSION even when the + // app has no v2->v3 work; apps that need it override this slot via + // registerMigration(2, ...). + registerMigration(2, [](const JsonArray& plugins) { + return plugins; + }); +} + +void KnownAudioPluginsMigrationRegister::registerMigration(int fromVersion, PluginsMigration cb) +{ + m_migrations[fromVersion] = std::move(cb); +} + +Ret KnownAudioPluginsMigrationRegister::migrate(int fromVersion, int toVersion, JsonArray& plugins) const +{ + if (fromVersion == toVersion) { + return make_ok(); + } + + if (fromVersion > toVersion) { + return Ret(static_cast(Ret::Code::UnknownError), + "cache file version " + std::to_string(fromVersion) + + " is newer than this build's expected version " + + std::to_string(toVersion) + + " (no downgrade). Delete the file or upgrade the build."); + } + + for (int v = fromVersion; v < toVersion; ++v) { + auto it = m_migrations.find(v); + if (it == m_migrations.end()) { + return Ret(static_cast(Ret::Code::UnknownError), + "missing migrator for cache version step " + + std::to_string(v) + " -> " + std::to_string(v + 1) + + ". If you just bumped CURRENT_KNOWN_AUDIO_PLUGINS_VERSION, " + + "register a migrator in your app's AudioPluginsAppConfigModule."); + } + + plugins = it->second(plugins); + } + + return make_ok(); +} diff --git a/framework/audioplugins/internal/audiopluginsutils.h b/framework/audioplugins/internal/knownaudiopluginsmigrationregister.h similarity index 55% rename from framework/audioplugins/internal/audiopluginsutils.h rename to framework/audioplugins/internal/knownaudiopluginsmigrationregister.h index 1defa30cb0..ab462c637c 100644 --- a/framework/audioplugins/internal/audiopluginsutils.h +++ b/framework/audioplugins/internal/knownaudiopluginsmigrationregister.h @@ -5,7 +5,7 @@ * MuseScore Studio * Music Composition & Notation * - * Copyright (C) 2021 MuseScore Limited and others + * Copyright (C) 2026 MuseScore Limited and others * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -21,22 +21,20 @@ */ #pragma once -#include "../audiopluginstypes.h" +#include + +#include "../iknownaudiopluginsmigrationregister.h" namespace muse::audioplugins { -inline AudioPluginType audioPluginTypeFromCategoriesString(const String& categoriesStr) +class KnownAudioPluginsMigrationRegister : public IKnownAudioPluginsMigrationRegister { - static const std::vector > STRING_TO_PLUGIN_TYPE_LIST = { - { u"Instrument", AudioPluginType::Instrument }, - { u"Fx", AudioPluginType::Fx }, - }; +public: + KnownAudioPluginsMigrationRegister(); - for (auto it = STRING_TO_PLUGIN_TYPE_LIST.cbegin(); it != STRING_TO_PLUGIN_TYPE_LIST.cend(); ++it) { - if (categoriesStr.contains(it->first)) { - return it->second; - } - } + void registerMigration(int fromVersion, PluginsMigration cb) override; + Ret migrate(int fromVersion, int toVersion, JsonArray& plugins) const override; - return AudioPluginType::Undefined; -} +private: + std::map m_migrations; +}; } diff --git a/framework/audioplugins/internal/knownaudiopluginsregister.cpp b/framework/audioplugins/internal/knownaudiopluginsregister.cpp index a6e10cb39e..9919fc855f 100644 --- a/framework/audioplugins/internal/knownaudiopluginsregister.cpp +++ b/framework/audioplugins/internal/knownaudiopluginsregister.cpp @@ -24,30 +24,19 @@ #include "global/serialization/json.h" -#include "audio/common/audiotypes.h" -#include "audiopluginsutils.h" - #include "log.h" using namespace muse; using namespace muse::audioplugins; -using namespace muse::audio; namespace muse::audioplugins { -static const std::map RESOURCE_TYPE_TO_STRING_MAP { - { audio::AudioResourceType::VstPlugin, "VstPlugin" }, - { audio::AudioResourceType::Lv2Plugin, "Lv2Plugin" }, - { audio::AudioResourceType::AudioUnit, "AudioUnit" }, - { audio::AudioResourceType::NyquistPlugin, "NyquistPlugin" }, - { audio::AudioResourceType::NativeEffect, "NativeEffect" }, -}; - -static JsonObject attributesToJson(const AudioResourceAttributes& attributes) +static JsonObject attributesToJson(const AudioResourceAttributes& attributes, + const AudioResourceAttributes& runtimeOnly) { JsonObject result; for (auto it = attributes.cbegin(); it != attributes.cend(); ++it) { - if (it->first == audio::PLAYBACK_SETUP_DATA_ATTRIBUTE) { + if (runtimeOnly.find(it->first) != runtimeOnly.cend()) { continue; } @@ -57,19 +46,18 @@ static JsonObject attributesToJson(const AudioResourceAttributes& attributes) return result; } -static JsonObject metaToJson(const AudioResourceMeta& meta) +static JsonObject metaToJson(const AudioResourceMeta& meta, const AudioResourceAttributes& runtimeOnly) { JsonObject result; result.set("id", meta.id); - result.set("type", muse::value(RESOURCE_TYPE_TO_STRING_MAP, meta.type, "Undefined")); - result.set("hasNativeEditorSupport", meta.hasNativeEditorSupport); + result.set("type", meta.type); if (!meta.vendor.empty()) { result.set("vendor", meta.vendor); } - JsonObject attributesJson = attributesToJson(meta.attributes); + JsonObject attributesJson = attributesToJson(meta.attributes, runtimeOnly); if (!attributesJson.empty()) { result.set("attributes", attributesJson); } @@ -93,9 +81,8 @@ static AudioResourceMeta metaFromJson(const JsonObject& object) AudioResourceMeta result; result.id = object.value("id").toStdString(); - result.type = muse::key(RESOURCE_TYPE_TO_STRING_MAP, object.value("type").toStdString()); + result.type = object.value("type").toStdString(); result.vendor = object.value("vendor").toStdString(); - result.hasNativeEditorSupport = object.value("hasNativeEditorSupport").toBool(); JsonValue attributes = object.value("attributes"); if (attributes.isObject()) { @@ -122,28 +109,73 @@ Ret KnownAudioPluginsRegister::load() RetVal file = fileSystem()->readFile(knownAudioPluginsPath); if (!file.ret) { + LOGE() << "Failed to read known-audio-plugins cache " + << knownAudioPluginsPath << ": " << file.ret.toString(); return file.ret; } std::string err; JsonDocument json = JsonDocument::fromJson(file.val, &err); if (!err.empty()) { + LOGE() << "Failed to parse known-audio-plugins cache " + << knownAudioPluginsPath << ": " << err; return Ret(static_cast(Ret::Code::UnknownError), err); } - JsonArray array = json.rootArray(); + JsonArray array; + int fileVersion = 0; + + if (json.isArray()) { + // Legacy format: bare array, treated as version 0. + array = json.rootArray(); + } else if (json.isObject()) { + JsonObject root = json.rootObject(); + const JsonValue versionVal = root.value("version"); + const JsonValue pluginsVal = root.value("plugins"); + if (!versionVal.isNumber() || !pluginsVal.isArray()) { + LOGE() << "Malformed known-audio-plugins.json envelope at " + << knownAudioPluginsPath << " (expected numeric \"version\" and array \"plugins\")"; + return Ret(static_cast(Ret::Code::UnknownError), "Malformed known_audio_plugins.json envelope"); + } + fileVersion = versionVal.toInt(); + array = pluginsVal.toArray(); + } else { + LOGE() << "Unrecognized known-audio-plugins.json root type at " + << knownAudioPluginsPath << " (expected array or object)"; + return Ret(static_cast(Ret::Code::UnknownError), "Unrecognized known_audio_plugins.json root type"); + } + + Ret migrationRet = migrations()->migrate(fileVersion, CURRENT_KNOWN_AUDIO_PLUGINS_VERSION, array); + if (!migrationRet) { + LOGE() << "Failed to migrate known-audio-plugins cache from v" << fileVersion + << " to v" << CURRENT_KNOWN_AUDIO_PLUGINS_VERSION + << ": " << migrationRet.toString(); + return migrationRet; + } for (size_t i = 0; i < array.size(); ++i) { JsonObject object = array.at(i).toObject(); AudioPluginInfo info; info.meta = metaFromJson(object.value("meta").toObject()); - info.meta.attributes.emplace(audio::PLAYBACK_SETUP_DATA_ATTRIBUTE, mpe::GENERIC_SETUP_DATA_STRING); - info.type = audioPluginTypeFromCategoriesString(info.meta.attributeVal(audio::CATEGORIES_ATTRIBUTE)); + for (const auto& kv : configuration()->runtimeAttributeDefaults()) { + // Assign, don't emplace: runtime-only defaults must override any + // stale value a legacy cache still carries for the same key. + info.meta.attributes[kv.first] = kv.second; + } info.path = object.value("path").toString(); - info.enabled = object.value("enabled").toBool(); + info.state = audioPluginStateFromName(object.value("state").toStdString()); info.errorCode = object.value("errorCode").toInt(); + // `id` and `path` are the register's lookup keys — a row missing + // either (e.g. a truncated `meta`) would poison m_pluginInfoMap with + // an empty-key record. Reject the whole file, as with a bad envelope. + if (info.meta.id.empty() || info.path.empty()) { + LOGE() << "Malformed known-audio-plugins entry at " + << knownAudioPluginsPath << " (missing id or path)"; + return Ret(static_cast(Ret::Code::UnknownError), "Malformed known_audio_plugins.json entry"); + } + m_pluginPaths.insert(info.path); m_pluginInfoMap.emplace(info.meta.id, std::move(info)); } @@ -230,6 +262,34 @@ Ret KnownAudioPluginsRegister::registerPlugins(const AudioPluginInfoList& list) return make_ok(); } +Ret KnownAudioPluginsRegister::setPluginsState(const AudioResourceIdList& resourceIds, AudioPluginState state) +{ + IF_ASSERT_FAILED(m_loaded) { + return false; + } + + if (resourceIds.empty()) { + return make_ok(); + } + + bool changed = false; + for (const AudioResourceId& resourceId : resourceIds) { + auto range = m_pluginInfoMap.equal_range(resourceId); + for (auto it = range.first; it != range.second; ++it) { + if (it->second.state != state) { + it->second.state = state; + changed = true; + } + } + } + + if (!changed) { + return make_ok(); + } + + return writePluginsInfo(); +} + Ret KnownAudioPluginsRegister::unregisterPlugins(const AudioResourceIdList& resourceIds) { IF_ASSERT_FAILED(m_loaded) { @@ -264,19 +324,52 @@ Ret KnownAudioPluginsRegister::unregisterPlugins(const AudioResourceIdList& reso return make_ok(); } +Ret KnownAudioPluginsRegister::removePluginsAtPath(const io::path_t& path) +{ + IF_ASSERT_FAILED(m_loaded) { + return false; + } + + bool removed = false; + for (auto it = m_pluginInfoMap.begin(); it != m_pluginInfoMap.end();) { + if (it->second.path == path) { + it = m_pluginInfoMap.erase(it); + removed = true; + } else { + ++it; + } + } + + if (!removed) { + return make_ok(); + } + + muse::remove(m_pluginPaths, path); + + return writePluginsInfo(); +} + Ret KnownAudioPluginsRegister::writePluginsInfo() { TRACEFUNC; JsonArray array; + const AudioResourceAttributes& runtimeOnly = configuration()->runtimeAttributeDefaults(); + for (const auto& pair : m_pluginInfoMap) { const AudioPluginInfo& info = pair.second; JsonObject obj; - obj.set("meta", metaToJson(info.meta)); + obj.set("meta", metaToJson(info.meta, runtimeOnly)); obj.set("path", info.path.toStdString()); - obj.set("enabled", info.enabled); + + // Omit the state field for Undefined entries — a written entry always + // carries a concrete state. load() reads a missing "state" back as + // Undefined, so this round-trips. + if (info.state != AudioPluginState::Undefined) { + obj.set("state", audioPluginStateName(info.state)); + } if (info.errorCode != 0) { obj.set("errorCode", info.errorCode); @@ -285,8 +378,12 @@ Ret KnownAudioPluginsRegister::writePluginsInfo() array << obj; } + JsonObject root; + root.set("version", CURRENT_KNOWN_AUDIO_PLUGINS_VERSION); + root.set("plugins", array); + io::path_t knownAudioPluginsPath = configuration()->knownAudioPluginsFilePath(); - Ret ret = fileSystem()->writeFile(knownAudioPluginsPath, JsonDocument(array).toJson()); + Ret ret = fileSystem()->writeFile(knownAudioPluginsPath, JsonDocument(root).toJson()); return ret; } diff --git a/framework/audioplugins/internal/knownaudiopluginsregister.h b/framework/audioplugins/internal/knownaudiopluginsregister.h index 036fdb9236..460385d3de 100644 --- a/framework/audioplugins/internal/knownaudiopluginsregister.h +++ b/framework/audioplugins/internal/knownaudiopluginsregister.h @@ -26,12 +26,14 @@ #include "global/io/ifilesystem.h" #include "../iknownaudiopluginsregister.h" +#include "../iknownaudiopluginsmigrationregister.h" #include "../iaudiopluginsconfiguration.h" namespace muse::audioplugins { class KnownAudioPluginsRegister : public IKnownAudioPluginsRegister { GlobalInject configuration; + GlobalInject migrations; GlobalInject fileSystem; friend class AudioPlugins_KnownAudioPluginsRegisterTest; @@ -44,19 +46,23 @@ class KnownAudioPluginsRegister : public IKnownAudioPluginsRegister AudioPluginInfoList pluginInfoList(PluginInfoAccepted accepted = PluginInfoAccepted()) const override; muse::async::Notification pluginInfoListChanged() const override; - const io::path_t& pluginPath(const audio::AudioResourceId& resourceId) const override; + const io::path_t& pluginPath(const AudioResourceId& resourceId) const override; bool exists(const io::path_t& pluginPath) const override; - bool exists(const audio::AudioResourceId& resourceId) const override; + bool exists(const AudioResourceId& resourceId) const override; Ret registerPlugins(const AudioPluginInfoList& list) override; - Ret unregisterPlugins(const audio::AudioResourceIdList& resourceIds) override; + Ret unregisterPlugins(const AudioResourceIdList& resourceIds) override; + + Ret setPluginsState(const AudioResourceIdList& resourceIds, AudioPluginState state) override; + + Ret removePluginsAtPath(const io::path_t& path) override; private: Ret writePluginsInfo(); async::Notification m_pluginInfoListChanged; bool m_loaded = false; - std::multimap m_pluginInfoMap; + std::multimap m_pluginInfoMap; std::set m_pluginPaths; }; } diff --git a/framework/audioplugins/internal/registeraudiopluginsscenario.cpp b/framework/audioplugins/internal/registeraudiopluginsscenario.cpp index 3008d64c61..0028c43212 100644 --- a/framework/audioplugins/internal/registeraudiopluginsscenario.cpp +++ b/framework/audioplugins/internal/registeraudiopluginsscenario.cpp @@ -23,17 +23,17 @@ #include "registeraudiopluginsscenario.h" #include -#include +#include +#include +#include #include "global/translation.h" #include "audiopluginserrors.h" -#include "audiopluginsutils.h" #include "log.h" using namespace muse; -using namespace muse::audio; using namespace muse::audioplugins; void RegisterAudioPluginsScenario::init() @@ -55,45 +55,91 @@ PluginScanResult RegisterAudioPluginsScenario::scanPlugins(Progress* progress) c TRACEFUNC; PluginScanResult result; - std::set scannedPaths; - for (const IAudioPluginsScannerPtr& scanner : scannerRegister()->scanners()) { - for (const io::path_t& path : scanner->scanPlugins(progress)) { - if (!knownPluginsRegister()->exists(path)) { + struct CacheEntry { + AudioResourceId id; + AudioPluginState state; + }; + // A single binary path can host several plugin IDs (shell / multi-effect + // bundles), so every cached entry under a path must be tracked. + std::map > registered; + for (const auto& info : knownPluginsRegister()->pluginInfoList()) { + registered[info.path].push_back({ info.meta.id, info.state }); + } + + for (const auto& scanner : scannerRegister()->scanners()) { + for (const auto& path : scanner->scanPlugins(progress)) { + auto it = registered.find(path); + if (it == registered.end()) { + result.newPluginPaths.push_back(path); + continue; + } + + const std::vector& entries = it->second; + + // A Discovered placeholder means a prior run was interrupted + // before this path finished validating — re-validate the path. + const bool hasDiscovered = std::any_of(entries.cbegin(), entries.cend(), + [](const CacheEntry& e) { + return e.state == AudioPluginState::Discovered; + }); + if (hasDiscovered) { result.newPluginPaths.push_back(path); + registered.erase(it); + continue; } - scannedPaths.insert(path); + // Every formerly Missing ID under this path is rediscovered. + for (const CacheEntry& entry : entries) { + if (entry.state == AudioPluginState::Missing) { + result.rediscoveredPluginIds.push_back(entry.id); + } + } + registered.erase(it); } } - for (const AudioPluginInfo& info : knownPluginsRegister()->pluginInfoList()) { - if (!muse::contains(scannedPaths, info.path)) { - result.missingPluginIds.push_back(info.meta.id); + // Paths no scanner reports anymore: every entry under them is missing. + for (const auto& [path, entries] : registered) { + for (const CacheEntry& entry : entries) { + // Skip entries already Missing — only transitions need a state change. + if (entry.state != AudioPluginState::Missing) { + result.missingPluginIds.push_back(entry.id); + } } } return result; } -void RegisterAudioPluginsScenario::updatePluginsRegistry() +Ret RegisterAudioPluginsScenario::updatePluginsRegistry() { TRACEFUNC; - const PluginScanResult result = scanPlugins(); + PluginScanResult result = scanPlugins(); - Ret ret = unregisterRemovedPlugins(result.missingPluginIds); + Ret ret = knownPluginsRegister()->setPluginsState(result.missingPluginIds, AudioPluginState::Missing); if (!ret) { - LOGE() << "Failed to unregister plugins: " << ret.toString(); + LOGE() << "Failed to mark missing plugins: " << ret.toString(); + return ret; } - ret = registerNewPlugins(result.newPluginPaths); + ret = knownPluginsRegister()->setPluginsState(result.rediscoveredPluginIds, AudioPluginState::Validated); if (!ret) { - LOGE() << "Failed to register plugins: " << ret.toString(); + LOGE() << "Failed to mark rediscovered plugins: " << ret.toString(); + return ret; } + + ret = registerNewPlugins(result.newPluginPaths, /*validate*/ true); + if (!ret) { + LOGE() << "Failed to register new plugins: " << ret.toString(); + return ret; + } + + return knownPluginsRegister()->load(); } -Ret RegisterAudioPluginsScenario::registerNewPlugins(const io::paths_t& pluginPaths) +Ret RegisterAudioPluginsScenario::registerNewPlugins(const io::paths_t& pluginPaths, bool validate) { TRACEFUNC; @@ -101,11 +147,44 @@ Ret RegisterAudioPluginsScenario::registerNewPlugins(const io::paths_t& pluginPa return make_ok(); } - processPluginsRegistration(pluginPaths); + Ret ret = persistDiscoveredPlaceholders(pluginPaths); + if (!ret) { + return ret; + } + + if (validate) { + processPluginsRegistration(pluginPaths); + } + return knownPluginsRegister()->load(); } -Ret RegisterAudioPluginsScenario::unregisterRemovedPlugins(const audio::AudioResourceIdList& pluginIds) +Ret RegisterAudioPluginsScenario::persistDiscoveredPlaceholders(const io::paths_t& pluginPaths) +{ + // Persist Discovered placeholders so scanPlugins() can re-validate them + // on the next launch — if the subprocess crashes mid-scan, or if the + // caller passed validate=false to defer validation. Clear any prior entry + // at the path first to avoid the same-id-same-path assertion when + // auto-resuming an interrupted run. + AudioPluginInfoList placeholders; + placeholders.reserve(pluginPaths.size()); + for (const io::path_t& path : pluginPaths) { + Ret ret = knownPluginsRegister()->removePluginsAtPath(path); + if (!ret) { + return ret; + } + + AudioPluginInfo info; + info.meta.id = io::completeBasename(path).toStdString(); + info.meta.type = metaType(path); + info.path = path; + info.state = AudioPluginState::Discovered; + placeholders.emplace_back(std::move(info)); + } + return knownPluginsRegister()->registerPlugins(placeholders); +} + +Ret RegisterAudioPluginsScenario::unregisterRemovedPlugins(const AudioResourceIdList& pluginIds) { TRACEFUNC; @@ -113,7 +192,12 @@ Ret RegisterAudioPluginsScenario::unregisterRemovedPlugins(const audio::AudioRes return make_ok(); } - return knownPluginsRegister()->unregisterPlugins(pluginIds); + Ret ret = knownPluginsRegister()->unregisterPlugins(pluginIds); + if (!ret) { + LOGE() << "Failed to unregister removed plugins: " << ret.toString(); + } + + return ret; } void RegisterAudioPluginsScenario::processPluginsRegistration(const io::paths_t& pluginPaths) @@ -123,8 +207,8 @@ void RegisterAudioPluginsScenario::processPluginsRegistration(const io::paths_t& m_aborted = false; m_progress.start(); - const std::string appPath = globalConfiguration()->appBinPath().toStdString(); - const int64_t pluginCount = static_cast(pluginPaths.size()); + std::string appPath = globalConfiguration()->appBinPath().toStdString(); + int64_t pluginCount = static_cast(pluginPaths.size()); for (int64_t i = 0; i < pluginCount; ++i) { if (m_aborted) { @@ -132,11 +216,16 @@ void RegisterAudioPluginsScenario::processPluginsRegistration(const io::paths_t& } const io::path_t& pluginPath = pluginPaths[i]; - const std::string pluginPathStr = pluginPath.toStdString(); + std::string pluginPathStr = pluginPath.toStdString(); m_progress.progress(i, pluginCount, io::filename(pluginPath).toStdString()); qApp->processEvents(); + // The subprocess clears its own Discovered placeholder via + // registerPlugin / registerFailedPlugin. Removing it here would + // operate on the main process's stale in-memory register and + // clobber the entries previous subprocesses already wrote. + LOGD() << "--register-audio-plugin " << pluginPathStr; int code = process()->execute(appPath, { "--register-audio-plugin", pluginPathStr }); if (code != 0) { code = process()->execute(appPath, { "--register-failed-audio-plugin", pluginPathStr, "--", std::to_string(code) }); @@ -158,6 +247,17 @@ Ret RegisterAudioPluginsScenario::registerPlugin(const io::path_t& pluginPath) return false; } + // Clear any prior Discovered placeholder at this path so the real + // validated metadata can be registered without tripping the + // same-id-same-path guard in registerPlugins(). Subprocess-side: the + // process just load()ed, so the register reflects what's on disk. + Ret ret = knownPluginsRegister()->removePluginsAtPath(pluginPath); + if (!ret) { + LOGE() << "Failed to clear existing entry at " << pluginPath.toStdString() + << ": " << ret.toString(); + return ret; + } + const IAudioPluginMetaReaderPtr reader = metaReader(pluginPath); if (!reader) { return make_ret(Err::UnknownPluginType); @@ -174,15 +274,13 @@ Ret RegisterAudioPluginsScenario::registerPlugin(const io::path_t& pluginPath) for (const AudioResourceMeta& meta : metaList.val) { AudioPluginInfo info; - info.type = audioPluginTypeFromCategoriesString(meta.attributeVal(audio::CATEGORIES_ATTRIBUTE)); info.meta = meta; info.path = pluginPath; - info.enabled = true; + info.state = AudioPluginState::Validated; infoList.emplace_back(std::move(info)); } - Ret ret = knownPluginsRegister()->registerPlugins(infoList); - return ret; + return knownPluginsRegister()->registerPlugins(infoList); } Ret RegisterAudioPluginsScenario::registerFailedPlugin(const io::path_t& pluginPath, int failCode) @@ -193,15 +291,24 @@ Ret RegisterAudioPluginsScenario::registerFailedPlugin(const io::path_t& pluginP return false; } + // Same reason as registerPlugin: the failed entry uses the basename as + // its id (matching the Discovered placeholder), so the placeholder must + // be cleared first to avoid the same-id-same-path guard. + Ret ret = knownPluginsRegister()->removePluginsAtPath(pluginPath); + if (!ret) { + LOGE() << "Failed to clear existing entry at " << pluginPath.toStdString() + << ": " << ret.toString(); + return ret; + } + AudioPluginInfo info; info.meta.id = io::completeBasename(pluginPath).toStdString(); info.meta.type = metaType(pluginPath); info.path = pluginPath; - info.enabled = false; + info.state = AudioPluginState::Error; info.errorCode = failCode; - Ret ret = knownPluginsRegister()->registerPlugins({ info }); - return ret; + return knownPluginsRegister()->registerPlugins({ info }); } IAudioPluginMetaReaderPtr RegisterAudioPluginsScenario::metaReader(const io::path_t& pluginPath) const @@ -215,8 +322,8 @@ IAudioPluginMetaReaderPtr RegisterAudioPluginsScenario::metaReader(const io::pat return nullptr; } -audio::AudioResourceType RegisterAudioPluginsScenario::metaType(const io::path_t& pluginPath) const +audioplugins::AudioResourceType RegisterAudioPluginsScenario::metaType(const io::path_t& pluginPath) const { const IAudioPluginMetaReaderPtr reader = metaReader(pluginPath); - return reader ? reader->metaType() : audio::AudioResourceType::Undefined; + return reader ? reader->metaType() : audioplugins::AudioResourceType(); } diff --git a/framework/audioplugins/internal/registeraudiopluginsscenario.h b/framework/audioplugins/internal/registeraudiopluginsscenario.h index 86c8836d64..cc6ea82807 100644 --- a/framework/audioplugins/internal/registeraudiopluginsscenario.h +++ b/framework/audioplugins/internal/registeraudiopluginsscenario.h @@ -52,18 +52,18 @@ class RegisterAudioPluginsScenario : public IRegisterAudioPluginsScenario, publi PluginScanResult scanPlugins(Progress* progress = nullptr) const override; - void updatePluginsRegistry() override; - - Ret registerNewPlugins(const io::paths_t& pluginPaths) override; - Ret unregisterRemovedPlugins(const audio::AudioResourceIdList& pluginIds) override; + Ret updatePluginsRegistry() override; + Ret registerNewPlugins(const io::paths_t& pluginPaths, bool validate) override; + Ret unregisterRemovedPlugins(const AudioResourceIdList& pluginIds) override; Ret registerPlugin(const io::path_t& pluginPath) override; Ret registerFailedPlugin(const io::path_t& pluginPath, int failCode) override; private: + Ret persistDiscoveredPlaceholders(const io::paths_t& pluginPaths); void processPluginsRegistration(const io::paths_t& pluginPaths); IAudioPluginMetaReaderPtr metaReader(const io::path_t& pluginPath) const; - audio::AudioResourceType metaType(const io::path_t& pluginPath) const; + AudioResourceType metaType(const io::path_t& pluginPath) const; Progress m_progress; bool m_aborted = false; diff --git a/framework/audioplugins/iregisteraudiopluginsscenario.h b/framework/audioplugins/iregisteraudiopluginsscenario.h index 92e9e3841b..2e43727f30 100644 --- a/framework/audioplugins/iregisteraudiopluginsscenario.h +++ b/framework/audioplugins/iregisteraudiopluginsscenario.h @@ -24,13 +24,16 @@ #include "modularity/imoduleinterface.h" -#include "audio/common/audiotypes.h" +#include "global/types/ret.h" +#include "global/io/path.h" #include "global/progress.h" +#include "audiopluginstypes.h" namespace muse::audioplugins { struct PluginScanResult { - io::paths_t newPluginPaths; - audio::AudioResourceIdList missingPluginIds; + io::paths_t newPluginPaths; // not in cache; will be inserted via subprocess validation + AudioResourceIdList missingPluginIds; // in cache but not currently found by any scanner + AudioResourceIdList rediscoveredPluginIds; // previously Missing entries the scanner found again }; class IRegisterAudioPluginsScenario : MODULE_CONTEXT_INTERFACE @@ -42,10 +45,13 @@ class IRegisterAudioPluginsScenario : MODULE_CONTEXT_INTERFACE virtual PluginScanResult scanPlugins(Progress* progress = nullptr) const = 0; - virtual void updatePluginsRegistry() = 0; - - virtual Ret registerNewPlugins(const io::paths_t& pluginPaths) = 0; - virtual Ret unregisterRemovedPlugins(const audio::AudioResourceIdList& pluginIds) = 0; + virtual Ret updatePluginsRegistry() = 0; + // `validate=false` persists the paths as Discovered placeholders only; + // out-of-process validation is skipped and the entries will be re-offered + // for validation on the next scan. Default `true` runs the full scan. + // Returns the first cache write/load failure encountered, or ok. + virtual Ret registerNewPlugins(const io::paths_t& pluginPaths, bool validate = true) = 0; + virtual Ret unregisterRemovedPlugins(const AudioResourceIdList& pluginIds) = 0; virtual Ret registerPlugin(const io::path_t& pluginPath) = 0; virtual Ret registerFailedPlugin(const io::path_t& pluginPath, int failCode) = 0; diff --git a/framework/audioplugins/tests/CMakeLists.txt b/framework/audioplugins/tests/CMakeLists.txt index 761af04dc8..fd9c33c3c2 100644 --- a/framework/audioplugins/tests/CMakeLists.txt +++ b/framework/audioplugins/tests/CMakeLists.txt @@ -23,14 +23,15 @@ set(MODULE_TEST muse_audioplugins_test) set(MODULE_TEST_SRC ${CMAKE_CURRENT_LIST_DIR}/mocks/audiopluginsconfigurationmock.h ${CMAKE_CURRENT_LIST_DIR}/mocks/knownaudiopluginsregistermock.h + ${CMAKE_CURRENT_LIST_DIR}/mocks/knownaudiopluginsmigrationregistermock.h ${CMAKE_CURRENT_LIST_DIR}/mocks/audiopluginsscannerregistermock.h ${CMAKE_CURRENT_LIST_DIR}/mocks/audiopluginsscannermock.h ${CMAKE_CURRENT_LIST_DIR}/mocks/audiopluginmetareaderregistermock.h ${CMAKE_CURRENT_LIST_DIR}/mocks/audiopluginmetareadermock.h ${CMAKE_CURRENT_LIST_DIR}/knownaudiopluginsregistertest.cpp + ${CMAKE_CURRENT_LIST_DIR}/knownaudiopluginsmigrationregistertest.cpp ${CMAKE_CURRENT_LIST_DIR}/registeraudiopluginsscenariotest.cpp - ${CMAKE_CURRENT_LIST_DIR}/audiopluginsutilstest.cpp ) set(MODULE_TEST_LINK muse_audioplugins) diff --git a/framework/audioplugins/tests/audiopluginsutilstest.cpp b/framework/audioplugins/tests/audiopluginsutilstest.cpp index dcb2d85c5c..e69de29bb2 100644 --- a/framework/audioplugins/tests/audiopluginsutilstest.cpp +++ b/framework/audioplugins/tests/audiopluginsutilstest.cpp @@ -1,52 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-only - * MuseScore-CLA-applies - * - * MuseScore Studio - * Music Composition & Notation - * - * Copyright (C) 2023 MuseScore Limited and others - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * 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, see . - */ - -#include - -#include "audioplugins/internal/audiopluginsutils.h" -#include "audioplugins/audiopluginstypes.h" - -using namespace muse::audioplugins; - -namespace muse::audioplugins { -class AudioPlugins_AudioUtilsTest : public ::testing::Test -{ -public: -}; -} - -TEST_F(AudioPlugins_AudioUtilsTest, AudioPluginTypeFromCategoriesString) -{ - EXPECT_EQ(AudioPluginType::Fx, audioPluginTypeFromCategoriesString(u"Fx|Delay")); - EXPECT_EQ(AudioPluginType::Fx, audioPluginTypeFromCategoriesString(u"Test|Fx")); - - EXPECT_EQ(AudioPluginType::Instrument, audioPluginTypeFromCategoriesString(u"Instrument|Test")); - EXPECT_EQ(AudioPluginType::Instrument, audioPluginTypeFromCategoriesString(u"Test|Instrument")); - - //! NOTE: "Instrument" has the highest priority for compatibility reasons - EXPECT_EQ(AudioPluginType::Instrument, audioPluginTypeFromCategoriesString(u"Instrument|Fx|Test")); - EXPECT_EQ(AudioPluginType::Instrument, audioPluginTypeFromCategoriesString(u"Fx|Instrument|Test")); - - EXPECT_EQ(AudioPluginType::Undefined, audioPluginTypeFromCategoriesString(u"Test")); - EXPECT_EQ(AudioPluginType::Undefined, audioPluginTypeFromCategoriesString(u"FX|Test")); - EXPECT_EQ(AudioPluginType::Undefined, audioPluginTypeFromCategoriesString(u"INSTRUMENT|Test")); -} diff --git a/framework/audioplugins/tests/knownaudiopluginsmigrationregistertest.cpp b/framework/audioplugins/tests/knownaudiopluginsmigrationregistertest.cpp new file mode 100644 index 0000000000..0b14b432a2 --- /dev/null +++ b/framework/audioplugins/tests/knownaudiopluginsmigrationregistertest.cpp @@ -0,0 +1,312 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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, see . + */ +#include + +#include "global/serialization/json.h" + +#include "audioplugins/internal/knownaudiopluginsmigrationregister.h" + +using namespace muse; +using namespace muse::audioplugins; + +namespace muse::audioplugins { +class AudioPlugins_KnownAudioPluginsMigrationRegisterTest : public ::testing::Test +{ +protected: + KnownAudioPluginsMigrationRegister m_register; + + static JsonArray makeArray(const std::string& tag) + { + JsonObject obj; + obj.set("tag", tag); + JsonArray arr; + arr << obj; + return arr; + } + + static std::string firstTag(const JsonArray& arr) + { + if (arr.empty()) { + return {}; + } + return arr.at(0).toObject().value("tag").toString().toStdString(); + } +}; +} + +TEST_F(AudioPlugins_KnownAudioPluginsMigrationRegisterTest, SameVersion_NoOp) +{ + JsonArray plugins = makeArray("v0"); + + Ret ret = m_register.migrate(0, 0, plugins); + + EXPECT_TRUE(ret); + EXPECT_EQ(firstTag(plugins), "v0"); +} + +TEST_F(AudioPlugins_KnownAudioPluginsMigrationRegisterTest, SingleStep_AppliesCallback) +{ + m_register.registerMigration(0, [](const JsonArray&) { + return makeArray("v1"); + }); + + JsonArray plugins = makeArray("v0"); + + Ret ret = m_register.migrate(0, 1, plugins); + + EXPECT_TRUE(ret); + EXPECT_EQ(firstTag(plugins), "v1"); +} + +TEST_F(AudioPlugins_KnownAudioPluginsMigrationRegisterTest, MultiStep_ChainsInOrder) +{ + m_register.registerMigration(0, [](const JsonArray& in) { + // v0 -> v1: input must still be v0 + EXPECT_EQ(firstTag(in), "v0"); + return makeArray("v1"); + }); + m_register.registerMigration(1, [](const JsonArray& in) { + // v1 -> v2: input must already be v1 (proves ordering) + EXPECT_EQ(firstTag(in), "v1"); + return makeArray("v2"); + }); + + JsonArray plugins = makeArray("v0"); + + Ret ret = m_register.migrate(0, 2, plugins); + + EXPECT_TRUE(ret); + EXPECT_EQ(firstTag(plugins), "v2"); +} + +TEST_F(AudioPlugins_KnownAudioPluginsMigrationRegisterTest, MissingMigration_Fails) +{ + // Test at slots beyond the framework's pre-registered range (0 and 1) + // so this test stays focused on the missing-migrator error path. + // Only v5 -> v6 is registered, but we request v5 -> v7. + m_register.registerMigration(5, [](const JsonArray&) { + return makeArray("v6"); + }); + + JsonArray plugins = makeArray("v5"); + + Ret ret = m_register.migrate(5, 7, plugins); + + EXPECT_FALSE(ret); + // The error message must point developers at the missing migrator so a + // future framework version bump that forgets to register a migrator is + // immediately actionable. + EXPECT_NE(ret.text().find("missing migrator"), std::string::npos); + EXPECT_NE(ret.text().find("6 -> 7"), std::string::npos); +} + +TEST_F(AudioPlugins_KnownAudioPluginsMigrationRegisterTest, BackwardMigration_Fails) +{ + JsonArray plugins = makeArray("v1"); + + Ret ret = m_register.migrate(2, 1, plugins); + + EXPECT_FALSE(ret); +} + +TEST_F(AudioPlugins_KnownAudioPluginsMigrationRegisterTest, FutureVersion_FailsWithNewerHint) +{ + // A cache file written by a newer build lands on an older build. The + // error message must say "newer" so the user / developer understands + // they cannot downgrade. + JsonArray plugins = makeArray("v99"); + + Ret ret = m_register.migrate(99, 3, plugins); + + EXPECT_FALSE(ret); + EXPECT_NE(ret.text().find("newer"), std::string::npos); +} + +TEST_F(AudioPlugins_KnownAudioPluginsMigrationRegisterTest, V2ToV3_MovesHasNativeEditorSupportIntoAttributes) +{ + // Mirrors the v2 -> v3 migration registered MuseScore-side in + // src/app/internal/audiopluginsappconfigmodule.cpp. + m_register.registerMigration(2, [](const JsonArray& plugins) { + JsonArray out; + for (size_t i = 0; i < plugins.size(); ++i) { + JsonObject obj = plugins.at(i).toObject(); + JsonObject meta = obj.value("meta").toObject(); + if (meta.contains("hasNativeEditorSupport")) { + JsonObject attrs; + if (meta.contains("attributes")) { + attrs = meta.value("attributes").toObject(); + } + const bool b = meta.value("hasNativeEditorSupport").toBool(); + attrs.set("hasNativeEditorSupport", b ? std::string("true") : std::string("false")); + meta.set("attributes", attrs); + + JsonObject metaWithoutLegacy; + for (const std::string& k : meta.keys()) { + if (k == "hasNativeEditorSupport") { + continue; + } + metaWithoutLegacy.set(k, meta.value(k)); + } + obj.set("meta", metaWithoutLegacy); + } + out << obj; + } + return out; + }); + + // [GIVEN] A v0-shaped plugin entry: meta.hasNativeEditorSupport = true at top level. + JsonObject meta; + meta.set("id", std::string("AAA")); + meta.set("type", std::string("VstPlugin")); + meta.set("hasNativeEditorSupport", true); + + JsonObject obj; + obj.set("meta", meta); + obj.set("path", std::string("/x/AAA.vst3")); + + JsonArray plugins; + plugins << obj; + + // [WHEN] migrating from v2 to v3 + Ret ret = m_register.migrate(2, 3, plugins); + + // [THEN] hasNativeEditorSupport moved into meta.attributes as the string "true" + ASSERT_TRUE(ret); + ASSERT_EQ(plugins.size(), size_t(1)); + JsonObject migratedMeta = plugins.at(0).toObject().value("meta").toObject(); + EXPECT_FALSE(migratedMeta.contains("hasNativeEditorSupport")); + EXPECT_EQ(migratedMeta.value("attributes").toObject().value("hasNativeEditorSupport").toStdString(), "true"); +} + +TEST_F(AudioPlugins_KnownAudioPluginsMigrationRegisterTest, V1ToV2_TranslatesEnabledIntoStateString) +{ + // Verifies the v1 -> v2 framework-owned migration shape. The production + // KnownAudioPluginsMigrationRegister pre-registers this in its + // constructor; this test overrides the slot with the same body to keep + // the assertion isolated from production state — for the production + // pre-registration check see FrameworkOwned_AutoRegisters below. + m_register.registerMigration(1, [](const JsonArray& plugins) { + JsonArray out; + for (size_t i = 0; i < plugins.size(); ++i) { + JsonObject obj = plugins.at(i).toObject(); + const bool enabled = obj.value("enabled").toBool(); + obj.set("state", enabled ? std::string("Validated") : std::string("Error")); + + JsonObject rebuilt; + for (const std::string& k : obj.keys()) { + if (k == "enabled") { + continue; + } + rebuilt.set(k, obj.value(k)); + } + out << rebuilt; + } + return out; + }); + + // [GIVEN] Two v1-shaped plugin entries: one enabled, one disabled-with-errorCode. + JsonObject enabledObj; + enabledObj.set("path", std::string("/x/AAA.vst3")); + enabledObj.set("enabled", true); + + JsonObject failedObj; + failedObj.set("path", std::string("/x/BBB.vst3")); + failedObj.set("enabled", false); + failedObj.set("errorCode", -42); + + JsonArray plugins; + plugins << enabledObj; + plugins << failedObj; + + // [WHEN] migrating from v1 to v2 + Ret ret = m_register.migrate(1, 2, plugins); + + // [THEN] enabled is gone; state reflects the boolean; errorCode preserved. + ASSERT_TRUE(ret); + ASSERT_EQ(plugins.size(), size_t(2)); + + JsonObject migratedEnabled = plugins.at(0).toObject(); + EXPECT_FALSE(migratedEnabled.contains("enabled")); + EXPECT_EQ(migratedEnabled.value("state").toStdString(), "Validated"); + + JsonObject migratedFailed = plugins.at(1).toObject(); + EXPECT_FALSE(migratedFailed.contains("enabled")); + EXPECT_EQ(migratedFailed.value("state").toStdString(), "Error"); + EXPECT_EQ(migratedFailed.value("errorCode").toInt(), -42); +} + +TEST_F(AudioPlugins_KnownAudioPluginsMigrationRegisterTest, FrameworkOwned_AutoRegisters) +{ + // A freshly-constructed register must already carry the framework-owned + // migrations: v0 -> v1 (structural no-op), v1 -> v2 (enabled -> state), + // and a no-op v2 -> v3 default (apps override the slot when they have + // real v2 -> v3 work). + KnownAudioPluginsMigrationRegister reg; + + // [WHEN] migrating a v0-shaped entry from v0 to v1 + JsonObject v0obj; + v0obj.set("path", std::string("/x/AAA.vst3")); + v0obj.set("enabled", true); + JsonArray v0arr; + v0arr << v0obj; + + Ret ret01 = reg.migrate(0, 1, v0arr); + + // [THEN] structural no-op: the entry passes through unchanged. + ASSERT_TRUE(ret01); + ASSERT_EQ(v0arr.size(), size_t(1)); + EXPECT_TRUE(v0arr.at(0).toObject().value("enabled").toBool()); + EXPECT_FALSE(v0arr.at(0).toObject().contains("state")); + + // [WHEN] migrating a v1-shaped entry from v1 to v2 + JsonObject v1enabled; + v1enabled.set("path", std::string("/x/AAA.vst3")); + v1enabled.set("enabled", true); + JsonObject v1failed; + v1failed.set("path", std::string("/x/BBB.vst3")); + v1failed.set("enabled", false); + v1failed.set("errorCode", -42); + JsonArray v1arr; + v1arr << v1enabled; + v1arr << v1failed; + + Ret ret12 = reg.migrate(1, 2, v1arr); + + // [THEN] enabled has been translated to state; errorCode preserved. + ASSERT_TRUE(ret12); + ASSERT_EQ(v1arr.size(), size_t(2)); + EXPECT_FALSE(v1arr.at(0).toObject().contains("enabled")); + EXPECT_EQ(v1arr.at(0).toObject().value("state").toStdString(), "Validated"); + EXPECT_FALSE(v1arr.at(1).toObject().contains("enabled")); + EXPECT_EQ(v1arr.at(1).toObject().value("state").toStdString(), "Error"); + EXPECT_EQ(v1arr.at(1).toObject().value("errorCode").toInt(), -42); + + // [WHEN] migrating a v2 entry from v2 to v3 + JsonArray v2arr = makeArray("v2"); + + Ret ret23 = reg.migrate(2, 3, v2arr); + + // [THEN] the framework's default v2 -> v3 is a no-op pass-through, so a v2 + // cache always reaches the current version even with no app override. + EXPECT_TRUE(ret23); + EXPECT_EQ(firstTag(v2arr), "v2"); +} diff --git a/framework/audioplugins/tests/knownaudiopluginsregistertest.cpp b/framework/audioplugins/tests/knownaudiopluginsregistertest.cpp index 844b2d29ab..6b584cd076 100644 --- a/framework/audioplugins/tests/knownaudiopluginsregistertest.cpp +++ b/framework/audioplugins/tests/knownaudiopluginsregistertest.cpp @@ -27,16 +27,22 @@ #include "global/tests/mocks/filesystemmock.h" #include "mocks/audiopluginsconfigurationmock.h" +#include "mocks/knownaudiopluginsmigrationregistermock.h" using ::testing::_; using ::testing::NiceMock; using ::testing::Return; +using ::testing::ReturnRef; using namespace muse; using namespace muse::audioplugins; -using namespace muse::audio; using namespace muse::io; +namespace { +const String kRuntimeAttrKey(u"playbackSetupData"); +const String kRuntimeAttrValue(u"general"); +} + namespace muse::audioplugins { class AudioPlugins_KnownAudioPluginsRegisterTest : public ::testing::Test { @@ -46,19 +52,28 @@ class AudioPlugins_KnownAudioPluginsRegisterTest : public ::testing::Test m_knownPlugins = std::make_shared(); m_fileSystem = std::make_shared >(); m_configuration = std::make_shared >(); + m_migrations = std::make_shared >(); m_knownPlugins->fileSystem.set(m_fileSystem); m_knownPlugins->configuration.set(m_configuration); + m_knownPlugins->migrations.set(m_migrations); m_knownAudioPluginsFilePath = "/test/some dir/known_audio_plugins.json"; ON_CALL(*m_configuration, knownAudioPluginsFilePath()) .WillByDefault(Return(m_knownAudioPluginsFilePath)); + + m_runtimeDefaults = AudioResourceAttributes { { kRuntimeAttrKey, kRuntimeAttrValue } }; + ON_CALL(*m_configuration, runtimeAttributeDefaults()) + .WillByDefault(ReturnRef(m_runtimeDefaults)); + + ON_CALL(*m_migrations, migrate(_, _, _)) + .WillByDefault(Return(muse::make_ok())); } ByteArray pluginInfoListToJson(const std::vector& infoList) const { const std::map RESOURCE_TYPE_TO_STR { - { AudioResourceType::VstPlugin, "VstPlugin" }, + { "VstPlugin", "VstPlugin" }, }; JsonArray array; @@ -66,7 +81,7 @@ class AudioPlugins_KnownAudioPluginsRegisterTest : public ::testing::Test for (const AudioPluginInfo& info : infoList) { JsonObject attributesObj; for (auto it = info.meta.attributes.cbegin(); it != info.meta.attributes.cend(); ++it) { - if (it->first == audio::PLAYBACK_SETUP_DATA_ATTRIBUTE) { + if (it->first == kRuntimeAttrKey) { continue; } @@ -76,7 +91,6 @@ class AudioPlugins_KnownAudioPluginsRegisterTest : public ::testing::Test JsonObject metaObj; metaObj.set("id", info.meta.id); metaObj.set("type", muse::value(RESOURCE_TYPE_TO_STR, info.meta.type, "Undefined")); - metaObj.set("hasNativeEditorSupport", info.meta.hasNativeEditorSupport); if (!info.meta.vendor.empty()) { metaObj.set("vendor", info.meta.vendor); @@ -89,7 +103,7 @@ class AudioPlugins_KnownAudioPluginsRegisterTest : public ::testing::Test JsonObject mainObj; mainObj.set("meta", metaObj); mainObj.set("path", info.path.toStdString()); - mainObj.set("enabled", info.enabled); + mainObj.set("state", audioPluginStateName(info.state)); if (info.errorCode != 0) { mainObj.set("errorCode", info.errorCode); @@ -98,7 +112,11 @@ class AudioPlugins_KnownAudioPluginsRegisterTest : public ::testing::Test array << mainObj; } - return JsonDocument(array).toJson(); + JsonObject root; + root.set("version", CURRENT_KNOWN_AUDIO_PLUGINS_VERSION); + root.set("plugins", array); + + return JsonDocument(root).toJson(); } std::vector setupTestData() @@ -106,39 +124,37 @@ class AudioPlugins_KnownAudioPluginsRegisterTest : public ::testing::Test std::vector plugins; AudioPluginInfo pluginInfo1; - pluginInfo1.type = AudioPluginType::Fx; pluginInfo1.path = "/some/path/to/vst/plugin/AAA.vst3"; pluginInfo1.meta.id = "AAA"; - pluginInfo1.meta.type = AudioResourceType::VstPlugin; + pluginInfo1.meta.type = "VstPlugin"; pluginInfo1.meta.vendor = "Some vendor"; - pluginInfo1.meta.attributes = { { audio::CATEGORIES_ATTRIBUTE, u"Fx|Reverb" }, - { audio::PLAYBACK_SETUP_DATA_ATTRIBUTE, mpe::GENERIC_SETUP_DATA_STRING } }; - pluginInfo1.enabled = true; + pluginInfo1.meta.attributes = { { String(u"categories"), u"Fx|Reverb" }, + { String(u"hasNativeEditorSupport"), u"true" }, + { kRuntimeAttrKey, kRuntimeAttrValue } }; + pluginInfo1.state = AudioPluginState::Validated; plugins.push_back(pluginInfo1); AudioPluginInfo pluginInfo2; - pluginInfo2.type = AudioPluginType::Fx; pluginInfo2.path = "/some/path/to/vst/plugin/BBB.vst3"; pluginInfo2.meta.id = "BBB"; - pluginInfo2.meta.type = AudioResourceType::VstPlugin; + pluginInfo2.meta.type = "VstPlugin"; pluginInfo2.meta.vendor = "Another vendor"; - pluginInfo2.meta.attributes = { { audio::CATEGORIES_ATTRIBUTE, u"Fx|Distortion" }, - { audio::PLAYBACK_SETUP_DATA_ATTRIBUTE, mpe::GENERIC_SETUP_DATA_STRING } }; - pluginInfo2.enabled = true; + pluginInfo2.meta.attributes = { { String(u"categories"), u"Fx|Distortion" }, + { kRuntimeAttrKey, kRuntimeAttrValue } }; + pluginInfo2.state = AudioPluginState::Validated; plugins.push_back(pluginInfo2); - AudioPluginInfo disabledPluginInfo; - disabledPluginInfo.type = AudioPluginType::Instrument; - disabledPluginInfo.path = "/some/path/to/vst/plugin/CCC.vst3"; - disabledPluginInfo.meta.id = "CCC"; - disabledPluginInfo.meta.type = AudioResourceType::VstPlugin; - disabledPluginInfo.enabled = false; - disabledPluginInfo.meta.attributes = { - { audio::CATEGORIES_ATTRIBUTE, u"Instrument|Synth" }, - { audio::PLAYBACK_SETUP_DATA_ATTRIBUTE, mpe::GENERIC_SETUP_DATA_STRING } + AudioPluginInfo erroredPluginInfo; + erroredPluginInfo.path = "/some/path/to/vst/plugin/CCC.vst3"; + erroredPluginInfo.meta.id = "CCC"; + erroredPluginInfo.meta.type = "VstPlugin"; + erroredPluginInfo.state = AudioPluginState::Error; + erroredPluginInfo.meta.attributes = { + { String(u"categories"), u"Instrument|Synth" }, + { kRuntimeAttrKey, kRuntimeAttrValue } }; - disabledPluginInfo.errorCode = -1; - plugins.push_back(disabledPluginInfo); + erroredPluginInfo.errorCode = -1; + plugins.push_back(erroredPluginInfo); ByteArray data = pluginInfoListToJson(plugins); ON_CALL(*m_fileSystem, readFile(m_knownAudioPluginsFilePath)) @@ -150,19 +166,18 @@ class AudioPlugins_KnownAudioPluginsRegisterTest : public ::testing::Test std::shared_ptr m_knownPlugins; std::shared_ptr m_fileSystem; std::shared_ptr m_configuration; + std::shared_ptr m_migrations; path_t m_knownAudioPluginsFilePath; + AudioResourceAttributes m_runtimeDefaults; }; inline bool operator==(const AudioPluginInfo& info1, const AudioPluginInfo& info2) { - bool equal = info1.type == info2.type; - equal &= (info1.path == info2.path); - equal &= (info1.meta == info2.meta); - equal &= (info1.enabled == info2.enabled); - equal &= (info1.errorCode == info2.errorCode); - - return equal; + return info1.path == info2.path + && info1.meta == info2.meta + && info1.state == info2.state + && info1.errorCode == info2.errorCode; } } @@ -199,11 +214,10 @@ TEST_F(AudioPlugins_KnownAudioPluginsRegisterTest, PluginInfoList) // [GIVEN] New plugin for registration AudioPluginInfo newPluginInfo; - newPluginInfo.type = AudioPluginType::Instrument; newPluginInfo.meta.id = "DDD"; - newPluginInfo.meta.type = AudioResourceType::VstPlugin; + newPluginInfo.meta.type = "VstPlugin"; newPluginInfo.path = "/path/to/new/plugin/plugin.vst"; - newPluginInfo.enabled = true; + newPluginInfo.state = AudioPluginState::Validated; expectedPluginInfoList.push_back(newPluginInfo); // [THEN] All the plugins will be written to the file @@ -260,3 +274,155 @@ TEST_F(AudioPlugins_KnownAudioPluginsRegisterTest, PluginInfoList) actualPluginInfoList = m_knownPlugins->pluginInfoList(); EXPECT_FALSE(muse::contains(actualPluginInfoList, unregisteredPlugin)); } + +TEST_F(AudioPlugins_KnownAudioPluginsRegisterTest, RegisterPlugins_SkipsDuplicateSameIdSamePath) +{ + // Registering the same id at the same path twice is a duplicate write + // (e.g. RegisterAudioPluginsScenario writing a Discovered placeholder + // over a leftover one from a crashed prior run). registerPlugins logs a + // warning and skips the duplicate - no crash, no double-write to the + // cache. This test pins the register-level behavior so a future caller + // change can't silently corrupt the cache. + ON_CALL(*m_fileSystem, writeFile(_, _)) + .WillByDefault(Return(muse::make_ok())); + + ASSERT_TRUE(m_knownPlugins->load()); + + AudioPluginInfo info; + info.meta.id = "Dup"; + info.meta.type = "VstPlugin"; + info.path = "/some/path/Dup.vst3"; + info.state = AudioPluginState::Discovered; + + ASSERT_TRUE(m_knownPlugins->registerPlugins({ info })); + + // Second register with same id + same path: graceful skip, no duplicate. + ASSERT_TRUE(m_knownPlugins->registerPlugins({ info })); + + int count = 0; + for (const auto& plugin : m_knownPlugins->pluginInfoList()) { + if (plugin.meta.id == "Dup" && plugin.path == "/some/path/Dup.vst3") { + ++count; + } + } + EXPECT_EQ(count, 1); +} + +TEST_F(AudioPlugins_KnownAudioPluginsRegisterTest, RegisterPlugins_SameIdDifferentPathSucceeds) +{ + // The multimap allows the same id at distinct paths (a plugin installed + // twice). This is the success case complementing the death test above. + ON_CALL(*m_fileSystem, writeFile(_, _)) + .WillByDefault(Return(muse::make_ok())); + + ASSERT_TRUE(m_knownPlugins->load()); + + AudioPluginInfo a; + a.meta.id = "Dup"; + a.meta.type = "VstPlugin"; + a.path = "/path/A/Dup.vst3"; + a.state = AudioPluginState::Validated; + + AudioPluginInfo b = a; + b.path = "/path/B/Dup.vst3"; + + EXPECT_TRUE(m_knownPlugins->registerPlugins({ a })); + EXPECT_TRUE(m_knownPlugins->registerPlugins({ b })); + EXPECT_EQ(m_knownPlugins->pluginInfoList().size(), 2u); +} + +TEST_F(AudioPlugins_KnownAudioPluginsRegisterTest, Load_MigrationFailure_LeavesRegisterEmpty) +{ + // A cache file from a future version (or with a missing migrator) makes + // migrate() return an error. load() must propagate the error and not + // populate the register — registerPlugins will then trip its m_loaded + // assertion, which is what surfaces the underlying migration failure. + JsonObject root; + root.set("version", 99); + root.set("plugins", JsonArray {}); + + ByteArray futureData = JsonDocument(root).toJson(); + + ON_CALL(*m_fileSystem, exists(m_knownAudioPluginsFilePath)) + .WillByDefault(Return(muse::make_ok())); + ON_CALL(*m_fileSystem, readFile(m_knownAudioPluginsFilePath)) + .WillByDefault(Return(RetVal::make_ok(futureData))); + + EXPECT_CALL(*m_migrations, migrate(99, CURRENT_KNOWN_AUDIO_PLUGINS_VERSION, _)) + .WillOnce(Return(Ret(static_cast(Ret::Code::UnknownError), std::string("cache file version 99 is newer")))); + + Ret ret = m_knownPlugins->load(); + + EXPECT_FALSE(ret); + EXPECT_NE(ret.text().find("newer"), std::string::npos); + EXPECT_TRUE(m_knownPlugins->pluginInfoList().empty()); +} + +TEST_F(AudioPlugins_KnownAudioPluginsRegisterTest, Load_LegacyArrayFormat) +{ + // [GIVEN] A legacy bare-array JSON file (pre-versioning) + JsonArray array; + + JsonObject metaObj; + metaObj.set("id", std::string("AAA")); + metaObj.set("type", std::string("VstPlugin")); + metaObj.set("hasNativeEditorSupport", true); + + JsonObject mainObj; + mainObj.set("meta", metaObj); + mainObj.set("path", std::string("/some/path/to/vst/plugin/AAA.vst3")); + mainObj.set("enabled", true); + + array << mainObj; + + ByteArray legacyData = JsonDocument(array).toJson(); + + ON_CALL(*m_fileSystem, exists(m_knownAudioPluginsFilePath)) + .WillByDefault(Return(muse::make_ok())); + ON_CALL(*m_fileSystem, readFile(m_knownAudioPluginsFilePath)) + .WillByDefault(Return(RetVal::make_ok(legacyData))); + + // [THEN] migrate() is called from version 0 to current + EXPECT_CALL(*m_migrations, migrate(0, CURRENT_KNOWN_AUDIO_PLUGINS_VERSION, _)) + .WillOnce(Return(muse::make_ok())); + + // [WHEN] Loading + Ret ret = m_knownPlugins->load(); + + // [THEN] Plugins parsed successfully + EXPECT_TRUE(ret); + EXPECT_TRUE(m_knownPlugins->exists(AudioResourceId("AAA"))); +} + +TEST_F(AudioPlugins_KnownAudioPluginsRegisterTest, Load_MalformedRow_RejectsFile) +{ + // [GIVEN] A versioned cache whose single row has no `meta` — a truncated + // entry that yields an empty id and would poison m_pluginInfoMap. + JsonObject rowObj; + rowObj.set("path", std::string("/some/path/AAA.vst3")); + // no "meta" + + JsonArray plugins; + plugins << rowObj; + + JsonObject root; + root.set("version", CURRENT_KNOWN_AUDIO_PLUGINS_VERSION); + root.set("plugins", plugins); + + ByteArray data = JsonDocument(root).toJson(); + + ON_CALL(*m_fileSystem, exists(m_knownAudioPluginsFilePath)) + .WillByDefault(Return(muse::make_ok())); + ON_CALL(*m_fileSystem, readFile(m_knownAudioPluginsFilePath)) + .WillByDefault(Return(RetVal::make_ok(data))); + + EXPECT_CALL(*m_migrations, migrate(CURRENT_KNOWN_AUDIO_PLUGINS_VERSION, CURRENT_KNOWN_AUDIO_PLUGINS_VERSION, _)) + .WillOnce(Return(muse::make_ok())); + + // [WHEN] Loading + Ret ret = m_knownPlugins->load(); + + // [THEN] The malformed row is rejected and the register left empty. + EXPECT_FALSE(ret); + EXPECT_TRUE(m_knownPlugins->pluginInfoList().empty()); +} diff --git a/framework/audioplugins/tests/mocks/audiopluginmetareadermock.h b/framework/audioplugins/tests/mocks/audiopluginmetareadermock.h index 17ff4194ef..4732612f01 100644 --- a/framework/audioplugins/tests/mocks/audiopluginmetareadermock.h +++ b/framework/audioplugins/tests/mocks/audiopluginmetareadermock.h @@ -29,8 +29,8 @@ namespace muse::audioplugins { class AudioPluginMetaReaderMock : public IAudioPluginMetaReader { public: - MOCK_METHOD(audio::AudioResourceType, metaType, (), (const, override)); + MOCK_METHOD(AudioResourceType, metaType, (), (const, override)); MOCK_METHOD(bool, canReadMeta, (const io::path_t&), (const, override)); - MOCK_METHOD(RetVal, readMeta, (const io::path_t&), (const, override)); + MOCK_METHOD(RetVal, readMeta, (const io::path_t&), (const, override)); }; } diff --git a/framework/audioplugins/tests/mocks/audiopluginsconfigurationmock.h b/framework/audioplugins/tests/mocks/audiopluginsconfigurationmock.h index f6334cd39e..84afb8f260 100644 --- a/framework/audioplugins/tests/mocks/audiopluginsconfigurationmock.h +++ b/framework/audioplugins/tests/mocks/audiopluginsconfigurationmock.h @@ -31,5 +31,8 @@ class AudioPluginsConfigurationMock : public IAudioPluginsConfiguration public: MOCK_METHOD(io::path_t, knownAudioPluginsFilePath, (), (const, override)); + + MOCK_METHOD(const AudioResourceAttributes&, runtimeAttributeDefaults, (), (const, override)); + MOCK_METHOD(void, setRuntimeAttributeDefaults, (const AudioResourceAttributes&), (override)); }; } diff --git a/framework/audioplugins/tests/mocks/knownaudiopluginsmigrationregistermock.h b/framework/audioplugins/tests/mocks/knownaudiopluginsmigrationregistermock.h new file mode 100644 index 0000000000..583ef3a79a --- /dev/null +++ b/framework/audioplugins/tests/mocks/knownaudiopluginsmigrationregistermock.h @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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, see . + */ +#pragma once + +#include + +#include "audioplugins/iknownaudiopluginsmigrationregister.h" + +namespace muse::audioplugins { +class KnownAudioPluginsMigrationRegisterMock : public IKnownAudioPluginsMigrationRegister +{ +public: + MOCK_METHOD(void, registerMigration, (int fromVersion, PluginsMigration cb), (override)); + MOCK_METHOD(Ret, migrate, (int fromVersion, int toVersion, JsonArray & plugins), (const, override)); +}; +} diff --git a/framework/audioplugins/tests/mocks/knownaudiopluginsregistermock.h b/framework/audioplugins/tests/mocks/knownaudiopluginsregistermock.h index 515fb01af0..452defa072 100644 --- a/framework/audioplugins/tests/mocks/knownaudiopluginsregistermock.h +++ b/framework/audioplugins/tests/mocks/knownaudiopluginsregistermock.h @@ -34,12 +34,16 @@ class KnownAudioPluginsRegisterMock : public IKnownAudioPluginsRegister MOCK_METHOD(AudioPluginInfoList, pluginInfoList, (PluginInfoAccepted), (const, override)); MOCK_METHOD(async::Notification, pluginInfoListChanged, (), (const, override)); - MOCK_METHOD(const io::path_t&, pluginPath, (const audio::AudioResourceId&), (const, override)); + MOCK_METHOD(const io::path_t&, pluginPath, (const AudioResourceId&), (const, override)); MOCK_METHOD(bool, exists, (const io::path_t&), (const, override)); - MOCK_METHOD(bool, exists, (const audio::AudioResourceId&), (const, override)); + MOCK_METHOD(bool, exists, (const AudioResourceId&), (const, override)); MOCK_METHOD(Ret, registerPlugins, (const AudioPluginInfoList&), (override)); - MOCK_METHOD(Ret, unregisterPlugins, (const audio::AudioResourceIdList&), (override)); + MOCK_METHOD(Ret, unregisterPlugins, (const AudioResourceIdList&), (override)); + + MOCK_METHOD(Ret, setPluginsState, (const AudioResourceIdList&, AudioPluginState), (override)); + + MOCK_METHOD(Ret, removePluginsAtPath, (const io::path_t&), (override)); }; } diff --git a/framework/audioplugins/tests/registeraudiopluginsscenariotest.cpp b/framework/audioplugins/tests/registeraudiopluginsscenariotest.cpp index afd4076fe8..7188028d7a 100644 --- a/framework/audioplugins/tests/registeraudiopluginsscenariotest.cpp +++ b/framework/audioplugins/tests/registeraudiopluginsscenariotest.cpp @@ -41,7 +41,6 @@ using ::testing::Return; using ::testing::ReturnRef; using namespace muse; -using namespace muse::audio; using namespace muse::audioplugins; using namespace muse::io; @@ -80,10 +79,19 @@ class AudioPlugins_RegisterAudioPluginsScenarioTest : public ::testing::Test .WillByDefault(ReturnRef(m_metaReaders)); ON_CALL(*metaReaderMock, metaType()) - .WillByDefault(Return(AudioResourceType::VstPlugin)); + .WillByDefault(Return("VstPlugin")); ON_CALL(*metaReaderMock, canReadMeta(_)) .WillByDefault(Return(true)); + + ON_CALL(*m_knownPlugins, setPluginsState(_, _)) + .WillByDefault(Return(muse::make_ok())); + + ON_CALL(*m_knownPlugins, removePluginsAtPath(_)) + .WillByDefault(Return(muse::make_ok())); + + ON_CALL(*m_knownPlugins, registerPlugins(_)) + .WillByDefault(Return(muse::make_ok())); } std::shared_ptr m_scenario; @@ -101,13 +109,10 @@ class AudioPlugins_RegisterAudioPluginsScenarioTest : public ::testing::Test inline bool operator==(const AudioPluginInfo& info1, const AudioPluginInfo& info2) { - bool equal = info1.type == info2.type; - equal &= (info1.path == info2.path); - equal &= (info1.meta == info2.meta); - equal &= (info1.enabled == info2.enabled); - equal &= (info1.errorCode == info2.errorCode); - - return equal; + return info1.path == info2.path + && info1.meta == info2.meta + && info1.state == info2.state + && info1.errorCode == info2.errorCode; } } @@ -140,6 +145,12 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry) .WillByDefault(Return(foundPluginPaths)); } + AudioPluginInfo incompatiblePluginInfo; + incompatiblePluginInfo.path = foundPluginPaths[4]; + incompatiblePluginInfo.meta.id = io::filename(incompatiblePluginInfo.path).toStdString(); + incompatiblePluginInfo.state = AudioPluginState::Error; + incompatiblePluginInfo.errorCode = -1; + // [GIVEN] Some plugins already exist in the register AudioPluginInfoList alreadyRegisteredPlugins; for (size_t i = 0; i < 2; ++i) { @@ -152,45 +163,49 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry) ON_CALL(*m_knownPlugins, pluginInfoList(_)) .WillByDefault(Return(alreadyRegisteredPlugins)); - for (const AudioPluginInfo& info : alreadyRegisteredPlugins) { - ON_CALL(*m_knownPlugins, exists(info.path)) - .WillByDefault(Return(true)); - } - // [THEN] The progress bar is shown EXPECT_CALL(*m_interactive, showProgress(muse::trc("audio", "Scanning audio plugins"), _)) .Times(1); - // [THEN] Already registered plugins are not processed again - EXPECT_CALL(*m_process, execute(_, std::vector { "--register-audio-plugin", foundPluginPaths[0].toStdString() })) - .Times(0); - EXPECT_CALL(*m_process, execute(_, std::vector { "--register-audio-plugin", foundPluginPaths[1].toStdString() })) - .Times(0); - - // [THEN] New compatible plugins are registered - EXPECT_CALL(*m_process, execute(m_appPath, std::vector { "--register-audio-plugin", foundPluginPaths[2].toStdString() })) - .WillOnce(Return(0)); - EXPECT_CALL(*m_process, execute(m_appPath, std::vector { "--register-audio-plugin", foundPluginPaths[3].toStdString() })) - .WillOnce(Return(0)); - - // [THEN] The incompatible plugin is registered as failed - EXPECT_CALL(*m_process, execute(m_appPath, std::vector { "--register-audio-plugin", foundPluginPaths[4].toStdString() })) - .WillOnce(Return(-1)); - EXPECT_CALL(*m_process, - execute(m_appPath, - std::vector { "--register-failed-audio-plugin", foundPluginPaths[4].toStdString(), "--", "-1" })) - .WillOnce(Return(0)); + // [THEN] Processes started only for unregistered plugins + paths_t alreadyRegisteredPaths { foundPluginPaths[0], foundPluginPaths[1] }; + for (const path_t& pluginPath : foundPluginPaths) { + std::vector args = { "--register-audio-plugin", pluginPath.toStdString() }; + + if (muse::contains(alreadyRegisteredPaths, pluginPath)) { + // Ignore already registered plugins + EXPECT_CALL(*m_process, execute(_, args)) + .Times(0); + } else if (incompatiblePluginInfo.path == pluginPath) { + // Incompatible plugin detected + EXPECT_CALL(*m_process, execute(m_appPath, args)) + .WillOnce(Return(-1)); + + args = { "--register-failed-audio-plugin", pluginPath.toStdString(), "--", "-1" }; + + EXPECT_CALL(*m_process, execute(m_appPath, args)) + .WillOnce(Return(0)); + } else { + // Successfully registered plugins + EXPECT_CALL(*m_process, execute(m_appPath, args)) + .WillOnce(Return(0)); + } + } // [THEN] All plugins remain in the register EXPECT_CALL(*m_knownPlugins, unregisterPlugins(_)) .Times(0); - // [THEN] Reloaded once inside registerNewPlugins, after subprocesses finish writing to disk + // [THEN] The register is refreshed EXPECT_CALL(*m_knownPlugins, load()) - .WillOnce(Return(muse::make_ok())); + .Times(2) + .WillRepeatedly(Return(muse::make_ok())); // [WHEN] Register new plugins - m_scenario->updatePluginsRegistry(); + Ret ret = m_scenario->updatePluginsRegistry(); + + // [THEN] Plugins successfully registered + EXPECT_TRUE(ret); } TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_NoNewPlugins) @@ -221,11 +236,6 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_NoNe ON_CALL(*m_knownPlugins, pluginInfoList(_)) .WillByDefault(Return(alreadyRegisteredPlugins)); - for (const path_t& pluginPath : foundPluginPaths) { - ON_CALL(*m_knownPlugins, exists(pluginPath)) - .WillByDefault(Return(true)); - } - // [THEN] Don't register the plugins again EXPECT_CALL(*m_process, execute(_, _)) .Times(0); @@ -233,26 +243,25 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_NoNe EXPECT_CALL(*m_interactive, showProgress(_, _)) .Times(0); - EXPECT_CALL(*m_knownPlugins, unregisterPlugins(_)) - .Times(0); - EXPECT_CALL(*m_knownPlugins, load()) - .Times(0); + .WillOnce(Return(muse::make_ok())); // [WHEN] Try to register the plugins again - m_scenario->updatePluginsRegistry(); + Ret ret = m_scenario->updatePluginsRegistry(); + + // [THEN] No error + EXPECT_TRUE(ret); } //! See: https://github.com/musescore/MuseScore/issues/16458 -TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_UnregUninstalledPlugins) +TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_MarkUninstalledAsMissing) { - auto createPluginInfo = [](const io::path_t& path) { + auto createPluginInfo = [](const io::path_t& path, AudioPluginState state = AudioPluginState::Validated) { AudioPluginInfo info; - info.type = AudioPluginType::Instrument; info.meta.id = io::completeBasename(path).toStdString(); - info.meta.type = AudioResourceType::VstPlugin; + info.meta.type = "VstPlugin"; info.path = path; - info.enabled = true; + info.state = state; return info; }; @@ -280,130 +289,245 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_Unre .WillByDefault(Return(foundPluginPaths)); } - for (const path_t& path : foundPluginPaths) { - ON_CALL(*m_knownPlugins, exists(path)) - .WillByDefault(Return(true)); - } - - // [THEN] Unreg the uninstalled plugins + // [THEN] Uninstalled plugins transition to Missing (kept in cache) AudioResourceIdList uninstalledPluginIdList { knownPlugins[0].meta.id, knownPlugins[1].meta.id }; - EXPECT_CALL(*m_knownPlugins, unregisterPlugins(uninstalledPluginIdList)) + EXPECT_CALL(*m_knownPlugins, setPluginsState(uninstalledPluginIdList, AudioPluginState::Missing)) .WillOnce(Return(make_ok())); - // [THEN] No new plugins to process - EXPECT_CALL(*m_process, execute(_, _)) - .Times(0); + EXPECT_CALL(*m_knownPlugins, setPluginsState(AudioResourceIdList {}, AudioPluginState::Validated)) + .WillOnce(Return(make_ok())); - EXPECT_CALL(*m_interactive, showProgress(_, _)) + EXPECT_CALL(*m_knownPlugins, unregisterPlugins(_)) .Times(0); EXPECT_CALL(*m_knownPlugins, load()) - .Times(0); + .WillOnce(Return(muse::make_ok())); // [WHEN] Update registry - m_scenario->updatePluginsRegistry(); + Ret ret = m_scenario->updatePluginsRegistry(); + + // [THEN] Successfully transitioned + EXPECT_TRUE(ret); } -//! A multi-component VST can expose several plugins (different IDs) from a single .vst3 path -//! When that path is still present on disk, none of the components should be re-registered -TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_SamePathDifferentIds_PluginPresent) +TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_RediscoverFormerlyMissing) { - path_t sharedPath = "/some/path/MultiPlugin.vst3"; + auto createPluginInfo = [](const io::path_t& path, AudioPluginState state) { + AudioPluginInfo info; + info.meta.id = io::completeBasename(path).toStdString(); + info.meta.type = "VstPlugin"; + info.path = path; + info.state = state; + return info; + }; - // [GIVEN] Two already registered components that share the same path + // [GIVEN] One Missing entry that gets reinstalled, one untouched Validated entry AudioPluginInfoList knownPlugins; - - AudioPluginInfo mono; - mono.path = sharedPath; - mono.meta.id = "MultiPlugin_Mono"; - knownPlugins.push_back(mono); - - AudioPluginInfo stereo; - stereo.path = sharedPath; - stereo.meta.id = "MultiPlugin_Stereo"; - knownPlugins.push_back(stereo); + knownPlugins.push_back(createPluginInfo("/some/path/AAA.vst3", AudioPluginState::Missing)); + knownPlugins.push_back(createPluginInfo("/some/path/BBB.vst3", AudioPluginState::Validated)); ON_CALL(*m_knownPlugins, pluginInfoList(_)) .WillByDefault(Return(knownPlugins)); - // [GIVEN] Scanner still finds the shared path + // [GIVEN] Scanner now finds both + paths_t foundPluginPaths { + "/some/path/AAA.vst3", + "/some/path/BBB.vst3", + }; + for (const IAudioPluginsScannerPtr& scanner : m_scanners) { AudioPluginsScannerMock* mock = dynamic_cast(scanner.get()); ASSERT_TRUE(mock); ON_CALL(*mock, scanPlugins(_)) - .WillByDefault(Return(paths_t { sharedPath })); + .WillByDefault(Return(foundPluginPaths)); } - ON_CALL(*m_knownPlugins, exists(sharedPath)) - .WillByDefault(Return(true)); + // [THEN] Only AAA gets transitioned back to Validated + AudioResourceIdList rediscoveredIds { knownPlugins[0].meta.id }; - // [THEN] Neither component is re-registered or unregistered - EXPECT_CALL(*m_process, execute(_, _)) - .Times(0); + EXPECT_CALL(*m_knownPlugins, setPluginsState(AudioResourceIdList {}, AudioPluginState::Missing)) + .WillOnce(Return(make_ok())); - EXPECT_CALL(*m_interactive, showProgress(_, _)) - .Times(0); + EXPECT_CALL(*m_knownPlugins, setPluginsState(rediscoveredIds, AudioPluginState::Validated)) + .WillOnce(Return(make_ok())); - EXPECT_CALL(*m_knownPlugins, unregisterPlugins(_)) - .Times(0); + EXPECT_CALL(*m_knownPlugins, load()) + .WillOnce(Return(muse::make_ok())); + + Ret ret = m_scenario->updatePluginsRegistry(); + EXPECT_TRUE(ret); +} + +TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_MultiPluginBinaryMarksEveryIdMissing) +{ + auto createPluginInfo = [](const io::path_t& path, const AudioResourceId& id, AudioPluginState state) { + AudioPluginInfo info; + info.meta.id = id; + info.meta.type = "VstPlugin"; + info.path = path; + info.state = state; + return info; + }; + + // [GIVEN] One binary path hosting two plugin ids (a shell / multi-effect + // bundle), plus a standalone plugin on another path. + const io::path_t shellPath = "/some/path/SHELL.vst3"; + const io::path_t soloPath = "/some/path/SOLO.vst3"; + + AudioPluginInfoList knownPlugins; + knownPlugins.push_back(createPluginInfo(shellPath, "Shell FxA", AudioPluginState::Validated)); + knownPlugins.push_back(createPluginInfo(shellPath, "Shell FxB", AudioPluginState::Validated)); + knownPlugins.push_back(createPluginInfo(soloPath, "Solo Fx", AudioPluginState::Validated)); + + ON_CALL(*m_knownPlugins, pluginInfoList(_)) + .WillByDefault(Return(knownPlugins)); + + // [GIVEN] The scanner no longer finds the shell binary; the solo plugin stays. + paths_t foundPluginPaths { soloPath }; + + for (const IAudioPluginsScannerPtr& scanner : m_scanners) { + AudioPluginsScannerMock* mock = dynamic_cast(scanner.get()); + ASSERT_TRUE(mock); + + ON_CALL(*mock, scanPlugins(_)) + .WillByDefault(Return(foundPluginPaths)); + } + + // [THEN] BOTH ids hosted by the missing binary transition to Missing — not + // just the last one (regression guard for per-path multi-id tracking). + AudioResourceIdList expectedMissing { "Shell FxA", "Shell FxB" }; + + EXPECT_CALL(*m_knownPlugins, setPluginsState(expectedMissing, AudioPluginState::Missing)) + .WillOnce(Return(make_ok())); + EXPECT_CALL(*m_knownPlugins, setPluginsState(AudioResourceIdList {}, AudioPluginState::Validated)) + .WillOnce(Return(make_ok())); EXPECT_CALL(*m_knownPlugins, load()) - .Times(0); + .WillOnce(Return(make_ok())); - // [WHEN] Update registry - m_scenario->updatePluginsRegistry(); + Ret ret = m_scenario->updatePluginsRegistry(); + EXPECT_TRUE(ret); } -//! When a multi-component plugin is uninstalled, ALL of its component IDs must be unregistered -TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_SamePathDifferentIds_PluginMissing) +TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_MultiPluginBinaryRediscoversEveryId) { - path_t sharedPath = "/some/path/MultiPlugin.vst3"; + auto createPluginInfo = [](const io::path_t& path, const AudioResourceId& id, AudioPluginState state) { + AudioPluginInfo info; + info.meta.id = id; + info.meta.type = "VstPlugin"; + info.path = path; + info.state = state; + return info; + }; + + // [GIVEN] A multi-effect bundle whose two ids are both currently Missing. + const io::path_t shellPath = "/some/path/SHELL.vst3"; - // [GIVEN] Two already registered components that share the same path AudioPluginInfoList knownPlugins; + knownPlugins.push_back(createPluginInfo(shellPath, "Shell FxA", AudioPluginState::Missing)); + knownPlugins.push_back(createPluginInfo(shellPath, "Shell FxB", AudioPluginState::Missing)); + + ON_CALL(*m_knownPlugins, pluginInfoList(_)) + .WillByDefault(Return(knownPlugins)); + + // [GIVEN] The scanner finds the binary again. + paths_t foundPluginPaths { shellPath }; + + for (const IAudioPluginsScannerPtr& scanner : m_scanners) { + AudioPluginsScannerMock* mock = dynamic_cast(scanner.get()); + ASSERT_TRUE(mock); + + ON_CALL(*mock, scanPlugins(_)) + .WillByDefault(Return(foundPluginPaths)); + } + + // [THEN] BOTH ids transition back to Validated — not just the last one. + AudioResourceIdList expectedRediscovered { "Shell FxA", "Shell FxB" }; + + EXPECT_CALL(*m_knownPlugins, setPluginsState(AudioResourceIdList {}, AudioPluginState::Missing)) + .WillOnce(Return(make_ok())); + EXPECT_CALL(*m_knownPlugins, setPluginsState(expectedRediscovered, AudioPluginState::Validated)) + .WillOnce(Return(make_ok())); + + EXPECT_CALL(*m_knownPlugins, load()) + .WillOnce(Return(make_ok())); + + Ret ret = m_scenario->updatePluginsRegistry(); + EXPECT_TRUE(ret); +} - AudioPluginInfo mono; - mono.path = sharedPath; - mono.meta.id = "MultiPlugin_Mono"; - knownPlugins.push_back(mono); +TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_LeftoverDiscoveredRevalidates) +{ + // [GIVEN] A Discovered entry from a previous scan that didn't complete + // (host crashed mid-validation). The scanner still sees the path on disk. + auto createPluginInfo = [](const io::path_t& path, AudioPluginState state) { + AudioPluginInfo info; + info.meta.id = io::completeBasename(path).toStdString(); + info.meta.type = "VstPlugin"; + info.path = path; + info.state = state; + return info; + }; - AudioPluginInfo stereo; - stereo.path = sharedPath; - stereo.meta.id = "MultiPlugin_Stereo"; - knownPlugins.push_back(stereo); + AudioPluginInfoList knownPlugins; + knownPlugins.push_back(createPluginInfo("/some/path/CRASHED.vst3", AudioPluginState::Discovered)); ON_CALL(*m_knownPlugins, pluginInfoList(_)) .WillByDefault(Return(knownPlugins)); - // [GIVEN] Scanner finds nothing — the plugin has been uninstalled + paths_t foundPluginPaths { "/some/path/CRASHED.vst3" }; + for (const IAudioPluginsScannerPtr& scanner : m_scanners) { AudioPluginsScannerMock* mock = dynamic_cast(scanner.get()); ASSERT_TRUE(mock); ON_CALL(*mock, scanPlugins(_)) - .WillByDefault(Return(paths_t {})); + .WillByDefault(Return(foundPluginPaths)); } - // [THEN] Both component IDs are unregistered - AudioResourceIdList expectedIds { mono.meta.id, stereo.meta.id }; - EXPECT_CALL(*m_knownPlugins, unregisterPlugins(expectedIds)) + // [THEN] The Discovered path is treated as new — registered as a fresh + // placeholder and re-validated via subprocess. It is NOT marked Missing + // (it's still on disk) and it is NOT considered "rediscovered" (that's + // for paths transitioning out of Missing). + EXPECT_CALL(*m_knownPlugins, setPluginsState(AudioResourceIdList {}, AudioPluginState::Missing)) + .WillOnce(Return(make_ok())); + EXPECT_CALL(*m_knownPlugins, setPluginsState(AudioResourceIdList {}, AudioPluginState::Validated)) .WillOnce(Return(make_ok())); - EXPECT_CALL(*m_process, execute(_, _)) - .Times(0); + // [THEN] registerNewPlugins writes a Discovered placeholder for the path + // before spawning the subprocess. + AudioPluginInfo expectedPlaceholder = createPluginInfo("/some/path/CRASHED.vst3", + AudioPluginState::Discovered); + EXPECT_CALL(*m_knownPlugins, registerPlugins(AudioPluginInfoList { expectedPlaceholder })) + .WillOnce(Return(make_ok())); - EXPECT_CALL(*m_interactive, showProgress(_, _)) - .Times(0); + // [THEN] The path is cleared once, inside persistDiscoveredPlaceholders, + // so a leftover Discovered entry from a previous interrupted run doesn't + // trip registerPlugins's same-id-same-path assertion. The subprocess + // takes care of clearing the placeholder again before persisting its + // Validated/Error result (see RegisterPlugin / RegisterFailedPlugin + // tests); the main process must NOT do it again here because that would + // operate on its stale in-memory register and clobber the accumulated + // results of previous subprocesses. + EXPECT_CALL(*m_knownPlugins, removePluginsAtPath(io::path_t("/some/path/CRASHED.vst3"))) + .Times(1) + .WillRepeatedly(Return(make_ok())); + + EXPECT_CALL(*m_process, execute(m_appPath, + std::vector { "--register-audio-plugin", "/some/path/CRASHED.vst3" })) + .WillOnce(Return(0)); + // [THEN] register loaded twice (once after registerNewPlugins, once at end of updatePluginsRegistry) EXPECT_CALL(*m_knownPlugins, load()) - .Times(0); + .Times(2) + .WillRepeatedly(Return(make_ok())); - // [WHEN] Update registry - m_scenario->updatePluginsRegistry(); + Ret ret = m_scenario->updatePluginsRegistry(); + EXPECT_TRUE(ret); } TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, RegisterPlugin) @@ -415,12 +539,12 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, RegisterPlugin) AudioResourceMeta pluginMeta1; pluginMeta1.id = "Mono plugin"; - pluginMeta1.attributes.insert({ muse::audio::CATEGORIES_ATTRIBUTE, u"Fx|Mono" }); + pluginMeta1.attributes.insert({ String(u"categories"), u"Fx|Mono" }); metaList.push_back(pluginMeta1); AudioResourceMeta pluginMeta2; pluginMeta2.id = "Stereo plugin"; - pluginMeta2.attributes.insert({ muse::audio::CATEGORIES_ATTRIBUTE, u"Fx|Stereo" }); + pluginMeta2.attributes.insert({ String(u"categories"), u"Fx|Stereo" }); metaList.push_back(pluginMeta2); ASSERT_FALSE(m_metaReaders.empty()); @@ -436,14 +560,20 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, RegisterPlugin) for (const AudioResourceMeta& meta : metaList) { AudioPluginInfo expectedPluginInfo; - expectedPluginInfo.type = AudioPluginType::Fx; expectedPluginInfo.meta = meta; expectedPluginInfo.path = pluginPath; - expectedPluginInfo.enabled = true; + expectedPluginInfo.state = AudioPluginState::Validated; expectedPluginInfo.errorCode = 0; expectedInfoList.emplace_back(std::move(expectedPluginInfo)); } + // [THEN] Any prior Discovered placeholder at this path is cleared before + // the real validated entries are persisted. Subprocess-side, this is what + // lets a single path with N sub-effects replace the 1 placeholder entry + // with N Validated ones without tripping the same-id-same-path guard. + ::testing::InSequence seq; + EXPECT_CALL(*m_knownPlugins, removePluginsAtPath(pluginPath)) + .WillOnce(Return(make_ok())); EXPECT_CALL(*m_knownPlugins, registerPlugins(expectedInfoList)) .WillOnce(Return(true)); @@ -462,11 +592,18 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, RegisterFailedPlugin) // [THEN] The plugin has been registered AudioPluginInfo expectedPluginInfo; expectedPluginInfo.meta.id = io::completeBasename(pluginPath).toStdString(); - expectedPluginInfo.meta.type = AudioResourceType::VstPlugin; + expectedPluginInfo.meta.type = "VstPlugin"; expectedPluginInfo.path = pluginPath; - expectedPluginInfo.enabled = false; + expectedPluginInfo.state = AudioPluginState::Error; expectedPluginInfo.errorCode = -42; + // [THEN] Same as RegisterPlugin: clear the prior placeholder first. Here + // it's load-bearing — the failed entry uses the basename as its id, which + // is the same id the Discovered placeholder used, so without the prior + // remove registerPlugins would hit the same-id-same-path guard. + ::testing::InSequence seq; + EXPECT_CALL(*m_knownPlugins, removePluginsAtPath(pluginPath)) + .WillOnce(Return(make_ok())); EXPECT_CALL(*m_knownPlugins, registerPlugins(AudioPluginInfoList { expectedPluginInfo })) .WillOnce(Return(true)); @@ -476,3 +613,77 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, RegisterFailedPlugin) // [THEN] The plugin successfully registered EXPECT_TRUE(ret); } + +TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, RegisterNewPlugins_ValidateFalsePersistsDiscoveredOnly) +{ + // [GIVEN] Two new plugin paths the host wants to record without validating + // (the "Skip this time" path on the validate prompt). + paths_t paths { + "/some/path/AAA.vst3", + "/some/path/BBB.vst3", + }; + + // [THEN] persistDiscoveredPlaceholders runs: one removePluginsAtPath per + // path (cleaning any leftover from a prior interrupted run) followed by a + // single batch registerPlugins of the Discovered placeholders. + EXPECT_CALL(*m_knownPlugins, removePluginsAtPath(io::path_t("/some/path/AAA.vst3"))) + .WillOnce(Return(make_ok())); + EXPECT_CALL(*m_knownPlugins, removePluginsAtPath(io::path_t("/some/path/BBB.vst3"))) + .WillOnce(Return(make_ok())); + EXPECT_CALL(*m_knownPlugins, registerPlugins(_)) + .WillOnce(Return(make_ok())); + + // [THEN] No out-of-process validation: no subprocess, no progress dialog. + EXPECT_CALL(*m_process, execute(_, _)) + .Times(0); + EXPECT_CALL(*m_interactive, showProgress(_, _)) + .Times(0); + + // [THEN] Final load() resyncs the register. + EXPECT_CALL(*m_knownPlugins, load()) + .WillOnce(Return(make_ok())); + + // [WHEN] Register with validation deferred + EXPECT_TRUE(m_scenario->registerNewPlugins(paths, /*validate*/ false)); +} + +TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, RegisterNewPlugins_NoPerIterationClobber) +{ + // [GIVEN] Three new plugin paths to scan + paths_t paths { + "/some/path/AAA.vst3", + "/some/path/BBB.vst3", + "/some/path/CCC.vst3", + }; + + // [THEN] removePluginsAtPath is called exactly once per path — inside + // persistDiscoveredPlaceholders only. The subprocess loop in + // processPluginsRegistration must NOT call it again on the main process's + // register; doing so would write the main's stale in-memory state to disk + // and clobber the Validated entries previous subprocesses had written. + // (The subprocess-side clearing now lives in registerPlugin / + // registerFailedPlugin and runs against a freshly-loaded register.) + EXPECT_CALL(*m_knownPlugins, removePluginsAtPath(_)) + .Times(3) + .WillRepeatedly(Return(make_ok())); + + EXPECT_CALL(*m_knownPlugins, registerPlugins(_)) + .Times(1) + .WillOnce(Return(make_ok())); + + // [THEN] One subprocess invocation per path + for (const path_t& path : paths) { + std::vector args = { "--register-audio-plugin", path.toStdString() }; + EXPECT_CALL(*m_process, execute(m_appPath, args)) + .WillOnce(Return(0)); + } + + EXPECT_CALL(*m_interactive, showProgress(_, _)) + .Times(1); + + EXPECT_CALL(*m_knownPlugins, load()) + .WillOnce(Return(make_ok())); + + // [WHEN] Register with validation enabled + EXPECT_TRUE(m_scenario->registerNewPlugins(paths, /*validate*/ true)); +} diff --git a/framework/musesampler/internal/musesamplerresolver.cpp b/framework/musesampler/internal/musesamplerresolver.cpp index c8efce503a..d46eef9ea3 100644 --- a/framework/musesampler/internal/musesamplerresolver.cpp +++ b/framework/musesampler/internal/musesamplerresolver.cpp @@ -201,7 +201,7 @@ AudioResourceMetaList MuseSamplerResolver::resolveResources() const AudioResourceMeta meta; meta.id = std::to_string(instrumentId); - meta.type = AudioResourceType::MuseSamplerSoundPack; + meta.type = "MuseSamplerSoundPack"; meta.vendor = "MuseSounds"; meta.attributes = { { u"playbackSetupData", instrumentSoundId }, diff --git a/framework/vst/internal/fx/vstfxprocessor.cpp b/framework/vst/internal/fx/vstfxprocessor.cpp index 5aca77ad50..72297e5d4d 100644 --- a/framework/vst/internal/fx/vstfxprocessor.cpp +++ b/framework/vst/internal/fx/vstfxprocessor.cpp @@ -42,7 +42,7 @@ void VstFxProcessor::init(const audio::OutputSpec& spec) m_outputSpec = spec; - m_vstAudioClient->init(AudioPluginType::Fx, m_pluginPtr); + m_vstAudioClient->init(PluginType::Fx, m_pluginPtr); auto onPluginLoaded = [this]() { m_pluginPtr->updatePluginConfig(m_params.configuration); diff --git a/framework/vst/internal/synth/vstsynthesiser.cpp b/framework/vst/internal/synth/vstsynthesiser.cpp index 041f50987b..79bc46b513 100644 --- a/framework/vst/internal/synth/vstsynthesiser.cpp +++ b/framework/vst/internal/synth/vstsynthesiser.cpp @@ -59,7 +59,7 @@ void VstSynthesiser::init(const OutputSpec& spec) m_pluginPtr = instancesRegister()->makeAndRegisterInstrPlugin(m_params.resourceMeta.id, m_trackId); - m_vstAudioClient->init(AudioPluginType::Instrument, m_pluginPtr); + m_vstAudioClient->init(PluginType::Instrument, m_pluginPtr); auto onPluginLoaded = [this]() { m_pluginPtr->updatePluginConfig(m_params.configuration); diff --git a/framework/vst/internal/vstaudioclient.cpp b/framework/vst/internal/vstaudioclient.cpp index a17f2b9be0..be82b789e0 100644 --- a/framework/vst/internal/vstaudioclient.cpp +++ b/framework/vst/internal/vstaudioclient.cpp @@ -72,9 +72,9 @@ VstAudioClient::~VstAudioClient() // editor view is destroyed first (required by ZENOLOGY). } -void VstAudioClient::init(AudioPluginType type, IVstPluginInstancePtr instance) +void VstAudioClient::init(PluginType type, IVstPluginInstancePtr instance) { - IF_ASSERT_FAILED(instance && type != AudioPluginType::Undefined) { + IF_ASSERT_FAILED(instance && type != PluginType::Undefined) { return; } @@ -273,7 +273,7 @@ audio::samples_t VstAudioClient::process(float* output, samples_t samplesPerChan setOutputSpec(newSpec); } - if (m_type == AudioPluginType::Fx) { + if (m_type == PluginType::Fx) { extractInputSamples(samplesPerChannel, output); } @@ -283,7 +283,7 @@ audio::samples_t VstAudioClient::process(float* output, samples_t samplesPerChan m_needUpdateState = false; - if (m_type == AudioPluginType::Instrument) { + if (m_type == PluginType::Instrument) { m_inputEvents.clear(); m_inputParamChanges.clearQueue(); diff --git a/framework/vst/internal/vstaudioclient.h b/framework/vst/internal/vstaudioclient.h index 88b5ad1bde..2c2672ca2a 100644 --- a/framework/vst/internal/vstaudioclient.h +++ b/framework/vst/internal/vstaudioclient.h @@ -21,8 +21,6 @@ */ #pragma once -#include "audioplugins/audiopluginstypes.h" - #include "../ivstplugininstance.h" #include "../vsttypes.h" @@ -40,7 +38,7 @@ class VstAudioClient VstAudioClient(); ~VstAudioClient(); - void init(audioplugins::AudioPluginType type, IVstPluginInstancePtr instance); + void init(PluginType type, IVstPluginInstancePtr instance); void loadSupportedParams(); void setIsActive(const bool isActive); @@ -102,7 +100,7 @@ class VstAudioClient bool m_needUnprepareProcessData = false; bool m_needUpdateState = false; - audioplugins::AudioPluginType m_type = audioplugins::AudioPluginType::Undefined; + PluginType m_type = PluginType::Undefined; audio::OutputSpec m_outputSpec; midiremote::IMMCDecoderPtr m_mmcDecoder; diff --git a/framework/vst/internal/vstmodulesrepository.cpp b/framework/vst/internal/vstmodulesrepository.cpp index e824327af1..11246c67cd 100644 --- a/framework/vst/internal/vstmodulesrepository.cpp +++ b/framework/vst/internal/vstmodulesrepository.cpp @@ -104,7 +104,7 @@ muse::audio::AudioResourceMetaList VstModulesRepository::instrumentModulesMeta() std::lock_guard lock(m_mutex); - return modulesMetaList(audioplugins::AudioPluginType::Instrument); + return modulesMetaList(PluginType::Instrument); } muse::audio::AudioResourceMetaList VstModulesRepository::fxModulesMeta() const @@ -113,17 +113,22 @@ muse::audio::AudioResourceMetaList VstModulesRepository::fxModulesMeta() const std::lock_guard lock(m_mutex); - return modulesMetaList(audioplugins::AudioPluginType::Fx); + return modulesMetaList(PluginType::Fx); } void VstModulesRepository::refresh() { } -muse::audio::AudioResourceMetaList VstModulesRepository::modulesMetaList(const audioplugins::AudioPluginType& type) const +muse::audio::AudioResourceMetaList VstModulesRepository::modulesMetaList(PluginType type) const { auto infoAccepted = [type](const audioplugins::AudioPluginInfo& info) { - return info.type == type && info.meta.type == muse::audio::AudioResourceType::VstPlugin && info.enabled; + if (info.meta.type != "VstPlugin" || info.state != audioplugins::AudioPluginState::Validated) { + return false; + } + const String& categories = info.meta.attributeVal(muse::audio::CATEGORIES_ATTRIBUTE); + const bool isInstrument = categories.contains(u"Instrument"); + return type == PluginType::Instrument ? isInstrument : !isInstrument; }; audioplugins::AudioPluginInfoList infoList = knownPlugins()->pluginInfoList(infoAccepted); diff --git a/framework/vst/internal/vstmodulesrepository.h b/framework/vst/internal/vstmodulesrepository.h index 2643206ceb..23775be595 100644 --- a/framework/vst/internal/vstmodulesrepository.h +++ b/framework/vst/internal/vstmodulesrepository.h @@ -31,7 +31,6 @@ #include "audioplugins/iknownaudiopluginsregister.h" #include "audio/common/iaudiothreadsecurer.h" -#include "audioplugins/audiopluginstypes.h" #include "vsttypes.h" namespace muse::vst { @@ -56,7 +55,7 @@ class VstModulesRepository : public IVstModulesRepository void refresh() override; private: - audio::AudioResourceMetaList modulesMetaList(const audioplugins::AudioPluginType& type) const; + audio::AudioResourceMetaList modulesMetaList(PluginType type) const; PluginContext m_pluginContext; diff --git a/framework/vst/internal/vstpluginmetareader.cpp b/framework/vst/internal/vstpluginmetareader.cpp index 41f9d0af05..02b8c64353 100644 --- a/framework/vst/internal/vstpluginmetareader.cpp +++ b/framework/vst/internal/vstpluginmetareader.cpp @@ -33,7 +33,7 @@ using namespace muse::vst; audio::AudioResourceType VstPluginMetaReader::metaType() const { - return audio::AudioResourceType::VstPlugin; + return "VstPlugin"; } bool VstPluginMetaReader::canReadMeta(const io::path_t& pluginPath) const @@ -58,10 +58,10 @@ RetVal VstPluginMetaReader::readMeta(const io::path_t& pl muse::audio::AudioResourceMeta meta; meta.id = io::completeBasename(pluginPath).toStdString(); - meta.type = muse::audio::AudioResourceType::VstPlugin; + meta.type = "VstPlugin"; meta.attributes.emplace(muse::audio::CATEGORIES_ATTRIBUTE, String::fromStdString(classInfo.subCategoriesString())); + meta.attributes.emplace(muse::audio::HAS_NATIVE_EDITOR_SUPPORT_ATTRIBUTE, u"true"); meta.vendor = classInfo.vendor(); - meta.hasNativeEditorSupport = true; result.emplace_back(std::move(meta)); break; diff --git a/framework/vst/vsttypes.h b/framework/vst/vsttypes.h index f338baa51c..e0627bb004 100644 --- a/framework/vst/vsttypes.h +++ b/framework/vst/vsttypes.h @@ -81,6 +81,12 @@ using ParamsMapping = std::unordered_map; static const std::string VST3_PACKAGE_EXTENSION = "vst3"; static const std::string VST3_PACKAGE_FILTER = "*." + VST3_PACKAGE_EXTENSION; +enum class PluginType { + Undefined = -1, + Instrument, + Fx, +}; + /// @see https://steinbergmedia.github.io/vst3_doc/vstinterfaces/namespaceSteinberg_1_1Vst_1_1PlugType.html namespace PluginCategory { static constexpr std::string_view Analyzer { "Analyzer" }; From 32298dbdb811051087113357719b5780f46b7304 Mon Sep 17 00:00:00 2001 From: Paul MARTIN Date: Mon, 11 May 2026 12:10:39 +0200 Subject: [PATCH 2/9] [vst] decouple plugin attributes; fix metaType signature Move CATEGORIES_ATTRIBUTE from audio/common/audiotypes.h into a new vst/vstpluginattrs.h header so VST consumers (including hosts that don't link the audio module) can use it without pulling audio. Fix VstPluginMetaReader::metaType() override return type to match the IAudioPluginMetaReader interface (audioplugins::AudioResourceType, not audio::AudioResourceType which is now an engine enum). --- framework/audio/common/audiotypes.h | 45 ++++----- framework/audio/common/audioutils.h | 6 +- .../internal/audioengineconfiguration.cpp | 2 +- .../engine/internal/enginerpccontroller.cpp | 4 +- .../synthesizers/fluidsynth/fluidresolver.cpp | 4 +- framework/audio/tests/CMakeLists.txt | 1 + .../audio/tests/audioresourcetypes_tests.cpp | 91 +++++++++++++++++++ .../internal/musesamplerresolver.cpp | 2 +- framework/musesampler/musesamplertypes.h | 4 + framework/vst/CMakeLists.txt | 1 + .../vst/internal/vstmodulesrepository.cpp | 7 +- .../vst/internal/vstpluginmetareader.cpp | 15 +-- framework/vst/internal/vstpluginmetareader.h | 4 +- framework/vst/vstpluginattrs.h | 40 ++++++++ framework/vst/vsttypes.h | 2 + 15 files changed, 186 insertions(+), 42 deletions(-) create mode 100644 framework/audio/tests/audioresourcetypes_tests.cpp create mode 100644 framework/vst/vstpluginattrs.h diff --git a/framework/audio/common/audiotypes.h b/framework/audio/common/audiotypes.h index 6ffc653d2b..fd347803a4 100644 --- a/framework/audio/common/audiotypes.h +++ b/framework/audio/common/audiotypes.h @@ -180,30 +180,38 @@ using AudioResourceMeta = muse::audioplugins::AudioResourceMeta; using AudioResourceMetaList = muse::audioplugins::AudioResourceMetaList; using AudioResourceMetaSet = muse::audioplugins::AudioResourceMetaSet; +// Wire-format identifiers for the formats the muse audio engine dispatches +// directly. The strings here are the canonical names persisted in the JSON +// plugin cache. VST and MuseSampler also own their own per-module copies of +// the matching string in their respective public headers; a round-trip test +// in muse_audio_tests asserts they stay in sync. +// +// constexpr std::string_view (not inline std::string) so that the values are +// available at compile time and don't participate in static initialization +// order — the RESOURCE_TYPE_NAMES map below depends on them. +inline constexpr std::string_view FLUID_SOUNDFONT_TYPE_NAME = "FluidSoundfont"; +inline constexpr std::string_view NATIVE_EFFECT_TYPE_NAME = "NativeEffect"; + // audio::AudioResourceType is an audio-engine-internal enum used to dispatch // synth/fx routing. It is intentionally kept separate from the framework's // opaque audioplugins::AudioResourceType (a std::string identifier persisted // by the audioplugins module). Map between them at the boundary via -// resourceTypeFromString() / resourceTypeName(). +// resourceTypeFromString() / resourceTypeName(). The enum lists only the +// formats the audio engine actually routes; apps with broader plugin support +// (e.g. Audacity's LV2/AU/Nyquist) define their own enum and bridge. enum class AudioResourceType { Undefined = -1, FluidSoundfont, VstPlugin, NativeEffect, MuseSamplerSoundPack, - Lv2Plugin, - AudioUnit, - NyquistPlugin, }; static const std::map RESOURCE_TYPE_NAMES = { - { AudioResourceType::FluidSoundfont, "FluidSoundfont" }, - { AudioResourceType::VstPlugin, "VstPlugin" }, - { AudioResourceType::NativeEffect, "NativeEffect" }, - { AudioResourceType::MuseSamplerSoundPack, "MuseSamplerSoundPack" }, - { AudioResourceType::Lv2Plugin, "Lv2Plugin" }, - { AudioResourceType::AudioUnit, "AudioUnit" }, - { AudioResourceType::NyquistPlugin, "NyquistPlugin" }, + { AudioResourceType::FluidSoundfont, std::string(FLUID_SOUNDFONT_TYPE_NAME) }, + { AudioResourceType::VstPlugin, "VstPlugin" }, + { AudioResourceType::NativeEffect, std::string(NATIVE_EFFECT_TYPE_NAME) }, + { AudioResourceType::MuseSamplerSoundPack, "MuseSamplerSoundPack" }, }; inline const std::string& resourceTypeName(AudioResourceType type) @@ -226,8 +234,12 @@ inline AudioResourceType resourceTypeFromString(const std::string& name) return AudioResourceType::Undefined; } +inline bool isResourceType(const AudioResourceMeta& meta, AudioResourceType type) +{ + return resourceTypeFromString(meta.type) == type; +} + static const String PLAYBACK_SETUP_DATA_ATTRIBUTE(u"playbackSetupData"); -static const String CATEGORIES_ATTRIBUTE(u"categories"); static const String HAS_NATIVE_EDITOR_SUPPORT_ATTRIBUTE(u"hasNativeEditorSupport"); inline bool hasNativeEditorSupport(const AudioResourceMeta& meta) @@ -241,9 +253,6 @@ static const std::map RESOURCE_TYPE_MAP = { { AudioResourceType::FluidSoundfont, "fluid_soundfont" }, { AudioResourceType::VstPlugin, "vst_plugin" }, { AudioResourceType::NativeEffect, "muse_plugin" }, - { AudioResourceType::Lv2Plugin, "lv2_plugin" }, - { AudioResourceType::AudioUnit, "audio_unit" }, - { AudioResourceType::NyquistPlugin, "nyquist_plugin" }, }; static const AudioResourceId MUSE_REVERB_ID("Muse Reverb"); @@ -306,11 +315,8 @@ struct AudioFxParams { switch (resourceTypeFromString(resourceMeta.type)) { case AudioResourceType::VstPlugin: return AudioFxType::VstFx; case AudioResourceType::NativeEffect: return AudioFxType::MuseFx; - case AudioResourceType::AudioUnit: - case AudioResourceType::Lv2Plugin: case AudioResourceType::FluidSoundfont: case AudioResourceType::MuseSamplerSoundPack: - case AudioResourceType::NyquistPlugin: case AudioResourceType::Undefined: break; } @@ -388,10 +394,7 @@ inline AudioSourceType sourceTypeFromResourceType(const muse::audioplugins::Audi case AudioResourceType::FluidSoundfont: return AudioSourceType::Fluid; case AudioResourceType::VstPlugin: return AudioSourceType::Vsti; case AudioResourceType::MuseSamplerSoundPack: return AudioSourceType::MuseSampler; - case AudioResourceType::AudioUnit: - case AudioResourceType::Lv2Plugin: case AudioResourceType::NativeEffect: - case AudioResourceType::NyquistPlugin: case AudioResourceType::Undefined: break; } diff --git a/framework/audio/common/audioutils.h b/framework/audio/common/audioutils.h index 4656f590b7..ecafab1511 100644 --- a/framework/audio/common/audioutils.h +++ b/framework/audio/common/audioutils.h @@ -30,7 +30,7 @@ inline AudioResourceMeta makeReverbMeta() { AudioResourceMeta meta; meta.id = MUSE_REVERB_ID; - meta.type = "NativeEffect"; + meta.type = NATIVE_EFFECT_TYPE_NAME; meta.vendor = "Muse"; meta.attributes.emplace(HAS_NATIVE_EDITOR_SUPPORT_ATTRIBUTE, u"true"); @@ -55,7 +55,7 @@ inline String audioSourceName(const AudioInputParams& params) return params.resourceMeta.attributeVal(u"museName"); } - if (params.resourceMeta.type == "FluidSoundfont") { + if (isResourceType(params.resourceMeta, AudioResourceType::FluidSoundfont)) { const String& presetName = params.resourceMeta.attributeVal(synth::PRESET_NAME_ATTRIBUTE); if (!presetName.empty()) { return presetName; @@ -85,7 +85,7 @@ inline String audioSourceCategoryName(const AudioInputParams& params) return params.resourceMeta.attributeVal(u"museCategory"); } - if (params.resourceMeta.type == "FluidSoundfont") { + if (isResourceType(params.resourceMeta, AudioResourceType::FluidSoundfont)) { return params.resourceMeta.attributeVal(synth::SOUNDFONT_NAME_ATTRIBUTE); } diff --git a/framework/audio/engine/internal/audioengineconfiguration.cpp b/framework/audio/engine/internal/audioengineconfiguration.cpp index e486c7826b..a46f09fa83 100644 --- a/framework/audio/engine/internal/audioengineconfiguration.cpp +++ b/framework/audio/engine/internal/audioengineconfiguration.cpp @@ -41,7 +41,7 @@ static const AudioResourceMeta DEFAULT_AUDIO_RESOURCE_META = { DEFAULT_SOUND_FONT_NAME, "Fluid", DEFAULT_AUDIO_RESOURCE_ATTRIBUTES, - "FluidSoundfont" }; + std::string(FLUID_SOUNDFONT_TYPE_NAME) }; void AudioEngineConfiguration::setConfig(const AudioEngineConfig& conf) { diff --git a/framework/audio/engine/internal/enginerpccontroller.cpp b/framework/audio/engine/internal/enginerpccontroller.cpp index a8ce339e10..cec6af24f6 100644 --- a/framework/audio/engine/internal/enginerpccontroller.cpp +++ b/framework/audio/engine/internal/enginerpccontroller.cpp @@ -225,10 +225,8 @@ void EngineRpcController::init() } }; - const std::string& resourceType = params.source.resourceMeta.type; - // Not Fluid - if (resourceType != "FluidSoundfont") { + if (!isResourceType(params.source.resourceMeta, AudioResourceType::FluidSoundfont)) { addTrackAndSendResponse(msg, trackName, playbackData, params); return make_response_delayed(msg); } diff --git a/framework/audio/engine/internal/synthesizers/fluidsynth/fluidresolver.cpp b/framework/audio/engine/internal/synthesizers/fluidsynth/fluidresolver.cpp index 714f06ca02..ffef69250d 100644 --- a/framework/audio/engine/internal/synthesizers/fluidsynth/fluidresolver.cpp +++ b/framework/audio/engine/internal/synthesizers/fluidsynth/fluidresolver.cpp @@ -113,7 +113,7 @@ void FluidResolver::refresh() AudioResourceMeta chooseAutomaticMeta; chooseAutomaticMeta.id = id; - chooseAutomaticMeta.type = "FluidSoundfont"; + chooseAutomaticMeta.type = FLUID_SOUNDFONT_TYPE_NAME; chooseAutomaticMeta.vendor = FLUID_VENDOR_NAME; chooseAutomaticMeta.attributes = { { PLAYBACK_SETUP_DATA_ATTRIBUTE, muse::mpe::GENERIC_SETUP_DATA_STRING }, @@ -128,7 +128,7 @@ void FluidResolver::refresh() AudioResourceMeta meta; meta.id = id; - meta.type = "FluidSoundfont"; + meta.type = FLUID_SOUNDFONT_TYPE_NAME; meta.vendor = FLUID_VENDOR_NAME; meta.attributes = { { PLAYBACK_SETUP_DATA_ATTRIBUTE, muse::mpe::GENERIC_SETUP_DATA_STRING }, diff --git a/framework/audio/tests/CMakeLists.txt b/framework/audio/tests/CMakeLists.txt index 444acbee33..eac52d9735 100644 --- a/framework/audio/tests/CMakeLists.txt +++ b/framework/audio/tests/CMakeLists.txt @@ -23,6 +23,7 @@ set(MODULE_TEST muse_audio_tests) set(MODULE_TEST_SRC ${CMAKE_CURRENT_LIST_DIR}/rpcpacker_tests.cpp ${CMAKE_CURRENT_LIST_DIR}/alignbuffer_tests.cpp + ${CMAKE_CURRENT_LIST_DIR}/audioresourcetypes_tests.cpp ) include(SetupGTest) diff --git a/framework/audio/tests/audioresourcetypes_tests.cpp b/framework/audio/tests/audioresourcetypes_tests.cpp new file mode 100644 index 0000000000..147f94ab1d --- /dev/null +++ b/framework/audio/tests/audioresourcetypes_tests.cpp @@ -0,0 +1,91 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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, see . + */ +#include + +// muse_audio_tests builds with byte-packed audio types; matching rpcpacker_tests.cpp +// is necessary to avoid an ODR-violation segfault at static init. +#pragma pack(push, 1) +#include "audio/common/audiotypes.h" +#include "musesampler/musesamplertypes.h" +#include "vst/vstpluginattrs.h" +#pragma pack(pop) + +using namespace muse; + +// The on-disk plugin cache stores AudioResourceMeta::type as the canonical +// wire string. These strings must stay stable across releases — caches written +// by older builds must remain readable. Each plugin module owns its own copy +// of the wire string; this test asserts (a) those module-owned constants match +// the canonical strings the framework engine routes on, and (b) the round-trip +// resourceTypeName -> resourceTypeFromString preserves the enum value. +TEST(Audio_AudioResourceTypes, WireStringsAreCanonical) +{ + EXPECT_EQ(audio::FLUID_SOUNDFONT_TYPE_NAME, "FluidSoundfont"); + EXPECT_EQ(audio::NATIVE_EFFECT_TYPE_NAME, "NativeEffect"); + EXPECT_EQ(vst::AUDIO_RESOURCE_TYPE_NAME, "VstPlugin"); + EXPECT_EQ(musesampler::AUDIO_RESOURCE_TYPE_NAME, "MuseSamplerSoundPack"); +} + +TEST(Audio_AudioResourceTypes, ResourceTypeNameMatchesModuleConstants) +{ + EXPECT_EQ(audio::resourceTypeName(audio::AudioResourceType::FluidSoundfont), + audio::FLUID_SOUNDFONT_TYPE_NAME); + EXPECT_EQ(audio::resourceTypeName(audio::AudioResourceType::NativeEffect), + audio::NATIVE_EFFECT_TYPE_NAME); + EXPECT_EQ(audio::resourceTypeName(audio::AudioResourceType::VstPlugin), + vst::AUDIO_RESOURCE_TYPE_NAME); + EXPECT_EQ(audio::resourceTypeName(audio::AudioResourceType::MuseSamplerSoundPack), + musesampler::AUDIO_RESOURCE_TYPE_NAME); +} + +TEST(Audio_AudioResourceTypes, ResourceTypeFromStringRoundTrips) +{ + for (auto type : { audio::AudioResourceType::FluidSoundfont, + audio::AudioResourceType::VstPlugin, + audio::AudioResourceType::NativeEffect, + audio::AudioResourceType::MuseSamplerSoundPack }) { + EXPECT_EQ(audio::resourceTypeFromString(audio::resourceTypeName(type)), type); + } +} + +TEST(Audio_AudioResourceTypes, ResourceTypeFromStringRejectsUnknown) +{ + // App-specific wire strings (Audacity's AU/LV2/Nyquist) are not part of the + // framework engine enum and must resolve to Undefined. + EXPECT_EQ(audio::resourceTypeFromString("AudioUnit"), audio::AudioResourceType::Undefined); + EXPECT_EQ(audio::resourceTypeFromString("Lv2Plugin"), audio::AudioResourceType::Undefined); + EXPECT_EQ(audio::resourceTypeFromString("NyquistPlugin"), audio::AudioResourceType::Undefined); + EXPECT_EQ(audio::resourceTypeFromString(""), audio::AudioResourceType::Undefined); + EXPECT_EQ(audio::resourceTypeFromString("Garbage"), audio::AudioResourceType::Undefined); +} + +TEST(Audio_AudioResourceTypes, IsResourceTypeHelper) +{ + audioplugins::AudioResourceMeta meta; + meta.type = vst::AUDIO_RESOURCE_TYPE_NAME; + EXPECT_TRUE(audio::isResourceType(meta, audio::AudioResourceType::VstPlugin)); + EXPECT_FALSE(audio::isResourceType(meta, audio::AudioResourceType::FluidSoundfont)); + + meta.type = "AudioUnit"; // app-specific, not in framework enum + EXPECT_FALSE(audio::isResourceType(meta, audio::AudioResourceType::VstPlugin)); + EXPECT_TRUE(audio::isResourceType(meta, audio::AudioResourceType::Undefined)); +} diff --git a/framework/musesampler/internal/musesamplerresolver.cpp b/framework/musesampler/internal/musesamplerresolver.cpp index d46eef9ea3..5b3ae15535 100644 --- a/framework/musesampler/internal/musesamplerresolver.cpp +++ b/framework/musesampler/internal/musesamplerresolver.cpp @@ -201,7 +201,7 @@ AudioResourceMetaList MuseSamplerResolver::resolveResources() const AudioResourceMeta meta; meta.id = std::to_string(instrumentId); - meta.type = "MuseSamplerSoundPack"; + meta.type = AUDIO_RESOURCE_TYPE_NAME; meta.vendor = "MuseSounds"; meta.attributes = { { u"playbackSetupData", instrumentSoundId }, diff --git a/framework/musesampler/musesamplertypes.h b/framework/musesampler/musesamplertypes.h index 4e39803dad..a0a7587c30 100644 --- a/framework/musesampler/musesamplertypes.h +++ b/framework/musesampler/musesamplertypes.h @@ -23,9 +23,13 @@ #ifndef MUSE_MUSESAMPLER_MUSESAMPLERTYPES_H #define MUSE_MUSESAMPLER_MUSESAMPLERTYPES_H +#include + #include "types/string.h" namespace muse::musesampler { +inline constexpr std::string_view AUDIO_RESOURCE_TYPE_NAME = "MuseSamplerSoundPack"; + enum class ClefType { None, Treble, diff --git a/framework/vst/CMakeLists.txt b/framework/vst/CMakeLists.txt index 5d2026e2f0..6c10896ffb 100644 --- a/framework/vst/CMakeLists.txt +++ b/framework/vst/CMakeLists.txt @@ -28,6 +28,7 @@ target_sources(muse_vst PRIVATE ivstinstancesregister.h ivstplugininstance.h vsterrors.h + vstpluginattrs.h vsttypes.h vstmodule.cpp vstmodule.h diff --git a/framework/vst/internal/vstmodulesrepository.cpp b/framework/vst/internal/vstmodulesrepository.cpp index 11246c67cd..ce324006da 100644 --- a/framework/vst/internal/vstmodulesrepository.cpp +++ b/framework/vst/internal/vstmodulesrepository.cpp @@ -123,11 +123,12 @@ void VstModulesRepository::refresh() muse::audio::AudioResourceMetaList VstModulesRepository::modulesMetaList(PluginType type) const { auto infoAccepted = [type](const audioplugins::AudioPluginInfo& info) { - if (info.meta.type != "VstPlugin" || info.state != audioplugins::AudioPluginState::Validated) { + if (!muse::audio::isResourceType(info.meta, muse::audio::AudioResourceType::VstPlugin) + || info.state != audioplugins::AudioPluginState::Validated) { return false; } - const String& categories = info.meta.attributeVal(muse::audio::CATEGORIES_ATTRIBUTE); - const bool isInstrument = categories.contains(u"Instrument"); + const String& categories = info.meta.attributeVal(muse::vst::CATEGORIES_ATTRIBUTE); + const bool isInstrument = categories.contains(muse::vst::INSTRUMENT_CATEGORY); return type == PluginType::Instrument ? isInstrument : !isInstrument; }; diff --git a/framework/vst/internal/vstpluginmetareader.cpp b/framework/vst/internal/vstpluginmetareader.cpp index 02b8c64353..470019c832 100644 --- a/framework/vst/internal/vstpluginmetareader.cpp +++ b/framework/vst/internal/vstpluginmetareader.cpp @@ -22,18 +22,21 @@ #include "vstpluginmetareader.h" +#include "audio/common/audiotypes.h" + #include "vsttypes.h" +#include "vstpluginattrs.h" #include "vsterrors.h" #include "log.h" using namespace muse; -using namespace muse::audio; +using namespace muse::audioplugins; using namespace muse::vst; -audio::AudioResourceType VstPluginMetaReader::metaType() const +audioplugins::AudioResourceType VstPluginMetaReader::metaType() const { - return "VstPlugin"; + return std::string(AUDIO_RESOURCE_TYPE_NAME); } bool VstPluginMetaReader::canReadMeta(const io::path_t& pluginPath) const @@ -56,10 +59,10 @@ RetVal VstPluginMetaReader::readMeta(const io::path_t& pl continue; } - muse::audio::AudioResourceMeta meta; + AudioResourceMeta meta; meta.id = io::completeBasename(pluginPath).toStdString(); - meta.type = "VstPlugin"; - meta.attributes.emplace(muse::audio::CATEGORIES_ATTRIBUTE, String::fromStdString(classInfo.subCategoriesString())); + meta.type = AUDIO_RESOURCE_TYPE_NAME; + meta.attributes.emplace(CATEGORIES_ATTRIBUTE, String::fromStdString(classInfo.subCategoriesString())); meta.attributes.emplace(muse::audio::HAS_NATIVE_EDITOR_SUPPORT_ATTRIBUTE, u"true"); meta.vendor = classInfo.vendor(); diff --git a/framework/vst/internal/vstpluginmetareader.h b/framework/vst/internal/vstpluginmetareader.h index 33ef4b93dd..417f3dff8e 100644 --- a/framework/vst/internal/vstpluginmetareader.h +++ b/framework/vst/internal/vstpluginmetareader.h @@ -29,9 +29,9 @@ namespace muse::vst { class VstPluginMetaReader : public audioplugins::IAudioPluginMetaReader { public: - audio::AudioResourceType metaType() const override; + audioplugins::AudioResourceType metaType() const override; bool canReadMeta(const io::path_t& pluginPath) const override; - RetVal readMeta(const io::path_t& pluginPath) const override; + RetVal readMeta(const io::path_t& pluginPath) const override; }; } diff --git a/framework/vst/vstpluginattrs.h b/framework/vst/vstpluginattrs.h new file mode 100644 index 0000000000..1837c9c7c1 --- /dev/null +++ b/framework/vst/vstpluginattrs.h @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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, see . + */ +#ifndef MUSE_VST_VSTPLUGINATTRS_H +#define MUSE_VST_VSTPLUGINATTRS_H + +#include + +#include "global/types/string.h" + +namespace muse::vst { +inline constexpr std::string_view AUDIO_RESOURCE_TYPE_NAME = "VstPlugin"; + +inline const String CATEGORIES_ATTRIBUTE(u"categories"); + +// The category value that marks a VST as an instrument rather than an FX. +// Wire-level contract between the plugin meta reader (producer) and +// modulesMetaList (consumer) — keep both referencing this symbol. +inline const String INSTRUMENT_CATEGORY(u"Instrument"); +} + +#endif // MUSE_VST_VSTPLUGINATTRS_H diff --git a/framework/vst/vsttypes.h b/framework/vst/vsttypes.h index e0627bb004..e96e7d7403 100644 --- a/framework/vst/vsttypes.h +++ b/framework/vst/vsttypes.h @@ -43,6 +43,8 @@ #include "io/path.h" #include "log.h" +#include "vstpluginattrs.h" + namespace muse::vst { class IVstPluginInstance; using IVstPluginInstancePtr = std::shared_ptr; From 375f349ef21376ca706ee3cbaf877b467c2b8452 Mon Sep 17 00:00:00 2001 From: Paul MARTIN Date: Thu, 21 May 2026 19:29:32 +0200 Subject: [PATCH 3/9] [audioplugins] make AudioResourceMeta::operator< a strict weak ordering The old `id < a.id || vendor < a.vendor` was not antisymmetric (could report a #include #include +#include #include #include "global/io/path.h" @@ -80,8 +81,8 @@ struct AudioResourceMeta { bool operator<(const AudioResourceMeta& other) const { - return id < other.id - || vendor < other.vendor; + return std::tie(id, vendor, type, attributes) + < std::tie(other.id, other.vendor, other.type, other.attributes); } }; From 7d85e6fcda182f26415a7468b91975ee732d2a49 Mon Sep 17 00:00:00 2001 From: Paul MARTIN Date: Tue, 26 May 2026 14:17:45 +0200 Subject: [PATCH 4/9] [audioplugins] rename AudioResourceId -> PluginResourceId Per reviewer feedback on PR #47: the audioplugins module owns plugin identity, audio uses an id to address resources in the pipeline. Different concepts, same name today - give them different names. This commit only renames. audio/common/audiotypes.h still re-exports under audio::AudioResourceId, now pointing at the renamed audioplugins::PluginResourceId, so everything compiles transparently. The next commit makes audio::AudioResourceId an independent alias and adds the conversion helper. --- framework/audio/common/audiotypes.h | 4 ++-- framework/audioplugins/audiopluginstypes.h | 6 ++--- .../audioplugins/iknownaudiopluginsregister.h | 8 +++---- .../internal/knownaudiopluginsregister.cpp | 12 +++++----- .../internal/knownaudiopluginsregister.h | 10 ++++---- .../internal/registeraudiopluginsscenario.cpp | 4 ++-- .../internal/registeraudiopluginsscenario.h | 2 +- .../iregisteraudiopluginsscenario.h | 6 ++--- .../tests/knownaudiopluginsregistertest.cpp | 4 ++-- .../mocks/knownaudiopluginsregistermock.h | 8 +++---- .../registeraudiopluginsscenariotest.cpp | 24 +++++++++---------- 11 files changed, 44 insertions(+), 44 deletions(-) diff --git a/framework/audio/common/audiotypes.h b/framework/audio/common/audiotypes.h index fd347803a4..5b6af61a3b 100644 --- a/framework/audio/common/audiotypes.h +++ b/framework/audio/common/audiotypes.h @@ -172,8 +172,8 @@ struct AudioEngineConfig { using AudioSourceName = std::string; using AudioUnitConfig = std::map; -using AudioResourceId = muse::audioplugins::AudioResourceId; -using AudioResourceIdList = muse::audioplugins::AudioResourceIdList; +using AudioResourceId = muse::audioplugins::PluginResourceId; +using AudioResourceIdList = muse::audioplugins::PluginResourceIdList; using AudioResourceVendor = muse::audioplugins::AudioResourceVendor; using AudioResourceAttributes = muse::audioplugins::AudioResourceAttributes; using AudioResourceMeta = muse::audioplugins::AudioResourceMeta; diff --git a/framework/audioplugins/audiopluginstypes.h b/framework/audioplugins/audiopluginstypes.h index a1ed21f57a..9a94bd1898 100644 --- a/framework/audioplugins/audiopluginstypes.h +++ b/framework/audioplugins/audiopluginstypes.h @@ -31,8 +31,8 @@ #include "global/types/string.h" namespace muse::audioplugins { -using AudioResourceId = std::string; -using AudioResourceIdList = std::vector; +using PluginResourceId = std::string; +using PluginResourceIdList = std::vector; using AudioResourceVendor = std::string; using AudioResourceAttributes = std::map; @@ -43,7 +43,7 @@ using AudioResourceAttributes = std::map; using AudioResourceType = std::string; struct AudioResourceMeta { - AudioResourceId id; + PluginResourceId id; AudioResourceVendor vendor; AudioResourceAttributes attributes; AudioResourceType type; diff --git a/framework/audioplugins/iknownaudiopluginsregister.h b/framework/audioplugins/iknownaudiopluginsregister.h index 3f110ee922..83d50cbcf3 100644 --- a/framework/audioplugins/iknownaudiopluginsregister.h +++ b/framework/audioplugins/iknownaudiopluginsregister.h @@ -44,15 +44,15 @@ class IKnownAudioPluginsRegister : MODULE_GLOBAL_INTERFACE virtual AudioPluginInfoList pluginInfoList(PluginInfoAccepted accepted = PluginInfoAccepted()) const = 0; virtual muse::async::Notification pluginInfoListChanged() const = 0; - virtual const io::path_t& pluginPath(const AudioResourceId& resourceId) const = 0; + virtual const io::path_t& pluginPath(const PluginResourceId& resourceId) const = 0; virtual bool exists(const io::path_t& pluginPath) const = 0; - virtual bool exists(const AudioResourceId& resourceId) const = 0; + virtual bool exists(const PluginResourceId& resourceId) const = 0; virtual Ret registerPlugins(const AudioPluginInfoList& list) = 0; - virtual Ret unregisterPlugins(const AudioResourceIdList& resourceIds) = 0; + virtual Ret unregisterPlugins(const PluginResourceIdList& resourceIds) = 0; - virtual Ret setPluginsState(const AudioResourceIdList& resourceIds, AudioPluginState state) = 0; + virtual Ret setPluginsState(const PluginResourceIdList& resourceIds, AudioPluginState state) = 0; // Erase every entry whose `path` matches. Used to clear a Discovered // placeholder before its (re)validation result is written, so a diff --git a/framework/audioplugins/internal/knownaudiopluginsregister.cpp b/framework/audioplugins/internal/knownaudiopluginsregister.cpp index 9919fc855f..bda0972e2f 100644 --- a/framework/audioplugins/internal/knownaudiopluginsregister.cpp +++ b/framework/audioplugins/internal/knownaudiopluginsregister.cpp @@ -208,7 +208,7 @@ muse::async::Notification KnownAudioPluginsRegister::pluginInfoListChanged() con return m_pluginInfoListChanged; } -const io::path_t& KnownAudioPluginsRegister::pluginPath(const AudioResourceId& resourceId) const +const io::path_t& KnownAudioPluginsRegister::pluginPath(const PluginResourceId& resourceId) const { auto it = m_pluginInfoMap.find(resourceId); if (it == m_pluginInfoMap.end()) { @@ -224,7 +224,7 @@ bool KnownAudioPluginsRegister::exists(const io::path_t& pluginPath) const return muse::contains(m_pluginPaths, pluginPath); } -bool KnownAudioPluginsRegister::exists(const AudioResourceId& resourceId) const +bool KnownAudioPluginsRegister::exists(const PluginResourceId& resourceId) const { return muse::contains(m_pluginInfoMap, resourceId); } @@ -262,7 +262,7 @@ Ret KnownAudioPluginsRegister::registerPlugins(const AudioPluginInfoList& list) return make_ok(); } -Ret KnownAudioPluginsRegister::setPluginsState(const AudioResourceIdList& resourceIds, AudioPluginState state) +Ret KnownAudioPluginsRegister::setPluginsState(const PluginResourceIdList& resourceIds, AudioPluginState state) { IF_ASSERT_FAILED(m_loaded) { return false; @@ -273,7 +273,7 @@ Ret KnownAudioPluginsRegister::setPluginsState(const AudioResourceIdList& resour } bool changed = false; - for (const AudioResourceId& resourceId : resourceIds) { + for (const PluginResourceId& resourceId : resourceIds) { auto range = m_pluginInfoMap.equal_range(resourceId); for (auto it = range.first; it != range.second; ++it) { if (it->second.state != state) { @@ -290,7 +290,7 @@ Ret KnownAudioPluginsRegister::setPluginsState(const AudioResourceIdList& resour return writePluginsInfo(); } -Ret KnownAudioPluginsRegister::unregisterPlugins(const AudioResourceIdList& resourceIds) +Ret KnownAudioPluginsRegister::unregisterPlugins(const PluginResourceIdList& resourceIds) { IF_ASSERT_FAILED(m_loaded) { return false; @@ -302,7 +302,7 @@ Ret KnownAudioPluginsRegister::unregisterPlugins(const AudioResourceIdList& reso bool changed = false; - for (const AudioResourceId& resourceId : resourceIds) { + for (const PluginResourceId& resourceId : resourceIds) { if (!exists(resourceId)) { continue; } diff --git a/framework/audioplugins/internal/knownaudiopluginsregister.h b/framework/audioplugins/internal/knownaudiopluginsregister.h index 460385d3de..31bf2a933e 100644 --- a/framework/audioplugins/internal/knownaudiopluginsregister.h +++ b/framework/audioplugins/internal/knownaudiopluginsregister.h @@ -46,15 +46,15 @@ class KnownAudioPluginsRegister : public IKnownAudioPluginsRegister AudioPluginInfoList pluginInfoList(PluginInfoAccepted accepted = PluginInfoAccepted()) const override; muse::async::Notification pluginInfoListChanged() const override; - const io::path_t& pluginPath(const AudioResourceId& resourceId) const override; + const io::path_t& pluginPath(const PluginResourceId& resourceId) const override; bool exists(const io::path_t& pluginPath) const override; - bool exists(const AudioResourceId& resourceId) const override; + bool exists(const PluginResourceId& resourceId) const override; Ret registerPlugins(const AudioPluginInfoList& list) override; - Ret unregisterPlugins(const AudioResourceIdList& resourceIds) override; + Ret unregisterPlugins(const PluginResourceIdList& resourceIds) override; - Ret setPluginsState(const AudioResourceIdList& resourceIds, AudioPluginState state) override; + Ret setPluginsState(const PluginResourceIdList& resourceIds, AudioPluginState state) override; Ret removePluginsAtPath(const io::path_t& path) override; @@ -62,7 +62,7 @@ class KnownAudioPluginsRegister : public IKnownAudioPluginsRegister Ret writePluginsInfo(); async::Notification m_pluginInfoListChanged; bool m_loaded = false; - std::multimap m_pluginInfoMap; + std::multimap m_pluginInfoMap; std::set m_pluginPaths; }; } diff --git a/framework/audioplugins/internal/registeraudiopluginsscenario.cpp b/framework/audioplugins/internal/registeraudiopluginsscenario.cpp index 0028c43212..727aa66d92 100644 --- a/framework/audioplugins/internal/registeraudiopluginsscenario.cpp +++ b/framework/audioplugins/internal/registeraudiopluginsscenario.cpp @@ -57,7 +57,7 @@ PluginScanResult RegisterAudioPluginsScenario::scanPlugins(Progress* progress) c PluginScanResult result; struct CacheEntry { - AudioResourceId id; + PluginResourceId id; AudioPluginState state; }; // A single binary path can host several plugin IDs (shell / multi-effect @@ -184,7 +184,7 @@ Ret RegisterAudioPluginsScenario::persistDiscoveredPlaceholders(const io::paths_ return knownPluginsRegister()->registerPlugins(placeholders); } -Ret RegisterAudioPluginsScenario::unregisterRemovedPlugins(const AudioResourceIdList& pluginIds) +Ret RegisterAudioPluginsScenario::unregisterRemovedPlugins(const PluginResourceIdList& pluginIds) { TRACEFUNC; diff --git a/framework/audioplugins/internal/registeraudiopluginsscenario.h b/framework/audioplugins/internal/registeraudiopluginsscenario.h index cc6ea82807..7e377e42ee 100644 --- a/framework/audioplugins/internal/registeraudiopluginsscenario.h +++ b/framework/audioplugins/internal/registeraudiopluginsscenario.h @@ -54,7 +54,7 @@ class RegisterAudioPluginsScenario : public IRegisterAudioPluginsScenario, publi Ret updatePluginsRegistry() override; Ret registerNewPlugins(const io::paths_t& pluginPaths, bool validate) override; - Ret unregisterRemovedPlugins(const AudioResourceIdList& pluginIds) override; + Ret unregisterRemovedPlugins(const PluginResourceIdList& pluginIds) override; Ret registerPlugin(const io::path_t& pluginPath) override; Ret registerFailedPlugin(const io::path_t& pluginPath, int failCode) override; diff --git a/framework/audioplugins/iregisteraudiopluginsscenario.h b/framework/audioplugins/iregisteraudiopluginsscenario.h index 2e43727f30..d7dc4c74ce 100644 --- a/framework/audioplugins/iregisteraudiopluginsscenario.h +++ b/framework/audioplugins/iregisteraudiopluginsscenario.h @@ -32,8 +32,8 @@ namespace muse::audioplugins { struct PluginScanResult { io::paths_t newPluginPaths; // not in cache; will be inserted via subprocess validation - AudioResourceIdList missingPluginIds; // in cache but not currently found by any scanner - AudioResourceIdList rediscoveredPluginIds; // previously Missing entries the scanner found again + PluginResourceIdList missingPluginIds; // in cache but not currently found by any scanner + PluginResourceIdList rediscoveredPluginIds; // previously Missing entries the scanner found again }; class IRegisterAudioPluginsScenario : MODULE_CONTEXT_INTERFACE @@ -51,7 +51,7 @@ class IRegisterAudioPluginsScenario : MODULE_CONTEXT_INTERFACE // for validation on the next scan. Default `true` runs the full scan. // Returns the first cache write/load failure encountered, or ok. virtual Ret registerNewPlugins(const io::paths_t& pluginPaths, bool validate = true) = 0; - virtual Ret unregisterRemovedPlugins(const AudioResourceIdList& pluginIds) = 0; + virtual Ret unregisterRemovedPlugins(const PluginResourceIdList& pluginIds) = 0; virtual Ret registerPlugin(const io::path_t& pluginPath) = 0; virtual Ret registerFailedPlugin(const io::path_t& pluginPath, int failCode) = 0; diff --git a/framework/audioplugins/tests/knownaudiopluginsregistertest.cpp b/framework/audioplugins/tests/knownaudiopluginsregistertest.cpp index 6b584cd076..4ea52a66a0 100644 --- a/framework/audioplugins/tests/knownaudiopluginsregistertest.cpp +++ b/framework/audioplugins/tests/knownaudiopluginsregistertest.cpp @@ -210,7 +210,7 @@ TEST_F(AudioPlugins_KnownAudioPluginsRegisterTest, PluginInfoList) // [THEN] Make sure that exists() does not always return true EXPECT_FALSE(m_knownPlugins->exists(path_t("/path/to/nonexistent/plugin.vst3"))); - EXPECT_FALSE(m_knownPlugins->exists(AudioResourceId("nonexistent_plugin"))); + EXPECT_FALSE(m_knownPlugins->exists(PluginResourceId("nonexistent_plugin"))); // [GIVEN] New plugin for registration AudioPluginInfo newPluginInfo; @@ -391,7 +391,7 @@ TEST_F(AudioPlugins_KnownAudioPluginsRegisterTest, Load_LegacyArrayFormat) // [THEN] Plugins parsed successfully EXPECT_TRUE(ret); - EXPECT_TRUE(m_knownPlugins->exists(AudioResourceId("AAA"))); + EXPECT_TRUE(m_knownPlugins->exists(PluginResourceId("AAA"))); } TEST_F(AudioPlugins_KnownAudioPluginsRegisterTest, Load_MalformedRow_RejectsFile) diff --git a/framework/audioplugins/tests/mocks/knownaudiopluginsregistermock.h b/framework/audioplugins/tests/mocks/knownaudiopluginsregistermock.h index 452defa072..2111ed1a4a 100644 --- a/framework/audioplugins/tests/mocks/knownaudiopluginsregistermock.h +++ b/framework/audioplugins/tests/mocks/knownaudiopluginsregistermock.h @@ -34,15 +34,15 @@ class KnownAudioPluginsRegisterMock : public IKnownAudioPluginsRegister MOCK_METHOD(AudioPluginInfoList, pluginInfoList, (PluginInfoAccepted), (const, override)); MOCK_METHOD(async::Notification, pluginInfoListChanged, (), (const, override)); - MOCK_METHOD(const io::path_t&, pluginPath, (const AudioResourceId&), (const, override)); + MOCK_METHOD(const io::path_t&, pluginPath, (const PluginResourceId&), (const, override)); MOCK_METHOD(bool, exists, (const io::path_t&), (const, override)); - MOCK_METHOD(bool, exists, (const AudioResourceId&), (const, override)); + MOCK_METHOD(bool, exists, (const PluginResourceId&), (const, override)); MOCK_METHOD(Ret, registerPlugins, (const AudioPluginInfoList&), (override)); - MOCK_METHOD(Ret, unregisterPlugins, (const AudioResourceIdList&), (override)); + MOCK_METHOD(Ret, unregisterPlugins, (const PluginResourceIdList&), (override)); - MOCK_METHOD(Ret, setPluginsState, (const AudioResourceIdList&, AudioPluginState), (override)); + MOCK_METHOD(Ret, setPluginsState, (const PluginResourceIdList&, AudioPluginState), (override)); MOCK_METHOD(Ret, removePluginsAtPath, (const io::path_t&), (override)); }; diff --git a/framework/audioplugins/tests/registeraudiopluginsscenariotest.cpp b/framework/audioplugins/tests/registeraudiopluginsscenariotest.cpp index 7188028d7a..05854a595f 100644 --- a/framework/audioplugins/tests/registeraudiopluginsscenariotest.cpp +++ b/framework/audioplugins/tests/registeraudiopluginsscenariotest.cpp @@ -290,14 +290,14 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_Mark } // [THEN] Uninstalled plugins transition to Missing (kept in cache) - AudioResourceIdList uninstalledPluginIdList { + PluginResourceIdList uninstalledPluginIdList { knownPlugins[0].meta.id, knownPlugins[1].meta.id }; EXPECT_CALL(*m_knownPlugins, setPluginsState(uninstalledPluginIdList, AudioPluginState::Missing)) .WillOnce(Return(make_ok())); - EXPECT_CALL(*m_knownPlugins, setPluginsState(AudioResourceIdList {}, AudioPluginState::Validated)) + EXPECT_CALL(*m_knownPlugins, setPluginsState(PluginResourceIdList {}, AudioPluginState::Validated)) .WillOnce(Return(make_ok())); EXPECT_CALL(*m_knownPlugins, unregisterPlugins(_)) @@ -347,9 +347,9 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_Redi } // [THEN] Only AAA gets transitioned back to Validated - AudioResourceIdList rediscoveredIds { knownPlugins[0].meta.id }; + PluginResourceIdList rediscoveredIds { knownPlugins[0].meta.id }; - EXPECT_CALL(*m_knownPlugins, setPluginsState(AudioResourceIdList {}, AudioPluginState::Missing)) + EXPECT_CALL(*m_knownPlugins, setPluginsState(PluginResourceIdList {}, AudioPluginState::Missing)) .WillOnce(Return(make_ok())); EXPECT_CALL(*m_knownPlugins, setPluginsState(rediscoveredIds, AudioPluginState::Validated)) @@ -364,7 +364,7 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_Redi TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_MultiPluginBinaryMarksEveryIdMissing) { - auto createPluginInfo = [](const io::path_t& path, const AudioResourceId& id, AudioPluginState state) { + auto createPluginInfo = [](const io::path_t& path, const PluginResourceId& id, AudioPluginState state) { AudioPluginInfo info; info.meta.id = id; info.meta.type = "VstPlugin"; @@ -399,11 +399,11 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_Mult // [THEN] BOTH ids hosted by the missing binary transition to Missing — not // just the last one (regression guard for per-path multi-id tracking). - AudioResourceIdList expectedMissing { "Shell FxA", "Shell FxB" }; + PluginResourceIdList expectedMissing { "Shell FxA", "Shell FxB" }; EXPECT_CALL(*m_knownPlugins, setPluginsState(expectedMissing, AudioPluginState::Missing)) .WillOnce(Return(make_ok())); - EXPECT_CALL(*m_knownPlugins, setPluginsState(AudioResourceIdList {}, AudioPluginState::Validated)) + EXPECT_CALL(*m_knownPlugins, setPluginsState(PluginResourceIdList {}, AudioPluginState::Validated)) .WillOnce(Return(make_ok())); EXPECT_CALL(*m_knownPlugins, load()) @@ -415,7 +415,7 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_Mult TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_MultiPluginBinaryRediscoversEveryId) { - auto createPluginInfo = [](const io::path_t& path, const AudioResourceId& id, AudioPluginState state) { + auto createPluginInfo = [](const io::path_t& path, const PluginResourceId& id, AudioPluginState state) { AudioPluginInfo info; info.meta.id = id; info.meta.type = "VstPlugin"; @@ -446,9 +446,9 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_Mult } // [THEN] BOTH ids transition back to Validated — not just the last one. - AudioResourceIdList expectedRediscovered { "Shell FxA", "Shell FxB" }; + PluginResourceIdList expectedRediscovered { "Shell FxA", "Shell FxB" }; - EXPECT_CALL(*m_knownPlugins, setPluginsState(AudioResourceIdList {}, AudioPluginState::Missing)) + EXPECT_CALL(*m_knownPlugins, setPluginsState(PluginResourceIdList {}, AudioPluginState::Missing)) .WillOnce(Return(make_ok())); EXPECT_CALL(*m_knownPlugins, setPluginsState(expectedRediscovered, AudioPluginState::Validated)) .WillOnce(Return(make_ok())); @@ -493,9 +493,9 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_Left // placeholder and re-validated via subprocess. It is NOT marked Missing // (it's still on disk) and it is NOT considered "rediscovered" (that's // for paths transitioning out of Missing). - EXPECT_CALL(*m_knownPlugins, setPluginsState(AudioResourceIdList {}, AudioPluginState::Missing)) + EXPECT_CALL(*m_knownPlugins, setPluginsState(PluginResourceIdList {}, AudioPluginState::Missing)) .WillOnce(Return(make_ok())); - EXPECT_CALL(*m_knownPlugins, setPluginsState(AudioResourceIdList {}, AudioPluginState::Validated)) + EXPECT_CALL(*m_knownPlugins, setPluginsState(PluginResourceIdList {}, AudioPluginState::Validated)) .WillOnce(Return(make_ok())); // [THEN] registerNewPlugins writes a Discovered placeholder for the path From 39c4bafc799530a85e61760672448d761419f112 Mon Sep 17 00:00:00 2001 From: Paul MARTIN Date: Tue, 26 May 2026 17:50:08 +0200 Subject: [PATCH 5/9] [audio] split audio::AudioResourceId off as independent alias with conversion helper audio::AudioResourceId is now an independent std::string alias (was a re-export of muse::audioplugins::PluginResourceId). Add toAudioResourceId / toAudioResourceIdList helpers in audio/common/ audiotypes.h - documenting the seam between plugin-identity and audio-pipeline-resource roles. One-directional today: both aliases bottom out at std::string so the conversion is one-to-one; the helper becomes a real bridge if either side later gains a strong wrapper. A static_assert in audioresourcetypes_tests.cpp pins the underlying- std::string contract; a new ToAudioResourceIdRoundTrips test exercises the helper end-to-end. VST and audio-engine resolvers stay on audio::AudioResourceId unchanged - structurally pinned by abstractfxresolver and keeps the Audacity surface unaffected. --- framework/audio/common/audiotypes.h | 19 ++++++++++++-- .../audio/tests/audioresourcetypes_tests.cpp | 26 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/framework/audio/common/audiotypes.h b/framework/audio/common/audiotypes.h index 5b6af61a3b..22abb24b78 100644 --- a/framework/audio/common/audiotypes.h +++ b/framework/audio/common/audiotypes.h @@ -172,8 +172,23 @@ struct AudioEngineConfig { using AudioSourceName = std::string; using AudioUnitConfig = std::map; -using AudioResourceId = muse::audioplugins::PluginResourceId; -using AudioResourceIdList = muse::audioplugins::PluginResourceIdList; +// AudioResourceId addresses a resource within the audio pipeline; +// audioplugins::PluginResourceId is a plugin's identity in the cache. +// Both alias std::string today and convert one-to-one - the seam is +// documentary so the two roles do not drift. +using AudioResourceId = std::string; +using AudioResourceIdList = std::vector; + +inline AudioResourceId toAudioResourceId(const muse::audioplugins::PluginResourceId& id) +{ + return id; +} + +inline AudioResourceIdList toAudioResourceIdList(const muse::audioplugins::PluginResourceIdList& ids) +{ + return AudioResourceIdList(ids.begin(), ids.end()); +} + using AudioResourceVendor = muse::audioplugins::AudioResourceVendor; using AudioResourceAttributes = muse::audioplugins::AudioResourceAttributes; using AudioResourceMeta = muse::audioplugins::AudioResourceMeta; diff --git a/framework/audio/tests/audioresourcetypes_tests.cpp b/framework/audio/tests/audioresourcetypes_tests.cpp index 147f94ab1d..ada918d600 100644 --- a/framework/audio/tests/audioresourcetypes_tests.cpp +++ b/framework/audio/tests/audioresourcetypes_tests.cpp @@ -31,6 +31,12 @@ using namespace muse; +// Document the underlying types behind the named seam between the audio and +// audioplugins resource ids. If either becomes a strong wrapper later, this +// assert fires and the migration is deliberate. +static_assert(std::is_same_v); +static_assert(std::is_same_v); + // The on-disk plugin cache stores AudioResourceMeta::type as the canonical // wire string. These strings must stay stable across releases — caches written // by older builds must remain readable. Each plugin module owns its own copy @@ -89,3 +95,23 @@ TEST(Audio_AudioResourceTypes, IsResourceTypeHelper) EXPECT_FALSE(audio::isResourceType(meta, audio::AudioResourceType::VstPlugin)); EXPECT_TRUE(audio::isResourceType(meta, audio::AudioResourceType::Undefined)); } + +// audio::toAudioResourceId is the documentary seam between the audioplugins +// module's PluginResourceId and the audio module's AudioResourceId. Today +// the conversion is one-to-one because both alias std::string; if either +// side later gains a real wrapper, this round-trip becomes a real one and +// this test is where it gets verified. +TEST(Audio_AudioResourceTypes, ToAudioResourceIdRoundTrips) +{ + const audioplugins::PluginResourceId plugId = "Muse Reverb"; + const audio::AudioResourceId audioId = audio::toAudioResourceId(plugId); + EXPECT_EQ(audioId, plugId); + EXPECT_EQ(audioId, "Muse Reverb"); + + const audioplugins::PluginResourceIdList plugIds = { "A", "B", "C" }; + const audio::AudioResourceIdList audioIds = audio::toAudioResourceIdList(plugIds); + ASSERT_EQ(audioIds.size(), plugIds.size()); + for (size_t i = 0; i < plugIds.size(); ++i) { + EXPECT_EQ(audioIds[i], plugIds[i]); + } +} From c712096eb5991f5b2dc51ffafd2f9f9070780246 Mon Sep 17 00:00:00 2001 From: Paul MARTIN Date: Fri, 29 May 2026 12:09:35 +0200 Subject: [PATCH 6/9] [audioplugins] rename AudioResourceMeta -> PluginMeta (Vendor/Attributes/MetaList/MetaSet/Type) Continuation of the prior PluginResourceId rename. The audioplugins module owns plugin-domain types; give them plugin names so the namespace tells you the role. Pure rename, no behavior change. audio/common/audiotypes.h still re-exports them under the audio::AudioResource* names so audio-engine code is unaffected; the next commit splits audios meta types off as audio-owned and drops the cross-module include for good. VST and audio-side qualified references (vstpluginmetareader, audio::sourceTypeFromResourceType, audio::audioResourceTypeToString, plus a fixture in audioresourcetypes_tests) updated to the new spelling in this same commit so the framework stays internally consistent at this point. --- framework/audio/common/audiotypes.h | 14 ++++----- framework/audio/common/audioutils.h | 2 +- .../audio/tests/audioresourcetypes_tests.cpp | 2 +- framework/audioplugins/audiopluginstypes.h | 30 +++++++++---------- .../audioplugins/iaudiopluginmetareader.h | 4 +-- .../audioplugins/iaudiopluginsconfiguration.h | 4 +-- .../internal/audiopluginsconfiguration.cpp | 4 +-- .../internal/audiopluginsconfiguration.h | 6 ++-- .../internal/knownaudiopluginsregister.cpp | 16 +++++----- .../internal/registeraudiopluginsscenario.cpp | 8 ++--- .../internal/registeraudiopluginsscenario.h | 2 +- .../tests/knownaudiopluginsregistertest.cpp | 6 ++-- .../tests/mocks/audiopluginmetareadermock.h | 4 +-- .../mocks/audiopluginsconfigurationmock.h | 4 +-- .../registeraudiopluginsscenariotest.cpp | 10 +++---- .../vst/internal/vstpluginmetareader.cpp | 2 +- framework/vst/internal/vstpluginmetareader.h | 4 +-- 17 files changed, 61 insertions(+), 61 deletions(-) diff --git a/framework/audio/common/audiotypes.h b/framework/audio/common/audiotypes.h index 22abb24b78..76ea12456c 100644 --- a/framework/audio/common/audiotypes.h +++ b/framework/audio/common/audiotypes.h @@ -189,11 +189,11 @@ inline AudioResourceIdList toAudioResourceIdList(const muse::audioplugins::Plugi return AudioResourceIdList(ids.begin(), ids.end()); } -using AudioResourceVendor = muse::audioplugins::AudioResourceVendor; -using AudioResourceAttributes = muse::audioplugins::AudioResourceAttributes; -using AudioResourceMeta = muse::audioplugins::AudioResourceMeta; -using AudioResourceMetaList = muse::audioplugins::AudioResourceMetaList; -using AudioResourceMetaSet = muse::audioplugins::AudioResourceMetaSet; +using AudioResourceVendor = muse::audioplugins::PluginVendor; +using AudioResourceAttributes = muse::audioplugins::PluginAttributes; +using AudioResourceMeta = muse::audioplugins::PluginMeta; +using AudioResourceMetaList = muse::audioplugins::PluginMetaList; +using AudioResourceMetaSet = muse::audioplugins::PluginMetaSet; // Wire-format identifiers for the formats the muse audio engine dispatches // directly. The strings here are the canonical names persisted in the JSON @@ -209,7 +209,7 @@ inline constexpr std::string_view NATIVE_EFFECT_TYPE_NAME = "NativeEffect"; // audio::AudioResourceType is an audio-engine-internal enum used to dispatch // synth/fx routing. It is intentionally kept separate from the framework's -// opaque audioplugins::AudioResourceType (a std::string identifier persisted +// opaque audioplugins::PluginType (a std::string identifier persisted // by the audioplugins module). Map between them at the boundary via // resourceTypeFromString() / resourceTypeName(). The enum lists only the // formats the audio engine actually routes; apps with broader plugin support @@ -403,7 +403,7 @@ enum class AudioSourceType { MuseSampler }; -inline AudioSourceType sourceTypeFromResourceType(const muse::audioplugins::AudioResourceType& metaType) +inline AudioSourceType sourceTypeFromResourceType(const muse::audioplugins::PluginType& metaType) { switch (resourceTypeFromString(metaType)) { case AudioResourceType::FluidSoundfont: return AudioSourceType::Fluid; diff --git a/framework/audio/common/audioutils.h b/framework/audio/common/audioutils.h index ecafab1511..303f2e7a20 100644 --- a/framework/audio/common/audioutils.h +++ b/framework/audio/common/audioutils.h @@ -37,7 +37,7 @@ inline AudioResourceMeta makeReverbMeta() return meta; } -inline String audioResourceTypeToString(const muse::audioplugins::AudioResourceType& metaType) +inline String audioResourceTypeToString(const muse::audioplugins::PluginType& metaType) { AudioResourceType type = resourceTypeFromString(metaType); auto search = RESOURCE_TYPE_MAP.find(type); diff --git a/framework/audio/tests/audioresourcetypes_tests.cpp b/framework/audio/tests/audioresourcetypes_tests.cpp index ada918d600..9401354e0f 100644 --- a/framework/audio/tests/audioresourcetypes_tests.cpp +++ b/framework/audio/tests/audioresourcetypes_tests.cpp @@ -86,7 +86,7 @@ TEST(Audio_AudioResourceTypes, ResourceTypeFromStringRejectsUnknown) TEST(Audio_AudioResourceTypes, IsResourceTypeHelper) { - audioplugins::AudioResourceMeta meta; + audioplugins::PluginMeta meta; meta.type = vst::AUDIO_RESOURCE_TYPE_NAME; EXPECT_TRUE(audio::isResourceType(meta, audio::AudioResourceType::VstPlugin)); EXPECT_FALSE(audio::isResourceType(meta, audio::AudioResourceType::FluidSoundfont)); diff --git a/framework/audioplugins/audiopluginstypes.h b/framework/audioplugins/audiopluginstypes.h index 9a94bd1898..dbda1725cd 100644 --- a/framework/audioplugins/audiopluginstypes.h +++ b/framework/audioplugins/audiopluginstypes.h @@ -33,20 +33,20 @@ namespace muse::audioplugins { using PluginResourceId = std::string; using PluginResourceIdList = std::vector; -using AudioResourceVendor = std::string; -using AudioResourceAttributes = std::map; +using PluginVendor = std::string; +using PluginAttributes = std::map; // Opaque app-defined plugin format identifier (e.g. "VstPlugin", "Lv2Plugin", // or any string the embedding app cares about). The framework persists it as-is // and never inspects the value. Apps map between this string and their own // engine-side type concepts at the boundary. -using AudioResourceType = std::string; +using PluginType = std::string; -struct AudioResourceMeta { +struct PluginMeta { PluginResourceId id; - AudioResourceVendor vendor; - AudioResourceAttributes attributes; - AudioResourceType type; + PluginVendor vendor; + PluginAttributes attributes; + PluginType type; const String& attributeVal(const String& key) const { @@ -66,7 +66,7 @@ struct AudioResourceMeta { && !type.empty(); } - bool operator==(const AudioResourceMeta& other) const + bool operator==(const PluginMeta& other) const { return id == other.id && vendor == other.vendor @@ -74,25 +74,25 @@ struct AudioResourceMeta { && attributes == other.attributes; } - bool operator!=(const AudioResourceMeta& other) const + bool operator!=(const PluginMeta& other) const { return !(*this == other); } - bool operator<(const AudioResourceMeta& other) const + bool operator<(const PluginMeta& other) const { return std::tie(id, vendor, type, attributes) < std::tie(other.id, other.vendor, other.type, other.attributes); } }; -using AudioResourceMetaList = std::vector; -using AudioResourceMetaSet = std::set; +using PluginMetaList = std::vector; +using PluginMetaSet = std::set; // Typed accessors over the string-encoded attributes map. Cheap alternative to // holding a typed variant in the storage itself; encodes the on-disk convention // ("true"/"1" → true) in one place so callers don't reimplement it. -inline bool boolAttribute(const AudioResourceMeta& meta, const String& key, bool fallback = false) +inline bool boolAttribute(const PluginMeta& meta, const String& key, bool fallback = false) { const String& v = meta.attributeVal(key); if (v.empty()) { @@ -101,7 +101,7 @@ inline bool boolAttribute(const AudioResourceMeta& meta, const String& key, bool return v == u"true" || v == u"1"; } -inline int intAttribute(const AudioResourceMeta& meta, const String& key, int fallback = 0) +inline int intAttribute(const PluginMeta& meta, const String& key, int fallback = 0) { const String& v = meta.attributeVal(key); if (v.empty()) { @@ -163,7 +163,7 @@ inline AudioPluginState audioPluginStateFromName(const std::string& name) } struct AudioPluginInfo { - AudioResourceMeta meta; + PluginMeta meta; io::path_t path; AudioPluginState state = AudioPluginState::Undefined; int errorCode = 0; diff --git a/framework/audioplugins/iaudiopluginmetareader.h b/framework/audioplugins/iaudiopluginmetareader.h index c1a1fb2dfc..9cbccea053 100644 --- a/framework/audioplugins/iaudiopluginmetareader.h +++ b/framework/audioplugins/iaudiopluginmetareader.h @@ -32,9 +32,9 @@ class IAudioPluginMetaReader public: virtual ~IAudioPluginMetaReader() = default; - virtual AudioResourceType metaType() const = 0; + virtual PluginType metaType() const = 0; virtual bool canReadMeta(const io::path_t& pluginPath) const = 0; - virtual RetVal readMeta(const io::path_t& pluginPath) const = 0; + virtual RetVal readMeta(const io::path_t& pluginPath) const = 0; }; using IAudioPluginMetaReaderPtr = std::shared_ptr; diff --git a/framework/audioplugins/iaudiopluginsconfiguration.h b/framework/audioplugins/iaudiopluginsconfiguration.h index f94e6579a9..7bebafab84 100644 --- a/framework/audioplugins/iaudiopluginsconfiguration.h +++ b/framework/audioplugins/iaudiopluginsconfiguration.h @@ -41,7 +41,7 @@ class IAudioPluginsConfiguration : MODULE_GLOBAL_INTERFACE // and re-injected (with the supplied default value) on load. The framework // is otherwise oblivious to their meaning. Apps register their own runtime // attributes (e.g. MuseScore registers playbackSetupData) at startup. - virtual const AudioResourceAttributes& runtimeAttributeDefaults() const = 0; - virtual void setRuntimeAttributeDefaults(const AudioResourceAttributes& defaults) = 0; + virtual const PluginAttributes& runtimeAttributeDefaults() const = 0; + virtual void setRuntimeAttributeDefaults(const PluginAttributes& defaults) = 0; }; } diff --git a/framework/audioplugins/internal/audiopluginsconfiguration.cpp b/framework/audioplugins/internal/audiopluginsconfiguration.cpp index a2acb7ebd2..0b22eaa82d 100644 --- a/framework/audioplugins/internal/audiopluginsconfiguration.cpp +++ b/framework/audioplugins/internal/audiopluginsconfiguration.cpp @@ -29,12 +29,12 @@ io::path_t AudioPluginsConfiguration::knownAudioPluginsFilePath() const return globalConfiguration()->userAppDataPath() + "/known_audio_plugins.json"; } -const AudioResourceAttributes& AudioPluginsConfiguration::runtimeAttributeDefaults() const +const PluginAttributes& AudioPluginsConfiguration::runtimeAttributeDefaults() const { return m_runtimeAttributeDefaults; } -void AudioPluginsConfiguration::setRuntimeAttributeDefaults(const AudioResourceAttributes& defaults) +void AudioPluginsConfiguration::setRuntimeAttributeDefaults(const PluginAttributes& defaults) { m_runtimeAttributeDefaults = defaults; } diff --git a/framework/audioplugins/internal/audiopluginsconfiguration.h b/framework/audioplugins/internal/audiopluginsconfiguration.h index 5d27a12df9..03a20d8e4d 100644 --- a/framework/audioplugins/internal/audiopluginsconfiguration.h +++ b/framework/audioplugins/internal/audiopluginsconfiguration.h @@ -38,10 +38,10 @@ class AudioPluginsConfiguration : public IAudioPluginsConfiguration, public muse io::path_t knownAudioPluginsFilePath() const override; - const AudioResourceAttributes& runtimeAttributeDefaults() const override; - void setRuntimeAttributeDefaults(const AudioResourceAttributes& defaults) override; + const PluginAttributes& runtimeAttributeDefaults() const override; + void setRuntimeAttributeDefaults(const PluginAttributes& defaults) override; private: - AudioResourceAttributes m_runtimeAttributeDefaults; + PluginAttributes m_runtimeAttributeDefaults; }; } diff --git a/framework/audioplugins/internal/knownaudiopluginsregister.cpp b/framework/audioplugins/internal/knownaudiopluginsregister.cpp index bda0972e2f..2000ff81a1 100644 --- a/framework/audioplugins/internal/knownaudiopluginsregister.cpp +++ b/framework/audioplugins/internal/knownaudiopluginsregister.cpp @@ -30,8 +30,8 @@ using namespace muse; using namespace muse::audioplugins; namespace muse::audioplugins { -static JsonObject attributesToJson(const AudioResourceAttributes& attributes, - const AudioResourceAttributes& runtimeOnly) +static JsonObject attributesToJson(const PluginAttributes& attributes, + const PluginAttributes& runtimeOnly) { JsonObject result; @@ -46,7 +46,7 @@ static JsonObject attributesToJson(const AudioResourceAttributes& attributes, return result; } -static JsonObject metaToJson(const AudioResourceMeta& meta, const AudioResourceAttributes& runtimeOnly) +static JsonObject metaToJson(const PluginMeta& meta, const PluginAttributes& runtimeOnly) { JsonObject result; @@ -65,9 +65,9 @@ static JsonObject metaToJson(const AudioResourceMeta& meta, const AudioResourceA return result; } -static AudioResourceAttributes attributesFromJson(const JsonObject& object) +static PluginAttributes attributesFromJson(const JsonObject& object) { - AudioResourceAttributes result; + PluginAttributes result; for (const std::string& key : object.keys()) { result.insert({ String::fromStdString(key), object.value(key).toString() }); @@ -76,9 +76,9 @@ static AudioResourceAttributes attributesFromJson(const JsonObject& object) return result; } -static AudioResourceMeta metaFromJson(const JsonObject& object) +static PluginMeta metaFromJson(const JsonObject& object) { - AudioResourceMeta result; + PluginMeta result; result.id = object.value("id").toStdString(); result.type = object.value("type").toStdString(); @@ -355,7 +355,7 @@ Ret KnownAudioPluginsRegister::writePluginsInfo() JsonArray array; - const AudioResourceAttributes& runtimeOnly = configuration()->runtimeAttributeDefaults(); + const PluginAttributes& runtimeOnly = configuration()->runtimeAttributeDefaults(); for (const auto& pair : m_pluginInfoMap) { const AudioPluginInfo& info = pair.second; diff --git a/framework/audioplugins/internal/registeraudiopluginsscenario.cpp b/framework/audioplugins/internal/registeraudiopluginsscenario.cpp index 727aa66d92..9fbeefcd1d 100644 --- a/framework/audioplugins/internal/registeraudiopluginsscenario.cpp +++ b/framework/audioplugins/internal/registeraudiopluginsscenario.cpp @@ -263,7 +263,7 @@ Ret RegisterAudioPluginsScenario::registerPlugin(const io::path_t& pluginPath) return make_ret(Err::UnknownPluginType); } - const RetVal metaList = reader->readMeta(pluginPath); + const RetVal metaList = reader->readMeta(pluginPath); if (!metaList.ret) { LOGE() << metaList.ret.toString(); return metaList.ret; @@ -272,7 +272,7 @@ Ret RegisterAudioPluginsScenario::registerPlugin(const io::path_t& pluginPath) AudioPluginInfoList infoList; infoList.reserve(metaList.val.size()); - for (const AudioResourceMeta& meta : metaList.val) { + for (const PluginMeta& meta : metaList.val) { AudioPluginInfo info; info.meta = meta; info.path = pluginPath; @@ -322,8 +322,8 @@ IAudioPluginMetaReaderPtr RegisterAudioPluginsScenario::metaReader(const io::pat return nullptr; } -audioplugins::AudioResourceType RegisterAudioPluginsScenario::metaType(const io::path_t& pluginPath) const +audioplugins::PluginType RegisterAudioPluginsScenario::metaType(const io::path_t& pluginPath) const { const IAudioPluginMetaReaderPtr reader = metaReader(pluginPath); - return reader ? reader->metaType() : audioplugins::AudioResourceType(); + return reader ? reader->metaType() : audioplugins::PluginType(); } diff --git a/framework/audioplugins/internal/registeraudiopluginsscenario.h b/framework/audioplugins/internal/registeraudiopluginsscenario.h index 7e377e42ee..e03aeee611 100644 --- a/framework/audioplugins/internal/registeraudiopluginsscenario.h +++ b/framework/audioplugins/internal/registeraudiopluginsscenario.h @@ -63,7 +63,7 @@ class RegisterAudioPluginsScenario : public IRegisterAudioPluginsScenario, publi Ret persistDiscoveredPlaceholders(const io::paths_t& pluginPaths); void processPluginsRegistration(const io::paths_t& pluginPaths); IAudioPluginMetaReaderPtr metaReader(const io::path_t& pluginPath) const; - AudioResourceType metaType(const io::path_t& pluginPath) const; + PluginType metaType(const io::path_t& pluginPath) const; Progress m_progress; bool m_aborted = false; diff --git a/framework/audioplugins/tests/knownaudiopluginsregistertest.cpp b/framework/audioplugins/tests/knownaudiopluginsregistertest.cpp index 4ea52a66a0..fc46172653 100644 --- a/framework/audioplugins/tests/knownaudiopluginsregistertest.cpp +++ b/framework/audioplugins/tests/knownaudiopluginsregistertest.cpp @@ -62,7 +62,7 @@ class AudioPlugins_KnownAudioPluginsRegisterTest : public ::testing::Test ON_CALL(*m_configuration, knownAudioPluginsFilePath()) .WillByDefault(Return(m_knownAudioPluginsFilePath)); - m_runtimeDefaults = AudioResourceAttributes { { kRuntimeAttrKey, kRuntimeAttrValue } }; + m_runtimeDefaults = PluginAttributes { { kRuntimeAttrKey, kRuntimeAttrValue } }; ON_CALL(*m_configuration, runtimeAttributeDefaults()) .WillByDefault(ReturnRef(m_runtimeDefaults)); @@ -72,7 +72,7 @@ class AudioPlugins_KnownAudioPluginsRegisterTest : public ::testing::Test ByteArray pluginInfoListToJson(const std::vector& infoList) const { - const std::map RESOURCE_TYPE_TO_STR { + const std::map RESOURCE_TYPE_TO_STR { { "VstPlugin", "VstPlugin" }, }; @@ -169,7 +169,7 @@ class AudioPlugins_KnownAudioPluginsRegisterTest : public ::testing::Test std::shared_ptr m_migrations; path_t m_knownAudioPluginsFilePath; - AudioResourceAttributes m_runtimeDefaults; + PluginAttributes m_runtimeDefaults; }; inline bool operator==(const AudioPluginInfo& info1, const AudioPluginInfo& info2) diff --git a/framework/audioplugins/tests/mocks/audiopluginmetareadermock.h b/framework/audioplugins/tests/mocks/audiopluginmetareadermock.h index 4732612f01..a95db020f8 100644 --- a/framework/audioplugins/tests/mocks/audiopluginmetareadermock.h +++ b/framework/audioplugins/tests/mocks/audiopluginmetareadermock.h @@ -29,8 +29,8 @@ namespace muse::audioplugins { class AudioPluginMetaReaderMock : public IAudioPluginMetaReader { public: - MOCK_METHOD(AudioResourceType, metaType, (), (const, override)); + MOCK_METHOD(PluginType, metaType, (), (const, override)); MOCK_METHOD(bool, canReadMeta, (const io::path_t&), (const, override)); - MOCK_METHOD(RetVal, readMeta, (const io::path_t&), (const, override)); + MOCK_METHOD(RetVal, readMeta, (const io::path_t&), (const, override)); }; } diff --git a/framework/audioplugins/tests/mocks/audiopluginsconfigurationmock.h b/framework/audioplugins/tests/mocks/audiopluginsconfigurationmock.h index 84afb8f260..8e2fb6cc98 100644 --- a/framework/audioplugins/tests/mocks/audiopluginsconfigurationmock.h +++ b/framework/audioplugins/tests/mocks/audiopluginsconfigurationmock.h @@ -32,7 +32,7 @@ class AudioPluginsConfigurationMock : public IAudioPluginsConfiguration MOCK_METHOD(io::path_t, knownAudioPluginsFilePath, (), (const, override)); - MOCK_METHOD(const AudioResourceAttributes&, runtimeAttributeDefaults, (), (const, override)); - MOCK_METHOD(void, setRuntimeAttributeDefaults, (const AudioResourceAttributes&), (override)); + MOCK_METHOD(const PluginAttributes&, runtimeAttributeDefaults, (), (const, override)); + MOCK_METHOD(void, setRuntimeAttributeDefaults, (const PluginAttributes&), (override)); }; } diff --git a/framework/audioplugins/tests/registeraudiopluginsscenariotest.cpp b/framework/audioplugins/tests/registeraudiopluginsscenariotest.cpp index 05854a595f..b97a1b0ea6 100644 --- a/framework/audioplugins/tests/registeraudiopluginsscenariotest.cpp +++ b/framework/audioplugins/tests/registeraudiopluginsscenariotest.cpp @@ -535,14 +535,14 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, RegisterPlugin) // [GIVEN] Some plugin we want to register path_t pluginPath = "/some/test/path/to/plugin/AAA.vst3"; - AudioResourceMetaList metaList; + PluginMetaList metaList; - AudioResourceMeta pluginMeta1; + PluginMeta pluginMeta1; pluginMeta1.id = "Mono plugin"; pluginMeta1.attributes.insert({ String(u"categories"), u"Fx|Mono" }); metaList.push_back(pluginMeta1); - AudioResourceMeta pluginMeta2; + PluginMeta pluginMeta2; pluginMeta2.id = "Stereo plugin"; pluginMeta2.attributes.insert({ String(u"categories"), u"Fx|Stereo" }); metaList.push_back(pluginMeta2); @@ -552,13 +552,13 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, RegisterPlugin) ASSERT_TRUE(mock); ON_CALL(*mock, readMeta(pluginPath)) - .WillByDefault(Return(RetVal::make_ok(metaList))); + .WillByDefault(Return(RetVal::make_ok(metaList))); // [THEN] The plugin has been registered AudioPluginInfoList expectedInfoList; expectedInfoList.reserve(metaList.size()); - for (const AudioResourceMeta& meta : metaList) { + for (const PluginMeta& meta : metaList) { AudioPluginInfo expectedPluginInfo; expectedPluginInfo.meta = meta; expectedPluginInfo.path = pluginPath; diff --git a/framework/vst/internal/vstpluginmetareader.cpp b/framework/vst/internal/vstpluginmetareader.cpp index 470019c832..775e4a0a97 100644 --- a/framework/vst/internal/vstpluginmetareader.cpp +++ b/framework/vst/internal/vstpluginmetareader.cpp @@ -34,7 +34,7 @@ using namespace muse; using namespace muse::audioplugins; using namespace muse::vst; -audioplugins::AudioResourceType VstPluginMetaReader::metaType() const +audioplugins::PluginType VstPluginMetaReader::metaType() const { return std::string(AUDIO_RESOURCE_TYPE_NAME); } diff --git a/framework/vst/internal/vstpluginmetareader.h b/framework/vst/internal/vstpluginmetareader.h index 417f3dff8e..971be3a0e3 100644 --- a/framework/vst/internal/vstpluginmetareader.h +++ b/framework/vst/internal/vstpluginmetareader.h @@ -29,9 +29,9 @@ namespace muse::vst { class VstPluginMetaReader : public audioplugins::IAudioPluginMetaReader { public: - audioplugins::AudioResourceType metaType() const override; + audioplugins::PluginType metaType() const override; bool canReadMeta(const io::path_t& pluginPath) const override; - RetVal readMeta(const io::path_t& pluginPath) const override; + RetVal readMeta(const io::path_t& pluginPath) const override; }; } From 2ea7afd0b63181808c10e8d5b8c3d7923d6772cb Mon Sep 17 00:00:00 2001 From: Paul MARTIN Date: Fri, 29 May 2026 13:33:52 +0200 Subject: [PATCH 7/9] [audio] decouple from audioplugins: define meta types locally + bridge in vstmodulesrepository Audio no longer re-exports AudioResourceMeta/Vendor/Attributes/MetaList/ MetaSet from audioplugins. They are defined locally in audio/common/ audiotypes.h with the same field layout as audioplugins::PluginMeta; the C++ type system treats them as distinct now. Audio also drops the #include "audioplugins/audiopluginstypes.h" - it forward-declares just the three audioplugins id/type aliases that the existing bridge helpers (toAudioResourceId, sourceTypeFromResourceType, audioResourceTypeToString) need in their signatures. Audio gets local boolAttribute / intAttribute helpers. hasNativeEditor- Support and isOnlineAudioResource now use the local versions instead of reaching into audioplugins for them. vstmodulesrepository.cpp - the only internal site where audioplugins:: PluginMeta crosses into audio::AudioResourceMetaList - converts field- wise at that loop. isResourceType call replaced by a direct comparison against the wire string so the lambda no longer needs audio:: helpers on audioplugins data. vstpluginmetareader.cpp - leftover from the prior rename: the file is "using namespace muse::audioplugins;" and produces audioplugins-domain data (it implements IAudioPluginMetaReader), so bare AudioResourceMeta references resolve via that using-directive and needed renaming to PluginMeta to match the rest of the audioplugins module. VST is stubbed in our debug build so the prior commit missed this; ran build with the module conceptually enabled to confirm syntax. A static_assert in audioresourcetypes_tests.cpp now pins that audio::AudioResourceMeta and audioplugins::PluginMeta are distinct types - documenting the seam at compile time. --- framework/audio/common/audiotypes.h | 89 +++++++++++++++++-- framework/audio/common/audioutils.h | 2 +- .../audio/tests/audioresourcetypes_tests.cpp | 9 +- .../vst/internal/vstmodulesrepository.cpp | 12 ++- .../vst/internal/vstpluginmetareader.cpp | 8 +- 5 files changed, 104 insertions(+), 16 deletions(-) diff --git a/framework/audio/common/audiotypes.h b/framework/audio/common/audiotypes.h index 76ea12456c..c4d3312ad3 100644 --- a/framework/audio/common/audiotypes.h +++ b/framework/audio/common/audiotypes.h @@ -22,11 +22,14 @@ #pragma once +#include #include #include #include #include #include +#include +#include #include "global/types/number.h" #include "global/types/secs.h" @@ -37,10 +40,20 @@ #include "mpe/events.h" -#include "audioplugins/audiopluginstypes.h" - #include "log.h" +// Audio module is now self-contained: it does NOT include +// audioplugins/audiopluginstypes.h. The bridge helpers below +// (toAudioResourceId, sourceTypeFromResourceType, etc.) take audioplugins +// id/type aliases - all std::string under the hood - so forward-declaring +// the aliases is enough. Pulling in the full plugin meta types is the +// caller's job at the bridge boundary; the engine never needs them. +namespace muse::audioplugins { +using PluginResourceId = std::string; +using PluginResourceIdList = std::vector; +using PluginType = std::string; +} + namespace muse::audio { using secs_t = muse::secs_t; using msecs_t = muse::msecs_t; @@ -189,11 +202,71 @@ inline AudioResourceIdList toAudioResourceIdList(const muse::audioplugins::Plugi return AudioResourceIdList(ids.begin(), ids.end()); } -using AudioResourceVendor = muse::audioplugins::PluginVendor; -using AudioResourceAttributes = muse::audioplugins::PluginAttributes; -using AudioResourceMeta = muse::audioplugins::PluginMeta; -using AudioResourceMetaList = muse::audioplugins::PluginMetaList; -using AudioResourceMetaSet = muse::audioplugins::PluginMetaSet; +// Engine-domain resource meta is owned here by audio; cache-domain plugin +// meta lives in audioplugins::PluginMeta. The two structs share a layout +// today (id + type-as-string + vendor + attributes-as-string-map) but stay +// separate so neither module depends on the other at the type level. Apps +// bridge field-wise at the audio<->audioplugins boundary. +using AudioResourceVendor = std::string; +using AudioResourceAttributes = std::map; + +struct AudioResourceMeta { + AudioResourceId id; + AudioResourceVendor vendor; + AudioResourceAttributes attributes; + std::string type; // wire-string format identifier; see AudioResourceType enum below + + const String& attributeVal(const String& key) const + { + auto search = attributes.find(key); + if (search != attributes.cend()) { + return search->second; + } + static const String empty; + return empty; + } + + bool isValid() const + { + return !id.empty() && !vendor.empty() && !type.empty(); + } + + bool operator==(const AudioResourceMeta& other) const + { + return id == other.id && vendor == other.vendor && type == other.type && attributes == other.attributes; + } + + bool operator!=(const AudioResourceMeta& other) const { return !(*this == other); } + + bool operator<(const AudioResourceMeta& other) const + { + return std::tie(id, vendor, type, attributes) + < std::tie(other.id, other.vendor, other.type, other.attributes); + } +}; + +using AudioResourceMetaList = std::vector; +using AudioResourceMetaSet = std::set; + +inline bool boolAttribute(const AudioResourceMeta& meta, const String& key, bool fallback = false) +{ + const String& v = meta.attributeVal(key); + if (v.empty()) { + return fallback; + } + return v == u"true" || v == u"1"; +} + +inline int intAttribute(const AudioResourceMeta& meta, const String& key, int fallback = 0) +{ + const String& v = meta.attributeVal(key); + if (v.empty()) { + return fallback; + } + bool ok = true; + int n = v.toInt(&ok); + return ok ? n : fallback; +} // Wire-format identifiers for the formats the muse audio engine dispatches // directly. The strings here are the canonical names persisted in the JSON @@ -259,7 +332,7 @@ static const String HAS_NATIVE_EDITOR_SUPPORT_ATTRIBUTE(u"hasNativeEditorSupport inline bool hasNativeEditorSupport(const AudioResourceMeta& meta) { - return muse::audioplugins::boolAttribute(meta, HAS_NATIVE_EDITOR_SUPPORT_ATTRIBUTE); + return boolAttribute(meta, HAS_NATIVE_EDITOR_SUPPORT_ATTRIBUTE); } static const std::map RESOURCE_TYPE_MAP = { diff --git a/framework/audio/common/audioutils.h b/framework/audio/common/audioutils.h index 303f2e7a20..c81d0e6509 100644 --- a/framework/audio/common/audioutils.h +++ b/framework/audio/common/audioutils.h @@ -110,6 +110,6 @@ inline AudioFxCategories audioFxCategoriesFromString(const String& str) inline bool isOnlineAudioResource(const AudioResourceMeta& meta) { - return muse::audioplugins::boolAttribute(meta, u"isOnline"); + return boolAttribute(meta, u"isOnline"); } } diff --git a/framework/audio/tests/audioresourcetypes_tests.cpp b/framework/audio/tests/audioresourcetypes_tests.cpp index 9401354e0f..5b2afd5047 100644 --- a/framework/audio/tests/audioresourcetypes_tests.cpp +++ b/framework/audio/tests/audioresourcetypes_tests.cpp @@ -25,6 +25,7 @@ // is necessary to avoid an ODR-violation segfault at static init. #pragma pack(push, 1) #include "audio/common/audiotypes.h" +#include "audioplugins/audiopluginstypes.h" #include "musesampler/musesamplertypes.h" #include "vst/vstpluginattrs.h" #pragma pack(pop) @@ -37,6 +38,12 @@ using namespace muse; static_assert(std::is_same_v); static_assert(std::is_same_v); +// audio::AudioResourceMeta and audioplugins::PluginMeta are independent struct +// types now (audio owns its own engine-domain meta). They share a layout but +// the C++ type system treats them as separate; conversion is field-wise at +// the app-side audio<->audioplugins boundary. +static_assert(!std::is_same_v); + // The on-disk plugin cache stores AudioResourceMeta::type as the canonical // wire string. These strings must stay stable across releases — caches written // by older builds must remain readable. Each plugin module owns its own copy @@ -86,7 +93,7 @@ TEST(Audio_AudioResourceTypes, ResourceTypeFromStringRejectsUnknown) TEST(Audio_AudioResourceTypes, IsResourceTypeHelper) { - audioplugins::PluginMeta meta; + audio::AudioResourceMeta meta; meta.type = vst::AUDIO_RESOURCE_TYPE_NAME; EXPECT_TRUE(audio::isResourceType(meta, audio::AudioResourceType::VstPlugin)); EXPECT_FALSE(audio::isResourceType(meta, audio::AudioResourceType::FluidSoundfont)); diff --git a/framework/vst/internal/vstmodulesrepository.cpp b/framework/vst/internal/vstmodulesrepository.cpp index ce324006da..0092e299fd 100644 --- a/framework/vst/internal/vstmodulesrepository.cpp +++ b/framework/vst/internal/vstmodulesrepository.cpp @@ -123,7 +123,7 @@ void VstModulesRepository::refresh() muse::audio::AudioResourceMetaList VstModulesRepository::modulesMetaList(PluginType type) const { auto infoAccepted = [type](const audioplugins::AudioPluginInfo& info) { - if (!muse::audio::isResourceType(info.meta, muse::audio::AudioResourceType::VstPlugin) + if (info.meta.type != muse::vst::AUDIO_RESOURCE_TYPE_NAME || info.state != audioplugins::AudioPluginState::Validated) { return false; } @@ -136,8 +136,16 @@ muse::audio::AudioResourceMetaList VstModulesRepository::modulesMetaList(PluginT muse::audio::AudioResourceMetaList result; result.reserve(infoList.size()); + // Bridge audioplugins::PluginMeta -> audio::AudioResourceMeta field-wise. + // The two structs share a layout today; this loop is the framework-internal + // seam between the plugin cache (audioplugins) and the audio engine. for (const audioplugins::AudioPluginInfo& info : infoList) { - result.push_back(info.meta); + muse::audio::AudioResourceMeta meta; + meta.id = info.meta.id; + meta.vendor = info.meta.vendor; + meta.attributes = info.meta.attributes; + meta.type = info.meta.type; + result.push_back(std::move(meta)); } return result; diff --git a/framework/vst/internal/vstpluginmetareader.cpp b/framework/vst/internal/vstpluginmetareader.cpp index 775e4a0a97..415c2801a7 100644 --- a/framework/vst/internal/vstpluginmetareader.cpp +++ b/framework/vst/internal/vstpluginmetareader.cpp @@ -44,7 +44,7 @@ bool VstPluginMetaReader::canReadMeta(const io::path_t& pluginPath) const return io::suffix(pluginPath) == VST3_PACKAGE_EXTENSION; } -RetVal VstPluginMetaReader::readMeta(const io::path_t& pluginPath) const +RetVal VstPluginMetaReader::readMeta(const io::path_t& pluginPath) const { PluginModulePtr module = createModule(pluginPath); if (!module) { @@ -52,14 +52,14 @@ RetVal VstPluginMetaReader::readMeta(const io::path_t& pl } const auto& factory = module->getFactory(); - AudioResourceMetaList result; + PluginMetaList result; for (const ClassInfo& classInfo : factory.classInfos()) { if (classInfo.category() != kVstAudioEffectClass) { continue; } - AudioResourceMeta meta; + PluginMeta meta; meta.id = io::completeBasename(pluginPath).toStdString(); meta.type = AUDIO_RESOURCE_TYPE_NAME; meta.attributes.emplace(CATEGORIES_ATTRIBUTE, String::fromStdString(classInfo.subCategoriesString())); @@ -74,5 +74,5 @@ RetVal VstPluginMetaReader::readMeta(const io::path_t& pl return make_ret(Err::NoAudioEffect); } - return RetVal::make_ok(result); + return RetVal::make_ok(result); } From 854cdbd75f86c1558bbb8012df9597c7ce74dcb9 Mon Sep 17 00:00:00 2001 From: Paul MARTIN Date: Wed, 10 Jun 2026 15:04:57 +0200 Subject: [PATCH 8/9] [audioplugins] fix stale Discovered state comment persistDiscoveredPlaceholders() produces the Discovered state (the validate-later flow), so it is no longer 'reserved with no producer'. --- framework/audioplugins/audiopluginstypes.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/framework/audioplugins/audiopluginstypes.h b/framework/audioplugins/audiopluginstypes.h index dbda1725cd..6115f5223c 100644 --- a/framework/audioplugins/audiopluginstypes.h +++ b/framework/audioplugins/audiopluginstypes.h @@ -113,9 +113,10 @@ inline int intAttribute(const PluginMeta& meta, const String& key, int fallback } // Lifecycle state of a plugin entry in the cache. -// Discovered: scanner found the file at `path`; validation has not yet completed. -// (Reserved: no producer in the framework yet — apps that want a -// pre-validation insertion flow can transition to this state.) +// Discovered: scanner found the file at `path`; validation has not yet +// completed. RegisterAudioPluginsScenario::persistDiscoveredPlaceholders() +// writes these when paths are registered with validate=false (the +// "validate later" flow); scanPlugins() re-validates them later. // Validated: validation succeeded; the plugin is usable. // Missing: `path` was known previously but the scanner no longer finds it. // Error: validation failed; `errorCode` carries the details. From b8ca198cc172b48ed6a7c4f2f0656f33c2225cc7 Mon Sep 17 00:00:00 2001 From: Paul MARTIN Date: Fri, 12 Jun 2026 12:46:33 +0200 Subject: [PATCH 9/9] [audioplugins] re-validate rediscovered plugins instead of trusting Missing scanPlugins marked a vanished plugin Missing regardless of its prior state, and a later rediscovery flipped it straight to Validated. A plugin that was Error before going Missing - or one whose binary changed to a broken build while away - would thus be trusted without re-validation. Route a reappeared (formerly Missing) path back through newPluginPaths so the out-of-process validation re-derives Validated/Error, mirroring the Discovered (interrupted-scan) handling. Drops the rediscoveredPluginIds list and the blind setPluginsState(..., Validated) step. Adds scanPlugins classification tests. --- .../internal/registeraudiopluginsscenario.cpp | 21 ++++--- .../iregisteraudiopluginsscenario.h | 6 +- .../registeraudiopluginsscenariotest.cpp | 57 +++++++------------ 3 files changed, 34 insertions(+), 50 deletions(-) diff --git a/framework/audioplugins/internal/registeraudiopluginsscenario.cpp b/framework/audioplugins/internal/registeraudiopluginsscenario.cpp index 9fbeefcd1d..25a4be39aa 100644 --- a/framework/audioplugins/internal/registeraudiopluginsscenario.cpp +++ b/framework/audioplugins/internal/registeraudiopluginsscenario.cpp @@ -89,11 +89,16 @@ PluginScanResult RegisterAudioPluginsScenario::scanPlugins(Progress* progress) c continue; } - // Every formerly Missing ID under this path is rediscovered. - for (const CacheEntry& entry : entries) { - if (entry.state == AudioPluginState::Missing) { - result.rediscoveredPluginIds.push_back(entry.id); - } + // A path we previously marked Missing is back. Re-validate it + // out-of-process rather than trusting the cache — the binary may now + // be a newer build that no longer passes validation. Mirrors the + // Discovered handling above. + const bool hasMissing = std::any_of(entries.cbegin(), entries.cend(), + [](const CacheEntry& e) { + return e.state == AudioPluginState::Missing; + }); + if (hasMissing) { + result.newPluginPaths.push_back(path); } registered.erase(it); } @@ -124,12 +129,6 @@ Ret RegisterAudioPluginsScenario::updatePluginsRegistry() return ret; } - ret = knownPluginsRegister()->setPluginsState(result.rediscoveredPluginIds, AudioPluginState::Validated); - if (!ret) { - LOGE() << "Failed to mark rediscovered plugins: " << ret.toString(); - return ret; - } - ret = registerNewPlugins(result.newPluginPaths, /*validate*/ true); if (!ret) { LOGE() << "Failed to register new plugins: " << ret.toString(); diff --git a/framework/audioplugins/iregisteraudiopluginsscenario.h b/framework/audioplugins/iregisteraudiopluginsscenario.h index d7dc4c74ce..c2c73c39de 100644 --- a/framework/audioplugins/iregisteraudiopluginsscenario.h +++ b/framework/audioplugins/iregisteraudiopluginsscenario.h @@ -31,9 +31,11 @@ namespace muse::audioplugins { struct PluginScanResult { - io::paths_t newPluginPaths; // not in cache; will be inserted via subprocess validation + // not in cache, or a previously Missing entry the scanner found again: both + // get (re-)validated via subprocess. A returned plugin is re-validated rather + // than trusted, since the binary on disk may now be a newer, broken build. + io::paths_t newPluginPaths; PluginResourceIdList missingPluginIds; // in cache but not currently found by any scanner - PluginResourceIdList rediscoveredPluginIds; // previously Missing entries the scanner found again }; class IRegisterAudioPluginsScenario : MODULE_CONTEXT_INTERFACE diff --git a/framework/audioplugins/tests/registeraudiopluginsscenariotest.cpp b/framework/audioplugins/tests/registeraudiopluginsscenariotest.cpp index b97a1b0ea6..d175bcb1b9 100644 --- a/framework/audioplugins/tests/registeraudiopluginsscenariotest.cpp +++ b/framework/audioplugins/tests/registeraudiopluginsscenariotest.cpp @@ -297,9 +297,6 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_Mark EXPECT_CALL(*m_knownPlugins, setPluginsState(uninstalledPluginIdList, AudioPluginState::Missing)) .WillOnce(Return(make_ok())); - EXPECT_CALL(*m_knownPlugins, setPluginsState(PluginResourceIdList {}, AudioPluginState::Validated)) - .WillOnce(Return(make_ok())); - EXPECT_CALL(*m_knownPlugins, unregisterPlugins(_)) .Times(0); @@ -313,7 +310,7 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_Mark EXPECT_TRUE(ret); } -TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_RediscoverFormerlyMissing) +TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, ScanPlugins_FormerlyMissingFoundAgainIsRevalidated) { auto createPluginInfo = [](const io::path_t& path, AudioPluginState state) { AudioPluginInfo info; @@ -324,7 +321,7 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_Redi return info; }; - // [GIVEN] One Missing entry that gets reinstalled, one untouched Validated entry + // [GIVEN] One Missing entry that reappears, one untouched Validated entry AudioPluginInfoList knownPlugins; knownPlugins.push_back(createPluginInfo("/some/path/AAA.vst3", AudioPluginState::Missing)); knownPlugins.push_back(createPluginInfo("/some/path/BBB.vst3", AudioPluginState::Validated)); @@ -346,20 +343,16 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_Redi .WillByDefault(Return(foundPluginPaths)); } - // [THEN] Only AAA gets transitioned back to Validated - PluginResourceIdList rediscoveredIds { knownPlugins[0].meta.id }; - - EXPECT_CALL(*m_knownPlugins, setPluginsState(PluginResourceIdList {}, AudioPluginState::Missing)) - .WillOnce(Return(make_ok())); - - EXPECT_CALL(*m_knownPlugins, setPluginsState(rediscoveredIds, AudioPluginState::Validated)) - .WillOnce(Return(make_ok())); - - EXPECT_CALL(*m_knownPlugins, load()) - .WillOnce(Return(muse::make_ok())); + // [WHEN] Scanning + const PluginScanResult result = m_scenario->scanPlugins(); - Ret ret = m_scenario->updatePluginsRegistry(); - EXPECT_TRUE(ret); + // [THEN] The reappeared Missing plugin is queued for out-of-process + // re-validation rather than trusted straight back to Validated — its binary + // may now be a newer build that no longer passes validation. The untouched + // Validated plugin is left alone. + EXPECT_TRUE(muse::contains(result.newPluginPaths, path_t("/some/path/AAA.vst3"))); + EXPECT_FALSE(muse::contains(result.newPluginPaths, path_t("/some/path/BBB.vst3"))); + EXPECT_TRUE(result.missingPluginIds.empty()); } TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_MultiPluginBinaryMarksEveryIdMissing) @@ -403,8 +396,6 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_Mult EXPECT_CALL(*m_knownPlugins, setPluginsState(expectedMissing, AudioPluginState::Missing)) .WillOnce(Return(make_ok())); - EXPECT_CALL(*m_knownPlugins, setPluginsState(PluginResourceIdList {}, AudioPluginState::Validated)) - .WillOnce(Return(make_ok())); EXPECT_CALL(*m_knownPlugins, load()) .WillOnce(Return(make_ok())); @@ -413,7 +404,7 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_Mult EXPECT_TRUE(ret); } -TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_MultiPluginBinaryRediscoversEveryId) +TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, ScanPlugins_FormerlyMissingMultiIdBinaryQueuedOnce) { auto createPluginInfo = [](const io::path_t& path, const PluginResourceId& id, AudioPluginState state) { AudioPluginInfo info; @@ -445,19 +436,14 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_Mult .WillByDefault(Return(foundPluginPaths)); } - // [THEN] BOTH ids transition back to Validated — not just the last one. - PluginResourceIdList expectedRediscovered { "Shell FxA", "Shell FxB" }; - - EXPECT_CALL(*m_knownPlugins, setPluginsState(PluginResourceIdList {}, AudioPluginState::Missing)) - .WillOnce(Return(make_ok())); - EXPECT_CALL(*m_knownPlugins, setPluginsState(expectedRediscovered, AudioPluginState::Validated)) - .WillOnce(Return(make_ok())); + // [WHEN] Scanning + const PluginScanResult result = m_scenario->scanPlugins(); - EXPECT_CALL(*m_knownPlugins, load()) - .WillOnce(Return(make_ok())); - - Ret ret = m_scenario->updatePluginsRegistry(); - EXPECT_TRUE(ret); + // [THEN] The reappeared binary is queued once for re-validation; the + // subprocess re-derives each id's state. Nothing is trusted back to + // Validated, and nothing is left Missing. + EXPECT_EQ(result.newPluginPaths, paths_t { shellPath }); + EXPECT_TRUE(result.missingPluginIds.empty()); } TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_LeftoverDiscoveredRevalidates) @@ -491,12 +477,9 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_Left // [THEN] The Discovered path is treated as new — registered as a fresh // placeholder and re-validated via subprocess. It is NOT marked Missing - // (it's still on disk) and it is NOT considered "rediscovered" (that's - // for paths transitioning out of Missing). + // (it's still on disk). EXPECT_CALL(*m_knownPlugins, setPluginsState(PluginResourceIdList {}, AudioPluginState::Missing)) .WillOnce(Return(make_ok())); - EXPECT_CALL(*m_knownPlugins, setPluginsState(PluginResourceIdList {}, AudioPluginState::Validated)) - .WillOnce(Return(make_ok())); // [THEN] registerNewPlugins writes a Discovered placeholder for the path // before spawning the subprocess.