diff --git a/framework/audio/common/CMakeLists.txt b/framework/audio/common/CMakeLists.txt
index 14ff38cce5..ca9237da11 100644
--- a/framework/audio/common/CMakeLists.txt
+++ b/framework/audio/common/CMakeLists.txt
@@ -33,6 +33,13 @@ target_sources(muse_audio_common PRIVATE
iaudiothreadsecurer.h
audiothreadsecurer.cpp
audiothreadsecurer.h
+ audioworkgroup.h
+ audioworkgroup.cpp
+ audiotaskscheduler.h
+ iaudiotaskscheduler.h
+ realtimethreadpool.h
+ concurrentqueue.h
+ lightweightsemaphore.h
workmode.cpp
workmode.h
alignmentbuffer.h
diff --git a/framework/audio/common/audiotaskscheduler.h b/framework/audio/common/audiotaskscheduler.h
new file mode 100644
index 0000000000..dac2c2716f
--- /dev/null
+++ b/framework/audio/common/audiotaskscheduler.h
@@ -0,0 +1,97 @@
+/*
+ * 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 "audioworkgroup.h"
+#include "realtimethreadpool.h"
+#include "iaudiodriver.h"
+#include "iaudiotaskscheduler.h"
+#include "audiosanitizer.h"
+#include "global/async/asyncable.h"
+#include "global/log.h"
+#include
+
+namespace muse::audio {
+class AudioTaskScheduler : public IAudioTaskScheduler, public muse::async::Asyncable
+{
+ constexpr static const char* threadpoolName = "audio_realtime_thread";
+public:
+ void submitRealtimeTasksAndWait(const std::vector& tasks) override
+ {
+ std::lock_guard lock(m_threadPoolMutex);
+ for (const auto& task : tasks) {
+ IF_ASSERT_FAILED(m_threadPool->enqueue(task)) {
+ task();
+ }
+ }
+ m_threadPool->participateAndWait();
+ }
+
+ void setAudioDriver(const IAudioDriverPtr& audioDriver)
+ {
+ if (!audioDriver) {
+ return;
+ }
+ setWorkgroup(audioDriver->getAudioWorkGroup());
+ audioDriver->currentWorkgroupChanged().onNotify(
+ this, [this, audioDriverWeak = std::weak_ptr(audioDriver)]() {
+ if (auto audioDriver = audioDriverWeak.lock()) {
+ setWorkgroup(audioDriver->getAudioWorkGroup());
+ }
+ });
+ }
+
+private:
+
+ void setWorkgroup(const AudioWorkGroup& workGroup)
+ {
+ std::lock_guard lock(m_threadPoolMutex);
+ ensureThreadPoolSize(workGroup);
+ m_threadPool->setAudioWorkgroup(workGroup);
+ }
+
+ int getIdealThreadCount(AudioWorkGroup workGroup) const
+ {
+ int bestThreadHint = std::thread::hardware_concurrency();
+ if (workGroup.getProvider() != nullptr) {
+ bestThreadHint = workGroup.getMaxParallelThreadCount();
+ }
+ constexpr int hardwareToRealtimeRatio = 2; // This is a heuristic value. The optimal value may vary depending on the workload and system.
+ return bestThreadHint > 0 ? static_cast(bestThreadHint / hardwareToRealtimeRatio) : 1;
+ }
+
+ void ensureThreadPoolSize(const AudioWorkGroup& currentWorkGroup)
+ {
+ auto idealWorkerCount = getIdealThreadCount(currentWorkGroup);
+ std::lock_guard lock(m_threadPoolMutex);
+ if (m_threadPool->getNumberOfWorkers() != idealWorkerCount) {
+ m_threadPool = std::make_unique(threadpoolName, idealWorkerCount);
+ AudioSanitizer::setMixerThreads(m_threadPool->threadIdSet());
+ }
+ }
+
+ std::unique_ptr m_threadPool{ std::make_unique(threadpoolName, getIdealThreadCount({})) };
+
+ std::recursive_mutex m_threadPoolMutex;
+};
+}
diff --git a/framework/audio/common/audioworkgroup.cpp b/framework/audio/common/audioworkgroup.cpp
new file mode 100644
index 0000000000..2ccd009822
--- /dev/null
+++ b/framework/audio/common/audioworkgroup.cpp
@@ -0,0 +1,244 @@
+/*
+ * 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 "audioworkgroup.h"
+
+#include
+#include
+
+#ifdef __APPLE__
+#include
+#include
+#else
+#include
+#endif
+
+namespace muse::audio {
+#ifdef __APPLE__
+class AudioWorkgroupTokenProvider
+{
+public:
+ AudioWorkgroupTokenProvider(os_workgroup_t workgroup)
+ : m_workgroup(workgroup)
+ {
+ if (!m_workgroup) {
+ return;
+ }
+
+ if (__builtin_available(macOS 11.0, *)) {
+ os_retain(m_workgroup);
+ auto status = os_workgroup_join(m_workgroup, &m_joinToken);
+ if (status == 0) {
+ return;
+ }
+
+ os_release(m_workgroup);
+ m_workgroup = nullptr;
+ m_joinToken = {};
+ return;
+ }
+
+ m_workgroup = nullptr;
+ }
+
+ bool isAttachedTo(os_workgroup_t wg) const { return m_workgroup == wg; }
+ bool isValid() const { return m_workgroup != nullptr; }
+
+ AudioWorkgroupTokenProvider(const AudioWorkgroupTokenProvider& other) = delete;
+
+ AudioWorkgroupTokenProvider& operator=(const AudioWorkgroupTokenProvider&) = delete;
+
+ AudioWorkgroupTokenProvider(AudioWorkgroupTokenProvider&& other) noexcept
+ : m_workgroup(other.m_workgroup), m_joinToken(other.m_joinToken)
+ {
+ other.m_workgroup = nullptr;
+ other.m_joinToken = {};
+ }
+
+ ~AudioWorkgroupTokenProvider()
+ {
+ leave();
+ }
+
+private:
+ void leave() noexcept
+ {
+ if (!m_workgroup) {
+ return;
+ }
+
+ if (__builtin_available(macOS 11.0, *)) {
+ os_workgroup_leave(m_workgroup, &m_joinToken);
+ }
+ os_release(m_workgroup);
+ m_workgroup = nullptr;
+ m_joinToken = {};
+ }
+
+ os_workgroup_t m_workgroup;
+ os_workgroup_join_token_s m_joinToken;
+};
+
+class AudioWorkgroupProvider
+{
+public:
+ explicit AudioWorkgroupProvider(os_workgroup_t wg)
+ : m_workgroup(wg)
+ {
+ os_retain(m_workgroup);
+ }
+
+ ~AudioWorkgroupProvider()
+ {
+ if (m_workgroup) {
+ os_release(m_workgroup);
+ }
+ }
+
+ AudioWorkgroupProvider(const AudioWorkgroupProvider& other)
+ : m_workgroup(other.m_workgroup)
+ {
+ os_retain(m_workgroup);
+ }
+
+ AudioWorkgroupProvider& operator=(const AudioWorkgroupProvider& other)
+ {
+ if (this != &other) {
+ os_retain(other.m_workgroup);
+ if (m_workgroup) {
+ os_release(m_workgroup);
+ }
+ m_workgroup = other.m_workgroup;
+ }
+ return *this;
+ }
+
+ AudioWorkgroupProvider(AudioWorkgroupProvider&& other) noexcept
+ : m_workgroup(other.m_workgroup)
+ {
+ other.m_workgroup = nullptr;
+ }
+
+ bool join(AudioWorkgroupToken& tokenProvider) const
+ {
+ if (auto existingProvider = AudioWorkGroup::providerFor(tokenProvider);
+ existingProvider != nullptr && existingProvider->isAttachedTo(m_workgroup)) {
+ return true;
+ }
+ AudioWorkGroup::resetProviderFor(tokenProvider);
+ AudioWorkgroupTokenProvider provider(m_workgroup);
+ if (!provider.isValid()) {
+ return false;
+ }
+
+ AudioWorkGroup::setProviderFor(tokenProvider, [provider = std::move(provider)]() {
+ return &provider;
+ });
+ return true;
+ }
+
+ size_t getMaxParallelThreadCount() const
+ {
+ if (__builtin_available(macOS 11.0, *)) {
+ return (size_t)os_workgroup_max_parallel_threads(m_workgroup, nullptr);
+ }
+ return 0;
+ }
+
+private:
+
+ os_workgroup_t m_workgroup;
+};
+
+bool AudioWorkGroup::join(AudioWorkgroupToken& token)
+{
+ auto provider = getProvider();
+ if (!provider) {
+ return false;
+ }
+ return provider->join(token);
+}
+
+AudioWorkGroup makeAudioWorkgroup(void* opaqueHandle)
+{
+ if (opaqueHandle == nullptr) {
+ return {};
+ }
+
+ os_workgroup_t handle = reinterpret_cast(opaqueHandle);
+
+ return AudioWorkGroup { std::make_unique(handle) };
+}
+
+size_t AudioWorkGroup::getMaxParallelThreadCount() const
+{
+ if (auto provider = getProvider(); provider) {
+ return provider->getMaxParallelThreadCount();
+ }
+ return 0;
+}
+
+#else
+
+class AudioWorkgroupProvider
+{
+};
+
+size_t AudioWorkGroup::getMaxParallelThreadCount() const
+{
+ return std::thread::hardware_concurrency();
+}
+
+bool AudioWorkGroup::join(AudioWorkgroupToken&)
+{
+ return false;
+}
+
+AudioWorkGroup makeAudioWorkgroup(void*)
+{
+ return {};
+}
+
+#endif
+
+AudioWorkGroup::AudioWorkGroup() = default;
+
+AudioWorkGroup::AudioWorkGroup(std::unique_ptr provider)
+ : m_provider(std::move(provider)) {}
+
+AudioWorkGroup::~AudioWorkGroup() = default;
+
+AudioWorkGroup::AudioWorkGroup(const AudioWorkGroup& other)
+ : m_provider(other.m_provider ? std::make_unique(*other.m_provider) : nullptr) {}
+
+AudioWorkGroup::AudioWorkGroup(AudioWorkGroup&& other) noexcept = default;
+
+AudioWorkGroup& AudioWorkGroup::operator=(const AudioWorkGroup& other)
+{
+ if (this != &other) {
+ m_provider = other.m_provider ? std::make_unique(*other.m_provider) : nullptr;
+ }
+ return *this;
+}
+
+AudioWorkGroup& AudioWorkGroup::operator=(AudioWorkGroup&& other) noexcept = default;
+} // namespace muse::audio
diff --git a/framework/audio/common/audioworkgroup.h b/framework/audio/common/audioworkgroup.h
new file mode 100644
index 0000000000..6f19b002cf
--- /dev/null
+++ b/framework/audio/common/audioworkgroup.h
@@ -0,0 +1,147 @@
+/*
+ * 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
+#include
+#include
+#include
+
+#include "global/functional/inplace_function_mv.h"
+
+namespace muse::audio {
+class AudioWorkgroupTokenProvider;
+
+using ErasedAudioWorkgroupTokenProvider = muse::functional::MoveOnlyInplaceFunction;
+
+class AudioWorkgroupToken
+{
+public:
+ AudioWorkgroupToken() = default;
+ AudioWorkgroupToken(const AudioWorkgroupToken&) = delete;
+ AudioWorkgroupToken& operator=(const AudioWorkgroupToken&) = delete;
+ AudioWorkgroupToken(AudioWorkgroupToken&& other) noexcept
+ {
+ moveFrom(std::move(other));
+ }
+
+ AudioWorkgroupToken& operator=(AudioWorkgroupToken&& other) noexcept
+ {
+ if (this != &other) {
+ reset();
+ moveFrom(std::move(other));
+ }
+
+ return *this;
+ }
+
+ ~AudioWorkgroupToken()
+ {
+ reset();
+ }
+
+private:
+ friend class AudioWorkGroup;
+
+ explicit AudioWorkgroupToken(ErasedAudioWorkgroupTokenProvider provider)
+ {
+ setProvider(std::move(provider));
+ }
+
+ const AudioWorkgroupTokenProvider* provider() const
+ {
+ assertCurrentThread();
+ return m_provider ? m_provider() : nullptr;
+ }
+
+ void setProvider(ErasedAudioWorkgroupTokenProvider workgroup)
+ {
+ reset();
+ m_provider = std::move(workgroup);
+ if (m_provider) {
+ m_threadId = std::this_thread::get_id();
+ }
+ }
+
+ void reset()
+ {
+ assertCurrentThread();
+ m_provider = nullptr;
+ m_threadId = {};
+ }
+
+ void moveFrom(AudioWorkgroupToken&& other) noexcept
+ {
+ other.assertCurrentThread();
+ m_provider = std::move(other.m_provider);
+ m_threadId = other.m_threadId;
+ other.m_threadId = {};
+ }
+
+ void assertCurrentThread() const
+ {
+ assert((!m_provider || m_threadId == std::this_thread::get_id())
+ && "AudioWorkgroupToken must be used on the thread that joined the audio workgroup");
+ }
+
+ ErasedAudioWorkgroupTokenProvider m_provider;
+ std::thread::id m_threadId;
+};
+
+class AudioWorkgroupProvider;
+
+class AudioWorkGroup
+{
+public:
+ AudioWorkGroup();
+ AudioWorkGroup(const AudioWorkGroup&);
+ AudioWorkGroup(AudioWorkGroup&&) noexcept;
+ AudioWorkGroup& operator=(const AudioWorkGroup&);
+ AudioWorkGroup& operator=(AudioWorkGroup&&) noexcept;
+ ~AudioWorkGroup();
+
+ bool join(AudioWorkgroupToken& token);
+
+ const AudioWorkgroupProvider* getProvider() const { return m_provider.get(); }
+
+ size_t getMaxParallelThreadCount() const;
+
+private:
+ friend class AudioWorkgroupProvider;
+ friend AudioWorkGroup makeAudioWorkgroup(void* opaqueHandle);
+
+ explicit AudioWorkGroup(std::unique_ptr provider);
+
+ static const AudioWorkgroupTokenProvider* providerFor(const AudioWorkgroupToken& token) { return token.provider(); }
+ static void setProviderFor(AudioWorkgroupToken& token, ErasedAudioWorkgroupTokenProvider provider)
+ {
+ token.setProvider(std::move(provider));
+ }
+
+ static void resetProviderFor(AudioWorkgroupToken& token) { token.reset(); }
+
+ std::unique_ptr m_provider;
+};
+
+AudioWorkGroup makeAudioWorkgroup(void* opaqueHandle);
+} // namespace muse::audio
diff --git a/framework/audio/common/concurrentqueue.h b/framework/audio/common/concurrentqueue.h
new file mode 100644
index 0000000000..8b64d63cc3
--- /dev/null
+++ b/framework/audio/common/concurrentqueue.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#if defined(__MACH__)
+#include
+
+#pragma push_macro("integer_t")
+#define integer_t ::integer_t
+#endif
+
+#include "../thirdparty/moodycamel/blockingconcurrentqueue.h"
+
+#if defined(__MACH__)
+#pragma pop_macro("integer_t")
+#endif
+
+namespace muse {
+template
+using BlockingConcurrentQueue = moodycamel::BlockingConcurrentQueue;
+}
diff --git a/framework/audio/common/iaudiotaskscheduler.h b/framework/audio/common/iaudiotaskscheduler.h
new file mode 100644
index 0000000000..29686b6690
--- /dev/null
+++ b/framework/audio/common/iaudiotaskscheduler.h
@@ -0,0 +1,40 @@
+/*
+ * SPDX-License-Identifier: GPL-3.0-only
+ * MuseScore-CLA-applies
+ *
+ * MuseScore
+ * Music Composition & Notation
+ *
+ * Copyright (C) 2026 MuseScore Limited and others
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#pragma once
+
+#include "global/modularity/imoduleinterface.h"
+#include "global/functional/inplace_function.h"
+#include
+#include
+
+namespace muse::audio {
+class IAudioTaskScheduler : MODULE_GLOBAL_INTERFACE
+{
+ INTERFACE_ID(IAudioTaskScheduler)
+public:
+ using Task = muse::functional::inplace_function;
+ virtual void submitRealtimeTasksAndWait(const std::vector& tasks) = 0;
+};
+
+using IAudioTaskSchedulerPtr = std::shared_ptr;
+}
diff --git a/framework/audio/common/lightweightsemaphore.h b/framework/audio/common/lightweightsemaphore.h
new file mode 100644
index 0000000000..0d0557d1a6
--- /dev/null
+++ b/framework/audio/common/lightweightsemaphore.h
@@ -0,0 +1,7 @@
+#pragma once
+
+#include "../thirdparty/moodycamel/lightweightsemaphore.h"
+
+namespace muse {
+using LightweightSemaphore = moodycamel::LightweightSemaphore;
+}
diff --git a/framework/audio/common/realtimethreadpool.h b/framework/audio/common/realtimethreadpool.h
new file mode 100644
index 0000000000..6605d0e887
--- /dev/null
+++ b/framework/audio/common/realtimethreadpool.h
@@ -0,0 +1,170 @@
+#pragma once
+
+#include "audioworkgroup.h"
+#include "concurrency/threadutils.h"
+#include "runtime.h"
+#include "concurrentqueue.h"
+#include "global/functional/inplace_function.h"
+#include "lightweightsemaphore.h"
+#include "thirdparty/kors_logger/src/log_base.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace muse::audio {
+class RealtimeThreadPool
+{
+ struct InflightSemaphoreRelease {
+ explicit InflightSemaphoreRelease(muse::LightweightSemaphore& semaphore)
+ : m_semaphore(semaphore) {}
+
+ InflightSemaphoreRelease(const InflightSemaphoreRelease&) = delete;
+ InflightSemaphoreRelease& operator=(const InflightSemaphoreRelease&) = delete;
+
+ ~InflightSemaphoreRelease() noexcept
+ {
+ m_semaphore.signal();
+ }
+
+ muse::LightweightSemaphore& m_semaphore;
+ };
+
+public:
+ static constexpr int maxTaskCount = 10000;
+ using Task = muse::functional::inplace_function;
+ RealtimeThreadPool(
+ std::string name,
+ int num_of_workers = std::thread::hardware_concurrency())
+ {
+ num_of_workers = std::max(0, num_of_workers);
+ m_workers.reserve(num_of_workers);
+ try {
+ for (size_t i = 0; i < static_cast(num_of_workers); ++i) {
+ auto worker = std::make_unique();
+ Worker* workerPtr = worker.get();
+ const size_t workerIndex = i;
+ worker->m_thread = std::make_unique(
+ [this, workerPtr, workerIndex, name] {
+ muse::runtime::setThreadName(name + "_" + std::to_string(workerIndex));
+ AudioWorkgroupToken workgroupToken;
+ for (;;) {
+ Task task;
+
+ m_queue.wait_dequeue(task);
+ {
+ std::lock_guard lock(workerPtr->m_workgroupMutex);
+ workerPtr->m_workgroup.join(workgroupToken);
+ }
+
+ if (this->m_should_stop) {
+ return;
+ }
+
+ InflightSemaphoreRelease release(m_inflightSemaphore);
+ task();
+ }
+ });
+ m_workers.push_back(std::move(worker));
+ muse::setThreadPriority(*m_workers.back()->m_thread, ThreadPriority::High);
+ }
+ } catch (...) {
+ terminate();
+ throw;
+ }
+ }
+
+ void setAudioWorkgroup(muse::audio::AudioWorkGroup audioworkgroup)
+ {
+ for (auto& worker : m_workers) {
+ std::lock_guard lock(worker->m_workgroupMutex);
+ worker->m_workgroup = audioworkgroup;
+ }
+ }
+
+ bool enqueue(const Task& func)
+ {
+ m_inflightSemaphore.wait();
+ if (!m_queue.enqueue(func)) {
+ m_inflightSemaphore.signal();
+ return false;
+ }
+ return true;
+ }
+
+ void participateAndWait()
+ {
+ Task task;
+ while (m_queue.try_dequeue(task)) {
+ InflightSemaphoreRelease release(m_inflightSemaphore);
+ task();
+ }
+ waitUntilFinished();
+ m_inflightSemaphore.signal(maxTaskCount);
+ }
+
+ std::set threadIdSet() const
+ {
+ std::set result;
+
+ for (const auto& worker : m_workers) {
+ result.insert(worker->m_thread->get_id());
+ }
+
+ return result;
+ }
+
+ int getNumberOfWorkers() const
+ {
+ return static_cast(m_workers.size());
+ }
+
+ ~RealtimeThreadPool()
+ {
+ terminate();
+ }
+
+private:
+ void waitUntilFinished()
+ {
+ auto actuallyAwaited = m_inflightSemaphore.waitMany(maxTaskCount);
+ for (size_t i = 0;
+ i < static_cast(maxTaskCount - actuallyAwaited); ++i) {
+ m_inflightSemaphore.wait();
+ }
+ }
+
+ void terminate()
+ {
+ m_should_stop = true;
+ for (size_t i = 0; i < m_workers.size(); ++i) {
+ IF_ASSERT_FAILED(m_queue.enqueue([] {})) {
+ // this is _extremely_ unlikely to fail. But if it does the threads would hang indefinitely.
+ std::terminate();
+ }
+ }
+
+ for (auto& worker : m_workers) {
+ if (worker->m_thread && worker->m_thread->joinable()) {
+ worker->m_thread->join();
+ }
+ }
+ }
+
+ struct Worker {
+ std::unique_ptr m_thread;
+ AudioWorkGroup m_workgroup;
+ std::mutex m_workgroupMutex;
+ };
+
+ muse::BlockingConcurrentQueue m_queue;
+ std::vector > m_workers;
+ muse::LightweightSemaphore m_inflightSemaphore { maxTaskCount };
+ std::atomic m_should_stop{ false };
+}; // namespace muse::audio
+} // namespace muse::audio
diff --git a/framework/audio/driver/CMakeLists.txt b/framework/audio/driver/CMakeLists.txt
index 950c7086bc..6bc44af87e 100644
--- a/framework/audio/driver/CMakeLists.txt
+++ b/framework/audio/driver/CMakeLists.txt
@@ -85,10 +85,13 @@ elseif(OS_IS_MAC)
target_sources(muse_audio_driver PRIVATE
platform/osx/osxaudiodriver.mm
platform/osx/osxaudiodriver.h
+ platform/osx/osxdirectaudiodriver.mm
+ platform/osx/osxdirectaudiodriver.h
)
-
+
set_source_files_properties(
platform/osx/osxaudiodriver.mm
+ platform/osx/osxdirectaudiodriver.mm
PROPERTIES
SKIP_UNITY_BUILD_INCLUSION ON
SKIP_PRECOMPILE_HEADERS ON
diff --git a/framework/audio/driver/platform/osx/osxdirectaudiodriver.h b/framework/audio/driver/platform/osx/osxdirectaudiodriver.h
new file mode 100644
index 0000000000..215d3bedd8
--- /dev/null
+++ b/framework/audio/driver/platform/osx/osxdirectaudiodriver.h
@@ -0,0 +1,93 @@
+/*
+ * SPDX-License-Identifier: GPL-3.0-only
+ * MuseScore-CLA-applies
+ *
+ * MuseScore
+ * Music Composition & Notation
+ *
+ * Copyright (C) 2026 MuseScore Limited and others
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+#ifndef MUSE_AUDIO_OSXDIRECTAUDIODRIVER_H
+#define MUSE_AUDIO_OSXDIRECTAUDIODRIVER_H
+
+#include