| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Focusrite Control Protocol Driver for ALSA |
| * |
| * Copyright (c) 2024-2025 by Geoffrey D. Bennett <g at b4.vu> |
| */ |
| /* |
| * DOC: Theory of Operation |
| * |
| * The Focusrite Control Protocol (FCP) driver provides a minimal |
| * kernel interface that allows a user-space driver (primarily |
| * fcp-server) to communicate with Focusrite USB audio interfaces |
| * using their vendor-specific protocol. This protocol is used by |
| * Scarlett 2nd Gen, 3rd Gen, 4th Gen, Clarett USB, Clarett+, and |
| * Vocaster series devices. |
| * |
| * Unlike the existing scarlett2 driver which implements all controls |
| * in kernel space, this driver takes a lighter-weight approach by |
| * moving most functionality to user space. The only control |
| * implemented in kernel space is the Level Meter, since it requires |
| * frequent polling of volatile data. |
| * |
| * The driver provides an hwdep interface that allows the user-space |
| * driver to: |
| * - Initialise the protocol |
| * - Send arbitrary FCP commands to the device |
| * - Receive notifications from the device |
| * - Configure the Level Meter control |
| * |
| * Usage Flow |
| * ---------- |
| * 1. Open the hwdep device (requires CAP_SYS_RAWIO) |
| * 2. Get protocol version using FCP_IOCTL_PVERSION |
| * 3. Initialise protocol using FCP_IOCTL_INIT |
| * 4. Send commands using FCP_IOCTL_CMD |
| * 5. Receive notifications using read() |
| * 6. Optionally set up the Level Meter control using |
| * FCP_IOCTL_SET_METER_MAP |
| * 7. Optionally add labels to the Level Meter control using |
| * FCP_IOCTL_SET_METER_LABELS |
| * |
| * Level Meter |
| * ----------- |
| * The Level Meter is implemented as an ALSA control that provides |
| * real-time level monitoring. When the control is read, the driver |
| * requests the current meter levels from the device, translates the |
| * levels using the configured mapping, and returns the result to the |
| * user. The mapping between device meters and the ALSA control's |
| * channels is configured with FCP_IOCTL_SET_METER_MAP. |
| * |
| * Labels for the Level Meter channels can be set using |
| * FCP_IOCTL_SET_METER_LABELS and read by applications through the |
| * control's TLV data. The labels are transferred as a sequence of |
| * null-terminated strings. |
| */ |
| |
| #include <linux/slab.h> |
| #include <linux/usb.h> |
| |
| #include <sound/control.h> |
| #include <sound/hwdep.h> |
| #include <sound/tlv.h> |
| |
| #include <uapi/sound/fcp.h> |
| |
| #include "usbaudio.h" |
| #include "mixer.h" |
| #include "helper.h" |
| |
| #include "fcp.h" |
| |
| /* notify waiting to send to *file */ |
| struct fcp_notify { |
| wait_queue_head_t queue; |
| u32 event; |
| spinlock_t lock; |
| }; |
| |
| struct fcp_data { |
| struct usb_mixer_interface *mixer; |
| |
| struct mutex mutex; /* serialise access to the device */ |
| struct completion cmd_done; /* wait for command completion */ |
| struct file *file; /* hwdep file */ |
| |
| struct fcp_notify notify; |
| |
| u8 bInterfaceNumber; |
| u8 bEndpointAddress; |
| u16 wMaxPacketSize; |
| u8 bInterval; |
| |
| uint16_t step0_resp_size; |
| uint16_t step2_resp_size; |
| uint32_t init1_opcode; |
| uint32_t init2_opcode; |
| |
| u8 init; |
| u16 seq; |
| |
| u8 num_meter_slots; |
| s16 *meter_level_map; |
| __le32 *meter_levels; |
| struct snd_kcontrol *meter_ctl; |
| |
| unsigned int *meter_labels_tlv; |
| int meter_labels_tlv_size; |
| }; |
| |
| /*** USB Interactions ***/ |
| |
| /* FCP Command ACK notification bit */ |
| #define FCP_NOTIFY_ACK 1 |
| |
| /* Vendor-specific USB control requests */ |
| #define FCP_USB_REQ_STEP0 0 |
| #define FCP_USB_REQ_CMD_TX 2 |
| #define FCP_USB_REQ_CMD_RX 3 |
| |
| /* Focusrite Control Protocol opcodes that the kernel side needs to |
| * know about |
| */ |
| #define FCP_USB_REBOOT 0x00000003 |
| #define FCP_USB_GET_METER 0x00001001 |
| #define FCP_USB_FLASH_ERASE 0x00004002 |
| #define FCP_USB_FLASH_WRITE 0x00004004 |
| |
| #define FCP_USB_METER_LEVELS_GET_MAGIC 1 |
| |
| #define FCP_SEGMENT_APP_GOLD 0 |
| |
| /* Forward declarations */ |
| static int fcp_init(struct usb_mixer_interface *mixer, |
| void *step0_resp, void *step2_resp); |
| |
| /* FCP command request/response format */ |
| struct fcp_usb_packet { |
| __le32 opcode; |
| __le16 size; |
| __le16 seq; |
| __le32 error; |
| __le32 pad; |
| u8 data[]; |
| }; |
| |
| static void fcp_fill_request_header(struct fcp_data *private, |
| struct fcp_usb_packet *req, |
| u32 opcode, u16 req_size) |
| { |
| /* sequence must go up by 1 for each request */ |
| u16 seq = private->seq++; |
| |
| req->opcode = cpu_to_le32(opcode); |
| req->size = cpu_to_le16(req_size); |
| req->seq = cpu_to_le16(seq); |
| req->error = 0; |
| req->pad = 0; |
| } |
| |
| static int fcp_usb_tx(struct usb_device *dev, int interface, |
| void *buf, u16 size) |
| { |
| return snd_usb_ctl_msg(dev, usb_sndctrlpipe(dev, 0), |
| FCP_USB_REQ_CMD_TX, |
| USB_RECIP_INTERFACE | USB_TYPE_CLASS | USB_DIR_OUT, |
| 0, interface, buf, size); |
| } |
| |
| static int fcp_usb_rx(struct usb_device *dev, int interface, |
| void *buf, u16 size) |
| { |
| return snd_usb_ctl_msg(dev, usb_rcvctrlpipe(dev, 0), |
| FCP_USB_REQ_CMD_RX, |
| USB_RECIP_INTERFACE | USB_TYPE_CLASS | USB_DIR_IN, |
| 0, interface, buf, size); |
| } |
| |
| /* Send an FCP command and get the response */ |
| static int fcp_usb(struct usb_mixer_interface *mixer, u32 opcode, |
| const void *req_data, u16 req_size, |
| void *resp_data, u16 resp_size) |
| { |
| struct fcp_data *private = mixer->private_data; |
| struct usb_device *dev = mixer->chip->dev; |
| struct fcp_usb_packet *req __free(kfree) = NULL; |
| struct fcp_usb_packet *resp __free(kfree) = NULL; |
| size_t req_buf_size = struct_size(req, data, req_size); |
| size_t resp_buf_size = struct_size(resp, data, resp_size); |
| int retries = 0; |
| const int max_retries = 5; |
| int err; |
| |
| if (!mixer->urb) |
| return -ENODEV; |
| |
| req = kmalloc(req_buf_size, GFP_KERNEL); |
| if (!req) |
| return -ENOMEM; |
| |
| resp = kmalloc(resp_buf_size, GFP_KERNEL); |
| if (!resp) |
| return -ENOMEM; |
| |
| /* build request message */ |
| fcp_fill_request_header(private, req, opcode, req_size); |
| if (req_size) |
| memcpy(req->data, req_data, req_size); |
| |
| /* send the request and retry on EPROTO */ |
| retry: |
| err = fcp_usb_tx(dev, private->bInterfaceNumber, req, req_buf_size); |
| if (err == -EPROTO && ++retries <= max_retries) { |
| msleep(1 << (retries - 1)); |
| goto retry; |
| } |
| |
| if (err != req_buf_size) { |
| usb_audio_err(mixer->chip, |
| "FCP request %08x failed: %d\n", opcode, err); |
| return -EINVAL; |
| } |
| |
| if (!wait_for_completion_timeout(&private->cmd_done, |
| msecs_to_jiffies(1000))) { |
| usb_audio_err(mixer->chip, |
| "FCP request %08x timed out\n", opcode); |
| |
| return -ETIMEDOUT; |
| } |
| |
| /* send a second message to get the response */ |
| err = fcp_usb_rx(dev, private->bInterfaceNumber, resp, resp_buf_size); |
| |
| /* validate the response */ |
| |
| if (err < 0) { |
| |
| /* ESHUTDOWN and EPROTO are valid responses to a |
| * reboot request |
| */ |
| if (opcode == FCP_USB_REBOOT && |
| (err == -ESHUTDOWN || err == -EPROTO)) |
| return 0; |
| |
| usb_audio_err(mixer->chip, |
| "FCP read response %08x failed: %d\n", |
| opcode, err); |
| return -EINVAL; |
| } |
| |
| if (err < sizeof(*resp)) { |
| usb_audio_err(mixer->chip, |
| "FCP response %08x too short: %d\n", |
| opcode, err); |
| return -EINVAL; |
| } |
| |
| if (req->seq != resp->seq) { |
| usb_audio_err(mixer->chip, |
| "FCP response %08x seq mismatch %d/%d\n", |
| opcode, |
| le16_to_cpu(req->seq), le16_to_cpu(resp->seq)); |
| return -EINVAL; |
| } |
| |
| if (req->opcode != resp->opcode) { |
| usb_audio_err(mixer->chip, |
| "FCP response %08x opcode mismatch %08x\n", |
| opcode, le32_to_cpu(resp->opcode)); |
| return -EINVAL; |
| } |
| |
| if (resp->error) { |
| usb_audio_err(mixer->chip, |
| "FCP response %08x error %d\n", |
| opcode, le32_to_cpu(resp->error)); |
| return -EINVAL; |
| } |
| |
| if (err != resp_buf_size) { |
| usb_audio_err(mixer->chip, |
| "FCP response %08x buffer size mismatch %d/%zu\n", |
| opcode, err, resp_buf_size); |
| return -EINVAL; |
| } |
| |
| if (resp_size != le16_to_cpu(resp->size)) { |
| usb_audio_err(mixer->chip, |
| "FCP response %08x size mismatch %d/%d\n", |
| opcode, resp_size, le16_to_cpu(resp->size)); |
| return -EINVAL; |
| } |
| |
| if (resp_data && resp_size > 0) |
| memcpy(resp_data, resp->data, resp_size); |
| |
| return 0; |
| } |
| |
| static int fcp_reinit(struct usb_mixer_interface *mixer) |
| { |
| struct fcp_data *private = mixer->private_data; |
| void *step0_resp __free(kfree) = NULL; |
| void *step2_resp __free(kfree) = NULL; |
| |
| if (mixer->urb) |
| return 0; |
| |
| step0_resp = kmalloc(private->step0_resp_size, GFP_KERNEL); |
| if (!step0_resp) |
| return -ENOMEM; |
| step2_resp = kmalloc(private->step2_resp_size, GFP_KERNEL); |
| if (!step2_resp) |
| return -ENOMEM; |
| |
| return fcp_init(mixer, step0_resp, step2_resp); |
| } |
| |
| /*** Control Functions ***/ |
| |
| /* helper function to create a new control */ |
| static int fcp_add_new_ctl(struct usb_mixer_interface *mixer, |
| const struct snd_kcontrol_new *ncontrol, |
| int index, int channels, const char *name, |
| struct snd_kcontrol **kctl_return) |
| { |
| struct snd_kcontrol *kctl; |
| struct usb_mixer_elem_info *elem; |
| int err; |
| |
| elem = kzalloc(sizeof(*elem), GFP_KERNEL); |
| if (!elem) |
| return -ENOMEM; |
| |
| /* We set USB_MIXER_BESPOKEN type, so that the core USB mixer code |
| * ignores them for resume and other operations. |
| * Also, the head.id field is set to 0, as we don't use this field. |
| */ |
| elem->head.mixer = mixer; |
| elem->control = index; |
| elem->head.id = 0; |
| elem->channels = channels; |
| elem->val_type = USB_MIXER_BESPOKEN; |
| |
| kctl = snd_ctl_new1(ncontrol, elem); |
| if (!kctl) { |
| kfree(elem); |
| return -ENOMEM; |
| } |
| kctl->private_free = snd_usb_mixer_elem_free; |
| |
| strscpy(kctl->id.name, name, sizeof(kctl->id.name)); |
| |
| err = snd_usb_mixer_add_control(&elem->head, kctl); |
| if (err < 0) |
| return err; |
| |
| if (kctl_return) |
| *kctl_return = kctl; |
| |
| return 0; |
| } |
| |
| /*** Level Meter Control ***/ |
| |
| static int fcp_meter_ctl_info(struct snd_kcontrol *kctl, |
| struct snd_ctl_elem_info *uinfo) |
| { |
| struct usb_mixer_elem_info *elem = kctl->private_data; |
| |
| uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; |
| uinfo->count = elem->channels; |
| uinfo->value.integer.min = 0; |
| uinfo->value.integer.max = 4095; |
| uinfo->value.integer.step = 1; |
| return 0; |
| } |
| |
| static int fcp_meter_ctl_get(struct snd_kcontrol *kctl, |
| struct snd_ctl_elem_value *ucontrol) |
| { |
| struct usb_mixer_elem_info *elem = kctl->private_data; |
| struct usb_mixer_interface *mixer = elem->head.mixer; |
| struct fcp_data *private = mixer->private_data; |
| int num_meter_slots, resp_size; |
| __le32 *resp = private->meter_levels; |
| int i, err = 0; |
| |
| struct { |
| __le16 pad; |
| __le16 num_meters; |
| __le32 magic; |
| } __packed req; |
| |
| guard(mutex)(&private->mutex); |
| |
| err = fcp_reinit(mixer); |
| if (err < 0) |
| return err; |
| |
| num_meter_slots = private->num_meter_slots; |
| resp_size = num_meter_slots * sizeof(u32); |
| |
| req.pad = 0; |
| req.num_meters = cpu_to_le16(num_meter_slots); |
| req.magic = cpu_to_le32(FCP_USB_METER_LEVELS_GET_MAGIC); |
| err = fcp_usb(mixer, FCP_USB_GET_METER, |
| &req, sizeof(req), resp, resp_size); |
| if (err < 0) |
| return err; |
| |
| /* copy & translate from resp[] using meter_level_map[] */ |
| for (i = 0; i < elem->channels; i++) { |
| int idx = private->meter_level_map[i]; |
| int value = idx < 0 ? 0 : le32_to_cpu(resp[idx]); |
| |
| ucontrol->value.integer.value[i] = value; |
| } |
| |
| return 0; |
| } |
| |
| static int fcp_meter_tlv_callback(struct snd_kcontrol *kctl, |
| int op_flag, unsigned int size, |
| unsigned int __user *tlv) |
| { |
| struct usb_mixer_elem_info *elem = kctl->private_data; |
| struct usb_mixer_interface *mixer = elem->head.mixer; |
| struct fcp_data *private = mixer->private_data; |
| |
| guard(mutex)(&private->mutex); |
| |
| if (op_flag == SNDRV_CTL_TLV_OP_READ) { |
| if (private->meter_labels_tlv_size == 0) |
| return 0; |
| |
| if (size > private->meter_labels_tlv_size) |
| size = private->meter_labels_tlv_size; |
| |
| if (copy_to_user(tlv, private->meter_labels_tlv, size)) |
| return -EFAULT; |
| |
| return size; |
| } |
| |
| return -EINVAL; |
| } |
| |
| static const struct snd_kcontrol_new fcp_meter_ctl = { |
| .iface = SNDRV_CTL_ELEM_IFACE_PCM, |
| .access = SNDRV_CTL_ELEM_ACCESS_READ | |
| SNDRV_CTL_ELEM_ACCESS_VOLATILE, |
| .info = fcp_meter_ctl_info, |
| .get = fcp_meter_ctl_get, |
| .tlv = { .c = fcp_meter_tlv_callback }, |
| }; |
| |
| /*** hwdep interface ***/ |
| |
| /* FCP initialisation */ |
| static int fcp_ioctl_init(struct usb_mixer_interface *mixer, |
| struct fcp_init __user *arg) |
| { |
| struct fcp_init init; |
| struct usb_device *dev = mixer->chip->dev; |
| struct fcp_data *private = mixer->private_data; |
| void *resp __free(kfree) = NULL; |
| void *step2_resp; |
| int err, buf_size; |
| |
| if (usb_pipe_type_check(dev, usb_sndctrlpipe(dev, 0))) |
| return -EINVAL; |
| |
| /* Get initialisation parameters */ |
| if (copy_from_user(&init, arg, sizeof(init))) |
| return -EFAULT; |
| |
| /* Validate the response sizes */ |
| if (init.step0_resp_size < 1 || |
| init.step0_resp_size > 255 || |
| init.step2_resp_size < 1 || |
| init.step2_resp_size > 255) |
| return -EINVAL; |
| |
| /* Allocate response buffer */ |
| buf_size = init.step0_resp_size + init.step2_resp_size; |
| |
| resp = kmalloc(buf_size, GFP_KERNEL); |
| if (!resp) |
| return -ENOMEM; |
| |
| private->step0_resp_size = init.step0_resp_size; |
| private->step2_resp_size = init.step2_resp_size; |
| private->init1_opcode = init.init1_opcode; |
| private->init2_opcode = init.init2_opcode; |
| |
| step2_resp = resp + private->step0_resp_size; |
| |
| err = fcp_init(mixer, resp, step2_resp); |
| if (err < 0) |
| return err; |
| |
| if (copy_to_user(arg->resp, resp, buf_size)) |
| return -EFAULT; |
| |
| return 0; |
| } |
| |
| /* Check that the command is allowed |
| * Don't permit erasing/writing segment 0 (App_Gold) |
| */ |
| static int fcp_validate_cmd(u32 opcode, void *data, u16 size) |
| { |
| if (opcode == FCP_USB_FLASH_ERASE) { |
| struct { |
| __le32 segment_num; |
| __le32 pad; |
| } __packed *req = data; |
| |
| if (size != sizeof(*req)) |
| return -EINVAL; |
| |
| if (le32_to_cpu(req->segment_num) == FCP_SEGMENT_APP_GOLD) |
| return -EPERM; |
| |
| if (req->pad != 0) |
| return -EINVAL; |
| |
| } else if (opcode == FCP_USB_FLASH_WRITE) { |
| struct { |
| __le32 segment_num; |
| __le32 offset; |
| __le32 pad; |
| u8 data[]; |
| } __packed *req = data; |
| |
| if (size < sizeof(*req)) |
| return -EINVAL; |
| |
| if (le32_to_cpu(req->segment_num) == FCP_SEGMENT_APP_GOLD) |
| return -EPERM; |
| |
| if (req->pad != 0) |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| /* Execute an FCP command specified by the user */ |
| static int fcp_ioctl_cmd(struct usb_mixer_interface *mixer, |
| struct fcp_cmd __user *arg) |
| { |
| struct fcp_cmd cmd; |
| int err, buf_size; |
| void *data __free(kfree) = NULL; |
| |
| /* get opcode and request/response size */ |
| if (copy_from_user(&cmd, arg, sizeof(cmd))) |
| return -EFAULT; |
| |
| /* validate request and response sizes */ |
| if (cmd.req_size > 4096 || cmd.resp_size > 4096) |
| return -EINVAL; |
| |
| /* reinit if needed */ |
| err = fcp_reinit(mixer); |
| if (err < 0) |
| return err; |
| |
| /* allocate request/response buffer */ |
| buf_size = max(cmd.req_size, cmd.resp_size); |
| |
| if (buf_size > 0) { |
| data = kmalloc(buf_size, GFP_KERNEL); |
| if (!data) |
| return -ENOMEM; |
| } |
| |
| /* copy request from user */ |
| if (cmd.req_size > 0) |
| if (copy_from_user(data, arg->data, cmd.req_size)) |
| return -EFAULT; |
| |
| /* check that the command is allowed */ |
| err = fcp_validate_cmd(cmd.opcode, data, cmd.req_size); |
| if (err < 0) |
| return err; |
| |
| /* send request, get response */ |
| err = fcp_usb(mixer, cmd.opcode, |
| data, cmd.req_size, data, cmd.resp_size); |
| if (err < 0) |
| return err; |
| |
| /* copy response to user */ |
| if (cmd.resp_size > 0) |
| if (copy_to_user(arg->data, data, cmd.resp_size)) |
| return -EFAULT; |
| |
| return 0; |
| } |
| |
| /* Validate the Level Meter map passed by the user */ |
| static int validate_meter_map(const s16 *map, int map_size, int meter_slots) |
| { |
| int i; |
| |
| for (i = 0; i < map_size; i++) |
| if (map[i] < -1 || map[i] >= meter_slots) |
| return -EINVAL; |
| |
| return 0; |
| } |
| |
| /* Set the Level Meter map and add the control */ |
| static int fcp_ioctl_set_meter_map(struct usb_mixer_interface *mixer, |
| struct fcp_meter_map __user *arg) |
| { |
| struct fcp_meter_map map; |
| struct fcp_data *private = mixer->private_data; |
| s16 *tmp_map __free(kfree) = NULL; |
| int err; |
| |
| if (copy_from_user(&map, arg, sizeof(map))) |
| return -EFAULT; |
| |
| /* Don't allow changing the map size or meter slots once set */ |
| if (private->meter_ctl) { |
| struct usb_mixer_elem_info *elem = |
| private->meter_ctl->private_data; |
| |
| if (map.map_size != elem->channels || |
| map.meter_slots != private->num_meter_slots) |
| return -EINVAL; |
| } |
| |
| /* Validate the map size */ |
| if (map.map_size < 1 || map.map_size > 255 || |
| map.meter_slots < 1 || map.meter_slots > 255) |
| return -EINVAL; |
| |
| /* Allocate and copy the map data */ |
| tmp_map = kmalloc_array(map.map_size, sizeof(s16), GFP_KERNEL); |
| if (!tmp_map) |
| return -ENOMEM; |
| |
| if (copy_from_user(tmp_map, arg->map, map.map_size * sizeof(s16))) |
| return -EFAULT; |
| |
| err = validate_meter_map(tmp_map, map.map_size, map.meter_slots); |
| if (err < 0) |
| return err; |
| |
| /* If the control doesn't exist, create it */ |
| if (!private->meter_ctl) { |
| s16 *new_map __free(kfree) = NULL; |
| __le32 *meter_levels __free(kfree) = NULL; |
| |
| /* Allocate buffer for the map */ |
| new_map = kmalloc_array(map.map_size, sizeof(s16), GFP_KERNEL); |
| if (!new_map) |
| return -ENOMEM; |
| |
| /* Allocate buffer for reading meter levels */ |
| meter_levels = kmalloc_array(map.meter_slots, sizeof(__le32), |
| GFP_KERNEL); |
| if (!meter_levels) |
| return -ENOMEM; |
| |
| /* Create the Level Meter control */ |
| err = fcp_add_new_ctl(mixer, &fcp_meter_ctl, 0, map.map_size, |
| "Level Meter", &private->meter_ctl); |
| if (err < 0) |
| return err; |
| |
| /* Success; save the pointers in private and don't free them */ |
| private->meter_level_map = new_map; |
| private->meter_levels = meter_levels; |
| private->num_meter_slots = map.meter_slots; |
| new_map = NULL; |
| meter_levels = NULL; |
| } |
| |
| /* Install the new map */ |
| memcpy(private->meter_level_map, tmp_map, map.map_size * sizeof(s16)); |
| |
| return 0; |
| } |
| |
| /* Set the Level Meter labels */ |
| static int fcp_ioctl_set_meter_labels(struct usb_mixer_interface *mixer, |
| struct fcp_meter_labels __user *arg) |
| { |
| struct fcp_meter_labels labels; |
| struct fcp_data *private = mixer->private_data; |
| unsigned int *tlv_data; |
| unsigned int tlv_size, data_size; |
| |
| if (copy_from_user(&labels, arg, sizeof(labels))) |
| return -EFAULT; |
| |
| /* Remove existing labels if size is zero */ |
| if (!labels.labels_size) { |
| |
| /* Clear TLV read/callback bits if labels were present */ |
| if (private->meter_labels_tlv) { |
| private->meter_ctl->vd[0].access &= |
| ~(SNDRV_CTL_ELEM_ACCESS_TLV_READ | |
| SNDRV_CTL_ELEM_ACCESS_TLV_CALLBACK); |
| snd_ctl_notify(mixer->chip->card, |
| SNDRV_CTL_EVENT_MASK_INFO, |
| &private->meter_ctl->id); |
| } |
| |
| kfree(private->meter_labels_tlv); |
| private->meter_labels_tlv = NULL; |
| private->meter_labels_tlv_size = 0; |
| |
| return 0; |
| } |
| |
| /* Validate size */ |
| if (labels.labels_size > 4096) |
| return -EINVAL; |
| |
| /* Calculate padded data size */ |
| data_size = ALIGN(labels.labels_size, sizeof(unsigned int)); |
| |
| /* Calculate total TLV size including header */ |
| tlv_size = sizeof(unsigned int) * 2 + data_size; |
| |
| /* Allocate, set up TLV header, and copy the labels data */ |
| tlv_data = kzalloc(tlv_size, GFP_KERNEL); |
| if (!tlv_data) |
| return -ENOMEM; |
| tlv_data[0] = SNDRV_CTL_TLVT_FCP_CHANNEL_LABELS; |
| tlv_data[1] = data_size; |
| if (copy_from_user(&tlv_data[2], arg->labels, labels.labels_size)) { |
| kfree(tlv_data); |
| return -EFAULT; |
| } |
| |
| /* Set TLV read/callback bits if labels weren't present */ |
| if (!private->meter_labels_tlv) { |
| private->meter_ctl->vd[0].access |= |
| SNDRV_CTL_ELEM_ACCESS_TLV_READ | |
| SNDRV_CTL_ELEM_ACCESS_TLV_CALLBACK; |
| snd_ctl_notify(mixer->chip->card, |
| SNDRV_CTL_EVENT_MASK_INFO, |
| &private->meter_ctl->id); |
| } |
| |
| /* Swap in the new labels */ |
| kfree(private->meter_labels_tlv); |
| private->meter_labels_tlv = tlv_data; |
| private->meter_labels_tlv_size = tlv_size; |
| |
| return 0; |
| } |
| |
| static int fcp_hwdep_open(struct snd_hwdep *hw, struct file *file) |
| { |
| struct usb_mixer_interface *mixer = hw->private_data; |
| struct fcp_data *private = mixer->private_data; |
| |
| if (!capable(CAP_SYS_RAWIO)) |
| return -EPERM; |
| |
| private->file = file; |
| |
| return 0; |
| } |
| |
| static int fcp_hwdep_ioctl(struct snd_hwdep *hw, struct file *file, |
| unsigned int cmd, unsigned long arg) |
| { |
| struct usb_mixer_interface *mixer = hw->private_data; |
| struct fcp_data *private = mixer->private_data; |
| void __user *argp = (void __user *)arg; |
| |
| guard(mutex)(&private->mutex); |
| |
| switch (cmd) { |
| |
| case FCP_IOCTL_PVERSION: |
| return put_user(FCP_HWDEP_VERSION, |
| (int __user *)argp) ? -EFAULT : 0; |
| break; |
| |
| case FCP_IOCTL_INIT: |
| return fcp_ioctl_init(mixer, argp); |
| |
| case FCP_IOCTL_CMD: |
| if (!private->init) |
| return -EINVAL; |
| return fcp_ioctl_cmd(mixer, argp); |
| |
| case FCP_IOCTL_SET_METER_MAP: |
| if (!private->init) |
| return -EINVAL; |
| return fcp_ioctl_set_meter_map(mixer, argp); |
| |
| case FCP_IOCTL_SET_METER_LABELS: |
| if (!private->init) |
| return -EINVAL; |
| if (!private->meter_ctl) |
| return -EINVAL; |
| return fcp_ioctl_set_meter_labels(mixer, argp); |
| |
| default: |
| return -ENOIOCTLCMD; |
| } |
| |
| /* not reached */ |
| } |
| |
| static long fcp_hwdep_read(struct snd_hwdep *hw, char __user *buf, |
| long count, loff_t *offset) |
| { |
| struct usb_mixer_interface *mixer = hw->private_data; |
| struct fcp_data *private = mixer->private_data; |
| unsigned long flags; |
| long ret = 0; |
| u32 event; |
| |
| if (count < sizeof(event)) |
| return -EINVAL; |
| |
| ret = wait_event_interruptible(private->notify.queue, |
| private->notify.event); |
| if (ret) |
| return ret; |
| |
| spin_lock_irqsave(&private->notify.lock, flags); |
| event = private->notify.event; |
| private->notify.event = 0; |
| spin_unlock_irqrestore(&private->notify.lock, flags); |
| |
| if (copy_to_user(buf, &event, sizeof(event))) |
| return -EFAULT; |
| |
| return sizeof(event); |
| } |
| |
| static __poll_t fcp_hwdep_poll(struct snd_hwdep *hw, |
| struct file *file, |
| poll_table *wait) |
| { |
| struct usb_mixer_interface *mixer = hw->private_data; |
| struct fcp_data *private = mixer->private_data; |
| __poll_t mask = 0; |
| |
| poll_wait(file, &private->notify.queue, wait); |
| |
| if (private->notify.event) |
| mask |= EPOLLIN | EPOLLRDNORM; |
| |
| return mask; |
| } |
| |
| static int fcp_hwdep_release(struct snd_hwdep *hw, struct file *file) |
| { |
| struct usb_mixer_interface *mixer = hw->private_data; |
| struct fcp_data *private = mixer->private_data; |
| |
| if (!private) |
| return 0; |
| |
| private->file = NULL; |
| |
| return 0; |
| } |
| |
| static int fcp_hwdep_init(struct usb_mixer_interface *mixer) |
| { |
| struct snd_hwdep *hw; |
| int err; |
| |
| err = snd_hwdep_new(mixer->chip->card, "Focusrite Control", 0, &hw); |
| if (err < 0) |
| return err; |
| |
| hw->private_data = mixer; |
| hw->exclusive = 1; |
| hw->ops.open = fcp_hwdep_open; |
| hw->ops.ioctl = fcp_hwdep_ioctl; |
| hw->ops.ioctl_compat = fcp_hwdep_ioctl; |
| hw->ops.read = fcp_hwdep_read; |
| hw->ops.poll = fcp_hwdep_poll; |
| hw->ops.release = fcp_hwdep_release; |
| |
| return 0; |
| } |
| |
| /*** Cleanup ***/ |
| |
| static void fcp_cleanup_urb(struct usb_mixer_interface *mixer) |
| { |
| if (!mixer->urb) |
| return; |
| |
| usb_kill_urb(mixer->urb); |
| kfree(mixer->urb->transfer_buffer); |
| usb_free_urb(mixer->urb); |
| mixer->urb = NULL; |
| } |
| |
| static void fcp_private_free(struct usb_mixer_interface *mixer) |
| { |
| struct fcp_data *private = mixer->private_data; |
| |
| fcp_cleanup_urb(mixer); |
| |
| kfree(private->meter_level_map); |
| kfree(private->meter_levels); |
| kfree(private->meter_labels_tlv); |
| kfree(private); |
| mixer->private_data = NULL; |
| } |
| |
| static void fcp_private_suspend(struct usb_mixer_interface *mixer) |
| { |
| fcp_cleanup_urb(mixer); |
| } |
| |
| /*** Callbacks ***/ |
| |
| static void fcp_notify(struct urb *urb) |
| { |
| struct usb_mixer_interface *mixer = urb->context; |
| struct fcp_data *private = mixer->private_data; |
| int len = urb->actual_length; |
| int ustatus = urb->status; |
| u32 data; |
| |
| if (ustatus != 0 || len != 8) |
| goto requeue; |
| |
| data = le32_to_cpu(*(__le32 *)urb->transfer_buffer); |
| |
| /* Handle command acknowledgement */ |
| if (data & FCP_NOTIFY_ACK) { |
| complete(&private->cmd_done); |
| data &= ~FCP_NOTIFY_ACK; |
| } |
| |
| if (data) { |
| unsigned long flags; |
| |
| spin_lock_irqsave(&private->notify.lock, flags); |
| private->notify.event |= data; |
| spin_unlock_irqrestore(&private->notify.lock, flags); |
| |
| wake_up_interruptible(&private->notify.queue); |
| } |
| |
| requeue: |
| if (ustatus != -ENOENT && |
| ustatus != -ECONNRESET && |
| ustatus != -ESHUTDOWN) { |
| urb->dev = mixer->chip->dev; |
| usb_submit_urb(urb, GFP_ATOMIC); |
| } else { |
| complete(&private->cmd_done); |
| } |
| } |
| |
| /* Submit a URB to receive notifications from the device */ |
| static int fcp_init_notify(struct usb_mixer_interface *mixer) |
| { |
| struct usb_device *dev = mixer->chip->dev; |
| struct fcp_data *private = mixer->private_data; |
| unsigned int pipe = usb_rcvintpipe(dev, private->bEndpointAddress); |
| void *transfer_buffer; |
| int err; |
| |
| /* Already set up */ |
| if (mixer->urb) |
| return 0; |
| |
| if (usb_pipe_type_check(dev, pipe)) |
| return -EINVAL; |
| |
| mixer->urb = usb_alloc_urb(0, GFP_KERNEL); |
| if (!mixer->urb) |
| return -ENOMEM; |
| |
| transfer_buffer = kmalloc(private->wMaxPacketSize, GFP_KERNEL); |
| if (!transfer_buffer) { |
| usb_free_urb(mixer->urb); |
| mixer->urb = NULL; |
| return -ENOMEM; |
| } |
| |
| usb_fill_int_urb(mixer->urb, dev, pipe, |
| transfer_buffer, private->wMaxPacketSize, |
| fcp_notify, mixer, private->bInterval); |
| |
| init_completion(&private->cmd_done); |
| |
| err = usb_submit_urb(mixer->urb, GFP_KERNEL); |
| if (err) { |
| usb_audio_err(mixer->chip, |
| "%s: usb_submit_urb failed: %d\n", |
| __func__, err); |
| kfree(transfer_buffer); |
| usb_free_urb(mixer->urb); |
| mixer->urb = NULL; |
| } |
| |
| return err; |
| } |
| |
| /*** Initialisation ***/ |
| |
| static int fcp_init(struct usb_mixer_interface *mixer, |
| void *step0_resp, void *step2_resp) |
| { |
| struct fcp_data *private = mixer->private_data; |
| struct usb_device *dev = mixer->chip->dev; |
| int err; |
| |
| err = snd_usb_ctl_msg(dev, usb_rcvctrlpipe(dev, 0), |
| FCP_USB_REQ_STEP0, |
| USB_RECIP_INTERFACE | USB_TYPE_CLASS | USB_DIR_IN, |
| 0, private->bInterfaceNumber, |
| step0_resp, private->step0_resp_size); |
| if (err < 0) |
| return err; |
| |
| err = fcp_init_notify(mixer); |
| if (err < 0) |
| return err; |
| |
| private->seq = 0; |
| private->init = 1; |
| |
| err = fcp_usb(mixer, private->init1_opcode, NULL, 0, NULL, 0); |
| if (err < 0) |
| return err; |
| |
| err = fcp_usb(mixer, private->init2_opcode, |
| NULL, 0, step2_resp, private->step2_resp_size); |
| if (err < 0) |
| return err; |
| |
| return 0; |
| } |
| |
| static int fcp_init_private(struct usb_mixer_interface *mixer) |
| { |
| struct fcp_data *private = |
| kzalloc(sizeof(struct fcp_data), GFP_KERNEL); |
| |
| if (!private) |
| return -ENOMEM; |
| |
| mutex_init(&private->mutex); |
| init_waitqueue_head(&private->notify.queue); |
| spin_lock_init(&private->notify.lock); |
| |
| mixer->private_data = private; |
| mixer->private_free = fcp_private_free; |
| mixer->private_suspend = fcp_private_suspend; |
| |
| private->mixer = mixer; |
| |
| return 0; |
| } |
| |
| /* Look through the interface descriptors for the Focusrite Control |
| * interface (bInterfaceClass = 255 Vendor Specific Class) and set |
| * bInterfaceNumber, bEndpointAddress, wMaxPacketSize, and bInterval |
| * in private |
| */ |
| static int fcp_find_fc_interface(struct usb_mixer_interface *mixer) |
| { |
| struct snd_usb_audio *chip = mixer->chip; |
| struct fcp_data *private = mixer->private_data; |
| struct usb_host_config *config = chip->dev->actconfig; |
| int i; |
| |
| for (i = 0; i < config->desc.bNumInterfaces; i++) { |
| struct usb_interface *intf = config->interface[i]; |
| struct usb_interface_descriptor *desc = |
| &intf->altsetting[0].desc; |
| struct usb_endpoint_descriptor *epd; |
| |
| if (desc->bInterfaceClass != 255) |
| continue; |
| |
| epd = get_endpoint(intf->altsetting, 0); |
| private->bInterfaceNumber = desc->bInterfaceNumber; |
| private->bEndpointAddress = epd->bEndpointAddress & |
| USB_ENDPOINT_NUMBER_MASK; |
| private->wMaxPacketSize = le16_to_cpu(epd->wMaxPacketSize); |
| private->bInterval = epd->bInterval; |
| return 0; |
| } |
| |
| usb_audio_err(chip, "Focusrite vendor-specific interface not found\n"); |
| return -EINVAL; |
| } |
| |
| int snd_fcp_init(struct usb_mixer_interface *mixer) |
| { |
| struct snd_usb_audio *chip = mixer->chip; |
| int err; |
| |
| /* only use UAC_VERSION_2 */ |
| if (!mixer->protocol) |
| return 0; |
| |
| err = fcp_init_private(mixer); |
| if (err < 0) |
| return err; |
| |
| err = fcp_find_fc_interface(mixer); |
| if (err < 0) |
| return err; |
| |
| err = fcp_hwdep_init(mixer); |
| if (err < 0) |
| return err; |
| |
| usb_audio_info(chip, |
| "Focusrite Control Protocol Driver ready (pid=0x%04x); " |
| "report any issues to " |
| "https://github.com/geoffreybennett/fcp-support/issues", |
| USB_ID_PRODUCT(chip->usb_id)); |
| |
| return err; |
| } |