Add QT6 frontend
Some checks failed
Build / build-gentoo (push) Failing after 1m25s
Build / download-system-deps (push) Successful in 3m44s
Build / get-source-code (push) Successful in 6m52s
Build / build-appimage (push) Successful in 3m29s
Build / build-android (push) Failing after 2m52s
Build / build-windows (push) Failing after 6m42s

This commit is contained in:
Zachary Hall 2024-11-20 08:15:45 -08:00
parent 8fdf30878b
commit ae74999276
28 changed files with 1404 additions and 44 deletions

View file

@ -1,2 +0,0 @@
CompileFlags:
CompilationDatabase: "./build/compile_commands.json"

20
.vscode/c_cpp_properties.json vendored Normal file
View file

@ -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
}

0
.zed/settings.json Normal file
View file

View file

@ -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})
@ -159,9 +157,13 @@ add_subdirectory(subprojects/SDL-Mixer-X)
add_subdirectory(subprojects/vgmstream)
if (DEFINED EMSCRIPTEN)
set(EXTRA_LIBS )
else()
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)
else()
@ -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)

9
audio_output_backend.hpp Normal file
View file

@ -0,0 +1,9 @@
#pragma once
#include <string>
#include <map>
#include "backend.hpp"
class AudioOutputBackend {
BACKEND_TYPE(AudioOutputBackend);
protected:
};

View file

@ -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);

View file

@ -14,18 +14,8 @@
#include "slider.h"
#include "prefs.h"
#include <atomic>
#include <util.hpp>
#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;

View file

@ -107,7 +107,7 @@ void PrefsWindow::MessageReceived(BMessage *msg) {
case CMD_REVERT: {
set_options_changed(false);
new_label_setting = get_option<std::string>("ui.haiku.label_setting", "icons");
new_frontend = get_option<std::string>("ui.frontend", "haiki");
new_frontend = get_option<std::string>("ui.frontend", "haiku");
if (new_frontend != "haiku") restart_warning->Show();
else restart_warning->Hide();
for (size_t i = 0; i < backend_ids.size(); i++) {

View file

@ -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})

View file

@ -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));
}

View file

@ -0,0 +1,33 @@
#pragma once
#include <QWidget>
#include <QTextBrowser>
#include <string>
#include <QListView>
#include <QAbstractListModel>
#include <QList>
#include <QBoxLayout>
#include <QSplitter>
#include <QLabel>
#include <license.hpp>
class LicenseModel : public QAbstractListModel {
Q_OBJECT;
std::vector<LicenseData> 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();
};

19
backends/ui/qt/main.cpp Normal file
View file

@ -0,0 +1,19 @@
#include "main.h"
#include "main_window.h"
#include <QMainWindow>
#include <QWindow>
#include <QGuiApplication>
#include <QApplication>
std::string QtUIBackend::get_id() {
return "qt";
}
std::string QtUIBackend::get_name() {
return "QT";
}
int QtUIBackend::run(std::vector<std::string> args, int argc, char **argv) {
UIBackend::run(args, argc, argv);
QApplication app(argc, argv);
LooperWindow window(this->playback);
window.show();
return app.exec();
}

10
backends/ui/qt/main.h Normal file
View file

@ -0,0 +1,10 @@
#pragma once
#include <backend.hpp>
#include <string>
#include <vector>
class QtUIBackend : public UIBackend {
public:
std::string get_id() override;
std::string get_name() override;
int run(std::vector<std::string> args, int argc, char **argv) override;
};

View file

@ -0,0 +1,167 @@
#include "main_window.h"
#include <QMenuBar>
#include "preferences.h"
void LooperWindow::Pulse() {
auto len = playback->GetLength();
auto pos = playback->GetPosition();
this->slider->SetLimits(0.0, len);
if (!this->slider->IsPressed()) 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 && len >= 0.0001);
pause_resume_btn->setEnabled(enable_ui);
stop_btn->setEnabled(enable_ui);
restart_btn->setEnabled(enable_ui);
auto volume = playback->GetVolume();
auto pitch = playback->GetPitch();
auto speed = playback->GetSpeed();
auto tempo = playback->GetTempo();
if (!volume_slider->IsPressed()) volume_slider->SetValue(volume);
if (!pitch_slider->IsPressed()) pitch_slider->SetValue(pitch);
if (!speed_slider->IsPressed()) speed_slider->SetValue(speed);
if (!tempo_slider->IsPressed()) 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);
slider->DisableModeButton();
slider->UseSlider();
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");
QObject::connect(volume_slider, &LooperSlider::changed, [=,this](double value) {
playback->SetVolume(value);
});
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);
}
}
}

View file

@ -0,0 +1,49 @@
#pragma once
#include "playback.h"
#include "util.hpp"
#include <QMainWindow>
#include <QSlider>
#include <QWidget>
#include <QPushButton>
#include <QTextEdit>
#include <QBoxLayout>
#include <QMenu>
#include <QTimer>
#include <QFileDialog>
#include <QApplication>
#include <util.hpp>
#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);
};

View file

@ -0,0 +1,95 @@
#include "preferences.h"
#include <QMenu>
#include <QBoxLayout>
#include <backend.hpp>
#include <options.hpp>
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<std::string>("ui.label_setting", "icons");
new_frontend = get_option<std::string>("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<std::string>("ui.label_setting", new_label_setting);
set_option<std::string>("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();
}

View file

@ -0,0 +1,35 @@
#pragma once
#include <QWindow>
#include <QPushButton>
#include <QCheckBox>
#include <QRadioButton>
#include <QBoxLayout>
#include <QLabel>
#include <vector>
#include <string>
#include <map>
class PrefsWindow : public QWidget {
Q_OBJECT;
std::vector<std::string> 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<QAction*> 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);
};

278
backends/ui/qt/slider.cpp Normal file
View file

@ -0,0 +1,278 @@
#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 _looperSlider(Qt::Orientation::Horizontal);
slider->connect(slider, &_looperSlider::mousePressed, [=,this]() {
pressed = true;
slider_value_changed_after_release = false;
emit(mousePressed());
});
slider->connect(slider, &_looperSlider::mouseReleased, [=,this]() {
emit(mouseReleased());
pressed = false;
});
slider->connect(slider, &QSlider::valueChanged, [=,this](int value) {
if (slider_value_updating) return;
double actual_value = value * tick;
if (logarithmic) actual_value = scaler->unscale_log(actual_value);
this->SetValue(actual_value);
emit(changed(actual_value));
slider_value_changed_after_release = true;
});
text_layout_view->addWidget(slider);
btn = new QPushButton();
btn->connect(btn, &QPushButton::pressed, [=,this]() {
SwitchModes();
});
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;
emit(changed(output));
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);
}
bool LooperSlider::IsPressed() {
return pressed;
}
double LooperSlider::Value() {
return value;
}
void LooperSlider::set_value(double value) {
if (!pressed && slider_value_changed_after_release) this->value = value;
this->UpdateSlider(true);
}
void LooperSlider::SetValue(double value) {
set_value(value);
}
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::SetMode(bool use_text_editor) {
this->text_edit_mode = use_text_editor;
UpdateSlider(true);
}
void LooperSlider::EnableModeButton(bool enabled) {
btn->setVisible(enabled);
}
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 {
if (!pressed && !text_edit_mode) this->text->setText(fmt::to_string(this->value).c_str());
}
slider_value_updating = true;
if (!pressed && slider_value_changed_after_release) {
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);
}
slider_value_updating = false;
}
}
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();
}
void LooperSlider::changeEvent(QEvent *event) {
if (event->type() == QEvent::EnabledChange) {
this->slider->setEnabled(isEnabled());
this->text->setEnabled(isEnabled());
}
}

119
backends/ui/qt/slider.hpp Normal file
View file

@ -0,0 +1,119 @@
#pragma once
#include <QSlider>
#include <QWidget>
#include <QPushButton>
#include <QLineEdit>
#include <QBoxLayout>
#include <QLabel>
#include <QEvent>
#include <util.hpp>
class _looperSlider : public QSlider {
Q_OBJECT;
protected:
inline void mousePressEvent(QMouseEvent *e) override {
QSlider::mousePressEvent(e);
emit(mousePressed());
}
inline void mouseReleaseEvent(QMouseEvent *e) override {
QSlider::mouseReleaseEvent(e);
emit(mouseReleased());
}
public:
inline _looperSlider(Qt::Orientation orientation, QWidget *parent = nullptr) : QSlider(orientation, parent) { }
Q_SIGNALS:
void mousePressed();
void mouseReleased();
};
class LooperSlider : public QWidget {
Q_OBJECT;
_looperSlider *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 slider_value_updating = false;
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 slider_value_changed_after_release = true;
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;
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);
void set_value(double value);
public:
bool IsPressed();
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);
void EnableModeButton(bool enabled = true);
inline void DisableModeButton() {
EnableModeButton(false);
}
void SetMode(bool use_text_editor);
inline void UseTextEditor() {
SetMode(true);
}
inline void UseSlider() {
SetMode(false);
}
inline bool UsingTextEditor() {
return text_edit_mode;
}
inline bool UsingSlider() {
return !UsingTextEditor();
}
inline void SwitchModes() {
SetMode(!text_edit_mode);
}
explicit LooperSlider(const char *name, const char *label, double min, double max, double tick = 0.0001, bool logarithmic = false);
~LooperSlider();
void changeEvent(QEvent *event) override;
Q_SIGNALS:
void changed(double value);
void mousePressed();
void mouseReleased();
};

4
backends/ui/qt/ui.json Normal file
View file

@ -0,0 +1,4 @@
{
"class_name": "QtUIBackend",
"include_path": "main.h"
}

View file

@ -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()

50
include/playback.h Normal file
View file

@ -0,0 +1,50 @@
#pragma once
#include <glib-object.h>
#include <glib.h>
#include <gio/gliststore.h>
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

View file

@ -0,0 +1,329 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="direct_access_persist.xml">
<option name="deviceSelectionList">
<list>
<PersistentDeviceSelectionData>
<option name="api" value="27" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="F01L" />
<option name="id" value="F01L" />
<option name="manufacturer" value="FUJITSU" />
<option name="name" value="F-01L" />
<option name="screenDensity" value="360" />
<option name="screenX" value="720" />
<option name="screenY" value="1280" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="28" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="SH-01L" />
<option name="id" value="SH-01L" />
<option name="manufacturer" value="SHARP" />
<option name="name" value="AQUOS sense2 SH-01L" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="Lenovo" />
<option name="codename" value="TB370FU" />
<option name="id" value="TB370FU" />
<option name="manufacturer" value="Lenovo" />
<option name="name" value="Tab P12" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1840" />
<option name="screenY" value="2944" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />
<option name="codename" value="a51" />
<option name="id" value="a51" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy A51" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="akita" />
<option name="id" value="akita" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="b0q" />
<option name="id" value="b0q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S22 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="32" />
<option name="brand" value="google" />
<option name="codename" value="bluejay" />
<option name="id" value="bluejay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="caiman" />
<option name="id" value="caiman" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro" />
<option name="screenDensity" value="360" />
<option name="screenX" value="960" />
<option name="screenY" value="2142" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="comet" />
<option name="id" value="comet" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro Fold" />
<option name="screenDensity" value="390" />
<option name="screenX" value="2076" />
<option name="screenY" value="2152" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="29" />
<option name="brand" value="samsung" />
<option name="codename" value="crownqlteue" />
<option name="id" value="crownqlteue" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Note9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2220" />
<option name="screenY" value="1080" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="dm3q" />
<option name="id" value="dm3q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S23 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="e1q" />
<option name="id" value="e1q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S24" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix_camera" />
<option name="id" value="felix_camera" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold (Camera-enabled)" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="gts8uwifi" />
<option name="id" value="gts8uwifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S8 Ultra" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1848" />
<option name="screenY" value="2960" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="husky" />
<option name="id" value="husky" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8 Pro" />
<option name="screenDensity" value="390" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="motorola" />
<option name="codename" value="java" />
<option name="id" value="java" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="G20" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="komodo" />
<option name="id" value="komodo" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro XL" />
<option name="screenDensity" value="360" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="lynx" />
<option name="id" value="lynx" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="google" />
<option name="codename" value="oriole" />
<option name="id" value="oriole" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="panther" />
<option name="id" value="panther" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q5q" />
<option name="id" value="q5q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold5" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1812" />
<option name="screenY" value="2176" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q6q" />
<option name="id" value="q6q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-F956B" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1856" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="r11" />
<option name="id" value="r11" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Watch" />
<option name="screenDensity" value="320" />
<option name="screenX" value="384" />
<option name="screenY" value="384" />
<option name="type" value="WEAR_OS" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="redfin" />
<option name="id" value="redfin" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 5" />
<option name="screenDensity" value="440" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="shiba" />
<option name="id" value="shiba" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="tangorpro" />
<option name="id" value="tangorpro" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Tablet" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="tokay" />
<option name="id" value="tokay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
</list>
</option>
</component>
</project>

@ -1 +1 @@
Subproject commit 720da57baba83b3b1829e20133575e57aa1a8a4f
Subproject commit 0379bf3a5d52d8542aec1874677c9df5ff9ba5f9

@ -1 +1 @@
Subproject commit d144031940543e15423a25ae5a8a74141044862f
Subproject commit 6dae7eb4a5c3a169f3e298392bff4680224aa94a

17
test.sh Executable file
View file

@ -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

View file

@ -245,3 +245,21 @@ PropertyHint make_hint(std::optional<double> min = {}, std::optional<double> 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;
}

View file

@ -563,3 +563,15 @@ PropertyHint make_hint(double min, double max);
Property make_property(PropertyType type, std::string name, PropertyId id, std::optional<PropertyHint> hint);
Property make_property(PropertyType type, std::string name, std::string path, std::optional<PropertyHint> hint = {});
Property make_property(PropertyType type, std::string name, PropertyId id = PropertyId::BackendSpecific, std::optional<std::string> path = {}, std::optional<PropertyHint> 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);
};