[Back to SOUND SWAG index]  [Back to Main SWAG index]  [Original]

{
From: colin.buckley@canrem.com (Colin Buckley)

>> Does anyone have some good Pascal code for 1) real-time MIDI performance
>> on a standard (Roland MPU-401) card; or 2) reading of MIDI data files.

>Nope.  Nobody has any of this, nor does any exist anywhere on the
>Internet, I can guarantee you this.  I have been asking for this for over
>a year now and it just does *not* exist anywhere.

Which question are you answering?

MIDI is very simple stuff.  Playing or reading and writing the files.
Playing just involves tossing the midi messages out a port.  To read or
write, all you need are the MIDI specs, and you shouldn't have any problem
finding those.  But I don't have FTP so I wouldn't really know.

I have "pascal code" for question 1, but it's actually assembler.  I write
all my low level stuff in assembler, then I wrote a pascal shell around it
when someone in the Fido pascal conference wanted to play MIDI.  It should
be in SWAG, never looked though.  I'll include it at the end of this message.
Just a little while ago I saw a pure Turbo Pascal version.

As for question 2, I'll post the pascal code to my MIDI file convertor as it
can read in *certain* MIDI files.  I've only tested it on files created with
ROL2MIDI, which means there single track and only certain events appear.
It converts to a simple format I use in my games.

Look for the source code disk from the Official Sound Blaster Book (I think
that's the title).  It uses nothing but Creative Lab drivers, but it does
have code to read MOD and MIDI files.  I have the MIDI Unit (MIDUNIT), but
I didn't write it, so I won't post it or email it.  It supports more midi
messages then my routines do, but it's also single track.
}
Program MDI2MUS;

Uses
  CRT,Fast,FM,GM;
  { It's obivously not going to compile.  FAST is just my unit of little
    routines, like screen writes, string routines, etc.

    FM and GM have the same functions and procedures for playing sounds
    on those devices as the purpose of the program is to convert a MDI
    composed on an Adlib to something I can play on Adlib or General Midi.
    The PickGMInstrument, lets me pick a GM instrument by hearing both.

    Comment out the sound stuff, and supply your own generic routines.
  }


Const
  MUSID=245;
  MUSTempo=72.8;
  MUSOverflow=0 SHL 4;
  MUSInstrument=1 SHL 4;
  MUSVolume=2 SHL 4;
  MUSNoteOn=3 SHL 4;
  MUSNoteOff=4 SHL 4;
  MUSPitch=5 SHL 4;
  MUSMarker=14 SHL 4;
  MUSEnd=15 SHL 4;

Type
  MUSHeaderRec=Record
    ID:Byte;
    Title:MStr;
  End;

  MUSInsRec=Record
    GMIns:Byte;
    FMIns:FMInstrument;
  End;

  MDIHeaderRec=Record
    ID:Array[0..3] of Char;
    Length:LongInt;
    Format:Word;
    NumTracks:Word;
    Division:Word;
  End;

  MDITrackRec=Record
    ID:Array[0..3] Of Char;
    Length:LongInt;
  End;

Var
  MUS:File;
  MUSHeader:MUSHeaderRec;
  MDI:File;
  MDIHeader:MDIHeaderRec;
  MDITrack:MDITrackRec;
  EndOfTrack:Boolean;
  Next:Byte;
  NewTempo:Real;
  Tempo:LongInt;
  Time:Word;
  Channel:Byte;
  Command:Byte;
  Note:Byte;
  Volume:Byte;
  VolumeArray:Array[0..15] of Byte;
  Pitch:Word;
  FMIns:Instrument;
  MUSIns:MUSInsRec;

  Procedure Error(Msg:String);
  Begin
    Writeln('* '+Msg);
    Halt;
  End;

  Procedure PickGMInstrument(Voice:Byte);
  Var
    Patch:Byte;
    Key:Char;
    FMOn:Boolean;

    Procedure InitMUS;
    Begin
      FM.InitFM;
      GM.InitGM;
    End;

    Procedure ResetMUS;
    Begin
      FM.ResetFM;
      GM.ResetGM;
    End;

    Procedure SetNoteOn(Voice,Note:Byte);
    Begin
      If FMOn then
        FM.SetNoteOn(Voice,Note)
      Else
        GM.GMSetNoteOn(Voice,Note)
    End;

    Procedure SetNoteOff(Voice:Byte);
    Begin
      If FMOn then
        FM.SetNoteOff(Voice)
      Else
        GM.GMSetNoteOn(Voice,Note)
    End;

    Procedure SetVolume(Voice,Volume:Byte);
    Begin
      FM.SetVolume(Voice,Volume);
      GM.GMSetVolume(Voice,Volume);
    End;

  Begin
    FMOn:=True;
    Writeln('Encountered FM Instrument on Channel ',Voice);
    Writeln;
    Writeln('Instructions:');
    Writeln('C     = Middle C Note');
    Writeln('1..0  = Note Scale');
    Writeln('SPACE = Toggle Device');
    Writeln('P     = Change Patch');
    Writeln;
    InitMUS;
    FM.SetInstrument(Voice,@MUSIns.FMIns);
    GMSetInstrument(Voice,MUSIns.GMIns);
    SetVolume(Voice,127);
    Repeat
      Write('Patch: ',MUSIns.GMIns);ClrEol;
      GotoXY(1,WhereY);
      Key:=Upcase(BIOSKey);
      Case Key of
        '1'..'9':Begin
                   SetNoteOn(Voice,54+(Ord(Key)-48));
                   Delay(175);
                   SetNoteOff(Voice);
                 End;
        '0':     Begin
                   SetNoteOn(Voice,65);
                   Delay(175);
                   SetNoteOff(Voice);
                 End;
         SPACEKey:FMOn:=Not FMOn;
         'P':    Begin
                   Writeln;
                   Write('New Patch [0-127]: ');
                   Readln(MUSIns.GMIns);
                   GotoXY(1,WhereY-1);
                   ClrEol;
                   GotoXY(1,WhereY-1);
                   ClrEol;
                   GMSetInstrument(Voice,MUSIns.GMIns);
                 End;
      End;
    Until (Key=ESCKey) or (Key=ENTERKey);
    ResetMUS;
  End;

  Procedure WriteMUS(Time:Byte; Command,Channel:Byte; Var Data; DataSize:Byte);
  Var
    Combined:Byte;
    T:Byte;
  Begin
    { Set Delta Time }
    BlockWrite(MUS,Time,SizeOf(Time));
    ASM
      MOV  AL,[Command]
      AND  AL,11110000b
      MOV  AH,[Channel]
      AND  AH,00001111b
      ADD  AL,AH
      MOV  [Combined],AL
    End;
    { Set Command/Channel Combined Byte }
    BlockWrite(MUS,Combined,SizeOf(Combined));
    { Set Command Data }
    If DataSize>=1 then
      BlockWrite(MUS,Data,DataSize);
  End;

  Function IntelLong(Motorolla:LongInt):LongInt; Assembler;
  ASM
    MOV  AX,[WORD PTR Motorolla]
    MOV  DX,[WORD PTR Motorolla+2]
    XCHG AL,AH
    XCHG DL,DH
    XCHG AX,DX
  End;

  Function IntelWord(Motorolla:Word):Word; Assembler;
  ASM
    MOV  AX,[Motorolla]
    XCHG AL,AH
  End;

  Procedure ReadMDI(MDIFile:LStr);
  Begin
    Assign(MDI,MDIFile);
    Reset(MDI,1);
    If IOResult<>0 then
      Error(MDIFile+' Not Found!');
    BlockRead(MDI,MDIHeader,SizeOf(MDIHeader));
    BlockRead(MDI,MDITrack,SizeOf(MDITrack));
    With MDIHeader do
    Begin
      Length:=IntelLong(Length);
      Format:=IntelWord(Format);
      NumTracks:=IntelWord(NumTracks);
      Division:=IntelWord(Division);
    End;
    With MDITrack do
      Length:=IntelLong(Length);
    If (MDIHeader.ID<>'MThd') or (MDIHeader.Format<>0) or (MDITrack.ID<>'MTrk')
      Error('Invalid Type 0 .MDI File: '+MDIFile);
  End;

  Procedure CreateMUS(MUSFile:LStr);
  Var
    Temp:Byte;
  Begin
    FillChar(MUSHeader,SizeOf(MUSHeader),0);
    MUSHeader.ID:=MUSID;
    Write('Enter Title: ');
    Readln(MUSHeader.Title);
    Assign(MUS,MUSFile);
    Rewrite(MUS,1);
    BlockWrite(MUS,MUSHeader,SizeOf(MUSHeader));
  End;

  Procedure ConvertMDI;

    Procedure DoDeltaTime;
    Var
      VarLength:LongInt;
    Begin
      BlockRead(MDI,Next,1);
      VarLength:=Next;
      If (Next And $80)=$80 then
      Begin
        VarLength:=VarLength And $7F;
        Repeat
          BlockRead(MDI,Next,1);
          VarLength:=(VarLength Shl 7) + (Next And $7F)
        Until (Next And $80)<>$80;
      End;
      Time:=Trunc(VarLength*NewTempo);
      If Time>255 then
      Begin
        WriteMUS(0,MUSOverflow,0,Time,SizeOf(Time));
        Time:=0;
      End;
    End;

    Procedure DoSysExEvent;
    Begin

    End;

    Procedure DoMIDIEvent;
    Begin
      Channel:=Command And $F;
      Command:=Command And $F0;
      Case Command of
        { Note Off }
        $80:Begin
              BlockRead(MDI,Note,1);
              BlockRead(MDI,Volume,1);
              WriteMUS(Time,MUSNoteOff,Channel,Note,0);
            End;
        { Note On }
        $90:Begin
              BlockRead(MDI,Note,1);
              BlockRead(MDI,Volume,1);
              { If Volume=0 it's the same as a NoteOff }
              If Volume=0 then
                WriteMUS(Time,MUSNoteOff,Channel,Note,0)
              Else
              Begin
                { Update Volume if different then previous }
                If VolumeArray[Channel]<>Volume then
                Begin
                  VolumeArray[Channel]:=Volume;
                  WriteMUS(Time,MUSVolume,Channel,Volume,SizeOf(Volume));
                  Time:=0;
                End;
                WriteMUS(Time,MUSNoteOn,Channel,Note,SizeOf(Note));
              End;
            End;
        { Volume Change / Channel Pressure / After Touch }
        $D0:Begin
              BlockRead(MDI,Volume,1);
            End;
        { Pitch Change }
        $E0:Begin
              BlockRead(MDI,Pitch,2);
              Pitch:=IntelWord(Pitch);
              WriteMUS(Time,MUSPitch,Channel,Pitch,SizeOf(Pitch));
            End;
        Else
          Writeln('Unknown MIDI event!');
      End;
    End;

    Procedure DoMetaEvent;
    Var
      Length:Byte;
      FPos:LongInt;
      ID:Array[0..4] of Byte;
    Begin
      BlockRead(MDI,Command,1);
      BlockRead(MDI,Length,1);
      FPos:=FilePos(MDI);
      Case Command of
        $2F:Begin
              EndOfTrack:=True;
              WriteMUS(0,MUSEnd,0,Note,0);
            End;
        { Tempo }
        $51:Begin
              BlockRead(MDI,Next,1);
              Tempo:=Next*65536;
              BlockRead(MDI,Next,1);
              Inc(Tempo,Word(Next*256));
              BlockRead(MDI,Next,1);
              Inc(Tempo,Next);
              NewTempo:=MUSTempo/(MDIHeader.Division*(1000000/Tempo));
            End;
        { Sequencer Specific }
        $7F:Begin
              BlockRead(MDI,ID,SizeOf(ID));
              { Adlib ID }
              If (ID[0]=$00) And (ID[1]=$00) And (ID[2]=$3F) then
                Case Ord(ID[4]) of
                  { Instrument }
                  1:Begin
                      BlockRead(MDI,Channel,1);
                      BlockRead(MDI,FMIns,SizeOf(FMIns));
                      ConvertInstrument(FMIns,MUSIns.FMIns);
                      PickGMInstrument(Channel);
                      WriteMUS(Time,MUSInstrument,Channel,MUSIns
SizeOf(MUSIns))
                    End;
                  { Melodic or Percussion Mode }
                  2:BlockRead(MDI,Next,1);
                  { Waveforms }
                  3:BlockRead(MDI,Next,1);
                End;
            End;
      End;
      Seek(MDI,FPos+Length);
    End;

  Begin
    EndOfTrack:=False;
    Time:=0;
    NewTempo:=500000;
    FillChar(VolumeArray,SizeOf(VolumeArray),0);
    While Not (EOF(MDI) And EndOfTrack) do
    Begin
      { Get Time of Event }
      DoDeltaTime;
      { Get Event from Midi Stream }
      BlockRead(MDI,Command,1);
      Case Command of
        $80..$EF:DoMidiEvent;
        {
        $F0,$F7: DoSysExEvent;
        }
        $FF:     DoMetaEvent;
      Else
        Writeln('Unknown Event in MIDI Stream!');
      End;
    End;
    Close(MDI);
    Close(MUS);
  End;

Begin
  Writeln('+-----------------------  +++++-++|+++++ +++++++++-----------------
  Writeln('|                         ++++-++++++++- +++++++++-
  Writeln('|
  Writeln('|                              Music Conversion
  Writeln('|
  Writeln('|              Copyright 1992 by Absolute Magic, Inc and Colin Buckle
  Writeln('+--------------------------------------------------------------------
  Writeln;
  If ParamCount<2 then
  Begin
    Writeln;
    Writeln('USAGE: MUS <MDIFile> <MUSFile>');
    Writeln;
    Writeln('<MDIFile> is created by converting a .ROL with ROL2MIDI.EXE');
    Writeln;
    Exit;
  End;
  ReadMDI(ParamStr(1));
  CreateMUS(ParamStr(2));
  ConvertMDI;
  Writeln;
  If Not EndOfTrack then
    Writeln('* Unsuccessful Conversion.  End Of Track Not Encountered.')
  Else
    Writeln('* Successful Conversion');
End.





-----------------------------------------------------------------------------

Program GMTest;

{
Public domain.  Do whatever you want with it.
Colin Buckley.
}

Const
  GMPort        = $331;
  Send          = $80;
  Receive       = $40;

{ AL:=Command; }
Procedure WriteGMCommand; Assembler;
ASM
    MOV   DX,GMPort                   {;DX:=GMStatusPort;                 }
    PUSH  AX                          {;Save AX                           }
    XOR   AX,AX                       {;AH:=TimeOutValue;                 }
@@WaitLoop:
    { ;Prevent Infinite Loop with Timeout }
    DEC   AH                          {; |If TimeOutCount=0 then          }
    JZ    @@TimeOut                   {;/   TimeOut;                      }
    {; Wait until GM is ready }
    IN    AL,DX                       {; |If Not Ready then               }
    AND   AL,Receive                  {; |  WaitLoop;                     }
    JNZ   @@WaitLoop                  {;/                                 }
@@TimeOut:
    POP   AX                          {;Restore AX                        }

    OUT   DX,AL                       {;Send Data                         }
End;

{ ; AL:=Data }
Procedure WriteGM; Assembler;
ASM
    MOV   DX,GMPort                   {;DX:=GMStatusPort;                 }
    PUSH  AX                          {;Save AX                           }
    XOR   AX,AX                       {;AH:=TimeOutValue;                 }
@@WaitLoop:
    { ; Prevent Infinite Loop with Timeout }
    DEC   AH                          {; |If TimeOutCount=0 then          }
    JZ    @@TimeOut                   {;/   TimeOut;                      }
    { ; Wait until GM is ready }
    IN    AL,DX                       {; |If Not Ready then               }
    AND   AL,Receive                  {; |  WaitLoop;                     }
    JNZ   @@WaitLoop                  {;/                                 }
@@TimeOut:
    POP   AX                          {;Restore AX                        }

    DEC   DX                          {;DX:=DataPort                     }
    OUT   DX,AL                       {;Send Data                        }
End;

{ ;Returns Data }
Function ReadGM:Byte; Assembler;
ASM
    MOV   DX,GMPort                   {;DX:=GMStatusPort;                 }
    PUSH  AX                          {;Save AX                           }
    XOR   AX,AX                       {;AH:=TimeOutValue;                 }
@@WaitLoop:
    { ; Prevent Infinite Loop with Timeout }
    DEC   AH                          {; |If TimeOutCount=0 then          }
    JZ    @@TimeOut                   {;/   TimeOut;                      }
    { ; Wait until GM is ready }
    IN    AL,DX                       {; |If Not Ready then               }
    AND   AL,Send                     {; |  WaitLoop;                     }
    JNZ   @@WaitLoop                  {;/                                 }
@@TimeOut:
    POP   AX                          {;Restore AX                        }

    DEC   DX                          {;DX:=DataPort                      }
    IN    AL,DX                       {;Receive Data                      }
End;

Procedure ResetGM; Assembler;
ASM
    { ;Reset GM }
    MOV   DX,GMPort
    MOV   AL,0FFh
    OUT   DX,AL
    {; Get ACK }
    CALL  ReadGM
    {; UART Mode }
    MOV   AL,03Fh
    CALL  WriteGMCommand
End;

Procedure SetNoteOn(Channel,Note,Volume:Byte); Assembler;
ASM
    MOV   AL,[Channel]
    ADD   AL,90h
    Call  WriteGM
    MOV   AL,[Note]
    CALL  WriteGM
    MOV   AL,[Volume]
    CALL  WriteGM
End;

Procedure SetNoteOff(Channel,Note,Volume:Byte); Assembler;
ASM
    MOV   AL,[Channel]
    ADD   AL,80h
    Call  WriteGM
    MOV   AL,[Note]
    CALL  WriteGM
    MOV   AL,[Volume]
    CALL  WriteGM
End;

Begin
  ResetGM;
  SetNoteOn(0,64,127);
  ASM
    { ;Wait for Key }
    XOR   AX,AX
    INT   16h
  End;
  SetNoteOff(0,64,127);
  ResetGM;
End.

[Back to SOUND SWAG index]  [Back to Main SWAG index]  [Original]