playback up to spec, alsa mixers and python gui control panel
This commit is contained in:
parent
fc3a76c66e
commit
049e12fd1e
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
|
|
@ -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()
|
||||
BIN
tascam_streamer
BIN
tascam_streamer
Binary file not shown.
485
us144mkii.c
485
us144mkii.c
|
|
@ -9,43 +9,32 @@
|
|||
#include <linux/usb.h>
|
||||
#include <linux/slab.h>
|
||||
#include <linux/delay.h>
|
||||
#include <linux/printk.h>
|
||||
|
||||
#include <sound/core.h>
|
||||
#include <sound/pcm.h>
|
||||
#include <sound/pcm_params.h>
|
||||
#include <sound/initval.h>
|
||||
#include <sound/control.h>
|
||||
|
||||
MODULE_AUTHOR("serifpersia <ramiserifpersia@gmail.com>");
|
||||
MODULE_DESCRIPTION("ALSA Driver for TASCAM US-144MKII");
|
||||
MODULE_LICENSE("GPL");
|
||||
|
||||
#define DRIVER_NAME "us144mkii"
|
||||
#define DRIVER_VERSION "1.0"
|
||||
|
||||
/*
|
||||
* TODO:
|
||||
* - Implement audio input capture.
|
||||
* - Implement MIDI IN/OUT.
|
||||
* - Expose hardware features via the ALSA Control API (mixers):
|
||||
* - Digital output format selection.
|
||||
* - Input/output routing.
|
||||
*/
|
||||
|
||||
|
||||
/* --- Module Parameters --- */
|
||||
static int index[SNDRV_CARDS] = SNDRV_DEFAULT_IDX;
|
||||
static char *id[SNDRV_CARDS] = SNDRV_DEFAULT_STR;
|
||||
static bool enable[SNDRV_CARDS] = {1, [1 ... (SNDRV_CARDS - 1)] = 0};
|
||||
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 --- */
|
||||
#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 --- */
|
||||
#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_D2H_VENDOR_DEV (USB_DIR_IN | USB_TYPE_VENDOR | USB_RECIP_DEVICE)
|
||||
#define UAC_SET_CUR 0x01
|
||||
#define UAC_GET_CUR 0x81
|
||||
#define UAC_SAMPLING_FREQ_CONTROL 0x0100
|
||||
#define VENDOR_REQ_REGISTER_WRITE 0x41
|
||||
#define VENDOR_REQ_MODE_CONTROL 0x49
|
||||
|
|
@ -81,8 +72,9 @@ MODULE_PARM_DESC(playback_urb_packets, "Number of isochronous packets per playba
|
|||
|
||||
/* --- URB Configuration --- */
|
||||
#define NUM_PLAYBACK_URBS 8
|
||||
#define PLAYBACK_URB_PACKETS 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 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 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;
|
||||
|
||||
/* --- 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_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) --- */
|
||||
static const unsigned int patterns_48khz[5][8] = {
|
||||
{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}
|
||||
};
|
||||
|
||||
/* --- 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 = {
|
||||
.info = (SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_INTERLEAVED |
|
||||
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;
|
||||
|
||||
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++) {
|
||||
struct urb *urb = usb_alloc_urb(playback_urb_packets, GFP_KERNEL);
|
||||
struct urb *urb = usb_alloc_urb(PLAYBACK_URB_PACKETS, GFP_KERNEL);
|
||||
if (!urb)
|
||||
goto error;
|
||||
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_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 */
|
||||
nominal_frames_per_packet = runtime->rate / 8000;
|
||||
for (i = 0; i < FEEDBACK_ACCUMULATOR_SIZE; i++)
|
||||
tascam->feedback_accumulator_pattern[i] = nominal_frames_per_packet;
|
||||
|
||||
/*
|
||||
* Configure URBs based on the fixed hardware profile for the current sample rate.
|
||||
* The hardware latency is fixed per rate; we use the lowest latency setting.
|
||||
*/
|
||||
switch (runtime->rate) {
|
||||
case 44100: feedback_packets = 1; break;
|
||||
case 48000: feedback_packets = 1; break;
|
||||
case 88200: feedback_packets = 1; break;
|
||||
case 96000: feedback_packets = 1; break;
|
||||
default: feedback_packets = 1; break; /* Failsafe */
|
||||
/* Validate and apply latency profile */
|
||||
switch (tascam->latency_profile) {
|
||||
case 1:
|
||||
case 2:
|
||||
case 5:
|
||||
feedback_packets = tascam->latency_profile;
|
||||
break;
|
||||
default:
|
||||
dev_warn(tascam->card->dev, "Invalid latency_profile value %d, falling back to default (2).\n", tascam->latency_profile);
|
||||
tascam->latency_profile = 2;
|
||||
feedback_packets = 2;
|
||||
}
|
||||
|
||||
dev_info(tascam->card->dev, "Prepare: Using fixed hardware profile for %u Hz (%u feedback packets)\n",
|
||||
runtime->rate, feedback_packets);
|
||||
dev_info(tascam->card->dev, "Prepare: Using latency profile %u (%u feedback packets) for %u Hz\n",
|
||||
tascam->latency_profile, feedback_packets, runtime->rate);
|
||||
|
||||
/* Configure Feedback URBs */
|
||||
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 */
|
||||
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++) {
|
||||
struct urb *urb = tascam->playback_urbs[u];
|
||||
|
||||
memset(urb->transfer_buffer, 0, tascam->playback_urb_alloc_size);
|
||||
urb->transfer_buffer_length = total_bytes_in_urb;
|
||||
urb->number_of_packets = playback_urb_packets;
|
||||
for (i = 0; i < playback_urb_packets; i++) {
|
||||
urb->number_of_packets = PLAYBACK_URB_PACKETS;
|
||||
for (i = 0; i < PLAYBACK_URB_PACKETS; i++) {
|
||||
urb->iso_frame_desc[i].offset = i * 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;
|
||||
unsigned long flags;
|
||||
char *src_buf, *dst_buf;
|
||||
unsigned int total_frames_for_urb = 0;
|
||||
size_t total_bytes_for_urb = 0;
|
||||
int ret, i;
|
||||
|
||||
|
|
@ -569,42 +740,7 @@ static void playback_urb_complete(struct urb *urb)
|
|||
|
||||
spin_lock_irqsave(&tascam->lock, flags);
|
||||
|
||||
/* Phase 1: Calculate the total number of frames needed for this URB. */
|
||||
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 */
|
||||
/* 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;
|
||||
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;
|
||||
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);
|
||||
|
||||
|
|
@ -637,35 +811,43 @@ static void feedback_urb_complete(struct urb *urb)
|
|||
struct snd_pcm_runtime *runtime;
|
||||
unsigned long flags;
|
||||
u64 current_period, total_frames_in_urb = 0;
|
||||
bool was_synced, sync_lost_this_urb = false;
|
||||
int ret, p;
|
||||
|
||||
if (urb->status) return;
|
||||
if (!tascam || !atomic_read(&tascam->playback_active)) return;
|
||||
if (urb->status)
|
||||
return;
|
||||
if (!tascam || !atomic_read(&tascam->playback_active))
|
||||
return;
|
||||
substream = tascam->playback_substream;
|
||||
if (!substream || !substream->runtime) return;
|
||||
if (!substream || !substream->runtime)
|
||||
return;
|
||||
runtime = substream->runtime;
|
||||
|
||||
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);
|
||||
sync_lost_this_urb = true;
|
||||
goto update_sync_state;
|
||||
}
|
||||
|
||||
/* Let a few URBs pass to allow the hardware to stabilize. */
|
||||
if (tascam->feedback_urb_skip_count > 0) {
|
||||
tascam->feedback_urb_skip_count--;
|
||||
goto unlock_and_resubmit;
|
||||
}
|
||||
for (p = 0; p < urb->number_of_packets; p++) {
|
||||
u8 feedback_value;
|
||||
const unsigned int *pattern;
|
||||
if (urb->iso_frame_desc[p].status != 0 || urb->iso_frame_desc[p].actual_length < 1) {
|
||||
sync_lost_this_urb = true;
|
||||
continue;
|
||||
|
||||
/* After the initial skip, we consider the stream synced. */
|
||||
if (!tascam->feedback_synced) {
|
||||
dev_dbg(tascam->card->dev, "Sync Acquired!\n");
|
||||
tascam->feedback_synced = true;
|
||||
}
|
||||
|
||||
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);
|
||||
if (feedback_value >= tascam->feedback_base_value &&
|
||||
|
||||
if (packet_ok && feedback_value >= tascam->feedback_base_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];
|
||||
int 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];
|
||||
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;
|
||||
} 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)
|
||||
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);
|
||||
if (current_period > tascam->last_period_pos) {
|
||||
tascam->last_period_pos = current_period;
|
||||
|
|
@ -697,6 +883,7 @@ update_sync_state:
|
|||
snd_pcm_period_elapsed(substream);
|
||||
goto resubmit;
|
||||
}
|
||||
|
||||
unlock_and_resubmit:
|
||||
spin_unlock_irqrestore(&tascam->lock, flags);
|
||||
resubmit:
|
||||
|
|
@ -711,11 +898,24 @@ static int tascam_create_pcm(struct tascam_card *tascam)
|
|||
struct snd_pcm *pcm;
|
||||
int err;
|
||||
|
||||
err = snd_pcm_new(tascam->card, "US144MKII PCM", 0, 1, 1, &pcm);
|
||||
if (err < 0) return err;
|
||||
err = snd_pcm_new(tascam->card, "US144MKII", 0, 1, 1, &pcm);
|
||||
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;
|
||||
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_CAPTURE, &tascam_capture_ops);
|
||||
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;
|
||||
usb_set_intfdata(intf, tascam);
|
||||
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->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)
|
||||
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);
|
||||
if (err < 0)
|
||||
goto release_iface1_and_free_card;
|
||||
|
|
@ -899,6 +1106,8 @@ static void tascam_disconnect(struct usb_interface *intf)
|
|||
if (!tascam)
|
||||
return;
|
||||
|
||||
device_remove_file(&intf->dev, &dev_attr_driver_version);
|
||||
|
||||
if (intf != tascam->iface0)
|
||||
return;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue