ALSA: usb-audio: work around CH345 input SysEx corruption
authorClemens Ladisch <clemens@ladisch.de>
Sun, 15 Nov 2015 21:39:08 +0000 (22:39 +0100)
committerTakashi Iwai <tiwai@suse.de>
Mon, 16 Nov 2015 07:59:29 +0000 (08:59 +0100)
One of the many faults of the QinHeng CH345 USB MIDI interface chip is
that it does not handle received SysEx messages correctly -- every second
event packet has a wrong code index number, which is the one from the last
seen message, instead of 4.  For example, the two messages "FE F0 01 02 03
04 05 06 07 08 09 0A 0B 0C 0D 0E F7" result in the following event
packets:

correct:       CH345:
0F FE 00 00    0F FE 00 00
04 F0 01 02    04 F0 01 02
04 03 04 05    0F 03 04 05
04 06 07 08    04 06 07 08
04 09 0A 0B    0F 09 0A 0B
04 0C 0D 0E    04 0C 0D 0E
05 F7 00 00    05 F7 00 00

A class-compliant driver must interpret an event packet with CIN 15 as
having a single data byte, so the other two bytes would be ignored.  The
message received by the host would then be missing two bytes out of six;
in this example, "F0 01 02 03 06 07 08 09 0C 0D 0E F7".

These corrupted SysEx event packages contain only data bytes, while the
CH345 uses event packets with a correct CIN value only for messages with
a status byte, so it is possible to distinguish between these two cases by
checking for the presence of this status byte.

(Other bugs in the CH345's input handling, such as the corruption resulting
from running status, cannot be worked around.)

Signed-off-by: Clemens Ladisch <clemens@ladisch.de>
Cc: stable@vger.kernel.org
Signed-off-by: Takashi Iwai <tiwai@suse.de>
sound/usb/midi.c

index 010094abf7521d0ad9336f0911fe7e229341156a..5b4c58c3e2c5f6115d55ad194cd06f971e3fdb70 100644 (file)
@@ -174,6 +174,8 @@ struct snd_usb_midi_in_endpoint {
                u8 running_status_length;
        } ports[0x10];
        u8 seen_f5;
+       bool in_sysex;
+       u8 last_cin;
        u8 error_resubmit;
        int current_port;
 };
@@ -467,6 +469,39 @@ static void snd_usbmidi_maudio_broken_running_status_input(
                }
 }
 
+/*
+ * QinHeng CH345 is buggy: every second packet inside a SysEx has not CIN 4
+ * but the previously seen CIN, but still with three data bytes.
+ */
+static void ch345_broken_sysex_input(struct snd_usb_midi_in_endpoint *ep,
+                                    uint8_t *buffer, int buffer_length)
+{
+       unsigned int i, cin, length;
+
+       for (i = 0; i + 3 < buffer_length; i += 4) {
+               if (buffer[i] == 0 && i > 0)
+                       break;
+               cin = buffer[i] & 0x0f;
+               if (ep->in_sysex &&
+                   cin == ep->last_cin &&
+                   (buffer[i + 1 + (cin == 0x6)] & 0x80) == 0)
+                       cin = 0x4;
+#if 0
+               if (buffer[i + 1] == 0x90) {
+                       /*
+                        * Either a corrupted running status or a real note-on
+                        * message; impossible to detect reliably.
+                        */
+               }
+#endif
+               length = snd_usbmidi_cin_length[cin];
+               snd_usbmidi_input_data(ep, 0, &buffer[i + 1], length);
+               ep->in_sysex = cin == 0x4;
+               if (!ep->in_sysex)
+                       ep->last_cin = cin;
+       }
+}
+
 /*
  * CME protocol: like the standard protocol, but SysEx commands are sent as a
  * single USB packet preceded by a 0x0F byte.
@@ -660,6 +695,12 @@ static struct usb_protocol_ops snd_usbmidi_cme_ops = {
        .output_packet = snd_usbmidi_output_standard_packet,
 };
 
+static struct usb_protocol_ops snd_usbmidi_ch345_broken_sysex_ops = {
+       .input = ch345_broken_sysex_input,
+       .output = snd_usbmidi_standard_output,
+       .output_packet = snd_usbmidi_output_standard_packet,
+};
+
 /*
  * AKAI MPD16 protocol:
  *
@@ -2380,6 +2421,7 @@ int snd_usbmidi_create(struct snd_card *card,
                err = snd_usbmidi_detect_per_port_endpoints(umidi, endpoints);
                break;
        case QUIRK_MIDI_CH345:
+               umidi->usb_protocol_ops = &snd_usbmidi_ch345_broken_sysex_ops;
                err = snd_usbmidi_detect_per_port_endpoints(umidi, endpoints);
                break;
        default: