native control panel app
|
|
@ -50,3 +50,7 @@ Module.symvers
|
||||||
Mkfile.old
|
Mkfile.old
|
||||||
dkms.conf
|
dkms.conf
|
||||||
*.ko
|
*.ko
|
||||||
|
*.AppImage
|
||||||
|
/tascam_controls/AppDir
|
||||||
|
*.cmake
|
||||||
|
/tascam_controls/build
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
project(TascamControlPanel LANGUAGES CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_AUTOMOC ON)
|
||||||
|
set(CMAKE_AUTORCC ON)
|
||||||
|
set(CMAKE_AUTOUIC ON)
|
||||||
|
|
||||||
|
find_package(Qt6 6.2 COMPONENTS Widgets REQUIRED)
|
||||||
|
find_package(ALSA REQUIRED)
|
||||||
|
|
||||||
|
add_executable(TascamControlPanel
|
||||||
|
src/main.cpp
|
||||||
|
src/mainwindow.h
|
||||||
|
src/mainwindow.cpp
|
||||||
|
src/alsacontroller.h
|
||||||
|
src/alsacontroller.cpp
|
||||||
|
resources/resources.qrc
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(TascamControlPanel PRIVATE
|
||||||
|
Qt6::Widgets
|
||||||
|
ALSA::ALSA
|
||||||
|
)
|
||||||
|
|
||||||
|
install(TARGETS TascamControlPanel
|
||||||
|
RUNTIME DESTINATION bin
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
APP_NAME="TascamControlPanel"
|
||||||
|
PROJECT_DIR=$(pwd)
|
||||||
|
BUILD_DIR="${PROJECT_DIR}/build"
|
||||||
|
|
||||||
|
TOOLS_DIR="${PROJECT_DIR}/.tools"
|
||||||
|
LINUXDEPLOY_FILENAME="linuxdeploy-x86_64.AppImage"
|
||||||
|
LINUXDEPLOY_PATH="${TOOLS_DIR}/${LINUXDEPLOY_FILENAME}"
|
||||||
|
LINUXDEPLOY_URL="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
|
||||||
|
|
||||||
|
|
||||||
|
echo "--- Checking for linuxdeploy tool ---"
|
||||||
|
if [ ! -f "${LINUXDEPLOY_PATH}" ]; then
|
||||||
|
echo "linuxdeploy not found. Downloading..."
|
||||||
|
mkdir -p "${TOOLS_DIR}"
|
||||||
|
wget -O "${LINUXDEPLOY_PATH}" "${LINUXDEPLOY_URL}"
|
||||||
|
echo "Making linuxdeploy executable..."
|
||||||
|
chmod +x "${LINUXDEPLOY_PATH}"
|
||||||
|
else
|
||||||
|
echo "linuxdeploy found at ${LINUXDEPLOY_PATH}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
echo "--- Building the C++ application ---"
|
||||||
|
mkdir -p ${BUILD_DIR}
|
||||||
|
cd ${BUILD_DIR}
|
||||||
|
cmake ..
|
||||||
|
make -j$(nproc)
|
||||||
|
cd ${PROJECT_DIR}
|
||||||
|
|
||||||
|
|
||||||
|
echo "--- Running linuxdeploy to create the AppImage ---"
|
||||||
|
rm -rf AppDir
|
||||||
|
|
||||||
|
"${LINUXDEPLOY_PATH}" --appdir AppDir \
|
||||||
|
-e "${BUILD_DIR}/${APP_NAME}" \
|
||||||
|
-i "${PROJECT_DIR}/resources/tascam-control-panel.png" \
|
||||||
|
-d "${PROJECT_DIR}/tascam-control-panel.desktop" \
|
||||||
|
--output appimage
|
||||||
|
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- DONE ---"
|
||||||
|
echo "AppImage created successfully!"
|
||||||
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
|
|
@ -0,0 +1,9 @@
|
||||||
|
<!DOCTYPE RCC>
|
||||||
|
<RCC version="1.0">
|
||||||
|
<qresource prefix="/">
|
||||||
|
<file>bg.png</file>
|
||||||
|
<file>logo.png</file>
|
||||||
|
<file>device.png</file>
|
||||||
|
<file>tascam-control-panel.png</file>
|
||||||
|
</qresource>
|
||||||
|
</RCC>
|
||||||
|
After Width: | Height: | Size: 349 KiB |
|
|
@ -0,0 +1,119 @@
|
||||||
|
#include "alsacontroller.h"
|
||||||
|
#include <alsa/asoundlib.h>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
AlsaController::AlsaController(const std::string& target_card_name)
|
||||||
|
{
|
||||||
|
int card = -1;
|
||||||
|
if (snd_card_next(&card) < 0 || card < 0) {
|
||||||
|
std::cerr << "No sound cards found." << std::endl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (card >= 0) {
|
||||||
|
char* long_name = nullptr;
|
||||||
|
snd_card_get_longname(card, &long_name);
|
||||||
|
if (long_name && std::string(long_name).find(target_card_name) != std::string::npos) {
|
||||||
|
m_card_num = card;
|
||||||
|
m_card_id_str = "hw:" + std::to_string(card);
|
||||||
|
m_card_found = true;
|
||||||
|
free(long_name);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (long_name) free(long_name);
|
||||||
|
|
||||||
|
if (snd_card_next(&card) < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_card_found) {
|
||||||
|
std::cerr << "Target sound card '" << target_card_name << "' not found." << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> AlsaController::getCardId() const {
|
||||||
|
if (m_card_found) {
|
||||||
|
return m_card_id_str;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
int AlsaController::getCardNumber() const {
|
||||||
|
return m_card_num;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AlsaController::isCardFound() const {
|
||||||
|
return m_card_found;
|
||||||
|
}
|
||||||
|
|
||||||
|
long AlsaController::getControlValue(const std::string& control_name) {
|
||||||
|
if (!m_card_found) return 0;
|
||||||
|
|
||||||
|
snd_ctl_t *handle;
|
||||||
|
if (snd_ctl_open(&handle, m_card_id_str.c_str(), 0) < 0) return 0;
|
||||||
|
|
||||||
|
snd_ctl_elem_id_t *id;
|
||||||
|
snd_ctl_elem_value_t *control;
|
||||||
|
snd_ctl_elem_id_alloca(&id);
|
||||||
|
snd_ctl_elem_value_alloca(&control);
|
||||||
|
|
||||||
|
snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER);
|
||||||
|
snd_ctl_elem_id_set_name(id, control_name.c_str());
|
||||||
|
|
||||||
|
snd_ctl_elem_value_set_id(control, id);
|
||||||
|
|
||||||
|
if (snd_ctl_elem_read(handle, control) < 0) {
|
||||||
|
snd_ctl_close(handle);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
long value = snd_ctl_elem_value_get_integer(control, 0);
|
||||||
|
snd_ctl_close(handle);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AlsaController::setControlValue(const std::string& control_name, long value) {
|
||||||
|
if (!m_card_found) return false;
|
||||||
|
|
||||||
|
snd_ctl_t *handle;
|
||||||
|
if (snd_ctl_open(&handle, m_card_id_str.c_str(), 0) < 0) return false;
|
||||||
|
|
||||||
|
snd_ctl_elem_id_t *id;
|
||||||
|
snd_ctl_elem_value_t *control;
|
||||||
|
snd_ctl_elem_id_alloca(&id);
|
||||||
|
snd_ctl_elem_value_alloca(&control);
|
||||||
|
|
||||||
|
snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER);
|
||||||
|
snd_ctl_elem_id_set_name(id, control_name.c_str());
|
||||||
|
snd_ctl_elem_value_set_id(control, id);
|
||||||
|
|
||||||
|
if (snd_ctl_elem_read(handle, control) < 0) {
|
||||||
|
snd_ctl_close(handle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
snd_ctl_elem_value_set_integer(control, 0, value);
|
||||||
|
|
||||||
|
if (snd_ctl_elem_write(handle, control) < 0) {
|
||||||
|
snd_ctl_close(handle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
snd_ctl_close(handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string AlsaController::readSysfsAttr(const std::string& attr_name) {
|
||||||
|
if (!m_card_found) return "N/A";
|
||||||
|
|
||||||
|
std::string path = "/sys/class/sound/card" + std::to_string(m_card_num) + "/device/" + attr_name;
|
||||||
|
std::ifstream file(path);
|
||||||
|
if (file.is_open()) {
|
||||||
|
std::string line;
|
||||||
|
std::getline(file, line);
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
return "N/A";
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
#ifndef ALSACONTROLLER_H
|
||||||
|
#define ALSACONTROLLER_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
class AlsaController
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AlsaController(const std::string& target_card_name = "US-144MKII");
|
||||||
|
|
||||||
|
std::optional<std::string> getCardId() const;
|
||||||
|
int getCardNumber() const;
|
||||||
|
bool isCardFound() const;
|
||||||
|
|
||||||
|
long getControlValue(const std::string& control_name);
|
||||||
|
bool setControlValue(const std::string& control_name, long value);
|
||||||
|
std::string readSysfsAttr(const std::string& attr_name);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string m_card_id_str; // e.g., "hw:0"
|
||||||
|
int m_card_num = -1;
|
||||||
|
bool m_card_found = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
#include <QApplication>
|
||||||
|
#include "mainwindow.h"
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
QApplication app(argc, argv);
|
||||||
|
|
||||||
|
MainWindow window;
|
||||||
|
window.show();
|
||||||
|
|
||||||
|
return app.exec();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,244 @@
|
||||||
|
#include "mainwindow.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QGridLayout>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QIcon>
|
||||||
|
|
||||||
|
const QString DARK_STYLESHEET = R"(
|
||||||
|
QWidget {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #DAE0ED;
|
||||||
|
font-family: Arial;
|
||||||
|
}
|
||||||
|
QLabel {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
QLabel#Title {
|
||||||
|
font-size: 15pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
QLabel#SectionHeader {
|
||||||
|
font-size: 11pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #92E8FF;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
QLabel#ControlLabel {
|
||||||
|
font-size: 9pt;
|
||||||
|
color: #CBD2E6;
|
||||||
|
}
|
||||||
|
QComboBox {
|
||||||
|
background-color: rgba(10, 10, 20, 0.25);
|
||||||
|
border: 1px solid #3A4760;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
color: #DAE0ED;
|
||||||
|
}
|
||||||
|
QComboBox:hover {
|
||||||
|
background-color: rgba(15, 15, 25, 0.35);
|
||||||
|
border: 1px solid #6482B4;
|
||||||
|
}
|
||||||
|
QComboBox::drop-down {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
QComboBox QAbstractItemView {
|
||||||
|
background-color: rgba(15, 15, 25, 0.9);
|
||||||
|
border: 1px solid #3A4760;
|
||||||
|
selection-background-color: #6A3AB1;
|
||||||
|
color: #DAE0ED;
|
||||||
|
}
|
||||||
|
QPushButton {
|
||||||
|
background-color: rgba(10, 10, 20, 0.25);
|
||||||
|
border: 1px solid #3A4760;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px;
|
||||||
|
color: #92E8FF;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: rgba(15, 15, 25, 0.35);
|
||||||
|
border: 1px solid #6482B4;
|
||||||
|
}
|
||||||
|
QPushButton:pressed {
|
||||||
|
background-color: rgba(20, 20, 30, 0.45);
|
||||||
|
border: 1px solid #A020F0;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
MainWindow::MainWindow(QWidget *parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_alsa()
|
||||||
|
{
|
||||||
|
if (!m_alsa.isCardFound()) {
|
||||||
|
QMessageBox::critical(this, "Error", "TASCAM US-144MKII Not Found.\nPlease ensure the device is connected and the 'us144mkii' driver is loaded.");
|
||||||
|
QTimer::singleShot(0, this, &QWidget::close);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
initUi();
|
||||||
|
loadDynamicSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::initUi() {
|
||||||
|
setWindowTitle("TASCAM US-144MKII Control Panel");
|
||||||
|
setWindowIcon(QIcon(":/tascam-control-panel.png"));
|
||||||
|
setFixedSize(820, 450);
|
||||||
|
setStyleSheet(DARK_STYLESHEET);
|
||||||
|
|
||||||
|
m_background.load(":/bg.png");
|
||||||
|
if (m_background.isNull()) {
|
||||||
|
qWarning() << "Failed to load background image from resources!";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *topLevelLayout = new QHBoxLayout(this);
|
||||||
|
topLevelLayout->setContentsMargins(20, 20, 20, 20);
|
||||||
|
topLevelLayout->setSpacing(25);
|
||||||
|
|
||||||
|
auto *leftPanel = new QVBoxLayout();
|
||||||
|
auto *middlePanel = new QVBoxLayout();
|
||||||
|
auto *rightPanel = new QVBoxLayout();
|
||||||
|
|
||||||
|
auto *logoLabel = new QLabel();
|
||||||
|
logoLabel->setPixmap(QPixmap(":/logo.png").scaledToWidth(250, Qt::SmoothTransformation));
|
||||||
|
auto *titleLabel = new QLabel("US-144 MKII Control Panel");
|
||||||
|
titleLabel->setObjectName("Title");
|
||||||
|
|
||||||
|
auto *infoGrid = new QGridLayout();
|
||||||
|
infoGrid->setSpacing(5);
|
||||||
|
const QMap<QString, QString> infoData = {
|
||||||
|
{"Driver Version:", "driver_version"}, {"Device:", "device"},
|
||||||
|
{"Sample Width:", "sample_width"}, {"Sample Rate:", "sample_rate"},
|
||||||
|
{"Sample Clock Source:", "clock_source"}, {"Digital Input Status:", "digital_status"}
|
||||||
|
};
|
||||||
|
int row = 0;
|
||||||
|
for (auto it = infoData.constBegin(); it != infoData.constEnd(); ++it) {
|
||||||
|
auto *label = new QLabel(it.key());
|
||||||
|
label->setFont(QFont("Arial", 9, QFont::Bold));
|
||||||
|
auto *valueLabel = new QLabel("N/A");
|
||||||
|
valueLabel->setFont(QFont("Arial", 9));
|
||||||
|
infoGrid->addWidget(label, row, 0);
|
||||||
|
infoGrid->addWidget(valueLabel, row, 1);
|
||||||
|
m_infoLabels[it.value()] = valueLabel;
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
|
||||||
|
leftPanel->addWidget(logoLabel);
|
||||||
|
leftPanel->addWidget(titleLabel);
|
||||||
|
leftPanel->addLayout(infoGrid);
|
||||||
|
leftPanel->addStretch();
|
||||||
|
|
||||||
|
middlePanel->setSpacing(0);
|
||||||
|
|
||||||
|
auto addSection = [&](const QString& title, QWidget* widget) {
|
||||||
|
auto* header = new QLabel(title);
|
||||||
|
header->setObjectName("SectionHeader");
|
||||||
|
middlePanel->addWidget(header);
|
||||||
|
middlePanel->addWidget(widget);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto latencyPair = createControlWidget("Latency Profile", {"low latency", "normal latency", "high latency"});
|
||||||
|
m_latencyCombo = latencyPair.second;
|
||||||
|
addSection("AUDIO PERFORMANCE", latencyPair.first);
|
||||||
|
|
||||||
|
auto capture12Pair = createControlWidget("ch1 and ch2", {"Analog In", "Digital In"});
|
||||||
|
m_capture12Combo = capture12Pair.second;
|
||||||
|
auto capture34Pair = createControlWidget("ch3 and ch4", {"Analog In", "Digital In"});
|
||||||
|
m_capture34Combo = capture34Pair.second;
|
||||||
|
addSection("INPUTS", capture12Pair.first);
|
||||||
|
middlePanel->addWidget(capture34Pair.first);
|
||||||
|
|
||||||
|
auto lineOutPair = createControlWidget("ch1 and ch2", {"Playback 1-2", "Playback 3-4"});
|
||||||
|
m_lineOutCombo = lineOutPair.second;
|
||||||
|
addSection("LINE", lineOutPair.first);
|
||||||
|
|
||||||
|
auto digitalOutPair = createControlWidget("ch3 and ch4", {"Playback 1-2", "Playback 3-4"});
|
||||||
|
m_digitalOutCombo = digitalOutPair.second;
|
||||||
|
addSection("DIGITAL", digitalOutPair.first);
|
||||||
|
|
||||||
|
middlePanel->addStretch();
|
||||||
|
|
||||||
|
auto *deviceImageLabel = new QLabel();
|
||||||
|
deviceImageLabel->setPixmap(QPixmap(":/device.png").scaled(250, 250, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||||
|
deviceImageLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
auto *exitButton = new QPushButton("Exit");
|
||||||
|
exitButton->setFixedSize(100, 30);
|
||||||
|
connect(exitButton, &QPushButton::clicked, this, &QWidget::close);
|
||||||
|
|
||||||
|
rightPanel->addWidget(deviceImageLabel);
|
||||||
|
rightPanel->addStretch();
|
||||||
|
rightPanel->addWidget(exitButton, 0, Qt::AlignCenter);
|
||||||
|
|
||||||
|
topLevelLayout->addLayout(leftPanel, 3);
|
||||||
|
topLevelLayout->addLayout(middlePanel, 3);
|
||||||
|
topLevelLayout->addLayout(rightPanel, 3);
|
||||||
|
|
||||||
|
connect(m_latencyCombo, &QComboBox::currentIndexChanged, this, [this](int index){ onControlChanged("Latency Profile", index); });
|
||||||
|
connect(m_lineOutCombo, &QComboBox::currentIndexChanged, this, [this](int index){ onControlChanged("Line Out Source", index); });
|
||||||
|
connect(m_digitalOutCombo, &QComboBox::currentIndexChanged, this, [this](int index){ onControlChanged("Digital Out Source", index); });
|
||||||
|
connect(m_capture12Combo, &QComboBox::currentIndexChanged, this, [this](int index){ onControlChanged("Capture 1-2 Source", index); });
|
||||||
|
connect(m_capture34Combo, &QComboBox::currentIndexChanged, this, [this](int index){ onControlChanged("Capture 3-4 Source", index); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::loadDynamicSettings() {
|
||||||
|
m_infoLabels["driver_version"]->setText(QString::fromStdString(m_alsa.readSysfsAttr("driver_version")));
|
||||||
|
m_infoLabels["device"]->setText("US-144 MKII");
|
||||||
|
m_infoLabels["sample_width"]->setText("24 bits");
|
||||||
|
m_infoLabels["clock_source"]->setText("internal");
|
||||||
|
m_infoLabels["digital_status"]->setText("unavailable");
|
||||||
|
|
||||||
|
long rate_val = m_alsa.getControlValue("Sample Rate");
|
||||||
|
m_infoLabels["sample_rate"]->setText(rate_val > 0 ? QString("%1 kHz").arg(rate_val / 1000.0, 0, 'f', 1) : "N/A (inactive)");
|
||||||
|
|
||||||
|
updateCombo(m_latencyCombo, "Latency Profile");
|
||||||
|
updateCombo(m_lineOutCombo, "Line Out Source");
|
||||||
|
updateCombo(m_digitalOutCombo, "Digital Out Source");
|
||||||
|
updateCombo(m_capture12Combo, "Capture 1-2 Source");
|
||||||
|
updateCombo(m_capture34Combo, "Capture 3-4 Source");
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::updateCombo(QComboBox* combo, const std::string& controlName) {
|
||||||
|
long value = m_alsa.getControlValue(controlName);
|
||||||
|
combo->blockSignals(true);
|
||||||
|
combo->setCurrentIndex(static_cast<int>(value));
|
||||||
|
combo->blockSignals(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::onControlChanged(const std::string& controlName, int index) {
|
||||||
|
m_alsa.setControlValue(controlName, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<QWidget*, QComboBox*> MainWindow::createControlWidget(const QString& labelText, const QStringList& items) {
|
||||||
|
auto *container = new QWidget();
|
||||||
|
auto *layout = new QVBoxLayout(container);
|
||||||
|
layout->setContentsMargins(0, 8, 0, 8);
|
||||||
|
layout->setSpacing(2);
|
||||||
|
|
||||||
|
auto *label = new QLabel(labelText);
|
||||||
|
label->setObjectName("ControlLabel");
|
||||||
|
auto *combo = new QComboBox();
|
||||||
|
combo->addItems(items);
|
||||||
|
|
||||||
|
layout->addWidget(label);
|
||||||
|
layout->addWidget(combo);
|
||||||
|
|
||||||
|
return {container, combo};
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::paintEvent(QPaintEvent *event) {
|
||||||
|
QPainter painter(this);
|
||||||
|
if (!m_background.isNull()) {
|
||||||
|
painter.drawPixmap(this->rect(), m_background);
|
||||||
|
}
|
||||||
|
QWidget::paintEvent(event);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
#ifndef MAINWINDOW_H
|
||||||
|
#define MAINWINDOW_H
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QMap>
|
||||||
|
#include "alsacontroller.h"
|
||||||
|
|
||||||
|
class QLabel;
|
||||||
|
class QComboBox;
|
||||||
|
class QPushButton;
|
||||||
|
|
||||||
|
class MainWindow : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit MainWindow(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent *event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void initUi();
|
||||||
|
void loadDynamicSettings();
|
||||||
|
std::pair<QWidget*, QComboBox*> createControlWidget(const QString& labelText, const QStringList& items);
|
||||||
|
void updateCombo(QComboBox* combo, const std::string& controlName);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onControlChanged(const std::string& controlName, int index);
|
||||||
|
|
||||||
|
private:
|
||||||
|
AlsaController m_alsa;
|
||||||
|
QPixmap m_background;
|
||||||
|
|
||||||
|
QMap<QString, QLabel*> m_infoLabels;
|
||||||
|
QComboBox* m_latencyCombo;
|
||||||
|
QComboBox* m_capture12Combo;
|
||||||
|
QComboBox* m_capture34Combo;
|
||||||
|
QComboBox* m_lineOutCombo;
|
||||||
|
QComboBox* m_digitalOutCombo;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=TASCAM US-144MKII Control Panel
|
||||||
|
Exec=TascamControlPanel
|
||||||
|
Icon=tascam-control-panel
|
||||||
|
Type=Application
|
||||||
|
Categories=AudioVideo;Audio;
|
||||||
|
|
@ -1,319 +0,0 @@
|
||||||
import sys
|
|
||||||
import subprocess
|
|
||||||
import re
|
|
||||||
import os
|
|
||||||
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel, QComboBox,
|
|
||||||
QPushButton, QGridLayout, QVBoxLayout, QHBoxLayout, QMessageBox)
|
|
||||||
from PyQt6.QtGui import QPixmap, QFont, QIcon, QPainter
|
|
||||||
from PyQt6.QtCore import Qt, QBuffer, QIODevice
|
|
||||||
import base64
|
|
||||||
|
|
||||||
def resource_path(relative_path):
|
|
||||||
try:
|
|
||||||
base_path = sys._MEIPASS
|
|
||||||
except Exception:
|
|
||||||
base_path = os.path.abspath(os.path.dirname(__file__))
|
|
||||||
return os.path.join(base_path, relative_path)
|
|
||||||
|
|
||||||
DARK_STYLESHEET = """
|
|
||||||
QWidget {
|
|
||||||
background-color: transparent;
|
|
||||||
color: #DAE0ED;
|
|
||||||
font-family: Arial;
|
|
||||||
}
|
|
||||||
QLabel {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
QLabel#Title {
|
|
||||||
font-size: 15pt;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #FFFFFF;
|
|
||||||
}
|
|
||||||
QLabel#SectionHeader {
|
|
||||||
font-size: 11pt;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #92E8FF;
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 3px;
|
|
||||||
}
|
|
||||||
QLabel#ControlLabel {
|
|
||||||
font-size: 9pt;
|
|
||||||
color: #CBD2E6;
|
|
||||||
}
|
|
||||||
QComboBox {
|
|
||||||
background-color: rgba(10, 10, 20, 0.25);
|
|
||||||
border: 1px solid #3A4760;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 4px;
|
|
||||||
color: #DAE0ED;
|
|
||||||
}
|
|
||||||
QComboBox:hover {
|
|
||||||
background-color: rgba(15, 15, 25, 0.35);
|
|
||||||
border: 1px solid #6482B4;
|
|
||||||
}
|
|
||||||
QComboBox::drop-down {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
QComboBox QAbstractItemView {
|
|
||||||
background-color: rgba(15, 15, 25, 0.9);
|
|
||||||
border: 1px solid #3A4760;
|
|
||||||
selection-background-color: #6A3AB1;
|
|
||||||
color: #DAE0ED;
|
|
||||||
}
|
|
||||||
QPushButton {
|
|
||||||
background-color: rgba(10, 10, 20, 0.25);
|
|
||||||
border: 1px solid #3A4760;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 5px;
|
|
||||||
color: #92E8FF;
|
|
||||||
}
|
|
||||||
QPushButton:hover {
|
|
||||||
background-color: rgba(15, 15, 25, 0.35);
|
|
||||||
border: 1px solid #6482B4;
|
|
||||||
}
|
|
||||||
QPushButton:pressed {
|
|
||||||
background-color: rgba(20, 20, 30, 0.45);
|
|
||||||
border: 1px solid #A020F0;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
class AmixerController:
|
|
||||||
@staticmethod
|
|
||||||
def get_card_id(card_name="US144MKII"):
|
|
||||||
try:
|
|
||||||
output = subprocess.check_output(['aplay', '-l'], text=True)
|
|
||||||
for line in output.splitlines():
|
|
||||||
if card_name in line:
|
|
||||||
match = re.match(r'card (\d+):', line)
|
|
||||||
if match:
|
|
||||||
return match.group(1)
|
|
||||||
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
||||||
return None
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_control_value(card_id, control_name):
|
|
||||||
if not card_id: return 0
|
|
||||||
try:
|
|
||||||
cmd = ['amixer', '-c', card_id, 'cget', f"name='{control_name}'"]
|
|
||||||
output = subprocess.check_output(cmd, text=True, stderr=subprocess.DEVNULL)
|
|
||||||
for line in output.splitlines():
|
|
||||||
if ': values=' in line:
|
|
||||||
return int(line.split('=')[1])
|
|
||||||
except (FileNotFoundError, subprocess.CalledProcessError, IndexError, ValueError):
|
|
||||||
return 0
|
|
||||||
return 0
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def set_control_value(card_id, control_name, value):
|
|
||||||
if not card_id: return False
|
|
||||||
try:
|
|
||||||
cmd = ['amixer', '-c', card_id, 'cset', f"name='{control_name}'", str(value)]
|
|
||||||
subprocess.check_call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
||||||
return True
|
|
||||||
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def read_sysfs_attr(card_id, attr_name):
|
|
||||||
path = f"/sys/class/sound/card{card_id}/device/{attr_name}"
|
|
||||||
if os.path.exists(path):
|
|
||||||
try:
|
|
||||||
with open(path, 'r') as f:
|
|
||||||
return f.read().strip()
|
|
||||||
except IOError:
|
|
||||||
return "N/A"
|
|
||||||
return "N/A"
|
|
||||||
|
|
||||||
class TascamControlPanel(QWidget):
|
|
||||||
def __init__(self, card_id):
|
|
||||||
super().__init__()
|
|
||||||
self.card_id = card_id
|
|
||||||
self.init_ui()
|
|
||||||
self.load_dynamic_settings()
|
|
||||||
|
|
||||||
def init_ui(self):
|
|
||||||
self.setWindowTitle("TASCAM US-144MKII Control Panel")
|
|
||||||
self.setWindowIcon(QIcon(resource_path("icon.ico")))
|
|
||||||
self.setFixedSize(820, 450)
|
|
||||||
|
|
||||||
self.setStyleSheet(DARK_STYLESHEET)
|
|
||||||
|
|
||||||
self.background_label = QLabel(self)
|
|
||||||
self.background_label.setGeometry(self.rect())
|
|
||||||
self.background_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
|
|
||||||
bg_image_path = resource_path("bg.png")
|
|
||||||
self.original_bg_pixmap = QPixmap(bg_image_path)
|
|
||||||
|
|
||||||
if self.original_bg_pixmap.isNull():
|
|
||||||
print(f"Warning: Could not load background image from {bg_image_path}. Using solid color.")
|
|
||||||
self.setStyleSheet(self.styleSheet() + "TascamControlPanel { background-color: #1a1a1a; }")
|
|
||||||
else:
|
|
||||||
self._update_background_pixmap()
|
|
||||||
|
|
||||||
self.background_label.lower()
|
|
||||||
|
|
||||||
content_container = QWidget(self)
|
|
||||||
top_level_layout = QHBoxLayout(content_container)
|
|
||||||
top_level_layout.setContentsMargins(20, 20, 20, 20)
|
|
||||||
top_level_layout.setSpacing(25)
|
|
||||||
|
|
||||||
main_overall_layout = QVBoxLayout(self)
|
|
||||||
main_overall_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
main_overall_layout.addWidget(content_container)
|
|
||||||
|
|
||||||
left_panel = QVBoxLayout()
|
|
||||||
info_grid = QGridLayout()
|
|
||||||
logo_label = QLabel()
|
|
||||||
logo_label.setPixmap(QPixmap(resource_path("logo.png")).scaledToWidth(250, Qt.TransformationMode.SmoothTransformation))
|
|
||||||
title_label = QLabel("US-144 MKII Control Panel")
|
|
||||||
title_label.setObjectName("Title")
|
|
||||||
left_panel.addWidget(logo_label)
|
|
||||||
left_panel.addWidget(title_label)
|
|
||||||
info_grid.setSpacing(5)
|
|
||||||
self.info_labels = {}
|
|
||||||
info_data = {
|
|
||||||
"Driver Version:": "driver_version", "Device:": "device",
|
|
||||||
"Sample Width:": "sample_width", "Sample Rate:": "sample_rate",
|
|
||||||
"Sample Clock Source:": "clock_source", "Digital Input Status:": "digital_status"
|
|
||||||
}
|
|
||||||
row = 0
|
|
||||||
for label_text, key in info_data.items():
|
|
||||||
label = QLabel(label_text)
|
|
||||||
label.setFont(QFont("Arial", 9, QFont.Weight.Bold))
|
|
||||||
value_label = QLabel("N/A")
|
|
||||||
value_label.setFont(QFont("Arial", 9))
|
|
||||||
info_grid.addWidget(label, row, 0)
|
|
||||||
info_grid.addWidget(value_label, row, 1)
|
|
||||||
self.info_labels[key] = value_label
|
|
||||||
row += 1
|
|
||||||
left_panel.addLayout(info_grid)
|
|
||||||
left_panel.addStretch()
|
|
||||||
|
|
||||||
middle_panel = QVBoxLayout()
|
|
||||||
middle_panel.setSpacing(0)
|
|
||||||
|
|
||||||
# --- Latency Setting Re-added ---
|
|
||||||
latency_header = QLabel("AUDIO PERFORMANCE")
|
|
||||||
latency_header.setObjectName("SectionHeader")
|
|
||||||
latency_container, self.latency_combo = self.create_control_widget("Latency Profile", ["low latency", "normal latency", "high latency"])
|
|
||||||
middle_panel.addWidget(latency_header)
|
|
||||||
middle_panel.addWidget(latency_container)
|
|
||||||
# --- End Latency Setting Re-added ---
|
|
||||||
|
|
||||||
inputs_header = QLabel("INPUTS")
|
|
||||||
inputs_header.setObjectName("SectionHeader")
|
|
||||||
capture_12_container, self.capture_12_combo = self.create_control_widget("ch1 and ch2", ["Analog In", "Digital In"])
|
|
||||||
capture_34_container, self.capture_34_combo = self.create_control_widget("ch3 and ch4", ["Analog In", "Digital In"])
|
|
||||||
middle_panel.addWidget(inputs_header)
|
|
||||||
middle_panel.addWidget(capture_12_container)
|
|
||||||
middle_panel.addWidget(capture_34_container)
|
|
||||||
|
|
||||||
line_header = QLabel("LINE")
|
|
||||||
line_header.setObjectName("SectionHeader")
|
|
||||||
line_out_container, self.line_out_combo = self.create_control_widget("ch1 and ch2", ["Playback 1-2", "Playback 3-4"])
|
|
||||||
middle_panel.addWidget(line_header)
|
|
||||||
middle_panel.addWidget(line_out_container)
|
|
||||||
|
|
||||||
digital_header = QLabel("DIGITAL")
|
|
||||||
digital_header.setObjectName("SectionHeader")
|
|
||||||
digital_out_container, self.digital_out_combo = self.create_control_widget("ch3 and ch4", ["Playback 1-2", "Playback 3-4"])
|
|
||||||
middle_panel.addWidget(digital_header)
|
|
||||||
middle_panel.addWidget(digital_out_container)
|
|
||||||
|
|
||||||
middle_panel.addStretch()
|
|
||||||
|
|
||||||
right_panel = QVBoxLayout()
|
|
||||||
device_image_label = QLabel()
|
|
||||||
device_image_label.setPixmap(QPixmap(resource_path("device.png")).scaled(250, 250, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))
|
|
||||||
device_image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
exit_button = QPushButton("Exit")
|
|
||||||
exit_button.setFixedSize(100, 30)
|
|
||||||
exit_button.clicked.connect(self.close)
|
|
||||||
right_panel.addWidget(device_image_label)
|
|
||||||
right_panel.addStretch()
|
|
||||||
right_panel.addWidget(exit_button, 0, Qt.AlignmentFlag.AlignCenter)
|
|
||||||
|
|
||||||
top_level_layout.addLayout(left_panel, 3)
|
|
||||||
top_level_layout.addLayout(middle_panel, 3)
|
|
||||||
top_level_layout.addLayout(right_panel, 3)
|
|
||||||
|
|
||||||
# --- Latency Signal Connection Re-added ---
|
|
||||||
self.latency_combo.currentIndexChanged.connect(lambda i: self.set_value("Latency Profile", i))
|
|
||||||
# --- End Latency Signal Connection Re-added ---
|
|
||||||
|
|
||||||
self.line_out_combo.currentIndexChanged.connect(lambda i: self.set_value("Line Out Source", i))
|
|
||||||
self.digital_out_combo.currentIndexChanged.connect(lambda i: self.set_value("Digital Out Source", i))
|
|
||||||
self.capture_12_combo.currentIndexChanged.connect(lambda i: self.set_value("Capture 1-2 Source", i))
|
|
||||||
self.capture_34_combo.currentIndexChanged.connect(lambda i: self.set_value("Capture 3-4 Source", i))
|
|
||||||
|
|
||||||
def _update_background_pixmap(self):
|
|
||||||
if not self.original_bg_pixmap.isNull():
|
|
||||||
scaled_pixmap = self.original_bg_pixmap.scaled(
|
|
||||||
self.size(),
|
|
||||||
Qt.AspectRatioMode.KeepAspectRatioByExpanding,
|
|
||||||
Qt.TransformationMode.SmoothTransformation
|
|
||||||
)
|
|
||||||
self.background_label.setPixmap(scaled_pixmap)
|
|
||||||
|
|
||||||
def resizeEvent(self, event):
|
|
||||||
self.background_label.setGeometry(self.rect())
|
|
||||||
self._update_background_pixmap()
|
|
||||||
super().resizeEvent(event)
|
|
||||||
|
|
||||||
def create_control_widget(self, label_text, combo_items):
|
|
||||||
container_widget = QWidget()
|
|
||||||
layout = QVBoxLayout(container_widget)
|
|
||||||
layout.setContentsMargins(0, 8, 0, 8)
|
|
||||||
layout.setSpacing(2)
|
|
||||||
label = QLabel(label_text)
|
|
||||||
label.setObjectName("ControlLabel")
|
|
||||||
combo_box = QComboBox()
|
|
||||||
combo_box.addItems(combo_items)
|
|
||||||
layout.addWidget(label)
|
|
||||||
layout.addWidget(combo_box)
|
|
||||||
return container_widget, combo_box
|
|
||||||
|
|
||||||
def load_dynamic_settings(self):
|
|
||||||
self.info_labels['driver_version'].setText(AmixerController.read_sysfs_attr(self.card_id, "driver_version"))
|
|
||||||
self.info_labels['device'].setText("US-144 MKII")
|
|
||||||
self.info_labels['sample_width'].setText("24 bits")
|
|
||||||
self.info_labels['clock_source'].setText("internal")
|
|
||||||
self.info_labels['digital_status'].setText("unavailable")
|
|
||||||
|
|
||||||
rate_val = AmixerController.get_control_value(self.card_id, "Sample Rate")
|
|
||||||
self.info_labels['sample_rate'].setText(f"{rate_val / 1000:.1f} kHz" if rate_val > 0 else "N/A (inactive)")
|
|
||||||
|
|
||||||
# --- Latency Setting Load Re-added ---
|
|
||||||
self.update_combo(self.latency_combo, "Latency Profile")
|
|
||||||
# --- End Latency Setting Load Re-added ---
|
|
||||||
|
|
||||||
self.update_combo(self.line_out_combo, "Line Out Source")
|
|
||||||
self.update_combo(self.digital_out_combo, "Digital Out Source")
|
|
||||||
self.update_combo(self.capture_12_combo, "Capture 1-2 Source")
|
|
||||||
self.update_combo(self.capture_34_combo, "Capture 3-4 Source")
|
|
||||||
|
|
||||||
def update_combo(self, combo, control_name):
|
|
||||||
value = AmixerController.get_control_value(self.card_id, control_name)
|
|
||||||
combo.blockSignals(True)
|
|
||||||
combo.setCurrentIndex(value)
|
|
||||||
combo.blockSignals(False)
|
|
||||||
|
|
||||||
def set_value(self, control_name, index):
|
|
||||||
AmixerController.set_control_value(self.card_id, control_name, index)
|
|
||||||
|
|
||||||
def main():
|
|
||||||
app = QApplication(sys.argv)
|
|
||||||
|
|
||||||
card_id = AmixerController.get_card_id()
|
|
||||||
if not card_id:
|
|
||||||
QMessageBox.critical(None, "Error", "TASCAM US-144MKII Not Found.\nPlease ensure the device is connected and the 'us144mkii' driver is loaded.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
panel = TascamControlPanel(card_id)
|
|
||||||
panel.show()
|
|
||||||
sys.exit(app.exec())
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||