JavaScript events
Event principles
JavaScript event handlers are called whenever an event of a specific type occurs.
Important: Unlike midiScript events, there are no filtering attributes that limit when an event triggers; for example, OnControlChangeReceived() event handlers in all scripts will be triggered every time a Control Change message is received. It is up to each script to decide whether to handle the specific command.
JavaScript is case-sensitive, but the plugin still tries to locate event handlers even when the case differs. If an event handler is not called even though it should be, please check the function name's case and verify that it matches what this documentation says; the case-insensitive search might fail for some reason.
Script initialization event
The script initialization event is triggered when the script is loaded, either when Stream Deck starts or when a profile/page/folder with the button/dial is loaded.
Midi events
Midi events differentiate between commands received from an external source and commands sent from a Stream Deck button or dial. If you want your script to manage both sent and received commands, you need to create event handlers for each type.
Midi channels are always in the range 1-16, both in Midi event handlers and in midi actions.
Events
OnInit()
The OnInit() event is triggered once when the script is loaded.
OnControlChangeReceived(channel, control, value)
OnControlChangeSent(channel, control, value)
OnControlChangeReceived() is triggered when a Control Change message is received from an external source.
OnControlChangeSent() is triggered when a Control Change message is sent by the plugin.
- channel is the Midi channel for the command. in the range 1-16.
- control is the control in the range 0-127 for single-byte commands and 128-160 for dual-byte commands.
- value is the value in the range 0-127 for single-byte commands and 0-16383 for dual-byte commands.
OnNRPNReceived(channel, control, value)
OnNRPNSent(channel, control, value)
OnNRPNReceived() is triggered when an NRPN message is received from an external source.
OnNRPNSent() is triggered when an NRPN message is sent by the plugin.
- channel is the Midi channel for the command, in the range 1-16.
- control is the NRPN control.
- value is the value for the NRPN command
OnProgramChangeSent(channel, program)
OnProgramChangeReceived(channel, program)
OnProgramChangeReceived() is triggered when a Program Change message is received from an external source.
OnProgramChangeSent() is triggered when a Program Change message is sent by the plugin.
- channel is the Midi channel for the command, in the range 1-16.
- program_ is the program in the range 0-127
OnNoteOnSent(channel, key, velocity)
OnNoteOnReceived(channel, key, velocity)
OnNoteOnReceived() is triggered when a NoteOn message is received from an external source.
OnNoteOnSent() is triggered when a NoteOn message is sent by the plugin.
- channel is the Midi channel for the command, in the range 1-16.
- key is the midi note number in the range 0-127
- velocity is the velocity for the note in the range 0-127
OnNoteOffSent(channel, key, velocity)
OnNoteOffReceived(channel, key, velocity)
OnNoteOffReceived() is triggered when a NoteOff message is received from an external source.
OnNoteOffSent() is triggered when a NoteOff message is sent by the plugin.
- channel is the Midi channel for the command, in the range 1-16.
- key is the midi note number in the range 0-127
- velocity is the velocity for the note in the range 0-127
OnChannelPressureSent(channel, pressure)
OnControlChangeReceived(channel, control, value)
OnChannelPressureReceived is triggered when a Channel Pressure message is received from an external source.
OnChannelPressureSent() is triggered when the plugin sends a Channel Pressure message.
Special treatment is needed if you want to get the track volume as defined in the Mackie Control protocol. Getting the VU level is a bit tricky, as bitwise operations are required to extract it from the received data. The following script will display the VU level for track 1 (not to be confused with Midi channel 1).
function OnChannelPressureReceived(channel,pressure)
{
if (channel==1 && pressure==0-15)
{
l_vu=(pressure & 15);
ui.text(Math.min(l_vu,12));
}
}
To get track 2, you need to trigger on incoming values in the range 16-31, and so on.
- channel is the Midi channel for the command, in the range 1-16.
- pressure is the pressure in the range 0-127
OnPitchBendReceived(channel, value)
OnPitchBendSent(channel, value)
OnPitchBendReceived() is triggered when a PitchBend message is received from an external source.
OnPitchBendSent() is triggered when a PitchBend message is sent by the plugin.
- channel is the Midi channel for the command, in the range 1-16.
- value is the value in the range 0-16383.
OnSysExReceived(sysexdata)
OnSysExReceived() is called when a sysex message is received. sysexdata is a byte array with the sysex start and end bytes (0xF0/0xF7) removed.
Helper functions
To handle sysex messages, several helper functions are available to provide functionality similar to the sysex event in midiScript.
- midi.sysex.match(sysexdata, filter) -> bool
Checks whether an incoming SysEx message matches a midiScript-style SysEx filter.
Returns true if it matches; otherwise, false.
Overloads accept either a byte[] or a JS value (typically an array of numbers). - midi.sysex.text(sysexdata, filter) -> string | null
If the SysEx matches the filter, it extracts the selected bytes and returns them as an ASCII string.
Returns null if the message doesn’t match or if nothing can be extracted.
Overloads accept byte[], JS array or typed array. - midi.sysex.part(sysexdata, filter) -> any[] | null
If the SysEx matches the filter, it extracts the selected bytes and returns them as a JS array of byte values.
Returns null if it doesn’t match.
(Useful when you want to parse bytes yourself in JS.) - midi.sysex.hex(sysexdata, filter) -> string | null
If the SysEx matches the filter, extracts the selected bytes and returns them as a hex string, e.g., "12 34 AB".
Returns null if it doesn’t match. - midi.sysex.midHex(sysexdata, filter, start, len) -> string | null
A MID() helper that operates on the hex string extracted by hex(...).
• Start() is 1-based
• Returns a string slice of the hex text (not bytes)
• Returns null if there is no match or no hex string
Example: if the extracted hex is "12 34", then midHex(..., 1, 1) returns "1".
Notes on filter
filter uses the same matching/extraction logic as the existing midiScript SysEx event filter. Please note that the filter definitions should include the sysex start- and stop bytes, even though the received sysexdata does not. This may seem a bit illogical, but it is done this way to be compatible with midiScript filter strings.
With the structure "F0 AA XX BB F7":
- F0 is the standardized "sysex start" byte.
- F7 is the standardized "sysex end" byte.
- AA and BB are arbitrarily long sequences of bytes required for the script to trigger on that sysex command.
- XX is the flexible part of the command stored for later use.
(It should be the string ”XX” in the event; the script engine searches for this string to determine which part of the sysex message is flexible.) - Apart from the "XX" part, bytes shall be defined in hexadecimal representation and separated by space characters.
Example:
Filter: F0 01 20 XX F7
Incoming SysEx (hex): F0 01 20 48 69 21 F7
- midi.sysex.match(incomingSysex, "F0 01 20 XX F7")
Result: true - midi.sysex.part(incomingSysex, "F0 01 20 XX F7")
Result: [0x48, 0x69, 0x21] (same as [72, 105, 33]) - midi.sysex.hex(incomingSysex, "F0 01 20 XX F7")
Result: "48 69 21" - midi.sysex.text(incomingSysex, "F0 01 20 XX F7")
Result: "Hi!" - midi.sysex.midHex(incomingSysex, "F0 01 20 XX F7", start, len) (1-based substring on the extracted hex string)
Given extracted hex is "48 69 21":
• midHex(..., 1, 2) ⇒ "48"
• midHex(..., 1, 1) ⇒ "4"
The sysex events are real-time events. When a sysex command is received, the appropriate script events are fired. Period. The sysex string isn't stored anywhere for later use.
- sysexdata is a byte array, excluding the start and stop bytes F0/F7.
OnTimeUpdated(time)
OnBPMUpdated(bpm)
Please note: OnTimeUpdated() is called whenever the time code updates, approximately every 10 ms. It is important to keep the processing in the OnTimeUpdated function as short as possible to avoid bogging down the system.
Helper objects for accessing time:
var t = midi.time.get() => time object
var t =midi.time.text() => time as string: “hour:minute:second.frame”
var t =midi.time.bpm() => bpm as number
The time object in the OnTimeUpdated() event is defined as follows:
interface TimeInfo {
hour: number;/** SMPTE hour component. */
minute: number; /** SMPTE minute component. */
second: number; /** SMPTE second component. */
frame: number; /** SMPTE frame within second. */
smpteType: number; /** SMPTE type code: 0=24fps, 1=25fps, 2=30fps(drop-frame), 3=30fps. */
smpte: string; /** Human-readable SMPTE frame rate text (e.g. "25 fps", "30 fps(df)"). */
bpm: number; /** Latest known BPM (0 if unknown). */
}
The bpm object in the OnBPMUpdated() event is a number that contains the same information as the bpm member of the time object.
OnDeepLinkReceived(part1, part2, part3)
According to the Stream Deck SDK documentation: "Deep-linking is the process of sending messages to local apps via custom URL scheme registered on the user's device, for example, a computer or mobile phone."
Deep links can be sent from, e.g., a web browser, using the following syntax:
streamdeck://plugins/message/se.trevligaspel.midi/hello?world#again?streamdeck=hidden
└─┬──┘ └─┬─┘ └─┬─┘└────────┬───────┘
part1 part2 part3 optional
part1, 2 and 3 keep the information that will reach the script.
- part1 must be prefixed with the "/" character.
- part2 must be prefixed with the "?" character.
- part3 must be prefixed with the "#" character.
All parts are optional; it is perfectly fine, e.g., to have a URL with only part2 present.
Sending a deep-link message will, by default, bring the Stream Deck editor window to the front and focus it. If you do not want that to happen, you can add the parameter "?streamdeck=hidden" to the URL.
The content of each part is totally up to you, as is what you should do with it in the script. A suggestion could be to have part1 as a script target denominator if you have different scripts that should respond to different messages.
Please note: (deeplink) events are accessible for button and dial scripts at all times. However, technical limitations mean that (deeplink) events can only be utilized by background scripts if at least one Midi plugin button or dial is present in an active profile and page on any device. If no Midi plugin actions are loaded, background scripts will not have access to deep links.
For more information about deep linking, please see the deep linking page.
_part1_, _part2_ and _part3_ will, of course, map to the corresponding part in the sent message (not including the prefix characters).
OnDeviceConnected(name)
OnDeviceDisconnected(name)
When a Stream Deck device is connected or disconnected, the script gets notified.
Please note: Device events are always available for button and dial scripts. However, technical limitations mean that background scripts can only access device events if at least one Midi plugin button or dial exists in an active profile and page on any device. If no Midi plugin actions are loaded, background scripts will not be able to access device events.
OnGlobalVariableChanged(name, value)
The OnGlobalVariableChanged() function is called every time a global variable changes. It is imperative that the first thing you do is check whether the variable is relevant to your script.
- name is the variable name as a string.
- value is the current value of the variable.
OnTimerElapsed(name, isGlobal, time)
The OnTimerElapsed() function is called every time a timer elapses. It is imperative that the first thing you do is check whether the timer is relevant to your script.
- name is name of the timer as a string.
- isGlobal is a boolean with the value true if it's a global timer.
- time is the expiration time (in milliseconds) causing the function to be called.
OnKeyPressed(state)
OnKeyReleased(state)
OnDialPressed()
OnDialReleased()
Press functions are called when the button or dial is pressed. Unlike midiScript (press) events, you cannot have multiple JavaScript press functions. If you need consecutive presses to produce different actions, you need to track this yourself.
Release functions are called when the button or dial is released. There is no automatic synchronization between press and release events; if you need to match a release event with a specific press event, you need to track this yourself. For dials, you may consider using release events instead of press events since it is unclear at the press moment whether the intention is to press-rotate or press.
Please note that press and release events are triggered for the Generic Midi fader and V-pot buttons only when the speed is set to 0. If speed is set to 1-100, the (fader) and (vpot) events are triggered instead.
state is the current state of the button as a number; 0 or 1.
OnFaderMoved(value)
When using the Generic Midi button as a fader, it can trigger either an OnKeyPressed() event or OnFaderMoved() events, depending on the current speed setting for the button.
Speed = 0
If the fader speed is 0, it operates in "button mode", and OnKeyPressed() events will be triggered. In "button mode", the fader does not move, and the OnKeyPressed() event will be triggered once.
Sequence of events when speed is 0:
- You press the button.
- The OnKeyPressed() function is called.
- You release the button
- The OnKeyReleased() function is called.
Speed = 1-100
If the fader speed is in the range of 1-100, fader events will be triggered as follows:
- You press the button.
- The plugin moves the fader.
- The OnFaderMoved(value) OnFaderMoved(value), where "value" is the current position of the fader (within the min/max range defined in the editor)
- Steps 2-3 are repeated as long as the button is pressed or until the fader reaches the endpoint.
value is the value for the fader in the range defined by the min/max properties.
OnVpotMoved(value)
When using the Generic Midi button as a V-Pot, it can trigger either an OnKeyPressed() event or OnVpotMoved() events, depending on the current speed setting for the button.
Speed = 0
If the V-Pot speed is 0, it operates in "button mode", and OnKeyPressed() events will be triggered. In "button mode", the V-Pot does not move, and the OnKeyPressed() event will be triggered once.
Sequence of events when speed is 0:
- You press the button.
- The OnKeyPressed() function is called.
- You release the button
- The OnKeyReleased() function is called.
Speed = 1-100
If the fader speed is in the range of 1-100, fader events will be triggered as follows:
- You press the button.
- The plugin moves the V-Pot.
- The OnVpotMoved(value) function is called, where "value" is the current position of the V-Pot (within the min/max range defined in the editor)
- Steps 2-3 are repeated as long as the button is pressed or until the V-Pot reaches the endpoint.
value is the value for the fader in the range defined by the min/max properties.
OnDialRotated(ticks, value)
The OnDialRotated() function is called when the dial is rotated. The fader/vpot/bar will move according to the step setting, and then the function will be called with the current position's value. If the step size is set to 0, no automatic movement occurs, but the function is called for each rotation of the dial.
- value is the value for the dial in the range defined by the min/max properties.
- tics is the direction and speed of the rotation, as reported by the Stream Deck software. A positive value indicates clockwise rotation, while a negative value indicates counterclockwise rotation. The value indicates the number of physical steps (you feel when rotating the dial) that have passed in the last 50 ms, and it is never 0. The maximum value is not documented, but my experience is that a range of +/- 1-20 (approximately) is a reasonable outcome. While the dial is rotated, the OnDialRotated() function will be called every 50 ms.
OnScreenTouched(x, y)
OnScreenTouched(x, y, isLongPressed)
The OnScreenTouched() function is called when you tap the dial screen.
- x, y are the X and Y coordinates of the screen tap within the 200x100 px screen space available for the dial. The top-left corner is (0,0), and the bottom-right corner is (200,100).
- isLongPressed: The Stream Deck software does not trigger an event when the screen is released, so, unfortunately, it is not possible to have functions run "while being touched". In the "screen touched" event, it sets a flag indicating whether the screen is "long pressed" (true/false). Elgato's definition of "long pressed" is that you hold it for longer than approximately 200 ms, which, in my opinion, is far from "long". Anyhow, if you are interested in that information, you can use this optional property.
OnSelectedfromlist(index, name)
When an item in a list is selected, the OnSelectedfromlist() function is called.
- index (number) is the index for the selected item. The first item has index 1. If the list is configured to show an exit item, this has an index of 0, so the first "real item always has an index of 1.
- name (string) is the text of the selected item.