[rec.music.synth] MIDI Manager/TC 4.0 code, here it is

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, &params);
	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], &params
						 );
		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.