none
Using winmm.dll to generate MIDI audio

    General discussion

  • The topic of working with MIDI files from VB.Net comes up from time to time and generally, if you just want to play a MIDI file, the easiest solution is to use an instance of MediaPlayer which can play MIDI files.

    But if you want to actually control the MIDI device and play music note-by-note, then things get a little more involved.  The winmm.dll offers an API over MIDI devices, but requires unmanaged code to utilize.  There are a number of wrappers available but they tend to be cumbersome and/or bloated for simple audio playback.  The winmm.dll provides full functionality for both MIDI Input and MIDI Output devices, but much of this functionality is unneeded when the only goal is to generate some MIDI music from code.

    To resolve this issue and provide the basic foundation for MIDI interfacing with VB.Net, the following code will introduce the SimpleMidiEngine API for generating MIDI music via code, or from simple strings of note information.

    CodeProject Article was used as reference for the winmm implementation, along with the Microsoft reference documentation.  Instrument names were taken from this site, while general MIDI message information was found here.

    The first class in the project is the NativeMethods class which wraps the winmm API calls and provides the required managed types:

    Option Strict On
    Imports System.Runtime.InteropServices
    
    'translated in part from: https://www.codeproject.com/articles/6228/c-midi-toolkit
    Public NotInheritable Class NativeMethods
        Public Delegate Sub MidiOutProc(handle As IntPtr, msg As Integer, instance As Integer,
                 param1 As Integer, param2 As Integer)
    
        Protected Declare Auto Function midiOutOpen Lib "winmm.dll" (ByRef handle As IntPtr, deviceId As Integer, proc As MidiOutProc, instance As Integer, flags As Integer) As Integer
        Protected Declare Auto Function midiOutClose Lib "winmm.dll" (handle As IntPtr) As Integer
        Protected Declare Auto Function midiOutReset Lib "winmm.dll" (handle As IntPtr) As Integer
        Protected Declare Auto Function midiOutShortMsg Lib "winmm.dll" (handle As IntPtr, message As Integer) As Integer
        Protected Declare Auto Function midiOutGetDevCaps Lib "winmm.dll" (handle As IntPtr, ByRef caps As MidiOutCaps, sizeOfmidiOutCaps As Integer) As Integer
        Protected Declare Auto Function midiOutGetNumDevs Lib "winmm.dll" () As Integer
    
        Protected Const MMSYSERR_NOERROR As Integer = 0
        Public Const CALLBACK_FUNCTION As Integer = &H30000
    
        Public Shared ReadOnly Property MidiOutCapsSize As Integer = Marshal.SizeOf(Of MidiOutCaps)
    
        Public Shared Sub OpenPlaybackDevice(ByRef handle As IntPtr, deviceId As Integer, proc As MidiOutProc, instance As Integer, flags As Integer)
            If Not midiOutOpen(handle, deviceId, proc, instance, flags) = MMSYSERR_NOERROR Then Throw New Exception("Open failed")
        End Sub
    
        Public Shared Sub ClosePlaybackDevice(handle As IntPtr)
            If Not midiOutClose(handle) = MMSYSERR_NOERROR Then Throw New Exception("Close failed")
        End Sub
    
        Public Shared Sub ResetPlaybackDevice(handle As IntPtr)
            If Not midiOutReset(handle) = MMSYSERR_NOERROR Then Throw New Exception("Reset failed")
        End Sub
    
        Public Shared Sub SendPlaybackDeviceMessage(handle As IntPtr, message As Integer)
            If Not midiOutShortMsg(handle, message) = MMSYSERR_NOERROR Then Throw New Exception("Send message failed")
        End Sub
    
        Public Shared Sub GetPlaybackDeviceCapabilities(handle As IntPtr, ByRef caps As MidiOutCaps, sizeOfmidiOutCaps As Integer)
            If Not midiOutGetDevCaps(handle, caps, sizeOfmidiOutCaps) = MMSYSERR_NOERROR Then Throw New Exception("Get device capabilities failed")
        End Sub
    
        Public Shared Function GetPlaybackDeviceCount() As Integer
            Dim result = midiOutGetNumDevs
            If Not result > MMSYSERR_NOERROR Then Throw New Exception("Get device count failed - no devices present")
            Return result
        End Function
    
        Protected Sub New()
        End Sub
    
        'translated from: https://www.codeproject.com/articles/6228/c-midi-toolkit
        Public Structure MidiOutCaps
            ''' <summary>
            ''' Manufacturer identifier of the device driver for the Midi output 
            ''' device. 
            ''' </summary>
            Public ManufacturerId As Short
    
            ''' <summary>
            ''' Product identifier of the Midi output device. 
            ''' </summary>
            Public ProductId As Short
    
            ''' <summary>
            ''' Version number of the device driver for the Midi output device. The 
            ''' high-order byte Is the major version number, And the low-order byte 
            ''' Is the minor version number. 
            ''' </summary>
            Public DriverVersion As Integer
    
            ''' <summary>
            ''' Product name string as ASCII byte array.
            ''' </summary>
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=32)>
            Public Name As Byte()
    
            ''' <summary>
            ''' Flags describing the type of the Midi output device. 
            ''' </summary>
            Public Technology As Short
    
            ''' <summary>
            ''' Number of voices supported by an internal synthesizer device. If 
            ''' the device Is a port, this member Is Not meaningful And Is set 
            ''' to 0. 
            ''' </summary>
            Public VoiceCount As Short
    
            ''' <summary>
            ''' Maximum number of simultaneous notes that can be played by an 
            ''' internal synthesizer device. If the device Is a port, this member 
            ''' Is Not meaningful And Is set to 0. 
            ''' </summary>
            Public NoteCount As Short
    
            ''' <summary>
            ''' Channels that an internal synthesizer device responds to, where the 
            ''' least significant bit refers to channel 0 And the most significant 
            ''' bit to channel 15. Port devices that transmit on all channels set 
            ''' this member to 0xFFFF. 
            ''' </summary>
            Public ChannelMask As Short
    
            ''' <summary>
            ''' Optional functionality supported by the device. 
            ''' </summary>
            Public Support As Integer
        End Structure
    End Class
    

    This is the primary code for working with winmm.dll and sending MIDI messages.  You could play music with this code alone, but it will be easier to add some helper classes for sending messages and representing musical note data.

    The MIDI messages can be wrapped in a simple 4-byte structure.  Only three of the bytes are actually used to hold information, the fourth is simply to make an even 4 bytes for integer conversion.

    <System.Runtime.InteropServices.StructLayout(Runtime.InteropServices.LayoutKind.Sequential)>
    Public Structure MidiShortMessage
        Private Const CHANNEL_MASK As Byte = &HF
        Private Const STATUS_MASK As Byte = &HF0
    
        Private data0 As Byte
        Private data1 As Byte
        Private data2 As Byte
        Private data3 As Byte
    
        Public Sub New(command As MessageCommandMask, midiChannel As Byte, value1 As Byte, value2 As Byte)
            StatusCommand = command
            Channel = midiChannel
            Parameter1 = value1
            Parameter2 = value2
        End Sub
    
        Public Property StatusCommand As MessageCommandMask
            Get
                Return CType(data0 >> 4, MessageCommandMask)
            End Get
            Set(value As MessageCommandMask)
                data0 = value Or (data0 And CHANNEL_MASK)
            End Set
        End Property
    
        Public Property Channel As Byte
            Get
                Return (data0 And CHANNEL_MASK)
            End Get
            Set(value As Byte)
                data0 = (data0 And STATUS_MASK) Or (value And CHANNEL_MASK)
            End Set
        End Property
    
        Public Property Parameter1 As Byte
            Get
                Return data1
            End Get
            Set(value As Byte)
                data1 = value
            End Set
        End Property
    
        Public Property Parameter2 As Byte
            Get
                Return data2
            End Get
            Set(value As Byte)
                data2 = value
            End Set
        End Property
    
        Public Shared Widening Operator CType(target As MidiShortMessage) As Integer
            Return BitConverter.ToInt32({target.data0, target.data1, target.data2, target.data3}, 0)
        End Operator
    
        Public Enum MessageCommandMask As Byte
            NoteOff = &H80
            NoteOn = &H90
            PolyKeyPressure = &HA0
            ControllerChange = &HB0
            ProgramChange = &HC0
            ChannelPressure = &HD0
            PitchBend = &HD0
        End Enum
    End Structure

    This message structure is used to send command such as setting the voice for a channel or playing notes.

    There will need to be a small class to encapsulate the MIDI playback device, holding its device id and information about its capabilities.

    Public Class PlaybackDevice
    
        Public Shared Function DefaultDevice() As PlaybackDevice
            Dim caps As New NativeMethods.MidiOutCaps
            NativeMethods.GetPlaybackDeviceCapabilities(0, caps, NativeMethods.MidiOutCapsSize)
            Return New PlaybackDevice(0, caps)
        End Function
    
        Public Shared Function GetDevices() As IEnumerable(Of PlaybackDevice)
            Dim result As New List(Of PlaybackDevice)
            Dim deviceCount = NativeMethods.GetPlaybackDeviceCount
            For i = 0 To deviceCount - 1
                Dim caps As New NativeMethods.MidiOutCaps
                NativeMethods.GetPlaybackDeviceCapabilities(i, caps, NativeMethods.MidiOutCapsSize)
                result.Add(New PlaybackDevice(i, caps))
            Next
            Return result.ToArray
        End Function
    
        Public Property DeviceId As Integer
        Public Property DeviceName As String
        Public Property VoiceCount As Integer
        Public Property ChannelEnabled As IEnumerable(Of Boolean)
        Public Property NoteCount As Integer
    
        Public Sub New(id As Integer, caps As NativeMethods.MidiOutCaps)
            DeviceId = id
            DeviceName = Text.ASCIIEncoding.ASCII.GetString(caps.name)
            VoiceCount = caps.VoiceCount
            Dim channels As New List(Of Boolean)
            For i = 0 To 15
                If (caps.ChannelMask And CShort(2 ^ i - 1)) > 0 Then
                    channels.Add(True)
                Else
                    channels.Add(False)
                End If
            Next
            NoteCount = caps.NoteCount
            ChannelEnabled = channels
        End Sub
    End Class

    With those classes and structures in place, we are almost ready to write a MidiPlayer.  But before we do, we'll need some objects and enums to represent the notes that we want to play, along with the instrument that will play them.

    The instrument (or Voice) can simply be represented by an enum of voice Ids.  The following list represents the default MIDI voice bank:

    Public Enum InstrumentVoice As Byte
        Acoustic_Grand_Piano = 1
        Bright_Acoustic_Piano = 2
        Electric_Grand_Piano = 3
        Honky_tonk_Piano = 4
        Electric_Piano_1 = 5
        Electric_Piano_2 = 6
        Harpsichord = 7
        Clavi = 8
        Celesta = 9
        Glockenspiel = 10
        Music_Box = 11
        Vibraphone = 12
        Marimba = 13
        Xylophone = 14
        Tubular_Bells = 15
        Dulcimer = 16
        Drawbar_Organ = 17
        Percussive_Organ = 18
        Rock_Organ = 19
        Church_Organ = 20
        Reed_Organ = 21
        Accordion = 22
        Harmonica = 23
        Tango_Accordion = 24
        Acoustic_Guitar_nylon = 25
        Acoustic_Guitar_steel = 26
        Electric_Guitar_jazz = 27
        Electric_Guitar_clean = 28
        Electric_Guitar_muted = 29
        Overdriven_Guitar = 30
        Distortion_Guitar = 31
        Guitar_harmonics = 32
        Acoustic_Bass = 33
        Electric_Bass_finger = 34
        Electric_Bass_pick = 35
        Fretless_Bass = 36
        Slap_Bass_1 = 37
        Slap_Bass_2 = 38
        Synth_Bass_1 = 39
        Synth_Bass_2 = 40
        Violin = 41
        Viola = 42
        Cello = 43
        Contrabass = 44
        Tremolo_Strings = 45
        Pizzicato_Strings = 46
        Orchestral_Harp = 47
        Timpani = 48
        String_Ensemble_1 = 49
        String_Ensemble_2 = 50
        SynthStrings_1 = 51
        SynthStrings_2 = 52
        Choir_Aahs = 53
        Voice_Oohs = 54
        Synth_Voice = 55
        Orchestra_Hit = 56
        Trumpet = 57
        Trombone = 58
        Tuba = 59
        Muted_Trumpet = 60
        French_Horn = 61
        Brass_Section = 62
        SynthBrass_1 = 63
        SynthBrass_2 = 64
        Soprano_Sax = 65
        Alto_Sax = 66
        Tenor_Sax = 67
        Baritone_Sax = 68
        Oboe = 69
        English_Horn = 70
        Bassoon = 71
        Clarinet = 72
        Piccolo = 73
        Flute = 74
        Recorder = 75
        Pan_Flute = 76
        Blown_Bottle = 77
        Shakuhachi = 78
        Whistle = 79
        Ocarina = 80
        Lead_1_square = 81
        Lead_2_sawtooth = 82
        Lead_3_calliope = 83
        Lead_4_chiff = 84
        Lead_5_charang = 85
        Lead_6_voice = 86
        Lead_7_fifths = 87
        Lead_8_bass_lead = 88
        Pad_1_new_age = 89
        Pad_2_warm = 90
        Pad_3_polysynth = 91
        Pad_4_choir = 92
        Pad_5_bowed = 93
        Pad_6_metallic = 94
        Pad_7_halo = 95
        Pad_8_sweep = 96
        FX_1_rain = 97
        FX_2_soundtrack = 98
        FX_3_crystal = 99
        FX_4_atmosphere = 100
        FX_5_brightness = 101
        FX_6_goblins = 102
        FX_7_echoes = 103
        FX_8_sci_fi = 104
        Sitar = 105
        Banjo = 106
        Shamisen = 107
        Koto = 108
        Kalimba = 109
        Bag_pipe = 110
        Fiddle = 111
        Shanai = 112
        Tinkle_Bell = 113
        Agogo = 114
        Steel_Drums = 115
        Woodblock = 116
        Taiko_Drum = 117
        Melodic_Tom = 118
        Synth_Drum = 119
        Reverse_Cymbal = 120
        Guitar_Fret_Noise = 121
        Breath_Noise = 122
        Seashore = 123
        Bird_Tweet = 124
        Telephone_Ring = 125
        Helicopter = 126
        Applause = 127
        Gunshot = 128
    End Enum

    Next we'll need a way to represent each of the chromatic notes across 8 octaves of a full piano keyboard.  This can be done with another enum holding an encoded version of the note name along with the associated MIDI note code:

    Public Enum NoteMidiCode As Byte
        Rest = 0
        C8 = 108
        B7 = 107
        A7s = 106
        A7 = 105
        G7s = 104
        G7 = 103
        F7s = 102
        F7 = 101
        E7 = 100
        D7s = 99
        D7 = 98
        C7s = 97
        C7 = 96
        B6 = 95
        A6s = 94
        A6 = 93
        G6s = 92
        G6 = 91
        F6s = 90
        F6 = 89
        E6 = 88
        D6s = 87
        D6 = 86
        C6s = 85
        C6 = 84
        B5 = 83
        A5s = 82
        A5 = 81
        G5s = 80
        G5 = 79
        F5s = 78
        F5 = 77
        E5 = 76
        D5s = 75
        D5 = 74
        C5s = 73
        C5 = 72
        B4 = 71
        A4s = 70
        A4 = 69
        G4s = 68
        G4 = 67
        F4s = 66
        F4 = 65
        E4 = 64
        D4s = 63
        D4 = 62
        C4s = 61
        C4 = 60
        B3 = 59
        A3s = 58
        A3 = 57
        G3s = 56
        G3 = 55
        F3s = 54
        F3 = 53
        E3 = 52
        D3s = 51
        D3 = 50
        C3s = 49
        C3 = 48
        B2 = 47
        A2s = 46
        A2 = 45
        G2s = 44
        G2 = 43
        F2s = 42
        F2 = 41
        E2 = 40
        D2s = 39
        D2 = 38
        C2s = 37
        C2 = 36
        B1 = 35
        A1s = 34
        A1 = 33
        G1s = 32
        G1 = 31
        F1s = 30
        F1 = 29
        E1 = 28
        D1s = 27
        D1 = 26
        C1s = 25
        C1 = 24
        B0 = 23
        A0s = 22
        A0 = 21
    End Enum

    Each note needs to play for a specified duration.  The standard note durations are represented in the following enum:

    Public Enum NoteDuration
        ThirtysecondthNode = 31
        SixteenthNote = 62
        EigthNote = 125
        QuarterNote = 250
        HalfNote = 500
        WholeNote = 1000
    End Enum

    Finally we need a class to encapsulate a note, or notes, with a duration.  We can define this class as a "chord":

    Public Class Chord
        Public Property Notes As IEnumerable(Of NoteMidiCode)
        Public Property Duration As NoteDuration
        Public Property Velocity As Byte = 127
        Public ReadOnly Property IsRest As Boolean
            Get
                If Notes?.Count = 1 AndAlso Notes.First = NoteMidiCode.Rest Then Return True
                Return False
            End Get
        End Property
    End Class

    Now we can create a MidiPlayer class capable of opening a device and playing a series of chords on one or more channels.

    Option Strict On
    
    Public Class MidiPlayer
        Implements IDisposable
        Public Event MidiMessageReceived As EventHandler(Of MidiMessageReceivedEventArgs)
    
        Public Property Device As PlaybackDevice
        Public ReadOnly Property IsOpen As Boolean
    
        Protected handle As Integer
        Private messageHandler As NativeMethods.MidiOutProc
    
        Public Sub New()
            Device = PlaybackDevice.DefaultDevice
            messageHandler = New NativeMethods.MidiOutProc(AddressOf OnMidiMessageReceived)
        End Sub
    
        Protected Overridable Sub OnMidiMessageReceived(handle As Integer, msg As Integer, instance As Integer, param1 As Integer, param2 As Integer)
            RaiseEvent MidiMessageReceived(Me, New MidiMessageReceivedEventArgs(handle, msg, instance, param1, param2))
        End Sub
    
        Public Overridable Sub Close()
            If IsOpen Then
                NativeMethods.ResetPlaybackDevice(handle)
                NativeMethods.ClosePlaybackDevice(handle)
                _IsOpen = False
            End If
        End Sub
    
        Public Overridable Sub Open()
            NativeMethods.OpenPlaybackDevice(handle, Device.DeviceId, Nothing,
                    0, NativeMethods.CALLBACK_FUNCTION)
            _IsOpen = True
        End Sub
    
        Public Overridable Async Function Play(channel As Byte, chords As IEnumerable(Of Chord)) As Task
            If Device IsNot Nothing Then
                For Each chord In chords
                    Try
                        If Not chord.IsRest Then
                            For i = 0 To chord.Notes.Count - 1
                                Dim note = chord.Notes.ElementAt(i)
                                SendMessage(New MidiShortMessage(MidiShortMessage.MessageCommandMask.NoteOn, channel, note, chord.Velocity))
                            Next
                        End If
                        Await Task.Delay(chord.Duration)
                        If Not chord.IsRest Then
                            For i = 0 To chord.Notes.Count - 1
                                Dim note = chord.Notes.ElementAt(i)
                                SendMessage(New MidiShortMessage(MidiShortMessage.MessageCommandMask.NoteOn, channel, note, 0))
                            Next
                        End If
                    Catch disposed_ex As ObjectDisposedException
                        Exit For
                    Catch ex As Exception
                        Throw
                    End Try
                Next
            End If
        End Function
    
        Public Overridable Async Function Play(channels As Dictionary(Of Byte, IEnumerable(Of Chord))) As Task
            If Device IsNot Nothing Then
                Dim tasks As New List(Of Task)
                For Each channel In channels
                    tasks.Add(Task.Run(Async Function()
                                           Await Play(channel.Key, channel.Value)
                                       End Function))
                Next
                Await Task.WhenAll(tasks)
            End If
        End Function
    
        Protected Overridable Sub SendMessage(message As MidiShortMessage)
            NativeMethods.SendPlaybackDeviceMessage(handle, message)
        End Sub
    
        Public Overridable Sub SetVoice(channel As Byte, voice As InstrumentVoice)
            SendMessage(New MidiShortMessage(MidiShortMessage.MessageCommandMask.ProgramChange, channel, voice, 0))
        End Sub
    
    #Region "IDisposable Support"
        Private disposedValue As Boolean
        Protected Overridable Sub Dispose(disposing As Boolean)
            If Not disposedValue Then
                If disposing Then
                    messageHandler = Nothing
                    Close()
                End If
            End If
            disposedValue = True
        End Sub
        Public Sub Dispose() Implements IDisposable.Dispose
            Dispose(True)
        End Sub
    #End Region
    End Class
    
    Public Class MidiMessageReceivedEventArgs
        Inherits EventArgs
    
        Public Handle As Integer, Message As Integer, Instance As Integer, Param1 As Integer, Param2 As Integer
    
        Public Sub New(hnd As Integer, msg As Integer, inst As Integer, p1 As Integer, p2 As Integer)
            Handle = hnd
            Message = msg
            Instance = inst
            Param1 = p1
            Param2 = p2
        End Sub
    End Class

    This implementation requires that all channels have the same number of notes.  While this isn't necessarily conducive to real-world music composition, it can be accommodated with rest notes and careful adjustment of note durations.  A more sophisticated player could implement a timeline by which to play each channel's notes.

    To facilitate creating MIDI music from code, an additional helper class can be created to parse a string representation of music notes into MIDI data.  There are any number of ways one might represent the notes, but this example uses upper case letters (CDEFGAB) followed by an optional # for sharps, followed by (whqest) for whole, half, quarter, eighth, sixteenth or thirty-seconds notes.  Parts can be separated by whitespace or punctuation (,; etc).  Chords can be combined using parentheses, for example (CEG) for C-major.

    Public Class Tune
        Inherits ObjectModel.Collection(Of Chord)
    
        Public Overloads Sub Add(notes As IEnumerable(Of NoteMidiCode), duration As NoteDuration)
            Add(New Chord() With {.Notes = notes, .Duration = duration})
        End Sub
    
        Public Shared Function Parse(tuneData As String) As Tune
            Dim result As New Tune
            Dim sb As New Text.StringBuilder
            Dim i As Integer
            Dim octave As Integer = 4
            While i < tuneData.Length
                sb.Clear()
                If IsWhiteSpaceOrPunctuation(tuneData(i)) Then i += 1 : Continue While
                If Char.ToUpper(tuneData(i)) = "O"c Then
                    i += 1
                    octave = Val(tuneData(i))
                    i += 1
                End If
                If IsWhiteSpaceOrPunctuation(tuneData(i)) Then i += 1 : Continue While
    
                Dim tones As IEnumerable(Of NoteMidiCode)
                If tuneData(i) = "("c Then
                    i += 1
                    Dim noteList As New List(Of NoteMidiCode)
                    Dim lastNoteChar As Char = Chr(0)
                    While Not tuneData(i) = ")"c
                        If IsWhiteSpaceOrPunctuation(tuneData(i)) Then i += 1
                        Dim noteChar = Char.ToUpper(tuneData(i))
                        sb.Append(noteChar)
                        If Not lastNoteChar = Chr(0) AndAlso noteChar > lastNoteChar Then
                            sb.Append(octave.ToString)
                        Else
                            sb.Append((octave + 1).ToString)
                        End If
                        lastNoteChar = noteChar
                        i += 1
                        If tuneData(i) = "#"c Then
                            sb.Append("s")
                            i += 1
                        End If
                        noteList.Add(CType([Enum].Parse(GetType(NoteMidiCode), sb.ToString), NoteMidiCode))
                        sb.Clear()
    
                    End While
                    tones = noteList.ToArray
                    i += 1
                Else
                    sb.Append(Char.ToUpper(tuneData(i)))
                    i += 1
                    If Not sb.Chars(sb.Length - 1) = "R"c Then
                        sb.Append(octave.ToString)
                        If tuneData(i) = "#"c Then
                            sb.Append("s")
                            i += 1
                        End If
                    Else
                        sb.Append("est")
                    End If
                    tones = {CType([Enum].Parse(GetType(NoteMidiCode), sb.ToString), NoteMidiCode)}
                End If
    
                If IsWhiteSpaceOrPunctuation(tuneData(i)) Then i += 1
    
                Dim dur As NoteDuration
                Select Case tuneData(i)
                    Case "t"c
                        dur = NoteDuration.ThirtysecondthNode
                    Case "s"c
                        dur = NoteDuration.SixteenthNote
                    Case "e"c
                        dur = NoteDuration.EigthNote
                    Case "q"c
                        dur = NoteDuration.QuarterNote
                    Case "h"c
                        dur = NoteDuration.HalfNote
                    Case "w"c
                        dur = NoteDuration.WholeNote
                End Select
                i += 1
    
                result.Add(tones, dur)
            End While
            Return result
        End Function
    
        Private Shared Function IsWhiteSpaceOrPunctuation(c As Char) As Boolean
            If Char.IsWhiteSpace(c) Then Return True
            If Char.GetUnicodeCategory(c) = Globalization.UnicodeCategory.OtherPunctuation Then Return True
            Return False
        End Function
    End Class

    And there we have it.  The ability to play MIDI music from code with direct messages or interpreted strings of note data.

    Here's an example of what a simple form using this code might look like:

    Imports System.ComponentModel
    
    Public Class Form1
        Dim midi As New SimpleMidiEngine.MidiPlayer
    
        Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
            Button1.Enabled = False
            Dim score As New Dictionary(Of Byte, IEnumerable(Of SimpleMidiEngine.Chord))
            score(1) = SimpleMidiEngine.Tune.Parse(RichTextBox1.Text)
            score(2) = score(1)
            Await midi.Play(score)
            Button1.Enabled = True
        End Sub
    
        Private Sub Form1_Closing(sender As Object, e As CancelEventArgs) Handles Me.Closing
            midi.Dispose()
        End Sub
    
        Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
            For Each entry In [Enum].GetValues(GetType(SimpleMidiEngine.InstrumentVoice))
                ComboBox1.Items.Add(entry)
            Next
            ComboBox1.SelectedIndex = 0
            ComboBox2.SelectedIndex = 0
            midi.Open()
            Label2.Text = $"Voices: {midi.Device.VoiceCount}, Notes: {midi.Device.NoteCount}"
        End Sub
    
        Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
            midi.SetVoice(CInt(ComboBox2.SelectedItem), CType(ComboBox1.SelectedItem, SimpleMidiEngine.InstrumentVoice))
        End Sub
    End Class

    And a little ditty to play might look like:

    O3
    CqCqCqEeFw
    FqFqFqEeCw
    CqCqCqEqFqEqFh
    FqFqFqEeCw
    O5
    GqFqGqFqGqFqEh
    EqEqEqDh
    O3
    EqDqEqDqEqDqDqDh
    EqEqEqDqCw

    Hopefully this example code provides a simple, solid foundation on which to build applications using simple MIDI music generation, or more complex MIDI device implementations.


    Reed Kimble - "When you do things right, people won't be sure you've done anything at all"



    Wednesday, March 15, 2017 10:01 PM
    Moderator

All replies

  •  Nice Reed.  8)  Not to be picky but,  i have one complaint that i noticed right away.  All those handle types in the midixxx functions should be IntPtr types,  not Integer types.

    If you say it can`t be done then i`ll try it

    Wednesday, March 15, 2017 11:02 PM
  •  Nice Reed.  8)  Not to be picky but,  i have one complaint that i noticed right away.  All those handle types in the midixxx functions should be IntPtr types,  not Integer types.

    If you say it can`t be done then i`ll try it

    Normally I would agree but the documentation specifically says not to cast the handle into a normal pointer - there is a special structure you are supposed to use but a normal integer seems to work happily.


    Reed Kimble - "When you do things right, people won't be sure you've done anything at all"

    Thursday, March 16, 2017 2:17 PM
    Moderator
  • Normally I would agree but the documentation specifically says not to cast the handle into a normal pointer - there is a special structure you are supposed to use but a normal integer seems to work happily.


    Reed Kimble - "When you do things right, people won't be sure you've done anything at all"

     I am not arguing with you because i know you are usually right but,  i don`t recall seeing that recommendation in the msdn documents for the midixxx functions,  do you have a link that explains this that i can read through?  I looked a bit last night but,  could not find anything about it.  The PInvoke Signature Toolkit shows them as IntPtr types and that is how i have always used them,  as IntPtrs.   8)

    If you say it can`t be done then i`ll try it

    Friday, March 17, 2017 9:46 AM
  • Its on the midioutmessage documentation for the handle parameter.  It may not even apply to managed pointers, and I didn't test it to see if it would fail as indicated.

    Reed Kimble - "When you do things right, people won't be sure you've done anything at all"

    Friday, March 17, 2017 3:03 PM
    Moderator
  • You were right, IntPtr was the way to go, I've modified the code and it works fine.

    I've also updated the Tune class to fix a bug in the chord parsing (forgot to account for octave shifts in chords).


    Reed Kimble - "When you do things right, people won't be sure you've done anything at all"

    Saturday, March 18, 2017 12:19 AM
    Moderator
  •  I was starting to think you where right after finding that the PInvoke Signature Toolkit does have a listing for the HMIDIOUT / LPHMIDIOUT structure which appears as below.

        <StructLayout(LayoutKind.Sequential)>
        Public Structure HMIDIOUT__
            Public unused As Integer
        End Structure

     However,  there does not seem to be an HMIDIOUT or LPHMIDIOUT structure to be found anywhere in the msdn documents,  or at least not that i could find.  Nor is there any real explanation anywhere in the msdn documents as to exactly what type(s) it is or contains.

     I was looking at the midiOutOpen function that has the lphmo parameter which indicates it is a pointer to an HMIDIOUT handle that is used by the rest of the midixxx functions,  along with another parameter,  the uDeviceID parameter which is an unsigned Integer type that indicates it is an identifier of the midi device.

     My best guess was that the midiOutOpen function creates the "HMIDIOUT" structure in the memory and then just passes back a handle to that structure in the lphmo parameter.  Does that sound right to you?


    If you say it can`t be done then i`ll try it

    • Edited by IronRazerz Saturday, March 18, 2017 10:14 AM
    Saturday, March 18, 2017 10:12 AM
  • The topic of working with MIDI files from VB.Net comes up from time to time 

    @Reed,

    Once in the 10 years? Despite of that I find it helpful.

    :-)


    Success
    Cor


    Saturday, March 18, 2017 2:01 PM
  • Well, the midiOutOpen function expects a pointer to a HMIDIOUT handle so I have to assume there was a way to create one in the first place, and the midiOutOpen function just points it the proper handle instance.  I guess that the managed pointer we use in .Net automagically represents whatever pointer type is assigned to it.

    Reed Kimble - "When you do things right, people won't be sure you've done anything at all"

    Saturday, March 18, 2017 2:08 PM
    Moderator
  •             If Notes?.Count = 1 AndAlso Notes.First = NoteMidiCode.Rest Then Return True
    


    Reed Kimble - "When you do things right, people won't be sure you've done anything at all"



    This is interesting... What is the '?' appended at the end of the Ienumerable of t

    Hire Me For This Job!
    Don't forget to vote for Helpful Posts and Mark Answers!
    *This post does not reflect the opinion of Microsoft, or its employees.

    Wednesday, August 1, 2018 10:50 AM
    Moderator
  • That's the Elvis operator (null-conditional operator).  If Notes is Nothing, attempting to read the .Count property won't throw an exception.  Instead the comparison will just be false.

    Reed Kimble - "When you do things right, people won't be sure you've done anything at all"

    Wednesday, August 1, 2018 11:11 AM
    Moderator
  • That's the Elvis operator (null-conditional operator).  If Notes is Nothing, attempting to read the .Count property won't throw an exception.  Instead the comparison will just be false.

    Reed Kimble - "When you do things right, people won't be sure you've done anything at all"

    Omg and all this time I've been doing it the long way...

    Hire Me For This Job!
    Don't forget to vote for Helpful Posts and Mark Answers!
    *This post does not reflect the opinion of Microsoft, or its employees.

    Wednesday, August 1, 2018 11:28 AM
    Moderator
  • That's the Elvis operator (null-conditional operator).  If Notes is Nothing, attempting to read the .Count property won't throw an exception.  Instead the comparison will just be false.


    Reed Kimble - "When you do things right, people won't be sure you've done anything at all"

    Omg and all this time I've been doing it the long way...

    Hire Me For This Job!
    Don't forget to vote for Helpful Posts and Mark Answers!
    *This post does not reflect the opinion of Microsoft, or its employees.


    Don't feel bad, this has only been around for a couple of years.  Came when the C#7 stuff was released.

    Reed Kimble - "When you do things right, people won't be sure you've done anything at all"

    Wednesday, August 1, 2018 11:31 AM
    Moderator

  • Don't feel bad, this has only been around for a couple of years.  Came when the C#7 stuff was released.
    Whats makes me happy to know is that regardless what I have read or done there are still features I have yet to discover in .net, that keeps a guy going...

    Hire Me For This Job!
    Don't forget to vote for Helpful Posts and Mark Answers!
    *This post does not reflect the opinion of Microsoft, or its employees.

    Wednesday, August 1, 2018 11:39 AM
    Moderator