diff options
Diffstat (limited to 'sound/core/seq/seq_ports.c')
-rw-r--r-- | sound/core/seq/seq_ports.c | 711 |
1 files changed, 711 insertions, 0 deletions
diff --git a/sound/core/seq/seq_ports.c b/sound/core/seq/seq_ports.c new file mode 100644 index 000000000..25fcf5a2c --- /dev/null +++ b/sound/core/seq/seq_ports.c @@ -0,0 +1,711 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * ALSA sequencer Ports + * Copyright (c) 1998 by Frank van de Pol <fvdpol@coil.demon.nl> + * Jaroslav Kysela <perex@perex.cz> + */ + +#include <sound/core.h> +#include <linux/slab.h> +#include <linux/module.h> +#include "seq_system.h" +#include "seq_ports.h" +#include "seq_clientmgr.h" + +/* + + registration of client ports + + */ + + +/* + +NOTE: the current implementation of the port structure as a linked list is +not optimal for clients that have many ports. For sending messages to all +subscribers of a port we first need to find the address of the port +structure, which means we have to traverse the list. A direct access table +(array) would be better, but big preallocated arrays waste memory. + +Possible actions: + +1) leave it this way, a client does normaly does not have more than a few +ports + +2) replace the linked list of ports by a array of pointers which is +dynamicly kmalloced. When a port is added or deleted we can simply allocate +a new array, copy the corresponding pointers, and delete the old one. We +then only need a pointer to this array, and an integer that tells us how +much elements are in array. + +*/ + +/* return pointer to port structure - port is locked if found */ +struct snd_seq_client_port *snd_seq_port_use_ptr(struct snd_seq_client *client, + int num) +{ + struct snd_seq_client_port *port; + + if (client == NULL) + return NULL; + read_lock(&client->ports_lock); + list_for_each_entry(port, &client->ports_list_head, list) { + if (port->addr.port == num) { + if (port->closing) + break; /* deleting now */ + snd_use_lock_use(&port->use_lock); + read_unlock(&client->ports_lock); + return port; + } + } + read_unlock(&client->ports_lock); + return NULL; /* not found */ +} + + +/* search for the next port - port is locked if found */ +struct snd_seq_client_port *snd_seq_port_query_nearest(struct snd_seq_client *client, + struct snd_seq_port_info *pinfo) +{ + int num; + struct snd_seq_client_port *port, *found; + + num = pinfo->addr.port; + found = NULL; + read_lock(&client->ports_lock); + list_for_each_entry(port, &client->ports_list_head, list) { + if (port->addr.port < num) + continue; + if (port->addr.port == num) { + found = port; + break; + } + if (found == NULL || port->addr.port < found->addr.port) + found = port; + } + if (found) { + if (found->closing) + found = NULL; + else + snd_use_lock_use(&found->use_lock); + } + read_unlock(&client->ports_lock); + return found; +} + + +/* initialize snd_seq_port_subs_info */ +static void port_subs_info_init(struct snd_seq_port_subs_info *grp) +{ + INIT_LIST_HEAD(&grp->list_head); + grp->count = 0; + grp->exclusive = 0; + rwlock_init(&grp->list_lock); + init_rwsem(&grp->list_mutex); + grp->open = NULL; + grp->close = NULL; +} + + +/* create a port, port number is returned (-1 on failure); + * the caller needs to unref the port via snd_seq_port_unlock() appropriately + */ +struct snd_seq_client_port *snd_seq_create_port(struct snd_seq_client *client, + int port) +{ + struct snd_seq_client_port *new_port, *p; + int num = -1; + + /* sanity check */ + if (snd_BUG_ON(!client)) + return NULL; + + if (client->num_ports >= SNDRV_SEQ_MAX_PORTS) { + pr_warn("ALSA: seq: too many ports for client %d\n", client->number); + return NULL; + } + + /* create a new port */ + new_port = kzalloc(sizeof(*new_port), GFP_KERNEL); + if (!new_port) + return NULL; /* failure, out of memory */ + /* init port data */ + new_port->addr.client = client->number; + new_port->addr.port = -1; + new_port->owner = THIS_MODULE; + sprintf(new_port->name, "port-%d", num); + snd_use_lock_init(&new_port->use_lock); + port_subs_info_init(&new_port->c_src); + port_subs_info_init(&new_port->c_dest); + snd_use_lock_use(&new_port->use_lock); + + num = max(port, 0); + mutex_lock(&client->ports_mutex); + write_lock_irq(&client->ports_lock); + list_for_each_entry(p, &client->ports_list_head, list) { + if (p->addr.port > num) + break; + if (port < 0) /* auto-probe mode */ + num = p->addr.port + 1; + } + /* insert the new port */ + list_add_tail(&new_port->list, &p->list); + client->num_ports++; + new_port->addr.port = num; /* store the port number in the port */ + sprintf(new_port->name, "port-%d", num); + write_unlock_irq(&client->ports_lock); + mutex_unlock(&client->ports_mutex); + + return new_port; +} + +/* */ +static int subscribe_port(struct snd_seq_client *client, + struct snd_seq_client_port *port, + struct snd_seq_port_subs_info *grp, + struct snd_seq_port_subscribe *info, int send_ack); +static int unsubscribe_port(struct snd_seq_client *client, + struct snd_seq_client_port *port, + struct snd_seq_port_subs_info *grp, + struct snd_seq_port_subscribe *info, int send_ack); + + +static struct snd_seq_client_port *get_client_port(struct snd_seq_addr *addr, + struct snd_seq_client **cp) +{ + struct snd_seq_client_port *p; + *cp = snd_seq_client_use_ptr(addr->client); + if (*cp) { + p = snd_seq_port_use_ptr(*cp, addr->port); + if (! p) { + snd_seq_client_unlock(*cp); + *cp = NULL; + } + return p; + } + return NULL; +} + +static void delete_and_unsubscribe_port(struct snd_seq_client *client, + struct snd_seq_client_port *port, + struct snd_seq_subscribers *subs, + bool is_src, bool ack); + +static inline struct snd_seq_subscribers * +get_subscriber(struct list_head *p, bool is_src) +{ + if (is_src) + return list_entry(p, struct snd_seq_subscribers, src_list); + else + return list_entry(p, struct snd_seq_subscribers, dest_list); +} + +/* + * remove all subscribers on the list + * this is called from port_delete, for each src and dest list. + */ +static void clear_subscriber_list(struct snd_seq_client *client, + struct snd_seq_client_port *port, + struct snd_seq_port_subs_info *grp, + int is_src) +{ + struct list_head *p, *n; + + list_for_each_safe(p, n, &grp->list_head) { + struct snd_seq_subscribers *subs; + struct snd_seq_client *c; + struct snd_seq_client_port *aport; + + subs = get_subscriber(p, is_src); + if (is_src) + aport = get_client_port(&subs->info.dest, &c); + else + aport = get_client_port(&subs->info.sender, &c); + delete_and_unsubscribe_port(client, port, subs, is_src, false); + + if (!aport) { + /* looks like the connected port is being deleted. + * we decrease the counter, and when both ports are deleted + * remove the subscriber info + */ + if (atomic_dec_and_test(&subs->ref_count)) + kfree(subs); + continue; + } + + /* ok we got the connected port */ + delete_and_unsubscribe_port(c, aport, subs, !is_src, true); + kfree(subs); + snd_seq_port_unlock(aport); + snd_seq_client_unlock(c); + } +} + +/* delete port data */ +static int port_delete(struct snd_seq_client *client, + struct snd_seq_client_port *port) +{ + /* set closing flag and wait for all port access are gone */ + port->closing = 1; + snd_use_lock_sync(&port->use_lock); + + /* clear subscribers info */ + clear_subscriber_list(client, port, &port->c_src, true); + clear_subscriber_list(client, port, &port->c_dest, false); + + if (port->private_free) + port->private_free(port->private_data); + + snd_BUG_ON(port->c_src.count != 0); + snd_BUG_ON(port->c_dest.count != 0); + + kfree(port); + return 0; +} + + +/* delete a port with the given port id */ +int snd_seq_delete_port(struct snd_seq_client *client, int port) +{ + struct snd_seq_client_port *found = NULL, *p; + + mutex_lock(&client->ports_mutex); + write_lock_irq(&client->ports_lock); + list_for_each_entry(p, &client->ports_list_head, list) { + if (p->addr.port == port) { + /* ok found. delete from the list at first */ + list_del(&p->list); + client->num_ports--; + found = p; + break; + } + } + write_unlock_irq(&client->ports_lock); + mutex_unlock(&client->ports_mutex); + if (found) + return port_delete(client, found); + else + return -ENOENT; +} + +/* delete the all ports belonging to the given client */ +int snd_seq_delete_all_ports(struct snd_seq_client *client) +{ + struct list_head deleted_list; + struct snd_seq_client_port *port, *tmp; + + /* move the port list to deleted_list, and + * clear the port list in the client data. + */ + mutex_lock(&client->ports_mutex); + write_lock_irq(&client->ports_lock); + if (! list_empty(&client->ports_list_head)) { + list_add(&deleted_list, &client->ports_list_head); + list_del_init(&client->ports_list_head); + } else { + INIT_LIST_HEAD(&deleted_list); + } + client->num_ports = 0; + write_unlock_irq(&client->ports_lock); + + /* remove each port in deleted_list */ + list_for_each_entry_safe(port, tmp, &deleted_list, list) { + list_del(&port->list); + snd_seq_system_client_ev_port_exit(port->addr.client, port->addr.port); + port_delete(client, port); + } + mutex_unlock(&client->ports_mutex); + return 0; +} + +/* set port info fields */ +int snd_seq_set_port_info(struct snd_seq_client_port * port, + struct snd_seq_port_info * info) +{ + if (snd_BUG_ON(!port || !info)) + return -EINVAL; + + /* set port name */ + if (info->name[0]) + strscpy(port->name, info->name, sizeof(port->name)); + + /* set capabilities */ + port->capability = info->capability; + + /* get port type */ + port->type = info->type; + + /* information about supported channels/voices */ + port->midi_channels = info->midi_channels; + port->midi_voices = info->midi_voices; + port->synth_voices = info->synth_voices; + + /* timestamping */ + port->timestamping = (info->flags & SNDRV_SEQ_PORT_FLG_TIMESTAMP) ? 1 : 0; + port->time_real = (info->flags & SNDRV_SEQ_PORT_FLG_TIME_REAL) ? 1 : 0; + port->time_queue = info->time_queue; + + return 0; +} + +/* get port info fields */ +int snd_seq_get_port_info(struct snd_seq_client_port * port, + struct snd_seq_port_info * info) +{ + if (snd_BUG_ON(!port || !info)) + return -EINVAL; + + /* get port name */ + strscpy(info->name, port->name, sizeof(info->name)); + + /* get capabilities */ + info->capability = port->capability; + + /* get port type */ + info->type = port->type; + + /* information about supported channels/voices */ + info->midi_channels = port->midi_channels; + info->midi_voices = port->midi_voices; + info->synth_voices = port->synth_voices; + + /* get subscriber counts */ + info->read_use = port->c_src.count; + info->write_use = port->c_dest.count; + + /* timestamping */ + info->flags = 0; + if (port->timestamping) { + info->flags |= SNDRV_SEQ_PORT_FLG_TIMESTAMP; + if (port->time_real) + info->flags |= SNDRV_SEQ_PORT_FLG_TIME_REAL; + info->time_queue = port->time_queue; + } + + return 0; +} + + + +/* + * call callback functions (if any): + * the callbacks are invoked only when the first (for connection) or + * the last subscription (for disconnection) is done. Second or later + * subscription results in increment of counter, but no callback is + * invoked. + * This feature is useful if these callbacks are associated with + * initialization or termination of devices (see seq_midi.c). + */ + +static int subscribe_port(struct snd_seq_client *client, + struct snd_seq_client_port *port, + struct snd_seq_port_subs_info *grp, + struct snd_seq_port_subscribe *info, + int send_ack) +{ + int err = 0; + + if (!try_module_get(port->owner)) + return -EFAULT; + grp->count++; + if (grp->open && grp->count == 1) { + err = grp->open(port->private_data, info); + if (err < 0) { + module_put(port->owner); + grp->count--; + } + } + if (err >= 0 && send_ack && client->type == USER_CLIENT) + snd_seq_client_notify_subscription(port->addr.client, port->addr.port, + info, SNDRV_SEQ_EVENT_PORT_SUBSCRIBED); + + return err; +} + +static int unsubscribe_port(struct snd_seq_client *client, + struct snd_seq_client_port *port, + struct snd_seq_port_subs_info *grp, + struct snd_seq_port_subscribe *info, + int send_ack) +{ + int err = 0; + + if (! grp->count) + return -EINVAL; + grp->count--; + if (grp->close && grp->count == 0) + err = grp->close(port->private_data, info); + if (send_ack && client->type == USER_CLIENT) + snd_seq_client_notify_subscription(port->addr.client, port->addr.port, + info, SNDRV_SEQ_EVENT_PORT_UNSUBSCRIBED); + module_put(port->owner); + return err; +} + + + +/* check if both addresses are identical */ +static inline int addr_match(struct snd_seq_addr *r, struct snd_seq_addr *s) +{ + return (r->client == s->client) && (r->port == s->port); +} + +/* check the two subscribe info match */ +/* if flags is zero, checks only sender and destination addresses */ +static int match_subs_info(struct snd_seq_port_subscribe *r, + struct snd_seq_port_subscribe *s) +{ + if (addr_match(&r->sender, &s->sender) && + addr_match(&r->dest, &s->dest)) { + if (r->flags && r->flags == s->flags) + return r->queue == s->queue; + else if (! r->flags) + return 1; + } + return 0; +} + +static int check_and_subscribe_port(struct snd_seq_client *client, + struct snd_seq_client_port *port, + struct snd_seq_subscribers *subs, + bool is_src, bool exclusive, bool ack) +{ + struct snd_seq_port_subs_info *grp; + struct list_head *p; + struct snd_seq_subscribers *s; + int err; + + grp = is_src ? &port->c_src : &port->c_dest; + err = -EBUSY; + down_write(&grp->list_mutex); + if (exclusive) { + if (!list_empty(&grp->list_head)) + goto __error; + } else { + if (grp->exclusive) + goto __error; + /* check whether already exists */ + list_for_each(p, &grp->list_head) { + s = get_subscriber(p, is_src); + if (match_subs_info(&subs->info, &s->info)) + goto __error; + } + } + + err = subscribe_port(client, port, grp, &subs->info, ack); + if (err < 0) { + grp->exclusive = 0; + goto __error; + } + + /* add to list */ + write_lock_irq(&grp->list_lock); + if (is_src) + list_add_tail(&subs->src_list, &grp->list_head); + else + list_add_tail(&subs->dest_list, &grp->list_head); + grp->exclusive = exclusive; + atomic_inc(&subs->ref_count); + write_unlock_irq(&grp->list_lock); + err = 0; + + __error: + up_write(&grp->list_mutex); + return err; +} + +/* called with grp->list_mutex held */ +static void __delete_and_unsubscribe_port(struct snd_seq_client *client, + struct snd_seq_client_port *port, + struct snd_seq_subscribers *subs, + bool is_src, bool ack) +{ + struct snd_seq_port_subs_info *grp; + struct list_head *list; + bool empty; + + grp = is_src ? &port->c_src : &port->c_dest; + list = is_src ? &subs->src_list : &subs->dest_list; + write_lock_irq(&grp->list_lock); + empty = list_empty(list); + if (!empty) + list_del_init(list); + grp->exclusive = 0; + write_unlock_irq(&grp->list_lock); + + if (!empty) + unsubscribe_port(client, port, grp, &subs->info, ack); +} + +static void delete_and_unsubscribe_port(struct snd_seq_client *client, + struct snd_seq_client_port *port, + struct snd_seq_subscribers *subs, + bool is_src, bool ack) +{ + struct snd_seq_port_subs_info *grp; + + grp = is_src ? &port->c_src : &port->c_dest; + down_write(&grp->list_mutex); + __delete_and_unsubscribe_port(client, port, subs, is_src, ack); + up_write(&grp->list_mutex); +} + +/* connect two ports */ +int snd_seq_port_connect(struct snd_seq_client *connector, + struct snd_seq_client *src_client, + struct snd_seq_client_port *src_port, + struct snd_seq_client *dest_client, + struct snd_seq_client_port *dest_port, + struct snd_seq_port_subscribe *info) +{ + struct snd_seq_subscribers *subs; + bool exclusive; + int err; + + subs = kzalloc(sizeof(*subs), GFP_KERNEL); + if (!subs) + return -ENOMEM; + + subs->info = *info; + atomic_set(&subs->ref_count, 0); + INIT_LIST_HEAD(&subs->src_list); + INIT_LIST_HEAD(&subs->dest_list); + + exclusive = !!(info->flags & SNDRV_SEQ_PORT_SUBS_EXCLUSIVE); + + err = check_and_subscribe_port(src_client, src_port, subs, true, + exclusive, + connector->number != src_client->number); + if (err < 0) + goto error; + err = check_and_subscribe_port(dest_client, dest_port, subs, false, + exclusive, + connector->number != dest_client->number); + if (err < 0) + goto error_dest; + + return 0; + + error_dest: + delete_and_unsubscribe_port(src_client, src_port, subs, true, + connector->number != src_client->number); + error: + kfree(subs); + return err; +} + +/* remove the connection */ +int snd_seq_port_disconnect(struct snd_seq_client *connector, + struct snd_seq_client *src_client, + struct snd_seq_client_port *src_port, + struct snd_seq_client *dest_client, + struct snd_seq_client_port *dest_port, + struct snd_seq_port_subscribe *info) +{ + struct snd_seq_port_subs_info *dest = &dest_port->c_dest; + struct snd_seq_subscribers *subs; + int err = -ENOENT; + + /* always start from deleting the dest port for avoiding concurrent + * deletions + */ + down_write(&dest->list_mutex); + /* look for the connection */ + list_for_each_entry(subs, &dest->list_head, dest_list) { + if (match_subs_info(info, &subs->info)) { + __delete_and_unsubscribe_port(dest_client, dest_port, + subs, false, + connector->number != dest_client->number); + err = 0; + break; + } + } + up_write(&dest->list_mutex); + if (err < 0) + return err; + + delete_and_unsubscribe_port(src_client, src_port, subs, true, + connector->number != src_client->number); + kfree(subs); + return 0; +} + + +/* get matched subscriber */ +int snd_seq_port_get_subscription(struct snd_seq_port_subs_info *src_grp, + struct snd_seq_addr *dest_addr, + struct snd_seq_port_subscribe *subs) +{ + struct snd_seq_subscribers *s; + int err = -ENOENT; + + down_read(&src_grp->list_mutex); + list_for_each_entry(s, &src_grp->list_head, src_list) { + if (addr_match(dest_addr, &s->info.dest)) { + *subs = s->info; + err = 0; + break; + } + } + up_read(&src_grp->list_mutex); + return err; +} + +/* + * Attach a device driver that wants to receive events from the + * sequencer. Returns the new port number on success. + * A driver that wants to receive the events converted to midi, will + * use snd_seq_midisynth_register_port(). + */ +/* exported */ +int snd_seq_event_port_attach(int client, + struct snd_seq_port_callback *pcbp, + int cap, int type, int midi_channels, + int midi_voices, char *portname) +{ + struct snd_seq_port_info portinfo; + int ret; + + /* Set up the port */ + memset(&portinfo, 0, sizeof(portinfo)); + portinfo.addr.client = client; + strscpy(portinfo.name, portname ? portname : "Unnamed port", + sizeof(portinfo.name)); + + portinfo.capability = cap; + portinfo.type = type; + portinfo.kernel = pcbp; + portinfo.midi_channels = midi_channels; + portinfo.midi_voices = midi_voices; + + /* Create it */ + ret = snd_seq_kernel_client_ctl(client, + SNDRV_SEQ_IOCTL_CREATE_PORT, + &portinfo); + + if (ret >= 0) + ret = portinfo.addr.port; + + return ret; +} +EXPORT_SYMBOL(snd_seq_event_port_attach); + +/* + * Detach the driver from a port. + */ +/* exported */ +int snd_seq_event_port_detach(int client, int port) +{ + struct snd_seq_port_info portinfo; + int err; + + memset(&portinfo, 0, sizeof(portinfo)); + portinfo.addr.client = client; + portinfo.addr.port = port; + err = snd_seq_kernel_client_ctl(client, + SNDRV_SEQ_IOCTL_DELETE_PORT, + &portinfo); + + return err; +} +EXPORT_SYMBOL(snd_seq_event_port_detach); |