( 
*************************************************************************
*			       MPU401.PCF				*
*									*
*	MIDI Record/Playback using Roland MPU401 MIDI Interface 	*
*									*
*	       Copyright Donald Swearingen, January 1985		*
*		 For private, non-commercial use only.			*
*************************************************************************

Note: The following program segments are written in Laboratory Microsystems 
PC/FORTH for the IBM PC. They were prepared using a standard text editor
and compiled into the FORTH dictionary using the command:

			INCLUDE MPU401.PCF
)

FORTH DEFINITIONS HEX

(
********************
* Data Definitions *
********************
)
0330	CONSTANT	MPU_DATA	( IO Port Addresses for MPU_401)
0331	CONSTANT	MPU_STAT
0331	CONSTANT	MPU_CMND

0010	CONSTANT	PGPH_SIZE	( 8086 paragraph size )
1000	CONSTANT	T_SIZE		( "Track" data buffer size )

0008	CONSTANT	N_TK		( Number of tracks )

CREATE	T_SEG		N_TK 2* ALLOT	( segment pointers for track data )
CREATE	T_RPTR		N_TK 2* ALLOT	( Record data pointers )
CREATE	T_PPTR		N_TK 2* ALLOT	( Play data pointers )
CREATE	T_RST		N_TK 2* ALLOT	( Record/play running status )
CREATE	T_NDAT		N_TK 2* ALLOT	( # data bytes for current status )

VARIABLE		ACK_RCVD	( Set to 1 when command ACK received )
2VARIABLE		PREV_IRQ2	( original IRQ2 vector contents )
VARIABLE		REC_TRK 	( Current record track 0-7 )
VARIABLE		MPU_VEC 	( Current MPU character handler )
VARIABLE		MPU_VEC_RST	( Initial MPU character handler )

(
******************************
* Auxiliary PC-DOS functions *
******************************
)

( free up memory paragraphs not used by FORTH
  note: FORTH uses CS:0000 thru SS:FFFF 
)
CODE	FREE_MEM			( --- n_avail )
	ES		PUSH		( Preserve registers )
	CS		PUSH
	AX		POP
	ES, AX		MOV
	BX, # FFFF	MOV		( Request maximum )
	AH, # 4A	MOV		( SETBLOCK function )
	21		INT		( Get max. avail. in BX )
	10$		JNC		( Got all FFFF pgphs )
	AX, # 8 	CMP		( Insufficient memory error?)
	5$		JE		( Yes, proceed )
	BX, # 0 	MOV		( No pgphs available )
	10$		JMP
5$:	SS		PUSH
	CX		POP
	CX, # 1000	ADD		( CX = SS:FFFF+1 )
	CS		PUSH
	AX		POP		( AX = CS:0000 )
	ES, AX		MOV
	CX, AX		SUB		( CX = # pgphs used by FORTH )
	BX, CX		SUB		( BX = # pgphs not used by FORTH )
	AH, # 4A	MOV		( SETBLOCK function )
	21		INT
	10$		JNC
	BX, # 0 	MOV		( error...return 0 )
10$:	ES		POP		( restore register )
	BX		PUSH		( return # free pgphs )
	NEXT,
END-CODE

( get memory paragraphs 
) 
CODE	GET_MEM 			( n_req --- seg n_alloc error_code )
	BX		POP		( # paragraphs requested )
	AH, # 48	MOV		( allocate memory function )
	21		INT		( DOS function interrupt ) 
	10$		JC		( Error return )
	AX		PUSH		( segment allocated )
	BX		PUSH		( # paragraphs allocated )
	AX, # 0 	MOV
	AX		PUSH		( no error )
	NEXT,
10$:	CX, # 0 	MOV
	CX		PUSH		( dummy segment )
	BX		PUSH		( # paragraphs actually available )
	AX		PUSH		( DOS error code )
	NEXT,
END-CODE
(
********************
* Output to MPU401 *
********************
)

( Wait until MPU401 is ready to receive data 
)
: MPU_TX_WAIT				( --- )
  BEGIN MPU_STAT PC@ 40 AND NOT UNTIL
;
( CALLable code version 
)
CODE	S_MPU_TX_WAIT 
	DX, # MPU_STAT	MOV		( MPU status port )
1$:	AL, DX		IN		( read status )
	AL, # 40	AND		( wait for bit 6 = 0 )
	1$		JNZ
	RET
END-CODE

( Send command to MPU401
)
: !MPU_CMND				( cmnd --- )
  0 ACK_RCVD !				( reset MPU ack flag ) 
  MPU_TX_WAIT MPU_CMND PC!		( send character )
  BEGIN ACK_RCVD @ UNTIL		( wait for ACK )
;
( Send command in AL to MPU401. CALLable code version. 
)
CODE	S_!MPU_CMND 
	AX PUSH 			( save command )
	' S_MPU_TX_WAIT >BODY CALL      ( wait for tx ready )
	AX, AX		XOR		( AX = 0)
	ACK_RCVD , AX	MOV		( reset MPU ack flag )
	DX, # MPU_CMND	MOV		( MPU command port )
	AX		POP		( Retrieve command )
	DX, AL		OUT		( Send command )
1$:	AX, ACK_RCVD	MOV		( Wait for command ACK )
	AX, # FFFF	AND
	1$		JZ	
	RET
END-CODE

( Send data byte to MPU401 
)
: !MPU_DATA				( data --- )
  MPU_TX_WAIT MPU_DATA PC!  
;
( Send data byte in AL to MPU401. CALLable code version. 
)
CODE	S_!MPU_DATA			
	AX		PUSH		( save character ) 
	' S_MPU_TX_WAIT >BODY CALL      ( Wait for tx ready)
	DX, # MPU_DATA	MOV		( MPU Data port )
	AX		POP		( Retrieve data )
	DX, AL		OUT		( Send data )
	RET
END-CODE

(
**************************
* IRQ2 interrupt handler *
**************************
(

( Begin interrupt: save registers and link to current character
  handling vector 
)
CODE	IRQ2_INT	
	STI				( enable interrupts )
	AX		PUSH		( save environment )
	BX		PUSH 
	CX		PUSH
	DX		PUSH 
	DS		PUSH 
	ES		PUSH
	SI		PUSH
	AX, CS		MOV		( establish data accessibility )  
	DS, AX		MOV  
	DX, # MPU_DATA	MOV		( read serial port)
	AL, DX		IN
	BX, MPU_VEC MOV
	BX		JMP		( jump to character handler )
END-CODE

( Signal end of interrupt and return from interrupt 
)
CODE	IRQ2_INT_END 
	AL, # 20	MOV		( send EOI to 8259 )
	# 20 , AL	OUT
	SI		POP		( restore environment ) 
	ES		POP  
	DS		POP  
	DX		POP 
	CX		POP  
	BX		POP  
	AX		POP
	IRET		      
END-CODE

(
*****************************
* Record/Playback functions *
*****************************
)

( Determine the number of data bytes associated with the MIDI
  status byte in CL and store for the currently active record 
  or playback track [SI]. Input CL destroyed.
)
CODE	SET_T_NDAT
	CL, # C0	CMP
	1$		JB
	CL, # DF	CMP
	1$		JA
	CX, # 1 	MOV
	T_NDAT [SI], CX MOV
	RET
1$:	CX, # 2 	MOV
	T_NDAT [SI], CX MOV
	RET
END-CODE

( place character in record buffer of current track
)
CODE	MPU_RECD_PUT 
	CLI				( clear interrupts for)
					( pointer manipulation)
	BX, T_SEG [SI]	MOV		( get trk seg in ES )
	ES, BX		MOV
	BX, T_RPTR [SI] MOV		( track record pointer )
	BX, # T_SIZE	CMP		( buffer full? )
	1$		JB
	STI				( Yes...don't store )
	RET
1$:	ES: [BX], AL	MOV		( place char. in track buffer )
	BX		INC		( bump record pointer)
15$:	T_RPTR [SI], BX MOV		( store updated ptr )
	STI				( enable interrupts)
	RET
END-CODE

( record data bytes of current MIDI message
)
CODE	MPU_RECD_3
	SI, REC_TRK	MOV		( Get record trk in SI )
	SI, # 1 	SHL		( Convert to word offset )
	' MPU_RECD_PUT >BODY CALL       ( store value )
	T_NDAT [SI]	DEC
	10$		JNZ
	BX, MPU_VEC_RST MOV
	MPU_VEC , BX	MOV		( reset char vector )
10$:	' IRQ2_INT_END >BODY JMP        ( leave )
END-CODE

( process second byte of current track event
) 
CODE	MPU_RECD_2
	SI, REC_TRK	MOV		( Get record trk in SI )
	SI, # 1 	SHL		( Convert to word offset )
	AL, # F8	CMP		( check for MPU marks )
	3$		JE
	AL, # F9	CMP
	3$		JE
	AL, # FC	CMP
	5$		JNE
3$:	' MPU_RECD_PUT >BODY CALL       ( store mark )
	BX, MPU_VEC_RST MOV
	MPU_VEC , BX	MOV		( reset char vector )
	' IRQ2_INT_END >BODY JMP
5$:	BX, # ' MPU_RECD_3 >BODY MOV    ( set char vec for MIDI data bytes )
	MPU_VEC , BX	MOV
	CL, T_RST [SI]	MOV
	' SET_T_NDAT >BODY CALL         ( compute # data bytes for status )
	AL, # 80	CMP		( new status?)
	8$		JAE
	' MPU_RECD_3 >BODY JMP
8$:	T_RST [SI], AL	MOV		( remember new running status)
	' MPU_RECD_PUT >BODY CALL       ( store value in track buffer )
	CX, AX		MOV
	' SET_T_NDAT >BODY CALL         ( compute # data bytes for status )
	' IRQ2_INT_END >BODY JMP
END-CODE

( record timing value for current track event
)
CODE	MPU_RECD	
	SI, REC_TRK	MOV		( Get record trk in SI )
	SI, # 1 	SHL		( Convert to word offset )
	' MPU_RECD_PUT >BODY CALL       ( Store timing value )
	AL, # F8	CMP		( timing overflow? )
	1$		JE		( Yes...one byte event )
	BX, # ' MPU_RECD_2 >BODY MOV
	MPU_VEC , BX	MOV		( set char. vector for second byte )
1$:	' IRQ2_INT_END >BODY JMP
END-CODE

( handle play request 
)
CODE	MPU_PLAY
	AX, # 07	AND		( ensure trk 0-7)
	AX, # 1 	SHL		( cnvt to word offset )
	SI, AX		MOV
	BX, T_PPTR [SI] MOV
	CX, T_RPTR [SI] MOV
	BX, CX		CMP		( Anything left ?)
	1$		JB		( Yes...send next event )
	AL, # 0 	MOV		( No...send data end FC )
	' S_!MPU_DATA >BODY CALL
	AL, # FC	MOV
	' S_!MPU_DATA >BODY CALL
	10$		JMP
1$:	AX, T_SEG [SI]	MOV		( Get trk seg in ES )
	ES, AX		MOV
	ES: AL, [BX]	MOV		( Get timing value )
	' S_!MPU_DATA >BODY CALL        ( send timing value )
	BX		INC
	AL, # F8	CMP		( Timing value = 240? )
	10$		JE		( Yes, EXIT )
	ES: AL, [BX]	MOV		( Check next character )
	AL, # F8	CMP		( MPU mark?)
	3$		JE
	AL, # F9	CMP
	3$		JE
	AL, # FC	CMP
	5$		JNE		( Not mark )
3$:	BX		INC		( "read" mark )
	' S_!MPU_DATA >BODY CALL        ( send mark )
	10$		JMP		( EXIT )
5$:	AL, # 80	CMP		( New status? )
	7$		JB
	T_RST [SI], AL	MOV		( Reset running status )
	BX		INC		( "read" status )
	' S_!MPU_DATA >BODY CALL        ( send status )
7$:	CL, T_RST [SI]	MOV
	' SET_T_NDAT >BODY CALL
9$:	ES: AL, [BX]	MOV		( Read next data byte )
	BX		INC
	' S_!MPU_DATA >BODY CALL        ( send data byte )
	T_NDAT [SI]	DEC
	9$		JNZ
10$:	T_PPTR [SI], BX MOV		( Reset play pointer )
	' IRQ2_INT_END >BODY JMP
END-CODE

(
*************************
* Interpret MPU message *
*************************
)

( Begin processing a new MPU message
)
CODE	MPU_MSG
	AL, # FE	CMP		( command ack? )
	1$		JNE
	ACK_RCVD , AX	MOV		( Yes, signal ack received )
	' IRQ2_INT_END >BODY JMP        ( leave )
1$:	AL, # EF	CMP		( timing value? )
	4$		JA
	' MPU_RECD >BODY JMP            ( Yes, record data )
4$:	AL, # F8	CMP		( timing overflow? )
	7$		JNE
	' MPU_RECD >BODY JMP            ( Yes, record data )    
7$:	AL, # F7	CMP		( Play request? )
	10$		JA
	' MPU_PLAY >BODY JMP            ( Yes, play data )
10$:	' IRQ2_INT_END >BODY JMP                ( Ignore F9-FF )
END-CODE

(
***************************
* Interrupt control words *
***************************
)

( save previous values and initialize IRQ2 interrupt vectors
)
: IRQ2_TRAP				( --- )
  ['] MPU_MSG >BODY DUP
  MPU_VEC ! MPU_VEC_RST !
  0 28 2@L  PREV_IRQ2 2!		( save old IRQ2 vector )
  ?CS: 0 2A !L				( segment of handler )
  ['] IRQ2_INT >BODY 0 28 !L            ( offset of handler )
;
( restore previous IRQ2 vectors
)
: IRQ2_REL				( --- )
  PREV_IRQ2 2@ 0 28 2!L
;
( enable interrupts on IRQ2
)
: IRQ2_ENABLE				( --- )
  21 PC@ 0FB AND 21 PC! 		( 8259 int mask IRQ2 )
;		 
( disable IRQ2 interrupts
)
: IRQ2_DISABLE				( --- )
  21 PC@ 04 OR 21 PC!			( mask IRQ2 interrupt )
;
( enable interrupts, set up vectors 
)
: IRQ2_ON				( --- )
  IRQ2_TRAP IRQ2_ENABLE 
;
( disable interrupts, release vectors 
)
: IRQ2_OFF				( --- )
  IRQ2_DISABLE	IRQ2_REL 
;
(
************************
* MPU401 control words *
************************
)

: RECD+ 	22 !MPU_CMND ;		( start record process )
: RECD- 	11 !MPU_CMND ;		( stop record process )
: PLAY+ 	0A !MPU_CMND ;		( start playback process )
: PLAY- 	05 !MPU_CMND ;		( stop playback process )
: OVDB+ 	2A !MPU_CMND ;		( start overdub process )
: OVDB- 	15 !MPU_CMND ;		( stop overdub process )
: MPU_RESET	FF !MPU_CMND ;		( system reset )
: THRU- 	33 !MPU_CMND ;		( disable MIDI thru function )
: RT_OUT-	32 !MPU_CMND ;		( disable MIDI real-time output ) 
: T_BYTE+	34 !MPU_CMND ;		( enable leading timing byte in
					  stop mode )
: RT_TO_HOST+	39 !MPU_CMND ;		( enable real-time to host )
: INT_CLK	80 !MPU_CMND ;		( use internal clock )
: FSK_CLK	81 !MPU_CMND ;		( use FSK clock from tape )
: MIDI_CLK	82 !MPU_CMND ;		( use MIDI clock )
: MET_ON	83 !MPU_CMND ;		( metronome on )
: MET_OFF	84 !MPU_CMND ;		( metronome off )
: MET_ON_ACC	85 !MPU_CMND ;		( metronome on with accent )
: BENDER-	86 !MPU_CMND ;		( allow bender data )
: BENDER+	87 !MPU_CMND ;		( screen bender data )
: MIDI_THRU-	88 !MPU_CMND ;		( enable MIDI thru )
: MIDI_THRU+	89 !MPU_CMND ;		( disable MIDI thru )
: DATA_IN_STP-	8A !MPU_CMND ;		( disable data in stop mode )
: DATA_IN_STP+	8B !MPU_CMND ;		( enable data in stop mode )
: MEAS_END-	8C !MPU_CMND ;		( enable meas. end to host )
: MEAS_END+	8D !MPU_CMND ;		( disable meas. end to host )
: COND- 	8E !MPU_CMND ;		( conductor off )
: COND+ 	8F !MPU_CMND ;		( conductor on )
: RT_AFF-	90 !MPU_CMND ;		( disable real time affection )
: RT_AFF+	91 !MPU_CMND ;		( enable real time affection )
: FSK<INT	92 !MPU_CMND ;		( sync FSK to internal clock )
: FSK<MIDI	93 !MPU_CMND ;		( sync FSK to MIDI clock )
: CLK-		94 !MPU_CMND ;		( disable clock to host )
: CLK+		95 !MPU_CMND ;		( enable clock to host )
: SYSX- 	96 !MPU_CMND ;		( disable system exclusive data )
: SYSX+ 	97 !MPU_CMND ;		( enable system exclusive data )
: TMPO_RST	B1 !MPU_CMND ;		( reset relative tempo )
: CLR_PLY_CTR	B8 !MPU_CMND ;		( clear play counters )
: CLR_PLY_TAB	B9 !MPU_CMND ;		( clear play table )
: CLR_REC_CTR	BA !MPU_CMND ;		( clear record counter )

( request to send MIDI data on specified track
) 
: RTS_DATA				( trk --- )
  D0 + !MPU_CMND 
;
( request to send system exclusive message
)
: RTS_SYSX				( --- )
  DF !MPU_CMND 
;
( set tempo 
)
: SET_TEMPO				( beats/min --- )
  E0 !MPU_CMND !MPU_DATA 
;
( set relative tempo where 40H = 1/1 ratio
)
: REL_TEMPO				( rel --- ) 
  E1 !MPU_CMND !MPU_DATA 
;
( set rate of tempo change, 0 = immediate, 1 = slowest, FF = fastest
)
: GRADUATION				( grad --- )
  E2 !MPU_CMND !MPU_DATA 
;
( send clock to host every rate/4 internal clocks
)
: CLK>HST_RATE				( rate	--- )
  E7 !MPU_CMND !MPU_DATA 
;
( set active playback tracks using 8 bit map
)
: SET_AC_TRK				( trks --- )
  EC !MPU_CMND !MPU_DATA 
;
( send play counter of track when switched from playback to record mode
)
: SND_PLY_CTR+				( trks --- )
  ED  !MPU_CMND !MPU_DATA 
;
( initialize state of MPU_401 
)
: MPU_INIT				( --- )
  MPU_RESET				( reset MPU-401 )
  THRU- MIDI_THRU-			( THRU off )
  T_BYTE+				( enable leading timing byte)
  BENDER-				( pitch bend data off )
  DATA_IN_STP-				( data in STOP off )
  MEAS_END-				( send measure end off )
  RT_AFF-				( real-time affection off )
  SYSX- 				( exclusive to host off )
  CLK-					( clock to host off )
  COND- 				( conductor off )
  60 CLK>HST_RATE			( Set clock to host to MIDI )
					( standard: 24 clks/beat )
  64 SET_TEMPO				( 100 beats per minute )
;
( enable interrupts and initialize MPU401
)
: MPU_ON				( --- )
  IRQ2_ON
  MPU_INIT
;
( disable MPU401 activity
)
: MPU_OFF				( --- )
  IRQ2_OFF
;
(
*********************************
* Record/playback control words *
*********************************
)

( reset record pointer for track n
)
: RESET_RPTR				( n --- )
  2* T_RPTR + 0 SWAP !
;
( reset play pointer for track n
)
: RESET_PPTR				( n --- )
  2* T_PPTR + 0 SWAP !
;
( intialize state of record/playback variables and obtain memory 
  for record/play data buffers 
)
: RECORD_INIT				( --- )
  FREE_MEM DROP
  T_SIZE PGPH_SIZE / N_TK *		( --- n_required_pgphs )
  GET_MEM				( --- seg n_avail_pgphs err_code )
  0<> IF
    2DROP
    ." Not enough memory for record/playback " CR 
    EXIT
  THEN
  DROP					( --- seg )
  T_SIZE PGPH_SIZE /			( --- seg pgphs/trk )
  N_TK 0 DO				( set up pointers to track buffers )
    2DUP I * +
    T_SEG I 2 * + !
  LOOP
  2DROP
  N_TK 0 DO				( reset record/playback pointers )
    I RESET_PPTR
    I RESET_RPTR
  LOOP
;
( record MIDI data on trk n
)
: RECORD				( n --- )
  7 AND REC_TRK !			( set record track )
  REC_TRK @ RESET_RPTR			( reset record pointer)
  RECD+ 				( Start record )
;
: RECORD_OFF	
  RECD-
;
( overdub MIDI data on trk n while playing other tracks
)
: OVERDUB				( n --- )
  7 AND REC_TRK !			( set record track )
  1 REC_TRK @ SHIFT NOT 
  SET_AC_TRK				( all trks active except REC_TRK )
  CLR_PLY_CTR				( clear play counters )
  REC_TRK @ RESET_RPTR			( reset record pointer)
  N_TK 0 DO				( reset play pointers )
    I RESET_PPTR
  LOOP
  OVDB+ 				( Start record )
;
: OVERDUB_OFF	
  OVDB- 
;
( play midi data for all tracks
)
: PLAY					( --- )
  N_TK 0 DO				( reset play pointers )
    I RESET_PPTR
  LOOP
  FF SET_AC_TRK 			( all trks active )
  CLR_PLY_CTR
  PLAY+ 				( start play )
;
: PLAY_OFF
  PLAY- 
;

DECIMAL
OOP
  FF SET_A