mathr / blog / #

MIDI network between Linux and Amiga

My first real experience of computers was with an Amiga A500+, a home computer from the early 1990s with a 7 MHz processor. One thing of note about Amiga hardware is the floppy disk drive controller: more advanced than PC hardware in some ways, Amiga drives can read/write 720kB PC format disks with the right software, but PC drives can't handle 880kB Amiga format isks. One traditional way of transferring files is a serial link, but modern PCs don't come with native serial ports, and buying a USB adaptor and a serial cable seemed like too much effort, when I already have a USB sound card with a MIDI interface for my modern PC and a MIDI interface for my Amiga, and MIDI cables. So I figured I'd try to write some software to transfer files using MIDI.

MIDI has a message protocol with the highest bit of 8 set to 1 for status bytes, and data bytes having the top bit 0 leaving 7 bits for the payload. System Exclusive messages provide a way to sidestep the music-specific parts of the protocol (note on and off messages, etc) and have arbitrary packets of 7-bit data bracketed by some metadata. I'm using the non-commercial SysEx manufacturer ID 0x7D, which should not be in released products apparently. Don't know if it's worth trying to register something, anyway this is all very preliminary and experimental. Maybe it will matter more if you have a lot of other MIDI devices connected.

Linux's ALSA layer has a MIDI driver, and the amidi tool can send and receive formatted SysEx files fairly straightforwardly:

Command line using Linux ALSA tools to send file over MIDI:

amidi -p hw:2,0,0 -s input.syx

(NOTE: replace the device with your own MIDI hardware.)

Command line using Linux ALSA tools to receive file from MIDI:

amidi -p hw:2,0,0 -r output.syx

(NOTE: press Ctrl-C to terminate once the transfer is complete.)

ASCII text data is natively 7-bit so that can be packed into SysEx quite easily, but 8-bit binary data needs repacking. I went for a simple but wasteful protocol, splitting each 8-bit input byte into two 4-bit nibbles and storing them in adjacent 7-bit output bytes. This wastes 3/8 bits, but so far I transferred only small files so the slowness was acceptable. I wrote a small C program to format ASCII as 7-bit or binary data as nibbles into MIDI SysEx:

#include <stdio.h>
#include <string.h>

int main(int argc, char **argv)
{
  if (argc != 2 || (strcmp("ascii", argv[1]) && strcmp("nibbles", argv[1])))
  {
    fprintf(stderr, "usage: %s (ascii|nibbles)\n", argv[0]);
    return 1;
  }
  int ascii = ! strcmp("ascii", argv[1]);
  fputc(0xF0, stdout);
  fputc(0x7D, stdout);
  int c;
  while (EOF != (c = fgetc(stdin)))
  {
    if (ascii)
    {
      fputc(c, stdout);
    }
    else
    {
      int hi = (c >> 4) & 0xF;
      int lo = c & 0xF;
      fputc(hi, stdout);
      fputc(lo, stdout);
    }
  }
  fputc(0xF7, stdout);
  return 0;
}

A better approach might be to pack binary more tightly, 8x 7-bit output bytes for 7x 8-bit input bytes, but the code is quite a bit more complicated.

At the other (Amiga A500+) side of the link I needed some software to receive and decode the incoming MIDI SysEx messages. I briefly tried an OctaMED Pro 4 magazine coverdisk edition, but that couldn't save SysEx and in any case I would need to strip off 2 bytes at the start and 1 at the end to get the raw ASCII, and further process to get binary from nibbles. Luckily I eventually found a copy of Blitz Basic 2, and wrote a small program to receive and extract MIDI SysEx as ASCII:

WBStartup
ser.l = OpenSerial("serial.device", 0, 31250, 144)
SetSerialBuffer 0, 100000
out.l = WriteFile(1, "RAM:out")
FileOutput 1
Repeat
  c.w = ReadSerial(0)
Until c.w = $F0
c.w = ReadSerial(0)
If c.w = $00
  c.w = ReadSerial(0)
  c.w = ReadSerial(0)
EndIf
c.w = ReadSerial(0)
While c.w <> $F7
  Print Chr$(c.w)
  c.w = ReadSerial(0)
Wend
CloseFile 1
DefaultOutput
CloseSerial 0
End

(TODO: error checking for OpenSerial and WriteFile.) This was more complicated and time consuming than necessary as my Amiga A500+'s E key is broken. I had to find the a text file with E and e characters to copy and paste for each occurrence. Then I could write the program to deal with binary nibbles on my Linux machine with working keyboard and more advanced text editor (though no Blitz Basic 2 online help), and transfer it over MIDI as ASCII. Here's the Amiga Blitz Basic 2 program to receive and extract binary data from MIDI SysEx as nibbles:

WBStartup
ser.l = OpenSerial("serial.device", 0, 31250, 144)
SetSerialBuffer 0, 100000
out.l = WriteFile(1, "RAM:out")
FileOutput 1
Repeat
  c.w = ReadSerial(0)
Until c.w = $F0
c.w = ReadSerial(0)
If c.w = $00
  c.w = ReadSerial(0)
  c.w = ReadSerial(0)
EndIf
c.w = ReadSerial(0)
While c.w <> $F7
  b.w = (c.w & $F) LSL 4
  c.w = ReadSerial(0)
  If c.w <> $F7
    b.w = (c.w & $F) | b.w
    Print Chr$(b.w)
    c.w = ReadSerial(0)
  EndIf
Wend
CloseFile 1
DefaultOutput
CloseSerial 0
End
End

So now I can transfer arbitrary data from Linux to Amiga over MIDI, the next step was going the other direction. I wrote two small programs to format ASCII and binary as nibbles, and send over the MIDI serial link, ASCII first:

WBStartup
ser.l = OpenSerial("serial.device", 0, 31250, 144)
SetSerialBuffer 0, 100000
in.l = ReadFile(1, "RAM:in")
WriteSerial 0, $F0
WriteSerial 0, $7D
While NOT Eof(1)
  char.b = 0
  ReadMem 1, &char.b, 1
  WriteSerialMem 0, &char.b, 1
Wend
WriteSerial 0, $F7
CloseFile 1
CloseSerial 0
End

and binary nibbles:

WBStartup
ser.l = OpenSerial("serial.device", 0, 31250, 144)
SetSerialBuffer 0, 100000
in.l = ReadFile(1, "RAM:in")
WriteSerial 0, $F0
WriteSerial 0, $7D
While NOT Eof(1)
  char.b = 0
  ReadMem 1, &char.b, 1
  c.w = char.b & $FF
  WriteSerial 0, (c.w LSR 4) & $F
  WriteSerial 0, c.w & $F
Wend
WriteSerial 0, $F7
CloseFile 1
CloseSerial 0
End

Finally I wrote a small Linux C program to extract ASCII as 7-bit or binary data as nibbles from the MIDI SysEx dumps from the amidi tool:

#include <stdio.h>
#include <string.h>

int main(int argc, char **argv)
{
  if (argc != 2 || (strcmp("ascii", argv[1]) && strcmp("nibbles", argv[1])))
  {
    fprintf(stderr, "usage: %s (ascii|nibbles)\n", argv[0]);
    return 1;
  }
  int ascii = ! strcmp("ascii", argv[1]);
  if (0xF0 == fgetc(stdin))
  {
    if (0x7D == fgetc(stdin))
    {
      if (ascii)
      {
        int c;
        while (EOF != (c = fgetc(stdin)))
        {
          if (c == 0xF7) break;
          fputc(c, stdout);
        }
      }
      else
      {
        int hi;
        while (EOF != (hi = fgetc(stdin)))
        {
          if (hi == 0xF7) break;
          int lo;
          if (EOF != (lo = fgetc(stdin)))
          {
            if (lo == 0xF7) break;
            fputc((hi << 4) | lo, stdout);
          }
        }
      }
    }
  }
  return 0;
}

Eventually I will neaten all of this up and make the Amiga code have a GUI for selecting files and transfer modes, but these programs are short enough to type in by hand when no other data transfer method is available. Another feature might be to specify the filename in the protocol, so that you don't have to keep track, as well as chunked transfers with checksums for data integrity. So far these tools are a bit inconvenient, but I successfully transferred a 6kB text file from Linux to Amiga, and a 42kB OctaMED module (binary) from Amiga to Linux. My most immediate next step is transferring an ADF (disk imager) tool and a data compression tool (to speed up transfers) from Linux to Amiga and making backups of my Blitz Basic 2 disks to transfer to Linux for use in the FS-UAE Amiga emulation software.