diff --git a/.clangd b/.clangd deleted file mode 100644 index 8127f1f..0000000 --- a/.clangd +++ /dev/null @@ -1,2 +0,0 @@ -CompileFlags: - CompilationDatabase: "./build/compile_commands.json" diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..14340ae --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,20 @@ +{ + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/**", + "/usr/include/qt6/QtCore", + "/usr/include/qt6/QtGui", + "/usr/include/qt6/QtWidgets" + ], + "defines": [], + "compilerPath": "/usr/bin/gcc", + "cStandard": "c17", + "cppStandard": "c++17", + "intelliSenseMode": "linux-gcc-x64", + "configurationProvider": "ms-vscode.cmake-tools" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..e69de29 diff --git a/CMakeLists.txt b/CMakeLists.txt index ee9af85..98ed936 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,8 +102,6 @@ if (DEFINED EMSCRIPTEN) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${EXTRA_FLAGS}") set(EXTRA_LINKER_FLAGS "${EXTRA_LINKER_FLAGS} ${EXTRA_FLAGS}") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${EXTRA_LINKER_FLAGS}") -else() - set(BUILD_STATIC OFF CACHE BOOL "") endif() option(BUILD_SDL "Enables built-in SDL" OFF) option(BUILD_SDL_IMAGE "Enables built-in SDL_image" ${BUILD_SDL}) @@ -160,7 +158,11 @@ add_subdirectory(subprojects/vgmstream) if (DEFINED EMSCRIPTEN) set(EXTRA_LIBS ) else() - set(EXTRA_LIBS libvgmstream_shared) + if (BUILD_STATIC) + set(EXTRA_LIBS libvgmstream) + else() + set(EXTRA_LIBS libvgmstream_shared) + endif() endif() if(SDL_MIXER_X_STATIC) set(SDL_MIXER_X_TARGET SDL2_mixer_ext_Static) @@ -402,7 +404,7 @@ else() if (TARGET SDL2-static) set(SDL2_TARGET SDL2-static) endif() - target_link_libraries(liblooper PUBLIC ${SDL2_TARGET} ${SDL_MIXER_X_TARGET} ${SOUNDTOUCH_TARGET} libvgmstream libvgmstream_shared ${JSONCPP_TARGET}) + target_link_libraries(liblooper PUBLIC ${SDL2_TARGET} ${SDL_MIXER_X_TARGET} ${SOUNDTOUCH_TARGET} libvgmstream ${JSONCPP_TARGET}) endif() if(BUILD_PROTOBUF) add_subdirectory(subprojects/protobuf) @@ -483,6 +485,7 @@ endmacro() set(ENABLED_UIS ) set(ENABLED_PLAYBACK_BACKENDS ) ui_backend_subdir(NAME "IMGUI" READABLE_NAME "Dear ImGui" SUBDIR backends/ui/imgui) +ui_backend_subdir(NAME "QT" READABLE_NAME "Qt" SUBDIR backends/ui/qt) if(CMAKE_SYSTEM_NAME STREQUAL "Haiku") ui_backend_subdir(NAME "HAIKU" READABLE_NAME "Haiku Native" SUBDIR backends/ui/haiku) endif() @@ -537,7 +540,7 @@ if(DEFINED EMSCRIPTEN) copy_to_bindir(assets/ForkAwesome/css/fork-awesome.min.css fork-awesome.min.css) copy_to_bindir(assets/ForkAwesome/css/fork-awesome.min.css.map fork-awesome.min.css.map) endif() -target_link_libraries(${TARGET_NAME} PUBLIC liblooper ${UI_BACKENDS} ${PLAYBACK_BACKENDS}) +target_link_libraries(${TARGET_NAME} PUBLIC liblooper ${UI_BACKENDS} ${PLAYBACK_BACKENDS} vorbis mpg123) install(TARGETS ${TARGET_NAME} ${EXTRA_LIBS}) if (${BUILD_SDL2}) install(EXPORT SDL2-static SDL2main) diff --git a/audio_output_backend.hpp b/audio_output_backend.hpp new file mode 100644 index 0000000..35a7639 --- /dev/null +++ b/audio_output_backend.hpp @@ -0,0 +1,9 @@ +#pragma once +#include +#include +#include "backend.hpp" +class AudioOutputBackend { + BACKEND_TYPE(AudioOutputBackend); + protected: + +}; \ No newline at end of file diff --git a/backends/ui/haiku/main_window.cpp b/backends/ui/haiku/main_window.cpp index 75eb599..d62ead1 100644 --- a/backends/ui/haiku/main_window.cpp +++ b/backends/ui/haiku/main_window.cpp @@ -40,7 +40,7 @@ std::vector Subwindow::windows; BMessage *make_slider_msg(uint32_t what, bool down) { BMessage *msg = new BMessage(what); msg->SetBool(CMD_MOUSE_DOWN_KEY, down); - return msg; + return msg; } bool is_slider_down_msg(BMessage *msg) { return msg->HasBool(CMD_MOUSE_DOWN_KEY) && msg->GetBool(CMD_MOUSE_DOWN_KEY); @@ -186,7 +186,7 @@ void LooperWindow::MessageReceived(BMessage *msg) { default: { msg->PrintToStream(); } break; - } + } return; } switch (msg->what) { @@ -263,7 +263,7 @@ void LooperWindow::MessageReceived(BMessage *msg) { WARNING.writefln("entry.GetPath(&path) == %d", (int32)(err)); msg->PrintToStream(); } - } else { + } else { WARNING.writefln("msg->FindRef(\"refs\", &ref) == %d", (int32)(err)); msg->PrintToStream(); } @@ -279,23 +279,6 @@ void LooperWindow::MessageReceived(BMessage *msg) { } break; }; } -LooperLogScaler::LooperLogScaler(double min, double max) { - update_min_max(min, max); -} -void LooperLogScaler::update_min_max(double min, double max) { - x0 = min; - x1 = (max - min) / (exp(1.0) - 1.0); - la = min; - lb = (max - min); -// x0 = scale_log(min); -// x1 = scale_log(max); -} -double LooperLogScaler::scale_log(double value) { - return (std::log(((value - x0) / x1) + 1.0) * lb) + la; -} -double LooperLogScaler::unscale_log(double value) { - return ((std::exp((value - la) / lb) - 1.0) * x1) + x0; -} void LooperWindow::Pulse() { auto len = playback->GetLength(); auto pos = playback->GetPosition(); @@ -324,7 +307,7 @@ void LooperWindow::Pulse() { auto speed = playback->GetSpeed(); auto tempo = playback->GetTempo(); if (!volume_clicked) volume_slider->SetValue(volume); - volume_slider->SetLabel(fmt::format("Volume: {}%", (uint8_t)volume).c_str()); + volume_slider->SetLabel(fmt::format("Volume: {}%", (int)volume).c_str()); if (!pitch_clicked) pitch_slider->SetValueDouble(pitch); pitch_slider->SetLabel(fmt::format("Pitch {:.02f}x", pitch).c_str()); if (!speed_clicked) speed_slider->SetValueDouble(speed); diff --git a/backends/ui/haiku/main_window.h b/backends/ui/haiku/main_window.h index 29eebdb..d1b7966 100644 --- a/backends/ui/haiku/main_window.h +++ b/backends/ui/haiku/main_window.h @@ -14,18 +14,8 @@ #include "slider.h" #include "prefs.h" #include +#include #define CMD_UPDATE_LABEL_SETTING 0x1000 -class LooperLogScaler { - double la; - double lb; - public: - double x0; - double x1; - void update_min_max(double min, double max); - LooperLogScaler(double min, double max); - double scale_log(double value); - double unscale_log(double value); -}; extern bool show_labels; extern bool show_icons; extern bool quitting; @@ -36,7 +26,7 @@ class Subwindow { std::atomic_bool Showing = false; std::atomic_bool ShownEver = false; private: - inline Subwindow(BWindow *window) + inline Subwindow(BWindow *window) : window(window) { } public: @@ -98,5 +88,5 @@ class LooperRefHandler : public BHandler { BHandler *next_handler; public: void MessageReceived(BMessage *msg) override; - LooperRefHandler(LooperWindow *win); + LooperRefHandler(LooperWindow *win); }; diff --git a/backends/ui/haiku/prefs.cpp b/backends/ui/haiku/prefs.cpp index 198b6ca..2e629f9 100644 --- a/backends/ui/haiku/prefs.cpp +++ b/backends/ui/haiku/prefs.cpp @@ -107,7 +107,7 @@ void PrefsWindow::MessageReceived(BMessage *msg) { case CMD_REVERT: { set_options_changed(false); new_label_setting = get_option("ui.haiku.label_setting", "icons"); - new_frontend = get_option("ui.frontend", "haiki"); + new_frontend = get_option("ui.frontend", "haiku"); if (new_frontend != "haiku") restart_warning->Show(); else restart_warning->Hide(); for (size_t i = 0; i < backend_ids.size(); i++) { diff --git a/backends/ui/qt/CMakeLists.txt b/backends/ui/qt/CMakeLists.txt new file mode 100644 index 0000000..5c1ce93 --- /dev/null +++ b/backends/ui/qt/CMakeLists.txt @@ -0,0 +1,13 @@ +set(BACKEND_QT_SRC_BASE main.cpp main.h main_window.cpp main_window.h slider.hpp slider.cpp preferences.h preferences.cpp aboutwindow.h aboutwindow.cpp) +set(BACKEND_QT_SRC ) +foreach(SRC IN ITEMS ${BACKEND_QT_SRC_BASE}) + set(BACKEND_QT_SRC ${BACKEND_QT_SRC} ${CMAKE_CURRENT_SOURCE_DIR}/${SRC}) +endforeach() +set(BACKEND_QT_INC ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) +set(CMAKE_AUTOMOC ON) +add_ui_backend(qt_ui ${BACKEND_QT_SRC}) +find_package(Qt6 COMPONENTS Core Gui Widgets) +target_link_libraries(qt_ui PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets SDL2::SDL2 fmt::fmt liblooper) +target_include_directories(qt_ui PRIVATE ../../..) +#target_link_libraries(qt_ui PRIVATE PkgConfig::GTK4 PkgConfig::gtkmm4) +#target_include_directories(qt_ui PRIVATE ${BACKEND_QT_INC}) diff --git a/backends/ui/qt/aboutwindow.cpp b/backends/ui/qt/aboutwindow.cpp new file mode 100644 index 0000000..95e7696 --- /dev/null +++ b/backends/ui/qt/aboutwindow.cpp @@ -0,0 +1,66 @@ +#include "aboutwindow.h" +QModelIndex LicenseModel::index(int row, int column, const QModelIndex &parent) const { + return createIndex(row, 0, row); +} +QModelIndex LicenseModel::parent(const QModelIndex &child) const { + return QModelIndex(); +} +Qt::ItemFlags LicenseModel::flags(const QModelIndex &index) const { + return Qt::ItemFlag::ItemIsEnabled|Qt::ItemFlag::ItemIsSelectable; +} +int LicenseModel::rowCount(const QModelIndex &parent) const { + return parent.isValid() ? 0 : licenseData.size(); +} +int LicenseModel::columnCount(const QModelIndex &parent) const { + return parent.isValid() ? 0 : 1; +} +QVariant LicenseModel::data(const QModelIndex &index, int role) const { + if ((index.row() < 0 || licenseData.size() <= index.row()) && (role == Qt::DisplayRole || role == Qt::UserRole)) { + return ""; + } + switch (role) { + case Qt::DisplayRole: { + const LicenseData &data = licenseData[index.row()]; + return QString::asprintf("%s (%s)", data.Project.c_str(), data.Spdx.c_str()); + } break; + case Qt::UserRole: { + return QString(licenseData[index.row()].LicenseContents.c_str()); + } break; + default: { + return QVariant(); + } break; + } +} +LicenseModel::LicenseModel() { + auto tmp = get_license_data(); + for (auto data : tmp) { + licenseData.push_back(data); + } +} +LicenseModel::~LicenseModel() { + +} +AboutWindow::AboutWindow() { + license_text = new QTextBrowser(); + license_list = new QListView(); + license_list->setModel(new LicenseModel()); + QObject::connect(license_list, &QListView::clicked, [=,this](const QModelIndex &idx) { + license_text->setText(license_list->model()->data(idx, Qt::UserRole).toString()); + }); + license_list->setSelectionMode(QAbstractItemView::SingleSelection); + license_list->setSelectionBehavior(QAbstractItemView::SelectRows); + QBoxLayout *mainLayout = new QBoxLayout(QBoxLayout::TopToBottom); + QLabel *title = new QLabel("Looper"); + auto font = title->font(); + font.setPointSize(24); + title->setFont(font); + mainLayout->addWidget(title); + QLabel *versionText = new QLabel(TAG); + mainLayout->addWidget(versionText); + QSplitter *splitter = new QSplitter(Qt::Orientation::Horizontal); + splitter->addWidget(license_list); + splitter->addWidget(license_text); + mainLayout->addWidget(splitter); + setLayout(mainLayout); + license_list->clicked(license_list->model()->index(0, 0)); +} \ No newline at end of file diff --git a/backends/ui/qt/aboutwindow.h b/backends/ui/qt/aboutwindow.h new file mode 100644 index 0000000..f0b92a7 --- /dev/null +++ b/backends/ui/qt/aboutwindow.h @@ -0,0 +1,33 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class LicenseModel : public QAbstractListModel { + Q_OBJECT; + std::vector licenseData; + public: + ~LicenseModel() override; + Q_INVOKABLE Qt::ItemFlags flags(const QModelIndex &index) const override; + Q_INVOKABLE QModelIndex index(int row, int column, const QModelIndex &parent) const override; + Q_INVOKABLE QModelIndex parent(const QModelIndex &child) const override; + Q_INVOKABLE int rowCount(const QModelIndex &parent) const override; + Q_INVOKABLE int columnCount(const QModelIndex &parent) const override; + Q_INVOKABLE QVariant data(const QModelIndex &index, int role) const override; + LicenseModel(); +}; +class AboutWindow : public QWidget { + Q_OBJECT; + public: + QListView *license_list; + QTextBrowser *license_text; + public: + AboutWindow(); +}; \ No newline at end of file diff --git a/backends/ui/qt/main.cpp b/backends/ui/qt/main.cpp new file mode 100644 index 0000000..7f50493 --- /dev/null +++ b/backends/ui/qt/main.cpp @@ -0,0 +1,19 @@ +#include "main.h" +#include "main_window.h" +#include +#include +#include +#include +std::string QtUIBackend::get_id() { + return "qt"; +} +std::string QtUIBackend::get_name() { + return "QT"; +} +int QtUIBackend::run(std::vector args, int argc, char **argv) { + UIBackend::run(args, argc, argv); + QApplication app(argc, argv); + LooperWindow window(this->playback); + window.show(); + return app.exec(); +} diff --git a/backends/ui/qt/main.h b/backends/ui/qt/main.h new file mode 100644 index 0000000..e10667f --- /dev/null +++ b/backends/ui/qt/main.h @@ -0,0 +1,10 @@ +#pragma once +#include +#include +#include +class QtUIBackend : public UIBackend { + public: + std::string get_id() override; + std::string get_name() override; + int run(std::vector args, int argc, char **argv) override; +}; diff --git a/backends/ui/qt/main_window.cpp b/backends/ui/qt/main_window.cpp new file mode 100644 index 0000000..9b83ea6 --- /dev/null +++ b/backends/ui/qt/main_window.cpp @@ -0,0 +1,159 @@ +#include "main_window.h" +#include +#include "preferences.h" +void LooperWindow::Pulse() { + auto len = playback->GetLength(); + auto pos = playback->GetPosition(); + this->slider->SetLimits(0.0, len); + this->slider->SetValue(pos); + auto component_count = TimeToComponentCount(len); + bool enable_ui = !playback->IsStopped(); + if (enable_ui) { + slider->SetLabel(fmt::format("Position: {}", TimeToString(pos, component_count)).c_str()); + slider->SetLimitLabels(TimeToString(0, component_count).c_str(), TimeToString(len).c_str()); + } else { + slider->SetLabel("Position"); + slider->SetLimitLabels("N/A", "N/A"); + } + update_label_setting(labels_visible, icons_visible); + slider->setEnabled(enable_ui); + auto volume = playback->GetVolume(); + auto pitch = playback->GetPitch(); + auto speed = playback->GetSpeed(); + auto tempo = playback->GetTempo(); + volume_slider->SetValue(volume); + pitch_slider->SetValue(pitch); + speed_slider->SetValue(speed); + tempo_slider->SetValue(tempo); + volume_slider->SetLabel(fmt::format("Volume: {}%", (int)volume).c_str()); + pitch_slider->SetLabel(fmt::format("Pitch {:.02f}x", pitch).c_str()); + speed_slider->SetLabel(fmt::format("Speed: {:.02f}x", speed).c_str()); + tempo_slider->SetLabel(fmt::format("Tempo: {:.02f}x", tempo).c_str()); +} +LooperWindow::LooperWindow(Playback *playback) : QMainWindow() { + labels_visible = false; + icons_visible = true; + this->playback = playback; + this->root_layout = new QBoxLayout(QBoxLayout::TopToBottom); + QWidget *central_widget = new QWidget(); + central_widget->setLayout(this->root_layout); + this->setCentralWidget(central_widget); + prefs_window = new PrefsWindow(); + about_window = new AboutWindow(); + QMenuBar *bar = this->menuBar(); + file_menu = new QMenu("File"); + open_item = new QAction("Open..."); + file_dialog = new QFileDialog(this); + QObject::connect(file_dialog, &QFileDialog::fileSelected, [=,this](const QString &file) { + playback->Start(file.toUtf8().constData()); + }); + QObject::connect(open_item, &QAction::triggered, [=,this]() { + file_dialog->show(); + }); + prefs_item = new QAction("Preferences..."); + QObject::connect(prefs_item, &QAction::triggered, [=,this]() { + prefs_window->show(); + }); + quit_item = new QAction("Quit"); + QObject::connect(quit_item, &QAction::triggered, [=,this]() { + qApp->quit(); + }); + file_menu->addAction(open_item); + file_menu->addAction(prefs_item); + file_menu->addAction(quit_item); + help_menu = new QMenu("Help"); + about_item = new QAction("About..."); + QObject::connect(about_item, &QAction::triggered, [=,this]() { + about_window->show(); + }); + help_menu->addAction(about_item); + bar->addMenu(file_menu); + bar->addMenu(help_menu); + root_layout->addWidget(bar); + QSpacerItem *spacer = new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::MinimumExpanding); + root_layout->addSpacerItem(spacer); + QBoxLayout *top_row = new QBoxLayout(QBoxLayout::LeftToRight); + pause_resume_btn = new QPushButton("Pause"); + QObject::connect(pause_resume_btn, &QPushButton::pressed, [=,this]() { + playback->Pause(); + }); + restart_btn = new QPushButton("Restart"); + QObject::connect(restart_btn, &QPushButton::pressed, [=,this]() { + playback->Seek(0.0); + }); + top_row->addWidget(pause_resume_btn); + top_row->addWidget(restart_btn); + slider = new LooperSlider("seek", "Position", 0.0, 1.0, 0.00000000001, false); + QObject::connect(slider, &LooperSlider::changed, [=,this](double value) { + playback->Seek(value); + }); + top_row->addWidget(slider); + stop_btn = new QPushButton("Stop"); + QObject::connect(stop_btn, &QPushButton::pressed, [=,this]() { + playback->Stop(); + }); + top_row->addWidget(stop_btn); + volume_slider = new LooperSlider("volume", "Volume", 0.0, 100.0, 1.0, false); + volume_slider->SetLimitLabels("Muted", "Full Volume"); + top_row->addWidget(volume_slider); + QWidget *top_row_widget = new QWidget(); + top_row_widget->setLayout(top_row); + root_layout->addWidget(top_row_widget); + QBoxLayout *bottom_row = new QBoxLayout(QBoxLayout::LeftToRight); + speed_slider = new LooperSlider("speed", "Speed", 0.25, 4.0, 0.01, true); + pitch_slider = new LooperSlider("pitch", "Pitch", 0.25, 4.0, 0.01, true); + tempo_slider = new LooperSlider("tempo", "Tempo", 0.25, 4.0, 0.01, true); + speed_slider->SetLimitLabels("0.25x", "4.00x"); + pitch_slider->SetLimitLabels("0.25x", "4.00x"); + tempo_slider->SetLimitLabels("0.25x", "4.00x"); + QObject::connect(speed_slider, &LooperSlider::changed, [=,this](double value) { + playback->SetSpeed(value); + }); + QObject::connect(pitch_slider, &LooperSlider::changed, [=,this](double value) { + playback->SetPitch(value); + }); + QObject::connect(tempo_slider, &LooperSlider::changed, [=,this](double value) { + playback->SetTempo(value); + }); + bottom_row->addWidget(speed_slider); + bottom_row->addWidget(pitch_slider); + bottom_row->addWidget(tempo_slider); + QWidget *bottom_row_widget = new QWidget(); + bottom_row_widget->setLayout(bottom_row); + root_layout->addWidget(bottom_row_widget); + QTimer *timer = new QTimer(this); + QObject::connect(timer, &QTimer::timeout, [=,this]() { + Pulse(); + }); + timer->setTimerType(Qt::TimerType::PreciseTimer); + timer->setSingleShot(false); + timer->setInterval(1); + timer->start(); + QObject::connect(prefs_window, &PrefsWindow::settings_changed, this, &LooperWindow::update_label_setting); + //setLayout(layout); +} +void LooperWindow::update_label_setting(bool labels_visible, bool icons_visible) { + this->labels_visible = labels_visible; + this->icons_visible = icons_visible; + for (auto *slider : {volume_slider, speed_slider, tempo_slider, pitch_slider, slider}) { + slider->SetShowButtonIcon(icons_visible); + slider->SetShowButtonText(labels_visible); + } + QString strings[] = {"Restart", "Stop", (playback->IsPaused() && !playback->IsStopped()) ? "Resume" : "Pause"}; + QIcon icons[] = {QIcon::fromTheme("view-refresh"), QIcon::fromTheme("media-playback-stop"), QIcon::fromTheme((playback->IsPaused() && !playback->IsStopped()) ? "media-playback-start" : "media-playback-pause")}; + QPushButton *buttons[] = {restart_btn, stop_btn, pause_resume_btn}; + for (int i = 0; i < 3; i++) { + QIcon icon = icons[i]; + if (icon.isNull() || labels_visible) { + buttons[i]->setText(strings[i]); + } else { + buttons[i]->setText(""); + } + if (icons_visible && !icon.isNull()) { + buttons[i]->setIcon(icons[i]); + } else { + QIcon emptyIcon; + buttons[i]->setIcon(emptyIcon); + } + } +} \ No newline at end of file diff --git a/backends/ui/qt/main_window.h b/backends/ui/qt/main_window.h new file mode 100644 index 0000000..2eb3e1b --- /dev/null +++ b/backends/ui/qt/main_window.h @@ -0,0 +1,49 @@ +#pragma once +#include "playback.h" +#include "util.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "preferences.h" +#include "aboutwindow.h" +#include "slider.hpp" +class LooperWindow : public QMainWindow { + Q_OBJECT; + bool labels_visible; + bool icons_visible; + LooperSlider *volume_slider; + LooperSlider *speed_slider; + LooperSlider *tempo_slider; + LooperSlider *pitch_slider; + std::thread *update_thread = nullptr; + bool done = false; + void Pulse(); + void ThreadFunc(); + const char *file_to_play = nullptr; + QPushButton *restart_btn; + QPushButton *stop_btn; + QPushButton *pause_resume_btn; + Playback *playback; + LooperSlider *slider; + QMenu *file_menu; + QAction *open_item; + QAction *prefs_item; + QAction *quit_item; + QMenu *help_menu; + QAction *about_item; + QFileDialog *file_dialog; + QBoxLayout *root_layout; + void update_label_setting(bool labels_visible, bool icons_visible); + public: + AboutWindow *about_window; + PrefsWindow *prefs_window; + explicit LooperWindow(Playback *playback); +}; diff --git a/backends/ui/qt/preferences.cpp b/backends/ui/qt/preferences.cpp new file mode 100644 index 0000000..b6a58b7 --- /dev/null +++ b/backends/ui/qt/preferences.cpp @@ -0,0 +1,95 @@ +#include "preferences.h" +#include +#include +#include +#include +using namespace Looper::Options; +PrefsWindow::PrefsWindow() { + auto *root_layout = new QBoxLayout(QBoxLayout::TopToBottom); + this->setLayout(root_layout); + restart_warning = new QLabel("A restart is needed to apply some changes."); + restart_warning->hide(); + root_layout->addWidget(restart_warning); + frontend_btn = new QPushButton(); + frontend_menu = new QMenu(); + for (auto &kv : UIBackend::backends) { + UIBackend *backend = kv.second; + const char *name = strdup(backend->get_name().c_str()); + QAction *action = new QAction(name); + action->connect(action, &QAction::triggered, [=,this]() { + this->new_frontend = backend->get_id(); + frontend_btn->setText(strdup(this->new_frontend.c_str())); + this->set_options_changed(true); + }); + frontend_menu->addAction(action); + frontend_options.push_back(action); + } + frontend_btn->setMenu(frontend_menu); + root_layout->addWidget(frontend_btn); + QFrame *frame = new QFrame(); + frame->setWindowTitle("Labels and Icons"); + auto *label_settings_group = new QBoxLayout(QBoxLayout::TopToBottom); + frame->setLayout(label_settings_group); + labels_only = new QRadioButton("Labels Only"); + labels_only->connect(labels_only, &QRadioButton::pressed, [=,this]() { + this->new_label_setting = "labels"; + this->set_options_changed(true); + }); + icons_only = new QRadioButton("Icons Only"); + icons_only->connect(icons_only, &QRadioButton::pressed, [=,this]() { + this->new_label_setting = "icons"; + this->set_options_changed(true); + }); + both_labels_icons = new QRadioButton("Both"); + both_labels_icons->connect(both_labels_icons, &QRadioButton::pressed, [=,this]() { + this->new_label_setting = "both"; + this->set_options_changed(true); + }); + label_settings_group->addWidget(labels_only); + label_settings_group->addWidget(icons_only); + label_settings_group->addWidget(both_labels_icons); + root_layout->addWidget(frame); + revert_btn = new QPushButton("Revert"); + QObject::connect(revert_btn, &QPushButton::pressed, this, &PrefsWindow::revert); + apply_btn = new QPushButton("Apply"); + QObject::connect(apply_btn, &QPushButton::pressed, this, &PrefsWindow::apply); + QWidget *btn_view = new QWidget(); + QBoxLayout *btn_box = new QBoxLayout(QBoxLayout::LeftToRight); + btn_view->setLayout(btn_box); + btn_box->addWidget(revert_btn); + btn_box->addWidget(apply_btn); + root_layout->addWidget(btn_view); + revert(); +} +void PrefsWindow::set_options_changed(bool changed) { + this->revert_btn->setEnabled(changed); + this->apply_btn->setEnabled(changed); +} +void PrefsWindow::update_label_setting() { + bool labels_enabled = true; + bool icons_enabled = true; + if (new_label_setting == "icons") { + labels_enabled = false; + } else if (new_label_setting == "labels") { + icons_enabled = false; + } + emit(settings_changed(labels_enabled, icons_enabled)); +} +void PrefsWindow::revert() { + set_options_changed(false); + new_label_setting = get_option("ui.label_setting", "icons"); + new_frontend = get_option("ui.frontend", "qt"); + if (new_frontend != "qt") restart_warning->show(); + else restart_warning->hide(); + frontend_btn->setText(new_frontend.c_str()); + update_label_setting(); +} +void PrefsWindow::apply() { + set_options_changed(false); + set_option("ui.label_setting", new_label_setting); + set_option("ui.frontend", new_frontend); + if (new_frontend != "qt") restart_warning->show(); + else restart_warning->hide(); + frontend_btn->setText(new_frontend.c_str()); + update_label_setting(); +} \ No newline at end of file diff --git a/backends/ui/qt/preferences.h b/backends/ui/qt/preferences.h new file mode 100644 index 0000000..5dad13a --- /dev/null +++ b/backends/ui/qt/preferences.h @@ -0,0 +1,35 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +class PrefsWindow : public QWidget { + Q_OBJECT; + std::vector backend_ids; + int cur_option = 0; + QLabel *restart_warning; + std::string new_label_setting; + std::string new_frontend; + QPushButton *frontend_btn; + QMenu *frontend_menu; + std::vector frontend_options; + QCheckBox *menu_icons; + QRadioButton *labels_only; + QRadioButton *icons_only; + QRadioButton *both_labels_icons; + QPushButton *revert_btn; + QPushButton *apply_btn; + void update_label_setting(); + void set_options_changed(bool changed); + void revert(); + void apply(); + public: + PrefsWindow(); + Q_SIGNALS: + void settings_changed(bool use_labels, bool use_icons); +}; \ No newline at end of file diff --git a/backends/ui/qt/slider.cpp b/backends/ui/qt/slider.cpp new file mode 100644 index 0000000..dba4f49 --- /dev/null +++ b/backends/ui/qt/slider.cpp @@ -0,0 +1,249 @@ +#include "slider.hpp" +LooperSlider::LooperSlider(const char *name, const char *label, double min, double max, double tick, bool logarithmic) : QWidget() { + root_layout = new QBoxLayout(QBoxLayout::LeftToRight, this); + text_layout_view = new QBoxLayout(QBoxLayout::TopToBottom); + QWidget *text_layout_widget = new QWidget(); + text_layout_widget->setLayout(text_layout_view); + root_layout->addWidget(text_layout_widget); + text_label = new QLabel(); + slider = new QSlider(Qt::Orientation::Horizontal); + slider->connect(slider, &QSlider::valueChanged, [=,this](int value) { + slider_changed = 3; + if (this->logarithmic) { + this->SetValue(scaler->unscale_log(value * tick)); + } else { + this->SetValue(value * tick); + } + }); + text_layout_view->addWidget(slider); + btn = new QPushButton(); + btn->connect(btn, &QPushButton::pressed, [=,this]() { + this->text_edit_mode = !this->text_edit_mode; + UpdateSlider(true); + }); + root_layout->addWidget(btn); + text = new QLineEdit(); + text->setVisible(false); + text_layout_view->addWidget(text); + text->connect(text, &QLineEdit::textChanged, [=,this]() { + const char *value = text->text().toLocal8Bit().constData(); + bool ok = true; + double output = this->Value(); + try { + output = std::stod(value); + } catch (std::exception) { + ok = false; + } + if (ok) { + textChanged = true; + this->SetValue(output); + } + }); + limits_view = new QBoxLayout(QBoxLayout::LeftToRight); + min_label_view = new QLabel(); + limits_view->addWidget(min_label_view, 0, Qt::Alignment::enum_type::AlignLeft); + max_label_view = new QLabel(); + limits_view->addWidget(max_label_view, 0, Qt::Alignment::enum_type::AlignRight); + scaler = new LooperLogScaler(min, max); + set_min(min); + set_max(max); + set_tick(tick); + set_logarithmic(logarithmic); + this->label = strdup(label); + this->value = min; + UpdateSlider(true); +} +double LooperSlider::Value() { + return value; +} +void LooperSlider::SetValue(double value) { + emit(changed(value)); + this->value = value; + this->UpdateSlider(true); +} +void LooperSlider::set_min(double min) { + settings_changed = true; + this->min = min; +} +void LooperSlider::set_max(double max) { + settings_changed = true; + this->max = max; +} +void LooperSlider::set_logarithmic(bool logarithmic) { + settings_changed = true; + this->logarithmic = true; +} +void LooperSlider::UpdateLogScaler() { + this->scaler->update_min_max(this->min, this->max); +} +void LooperSlider::SetMin(double min) { + this->set_min(min); + UpdateSlider(false); +} +void LooperSlider::SetMax(double max) { + this->set_max(max); + UpdateSlider(false); +} +double LooperSlider::Min() { + return this->min; +} +double LooperSlider::Max() { + return this->max; +} +void LooperSlider::SetLimits(double min, double max) { + set_min(min); + set_max(max); + UpdateSlider(false); +} +void LooperSlider::SetMinLabel(const char *label) { + if (this->min_label) free((void*)this->min_label); + this->min_label = strdup(label); + UpdateSlider(); +} +void LooperSlider::SetMaxLabel(const char *label) { + if (this->max_label) free((void*)this->max_label); + this->max_label = strdup(label); + UpdateSlider(); +} +void LooperSlider::SetLimitLabels(const char *min, const char *max) { + SetMinLabel(min); + SetMaxLabel(max); +} +LooperSlider::~LooperSlider() { + if (this->min_label) free((void*)this->min_label); + if (this->max_label) free((void*)this->max_label); + if (this->label) free((void*)this->label); +} +const char *LooperSlider::MinLabel() { + return this->min_label; +} +const char *LooperSlider::MaxLabel() { + return this->max_label; +} +void LooperSlider::SetLogarithmic(bool logarithmic) { + set_logarithmic(logarithmic); + UpdateSlider(false); +} +bool LooperSlider::IsLogarithmic() { + return this->logarithmic; +} +void LooperSlider::SetLabel(const char *label) { + if (this->label) free((void*)this->label); + this->label = strdup(label); +} +const char *LooperSlider::Label() { + return this->label; +} +double LooperSlider::Tick() { + return this->tick; +} +void LooperSlider::SetTick(double value) { + set_tick(value); + UpdateSlider(false); +} +void LooperSlider::set_tick(double value) { + this->settings_changed = true; + this->tick = value; +} +void LooperSlider::UpdateSlider(bool update_value) { + UpdateLogScaler(); + if (this->min_label == NULL) { + this->min_label_view->setText(""); + } else { + this->min_label_view->setText(this->min_label); + } + if (this->max_label == NULL) { + this->max_label_view->setText(""); + } else { + this->max_label_view->setText(this->max_label); + } + limits_visible = (this->min_label == NULL && this->max_label == NULL); + if (limits_visible) { + if (!limits_actually_visible) { + this->text_layout_view->addItem(this->limits_view); + limits_actually_visible = true; + } + } else { + if (limits_actually_visible) { + this->text_layout_view->removeItem(this->limits_view); + limits_actually_visible = false; + } + } + if (this->label == NULL) { + if (text_label_visible) { + this->text_layout_view->removeWidget(this->text_label); + text_label_visible = false; + } + } else { + this->text_label->setText(this->label); + if (!text_label_visible) { + this->text_layout_view->insertWidget(0, this->text_label); + text_label_visible = true; + } + } + if (this->text_edit_mode) { + if (!text_editor_visible) { + this->text_layout_view->insertWidget(1, this->text); + text_editor_visible = true; + } + if (slider_visible) { + this->text_layout_view->removeWidget(this->slider); + slider_visible = false; + } + } else { + if (!slider_visible) { + this->text_layout_view->insertWidget(1, this->slider); + slider_visible = true; + } + if (text_editor_visible) { + this->text_layout_view->removeWidget(this->text); + text_editor_visible = false; + } + } + auto icon = QIcon::fromTheme(text_edit_mode ? "slider_edit_mode" : "text_edit_mode", QIcon::fromTheme(text_edit_mode ? "dialog-ok" : "edit")); + auto text = text_edit_mode ? "Imprecise" : "Precise"; + if (icon.isNull() || show_button_text) { + this->btn->setText(text); + } else { + this->btn->setText(""); + } + if (show_button_icon && !icon.isNull()) { + this->btn->setIcon(icon); + } else { + QIcon emptyIcon; + this->btn->setIcon(emptyIcon); + } + this->slider->setVisible(slider_visible); + this->text->setVisible(text_editor_visible); + this->text_label->setVisible(text_label_visible); + this->min_label_view->setVisible(limits_actually_visible); + this->max_label_view->setVisible(limits_actually_visible); + if (update_value) { + if (textChanged) { + textChanged = false; + } else { + this->text->setText(fmt::to_string(this->value).c_str()); + } + if (slider_changed == 0) slider_changed = 1; + if ((slider_changed--) == 0) { + this->slider->setValue(this->logarithmic ? this->scaler->scale_log(this->value / this->tick) : (this->value / this->tick)); + this->slider->setMinimum(this->min / tick); + this->slider->setMaximum(this->max / tick); + } + } +} + +bool LooperSlider::ShowButtonText() { + return show_button_text; +} +bool LooperSlider::ShowButtonIcon() { + return show_button_icon; +} +void LooperSlider::SetShowButtonIcon(bool enable) { + this->show_button_icon = enable; + UpdateSlider(); +} +void LooperSlider::SetShowButtonText(bool enable) { + this->show_button_text = enable; + UpdateSlider(); +} \ No newline at end of file diff --git a/backends/ui/qt/slider.hpp b/backends/ui/qt/slider.hpp new file mode 100644 index 0000000..5d5ad50 --- /dev/null +++ b/backends/ui/qt/slider.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +class LooperSlider : public QWidget { + Q_OBJECT; + QSlider *slider; + QPushButton *btn; + QLineEdit *text; + bool show_button_text; + bool show_button_icon; + QLabel *text_label; + QLabel *min_label_view; + QLabel *max_label_view; + QBoxLayout *limits_view; + QBoxLayout *root_layout; + bool limits_visible = false; + bool settings_changed = false; + QBoxLayout *text_layout_view; + bool pressed = false; + LooperLogScaler *scaler; + void UpdateLogScaler(); + void UpdateSlider(bool update_value = false); + bool textChanged = false; + bool text_editor_visible = false; + bool slider_visible = false; + bool text_label_visible = false; + bool limits_actually_visible = false; + double value; + double tick; + double min; + double max; + bool text_edit_mode = false; + bool logarithmic; + int slider_changed = 0; + const char *label = NULL; + const char *min_label = NULL; + const char *max_label = NULL; + void set_min(double min); + void set_max(double max); + void set_tick(double value); + void set_logarithmic(bool logarithmic); + public: + void SetMin(double min); + double Min(); + void SetMax(double max); + double Max(); + void SetLimits(double min, double max); + void SetMinLabel(const char *label); + void SetMaxLabel(const char *label); + const char *MinLabel(); + const char *MaxLabel(); + void SetLimitLabels(const char *min, const char *max); + void SetValue(double value); + double Value(); + void SetLogarithmic(bool logarithmic); + bool IsLogarithmic(); + void SetLabel(const char *label); + const char *Label(); + double Tick(); + bool ShowButtonText(); + bool ShowButtonIcon(); + void SetShowButtonText(bool enable); + void SetShowButtonIcon(bool enable); + void SetTick(double value); + explicit LooperSlider(const char *name, const char *label, double min, double max, double tick = 0.0001, bool logarithmic = false); + ~LooperSlider(); + Q_SIGNALS: + void changed(double value); +}; diff --git a/backends/ui/qt/ui.json b/backends/ui/qt/ui.json new file mode 100644 index 0000000..a512fc3 --- /dev/null +++ b/backends/ui/qt/ui.json @@ -0,0 +1,4 @@ +{ + "class_name": "QtUIBackend", + "include_path": "main.h" +} diff --git a/cmake/built_sdl/SDL3Config.cmake b/cmake/built_sdl/SDL3Config.cmake new file mode 100644 index 0000000..698cf1b --- /dev/null +++ b/cmake/built_sdl/SDL3Config.cmake @@ -0,0 +1,43 @@ +# sdl2 cmake project-config input for CMakeLists.txt script + +include(FeatureSummary) +set_package_properties(SDL3 PROPERTIES + URL "https://www.libsdl.org/" + DESCRIPTION "low level access to audio, keyboard, mouse, joystick, and graphics hardware" +) + + +######################################################################## + +set(SDL3_FOUND TRUE CACHE INTERNAL "") + +set(SDL3_SDL3_FOUND TRUE CACHE INTERNAL "") +set(SDL3_SDL3-static_FOUND TRUE CACHE INTERNAL "") +set(SDL3_SDL3test_FOUND OFF CACHE INTERNAL "") +# Find SDL3::Headers +if(NOT TARGET SDL3::Headers) + add_library(SDL3::Headers ALIAS SDL3_Headers) +endif() +set(SDL3_Headers_FOUND TRUE) +if (NOT DEFINED SDL3_FOUND) + if (SDL2::SDL3main) + set(SDL3_SDL3main_FOUND ON) + else() + set(SDL3_SDL3main_FOUND OFF) + endif() + set(SDL3_LIBRARY SDL3::SDL3 CACHE INTERNAL "") + set(SDL3_INCLUDE_DIR CACHE INTERNAL "") + set(SDL3_LIBRARIES SDL3::SDL3 CACHE INTERNAL "") + set(SDL3_STATIC_LIBRARIES SDL3::SDL3 CACHE INTERNAL "") + set(SDL3_STATIC_PRIVATE_LIBS "" CACHE INTERNAL "") + set(SDL3_INCLUDE_DIRS "" CACHE INTERNAL "") + #get_target_property(SDL2_STATIC_PRIVATE_LIBS SDL2-static LINK_LIBRARIES) + get_target_property(_SDL3_INCLUDE_DIRS SDL3::Headers INCLUDE_DIRECTORIES) + set(SDL3_INCLUDE_DIRS ${_SDL3_INCLUDE_DIRS} CACHE INTERNAL "") + unset(_SDL3_INCLUDE_DIRS) + if(SDL3_SDL3main_FOUND) + set(SDL3MAIN_LIBRARY SDL3::SDL3main CACHE INTERNAL "") + else() + set(SDL3MAIN_LIBRARY CACHE INTERNAL "") + endif() +endif() \ No newline at end of file diff --git a/include/playback.h b/include/playback.h new file mode 100644 index 0000000..01811df --- /dev/null +++ b/include/playback.h @@ -0,0 +1,50 @@ +#pragma once +#include +#include +#include +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 +}; +G_BEGIN_DECLS +#define LOOPER_TYPE_STREAM looper_stream_get_type() +#define LOOPER_TYPE_PLAYBACK looper_playback_get_type() +G_DECLARE_FINAL_TYPE(LooperStream, looper_stream, LOOPER, STREAM, GObject) +G_DECLARE_FINAL_TYPE(LooperPlayback, looper_playback, LOOPER, PLAYBACK, GObject) + +LooperPlayback *looper_playback_new(void); +void looper_playback_load(LooperPlayback *self, const gchar *file_path); +void looper_playback_start(LooperPlayback *self); +void looper_playback_set_stream_idx(LooperPlayback *self, gint stream_idx); +bool looper_playback_is_paused(LooperPlayback *self); +void looper_playback_seek(LooperPlayback *self, gdouble position); +gdouble looper_playback_get_length(LooperPlayback *self); +gdouble looper_playback_get_position(LooperPlayback *self); +const gchar *looper_stream_get_name(LooperStream *self); +gdouble looper_stream_get_length(LooperStream *self); +gint looper_stream_get_id(LooperStream *self); +void looper_stream_set_name(LooperStream *self, const gchar *name); +void looper_stream_set_length(LooperStream *self, gdouble length); +void looper_stream_set_id(LooperStream *self, gint id); +GListStore *looper_playback_get_streams(LooperPlayback *self); +G_END_DECLS \ No newline at end of file diff --git a/sdl-android-project/.idea/other.xml b/sdl-android-project/.idea/other.xml new file mode 100644 index 0000000..49481ad --- /dev/null +++ b/sdl-android-project/.idea/other.xml @@ -0,0 +1,329 @@ + + + + + + \ No newline at end of file diff --git a/subprojects/fmt b/subprojects/fmt index 720da57..0379bf3 160000 --- a/subprojects/fmt +++ b/subprojects/fmt @@ -1 +1 @@ -Subproject commit 720da57baba83b3b1829e20133575e57aa1a8a4f +Subproject commit 0379bf3a5d52d8542aec1874677c9df5ff9ba5f9 diff --git a/subprojects/googletest b/subprojects/googletest index d144031..6dae7eb 160000 --- a/subprojects/googletest +++ b/subprojects/googletest @@ -1 +1 @@ -Subproject commit d144031940543e15423a25ae5a8a74141044862f +Subproject commit 6dae7eb4a5c3a169f3e298392bff4680224aa94a diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..c5feb78 --- /dev/null +++ b/test.sh @@ -0,0 +1,17 @@ +#!/bin/bash +source venv/bin/activate +PIDFILE="$(mktemp)" +looper() { + echo $$ > "$PIDFILE" + ./build/default/looper -m + rm "$PIDFILE" -f +} +./build.sh || exit $? +looper & +tl & +while [ -f "$PIDFILE" ]; do + sleep 1 +done +for i in $(jobs -p); do + kill -HUP $i +done \ No newline at end of file diff --git a/util.cpp b/util.cpp index 4489dab..4850352 100644 --- a/util.cpp +++ b/util.cpp @@ -245,3 +245,21 @@ PropertyHint make_hint(std::optional min = {}, std::optional max } return output; } + +LooperLogScaler::LooperLogScaler(double min, double max) { + update_min_max(min, max); +} +void LooperLogScaler::update_min_max(double min, double max) { + x0 = min; + x1 = (max - min) / (exp(1.0) - 1.0); + la = min; + lb = (max - min); +// x0 = scale_log(min); +// x1 = scale_log(max); +} +double LooperLogScaler::scale_log(double value) { + return (std::log(((value - x0) / x1) + 1.0) * lb) + la; +} +double LooperLogScaler::unscale_log(double value) { + return ((std::exp((value - la) / lb) - 1.0) * x1) + x0; +} diff --git a/util.hpp b/util.hpp index e8fc9d2..2eadba1 100644 --- a/util.hpp +++ b/util.hpp @@ -563,3 +563,15 @@ PropertyHint make_hint(double min, double max); Property make_property(PropertyType type, std::string name, PropertyId id, std::optional hint); Property make_property(PropertyType type, std::string name, std::string path, std::optional hint = {}); Property make_property(PropertyType type, std::string name, PropertyId id = PropertyId::BackendSpecific, std::optional path = {}, std::optional hint = {}); + +class LooperLogScaler { + double la; + double lb; + public: + double x0; + double x1; + void update_min_max(double min, double max); + LooperLogScaler(double min, double max); + double scale_log(double value); + double unscale_log(double value); +};