linux/sound/core/ump_convert.c
Takashi Iwai 0b5288f5fe ALSA: ump: Add legacy raw MIDI support
This patch extends the UMP core code to support the legacy MIDI 1.0
rawmidi devices.  When the new kconfig CONFIG_SND_UMP_LEGACY_RAWMIDI
is set, the UMP core allows to attach an additional rawmidi device for
each UMP Endpoint.  The rawmidi device contains 16 substreams where
each substream corresponds to a UMP Group belonging to the EP.  The
device reads/writes the legacy MIDI 1.0 byte streams and translates
from/to UMP packets.

The legacy rawmidi devices are exclusive with the UMP rawmidi devices,
hence both of them can't be opened at the same time unless the UMP
rawmidi is opened in APPEND mode.

Reviewed-by: Jaroslav Kysela <perex@perex.cz>
Link: https://lore.kernel.org/r/20230523075358.9672-15-tiwai@suse.de
Signed-off-by: Takashi Iwai <tiwai@suse.de>
2023-05-23 12:11:09 +02:00

521 lines
12 KiB
C

// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Helpers for UMP <-> MIDI 1.0 byte stream conversion
*/
#include <linux/module.h>
#include <linux/export.h>
#include <sound/core.h>
#include <sound/asound.h>
#include <sound/ump.h>
#include "ump_convert.h"
/*
* Upgrade / downgrade value bits
*/
static u8 downscale_32_to_7bit(u32 src)
{
return src >> 25;
}
static u16 downscale_32_to_14bit(u32 src)
{
return src >> 18;
}
static u8 downscale_16_to_7bit(u16 src)
{
return src >> 9;
}
static u16 upscale_7_to_16bit(u8 src)
{
u16 val, repeat;
val = (u16)src << 9;
if (src <= 0x40)
return val;
repeat = src & 0x3f;
return val | (repeat << 3) | (repeat >> 3);
}
static u32 upscale_7_to_32bit(u8 src)
{
u32 val, repeat;
val = src << 25;
if (src <= 0x40)
return val;
repeat = src & 0x3f;
return val | (repeat << 19) | (repeat << 13) |
(repeat << 7) | (repeat << 1) | (repeat >> 5);
}
static u32 upscale_14_to_32bit(u16 src)
{
u32 val, repeat;
val = src << 18;
if (src <= 0x2000)
return val;
repeat = src & 0x1fff;
return val | (repeat << 5) | (repeat >> 8);
}
/*
* UMP -> MIDI 1 byte stream conversion
*/
/* convert a UMP System message to MIDI 1.0 byte stream */
static int cvt_ump_system_to_legacy(u32 data, unsigned char *buf)
{
buf[0] = ump_message_status_channel(data);
switch (ump_message_status_code(data)) {
case UMP_SYSTEM_STATUS_MIDI_TIME_CODE:
case UMP_SYSTEM_STATUS_SONG_SELECT:
buf[1] = (data >> 8) & 0x7f;
return 1;
case UMP_SYSTEM_STATUS_SONG_POSITION:
buf[1] = (data >> 8) & 0x7f;
buf[2] = data & 0x7f;
return 3;
default:
return 1;
}
}
/* convert a UMP MIDI 1.0 Channel Voice message to MIDI 1.0 byte stream */
static int cvt_ump_midi1_to_legacy(u32 data, unsigned char *buf)
{
buf[0] = ump_message_status_channel(data);
buf[1] = (data >> 8) & 0xff;
switch (ump_message_status_code(data)) {
case UMP_MSG_STATUS_PROGRAM:
case UMP_MSG_STATUS_CHANNEL_PRESSURE:
return 2;
default:
buf[2] = data & 0xff;
return 3;
}
}
/* convert a UMP MIDI 2.0 Channel Voice message to MIDI 1.0 byte stream */
static int cvt_ump_midi2_to_legacy(const union snd_ump_midi2_msg *midi2,
unsigned char *buf)
{
unsigned char status = midi2->note.status;
unsigned char channel = midi2->note.channel;
u16 v;
buf[0] = (status << 4) | channel;
switch (status) {
case UMP_MSG_STATUS_NOTE_OFF:
case UMP_MSG_STATUS_NOTE_ON:
buf[1] = midi2->note.note;
buf[2] = downscale_16_to_7bit(midi2->note.velocity);
if (status == UMP_MSG_STATUS_NOTE_ON && !buf[2])
buf[2] = 1;
return 3;
case UMP_MSG_STATUS_POLY_PRESSURE:
buf[1] = midi2->paf.note;
buf[2] = downscale_32_to_7bit(midi2->paf.data);
return 3;
case UMP_MSG_STATUS_CC:
buf[1] = midi2->cc.index;
buf[2] = downscale_32_to_7bit(midi2->cc.data);
return 3;
case UMP_MSG_STATUS_CHANNEL_PRESSURE:
buf[1] = downscale_32_to_7bit(midi2->caf.data);
return 2;
case UMP_MSG_STATUS_PROGRAM:
if (midi2->pg.bank_valid) {
buf[0] = channel | (UMP_MSG_STATUS_CC << 4);
buf[1] = UMP_CC_BANK_SELECT;
buf[2] = midi2->pg.bank_msb;
buf[3] = channel | (UMP_MSG_STATUS_CC << 4);
buf[4] = UMP_CC_BANK_SELECT_LSB;
buf[5] = midi2->pg.bank_lsb;
buf[6] = channel | (UMP_MSG_STATUS_PROGRAM << 4);
buf[7] = midi2->pg.program;
return 8;
}
buf[1] = midi2->pg.program;
return 2;
case UMP_MSG_STATUS_PITCH_BEND:
v = downscale_32_to_14bit(midi2->pb.data);
buf[1] = v & 0x7f;
buf[2] = v >> 7;
return 3;
case UMP_MSG_STATUS_RPN:
case UMP_MSG_STATUS_NRPN:
buf[0] = channel | (UMP_MSG_STATUS_CC << 4);
buf[1] = status == UMP_MSG_STATUS_RPN ? UMP_CC_RPN_MSB : UMP_CC_NRPN_MSB;
buf[2] = midi2->rpn.bank;
buf[3] = buf[0];
buf[4] = status == UMP_MSG_STATUS_RPN ? UMP_CC_RPN_LSB : UMP_CC_NRPN_LSB;
buf[5] = midi2->rpn.index;
buf[6] = buf[0];
buf[7] = UMP_CC_DATA;
v = downscale_32_to_14bit(midi2->rpn.data);
buf[8] = v >> 7;
buf[9] = buf[0];
buf[10] = UMP_CC_DATA_LSB;
buf[11] = v & 0x7f;
return 12;
default:
return 0;
}
}
/* convert a UMP 7-bit SysEx message to MIDI 1.0 byte stream */
static int cvt_ump_sysex7_to_legacy(const u32 *data, unsigned char *buf)
{
unsigned char status;
unsigned char bytes;
int size, offset;
status = ump_sysex_message_status(*data);
if (status > UMP_SYSEX_STATUS_END)
return 0; // unsupported, skip
bytes = ump_sysex_message_length(*data);
if (bytes > 6)
return 0; // skip
size = 0;
if (status == UMP_SYSEX_STATUS_SINGLE ||
status == UMP_SYSEX_STATUS_START) {
buf[0] = UMP_MIDI1_MSG_SYSEX_START;
size = 1;
}
offset = 8;
for (; bytes; bytes--, size++) {
buf[size] = (*data >> offset) & 0x7f;
if (!offset) {
offset = 24;
data++;
} else {
offset -= 8;
}
}
if (status == UMP_SYSEX_STATUS_SINGLE ||
status == UMP_SYSEX_STATUS_END)
buf[size++] = UMP_MIDI1_MSG_SYSEX_END;
return size;
}
/* convert from a UMP packet @data to MIDI 1.0 bytes at @buf;
* the target group is stored at @group_ret,
* returns the number of bytes of MIDI 1.0 stream
*/
int snd_ump_convert_from_ump(struct snd_ump_endpoint *ump,
const u32 *data,
unsigned char *buf,
unsigned char *group_ret)
{
*group_ret = ump_message_group(*data);
switch (ump_message_type(*data)) {
case UMP_MSG_TYPE_SYSTEM:
return cvt_ump_system_to_legacy(*data, buf);
case UMP_MSG_TYPE_MIDI1_CHANNEL_VOICE:
return cvt_ump_midi1_to_legacy(*data, buf);
case UMP_MSG_TYPE_MIDI2_CHANNEL_VOICE:
return cvt_ump_midi2_to_legacy((const union snd_ump_midi2_msg *)data,
buf);
case UMP_MSG_TYPE_DATA:
return cvt_ump_sysex7_to_legacy(data, buf);
}
return 0;
}
/*
* MIDI 1 byte stream -> UMP conversion
*/
/* convert MIDI 1.0 SysEx to a UMP packet */
static int cvt_legacy_sysex_to_ump(struct ump_cvt_to_ump *cvt,
unsigned char group, u32 *data, bool finish)
{
unsigned char status;
bool start = cvt->in_sysex == 1;
int i, offset;
if (start && finish)
status = UMP_SYSEX_STATUS_SINGLE;
else if (start)
status = UMP_SYSEX_STATUS_START;
else if (finish)
status = UMP_SYSEX_STATUS_END;
else
status = UMP_SYSEX_STATUS_CONTINUE;
*data = ump_compose(UMP_MSG_TYPE_DATA, group, status, cvt->len);
offset = 8;
for (i = 0; i < cvt->len; i++) {
*data |= cvt->buf[i] << offset;
if (!offset) {
offset = 24;
data++;
} else
offset -= 8;
}
cvt->len = 0;
if (finish)
cvt->in_sysex = 0;
else
cvt->in_sysex++;
return 8;
}
/* convert to a UMP System message */
static int cvt_legacy_system_to_ump(struct ump_cvt_to_ump *cvt,
unsigned char group, u32 *data)
{
data[0] = ump_compose(UMP_MSG_TYPE_SYSTEM, group, 0, cvt->buf[0]);
if (cvt->cmd_bytes > 1)
data[0] |= cvt->buf[1] << 8;
if (cvt->cmd_bytes > 2)
data[0] |= cvt->buf[2];
return 4;
}
static void fill_rpn(struct ump_cvt_to_ump_bank *cc,
union snd_ump_midi2_msg *midi2)
{
if (cc->rpn_set) {
midi2->rpn.status = UMP_MSG_STATUS_RPN;
midi2->rpn.bank = cc->cc_rpn_msb;
midi2->rpn.index = cc->cc_rpn_lsb;
cc->rpn_set = 0;
cc->cc_rpn_msb = cc->cc_rpn_lsb = 0;
} else {
midi2->rpn.status = UMP_MSG_STATUS_NRPN;
midi2->rpn.bank = cc->cc_nrpn_msb;
midi2->rpn.index = cc->cc_nrpn_lsb;
cc->nrpn_set = 0;
cc->cc_nrpn_msb = cc->cc_nrpn_lsb = 0;
}
midi2->rpn.data = upscale_14_to_32bit((cc->cc_data_msb << 7) |
cc->cc_data_lsb);
cc->cc_data_msb = cc->cc_data_lsb = 0;
}
/* convert to a MIDI 1.0 Channel Voice message */
static int cvt_legacy_cmd_to_ump(struct snd_ump_endpoint *ump,
struct ump_cvt_to_ump *cvt,
unsigned char group, u32 *data,
unsigned char bytes)
{
const unsigned char *buf = cvt->buf;
struct ump_cvt_to_ump_bank *cc;
union snd_ump_midi2_msg *midi2 = (union snd_ump_midi2_msg *)data;
unsigned char status, channel;
BUILD_BUG_ON(sizeof(union snd_ump_midi1_msg) != 4);
BUILD_BUG_ON(sizeof(union snd_ump_midi2_msg) != 8);
/* for MIDI 1.0 UMP, it's easy, just pack it into UMP */
if (ump->info.protocol & SNDRV_UMP_EP_INFO_PROTO_MIDI1) {
data[0] = ump_compose(UMP_MSG_TYPE_MIDI1_CHANNEL_VOICE,
group, 0, buf[0]);
data[0] |= buf[1] << 8;
if (bytes > 2)
data[0] |= buf[2];
return 4;
}
status = *buf >> 4;
channel = *buf & 0x0f;
cc = &cvt->bank[channel];
/* special handling: treat note-on with 0 velocity as note-off */
if (status == UMP_MSG_STATUS_NOTE_ON && !buf[2])
status = UMP_MSG_STATUS_NOTE_OFF;
/* initialize the packet */
data[0] = ump_compose(UMP_MSG_TYPE_MIDI2_CHANNEL_VOICE,
group, status, channel);
data[1] = 0;
switch (status) {
case UMP_MSG_STATUS_NOTE_ON:
if (!buf[2])
status = UMP_MSG_STATUS_NOTE_OFF;
fallthrough;
case UMP_MSG_STATUS_NOTE_OFF:
midi2->note.note = buf[1];
midi2->note.velocity = upscale_7_to_16bit(buf[2]);
break;
case UMP_MSG_STATUS_POLY_PRESSURE:
midi2->paf.note = buf[1];
midi2->paf.data = upscale_7_to_32bit(buf[2]);
break;
case UMP_MSG_STATUS_CC:
switch (buf[1]) {
case UMP_CC_RPN_MSB:
cc->rpn_set = 1;
cc->cc_rpn_msb = buf[2];
return 0; // skip
case UMP_CC_RPN_LSB:
cc->rpn_set = 1;
cc->cc_rpn_lsb = buf[2];
return 0; // skip
case UMP_CC_NRPN_MSB:
cc->nrpn_set = 1;
cc->cc_nrpn_msb = buf[2];
return 0; // skip
case UMP_CC_NRPN_LSB:
cc->nrpn_set = 1;
cc->cc_nrpn_lsb = buf[2];
return 0; // skip
case UMP_CC_DATA:
cc->cc_data_msb = buf[2];
return 0; // skip
case UMP_CC_BANK_SELECT:
cc->bank_set = 1;
cc->cc_bank_msb = buf[2];
return 0; // skip
case UMP_CC_BANK_SELECT_LSB:
cc->bank_set = 1;
cc->cc_bank_lsb = buf[2];
return 0; // skip
case UMP_CC_DATA_LSB:
cc->cc_data_lsb = buf[2];
if (cc->rpn_set || cc->nrpn_set)
fill_rpn(cc, midi2);
else
return 0; // skip
break;
default:
midi2->cc.index = buf[1];
midi2->cc.data = upscale_7_to_32bit(buf[2]);
break;
}
break;
case UMP_MSG_STATUS_PROGRAM:
midi2->pg.program = buf[1];
if (cc->bank_set) {
midi2->pg.bank_valid = 1;
midi2->pg.bank_msb = cc->cc_bank_msb;
midi2->pg.bank_lsb = cc->cc_bank_lsb;
cc->bank_set = 0;
cc->cc_bank_msb = cc->cc_bank_lsb = 0;
}
break;
case UMP_MSG_STATUS_CHANNEL_PRESSURE:
midi2->caf.data = upscale_7_to_32bit(buf[1]);
break;
case UMP_MSG_STATUS_PITCH_BEND:
midi2->pb.data = upscale_14_to_32bit(buf[1] | (buf[2] << 7));
break;
default:
return 0;
}
return 8;
}
static int do_convert_to_ump(struct snd_ump_endpoint *ump,
unsigned char group, unsigned char c, u32 *data)
{
/* bytes for 0x80-0xf0 */
static unsigned char cmd_bytes[8] = {
3, 3, 3, 3, 2, 2, 3, 0
};
/* bytes for 0xf0-0xff */
static unsigned char system_bytes[16] = {
0, 2, 3, 2, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1
};
struct ump_cvt_to_ump *cvt = &ump->out_cvts[group];
unsigned char bytes;
if (c == UMP_MIDI1_MSG_SYSEX_START) {
cvt->in_sysex = 1;
cvt->len = 0;
return 0;
}
if (c == UMP_MIDI1_MSG_SYSEX_END) {
if (!cvt->in_sysex)
return 0; /* skip */
return cvt_legacy_sysex_to_ump(cvt, group, data, true);
}
if ((c & 0xf0) == UMP_MIDI1_MSG_REALTIME) {
bytes = system_bytes[c & 0x0f];
if (!bytes)
return 0; /* skip */
if (bytes == 1) {
data[0] = ump_compose(UMP_MSG_TYPE_SYSTEM, group, 0, c);
return 4;
}
cvt->buf[0] = c;
cvt->len = 1;
cvt->cmd_bytes = bytes;
cvt->in_sysex = 0; /* abort SysEx */
return 0;
}
if (c & 0x80) {
bytes = cmd_bytes[(c >> 8) & 7];
cvt->buf[0] = c;
cvt->len = 1;
cvt->cmd_bytes = bytes;
cvt->in_sysex = 0; /* abort SysEx */
return 0;
}
if (cvt->in_sysex) {
cvt->buf[cvt->len++] = c;
if (cvt->len == 6)
return cvt_legacy_sysex_to_ump(cvt, group, data, false);
return 0;
}
if (!cvt->len)
return 0;
cvt->buf[cvt->len++] = c;
if (cvt->len < cvt->cmd_bytes)
return 0;
cvt->len = 1;
if ((cvt->buf[0] & 0xf0) == UMP_MIDI1_MSG_REALTIME)
return cvt_legacy_system_to_ump(cvt, group, data);
return cvt_legacy_cmd_to_ump(ump, cvt, group, data, cvt->cmd_bytes);
}
/* feed a MIDI 1.0 byte @c and convert to a UMP packet;
* the target group is @group,
* the result is stored in out_cvts[group].ump[] and out_cvts[group].ump_bytes
*/
void snd_ump_convert_to_ump(struct snd_ump_endpoint *ump,
unsigned char group, unsigned char c)
{
struct ump_cvt_to_ump *cvt = &ump->out_cvts[group];
cvt->ump_bytes = do_convert_to_ump(ump, group, c, cvt->ump);
}
/* reset the converter context, called at each open */
void snd_ump_reset_convert_to_ump(struct snd_ump_endpoint *ump,
unsigned char group)
{
memset(&ump->out_cvts[group], 0, sizeof(*ump->out_cvts));
}
/* initialize converters */
int snd_ump_convert_init(struct snd_ump_endpoint *ump)
{
ump->out_cvts = kcalloc(16, sizeof(*ump->out_cvts), GFP_KERNEL);
if (!ump->out_cvts)
return -ENOMEM;
return 0;
}
/* release resources */
void snd_ump_convert_free(struct snd_ump_endpoint *ump)
{
kfree(ump->out_cvts);
ump->out_cvts = NULL;
}