// SPDX-License-Identifier: GPL-2.0-or-later /* ALSA sequencer binding for UMP device */ #include #include #include #include #include #include #include #include #include #include #include #include "seq_clientmgr.h" #include "seq_system.h" struct seq_ump_client; struct seq_ump_group; enum { STR_IN = SNDRV_RAWMIDI_STREAM_INPUT, STR_OUT = SNDRV_RAWMIDI_STREAM_OUTPUT }; /* object per UMP group; corresponding to a sequencer port */ struct seq_ump_group { int group; /* group index (0-based) */ unsigned int dir_bits; /* directions */ bool active; /* activeness */ char name[64]; /* seq port name */ }; /* context for UMP input parsing, per EP */ struct seq_ump_input_buffer { unsigned char len; /* total length in words */ unsigned char pending; /* pending words */ unsigned char type; /* parsed UMP packet type */ unsigned char group; /* parsed UMP packet group */ u32 buf[4]; /* incoming UMP packet */ }; /* sequencer client, per UMP EP (rawmidi) */ struct seq_ump_client { struct snd_ump_endpoint *ump; /* assigned endpoint */ int seq_client; /* sequencer client id */ int opened[2]; /* current opens for each direction */ struct snd_rawmidi_file out_rfile; /* rawmidi for output */ struct seq_ump_input_buffer input; /* input parser context */ struct seq_ump_group groups[SNDRV_UMP_MAX_GROUPS]; /* table of groups */ void *ump_info[SNDRV_UMP_MAX_BLOCKS + 1]; /* shadow of seq client ump_info */ struct work_struct group_notify_work; /* FB change notification */ }; /* number of 32bit words for each UMP 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 }; /* conversion between UMP group and seq port; * assume the port number is equal with UMP group number (1-based) */ static unsigned char ump_group_to_seq_port(unsigned char group) { return group + 1; } /* process the incoming rawmidi stream */ static void seq_ump_input_receive(struct snd_ump_endpoint *ump, const u32 *val, int words) { struct seq_ump_client *client = ump->seq_client; struct snd_seq_ump_event ev = {}; if (!client->opened[STR_IN]) return; if (ump_is_groupless_msg(ump_message_type(*val))) ev.source.port = 0; /* UMP EP port */ else ev.source.port = ump_group_to_seq_port(ump_message_group(*val)); ev.dest.client = SNDRV_SEQ_ADDRESS_SUBSCRIBERS; ev.flags = SNDRV_SEQ_EVENT_UMP; memcpy(ev.ump, val, words << 2); snd_seq_kernel_client_dispatch(client->seq_client, (struct snd_seq_event *)&ev, true, 0); } /* process an input sequencer event; only deal with UMP types */ static int seq_ump_process_event(struct snd_seq_event *ev, int direct, void *private_data, int atomic, int hop) { struct seq_ump_client *client = private_data; struct snd_rawmidi_substream *substream; struct snd_seq_ump_event *ump_ev; unsigned char type; int len; substream = client->out_rfile.output; if (!substream) return -ENODEV; if (!snd_seq_ev_is_ump(ev)) return 0; /* invalid event, skip */ ump_ev = (struct snd_seq_ump_event *)ev; type = ump_message_type(ump_ev->ump[0]); len = ump_packet_words[type]; if (len > 4) return 0; // invalid - skip snd_rawmidi_kernel_write(substream, ev->data.raw8.d, len << 2); return 0; } /* open the rawmidi */ static int seq_ump_client_open(struct seq_ump_client *client, int dir) { struct snd_ump_endpoint *ump = client->ump; int err = 0; mutex_lock(&ump->open_mutex); if (dir == STR_OUT && !client->opened[dir]) { err = snd_rawmidi_kernel_open(&ump->core, 0, SNDRV_RAWMIDI_LFLG_OUTPUT | SNDRV_RAWMIDI_LFLG_APPEND, &client->out_rfile); if (err < 0) goto unlock; } client->opened[dir]++; unlock: mutex_unlock(&ump->open_mutex); return err; } /* close the rawmidi */ static int seq_ump_client_close(struct seq_ump_client *client, int dir) { struct snd_ump_endpoint *ump = client->ump; mutex_lock(&ump->open_mutex); if (!--client->opened[dir]) if (dir == STR_OUT) snd_rawmidi_kernel_release(&client->out_rfile); mutex_unlock(&ump->open_mutex); return 0; } /* sequencer subscription ops for each client */ static int seq_ump_subscribe(void *pdata, struct snd_seq_port_subscribe *info) { struct seq_ump_client *client = pdata; return seq_ump_client_open(client, STR_IN); } static int seq_ump_unsubscribe(void *pdata, struct snd_seq_port_subscribe *info) { struct seq_ump_client *client = pdata; return seq_ump_client_close(client, STR_IN); } static int seq_ump_use(void *pdata, struct snd_seq_port_subscribe *info) { struct seq_ump_client *client = pdata; return seq_ump_client_open(client, STR_OUT); } static int seq_ump_unuse(void *pdata, struct snd_seq_port_subscribe *info) { struct seq_ump_client *client = pdata; return seq_ump_client_close(client, STR_OUT); } /* fill port_info from the given UMP EP and group info */ static void fill_port_info(struct snd_seq_port_info *port, struct seq_ump_client *client, struct seq_ump_group *group) { unsigned int rawmidi_info = client->ump->core.info_flags; port->addr.client = client->seq_client; port->addr.port = ump_group_to_seq_port(group->group); port->capability = 0; if (rawmidi_info & SNDRV_RAWMIDI_INFO_OUTPUT) port->capability |= SNDRV_SEQ_PORT_CAP_WRITE | SNDRV_SEQ_PORT_CAP_SYNC_WRITE | SNDRV_SEQ_PORT_CAP_SUBS_WRITE; if (rawmidi_info & SNDRV_RAWMIDI_INFO_INPUT) port->capability |= SNDRV_SEQ_PORT_CAP_READ | SNDRV_SEQ_PORT_CAP_SYNC_READ | SNDRV_SEQ_PORT_CAP_SUBS_READ; if (rawmidi_info & SNDRV_RAWMIDI_INFO_DUPLEX) port->capability |= SNDRV_SEQ_PORT_CAP_DUPLEX; if (group->dir_bits & (1 << STR_IN)) port->direction |= SNDRV_SEQ_PORT_DIR_INPUT; if (group->dir_bits & (1 << STR_OUT)) port->direction |= SNDRV_SEQ_PORT_DIR_OUTPUT; port->ump_group = group->group + 1; if (!group->active) port->capability |= SNDRV_SEQ_PORT_CAP_INACTIVE; port->type = SNDRV_SEQ_PORT_TYPE_MIDI_GENERIC | SNDRV_SEQ_PORT_TYPE_MIDI_UMP | SNDRV_SEQ_PORT_TYPE_HARDWARE | SNDRV_SEQ_PORT_TYPE_PORT; port->midi_channels = 16; if (*group->name) snprintf(port->name, sizeof(port->name), "Group %d (%.53s)", group->group + 1, group->name); else sprintf(port->name, "Group %d", group->group + 1); } /* create a new sequencer port per UMP group */ static int seq_ump_group_init(struct seq_ump_client *client, int group_index) { struct seq_ump_group *group = &client->groups[group_index]; struct snd_seq_port_info *port; struct snd_seq_port_callback pcallbacks; int err; port = kzalloc(sizeof(*port), GFP_KERNEL); if (!port) { err = -ENOMEM; goto error; } fill_port_info(port, client, group); port->flags = SNDRV_SEQ_PORT_FLG_GIVEN_PORT; memset(&pcallbacks, 0, sizeof(pcallbacks)); pcallbacks.owner = THIS_MODULE; pcallbacks.private_data = client; pcallbacks.subscribe = seq_ump_subscribe; pcallbacks.unsubscribe = seq_ump_unsubscribe; pcallbacks.use = seq_ump_use; pcallbacks.unuse = seq_ump_unuse; pcallbacks.event_input = seq_ump_process_event; port->kernel = &pcallbacks; err = snd_seq_kernel_client_ctl(client->seq_client, SNDRV_SEQ_IOCTL_CREATE_PORT, port); error: kfree(port); return err; } /* update the sequencer ports; called from notify_fb_change callback */ static void update_port_infos(struct seq_ump_client *client) { struct snd_seq_port_info *old, *new; int i, err; old = kzalloc(sizeof(*old), GFP_KERNEL); new = kzalloc(sizeof(*new), GFP_KERNEL); if (!old || !new) goto error; for (i = 0; i < SNDRV_UMP_MAX_GROUPS; i++) { old->addr.client = client->seq_client; old->addr.port = i; err = snd_seq_kernel_client_ctl(client->seq_client, SNDRV_SEQ_IOCTL_GET_PORT_INFO, old); if (err < 0) goto error; fill_port_info(new, client, &client->groups[i]); if (old->capability == new->capability && !strcmp(old->name, new->name)) continue; err = snd_seq_kernel_client_ctl(client->seq_client, SNDRV_SEQ_IOCTL_SET_PORT_INFO, new); if (err < 0) goto error; /* notify to system port */ snd_seq_system_client_ev_port_change(client->seq_client, i); } error: kfree(new); kfree(old); } /* update dir_bits and active flag for all groups in the client */ static void update_group_attrs(struct seq_ump_client *client) { struct snd_ump_block *fb; struct seq_ump_group *group; int i; for (i = 0; i < SNDRV_UMP_MAX_GROUPS; i++) { group = &client->groups[i]; *group->name = 0; group->dir_bits = 0; group->active = 0; group->group = i; } list_for_each_entry(fb, &client->ump->block_list, list) { if (fb->info.first_group + fb->info.num_groups > SNDRV_UMP_MAX_GROUPS) break; group = &client->groups[fb->info.first_group]; for (i = 0; i < fb->info.num_groups; i++, group++) { if (fb->info.active) group->active = 1; switch (fb->info.direction) { case SNDRV_UMP_DIR_INPUT: group->dir_bits |= (1 << STR_IN); break; case SNDRV_UMP_DIR_OUTPUT: group->dir_bits |= (1 << STR_OUT); break; case SNDRV_UMP_DIR_BIDIRECTION: group->dir_bits |= (1 << STR_OUT) | (1 << STR_IN); 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)); } } } } /* create a UMP Endpoint port */ static int create_ump_endpoint_port(struct seq_ump_client *client) { struct snd_seq_port_info *port; struct snd_seq_port_callback pcallbacks; unsigned int rawmidi_info = client->ump->core.info_flags; int err; port = kzalloc(sizeof(*port), GFP_KERNEL); if (!port) return -ENOMEM; port->addr.client = client->seq_client; port->addr.port = 0; /* fixed */ port->flags = SNDRV_SEQ_PORT_FLG_GIVEN_PORT; port->capability = SNDRV_SEQ_PORT_CAP_UMP_ENDPOINT; if (rawmidi_info & SNDRV_RAWMIDI_INFO_INPUT) { port->capability |= SNDRV_SEQ_PORT_CAP_READ | SNDRV_SEQ_PORT_CAP_SYNC_READ | SNDRV_SEQ_PORT_CAP_SUBS_READ; port->direction |= SNDRV_SEQ_PORT_DIR_INPUT; } if (rawmidi_info & SNDRV_RAWMIDI_INFO_OUTPUT) { port->capability |= SNDRV_SEQ_PORT_CAP_WRITE | SNDRV_SEQ_PORT_CAP_SYNC_WRITE | SNDRV_SEQ_PORT_CAP_SUBS_WRITE; port->direction |= SNDRV_SEQ_PORT_DIR_OUTPUT; } if (rawmidi_info & SNDRV_RAWMIDI_INFO_DUPLEX) port->capability |= SNDRV_SEQ_PORT_CAP_DUPLEX; port->ump_group = 0; /* no associated group, no conversion */ port->type = SNDRV_SEQ_PORT_TYPE_MIDI_UMP | SNDRV_SEQ_PORT_TYPE_HARDWARE | SNDRV_SEQ_PORT_TYPE_PORT; port->midi_channels = 16; strcpy(port->name, "MIDI 2.0"); memset(&pcallbacks, 0, sizeof(pcallbacks)); pcallbacks.owner = THIS_MODULE; pcallbacks.private_data = client; if (rawmidi_info & SNDRV_RAWMIDI_INFO_INPUT) { pcallbacks.subscribe = seq_ump_subscribe; pcallbacks.unsubscribe = seq_ump_unsubscribe; } if (rawmidi_info & SNDRV_RAWMIDI_INFO_OUTPUT) { pcallbacks.use = seq_ump_use; pcallbacks.unuse = seq_ump_unuse; pcallbacks.event_input = seq_ump_process_event; } port->kernel = &pcallbacks; err = snd_seq_kernel_client_ctl(client->seq_client, SNDRV_SEQ_IOCTL_CREATE_PORT, port); kfree(port); return err; } /* release the client resources */ static void seq_ump_client_free(struct seq_ump_client *client) { cancel_work_sync(&client->group_notify_work); if (client->seq_client >= 0) snd_seq_delete_kernel_client(client->seq_client); client->ump->seq_ops = NULL; client->ump->seq_client = NULL; kfree(client); } /* update the MIDI version for the given client */ static void setup_client_midi_version(struct seq_ump_client *client) { struct snd_seq_client *cptr; cptr = snd_seq_kernel_client_get(client->seq_client); if (!cptr) return; if (client->ump->info.protocol & SNDRV_UMP_EP_INFO_PROTO_MIDI2) cptr->midi_version = SNDRV_SEQ_CLIENT_UMP_MIDI_2_0; else cptr->midi_version = SNDRV_SEQ_CLIENT_UMP_MIDI_1_0; snd_seq_kernel_client_put(cptr); } /* set up client's group_filter bitmap */ static void setup_client_group_filter(struct seq_ump_client *client) { struct snd_seq_client *cptr; unsigned int filter; int p; cptr = snd_seq_kernel_client_get(client->seq_client); if (!cptr) return; filter = ~(1U << 0); /* always allow groupless messages */ for (p = 0; p < SNDRV_UMP_MAX_GROUPS; p++) { if (client->groups[p].active) filter &= ~(1U << (p + 1)); } cptr->group_filter = filter; snd_seq_kernel_client_put(cptr); } /* UMP group change notification */ static void handle_group_notify(struct work_struct *work) { struct seq_ump_client *client = container_of(work, struct seq_ump_client, group_notify_work); update_group_attrs(client); update_port_infos(client); setup_client_group_filter(client); } /* UMP FB change notification */ static int seq_ump_notify_fb_change(struct snd_ump_endpoint *ump, struct snd_ump_block *fb) { struct seq_ump_client *client = ump->seq_client; if (!client) return -ENODEV; schedule_work(&client->group_notify_work); return 0; } /* UMP protocol change notification; just update the midi_version field */ static int seq_ump_switch_protocol(struct snd_ump_endpoint *ump) { if (!ump->seq_client) return -ENODEV; setup_client_midi_version(ump->seq_client); return 0; } static const struct snd_seq_ump_ops seq_ump_ops = { .input_receive = seq_ump_input_receive, .notify_fb_change = seq_ump_notify_fb_change, .switch_protocol = seq_ump_switch_protocol, }; /* create a sequencer client and ports for the given UMP endpoint */ static int snd_seq_ump_probe(struct device *_dev) { struct snd_seq_device *dev = to_seq_dev(_dev); struct snd_ump_endpoint *ump = dev->private_data; struct snd_card *card = dev->card; struct seq_ump_client *client; struct snd_ump_block *fb; struct snd_seq_client *cptr; int p, err; client = kzalloc(sizeof(*client), GFP_KERNEL); if (!client) return -ENOMEM; INIT_WORK(&client->group_notify_work, handle_group_notify); client->ump = ump; client->seq_client = snd_seq_create_kernel_client(card, ump->core.device, ump->core.name); if (client->seq_client < 0) { err = client->seq_client; goto error; } client->ump_info[0] = &ump->info; list_for_each_entry(fb, &ump->block_list, list) client->ump_info[fb->info.block_id + 1] = &fb->info; setup_client_midi_version(client); update_group_attrs(client); for (p = 0; p < SNDRV_UMP_MAX_GROUPS; p++) { err = seq_ump_group_init(client, p); if (err < 0) goto error; } setup_client_group_filter(client); err = create_ump_endpoint_port(client); if (err < 0) goto error; cptr = snd_seq_kernel_client_get(client->seq_client); if (!cptr) { err = -EINVAL; goto error; } cptr->ump_info = client->ump_info; snd_seq_kernel_client_put(cptr); ump->seq_client = client; ump->seq_ops = &seq_ump_ops; return 0; error: seq_ump_client_free(client); return err; } /* remove a sequencer client */ static int snd_seq_ump_remove(struct device *_dev) { struct snd_seq_device *dev = to_seq_dev(_dev); struct snd_ump_endpoint *ump = dev->private_data; if (ump->seq_client) seq_ump_client_free(ump->seq_client); return 0; } static struct snd_seq_driver seq_ump_driver = { .driver = { .name = KBUILD_MODNAME, .probe = snd_seq_ump_probe, .remove = snd_seq_ump_remove, }, .id = SNDRV_SEQ_DEV_ID_UMP, .argsize = 0, }; module_snd_seq_driver(seq_ump_driver); MODULE_DESCRIPTION("ALSA sequencer client for UMP rawmidi"); MODULE_LICENSE("GPL");