It is the 1728th of March 2020 (aka the 22nd of November 2024)
You are 3.145.91.152,
pleased to meet you!
mailto:blog-at-heyrick-dot-eu
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.
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.
Using !USBinfo to see what the keyboard identified itself as.
With the device and endpoint known, the first program was dead simple:
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:
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:
Miscellaneous Function Codes (for future expansion)
Cable Events (for future expansion)
Two-byte System Common Messages
Three-byte System Common Messages
System Exclusive Messages Start/Continue
Single-byte System Common Messages
Two-byte End of System Exclusive Message
Three-byte End of System Exclusive Message
Note Off
Note On
Poly Keypress
Control Change (stuff like Sustain etc)
Program Change (active voice/sample/patch)
Channel Pressure
Pitch Bend Change
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.
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:
Looks for a device class 1 subclass 3. If your MIDI device presents something different, it won't be seen. Alter the code. This is neither a bug nor a quirk, AUDIO/MIDIStreaming is correct, but some devices identify themselves differently.
If the program is aborted while a note is playing, the note won't be stopped. Add SOUND 1,0,0,0 into the second ON ERROR to fix this. - fixed
Sound will not be turned off if your keyboard actually uses the correct Note Off commands; nor will Note Off be reported. Duh! Add some code into the note parser - if on% is FALSE then it was a Note Off command. - fixed
The parsing of voices is missing, and of commands is incomplete. Got bored. I'll do it another time (along with the above fixes).
It is an uncompressed and commented BASIC file within a Zip:
Written on a Pi! (explains the lower quality video grabs; CVBS != S-video)
Your comments:
Please note that while I check this page every so often, I am not able to control what users write; therefore I disclaim all liability for unpleasant and/or infringing and/or defamatory material. Undesired content will be removed as soon as it is noticed. By leaving a comment, you agree not to post material that is illegal or in bad taste, and you should be aware that the time and your IP address are both recorded, should it be necessary to find out who you are. Oh, and don't bother trying to inline HTML. I'm not that stupid! ☺ ADDING COMMENTS DOES NOT WORK IF READING TRANSLATED VERSIONS.
You can now follow comment additions with the comment RSS feed. This is distinct from the b.log RSS feed, so you can subscribe to one or both as you wish.
This web page is licenced for your personal, private, non-commercial use only. No automated processing by advertising systems is permitted.
RIPA notice: No consent is given for interception of page transmission.