Windows MIDI Glass is the Windows MIDI Services app for building your own touch control surface. You put knobs, faders, pads, buttons, and keys on a page, choose what MIDI each one sends, and play it with a finger, a pen, or a mouse. Each design is called a layout, and each layout is one .midilayout file.
This article is written for AI agents and online AI chats that build layouts for people. It’s also for anyone who wants to write or check a layout file by hand. If you’re asking an AI to build a layout for you, give it the link to this article and ask it to read the whole page before it starts.
Windows MIDI Glass is a preview app. The layout file described here is version 1. Later versions can add settings, and Windows MIDI Glass keeps reading version 1 files when they do.
For AI agents: Read this article from start to finish before you write anything. Follow Building a layout for someone as your process, and use The layout file as your reference. You usually can’t see Windows MIDI Glass yourself, so the notes marked For agents point out mistakes that load without an error and give the customer the wrong layout.
The rules that matter most:
provenance block that says an AI made the layout, and ask the customer whose name and which license go on it. See Who made it.On this page:
Ask these before you write anything. Put them in one message, in plain words, and offer a sensible answer for each so the customer can just say yes.
If the customer pasted a prompt from Ask an AI assistant… in Windows MIDI Glass, it already lists the MIDI devices and the themes on their PC. Use those names exactly. You still need to ask which device the layout is for.
| Ask | Why it matters |
|---|---|
| What will the layout control? Get each device’s name exactly as Windows shows it, for example in MIDI Settings or in the Send to list in Windows MIDI Glass’s New layout dialog. | The file finds devices by name. A name that’s only close doesn’t match. |
| Is it hardware, a plug-in, or an app like a DAW? An app on the same PC is reached through a loopback, such as Default App Loopback (A), with the app listening on Default App Loopback (B). | You need a device that Windows MIDI Glass can send to. |
| Do they have the device’s manual or its MIDI implementation chart? Ask for it, or for a link to it. | The same name means different numbers on different gear. Don’t guess. |
| Does the device use MIDI 1.0 or MIDI 2.0? | It changes how you write values that are codes, like pad colors. Most hardware is MIDI 1.0. |
| What screen will it run on, and which way up? | It decides the page size. |
| Will they use touch, a pen, or a mouse? | Fingers need bigger controls. |
| Where will they play it: a dark stage, a lit studio, a desk by a window? | It decides the theme. |
| What should be on the page, how should it be grouped, and what matters most? | It decides the sections, the sizes, and the order. |
| Do they need more than one page? | Each page needs page tabs to get to the others. |
| For each control: what it changes, on which channel, and whether it springs back when let go, like a pitch wheel. | |
| Should anything follow the device, like a fader the DAW moves or a light that shows notes arriving? | Those controls need to listen as well as send. |
| Do the colors already mean something to them, like drums in red? | Keep the meaning. |
| Does anyone who will use it have trouble telling colors apart, or use a screen reader? | Pick a theme that doesn’t depend on color alone, and give every control a clear name. |
| Whose name should go on the layout, and may others share or change it? Offer to leave the name off. | It goes in the provenance block, so anyone the customer shares the layout with can see who made it and what they may do with it. See Who made it. |
For agents: If the customer can’t give you a device name yet, use a short placeholder and tell them that Windows MIDI Glass will show the device as missing until they pick it. Don’t make up a name that looks real.
Take every number from the manual or a published MIDI implementation chart, and tell the customer where each one came from so they can check it.
| The manual says | Message kind in the file | Notes |
|---|---|---|
| Control change (CC) 74 | controlChange, number 74 |
|
| NRPN with MSB 7 and LSB 54, sent as CC 99 = 7 and CC 98 = 54 | assignedController, number 950 |
The number is MSB × 128 + LSB. Windows MIDI Glass sends one MIDI 2.0 message, and Windows turns it into CC 99, CC 98, CC 6 and CC 38 for a MIDI 1.0 device. |
| RPN 0, pitch bend sensitivity, sent as CC 101 = 0 and CC 100 = 0 | registeredController, number 0 |
The same rule, with CC 101 and CC 100. |
| Note 36 | note, number 36 |
Go by the note number. Manuals don’t agree on note names: note 60, middle C, is C3 in some and C4 in others. Windows MIDI Glass calls it C3, the same as the rest of Windows MIDI Services. |
| Program 1 to 128 | programChange, number 0 to 127 |
The file uses the number sent on the wire, one less than most manuals print. |
| Bank select, then a program | Two controlChange rows (0 and 32), then a programChange row, all on the same trigger |
Rows on the same trigger go out in the order they’re listed. |
| Pitch bend | pitchBend |
No number. The middle is 0.5. |
| Channel pressure, or aftertouch | channelPressure |
No number. |
| A MIDI 2.0 per-note controller, such as the volume of one note | perNoteController, number is the note and controller is the controller |
Use assignablePerNoteController for a controller the device defines itself. See Per-note controllers and note attributes. |
| A MIDI 2.0 note attribute, such as an articulation | attributeType and attributeData on a note row |
See Per-note controllers and note attributes. |
| A system exclusive message | systemExclusive, with the bytes as hexadecimal text |
F0 and F7 are optional. |
| Any other single message, in Universal MIDI Packet form | rawUmp, with one to four 32-bit words |
See Raw messages. |
| A DAW set up for a Mackie Control surface | mackieControl, with a function name |
See Mackie Control functions. |
MIDI 2.0 profiles. Windows MIDI Glass sends messages. It doesn’t use MIDI-CI, so it can’t turn a profile on or ask a device what it supports. Check that the device or plug-in already uses the profile, then send the messages the profile describes. Where the profile needs something Windows MIDI Glass can’t do, such as a button that changes the articulation a keyboard plays, say so and offer what it can do instead.
Pick the page size closest to the shape of the customer’s screen. The page scales to fit, so its shape matters more than its exact size.
| Screen | Page size in pixels |
|---|---|
| Tablet | 1280 × 800 |
| Full HD monitor or TV | 1920 × 1080 |
| Quad HD monitor | 2560 × 1440 |
| Surface Pro or another 3 : 2 screen | 2736 × 1824 |
| Classic 4 : 3 screen | 1024 × 768 |
| Phone-shaped, or a monitor turned on its side | 1080 × 1920 |
| Horizontal toolbar that floats over other apps | 800 × 120 |
| Vertical toolbar | 120 × 800 |
| Floating palette | 360 × 360 |
For a toolbar or a palette, also read How to make a floating toolbar or palette in Windows MIDI Glass.
Use a built-in theme. Windows MIDI Glass comes with twenty-five, and they’ve been checked for contrast and readability. Write its exact name in theme, capital letters and all, and leave themeColors out. A name Windows MIDI Glass doesn’t know silently gives the customer Studio Dark.
The built-in themes are: Studio Dark, Airy System, Bigwig, Blueprint, Bone, Cathode, Chicago, Daylight, Five-iSH, Good Form, Groovy, Groovy Dark, Hard Sector, High contrast, Insert Coin, Jove, Night Drive, Off-world Colonies, Soft Sector, Supersaw, Terminal Amber, Terminal Green, Tonal Dark, Tonal Light, and Visor.
| If the customer wants | Start with |
|---|---|
| A busy page on a dark stage | Studio Dark |
| A bright room, or a desk by a window | Daylight, or Bone for something warm |
| Big colorful pads | Insert Coin, or Tonal Dark |
| A DAW or plug-in look: grays and one accent color | Bigwig |
| A hardware synth panel | Jove, Supersaw, or Five-iSH |
| The easiest possible reading | High contrast |
| Something retro | Cathode, Terminal Amber, Terminal Green, or Chicago |
The shipping themes describes each one, and says what it costs, such as colors that are hard to tell apart. Read the row for the theme you pick.
If the customer wants a look of their own, design a theme file as well, following Designing a theme for someone else, and name that theme in the layout.
x and y are the control’s top left corner, measured from the top left of the page, with y growing downward. width and height are its size.panel in the file, the way a hardware panel has sections. Leave about 32 pixels inside the top of the Group for its name, and 16 to 24 pixels on the other sides.label, even one whose name you hide. It’s what a screen reader says.keyboardOrder from 1 in the order a person reads the page: section by section, left to right, top to bottom. It’s the order the Tab key and a screen reader follow.| Control (name in the app) | kind in the file |
Default size | What it is |
|---|---|---|---|
| Button | button |
96 × 40 | On while you hold it. |
| Toggle | toggle |
96 × 40 | Press for on, press again for off. |
| Pad | pad |
56 × 56 | A square button for drums and clips. |
| Page tab | pageTab |
120 × 36 | Goes to another page. |
| Switch | switch |
180 × 44 | Two to sixteen named positions, each sending its own message. Or a picker for the key of the pad grids, or for a fretboard’s tuning. |
| Knob | knob |
56 × 56 | |
| Turntable | turntable |
160 × 160 | A platter you push. Springs back to the middle. |
| Fader | fader |
40 × 180 | |
| Wheel | wheel |
44 × 160 | A pitch or modulation wheel. |
| Ribbon | ribbon |
280 × 48 | A strip you slide a finger along. |
| XY pad | xyPad |
240 × 240 | Two values at once. |
| Joystick | joystick |
180 × 180 | Two values at once, and springs back to the middle. |
| Beat clock | beatClock |
100 × 120 | Sends MIDI clock. |
| LFO | lfo |
160 × 80 | Sweeps a value by itself, in time with the layout’s tempo. |
| Steps | steps |
320 × 96 | A step sequencer. |
| Mono keyboard | pianoKeyboard |
420 × 120 | Piano keys that play one note at a time. |
| Note pads | notePads |
428 × 164 | A grid of pads that each play a note, for chords. |
| Hex pads | hexPads |
456 × 160 | The same, with hexagons. |
| Fretboard | fretboard |
760 × 300 | A guitar neck: strings and frets, one note per string, with bends, slides, hammer-ons and strumming. |
| Meter | meter |
24 × 180 | Shows a value the device sends. |
| Lamp | lamp |
32 × 32 | Lights up when the device sends something. |
| Readout | readout |
120 × 40 | Shows a value the device sends, where a pad grid is, or a fretboard’s capo or tuning. |
| Stopwatch | timeDisplay |
180 × 64 | Counts up from when the layout starts. |
| Text | label |
120 × 24 | Words on the page. |
| Image | image |
120 × 120 | A picture or a video. |
| Group | panel |
280 × 220 | A frame around controls that belong together. |
| Line | line |
240 × 8 | A printed line that divides the page. |
0x."controlChange", "fitToScreen".id, and every control’s id on every page. Windows MIDI Glass writes GUIDs, but any unique text works, such as cutoff or page-mix.provenance block with a new GUID for id, trainedAlgorithmicMedia for digitalSourceType, and the names and license the customer gave you. See Who made it.If you can run commands on the customer’s PC, write the file with PowerShell 7, which finds the Documents folder even when it has been moved into OneDrive:
$folder = Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'MIDI Layouts'
New-Item -ItemType Directory -Path $folder -Force | Out-Null
$path = Join-Path $folder 'Synth basics.midilayout'
if (Test-Path -LiteralPath $path) { throw 'A layout with that name is already there. Pick another name.' }
[IO.File]::WriteAllText($path, $json, [Text.UTF8Encoding]::new($false))
If you’re an online chat, give the customer the file as a download named after the layout, ending in .midilayout. If you can only show text, put the whole file in one code block and tell them how to save it: paste it into Notepad, select File > Save as, set Save as type to All files, type a name that ends in .midilayout, and leave Encoding at UTF-8.
For agents: Don’t change a layout file that’s open in Windows MIDI Glass. The app saves its own copy as the customer works, and it can write over your change. Write a new file instead.
Go through Mistakes that are easy to miss for every file. If you can run PowerShell 7, save this as Test-MidiGlassLayout.ps1 and run pwsh -File Test-MidiGlassLayout.ps1 -Path "<layout file>". It reads the file as strictly as Windows MIDI Glass does, and lists the mistakes that load without an error.
param([Parameter(Mandatory)][string]$Path)
$text = [IO.File]::ReadAllText($Path)
$null = [Text.Json.JsonDocument]::Parse($text) # throws on a comment, a trailing comma, or a hexadecimal number
$layout = $text | ConvertFrom-Json
$problems = [Collections.Generic.List[string]]::new()
$themes = 'Studio Dark', 'Airy System', 'Bigwig', 'Blueprint', 'Bone', 'Cathode', 'Chicago', 'Daylight', 'Five-iSH', 'Good Form', 'Groovy', 'Groovy Dark', 'Hard Sector', 'High contrast', 'Insert Coin', 'Jove', 'Night Drive', 'Off-world Colonies', 'Soft Sector', 'Supersaw', 'Terminal Amber', 'Terminal Green', 'Tonal Dark', 'Tonal Light', 'Visor'
$controlKinds = 'knob', 'fader', 'pad', 'button', 'toggle', 'xyPad', 'meter', 'lamp', 'readout', 'label', 'image', 'pageTab', 'panel', 'joystick', 'ribbon', 'pianoKeyboard', 'beatClock', 'timeDisplay', 'lfo', 'turntable', 'wheel', 'switch', 'steps', 'line', 'notePads', 'hexPads', 'fretboard'
$messageKinds = 'note', 'controlChange', 'programChange', 'pitchBend', 'channelPressure', 'perNoteController', 'assignablePerNoteController', 'registeredController', 'assignedController', 'systemExclusive', 'rawUmp', 'sequence', 'goToPage', 'mackieControl', 'padGrid'
$numbered = 'note', 'controlChange', 'programChange', 'perNoteController', 'assignablePerNoteController', 'registeredController', 'assignedController'
$scales = 'major', 'minor', 'harmonicMinor', 'melodicMinor', 'dorian', 'phrygian', 'lydian', 'mixolydian', 'locrian', 'majorPentatonic', 'minorPentatonic', 'blues', 'wholeTone'
$picks = 'nothing', 'rootNote', 'scale', 'inKeyOnly', 'rowOffset', 'octave', 'tuning'
$padFunctions = 'octaveUp', 'octaveDown', 'rowUp', 'rowDown', 'shiftLeft', 'shiftRight', 'semitoneUp', 'semitoneDown', 'inKeyOnlyOn', 'inKeyOnlyOff', 'inKeyOnlyToggle', 'nextRoot', 'previousRoot', 'nextScale', 'previousScale', 'nextRowOffset', 'reset', 'capoUp', 'capoDown', 'upTheNeck', 'downTheNeck', 'muteStrings'
$gridIds = @($layout.pages | ForEach-Object { $_.controls } | Where-Object { $_.kind -cin 'notePads', 'hexPads', 'fretboard' } | ForEach-Object { $_.id })
$keys = 'none', 'c', 'cSharp', 'd', 'dSharp', 'e', 'f', 'fSharp', 'g', 'gSharp', 'a', 'aSharp', 'b'
$tunings = 'standard', 'dropD', 'halfStepDown', 'wholeStepDown', 'dadgad', 'openG', 'openD', 'openE', 'sevenString', 'bass', 'fiveStringBass', 'ukulele', 'baritoneUkulele', 'mandolin', 'banjo', 'custom'
$fretChoices = @{ strumSide = 'right', 'left', 'none'; playMode = 'tap', 'strum'; sustain = 'untilReplayed', 'whileHeld'; outOfKeyFrets = 'play', 'dim', 'mute'; slide = 'stepped', 'smooth', 'off'; output = 'channelPerString', 'perNoteBend', 'mpe'; tuning = $tunings; key = $keys }
$pageWidth = $layout.pageWidth ?? 1280
$pageHeight = $layout.pageHeight ?? 800
$devices = @($layout.devices | ForEach-Object { $_.name })
$pageIds = @($layout.pages | ForEach-Object { $_.id })
$bandIds = @($layout.pages | Where-Object { $_.sharedBand } | ForEach-Object { $_.id })
$sequences = @($layout.sequences | ForEach-Object { $_.name })
$controlIds = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
if ($null -eq $layout.themeColors -and $layout.theme -cnotin $themes) { $problems.Add("'$($layout.theme)' isn't a built-in theme. Unless that theme file is installed, the customer sees Studio Dark.") }
if ($pageIds.Count -eq 0) { $problems.Add('The layout has no pages.') }
foreach ($name in ($devices | Group-Object -CaseSensitive | Where-Object Count -gt 1).Name) { $problems.Add("Two devices are named '$name'.") }
foreach ($id in ($pageIds | Group-Object -CaseSensitive | Where-Object Count -gt 1).Name) { $problems.Add("Two pages have the id '$id'.") }
foreach ($name in ($sequences | Group-Object -CaseSensitive | Where-Object Count -gt 1).Name) { $problems.Add("Two sequences are named '$name'.") }
if ($layout.tempo.kind -ceq 'followIncomingClock' -and $layout.tempo.device -cnotin $devices) { $problems.Add("The tempo follows the clock from '$($layout.tempo.device)', which isn't in devices.") }
foreach ($page in $layout.pages) {
foreach ($c in $page.controls) {
$where = "'$($c.label)' ($($c.id)) on page '$($page.name)'"
if (-not $c.id -or -not $controlIds.Add($c.id)) { $problems.Add("$where needs an id of its own.") }
if ($c.kind -cnotin $controlKinds) { $problems.Add("$where has the kind '$($c.kind)', which Windows MIDI Glass doesn't know. It shows the control as an outline that does nothing, and won't save changes to the layout.") }
if (-not $c.label) { $problems.Add("$where has no label, so a screen reader can't name it.") }
$x = $c.x ?? 0; $y = $c.y ?? 0; $w = $c.width ?? 56; $h = $c.height ?? 56
if ($w -le 0 -or $h -le 0) { $problems.Add("$where has no size.") }
elseif ($x -lt 0 -or $y -lt 0 -or ($x + $w) -gt $pageWidth -or ($y + $h) -gt $pageHeight) { $problems.Add("$where hangs off the page, so it won't show when the layout runs.") }
$slot = $c.hueSlot ?? 0
if ($slot -lt -1 -or $slot -gt 6 -or ($slot -eq -1 -and -not $c.literalColor)) { $problems.Add("$where needs a hueSlot from 0 to 6, or -1 with a literalColor.") }
if ($null -ne $c.pads.notes -and $c.pads.notes -cnotin 'chromatic', 'inKey') { $problems.Add("$where has the pad notes '$($c.pads.notes)', which isn't a choice, so the pads play every note.") }
elseif ($c.pads.notes -ceq 'inKey' -and $c.pads.key -ceq 'none') { $problems.Add("$where plays only the notes in the key, but has no key, so it plays every note.") }
if ($null -ne $c.switch.picks -and $c.switch.picks -cnotin $picks) { $problems.Add("$where picks '$($c.switch.picks)', which isn't a choice, so it's a switch that sends.") }
elseif ($null -ne $c.switch.picks -and $c.switch.picks -cne 'nothing' -and $c.messages.Count -gt 0) { $problems.Add("$where is a picker, so its messages are never sent.") }
foreach ($s in $c.switch.scales) { if ($s -cnotin $scales) { $problems.Add("$where offers the scale '$s', which isn't a choice, so the picker leaves it out.") } }
if ($c.switch.picks -ceq 'scale' -and $null -ne $c.switch.scales -and @($c.switch.scales | Where-Object { $_ -cin $scales } | Select-Object -Unique).Count -lt 2) { $problems.Add("$where offers fewer than two scales, so it offers the usual seven instead.") }
foreach ($t in @($c.switch.targets) + @($c.readout.targets)) { if ($null -ne $t -and $t -cnotin $gridIds) { $problems.Add("$where targets '$t', which isn't the id of a pad grid or a fretboard, so nothing follows it.") } }
if ($null -ne $c.pads.arrangement -and $c.pads.arrangement -cnotin 'flow', 'rows', 'columns') { $problems.Add("$where has the arrangement '$($c.pads.arrangement)', which isn't a choice, so its pads flow.") }
elseif ($c.pads.arrangement -cin 'rows', 'columns' -and (($c.pads.rows ?? 8) -notin 1..32 -or ($c.pads.padsPerRow ?? 8) -notin 1..64 -or ($c.pads.rows ?? 8) * ($c.pads.padsPerRow ?? 8) -gt 1024)) { $problems.Add("$where needs rows from 1 to 32 and padsPerRow from 1 to 64, and no more than 1,024 pads in all.") }
if ($null -ne $c.readout.source -and $c.readout.source -cnotin 'value', 'padKey', 'octave', 'rowOffset', 'padSummary', 'capo', 'tuning') { $problems.Add("$where shows '$($c.readout.source)', which isn't a choice, so it shows its own value.") }
if ($c.kind -ceq 'fretboard') {
$f = $c.fretboard
foreach ($name in $fretChoices.Keys) { $v = $f.$name; if ($null -ne $v -and $v -cnotin $fretChoices[$name]) { $problems.Add("$where has the $name '$v', which isn't a choice, so the fretboard uses its default.") } }
$strings = @($f.strings | Where-Object { $null -ne $_ })
if ($null -ne $f.strings -and ($strings.Count -lt 1 -or $strings.Count -gt 12)) { $problems.Add("$where needs one to twelve strings. With none it gets standard tuning, and with more it keeps the first twelve.") }
foreach ($s in $strings) { if ($s.note -isnot [ValueType] -or $s.note -notin 0..127) { $problems.Add("$where has a string whose note isn't a number from 0 to 127, so that string plays E1 (40).") } }
$frets = $f.frets ?? 22
if ($frets -notin 1..36) { $problems.Add("$where needs frets from 1 to 36, so it gets 22.") }
elseif (($f.firstFret ?? 0) -notin 0..($frets - 1) -or ($f.lastFret ?? 12) -notin ([Math]::Max(($f.firstFret ?? 0), 1))..$frets -or ($f.capo ?? 0) -notin 0..($frets - 1)) { $problems.Add("$where needs firstFret below frets, lastFret above firstFret and no more than frets, and capo below frets.") }
if (($f.bendRange ?? 2) -notin 1..12) { $problems.Add("$where needs a bendRange from 1 to 12 semitones, so it bends 2.") }
if ($f.playMode -ceq 'strum' -and $f.strumSide -ceq 'none') { $problems.Add("$where strums, but has no strum area, so touching a fret plays it.") }
$noteRows = @($c.messages | Where-Object { $_.kind -ceq 'note' })
if ($noteRows.Count -eq 0) { $problems.Add("$where has no note row, so its strings send nothing.") }
else {
$channel = $noteRows[0].channel ?? 0
$count = if ($strings.Count -gt 0) { [Math]::Min($strings.Count, 12) } else { 6 }
if ($f.output -ceq 'mpe' -and $channel + 1 + $count -gt 16) { $problems.Add("$where uses MPE with the manager on channel $($channel + 1), and its $count strings run past channel 16, so some strings share a channel.") }
elseif (($f.output ?? 'channelPerString') -ceq 'channelPerString' -and $channel + $count -gt 16) { $problems.Add("$where gives each of its $count strings a channel from channel $($channel + 1), which runs past 16, so some strings share a channel.") }
}
}
foreach ($m in $c.messages) {
$kind = $m.kind ?? 'controlChange'
if ($kind -cnotin $messageKinds) { $problems.Add("$where sends '$kind', which isn't a message kind."); continue }
if (($m.trigger ?? 'changes') -cnotin @('turnsOn', 'turnsOff', 'changes', 'touched', 'released')) { $problems.Add("$where has the trigger '$($m.trigger)'.") }
if ($kind -ceq 'goToPage') {
if ($m.targetPage -cnotin $pageIds) { $problems.Add("$where goes to a page that isn't in the layout.") }
elseif ($m.targetPage -cin $bandIds) { $problems.Add("$where goes to a band. A band shows on every page, so it isn't somewhere to go.") }
continue
}
if ($kind -ceq 'sequence') { if ($m.sequence -cnotin $sequences) { $problems.Add("$where runs a sequence that isn't in the layout.") }; continue }
if ($kind -ceq 'padGrid') {
if ($m.function -cnotin $padFunctions) { $problems.Add("$where has the pad grid function '$($m.function)', which isn't a choice, so the row does nothing.") }
foreach ($t in @($m.targets)) { if ($null -ne $t -and $t -cnotin $gridIds) { $problems.Add("$where targets '$t', which isn't the id of a pad grid or a fretboard.") } }
if (($m.trigger ?? 'changes') -cnotin 'turnsOn', 'turnsOff', 'touched', 'released') { $problems.Add("$where has a pad grid row on '$($m.trigger ?? 'changes')'. Use turnsOn, or it never runs.") }
continue
}
if (-not $m.device) { $problems.Add("$where has a row with no device, so the row sends nothing.") }
elseif ($m.device -cnotin $devices) { $problems.Add("$where sends to '$($m.device)', which isn't in devices.") }
if (($m.group ?? 0) -notin -1..15 -or ($m.channel ?? 0) -notin 0..15) { $problems.Add("$where has a group or a channel outside 0 to 15. The file counts from 0.") }
$limit = if ($kind -cin @('registeredController', 'assignedController')) { 16383 } else { 127 }
if (($kind -cin $numbered) -and ($m.number ?? 0) -notin 0..$limit) { $problems.Add("$where has the number $($m.number), outside 0 to $limit.") }
if ($kind -cin @('perNoteController', 'assignablePerNoteController')) {
$controller = $m.controller ?? 0
if ($controller -isnot [ValueType] -or $controller -lt 0 -or $controller -gt 255) { $problems.Add("$where has the per-note controller $controller, outside 0 to 255.") }
}
if ($null -ne $m.attributeType -or $null -ne $m.attributeData) {
$type = $m.attributeType ?? 0; $data = $m.attributeData ?? 0
if ($kind -cne 'note') { $problems.Add("$where has a note attribute on a row that isn't a note, so it's never sent.") }
elseif ($type -isnot [ValueType] -or $data -isnot [ValueType] -or $type -lt 0 -or $type -gt 255 -or $data -lt 0 -or $data -gt 65535) { $problems.Add("$where needs attributeType 0 to 255 and attributeData 0 to 65535, as decimal numbers.") }
elseif ($type -eq 0 -and $data -ne 0) { $problems.Add("$where has attribute data with attribute type 0, so the data is never sent.") }
}
if ($kind -ceq 'rawUmp' -and $c.kind -cne 'beatClock') {
$words = @($m.words)
if ($words.Count -notin 1..4 -or @($words | Where-Object { $_ -is [string] -or $_ -lt 0 -or $_ -gt 4294967295 }).Count -gt 0) { $problems.Add("$where needs one to four words, written as decimal numbers.") }
}
if ($kind -ceq 'systemExclusive' -and $m.systemExclusive -notmatch '^([0-9A-Fa-f]{2})+$') { $problems.Add("$where needs its bytes as hexadecimal text with no spaces, like F07E7F0601F7.") }
}
if ($c.feedback.enabled -and $c.feedback.device -and $c.feedback.device -cnotin $devices) { $problems.Add("$where listens to '$($c.feedback.device)', which isn't in devices.") }
}
}
$remoteActions = 'goToPage', 'nextPage', 'previousPage', 'pageFromMessage', 'pressControl', 'setControlValue', 'enableControl', 'disableControl', 'toggleEnabled', 'showControl', 'hideControl', 'toggleShown', 'showOnlyThis', 'padGrid', 'setKeyRoot', 'setScale', 'runSequence', 'panic'
$allControls = @($layout.pages | ForEach-Object { $_.controls })
$groupIds = @($allControls | ForEach-Object { $_.controlGroup } | Where-Object { $_ })
foreach ($page in $layout.pages) { if ($page.openSequence -and $page.openSequence -cnotin $sequences) { $problems.Add("Page '$($page.name)' runs '$($page.openSequence)' when it opens, which isn't in sequences.") } }
$ruleNumber = 0
foreach ($r in $layout.remoteControl.rules) {
$ruleNumber++
$where = "Remote control rule $ruleNumber"
$w = $r.when
if ($null -eq $w) { $problems.Add("$where has no 'when', so it listens for program change 0 from any device.") }
elseif ($w.device -and $w.device -cnotin $devices) { $problems.Add("$where listens to '$($w.device)', which isn't in devices, so it never runs.") }
if ($w.kind -and $w.kind -cnotin 'programChange', 'controlChange', 'note') { $problems.Add("$where listens for '$($w.kind)'. A rule can only listen for programChange, controlChange or note.") }
if (($w.group ?? 0) -notin 0..15 -or ($w.channel ?? 0) -notin 0..15 -or ($w.number ?? 0) -notin 0..127) { $problems.Add("$where needs a group and a channel from 0 to 15, and a number from 0 to 127. The file counts from 0.") }
if ($r.do -cnotin $remoteActions) { $problems.Add("$where does '$($r.do)', which isn't an action. Windows MIDI Glass keeps it but never runs it, and won't save changes to the layout."); continue }
if ($r.do -ceq 'goToPage') { if ($r.page -cnotin $pageIds) { $problems.Add("$where goes to a page that isn't in the layout.") } elseif ($r.page -cin $bandIds) { $problems.Add("$where goes to a band, which shows on every page.") } }
if ($r.do -cin 'pressControl', 'setControlValue', 'enableControl', 'disableControl', 'toggleEnabled', 'showControl', 'hideControl', 'toggleShown', 'showOnlyThis') {
if ($r.controlGroup) { if ($r.controlGroup -cnotin $groupIds) { $problems.Add("$where names the group '$($r.controlGroup)', which no control is in.") } }
elseif ($r.control -cnotin $controlIds) { $problems.Add("$where names the control '$($r.control)', which isn't in the layout.") }
elseif ($r.do -ceq 'showOnlyThis' -and -not ($allControls | Where-Object { $_.id -ceq $r.control } | Select-Object -First 1).controlGroup) { $problems.Add("$where shows only '$($r.control)', which isn't in a group, so nothing else is hidden.") }
}
if ($r.do -ceq 'padGrid' -and $r.function -cnotin $padFunctions) { $problems.Add("$where has the pad grid function '$($r.function)', which isn't a choice.") }
foreach ($t in @($r.targets)) { if ($null -ne $t -and $t -cnotin $gridIds) { $problems.Add("$where targets '$t', which isn't the id of a pad grid or a fretboard.") } }
if ($r.do -ceq 'setKeyRoot' -and -not $r.fromMessage -and $null -ne $r.key -and ($r.key -cnotin $keys -or $r.key -ceq 'none')) { $problems.Add("$where sets the root '$($r.key)', which isn't a choice, so it sets C.") }
if ($r.do -ceq 'setScale' -and -not $r.fromMessage -and $null -ne $r.scale -and $r.scale -cnotin $scales) { $problems.Add("$where sets the scale '$($r.scale)', which isn't a choice, so it sets major.") }
if ($r.do -ceq 'runSequence' -and $r.sequence -cnotin $sequences) { $problems.Add("$where runs a sequence that isn't in the layout.") }
if ($null -ne $r.value -and ($r.value -lt 0 -or $r.value -gt 1)) { $problems.Add("$where has a value outside 0 to 1.") }
}
$tell = $layout.remoteControl.tellPage
if ($tell -and $tell.enabled -ne $false) {
if ($tell.device -cnotin $devices) { $problems.Add("tellPage sends to '$($tell.device)', which isn't in devices, so the controller is never told the page.") }
if ($tell.kind -and $tell.kind -cnotin 'programChange', 'controlChange') { $problems.Add("tellPage sends '$($tell.kind)'. It can send programChange or controlChange.") }
if (($tell.firstValue ?? 0) + @($layout.pages | Where-Object { -not $_.sharedBand }).Count - 1 -gt 127) { $problems.Add("tellPage's firstValue plus the number of pages goes past 127, so the last pages aren't told.") }
}
foreach ($c in $allControls) {
if (-not $c.startsHidden -and -not $c.startsDisabled) { continue }
$reached = @($layout.remoteControl.rules | Where-Object { $_.control -ceq $c.id -or ($c.controlGroup -and $_.controlGroup -ceq $c.controlGroup) })
if ($reached.Count -eq 0) { $problems.Add("'$($c.label)' ($($c.id)) starts turned off or hidden, and no remote control rule reaches it, so it stays that way.") }
}
$sourceTypes = 'digitalCreation', 'digitalCapture', 'trainedAlgorithmicMedia', 'compositeWithTrainedAlgorithmicMedia', 'compositeSynthetic', 'composite', 'algorithmicMedia'
$oversight = 'fully_autonomous', 'prompt_guided', 'human_validated'
$p = $layout.provenance
if ($null -eq $p) { $problems.Add('There is no provenance block, so nothing says an AI made this layout.') }
else {
foreach ($name in $p.PSObject.Properties.Name) { if ($name -cnotin 'id', 'version', 'author', 'organization', 'url', 'license', 'created', 'tool', 'digitalSourceType', 'aiDisclosure', 'basedOn') { $problems.Add("Windows MIDI Glass doesn't know the key 'provenance.$name', so it does nothing.") } }
if ($p.aiDisclosure) { foreach ($name in $p.aiDisclosure.PSObject.Properties.Name) { if ($name -cnotin 'humanOversightLevel', 'modelName') { $problems.Add("Windows MIDI Glass doesn't know the key 'provenance.aiDisclosure.$name', so it does nothing.") } } }
if ($p.basedOn) { foreach ($name in $p.basedOn.PSObject.Properties.Name) { if ($name -cnotin 'name', 'author', 'id', 'version', 'builtIn') { $problems.Add("Windows MIDI Glass doesn't know the key 'provenance.basedOn.$name', so it does nothing.") } } }
if ($p.id -cnotmatch '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$') { $problems.Add('provenance.id needs a lowercase GUID with no braces.') }
if ($p.digitalSourceType -cnotin $sourceTypes) { $problems.Add("provenance.digitalSourceType '$($p.digitalSourceType)' isn't a type Windows MIDI Glass knows. A layout you made is trainedAlgorithmicMedia.") }
if ($p.aiDisclosure -and $p.aiDisclosure.humanOversightLevel -cnotin $oversight) { $problems.Add("provenance.aiDisclosure.humanOversightLevel '$($p.aiDisclosure.humanOversightLevel)' isn't a level Windows MIDI Glass knows.") }
if ($p.url -and $p.url -notmatch '^https://[^/@\s]+(/\S*)?$') { $problems.Add('provenance.url has to start with https:// and have no user name, or Windows MIDI Glass shows it as plain text.') }
if ($p.created -and $p.created -isnot [datetime] -and $p.created -notmatch '^\d{4}-\d{2}-\d{2}') { $problems.Add('provenance.created needs a date and time like 2026-10-08T21:14:00Z.') }
}
if ($problems.Count -gt 0) { $problems; exit 1 }
'No problems found.'
A file that passes can still send the wrong controller. Only the manual and the customer can tell you that.
Show the customer what they’ll get before you hand it over, and ask them to confirm the controls, the names, the colors, and what each one sends. Draw the mockup from the file you wrote, using its own positions and sizes, so what they approve is what they get.
If Windows MIDI Glass is installed and you can run commands, it can draw the first page for you:
midiglass --thumbnail "<layout file>" "<picture.png>" 1600
The last number is the width of the picture in pixels. The picture shows the page in the layout’s theme, with each control as an outline in its color. It doesn’t show names or values, so it’s a check on positions and colors rather than a finished picture. It’s drawn without opening a window or touching any device. The command returns 0 when it works, 2 when an argument is missing, 3 when the file can’t be read as a layout, and 4 when the picture can’t be written.
The real thing is better still: midiglass --run "<layout file>" opens the layout in Windows MIDI Glass, so the customer can look at every page and try it. Opening it sends nothing until a control is touched, unless a control is set to send its value when the layout starts, or a beat clock, LFO, or Steps control is set to start running.
If you’re an online chat, draw the mockup yourself as HTML, SVG, or a picture. This page draws every page of a layout file in Studio Dark’s colors. Paste the whole layout file where it says, and change the six colors to your theme’s if you know them:
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Layout mockup</title>
<style>
body { margin: 24px; background: #2B2B2B; color: #E8EAED; font: 14px "Segoe UI", sans-serif; }
.page { position: relative; overflow: hidden; margin: 8px 0 32px; background: radial-gradient(ellipse 65% 100% at 50% -10%, #1D1E21, #121316 58%, #07080A); }
.control { position: absolute; box-sizing: border-box; border: 1px solid; border-radius: 7px; background: rgba(23, 24, 27, 0.86); }
.round { border-radius: 50%; }
.panel { background: rgba(23, 24, 27, 0.45); }
.bare { border-color: transparent !important; background: none; }
.name { position: absolute; left: -40px; right: -40px; text-align: center; color: #D7DAE0; font: 12px "Segoe UI Variable Text", "Segoe UI", sans-serif; }
.caption { left: 10px; right: auto; text-align: left; }
</style>
<p>A mockup drawn from the layout file. It isn't a Windows MIDI Glass screenshot.</p>
<div id="pages"></div>
<script type="application/json" id="layout">
PASTE THE WHOLE LAYOUT FILE HERE
</script>
<script>
const layout = JSON.parse(document.getElementById('layout').textContent);
const slots = ['#4FC3F7', '#81C784', '#FFC247', '#FF7043', '#BA68C8', '#4DD0E1']; // Studio Dark, slots 0 to 5
const round = ['knob', 'turntable', 'joystick', 'lamp'];
const width = layout.pageWidth ?? 1280, height = layout.pageHeight ?? 800;
const scale = Math.min(1, 1200 / width);
for (const page of layout.pages) {
const heading = document.createElement('h2');
heading.textContent = page.name || page.id;
const host = document.createElement('div');
host.className = 'page';
Object.assign(host.style, { width: width * scale + 'px', height: height * scale + 'px' });
for (const c of page.controls ?? []) {
const slot = c.hueSlot ?? 0;
const box = document.createElement('div');
box.className = 'control' + (round.includes(c.kind) ? ' round' : '') + (c.kind === 'panel' ? ' panel' : '') + (c.style === 'bare' ? ' bare' : '');
Object.assign(box.style, {
left: c.x * scale + 'px', top: c.y * scale + 'px',
width: (c.width ?? 56) * scale + 'px', height: (c.height ?? 56) * scale + 'px',
borderColor: slot === -1 ? c.literalColor : (slots[slot] ?? slots[0])
});
const placed = c.labelPlaced ?? (c.kind === 'panel' ? 'inside' : 'below');
if (c.label && placed !== 'none') {
const name = document.createElement('div');
name.className = 'name' + (c.kind === 'panel' ? ' caption' : '');
name.textContent = c.label;
name.style.top = placed === 'above' ? '-18px' : placed.startsWith('inside') ? '6px' : 'calc(100% + 4px)';
box.append(name);
}
host.append(box);
}
document.getElementById('pages').append(heading, host);
}
</script>
</html>
Beside the mockup, list what each control sends in plain words, such as “Cutoff: CC 74 on channel 1” or “Filter: NRPN 7:54 on channel 1”. Customers check that list more carefully than the picture.
Label the mockup as a mockup. A theme draws much more than outlines: knob arcs, fader caps, lights, and textures. If the customer wants to see the real thing, they can open the file in Windows MIDI Glass and look before they play it.
Tell the customer how to get the file into Windows MIDI Glass, using Where layout files go, and how to bring one in. Then tell them:
Windows MIDI Glass keeps its layouts in Documents › MIDI Layouts, one .midilayout file per layout. The library shows every layout file in that folder. A picture or a video a layout uses sits next to it, in the same folder.
C:\Users\<name>\Documents. On many PCs it has been moved into OneDrive. In File Explorer, select Documents and look for MIDI Layouts there..midilayoutpack file. In the library, select … (More options) > Import pack…. Make one in Windows MIDI Glass with Export pack… on a card’s … menu. A pack lists every file it holds with its size and SHA-256 hash, so Windows MIDI Glass can tell if anything was changed, and it can be signed. Don’t make a pack yourself: give the customer the layout file and its pictures, and ask them to put them all in the layouts folder. Windows MIDI Glass still imports the .zip packages that older versions made..midilayout.json. The app still reads those, and renames them to .midilayout when it starts.System exclusive. A layout opened from outside the layouts folder asks before it sends system exclusive, because the wrong system exclusive can harm a device. If your layout sends system exclusive, give the customer the file outside the layouts folder, such as in Downloads, so they see that question the first time. They can copy it into the folder once they trust it.
Tell the customer about these before they find out on their own.
targets, so two grids can follow two different sets of pickers on one layout.output set to mpe, Windows MIDI Glass sends on the manager channel and the member channels above it, but doesn’t turn the MPE profile on with MIDI-CI. Set the instrument up for MPE on those channels yourself.output set to perNoteBend, every string is on one channel, so the same note on two strings is one note, and bending one bends both. Use channelPerString or mpe for unison bends.publishesVirtualDevice is kept in the file but doesn’t do anything yet, so leave it out.devices. A layout doesn’t appear to other apps as a MIDI device, so a foot controller or another app has to be listed in devices even when the layout sends it nothing. A rule listens for a program change, a control change, or a note, and nothing else. It can’t open another layout or a set list, and can’t tap a tempo.This layout has a section with two knobs, a volume fader, a pitch wheel, a hold toggle, and a pad that plays middle C. It sends to the built-in Default App Loopback (A), so you can try it on any PC with Windows MIDI Services: open MIDI Monitor on Default App Loopback (B) and watch what it sends. For a real device, change the name in match.
{
"fileVersion": 1,
"name": "Synth basics",
"description": "Filter, volume, pitch bend, hold, and a pad for one synth.",
"provenance": {
"id": "2f1c7d0e-9b8a-4c6d-8e5f-3a2b1c0d9e8f",
"version": "1.0",
"author": "Pat Example",
"license": "CC-BY-4.0",
"created": "2026-10-08T21:14:00Z",
"tool": "Example Assistant 2.1",
"digitalSourceType": "trainedAlgorithmicMedia",
"aiDisclosure": {
"humanOversightLevel": "prompt_guided"
}
},
"pageWidth": 1280,
"pageHeight": 800,
"canvasWidth": 1280,
"canvasHeight": 800,
"theme": "Studio Dark",
"scaleMode": "fitToScreen",
"devices": [
{
"name": "Synth",
"match": { "transportSuppliedEndpointName": "Default App Loopback (A)" },
"matchMode": "endpointName"
}
],
"pages": [
{
"id": "main",
"name": "Main",
"controls": [
{
"id": "filter-section", "kind": "panel", "label": "Filter",
"x": 48, "y": 48, "width": 368, "height": 264,
"hueSlot": 0, "labelPlaced": "inside", "keyboardOrder": 1
},
{
"id": "cutoff", "kind": "knob", "label": "Cutoff",
"x": 80, "y": 112, "width": 120, "height": 120,
"hueSlot": 0, "aspectLocked": true, "keyboardOrder": 2,
"messages": [
{ "trigger": "changes", "kind": "controlChange", "device": "Synth", "group": 0, "channel": 0, "number": 74 }
]
},
{
"id": "resonance", "kind": "knob", "label": "Resonance",
"x": 264, "y": 112, "width": 120, "height": 120,
"hueSlot": 0, "aspectLocked": true, "keyboardOrder": 3,
"messages": [
{ "trigger": "changes", "kind": "controlChange", "device": "Synth", "group": 0, "channel": 0, "number": 71 }
]
},
{
"id": "volume", "kind": "fader", "label": "Volume",
"x": 464, "y": 48, "width": 56, "height": 264,
"hueSlot": 1, "defaultValue": 0.8, "keyboardOrder": 4,
"messages": [
{ "trigger": "changes", "kind": "controlChange", "device": "Synth", "group": 0, "channel": 0, "number": 7 }
]
},
{
"id": "bend", "kind": "wheel", "label": "Bend",
"x": 568, "y": 48, "width": 56, "height": 264,
"hueSlot": 4, "defaultValue": 0.5, "returnsToDefault": true, "keyboardOrder": 5,
"messages": [
{ "trigger": "changes", "kind": "pitchBend", "device": "Synth", "group": 0, "channel": 0 }
]
},
{
"id": "hold", "kind": "toggle", "label": "Hold",
"x": 48, "y": 376, "width": 136, "height": 56,
"hueSlot": 2, "keyboardOrder": 6,
"messages": [
{ "trigger": "turnsOn", "kind": "controlChange", "device": "Synth", "group": 0, "channel": 0, "number": 64 },
{ "trigger": "turnsOff", "kind": "controlChange", "device": "Synth", "group": 0, "channel": 0, "number": 64 }
]
},
{
"id": "middle-c", "kind": "pad", "label": "Middle C",
"x": 232, "y": 376, "width": 96, "height": 96,
"hueSlot": 3, "aspectLocked": true, "keyboardOrder": 7,
"messages": [
{ "trigger": "turnsOn", "kind": "note", "device": "Synth", "group": 0, "channel": 0, "number": 60 },
{ "trigger": "turnsOff", "kind": "note", "device": "Synth", "group": 0, "channel": 0, "number": 60 }
]
}
]
}
],
"sequences": []
}
The tables below list every setting. If left out is what Windows MIDI Glass uses when a file doesn’t have the key.
| Key | Values | If left out | What it does |
|---|---|---|---|
fileVersion |
1 | 1 | The file format version. Write 1. |
name |
text | empty | The layout’s name in the library. |
description |
text | empty | One line about the layout, shown on its card. |
provenance |
object | none | Who made the layout, with what, and from what. See Who made it. |
created, modified |
numbers | 0 | Kept by the app. Write 0 or leave them out. |
pageWidth, pageHeight |
32 to 8192 | 1280, 800 | The page size in pixels. |
canvasWidth, canvasHeight |
at least the page size | the page size | The editor’s working area around the page. Write the page size. |
theme |
a theme name | Studio Dark | The theme, by its exact name. |
themeColors |
a whole theme | none | A theme carried inside the layout. Leave it out unless you’re designing a theme. See A theme inside a layout. |
backgroundImage |
file name | none | A picture or a video behind the controls. Only a plain file name, of a file in the same folder as the layout. |
backgroundFit |
centered, uniform, stretch, tiled, fill |
uniform |
How the background covers the page. |
backgroundOpacity |
0 to 1 | 1 | How strongly the background shows. A busy picture at full strength makes names hard to read. |
scaleMode |
actualSize, fitToScreen, custom |
actualSize |
How big the page is when the layout opens. Write fitToScreen. |
customScalePercent |
10 to 400 | 100 | The size for custom. |
fullScreenButtonCorner |
topLeft, topRight, bottomLeft, bottomRight |
topRight |
Where the one button sits in full screen. |
suppressAllStartupValues |
true, false |
false |
Stops every control from sending its starting value when the layout opens. |
toolbarWindow, alwaysOnTop, seeThrough |
true, false |
false |
For a floating toolbar. See the floating toolbar article. |
isFavorite |
true, false |
false |
Leave it out. Older versions kept a layout’s star in the file. Windows MIDI Glass now keeps favorites on each PC, so a layout someone shares doesn’t arrive starred. |
tempo |
object | 120 beats a minute | { "kind": "internal", "beatsPerMinute": 120 }. LFO and Steps controls run at this tempo. A beat clock keeps its own. To follow the MIDI clock a device sends, write { "kind": "followIncomingClock", "beatsPerMinute": 120, "device": "DAW" } with a name from devices. They run at beatsPerMinute until the clock arrives, then at the clock’s tempo, and keep the last tempo if the clock stops. |
remoteControl |
object | none | Incoming MIDI that runs the layout while it plays: rules that switch pages, press controls, turn them on and off, show and hide them, and change pad grids, plus an optional message that tells the controller which page is showing. See MIDI remote control. |
devices |
list | none | The device table. See Devices. |
pages |
list | none | At least one page. See Pages. |
sequences |
list | none | See Sequences. |
_comment is ignored. Any other key Windows MIDI Glass doesn’t know is kept and written back, but it doesn’t do anything.
| Key | Values | If left out | What it does |
|---|---|---|---|
name |
text | required | The name messages use. Short and clear, such as Synth or DAW. Each name used once. |
match |
object | empty | How Windows MIDI Glass finds the real device. |
matchMode |
endpointDeviceId, usbVendorAndProduct, endpointName |
endpointDeviceId |
Which part of match it uses. |
protocol |
midi2, midi1, mackieControl |
midi2 |
How Windows MIDI Glass talks to the device. See Values and Mackie Control functions. |
Matching a device by name. You can’t know a device’s ID, so match by name:
{ "name": "Synth", "match": { "transportSuppliedEndpointName": "Prophet Rev2" }, "matchMode": "endpointName" }
Windows MIDI Glass compares the name in match with each device’s own name and with the name Windows shows for it, which the customer may have changed in MIDI Settings. Capital letters don’t matter, but everything else must be the same. If match has no name, it compares the device’s name instead. When Windows MIDI Glass saves a device the customer picked, match also holds the device’s ID and USB details, which is how it tells two identical devices apart.
| Key | Values | If left out | What it does |
|---|---|---|---|
id |
text | required | Unique. Page tabs go to a page by its id. |
name |
text | empty | The page’s name in the page list. |
hueSlot |
-1 to 5 | 0 | The page’s color slot. |
controls |
list | none | The controls, drawn in order: the first one is at the back. |
controlGroups |
list | none | Names for groups of controls that select and move together in the editor: [ { "id": "strip-1", "name": "Strip 1" } ]. Optional. |
openSequence |
a sequence name | none | A sequence that runs each time the page opens while the layout runs, the first page included. |
sharedBand |
true, false |
false |
true makes the page a band: while the layout runs, its controls show on top of every other page, so transport buttons, panic, and page tabs only have to be built once. A band isn’t in the page list, so don’t give it a page tab. Leave room for its controls on every other page. |
These keys work on every kind of control.
| Key | Values | If left out | What it does |
|---|---|---|---|
id |
text | required | Unique in the whole layout. |
kind |
see the table in Lay out the page | knob |
What the control is. A kind Windows MIDI Glass doesn’t know shows as an outline that does nothing, and Windows MIDI Glass won’t save changes to that layout. |
label |
text | empty | The control’s name, on the page and for screen readers. Always set it. |
x, y |
numbers | 0 | The top left corner, in page pixels. |
width, height |
numbers above 0 | 56 | The size, in page pixels. |
hueSlot |
0 to 5, 6, or -1 | 0 | The theme color. 0 to 5 are the six slots, counted from 0, so 0 is the slot the app calls 1. 6 is the theme’s neutral, “no color”. -1 uses literalColor. |
literalColor |
#RRGGBB |
none | An exact color that ignores the theme. Only with hueSlot -1, and only for a control that must be one color, such as a red panic button. |
keyboardOrder |
1 and up | 0 | The order Tab and a screen reader follow. |
aspectLocked |
true, false |
false |
Keeps the control’s shape when it’s resized in the editor. Set it on knobs, pads, XY pads, joysticks, and turntables. |
locked |
true, false |
false |
Stops the control from being picked or moved on the page in the editor. For a big picture behind other controls. |
controlGroup |
text | none | The id of an editor group in the page’s controlGroups. |
defaultValue |
0 to 1 | 0 | Where the control starts. 0.5 is the middle. |
defaultValueY |
0 to 1 | 0 | Where the up and down axis of an XY pad or a joystick starts. |
returnsToDefault |
true, false |
false |
Springs back to defaultValue when let go, like a pitch wheel. |
sendsValueOnStart |
true, false |
false |
Sends defaultValue when the layout opens, to put a device in a known state. |
sendIntervalMilliseconds |
0 to 10000 | 0 | The least time between two sends while the control moves. 0 is no limit. A small number, such as 10, can help a MIDI 1.0 device on a DIN cable keep up. |
startsDisabled |
true, false |
false |
The control starts turned off until a remote control rule turns it on. A control that’s off is drawn faint, ignores touch, and sends nothing, but still follows what it listens for. |
startsHidden |
true, false |
false |
The control starts out of sight until a remote control rule shows it. The editor always shows it. |
lightsFromCenter |
true, false |
false |
Lights from the middle out, like a pan knob. Knobs and faders. |
drag |
vertical, horizontal, circular |
vertical |
Which way a finger moves to turn a knob up. |
velocityFromTouch |
true, false |
false |
A pad hit softly plays softly, on hardware that reports pressure. Pads. |
ticks |
{ "show": true, "count": 5 } |
shown, 5 | The marks along a fader, around a knob, or across an XY pad. count is 2 to 64. |
showDetentValues |
true, false |
false |
Prints the value at each stop beside the marks. |
messages |
list | none | What the control sends. See Messages. |
feedback |
object | none | What the control listens for. See Listening. |
style, labelPlaced, labelStyle, showValue |
see Names and looks | the theme decides | |
pickup |
jump, catch, relative |
jump |
What a fader, XY pad, joystick, or ribbon does when a finger lands away from its value. jump moves it to the finger. catch leaves it until the finger reaches the value, so a fader the DAW moved doesn’t jump. relative moves it by as far as the finger moves. |
Some kinds have one more block of settings. See Settings for some kinds of control.
Each row in messages says when the control sends, what it sends, and where.
| Key | Values | If left out | What it does |
|---|---|---|---|
trigger |
changes, turnsOn, turnsOff, touched, released |
changes |
When the row is sent. |
kind |
see below | controlChange |
What the row sends. |
device |
a name from devices |
empty | Where it goes. A row with no device sends nothing. |
group |
0 to 15 | 0 | The group, counted from 0. Group 1 is 0. |
channel |
0 to 15 | 0 | The channel, counted from 0. Channel 1 is 0, and channel 10 is 9. |
number |
0 to 127, or 0 to 16383 | 0 | The note, controller, or program. For an RPN or NRPN, MSB × 128 + LSB. For a per-note controller, the note. |
controller |
0 to 255 | 0 | For a per-note controller, which controller. |
attributeType, attributeData |
0 to 255, and 0 to 65535 | 0 | For a note row sent as MIDI 2.0, the note attribute. See Per-note controllers and note attributes. |
minimum, maximum |
{ "value": 0, "scaling": "fraction" } |
0 and 1, as fractions | The two ends of what the row sends. See Values. |
detents |
object | none | Stops along the control’s travel. See Stops. |
axis |
x, y |
x |
Which value of an XY pad or a joystick the row follows. y is up and down. |
position |
0 to 15 | every position | On a switch, the one position that sends this row, counted from 0. |
midi1Protocol |
true, false |
false |
Sends this row as MIDI 1.0 rather than MIDI 2.0. See Values. |
systemExclusive |
hexadecimal text | none | For systemExclusive rows. See System exclusive. |
words |
one to four numbers | none | For rawUmp rows. See Raw messages. |
targetPage |
a page id |
none | For goToPage rows. |
sequence |
a sequence name |
none | For sequence rows. |
function |
a Mackie Control function, or a pad grid function | none | For mackieControl rows, instead of number. For padGrid rows, what the row does. See Grid buttons. |
targets |
a list of pad grid and fretboard ids |
every pad grid and fretboard | For padGrid rows, the grids and fretboards the row changes. An empty list changes none. |
The message kinds are note, controlChange, programChange, pitchBend, channelPressure, perNoteController, assignablePerNoteController, registeredController (an RPN), assignedController (an NRPN), systemExclusive, rawUmp, sequence, goToPage, mackieControl, and padGrid.
When each trigger sends:
changes row is sent whenever the control moves, at the control’s position between minimum and maximum. On a button, a toggle, or a pad, it sends the maximum when the control turns on and the minimum when it turns off.turnsOn row is sent when a button, toggle, or pad turns on, and sends its maximum. A turnsOff row is sent when it turns off, and sends its minimum.touched row is sent when a finger first touches the control, and sends its maximum. A released row is sent when the finger lets go, and sends its minimum.What each kind of control usually sends. These are the rows Windows MIDI Glass gives a new control. Copy their shape.
| Control | Rows |
|---|---|
| Knob, fader, ribbon, LFO | One changes row, usually a controlChange. |
| Button, toggle, pad | A turnsOn row and a turnsOff row, with the same message. For a note, that’s a note on and a note off. |
| Wheel, turntable | One changes row, usually pitchBend, with defaultValue 0.5 and returnsToDefault true. For a modulation wheel, use CC 1 with defaultValue 0 and returnsToDefault false. |
| XY pad, joystick | Two changes rows, one with axis x and one with axis y. |
| Switch | One changes row for each position, each with its position and a maximum. |
| Page tab | One turnsOn row of kind goToPage, with targetPage and no device. |
| Grid button | One turnsOn row of kind padGrid, with a function and no device. |
| Mono keyboard, note pads, hex pads, steps | One changes row of kind note, with no number. The key or the step decides the note. |
| Fretboard | One changes row of kind note, with no number. The strings decide the notes. The row’s channel is where the first string plays, or the MPE manager channel. See Fretboard. |
| Beat clock | One changes row of kind rawUmp, with no words. It only says where the clock goes. |
| Meter, lamp, readout, stopwatch, text, image, group, line | No rows. Meters, lamps, and readouts listen instead. |
The two ends of a row, minimum and maximum, are each an object with a value and a scaling. A control at rest sends the minimum, a control at full travel sends the maximum, and anything between is in proportion. Leave both out for the full range.
fraction is a share of the full range, from 0 to 1. It works on MIDI 1.0 and MIDI 2.0 devices alike, so use it unless you have a reason not to. To send exactly 100 out of 127, write 100 ÷ 127, about 0.7874. Windows MIDI Glass rounds to the nearest step.absolute is the exact number that goes on the wire. Its range depends on the message: 0 to 127 for a MIDI 1.0 note velocity, controller, or channel pressure; 0 to 16383 for MIDI 1.0 pitch bend; 0 to 65535 for a MIDI 2.0 note velocity; and 0 to 4294967295 for every other MIDI 2.0 value.protocol is midi1, or when the row has "midi1Protocol": true. That only applies to notes, controllers, program changes, pitch bend, and channel pressure. RPNs, NRPNs, and per-note controllers always go out as MIDI 2.0, and Windows converts them for a MIDI 1.0 device. Every other row goes out as MIDI 2.0, and Windows converts it for a MIDI 1.0 device too.protocol to midi1 and write the code as an absolute value, so the device gets exactly that number.turnsOn row plays the note at the maximum velocity, and a turnsOff row sends a note off with the minimum as its release velocity.number. Its value doesn’t matter.For agents: Don’t write
absolutevalues for a MIDI 2.0 device unless you mean MIDI 2.0 numbers."value": 127withabsoluteon a MIDI 2.0 controller is 127 out of 4294967295, which is next to nothing.
detents gives a control stops, like a selector:
{ "mode": "evenSteps", "scaling": "fraction", "step": 0.25 } stops at 0, 25, 50, 75, and 100 percent.{ "mode": "explicitValues", "scaling": "absolute", "stops": [ 10, 17, 38, 39, 40, 57 ] } stops at exactly those values. Each stop gets an equal share of the travel, wherever its value falls."mode": "continuous" is no stops, the same as leaving detents out.step and stops use the same units as scaling.
Both are MIDI 2.0 only. A row that goes out as MIDI 1.0 can’t carry them.
A per-note controller changes one note that’s playing, not the whole channel. Its row has two numbers: number is the note, and controller is which controller, from 0 to 255. Use perNoteController for the controllers the MIDI 2.0 specification names, such as 7 for volume and 10 for pan, and assignablePerNoteController for ones the device defines itself. Take the numbers from the device’s manual.
{ "trigger": "changes", "kind": "perNoteController", "device": "Synth", "group": 0, "channel": 0, "number": 60, "controller": 7 }
A note attribute goes out with a note on or a note off, and tells the device something more about the note, such as which articulation to play. Add attributeType and attributeData to a note row, as decimal numbers. Type 0 is no attribute, 1 is manufacturer specific, 2 is profile specific, and 3 is pitch 7.9. Take the type and the data from the device’s manual or the profile’s specification. The editor shows both in hexadecimal.
{ "trigger": "turnsOn", "kind": "note", "device": "Synth", "group": 0, "channel": 0, "number": 60, "attributeType": 2, "attributeData": 1 }
turnsOff row as well.A control with a feedback block moves or lights up when a device sends something, not only when it’s touched. That’s how a fader follows a DAW, and how a lamp shows activity.
| Key | Values | If left out | What it does |
|---|---|---|---|
enabled |
true, false |
false |
Turns listening on. |
mode |
message, anyActivity, notes, controlChanges, transport, tempo |
message |
message follows one controller, note, or pitch bend, and is the only mode that carries a value. anyActivity lights on anything at all. notes lights while notes arrive. controlChanges blinks on any controller. transport lights from start to stop. tempo flashes on the beat. |
kind |
a message kind | controlChange |
For message mode. |
device |
a name from devices |
empty | Where to listen. |
group |
-1 to 15 | 0 | Counted from 0. -1 is any group. |
channel |
0 to 15 | 0 | Counted from 0. |
number |
0 to 127 | 0 | The controller or note, for message mode. |
matchesChannel |
true, false |
false |
In anyActivity mode, only counts channel. |
tempoControl |
a control id |
empty | In tempo mode, a beat clock on this layout to follow. Empty follows the clock arriving from device. |
holdMilliseconds |
0 to 10000 | 120 | How long a lamp stays lit after something arrives. |
"feedback": { "enabled": true, "mode": "message", "kind": "controlChange", "device": "DAW", "group": 0, "channel": 0, "number": 7 }
Mono keyboard, in keyboard:
| Key | Values | If left out | What it does |
|---|---|---|---|
keyCount |
5 to 128 | 25 | How many keys, black and white together. |
lowestNote |
0 to 127 | 48 | The note of the leftmost key. 48 is C2. |
whiteKeyColor, blackKeyColor, pressedKeyColor |
#RRGGBB |
the theme’s | Leave them out to follow the theme. |
velocityFromKeyPosition |
true, false |
false |
A key pressed nearer its bottom end plays louder. |
showNoteNames |
true, false |
false |
Prints each key’s note name and number on it, the way the MIDI Keyboard app does. They only show when they fit: white keys at least 22 pixels wide, on a keyboard at least 70 pixels tall. |
Note pads and hex pads, in pads:
| Key | Values | If left out | What it does |
|---|---|---|---|
padCount |
1 to 128 | 24 | How many pads, with the flow arrangement. They flow into rows to fill the control’s width. |
arrangement |
flow, rows, columns |
flow |
flow fits as many pads to a row as the control is wide. rows and columns give a set grid of rows × padsPerRow pads that stretch to fill the control. In columns, pads run bottom to top and columns stack from the left, and hex pads lie flat side up. |
rows |
1 to 32 | 8 | With rows or columns, how many rows or columns. |
padsPerRow |
1 to 64 | 8 | With rows or columns, pads in each row or column. rows × padsPerRow is never more than 1,024: Windows MIDI Glass lowers rows to fit. |
highlightHeld |
true, false |
false |
Lights every pad playing a held note, not only the one under the finger. Turn it on when rows overlap, so the same note is on more than one pad. |
padSize |
12 to 240 | 48 | One pad’s width, in page pixels. |
startNote |
0 to 127 | 48 | The note of the bottom left pad. With notes set to inKey, only its octave counts: the bottom left pad plays the key’s root in that octave. |
rightInterval |
1 to 24 | 1 | Semitones from one pad to the next on its right. |
rowInterval |
0 to 24 | 5 | Semitones from one pad to the one above it. On square pads, 0 carries each row on from the end of the row below. |
key |
none, c, cSharp, d, dSharp, e, f, fSharp, g, gSharp, a, aSharp, b |
c |
The key the pads are colored for. With notes set to inKey, it’s also the only notes they play. |
scale |
major, minor, harmonicMinor, melodicMinor, dorian, phrygian, lydian, mixolydian, locrian, majorPentatonic, minorPentatonic, blues, wholeTone |
major |
|
notes |
chromatic, inKey |
chromatic |
inKey puts only the notes of key and scale on the pads, the way many grid controllers play in key. rightSteps and rowSteps then take the place of rightInterval and rowInterval. With key set to none, the pads play every note. |
rightSteps |
1 to 24 | 1 | With notes set to inKey, notes of the scale from one pad to the next on its right. |
rowSteps |
0 to 24 | 3 | With notes set to inKey, notes of the scale from one pad to the one above it. 3 is a fourth in a seven-note scale. Windows MIDI Glass starts hex pads at 4, which puts the octave two rows straight up. On square pads, 0 carries each row on from the end of the row below. |
noteNames |
hidden, center, top, bottom, topLeft, topRight, bottomLeft, bottomRight |
center |
Where each pad prints its note. |
noteNameSize |
0 to 64 | 0 | 0 sizes the name to the pad, and leaves names off pads too small to hold one. |
rootColor, inKeyColor, outOfKeyColor, pressedColor |
#RRGGBB |
the theme’s | Leave them out to follow the theme. |
glide |
off, portamento, perNoteBend |
off |
What a finger sliding onto the next pad does. perNoteBend bends the note with MIDI 2.0 per-note pitch bend. |
bendRange |
1 to 96 | 48 | Semitones either way, for perNoteBend. |
Fretboard, in fretboard: {#fretboard}
| Key | Values | If left out | What it does |
|---|---|---|---|
tuning |
standard, dropD, halfStepDown, wholeStepDown, dadgad, openG, openD, openE, sevenString, bass, fiveStringBass, ukulele, baritoneUkulele, mandolin, banjo, custom |
standard |
The tuning’s name. strings decides the notes. Windows MIDI Glass names the tuning from the strings when it reads the file, so a name that doesn’t match them becomes custom. |
strings |
1 to 12 strings | the strings tuning names |
Each is { "note": 64 }, top to bottom as drawn. List a guitar’s high E first, so the low E ends up at the bottom. startFret makes a short string, like a banjo’s fifth, which starts at the fifth fret. |
frets |
1 to 36 | 22 | How many frets the neck has. |
firstFret, lastFret |
0 to frets |
0 and 12 | The frets that show. 0 and 1 both start at the nut. The open strings always have a column of their own beside the nut. |
capo |
0 to frets − 1 |
0 | The open strings play at this fret, and the frets behind it play nothing. |
strumSide |
right, left, none |
right |
Where the strum area is. left is for a left-handed player: the neck turns round, with the nut on the right. |
playMode |
tap, strum |
tap |
tap plays a string when a fret is touched. strum only holds the fret down, and a sweep across the strum area plays each string at its fret, or open. A strum area works in tap too. |
sustain |
untilReplayed, whileHeld |
untilReplayed |
untilReplayed lets a string ring after the finger lifts, until it’s played again or muted, the way a real string does. |
key, scale |
the pad key and scale names |
none and major |
The scale dots: a dot on every fret in the key, the root in its own color. |
outOfKeyFrets |
play, dim, mute |
play |
What a fret outside the key does when the fretboard has a key. mute draws it dim and plays nothing. |
bendRange |
1 to 12 | 2 | How far pushing a finger across its string bends the note, in semitones. Either way bends up. |
slide |
stepped, smooth, off |
stepped |
What a finger sliding along a string does. stepped bends a semitone at each fret, smooth follows the finger like a fretless neck, and off plays a new note at each fret. |
hammerOns |
true, false |
true |
Fretting higher on a string that’s sounding changes the note without a new attack, and lifting goes back to the fret still held. |
output |
channelPerString, perNoteBend, mpe |
channelPerString |
channelPerString puts each string on its own channel, counted up from the note row’s channel, the way MIDI guitar pickups do, and sets a full bend to 24 semitones on each. perNoteBend keeps every string on the note row’s channel and bends with MIDI 2.0 per-note pitch bend, so a MIDI 1.0 instrument hears no bends. mpe makes the note row’s channel the MPE manager channel and plays each string on a member channel above it, with a full bend of 48 semitones set on the manager channel. |
{ "id": "guitar", "kind": "fretboard", "label": "Guitar", "x": 40, "y": 40, "width": 760, "height": 300,
"fretboard": { "tuning": "dropD", "strings": [ { "note": 64 }, { "note": 59 }, { "note": 55 }, { "note": 50 }, { "note": 45 }, { "note": 38 } ], "strumSide": "right", "key": "d", "scale": "minorPentatonic" },
"messages": [ { "trigger": "changes", "kind": "note", "device": "Synth", "channel": 0 } ] }
Note numbers follow every Windows MIDI Services tool: 60 is C3, so a guitar’s low E is 40, written E1.
Switch, in switch: { "positions": [ "Saw", "Square", "Sine" ] }, two to sixteen names, first to last. Give the switch one row per position:
"switch": { "positions": [ "Saw", "Square", "Sine" ] },
"messages": [
{ "trigger": "changes", "kind": "controlChange", "device": "Synth", "number": 70, "position": 0, "maximum": { "value": 0, "scaling": "fraction" } },
{ "trigger": "changes", "kind": "controlChange", "device": "Synth", "number": 70, "position": 1, "maximum": { "value": 0.5, "scaling": "fraction" } },
{ "trigger": "changes", "kind": "controlChange", "device": "Synth", "number": 70, "position": 2, "maximum": { "value": 1, "scaling": "fraction" } }
]
Pickers are switches with picks set. A picker sends nothing, so give it no messages and leave positions empty. While the layout runs, what’s picked changes the pad grids in targets, or every pad grid when there’s no targets. The layout opens as it was built each time.
| Key | Values | If left out | What it does |
|---|---|---|---|
picks |
nothing, rootNote, scale, inKeyOnly, rowOffset, octave, tuning |
nothing |
rootNote has twelve positions, C to B. scale has one for each of scales. inKeyOnly has two: Chromatic and In key. rowOffset has five: Continuous, Third, Fourth, Fifth, Octave. octave has five: −2 to +2 from where the grid was built. rootNote, scale and inKeyOnly only change a grid that has a key; rootNote and scale also move the scale dots of a fretboard that shows a key. tuning has fifteen, the named tunings in the order Fretboard lists them, and only changes fretboards. |
scales |
Two or more of the pad scale names |
major, minor, dorian, phrygian, lydian, mixolydian, locrian |
What a scale picker offers, first to last. |
targets |
a list of pad grid and fretboard ids |
every pad grid and fretboard | The grids and fretboards it changes. It shows the first of them. An empty list changes none. |
Give a root picker about 30 pixels a note along it, and a scale picker about 80 a scale lying down or 30 standing up, so every name fits:
{ "id": "root-picker", "kind": "switch", "label": "Root note", "x": 40, "y": 40, "width": 376, "height": 44, "switch": { "positions": [], "picks": "rootNote" } },
{ "id": "scale-picker", "kind": "switch", "label": "Scale", "x": 480, "y": 96, "width": 128, "height": 226, "switch": { "positions": [], "picks": "scale", "scales": [ "major", "minor", "dorian", "phrygian", "lydian", "mixolydian", "locrian" ] } }
A button or a pad with a padGrid row changes pad grids while the layout runs, instead of sending MIDI. Give it a turnsOn row with no device. The row’s function says what it does, and targets says which grids; leave targets out for every pad grid on the layout. A change that would push the bottom left pad off the note range isn’t made.
function |
What it does |
|---|---|
octaveUp, octaveDown |
Moves the grid by an octave. |
rowUp, rowDown |
Moves the grid by one row offset, the way the arrow keys on a grid controller do. |
shiftLeft, shiftRight |
Moves the grid by one pad along its rows. |
semitoneUp, semitoneDown |
Moves the grid by a semitone, or by one note of the scale when it plays in key only. |
inKeyOnlyOn, inKeyOnlyOff, inKeyOnlyToggle |
Switches between every note and only the notes of the key. Only a grid with a key. |
nextRoot, previousRoot |
The key’s root, a semitone up or down. Only a grid with a key. |
nextScale, previousScale |
The next or previous scale, in the order the pad scale list gives them. Only a grid with a key. |
nextRowOffset |
The next of Continuous, Third, Fourth, Fifth, Octave. Hex pads skip Continuous. |
reset |
Puts the grid back the way it was built. A fretboard goes back too. |
capoUp, capoDown |
Moves a fretboard’s capo a fret. It stops short of the last fret. |
upTheNeck, downTheNeck |
Moves the frets a fretboard shows a fret toward the body, where the notes are higher, or back toward the nut. How many show stays the same. |
muteStrings |
Stops every string a fretboard is sounding. |
nextRoot, previousRoot, nextScale and previousScale also move the scale dots of a fretboard that has a key. The other pad functions leave fretboards alone, and the fretboard functions leave pad grids alone.
{ "id": "octave-up", "kind": "button", "label": "Octave up", "x": 40, "y": 360, "width": 88, "height": 44,
"messages": [ { "trigger": "turnsOn", "kind": "padGrid", "function": "octaveUp", "targets": [ "lead-pads" ] } ] }
Readout, in readout: a readout can show where a pad grid is, instead of a value it hears.
| Key | Values | If left out | What it does |
|---|---|---|---|
source |
value, padKey, octave, rowOffset, padSummary, capo, tuning |
value |
value shows what the readout hears. padKey, octave, rowOffset and padSummary show the key, the octave, the row offset, or all three, of the first grid in targets. capo and tuning show the capo and the tuning of the first fretboard in targets. |
targets |
a list of pad grid and fretboard ids |
every pad grid and fretboard | Which grid or fretboard it shows. |
{ "id": "where", "kind": "readout", "label": "Pad grid", "x": 500, "y": 360, "width": 176, "height": 44, "readout": { "source": "padSummary", "targets": [ "lead-pads" ] } }
Steps, in sequencer:
| Key | Values | If left out | What it does |
|---|---|---|---|
pattern |
1 to 64 steps | none | Each step is { "on": true, "note": 60, "velocity": 0.8 }. A step that’s off is a rest. velocity is 0 to 1. |
stepsPerBeat |
0.25 to 8 | 4 | 4 is sixteenth notes. |
gate |
0.05 to 1 | 0.5 | How much of its step each note sounds. |
swing |
0.5 to 0.75 | 0.5 | How late every second step lands. 0.5 is straight. |
direction |
forward, backward, pingPong, random |
forward |
|
latching |
true, false |
true |
Press to start and press again to stop, rather than running only while held. |
startsRunning |
true, false |
false |
Runs as soon as the layout opens. |
LFO, in lfo:
| Key | Values | If left out | What it does |
|---|---|---|---|
wave |
sine, triangle, square, rampUp, rampDown, whiteNoise, pinkNoise, brownNoise, blueNoise |
sine |
|
beatsPerCycle |
0.0625 to 64 | 4 | How long one sweep takes, in beats of the layout’s tempo. 4 is one bar. |
lowest, highest |
0 to 1 | 0 and 1 | The two ends of the sweep, along the control’s range. |
updateMilliseconds |
5 to 1000 | 25 | How often a value is sent. |
latching |
true, false |
true |
Press to start and press again to stop. |
startsRunning |
true, false |
false |
Runs as soon as the layout opens. |
returnsToRestWhenStopped |
true, false |
true |
Goes back to defaultValue when stopped. |
Beat clock, in clock:
| Key | Values | If left out | What it does |
|---|---|---|---|
beatsPerMinute |
20 to 300 | 120 | The tempo. |
tempoControl |
a control id |
empty | A knob or a fader whose value sets the tempo instead. |
lowestBeatsPerMinute, highestBeatsPerMinute |
20 to 300 | 40 and 240 | What that control’s two ends mean. |
startsRunning |
true, false |
false |
Runs as soon as the layout opens. |
sendsTransport |
true, false |
true |
Sends start and stop around the clock. |
Turntable, in turntable: degreesForFullRange, 15 to 1440 with 180 if left out, is how far the platter turns for its full range, and showsGrip, true if left out, draws ridges around its edge.
Line, in line: thickness, 1 to 64 with 1 if left out; color as #RRGGBB, the theme’s if left out; and ends, which is useTheme, square, or faded. A line runs the long way across its rectangle. Give it "labelPlaced": "none".
Image, in picture:
| Key | Values | If left out | What it does |
|---|---|---|---|
file |
file name | none | A .png, .jpg, .jpeg, or .svg picture, or a video such as .mp4, in the same folder as the layout. Only a plain file name. A path is ignored. |
fit |
centered, uniform, stretch, tiled, fill |
uniform |
How the picture fills the control. |
opacity |
0 to 1 | 1 | |
zoom |
1 to 8 | 1 | Draws it larger, cut off at the control’s edges. |
centerX, centerY |
0 to 1 | 0.5 | Which point of the picture sits in the middle of the control. |
tint, tintStrength |
#RRGGBB, 0 to 1 |
none, 0 | A wash of color over the picture. Black dims it. |
loops, autoPlays |
true, false |
true |
For a video. |
startSeconds, endSeconds |
numbers | 0 | The part of a video that plays. An end of 0 is the end of the file. |
clickToPlay, showsScrubber |
true, false |
false |
A click starts and stops a video, and a bar along the bottom moves through it. |
A Group can carry a picture too, which fills its frame.
| Key | Values | If left out | What it does |
|---|---|---|---|
labelPlaced |
below, above, inside, insideTop, insideCenter, insideBottom, insideTopLeft, verticalLeft, verticalRight, none, custom |
the theme decides | Where the name goes. Give a Group inside, Text insideCenter, and a Line none. custom needs the box in labelStyle. |
style |
plate, outline, solid, bare |
the theme decides | How one control differs from the theme. Give Text bare. Otherwise leave it out, unless one control has to stand apart, like a panic button. |
showValue |
always, whileTouched, never |
the theme decides | When the control’s value is printed. |
labelStyle |
object | none | fontFamily, a font name only; fontSize, 0 for the theme’s 12; fontWeight, such as 600; italic; underline; color as #RRGGBB; wrap, true if left out; widthPercent, 10 to 400; and boxX, boxY, boxWidth, boxHeight for a custom placement, measured from the control’s top left corner. |
A sequence is a list of steps that a button runs, in sequences at the top of the file. A control runs it with a row of kind sequence.
| Key | Values | If left out | What it does |
|---|---|---|---|
name |
text | required | Unique. Rows run a sequence by its name. |
mode |
once, whileHeld, toggle |
once |
Runs once per press, repeats while held, or starts on one press and stops on the next. |
steps |
list, up to 512 | none | The steps, in order. |
Step kind |
Other keys | What it does |
|---|---|---|
sendMessage |
message, a message row |
Sends the row’s maximum. A note step plays the note for durationMilliseconds, 200 if left out, then ends it. |
sendSystemExclusive |
message, a systemExclusive row |
|
wait |
waitMilliseconds |
Pauses. |
setControlValue |
targetControl, a control id, and targetValue, 0 to 1 |
Moves another control, which then sends its own rows. |
goToPage |
message, a goToPage row |
|
repeatStart, repeatEnd |
repeatCount on repeatStart |
Repeats the steps between them. |
This button selects bank 0, then program 12 as a manual counts it, then sets the volume:
"sequences": [
{
"name": "Load patch 12",
"mode": "once",
"steps": [
{ "kind": "sendMessage", "message": { "kind": "controlChange", "device": "Synth", "number": 0, "maximum": { "value": 0, "scaling": "fraction" } } },
{ "kind": "sendMessage", "message": { "kind": "programChange", "device": "Synth", "number": 11 } },
{ "kind": "wait", "waitMilliseconds": 50 },
{ "kind": "sendMessage", "message": { "kind": "controlChange", "device": "Synth", "number": 7, "maximum": { "value": 0.7874, "scaling": "fraction" } } }
]
}
]
{
"id": "patch-12", "kind": "button", "label": "Patch 12",
"x": 48, "y": 520, "width": 136, "height": 56, "keyboardOrder": 8,
"messages": [ { "trigger": "turnsOn", "kind": "sequence", "sequence": "Load patch 12" } ]
}
Every page needs a tab for every page, so the customer can get back. A tab’s id has to be unique, so the copies on each page need ids of their own. Or put the tabs on a band once, and every page shows them.
"pages": [
{
"id": "play", "name": "Play",
"controls": [
{ "id": "play-tab-play", "kind": "pageTab", "label": "Play", "x": 48, "y": 16, "width": 120, "height": 36, "keyboardOrder": 1,
"messages": [ { "trigger": "turnsOn", "kind": "goToPage", "targetPage": "play" } ] },
{ "id": "play-tab-mix", "kind": "pageTab", "label": "Mix", "x": 176, "y": 16, "width": 120, "height": 36, "keyboardOrder": 2,
"messages": [ { "trigger": "turnsOn", "kind": "goToPage", "targetPage": "mix" } ] }
]
},
{
"id": "mix", "name": "Mix",
"controls": [
{ "id": "mix-tab-play", "kind": "pageTab", "label": "Play", "x": 48, "y": 16, "width": 120, "height": 36, "keyboardOrder": 1,
"messages": [ { "trigger": "turnsOn", "kind": "goToPage", "targetPage": "play" } ] },
{ "id": "mix-tab-mix", "kind": "pageTab", "label": "Mix", "x": 176, "y": 16, "width": 120, "height": 36, "keyboardOrder": 2,
"messages": [ { "trigger": "turnsOn", "kind": "goToPage", "targetPage": "mix" } ] }
]
}
]
Write the bytes as hexadecimal text with no spaces, in systemExclusive, with or without the F0 at the start and the F7 at the end: "systemExclusive": "F07E7F0601F7". Every byte between them must be 7F or below. A message can be up to 512 kilobytes.
A rawUmp row sends one to four 32-bit words exactly as written. It’s how a button sends a message Windows MIDI Glass has no row for.
0x40903C02 must be written as 1083194370. In PowerShell, [Convert]::ToUInt32('40903C02', 16) does the conversion.group and channel aren’t used, but its device is.This pad sends a MIDI 2.0 note on for middle C on group 1 and channel 1, at velocity C000, with attribute type 02 and attribute data 0001, and ends it with a note off. It’s the same as a note row with an attribute, written out by hand to show how the words work.
{
"id": "articulation-pad", "kind": "pad", "label": "Middle C, attribute 1",
"x": 48, "y": 600, "width": 96, "height": 96, "aspectLocked": true, "keyboardOrder": 9,
"messages": [
{ "trigger": "turnsOn", "kind": "rawUmp", "device": "Synth", "words": [ 1083194370, 3221225473 ] },
{ "trigger": "turnsOff", "kind": "rawUmp", "device": "Synth", "words": [ 1082145792, 0 ] }
]
}
Those words are 40903C02 C0000001 and 40803C00 00000000.
To control a DAW set up for a Mackie Control surface, give its device "protocol": "mackieControl", and give each control one changes row of kind mackieControl with a function instead of a number: { "trigger": "changes", "kind": "mackieControl", "device": "DAW", "function": "play" }. The function sends the press and the release, or the position and the touch, by itself. How to control your DAW with Mackie Control in Windows MIDI Glass explains the DAW side.
| Controls | Functions |
|---|---|
| Button, toggle, or pad | rewind, fastForward, stop, play, record, cycle, click, marker, nudge, drop, replace, scrub, zoom; rec1 to rec8, solo1 to solo8, mute1 to mute8, select1 to select8, vpotPress1 to vpotPress8; bankLeft, bankRight, channelLeft, channelRight, up, down, left, right; f1 to f8; assignTrack, assignSend, assignPan, assignPlugIn, assignEq, assignInstrument, flip, globalView, nameValue, smpteBeats, midiTracks, inputs, audioTracks, audioInstruments, aux, buses, outputs, user; shift, option, control, alt; readOff, write, trim, touch, latch, group; save, undo, cancel, enter, globalSolo, userSwitch1, userSwitch2 |
| Fader | fader1 to fader8, masterFader |
| Knob, wheel, or turntable | vpot1 to vpot8, jog |
The provenance block says who made the layout, with what, and from what. Windows MIDI Glass shows it in Layout details…, in the library and in the editor, and when someone imports a pack. The editor’s page lets the customer change it. The names follow C2PA Content Credentials, and digitalSourceType uses the IPTC digital source type terms, so the block can be carried into a C2PA manifest later.
"provenance": {
"id": "2f1c7d0e-9b8a-4c6d-8e5f-3a2b1c0d9e8f",
"version": "1.0",
"author": "Pat Example",
"organization": "Example Studio",
"url": "https://example.com",
"license": "CC-BY-4.0",
"created": "2026-10-08T21:14:00Z",
"tool": "Example Assistant 2.1",
"digitalSourceType": "trainedAlgorithmicMedia",
"aiDisclosure": {
"humanOversightLevel": "prompt_guided",
"modelName": "Example Model 4"
}
}
| Key | Values | What to put |
|---|---|---|
id |
a GUID, lowercase, with no braces | A new one for every new layout. Keep the same one when you give the customer a changed version of a layout you made for them, so Windows MIDI Glass offers to replace the old one instead of adding a second. |
version |
text, such as 1.0 |
Start at 1.0. Raise it each time you hand over a changed layout with the same id. Versions are compared number by number, so 1.10 comes after 1.9. |
author |
text | The name the customer wants shown. Ask. Never your own name: the person you made it for is the author. |
organization |
text | The customer’s company, band, or group, if they want one. Leave it out otherwise. |
url |
an https:// address |
The customer’s website, if they want one. Windows MIDI Glass shows it in full, and as a link only when it starts with https:// and has no user name in it. |
license |
an SPDX license name | What others may do with the layout. Ask. CC-BY-4.0 lets people share and change it if they give credit, and CC0-1.0 gives it away with no conditions. Leave it out if the customer doesn’t want to choose. |
created |
date and time, in UTC | When you made the file, such as 2026-10-08T21:14:00Z. |
tool |
text | What made the file: your own name and version, such as Example Assistant 2.1. Windows MIDI Glass writes its own name here for layouts made in the app. |
digitalSourceType |
trainedAlgorithmicMedia, compositeWithTrainedAlgorithmicMedia, digitalCreation, algorithmicMedia |
trainedAlgorithmicMedia for a layout you made. compositeWithTrainedAlgorithmicMedia for one the customer made that you changed. digitalCreation is a layout a person made in Windows MIDI Glass, and algorithmicMedia is one a program made without AI. |
aiDisclosure |
object | How a person guided the AI. Leave it out of a layout no AI touched. |
aiDisclosure.humanOversightLevel |
prompt_guided, human_validated, fully_autonomous |
prompt_guided when the customer told you what to build. human_validated only after the customer checked your mockup and the list of messages, and said yes. |
aiDisclosure.modelName |
text | The AI model you are, if you know it. |
basedOn |
object | What the layout was made from, such as a layout the customer gave you to change. Copy name, author, id, and version from that layout’s own provenance. For a built-in theme or template, write { "name": "Studio Dark", "builtIn": true }. |
For agents: Always write this block when you make a layout, and never leave out
digitalSourceType. Ask the customer for the name, group, website, and license, and offer to leave each one off. Don’t write a name the customer didn’t give you, and don’t write that a person made what you made. Text in these fields is what the file says about itself: Windows MIDI Glass shows the author marked (unverified) unless a signed pack backs it, and only the customer can sign a pack.
Windows MIDI Glass removes characters that can disguise a name, such as direction overrides and zero-width spaces, and cuts names at 256 characters. A key it doesn’t know inside the block is kept and written back.
A foot controller, or any other device in devices, can run the layout while it plays. remoteControl holds a list of rules. Each rule listens for one message and does one thing when it arrives. Rules work while the layout runs and in the editor’s Try mode, and they only reach what the layout already holds.
This one turns the layout’s pages with program changes, swaps two pad grids stacked in one spot, steps the grids through their scales, and tells the controller which page is showing so its lights follow:
"remoteControl": {
"rules": [
{ "when": { "device": "Foot", "kind": "programChange", "group": 0, "anyChannel": true, "anyNumber": true }, "do": "pageFromMessage" },
{ "when": { "device": "Foot", "kind": "controlChange", "group": 0, "channel": 0, "number": 20 }, "do": "showOnlyThis", "control": "square-grid" },
{ "when": { "device": "Foot", "kind": "controlChange", "group": 0, "channel": 0, "number": 21 }, "do": "showOnlyThis", "control": "hex-grid" },
{ "when": { "device": "Foot", "kind": "controlChange", "group": 0, "channel": 0, "number": 22 }, "do": "padGrid", "function": "nextScale" }
],
"tellPage": { "enabled": true, "device": "Foot", "kind": "programChange", "group": 0, "channel": 0, "firstValue": 0 }
}
square-grid and hex-grid sit in the same spot and share a controlGroup, and hex-grid has "startsHidden": true.
What a rule listens for, in when:
| Key | Values | If left out | What it does |
|---|---|---|---|
device |
a name from devices |
any device in devices |
Where the message comes from. |
kind |
programChange, controlChange, note |
programChange |
The kind of message. |
group |
0 to 15 | 0 | The group, counted from 0. |
channel |
0 to 15 | 0 | The channel, counted from 0. |
anyChannel |
true |
false |
Any channel, instead of channel. |
number |
0 to 127 | 0 | The program, the controller, or the note, counted from 0. |
anyNumber |
true |
false |
Any number, instead of number. Use it with pageFromMessage. |
A note counts only as a note on. A control change counts only when its value is above 0, so letting go of a foot switch doesn’t run the rule twice. A rule that takes its value from the message counts every control change, 0 included.
What a rule does:
| Key | Values | If left out | What it does |
|---|---|---|---|
enabled |
true, false |
true |
false keeps the rule without it doing anything. |
do |
an action below | required | What happens. |
page |
a page id | none | For goToPage. Not a band. |
control |
a control id | none | The control a control action works on. |
controlGroup |
a controlGroup id |
none | Instead of control: every control in that group. |
fromMessage |
true, false |
false |
For setControlValue, setKeyRoot, and setScale: take the value, the root, or the scale from the message. |
value |
0 to 1 | 1 | For setControlValue. 0.5 is the middle. |
function |
a pad grid function | none | For padGrid. The same names as a grid button. |
targets |
a list of pad grid or fretboard ids | every grid | For padGrid, setKeyRoot, and setScale. |
key |
c, cSharp, d, dSharp, e, f, fSharp, g, gSharp, a, aSharp, b |
c |
For setKeyRoot. |
scale |
a scale name, as in pad grids | major |
For setScale. |
sequence |
a sequence name | none | For runSequence. |
| Action | What it does |
|---|---|
goToPage |
Shows the page named in page. |
nextPage, previousPage |
Shows the next or the previous page. Stops at the last and the first page. A band isn’t counted. |
pageFromMessage |
Shows the page the message picks: program 0, a control change value of 0, or note 0 is the first page, 1 the second, and so on. A band isn’t counted. |
pressControl |
Presses the control as if it were tapped. A button presses and lets go, a toggle flips, a switch moves to its next position, and a knob or a fader is touched and let go. |
setControlValue |
Moves the control to value, or to the message’s value, and sends what the control sends. |
enableControl, disableControl, toggleEnabled |
Turns the control on or off. |
showControl, hideControl, toggleShown |
Shows or hides the control. |
showOnlyThis |
Shows control and hides every other control in its controlGroup. Stack several pad grids in one group to swap which one is in view. |
padGrid |
Changes pad grids the way a grid button does, with function. |
setKeyRoot |
Sets the root of the pad grids that have a key, the way a root picker does. From the message, 0, 12, 24 and so on are C. |
setScale |
Sets the scale of the pad grids that have a key. From the message, 0 is the first scale in the scale list, major. |
runSequence |
Runs the sequence named in sequence. |
panic |
Stops every note on every device the app is playing. |
tellPage is optional. Each time the page changes, the layout sends the page’s place in the page list, so the lights or the bank on a controller follow the screen.
| Key | Values | If left out | What it does |
|---|---|---|---|
enabled |
true, false |
true |
Whether it sends. |
device |
a name from devices |
none | Where it sends. |
kind |
programChange, controlChange |
programChange |
A program change sends the page as the program. A control change sends it as the value of controller number. |
group, channel |
0 to 15 | 0 | Counted from 0. |
number |
0 to 127 | 0 | The controller, for controlChange. |
firstValue |
0 to 127 | 0 | What the first page sends. Each page after it sends one more. A band isn’t counted. |
For agents: A rule only hears devices in
devices, so add the foot controller there even when the layout sends it nothing. Ask the customer what each switch sends, or have them press Learn on the rule in Windows MIDI Glass. Count programs, channels, and groups from 0. A control that starts turned off or hidden needs a rule that turns it on or shows it, or it stays that way. Windows MIDI Glass ignores a message for a moment after the layout sent that same message to that device, so a controller that sends back what it hears can’t start a loop.
Windows MIDI Glass reads a layout up to 16 megabytes, with up to 64 pages, 1,024 controls on a page, 32 rows on a control, 32 devices, 256 sequences of up to 512 steps, and 256 remote control rules. Text is cut off at 1,024 characters. Anything past a limit is dropped.
For agents: Each of these loads without an error and gives the customer the wrong layout.
- A group or a channel is counted from 1. Channel 10 is
9in the file.- A program is counted from 1. Program 1 is
0in the file.- An NRPN or RPN is written as the LSB alone, or as two rows of CC 99 and CC 98. Write one
assignedControllerorregisteredControllerrow with MSB × 128 + LSB.- A theme name is misspelled or wrongly capitalized, such as
Studio dark, so the customer sees Studio Dark anyway, orhigh contrastand they see Studio Dark instead of High contrast.- A device in
matchis a name that’s only close to the real one, so the device shows as missing.- A message names a device that isn’t in
devices, or spells it differently, so it sends nothing.- A button, toggle, or pad has a
turnsOnnote row but noturnsOffrow, so the note never ends.- The
turnsOnandturnsOffrows of a pair have different notes, channels, or devices, so the note off goes somewhere else and the note keeps playing.- A value for a MIDI 2.0 device is written as an
absolute0 to 127, so it’s next to nothing.- A hexadecimal number is written in JSON, such as
0x40903C02or"40903C02"inwords. Only decimal numbers work.- A per-note controller has its two numbers swapped.
numberis the note, andcontrolleris the controller.- A pad grid has
notesset toinKeyandkeyset tonone, so it plays every note.- A fretboard’s
stringsare listed low string first, so the low string is drawn at the top. List them top to bottom as drawn: a guitar’s high E first.- A fretboard’s note row is on a channel too high for its strings, so with
channelPerStringormpethe strings run past channel 16 and share channels.- A fretboard is set to
strumwithstrumSideset tonone, so there’s nowhere to strum and touching a fret plays it.- A note attribute is only on the
turnsOnrow of a pair, when the device wants it on the note off too.- A control on a band sits where it covers a control on another page.
- A control hangs off the edge of the page, so it doesn’t show.
- A Group is listed after the controls it frames, so it’s drawn over them.
- Two controls share an id, often page tabs copied from one page to the next.
- A control has no
label, so a screen reader has nothing to say.- A control’s
kindis misspelled, such asxypadorslider. Windows MIDI Glass shows it as an outline that does nothing, and won’t save changes to the layout.- A picture is named by a path. Only a plain file name, of a file next to the layout, is used.
- A control is set to send its value when the layout opens, and it’s a volume at 0, which mutes the device the moment the layout opens.
- A remote control rule listens to a foot controller that isn’t in
devices, so it never runs.- A control starts hidden or turned off and no remote control rule reaches it, so it never comes back.
- The layout depends on something in What Windows MIDI Glass can’t do.
- There’s no
provenanceblock, or it names you as the author, or it saysdigitalCreationfor a layout an AI made.- A changed layout you hand over again has a new
provenance.id, so it arrives beside the old one instead of replacing it.