diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 10:05:51 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 10:05:51 +0000 |
commit | 5d1646d90e1f2cceb9f0828f4b28318cd0ec7744 (patch) | |
tree | a94efe259b9009378be6d90eb30d2b019d95c194 /sound/usb/line6 | |
parent | Initial commit. (diff) | |
download | linux-5d1646d90e1f2cceb9f0828f4b28318cd0ec7744.tar.xz linux-5d1646d90e1f2cceb9f0828f4b28318cd0ec7744.zip |
Adding upstream version 5.10.209.upstream/5.10.209
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'sound/usb/line6')
-rw-r--r-- | sound/usb/line6/Kconfig | 44 | ||||
-rw-r--r-- | sound/usb/line6/Makefile | 19 | ||||
-rw-r--r-- | sound/usb/line6/capture.c | 294 | ||||
-rw-r--r-- | sound/usb/line6/capture.h | 25 | ||||
-rw-r--r-- | sound/usb/line6/driver.c | 900 | ||||
-rw-r--r-- | sound/usb/line6/driver.h | 216 | ||||
-rw-r--r-- | sound/usb/line6/midi.c | 295 | ||||
-rw-r--r-- | sound/usb/line6/midi.h | 47 | ||||
-rw-r--r-- | sound/usb/line6/midibuf.c | 257 | ||||
-rw-r--r-- | sound/usb/line6/midibuf.h | 34 | ||||
-rw-r--r-- | sound/usb/line6/pcm.c | 607 | ||||
-rw-r--r-- | sound/usb/line6/pcm.h | 195 | ||||
-rw-r--r-- | sound/usb/line6/playback.c | 439 | ||||
-rw-r--r-- | sound/usb/line6/playback.h | 31 | ||||
-rw-r--r-- | sound/usb/line6/pod.c | 542 | ||||
-rw-r--r-- | sound/usb/line6/podhd.c | 559 | ||||
-rw-r--r-- | sound/usb/line6/toneport.c | 582 | ||||
-rw-r--r-- | sound/usb/line6/variax.c | 242 |
18 files changed, 5328 insertions, 0 deletions
diff --git a/sound/usb/line6/Kconfig b/sound/usb/line6/Kconfig new file mode 100644 index 000000000..ab557e5ee --- /dev/null +++ b/sound/usb/line6/Kconfig @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: GPL-2.0-only +config SND_USB_LINE6 + tristate + select SND_RAWMIDI + select SND_PCM + select SND_HWDEP + +config SND_USB_POD + tristate "Line 6 POD USB support" + select SND_USB_LINE6 + help + This is a driver for PODxt and other similar devices, + supporting the following features: + * Reading/writing individual parameters + * Reading/writing complete channel, effects setup, and amp + setup data + * Channel switching + * Virtual MIDI interface + * Tuner access + * Playback/capture/mixer device for any ALSA-compatible PCM + audio application + * Signal routing (record clean/processed guitar signal, + re-amping) + +config SND_USB_PODHD + tristate "Line 6 POD X3/HD300/400/500 USB support" + select SND_USB_LINE6 + help + This is a driver for POD X3, HD300, 400 and 500 devices. + +config SND_USB_TONEPORT + tristate "TonePort GX, UX1 and UX2 USB support" + select SND_USB_LINE6 + select NEW_LEDS + select LEDS_CLASS + help + This is a driver for TonePort GX, UX1 and UX2 devices. + +config SND_USB_VARIAX + tristate "Variax Workbench USB support" + select SND_USB_LINE6 + help + This is a driver for Variax Workbench device. + diff --git a/sound/usb/line6/Makefile b/sound/usb/line6/Makefile new file mode 100644 index 000000000..4ba98eb32 --- /dev/null +++ b/sound/usb/line6/Makefile @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: GPL-2.0 +snd-usb-line6-y := \ + capture.o \ + driver.o \ + midi.o \ + midibuf.o \ + pcm.o \ + playback.o + +snd-usb-pod-y := pod.o +snd-usb-podhd-y := podhd.o +snd-usb-toneport-y := toneport.o +snd-usb-variax-y := variax.o + +obj-$(CONFIG_SND_USB_LINE6) += snd-usb-line6.o +obj-$(CONFIG_SND_USB_POD) += snd-usb-pod.o +obj-$(CONFIG_SND_USB_PODHD) += snd-usb-podhd.o +obj-$(CONFIG_SND_USB_TONEPORT) += snd-usb-toneport.o +obj-$(CONFIG_SND_USB_VARIAX) += snd-usb-variax.o diff --git a/sound/usb/line6/capture.c b/sound/usb/line6/capture.c new file mode 100644 index 000000000..970c9bdce --- /dev/null +++ b/sound/usb/line6/capture.c @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Line 6 Linux USB driver + * + * Copyright (C) 2004-2010 Markus Grabner (grabner@icg.tugraz.at) + */ + +#include <linux/slab.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/pcm_params.h> + +#include "capture.h" +#include "driver.h" +#include "pcm.h" + +/* + Find a free URB and submit it. + must be called in line6pcm->in.lock context +*/ +static int submit_audio_in_urb(struct snd_line6_pcm *line6pcm) +{ + int index; + int i, urb_size; + int ret; + struct urb *urb_in; + + index = find_first_zero_bit(&line6pcm->in.active_urbs, + line6pcm->line6->iso_buffers); + + if (index < 0 || index >= line6pcm->line6->iso_buffers) { + dev_err(line6pcm->line6->ifcdev, "no free URB found\n"); + return -EINVAL; + } + + urb_in = line6pcm->in.urbs[index]; + urb_size = 0; + + for (i = 0; i < LINE6_ISO_PACKETS; ++i) { + struct usb_iso_packet_descriptor *fin = + &urb_in->iso_frame_desc[i]; + fin->offset = urb_size; + fin->length = line6pcm->max_packet_size_in; + urb_size += line6pcm->max_packet_size_in; + } + + urb_in->transfer_buffer = + line6pcm->in.buffer + + index * LINE6_ISO_PACKETS * line6pcm->max_packet_size_in; + urb_in->transfer_buffer_length = urb_size; + urb_in->context = line6pcm; + + ret = usb_submit_urb(urb_in, GFP_ATOMIC); + + if (ret == 0) + set_bit(index, &line6pcm->in.active_urbs); + else + dev_err(line6pcm->line6->ifcdev, + "URB in #%d submission failed (%d)\n", index, ret); + + return 0; +} + +/* + Submit all currently available capture URBs. + must be called in line6pcm->in.lock context +*/ +int line6_submit_audio_in_all_urbs(struct snd_line6_pcm *line6pcm) +{ + int ret = 0, i; + + for (i = 0; i < line6pcm->line6->iso_buffers; ++i) { + ret = submit_audio_in_urb(line6pcm); + if (ret < 0) + break; + } + + return ret; +} + +/* + Copy data into ALSA capture buffer. +*/ +void line6_capture_copy(struct snd_line6_pcm *line6pcm, char *fbuf, int fsize) +{ + struct snd_pcm_substream *substream = + get_substream(line6pcm, SNDRV_PCM_STREAM_CAPTURE); + struct snd_pcm_runtime *runtime = substream->runtime; + const int bytes_per_frame = + line6pcm->properties->bytes_per_channel * + line6pcm->properties->capture_hw.channels_max; + int frames = fsize / bytes_per_frame; + + if (runtime == NULL) + return; + + if (line6pcm->in.pos_done + frames > runtime->buffer_size) { + /* + The transferred area goes over buffer boundary, + copy two separate chunks. + */ + int len; + + len = runtime->buffer_size - line6pcm->in.pos_done; + + if (len > 0) { + memcpy(runtime->dma_area + + line6pcm->in.pos_done * bytes_per_frame, fbuf, + len * bytes_per_frame); + memcpy(runtime->dma_area, fbuf + len * bytes_per_frame, + (frames - len) * bytes_per_frame); + } else { + /* this is somewhat paranoid */ + dev_err(line6pcm->line6->ifcdev, + "driver bug: len = %d\n", len); + } + } else { + /* copy single chunk */ + memcpy(runtime->dma_area + + line6pcm->in.pos_done * bytes_per_frame, fbuf, fsize); + } + + line6pcm->in.pos_done += frames; + if (line6pcm->in.pos_done >= runtime->buffer_size) + line6pcm->in.pos_done -= runtime->buffer_size; +} + +void line6_capture_check_period(struct snd_line6_pcm *line6pcm, int length) +{ + struct snd_pcm_substream *substream = + get_substream(line6pcm, SNDRV_PCM_STREAM_CAPTURE); + + line6pcm->in.bytes += length; + if (line6pcm->in.bytes >= line6pcm->in.period) { + line6pcm->in.bytes %= line6pcm->in.period; + spin_unlock(&line6pcm->in.lock); + snd_pcm_period_elapsed(substream); + spin_lock(&line6pcm->in.lock); + } +} + +/* + * Callback for completed capture URB. + */ +static void audio_in_callback(struct urb *urb) +{ + int i, index, length = 0, shutdown = 0; + unsigned long flags; + + struct snd_line6_pcm *line6pcm = (struct snd_line6_pcm *)urb->context; + + line6pcm->in.last_frame = urb->start_frame; + + /* find index of URB */ + for (index = 0; index < line6pcm->line6->iso_buffers; ++index) + if (urb == line6pcm->in.urbs[index]) + break; + + spin_lock_irqsave(&line6pcm->in.lock, flags); + + for (i = 0; i < LINE6_ISO_PACKETS; ++i) { + char *fbuf; + int fsize; + struct usb_iso_packet_descriptor *fin = &urb->iso_frame_desc[i]; + + if (fin->status == -EXDEV) { + shutdown = 1; + break; + } + + fbuf = urb->transfer_buffer + fin->offset; + fsize = fin->actual_length; + + if (fsize > line6pcm->max_packet_size_in) { + dev_err(line6pcm->line6->ifcdev, + "driver and/or device bug: packet too large (%d > %d)\n", + fsize, line6pcm->max_packet_size_in); + } + + length += fsize; + + BUILD_BUG_ON_MSG(LINE6_ISO_PACKETS != 1, + "The following code assumes LINE6_ISO_PACKETS == 1"); + /* TODO: + * Also, if iso_buffers != 2, the prev frame is almost random at + * playback side. + * This needs to be redesigned. It should be "stable", but we may + * experience sync problems on such high-speed configs. + */ + + line6pcm->prev_fbuf = fbuf; + line6pcm->prev_fsize = fsize / + (line6pcm->properties->bytes_per_channel * + line6pcm->properties->capture_hw.channels_max); + + if (!test_bit(LINE6_STREAM_IMPULSE, &line6pcm->in.running) && + test_bit(LINE6_STREAM_PCM, &line6pcm->in.running) && + fsize > 0) + line6_capture_copy(line6pcm, fbuf, fsize); + } + + clear_bit(index, &line6pcm->in.active_urbs); + + if (test_and_clear_bit(index, &line6pcm->in.unlink_urbs)) + shutdown = 1; + + if (!shutdown) { + submit_audio_in_urb(line6pcm); + + if (!test_bit(LINE6_STREAM_IMPULSE, &line6pcm->in.running) && + test_bit(LINE6_STREAM_PCM, &line6pcm->in.running)) + line6_capture_check_period(line6pcm, length); + } + + spin_unlock_irqrestore(&line6pcm->in.lock, flags); +} + +/* open capture callback */ +static int snd_line6_capture_open(struct snd_pcm_substream *substream) +{ + int err; + struct snd_pcm_runtime *runtime = substream->runtime; + struct snd_line6_pcm *line6pcm = snd_pcm_substream_chip(substream); + + err = snd_pcm_hw_constraint_ratdens(runtime, 0, + SNDRV_PCM_HW_PARAM_RATE, + &line6pcm->properties->rates); + if (err < 0) + return err; + + line6_pcm_acquire(line6pcm, LINE6_STREAM_CAPTURE_HELPER, false); + + runtime->hw = line6pcm->properties->capture_hw; + return 0; +} + +/* close capture callback */ +static int snd_line6_capture_close(struct snd_pcm_substream *substream) +{ + struct snd_line6_pcm *line6pcm = snd_pcm_substream_chip(substream); + + line6_pcm_release(line6pcm, LINE6_STREAM_CAPTURE_HELPER); + return 0; +} + +/* capture operators */ +const struct snd_pcm_ops snd_line6_capture_ops = { + .open = snd_line6_capture_open, + .close = snd_line6_capture_close, + .hw_params = snd_line6_hw_params, + .hw_free = snd_line6_hw_free, + .prepare = snd_line6_prepare, + .trigger = snd_line6_trigger, + .pointer = snd_line6_pointer, +}; + +int line6_create_audio_in_urbs(struct snd_line6_pcm *line6pcm) +{ + struct usb_line6 *line6 = line6pcm->line6; + int i; + + line6pcm->in.urbs = kcalloc(line6->iso_buffers, sizeof(struct urb *), + GFP_KERNEL); + if (line6pcm->in.urbs == NULL) + return -ENOMEM; + + /* create audio URBs and fill in constant values: */ + for (i = 0; i < line6->iso_buffers; ++i) { + struct urb *urb; + + /* URB for audio in: */ + urb = line6pcm->in.urbs[i] = + usb_alloc_urb(LINE6_ISO_PACKETS, GFP_KERNEL); + + if (urb == NULL) + return -ENOMEM; + + urb->dev = line6->usbdev; + urb->pipe = + usb_rcvisocpipe(line6->usbdev, + line6->properties->ep_audio_r & + USB_ENDPOINT_NUMBER_MASK); + urb->transfer_flags = URB_ISO_ASAP; + urb->start_frame = -1; + urb->number_of_packets = LINE6_ISO_PACKETS; + urb->interval = LINE6_ISO_INTERVAL; + urb->error_count = 0; + urb->complete = audio_in_callback; + if (usb_urb_ep_type_check(urb)) + return -EINVAL; + } + + return 0; +} diff --git a/sound/usb/line6/capture.h b/sound/usb/line6/capture.h new file mode 100644 index 000000000..20e05a5ec --- /dev/null +++ b/sound/usb/line6/capture.h @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Line 6 Linux USB driver + * + * Copyright (C) 2004-2010 Markus Grabner (grabner@icg.tugraz.at) + */ + +#ifndef CAPTURE_H +#define CAPTURE_H + +#include <sound/pcm.h> + +#include "driver.h" +#include "pcm.h" + +extern const struct snd_pcm_ops snd_line6_capture_ops; + +extern void line6_capture_copy(struct snd_line6_pcm *line6pcm, char *fbuf, + int fsize); +extern void line6_capture_check_period(struct snd_line6_pcm *line6pcm, + int length); +extern int line6_create_audio_in_urbs(struct snd_line6_pcm *line6pcm); +extern int line6_submit_audio_in_all_urbs(struct snd_line6_pcm *line6pcm); + +#endif diff --git a/sound/usb/line6/driver.c b/sound/usb/line6/driver.c new file mode 100644 index 000000000..b67617b68 --- /dev/null +++ b/sound/usb/line6/driver.c @@ -0,0 +1,900 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Line 6 Linux USB driver + * + * Copyright (C) 2004-2010 Markus Grabner (grabner@icg.tugraz.at) + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/export.h> +#include <linux/slab.h> +#include <linux/usb.h> + +#include <sound/core.h> +#include <sound/initval.h> +#include <sound/hwdep.h> + +#include "capture.h" +#include "driver.h" +#include "midi.h" +#include "playback.h" + +#define DRIVER_AUTHOR "Markus Grabner <grabner@icg.tugraz.at>" +#define DRIVER_DESC "Line 6 USB Driver" + +/* + This is Line 6's MIDI manufacturer ID. +*/ +const unsigned char line6_midi_id[3] = { + 0x00, 0x01, 0x0c +}; +EXPORT_SYMBOL_GPL(line6_midi_id); + +/* + Code to request version of POD, Variax interface + (and maybe other devices). +*/ +static const char line6_request_version[] = { + 0xf0, 0x7e, 0x7f, 0x06, 0x01, 0xf7 +}; + +/* + Class for asynchronous messages. +*/ +struct message { + struct usb_line6 *line6; + const char *buffer; + int size; + int done; +}; + +/* + Forward declarations. +*/ +static void line6_data_received(struct urb *urb); +static int line6_send_raw_message_async_part(struct message *msg, + struct urb *urb); + +/* + Start to listen on endpoint. +*/ +static int line6_start_listen(struct usb_line6 *line6) +{ + int err; + + if (line6->properties->capabilities & LINE6_CAP_CONTROL_MIDI) { + usb_fill_int_urb(line6->urb_listen, line6->usbdev, + usb_rcvintpipe(line6->usbdev, line6->properties->ep_ctrl_r), + line6->buffer_listen, LINE6_BUFSIZE_LISTEN, + line6_data_received, line6, line6->interval); + } else { + usb_fill_bulk_urb(line6->urb_listen, line6->usbdev, + usb_rcvbulkpipe(line6->usbdev, line6->properties->ep_ctrl_r), + line6->buffer_listen, LINE6_BUFSIZE_LISTEN, + line6_data_received, line6); + } + + /* sanity checks of EP before actually submitting */ + if (usb_urb_ep_type_check(line6->urb_listen)) { + dev_err(line6->ifcdev, "invalid control EP\n"); + return -EINVAL; + } + + line6->urb_listen->actual_length = 0; + err = usb_submit_urb(line6->urb_listen, GFP_ATOMIC); + return err; +} + +/* + Stop listening on endpoint. +*/ +static void line6_stop_listen(struct usb_line6 *line6) +{ + usb_kill_urb(line6->urb_listen); +} + +/* + Send raw message in pieces of wMaxPacketSize bytes. +*/ +int line6_send_raw_message(struct usb_line6 *line6, const char *buffer, + int size) +{ + int i, done = 0; + const struct line6_properties *properties = line6->properties; + + for (i = 0; i < size; i += line6->max_packet_size) { + int partial; + const char *frag_buf = buffer + i; + int frag_size = min(line6->max_packet_size, size - i); + int retval; + + if (properties->capabilities & LINE6_CAP_CONTROL_MIDI) { + retval = usb_interrupt_msg(line6->usbdev, + usb_sndintpipe(line6->usbdev, properties->ep_ctrl_w), + (char *)frag_buf, frag_size, + &partial, LINE6_TIMEOUT); + } else { + retval = usb_bulk_msg(line6->usbdev, + usb_sndbulkpipe(line6->usbdev, properties->ep_ctrl_w), + (char *)frag_buf, frag_size, + &partial, LINE6_TIMEOUT); + } + + if (retval) { + dev_err(line6->ifcdev, + "usb_bulk_msg failed (%d)\n", retval); + break; + } + + done += frag_size; + } + + return done; +} +EXPORT_SYMBOL_GPL(line6_send_raw_message); + +/* + Notification of completion of asynchronous request transmission. +*/ +static void line6_async_request_sent(struct urb *urb) +{ + struct message *msg = (struct message *)urb->context; + + if (msg->done >= msg->size) { + usb_free_urb(urb); + kfree(msg); + } else + line6_send_raw_message_async_part(msg, urb); +} + +/* + Asynchronously send part of a raw message. +*/ +static int line6_send_raw_message_async_part(struct message *msg, + struct urb *urb) +{ + int retval; + struct usb_line6 *line6 = msg->line6; + int done = msg->done; + int bytes = min(msg->size - done, line6->max_packet_size); + + if (line6->properties->capabilities & LINE6_CAP_CONTROL_MIDI) { + usb_fill_int_urb(urb, line6->usbdev, + usb_sndintpipe(line6->usbdev, line6->properties->ep_ctrl_w), + (char *)msg->buffer + done, bytes, + line6_async_request_sent, msg, line6->interval); + } else { + usb_fill_bulk_urb(urb, line6->usbdev, + usb_sndbulkpipe(line6->usbdev, line6->properties->ep_ctrl_w), + (char *)msg->buffer + done, bytes, + line6_async_request_sent, msg); + } + + msg->done += bytes; + + /* sanity checks of EP before actually submitting */ + retval = usb_urb_ep_type_check(urb); + if (retval < 0) + goto error; + + retval = usb_submit_urb(urb, GFP_ATOMIC); + if (retval < 0) + goto error; + + return 0; + + error: + dev_err(line6->ifcdev, "%s: usb_submit_urb failed (%d)\n", + __func__, retval); + usb_free_urb(urb); + kfree(msg); + return retval; +} + +/* + Asynchronously send raw message. +*/ +int line6_send_raw_message_async(struct usb_line6 *line6, const char *buffer, + int size) +{ + struct message *msg; + struct urb *urb; + + /* create message: */ + msg = kmalloc(sizeof(struct message), GFP_ATOMIC); + if (msg == NULL) + return -ENOMEM; + + /* create URB: */ + urb = usb_alloc_urb(0, GFP_ATOMIC); + + if (urb == NULL) { + kfree(msg); + return -ENOMEM; + } + + /* set message data: */ + msg->line6 = line6; + msg->buffer = buffer; + msg->size = size; + msg->done = 0; + + /* start sending: */ + return line6_send_raw_message_async_part(msg, urb); +} +EXPORT_SYMBOL_GPL(line6_send_raw_message_async); + +/* + Send asynchronous device version request. +*/ +int line6_version_request_async(struct usb_line6 *line6) +{ + char *buffer; + int retval; + + buffer = kmemdup(line6_request_version, + sizeof(line6_request_version), GFP_ATOMIC); + if (buffer == NULL) + return -ENOMEM; + + retval = line6_send_raw_message_async(line6, buffer, + sizeof(line6_request_version)); + kfree(buffer); + return retval; +} +EXPORT_SYMBOL_GPL(line6_version_request_async); + +/* + Send sysex message in pieces of wMaxPacketSize bytes. +*/ +int line6_send_sysex_message(struct usb_line6 *line6, const char *buffer, + int size) +{ + return line6_send_raw_message(line6, buffer, + size + SYSEX_EXTRA_SIZE) - + SYSEX_EXTRA_SIZE; +} +EXPORT_SYMBOL_GPL(line6_send_sysex_message); + +/* + Allocate buffer for sysex message and prepare header. + @param code sysex message code + @param size number of bytes between code and sysex end +*/ +char *line6_alloc_sysex_buffer(struct usb_line6 *line6, int code1, int code2, + int size) +{ + char *buffer = kmalloc(size + SYSEX_EXTRA_SIZE, GFP_ATOMIC); + + if (!buffer) + return NULL; + + buffer[0] = LINE6_SYSEX_BEGIN; + memcpy(buffer + 1, line6_midi_id, sizeof(line6_midi_id)); + buffer[sizeof(line6_midi_id) + 1] = code1; + buffer[sizeof(line6_midi_id) + 2] = code2; + buffer[sizeof(line6_midi_id) + 3 + size] = LINE6_SYSEX_END; + return buffer; +} +EXPORT_SYMBOL_GPL(line6_alloc_sysex_buffer); + +/* + Notification of data received from the Line 6 device. +*/ +static void line6_data_received(struct urb *urb) +{ + struct usb_line6 *line6 = (struct usb_line6 *)urb->context; + struct midi_buffer *mb = &line6->line6midi->midibuf_in; + int done; + + if (urb->status == -ESHUTDOWN) + return; + + if (line6->properties->capabilities & LINE6_CAP_CONTROL_MIDI) { + done = + line6_midibuf_write(mb, urb->transfer_buffer, urb->actual_length); + + if (done < urb->actual_length) { + line6_midibuf_ignore(mb, done); + dev_dbg(line6->ifcdev, "%d %d buffer overflow - message skipped\n", + done, urb->actual_length); + } + + for (;;) { + done = + line6_midibuf_read(mb, line6->buffer_message, + LINE6_MIDI_MESSAGE_MAXLEN, + LINE6_MIDIBUF_READ_RX); + + if (done <= 0) + break; + + line6->message_length = done; + line6_midi_receive(line6, line6->buffer_message, done); + + if (line6->process_message) + line6->process_message(line6); + } + } else { + line6->buffer_message = urb->transfer_buffer; + line6->message_length = urb->actual_length; + if (line6->process_message) + line6->process_message(line6); + line6->buffer_message = NULL; + } + + line6_start_listen(line6); +} + +#define LINE6_READ_WRITE_STATUS_DELAY 2 /* milliseconds */ +#define LINE6_READ_WRITE_MAX_RETRIES 50 + +/* + Read data from device. +*/ +int line6_read_data(struct usb_line6 *line6, unsigned address, void *data, + unsigned datalen) +{ + struct usb_device *usbdev = line6->usbdev; + int ret; + u8 len; + unsigned count; + + if (address > 0xffff || datalen > 0xff) + return -EINVAL; + + /* query the serial number: */ + ret = usb_control_msg_send(usbdev, 0, 0x67, + USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_OUT, + (datalen << 8) | 0x21, address, NULL, 0, + LINE6_TIMEOUT, GFP_KERNEL); + if (ret) { + dev_err(line6->ifcdev, "read request failed (error %d)\n", ret); + goto exit; + } + + /* Wait for data length. We'll get 0xff until length arrives. */ + for (count = 0; count < LINE6_READ_WRITE_MAX_RETRIES; count++) { + mdelay(LINE6_READ_WRITE_STATUS_DELAY); + + ret = usb_control_msg_recv(usbdev, 0, 0x67, + USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_IN, + 0x0012, 0x0000, &len, 1, + LINE6_TIMEOUT, GFP_KERNEL); + if (ret) { + dev_err(line6->ifcdev, + "receive length failed (error %d)\n", ret); + goto exit; + } + + if (len != 0xff) + break; + } + + ret = -EIO; + if (len == 0xff) { + dev_err(line6->ifcdev, "read failed after %d retries\n", + count); + goto exit; + } else if (len != datalen) { + /* should be equal or something went wrong */ + dev_err(line6->ifcdev, + "length mismatch (expected %d, got %d)\n", + (int)datalen, len); + goto exit; + } + + /* receive the result: */ + ret = usb_control_msg_recv(usbdev, 0, 0x67, + USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_IN, + 0x0013, 0x0000, data, datalen, LINE6_TIMEOUT, + GFP_KERNEL); + if (ret) + dev_err(line6->ifcdev, "read failed (error %d)\n", ret); + +exit: + return ret; +} +EXPORT_SYMBOL_GPL(line6_read_data); + +/* + Write data to device. +*/ +int line6_write_data(struct usb_line6 *line6, unsigned address, void *data, + unsigned datalen) +{ + struct usb_device *usbdev = line6->usbdev; + int ret; + unsigned char *status; + int count; + + if (address > 0xffff || datalen > 0xffff) + return -EINVAL; + + status = kmalloc(1, GFP_KERNEL); + if (!status) + return -ENOMEM; + + ret = usb_control_msg_send(usbdev, 0, 0x67, + USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_OUT, + 0x0022, address, data, datalen, LINE6_TIMEOUT, + GFP_KERNEL); + if (ret) { + dev_err(line6->ifcdev, + "write request failed (error %d)\n", ret); + goto exit; + } + + for (count = 0; count < LINE6_READ_WRITE_MAX_RETRIES; count++) { + mdelay(LINE6_READ_WRITE_STATUS_DELAY); + + ret = usb_control_msg_recv(usbdev, 0, 0x67, + USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_IN, + 0x0012, 0x0000, status, 1, LINE6_TIMEOUT, + GFP_KERNEL); + if (ret) { + dev_err(line6->ifcdev, + "receiving status failed (error %d)\n", ret); + goto exit; + } + + if (*status != 0xff) + break; + } + + if (*status == 0xff) { + dev_err(line6->ifcdev, "write failed after %d retries\n", + count); + ret = -EIO; + } else if (*status != 0) { + dev_err(line6->ifcdev, "write failed (error %d)\n", ret); + ret = -EIO; + } +exit: + kfree(status); + return ret; +} +EXPORT_SYMBOL_GPL(line6_write_data); + +/* + Read Line 6 device serial number. + (POD, TonePort, GuitarPort) +*/ +int line6_read_serial_number(struct usb_line6 *line6, u32 *serial_number) +{ + return line6_read_data(line6, 0x80d0, serial_number, + sizeof(*serial_number)); +} +EXPORT_SYMBOL_GPL(line6_read_serial_number); + +/* + Card destructor. +*/ +static void line6_destruct(struct snd_card *card) +{ + struct usb_line6 *line6 = card->private_data; + struct usb_device *usbdev = line6->usbdev; + + /* Free buffer memory first. We cannot depend on the existence of private + * data from the (podhd) module, it may be gone already during this call + */ + kfree(line6->buffer_message); + + kfree(line6->buffer_listen); + + /* then free URBs: */ + usb_free_urb(line6->urb_listen); + line6->urb_listen = NULL; + + /* decrement reference counters: */ + usb_put_dev(usbdev); +} + +static void line6_get_usb_properties(struct usb_line6 *line6) +{ + struct usb_device *usbdev = line6->usbdev; + const struct line6_properties *properties = line6->properties; + int pipe; + struct usb_host_endpoint *ep = NULL; + + if (properties->capabilities & LINE6_CAP_CONTROL) { + if (properties->capabilities & LINE6_CAP_CONTROL_MIDI) { + pipe = usb_rcvintpipe(line6->usbdev, + line6->properties->ep_ctrl_r); + } else { + pipe = usb_rcvbulkpipe(line6->usbdev, + line6->properties->ep_ctrl_r); + } + ep = usbdev->ep_in[usb_pipeendpoint(pipe)]; + } + + /* Control data transfer properties */ + if (ep) { + line6->interval = ep->desc.bInterval; + line6->max_packet_size = le16_to_cpu(ep->desc.wMaxPacketSize); + } else { + if (properties->capabilities & LINE6_CAP_CONTROL) { + dev_err(line6->ifcdev, + "endpoint not available, using fallback values"); + } + line6->interval = LINE6_FALLBACK_INTERVAL; + line6->max_packet_size = LINE6_FALLBACK_MAXPACKETSIZE; + } + + /* Isochronous transfer properties */ + if (usbdev->speed == USB_SPEED_LOW) { + line6->intervals_per_second = USB_LOW_INTERVALS_PER_SECOND; + line6->iso_buffers = USB_LOW_ISO_BUFFERS; + } else { + line6->intervals_per_second = USB_HIGH_INTERVALS_PER_SECOND; + line6->iso_buffers = USB_HIGH_ISO_BUFFERS; + } +} + +/* Enable buffering of incoming messages, flush the buffer */ +static int line6_hwdep_open(struct snd_hwdep *hw, struct file *file) +{ + struct usb_line6 *line6 = hw->private_data; + + /* NOTE: hwdep layer provides atomicity here */ + + line6->messages.active = 1; + line6->messages.nonblock = file->f_flags & O_NONBLOCK ? 1 : 0; + + return 0; +} + +/* Stop buffering */ +static int line6_hwdep_release(struct snd_hwdep *hw, struct file *file) +{ + struct usb_line6 *line6 = hw->private_data; + + line6->messages.active = 0; + + return 0; +} + +/* Read from circular buffer, return to user */ +static long +line6_hwdep_read(struct snd_hwdep *hwdep, char __user *buf, long count, + loff_t *offset) +{ + struct usb_line6 *line6 = hwdep->private_data; + long rv = 0; + unsigned int out_count; + + if (mutex_lock_interruptible(&line6->messages.read_lock)) + return -ERESTARTSYS; + + while (kfifo_len(&line6->messages.fifo) == 0) { + mutex_unlock(&line6->messages.read_lock); + + if (line6->messages.nonblock) + return -EAGAIN; + + rv = wait_event_interruptible( + line6->messages.wait_queue, + kfifo_len(&line6->messages.fifo) != 0); + if (rv < 0) + return rv; + + if (mutex_lock_interruptible(&line6->messages.read_lock)) + return -ERESTARTSYS; + } + + if (kfifo_peek_len(&line6->messages.fifo) > count) { + /* Buffer too small; allow re-read of the current item... */ + rv = -EINVAL; + } else { + rv = kfifo_to_user(&line6->messages.fifo, buf, count, &out_count); + if (rv == 0) + rv = out_count; + } + + mutex_unlock(&line6->messages.read_lock); + return rv; +} + +/* Write directly (no buffering) to device by user*/ +static long +line6_hwdep_write(struct snd_hwdep *hwdep, const char __user *data, long count, + loff_t *offset) +{ + struct usb_line6 *line6 = hwdep->private_data; + int rv; + char *data_copy; + + if (count > line6->max_packet_size * LINE6_RAW_MESSAGES_MAXCOUNT) { + /* This is an arbitrary limit - still better than nothing... */ + return -EINVAL; + } + + data_copy = memdup_user(data, count); + if (IS_ERR(data_copy)) + return PTR_ERR(data_copy); + + rv = line6_send_raw_message(line6, data_copy, count); + + kfree(data_copy); + return rv; +} + +static __poll_t +line6_hwdep_poll(struct snd_hwdep *hwdep, struct file *file, poll_table *wait) +{ + __poll_t rv; + struct usb_line6 *line6 = hwdep->private_data; + + poll_wait(file, &line6->messages.wait_queue, wait); + + mutex_lock(&line6->messages.read_lock); + rv = kfifo_len(&line6->messages.fifo) == 0 ? 0 : EPOLLIN | EPOLLRDNORM; + mutex_unlock(&line6->messages.read_lock); + + return rv; +} + +static const struct snd_hwdep_ops hwdep_ops = { + .open = line6_hwdep_open, + .release = line6_hwdep_release, + .read = line6_hwdep_read, + .write = line6_hwdep_write, + .poll = line6_hwdep_poll, +}; + +/* Insert into circular buffer */ +static void line6_hwdep_push_message(struct usb_line6 *line6) +{ + if (!line6->messages.active) + return; + + if (kfifo_avail(&line6->messages.fifo) >= line6->message_length) { + /* No race condition here, there's only one writer */ + kfifo_in(&line6->messages.fifo, + line6->buffer_message, line6->message_length); + } /* else TODO: signal overflow */ + + wake_up_interruptible(&line6->messages.wait_queue); +} + +static int line6_hwdep_init(struct usb_line6 *line6) +{ + int err; + struct snd_hwdep *hwdep; + + /* TODO: usb_driver_claim_interface(); */ + line6->process_message = line6_hwdep_push_message; + line6->messages.active = 0; + init_waitqueue_head(&line6->messages.wait_queue); + mutex_init(&line6->messages.read_lock); + INIT_KFIFO(line6->messages.fifo); + + err = snd_hwdep_new(line6->card, "config", 0, &hwdep); + if (err < 0) + goto end; + strcpy(hwdep->name, "config"); + hwdep->iface = SNDRV_HWDEP_IFACE_LINE6; + hwdep->ops = hwdep_ops; + hwdep->private_data = line6; + hwdep->exclusive = true; + +end: + return err; +} + +static int line6_init_cap_control(struct usb_line6 *line6) +{ + int ret; + + /* initialize USB buffers: */ + line6->buffer_listen = kmalloc(LINE6_BUFSIZE_LISTEN, GFP_KERNEL); + if (!line6->buffer_listen) + return -ENOMEM; + + line6->urb_listen = usb_alloc_urb(0, GFP_KERNEL); + if (!line6->urb_listen) + return -ENOMEM; + + if (line6->properties->capabilities & LINE6_CAP_CONTROL_MIDI) { + line6->buffer_message = kmalloc(LINE6_MIDI_MESSAGE_MAXLEN, GFP_KERNEL); + if (!line6->buffer_message) + return -ENOMEM; + + ret = line6_init_midi(line6); + if (ret < 0) + return ret; + } else { + ret = line6_hwdep_init(line6); + if (ret < 0) + return ret; + } + + ret = line6_start_listen(line6); + if (ret < 0) { + dev_err(line6->ifcdev, "cannot start listening: %d\n", ret); + return ret; + } + + return 0; +} + +static void line6_startup_work(struct work_struct *work) +{ + struct usb_line6 *line6 = + container_of(work, struct usb_line6, startup_work.work); + + if (line6->startup) + line6->startup(line6); +} + +/* + Probe USB device. +*/ +int line6_probe(struct usb_interface *interface, + const struct usb_device_id *id, + const char *driver_name, + const struct line6_properties *properties, + int (*private_init)(struct usb_line6 *, const struct usb_device_id *id), + size_t data_size) +{ + struct usb_device *usbdev = interface_to_usbdev(interface); + struct snd_card *card; + struct usb_line6 *line6; + int interface_number; + int ret; + + if (WARN_ON(data_size < sizeof(*line6))) + return -EINVAL; + + /* we don't handle multiple configurations */ + if (usbdev->descriptor.bNumConfigurations != 1) + return -ENODEV; + + ret = snd_card_new(&interface->dev, + SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1, + THIS_MODULE, data_size, &card); + if (ret < 0) + return ret; + + /* store basic data: */ + line6 = card->private_data; + line6->card = card; + line6->properties = properties; + line6->usbdev = usbdev; + line6->ifcdev = &interface->dev; + INIT_DELAYED_WORK(&line6->startup_work, line6_startup_work); + + strcpy(card->id, properties->id); + strcpy(card->driver, driver_name); + strcpy(card->shortname, properties->name); + sprintf(card->longname, "Line 6 %s at USB %s", properties->name, + dev_name(line6->ifcdev)); + card->private_free = line6_destruct; + + usb_set_intfdata(interface, line6); + + /* increment reference counters: */ + usb_get_dev(usbdev); + + /* initialize device info: */ + dev_info(&interface->dev, "Line 6 %s found\n", properties->name); + + /* query interface number */ + interface_number = interface->cur_altsetting->desc.bInterfaceNumber; + + /* TODO reserves the bus bandwidth even without actual transfer */ + ret = usb_set_interface(usbdev, interface_number, + properties->altsetting); + if (ret < 0) { + dev_err(&interface->dev, "set_interface failed\n"); + goto error; + } + + line6_get_usb_properties(line6); + + if (properties->capabilities & LINE6_CAP_CONTROL) { + ret = line6_init_cap_control(line6); + if (ret < 0) + goto error; + } + + /* initialize device data based on device: */ + ret = private_init(line6, id); + if (ret < 0) + goto error; + + /* creation of additional special files should go here */ + + dev_info(&interface->dev, "Line 6 %s now attached\n", + properties->name); + + return 0; + + error: + /* we can call disconnect callback here because no close-sync is + * needed yet at this point + */ + line6_disconnect(interface); + return ret; +} +EXPORT_SYMBOL_GPL(line6_probe); + +/* + Line 6 device disconnected. +*/ +void line6_disconnect(struct usb_interface *interface) +{ + struct usb_line6 *line6 = usb_get_intfdata(interface); + struct usb_device *usbdev = interface_to_usbdev(interface); + + if (!line6) + return; + + if (WARN_ON(usbdev != line6->usbdev)) + return; + + cancel_delayed_work_sync(&line6->startup_work); + + if (line6->urb_listen != NULL) + line6_stop_listen(line6); + + snd_card_disconnect(line6->card); + if (line6->line6pcm) + line6_pcm_disconnect(line6->line6pcm); + if (line6->disconnect) + line6->disconnect(line6); + + dev_info(&interface->dev, "Line 6 %s now disconnected\n", + line6->properties->name); + + /* make sure the device isn't destructed twice: */ + usb_set_intfdata(interface, NULL); + + snd_card_free_when_closed(line6->card); +} +EXPORT_SYMBOL_GPL(line6_disconnect); + +#ifdef CONFIG_PM + +/* + Suspend Line 6 device. +*/ +int line6_suspend(struct usb_interface *interface, pm_message_t message) +{ + struct usb_line6 *line6 = usb_get_intfdata(interface); + struct snd_line6_pcm *line6pcm = line6->line6pcm; + + snd_power_change_state(line6->card, SNDRV_CTL_POWER_D3hot); + + if (line6->properties->capabilities & LINE6_CAP_CONTROL) + line6_stop_listen(line6); + + if (line6pcm != NULL) + line6pcm->flags = 0; + + return 0; +} +EXPORT_SYMBOL_GPL(line6_suspend); + +/* + Resume Line 6 device. +*/ +int line6_resume(struct usb_interface *interface) +{ + struct usb_line6 *line6 = usb_get_intfdata(interface); + + if (line6->properties->capabilities & LINE6_CAP_CONTROL) + line6_start_listen(line6); + + snd_power_change_state(line6->card, SNDRV_CTL_POWER_D0); + return 0; +} +EXPORT_SYMBOL_GPL(line6_resume); + +#endif /* CONFIG_PM */ + +MODULE_AUTHOR(DRIVER_AUTHOR); +MODULE_DESCRIPTION(DRIVER_DESC); +MODULE_LICENSE("GPL"); + diff --git a/sound/usb/line6/driver.h b/sound/usb/line6/driver.h new file mode 100644 index 000000000..ecf3a2b39 --- /dev/null +++ b/sound/usb/line6/driver.h @@ -0,0 +1,216 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Line 6 Linux USB driver + * + * Copyright (C) 2004-2010 Markus Grabner (grabner@icg.tugraz.at) + */ + +#ifndef DRIVER_H +#define DRIVER_H + +#include <linux/usb.h> +#include <linux/mutex.h> +#include <linux/kfifo.h> +#include <sound/core.h> + +#include "midi.h" + +/* USB 1.1 speed configuration */ +#define USB_LOW_INTERVALS_PER_SECOND 1000 +#define USB_LOW_ISO_BUFFERS 2 + +/* USB 2.0+ speed configuration */ +#define USB_HIGH_INTERVALS_PER_SECOND 8000 +#define USB_HIGH_ISO_BUFFERS 16 + +/* Fallback USB interval and max packet size values */ +#define LINE6_FALLBACK_INTERVAL 10 +#define LINE6_FALLBACK_MAXPACKETSIZE 16 + +#define LINE6_TIMEOUT 1000 +#define LINE6_BUFSIZE_LISTEN 64 +#define LINE6_MIDI_MESSAGE_MAXLEN 256 + +#define LINE6_RAW_MESSAGES_MAXCOUNT_ORDER 7 +/* 4k packets are common, BUFSIZE * MAXCOUNT should be bigger... */ +#define LINE6_RAW_MESSAGES_MAXCOUNT (1 << LINE6_RAW_MESSAGES_MAXCOUNT_ORDER) + + +#if LINE6_BUFSIZE_LISTEN > 65535 +#error "Use dynamic fifo instead" +#endif + +/* + Line 6 MIDI control commands +*/ +#define LINE6_PARAM_CHANGE 0xb0 +#define LINE6_PROGRAM_CHANGE 0xc0 +#define LINE6_SYSEX_BEGIN 0xf0 +#define LINE6_SYSEX_END 0xf7 +#define LINE6_RESET 0xff + +/* + MIDI channel for messages initiated by the host + (and eventually echoed back by the device) +*/ +#define LINE6_CHANNEL_HOST 0x00 + +/* + MIDI channel for messages initiated by the device +*/ +#define LINE6_CHANNEL_DEVICE 0x02 + +#define LINE6_CHANNEL_UNKNOWN 5 /* don't know yet what this is good for */ + +#define LINE6_CHANNEL_MASK 0x0f + +extern const unsigned char line6_midi_id[3]; + +#define SYSEX_DATA_OFS (sizeof(line6_midi_id) + 3) +#define SYSEX_EXTRA_SIZE (sizeof(line6_midi_id) + 4) + +/* + Common properties of Line 6 devices. +*/ +struct line6_properties { + /* Card id string (maximum 16 characters). + * This can be used to address the device in ALSA programs as + * "default:CARD=<id>" + */ + const char *id; + + /* Card short name (maximum 32 characters) */ + const char *name; + + /* Bit vector defining this device's capabilities in line6usb driver */ + int capabilities; + + int altsetting; + + unsigned int ctrl_if; + unsigned int ep_ctrl_r; + unsigned int ep_ctrl_w; + unsigned int ep_audio_r; + unsigned int ep_audio_w; +}; + +/* Capability bits */ +enum { + /* device supports settings parameter via USB */ + LINE6_CAP_CONTROL = 1 << 0, + /* device supports PCM input/output via USB */ + LINE6_CAP_PCM = 1 << 1, + /* device supports hardware monitoring */ + LINE6_CAP_HWMON = 1 << 2, + /* device requires output data when input is read */ + LINE6_CAP_IN_NEEDS_OUT = 1 << 3, + /* device uses raw MIDI via USB (data endpoints) */ + LINE6_CAP_CONTROL_MIDI = 1 << 4, + /* device provides low-level information */ + LINE6_CAP_CONTROL_INFO = 1 << 5, + /* device provides hardware monitoring volume control */ + LINE6_CAP_HWMON_CTL = 1 << 6, +}; + +/* + Common data shared by all Line 6 devices. + Corresponds to a pair of USB endpoints. +*/ +struct usb_line6 { + /* USB device */ + struct usb_device *usbdev; + + /* Properties */ + const struct line6_properties *properties; + + /* Interval for data USB packets */ + int interval; + /* ...for isochronous transfers framing */ + int intervals_per_second; + + /* Number of isochronous URBs used for frame transfers */ + int iso_buffers; + + /* Maximum size of data USB packet */ + int max_packet_size; + + /* Device representing the USB interface */ + struct device *ifcdev; + + /* Line 6 sound card data structure. + * Each device has at least MIDI or PCM. + */ + struct snd_card *card; + + /* Line 6 PCM device data structure */ + struct snd_line6_pcm *line6pcm; + + /* Line 6 MIDI device data structure */ + struct snd_line6_midi *line6midi; + + /* URB for listening to POD data endpoint */ + struct urb *urb_listen; + + /* Buffer for incoming data from POD data endpoint */ + unsigned char *buffer_listen; + + /* Buffer for message to be processed, generated from MIDI layer */ + unsigned char *buffer_message; + + /* Length of message to be processed, generated from MIDI layer */ + int message_length; + + /* Circular buffer for non-MIDI control messages */ + struct { + struct mutex read_lock; + wait_queue_head_t wait_queue; + unsigned int active:1; + unsigned int nonblock:1; + STRUCT_KFIFO_REC_2(LINE6_BUFSIZE_LISTEN * LINE6_RAW_MESSAGES_MAXCOUNT) + fifo; + } messages; + + /* Work for delayed PCM startup */ + struct delayed_work startup_work; + + /* If MIDI is supported, buffer_message contains the pre-processed data; + * otherwise the data is only in urb_listen (buffer_incoming). + */ + void (*process_message)(struct usb_line6 *); + void (*disconnect)(struct usb_line6 *line6); + void (*startup)(struct usb_line6 *line6); +}; + +extern char *line6_alloc_sysex_buffer(struct usb_line6 *line6, int code1, + int code2, int size); +extern int line6_read_data(struct usb_line6 *line6, unsigned address, + void *data, unsigned datalen); +extern int line6_read_serial_number(struct usb_line6 *line6, + u32 *serial_number); +extern int line6_send_raw_message(struct usb_line6 *line6, + const char *buffer, int size); +extern int line6_send_raw_message_async(struct usb_line6 *line6, + const char *buffer, int size); +extern int line6_send_sysex_message(struct usb_line6 *line6, + const char *buffer, int size); +extern ssize_t line6_set_raw(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count); +extern int line6_version_request_async(struct usb_line6 *line6); +extern int line6_write_data(struct usb_line6 *line6, unsigned address, + void *data, unsigned datalen); + +int line6_probe(struct usb_interface *interface, + const struct usb_device_id *id, + const char *driver_name, + const struct line6_properties *properties, + int (*private_init)(struct usb_line6 *, const struct usb_device_id *id), + size_t data_size); + +void line6_disconnect(struct usb_interface *interface); + +#ifdef CONFIG_PM +int line6_suspend(struct usb_interface *interface, pm_message_t message); +int line6_resume(struct usb_interface *interface); +#endif + +#endif diff --git a/sound/usb/line6/midi.c b/sound/usb/line6/midi.c new file mode 100644 index 000000000..0838632c7 --- /dev/null +++ b/sound/usb/line6/midi.c @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Line 6 Linux USB driver + * + * Copyright (C) 2004-2010 Markus Grabner (grabner@icg.tugraz.at) + */ + +#include <linux/slab.h> +#include <linux/usb.h> +#include <linux/export.h> +#include <sound/core.h> +#include <sound/rawmidi.h> + +#include "driver.h" +#include "midi.h" + +#define line6_rawmidi_substream_midi(substream) \ + ((struct snd_line6_midi *)((substream)->rmidi->private_data)) + +static int send_midi_async(struct usb_line6 *line6, unsigned char *data, + int length); + +/* + Pass data received via USB to MIDI. +*/ +void line6_midi_receive(struct usb_line6 *line6, unsigned char *data, + int length) +{ + if (line6->line6midi->substream_receive) + snd_rawmidi_receive(line6->line6midi->substream_receive, + data, length); +} + +/* + Read data from MIDI buffer and transmit them via USB. +*/ +static void line6_midi_transmit(struct snd_rawmidi_substream *substream) +{ + struct usb_line6 *line6 = + line6_rawmidi_substream_midi(substream)->line6; + struct snd_line6_midi *line6midi = line6->line6midi; + struct midi_buffer *mb = &line6midi->midibuf_out; + unsigned char chunk[LINE6_FALLBACK_MAXPACKETSIZE]; + int req, done; + + for (;;) { + req = min3(line6_midibuf_bytes_free(mb), line6->max_packet_size, + LINE6_FALLBACK_MAXPACKETSIZE); + done = snd_rawmidi_transmit_peek(substream, chunk, req); + + if (done == 0) + break; + + line6_midibuf_write(mb, chunk, done); + snd_rawmidi_transmit_ack(substream, done); + } + + for (;;) { + done = line6_midibuf_read(mb, chunk, + LINE6_FALLBACK_MAXPACKETSIZE, + LINE6_MIDIBUF_READ_TX); + + if (done == 0) + break; + + send_midi_async(line6, chunk, done); + } +} + +/* + Notification of completion of MIDI transmission. +*/ +static void midi_sent(struct urb *urb) +{ + unsigned long flags; + int status; + int num; + struct usb_line6 *line6 = (struct usb_line6 *)urb->context; + + status = urb->status; + kfree(urb->transfer_buffer); + usb_free_urb(urb); + + if (status == -ESHUTDOWN) + return; + + spin_lock_irqsave(&line6->line6midi->lock, flags); + num = --line6->line6midi->num_active_send_urbs; + + if (num == 0) { + line6_midi_transmit(line6->line6midi->substream_transmit); + num = line6->line6midi->num_active_send_urbs; + } + + if (num == 0) + wake_up(&line6->line6midi->send_wait); + + spin_unlock_irqrestore(&line6->line6midi->lock, flags); +} + +/* + Send an asynchronous MIDI message. + Assumes that line6->line6midi->lock is held + (i.e., this function is serialized). +*/ +static int send_midi_async(struct usb_line6 *line6, unsigned char *data, + int length) +{ + struct urb *urb; + int retval; + unsigned char *transfer_buffer; + + urb = usb_alloc_urb(0, GFP_ATOMIC); + + if (urb == NULL) + return -ENOMEM; + + transfer_buffer = kmemdup(data, length, GFP_ATOMIC); + + if (transfer_buffer == NULL) { + usb_free_urb(urb); + return -ENOMEM; + } + + usb_fill_int_urb(urb, line6->usbdev, + usb_sndintpipe(line6->usbdev, + line6->properties->ep_ctrl_w), + transfer_buffer, length, midi_sent, line6, + line6->interval); + urb->actual_length = 0; + retval = usb_urb_ep_type_check(urb); + if (retval < 0) + goto error; + + retval = usb_submit_urb(urb, GFP_ATOMIC); + if (retval < 0) + goto error; + + ++line6->line6midi->num_active_send_urbs; + return 0; + + error: + dev_err(line6->ifcdev, "usb_submit_urb failed\n"); + usb_free_urb(urb); + return retval; +} + +static int line6_midi_output_open(struct snd_rawmidi_substream *substream) +{ + return 0; +} + +static int line6_midi_output_close(struct snd_rawmidi_substream *substream) +{ + return 0; +} + +static void line6_midi_output_trigger(struct snd_rawmidi_substream *substream, + int up) +{ + unsigned long flags; + struct usb_line6 *line6 = + line6_rawmidi_substream_midi(substream)->line6; + + line6->line6midi->substream_transmit = substream; + spin_lock_irqsave(&line6->line6midi->lock, flags); + + if (line6->line6midi->num_active_send_urbs == 0) + line6_midi_transmit(substream); + + spin_unlock_irqrestore(&line6->line6midi->lock, flags); +} + +static void line6_midi_output_drain(struct snd_rawmidi_substream *substream) +{ + struct usb_line6 *line6 = + line6_rawmidi_substream_midi(substream)->line6; + struct snd_line6_midi *midi = line6->line6midi; + + wait_event_interruptible(midi->send_wait, + midi->num_active_send_urbs == 0); +} + +static int line6_midi_input_open(struct snd_rawmidi_substream *substream) +{ + return 0; +} + +static int line6_midi_input_close(struct snd_rawmidi_substream *substream) +{ + return 0; +} + +static void line6_midi_input_trigger(struct snd_rawmidi_substream *substream, + int up) +{ + struct usb_line6 *line6 = + line6_rawmidi_substream_midi(substream)->line6; + + if (up) + line6->line6midi->substream_receive = substream; + else + line6->line6midi->substream_receive = NULL; +} + +static const struct snd_rawmidi_ops line6_midi_output_ops = { + .open = line6_midi_output_open, + .close = line6_midi_output_close, + .trigger = line6_midi_output_trigger, + .drain = line6_midi_output_drain, +}; + +static const struct snd_rawmidi_ops line6_midi_input_ops = { + .open = line6_midi_input_open, + .close = line6_midi_input_close, + .trigger = line6_midi_input_trigger, +}; + +/* Create a MIDI device */ +static int snd_line6_new_midi(struct usb_line6 *line6, + struct snd_rawmidi **rmidi_ret) +{ + struct snd_rawmidi *rmidi; + int err; + + err = snd_rawmidi_new(line6->card, "Line 6 MIDI", 0, 1, 1, rmidi_ret); + if (err < 0) + return err; + + rmidi = *rmidi_ret; + strcpy(rmidi->id, line6->properties->id); + strcpy(rmidi->name, line6->properties->name); + + rmidi->info_flags = + SNDRV_RAWMIDI_INFO_OUTPUT | + SNDRV_RAWMIDI_INFO_INPUT | SNDRV_RAWMIDI_INFO_DUPLEX; + + snd_rawmidi_set_ops(rmidi, SNDRV_RAWMIDI_STREAM_OUTPUT, + &line6_midi_output_ops); + snd_rawmidi_set_ops(rmidi, SNDRV_RAWMIDI_STREAM_INPUT, + &line6_midi_input_ops); + return 0; +} + +/* MIDI device destructor */ +static void snd_line6_midi_free(struct snd_rawmidi *rmidi) +{ + struct snd_line6_midi *line6midi = rmidi->private_data; + + line6_midibuf_destroy(&line6midi->midibuf_in); + line6_midibuf_destroy(&line6midi->midibuf_out); + kfree(line6midi); +} + +/* + Initialize the Line 6 MIDI subsystem. +*/ +int line6_init_midi(struct usb_line6 *line6) +{ + int err; + struct snd_rawmidi *rmidi; + struct snd_line6_midi *line6midi; + + if (!(line6->properties->capabilities & LINE6_CAP_CONTROL_MIDI)) { + /* skip MIDI initialization and report success */ + return 0; + } + + err = snd_line6_new_midi(line6, &rmidi); + if (err < 0) + return err; + + line6midi = kzalloc(sizeof(struct snd_line6_midi), GFP_KERNEL); + if (!line6midi) + return -ENOMEM; + + rmidi->private_data = line6midi; + rmidi->private_free = snd_line6_midi_free; + + init_waitqueue_head(&line6midi->send_wait); + spin_lock_init(&line6midi->lock); + line6midi->line6 = line6; + + err = line6_midibuf_init(&line6midi->midibuf_in, MIDI_BUFFER_SIZE, 0); + if (err < 0) + return err; + + err = line6_midibuf_init(&line6midi->midibuf_out, MIDI_BUFFER_SIZE, 1); + if (err < 0) + return err; + + line6->line6midi = line6midi; + return 0; +} +EXPORT_SYMBOL_GPL(line6_init_midi); diff --git a/sound/usb/line6/midi.h b/sound/usb/line6/midi.h new file mode 100644 index 000000000..918754e79 --- /dev/null +++ b/sound/usb/line6/midi.h @@ -0,0 +1,47 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Line 6 Linux USB driver + * + * Copyright (C) 2004-2010 Markus Grabner (grabner@icg.tugraz.at) + */ + +#ifndef MIDI_H +#define MIDI_H + +#include <sound/rawmidi.h> + +#include "midibuf.h" + +#define MIDI_BUFFER_SIZE 1024 + +struct snd_line6_midi { + /* Pointer back to the Line 6 driver data structure */ + struct usb_line6 *line6; + + /* MIDI substream for receiving (or NULL if not active) */ + struct snd_rawmidi_substream *substream_receive; + + /* MIDI substream for transmitting (or NULL if not active) */ + struct snd_rawmidi_substream *substream_transmit; + + /* Number of currently active MIDI send URBs */ + int num_active_send_urbs; + + /* Spin lock to protect MIDI buffer handling */ + spinlock_t lock; + + /* Wait queue for MIDI transmission */ + wait_queue_head_t send_wait; + + /* Buffer for incoming MIDI stream */ + struct midi_buffer midibuf_in; + + /* Buffer for outgoing MIDI stream */ + struct midi_buffer midibuf_out; +}; + +extern int line6_init_midi(struct usb_line6 *line6); +extern void line6_midi_receive(struct usb_line6 *line6, unsigned char *data, + int length); + +#endif diff --git a/sound/usb/line6/midibuf.c b/sound/usb/line6/midibuf.c new file mode 100644 index 000000000..e7f830f75 --- /dev/null +++ b/sound/usb/line6/midibuf.c @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Line 6 Linux USB driver + * + * Copyright (C) 2004-2010 Markus Grabner (grabner@icg.tugraz.at) + */ + +#include <linux/slab.h> + +#include "midibuf.h" + + +static int midibuf_message_length(unsigned char code) +{ + int message_length; + + if (code < 0x80) + message_length = -1; + else if (code < 0xf0) { + static const int length[] = { 3, 3, 3, 3, 2, 2, 3 }; + + message_length = length[(code >> 4) - 8]; + } else { + static const int length[] = { -1, 2, 2, 2, -1, -1, 1, 1, 1, -1, + 1, 1, 1, -1, 1, 1 + }; + message_length = length[code & 0x0f]; + } + + return message_length; +} + +static int midibuf_is_empty(struct midi_buffer *this) +{ + return (this->pos_read == this->pos_write) && !this->full; +} + +static int midibuf_is_full(struct midi_buffer *this) +{ + return this->full; +} + +void line6_midibuf_reset(struct midi_buffer *this) +{ + this->pos_read = this->pos_write = this->full = 0; + this->command_prev = -1; +} + +int line6_midibuf_init(struct midi_buffer *this, int size, int split) +{ + this->buf = kmalloc(size, GFP_KERNEL); + + if (this->buf == NULL) + return -ENOMEM; + + this->size = size; + this->split = split; + line6_midibuf_reset(this); + return 0; +} + +int line6_midibuf_bytes_free(struct midi_buffer *this) +{ + return + midibuf_is_full(this) ? + 0 : + (this->pos_read - this->pos_write + this->size - 1) % this->size + + 1; +} + +int line6_midibuf_bytes_used(struct midi_buffer *this) +{ + return + midibuf_is_empty(this) ? + 0 : + (this->pos_write - this->pos_read + this->size - 1) % this->size + + 1; +} + +int line6_midibuf_write(struct midi_buffer *this, unsigned char *data, + int length) +{ + int bytes_free; + int length1, length2; + int skip_active_sense = 0; + + if (midibuf_is_full(this) || (length <= 0)) + return 0; + + /* skip trailing active sense */ + if (data[length - 1] == 0xfe) { + --length; + skip_active_sense = 1; + } + + bytes_free = line6_midibuf_bytes_free(this); + + if (length > bytes_free) + length = bytes_free; + + if (length > 0) { + length1 = this->size - this->pos_write; + + if (length < length1) { + /* no buffer wraparound */ + memcpy(this->buf + this->pos_write, data, length); + this->pos_write += length; + } else { + /* buffer wraparound */ + length2 = length - length1; + memcpy(this->buf + this->pos_write, data, length1); + memcpy(this->buf, data + length1, length2); + this->pos_write = length2; + } + + if (this->pos_write == this->pos_read) + this->full = 1; + } + + return length + skip_active_sense; +} + +int line6_midibuf_read(struct midi_buffer *this, unsigned char *data, + int length, int read_type) +{ + int bytes_used; + int length1, length2; + int command; + int midi_length; + int repeat = 0; + int i; + + /* we need to be able to store at least a 3 byte MIDI message */ + if (length < 3) + return -EINVAL; + + if (midibuf_is_empty(this)) + return 0; + + bytes_used = line6_midibuf_bytes_used(this); + + if (length > bytes_used) + length = bytes_used; + + length1 = this->size - this->pos_read; + + command = this->buf[this->pos_read]; + /* + PODxt always has status byte lower nibble set to 0010, + when it means to send 0000, so we correct if here so + that control/program changes come on channel 1 and + sysex message status byte is correct + */ + if (read_type == LINE6_MIDIBUF_READ_RX) { + if (command == 0xb2 || command == 0xc2 || command == 0xf2) { + unsigned char fixed = command & 0xf0; + this->buf[this->pos_read] = fixed; + command = fixed; + } + } + + /* check MIDI command length */ + if (command & 0x80) { + midi_length = midibuf_message_length(command); + this->command_prev = command; + } else { + if (this->command_prev > 0) { + int midi_length_prev = + midibuf_message_length(this->command_prev); + + if (midi_length_prev > 1) { + midi_length = midi_length_prev - 1; + repeat = 1; + } else + midi_length = -1; + } else + midi_length = -1; + } + + if (midi_length < 0) { + /* search for end of message */ + if (length < length1) { + /* no buffer wraparound */ + for (i = 1; i < length; ++i) + if (this->buf[this->pos_read + i] & 0x80) + break; + + midi_length = i; + } else { + /* buffer wraparound */ + length2 = length - length1; + + for (i = 1; i < length1; ++i) + if (this->buf[this->pos_read + i] & 0x80) + break; + + if (i < length1) + midi_length = i; + else { + for (i = 0; i < length2; ++i) + if (this->buf[i] & 0x80) + break; + + midi_length = length1 + i; + } + } + + if (midi_length == length) + midi_length = -1; /* end of message not found */ + } + + if (midi_length < 0) { + if (!this->split) + return 0; /* command is not yet complete */ + } else { + if (length < midi_length) + return 0; /* command is not yet complete */ + + length = midi_length; + } + + if (length < length1) { + /* no buffer wraparound */ + memcpy(data + repeat, this->buf + this->pos_read, length); + this->pos_read += length; + } else { + /* buffer wraparound */ + length2 = length - length1; + memcpy(data + repeat, this->buf + this->pos_read, length1); + memcpy(data + repeat + length1, this->buf, length2); + this->pos_read = length2; + } + + if (repeat) + data[0] = this->command_prev; + + this->full = 0; + return length + repeat; +} + +int line6_midibuf_ignore(struct midi_buffer *this, int length) +{ + int bytes_used = line6_midibuf_bytes_used(this); + + if (length > bytes_used) + length = bytes_used; + + this->pos_read = (this->pos_read + length) % this->size; + this->full = 0; + return length; +} + +void line6_midibuf_destroy(struct midi_buffer *this) +{ + kfree(this->buf); + this->buf = NULL; +} diff --git a/sound/usb/line6/midibuf.h b/sound/usb/line6/midibuf.h new file mode 100644 index 000000000..542e8d836 --- /dev/null +++ b/sound/usb/line6/midibuf.h @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Line 6 Linux USB driver + * + * Copyright (C) 2004-2010 Markus Grabner (grabner@icg.tugraz.at) + */ + +#ifndef MIDIBUF_H +#define MIDIBUF_H + +#define LINE6_MIDIBUF_READ_TX 0 +#define LINE6_MIDIBUF_READ_RX 1 + +struct midi_buffer { + unsigned char *buf; + int size; + int split; + int pos_read, pos_write; + int full; + int command_prev; +}; + +extern int line6_midibuf_bytes_used(struct midi_buffer *mb); +extern int line6_midibuf_bytes_free(struct midi_buffer *mb); +extern void line6_midibuf_destroy(struct midi_buffer *mb); +extern int line6_midibuf_ignore(struct midi_buffer *mb, int length); +extern int line6_midibuf_init(struct midi_buffer *mb, int size, int split); +extern int line6_midibuf_read(struct midi_buffer *mb, unsigned char *data, + int length, int read_type); +extern void line6_midibuf_reset(struct midi_buffer *mb); +extern int line6_midibuf_write(struct midi_buffer *mb, unsigned char *data, + int length); + +#endif diff --git a/sound/usb/line6/pcm.c b/sound/usb/line6/pcm.c new file mode 100644 index 000000000..fdbdfb7bc --- /dev/null +++ b/sound/usb/line6/pcm.c @@ -0,0 +1,607 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Line 6 Linux USB driver + * + * Copyright (C) 2004-2010 Markus Grabner (grabner@icg.tugraz.at) + */ + +#include <linux/slab.h> +#include <linux/export.h> +#include <sound/core.h> +#include <sound/control.h> +#include <sound/pcm.h> +#include <sound/pcm_params.h> + +#include "capture.h" +#include "driver.h" +#include "playback.h" + +/* impulse response volume controls */ +static int snd_line6_impulse_volume_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 = 255; + return 0; +} + +static int snd_line6_impulse_volume_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_line6_pcm *line6pcm = snd_kcontrol_chip(kcontrol); + + ucontrol->value.integer.value[0] = line6pcm->impulse_volume; + return 0; +} + +static int snd_line6_impulse_volume_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_line6_pcm *line6pcm = snd_kcontrol_chip(kcontrol); + int value = ucontrol->value.integer.value[0]; + int err; + + if (line6pcm->impulse_volume == value) + return 0; + + line6pcm->impulse_volume = value; + if (value > 0) { + err = line6_pcm_acquire(line6pcm, LINE6_STREAM_IMPULSE, true); + if (err < 0) { + line6pcm->impulse_volume = 0; + return err; + } + } else { + line6_pcm_release(line6pcm, LINE6_STREAM_IMPULSE); + } + return 1; +} + +/* impulse response period controls */ +static int snd_line6_impulse_period_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 = 2000; + return 0; +} + +static int snd_line6_impulse_period_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_line6_pcm *line6pcm = snd_kcontrol_chip(kcontrol); + + ucontrol->value.integer.value[0] = line6pcm->impulse_period; + return 0; +} + +static int snd_line6_impulse_period_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_line6_pcm *line6pcm = snd_kcontrol_chip(kcontrol); + int value = ucontrol->value.integer.value[0]; + + if (line6pcm->impulse_period == value) + return 0; + + line6pcm->impulse_period = value; + return 1; +} + +/* + Unlink all currently active URBs. +*/ +static void line6_unlink_audio_urbs(struct snd_line6_pcm *line6pcm, + struct line6_pcm_stream *pcms) +{ + int i; + + for (i = 0; i < line6pcm->line6->iso_buffers; i++) { + if (test_bit(i, &pcms->active_urbs)) { + if (!test_and_set_bit(i, &pcms->unlink_urbs)) + usb_unlink_urb(pcms->urbs[i]); + } + } +} + +/* + Wait until unlinking of all currently active URBs has been finished. +*/ +static void line6_wait_clear_audio_urbs(struct snd_line6_pcm *line6pcm, + struct line6_pcm_stream *pcms) +{ + int timeout = HZ; + int i; + int alive; + + do { + alive = 0; + for (i = 0; i < line6pcm->line6->iso_buffers; i++) { + if (test_bit(i, &pcms->active_urbs)) + alive++; + } + if (!alive) + break; + set_current_state(TASK_UNINTERRUPTIBLE); + schedule_timeout(1); + } while (--timeout > 0); + if (alive) + dev_err(line6pcm->line6->ifcdev, + "timeout: still %d active urbs..\n", alive); +} + +static inline struct line6_pcm_stream * +get_stream(struct snd_line6_pcm *line6pcm, int direction) +{ + return (direction == SNDRV_PCM_STREAM_PLAYBACK) ? + &line6pcm->out : &line6pcm->in; +} + +/* allocate a buffer if not opened yet; + * call this in line6pcm.state_mutex + */ +static int line6_buffer_acquire(struct snd_line6_pcm *line6pcm, + struct line6_pcm_stream *pstr, int direction, int type) +{ + const int pkt_size = + (direction == SNDRV_PCM_STREAM_PLAYBACK) ? + line6pcm->max_packet_size_out : + line6pcm->max_packet_size_in; + + /* Invoked multiple times in a row so allocate once only */ + if (!test_and_set_bit(type, &pstr->opened) && !pstr->buffer) { + pstr->buffer = + kmalloc(array3_size(line6pcm->line6->iso_buffers, + LINE6_ISO_PACKETS, pkt_size), + GFP_KERNEL); + if (!pstr->buffer) + return -ENOMEM; + } + return 0; +} + +/* free a buffer if all streams are closed; + * call this in line6pcm.state_mutex + */ +static void line6_buffer_release(struct snd_line6_pcm *line6pcm, + struct line6_pcm_stream *pstr, int type) +{ + clear_bit(type, &pstr->opened); + if (!pstr->opened) { + line6_wait_clear_audio_urbs(line6pcm, pstr); + kfree(pstr->buffer); + pstr->buffer = NULL; + } +} + +/* start a PCM stream */ +static int line6_stream_start(struct snd_line6_pcm *line6pcm, int direction, + int type) +{ + unsigned long flags; + struct line6_pcm_stream *pstr = get_stream(line6pcm, direction); + int ret = 0; + + spin_lock_irqsave(&pstr->lock, flags); + if (!test_and_set_bit(type, &pstr->running) && + !(pstr->active_urbs || pstr->unlink_urbs)) { + pstr->count = 0; + /* Submit all currently available URBs */ + if (direction == SNDRV_PCM_STREAM_PLAYBACK) + ret = line6_submit_audio_out_all_urbs(line6pcm); + else + ret = line6_submit_audio_in_all_urbs(line6pcm); + } + + if (ret < 0) + clear_bit(type, &pstr->running); + spin_unlock_irqrestore(&pstr->lock, flags); + return ret; +} + +/* stop a PCM stream; this doesn't sync with the unlinked URBs */ +static void line6_stream_stop(struct snd_line6_pcm *line6pcm, int direction, + int type) +{ + unsigned long flags; + struct line6_pcm_stream *pstr = get_stream(line6pcm, direction); + + spin_lock_irqsave(&pstr->lock, flags); + clear_bit(type, &pstr->running); + if (!pstr->running) { + spin_unlock_irqrestore(&pstr->lock, flags); + line6_unlink_audio_urbs(line6pcm, pstr); + spin_lock_irqsave(&pstr->lock, flags); + if (direction == SNDRV_PCM_STREAM_CAPTURE) { + line6pcm->prev_fbuf = NULL; + line6pcm->prev_fsize = 0; + } + } + spin_unlock_irqrestore(&pstr->lock, flags); +} + +/* common PCM trigger callback */ +int snd_line6_trigger(struct snd_pcm_substream *substream, int cmd) +{ + struct snd_line6_pcm *line6pcm = snd_pcm_substream_chip(substream); + struct snd_pcm_substream *s; + int err; + + clear_bit(LINE6_FLAG_PREPARED, &line6pcm->flags); + + snd_pcm_group_for_each_entry(s, substream) { + if (s->pcm->card != substream->pcm->card) + continue; + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + if (s->stream == SNDRV_PCM_STREAM_CAPTURE && + (line6pcm->line6->properties->capabilities & + LINE6_CAP_IN_NEEDS_OUT)) { + err = line6_stream_start(line6pcm, SNDRV_PCM_STREAM_PLAYBACK, + LINE6_STREAM_CAPTURE_HELPER); + if (err < 0) + return err; + } + err = line6_stream_start(line6pcm, s->stream, + LINE6_STREAM_PCM); + if (err < 0) + return err; + break; + + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + if (s->stream == SNDRV_PCM_STREAM_CAPTURE && + (line6pcm->line6->properties->capabilities & + LINE6_CAP_IN_NEEDS_OUT)) { + line6_stream_stop(line6pcm, SNDRV_PCM_STREAM_PLAYBACK, + LINE6_STREAM_CAPTURE_HELPER); + } + line6_stream_stop(line6pcm, s->stream, + LINE6_STREAM_PCM); + break; + + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + if (s->stream != SNDRV_PCM_STREAM_PLAYBACK) + return -EINVAL; + set_bit(LINE6_FLAG_PAUSE_PLAYBACK, &line6pcm->flags); + break; + + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + if (s->stream != SNDRV_PCM_STREAM_PLAYBACK) + return -EINVAL; + clear_bit(LINE6_FLAG_PAUSE_PLAYBACK, &line6pcm->flags); + break; + + default: + return -EINVAL; + } + } + + return 0; +} + +/* common PCM pointer callback */ +snd_pcm_uframes_t snd_line6_pointer(struct snd_pcm_substream *substream) +{ + struct snd_line6_pcm *line6pcm = snd_pcm_substream_chip(substream); + struct line6_pcm_stream *pstr = get_stream(line6pcm, substream->stream); + + return pstr->pos_done; +} + +/* Acquire and optionally start duplex streams: + * type is either LINE6_STREAM_IMPULSE or LINE6_STREAM_MONITOR + */ +int line6_pcm_acquire(struct snd_line6_pcm *line6pcm, int type, bool start) +{ + struct line6_pcm_stream *pstr; + int ret = 0, dir; + + /* TODO: We should assert SNDRV_PCM_STREAM_PLAYBACK/CAPTURE == 0/1 */ + mutex_lock(&line6pcm->state_mutex); + for (dir = 0; dir < 2; dir++) { + pstr = get_stream(line6pcm, dir); + ret = line6_buffer_acquire(line6pcm, pstr, dir, type); + if (ret < 0) + goto error; + if (!pstr->running) + line6_wait_clear_audio_urbs(line6pcm, pstr); + } + if (start) { + for (dir = 0; dir < 2; dir++) { + ret = line6_stream_start(line6pcm, dir, type); + if (ret < 0) + goto error; + } + } + error: + mutex_unlock(&line6pcm->state_mutex); + if (ret < 0) + line6_pcm_release(line6pcm, type); + return ret; +} +EXPORT_SYMBOL_GPL(line6_pcm_acquire); + +/* Stop and release duplex streams */ +void line6_pcm_release(struct snd_line6_pcm *line6pcm, int type) +{ + struct line6_pcm_stream *pstr; + int dir; + + mutex_lock(&line6pcm->state_mutex); + for (dir = 0; dir < 2; dir++) + line6_stream_stop(line6pcm, dir, type); + for (dir = 0; dir < 2; dir++) { + pstr = get_stream(line6pcm, dir); + line6_buffer_release(line6pcm, pstr, type); + } + mutex_unlock(&line6pcm->state_mutex); +} +EXPORT_SYMBOL_GPL(line6_pcm_release); + +/* common PCM hw_params callback */ +int snd_line6_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *hw_params) +{ + int ret; + struct snd_line6_pcm *line6pcm = snd_pcm_substream_chip(substream); + struct line6_pcm_stream *pstr = get_stream(line6pcm, substream->stream); + + mutex_lock(&line6pcm->state_mutex); + ret = line6_buffer_acquire(line6pcm, pstr, substream->stream, + LINE6_STREAM_PCM); + if (ret < 0) + goto error; + + pstr->period = params_period_bytes(hw_params); + error: + mutex_unlock(&line6pcm->state_mutex); + return ret; +} + +/* common PCM hw_free callback */ +int snd_line6_hw_free(struct snd_pcm_substream *substream) +{ + struct snd_line6_pcm *line6pcm = snd_pcm_substream_chip(substream); + struct line6_pcm_stream *pstr = get_stream(line6pcm, substream->stream); + + mutex_lock(&line6pcm->state_mutex); + line6_buffer_release(line6pcm, pstr, LINE6_STREAM_PCM); + mutex_unlock(&line6pcm->state_mutex); + return 0; +} + + +/* control info callback */ +static int snd_line6_control_playback_info(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_info *uinfo) +{ + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 2; + uinfo->value.integer.min = 0; + uinfo->value.integer.max = 256; + return 0; +} + +/* control get callback */ +static int snd_line6_control_playback_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + int i; + struct snd_line6_pcm *line6pcm = snd_kcontrol_chip(kcontrol); + + for (i = 0; i < 2; i++) + ucontrol->value.integer.value[i] = line6pcm->volume_playback[i]; + + return 0; +} + +/* control put callback */ +static int snd_line6_control_playback_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + int i, changed = 0; + struct snd_line6_pcm *line6pcm = snd_kcontrol_chip(kcontrol); + + for (i = 0; i < 2; i++) + if (line6pcm->volume_playback[i] != + ucontrol->value.integer.value[i]) { + line6pcm->volume_playback[i] = + ucontrol->value.integer.value[i]; + changed = 1; + } + + return changed; +} + +/* control definition */ +static const struct snd_kcontrol_new line6_controls[] = { + { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "PCM Playback Volume", + .info = snd_line6_control_playback_info, + .get = snd_line6_control_playback_get, + .put = snd_line6_control_playback_put + }, + { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Impulse Response Volume", + .info = snd_line6_impulse_volume_info, + .get = snd_line6_impulse_volume_get, + .put = snd_line6_impulse_volume_put + }, + { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Impulse Response Period", + .info = snd_line6_impulse_period_info, + .get = snd_line6_impulse_period_get, + .put = snd_line6_impulse_period_put + }, +}; + +/* + Cleanup the PCM device. +*/ +static void cleanup_urbs(struct line6_pcm_stream *pcms, int iso_buffers) +{ + int i; + + /* Most likely impossible in current code... */ + if (pcms->urbs == NULL) + return; + + for (i = 0; i < iso_buffers; i++) { + if (pcms->urbs[i]) { + usb_kill_urb(pcms->urbs[i]); + usb_free_urb(pcms->urbs[i]); + } + } + kfree(pcms->urbs); + pcms->urbs = NULL; +} + +static void line6_cleanup_pcm(struct snd_pcm *pcm) +{ + struct snd_line6_pcm *line6pcm = snd_pcm_chip(pcm); + + cleanup_urbs(&line6pcm->out, line6pcm->line6->iso_buffers); + cleanup_urbs(&line6pcm->in, line6pcm->line6->iso_buffers); + kfree(line6pcm); +} + +/* create a PCM device */ +static int snd_line6_new_pcm(struct usb_line6 *line6, struct snd_pcm **pcm_ret) +{ + struct snd_pcm *pcm; + int err; + + err = snd_pcm_new(line6->card, (char *)line6->properties->name, + 0, 1, 1, pcm_ret); + if (err < 0) + return err; + pcm = *pcm_ret; + strcpy(pcm->name, line6->properties->name); + + /* set operators */ + snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, + &snd_line6_playback_ops); + snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &snd_line6_capture_ops); + + /* pre-allocation of buffers */ + snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_CONTINUOUS, + NULL, 64 * 1024, 128 * 1024); + return 0; +} + +/* + Sync with PCM stream stops. +*/ +void line6_pcm_disconnect(struct snd_line6_pcm *line6pcm) +{ + line6_unlink_audio_urbs(line6pcm, &line6pcm->out); + line6_unlink_audio_urbs(line6pcm, &line6pcm->in); + line6_wait_clear_audio_urbs(line6pcm, &line6pcm->out); + line6_wait_clear_audio_urbs(line6pcm, &line6pcm->in); +} + +/* + Create and register the PCM device and mixer entries. + Create URBs for playback and capture. +*/ +int line6_init_pcm(struct usb_line6 *line6, + struct line6_pcm_properties *properties) +{ + int i, err; + unsigned ep_read = line6->properties->ep_audio_r; + unsigned ep_write = line6->properties->ep_audio_w; + struct snd_pcm *pcm; + struct snd_line6_pcm *line6pcm; + + if (!(line6->properties->capabilities & LINE6_CAP_PCM)) + return 0; /* skip PCM initialization and report success */ + + err = snd_line6_new_pcm(line6, &pcm); + if (err < 0) + return err; + + line6pcm = kzalloc(sizeof(*line6pcm), GFP_KERNEL); + if (!line6pcm) + return -ENOMEM; + + mutex_init(&line6pcm->state_mutex); + line6pcm->pcm = pcm; + line6pcm->properties = properties; + line6pcm->volume_playback[0] = line6pcm->volume_playback[1] = 255; + line6pcm->volume_monitor = 255; + line6pcm->line6 = line6; + + spin_lock_init(&line6pcm->out.lock); + spin_lock_init(&line6pcm->in.lock); + line6pcm->impulse_period = LINE6_IMPULSE_DEFAULT_PERIOD; + + line6->line6pcm = line6pcm; + + pcm->private_data = line6pcm; + pcm->private_free = line6_cleanup_pcm; + + line6pcm->max_packet_size_in = + usb_maxpacket(line6->usbdev, + usb_rcvisocpipe(line6->usbdev, ep_read), 0); + line6pcm->max_packet_size_out = + usb_maxpacket(line6->usbdev, + usb_sndisocpipe(line6->usbdev, ep_write), 1); + if (!line6pcm->max_packet_size_in || !line6pcm->max_packet_size_out) { + dev_err(line6pcm->line6->ifcdev, + "cannot get proper max packet size\n"); + return -EINVAL; + } + + err = line6_create_audio_out_urbs(line6pcm); + if (err < 0) + return err; + + err = line6_create_audio_in_urbs(line6pcm); + if (err < 0) + return err; + + /* mixer: */ + for (i = 0; i < ARRAY_SIZE(line6_controls); i++) { + err = snd_ctl_add(line6->card, + snd_ctl_new1(&line6_controls[i], line6pcm)); + if (err < 0) + return err; + } + + return 0; +} +EXPORT_SYMBOL_GPL(line6_init_pcm); + +/* prepare pcm callback */ +int snd_line6_prepare(struct snd_pcm_substream *substream) +{ + struct snd_line6_pcm *line6pcm = snd_pcm_substream_chip(substream); + struct line6_pcm_stream *pstr = get_stream(line6pcm, substream->stream); + + mutex_lock(&line6pcm->state_mutex); + if (!pstr->running) + line6_wait_clear_audio_urbs(line6pcm, pstr); + + if (!test_and_set_bit(LINE6_FLAG_PREPARED, &line6pcm->flags)) { + line6pcm->out.count = 0; + line6pcm->out.pos = 0; + line6pcm->out.pos_done = 0; + line6pcm->out.bytes = 0; + line6pcm->in.count = 0; + line6pcm->in.pos_done = 0; + line6pcm->in.bytes = 0; + } + + mutex_unlock(&line6pcm->state_mutex); + return 0; +} diff --git a/sound/usb/line6/pcm.h b/sound/usb/line6/pcm.h new file mode 100644 index 000000000..9c683042f --- /dev/null +++ b/sound/usb/line6/pcm.h @@ -0,0 +1,195 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Line 6 Linux USB driver + * + * Copyright (C) 2004-2010 Markus Grabner (grabner@icg.tugraz.at) + */ + +/* + PCM interface to POD series devices. +*/ + +#ifndef PCM_H +#define PCM_H + +#include <sound/pcm.h> + +#include "driver.h" + +/* + number of USB frames per URB + The Line 6 Windows driver always transmits two frames per packet, but + the Linux driver performs significantly better (i.e., lower latency) + with only one frame per packet. +*/ +#define LINE6_ISO_PACKETS 1 + +/* in a "full speed" device (such as the PODxt Pro) this means 1ms, + * for "high speed" it's 1/8ms + */ +#define LINE6_ISO_INTERVAL 1 + +#define LINE6_IMPULSE_DEFAULT_PERIOD 100 + +/* + Get substream from Line 6 PCM data structure +*/ +#define get_substream(line6pcm, stream) \ + (line6pcm->pcm->streams[stream].substream) + +/* + PCM mode bits. + + There are several features of the Line 6 USB driver which require PCM + data to be exchanged with the device: + *) PCM playback and capture via ALSA + *) software monitoring (for devices without hardware monitoring) + *) optional impulse response measurement + However, from the device's point of view, there is just a single + capture and playback stream, which must be shared between these + subsystems. It is therefore necessary to maintain the state of the + subsystems with respect to PCM usage. + + We define two bit flags, "opened" and "running", for each playback + or capture stream. Both can contain the bit flag corresponding to + LINE6_STREAM_* type, + LINE6_STREAM_PCM = ALSA PCM playback or capture + LINE6_STREAM_MONITOR = software monitoring + IMPULSE = optional impulse response measurement + The opened flag indicates whether the buffer is allocated while + the running flag indicates whether the stream is running. + + For monitor or impulse operations, the driver needs to call + line6_pcm_acquire() or line6_pcm_release() with the appropriate + LINE6_STREAM_* flag. +*/ + +/* stream types */ +enum { + LINE6_STREAM_PCM, + LINE6_STREAM_MONITOR, + LINE6_STREAM_IMPULSE, + LINE6_STREAM_CAPTURE_HELPER, +}; + +/* misc bit flags for PCM operation */ +enum { + LINE6_FLAG_PAUSE_PLAYBACK, + LINE6_FLAG_PREPARED, +}; + +struct line6_pcm_properties { + struct snd_pcm_hardware playback_hw, capture_hw; + struct snd_pcm_hw_constraint_ratdens rates; + int bytes_per_channel; +}; + +struct line6_pcm_stream { + /* allocated URBs */ + struct urb **urbs; + + /* Temporary buffer; + * Since the packet size is not known in advance, this buffer is + * large enough to store maximum size packets. + */ + unsigned char *buffer; + + /* Free frame position in the buffer. */ + snd_pcm_uframes_t pos; + + /* Count processed bytes; + * This is modulo period size (to determine when a period is finished). + */ + unsigned bytes; + + /* Counter to create desired sample rate */ + unsigned count; + + /* period size in bytes */ + unsigned period; + + /* Processed frame position in the buffer; + * The contents of the ring buffer have been consumed by the USB + * subsystem (i.e., sent to the USB device) up to this position. + */ + snd_pcm_uframes_t pos_done; + + /* Bit mask of active URBs */ + unsigned long active_urbs; + + /* Bit mask of URBs currently being unlinked */ + unsigned long unlink_urbs; + + /* Spin lock to protect updates of the buffer positions (not contents) + */ + spinlock_t lock; + + /* Bit flags for operational stream types */ + unsigned long opened; + + /* Bit flags for running stream types */ + unsigned long running; + + int last_frame; +}; + +struct snd_line6_pcm { + /* Pointer back to the Line 6 driver data structure */ + struct usb_line6 *line6; + + /* Properties. */ + struct line6_pcm_properties *properties; + + /* ALSA pcm stream */ + struct snd_pcm *pcm; + + /* protection to state changes of in/out streams */ + struct mutex state_mutex; + + /* Capture and playback streams */ + struct line6_pcm_stream in; + struct line6_pcm_stream out; + + /* Previously captured frame (for software monitoring) */ + unsigned char *prev_fbuf; + + /* Size of previously captured frame (for software monitoring/sync) */ + int prev_fsize; + + /* Maximum size of USB packet */ + int max_packet_size_in; + int max_packet_size_out; + + /* PCM playback volume (left and right) */ + int volume_playback[2]; + + /* PCM monitor volume */ + int volume_monitor; + + /* Volume of impulse response test signal (if zero, test is disabled) */ + int impulse_volume; + + /* Period of impulse response test signal */ + int impulse_period; + + /* Counter for impulse response test signal */ + int impulse_count; + + /* Several status bits (see LINE6_FLAG_*) */ + unsigned long flags; +}; + +extern int line6_init_pcm(struct usb_line6 *line6, + struct line6_pcm_properties *properties); +extern int snd_line6_trigger(struct snd_pcm_substream *substream, int cmd); +extern int snd_line6_prepare(struct snd_pcm_substream *substream); +extern int snd_line6_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *hw_params); +extern int snd_line6_hw_free(struct snd_pcm_substream *substream); +extern snd_pcm_uframes_t snd_line6_pointer(struct snd_pcm_substream *substream); +extern void line6_pcm_disconnect(struct snd_line6_pcm *line6pcm); +extern int line6_pcm_acquire(struct snd_line6_pcm *line6pcm, int type, + bool start); +extern void line6_pcm_release(struct snd_line6_pcm *line6pcm, int type); + +#endif diff --git a/sound/usb/line6/playback.c b/sound/usb/line6/playback.c new file mode 100644 index 000000000..8233c61e2 --- /dev/null +++ b/sound/usb/line6/playback.c @@ -0,0 +1,439 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Line 6 Linux USB driver + * + * Copyright (C) 2004-2010 Markus Grabner (grabner@icg.tugraz.at) + */ + +#include <linux/slab.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/pcm_params.h> + +#include "capture.h" +#include "driver.h" +#include "pcm.h" +#include "playback.h" + +/* + Software stereo volume control. +*/ +static void change_volume(struct urb *urb_out, int volume[], + int bytes_per_frame) +{ + int chn = 0; + + if (volume[0] == 256 && volume[1] == 256) + return; /* maximum volume - no change */ + + if (bytes_per_frame == 4) { + __le16 *p, *buf_end; + + p = (__le16 *)urb_out->transfer_buffer; + buf_end = p + urb_out->transfer_buffer_length / sizeof(*p); + + for (; p < buf_end; ++p) { + short pv = le16_to_cpu(*p); + int val = (pv * volume[chn & 1]) >> 8; + pv = clamp(val, -0x8000, 0x7fff); + *p = cpu_to_le16(pv); + ++chn; + } + } else if (bytes_per_frame == 6) { + unsigned char *p, *buf_end; + + p = (unsigned char *)urb_out->transfer_buffer; + buf_end = p + urb_out->transfer_buffer_length; + + for (; p < buf_end; p += 3) { + int val; + + val = p[0] + (p[1] << 8) + ((signed char)p[2] << 16); + val = (val * volume[chn & 1]) >> 8; + val = clamp(val, -0x800000, 0x7fffff); + p[0] = val; + p[1] = val >> 8; + p[2] = val >> 16; + ++chn; + } + } +} + +/* + Create signal for impulse response test. +*/ +static void create_impulse_test_signal(struct snd_line6_pcm *line6pcm, + struct urb *urb_out, int bytes_per_frame) +{ + int frames = urb_out->transfer_buffer_length / bytes_per_frame; + + if (bytes_per_frame == 4) { + int i; + short *pi = (short *)line6pcm->prev_fbuf; + short *po = (short *)urb_out->transfer_buffer; + + for (i = 0; i < frames; ++i) { + po[0] = pi[0]; + po[1] = 0; + pi += 2; + po += 2; + } + } else if (bytes_per_frame == 6) { + int i, j; + unsigned char *pi = line6pcm->prev_fbuf; + unsigned char *po = urb_out->transfer_buffer; + + for (i = 0; i < frames; ++i) { + for (j = 0; j < bytes_per_frame / 2; ++j) + po[j] = pi[j]; + + for (; j < bytes_per_frame; ++j) + po[j] = 0; + + pi += bytes_per_frame; + po += bytes_per_frame; + } + } + if (--line6pcm->impulse_count <= 0) { + ((unsigned char *)(urb_out->transfer_buffer))[bytes_per_frame - + 1] = + line6pcm->impulse_volume; + line6pcm->impulse_count = line6pcm->impulse_period; + } +} + +/* + Add signal to buffer for software monitoring. +*/ +static void add_monitor_signal(struct urb *urb_out, unsigned char *signal, + int volume, int bytes_per_frame) +{ + if (volume == 0) + return; /* zero volume - no change */ + + if (bytes_per_frame == 4) { + __le16 *pi, *po, *buf_end; + + pi = (__le16 *)signal; + po = (__le16 *)urb_out->transfer_buffer; + buf_end = po + urb_out->transfer_buffer_length / sizeof(*po); + + for (; po < buf_end; ++pi, ++po) { + short pov = le16_to_cpu(*po); + short piv = le16_to_cpu(*pi); + int val = pov + ((piv * volume) >> 8); + pov = clamp(val, -0x8000, 0x7fff); + *po = cpu_to_le16(pov); + } + } + + /* + We don't need to handle devices with 6 bytes per frame here + since they all support hardware monitoring. + */ +} + +/* + Find a free URB, prepare audio data, and submit URB. + must be called in line6pcm->out.lock context +*/ +static int submit_audio_out_urb(struct snd_line6_pcm *line6pcm) +{ + int index; + int i, urb_size, urb_frames; + int ret; + const int bytes_per_frame = + line6pcm->properties->bytes_per_channel * + line6pcm->properties->playback_hw.channels_max; + const int frame_increment = + line6pcm->properties->rates.rats[0].num_min; + const int frame_factor = + line6pcm->properties->rates.rats[0].den * + (line6pcm->line6->intervals_per_second / LINE6_ISO_INTERVAL); + struct urb *urb_out; + + index = find_first_zero_bit(&line6pcm->out.active_urbs, + line6pcm->line6->iso_buffers); + + if (index < 0 || index >= line6pcm->line6->iso_buffers) { + dev_err(line6pcm->line6->ifcdev, "no free URB found\n"); + return -EINVAL; + } + + urb_out = line6pcm->out.urbs[index]; + urb_size = 0; + + /* TODO: this may not work for LINE6_ISO_PACKETS != 1 */ + for (i = 0; i < LINE6_ISO_PACKETS; ++i) { + /* compute frame size for given sampling rate */ + int fsize = 0; + struct usb_iso_packet_descriptor *fout = + &urb_out->iso_frame_desc[i]; + + fsize = line6pcm->prev_fsize; + if (fsize == 0) { + int n; + + line6pcm->out.count += frame_increment; + n = line6pcm->out.count / frame_factor; + line6pcm->out.count -= n * frame_factor; + fsize = n; + } + + fsize *= bytes_per_frame; + + fout->offset = urb_size; + fout->length = fsize; + urb_size += fsize; + } + + if (urb_size == 0) { + /* can't determine URB size */ + dev_err(line6pcm->line6->ifcdev, "driver bug: urb_size = 0\n"); + return -EINVAL; + } + + urb_frames = urb_size / bytes_per_frame; + urb_out->transfer_buffer = + line6pcm->out.buffer + + index * LINE6_ISO_PACKETS * line6pcm->max_packet_size_out; + urb_out->transfer_buffer_length = urb_size; + urb_out->context = line6pcm; + + if (test_bit(LINE6_STREAM_PCM, &line6pcm->out.running) && + !test_bit(LINE6_FLAG_PAUSE_PLAYBACK, &line6pcm->flags)) { + struct snd_pcm_runtime *runtime = + get_substream(line6pcm, SNDRV_PCM_STREAM_PLAYBACK)->runtime; + + if (line6pcm->out.pos + urb_frames > runtime->buffer_size) { + /* + The transferred area goes over buffer boundary, + copy the data to the temp buffer. + */ + int len; + + len = runtime->buffer_size - line6pcm->out.pos; + + if (len > 0) { + memcpy(urb_out->transfer_buffer, + runtime->dma_area + + line6pcm->out.pos * bytes_per_frame, + len * bytes_per_frame); + memcpy(urb_out->transfer_buffer + + len * bytes_per_frame, runtime->dma_area, + (urb_frames - len) * bytes_per_frame); + } else + dev_err(line6pcm->line6->ifcdev, "driver bug: len = %d\n", + len); + } else { + memcpy(urb_out->transfer_buffer, + runtime->dma_area + + line6pcm->out.pos * bytes_per_frame, + urb_out->transfer_buffer_length); + } + + line6pcm->out.pos += urb_frames; + if (line6pcm->out.pos >= runtime->buffer_size) + line6pcm->out.pos -= runtime->buffer_size; + + change_volume(urb_out, line6pcm->volume_playback, + bytes_per_frame); + } else { + memset(urb_out->transfer_buffer, 0, + urb_out->transfer_buffer_length); + } + + spin_lock_nested(&line6pcm->in.lock, SINGLE_DEPTH_NESTING); + if (line6pcm->prev_fbuf) { + if (test_bit(LINE6_STREAM_IMPULSE, &line6pcm->out.running)) { + create_impulse_test_signal(line6pcm, urb_out, + bytes_per_frame); + if (test_bit(LINE6_STREAM_PCM, &line6pcm->in.running)) { + line6_capture_copy(line6pcm, + urb_out->transfer_buffer, + urb_out-> + transfer_buffer_length); + line6_capture_check_period(line6pcm, + urb_out->transfer_buffer_length); + } + } else { + if (!(line6pcm->line6->properties->capabilities & LINE6_CAP_HWMON) + && line6pcm->out.running && line6pcm->in.running) + add_monitor_signal(urb_out, line6pcm->prev_fbuf, + line6pcm->volume_monitor, + bytes_per_frame); + } + line6pcm->prev_fbuf = NULL; + line6pcm->prev_fsize = 0; + } + spin_unlock(&line6pcm->in.lock); + + ret = usb_submit_urb(urb_out, GFP_ATOMIC); + + if (ret == 0) + set_bit(index, &line6pcm->out.active_urbs); + else + dev_err(line6pcm->line6->ifcdev, + "URB out #%d submission failed (%d)\n", index, ret); + + return 0; +} + +/* + Submit all currently available playback URBs. + must be called in line6pcm->out.lock context + */ +int line6_submit_audio_out_all_urbs(struct snd_line6_pcm *line6pcm) +{ + int ret = 0, i; + + for (i = 0; i < line6pcm->line6->iso_buffers; ++i) { + ret = submit_audio_out_urb(line6pcm); + if (ret < 0) + break; + } + + return ret; +} + +/* + Callback for completed playback URB. +*/ +static void audio_out_callback(struct urb *urb) +{ + int i, index, length = 0, shutdown = 0; + unsigned long flags; + struct snd_line6_pcm *line6pcm = (struct snd_line6_pcm *)urb->context; + struct snd_pcm_substream *substream = + get_substream(line6pcm, SNDRV_PCM_STREAM_PLAYBACK); + const int bytes_per_frame = + line6pcm->properties->bytes_per_channel * + line6pcm->properties->playback_hw.channels_max; + +#if USE_CLEAR_BUFFER_WORKAROUND + memset(urb->transfer_buffer, 0, urb->transfer_buffer_length); +#endif + + line6pcm->out.last_frame = urb->start_frame; + + /* find index of URB */ + for (index = 0; index < line6pcm->line6->iso_buffers; index++) + if (urb == line6pcm->out.urbs[index]) + break; + + if (index >= line6pcm->line6->iso_buffers) + return; /* URB has been unlinked asynchronously */ + + for (i = 0; i < LINE6_ISO_PACKETS; i++) + length += urb->iso_frame_desc[i].length; + + spin_lock_irqsave(&line6pcm->out.lock, flags); + + if (test_bit(LINE6_STREAM_PCM, &line6pcm->out.running)) { + struct snd_pcm_runtime *runtime = substream->runtime; + + line6pcm->out.pos_done += + length / bytes_per_frame; + + if (line6pcm->out.pos_done >= runtime->buffer_size) + line6pcm->out.pos_done -= runtime->buffer_size; + } + + clear_bit(index, &line6pcm->out.active_urbs); + + for (i = 0; i < LINE6_ISO_PACKETS; i++) + if (urb->iso_frame_desc[i].status == -EXDEV) { + shutdown = 1; + break; + } + + if (test_and_clear_bit(index, &line6pcm->out.unlink_urbs)) + shutdown = 1; + + if (!shutdown) { + submit_audio_out_urb(line6pcm); + + if (test_bit(LINE6_STREAM_PCM, &line6pcm->out.running)) { + line6pcm->out.bytes += length; + if (line6pcm->out.bytes >= line6pcm->out.period) { + line6pcm->out.bytes %= line6pcm->out.period; + spin_unlock(&line6pcm->out.lock); + snd_pcm_period_elapsed(substream); + spin_lock(&line6pcm->out.lock); + } + } + } + spin_unlock_irqrestore(&line6pcm->out.lock, flags); +} + +/* open playback callback */ +static int snd_line6_playback_open(struct snd_pcm_substream *substream) +{ + int err; + struct snd_pcm_runtime *runtime = substream->runtime; + struct snd_line6_pcm *line6pcm = snd_pcm_substream_chip(substream); + + err = snd_pcm_hw_constraint_ratdens(runtime, 0, SNDRV_PCM_HW_PARAM_RATE, + &line6pcm->properties->rates); + if (err < 0) + return err; + + runtime->hw = line6pcm->properties->playback_hw; + return 0; +} + +/* close playback callback */ +static int snd_line6_playback_close(struct snd_pcm_substream *substream) +{ + return 0; +} + +/* playback operators */ +const struct snd_pcm_ops snd_line6_playback_ops = { + .open = snd_line6_playback_open, + .close = snd_line6_playback_close, + .hw_params = snd_line6_hw_params, + .hw_free = snd_line6_hw_free, + .prepare = snd_line6_prepare, + .trigger = snd_line6_trigger, + .pointer = snd_line6_pointer, +}; + +int line6_create_audio_out_urbs(struct snd_line6_pcm *line6pcm) +{ + struct usb_line6 *line6 = line6pcm->line6; + int i; + + line6pcm->out.urbs = kcalloc(line6->iso_buffers, sizeof(struct urb *), + GFP_KERNEL); + if (line6pcm->out.urbs == NULL) + return -ENOMEM; + + /* create audio URBs and fill in constant values: */ + for (i = 0; i < line6->iso_buffers; ++i) { + struct urb *urb; + + /* URB for audio out: */ + urb = line6pcm->out.urbs[i] = + usb_alloc_urb(LINE6_ISO_PACKETS, GFP_KERNEL); + + if (urb == NULL) + return -ENOMEM; + + urb->dev = line6->usbdev; + urb->pipe = + usb_sndisocpipe(line6->usbdev, + line6->properties->ep_audio_w & + USB_ENDPOINT_NUMBER_MASK); + urb->transfer_flags = URB_ISO_ASAP; + urb->start_frame = -1; + urb->number_of_packets = LINE6_ISO_PACKETS; + urb->interval = LINE6_ISO_INTERVAL; + urb->error_count = 0; + urb->complete = audio_out_callback; + if (usb_urb_ep_type_check(urb)) + return -EINVAL; + } + + return 0; +} diff --git a/sound/usb/line6/playback.h b/sound/usb/line6/playback.h new file mode 100644 index 000000000..2ca832c83 --- /dev/null +++ b/sound/usb/line6/playback.h @@ -0,0 +1,31 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Line 6 Linux USB driver + * + * Copyright (C) 2004-2010 Markus Grabner (grabner@icg.tugraz.at) + */ + +#ifndef PLAYBACK_H +#define PLAYBACK_H + +#include <sound/pcm.h> + +#include "driver.h" + +/* + * When the TonePort is used with jack in full duplex mode and the outputs are + * not connected, the software monitor produces an ugly noise since everything + * written to the output buffer (i.e., the input signal) will be repeated in + * the next period (sounds like a delay effect). As a workaround, the output + * buffer is cleared after the data have been read, but there must be a better + * solution. Until one is found, this workaround can be used to fix the + * problem. + */ +#define USE_CLEAR_BUFFER_WORKAROUND 1 + +extern const struct snd_pcm_ops snd_line6_playback_ops; + +extern int line6_create_audio_out_urbs(struct snd_line6_pcm *line6pcm); +extern int line6_submit_audio_out_all_urbs(struct snd_line6_pcm *line6pcm); + +#endif diff --git a/sound/usb/line6/pod.c b/sound/usb/line6/pod.c new file mode 100644 index 000000000..54be5ac91 --- /dev/null +++ b/sound/usb/line6/pod.c @@ -0,0 +1,542 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Line 6 Linux USB driver + * + * Copyright (C) 2004-2010 Markus Grabner (grabner@icg.tugraz.at) + */ + +#include <linux/slab.h> +#include <linux/wait.h> +#include <linux/interrupt.h> +#include <linux/module.h> +#include <linux/usb.h> + +#include <sound/core.h> +#include <sound/control.h> + +#include "capture.h" +#include "driver.h" +#include "playback.h" + +/* + Locate name in binary program dump +*/ +#define POD_NAME_OFFSET 0 +#define POD_NAME_LENGTH 16 + +/* + Other constants +*/ +#define POD_CONTROL_SIZE 0x80 +#define POD_BUFSIZE_DUMPREQ 7 +#define POD_STARTUP_DELAY 1000 + +/* + Stages of POD startup procedure +*/ +enum { + POD_STARTUP_VERSIONREQ, + POD_STARTUP_SETUP, + POD_STARTUP_DONE, +}; + +enum { + LINE6_BASSPODXT, + LINE6_BASSPODXTLIVE, + LINE6_BASSPODXTPRO, + LINE6_POCKETPOD, + LINE6_PODXT, + LINE6_PODXTLIVE_POD, + LINE6_PODXTPRO, +}; + +struct usb_line6_pod { + /* Generic Line 6 USB data */ + struct usb_line6 line6; + + /* Instrument monitor level */ + int monitor_level; + + /* Current progress in startup procedure */ + int startup_progress; + + /* Serial number of device */ + u32 serial_number; + + /* Firmware version (x 100) */ + int firmware_version; + + /* Device ID */ + int device_id; +}; + +#define line6_to_pod(x) container_of(x, struct usb_line6_pod, line6) + +#define POD_SYSEX_CODE 3 + +/* *INDENT-OFF* */ + +enum { + POD_SYSEX_SAVE = 0x24, + POD_SYSEX_SYSTEM = 0x56, + POD_SYSEX_SYSTEMREQ = 0x57, + /* POD_SYSEX_UPDATE = 0x6c, */ /* software update! */ + POD_SYSEX_STORE = 0x71, + POD_SYSEX_FINISH = 0x72, + POD_SYSEX_DUMPMEM = 0x73, + POD_SYSEX_DUMP = 0x74, + POD_SYSEX_DUMPREQ = 0x75 + + /* dumps entire internal memory of PODxt Pro */ + /* POD_SYSEX_DUMPMEM2 = 0x76 */ +}; + +enum { + POD_MONITOR_LEVEL = 0x04, + POD_SYSTEM_INVALID = 0x10000 +}; + +/* *INDENT-ON* */ + +enum { + POD_DUMP_MEMORY = 2 +}; + +enum { + POD_BUSY_READ, + POD_BUSY_WRITE, + POD_CHANNEL_DIRTY, + POD_SAVE_PRESSED, + POD_BUSY_MIDISEND +}; + +static const struct snd_ratden pod_ratden = { + .num_min = 78125, + .num_max = 78125, + .num_step = 1, + .den = 2 +}; + +static struct line6_pcm_properties pod_pcm_properties = { + .playback_hw = { + .info = (SNDRV_PCM_INFO_MMAP | + SNDRV_PCM_INFO_INTERLEAVED | + SNDRV_PCM_INFO_BLOCK_TRANSFER | + SNDRV_PCM_INFO_MMAP_VALID | + SNDRV_PCM_INFO_PAUSE | + SNDRV_PCM_INFO_SYNC_START), + .formats = SNDRV_PCM_FMTBIT_S24_3LE, + .rates = SNDRV_PCM_RATE_KNOT, + .rate_min = 39062, + .rate_max = 39063, + .channels_min = 2, + .channels_max = 2, + .buffer_bytes_max = 60000, + .period_bytes_min = 64, + .period_bytes_max = 8192, + .periods_min = 1, + .periods_max = 1024}, + .capture_hw = { + .info = (SNDRV_PCM_INFO_MMAP | + SNDRV_PCM_INFO_INTERLEAVED | + SNDRV_PCM_INFO_BLOCK_TRANSFER | + SNDRV_PCM_INFO_MMAP_VALID | + SNDRV_PCM_INFO_SYNC_START), + .formats = SNDRV_PCM_FMTBIT_S24_3LE, + .rates = SNDRV_PCM_RATE_KNOT, + .rate_min = 39062, + .rate_max = 39063, + .channels_min = 2, + .channels_max = 2, + .buffer_bytes_max = 60000, + .period_bytes_min = 64, + .period_bytes_max = 8192, + .periods_min = 1, + .periods_max = 1024}, + .rates = { + .nrats = 1, + .rats = &pod_ratden}, + .bytes_per_channel = 3 /* SNDRV_PCM_FMTBIT_S24_3LE */ +}; + + +static const char pod_version_header[] = { + 0xf0, 0x7e, 0x7f, 0x06, 0x02 +}; + +static char *pod_alloc_sysex_buffer(struct usb_line6_pod *pod, int code, + int size) +{ + return line6_alloc_sysex_buffer(&pod->line6, POD_SYSEX_CODE, code, + size); +} + +/* + Process a completely received message. +*/ +static void line6_pod_process_message(struct usb_line6 *line6) +{ + struct usb_line6_pod *pod = line6_to_pod(line6); + const unsigned char *buf = pod->line6.buffer_message; + + if (memcmp(buf, pod_version_header, sizeof(pod_version_header)) == 0) { + pod->firmware_version = buf[13] * 100 + buf[14] * 10 + buf[15]; + pod->device_id = ((int)buf[8] << 16) | ((int)buf[9] << 8) | + (int) buf[10]; + if (pod->startup_progress == POD_STARTUP_VERSIONREQ) { + pod->startup_progress = POD_STARTUP_SETUP; + schedule_delayed_work(&line6->startup_work, 0); + } + return; + } + + /* Only look for sysex messages from this device */ + if (buf[0] != (LINE6_SYSEX_BEGIN | LINE6_CHANNEL_DEVICE) && + buf[0] != (LINE6_SYSEX_BEGIN | LINE6_CHANNEL_UNKNOWN)) { + return; + } + if (memcmp(buf + 1, line6_midi_id, sizeof(line6_midi_id)) != 0) + return; + + if (buf[5] == POD_SYSEX_SYSTEM && buf[6] == POD_MONITOR_LEVEL) { + short value = ((int)buf[7] << 12) | ((int)buf[8] << 8) | + ((int)buf[9] << 4) | (int)buf[10]; + pod->monitor_level = value; + } +} + +/* + Send system parameter (from integer). +*/ +static int pod_set_system_param_int(struct usb_line6_pod *pod, int value, + int code) +{ + char *sysex; + static const int size = 5; + + sysex = pod_alloc_sysex_buffer(pod, POD_SYSEX_SYSTEM, size); + if (!sysex) + return -ENOMEM; + sysex[SYSEX_DATA_OFS] = code; + sysex[SYSEX_DATA_OFS + 1] = (value >> 12) & 0x0f; + sysex[SYSEX_DATA_OFS + 2] = (value >> 8) & 0x0f; + sysex[SYSEX_DATA_OFS + 3] = (value >> 4) & 0x0f; + sysex[SYSEX_DATA_OFS + 4] = (value) & 0x0f; + line6_send_sysex_message(&pod->line6, sysex, size); + kfree(sysex); + return 0; +} + +/* + "read" request on "serial_number" special file. +*/ +static ssize_t serial_number_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct snd_card *card = dev_to_snd_card(dev); + struct usb_line6_pod *pod = card->private_data; + + return sprintf(buf, "%u\n", pod->serial_number); +} + +/* + "read" request on "firmware_version" special file. +*/ +static ssize_t firmware_version_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct snd_card *card = dev_to_snd_card(dev); + struct usb_line6_pod *pod = card->private_data; + + return sprintf(buf, "%d.%02d\n", pod->firmware_version / 100, + pod->firmware_version % 100); +} + +/* + "read" request on "device_id" special file. +*/ +static ssize_t device_id_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct snd_card *card = dev_to_snd_card(dev); + struct usb_line6_pod *pod = card->private_data; + + return sprintf(buf, "%d\n", pod->device_id); +} + +/* + POD startup procedure. + This is a sequence of functions with special requirements (e.g., must + not run immediately after initialization, must not run in interrupt + context). After the last one has finished, the device is ready to use. +*/ + +static void pod_startup(struct usb_line6 *line6) +{ + struct usb_line6_pod *pod = line6_to_pod(line6); + + switch (pod->startup_progress) { + case POD_STARTUP_VERSIONREQ: + /* request firmware version: */ + line6_version_request_async(line6); + break; + case POD_STARTUP_SETUP: + /* serial number: */ + line6_read_serial_number(&pod->line6, &pod->serial_number); + + /* ALSA audio interface: */ + if (snd_card_register(line6->card)) + dev_err(line6->ifcdev, "Failed to register POD card.\n"); + pod->startup_progress = POD_STARTUP_DONE; + break; + default: + break; + } +} + +/* POD special files: */ +static DEVICE_ATTR_RO(device_id); +static DEVICE_ATTR_RO(firmware_version); +static DEVICE_ATTR_RO(serial_number); + +static struct attribute *pod_dev_attrs[] = { + &dev_attr_device_id.attr, + &dev_attr_firmware_version.attr, + &dev_attr_serial_number.attr, + NULL +}; + +static const struct attribute_group pod_dev_attr_group = { + .name = "pod", + .attrs = pod_dev_attrs, +}; + +/* control info callback */ +static int snd_pod_control_monitor_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 = 65535; + return 0; +} + +/* control get callback */ +static int snd_pod_control_monitor_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_line6_pcm *line6pcm = snd_kcontrol_chip(kcontrol); + struct usb_line6_pod *pod = line6_to_pod(line6pcm->line6); + + ucontrol->value.integer.value[0] = pod->monitor_level; + return 0; +} + +/* control put callback */ +static int snd_pod_control_monitor_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_line6_pcm *line6pcm = snd_kcontrol_chip(kcontrol); + struct usb_line6_pod *pod = line6_to_pod(line6pcm->line6); + + if (ucontrol->value.integer.value[0] == pod->monitor_level) + return 0; + + pod->monitor_level = ucontrol->value.integer.value[0]; + pod_set_system_param_int(pod, ucontrol->value.integer.value[0], + POD_MONITOR_LEVEL); + return 1; +} + +/* control definition */ +static const struct snd_kcontrol_new pod_control_monitor = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Monitor Playback Volume", + .index = 0, + .access = SNDRV_CTL_ELEM_ACCESS_READWRITE, + .info = snd_pod_control_monitor_info, + .get = snd_pod_control_monitor_get, + .put = snd_pod_control_monitor_put +}; + +/* + Try to init POD device. +*/ +static int pod_init(struct usb_line6 *line6, + const struct usb_device_id *id) +{ + int err; + struct usb_line6_pod *pod = line6_to_pod(line6); + + line6->process_message = line6_pod_process_message; + line6->startup = pod_startup; + + /* create sysfs entries: */ + err = snd_card_add_dev_attr(line6->card, &pod_dev_attr_group); + if (err < 0) + return err; + + /* initialize PCM subsystem: */ + err = line6_init_pcm(line6, &pod_pcm_properties); + if (err < 0) + return err; + + /* register monitor control: */ + err = snd_ctl_add(line6->card, + snd_ctl_new1(&pod_control_monitor, line6->line6pcm)); + if (err < 0) + return err; + + /* + When the sound card is registered at this point, the PODxt Live + displays "Invalid Code Error 07", so we do it later in the event + handler. + */ + + if (pod->line6.properties->capabilities & LINE6_CAP_CONTROL) { + pod->monitor_level = POD_SYSTEM_INVALID; + + /* initiate startup procedure: */ + schedule_delayed_work(&line6->startup_work, + msecs_to_jiffies(POD_STARTUP_DELAY)); + } + + return 0; +} + +#define LINE6_DEVICE(prod) USB_DEVICE(0x0e41, prod) +#define LINE6_IF_NUM(prod, n) USB_DEVICE_INTERFACE_NUMBER(0x0e41, prod, n) + +/* table of devices that work with this driver */ +static const struct usb_device_id pod_id_table[] = { + { LINE6_DEVICE(0x4250), .driver_info = LINE6_BASSPODXT }, + { LINE6_DEVICE(0x4642), .driver_info = LINE6_BASSPODXTLIVE }, + { LINE6_DEVICE(0x4252), .driver_info = LINE6_BASSPODXTPRO }, + { LINE6_IF_NUM(0x5051, 1), .driver_info = LINE6_POCKETPOD }, + { LINE6_DEVICE(0x5044), .driver_info = LINE6_PODXT }, + { LINE6_IF_NUM(0x4650, 0), .driver_info = LINE6_PODXTLIVE_POD }, + { LINE6_DEVICE(0x5050), .driver_info = LINE6_PODXTPRO }, + {} +}; + +MODULE_DEVICE_TABLE(usb, pod_id_table); + +static const struct line6_properties pod_properties_table[] = { + [LINE6_BASSPODXT] = { + .id = "BassPODxt", + .name = "BassPODxt", + .capabilities = LINE6_CAP_CONTROL + | LINE6_CAP_CONTROL_MIDI + | LINE6_CAP_PCM + | LINE6_CAP_HWMON, + .altsetting = 5, + .ep_ctrl_r = 0x84, + .ep_ctrl_w = 0x03, + .ep_audio_r = 0x82, + .ep_audio_w = 0x01, + }, + [LINE6_BASSPODXTLIVE] = { + .id = "BassPODxtLive", + .name = "BassPODxt Live", + .capabilities = LINE6_CAP_CONTROL + | LINE6_CAP_CONTROL_MIDI + | LINE6_CAP_PCM + | LINE6_CAP_HWMON, + .altsetting = 1, + .ep_ctrl_r = 0x84, + .ep_ctrl_w = 0x03, + .ep_audio_r = 0x82, + .ep_audio_w = 0x01, + }, + [LINE6_BASSPODXTPRO] = { + .id = "BassPODxtPro", + .name = "BassPODxt Pro", + .capabilities = LINE6_CAP_CONTROL + | LINE6_CAP_CONTROL_MIDI + | LINE6_CAP_PCM + | LINE6_CAP_HWMON, + .altsetting = 5, + .ep_ctrl_r = 0x84, + .ep_ctrl_w = 0x03, + .ep_audio_r = 0x82, + .ep_audio_w = 0x01, + }, + [LINE6_POCKETPOD] = { + .id = "PocketPOD", + .name = "Pocket POD", + .capabilities = LINE6_CAP_CONTROL + | LINE6_CAP_CONTROL_MIDI, + .altsetting = 0, + .ep_ctrl_r = 0x82, + .ep_ctrl_w = 0x02, + /* no audio channel */ + }, + [LINE6_PODXT] = { + .id = "PODxt", + .name = "PODxt", + .capabilities = LINE6_CAP_CONTROL + | LINE6_CAP_CONTROL_MIDI + | LINE6_CAP_PCM + | LINE6_CAP_HWMON, + .altsetting = 5, + .ep_ctrl_r = 0x84, + .ep_ctrl_w = 0x03, + .ep_audio_r = 0x82, + .ep_audio_w = 0x01, + }, + [LINE6_PODXTLIVE_POD] = { + .id = "PODxtLive", + .name = "PODxt Live", + .capabilities = LINE6_CAP_CONTROL + | LINE6_CAP_CONTROL_MIDI + | LINE6_CAP_PCM + | LINE6_CAP_HWMON, + .altsetting = 1, + .ep_ctrl_r = 0x84, + .ep_ctrl_w = 0x03, + .ep_audio_r = 0x82, + .ep_audio_w = 0x01, + }, + [LINE6_PODXTPRO] = { + .id = "PODxtPro", + .name = "PODxt Pro", + .capabilities = LINE6_CAP_CONTROL + | LINE6_CAP_CONTROL_MIDI + | LINE6_CAP_PCM + | LINE6_CAP_HWMON, + .altsetting = 5, + .ep_ctrl_r = 0x84, + .ep_ctrl_w = 0x03, + .ep_audio_r = 0x82, + .ep_audio_w = 0x01, + }, +}; + +/* + Probe USB device. +*/ +static int pod_probe(struct usb_interface *interface, + const struct usb_device_id *id) +{ + return line6_probe(interface, id, "Line6-POD", + &pod_properties_table[id->driver_info], + pod_init, sizeof(struct usb_line6_pod)); +} + +static struct usb_driver pod_driver = { + .name = KBUILD_MODNAME, + .probe = pod_probe, + .disconnect = line6_disconnect, +#ifdef CONFIG_PM + .suspend = line6_suspend, + .resume = line6_resume, + .reset_resume = line6_resume, +#endif + .id_table = pod_id_table, +}; + +module_usb_driver(pod_driver); + +MODULE_DESCRIPTION("Line 6 POD USB driver"); +MODULE_LICENSE("GPL"); diff --git a/sound/usb/line6/podhd.c b/sound/usb/line6/podhd.c new file mode 100644 index 000000000..b24bc82f8 --- /dev/null +++ b/sound/usb/line6/podhd.c @@ -0,0 +1,559 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Line 6 Pod HD + * + * Copyright (C) 2011 Stefan Hajnoczi <stefanha@gmail.com> + * Copyright (C) 2015 Andrej Krutak <dev@andree.sk> + * Copyright (C) 2017 Hans P. Moller <hmoller@uc.cl> + */ + +#include <linux/usb.h> +#include <linux/slab.h> +#include <linux/module.h> +#include <sound/core.h> +#include <sound/control.h> +#include <sound/pcm.h> + +#include "driver.h" +#include "pcm.h" + +#define PODHD_STARTUP_DELAY 500 + +enum { + LINE6_PODHD300, + LINE6_PODHD400, + LINE6_PODHD500, + LINE6_PODX3, + LINE6_PODX3LIVE, + LINE6_PODHD500X, + LINE6_PODHDDESKTOP +}; + +struct usb_line6_podhd { + /* Generic Line 6 USB data */ + struct usb_line6 line6; + + /* Serial number of device */ + u32 serial_number; + + /* Firmware version */ + int firmware_version; + + /* Monitor level */ + int monitor_level; +}; + +#define line6_to_podhd(x) container_of(x, struct usb_line6_podhd, line6) + +static const struct snd_ratden podhd_ratden = { + .num_min = 48000, + .num_max = 48000, + .num_step = 1, + .den = 1, +}; + +static struct line6_pcm_properties podhd_pcm_properties = { + .playback_hw = { + .info = (SNDRV_PCM_INFO_MMAP | + SNDRV_PCM_INFO_INTERLEAVED | + SNDRV_PCM_INFO_BLOCK_TRANSFER | + SNDRV_PCM_INFO_MMAP_VALID | + SNDRV_PCM_INFO_PAUSE | + SNDRV_PCM_INFO_SYNC_START), + .formats = SNDRV_PCM_FMTBIT_S24_3LE, + .rates = SNDRV_PCM_RATE_48000, + .rate_min = 48000, + .rate_max = 48000, + .channels_min = 2, + .channels_max = 2, + .buffer_bytes_max = 60000, + .period_bytes_min = 64, + .period_bytes_max = 8192, + .periods_min = 1, + .periods_max = 1024}, + .capture_hw = { + .info = (SNDRV_PCM_INFO_MMAP | + SNDRV_PCM_INFO_INTERLEAVED | + SNDRV_PCM_INFO_BLOCK_TRANSFER | + SNDRV_PCM_INFO_MMAP_VALID | + SNDRV_PCM_INFO_SYNC_START), + .formats = SNDRV_PCM_FMTBIT_S24_3LE, + .rates = SNDRV_PCM_RATE_48000, + .rate_min = 48000, + .rate_max = 48000, + .channels_min = 2, + .channels_max = 2, + .buffer_bytes_max = 60000, + .period_bytes_min = 64, + .period_bytes_max = 8192, + .periods_min = 1, + .periods_max = 1024}, + .rates = { + .nrats = 1, + .rats = &podhd_ratden}, + .bytes_per_channel = 3 /* SNDRV_PCM_FMTBIT_S24_3LE */ +}; + +static struct line6_pcm_properties podx3_pcm_properties = { + .playback_hw = { + .info = (SNDRV_PCM_INFO_MMAP | + SNDRV_PCM_INFO_INTERLEAVED | + SNDRV_PCM_INFO_BLOCK_TRANSFER | + SNDRV_PCM_INFO_MMAP_VALID | + SNDRV_PCM_INFO_PAUSE | + SNDRV_PCM_INFO_SYNC_START), + .formats = SNDRV_PCM_FMTBIT_S24_3LE, + .rates = SNDRV_PCM_RATE_48000, + .rate_min = 48000, + .rate_max = 48000, + .channels_min = 2, + .channels_max = 2, + .buffer_bytes_max = 60000, + .period_bytes_min = 64, + .period_bytes_max = 8192, + .periods_min = 1, + .periods_max = 1024}, + .capture_hw = { + .info = (SNDRV_PCM_INFO_MMAP | + SNDRV_PCM_INFO_INTERLEAVED | + SNDRV_PCM_INFO_BLOCK_TRANSFER | + SNDRV_PCM_INFO_MMAP_VALID | + SNDRV_PCM_INFO_SYNC_START), + .formats = SNDRV_PCM_FMTBIT_S24_3LE, + .rates = SNDRV_PCM_RATE_48000, + .rate_min = 48000, + .rate_max = 48000, + /* 1+2: Main signal (out), 3+4: Tone 1, + * 5+6: Tone 2, 7+8: raw + */ + .channels_min = 8, + .channels_max = 8, + .buffer_bytes_max = 60000, + .period_bytes_min = 64, + .period_bytes_max = 8192, + .periods_min = 1, + .periods_max = 1024}, + .rates = { + .nrats = 1, + .rats = &podhd_ratden}, + .bytes_per_channel = 3 /* SNDRV_PCM_FMTBIT_S24_3LE */ +}; +static struct usb_driver podhd_driver; + +static ssize_t serial_number_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct snd_card *card = dev_to_snd_card(dev); + struct usb_line6_podhd *pod = card->private_data; + + return sprintf(buf, "%u\n", pod->serial_number); +} + +static ssize_t firmware_version_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct snd_card *card = dev_to_snd_card(dev); + struct usb_line6_podhd *pod = card->private_data; + + return sprintf(buf, "%06x\n", pod->firmware_version); +} + +static DEVICE_ATTR_RO(firmware_version); +static DEVICE_ATTR_RO(serial_number); + +static struct attribute *podhd_dev_attrs[] = { + &dev_attr_firmware_version.attr, + &dev_attr_serial_number.attr, + NULL +}; + +static const struct attribute_group podhd_dev_attr_group = { + .name = "podhd", + .attrs = podhd_dev_attrs, +}; + +/* + * POD X3 startup procedure. + * + * May be compatible with other POD HD's, since it's also similar to the + * previous POD setup. In any case, it doesn't seem to be required for the + * audio nor bulk interfaces to work. + */ + +static int podhd_dev_start(struct usb_line6_podhd *pod) +{ + int ret; + u8 init_bytes[8]; + int i; + struct usb_device *usbdev = pod->line6.usbdev; + + ret = usb_control_msg_send(usbdev, 0, + 0x67, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_OUT, + 0x11, 0, + NULL, 0, LINE6_TIMEOUT, GFP_KERNEL); + if (ret) { + dev_err(pod->line6.ifcdev, "read request failed (error %d)\n", ret); + goto exit; + } + + /* NOTE: looks like some kind of ping message */ + ret = usb_control_msg_recv(usbdev, 0, 0x67, + USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_IN, + 0x11, 0x0, + init_bytes, 3, LINE6_TIMEOUT, GFP_KERNEL); + if (ret) { + dev_err(pod->line6.ifcdev, + "receive length failed (error %d)\n", ret); + goto exit; + } + + pod->firmware_version = + (init_bytes[0] << 16) | (init_bytes[1] << 8) | (init_bytes[2] << 0); + + for (i = 0; i <= 16; i++) { + ret = line6_read_data(&pod->line6, 0xf000 + 0x08 * i, init_bytes, 8); + if (ret < 0) + goto exit; + } + + ret = usb_control_msg_send(usbdev, 0, + USB_REQ_SET_FEATURE, + USB_TYPE_STANDARD | USB_RECIP_DEVICE | USB_DIR_OUT, + 1, 0, + NULL, 0, LINE6_TIMEOUT, GFP_KERNEL); +exit: + return ret; +} + +static void podhd_startup(struct usb_line6 *line6) +{ + struct usb_line6_podhd *pod = line6_to_podhd(line6); + + podhd_dev_start(pod); + line6_read_serial_number(&pod->line6, &pod->serial_number); + if (snd_card_register(line6->card)) + dev_err(line6->ifcdev, "Failed to register POD HD card.\n"); +} + +static void podhd_disconnect(struct usb_line6 *line6) +{ + struct usb_line6_podhd *pod = line6_to_podhd(line6); + + if (pod->line6.properties->capabilities & LINE6_CAP_CONTROL_INFO) { + struct usb_interface *intf; + + intf = usb_ifnum_to_if(line6->usbdev, + pod->line6.properties->ctrl_if); + if (intf) + usb_driver_release_interface(&podhd_driver, intf); + } +} + +static const unsigned int float_zero_to_one_lookup[] = { +0x00000000, 0x3c23d70a, 0x3ca3d70a, 0x3cf5c28f, 0x3d23d70a, 0x3d4ccccd, +0x3d75c28f, 0x3d8f5c29, 0x3da3d70a, 0x3db851ec, 0x3dcccccd, 0x3de147ae, +0x3df5c28f, 0x3e051eb8, 0x3e0f5c29, 0x3e19999a, 0x3e23d70a, 0x3e2e147b, +0x3e3851ec, 0x3e428f5c, 0x3e4ccccd, 0x3e570a3d, 0x3e6147ae, 0x3e6b851f, +0x3e75c28f, 0x3e800000, 0x3e851eb8, 0x3e8a3d71, 0x3e8f5c29, 0x3e947ae1, +0x3e99999a, 0x3e9eb852, 0x3ea3d70a, 0x3ea8f5c3, 0x3eae147b, 0x3eb33333, +0x3eb851ec, 0x3ebd70a4, 0x3ec28f5c, 0x3ec7ae14, 0x3ecccccd, 0x3ed1eb85, +0x3ed70a3d, 0x3edc28f6, 0x3ee147ae, 0x3ee66666, 0x3eeb851f, 0x3ef0a3d7, +0x3ef5c28f, 0x3efae148, 0x3f000000, 0x3f028f5c, 0x3f051eb8, 0x3f07ae14, +0x3f0a3d71, 0x3f0ccccd, 0x3f0f5c29, 0x3f11eb85, 0x3f147ae1, 0x3f170a3d, +0x3f19999a, 0x3f1c28f6, 0x3f1eb852, 0x3f2147ae, 0x3f23d70a, 0x3f266666, +0x3f28f5c3, 0x3f2b851f, 0x3f2e147b, 0x3f30a3d7, 0x3f333333, 0x3f35c28f, +0x3f3851ec, 0x3f3ae148, 0x3f3d70a4, 0x3f400000, 0x3f428f5c, 0x3f451eb8, +0x3f47ae14, 0x3f4a3d71, 0x3f4ccccd, 0x3f4f5c29, 0x3f51eb85, 0x3f547ae1, +0x3f570a3d, 0x3f59999a, 0x3f5c28f6, 0x3f5eb852, 0x3f6147ae, 0x3f63d70a, +0x3f666666, 0x3f68f5c3, 0x3f6b851f, 0x3f6e147b, 0x3f70a3d7, 0x3f733333, +0x3f75c28f, 0x3f7851ec, 0x3f7ae148, 0x3f7d70a4, 0x3f800000 +}; + +static void podhd_set_monitor_level(struct usb_line6_podhd *podhd, int value) +{ + unsigned int fl; + static const unsigned char msg[16] = { + /* Chunk is 0xc bytes (without first word) */ + 0x0c, 0x00, + /* First chunk in the message */ + 0x01, 0x00, + /* Message size is 2 4-byte words */ + 0x02, 0x00, + /* Unknown */ + 0x04, 0x41, + /* Unknown */ + 0x04, 0x00, 0x13, 0x00, + /* Volume, LE float32, 0.0 - 1.0 */ + 0x00, 0x00, 0x00, 0x00 + }; + unsigned char *buf; + + buf = kmemdup(msg, sizeof(msg), GFP_KERNEL); + if (!buf) + return; + + if (value < 0) + value = 0; + + if (value >= ARRAY_SIZE(float_zero_to_one_lookup)) + value = ARRAY_SIZE(float_zero_to_one_lookup) - 1; + + fl = float_zero_to_one_lookup[value]; + + buf[12] = (fl >> 0) & 0xff; + buf[13] = (fl >> 8) & 0xff; + buf[14] = (fl >> 16) & 0xff; + buf[15] = (fl >> 24) & 0xff; + + line6_send_raw_message(&podhd->line6, buf, sizeof(msg)); + kfree(buf); + + podhd->monitor_level = value; +} + +/* control info callback */ +static int snd_podhd_control_monitor_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 = 100; + uinfo->value.integer.step = 1; + return 0; +} + +/* control get callback */ +static int snd_podhd_control_monitor_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_line6_pcm *line6pcm = snd_kcontrol_chip(kcontrol); + struct usb_line6_podhd *podhd = line6_to_podhd(line6pcm->line6); + + ucontrol->value.integer.value[0] = podhd->monitor_level; + return 0; +} + +/* control put callback */ +static int snd_podhd_control_monitor_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_line6_pcm *line6pcm = snd_kcontrol_chip(kcontrol); + struct usb_line6_podhd *podhd = line6_to_podhd(line6pcm->line6); + + if (ucontrol->value.integer.value[0] == podhd->monitor_level) + return 0; + + podhd_set_monitor_level(podhd, ucontrol->value.integer.value[0]); + return 1; +} + +/* control definition */ +static const struct snd_kcontrol_new podhd_control_monitor = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Monitor Playback Volume", + .index = 0, + .access = SNDRV_CTL_ELEM_ACCESS_READWRITE, + .info = snd_podhd_control_monitor_info, + .get = snd_podhd_control_monitor_get, + .put = snd_podhd_control_monitor_put +}; + +/* + Try to init POD HD device. +*/ +static int podhd_init(struct usb_line6 *line6, + const struct usb_device_id *id) +{ + int err; + struct usb_line6_podhd *pod = line6_to_podhd(line6); + struct usb_interface *intf; + + line6->disconnect = podhd_disconnect; + line6->startup = podhd_startup; + + if (pod->line6.properties->capabilities & LINE6_CAP_CONTROL) { + /* claim the data interface */ + intf = usb_ifnum_to_if(line6->usbdev, + pod->line6.properties->ctrl_if); + if (!intf) { + dev_err(pod->line6.ifcdev, "interface %d not found\n", + pod->line6.properties->ctrl_if); + return -ENODEV; + } + + err = usb_driver_claim_interface(&podhd_driver, intf, NULL); + if (err != 0) { + dev_err(pod->line6.ifcdev, "can't claim interface %d, error %d\n", + pod->line6.properties->ctrl_if, err); + return err; + } + } + + if (pod->line6.properties->capabilities & LINE6_CAP_CONTROL_INFO) { + /* create sysfs entries: */ + err = snd_card_add_dev_attr(line6->card, &podhd_dev_attr_group); + if (err < 0) + return err; + } + + if (pod->line6.properties->capabilities & LINE6_CAP_PCM) { + /* initialize PCM subsystem: */ + err = line6_init_pcm(line6, + (id->driver_info == LINE6_PODX3 || + id->driver_info == LINE6_PODX3LIVE) ? &podx3_pcm_properties : + &podhd_pcm_properties); + if (err < 0) + return err; + } + + if (pod->line6.properties->capabilities & LINE6_CAP_HWMON_CTL) { + podhd_set_monitor_level(pod, 100); + err = snd_ctl_add(line6->card, + snd_ctl_new1(&podhd_control_monitor, + line6->line6pcm)); + if (err < 0) + return err; + } + + if (!(pod->line6.properties->capabilities & LINE6_CAP_CONTROL_INFO)) { + /* register USB audio system directly */ + return snd_card_register(line6->card); + } + + /* init device and delay registering */ + schedule_delayed_work(&line6->startup_work, + msecs_to_jiffies(PODHD_STARTUP_DELAY)); + return 0; +} + +#define LINE6_DEVICE(prod) USB_DEVICE(0x0e41, prod) +#define LINE6_IF_NUM(prod, n) USB_DEVICE_INTERFACE_NUMBER(0x0e41, prod, n) + +/* table of devices that work with this driver */ +static const struct usb_device_id podhd_id_table[] = { + /* TODO: no need to alloc data interfaces when only audio is used */ + { LINE6_DEVICE(0x5057), .driver_info = LINE6_PODHD300 }, + { LINE6_DEVICE(0x5058), .driver_info = LINE6_PODHD400 }, + { LINE6_IF_NUM(0x414D, 0), .driver_info = LINE6_PODHD500 }, + { LINE6_IF_NUM(0x414A, 0), .driver_info = LINE6_PODX3 }, + { LINE6_IF_NUM(0x414B, 0), .driver_info = LINE6_PODX3LIVE }, + { LINE6_IF_NUM(0x4159, 0), .driver_info = LINE6_PODHD500X }, + { LINE6_IF_NUM(0x4156, 0), .driver_info = LINE6_PODHDDESKTOP }, + {} +}; + +MODULE_DEVICE_TABLE(usb, podhd_id_table); + +static const struct line6_properties podhd_properties_table[] = { + [LINE6_PODHD300] = { + .id = "PODHD300", + .name = "POD HD300", + .capabilities = LINE6_CAP_PCM + | LINE6_CAP_HWMON, + .altsetting = 5, + .ep_ctrl_r = 0x84, + .ep_ctrl_w = 0x03, + .ep_audio_r = 0x82, + .ep_audio_w = 0x01, + }, + [LINE6_PODHD400] = { + .id = "PODHD400", + .name = "POD HD400", + .capabilities = LINE6_CAP_PCM + | LINE6_CAP_HWMON, + .altsetting = 5, + .ep_ctrl_r = 0x84, + .ep_ctrl_w = 0x03, + .ep_audio_r = 0x82, + .ep_audio_w = 0x01, + }, + [LINE6_PODHD500] = { + .id = "PODHD500", + .name = "POD HD500", + .capabilities = LINE6_CAP_PCM | LINE6_CAP_CONTROL + | LINE6_CAP_HWMON | LINE6_CAP_HWMON_CTL, + .altsetting = 1, + .ctrl_if = 1, + .ep_ctrl_r = 0x81, + .ep_ctrl_w = 0x01, + .ep_audio_r = 0x86, + .ep_audio_w = 0x02, + }, + [LINE6_PODX3] = { + .id = "PODX3", + .name = "POD X3", + .capabilities = LINE6_CAP_CONTROL | LINE6_CAP_CONTROL_INFO + | LINE6_CAP_PCM | LINE6_CAP_HWMON | LINE6_CAP_IN_NEEDS_OUT, + .altsetting = 1, + .ep_ctrl_r = 0x81, + .ep_ctrl_w = 0x01, + .ctrl_if = 1, + .ep_audio_r = 0x86, + .ep_audio_w = 0x02, + }, + [LINE6_PODX3LIVE] = { + .id = "PODX3LIVE", + .name = "POD X3 LIVE", + .capabilities = LINE6_CAP_CONTROL | LINE6_CAP_CONTROL_INFO + | LINE6_CAP_PCM | LINE6_CAP_HWMON | LINE6_CAP_IN_NEEDS_OUT, + .altsetting = 1, + .ep_ctrl_r = 0x81, + .ep_ctrl_w = 0x01, + .ctrl_if = 1, + .ep_audio_r = 0x86, + .ep_audio_w = 0x02, + }, + [LINE6_PODHD500X] = { + .id = "PODHD500X", + .name = "POD HD500X", + .capabilities = LINE6_CAP_CONTROL + | LINE6_CAP_PCM | LINE6_CAP_HWMON, + .altsetting = 1, + .ep_ctrl_r = 0x81, + .ep_ctrl_w = 0x01, + .ctrl_if = 1, + .ep_audio_r = 0x86, + .ep_audio_w = 0x02, + }, + [LINE6_PODHDDESKTOP] = { + .id = "PODHDDESKTOP", + .name = "POD HDDESKTOP", + .capabilities = LINE6_CAP_CONTROL + | LINE6_CAP_PCM | LINE6_CAP_HWMON, + .altsetting = 1, + .ep_ctrl_r = 0x81, + .ep_ctrl_w = 0x01, + .ctrl_if = 1, + .ep_audio_r = 0x86, + .ep_audio_w = 0x02, + }, +}; + +/* + Probe USB device. +*/ +static int podhd_probe(struct usb_interface *interface, + const struct usb_device_id *id) +{ + return line6_probe(interface, id, "Line6-PODHD", + &podhd_properties_table[id->driver_info], + podhd_init, sizeof(struct usb_line6_podhd)); +} + +static struct usb_driver podhd_driver = { + .name = KBUILD_MODNAME, + .probe = podhd_probe, + .disconnect = line6_disconnect, +#ifdef CONFIG_PM + .suspend = line6_suspend, + .resume = line6_resume, + .reset_resume = line6_resume, +#endif + .id_table = podhd_id_table, +}; + +module_usb_driver(podhd_driver); + +MODULE_DESCRIPTION("Line 6 PODHD USB driver"); +MODULE_LICENSE("GPL"); diff --git a/sound/usb/line6/toneport.c b/sound/usb/line6/toneport.c new file mode 100644 index 000000000..e33df5874 --- /dev/null +++ b/sound/usb/line6/toneport.c @@ -0,0 +1,582 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Line 6 Linux USB driver + * + * Copyright (C) 2004-2010 Markus Grabner (grabner@icg.tugraz.at) + * Emil Myhrman (emil.myhrman@gmail.com) + */ + +#include <linux/wait.h> +#include <linux/usb.h> +#include <linux/slab.h> +#include <linux/module.h> +#include <linux/leds.h> +#include <sound/core.h> +#include <sound/control.h> + +#include "capture.h" +#include "driver.h" +#include "playback.h" + +enum line6_device_type { + LINE6_GUITARPORT, + LINE6_PODSTUDIO_GX, + LINE6_PODSTUDIO_UX1, + LINE6_PODSTUDIO_UX2, + LINE6_TONEPORT_GX, + LINE6_TONEPORT_UX1, + LINE6_TONEPORT_UX2, +}; + +struct usb_line6_toneport; + +struct toneport_led { + struct led_classdev dev; + char name[64]; + struct usb_line6_toneport *toneport; + bool registered; +}; + +struct usb_line6_toneport { + /* Generic Line 6 USB data */ + struct usb_line6 line6; + + /* Source selector */ + int source; + + /* Serial number of device */ + u32 serial_number; + + /* Firmware version (x 100) */ + u8 firmware_version; + + /* Device type */ + enum line6_device_type type; + + /* LED instances */ + struct toneport_led leds[2]; +}; + +#define line6_to_toneport(x) container_of(x, struct usb_line6_toneport, line6) + +static int toneport_send_cmd(struct usb_device *usbdev, int cmd1, int cmd2); + +#define TONEPORT_PCM_DELAY 1 + +static const struct snd_ratden toneport_ratden = { + .num_min = 44100, + .num_max = 44100, + .num_step = 1, + .den = 1 +}; + +static struct line6_pcm_properties toneport_pcm_properties = { + .playback_hw = { + .info = (SNDRV_PCM_INFO_MMAP | + SNDRV_PCM_INFO_INTERLEAVED | + SNDRV_PCM_INFO_BLOCK_TRANSFER | + SNDRV_PCM_INFO_MMAP_VALID | + SNDRV_PCM_INFO_PAUSE | + SNDRV_PCM_INFO_SYNC_START), + .formats = SNDRV_PCM_FMTBIT_S16_LE, + .rates = SNDRV_PCM_RATE_KNOT, + .rate_min = 44100, + .rate_max = 44100, + .channels_min = 2, + .channels_max = 2, + .buffer_bytes_max = 60000, + .period_bytes_min = 64, + .period_bytes_max = 8192, + .periods_min = 1, + .periods_max = 1024}, + .capture_hw = { + .info = (SNDRV_PCM_INFO_MMAP | + SNDRV_PCM_INFO_INTERLEAVED | + SNDRV_PCM_INFO_BLOCK_TRANSFER | + SNDRV_PCM_INFO_MMAP_VALID | + SNDRV_PCM_INFO_SYNC_START), + .formats = SNDRV_PCM_FMTBIT_S16_LE, + .rates = SNDRV_PCM_RATE_KNOT, + .rate_min = 44100, + .rate_max = 44100, + .channels_min = 2, + .channels_max = 2, + .buffer_bytes_max = 60000, + .period_bytes_min = 64, + .period_bytes_max = 8192, + .periods_min = 1, + .periods_max = 1024}, + .rates = { + .nrats = 1, + .rats = &toneport_ratden}, + .bytes_per_channel = 2 +}; + +static const struct { + const char *name; + int code; +} toneport_source_info[] = { + {"Microphone", 0x0a01}, + {"Line", 0x0801}, + {"Instrument", 0x0b01}, + {"Inst & Mic", 0x0901} +}; + +static int toneport_send_cmd(struct usb_device *usbdev, int cmd1, int cmd2) +{ + int ret; + + ret = usb_control_msg_send(usbdev, 0, 0x67, + USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_OUT, + cmd1, cmd2, NULL, 0, LINE6_TIMEOUT, + GFP_KERNEL); + + if (ret) { + dev_err(&usbdev->dev, "send failed (error %d)\n", ret); + return ret; + } + + return 0; +} + +/* monitor info callback */ +static int snd_toneport_monitor_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 = 256; + return 0; +} + +/* monitor get callback */ +static int snd_toneport_monitor_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_line6_pcm *line6pcm = snd_kcontrol_chip(kcontrol); + + ucontrol->value.integer.value[0] = line6pcm->volume_monitor; + return 0; +} + +/* monitor put callback */ +static int snd_toneport_monitor_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_line6_pcm *line6pcm = snd_kcontrol_chip(kcontrol); + int err; + + if (ucontrol->value.integer.value[0] == line6pcm->volume_monitor) + return 0; + + line6pcm->volume_monitor = ucontrol->value.integer.value[0]; + + if (line6pcm->volume_monitor > 0) { + err = line6_pcm_acquire(line6pcm, LINE6_STREAM_MONITOR, true); + if (err < 0) { + line6pcm->volume_monitor = 0; + line6_pcm_release(line6pcm, LINE6_STREAM_MONITOR); + return err; + } + } else { + line6_pcm_release(line6pcm, LINE6_STREAM_MONITOR); + } + + return 1; +} + +/* source info callback */ +static int snd_toneport_source_info(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_info *uinfo) +{ + const int size = ARRAY_SIZE(toneport_source_info); + + uinfo->type = SNDRV_CTL_ELEM_TYPE_ENUMERATED; + uinfo->count = 1; + uinfo->value.enumerated.items = size; + + if (uinfo->value.enumerated.item >= size) + uinfo->value.enumerated.item = size - 1; + + strcpy(uinfo->value.enumerated.name, + toneport_source_info[uinfo->value.enumerated.item].name); + + return 0; +} + +/* source get callback */ +static int snd_toneport_source_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_line6_pcm *line6pcm = snd_kcontrol_chip(kcontrol); + struct usb_line6_toneport *toneport = line6_to_toneport(line6pcm->line6); + + ucontrol->value.enumerated.item[0] = toneport->source; + return 0; +} + +/* source put callback */ +static int snd_toneport_source_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_line6_pcm *line6pcm = snd_kcontrol_chip(kcontrol); + struct usb_line6_toneport *toneport = line6_to_toneport(line6pcm->line6); + unsigned int source; + + source = ucontrol->value.enumerated.item[0]; + if (source >= ARRAY_SIZE(toneport_source_info)) + return -EINVAL; + if (source == toneport->source) + return 0; + + toneport->source = source; + toneport_send_cmd(toneport->line6.usbdev, + toneport_source_info[source].code, 0x0000); + return 1; +} + +static void toneport_startup(struct usb_line6 *line6) +{ + line6_pcm_acquire(line6->line6pcm, LINE6_STREAM_MONITOR, true); +} + +/* control definition */ +static const struct snd_kcontrol_new toneport_control_monitor = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Monitor Playback Volume", + .index = 0, + .access = SNDRV_CTL_ELEM_ACCESS_READWRITE, + .info = snd_toneport_monitor_info, + .get = snd_toneport_monitor_get, + .put = snd_toneport_monitor_put +}; + +/* source selector definition */ +static const struct snd_kcontrol_new toneport_control_source = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "PCM Capture Source", + .index = 0, + .access = SNDRV_CTL_ELEM_ACCESS_READWRITE, + .info = snd_toneport_source_info, + .get = snd_toneport_source_get, + .put = snd_toneport_source_put +}; + +/* + For the led on Guitarport. + Brightness goes from 0x00 to 0x26. Set a value above this to have led + blink. + (void cmd_0x02(byte red, byte green) +*/ + +static bool toneport_has_led(struct usb_line6_toneport *toneport) +{ + switch (toneport->type) { + case LINE6_GUITARPORT: + case LINE6_TONEPORT_GX: + /* add your device here if you are missing support for the LEDs */ + return true; + + default: + return false; + } +} + +static const char * const toneport_led_colors[2] = { "red", "green" }; +static const int toneport_led_init_vals[2] = { 0x00, 0x26 }; + +static void toneport_update_led(struct usb_line6_toneport *toneport) +{ + toneport_send_cmd(toneport->line6.usbdev, + (toneport->leds[0].dev.brightness << 8) | 0x0002, + toneport->leds[1].dev.brightness); +} + +static void toneport_led_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct toneport_led *leds = + container_of(led_cdev, struct toneport_led, dev); + toneport_update_led(leds->toneport); +} + +static int toneport_init_leds(struct usb_line6_toneport *toneport) +{ + struct device *dev = &toneport->line6.usbdev->dev; + int i, err; + + for (i = 0; i < 2; i++) { + struct toneport_led *led = &toneport->leds[i]; + struct led_classdev *leddev = &led->dev; + + led->toneport = toneport; + snprintf(led->name, sizeof(led->name), "%s::%s", + dev_name(dev), toneport_led_colors[i]); + leddev->name = led->name; + leddev->brightness = toneport_led_init_vals[i]; + leddev->max_brightness = 0x26; + leddev->brightness_set = toneport_led_brightness_set; + err = led_classdev_register(dev, leddev); + if (err) + return err; + led->registered = true; + } + + return 0; +} + +static void toneport_remove_leds(struct usb_line6_toneport *toneport) +{ + struct toneport_led *led; + int i; + + for (i = 0; i < 2; i++) { + led = &toneport->leds[i]; + if (!led->registered) + break; + led_classdev_unregister(&led->dev); + led->registered = false; + } +} + +static bool toneport_has_source_select(struct usb_line6_toneport *toneport) +{ + switch (toneport->type) { + case LINE6_TONEPORT_UX1: + case LINE6_TONEPORT_UX2: + case LINE6_PODSTUDIO_UX1: + case LINE6_PODSTUDIO_UX2: + return true; + + default: + return false; + } +} + +/* + Setup Toneport device. +*/ +static int toneport_setup(struct usb_line6_toneport *toneport) +{ + u32 *ticks; + struct usb_line6 *line6 = &toneport->line6; + struct usb_device *usbdev = line6->usbdev; + + ticks = kmalloc(sizeof(*ticks), GFP_KERNEL); + if (!ticks) + return -ENOMEM; + + /* sync time on device with host: */ + /* note: 32-bit timestamps overflow in year 2106 */ + *ticks = (u32)ktime_get_real_seconds(); + line6_write_data(line6, 0x80c6, ticks, 4); + kfree(ticks); + + /* enable device: */ + toneport_send_cmd(usbdev, 0x0301, 0x0000); + + /* initialize source select: */ + if (toneport_has_source_select(toneport)) + toneport_send_cmd(usbdev, + toneport_source_info[toneport->source].code, + 0x0000); + + if (toneport_has_led(toneport)) + toneport_update_led(toneport); + + schedule_delayed_work(&toneport->line6.startup_work, + msecs_to_jiffies(TONEPORT_PCM_DELAY * 1000)); + return 0; +} + +/* + Toneport device disconnected. +*/ +static void line6_toneport_disconnect(struct usb_line6 *line6) +{ + struct usb_line6_toneport *toneport = line6_to_toneport(line6); + + if (toneport_has_led(toneport)) + toneport_remove_leds(toneport); +} + + +/* + Try to init Toneport device. +*/ +static int toneport_init(struct usb_line6 *line6, + const struct usb_device_id *id) +{ + int err; + struct usb_line6_toneport *toneport = line6_to_toneport(line6); + + toneport->type = id->driver_info; + + line6->disconnect = line6_toneport_disconnect; + line6->startup = toneport_startup; + + /* initialize PCM subsystem: */ + err = line6_init_pcm(line6, &toneport_pcm_properties); + if (err < 0) + return err; + + /* register monitor control: */ + err = snd_ctl_add(line6->card, + snd_ctl_new1(&toneport_control_monitor, + line6->line6pcm)); + if (err < 0) + return err; + + /* register source select control: */ + if (toneport_has_source_select(toneport)) { + err = + snd_ctl_add(line6->card, + snd_ctl_new1(&toneport_control_source, + line6->line6pcm)); + if (err < 0) + return err; + } + + line6_read_serial_number(line6, &toneport->serial_number); + line6_read_data(line6, 0x80c2, &toneport->firmware_version, 1); + + if (toneport_has_led(toneport)) { + err = toneport_init_leds(toneport); + if (err < 0) + return err; + } + + err = toneport_setup(toneport); + if (err) + return err; + + /* register audio system: */ + return snd_card_register(line6->card); +} + +#ifdef CONFIG_PM +/* + Resume Toneport device after reset. +*/ +static int toneport_reset_resume(struct usb_interface *interface) +{ + int err; + + err = toneport_setup(usb_get_intfdata(interface)); + if (err) + return err; + return line6_resume(interface); +} +#endif + +#define LINE6_DEVICE(prod) USB_DEVICE(0x0e41, prod) +#define LINE6_IF_NUM(prod, n) USB_DEVICE_INTERFACE_NUMBER(0x0e41, prod, n) + +/* table of devices that work with this driver */ +static const struct usb_device_id toneport_id_table[] = { + { LINE6_DEVICE(0x4750), .driver_info = LINE6_GUITARPORT }, + { LINE6_DEVICE(0x4153), .driver_info = LINE6_PODSTUDIO_GX }, + { LINE6_DEVICE(0x4150), .driver_info = LINE6_PODSTUDIO_UX1 }, + { LINE6_IF_NUM(0x4151, 0), .driver_info = LINE6_PODSTUDIO_UX2 }, + { LINE6_DEVICE(0x4147), .driver_info = LINE6_TONEPORT_GX }, + { LINE6_DEVICE(0x4141), .driver_info = LINE6_TONEPORT_UX1 }, + { LINE6_IF_NUM(0x4142, 0), .driver_info = LINE6_TONEPORT_UX2 }, + {} +}; + +MODULE_DEVICE_TABLE(usb, toneport_id_table); + +static const struct line6_properties toneport_properties_table[] = { + [LINE6_GUITARPORT] = { + .id = "GuitarPort", + .name = "GuitarPort", + .capabilities = LINE6_CAP_PCM, + .altsetting = 2, /* 1..4 seem to be ok */ + /* no control channel */ + .ep_audio_r = 0x82, + .ep_audio_w = 0x01, + }, + [LINE6_PODSTUDIO_GX] = { + .id = "PODStudioGX", + .name = "POD Studio GX", + .capabilities = LINE6_CAP_PCM, + .altsetting = 2, /* 1..4 seem to be ok */ + /* no control channel */ + .ep_audio_r = 0x82, + .ep_audio_w = 0x01, + }, + [LINE6_PODSTUDIO_UX1] = { + .id = "PODStudioUX1", + .name = "POD Studio UX1", + .capabilities = LINE6_CAP_PCM, + .altsetting = 2, /* 1..4 seem to be ok */ + /* no control channel */ + .ep_audio_r = 0x82, + .ep_audio_w = 0x01, + }, + [LINE6_PODSTUDIO_UX2] = { + .id = "PODStudioUX2", + .name = "POD Studio UX2", + .capabilities = LINE6_CAP_PCM, + .altsetting = 2, /* defaults to 44.1kHz, 16-bit */ + /* no control channel */ + .ep_audio_r = 0x82, + .ep_audio_w = 0x01, + }, + [LINE6_TONEPORT_GX] = { + .id = "TonePortGX", + .name = "TonePort GX", + .capabilities = LINE6_CAP_PCM, + .altsetting = 2, /* 1..4 seem to be ok */ + /* no control channel */ + .ep_audio_r = 0x82, + .ep_audio_w = 0x01, + }, + [LINE6_TONEPORT_UX1] = { + .id = "TonePortUX1", + .name = "TonePort UX1", + .capabilities = LINE6_CAP_PCM, + .altsetting = 2, /* 1..4 seem to be ok */ + /* no control channel */ + .ep_audio_r = 0x82, + .ep_audio_w = 0x01, + }, + [LINE6_TONEPORT_UX2] = { + .id = "TonePortUX2", + .name = "TonePort UX2", + .capabilities = LINE6_CAP_PCM, + .altsetting = 2, /* defaults to 44.1kHz, 16-bit */ + /* no control channel */ + .ep_audio_r = 0x82, + .ep_audio_w = 0x01, + }, +}; + +/* + Probe USB device. +*/ +static int toneport_probe(struct usb_interface *interface, + const struct usb_device_id *id) +{ + return line6_probe(interface, id, "Line6-TonePort", + &toneport_properties_table[id->driver_info], + toneport_init, sizeof(struct usb_line6_toneport)); +} + +static struct usb_driver toneport_driver = { + .name = KBUILD_MODNAME, + .probe = toneport_probe, + .disconnect = line6_disconnect, +#ifdef CONFIG_PM + .suspend = line6_suspend, + .resume = line6_resume, + .reset_resume = toneport_reset_resume, +#endif + .id_table = toneport_id_table, +}; + +module_usb_driver(toneport_driver); + +MODULE_DESCRIPTION("TonePort USB driver"); +MODULE_LICENSE("GPL"); diff --git a/sound/usb/line6/variax.c b/sound/usb/line6/variax.c new file mode 100644 index 000000000..c2245aa93 --- /dev/null +++ b/sound/usb/line6/variax.c @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Line 6 Linux USB driver + * + * Copyright (C) 2004-2010 Markus Grabner (grabner@icg.tugraz.at) + */ + +#include <linux/slab.h> +#include <linux/spinlock.h> +#include <linux/usb.h> +#include <linux/wait.h> +#include <linux/module.h> +#include <sound/core.h> + +#include "driver.h" + +#define VARIAX_STARTUP_DELAY1 1000 +#define VARIAX_STARTUP_DELAY3 100 +#define VARIAX_STARTUP_DELAY4 100 + +/* + Stages of Variax startup procedure +*/ +enum { + VARIAX_STARTUP_VERSIONREQ, + VARIAX_STARTUP_ACTIVATE, + VARIAX_STARTUP_SETUP, +}; + +enum { + LINE6_PODXTLIVE_VARIAX, + LINE6_VARIAX +}; + +struct usb_line6_variax { + /* Generic Line 6 USB data */ + struct usb_line6 line6; + + /* Buffer for activation code */ + unsigned char *buffer_activate; + + /* Current progress in startup procedure */ + int startup_progress; +}; + +#define line6_to_variax(x) container_of(x, struct usb_line6_variax, line6) + +#define VARIAX_OFFSET_ACTIVATE 7 + +/* + This message is sent by the device during initialization and identifies + the connected guitar version. +*/ +static const char variax_init_version[] = { + 0xf0, 0x7e, 0x7f, 0x06, 0x02, 0x00, 0x01, 0x0c, + 0x07, 0x00, 0x00, 0x00 +}; + +/* + This message is the last one sent by the device during initialization. +*/ +static const char variax_init_done[] = { + 0xf0, 0x00, 0x01, 0x0c, 0x07, 0x00, 0x6b +}; + +static const char variax_activate[] = { + 0xf0, 0x00, 0x01, 0x0c, 0x07, 0x00, 0x2a, 0x01, + 0xf7 +}; + +static void variax_activate_async(struct usb_line6_variax *variax, int a) +{ + variax->buffer_activate[VARIAX_OFFSET_ACTIVATE] = a; + line6_send_raw_message_async(&variax->line6, variax->buffer_activate, + sizeof(variax_activate)); +} + +/* + Variax startup procedure. + This is a sequence of functions with special requirements (e.g., must + not run immediately after initialization, must not run in interrupt + context). After the last one has finished, the device is ready to use. +*/ + +static void variax_startup(struct usb_line6 *line6) +{ + struct usb_line6_variax *variax = line6_to_variax(line6); + + switch (variax->startup_progress) { + case VARIAX_STARTUP_VERSIONREQ: + /* repeat request until getting the response */ + schedule_delayed_work(&line6->startup_work, + msecs_to_jiffies(VARIAX_STARTUP_DELAY1)); + /* request firmware version: */ + line6_version_request_async(line6); + break; + case VARIAX_STARTUP_ACTIVATE: + /* activate device: */ + variax_activate_async(variax, 1); + variax->startup_progress = VARIAX_STARTUP_SETUP; + schedule_delayed_work(&line6->startup_work, + msecs_to_jiffies(VARIAX_STARTUP_DELAY4)); + break; + case VARIAX_STARTUP_SETUP: + /* ALSA audio interface: */ + snd_card_register(variax->line6.card); + break; + } +} + +/* + Process a completely received message. +*/ +static void line6_variax_process_message(struct usb_line6 *line6) +{ + struct usb_line6_variax *variax = line6_to_variax(line6); + const unsigned char *buf = variax->line6.buffer_message; + + switch (buf[0]) { + case LINE6_RESET: + dev_info(variax->line6.ifcdev, "VARIAX reset\n"); + break; + + case LINE6_SYSEX_BEGIN: + if (memcmp(buf + 1, variax_init_version + 1, + sizeof(variax_init_version) - 1) == 0) { + if (variax->startup_progress >= VARIAX_STARTUP_ACTIVATE) + break; + variax->startup_progress = VARIAX_STARTUP_ACTIVATE; + cancel_delayed_work(&line6->startup_work); + schedule_delayed_work(&line6->startup_work, + msecs_to_jiffies(VARIAX_STARTUP_DELAY3)); + } else if (memcmp(buf + 1, variax_init_done + 1, + sizeof(variax_init_done) - 1) == 0) { + /* notify of complete initialization: */ + if (variax->startup_progress >= VARIAX_STARTUP_SETUP) + break; + cancel_delayed_work(&line6->startup_work); + schedule_delayed_work(&line6->startup_work, 0); + } + break; + } +} + +/* + Variax destructor. +*/ +static void line6_variax_disconnect(struct usb_line6 *line6) +{ + struct usb_line6_variax *variax = line6_to_variax(line6); + + kfree(variax->buffer_activate); +} + +/* + Try to init workbench device. +*/ +static int variax_init(struct usb_line6 *line6, + const struct usb_device_id *id) +{ + struct usb_line6_variax *variax = line6_to_variax(line6); + + line6->process_message = line6_variax_process_message; + line6->disconnect = line6_variax_disconnect; + line6->startup = variax_startup; + + /* initialize USB buffers: */ + variax->buffer_activate = kmemdup(variax_activate, + sizeof(variax_activate), GFP_KERNEL); + + if (variax->buffer_activate == NULL) + return -ENOMEM; + + /* initiate startup procedure: */ + schedule_delayed_work(&line6->startup_work, + msecs_to_jiffies(VARIAX_STARTUP_DELAY1)); + return 0; +} + +#define LINE6_DEVICE(prod) USB_DEVICE(0x0e41, prod) +#define LINE6_IF_NUM(prod, n) USB_DEVICE_INTERFACE_NUMBER(0x0e41, prod, n) + +/* table of devices that work with this driver */ +static const struct usb_device_id variax_id_table[] = { + { LINE6_IF_NUM(0x4650, 1), .driver_info = LINE6_PODXTLIVE_VARIAX }, + { LINE6_DEVICE(0x534d), .driver_info = LINE6_VARIAX }, + {} +}; + +MODULE_DEVICE_TABLE(usb, variax_id_table); + +static const struct line6_properties variax_properties_table[] = { + [LINE6_PODXTLIVE_VARIAX] = { + .id = "PODxtLive", + .name = "PODxt Live", + .capabilities = LINE6_CAP_CONTROL + | LINE6_CAP_CONTROL_MIDI, + .altsetting = 1, + .ep_ctrl_r = 0x86, + .ep_ctrl_w = 0x05, + .ep_audio_r = 0x82, + .ep_audio_w = 0x01, + }, + [LINE6_VARIAX] = { + .id = "Variax", + .name = "Variax Workbench", + .capabilities = LINE6_CAP_CONTROL + | LINE6_CAP_CONTROL_MIDI, + .altsetting = 1, + .ep_ctrl_r = 0x82, + .ep_ctrl_w = 0x01, + /* no audio channel */ + } +}; + +/* + Probe USB device. +*/ +static int variax_probe(struct usb_interface *interface, + const struct usb_device_id *id) +{ + return line6_probe(interface, id, "Line6-Variax", + &variax_properties_table[id->driver_info], + variax_init, sizeof(struct usb_line6_variax)); +} + +static struct usb_driver variax_driver = { + .name = KBUILD_MODNAME, + .probe = variax_probe, + .disconnect = line6_disconnect, +#ifdef CONFIG_PM + .suspend = line6_suspend, + .resume = line6_resume, + .reset_resume = line6_resume, +#endif + .id_table = variax_id_table, +}; + +module_usb_driver(variax_driver); + +MODULE_DESCRIPTION("Variax Workbench USB driver"); +MODULE_LICENSE("GPL"); |