I know there has been some interest in the past in AHK supporting MIDI I/O. I have a real need that cropped up for MIDI output from AHK, which has turned into my little project from hell for the week. The bottom line is that I finally have it working, which I guess isn't too bad since I hardly new what a DLL call was before Monday. It has been a long and difficult process for me, but I am really excited to have this working, and I know some of you could benefit from this as well.
Below are three scripts. The first and most important contains the MIDI functions, which to a large extent are just wrapped versions of the DLL functions found in winmm.dll. However, in many cases, my functions incorporate several dll calls into one function. The latter two scripts provide examples of how to use the functions to actually output MIDI data.
I should point out the limitations:
1- There is no MIDI input here, which I suspect is more important to many of you than MIDI output. There are two good reasons for this - first, I currently only need MIDI output, and it was a big enough struggle getting this to work so I have no current plans to address MIDI input. Secondly, I am not sure MIDI Input is even possible in AHK right now, because I don't believe AHK is capable of handling callbacks from dll functions. This is required for MIDI input, since windows needs to callback to your script to let you know when MIDI data has been received. Callbacks are possible with MIDI output, but not required, which brings me to point 2:
2- There is no callback mechanism. IOW, the midi data is sent out and you just have to trust that it went. The only real issue I see with this is that you are practically limited to sending a single buffer of midi data. You can make that buffer as big as you want, but sending successive buffers would require a callback notifying you when to send the next buffer. However, if you are doing that kind of midi output, you probably ought to be using something more powerful than AHK!
3- SysEx data is not supported by this script. SysEx requires it's own special handling since it involves large, variably sized messages. I am confident that this could be done from AHK, but haven't tried it.
4- I am a big-time novice at programming, so my code is probably not as clean and efficient as it could be. If anyone wants to clean it up, I am totally open to that.
Note that there are two methods of sending midi output. The first is designed to send individual messages. Script #2 below shows how to do this. The other method involves filling a buffer with a series of midi events, which includes timing information for those events. That buffer is then sent to windows, which handles sending out the individual events at the proper time. This is a much better way of sending multiple events since it is less CPU intensive and is the only way from AHK to make sure the timing of the events is precise. This method is illustrated in script #3 below.
Finally, if you plan to use these functions, I highly recommend checking out this site:
http://www.borg.com/~jglatt/tech/lowmidi.htm
It is designed for C programmers, but it provides a great explanation of how the windows midi functions work, and how to use them. I don't know C from Q, but the examples here helped me tremendously in figuring out how to do this in AHK.
I hope this helps someone. I also hope this motivates someone to look into midi input and sysex.
EDIT: 4/30
I just discovered (the hard way) that when using the midi stream method, windows expects to receive a steady stream of midi buffers. If it doesn't get any for about 1s, the stream output goes to sleep. One nasty side effect (bug?) of this is that the next buffer you send it gets all of its events output at the exact same time. There are two ways around this - send buffers with "NOP" events, which are basically just dummy blank events, or stop or pause the stream after you send each buffer. For my purposes, and probably most practical applications involving AHK, the latter method makes more sense, so I have updated the MIDI Functions script to automatically stop the stream after sending the buffer. This cannot be done until the buffer is done being played, which means you now need to pass the duration of the buffer (in ms) to this function, so it knows how long to sleep. This is another case where having callback functionality would be helpful, but sleep does work. One slightly annoying "feature" of the midiStreamStop and midiStreamPause functions is that they automatically send out cc64 0 (turn off sustain pedal) on every midi channel. This kinda makes sense but I wish there was a way to bypass it. It should be harmless for most purposes, though.
TomB
MIDI Functions
Code:
;+++++++++++++++++++++++++++++++++MIDI Functions++++++++++++++++++++++++++++++++++++++++
;AHK functions for performing various midi output operations by calling winmm.dll
;by Tom Boughner
;Last Modified 4/27/07
;
;
;
;-----------------------------Open the Windows midi API dll---------------------------------
OpenMidiAPI() ;this should be done at the beginning of every script that uses any of these functions to load winmm.dll into memory
{
;it is important that you call this function by assigning it to a variable, so you retain the handle to it for closing later
hModule := DllCall("LoadLibrary", "str", "winmm.dll")
return %hModule%
}
;*********************************************************************************************
;**********************Functions for Sending Individual Messages******************************
;*********************************************************************************************
;Keep in mind that ahk doesn't allow for precise timing control - sleep is always at least 10ms and can vary depending on processor load
;So, if you need to send several events with precise timing, use the Midi Stream functions instead.
;---------------------------------Open the midi port---------------------------------
;This is only used when opening the port for sending individual midi messages.
;To send a buffer of midi stream data, use midiStreamOpen
midiOutOpen(uDeviceID = 0)
{
;returns a handle for the device to be opened. This handle must be used in all other function calls that reference this device.
;uDeviceID is the midi output port to open. You can list these ports with the midiOutGetDevCaps function
strh_midiout = "0000" ;initialize as a 4 byte string
dwFlags := 0
result := DllCall("winmm.dll\midiOutOpen"
, UInt, &strh_midiout
, UInt, uDeviceID
, UInt, 0
, UInt, 0
, UInt, dwFlags
, "UInt")
if (result or errorlevel)
{
msgbox There was an error opening the midi port. The port may be in use. Try closing and reopening all midi-enabled applications.
return -1
}
;not sure why this is necessary, but handle is invalid without converting it:
VarSetCapacity(h_midiout,4,0)
h_midiout := ExtractInteger(strh_midiout, 0, False, 4)
return %h_midiout%
}
;---------------------------------Send 1 Midi message---------------------------------
midiOutShortMsg(h_midiout, EventType, Channel, Param1, Param2)
{
;h_midiout is handle to midi output device returned by midiOutOpen function
;EventType and Channel are combined to create the MidiStatus byte.
;MidiStatus message table can be found at http://www.harmony-central.com/MIDI/Doc/table1.html
;Possible values for EventTypes are NoteOn (N1), NoteOff (N0), CC, PolyAT (PA), ChanAT (AT), PChange (PC), Wheel (W) - vals in () are optional shorthand
;SysEx not supported by the midiOutShortMsg call
;Param3 should be 0 for PChange, ChanAT, or Wheel. When sending Wheel events, put the entire Wheel value
;in Param2 - the function will split it into it's two bytes
;returns 0 if successful, -1 if not.
;Calc MidiStatus byte
If (EventType = "NoteOn" OR EventType = "N1")
MidiStatus := 143 + Channel
Else if (EventType = "NoteOff" OR EventType = "N0")
MidiStatus := 127 + Channel
Else if (EventType = "CC")
MidiStatus := 175 + Channel
Else if (EventType = "PolyAT" OR EventType = "PA")
MidiStatus := 159 + Channel
Else if (EventType = "ChanAT" OR EventType = "AT")
MidiStatus := 207 + Channel
Else if (EventType = "PChange" OR EventType = "PC")
MidiStatus := 191 + Channel
Else if (EventType = "Wheel" OR EventType = "W")
{
MidiStatus := 223 + Channel
Param2 := Param1 >> 8 ;MSB of wheel value
Param1 := Param1 & 0x00FF ;strip MSB, leave LSB only
}
;Midi message Dword is made up of Midi Status in lowest byte, then 1st parameter, then 2nd parameter. Highest byte is always 0
dwMidi := MidiStatus + (Param1 << 8) + (Param2 << 16)
;Call api function to send midi event
result := DllCall("winmm.dll\midiOutShortMsg"
, UInt, h_midiout
, UInt, dwMidi
, UInt)
if (result or errorlevel)
{
msgbox, There was an error sending the midi event
return -1
}
return
}
;---------------------------------Close MidiOutput---------------------------------
;This function should only be called when you are done using the midi output port, such as in a script's OnExit routine
midiOutClose(h_midiout)
{
result := DllCall("winmm.dll\midiOutClose", UInt, h_midiout)
if (result or errorlevel)
{
msgbox, There was an error closing the midi output port. There may still be midi events being processed through it.
return -1
}
return
}
;---------------------------------Free winmm.dll---------------------------------
FreeMidiAPI(hModule)
{
DllCall("FreeLibrary", "Uint", hModule)
if (result or errorlevel)
{
msgbox, There was an error freeing the MidiAPI file. Are you sure you assigned the OpenMidiAPI call to a variable and passed that variable (unchanged) to this function?
return -1
}
return
}
;*********************************************************************************************
;**********************************Functions for Stream Output********************************
;*********************************************************************************************
;Functions:
;midiStreamOpen
;
;-------------Open the midi port for streaming-------------------------------------------
midiStreamOpen(DeviceID)
;MMRESULT midiStreamOpen(
;LPHMIDISTRM lphStream, pointer to handle to identify stream - filled by call to midiStreamOpen
;LPUINT puDeviceID, pointer to DeviceID (for some reason, pointing to the DeviceID doesn't work for me, I had to just pass the deviceID itself.)
;DWORD cMidi, Always 1
;DWORD_PTR dwCallback, pointer to callback function, event, etc. (0 = none)
;DWORD_PTR dwInstance, number you can assign to this stream
;DWORD fdwOpen type of callback
;);
;Returns handle to midi stream, used by all other midi stream out functions
;Note this routine does not use any callbacks
{
strh_stream = "0000" ;init stream pointer
cMidi := 1 ;must be 1 per spec
dwCallback := 0
dwInstance := 0
CALLBACK_NULL := 0
VarSetCapacity(uDeviceID, 4, 0)
InsertInteger(DeviceID, uDeviceID, 0, 4)
result := DllCall("winmm.dll\midiStreamOpen"
, UInt, &strh_stream
, UInt, &uDeviceID
, UInt, cMidi
, UInt, dwCallback
, UInt, dwInstance
, UInt, CALLBACK_NULL
, "UInt")
if (result or errorlevel)
{
msgbox There was an error opening the midi port. The port may be in use. Try closing and reopening all midi-enabled applications.
return -1
}
;Not sure why, but the lines below are necessary to get AHK to treat the handle as a number instead of a string:
;the h_stream handle created by the above dll call is converted to an unsigned integer value - uih_stream,
;which is used as the handle for all of send midi commands that follow
VarSetCapacity(h_stream,4,0)
h_stream := ExtractInteger(strh_stream, 0, False, 4)
return h_stream
}
;-------------Create Single Event----------------------------------------------------------
;Assembles MIDIEVENT structure for a single event. This structure contains the event itself, plus it's timing.
;This MIDIEVENT is then placed into the MidiBuffer.
;--Event Structure
;typedef struct {
; DWORD dwDeltaTime; offset to time this event should be sent
; DWORD dwStreamID; streamID this should be sent to (assumed to always be 0 for our purposes)
; DWORD dwEvent; Event DWord (Highest byte is EventCode [shortMsg for us], followed by param2, param1, status)
; DWORD dwParms[]; not needed for short messages
;} MIDIEVENT;
;NOTE: MidiBuffer needs to have already been given the correct size using VarSetCapacity.
;This means you must determine how many events to send in the buffer before calling this routine
;BufferSize = 12 * number of events
;The function automatically places events in the buffer in the order they are received.
AddEventToBuffer(ByRef MidiBuffer, DeltaTime, EventType, Channel, Param1, Param2, NewBuffer = 0)
;NewBuffer is optional parameter - it signals the function to reset BufOffset to 0, meaning we are starting a new buffer
{
Static BufOffset := 0 ;variable to keep track of where in the buffer the next event goes, set to global so that it can be reset by calling script when starting a new buffer
if (NewBuffer)
{
BufOffset := 0
}
;Check to make sure we haven't reached end of buffer already
If (BufOffset + 12 > VarSetCapacity(MidiBuffer))
{
msgbox, Midi Buffer is already full.`nEvent %EventType% %Channel% %Param1% %Param2%`n could not be added.
return -1
}
;Calc MidiStatus byte (same as in midiOutShortMsg Function)
If (EventType = "NoteOn" OR EventType = "N1")
MidiStatus := 143 + Channel
Else if (EventType = "NoteOff" OR EventType = "N0")
MidiStatus := 127 + Channel
Else if (EventType = "CC")
MidiStatus := 175 + Channel
Else if (EventType = "PolyAT" OR EventType = "PA")
MidiStatus := 159 + Channel
Else if (EventType = "ChanAT" OR EventType = "AT")
MidiStatus := 207 + Channel
Else if (EventType = "PChange" OR EventType = "PC")
MidiStatus := 191 + Channel
Else if (EventType = "Wheel" OR EventType = "W")
{
MidiStatus := 223 + Channel
Param2 := Param1 >> 8 ;MSB of wheel value
Param1 := Param1 & 0x00FF ;strip MSB, leave LSB only
}
Else
{
msgbox, Invalid EventType.
pause
}
;Midi message Dword is made up of Midi Status in lowest byte, then 1st parameter, then 2nd parameter. Highest byte is always 0
dwEvent := MidiStatus + (Param1 << 8) + (Param2 << 16)
VarSetCapacity(MIDIEVENT, 12) ;12 is size of a single midievent
;create MIDIEVENT
InsertInteger(DeltaTime, MIDIEVENT, 0, 4)
InsertInteger(StreamID, MIDIEVENT, 4, 4)
InsertInteger(dwEvent, MIDIEVENT, 8, 4)
;MEEvent := ExtractInteger(MIDIEVENT, 8, False, 4) ;should be midi event
;Add Event to Buffer
DllCall("RtlMoveMemory", "UInt", &MidiBuffer + BufOffset, "UInt", &MIDIEVENT, "UInt", 12)
;msgbox % errorlevel . " " . dwEvent . " " . dwEventTest . " " . &MIDIEVENT . " " . &MidiBuffer
;MBDeltaTime := ExtractInteger(MidiBuffer, 0, False, 4) ;should equal deltatime
;MBStreamID := ExtractInteger(MidiBuffer, 4, False, 4) ;should equal 0
;MBEvent := ExtractInteger(MidiBuffer, 8, False, 4) ;should be midi event
;msgbox, DT = %MBDeltaTime% ID = %MBStreamID% BufEvent = %MBEvent% MidiEvent = %MEEvent%
;pause
BufOffset := BufOffset + 12
}
;--------------Set Tempo/Timebase for Stream------------------------------------
;Tempo and timebase are set by calling the midiStreamProperty function
;MMRESULT midiStreamProperty(
; HMIDISTRM hm, handle to midi out device
; LPBYTE lppropdata, Pointer to Property data
; DWORD dwProperty Flags to specify what to change
;);
SetTempoandTimebase(h_stream, BPM, PPQ)
;BPM = tempo in beats per minute, PPQ = ticks (parts) per quarter note
{
;Create MIDIPROPTIMEDIV structure
;typedef struct {
; DWORD cbStruct; seems to always = 8? why is this even needed?
; DWORD dwTimeDiv; contains number of ticks per quarter note
;} MIDIPROPTIMEDIV;
VarSetCapacity(MIDIPROPTIMEDIV, 8)
InsertInteger(8, MIDIPROPTIMEDIV, 0, 4)
InsertInteger(PPQ, MIDIPROPTIMEDIV, 4, 4)
;call function to set TimeDiv
result := DllCall("winmm.dll\midiStreamProperty"
, UInt, h_stream
, UInt, &MIDIPROPTIMEDIV
, UInt, 0x80000001 ;flags = MIDIPROPSET (0x80000000) and MIDIPROP_TIMEDIV (1)
, "UInt")
;msgbox % errorlevel . " " . result
if (result)
{
msgbox There was an error setting the Timebase.
pause
return -1
}
;Create MIDIPROPTEMPO structure and call function to set tempo
;note - default tempo is 120BPM, we are changing to 125BPM since that makes each midi tick almost exactly .5ms
;typedef struct {
; DWORD cbStruct;
; DWORD dwTempo; tempo as microseconds per quarter = 1/[125BPM/60(s/m)/1000000(us/s)] = 480,000
;} MIDIPROPTEMPO;
;calculate tempo in micro-seconds per beat
Tempo := 6.E7/BPM
VarSetCapacity(MIDIPROPTEMPO, 8)
InsertInteger(8, MIDIPROPTEMPO, 0, 4)
InsertInteger(Tempo, MIDIPROPTEMPO, 4, 4)
result := DllCall("winmm.dll\midiStreamProperty"
, UInt, h_stream
, UInt, &MIDIPROPTEMPO
, UInt, 0x80000002 ;flags = MIDIPROPSET (0x80000000) and MIDIPROP_TEMPO (2)
, "UInt")
if (result)
{
msgbox There was an error setting the tempo.
return -1
}
return
}
;---------------------------Play Midi Buffer------------------------------------------
;Once the Buffer is created it's header must be 'Prepared' before sending it to the stream device.
;typedef struct {
; LPSTR lpData; pointer to midi data stream
; DWORD dwBufferLength; size of buffer
; DWORD dwBytesRecorded; number of bytes of actual midi data in buffer
; DWORD_PTR dwUser; custom user data
; DWORD dwFlags; should be 0
; struct midihdr_tag far * lpNext; do not use
; DWORD_PTR reserved; do not use
; DWORD dwOffset; offset generated by callback - not used in this routine
; DWORD_PTR dwReserved[4]; do not use
;} MIDIHDR;
midiOutputBuffer(h_stream, ByRef MidiBuffer, BufSize, BufDur)
;BufSize is size of buffer in bytes.
;BufDur is the duration in ms of the buffer
{
Global MIDIHDR ;necessary so other functions can access MIDIHDR
VarSetCapacity(MIDIHDR, 36, 0)
InsertInteger(&MidiBuffer, MIDIHDR, 0, 4)
InsertInteger(BufSize, MIDIHDR, 4, 4)
InsertInteger(BufSize, MIDIHDR, 8, 4)
; remaining props can all be 0
;Send header to prepare header function
;MMRESULT midiOutPrepareHeader(
; HMIDIOUT hmo,
; LPMIDIHDR lpMidiOutHdr,
; UINT cbMidiOutHdr
;);
result := DllCall("winmm.dll\midiOutPrepareHeader"
, UInt, h_stream
, UInt, &MIDIHDR
, UInt, 36 ;size of header
, "UInt")
if (result)
{
msgbox There was an error in the midiOutPrepareHeader call.
return -1
}
;Send Header to MidiOut
;Sends Midi Buffer to Stream Output device, ready to play.
;Note - this function does not actually play the buffer, it just cues it up.
;Use midiStreamRestart to play the buffer
;MMRESULT midiStreamOut(
; HMIDISTRM hMidiStream, handle for midi stream
; LPMIDIHDR lpMidiHdr, pointer to MIDIHDR
; UINT cbMidiHdr size of MIDIHDR
;);
result := DllCall("winmm.dll\midiStreamOut"
, UInt, h_stream
, UInt, &MIDIHDR
, UInt, 36 ;size of header
, "UInt")
if (result)
{
msgbox There was an error in the midiStreamOut function.
return -1
}
;Start playback
result := DllCall("winmm.dll\midiStreamRestart"
, UInt, h_stream
, "UInt")
if (result)
{
msgbox There was an error in the midiStreamRestart function.
return -1
}
;Wait for duration of entire buffer - actual wait time will be at least this long
Sleep, BufDur
;Stop Stream - this keeps it from going to sleep. If this is not done, the stream seems to get suspended when not in use for about 1s, which causes it to
;send the next buffer's events all at the same time.
DllCall("winmm.dll\midiStreamStop", UInt, h_stream)
return
}
;------------------When closing routine, unprepare header and close stream:------------------
midiOutCloseStream(h_stream, ByRef MIDIHDR)
;Unprepare Header
;uses identical format to midiOutPrepareHeader
{
result := DllCall("winmm.dll\midiOutUnprepareHeader"
, UInt, h_stream
, UInt, &MIDIHDR
, UInt, 36 ;size of header
, "UInt")
if (result)
{
msgbox There was an error in the midiOutUnprepareHeader function.
return -1
}
;CloseMidiStream
result := DllCall("winmm.dll\midiStreamClose"
, UInt, h_stream
, "UInt")
if (result)
{
msgbox There was an error closing the midi stream.
return -1
}
return
}
;*********************************************************************************************
;***********************************Utility Functions*****************************************
;*********************************************************************************************
;Get number of midi output devices on system
;Note that the first device has an ID of 0
MidiOutGetNumDevs()
{
result := DllCall("winmm.dll\midiOutGetNumDevs")
return %result%
}
;Get name of a midiOut device for a given ID
MidiOutNameGet(uDeviceID = 0)
{
;MMRESULT midiOutGetDevCaps(
; UINT_PTR uDeviceID,
; LPMIDIOUTCAPS lpMidiOutCaps,
; UINT cbMidiOutCaps
;);
;typedef struct {
; WORD wMid;
; WORD wPid;
; MMVERSION vDriverVersion;
; CHAR szPname[MAXPNAMELEN];
; WORD wTechnology;
; WORD wVoices;
; WORD wNotes;
; WORD wChannelMask;
; DWORD dwSupport;
;} MIDIOUTCAPS;
;Setup midiOutCaps structure (the only value we care about is szPname)
VarSetCapacity(MidiOutCaps, 50, 0) ;allows for szPname to be 32 bytes
OffsettoPortName := 8
PortNameSize := 32
result := DllCall("winmm.dll\midiOutGetDevCapsA"
, UInt, uDeviceID
, UInt, &MidiOutCaps
, UInt, 50
, UInt)
if (result OR errorlevel)
{
msgbox, There was an error retrieving the name of midi output %uDeviceID%
return -1
}
VarSetCapacity(PortName, 32)
DllCall("RtlMoveMemory", str, PortName, Uint, &MidiOutCaps + OffsettoPortName, Uint, PortNameSize)
;PortName := ExtractInteger(MidiOutCaps, OffsettoPortName, False, 4)
;SubStr(MidiOutCaps, OffsettoPortName, PortNameSize)
return %PortName%
}
MidiOutsEnumerate()
{
;Returns the number of midi output devices, and also creates
;a global array called MidiOutPortName with the names of each device
Global ;variables created will be global by default
local NumPorts, PortName, PortID
NumPorts := MidiOutGetNumDevs()
Loop, %NumPorts%
{
PortID := A_Index -1
MidiOutPortName%PortID% := MidiOutNameGet(PortID)
;PortList = %PortList%PortID %PortID%: %PortName%`n
}
;msgbox % msg
return % NumPorts
}
ExtractInteger(ByRef pSource, pOffset = 0, pIsSigned = false, pSize = 4)
; pSource is a string (buffer) whose memory area contains a raw/binary integer at pOffset.
; The caller should pass true for pSigned to interpret the result as signed vs. unsigned.
; pSize is the size of PSource's integer in bytes (e.g. 4 bytes for a DWORD or Int).
; pSource must be ByRef to avoid corruption during the formal-to-actual copying process
; (since pSource might contain valid data beyond its first binary zero).
{
Loop %pSize% ; Build the integer by adding up its bytes.
result += *(&pSource + pOffset + A_Index-1) << 8*(A_Index-1)
if (!pIsSigned OR pSize > 4 OR result < 0x80000000)
return result ; Signed vs. unsigned doesn't matter in these cases.
; Otherwise, convert the value (now known to be 32-bit) to its signed counterpart:
return -(0xFFFFFFFF - result + 1)
}
InsertInteger(pInteger, ByRef pDest, pOffset = 0, pSize = 4)
; The caller must ensure that pDest has sufficient capacity. To preserve any existing contents in pDest,
; only pSize number of bytes starting at pOffset are altered in it.
{
Loop %pSize% ; Copy each byte in the integer into the structure as raw binary data.
DllCall("RtlFillMemory", "UInt", &pDest + pOffset + A_Index-1, "UInt", 1, "UChar", pInteger >> 8*(A_Index-1) & 0xFF)
}
Single Midi Event ExampleCode:
;Routine to send a single middle C note by using the midiOutShortMsg function.
;Note that the timing of the note-off command cannot be precisely controlled using this method
;since AHK's Sleep command doesn't always sleep for the specified duration.
;The midiOutShortMsg command is better used for sending single events, such as program changes or control changes
;To send a series of events that require precise timing, use the MidiStream functions instead.
;This script provides an example of the correct order and format that the functions need to be called in to send a midi event.
#Include Midi Functions.ahk
;Constants:
channel := 1 ;midi channel to send on
MidiDevice := 0 ;number of midi output device to use.
Note := 60 ;midi number for middle C
NoteDur := 1000 ;duration to hold note for (approx.)
NoteVel := 100 ;velocity of note to send
;See if user wants to pick an output
MsgBox, 4, Enumerate Midi Outputs?
, Do you want to select from a list of midi outputs on this system, and their associated IDs?`n`nIf you select NO, the default midi output will be used.
IfMsgBox Yes
{
NumPorts := MidiOutsEnumerate() ;function that fills an global array called MidiOutPortName and returns the number of ports
Loop, % NumPorts
{
Port := A_Index -1
msg := msg . "ID: " . Port . " --> " . MidiOutPortName%Port% . "`n"
}
InputBoxH := 100 + NumPorts * 35
InputBox, MidiDevice, Select Midi Port, Enter the number of the port you would like to use`n`n%msg%,, 350, % (NumPorts * 35 + 100),,,,, 0
if (errorlevel)
exit
}
;Open the Windows midi API dll
hModule := OpenMidiAPI()
;pause
;Open the midi port
h_midiout := midiOutOpen(1)
;pause
;-------------Send middle C-----------------------------------------------------
; "N1" is shorthand for "NoteOn". See comments in midiOutShortMsg for a full list of allowable event types
midiOutShortMsg(h_midiout, "N1", Channel, Note, NoteVel)
;pause
Sleep %NoteDur%
;Send Note-Off command for middle C
midiOutShortMsg(h_midiout, "N0", Channel, Note, 0)
exit
Midi Stream ExampleCode:
;Routine to send an escalating or decreasing series of cc commands. The ccStart value is sent immediately to initialize the control
;The rest of the values are sent after StartDelay (which can be set to 0)
;The total duration of the series is controlled by SeriesDuration
;This was created to fade-in Sonar's output by setting the Master bus Input Gain control's remote control feature
;to respond to cc118 values. The routine could obviously be modified to
;send any series of cc values
;it relies on the Midi Stream functions in Windows API = winmm.dll
#Include d:\Tom Documents\AutoHotkey Macros\Midi Functions.ahk
;Constants:
ch := 16 ;midi channel to send on
cc := 118 ;cc # to send
ccStart := 101 ;first cc value to send
ccEnd := 0 ;last cc value to send (101 corresponds to 0dB for a SONAR fader with midi remote control)
ccStep := -5 ;incremental value of each successive cc value sent e.g. 2 would mean send out 0, 2, 4, 6, etc. *Must be negative if descending values desired*
StartDelay := 700 ;number of ms before sending the 2nd event. The first event is sent immediately to 'initialize the cc value'
SeriesDuration := 500 ;number of ms that the entire series of cc events should take.
MidiDevice := 0 ;number of midi output device to use. The first midi out is 0.
;You can determine this number by calling the MidiOutsEnumerate() function.
MsgBox, 4, Enumerate Midi Outputs?
, Do you want to select from a list of midi outputs on this system, and their associated IDs?`n`nIf you select NO, the default midi output will be used.
IfMsgBox Yes
{
NumPorts := MidiOutsEnumerate() ;function that fills an global array called MidiOutPortName and returns the number of ports
Loop, % NumPorts
{
Port := A_Index -1
msg := msg . "ID: " . Port . " --> " . MidiOutPortName%Port% . "`n"
}
InputBoxH := 100 + NumPorts * 35
InputBox, MidiDevice, Select Midi Port, Enter the number of the port you would like to use`n`n%msg%,, 350, % (NumPorts * 35 + 100),,,,, 0
if (errorlevel)
exit
}
;Open the Windows midi API dll
hModule := OpenMidiAPI()
;-------------Open the midi port for streaming-------------------------------------------
h_stream := midiStreamOpen(1)
if (h_stream = -1) ;returned error
exit
;--------------Set Tempo/Timebase for Stream
result := SetTempoandTimebase(h_stream, 125, 960) ;tempo of 125BPM, timbease of 960ppq
if (result = -1) ;returned error
exit
;-------------Create Stream of Events----------------------------------------------------------
;Number of Events is a bit tricky - if there is a step greater than 1, it is possible that
;the ccEnd value won't get sent (e.g. 0-127, step 2: 127 won't get sent)
;so, we calculate the total number of events that will be sent by rounding NumEvents Up.
;This is used to calc the delay between events. We round NumEvents down to get the number of times to loop.
;at end of loop we send the final value if it hasn't already been sent in the loop.
NumEvents := Abs((ccEnd - ccStart)/ccStep) ;this doesn't include the 1st event, since that is sent separately from the others (before StartDelay)
EventDelay := 2 * SeriesDuration/Ceil(NumEvents - 1) ;time to wait between each event send - * 2 to convert to ticks (1 tick = .5ms at 125BPM, 960ppq). subtract 1 from NumEvents since 1st event in series is at time 0 (relative to StartDelay)
;Need to establish and declare size of buffer before sending events to it
BufSize := 12 * (Ceil(NumEvents) + 1) ;each midi event takes 12 bytes, add 1 to include first event
VarSetCapacity(MidiBuffer, BufSize, 0)
;RunningDelay keeps track of how much total time we have delayed so far. EventDelay is not likely to be an integer.
;The delay time between each event has to be an integer of ms, so we calculate what the delay so far should be, compare
;it to the actual total delay time of the last event sent, then round the difference to nearest integer. Note that
;it is possible that some events will be sent with no delay in this scenario. In any event
;the cc crescendo will not be completely smooth
;another issue is that Sleep values less than 10ms usually take 10ms in reality
RunningDelay := 0
DeltaTime := 0 ;first event should be send immediately
;create first event in buffer
AddEventToBuffer(MidiBuffer, DeltaTime, "CC", ch, cc, ccStart)
;------Create 2nd MidiEvent with DeltaTime = StartDelay
NextccVal := ccStart + ccStep
dwEvent := dwMidiLowWord + (NextccVal << 16)
DeltaTime := StartDelay * 2 ;our timebase of 960ppq and Tempo of 125 (setup further down) means each tick is exactly .5ms
AddEventToBuffer(MidiBuffer, DeltaTime, "CC", ch, cc, NextccVal)
;MBDeltaTime := ExtractInteger(MidiBuffer, 12, False, 4) ;should equal deltatime
;MBStreamID := ExtractInteger(MidiBuffer, 16, False, 4) ;should equal 0
;MBEvent := ExtractInteger(MidiBuffer, 20, False, 4) ;should be midi event
;msgbox, DT = %MBDeltaTime% ID = %MBStreamID% Event = %MBEvent%
;pause
;-------Create all remaining midi events--------------------
Loop, % Floor(NumEvents) - 1 ;we've already sent the 2nd event
{
NextccVal := NextccVal + ccStep
;calculate deltatime
ShouldBeDelay := EventDelay * A_Index
ActualDelay := Round(ShouldBeDelay - RunningDelay)
RunningDelay := RunningDelay + ActualDelay
AddEventToBuffer(MidiBuffer, ActualDelay, "CC", ch, cc, NextccVal)
}
;check to see if we still need to send ccEnd value
if (NextccVal < ccEnd)
{
;calculate deltatime
ShouldBeDelay := EventDelay * A_Index
ActualDelay := Round(ShouldBeDelay - RunningDelay)
RunningDelay := RunningDelay + ActualDelay
AddEventToBuffer(MidiBuffer, ActualDelay, "CC", ch, cc, ccEnd)
}
;------------------------------Play Midi Buffer---------------------
result := midiOutputBuffer(h_stream, MidiBuffer, BufSize)
if (result = -1) ;returned error
exit
Sleep, 4000
;When closing routine:
midiOutCloseStream(h_stream, MIDIHDR)
exit