NOW LOADING

PowerShell cmdlets for MIDI


About the PowerShell cmdlets which enable scripting Windows MIDI Services

PowerShell support for MIDI is currently experimental and in-development. Expect minor changes in the cmdlets in the future.

These cmdlets require a minimum of PowerShell 7.6. We recommend using the latest official version. Earlier 7.x releases run on an earlier .NET, which cannot load the module, so Import-Module refuses rather than failing later.

PowerShell itself is not installed by the SDK Runtime and Tools installer. The .NET desktop runtime the cmdlets need is, currently .NET 10.

The version of PowerShell which usually ships with Windows is currently the older Windows PowerShell. These cmdlets support the new cross-platform version of PowerShell. Please see the link above for how to install the latest 7.x version of PowerShell

What these are for

PowerShell is the main command-line scripting language in Windows, and the current version also runs on Linux and macOS. System administrators use it to set up PCs, developers use it to automate deployment and testing, and technical Windows users use it for everything else. The PowerShell documentation is the place to learn it.

The MIDI cmdlets exist so you can script MIDI. Some things people do with them:

  • Set up a room full of MIDI gear, mixers and lighting in a large venue, all at once
  • Load patches and starting state into synthesizers, drum machines and sequencers before a show
  • Take input from a MIDI controller and use it to launch an app, send keystrokes, or drive something else on the PC

They’re reasonably fast, but they aren’t the way to build a MIDI sequencer or anything else where timing has to be tight.

The MIDI Console can do much of what these cmdlets can. The difference is that the console opens a new connection every time you send a message and closes it again afterwards. That’s wasteful if your script does many things with the same connection. For a single message, midi endpoint send-message 0x25971234 is simple and fast.

Startup cmdlet

Windows MIDI Services starts on demand. It doesn’t run, enumerate endpoints or connect to anything until something asks it to, so that MIDI doesn’t slow down Windows startup for people who don’t use it. You can change that in the Services snap-in, or from the command line.

Start-Midi

This confirms Windows MIDI Services is available, starting the service if it is not already running. Every other cmdlet in the module performs the same check, so calling this first is optional. It is a useful first line in a script because it fails immediately, with a clear message, on a PC where Windows MIDI Services is not installed.

#Requires -Version 7.6
import-module WindowsMidiServices

Start-Midi

There is no matching shutdown cmdlet. Sessions and connections are released when you stop them, or when the PowerShell process ends.

Enumeration cmdlets

One of the first things a MIDI tool or application typically does is list out all the available connections so that the software or its user can decide which endpoint(s) to create a connection to.

Get-MidiEndpointDeviceInfo

With no parameters, returns all UMP MIDI Endpoints. With an endpoint device id, returns just that one. Does not require an active MIDI Session.

#Requires -Version 7.6
import-module WindowsMidiServices

# List all available endpoints. Enumeration functions do not require an active session.
Write-Host "Available MIDI Endpoints" -ForegroundColor Cyan
(Get-MidiEndpointDeviceInfo) | Sort-Object -Property Name | Format-Table -AutoSize
# I'm using the default loopback that is created when you set up MIDI through the MIDI Settings app
$endpointDeviceId = "\\?\swd#midisrv#midiu_loop_a_default#{e7cce071-3c03-423f-88d3-f1045d02552b}"

# show some info about the device we're interested in
Write-Host "Endpoint we intend to connect to" -ForegroundColor Cyan
(Get-MidiEndpointDeviceInfo $endpointDeviceId)  | Format-List

Get-MidiEndpointGroup

Returns the MIDI Groups an endpoint uses, taken from its declared function blocks when it has them, and from its group terminal blocks when it does not. Inactive function blocks are left out unless -IncludeInactive is supplied.

Get-MidiEndpointGroup -EndpointDeviceId $endpointDeviceId | Format-Table -AutoSize

Get-MidiLegacyPort

Returns the MIDI 1.0 ports which the older Windows MIDI APIs (WinMM and WinRT MIDI 1.0) see. These are created by Windows MIDI Services alongside the UMP endpoints they belong to, so this is how you map an endpoint to the port numbers an older application will show.

The ports can be listed in full, or narrowed by direction, by owning endpoint, by name, by container, or fetched individually by port device id.

# every MIDI 1.0 input port on the PC
Get-MidiLegacyPort -Flow MidiMessageSource | Format-Table -AutoSize

# only the ports belonging to one endpoint
Get-MidiLegacyPort -EndpointDeviceId $endpointDeviceId | Format-Table -AutoSize

# by the name an older application displays
Get-MidiLegacyPort -Name "MIDISPORT 2x2 In A"

Get-MidiSession

Enumerates all the active MIDI Sessions in the service, from every application, not just this one.

# list all the active sessions
Write-Host "All active MIDI sessions" -ForegroundColor Cyan
(Get-MidiSession) | Sort-Object -Property Name | Format-Table -AutoSize

Session cmdlets

To send and receive messages with Windows MIDI Services, you must have an active session. The session is tracked in the service so that a MIDI users has visibility into the processes using MIDI on their PC. Most processes only need one MIDI Session, but they may open more than one if they need to group and manage connection usage by project, page, or similar.

Once you have an active session, you can open one or more connections to endpoints. Each active connection allocate resources on the client and in the service, so you only want to open connections you need, and ideally, only one connection per endpoint device id.

Start-MidiSession

Given the session name as a parameter, creates and activates a new MIDI Session. The returned object is required for calls which use a session, such as sending and receiving messages.

# create a new session so we can send and receive messages
$session = Start-MidiSession "Powershell Demo Session"

Stop-MidiSession

Ends the MIDI session

Stop-MidiSession $session

Open-MidiEndpointConnection

Given the session object and an endpoint device id, opens a connection to a MIDI UMP endpoint. The returned connection object is required for cmdlets which send and receive messages.

# open a connection to the endpoint
$connection = Open-MidiEndpointConnection $session $endpointDeviceId

Close-MidiEndpointConnection

Given session and connection objects, closes an open MIDI Endpoint Connection within the specified session.

Close-MidiEndpointConnection $session $connection

Send-MidiMessage

Given a connection object and an array of valid UMP message words (formatted as 32 bit integer values as complete UMPs), sends the single message. Do not include more than one valid UMP in the array of words.

A Timestamp value of 0 means to send the message immediately. Otherwise, a valid 64 bit integer derived from the MIDI Clock should be provided for scheduling a message in the near future.

# each sub-array is a complete MIDI UMP
$messages = (0x40905252, 0x02001111), (0x40805252, 0x02000000), 0x25971234

foreach ($message in $messages)
{
    Write-Host "Sending MIDI message" -ForegroundColor Cyan

    Send-MidiMessage $connection $message -Timestamp 0
}

Receiving messages

To receive MIDI messages, use PowerShell’s Register-ObjectEvent and background job support to handle the incoming messages. The monitor-messages sample includes the code for this.

The event handler args themselves are simplified from what direct WinRT clients receive. In this case, the data is supplied as a Timestamp field and an array of MIDI words as the Words field

$eventHandlerAction = {
    #Write-Host "Message Received"
    #Write-Host $EventArgs.Timestamp
    Get-MidiMessageInfo $EventArgs.Words
}

$job = Register-ObjectEvent -SourceIdentifier "OnMessageReceivedHandler" -InputObject $connection -EventName "MessageReceived" -Action $eventHandlerAction

# just spin until a key is pressed
do
{
    Receive-Job -Job $job
} until ([System.Console]::KeyAvailable)

# we don't do anything with the key here, but you could
$keyPressed = [System.Console]::ReadKey($true)

Write-Host
Write-Host "Key pressed. Shutting down ... "

Unregister-Event -SourceIdentifier "OnMessageReceivedHandler"
Stop-Job $job
Remove-Job $job

To record what arrives into a MIDI file instead, use Receive-MidiMessage, described under MIDI file cmdlets.

System Exclusive cmdlets

MIDI 1.0 bytestream System Exclusive data, of the kind held in a .syx file, is carried over UMP as SysEx7 messages. These two cmdlets do the conversion and the flow control for you.

Both take either an open connection or -EndpointDeviceId. With an endpoint device id, the cmdlet opens its own session and connection, and closes them when it’s done, so a one-off transfer doesn’t need Start-MidiSession first.

Send-MidiSystemExclusive

Sends a .syx file, or a byte array. Progress is reported through PowerShell’s normal progress bar, and Ctrl+C cancels the transfer.

-MessagesPerTransfer and -DelayBetweenTransfersMilliseconds pace the data. Some devices, particularly older ones, need a slower pace to keep up.

Send-MidiSystemExclusive -Connection $connection -Path .\patches.syx -GroupIndex 0

# a one-off transfer, with no session to set up
Send-MidiSystemExclusive -EndpointDeviceId $endpointDeviceId -Path .\patches.syx

# every .syx file in a folder, one after another, over one connection
Get-ChildItem .\patches\*.syx | Send-MidiSystemExclusive -EndpointDeviceId $endpointDeviceId

Receive-MidiSystemExclusive

Captures System Exclusive data arriving on a group. Without -Path it writes one object per block of received bytes; with -Path it writes a .syx file instead, as the data arrives, so the file is complete even if the capture is interrupted.

Receiving is open ended, so it runs until -MessageCount messages have arrived, -TimeoutSeconds elapses, or Ctrl+C is pressed. Supply at least one of those unless you intend to stop it by hand.

# capture one complete message, or give up after 30 seconds
Receive-MidiSystemExclusive -Connection $connection -Path .\dump.syx -MessageCount 1 -TimeoutSeconds 30

# the same, with no session to set up
Receive-MidiSystemExclusive -EndpointDeviceId $endpointDeviceId -Path .\dump.syx -MessageCount 1 -TimeoutSeconds 30

MIDI file cmdlets

These play a Standard MIDI File (.mid) to an endpoint, and record what an endpoint receives into one.

Start-MidiFilePlayback

Plays a Standard MIDI File. With no endpoint, it plays to the built-in General MIDI synthesizer, so Start-MidiFilePlayback .\song.mid is all it takes. The cmdlet waits until the file ends and shows its progress. Press Ctrl+C to stop early, and the notes that are sounding are turned off.

-StartAtSeconds starts part way into the file. Each channel’s bank, program and controllers are sent as they stand at that point, so the music starts on the right sounds.

-NoWait returns right away with an object you use to control the playback. Keep it in a variable. If nothing holds on to it, playback stops.

# play to the built-in synthesizer, and wait for the end
Start-MidiFilePlayback .\song.mid

# play to another endpoint, starting 30 seconds in
Start-MidiFilePlayback .\song.mid -EndpointDeviceId $endpointDeviceId -StartAtSeconds 30

# start it, then carry on with the script
$playback = Start-MidiFilePlayback .\song.mid -NoWait

Suspend-MidiFilePlayback, Resume-MidiFilePlayback and Stop-MidiFilePlayback

These control a playback started with -NoWait. Suspending turns off the notes that are sounding and holds the position. Resuming carries on from the same place, with each channel’s sound put back first. Stopping ends it and releases the connection. The State and Position properties of the playback object tell you where it’s at.

Suspend-MidiFilePlayback $playback
$playback.Position.Microseconds / 1000000    # seconds into the file
Resume-MidiFilePlayback $playback
Stop-MidiFilePlayback $playback

Receive-MidiMessage

Records the messages an endpoint receives into a Standard MIDI File, so you can play the recording back or load it into a sequencer. Like the System Exclusive cmdlets, it takes an open connection or -EndpointDeviceId.

It runs until -MessageCount messages have arrived, -TimeoutSeconds passes, or you press Ctrl+C, and the file is written in each case. Nothing goes into the file until the recording ends, because a MIDI file stores the length of each track in front of it. If nothing arrived, no file is written. An existing file is only replaced when you add -Force.

A few things to know about the file:

  • The first message is at the very start of the file. Times are kept to about half a millisecond: 960 ticks per quarter note, at a fixed 120 beats per minute.
  • Each group gets its own track, because a MIDI file has no other place to record which group a message came from.
  • MIDI 2.0 messages are converted to MIDI 1.0 where MIDI 1.0 has the same message. The ones it can’t hold, such as per-note controllers, are left out, and the cmdlet tells you how many.
  • MIDI clock, active sensing and the other real-time messages are left out unless you add -IncludeRealTimeMessages. Clock alone arrives dozens of times a second.
# record for one minute
Receive-MidiMessage -EndpointDeviceId $endpointDeviceId -Path .\take1.mid -TimeoutSeconds 60

# record until Ctrl+C
Receive-MidiMessage -Connection $connection -Path .\take2.mid

MIDI Capability Inquiry cmdlets

MIDI Capability Inquiry (MIDI-CI) is how a device tells you about itself. Many devices that support it publish a list of their channels and a list of their programs (patches) through Property Exchange, which is part of MIDI-CI. These cmdlets ask for those lists. The built-in General MIDI synthesizer answers both, so it’s a good one to try them on.

Each cmdlet takes an endpoint device id, or an open connection with -Connection, and asks every device on the endpoint that answers. Finding those devices always takes the whole response time, two seconds unless you change it with -ResponseTimeoutMilliseconds, because there’s no way to know how many will answer. So expect each call to take at least that long. -GroupIndex picks the group to ask on. It’s the first group unless you say otherwise.

A device that doesn’t support MIDI-CI, or doesn’t publish the list, gives you an error rather than an empty list, so a script can tell the difference.

Get-MidiChannelList

Returns each channel’s name, and the program it’s set to right now. Channel numbers run from 1 to 256, not 1 to 16, because MIDI-CI counts across all 16 groups.

Get-MidiChannelList (Get-MidiSynthEndpointDeviceId) | Format-Table -AutoSize

Get-MidiProgramList

Returns every program the device offers, with the bank select and program change that choose it. The bank and program numbers start at 0, the way they’re sent in MIDI messages. A long list arrives a page at a time, and the cmdlet asks for every page.

When a device has more than one collection, CollectionTitle says which one each program belongs to. The synthesizer has two, one for instruments and one for drum kits. -ResourceId asks for just one collection, by the id the device gave it.

# find the organs
Get-MidiProgramList (Get-MidiSynthEndpointDeviceId) | Where-Object Title -like '*Organ*'

Loopback endpoint cmdlets

Loopback endpoints are a pair of endpoints wired together, so what an application sends to one, another application receives from the other. Basic loopbacks are the single-endpoint MIDI 1.0 flavor, where an endpoint simply receives what it sends.

An endpoint created by these cmdlets is transient: it disappears when the service restarts. Supply -SaveToConfiguration to also write it to the Windows MIDI Services configuration file so it comes back.

New-MidiLoopback and New-MidiBasicLoopback

# the base name gets " (A)" and " (B)" appended, matching the other MIDI tools
New-MidiLoopback -BaseName "My Loopback" -SaveToConfiguration

# or name each side yourself
New-MidiLoopback -NameA "Sequencer Out" -NameB "Synth In"

New-MidiBasicLoopback -Name "My Basic Loopback"

Get-MidiLoopback and Get-MidiBasicLoopback

Lists the loopbacks the service currently has, including any created by other applications.

Get-MidiLoopback | Format-Table -AutoSize

Set-MidiLoopbackMute and Set-MidiBasicLoopbackMute

Mute is a property rather than an action, so it is set rather than toggled. Mute is not an approved PowerShell verb.

Set-MidiLoopbackMute -AssociationId $loopback.AssociationId -Muted $true

Remove-MidiLoopback and Remove-MidiBasicLoopback

Removes the endpoint from the running service. An entry saved in the configuration file is not affected, so a saved loopback returns when the service restarts.

Get-MidiLoopback | Where-Object { $_.EndpointA.Name -like 'My Loopback*' } | Remove-MidiLoopback

Network MIDI 2.0 cmdlets

Get-MidiNetworkAdvertisedHost

Lists the remote hosts this PC can currently see advertised on the network.

Get-MidiNetworkAdvertisedHost | Format-Table -AutoSize

Connect-MidiNetworkHost

Connects to a remote host, either one which was discovered, or one at a fixed address and port. A discovered host is matched on its advertisement, so the connection survives it moving to a new address; a direct address cannot do that, and is not retried automatically if it stops answering.

# connect to something which was discovered
Get-MidiNetworkAdvertisedHost |
    Where-Object { $_.DeviceName -eq 'BomeBox' } |
    Connect-MidiNetworkHost -SaveToConfiguration

# connect to a fixed address
Connect-MidiNetworkHost -HostNameOrAddress 192.168.1.243 -Port 33327 -SaveToConfiguration

Disconnect-MidiNetworkHost

Disconnects by client identifier, by the device id of the host it was matched to, or by address and port.

Disconnect-MidiNetworkHost -ClientId $response.ClientId
Disconnect-MidiNetworkHost -HostNameOrAddress 192.168.1.243 -Port 33327

Get-MidiNetworkConfiguredHost and Get-MidiNetworkConfiguredClient

Hosts are what this PC advertises for remote devices to connect to. Clients are the connections this PC makes out to remote hosts. A client is reported even when it is not connected, so EntryState is what says whether it is usable.

Get-MidiNetworkConfiguredHost | Format-Table -AutoSize
Get-MidiNetworkConfiguredClient | Format-Table -AutoSize

RTP-MIDI cmdlets

RTP-MIDI is the network MIDI 1.0 protocol that macOS, iOS and many MIDI interfaces use. These cmdlets work the same way as the Network MIDI 2.0 ones.

Get-MidiRtpAdvertisedHost

Lists the RTP-MIDI devices this PC can currently see advertised on the network. A host on this PC is listed too, with IsThisPc set.

Get-MidiRtpAdvertisedHost | Format-Table -AutoSize

Connect-MidiRtpHost

Connects to a device that was discovered, to a device by the name it advertises, or to a fixed address. A device found by name is looked up again each time it connects, so the connection survives it moving to a new address. Leave out -Port to use 5004, where RTP-MIDI devices listen unless they’re set up otherwise.

The cmdlet returns once the service has the entry. Connecting happens in the background, and Get-MidiRtpConfiguredClient shows how it’s going. Like the Network MIDI 2.0 cmdlet, the connection lasts until the service restarts unless you add -SaveToConfiguration.

Two name parameters are easy to mix up. -LocalEndpointName is what the other device shows for this PC. -EndpointName is what Windows calls the endpoint this connection creates.

# connect to something which was discovered
Get-MidiRtpAdvertisedHost |
    Where-Object { $_.ServiceInstanceName -eq 'Studio Mac' } |
    Connect-MidiRtpHost -SaveToConfiguration

# connect to a fixed address
Connect-MidiRtpHost -HostNameOrAddress 192.168.1.167 -Port 5006

Disconnect-MidiRtpHost

Disconnects by client identifier, by the name the device advertises, or by address and port. The connection ends and the entry is removed from the running service. An entry saved in the configuration file comes back when the service restarts.

Disconnect-MidiRtpHost -ClientId $response.ClientId
Disconnect-MidiRtpHost -HostNameOrAddress 192.168.1.167 -Port 5006

Get-MidiRtpConfiguredHost and Get-MidiRtpConfiguredClient

Hosts are what this PC offers for other devices to connect to. Clients are the connections this PC makes to other devices. A client is listed even when it isn’t connected, so EntryState is what says whether it’s usable: Pending while it looks for the device, Active when it’s connected, Retrying after a try that didn’t work, and Unavailable when it has stopped trying. To try an Unavailable client again, run Connect-MidiRtpHost with the same target and the same -ClientId.

Get-MidiRtpConfiguredHost | Format-Table -AutoSize
Get-MidiRtpConfiguredClient | Format-Table -AutoSize

General MIDI synthesizer cmdlets

Windows MIDI Services includes a General MIDI synthesizer, which shows up as the General MIDI Synth endpoint. These cmdlets show and change its settings, and list the sounds it has.

Get-MidiSynth and Set-MidiSynth

Get-MidiSynth shows the synthesizer’s settings. Set-MidiSynth changes the ones you name and leaves the rest alone. A change lasts until the service restarts, unless you add -Persist.

Turning the synthesizer off removes its endpoint and releases the audio device. That matters if you use audio software which needs the audio device to itself, through WASAPI exclusive mode or ASIO.

Get-MidiSynth
Set-MidiSynth -VolumeDecibels -6 -Persist
Set-MidiSynth -Disabled

Get-MidiSynthEndpointDeviceId

Returns the synthesizer’s endpoint device id, for any cmdlet that takes one. It returns nothing while the synthesizer is turned off, because then the endpoint doesn’t exist.

Get-MidiSynthSoundSet and Get-MidiSynthInstrument

Get-MidiSynthSoundSet describes the sound set the synthesizer plays: its name and version, how many instruments it has, and its drum kits. Get-MidiSynthInstrument lists every melodic instrument, with the bank select and program change that choose it. Drum kits are chosen with a program change on a drum channel, so they’re only in the sound set.

Get-MidiSynthSoundSet
(Get-MidiSynthSoundSet).DrumKits
Get-MidiSynthInstrument | Where-Object Name -like '*Guitar*'

Set-MidiSynthDrumChannel

Makes a channel play drum kits, or makes it play instruments again. Channel 10 is the drum channel unless something changes it. Channel indexes start at 0, so channel 10 is index 9.

This isn’t saved, and a MIDI file can change it too: a reset in a file puts channel 10 back as the only drum channel.

# make channel 11 a second drum channel
Set-MidiSynthDrumChannel -ChannelIndex 10 -IsDrumChannel $true

MIDI utility cmdlets

Alongside enumeration, sessions and messaging, there are a few utility cmdlets.

Get-MidiMessageInfo

Given a valid MIDI UMP message, this decodes it and returns readable information about it.

# each sub-array is a complete MIDI UMP
$messages = (0x40905252, 0x02001111), (0x40805252, 0x02000000), 0x25971234

foreach ($message in $messages)
{
    # this gets / displays information about the MIDI message we're sending
    Get-MidiMessageInfo $message | Format-List 
}

PowerShell samples in the repo on GitHub.

Didn't find what you were looking for?

Windows MIDI Services is an open source project with all source available on GitHub. We have a great community on Discord as well. Between GitHub and Discord, you should find the information you are looking for.