From a652bf4324c93cd538e16fc63ffc520edaabe505 Mon Sep 17 00:00:00 2001 From: Casper Jeukendrup <48658420+cbjeukendrup@users.noreply.github.com> Date: Sun, 16 Aug 2026 01:35:58 +0200 Subject: [PATCH 1/2] Audio: don't prevent the system from sleeping while idle The audio driver is opened at startup and kept open for the whole session. On macOS, the system is prevented from going to idle sleep for as long as a process has audio IO running, regardless of whether that IO is audible, so this kept the Mac awake permanently. Tell the driver whether live playback is ongoing, and let the macOS drivers use that to allow idle sleep while nothing is being played. Co-Authored-By: Claude Opus 5 --- framework/audio/driver/CMakeLists.txt | 5 +- .../driver/platform/osx/osxaudiodriver.h | 3 + .../driver/platform/osx/osxaudiodriver.mm | 12 +++ .../platform/osx/osxdirectaudiodriver.h | 3 + .../platform/osx/osxdirectaudiodriver.mm | 11 +++ .../driver/platform/osx/osxidlesleeppolicy.h | 38 ++++++++ .../driver/platform/osx/osxidlesleeppolicy.mm | 92 +++++++++++++++++++ framework/audio/iaudiodriver.h | 6 ++ framework/audio/iaudiodrivercontroller.h | 3 + .../main/internal/audiodrivercontroller.cpp | 16 ++++ .../main/internal/audiodrivercontroller.h | 3 + framework/audio/main/internal/player.cpp | 38 ++++++++ framework/audio/main/internal/player.h | 6 ++ .../stubs/audio/audiodrivercontrollerstub.cpp | 4 + .../stubs/audio/audiodrivercontrollerstub.h | 2 + 15 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 framework/audio/driver/platform/osx/osxidlesleeppolicy.h create mode 100644 framework/audio/driver/platform/osx/osxidlesleeppolicy.mm diff --git a/framework/audio/driver/CMakeLists.txt b/framework/audio/driver/CMakeLists.txt index 442cb70cb7..6c02abbf82 100644 --- a/framework/audio/driver/CMakeLists.txt +++ b/framework/audio/driver/CMakeLists.txt @@ -79,11 +79,14 @@ elseif(OS_IS_MAC) platform/osx/osxaudiodriver.h platform/osx/osxdirectaudiodriver.mm platform/osx/osxdirectaudiodriver.h + platform/osx/osxidlesleeppolicy.mm + platform/osx/osxidlesleeppolicy.h ) - + set_source_files_properties( platform/osx/osxaudiodriver.mm platform/osx/osxdirectaudiodriver.mm + platform/osx/osxidlesleeppolicy.mm PROPERTIES SKIP_UNITY_BUILD_INCLUSION ON SKIP_PRECOMPILE_HEADERS ON diff --git a/framework/audio/driver/platform/osx/osxaudiodriver.h b/framework/audio/driver/platform/osx/osxaudiodriver.h index 74f27aedfe..f9a957f6f8 100644 --- a/framework/audio/driver/platform/osx/osxaudiodriver.h +++ b/framework/audio/driver/platform/osx/osxaudiodriver.h @@ -51,6 +51,8 @@ class OSXAudioDriver : public IAudioDriver void close() override; bool isOpened() const override; + void setLivePlaybackOngoing(bool ongoing) override; + const Spec& activeSpec() const override; async::Channel activeSpecChanged() const override; @@ -79,6 +81,7 @@ class OSXAudioDriver : public IAudioDriver std::map m_outputDevices = {}, m_inputDevices = {}; mutable std::mutex m_devicesMutex; async::Notification m_availableOutputDevicesChanged; + bool m_livePlaybackOngoing = false; }; } #endif // MUSE_AUDIO_OSXAUDIODRIVER_H diff --git a/framework/audio/driver/platform/osx/osxaudiodriver.mm b/framework/audio/driver/platform/osx/osxaudiodriver.mm index 79eae85c12..655e82cb98 100644 --- a/framework/audio/driver/platform/osx/osxaudiodriver.mm +++ b/framework/audio/driver/platform/osx/osxaudiodriver.mm @@ -26,6 +26,8 @@ #include +#include "osxidlesleeppolicy.h" + #include "translation.h" #include "log.h" @@ -165,6 +167,10 @@ void clear() return false; } + //! NOTE From now on the device is running, so make sure the system knows whether + //! it is allowed to go to sleep while that is the case + OSXIdleSleepPolicy::setPreventIdleSleep(m_livePlaybackOngoing); + m_activeSpecChanged.send(m_data->format); LOGI() << "Connected to " << m_data->format.deviceId @@ -193,6 +199,12 @@ void clear() return m_data->audioQueue != nullptr; } +void OSXAudioDriver::setLivePlaybackOngoing(bool ongoing) +{ + m_livePlaybackOngoing = ongoing; + OSXIdleSleepPolicy::setPreventIdleSleep(ongoing); +} + const OSXAudioDriver::Spec& OSXAudioDriver::activeSpec() const { return m_data->format; diff --git a/framework/audio/driver/platform/osx/osxdirectaudiodriver.h b/framework/audio/driver/platform/osx/osxdirectaudiodriver.h index 215d3bedd8..edec6885d7 100644 --- a/framework/audio/driver/platform/osx/osxdirectaudiodriver.h +++ b/framework/audio/driver/platform/osx/osxdirectaudiodriver.h @@ -53,6 +53,8 @@ class OSXDirectAudioDriver : public IAudioDriver void close() override; bool isOpened() const override; + void setLivePlaybackOngoing(bool ongoing) override; + const Spec& activeSpec() const override; async::Channel activeSpecChanged() const override; @@ -88,6 +90,7 @@ class OSXDirectAudioDriver : public IAudioDriver AudioWorkGroup m_audioWorkGroup; bool m_deviceMapListenerRegistered = false; + bool m_livePlaybackOngoing = false; }; } #endif // MUSE_AUDIO_OSXDIRECTAUDIODRIVER_H diff --git a/framework/audio/driver/platform/osx/osxdirectaudiodriver.mm b/framework/audio/driver/platform/osx/osxdirectaudiodriver.mm index fa2423569d..5008b05836 100644 --- a/framework/audio/driver/platform/osx/osxdirectaudiodriver.mm +++ b/framework/audio/driver/platform/osx/osxdirectaudiodriver.mm @@ -39,6 +39,7 @@ #include "common/audiotypes.h" #include "common/audioworkgroup.h" +#include "osxidlesleeppolicy.h" #include "translation.h" #include "log.h" @@ -668,6 +669,10 @@ AudioWorkGroup createAudioWorkgroup(OSXAudioDeviceID deviceId) return false; } + //! NOTE From now on the device is running, so make sure the system knows whether + //! it is allowed to go to sleep while that is the case + OSXIdleSleepPolicy::setPreventIdleSleep(m_livePlaybackOngoing); + m_audioWorkGroup = createAudioWorkgroup(*deviceId); m_currentWorkgroupChanged.notify(); @@ -717,6 +722,12 @@ AudioWorkGroup createAudioWorkgroup(OSXAudioDeviceID deviceId) return m_data->procId != nullptr; } +void OSXDirectAudioDriver::setLivePlaybackOngoing(bool ongoing) +{ + m_livePlaybackOngoing = ongoing; + OSXIdleSleepPolicy::setPreventIdleSleep(ongoing); +} + const OSXDirectAudioDriver::Spec& OSXDirectAudioDriver::activeSpec() const { return m_data->format; diff --git a/framework/audio/driver/platform/osx/osxidlesleeppolicy.h b/framework/audio/driver/platform/osx/osxidlesleeppolicy.h new file mode 100644 index 0000000000..ebe9ab62da --- /dev/null +++ b/framework/audio/driver/platform/osx/osxidlesleeppolicy.h @@ -0,0 +1,38 @@ +/* + * 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 + +namespace muse::audio { +//! NOTE macOS prevents the system from going to idle sleep for as long as a process has +//! audio IO running, no matter whether that IO is audible. Since we keep the audio device +//! open for the whole session, that would keep the Mac awake permanently, also when idle. +//! So we tell CoreAudio that idle sleep is fine, and only opt out of it while something +//! is actually being played back. +//! +//! The policy applies to the process as a whole, so it is shared by all drivers. +class OSXIdleSleepPolicy +{ +public: + static void setPreventIdleSleep(bool prevent); +}; +} diff --git a/framework/audio/driver/platform/osx/osxidlesleeppolicy.mm b/framework/audio/driver/platform/osx/osxidlesleeppolicy.mm new file mode 100644 index 0000000000..f0fb06d08a --- /dev/null +++ b/framework/audio/driver/platform/osx/osxidlesleeppolicy.mm @@ -0,0 +1,92 @@ +/* + * 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 "osxidlesleeppolicy.h" + +#include + +#include + +#include "log.h" + +using namespace muse::audio; + +//! NOTE The policy is set from the thread that starts and stops playback, but reapplied +//! from a CoreAudio owned thread, so reading it and applying it has to be serialized +static std::mutex s_mutex; +static bool s_preventIdleSleep = false; + +static void applyPolicy() +{ + std::lock_guard lock(s_mutex); + + //! NOTE 1 means: allow the CPU to idle sleep even if there is audio IO in progress + UInt32 sleepingIsAllowed = s_preventIdleSleep ? 0 : 1; + + AudioObjectPropertyAddress address = { + .mSelector = kAudioHardwarePropertySleepingIsAllowed, + .mScope = kAudioObjectPropertyScopeGlobal, + .mElement = kAudioObjectPropertyElementMain + }; + + OSStatus result = AudioObjectSetPropertyData(kAudioObjectSystemObject, &address, 0, nullptr, + sizeof(sleepingIsAllowed), &sleepingIsAllowed); + if (result != noErr) { + LOGE() << "Failed to set idle sleep policy, err: " << result; + } +} + +static OSStatus onServiceRestarted(AudioObjectID, UInt32, const AudioObjectPropertyAddress*, void*) +{ + //! NOTE The policy belongs to our HAL client, which is recreated when coreaudiod restarts + applyPolicy(); + return noErr; +} + +static void initServiceRestartListener() +{ + static std::once_flag onceFlag; + std::call_once(onceFlag, []() { + AudioObjectPropertyAddress address = { + .mSelector = kAudioHardwarePropertyServiceRestarted, + .mScope = kAudioObjectPropertyScopeGlobal, + .mElement = kAudioObjectPropertyElementMain + }; + + OSStatus result = AudioObjectAddPropertyListener(kAudioObjectSystemObject, &address, &onServiceRestarted, nullptr); + if (result != noErr) { + LOGE() << "Failed to add service restart listener, err: " << result; + } + }); +} + +void OSXIdleSleepPolicy::setPreventIdleSleep(bool prevent) +{ + initServiceRestartListener(); + + { + std::lock_guard lock(s_mutex); + s_preventIdleSleep = prevent; + } + + applyPolicy(); +} diff --git a/framework/audio/iaudiodriver.h b/framework/audio/iaudiodriver.h index b10d139c65..d0160511e0 100644 --- a/framework/audio/iaudiodriver.h +++ b/framework/audio/iaudiodriver.h @@ -61,6 +61,12 @@ class IAudioDriver virtual void close() = 0; virtual bool isOpened() const = 0; + //! NOTE Tells the driver whether audio is currently being played back in real time. + //! The driver is kept open for the whole session and simply receives silence when + //! nothing is playing, so it cannot tell the difference on its own. + //! Drivers may use this to adjust platform specific behaviour; ignored by default. + virtual void setLivePlaybackOngoing(bool ongoing) { (void)ongoing; } + virtual const Spec& activeSpec() const = 0; virtual async::Channel activeSpecChanged() const = 0; diff --git a/framework/audio/iaudiodrivercontroller.h b/framework/audio/iaudiodrivercontroller.h index d3d2f630e1..a9fbb14433 100644 --- a/framework/audio/iaudiodrivercontroller.h +++ b/framework/audio/iaudiodrivercontroller.h @@ -48,6 +48,9 @@ class IAudioDriverController : MODULE_GLOBAL_INTERFACE virtual void close() = 0; virtual bool isOpened() const = 0; + //! NOTE See IAudioDriver::setLivePlaybackOngoing + virtual void setLivePlaybackOngoing(bool ongoing) = 0; + virtual const IAudioDriver::Spec& activeSpec() const = 0; virtual async::Channel activeSpecChanged() const = 0; diff --git a/framework/audio/main/internal/audiodrivercontroller.cpp b/framework/audio/main/internal/audiodrivercontroller.cpp index 64ed274120..2c2ed5525b 100644 --- a/framework/audio/main/internal/audiodrivercontroller.cpp +++ b/framework/audio/main/internal/audiodrivercontroller.cpp @@ -178,6 +178,9 @@ void AudioDriverController::setNewDriver(IAudioDriverPtr newDriver) m_audioDriver = newDriver; if (m_audioDriver) { + //! NOTE The new driver doesn't know yet what is going on + m_audioDriver->setLivePlaybackOngoing(m_livePlaybackOngoing); + // subscribe m_audioDriver->availableOutputDevicesChanged().onNotify(this, [this]() { async::Async::call(this, [this]() { @@ -317,6 +320,19 @@ bool AudioDriverController::isOpened() const return m_audioDriver->isOpened(); } +void AudioDriverController::setLivePlaybackOngoing(bool ongoing) +{ + if (m_livePlaybackOngoing == ongoing) { + return; + } + + m_livePlaybackOngoing = ongoing; + + if (m_audioDriver) { + m_audioDriver->setLivePlaybackOngoing(ongoing); + } +} + const IAudioDriver::Spec& AudioDriverController::activeSpec() const { IF_ASSERT_FAILED(m_audioDriver) { diff --git a/framework/audio/main/internal/audiodrivercontroller.h b/framework/audio/main/internal/audiodrivercontroller.h index 7a6260858b..965177b661 100644 --- a/framework/audio/main/internal/audiodrivercontroller.h +++ b/framework/audio/main/internal/audiodrivercontroller.h @@ -54,6 +54,8 @@ class AudioDriverController : public IAudioDriverController, public async::Async void close() override; bool isOpened() const override; + void setLivePlaybackOngoing(bool ongoing) override; + const IAudioDriver::Spec& activeSpec() const override; async::Channel activeSpecChanged() const override; @@ -91,5 +93,6 @@ class AudioDriverController : public IAudioDriverController, public async::Async async::Notification m_outputDeviceSampleRateChanged; bool m_retryOpenDevice = false; + bool m_livePlaybackOngoing = false; }; } diff --git a/framework/audio/main/internal/player.cpp b/framework/audio/main/internal/player.cpp index afe9ebbe6a..a2905d201c 100644 --- a/framework/audio/main/internal/player.cpp +++ b/framework/audio/main/internal/player.cpp @@ -21,6 +21,8 @@ */ #include "player.h" +#include + #include "audio/common/rpc/rpcpacker.h" #include "audio/common/audiosanitizer.h" @@ -31,11 +33,37 @@ using namespace muse::async; using namespace muse::audio; using namespace muse::audio::rpc; +//! NOTE Playback is contextual (each window has its own), and every context can hand out +//! more than one player, so live playback is ongoing as long as any of them is running. +static std::set& runningPlayers() +{ + static std::set s_runningPlayers; + return s_runningPlayers; +} + +Player::~Player() +{ + if (runningPlayers().erase(this) != 0) { + updateLivePlaybackOngoing(); + } +} + rpc::CtxId Player::ctxId() const { return rpc::ctxId(iocContext()); } +void Player::updateLivePlaybackOngoing() +{ + ONLY_AUDIO_MAIN_THREAD; + + //! NOTE Can already be gone on shutdown + const std::shared_ptr& controller = audioDriverController.get(); + if (controller) { + controller->setLivePlaybackOngoing(!runningPlayers().empty()); + } +} + void Player::init() { ONLY_AUDIO_MAIN_THREAD; @@ -45,6 +73,16 @@ void Player::init() { m_playbackStatusChanged.onReceive(this, [this](PlaybackStatus st) { m_playbackStatus = st; + + //! NOTE Only while running is audio actually being produced; + //! when paused or stopped there is nothing to keep the system awake for + if (m_playbackStatus == PlaybackStatus::Running) { + runningPlayers().insert(this); + } else { + runningPlayers().erase(this); + } + + updateLivePlaybackOngoing(); }); Msg msg = rpc::make_request(ctxId(), MsgCode::GetPlaybackStatus); diff --git a/framework/audio/main/internal/player.h b/framework/audio/main/internal/player.h index 0ae06a14d3..0f50a01b25 100644 --- a/framework/audio/main/internal/player.h +++ b/framework/audio/main/internal/player.h @@ -29,16 +29,20 @@ #include "modularity/ioc.h" #include "audio/common/rpc/irpcchannel.h" +#include "audio/iaudiodrivercontroller.h" namespace muse::audio { class Player : public IPlayer, public Contextable, public async::Asyncable { GlobalInject channel; + GlobalInject audioDriverController; public: Player(const muse::modularity::ContextPtr& ctx) : Contextable(ctx) {} + ~Player() override; + void init(); async::Promise prepareToPlay() override; @@ -63,6 +67,8 @@ class Player : public IPlayer, public Contextable, public async::Asyncable rpc::CtxId ctxId() const; + void updateLivePlaybackOngoing(); + PlaybackStatus m_playbackStatus = PlaybackStatus::Stopped; async::Channel m_playbackStatusChanged; diff --git a/framework/stubs/audio/audiodrivercontrollerstub.cpp b/framework/stubs/audio/audiodrivercontrollerstub.cpp index 8c707018e9..2733138c4e 100644 --- a/framework/stubs/audio/audiodrivercontrollerstub.cpp +++ b/framework/stubs/audio/audiodrivercontrollerstub.cpp @@ -69,6 +69,10 @@ bool AudioDriverControllerStub::isOpened() const return false; } +void AudioDriverControllerStub::setLivePlaybackOngoing(bool) +{ +} + const IAudioDriver::Spec& AudioDriverControllerStub::activeSpec() const { static IAudioDriver::Spec spec; diff --git a/framework/stubs/audio/audiodrivercontrollerstub.h b/framework/stubs/audio/audiodrivercontrollerstub.h index a6516f23fc..c38d293184 100644 --- a/framework/stubs/audio/audiodrivercontrollerstub.h +++ b/framework/stubs/audio/audiodrivercontrollerstub.h @@ -43,6 +43,8 @@ class AudioDriverControllerStub : public IAudioDriverController void close() override; bool isOpened() const override; + void setLivePlaybackOngoing(bool ongoing) override; + const IAudioDriver::Spec& activeSpec() const override; async::Channel activeSpecChanged() const override; From 40c279be7f33128e1b1f772263614f7c3bdf1836 Mon Sep 17 00:00:00 2001 From: Casper Jeukendrup <48658420+cbjeukendrup@users.noreply.github.com> Date: Sun, 16 Aug 2026 01:49:26 +0200 Subject: [PATCH 2/2] Global: add IdleSleepBlocker Prevents the system from going to idle sleep while it exists, so that long running tasks that are neither audible nor interactive don't get suspended halfway through. Implemented with IOPMAssertion on macOS, PowerRequest on Windows and a login1 idle inhibitor on Linux; other platforms get a stub. Co-Authored-By: Claude Opus 5 --- framework/global/CMakeLists.txt | 29 ++++++++ framework/global/idlesleepblocker.h | 56 +++++++++++++++ .../platform/linux/idlesleepblocker.cpp | 68 ++++++++++++++++++ .../platform/macos/idlesleepblocker.cpp | 63 ++++++++++++++++ .../platform/stub/idlesleepblocker.cpp | 35 +++++++++ .../platform/windows/idlesleepblocker.cpp | 72 +++++++++++++++++++ 6 files changed, 323 insertions(+) create mode 100644 framework/global/idlesleepblocker.h create mode 100644 framework/global/internal/platform/linux/idlesleepblocker.cpp create mode 100644 framework/global/internal/platform/macos/idlesleepblocker.cpp create mode 100644 framework/global/internal/platform/stub/idlesleepblocker.cpp create mode 100644 framework/global/internal/platform/windows/idlesleepblocker.cpp diff --git a/framework/global/CMakeLists.txt b/framework/global/CMakeLists.txt index ebd5e9a810..c3b1ffebf5 100644 --- a/framework/global/CMakeLists.txt +++ b/framework/global/CMakeLists.txt @@ -58,6 +58,7 @@ target_sources(muse_global PRIVATE timer.cpp timer.h progress.h + idlesleepblocker.h utils.cpp utils.h interpolation.h @@ -243,6 +244,34 @@ else() endif() endif() +#! NOTE IdleSleepBlocker is always declared, so it always needs an implementation +include(GetPlatformInfo) + +if (GLOBAL_NO_INTERNAL) + target_sources(muse_global PRIVATE + internal/platform/stub/idlesleepblocker.cpp + ) +elseif (OS_IS_MAC) + target_sources(muse_global PRIVATE + internal/platform/macos/idlesleepblocker.cpp + ) + find_library(IOKit NAMES IOKit) + target_link_libraries(muse_global PRIVATE ${IOKit}) +elseif (OS_IS_WIN) + target_sources(muse_global PRIVATE + internal/platform/windows/idlesleepblocker.cpp + ) +elseif ((OS_IS_LIN OR OS_IS_FBSD) AND MUSE_QT_SUPPORT) + target_sources(muse_global PRIVATE + internal/platform/linux/idlesleepblocker.cpp + ) + target_link_libraries(muse_global PRIVATE Qt::DBus) +else() + target_sources(muse_global PRIVATE + internal/platform/stub/idlesleepblocker.cpp + ) +endif() + include(GetCompilerInfo) if (NOT CC_IS_EMCC) #zlib included in main linker target_link_libraries(muse_global PRIVATE zlib::zlib) diff --git a/framework/global/idlesleepblocker.h b/framework/global/idlesleepblocker.h new file mode 100644 index 0000000000..cd9c35321c --- /dev/null +++ b/framework/global/idlesleepblocker.h @@ -0,0 +1,56 @@ +/* + * 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 + +namespace muse { +//! NOTE Prevents the system from going to idle sleep for as long as this object exists. +//! Meant for tasks that can take a long time while the user is not interacting with the +//! application and nothing is audible, like exporting an audio file: without this, the +//! system can suspend halfway through such a task. +//! +//! It does not prevent the display from sleeping, and it does not prevent sleep that the +//! user or the system asks for explicitly (closing the lid, low battery, etc.). +//! +//! Blocking is best effort: if the platform refuses, or has no way to express this, the +//! failure is logged and the object simply does nothing. +class IdleSleepBlocker +{ +public: + //! NOTE `reason` is shown to the user by some systems, so it should describe the task + //! being performed, and be translated + explicit IdleSleepBlocker(const std::string& reason); + ~IdleSleepBlocker(); + + IdleSleepBlocker(const IdleSleepBlocker&) = delete; + IdleSleepBlocker& operator=(const IdleSleepBlocker&) = delete; + +private: + struct Data; + std::unique_ptr m_data; +}; + +using IdleSleepBlockerPtr = std::shared_ptr; +} diff --git a/framework/global/internal/platform/linux/idlesleepblocker.cpp b/framework/global/internal/platform/linux/idlesleepblocker.cpp new file mode 100644 index 0000000000..c99f42a14b --- /dev/null +++ b/framework/global/internal/platform/linux/idlesleepblocker.cpp @@ -0,0 +1,68 @@ +/* + * 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 "../../../idlesleepblocker.h" + +#include +#include +#include +#include +#include + +#include "log.h" + +using namespace muse; + +struct IdleSleepBlocker::Data { + //! NOTE Holding the descriptor is what holds the inhibitor; closing it releases it + QDBusUnixFileDescriptor descriptor; +}; + +IdleSleepBlocker::IdleSleepBlocker(const std::string& reason) + : m_data(std::make_unique()) +{ + QDBusInterface manager("org.freedesktop.login1", + "/org/freedesktop/login1", + "org.freedesktop.login1.Manager", + QDBusConnection::systemBus()); + + if (!manager.isValid()) { + LOGW() << "Failed to connect to login1, the system may go to sleep"; + return; + } + + QDBusReply reply = manager.call("Inhibit", + QStringLiteral("idle"), + QCoreApplication::applicationName(), + QString::fromStdString(reason), + QStringLiteral("block")); + + if (!reply.isValid()) { + LOGW() << "Failed to inhibit idle sleep: " << reply.error().message().toStdString() + << ", the system may go to sleep"; + return; + } + + m_data->descriptor = reply.value(); +} + +IdleSleepBlocker::~IdleSleepBlocker() = default; diff --git a/framework/global/internal/platform/macos/idlesleepblocker.cpp b/framework/global/internal/platform/macos/idlesleepblocker.cpp new file mode 100644 index 0000000000..55a11affbd --- /dev/null +++ b/framework/global/internal/platform/macos/idlesleepblocker.cpp @@ -0,0 +1,63 @@ +/* + * 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 "../../../idlesleepblocker.h" + +#include +#include + +#include "log.h" + +using namespace muse; + +struct IdleSleepBlocker::Data { + IOPMAssertionID assertionId = kIOPMNullAssertionID; +}; + +IdleSleepBlocker::IdleSleepBlocker(const std::string& reason) + : m_data(std::make_unique()) +{ + CFStringRef reasonRef = CFStringCreateWithCString(kCFAllocatorDefault, reason.c_str(), kCFStringEncodingUTF8); + if (!reasonRef) { + LOGW() << "Failed to create reason string, the system may go to sleep"; + return; + } + + IOPMAssertionID assertionId = kIOPMNullAssertionID; + IOReturn result = IOPMAssertionCreateWithName(kIOPMAssertionTypePreventUserIdleSystemSleep, + kIOPMAssertionLevelOn, reasonRef, &assertionId); + CFRelease(reasonRef); + + if (result != kIOReturnSuccess) { + LOGW() << "Failed to create power assertion, err: " << result << ", the system may go to sleep"; + return; + } + + m_data->assertionId = assertionId; +} + +IdleSleepBlocker::~IdleSleepBlocker() +{ + if (m_data->assertionId != kIOPMNullAssertionID) { + IOPMAssertionRelease(m_data->assertionId); + } +} diff --git a/framework/global/internal/platform/stub/idlesleepblocker.cpp b/framework/global/internal/platform/stub/idlesleepblocker.cpp new file mode 100644 index 0000000000..c83720c8d0 --- /dev/null +++ b/framework/global/internal/platform/stub/idlesleepblocker.cpp @@ -0,0 +1,35 @@ +/* + * 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 "../../../idlesleepblocker.h" + +using namespace muse; + +struct IdleSleepBlocker::Data { +}; + +IdleSleepBlocker::IdleSleepBlocker(const std::string&) + : m_data(std::make_unique()) +{ +} + +IdleSleepBlocker::~IdleSleepBlocker() = default; diff --git a/framework/global/internal/platform/windows/idlesleepblocker.cpp b/framework/global/internal/platform/windows/idlesleepblocker.cpp new file mode 100644 index 0000000000..b18c5a2a18 --- /dev/null +++ b/framework/global/internal/platform/windows/idlesleepblocker.cpp @@ -0,0 +1,72 @@ +/* + * 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 "../../../idlesleepblocker.h" + +#include + +#include "log.h" + +using namespace muse; + +struct IdleSleepBlocker::Data { + //! NOTE Kept alive because the reason string is referenced by the request, not copied + std::wstring reason; + HANDLE request = INVALID_HANDLE_VALUE; +}; + +IdleSleepBlocker::IdleSleepBlocker(const std::string& reason) + : m_data(std::make_unique()) +{ + int size = MultiByteToWideChar(CP_UTF8, 0, reason.c_str(), static_cast(reason.size()), nullptr, 0); + m_data->reason.resize(static_cast(size)); + MultiByteToWideChar(CP_UTF8, 0, reason.c_str(), static_cast(reason.size()), m_data->reason.data(), size); + + REASON_CONTEXT context = {}; + context.Version = POWER_REQUEST_CONTEXT_VERSION; + context.Flags = POWER_REQUEST_CONTEXT_SIMPLE_STRING; + context.Reason.SimpleReasonString = m_data->reason.data(); + + HANDLE request = PowerCreateRequest(&context); + if (request == INVALID_HANDLE_VALUE) { + LOGW() << "Failed to create power request, err: " << GetLastError() << ", the system may go to sleep"; + return; + } + + if (!PowerSetRequest(request, PowerRequestSystemRequired)) { + LOGW() << "Failed to set power request, err: " << GetLastError() << ", the system may go to sleep"; + CloseHandle(request); + return; + } + + m_data->request = request; +} + +IdleSleepBlocker::~IdleSleepBlocker() +{ + if (m_data->request == INVALID_HANDLE_VALUE) { + return; + } + + PowerClearRequest(m_data->request, PowerRequestSystemRequired); + CloseHandle(m_data->request); +}