// SPDX-License-Identifier: GPL-2.0-or-later /* * Universal MIDI Packet (UMP) support */ #include #include #include #include #include #include #include #include #include #define ump_err(ump, fmt, args...) dev_err((ump)->core.dev, fmt, ##args) #define ump_warn(ump, fmt, args...) dev_warn((ump)->core.dev, fmt, ##args) #define ump_info(ump, fmt, args...) dev_info((ump)->core.dev, fmt, ##args) #define ump_dbg(ump, fmt, args...) dev_dbg((ump)->core.dev, fmt, ##args) static int snd_ump_dev_register(struct snd_rawmidi *rmidi); static int snd_ump_dev_unregister(struct snd_rawmidi *rmidi); static long snd_ump_ioctl(struct snd_rawmidi *rmidi, unsigned int cmd, void __user *argp); static void snd_ump_proc_read(struct snd_info_entry *entry, struct snd_info_buffer *buffer); static int snd_ump_rawmidi_open(struct snd_rawmidi_substream *substream); static int snd_ump_rawmidi_close(struct snd_rawmidi_substream *substream); static void snd_ump_rawmidi_trigger(struct snd_rawmidi_substream *substream, int up); static void snd_ump_rawmidi_drain(struct snd_rawmidi_substream *substream); static void ump_handle_stream_msg(struct snd_ump_endpoint *ump, const u32 *buf, int size); #if IS_ENABLED(CONFIG_SND_UMP_LEGACY_RAWMIDI) static int process_legacy_output(struct snd_ump_endpoint *ump, u32 *buffer, int count); static void process_legacy_input(struct snd_ump_endpoint *ump, const u32 *src, int words); static void update_legacy_names(struct snd_ump_endpoint *ump); #else static inline int process_legacy_output(struct snd_ump_endpoint *ump, u32 *buffer, int count) { return 0; } static inline void process_legacy_input(struct snd_ump_endpoint *ump, const u32 *src, int words) { } static inline void update_legacy_names(struct snd_ump_endpoint *ump) { } #endif static const struct snd_rawmidi_global_ops snd_ump_rawmidi_ops = { .dev_register = snd_ump_dev_register, .dev_unregister = snd_ump_dev_unregister, .ioctl = snd_ump_ioctl, .proc_read = snd_ump_proc_read, }; static const struct snd_rawmidi_ops snd_ump_rawmidi_input_ops = { .open = snd_ump_rawmidi_open, .close = snd_ump_rawmidi_close, .trigger = snd_ump_rawmidi_trigger, }; static const struct snd_rawmidi_ops snd_ump_rawmidi_output_ops = { .open = snd_ump_rawmidi_open, .close = snd_ump_rawmidi_close, .trigger = snd_ump_rawmidi_trigger, .drain = snd_ump_rawmidi_drain, }; static void snd_ump_endpoint_free(struct snd_rawmidi *rmidi) { struct snd_ump_endpoint *ump = rawmidi_to_ump(rmidi); struct snd_ump_block *fb; while (!list_empty(&ump->block_list)) { fb = list_first_entry(&ump->block_list, struct snd_ump_block, list); list_del(&fb->list); if (fb->private_free) fb->private_free(fb); kfree(fb); } if (ump->private_free) ump->private_free(ump); #if IS_ENABLED(CONFIG_SND_UMP_LEGACY_RAWMIDI) kfree(ump->out_cvts); #endif } /** * snd_ump_endpoint_new - create a UMP Endpoint object * @card: the card instance * @id: the id string for rawmidi * @device: the device index for rawmidi * @output: 1 for enabling output * @input: 1 for enabling input * @ump_ret: the pointer to store the new UMP instance * * Creates a new UMP Endpoint object. A UMP Endpoint is tied with one rawmidi * instance with one input and/or one output rawmidi stream (either uni- * or bi-directional). A UMP Endpoint may contain one or multiple UMP Blocks * that consist of one or multiple UMP Groups. * * Use snd_rawmidi_set_ops() to set the operators to the new instance. * Unlike snd_rawmidi_new(), this function sets up the info_flags by itself * depending on the given @output and @input. * * The device has SNDRV_RAWMIDI_INFO_UMP flag set and a different device * file ("umpCxDx") than a standard MIDI 1.x device ("midiCxDx") is * created. * * Return: Zero if successful, or a negative error code on failure. */ int snd_ump_endpoint_new(struct snd_card *card, char *id, int device, int output, int input, struct snd_ump_endpoint **ump_ret) { unsigned int info_flags = SNDRV_RAWMIDI_INFO_UMP; struct snd_ump_endpoint *ump; int err; if (input) info_flags |= SNDRV_RAWMIDI_INFO_INPUT; if (output) info_flags |= SNDRV_RAWMIDI_INFO_OUTPUT; if (input && output) info_flags |= SNDRV_RAWMIDI_INFO_DUPLEX; ump = kzalloc(sizeof(*ump), GFP_KERNEL); if (!ump) return -ENOMEM; INIT_LIST_HEAD(&ump->block_list); mutex_init(&ump->open_mutex); init_waitqueue_head(&ump->stream_wait); #if IS_ENABLED(CONFIG_SND_UMP_LEGACY_RAWMIDI) spin_lock_init(&ump->legacy_locks[0]); spin_lock_init(&ump->legacy_locks[1]); #endif err = snd_rawmidi_init(&ump->core, card, id, device, output, input, info_flags); if (err < 0) { snd_rawmidi_free(&ump->core); return err; } ump->info.card = card->number; ump->info.device = device; ump->core.private_free = snd_ump_endpoint_free; ump->core.ops = &snd_ump_rawmidi_ops; if (input) snd_rawmidi_set_ops(&ump->core, SNDRV_RAWMIDI_STREAM_INPUT, &snd_ump_rawmidi_input_ops); if (output) snd_rawmidi_set_ops(&ump->core, SNDRV_RAWMIDI_STREAM_OUTPUT, &snd_ump_rawmidi_output_ops); ump_dbg(ump, "Created a UMP EP #%d (%s)\n", device, id); *ump_ret = ump; return 0; } EXPORT_SYMBOL_GPL(snd_ump_endpoint_new); /* * Device register / unregister hooks; * do nothing, placeholders for avoiding the default rawmidi handling */ #if IS_ENABLED(CONFIG_SND_SEQUENCER) static void snd_ump_dev_seq_free(struct snd_seq_device *device) { struct snd_ump_endpoint *ump = device->private_data; ump->seq_dev = NULL; } #endif static int snd_ump_dev_register(struct snd_rawmidi *rmidi) { #if IS_ENABLED(CONFIG_SND_SEQUENCER) struct snd_ump_endpoint *ump = rawmidi_to_ump(rmidi); int err; err = snd_seq_device_new(ump->core.card, ump->core.device, SNDRV_SEQ_DEV_ID_UMP, 0, &ump->seq_dev); if (err < 0) return err; ump->seq_dev->private_data = ump; ump->seq_dev->private_free = snd_ump_dev_seq_free; snd_device_register(ump->core.card, ump->seq_dev); #endif return 0; } static int snd_ump_dev_unregister(struct snd_rawmidi *rmidi) { return 0; } static struct snd_ump_block * snd_ump_get_block(struct snd_ump_endpoint *ump, unsigned char id) { struct snd_ump_block *fb; list_for_each_entry(fb, &ump->block_list, list) { if (fb->info.block_id == id) return fb; } return NULL; } /* * rawmidi ops for UMP endpoint */ static int snd_ump_rawmidi_open(struct snd_rawmidi_substream *substream) { struct snd_ump_endpoint *ump = rawmidi_to_ump(substream->rmidi); int dir = substream->stream; int err; if (ump->substreams[dir]) return -EBUSY; err = ump->ops->open(ump, dir); if (err < 0) return err; ump->substreams[dir] = substream; return 0; } static int snd_ump_rawmidi_close(struct snd_rawmidi_substream *substream) { struct snd_ump_endpoint *ump = rawmidi_to_ump(substream->rmidi); int dir = substream->stream; ump->substreams[dir] = NULL; ump->ops->close(ump, dir); return 0; } static void snd_ump_rawmidi_trigger(struct snd_rawmidi_substream *substream, int up) { struct snd_ump_endpoint *ump = rawmidi_to_ump(substream->rmidi); int dir = substream->stream; ump->ops->trigger(ump, dir, up); } static void snd_ump_rawmidi_drain(struct snd_rawmidi_substream *substream) { struct snd_ump_endpoint *ump = rawmidi_to_ump(substream->rmidi); if (ump->ops->drain) ump->ops->drain(ump, SNDRV_RAWMIDI_STREAM_OUTPUT); } /* number of 32bit words per message type */ static unsigned char ump_packet_words[0x10] = { 1, 1, 1, 2, 2, 4, 1, 1, 2, 2, 2, 3, 3, 4, 4, 4 }; /** * snd_ump_receive_ump_val - parse the UMP packet data * @ump: UMP endpoint * @val: UMP packet data * * The data is copied onto ump->input_buf[]. * When a full packet is completed, returns the number of words (from 1 to 4). * OTOH, if the packet is incomplete, returns 0. */ int snd_ump_receive_ump_val(struct snd_ump_endpoint *ump, u32 val) { int words; if (!ump->input_pending) ump->input_pending = ump_packet_words[ump_message_type(val)]; ump->input_buf[ump->input_buf_head++] = val; ump->input_pending--; if (!ump->input_pending) { words = ump->input_buf_head; ump->input_buf_head = 0; return words; } return 0; } EXPORT_SYMBOL_GPL(snd_ump_receive_ump_val); /** * snd_ump_receive - transfer UMP packets from the device * @ump: the UMP endpoint * @buffer: the buffer pointer to transfer * @count: byte size to transfer * * Called from the driver to submit the received UMP packets from the device * to user-space. It's essentially a wrapper of rawmidi_receive(). * The data to receive is in CPU-native endianness. */ int snd_ump_receive(struct snd_ump_endpoint *ump, const u32 *buffer, int count) { struct snd_rawmidi_substream *substream; const u32 *p = buffer; int n, words = count >> 2; while (words--) { n = snd_ump_receive_ump_val(ump, *p++); if (!n) continue; ump_handle_stream_msg(ump, ump->input_buf, n); #if IS_ENABLED(CONFIG_SND_SEQUENCER) if (ump->seq_ops) ump->seq_ops->input_receive(ump, ump->input_buf, n); #endif process_legacy_input(ump, ump->input_buf, n); } substream = ump->substreams[SNDRV_RAWMIDI_STREAM_INPUT]; if (!substream) return 0; return snd_rawmidi_receive(substream, (const char *)buffer, count); } EXPORT_SYMBOL_GPL(snd_ump_receive); /** * snd_ump_transmit - transmit UMP packets * @ump: the UMP endpoint * @buffer: the buffer pointer to transfer * @count: byte size to transfer * * Called from the driver to obtain the UMP packets from user-space to the * device. It's essentially a wrapper of rawmidi_transmit(). * The data to transmit is in CPU-native endianness. */ int snd_ump_transmit(struct snd_ump_endpoint *ump, u32 *buffer, int count) { struct snd_rawmidi_substream *substream = ump->substreams[SNDRV_RAWMIDI_STREAM_OUTPUT]; int err; if (!substream) return -ENODEV; err = snd_rawmidi_transmit(substream, (char *)buffer, count); /* received either data or an error? */ if (err) return err; return process_legacy_output(ump, buffer, count); } EXPORT_SYMBOL_GPL(snd_ump_transmit); /** * snd_ump_block_new - Create a UMP block * @ump: UMP object * @blk: block ID number to create * @direction: direction (in/out/bidirection) * @first_group: the first group ID (0-based) * @num_groups: the number of groups in this block * @blk_ret: the pointer to store the resultant block object */ int snd_ump_block_new(struct snd_ump_endpoint *ump, unsigned int blk, unsigned int direction, unsigned int first_group, unsigned int num_groups, struct snd_ump_block **blk_ret) { struct snd_ump_block *fb, *p; if (blk >= SNDRV_UMP_MAX_BLOCKS) return -EINVAL; if (snd_ump_get_block(ump, blk)) return -EBUSY; fb = kzalloc(sizeof(*fb), GFP_KERNEL); if (!fb) return -ENOMEM; fb->ump = ump; fb->info.card = ump->info.card; fb->info.device = ump->info.device; fb->info.block_id = blk; if (blk >= ump->info.num_blocks) ump->info.num_blocks = blk + 1; fb->info.direction = direction; fb->info.active = 1; fb->info.first_group = first_group; fb->info.num_groups = num_groups; /* fill the default name, may be overwritten to a better name */ snprintf(fb->info.name, sizeof(fb->info.name), "Group %u-%u", first_group + 1, first_group + num_groups); /* put the entry in the ordered list */ list_for_each_entry(p, &ump->block_list, list) { if (p->info.block_id > blk) { list_add_tail(&fb->list, &p->list); goto added; } } list_add_tail(&fb->list, &ump->block_list); added: ump_dbg(ump, "Created a UMP Block #%d (%s)\n", blk, fb->info.name); *blk_ret = fb; return 0; } EXPORT_SYMBOL_GPL(snd_ump_block_new); static int snd_ump_ioctl_block(struct snd_ump_endpoint *ump, struct snd_ump_block_info __user *argp) { struct snd_ump_block *fb; unsigned char id; if (get_user(id, &argp->block_id)) return -EFAULT; fb = snd_ump_get_block(ump, id); if (!fb) return -ENOENT; if (copy_to_user(argp, &fb->info, sizeof(fb->info))) return -EFAULT; return 0; } /* * Handle UMP-specific ioctls; called from snd_rawmidi_ioctl() */ static long snd_ump_ioctl(struct snd_rawmidi *rmidi, unsigned int cmd, void __user *argp) { struct snd_ump_endpoint *ump = rawmidi_to_ump(rmidi); switch (cmd) { case SNDRV_UMP_IOCTL_ENDPOINT_INFO: if (copy_to_user(argp, &ump->info, sizeof(ump->info))) return -EFAULT; return 0; case SNDRV_UMP_IOCTL_BLOCK_INFO: return snd_ump_ioctl_block(ump, argp); default: ump_dbg(ump, "rawmidi: unknown command = 0x%x\n", cmd); return -ENOTTY; } } static const char *ump_direction_string(int dir) { switch (dir) { case SNDRV_UMP_DIR_INPUT: return "input"; case SNDRV_UMP_DIR_OUTPUT: return "output"; case SNDRV_UMP_DIR_BIDIRECTION: return "bidirection"; default: return "unknown"; } } static const char *ump_ui_hint_string(int dir) { switch (dir) { case SNDRV_UMP_BLOCK_UI_HINT_RECEIVER: return "receiver"; case SNDRV_UMP_BLOCK_UI_HINT_SENDER: return "sender"; case SNDRV_UMP_BLOCK_UI_HINT_BOTH: return "both"; default: return "unknown"; } } /* Additional proc file output */ static void snd_ump_proc_read(struct snd_info_entry *entry, struct snd_info_buffer *buffer) { struct snd_rawmidi *rmidi = entry->private_data; struct snd_ump_endpoint *ump = rawmidi_to_ump(rmidi); struct snd_ump_block *fb; snd_iprintf(buffer, "EP Name: %s\n", ump->info.name); snd_iprintf(buffer, "EP Product ID: %s\n", ump->info.product_id); snd_iprintf(buffer, "UMP Version: 0x%04x\n", ump->info.version); snd_iprintf(buffer, "Protocol Caps: 0x%08x\n", ump->info.protocol_caps); snd_iprintf(buffer, "Protocol: 0x%08x\n", ump->info.protocol); if (ump->info.version) { snd_iprintf(buffer, "Manufacturer ID: 0x%08x\n", ump->info.manufacturer_id); snd_iprintf(buffer, "Family ID: 0x%04x\n", ump->info.family_id); snd_iprintf(buffer, "Model ID: 0x%04x\n", ump->info.model_id); snd_iprintf(buffer, "SW Revision: 0x%4phN\n", ump->info.sw_revision); } snd_iprintf(buffer, "Static Blocks: %s\n", (ump->info.flags & SNDRV_UMP_EP_INFO_STATIC_BLOCKS) ? "Yes" : "No"); snd_iprintf(buffer, "Num Blocks: %d\n\n", ump->info.num_blocks); list_for_each_entry(fb, &ump->block_list, list) { snd_iprintf(buffer, "Block %d (%s)\n", fb->info.block_id, fb->info.name); snd_iprintf(buffer, " Direction: %s\n", ump_direction_string(fb->info.direction)); snd_iprintf(buffer, " Active: %s\n", fb->info.active ? "Yes" : "No"); snd_iprintf(buffer, " Groups: %d-%d\n", fb->info.first_group + 1, fb->info.first_group + fb->info.num_groups); snd_iprintf(buffer, " Is MIDI1: %s%s\n", (fb->info.flags & SNDRV_UMP_BLOCK_IS_MIDI1) ? "Yes" : "No", (fb->info.flags & SNDRV_UMP_BLOCK_IS_LOWSPEED) ? " (Low Speed)" : ""); if (ump->info.version) { snd_iprintf(buffer, " MIDI-CI Version: %d\n", fb->info.midi_ci_version); snd_iprintf(buffer, " Sysex8 Streams: %d\n", fb->info.sysex8_streams); snd_iprintf(buffer, " UI Hint: %s\n", ump_ui_hint_string(fb->info.ui_hint)); } snd_iprintf(buffer, "\n"); } } /* update dir_bits and active flag for all groups in the client */ void snd_ump_update_group_attrs(struct snd_ump_endpoint *ump) { struct snd_ump_block *fb; struct snd_ump_group *group; int i; for (i = 0; i < SNDRV_UMP_MAX_GROUPS; i++) { group = &ump->groups[i]; *group->name = 0; group->dir_bits = 0; group->active = 0; group->group = i; group->valid = false; group->is_midi1 = false; } list_for_each_entry(fb, &ump->block_list, list) { if (fb->info.first_group + fb->info.num_groups > SNDRV_UMP_MAX_GROUPS) break; group = &ump->groups[fb->info.first_group]; for (i = 0; i < fb->info.num_groups; i++, group++) { group->valid = true; if (fb->info.active) group->active = 1; if (fb->info.flags & SNDRV_UMP_BLOCK_IS_MIDI1) group->is_midi1 = true; switch (fb->info.direction) { case SNDRV_UMP_DIR_INPUT: group->dir_bits |= (1 << SNDRV_RAWMIDI_STREAM_INPUT); break; case SNDRV_UMP_DIR_OUTPUT: group->dir_bits |= (1 << SNDRV_RAWMIDI_STREAM_OUTPUT); break; case SNDRV_UMP_DIR_BIDIRECTION: group->dir_bits |= (1 << SNDRV_RAWMIDI_STREAM_INPUT) | (1 << SNDRV_RAWMIDI_STREAM_OUTPUT); break; } if (!*fb->info.name) continue; if (!*group->name) { /* store the first matching name */ strscpy(group->name, fb->info.name, sizeof(group->name)); } else { /* when overlapping, concat names */ strlcat(group->name, ", ", sizeof(group->name)); strlcat(group->name, fb->info.name, sizeof(group->name)); } } } } EXPORT_SYMBOL_GPL(snd_ump_update_group_attrs); /* * UMP endpoint and function block handling */ /* open / close UMP streams for the internal stream msg communication */ static int ump_request_open(struct snd_ump_endpoint *ump) { return snd_rawmidi_kernel_open(&ump->core, 0, SNDRV_RAWMIDI_LFLG_OUTPUT, &ump->stream_rfile); } static void ump_request_close(struct snd_ump_endpoint *ump) { snd_rawmidi_kernel_release(&ump->stream_rfile); } /* request a command and wait for the given response; * @req1 and @req2 are u32 commands * @reply is the expected UMP stream status */ static int ump_req_msg(struct snd_ump_endpoint *ump, u32 req1, u32 req2, u32 reply) { u32 buf[4]; ump_dbg(ump, "%s: request %08x %08x, wait-for %08x\n", __func__, req1, req2, reply); memset(buf, 0, sizeof(buf)); buf[0] = req1; buf[1] = req2; ump->stream_finished = 0; ump->stream_wait_for = reply; snd_rawmidi_kernel_write(ump->stream_rfile.output, (unsigned char *)&buf, 16); wait_event_timeout(ump->stream_wait, ump->stream_finished, msecs_to_jiffies(500)); if (!READ_ONCE(ump->stream_finished)) { ump_dbg(ump, "%s: request timed out\n", __func__); return -ETIMEDOUT; } ump->stream_finished = 0; ump_dbg(ump, "%s: reply: %08x %08x %08x %08x\n", __func__, buf[0], buf[1], buf[2], buf[3]); return 0; } /* append the received letters via UMP packet to the given string buffer; * return 1 if the full string is received or 0 to continue */ static int ump_append_string(struct snd_ump_endpoint *ump, char *dest, int maxsize, const u32 *buf, int offset) { unsigned char format; int c; format = ump_stream_message_format(buf[0]); if (format == UMP_STREAM_MSG_FORMAT_SINGLE || format == UMP_STREAM_MSG_FORMAT_START) { c = 0; } else { c = strlen(dest); if (c >= maxsize - 1) return 1; } for (; offset < 16; offset++) { dest[c] = buf[offset / 4] >> (3 - (offset % 4)) * 8; if (!dest[c]) break; if (++c >= maxsize - 1) break; } dest[c] = 0; return (format == UMP_STREAM_MSG_FORMAT_SINGLE || format == UMP_STREAM_MSG_FORMAT_END); } /* Choose the default protocol */ static void choose_default_protocol(struct snd_ump_endpoint *ump) { if (ump->info.protocol & SNDRV_UMP_EP_INFO_PROTO_MIDI_MASK) return; if (ump->info.protocol_caps & SNDRV_UMP_EP_INFO_PROTO_MIDI2) ump->info.protocol |= SNDRV_UMP_EP_INFO_PROTO_MIDI2; else ump->info.protocol |= SNDRV_UMP_EP_INFO_PROTO_MIDI1; } /* handle EP info stream message; update the UMP attributes */ static int ump_handle_ep_info_msg(struct snd_ump_endpoint *ump, const union snd_ump_stream_msg *buf) { ump->info.version = (buf->ep_info.ump_version_major << 8) | buf->ep_info.ump_version_minor; ump->info.num_blocks = buf->ep_info.num_function_blocks; if (ump->info.num_blocks > SNDRV_UMP_MAX_BLOCKS) { ump_info(ump, "Invalid function blocks %d, fallback to 1\n", ump->info.num_blocks); ump->info.num_blocks = 1; } if (buf->ep_info.static_function_block) ump->info.flags |= SNDRV_UMP_EP_INFO_STATIC_BLOCKS; ump->info.protocol_caps = (buf->ep_info.protocol << 8) | buf->ep_info.jrts; ump_dbg(ump, "EP info: version=%x, num_blocks=%x, proto_caps=%x\n", ump->info.version, ump->info.num_blocks, ump->info.protocol_caps); ump->info.protocol &= ump->info.protocol_caps; choose_default_protocol(ump); return 1; /* finished */ } /* handle EP device info stream message; update the UMP attributes */ static int ump_handle_device_info_msg(struct snd_ump_endpoint *ump, const union snd_ump_stream_msg *buf) { ump->info.manufacturer_id = buf->device_info.manufacture_id & 0x7f7f7f; ump->info.family_id = (buf->device_info.family_msb << 8) | buf->device_info.family_lsb; ump->info.model_id = (buf->device_info.model_msb << 8) | buf->device_info.model_lsb; ump->info.sw_revision[0] = (buf->device_info.sw_revision >> 24) & 0x7f; ump->info.sw_revision[1] = (buf->device_info.sw_revision >> 16) & 0x7f; ump->info.sw_revision[2] = (buf->device_info.sw_revision >> 8) & 0x7f; ump->info.sw_revision[3] = buf->device_info.sw_revision & 0x7f; ump_dbg(ump, "EP devinfo: manid=%08x, family=%04x, model=%04x, sw=%4phN\n", ump->info.manufacturer_id, ump->info.family_id, ump->info.model_id, ump->info.sw_revision); return 1; /* finished */ } /* handle EP name stream message; update the UMP name string */ static int ump_handle_ep_name_msg(struct snd_ump_endpoint *ump, const union snd_ump_stream_msg *buf) { return ump_append_string(ump, ump->info.name, sizeof(ump->info.name), buf->raw, 2); } /* handle EP product id stream message; update the UMP product_id string */ static int ump_handle_product_id_msg(struct snd_ump_endpoint *ump, const union snd_ump_stream_msg *buf) { return ump_append_string(ump, ump->info.product_id, sizeof(ump->info.product_id), buf->raw, 2); } /* notify the protocol change to sequencer */ static void seq_notify_protocol(struct snd_ump_endpoint *ump) { #if IS_ENABLED(CONFIG_SND_SEQUENCER) if (ump->seq_ops && ump->seq_ops->switch_protocol) ump->seq_ops->switch_protocol(ump); #endif /* CONFIG_SND_SEQUENCER */ } /** * snd_ump_switch_protocol - switch MIDI protocol * @ump: UMP endpoint * @protocol: protocol to switch to * * Returns 1 if the protocol is actually switched, 0 if unchanged */ int snd_ump_switch_protocol(struct snd_ump_endpoint *ump, unsigned int protocol) { unsigned int type; protocol &= ump->info.protocol_caps; if (protocol == ump->info.protocol) return 0; type = protocol & SNDRV_UMP_EP_INFO_PROTO_MIDI_MASK; if (type != SNDRV_UMP_EP_INFO_PROTO_MIDI1 && type != SNDRV_UMP_EP_INFO_PROTO_MIDI2) return 0; ump->info.protocol = protocol; ump_dbg(ump, "New protocol = %x (caps = %x)\n", protocol, ump->info.protocol_caps); seq_notify_protocol(ump); return 1; } EXPORT_SYMBOL_GPL(snd_ump_switch_protocol); /* handle EP stream config message; update the UMP protocol */ static int ump_handle_stream_cfg_msg(struct snd_ump_endpoint *ump, const union snd_ump_stream_msg *buf) { unsigned int protocol = (buf->stream_cfg.protocol << 8) | buf->stream_cfg.jrts; snd_ump_switch_protocol(ump, protocol); return 1; /* finished */ } /* Extract Function Block info from UMP packet */ static void fill_fb_info(struct snd_ump_endpoint *ump, struct snd_ump_block_info *info, const union snd_ump_stream_msg *buf) { info->direction = buf->fb_info.direction; info->ui_hint = buf->fb_info.ui_hint; info->first_group = buf->fb_info.first_group; info->num_groups = buf->fb_info.num_groups; if (buf->fb_info.midi_10 < 2) info->flags = buf->fb_info.midi_10; else info->flags = SNDRV_UMP_BLOCK_IS_MIDI1 | SNDRV_UMP_BLOCK_IS_LOWSPEED; info->active = buf->fb_info.active; info->midi_ci_version = buf->fb_info.midi_ci_version; info->sysex8_streams = buf->fb_info.sysex8_streams; ump_dbg(ump, "FB %d: dir=%d, active=%d, first_gp=%d, num_gp=%d, midici=%d, sysex8=%d, flags=0x%x\n", info->block_id, info->direction, info->active, info->first_group, info->num_groups, info->midi_ci_version, info->sysex8_streams, info->flags); if ((info->flags & SNDRV_UMP_BLOCK_IS_MIDI1) && info->num_groups != 1) { info->num_groups = 1; ump_dbg(ump, "FB %d: corrected groups to 1 for MIDI1\n", info->block_id); } } /* check whether the FB info gets updated by the current message */ static bool is_fb_info_updated(struct snd_ump_endpoint *ump, struct snd_ump_block *fb, const union snd_ump_stream_msg *buf) { char tmpbuf[offsetof(struct snd_ump_block_info, name)]; if (ump->info.flags & SNDRV_UMP_EP_INFO_STATIC_BLOCKS) { ump_info(ump, "Skipping static FB info update (blk#%d)\n", fb->info.block_id); return 0; } memcpy(tmpbuf, &fb->info, sizeof(tmpbuf)); fill_fb_info(ump, (struct snd_ump_block_info *)tmpbuf, buf); return memcmp(&fb->info, tmpbuf, sizeof(tmpbuf)) != 0; } /* notify the FB info/name change to sequencer */ static void seq_notify_fb_change(struct snd_ump_endpoint *ump, struct snd_ump_block *fb) { #if IS_ENABLED(CONFIG_SND_SEQUENCER) if (ump->seq_ops && ump->seq_ops->notify_fb_change) ump->seq_ops->notify_fb_change(ump, fb); #endif } /* handle FB info message; update FB info if the block is present */ static int ump_handle_fb_info_msg(struct snd_ump_endpoint *ump, const union snd_ump_stream_msg *buf) { unsigned char blk; struct snd_ump_block *fb; blk = buf->fb_info.function_block_id; fb = snd_ump_get_block(ump, blk); /* complain only if updated after parsing */ if (!fb && ump->parsed) { ump_info(ump, "Function Block Info Update for non-existing block %d\n", blk); return -ENODEV; } /* When updated after the initial parse, check the FB info update */ if (ump->parsed && !is_fb_info_updated(ump, fb, buf)) return 1; /* no content change */ if (fb) { fill_fb_info(ump, &fb->info, buf); if (ump->parsed) { snd_ump_update_group_attrs(ump); update_legacy_names(ump); seq_notify_fb_change(ump, fb); } } return 1; /* finished */ } /* handle FB name message; update the FB name string */ static int ump_handle_fb_name_msg(struct snd_ump_endpoint *ump, const union snd_ump_stream_msg *buf) { unsigned char blk; struct snd_ump_block *fb; int ret; blk = buf->fb_name.function_block_id; fb = snd_ump_get_block(ump, blk); if (!fb) return -ENODEV; if (ump->parsed && (ump->info.flags & SNDRV_UMP_EP_INFO_STATIC_BLOCKS)) { ump_dbg(ump, "Skipping static FB name update (blk#%d)\n", fb->info.block_id); return 0; } ret = ump_append_string(ump, fb->info.name, sizeof(fb->info.name), buf->raw, 3); /* notify the FB name update to sequencer, too */ if (ret > 0 && ump->parsed) { snd_ump_update_group_attrs(ump); update_legacy_names(ump); seq_notify_fb_change(ump, fb); } return ret; } static int create_block_from_fb_info(struct snd_ump_endpoint *ump, int blk) { struct snd_ump_block *fb; unsigned char direction, first_group, num_groups; const union snd_ump_stream_msg *buf = (const union snd_ump_stream_msg *)ump->input_buf; u32 msg; int err; /* query the FB info once */ msg = ump_stream_compose(UMP_STREAM_MSG_STATUS_FB_DISCOVERY, 0) | (blk << 8) | UMP_STREAM_MSG_REQUEST_FB_INFO; err = ump_req_msg(ump, msg, 0, UMP_STREAM_MSG_STATUS_FB_INFO); if (err < 0) { ump_dbg(ump, "Unable to get FB info for block %d\n", blk); return err; } /* the last input must be the FB info */ if (buf->fb_info.status != UMP_STREAM_MSG_STATUS_FB_INFO) { ump_dbg(ump, "Inconsistent input: 0x%x\n", *buf->raw); return -EINVAL; } direction = buf->fb_info.direction; first_group = buf->fb_info.first_group; num_groups = buf->fb_info.num_groups; err = snd_ump_block_new(ump, blk, direction, first_group, num_groups, &fb); if (err < 0) return err; fill_fb_info(ump, &fb->info, buf); msg = ump_stream_compose(UMP_STREAM_MSG_STATUS_FB_DISCOVERY, 0) | (blk << 8) | UMP_STREAM_MSG_REQUEST_FB_NAME; err = ump_req_msg(ump, msg, 0, UMP_STREAM_MSG_STATUS_FB_NAME); if (err) ump_dbg(ump, "Unable to get UMP FB name string #%d\n", blk); return 0; } /* handle stream messages, called from snd_ump_receive() */ static void ump_handle_stream_msg(struct snd_ump_endpoint *ump, const u32 *buf, int size) { const union snd_ump_stream_msg *msg; unsigned int status; int ret; /* UMP stream message suppressed (for gadget UMP)? */ if (ump->no_process_stream) return; BUILD_BUG_ON(sizeof(*msg) != 16); ump_dbg(ump, "Stream msg: %08x %08x %08x %08x\n", buf[0], buf[1], buf[2], buf[3]); if (size != 4 || ump_message_type(*buf) != UMP_MSG_TYPE_STREAM) return; msg = (const union snd_ump_stream_msg *)buf; status = ump_stream_message_status(*buf); switch (status) { case UMP_STREAM_MSG_STATUS_EP_INFO: ret = ump_handle_ep_info_msg(ump, msg); break; case UMP_STREAM_MSG_STATUS_DEVICE_INFO: ret = ump_handle_device_info_msg(ump, msg); break; case UMP_STREAM_MSG_STATUS_EP_NAME: ret = ump_handle_ep_name_msg(ump, msg); break; case UMP_STREAM_MSG_STATUS_PRODUCT_ID: ret = ump_handle_product_id_msg(ump, msg); break; case UMP_STREAM_MSG_STATUS_STREAM_CFG: ret = ump_handle_stream_cfg_msg(ump, msg); break; case UMP_STREAM_MSG_STATUS_FB_INFO: ret = ump_handle_fb_info_msg(ump, msg); break; case UMP_STREAM_MSG_STATUS_FB_NAME: ret = ump_handle_fb_name_msg(ump, msg); break; default: return; } /* when the message has been processed fully, wake up */ if (ret > 0 && ump->stream_wait_for == status) { WRITE_ONCE(ump->stream_finished, 1); wake_up(&ump->stream_wait); } } /** * snd_ump_parse_endpoint - parse endpoint and create function blocks * @ump: UMP object * * Returns 0 for successful parse, -ENODEV if device doesn't respond * (or the query is unsupported), or other error code for serious errors. */ int snd_ump_parse_endpoint(struct snd_ump_endpoint *ump) { int blk, err; u32 msg; if (!(ump->core.info_flags & SNDRV_RAWMIDI_INFO_DUPLEX)) return -ENODEV; err = ump_request_open(ump); if (err < 0) { ump_dbg(ump, "Unable to open rawmidi device: %d\n", err); return err; } /* Check Endpoint Information */ msg = ump_stream_compose(UMP_STREAM_MSG_STATUS_EP_DISCOVERY, 0) | 0x0101; /* UMP version 1.1 */ err = ump_req_msg(ump, msg, UMP_STREAM_MSG_REQUEST_EP_INFO, UMP_STREAM_MSG_STATUS_EP_INFO); if (err < 0) { ump_dbg(ump, "Unable to get UMP EP info\n"); goto error; } /* Request Endpoint Device Info */ err = ump_req_msg(ump, msg, UMP_STREAM_MSG_REQUEST_DEVICE_INFO, UMP_STREAM_MSG_STATUS_DEVICE_INFO); if (err < 0) ump_dbg(ump, "Unable to get UMP EP device info\n"); /* Request Endpoint Name */ err = ump_req_msg(ump, msg, UMP_STREAM_MSG_REQUEST_EP_NAME, UMP_STREAM_MSG_STATUS_EP_NAME); if (err < 0) ump_dbg(ump, "Unable to get UMP EP name string\n"); /* Request Endpoint Product ID */ err = ump_req_msg(ump, msg, UMP_STREAM_MSG_REQUEST_PRODUCT_ID, UMP_STREAM_MSG_STATUS_PRODUCT_ID); if (err < 0) ump_dbg(ump, "Unable to get UMP EP product ID string\n"); /* Get the current stream configuration */ err = ump_req_msg(ump, msg, UMP_STREAM_MSG_REQUEST_STREAM_CFG, UMP_STREAM_MSG_STATUS_STREAM_CFG); if (err < 0) ump_dbg(ump, "Unable to get UMP EP stream config\n"); /* If no protocol is set by some reason, assume the valid one */ choose_default_protocol(ump); /* Query and create blocks from Function Blocks */ for (blk = 0; blk < ump->info.num_blocks; blk++) { err = create_block_from_fb_info(ump, blk); if (err < 0) continue; } /* initialize group attributions */ snd_ump_update_group_attrs(ump); error: ump->parsed = true; ump_request_close(ump); if (err == -ETIMEDOUT) err = -ENODEV; return err; } EXPORT_SYMBOL_GPL(snd_ump_parse_endpoint); #if IS_ENABLED(CONFIG_SND_UMP_LEGACY_RAWMIDI) /* * Legacy rawmidi support */ static int snd_ump_legacy_open(struct snd_rawmidi_substream *substream) { struct snd_ump_endpoint *ump = substream->rmidi->private_data; int dir = substream->stream; int group = ump->legacy_mapping[substream->number]; int err; guard(mutex)(&ump->open_mutex); if (ump->legacy_substreams[dir][group]) return -EBUSY; if (!ump->groups[group].active) return -ENODEV; if (dir == SNDRV_RAWMIDI_STREAM_OUTPUT) { if (!ump->legacy_out_opens) { err = snd_rawmidi_kernel_open(&ump->core, 0, SNDRV_RAWMIDI_LFLG_OUTPUT | SNDRV_RAWMIDI_LFLG_APPEND, &ump->legacy_out_rfile); if (err < 0) return err; } ump->legacy_out_opens++; snd_ump_convert_reset(&ump->out_cvts[group]); } guard(spinlock_irq)(&ump->legacy_locks[dir]); ump->legacy_substreams[dir][group] = substream; return 0; } static int snd_ump_legacy_close(struct snd_rawmidi_substream *substream) { struct snd_ump_endpoint *ump = substream->rmidi->private_data; int dir = substream->stream; int group = ump->legacy_mapping[substream->number]; guard(mutex)(&ump->open_mutex); scoped_guard(spinlock_irq, &ump->legacy_locks[dir]) ump->legacy_substreams[dir][group] = NULL; if (dir == SNDRV_RAWMIDI_STREAM_OUTPUT) { if (!--ump->legacy_out_opens) snd_rawmidi_kernel_release(&ump->legacy_out_rfile); } return 0; } static void snd_ump_legacy_trigger(struct snd_rawmidi_substream *substream, int up) { struct snd_ump_endpoint *ump = substream->rmidi->private_data; int dir = substream->stream; ump->ops->trigger(ump, dir, up); } static void snd_ump_legacy_drain(struct snd_rawmidi_substream *substream) { struct snd_ump_endpoint *ump = substream->rmidi->private_data; if (ump->ops->drain) ump->ops->drain(ump, SNDRV_RAWMIDI_STREAM_OUTPUT); } static int snd_ump_legacy_dev_register(struct snd_rawmidi *rmidi) { /* dummy, just for avoiding create superfluous seq clients */ return 0; } static const struct snd_rawmidi_ops snd_ump_legacy_input_ops = { .open = snd_ump_legacy_open, .close = snd_ump_legacy_close, .trigger = snd_ump_legacy_trigger, }; static const struct snd_rawmidi_ops snd_ump_legacy_output_ops = { .open = snd_ump_legacy_open, .close = snd_ump_legacy_close, .trigger = snd_ump_legacy_trigger, .drain = snd_ump_legacy_drain, }; static const struct snd_rawmidi_global_ops snd_ump_legacy_ops = { .dev_register = snd_ump_legacy_dev_register, }; static int process_legacy_output(struct snd_ump_endpoint *ump, u32 *buffer, int count) { struct snd_rawmidi_substream *substream; struct ump_cvt_to_ump *ctx; const int dir = SNDRV_RAWMIDI_STREAM_OUTPUT; unsigned int protocol; unsigned char c; int group, size = 0; if (!ump->out_cvts || !ump->legacy_out_opens) return 0; guard(spinlock_irqsave)(&ump->legacy_locks[dir]); for (group = 0; group < SNDRV_UMP_MAX_GROUPS; group++) { substream = ump->legacy_substreams[dir][group]; if (!substream) continue; ctx = &ump->out_cvts[group]; protocol = ump->info.protocol; if ((protocol & SNDRV_UMP_EP_INFO_PROTO_MIDI2) && ump->groups[group].is_midi1) protocol = SNDRV_UMP_EP_INFO_PROTO_MIDI1; while (!ctx->ump_bytes && snd_rawmidi_transmit(substream, &c, 1) > 0) snd_ump_convert_to_ump(ctx, group, protocol, c); if (ctx->ump_bytes && ctx->ump_bytes <= count) { size = ctx->ump_bytes; memcpy(buffer, ctx->ump, size); ctx->ump_bytes = 0; break; } } return size; } static void process_legacy_input(struct snd_ump_endpoint *ump, const u32 *src, int words) { struct snd_rawmidi_substream *substream; unsigned char buf[16]; unsigned char group; const int dir = SNDRV_RAWMIDI_STREAM_INPUT; int size; size = snd_ump_convert_from_ump(src, buf, &group); if (size <= 0) return; guard(spinlock_irqsave)(&ump->legacy_locks[dir]); substream = ump->legacy_substreams[dir][group]; if (substream) snd_rawmidi_receive(substream, buf, size); } /* Fill ump->legacy_mapping[] for groups to be used for legacy rawmidi */ static int fill_legacy_mapping(struct snd_ump_endpoint *ump) { struct snd_ump_block *fb; unsigned int group_maps = 0; int i, num; if (ump->info.flags & SNDRV_UMP_EP_INFO_STATIC_BLOCKS) { list_for_each_entry(fb, &ump->block_list, list) { for (i = 0; i < fb->info.num_groups; i++) group_maps |= 1U << (fb->info.first_group + i); } if (!group_maps) ump_info(ump, "No UMP Group is found in FB\n"); } /* use all groups for non-static case */ if (!group_maps) group_maps = (1U << SNDRV_UMP_MAX_GROUPS) - 1; num = 0; for (i = 0; i < SNDRV_UMP_MAX_GROUPS; i++) if ((group_maps & (1U << i)) && ump->groups[i].valid) ump->legacy_mapping[num++] = i; return num; } static void fill_substream_names(struct snd_ump_endpoint *ump, struct snd_rawmidi *rmidi, int dir) { struct snd_rawmidi_substream *s; const char *name; int idx; list_for_each_entry(s, &rmidi->streams[dir].substreams, list) { idx = ump->legacy_mapping[s->number]; name = ump->groups[idx].name; if (!*name) name = ump->info.name; scnprintf(s->name, sizeof(s->name), "Group %d (%.16s)%s", idx + 1, name, ump->groups[idx].active ? "" : " [Inactive]"); } } static void update_legacy_names(struct snd_ump_endpoint *ump) { struct snd_rawmidi *rmidi = ump->legacy_rmidi; fill_substream_names(ump, rmidi, SNDRV_RAWMIDI_STREAM_INPUT); fill_substream_names(ump, rmidi, SNDRV_RAWMIDI_STREAM_OUTPUT); } int snd_ump_attach_legacy_rawmidi(struct snd_ump_endpoint *ump, char *id, int device) { struct snd_rawmidi *rmidi; bool input, output; int err, num; ump->out_cvts = kcalloc(SNDRV_UMP_MAX_GROUPS, sizeof(*ump->out_cvts), GFP_KERNEL); if (!ump->out_cvts) return -ENOMEM; num = fill_legacy_mapping(ump); input = ump->core.info_flags & SNDRV_RAWMIDI_INFO_INPUT; output = ump->core.info_flags & SNDRV_RAWMIDI_INFO_OUTPUT; err = snd_rawmidi_new(ump->core.card, id, device, output ? num : 0, input ? num : 0, &rmidi); if (err < 0) { kfree(ump->out_cvts); return err; } if (input) snd_rawmidi_set_ops(rmidi, SNDRV_RAWMIDI_STREAM_INPUT, &snd_ump_legacy_input_ops); if (output) snd_rawmidi_set_ops(rmidi, SNDRV_RAWMIDI_STREAM_OUTPUT, &snd_ump_legacy_output_ops); snprintf(rmidi->name, sizeof(rmidi->name), "%.68s (MIDI 1.0)", ump->info.name); rmidi->info_flags = ump->core.info_flags & ~SNDRV_RAWMIDI_INFO_UMP; rmidi->ops = &snd_ump_legacy_ops; rmidi->private_data = ump; ump->legacy_rmidi = rmidi; update_legacy_names(ump); ump_dbg(ump, "Created a legacy rawmidi #%d (%s)\n", device, id); return 0; } EXPORT_SYMBOL_GPL(snd_ump_attach_legacy_rawmidi); #endif /* CONFIG_SND_UMP_LEGACY_RAWMIDI */ MODULE_DESCRIPTION("Universal MIDI Packet (UMP) Core Driver"); MODULE_LICENSE("GPL");