native control panel app

This commit is contained in:
serifpersia 2025-07-18 12:53:11 +02:00
parent 50527639ec
commit ba3c08bfd5
16 changed files with 540 additions and 319 deletions

4
.gitignore vendored
View File

@ -50,3 +50,7 @@ Module.symvers
Mkfile.old
dkms.conf
*.ko
*.AppImage
/tascam_controls/AppDir
*.cmake
/tascam_controls/build

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 163 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
[Desktop Entry]
Name=TASCAM US-144MKII Control Panel
Exec=TascamControlPanel
Icon=tascam-control-panel
Type=Application
Categories=AudioVideo;Audio;

View File

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