Controlling Excel via Midi controller
Asked Answered
H

3

24

I've got one of these nanoKontrol's
(source: netdna-cdn.com)

and wanted to use the sliders on it to control Excel, just like one of the Excel form control scroll bars.

I've managed to modify this code for VBA, but it is extremely unstable. Can anyone help me stabilize it? I think the function MidiIn_Event may crash if it doesn't return fast enough, but I may be wrong.

Thanks in advance.

Public Const CALLBACK_FUNCTION = &H30000
Public Declare Function midiInOpen Lib "winmm.dll" 
        (lphMidiIn As Long, 
        ByVal uDeviceID As Long, ByVal dwCallback As Any, 
        ByVal dwInstance As Long, ByVal dwFlags As Long) As Long
Public Declare Function midiInClose Lib "winmm.dll" 
        (ByVal hMidiIn As Long) As Long
Public Declare Function midiInStart Lib "winmm.dll" 
        (ByVal hMidiIn As Long) As Long
Public Declare Function midiInStop Lib "winmm.dll" 
        (ByVal hMidiIn As Long) As Long
Public Declare Function midiInReset Lib "winmm.dll" 
        (ByVal hMidiIn As Long) As Long
Private ri As Long

Public Sub StartMidiFunction()
    Dim lngInputIndex As Long
    lngInputIndex=0
    Call midiInOpen(ri, lngInputIndex, AddressOf MidiIn_Event, 
            0, CALLBACK_FUNCTION)
    Call midiInStart(ri)
End Function

Public Sub EndMidiRecieve()
    Call midiInReset(ri)
    Call midiInStop(ri)
    Call midiInClose(ri)
End Sub

Public Function MidiIn_Event(ByVal MidiInHandle As Long, 
        ByVal Message As Long, ByVal Instance As Long, 
        ByVal dw1 As Long, ByVal dw2 As Long) As Long

    'dw1 contains the midi code
    If dw1 > 255 Then 'Ignore time codes
        Call MsgBox(dw1)    'This part is unstable
    End If
End Function        
Hubert answered 30/12, 2012 at 22:27 Comment(7)
I'm going to upvote this just for the sheer randomness of the title.Nablus
GlovePIE can do this for you out of the box. glovepie.org/glovepie.phpFusillade
GlovePIE looks like a useful app, but I can't see how to link it to values of individual cells in Excel, it just turns one form of input into another. Also I'd rather not have to install extra software if Excel.VBA can handle everything.Hubert
I'm upvoting Virkkunen's comment ... and prolly the post too, since the presentation is good too. Great nerve for a SO newbie.Motheaten
Try declaring MidiIn_Event as a sub (because it is). If doesn't help, try replacing MsgBox with MessageBox.Unshakable
MidiIn_Event should be called async, so MsgBox itself could cause the instability. Use log instead of modal window.Trod
This is just an idea and I am not very experienced but if "MidiIn_Event" causes the crash, could you use some sort of "Wait" command. I don't know what that would be, but it's worth a try if nothing else works.Malefactor
A
2

The problem is probably MsgBox:

  • Since the MIDI events use callbacks they are most likely run from another thread. VBA is inherently single-threaded (see e.g. Multi-threading in VBA), so trying to show a modal dialog from another thread will likely cause issues (undefined behavior, crash, anything else...)
  • MIDI usually triggers huge amounts of events (the tiniest movement of a slider or knob would trigger an event), so moving something a noticeable amount may cause hundreds of events. Showing a dialog (requiring an OK click) at each event could be a problem.

For testing, try to replace Call MsgBox(dw1) with Debug.Print dw1 so that the values are just printed in the Immediate Window, which should be much more stable. If you are trying to execute some simple action (e.g. update the value in a cell, scroll the window) you may be able to get away with it as long as each call to MidiIn_Event completes before the next event.

A much more complex but stable solution could be to push data points onto a queue in the event handler, and use a repeating timer in VBA that pops items from the queue and executes some action on the VBA thread.

Aplomb answered 24/1, 2013 at 23:33 Comment(3)
When using Debug.Print dw1 I don't experience any crashing. Great! I tried the Application.OnTime method given by user1835812 below, without sucess. Can you suggest another way to implement your stable solution? Should I start messing about with multithreading in VBA or should I start to write a .NET solution? Thanks for your adviceHubert
I think multithreading in VBA is not going to work. user1835812's solution below should work but it's missing Application.OnTime alertTime, "EventMacro" at the end of the EventMacro function. If you add that, cell A1 should update once/second with the value from the controller. You may want to assign some initial value to lngMessage in Workbook_Open.Aplomb
I have added the extra code as you suggest, but unfortunately if 'Application.OnTime' is called at the same time that a Midi event occurs then Excel crashes. Shame, your approach looked so promising.Hubert
N
1

This is so fantasticly cool :D

but the message box as mentioned above will kill it, but removing the messagebox will probably not help that much. You want to minimize the abount of traffic to excel too because the vba->excel will not be instantanious.

Soooo the solution would be

on workbook start macro

    Public lngMessage As String

    Private Sub Workbook_Open()
        alertTime = Now + TimeValue("00:00:01")
        Application.OnTime alertTime, "EventMacro"
    End Sub
    Sub EventMacro()
        ActiveSheet.Cells(1, 1).Value = lngMessage
        alertTime = Now + TimeValue("00:00:01")
    End Sub

    Public Function MidiIn_Event(ByVal MidiInHandle As Long, ByVal Message As Long, ByVal Instance As Long, ByVal dw1 As Long, ByVal dw2 As Long) As Long
        'dw1 contains the midi code
        If dw1 > 255 Then 'Ignore time codes
            lngMessage = dw1    'This part is now happy
        End If
    End Function
Ng answered 25/1, 2013 at 14:7 Comment(1)
I think this is along the right lines, but unfortunatly it is also unstable. If I create a few midi events at around the same time that "EventMacro" is being called then excel crashes (at times inbetween this code works fine).Hubert
L
1

You need a general Function that processes the data given by the MidiIn_Event one, in my example bellow that function is the runClock() one.

I did this that is able the uses the status bar to count the keys and clock type of messages.

Option Explicit

Private Const CALLBACK_FUNCTION = &H30000

'MIDI Functions here: https://learn.microsoft.com/en-us/windows/win32/multimedia/midi-functions
#If Win64 Then
    Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As LongPtr)
    Private Declare PtrSafe Function timeGetTime Lib "winmm.dll" () As Long
    'For MIDI device INPUT
    Private Declare PtrSafe Function midiInOpen Lib "winmm.dll" (lphMidiIn As LongPtr, ByVal uDeviceID As LongPtr, ByVal dwCallback As LongPtr, ByVal dwInstance As LongPtr, ByVal dwFlags As LongPtr) As Long
    Private Declare PtrSafe Function midiInClose Lib "winmm.dll" (ByVal hMidiIn As LongPtr) As Long
    Private Declare PtrSafe Function midiInStart Lib "winmm.dll" (ByVal hMidiIn As LongPtr) As Long
    Private Declare PtrSafe Function midiInStop Lib "winmm.dll" (ByVal hMidiIn As LongPtr) As Long
    Private Declare PtrSafe Function midiInReset Lib "winmm.dll" (ByVal hMidiIn As LongPtr) As Long
#Else
    Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
    Private Declare Function timeGetTime Lib "winmm.dll" () As Long
    'For MIDI device INPUT
    Private Declare Function midiInOpen Lib "winmm.dll" (lphMidiIn As Long, ByVal uDeviceID As Long, ByVal dwCallback As Long, ByVal dwInstance As Long, ByVal dwFlags As Long) As Long
    Private Declare Function midiInClose Lib "winmm.dll" (ByVal hMidiIn As Long) As Long
    Private Declare Function midiInStart Lib "winmm.dll" (ByVal hMidiIn As Long) As Long
    Private Declare Function midiInStop Lib "winmm.dll" (ByVal hMidiIn As Long) As Long
    Private Declare Function midiInReset Lib "winmm.dll" (ByVal hMidiIn As Long) As Long
#End If

#If Win64 Then
    Private mlngCurDevice      As Long
    Private mlngHmidi          As LongPtr
#Else
    Private mlngCurDevice      As Long
    Private mlngHmidi          As Long
#End If

Private ClockTicks             As Integer
Private Notes                  As Integer
Private Looper                 As Long
Private LongMessage            As Long
Private actualTime             As Long

Public Sub runClock()

    'When canceled become able to close opened Input devices (For ESC press)
    On Error GoTo handleCancel
    Application.EnableCancelKey = xlErrorHandler

    With Application
        .Calculation = xlCalculationManual
        .ScreenUpdating = False
        '.DisplayStatusBar = False
        '.EnableEvents = False
    End With

    mlngCurDevice = 8 'My Device is 8 but yours is 0
    Notes = 0
    Looper = 0

    'Open Input Device
    Call midiInOpen(mlngHmidi, mlngCurDevice, AddressOf MidiIn_Event, 0, CALLBACK_FUNCTION)

    'Ends only when Status is different from 0
    Do While Notes < 10
        'Reset Status count
        ClockTicks = 0

        'Begins lissinting the MIDI input
        Call midiInStart(mlngHmidi)

        'Loops until the right message is given <= 255 and > 0
        Do While ClockTicks < 1000 And Notes < 10
            'Sleep if needed
            Sleep 10
            Application.StatusBar = "Looper=" & Looper & " | Notes=" & Notes & " | ClockTicks=" & ClockTicks & " | Message=" & LongMessage
            Looper = Looper + 1
            'DoEvents enables ESC key
            If Abs(timeGetTime - actualTime) > 3000 Then
                DoEvents
                actualTime = timeGetTime
            End If
        Loop

        'Ends lisingting the MIDI input
        Call midiInReset(mlngHmidi)
        Call midiInStop(mlngHmidi)

    Loop

    'Closes Input device
    Do While midiInClose(mlngHmidi) <> 0
    Loop

    With Application
        .Calculation = xlCalculationAutomatic
        .ScreenUpdating = True
        .DisplayStatusBar = True
        .EnableEvents = True
    End With

    MsgBox "ENDED WITH SUCCESS", , "Message:"

    'Close all opened MIDI Inputs when canceled (ESC key pressed)
handleCancel:
        If Err.Number = 18 Then

            'Ends lisingting the MIDI input
            Call midiInReset(mlngHmidi)
            Call midiInStop(mlngHmidi)
            Do While midiInClose(mlngHmidi) <> 0
            Loop

            With Application
                .Calculation = xlCalculationAutomatic
                .ScreenUpdating = True
                .DisplayStatusBar = True
                .EnableEvents = True
            End With

            MsgBox "ENDED WITH SUCCESS", , "Message:"

        End If

End Sub

Private Function MidiIn_Event(ByVal mlngHmidi As Long, ByVal Message As Long, ByVal Instance As Long, ByVal dw1 As Long, ByVal dw2 As Long) As Long

    'The value 963 is the MIM_DATA concerning regular MIDI messages
    If Message = 963 Then
        LongMessage = Message
        If dw1 > 255 Then
            Notes = Notes + 1
        Else
            ClockTicks = ClockTicks + 1
        End If
    End If

End Function

The issue comes when ESC key is presses while receiving MIDI data, like clock sync, for some reason, and despite everything else works well, the ESC key many times crashes the script. However if you don't use the ESC key during input MIDI messages you will not have this problem.

Nevertheless I would like to know why pressing the ESC key while receiving clock signals crashes the script.

You just need to adapt the global variable to your needs.

Hope I have helped.

Labrecque answered 29/12, 2019 at 8:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.