Laszlo wrote:
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.
Laszlo's general functions: unedited from this thread
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)
}
Laszlo, or another...
Can you please make a midi port selection in a traymenu item called
midi_out port into your general functions.ahk (midi out)?
The
midi_in_lib from orbik does this, but I can't, figure out how do it for the midi output part?
Rockum and I have something very interesting we have been collaborating on and will release it soon, but we have a few stumbling blocks, to making it look nice.
You have already helped a lot, but we need this part too.
Please!
Here is the code from orbik's midi_in_lib
Code:
midi_in_Open(defaultDevID = -1)
{
global
if ((midi_in_hModule := DllCall("LoadLibrary", Str,A_ScriptDir . "\midi_in.dll")) == 0)
{
MsgBox Cannot load library midi_in.dll"
return 1
}
if (defaultDevID >= DllCall("midi_in.dll\getNumDevs"))
defaultDevID := -1
midi_in_MakeTrayMenu(defaultDevID)
if (defaultDevID >= 0)
midi_in_OpenDevice(defaultDevID)
return 0
}
midi_in_MakeTrayMenu(defaultDevID)
{
numDevs := DllCall("midi_in.dll\getNumDevs")
global midi_in_lastSelectedMenuItem
Menu devNameMenu, Add, No input, sub_menu_openinput
Menu devNameMenu, Add ; separator
if (defaultDevID < 0)
midi_in_lastSelectedMenuItem := "No Input"
loop %numDevs%
{
devID := A_Index-1
if ((devName := DllCall("midi_in.dll\getDevName", Int,devID, Str)) == 0)
{
MsgBox, Error in creating midi input device list
return 1
}
Menu devNameMenu, Add, %devName%, sub_menu_openinput
if (devID == defaultDevID)
{
Menu devNameMenu, Check, %devName%
midi_in_lastSelectedMenuItem := devName
}
}
Menu TRAY, Add, MIDI-in device, :devNameMenu
}
sub_menu_openinput:
midi_in_OpenDevice(A_ThisMenuItemPos-3)
; Move the check mark to new position
Menu %A_ThisMenu%, Check, %A_ThisMenuItem%
Menu %A_ThisMenu%, Uncheck, %midi_in_lastSelectedMenuItem%
midi_in_lastSelectedMenuItem := A_ThisMenuItem
return
midi_in_OpenDevice(deviceID) ;deviceID < 0 means no input
{
Critical
midi_in_Stop()
Gui +LastFound
hWnd := WinExist()
curDevID := DllCall("midi_in.dll\getCurDevID", Int)
if (deviceID == curDevID)
return 0
if (curDevID >= 0)
result := DllCall("midi_in.dll\close")
if (result)
{
MsgBox Error closing midi device`nmidi_in.dll\close returned %result%
return 1
}
if (deviceID < 0)
return 0
result := DllCall("midi_in.dll\open", UInt,hWnd, Int,deviceID, Int)
if (result)
{
MsgBox Error opening midi device`nmidi_in.dll\open(%hWnd%, %deviceID%) returned %result%
return 1
}
; MsgBox Press OK to start midi input
midi_in_Start()
return 0
}
midi_in_Close()
{
global
if (midi_in_hModule)
DllCall("FreeLibrary", UInt,midi_in_hModule), midi_in_hModule := ""
}
midi_in_Start()
{
DllCall("midi_in.dll\start")
}
midi_in_Stop()
{
DllCall("midi_in.dll\stop")
}
listenNote(noteNumber, funcName, channel=0)
{
global msgNum
GoSub, sub_increase_msgnum
DllCall("midi_in.dll\listenNote", Int,noteNumber, Int,channel, Int,msgNum)
OnMessage(msgNum, funcName)
}
listenNoteRange(rangeStart, rangeEnd, funcName, flags=0, channel=0)
{
global msgNum
GoSub, sub_increase_msgnum
msgCount := DllCall("midi_in.dll\listenNoteRange", int,rangeStart, int,rangeEnd, int,(flags & 0x07), int,channel, int,msgNum)
if (msgCount <= 0)
return
if (flags & 0x01)
loop %msgCount%
{
OnMessage(msgNum, funcName . A_Index)
GoSub, sub_increase_msgnum
}
else
OnMessage(msgNum, funcName)
}
listenCC(ccNumber, funcName, channel=0)
{
global msgNum
GoSub, sub_increase_msgnum
DllCall("midi_in.dll\listenCC", Int,ccNumber, Int,channel, Int,msgNum)
OnMessage(msgNum, funcName)
}
listenWheel(funcName, channel=0)
{
global msgNum
GoSub, sub_increase_msgnum
DllCall("midi_in.dll\listenWheel", Int,channel, Int,msgNum)
OnMessage(msgNum, funcName)
}
listenChanAT(funcName, channel=0)
{
global msgNum
GoSub, sub_increase_msgnum
DllCall("midi_in.dll\listenChanAT", Int,channel, Int,msgNum)
OnMessage(msgNum, funcName)
}
getNoteOn(noteNumber, channel)
{
return DllCall("midi_in.dll\getNoteOn", Int,noteNumber, Int,channel)
}
getCC(ccNumber, channel)
{
return DllCall("midi_in.dll\getCC", Int,ccNumber, Int,channel)
}
getWheel(channel)
{
return DllCall("midi_in.dll\getWheel", Int,channel)
}
getChanAT(channel)
{
return DllCall("midi_in.dll\getChanAT", Int,channel)
}
sub_increase_msgnum:
if msgNum
msgNum++
else
msgNum := 0x2000
return
We just need the midi out port on the traymenu along with the midi in port (which is already there).
Here was my lame attempt ....
Code:
; This was copied from your file, general functions.
; I tired to insert my commented code into your general functions
; below this point
; above code snipped.
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
}
/*
midi_out_MakeTrayMenu(uDeviceID)
{
numDevs := MidiOutNameGet(PortID) ;DllCall("midi_in.dll\getNumDevs")
global midi_out_lastSelectedMenuItem
Menu devNameMenu, Add, No input, sub_menu_openOutput
Menu devNameMenu, Add ; separator
if (uDeviceID < 0)
midi_out_lastSelectedMenuItem := "No Output"
loop %numDevs%
{
uDeviceID := A_Index-1
if ((devName := DllCall("midi_in.dll\getDevName", Int,uDeviceID, Str)) == 0)
{
MsgBox, Error in creating midi output device list
return 1
}
Menu devNameMenu, Add, %devName%, sub_menu_openOutput
if (uDeviceID == defaultuDeviceID)
{
Menu devNameMenu, Check, %devName%
midi_out_lastSelectedMenuItem := devName
}
}
Menu TRAY, Add, MIDI-out device, :devNameMenu
}
sub_menu_openOutput:
midi_out_OpenDevice(A_ThisMenuItemPos-3)
; Move the check mark to new position
Menu %A_ThisMenu%, Check, %A_ThisMenuItem%
Menu %A_ThisMenu%, Uncheck, %midi_out_lastSelectedMenuItem%
midi_out_lastSelectedMenuItem := A_ThisMenuItem
return
*/
A million thanks to you!