native control panel app
|
|
@ -50,3 +50,7 @@ Module.symvers
|
|||
Mkfile.old
|
||||
dkms.conf
|
||||
*.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()
|
||||