playback up to spec, alsa mixers and python gui control panel

This commit is contained in:
serifpersia 2025-07-12 21:42:15 +02:00
parent fc3a76c66e
commit 049e12fd1e
6 changed files with 608 additions and 149 deletions

BIN
tascam_controls/device.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

BIN
tascam_controls/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
tascam_controls/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1,250 @@
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
from PyQt6.QtCore import Qt
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: #2b2b2b;
color: #f0f0f0;
font-family: Arial;
}
QLabel {
background-color: transparent;
}
QComboBox {
background-color: #3c3f41;
border: 1px solid #555;
border-radius: 4px;
padding: 4px;
}
QComboBox:hover {
border: 1px solid #777;
}
QComboBox::drop-down {
border: none;
}
QComboBox QAbstractItemView {
background-color: #3c3f41;
border: 1px solid #555;
selection-background-color: #5a5d5f;
}
QPushButton {
background-color: #3c3f41;
border: 1px solid #555;
border-radius: 4px;
padding: 5px;
}
QPushButton:hover {
background-color: #4f5254;
}
QPushButton:pressed {
background-color: #5a5d5f;
}
"""
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 get_control_string(card_id, control_name):
if not card_id: return "N/A"
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:
value_str = line.split('=')[1]
byte_values = [int(b, 16) for b in value_str.split(',')]
return bytes(byte_values).partition(b'\0')[0].decode('utf-8', errors='ignore').strip()
except (FileNotFoundError, subprocess.CalledProcessError, IndexError, ValueError):
return "Error"
return "N/A"
@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(800, 450)
self.setStyleSheet(DARK_STYLESHEET)
main_layout = QHBoxLayout(self)
left_panel, middle_panel, right_panel = QVBoxLayout(), QVBoxLayout(), QVBoxLayout()
header_layout = QVBoxLayout()
logo_label = QLabel()
logo_label.setPixmap(QPixmap(resource_path("logo.png")).scaled(250, 50, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))
title_label = QLabel("US-144 MKII Control Panel")
title_label.setFont(QFont("Arial", 15, QFont.Weight.Bold))
header_layout.addWidget(logo_label)
header_layout.addWidget(title_label)
info_layout = QGridLayout()
self.info_labels = {}
info_data = {
"Driver Version:": "N/A", "Device:": "US-144 MKII",
"Sample Width:": "24 bits", "Sample Rate:": "N/A",
"Sample Clock Source:": "internal", "Digital Input Status:": "unavailable"
}
for row, (label_text, value_text) in enumerate(info_data.items()):
key = label_text.replace(":", "").replace(" ", "_").lower()
label = QLabel(label_text, font=QFont("Arial", 10, QFont.Weight.Bold))
value_label = QLabel(value_text, font=QFont("Arial", 10))
info_layout.addWidget(label, row, 0, Qt.AlignmentFlag.AlignLeft)
info_layout.addWidget(value_label, row, 1, Qt.AlignmentFlag.AlignLeft)
self.info_labels[key] = value_label
left_panel.addLayout(info_layout)
left_panel.addStretch()
middle_panel.setSpacing(15)
latency_container, self.latency_combo = self.create_control_widget("Audio Performance", ["Low", "Normal", "High"])
mock_container1, _ = self.create_control_widget("Sample Clock Source", ["Internal", "Auto"])
mock_container2, _ = self.create_control_widget("Digital Output Format", ["S/PDIF"])
routing_container, self.routing_combo = self.create_control_widget("LINE OUTPUTS", ["Stereo to All", "Swapped", "Digital In to All"])
middle_panel.addWidget(latency_container)
middle_panel.addWidget(mock_container1)
middle_panel.addWidget(mock_container2)
middle_panel.addWidget(routing_container)
middle_panel.addStretch()
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 = QVBoxLayout()
top_level_layout.addLayout(header_layout)
top_level_layout.addSpacing(20)
panels_layout = QHBoxLayout()
panels_layout.setContentsMargins(10, 0, 10, 0)
panels_layout.addLayout(left_panel, 1)
panels_layout.addLayout(middle_panel, 1)
panels_layout.addLayout(right_panel, 1)
top_level_layout.addLayout(panels_layout)
main_layout.addLayout(top_level_layout)
self.setLayout(main_layout)
self.latency_combo.currentIndexChanged.connect(self.on_latency_changed)
self.routing_combo.currentIndexChanged.connect(self.on_routing_changed)
def create_control_widget(self, label_text, combo_items):
container_widget = QWidget()
layout = QVBoxLayout(container_widget)
layout.setContentsMargins(0,0,0,0)
layout.setSpacing(2)
label = QLabel(label_text, font=QFont("Arial", 10, QFont.Weight.Bold))
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):
driver_ver = AmixerController.read_sysfs_attr(self.card_id, "driver_version")
self.info_labels['driver_version'].setText(driver_ver)
rate_val = AmixerController.get_control_value(self.card_id, "Sample Rate")
if rate_val > 0:
self.info_labels['sample_rate'].setText(f"{rate_val / 1000:.1f} kHz")
else:
self.info_labels['sample_rate'].setText("N/A (inactive)")
latency_val = AmixerController.get_control_value(self.card_id, "Latency Profile")
self.latency_combo.blockSignals(True)
self.latency_combo.setCurrentIndex(latency_val)
self.latency_combo.blockSignals(False)
routing_val = AmixerController.get_control_value(self.card_id, "Playback Routing")
self.routing_combo.blockSignals(True)
self.routing_combo.setCurrentIndex(routing_val)
self.routing_combo.blockSignals(False)
def on_latency_changed(self, index):
AmixerController.set_control_value(self.card_id, "Latency Profile", index)
def on_routing_changed(self, index):
AmixerController.set_control_value(self.card_id, "Playback Routing", 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()

Binary file not shown.

View File

@ -9,43 +9,32 @@
#include <linux/usb.h> #include <linux/usb.h>
#include <linux/slab.h> #include <linux/slab.h>
#include <linux/delay.h> #include <linux/delay.h>
#include <linux/printk.h>
#include <sound/core.h> #include <sound/core.h>
#include <sound/pcm.h> #include <sound/pcm.h>
#include <sound/pcm_params.h> #include <sound/pcm_params.h>
#include <sound/initval.h> #include <sound/initval.h>
#include <sound/control.h>
MODULE_AUTHOR("serifpersia <ramiserifpersia@gmail.com>"); MODULE_AUTHOR("serifpersia <ramiserifpersia@gmail.com>");
MODULE_DESCRIPTION("ALSA Driver for TASCAM US-144MKII"); MODULE_DESCRIPTION("ALSA Driver for TASCAM US-144MKII");
MODULE_LICENSE("GPL"); MODULE_LICENSE("GPL");
#define DRIVER_NAME "us144mkii" #define DRIVER_NAME "us144mkii"
#define DRIVER_VERSION "1.0"
/* /*
* TODO: * TODO:
* - Implement audio input capture. * - Implement audio input capture.
* - Implement MIDI IN/OUT. * - Implement MIDI IN/OUT.
* - Expose hardware features via the ALSA Control API (mixers):
* - Digital output format selection.
* - Input/output routing.
*/ */
/* --- Module Parameters --- */ /* --- Module Parameters --- */
static int index[SNDRV_CARDS] = SNDRV_DEFAULT_IDX; static int index[SNDRV_CARDS] = SNDRV_DEFAULT_IDX;
static char *id[SNDRV_CARDS] = SNDRV_DEFAULT_STR; static char *id[SNDRV_CARDS] = SNDRV_DEFAULT_STR;
static bool enable[SNDRV_CARDS] = {1, [1 ... (SNDRV_CARDS - 1)] = 0}; static bool enable[SNDRV_CARDS] = {1, [1 ... (SNDRV_CARDS - 1)] = 0};
static int dev_idx; static int dev_idx;
static int playback_urb_packets = 8;
module_param_array(index, int, NULL, 0444);
MODULE_PARM_DESC(index, "Index value for the US-144MKII soundcard.");
module_param_array(id, charp, NULL, 0444);
MODULE_PARM_DESC(id, "ID string for the US-144MKII soundcard.");
module_param_array(enable, bool, NULL, 0444);
MODULE_PARM_DESC(enable, "Enable this US-144MKII soundcard.");
module_param_named(playback_urb_packets, playback_urb_packets, int, 0644);
MODULE_PARM_DESC(playback_urb_packets, "Number of isochronous packets per playback URB (default: 8). Higher values may improve CPU efficiency but require a low-latency or RT kernel to avoid xruns.");
/* --- USB Device Identification --- */ /* --- USB Device Identification --- */
#define USB_VID_TASCAM 0x0644 #define USB_VID_TASCAM 0x0644
@ -60,9 +49,11 @@ MODULE_PARM_DESC(playback_urb_packets, "Number of isochronous packets per playba
/* --- USB Control Message Protocol --- */ /* --- USB Control Message Protocol --- */
#define RT_H2D_CLASS_EP (USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_ENDPOINT) #define RT_H2D_CLASS_EP (USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_ENDPOINT)
#define RT_D2H_CLASS_EP (USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_ENDPOINT)
#define RT_H2D_VENDOR_DEV (USB_DIR_OUT | USB_TYPE_VENDOR | USB_RECIP_DEVICE) #define RT_H2D_VENDOR_DEV (USB_DIR_OUT | USB_TYPE_VENDOR | USB_RECIP_DEVICE)
#define RT_D2H_VENDOR_DEV (USB_DIR_IN | USB_TYPE_VENDOR | USB_RECIP_DEVICE) #define RT_D2H_VENDOR_DEV (USB_DIR_IN | USB_TYPE_VENDOR | USB_RECIP_DEVICE)
#define UAC_SET_CUR 0x01 #define UAC_SET_CUR 0x01
#define UAC_GET_CUR 0x81
#define UAC_SAMPLING_FREQ_CONTROL 0x0100 #define UAC_SAMPLING_FREQ_CONTROL 0x0100
#define VENDOR_REQ_REGISTER_WRITE 0x41 #define VENDOR_REQ_REGISTER_WRITE 0x41
#define VENDOR_REQ_MODE_CONTROL 0x49 #define VENDOR_REQ_MODE_CONTROL 0x49
@ -81,8 +72,9 @@ MODULE_PARM_DESC(playback_urb_packets, "Number of isochronous packets per playba
/* --- URB Configuration --- */ /* --- URB Configuration --- */
#define NUM_PLAYBACK_URBS 8 #define NUM_PLAYBACK_URBS 8
#define PLAYBACK_URB_PACKETS 4
#define NUM_FEEDBACK_URBS 4 #define NUM_FEEDBACK_URBS 4
#define MAX_FEEDBACK_PACKETS 5 /* Max packets needed for any profile */ #define MAX_FEEDBACK_PACKETS 5
#define FEEDBACK_PACKET_SIZE 3 #define FEEDBACK_PACKET_SIZE 3
#define USB_CTRL_TIMEOUT_MS 1000 #define USB_CTRL_TIMEOUT_MS 1000
@ -92,7 +84,42 @@ MODULE_PARM_DESC(playback_urb_packets, "Number of isochronous packets per playba
#define BYTES_PER_FRAME (NUM_CHANNELS * BYTES_PER_SAMPLE) #define BYTES_PER_FRAME (NUM_CHANNELS * BYTES_PER_SAMPLE)
#define FEEDBACK_ACCUMULATOR_SIZE 128 #define FEEDBACK_ACCUMULATOR_SIZE 128
struct tascam_card; /* --- Main Driver Data Structure --- */
struct tascam_card {
struct usb_device *dev;
struct usb_interface *iface0;
struct usb_interface *iface1;
struct snd_card *card;
struct snd_pcm *pcm;
struct snd_pcm_substream *playback_substream;
struct urb *playback_urbs[NUM_PLAYBACK_URBS];
size_t playback_urb_alloc_size;
struct urb *feedback_urbs[NUM_FEEDBACK_URBS];
size_t feedback_urb_alloc_size;
spinlock_t lock;
atomic_t playback_active;
int current_rate;
unsigned int latency_profile;
unsigned int playback_routing;
unsigned int feedback_accumulator_pattern[FEEDBACK_ACCUMULATOR_SIZE];
unsigned int feedback_pattern_out_idx;
unsigned int feedback_pattern_in_idx;
bool feedback_synced;
unsigned int feedback_urb_skip_count;
u64 playback_frames_consumed;
snd_pcm_uframes_t driver_playback_pos;
u64 last_period_pos;
const unsigned int (*feedback_patterns)[8];
unsigned int feedback_base_value;
unsigned int feedback_max_value;
};
static struct usb_driver tascam_alsa_driver; static struct usb_driver tascam_alsa_driver;
/* --- Forward Declarations --- */ /* --- Forward Declarations --- */
@ -104,6 +131,186 @@ static void tascam_disconnect(struct usb_interface *intf);
static int tascam_suspend(struct usb_interface *intf, pm_message_t message); static int tascam_suspend(struct usb_interface *intf, pm_message_t message);
static int tascam_resume(struct usb_interface *intf); static int tascam_resume(struct usb_interface *intf);
/* --- Sysfs Attribute for Driver Version --- */
/**
* driver_version_show - Sysfs callback to show the driver version.
* @dev: The device structure.
* @attr: The device attribute structure.
* @buf: The buffer to write the version string into.
*
* Returns: The number of bytes written.
*/
static ssize_t driver_version_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
return sysfs_emit(buf, "%s\n", DRIVER_VERSION);
}
static DEVICE_ATTR_RO(driver_version);
/* --- ALSA Control Definitions --- */
static const char * const latency_profile_texts[] = {"Low", "Normal", "High"};
static int tascam_latency_info(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_info *uinfo)
{
uinfo->type = SNDRV_CTL_ELEM_TYPE_ENUMERATED;
uinfo->count = 1;
uinfo->value.enumerated.items = 3;
if (uinfo->value.enumerated.item >= 3)
uinfo->value.enumerated.item = 2;
strcpy(uinfo->value.enumerated.name, latency_profile_texts[uinfo->value.enumerated.item]);
return 0;
}
static int tascam_latency_get(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol)
{
struct tascam_card *tascam = (struct tascam_card *)snd_kcontrol_chip(kcontrol);
switch (tascam->latency_profile) {
case 1: ucontrol->value.enumerated.item[0] = 0; break;
case 2: ucontrol->value.enumerated.item[0] = 1; break;
case 5: ucontrol->value.enumerated.item[0] = 2; break;
default: ucontrol->value.enumerated.item[0] = 1; break;
}
return 0;
}
static int tascam_latency_put(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol)
{
struct tascam_card *tascam = (struct tascam_card *)snd_kcontrol_chip(kcontrol);
unsigned int new_profile;
bool changed = false;
switch (ucontrol->value.enumerated.item[0]) {
case 0: new_profile = 1; break;
case 1: new_profile = 2; break;
case 2: new_profile = 5; break;
default: return -EINVAL;
}
if (tascam->latency_profile != new_profile) {
tascam->latency_profile = new_profile;
changed = true;
}
return changed;
}
static const struct snd_kcontrol_new tascam_latency_control = {
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.name = "Latency Profile",
.info = tascam_latency_info,
.get = tascam_latency_get,
.put = tascam_latency_put,
};
static const char * const playback_routing_texts[] = {"Stereo to All", "Swapped", "Digital In to All"};
static int tascam_routing_info(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_info *uinfo)
{
uinfo->type = SNDRV_CTL_ELEM_TYPE_ENUMERATED;
uinfo->count = 1;
uinfo->value.enumerated.items = 3;
if (uinfo->value.enumerated.item >= 3)
uinfo->value.enumerated.item = 2;
strcpy(uinfo->value.enumerated.name, playback_routing_texts[uinfo->value.enumerated.item]);
return 0;
}
static int tascam_routing_get(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol)
{
struct tascam_card *tascam = (struct tascam_card *)snd_kcontrol_chip(kcontrol);
ucontrol->value.enumerated.item[0] = tascam->playback_routing;
return 0;
}
static int tascam_routing_put(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol)
{
struct tascam_card *tascam = (struct tascam_card *)snd_kcontrol_chip(kcontrol);
unsigned int new_routing = ucontrol->value.enumerated.item[0];
bool changed = false;
if (new_routing >= 3)
return -EINVAL;
if (tascam->playback_routing != new_routing) {
tascam->playback_routing = new_routing;
changed = true;
}
return changed;
}
static const struct snd_kcontrol_new tascam_routing_control = {
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.name = "Playback Routing",
.info = tascam_routing_info,
.get = tascam_routing_get,
.put = tascam_routing_put,
};
/**
* tascam_samplerate_info - ALSA control info callback for the sample rate.
* @kcontrol: The kcontrol instance.
* @uinfo: The user control element info structure to fill.
*
* Provides information about the read-only sample rate control.
*
* Returns: 0 on success.
*/
static int tascam_samplerate_info(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_info *uinfo)
{
uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
uinfo->count = 1;
uinfo->value.integer.min = 0;
uinfo->value.integer.max = 96000;
return 0;
}
/**
* tascam_samplerate_get - ALSA control get callback for the sample rate.
* @kcontrol: The kcontrol instance.
* @ucontrol: The user control element value structure to fill.
*
* Reports the current sample rate of the device. It first checks the driver's
* internal state. If no stream is active, it queries the device directly via
* a USB control message.
*
* Returns: 0 on success, or a negative error code on failure.
*/
static int tascam_samplerate_get(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol)
{
struct tascam_card *tascam = (struct tascam_card *)snd_kcontrol_chip(kcontrol);
u8 *buf;
int err;
u32 rate = 0;
if (tascam->current_rate > 0) {
ucontrol->value.integer.value[0] = tascam->current_rate;
return 0;
}
buf = kmalloc(3, GFP_KERNEL);
if (!buf)
return -ENOMEM;
err = usb_control_msg(tascam->dev, usb_rcvctrlpipe(tascam->dev, 0),
UAC_GET_CUR, RT_D2H_CLASS_EP,
UAC_SAMPLING_FREQ_CONTROL, EP_AUDIO_IN,
buf, 3, USB_CTRL_TIMEOUT_MS);
if (err >= 3)
rate = buf[0] | (buf[1] << 8) | (buf[2] << 16);
ucontrol->value.integer.value[0] = rate;
kfree(buf);
return 0;
}
static const struct snd_kcontrol_new tascam_samplerate_control = {
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.name = "Sample Rate",
.info = tascam_samplerate_info,
.get = tascam_samplerate_get,
.access = SNDRV_CTL_ELEM_ACCESS_READ,
};
/* --- Rate-to-Packet Fixing Data (Verified) --- */ /* --- Rate-to-Packet Fixing Data (Verified) --- */
static const unsigned int patterns_48khz[5][8] = { static const unsigned int patterns_48khz[5][8] = {
{5, 6, 6, 6, 5, 6, 6, 6}, {5, 6, 6, 6, 6, 6, 6, 6}, {5, 6, 6, 6, 5, 6, 6, 6}, {5, 6, 6, 6, 6, 6, 6, 6},
@ -126,40 +333,6 @@ static const unsigned int patterns_44khz[5][8] = {
{6, 6, 6, 5, 6, 6, 6, 5} {6, 6, 6, 5, 6, 6, 6, 5}
}; };
/* --- Main Driver Data Structure --- */
struct tascam_card {
struct usb_device *dev;
struct usb_interface *iface0;
struct usb_interface *iface1;
struct snd_card *card;
struct snd_pcm *pcm;
struct snd_pcm_substream *playback_substream;
struct urb *playback_urbs[NUM_PLAYBACK_URBS];
size_t playback_urb_alloc_size;
struct urb *feedback_urbs[NUM_FEEDBACK_URBS];
size_t feedback_urb_alloc_size;
spinlock_t lock;
atomic_t playback_active;
int current_rate;
unsigned int feedback_accumulator_pattern[FEEDBACK_ACCUMULATOR_SIZE];
unsigned int feedback_pattern_out_idx;
unsigned int feedback_pattern_in_idx;
bool feedback_synced;
unsigned int feedback_urb_skip_count;
u64 playback_frames_consumed;
snd_pcm_uframes_t driver_playback_pos;
u64 last_period_pos;
const unsigned int (*feedback_patterns)[8];
unsigned int feedback_base_value;
unsigned int feedback_max_value;
};
static const struct snd_pcm_hardware tascam_pcm_hw = { static const struct snd_pcm_hardware tascam_pcm_hw = {
.info = (SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_INTERLEAVED | .info = (SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_BLOCK_TRANSFER | SNDRV_PCM_INFO_MMAP_VALID | SNDRV_PCM_INFO_BLOCK_TRANSFER | SNDRV_PCM_INFO_MMAP_VALID |
@ -218,10 +391,10 @@ static int tascam_alloc_urbs(struct tascam_card *tascam)
size_t max_packet_size; size_t max_packet_size;
max_packet_size = ((96000 / 8000) + 2) * BYTES_PER_FRAME; max_packet_size = ((96000 / 8000) + 2) * BYTES_PER_FRAME;
tascam->playback_urb_alloc_size = max_packet_size * playback_urb_packets; tascam->playback_urb_alloc_size = max_packet_size * PLAYBACK_URB_PACKETS;
for (i = 0; i < NUM_PLAYBACK_URBS; i++) { for (i = 0; i < NUM_PLAYBACK_URBS; i++) {
struct urb *urb = usb_alloc_urb(playback_urb_packets, GFP_KERNEL); struct urb *urb = usb_alloc_urb(PLAYBACK_URB_PACKETS, GFP_KERNEL);
if (!urb) if (!urb)
goto error; goto error;
tascam->playback_urbs[i] = urb; tascam->playback_urbs[i] = urb;
@ -418,27 +591,26 @@ static int tascam_pcm_prepare(struct snd_pcm_substream *substream)
tascam->feedback_synced = false; tascam->feedback_synced = false;
tascam->feedback_urb_skip_count = NUM_FEEDBACK_URBS * 2; tascam->feedback_urb_skip_count = NUM_FEEDBACK_URBS * 2;
dev_dbg(tascam->card->dev, "Prepare: Sync state reset, starting in unsynced mode.\n");
/* Initialize feedback accumulator with nominal values */ /* Initialize feedback accumulator with nominal values */
nominal_frames_per_packet = runtime->rate / 8000; nominal_frames_per_packet = runtime->rate / 8000;
for (i = 0; i < FEEDBACK_ACCUMULATOR_SIZE; i++) for (i = 0; i < FEEDBACK_ACCUMULATOR_SIZE; i++)
tascam->feedback_accumulator_pattern[i] = nominal_frames_per_packet; tascam->feedback_accumulator_pattern[i] = nominal_frames_per_packet;
/* /* Validate and apply latency profile */
* Configure URBs based on the fixed hardware profile for the current sample rate. switch (tascam->latency_profile) {
* The hardware latency is fixed per rate; we use the lowest latency setting. case 1:
*/ case 2:
switch (runtime->rate) { case 5:
case 44100: feedback_packets = 1; break; feedback_packets = tascam->latency_profile;
case 48000: feedback_packets = 1; break; break;
case 88200: feedback_packets = 1; break; default:
case 96000: feedback_packets = 1; break; dev_warn(tascam->card->dev, "Invalid latency_profile value %d, falling back to default (2).\n", tascam->latency_profile);
default: feedback_packets = 1; break; /* Failsafe */ tascam->latency_profile = 2;
feedback_packets = 2;
} }
dev_info(tascam->card->dev, "Prepare: Using fixed hardware profile for %u Hz (%u feedback packets)\n", dev_info(tascam->card->dev, "Prepare: Using latency profile %u (%u feedback packets) for %u Hz\n",
runtime->rate, feedback_packets); tascam->latency_profile, feedback_packets, runtime->rate);
/* Configure Feedback URBs */ /* Configure Feedback URBs */
for (i = 0; i < NUM_FEEDBACK_URBS; i++) { for (i = 0; i < NUM_FEEDBACK_URBS; i++) {
@ -455,15 +627,15 @@ static int tascam_pcm_prepare(struct snd_pcm_substream *substream)
/* Configure Playback URBs */ /* Configure Playback URBs */
nominal_bytes_per_packet = nominal_frames_per_packet * BYTES_PER_FRAME; nominal_bytes_per_packet = nominal_frames_per_packet * BYTES_PER_FRAME;
total_bytes_in_urb = nominal_bytes_per_packet * playback_urb_packets; total_bytes_in_urb = nominal_bytes_per_packet * PLAYBACK_URB_PACKETS;
for (u = 0; u < NUM_PLAYBACK_URBS; u++) { for (u = 0; u < NUM_PLAYBACK_URBS; u++) {
struct urb *urb = tascam->playback_urbs[u]; struct urb *urb = tascam->playback_urbs[u];
memset(urb->transfer_buffer, 0, tascam->playback_urb_alloc_size); memset(urb->transfer_buffer, 0, tascam->playback_urb_alloc_size);
urb->transfer_buffer_length = total_bytes_in_urb; urb->transfer_buffer_length = total_bytes_in_urb;
urb->number_of_packets = playback_urb_packets; urb->number_of_packets = PLAYBACK_URB_PACKETS;
for (i = 0; i < playback_urb_packets; i++) { for (i = 0; i < PLAYBACK_URB_PACKETS; i++) {
urb->iso_frame_desc[i].offset = i * nominal_bytes_per_packet; urb->iso_frame_desc[i].offset = i * nominal_bytes_per_packet;
urb->iso_frame_desc[i].length = nominal_bytes_per_packet; urb->iso_frame_desc[i].length = nominal_bytes_per_packet;
} }
@ -554,7 +726,6 @@ static void playback_urb_complete(struct urb *urb)
struct snd_pcm_runtime *runtime; struct snd_pcm_runtime *runtime;
unsigned long flags; unsigned long flags;
char *src_buf, *dst_buf; char *src_buf, *dst_buf;
unsigned int total_frames_for_urb = 0;
size_t total_bytes_for_urb = 0; size_t total_bytes_for_urb = 0;
int ret, i; int ret, i;
@ -569,42 +740,7 @@ static void playback_urb_complete(struct urb *urb)
spin_lock_irqsave(&tascam->lock, flags); spin_lock_irqsave(&tascam->lock, flags);
/* Phase 1: Calculate the total number of frames needed for this URB. */ /* Phase 1: Populate the isochronous frame descriptors and calculate total size. */
for (i = 0; i < urb->number_of_packets; i++) {
unsigned int frames_for_packet;
if (tascam->feedback_synced) {
frames_for_packet = tascam->feedback_accumulator_pattern[
(tascam->feedback_pattern_out_idx + i) % FEEDBACK_ACCUMULATOR_SIZE];
} else {
frames_for_packet = runtime->rate / 8000;
}
total_frames_for_urb += frames_for_packet;
}
total_bytes_for_urb = total_frames_for_urb * BYTES_PER_FRAME;
/* Phase 2: Perform an efficient bulk memory copy. */
src_buf = runtime->dma_area;
dst_buf = urb->transfer_buffer;
if (total_bytes_for_urb > 0) {
snd_pcm_uframes_t offset_frames = tascam->driver_playback_pos;
snd_pcm_uframes_t frames_to_end = runtime->buffer_size - offset_frames;
size_t bytes_to_end = frames_to_bytes(runtime, frames_to_end);
if (total_bytes_for_urb > bytes_to_end) {
/* Data wraps around the end of the circular buffer */
memcpy(dst_buf, src_buf + frames_to_bytes(runtime, offset_frames), bytes_to_end);
memcpy(dst_buf + bytes_to_end, src_buf, total_bytes_for_urb - bytes_to_end);
} else {
/* Data is in a single contiguous block */
memcpy(dst_buf, src_buf + frames_to_bytes(runtime, offset_frames), total_bytes_for_urb);
}
}
tascam->driver_playback_pos = (tascam->driver_playback_pos + total_frames_for_urb) % runtime->buffer_size;
/* Phase 3: Populate the isochronous frame descriptors. */
urb->transfer_buffer_length = total_bytes_for_urb;
total_bytes_for_urb = 0; /* Reuse as running offset */
for (i = 0; i < urb->number_of_packets; i++) { for (i = 0; i < urb->number_of_packets; i++) {
unsigned int frames_for_packet; unsigned int frames_for_packet;
size_t bytes_for_packet; size_t bytes_for_packet;
@ -621,6 +757,44 @@ static void playback_urb_complete(struct urb *urb)
urb->iso_frame_desc[i].length = bytes_for_packet; urb->iso_frame_desc[i].length = bytes_for_packet;
total_bytes_for_urb += bytes_for_packet; total_bytes_for_urb += bytes_for_packet;
} }
urb->transfer_buffer_length = total_bytes_for_urb;
/* Phase 2: Copy and format audio data from ALSA buffer to URB buffer. */
src_buf = runtime->dma_area;
dst_buf = urb->transfer_buffer;
if (total_bytes_for_urb > 0) {
snd_pcm_uframes_t offset_frames = tascam->driver_playback_pos;
snd_pcm_uframes_t frames_to_copy = bytes_to_frames(runtime, total_bytes_for_urb);
int f;
for (f = 0; f < frames_to_copy; ++f) {
snd_pcm_uframes_t current_frame_pos = (offset_frames + f) % runtime->buffer_size;
char *src_frame = src_buf + frames_to_bytes(runtime, current_frame_pos);
char *dst_frame = dst_buf + (f * BYTES_PER_FRAME);
switch (tascam->playback_routing) {
case 0: /* Stereo to All */
*(u32 *)dst_frame = *(u32 *)src_frame;
*(u32 *)(dst_frame + 3) = *(u32 *)(src_frame + 3);
*(u32 *)(dst_frame + 6) = *(u32 *)src_frame;
*(u32 *)(dst_frame + 9) = *(u32 *)(src_frame + 3);
break;
case 1: /* Swapped */
*(u32 *)dst_frame = *(u32 *)(src_frame + 6);
*(u32 *)(dst_frame + 3) = *(u32 *)(src_frame + 9);
*(u32 *)(dst_frame + 6) = *(u32 *)src_frame;
*(u32 *)(dst_frame + 9) = *(u32 *)(src_frame + 3);
break;
case 2: /* Digital In to All */
*(u32 *)dst_frame = *(u32 *)(src_frame + 6);
*(u32 *)(dst_frame + 3) = *(u32 *)(src_frame + 9);
*(u32 *)(dst_frame + 6) = *(u32 *)(src_frame + 6);
*(u32 *)(dst_frame + 9) = *(u32 *)(src_frame + 9);
break;
}
}
}
tascam->driver_playback_pos = (tascam->driver_playback_pos + bytes_to_frames(runtime, total_bytes_for_urb)) % runtime->buffer_size;
spin_unlock_irqrestore(&tascam->lock, flags); spin_unlock_irqrestore(&tascam->lock, flags);
@ -637,35 +811,43 @@ static void feedback_urb_complete(struct urb *urb)
struct snd_pcm_runtime *runtime; struct snd_pcm_runtime *runtime;
unsigned long flags; unsigned long flags;
u64 current_period, total_frames_in_urb = 0; u64 current_period, total_frames_in_urb = 0;
bool was_synced, sync_lost_this_urb = false;
int ret, p; int ret, p;
if (urb->status) return; if (urb->status)
if (!tascam || !atomic_read(&tascam->playback_active)) return; return;
if (!tascam || !atomic_read(&tascam->playback_active))
return;
substream = tascam->playback_substream; substream = tascam->playback_substream;
if (!substream || !substream->runtime) return; if (!substream || !substream->runtime)
return;
runtime = substream->runtime; runtime = substream->runtime;
spin_lock_irqsave(&tascam->lock, flags); spin_lock_irqsave(&tascam->lock, flags);
if (urb->status != 0) {
dev_warn_ratelimited(tascam->card->dev, "Feedback URB failed with status %d\n", urb->status); /* Let a few URBs pass to allow the hardware to stabilize. */
sync_lost_this_urb = true;
goto update_sync_state;
}
if (tascam->feedback_urb_skip_count > 0) { if (tascam->feedback_urb_skip_count > 0) {
tascam->feedback_urb_skip_count--; tascam->feedback_urb_skip_count--;
goto unlock_and_resubmit; goto unlock_and_resubmit;
} }
for (p = 0; p < urb->number_of_packets; p++) {
u8 feedback_value; /* After the initial skip, we consider the stream synced. */
const unsigned int *pattern; if (!tascam->feedback_synced) {
if (urb->iso_frame_desc[p].status != 0 || urb->iso_frame_desc[p].actual_length < 1) { dev_dbg(tascam->card->dev, "Sync Acquired!\n");
sync_lost_this_urb = true; tascam->feedback_synced = true;
continue;
} }
for (p = 0; p < urb->number_of_packets; p++) {
u8 feedback_value = 0; /* Initialize to a known invalid value */
const unsigned int *pattern;
bool packet_ok = (urb->iso_frame_desc[p].status == 0 &&
urb->iso_frame_desc[p].actual_length >= 1);
if (packet_ok)
feedback_value = *((u8 *)urb->transfer_buffer + urb->iso_frame_desc[p].offset); feedback_value = *((u8 *)urb->transfer_buffer + urb->iso_frame_desc[p].offset);
if (feedback_value >= tascam->feedback_base_value &&
if (packet_ok && feedback_value >= tascam->feedback_base_value &&
feedback_value <= tascam->feedback_max_value) { feedback_value <= tascam->feedback_max_value) {
/* Valid Feedback: Use the pattern from the table. */
pattern = tascam->feedback_patterns[feedback_value - tascam->feedback_base_value]; pattern = tascam->feedback_patterns[feedback_value - tascam->feedback_base_value];
int i; int i;
for (i = 0; i < 8; i++) { for (i = 0; i < 8; i++) {
@ -673,23 +855,27 @@ static void feedback_urb_complete(struct urb *urb)
tascam->feedback_accumulator_pattern[in_idx] = pattern[i]; tascam->feedback_accumulator_pattern[in_idx] = pattern[i];
total_frames_in_urb += pattern[i]; total_frames_in_urb += pattern[i];
} }
} else {
/* Invalid Feedback: Use the nominal pattern as a fallback. */
unsigned int nominal_frames = runtime->rate / 8000;
int i;
if (packet_ok) /* Only log if the packet itself was ok but the value was not */
dev_warn_ratelimited(tascam->card->dev, "Invalid feedback value %u, using nominal rate.\n", feedback_value);
for (i = 0; i < 8; i++) {
unsigned int in_idx = (tascam->feedback_pattern_in_idx + i) % FEEDBACK_ACCUMULATOR_SIZE;
tascam->feedback_accumulator_pattern[in_idx] = nominal_frames;
total_frames_in_urb += nominal_frames;
}
}
/* Always advance the accumulator index */
tascam->feedback_pattern_in_idx = (tascam->feedback_pattern_in_idx + 8) % FEEDBACK_ACCUMULATOR_SIZE; tascam->feedback_pattern_in_idx = (tascam->feedback_pattern_in_idx + 8) % FEEDBACK_ACCUMULATOR_SIZE;
} else {
sync_lost_this_urb = true;
total_frames_in_urb += runtime->rate / 1000;
}
}
update_sync_state:
was_synced = tascam->feedback_synced;
if (sync_lost_this_urb) {
if (was_synced) dev_dbg(tascam->card->dev, "Sync Lost!\n");
tascam->feedback_synced = false;
} else {
if (!was_synced) dev_dbg(tascam->card->dev, "Sync Acquired!\n");
tascam->feedback_synced = true;
} }
if (total_frames_in_urb > 0) if (total_frames_in_urb > 0)
tascam->playback_frames_consumed += total_frames_in_urb; tascam->playback_frames_consumed += total_frames_in_urb;
/* Check if a period has elapsed and notify ALSA */
current_period = div_u64(tascam->playback_frames_consumed, runtime->period_size); current_period = div_u64(tascam->playback_frames_consumed, runtime->period_size);
if (current_period > tascam->last_period_pos) { if (current_period > tascam->last_period_pos) {
tascam->last_period_pos = current_period; tascam->last_period_pos = current_period;
@ -697,6 +883,7 @@ update_sync_state:
snd_pcm_period_elapsed(substream); snd_pcm_period_elapsed(substream);
goto resubmit; goto resubmit;
} }
unlock_and_resubmit: unlock_and_resubmit:
spin_unlock_irqrestore(&tascam->lock, flags); spin_unlock_irqrestore(&tascam->lock, flags);
resubmit: resubmit:
@ -711,11 +898,24 @@ static int tascam_create_pcm(struct tascam_card *tascam)
struct snd_pcm *pcm; struct snd_pcm *pcm;
int err; int err;
err = snd_pcm_new(tascam->card, "US144MKII PCM", 0, 1, 1, &pcm); err = snd_pcm_new(tascam->card, "US144MKII", 0, 1, 1, &pcm);
if (err < 0) return err; if (err < 0)
return err;
err = snd_ctl_add(tascam->card, snd_ctl_new1(&tascam_latency_control, tascam));
if (err < 0)
return err;
err = snd_ctl_add(tascam->card, snd_ctl_new1(&tascam_routing_control, tascam));
if (err < 0)
return err;
err = snd_ctl_add(tascam->card, snd_ctl_new1(&tascam_samplerate_control, tascam));
if (err < 0)
return err;
tascam->pcm = pcm; tascam->pcm = pcm;
pcm->private_data = tascam; pcm->private_data = tascam;
strscpy(pcm->name, "US-144MKII Audio", sizeof(pcm->name));
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &tascam_playback_ops); snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &tascam_playback_ops);
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &tascam_capture_ops); snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &tascam_capture_ops);
snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_CONTINUOUS, snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_CONTINUOUS,
@ -823,6 +1023,10 @@ static int tascam_probe(struct usb_interface *intf, const struct usb_device_id *
card->private_free = tascam_card_private_free; card->private_free = tascam_card_private_free;
usb_set_intfdata(intf, tascam); usb_set_intfdata(intf, tascam);
spin_lock_init(&tascam->lock); spin_lock_init(&tascam->lock);
/* Initialize mixer controls to default values */
tascam->latency_profile = 2; /* Default to Normal Latency */
tascam->playback_routing = 0; /* Default to Stereo to All */
tascam->current_rate = 0; /* Not known until hw_params */
strscpy(card->driver, DRIVER_NAME, sizeof(card->driver)); strscpy(card->driver, DRIVER_NAME, sizeof(card->driver));
strscpy(card->shortname, "TASCAM US-144MKII", sizeof(card->shortname)); strscpy(card->shortname, "TASCAM US-144MKII", sizeof(card->shortname));
@ -873,6 +1077,9 @@ static int tascam_probe(struct usb_interface *intf, const struct usb_device_id *
if (err < 0) if (err < 0)
goto release_iface1_and_free_card; goto release_iface1_and_free_card;
if (device_create_file(&intf->dev, &dev_attr_driver_version))
dev_warn(&intf->dev, "Could not create sysfs attribute for driver version\n");
err = snd_card_register(card); err = snd_card_register(card);
if (err < 0) if (err < 0)
goto release_iface1_and_free_card; goto release_iface1_and_free_card;
@ -899,6 +1106,8 @@ static void tascam_disconnect(struct usb_interface *intf)
if (!tascam) if (!tascam)
return; return;
device_remove_file(&intf->dev, &dev_attr_driver_version);
if (intf != tascam->iface0) if (intf != tascam->iface0)
return; return;