nick@lfcs.ed.ac.uk (Nick Rothwell) (09/21/89)
Well, I've had a few hollers coming in from the net, so here's the code. It's only a single module, so I haven't bothered running it through StuffIt or any of that jazz. THINK C uses 4-space tabs, so this might look a bit weird out in the open. I've not given this code a thorough shakedown, though it seems fine. If you have any problems, or if anything looks non-kosher, please let me know. Nick. -- /* midi.c: Anodyne's interface to the MIDI Manager. Make sure this module is in the same segment of the project as main(), so that the interrupt handler here is loaded and locked. This module is essentially a simple channelising echo, plus facilities to output arbitrary messages. We have a single input port, since those of us lucky enough to own two keyboards can have the MIDI Manager do the merging for us. Anodyne doesn't distinguish between input sources. We drive several outputs; Anodyne keeps track of output (and channel) for each device. We don't echo SysEx messages by default, but can choose to capture them for the application. Nick Rothwell, September 1989. */ #include <MidiManager.h> #include <assertDie.h> #include "GLOBAL.h" #include "resources.h" #include "midi.h" #include "signature.h" #include "callback.h" /* We provide some callback routines. */ #include "misc.h" extern void diagnostic(char *template, ...); #define BUFSIZE 4096 /* MIDI port buffer size. */ #define MAX_MESSAGE 249 /* Why isn't this defined in MIDI.h?? */ #define PACKET_JUNK 6 /* Number of bytes other than data. */ #define UNLOAD_TIMEOUT 200 /* Ticks. */ #define MAX_DIAGS 10 /* Circular buffer of diagnostic messages. */ #define MIN(x, y) ((x) < (y) ? (x) : (y)) /* Local prototypes. */ static void postDiagnostic(char *mesg); static void postError(char *mesg, int extra); static void doCapture(MIDIPacketPtr packet); static void dealWithMessage(MIDIPacketPtr inPacket); static pascal int myEchoHook(MIDIPacketPtr myPacket, long myRefCon), myCaptureHook(MIDIPacketPtr myPacket, long myRefCon); static void silence(void); static void transmit(Byte *mesg, int len); static int calcSysExLength(Byte *mesg); static void setInputLock(Boolean how); static void check(OSErr err); #define NUM_OUTPUTS 2 #define SOX 0xF0 #define EOX 0xF7 /* A static record to hold the variables needed by the input interrupt routines. It doesn't need to be a record, but it makes things clearer. Besides, we might want to have more than one input port some day. Even with this, we still need the A5 magic to be able to call any routines. */ /* A few words about System Exclusive capture and locking. The structure of the various critical sections is, err, critical. If we want to capture a sequence of system exclusive messages, without missing any or getting into a race condition, we have to be very careful. We install a different readHook, one which does nothing but capture system exclusive messages. It respects the locked flag, and in addition it sets itself to a locked state whenever it has captured a complete set of system exclusives. There's a small fault here: we may be half-way through dealing with a multi-packet system exclusive anyway, in which case the capture will catch the end of it. I'm quite happy to ignore this (or fix it elsewhere) rather than add complexity to the interrupt routines. */ typedef struct { Boolean locked; /* Lock out the input routine from appl. */ int captureHowMany; /* How many SysEx's to capture? */ Byte *capturePtr; /* Destination for SysEx data. */ long captureSpace; /* Space left. */ PORT destination; /* 0=NONE, 1..n=output #1..n. */ int inputRefNum; int outputRefNum[NUM_OUTPUTS]; /* Reference num for output ports. */ int outputMidiChan; /* MIDI channel for current output port. */ char *error; /* if non-zero, it's a C string for a fatal error. */ int extra; /* Extra error info. */ char *diagnostics[MAX_DIAGS]; int diagProduce; /* Circular buffer of diagnostics. */ int diagConsume; } InputPortContext; static InputPortContext context; /* Interrupt routine(s). No calls to other segments are wise here, nor calls to ToolBox routines. */ /* We maintain a circular buffer of diagnostics which the interrupt routine is allowed to post to. */ static void postDiagnostic(mesg) char *mesg; { context.diagnostics[context.diagProduce] = mesg; context.diagProduce = (context.diagProduce+1) % MAX_DIAGS; } static void postError(mesg, extra) char *mesg; int extra; { context.error = mesg; context.extra = extra; context.locked = TRUE; /* Stop further processing. */ } static void dealWithMessage(inPacket) MIDIPacketPtr inPacket; { Byte status = inPacket->data[0]; /* Status byte */ Byte status0 = status&0xF0; /* Status byte sans channel. */ PORT dest; int refNum; MIDIPacket outPacket; OSErr err; switch (status0) { case 0x80: /* NOTE OFF. */ case 0x90: /* NOTE ON. */ case 0xA0: /* POLYPHONIC AFTERTOUCH. */ case 0xB0: /* CONTROL CHANGE. */ case 0xC0: /* PROGRAM CHANGE. */ case 0xD0: /* CHANNEL AFTERTOUCH. */ case 0xE0: /* PITCH-WHEEL. */ dest = context.destination; if (dest != NOWHERE) { outPacket = *inPacket; /* Copy the whole record (ouch!) */ outPacket.data[0] = status0 + (context.outputMidiChan-1); /* Channelise... */ refNum = context.outputRefNum[dest-1]; err = MIDIWritePacket(refNum, &outPacket); if (err != noErr) postError("MIDIWritePacket(chan msg) from readHook", err); } break; case 0xF0: /* SYSTEM MESSAGE... */ switch (status) { /* Common: */ case 0xF0: /* START OF EXCLUSIVE. */ break; case 0xF7: /* END OF EXCLUSIVE. */ postError("Unexpected data[0] == F7", 0); /* We ignore continuation packets, remember? */ break; case 0xF1: /* MIDI TIME CODE 1/4 FRAME. */ case 0xF2: /* SONG POSITION POINTER. */ case 0xF3: /* SONG SELECT. */ case 0xF4: /* undefined. */ case 0xF5: /* undefined. */ case 0xF6: /* TUNE REQUEST. */ /* Realtime: */ case 0xF8: /* TIMING CLOCK. */ case 0xF9: /* undefined. */ case 0xFA: /* START. */ case 0xFB: /* CONTINUE. */ case 0xFC: /* STOP. */ case 0xFD: /* undefined. */ case 0xFE: /* ACTIVE SENSING. */ case 0xFF: /* SYSTEM RESET. */ dest = context.destination; if (dest != NOWHERE) { refNum = context.outputRefNum[dest-1]; err = MIDIWritePacket(refNum, inPacket); if (err != noErr) postError("MIDIWritePacket(sys msg) from readHook", err); } break; } break; default: /* Not a status byte? */ postError("readHook: bad data[0]", (int) status); } } /* This is the readhook for normal operation; echo everything except System Exclusives, which we discard. */ static pascal int myEchoHook(myPacket, myRefCon) MIDIPacketPtr myPacket; long myRefCon; /* The refCon is the saved A5 for this context. */ { long safeA5 = SetA5(myRefCon); /* Is "SetA5()" in Inside Mac anywhere? */ Byte flags; if (context.locked) /* We're locked. Catch it next time. */ { (void) SetA5(safeA5); return midiKeepPacket; } else { flags = myPacket->flags; switch (flags&midiContMask) { case midiNoCont: /* Single packet message: process. */ switch (flags&midiTypeMask) { case midiMsgType: /* MIDI data. */ dealWithMessage(myPacket); break; case midiMgrType: /* Message from MIDI manager. */ postError("Unexpected message from MIDI Manager", *((int *) &myPacket->data[0]) ); /* data[0..1] is a word containing the error. */ break; default: break; /* Ignore unknown message types. */ } break; case midiStartCont: /* Start of long SysEx sequence - ignore. */ case midiMidCont: /* Must be middle of a SysEx. */ case midiEndCont: /* End of SysEx. */ break; } (void) SetA5(safeA5); return midiMorePacket; } } /* myCaptureHook(): capture system exclusives, ignore anything else. */ static void doCapture(packet) MIDIPacketPtr packet; { int len = packet->len - PACKET_JUNK; /*postDiagnostic("doCapture");*/ if (len <= context.captureSpace) { BlockMove(&packet->data[0], context.capturePtr, len); context.capturePtr += len; context.captureSpace -= len; } else /* Overflow! */ postError("SysEx capture: overflow!", len); } static pascal int myCaptureHook(myPacket, myRefCon) MIDIPacketPtr myPacket; long myRefCon; { long safeA5 = SetA5(myRefCon); Byte flags; if (context.locked) /* We're locked. Catch it next time. */ { (void) SetA5(safeA5); return midiKeepPacket; } else { flags = myPacket->flags; switch (flags&midiContMask) { case midiNoCont: /* Single packet message. */ switch (flags&midiTypeMask) { case midiMsgType: /* MIDI data. */ if (myPacket->data[0] == SOX) { doCapture(myPacket); if (--context.captureHowMany == 0) context.locked = TRUE; } break; case midiMgrType: /* Message from MIDI manager. */ postError("Unexpected message from MIDI Manager", *((int *) &myPacket->data[0]) ); /* data[0..1] is a word containing the error. */ break; default: break; /* Ignore unknown message types. */ } break; case midiStartCont: /* Start of long SysEx sequence. */ case midiMidCont: /* Must be middle of a SysEx. */ doCapture(myPacket); break; case midiEndCont: /* End of SysEx. */ doCapture(myPacket); if (--context.captureHowMany == 0) context.locked = TRUE; break; } (void) SetA5(safeA5); return midiMorePacket; } } /* checkMidi(): the application calls this routine periodically to make sure the MIDI module is happy. */ void checkMidi() { char *error = context.error; /*Check for diagnostics first. */ while (context.diagConsume != context.diagProduce) { diagnostic("readHook: %s\r", context.diagnostics[context.diagConsume]); context.diagConsume = (context.diagConsume+1) % MAX_DIAGS; } if (error != 0L) /* Whoops! error */ die("MIDI hook error: %s (%d)", error, context.extra); } /* transmit(): not used at interrupt time, where we just use MIDIWritePacket() to echo things. I'm assuming that the MIDI Manager is structured so that two calls to MIDIWritePacket() which happen at once don't interfere; after all, the readHook might echo something while we're in the middle of transmitting something else. It's an important property that the readHook doesn't echo SysEx messages; otherwise, we'd have to have some safe way of waiting for it to come out of any run of continued packets. As it is, with have to deal with the inverse case; when we want to transmit a long message, we must lock out the readHook so that it doesn't insert one of its own messages in the middle. */ static void transmit(mesg, len) Byte *mesg; int len; { int dest, refNum; Byte type; Byte *ptr; int thisTime; MIDIPacket p; OSErr err; dest = context.destination; assert("transmit(NOWHERE)", dest != NOWHERE); refNum = context.outputRefNum[dest-1]; p.flags = midiTimeStampCurrent; /* Ignore packet timestamp, use dest. */ p.tStamp = 0L; /* Junk value. */ /*diagnostic("transmit(%d)\r", len);*/ if (len <= MAX_MESSAGE) /* Can do in a single packet. */ { p.flags |= midiNoCont; /* (midiNoCont is actually 0, but...) */ /*diagnostic("send %d, flags=%02x\r", len, p.flags);*/ BlockMove(mesg, &p.data[0], len); p.len = len + PACKET_JUNK; err = MIDIWritePacket(refNum, &p); if (err != noErr) postError("MIDIWritePacket from transmit()", err); } else /* Several stages. */ { setInputLock(TRUE); /* Stop the readHook from gate-crashing. */ type = midiStartCont; ptr = mesg; while (len > 0) { thisTime = MIN(len, MAX_MESSAGE); p.flags &= (~midiContMask); p.flags |= type; /*diagnostic("send %d, flags=%02x\r", len, p.flags);*/ BlockMove(ptr, &p.data[0], thisTime); p.len = thisTime + PACKET_JUNK; err = MIDIWritePacket(refNum, &p); if (err != noErr) postError("MIDIWritePacket from transmit()", err); ptr += thisTime; len -= thisTime; type = (len > MAX_MESSAGE) ? midiMidCont : midiEndCont; } setInputLock(FALSE); } } static void silence() { Byte mesg[3]; /* All notes off: */ mesg[0] = 0xB0 + (context.outputMidiChan - 1); mesg[1] = 0x7B; mesg[2] = 0x00; transmit(mesg, 3); } /* select a default for midi idling and for messages; or, nothing. The lock is just to stop any note-on messages sneaking in after we've silenced the channel. */ void midiSelect(port, chan) PORT port; int chan; { setInputLock(TRUE); if (port < 0 || port > NUM_OUTPUTS) die("midiSelect: bad port (%d)", port); if (port != context.destination || chan != context.outputMidiChan) if (context.destination != NOWHERE) silence(); context.destination = port; context.outputMidiChan = chan; setInputLock(FALSE); diagnostic("midiSelect: dest=%d, chan=%d\r", context.destination, context.outputMidiChan ); } /* midiNoselection(): we lock to stop any note-on messages sneaking in after we've silenced the channel. */ void midiNoSelection() { setInputLock(TRUE); if (context.destination != NOWHERE) silence(); context.destination = NOWHERE; setInputLock(FALSE); } static int calcSysExLength(mesg) Byte *mesg; { Byte *p = mesg; while (*p != EOX) p++; return((p - mesg) + 1); } pascal void sendSysEx(mesg) /* "pascal" because we callback. */ Byte *mesg; { if (context.destination != NOWHERE) transmit(mesg, calcSysExLength(mesg)); } static void setInputLock(how) Boolean how; { context.locked = how; } /* capturing(). The first thing this does is to set up the input lock, so that we can capture some SysEx data. After each capture, the input reader stays locked. So, the capturer *must* clear the lock afterwards. */ pascal void capturing(how) Boolean how; { if (how) /* Set up for capturing SysEx. */ { setInputLock(TRUE); /* Lock the reader. */ MIDISetReadHook(context.inputRefNum, &myCaptureHook); } /* ...and we stay locked for now. */ else { MIDISetReadHook(context.inputRefNum, &myEchoHook); setInputLock(FALSE); } } pascal Boolean captureSysex(howMany, buffPtr, maxSize_, actualSize) int howMany; Byte *buffPtr; long maxSize_; long *actualSize; { long until; Byte *lastPtr; Boolean done; context.captureHowMany = howMany; context.capturePtr = buffPtr; context.captureSpace = maxSize_; context.locked = FALSE; /* Let 'er rip. */ /*Now, we have to spin-wait until we've captured everything (in fact, until the reader sets the locked flag, which amounts to the same thing). */ until = TickCount() + UNLOAD_TIMEOUT; lastPtr = context.capturePtr; done = FALSE; while (!done) { SystemTask(); checkMidi(); /* Just in case. */ if (context.locked) /* Capture finished. */ done = TRUE; if (context.capturePtr != lastPtr) /* Something has arrived recently. reset the timeout. */ { lastPtr = context.capturePtr; until = TickCount() + UNLOAD_TIMEOUT; } if (TickCount() > until) /* Timed out. Clean up and return. */ { diagnostic("timed out during capture\r"); return FALSE; } } *actualSize = context.capturePtr - buffPtr; } void initMidi() { Handle myIcon; MIDIPortParams params; OSErr err; int i; /*Set up the context values. */ context.locked = FALSE; context.destination = NOWHERE; context.error = 0L; context.diagProduce = 0; context.diagConsume = 0; /*Get an ICON (in fact, the beginning of an ICN#) */ myIcon = mustGetResource('ICN#', APPLiconlist); /*Do we have the MIDI Manager installed? */ assert("Can't connect to the MIDI Manager", SndDispVersion(midiToolNum) != 0 ); /*Try to sign in. */ assert("MIDISignIn", MIDISignIn(CREATOR, 0L, myIcon, getApplVersion()) == noErr ); /*Add the input port. */ params.portID = 'In '; params.portType = midiPortTypeInput; params.timeBase = 0; params.offsetTime = 0L; params.readHook = (Ptr) &myEchoHook; params.refCon = SetCurrentA5(); /*I don't think the initClock stuff is important, but I'll set up sensible values anyway. */ params.initClock.sync = midiInternalSync; params.initClock.curTime = 0L; params.initClock.format = midiFormatMSec; copyPascalString(getSTR(INPUTPORTNAMEstr), params.name); err = MIDIAddPort(CREATOR, BUFSIZE, &context.inputRefNum, ¶ms); if (err != noErr && err != midiVConnectMade) die("MIDIAddPort(input): err = %d", err); /*Connect the output ports. Not many of the fields have to change in the params record. */ params.portType = midiPortTypeOutput; params.readHook = 0L; params.refCon = 0L; for (i = 0; i < NUM_OUTPUTS; i++) { params.portID = 'Out1' + i; /* 'Out1', 'Out2' etc. */ copyPascalString(getSTR(OUTPUT1STPORTNAMEstr+i), params.name); err = MIDIAddPort(CREATOR, BUFSIZE, &context.outputRefNum[i], ¶ms ); if (err != noErr && err != midiVConnectMade) die("MIDIAddPort(output): err = %d", err); } } /* patchMeIn(): shouldn't go into the final release, but convenient for testing. Connects up to the Apple MIDI Drivers, and opens the PatchBay desk accessory. */ static void check(err) OSErr err; { assert("connect failed", err == noErr || err == midiVConnectErr); } void patchMeIn() { /*Open the PatchBay first, for the hell of it. */ (void) OpenDeskAcc("\pPatchBay"); /* DOESN'T DO ANYTHING! WHY NOT? */ /*Both real input ports to my single input port: */ check(MIDIConnectData('amdr', 'Ain ', CREATOR, 'In ')); check(MIDIConnectData('amdr', 'Bin ', CREATOR, 'In ')); /*Each of my output ports to the corresponding real output: */ check(MIDIConnectData(CREATOR, 'Out1', 'amdr', 'Aout')); check(MIDIConnectData(CREATOR, 'Out2', 'amdr', 'Bout')); } void stopMidi() { MIDISignOut(CREATOR); } Nick Rothwell, Laboratory for Foundations of Computer Science, Edinburgh. nick@lfcs.ed.ac.uk <Atlantic Ocean>!mcvax!ukc!lfcs!nick ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ Fais que ton reve soit plus long que la nuit.