diff --git a/SetupConfigure.cmake b/SetupConfigure.cmake index 0bdc7093ade4d..c9335275ae6b8 100644 --- a/SetupConfigure.cmake +++ b/SetupConfigure.cmake @@ -148,6 +148,7 @@ if(BUILD_CONFIGURATION STREQUAL "APP-WEB") set(MUSE_MODULE_ACCESSIBILITY OFF) set(MUSE_MODULE_DOCKWINDOW OFF) set(MUSE_MODULE_MIDI OFF) + set(MUSE_MODULE_MIDIREMOTE OFF) set(MUSE_MODULE_MUSESAMPLER OFF) set(MUSE_MODULE_NETWORK OFF) set(MUSE_MODULE_VST OFF) @@ -212,6 +213,7 @@ if(BUILD_CONFIGURATION STREQUAL "VTEST") set(MUSE_MODULE_LANGUAGES OFF) set(MUSE_MODULE_LEARN OFF) set(MUSE_MODULE_MIDI OFF) + set(MUSE_MODULE_MIDIREMOTE OFF) set(MUSE_MODULE_MPE OFF) set(MUSE_MODULE_MULTIWINDOWS OFF) set(MUSE_MODULE_MUSESAMPLER OFF) @@ -280,6 +282,7 @@ if(BUILD_CONFIGURATION STREQUAL "UTEST") set(MUSE_MODULE_INTERACTIVE OFF) set(MUSE_MODULE_LEARN OFF) set(MUSE_MODULE_MIDI OFF) + set(MUSE_MODULE_MIDIREMOTE OFF) set(MUSE_MODULE_MPE_QML OFF) set(MUSE_MODULE_MULTIWINDOWS OFF) set(MUSE_MODULE_MUSESAMPLER OFF) diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index b5f1231afe94d..d8c751fef85b3 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -131,6 +131,7 @@ add_to_link_if_exists(muse::interactive) add_to_link_if_exists(muse::languages) add_to_link_if_exists(muse::learn) add_to_link_if_exists(muse::midi) +add_to_link_if_exists(muse::midiremote) add_to_link_if_exists(muse::mpe) add_to_link_if_exists(muse::multiwindows) add_to_link_if_exists(muse::musesampler) diff --git a/src/app/appfactory.cpp b/src/app/appfactory.cpp index 2085dbe2e9da3..b764f18e59dfb 100644 --- a/src/app/appfactory.cpp +++ b/src/app/appfactory.cpp @@ -56,6 +56,12 @@ #include "framework/stubs/midi/midistubmodule.h" #endif +#ifdef MUSE_MODULE_MIDIREMOTE +#include "framework/midiremote/midiremotemodule.h" +#else +#include "framework/stubs/midiremote/midiremotestubmodule.h" +#endif + #ifdef MUSE_MODULE_MPE #include "framework/mpe/mpemodule.h" #else @@ -298,6 +304,7 @@ std::shared_ptr AppFactory::newGuiApp(const CmdOptions& opti app->addModule(new muse::interactive::InteractiveModule()); #endif app->addModule(new muse::midi::MidiModule()); + app->addModule(new muse::midiremote::MidiRemoteModule()); app->addModule(new muse::mpe::MpeModule()); #ifdef MUSE_MODULE_MUSESAMPLER diff --git a/src/framework/CMakeLists.txt b/src/framework/CMakeLists.txt index ba071d31ad4df..23ecba0f14d14 100644 --- a/src/framework/CMakeLists.txt +++ b/src/framework/CMakeLists.txt @@ -77,6 +77,10 @@ if (MUSE_MODULE_MIDI) add_subdirectory(midi) endif() +if (MUSE_MODULE_MIDIREMOTE) + add_subdirectory(midiremote) +endif() + if (MUSE_MODULE_MPE) add_subdirectory(mpe) endif() diff --git a/src/framework/audio/common/audiotypes.h b/src/framework/audio/common/audiotypes.h index aa5650c32a50d..c3699570f372e 100644 --- a/src/framework/audio/common/audiotypes.h +++ b/src/framework/audio/common/audiotypes.h @@ -257,7 +257,26 @@ enum class AudioFxCategory { FxRestoration, FxReverb, FxSurround, - FxTools + FxTools, + FxOther, +}; + +static const std::unordered_map AUDIO_FX_CATEGORY_TO_STRING_MAP { + { AudioFxCategory::FxEqualizer, u"EQ" }, + { AudioFxCategory::FxAnalyzer, u"Analyzer" }, + { AudioFxCategory::FxDelay, u"Delay" }, + { AudioFxCategory::FxDistortion, u"Distortion" }, + { AudioFxCategory::FxDynamics, u"Dynamics" }, + { AudioFxCategory::FxFilter, u"Filter" }, + { AudioFxCategory::FxGenerator, u"Generator" }, + { AudioFxCategory::FxMastering, u"Mastering" }, + { AudioFxCategory::FxModulation, u"Modulation" }, + { AudioFxCategory::FxPitchShift, u"Pitch Shift" }, + { AudioFxCategory::FxRestoration, u"Restoration" }, + { AudioFxCategory::FxReverb, u"Reverb" }, + { AudioFxCategory::FxSurround, u"Surround" }, + { AudioFxCategory::FxTools, u"Tools" }, + { AudioFxCategory::FxOther, u"Fx" }, }; using AudioFxCategories = std::set; diff --git a/src/framework/audio/common/audioutils.h b/src/framework/audio/common/audioutils.h index 87039140e6b1e..b876a7e878409 100644 --- a/src/framework/audio/common/audioutils.h +++ b/src/framework/audio/common/audioutils.h @@ -20,8 +20,7 @@ * along with this program. If not, see . */ -#ifndef MUSE_AUDIO_AUDIOUTILS_H -#define MUSE_AUDIO_AUDIOUTILS_H +#pragma once #include "audiotypes.h" #include "soundfonttypes.h" @@ -92,6 +91,18 @@ inline String audioSourceCategoryName(const AudioInputParams& params) return String::fromStdString(params.resourceMeta.id); } +inline AudioFxCategories audioFxCategoriesFromString(const String& str) +{ + StringList list = str.split('|'); + + AudioFxCategories result; + for (const String& name : list) { + result.insert(muse::key(AUDIO_FX_CATEGORY_TO_STRING_MAP, name, AudioFxCategory::FxOther)); + } + + return result; +} + inline bool isOnlineAudioResource(const AudioResourceMeta& meta) { const String& attr = meta.attributeVal(u"isOnline"); @@ -119,5 +130,3 @@ inline samples_t minSamplesToReserve(RenderMode mode) return 1024; } } - -#endif // MUSE_AUDIO_AUDIOUTILS_H diff --git a/src/framework/audio/engine/ifxprocessor.h b/src/framework/audio/engine/ifxprocessor.h index 966376f2043d5..2d95b74dc28cb 100644 --- a/src/framework/audio/engine/ifxprocessor.h +++ b/src/framework/audio/engine/ifxprocessor.h @@ -19,8 +19,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef MUSE_AUDIO_IAUDIOPROCESSOR_H -#define MUSE_AUDIO_IAUDIOPROCESSOR_H + +#pragma once #include @@ -33,17 +33,21 @@ class IFxProcessor virtual ~IFxProcessor() = default; virtual AudioFxType type() const = 0; + virtual const AudioFxParams& params() const = 0; virtual async::Channel paramsChanged() const = 0; + virtual void setOutputSpec(const OutputSpec& spec) = 0; virtual bool active() const = 0; virtual void setActive(bool active) = 0; + virtual void setPlaying(bool playing) = 0; + + virtual bool shouldProcessDuringSilence() const = 0; + virtual void process(float* buffer, unsigned int sampleCount, muse::audio::msecs_t playbackPosition = 0) = 0; }; using IFxProcessorPtr = std::shared_ptr; } - -#endif // MUSE_AUDIO_IAUDIOPROCESSOR_H diff --git a/src/framework/audio/engine/ifxresolver.h b/src/framework/audio/engine/ifxresolver.h index cb4ee34ca8b6d..889ec85bf9dcd 100644 --- a/src/framework/audio/engine/ifxresolver.h +++ b/src/framework/audio/engine/ifxresolver.h @@ -46,6 +46,7 @@ class IFxResolver : MODULE_GLOBAL_INTERFACE const OutputSpec& outputSpec) = 0; virtual std::vector resolveMasterFxList(const AudioFxChain& fxChain, const OutputSpec& outputSpec) = 0; virtual AudioResourceMetaList resolveResources() const = 0; + virtual void refresh() = 0; virtual void clearAllFx() = 0; }; @@ -55,6 +56,7 @@ class IFxResolver : MODULE_GLOBAL_INTERFACE virtual std::vector resolveFxList(const TrackId trackId, const AudioFxChain& fxChain, const OutputSpec& outputSpec) = 0; virtual AudioResourceMetaList resolveAvailableResources() const = 0; + virtual void registerResolver(const AudioFxType type, IResolverPtr resolver) = 0; virtual void clearAllFx() = 0; }; diff --git a/src/framework/audio/engine/internal/fx/abstractfxresolver.cpp b/src/framework/audio/engine/internal/fx/abstractfxresolver.cpp index 58dddebde7485..e3d4fa207276f 100644 --- a/src/framework/audio/engine/internal/fx/abstractfxresolver.cpp +++ b/src/framework/audio/engine/internal/fx/abstractfxresolver.cpp @@ -38,13 +38,7 @@ std::vector AbstractFxResolver::resolveFxList(const TrackId tra FxMap& fxMap = m_tracksFxMap[trackId]; updateTrackFxMap(fxMap, trackId, fxChain, outputSpec); - std::vector result; - - for (const auto& pair : fxMap) { - result.emplace_back(pair.second); - } - - return result; + return muse::values(fxMap); } std::vector AbstractFxResolver::resolveMasterFxList(const AudioFxChain& fxChain, const OutputSpec& outputSpec) @@ -56,13 +50,7 @@ std::vector AbstractFxResolver::resolveMasterFxList(const Audio updateMasterFxMap(fxChain, outputSpec); - std::vector result; - - for (const auto& pair : m_masterFxMap) { - result.emplace_back(pair.second); - } - - return result; + return muse::values(m_masterFxMap); } void AbstractFxResolver::refresh() diff --git a/src/framework/audio/engine/internal/fx/abstractfxresolver.h b/src/framework/audio/engine/internal/fx/abstractfxresolver.h index 72556207e1178..dc67156166516 100644 --- a/src/framework/audio/engine/internal/fx/abstractfxresolver.h +++ b/src/framework/audio/engine/internal/fx/abstractfxresolver.h @@ -34,6 +34,7 @@ class AbstractFxResolver : public IFxResolver::IResolver public: std::vector resolveFxList(const TrackId trackId, const AudioFxChain& fxChain, const OutputSpec& outputSpec) override; std::vector resolveMasterFxList(const AudioFxChain& fxChain, const OutputSpec& outputSpec) override; + void refresh() override; void clearAllFx() override; diff --git a/src/framework/audio/engine/internal/fx/reverb/reverbprocessor.cpp b/src/framework/audio/engine/internal/fx/reverb/reverbprocessor.cpp index e7152738d2a96..8725a3eb6c847 100644 --- a/src/framework/audio/engine/internal/fx/reverb/reverbprocessor.cpp +++ b/src/framework/audio/engine/internal/fx/reverb/reverbprocessor.cpp @@ -333,6 +333,15 @@ void ReverbProcessor::setActive(bool active) m_params.active = active; } +void ReverbProcessor::setPlaying(bool) +{ +} + +bool ReverbProcessor::shouldProcessDuringSilence() const +{ + return false; +} + void ReverbProcessor::process(float* buffer, unsigned int sampleCount, muse::audio::msecs_t) { if (m_processor._blockSize != static_cast(sampleCount)) { diff --git a/src/framework/audio/engine/internal/fx/reverb/reverbprocessor.h b/src/framework/audio/engine/internal/fx/reverb/reverbprocessor.h index 5e861c98407ba..b79704c1f391f 100644 --- a/src/framework/audio/engine/internal/fx/reverb/reverbprocessor.h +++ b/src/framework/audio/engine/internal/fx/reverb/reverbprocessor.h @@ -47,6 +47,10 @@ class ReverbProcessor : public IFxProcessor bool active() const override; void setActive(bool active) override; + void setPlaying(bool playing) override; + + bool shouldProcessDuringSilence() const override; + void process(float* buffer, unsigned int sampleCount, muse::audio::msecs_t playbackPosition = 0) override; private: diff --git a/src/framework/audio/engine/internal/mixer.cpp b/src/framework/audio/engine/internal/mixer.cpp index 97cf5b0cce252..1604a77cd62cf 100644 --- a/src/framework/audio/engine/internal/mixer.cpp +++ b/src/framework/audio/engine/internal/mixer.cpp @@ -220,7 +220,7 @@ samples_t Mixer::process(float* outBuffer, samples_t samplesPerChannel) size_t outBufferSize = samplesPerChannel * m_outputSpec.audioChannelCount; std::fill(outBuffer, outBuffer + outBufferSize, 0.f); - if (m_isIdle && m_tracksToProcessWhenIdle.empty() && m_isSilence) { + if (m_isIdle && m_tracksToProcessWhenIdle.empty() && (m_isSilence && !m_shouldProcessMasterFxDuringSilence)) { notifyNoAudioSignal(); return 0; } @@ -248,20 +248,15 @@ samples_t Mixer::process(float* outBuffer, samples_t samplesPerChannel) writeTrackToAuxBuffers(trackBuffer.data(), channel->outputParams().auxSends, samplesPerChannel); } - if (m_masterParams.muted || samplesPerChannel == 0 || m_isSilence) { + if (m_masterParams.muted || samplesPerChannel == 0 || (m_isSilence && !m_shouldProcessMasterFxDuringSilence)) { notifyNoAudioSignal(); return 0; } processAuxChannels(outBuffer, samplesPerChannel); - - for (IFxProcessorPtr& fxProcessor : m_masterFxProcessors) { - if (fxProcessor->active()) { - fxProcessor->process(outBuffer, samplesPerChannel, playbackPosition()); - } - } - + processMasterFx(outBuffer, samplesPerChannel); completeOutput(outBuffer, samplesPerChannel); + notifyAboutAudioSignalChanges(); return samplesPerChannel; @@ -364,6 +359,10 @@ void Mixer::setIsActive(bool arg) aux.channel->setIsActive(arg); } } + + for (IFxProcessorPtr& fx : m_masterFxProcessors) { + fx->setPlaying(arg); + } } void Mixer::addClock(IClockPtr clock) @@ -397,13 +396,18 @@ void Mixer::setMasterOutputParams(const AudioOutputParams& params) m_masterFxProcessors.clear(); m_masterFxProcessors = fxResolver()->resolveMasterFxList(params.fxChain, m_outputSpec); + m_shouldProcessMasterFxDuringSilence = false; for (IFxProcessorPtr& fx : m_masterFxProcessors) { fx->setOutputSpec(m_outputSpec); fx->paramsChanged().onReceive(this, [this](const AudioFxParams& fxParams) { m_masterParams.fxChain.insert_or_assign(fxParams.chainOrder, fxParams); m_masterOutputParamsChanged.send(m_masterParams); - }); + }, async::Asyncable::Mode::SetReplace); + + if (!m_shouldProcessMasterFxDuringSilence && fx->shouldProcessDuringSilence()) { + m_shouldProcessMasterFxDuringSilence = true; + } } AudioOutputParams resultParams = params; @@ -556,6 +560,15 @@ void Mixer::processAuxChannels(float* buffer, samples_t samplesPerChannel) } } +void Mixer::processMasterFx(float* buffer, samples_t samplesPerChannel) +{ + for (IFxProcessorPtr& fxProcessor : m_masterFxProcessors) { + if (fxProcessor->active()) { + fxProcessor->process(buffer, samplesPerChannel, playbackPosition()); + } + } +} + void Mixer::completeOutput(float* buffer, samples_t samplesPerChannel) { IF_ASSERT_FAILED(buffer) { diff --git a/src/framework/audio/engine/internal/mixer.h b/src/framework/audio/engine/internal/mixer.h index 779930e29060f..539a8fd0c97bf 100644 --- a/src/framework/audio/engine/internal/mixer.h +++ b/src/framework/audio/engine/internal/mixer.h @@ -74,9 +74,11 @@ class Mixer : public AbstractAudioSource, public IGetPlaybackPosition, public as // IAudioSource void setOutputSpec(const OutputSpec& spec) override; unsigned int audioChannelsCount() const override; - samples_t process(float* outBuffer, samples_t samplesPerChannel) override; + void setIsActive(bool arg) override; + samples_t process(float* outBuffer, samples_t samplesPerChannel) override; + private: using TracksData = std::map >; @@ -87,6 +89,7 @@ class Mixer : public AbstractAudioSource, public IGetPlaybackPosition, public as void prepareAuxBuffers(size_t outBufferSize); void writeTrackToAuxBuffers(const float* trackBuffer, const AuxSendsParams& auxSends, samples_t samplesPerChannel); void processAuxChannels(float* buffer, samples_t samplesPerChannel); + void processMasterFx(float* buffer, samples_t samplesPerChannel); void completeOutput(float* buffer, samples_t samplesPerChannel); bool useMultithreading() const; @@ -119,6 +122,7 @@ class Mixer : public AbstractAudioSource, public IGetPlaybackPosition, public as mutable AudioSignalsNotifier m_audioSignalNotifier; bool m_isSilence = false; + bool m_shouldProcessMasterFxDuringSilence = false; bool m_isIdle = false; }; diff --git a/src/framework/audio/engine/internal/mixerchannel.cpp b/src/framework/audio/engine/internal/mixerchannel.cpp index 41608087beb7b..baab473b90107 100644 --- a/src/framework/audio/engine/internal/mixerchannel.cpp +++ b/src/framework/audio/engine/internal/mixerchannel.cpp @@ -154,6 +154,10 @@ void MixerChannel::setIsActive(bool arg) if (m_audioSource) { m_audioSource->setIsActive(arg); } + + for (IFxProcessorPtr& fx : m_fxProcessors) { + fx->setPlaying(arg); + } } void MixerChannel::setOutputSpec(const OutputSpec& spec) diff --git a/src/framework/autobot/internal/autobotinteractive.cpp b/src/framework/autobot/internal/autobotinteractive.cpp index 3344b16739a16..22516a38ec605 100644 --- a/src/framework/autobot/internal/autobotinteractive.cpp +++ b/src/framework/autobot/internal/autobotinteractive.cpp @@ -178,19 +178,24 @@ void AutobotInteractive::raise(const UriQuery& uri) m_real->raise(uri); } -void AutobotInteractive::close(const UriQuery& uri) +async::Promise AutobotInteractive::close(const UriQuery& uri) { - m_real->close(uri); + return m_real->close(uri); } -void AutobotInteractive::close(const Uri& uri) +async::Promise AutobotInteractive::close(const Uri& uri) { - m_real->close(uri); + return m_real->close(uri); } -void AutobotInteractive::closeAllDialogs() +Ret AutobotInteractive::closeSync(const UriQuery& uri) { - m_real->closeAllDialogs(); + return m_real->closeSync(uri); +} + +Ret AutobotInteractive::closeAllDialogsSync() +{ + return m_real->closeAllDialogsSync(); } ValCh AutobotInteractive::currentUri() const diff --git a/src/framework/autobot/internal/autobotinteractive.h b/src/framework/autobot/internal/autobotinteractive.h index ea4322472e67e..4a97f92cba688 100644 --- a/src/framework/autobot/internal/autobotinteractive.h +++ b/src/framework/autobot/internal/autobotinteractive.h @@ -96,9 +96,10 @@ class AutobotInteractive : public IInteractive void raise(const UriQuery& uri) override; - void close(const UriQuery& uri) override; - void close(const Uri& uri) override; - void closeAllDialogs() override; + async::Promise close(const UriQuery& uri) override; + async::Promise close(const Uri& uri) override; + Ret closeSync(const UriQuery& uri) override; + Ret closeAllDialogsSync() override; // state ValCh currentUri() const override; diff --git a/src/framework/cmake/MuseDeclareOptions.cmake b/src/framework/cmake/MuseDeclareOptions.cmake index 8ed2295e3f072..22e48088220a4 100644 --- a/src/framework/cmake/MuseDeclareOptions.cmake +++ b/src/framework/cmake/MuseDeclareOptions.cmake @@ -64,6 +64,7 @@ option(MUSE_MODULE_INTERACTIVE_SYNC_SUPPORTED "Sync interactive supported" ON) declare_muse_module_opt(LANGUAGES ON) declare_muse_module_opt(LEARN ON) declare_muse_module_opt(MIDI ON) +declare_muse_module_opt(MIDIREMOTE ON) declare_muse_module_opt(MPE ON) declare_muse_module_opt(MULTIWINDOWS ON) diff --git a/src/framework/cmake/MuseModules.cmake b/src/framework/cmake/MuseModules.cmake index bab3feaabd952..518e1e7ae6251 100644 --- a/src/framework/cmake/MuseModules.cmake +++ b/src/framework/cmake/MuseModules.cmake @@ -15,6 +15,7 @@ set(MUSE_FRAMEWORK_MODULES LANGUAGES LEARN MIDI + MIDIREMOTE MPE MULTIWINDOWS MUSESAMPLER diff --git a/src/framework/cmake/muse_framework_config.h.in b/src/framework/cmake/muse_framework_config.h.in index 5a3889037fdf8..d965619b974dc 100644 --- a/src/framework/cmake/muse_framework_config.h.in +++ b/src/framework/cmake/muse_framework_config.h.in @@ -115,6 +115,10 @@ #cmakedefine MUSE_MODULE_MIDI_TESTS 1 #cmakedefine MUSE_MODULE_MIDI_API 1 +#cmakedefine MUSE_MODULE_MIDIREMOTE 1 +#cmakedefine MUSE_MODULE_MIDIREMOTE_TESTS 1 +#cmakedefine MUSE_MODULE_MIDIREMOTE_API 1 + #cmakedefine MUSE_MODULE_MPE 1 #cmakedefine MUSE_MODULE_MPE_TESTS 1 #cmakedefine MUSE_MODULE_MPE_API 1 diff --git a/src/framework/extensions/qml/Muse/Extensions/ExtensionViewerDialog.qml b/src/framework/extensions/qml/Muse/Extensions/ExtensionViewerDialog.qml index 13e8c9951e1bd..edbcf37d16d9a 100644 --- a/src/framework/extensions/qml/Muse/Extensions/ExtensionViewerDialog.qml +++ b/src/framework/extensions/qml/Muse/Extensions/ExtensionViewerDialog.qml @@ -34,7 +34,7 @@ StyledDialogView { contentWidth: viewer.width contentHeight: viewer.height - alwaysOnTop: true + nativeChildWindow: true ExtensionViewer { id: viewer diff --git a/src/framework/interactive/iinteractive.h b/src/framework/interactive/iinteractive.h index d99f9986178f0..af3c6268a5d73 100644 --- a/src/framework/interactive/iinteractive.h +++ b/src/framework/interactive/iinteractive.h @@ -242,9 +242,10 @@ class IInteractive : MODULE_CONTEXT_INTERFACE virtual void raise(const UriQuery& uri) = 0; - virtual void close(const UriQuery& uri) = 0; - virtual void close(const Uri& uri) = 0; - virtual void closeAllDialogs() = 0; + virtual async::Promise close(const UriQuery& uri) = 0; + virtual async::Promise close(const Uri& uri) = 0; + virtual Ret closeSync(const UriQuery& uri) = 0; + virtual Ret closeAllDialogsSync() = 0; // state virtual ValCh currentUri() const = 0; diff --git a/src/framework/interactive/internal/interactive.cpp b/src/framework/interactive/internal/interactive.cpp index f7236dbdc0903..b99785f27636b 100644 --- a/src/framework/interactive/internal/interactive.cpp +++ b/src/framework/interactive/internal/interactive.cpp @@ -742,54 +742,107 @@ void Interactive::raise(const UriQuery& uri) } } -void Interactive::close(const UriQuery& uri) +Promise Interactive::close(const UriQuery& uri) { - for (const ObjectInfo& obj : allOpenObjects()) { - if (obj.query == uri) { - closeObject(obj); - } - } + std::vector objs = collectOpenObjects([&uri](const ObjectInfo& obj) { + return obj.query == uri; + }); + + return closeObjects(objs); } -void Interactive::close(const Uri& uri) +Promise Interactive::close(const Uri& uri) { - for (const ObjectInfo& obj : allOpenObjects()) { - if (obj.query.uri() == uri) { - closeObject(obj); + std::vector objs = collectOpenObjects([&uri](const ObjectInfo& obj) { + return obj.query.uri() == uri; + }); + + return closeObjects(objs); +} + +Ret Interactive::closeSync(const UriQuery& uri) +{ + std::vector objs = collectOpenObjects([&uri](const ObjectInfo& obj) { + return obj.query == uri; + }); + + return closeObjectsSync(objs); +} + +Ret Interactive::closeAllDialogsSync() +{ + std::vector objs = collectOpenObjects([this](const ObjectInfo& obj) { + if (muse::diagnostics::isDiagnosticsUri(obj.query.uri())) { + return false; } - } + ContainerMeta meta = uriRegister()->meta(obj.query.uri()); + return meta.type == ContainerMeta::QWidgetDialog || meta.type == ContainerMeta::QmlDialog; + }); + + return closeObjectsSync(objs); } -void Interactive::closeAllDialogs() +Promise Interactive::closeObjects(const std::vector& objs) { - for (const ObjectInfo& objectInfo: allOpenObjects()) { - UriQuery uriQuery = objectInfo.query; - if (muse::diagnostics::isDiagnosticsUri(uriQuery.uri())) { - continue; + return async::make_promise([this, objs](auto resolve, auto) { + if (objs.empty()) { + return resolve(make_ok()); } - ContainerMeta openMeta = uriRegister()->meta(uriQuery.uri()); - if (openMeta.type == ContainerMeta::QWidgetDialog || openMeta.type == ContainerMeta::QmlDialog) { - closeObject(objectInfo); + + auto count = std::make_shared(objs.size()); + auto ret = std::make_shared(make_ok()); + + for (const ObjectInfo& obj : objs) { + const QString objectId = obj.objectId.toString(); + m_onClosedFuncs.emplace(objectId, [this, objectId, count, ret, resolve](const Ret& funcRet) { + if (!funcRet) { + *ret = funcRet; + } + + if (--(*count) == 0) { + (void)resolve(*ret); + } + + muse::remove(m_onClosedFuncs, objectId); + }); + + ContainerMeta openMeta = uriRegister()->meta(obj.query.uri()); + switch (openMeta.type) { + case ContainerMeta::QWidgetDialog: { + if (auto window = dynamic_cast(obj.window)) { + window->close(); + } + } break; + case ContainerMeta::QmlDialog: + closeQml(obj.objectId); + break; + case ContainerMeta::PrimaryPage: + case ContainerMeta::Undefined: + break; + } } - } + + return Promise::dummy_result(); + }); } -void Interactive::closeObject(const ObjectInfo& obj) +Ret Interactive::closeObjectsSync(const std::vector& objs) { - ContainerMeta openMeta = uriRegister()->meta(obj.query.uri()); - switch (openMeta.type) { - case ContainerMeta::QWidgetDialog: { - if (auto window = dynamic_cast(obj.window)) { - window->close(); - } - } break; - case ContainerMeta::QmlDialog: - closeQml(obj.objectId); - break; - case ContainerMeta::PrimaryPage: - case ContainerMeta::Undefined: - break; + if (objs.empty()) { + return make_ok(); } + + QEventLoop loop; + Ret ret = make_ok(); + + closeObjects(objs).onResolve(this, [&loop, &ret](const Ret& closeRet) { + ret = closeRet; + loop.quit(); + }); + + loop.exec(); + + return ret; } void Interactive::fillExtData(QmlLaunchData* data, const UriQuery& q, const QVariantMap& params_) const @@ -1172,6 +1225,13 @@ void Interactive::onClose(const QString& objectId, const QVariant& jsrv) if (inStack) { notifyAboutCurrentUriChanged(); } + + Async::call(this, [this, objectId, rv]() { + auto onClosedIt = m_onClosedFuncs.find(objectId); + if (onClosedIt != m_onClosedFuncs.end()) { + onClosedIt->second(rv.ret); + } + }); } std::vector Interactive::allOpenObjects() const @@ -1184,6 +1244,18 @@ std::vector Interactive::allOpenObjects() const return result; } +std::vector Interactive::collectOpenObjects(std::function accepted) const +{ + std::vector result; + for (const ObjectInfo& obj : allOpenObjects()) { + if (accepted(obj)) { + result.push_back(obj); + } + } + + return result; +} + void Interactive::notifyAboutCurrentUriChanged() { m_currentUriChanged.send(currentUri().val); diff --git a/src/framework/interactive/internal/interactive.h b/src/framework/interactive/internal/interactive.h index aacfd8c379da6..1c0d36a36242b 100644 --- a/src/framework/interactive/internal/interactive.h +++ b/src/framework/interactive/internal/interactive.h @@ -115,9 +115,10 @@ class Interactive : public QObject, public IInteractive, public IInteractiveProv void raise(const UriQuery& uri) override; - void close(const UriQuery& uri) override; - void close(const Uri& uri) override; - void closeAllDialogs() override; + async::Promise close(const UriQuery& uri) override; + async::Promise close(const Uri& uri) override; + Ret closeSync(const UriQuery& uri) override; + Ret closeAllDialogsSync() override; // state ValCh currentUri() const override; @@ -172,12 +173,14 @@ class Interactive : public QObject, public IInteractive, public IInteractiveProv RetVal openWidgetDialog(const Uri& uri, const QVariantMap& params); RetVal openQml(const Uri& uri, const QVariantMap& params); - void closeObject(const ObjectInfo& obj); + async::Promise closeObjects(const std::vector& objs); + Ret closeObjectsSync(const std::vector& objs); void closeQml(const QVariant& objectId); void raiseQml(const QVariant& objectId); std::vector allOpenObjects() const; + std::vector collectOpenObjects(std::function accepted) const; void notifyAboutCurrentUriChanged(); void notifyAboutCurrentUriWillBeChanged(); @@ -207,6 +210,8 @@ class Interactive : public QObject, public IInteractive, public IInteractiveProv async::Channel m_closeRequested; async::Channel m_raiseRequested; + std::map > m_onClosedFuncs; + bool m_isSelectColorOpened = false; }; } diff --git a/src/framework/interactive/tests/mocks/interactivemock.h b/src/framework/interactive/tests/mocks/interactivemock.h index dcf5f683160df..1a11d43d370ca 100644 --- a/src/framework/interactive/tests/mocks/interactivemock.h +++ b/src/framework/interactive/tests/mocks/interactivemock.h @@ -76,9 +76,10 @@ class InteractiveMock : public IInteractive MOCK_METHOD(void, raise, (const UriQuery&), (override)); - MOCK_METHOD(void, close, (const UriQuery&), (override)); - MOCK_METHOD(void, close, (const Uri&), (override)); - MOCK_METHOD(void, closeAllDialogs, (), (override)); + MOCK_METHOD(async::Promise, close, (const UriQuery&), (override)); + MOCK_METHOD(async::Promise, close, (const Uri&), (override)); + MOCK_METHOD(Ret, closeSync, (const UriQuery&), (override)); + MOCK_METHOD(Ret, closeAllDialogsSync, (), (override)); MOCK_METHOD(ValCh, currentUri, (), (const, override)); MOCK_METHOD(RetVal, isCurrentUriDialog, (), (const, override)); diff --git a/src/framework/midi/CMakeLists.txt b/src/framework/midi/CMakeLists.txt index 9a384d66c5a7c..54a939dd469c2 100644 --- a/src/framework/midi/CMakeLists.txt +++ b/src/framework/midi/CMakeLists.txt @@ -72,4 +72,4 @@ elseif (OS_IS_LIN OR OS_IS_FBSD) find_package(ALSA REQUIRED) target_include_directories(muse_midi PRIVATE ${ALSA_INCLUDE_DIRS}) target_link_libraries(muse_midi PRIVATE ${ALSA_LIBRARIES} pthread) -endif() +endif() \ No newline at end of file diff --git a/src/framework/midi/internal/platform/win/winmidiinport.cpp b/src/framework/midi/internal/platform/win/winmidiinport.cpp index 29324809ff3ec..734be16bcdea5 100644 --- a/src/framework/midi/internal/platform/win/winmidiinport.cpp +++ b/src/framework/midi/internal/platform/win/winmidiinport.cpp @@ -37,6 +37,8 @@ struct muse::midi::WinMidiInPort::Win { HMIDIIN midiIn; int deviceID = -1; + MIDIHDR header; + std::vector buffer; }; using namespace muse; @@ -125,9 +127,8 @@ async::Notification WinMidiInPort::availableDevicesChanged() const static void CALLBACK process(HMIDIIN hMidiIn, UINT wMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2) { - UNUSED(hMidiIn); - WinMidiInPort* self = reinterpret_cast(dwInstance); + switch (wMsg) { case MIM_OPEN: case MIM_CLOSE: @@ -135,16 +136,30 @@ static void CALLBACK process(HMIDIIN hMidiIn, UINT wMsg, DWORD_PTR dwInstance, D case MIM_DATA: self->doProcess(static_cast(dwParam1), static_cast(dwParam2)); break; + case MIM_LONGDATA: { + MIDIHDR* hdr = reinterpret_cast(dwParam1); + uint8_t* data = reinterpret_cast(hdr->lpData); + self->doProcessLongData(data, hdr->dwBytesRecorded, static_cast(dwParam2)); + midiInAddBuffer(hMidiIn, hdr, sizeof(MIDIHDR)); + } break; default: NOT_IMPLEMENTED << wMsg; } } -void WinMidiInPort::doProcess(uint32_t message, tick_t timing) +void WinMidiInPort::doProcess(uint32_t message, tick_t tick) { auto e = Event::fromMidi10Package(message).toMIDI20(); if (e) { - m_eventReceived.send(timing, e); + m_eventReceived.send(tick, e); + } +} + +void WinMidiInPort::doProcessLongData(uint8_t* data, size_t size, tick_t tick) +{ + std::vector events = Event::fromMidi10SysExBytes(data, size); + for (const Event& e : events) { + m_eventReceived.send(tick, e); } } @@ -238,7 +253,15 @@ Ret WinMidiInPort::run() return true; } + m_win->buffer.clear(); + m_win->buffer.resize(1024, 0); + m_win->header.lpData = reinterpret_cast(m_win->buffer.data()); + m_win->header.dwBufferLength = static_cast(m_win->buffer.size()); + + midiInPrepareHeader(m_win->midiIn, &m_win->header, sizeof(MIDIHDR)); + midiInAddBuffer(m_win->midiIn, &m_win->header, sizeof(MIDIHDR)); midiInStart(m_win->midiIn); + m_running = true; return Ret(true); diff --git a/src/framework/midi/internal/platform/win/winmidiinport.h b/src/framework/midi/internal/platform/win/winmidiinport.h index 0ef3c8a6d80b7..73de088963856 100644 --- a/src/framework/midi/internal/platform/win/winmidiinport.h +++ b/src/framework/midi/internal/platform/win/winmidiinport.h @@ -51,7 +51,8 @@ class WinMidiInPort : public IMidiInPort, public async::Asyncable async::Channel eventReceived() const override; // internal; - void doProcess(uint32_t message, tick_t timing); + void doProcess(uint32_t message, tick_t tick); + void doProcessLongData(uint8_t* data, size_t size, tick_t tick); private: Ret run(); diff --git a/src/framework/midi/midievent.h b/src/framework/midi/midievent.h index 2ade9c6312a67..ddaf33dd12b82 100644 --- a/src/framework/midi/midievent.h +++ b/src/framework/midi/midievent.h @@ -171,6 +171,74 @@ struct Event { return 0; } + static std::vector fromMidi10SysExBytes(const uint8_t* data, size_t size) + { + if (!data || size == 0) { + return {}; + } + + // Remove F0/F7 if present + if (size >= 2 && data[0] == 0xF0 && data[size - 1] == 0xF7) { + data += 1; + size -= 2; + } + + std::vector result; + size_t pos = 0; + + while (pos < size) { + const size_t chunk = std::min(6, size - pos); + std::array words; + + uint32_t status; + if (pos == 0 && chunk == size) { + status = 0; // COMPLETE + } else if (pos == 0) { + status = 1; // START + } else if (pos + chunk >= size) { + status = 3; // END + } else { + status = 2; // CONTINUE + } + + uint32_t w0 = 0; + + // MessageType = SysEx + w0 |= (static_cast(MessageType::SystemExclusiveData) << 28); + + // Group = 0 + w0 |= (0 << 24); + + // Status + w0 |= (status << 20); + + // Byte count + w0 |= (static_cast(chunk) << 16); + + // First 2 bytes + if (chunk >= 1) { + w0 |= data[pos + 0] << 8; + } + if (chunk >= 2) { + w0 |= data[pos + 1]; + } + + // Remaining bytes (up to 4) + uint32_t w1 = 0; + for (size_t i = 0; i < chunk - 2; ++i) { + w1 |= data[pos + 2 + i] << (24 - i * 8); + } + + words[0] = w0; + words[1] = w1; + + result.emplace_back(words); + pos += chunk; + } + + return result; + } + static Event fromMidi20Words(const uint32_t* data, size_t count) { Event e; diff --git a/src/framework/midiremote/CMakeLists.txt b/src/framework/midiremote/CMakeLists.txt new file mode 100644 index 0000000000000..975ca2ab70727 --- /dev/null +++ b/src/framework/midiremote/CMakeLists.txt @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: GPL-3.0-only +# MuseScore-Studio-CLA-applies +# +# MuseScore Studio +# Music Composition & Notation +# +# Copyright (C) 2026 MuseScore Limited +# +# 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 . + +muse_create_module(muse_midiremote ALIAS muse::midiremote) + +target_sources(muse_midiremote PRIVATE + midiremotemodule.cpp + midiremotemodule.h + mmc.cpp + mmc.h + midiremotetypes.h + imidiremoteconfiguration.h + imidiremote.h + + internal/midiremoteconfiguration.cpp + internal/midiremoteconfiguration.h + internal/midiremote.cpp + internal/midiremote.h +) + +if (MUSE_MODULE_MIDIREMOTE_QML) + add_subdirectory(qml/Muse/MidiRemote) +endif() + +if (MUSE_MODULE_MIDIREMOTE_TESTS) + add_subdirectory(tests) +endif() diff --git a/src/framework/shortcuts/imidiremote.h b/src/framework/midiremote/imidiremote.h similarity index 88% rename from src/framework/shortcuts/imidiremote.h rename to src/framework/midiremote/imidiremote.h index 832b51495a5f3..951f641aff2c2 100644 --- a/src/framework/shortcuts/imidiremote.h +++ b/src/framework/midiremote/imidiremote.h @@ -19,15 +19,16 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef MUSE_SHORTCUTS_IMIDIREMOTE_H -#define MUSE_SHORTCUTS_IMIDIREMOTE_H + +#pragma once #include "modularity/imoduleinterface.h" -#include "midi/miditypes.h" + +#include "async/notification.h" +#include "midiremotetypes.h" #include "types/ret.h" -#include "shortcutstypes.h" -namespace muse::shortcuts { +namespace muse::midiremote { class IMidiRemote : MODULE_CONTEXT_INTERFACE { INTERFACE_ID(IMidiRemote) @@ -49,5 +50,3 @@ class IMidiRemote : MODULE_CONTEXT_INTERFACE virtual Ret process(const muse::midi::Event& ev) = 0; }; } - -#endif // MUSE_SHORTCUTS_IMIDIREMOTE_H diff --git a/src/framework/midiremote/imidiremoteconfiguration.h b/src/framework/midiremote/imidiremoteconfiguration.h new file mode 100644 index 0000000000000..da841ae222572 --- /dev/null +++ b/src/framework/midiremote/imidiremoteconfiguration.h @@ -0,0 +1,43 @@ +/* + * 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 "modularity/imoduleinterface.h" +#include "io/path.h" +#include "types/retval.h" + +namespace muse::midiremote { +class IMidiRemoteConfiguration : MODULE_GLOBAL_INTERFACE +{ + INTERFACE_ID(IMidiRemoteConfiguration) + +public: + virtual ~IMidiRemoteConfiguration() = default; + + virtual io::path_t midiMappingUserAppDataPath() const = 0; + + virtual bool advanceToNextNoteOnKeyRelease() const = 0; + virtual void setAdvanceToNextNoteOnKeyRelease(bool value) = 0; + virtual muse::async::Channel advanceToNextNoteOnKeyReleaseChanged() const = 0; +}; +} diff --git a/src/framework/shortcuts/internal/midiremote.cpp b/src/framework/midiremote/internal/midiremote.cpp similarity index 83% rename from src/framework/shortcuts/internal/midiremote.cpp rename to src/framework/midiremote/internal/midiremote.cpp index 711bb6eae7fbb..9f91b11a01cce 100644 --- a/src/framework/shortcuts/internal/midiremote.cpp +++ b/src/framework/midiremote/internal/midiremote.cpp @@ -22,17 +22,20 @@ #include "midiremote.h" +#include "global/types/secs.h" #include "global/io/buffer.h" #include "global/serialization/xmlstreamreader.h" #include "global/serialization/xmlstreamwriter.h" #include "multiwindows/resourcelockguard.h" +#include "actions/actiontypes.h" + #include "log.h" using namespace muse; -using namespace muse::shortcuts; using namespace muse::midi; +using namespace muse::midiremote; static constexpr std::string_view MIDIMAPPING_TAG("MidiMapping"); static constexpr std::string_view EVENT_TAG("Event"); @@ -114,6 +117,15 @@ Ret MidiRemote::process(const Event& ev) return Ret(Ret::Code::Undefined); } + if (ev.messageType() == Event::MessageType::SystemExclusiveData) { + const std::optional msg = m_mmcParser.process(ev); + if (msg.has_value()) { + processMMC(msg.value()); + } + + return make_ret(Ret::Code::Ok); + } + RemoteEvent event = remoteEventFromMidiEvent(ev); for (const MidiControlsMapping& midiMapping : m_midiMappings) { @@ -230,17 +242,19 @@ bool MidiRemote::needIgnoreEvent(const Event& event) const return true; } - if (event.opcode() != Event::Opcode::NoteOn && event.opcode() != Event::Opcode::NoteOff - && event.opcode() != Event::Opcode::ControlChange) { - return true; + if (event.messageType() == Event::MessageType::SystemExclusiveData) { + return false; } - static const QList releaseOps { - Event::Opcode::NoteOff - }; + const Event::Opcode opcode = event.opcode(); + if (opcode != Event::Opcode::NoteOn + && opcode != Event::Opcode::NoteOff + && opcode != Event::Opcode::ControlChange) { + return true; + } - bool release = releaseOps.contains(event.opcode()) - || (event.opcode() == Event::Opcode::ControlChange && event.data() == 0); + bool release = opcode == Event::Opcode::NoteOff + || (opcode == Event::Opcode::ControlChange && event.data() == 0); if (release) { bool advanceToNextNoteOnKeyRelease = configuration()->advanceToNextNoteOnKeyRelease(); @@ -268,3 +282,25 @@ RemoteEvent MidiRemote::remoteEvent(const std::string& action) const return RemoteEvent(); } + +void MidiRemote::processMMC(const MMCMessage& msg) +{ + switch (msg.command) { + case MMCCommand::Play: + dispatcher()->dispatch("play"); + break; + case MMCCommand::Pause: + dispatcher()->dispatch("pause"); + break; + case MMCCommand::Stop: + dispatcher()->dispatch("stop"); + break; + case MMCCommand::Locate: { + const std::optional pos = MMCParser::locateToSeconds(msg); + if (pos.has_value()) { + dispatcher()->dispatch("rewind", actions::ActionData::make_arg1(pos.value())); + } + } break; + default: break; + } +} diff --git a/src/framework/shortcuts/internal/midiremote.h b/src/framework/midiremote/internal/midiremote.h similarity index 89% rename from src/framework/shortcuts/internal/midiremote.h rename to src/framework/midiremote/internal/midiremote.h index 2a7a32e25d28f..130545d83dc48 100644 --- a/src/framework/shortcuts/internal/midiremote.h +++ b/src/framework/midiremote/internal/midiremote.h @@ -22,28 +22,28 @@ #pragma once +#include "midiremote/imidiremote.h" #include "async/asyncable.h" - #include "modularity/ioc.h" + +#include "midiremote/imidiremoteconfiguration.h" #include "io/ifilesystem.h" #include "actions/iactionsdispatcher.h" #include "multiwindows/imultiwindowsprovider.h" -#include "ishortcutsconfiguration.h" -#include "shortcutstypes.h" -#include "../imidiremote.h" +#include "midiremote/mmc.h" namespace muse { class XmlStreamReader; class XmlStreamWriter; } -namespace muse::shortcuts { +namespace muse::midiremote { class MidiRemote : public IMidiRemote, public Contextable, public async::Asyncable { + GlobalInject configuration; GlobalInject fileSystem; GlobalInject multiwindowsProvider; - GlobalInject configuration; ContextInject dispatcher = { this }; public: @@ -77,9 +77,13 @@ class MidiRemote : public IMidiRemote, public Contextable, public async::Asyncab RemoteEvent remoteEvent(const std::string& action) const; + void processMMC(const midiremote::MMCMessage& msg); + bool m_isSettingMode = false; MidiMappingList m_midiMappings; async::Notification m_midiMappingsChanged; + + midiremote::MMCParser m_mmcParser; }; } diff --git a/src/framework/midiremote/internal/midiremoteconfiguration.cpp b/src/framework/midiremote/internal/midiremoteconfiguration.cpp new file mode 100644 index 0000000000000..db2148c79260c --- /dev/null +++ b/src/framework/midiremote/internal/midiremoteconfiguration.cpp @@ -0,0 +1,59 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2021 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 "midiremoteconfiguration.h" +#include "global/settings.h" + +#include "log.h" + +using namespace muse; +using namespace muse::midiremote; + +static const Settings::Key ADVANCE_TO_NEXT_NOTE_ON_KEY_RELEASE("midiremote", "io/midi/advanceOnRelease"); + +void MidiRemoteConfiguration::init() +{ + settings()->setDefaultValue(ADVANCE_TO_NEXT_NOTE_ON_KEY_RELEASE, Val(true)); + settings()->valueChanged(ADVANCE_TO_NEXT_NOTE_ON_KEY_RELEASE).onReceive(this, [this](const Val& val) { + m_advanceToNextNoteOnKeyReleaseChanged.send(val.toBool()); + }); +} + +io::path_t MidiRemoteConfiguration::midiMappingUserAppDataPath() const +{ + return globalConfiguration()->userAppDataPath() + "/midi_mappings.xml"; +} + +bool MidiRemoteConfiguration::advanceToNextNoteOnKeyRelease() const +{ + return settings()->value(ADVANCE_TO_NEXT_NOTE_ON_KEY_RELEASE).toBool(); +} + +void MidiRemoteConfiguration::setAdvanceToNextNoteOnKeyRelease(bool value) +{ + settings()->setSharedValue(ADVANCE_TO_NEXT_NOTE_ON_KEY_RELEASE, Val(value)); +} + +async::Channel MidiRemoteConfiguration::advanceToNextNoteOnKeyReleaseChanged() const +{ + return m_advanceToNextNoteOnKeyReleaseChanged; +} diff --git a/src/framework/midiremote/internal/midiremoteconfiguration.h b/src/framework/midiremote/internal/midiremoteconfiguration.h new file mode 100644 index 0000000000000..dab1ee973de87 --- /dev/null +++ b/src/framework/midiremote/internal/midiremoteconfiguration.h @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2021 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 "midiremote/imidiremoteconfiguration.h" +#include "async/asyncable.h" +#include "modularity/ioc.h" + +#include "iglobalconfiguration.h" + +namespace muse::midiremote { +class MidiRemoteConfiguration : public IMidiRemoteConfiguration, public async::Asyncable +{ + GlobalInject globalConfiguration; + +public: + void init(); + + io::path_t midiMappingUserAppDataPath() const override; + + bool advanceToNextNoteOnKeyRelease() const override; + void setAdvanceToNextNoteOnKeyRelease(bool value) override; + muse::async::Channel advanceToNextNoteOnKeyReleaseChanged() const override; + +private: + muse::async::Channel m_advanceToNextNoteOnKeyReleaseChanged; +}; +} diff --git a/src/framework/midiremote/midiremotemodule.cpp b/src/framework/midiremote/midiremotemodule.cpp new file mode 100644 index 0000000000000..9a0c200316992 --- /dev/null +++ b/src/framework/midiremote/midiremotemodule.cpp @@ -0,0 +1,76 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited + * + * 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 "midiremotemodule.h" + +#include "midiremote/internal/midiremoteconfiguration.h" +#include "midiremote/internal/midiremote.h" + +#ifdef MUSE_MODULE_DIAGNOSTICS +#include "diagnostics/idiagnosticspathsregister.h" +#endif + +using namespace muse::midiremote; +using namespace muse::modularity; + +static const std::string mname("midiremote"); + +std::string MidiRemoteModule::moduleName() const +{ + return mname; +} + +void MidiRemoteModule::registerExports() +{ + m_configuration = std::make_shared(); + + globalIoc()->registerExport(mname, m_configuration); + +#ifdef MUSE_MODULE_DIAGNOSTICS + auto pr = ioc()->resolve(moduleName()); + if (pr) { + pr->reg("midiMappingUserAppDataPath", m_configuration->midiMappingUserAppDataPath()); + } +#endif +} + +void MidiRemoteModule::onInit(const IApplication::RunMode&) +{ + m_configuration->init(); +} + +IContextSetup* MidiRemoteModule::newContext(const modularity::ContextPtr& ctx) const +{ + return new MidiRemoteContext(ctx); +} + +void MidiRemoteContext::registerExports() +{ + m_midiRemote = std::make_shared(iocContext()); + + ioc()->registerExport(mname, m_midiRemote); +} + +void MidiRemoteContext::onInit(const IApplication::RunMode&) +{ + m_midiRemote->init(); +} diff --git a/src/framework/midiremote/midiremotemodule.h b/src/framework/midiremote/midiremotemodule.h new file mode 100644 index 0000000000000..13042f65c4e11 --- /dev/null +++ b/src/framework/midiremote/midiremotemodule.h @@ -0,0 +1,58 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +#include "modularity/imodulesetup.h" + +namespace muse::midiremote { +class MidiRemoteConfiguration; +class MidiRemote; + +class MidiRemoteModule : public modularity::IModuleSetup +{ +public: + std::string moduleName() const override; + void registerExports() override; + void onInit(const IApplication::RunMode& mode) override; + + modularity::IContextSetup* newContext(const modularity::ContextPtr& ctx) const override; + +private: + std::shared_ptr m_configuration; +}; + +class MidiRemoteContext : public modularity::IContextSetup +{ +public: + MidiRemoteContext(const modularity::ContextPtr& ctx) + : modularity::IContextSetup(ctx) {} + + void registerExports() override; + void onInit(const IApplication::RunMode& mode) override; + +private: + std::shared_ptr m_midiRemote; +}; +} diff --git a/src/framework/midiremote/midiremotetypes.h b/src/framework/midiremote/midiremotetypes.h new file mode 100644 index 0000000000000..24d55aeaaf2b6 --- /dev/null +++ b/src/framework/midiremote/midiremotetypes.h @@ -0,0 +1,113 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2021 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 "midi/midievent.h" + +#include "global/types/string.h" +#include "global/utils.h" +#include "global/translation.h" + +namespace muse::midiremote { +enum RemoteEventType { + Undefined = 0, + Note, + Controller +}; + +struct RemoteEvent { + RemoteEventType type = RemoteEventType::Undefined; + int value = -1; + + RemoteEvent() = default; + RemoteEvent(RemoteEventType type, int value) + : type(type), value(value) {} + + bool isValid() const + { + return type != RemoteEventType::Undefined && value != -1; + } + + String name() const + { + if (this->type == RemoteEventType::Note) { + //: A MIDI remote event, namely a note event + return muse::mtrc("shortcuts", "Note %1") + .arg(String::fromStdString(muse::pitchToString(this->value))); + } else if (this->type == RemoteEventType::Controller) { + //: A MIDI remote event, namely a MIDI controller event + return muse::mtrc("shortcuts", "CC %1").arg(String::number(this->value)); + } + + //: No MIDI remote event + return muse::mtrc("shortcuts", "None"); + } + + bool operator ==(const RemoteEvent& other) const + { + return type == other.type && value == other.value; + } + + bool operator !=(const RemoteEvent& other) const + { + return !operator==(other); + } +}; + +struct MidiControlsMapping { + std::string action; + RemoteEvent event; + + MidiControlsMapping() = default; + MidiControlsMapping(const std::string& action) + : action(action) {} + + bool isValid() const + { + return !action.empty() && event.isValid(); + } + + bool operator ==(const MidiControlsMapping& other) const + { + return action == other.action && other.event == event; + } +}; + +using MidiMappingList = std::vector; + +inline RemoteEvent remoteEventFromMidiEvent(const midi::Event& midiEvent) +{ + RemoteEvent event; + bool isNote = midiEvent.isOpcodeIn({ midi::Event::Opcode::NoteOff, midi::Event::Opcode::NoteOn }); + bool isController = midiEvent.isOpcodeIn({ midi::Event::Opcode::ControlChange }); + if (isNote) { + event.type = RemoteEventType::Note; + event.value = midiEvent.note(); + } else if (isController) { + event.type = RemoteEventType::Controller; + event.value = midiEvent.index(); + } + + return event; +} +} diff --git a/src/framework/midiremote/mmc.cpp b/src/framework/midiremote/mmc.cpp new file mode 100644 index 0000000000000..2374a1e5780a5 --- /dev/null +++ b/src/framework/midiremote/mmc.cpp @@ -0,0 +1,247 @@ +/* + * 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 "mmc.h" + +#include "midi/midievent.h" +#include "log.h" + +using namespace muse::midi; +using namespace muse::midiremote; + +inline std::optional fastParseMMC(const Event& event) +{ + const uint32_t* words = event.midi20Words(); + const uint32_t w0 = words[0]; + + // Extract UMP SysEx header fields + const uint8_t status = (w0 >> 20) & 0xF; // 0 = complete packet + const uint8_t count = (w0 >> 16) & 0xF; // number of valid bytes (0-6) + + // Fast path only handles complete, short messages + // Long messages, such as Locate, are ignored here + if (status != 0 || count < 4) { + return std::nullopt; + } + + // SysEx packs first 2 bytes in word 0, next bytes in word 1 + const uint8_t b0 = (w0 >> 8) & 0xFF; + const uint8_t b1 = (w0 >> 0) & 0xFF; + const uint32_t w1 = words[1]; + const uint8_t b2 = (w1 >> 24) & 0xFF; + const uint8_t b3 = (w1 >> 16) & 0xFF; + + // MMC structure: 7F 06 + if (!(b0 == 0x7F && b2 == 0x06)) { + return std::nullopt; + } + + MMCMessage msg; + msg.deviceId = b1; + msg.command = static_cast(b3); + + return msg; +} + +static bool extractSysExBytes(const Event& event, std::vector& out) +{ + const uint32_t* words = event.midi20Words(); + const size_t wordCount = event.midi20WordCount(); + const uint32_t w0 = words[0]; + const uint8_t count = (w0 >> 16) & 0xF; // number of valid bytes (0-6) + + if (count == 0) { + return true; + } + + out.reserve(out.size() + count); + + // Word 0: [header | byte0 | byte1] + // Word 1: [byte2 | byte3 | byte4 | byte5] + + // Extract first 2 bytes from word 0 + if (count >= 1) { + out.push_back((w0 >> 8) & 0xFF); + } + if (count >= 2) { + out.push_back((w0 >> 0) & 0xFF); + } + + // Extract remaining bytes from word 1 (if present) + if (wordCount > 1) { + const uint32_t w1 = words[1]; + + // Bytes are stored MSB -> LSB + // i = 0 -> highest byte, i = 3 -> lowest byte + for (int i = 0; i < 4; ++i) { + if (out.size() >= count) { + break; + } + + out.push_back((w1 >> (24 - i * 8)) & 0xFF); + } + } + + return true; +} + +static bool isMMC(const std::vector& sysex) +{ + return sysex.size() >= 4 + && sysex[0] == 0x7F // Device + && sysex[2] == 0x06; // Command +} + +static MMCMessage parseMMC(const std::vector& sysex) +{ + MMCMessage msg; + msg.deviceId = sysex[1]; + msg.command = static_cast(sysex[3]); + + if (sysex.size() > 4) { + msg.data.assign(sysex.begin() + 4, sysex.end()); + } + + return msg; +} + +class MMCParser::SysExAssembler +{ +public: + bool process(const Event& event, std::vector& result) + { + const uint32_t* words = event.midi20Words(); + const uint8_t status = (words[0] >> 20) & 0xF; + + std::vector bytes; + extractSysExBytes(event, bytes); + + switch (status) { + case 0: // COMPLETE + result = bytes; + return true; + case 1: // START + m_buffer = bytes; + break; + case 2: // CONTINUE + m_buffer.insert(m_buffer.end(), bytes.begin(), bytes.end()); + break; + case 3: // END + m_buffer.insert(m_buffer.end(), bytes.begin(), bytes.end()); + result = m_buffer; + m_buffer.clear(); + return true; + } + + return false; + } + +private: + std::vector m_buffer; +}; + +MMCParser::MMCParser() + : m_assembler(new SysExAssembler()) +{ +} + +MMCParser::~MMCParser() +{ + delete m_assembler; +} + +std::optional MMCParser::process(const Event& event) +{ + if (event.messageType() != Event::MessageType::SystemExclusiveData) { + return std::nullopt; + } + + if (std::optional msg = fastParseMMC(event)) { + return msg; + } + + std::vector sysex; + if (!m_assembler->process(event, sysex)) { + return std::nullopt; + } + + if (!isMMC(sysex)) { + return std::nullopt; + } + + return parseMMC(sysex); +} + +std::optional MMCParser::locateToSeconds(const MMCMessage& msg) +{ + IF_ASSERT_FAILED(msg.command == MMCCommand::Locate) { + return std::nullopt; + } + + if (msg.data.size() < 6) { + return std::nullopt; + } + + const bool hasFormat = (msg.data.size() >= 7); + const uint8_t format = hasFormat ? msg.data[1] : 0x01; + + // Only support SMPTE + if (format != 0x01) { + return std::nullopt; + } + + const size_t offset = hasFormat ? 1 : 0; + const uint8_t hr_byte = msg.data[1 + offset]; + const uint8_t min = msg.data[2 + offset]; + const uint8_t sec = msg.data[3 + offset]; + const uint8_t frame = msg.data[4 + offset]; + const uint8_t subf = msg.data[5 + offset]; + const uint8_t fpsCode = (hr_byte >> 5) & 0x03; + const uint8_t hr = hr_byte & 0x1F; + + double fps = 25.0; + switch (fpsCode) { + case 0: + fps = 24.0; + break; + case 1: + fps = 25.0; + break; + case 2: + fps = 29.97; + break; + case 3: + fps = 30.0; + break; + } + + const double total + = hr * 3600.0 + + min * 60.0 + + sec + + (frame / fps) + + (subf / (fps * 100.0)); + + return total; +} diff --git a/src/framework/midiremote/mmc.h b/src/framework/midiremote/mmc.h new file mode 100644 index 0000000000000..9b281220878e5 --- /dev/null +++ b/src/framework/midiremote/mmc.h @@ -0,0 +1,74 @@ +/* + * 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 + +namespace muse::midi { +struct Event; +} + +namespace muse::midiremote { +enum class MMCCommand : uint8_t +{ + Stop = 0x01, + Play = 0x02, + DeferredPlay = 0x03, + FastForward = 0x04, + Rewind = 0x05, + RecordStrobe = 0x06, + RecordExit = 0x07, + RecordPause = 0x08, + Pause = 0x09, + Eject = 0x0A, + Chase = 0x0B, + CommandErrorReset = 0x0D, + MMCReset = 0x0F, + Locate = 0x44, + Shuttle = 0x47, + Unknown = 0xFF +}; + +struct MMCMessage +{ + uint8_t deviceId = 0; + MMCCommand command = MMCCommand::Unknown; + std::vector data; +}; + +class MMCParser +{ +public: + MMCParser(); + ~MMCParser(); + + std::optional process(const midi::Event& event); + + static std::optional locateToSeconds(const MMCMessage& msg); + +private: + class SysExAssembler; + SysExAssembler* m_assembler = nullptr; +}; +} diff --git a/src/framework/midiremote/qml/Muse/MidiRemote/CMakeLists.txt b/src/framework/midiremote/qml/Muse/MidiRemote/CMakeLists.txt new file mode 100644 index 0000000000000..0d32eb621baa6 --- /dev/null +++ b/src/framework/midiremote/qml/Muse/MidiRemote/CMakeLists.txt @@ -0,0 +1,21 @@ +muse_create_qml_module(muse_midiremote_qml ALIAS muse::midiremote_qml FOR muse_midiremote) + +qt_add_qml_module(muse_midiremote_qml + URI Muse.MidiRemote + VERSION 1.0 + SOURCES + editmidimappingmodel.cpp + editmidimappingmodel.h + mididevicemappingmodel.cpp + mididevicemappingmodel.h + QML_FILES + EditMidiMappingDialog.qml + internal/MidiMappingBottomPanel.qml + internal/MidiMappingTopPanel.qml + MidiDeviceMappingPage.qml + IMPORTS + TARGET muse_ui_qml + TARGET muse_uicomponents_qml +) + +fixup_qml_module_dependencies(muse_midiremote_qml) \ No newline at end of file diff --git a/src/framework/shortcuts/qml/Muse/Shortcuts/EditMidiMappingDialog.qml b/src/framework/midiremote/qml/Muse/MidiRemote/EditMidiMappingDialog.qml similarity index 92% rename from src/framework/shortcuts/qml/Muse/Shortcuts/EditMidiMappingDialog.qml rename to src/framework/midiremote/qml/Muse/MidiRemote/EditMidiMappingDialog.qml index 5e240a275024f..dde45fe4ddbe7 100644 --- a/src/framework/shortcuts/qml/Muse/Shortcuts/EditMidiMappingDialog.qml +++ b/src/framework/midiremote/qml/Muse/MidiRemote/EditMidiMappingDialog.qml @@ -24,12 +24,12 @@ import QtQuick.Layouts import Muse.Ui import Muse.UiComponents -import Muse.Shortcuts +import Muse.MidiRemote StyledDialogView { id: root - title: qsTrc("shortcuts", "MIDI remote control") + title: qsTrc("midiremote", "MIDI remote control") contentWidth: 538 contentHeight: 164 @@ -91,7 +91,7 @@ StyledDialogView { StyledTextLabel { width: parent.width - text: qsTrc("shortcuts", "Press a key or adjust a control on your MIDI device to assign it to this action.") + text: qsTrc("midiremote", "Press a key or adjust a control on your MIDI device to assign it to this action.") } RowLayout { @@ -100,7 +100,7 @@ StyledDialogView { spacing: 10 StyledTextLabel { - text: qsTrc("shortcuts", "MIDI mapping:") + text: qsTrc("midiremote", "MIDI mapping:") } TextInputField { @@ -115,7 +115,7 @@ StyledDialogView { currentText: model.mappingTitle //: The app is waiting for the user to trigger a valid MIDI remote event - hint: qsTrc("shortcuts", "Waiting…") + hint: qsTrc("global", "Waiting…") navigation.panel: navPanel navigation.order: 1 diff --git a/src/framework/shortcuts/qml/Muse/Shortcuts/MidiDeviceMappingPage.qml b/src/framework/midiremote/qml/Muse/MidiRemote/MidiDeviceMappingPage.qml similarity index 96% rename from src/framework/shortcuts/qml/Muse/Shortcuts/MidiDeviceMappingPage.qml rename to src/framework/midiremote/qml/Muse/MidiRemote/MidiDeviceMappingPage.qml index 7ea404dc579ef..fe716c10aa270 100644 --- a/src/framework/shortcuts/qml/Muse/Shortcuts/MidiDeviceMappingPage.qml +++ b/src/framework/midiremote/qml/Muse/MidiRemote/MidiDeviceMappingPage.qml @@ -24,7 +24,7 @@ import QtQuick.Layouts import Muse.Ui import Muse.UiComponents -import Muse.Shortcuts +import Muse.MidiRemote import "internal" @@ -90,9 +90,9 @@ Item { readOnly: true keyRoleName: "title" - keyTitle: qsTrc("shortcuts", "action") + keyTitle: qsTrc("global", "action") valueRoleName: "status" - valueTitle: qsTrc("shortcuts", "status") + valueTitle: qsTrc("global", "status") iconRoleName: "icon" valueEnabledRoleName: "enabled" diff --git a/src/framework/shortcuts/qml/Muse/Shortcuts/editmidimappingmodel.cpp b/src/framework/midiremote/qml/Muse/MidiRemote/editmidimappingmodel.cpp similarity index 96% rename from src/framework/shortcuts/qml/Muse/Shortcuts/editmidimappingmodel.cpp rename to src/framework/midiremote/qml/Muse/MidiRemote/editmidimappingmodel.cpp index a5a66e341cc73..3ee06fcdd368a 100644 --- a/src/framework/shortcuts/qml/Muse/Shortcuts/editmidimappingmodel.cpp +++ b/src/framework/midiremote/qml/Muse/MidiRemote/editmidimappingmodel.cpp @@ -24,8 +24,8 @@ #include "translation.h" -using namespace muse::shortcuts; using namespace muse::midi; +using namespace muse::midiremote; EditMidiMappingModel::EditMidiMappingModel(QObject* parent) : QObject(parent), Contextable(muse::iocCtxForQmlObject(this)) @@ -56,7 +56,7 @@ QString EditMidiMappingModel::mappingTitle() const { MidiDeviceID currentMidiInDeviceId = midiInPort()->deviceID(); if (currentMidiInDeviceId.empty() || !m_event.isValid()) { - return muse::qtrc("shortcuts", "Waiting…"); + return muse::qtrc("global", "Waiting…"); } return deviceName(currentMidiInDeviceId) + " > " + m_event.name().toQString(); diff --git a/src/framework/shortcuts/qml/Muse/Shortcuts/editmidimappingmodel.h b/src/framework/midiremote/qml/Muse/MidiRemote/editmidimappingmodel.h similarity index 91% rename from src/framework/shortcuts/qml/Muse/Shortcuts/editmidimappingmodel.h rename to src/framework/midiremote/qml/Muse/MidiRemote/editmidimappingmodel.h index df22ce3ac085a..23adaf20110df 100644 --- a/src/framework/shortcuts/qml/Muse/Shortcuts/editmidimappingmodel.h +++ b/src/framework/midiremote/qml/Muse/MidiRemote/editmidimappingmodel.h @@ -28,9 +28,9 @@ #include "modularity/ioc.h" #include "async/asyncable.h" #include "midi/imidiinport.h" -#include "imidiremote.h" +#include "midiremote/imidiremote.h" -namespace muse::shortcuts { +namespace muse::midiremote { class EditMidiMappingModel : public QObject, public Contextable, public async::Asyncable { Q_OBJECT @@ -39,12 +39,12 @@ class EditMidiMappingModel : public QObject, public Contextable, public async::A QML_ELEMENT - GlobalInject midiInPort; + GlobalInject midiInPort; ContextInject midiRemote = { this }; public: explicit EditMidiMappingModel(QObject* parent = nullptr); - ~EditMidiMappingModel(); + ~EditMidiMappingModel() override; QString mappingTitle() const; diff --git a/src/framework/shortcuts/qml/Muse/Shortcuts/internal/MidiMappingBottomPanel.qml b/src/framework/midiremote/qml/Muse/MidiRemote/internal/MidiMappingBottomPanel.qml similarity index 94% rename from src/framework/shortcuts/qml/Muse/Shortcuts/internal/MidiMappingBottomPanel.qml rename to src/framework/midiremote/qml/Muse/MidiRemote/internal/MidiMappingBottomPanel.qml index 0ae21e00ce1f7..9655bc92daa9b 100644 --- a/src/framework/shortcuts/qml/Muse/Shortcuts/internal/MidiMappingBottomPanel.qml +++ b/src/framework/midiremote/qml/Muse/MidiRemote/internal/MidiMappingBottomPanel.qml @@ -40,7 +40,7 @@ Row { name: "MidiMappingBottomPanel" enabled: root.enabled && root.visible direction: NavigationPanel.Horizontal - accessible.name: qsTrc("shortcuts", "MIDI mapping bottom panel") + accessible.name: qsTrc("midiremote", "MIDI mapping bottom panel") onActiveChanged: function(active) { if (active) { @@ -52,7 +52,7 @@ Row { FlatButton { id: editActionButton - text: qsTrc("shortcuts", "Assign MIDI mapping…") + text: qsTrc("midiremote", "Assign MIDI mapping…") navigation.name: "EditActionButton" navigation.panel: root.navigation diff --git a/src/framework/shortcuts/qml/Muse/Shortcuts/internal/MidiMappingTopPanel.qml b/src/framework/midiremote/qml/Muse/MidiRemote/internal/MidiMappingTopPanel.qml similarity index 92% rename from src/framework/shortcuts/qml/Muse/Shortcuts/internal/MidiMappingTopPanel.qml rename to src/framework/midiremote/qml/Muse/MidiRemote/internal/MidiMappingTopPanel.qml index 5e4d2cad891a3..fbca0f354d54a 100644 --- a/src/framework/shortcuts/qml/Muse/Shortcuts/internal/MidiMappingTopPanel.qml +++ b/src/framework/midiremote/qml/Muse/MidiRemote/internal/MidiMappingTopPanel.qml @@ -37,7 +37,7 @@ RowLayout { name: "MidiMappingTopPanel" enabled: root.enabled && root.visible direction: NavigationPanel.Horizontal - accessible.name: qsTrc("shortcuts", "MIDI mapping top panel") + accessible.name: qsTrc("midiremote", "MIDI mapping top panel") onActiveChanged: function(active) { if (active) { @@ -48,7 +48,7 @@ RowLayout { CheckBox { id: remoteControlCheckBox - text: qsTrc("shortcuts", "MIDI remote control") + text: qsTrc("midiremote", "MIDI remote control") font: ui.theme.bodyBoldFont navigation.name: "RemoteControlCheckBox" diff --git a/src/framework/shortcuts/qml/Muse/Shortcuts/mididevicemappingmodel.cpp b/src/framework/midiremote/qml/Muse/MidiRemote/mididevicemappingmodel.cpp similarity index 91% rename from src/framework/shortcuts/qml/Muse/Shortcuts/mididevicemappingmodel.cpp rename to src/framework/midiremote/qml/Muse/MidiRemote/mididevicemappingmodel.cpp index 9fbca4c6b2c99..1f4b3e8684a2d 100644 --- a/src/framework/shortcuts/qml/Muse/Shortcuts/mididevicemappingmodel.cpp +++ b/src/framework/midiremote/qml/Muse/MidiRemote/mididevicemappingmodel.cpp @@ -28,8 +28,8 @@ #include "log.h" #include "translation.h" -using namespace muse::shortcuts; using namespace muse::midi; +using namespace muse::midiremote; using namespace muse::ui; using namespace muse::actions; @@ -92,14 +92,13 @@ QVariant MidiDeviceMappingModel::data(const QModelIndex& index, int role) const QVariantMap MidiDeviceMappingModel::midiMappingToObject(const MidiControlsMapping& midiMapping) const { - UiAction action = uiActionsRegister()->action(midiMapping.action); + const UiAction& action = uiActionsRegister()->action(midiMapping.action); QVariantMap obj; - obj[TITLE_KEY] = !action.description.isEmpty() ? action.description.qTranslated() : action.title.qTranslatedWithoutMnemonic(); obj[ICON_KEY] = static_cast(action.iconCode); obj[ENABLED_KEY] = midiMapping.isValid(); - obj[STATUS_KEY] = midiMapping.isValid() ? midiMapping.event.name().toQString() : muse::qtrc("shortcuts", "Inactive"); + obj[STATUS_KEY] = midiMapping.isValid() ? midiMapping.event.name().toQString() : muse::qtrc("global", "Inactive"); obj[MAPPED_TYPE_KEY] = static_cast(midiMapping.event.type); obj[MAPPED_VALUE_KEY] = midiMapping.event.value; @@ -108,7 +107,7 @@ QVariantMap MidiDeviceMappingModel::midiMappingToObject(const MidiControlsMappin int MidiDeviceMappingModel::rowCount(const QModelIndex&) const { - return m_midiMappings.size(); + return static_cast(m_midiMappings.size()); } QHash MidiDeviceMappingModel::roleNames() const @@ -132,7 +131,7 @@ void MidiDeviceMappingModel::load() beginResetModel(); m_midiMappings.clear(); - shortcuts::MidiMappingList midiMappings = midiRemote()->midiMappings(); + const MidiMappingList& midiMappings = midiRemote()->midiMappings(); auto remoteEvent = [&midiMappings](const ActionCode& actionCode) { for (const MidiControlsMapping& midiMapping : midiMappings) { @@ -163,12 +162,7 @@ void MidiDeviceMappingModel::load() bool MidiDeviceMappingModel::apply() { - MidiMappingList midiMappings; - for (const MidiControlsMapping& midiMapping : std::as_const(m_midiMappings)) { - midiMappings.push_back(midiMapping); - } - - Ret ret = midiRemote()->setMidiMappings(midiMappings); + Ret ret = midiRemote()->setMidiMappings(m_midiMappings); if (!ret) { LOGE() << ret.toString(); } @@ -242,7 +236,7 @@ QVariant MidiDeviceMappingModel::currentAction() const return QVariant(); } - MidiControlsMapping midiMapping = m_midiMappings[indexes.first().row()]; + const MidiControlsMapping& midiMapping = m_midiMappings[indexes.first().row()]; return midiMappingToObject(midiMapping); } diff --git a/src/framework/shortcuts/qml/Muse/Shortcuts/mididevicemappingmodel.h b/src/framework/midiremote/qml/Muse/MidiRemote/mididevicemappingmodel.h similarity index 92% rename from src/framework/shortcuts/qml/Muse/Shortcuts/mididevicemappingmodel.h rename to src/framework/midiremote/qml/Muse/MidiRemote/mididevicemappingmodel.h index e4442a6c81f19..a49ceedfb3d4e 100644 --- a/src/framework/shortcuts/qml/Muse/Shortcuts/mididevicemappingmodel.h +++ b/src/framework/midiremote/qml/Muse/MidiRemote/mididevicemappingmodel.h @@ -30,11 +30,10 @@ #include "modularity/ioc.h" #include "imidiremote.h" -#include "ishortcutsconfiguration.h" #include "midi/imidiconfiguration.h" #include "ui/iuiactionsregister.h" -namespace muse::shortcuts { +namespace muse::midiremote { class MidiDeviceMappingModel : public QAbstractListModel, public Contextable, public async::Asyncable { Q_OBJECT @@ -46,10 +45,9 @@ class MidiDeviceMappingModel : public QAbstractListModel, public Contextable, pu QML_ELEMENT - GlobalInject configuration; GlobalInject midiConfiguration; ContextInject uiActionsRegister = { this }; - ContextInject midiRemote = { this }; + ContextInject midiRemote = { this }; public: explicit MidiDeviceMappingModel(QObject* parent = nullptr); @@ -92,7 +90,7 @@ public slots: QVariantMap midiMappingToObject(const MidiControlsMapping& midiMapping) const; - QList m_midiMappings; + MidiMappingList m_midiMappings; QItemSelection m_selection; }; } diff --git a/src/framework/midiremote/tests/CMakeLists.txt b/src/framework/midiremote/tests/CMakeLists.txt new file mode 100644 index 0000000000000..af34da6e952c8 --- /dev/null +++ b/src/framework/midiremote/tests/CMakeLists.txt @@ -0,0 +1,32 @@ +# 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 . + +set(MODULE_TEST muse_midiremote_test) + +set(MODULE_TEST_SRC + ${CMAKE_CURRENT_LIST_DIR}/mmc_tests.cpp + ) + +set(MODULE_TEST_LINK + muse_midi + muse_midiremote +) + +include(SetupGTest) diff --git a/src/framework/midiremote/tests/mmc_tests.cpp b/src/framework/midiremote/tests/mmc_tests.cpp new file mode 100644 index 0000000000000..c444fcf7e8317 --- /dev/null +++ b/src/framework/midiremote/tests/mmc_tests.cpp @@ -0,0 +1,187 @@ +/* + * 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 "midiremote/mmc.h" +#include "midi/midievent.h" + +using namespace muse::midi; +using namespace muse::midiremote; + +static constexpr double LOCATE_ERROR(0.000001); + +class MMCParserTests : public ::testing::Test +{ +protected: + void SetUp() override {} + + static std::vector makeSysExEvents(std::initializer_list bytes) + { + std::vector result; + + const uint8_t* data = bytes.begin(); + const size_t size = bytes.size(); + + for (size_t pos = 0; pos < size; pos += 6) { + const size_t remaining = size - pos; + const size_t count = remaining < 6 ? remaining : 6; + + const bool first = (pos == 0); + const bool last = (pos + count >= size); + + const uint8_t status + = first && last ? 0 // COMPLETE + : first ? 1 // START + : last ? 3 // END + : 2; // CONTINUE + + uint32_t w0 = 0; + uint32_t w1 = 0; + + w0 |= (static_cast(Event::MessageType::SystemExclusiveData) << 28); + w0 |= (status << 20); + w0 |= (static_cast(count) << 16); + + // First 2 bytes -> w0 + if (count > 0) { + w0 |= data[pos + 0] << 8; + } + if (count > 1) { + w0 |= data[pos + 1]; + } + + // Remaining -> w1 + for (size_t i = 2; i < count; ++i) { + w1 |= data[pos + i] << (24 - (i - 2) * 8); + } + + result.emplace_back(std::array { w0, w1, 0, 0 }); + } + + return result; + } +}; + +TEST_F(MMCParserTests, Process_PlayCommand) +{ + // [GIVEN] Play command + // SysEx: 7F 06 02 + std::vector events = makeSysExEvents({ 0x7F, 0x7F, 0x06, 0x02 }); + ASSERT_EQ(events.size(), 1); + + // [WHEN] Parse MMC message + MMCParser parser; + std::optional msg = parser.process(events.front()); + + // [THEN] Message is valid + ASSERT_TRUE(msg.has_value()); + EXPECT_EQ(msg->command, MMCCommand::Play); + EXPECT_EQ(msg->deviceId, 0x7F); + EXPECT_TRUE(msg->data.empty()); +} + +TEST_F(MMCParserTests, Process_LocateCommand_FragmentedSysEx) +{ + // [GIVEN] Locate command + std::vector events = makeSysExEvents({ + 0x7F, 0x7F, 0x06, 0x44, + 0x06, 0x01, 0x96, 0x00, + 0x15, 0x04, 0x00 + }); + ASSERT_EQ(events.size(), 2); + + // [WHEN] Parse MMC message + MMCParser parser; + std::optional msg; + for (const Event& event : events) { + msg = parser.process(event); + } + + // [THEN] Message is valid + ASSERT_TRUE(msg.has_value()); + EXPECT_EQ(msg->command, MMCCommand::Locate); + EXPECT_EQ(msg->deviceId, 0x7F); + EXPECT_FALSE(msg->data.empty()); +} + +TEST_F(MMCParserTests, Process_NonMMCMessage) +{ + // [GIVEN] Not MMC (missing 0x7F / 0x06) + std::vector events = makeSysExEvents({ 0x01, 0x02, 0x03, 0x04 }); + ASSERT_EQ(events.size(), 1); + + // [WHEN] Parse message + MMCParser parser; + std::optional msg = parser.process(events.front()); + + // [THEN] No message + EXPECT_FALSE(msg.has_value()); +} + +TEST_F(MMCParserTests, LocateToSeconds_WithFormatByte) +{ + // [GIVEN] format = 1, hr_byte = 96 (30 fps, 0h), min = 0, sec = 15, frame = 4, subframe = 0 + MMCMessage msg; + msg.command = MMCCommand::Locate; + msg.data = { 6, 1, 96, 0, 15, 4, 0 }; + + // [WHEN] Convert msg to seconds + std::optional secs = MMCParser::locateToSeconds(msg); + + // [THEN] Result is valid + ASSERT_TRUE(secs.has_value()); + + constexpr double expected = 15.0 + (4.0 / 30.0); + EXPECT_NEAR(secs.value(), expected, LOCATE_ERROR); +} + +TEST_F(MMCParserTests, LocateToSeconds_WithoutFormatByte) +{ + // [GIVEN] hr_byte = 96 (30 fps), min = 0, sec = 15, frame = 4, subframe = 0 + MMCMessage msg; + msg.command = MMCCommand::Locate; + msg.data = { 6, 96, 0, 15, 4, 0 }; + + // [WHEN] Convert msg to seconds + std::optional secs = MMCParser::locateToSeconds(msg); + + // [THEN] Result is valid + ASSERT_TRUE(secs.has_value()); + + constexpr double expected = 15.0 + (4.0 / 30.0); + EXPECT_NEAR(secs.value(), expected, LOCATE_ERROR); +} + +TEST_F(MMCParserTests, LocateToSeconds_InvalidFormat) +{ + // [GIVEN] Invalid msg + MMCMessage msg; + msg.command = MMCCommand::Locate; + msg.data = { 6, 2, 96, 0, 15, 4, 0 }; // format != 1 + + // [WHEN] Convert msg to seconds + std::optional secs = MMCParser::locateToSeconds(msg); + + // [THEN] + EXPECT_FALSE(secs.has_value()); +} diff --git a/src/framework/shortcuts/CMakeLists.txt b/src/framework/shortcuts/CMakeLists.txt index 2a15a0cfb34de..f193260325107 100644 --- a/src/framework/shortcuts/CMakeLists.txt +++ b/src/framework/shortcuts/CMakeLists.txt @@ -30,7 +30,6 @@ target_sources(muse_shortcuts PRIVATE ishortcutsregister.h ishortcutscontroller.h ishortcutsconfiguration.h - imidiremote.h api/shortcutsapi.cpp api/shortcutsapi.h @@ -41,8 +40,6 @@ target_sources(muse_shortcuts PRIVATE internal/shortcutscontroller.h internal/shortcutsconfiguration.cpp internal/shortcutsconfiguration.h - internal/midiremote.cpp - internal/midiremote.h ) target_link_libraries(muse_shortcuts PRIVATE Qt::Gui) diff --git a/src/framework/shortcuts/internal/shortcutsconfiguration.cpp b/src/framework/shortcuts/internal/shortcutsconfiguration.cpp index f2a99e5b3212a..c7e84ae47043d 100644 --- a/src/framework/shortcuts/internal/shortcutsconfiguration.cpp +++ b/src/framework/shortcuts/internal/shortcutsconfiguration.cpp @@ -31,18 +31,9 @@ using namespace muse; using namespace muse::shortcuts; -static const std::string MIDIMAPPINGS_FILE_NAME("/midi_mappings.xml"); - -static const Settings::Key ADVANCE_TO_NEXT_NOTE_ON_KEY_RELEASE("shortcuts", "io/midi/advanceOnRelease"); - void ShortcutsConfiguration::init() { m_config = ConfigReader::read(":/configs/shortcuts.cfg"); - - settings()->setDefaultValue(ADVANCE_TO_NEXT_NOTE_ON_KEY_RELEASE, Val(true)); - settings()->valueChanged(ADVANCE_TO_NEXT_NOTE_ON_KEY_RELEASE).onReceive(this, [this](const Val& val) { - m_advanceToNextNoteOnKeyReleaseChanged.send(val.toBool()); - }); } QString ShortcutsConfiguration::currentKeyboardLayout() const @@ -71,23 +62,3 @@ io::path_t ShortcutsConfiguration::shortcutsAppDataPath() const return m_config.value("shortcuts").toPath(); } - -io::path_t ShortcutsConfiguration::midiMappingUserAppDataPath() const -{ - return globalConfiguration()->userAppDataPath() + MIDIMAPPINGS_FILE_NAME; -} - -bool ShortcutsConfiguration::advanceToNextNoteOnKeyRelease() const -{ - return settings()->value(ADVANCE_TO_NEXT_NOTE_ON_KEY_RELEASE).toBool(); -} - -void ShortcutsConfiguration::setAdvanceToNextNoteOnKeyRelease(bool value) -{ - settings()->setSharedValue(ADVANCE_TO_NEXT_NOTE_ON_KEY_RELEASE, Val(value)); -} - -muse::async::Channel ShortcutsConfiguration::advanceToNextNoteOnKeyReleaseChanged() const -{ - return m_advanceToNextNoteOnKeyReleaseChanged; -} diff --git a/src/framework/shortcuts/internal/shortcutsconfiguration.h b/src/framework/shortcuts/internal/shortcutsconfiguration.h index 181c93ff2d67d..170cedb523cdb 100644 --- a/src/framework/shortcuts/internal/shortcutsconfiguration.h +++ b/src/framework/shortcuts/internal/shortcutsconfiguration.h @@ -47,15 +47,7 @@ class ShortcutsConfiguration : public IShortcutsConfiguration, public Contextabl io::path_t shortcutsUserAppDataPath() const override; io::path_t shortcutsAppDataPath() const override; - io::path_t midiMappingUserAppDataPath() const override; - - bool advanceToNextNoteOnKeyRelease() const override; - void setAdvanceToNextNoteOnKeyRelease(bool value) override; - virtual muse::async::Channel advanceToNextNoteOnKeyReleaseChanged() const override; - private: Config m_config; - - muse::async::Channel m_advanceToNextNoteOnKeyReleaseChanged; }; } diff --git a/src/framework/shortcuts/ishortcutsconfiguration.h b/src/framework/shortcuts/ishortcutsconfiguration.h index e862b63cf8416..637e2969f5e0c 100644 --- a/src/framework/shortcuts/ishortcutsconfiguration.h +++ b/src/framework/shortcuts/ishortcutsconfiguration.h @@ -19,8 +19,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef MUSE_SHORTCUTS_ISHORTCUTSCONFIGURATION_H -#define MUSE_SHORTCUTS_ISHORTCUTSCONFIGURATION_H + +#pragma once #include "modularity/imoduleinterface.h" #include "io/path.h" @@ -39,13 +39,5 @@ class IShortcutsConfiguration : MODULE_GLOBAL_INTERFACE virtual io::path_t shortcutsUserAppDataPath() const = 0; virtual io::path_t shortcutsAppDataPath() const = 0; - - virtual io::path_t midiMappingUserAppDataPath() const = 0; - - virtual bool advanceToNextNoteOnKeyRelease() const = 0; - virtual void setAdvanceToNextNoteOnKeyRelease(bool value) = 0; - virtual muse::async::Channel advanceToNextNoteOnKeyReleaseChanged() const = 0; }; } - -#endif // MUSE_SHORTCUTS_ISHORTCUTSCONFIGURATION_H diff --git a/src/framework/shortcuts/qml/Muse/Shortcuts/CMakeLists.txt b/src/framework/shortcuts/qml/Muse/Shortcuts/CMakeLists.txt index 67ab588452bf0..718bf9c48a77a 100644 --- a/src/framework/shortcuts/qml/Muse/Shortcuts/CMakeLists.txt +++ b/src/framework/shortcuts/qml/Muse/Shortcuts/CMakeLists.txt @@ -4,25 +4,17 @@ qt_add_qml_module(muse_shortcuts_qml URI Muse.Shortcuts VERSION 1.0 SOURCES - editmidimappingmodel.cpp - editmidimappingmodel.h editshortcutmodel.cpp editshortcutmodel.h - mididevicemappingmodel.cpp - mididevicemappingmodel.h shortcutsinstancemodel.cpp shortcutsinstancemodel.h shortcutsmodel.cpp shortcutsmodel.h QML_FILES - EditMidiMappingDialog.qml EditShortcutDialogContent.qml - internal/MidiMappingBottomPanel.qml - internal/MidiMappingTopPanel.qml internal/ShortcutsBottomPanel.qml internal/ShortcutsList.qml internal/ShortcutsTopPanel.qml - MidiDeviceMappingPage.qml Shortcuts.qml ShortcutsPage.qml StandardEditShortcutDialog.qml diff --git a/src/framework/shortcuts/shortcutsmodule.cpp b/src/framework/shortcuts/shortcutsmodule.cpp index c128a91631d97..bf6a1e344cb4d 100644 --- a/src/framework/shortcuts/shortcutsmodule.cpp +++ b/src/framework/shortcuts/shortcutsmodule.cpp @@ -25,7 +25,6 @@ #include "internal/shortcutsregister.h" #include "internal/shortcutscontroller.h" -#include "internal/midiremote.h" #include "internal/shortcutsconfiguration.h" #include "global/api/iapiregister.h" @@ -74,7 +73,6 @@ void ShortcutsModule::onInit(const IApplication::RunMode&) if (pr) { pr->reg("shortcutsUserAppDataPath", m_configuration->shortcutsUserAppDataPath()); pr->reg("shortcutsAppDataPath", m_configuration->shortcutsAppDataPath()); - pr->reg("midiMappingUserAppDataPath", m_configuration->midiMappingUserAppDataPath()); } #endif } @@ -88,17 +86,14 @@ void ShortcutsContext::registerExports() { m_shortcutsController = std::make_shared(iocContext()); m_shortcutsRegister = std::make_shared(iocContext()); - m_midiRemote = std::make_shared(iocContext()); ioc()->registerExport(mname, m_shortcutsRegister); ioc()->registerExport(mname, m_shortcutsController); - ioc()->registerExport(mname, m_midiRemote); } void ShortcutsContext::onInit(const IApplication::RunMode&) { m_shortcutsController->init(); - m_midiRemote->init(); } void ShortcutsContext::onAllInited(const IApplication::RunMode&) diff --git a/src/framework/shortcuts/shortcutsmodule.h b/src/framework/shortcuts/shortcutsmodule.h index 6f54ba47c0024..24b5a7967fcfb 100644 --- a/src/framework/shortcuts/shortcutsmodule.h +++ b/src/framework/shortcuts/shortcutsmodule.h @@ -19,8 +19,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef MUSE_SHORTCUTS_SHORTCUTSMODULE_H -#define MUSE_SHORTCUTS_SHORTCUTSMODULE_H + +#pragma once #include #include @@ -31,11 +31,9 @@ namespace muse::shortcuts { class ShortcutsController; class ShortcutsRegister; class ShortcutsConfiguration; -class MidiRemote; class ShortcutsModule : public modularity::IModuleSetup { public: - std::string moduleName() const override; void registerExports() override; void registerApi() override; @@ -44,7 +42,6 @@ class ShortcutsModule : public modularity::IModuleSetup modularity::IContextSetup* newContext(const modularity::ContextPtr& ctx) const override; private: - std::shared_ptr m_configuration; }; @@ -61,8 +58,5 @@ class ShortcutsContext : public modularity::IContextSetup private: std::shared_ptr m_shortcutsController; std::shared_ptr m_shortcutsRegister; - std::shared_ptr m_midiRemote; }; } - -#endif // MUSE_SHORTCUTS_SHORTCUTSMODULE_H diff --git a/src/framework/shortcuts/shortcutstypes.h b/src/framework/shortcuts/shortcutstypes.h index 287639257400f..f8aaee1f7342b 100644 --- a/src/framework/shortcuts/shortcutstypes.h +++ b/src/framework/shortcuts/shortcutstypes.h @@ -19,17 +19,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef MUSE_SHORTCUTS_SHORTCUTSTYPES_H -#define MUSE_SHORTCUTS_SHORTCUTSTYPES_H + +#pragma once #include #include #include -#include "global/utils.h" #include "global/stringutils.h" -#include "global/translation.h" -#include "midi/midievent.h" namespace muse::shortcuts { struct Shortcut @@ -80,90 +77,6 @@ struct Shortcut using ShortcutList = std::list; -enum RemoteEventType { - Undefined = 0, - Note, - Controller -}; - -struct RemoteEvent { - RemoteEventType type = RemoteEventType::Undefined; - int value = -1; - - RemoteEvent() = default; - RemoteEvent(RemoteEventType type, int value) - : type(type), value(value) {} - - bool isValid() const - { - return type != RemoteEventType::Undefined && value != -1; - } - - String name() const - { - if (this->type == RemoteEventType::Note) { - //: A MIDI remote event, namely a note event - return muse::mtrc("shortcuts", "Note %1") - .arg(String::fromStdString(muse::pitchToString(this->value))); - } else if (this->type == RemoteEventType::Controller) { - //: A MIDI remote event, namely a MIDI controller event - return muse::mtrc("shortcuts", "CC %1").arg(String::number(this->value)); - } - - //: No MIDI remote event - return muse::mtrc("shortcuts", "None"); - } - - bool operator ==(const RemoteEvent& other) const - { - return type == other.type && value == other.value; - } - - bool operator !=(const RemoteEvent& other) const - { - return !operator==(other); - } -}; - -struct MidiControlsMapping { - std::string action; - RemoteEvent event; - - MidiControlsMapping() = default; - MidiControlsMapping(const std::string& action) - : action(action) {} - - bool isValid() const - { - return !action.empty() && event.isValid(); - } - - bool operator ==(const MidiControlsMapping& other) const - { - return action == other.action && other.event == event; - } -}; - -using MidiMappingList = std::list; - -inline RemoteEvent remoteEventFromMidiEvent(const muse::midi::Event& midiEvent) -{ - using namespace muse; - - RemoteEvent event; - bool isNote = midiEvent.isOpcodeIn({ midi::Event::Opcode::NoteOff, midi::Event::Opcode::NoteOn }); - bool isController = midiEvent.isOpcodeIn({ midi::Event::Opcode::ControlChange }); - if (isNote) { - event.type = RemoteEventType::Note; - event.value = midiEvent.note(); - } else if (isController) { - event.type = RemoteEventType::Controller; - event.value = midiEvent.index(); - } - - return event; -} - inline bool needIgnoreKey(Qt::Key key) { static const std::set ignoredKeys { @@ -219,5 +132,3 @@ inline bool areContextPrioritiesEqual(const std::string& shortcutCtx1, const std return shortcutCtx1 == shortcutCtx2; } } - -#endif // MUSE_SHORTCUTS_SHORTCUTSTYPES_H diff --git a/src/framework/stubs/CMakeLists.txt b/src/framework/stubs/CMakeLists.txt index d70577b6c6d00..7ead4c89f6670 100644 --- a/src/framework/stubs/CMakeLists.txt +++ b/src/framework/stubs/CMakeLists.txt @@ -46,6 +46,10 @@ if (NOT MUSE_MODULE_MIDI) add_subdirectory(midi) endif() +if (NOT MUSE_MODULE_MIDIREMOTE) + add_subdirectory(midiremote) +endif() + if (NOT MUSE_MODULE_MPE) add_subdirectory(mpe) endif() diff --git a/src/framework/stubs/midiremote/CMakeLists.txt b/src/framework/stubs/midiremote/CMakeLists.txt new file mode 100644 index 0000000000000..12da17b062cf1 --- /dev/null +++ b/src/framework/stubs/midiremote/CMakeLists.txt @@ -0,0 +1,30 @@ +# 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 . + +muse_create_module(muse_midiremote ALIAS muse::midiremote STUB) + +target_sources(muse_midiremote PRIVATE + midiremotestubmodule.cpp + midiremotestubmodule.h + midiremoteconfigurationstub.cpp + midiremoteconfigurationstub.h + midiremotestub.cpp + midiremotestub.h +) \ No newline at end of file diff --git a/src/framework/stubs/midiremote/midiremoteconfigurationstub.cpp b/src/framework/stubs/midiremote/midiremoteconfigurationstub.cpp new file mode 100644 index 0000000000000..bee5e3acb844e --- /dev/null +++ b/src/framework/stubs/midiremote/midiremoteconfigurationstub.cpp @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "midiremoteconfigurationstub.h" + +using namespace muse; +using namespace muse::midiremote; + +io::path_t MidiRemoteConfigurationStub::midiMappingUserAppDataPath() const +{ + return io::path_t(); +} + +bool MidiRemoteConfigurationStub::advanceToNextNoteOnKeyRelease() const +{ + return false; +} + +void MidiRemoteConfigurationStub::setAdvanceToNextNoteOnKeyRelease(bool) +{ +} + +async::Channel MidiRemoteConfigurationStub::advanceToNextNoteOnKeyReleaseChanged() const +{ + static async::Channel ch; + return ch; +} diff --git a/src/framework/stubs/midiremote/midiremoteconfigurationstub.h b/src/framework/stubs/midiremote/midiremoteconfigurationstub.h new file mode 100644 index 0000000000000..5e42094d98247 --- /dev/null +++ b/src/framework/stubs/midiremote/midiremoteconfigurationstub.h @@ -0,0 +1,37 @@ +/* + * 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 "midiremote/imidiremoteconfiguration.h" + +namespace muse::midiremote { +class MidiRemoteConfigurationStub : public IMidiRemoteConfiguration +{ +public: + io::path_t midiMappingUserAppDataPath() const override; + + bool advanceToNextNoteOnKeyRelease() const override; + void setAdvanceToNextNoteOnKeyRelease(bool value) override; + muse::async::Channel advanceToNextNoteOnKeyReleaseChanged() const override; +}; +} diff --git a/src/framework/stubs/shortcuts/midiremotestub.cpp b/src/framework/stubs/midiremote/midiremotestub.cpp similarity index 94% rename from src/framework/stubs/shortcuts/midiremotestub.cpp rename to src/framework/stubs/midiremote/midiremotestub.cpp index 9c96efd18f30b..9d333c14c64aa 100644 --- a/src/framework/stubs/shortcuts/midiremotestub.cpp +++ b/src/framework/stubs/midiremote/midiremotestub.cpp @@ -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 @@ -22,7 +22,7 @@ #include "midiremotestub.h" using namespace muse; -using namespace muse::shortcuts; +using namespace muse::midiremote; const MidiMappingList& MidiRemoteStub::midiMappings() const { diff --git a/src/framework/stubs/shortcuts/midiremotestub.h b/src/framework/stubs/midiremote/midiremotestub.h similarity index 84% rename from src/framework/stubs/shortcuts/midiremotestub.h rename to src/framework/stubs/midiremote/midiremotestub.h index 1d821e8e7248b..27c86f49c4d9f 100644 --- a/src/framework/stubs/shortcuts/midiremotestub.h +++ b/src/framework/stubs/midiremote/midiremotestub.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 @@ -19,12 +19,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef MUSE_SHORTCUTS_MIDIREMOTESTUB_H -#define MUSE_SHORTCUTS_MIDIREMOTESTUB_H -#include "shortcuts/imidiremote.h" +#pragma once -namespace muse::shortcuts { +#include "midiremote/imidiremote.h" + +namespace muse::midiremote { class MidiRemoteStub : public IMidiRemote { public: @@ -43,5 +43,3 @@ class MidiRemoteStub : public IMidiRemote Ret process(const muse::midi::Event& ev) override; }; } - -#endif // MUSE_SHORTCUTS_MIDIREMOTESTUB_H diff --git a/src/framework/stubs/midiremote/midiremotestubmodule.cpp b/src/framework/stubs/midiremote/midiremotestubmodule.cpp new file mode 100644 index 0000000000000..a645d09de681b --- /dev/null +++ b/src/framework/stubs/midiremote/midiremotestubmodule.cpp @@ -0,0 +1,41 @@ +/* + * 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 "midiremotestubmodule.h" + +#include "modularity/ioc.h" + +#include "midiremotestub.h" +#include "midiremoteconfigurationstub.h" + +using namespace muse::midiremote; +using namespace muse::modularity; + +std::string MidiRemoteModule::moduleName() const +{ + return "midiremote_stub"; +} + +void MidiRemoteModule::registerExports() +{ + globalIoc()->registerExport(moduleName(), new MidiRemoteStub()); + globalIoc()->registerExport(moduleName(), new MidiRemoteConfigurationStub()); +} diff --git a/src/framework/stubs/midiremote/midiremotestubmodule.h b/src/framework/stubs/midiremote/midiremotestubmodule.h new file mode 100644 index 0000000000000..53237061d9afd --- /dev/null +++ b/src/framework/stubs/midiremote/midiremotestubmodule.h @@ -0,0 +1,34 @@ +/* + * 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 "modularity/imodulesetup.h" + +namespace muse::midiremote { +class MidiRemoteModule : public modularity::IModuleSetup +{ +public: + std::string moduleName() const override; + void registerExports() override; +}; +} diff --git a/src/framework/stubs/shortcuts/CMakeLists.txt b/src/framework/stubs/shortcuts/CMakeLists.txt index 3a8e9211071b1..58869e18eee1f 100644 --- a/src/framework/stubs/shortcuts/CMakeLists.txt +++ b/src/framework/stubs/shortcuts/CMakeLists.txt @@ -29,8 +29,6 @@ target_sources(muse_shortcuts PRIVATE shortcutscontrollerstub.h shortcutsconfigurationstub.cpp shortcutsconfigurationstub.h - midiremotestub.cpp - midiremotestub.h ) if (MUSE_MODULE_SHORTCUTS_QML) diff --git a/src/framework/stubs/shortcuts/shortcutsconfigurationstub.cpp b/src/framework/stubs/shortcuts/shortcutsconfigurationstub.cpp index 1d8f266f9db82..5575114ebdf60 100644 --- a/src/framework/stubs/shortcuts/shortcutsconfigurationstub.cpp +++ b/src/framework/stubs/shortcuts/shortcutsconfigurationstub.cpp @@ -42,23 +42,3 @@ io::path_t ShortcutsConfigurationStub::shortcutsAppDataPath() const { return io::path_t(); } - -io::path_t ShortcutsConfigurationStub::midiMappingUserAppDataPath() const -{ - return io::path_t(); -} - -bool ShortcutsConfigurationStub::advanceToNextNoteOnKeyRelease() const -{ - return false; -} - -void ShortcutsConfigurationStub::setAdvanceToNextNoteOnKeyRelease(bool) -{ -} - -async::Channel ShortcutsConfigurationStub::advanceToNextNoteOnKeyReleaseChanged() const -{ - static async::Channel ch; - return ch; -} diff --git a/src/framework/stubs/shortcuts/shortcutsconfigurationstub.h b/src/framework/stubs/shortcuts/shortcutsconfigurationstub.h index 8ef3fc08da046..634cc22ce8158 100644 --- a/src/framework/stubs/shortcuts/shortcutsconfigurationstub.h +++ b/src/framework/stubs/shortcuts/shortcutsconfigurationstub.h @@ -19,8 +19,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef MUSE_SHORTCUTS_SHORTCUTSCONFIGURATIONSTUB_H -#define MUSE_SHORTCUTS_SHORTCUTSCONFIGURATIONSTUB_H +#pragma once #include "shortcuts/ishortcutsconfiguration.h" @@ -33,13 +32,5 @@ class ShortcutsConfigurationStub : public IShortcutsConfiguration io::path_t shortcutsUserAppDataPath() const override; io::path_t shortcutsAppDataPath() const override; - - io::path_t midiMappingUserAppDataPath() const override; - - bool advanceToNextNoteOnKeyRelease() const override; - void setAdvanceToNextNoteOnKeyRelease(bool value) override; - muse::async::Channel advanceToNextNoteOnKeyReleaseChanged() const override; }; } - -#endif // MUSE_SHORTCUTS_SHORTCUTSCONFIGURATIONSTUB_H diff --git a/src/framework/stubs/shortcuts/shortcutsstubmodule.cpp b/src/framework/stubs/shortcuts/shortcutsstubmodule.cpp index ee0cb863efc82..6b09b5f9639b3 100644 --- a/src/framework/stubs/shortcuts/shortcutsstubmodule.cpp +++ b/src/framework/stubs/shortcuts/shortcutsstubmodule.cpp @@ -25,7 +25,6 @@ #include "shortcutsregisterstub.h" #include "shortcutscontrollerstub.h" -#include "midiremotestub.h" #include "shortcutsconfigurationstub.h" using namespace muse::shortcuts; @@ -40,6 +39,5 @@ void ShortcutsModule::registerExports() { globalIoc()->registerExport(moduleName(), new ShortcutsRegisterStub()); globalIoc()->registerExport(moduleName(), new ShortcutsControllerStub()); - globalIoc()->registerExport(moduleName(), new MidiRemoteStub()); globalIoc()->registerExport(moduleName(), new ShortcutsConfigurationStub()); } diff --git a/src/framework/uicomponents/qml/Muse/UiComponents/CMakeLists.txt b/src/framework/uicomponents/qml/Muse/UiComponents/CMakeLists.txt index 6877c2434089f..bbdaf419d3a4b 100644 --- a/src/framework/uicomponents/qml/Muse/UiComponents/CMakeLists.txt +++ b/src/framework/uicomponents/qml/Muse/UiComponents/CMakeLists.txt @@ -186,11 +186,14 @@ fixup_qml_module_dependencies(muse_uicomponents_qml) if (OS_IS_MAC) target_sources(muse_uicomponents_qml PRIVATE + internal/platform/macos/macoschildwindowcontroller.mm + internal/platform/macos/macoschildwindowcontroller.h internal/platform/macos/macospopupviewclosecontroller.mm internal/platform/macos/macospopupviewclosecontroller.h ) set_source_files_properties( + internal/platform/macos/macoschildwindowcontroller.mm internal/platform/macos/macospopupviewclosecontroller.mm PROPERTIES SKIP_UNITY_BUILD_INCLUSION ON diff --git a/src/framework/uicomponents/qml/Muse/UiComponents/dialogview.cpp b/src/framework/uicomponents/qml/Muse/UiComponents/dialogview.cpp index 0bc791092bec9..2207527f7b466 100644 --- a/src/framework/uicomponents/qml/Muse/UiComponents/dialogview.cpp +++ b/src/framework/uicomponents/qml/Muse/UiComponents/dialogview.cpp @@ -27,6 +27,10 @@ #include #include +#ifdef Q_OS_MAC +#include "internal/platform/macos/macoschildwindowcontroller.h" +#endif + #include "log.h" using namespace Qt::Literals::StringLiterals; @@ -83,17 +87,6 @@ void DialogView::beforeOpen() m_view->setTitle(m_title); m_view->setFlag(Qt::FramelessWindowHint, m_frameless); -#ifdef Q_OS_MAC - if (m_alwaysOnTop) { - auto updateStayOnTopHint = [this]() { - bool stay = qApp->applicationState() == Qt::ApplicationActive; - m_view->setFlag(Qt::WindowStaysOnTopHint, stay); - }; - updateStayOnTopHint(); - connect(qApp, &QApplication::applicationStateChanged, this, updateStayOnTopHint); - } -#endif - #ifdef MUSE_MODULE_UI_DISABLE_MODALITY m_view->setModality(Qt::NonModal); #else @@ -108,11 +101,26 @@ void DialogView::beforeOpen() void DialogView::onHidden() { +#ifdef Q_OS_MAC + if (m_nativeChildWindow) { + MacOSChildWindowController::detachWindow(m_view); + } +#endif + WindowView::onHidden(); windowsController()->unregWindow(m_view->winId()); } +void DialogView::afterShow() +{ +#ifdef Q_OS_MAC + if (m_nativeChildWindow) { + MacOSChildWindowController::attachWindow(m_view, m_parentWindow); + } +#endif +} + void DialogView::updateGeometry() { QRect anchorRect = currentScreenGeometry(); @@ -255,19 +263,19 @@ void DialogView::setResizable(bool resizable) } } -bool DialogView::alwaysOnTop() const +bool DialogView::nativeChildWindow() const { - return m_alwaysOnTop; + return m_nativeChildWindow; } -void DialogView::setAlwaysOnTop(bool alwaysOnTop) +void DialogView::setNativeChildWindow(bool nativeChildWindow) { - if (m_alwaysOnTop == alwaysOnTop) { + if (m_nativeChildWindow == nativeChildWindow) { return; } - m_alwaysOnTop = alwaysOnTop; - emit alwaysOnTopChanged(); + m_nativeChildWindow = nativeChildWindow; + emit nativeChildWindowChanged(); } QVariantMap DialogView::ret() const diff --git a/src/framework/uicomponents/qml/Muse/UiComponents/dialogview.h b/src/framework/uicomponents/qml/Muse/UiComponents/dialogview.h index 13392dd45ae65..51a0938abc101 100644 --- a/src/framework/uicomponents/qml/Muse/UiComponents/dialogview.h +++ b/src/framework/uicomponents/qml/Muse/UiComponents/dialogview.h @@ -43,7 +43,7 @@ class DialogView : public WindowView Q_PROPERTY(bool modal READ modal WRITE setModal NOTIFY modalChanged) Q_PROPERTY(bool frameless READ frameless WRITE setFrameless NOTIFY framelessChanged) Q_PROPERTY(bool resizable READ resizable WRITE setResizable NOTIFY resizableChanged) - Q_PROPERTY(bool alwaysOnTop READ alwaysOnTop WRITE setAlwaysOnTop NOTIFY alwaysOnTopChanged) + Q_PROPERTY(bool nativeChildWindow READ nativeChildWindow WRITE setNativeChildWindow NOTIFY nativeChildWindowChanged) Q_PROPERTY(QVariantMap ret READ ret WRITE setRet NOTIFY retChanged) GlobalInject application; @@ -68,8 +68,8 @@ class DialogView : public WindowView bool resizable() const; void setResizable(bool resizable); - bool alwaysOnTop() const; - void setAlwaysOnTop(bool alwaysOnTop); + bool nativeChildWindow() const; + void setNativeChildWindow(bool nativeChildWindow); QVariantMap ret() const; void setRet(QVariantMap ret); @@ -87,13 +87,14 @@ class DialogView : public WindowView void modalChanged(bool modal); void framelessChanged(bool frameless); void resizableChanged(bool resizable); - void alwaysOnTopChanged(); + void nativeChildWindowChanged(); void retChanged(QVariantMap ret); private: void initView() override; void beforeOpen() override; + void afterShow() override; void onHidden() override; void updateGeometry() override; @@ -103,7 +104,7 @@ class DialogView : public WindowView QString m_title; bool m_modal = true; bool m_frameless = false; - bool m_alwaysOnTop = false; + bool m_nativeChildWindow = false; QVariantMap m_ret; }; } diff --git a/src/framework/uicomponents/qml/Muse/UiComponents/internal/platform/macos/macoschildwindowcontroller.h b/src/framework/uicomponents/qml/Muse/UiComponents/internal/platform/macos/macoschildwindowcontroller.h new file mode 100644 index 0000000000000..0197738ada184 --- /dev/null +++ b/src/framework/uicomponents/qml/Muse/UiComponents/internal/platform/macos/macoschildwindowcontroller.h @@ -0,0 +1,33 @@ +/* + * 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 + +class QWindow; + +namespace muse::uicomponents { +class MacOSChildWindowController +{ +public: + static void attachWindow(QWindow* childWindow, QWindow* parentWindow); + static void detachWindow(QWindow* childWindow); +}; +} diff --git a/src/framework/uicomponents/qml/Muse/UiComponents/internal/platform/macos/macoschildwindowcontroller.mm b/src/framework/uicomponents/qml/Muse/UiComponents/internal/platform/macos/macoschildwindowcontroller.mm new file mode 100644 index 0000000000000..3c0ecaefc286e --- /dev/null +++ b/src/framework/uicomponents/qml/Muse/UiComponents/internal/platform/macos/macoschildwindowcontroller.mm @@ -0,0 +1,71 @@ +/* + * 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 "macoschildwindowcontroller.h" + +#include +#include + +using namespace muse::uicomponents; + +static NSWindow* nsWindowForQWindow(QWindow* window) +{ + if (!window) { + return nil; + } + + NSView* nsView = (__bridge NSView*)reinterpret_cast(window->winId()); + return [nsView window]; +} + +void MacOSChildWindowController::attachWindow(QWindow* childWindow, QWindow* parentWindow) +{ + NSWindow* child = nsWindowForQWindow(childWindow); + NSWindow* parent = nsWindowForQWindow(parentWindow); + if (!child || !parent || child == parent) { + return; + } + + NSWindow* currentParent = [child parentWindow]; + if (currentParent && currentParent != parent) { + [currentParent removeChildWindow:child]; + } + + [child orderFront:nil]; + + if ([child parentWindow] != parent) { + [parent addChildWindow:child ordered:NSWindowAbove]; + } +} + +void MacOSChildWindowController::detachWindow(QWindow* childWindow) +{ + NSWindow* child = nsWindowForQWindow(childWindow); + if (!child) { + return; + } + + NSWindow* parent = [child parentWindow]; + if (parent) { + [parent removeChildWindow:child]; + } +} diff --git a/src/framework/uicomponents/qml/Muse/UiComponents/windowview.cpp b/src/framework/uicomponents/qml/Muse/UiComponents/windowview.cpp index 779840f05779a..aadc7cd4dc5b7 100644 --- a/src/framework/uicomponents/qml/Muse/UiComponents/windowview.cpp +++ b/src/framework/uicomponents/qml/Muse/UiComponents/windowview.cpp @@ -197,6 +197,7 @@ void WindowView::showView() }); m_view->show(); + afterShow(); const QQuickItem* item = m_view->rootObject(); if (item) { diff --git a/src/framework/uicomponents/qml/Muse/UiComponents/windowview.h b/src/framework/uicomponents/qml/Muse/UiComponents/windowview.h index 8e3c77c81922a..117fbb2e85d11 100644 --- a/src/framework/uicomponents/qml/Muse/UiComponents/windowview.h +++ b/src/framework/uicomponents/qml/Muse/UiComponents/windowview.h @@ -173,6 +173,7 @@ public slots: void setViewContent(QQuickItem* item); virtual void beforeOpen(); + virtual void afterShow() {} void doOpen(); void showView(); diff --git a/src/framework/vst/internal/fx/vstfxprocessor.cpp b/src/framework/vst/internal/fx/vstfxprocessor.cpp index 200d8a507d909..4dd52736613d8 100644 --- a/src/framework/vst/internal/fx/vstfxprocessor.cpp +++ b/src/framework/vst/internal/fx/vstfxprocessor.cpp @@ -27,7 +27,7 @@ using namespace muse::vst; using namespace muse::audio; using namespace muse::audioplugins; -VstFxProcessor::VstFxProcessor(IVstPluginInstancePtr&& instance, const AudioFxParams& params) +VstFxProcessor::VstFxProcessor(IVstPluginInstancePtr instance, const AudioFxParams& params) : m_pluginPtr(instance), m_vstAudioClient(std::make_unique()), m_params(params) @@ -100,6 +100,16 @@ void VstFxProcessor::setActive(bool active) m_params.active = active; } +void VstFxProcessor::setPlaying(bool playing) +{ + m_vstAudioClient->setIsPlaying(playing); +} + +bool VstFxProcessor::shouldProcessDuringSilence() const +{ + return m_params.active && muse::contains(m_params.categories, AudioFxCategory::FxGenerator); +} + void VstFxProcessor::process(float* buffer, unsigned int sampleCount, msecs_t playbackPosition) { if (!buffer || !m_inited) { diff --git a/src/framework/vst/internal/fx/vstfxprocessor.h b/src/framework/vst/internal/fx/vstfxprocessor.h index 24f8174708f1b..46e4d19ea0071 100644 --- a/src/framework/vst/internal/fx/vstfxprocessor.h +++ b/src/framework/vst/internal/fx/vstfxprocessor.h @@ -35,16 +35,24 @@ namespace muse::vst { class VstFxProcessor : public muse::audio::IFxProcessor, public async::Asyncable { public: - explicit VstFxProcessor(IVstPluginInstancePtr&& instance, const muse::audio::AudioFxParams& params); + explicit VstFxProcessor(IVstPluginInstancePtr instance, const muse::audio::AudioFxParams& params); void init(const audio::OutputSpec& spec); muse::audio::AudioFxType type() const override; + const muse::audio::AudioFxParams& params() const override; async::Channel paramsChanged() const override; + void setOutputSpec(const audio::OutputSpec& spec) override; + bool active() const override; void setActive(bool active) override; + + void setPlaying(bool playing) override; + + bool shouldProcessDuringSilence() const override; + void process(float* buffer, unsigned int sampleCount, muse::audio::msecs_t playbackPosition = 0) override; private: diff --git a/src/framework/vst/internal/fx/vstfxresolver.cpp b/src/framework/vst/internal/fx/vstfxresolver.cpp index 8f82d3ec665b3..58cbf348973f6 100644 --- a/src/framework/vst/internal/fx/vstfxresolver.cpp +++ b/src/framework/vst/internal/fx/vstfxresolver.cpp @@ -55,7 +55,7 @@ IFxProcessorPtr VstFxResolver::createMasterFx(const AudioFxParams& fxParams, con IVstPluginInstancePtr pluginPtr = instancesRegister()->makeAndRegisterMasterFxPlugin(fxParams.resourceMeta.id, fxParams.chainOrder); - std::shared_ptr fx = std::make_shared(std::move(pluginPtr), fxParams); + VstFxPtr fx = std::make_shared(pluginPtr, fxParams); fx->init(outputSpec); return fx; @@ -73,7 +73,7 @@ IFxProcessorPtr VstFxResolver::createTrackFx(const TrackId trackId, const AudioF IVstPluginInstancePtr pluginPtr = instancesRegister()->makeAndRegisterFxPlugin(fxParams.resourceMeta.id, trackId, fxParams.chainOrder); - std::shared_ptr fx = std::make_shared(std::move(pluginPtr), fxParams); + VstFxPtr fx = std::make_shared(pluginPtr, fxParams); fx->init(outputSpec); return fx; diff --git a/src/framework/vst/internal/synth/vstsynthesiser.cpp b/src/framework/vst/internal/synth/vstsynthesiser.cpp index dd1c5346a8163..0ef00c20f46c8 100644 --- a/src/framework/vst/internal/synth/vstsynthesiser.cpp +++ b/src/framework/vst/internal/synth/vstsynthesiser.cpp @@ -161,9 +161,14 @@ bool VstSynthesiser::isActive() const void VstSynthesiser::setIsActive(const bool isActive) { + if (m_sequencer.isActive() == isActive) { + return; + } + m_sequencer.setActive(isActive); toggleVolumeGain(isActive); m_vstAudioClient->setIsActive(isActive); + m_vstAudioClient->setIsPlaying(isActive); } muse::audio::msecs_t VstSynthesiser::playbackPosition() const diff --git a/src/framework/vst/internal/vstactionscontroller.cpp b/src/framework/vst/internal/vstactionscontroller.cpp index cd9904dc4c9e9..ab033bfd5e59c 100644 --- a/src/framework/vst/internal/vstactionscontroller.cpp +++ b/src/framework/vst/internal/vstactionscontroller.cpp @@ -62,6 +62,7 @@ void VstActionsController::fxEditor(const actions::ActionQuery& actionQuery) int trackId = actionQuery.param("trackId", Val(-1)).toInt(); int chainOrder = actionQuery.param("chainOrder", Val(0)).toInt(); std::string operation = actionQuery.param("operation", Val("open")).toString(); + bool sync = actionQuery.param("sync", Val(false)).toBool(); auto instance = instancesRegister()->fxPlugin(resourceId, trackId, chainOrder); @@ -75,7 +76,7 @@ void VstActionsController::fxEditor(const actions::ActionQuery& actionQuery) return; } - editorOperation(operation, instance->id()); + editorOperation(operation, instance->id(), sync); } void VstActionsController::instEditor(const actions::ActionQuery& actionQuery) @@ -100,6 +101,7 @@ void VstActionsController::instEditor(const actions::ActionQuery& actionQuery) auto instance = instancesRegister()->instrumentPlugin(resourceId, trackId); std::string operation = actionQuery.param("operation", Val("open")).toString(); + bool sync = actionQuery.param("sync", Val(false)).toBool(); if (operation == "close" && !instance) { return; @@ -111,21 +113,28 @@ void VstActionsController::instEditor(const actions::ActionQuery& actionQuery) return; } - editorOperation(operation, instance->id()); + editorOperation(operation, instance->id(), sync); } -void VstActionsController::editorOperation(const std::string& operation, int instanceId) +void VstActionsController::editorOperation(const std::string& operation, int instanceId, bool sync) { UriQuery editorUri = UriQuery(String(VST_EDITOR_URI).arg(instanceId)); if (operation == "close") { - interactive()->close(editorUri); - } else { - if (interactive()->isOpened(editorUri).val) { - interactive()->raise(editorUri); + if (sync) { + interactive()->closeSync(editorUri); } else { - interactive()->open(editorUri); + interactive()->close(editorUri); } + return; + } + + if (interactive()->isOpened(editorUri).val) { + interactive()->raise(editorUri); + } else if (sync) { + interactive()->openSync(editorUri); + } else { + interactive()->open(editorUri); } } diff --git a/src/framework/vst/internal/vstactionscontroller.h b/src/framework/vst/internal/vstactionscontroller.h index 9ad7855edbb4a..c8cac92e1e2c7 100644 --- a/src/framework/vst/internal/vstactionscontroller.h +++ b/src/framework/vst/internal/vstactionscontroller.h @@ -50,7 +50,7 @@ class VstActionsController : public actions::Actionable, public muse::Contextabl void fxEditor(const actions::ActionQuery& actionQuery); void instEditor(const actions::ActionQuery& actionQuery); - void editorOperation(const std::string& operation, int instanceId); + void editorOperation(const std::string& operation, int instanceId, bool sync); void setupUsedView(); void useView(bool isNew); diff --git a/src/framework/vst/internal/vstaudioclient.cpp b/src/framework/vst/internal/vstaudioclient.cpp index 6b0b4f3dc78fe..b1da9c7d60fc0 100644 --- a/src/framework/vst/internal/vstaudioclient.cpp +++ b/src/framework/vst/internal/vstaudioclient.cpp @@ -36,6 +36,11 @@ static size_t noteEventKey(int pitch, int channel) return h1 ^ (h2 << 1); } +VstAudioClient::VstAudioClient() +{ + m_processContext.state = 0; +} + VstAudioClient::~VstAudioClient() { if (!m_pluginComponent) { @@ -69,7 +74,8 @@ void VstAudioClient::loadSupportedParams() return; } - int paramCount = controller->getParameterCount(); + const int paramCount = controller->getParameterCount(); + m_pluginParamInfoMap.reserve(static_cast(paramCount)); for (int i = 0; i < paramCount; ++i) { PluginParamInfo info; @@ -89,6 +95,15 @@ void VstAudioClient::setIsActive(const bool isActive) } } +void VstAudioClient::setIsPlaying(const bool isPlaying) +{ + if (isPlaying) { + m_processContext.state |= static_cast(VstProcessContext::kPlaying); + } else { + m_processContext.state &= ~static_cast(VstProcessContext::kPlaying); + } +} + void VstAudioClient::setOutputSpec(const audio::OutputSpec& spec) { if (m_outputSpec == spec) { diff --git a/src/framework/vst/internal/vstaudioclient.h b/src/framework/vst/internal/vstaudioclient.h index 6ac990d34846f..bd06a80066926 100644 --- a/src/framework/vst/internal/vstaudioclient.h +++ b/src/framework/vst/internal/vstaudioclient.h @@ -30,13 +30,14 @@ namespace muse::vst { class VstAudioClient { public: - VstAudioClient() = default; + VstAudioClient(); ~VstAudioClient(); void init(audioplugins::AudioPluginType type, IVstPluginInstancePtr instance); void loadSupportedParams(); void setIsActive(const bool isActive); + void setIsPlaying(const bool isPlaying); void setOutputSpec(const audio::OutputSpec& spec); void setProcessMode(VstProcessMode mode); void setVolumeGain(const muse::audio::gain_t newVolumeGain); diff --git a/src/framework/vst/qml/Muse/Vst/VstEditorDialog.qml b/src/framework/vst/qml/Muse/Vst/VstEditorDialog.qml index bd2fc7a4bcd74..b7b31d0689123 100644 --- a/src/framework/vst/qml/Muse/Vst/VstEditorDialog.qml +++ b/src/framework/vst/qml/Muse/Vst/VstEditorDialog.qml @@ -33,7 +33,7 @@ StyledDialogView { contentHeight: editor.implicitHeight contentWidth: editor.implicitWidth - alwaysOnTop: true + nativeChildWindow: true VstEditor { id: editor diff --git a/src/notationscene/internal/midiinputoutputcontroller.h b/src/notationscene/internal/midiinputoutputcontroller.h index 4318fdeb12d54..8c80460985e5c 100644 --- a/src/notationscene/internal/midiinputoutputcontroller.h +++ b/src/notationscene/internal/midiinputoutputcontroller.h @@ -27,9 +27,9 @@ #include "midi/imidiconfiguration.h" #include "midi/imidiinport.h" #include "midi/imidioutport.h" +#include "midiremote/imidiremote.h" #include "modularity/ioc.h" #include "notation/inotationconfiguration.h" -#include "shortcuts/imidiremote.h" namespace mu::notation { class MidiInputOutputController : public muse::async::Asyncable, public muse::Contextable @@ -38,8 +38,8 @@ class MidiInputOutputController : public muse::async::Asyncable, public muse::Co muse::GlobalInject midiConfiguration; muse::GlobalInject midiInPort; muse::GlobalInject midiOutPort; + muse::ContextInject midiRemote = { this }; muse::ContextInject globalContext = { this }; - muse::ContextInject midiRemote = { this }; public: diff --git a/src/playback/internal/playbackcontroller.cpp b/src/playback/internal/playbackcontroller.cpp index c83fda3d6e5a1..03e1347bd1558 100644 --- a/src/playback/internal/playbackcontroller.cpp +++ b/src/playback/internal/playbackcontroller.cpp @@ -72,6 +72,7 @@ static AudioOutputParams makeReverbOutputParams() { AudioFxParams reverbParams; reverbParams.resourceMeta = makeReverbMeta(); + reverbParams.categories.insert(AudioFxCategory::FxReverb); reverbParams.chainOrder = 0; reverbParams.active = true; diff --git a/src/playback/qml/MuseScore/Playback/mixerchannelitem.cpp b/src/playback/qml/MuseScore/Playback/mixerchannelitem.cpp index 3de3135b14824..e47be1445ec14 100644 --- a/src/playback/qml/MuseScore/Playback/mixerchannelitem.cpp +++ b/src/playback/qml/MuseScore/Playback/mixerchannelitem.cpp @@ -645,6 +645,7 @@ void MixerChannelItem::openEditor(AbstractAudioResourceItem* item, const actions // make and send close actions::ActionQuery closeAction = item->editorAction(); closeAction.addParam("operation", Val("close")); + closeAction.addParam("sync", Val(true)); dispatcher()->dispatch(closeAction); } // set new action @@ -659,6 +660,7 @@ void MixerChannelItem::closeEditor(AbstractAudioResourceItem* item) // make and send close actions::ActionQuery closeAction = item->editorAction(); closeAction.addParam("operation", Val("close")); + closeAction.addParam("sync", Val(true)); dispatcher()->dispatch(closeAction); item->setEditorAction(UriQuery()); diff --git a/src/playback/qml/MuseScore/Playback/outputresourceitem.cpp b/src/playback/qml/MuseScore/Playback/outputresourceitem.cpp index 8ddd9f7fc00df..3fbf07813396d 100644 --- a/src/playback/qml/MuseScore/Playback/outputresourceitem.cpp +++ b/src/playback/qml/MuseScore/Playback/outputresourceitem.cpp @@ -2,6 +2,8 @@ #include +#include "audio/common/audioutils.h" + #include "log.h" #include "translation.h" #include "stringutils.h" @@ -181,6 +183,7 @@ void OutputResourceItem::updateCurrentFxParams(const AudioResourceMeta& newMeta) requestToCloseNativeEditorView(); audio::AudioFxParams newParams = m_currentFxParams; + newParams.categories = audio::audioFxCategoriesFromString(newMeta.attributeVal(audio::CATEGORIES_ATTRIBUTE)); newParams.resourceMeta = newMeta; newParams.active = newMeta.isValid(); diff --git a/src/preferences/qml/MuseScore/Preferences/MidiDeviceMappingPreferencesPage.qml b/src/preferences/qml/MuseScore/Preferences/MidiDeviceMappingPreferencesPage.qml index 50a8344be04f8..820b5daa448c0 100644 --- a/src/preferences/qml/MuseScore/Preferences/MidiDeviceMappingPreferencesPage.qml +++ b/src/preferences/qml/MuseScore/Preferences/MidiDeviceMappingPreferencesPage.qml @@ -22,7 +22,7 @@ import QtQuick import MuseScore.Preferences -import Muse.Shortcuts +import Muse.MidiRemote PreferencesPage { id: root diff --git a/src/preferences/qml/MuseScore/Preferences/noteinputpreferencesmodel.cpp b/src/preferences/qml/MuseScore/Preferences/noteinputpreferencesmodel.cpp index 22f31810e92a9..f3aadd11715ef 100644 --- a/src/preferences/qml/MuseScore/Preferences/noteinputpreferencesmodel.cpp +++ b/src/preferences/qml/MuseScore/Preferences/noteinputpreferencesmodel.cpp @@ -79,7 +79,7 @@ void NoteInputPreferencesModel::load() emit playPreviewNotesInInputByDurationChanged(playPreviewNotesInInputByDuration()); }); - shortcutsConfiguration()->advanceToNextNoteOnKeyReleaseChanged().onReceive(this, [this](bool value) { + midiRemoteConfiguration()->advanceToNextNoteOnKeyReleaseChanged().onReceive(this, [this](bool value) { emit advanceToNextNoteOnKeyReleaseChanged(value); }); @@ -159,7 +159,7 @@ bool NoteInputPreferencesModel::startNoteInputAtSelectedNoteRestWhenPressingMidi bool NoteInputPreferencesModel::advanceToNextNoteOnKeyRelease() const { - return shortcutsConfiguration()->advanceToNextNoteOnKeyRelease(); + return midiRemoteConfiguration()->advanceToNextNoteOnKeyRelease(); } int NoteInputPreferencesModel::delayBetweenNotesInRealTimeModeMilliseconds() const @@ -273,7 +273,7 @@ void NoteInputPreferencesModel::setAdvanceToNextNoteOnKeyRelease(bool value) return; } - shortcutsConfiguration()->setAdvanceToNextNoteOnKeyRelease(value); + midiRemoteConfiguration()->setAdvanceToNextNoteOnKeyRelease(value); } void NoteInputPreferencesModel::setDelayBetweenNotesInRealTimeModeMilliseconds(int delay) diff --git a/src/preferences/qml/MuseScore/Preferences/noteinputpreferencesmodel.h b/src/preferences/qml/MuseScore/Preferences/noteinputpreferencesmodel.h index 577cabcbd9916..930292f5510f4 100644 --- a/src/preferences/qml/MuseScore/Preferences/noteinputpreferencesmodel.h +++ b/src/preferences/qml/MuseScore/Preferences/noteinputpreferencesmodel.h @@ -29,7 +29,7 @@ #include "modularity/ioc.h" #include "engraving/iengravingconfiguration.h" -#include "shortcuts/ishortcutsconfiguration.h" +#include "midiremote/imidiremoteconfiguration.h" #include "notation/inotationconfiguration.h" #include "playback/iplaybackconfiguration.h" #include "ui/iuiactionsregister.h" @@ -79,7 +79,7 @@ class NoteInputPreferencesModel : public QObject, public muse::Contextable, publ Q_PROPERTY( bool autoUpdateFretboardDiagrams READ autoUpdateFretboardDiagrams WRITE setAutoUpdateFretboardDiagrams NOTIFY autoUpdateFretboardDiagramsChanged FINAL) - muse::GlobalInject shortcutsConfiguration; + muse::GlobalInject midiRemoteConfiguration; muse::GlobalInject notationConfiguration; muse::GlobalInject playbackConfiguration; muse::GlobalInject engravingConfiguration; diff --git a/src/project/internal/projectactionscontroller.cpp b/src/project/internal/projectactionscontroller.cpp index 055d490a6130f..0949dee00630a 100644 --- a/src/project/internal/projectactionscontroller.cpp +++ b/src/project/internal/projectactionscontroller.cpp @@ -725,7 +725,7 @@ bool ProjectActionsController::closeOpenedProject(bool goToHome) } if (result) { - interactive()->closeAllDialogs(); + interactive()->closeAllDialogsSync(); globalContext()->setCurrentProject(nullptr); if (goToHome) { diff --git a/src/project/internal/projectaudiosettings.cpp b/src/project/internal/projectaudiosettings.cpp index a51e01f522547..94e19cde604f2 100644 --- a/src/project/internal/projectaudiosettings.cpp +++ b/src/project/internal/projectaudiosettings.cpp @@ -380,6 +380,7 @@ AudioFxParams ProjectAudioSettings::fxParamsFromJson(const QJsonObject& object) result.chainOrder = static_cast(object.value("chainOrder").toInt()); result.resourceMeta = resourceMetaFromJson(object.value("resourceMeta").toObject()); result.configuration = unitConfigFromJson(object.value("unitConfiguration").toObject()); + result.categories = audioFxCategoriesFromString(result.resourceMeta.attributeVal(audio::CATEGORIES_ATTRIBUTE)); return result; }