From 44bbe782a707057c4b9c3660fdccce8fbec159c7 Mon Sep 17 00:00:00 2001 From: trevor holt Date: Sat, 30 May 2026 12:31:56 -0500 Subject: [PATCH 1/2] Stabilize Muse Sounds reassignment startup --- muse | 2 +- src/macos_integration/CMakeLists.txt | 1 + src/playback/internal/playbackcontroller.cpp | 108 +++++- src/playback/internal/playbackcontroller.h | 3 + src/playback/iplaybackcontroller.h | 2 + src/playback/playbackmodule.cpp | 1 + .../qml/MuseScore/Playback/CMakeLists.txt | 3 + .../Playback/MuseSoundsReassignDialog.qml | 155 +++++++++ .../Playback/SoundProfilesDialog.qml | 38 ++- .../Playback/musesoundsreassignmodel.cpp | 316 ++++++++++++++++++ .../Playback/musesoundsreassignmodel.h | 84 +++++ .../MuseScore/Playback/soundprofilesmodel.cpp | 23 ++ .../MuseScore/Playback/soundprofilesmodel.h | 3 + .../tests/mocks/playbackcontrollermock.h | 2 + .../internal/ScoresPage/ScoresGridView.qml | 11 +- src/stubs/playback/playbackcontrollerstub.cpp | 9 + src/stubs/playback/playbackcontrollerstub.h | 2 + 17 files changed, 743 insertions(+), 20 deletions(-) create mode 100644 src/playback/qml/MuseScore/Playback/MuseSoundsReassignDialog.qml create mode 100644 src/playback/qml/MuseScore/Playback/musesoundsreassignmodel.cpp create mode 100644 src/playback/qml/MuseScore/Playback/musesoundsreassignmodel.h diff --git a/muse b/muse index 5246c265c45f6..b647aa8c5e9d4 160000 --- a/muse +++ b/muse @@ -1 +1 @@ -Subproject commit 5246c265c45f6227c8e35f41bc581d795e0e8465 +Subproject commit b647aa8c5e9d45efb5389d3db01935e5bd4a002c diff --git a/src/macos_integration/CMakeLists.txt b/src/macos_integration/CMakeLists.txt index b11102e6375ac..65fa60fb61df1 100644 --- a/src/macos_integration/CMakeLists.txt +++ b/src/macos_integration/CMakeLists.txt @@ -116,6 +116,7 @@ install(FILES ${QT_INSTALL_PLUGINS}/platforms/libqcocoa.dylib install(CODE " execute_process(COMMAND rm -f \"${the_appex_path}/Contents/MacOS/MuseScoreQuickLookPreviewExtension.emit-module.d\") execute_process(COMMAND rm -f \"${the_appex_path}/Contents/MacOS/MuseScoreQuickLookPreviewExtension.d\") + execute_process(COMMAND codesign --force --sign - --timestamp=none \"${the_appex_path}/Contents/MacOS/platforms/libqcocoa.dylib\") execute_process(COMMAND codesign --force --sign - --timestamp=none --options runtime --entitlements \"${CMAKE_CURRENT_SOURCE_DIR}/entitlements.plist\" \"${the_appex_path}\") ") diff --git a/src/playback/internal/playbackcontroller.cpp b/src/playback/internal/playbackcontroller.cpp index 54dd5ba15c4f4..e1f61932ddaba 100644 --- a/src/playback/internal/playbackcontroller.cpp +++ b/src/playback/internal/playbackcontroller.cpp @@ -34,6 +34,7 @@ #include "engraving/dom/factory.h" #include "audio/common/audioutils.h" +#include "audio/common/soundfonttypes.h" #include "audio/devtools/inputlag.h" #include "containers.h" @@ -84,6 +85,44 @@ static AudioOutputParams makeReverbOutputParams() return result; } +static AudioInputParams defaultBasicInputParams() +{ + static const AudioResourceId DEFAULT_SOUND_FONT_NAME = "MS Basic"; + static const AudioResourceMeta DEFAULT_AUDIO_RESOURCE_META = { + DEFAULT_SOUND_FONT_NAME, + "Fluid", + { + { PLAYBACK_SETUP_DATA_ATTRIBUTE, muse::mpe::GENERIC_SETUP_DATA_STRING }, + { synth::SOUNDFONT_NAME_ATTRIBUTE, muse::String::fromStdString(DEFAULT_SOUND_FONT_NAME) } + }, + AudioResourceType::FluidSoundfont, + false /*hasNativeEditor*/ + }; + + return { DEFAULT_AUDIO_RESOURCE_META, {} }; +} + +static bool needsProfileInputFallback(const AudioInputParams& params) +{ + if (!params.isValid()) { + return true; + } + + if (params.resourceMeta.type == AudioResourceType::MuseSamplerSoundPack + && params.resourceMeta.attributeVal(PLAYBACK_SETUP_DATA_ATTRIBUTE).isEmpty()) { + return true; + } + + return false; +} + +static bool shouldAvoidMuseSamplerInput(const AudioInputParams& params, const SoundProfileName& activeProfileName, + const SoundProfileName& museSoundsProfileName) +{ + return params.resourceMeta.type == AudioResourceType::MuseSamplerSoundPack + || activeProfileName == museSoundsProfileName; +} + static std::string resolveAuxTrackTitle(aux_channel_idx_t index, const AudioOutputParams& params, bool considerFx = true) { if (considerFx && params.fxChain.size() == 1) { @@ -1128,8 +1167,12 @@ void PlaybackController::doAddTrack(const InstrumentTrackId& instrumentTrackId, bool isMetronome = notationPlayback()->metronomeTrackId() == instrumentTrackId; - if (!inParams.isValid()) { - if (isMetronome) { + const bool useBasicProfile = isMetronome + || shouldAvoidMuseSamplerInput(inParams, audioSettings()->activeSoundProfile(), + configuration()->museSoundsProfileName()); + + if (needsProfileInputFallback(inParams) || useBasicProfile) { + if (useBasicProfile) { const SoundProfile& profile = profilesRepo()->profile(configuration()->basicSoundProfileName()); inParams = { profile.findResource(playbackData.setupData), {} }; } else { @@ -1138,6 +1181,10 @@ void PlaybackController::doAddTrack(const InstrumentTrackId& instrumentTrackId, } } + if (!inParams.isValid()) { + inParams = defaultBasicInputParams(); + } + if (!isMetronome && originParams.auxSends.empty()) { const muse::String& instrumentSoundId = inParams.resourceMeta.attributeVal(PLAYBACK_SETUP_DATA_ATTRIBUTE); AudioSourceType sourceType = inParams.isValid() ? inParams.type() : AudioSourceType::Fluid; @@ -1700,6 +1747,63 @@ void PlaybackController::applyProfile(const SoundProfileName& profileName) audioSettingsPtr->setActiveSoundProfile(profileName); } +bool PlaybackController::hasAvailableMuseSoundsReassignments() const +{ + return museSoundsReassignmentCandidateCount() > 0; +} + +void PlaybackController::reassignInstrumentsToAvailableMuseSounds() +{ + if (!hasAvailableMuseSoundsReassignments()) { + return; + } + + applyProfile(configuration()->museSoundsProfileName()); +} + +size_t PlaybackController::museSoundsReassignmentCandidateCount() const +{ + INotationPlaybackPtr nPlayback = notationPlayback(); + project::IProjectAudioSettingsPtr audioSettingsPtr = audioSettings(); + + if (!nPlayback || !audioSettingsPtr) { + return 0; + } + + const SoundProfile& museSoundsProfile = profilesRepo()->profile(configuration()->museSoundsProfileName()); + if (!museSoundsProfile.isValid()) { + return 0; + } + + const InstrumentTrackId& metronomeTrackId = nPlayback->metronomeTrackId(); + size_t result = 0; + + for (const auto& pair : m_instrumentTrackIdMap) { + const InstrumentTrackId& instrumentTrackId = pair.first; + if (instrumentTrackId == metronomeTrackId) { + continue; + } + + const mpe::PlaybackData& playbackData = nPlayback->trackPlaybackData(instrumentTrackId); + const AudioResourceMeta& museSoundsMatch = museSoundsProfile.findResource(playbackData.setupData); + if (!museSoundsMatch.isValid()) { + continue; + } + + const AudioInputParams& currentParams = audioSettingsPtr->trackInputParams(instrumentTrackId); + if (currentParams.resourceMeta.type == AudioResourceType::MuseSamplerSoundPack + && currentParams.resourceMeta.id == museSoundsMatch.id) { + continue; + } + + if (!currentParams.isValid() || currentParams.resourceMeta.type == AudioResourceType::FluidSoundfont) { + ++result; + } + } + + return result; +} + void PlaybackController::setNotation(notation::INotationPtr notation) { if (m_notation == notation) { diff --git a/src/playback/internal/playbackcontroller.h b/src/playback/internal/playbackcontroller.h index bfd8184f19519..2dad753d6f735 100644 --- a/src/playback/internal/playbackcontroller.h +++ b/src/playback/internal/playbackcontroller.h @@ -120,6 +120,8 @@ class PlaybackController : public IPlaybackController, public muse::actions::Act muse::Progress loadingProgress() const override; void applyProfile(const SoundProfileName& profileName) override; + bool hasAvailableMuseSoundsReassignments() const override; + void reassignInstrumentsToAvailableMuseSounds() override; void setNotation(notation::INotationPtr notation) override; void setMasterNotation(notation::IMasterNotationPtr masterNotation); @@ -194,6 +196,7 @@ class PlaybackController : public IPlaybackController, public muse::actions::Act void reloadPlaybackCache(); void openPlaybackSetupDialog(); + size_t museSoundsReassignmentCandidateCount() const; void addLoopBoundary(notation::LoopBoundaryType type); void addLoopBoundaryToTick(notation::LoopBoundaryType type, int tick); diff --git a/src/playback/iplaybackcontroller.h b/src/playback/iplaybackcontroller.h index b31d8ec35460e..24a0c14df18e3 100644 --- a/src/playback/iplaybackcontroller.h +++ b/src/playback/iplaybackcontroller.h @@ -113,6 +113,8 @@ class IPlaybackController : MODULE_CONTEXT_INTERFACE virtual muse::Progress loadingProgress() const = 0; virtual void applyProfile(const SoundProfileName& profileName) = 0; + virtual bool hasAvailableMuseSoundsReassignments() const = 0; + virtual void reassignInstrumentsToAvailableMuseSounds() = 0; virtual void setNotation(notation::INotationPtr notation) = 0; virtual void setIsExportingAudio(bool exporting) = 0; diff --git a/src/playback/playbackmodule.cpp b/src/playback/playbackmodule.cpp index d88c9822db449..3fc1536eebc2a 100644 --- a/src/playback/playbackmodule.cpp +++ b/src/playback/playbackmodule.cpp @@ -56,6 +56,7 @@ void PlaybackModule::resolveImports() auto ir = globalIoc()->resolve(mname); if (ir) { ir->registerQmlUri(Uri("musescore://playback/soundprofilesdialog"), "MuseScore.Playback", "SoundProfilesDialog"); + ir->registerQmlUri(Uri("musescore://playback/musesoundsreassigndialog"), "MuseScore.Playback", "MuseSoundsReassignDialog"); } } diff --git a/src/playback/qml/MuseScore/Playback/CMakeLists.txt b/src/playback/qml/MuseScore/Playback/CMakeLists.txt index 279583f143e4d..b8457711fb259 100644 --- a/src/playback/qml/MuseScore/Playback/CMakeLists.txt +++ b/src/playback/qml/MuseScore/Playback/CMakeLists.txt @@ -36,6 +36,8 @@ qt_add_qml_module(playback_qml mixerpanelcontextmenumodel.h mixerpanelmodel.cpp mixerpanelmodel.h + musesoundsreassignmodel.cpp + musesoundsreassignmodel.h msbasicpresetscategories.h notationregionsbeingprocessedmodel.cpp notationregionsbeingprocessedmodel.h @@ -75,6 +77,7 @@ qt_add_qml_module(playback_qml internal/VolumePressureMeter.qml internal/VolumeSlider.qml MixerPanel.qml + MuseSoundsReassignDialog.qml NotationRegionsBeingProcessedView.qml OnlineSoundsStatusView.qml PlaybackLoadingInfo.qml diff --git a/src/playback/qml/MuseScore/Playback/MuseSoundsReassignDialog.qml b/src/playback/qml/MuseScore/Playback/MuseSoundsReassignDialog.qml new file mode 100644 index 0000000000000..68ada4038aa91 --- /dev/null +++ b/src/playback/qml/MuseScore/Playback/MuseSoundsReassignDialog.qml @@ -0,0 +1,155 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + */ + +import QtQuick +import QtQuick.Layouts + +import Muse.Ui +import Muse.UiComponents +import MuseScore.Playback + +StyledDialogView { + id: root + + title: qsTrc("playback", "Reassign Muse Sounds") + + contentWidth: 920 + contentHeight: 520 + margins: 24 + modal: true + + Component.onCompleted: { + reassignModel.load() + } + + MuseSoundsReassignModel { + id: reassignModel + } + + ColumnLayout { + anchors.fill: parent + spacing: 16 + + StyledListView { + id: choicesView + + Layout.fillWidth: true + Layout.fillHeight: true + + model: reassignModel + spacing: 8 + + navigation.name: "MuseSoundsReassignChoices" + accessible.name: root.title + + delegate: ListItemBlank { + id: choiceItem + + required property string staffName + required property string currentSound + required property var candidateTitles + required property int selectedCandidateIndex + required property int candidateCount + required property int index + + height: 64 + + navigation.panel: choicesView.navigation + navigation.row: index + navigation.accessible.name: staffName + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 12 + anchors.rightMargin: 12 + anchors.topMargin: 8 + anchors.bottomMargin: 8 + spacing: 16 + + ColumnLayout { + Layout.fillWidth: true + Layout.preferredWidth: 300 + spacing: 2 + + StyledTextLabel { + Layout.fillWidth: true + text: choiceItem.staffName + font: ui.theme.bodyBoldFont + horizontalAlignment: Text.AlignLeft + } + + StyledTextLabel { + Layout.fillWidth: true + text: choiceItem.currentSound + opacity: 0.7 + horizontalAlignment: Text.AlignLeft + } + } + + StyledDropdown { + id: soundDropdown + + Layout.preferredWidth: 500 + Layout.alignment: Qt.AlignVCenter + height: 32 + + enabled: choiceItem.candidateCount > 0 + model: choiceItem.candidateTitles + currentIndex: choiceItem.selectedCandidateIndex + + navigation.name: "MuseSoundsChoice" + navigation.panel: choicesView.navigation + navigation.row: choiceItem.index + navigation.column: 1 + + onActivated: function(index, value) { + reassignModel.setSelectedCandidate(choiceItem.index, index) + } + } + } + } + } + + StyledTextLabel { + Layout.fillWidth: true + visible: !reassignModel.hasChoices + text: qsTrc("playback", "No matching Muse Sounds were found for this score.") + horizontalAlignment: Text.AlignHCenter + } + + ButtonBox { + Layout.alignment: Qt.AlignRight | Qt.AlignBottom + + buttons: [ ButtonBoxModel.Cancel ] + + navigationPanel.section: root.navigationSection + navigationPanel.order: 2 + + FlatButton { + text: qsTrc("global", "OK") + buttonRole: ButtonBoxModel.AcceptRole + buttonId: ButtonBoxModel.Ok + enabled: reassignModel.hasChoices + accentButton: true + + onClicked: { + reassignModel.apply() + root.hide() + } + } + + onStandardButtonClicked: function(buttonId) { + if (buttonId === ButtonBoxModel.Cancel) { + root.reject() + } + } + } + } +} diff --git a/src/playback/qml/MuseScore/Playback/SoundProfilesDialog.qml b/src/playback/qml/MuseScore/Playback/SoundProfilesDialog.qml index bf56ac241d791..e617b1730423f 100644 --- a/src/playback/qml/MuseScore/Playback/SoundProfilesDialog.qml +++ b/src/playback/qml/MuseScore/Playback/SoundProfilesDialog.qml @@ -224,10 +224,11 @@ StyledDialogView { } } - Row { - id: buttons + RowLayout { + id: footer - Layout.alignment: Qt.AlignRight | Qt.AlignBottom + Layout.fillWidth: true + Layout.alignment: Qt.AlignBottom spacing: 12 @@ -238,6 +239,29 @@ StyledDialogView { order: 3 } + Item { + Layout.fillWidth: true + } + + FlatButton { + height: 30 + width: 430 + + enabled: profilesListModel.canReassignInstrumentsToMuseSounds() + text: qsTrc("playback", "Reassign instruments to available Muse Sounds matches") + + navigation.panel: footer.navigationPanel + navigation.order: 1 + + onClicked: { + profilesListModel.openMuseSoundsReassignDialog() + } + } + + Item { + Layout.fillWidth: true + } + FlatButton { height: 30 width: 160 @@ -246,8 +270,8 @@ StyledDialogView { visible: profilesListModel.currentlySelectedProfile != profilesListModel.activeProfile text: qsTrc("playback", "Activate this profile") - navigation.panel: buttons.navigationPanel - navigation.order: 1 + navigation.panel: footer.navigationPanel + navigation.order: 2 onClicked: { profilesListModel.activeProfile = profilesListModel.currentlySelectedProfile @@ -260,8 +284,8 @@ StyledDialogView { text: qsTrc("global", "OK") - navigation.panel: buttons.navigationPanel - navigation.order: 2 + navigation.panel: footer.navigationPanel + navigation.order: 3 onClicked: { root.hide() diff --git a/src/playback/qml/MuseScore/Playback/musesoundsreassignmodel.cpp b/src/playback/qml/MuseScore/Playback/musesoundsreassignmodel.cpp new file mode 100644 index 0000000000000..8c5c31b4779d2 --- /dev/null +++ b/src/playback/qml/MuseScore/Playback/musesoundsreassignmodel.cpp @@ -0,0 +1,316 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + */ + +#include "musesoundsreassignmodel.h" + +#include +#include + +#include "project/inotationproject.h" +#include "notation/inotationparts.h" +#include "notation/inotationplayback.h" + +#include "engraving/dom/instrument.h" +#include "engraving/dom/part.h" + +#include "audio/common/audioutils.h" + +using namespace mu::playback; +using namespace mu::project; +using namespace mu::notation; +using namespace mu::engraving; +using namespace muse; +using namespace muse::audio; + +static QString cleanText(QString text) +{ + text = text.toLower(); + + for (QChar& ch : text) { + if (!ch.isLetterOrNumber()) { + ch = u' '; + } + } + + return text.simplified(); +} + +static QStringList tokensFrom(const QString& text) +{ + static const QSet ignored { + QStringLiteral("a"), QStringLiteral("an"), QStringLiteral("and"), QStringLiteral("for"), + QStringLiteral("in"), QStringLiteral("of"), QStringLiteral("the"), QStringLiteral("to"), + QStringLiteral("with"), QStringLiteral("solo"), QStringLiteral("primary"), + QStringLiteral("orchestral"), QStringLiteral("instrument") + }; + + QStringList result; + for (const QString& token : cleanText(text).split(u' ', Qt::SkipEmptyParts)) { + if (token.size() < 3 || ignored.contains(token)) { + continue; + } + + result.push_back(token); + } + + return result; +} + +static QString resourceTitle(const AudioResourceMeta& resource) +{ + QStringList parts; + + const QString vendor = resource.attributeVal(u"museVendorName").toQString(); + const QString pack = resource.attributeVal(u"musePack").toQString(); + const QString name = resource.attributeVal(u"museName").toQString(); + + if (!vendor.isEmpty()) { + parts << vendor; + } + + if (!pack.isEmpty() && pack != vendor) { + parts << pack; + } + + if (!name.isEmpty()) { + parts << name; + } else { + parts << QString::fromStdString(resource.id); + } + + return parts.join(u" / "); +} + +MuseSoundsReassignModel::MuseSoundsReassignModel(QObject* parent) + : QAbstractListModel(parent), muse::Contextable(muse::iocCtxForQmlObject(this)) +{ +} + +void MuseSoundsReassignModel::load() +{ + beginResetModel(); + m_choices.clear(); + endResetModel(); + emit choicesChanged(); + + playback()->availableInputResources() + .onResolve(this, [this](const AudioResourceMetaList& availableResources) { + buildChoices(availableResources); + }); +} + +void MuseSoundsReassignModel::setSelectedCandidate(int row, int candidateIndex) +{ + if (row < 0 || row >= static_cast(m_choices.size())) { + return; + } + + TrackChoice& choice = m_choices[row]; + if (candidateIndex < 0 || candidateIndex >= static_cast(choice.candidates.size())) { + return; + } + + if (choice.selectedCandidateIndex == candidateIndex) { + return; + } + + choice.selectedCandidateIndex = candidateIndex; + + QModelIndex modelIndex = index(row, 0); + emit dataChanged(modelIndex, modelIndex, { SelectedCandidateIndexRole }); +} + +void MuseSoundsReassignModel::apply() +{ + for (const TrackChoice& choice : m_choices) { + if (choice.selectedCandidateIndex < 0 || choice.selectedCandidateIndex >= static_cast(choice.candidates.size())) { + continue; + } + + AudioInputParams params; + params.resourceMeta = choice.candidates[choice.selectedCandidateIndex].resource; + playback()->setSourceParams(choice.audioTrackId, params); + } +} + +int MuseSoundsReassignModel::rowCount(const QModelIndex&) const +{ + return static_cast(m_choices.size()); +} + +QVariant MuseSoundsReassignModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(m_choices.size())) { + return QVariant(); + } + + const TrackChoice& choice = m_choices[index.row()]; + + switch (role) { + case StaffNameRole: + return choice.staffName; + case CurrentSoundRole: + return choice.currentSound; + case CandidateTitlesRole: + return choice.candidateTitles; + case SelectedCandidateIndexRole: + return choice.selectedCandidateIndex; + case CandidateCountRole: + return static_cast(choice.candidates.size()); + } + + return QVariant(); +} + +QHash MuseSoundsReassignModel::roleNames() const +{ + static const QHash roles { + { StaffNameRole, "staffName" }, + { CurrentSoundRole, "currentSound" }, + { CandidateTitlesRole, "candidateTitles" }, + { SelectedCandidateIndexRole, "selectedCandidateIndex" }, + { CandidateCountRole, "candidateCount" } + }; + + return roles; +} + +bool MuseSoundsReassignModel::hasChoices() const +{ + return !m_choices.empty(); +} + +void MuseSoundsReassignModel::buildChoices(const AudioResourceMetaList& availableResources) +{ + AudioResourceMetaList museResources; + for (const AudioResourceMeta& resource : availableResources) { + if (resource.type == AudioResourceType::MuseSamplerSoundPack + && resource.isValid() + && !resource.attributeVal(PLAYBACK_SETUP_DATA_ATTRIBUTE).isEmpty()) { + museResources.push_back(resource); + } + } + + INotationProjectPtr project = context()->currentProject(); + INotationPlaybackPtr notationPlayback = project ? project->masterNotation()->playback() : nullptr; + IProjectAudioSettingsPtr audioSettings = project ? project->audioSettings() : nullptr; + + if (!notationPlayback || !audioSettings || museResources.empty()) { + return; + } + + std::vector choices; + const InstrumentTrackId& metronomeTrackId = notationPlayback->metronomeTrackId(); + + for (const auto& pair : controller()->instrumentTrackIdMap()) { + const InstrumentTrackId& instrumentTrackId = pair.first; + if (instrumentTrackId == metronomeTrackId || notationPlayback->isChordSymbolsTrack(instrumentTrackId)) { + continue; + } + + const mpe::PlaybackData& playbackData = notationPlayback->trackPlaybackData(instrumentTrackId); + QString staff = trackName(instrumentTrackId); + std::vector candidates = candidatesForTrack(staff, playbackData.setupData, museResources); + if (candidates.empty()) { + continue; + } + + const AudioInputParams& currentParams = audioSettings->trackInputParams(instrumentTrackId); + + TrackChoice choice; + choice.instrumentTrackId = instrumentTrackId; + choice.audioTrackId = pair.second; + choice.staffName = staff; + choice.currentSound = audioSourceName(currentParams).toQString(); + choice.candidates = std::move(candidates); + + for (size_t i = 0; i < choice.candidates.size(); ++i) { + choice.candidateTitles << choice.candidates[i].title; + if (currentParams.resourceMeta.type == AudioResourceType::MuseSamplerSoundPack + && currentParams.resourceMeta.id == choice.candidates[i].resource.id) { + choice.selectedCandidateIndex = static_cast(i); + } + } + + choices.push_back(std::move(choice)); + } + + beginResetModel(); + m_choices = std::move(choices); + endResetModel(); + emit choicesChanged(); +} + +std::vector MuseSoundsReassignModel::candidatesForTrack( + const QString& staffName, const mpe::PlaybackSetupData& setupData, const AudioResourceMetaList& museResources) const +{ + QString setupText = setupData.toString().toQString(); + QStringList queryTokens = tokensFrom(staffName + u" " + setupText); + + std::vector result; + + for (const AudioResourceMeta& resource : museResources) { + QString title = resourceTitle(resource); + QString haystack = title + u" " + + resource.attributeVal(PLAYBACK_SETUP_DATA_ATTRIBUTE).toQString() + u" " + + resource.attributeVal(u"museCategory").toQString(); + + QString cleanHaystack = cleanText(haystack); + + int score = 0; + if (resource.attributeVal(PLAYBACK_SETUP_DATA_ATTRIBUTE).toQString() == setupText) { + score += 100; + } + + for (const QString& token : queryTokens) { + if (cleanHaystack.contains(token)) { + score += 10; + } + } + + if (score == 0) { + continue; + } + + result.push_back(Candidate { resource, title, score }); + } + + std::sort(result.begin(), result.end(), [](const Candidate& left, const Candidate& right) { + if (left.score != right.score) { + return left.score > right.score; + } + + return left.title.localeAwareCompare(right.title) < 0; + }); + + if (result.size() > 20) { + result.resize(20); + } + + return result; +} + +QString MuseSoundsReassignModel::trackName(const InstrumentTrackId& instrumentTrackId) const +{ + INotationProjectPtr project = context()->currentProject(); + INotationPartsPtr parts = project ? project->masterNotation()->parts() : nullptr; + + const Part* part = parts ? parts->part(instrumentTrackId.partId) : nullptr; + if (!part) { + return QString(); + } + + const Instrument* instrument = part->instrumentById(instrumentTrackId.instrumentId); + if (instrument && instrument != part->instrument()) { + return instrument->trackName().toQString(); + } + + return part->partName().toQString(); +} diff --git a/src/playback/qml/MuseScore/Playback/musesoundsreassignmodel.h b/src/playback/qml/MuseScore/Playback/musesoundsreassignmodel.h new file mode 100644 index 0000000000000..48ea6e54e5460 --- /dev/null +++ b/src/playback/qml/MuseScore/Playback/musesoundsreassignmodel.h @@ -0,0 +1,84 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + */ + +#pragma once + +#include +#include +#include + +#include "async/asyncable.h" +#include "modularity/ioc.h" +#include "context/iglobalcontext.h" + +#include "audio/main/iplayback.h" +#include "iplaybackcontroller.h" + +namespace mu::playback { +class MuseSoundsReassignModel : public QAbstractListModel, public muse::Contextable, public muse::async::Asyncable +{ + Q_OBJECT + Q_PROPERTY(bool hasChoices READ hasChoices NOTIFY choicesChanged) + + QML_ELEMENT + + muse::ContextInject context = { this }; + muse::ContextInject controller = { this }; + muse::ContextInject playback = { this }; + +public: + explicit MuseSoundsReassignModel(QObject* parent = nullptr); + + Q_INVOKABLE void load(); + Q_INVOKABLE void setSelectedCandidate(int row, int candidateIndex); + Q_INVOKABLE void apply(); + + int rowCount(const QModelIndex& parent) const override; + QVariant data(const QModelIndex& index, int role) const override; + QHash roleNames() const override; + + bool hasChoices() const; + +signals: + void choicesChanged(); + +private: + enum Roles { + StaffNameRole = Qt::UserRole + 1, + CurrentSoundRole, + CandidateTitlesRole, + SelectedCandidateIndexRole, + CandidateCountRole + }; + + struct Candidate { + muse::audio::AudioResourceMeta resource; + QString title; + int score = 0; + }; + + struct TrackChoice { + engraving::InstrumentTrackId instrumentTrackId; + muse::audio::TrackId audioTrackId = muse::audio::INVALID_TRACK_ID; + QString staffName; + QString currentSound; + QStringList candidateTitles; + std::vector candidates; + int selectedCandidateIndex = 0; + }; + + void buildChoices(const muse::audio::AudioResourceMetaList& availableResources); + std::vector candidatesForTrack(const QString& staffName, const muse::mpe::PlaybackSetupData& setupData, + const muse::audio::AudioResourceMetaList& museResources) const; + QString trackName(const engraving::InstrumentTrackId& instrumentTrackId) const; + + std::vector m_choices; +}; +} diff --git a/src/playback/qml/MuseScore/Playback/soundprofilesmodel.cpp b/src/playback/qml/MuseScore/Playback/soundprofilesmodel.cpp index 323de574461cc..c8932a9749253 100644 --- a/src/playback/qml/MuseScore/Playback/soundprofilesmodel.cpp +++ b/src/playback/qml/MuseScore/Playback/soundprofilesmodel.cpp @@ -60,6 +60,29 @@ void SoundProfilesModel::init() endResetModel(); } +bool SoundProfilesModel::canReassignInstrumentsToMuseSounds() const +{ + return context()->currentProject() != nullptr + && controller()->hasAvailableMuseSoundsReassignments(); +} + +void SoundProfilesModel::reassignInstrumentsToMuseSounds() +{ + controller()->reassignInstrumentsToAvailableMuseSounds(); + + if (INotationProjectPtr project = context()->currentProject()) { + m_activeProfile = project->audioSettings()->activeSoundProfile().toQString(); + m_currentlySelectedProfile = m_activeProfile; + emit activeProfileChanged(); + emit currentlySelectedProfileChanged(); + } +} + +void SoundProfilesModel::openMuseSoundsReassignDialog() +{ + interactive()->open("musescore://playback/musesoundsreassigndialog"); +} + int SoundProfilesModel::rowCount(const QModelIndex& /*parent*/) const { return static_cast(m_profiles.size()); diff --git a/src/playback/qml/MuseScore/Playback/soundprofilesmodel.h b/src/playback/qml/MuseScore/Playback/soundprofilesmodel.h index 7321e0659f014..b149645731d35 100644 --- a/src/playback/qml/MuseScore/Playback/soundprofilesmodel.h +++ b/src/playback/qml/MuseScore/Playback/soundprofilesmodel.h @@ -57,6 +57,9 @@ class SoundProfilesModel : public QAbstractListModel, public muse::Contextable explicit SoundProfilesModel(QObject* parent = nullptr); Q_INVOKABLE void init(); + Q_INVOKABLE bool canReassignInstrumentsToMuseSounds() const; + Q_INVOKABLE void reassignInstrumentsToMuseSounds(); + Q_INVOKABLE void openMuseSoundsReassignDialog(); int rowCount(const QModelIndex& parent) const override; QVariant data(const QModelIndex& index, int role) const override; diff --git a/src/playback/tests/mocks/playbackcontrollermock.h b/src/playback/tests/mocks/playbackcontrollermock.h index 0172b1899d141..af6aff365ab55 100644 --- a/src/playback/tests/mocks/playbackcontrollermock.h +++ b/src/playback/tests/mocks/playbackcontrollermock.h @@ -89,6 +89,8 @@ class PlaybackControllerMock : public IPlaybackController MOCK_METHOD(muse::Progress, loadingProgress, (), (const, override)); MOCK_METHOD(void, applyProfile, (const SoundProfileName&), (override)); + MOCK_METHOD(bool, hasAvailableMuseSoundsReassignments, (), (const, override)); + MOCK_METHOD(void, reassignInstrumentsToAvailableMuseSounds, (), (override)); MOCK_METHOD(void, setNotation, (notation::INotationPtr), (override)); MOCK_METHOD(void, setIsExportingAudio, (bool), (override)); diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresGridView.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresGridView.qml index 84c0dcbb1fffd..60c2917603953 100644 --- a/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresGridView.qml +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresGridView.qml @@ -120,16 +120,7 @@ Item { flickableDirection: Flickable.VerticalFlick - ScrollBar.vertical: StyledScrollBar { - parent: root - - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right - - visible: view.contentHeight > view.height - z: 2 - } + ScrollBar.vertical: null model: itemTypeFilterModel diff --git a/src/stubs/playback/playbackcontrollerstub.cpp b/src/stubs/playback/playbackcontrollerstub.cpp index f50b2817623e2..206288b7ceee7 100644 --- a/src/stubs/playback/playbackcontrollerstub.cpp +++ b/src/stubs/playback/playbackcontrollerstub.cpp @@ -196,6 +196,15 @@ void PlaybackControllerStub::applyProfile(const SoundProfileName&) { } +bool PlaybackControllerStub::hasAvailableMuseSoundsReassignments() const +{ + return false; +} + +void PlaybackControllerStub::reassignInstrumentsToAvailableMuseSounds() +{ +} + void PlaybackControllerStub::setNotation(notation::INotationPtr) { } diff --git a/src/stubs/playback/playbackcontrollerstub.h b/src/stubs/playback/playbackcontrollerstub.h index e5472b81d799b..4b43341dc9512 100644 --- a/src/stubs/playback/playbackcontrollerstub.h +++ b/src/stubs/playback/playbackcontrollerstub.h @@ -85,6 +85,8 @@ class PlaybackControllerStub : public IPlaybackController muse::Progress loadingProgress() const override; void applyProfile(const SoundProfileName& profileName) override; + bool hasAvailableMuseSoundsReassignments() const override; + void reassignInstrumentsToAvailableMuseSounds() override; void setNotation(notation::INotationPtr notation) override; void setIsExportingAudio(bool exporting) override; From 2e86a014a3e89ebfd5e4e4aa3ce977b1ada1769c Mon Sep 17 00:00:00 2001 From: trevor holt Date: Sat, 30 May 2026 16:52:16 -0500 Subject: [PATCH 2/2] Stabilize Muse Sounds reassignment mixer --- muse | 2 +- .../qml/MuseScore/Playback/MixerPanel.qml | 24 ++++++++++--- .../Playback/musesoundsreassignmodel.cpp | 34 +++++++++++++++---- 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/muse b/muse index b647aa8c5e9d4..67441dc1d46eb 160000 --- a/muse +++ b/muse @@ -1 +1 @@ -Subproject commit b647aa8c5e9d45efb5389d3db01935e5bd4a002c +Subproject commit 67441dc1d46ebdaf9a79b92e003b002439eb19cc diff --git a/src/playback/qml/MuseScore/Playback/MixerPanel.qml b/src/playback/qml/MuseScore/Playback/MixerPanel.qml index 0cbe7bccad710..86ddfeeb29e0f 100644 --- a/src/playback/qml/MuseScore/Playback/MixerPanel.qml +++ b/src/playback/qml/MuseScore/Playback/MixerPanel.qml @@ -140,9 +140,10 @@ ColumnLayout { ScrollBar.horizontal: horizontalScrollBar - ScrollBar.vertical: StyledScrollBar { policy: ScrollBar.AlwaysOn } + ScrollBar.vertical: null property bool completed: false + property bool pendingPositionViewAtEnd: false property bool resourcePickingActive: soundSection.resourcePickingActive || fxSection.resourcePickingActive function positionViewAtEnd() { @@ -150,15 +151,28 @@ ColumnLayout { return } - if (flickable.contentY == flickable.contentHeight) { + let endY = Math.max(0, flickable.contentHeight - flickable.height) + if (Math.abs(flickable.contentY - endY) < 1) { return } - flickable.contentY = flickable.contentHeight - flickable.height + flickable.contentY = endY + } + + function schedulePositionViewAtEnd() { + if (!flickable.completed || flickable.pendingPositionViewAtEnd) { + return + } + + flickable.pendingPositionViewAtEnd = true + Qt.callLater(function() { + flickable.pendingPositionViewAtEnd = false + flickable.positionViewAtEnd() + }) } onContentHeightChanged: { - flickable.positionViewAtEnd() + flickable.schedulePositionViewAtEnd() } Component.onCompleted: { @@ -360,6 +374,6 @@ ColumnLayout { } onHeightChanged: { - flickable.positionViewAtEnd() + flickable.schedulePositionViewAtEnd() } } diff --git a/src/playback/qml/MuseScore/Playback/musesoundsreassignmodel.cpp b/src/playback/qml/MuseScore/Playback/musesoundsreassignmodel.cpp index 8c5c31b4779d2..e0bf2ec95b0ed 100644 --- a/src/playback/qml/MuseScore/Playback/musesoundsreassignmodel.cpp +++ b/src/playback/qml/MuseScore/Playback/musesoundsreassignmodel.cpp @@ -29,6 +29,9 @@ using namespace mu::engraving; using namespace muse; using namespace muse::audio; +static const muse::String MUSICXML_SOUND_ATTRIBUTE(u"musicXmlSound"); +static const muse::String MUSE_UID_ATTRIBUTE(u"museUID"); + static QString cleanText(QString text) { text = text.toLower(); @@ -88,6 +91,13 @@ static QString resourceTitle(const AudioResourceMeta& resource) return parts.join(u" / "); } +static bool isUsableMuseSamplerResource(const AudioResourceMeta& resource) +{ + return resource.type == AudioResourceType::MuseSamplerSoundPack + && resource.isValid() + && !resource.attributeVal(MUSE_UID_ATTRIBUTE).isEmpty(); +} + MuseSoundsReassignModel::MuseSoundsReassignModel(QObject* parent) : QAbstractListModel(parent), muse::Contextable(muse::iocCtxForQmlObject(this)) { @@ -130,6 +140,10 @@ void MuseSoundsReassignModel::setSelectedCandidate(int row, int candidateIndex) void MuseSoundsReassignModel::apply() { for (const TrackChoice& choice : m_choices) { + if (choice.audioTrackId == INVALID_TRACK_ID) { + continue; + } + if (choice.selectedCandidateIndex < 0 || choice.selectedCandidateIndex >= static_cast(choice.candidates.size())) { continue; } @@ -191,9 +205,7 @@ void MuseSoundsReassignModel::buildChoices(const AudioResourceMetaList& availabl { AudioResourceMetaList museResources; for (const AudioResourceMeta& resource : availableResources) { - if (resource.type == AudioResourceType::MuseSamplerSoundPack - && resource.isValid() - && !resource.attributeVal(PLAYBACK_SETUP_DATA_ATTRIBUTE).isEmpty()) { + if (isUsableMuseSamplerResource(resource)) { museResources.push_back(resource); } } @@ -252,23 +264,33 @@ std::vector MuseSoundsReassignModel::candida const QString& staffName, const mpe::PlaybackSetupData& setupData, const AudioResourceMetaList& museResources) const { QString setupText = setupData.toString().toQString(); - QStringList queryTokens = tokensFrom(staffName + u" " + setupText); + QString musicXmlSound = setupData.musicXmlSoundId.has_value() + ? QString::fromStdString(setupData.musicXmlSoundId.value()) + : QString(); + QStringList queryTokens = tokensFrom(staffName + u" " + setupText + u" " + musicXmlSound); std::vector result; for (const AudioResourceMeta& resource : museResources) { QString title = resourceTitle(resource); + QString resourceSetupText = resource.attributeVal(PLAYBACK_SETUP_DATA_ATTRIBUTE).toQString(); + QString resourceMusicXmlSound = resource.attributeVal(MUSICXML_SOUND_ATTRIBUTE).toQString(); QString haystack = title + u" " - + resource.attributeVal(PLAYBACK_SETUP_DATA_ATTRIBUTE).toQString() + u" " + + resourceSetupText + u" " + + resourceMusicXmlSound + u" " + resource.attributeVal(u"museCategory").toQString(); QString cleanHaystack = cleanText(haystack); int score = 0; - if (resource.attributeVal(PLAYBACK_SETUP_DATA_ATTRIBUTE).toQString() == setupText) { + if (!setupText.isEmpty() && resourceSetupText == setupText) { score += 100; } + if (!musicXmlSound.isEmpty() && resourceMusicXmlSound == musicXmlSound) { + score += 90; + } + for (const QString& token : queryTokens) { if (cleanHaystack.contains(token)) { score += 10;