Rick's b.log - 2013/02/24 |
|
It is the 21st of November 2024 You are 3.15.17.60, pleased to meet you! |
|
mailto:
blog -at- heyrick -dot- eu
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.
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.
With the device and endpoint known, the first program was dead simple:
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.
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.
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:
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.
Prerequisites: clk% and tempo% are initialised to TIME; tempotick% is initialised to zero.
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.
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:
Converting this to a value to present to the RISC OS sound system is as follows:
The program has a few bugs/quirks I have noticed:
RISC OS MIDI
This weekend's project was simple on the face of it. A little harder in reality.
The Pi and the Keyboard.
Using !USBinfo to see what the keyboard identified itself as.
This returned many many zero bytes, but I could see other activity when I pressed keys on the keyboard.
in% = OPENIN("devices#endpoint2:USB7")
REPEAT
b% = BGET#in%
PRINT b%
UNTIL 0
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.
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.
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
in% = OPENIN("devices#endpoint"+STR$(devep%)+":"+devname$)
The upper four bits of that byte specify the "cable". You can have up to sixteen virtual "cables" per USB connection.
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...
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.
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
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.
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.
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).
note% = (((oct% - 1) << 12) + (fra% * 341.3333))
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).
It is an uncompressed and commented BASIC file within a Zip:
If the program is aborted while a note is playing, the note won't be stopped. Add - fixed
SOUND 1,0,0,0
into the second ON ERROR
to fix this.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 - fixed
on%
is FALSE
then it was a Note Off command.
Written on a Pi! (explains the lower quality video grabs; CVBS != S-video)
No comments yet...
© 2013 Rick Murray |
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. |