diff --git a/CMakeLists.txt b/CMakeLists.txt index 4523229..b475ea9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(SDL_MIXER_X_STATIC ON CACHE BOOL "") set(SDL_MIXER_X_SHARED OFF CACHE BOOL "") -set(DOWNLOAD_AUDIO_CODECS_DEPENDENCY ON CACHE BOOL "") +set(DOWNLOAD_AUDIO_CODECS_DEPENDENCY OFF CACHE BOOL "") set(AUDIO_CODECS_BUILD_LOCAL_SDL2 OFF CACHE BOOL "" FORCE) set(MIXERX_LGPL ON CACHE BOOL "" FORCE) set(USE_MIDI ON CACHE BOOL "" FORCE) @@ -30,6 +30,7 @@ find_package(PkgConfig) add_subdirectory(subprojects/jsoncpp) find_package(SDL2 REQUIRED) find_package(sdbus-c++ REQUIRED) +include(GNUInstallDirs) add_subdirectory(subprojects/SDL-Mixer-X) add_subdirectory(subprojects/vgmstream) @@ -136,18 +137,18 @@ if (NOT DISABLE_GTK_UI) list(APPEND ENABLED_UIS "gtk") endif() execute_process(COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/gen_ui_backend_inc.py ${ENABLED_UIS}) -prefix_all(SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/ backend_glue.cpp main.cpp) +prefix_all(SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/ backend_glue.cpp main.cpp daemon_backend.cpp proxy_backend.cpp) add_executable(looper ${SOURCES}) add_dependencies(looper looper_assets ${UI_BACKENDS}) find_program(ASCLI_EXE NAMES "appstreamcli" NO_CACHE) if(${ASCLI_EXE} STREQUAL "ASCLIEXE-NOTFOUND") message("Cannot verify Appstream Metadata.") else() - add_test(NAME "verify appstream metadata" COMMAND ${ASCLI_EXE} validate --no-net --pedantic "assets/com.experimentalcraft.Looper.metainfo.xml") + add_test(NAME "verify appstream metadata" COMMAND ${ASCLI_EXE} validate --no-net --pedantic "assets/com.complecwaft.Looper.metainfo.xml") endif() -target_link_libraries(looper PRIVATE liblooper libvgmstream jsoncpp ${UI_BACKENDS}) +target_link_libraries(looper PUBLIC liblooper libvgmstream jsoncpp ${UI_BACKENDS}) install(TARGETS looper ${EXTRA_LIBS}) install(FILES assets/icon.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps/) -install(FILES assets/looper.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) -install(FILES assets/com.experimentalcraft.Looper.metainfo.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo) +install(FILES assets/com.complecwaft.Looper.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) +install(FILES assets/com.complecwaft.Looper.metainfo.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo) install(DIRECTORY assets/translations/ TYPE LOCALE PATTERN "*" EXCLUDE PATTERN "looper.pot") diff --git a/assets/app.dbus.xml b/assets/app.dbus.xml new file mode 100644 index 0000000..b42fcbf --- /dev/null +++ b/assets/app.dbus.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/com.complecwaft.Looper.desktop b/assets/com.complecwaft.Looper.desktop new file mode 100755 index 0000000..9328c4f --- /dev/null +++ b/assets/com.complecwaft.Looper.desktop @@ -0,0 +1,28 @@ +[Desktop Entry] +Version=1.5 +Type=Application +Name=Looper +Comment=An audio player that can properly loop audio files +GenericName=Looping audio player +Exec=looper -n %f +Icon=looper +StartupWMClass=looper;com.complecwaft.Looper;com.complecwaft.Looper.GTK +MimeType=audio/x-wav;audio/ogg;audio/x-vorbis+ogg;audio/x-opus+ogg;audio/mpeg;audio/flac;audio/xm;audio/x-mod; +Categories=Audio;AudioVideo; +Terminal=false +SingleMainWindow=true +StartupNotify=false +Actions=Open;Daemon;Quit; +Hidden=false +PrefersNonDefaultGPU=false +[Desktop Action Open] +Name=Open Window +Icon=window-new +Exec=looper +[Desktop Action Quit] +Name=Quit Looper +Icon=application-exit +Exec=looper -q +[Desktop Action Daemon] +Name=Start Daemon +Exec=looper -d -n diff --git a/assets/com.experimentalcraft.Looper.metainfo.xml b/assets/com.complecwaft.Looper.metainfo.xml similarity index 91% rename from assets/com.experimentalcraft.Looper.metainfo.xml rename to assets/com.complecwaft.Looper.metainfo.xml index c163785..85779f5 100644 --- a/assets/com.experimentalcraft.Looper.metainfo.xml +++ b/assets/com.complecwaft.Looper.metainfo.xml @@ -1,13 +1,13 @@ - com.experimentalcraft.NekoPlayer + com.complecwaft.Looper Catmeow72 - Neko Player + Looper An audio player that can play back files with seamless loops using their metadata. MIT - MIT AND LGPL-2.1-only AND Zlib + MIT OR GPL-3.0-or-later diff --git a/assets/dbus_stub_adaptor.hpp b/assets/dbus_stub_adaptor.hpp new file mode 100644 index 0000000..8e9b06f --- /dev/null +++ b/assets/dbus_stub_adaptor.hpp @@ -0,0 +1,224 @@ + +/* + * This file was automatically generated by sdbus-c++-xml2cpp; DO NOT EDIT! + */ + +#ifndef __sdbuscpp__dbus_stub_adaptor_hpp__adaptor__H__ +#define __sdbuscpp__dbus_stub_adaptor_hpp__adaptor__H__ + +#include +#include +#include + +namespace org { +namespace freedesktop { + +class Application_adaptor +{ +public: + static constexpr const char* INTERFACE_NAME = "org.freedesktop.Application"; + +protected: + Application_adaptor(sdbus::IObject& object) + : object_(&object) + { + object_->registerMethod("Activate").onInterface(INTERFACE_NAME).withInputParamNames("platform_data").implementedAs([this](const std::map& platform_data){ return this->Activate(platform_data); }); + object_->registerMethod("Open").onInterface(INTERFACE_NAME).withInputParamNames("uris", "platform_data").implementedAs([this](const std::vector& uris, const std::map& platform_data){ return this->Open(uris, platform_data); }); + object_->registerMethod("ActivateAction").onInterface(INTERFACE_NAME).withInputParamNames("action_name", "parameter", "platform_data").implementedAs([this](const std::string& action_name, const std::vector& parameter, const std::map& platform_data){ return this->ActivateAction(action_name, parameter, platform_data); }); + } + + Application_adaptor(const Application_adaptor&) = delete; + Application_adaptor& operator=(const Application_adaptor&) = delete; + Application_adaptor(Application_adaptor&&) = default; + Application_adaptor& operator=(Application_adaptor&&) = default; + + ~Application_adaptor() = default; + +private: + virtual void Activate(const std::map& platform_data) = 0; + virtual void Open(const std::vector& uris, const std::map& platform_data) = 0; + virtual void ActivateAction(const std::string& action_name, const std::vector& parameter, const std::map& platform_data) = 0; + +private: + sdbus::IObject* object_; +}; + +}} // namespaces + +namespace com { +namespace complecwaft { + +class Looper_adaptor +{ +public: + static constexpr const char* INTERFACE_NAME = "com.complecwaft.Looper"; + +protected: + Looper_adaptor(sdbus::IObject& object) + : object_(&object) + { + object_->registerMethod("CreateHandle").onInterface(INTERFACE_NAME).withOutputParamNames("new_handle").implementedAs([this](){ return this->CreateHandle(); }); + object_->registerMethod("ClearHandle").onInterface(INTERFACE_NAME).withInputParamNames("handle").implementedAs([this](const std::string& handle){ return this->ClearHandle(handle); }); + object_->registerMethod("Start").onInterface(INTERFACE_NAME).withInputParamNames("path").implementedAs([this](const std::string& path){ return this->Start(path); }); + object_->registerMethod("StartByURI").onInterface(INTERFACE_NAME).withInputParamNames("uri").implementedAs([this](const std::string& uri){ return this->StartByURI(uri); }); + object_->registerMethod("Quit").onInterface(INTERFACE_NAME).implementedAs([this](){ return this->Quit(); }); + object_->registerMethod("Stop").onInterface(INTERFACE_NAME).implementedAs([this](){ return this->Stop(); }); + object_->registerMethod("TogglePause").onInterface(INTERFACE_NAME).implementedAs([this](){ return this->TogglePause(); }); + object_->registerSignal("PlaybackEngineStarted").onInterface(INTERFACE_NAME); + object_->registerSignal("SpeedChanged").onInterface(INTERFACE_NAME).withParameters("new_speed"); + object_->registerSignal("TempoChanged").onInterface(INTERFACE_NAME).withParameters("new_tempo"); + object_->registerSignal("PitchChanged").onInterface(INTERFACE_NAME).withParameters("new_pitch"); + object_->registerSignal("PauseChanged").onInterface(INTERFACE_NAME).withParameters("now_paused"); + object_->registerSignal("Stopped").onInterface(INTERFACE_NAME); + object_->registerSignal("ErrorOccurred").onInterface(INTERFACE_NAME).withParameters("error_desc", "error_type"); + object_->registerSignal("Seeked").onInterface(INTERFACE_NAME).withParameters("to_position"); + object_->registerSignal("FileChanged").onInterface(INTERFACE_NAME).withParameters("path", "title"); + object_->registerProperty("FilePath").onInterface(INTERFACE_NAME).withGetter([this](){ return this->FilePath(); }); + object_->registerProperty("FileTitle").onInterface(INTERFACE_NAME).withGetter([this](){ return this->FileTitle(); }); + object_->registerProperty("Position").onInterface(INTERFACE_NAME).withGetter([this](){ return this->Position(); }).withSetter([this](const double& value){ this->Position(value); }); + object_->registerProperty("Length").onInterface(INTERFACE_NAME).withGetter([this](){ return this->Length(); }); + object_->registerProperty("Speed").onInterface(INTERFACE_NAME).withGetter([this](){ return this->Speed(); }).withSetter([this](const double& value){ this->Speed(value); }); + object_->registerProperty("Tempo").onInterface(INTERFACE_NAME).withGetter([this](){ return this->Tempo(); }).withSetter([this](const double& value){ this->Tempo(value); }); + object_->registerProperty("Pitch").onInterface(INTERFACE_NAME).withGetter([this](){ return this->Pitch(); }).withSetter([this](const double& value){ this->Pitch(value); }); + object_->registerProperty("Volume").onInterface(INTERFACE_NAME).withGetter([this](){ return this->Volume(); }).withSetter([this](const double& value){ this->Volume(value); }); + object_->registerProperty("Paused").onInterface(INTERFACE_NAME).withGetter([this](){ return this->Paused(); }).withSetter([this](const bool& value){ this->Paused(value); }); + object_->registerProperty("IsStopped").onInterface(INTERFACE_NAME).withGetter([this](){ return this->IsStopped(); }); + object_->registerProperty("IsDaemon").onInterface(INTERFACE_NAME).withGetter([this](){ return this->IsDaemon(); }); + } + + Looper_adaptor(const Looper_adaptor&) = delete; + Looper_adaptor& operator=(const Looper_adaptor&) = delete; + Looper_adaptor(Looper_adaptor&&) = default; + Looper_adaptor& operator=(Looper_adaptor&&) = default; + + ~Looper_adaptor() = default; + +public: + void emitPlaybackEngineStarted() + { + object_->emitSignal("PlaybackEngineStarted").onInterface(INTERFACE_NAME); + } + + void emitSpeedChanged(const double& new_speed) + { + object_->emitSignal("SpeedChanged").onInterface(INTERFACE_NAME).withArguments(new_speed); + } + + void emitTempoChanged(const double& new_tempo) + { + object_->emitSignal("TempoChanged").onInterface(INTERFACE_NAME).withArguments(new_tempo); + } + + void emitPitchChanged(const double& new_pitch) + { + object_->emitSignal("PitchChanged").onInterface(INTERFACE_NAME).withArguments(new_pitch); + } + + void emitPauseChanged(const bool& now_paused) + { + object_->emitSignal("PauseChanged").onInterface(INTERFACE_NAME).withArguments(now_paused); + } + + void emitStopped() + { + object_->emitSignal("Stopped").onInterface(INTERFACE_NAME); + } + + void emitErrorOccurred(const std::string& error_desc, const std::string& error_type) + { + object_->emitSignal("ErrorOccurred").onInterface(INTERFACE_NAME).withArguments(error_desc, error_type); + } + + void emitSeeked(const double& to_position) + { + object_->emitSignal("Seeked").onInterface(INTERFACE_NAME).withArguments(to_position); + } + + void emitFileChanged(const std::string& path, const std::string& title) + { + object_->emitSignal("FileChanged").onInterface(INTERFACE_NAME).withArguments(path, title); + } + +private: + virtual std::string CreateHandle() = 0; + virtual void ClearHandle(const std::string& handle) = 0; + virtual void Start(const std::string& path) = 0; + virtual void StartByURI(const std::string& uri) = 0; + virtual void Quit() = 0; + virtual void Stop() = 0; + virtual void TogglePause() = 0; + +private: + virtual std::string FilePath() = 0; + virtual std::string FileTitle() = 0; + virtual double Position() = 0; + virtual void Position(const double& value) = 0; + virtual double Length() = 0; + virtual double Speed() = 0; + virtual void Speed(const double& value) = 0; + virtual double Tempo() = 0; + virtual void Tempo(const double& value) = 0; + virtual double Pitch() = 0; + virtual void Pitch(const double& value) = 0; + virtual double Volume() = 0; + virtual void Volume(const double& value) = 0; + virtual bool Paused() = 0; + virtual void Paused(const bool& value) = 0; + virtual bool IsStopped() = 0; + virtual bool IsDaemon() = 0; + +private: + sdbus::IObject* object_; +}; + +}} // namespaces + +namespace com { +namespace complecwaft { +namespace Looper { + +class Errors_adaptor +{ +public: + static constexpr const char* INTERFACE_NAME = "com.complecwaft.Looper.Errors"; + +protected: + Errors_adaptor(sdbus::IObject& object) + : object_(&object) + { + object_->registerMethod("PopFront").onInterface(INTERFACE_NAME).withInputParamNames("handle").withOutputParamNames("error").implementedAs([this](const std::string& handle){ return this->PopFront(handle); }); + object_->registerMethod("PopBack").onInterface(INTERFACE_NAME).withInputParamNames("handle").withOutputParamNames("error").implementedAs([this](const std::string& handle){ return this->PopBack(handle); }); + object_->registerMethod("PeekFront").onInterface(INTERFACE_NAME).withInputParamNames("handle").withOutputParamNames("error").implementedAs([this](const std::string& handle){ return this->PeekFront(handle); }); + object_->registerMethod("PeekBack").onInterface(INTERFACE_NAME).withInputParamNames("handle").withOutputParamNames("error").implementedAs([this](const std::string& handle){ return this->PeekBack(handle); }); + object_->registerMethod("GetCount").onInterface(INTERFACE_NAME).withInputParamNames("handle").withOutputParamNames("count").implementedAs([this](const std::string& handle){ return this->GetCount(handle); }); + object_->registerMethod("IsEmpty").onInterface(INTERFACE_NAME).withInputParamNames("handle").withOutputParamNames("empty").implementedAs([this](const std::string& handle){ return this->IsEmpty(handle); }); + object_->registerMethod("Clear").onInterface(INTERFACE_NAME).withInputParamNames("handle").implementedAs([this](const std::string& handle){ return this->Clear(handle); }); + object_->registerMethod("PeekAll").onInterface(INTERFACE_NAME).withInputParamNames("handle").withOutputParamNames("errors").implementedAs([this](const std::string& handle){ return this->PeekAll(handle); }); + object_->registerMethod("GetAllAndClear").onInterface(INTERFACE_NAME).withInputParamNames("handle").withOutputParamNames("errors").implementedAs([this](const std::string& handle){ return this->GetAllAndClear(handle); }); + } + + Errors_adaptor(const Errors_adaptor&) = delete; + Errors_adaptor& operator=(const Errors_adaptor&) = delete; + Errors_adaptor(Errors_adaptor&&) = default; + Errors_adaptor& operator=(Errors_adaptor&&) = default; + + ~Errors_adaptor() = default; + +private: + virtual std::string PopFront(const std::string& handle) = 0; + virtual std::string PopBack(const std::string& handle) = 0; + virtual std::string PeekFront(const std::string& handle) = 0; + virtual std::string PeekBack(const std::string& handle) = 0; + virtual uint32_t GetCount(const std::string& handle) = 0; + virtual bool IsEmpty(const std::string& handle) = 0; + virtual void Clear(const std::string& handle) = 0; + virtual std::vector PeekAll(const std::string& handle) = 0; + virtual std::vector GetAllAndClear(const std::string& handle) = 0; + +private: + sdbus::IObject* object_; +}; + +}}} // namespaces + +#endif diff --git a/assets/dbus_stub_proxy.hpp b/assets/dbus_stub_proxy.hpp new file mode 100644 index 0000000..68ece7f --- /dev/null +++ b/assets/dbus_stub_proxy.hpp @@ -0,0 +1,316 @@ + +/* + * This file was automatically generated by sdbus-c++-xml2cpp; DO NOT EDIT! + */ + +#ifndef __sdbuscpp__dbus_stub_proxy_hpp__proxy__H__ +#define __sdbuscpp__dbus_stub_proxy_hpp__proxy__H__ + +#include +#include +#include + +namespace org { +namespace freedesktop { + +class Application_proxy +{ +public: + static constexpr const char* INTERFACE_NAME = "org.freedesktop.Application"; + +protected: + Application_proxy(sdbus::IProxy& proxy) + : proxy_(&proxy) + { + } + + Application_proxy(const Application_proxy&) = delete; + Application_proxy& operator=(const Application_proxy&) = delete; + Application_proxy(Application_proxy&&) = default; + Application_proxy& operator=(Application_proxy&&) = default; + + ~Application_proxy() = default; + +public: + void Activate(const std::map& platform_data) + { + proxy_->callMethod("Activate").onInterface(INTERFACE_NAME).withArguments(platform_data); + } + + void Open(const std::vector& uris, const std::map& platform_data) + { + proxy_->callMethod("Open").onInterface(INTERFACE_NAME).withArguments(uris, platform_data); + } + + void ActivateAction(const std::string& action_name, const std::vector& parameter, const std::map& platform_data) + { + proxy_->callMethod("ActivateAction").onInterface(INTERFACE_NAME).withArguments(action_name, parameter, platform_data); + } + +private: + sdbus::IProxy* proxy_; +}; + +}} // namespaces + +namespace com { +namespace complecwaft { + +class Looper_proxy +{ +public: + static constexpr const char* INTERFACE_NAME = "com.complecwaft.Looper"; + +protected: + Looper_proxy(sdbus::IProxy& proxy) + : proxy_(&proxy) + { + proxy_->uponSignal("PlaybackEngineStarted").onInterface(INTERFACE_NAME).call([this](){ this->onPlaybackEngineStarted(); }); + proxy_->uponSignal("SpeedChanged").onInterface(INTERFACE_NAME).call([this](const double& new_speed){ this->onSpeedChanged(new_speed); }); + proxy_->uponSignal("TempoChanged").onInterface(INTERFACE_NAME).call([this](const double& new_tempo){ this->onTempoChanged(new_tempo); }); + proxy_->uponSignal("PitchChanged").onInterface(INTERFACE_NAME).call([this](const double& new_pitch){ this->onPitchChanged(new_pitch); }); + proxy_->uponSignal("PauseChanged").onInterface(INTERFACE_NAME).call([this](const bool& now_paused){ this->onPauseChanged(now_paused); }); + proxy_->uponSignal("Stopped").onInterface(INTERFACE_NAME).call([this](){ this->onStopped(); }); + proxy_->uponSignal("ErrorOccurred").onInterface(INTERFACE_NAME).call([this](const std::string& error_desc, const std::string& error_type){ this->onErrorOccurred(error_desc, error_type); }); + proxy_->uponSignal("Seeked").onInterface(INTERFACE_NAME).call([this](const double& to_position){ this->onSeeked(to_position); }); + proxy_->uponSignal("FileChanged").onInterface(INTERFACE_NAME).call([this](const std::string& path, const std::string& title){ this->onFileChanged(path, title); }); + } + + Looper_proxy(const Looper_proxy&) = delete; + Looper_proxy& operator=(const Looper_proxy&) = delete; + Looper_proxy(Looper_proxy&&) = default; + Looper_proxy& operator=(Looper_proxy&&) = default; + + ~Looper_proxy() = default; + + virtual void onPlaybackEngineStarted() = 0; + virtual void onSpeedChanged(const double& new_speed) = 0; + virtual void onTempoChanged(const double& new_tempo) = 0; + virtual void onPitchChanged(const double& new_pitch) = 0; + virtual void onPauseChanged(const bool& now_paused) = 0; + virtual void onStopped() = 0; + virtual void onErrorOccurred(const std::string& error_desc, const std::string& error_type) = 0; + virtual void onSeeked(const double& to_position) = 0; + virtual void onFileChanged(const std::string& path, const std::string& title) = 0; + +public: + std::string CreateHandle() + { + std::string result; + proxy_->callMethod("CreateHandle").onInterface(INTERFACE_NAME).storeResultsTo(result); + return result; + } + + void ClearHandle(const std::string& handle) + { + proxy_->callMethod("ClearHandle").onInterface(INTERFACE_NAME).withArguments(handle); + } + + void Start(const std::string& path) + { + proxy_->callMethod("Start").onInterface(INTERFACE_NAME).withArguments(path); + } + + void StartByURI(const std::string& uri) + { + proxy_->callMethod("StartByURI").onInterface(INTERFACE_NAME).withArguments(uri); + } + + void Quit() + { + proxy_->callMethod("Quit").onInterface(INTERFACE_NAME); + } + + void Stop() + { + proxy_->callMethod("Stop").onInterface(INTERFACE_NAME); + } + + void TogglePause() + { + proxy_->callMethod("TogglePause").onInterface(INTERFACE_NAME); + } + +public: + std::string FilePath() + { + return proxy_->getProperty("FilePath").onInterface(INTERFACE_NAME); + } + + std::string FileTitle() + { + return proxy_->getProperty("FileTitle").onInterface(INTERFACE_NAME); + } + + double Position() + { + return proxy_->getProperty("Position").onInterface(INTERFACE_NAME); + } + + void Position(const double& value) + { + proxy_->setProperty("Position").onInterface(INTERFACE_NAME).toValue(value); + } + + double Length() + { + return proxy_->getProperty("Length").onInterface(INTERFACE_NAME); + } + + double Speed() + { + return proxy_->getProperty("Speed").onInterface(INTERFACE_NAME); + } + + void Speed(const double& value) + { + proxy_->setProperty("Speed").onInterface(INTERFACE_NAME).toValue(value); + } + + double Tempo() + { + return proxy_->getProperty("Tempo").onInterface(INTERFACE_NAME); + } + + void Tempo(const double& value) + { + proxy_->setProperty("Tempo").onInterface(INTERFACE_NAME).toValue(value); + } + + double Pitch() + { + return proxy_->getProperty("Pitch").onInterface(INTERFACE_NAME); + } + + void Pitch(const double& value) + { + proxy_->setProperty("Pitch").onInterface(INTERFACE_NAME).toValue(value); + } + + double Volume() + { + return proxy_->getProperty("Volume").onInterface(INTERFACE_NAME); + } + + void Volume(const double& value) + { + proxy_->setProperty("Volume").onInterface(INTERFACE_NAME).toValue(value); + } + + bool Paused() + { + return proxy_->getProperty("Paused").onInterface(INTERFACE_NAME); + } + + void Paused(const bool& value) + { + proxy_->setProperty("Paused").onInterface(INTERFACE_NAME).toValue(value); + } + + bool IsStopped() + { + return proxy_->getProperty("IsStopped").onInterface(INTERFACE_NAME); + } + + bool IsDaemon() + { + return proxy_->getProperty("IsDaemon").onInterface(INTERFACE_NAME); + } + +private: + sdbus::IProxy* proxy_; +}; + +}} // namespaces + +namespace com { +namespace complecwaft { +namespace Looper { + +class Errors_proxy +{ +public: + static constexpr const char* INTERFACE_NAME = "com.complecwaft.Looper.Errors"; + +protected: + Errors_proxy(sdbus::IProxy& proxy) + : proxy_(&proxy) + { + } + + Errors_proxy(const Errors_proxy&) = delete; + Errors_proxy& operator=(const Errors_proxy&) = delete; + Errors_proxy(Errors_proxy&&) = default; + Errors_proxy& operator=(Errors_proxy&&) = default; + + ~Errors_proxy() = default; + +public: + std::string PopFront(const std::string& handle) + { + std::string result; + proxy_->callMethod("PopFront").onInterface(INTERFACE_NAME).withArguments(handle).storeResultsTo(result); + return result; + } + + std::string PopBack(const std::string& handle) + { + std::string result; + proxy_->callMethod("PopBack").onInterface(INTERFACE_NAME).withArguments(handle).storeResultsTo(result); + return result; + } + + std::string PeekFront(const std::string& handle) + { + std::string result; + proxy_->callMethod("PeekFront").onInterface(INTERFACE_NAME).withArguments(handle).storeResultsTo(result); + return result; + } + + std::string PeekBack(const std::string& handle) + { + std::string result; + proxy_->callMethod("PeekBack").onInterface(INTERFACE_NAME).withArguments(handle).storeResultsTo(result); + return result; + } + + uint32_t GetCount(const std::string& handle) + { + uint32_t result; + proxy_->callMethod("GetCount").onInterface(INTERFACE_NAME).withArguments(handle).storeResultsTo(result); + return result; + } + + bool IsEmpty(const std::string& handle) + { + bool result; + proxy_->callMethod("IsEmpty").onInterface(INTERFACE_NAME).withArguments(handle).storeResultsTo(result); + return result; + } + + void Clear(const std::string& handle) + { + proxy_->callMethod("Clear").onInterface(INTERFACE_NAME).withArguments(handle); + } + + std::vector PeekAll(const std::string& handle) + { + std::vector result; + proxy_->callMethod("PeekAll").onInterface(INTERFACE_NAME).withArguments(handle).storeResultsTo(result); + return result; + } + + std::vector GetAllAndClear(const std::string& handle) + { + std::vector result; + proxy_->callMethod("GetAllAndClear").onInterface(INTERFACE_NAME).withArguments(handle).storeResultsTo(result); + return result; + } + +private: + sdbus::IProxy* proxy_; +}; + +}}} // namespaces + +#endif diff --git a/assets/looper.desktop b/assets/looper.desktop deleted file mode 100755 index 20bc2c8..0000000 --- a/assets/looper.desktop +++ /dev/null @@ -1,10 +0,0 @@ -[Desktop Entry] -Type=Application -Name=Looper -Comment=An audio player that can properly loop audio files -GenericName=Looping audio player -Exec=looper %f -Icon=looper -MimeType=audio/x-wav;audio/ogg;audio/x-vorbis+ogg;audio/x-opus+ogg;audio/mpeg;audio/flac;audio/xm;audio/x-mod; -StartupWMClass=looper -Categories=Audio;AudioVideo; diff --git a/assets/mpris.dbus.xml b/assets/mpris.dbus.xml new file mode 100644 index 0000000..642582b --- /dev/null +++ b/assets/mpris.dbus.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/mpris_stub_adaptor.hpp b/assets/mpris_stub_adaptor.hpp new file mode 100644 index 0000000..ccf5da5 --- /dev/null +++ b/assets/mpris_stub_adaptor.hpp @@ -0,0 +1,214 @@ + +/* + * This file was automatically generated by sdbus-c++-xml2cpp; DO NOT EDIT! + */ + +#ifndef __sdbuscpp__mpris_stub_adaptor_hpp__adaptor__H__ +#define __sdbuscpp__mpris_stub_adaptor_hpp__adaptor__H__ + +#include +#include +#include + +namespace org { +namespace mpris { + +class MediaPlayer2_adaptor +{ +public: + static constexpr const char* INTERFACE_NAME = "org.mpris.MediaPlayer2"; + +protected: + MediaPlayer2_adaptor(sdbus::IObject& object) + : object_(&object) + { + object_->registerMethod("Raise").onInterface(INTERFACE_NAME).implementedAs([this](){ return this->Raise(); }); + object_->registerMethod("Quit").onInterface(INTERFACE_NAME).implementedAs([this](){ return this->Quit(); }); + object_->registerProperty("CanRaise").onInterface(INTERFACE_NAME).withGetter([this](){ return this->CanRaise(); }); + object_->registerProperty("CanQuit").onInterface(INTERFACE_NAME).withGetter([this](){ return this->CanQuit(); }); + object_->registerProperty("HasTrackList").onInterface(INTERFACE_NAME).withGetter([this](){ return this->HasTrackList(); }); + object_->registerProperty("Identity").onInterface(INTERFACE_NAME).withGetter([this](){ return this->Identity(); }); + object_->registerProperty("SupportedUriSchemes").onInterface(INTERFACE_NAME).withGetter([this](){ return this->SupportedUriSchemes(); }); + object_->registerProperty("SupportedMimeTypes").onInterface(INTERFACE_NAME).withGetter([this](){ return this->SupportedMimeTypes(); }); + } + + MediaPlayer2_adaptor(const MediaPlayer2_adaptor&) = delete; + MediaPlayer2_adaptor& operator=(const MediaPlayer2_adaptor&) = delete; + MediaPlayer2_adaptor(MediaPlayer2_adaptor&&) = default; + MediaPlayer2_adaptor& operator=(MediaPlayer2_adaptor&&) = default; + + ~MediaPlayer2_adaptor() = default; + +private: + virtual void Raise() = 0; + virtual void Quit() = 0; + +private: + virtual bool CanRaise() = 0; + virtual bool CanQuit() = 0; + virtual bool HasTrackList() = 0; + virtual std::string Identity() = 0; + virtual std::vector SupportedUriSchemes() = 0; + virtual std::vector SupportedMimeTypes() = 0; + +private: + sdbus::IObject* object_; +}; + +}} // namespaces + +namespace org { +namespace mpris { +namespace MediaPlayer2 { + +class Player_adaptor +{ +public: + static constexpr const char* INTERFACE_NAME = "org.mpris.MediaPlayer2.Player"; + +protected: + Player_adaptor(sdbus::IObject& object) + : object_(&object) + { + object_->registerMethod("Next").onInterface(INTERFACE_NAME).implementedAs([this](){ return this->Next(); }); + object_->registerMethod("Previous").onInterface(INTERFACE_NAME).implementedAs([this](){ return this->Previous(); }); + object_->registerMethod("Pause").onInterface(INTERFACE_NAME).implementedAs([this](){ return this->Pause(); }); + object_->registerMethod("PlayPause").onInterface(INTERFACE_NAME).implementedAs([this](){ return this->PlayPause(); }); + object_->registerMethod("Stop").onInterface(INTERFACE_NAME).implementedAs([this](){ return this->Stop(); }); + object_->registerMethod("Play").onInterface(INTERFACE_NAME).implementedAs([this](){ return this->Play(); }); + object_->registerMethod("Seek").onInterface(INTERFACE_NAME).withInputParamNames("Offset").implementedAs([this](const int64_t& Offset){ return this->Seek(Offset); }); + object_->registerMethod("SetPosition").onInterface(INTERFACE_NAME).withInputParamNames("TrackId", "Position").implementedAs([this](const sdbus::ObjectPath& TrackId, const int64_t& Position){ return this->SetPosition(TrackId, Position); }); + object_->registerMethod("OpenUri").onInterface(INTERFACE_NAME).withInputParamNames("Uri").implementedAs([this](const std::string& Uri){ return this->OpenUri(Uri); }); + object_->registerSignal("Seeked").onInterface(INTERFACE_NAME).withParameters("Position"); + object_->registerProperty("PlaybackStatus").onInterface(INTERFACE_NAME).withGetter([this](){ return this->PlaybackStatus(); }); + object_->registerProperty("Rate").onInterface(INTERFACE_NAME).withGetter([this](){ return this->Rate(); }).withSetter([this](const double& value){ this->Rate(value); }); + object_->registerProperty("Metadata").onInterface(INTERFACE_NAME).withGetter([this](){ return this->Metadata(); }); + object_->registerProperty("Volume").onInterface(INTERFACE_NAME).withGetter([this](){ return this->Volume(); }).withSetter([this](const double& value){ this->Volume(value); }); + object_->registerProperty("Position").onInterface(INTERFACE_NAME).withGetter([this](){ return this->Position(); }); + object_->registerProperty("MinimumRate").onInterface(INTERFACE_NAME).withGetter([this](){ return this->MinimumRate(); }); + object_->registerProperty("MaximumRate").onInterface(INTERFACE_NAME).withGetter([this](){ return this->MaximumRate(); }); + object_->registerProperty("CanGoNext").onInterface(INTERFACE_NAME).withGetter([this](){ return this->CanGoNext(); }); + object_->registerProperty("CanGoPrevious").onInterface(INTERFACE_NAME).withGetter([this](){ return this->CanGoPrevious(); }); + object_->registerProperty("CanPlay").onInterface(INTERFACE_NAME).withGetter([this](){ return this->CanPlay(); }); + object_->registerProperty("CanPause").onInterface(INTERFACE_NAME).withGetter([this](){ return this->CanPause(); }); + object_->registerProperty("CanSeek").onInterface(INTERFACE_NAME).withGetter([this](){ return this->CanSeek(); }); + object_->registerProperty("CanControl").onInterface(INTERFACE_NAME).withGetter([this](){ return this->CanControl(); }); + } + + Player_adaptor(const Player_adaptor&) = delete; + Player_adaptor& operator=(const Player_adaptor&) = delete; + Player_adaptor(Player_adaptor&&) = default; + Player_adaptor& operator=(Player_adaptor&&) = default; + + ~Player_adaptor() = default; + +public: + void emitSeeked(const int64_t& Position) + { + object_->emitSignal("Seeked").onInterface(INTERFACE_NAME).withArguments(Position); + } + +private: + virtual void Next() = 0; + virtual void Previous() = 0; + virtual void Pause() = 0; + virtual void PlayPause() = 0; + virtual void Stop() = 0; + virtual void Play() = 0; + virtual void Seek(const int64_t& Offset) = 0; + virtual void SetPosition(const sdbus::ObjectPath& TrackId, const int64_t& Position) = 0; + virtual void OpenUri(const std::string& Uri) = 0; + +private: + virtual std::string PlaybackStatus() = 0; + virtual double Rate() = 0; + virtual void Rate(const double& value) = 0; + virtual std::map Metadata() = 0; + virtual double Volume() = 0; + virtual void Volume(const double& value) = 0; + virtual int64_t Position() = 0; + virtual double MinimumRate() = 0; + virtual double MaximumRate() = 0; + virtual bool CanGoNext() = 0; + virtual bool CanGoPrevious() = 0; + virtual bool CanPlay() = 0; + virtual bool CanPause() = 0; + virtual bool CanSeek() = 0; + virtual bool CanControl() = 0; + +private: + sdbus::IObject* object_; +}; + +}}} // namespaces + +namespace org { +namespace mpris { +namespace MediaPlayer2 { + +class TrackList_adaptor +{ +public: + static constexpr const char* INTERFACE_NAME = "org.mpris.MediaPlayer2.TrackList"; + +protected: + TrackList_adaptor(sdbus::IObject& object) + : object_(&object) + { + object_->registerMethod("GetTracksMetadata").onInterface(INTERFACE_NAME).withInputParamNames("TrackIds").withOutputParamNames("Metadata").implementedAs([this](const std::vector& TrackIds){ return this->GetTracksMetadata(TrackIds); }); + object_->registerMethod("AddTrack").onInterface(INTERFACE_NAME).withInputParamNames("Uri", "AfterTrack", "SetAsCurrent").implementedAs([this](const std::string& Uri, const sdbus::ObjectPath& AfterTrack, const bool& SetAsCurrent){ return this->AddTrack(Uri, AfterTrack, SetAsCurrent); }); + object_->registerMethod("RemoveTrack").onInterface(INTERFACE_NAME).withInputParamNames("TrackId").implementedAs([this](const sdbus::ObjectPath& TrackId){ return this->RemoveTrack(TrackId); }); + object_->registerMethod("GoTo").onInterface(INTERFACE_NAME).withInputParamNames("TrackId").implementedAs([this](const sdbus::ObjectPath& TrackId){ return this->GoTo(TrackId); }); + object_->registerSignal("TrackListReplaced").onInterface(INTERFACE_NAME).withParameters, sdbus::ObjectPath>("Tracks", "CurrentTrack"); + object_->registerSignal("TrackAdded").onInterface(INTERFACE_NAME).withParameters, sdbus::ObjectPath>("Metadata", "AfterTrack"); + object_->registerSignal("TrackRemoved").onInterface(INTERFACE_NAME).withParameters("TrackId"); + object_->registerSignal("TrackMetadataChanged").onInterface(INTERFACE_NAME).withParameters>("TrackId", "Metadata"); + object_->registerProperty("Tracks").onInterface(INTERFACE_NAME).withGetter([this](){ return this->Tracks(); }); + object_->registerProperty("CanEditTracks").onInterface(INTERFACE_NAME).withGetter([this](){ return this->CanEditTracks(); }); + } + + TrackList_adaptor(const TrackList_adaptor&) = delete; + TrackList_adaptor& operator=(const TrackList_adaptor&) = delete; + TrackList_adaptor(TrackList_adaptor&&) = default; + TrackList_adaptor& operator=(TrackList_adaptor&&) = default; + + ~TrackList_adaptor() = default; + +public: + void emitTrackListReplaced(const std::vector& Tracks, const sdbus::ObjectPath& CurrentTrack) + { + object_->emitSignal("TrackListReplaced").onInterface(INTERFACE_NAME).withArguments(Tracks, CurrentTrack); + } + + void emitTrackAdded(const std::map& Metadata, const sdbus::ObjectPath& AfterTrack) + { + object_->emitSignal("TrackAdded").onInterface(INTERFACE_NAME).withArguments(Metadata, AfterTrack); + } + + void emitTrackRemoved(const sdbus::ObjectPath& TrackId) + { + object_->emitSignal("TrackRemoved").onInterface(INTERFACE_NAME).withArguments(TrackId); + } + + void emitTrackMetadataChanged(const sdbus::ObjectPath& TrackId, const std::map& Metadata) + { + object_->emitSignal("TrackMetadataChanged").onInterface(INTERFACE_NAME).withArguments(TrackId, Metadata); + } + +private: + virtual std::vector> GetTracksMetadata(const std::vector& TrackIds) = 0; + virtual void AddTrack(const std::string& Uri, const sdbus::ObjectPath& AfterTrack, const bool& SetAsCurrent) = 0; + virtual void RemoveTrack(const sdbus::ObjectPath& TrackId) = 0; + virtual void GoTo(const sdbus::ObjectPath& TrackId) = 0; + +private: + virtual std::vector Tracks() = 0; + virtual bool CanEditTracks() = 0; + +private: + sdbus::IObject* object_; +}; + +}}} // namespaces + +#endif diff --git a/assets/mpris_stub_proxy.hpp b/assets/mpris_stub_proxy.hpp new file mode 100644 index 0000000..72a660b --- /dev/null +++ b/assets/mpris_stub_proxy.hpp @@ -0,0 +1,306 @@ + +/* + * This file was automatically generated by sdbus-c++-xml2cpp; DO NOT EDIT! + */ + +#ifndef __sdbuscpp__mpris_stub_proxy_hpp__proxy__H__ +#define __sdbuscpp__mpris_stub_proxy_hpp__proxy__H__ + +#include +#include +#include + +namespace org { +namespace mpris { + +class MediaPlayer2_proxy +{ +public: + static constexpr const char* INTERFACE_NAME = "org.mpris.MediaPlayer2"; + +protected: + MediaPlayer2_proxy(sdbus::IProxy& proxy) + : proxy_(&proxy) + { + } + + MediaPlayer2_proxy(const MediaPlayer2_proxy&) = delete; + MediaPlayer2_proxy& operator=(const MediaPlayer2_proxy&) = delete; + MediaPlayer2_proxy(MediaPlayer2_proxy&&) = default; + MediaPlayer2_proxy& operator=(MediaPlayer2_proxy&&) = default; + + ~MediaPlayer2_proxy() = default; + +public: + void Raise() + { + proxy_->callMethod("Raise").onInterface(INTERFACE_NAME); + } + + void Quit() + { + proxy_->callMethod("Quit").onInterface(INTERFACE_NAME); + } + +public: + bool CanRaise() + { + return proxy_->getProperty("CanRaise").onInterface(INTERFACE_NAME); + } + + bool CanQuit() + { + return proxy_->getProperty("CanQuit").onInterface(INTERFACE_NAME); + } + + bool HasTrackList() + { + return proxy_->getProperty("HasTrackList").onInterface(INTERFACE_NAME); + } + + std::string Identity() + { + return proxy_->getProperty("Identity").onInterface(INTERFACE_NAME); + } + + std::vector SupportedUriSchemes() + { + return proxy_->getProperty("SupportedUriSchemes").onInterface(INTERFACE_NAME); + } + + std::vector SupportedMimeTypes() + { + return proxy_->getProperty("SupportedMimeTypes").onInterface(INTERFACE_NAME); + } + +private: + sdbus::IProxy* proxy_; +}; + +}} // namespaces + +namespace org { +namespace mpris { +namespace MediaPlayer2 { + +class Player_proxy +{ +public: + static constexpr const char* INTERFACE_NAME = "org.mpris.MediaPlayer2.Player"; + +protected: + Player_proxy(sdbus::IProxy& proxy) + : proxy_(&proxy) + { + proxy_->uponSignal("Seeked").onInterface(INTERFACE_NAME).call([this](const int64_t& Position){ this->onSeeked(Position); }); + } + + Player_proxy(const Player_proxy&) = delete; + Player_proxy& operator=(const Player_proxy&) = delete; + Player_proxy(Player_proxy&&) = default; + Player_proxy& operator=(Player_proxy&&) = default; + + ~Player_proxy() = default; + + virtual void onSeeked(const int64_t& Position) = 0; + +public: + void Next() + { + proxy_->callMethod("Next").onInterface(INTERFACE_NAME); + } + + void Previous() + { + proxy_->callMethod("Previous").onInterface(INTERFACE_NAME); + } + + void Pause() + { + proxy_->callMethod("Pause").onInterface(INTERFACE_NAME); + } + + void PlayPause() + { + proxy_->callMethod("PlayPause").onInterface(INTERFACE_NAME); + } + + void Stop() + { + proxy_->callMethod("Stop").onInterface(INTERFACE_NAME); + } + + void Play() + { + proxy_->callMethod("Play").onInterface(INTERFACE_NAME); + } + + void Seek(const int64_t& Offset) + { + proxy_->callMethod("Seek").onInterface(INTERFACE_NAME).withArguments(Offset); + } + + void SetPosition(const sdbus::ObjectPath& TrackId, const int64_t& Position) + { + proxy_->callMethod("SetPosition").onInterface(INTERFACE_NAME).withArguments(TrackId, Position); + } + + void OpenUri(const std::string& Uri) + { + proxy_->callMethod("OpenUri").onInterface(INTERFACE_NAME).withArguments(Uri); + } + +public: + std::string PlaybackStatus() + { + return proxy_->getProperty("PlaybackStatus").onInterface(INTERFACE_NAME); + } + + double Rate() + { + return proxy_->getProperty("Rate").onInterface(INTERFACE_NAME); + } + + void Rate(const double& value) + { + proxy_->setProperty("Rate").onInterface(INTERFACE_NAME).toValue(value); + } + + std::map Metadata() + { + return proxy_->getProperty("Metadata").onInterface(INTERFACE_NAME); + } + + double Volume() + { + return proxy_->getProperty("Volume").onInterface(INTERFACE_NAME); + } + + void Volume(const double& value) + { + proxy_->setProperty("Volume").onInterface(INTERFACE_NAME).toValue(value); + } + + int64_t Position() + { + return proxy_->getProperty("Position").onInterface(INTERFACE_NAME); + } + + double MinimumRate() + { + return proxy_->getProperty("MinimumRate").onInterface(INTERFACE_NAME); + } + + double MaximumRate() + { + return proxy_->getProperty("MaximumRate").onInterface(INTERFACE_NAME); + } + + bool CanGoNext() + { + return proxy_->getProperty("CanGoNext").onInterface(INTERFACE_NAME); + } + + bool CanGoPrevious() + { + return proxy_->getProperty("CanGoPrevious").onInterface(INTERFACE_NAME); + } + + bool CanPlay() + { + return proxy_->getProperty("CanPlay").onInterface(INTERFACE_NAME); + } + + bool CanPause() + { + return proxy_->getProperty("CanPause").onInterface(INTERFACE_NAME); + } + + bool CanSeek() + { + return proxy_->getProperty("CanSeek").onInterface(INTERFACE_NAME); + } + + bool CanControl() + { + return proxy_->getProperty("CanControl").onInterface(INTERFACE_NAME); + } + +private: + sdbus::IProxy* proxy_; +}; + +}}} // namespaces + +namespace org { +namespace mpris { +namespace MediaPlayer2 { + +class TrackList_proxy +{ +public: + static constexpr const char* INTERFACE_NAME = "org.mpris.MediaPlayer2.TrackList"; + +protected: + TrackList_proxy(sdbus::IProxy& proxy) + : proxy_(&proxy) + { + proxy_->uponSignal("TrackListReplaced").onInterface(INTERFACE_NAME).call([this](const std::vector& Tracks, const sdbus::ObjectPath& CurrentTrack){ this->onTrackListReplaced(Tracks, CurrentTrack); }); + proxy_->uponSignal("TrackAdded").onInterface(INTERFACE_NAME).call([this](const std::map& Metadata, const sdbus::ObjectPath& AfterTrack){ this->onTrackAdded(Metadata, AfterTrack); }); + proxy_->uponSignal("TrackRemoved").onInterface(INTERFACE_NAME).call([this](const sdbus::ObjectPath& TrackId){ this->onTrackRemoved(TrackId); }); + proxy_->uponSignal("TrackMetadataChanged").onInterface(INTERFACE_NAME).call([this](const sdbus::ObjectPath& TrackId, const std::map& Metadata){ this->onTrackMetadataChanged(TrackId, Metadata); }); + } + + TrackList_proxy(const TrackList_proxy&) = delete; + TrackList_proxy& operator=(const TrackList_proxy&) = delete; + TrackList_proxy(TrackList_proxy&&) = default; + TrackList_proxy& operator=(TrackList_proxy&&) = default; + + ~TrackList_proxy() = default; + + virtual void onTrackListReplaced(const std::vector& Tracks, const sdbus::ObjectPath& CurrentTrack) = 0; + virtual void onTrackAdded(const std::map& Metadata, const sdbus::ObjectPath& AfterTrack) = 0; + virtual void onTrackRemoved(const sdbus::ObjectPath& TrackId) = 0; + virtual void onTrackMetadataChanged(const sdbus::ObjectPath& TrackId, const std::map& Metadata) = 0; + +public: + std::vector> GetTracksMetadata(const std::vector& TrackIds) + { + std::vector> result; + proxy_->callMethod("GetTracksMetadata").onInterface(INTERFACE_NAME).withArguments(TrackIds).storeResultsTo(result); + return result; + } + + void AddTrack(const std::string& Uri, const sdbus::ObjectPath& AfterTrack, const bool& SetAsCurrent) + { + proxy_->callMethod("AddTrack").onInterface(INTERFACE_NAME).withArguments(Uri, AfterTrack, SetAsCurrent); + } + + void RemoveTrack(const sdbus::ObjectPath& TrackId) + { + proxy_->callMethod("RemoveTrack").onInterface(INTERFACE_NAME).withArguments(TrackId); + } + + void GoTo(const sdbus::ObjectPath& TrackId) + { + proxy_->callMethod("GoTo").onInterface(INTERFACE_NAME).withArguments(TrackId); + } + +public: + std::vector Tracks() + { + return proxy_->getProperty("Tracks").onInterface(INTERFACE_NAME); + } + + bool CanEditTracks() + { + return proxy_->getProperty("CanEditTracks").onInterface(INTERFACE_NAME); + } + +private: + sdbus::IProxy* proxy_; +}; + +}}} // namespaces + +#endif diff --git a/assets/update_assets.py b/assets/update_assets.py index b709fae..cff910a 100755 --- a/assets/update_assets.py +++ b/assets/update_assets.py @@ -4,11 +4,21 @@ import sys import subprocess import os.path as path import shutil +from shutil import which from glob import glob olddir = os.curdir print("Entering assets directory to begin asset conversion...") os.chdir(path.realpath(path.dirname(__file__))) ASSETS = [] +WARNINGS: dict[str, bool] = {} +def warn(id: str, message: str, WARNINGS = WARNINGS) -> None: + warned_already: bool = False + try: + warned_already = WARNINGS[id] + except KeyError: + pass + if not warned_already: + print("WARNING: %s" % message) def run_btcc(args: list[str], outpath: str): with open(outpath + ".h", "wt+") as f: actual_args = ["./btcc"] @@ -44,6 +54,15 @@ def add_css(file: str, output: str): CSS_FILE = output + ".h" print("Adding CSS file '%s' (C identifier '%s') from file '%s'" % (file, output, CSS_FILE)) add_basic(file, output) +def add_dbus(file: str, output_basename: str, adaptor: str|None = None, proxy: str|None = None, WARNINGS = WARNINGS): + if adaptor == None: + adaptor = output_basename + '_adaptor.hpp' + if proxy == None: + proxy = output_basename + '_proxy.hpp' + if which('sdbus-c++-xml2cpp') is not None: + subprocess.call(['sdbus-c++-xml2cpp', file, '--adaptor=' + adaptor, '--proxy=' + proxy]) + else: + warn("Not generating DBus API stubs.") compile_program("../backends/ui/imgui/imgui/misc/fonts/binary_to_compressed_c.cpp", "btcc") for i in glob("Noto_Sans/*.ttf"): add_font(i) @@ -69,6 +88,8 @@ add_license("licenses/cli11.txt", "cli11") add_license("licenses/TomlPlusPlus.txt", "tomlplusplus") add_license("../backends/ui/imgui/IconFontCppHeaders/licence.txt", "icnfntcpphdrs") add_css("gtk-frontend.css", "gtk_frontend_css") +add_dbus('app.dbus.xml', 'dbus_stub') +add_dbus('mpris.dbus.xml', 'mpris_stub') def finalize(output: str, ASSETS = ASSETS): print("Writing a header including all previous asset headers and writing it to '%s'..." % output) with open(output, "wt+") as f: diff --git a/backend.cpp b/backend.cpp index f042d84..3848f69 100644 --- a/backend.cpp +++ b/backend.cpp @@ -8,57 +8,87 @@ std::map UIBackend::backends; std::string UIBackend::get_id() { return ""; } -int UIBackend::run(std::vector realArgs, int argc, char **argv) { +UIBackend *UIBackend::running_ui_backend = nullptr; +void UIBackend::init_playback() { + if (multi_instance) { + playback = new PlaybackInstance(); + } else { + playback = Playback::Create(&daemon_found); + if (playback == nullptr) { + throw 1; + } + } +} +void UIBackend::init_libportal() { #ifdef GLIB_ENABLED - g_set_application_name("Looper") + g_set_application_name("Looper"); #endif +} +void UIBackend::setup_playback_args() { + if (speed_set) { + playback->SetSpeed(new_speed); + } + if (tempo_set) { + playback->SetTempo(new_tempo); + } + if (pitch_set) { + playback->SetPitch(new_pitch); + } + if (args.size() > 0) { + playback->Start(args[0]); + } + if (!daemon_found && playback->is_proxy()) { + throw 0; + } +} +bool UIBackend::parse_args(std::vector realArgs, int argc, char **argv) { args = realArgs; - playback = new PlaybackInstance(); CLI::App app{DESCRIPTION}; std::string filename = ""; app.allow_extras(); - double new_speed = 1.0; - double new_tempo = 1.0; - double new_pitch = 1.0; auto speed_opt = app.add_option("-s,--speed", new_speed, "Set the initial speed of the playback.")->default_val(1.0); auto tempo_opt = app.add_option("-t,--tempo", new_tempo, "Set the initial tempo of the playback.")->default_val(1.0); auto pitch_opt = app.add_option("-p,--pitch", new_pitch, "Set the initial pitch of the playback.")->default_val(1.0); - bool multi_instance = false; - auto multi_instance_opt = app.add_flag("-m,--multi-instance", multi_instance, "Disables the DBus api to allow multiple instances."); + std::vector options; + options.push_back(speed_opt); + options.push_back(tempo_opt); + options.push_back(pitch_opt); + if (allow_multi_instance()) { + auto multi_instance_opt = app.add_flag("-m,--multi-instance", multi_instance, "Disables the DBus api to allow multiple instances."); + options.push_back(multi_instance_opt); + } try { app.parse(args); } catch (const CLI::ParseError &e) { throw app.exit(e); } - playback->SetSpeed(new_speed); - playback->SetTempo(new_tempo); - playback->SetPitch(new_pitch); args = app.remaining(); - if (!multi_instance) { - DBusAPISender sender; - if (!sender.isOnlyInstance()) { - if (args.size() > 0) { - sender.playFile(args[0]); - } - if (!speed_opt->empty()) { - sender.setSpeed(new_speed); - } - if (!tempo_opt->empty()) { - sender.setTempo(new_tempo); - } - if (!pitch_opt->empty()) { - sender.setPitch(new_pitch); - } - throw 0; - } else { - dbus_api = new DBusAPI(playback); - } - } else { - dbus_api = nullptr; - } + + speed_set = !speed_opt->empty(); + tempo_set = !tempo_opt->empty(); + pitch_set = !pitch_opt->empty(); if (args.size() > 0) { - playback->Start(args[0]); + return true; + } else { + for (auto opt : options) { + if (!opt->empty()) { + return true; + } + } } + return false; +} +void UIBackend::init_dbus() { + if (!multi_instance && !playback->is_proxy()) { + dbus_api = DBusAPI::Create(playback); + } +} +int UIBackend::run(std::vector realArgs, int argc, char **argv) { + init_libportal(); + parse_args(realArgs, argc, argv); + init_playback(); + setup_playback_args(); + init_dbus(); return 0; } void UIBackend::deinit_backends() { diff --git a/backend.hpp b/backend.hpp index d37e977..b992d68 100644 --- a/backend.hpp +++ b/backend.hpp @@ -3,28 +3,53 @@ #include #include #include +#include #include "playback.h" #include "dbus.hpp" class UIBackend { protected: std::vector args; Playback *playback; + std::atomic_bool exit_flag = false; + double new_speed = 1.0; + double new_tempo = 1.0; + double new_pitch = 1.0; + bool speed_set = false; + bool tempo_set = false; + bool pitch_set = false; + bool multi_instance = false; + bool daemon_found = false; public: DBusAPI *dbus_api; + inline virtual bool allow_multi_instance() { + return true; + } virtual std::string get_id(); virtual std::string get_name(); UIBackend() = default; - virtual void add_licenses() { + /// @brief A hook to add any licenses of software packages used by the UI backend. + inline virtual void add_licenses() { // Don't add any here, but leave this specified. That way, licenses specific to UI frontends are only added per UI frontend. } + void init_libportal(); + void init_playback(); + void setup_playback_args(); + void init_dbus(); + bool parse_args(std::vector realArgs, int argc, char **argv); + /// @brief The main loop of the UI. Be sure to call @ref UIBackend::run and be prepared for it to throw an integer exit code that needs to escape your implementation. virtual int run(std::vector realArgs, int argc, char **argv); + /// @brief A hook that is called when the D-Bus API receives a request to exit the program. + inline virtual void QuitHandler() { + // Set a flag for loops managed by the UI backend. + exit_flag.store(true); + } static std::map backends; - static inline std::optional get_backend(std::string id) { - if (backends.contains(id)) { - return backends[id]; - } else { - return {}; - } + template + static inline std::string get_backend_id() { + UIBackend *backend = new T(); + auto output = backend->get_id(); + delete backend; + return output; } static inline UIBackend* get_first_backend() { if (backends.empty()) { @@ -32,9 +57,7 @@ class UIBackend { } return (*backends.begin()).second; } - template - static void register_backend() { - UIBackend *backend = new T(); + static void register_backend(UIBackend *backend) { std::string backend_id = backend->get_id(); if (backends.contains(backend_id)) { // Guard against potential memory leak due to reassigning a new pointer without deallocating the previous one delete backend; @@ -42,7 +65,47 @@ class UIBackend { } backends[backend_id] = backend; } + template + static void register_backend() { + UIBackend *backend = new T(); + backend->register_self(); + } + static inline void unregister_backend(std::string id) { + if (backends.contains(id)) { + auto backend = backends[id]; + delete backend; + backends.erase(id); + } + } + inline void unregister_self() { + UIBackend::unregister_backend(get_id()); + } + inline void register_self() { + UIBackend::register_backend(this); + } + template + static void unregister_backend() { + unregister_backend(get_backend_id()); + } static void deinit_backends(); + static inline std::optional get_backend(std::string id) { + if (backends.contains(id)) { + return backends[id]; + } else { + return {}; + } + } + template + static inline std::optional get_backend() { + auto id = get_backend_id(); + auto output = get_backend(id); + if (output.has_value()) { + return (T*)output.value(); + } else { + return {}; + } + } + static UIBackend *running_ui_backend; virtual ~UIBackend(); }; void init_backends(); \ No newline at end of file diff --git a/backends/ui/gtk/main.cpp b/backends/ui/gtk/main.cpp index 3dc8202..acbe44c 100644 --- a/backends/ui/gtk/main.cpp +++ b/backends/ui/gtk/main.cpp @@ -14,7 +14,7 @@ int GtkBackend::run(std::vector realArgs, int argc, char **argv) { return ret; } Glib::set_application_name("Looper"); - auto app = Gtk::Application::create("com.experimentalcraft.Looper.GTK", Gio::Application::Flags::NON_UNIQUE); + auto app = Gtk::Application::create("com.complecwaft.Looper.GTK", Gio::Application::Flags::NON_UNIQUE); char *gtk_frontend_css = (char*)malloc(gtk_frontend_css_size + 1); memcpy(gtk_frontend_css, gtk_frontend_css_data, gtk_frontend_css_size); gtk_frontend_css[gtk_frontend_css_size] = '\0'; @@ -32,10 +32,16 @@ int GtkBackend::run(std::vector realArgs, int argc, char **argv) { auto *win = new MainWindow(playback, app); win->present(); }); + app_ptr = app.get(); ret = app->run(argc, argv); - + app_ptr = nullptr; return ret; } +void GtkBackend::QuitHandler() { + if (app_ptr != nullptr) { + ((Gtk::Application*)app_ptr)->quit(); + } +} void GtkBackend::add_licenses() { auto &license_data = get_license_data(); auto gtkmm = LicenseData("GtkMM", "lgpl-2.1"); diff --git a/backends/ui/gtk/main.h b/backends/ui/gtk/main.h index 9495476..de5f09e 100644 --- a/backends/ui/gtk/main.h +++ b/backends/ui/gtk/main.h @@ -4,9 +4,11 @@ #include #define GTK_FRONTEND class GtkBackend : public UIBackend { + void *app_ptr; public: std::string get_id() override; std::string get_name() override; void add_licenses() override; + void QuitHandler() override; int run(std::vector realArgs, int argc, char **argv) override; }; \ No newline at end of file diff --git a/backends/ui/imgui/main.cpp b/backends/ui/imgui/main.cpp index 7b5895e..5bcd5b7 100644 --- a/backends/ui/imgui/main.cpp +++ b/backends/ui/imgui/main.cpp @@ -361,6 +361,9 @@ void MainLoop::GuiFunction() { // Make sure to not load the file unnecessarily. fileDialog.ClearSelected(); } + if (exit_flag.load()) { + done = true; + } } void MainLoop::LoadFile(std::string file) { playback->Start(file); @@ -408,6 +411,12 @@ std::string ImGuiUIBackend::get_id() { std::string ImGuiUIBackend::get_name() { return "Dear ImGui frontend"; } +void ImGuiUIBackend::QuitHandler() { + if (main_loop == nullptr) { + return; + } + ((MainLoop*)main_loop)->exit_flag.store(true); +} // Main code int ImGuiUIBackend::run(std::vector realArgs, int argc, char** argv) { @@ -418,6 +427,7 @@ int ImGuiUIBackend::run(std::vector realArgs, int argc, char** argv MainLoop loop; loop.playback = playback; loop.args = args; + main_loop = &loop; return loop.Run(); } void ImGuiUIBackend::add_licenses() { diff --git a/backends/ui/imgui/main.h b/backends/ui/imgui/main.h index a58ea6e..7773e3d 100644 --- a/backends/ui/imgui/main.h +++ b/backends/ui/imgui/main.h @@ -33,6 +33,7 @@ #include "../libs/emscripten/emscripten_mainloop_stub.h" #endif #include "../../../backend.hpp" +#include "ui_backend.hpp" using namespace std::filesystem; using namespace std::numbers; using std::string; @@ -50,6 +51,8 @@ class MainLoop : public RendererBackend { bool stopped = true; std::vector backends; UIBackend *cur_backend; + friend class ImGuiUIBackend; + std::atomic_bool exit_flag; public: Playback *playback; vector args; diff --git a/backends/ui/imgui/ui_backend.hpp b/backends/ui/imgui/ui_backend.hpp index 8beec42..72e73a6 100644 --- a/backends/ui/imgui/ui_backend.hpp +++ b/backends/ui/imgui/ui_backend.hpp @@ -6,6 +6,8 @@ class ImGuiUIBackend : public UIBackend { public: std::string get_id() override; std::string get_name() override; + void QuitHandler() override; + void *main_loop; int run(std::vector realArgs, int argc, char **argv) override; void add_licenses() override; ImGuiUIBackend() = default; diff --git a/daemon_backend.cpp b/daemon_backend.cpp new file mode 100644 index 0000000..0107894 --- /dev/null +++ b/daemon_backend.cpp @@ -0,0 +1,29 @@ +#include "daemon_backend.hpp" +#include "log.hpp" +#include +#include +using namespace std::literals::chrono_literals; +std::string DaemonGlueBackend::get_id() { + return "daemon"; +} +std::string DaemonGlueBackend::get_name() { + return "Daemon Backend"; +} +bool DaemonGlueBackend::allow_multi_instance() { + return false; +} +int DaemonGlueBackend::run(std::vector realArgs, int argc, char **argv) { + parse_args(realArgs, argc, argv); + playback = Playback::Create(nullptr, true); + if (playback == nullptr) { + ERROR.writeln("Cannot start daemon with existing instance running."); + return 1; + } + setup_playback_args(); + dbus_api = DBusAPI::Create(playback, true); + exit_flag.store(false); + while (!exit_flag.load()) { + std::this_thread::sleep_for(100ms); + } + return 0; +} \ No newline at end of file diff --git a/daemon_backend.hpp b/daemon_backend.hpp new file mode 100644 index 0000000..f54a2bb --- /dev/null +++ b/daemon_backend.hpp @@ -0,0 +1,9 @@ +#pragma once +#include "backend.hpp" +class DaemonGlueBackend : public UIBackend { + public: + bool allow_multi_instance() override; + std::string get_id() override; + std::string get_name() override; + int run(std::vector realArgs, int argc, char **argv) override; +}; \ No newline at end of file diff --git a/dbus.cpp b/dbus.cpp index f1960c1..843e4c4 100644 --- a/dbus.cpp +++ b/dbus.cpp @@ -1,173 +1,518 @@ #include "dbus.hpp" #include "log.hpp" -const char *DBusAPI::serviceName = "com.experimentalcraft.looper"; -const char *DBusAPI::objectPath = "/com/experimentalcraft/looper"; -const char *DBusAPI::interfaceName = "com.experimentalcraft.Looper"; -DBusAPI::DBusAPI(Playback *playback) { - this->playback = playback; - connection = sdbus::createSessionBusConnection(serviceName); - api = sdbus::createObject(*connection, objectPath); - api->registerMethod(interfaceName, "open", "s", "", [this](sdbus::MethodCall call) { - std::string filePath; - call >> filePath; - this->playback->Start(filePath); - }); - api->registerSignal(interfaceName, "playbackEngineStarted", ""); - api->registerSignal(interfaceName, "speedChanged", "d"); - api->registerSignal(interfaceName, "tempoChanged", "d"); - api->registerSignal(interfaceName, "pitchChanged", "d"); - api->registerSignal(interfaceName, "pauseChanged", "b"); - api->registerSignal(interfaceName, "stopped", ""); - api->registerSignal(interfaceName, "errorOccurred", "s"); - api->registerSignal(interfaceName, "seeked", "d"); - api->registerSignal(interfaceName, "fileChanged", "s"); - api->registerProperty(interfaceName, "speed", "d", [this](sdbus::PropertyGetReply reply) { - reply << this->playback->GetSpeed(); - }, [this](sdbus::PropertySetCall set_call) { - double newValue; - set_call >> newValue; - this->playback->SetSpeed(newValue); - }); - api->registerProperty(interfaceName, "tempo", "d", [this](sdbus::PropertyGetReply reply) { - reply << this->playback->GetTempo(); - }, [this](sdbus::PropertySetCall set_call) { - double newValue; - set_call >> newValue; - this->playback->SetTempo(newValue); - }); - api->registerProperty(interfaceName, "pitch", "d", [this](sdbus::PropertyGetReply reply) { - reply << this->playback->GetPitch(); - }, [this](sdbus::PropertySetCall set_call) { - double newValue; - set_call >> newValue; - this->playback->SetPitch(newValue); - }); - api->registerProperty(interfaceName, "volume", "d", [this](sdbus::PropertyGetReply reply) { - reply << this->playback->GetVolume(); - }, [this](sdbus::PropertySetCall set_call) { - double newValue; - set_call >> newValue; - this->playback->SetVolume(newValue); - }); - api->registerMethod(interfaceName, "stop", "", "", [this](sdbus::MethodCall call) { - this->playback->Stop(); - }); - api->registerMethod(interfaceName, "togglePause", "", "", [this](sdbus::MethodCall call) { - this->playback->Pause(); - }); - api->registerProperty(interfaceName, "paused", "b", [this](sdbus::PropertyGetReply reply) { - reply << this->playback->IsPaused(); - }, [this](sdbus::PropertySetCall call) { - bool shouldBePaused; - call >> shouldBePaused; - this->playback->SetPaused(shouldBePaused); - }); - api->registerProperty(interfaceName, "stopped", "b", [this](sdbus::PropertyGetReply reply) { - reply << this->playback->IsStopped(); - }); - api->registerMethod(interfaceName, "errorExists", "", "b", [this](sdbus::MethodCall call) { - auto reply = call.createReply(); - reply << this->playback->ErrorExists(); - }); - api->registerMethod(interfaceName, "getLastError", "", "s", [this](sdbus::MethodCall call) { - auto error = this->playback->GetError(); - if (error.has_value()) { - auto reply = call.createReply(); - reply << error.value(); - } else { - auto errorReply = call.createErrorReply(sdbus::createError(0, "No error found.")); +#include "backend.hpp" +#include +MprisAPI::MprisAPI(sdbus::IConnection &connection, std::string objectPath, DBusAPI *dbus_api) + : AdaptorInterfaces(connection, std::move(objectPath)) + , dbus_api(dbus_api) + , connection(connection) +{ + registerAdaptor(); + connection.enterEventLoopAsync(); +} +void MprisAPI::Quit() { + dbus_api->Quit(); +} +void MprisAPI::Pause() { + dbus_api->Paused(true); +} +void MprisAPI::PlayPause() { + dbus_api->TogglePause(); +} +void MprisAPI::Stop() { + dbus_api->Stop(); +} +void MprisAPI::Play() { + dbus_api->Paused(false); +} +void MprisAPI::Seek(const int64_t &offset) { + double value = offset; + value /= 1000000; + dbus_api->Position(value); +} +void MprisAPI::SetPosition(const sdbus::ObjectPath &TrackId, const int64_t &offset) { + Seek(offset); +} +void MprisAPI::OpenUri(const std::string &Uri) { + dbus_api->StartByURI(Uri); +} +std::string MprisAPI::PlaybackStatus() { + if (dbus_api->IsStopped()) { + return "Stopped"; + } else { + return dbus_api->Paused() ? "Paused" : "Playing"; + } +} +double MprisAPI::Rate() { + return dbus_api->Speed() * dbus_api->Tempo(); +} +void MprisAPI::Rate(const double &value) { + dbus_api->Speed(value / dbus_api->Tempo()); +} +std::map MprisAPI::Metadata() { + std::map output; + if (!dbus_api->IsStopped()) { + output["mpris:length"] = (int64_t)(dbus_api->Length() * 1000000); + output["mpris:trackid"] = playing_track_id; + output["xesam:title"] = dbus_api->FileTitle(); + output["xesam:url"] = dbus_api->FilePath(); + } else { + output["mpris:trackid"] = empty_track_id; + } + return output; +} +double MprisAPI::Volume() { + return dbus_api->Volume(); +} +void MprisAPI::Volume(const double &value) { + dbus_api->Volume(value); +} +int64_t MprisAPI::Position() { + return dbus_api->Position() * 1000000; +} +bool MprisAPI::CanPlay() { + return !dbus_api->IsStopped(); +} +bool MprisAPI::CanPause() { + return !dbus_api->IsStopped(); +} +bool MprisAPI::CanSeek() { + return !dbus_api->IsStopped() && dbus_api->Length() > 0; +} + +std::vector MprisAPI::GetTracksMetadata(const std::vector &TrackIds) { + std::vector output; + if (!dbus_api->IsStopped()) { + output.push_back(Metadata()); + } + return output; +} +void MprisAPI::AddTrack(const std::string &Uri, const track_id_t &AfterTrack, const bool &SetAsCurrent) { + return; +} +void MprisAPI::RemoveTrack(const track_id_t &TrackId) { + return; +} +void MprisAPI::GoTo(const track_id_t &TrackId) { + return; +} +std::vector MprisAPI::Tracks() { + std::vector output; + if (dbus_api->IsStopped()) { + return output; + } + output.push_back(playing_track_id); + return output; +} +bool MprisAPI::CanEditTracks() { + return false; +} +MprisAPI::~MprisAPI() { + unregisterAdaptor(); +} +DBusAPI::DBusAPI(Playback *playback, sdbus::IConnection &connection, std::string objectPath, bool daemon) + : AdaptorInterfaces(connection, std::move(objectPath)) + , daemon(daemon) + , playback(playback) + , connection(connection) +{ + registerAdaptor(); + playback->register_handle(this); + auto mprisConnection = sdbus::createSessionBusConnection("org.mpris.MediaPlayer2.Looper"); + auto &mprisConRef = *mprisConnection.release(); + mpris = new MprisAPI(mprisConRef, "/org/mpris/MediaPlayer2", this); + threadFunc = std::thread([this]() { + while (!threadExitFlag.load()) { + Update(); + std::this_thread::yield(); } }); - api->registerProperty(interfaceName, "position", "d", [this](sdbus::PropertyGetReply reply) { - reply << this->playback->GetPosition(); - }, [this](sdbus::PropertySetCall call) { - double value; - call >> value; - this->playback->Seek(value); - }); - api->registerProperty(interfaceName, "length", "d", [this](sdbus::PropertyGetReply reply) { - reply << this->playback->GetLength(); - }); - api->registerProperty(interfaceName, "current_file_path", "s", [this](sdbus::PropertyGetReply reply) { - auto file_maybe = this->playback->get_current_file(); - if (file_maybe.has_value()) { - reply << file_maybe.value(); - } else { - reply << ""; + connection.enterEventLoopAsync(); +} +const char *DBusAPI::objectPath = "/com/complecwaft/Looper"; +const char *DBusAPI::busName = "com.complecwaft.Looper"; +DBusAPI *DBusAPI::Create(Playback *playback, bool daemon) { + auto connection = sdbus::createSessionBusConnection(busName); + auto &con_ref = *connection.release(); + return new DBusAPI(playback, con_ref, objectPath, daemon); +} +double DBusAPI::Position() { + return playback->GetPosition(); +} +void DBusAPI::Position(const double &value) { + playback->Seek(value); +} +double DBusAPI::Length() { + return playback->GetLength(); +} +std::string DBusAPI::FilePath() { + return playback->get_current_file().value_or(""); +} +std::string DBusAPI::FileTitle() { + return playback->get_current_title().value_or(""); +} +void DBusAPI::Update() { + uint16_t signals = playback->handle_signals(PlaybackSignalErrorOccurred|PlaybackSignalFileChanged|PlaybackSignalPaused|PlaybackSignalPitchChanged|PlaybackSignalResumed|PlaybackSignalSeeked|PlaybackSignalSpeedChanged|PlaybackSignalStarted|PlaybackSignalStopped|PlaybackSignalTempoChanged, this); + if (signals & PlaybackSignalErrorOccurred) { + auto error = playback->GetError(this).value_or("Unknown error."); + emitErrorOccurred(error, error); + mpris->sendPropertiesChanged(mpris->playerInterface, {"PlaybackStatus", "CanSeek", "CanPlay", "CanPause", "Metadata"}); + } + if (signals & PlaybackSignalFileChanged) { + emitFileChanged(playback->get_current_file().value_or(""), playback->get_current_title().value_or("")); + mpris->emitTrackMetadataChanged(mpris->playing_track_id, mpris->Metadata()); + mpris->sendPropertiesChanged(mpris->playerInterface, {"Position", "Metadata"}); + } + if (signals & (PlaybackSignalPaused|PlaybackSignalResumed)) { + emitPauseChanged(playback->IsPaused()); + mpris->sendPropertiesChanged(mpris->playerInterface, {"PlaybackStatus"}); + } + if (signals & PlaybackSignalStopped) { + emitStopped(); + emitPauseChanged(playback->IsPaused()); + mpris->emitTrackRemoved(mpris->playing_track_id); + mpris->sendPropertiesChanged(mpris->playerInterface, {"PlaybackStatus", "CanSeek", "CanPlay", "CanPause", "Metadata"}); + } + if (signals & PlaybackSignalPitchChanged) { + emitPitchChanged(playback->GetPitch()); + } + if (signals & PlaybackSignalTempoChanged) { + emitTempoChanged(playback->GetTempo()); + mpris->sendPropertiesChanged(mpris->playerInterface, {"Rate"}); + } + if (signals & PlaybackSignalSpeedChanged) { + emitSpeedChanged(playback->GetSpeed()); + mpris->sendPropertiesChanged(mpris->playerInterface, {"Rate"}); + } + if (signals & PlaybackSignalSeeked) { + emitSeeked(playback->GetPosition()); + mpris->emitSeeked(playback->GetPosition() * 1000000); + mpris->sendPropertiesChanged(mpris->playerInterface, {"Position"}); + } + if (signals & PlaybackSignalStarted) { + emitPlaybackEngineStarted(); + mpris->emitTrackAdded(mpris->Metadata(), mpris->empty_track_id); + mpris->sendPropertiesChanged(mpris->playerInterface, {"CanSeek", "CanPlay", "CanPause", "Position", "Metadata", "PlaybackStatus"}); + } +} +void DBusAPI::Activate(const std::map &platform_data) { + // TODO: Raise window? +} +void DBusAPI::Open(const std::vector& uris, const std::map &platform_data) { + if (uris.size() > 0) { + StartByURI(uris[0]); + } +} +void DBusAPI::ActivateAction(const std::string &action_name, const std::vector& parameter, const std::map &platform_data) { + // Nothing yet. +} +void DBusAPI::Start(const std::string &path) { + playback->Start(path); +} +void DBusAPI::StartByURI(const std::string &uri) { + const static std::string file_prefix = "file:"; + std::string tmp = uri; + if (tmp.starts_with(file_prefix)) { + tmp = tmp.substr(file_prefix.length()); + Start(tmp); + } +} +void DBusAPI::Stop() { + playback->Stop(); +} +void DBusAPI::TogglePause() { + playback->Pause(); +} +double DBusAPI::Speed() { + return playback->GetSpeed(); +} +void DBusAPI::Speed(const double &value) { + playback->SetSpeed(value); +} +double DBusAPI::Tempo() { + return playback->GetTempo(); +} +void DBusAPI::Tempo(const double &value) { + playback->SetTempo(value); +} +double DBusAPI::Pitch() { + return playback->GetPitch(); +} +void DBusAPI::Pitch(const double &value) { + playback->SetPitch(value); +} +double DBusAPI::Volume() { + return playback->GetVolume(); +} +void DBusAPI::Volume(const double &value) { + playback->SetVolume(value); +} +bool DBusAPI::Paused() { + return playback->IsPaused(); +} +void DBusAPI::Paused(const bool &value) { + if (value != playback->IsPaused()) { + playback->Pause(); + } +} +bool DBusAPI::IsStopped() { + return playback->IsStopped(); +} +bool DBusAPI::IsDaemon() { + return daemon; +} +std::string DBusAPI::CreateHandle() { + size_t idx = handle_idx++; + size_t hash = std::hash()(idx); + size_t rand = (size_t)rand_engine(); + size_t rand_hash = std::hash()(rand); + std::string value = std::format("{}:{}:{}", idx, hash, rand_hash); + handles[value] = (void*)&value; + playback->register_handle(handles[value]); + return value; +} +void DBusAPI::ClearHandle(const std::string &handle) { + if (handles.contains(handle)) { + playback->unregister_handle(handles[handle]); + handles.erase(handle); + } +} +std::deque *DBusAPI::get_errors_by_handle(const std::string &handle) { + if (handles.contains(handle)) { + auto *v_handle = handles[handle]; + if (playback->errors_occurred.contains(v_handle)) { + return &playback->errors_occurred[v_handle]; } - }); - api->registerProperty(interfaceName, "current_file_title", "s", [this](sdbus::PropertyGetReply reply) { - auto title_maybe = this->playback->get_current_title(); - if (title_maybe.has_value()) { - reply << title_maybe.value(); - } else { - reply << ""; + } + return nullptr; +} +std::string DBusAPI::PopFront(const std::string &handle) { + auto *errors_list = get_errors_by_handle(handle); + if (errors_list != nullptr) { + if (errors_list->empty()) { + return ""; } - }); - api->finishRegistration(); - connection->enterEventLoopAsync(); + std::string output = errors_list->front(); + errors_list->pop_front(); + return output; + } + return ""; +} +std::string DBusAPI::PopBack(const std::string &handle) { + auto *errors_list = get_errors_by_handle(handle); + if (errors_list != nullptr) { + if (errors_list->empty()) { + return ""; + } + std::string output = errors_list->back(); + errors_list->pop_back(); + return output; + } + return ""; +} +std::string DBusAPI::PeekFront(const std::string &handle) { + auto *errors_list = get_errors_by_handle(handle); + if (errors_list != nullptr) { + if (errors_list->empty()) { + return ""; + } + std::string output = errors_list->front(); + return output; + } + return ""; +} +void DBusAPI::Quit() { + UIBackend::running_ui_backend->QuitHandler(); +} +std::string DBusAPI::PeekBack(const std::string &handle) { + auto *errors_list = get_errors_by_handle(handle); + if (errors_list != nullptr) { + if (errors_list->empty()) { + return ""; + } + std::string output = errors_list->back(); + return output; + } + return ""; +} +uint32_t DBusAPI::GetCount(const std::string &handle) { + auto *errors_list = get_errors_by_handle(handle); + if (errors_list != nullptr) { + return errors_list->size(); + } + return 0; +} +bool DBusAPI::IsEmpty(const std::string& handle) { + return GetCount(handle) == 0; +} +void DBusAPI::Clear(const std::string &handle) { + auto *errors_list = get_errors_by_handle(handle); + if (errors_list != nullptr) { + errors_list->clear(); + } +} +std::vector DBusAPI::PeekAll(const std::string &handle) { + auto *errors_list = get_errors_by_handle(handle); + if (errors_list != nullptr) { + std::vector output; + for (auto error : (*errors_list)) { + output.push_back(error); + } + return output; + } + return std::vector(); +} +std::vector DBusAPI::GetAllAndClear(const std::string &handle) { + auto output = PeekAll(handle); + Clear(handle); + return output; +} +DBusAPI::~DBusAPI() { + threadExitFlag.store(true); + threadFunc.join(); + unregisterAdaptor(); } bool DBusAPISender::isOnlyInstance() { - return !connected; -} -void DBusAPISender::playFile(std::string file) { - if (connected) { - auto method = proxy->createMethodCall(DBusAPI::interfaceName, "open"); - method.dontExpectReply(); - method << file; - proxy->callMethod(method); - } -} -void DBusAPISender::setPitch(double value) { - if (connected) { - proxy->setProperty("pitch").onInterface(DBusAPI::interfaceName).toValue(value); - } -} -void DBusAPISender::setSpeed(double value) { - if (connected) { - proxy->setProperty("speed").onInterface(DBusAPI::interfaceName).toValue(value); - } -} -void DBusAPISender::setTempo(double value) { - if (connected) { - proxy->setProperty("tempo").onInterface(DBusAPI::interfaceName).toValue(value); - } -} -double DBusAPISender::getPitch() { - if (connected) { - return proxy->getProperty("pitch").onInterface(DBusAPI::interfaceName); - } else { - return -1.0; - } -} -double DBusAPISender::getTempo() { - if (connected) { - return proxy->getProperty("tempo").onInterface(DBusAPI::interfaceName); - } else { - return -1.0; - } -} -double DBusAPISender::getSpeed() { - if (connected) { - return proxy->getProperty("speed").onInterface(DBusAPI::interfaceName); - } else { - return -1.0; - } -} -DBusAPISender::DBusAPISender() { - DEBUG.writeln("Checking and connecting to main instance..."); + bool output; try { - connection = sdbus::createSessionBusConnection(); - proxy = sdbus::createProxy(*connection, DBusAPI::serviceName, DBusAPI::objectPath); - - proxy->finishRegistration(); - { - auto method = proxy->createMethodCall("org.freedesktop.DBus.Peer", "Ping"); - proxy->callMethod(method); - } - DEBUG.writeln("Main instance found."); + auto *tmp = Create(); + output = tmp == nullptr; + delete tmp; + return output; } catch (sdbus::Error) { - connected = false; - DEBUG.writeln("Main instance not found."); + return true; } +} + +std::optional DBusAPISender::get_current_file() { + if (IsStopped()) { + return {}; + } else { + return FilePath(); + } +} +std::optional DBusAPISender::get_current_title() { + if (IsStopped()) { + return {}; + } else { + return FileTitle(); + } +} +void DBusAPISender::onPlaybackEngineStarted() { + set_signal(PlaybackSignalStarted); +} +void DBusAPISender::onFileChanged(const std::string &FilePath, const std::string &Title) { + set_signal(PlaybackSignalFileChanged); +} +void DBusAPISender::onSpeedChanged(const double &new_speed) { + set_signal(PlaybackSignalSpeedChanged); +} +void DBusAPISender::onTempoChanged(const double &new_tempo) { + set_signal(PlaybackSignalTempoChanged); +} +void DBusAPISender::onPitchChanged(const double &new_pitch) { + set_signal(PlaybackSignalPitchChanged); +} +void DBusAPISender::onPauseChanged(const bool &now_paused) { + set_signal(now_paused ? PlaybackSignalPaused : PlaybackSignalResumed); +} +void DBusAPISender::onStopped() { + set_signal(PlaybackSignalStopped); +} +void DBusAPISender::onErrorOccurred(const std::string &error_desc, const std::string &error_type) { + set_signal(PlaybackSignalErrorOccurred); +} +void DBusAPISender::onSeeked(const double &to_position) { + set_signal(PlaybackSignalSeeked); +} +double DBusAPISender::GetPosition() { + return ProxyInterfaces::Position(); +} +double DBusAPISender::GetLength() { + return ProxyInterfaces::Length(); +} +void DBusAPISender::Seek(double position) { + ProxyInterfaces::Position(position); +} +void DBusAPISender::Start(std::string filePath) { + ProxyInterfaces::Start(filePath); +} +bool DBusAPISender::IsPaused() { + return ProxyInterfaces::Paused(); +} +void DBusAPISender::Pause() { + ProxyInterfaces::TogglePause(); +} +void DBusAPISender::Stop() { + ProxyInterfaces::Stop(); +} +bool DBusAPISender::IsStopped() { + return ProxyInterfaces::IsStopped(); +} +void DBusAPISender::SetTempo(float tempo) { + ProxyInterfaces::Tempo(tempo); +} +void DBusAPISender::SetPitch(float pitch) { + ProxyInterfaces::Pitch(pitch); +} +void DBusAPISender::SetSpeed(float speed) { + ProxyInterfaces::Speed(speed); +} +void DBusAPISender::SetVolume(float volume) { + ProxyInterfaces::Volume(volume); +} +float DBusAPISender::GetTempo() { + return (float)ProxyInterfaces::Tempo(); +} +float DBusAPISender::GetPitch() { + return (float)ProxyInterfaces::Pitch(); +} +float DBusAPISender::GetSpeed() { + return (float)ProxyInterfaces::Speed(); +} +float DBusAPISender::GetVolume() { + return (float)ProxyInterfaces::Volume(); +} +void DBusAPISender::Update() { + +} +DBusAPISender *DBusAPISender::Create() { + try { + auto connection = sdbus::createSessionBusConnection(); + auto &con_ref = *connection.release(); + auto *output = new DBusAPISender(con_ref, DBusAPI::busName, DBusAPI::objectPath); + return output; + } catch (sdbus::Error error) { + DEBUG.writefln("sdbus::Error: %s: %s", error.getName().c_str(), error.getMessage().c_str()); + return nullptr; + } +} +DBusAPISender::DBusAPISender(sdbus::IConnection &connection, std::string busName, std::string objectPath) + : ProxyInterfaces(connection, std::move(busName), std::move(objectPath)) { + registerProxy(); + DEBUG.writeln("Pinging DBus API to check for its existance."); + Ping(); + DEBUG.writeln("Requesting handle..."); + handle = CreateHandle(); + DEBUG.writeln("Preparing updates for UI frontend..."); + if (IsStopped()) { + set_signal(PlaybackSignalStopped); + } else { + filePath = ProxyInterfaces::FilePath(); + title = ProxyInterfaces::FileTitle(); + length = ProxyInterfaces::Length(); + set_signal(PlaybackSignalStarted|PlaybackSignalSeeked|PlaybackSignalFileChanged); + } + if (IsPaused()) { + set_signal(PlaybackSignalPaused); + } else { + set_signal(PlaybackSignalResumed); + } + set_signal(PlaybackSignalSpeedChanged|PlaybackSignalPitchChanged|PlaybackSignalTempoChanged); + connection.enterEventLoopAsync(); +} +DBusAPISender::~DBusAPISender() { + DEBUG.writeln("Requesting handle deletion..."); + ClearHandle(handle); + unregisterProxy(); } \ No newline at end of file diff --git a/dbus.hpp b/dbus.hpp index b12f7ad..8bbba39 100644 --- a/dbus.hpp +++ b/dbus.hpp @@ -1,31 +1,231 @@ #pragma once #include +#include +#include "assets/dbus_stub_adaptor.hpp" +#include "assets/dbus_stub_proxy.hpp" +#include "assets/mpris_stub_adaptor.hpp" #include "playback.h" -class DBusAPI { - friend class DBusAPISender; - static const char *serviceName; - static const char *objectPath; - static const char *interfaceName; - std::unique_ptr connection; - std::unique_ptr api; +#include +#include +#include +#include +class DBusAPI; +class MprisAPI : public sdbus::AdaptorInterfaces { + friend class DBusAPI; + DBusAPI *dbus_api; + std::string curPlayingObjectPath; + sdbus::IConnection &connection; + const std::string mainInterface = "org.mpris.MediaPlayer2"; + const std::string playerInterface = "org.mpris.MediaPlayer2.Player"; + const std::string trackInterface = "org.mpris.MediaPlayer2.TrackList"; + inline void sendPropertiesChanged(const std::string interface, const std::initializer_list properties) { + std::vector property_vec; + for (auto property : properties) { + property_vec.push_back(property); + } + emitPropertiesChangedSignal(interface, property_vec); + } public: - Playback *playback; - DBusAPI(Playback *playback); +#define meta_t std::map +#define track_id_t sdbus::ObjectPath + const sdbus::ObjectPath playing_track_id = "/com/complecwaft/Looper/PlayingTrack"; + const sdbus::ObjectPath empty_track_id = "/org/mpris/MediaPlayer2/TrackList/NoTrack"; + + inline void Raise() override { } + void Quit() override; + inline std::string Identity() override { + return "Looper"; + } + inline std::vector SupportedUriSchemes() override { + std::vector output; + output.push_back("file:"); + return output; + } + inline std::vector SupportedMimeTypes() override { + std::vector output; + output.push_back("audio/*"); + return output; + } + inline void Next() override { } + inline void Previous() override { } + void Pause() override; + void PlayPause() override; + void Stop() override; + void Play() override; + void Seek(const int64_t &offset) override; + void SetPosition(const track_id_t &TrackId, const int64_t &Position) override; + void OpenUri(const std::string &Uri) override; + std::string PlaybackStatus() override; + double Rate() override; + void Rate(const double &value) override; + meta_t Metadata() override; + double Volume() override; + void Volume(const double& value) override; + int64_t Position() override; + inline double MinimumRate() override { + return 0.25; + } + inline double MaximumRate() override { + return 4.0; + } + inline bool CanGoNext() override { + return false; + } + inline bool CanGoPrevious() override { + return false; + } + bool CanPlay() override; + bool CanPause() override; + bool CanSeek() override; + inline bool CanControl() override { + return true; + } + inline bool CanRaise() override { + return false; + } + inline bool CanQuit() override { + return true; + } + inline bool HasTrackList() override { + return true; + } + std::vector GetTracksMetadata(const std::vector &TrackIds) override; + void AddTrack(const std::string &Uri, const track_id_t &AfterTrack, const bool &SetAsCurrent) override; + void RemoveTrack(const track_id_t &TrackId) override; + void GoTo(const track_id_t &TrackId) override; + std::vector Tracks() override; + bool CanEditTracks() override; + MprisAPI(sdbus::IConnection &connection, std::string objectPath, DBusAPI *dbus_api); + ~MprisAPI(); }; -class DBusAPISender { - std::unique_ptr proxy; - std::unique_ptr connection; - bool connected = true; +class DBusAPI : public sdbus::AdaptorInterfaces { + std::map handles; + size_t handle_idx = 0; + MprisAPI *mpris; + std::minstd_rand rand_engine; + std::deque *get_errors_by_handle(const std::string &handle); + bool daemon; + std::atomic_bool threadExitFlag = false; + std::thread threadFunc; + sdbus::IConnection &connection; public: - void playFile(std::string file); - void setPitch(double pitch); - void setTempo(double tempo); - void setSpeed(double speed); - void setVolume(double volume); - double getPitch(); - double getTempo(); - double getSpeed(); - double getVolume(); - bool isOnlyInstance(); - DBusAPISender(); + static const char *objectPath; + static const char *busName; + void Activate(const std::map& platform_data) override; + void Open(const std::vector& uris, const std::map& platform_data) override; + void ActivateAction(const std::string& action_name, const std::vector& parameter, const std::map& platform_data) override; + // com.experimentalcraft.Looper + // Properties + std::string FilePath() override; + std::string FileTitle() override; + double Speed() override; + void Speed(const double &value) override; + double Tempo() override; + void Tempo(const double& value) override; + double Pitch() override; + void Pitch(const double& value) override; + double Volume() override; + void Volume(const double& value) override; + double Position() override; + void Position(const double &value) override; + double Length() override; + // Pausing and Resuming + bool Paused() override; + void Paused(const bool& value) override; + void TogglePause() override; + // Starting new Files + void Start(const std::string& path) override; + void StartByURI(const std::string& uri) override; + // Playback stopping + void Stop() override; + bool IsStopped() override; + // Handles + std::string CreateHandle() override; + void ClearHandle(const std::string& handle) override; + // com.experimentalcraft.Looper.Errors + // Popping from the queue + std::string PopFront(const std::string &handle) override; + std::string PopBack(const std::string &handle) override; + // Peeking at the values + std::string PeekFront(const std::string &handle) override; + std::string PeekBack(const std::string &handle) override; + // Getting the amount of unread errors. + uint32_t GetCount(const std::string &handle) override; + bool IsEmpty(const std::string& handle) override; + // Getting and Clearing Errors. + void Clear(const std::string &handle) override; + std::vector PeekAll(const std::string &handle) override; + std::vector GetAllAndClear(const std::string &handle) override; + bool IsDaemon() override; + void Quit() override; + + // API + Playback *playback; + void Update(); + DBusAPI(Playback *playback, sdbus::IConnection &connection, std::string objectPath, bool daemon); + ~DBusAPI(); + static DBusAPI *Create(Playback *playback, bool daemon = false); +}; +class DBusAPISender : public Playback, public sdbus::ProxyInterfaces { + // Cache + double length, pitch, speed, tempo, volume; + bool stopped, paused; + std::string filePath; + std::string title; + std::mutex cacheMutex; + optional last_error; + // Handle for error handling. + std::string handle; + + public: + // Public API for creating this object, and checking if it is needed. + /// @brief Checks if this is the only instance, by attempting creation and immediately deleting the created object. + /// @returns true, if this is the only instance, false otherwise. + static bool isOnlyInstance(); + /// @brief Creates a proxy playback engine, if possible. + /// @returns A proxy to the main instance of the playback engine, or nullptr if there is none. + static DBusAPISender *Create(); + + // Signals. Protected so that they aren't seen as a proper API + protected: + void onPlaybackEngineStarted() override; + void onSpeedChanged(const double &new_speed) override; + void onTempoChanged(const double &new_tempo) override; + void onPitchChanged(const double &new_pitch) override; + void onPauseChanged(const bool &now_paused) override; + void onStopped() override; + void onErrorOccurred(const std::string &error_desc, const std::string &error_type) override; + void onSeeked(const double &to_position) override; + void onFileChanged(const std::string &path, const std::string &title) override; + + // Playback API. This is the API to be used by UI frontends. + public: + std::optional get_current_file() override; + std::optional get_current_title() override; + inline bool is_proxy() override { + return true; + } + double GetPosition() override; + double GetLength() override; + void Seek(double position) override; + void Start(std::string filePath) override; + bool IsPaused() override; + void Pause() override; + void Stop() override; + void Update(); + bool IsStopped() override; + void SetTempo(float tempo) override; + void SetPitch(float pitch) override; + void SetSpeed(float speed) override; + void SetVolume(float volume) override; + float GetTempo() override; + float GetPitch() override; + float GetSpeed() override; + float GetVolume() override; + // Constructors and destructors. + // The constructor is protected because there is a different API that should be used for creation. + protected: + DBusAPISender(sdbus::IConnection &connection, std::string busName, std::string objectPath); + public: + ~DBusAPISender(); }; \ No newline at end of file diff --git a/main.cpp b/main.cpp index 36aee7b..bb54272 100644 --- a/main.cpp +++ b/main.cpp @@ -1,5 +1,7 @@ #include "options.hpp" #include "backend.hpp" +#include "daemon_backend.hpp" +#include "proxy_backend.hpp" #include "log.hpp" #include "thirdparty/CLI11.hpp" #include "data.h" @@ -23,8 +25,15 @@ int main(int argc, char **argv) { int log_level; std::string ui_backend_option = ""; bool full_help = false; + bool daemonize = false; + bool disable_gui = false; + bool quit = false; + bool open_window = false; app.add_option("-l, --log-level", LogStream::log_level, "Sets the minimum log level to display in the logs."); app.add_option("-u, --ui-backend", ui_backend_option, "Specifies which UI backend to use."); + app.add_flag("-d, --daemon", daemonize, "Daemonizes the program."); + app.add_flag("-n, --no-gui", disable_gui, "Don't open the GUI when there is a daemon and there are settings or commands for it. Ignored in daemon mode, or when no changes in state are issued."); + app.add_flag("-q, --quit", quit, "Quits an existing instance."); try { app.parse(args); } catch (const CLI::ParseError &e) { @@ -34,11 +43,25 @@ int main(int argc, char **argv) { exit(app.exit(e)); } } + if (daemonize) { + ui_backend_option = "daemon"; + } args.clear(); args = app.remaining(false); int new_argc = args.size(); char **new_argv = (char**)malloc(new_argc * sizeof(char*)); init_logging(); + + if (quit) { + DBusAPISender *sender = DBusAPISender::Create(); + if (sender != nullptr) { + sender->Quit(); + } else { + ERROR.writeln("Did not find existing instance to quit."); + return 1; + } + return 0; + } { auto looper_mit = LicenseData("Looper (MIT)", "MIT"); auto looper_gpl = LicenseData("Looper (GPL)", "GPL-3.0-or-later"); @@ -73,9 +96,26 @@ int main(int argc, char **argv) { } DEBUG.writeln("Initializing frontends..."); init_backends(); - DEBUG.writeln("Loaded frontends: "); + ProxyGlueBackend *proxy_backend = nullptr; + if ((disable_gui && !daemonize) || quit) { + if (!DBusAPISender::isOnlyInstance()) { + UIBackend::register_backend(); + if (disable_gui) { + proxy_backend = UIBackend::get_backend().value_or(nullptr); + } + } + if (quit && proxy_backend == nullptr) { + proxy_backend = UIBackend::get_backend().value_or(nullptr); + } + } + if (daemonize) { + UIBackend::register_backend(); + } for (auto kv : UIBackend::backends) { kv.second->add_licenses(); + } + DEBUG.writeln("Loaded frontends: "); + for (auto kv : UIBackend::backends) { DEBUG.writefln(" - '%s'", kv.first.c_str()); } DEBUG.writeln("Loading options file..."); @@ -88,12 +128,26 @@ int main(int argc, char **argv) { return -1; } else { DEBUG.writefln("Using backend: '%s'...", backend->get_id().c_str()); + UIBackend::running_ui_backend = backend; if (full_help) { args.clear(); args.push_back("--help"); } try { + if (proxy_backend != nullptr && !quit) { + if (!proxy_backend->run(args, new_argc, new_argv)) { + throw 0; + } + proxy_backend = nullptr; + } + if (!quit) { + UIBackend::unregister_backend(); + } output = backend->run(args, new_argc, new_argv); + if (quit && proxy_backend != nullptr) { + proxy_backend->quitDaemon(); + proxy_backend->unregister_self(); + } } catch (int return_code) { if (full_help) { std::string helpstr = app.help(); diff --git a/playback.cpp b/playback.cpp index 9b1fad0..47776e0 100644 --- a/playback.cpp +++ b/playback.cpp @@ -10,6 +10,7 @@ #endif #include "log.hpp" #include +#include "dbus.hpp" using namespace std::chrono; size_t CalculateBufSize(SDL_AudioSpec *obtained, double seconds, double max_seconds, size_t samples_override = 0) { return ((((samples_override == 0) ? obtained->samples : samples_override) * std::min(seconds, max_seconds)) + 1) * sizeof(SAMPLETYPE) * obtained->channels; @@ -69,9 +70,7 @@ Mix_Music *PlaybackInstance::Load(const char *file) { Mix_Music *output = Mix_LoadMUS(file); if (output == nullptr) { ERROR.writefln("Error loading music '%s': %s", file, Mix_GetError()); - error_mutex.lock(); - errors.emplace("Error loading music!"); - error_mutex.unlock(); + set_error("Error loading music!"); return nullptr; } Mix_PlayMusicStream(output, -1); @@ -129,9 +128,7 @@ void PlaybackInstance::ThreadFunc() { if (!SDL_WasInit(SDL_INIT_AUDIO)) { if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { ERROR.writefln("Error initializing SDL: '%s'", SDL_GetError()); - error_mutex.lock(); - errors.emplace("Failed to initialize SDL!"); - error_mutex.unlock(); + set_error("Failed to initialize SDL!"); return; } } @@ -152,9 +149,7 @@ void PlaybackInstance::ThreadFunc() { Mix_Init(MIX_INIT_FLAC|MIX_INIT_MID|MIX_INIT_MOD|MIX_INIT_MP3|MIX_INIT_OGG|MIX_INIT_OPUS|MIX_INIT_WAVPACK); if ((device = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, SDL_AUDIO_ALLOW_CHANNELS_CHANGE|SDL_AUDIO_ALLOW_FREQUENCY_CHANGE|SDL_AUDIO_ALLOW_SAMPLES_CHANGE)) == 0) { ERROR.writefln("Error opening audio device: '%s'", SDL_GetError()); - error_mutex.lock(); - errors.emplace("Failed to open audio device!"); - error_mutex.unlock(); + set_error("Failed to open audio device!"); running = false; break; } @@ -171,9 +166,7 @@ void PlaybackInstance::ThreadFunc() { buf = (Uint8*)malloc(new_bufsize); if (buf == nullptr) { ERROR.writeln("Failed to allocate memory for playback!"); - error_mutex.lock(); - errors.emplace("Failed to allocate memory for playback!"); - error_mutex.unlock(); + set_error("Failed to allocate memory for playback!"); set_signal(PlaybackSignalErrorOccurred); running = false; break; @@ -234,9 +227,7 @@ void PlaybackInstance::ThreadFunc() { buf = (Uint8*)realloc((void*)buf, correct_buf_size); if (buf == nullptr) { ERROR.writes("Failed to allocate memory for playback!"); - error_mutex.lock(); - errors.emplace("Failed to allocate memory for playback!"); - error_mutex.unlock(); + set_error("Failed to allocate memory for playback!"); set_signal(PlaybackSignalErrorOccurred); running = false; break; @@ -359,38 +350,55 @@ void PlaybackInstance::Update() { bool PlaybackInstance::IsStopped() { return !running; } -optional PlaybackInstance::GetError() { - if (ErrorExists()) { - error_mutex.lock(); - std::string error = errors.back(); - errors.pop(); - error_mutex.unlock(); - return error; - } else { - return {}; - } -} -bool PlaybackInstance::ErrorExists() { - error_mutex.lock(); - bool output = !errors.empty(); - error_mutex.unlock(); - return output; -} -void PlaybackInstance::set_signal(uint16_t signal) { +void Playback::set_signal(uint16_t signal) { signal_mutex.lock(); - signals_occurred |= signal; + for (auto &kv : signals_occurred) { + kv.second |= signal; + } signal_mutex.unlock(); } -uint16_t PlaybackInstance::handle_signals(uint16_t signals) { +uint16_t Playback::handle_signals(uint16_t signals, void *handle) { if (signal_mutex.try_lock()) { - uint16_t output = signals_occurred & signals; - signals_occurred &= ~output; + if (!signals_occurred.contains(handle)) { + signals_occurred[handle] = PlaybackSignalNone; + } + uint16_t output = signals_occurred[handle]; + signals_occurred[handle] &= ~output; signal_mutex.unlock(); return output; } else { return PlaybackSignalNone; } } +void Playback::register_handle(void *handle) { + signal_mutex.lock(); + if (!signals_occurred.contains(handle)) { + signals_occurred[handle] = PlaybackSignalNone; + } + signal_mutex.unlock(); + error_mutex.lock(); + if (!errors_occurred.contains(handle)) { + std::deque new_value; + for (size_t i = 0; i < errors.size(); i++) { + new_value.push_back(errors[i]); + } + errors_occurred[handle] = new_value; + } + error_mutex.unlock(); +} + +void Playback::unregister_handle(void *handle) { + signal_mutex.lock(); + if (signals_occurred.contains(handle)) { + signals_occurred.erase(handle); + } + signal_mutex.unlock(); + error_mutex.lock(); + if (errors_occurred.contains(handle)) { + errors_occurred.erase(handle); + } + error_mutex.unlock(); +} void PlaybackInstance::SetTempo(float tempo) { this->tempo = tempo; Update(); @@ -418,4 +426,33 @@ float PlaybackInstance::GetSpeed() { } float PlaybackInstance::GetVolume() { return volume; +} +void Playback::set_error(std::string desc) { + error_mutex.lock(); + errors.push_back(desc); + for (auto &kv : errors_occurred) { + kv.second.push_front(desc); + } + error_mutex.unlock(); + set_signal(PlaybackSignalErrorOccurred); +} +Playback *Playback::Create(bool *daemon_found, bool daemon) { + auto *dbus_proxy = DBusAPISender::Create(); + if (dbus_proxy != nullptr) { + if (daemon_found != nullptr) { + *daemon_found = dbus_proxy->IsDaemon(); + } + DEBUG.writefln("DBus proxy daemon found: %s", *daemon_found ? "true" : "false"); + if (daemon) { + delete dbus_proxy; + return nullptr; + } else { + return dbus_proxy; + } + } + if (daemon_found != nullptr) { + *daemon_found = false; + } + DEBUG.writeln("Creating new playback instance."); + return new PlaybackInstance(); } \ No newline at end of file diff --git a/playback.h b/playback.h index f3f3f06..b87400c 100644 --- a/playback.h +++ b/playback.h @@ -11,109 +11,166 @@ #include #include #include +#include +#include using namespace soundtouch; using std::span; using std::optional; using std::vector; using std::queue; +using std::deque; enum { + /// @brief No signals have occurred. PlaybackSignalNone = 0, + /// @brief The file was changed. Recheck the properties of the file because they are likely different. PlaybackSignalFileChanged = 1 << 0, + /// @brief The speed was changed. PlaybackSignalSpeedChanged = 1 << 1, + /// @brief The speed was changed. PlaybackSignalTempoChanged = 1 << 2, + /// @brief The speed was changed. PlaybackSignalPitchChanged = 1 << 3, + /// @brief Playback was paused. If @ref PlaybackSignalResumed has also been sent, you must use @ref Playback::IsPaused to check if playback was paused or resumed. PlaybackSignalPaused = 1 << 4, + /// @brief Playback was resumed. If @ref PlaybackSignalPaused has also been sent, you must use @ref Playback::IsPaused to check if playback was paused or resumed. PlaybackSignalResumed = 1 << 5, + /// @brief Playback was stopped entirely. If @ref PlaybackSignalStarted has also been signalled, call @ref Playback::IsStopped to find out if playback is currently playing. PlaybackSignalStopped = 1 << 6, + /// @brief An error occurred and playback has likely (but not necessarily) stopped. Call @ref Playback::GetError for details. PlaybackSignalErrorOccurred = 1 << 7, + /// @brief Playback was seeked by the @ref Playback::Seek function PlaybackSignalSeeked = 1 << 8, + /// @brief Playback has started. If @ref PlaybackSignalStopped has also been signalled, call @ref Playback::IsStopped to find out if playback is currently playing. PlaybackSignalStarted = 1 << 9 }; +/// @brief Playback handler base class. class Playback { + protected: + friend class DBusAPI; + /// @brief The handle-specific signal state. + std::map signals_occurred; + /// @brief The global error state for initializing handle-specific state. + std::vector errors; + /// @brief The handle-specific error state. + std::map> errors_occurred; + /// @brief The mutex for signals + std::mutex signal_mutex; + /// @brief The mutex for errors + std::mutex error_mutex; + /// @brief Signals other code with the specified signal(s). Use with PlaybackSignal* enum variants. + void set_signal(uint16_t signal); + /// @brief Signals an error, and pushes details to other code. + void set_error(std::string desc); + public: + inline virtual bool is_proxy() { + return false; + } inline Playback() {}; inline virtual ~Playback() {} + /// @brief Gets the current file, if any. + /// @returns No value if there is no file playing, or the path to the file if there is a file playing. inline virtual std::optional get_current_file() { return {}; } + /// @brief Gets the current file's title, if any, for display to the user. + /// @returns No value if there is no file playing. If there is a file playing, a string is returned. The contents of the string depend on whether or not the file has a title tag. If so, the string contains the contents of that tag. Otherwise, the string contains the stem of the file's path. inline virtual std::optional get_current_title() { return {}; } + /// @brief Gets the position of the playing file. inline virtual double GetPosition() { return 0.0; } + /// @brief Gets the length of the file. If the length is less than or equal to zero, the file can be considered to have an unknown length. inline virtual double GetLength() { return 0.0; } + /// @brief Sets the playback position into the current file. inline virtual void Seek(double position) {} + /// @brief Plays a new file. inline virtual void Start(std::string filePath) {} + /// @brief Checks whether or not the file is paused. + /// @returns true if playback is paused, false otherwise inline virtual bool IsPaused() { return true; } + /// @brief Toggles the pause state of the playback. inline virtual void Pause() {} + /// @brief Stops all playback. inline virtual void Stop() {} + /// @brief Checks whether or not playback is stopped. + /// @returns true if playback is stopped, false otherwise. inline virtual bool IsStopped() { return true; } + /// @brief Sets the tempo multiplier of the playback. inline virtual void SetTempo(float tempo) {} + /// @brief Sets the speed multiplier (Both speed and pitch, but this is a separate property) of the playback. inline virtual void SetSpeed(float speed) {} + /// @brief Sets the pitch multiplier of the playback inline virtual void SetPitch(float pitch) {} + /// @brief Sets the volume of the playback, as a percentage inline virtual void SetVolume(float volume) {} + /// @brief Gets the tempo multiplier of the playback. inline virtual float GetTempo() { return 1.0; } + /// @brief Gets the speed multiplier (Both speed and pitch, but this is a separate property) of the playback. inline virtual float GetSpeed() { return 1.0; } + /// @brief Gets the pitch multiplier of the playback. inline virtual float GetPitch() { return 1.0; } + /// @brief Gets the volume of the playback, as a percentage inline virtual float GetVolume() { return 1.0; } - inline virtual uint16_t handle_signals(uint16_t signal) { - return 0; + /// @brief Returns and clears signals, using a registered handle if specified. + /// @param signal The signal(s) to check for and clear. + /// @param handle The handle, which was specified to @ref Playback::register_handle , or nullptr. + uint16_t handle_signals(uint16_t signal, void *handle = nullptr); + /// @brief Registers a handle for usage in handle_signals and GetError. + void register_handle(void *handle); + /// @brief Unregisters a handle that is no longer needed. + void unregister_handle(void *handle); + /// @brief Gets the last error from the playback engine, or no value if there was none. + /// @param handle The handle as registered with @ref Playback::register_handle + inline virtual optional GetError(void *handle) { + std::optional output = {}; + if (ErrorExists(handle)) { + error_mutex.lock(); + if (errors_occurred.contains(handle)) { + output = errors_occurred[handle].back(); + errors_occurred[handle].pop_back(); + } + error_mutex.unlock(); + } + return output; } - inline virtual optional GetError() { - return {}; - } - inline virtual bool ErrorExists() { - return false; + /// @brief Checks if an unread error has occurred. + /// @param handle The handle as registered with @ref Playback::register_handle + inline virtual bool ErrorExists(void *handle) { + bool output = false; + error_mutex.lock(); + if (errors_occurred.contains(handle)) { + output = !errors_occurred[handle].empty(); + } + error_mutex.unlock(); + return output; } + /// @brief Helper function to set the paused status via the @ref Playback::IsPaused and @ref Playback::Pause APIs. + /// @param paused The new pause state. inline virtual void SetPaused(bool paused) { if (IsPaused() != paused) { Pause(); } } + static Playback *Create(bool *daemon_found, bool daemon = false); }; class DBusAPISender; -class PlaybackRemote : public Playback { -private: - DBusAPISender *sender; -public: - - double GetPosition() override; - double GetLength() override; - void Seek(double position) override; - void Start(std::string filePath) override; - bool IsPaused() override; - void Pause() override; - void Stop() override; - void Update(); - bool IsStopped() override; - void SetTempo(float tempo) override; - void SetPitch(float pitch) override; - void SetSpeed(float speed) override; - void SetVolume(float volume) override; - float GetTempo() override; - float GetPitch() override; - float GetSpeed() override; - float GetVolume() override; - uint16_t handle_signals(uint16_t signal) override; - optional GetError() override; - bool ErrorExists() override; - PlaybackRemote(); -}; class PlaybackInstance : public Playback { private: std::string filePath; @@ -152,9 +209,6 @@ private: std::mutex current_file_mutex; std::optional current_file; std::optional current_title; - uint16_t signals_occurred = PlaybackSignalNone; - std::mutex signal_mutex; - void set_signal(uint16_t signal); float prev_pitch, prev_speed, prev_tempo; public: @@ -162,7 +216,9 @@ public: ~PlaybackInstance() override; std::optional get_current_file() override; std::optional get_current_title() override; - + inline bool is_proxy() override { + return false; + } double GetPosition() override; double GetLength() override; void Seek(double position) override; @@ -191,7 +247,4 @@ public: float MinSpeed = 0.25; float MinPitch = 0.25; float MinTempo = 0.25; - uint16_t handle_signals(uint16_t signal) override; - optional GetError() override; - bool ErrorExists() override; }; \ No newline at end of file diff --git a/proxy_backend.cpp b/proxy_backend.cpp new file mode 100644 index 0000000..93e752f --- /dev/null +++ b/proxy_backend.cpp @@ -0,0 +1,32 @@ +#include "proxy_backend.hpp" +#include "log.hpp" +#include +#include +using namespace std::literals::chrono_literals; +std::string ProxyGlueBackend::get_id() { + return "proxy"; +} +std::string ProxyGlueBackend::get_name() { + return "Proxy Backend"; +} +bool ProxyGlueBackend::allow_multi_instance() { + return false; +} +int ProxyGlueBackend::run(std::vector realArgs, int argc, char **argv) { + bool commands_specified = parse_args(realArgs, argc, argv); + if (!commands_specified) { + DEBUG.writeln("No commands specified."); + return 1; + } + sender = DBusAPISender::Create(); + if (sender == nullptr) { + ERROR.writeln("Existing instance required to send commands."); + return 1; + } + playback = (Playback*)sender; + setup_playback_args(); + return 0; +} +void ProxyGlueBackend::quitDaemon() { + ((DBusAPISender*)playback)->Quit(); +} \ No newline at end of file diff --git a/proxy_backend.hpp b/proxy_backend.hpp new file mode 100644 index 0000000..d54ec87 --- /dev/null +++ b/proxy_backend.hpp @@ -0,0 +1,11 @@ +#pragma once +#include "backend.hpp" +class ProxyGlueBackend : public UIBackend { + DBusAPISender *sender; + public: + bool allow_multi_instance() override; + std::string get_id() override; + std::string get_name() override; + void quitDaemon(); + int run(std::vector realArgs, int argc, char **argv) override; +}; \ No newline at end of file