Jump to content

Sky Slate Blueberry Blackcurrant Watermelon Strawberry Orange Banana Apple Emerald Chocolate
Photo

MIDI Output from AHK


  • Please log in to reply
65 replies to this topic
TomB
  • Members
  • 17 posts
  • Last active: Oct 29 2009 03:03 PM
  • Joined: 14 Jan 2005
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/...ech/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
;+++++++++++++++++++++++++++++++++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 Example

  ;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 Example

  ;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


Chris
  • Administrators
  • 10727 posts
  • Last active:
  • Joined: 02 Mar 2004
Great presentation and comments. You packaged evreything up very nicely.

This looks like it will be a great asset to users of MIDI.

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
I tried with the only MIDI playback device I have: "Microsoft GS Wavetable SW Synth", but the script gives an error message (There was an error opening the midi port…). Do I need a physical midi device? Other SW, like NoteWorthy Composer Version 1.75c happily plays back midi music through the speakers connected to sound output of my laptop.

TomB
  • Members
  • 17 posts
  • Last active: Oct 29 2009 03:03 PM
  • Joined: 14 Jan 2005
Laszlo,

I tried with the only MIDI playback device I have: "Microsoft GS Wavetable SW Synth", but the script gives an error message (There was an error opening the midi port…). Do I need a physical midi device? Other SW, like NoteWorthy Composer Version 1.75c happily plays back midi music through the speakers connected to sound output of my laptop.


This could mean that another program has already opened the device. I tried using that output on my system and it did not give an error. You could try rebooting and see if that frees it up.

Tom

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
I removed-reinstalled the audio driver (SigmaTel C-Major) and started the script before any application, still I only get the error message. Is there a way to take control forcibly over the midi driver? Or, to find out what uses it? Every music player I have plays back my midi files, even if there are more than one of them open.

TomB
  • Members
  • 17 posts
  • Last active: Oct 29 2009 03:03 PM
  • Joined: 14 Jan 2005
Laszlo,

Is there a way to take control forcibly over the midi driver? Or, to find out what uses it? Every music player I have plays back my midi files, even if there are more than one of them open.


I really don't know. There isn't any way that I am aware of. I do know that some midi drivers are multi-client and some (most) are not. Also, I suspect that many applications, especially dedicated midi sequencers bypass the windows midi API and talk to the midi drivers directly.

You could modify the script to tell you what the error code is. Change line 42
msgbox There was an error opening the midi port.  The port may be in use.  Try closing and reopening all midi-enabled applications.)

To:
msgbox There was an error opening the midi port.  Error code %result%.

Then compare that code to this list (note the BASE value is 0):
MSYSERR_ERROR (MMSYSERR_BASE+1)
00079 #define MMSYSERR_BADDEVICEID (MMSYSERR_BASE+2)
00080 #define MMSYSERR_NOTENABLED (MMSYSERR_BASE+3)
00081 #define MMSYSERR_ALLOCATED (MMSYSERR_BASE+4)
00082 #define MMSYSERR_INVALHANDLE (MMSYSERR_BASE+5)
00083 #define MMSYSERR_NODRIVER (MMSYSERR_BASE+6)
00084 #define MMSYSERR_NOMEM (MMSYSERR_BASE+7)
00085 #define MMSYSERR_NOTSUPPORTED (MMSYSERR_BASE+8)
00086 #define MMSYSERR_BADERRNUM (MMSYSERR_BASE+9)
00087 #define MMSYSERR_INVALFLAG (MMSYSERR_BASE+10)
00088 #define MMSYSERR_INVALPARAM (MMSYSERR_BASE+11)
00089 #define MMSYSERR_HANDLEBUSY (MMSYSERR_BASE+12)
00090 #define MMSYSERR_INVALIDALIAS (MMSYSERR_BASE+13)
00091 #define MMSYSERR_BADDB (MMSYSERR_BASE+14)
00092 #define MMSYSERR_KEYNOTFOUND (MMSYSERR_BASE+15)
00093 #define MMSYSERR_READERROR (MMSYSERR_BASE+16)
00094 #define MMSYSERR_WRITEERROR (MMSYSERR_BASE+17)
00095 #define MMSYSERR_DELETEERROR (MMSYSERR_BASE+18)
00096 #define MMSYSERR_VALNOTFOUND (MMSYSERR_BASE+19)
00097 #define MMSYSERR_NODRIVERCB (MMSYSERR_BASE+20)
00098 #define MMSYSERR_LASTERROR (MMSYSERR_BASE+20)

Tom

majkinetor
  • Moderators
  • 4512 posts
  • Last active: May 20 2019 07:41 AM
  • Joined: 24 May 2006
I will check it out with some of the number of virtual synts I have here.

Thx.
Posted Image

Sean
  • Members
  • 2462 posts
  • Last active: Feb 07 2012 04:00 AM
  • Joined: 12 Feb 2007

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:

Although callback is not possible yet, looks like you can use Window Messages instead here, specifying the last parameter to CALLBACK_WINDOW, meaning there is indeed a way of callback with AHK.
Read about OnMessage in the help file.

BTW, you didn't mention about Midi Mapper, which can be specified by setting uDeviceID to -1. That may solve the issue here.

TomB
  • Members
  • 17 posts
  • Last active: Oct 29 2009 03:03 PM
  • Joined: 14 Jan 2005
Thanks Sean - I wondered if the callback to a window would work, but didn't look into it much since callback isn't required for midi out.

You are right about midi mapper. I forgot all about that since I never use it.

Tom

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
Device -1,0,1... all give error code 2 (bad device ID), although the Select Midi Port inputbox lists ID: 0 --> Microsoft GS Wavetable SW Synth

Sean
  • Members
  • 2462 posts
  • Last active: Feb 07 2012 04:00 AM
  • Joined: 12 Feb 2007

Device -1,0,1... all give error code 2 (bad device ID), although the Select Midi Port inputbox lists ID: 0 --> Microsoft GS Wavetable SW Synth

I think it's because he strangely hard coded DeviceID to 1 like

midiOutOpen(1)
midiStreamOpen(1)

Try to change 1 to 0 or -1.

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005

strangely hard coded DeviceID to 1 like

midiOutOpen(1)
midiStreamOpen(1)
Try to change 1 to 0 or -1.

That was it. Thanks! Both 0 and -1 work in the single event example.

The stream example does not give any error message, but nothing is played back. The script just exits after a few seconds.

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
I guess the MS Wave device needs other parameters, than the SONAR fader. Does anyone know them?

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
There are a number of other bugs in the Streaming script, too. For example, the midiOutputBuffer function is called with one too few parameters, the second event is set up with dwMidiLowWord, which was never set, and therefore the dwEvent value becomes empty, but it is not used later.

Could someone, who knows about midi, fix the script, so it plays on midi device 0?

As I understand, the following should produce a beep, without all the fancy stuff, but it is as silent as the script in the first post.
hModule := OpenMidiAPI()
  h_stream := midiStreamOpen(0)
  SetTempoandTimebase(h_stream, 120, 960)
  BufSize := 3*12
  VarSetCapacity(MidiBuffer, BufSize, 0)
  AddEventToBuffer(MidiBuffer,  0, "CC", 1, 7, 64, 1)   ; ControlChange, ch = 1, volume, 64 of 127
  AddEventToBuffer(MidiBuffer,500, "N1", 1, 60, 64)     ; NoteOn,        ch = 1,  middle C, velocity 64
  AddEventToBuffer(MidiBuffer,900, "N0", 1, 60, 64)     ; NoteOff,       ch = 1,  middle C, velocity 64
  midiOutputBuffer(h_stream, MidiBuffer, BufSize, 2000) ; --> MIDIHDR global
  midiOutCloseStream(h_stream, MIDIHDR)


Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
After a couple of days debugging I ended up rewriting the whole code. Now it works with the default midi device: ID 0: Microsoft GS Wavetable SW Synth. Most of the changes were bug fixes and simplifications, to reduce code size. The hardest part is TomB's original discovery work, which made midi output possible from an AHK script.

The general functions:
;;;;;;;;; AHK functions for midi output by calling winmm.dll ;;;;;;;;;;
;http://msdn.microsoft.com/library/default.asp?url=/library/en-us/multimed/htm/_win32_multimedia_functions.asp

OpenCloseMidiAPI() {  ; at the beginning to load, at the end to unload winmm.dll
   Static hModule
   If hModule
      DllCall("FreeLibrary", UInt,hModule), hModule := ""
   If (0 = hModule := DllCall("LoadLibrary",Str,"winmm.dll")) {
      MsgBox Cannot load libray winmm.dll
      Exit
   }
}

;;;;;;;;;;;;;;; Functions for Sending Individual Messages ;;;;;;;;;;;;;;;

midiOutOpen(uDeviceID = 0) { ; Open midi port for sending individual midi messages --> handle
   strh_midiout = 0000

   result := DllCall("winmm.dll\midiOutOpen", UInt,&strh_midiout, UInt,uDeviceID, UInt,0, UInt,0, UInt,0, UInt)
   If (result or ErrorLevel) {
      MsgBox There was an error opening the midi port.`nError code %result%`nErrorLevel = %ErrorLevel%
      Return -1
   }
   Return [email protected](&strh_midiout)
}

midiOutShortMsg(h_midiout, EventType, Channel, Param1, Param2) {
  ;h_midiout: handle to midi output device returned by midiOutOpen
  ;EventType, Channel combined -> MidiStatus byte: http://www.harmony-central.com/MIDI/Doc/table1.html
  ;Param3 should be 0 for PChange, ChanAT, or Wheel
  ;Wheel events: entire Wheel value in Param2 - the function splits it into two bytes

  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
  }
  result := DllCall("winmm.dll\midiOutShortMsg", UInt,h_midiout, UInt, MidiStatus|(Param1<<8)|(Param2<<16), UInt)
  If (result or ErrorLevel)  {
    MsgBox There was an error sending the midi event: (%result%`, %ErrorLevel%)
    Return -1
  }
}

midiOutClose(h_midiout) {  ; Close MidiOutput
  Loop 9 {
     result := DllCall("winmm.dll\midiOutClose", UInt,h_midiout)
     If !(result or ErrorLevel)
        Return
     Sleep 250
  }
  MsgBox Error in closing the midi output port. There may still be midi events being processed.
  Return -1
}

;;;;;;;;;;;;;;; Functions for Stream Output ;;;;;;;;;;;;;;;

midiStreamOpen(DeviceID) { ; Open the midi port for streaming
  ;MMRESULT    midiStreamOpen( --> handle to midi stream, used by midi stream out functions
  ;LPHMIDISTRM lphStream,  Pointer to handle to stream - filled by call to midiStreamOpen
  ;LPUINT      puDeviceID, Pointer to DeviceID
  ;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

  VarSetCapacity(strh_stream, 4, 0)
  result:=DllCall("winmm.dll\midiStreamOpen", UInt,&strh_stream, UIntP,DeviceID, UInt,1, UInt,0, UInt,0, UInt,0, UInt)
  If (result or ErrorLevel) {
     MsgBox There was an error opening the midi port.`nError code %result%`nErrorLevel = %ErrorLevel%
     Return -1
  }
  Return [email protected](&strh_stream)
}

AddEventToBuffer(ByRef MidiBuffer, DeltaTime, EventType, Channel, Param1, Param2, NewBuffer = 0) {
; MIDIEVENT Structure
;    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
; BufferSize = 12 * number of events

  Static BufOffset = 0     ; keep track of where in the buffer the next event goes
  If (NewBuffer)
     BufOffset = 0

  If (BufOffset + 12 > VarSetCapacity(MidiBuffer)) {
     MsgBox Midi Buffer is full.`nEvent %EventType% %Channel% %Param1% %Param2%`n could not be added.
     Return -1
  }

  If (EventType = "NoteOn" OR EventType = "N1")  ; Calc MidiStatus byte (~ midiOutShortMsg Function)
    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
    Param1 := Param1 & 0x00FF
  }
  Else {
    MsgBox Invalid EventType.
    Return -1
  }

  PokeInt(DeltaTime, &MidiBuffer+BufOffset)
  PokeInt(0, &MidiBuffer+BufOffset+4)
  PokeInt(MidiStatus|(Param1 << 8)|(Param2 << 16), &MidiBuffer+BufOffset+8)
  BufOffset += 12
}

SetTempoAndTimebase(h_stream, BPM, PPQ) { ; BPM = tempo in Beats Per Minute, PPQ = ticks (Parts) Per Quarter note
  VarSetCapacity(struct, 8) ; structure
  PokeInt( 8,   &struct)    ; always = 8 (?)
  PokeInt(PPQ,  &struct+4)  ; contains number of ticks per quarter note

  result := DllCall("winmm.dll\midiStreamProperty", UInt,h_stream, UInt,&struct
         ,  UInt,0x80000001, UInt)   ; flags = MIDIPROPSET (0x80000000) and MIDIPROP_TIMEDIV (1)
  If (result) {
    MsgBox Error %result% in setting the Timebase
    Return -1
  }

  PokeInt(6.e7//BPM,&struct+4) ; dwTempo as microseconds per quarter note
  result := DllCall("winmm.dll\midiStreamProperty", UInt,h_stream, UInt,&struct
         ,  UInt,0x80000002, UInt)   ; flags = MIDIPROPSET (0x80000000) and MIDIPROP_TEMPO (2)
  If (result) {
    MsgBox Error %result% in setting the Tempo
    Return -1
  }
}

;MIDIHDR 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

midiOutputBuffer(h_stream, ByRef MidiBuffer, BufSize, BufDur) { ; Play Midi Buffer... Buf-fer Dur-ation in ms
  Global MIDIHDR      ; other functions can access MIDIHDR
  VarSetCapacity(MIDIHDR, 36, 0)
  PokeInt(&MidiBuffer,&MIDIHDR)
  PokeInt(BufSize,    &MIDIHDR+4)
  PokeInt(BufSize,    &MIDIHDR+8) ; remaining props can all be 0

  result := DllCall("winmm.dll\midiOutPrepareHeader", UInt,h_stream, UInt,&MIDIHDR, UInt,36, UInt) ; 36 = size of header
  If (result)  {
    MsgBox Error %result% in midiOutPrepareHeader
    Return -1
  }
  result := DllCall("winmm.dll\midiStreamOut", UInt,h_stream, UInt,&MIDIHDR, UInt,36, UInt) ; Queue up buffer, ready to play
  If (result)  {
    MsgBox Error %result% in midiStreamOut
    Return -1
  }
  result := DllCall("winmm.dll\midiStreamRestart", UInt,h_stream, UInt) ; Start playback
  If (result) {
    MsgBox Error %result% in midiStreamRestart
    Return -1
  }

  Sleep %BufDur% ; Wait for duration of entire buffer

  DllCall("winmm.dll\midiStreamStop", UInt, h_stream) ; Stop Stream - keeps it from sleep.
}

midiOutCloseStream(h_stream, ByRef MIDIHDR) { ; unprepare header and close stream
  result := DllCall("winmm.dll\midiOutUnprepareHeader", UInt,h_stream, UInt,&MIDIHDR, UInt,36, UInt)
  If (result) {
    MsgBox Error %result% in midiOutUnprepareHeader
    Return -1
  }
  result := DllCall("winmm.dll\midiStreamClose", UInt,h_stream, UInt) ; CloseMidiStream
  If (result)  {
    MsgBox Error %result% in midiStreamClose
    Return -1
  }
}

;;;;;;;;;;;;;;; Utility Functions ;;;;;;;;;;;;;;;

MidiOutGetNumDevs() { ; Get number of midi output devices on system, first device has an ID of 0
  Return DllCall("winmm.dll\midiOutGetNumDevs")
}

MidiOutNameGet(uDeviceID = 0) { ; Get name of a midiOut device for a given ID
;MIDIOUTCAPS struct
;    WORD      wMid;
;    WORD      wPid;
;    MMVERSION vDriverVersion;
;    CHAR      szPname[MAXPNAMELEN];
;    WORD      wTechnology;
;    WORD      wVoices;
;    WORD      wNotes;
;    WORD      wChannelMask;
;    DWORD     dwSupport;

  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 Error %result% (ErrorLevel = %ErrorLevel%) in retrieving the name of midi output %uDeviceID%
    Return -1
  }
  VarSetCapacity(PortName, PortNameSize)
  DllCall("RtlMoveMemory", Str,PortName, Uint,&MidiOutCaps+OffsettoPortName, Uint,PortNameSize)
  Return PortName
}

MidiOutsEnumerate() { ; Returns number of midi output devices, creates global array MidiOutPortName with their names
  Local NumPorts, PortID
  MidiOutPortName =
  NumPorts := MidiOutGetNumDevs()

  Loop %NumPorts% {
    PortID := A_Index -1
    MidiOutPortName%PortID% := MidiOutNameGet(PortID)
  }
  Return NumPorts
}

[email protected](ptr) {
   Return *ptr | *(ptr+1) << 8 | *(ptr+2) << 16 | *(ptr+3) << 24
}

PokeInt(p_value, p_address) { ; Windows 2000 and later
   DllCall("ntdll\RtlFillMemoryUlong", UInt,p_address, UInt,4, UInt,p_value)
}
Examples:
#SingleInstance Force
#NoEnv
;;;;;;;;;;;;;; single middle C note by midiOutShortMsg ;;;;;;;;;;;;;;;;;

  ; To send a series of events that require precise timing, use the MidiStream functions instead.

  channel := 1          ; midi channel to send on
  Note    := 60         ; midi number for middle C
  NoteDur := 1000       ; duration to hold note for (approx.)
  NoteVel := 100        ; velocity of note to send

  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
  {
    msg := "", NumPorts := MidiOutsEnumerate() ; fills global array MidiOutPortName
    Loop % NumPorts {
      Port := A_Index -1
      msg := msg . "ID: " . Port . " --> " . MidiOutPortName%Port% . "`n"
    }
    InputBox MidiDevice, Select Midi Port, Enter the number of the port you would like to use`n`n%msg%,, 450, % (NumPorts * 35 + 200),,,,,0
    If (ErrorLevel)
        Exit
  } Else MidiDevice = 0

  OpenCloseMidiAPI()
  h_midiout := midiOutOpen(MidiDevice)

  midiOutShortMsg(h_midiout, "N1", Channel, Note, NoteVel) ; "N1" = "NoteOn"

  Sleep %NoteDur%

  midiOutShortMsg(h_midiout, "N0", Channel, Note, 0) ; Send Note-Off command for middle C

  midiOutClose(h_midiout)
  OpenCloseMidiAPI()

;;;;;;;;;;;;;; Send two notes on device 0 by MidiStream ;;;;;;;;;;;;;;

  MsgBox Two keys
  OpenCloseMidiAPI()
  h_stream := midiStreamOpen(0)
  SetTempoandTimebase(h_stream, 120, 96)                ; 5 ms time base
  BufSize := 4*12
  VarSetCapacity(MidiBuffer, BufSize, 0)                ; \/ time increment (since last event)
  AddEventToBuffer(MidiBuffer,  0, "CC", 1, 7, 128, 1)  ; ControlChange, ch = 1, volume, 128 (max)
  AddEventToBuffer(MidiBuffer,  0, "N1", 1, 60, 64)     ; NoteOn,        ch = 1, middle C, velocity 64
  AddEventToBuffer(MidiBuffer, 99, "N1", 1, 99,128)     ; NoteON                           velocity MAX
  AddEventToBuffer(MidiBuffer,  0, "N0", 1, 60, 64)     ; NoteOff,       ch = 1, middle C, velocity 64
  midiOutputBuffer(h_stream, MidiBuffer, BufSize, 2000) ; --> MIDIHDR global
  midiOutCloseStream(h_stream, MIDIHDR)
  OpenCloseMidiAPI()

;;;;;;;;;;;;;; Send a decaying series of C notes ;;;;;;;;;;;;;;

  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
  {
    msg := "", NumPorts := MidiOutsEnumerate() ; fills global array MidiOutPortName
    Loop % NumPorts {
      Port := A_Index -1
      msg := msg . "ID: " . Port . " --> " . MidiOutPortName%Port% . "`n"
    }
    InputBox MidiDevice, Select Midi Port, Enter the number of the port you would like to use`n`n%msg%,, 450, % (NumPorts * 35 + 200),,,,,0
    If (ErrorLevel)
        Exit
  } Else MidiDevice = 0

  OpenCloseMidiAPI()
  ch := 9               ; midi channel to send on
  Note := 60            ; Note to send (C)
  vStart := 128         ; first velocity
  vEnd := 0             ; last velocity
  vStep := -4           ; incremental value of each successive velocity sent
  StartDelay := 700     ; ms before sending the 2nd event. The 1st event is sent immediately to initialize
  SeriesDuration:= 6000 ; ms duration of the entire series
  BPM := 125            ; Bits Per Minute
  PPQ := 960            ; Parts (tick) Per Quarter note
  tck := 60000/PPQ/BPM  ; tick period (0.5ms)

  If (-1 = h_stream := midiStreamOpen(MidiDevice))  ; Open the midi port for streaming
     Exit
  If (-1 = SetTempoandTimebase(h_stream, BPM, PPQ)) ; tempo of 125BPM, timbease of 960ppq
     Exit

  NumEvents := (vEnd-vStart)//vStep       ; doesn't include the 1st event, sent separately
  BufSize := 12 * (NumEvents + 1)         ; each midi event takes 12 bytes, +1 for 1st event
  VarSetCapacity(MidiBuffer, BufSize, 0)
  EventTicks := Round(SeriesDuration/NumEvents/tck) ; ticks between start of events

  AddEventToBuffer(MidiBuffer, 0, "CC", ch, 7, 128, 1) ; First event in buffer: max volume

  AddEventToBuffer(MidiBuffer,StartDelay//tck,"N1",ch,Note,NextV:=vStart) ; Create 2nd MidiEvent after StartDelay

  Loop % NumEvents - 1                    ; Create remaining midi events
    AddEventToBuffer(MidiBuffer, EventTicks, "N1", ch, Note, NextV+=vStep)

  If (-1 = midiOutputBuffer(h_stream, MidiBuffer, BufSize, StartDelay+SeriesDuration)) ; Play Midi Buffer
     Exit

  midiOutCloseStream(h_stream, MIDIHDR)
  OpenCloseMidiAPI()
There are hundreds of changes, so I may have introduced new bugs. If something does not work, compare the failing part with the original.