fe85f6e607
The current code clears the bank selection MSB/LSB after sending a
program change, but this can be wrong, as many apps may not send the
full bank selection with both MSB and LSB but sending only one.
Better to keep the previous bank set.
Fixes: 0b5288f5fe
("ALSA: ump: Add legacy raw MIDI support")
Cc: <stable@vger.kernel.org>
Link: https://lore.kernel.org/r/20240529083823.5778-1-tiwai@suse.de
Signed-off-by: Takashi Iwai <tiwai@suse.de>
505 lines
12 KiB
C
505 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 <sound/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 2;
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* snd_ump_convert_from_ump - convert from UMP to legacy MIDI
|
|
* @data: UMP packet
|
|
* @buf: buffer to store legacy MIDI data
|
|
* @group_ret: pointer to store the target group
|
|
*
|
|
* Convert from a UMP packet @data to MIDI 1.0 bytes at @buf.
|
|
* The target group is stored at @group_ret.
|
|
*
|
|
* The function returns the number of bytes of MIDI 1.0 stream.
|
|
*/
|
|
int snd_ump_convert_from_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;
|
|
}
|
|
EXPORT_SYMBOL_GPL(snd_ump_convert_from_ump);
|
|
|
|
/*
|
|
* 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 ump_cvt_to_ump *cvt,
|
|
unsigned char group,
|
|
unsigned int protocol,
|
|
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 (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:
|
|
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;
|
|
}
|
|
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 ump_cvt_to_ump *cvt, unsigned char group,
|
|
unsigned int protocol, 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
|
|
};
|
|
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 >> 4) & 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(cvt, group, protocol, data, cvt->cmd_bytes);
|
|
}
|
|
|
|
/**
|
|
* snd_ump_convert_to_ump - convert legacy MIDI byte to UMP packet
|
|
* @cvt: converter context
|
|
* @group: target UMP group
|
|
* @protocol: target UMP protocol
|
|
* @c: MIDI 1.0 byte data
|
|
*
|
|
* Feed a MIDI 1.0 byte @c and convert to a UMP packet if completed.
|
|
* The result is stored in the buffer in @cvt.
|
|
*/
|
|
void snd_ump_convert_to_ump(struct ump_cvt_to_ump *cvt, unsigned char group,
|
|
unsigned int protocol, unsigned char c)
|
|
{
|
|
cvt->ump_bytes = do_convert_to_ump(cvt, group, protocol, c, cvt->ump);
|
|
}
|
|
EXPORT_SYMBOL_GPL(snd_ump_convert_to_ump);
|