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