diff --git a/SetupConfigure.cmake b/SetupConfigure.cmake index e70c7f504734f..be5efe9add946 100644 --- a/SetupConfigure.cmake +++ b/SetupConfigure.cmake @@ -195,10 +195,10 @@ endif() set(QT_SUPPORT ON) if (MUSE_MODULE_AUDIO_JACK) - if (OS_IS_LIN OR MINGW) - add_compile_definitions(JACK_AUDIO) - else() + if (OS_IS_WIN AND (NOT MINGW)) set(MUSE_MODULE_AUDIO_JACK OFF) + else() + add_compile_definitions(JACK_AUDIO) endif() endif() diff --git a/buildscripts/ci/macos/setup.sh b/buildscripts/ci/macos/setup.sh index 2cc0215d93450..0a125cc8c88de 100644 --- a/buildscripts/ci/macos/setup.sh +++ b/buildscripts/ci/macos/setup.sh @@ -28,6 +28,11 @@ export MACOSX_DEPLOYMENT_TARGET=10.14 echo "Install build tools" brew install cmake ninja --formula --quiet +wget -q https://github.com/jackaudio/jack2-releases/releases/download/v1.9.22/jack2-macOS-universal-v1.9.22.tar.gz +tar xvf jack2-macOS-universal-v1.9.22.tar.gz +ls -ltr +sudo installer -pkg ./jack2-osx-1.9.22.pkg -target / + # Download dependencies echo "Download dependencies" diff --git a/src/app/cmdoptions.h b/src/app/cmdoptions.h index 3e1e2be50109e..47bbfaa1bfbf9 100644 --- a/src/app/cmdoptions.h +++ b/src/app/cmdoptions.h @@ -63,6 +63,10 @@ struct CmdOptions { std::optional pngDpiResolution; } exportImage; + struct { + std::optional audioDelayCompensate; + } audio; + struct { std::optional mp3Bitrate; } exportAudio; diff --git a/src/app/internal/commandlineparser.cpp b/src/app/internal/commandlineparser.cpp index 9db35c83e2acc..ec41a11ea6198 100644 --- a/src/app/internal/commandlineparser.cpp +++ b/src/app/internal/commandlineparser.cpp @@ -163,6 +163,8 @@ void CommandLineParser::init() "Check an audio plugin for compatibility with the application and register it", "path")); m_parser.addOption(QCommandLineOption("register-failed-audio-plugin", "Register an incompatible audio plugin", "path")); + m_parser.addOption(QCommandLineOption("audioDelayCompensate", "Compensate for delay in frames caused by MuseScore buffering", "1024")); + // Internal m_parser.addOption(internalCommandLineOption("score-display-name-override", "Display name to be shown in splash screen for the score that is being opened", "name")); @@ -275,6 +277,10 @@ void CommandLineParser::parse(int argc, char** argv) m_options.audioPluginRegistration.failCode = !args1.empty() ? args1[0].toInt() : -1; } + if (m_parser.isSet("audioDelayCompensate")) { + m_options.audio.audioDelayCompensate = intValue("audioDelayCompensate"); + } + // Converter mode if (m_parser.isSet("r")) { std::optional val = floatValue("r"); diff --git a/src/app/internal/guiapp.cpp b/src/app/internal/guiapp.cpp index 615c0dfdfdb98..57b1e4a3323b7 100644 --- a/src/app/internal/guiapp.cpp +++ b/src/app/internal/guiapp.cpp @@ -314,4 +314,6 @@ void GuiApp::applyCommandLineOptions(const CmdOptions& options) if (options.app.loggerLevel) { m_globalModule.setLoggerLevel(options.app.loggerLevel.value()); } + + audioConfiguration()->setAudioDelayCompensate(options.audio.audioDelayCompensate.value_or(1024)); // FIX: equal to buffer-size } diff --git a/src/app/internal/guiapp.h b/src/app/internal/guiapp.h index 8c3943f0940eb..d3a516618e3dc 100644 --- a/src/app/internal/guiapp.h +++ b/src/app/internal/guiapp.h @@ -16,6 +16,7 @@ #include "appshell/iappshellconfiguration.h" #include "appshell/internal/istartupscenario.h" #include "importexport/guitarpro/iguitarproconfiguration.h" +#include "audio/iaudioconfiguration.h" namespace mu::app { class GuiApp : public muse::BaseApplication, public std::enable_shared_from_this @@ -25,6 +26,7 @@ class GuiApp : public muse::BaseApplication, public std::enable_shared_from_this muse::Inject appshellConfiguration; muse::Inject startupScenario; muse::Inject guitarProConfiguration; + muse::Inject audioConfiguration; public: GuiApp(const CmdOptions& options, const muse::modularity::ContextPtr& ctx); diff --git a/src/appshell/appshell.qrc b/src/appshell/appshell.qrc index 0c0c867273700..7f289f119e92b 100644 --- a/src/appshell/appshell.qrc +++ b/src/appshell/appshell.qrc @@ -103,6 +103,7 @@ qml/Preferences/internal/MeiSection.qml qml/Preferences/internal/PublishMuseScoreComSection.qml qml/Preferences/internal/MixerSection.qml + qml/Preferences/internal/JackSection.qml qml/DevTools/Extensions/ExtensionsListView.qml qml/platform/PlatformMenuBar.qml qml/Preferences/PercussionPreferencesPage.qml diff --git a/src/appshell/qml/Preferences/AudioMidiPreferencesPage.qml b/src/appshell/qml/Preferences/AudioMidiPreferencesPage.qml index 8724cccb6dbd0..00989d7f5d514 100644 --- a/src/appshell/qml/Preferences/AudioMidiPreferencesPage.qml +++ b/src/appshell/qml/Preferences/AudioMidiPreferencesPage.qml @@ -82,6 +82,19 @@ PreferencesPage { SeparatorLine {} + JackSection { + jackTransportEnable: audioMidiModel.jackTransportEnable + + navigation.section: root.navigationSection + navigation.order: root.navigationOrderStart + 3 + + onJackTransportEnableChangeRequested: function(enable) { + audioMidiModel.jackTransportEnable = enable + } + } + + SeparatorLine {} + MixerSection { muteHiddenInstruments: audioMidiModel.muteHiddenInstruments diff --git a/src/appshell/qml/Preferences/internal/JackSection.qml b/src/appshell/qml/Preferences/internal/JackSection.qml new file mode 100644 index 0000000000000..f31ad7b21568b --- /dev/null +++ b/src/appshell/qml/Preferences/internal/JackSection.qml @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 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 . + */ +import QtQuick 2.15 + +import Muse.Ui 1.0 +import Muse.UiComponents 1.0 + +BaseSection { + id: root + + title: qsTrc("appshell/preferences", "Jack") + + property alias jackTransportEnable: jackTransportEnableCheckBox.checked + + signal jackTransportEnableChangeRequested(bool enable) + + CheckBox { + id: jackTransportEnableCheckBox + + width: parent.width + + text: qsTrc("appshell/preferences", "Enable Jack Transport") + + navigation.name: "JackTransportEnableCheckbox" + navigation.panel: root.navigation + + onClicked: { + root.jackTransportEnableChangeRequested(!checked) + } + } +} diff --git a/src/appshell/view/preferences/audiomidipreferencesmodel.cpp b/src/appshell/view/preferences/audiomidipreferencesmodel.cpp index 9be02db3429c6..13bfe05e29b96 100644 --- a/src/appshell/view/preferences/audiomidipreferencesmodel.cpp +++ b/src/appshell/view/preferences/audiomidipreferencesmodel.cpp @@ -99,6 +99,10 @@ void AudioMidiPreferencesModel::init() playbackConfiguration()->muteHiddenInstrumentsChanged().onReceive(this, [this](bool mute) { emit muteHiddenInstrumentsChanged(mute); }); + + playbackConfiguration()->jackTransportEnableChanged().onReceive(this, [this](bool mute) { + emit jackTransportEnableChanged(mute); + }); } QStringList AudioMidiPreferencesModel::audioApiList() const @@ -188,3 +192,17 @@ void AudioMidiPreferencesModel::setMuteHiddenInstruments(bool mute) playbackConfiguration()->setMuteHiddenInstruments(mute); } + +bool AudioMidiPreferencesModel::jackTransportEnable() const +{ + return playbackConfiguration()->jackTransportEnable(); +} + +void AudioMidiPreferencesModel::setJackTransportEnable(bool enable) +{ + if (enable == jackTransportEnable()) { + return; + } + + playbackConfiguration()->setJackTransportEnable(enable); +} diff --git a/src/appshell/view/preferences/audiomidipreferencesmodel.h b/src/appshell/view/preferences/audiomidipreferencesmodel.h index 5538d5e2c50e6..d2bc1e031fc8b 100644 --- a/src/appshell/view/preferences/audiomidipreferencesmodel.h +++ b/src/appshell/view/preferences/audiomidipreferencesmodel.h @@ -50,6 +50,8 @@ class AudioMidiPreferencesModel : public QObject, public muse::Injectable, publi Q_PROPERTY(bool muteHiddenInstruments READ muteHiddenInstruments WRITE setMuteHiddenInstruments NOTIFY muteHiddenInstrumentsChanged) + Q_PROPERTY(bool jackTransportEnable READ jackTransportEnable WRITE setJackTransportEnable NOTIFY jackTransportEnableChanged) + muse::Inject audioConfiguration = { this }; muse::Inject midiConfiguration = { this }; muse::Inject midiOutPort = { this }; @@ -81,6 +83,8 @@ class AudioMidiPreferencesModel : public QObject, public muse::Injectable, publi bool muteHiddenInstruments() const; + bool jackTransportEnable() const; + public slots: void setCurrentAudioApiIndex(int index); @@ -88,6 +92,8 @@ public slots: void setMuteHiddenInstruments(bool mute); + void setJackTransportEnable(bool enable); + signals: void currentAudioApiIndexChanged(int index); void midiInputDeviceIdChanged(); @@ -100,6 +106,8 @@ public slots: void muteHiddenInstrumentsChanged(bool mute); + void jackTransportEnableChanged(bool enable); + private: muse::midi::MidiDeviceID midiInputDeviceId(int index) const; muse::midi::MidiDeviceID midiOutputDeviceId(int index) const; diff --git a/src/framework/audio/CMakeLists.txt b/src/framework/audio/CMakeLists.txt index 3efcbeb488376..bd69e3c53ce29 100644 --- a/src/framework/audio/CMakeLists.txt +++ b/src/framework/audio/CMakeLists.txt @@ -23,7 +23,15 @@ set(MODULE_ALIAS muse::audio) include(GetPlatformInfo) -if (OS_IS_WIN) +set(JACK_SRC "") +if (MUSE_MODULE_AUDIO_JACK) + set(JACK_SRC + ${CMAKE_CURRENT_LIST_DIR}/internal/platform/jack/jackaudiodriver.cpp + ${CMAKE_CURRENT_LIST_DIR}/internal/platform/jack/jackaudiodriver.h + ) +endif(MUSE_MODULE_AUDIO_JACK) + +if (OS_IS_WIN AND (NOT MINGW)) set(DRIVER_SRC #${CMAKE_CURRENT_LIST_DIR}/internal/platform/win/winmmdriver.cpp #${CMAKE_CURRENT_LIST_DIR}/internal/platform/win/winmmdriver.h @@ -37,28 +45,23 @@ if (OS_IS_WIN) ${CMAKE_CURRENT_LIST_DIR}/internal/platform/win/audiodeviceslistener.cpp ${CMAKE_CURRENT_LIST_DIR}/internal/platform/win/audiodeviceslistener.h ) -elseif(OS_IS_LIN OR OS_IS_FBSD) - if (MUSE_MODULE_AUDIO_JACK) - set(DRIVER_SRC - ${CMAKE_CURRENT_LIST_DIR}/internal/platform/jack/jackaudiodriver.cpp - ${CMAKE_CURRENT_LIST_DIR}/internal/platform/jack/jackaudiodriver.h - ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/linuxaudiodriver.cpp - ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/linuxaudiodriver.h - ${CMAKE_CURRENT_LIST_DIR}/internal/platform/jack/audiodeviceslistener.cpp - ${CMAKE_CURRENT_LIST_DIR}/internal/platform/jack/audiodeviceslistener.h - ) - else() - set(DRIVER_SRC - ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/linuxaudiodriver.cpp - ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/linuxaudiodriver.h - ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/audiodeviceslistener.cpp - ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/audiodeviceslistener.h - ) - endif() +elseif(OS_IS_LIN OR OS_IS_FBSD OR MINGW) + set(DRIVER_SRC + ${CMAKE_CURRENT_LIST_DIR}/internal/audiomidimanager.cpp + ${CMAKE_CURRENT_LIST_DIR}/internal/audiomidimanager.h + ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/audiodeviceslistener.cpp + ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/audiodeviceslistener.h + ${CMAKE_CURRENT_LIST_DIR}/internal/platform/alsa/alsaaudiodriver.cpp + ${CMAKE_CURRENT_LIST_DIR}/internal/platform/alsa/alsaaudiodriver.h + ${JACK_SRC} + ) elseif(OS_IS_MAC) set(DRIVER_SRC + ${CMAKE_CURRENT_LIST_DIR}/internal/audiomidimanager.cpp + ${CMAKE_CURRENT_LIST_DIR}/internal/audiomidimanager.h ${CMAKE_CURRENT_LIST_DIR}/internal/platform/osx/osxaudiodriver.mm ${CMAKE_CURRENT_LIST_DIR}/internal/platform/osx/osxaudiodriver.h + ${JACK_SRC} ) set_source_files_properties( @@ -277,23 +280,22 @@ endif() if (MUSE_MODULE_AUDIO_JACK) find_package(Jack REQUIRED) - set(MODULE_INCLUDE_PRIVATE ${MODULE_INCLUDE_PRIVATE} ${JACK_INCLUDE_DIRS} ) - set(MODULE_LINK ${MODULE_LINK} ${JACK_LIBRARIES} pthread ) + set(MODULE_INCLUDE_PRIVATE ${MODULE_INCLUDE_PRIVATE} ${JACK_INCLUDE_DIRS}) + set(MODULE_LINK ${MODULE_LINK} ${JACK_LDFLAGS} pthread) +endif() + +if (OS_IS_MAC) + find_library(AudioToolbox NAMES AudioToolbox) + find_library(CoreAudio NAMES CoreAudio) + set(MODULE_LINK ${MODULE_LINK} ${AudioToolbox} ${CoreAudio}) +elseif (OS_IS_WIN AND (NOT (MINGW))) + set(MODULE_LINK ${MODULE_LINK} winmm mmdevapi mfplat) +elseif (OS_IS_LIN) find_package(ALSA REQUIRED) - set(MODULE_INCLUDE_PRIVATE ${MODULE_INCLUDE_PRIVATE} ${ALSA_INCLUDE_DIRS} ) - set(MODULE_LINK ${MODULE_LINK} ${ALSA_LIBRARIES} pthread ) -else () - if (OS_IS_MAC) - find_library(AudioToolbox NAMES AudioToolbox) - find_library(CoreAudio NAMES CoreAudio) - set(MODULE_LINK ${MODULE_LINK} ${AudioToolbox} ${CoreAudio}) - elseif (OS_IS_WIN) - set(MODULE_LINK ${MODULE_LINK} winmm mmdevapi mfplat) - elseif (OS_IS_LIN) - find_package(ALSA REQUIRED) - set(MODULE_INCLUDE_PRIVATE ${MODULE_INCLUDE_PRIVATE} ${ALSA_INCLUDE_DIRS} ) - set(MODULE_LINK ${MODULE_LINK} ${ALSA_LIBRARIES} pthread ) - endif() + set(MODULE_INCLUDE_PRIVATE ${MODULE_INCLUDE_PRIVATE} ${ALSA_INCLUDE_DIRS}) + set(MODULE_LINK ${MODULE_LINK} ${ALSA_LIBRARIES} pthread) +elseif (MINGW) + set(MODULE_LINK ${MODULE_LINK} winmm mmdevapi mfplat) endif() set(MODULE_QRC audio.qrc) diff --git a/src/framework/audio/audiomodule.cpp b/src/framework/audio/audiomodule.cpp index 68217272404c4..70d9e31f19895 100644 --- a/src/framework/audio/audiomodule.cpp +++ b/src/framework/audio/audiomodule.cpp @@ -24,6 +24,7 @@ #include "ui/iuiengine.h" #include "global/modularity/ioc.h" +#include "framework/midi/midimodule.h" #include "internal/audioconfiguration.h" #include "internal/audiosanitizer.h" #include "internal/audiothread.h" @@ -54,27 +55,16 @@ using namespace muse::audio; using namespace muse::audio::synth; using namespace muse::audio::fx; -#ifdef MUSE_MODULE_AUDIO_JACK -#include "internal/platform/jack/jackaudiodriver.h" +#if defined(Q_OS_MACOS) || defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(MINGW) +#include "internal/audiomidimanager.h" #endif -#ifdef Q_OS_LINUX -#include "internal/platform/lin/linuxaudiodriver.h" -#endif - -#ifdef Q_OS_FREEBSD -#include "internal/platform/lin/linuxaudiodriver.h" -#endif -#ifdef Q_OS_WIN +#if defined(Q_OS_WIN) && !defined(MINGW) //#include "internal/platform/win/winmmdriver.h" //#include "internal/platform/win/wincoreaudiodriver.h" #include "internal/platform/win/wasapiaudiodriver.h" #endif -#ifdef Q_OS_MACOS -#include "internal/platform/osx/osxaudiodriver.h" -#endif - #ifdef Q_OS_WASM #include "internal/platform/web/webaudiodriver.h" #endif @@ -118,30 +108,20 @@ void AudioModule::registerExports() m_playbackFacade = std::make_shared(iocContext()); m_soundFontRepository = std::make_shared(iocContext()); -#if defined(MUSE_MODULE_AUDIO_JACK) - m_audioDriver = std::shared_ptr(new JackAudioDriver()); -#else - -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) - m_audioDriver = std::shared_ptr(new LinuxAudioDriver()); +#if defined(Q_OS_MACOS) || defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(MINGW) + m_audioDriver = std::shared_ptr(new AudioMidiManager()); #endif -#ifdef Q_OS_WIN +#if defined(Q_OS_WIN) && !defined(MINGW) //m_audioDriver = std::shared_ptr(new WinmmDriver()); //m_audioDriver = std::shared_ptr(new CoreAudioDriver()); m_audioDriver = std::shared_ptr(new WasapiAudioDriver()); #endif -#ifdef Q_OS_MACOS - m_audioDriver = std::shared_ptr(new OSXAudioDriver()); -#endif - #ifdef Q_OS_WASM m_audioDriver = std::shared_ptr(new WebAudioDriver()); #endif -#endif // MUSE_MODULE_AUDIO_JACK - ioc()->registerExport(moduleName(), m_configuration); ioc()->registerExport(moduleName(), m_audioEngine); ioc()->registerExport(moduleName(), std::make_shared()); @@ -271,7 +251,7 @@ void AudioModule::setupAudioDriver(const IApplication::RunMode& mode) if (mode == IApplication::RunMode::GuiApp) { m_audioDriver->init(); - + m_audioDriver->setAudioDelayCompensate(m_configuration->audioDelayCompensate()); IAudioDriver::Spec activeSpec; if (m_audioDriver->open(requiredSpec, &activeSpec)) { setupAudioWorker(activeSpec); diff --git a/src/framework/audio/iaudioconfiguration.h b/src/framework/audio/iaudioconfiguration.h index b210eb3628e59..27bab87a94d0f 100644 --- a/src/framework/audio/iaudioconfiguration.h +++ b/src/framework/audio/iaudioconfiguration.h @@ -63,6 +63,9 @@ class IAudioConfiguration : MODULE_EXPORT_INTERFACE virtual async::Notification sampleRateChanged() const = 0; virtual size_t desiredAudioThreadNumber() const = 0; + virtual int audioDelayCompensate() const = 0; + virtual void setAudioDelayCompensate(const int frames) = 0; + virtual size_t minTrackCountForMultithreading() const = 0; // synthesizers diff --git a/src/framework/audio/iaudiodriver.h b/src/framework/audio/iaudiodriver.h index 4a6a80520becd..a7ff9b74df93a 100644 --- a/src/framework/audio/iaudiodriver.h +++ b/src/framework/audio/iaudiodriver.h @@ -32,8 +32,11 @@ #include "modularity/imoduleinterface.h" #include "audiotypes.h" +#include "framework/midi/miditypes.h" namespace muse::audio { +class AudioDriverState; + class IAudioDriver : MODULE_EXPORT_INTERFACE { INTERFACE_ID(IAudioDriver) @@ -86,11 +89,32 @@ class IAudioDriver : MODULE_EXPORT_INTERFACE virtual async::Notification outputDeviceSampleRateChanged() const = 0; virtual std::vector availableOutputDeviceSampleRates() const = 0; + virtual bool isPlaying() const = 0; + virtual void remotePlayOrStop(bool) const = 0; + virtual void remoteSeek(msecs_t) const = 0; + + virtual int audioDelayCompensate() const = 0; + virtual void setAudioDelayCompensate(const int frames) = 0; virtual void resume() = 0; virtual void suspend() = 0; }; -using IAudioDriverPtr = std::shared_ptr; + +class AudioDriverState +{ +public: + virtual std::string name() const = 0; + virtual bool open(const IAudioDriver::Spec& spec, IAudioDriver::Spec* activeSpec) = 0; + virtual void close() = 0; + virtual bool isOpened() const = 0; + virtual void setAudioDelayCompensate(const int frames) = 0; + + virtual void changedPlaying() const = 0; + virtual void changedPosition(muse::audio::secs_t secs, muse::midi::tick_t tick) const = 0; + + IAudioDriver::Spec deviceSpec; // current running spec + std::string deviceId; +}; } #endif // MUSE_AUDIO_IAUDIODRIVER_H diff --git a/src/framework/audio/internal/audioconfiguration.cpp b/src/framework/audio/internal/audioconfiguration.cpp index 6cf4681f6019d..0b5ac64631d14 100644 --- a/src/framework/audio/internal/audioconfiguration.cpp +++ b/src/framework/audio/internal/audioconfiguration.cpp @@ -75,8 +75,8 @@ void AudioConfiguration::init() m_audioOutputDeviceIdChanged.notify(); }); - settings()->setDefaultValue(AUDIO_SAMPLE_RATE_KEY, Val(44100)); - settings()->setCanBeManuallyEdited(AUDIO_SAMPLE_RATE_KEY, false, Val(44100), Val(192000)); + settings()->setDefaultValue(AUDIO_SAMPLE_RATE_KEY, Val(48000)); + settings()->setCanBeManuallyEdited(AUDIO_SAMPLE_RATE_KEY, false, Val(48000), Val(192000)); settings()->valueChanged(AUDIO_SAMPLE_RATE_KEY).onReceive(nullptr, [this](const Val&) { m_driverSampleRateChanged.notify(); }); @@ -206,6 +206,16 @@ size_t AudioConfiguration::desiredAudioThreadNumber() const return settings()->value(AUDIO_DESIRED_THREAD_NUMBER_KEY).toInt(); } +int AudioConfiguration::audioDelayCompensate() const +{ + return m_audioDelayCompensate; +} + +void AudioConfiguration::setAudioDelayCompensate(const int frames) +{ + m_audioDelayCompensate = frames; +} + size_t AudioConfiguration::minTrackCountForMultithreading() const { // Start mutlithreading-processing only when there are more or equal number of tracks diff --git a/src/framework/audio/internal/audioconfiguration.h b/src/framework/audio/internal/audioconfiguration.h index 453c68b2f8cc3..41aedfbc110e9 100644 --- a/src/framework/audio/internal/audioconfiguration.h +++ b/src/framework/audio/internal/audioconfiguration.h @@ -66,6 +66,9 @@ class AudioConfiguration : public IAudioConfiguration, public Injectable async::Notification sampleRateChanged() const override; size_t desiredAudioThreadNumber() const override; + int audioDelayCompensate() const override; + void setAudioDelayCompensate(const int frames) override; + size_t minTrackCountForMultithreading() const override; // synthesizers @@ -80,6 +83,7 @@ class AudioConfiguration : public IAudioConfiguration, public Injectable private: void updateSamplesToPreallocate(); + int m_audioDelayCompensate = 0; async::Channel m_soundFontDirsChanged; async::Channel m_samplesToPreallocateChanged; diff --git a/src/framework/audio/internal/audiomidimanager.cpp b/src/framework/audio/internal/audiomidimanager.cpp new file mode 100644 index 0000000000000..4c4b64ab026c6 --- /dev/null +++ b/src/framework/audio/internal/audiomidimanager.cpp @@ -0,0 +1,360 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2021 MuseScore BVBA 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 "audiomidimanager.h" + +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) +#include "platform/alsa/alsaaudiodriver.h" //FIX: relative path, set path in CMakeLists +#endif + +#if JACK_AUDIO +#include "platform/jack/jackaudiodriver.h" //FIX: relative path, set path in CMakeLists +#endif + +#ifdef Q_OS_MACOS +#include "internal/platform/osx/osxaudiodriver.h" +#endif + +#include "translation.h" +#include "log.h" +#include "runtime.h" + +using namespace muse; +using namespace muse::audio; + +AudioMidiManager::AudioMidiManager() +{ +} + +AudioMidiManager::~AudioMidiManager() +{ +} + +void AudioMidiManager::init() +{ +#ifndef Q_OS_MACOS + m_devicesListener.startWithCallback([this]() { + return availableOutputDevices(); + }); + + m_devicesListener.devicesChanged().onNotify(this, [this]() { + m_availableOutputDevicesChanged.notify(); + }); +#endif + + // notify driver if when musescore changes play-position or play/pause + playbackController()->isPlayingChanged().onNotify(this, [this]() { + isPlayingChanged(); + }); + + // HELP: is playbackPositionChanged notified for incremental changes too? + playbackController()->currentPlaybackPositionChanged().onReceive(this, [this](audio::secs_t secs, midi::tick_t tick) { + positionChanged(secs, tick); + }); +} + +std::string AudioMidiManager::name() const +{ + return m_current_audioDriverState->name(); +} + +bool AudioMidiManager::open(const Spec& spec, Spec* activeSpec) +{ + // re-initialize device + m_current_audioDriverState->setAudioDelayCompensate(m_audioDelayCompensate); + + if (!m_current_audioDriverState->open(spec, activeSpec)) { + // FIX: need to carry around the spec because of callback + m_spec = spec; + return false; + } + m_spec = *activeSpec; + return true; +} + +void AudioMidiManager::close() +{ + return m_current_audioDriverState->close(); +} + +bool AudioMidiManager::isOpened() const +{ + return m_current_audioDriverState->isOpened(); +} + +const AudioMidiManager::Spec& AudioMidiManager::activeSpec() const +{ + return m_current_audioDriverState->deviceSpec; +} + +AudioDeviceID AudioMidiManager::outputDevice() const +{ + if (m_current_audioDriverState != nullptr) { + return m_current_audioDriverState->deviceId; + } else { + LOGE() << "device is not opened, deviceId: " << m_deviceId; + return m_deviceId; // FIX: should return optional type + } +} + +bool AudioMidiManager::makeDevice(const AudioDeviceID& deviceId) +{ +#if defined(JACK_AUDIO) + if (deviceId == "jack") { + bool transportEnable = playbackConfiguration()->jackTransportEnable(); + m_current_audioDriverState = std::make_unique(this, transportEnable); + return true; + } +#endif +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) + if (deviceId == "alsa") { + m_current_audioDriverState = std::make_unique(); + return true; + } +#endif +//#ifdef Q_OS_MACOS +// if (deviceId == "osx") { +// m_current_audioDriverState = std::make_unique(); +// return true; +// } +//#endif + LOGE() << "Unknown device name: " << deviceId; + return false; +} + +// reopens the same device (if m_spec has changed) +bool AudioMidiManager::reopen(const AudioDeviceID& deviceId, Spec newSpec) +{ + // close current device if opened + if (m_current_audioDriverState->isOpened()) { + m_current_audioDriverState->close(); + } + // maybe change device, if needed + if (m_current_audioDriverState->name() != deviceId) { + // select the new device driver + if (!makeDevice(deviceId)) { + return false; + } + } + + // re-initialize devide + m_current_audioDriverState->setAudioDelayCompensate(m_audioDelayCompensate); + + // open the device driver + if (!m_current_audioDriverState->open(newSpec, &newSpec)) { + return false; + } + return true; +} + +bool AudioMidiManager::selectOutputDevice(const AudioDeviceID& deviceId) +{ + // When starting, no previously device has been selected + if (m_current_audioDriverState == nullptr) { + LOGW() << "no previously opened device"; + return makeDevice(deviceId); + } + // If for some reason we select the same device, do nothing + if (m_current_audioDriverState->name() == deviceId) { + return true; + } + reopen(deviceId, m_spec); + m_outputDeviceChanged.notify(); + return true; +} + +bool AudioMidiManager::resetToDefaultOutputDevice() +{ +#if defined(JACK_AUDIO) + return selectOutputDevice("jack"); // FIX: +#else + return selectOutputDevice("alsa"); // FIX: +#endif +} + +muse::async::Notification AudioMidiManager::outputDeviceChanged() const +{ + return m_outputDeviceChanged; +} + +AudioDeviceList AudioMidiManager::availableOutputDevices() const +{ + AudioDeviceList devices; +#if defined(JACK_AUDIO) + devices.push_back({ "jack", muse::trc("audio", "JACK") }); +#endif +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) + devices.push_back({ "alsa", muse::trc("audio", "ALSA") }); +#endif + return devices; +} + +muse::async::Notification AudioMidiManager::availableOutputDevicesChanged() const +{ + return m_availableOutputDevicesChanged; +} + +void AudioMidiManager::isPlayingChanged() +{ + if (m_current_audioDriverState) { + m_current_audioDriverState->changedPlaying(); + } +} + +void AudioMidiManager::positionChanged(muse::audio::secs_t secs, muse::midi::tick_t tick) +{ + if (m_current_audioDriverState) { + m_current_audioDriverState->changedPosition(secs, tick); + } +} + +bool AudioMidiManager::isPlaying() const +{ + return playbackController()->isPlaying(); +} + +void AudioMidiManager::remotePlayOrStop(bool ps) const +{ + playbackController()->remotePlayOrStop(ps); +} + +void AudioMidiManager::remoteSeek(msecs_t millis) const +{ + playbackController()->remoteSeek(millis); +} + +int AudioMidiManager::audioDelayCompensate() const +{ + return m_audioDelayCompensate; +} + +void AudioMidiManager::setAudioDelayCompensate(const int frames) +{ + m_audioDelayCompensate = frames; +} + +unsigned int AudioMidiManager::outputDeviceBufferSize() const +{ + return m_current_audioDriverState->deviceSpec.samples; +} + +bool AudioMidiManager::setOutputDeviceBufferSize(unsigned int bufferSize) +{ + if (m_spec.samples == (int)bufferSize) { + return true; + } + m_spec.samples = (int)bufferSize; + if (!reopen(m_current_audioDriverState->name(), m_spec)) { + return false; + } + m_bufferSizeChanged.notify(); + return true; +} + +muse::async::Notification AudioMidiManager::outputDeviceBufferSizeChanged() const +{ + return m_bufferSizeChanged; +} + +std::vector AudioMidiManager::availableOutputDeviceBufferSizes() const +{ + std::vector result; + + unsigned int n = MAXIMUM_BUFFER_SIZE; + while (n >= MINIMUM_BUFFER_SIZE) { + result.push_back(n); + n /= 2; + } + + std::sort(result.begin(), result.end()); + + return result; +} + +// FIX-JACK-20240823: change api callers, WAS: +// unsigned int LinuxAudioDriver::sampleRate() const +unsigned int AudioMidiManager::outputDeviceSampleRate() const +{ + return m_current_audioDriverState->deviceSpec.sampleRate; +} + +// FIX-JACK-20240823: merge this code +bool AudioMidiManager::setOutputDeviceSampleRate(unsigned int sampleRate) +{ + LOGE("------ setSamplerate: %u", sampleRate); + if (m_spec.sampleRate == sampleRate) { + LOGE("------ SAME setSamplerate, doing nothing ------"); + return true; + } + m_spec.sampleRate = (int)sampleRate; + LOGE("------ CHANGED setSamplerate, doing nothing ------"); + if (!reopen(m_current_audioDriverState->name(), m_spec)) { + return false; + } + m_sampleRateChanged.notify(); + return true; +#if 0 + if (s_format.sampleRate == sampleRate) { + return true; + } + + bool reopen = isOpened(); + close(); + s_format.sampleRate = sampleRate; + + bool ok = true; + if (reopen) { + ok = open(s_format, &s_format); + } + + if (ok) { + m_sampleRateChanged.notify(); + } + + return ok; +#endif +} + +muse::async::Notification AudioMidiManager::outputDeviceSampleRateChanged() const +{ + return m_sampleRateChanged; +} + +std::vector AudioMidiManager::availableOutputDeviceSampleRates() const +{ + // ALSA API is not of any help to get sample rates supported by the driver. + // (snd_pcm_hw_params_get_rate_[min|max] will return 1 to 384000 Hz) + // So just returning a sensible hard-coded list. + return { + 44100, + 48000, + 88200, + 96000, + }; +} + +void AudioMidiManager::resume() +{ +} + +void AudioMidiManager::suspend() +{ +} diff --git a/src/framework/audio/internal/platform/lin/linuxaudiodriver.h b/src/framework/audio/internal/audiomidimanager.h similarity index 57% rename from src/framework/audio/internal/platform/lin/linuxaudiodriver.h rename to src/framework/audio/internal/audiomidimanager.h index f827178bb0824..06c923a0627c8 100644 --- a/src/framework/audio/internal/platform/lin/linuxaudiodriver.h +++ b/src/framework/audio/internal/audiomidimanager.h @@ -27,14 +27,19 @@ #include "iaudiodriver.h" -#include "audiodeviceslistener.h" +#include "platform/lin/audiodeviceslistener.h" +#include "playback/iplaybackconfiguration.h" +#include "playback/iplaybackcontroller.h" namespace muse::audio { -class LinuxAudioDriver : public IAudioDriver, public async::Asyncable +class AudioMidiManager : public IAudioDriver, public async::Asyncable { + Inject playbackConfiguration; + Inject playbackController; + public: - LinuxAudioDriver(); - ~LinuxAudioDriver(); + AudioMidiManager(); + ~AudioMidiManager(); void init() override; @@ -48,37 +53,55 @@ class LinuxAudioDriver : public IAudioDriver, public async::Asyncable AudioDeviceID outputDevice() const override; bool selectOutputDevice(const AudioDeviceID& deviceId) override; bool resetToDefaultOutputDevice() override; - async::Notification outputDeviceChanged() const override; + muse::async::Notification outputDeviceChanged() const override; AudioDeviceList availableOutputDevices() const override; - async::Notification availableOutputDevicesChanged() const override; + muse::async::Notification availableOutputDevicesChanged() const override; unsigned int outputDeviceBufferSize() const override; bool setOutputDeviceBufferSize(unsigned int bufferSize) override; - async::Notification outputDeviceBufferSizeChanged() const override; + muse::async::Notification outputDeviceBufferSizeChanged() const override; std::vector availableOutputDeviceBufferSizes() const override; + int audioDelayCompensate() const override; + void setAudioDelayCompensate(const int frames) override; + unsigned int outputDeviceSampleRate() const override; bool setOutputDeviceSampleRate(unsigned int sampleRate) override; - async::Notification outputDeviceSampleRateChanged() const override; + muse::async::Notification outputDeviceSampleRateChanged() const override; std::vector availableOutputDeviceSampleRates() const override; + void isPlayingChanged(); + void positionChanged(muse::audio::secs_t secs, muse::midi::tick_t tick); + + bool isPlaying() const override; + void remotePlayOrStop(bool) const override; + void remoteSeek(msecs_t) const override; + void resume() override; void suspend() override; private: - async::Notification m_outputDeviceChanged; + bool makeDevice(const AudioDeviceID& deviceId); + bool reopen(const AudioDeviceID& deviceId, Spec newSpec); + muse::async::Notification m_outputDeviceChanged; mutable std::mutex m_devicesMutex; +#ifndef Q_OS_MACOS AudioDevicesListener m_devicesListener; - async::Notification m_availableOutputDevicesChanged; +#endif + muse::async::Notification m_availableOutputDevicesChanged; std::string m_deviceId; - async::Notification m_bufferSizeChanged; - async::Notification m_sampleRateChanged; + muse::async::Notification m_bufferSizeChanged; + muse::async::Notification m_sampleRateChanged; + int m_audioDelayCompensate; + + struct IAudioDriver::Spec m_spec; + std::unique_ptr m_current_audioDriverState; }; } diff --git a/src/framework/audio/internal/audiooutputdevicecontroller.cpp b/src/framework/audio/internal/audiooutputdevicecontroller.cpp index fb1a572c41c5f..ba2888dbfd136 100644 --- a/src/framework/audio/internal/audiooutputdevicecontroller.cpp +++ b/src/framework/audio/internal/audiooutputdevicecontroller.cpp @@ -47,6 +47,15 @@ void AudioOutputDeviceController::init() onOutputDeviceChanged(); } }); + configuration()->sampleRateChanged().onNotify(this, [this]() { + unsigned int sampleRate = configuration()->sampleRate(); + bool ok = audioDriver()->setOutputDeviceSampleRate(sampleRate); + if (ok) { + async::Async::call(this, [this, sampleRate](){ + audioEngine()->setSampleRate(sampleRate); + }, AudioThread::ID); + } + }); configuration()->driverBufferSizeChanged().onNotify(this, [this]() { unsigned int bufferSize = configuration()->driverBufferSize(); diff --git a/src/framework/audio/internal/platform/alsa/alsaaudiodriver.cpp b/src/framework/audio/internal/platform/alsa/alsaaudiodriver.cpp new file mode 100644 index 0000000000000..3121f46efb86c --- /dev/null +++ b/src/framework/audio/internal/platform/alsa/alsaaudiodriver.cpp @@ -0,0 +1,207 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2023 MuseScore BVBA 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 "alsaaudiodriver.h" + +#define ALSA_PCM_NEW_HW_PARAMS_API +#include + +#include +#include +#include +#include +#include + +#include "translation.h" +#include "log.h" +#include "runtime.h" + +#define ALSA_DEFAULT_DEVICE_ID "default" + +using namespace muse::audio; + +static void* alsaThread(void* aParam) +{ + muse::runtime::setThreadName("audio_driver"); + AlsaDriverState* state = static_cast(aParam); + + int ret = snd_pcm_wait(static_cast(state->alsaDeviceHandle), 1000); + IF_ASSERT_FAILED(ret > 0) { + return nullptr; + } + + while (!state->audioProcessingDone) + { + uint8_t* stream = (uint8_t*)state->buffer; + int len = state->deviceSpec.samples * state->deviceSpec.channels * sizeof(float); + + state->deviceSpec.callback(state->deviceSpec.userdata, stream, len); + + snd_pcm_sframes_t pcm = snd_pcm_writei(static_cast(state->alsaDeviceHandle), state->buffer, state->deviceSpec.samples); + if (pcm != -EPIPE) { + } else { + snd_pcm_prepare(static_cast(state->alsaDeviceHandle)); + } + } + + LOGI() << "exit"; + return nullptr; +} + +AlsaDriverState::AlsaDriverState() +{ + deviceId = "alsa"; + m_deviceName = ALSA_DEFAULT_DEVICE_ID; +} + +AlsaDriverState::~AlsaDriverState() +{ + alsaCleanup(); +} + +void AlsaDriverState::alsaCleanup() +{ + audioProcessingDone = true; + if (m_threadHandle) { + pthread_join(m_threadHandle, nullptr); + } + if (alsaDeviceHandle != nullptr) { + snd_pcm_t* aDevice = static_cast(alsaDeviceHandle); + snd_pcm_drain(aDevice); + snd_pcm_close(aDevice); + alsaDeviceHandle = nullptr; + } + if (buffer) { + delete[] buffer; + } + buffer = nullptr; +} + +std::string AlsaDriverState::name() const +{ + return deviceId; +} + +std::string AlsaDriverState::deviceName() const +{ + return m_deviceName; +} + +void AlsaDriverState::deviceName(const std::string newDeviceName) +{ + m_deviceName = newDeviceName; +} + +void AlsaDriverState::setAudioDelayCompensate([[maybe_unused]] const int frames) +{ +} + +bool AlsaDriverState::open(const IAudioDriver::Spec& spec, IAudioDriver::Spec* activeSpec) +{ + deviceSpec.samples = spec.samples; + deviceSpec.channels = spec.channels; + deviceSpec.callback = spec.callback; + deviceSpec.userdata = spec.userdata; + + snd_pcm_t* handle; + int rc = snd_pcm_open(&handle, m_deviceName.c_str(), SND_PCM_STREAM_PLAYBACK, 0); + if (rc < 0) { + LOGE() << "Unable to open device: " << name() << ", err code: " << rc; + return false; + } + + alsaDeviceHandle = handle; + + snd_pcm_hw_params_t* params; + snd_pcm_hw_params_alloca(¶ms); + snd_pcm_hw_params_any(handle, params); + + snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED); + snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_FLOAT_LE); + snd_pcm_hw_params_set_channels(handle, params, spec.channels); + + unsigned int aSamplerate = spec.sampleRate; + unsigned int val = aSamplerate; + int dir = 0; + rc = snd_pcm_hw_params_set_rate_near(handle, params, &val, &dir); + if (rc < 0) { + LOGE() << "Unable to set sample rate: " << val << ", err code: " << rc; + return false; + } + + long unsigned int samples = deviceSpec.samples; + rc = snd_pcm_hw_params_set_buffer_size_near(handle, params, &samples); + if (rc < 0) { + LOGE() << "Unable to set buffer size: " << samples << ", err code: " << rc; + } + deviceSpec.samples = (int)samples; + + rc = snd_pcm_hw_params(handle, params); + if (rc < 0) { + return false; + } + + snd_pcm_hw_params_get_rate(params, &val, &dir); + aSamplerate = val; + deviceSpec.sampleRate = aSamplerate; + + if (buffer != nullptr) { + LOGW() << "open before close"; + delete[] buffer; + buffer = nullptr; + } + + buffer = new float[deviceSpec.samples * deviceSpec.channels]; + + if (activeSpec) { + *activeSpec = spec; + activeSpec->format = IAudioDriver::Format::AudioF32; + activeSpec->sampleRate = aSamplerate; + deviceSpec = *activeSpec; + } + + m_threadHandle = 0; + int ret = pthread_create(&m_threadHandle, NULL, alsaThread, (void*)this); + if (0 != ret) { + return false; + } + + LOGD() << "Connected to " << name(); + return true; +} + +void AlsaDriverState::close() +{ + alsaCleanup(); +} + +bool AlsaDriverState::isOpened() const +{ + return alsaDeviceHandle != nullptr; +} + +void AlsaDriverState::changedPlaying() const +{ +} + +void AlsaDriverState::changedPosition(muse::audio::secs_t secs, muse::midi::tick_t tick) const +{ +} diff --git a/src/framework/audio/internal/platform/alsa/alsaaudiodriver.h b/src/framework/audio/internal/platform/alsa/alsaaudiodriver.h new file mode 100644 index 0000000000000..1eaeb0bb9a403 --- /dev/null +++ b/src/framework/audio/internal/platform/alsa/alsaaudiodriver.h @@ -0,0 +1,59 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2023 MuseScore BVBA 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 MU_AUDIO_ALSAAUDIODRIVER_H +#define MU_AUDIO_ALSAAUDIODRIVER_H + +#include "iaudiodriver.h" + +namespace muse::audio { +class AlsaDriverState : public AudioDriverState +{ +public: + AlsaDriverState(); + ~AlsaDriverState(); + + std::string name() const override; + bool open(const IAudioDriver::Spec& spec, IAudioDriver::Spec* activeSpec) override; + void close() override; + bool isOpened() const override; + void setAudioDelayCompensate(const int frames) override; + std::string deviceName() const; + void deviceName(const std::string newDeviceName); + void changedPlaying() const override; + void changedPosition(muse::audio::secs_t secs, muse::midi::tick_t tick) const override; + + void* alsaDeviceHandle = nullptr; + float* buffer = nullptr; + bool audioProcessingDone = false; + +private: + void alsaCleanup(); + + pthread_t m_threadHandle = 0; + + async::Channel m_eventReceived; + std::string m_deviceName = "default"; +}; +} + +#endif // MU_AUDIO_ALSAAUDIODRIVER_H diff --git a/src/framework/audio/internal/platform/jack/audiodeviceslistener.cpp b/src/framework/audio/internal/platform/jack/audiodeviceslistener.cpp deleted file mode 100644 index ba08eceeff3b2..0000000000000 --- a/src/framework/audio/internal/platform/jack/audiodeviceslistener.cpp +++ /dev/null @@ -1,86 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-only - * MuseScore-CLA-applies - * - * MuseScore - * Music Composition & Notation - * - * Copyright (C) 2021 MuseScore BVBA 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 "audiodeviceslistener.h" - -#include - -#include "log.h" - -using namespace muse::audio; - -AudioDevicesListener::~AudioDevicesListener() -{ - stop(); -} - -void AudioDevicesListener::startWithCallback(const ActualDevicesCallback& callback) -{ - IF_ASSERT_FAILED(!m_isRunning) { - LOGE() << "Cannot set callback while already running."; - return; - } - - m_actualDevicesCallback = callback; - m_isRunning = true; - m_devicesUpdateThread = std::make_shared(&AudioDevicesListener::th_updateDevices, this); -} - -void AudioDevicesListener::stop() -{ - if (!m_devicesUpdateThread) { - return; - } - - m_isRunning = false; - m_runningCv.notify_all(); - - m_devicesUpdateThread->join(); - m_devicesUpdateThread = nullptr; -} - -async::Notification AudioDevicesListener::devicesChanged() const -{ - return m_devicesChanged; -} - -void AudioDevicesListener::th_updateDevices() -{ - std::unique_lock lock(m_mutex); - - while (m_isRunning) { - AudioDeviceList devices = m_actualDevicesCallback(); - - th_setDevices(devices); - - m_runningCv.wait_for(lock, std::chrono::milliseconds(5000)); - } -} - -void AudioDevicesListener::th_setDevices(const AudioDeviceList& devices) -{ - if (devices == m_devices) { - return; - } - - m_devices = devices; - m_devicesChanged.notify(); -} diff --git a/src/framework/audio/internal/platform/jack/audiodeviceslistener.h b/src/framework/audio/internal/platform/jack/audiodeviceslistener.h deleted file mode 100644 index 29095ceaa8a60..0000000000000 --- a/src/framework/audio/internal/platform/jack/audiodeviceslistener.h +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-only - * MuseScore-CLA-applies - * - * MuseScore - * Music Composition & Notation - * - * Copyright (C) 2021 MuseScore BVBA 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_AUDIODEVICESLISTENER_H -#define MUSE_AUDIO_AUDIODEVICESLISTENER_H - -#include -#include -#include - -#include "async/notification.h" -#include "audiotypes.h" - -namespace muse::audio { -class AudioDevicesListener -{ -public: - ~AudioDevicesListener(); - - using ActualDevicesCallback = std::function; - - void startWithCallback(const ActualDevicesCallback& callback); - - async::Notification devicesChanged() const; - -private: - void th_updateDevices(); - void th_setDevices(const AudioDeviceList& devices); - void stop(); - - std::shared_ptr m_devicesUpdateThread; - std::atomic m_isRunning = false; - - mutable std::mutex m_mutex; - std::condition_variable m_runningCv; - - AudioDeviceList m_devices; - async::Notification m_devicesChanged; - - ActualDevicesCallback m_actualDevicesCallback; -}; -} - -#endif // MUSE_AUDIO_AUDIODEVICESLISTENER_H diff --git a/src/framework/audio/internal/platform/jack/jackaudiodriver.cpp b/src/framework/audio/internal/platform/jack/jackaudiodriver.cpp index 3245ec240d300..e0ae70a63289a 100644 --- a/src/framework/audio/internal/platform/jack/jackaudiodriver.cpp +++ b/src/framework/audio/internal/platform/jack/jackaudiodriver.cpp @@ -5,7 +5,7 @@ * MuseScore * Music Composition & Notation * - * Copyright (C) 2021 MuseScore BVBA and others + * Copyright (C) MuseScore BVBA 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 @@ -20,324 +20,514 @@ * along with this program. If not, see . */ #include "jackaudiodriver.h" +#include #include #include #include #include -#include -#include // Used by usleep -#include // Used by usleep +#include +#include +#include #include "translation.h" #include "log.h" #include "runtime.h" -using namespace muse::audio; +/* How many milliseconds to we allow musescore to be out-of-sync to jack + * before we tell musescore to adjust (seek) its position + */ +#define FRAMESLIMIT 500 -struct JackData -{ - float* buffer = nullptr; - jack_client_t* jackDeviceHandle = nullptr; - unsigned long samples = 0; - int channels = 0; - std::vector outputPorts; - IAudioDriver::Callback callback; - void* userdata = nullptr; -}; - -static JackData* s_jackData{ nullptr }; -static muse::audio::IAudioDriver::Spec s_format2; - -int muse::audio::jack_process_callback(jack_nframes_t nframes, void*) -{ - JackData* data = s_jackData; +#define JACK_DEFAULT_DEVICE_ID "jack" +#define JACK_DEFAULT_IDENTIFY_AS "MuseScore" +static int g_jackTransportDelay = 0; - jack_default_audio_sample_t* l = (float*)jack_port_get_buffer(data->outputPorts[0], nframes); - jack_default_audio_sample_t* r = (float*)jack_port_get_buffer(data->outputPorts[1], nframes); +using namespace muse::audio; +namespace muse::audio { +static bool g_transportEnable = false; +// variables to communicate between soft-realtime jack thread and musescore +static msecs_t mpos_frame; // musescore frame and state +static jack_nframes_t muse_frame; // musescore frame and state +static jack_transport_state_t muse_state; +static jack_nframes_t jack_frame; // jack frame and state +static jack_transport_state_t jack_state; +static int g_musescore_is_synced; // tells jack-transport if musescore is synced +static jack_nframes_t g_nframes; +unsigned int g_samplerate; +msecs_t g_frameslimit; +static jack_nframes_t muse_seek_requested; +static int muse_seek_countdown = 0; + +std::chrono::time_point musescore_act_time; +static bool musescore_act; +static msecs_t musescore_act_seek; +static bool running_musescore_state; +//static std::vector threads; +static JackDriverState* s_jackDriver; + +muse::audio::secs_t g_secs = 0; +muse::midi::tick_t g_tick = 0; + +void musescore_state_check_musescore() +{ + jack_nframes_t pos = mpos_frame; + if (pos > static_cast(g_jackTransportDelay)) { + pos -= g_jackTransportDelay; + } + muse_frame = pos; - uint8_t* stream = (uint8_t*)data->buffer; - data->callback(data->userdata, stream, nframes * data->channels * sizeof(float)); - float* sp = data->buffer; - for (size_t i = 0; i < nframes; i++) { - *l++ = *sp++; - *r++ = *sp++; + if (s_jackDriver->isPlaying()) { + muse_state = JackTransportRolling; + } else { + muse_state = JackTransportStopped; } - return 0; } -void muse::audio::jack_cleanup_callback(void*) +void musescore_state_do_seek() { + auto now = std::chrono::steady_clock::now(); + auto diff = std::chrono::duration_cast(now - musescore_act_time); + auto ms = static_cast(diff.count()); + auto millis = static_cast((double)musescore_act_seek * 1000 / (double)g_samplerate); + + millis = static_cast(std::max(static_cast(millis - ms), 0L)); + LOGW("Jack mst: really do musescore-seek to %lu (%lims) (diff: %i) mf=%u/%li jf=%u lag: %lims", + musescore_act_seek, millis, muse_frame - jack_frame, muse_frame, mpos_frame, jack_frame, ms); + s_jackDriver->remoteSeek(millis); + //mpos_frame = static_cast(millis * g_samplerate / 1000); } -void jackCleanup() +/* + * state thread + */ +void musescore_state() { - if (!s_jackData) { - return; - } - - if (nullptr != s_jackData->buffer) { - delete[] s_jackData->buffer; + LOGI("Jack: start musescore_state thread"); + int is_seeking = 0; + int cnt = 0; + while (running_musescore_state) { + musescore_state_check_musescore(); + cnt++; + if (cnt > 4) { + cnt = 0; + /* + LOGI("state: mframe=%u/%li jframe=%u (framedrift: %i / %i) ms=%s js=%s", + muse_frame, mpos_frame, jack_frame, + muse_frame - jack_frame, + mpos_frame - jack_frame, + (muse_state == JackTransportStopped ? "stop" + : (muse_state == JackTransportStarting ? "start" + : (muse_state == JackTransportRolling ? "roll" : "other"))), + (jack_state == JackTransportStopped ? "stop" + : (jack_state == JackTransportStarting ? "start" + : (jack_state == JackTransportRolling ? "roll" : "other")))); + */ + } + if (musescore_act) { + if (is_seeking) { + // already seeking + } else { + if (labs(muse_frame - jack_frame) > g_frameslimit + && muse_seek_countdown == 0) { // && (muse_frame - jack_frame) != 0) { + musescore_state_do_seek(); + is_seeking = 1; + } else { + LOGI("Jack mst: act avoid musescore-seek to %lu (jack: %u) ", musescore_act_seek, jack_frame); + musescore_act = false; + } + } + } + if (is_seeking) { + is_seeking++; + if (is_seeking > 10) { + is_seeking = 0; + musescore_act = false; + } + } + if (muse_seek_countdown) { + muse_seek_countdown--; + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); } - - delete s_jackData; - s_jackData = nullptr; + LOGW("Jack: quiting musescore_state thread"); } -JackAudioDriver::JackAudioDriver() +bool musescore_seek(unsigned int pos) { - m_deviceId = DEFAULT_DEVICE_ID; + if (musescore_act) { + return false; // already seeking + } else { + musescore_act_seek = pos; + musescore_act = true; + musescore_act_time = std::chrono::steady_clock::now(); // record clock that pos is valid for + } + return true; } -JackAudioDriver::~JackAudioDriver() +void JackDriverState::changedPlaying() const { - jackCleanup(); + if (!g_transportEnable) { + return; + } + jack_client_t* client = static_cast(jackDeviceHandle); + + if (s_jackDriver->isPlaying()) { + jack_nframes_t frames = static_cast(s_jackDriver->playbackPositionInSeconds() * g_samplerate); + LOGW("musescore tell transport to START at %u", frames); + jack_transport_locate(client, frames); + jack_transport_start(client); + } else { + LOGW("musescore tell transport to STOP"); + jack_transport_stop(client); + } } -void JackAudioDriver::init() +void JackDriverState::changedPosition(muse::audio::secs_t secs, muse::midi::tick_t tick) const { - m_devicesListener.startWithCallback([this]() { - return availableOutputDevices(); - }); + // ugly, but provide position to playbackPositionInSeconds + g_secs = secs; + g_tick = tick; - m_devicesListener.devicesChanged().onNotify(this, [this]() { - m_availableOutputDevicesChanged.notify(); - }); + if (!g_transportEnable) { + return; + } + jack_client_t* client = static_cast(jackDeviceHandle); + jack_nframes_t frames = static_cast(s_jackDriver->playbackPositionInSeconds() * g_samplerate); + + // worker thread is updating muse_frame (which runs every 100ms) + // thats why we can get out of sync with "ourself" + if (labs((long int)muse_frame - (long int)frames) > g_frameslimit) { + LOGW("musescore tell transport to LOCATE mf=%u, fr=%u diff: %i (lim: %li) g_secs: %f", + muse_frame, frames, + muse_frame - frames, + g_frameslimit, g_secs.to_double()); + jack_transport_locate(client, frames); + muse_frame = mpos_frame = frames; + muse_seek_countdown = 10; // KLUDGE: avoid transport seeking musescore for some time + } } -std::string JackAudioDriver::name() const +// musescore has around 200ms inaccuracy in playbackPositionInSeconds +bool is_muse_jack_frame_sync(jack_nframes_t mf, jack_nframes_t jf) { - return "MUAUDIO(JACK)"; + return labs((long int)jack_frame - (long int)muse_frame + g_jackTransportDelay) < g_frameslimit; } -int jack_srate_callback(jack_nframes_t nframes, void* args) +bool is_muse_jack_state_sync(jack_transport_state_t ms, jack_transport_state_t js) { - IAudioDriver::Spec* spec = (IAudioDriver::Spec*)args; - LOGI() << "Jack reported sampleRate change. Pray to god, musescores samplerate: " << spec->sampleRate << ", is the same as jacks: " << - nframes; - return 0; + if (muse_state == JackTransportRolling) { + return jack_state == JackTransportRolling + || jack_state == JackTransportStarting; + } else { + return jack_state == JackTransportStopped; + } } -bool JackAudioDriver::open(const Spec& spec, Spec* activeSpec) +void jack_muse_update_verify_sync(JackDriverState* state, jack_client_t* client) { - s_jackData = new JackData(); - // s_jackData->samples = spec.samples; // client doesn't set sample-rate - s_jackData->channels = spec.channels; - s_jackData->callback = spec.callback; - s_jackData->userdata = spec.userdata; - // FIX: "default" is not a good name for jack-clients - // const char *clientName = - // outputDevice().c_str() == "default" ? "MuseScore" : - // outputDevice().c_str(); - const char* clientName = "MuseScore"; - LOGI() << "clientName: " << clientName; - - jack_status_t status; - jack_client_t* handle; - if (!(handle = jack_client_open(clientName, JackNullOption, &status))) { - LOGE() << "jack_client_open() failed: " << status; - return false; + jack_position_t jpos; + jack_state = jack_transport_query(client, &jpos); + jack_frame = jpos.frame; + if (is_muse_jack_state_sync(muse_state, jack_state) + && is_muse_jack_frame_sync(muse_frame, jack_frame)) { + g_musescore_is_synced = 1; + } else { + g_musescore_is_synced = 0; } +} - jack_set_sample_rate_callback(handle, jack_srate_callback, (void*)&spec); - - s_jackData->jackDeviceHandle = handle; +static int framecnt = 0; - jack_port_t* output_port_left = jack_port_register(handle, "audio_out_left", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); - s_jackData->outputPorts.push_back(output_port_left); - jack_port_t* output_port_right = jack_port_register(handle, "audio_out_right", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); - s_jackData->outputPorts.push_back(output_port_right); +void check_jack_midi_transport(JackDriverState* state, jack_nframes_t nframes) +{ + jack_client_t* client = static_cast(state->jackDeviceHandle); - s_jackData->samples = jack_get_buffer_size(handle); - LOGI() << "buffer size (in samples): " << s_jackData->samples; + jack_muse_update_verify_sync(state, client); - unsigned int jackSamplerate = jack_get_sample_rate(handle); - LOGI() << "sampleRate used by jack: " << jackSamplerate; - if (spec.sampleRate != jackSamplerate) { - LOGW() << "Musescores samplerate: " << spec.sampleRate << ", is NOT the same as jack's: " << jackSamplerate; - // FIX: enable this if it is possible for user to adjust samplerate (AUDIO_SAMPLE_RATE_KEY) - //jack_client_close(handle); - //return false; + if (g_musescore_is_synced) { + return; } - s_jackData->buffer = new float[s_jackData->samples * s_jackData->channels]; - - if (activeSpec) { - *activeSpec = spec; - activeSpec->format = Format::AudioF32; - activeSpec->sampleRate = jackSamplerate; - s_format2 = *activeSpec; + framecnt++; + if (framecnt > 40) { + framecnt = 0; + LOGI("jack-transport: mframe=%u/%li jframe=%u d=%i ms=%s js=%s nf=%i d=%i\n", + muse_frame, mpos_frame, jack_frame, + muse_frame - jack_frame, + (muse_state == JackTransportStopped ? "stop" + : (muse_state == JackTransportStarting ? "start" + : (muse_state == JackTransportRolling ? "roll" : "other"))), + (jack_state == JackTransportStopped ? "stop" + : (jack_state == JackTransportStarting ? "start" + : (jack_state == JackTransportRolling ? "roll" : "other"))), + nframes, + g_jackTransportDelay); } - jack_on_shutdown(handle, jack_cleanup_callback, 0); - jack_set_process_callback(handle, jack_process_callback, (void*)&s_jackData); - - if (jack_activate(handle)) { - LOGE() << "cannot activate client"; - return false; + bool state_sync = false; + if (muse_state == JackTransportStopped + && (jack_state == JackTransportStarting || jack_state == JackTransportRolling)) { + state->remotePlayOrStop(true); + muse_state = JackTransportRolling; + } else if (muse_state == JackTransportRolling + && jack_state == JackTransportStopped) { + state->remotePlayOrStop(false); + muse_state = JackTransportStopped; + } else { + state_sync = true; } - return true; + if (!(is_muse_jack_frame_sync(muse_frame, jack_frame))) { + jack_nframes_t pos = jack_frame; + if (pos > static_cast(g_jackTransportDelay)) { + pos -= g_jackTransportDelay; + } else { + pos = 0; + } + muse_seek_requested = pos; + musescore_seek(pos); + } else { + g_musescore_is_synced = state_sync; // only sync if both state and position matches jack + LOGW("jack-transport: SYNCED!"); + } } -void JackAudioDriver::close() +// because jack callbacks are soft-realtime we use no resources +static int handle_jack_sync(jack_transport_state_t ts, jack_position_t* pos, void* args) { - jackCleanup(); + if (jack_frame != pos->frame + || jack_state != ts + || (!is_muse_jack_frame_sync(jack_frame, muse_frame)) + || (!is_muse_jack_state_sync(jack_state, muse_state)) + ) { + jack_frame = pos->frame; + jack_state = ts; + g_musescore_is_synced = 0; + } + /* + LOGW("jack-transport: SYNC ms=%s ts=%s m/j-frame: %lu/%lu sync? %s", + (muse_state == JackTransportStopped ? "stop" : + (muse_state == JackTransportStarting ? "start" : + (muse_state == JackTransportRolling ? "roll" : "other"))), + (ts == JackTransportStopped ? "stop" : + (ts == JackTransportStarting ? "start" : + (ts == JackTransportRolling ? "roll" : "other"))), + muse_frame, + jack_frame, + g_musescore_is_synced ? "---- YES ----" : " -- no --"); + */ + return g_musescore_is_synced; } -bool JackAudioDriver::isOpened() const +/* + * AUDIO + */ + +static int jack_process_callback(jack_nframes_t nframes, void* args) { - return s_jackData != nullptr; + JackDriverState* state = static_cast(args); + + jack_default_audio_sample_t* l = (float*)jack_port_get_buffer(state->outputPorts[0], nframes); + jack_default_audio_sample_t* r = (float*)jack_port_get_buffer(state->outputPorts[1], nframes); + + uint8_t* stream = (uint8_t*)state->buffer; + state->deviceSpec.callback(state->deviceSpec.userdata, stream, nframes * state->deviceSpec.channels * sizeof(float)); + float* sp = state->buffer; + for (size_t i = 0; i < nframes; i++) { + *l++ = *sp++; + *r++ = *sp++; + } + jack_client_t* client = static_cast(state->jackDeviceHandle); + // if (!isConnected()) { + // LOGI() << "---- JACK-midi output sendEvent SORRY, not connected"; + // return make_ret(Err::MidiNotConnected); + // } + + if (g_transportEnable) { + if (muse_state == JackTransportRolling) { + mpos_frame += nframes; + } + check_jack_midi_transport(state, nframes); + } + + return 0; } -const JackAudioDriver::Spec& JackAudioDriver::activeSpec() const +static int handle_buffersize_change(jack_nframes_t nframes, void* arg) { - return s_format2; + g_nframes = nframes; + return 0; // successfully reallocated buffer } -AudioDeviceID JackAudioDriver::outputDevice() const +static void jack_cleanup_callback(void*) { - return m_deviceId; +} } -bool JackAudioDriver::selectOutputDevice(const AudioDeviceID& deviceId) +JackDriverState::JackDriverState(IAudioDriver* amm, bool transportEnable) { - if (m_deviceId == deviceId) { - return true; - } - - bool reopen = isOpened(); - close(); - m_deviceId = deviceId; - - bool ok = true; - if (reopen) { - ok = open(s_format2, &s_format2); - } + s_jackDriver = this; + deviceId = JACK_DEFAULT_DEVICE_ID; + m_deviceName = JACK_DEFAULT_IDENTIFY_AS; + m_audiomidiManager = amm; + g_transportEnable = transportEnable; +} - if (ok) { - m_outputDeviceChanged.notify(); +JackDriverState::~JackDriverState() +{ + if (jackDeviceHandle != nullptr) { + jack_client_close(static_cast(jackDeviceHandle)); } - - return ok; + delete[] buffer; } -bool JackAudioDriver::resetToDefaultOutputDevice() +std::string JackDriverState::name() const { - return selectOutputDevice(DEFAULT_DEVICE_ID); + return deviceId; } -async::Notification JackAudioDriver::outputDeviceChanged() const +std::string JackDriverState::deviceName() const { - return m_outputDeviceChanged; + return m_deviceName; } -AudioDeviceList JackAudioDriver::availableOutputDevices() const +void JackDriverState::deviceName(const std::string newDeviceName) { - AudioDeviceList devices; - devices.push_back({ DEFAULT_DEVICE_ID, muse::trc("audio", "System default") }); - - return devices; + m_deviceName = newDeviceName; } -async::Notification JackAudioDriver::availableOutputDevicesChanged() const +int jack_srate_callback(jack_nframes_t newSampleRate, void* args) { - return m_availableOutputDevicesChanged; + IAudioDriver::Spec* spec = (IAudioDriver::Spec*)args; + if (newSampleRate != spec->sampleRate) { + LOGW() << "Jack reported system sampleRate change. new samplerate: " << newSampleRate << ", MuseScore: " << spec->sampleRate; + // FIX: notify Musescore audio-layer to adjust musescores samplerate + } + spec->sampleRate = newSampleRate; + return 0; } -unsigned int JackAudioDriver::outputDeviceBufferSize() const +void JackDriverState::setAudioDelayCompensate(const int frames) { - return s_format2.samples; + g_jackTransportDelay = frames; } -bool JackAudioDriver::setOutputDeviceBufferSize(unsigned int bufferSize) +bool JackDriverState::open(const IAudioDriver::Spec& spec, IAudioDriver::Spec* activeSpec) { - if (s_format2.samples == bufferSize) { + LOGI("using jack-transport: %b, jackTransportDelay: %i", g_transportEnable, g_jackTransportDelay); + if (isOpened()) { + LOGW() << "Jack is already opened"; return true; } - bool reopen = isOpened(); - close(); - s_format2.samples = bufferSize; + deviceSpec.samples = spec.samples; // client doesn't set sample-rate + deviceSpec.channels = spec.channels; + deviceSpec.callback = spec.callback; + deviceSpec.userdata = spec.userdata; - bool ok = true; - if (reopen) { - ok = open(s_format2, &s_format2); + const char* clientName = m_deviceName.c_str(); + jack_status_t status; + jack_client_t* handle; + if (!(handle = jack_client_open(clientName, JackNullOption, &status))) { + LOGE() << "jack_client_open() failed: " << status; + return false; + } + jackDeviceHandle = handle; + + unsigned int jackSamplerate = jack_get_sample_rate(handle); + deviceSpec.sampleRate = jackSamplerate; + g_samplerate = jackSamplerate; + // FIX: at samplerate change, this need to be adjusted + g_frameslimit = static_cast((double)g_samplerate * (double)FRAMESLIMIT / 1000.0L); + if (spec.sampleRate != jackSamplerate) { + LOGW() << "Musescores samplerate: " << spec.sampleRate << ", is NOT the same as jack's: " << jackSamplerate; + // FIX: enable this if it is possible for user to adjust samplerate (AUDIO_SAMPLE_RATE_KEY) + //jack_client_close(handle); + //return false; } - if (ok) { - m_bufferSizeChanged.notify(); + if (g_transportEnable) { + // start musescore state thread + running_musescore_state = true; + std::thread thread_musescore_state(musescore_state); + thread_musescore_state.detach(); + //std::vector threadv; + //threadv.push_back(std::move(thread_musescore_state)); + //threads = std::move(threadv); } - return ok; -} + jack_set_sample_rate_callback(handle, jack_srate_callback, (void*)&deviceSpec); -async::Notification JackAudioDriver::outputDeviceBufferSizeChanged() const -{ - return m_bufferSizeChanged; -} + jack_port_t* output_port_left = jack_port_register(handle, "audio_out_left", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); + outputPorts.push_back(output_port_left); + jack_port_t* output_port_right = jack_port_register(handle, "audio_out_right", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); + outputPorts.push_back(output_port_right); + deviceSpec.samples = jack_get_buffer_size(handle); + buffer = new float[deviceSpec.samples * deviceSpec.channels]; -std::vector JackAudioDriver::availableOutputDeviceBufferSizes() const -{ - std::vector result; + if (activeSpec) { + *activeSpec = spec; + activeSpec->format = IAudioDriver::Format::AudioF32; + activeSpec->sampleRate = jackSamplerate; + deviceSpec = *activeSpec; + } - unsigned int n = MAXIMUM_BUFFER_SIZE; - while (n >= MINIMUM_BUFFER_SIZE) { - result.push_back(n); - n /= 2; + jack_on_shutdown(handle, jack_cleanup_callback, (void*)this); + jack_set_process_callback(handle, jack_process_callback, (void*)this); + jack_set_sync_callback(handle, handle_jack_sync, (void*)this); + jack_set_buffer_size_callback(handle, handle_buffersize_change, NULL); + if (jack_activate(handle)) { + LOGE() << "cannot activate client"; + return false; + } + + muse_seek_requested = 0; + mpos_frame = muse_frame = static_cast(s_jackDriver->playbackPositionInSeconds() * jackSamplerate); + if (muse_frame >= g_jackTransportDelay) { + muse_frame -= g_jackTransportDelay; } - std::sort(result.begin(), result.end()); + if (s_jackDriver->isPlaying()) { + muse_state = JackTransportRolling; + } else { + muse_state = JackTransportStopped; + } - return result; + return true; } -unsigned int JackAudioDriver::outputDeviceSampleRate() const +void JackDriverState::close() { - return s_format2.sampleRate; + jack_client_close(static_cast(jackDeviceHandle)); + jackDeviceHandle = nullptr; + delete[] buffer; + buffer = nullptr; } -bool JackAudioDriver::setOutputDeviceSampleRate(unsigned int sampleRate) +bool JackDriverState::isOpened() const { - if (s_format2.sampleRate == sampleRate) { - return true; - } - - bool reopen = isOpened(); - close(); - s_format2.sampleRate = sampleRate; - - bool ok = true; - if (reopen) { - ok = open(s_format2, &s_format2); - } - - if (ok) { - m_sampleRateChanged.notify(); - } - - return ok; + return jackDeviceHandle != nullptr; } -async::Notification JackAudioDriver::outputDeviceSampleRateChanged() const +bool JackDriverState::isPlaying() const { - return m_sampleRateChanged; + return m_audiomidiManager->isPlaying(); } -std::vector JackAudioDriver::availableOutputDeviceSampleRates() const +// FIX: return type double +float JackDriverState::playbackPositionInSeconds() const { - return { - 44100, - 48000, - 88200, - 96000, - }; + // this round-trip could be avoided if the caller uses info from changedposition + return g_secs.to_double(); } -void JackAudioDriver::resume() +void JackDriverState::remotePlayOrStop(bool ps) const { + m_audiomidiManager->remotePlayOrStop(ps); } -void JackAudioDriver::suspend() +void JackDriverState::remoteSeek(msecs_t millis) const { + m_audiomidiManager->remoteSeek(millis); } diff --git a/src/framework/audio/internal/platform/jack/jackaudiodriver.h b/src/framework/audio/internal/platform/jack/jackaudiodriver.h index 0d488693a9a10..b75ffd5c7378d 100644 --- a/src/framework/audio/internal/platform/jack/jackaudiodriver.h +++ b/src/framework/audio/internal/platform/jack/jackaudiodriver.h @@ -5,7 +5,7 @@ * MuseScore * Music Composition & Notation * - * Copyright (C) 2021 MuseScore BVBA and others + * Copyright (C) MuseScore BVBA 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 @@ -25,63 +25,41 @@ #include -#include "async/asyncable.h" #include "iaudiodriver.h" -#include "audiodeviceslistener.h" +#include "playback/iplaybackcontroller.h" namespace muse::audio { -int jack_process_callback(jack_nframes_t nframes, void* jackParam); -void jack_cleanup_callback(void* args); - -class JackAudioDriver : public IAudioDriver, public async::Asyncable +class JackDriverState : public AudioDriverState, public async::Asyncable { public: - JackAudioDriver(); - ~JackAudioDriver(); - - void init() override; + JackDriverState(IAudioDriver* amm, bool transportEnable); + ~JackDriverState(); std::string name() const override; - bool open(const Spec& spec, Spec* activeSpec) override; + bool open(const IAudioDriver::Spec& spec, IAudioDriver::Spec* activeSpec) override; void close() override; bool isOpened() const override; + void setAudioDelayCompensate(const int frames) override; - const Spec& activeSpec() const override; - - AudioDeviceID outputDevice() const override; - bool selectOutputDevice(const AudioDeviceID& deviceId) override; - bool resetToDefaultOutputDevice() override; - async::Notification outputDeviceChanged() const override; - - AudioDeviceList availableOutputDevices() const override; - async::Notification availableOutputDevicesChanged() const override; - - unsigned int outputDeviceBufferSize() const override; - bool setOutputDeviceBufferSize(unsigned int bufferSize) override; - async::Notification outputDeviceBufferSizeChanged() const override; + std::string deviceName() const; + void deviceName(const std::string newDeviceName); - std::vector availableOutputDeviceBufferSizes() const override; + void changedPlaying() const override; + void changedPosition(muse::audio::secs_t secs, muse::midi::tick_t tick) const override; - unsigned int outputDeviceSampleRate() const override; - bool setOutputDeviceSampleRate(unsigned int bufferSize) override; - async::Notification outputDeviceSampleRateChanged() const override; + bool isPlaying() const; + float playbackPositionInSeconds() const; + void remotePlayOrStop(bool) const; + void remoteSeek(msecs_t) const; - std::vector availableOutputDeviceSampleRates() const override; - - void resume() override; - void suspend() override; + void* jackDeviceHandle = nullptr; + float* buffer = nullptr; + std::vector outputPorts; + mu::playback::IPlaybackController* playbackController; private: - async::Notification m_outputDeviceChanged; - - mutable std::mutex m_devicesMutex; - AudioDevicesListener m_devicesListener; - async::Notification m_availableOutputDevicesChanged; - - std::string m_deviceId; - - async::Notification m_bufferSizeChanged; - async::Notification m_sampleRateChanged; + IAudioDriver* m_audiomidiManager; + std::string m_deviceName; }; } diff --git a/src/framework/audio/internal/platform/lin/linuxaudiodriver.cpp b/src/framework/audio/internal/platform/lin/linuxaudiodriver.cpp deleted file mode 100644 index 7c73c5975f694..0000000000000 --- a/src/framework/audio/internal/platform/lin/linuxaudiodriver.cpp +++ /dev/null @@ -1,371 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-only - * MuseScore-CLA-applies - * - * MuseScore - * Music Composition & Notation - * - * Copyright (C) 2021 MuseScore BVBA 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 "linuxaudiodriver.h" - -#define ALSA_PCM_NEW_HW_PARAMS_API -#include - -#include -#include -#include -#include -#include - -#include "translation.h" -#include "log.h" -#include "runtime.h" - -using namespace muse; -using namespace muse::audio; - -namespace { -struct ALSAData -{ - float* buffer = nullptr; - snd_pcm_t* alsaDeviceHandle = nullptr; - unsigned long samples = 0; - int channels = 0; - bool audioProcessingDone = false; - pthread_t threadHandle = 0; - IAudioDriver::Callback callback; - void* userdata = nullptr; -}; - -static ALSAData* s_alsaData{ nullptr }; -static muse::audio::IAudioDriver::Spec s_format; - -static void* alsaThread(void* aParam) -{ - muse::runtime::setThreadName("audio_driver"); - ALSAData* data = static_cast(aParam); - - int ret = snd_pcm_wait(data->alsaDeviceHandle, 1000); - IF_ASSERT_FAILED(ret > 0) { - return nullptr; - } - - while (!data->audioProcessingDone) - { - uint8_t* stream = (uint8_t*)data->buffer; - int len = data->samples * data->channels * sizeof(float); - - data->callback(data->userdata, stream, len); - - snd_pcm_sframes_t pcm = snd_pcm_writei(data->alsaDeviceHandle, data->buffer, data->samples); - if (pcm != -EPIPE) { - } else { - snd_pcm_prepare(data->alsaDeviceHandle); - } - } - - LOGI() << "exit"; - return nullptr; -} - -static void alsaCleanup() -{ - if (!s_alsaData) { - return; - } - - s_alsaData->audioProcessingDone = true; - if (s_alsaData->threadHandle) { - pthread_join(s_alsaData->threadHandle, nullptr); - } - if (nullptr != s_alsaData->alsaDeviceHandle) { - snd_pcm_drain(s_alsaData->alsaDeviceHandle); - snd_pcm_close(s_alsaData->alsaDeviceHandle); - } - - if (nullptr != s_alsaData->buffer) { - delete[] s_alsaData->buffer; - } - - delete s_alsaData; - s_alsaData = nullptr; -} -} - -LinuxAudioDriver::LinuxAudioDriver() -{ - m_deviceId = DEFAULT_DEVICE_ID; -} - -LinuxAudioDriver::~LinuxAudioDriver() -{ - alsaCleanup(); -} - -void LinuxAudioDriver::init() -{ - m_devicesListener.startWithCallback([this]() { - return availableOutputDevices(); - }); - - m_devicesListener.devicesChanged().onNotify(this, [this]() { - m_availableOutputDevicesChanged.notify(); - }); -} - -std::string LinuxAudioDriver::name() const -{ - return "MUAUDIO(ALSA)"; -} - -bool LinuxAudioDriver::open(const Spec& spec, Spec* activeSpec) -{ - s_alsaData = new ALSAData(); - s_alsaData->samples = spec.samples; - s_alsaData->channels = spec.channels; - s_alsaData->callback = spec.callback; - s_alsaData->userdata = spec.userdata; - - snd_pcm_t* handle; - int rc = snd_pcm_open(&handle, outputDevice().c_str(), SND_PCM_STREAM_PLAYBACK, 0); - if (rc < 0) { - LOGE() << "Unable to open device: " << outputDevice() << ", err code: " << rc; - return false; - } - - s_alsaData->alsaDeviceHandle = handle; - - snd_pcm_hw_params_t* params; - snd_pcm_hw_params_alloca(¶ms); - snd_pcm_hw_params_any(handle, params); - - snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED); - snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_FLOAT_LE); - snd_pcm_hw_params_set_channels(handle, params, spec.channels); - - unsigned int aSamplerate = spec.sampleRate; - unsigned int val = aSamplerate; - int dir = 0; - rc = snd_pcm_hw_params_set_rate_near(handle, params, &val, &dir); - if (rc < 0) { - LOGE() << "Unable to set sample rate: " << val << ", err code: " << rc; - return false; - } - - rc = snd_pcm_hw_params_set_buffer_size_near(handle, params, &s_alsaData->samples); - if (rc < 0) { - LOGE() << "Unable to set buffer size: " << s_alsaData->samples << ", err code: " << rc; - } - - rc = snd_pcm_hw_params(handle, params); - if (rc < 0) { - LOGE() << "Unable to set params, err code: " << rc; - return false; - } - - snd_pcm_hw_params_get_rate(params, &val, &dir); - aSamplerate = val; - - s_alsaData->buffer = new float[s_alsaData->samples * s_alsaData->channels]; - //_alsaData->sampleBuffer = new short[_alsaData->samples * _alsaData->channels]; - - if (activeSpec) { - *activeSpec = spec; - activeSpec->format = Format::AudioF32; - activeSpec->sampleRate = aSamplerate; - s_format = *activeSpec; - } - - s_alsaData->threadHandle = 0; - int ret = pthread_create(&s_alsaData->threadHandle, NULL, alsaThread, (void*)s_alsaData); - - if (0 != ret) { - LOGE() << "Unable to create audio thread, err code: " << ret; - return false; - } - - LOGI() << "Connected to " << outputDevice() - << " with bufferSize " << s_format.samples - << ", sampleRate " << s_format.sampleRate - << ", channels: " << s_format.channels; - - return true; -} - -void LinuxAudioDriver::close() -{ - alsaCleanup(); -} - -bool LinuxAudioDriver::isOpened() const -{ - return s_alsaData != nullptr; -} - -const LinuxAudioDriver::Spec& LinuxAudioDriver::activeSpec() const -{ - return s_format; -} - -AudioDeviceID LinuxAudioDriver::outputDevice() const -{ - return m_deviceId; -} - -bool LinuxAudioDriver::selectOutputDevice(const AudioDeviceID& deviceId) -{ - if (m_deviceId == deviceId) { - return true; - } - - bool reopen = isOpened(); - close(); - m_deviceId = deviceId; - - bool ok = true; - if (reopen) { - ok = open(s_format, &s_format); - } - - if (ok) { - m_outputDeviceChanged.notify(); - } - - return ok; -} - -bool LinuxAudioDriver::resetToDefaultOutputDevice() -{ - return selectOutputDevice(DEFAULT_DEVICE_ID); -} - -async::Notification LinuxAudioDriver::outputDeviceChanged() const -{ - return m_outputDeviceChanged; -} - -AudioDeviceList LinuxAudioDriver::availableOutputDevices() const -{ - AudioDeviceList devices; - devices.push_back({ DEFAULT_DEVICE_ID, muse::trc("audio", "System default") }); - - return devices; -} - -async::Notification LinuxAudioDriver::availableOutputDevicesChanged() const -{ - return m_availableOutputDevicesChanged; -} - -unsigned int LinuxAudioDriver::outputDeviceBufferSize() const -{ - return s_format.samples; -} - -bool LinuxAudioDriver::setOutputDeviceBufferSize(unsigned int bufferSize) -{ - if (s_format.samples == bufferSize) { - return true; - } - - bool reopen = isOpened(); - close(); - s_format.samples = bufferSize; - - bool ok = true; - if (reopen) { - ok = open(s_format, &s_format); - } - - if (ok) { - m_bufferSizeChanged.notify(); - } - - return ok; -} - -async::Notification LinuxAudioDriver::outputDeviceBufferSizeChanged() const -{ - return m_bufferSizeChanged; -} - -std::vector LinuxAudioDriver::availableOutputDeviceBufferSizes() const -{ - std::vector result; - - unsigned int n = MAXIMUM_BUFFER_SIZE; - while (n >= MINIMUM_BUFFER_SIZE) { - result.push_back(n); - n /= 2; - } - - std::sort(result.begin(), result.end()); - - return result; -} - -unsigned int LinuxAudioDriver::outputDeviceSampleRate() const -{ - return s_format.sampleRate; -} - -bool LinuxAudioDriver::setOutputDeviceSampleRate(unsigned int sampleRate) -{ - if (s_format.sampleRate == sampleRate) { - return true; - } - - bool reopen = isOpened(); - close(); - s_format.sampleRate = sampleRate; - - bool ok = true; - if (reopen) { - ok = open(s_format, &s_format); - } - - if (ok) { - m_sampleRateChanged.notify(); - } - - return ok; -} - -async::Notification LinuxAudioDriver::outputDeviceSampleRateChanged() const -{ - return m_sampleRateChanged; -} - -std::vector LinuxAudioDriver::availableOutputDeviceSampleRates() const -{ - // ALSA API is not of any help to get sample rates supported by the driver. - // (snd_pcm_hw_params_get_rate_[min|max] will return 1 to 384000 Hz) - // So just returning a sensible hard-coded list. - return { - 44100, - 48000, - 88200, - 96000, - }; -} - -void LinuxAudioDriver::resume() -{ -} - -void LinuxAudioDriver::suspend() -{ -} diff --git a/src/framework/audio/internal/platform/osx/osxaudiodriver.h b/src/framework/audio/internal/platform/osx/osxaudiodriver.h index 11298dfd25652..d5d9b0e16ad9b 100644 --- a/src/framework/audio/internal/platform/osx/osxaudiodriver.h +++ b/src/framework/audio/internal/platform/osx/osxaudiodriver.h @@ -66,6 +66,12 @@ class OSXAudioDriver : public IAudioDriver bool setOutputDeviceBufferSize(unsigned int bufferSize) override; async::Notification outputDeviceBufferSizeChanged() const override; + int audioDelayCompensate() const override; + void setAudioDelayCompensate(const int frames) override; + bool isPlaying() const override; + void remotePlayOrStop(bool) const override; + void remoteSeek(msecs_t) const override; + std::vector availableOutputDeviceBufferSizes() const override; unsigned int outputDeviceSampleRate() const override; @@ -86,7 +92,7 @@ class OSXAudioDriver : public IAudioDriver struct Data; - std::shared_ptr m_data = nullptr; + std::shared_ptr m_data; std::map m_outputDevices = {}, m_inputDevices = {}; mutable std::mutex m_devicesMutex; async::Notification m_outputDeviceChanged; diff --git a/src/framework/audio/internal/platform/osx/osxaudiodriver.mm b/src/framework/audio/internal/platform/osx/osxaudiodriver.mm index 25ebc51bb75ce..a11423c5304b1 100644 --- a/src/framework/audio/internal/platform/osx/osxaudiodriver.mm +++ b/src/framework/audio/internal/platform/osx/osxaudiodriver.mm @@ -42,7 +42,6 @@ }; OSXAudioDriver::OSXAudioDriver() - : m_data(nullptr) { m_data = std::make_shared(); m_data->audioQueue = nullptr; @@ -413,6 +412,28 @@ }; } +int OSXAudioDriver::audioDelayCompensate() const +{ + return 0; +} + +void OSXAudioDriver::setAudioDelayCompensate(const int frames) +{ +} + +bool OSXAudioDriver::isPlaying() const +{ + return false; +} + +void OSXAudioDriver::remotePlayOrStop([[maybe_unused]] bool ps) const +{ +} + +void OSXAudioDriver::remoteSeek([[maybe_unused]] msecs_t millis) const +{ +} + bool OSXAudioDriver::audioQueueSetDeviceName(const AudioDeviceID& deviceId) { if (deviceId.empty() || deviceId == DEFAULT_DEVICE_ID) { diff --git a/src/framework/audio/internal/platform/win/wasapiaudiodriver.cpp b/src/framework/audio/internal/platform/win/wasapiaudiodriver.cpp index c5b8f5f5da9ac..56fbb9e6726b5 100644 --- a/src/framework/audio/internal/platform/win/wasapiaudiodriver.cpp +++ b/src/framework/audio/internal/platform/win/wasapiaudiodriver.cpp @@ -338,6 +338,28 @@ void WasapiAudioDriver::reopen() open(m_activeSpec, &m_activeSpec); } +int WasapiAudioDriver::audioDelayCompensate(void) const +{ + return 0; +} + +void WasapiAudioDriver::setAudioDelayCompensate(const int frames) +{ +} + +bool WasapiAudioDriver::isPlaying() const +{ + return false; +} + +void WasapiAudioDriver::remotePlayOrStop(bool ps) const +{ +} + +void WasapiAudioDriver::remoteSeek(msecs_t millis) const +{ +} + AudioDeviceID WasapiAudioDriver::defaultDeviceId() const { using namespace winrt::Windows::Media::Devices; diff --git a/src/framework/audio/internal/platform/win/wasapiaudiodriver.h b/src/framework/audio/internal/platform/win/wasapiaudiodriver.h index 8388d734edc12..d0d2d7e142c2a 100644 --- a/src/framework/audio/internal/platform/win/wasapiaudiodriver.h +++ b/src/framework/audio/internal/platform/win/wasapiaudiodriver.h @@ -62,8 +62,15 @@ class WasapiAudioDriver : public IAudioDriver, public async::Asyncable unsigned int outputDeviceSampleRate() const override; bool setOutputDeviceSampleRate(unsigned int sampleRate) override; async::Notification outputDeviceSampleRateChanged() const override; + std::vector availableOutputDeviceSampleRates() const override; + int audioDelayCompensate(void) const override; + void setAudioDelayCompensate(const int frames) override; + bool isPlaying() const override; + void remotePlayOrStop(bool) const override; + void remoteSeek(msecs_t) const override; + void resume() override; void suspend() override; @@ -82,6 +89,7 @@ class WasapiAudioDriver : public IAudioDriver, public async::Asyncable std::unique_ptr m_devicesListener; async::Notification m_outputDeviceChanged; + async::Notification m_sampleRateChanged; async::Notification m_availableOutputDevicesChanged; async::Notification m_outputDeviceBufferSizeChanged; async::Notification m_outputDeviceSampleRateChanged; diff --git a/src/framework/audio/internal/worker/player.cpp b/src/framework/audio/internal/worker/player.cpp index ccd6842fe17d0..941b02d03c007 100644 --- a/src/framework/audio/internal/worker/player.cpp +++ b/src/framework/audio/internal/worker/player.cpp @@ -215,7 +215,7 @@ async::Channel Player::playbackPositionChanged() const PlaybackStatus Player::playbackStatus() const { - ONLY_AUDIO_MAIN_THREAD; + //ONLY_AUDIO_MAIN_THREAD; return m_playbackStatus; } diff --git a/src/framework/cmake/MuseDeclareOptions.cmake b/src/framework/cmake/MuseDeclareOptions.cmake index 5c3b4a2221822..296132d9a50ce 100644 --- a/src/framework/cmake/MuseDeclareOptions.cmake +++ b/src/framework/cmake/MuseDeclareOptions.cmake @@ -11,7 +11,7 @@ option(MUSE_MODULE_ACCESSIBILITY_TRACE "Enable accessibility logging" OFF) declare_muse_module_opt(ACTIONS ON) declare_muse_module_opt(AUDIO ON) -option(MUSE_MODULE_AUDIO_JACK "Enable jack support" OFF) +option(MUSE_MODULE_AUDIO_JACK "Enable jack support" ON) option(MUSE_MODULE_AUDIO_EXPORT "Enable audio export" ON) declare_muse_module_opt(AUDIOPLUGINS ON) diff --git a/src/framework/midi/CMakeLists.txt b/src/framework/midi/CMakeLists.txt index af737341c397d..cdc9041ddb815 100644 --- a/src/framework/midi/CMakeLists.txt +++ b/src/framework/midi/CMakeLists.txt @@ -22,6 +22,7 @@ declare_module(muse_midi) set(MODULE_ALIAS muse::midi) include(GetPlatformInfo) + if (OS_IS_WIN) set(DRIVER_SRC ${CMAKE_CURRENT_LIST_DIR}/internal/platform/win/winmidioutport.cpp @@ -31,17 +32,25 @@ if (OS_IS_WIN) ) elseif(OS_IS_LIN) set(DRIVER_SRC - ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/alsamidioutport.cpp - ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/alsamidioutport.h - ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/alsamidiinport.cpp - ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/alsamidiinport.h + ${CMAKE_CURRENT_LIST_DIR}/internal/platform/alsa/alsamidioutport.cpp + ${CMAKE_CURRENT_LIST_DIR}/internal/platform/alsa/alsamidioutport.h + ${CMAKE_CURRENT_LIST_DIR}/internal/platform/alsa/alsamidiinport.cpp + ${CMAKE_CURRENT_LIST_DIR}/internal/platform/alsa/alsamidiinport.h + ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/linuxmidioutport.cpp + ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/linuxmidioutport.h + ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/linuxmidiinport.cpp + ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/linuxmidiinport.h ) elseif(OS_IS_FBSD) set(DRIVER_SRC - ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/alsamidioutport.cpp - ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/alsamidioutport.h - ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/alsamidiinport.cpp - ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/alsamidiinport.h + ${CMAKE_CURRENT_LIST_DIR}/internal/platform/alsa/alsamidioutport.cpp + ${CMAKE_CURRENT_LIST_DIR}/internal/platform/alsa/alsamidioutport.h + ${CMAKE_CURRENT_LIST_DIR}/internal/platform/alsa/alsamidiinport.cpp + ${CMAKE_CURRENT_LIST_DIR}/internal/platform/alsa/alsamidiinport.h + ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/linuxmidioutport.cpp + ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/linuxmidioutport.h + ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/linuxmidiinport.cpp + ${CMAKE_CURRENT_LIST_DIR}/internal/platform/lin/linuxmidiinport.h ) elseif(OS_IS_MAC) set(DRIVER_SRC @@ -52,7 +61,6 @@ elseif(OS_IS_MAC) ) endif() - set(MODULE_SRC ${DRIVER_SRC} ${CMAKE_CURRENT_LIST_DIR}/midimodule.cpp @@ -82,6 +90,7 @@ if (OS_IS_MAC) elseif (OS_IS_WIN) set(MODULE_LINK ${MODULE_LINK} winmm) elseif (OS_IS_LIN) + # FIX: why does jack sources compile without a find_library here? find_package(ALSA REQUIRED) set(MODULE_INCLUDE_PRIVATE ${MODULE_INCLUDE_PRIVATE} ${ALSA_INCLUDE_DIRS} ) set(MODULE_LINK ${MODULE_LINK} ${ALSA_LIBRARIES} pthread ) diff --git a/src/framework/midi/internal/platform/lin/alsamidiinport.cpp b/src/framework/midi/internal/platform/alsa/alsamidiinport.cpp similarity index 98% rename from src/framework/midi/internal/platform/lin/alsamidiinport.cpp rename to src/framework/midi/internal/platform/alsa/alsamidiinport.cpp index 1e9fcc72a7a1b..cda5737c6bcdc 100644 --- a/src/framework/midi/internal/platform/lin/alsamidiinport.cpp +++ b/src/framework/midi/internal/platform/alsa/alsamidiinport.cpp @@ -71,7 +71,7 @@ void AlsaMidiInPort::deinit() } } -MidiDeviceList AlsaMidiInPort::availableDevices() const +std::vector AlsaMidiInPort::availableDevices() const { std::lock_guard lock(m_devicesMutex); @@ -80,7 +80,7 @@ MidiDeviceList AlsaMidiInPort::availableDevices() const const unsigned int type_hw = SND_SEQ_PORT_TYPE_PORT | SND_SEQ_PORT_TYPE_HARDWARE; const unsigned int type_sw = SND_SEQ_PORT_TYPE_PORT | SND_SEQ_PORT_TYPE_SOFTWARE; - MidiDeviceList ret; + std::vector ret; ret.push_back({ NONE_DEVICE_ID, muse::trc("midi", "No device") }); diff --git a/src/framework/midi/internal/platform/lin/alsamidiinport.h b/src/framework/midi/internal/platform/alsa/alsamidiinport.h similarity index 97% rename from src/framework/midi/internal/platform/lin/alsamidiinport.h rename to src/framework/midi/internal/platform/alsa/alsamidiinport.h index bea92176ef0bc..e83a4b1f77e77 100644 --- a/src/framework/midi/internal/platform/lin/alsamidiinport.h +++ b/src/framework/midi/internal/platform/alsa/alsamidiinport.h @@ -40,7 +40,7 @@ class AlsaMidiInPort : public IMidiInPort, public async::Asyncable void init(); void deinit(); - MidiDeviceList availableDevices() const override; + std::vector availableDevices() const override; async::Notification availableDevicesChanged() const override; Ret connect(const MidiDeviceID& deviceID) override; diff --git a/src/framework/midi/internal/platform/lin/alsamidioutport.cpp b/src/framework/midi/internal/platform/alsa/alsamidioutport.cpp similarity index 74% rename from src/framework/midi/internal/platform/lin/alsamidioutport.cpp rename to src/framework/midi/internal/platform/alsa/alsamidioutport.cpp index 90e2496a719bb..44fe54a6546eb 100644 --- a/src/framework/midi/internal/platform/lin/alsamidioutport.cpp +++ b/src/framework/midi/internal/platform/alsa/alsamidioutport.cpp @@ -5,7 +5,7 @@ * MuseScore * Music Composition & Notation * - * Copyright (C) 2021 MuseScore BVBA and others + * Copyright (C) 2023 MuseScore BVBA 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 @@ -41,8 +41,10 @@ using namespace muse::midi; void AlsaMidiOutPort::init() { + LOGI("---- linux ALSA init ----"); m_alsa = std::make_shared(); + /* m_devicesListener.startWithCallback([this]() { return availableDevices(); }); @@ -61,10 +63,12 @@ void AlsaMidiOutPort::init() m_availableDevicesChanged.notify(); }); + */ } void AlsaMidiOutPort::deinit() { + LOGI("---- linux ALSA deinit ----"); if (isConnected()) { disconnect(); } @@ -72,8 +76,6 @@ void AlsaMidiOutPort::deinit() std::vector AlsaMidiOutPort::availableDevices() const { - std::lock_guard lock(m_devicesMutex); - int streams = SND_SEQ_OPEN_OUTPUT; const unsigned int cap = SND_SEQ_PORT_CAP_SUBS_WRITE | SND_SEQ_PORT_CAP_WRITE; const unsigned int type_hw = SND_SEQ_PORT_TYPE_PORT | SND_SEQ_PORT_TYPE_HARDWARE; @@ -81,8 +83,6 @@ std::vector AlsaMidiOutPort::availableDevices() const std::vector ret; - ret.push_back({ NONE_DEVICE_ID, muse::trc("midi", "No device") }); - snd_seq_client_info_t* cinfo; snd_seq_port_info_t* pinfo; int client; @@ -91,7 +91,7 @@ std::vector AlsaMidiOutPort::availableDevices() const err = snd_seq_open(&handle, "hw", streams, 0); if (err < 0) { - /* Use snd_strerror(errno) to get the error here. */ + LOGE("snd_seq_open failed: err: %s", snd_strerror(err)); return ret; } @@ -117,8 +117,7 @@ std::vector AlsaMidiOutPort::availableDevices() const if (canConnect) { MidiDevice dev; - dev.name = snd_seq_client_info_get_name(cinfo); - + dev.name = "ALSA/" + std::string(snd_seq_client_info_get_name(cinfo)); int client = snd_seq_port_info_get_client(pinfo); int port = snd_seq_port_info_get_port(pinfo); dev.id = makeUniqueDeviceId(index++, client, port); @@ -133,58 +132,36 @@ std::vector AlsaMidiOutPort::availableDevices() const return ret; } -async::Notification AlsaMidiOutPort::availableDevicesChanged() const -{ - return m_availableDevicesChanged; -} - -Ret AlsaMidiOutPort::connect(const MidiDeviceID& deviceID) +muse::Ret AlsaMidiOutPort::connect(const MidiDeviceID& deviceID) { - if (!deviceExists(deviceID)) { - return make_ret(Err::MidiFailedConnect, "not found device, id: " + deviceID); - } - - DEFER { - m_deviceChanged.notify(); - }; - Ret ret = muse::make_ok(); - if (!deviceID.empty() && deviceID != NONE_DEVICE_ID) { - std::vector deviceParams = splitDeviceId(deviceID); - IF_ASSERT_FAILED(deviceParams.size() == 3) { - return make_ret(Err::MidiInvalidDeviceID, "invalid device id: " + deviceID); - } - - if (isConnected()) { - disconnect(); - } - - int err = snd_seq_open(&m_alsa->midiOut, "default", SND_SEQ_OPEN_OUTPUT, 0); - if (err < 0) { - return make_ret(Err::MidiFailedConnect, "failed open seq, err: " + std::string(snd_strerror(err))); - } - snd_seq_set_client_name(m_alsa->midiOut, "MuseScore"); - - int port = snd_seq_create_simple_port(m_alsa->midiOut, "MuseScore Port-0", SND_SEQ_PORT_CAP_READ, SND_SEQ_PORT_TYPE_MIDI_GENERIC); - if (port < 0) { - return make_ret(Err::MidiFailedConnect, "failed create port"); - } + std::vector deviceParams = splitDeviceId(deviceID); + IF_ASSERT_FAILED(deviceParams.size() == 3) { + return make_ret(Err::MidiInvalidDeviceID, "invalid device id: " + deviceID); + } - m_alsa->client = deviceParams.at(1); - m_alsa->port = deviceParams.at(2); - err = snd_seq_connect_to(m_alsa->midiOut, port, m_alsa->client, m_alsa->port); - if (err < 0) { - return make_ret(Err::MidiFailedConnect, "failed connect, err: " + std::string(snd_strerror(err))); - } + if (isConnected()) { + disconnect(); } - m_deviceID = deviceID; + int err = snd_seq_open(&m_alsa->midiOut, "default", SND_SEQ_OPEN_OUTPUT, 0); + if (err < 0) { + return make_ret(Err::MidiFailedConnect, "failed open seq, err: " + std::string(snd_strerror(err))); + } + snd_seq_set_client_name(m_alsa->midiOut, "MuseScore"); - if (ret) { - LOGD() << "Connected to " << m_deviceID; + int port = snd_seq_create_simple_port(m_alsa->midiOut, "MuseScore Port-0", SND_SEQ_PORT_CAP_READ, SND_SEQ_PORT_TYPE_MIDI_GENERIC); + if (port < 0) { + return make_ret(Err::MidiFailedConnect, "failed create port"); } + m_alsa->client = deviceParams.at(1); + m_alsa->port = deviceParams.at(2); + err = snd_seq_connect_to(m_alsa->midiOut, port, m_alsa->client, m_alsa->port); + if (err < 0) { + return make_ret(Err::MidiFailedConnect, "failed connect, err: " + std::string(snd_strerror(err))); + } return Ret(true); } @@ -215,11 +192,6 @@ MidiDeviceID AlsaMidiOutPort::deviceID() const return m_deviceID; } -async::Notification AlsaMidiOutPort::deviceChanged() const -{ - return m_deviceChanged; -} - bool AlsaMidiOutPort::supportsMIDI20Output() const { return false; @@ -227,8 +199,6 @@ bool AlsaMidiOutPort::supportsMIDI20Output() const Ret AlsaMidiOutPort::sendEvent(const Event& e) { - // LOGI() << e.to_string(); - if (!isConnected()) { return make_ret(Err::MidiNotConnected); } diff --git a/src/framework/midi/internal/platform/lin/alsamidioutport.h b/src/framework/midi/internal/platform/alsa/alsamidioutport.h similarity index 73% rename from src/framework/midi/internal/platform/lin/alsamidioutport.h rename to src/framework/midi/internal/platform/alsa/alsamidioutport.h index 0396b405abb7f..e5dfda43978b5 100644 --- a/src/framework/midi/internal/platform/lin/alsamidioutport.h +++ b/src/framework/midi/internal/platform/alsa/alsamidioutport.h @@ -24,31 +24,24 @@ #include -#include "async/asyncable.h" -#include "midi/imidioutport.h" -#include "internal/midideviceslistener.h" +#include "midi/midiportstate.h" namespace muse::midi { -class AlsaMidiOutPort : public IMidiOutPort, public async::Asyncable +class AlsaMidiOutPort : public MidiPortState { public: AlsaMidiOutPort() = default; ~AlsaMidiOutPort() = default; - void init(); void deinit(); - MidiDeviceList availableDevices() const override; - async::Notification availableDevicesChanged() const override; + std::vector availableDevices() const override; Ret connect(const MidiDeviceID& deviceID) override; void disconnect() override; bool isConnected() const override; MidiDeviceID deviceID() const override; - async::Notification deviceChanged() const override; - bool supportsMIDI20Output() const override; - Ret sendEvent(const Event& e) override; private: @@ -57,12 +50,6 @@ class AlsaMidiOutPort : public IMidiOutPort, public async::Asyncable struct Alsa; std::shared_ptr m_alsa; MidiDeviceID m_deviceID; - async::Notification m_deviceChanged; - - async::Notification m_availableDevicesChanged; - MidiDevicesListener m_devicesListener; - - mutable std::mutex m_devicesMutex; }; } diff --git a/src/framework/midi/internal/platform/lin/linuxmidiinport.cpp b/src/framework/midi/internal/platform/lin/linuxmidiinport.cpp new file mode 100644 index 0000000000000..377d3a3933750 --- /dev/null +++ b/src/framework/midi/internal/platform/lin/linuxmidiinport.cpp @@ -0,0 +1,189 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) MuseScore BVBA 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 "linuxmidiinport.h" +#include "framework/audio/audiomodule.h" + +#include "midierrors.h" +#include "stringutils.h" +#include "translation.h" +#include "defer.h" +#include "log.h" + +using namespace muse::midi; + +void LinuxMidiInPort::init() +{ +#if defined(JACK_AUDIO) +#else + m_midiInPortAlsa = std::make_unique(); +#endif +} + +void LinuxMidiInPort::deinit() +{ + if (isConnected()) { + disconnect(); + } +} + +std::vector LinuxMidiInPort::availableDevices() const +{ +// FIX: this is compile-time, change so that we call availableMidiDevices if jack is selected +#if defined(JACK_AUDIO) + std::vector ret; + return ret; +#else + std::lock_guard lock(m_devicesMutex); + std::vector ret; + ret.push_back({ NONE_DEVICE_ID, muse::trc("midi", "No device") }); + // return concatenation of alsa + jack devices + std::shared_ptr m_midiInPortJack; + return ret; +#endif +} + +muse::async::Notification LinuxMidiInPort::availableDevicesChanged() const +{ + return m_availableDevicesChanged; +} + +muse::Ret LinuxMidiInPort::connect(const MidiDeviceID& deviceID) +{ + if (!deviceExists(deviceID)) { + return make_ret(Err::MidiFailedConnect, "not found device, id: " + deviceID); + } + + if (isConnected()) { + disconnect(); + } + + DEFER { + m_deviceChanged.notify(); + }; + + muse::Ret ret = muse::make_ok(); + + if (!deviceID.empty() && deviceID != NONE_DEVICE_ID) { + std::vector deviceParams = splitDeviceId(deviceID); + IF_ASSERT_FAILED(deviceParams.size() == 3) { + return make_ret(Err::MidiInvalidDeviceID, "invalid device id: " + deviceID); + } + + //m_alsa->client = deviceParams.at(1); + //m_alsa->port = deviceParams.at(2); + + m_deviceID = deviceID; + ret = run(); + } else { + m_deviceID = deviceID; + } + + if (ret) { + LOGD() << "Connected to " << m_deviceID; + } + + return ret; +} + +void LinuxMidiInPort::disconnect() +{ + if (!isConnected()) { + return; + } + + stop(); + + LOGD() << "Disconnected from " << m_deviceID; + + //m_alsa->client = -1; + //m_alsa->port = -1; + //m_alsa->midiIn = nullptr; + m_deviceID.clear(); +} + +bool LinuxMidiInPort::isConnected() const +{ + return /* m_alsa && m_alsa->midiIn && */ !m_deviceID.empty(); +} + +MidiDeviceID LinuxMidiInPort::deviceID() const +{ + return m_deviceID; +} + +muse::async::Notification LinuxMidiInPort::deviceChanged() const +{ + return m_deviceChanged; +} + +muse::async::Channel LinuxMidiInPort::eventReceived() const +{ + return m_eventReceived; +} + +muse::Ret LinuxMidiInPort::run() +{ + if (!isConnected()) { + return make_ret(Err::MidiNotConnected); + } + + if (m_thread) { + LOGW() << "already started"; + return muse::Ret(true); + } + + m_running.store(true); + m_thread = std::make_shared(process, this); + return muse::Ret(true); +} + +void LinuxMidiInPort::stop() +{ + if (!m_thread) { + LOGW() << "already stopped"; + return; + } + + m_running.store(false); + m_thread->join(); + m_thread = nullptr; +} + +void LinuxMidiInPort::process(LinuxMidiInPort* self) +{ + self->doProcess(); +} + +void LinuxMidiInPort::doProcess() +{ +} + +bool LinuxMidiInPort::deviceExists(const MidiDeviceID& deviceId) const +{ + for (const MidiDevice& device : availableDevices()) { + if (device.id == deviceId) { + return true; + } + } + + return false; +} diff --git a/src/framework/midi/internal/platform/lin/linuxmidiinport.h b/src/framework/midi/internal/platform/lin/linuxmidiinport.h new file mode 100644 index 0000000000000..5cbea936e8ce1 --- /dev/null +++ b/src/framework/midi/internal/platform/lin/linuxmidiinport.h @@ -0,0 +1,85 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) MuseScore BVBA 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 MU_MIDI_LINUXMIDIINPORT_H +#define MU_MIDI_LINUXMIDIINPORT_H + +#include +#include + +#include "modularity/ioc.h" +#include "async/asyncable.h" + +#include "framework/audio/audiomodule.h" +#include "framework/audio/iaudiodriver.h" +#include "imidiinport.h" +#include "internal/midideviceslistener.h" + +#include "internal/platform/alsa/alsamidiinport.h" + +namespace muse::midi { +class LinuxMidiInPort : public IMidiInPort, public async::Asyncable +{ + Inject audioDriver; +public: + LinuxMidiInPort() = default; + ~LinuxMidiInPort() = default; + + void init(); + void deinit(); + + std::vector availableDevices() const override; + async::Notification availableDevicesChanged() const override; + + Ret connect(const MidiDeviceID& deviceID) override; + void disconnect() override; + bool isConnected() const override; + MidiDeviceID deviceID() const override; + async::Notification deviceChanged() const override; + + async::Channel eventReceived() const override; + +private: + Ret run(); + void stop(); + + static void process(LinuxMidiInPort* self); + void doProcess(); + + bool deviceExists(const MidiDeviceID& deviceId) const; + + MidiDeviceID m_deviceID; + std::shared_ptr m_thread; + std::atomic m_running{ false }; + async::Notification m_deviceChanged; + + async::Notification m_availableDevicesChanged; + MidiDevicesListener m_devicesListener; + + mutable std::mutex m_devicesMutex; + + async::Channel m_eventReceived; + + std::unique_ptr m_midiInPortAlsa; +}; +} + +#endif // MU_MIDI_LINUXMIDIINPORT_H diff --git a/src/framework/midi/internal/platform/lin/linuxmidioutport.cpp b/src/framework/midi/internal/platform/lin/linuxmidioutport.cpp new file mode 100644 index 0000000000000..99fad06ac6198 --- /dev/null +++ b/src/framework/midi/internal/platform/lin/linuxmidioutport.cpp @@ -0,0 +1,181 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) MuseScore BVBA 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 +//#include +#include "linuxmidioutport.h" +#include "framework/audio/audiomodule.h" + +#include "midierrors.h" +#include "translation.h" +#include "defer.h" +#include "log.h" + +using namespace muse::midi; + +void LinuxMidiOutPort::init() +{ + m_midiOutPortAlsa = std::make_unique(); + m_midiOutPortAlsa->init(); + + m_devicesListener.startWithCallback([this]() { + return availableDevices(); + }); + + m_devicesListener.devicesChanged().onNotify(this, [this]() { + bool connectedDeviceRemoved = true; + for (const MidiDevice& device: availableDevices()) { + if (m_deviceID == device.id) { + connectedDeviceRemoved = false; + } + } + + if (connectedDeviceRemoved) { + disconnect(); + } + + m_availableDevicesChanged.notify(); + }); +} + +void LinuxMidiOutPort::deinit() +{ + if (isConnected()) { + disconnect(); + } +} + +std::vector LinuxMidiOutPort::availableDevices() const +{ + std::lock_guard lock(m_devicesMutex); + + std::vector ret; + auto va = m_midiOutPortAlsa->availableDevices(); + ret.insert(ret.end(), va.begin(), va.end()); + + // FIX: this should be done by gui/caller + ret.push_back({ NONE_DEVICE_ID, muse::trc("midi", "No device") }); + return ret; +} + +muse::async::Notification LinuxMidiOutPort::availableDevicesChanged() const +{ + return m_availableDevicesChanged; +} + +muse::Ret LinuxMidiOutPort::connect(const MidiDeviceID& deviceID) +{ + if (!deviceExists(deviceID)) { + return make_ret(Err::MidiFailedConnect, "not found device, id: " + deviceID); + } + + DEFER { + m_deviceChanged.notify(); + }; + + if (!deviceID.empty() && deviceID != NONE_DEVICE_ID) { + std::vector deviceParams = splitDeviceId(deviceID); + IF_ASSERT_FAILED(deviceParams.size() == 3) { + return make_ret(Err::MidiInvalidDeviceID, "invalid device id: " + deviceID); + } + + if (deviceParams.at(1) == 9999) { // This is an jack device + } else { + m_midiOutPortCurrent = /* AlsaMidiOutPort */ m_midiOutPortAlsa.get(); + } + + if (m_midiOutPortCurrent->isConnected()) { + m_midiOutPortCurrent->disconnect(); + } + LOGD() << "Connected to " << deviceID; // FIX: let caller log instead (has return state of connect) + m_deviceID = deviceID; + // continue the connect in the driver + return m_midiOutPortCurrent->connect(deviceID); + } + + return Ret(true); +} + +void LinuxMidiOutPort::disconnect() +{ + if (!isConnected()) { + return; + } + + LOGD() << "Disconnected from " << m_deviceID; + + m_deviceID.clear(); +} + +bool LinuxMidiOutPort::isConnected() const +{ + return m_midiOutPortCurrent && !m_deviceID.empty(); +} + +MidiDeviceID LinuxMidiOutPort::deviceID() const +{ + return m_deviceID; +} + +muse::async::Notification LinuxMidiOutPort::deviceChanged() const +{ + return m_deviceChanged; +} + +bool LinuxMidiOutPort::supportsMIDI20Output() const +{ + return false; +} + +muse::Ret LinuxMidiOutPort::sendEvent(const Event& e) +{ + if (!isConnected()) { + return make_ret(Err::MidiNotConnected); + } + + if (e.isChannelVoice20()) { + auto events = e.toMIDI10(); + for (auto& event : events) { + muse::Ret ret = sendEvent(event); + if (!ret) { + return ret; + } + } + return muse::Ret(true); + } + +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) + return muse::Ret(true); +#else // alsa + return m_midiOutPortCurrent->sendEvent(e); +#endif +} + +bool LinuxMidiOutPort::deviceExists(const MidiDeviceID& deviceId) const +{ + for (const MidiDevice& device : availableDevices()) { + if (device.id == deviceId) { + return true; + } + } + return false; +} diff --git a/src/framework/midi/internal/platform/lin/linuxmidioutport.h b/src/framework/midi/internal/platform/lin/linuxmidioutport.h new file mode 100644 index 0000000000000..fbc2bc90dddbd --- /dev/null +++ b/src/framework/midi/internal/platform/lin/linuxmidioutport.h @@ -0,0 +1,75 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) MuseScore BVBA 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 MU_MIDI_LINUXMIDIOUTPORT_H +#define MU_MIDI_LINUXMIDIOUTPORT_H + +#include + +#include "modularity/ioc.h" +#include "async/asyncable.h" +#include "framework/audio/audiomodule.h" +#include "midi/imidioutport.h" +#include "internal/midideviceslistener.h" + +#include "internal/platform/alsa/alsamidioutport.h" + +namespace muse::midi { +class LinuxMidiOutPort : public IMidiOutPort, public async::Asyncable +{ + Inject audioDriver; +public: + LinuxMidiOutPort() = default; + ~LinuxMidiOutPort() = default; + + void init(); + void deinit(); + + std::vector availableDevices() const override; + async::Notification availableDevicesChanged() const override; + + Ret connect(const MidiDeviceID& deviceID) override; + void disconnect() override; + bool isConnected() const override; + MidiDeviceID deviceID() const override; + async::Notification deviceChanged() const override; + + bool supportsMIDI20Output() const override; + + Ret sendEvent(const Event& e) override; + +private: + bool deviceExists(const MidiDeviceID& deviceId) const; + + MidiDeviceID m_deviceID; + async::Notification m_deviceChanged; + + async::Notification m_availableDevicesChanged; + MidiDevicesListener m_devicesListener; + + mutable std::mutex m_devicesMutex; + + MidiPortState* m_midiOutPortCurrent; + std::unique_ptr m_midiOutPortAlsa; +}; +} + +#endif // MU_MIDI_ALSAMIDIOUTPORT_H diff --git a/src/framework/midi/midimodule.cpp b/src/framework/midi/midimodule.cpp index 9bbd1bc698024..41119b67c6dd5 100644 --- a/src/framework/midi/midimodule.cpp +++ b/src/framework/midi/midimodule.cpp @@ -35,8 +35,8 @@ using namespace muse::midi; #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) -#include "internal/platform/lin/alsamidioutport.h" -#include "internal/platform/lin/alsamidiinport.h" +#include "internal/platform/lin/linuxmidioutport.h" +#include "internal/platform/lin/linuxmidiinport.h" #elif defined(Q_OS_WIN) #include "internal/platform/win/winmidioutport.h" #include "internal/platform/win/winmidiinport.h" @@ -57,19 +57,19 @@ void MidiModule::registerExports() { m_configuration = std::make_shared(); - #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) - m_midiOutPort = std::make_shared(); - m_midiInPort = std::make_shared(); - #elif defined(Q_OS_WIN) +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) + m_midiOutPort = std::make_shared(); + m_midiInPort = std::make_shared(); +#elif defined(Q_OS_WIN) m_midiOutPort = std::make_shared(); m_midiInPort = std::make_shared(); - #elif defined(Q_OS_MACOS) +#elif defined(Q_OS_MACOS) m_midiOutPort = std::make_shared(); m_midiInPort = std::make_shared(); - #else +#else m_midiOutPort = std::make_shared(); m_midiInPort = std::make_shared(); - #endif +#endif ioc()->registerExport(moduleName(), m_configuration); ioc()->registerExport(moduleName(), m_midiOutPort); @@ -86,8 +86,8 @@ void MidiModule::onInit(const IApplication::RunMode& mode) m_configuration->init(); if (mode == IApplication::RunMode::GuiApp) { - m_midiOutPort->init(); m_midiInPort->init(); + m_midiOutPort->init(); } } diff --git a/src/framework/midi/midimodule.h b/src/framework/midi/midimodule.h index c2d7dfafb6d08..b0121d3fad896 100644 --- a/src/framework/midi/midimodule.h +++ b/src/framework/midi/midimodule.h @@ -29,8 +29,8 @@ namespace muse::midi { class MidiConfiguration; #if defined(Q_OS_LINUX) -class AlsaMidiOutPort; -class AlsaMidiInPort; +class LinuxMidiOutPort; +class LinuxMidiInPort; #elif defined(Q_OS_FREEBSD) class AlsaMidiOutPort; class AlsaMidiInPort; @@ -58,8 +58,8 @@ class MidiModule : public modularity::IModuleSetup std::shared_ptr m_configuration; #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) - std::shared_ptr m_midiOutPort; - std::shared_ptr m_midiInPort; + std::shared_ptr m_midiOutPort; + std::shared_ptr m_midiInPort; #elif defined(Q_OS_WIN) std::shared_ptr m_midiOutPort; diff --git a/src/framework/midi/midiportstate.h b/src/framework/midi/midiportstate.h new file mode 100644 index 0000000000000..95df6579cb1e0 --- /dev/null +++ b/src/framework/midi/midiportstate.h @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) MuseScore BVBA 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 MU_MIDI_MIDIPORTSTATE_H +#define MU_MIDI_MIDIPORTSTATE_H + +#include "types/ret.h" +#include "miditypes.h" + +namespace muse::midi { +class MidiPortState +{ +public: + virtual ~MidiPortState() = default; + + virtual void init() = 0; + virtual void deinit() = 0; + + virtual std::vector availableDevices() const = 0; + + virtual Ret connect(const MidiDeviceID& deviceID) = 0; + virtual void disconnect() = 0; + virtual bool isConnected() const = 0; + virtual MidiDeviceID deviceID() const = 0; + + // + // Whether the output port supports it, rather than whether the receiver supports it + // (If the receiver does not support MIDI 2.0, then it's the output port's resposibility to convert to MIDI 1.0) + virtual bool supportsMIDI20Output() const = 0; + + virtual Ret sendEvent(const Event& e) = 0; +}; +} + +#endif // MU_MIDI_MIDIPORTSTATE_H diff --git a/src/framework/midi/miditypes.h b/src/framework/midi/miditypes.h index 9bff74e0da844..58532d1b2f37c 100644 --- a/src/framework/midi/miditypes.h +++ b/src/framework/midi/miditypes.h @@ -47,6 +47,15 @@ static constexpr int EXPRESSION_CONTROLLER = 11; static constexpr int SUSTAIN_PEDAL_CONTROLLER = 64; static constexpr int SOSTENUTO_PEDAL_CONTROLLER = 66; +// input/output relative to context +// ie, a MusescoreIngress port would be an external jack-midi output port +enum class MidiPortDirection +{ + Any, + Input, + Output +}; + struct Program { Program(bank_t b = 0, program_t p = 0) : bank(b), program(p) {} diff --git a/src/framework/stubs/audio/audioconfigurationstub.cpp b/src/framework/stubs/audio/audioconfigurationstub.cpp index 59480cbd0b5b0..828af251c2e49 100644 --- a/src/framework/stubs/audio/audioconfigurationstub.cpp +++ b/src/framework/stubs/audio/audioconfigurationstub.cpp @@ -52,6 +52,15 @@ async::Notification AudioConfigurationStub::audioOutputDeviceIdChanged() const return async::Notification(); } +int AudioConfigurationStub::audioDelayCompensate() const +{ + return 0; +} + +void AudioConfigurationStub::setAudioDelayCompensate(const int) +{ +} + audioch_t AudioConfigurationStub::audioChannelsCount() const { return 2; diff --git a/src/framework/stubs/audio/audioconfigurationstub.h b/src/framework/stubs/audio/audioconfigurationstub.h index 798778e6cfa5b..0246674fa5272 100644 --- a/src/framework/stubs/audio/audioconfigurationstub.h +++ b/src/framework/stubs/audio/audioconfigurationstub.h @@ -54,6 +54,9 @@ class AudioConfigurationStub : public IAudioConfiguration async::Notification sampleRateChanged() const override; size_t desiredAudioThreadNumber() const override; + int audioDelayCompensate() const override; + void setAudioDelayCompensate(const int frames) override; + size_t minTrackCountForMultithreading() const override; // synthesizers diff --git a/src/framework/stubs/audio/audiodriverstub.cpp b/src/framework/stubs/audio/audiodriverstub.cpp index 4dd4e97c5e3de..13f1ba44618c0 100644 --- a/src/framework/stubs/audio/audiodriverstub.cpp +++ b/src/framework/stubs/audio/audiodriverstub.cpp @@ -83,6 +83,28 @@ async::Notification AudioDriverStub::availableOutputDevicesChanged() const return async::Notification(); } +int AudioDriverStub::audioDelayCompensate() const +{ + return 0; +} + +void AudioDriverStub::setAudioDelayCompensate(const int) +{ +} + +bool AudioDriverStub::isPlaying() const +{ + return false; +} + +void AudioDriverStub::remotePlayOrStop([[maybe_unused]] bool ps) const +{ +} + +void AudioDriverStub::remoteSeek([[maybe_unused]] msecs_t millis) const +{ +} + unsigned int AudioDriverStub::outputDeviceBufferSize() const { return 0; diff --git a/src/framework/stubs/audio/audiodriverstub.h b/src/framework/stubs/audio/audiodriverstub.h index bfba3859020bd..c657e10b9300c 100644 --- a/src/framework/stubs/audio/audiodriverstub.h +++ b/src/framework/stubs/audio/audiodriverstub.h @@ -56,6 +56,12 @@ class AudioDriverStub : public IAudioDriver async::Notification outputDeviceSampleRateChanged() const override; std::vector availableOutputDeviceSampleRates() const override; + bool isPlaying() const override; + void remotePlayOrStop(bool) const override; + void remoteSeek(msecs_t) const override; + + int audioDelayCompensate() const override; + void setAudioDelayCompensate(const int frames) override; void resume() override; void suspend() override; diff --git a/src/importexport/audioexport/internal/audioexportconfiguration.cpp b/src/importexport/audioexport/internal/audioexportconfiguration.cpp index 0dd8d1502f590..79a467fc432be 100644 --- a/src/importexport/audioexport/internal/audioexportconfiguration.cpp +++ b/src/importexport/audioexport/internal/audioexportconfiguration.cpp @@ -34,7 +34,7 @@ static const Settings::Key EXPORT_MP3_BITRATE("iex_audioexport", "export/audio/m void AudioExportConfiguration::init() { - settings()->setDefaultValue(EXPORT_SAMPLE_RATE_KEY, Val(44100)); + settings()->setDefaultValue(EXPORT_SAMPLE_RATE_KEY, Val(48000)); settings()->setDefaultValue(EXPORT_MP3_BITRATE, Val(128)); } diff --git a/src/playback/internal/playbackconfiguration.cpp b/src/playback/internal/playbackconfiguration.cpp index 4932da6a6a87c..f0c7af3f0af69 100644 --- a/src/playback/internal/playbackconfiguration.cpp +++ b/src/playback/internal/playbackconfiguration.cpp @@ -58,6 +58,8 @@ static const Settings::Key MIXER_RESET_SOUND_FLAGS_WHEN_CHANGE_PLAYBACK_PROFILE_ static const Settings::Key MUTE_HIDDEN_INSTRUMENTS(moduleName, "playback/mixer/muteHiddenInstruments"); +static const Settings::Key JACK_TRANSPORT_ENABLE(moduleName, "playback/jack/transportEnable"); + static const Settings::Key DEFAULT_SOUND_PROFILE_FOR_NEW_PROJECTS(moduleName, "playback/profiles/defaultProfileName"); static const SoundProfileName BASIC_PROFILE_NAME(u"MuseScore Basic"); static const SoundProfileName MUSE_PROFILE_NAME(u"Muse Sounds"); @@ -121,6 +123,11 @@ void PlaybackConfiguration::init() m_muteHiddenInstrumentsChanged.send(mute.toBool()); }); + settings()->setDefaultValue(JACK_TRANSPORT_ENABLE, Val(true)); + settings()->valueChanged(JACK_TRANSPORT_ENABLE).onReceive(nullptr, [this](const Val& enable) { + m_jackTransportEnableChanged.send(enable.toBool()); + }); + settings()->setDefaultValue(DEFAULT_SOUND_PROFILE_FOR_NEW_PROJECTS, Val(fallbackSoundProfileStr().toStdString())); for (aux_channel_idx_t idx = 0; idx < AUX_CHANNEL_NUM; ++idx) { @@ -269,6 +276,21 @@ muse::async::Channel PlaybackConfiguration::muteHiddenInstrumentsChanged() return m_muteHiddenInstrumentsChanged; } +bool PlaybackConfiguration::jackTransportEnable() const +{ + return settings()->value(JACK_TRANSPORT_ENABLE).toBool(); +} + +void PlaybackConfiguration::setJackTransportEnable(bool enable) +{ + settings()->setSharedValue(JACK_TRANSPORT_ENABLE, Val(enable)); +} + +muse::async::Channel PlaybackConfiguration::jackTransportEnableChanged() const +{ + return m_jackTransportEnableChanged; +} + const SoundProfileName& PlaybackConfiguration::basicSoundProfileName() const { return BASIC_PROFILE_NAME; diff --git a/src/playback/internal/playbackconfiguration.h b/src/playback/internal/playbackconfiguration.h index 1c13bcc328afb..d96e9081f7609 100644 --- a/src/playback/internal/playbackconfiguration.h +++ b/src/playback/internal/playbackconfiguration.h @@ -71,6 +71,10 @@ class PlaybackConfiguration : public IPlaybackConfiguration, public muse::async: void setMuteHiddenInstruments(bool mute) override; muse::async::Channel muteHiddenInstrumentsChanged() const override; + bool jackTransportEnable() const override; + void setJackTransportEnable(bool mute) override; + muse::async::Channel jackTransportEnableChanged() const override; + const SoundProfileName& basicSoundProfileName() const override; const SoundProfileName& museSoundProfileName() const override; SoundProfileName defaultProfileForNewProjects() const override; @@ -98,6 +102,8 @@ class PlaybackConfiguration : public IPlaybackConfiguration, public muse::async: muse::async::Channel m_isMixerSectionVisibleChanged; muse::async::Channel m_muteHiddenInstrumentsChanged; + + muse::async::Channel m_jackTransportEnableChanged; }; } diff --git a/src/playback/internal/playbackcontroller.cpp b/src/playback/internal/playbackcontroller.cpp index 1eb8a8932c6c7..d6e61d3565743 100644 --- a/src/playback/internal/playbackcontroller.cpp +++ b/src/playback/internal/playbackcontroller.cpp @@ -139,6 +139,24 @@ void PlaybackController::init() }); m_measureInputLag = configuration()->shouldMeasureInputLag(); + + m_remoteSeek.onReceive(this, [this](const muse::audio::msecs_t msecs) { + seek(msecs / 1000L); //FIX: dont do scaling here + }); + + m_remotePlayOrStop.onReceive(this, [this](const bool playOrStop) { + if (playOrStop) { + if (isPlaying()) { + resume(); + } else { + play(); + } + } else { + if (isPlaying()) { + pause(); + } + } + }); } void PlaybackController::updateCurrentTempo() @@ -234,6 +252,25 @@ void PlaybackController::seek(const audio::secs_t secs) currentPlayer()->seek(secs); } +void PlaybackController::remoteSeek(const msecs_t msecs) +{ + if (!currentPlayer() || !playback()) { + return; + } + IF_ASSERT_FAILED(playback()) { + return; + } + m_remoteSeek.send(msecs); +} + +void PlaybackController::remotePlayOrStop(const bool playOrStop) +{ + if (!isPlayAllowed()) { + return; + } + m_remotePlayOrStop.send(playOrStop); +} + muse::async::Channel PlaybackController::currentPlaybackPositionChanged() const { return m_currentPlaybackPositionChanged; @@ -1102,7 +1139,7 @@ void PlaybackController::setTrackActivity(const engraving::InstrumentTrackId& in outParams.muted = !isActive; - audio::TrackId trackId = m_instrumentTrackIdMap[instrumentTrackId]; + TrackId trackId = m_instrumentTrackIdMap[instrumentTrackId]; playback()->audioOutput()->setOutputParams(m_currentSequenceId, trackId, std::move(outParams)); } @@ -1205,7 +1242,7 @@ void PlaybackController::setupNewCurrentSequence(const TrackSequenceId sequenceI return; } - const audio::AudioOutputParams& masterOutputParams = audioSettings()->masterAudioOutputParams(); + const AudioOutputParams& masterOutputParams = audioSettings()->masterAudioOutputParams(); playback()->audioOutput()->setMasterOutputParams(masterOutputParams); subscribeOnAudioParamsChanges(); @@ -1217,7 +1254,7 @@ void PlaybackController::setupNewCurrentSequence(const TrackSequenceId sequenceI void PlaybackController::subscribeOnAudioParamsChanges() { - playback()->audioOutput()->masterOutputParamsChanged().onReceive(this, [this](const audio::AudioOutputParams& params) { + playback()->audioOutput()->masterOutputParamsChanged().onReceive(this, [this](const AudioOutputParams& params) { audioSettings()->setMasterAudioOutputParams(params); }); @@ -1445,7 +1482,7 @@ void PlaybackController::updateSoloMuteStates() params.muted = soloMuteState.mute || shouldForceMute; params.forceMute = shouldForceMute; - audio::TrackId trackId = m_instrumentTrackIdMap.at(instrumentTrackId); + TrackId trackId = m_instrumentTrackIdMap.at(instrumentTrackId); playback()->audioOutput()->setOutputParams(m_currentSequenceId, trackId, std::move(params)); } diff --git a/src/playback/internal/playbackcontroller.h b/src/playback/internal/playbackcontroller.h index be1a42229a920..bd842f28fa90d 100644 --- a/src/playback/internal/playbackcontroller.h +++ b/src/playback/internal/playbackcontroller.h @@ -64,6 +64,8 @@ class PlaybackController : public IPlaybackController, public muse::actions::Act void reset() override; muse::async::Channel currentPlaybackPositionChanged() const override; + void remoteSeek(const muse::audio::msecs_t msecs) override; + void remotePlayOrStop(const bool playOrStop) override; muse::audio::TrackSequenceId currentTrackSequenceId() const override; muse::async::Notification currentTrackSequenceIdChanged() const override; @@ -224,6 +226,8 @@ class PlaybackController : public IPlaybackController, public muse::actions::Act muse::async::Notification m_currentTempoChanged; muse::async::Channel m_currentPlaybackPositionChanged; muse::async::Channel m_actionCheckedChanged; + muse::async::Channel m_remoteSeek; + muse::async::Channel m_remotePlayOrStop; muse::audio::TrackSequenceId m_currentSequenceId = -1; diff --git a/src/playback/iplaybackconfiguration.h b/src/playback/iplaybackconfiguration.h index e58fec07a4b8b..348c5a463fcfa 100644 --- a/src/playback/iplaybackconfiguration.h +++ b/src/playback/iplaybackconfiguration.h @@ -67,6 +67,10 @@ class IPlaybackConfiguration : MODULE_EXPORT_INTERFACE virtual void setMuteHiddenInstruments(bool mute) = 0; virtual muse::async::Channel muteHiddenInstrumentsChanged() const = 0; + virtual bool jackTransportEnable() const = 0; + virtual void setJackTransportEnable(bool enable) = 0; + virtual muse::async::Channel jackTransportEnableChanged() const = 0; + virtual const SoundProfileName& basicSoundProfileName() const = 0; virtual const SoundProfileName& museSoundProfileName() const = 0; diff --git a/src/playback/iplaybackcontroller.h b/src/playback/iplaybackcontroller.h index 0248e56851637..ff72318b685a4 100644 --- a/src/playback/iplaybackcontroller.h +++ b/src/playback/iplaybackcontroller.h @@ -48,6 +48,8 @@ class IPlaybackController : MODULE_EXPORT_INTERFACE virtual bool isPlaying() const = 0; virtual muse::async::Notification isPlayingChanged() const = 0; + virtual void remoteSeek(const muse::audio::msecs_t msecs) = 0; + virtual void remotePlayOrStop(const bool playOrStop) = 0; virtual void reset() = 0; virtual muse::async::Channel currentPlaybackPositionChanged() const = 0; diff --git a/src/playback/tests/mocks/playbackcontrollermock.h b/src/playback/tests/mocks/playbackcontrollermock.h index 317da3bc41388..2e43fccbbb899 100644 --- a/src/playback/tests/mocks/playbackcontrollermock.h +++ b/src/playback/tests/mocks/playbackcontrollermock.h @@ -36,6 +36,8 @@ class PlaybackControllerMock : public IPlaybackController MOCK_METHOD(bool, isPlaying, (), (const, override)); MOCK_METHOD(muse::async::Notification, isPlayingChanged, (), (const, override)); + MOCK_METHOD(void, remoteSeek, (const muse::audio::msecs_t), (override)); + MOCK_METHOD(void, remotePlayOrStop, (const bool), (override)); MOCK_METHOD(void, reset, (), (override)); MOCK_METHOD((muse::async::Channel), currentPlaybackPositionChanged, (), (const, override)); diff --git a/src/stubs/playback/playbackconfigurationstub.cpp b/src/stubs/playback/playbackconfigurationstub.cpp index 61f069f9ad221..9351ee9da4337 100644 --- a/src/stubs/playback/playbackconfigurationstub.cpp +++ b/src/stubs/playback/playbackconfigurationstub.cpp @@ -135,6 +135,21 @@ muse::async::Channel PlaybackConfigurationStub::muteHiddenInstrumentsChang return ch; } +bool PlaybackConfigurationStub::jackTransportEnable() const +{ + return false; +} + +void PlaybackConfigurationStub::setJackTransportEnable(bool) +{ +} + +muse::async::Channel PlaybackConfigurationStub::jackTransportEnableChanged() const +{ + static muse::async::Channel ch; + return ch; +} + const SoundProfileName& PlaybackConfigurationStub::basicSoundProfileName() const { static const SoundProfileName basic; diff --git a/src/stubs/playback/playbackconfigurationstub.h b/src/stubs/playback/playbackconfigurationstub.h index 9059819e6a5dd..db38a5fc388ee 100644 --- a/src/stubs/playback/playbackconfigurationstub.h +++ b/src/stubs/playback/playbackconfigurationstub.h @@ -61,6 +61,10 @@ class PlaybackConfigurationStub : public IPlaybackConfiguration void setMuteHiddenInstruments(bool mute) override; muse::async::Channel muteHiddenInstrumentsChanged() const override; + bool jackTransportEnable() const override; + void setJackTransportEnable(bool enable) override; + muse::async::Channel jackTransportEnableChanged() const override; + const SoundProfileName& basicSoundProfileName() const override; const SoundProfileName& museSoundProfileName() const override; diff --git a/src/stubs/playback/playbackcontrollerstub.cpp b/src/stubs/playback/playbackcontrollerstub.cpp index fa2ad74f3aacd..7acb28c489c4d 100644 --- a/src/stubs/playback/playbackcontrollerstub.cpp +++ b/src/stubs/playback/playbackcontrollerstub.cpp @@ -44,6 +44,14 @@ muse::async::Notification PlaybackControllerStub::isPlayingChanged() const return muse::async::Notification(); } +void PlaybackControllerStub::remoteSeek(const muse::audio::msecs_t msecs) +{ +} + +void PlaybackControllerStub::remotePlayOrStop(const bool playOrStop) +{ +} + void PlaybackControllerStub::reset() { } diff --git a/src/stubs/playback/playbackcontrollerstub.h b/src/stubs/playback/playbackcontrollerstub.h index 9ab3f484559fe..41e1c72d08d69 100644 --- a/src/stubs/playback/playbackcontrollerstub.h +++ b/src/stubs/playback/playbackcontrollerstub.h @@ -34,6 +34,8 @@ class PlaybackControllerStub : public IPlaybackController bool isPlaying() const override; muse::async::Notification isPlayingChanged() const override; + void remoteSeek(const muse::audio::msecs_t msecs) override; + void remotePlayOrStop(const bool playOrStop) override; void reset() override; muse::async::Channel currentPlaybackPositionChanged() const override;