diff --git a/.gitignore b/.gitignore index c214956..b8b4b7c 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,7 @@ Module.symvers Mkfile.old dkms.conf *.ko +*.AppImage +/tascam_controls/AppDir +*.cmake +/tascam_controls/build diff --git a/tascam_controls/CMakeLists.txt b/tascam_controls/CMakeLists.txt new file mode 100644 index 0000000..71c754b --- /dev/null +++ b/tascam_controls/CMakeLists.txt @@ -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 +) diff --git a/tascam_controls/build_appimage.sh b/tascam_controls/build_appimage.sh new file mode 100755 index 0000000..f2ea52e --- /dev/null +++ b/tascam_controls/build_appimage.sh @@ -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!" diff --git a/tascam_controls/icon.ico b/tascam_controls/icon.ico deleted file mode 100644 index e2fa7c0..0000000 Binary files a/tascam_controls/icon.ico and /dev/null differ diff --git a/tascam_controls/bg.png b/tascam_controls/resources/bg.png similarity index 100% rename from tascam_controls/bg.png rename to tascam_controls/resources/bg.png diff --git a/tascam_controls/device.png b/tascam_controls/resources/device.png similarity index 100% rename from tascam_controls/device.png rename to tascam_controls/resources/device.png diff --git a/tascam_controls/logo.png b/tascam_controls/resources/logo.png similarity index 100% rename from tascam_controls/logo.png rename to tascam_controls/resources/logo.png diff --git a/tascam_controls/resources/resources.qrc b/tascam_controls/resources/resources.qrc new file mode 100644 index 0000000..0718bf4 --- /dev/null +++ b/tascam_controls/resources/resources.qrc @@ -0,0 +1,9 @@ + + + + bg.png + logo.png + device.png + tascam-control-panel.png + + diff --git a/tascam_controls/resources/tascam-control-panel.png b/tascam_controls/resources/tascam-control-panel.png new file mode 100644 index 0000000..7b211c5 Binary files /dev/null and b/tascam_controls/resources/tascam-control-panel.png differ diff --git a/tascam_controls/src/alsacontroller.cpp b/tascam_controls/src/alsacontroller.cpp new file mode 100644 index 0000000..9bafd9c --- /dev/null +++ b/tascam_controls/src/alsacontroller.cpp @@ -0,0 +1,119 @@ +#include "alsacontroller.h" +#include +#include +#include + +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 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"; +} diff --git a/tascam_controls/src/alsacontroller.h b/tascam_controls/src/alsacontroller.h new file mode 100644 index 0000000..d3e82e8 --- /dev/null +++ b/tascam_controls/src/alsacontroller.h @@ -0,0 +1,26 @@ +#ifndef ALSACONTROLLER_H +#define ALSACONTROLLER_H + +#include +#include + +class AlsaController +{ +public: + AlsaController(const std::string& target_card_name = "US-144MKII"); + + std::optional 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 diff --git a/tascam_controls/src/main.cpp b/tascam_controls/src/main.cpp new file mode 100644 index 0000000..f9d6aad --- /dev/null +++ b/tascam_controls/src/main.cpp @@ -0,0 +1,12 @@ +#include +#include "mainwindow.h" + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + MainWindow window; + window.show(); + + return app.exec(); +} diff --git a/tascam_controls/src/mainwindow.cpp b/tascam_controls/src/mainwindow.cpp new file mode 100644 index 0000000..b4c18bf --- /dev/null +++ b/tascam_controls/src/mainwindow.cpp @@ -0,0 +1,244 @@ +#include "mainwindow.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +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 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(value)); + combo->blockSignals(false); +} + +void MainWindow::onControlChanged(const std::string& controlName, int index) { + m_alsa.setControlValue(controlName, index); +} + +std::pair 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); +} diff --git a/tascam_controls/src/mainwindow.h b/tascam_controls/src/mainwindow.h new file mode 100644 index 0000000..f87194e --- /dev/null +++ b/tascam_controls/src/mainwindow.h @@ -0,0 +1,44 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include +#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 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 m_infoLabels; + QComboBox* m_latencyCombo; + QComboBox* m_capture12Combo; + QComboBox* m_capture34Combo; + QComboBox* m_lineOutCombo; + QComboBox* m_digitalOutCombo; +}; + +#endif diff --git a/tascam_controls/tascam-control-panel.desktop b/tascam_controls/tascam-control-panel.desktop new file mode 100644 index 0000000..0c39a7d --- /dev/null +++ b/tascam_controls/tascam-control-panel.desktop @@ -0,0 +1,6 @@ +[Desktop Entry] +Name=TASCAM US-144MKII Control Panel +Exec=TascamControlPanel +Icon=tascam-control-panel +Type=Application +Categories=AudioVideo;Audio; diff --git a/tascam_controls/tascam-controls.py b/tascam_controls/tascam-controls.py deleted file mode 100644 index 51aa04c..0000000 --- a/tascam_controls/tascam-controls.py +++ /dev/null @@ -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()