 |
AutoHotkey Community Let's help each other out
|
| View previous topic :: View next topic |
| Author |
Message |
TomB
Joined: 14 Jan 2005 Posts: 17
|
Posted: Fri Apr 27, 2007 8:25 pm Post subject: MIDI Output from AHK |
|
|
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 Example
| Code: |
;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
| Code: |
;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
|
Last edited by TomB on Mon Apr 30, 2007 1:41 pm; edited 1 time in total |
|
| Back to top |
|
 |
Chris Site Admin
Joined: 02 Mar 2004 Posts: 10667
|
Posted: Sat Apr 28, 2007 7:58 am Post subject: |
|
|
Great presentation and comments. You packaged evreything up very nicely.
This looks like it will be a great asset to users of MIDI. |
|
| Back to top |
|
 |
Laszlo
Joined: 14 Feb 2005 Posts: 4502 Location: Boulder, CO
|
Posted: Sat Apr 28, 2007 3:37 pm Post subject: |
|
|
| 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. |
|
| Back to top |
|
 |
TomB
Joined: 14 Jan 2005 Posts: 17
|
Posted: Mon Apr 30, 2007 4:10 am Post subject: |
|
|
Laszlo,
| Quote: | 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 |
|
| Back to top |
|
 |
Laszlo
Joined: 14 Feb 2005 Posts: 4502 Location: Boulder, CO
|
Posted: Mon Apr 30, 2007 8:45 pm Post subject: |
|
|
| 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. |
|
| Back to top |
|
 |
TomB
Joined: 14 Jan 2005 Posts: 17
|
Posted: Tue May 01, 2007 2:26 am Post subject: |
|
|
Laszlo,
| Quote: |
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 | Code: | | msgbox There was an error opening the midi port. The port may be in use. Try closing and reopening all midi-enabled applications.) |
To:
| Code: | | 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+
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+1
00096 #define MMSYSERR_VALNOTFOUND (MMSYSERR_BASE+19)
00097 #define MMSYSERR_NODRIVERCB (MMSYSERR_BASE+20)
00098 #define MMSYSERR_LASTERROR (MMSYSERR_BASE+20)
Tom |
|
| Back to top |
|
 |
majkinetor
Joined: 24 May 2006 Posts: 4114 Location: Belgrade
|
Posted: Tue May 01, 2007 9:35 am Post subject: |
|
|
I will check it out with some of the number of virtual synts I have here.
Thx. _________________
 |
|
| Back to top |
|
 |
Sean
Joined: 12 Feb 2007 Posts: 2185
|
Posted: Tue May 01, 2007 11:26 am Post subject: Re: MIDI Output from AHK |
|
|
| TomB wrote: | | 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. |
|
| Back to top |
|
 |
TomB
Joined: 14 Jan 2005 Posts: 17
|
Posted: Tue May 01, 2007 1:40 pm Post subject: |
|
|
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 |
|
| Back to top |
|
 |
Laszlo
Joined: 14 Feb 2005 Posts: 4502 Location: Boulder, CO
|
Posted: Tue May 01, 2007 1:42 pm Post subject: |
|
|
| 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 |
|
| Back to top |
|
 |
Sean
Joined: 12 Feb 2007 Posts: 2185
|
Posted: Tue May 01, 2007 2:46 pm Post subject: |
|
|
| Laszlo wrote: | | 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
| Code: | midiOutOpen(1)
midiStreamOpen(1) |
Try to change 1 to 0 or -1. |
|
| Back to top |
|
 |
Laszlo
Joined: 14 Feb 2005 Posts: 4502 Location: Boulder, CO
|
Posted: Tue May 01, 2007 2:59 pm Post subject: |
|
|
| Sean wrote: | strangely hard coded DeviceID to 1 like
| Code: | 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. |
|
| Back to top |
|
 |
Laszlo
Joined: 14 Feb 2005 Posts: 4502 Location: Boulder, CO
|
Posted: Tue May 01, 2007 8:12 pm Post subject: |
|
|
| I guess the MS Wave device needs other parameters, than the SONAR fader. Does anyone know them? |
|
| Back to top |
|
 |
Laszlo
Joined: 14 Feb 2005 Posts: 4502 Location: Boulder, CO
|
Posted: Wed May 02, 2007 8:31 pm Post subject: |
|
|
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. | Code: | 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) |
|
|
| Back to top |
|
 |
Laszlo
Joined: 14 Feb 2005 Posts: 4502 Location: Boulder, CO
|
Posted: Thu May 03, 2007 4:28 pm Post subject: |
|
|
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: | Code: | ;;;;;;;;; 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 UInt@(&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 UInt@(&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
}
UInt@(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: | Code: | #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. |
|
| Back to top |
|
 |
|
|
You can post new topics in this forum You can reply to topics in this forum
|
Powered by phpBB © 2001, 2005 phpBB Group
|