mailto: blog -at- heyrick -dot- eu

Navi: Previous entry Display calendar Next entry
Switch to desktop version

FYI! Last read at 19:00 on 2024/11/21.

RISC OS MIDI

This weekend's project was simple on the face of it. A little harder in reality.

My Yamaha keyboard has a USB socket that outputs some sort of MIDI.

My RaspberryPi has a USB socket.

The resultant "thought" ought to be obvious.

Pi and keyboard
The Pi and the Keyboard.

When you are working with raw USB on RISC OS, you are dealing with DeviceFS and you will need to know two things - the name of the USB device to which you wish to receive data, and the correct endpoint. An endpoint is sort of like a pipe, either data goes in, or data falls out. But not at the same time. So a bi-di communication will have two endpoints. Actually, it is a lot more complex than that as some devices have numerous endpoints for different purposes.

I had originally determined my keyboard to be device "USB7" with endpoint 2.

USB info
Using !USBinfo to see what the keyboard identified itself as.

With the device and endpoint known, the first program was dead simple:

in% = OPENIN("devices#endpoint2:USB7")
REPEAT
  b% = BGET#in%
  PRINT b%
UNTIL 0
This returned many many zero bytes, but I could see other activity when I pressed keys on the keyboard.

I will skip a lot of headscratching and say that there are some fundamental limitations in the RISC OS USB stack. The first of which is that if you use USB in interrupt mode, you need to request a certain amount of data to be received, or else the system will block awaiting it. This is not a problem with bulk storage as the important transfers will be a request to your actions so you'll know how much data will be coming in (say, a sector or somesuch). However erratic transfers such as MIDI - you cannot say how much data will be coming in, if any. My keyboard transmits a clock tick every quarter beat (every 2-10cs depending on tempo) and an Active Sense request (about every 3-4cs), but other keyboards don't do this. They may be silent until something happens.
The alternative is to just keep looking for bytes, but due to how the system is set up it will block if there is no byte to return. I don't know why, OS_BGet provides for the validity of data to be flagged with the Carry flag. In addition there is a call to return the size of the buffer and the number of bytes free...which doesn't seem to bear any semblance to reality.
So the accepted method is padding. Stuffing in zero bytes when there is nothing actually there to receive.

For the purposes of my MIDI receiver, I look for specific codes and ignore everything else. One thing that will certainly fail is USBMIDI events starting with a zero byte...thankfully it is "for expansion" so there aren't any. Yet.

My keyboard has an auto-off. Switching back on will cause the USB attachment to be reassigned, so I had to make the program look for the correct device.

REM Step one - enumerate USB devices looking for one with
REM an interface "class 1.3" (audio, midi streaming).
SYS "OS_ServiceCall", 1, &D2, 0 TO ,,blk%
devtmp$ = ""
devname$ = ""
devep% = 0
REPEAT
  REM Get pointer to next device
  next% = blk%!0

  REM Read device name
  blk%?13 = 13
  IF blk%?12 = 0 THEN blk%?12 = 13
  devtmp$ = $(blk%+8)

  REM Skip to description blocks
  blk% = blk% + (blk%!4 >> 16) + 4

  REPEAT
    blksize% = blk%?0
    blktype% = blk%?1

    REM Interface description block, looking for
    REM device with class 1 (audio), subclass 3
    REM (midi streaming).
    IF blktype% = 4 THEN
      IF ((blk%?5 = 1) AND (blk%?6 = 3)) THEN
        PRINT "Identified device: "+devtmp$
        devname$ = devtmp$
      ENDIF
    ENDIF

    REM Look at endpoints for the one to read
    REM in data from the device (addr > 128).
    IF devname$ <> "" THEN
      REM Only scan endpoints if device matched
      IF blktype% = 5 THEN
        IF (blk%?2 > 128) THEN
          devep% = (blk%?2 - 128)
          PRINT "Identified endpoint: "+STR$(devep%)
          blksize% = 0 : REM Force parsing to stop now...
        ENDIF
      ENDIF
    ENDIF

    blk% = blk% + blksize%
  UNTIL blksize% = 0

  blk% = next%
UNTIL next% = 0
This will not make sense without the Castle documentation describing the format of USB descriptors (the info on the ROOL website is partial), and even then I had to make the assumption that an endpoint address with bit 7 set was a "send" not a "receive" (point of view of the device, not the host). The available documentation is minimal, so that code above was the result of a lot of "try this" followed by poking around in memory with the debugger to see what was there and where it was.

Once the device and endpoint had been determined, we can open it with:

in% = OPENIN("devices#endpoint"+STR$(devep%)+":"+devname$)

And, then, it is as simple as just running BGET over and over.

USBMIDI, as opposed to plain MIDI, sends MIDI events with a prefix. The prefixes are provided in the low nibble (bits 0-3) of the first byte received:

  1. Miscellaneous Function Codes (for future expansion)
  2. Cable Events (for future expansion)
  3. Two-byte System Common Messages
  4. Three-byte System Common Messages
  5. System Exclusive Messages Start/Continue
  6. Single-byte System Common Messages
  7. Two-byte End of System Exclusive Message
  8. Three-byte End of System Exclusive Message
  9. Note Off
  10. Note On
  11. Poly Keypress
  12. Control Change (stuff like Sustain etc)
  13. Program Change (active voice/sample/patch)
  14. Channel Pressure
  15. Pitch Bend Change
  16. Single Byte Immediate Messages
The upper four bits of that byte specify the "cable". You can have up to sixteen virtual "cables" per USB connection.

The thing seems to be a little bit over-engineered, doesn't it? Why not just send System Exclusive messages in chunks with a start/continue, use the EOX byte (&F7) to mark the end, and pad it to fit with zero bytes. That would use one code, not the three provided.

That said, manufacturers are known for doing their own thing. My keyboard never sends a Note Off. Instead I receive a Note On with a velocity of zero.

Once this prefix code has been stripped, what remains is plain MIDI. Once all the quirks and headaches have been sorted, it seemed to be remarkably simple to turn a plugged in MIDI lead into a source of usable data.

MIDI events report
MIDI events report.

 

Some things I encountered in my coding

Converting MIDI ticks to BPM. There seems to be a hell of a lot of rubbish out there on-line, so...

Prerequisites: clk% and tempo% are initialised to TIME; tempotick% is initialised to zero.

  WHEN &F8 : PRINT "[Clock]        ";
             diff% = TIME - clk%
             PRINT "Interval = "+STR$(diff%)+" cs";
             clk% = TIME
             tempotick% = tempotick% + 1
             IF (tempotick% = 24) THEN
               diff% = TIME - tempo%
               PRINT ", tempo = "+STR$(INT(6000 / diff%))+"BPM"
               tempo% = TIME
               tempotick% = 0
             ELSE
               PRINT
             ENDIF
This takes place in the Single Byte System Message handler. Every time a clock tick (MIDI &F8) is received, it is reported. Typically these will come in every 3-4 centiseconds (25-30 per second) but this depends upon the tempo.

The biggest complication here is dealing with crappy Americanisms. The MIDI tick runs at a rate of 24ppqn - which means 24 pulses per quarter note. It is only when you see that there is a direct correlation between "quarter notes" and BPM (beats per minute) that you realise that they are talking about crotchets. This isn't, of course, universal as different time signatures may alter this. However it stands for 4:4 time.
As the RISC OS time is a centisecond ticker, we can work out the BPM by dividing 6000 (cs per minute) by the length of time it took to receive 24 MIDI ticks. The result is the BPM.

 

A note, specified by MIDI, is a value from zero to 127, with 60 being middle C (on a piano voice, at least). This can be fairly easily translated into octave and note by:

  oct% = INT(b% / 12)
  fra% = b% MOD 12
oct% is the octave number, and fra% is the note within the octave - zero is a C and it counts up in the following sequence: C, C#, D, D#, E, F, F#, G, G#, A, A#, B.

Converting this to a value to present to the RISC OS sound system is as follows:

    note% = (((oct% - 1) << 12) + (fra% * 341.3333))
This derives from &4000 being the value for middle C; and the fractional part having 4096 possible values (although I don't know if it is even possible to distinguish 4096 steps between octaves).

 

The code

It has been tested on my RaspberryPi. I see no reason why it wouldn't work on a Beagleboard or anything else running RISC OS with a USB interface except a RiscPC with the Simtec interface (I think the USB mechanism is different).

The program has a few bugs/quirks I have noticed:

It is an uncompressed and commented BASIC file within a Zip:
miditest.zip (about 4½KiB)

Written on a Pi!
Written on a Pi! (explains the lower quality video grabs; CVBS != S-video)

 

Your comments:

No comments yet...

Add a comment (v0.11) [help?]
Your name:

 
Your email (optional):

 
Validation:
Please type 02546 backwards.

 
Your comment:

 

Navi: Previous entry Display calendar Next entry
Switch to desktop version

Search:

See the rest of HeyRick :-)