diff --git a/framework/audio/common/audiotypes.h b/framework/audio/common/audiotypes.h index 3e92303386..312a7c7464 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 { @@ -167,86 +169,91 @@ 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; + +// 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(). 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_MAP = { - { AudioResourceType::Undefined, "undefined" }, - { AudioResourceType::MuseSamplerSoundPack, "muse_sampler_sound_pack" }, - { 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 std::map RESOURCE_TYPE_NAMES = { + { AudioResourceType::FluidSoundfont, std::string(FLUID_SOUNDFONT_TYPE_NAME) }, + { AudioResourceType::VstPlugin, "VstPlugin" }, + { AudioResourceType::NativeEffect, std::string(NATIVE_EFFECT_TYPE_NAME) }, + { AudioResourceType::MuseSamplerSoundPack, "MuseSamplerSoundPack" }, }; -struct AudioResourceMeta { - AudioResourceId id; - AudioResourceVendor vendor; - AudioResourceAttributes attributes; - AudioResourceType type = AudioResourceType::Undefined; - bool hasNativeEditorSupport = false; +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; +} - const String& attributeVal(const String& key) const - { - auto search = attributes.find(key); - if (search != attributes.cend()) { - return search->second; +inline AudioResourceType resourceTypeFromString(const std::string& name) +{ + for (const auto& kv : RESOURCE_TYPE_NAMES) { + if (kv.second == name) { + return kv.first; } - - static String empty; - return empty; } + return AudioResourceType::Undefined; +} - bool isValid() const - { - return !id.empty() - && !vendor.empty() - && type != AudioResourceType::Undefined; - } +inline bool isResourceType(const AudioResourceMeta& meta, AudioResourceType type) +{ + return resourceTypeFromString(meta.type) == type; +} - bool operator==(const AudioResourceMeta& other) const - { - return id == other.id - && vendor == other.vendor - && type == other.type - && hasNativeEditorSupport == other.hasNativeEditorSupport - && attributes == other.attributes; - } +static const String PLAYBACK_SETUP_DATA_ATTRIBUTE(u"playbackSetupData"); +static const String HAS_NATIVE_EDITOR_SUPPORT_ATTRIBUTE(u"hasNativeEditorSupport"); - bool operator!=(const AudioResourceMeta& other) const - { - return !(*this == other); - } +inline bool hasNativeEditorSupport(const AudioResourceMeta& meta) +{ + return muse::audioplugins::boolAttribute(meta, HAS_NATIVE_EDITOR_SUPPORT_ATTRIBUTE); +} - bool operator<(const AudioResourceMeta& other) const - { - return id < other.id - || vendor < other.vendor; - } +static const std::map RESOURCE_TYPE_MAP = { + { AudioResourceType::Undefined, "undefined" }, + { AudioResourceType::MuseSamplerSoundPack, "muse_sampler_sound_pack" }, + { AudioResourceType::FluidSoundfont, "fluid_soundfont" }, + { AudioResourceType::VstPlugin, "vst_plugin" }, + { AudioResourceType::NativeEffect, "muse_plugin" }, }; -using AudioResourceMetaList = std::vector; -using AudioResourceMetaSet = std::set; - static const AudioResourceId MUSE_REVERB_ID("Muse Reverb"); enum class AudioFxType { @@ -304,14 +311,11 @@ 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: - case AudioResourceType::Lv2Plugin: case AudioResourceType::FluidSoundfont: case AudioResourceType::MuseSamplerSoundPack: - case AudioResourceType::NyquistPlugin: case AudioResourceType::Undefined: break; } @@ -383,16 +387,13 @@ 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; - 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 abe2b6758e..299d30acd4 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 = NATIVE_EFFECT_TYPE_NAME; 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 (isResourceType(params.resourceMeta, AudioResourceType::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 (isResourceType(params.resourceMeta, AudioResourceType::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 f02a13e23b..1986cfeb00 100644 --- a/framework/audio/common/rpc/rpcpacker.h +++ b/framework/audio/common/rpc/rpcpacker.h @@ -186,12 +186,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 7906bc4316..835b7a9ec1 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 b86fe8c184..92beb5bf49 100644 --- a/framework/audio/engine/internal/enginerpccontroller.cpp +++ b/framework/audio/engine/internal/enginerpccontroller.cpp @@ -225,10 +225,8 @@ void EngineRpcController::init() } }; - AudioResourceType resourceType = params.source.resourceMeta.type; - // Not Fluid - if (resourceType != AudioResourceType::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 e493098df5..8a9b145c3b 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 = FLUID_SOUNDFONT_TYPE_NAME; 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 = FLUID_SOUNDFONT_TYPE_NAME; 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/CMakeLists.txt b/framework/audio/tests/CMakeLists.txt index 222a0445d9..569cf9f304 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/audio/tests/rpcpacker_tests.cpp b/framework/audio/tests/rpcpacker_tests.cpp index f6e39ccb2d..4dec309b0b 100644 --- a/framework/audio/tests/rpcpacker_tests.cpp +++ b/framework/audio/tests/rpcpacker_tests.cpp @@ -80,17 +80,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); @@ -102,7 +101,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) @@ -114,7 +113,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 a91d86ea01..dc18ed19fc 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 e7879403f7..f80c3c306b 100644 --- a/framework/audioplugins/audiopluginstypes.h +++ b/framework/audioplugins/audiopluginstypes.h @@ -21,21 +21,149 @@ */ #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::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 d4259d8578..9c472d3ed9 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 cb3e660def..2048ce7772 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/internal/audiopluginsutils.h b/framework/audioplugins/iknownaudiopluginsmigrationregister.h similarity index 56% rename from framework/audioplugins/internal/audiopluginsutils.h rename to framework/audioplugins/iknownaudiopluginsmigrationregister.h index d24fd48b26..3ff2d075ae 100644 --- a/framework/audioplugins/internal/audiopluginsutils.h +++ b/framework/audioplugins/iknownaudiopluginsmigrationregister.h @@ -5,7 +5,7 @@ * MuseScore * 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,26 @@ */ #pragma once -#include "../audiopluginstypes.h" +#include + +#include "modularity/imoduleinterface.h" + +#include "global/serialization/json.h" +#include "global/types/ret.h" namespace muse::audioplugins { -inline AudioPluginType audioPluginTypeFromCategoriesString(const String& categoriesStr) +inline constexpr int CURRENT_KNOWN_AUDIO_PLUGINS_VERSION = 3; + +class IKnownAudioPluginsMigrationRegister : MODULE_GLOBAL_INTERFACE { - static const std::vector > STRING_TO_PLUGIN_TYPE_LIST = { - { u"Instrument", AudioPluginType::Instrument }, - { u"Fx", AudioPluginType::Fx }, - }; - - 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; - } - } - - return AudioPluginType::Undefined; -} + 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 3e27623fe0..4b90a56b25 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 babfab8e5a..2409e1fc01 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 204f63ca07..e3863ba5be 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..a56a67a1ca --- /dev/null +++ b/framework/audioplugins/internal/knownaudiopluginsmigrationregister.cpp @@ -0,0 +1,94 @@ +/* + * 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", 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; + }); +} + +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/knownaudiopluginsmigrationregister.h b/framework/audioplugins/internal/knownaudiopluginsmigrationregister.h new file mode 100644 index 0000000000..4c5be2eaa1 --- /dev/null +++ b/framework/audioplugins/internal/knownaudiopluginsmigrationregister.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 . + */ +#pragma once + +#include + +#include "../iknownaudiopluginsmigrationregister.h" + +namespace muse::audioplugins { +class KnownAudioPluginsMigrationRegister : public IKnownAudioPluginsMigrationRegister +{ +public: + KnownAudioPluginsMigrationRegister(); + + void registerMigration(int fromVersion, PluginsMigration cb) override; + Ret migrate(int fromVersion, int toVersion, JsonArray& plugins) const override; + +private: + std::map m_migrations; +}; +} diff --git a/framework/audioplugins/internal/knownaudiopluginsregister.cpp b/framework/audioplugins/internal/knownaudiopluginsregister.cpp index ce2d250163..12a931fee3 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,26 +109,53 @@ 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(); + fileVersion = root.value("version").toInt(); + array = root.value("plugins").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()) { + info.meta.attributes.emplace(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(); m_pluginPaths.insert(info.path); @@ -223,6 +237,34 @@ Ret KnownAudioPluginsRegister::registerPlugins(const AudioPluginInfoList& list) return ret; } +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) { @@ -251,19 +293,51 @@ Ret KnownAudioPluginsRegister::unregisterPlugins(const AudioResourceIdList& reso return ret; } +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); + + // Skip writing the state field for Undefined (would serialize as ""); + // load() reads a missing/empty "state" as Undefined too, so this round-trips. + if (info.state != AudioPluginState::Undefined) { + obj.set("state", audioPluginStateName(info.state)); + } if (info.errorCode != 0) { obj.set("errorCode", info.errorCode); @@ -272,8 +346,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 aec9dab529..3d98757124 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 5994e8926a..f9e99ba6a5 100644 --- a/framework/audioplugins/internal/registeraudiopluginsscenario.cpp +++ b/framework/audioplugins/internal/registeraudiopluginsscenario.cpp @@ -28,12 +28,10 @@ #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() @@ -56,23 +54,42 @@ PluginScanResult RegisterAudioPluginsScenario::scanPlugins(Progress* progress) c PluginScanResult result; - std::map registered; + struct CacheEntry { + AudioResourceId id; + AudioPluginState state; + }; + std::map registered; for (const auto& info : knownPluginsRegister()->pluginInfoList()) { - registered[info.path] = info.meta.id; + registered[info.path] = { info.meta.id, info.state }; } for (const auto& scanner : scannerRegister()->scanners()) { for (const auto& path : scanner->scanPlugins(progress)) { - if (auto it = registered.find(path); it != registered.end()) { - registered.erase(it); - } else { + auto it = registered.find(path); + if (it == registered.end()) { result.newPluginPaths.push_back(path); + continue; + } + + // Placeholder from a prior interrupted run: re-validate. + if (it->second.state == AudioPluginState::Discovered) { + result.newPluginPaths.push_back(path); + registered.erase(it); + continue; } + + if (it->second.state == AudioPluginState::Missing) { + result.rediscoveredPluginIds.push_back(it->second.id); + } + registered.erase(it); } } - for (const auto& [path, id] : registered) { - result.missingPluginIds.push_back(id); + for (const auto& [path, entry] : registered) { + // Skip entries already Missing — only transitions need a state change. + if (entry.state != AudioPluginState::Missing) { + result.missingPluginIds.push_back(entry.id); + } } return result; @@ -84,13 +101,14 @@ Ret RegisterAudioPluginsScenario::updatePluginsRegistry() PluginScanResult result = scanPlugins(); - unregisterRemovedPlugins(result.missingPluginIds); - registerNewPlugins(result.newPluginPaths); + knownPluginsRegister()->setPluginsState(result.missingPluginIds, AudioPluginState::Missing); + knownPluginsRegister()->setPluginsState(result.rediscoveredPluginIds, AudioPluginState::Validated); + registerNewPlugins(result.newPluginPaths, /*validate*/ true); return knownPluginsRegister()->load(); } -void RegisterAudioPluginsScenario::registerNewPlugins(const io::paths_t& pluginPaths) +void RegisterAudioPluginsScenario::registerNewPlugins(const io::paths_t& pluginPaths, bool validate) { TRACEFUNC; @@ -98,11 +116,38 @@ void RegisterAudioPluginsScenario::registerNewPlugins(const io::paths_t& pluginP return; } - processPluginsRegistration(pluginPaths); + persistDiscoveredPlaceholders(pluginPaths); + + if (validate) { + processPluginsRegistration(pluginPaths); + } + knownPluginsRegister()->load(); } -Ret RegisterAudioPluginsScenario::unregisterRemovedPlugins(const audio::AudioResourceIdList& pluginIds) +void 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) { + knownPluginsRegister()->removePluginsAtPath(path); + + 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)); + } + knownPluginsRegister()->registerPlugins(placeholders); +} + +Ret RegisterAudioPluginsScenario::unregisterRemovedPlugins(const AudioResourceIdList& pluginIds) { TRACEFUNC; @@ -139,6 +184,10 @@ void RegisterAudioPluginsScenario::processPluginsRegistration(const io::paths_t& 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) { @@ -161,6 +210,12 @@ 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. + knownPluginsRegister()->removePluginsAtPath(pluginPath); + const IAudioPluginMetaReaderPtr reader = metaReader(pluginPath); if (!reader) { return make_ret(Err::UnknownPluginType); @@ -177,10 +232,9 @@ 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)); } @@ -196,11 +250,16 @@ 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. + knownPluginsRegister()->removePluginsAtPath(pluginPath); + 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 }); @@ -218,8 +277,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 8f61cede3d..699ebaf17c 100644 --- a/framework/audioplugins/internal/registeraudiopluginsscenario.h +++ b/framework/audioplugins/internal/registeraudiopluginsscenario.h @@ -53,16 +53,17 @@ class RegisterAudioPluginsScenario : public IRegisterAudioPluginsScenario, publi PluginScanResult scanPlugins(Progress* progress = nullptr) const override; Ret updatePluginsRegistry() override; - void registerNewPlugins(const io::paths_t& pluginPaths) override; - Ret unregisterRemovedPlugins(const audio::AudioResourceIdList& pluginIds) override; + void 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: + void 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 739da2852e..e939bd2746 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 @@ -43,8 +46,11 @@ class IRegisterAudioPluginsScenario : MODULE_CONTEXT_INTERFACE virtual PluginScanResult scanPlugins(Progress* progress = nullptr) const = 0; virtual Ret updatePluginsRegistry() = 0; - virtual void registerNewPlugins(const io::paths_t& pluginPaths) = 0; - virtual Ret unregisterRemovedPlugins(const audio::AudioResourceIdList& pluginIds) = 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. + virtual void 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 f5dbba7a48..3b3581e28f 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 deleted file mode 100644 index b4c880f89c..0000000000 --- a/framework/audioplugins/tests/audiopluginsutilstest.cpp +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-only - * MuseScore-CLA-applies - * - * MuseScore - * 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..f26a9f2e06 --- /dev/null +++ b/framework/audioplugins/tests/knownaudiopluginsmigrationregistertest.cpp @@ -0,0 +1,311 @@ +/* + * 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) and v1 -> v2 (enabled -> state). + // App-owned steps (e.g. v2 -> v3) are not pre-registered. + 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] requesting a v2 -> v3 migration that the framework does NOT own + JsonArray v2arr = makeArray("v2"); + + Ret ret23 = reg.migrate(2, 3, v2arr); + + // [THEN] fails with the missing-migrator error — apps must register v2 -> v3. + EXPECT_FALSE(ret23); + EXPECT_NE(ret23.text().find("missing migrator"), std::string::npos); + EXPECT_NE(ret23.text().find("2 -> 3"), std::string::npos); +} diff --git a/framework/audioplugins/tests/knownaudiopluginsregistertest.cpp b/framework/audioplugins/tests/knownaudiopluginsregistertest.cpp index c16adde91e..d3f811c145 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,117 @@ TEST_F(AudioPlugins_KnownAudioPluginsRegisterTest, PluginInfoList) actualPluginInfoList = m_knownPlugins->pluginInfoList(); EXPECT_FALSE(muse::contains(actualPluginInfoList, unregisteredPlugin)); } + +using AudioPlugins_KnownAudioPluginsRegisterDeathTest = AudioPlugins_KnownAudioPluginsRegisterTest; + +TEST_F(AudioPlugins_KnownAudioPluginsRegisterDeathTest, RegisterPlugins_RejectsSameIdSamePath) +{ + // 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 guards + // against it with IF_ASSERT_FAILED, which aborts under debug. The + // scenario side clears the path first via removePluginsAtPath; this + // test pins the register-level invariant 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 })); + + EXPECT_DEATH({ m_knownPlugins->registerPlugins({ info }); + }, "Assertion failed"); +} + +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"))); +} diff --git a/framework/audioplugins/tests/mocks/audiopluginmetareadermock.h b/framework/audioplugins/tests/mocks/audiopluginmetareadermock.h index b2b983d47c..50e852ac62 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 dc05c256f4..94bbc1c909 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 86fa8e7f35..4e8c5a3753 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 1218dbd6b0..33ba3bdb49 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,7 +79,7 @@ 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)); @@ -101,13 +100,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; } } @@ -143,7 +139,7 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry) AudioPluginInfo incompatiblePluginInfo; incompatiblePluginInfo.path = foundPluginPaths[4]; incompatiblePluginInfo.meta.id = io::filename(incompatiblePluginInfo.path).toStdString(); - incompatiblePluginInfo.enabled = false; + incompatiblePluginInfo.state = AudioPluginState::Error; incompatiblePluginInfo.errorCode = -1; // [GIVEN] Some plugins already exist in the register @@ -249,15 +245,14 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_NoNe } //! 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; }; @@ -285,21 +280,146 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_Unre .WillByDefault(Return(foundPluginPaths)); } - // [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())); + + EXPECT_CALL(*m_knownPlugins, setPluginsState(AudioResourceIdList {}, AudioPluginState::Validated)) .WillOnce(Return(make_ok())); + EXPECT_CALL(*m_knownPlugins, unregisterPlugins(_)) + .Times(0); + EXPECT_CALL(*m_knownPlugins, load()) .WillOnce(Return(muse::make_ok())); // [WHEN] Update registry Ret ret = m_scenario->updatePluginsRegistry(); - // [THEN] Successfully unregistered + // [THEN] Successfully transitioned + EXPECT_TRUE(ret); +} + +TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_RediscoverFormerlyMissing) +{ + 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] One Missing entry that gets reinstalled, 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)); + + ON_CALL(*m_knownPlugins, pluginInfoList(_)) + .WillByDefault(Return(knownPlugins)); + + // [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(foundPluginPaths)); + } + + // [THEN] Only AAA gets transitioned back to Validated + AudioResourceIdList rediscoveredIds { knownPlugins[0].meta.id }; + + EXPECT_CALL(*m_knownPlugins, setPluginsState(AudioResourceIdList {}, 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())); + + Ret ret = m_scenario->updatePluginsRegistry(); + EXPECT_TRUE(ret); +} + +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; + }; + + AudioPluginInfoList knownPlugins; + knownPlugins.push_back(createPluginInfo("/some/path/CRASHED.vst3", AudioPluginState::Discovered)); + + ON_CALL(*m_knownPlugins, pluginInfoList(_)) + .WillByDefault(Return(knownPlugins)); + + 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(foundPluginPaths)); + } + + // [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())); + + // [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())); + + // [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(2) + .WillRepeatedly(Return(make_ok())); + + Ret ret = m_scenario->updatePluginsRegistry(); EXPECT_TRUE(ret); } @@ -312,12 +432,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()); @@ -333,14 +453,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)); @@ -359,11 +485,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)); @@ -373,3 +506,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 + 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 + m_scenario->registerNewPlugins(paths, /*validate*/ true); +} diff --git a/framework/musesampler/internal/musesamplerresolver.cpp b/framework/musesampler/internal/musesamplerresolver.cpp index 72835b86ee..51877bcbb6 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 = 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 da800b9a1d..68ff6fe9a3 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/fx/vstfxprocessor.cpp b/framework/vst/internal/fx/vstfxprocessor.cpp index abe32e19f4..8b65aa20e5 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 395e19bee7..4e50e8c994 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 cf61ee40e3..cd2929d495 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 36ffd977fa..67feda1daa 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 ad576aff4a..74b29c8840 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,23 @@ 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 (!muse::audio::isResourceType(info.meta, muse::audio::AudioResourceType::VstPlugin) + || info.state != audioplugins::AudioPluginState::Validated) { + return false; + } + const String& categories = info.meta.attributeVal(muse::vst::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 403b8d0a4b..570cc4ca67 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 7a14a50b41..d48a5eb064 100644 --- a/framework/vst/internal/vstpluginmetareader.cpp +++ b/framework/vst/internal/vstpluginmetareader.cpp @@ -22,18 +22,20 @@ #include "vstpluginmetareader.h" +#include "audio/common/audiotypes.h" + #include "vsttypes.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 audio::AudioResourceType::VstPlugin; + return std::string(AUDIO_RESOURCE_TYPE_NAME); } bool VstPluginMetaReader::canReadMeta(const io::path_t& pluginPath) const @@ -56,12 +58,12 @@ RetVal VstPluginMetaReader::readMeta(const io::path_t& pl continue; } - muse::audio::AudioResourceMeta meta; + AudioResourceMeta meta; meta.id = io::completeBasename(pluginPath).toStdString(); - meta.type = muse::audio::AudioResourceType::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(); - meta.hasNativeEditorSupport = true; result.emplace_back(std::move(meta)); break; diff --git a/framework/vst/internal/vstpluginmetareader.h b/framework/vst/internal/vstpluginmetareader.h index ddc6a97785..ebf00800bf 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..7fb981ad92 --- /dev/null +++ b/framework/vst/vstpluginattrs.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 . + */ +#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"; + +static const String CATEGORIES_ATTRIBUTE(u"categories"); +} + +#endif // MUSE_VST_VSTPLUGINATTRS_H diff --git a/framework/vst/vsttypes.h b/framework/vst/vsttypes.h index 268f786cd9..27c34f01cb 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; @@ -81,6 +83,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" };