| // SPDX-License-Identifier: GPL-2.0-or-later |
| /* |
| * MIDI 2.0 support |
| */ |
| |
| #include <linux/bitops.h> |
| #include <linux/string.h> |
| #include <linux/init.h> |
| #include <linux/slab.h> |
| #include <linux/usb.h> |
| #include <linux/wait.h> |
| #include <linux/module.h> |
| #include <linux/moduleparam.h> |
| #include <linux/usb/audio.h> |
| #include <linux/usb/midi.h> |
| #include <linux/usb/midi-v2.h> |
| |
| #include <sound/core.h> |
| #include <sound/control.h> |
| #include <sound/ump.h> |
| #include "usbaudio.h" |
| #include "midi.h" |
| #include "midi2.h" |
| #include "helper.h" |
| |
| static bool midi2_enable = true; |
| module_param(midi2_enable, bool, 0444); |
| MODULE_PARM_DESC(midi2_enable, "Enable MIDI 2.0 support."); |
| |
| static bool midi2_ump_probe = true; |
| module_param(midi2_ump_probe, bool, 0444); |
| MODULE_PARM_DESC(midi2_ump_probe, "Probe UMP v1.1 support at first."); |
| |
| /* stream direction; just shorter names */ |
| enum { |
| STR_OUT = SNDRV_RAWMIDI_STREAM_OUTPUT, |
| STR_IN = SNDRV_RAWMIDI_STREAM_INPUT |
| }; |
| |
| #define NUM_URBS 8 |
| |
| struct snd_usb_midi2_urb; |
| struct snd_usb_midi2_endpoint; |
| struct snd_usb_midi2_ump; |
| struct snd_usb_midi2_interface; |
| |
| /* URB context */ |
| struct snd_usb_midi2_urb { |
| struct urb *urb; |
| struct snd_usb_midi2_endpoint *ep; |
| unsigned int index; /* array index */ |
| }; |
| |
| /* A USB MIDI input/output endpoint */ |
| struct snd_usb_midi2_endpoint { |
| struct usb_device *dev; |
| const struct usb_ms20_endpoint_descriptor *ms_ep; /* reference to EP descriptor */ |
| struct snd_usb_midi2_endpoint *pair; /* bidirectional pair EP */ |
| struct snd_usb_midi2_ump *rmidi; /* assigned UMP EP pair */ |
| struct snd_ump_endpoint *ump; /* assigned UMP EP */ |
| int direction; /* direction (STR_IN/OUT) */ |
| unsigned int endpoint; /* EP number */ |
| unsigned int pipe; /* URB pipe */ |
| unsigned int packets; /* packet buffer size in bytes */ |
| unsigned int interval; /* interval for INT EP */ |
| wait_queue_head_t wait; /* URB waiter */ |
| spinlock_t lock; /* URB locking */ |
| struct snd_rawmidi_substream *substream; /* NULL when closed */ |
| unsigned int num_urbs; /* number of allocated URBs */ |
| unsigned long urb_free; /* bitmap for free URBs */ |
| unsigned long urb_free_mask; /* bitmask for free URBs */ |
| atomic_t running; /* running status */ |
| atomic_t suspended; /* saved running status for suspend */ |
| bool disconnected; /* shadow of umidi->disconnected */ |
| struct list_head list; /* list to umidi->ep_list */ |
| struct snd_usb_midi2_urb urbs[NUM_URBS]; |
| }; |
| |
| /* A UMP endpoint - one or two USB MIDI endpoints are assigned */ |
| struct snd_usb_midi2_ump { |
| struct usb_device *dev; |
| struct snd_usb_midi2_interface *umidi; /* reference to MIDI iface */ |
| struct snd_ump_endpoint *ump; /* assigned UMP EP object */ |
| struct snd_usb_midi2_endpoint *eps[2]; /* USB MIDI endpoints */ |
| int index; /* rawmidi device index */ |
| unsigned char usb_block_id; /* USB GTB id used for finding a pair */ |
| bool ump_parsed; /* Parsed UMP 1.1 EP/FB info*/ |
| struct list_head list; /* list to umidi->rawmidi_list */ |
| }; |
| |
| /* top-level instance per USB MIDI interface */ |
| struct snd_usb_midi2_interface { |
| struct snd_usb_audio *chip; /* assigned USB-audio card */ |
| struct usb_interface *iface; /* assigned USB interface */ |
| struct usb_host_interface *hostif; |
| const char *blk_descs; /* group terminal block descriptors */ |
| unsigned int blk_desc_size; /* size of GTB descriptors */ |
| bool disconnected; |
| struct list_head ep_list; /* list of endpoints */ |
| struct list_head rawmidi_list; /* list of UMP rawmidis */ |
| struct list_head list; /* list to chip->midi_v2_list */ |
| }; |
| |
| /* submit URBs as much as possible; used for both input and output */ |
| static void do_submit_urbs_locked(struct snd_usb_midi2_endpoint *ep, |
| int (*prepare)(struct snd_usb_midi2_endpoint *, |
| struct urb *)) |
| { |
| struct snd_usb_midi2_urb *ctx; |
| int index, err = 0; |
| |
| if (ep->disconnected) |
| return; |
| |
| while (ep->urb_free) { |
| index = find_first_bit(&ep->urb_free, ep->num_urbs); |
| if (index >= ep->num_urbs) |
| return; |
| ctx = &ep->urbs[index]; |
| err = prepare(ep, ctx->urb); |
| if (err < 0) |
| return; |
| if (!ctx->urb->transfer_buffer_length) |
| return; |
| ctx->urb->dev = ep->dev; |
| err = usb_submit_urb(ctx->urb, GFP_ATOMIC); |
| if (err < 0) { |
| dev_dbg(&ep->dev->dev, |
| "usb_submit_urb error %d\n", err); |
| return; |
| } |
| clear_bit(index, &ep->urb_free); |
| } |
| } |
| |
| /* prepare for output submission: copy from rawmidi buffer to urb packet */ |
| static int prepare_output_urb(struct snd_usb_midi2_endpoint *ep, |
| struct urb *urb) |
| { |
| int count; |
| |
| count = snd_ump_transmit(ep->ump, urb->transfer_buffer, |
| ep->packets); |
| if (count < 0) { |
| dev_dbg(&ep->dev->dev, "rawmidi transmit error %d\n", count); |
| return count; |
| } |
| cpu_to_le32_array((u32 *)urb->transfer_buffer, count >> 2); |
| urb->transfer_buffer_length = count; |
| return 0; |
| } |
| |
| static void submit_output_urbs_locked(struct snd_usb_midi2_endpoint *ep) |
| { |
| do_submit_urbs_locked(ep, prepare_output_urb); |
| } |
| |
| /* URB completion for output; re-filling and re-submit */ |
| static void output_urb_complete(struct urb *urb) |
| { |
| struct snd_usb_midi2_urb *ctx = urb->context; |
| struct snd_usb_midi2_endpoint *ep = ctx->ep; |
| unsigned long flags; |
| |
| spin_lock_irqsave(&ep->lock, flags); |
| set_bit(ctx->index, &ep->urb_free); |
| if (urb->status >= 0 && atomic_read(&ep->running)) |
| submit_output_urbs_locked(ep); |
| if (ep->urb_free == ep->urb_free_mask) |
| wake_up(&ep->wait); |
| spin_unlock_irqrestore(&ep->lock, flags); |
| } |
| |
| /* prepare for input submission: just set the buffer length */ |
| static int prepare_input_urb(struct snd_usb_midi2_endpoint *ep, |
| struct urb *urb) |
| { |
| urb->transfer_buffer_length = ep->packets; |
| return 0; |
| } |
| |
| static void submit_input_urbs_locked(struct snd_usb_midi2_endpoint *ep) |
| { |
| do_submit_urbs_locked(ep, prepare_input_urb); |
| } |
| |
| /* URB completion for input; copy into rawmidi buffer and resubmit */ |
| static void input_urb_complete(struct urb *urb) |
| { |
| struct snd_usb_midi2_urb *ctx = urb->context; |
| struct snd_usb_midi2_endpoint *ep = ctx->ep; |
| unsigned long flags; |
| int len; |
| |
| spin_lock_irqsave(&ep->lock, flags); |
| if (ep->disconnected || urb->status < 0) |
| goto dequeue; |
| len = urb->actual_length; |
| len &= ~3; /* align UMP */ |
| if (len > ep->packets) |
| len = ep->packets; |
| if (len > 0) { |
| le32_to_cpu_array((u32 *)urb->transfer_buffer, len >> 2); |
| snd_ump_receive(ep->ump, (u32 *)urb->transfer_buffer, len); |
| } |
| dequeue: |
| set_bit(ctx->index, &ep->urb_free); |
| submit_input_urbs_locked(ep); |
| if (ep->urb_free == ep->urb_free_mask) |
| wake_up(&ep->wait); |
| spin_unlock_irqrestore(&ep->lock, flags); |
| } |
| |
| /* URB submission helper; for both direction */ |
| static void submit_io_urbs(struct snd_usb_midi2_endpoint *ep) |
| { |
| unsigned long flags; |
| |
| if (!ep) |
| return; |
| spin_lock_irqsave(&ep->lock, flags); |
| if (ep->direction == STR_IN) |
| submit_input_urbs_locked(ep); |
| else |
| submit_output_urbs_locked(ep); |
| spin_unlock_irqrestore(&ep->lock, flags); |
| } |
| |
| /* kill URBs for close, suspend and disconnect */ |
| static void kill_midi_urbs(struct snd_usb_midi2_endpoint *ep, bool suspending) |
| { |
| int i; |
| |
| if (!ep) |
| return; |
| if (suspending) |
| ep->suspended = ep->running; |
| atomic_set(&ep->running, 0); |
| for (i = 0; i < ep->num_urbs; i++) { |
| if (!ep->urbs[i].urb) |
| break; |
| usb_kill_urb(ep->urbs[i].urb); |
| } |
| } |
| |
| /* wait until all URBs get freed */ |
| static void drain_urb_queue(struct snd_usb_midi2_endpoint *ep) |
| { |
| if (!ep) |
| return; |
| spin_lock_irq(&ep->lock); |
| atomic_set(&ep->running, 0); |
| wait_event_lock_irq_timeout(ep->wait, |
| ep->disconnected || |
| ep->urb_free == ep->urb_free_mask, |
| ep->lock, msecs_to_jiffies(500)); |
| spin_unlock_irq(&ep->lock); |
| } |
| |
| /* release URBs for an EP */ |
| static void free_midi_urbs(struct snd_usb_midi2_endpoint *ep) |
| { |
| struct snd_usb_midi2_urb *ctx; |
| int i; |
| |
| if (!ep) |
| return; |
| for (i = 0; i < NUM_URBS; ++i) { |
| ctx = &ep->urbs[i]; |
| if (!ctx->urb) |
| break; |
| usb_free_coherent(ep->dev, ep->packets, |
| ctx->urb->transfer_buffer, |
| ctx->urb->transfer_dma); |
| usb_free_urb(ctx->urb); |
| ctx->urb = NULL; |
| } |
| ep->num_urbs = 0; |
| } |
| |
| /* allocate URBs for an EP */ |
| /* the callers should handle allocation errors via free_midi_urbs() */ |
| static int alloc_midi_urbs(struct snd_usb_midi2_endpoint *ep) |
| { |
| struct snd_usb_midi2_urb *ctx; |
| void (*comp)(struct urb *urb); |
| void *buffer; |
| int i, err; |
| int endpoint, len; |
| |
| endpoint = ep->endpoint; |
| len = ep->packets; |
| if (ep->direction == STR_IN) |
| comp = input_urb_complete; |
| else |
| comp = output_urb_complete; |
| |
| ep->num_urbs = 0; |
| ep->urb_free = ep->urb_free_mask = 0; |
| for (i = 0; i < NUM_URBS; i++) { |
| ctx = &ep->urbs[i]; |
| ctx->index = i; |
| ctx->urb = usb_alloc_urb(0, GFP_KERNEL); |
| if (!ctx->urb) { |
| dev_err(&ep->dev->dev, "URB alloc failed\n"); |
| return -ENOMEM; |
| } |
| ctx->ep = ep; |
| buffer = usb_alloc_coherent(ep->dev, len, GFP_KERNEL, |
| &ctx->urb->transfer_dma); |
| if (!buffer) { |
| dev_err(&ep->dev->dev, |
| "URB buffer alloc failed (size %d)\n", len); |
| return -ENOMEM; |
| } |
| if (ep->interval) |
| usb_fill_int_urb(ctx->urb, ep->dev, ep->pipe, |
| buffer, len, comp, ctx, ep->interval); |
| else |
| usb_fill_bulk_urb(ctx->urb, ep->dev, ep->pipe, |
| buffer, len, comp, ctx); |
| err = usb_urb_ep_type_check(ctx->urb); |
| if (err < 0) { |
| dev_err(&ep->dev->dev, "invalid MIDI EP %x\n", |
| endpoint); |
| return err; |
| } |
| ctx->urb->transfer_flags = URB_NO_TRANSFER_DMA_MAP; |
| ep->num_urbs++; |
| } |
| ep->urb_free = ep->urb_free_mask = GENMASK(ep->num_urbs - 1, 0); |
| return 0; |
| } |
| |
| static struct snd_usb_midi2_endpoint * |
| ump_to_endpoint(struct snd_ump_endpoint *ump, int dir) |
| { |
| struct snd_usb_midi2_ump *rmidi = ump->private_data; |
| |
| return rmidi->eps[dir]; |
| } |
| |
| /* ump open callback */ |
| static int snd_usb_midi_v2_open(struct snd_ump_endpoint *ump, int dir) |
| { |
| struct snd_usb_midi2_endpoint *ep = ump_to_endpoint(ump, dir); |
| int err = 0; |
| |
| if (!ep || !ep->endpoint) |
| return -ENODEV; |
| if (ep->disconnected) |
| return -EIO; |
| if (ep->direction == STR_OUT) { |
| err = alloc_midi_urbs(ep); |
| if (err) { |
| free_midi_urbs(ep); |
| return err; |
| } |
| } |
| return 0; |
| } |
| |
| /* ump close callback */ |
| static void snd_usb_midi_v2_close(struct snd_ump_endpoint *ump, int dir) |
| { |
| struct snd_usb_midi2_endpoint *ep = ump_to_endpoint(ump, dir); |
| |
| if (ep->direction == STR_OUT) { |
| kill_midi_urbs(ep, false); |
| drain_urb_queue(ep); |
| free_midi_urbs(ep); |
| } |
| } |
| |
| /* ump trigger callback */ |
| static void snd_usb_midi_v2_trigger(struct snd_ump_endpoint *ump, int dir, |
| int up) |
| { |
| struct snd_usb_midi2_endpoint *ep = ump_to_endpoint(ump, dir); |
| |
| atomic_set(&ep->running, up); |
| if (up && ep->direction == STR_OUT && !ep->disconnected) |
| submit_io_urbs(ep); |
| } |
| |
| /* ump drain callback */ |
| static void snd_usb_midi_v2_drain(struct snd_ump_endpoint *ump, int dir) |
| { |
| struct snd_usb_midi2_endpoint *ep = ump_to_endpoint(ump, dir); |
| |
| drain_urb_queue(ep); |
| } |
| |
| /* allocate and start all input streams */ |
| static int start_input_streams(struct snd_usb_midi2_interface *umidi) |
| { |
| struct snd_usb_midi2_endpoint *ep; |
| int err; |
| |
| list_for_each_entry(ep, &umidi->ep_list, list) { |
| if (ep->direction == STR_IN) { |
| err = alloc_midi_urbs(ep); |
| if (err < 0) |
| goto error; |
| } |
| } |
| |
| list_for_each_entry(ep, &umidi->ep_list, list) { |
| if (ep->direction == STR_IN) |
| submit_io_urbs(ep); |
| } |
| |
| return 0; |
| |
| error: |
| list_for_each_entry(ep, &umidi->ep_list, list) { |
| if (ep->direction == STR_IN) |
| free_midi_urbs(ep); |
| } |
| |
| return err; |
| } |
| |
| static const struct snd_ump_ops snd_usb_midi_v2_ump_ops = { |
| .open = snd_usb_midi_v2_open, |
| .close = snd_usb_midi_v2_close, |
| .trigger = snd_usb_midi_v2_trigger, |
| .drain = snd_usb_midi_v2_drain, |
| }; |
| |
| /* create a USB MIDI 2.0 endpoint object */ |
| static int create_midi2_endpoint(struct snd_usb_midi2_interface *umidi, |
| struct usb_host_endpoint *hostep, |
| const struct usb_ms20_endpoint_descriptor *ms_ep) |
| { |
| struct snd_usb_midi2_endpoint *ep; |
| int endpoint, dir; |
| |
| usb_audio_dbg(umidi->chip, "Creating an EP 0x%02x, #GTB=%d\n", |
| hostep->desc.bEndpointAddress, |
| ms_ep->bNumGrpTrmBlock); |
| |
| ep = kzalloc(sizeof(*ep), GFP_KERNEL); |
| if (!ep) |
| return -ENOMEM; |
| |
| spin_lock_init(&ep->lock); |
| init_waitqueue_head(&ep->wait); |
| ep->dev = umidi->chip->dev; |
| endpoint = hostep->desc.bEndpointAddress; |
| dir = (endpoint & USB_DIR_IN) ? STR_IN : STR_OUT; |
| |
| ep->endpoint = endpoint; |
| ep->direction = dir; |
| ep->ms_ep = ms_ep; |
| if (usb_endpoint_xfer_int(&hostep->desc)) |
| ep->interval = hostep->desc.bInterval; |
| else |
| ep->interval = 0; |
| if (dir == STR_IN) { |
| if (ep->interval) |
| ep->pipe = usb_rcvintpipe(ep->dev, endpoint); |
| else |
| ep->pipe = usb_rcvbulkpipe(ep->dev, endpoint); |
| } else { |
| if (ep->interval) |
| ep->pipe = usb_sndintpipe(ep->dev, endpoint); |
| else |
| ep->pipe = usb_sndbulkpipe(ep->dev, endpoint); |
| } |
| ep->packets = usb_maxpacket(ep->dev, ep->pipe); |
| list_add_tail(&ep->list, &umidi->ep_list); |
| |
| return 0; |
| } |
| |
| /* destructor for endpoint; from snd_usb_midi_v2_free() */ |
| static void free_midi2_endpoint(struct snd_usb_midi2_endpoint *ep) |
| { |
| list_del(&ep->list); |
| free_midi_urbs(ep); |
| kfree(ep); |
| } |
| |
| /* call all endpoint destructors */ |
| static void free_all_midi2_endpoints(struct snd_usb_midi2_interface *umidi) |
| { |
| struct snd_usb_midi2_endpoint *ep; |
| |
| while (!list_empty(&umidi->ep_list)) { |
| ep = list_first_entry(&umidi->ep_list, |
| struct snd_usb_midi2_endpoint, list); |
| free_midi2_endpoint(ep); |
| } |
| } |
| |
| /* find a MIDI STREAMING descriptor with a given subtype */ |
| static void *find_usb_ms_endpoint_descriptor(struct usb_host_endpoint *hostep, |
| unsigned char subtype) |
| { |
| unsigned char *extra = hostep->extra; |
| int extralen = hostep->extralen; |
| |
| while (extralen > 3) { |
| struct usb_ms_endpoint_descriptor *ms_ep = |
| (struct usb_ms_endpoint_descriptor *)extra; |
| |
| if (ms_ep->bLength > 3 && |
| ms_ep->bDescriptorType == USB_DT_CS_ENDPOINT && |
| ms_ep->bDescriptorSubtype == subtype) |
| return ms_ep; |
| if (!extra[0]) |
| break; |
| extralen -= extra[0]; |
| extra += extra[0]; |
| } |
| return NULL; |
| } |
| |
| /* get the full group terminal block descriptors and return the size */ |
| static int get_group_terminal_block_descs(struct snd_usb_midi2_interface *umidi) |
| { |
| struct usb_host_interface *hostif = umidi->hostif; |
| struct usb_device *dev = umidi->chip->dev; |
| struct usb_ms20_gr_trm_block_header_descriptor header = { 0 }; |
| unsigned char *data; |
| int err, size; |
| |
| err = snd_usb_ctl_msg(dev, usb_rcvctrlpipe(dev, 0), |
| USB_REQ_GET_DESCRIPTOR, |
| USB_RECIP_INTERFACE | USB_TYPE_STANDARD | USB_DIR_IN, |
| USB_DT_CS_GR_TRM_BLOCK << 8 | hostif->desc.bAlternateSetting, |
| hostif->desc.bInterfaceNumber, |
| &header, sizeof(header)); |
| if (err < 0) |
| return err; |
| size = __le16_to_cpu(header.wTotalLength); |
| if (!size) { |
| dev_err(&dev->dev, "Failed to get GTB descriptors for %d:%d\n", |
| hostif->desc.bInterfaceNumber, hostif->desc.bAlternateSetting); |
| return -EINVAL; |
| } |
| |
| data = kzalloc(size, GFP_KERNEL); |
| if (!data) |
| return -ENOMEM; |
| |
| err = snd_usb_ctl_msg(dev, usb_rcvctrlpipe(dev, 0), |
| USB_REQ_GET_DESCRIPTOR, |
| USB_RECIP_INTERFACE | USB_TYPE_STANDARD | USB_DIR_IN, |
| USB_DT_CS_GR_TRM_BLOCK << 8 | hostif->desc.bAlternateSetting, |
| hostif->desc.bInterfaceNumber, data, size); |
| if (err < 0) { |
| kfree(data); |
| return err; |
| } |
| |
| umidi->blk_descs = data; |
| umidi->blk_desc_size = size; |
| return 0; |
| } |
| |
| /* find the corresponding group terminal block descriptor */ |
| static const struct usb_ms20_gr_trm_block_descriptor * |
| find_group_terminal_block(struct snd_usb_midi2_interface *umidi, int id) |
| { |
| const unsigned char *data = umidi->blk_descs; |
| int size = umidi->blk_desc_size; |
| const struct usb_ms20_gr_trm_block_descriptor *desc; |
| |
| size -= sizeof(struct usb_ms20_gr_trm_block_header_descriptor); |
| data += sizeof(struct usb_ms20_gr_trm_block_header_descriptor); |
| while (size > 0 && *data && *data <= size) { |
| desc = (const struct usb_ms20_gr_trm_block_descriptor *)data; |
| if (desc->bLength >= sizeof(*desc) && |
| desc->bDescriptorType == USB_DT_CS_GR_TRM_BLOCK && |
| desc->bDescriptorSubtype == USB_MS_GR_TRM_BLOCK && |
| desc->bGrpTrmBlkID == id) |
| return desc; |
| size -= *data; |
| data += *data; |
| } |
| |
| return NULL; |
| } |
| |
| /* fill up the information from GTB */ |
| static int parse_group_terminal_block(struct snd_usb_midi2_ump *rmidi, |
| const struct usb_ms20_gr_trm_block_descriptor *desc) |
| { |
| struct snd_ump_endpoint *ump = rmidi->ump; |
| unsigned int protocol, protocol_caps; |
| |
| /* set default protocol */ |
| switch (desc->bMIDIProtocol) { |
| case USB_MS_MIDI_PROTO_1_0_64: |
| case USB_MS_MIDI_PROTO_1_0_64_JRTS: |
| case USB_MS_MIDI_PROTO_1_0_128: |
| case USB_MS_MIDI_PROTO_1_0_128_JRTS: |
| protocol = SNDRV_UMP_EP_INFO_PROTO_MIDI1; |
| break; |
| case USB_MS_MIDI_PROTO_2_0: |
| case USB_MS_MIDI_PROTO_2_0_JRTS: |
| protocol = SNDRV_UMP_EP_INFO_PROTO_MIDI2; |
| break; |
| default: |
| return 0; |
| } |
| |
| if (!ump->info.protocol) |
| ump->info.protocol = protocol; |
| |
| protocol_caps = protocol; |
| switch (desc->bMIDIProtocol) { |
| case USB_MS_MIDI_PROTO_1_0_64_JRTS: |
| case USB_MS_MIDI_PROTO_1_0_128_JRTS: |
| case USB_MS_MIDI_PROTO_2_0_JRTS: |
| protocol_caps |= SNDRV_UMP_EP_INFO_PROTO_JRTS_TX | |
| SNDRV_UMP_EP_INFO_PROTO_JRTS_RX; |
| break; |
| } |
| |
| ump->info.protocol_caps |= protocol_caps; |
| return 0; |
| } |
| |
| /* allocate and parse for each assigned group terminal block */ |
| static int parse_group_terminal_blocks(struct snd_usb_midi2_interface *umidi) |
| { |
| struct snd_usb_midi2_ump *rmidi; |
| const struct usb_ms20_gr_trm_block_descriptor *desc; |
| int err; |
| |
| err = get_group_terminal_block_descs(umidi); |
| if (err < 0) |
| return err; |
| if (!umidi->blk_descs) |
| return 0; |
| |
| list_for_each_entry(rmidi, &umidi->rawmidi_list, list) { |
| desc = find_group_terminal_block(umidi, rmidi->usb_block_id); |
| if (!desc) |
| continue; |
| err = parse_group_terminal_block(rmidi, desc); |
| if (err < 0) |
| return err; |
| } |
| |
| return 0; |
| } |
| |
| /* parse endpoints included in the given interface and create objects */ |
| static int parse_midi_2_0_endpoints(struct snd_usb_midi2_interface *umidi) |
| { |
| struct usb_host_interface *hostif = umidi->hostif; |
| struct usb_host_endpoint *hostep; |
| struct usb_ms20_endpoint_descriptor *ms_ep; |
| int i, err; |
| |
| for (i = 0; i < hostif->desc.bNumEndpoints; i++) { |
| hostep = &hostif->endpoint[i]; |
| if (!usb_endpoint_xfer_bulk(&hostep->desc) && |
| !usb_endpoint_xfer_int(&hostep->desc)) |
| continue; |
| ms_ep = find_usb_ms_endpoint_descriptor(hostep, USB_MS_GENERAL_2_0); |
| if (!ms_ep) |
| continue; |
| if (ms_ep->bLength <= sizeof(*ms_ep)) |
| continue; |
| if (!ms_ep->bNumGrpTrmBlock) |
| continue; |
| if (ms_ep->bLength < sizeof(*ms_ep) + ms_ep->bNumGrpTrmBlock) |
| continue; |
| err = create_midi2_endpoint(umidi, hostep, ms_ep); |
| if (err < 0) |
| return err; |
| } |
| return 0; |
| } |
| |
| static void free_all_midi2_umps(struct snd_usb_midi2_interface *umidi) |
| { |
| struct snd_usb_midi2_ump *rmidi; |
| |
| while (!list_empty(&umidi->rawmidi_list)) { |
| rmidi = list_first_entry(&umidi->rawmidi_list, |
| struct snd_usb_midi2_ump, list); |
| list_del(&rmidi->list); |
| kfree(rmidi); |
| } |
| } |
| |
| static int create_midi2_ump(struct snd_usb_midi2_interface *umidi, |
| struct snd_usb_midi2_endpoint *ep_in, |
| struct snd_usb_midi2_endpoint *ep_out, |
| int blk_id) |
| { |
| struct snd_usb_midi2_ump *rmidi; |
| struct snd_ump_endpoint *ump; |
| int input, output; |
| char idstr[16]; |
| int err; |
| |
| rmidi = kzalloc(sizeof(*rmidi), GFP_KERNEL); |
| if (!rmidi) |
| return -ENOMEM; |
| INIT_LIST_HEAD(&rmidi->list); |
| rmidi->dev = umidi->chip->dev; |
| rmidi->umidi = umidi; |
| rmidi->usb_block_id = blk_id; |
| |
| rmidi->index = umidi->chip->num_rawmidis; |
| snprintf(idstr, sizeof(idstr), "UMP %d", rmidi->index); |
| input = ep_in ? 1 : 0; |
| output = ep_out ? 1 : 0; |
| err = snd_ump_endpoint_new(umidi->chip->card, idstr, rmidi->index, |
| output, input, &ump); |
| if (err < 0) { |
| usb_audio_dbg(umidi->chip, "Failed to create a UMP object\n"); |
| kfree(rmidi); |
| return err; |
| } |
| |
| rmidi->ump = ump; |
| umidi->chip->num_rawmidis++; |
| |
| ump->private_data = rmidi; |
| ump->ops = &snd_usb_midi_v2_ump_ops; |
| |
| rmidi->eps[STR_IN] = ep_in; |
| rmidi->eps[STR_OUT] = ep_out; |
| if (ep_in) { |
| ep_in->pair = ep_out; |
| ep_in->rmidi = rmidi; |
| ep_in->ump = ump; |
| } |
| if (ep_out) { |
| ep_out->pair = ep_in; |
| ep_out->rmidi = rmidi; |
| ep_out->ump = ump; |
| } |
| |
| list_add_tail(&rmidi->list, &umidi->rawmidi_list); |
| return 0; |
| } |
| |
| /* find the UMP EP with the given USB block id */ |
| static struct snd_usb_midi2_ump * |
| find_midi2_ump(struct snd_usb_midi2_interface *umidi, int blk_id) |
| { |
| struct snd_usb_midi2_ump *rmidi; |
| |
| list_for_each_entry(rmidi, &umidi->rawmidi_list, list) { |
| if (rmidi->usb_block_id == blk_id) |
| return rmidi; |
| } |
| return NULL; |
| } |
| |
| /* look for the matching output endpoint and create UMP object if found */ |
| static int find_matching_ep_partner(struct snd_usb_midi2_interface *umidi, |
| struct snd_usb_midi2_endpoint *ep, |
| int blk_id) |
| { |
| struct snd_usb_midi2_endpoint *pair_ep; |
| int blk; |
| |
| usb_audio_dbg(umidi->chip, "Looking for a pair for EP-in 0x%02x\n", |
| ep->endpoint); |
| list_for_each_entry(pair_ep, &umidi->ep_list, list) { |
| if (pair_ep->direction != STR_OUT) |
| continue; |
| if (pair_ep->pair) |
| continue; /* already paired */ |
| for (blk = 0; blk < pair_ep->ms_ep->bNumGrpTrmBlock; blk++) { |
| if (pair_ep->ms_ep->baAssoGrpTrmBlkID[blk] == blk_id) { |
| usb_audio_dbg(umidi->chip, |
| "Found a match with EP-out 0x%02x blk %d\n", |
| pair_ep->endpoint, blk); |
| return create_midi2_ump(umidi, ep, pair_ep, blk_id); |
| } |
| } |
| } |
| return 0; |
| } |
| |
| /* Call UMP helper to parse UMP endpoints; |
| * this needs to be called after starting the input streams for bi-directional |
| * communications |
| */ |
| static int parse_ump_endpoints(struct snd_usb_midi2_interface *umidi) |
| { |
| struct snd_usb_midi2_ump *rmidi; |
| int err; |
| |
| list_for_each_entry(rmidi, &umidi->rawmidi_list, list) { |
| if (!rmidi->ump || |
| !(rmidi->ump->core.info_flags & SNDRV_RAWMIDI_INFO_DUPLEX)) |
| continue; |
| err = snd_ump_parse_endpoint(rmidi->ump); |
| if (!err) { |
| rmidi->ump_parsed = true; |
| } else { |
| if (err == -ENOMEM) |
| return err; |
| /* fall back to GTB later */ |
| } |
| } |
| return 0; |
| } |
| |
| /* create a UMP block from a GTB entry */ |
| static int create_gtb_block(struct snd_usb_midi2_ump *rmidi, int dir, int blk) |
| { |
| struct snd_usb_midi2_interface *umidi = rmidi->umidi; |
| const struct usb_ms20_gr_trm_block_descriptor *desc; |
| struct snd_ump_block *fb; |
| int type, err; |
| |
| desc = find_group_terminal_block(umidi, blk); |
| if (!desc) |
| return 0; |
| |
| usb_audio_dbg(umidi->chip, |
| "GTB %d: type=%d, group=%d/%d, protocol=%d, in bw=%d, out bw=%d\n", |
| blk, desc->bGrpTrmBlkType, desc->nGroupTrm, |
| desc->nNumGroupTrm, desc->bMIDIProtocol, |
| __le16_to_cpu(desc->wMaxInputBandwidth), |
| __le16_to_cpu(desc->wMaxOutputBandwidth)); |
| |
| /* assign the direction */ |
| switch (desc->bGrpTrmBlkType) { |
| case USB_MS_GR_TRM_BLOCK_TYPE_BIDIRECTIONAL: |
| type = SNDRV_UMP_DIR_BIDIRECTION; |
| break; |
| case USB_MS_GR_TRM_BLOCK_TYPE_INPUT_ONLY: |
| type = SNDRV_UMP_DIR_INPUT; |
| break; |
| case USB_MS_GR_TRM_BLOCK_TYPE_OUTPUT_ONLY: |
| type = SNDRV_UMP_DIR_OUTPUT; |
| break; |
| default: |
| usb_audio_dbg(umidi->chip, "Unsupported GTB type %d\n", |
| desc->bGrpTrmBlkType); |
| return 0; /* unsupported */ |
| } |
| |
| /* guess work: set blk-1 as the (0-based) block ID */ |
| err = snd_ump_block_new(rmidi->ump, blk - 1, type, |
| desc->nGroupTrm, desc->nNumGroupTrm, |
| &fb); |
| if (err == -EBUSY) |
| return 0; /* already present */ |
| else if (err) |
| return err; |
| |
| if (desc->iBlockItem) |
| usb_string(rmidi->dev, desc->iBlockItem, |
| fb->info.name, sizeof(fb->info.name)); |
| |
| if (__le16_to_cpu(desc->wMaxInputBandwidth) == 1 || |
| __le16_to_cpu(desc->wMaxOutputBandwidth) == 1) |
| fb->info.flags |= SNDRV_UMP_BLOCK_IS_MIDI1 | |
| SNDRV_UMP_BLOCK_IS_LOWSPEED; |
| |
| /* if MIDI 2.0 protocol is supported and yet the GTB shows MIDI 1.0, |
| * treat it as a MIDI 1.0-specific block |
| */ |
| if (rmidi->ump->info.protocol_caps & SNDRV_UMP_EP_INFO_PROTO_MIDI2) { |
| switch (desc->bMIDIProtocol) { |
| case USB_MS_MIDI_PROTO_1_0_64: |
| case USB_MS_MIDI_PROTO_1_0_64_JRTS: |
| case USB_MS_MIDI_PROTO_1_0_128: |
| case USB_MS_MIDI_PROTO_1_0_128_JRTS: |
| fb->info.flags |= SNDRV_UMP_BLOCK_IS_MIDI1; |
| break; |
| } |
| } |
| |
| snd_ump_update_group_attrs(rmidi->ump); |
| |
| usb_audio_dbg(umidi->chip, |
| "Created a UMP block %d from GTB, name=%s, flags=0x%x\n", |
| blk, fb->info.name, fb->info.flags); |
| return 0; |
| } |
| |
| /* Create UMP blocks for each UMP EP */ |
| static int create_blocks_from_gtb(struct snd_usb_midi2_interface *umidi) |
| { |
| struct snd_usb_midi2_ump *rmidi; |
| int i, blk, err, dir; |
| |
| list_for_each_entry(rmidi, &umidi->rawmidi_list, list) { |
| if (!rmidi->ump) |
| continue; |
| /* Blocks have been already created? */ |
| if (rmidi->ump_parsed || rmidi->ump->info.num_blocks) |
| continue; |
| /* GTB is static-only */ |
| rmidi->ump->info.flags |= SNDRV_UMP_EP_INFO_STATIC_BLOCKS; |
| /* loop over GTBs */ |
| for (dir = 0; dir < 2; dir++) { |
| if (!rmidi->eps[dir]) |
| continue; |
| for (i = 0; i < rmidi->eps[dir]->ms_ep->bNumGrpTrmBlock; i++) { |
| blk = rmidi->eps[dir]->ms_ep->baAssoGrpTrmBlkID[i]; |
| err = create_gtb_block(rmidi, dir, blk); |
| if (err < 0) |
| return err; |
| } |
| } |
| } |
| |
| return 0; |
| } |
| |
| /* attach legacy rawmidis */ |
| static int attach_legacy_rawmidi(struct snd_usb_midi2_interface *umidi) |
| { |
| #if IS_ENABLED(CONFIG_SND_UMP_LEGACY_RAWMIDI) |
| struct snd_usb_midi2_ump *rmidi; |
| int err; |
| |
| list_for_each_entry(rmidi, &umidi->rawmidi_list, list) { |
| err = snd_ump_attach_legacy_rawmidi(rmidi->ump, |
| "Legacy MIDI", |
| umidi->chip->num_rawmidis); |
| if (err < 0) |
| return err; |
| umidi->chip->num_rawmidis++; |
| } |
| #endif |
| return 0; |
| } |
| |
| static void snd_usb_midi_v2_free(struct snd_usb_midi2_interface *umidi) |
| { |
| free_all_midi2_endpoints(umidi); |
| free_all_midi2_umps(umidi); |
| list_del(&umidi->list); |
| kfree(umidi->blk_descs); |
| kfree(umidi); |
| } |
| |
| /* parse the interface for MIDI 2.0 */ |
| static int parse_midi_2_0(struct snd_usb_midi2_interface *umidi) |
| { |
| struct snd_usb_midi2_endpoint *ep; |
| int blk, id, err; |
| |
| /* First, create an object for each USB MIDI Endpoint */ |
| err = parse_midi_2_0_endpoints(umidi); |
| if (err < 0) |
| return err; |
| if (list_empty(&umidi->ep_list)) { |
| usb_audio_warn(umidi->chip, "No MIDI endpoints found\n"); |
| return -ENODEV; |
| } |
| |
| /* |
| * Next, look for EP I/O pairs that are found in group terminal blocks |
| * A UMP object is created for each EP I/O pair as bidirecitonal |
| * UMP EP |
| */ |
| list_for_each_entry(ep, &umidi->ep_list, list) { |
| /* only input in this loop; output is matched in find_midi_ump() */ |
| if (ep->direction != STR_IN) |
| continue; |
| for (blk = 0; blk < ep->ms_ep->bNumGrpTrmBlock; blk++) { |
| id = ep->ms_ep->baAssoGrpTrmBlkID[blk]; |
| err = find_matching_ep_partner(umidi, ep, id); |
| if (err < 0) |
| return err; |
| } |
| } |
| |
| /* |
| * For the remaining EPs, treat as singles, create a UMP object with |
| * unidirectional EP |
| */ |
| list_for_each_entry(ep, &umidi->ep_list, list) { |
| if (ep->rmidi) |
| continue; /* already paired */ |
| for (blk = 0; blk < ep->ms_ep->bNumGrpTrmBlock; blk++) { |
| id = ep->ms_ep->baAssoGrpTrmBlkID[blk]; |
| if (find_midi2_ump(umidi, id)) |
| continue; |
| usb_audio_dbg(umidi->chip, |
| "Creating a unidirection UMP for EP=0x%02x, blk=%d\n", |
| ep->endpoint, id); |
| if (ep->direction == STR_IN) |
| err = create_midi2_ump(umidi, ep, NULL, id); |
| else |
| err = create_midi2_ump(umidi, NULL, ep, id); |
| if (err < 0) |
| return err; |
| break; |
| } |
| } |
| |
| return 0; |
| } |
| |
| /* is the given interface for MIDI 2.0? */ |
| static bool is_midi2_altset(struct usb_host_interface *hostif) |
| { |
| struct usb_ms_header_descriptor *ms_header = |
| (struct usb_ms_header_descriptor *)hostif->extra; |
| |
| if (hostif->extralen < 7 || |
| ms_header->bLength < 7 || |
| ms_header->bDescriptorType != USB_DT_CS_INTERFACE || |
| ms_header->bDescriptorSubtype != UAC_HEADER) |
| return false; |
| |
| return le16_to_cpu(ms_header->bcdMSC) == USB_MS_REV_MIDI_2_0; |
| } |
| |
| /* change the altsetting */ |
| static int set_altset(struct snd_usb_midi2_interface *umidi) |
| { |
| usb_audio_dbg(umidi->chip, "Setting host iface %d:%d\n", |
| umidi->hostif->desc.bInterfaceNumber, |
| umidi->hostif->desc.bAlternateSetting); |
| return usb_set_interface(umidi->chip->dev, |
| umidi->hostif->desc.bInterfaceNumber, |
| umidi->hostif->desc.bAlternateSetting); |
| } |
| |
| /* fill UMP Endpoint name string from USB descriptor */ |
| static void fill_ump_ep_name(struct snd_ump_endpoint *ump, |
| struct usb_device *dev, int id) |
| { |
| int len; |
| |
| usb_string(dev, id, ump->info.name, sizeof(ump->info.name)); |
| |
| /* trim superfluous "MIDI" suffix */ |
| len = strlen(ump->info.name); |
| if (len > 5 && !strcmp(ump->info.name + len - 5, " MIDI")) |
| ump->info.name[len - 5] = 0; |
| } |
| |
| /* fill the fallback name string for each rawmidi instance */ |
| static void set_fallback_rawmidi_names(struct snd_usb_midi2_interface *umidi) |
| { |
| struct usb_device *dev = umidi->chip->dev; |
| struct snd_usb_midi2_ump *rmidi; |
| struct snd_ump_endpoint *ump; |
| |
| list_for_each_entry(rmidi, &umidi->rawmidi_list, list) { |
| ump = rmidi->ump; |
| /* fill UMP EP name from USB descriptors */ |
| if (!*ump->info.name && umidi->hostif->desc.iInterface) |
| fill_ump_ep_name(ump, dev, umidi->hostif->desc.iInterface); |
| else if (!*ump->info.name && dev->descriptor.iProduct) |
| fill_ump_ep_name(ump, dev, dev->descriptor.iProduct); |
| /* fill fallback name */ |
| if (!*ump->info.name) |
| sprintf(ump->info.name, "USB MIDI %d", rmidi->index); |
| /* copy as rawmidi name if not set */ |
| if (!*ump->core.name) |
| strscpy(ump->core.name, ump->info.name, |
| sizeof(ump->core.name)); |
| /* use serial number string as unique UMP product id */ |
| if (!*ump->info.product_id && dev->descriptor.iSerialNumber) |
| usb_string(dev, dev->descriptor.iSerialNumber, |
| ump->info.product_id, |
| sizeof(ump->info.product_id)); |
| } |
| } |
| |
| /* create MIDI interface; fallback to MIDI 1.0 if needed */ |
| int snd_usb_midi_v2_create(struct snd_usb_audio *chip, |
| struct usb_interface *iface, |
| const struct snd_usb_audio_quirk *quirk, |
| unsigned int usb_id) |
| { |
| struct snd_usb_midi2_interface *umidi; |
| struct usb_host_interface *hostif; |
| int err; |
| |
| usb_audio_dbg(chip, "Parsing interface %d...\n", |
| iface->altsetting[0].desc.bInterfaceNumber); |
| |
| /* fallback to MIDI 1.0? */ |
| if (!midi2_enable) { |
| usb_audio_info(chip, "Falling back to MIDI 1.0 by module option\n"); |
| goto fallback_to_midi1; |
| } |
| if ((quirk && quirk->type != QUIRK_MIDI_STANDARD_INTERFACE) || |
| iface->num_altsetting < 2) { |
| usb_audio_info(chip, "Quirk or no altset; falling back to MIDI 1.0\n"); |
| goto fallback_to_midi1; |
| } |
| hostif = &iface->altsetting[1]; |
| if (!is_midi2_altset(hostif)) { |
| usb_audio_info(chip, "No MIDI 2.0 at altset 1, falling back to MIDI 1.0\n"); |
| goto fallback_to_midi1; |
| } |
| if (!hostif->desc.bNumEndpoints) { |
| usb_audio_info(chip, "No endpoint at altset 1, falling back to MIDI 1.0\n"); |
| goto fallback_to_midi1; |
| } |
| |
| usb_audio_dbg(chip, "Creating a MIDI 2.0 instance for %d:%d\n", |
| hostif->desc.bInterfaceNumber, |
| hostif->desc.bAlternateSetting); |
| |
| umidi = kzalloc(sizeof(*umidi), GFP_KERNEL); |
| if (!umidi) |
| return -ENOMEM; |
| umidi->chip = chip; |
| umidi->iface = iface; |
| umidi->hostif = hostif; |
| INIT_LIST_HEAD(&umidi->rawmidi_list); |
| INIT_LIST_HEAD(&umidi->ep_list); |
| |
| list_add_tail(&umidi->list, &chip->midi_v2_list); |
| |
| err = set_altset(umidi); |
| if (err < 0) { |
| usb_audio_err(chip, "Failed to set altset\n"); |
| goto error; |
| } |
| |
| /* assume only altset 1 corresponding to MIDI 2.0 interface */ |
| err = parse_midi_2_0(umidi); |
| if (err < 0) { |
| usb_audio_err(chip, "Failed to parse MIDI 2.0 interface\n"); |
| goto error; |
| } |
| |
| /* parse USB group terminal blocks */ |
| err = parse_group_terminal_blocks(umidi); |
| if (err < 0) { |
| usb_audio_err(chip, "Failed to parse GTB\n"); |
| goto error; |
| } |
| |
| err = start_input_streams(umidi); |
| if (err < 0) { |
| usb_audio_err(chip, "Failed to start input streams\n"); |
| goto error; |
| } |
| |
| if (midi2_ump_probe) { |
| err = parse_ump_endpoints(umidi); |
| if (err < 0) { |
| usb_audio_err(chip, "Failed to parse UMP endpoint\n"); |
| goto error; |
| } |
| } |
| |
| err = create_blocks_from_gtb(umidi); |
| if (err < 0) { |
| usb_audio_err(chip, "Failed to create GTB blocks\n"); |
| goto error; |
| } |
| |
| set_fallback_rawmidi_names(umidi); |
| |
| err = attach_legacy_rawmidi(umidi); |
| if (err < 0) { |
| usb_audio_err(chip, "Failed to create legacy rawmidi\n"); |
| goto error; |
| } |
| |
| return 0; |
| |
| error: |
| snd_usb_midi_v2_free(umidi); |
| return err; |
| |
| fallback_to_midi1: |
| return __snd_usbmidi_create(chip->card, iface, &chip->midi_list, |
| quirk, usb_id, &chip->num_rawmidis); |
| } |
| |
| static void suspend_midi2_endpoint(struct snd_usb_midi2_endpoint *ep) |
| { |
| kill_midi_urbs(ep, true); |
| drain_urb_queue(ep); |
| } |
| |
| void snd_usb_midi_v2_suspend_all(struct snd_usb_audio *chip) |
| { |
| struct snd_usb_midi2_interface *umidi; |
| struct snd_usb_midi2_endpoint *ep; |
| |
| list_for_each_entry(umidi, &chip->midi_v2_list, list) { |
| list_for_each_entry(ep, &umidi->ep_list, list) |
| suspend_midi2_endpoint(ep); |
| } |
| } |
| |
| static void resume_midi2_endpoint(struct snd_usb_midi2_endpoint *ep) |
| { |
| ep->running = ep->suspended; |
| if (ep->direction == STR_IN) |
| submit_io_urbs(ep); |
| /* FIXME: does it all? */ |
| } |
| |
| void snd_usb_midi_v2_resume_all(struct snd_usb_audio *chip) |
| { |
| struct snd_usb_midi2_interface *umidi; |
| struct snd_usb_midi2_endpoint *ep; |
| |
| list_for_each_entry(umidi, &chip->midi_v2_list, list) { |
| set_altset(umidi); |
| list_for_each_entry(ep, &umidi->ep_list, list) |
| resume_midi2_endpoint(ep); |
| } |
| } |
| |
| void snd_usb_midi_v2_disconnect_all(struct snd_usb_audio *chip) |
| { |
| struct snd_usb_midi2_interface *umidi; |
| struct snd_usb_midi2_endpoint *ep; |
| |
| list_for_each_entry(umidi, &chip->midi_v2_list, list) { |
| umidi->disconnected = 1; |
| list_for_each_entry(ep, &umidi->ep_list, list) { |
| ep->disconnected = 1; |
| kill_midi_urbs(ep, false); |
| drain_urb_queue(ep); |
| } |
| } |
| } |
| |
| /* release the MIDI instance */ |
| void snd_usb_midi_v2_free_all(struct snd_usb_audio *chip) |
| { |
| struct snd_usb_midi2_interface *umidi, *next; |
| |
| list_for_each_entry_safe(umidi, next, &chip->midi_v2_list, list) |
| snd_usb_midi_v2_free(umidi); |
| } |