123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582 |
- unit MIDI;
- {$ifdef fpc}
- {$mode objfpc}{$H+}
- {$endif}
- { *** You can choose if You want MIDI.PAS to use exceptions *** }
- { *** in case of Errors. If not, MIDI.PAS uses POSTMESSAGE() *** }
- { *** and You have to handle these messages in Your MainForm *** }
- //{$define MIDI_UseExceptions}
- //{$define DebugSysEx}
- {*******************************************************************************
- * *
- * MIDI.PAS *
- * *
- * This file is based on the MIDI device classes by Adrian Meyer *
- * This file was taken from the ZIP archive 'demo_MidiDevices_D6.zip' *
- * and partly changed by me, some changes take over from 'DAV_MidiIO.pas' *
- * *
- * latest changes 2015-04-13 *
- ********************************************************************************
- * V1.0 First release with simple MIDI Input/Output *
- * V1.1 SysEx Input Event added, refactured error handling *
- * V1.2 SysEx Output procedure added, changes sysex input for multiple ports *
- * V1.3 Changes by BREAKOUTBOX 2009-07 (www.breakoutbox.de) *
- * V1.4 Changes adapted from DAV_MidiIO.pas - see http://www.ohloh.net/p/DAV *
- * V1.5 removed an Exception on sending SysEx Data to a CLOSED Output *
- * V1.6 added a Switch to choose between Exceptions and Other behaviore ... *
- * V1.7 replaced pChar => pAnsiChar Char => AnsiChar string => AnsiString *
- * to gain compatibility to DelphiXE and higher versions .. *
- * - *
- *******************************************************************************}
- interface
- uses
- {$ifdef FPC}
- Interfaces,
- JWAwindows,
- {$else}
- Windows,
- {$endif}
- Forms, Classes, Messages, SysUtils, Math, Contnrs, MMSystem, Dialogs;
- // -------------------------- WARNING --------------------------
- // If Your Application uses User-defined Messages YOU MUST CHECK
- // if these values collide with one of Your own messages !!!
- const
- // define window messages for MainForm :
- WM_MIDISYSTEM_MESSAGE = WM_USER +1;
- WM_MIDIDATA_ARRIVED = WM_USER +2;
- WM_MIM_ERROR = WM_USER +3;//1001;
- WM_MIM_LONGERROR = WM_USER +4;//1002;
- const
- // define Your size of System Exclusive buffer :
- cMySysExBufferSize = 2048;
- type
- TMIDIChannel = 1..16;
- TMIDIDataByte = 0..$7F; // 7 bits
- TMIDIDataWord = 0..$3FFF; // 14 bits
- TMIDIStatusByte = $80..$FF;
- TMIDIVelocity = TMIDIDataByte;
- TMIDIKey = TMIDIDataByte;
- TMIDINote = TMIDIKey;
- type
- // event if data is received
- TOnMidiInData = procedure (const aDeviceIndex: integer; const aStatus, aData1, aData2: byte) of object;
- // event if system exclusive data is received
- TOnSysExData = procedure (const aDeviceIndex: integer; const aStream: TMemoryStream) of object;
- EMidiDevices = Exception;
- // base class for MIDI devices
- TMidiDevices = class
- private
- fDevices : TStringList;
- fMidiResult : MMResult;
- procedure SetMidiResult(const Value: MMResult);
- protected
- property MidiResult: MMResult read fMidiResult write SetMidiResult;
- function GetHandle(const aDeviceIndex: integer): THandle;
- public
- // create the MIDI devices
- constructor Create; virtual;
- // whack the devices
- destructor Destroy; override;
- // open a specific device
- procedure Open(const aDeviceIndex: integer); virtual; abstract;
- // close a specific device
- procedure Close(const aDeviceIndex: integer); virtual; abstract;
- // close all devices
- procedure CloseAll;
- // THE devices
- function IsOpen(ADeviceIndex: Integer): Boolean; // check if open
- property Devices: TStringList read fDevices;
- end;
- // MIDI input devices
- TMidiInput = class(TMidiDevices)
- private
- fOnMidiData : TOnMidiInData;
- fOnSysExData : TOnSysExData;
- fSysExData : TObjectList;
- protected
- procedure DoSysExData( const aDeviceIndex: integer);
- public
- // create an input device
- constructor Create; override;
- // what the input devices
- destructor Destroy; override;
- // open a specific input device
- procedure Open( const aDeviceIndex: integer); override;
- // close a specific device
- procedure Close( const aDeviceIndex: integer); override;
- // midi data event
- property OnMidiData: TOnMidiInData read fOnMidiData write fOnMidiData;
- // midi system exclusive is received
- property OnSysExData: TOnSysExData read fOnSysExData write fOnSysExData;
- end;
- // MIDI output devices
- TMidiOutput = class(TMidiDevices)
- constructor Create; override;
- // open a specific input device
- procedure Open(const aDeviceIndex: integer); override;
- // close a specific device
- procedure Close(const aDeviceIndex: integer); override;
- // send some midi data to the indexed device
- procedure Send(const aDeviceIndex: integer; const aStatus, aData1, aData2: byte);
- procedure SendSystemReset( const aDeviceIndex: integer);
- procedure SendAllSoundOff( const aDeviceIndex: integer; const channel: byte);
- // send system exclusive data to a device
- procedure SendSysEx( const aDeviceIndex: integer; const aStream: TMemoryStream); overload;
- procedure SendSysEx( const aDeviceIndex: integer; const aString: AnsiString); overload;
- end;
- // convert the stream into xx xx xx xx AnsiString
- function SysExStreamToStr( const aStream: TMemoryStream): AnsiString;
- // fill the AnsiString in a xx xx xx xx into the stream
- procedure StrToSysExStream( const aString: AnsiString; const aStream: TMemoryStream);
- // MIDI input devices
- function MidiInput: TMidiInput;
- // MIDI output Devices
- function MidiOutput: TMidiOutput;
- type
- TSysExBuffer = array[0..cMySysExBufferSize -1] of AnsiChar;
- TSysExData = class
- private
- fSysExStream: TMemoryStream;
- public
- SysExHeader: {$ifdef FPC} _midihdr {$else} TMidiHdr {$endif};
- SysExData: TSysExBuffer;
- constructor Create;
- destructor Destroy; override;
- property SysExStream: TMemoryStream read fSysExStream;
- end;
- implementation
- { ***** TMidiBase ************************************************************ }
- constructor TMidiDevices.Create;
- begin
- FDevices:= TStringList.Create;
- end;
- destructor TMidiDevices.Destroy;
- begin
- FreeAndNil( FDevices);
- inherited;
- end;
- var
- gMidiInput: TMidiInput;
- gMidiOutput: TMidiOutput;
- function MidiInput: TMidiInput;
- begin
- if not Assigned( gMidiInput)
- then gMidiInput := TMidiInput.Create;
- Result := gMidiInput;
- end;
- function MidiOutput: TMidiOutput;
- begin
- if not Assigned( gMidiOutput)
- then gMidiOutput := TMidiOutput.Create;
- Result := gMidiOutput;
- end;
- {$IFDEF FPC}
- //type
- // PHMIDIIN = ^HMIDIIN;
- // TMidiOutCaps = TMidiOutCapsA;
- {$ENDIF}
- { I don't know whatfor this RECORD is used in DAV_MidiIO.pas
- but I think this maybe allows renumeration / resorting of MIDI devices
- for use in Your application - I didn't use this until now
- so I didn't take over the implementation in this MIDI.PAS ... !
- TMidiInputDeviceRecord = record
- MidiInput : TMidiInput;
- DeviceNumber : Integer;
- end;
- PMidiInputDeviceRecord = ^TMidiInputDeviceRecord;
- }
- { The Callback-Procedure receives MIDI data on interrupt : }
- procedure MidiInCallback( aMidiInHandle: PHMIDIIN; aMsg: Integer; aInstance,
- aMidiData, aTimeStamp: integer); stdcall;
- begin
- case aMsg of
- MIM_DATA:
- begin
- if Assigned(MidiInput.OnMidiData) then
- begin
- MidiInput.OnMidiData( aInstance,
- aMidiData and $000000FF,
- ( aMidiData and $0000FF00) shr 8,
- ( aMidiData and $00FF0000) shr 16);
- PostMessage( Application.MainForm.Handle, WM_MIDIDATA_ARRIVED, aInstance, aMidiData);
- end;
- end;
- MIM_LONGDATA:
- MidiInput.DoSysExData( aInstance);
- MIM_ERROR: PostMessage( Application.MainForm.Handle, WM_MIM_ERROR, aInstance, aMidiData);
- MIM_LONGERROR:
- {$ifdef MIDI_UseExceptions}
- raise Exception.Create( 'Midi In Error!');
- {$else} // in a Callback Function you CANNOT use a MessageBox() !!!
- PostMessage( Application.MainForm.Handle, WM_MIM_LONGERROR, aInstance, aMidiData);
- {$endif}
- end;
- end;
- { ***** TMidiInput *********************************************************** }
- constructor TMidiInput.Create;
- var
- AvailableMIDIinputs : integer;
- lInCaps : TMidiInCaps;
- i : integer;
- begin
- inherited;
- fSysExData := TObjectList.Create(true);
- TRY // TRY..EXCEPT was adapted from file "DAV_MidiIO.pas"
- AvailableMIDIinputs:= MidiInGetNumDevs;
- EXCEPT
- AvailableMIDIinputs:= 0;
- end;
- if AvailableMIDIinputs > 0 then
- for i:= 0 to AvailableMIDIinputs - 1 do
- begin
- MidiResult := midiInGetDevCaps(i, @lInCaps, SizeOf(TMidiInCaps));
- if MidiResult = MMSYSERR_NOERROR then
- begin
- fDevices.Add(StrPas(lInCaps.szPname));
- fSysExData.Add(TSysExData.Create);
- end;
- end;
- end;
- destructor TMidiInput.Destroy;
- begin
- FreeAndNil( fSysExData);
- inherited;
- end;
- procedure TMidiInput.Close( const aDeviceIndex: integer);
- begin
- if GetHandle( aDeviceIndex) <> 0 then
- begin
- MidiResult := midiInStop(GetHandle(aDeviceIndex));
- MidiResult := midiInReset(GetHandle(aDeviceIndex));
- MidiResult := midiInUnprepareHeader(GetHandle(aDeviceIndex),
- @TSysExData(fSysExData[aDeviceIndex]).SysExHeader, SizeOf(TMidiHdr));
- MidiResult := midiInClose(GetHandle(aDeviceIndex));
- FDevices.Objects[aDeviceIndex] := nil;
- end;
- end;
-
- procedure TMidiDevices.CloseAll;
- var
- i : integer;
- begin
- for i:= 0 to FDevices.Count - 1 do Close(i);
- end;
- procedure TMidiInput.Open( const aDeviceIndex: integer);
- var
- lSysExData : TSysExData;
- lHandle : THandle;
- begin
- if GetHandle(aDeviceIndex) <> 0 then Exit;
- MidiResult := midiInOpen( @lHandle, aDeviceIndex, cardinal(@midiInCallback),
- aDeviceIndex, CALLBACK_FUNCTION);
- fDevices.Objects[ aDeviceIndex ] := TObject(lHandle);
- lSysExData := TSysExData(fSysExData[aDeviceIndex]);
- lSysExData.SysExHeader.dwFlags := 0;
- // DRAGONS: why are the function returns not checked on errors here ?
- MidiResult := midiInPrepareHeader(lHandle, @lSysExData.SysExHeader, SizeOf(TMidiHdr));
- MidiResult := midiInAddBuffer( lHandle, @lSysExData.SysExHeader, SizeOf(TMidiHdr));
- MidiResult := midiInStart( lHandle);
- end;
- { ***** TMidiInput - SysEx *************************************************** }
- procedure TMidiInput.DoSysExData( const aDeviceIndex: integer);
- var
- lSysExData : TSysExData;
- begin
- lSysExData := TSysExData(fSysExData[aDeviceIndex]);
- if lSysExData.SysExHeader.dwBytesRecorded = 0 then Exit;
- lSysExData.SysExStream.Write( lSysExData.SysExData,
- lSysExData.SysExHeader.dwBytesRecorded);
- if lSysExData.SysExHeader.dwFlags and MHDR_DONE = MHDR_DONE then
- begin
- lSysExData.SysExStream.Position := 0;
- if assigned(fOnSysExData)
- then fOnSysExData(aDeviceIndex, lSysExData.SysExStream);
- lSysExData.SysExStream.Clear;
- end;
- lSysExData.SysExHeader.dwBytesRecorded := 0;
- // DRAGONS: why not check function returns on errors here ?
- MidiResult := MidiInPrepareHeader( GetHandle(aDeviceIndex),
- @lSysExData.SysExHeader, SizeOf(TMidiHdr));
- MidiResult := MidiInAddBuffer( GetHandle(aDeviceIndex), @lSysExData.SysExHeader,
- SizeOf( TMidiHdr));
- end;
- { ***** TMidiOutput ********************************************************** }
- constructor TMidiOutput.Create;
- var
- AvailableMIDIoutputs : integer;
- lOutCaps : TMidiOutCaps;
- i : integer;
- begin
- inherited;
- TRY
- AvailableMIDIoutputs := MidiOutGetNumDevs;
- EXCEPT
- AvailableMIDIoutputs := 0;
- end;
- //ShowMessage( 'DEBUG - AvailableMIDIoutputs = ' +IntToStr( AvailableMIDIoutputs));
- for i:= 0 to AvailableMIDIoutputs - 1 do
- begin
- MidiResult := MidiOutGetDevCaps( i, @lOutCaps, SizeOf(TMidiOutCaps));
- fDevices.Add( lOutCaps.szPname);
- end;
- end;
- procedure TMidiOutput.Open( const aDeviceIndex: integer);
- var
- lHandle: THandle;
- begin
- {$ifndef FPC}
- inherited; // Lazarus doesn't like this, so for Delphi only ..
- {$endif}
- // device already open;
- if GetHandle(aDeviceIndex) <> 0 then exit;
- MidiResult := midiOutOpen( @lHandle, aDeviceIndex, 0, 0, CALLBACK_NULL);
- fDevices.Objects[ aDeviceIndex ]:= TObject( lHandle);
- end;
- procedure TMidiOutput.Close( const aDeviceIndex: integer);
- begin
- {$ifndef FPC}
- inherited; // Lazarus doesn't like this, so for Delphi only ..
- {$endif}
- if GetHandle(aDeviceIndex) <> 0 then // 'if .. then' added by BREAKOUTBOX 2009-07-15
- begin
- MidiResult := midiOutClose(GetHandle(aDeviceIndex));
- fDevices.Objects[ aDeviceIndex ] := nil;
- end;
- end;
- procedure TMidiOutput.Send( const aDeviceIndex: integer; const aStatus,
- aData1, aData2: byte);
- var
- lMsg: cardinal;
- begin
- // open if the device is not open // NOW: do NOT open .. !
- if not Assigned( fDevices.Objects[ aDeviceIndex ])
- then exit; // Open( aDeviceIndex); // Breakoutbox changed 2008-07-01
- //lMsg := aStatus + (aData1 * $100) + (aData2 * $10000);
- lMsg:= aStatus or (aData1 shl 8) or (aData2 shl 16); // better ?
- MidiResult := MidiOutShortMsg( GetHandle( aDeviceIndex), lMSG);
- end;
- { --- common MIDI Out messages ----------------------------------------------- }
- { System Reset = Status Byte FFh }
- procedure TMidiOutput.SendSystemReset( const aDeviceIndex: integer);
- begin
- Self.Send( aDeviceIndex, $FF, $0, $0);
- end;
- { All Sound Off = Status + Channel Byte Bnh, n = Channel number }
- { Controller-ID = Byte 78h, 2nd Data-Byte = 00h }
- procedure TMidiOutput.SendAllSoundOff(const aDeviceIndex: integer; const channel: byte);
- begin
- Self.Send( aDeviceIndex, $b0 +channel, $78, $0);
- end;
- // HINT: in a Thread MidiInGetErrorText() makes no sense ..
- // read out the error text in Your main thread / MainForm !
- procedure TMidiDevices.SetMidiResult( const Value: MMResult);
- var
- lError: array[0..MAXERRORLENGTH] of AnsiChar;
- begin
- fMidiResult := Value;
- if fMidiResult <> MMSYSERR_NOERROR then
- if MidiInGetErrorText( fMidiResult, @lError, MAXERRORLENGTH) = MMSYSERR_NOERROR
- {$ifdef MIDI_UseExceptions}
- then raise EMidiDevices.Create(StrPas(lError));
- {$else}
- // in a Thread or a Callback Function you CANNOT use a MessageBox() !!!
- //then ShowMessage( 'ERROR in TMidiDevices.SetMidiResult() Line 409' +#13#13
- // +StrPas( lError) );
- then PostMessage( Application.MainForm.Handle, WM_MIDISYSTEM_MESSAGE, fMidiResult, 0);
- {$endif}
- end;
- function TMidiDevices.GetHandle( const aDeviceIndex: integer): THandle;
- begin
- try
- if not InRange(aDeviceIndex, 0, fDevices.Count - 1) then
- raise EMidiDevices.CreateFmt( '%s: Device index out of bounds! (%d)',
- [ClassName,aDeviceIndex]);
- Result:= THandle(fDevices.Objects[ aDeviceIndex ]);
- except
- Result:= 0;
- end;
- end;
- function TMidiDevices.IsOpen(ADeviceIndex: Integer): boolean;
- begin
- Result := GetHandle(ADeviceIndex) <> 0;
- end;
- { ***** TMidiOutput - SysEx ************************************************** }
- procedure TMidiOutput.SendSysEx( const aDeviceIndex: integer;
- const aString: AnsiString);
- var
- lStream: TMemoryStream;
- begin
- lStream := TMemoryStream.Create;
- try
- StrToSysExStream( aString, lStream);
- SendSysEx( aDeviceIndex, lStream);
- finally
- FreeAndNil( lStream);
- end;
- end;
- procedure TMidiOutput.SendSysEx( const aDeviceIndex: integer;
- const aStream: TMemoryStream);
- var
- lSysExHeader: TMidiHdr;
- begin
- // exit here if DeviceIndex is not open !
- if not assigned(fDevices.Objects[ aDeviceIndex ])
- then exit; // Breakoutbox added this 2013-06-15
- aStream.Position := 0;
- lSysExHeader.dwBufferLength := aStream.Size;
- lSysExHeader.lpData := aStream.Memory;
- lSysExHeader.dwFlags := 0;
- //ShowMessage( 'HEX: ' +SysExStreamToStr( aStream));
- {$ifdef DebugSysEx} ShowMessage( '0 - ' +IntToStr(MidiResult)); {$endif}
- MidiResult := midiOutPrepareHeader(GetHandle(aDeviceIndex), @lSysExHeader, SizeOf(TMidiHdr));
- {$ifdef DebugSysEx} ShowMessage( '1 - ' +IntToStr(MidiResult)); {$endif}
- MidiResult := midiOutLongMsg( GetHandle(aDeviceIndex), @lSysExHeader, SizeOf(TMidiHdr));
- {$ifdef DebugSysEx} ShowMessage( '2 - ' +IntToStr(MidiResult)); {$endif}
- MidiResult := midiOutUnprepareHeader(GetHandle(aDeviceIndex), @lSysExHeader, SizeOf(TMidiHdr));
- {$ifdef DebugSysEx} ShowMessage( '3 - ' +IntToStr(MidiResult)); {$endif}
- {$ifdef DebugSysEx} ShowMessage( '4 - ' +aStream.ReadAnsiString); {$endif}
- end;
- { ***** TSysExData *********************************************************** }
- constructor TSysExData.Create;
- begin
- SysExHeader.dwBufferLength := cMySysExBufferSize;
- SysExHeader.lpData := SysExData;
- fSysExStream := TMemoryStream.Create;
- end;
- destructor TSysExData.Destroy;
- begin
- FreeAndNil( fSysExStream);
- end;
- { ***** Helper Funktions ***************************************************** }
- function SysExStreamToStr(const aStream: TMemoryStream): AnsiString;
- var
- i : integer;
- begin
- Result := '';
- aStream.Position:= 0;
- for i:= 0 to aStream.Size - 1
- do Result := Result + Format( '%.2x ', [ byte(pAnsiChar(aStream.Memory)[i]) ]);
- end;
- procedure StrToSysExStream(const aString: AnsiString; const aStream: TMemoryStream);
- const
- cHex : AnsiString = '123456789ABCDEF';
- var
- lStr : AnsiString;
- i : integer;
- L : integer;
- begin
- // check on errors - added by BREAKOUTBOX 2009-07-30
- L := length( aString);
- if not (L mod 2 = 0) // as HEX every byte must be two AnsiChars long, for example '0F'
- then raise EMidiDevices.Create( 'SysEx string corrupted')
- else if l < 10 // shortest System Exclusive Message = 5 bytes = 10 hex AnsiChars
- then raise EMidiDevices.Create( 'SysEx string too short');
- lStr := StringReplace( AnsiUpperCase( aString), ' ', '', [rfReplaceAll]);
- aStream.Size := Length( lStr) div 2; // ' - 1' removed by BREAKOUTBOX 2009-07-15
- aStream.Position := 0;
- for i:= 1 to aStream.Size do
- pAnsiChar( aStream.Memory)[i-1] :=
- AnsiChar( AnsiPos( lStr[ i*2 - 1], cHex) shl 4 + AnsiPos( lStr[i*2], cHex));
- end;
- initialization
- gMidiInput := nil;
- gMidiOutput := nil;
- finalization
- FreeAndNil( gMidiInput);
- FreeAndNil( gMidiOutput);
- end.
|