Beta version 4.3

OK. If you upload a log file, I can see facts instead of guessing. Adding the keyword WinRTMIDI definitely makes a difference on my PC.

and everything seems broken. in my scripts. Too much things to report.
Should i migrate all ports to the new naming ?

Sorry … I overlooked the new line requirement in keywords.
So with WinRTMIDI, yes ! I get the old port names back and no more unusual response time.

Edit: Maybe the reponse time was caused by many invalid ports tested ?
I don’t know

and another question. Previously i had a statement : global.set(varName, result);
where varname is a string containing a global variable name.
How should i convert that with gvar ?

This what chatgpt says :slight_smile: Why gvar.somevar won’t work dynamically

gvar.somevar // fixed name “somevar”

There is no way to replace somevar dynamically unless the library explicitly supports it (e.g., via a function call or proxy).


What to check

Look at how the library defines gvar:

  • If it’s a function → use gvar(name)
  • If it has methods → use get/set
  • If it explicitly documents gvar.somevar → then it probably expects static names only (no dynamic access)

So :

I guess its what you have done, but sorry it was a bad suggestion, if its the only way to have dynamic variables names.

I tried to explain that in the version notes:

The global object
Breaking change! The “global” object is replaced by “gvar”. Global variables can now be accessed directly by name ‘gvar.MyVar = 10’ instead of ‘gvar.set(“MyVar”, 10)’. The old get/set functionality is still available.

Just replace “global.” with “gvar.” and you’re all set.

1 Like

I read too quickly, sorry.

I also have this script :

[(config){TriggerOnLocalMidiEvents:No}{TriggerOnUnchangedVariables:No}]
[(init){@l_event:0}{@l_disp:0}] Display 0=Pan, 1=Chain, 2=Var

1st press
[(press)(@l_event:0){@l_event:1}{@lt_press:restart}]
[(release)(@l_event:1){@l_event:#@l_event+1#}] 
[(press)(@l_event:2-8){@l_event:#@l_event+1#}]
[(release)(@l_event:2-8){@l_event:#@l_event+1#}] 

Never happens
[(@lt_press:400)(@l_event:0){@l_event:0}{@lt_press:reset}{@l_event:0}]
Long Press : Do nothing
[(@l_disp:1)(@lt_press:400)(@l_event:1){@lt_press:reset}{@l_event:0}]
Long Press : Add variation
[(@l_disp:2)(@lt_press:400)(@l_event:1){@lt_press:reset}{@l_event:0}{cc:16,88,127}{@l_vdupdt:1}]

Single Clic (Chain selector) : Switch view
[(@l_disp:1)(@lt_press:400)(@l_event:2)(@l_chmatch:0){@lt_press:reset}{@l_event:0}{cc:16,50,127}] Set chain position from streamdeck, will trigger sysex & names refresh; Reset first to avoid triggering command with @l_disp=1
[(@l_disp:1)(@lt_press:400)(@l_event:2)(@l_chmatch:1)(@g_dev_rack:0){@lt_press:reset}{@l_event:0}] Do nothing
[(@l_disp:1)(@lt_press:400)(@l_event:2)(@l_chmatch:1)(@g_dev_rack:1){@lt_press:reset}{@l_event:0}{@l_disp:2}] Switch to variation control

/* (@l_disp:1)(@lt_press:400)(@l_event:2)(@l_chmatch:1)(@g_max_var:0-8){@lt_press:reset}{@l_event:0}{@l_disp:#IF(@g_dev_rack=1,2,1)#} Switch to variation control, even if no variation, only if device is a rack */

Single Clic (Variation Control) : Select variation (when changed) or back to parameters view
[(@l_disp:2)(@lt_press:400)(@l_event:2)(@l_vmatch:0)(@l_vnum:1-8){@lt_press:reset}{@l_event:0}{cc:10,#@l_vnum#,127}] Set variation position from streamdeck, will trigger sysex & names refresh
[(@l_disp:2)(@lt_press:400)(@l_event:2)(@l_vmatch:1){@lt_press:reset}{@l_event:0}{@l_disp:1}] Return to chain view

Double clic : Show / Hide Chain devices
[(@l_disp:1)(@lt_press:400)(@l_event:3-9){@lt_press:reset}{@l_event:0}{cc:1,49,127}] Show/Hide chain devices
Double clic : Delete Variation
[(@l_disp:2)(@lt_press:400)(@l_event:3-9)(@g_max_var:1-9){@lt_press:reset}{@l_event:0}{cc:16,89,127}{@l_vdupdt:1}]


[(tap){@l_disp:#SWITCH(@l_disp,0,1,0)#}{minmax:1,#SWITCH(@l_disp,0,127,8)#}]

Set state and design
Pan View
[(init+)(@l_disp:0){display:V-pot}{valuedisplay:Panning L50-R50}{design:Black (Custom)}{layout:#CONCAT(@g_lp, "ts_vpot_PAN.json")#}{iconleft:#none#}{title:"Pan"}{step:v}{design:Black (Custom)}]  
Chain view
[(@l_disp:1){display:Fader}{valuedisplay:Controlled by script}{layout:#CONCAT(@g_lp, "ts_fader_CH.json")#}{iconleft:#none#}{step:f}]
Variation View
[(init+)(@l_disp:2){value:#@l_vnum#}{layout:#CONCAT(@g_lp, "ts_fader_VAR.json")#}{iconleft:"none"}{@l_vdupdt:1}{@lt_vartimer:reset}] Triggers variation design update, Reset timer

Device changed
[(@g_chnum0:-1){@l_chnum:0}]
[(@g_chnum0:-1)(@l_disp:2){@l_disp:1}] switch to Device view
Device changed triggered by g_chnum0 ... or returning to chain view
[(@l_disp:1)(@g_chnum0:0-64){@l_chnum:#@g_chnum0#}{value:#@g_chnum0#}{design:#CONCAT(@g_dp, "CH_Navigator ", IF(AND(@g_dev_rack=0,@g_chnum0=0),"0-0",@g_max_ch), ".xml")#}]
[(@l_disp:1)(@g_dname_sel:*){title:#@g_dname_sel#}] Selected device name received
Variations received (when device or variation changed), l_vnum set to -1 to ensure @l_vmatch is refreshed
[(init+)(@l_disp:1)(@g_vnum0:0-64){@l_vnum:-1}{@l_vnum:#@g_vnum0#} 
             {@l_vartext:#IF(@g_dev_rack=0,SWITCH(@g_dev_type,1,"Instrument",2,"Audio Effect",4,"Midi Effect","Unknown Device Type"),IF(@g_max_var=0,"No variation",CONCAT("Var. ",@g_vnum0," / ",@g_max_var)))#}
			 {text:#@l_vartext#}]


COLORS
Pan vpot
[(@l_disp:0){@l_toptext_full_width.color:Gray}
            {@l_text_on_half_vpot_middle.color:##B8A46E}] Light Yellow
Chain - Idle or Initial position
[(@l_disp:1)(@l_chmatch:1){@l_toptext_full_width.color:##B8A46E} /* Light Yellow */
                          {@l_fadertext_left_side_upper_row.color:Gray}] Gray
Navigating - Other
[(@l_disp:1)(@l_chmatch:0){@l_toptext_left_icon_present.color:Gray}
                          {@l_fadertext_left_side_upper_row.color:##C29C2B}]
Variation
[(@l_disp:2){@l_toptext_full_width.color:Grey}
            {@l_toptext_left_icon_present.color:Gray}
			{@l_fadertext_left_side_upper_row.color:##C29C2B}] Strong Yellow

/* ##5071B8  Blue*/

PAN VIEW
Value
[(init+)(@l_disp:0)(cc:1,25,*){value:#@e_ccvalue#}]
[(@l_disp:0)(rotate:*){cc:1,25,#@e_value#}]


DEVICE CHAIN CONTROL
Rotate and release Unpressed
[(@l_disp:1)(rotate:rn,1-8){@l_chnum:#MIN(@e_value,@g_max_ch)#}{@l_jbr:#IF(@e_value>@g_max_ch,0,1)#}{value:#@l_chnum#}] Order is important 
[(@l_disp:1)(rotate:ln,1-8)(@g_max_ch:1-8){@l_chnum:#@e_value#}{@l_jbr:1}] Do not set if no chain
[(@l_jbr:1){cc:16,51,#@l_chnum#}{@lt_chtimer:restart}{@l_jbr:0}] Browse to current value, Start activity timer
Manage chain position and status
[(@l_chnum:1-64){@l_chmatch:#IF(@g_chnum0=@l_chnum,1,0)#}] 
/* */

Set base and browsing appearance
[(@l_chmatch:1)
			{iconleft:#none#}
			{text:#@l_vartext#}]
[(@l_chmatch:0)
			{iconleft:#CONCAT(@g_pp,"Hand-Blue1.png")#}]
[(@l_chmatch:0)(@g_dname_nav:*)
			{text:#@g_dname_nav#}] Navigated device name
Restore to initial position  and clear browsing variables in script after 7 sec. of inactivity
[(@lt_chtimer:7000){cc:16,50,0}{@lt_chtimer:reset}{@l_chnum:#@g_chnum0#}{value:#@g_chnum0#}] Reset browsing and timer, value and position


VARIATION CONTROL
Rotate Unpressed
[(@l_disp:2)(rotate:rn,1-8){@l_vnum:#MIN(@e_value,@g_max_var)#}{value:#@l_vnum#}] 
[(@l_disp:2)(rotate:ln,0-8){@l_vnum:#IF(@g_max_var=0,0,MAX(@e_value,1))#}{value:#@l_vnum#}] Neutralize 0 (reserved fo no variation)

Update Design
[(@l_disp:2)(@l_vnum:>1>){@l_vdupdt:1}] Set non transparent handle
[(@l_disp:2)(@l_vnum:<0<){@l_vdupdt:1}] Set transparent handle
[(@l_disp:2)(@l_vnum:>0>){@l_vdupdt:1}] Set transparent handle (when previously -1, received from DAW), may be redundant with >1> above, but needed after deletion and 0 variations left
[(@l_vdupdt:1){design:#CONCAT(@g_dp, "VAR_Navigator ", @g_max_var, IF(@l_vnum=0,"-0",""), ".xml")#}{@l_vdupdt:0}]


Manage variation position and status, restart timer
[(@l_disp:2)(@l_vnum:0-64){value:#@l_vnum#}{@l_vmatch:#IF(@g_vnum0=@l_vnum,1,0)#}{cc:10,#@l_vnum#,0}{@lt_vartimer:restart}
                       {text:#IF(@g_max_var=0,"No variation",CONCAT("Var. ",@l_vnum," / ",@g_max_var))#}]
[(@l_vmatch:1){iconleft:#none#}]
[(@l_vmatch:0){iconleft:#CONCAT(@g_pp,"Hand-Orange1.png")#}]



Return to current variation if unpressed, update design needed when no variation intially selected but a variation wwas browsed
[(@lt_vartimer:7000){@l_vnum:#@g_vnum0#}{@lt_vartimer:reset}] 



Swith to DAW device view
[(rotate:*){@g_dc:1}{cc:16,111,127}{@g_dc:0}] Show Device

/// {@l_chnum:#MIN(@e_value,@g_max_ch)#}
//// {text:"Var. ..."}
///// {sysex:F0 6B 4F #IF(@l_vnum=@g_vnum0,1,0)# 4F #@g_vnum0# #@l_vnum# #@g_max_var# F7}

who was working before.
Now the editor says : Error while parsing script.

Log Id: c3abf842-6b0b-4ee3-a16d-85542a1ea508

Thanks, I have uploaded a fix:

Version 4.2.0.55

For a simple script :

[(press){cc:1,0,127}]
[(release){cc:1,0,0}]
[(init+)(@g_t_img_idx:*){image:#CONCAT(@g_pp, "TrackStatus-", @g_t_img_idx, ".png")#}]

I made a small comparison between my manually converted script :

function OnInit() {ui.text("Mute\n\n\n\n\nSolo / Arm");RefreshImage();}
function OnGlobalVariableChanged(name, value) {if (name == "g_pp" || name == "g_t_img_idx") RefreshImage();}
function RefreshImage(value) {ui.image(gvar.g_pp + "TrackStatus-" + gvar.g_t_img_idx + ".png");}
// Press/Release
function OnKeyPressed() {midi.sendCC(1,0,127);}
function OnKeyReleased() {midi.sendCC(1,0,0);}

and the automatically generated script :open_mouth:

// Auto-generated migration from midiScript -> JavaScript (Version 1.0)
// Generated: 2026-03-31 05:58:44 UTC

function __ms_toExpr(value) {
  if (value === null || value === undefined) return "null";
  if (typeof value === "boolean") return value ? "true" : "false";
  if (typeof value === "number") return Number.isFinite(value) ? value.toString() : "0";
  var s = String(value);
  s = s.replace(/\\/g, "\\\\");
  s = s.replace(/\"/g, "\\\"");
  s = s.replace(/\r/g, "\\r");
  s = s.replace(/\n/g, "\\n");
  return "\"" + s + "\"";
}

function __ms_eval(expr, ctx) {
  expr = String(expr ?? "");
  expr = expr.replace(/@([A-Za-z0-9_\-]+)/g, function (_, name) {
    if (name && (name.indexOf("l_") === 0 || name.indexOf("t_") === 0 || name.indexOf("lt_") === 0 || name.indexOf("local_") === 0)) {
      if (ctx && Object.prototype.hasOwnProperty.call(ctx, name)) return __ms_toExpr(ctx[name]);
      return "null";
    }
    return __ms_toExpr(gvar[name]);
  });

  if (ctx) {
    var repl = function (key, val) {
      var re = new RegExp("\\\\b" + key + "\\\\b", "gi");
      expr = expr.replace(re, function () { return __ms_toExpr(val); });
    };
    if (Object.prototype.hasOwnProperty.call(ctx, "value")) repl("value", ctx.value);
    if (Object.prototype.hasOwnProperty.call(ctx, "state")) repl("state", ctx.state);
    if (Object.prototype.hasOwnProperty.call(ctx, "channel")) repl("channel", ctx.channel);
    if (Object.prototype.hasOwnProperty.call(ctx, "control")) repl("control", ctx.control);
    if (Object.prototype.hasOwnProperty.call(ctx, "key")) repl("key", ctx.key);
    if (Object.prototype.hasOwnProperty.call(ctx, "velocity")) repl("velocity", ctx.velocity);
    if (Object.prototype.hasOwnProperty.call(ctx, "program")) repl("program", ctx.program);
    if (Object.prototype.hasOwnProperty.call(ctx, "pressure")) repl("pressure", ctx.pressure);
    if (Object.prototype.hasOwnProperty.call(ctx, "ticks")) repl("ticks", ctx.ticks);
    if (Object.prototype.hasOwnProperty.call(ctx, "x")) repl("x", ctx.x);
    if (Object.prototype.hasOwnProperty.call(ctx, "y")) repl("y", ctx.y);
    if (Object.prototype.hasOwnProperty.call(ctx, "time")) repl("time", ctx.time);
    if (Object.prototype.hasOwnProperty.call(ctx, "name")) repl("name", ctx.name);
    if (Object.prototype.hasOwnProperty.call(ctx, "isGlobal")) repl("isGlobal", ctx.isGlobal);
    if (Object.prototype.hasOwnProperty.call(ctx, "part1")) repl("part1", ctx.part1);
    if (Object.prototype.hasOwnProperty.call(ctx, "part2")) repl("part2", ctx.part2);
    if (Object.prototype.hasOwnProperty.call(ctx, "part3")) repl("part3", ctx.part3);
  }

  return func.eval(expr);
}

function OnGlobalVariableChanged(name, value) {
  if (name === "g_t_img_idx") {
    ui.image(__ms_eval("CONCAT(@g_pp, \"TrackStatus-\", @g_t_img_idx, \".png\")", { name: name, value: value }));
  }
}

function OnInit() {
  ui.image(__ms_eval("CONCAT(@g_pp, \"TrackStatus-\", @g_t_img_idx, \".png\")", null));
}

function OnKeyPressed(state) {
  var __idx = gvar.__rr_OnKeyPressed_idx;
  if (__idx == null) __idx = 0;
  switch (__idx) {
    case 0:
      midi.sendCC(0, 0, 127);
      gvar.__last_press_slot = 0;
      break;
    default:
      break;
  }
  __idx = (__idx + 1) % 1;
  gvar.__rr_OnKeyPressed_idx = __idx;
}

function OnKeyReleased(state) {
  var __slot = gvar.__last_press_slot;
  if (__slot == null) return;
  switch (__slot) {
    case 0:
      midi.sendCC(0, 0, 0);
      gvar.__last_press_slot = null;
      break;
    default:
      break;
  }
}


In which I am not sure i understand everything. A quick test revealed that the automatically converted version does not send the cc as expected.

Also i converted all my scripts to use global ports, so i could make another test without the WinRTMIDI option.
Response time have improved but it is still much slower than with the WinRTMIDI option.
Is it “normal” ? I’m curious to know if others are experiencing a performance penalty when not using WinRTMIDI.

For the record I have also posted a request on Bome Forum: here

I uploaded a log file with the slower interaction, but i am not sure it reveals anything : 79dafa4c-0696-44f3-8447-7ec78f70a0b4

And another issue …
i’m trying to convert the sysex processing with the following code

/// <reference path="C:/Users/LPA/AppData/Roaming/Elgato/StreamDeck/Plugins/se.trevligaspel.midi.sdPlugin/ScriptJint/streamdeck-midi.d.ts" />
/*
Selected Track : Name
[(init){@g_tname_sel:"_Track_"}]
Reinit and accumulate, Fire global on last one
[(sysex:F0 6A 00 XX F7){@l_tname_sel:#IF(HEXVALUE(MID(@e_sysex,1,1))=1, RIGHT(@e_sysextext, 8), CONCAT(@l_tname_sel, RIGHT(@e_sysextext, 8)))#}
					   {@l_tnupdt:#IF(HEXVALUE(MID(@e_sysex,1,1))=HEXVALUE(MID(@e_sysex,2,1)),1,0)#}]
[(@l_tnupdt:1){@g_tname_sel:#SUBSTITUTE(TRIM(@l_tname_sel),CHAR(39),CHAR(34))#}{@l_tnupdt:0}]
*/

// Buffer for parallel chunk accumulation
const buffers = {};

// Human-readable mapping
const sysExList = [
    [106, 0, "g_tname_sel2"],
    [108, 0, "z_Scene"],
    [107, 1, "z_mTrack1"],
    [107, 2, "z_mTrack2"],
    [107, 3, "z_mTrack3"],
    [107, 4, "z_mTrack4"],
    [113, 0, "z_Clip"],
    [110, 0, "z_Device"],
    [111, 0, "z_NavDevice"]
];

// Fast lookup map
const sysExMap = Object.fromEntries(
    sysExList.map(([p, i, n]) => [`${p}_${i}`, n])
);

// Timeout settings (ms)
const BUFFER_TIMEOUT = 2000;

/**
 * Stream Deck SysEx handler with integrated timeout cleanup
 */
function OnSysExReceived(sysexdata) {
    const data = Array.from(sysexdata),
          key = data[0] + "_" + data[1],
          varName = sysExMap[key];

    if (!varName || data.length < 3) return;

    const control = data[2],
          subindex = control >> 4,
          parts = control & 0x0F;

    // Initialize or refresh buffer
    if (!buffers[key] || subindex === 1) {
        buffers[key] = {
            chunks: {},
            expected: parts,
            ts: Date.now()
        };
    }

    buffers[key].ts = Date.now();

    // Decode ASCII chunk
    let text = "";
    for (let i = 3; i < data.length; i++) {
        text += String.fromCharCode(data[i]);
    }

    buffers[key].chunks[subindex] = text;

    // Remove stale buffers while processing
    const now = Date.now();
    for (const k in buffers) {
        if (now - buffers[k].ts > BUFFER_TIMEOUT) {
            console.warn("Timeout clearing buffer:", k);
            delete buffers[k];
        }
    }

    // Reconstruct text if all chunks received
    const buf = buffers[key];

    if (Object.keys(buf.chunks).length === buf.expected) {
        let result = "";

        for (let i = 1; i <= buf.expected; i++) {
            result += buf.chunks[i] || "";
        }

        delete buffers[key];

        result = result.trim().replace(/'/g, '"');

        gvar.set(varName, result);
        console.warn("Js Bnames:", varName, "=", result);
    }
}

which works well.

but now if i comment the original scripts lines for F0 6A like this :

Selected Track : Name
[(init){@g_tname_sel:"_Track_"}]
Reinit and accumulate, Fire global on last one
/*
[(sysex:F0 6A 00 XX F7){@l_tname_sel:#IF(HEXVALUE(MID(@e_sysex,1,1))=1, RIGHT(@e_sysextext, 8), CONCAT(@l_tname_sel, RIGHT(@e_sysextext, 8)))#}
					   {@l_tnupdt:#IF(HEXVALUE(MID(@e_sysex,1,1))=HEXVALUE(MID(@e_sysex,2,1)),1,0)#}]
[(@l_tnupdt:1){@g_tname_sel:#SUBSTITUTE(TRIM(@l_tname_sel),CHAR(39),CHAR(34))#}{@l_tnupdt:0}]
*/

it seems that F0 6A is not processed anymore by the javascript.

Here are the log files :

  • with F0 6A processed, both in the original script and the js (as exhibited with this output Js Bnames: g_tname_sel2 =) : 07842954-9cf7-44f1-a581-90b37364ffb3
  • after commenting the F0 6A processing in the original script, and where F0 6A is no more processed in javascript (no trace of Js Bnames: g_tname_sel2=):
    ead5ecdd-b7d3-4ad9-9225-09c918ec290c

Edit: Both are background scripts, and maybe I missed something. Thanks for your help.

You’re right, the migration is trying to do too much just to get a working script. It ends up being more confusing than helpful. I removed all special handling of #…# expressions and all attempts to manage multiple press commands. I also saw that the MIDI channel was wrong in the migrated script. The result for your script with the new migration function looks like this, which I think is much better.

// Auto-generated migration from midiScript -> JavaScript (Migrate2, Version 1.1)
// Generated: 2026-03-31 06:41:32 UTC

function OnGlobalVariableChanged(name, value) {
  if (name === "g_t_img_idx") {
    ui.image("#CONCAT(@g_pp, \"TrackStatus-\", @g_t_img_idx, \".png\")#");
  }
}

function OnInit() {
  ui.image("#CONCAT(@g_pp, \"TrackStatus-\", @g_t_img_idx, \".png\")#");
}

function OnKeyPressed(state) {
  midi.sendCC(1, 0, 127);
}

function OnKeyReleased(state) {
  midi.sendCC(1, 0, 0);
}

That is correct. When a midiScript is parsed, the result is stored as an internal data structure that represents commands, events, and actions. During script parsing, commented sections of the script are ignored and are not included in this internal data structure.

The migration function operates based on this internal data structure, not from the script text, so any parts of the script that are commented out will not be included in the migration.

Thanks, but the sysex problem has nothing to do with the migration.
It happens that when F0 6A is excluded (commented) from the midiscript, it does not reach the other (javascript) script that i wrote myself.

Much better indeed, I will try it when it’s published.

Sorry, I misunderstood the issue. I’ll take a look at it.

What does “response time” mean in your statement? Is it in response to a received message, or the time to send a message?

I found an unnecessary delay in receiving messages when not using WinRTMIDI. I believe that’s what you meant. Thanks.

1 Like

This version addresses the issues you reported (huge thanks; this help is invaluable).

  • The migration function now produces much cleaner scripts, but they need adjustments to run properly.
  • The delays when receiving MIDI messages when using the MIDI Service should be removed.
  • The sysex problem was a timing and queuing issue where a random sysex message could be dropped if another sysex message arrived before it was processed. This should be fixed in this version.

Version 4.2.0.56

Hello,
I can confirm the fix for the SYSEX problem, as well as for delays without the WINRTMIDI option. Thank you.

On the occasion, I was wondering if you could have a look at the possibility of processing events in exported modules ?
If you look at my code here ; Managing single, double and long press on a button with javascript, this line in the calling script :
case bName_pt: handleTimerElapsed(tname, isGlobal, time); break; is unnecessary and we should have the capability to use OnTimerElapsedin the exported PressHandler.js. Any idea ?