Sclang crash when handling sysex

Hi all, returning to the MIDIOut.sysex question, from the macOS side. I am facing the issue that I would like to send or receive a 12 byte Sysex MIDI Tuning Standard message immediately before a Note On to retune the note before it is played. I have implemented incoming and outgoing sysex functions that convert between the MTS data (midi note and two bytes of upward deviation measured as a fraction of 1 semitone) and frequency/MIDIfloat information. The problem I get is a cross-platform issue: on Windows, incoming MTS crashes the Interpreter as described in the posts above. On macOS it works perfectly. The reverse situation, generating MTS data and sending it out from Supercollider to e.g. Pianoteq, exhibits the opposite issue: it works perfectly on Windows, but on macOS the sysex message does not always arrive BEFORE the following noteOn. Since the two OS’s have different MIDI code, this is somehow not surprising, but I am wondering how to fix it. Any thoughts?

here are the mts functions:

~mts_in = { |src, val|
		if ([val[0], val[1], val[2], val[3], val[4]] == [-16, 127, 127, 8, 2]) {
			if (val.size == 12) {
				var pitch;
				pitch = (val[8] + (val[9]/128.0) + (val[10]/16384.0));
				~tuningMap[val[5]][val[7]] = pitch;
				~tuningMap[127][val[7]] = pitch;
				"MTS received: ".post; "Map ".post; val[5].post;
				" Note ".post; val[7].post; " => ".post; pitch.postln;
			};
		};
	};

~output_map = 0;
	
	~mts_out = { |index, midifloat|
		var note_number, tuning, msb, lsb, message;
		note_number = midifloat.floor;
		tuning = 128 * (midifloat - note_number);
		msb = tuning.floor;
		lsb = (128 * (tuning - msb)).round;
		if (lsb == 128) { lsb = 127 };
		
		"MTS sent: ".post; "Map ".post; ~output_map.post;" Note ".post; index.post; " => ".post; midifloat.postln;
		
		Int8Array [ -16, 127, 127, 8, 2, ~output_map, 1, index, note_number, msb, lsb, -9 ];
	};
// the Int8Array is then sent inside a function triggered by a note on:

~mts_index_array = Array.fill2D(16, 128, {-1} ); // incoming notes on the 16 channels are tracked
~mts_index = 0; // each new note is assigned an index with which the MTS data is associated
~mts_channel = 1; // outgoing MIDI channel for the notes

// inside a MIDIFunc.noteOn the following happens; it should send MTS followed by a noteOn:

~midiout.sysex( ~mts_out.value( ~mts_index, ~tuningMap[~current_map][num] ) );
"Sysex sent!".postln;
			
if (~mts_index_array[chan][num] >= 0) {
				~midiout.noteOff(~mts_channel, ~mts_index_array[chan][num], on_vel); // if a note is already playing in that slot, kill it
				if (~post_note_output) { "extra noteoff generated!".postln; }
			};
			
~midiout.noteOn(~mts_channel, ~mts_index, on_vel);
			
~mts_index_array[chan][num] = ~mts_index;

~mts_index = ( ~mts_index + 1 ) % 128;

I am wondering if there is any way to be sure the outgoing sysex message is sent BEFORE the noteOn? I have my blockSize and hardwareBuffer down to 16 samples, but still the problem persists. Thanks for any help on this :frowning:

I have a similar issue (Supercollider 3.13), what we seem to have in common is Windows, 32 GB RAM, ASIO Fireface USB. I am sending in MIDI real time tuning and then playing from a Lumatone keyboard and generating audio using 4 mixed SynthDefs. Behavior similar: sometimes no crash, sometimes after 1-2 minutes, sometimes after 20 minutes. “Interpreter has crashed or stopped forcefully. [Exit code: -1073741819]”. As far as I can judge the code I’ve written seems solid. Anyone else on Windows out there experiencing similar issues?

Hey 000masa000 :slight_smile:

It would be great if you could attach a debugger to it and see where it fails exactly. Or if you have additional information (e.g. a crash report from the OS), please do not hesitate to post it here or on GitHub.

Windows has a dedicated MIDI implementation, maybe it contains a bug b/c I am not aware of any problems on Linux or macOS regarding MIDI usage.

I have a suspicion it could be a corrupted stack pointer(?) of sclang due to a missing mutex as it only seems to affect Windows and it happens occasionally/non-deterministic. Think of 2 people speaking to a single person at the same time and now the words of both people get mixed up which can result in wrong results or a crash - a mutex makes sure that only one person/process is accessing the resources at a time. At least I get a similar error working on a different part of the language due to a corrupted stack pointer b/c of a missing mutex.

There are sections in the Windows MIDI implementation which are guarded by a mutex

but also parts where it is not invoked, e.g.

I don’t have a Windows machine so sadly I can’t assist or check further, maybe someone who has a Windows machine can have a look at this?

Hello, thank you for the useful reply, I would very much like to see this fixed! I just tested my code on an old Mac M1 machine and there was no interpreter crash even with tons of MIDI data flowing in. Would like not to have to switch back to an Apple machine just to sidestep this! Will study your example and see what I can learn… more soon!

Here is the latest crash dump analysis:


************* Preparing the environment for Debugger Extensions Gallery repositories **************
   ExtensionRepository : Implicit
   UseExperimentalFeatureForNugetShare : true
   AllowNugetExeUpdate : true
   NonInteractiveNuget : true
   AllowNugetMSCredentialProviderInstall : true
   AllowParallelInitializationOfLocalRepositories : true
   EnableRedirectToChakraJsProvider : false

   -- Configuring repositories
      ----> Repository : LocalInstalled, Enabled: true
      ----> Repository : UserExtensions, Enabled: true

>>>>>>>>>>>>> Preparing the environment for Debugger Extensions Gallery repositories completed, duration 0.016 seconds

************* Waiting for Debugger Extensions Gallery to Initialize **************

>>>>>>>>>>>>> Waiting for Debugger Extensions Gallery to Initialize completed, duration 0.343 seconds
   ----> Repository : UserExtensions, Enabled: true, Packages count: 0
   ----> Repository : LocalInstalled, Enabled: true, Packages count: 42

Microsoft (R) Windows Debugger Version 10.0.27725.1000 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.


Loading Dump File [C:\Users\admin\AppData\Local\CrashDumps\sclang.exe.33768.dmp]
User Mini Dump File with Full Memory: Only application data is available


************* Path validation summary **************
Response                         Time (ms)     Location
Deferred                                       srv*
Symbol search path is: srv*
Executable search path is: 
Windows 10 Version 22635 MP (12 procs) Free x64
Product: WinNt, suite: SingleUserTS
Edition build lab: 22621.1.amd64fre.ni_release.220506-1250
Debug session time: Tue Dec 24 23:56:42.000 2024 (UTC + 1:00)
System Uptime: 2 days 7:05:54.836
Process Uptime: 0 days 1:33:46.000
................................................................
................................................................
................................................................
..
Loading unloaded module list
...............
This dump file has an exception of interest stored in it.
The stored exception information can be accessed via .ecxr
(83e8.4e0): Access violation - code c0000005 (first/second chance not available)
For analysis of this file, run !analyze -v
ntdll!NtWaitForMultipleObjects+0x14:
00007ffc`46690f14 c3              ret
0:027> !analyze -v
*******************************************************************************
*                                                                             *
*                        Exception Analysis                                   *
*                                                                             *
*******************************************************************************


KEY_VALUES_STRING: 1

    Key  : AV.Dereference
    Value: NullClassPtr

    Key  : AV.Fault
    Value: Read

    Key  : Analysis.CPU.mSec
    Value: 718

    Key  : Analysis.Elapsed.mSec
    Value: 11575

    Key  : Analysis.IO.Other.Mb
    Value: 22

    Key  : Analysis.IO.Read.Mb
    Value: 1

    Key  : Analysis.IO.Write.Mb
    Value: 46

    Key  : Analysis.Init.CPU.mSec
    Value: 234

    Key  : Analysis.Init.Elapsed.mSec
    Value: 63762

    Key  : Analysis.Memory.CommitPeak.Mb
    Value: 158

    Key  : Analysis.Version.DbgEng
    Value: 10.0.27725.1000

    Key  : Analysis.Version.Description
    Value: 10.2408.27.01 amd64fre

    Key  : Analysis.Version.Ext
    Value: 1.2408.27.1

    Key  : Failure.Bucket
    Value: NULL_CLASS_PTR_READ_c0000005_sclang.exe!Unknown

    Key  : Failure.Hash
    Value: {6b3d47f4-07da-8485-1288-5fa9b013debb}

    Key  : Timeline.OS.Boot.DeltaSec
    Value: 198354

    Key  : Timeline.Process.Start.DeltaSec
    Value: 5626

    Key  : WER.OS.Branch
    Value: ni_release

    Key  : WER.OS.Version
    Value: 10.0.22621.1


FILE_IN_CAB:  sclang.exe.33768.dmp

NTGLOBALFLAG:  0

APPLICATION_VERIFIER_FLAGS:  0

CONTEXT:  (.ecxr)
rax=0000000000000000 rbx=00007ff704c18730 rcx=00007ff704c18730
rdx=000001fb0a5509d8 rsi=000001fb0a798c38 rdi=0000000000000003
rip=00007ff7049ab966 rsp=000000b5b34ffca0 rbp=0000000000000000
 r8=0000000000000003  r9=0000000000000003 r10=0000000000000001
r11=0000000000000000 r12=000000007ffe000c r13=000000007ffe0008
r14=00007ff704940000 r15=00007ff704940000
iopl=0         nv up ei ng nz ac po cy
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010297
sclang!get_device_handle+0x549f6:
00007ff7`049ab966 418b8300010000  mov     eax,dword ptr [r11+100h] ds:00000000`00000100=????????
Resetting default scope

EXCEPTION_RECORD:  (.exr -1)
ExceptionAddress: 00007ff7049ab966 (sclang!get_device_handle+0x00000000000549f6)
   ExceptionCode: c0000005 (Access violation)
  ExceptionFlags: 00000000
NumberParameters: 2
   Parameter[0]: 0000000000000000
   Parameter[1]: 0000000000000100
Attempt to read from address 0000000000000100

PROCESS_NAME:  sclang.exe

READ_ADDRESS:  0000000000000100 

ERROR_CODE: (NTSTATUS) 0xc0000005 - The instruction at 0x%p referenced memory at 0x%p. The memory could not be %s.

EXCEPTION_CODE_STR:  c0000005

EXCEPTION_PARAMETER1:  0000000000000000

EXCEPTION_PARAMETER2:  0000000000000100

STACK_TEXT:  
000000b5`b34ffca0 00007ff7`0499ac9a     : 00007ff7`04940000 00007ffc`249f3107 00007ff7`04c18730 000001fb`0a5509d8 : sclang!get_device_handle+0x549f6
000000b5`b34ffcd0 00007ff7`04a38a0b     : 00000000`000000f7 00000000`00000003 00000000`00000003 00000000`00000000 : sclang!get_device_handle+0x43d2a
000000b5`b34ffd00 00007ff7`04a37c99     : 000001fb`f7000045 00007ff7`04c18730 00007ff7`04940000 00000000`00000006 : sclang!get_device_handle+0xe1a9b
000000b5`b34ffd40 00007ffc`3a70231b     : 00007ffc`3a727f20 00000000`00000000 004f3b65`f7000045 00007ff7`04b2abc0 : sclang!get_device_handle+0xe0d29
000000b5`b34ffd80 00007ffc`4572259d     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : winmm!timeThread+0x15b
000000b5`b34fff00 00007ffc`4664af38     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : kernel32!BaseThreadInitThunk+0x1d
000000b5`b34fff30 00000000`00000000     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x28


SYMBOL_NAME:  sclang+549f6

MODULE_NAME: sclang

IMAGE_NAME:  sclang.exe

STACK_COMMAND:  ~27s; .ecxr ; kb

FAILURE_BUCKET_ID:  NULL_CLASS_PTR_READ_c0000005_sclang.exe!Unknown

OS_VERSION:  10.0.22621.1

BUILDLAB_STR:  ni_release

OSPLATFORM_TYPE:  x64

OSNAME:  Windows 10

FAILURE_ID_HASH:  {6b3d47f4-07da-8485-1288-5fa9b013debb}

Followup:     MachineOwner
---------

It seems your idea may be correct… any idea from the crash dump where a fix might need to be implemented? cheers, Msrc

Hm. A quick search of the sources shows that get_device_handle() is part of the hidapi library:

./external_libraries/hidapi/hidapi/hidapi.h\0434:        HANDLE HID_API_EXPORT HID_API_CALL get_device_handle(hid_device *dev);

(… and several other references, all in the same folder).

It would be good to fill in the missing mutexes for sure, but the issue here might be something else.

Actually…

The function midiProcessPartialSystemPacket() is only called from within a mutex, so I don’t think it’s necessary to double-mutex.

hjh

1 Like

would it be helpful to see the synthdefs and MIDI code I made?

( // main patch
s.waitForBoot {
	s.plotTree;
	s.meter;

	if (MIDIClient.initialized.not) {
		MIDIClient.init;
		postln("MIDI has been initialised!");
	} {
		postln("MIDI already running!")
	};

	~lumatone = MIDIIn.findPort("from-Lumatone", "from-Lumatone");
	//MIDIIn.connect(0, ~lumatone);
	//MIDIIn.disconnect(0, ~lumatone);

	//~keyboardIn = MIDIIn.findPort("from-Keyboard", "from-Keyboard");
	//MIDIIn.connect(0, ~keyboardIn);
	//MIDIIn.disconnect(0, ~keyboardIn);

	~loopMIDI_Port1 = MIDIIn.findPort("loopMIDI_Port1", "loopMIDI_Port1");
	//MIDIIn.connect(0, ~loopMIDI_Port1);
	//MIDIIn.disconnect(0, ~loopMIDI_Port1);

	~loopMIDI_Port2 = MIDIIn.findPort("loopMIDI_Port2", "loopMIDI_Port2");
	MIDIIn.connect(0, ~loopMIDI_Port2);
	//MIDIIn.disconnect(0, ~loopMIDI_Port2);

	~linnstrument = MIDIIn.findPort("from-Linnstrument", "from-Linnstrument");
	MIDIIn.connect(0, ~linnstrument);
	//MIDIIn.disconnect(0, ~linnstrument);


	// Synth Definitions

	////////////////
	//	         //
	//  SYNTHS  //
	//         //
	////////////


	SynthDef(\pluck, {|freq,bend=1,on_vel=64,off_vel=64,mod=1,gate=0,vol=0|

		var e,f,o,amp;

		gate = gate + Impulse.kr(0);
		amp = on_vel.linexp(0,127,0.08,1);
		off_vel = off_vel.linlin(0,127,0.001,1);
		e = EnvGen.ar(Env(levels:[1,1,0.85,0],times:[1,3,5],curve:'sin'),doneAction:Done.freeSelf);
		f = EnvGen.ar(Env.asr(attackTime:0.00001, sustainLevel:1.0, releaseTime:(0.005 + (off_vel * 3)), curve:'sin'),gate,doneAction:Done.freeSelf);
		o = WhiteNoise.ar(1);
		o = Pluck.ar(
			in: o,
			trig: Impulse.ar(0),
			maxdelaytime: 8/freq,
			delaytime: 1/((freq*bend).lag(0.3)),
			decaytime: on_vel.linlin(0,127,3.5,8),
			coef: mod.explin(1,2,0.65,-0.038) * on_vel.linlin(0,127,1,0.3));
		o = BHiShelf.ar(o,2000 * ((freq / 330.0) ** 0.65),0.85,-16);
		o = DFM1.ar(o,freq*19*((330.0 / freq )**0.35), 0.1, 1.0, 0.0, 0.0003, 0.6);
		o = LeakDC.ar(o);
		o = o * 0.5 * amp * e * f * Lag2.kr(vol * mod.linexp(1,2,1.075,0.75), 0.3);
		Out.ar(0,o!2);
	}).add.postln;


	SynthDef(\string, {|freq,bend=1,on_vel=64,off_vel=64,mod=1,gate=0,vol=0|

		var e,f,o,amp;

		gate = gate + Impulse.kr(0);
		amp = on_vel.linexp(0,127,0.2,1);
		off_vel = off_vel.linlin(0,127,1,0.2);
		e = EnvGen.ar(Env([0,1,1,0],[(on_vel.linexp(0,127,2.5,1)),[1,1.25,1.5,1.75,2].choose,[1,1.5,2,2.5].choose * off_vel],'sin'),doneAction:Done.freeSelf);
		f = EnvGen.ar(Env.asr(attackTime:0.001, sustainLevel:1.0, releaseTime:(0.5 + (off_vel * 3)),curve:'sin'),gate,doneAction:Done.freeSelf);
		o = WhiteNoise.ar(1);
		o = Pluck.ar(
			in: o,
			trig: Impulse.ar(0),
			maxdelaytime: 8/freq,
			delaytime: 1/((freq*bend).lag(0.3)),
			decaytime: 1 + (on_vel.linlin(0,127,20,1) * 19),
			coef: freq.explin(110,3300,0.08 * Lag2.kr(mod.linexp(1,2,3.5,0.08),0.03),0.004)
		) * e;
		o = BHiShelf.ar(o,2000,2,-16);
		o = BHiPass.ar(o,freq*0.9);
		o = LeakDC.ar(o);
		o = o * Lag2.kr(amp * vol * on_vel.linexp(0,127,0.15, 1.0) * freq.explin(88,1100, 0.1, 1) * mod.linexp(1,2,1.0,0.2), 0.2) * e * f;
		Out.ar(0,o!2);
	}).add.postln;


	SynthDef(\formant, {
		|freq, bend=1,
		formant_freq = #[1,1,1,1,1],
		formant_reso = #[1,1,1,1,1],
		formant_amp = #[1,1,1,1,1],
		on_vel=64,off_vel=64,mod=1,gate=0,vol=0|

		var e,f,o,amp;

		gate = gate + Impulse.kr(0);
		amp = on_vel.linlin(0,127,0,1);
		off_vel = off_vel.linlin(0,127,2,1);
		e = EnvGen.ar(Env([0,1,0],[[1,2,3,4].choose,0.4 * [3,4,5,6,7].choose],curve: 'sin'),gate,doneAction:Done.freeSelf);
		f = EnvGen.ar(Env.asr(attackTime:0.001, sustainLevel:1.0, releaseTime:(0.02 + off_vel),curve:'sin'),gate,doneAction:Done.freeSelf);
		o = Saw.ar((freq*bend).lag(0.3));
		o = Mix.new(BBandPass.ar(o, formant_freq, formant_reso, formant_amp));
		o = BLowPass.ar(o,Lag2.kr(800 * mod.expexp(1,2,1,2), 0.5));
		o = BPeakEQ.ar(o,freq,1,-16);
		o = HPF.ar(o,freq*0.85);
		o = LeakDC.ar(o) * AmpComp.kr(440*440/freq);
		o = o * e * f * 2 * Lag2.kr(amp * vol, 0.4);
		Out.ar(0,o!2);
	}).add.postln;


	SynthDef(\tone, {|freq,bend=1,on_vel=64,off_vel=64,gate=1,mod=1,filter=1,vol=0|

		var e,o;
		var attack_time = on_vel.bilin(70,0,127,0.001,0.005,0);
		var rise_time = on_vel.bilin(70,0,127,0.02,0.05,0.005);
		var release_time = off_vel.bilin(70,0,127,0.02,0.3,0.005);
		var sustain_level = on_vel.bilin(70,0,127,1.65,1.25,2);

		gate = gate + Impulse.kr(0);
		e = EnvGen.ar(Env.adsr(attackTime:attack_time,decayTime:rise_time,sustainLevel:sustain_level,releaseTime:release_time,peakLevel:0.5,curve:0),gate,doneAction:Done.freeSelf);
		o = DFM1.ar(Saw.ar(freq*bend.lag(0.3)),freq*2*mod.linexp(1,2,1,2.4).lag(0.75)*filter.linexp(1,2,1,5).lag2(0.4),0.3);
		o = LPF.ar(o,freq * 2 * ((330.0 / freq )**0.9));
		o = BPeakEQ.ar(o,freq,1.2,-26);
		o = BPeakEQ.ar(o,160,1.5,-4);
		o = BPeakEQ.ar(o,800,1,-2);
		o = HPF.ar(o,110);
		o = o * e * Lag2.kr(vol, 0.2);
		o = o * mod.expexp(1,2,1.4,1.1).lag(0.35) * filter.explin(1,2,1,2).lag2(0.04);
		Out.ar(0,o!2);
	}).add.postln;

	postln("Synths loaded !");


	// load MTS single-note retuning and MIDI IO for Lumatone

	///////////////
	//	        //
	//   MTS   //
	//        //
	///////////

	~tuningMap = Array.newClear(128);
	(0..127).do {|j| ~tuningMap[j] = Array.fill(128, {|i| i }) };
	// array has one slot per possible MIDI note and stores the actual retuned pitches to be triggered by noteon messages as MIDIfloats

	~mts = MIDIFunc.sysex(
		{|val| if ([val[0], val[1], val[2], val[3], val[4]] == [-16,127,127,8,2]) {
			if (val.size > 10) {
				"MIDI tuning received: ".post; "Tuning Map ".post; val[5].postln;

				"note ".post; val[7].post; " => ".post;
				~tuningMap[val[5]][val[7]] = (val[8] + (val[9]/128.0) + (val[10]/16384.0)).postln;

				//(0..127).do { |note| // duplicate incoming MTS to tuning map 127, which is where the morphing function is calculated
				~tuningMap[127][val[7]] = ~tuningMap[val[5]][val[7]];
			};
			//};
		};
		};
	); // convert real-time MTS

	//~mts.free;

	// 53-tone isomorphic odd-partial set (default)
	~tuningMap[0] = [ 46.3857421875, 46.533081054688, 46.824035644531, 46.967712402344, 47.391723632812, 47.668701171875, 47.760070800781, 47.941345214844, 48.209777832031, 48.474060058594, 48.604736328125, 48.863159179688, 48.990905761719, 49.24365234375, 49.492736816406, 49.859802246094, 49.98046875, 50.33740234375, 50.610046386719, 50.687194824219, 50.91650390625, 51.142761230469, 51.366149902344, 51.531860351562, 51.804504394531, 52.01953125, 52.337219238281, 52.649169921875, 52.751892089844, 52.95556640625, 53.190185546875, 53.454528808594, 53.552612304688, 53.843566894531, 54.034912109375, 54.22412109375, 54.688232421875, 54.779602050781, 54.960876464844, 55.31787109375, 55.49365234375, 55.667602539062, 55.896911621094, 56.010437011719, 56.346618652344, 56.512268066406, 56.784912109375, 57.0, 57.317687988281, 57.629638671875, 57.706726074219, 57.93603515625, 58.162353515625, 58.3857421875, 58.533081054688, 58.824035644531, 58.967712402344, 59.391723632812, 59.668701171875, 59.760070800781, 59.941345214844, 60.209777832031, 60.474060058594, 60.604736328125, 60.863159179688, 60.990905761719, 61.24365234375, 61.492736816406, 61.859802246094, 61.98046875, 62.33740234375, 62.610046386719, 62.687194824219, 62.91650390625, 63.142761230469, 63.366149902344, 63.531860351562, 63.804504394531, 64.01953125, 64.337219238281, 64.649169921875, 64.751892089844, 64.95556640625, 65.190185546875, 65.454528808594, 65.552612304688, 65.843566894531, 66.034912109375, 66.22412109375, 66.688232421875, 66.779602050781, 66.960876464844, 67.31787109375, 67.49365234375, 67.667602539062, 67.896911621094, 68.010437011719, 68.346618652344, 68.512268066406, 68.784912109375, 69.0, 69.317687988281, 69.629638671875, 69.706726074219, 69.93603515625, 70.162353515625, 70.3857421875, 70.533081054688, 70.824035644531, 70.967712402344, 71.391723632812, 71.668701171875, 71.760070800781, 71.941345214844, 72.209777832031, 72.474060058594, 72.604736328125, 72.863159179688, 72.990905761719, 73.24365234375, 73.492736816406, 73.859802246094, 73.98046875, 74.33740234375, 74.610046386719, 74.687194824219, 74.91650390625, 75.142761230469 ];

	~tuningMap[1] = [ 46.3857421875, 46.533081054688, 46.824035644531, 46.967712402344, 47.391723632812, 47.668701171875, 47.760070800781, 47.941345214844, 48.209777832031, 48.474060058594, 48.604736328125, 48.863159179688, 48.990905761719, 49.24365234375, 49.492736816406, 49.859802246094, 49.98046875, 50.33740234375, 50.610046386719, 50.687194824219, 50.91650390625, 51.142761230469, 51.366149902344, 51.531860351562, 51.804504394531, 52.01953125, 52.337219238281, 52.649169921875, 52.751892089844, 52.95556640625, 53.190185546875, 53.454528808594, 53.552612304688, 53.843566894531, 54.034912109375, 54.22412109375, 54.688232421875, 54.779602050781, 54.960876464844, 55.31787109375, 55.49365234375, 55.667602539062, 55.896911621094, 56.010437011719, 56.346618652344, 56.512268066406, 56.784912109375, 57.0, 57.317687988281, 57.629638671875, 57.706726074219, 57.93603515625, 58.162353515625, 58.3857421875, 58.533081054688, 58.824035644531, 58.967712402344, 59.391723632812, 59.668701171875, 59.760070800781, 59.941345214844, 60.209777832031, 60.474060058594, 60.604736328125, 60.863159179688, 60.990905761719, 61.24365234375, 61.492736816406, 61.859802246094, 61.98046875, 62.33740234375, 62.610046386719, 62.687194824219, 62.91650390625, 63.142761230469, 63.366149902344, 63.531860351562, 63.804504394531, 64.01953125, 64.337219238281, 64.649169921875, 64.751892089844, 64.95556640625, 65.190185546875, 65.454528808594, 65.552612304688, 65.843566894531, 66.034912109375, 66.22412109375, 66.688232421875, 66.779602050781, 66.960876464844, 67.31787109375, 67.49365234375, 67.667602539062, 67.896911621094, 68.010437011719, 68.346618652344, 68.512268066406, 68.784912109375, 69.0, 69.317687988281, 69.629638671875, 69.706726074219, 69.93603515625, 70.162353515625, 70.3857421875, 70.533081054688, 70.824035644531, 70.967712402344, 71.391723632812, 71.668701171875, 71.760070800781, 71.941345214844, 72.209777832031, 72.474060058594, 72.604736328125, 72.863159179688, 72.990905761719, 73.24365234375, 73.492736816406, 73.859802246094, 73.98046875, 74.33740234375, 74.610046386719, 74.687194824219, 74.91650390625, 75.142761230469 ];

	// active map
	~tuningMap[127] = [ 46.3857421875, 46.533081054688, 46.824035644531, 46.967712402344, 47.391723632812, 47.668701171875, 47.760070800781, 47.941345214844, 48.209777832031, 48.474060058594, 48.604736328125, 48.863159179688, 48.990905761719, 49.24365234375, 49.492736816406, 49.859802246094, 49.98046875, 50.33740234375, 50.610046386719, 50.687194824219, 50.91650390625, 51.142761230469, 51.366149902344, 51.531860351562, 51.804504394531, 52.01953125, 52.337219238281, 52.649169921875, 52.751892089844, 52.95556640625, 53.190185546875, 53.454528808594, 53.552612304688, 53.843566894531, 54.034912109375, 54.22412109375, 54.688232421875, 54.779602050781, 54.960876464844, 55.31787109375, 55.49365234375, 55.667602539062, 55.896911621094, 56.010437011719, 56.346618652344, 56.512268066406, 56.784912109375, 57.0, 57.317687988281, 57.629638671875, 57.706726074219, 57.93603515625, 58.162353515625, 58.3857421875, 58.533081054688, 58.824035644531, 58.967712402344, 59.391723632812, 59.668701171875, 59.760070800781, 59.941345214844, 60.209777832031, 60.474060058594, 60.604736328125, 60.863159179688, 60.990905761719, 61.24365234375, 61.492736816406, 61.859802246094, 61.98046875, 62.33740234375, 62.610046386719, 62.687194824219, 62.91650390625, 63.142761230469, 63.366149902344, 63.531860351562, 63.804504394531, 64.01953125, 64.337219238281, 64.649169921875, 64.751892089844, 64.95556640625, 65.190185546875, 65.454528808594, 65.552612304688, 65.843566894531, 66.034912109375, 66.22412109375, 66.688232421875, 66.779602050781, 66.960876464844, 67.31787109375, 67.49365234375, 67.667602539062, 67.896911621094, 68.010437011719, 68.346618652344, 68.512268066406, 68.784912109375, 69.0, 69.317687988281, 69.629638671875, 69.706726074219, 69.93603515625, 70.162353515625, 70.3857421875, 70.533081054688, 70.824035644531, 70.967712402344, 71.391723632812, 71.668701171875, 71.760070800781, 71.941345214844, 72.209777832031, 72.474060058594, 72.604736328125, 72.863159179688, 72.990905761719, 73.24365234375, 73.492736816406, 73.859802246094, 73.98046875, 74.33740234375, 74.610046386719, 74.687194824219, 74.91650390625, 75.142761230469 ];

// Post << ~tuningMap[0]; // complete array in post window

};

// MIDI I/O and GUI

////////////////
//	         //
//   MIDI   //
//         //
////////////


~levels=[0.0,0.0,0.0,0.0];
~transpose = 1;

// post MIDI to window
~post_note = true;
~post_cc = true;
~post_bend = false;
~post_pressure = false;
~post_polytouch = false;

// change these values for multichannnel tuning (channel in 1-16 format)
~multichannel = true;
~main_channel = 3;
~equave = 2;

// morphing (interpolating) between two maps
~current_map = 127; // choose active tuning map
~map_down = 0; // choose tuning map when controller value is low
~map_up = 1; // choose tuning map when controller value is high

// polyphony array has one slot per possible MIDI note
~notes = Array.fill2D(16, 128, {[nil, nil, nil, nil]});

// sustain pedal array for sustained notes (note-off velocities)
~sustain_pedal = 0;
~sustained = Array.fill2D(16, 128, {-1} );
~velocities = Array.fill2D(16, 128, {0} );

// bend : pitch-bend wheel is applied to most recent note ONLY
~bend = 1; // center value
~bend_range = 28/27; // fixed ratio for maximum pitch bend
~most_recent_note = []; // array to keep track of notes in order played

// cc from any channel
~controllers = Array.fill(128,{0});

~mod_cc = 1; // set cc number for timbral modification
//~morph_cc = nil; // set cc number for crossfading between tuning maps
//~finetuning_cc = 0;

// MIDI functions

// note-on function
~on = MIDIFunc.noteOn({ |on_vel, num, chan, src|
	var formants;

	o = 0; // set number of equaves transposition (default to no transposition)

	if (~multichannel) { // only transpose when multichannel is set to true
		o = (chan % 8) - ~main_channel + 1;
		while { o < -4 } { o = o + 8 };
		while { o >= 4 } { o = o - 8 };
	};

	m = ~equave ** o;// determine multiplier

	f = ~tuningMap[~current_map][num].midicps * ~transpose * m;

	if (~post_note) { "noteon: ".post;[chan + 1, num, on_vel].post; " transposed by ".post; o.post; " equave(s)".postln;
	};

	if (on_vel > 0) {
		~most_recent_note = ~most_recent_note.removeEvery([num + (chan * 128)]);
		~most_recent_note = ~most_recent_note.addFirst(num + (chan * 128));
		~velocities[chan][num] = on_vel;
	};

	(0..3).do { |synth| if (~notes[chan][num][synth].isPlaying) {
		~notes[chan][num][synth].set(\off_vel, on_vel, \gate, 0); // node will disappear by itself
		~notes[chan][num][synth] = nil; // clear the slot !
	} }; // check to make sure slot is not sounding already, if it is, turn it off before producing a new synth in the slot!

	~notes[chan][num][0] = Synth(\pluck, [\freq, f, \bend, ~bend, \on_vel, on_vel, \mod, ~controllers[~mod_cc], \vol, ~levels[0], \gate, 1]);
	NodeWatcher.register(~notes[chan][num][0]);

	~notes[chan][num][1] = Synth(\string, [\freq, f, \bend, ~bend, \on_vel, on_vel, \mod, ~controllers[~mod_cc], \vol, ~levels[1], \gate, 1]);
	NodeWatcher.register(~notes[chan][num][1]);

	formants = { FormantTable.rand }.value;

	~notes[chan][num][2] = Synth(\formant, [\formant_freq, formants[0], \formant_reso, formants[1], \formant_amp, formants[2], \freq, f, \bend, ~bend, \on_vel, on_vel, \mod, ~controllers[~mod_cc], \vol, ~levels[2], \gate, 1]);
	NodeWatcher.register(~notes[chan][num][2]);

	~notes[chan][num][3] = Synth(\tone, [\freq, f, \bend, ~bend, \on_vel, on_vel, \mod, ~controllers[~mod_cc], \vol, ~levels[3], \gate, 1]);
	NodeWatcher.register(~notes[chan][num][3]);
});


// note-off
~off = MIDIFunc.noteOff({ |off_vel, num, chan, src|
	if (off_vel == 0) {
		off_vel = ~velocities[chan][num];
	};

	if (~sustain_pedal == 0)
	{ if (~post_note) {
		"noteoff: ".post;[chan + 1, num, off_vel].postln;
	};

	~most_recent_note = ~most_recent_note.removeEvery([num + (chan * 128)]);

	(0..3).do {|synth| if (~notes[chan][num][synth].isPlaying)
		{ ~notes[chan][num][synth].set(\off_vel, off_vel, \gate, 0); // node will disappear by itself
			~notes[chan][num][synth] = nil;
	} };
	} { ~sustained[chan][num] = off_vel };

});


// polytouch further modifies brightness per note
~keypressure = MIDIFunc.polytouch({ |val,num,chan,src|
	~filter = (val+1).linlin(1,128,1,2);

	if (~post_polytouch) {
		(chan + 1).post; ": polytouch ".post; num.post; " ".post; val.post; " ".post; ~filter.postln;
	};

	~notes[chan][num][3].set(\filter,~filter)
});

// channel pressure modifies timbre globally
~channelpressure = MIDIFunc.touch({ |val,chan,src|
	~filter_global = (val+1).linlin(1,128,1,2);

	if (~post_pressure) {
		(chan + 1).post; ": touch ".post; val.post; " ".post; ~filter_global.postln;
	};

	(0..15).do { |channel|
		(0..127).do { |note| if (~notes[channel][note][3].isPlaying) {
			~notes[channel][note][3].set(\filter,~filter_global)
	} } };
});


// apply pitch bend to last played note
~pitchbend = MIDIFunc.bend({ |val=8192,chan,src|

	~bend = (val).clip(1,16383).linexp(1,16383,1/~bend_range,~bend_range);

	if (~post_bend) {
		(chan + 1).post; ": bend ".post; val.post; " ".post; ~bend.postln;
	};

	if (~most_recent_note[0] != nil) {
		var channel = ((~most_recent_note[0] / 128).roundUp - 1).asInteger;
		var note = ~most_recent_note[0] % 128;
		// "bend-target: [".post; channel.post; ", ".post; note.post; "] ".postln;

		(0..3).do { |synth| if(~notes[channel][note][synth].isPlaying)
			{ ~notes[channel][note][synth].set(\bend, ~bend) }
		}; // apply pitchbend to all channels
	};
});


// any cc converted to an exponential value between 1 and 2
~cc = MIDIFunc.cc({ |val,num,chan,src|
	c = (val+1).linexp(8,119,1,2); // i
	~controllers[num] = c;

	if (~post_cc) {
		(chan + 1).post; ": cc ".post; num.post; " ".post; val.post; " ".post;~controllers[num].postln;
	};

	if (num == ~mod_cc) { // timbral modulation controller
		(0..15).do { |channel|
			(0..127).do { |note|
				(0..3).do { |synth| if(~notes[channel][note][synth].isPlaying)
					{ ~notes[channel][note][synth].set(\mod,~controllers[~mod_cc]) }
	} } } };

if (num == 64) { // sustain pedal
		~sustain_pedal = val;
		if (val > 0)
		{ if (~post_cc) {"sustaining!".postln} }
		{ if (~post_cc) {"releasing!".postln};
			(0..15).do { |channel|
				(0..127).do { |note| if (~sustained[channel][note] >= 0) {
					if (~post_note) {
						"noteoff: ".post;[channel + 1, note, ~sustained[channel][note]].postln;};
					~most_recent_note = ~most_recent_note.removeEvery([note + (channel * 128)]);

					(0..3).do {|synth| if (~notes[channel][note][synth].isPlaying)
						{ ~notes[channel][note][synth].set(\off_vel, ~sustained[channel][note], \gate, 0);
							~notes[channel][note][synth] = nil;
						};
					};

					~sustained[channel][note] = -1; // reset the slot after releasing
			} } };
		};
	};

	if (num == 2) { // level of pluck
		~levels[0] = val.linlin(0,127,0,1);
		v.valueAction = ~levels[0];
		(0..15).do { |channel|
			(0..127).do { |note| if (~notes[channel][note][0].isPlaying)
				{ ~notes[channel][note][0].set(\vol,~levels[0])}
		} };
	};

	if (num == 3) { // level of buzz
		~levels[1] = val.linlin(0,127,0,1);
		(0..15).do { |channel|
			(0..127).do { |note| if (~notes[channel][note][1].isPlaying)
				{ ~notes[channel][note][1].set(\vol,~levels[1])}
		} };
	};

	if (num == 5) { // level of formant
		~levels[2] = val.linlin(0,127,0,1);
		(0..15).do { |channel|
			(0..127).do { |note| if (~notes[channel][note][2].isPlaying)
				{ ~notes[channel][note][2].set(\vol,~levels[2])}
		} };
	};

	if (num == 6) { // level of saw
		~levels[3] = val.linlin(0,127,0,1);
		(0..15).do { |channel|
			(0..127).do { |note| if (~notes[channel][note][3].isPlaying)
				{ ~notes[channel][note][3].set(\vol,~levels[3])}
		} };
	};

});


//////////////
//	       //
//  ESC   //
//       //
//////////



k = UnicodeResponder.new;
	k.register(   27  ,   false, false, false, false,
		{ "Killing all!".postln;
			(0..3).do
			{ |synth| (0..127).do
				{ |note| (0..15).do
					{|chan| if (~notes[chan][note][synth].isPlaying) {
						~notes[chan][note][synth].set(\off_vel, 64, \gate, 0);
						~notes[chan][note][synth] = nil;} } } }; // all ~notes off
});

View.globalKeyDownAction = k;

//////////////
//	       //
//  GUI   //
//       //
//////////

w = Window.new("Electric Harpsichord Tanpura Organ",Rect((Window.screenBounds.width)*3/4,(Window.screenBounds.height/2)-((Window.screenBounds.width/4-200)/4),
	Window.screenBounds.width/4,(Window.screenBounds.height/2)+((Window.screenBounds.width/4-200)/4)-60)); // GUI window

w.front;

v = GUI.slider.new(w,Rect(40,40,(Window.screenBounds.width/4-200)/4,(Window.screenBounds.height/2)-180)); // slider 1
v.action = { ~levels[0]=v.value;
	(0..15).do { |channel|
		(0..127).do { |note| if (~notes[channel][note][0].isPlaying)
			{ ~notes[channel][note][0].set(\vol,v.value)}
	} };
};

u = GUI.slider.new(w,Rect(80+((Window.screenBounds.width/4-200)/4),40,(Window.screenBounds.width/4-200)/4,(Window.screenBounds.height/2)-180));  // slider 2
u.action = { ~levels[1]=u.value;
	(0..15).do { |channel|
		(0..127).do { |note| if (~notes[channel][note][1].isPlaying)
			{ ~notes[channel][note][1].set(\vol,u.value)}
	} };

};

q = GUI.slider.new(w,Rect(120+(2*(Window.screenBounds.width/4-200)/4),40,(Window.screenBounds.width/4-200)/4,(Window.screenBounds.height/2)-180));  // slider 3
q.action = { ~levels[2]=q.value;
	(0..15).do { |channel|
		(0..127).do { |note| if (~notes[channel][note][2].isPlaying)
			{ ~notes[channel][note][2].set(\vol,q.value)}
	} };
};

z = GUI.slider.new(w,Rect(160+(3*(Window.screenBounds.width/4-200)/4),40,(Window.screenBounds.width/4-200)/4,(Window.screenBounds.height/2)-180));  // slider 4
z.action = { ~levels[3]=z.value;
	(0..15).do { |channel|
		(0..127).do { |note| if (~notes[channel][note][3].isPlaying)
			{ ~notes[channel][note][3].set(\vol,z.value)}
	} };
};

CmdPeriod.add({ w.close; });

"Use faders to mix the Synths!";

)

Thanks for sharing the code, it helps a bit to figure out your environment.

Can you see if the problem persists when using e.g. Ableton (or else) for MIDI sequencing instead of using external controllers? Try to bypass any USB I/O within SuperCollider.
James found some information regarding hidapi in the stacktrace, so it could be worth trying to narrow it down if this is a USB/HID bug or a bug within the MIDI stack.

So I recorded MIDI data from the Lumatone to Reaper (which handles multiple channel input and sysex, unlike Ableton) until an Interpreter crash occurred (not long). I tested the file in various playback scenarios. If it is slowed down, it does not cause a crash, so the data is essentially OK, if somewhat dense. But it is still at a density that reflects live playing and should actually play fine. For example, Pianoteq, which plays MTS data, had no problem firing out all the notes. I guess the problem is NOT USB/HID, but in fact MIDI processing. I am happy to attach or link to the MIDI file for further testing. The crash dump looks similar to the one I already posted.

https://1drv.ms/u/s!AqqKGcn30zw8kaEshdWOwhNawSQtxg?e=r3Uu12

(link to MIDI file)

Hello and happy new year! Hoping you might still be able to help, I have a little more information: the problem of interpreter crashing seems to be linked to my use case with incoming real-time MIDI tuning messages, which are 12 byte Sysex messages immediately preceding each Note On, giving a retuning (new frequency) for the immediately following MIDI note. The MIDI note number acts as an index for storing Frequencies for the created Synth Nodes (standard practice with the MIDI Tuning Standard). This is also one recommended method for retuning notes in MIDI 2 when one wants to be able to have many arbitrarily changing microtonal pitches.

When I turn off the incoming Sysex data and use an internally stored (fixed) tuning map, the interpreter does not crash. When I use Sysex and send lots of notes, it seems that the Sysex responder (MIDIFunc.sysex) is operating correctly (I hear the retuning and see the results posted correctly) but at some point when CPU usage gets high it crashes the IDE.There is no visible error when tracking the OSC messages, but maybe I am missing something.

This crash does not happen on an M1 Macbook Air I have access to, only on my own Windows 11 Laptop.

If I record the data to a sequencer (Reaper) and play it back using LoopMIDI, the same crashes happen, so I believe the hidapi library is likely not causing them.

The incoming data plays Pianoteq without any problems (receiving the tuning messages in real time and no stuck notes), so the data is probably OK:

Do you have an idea? I will try a different sysex class (directly using MIDIin) and see if there is any difference, but I don’t imagine that will help. What would cause the pointer error?

Would it help to somehow sync or make bundle with the mts~ and on~ MIDI responders?

If you have any hints how to approach debugging the code (PortMIDI) I would be willing to try, but just reading through it I don’t feel knowledgeable enough to try anything without a little help.

Thanks all for your input!

Marc,

You may have done this already, but what happens if you keep the MIDIFunc.sysex function in there, but don’t do anything in it? Does it still crash? And what happens if you just get rid of all the post messages in the sysex function? I just wonder if there is another culprit.

Big fan, btw!

Sam

Hi Sam, many thanks for the suggestion: if the MIDIFunc.sysex does nothing it doesn’t crash, if it does the math and puts the number in the array, without posting, it does crash, but as before, only when overloaded with lots of notes. Not sure if there is a way to bundle this with the note ons so there is no way it can happen…

CORRECTION: even if the MIDIFunc.sysex ONLY does the check
if (val.size)
it DOES crash
… and replacing MIDIFunc with
MIDIIn.addFuncTo(\sysex, ~mts)
behaves the same…

I believe there is somethings in the Windows sysex implementation that is causing this… how can I compare this in code to Linux or Mac?

  • Mac: (source tree)/lang/LangPrimSource/SC_CoreMIDI.cpp
  • Linux: (source tree)/lang/LangPrimSource/SC_AlsaMIDI.cpp
  • Win: (source tree)/lang/LangPrimSource/SC_PortMIDI.cpp

hjh

Seems I’ve messed up the thread split a bit – the one topic was about a server crash, and this topic is about a language crash with MIDI – so the topic shouldn’t have been a reply to the old thread (which incidentally didn’t get answered).

To summarize the most recent post, which is now out of order (my fault – I hoped the forum would sort them in date order, but it didn’t):

  • Windows receives MTS sysex and crashes; macOS doesn’t. But this happens when overloaded with lots of notes, and only when actually calling a sysex response function in sclang.
    • Crash is definitely a source code problem. dscheiba initially suggested maybe it’s a mutex problem – the interpreter needs to finish answering one packet fully before handling the next; if the crash happens when flooded with a lot of messages, the likely explanation is that it is trying to handle two or three at once. I agree with that assessment but I remember looking at PortMIDI.cpp and it looks like MIDI responses should all be mutex protected. So I don’t have an answer :confused:
  • macOS: send sysex, then noteOn immediately, and CoreMIDI may prioritize the note on (bad for tuning messages).
    • I suspect the idea here might be that notes are time sensitive while sysexes generally aren’t. You might need to send the sysex slightly early… annoying but if this is something deep in CoreMIDI, unlikely that we’d be able to override in sclang sources.

Sorry again – I saw a mismatch between the thread title and the message contents, and didn’t immediately notice that the thread hijack took place a long time ago. Hope that this is an accurate summary of the issue, to proceed from here.

hjh

1 Like

A very succinct summary :slight_smile: About the macOS side, I compared the behaviour of the SC code against my webapp https://hexatone.plainsound.org in MIDI mode with MTS output on: the code does exactly the same thing, sending sysex followed by noteOn using the webmidi.js API – if the culprit is CoreMidi, shouldn’t there be a similar behaviour? I didn’t find any timing glitches…

I no longer know where this thread is, but I noticed sysex has a different latency than noteOn.

~midiout.latency_(1)

~midiout.sysex(Int8Array[0xF0, 1, 2, 3, 0xF7]);
~midiout.noteOn(0, 60, 60);

Looking in Protocol, the sysex message has no latency and the noteOn has it. So maybe setting a very small latency would fix the issue?

Sam

Thank you Sam, where exactly can I find these latency settings in the supercollider codebase, curious to look closer. Which Protocol do you mean?

Thanks, Marc

… and a follow-up: I tested sending many times the same sequence of sysex - noteon - noteoff with different latencies and checked in MIDIMonitor what’s going on. It seems that with short latencies between 0 and 0.1 the note on and note off messages get there before MOST of the sysex (except the first one) and with longer latencies the sysex all get there first. So introducing a little latency would (I suppose) “solve” the issue, but I am wondering if there is any way to more effectively control timing of MIDI based on the IDE code?

~midiout.latency_(0.02);

~midiout.sysex(Int8Array[0xF0, 1, 2, 3, 0xF7]);
~midiout.noteOn(0, 60, 60);
~midiout.noteOff(0, 60, 60);
~midiout.sysex(Int8Array[0xF0, 1, 2, 3, 0xF7]);
~midiout.noteOn(0, 60, 60);
~midiout.noteOff(0, 60, 60);
~midiout.sysex(Int8Array[0xF0, 1, 2, 3, 0xF7]);
~midiout.noteOn(0, 60, 60);
~midiout.noteOff(0, 60, 60);
~midiout.sysex(Int8Array[0xF0, 1, 2, 3, 0xF7]);
~midiout.noteOn(0, 60, 60);
~midiout.noteOff(0, 60, 60);
~midiout.sysex(Int8Array[0xF0, 1, 2, 3, 0xF7]);
~midiout.noteOn(0, 60, 60);
~midiout.noteOff(0, 60, 60);
~midiout.sysex(Int8Array[0xF0, 1, 2, 3, 0xF7]);
~midiout.noteOn(0, 60, 60);
~midiout.noteOff(0, 60, 60);
~midiout.sysex(Int8Array[0xF0, 1, 2, 3, 0xF7]);
~midiout.noteOn(0, 60, 60);
~midiout.noteOff(0, 60, 60);
~midiout.sysex(Int8Array[0xF0, 1, 2, 3, 0xF7]);
~midiout.noteOn(0, 60, 60);
~midiout.noteOff(0, 60, 60);
~midiout.sysex(Int8Array[0xF0, 1, 2, 3, 0xF7]);
~midiout.noteOn(0, 60, 60);
~midiout.noteOff(0, 60, 60);
~midiout.sysex(Int8Array[0xF0, 1, 2, 3, 0xF7]);
~midiout.noteOn(0, 60, 60);
~midiout.noteOff(0, 60, 60);
~midiout.sysex(Int8Array[0xF0, 1, 2, 3, 0xF7]);
~midiout.noteOn(0, 60, 60);
~midiout.noteOff(0, 60, 60);
~midiout.sysex(Int8Array[0xF0, 1, 2, 3, 0xF7]);
~midiout.noteOn(0, 60, 60);
~midiout.noteOff(0, 60, 60);

Protokol is Hexler Protokol. Invaluable for testing MIDI and OSC stuff.

In MIDIOut.sc, every midi message other than sysex calls _SendMIDIOut. sysex messages call _SendSysex. These are defined in SC_CoreMIDI.cpp. If you trace the function calls, they end up in different final calls. _SendMIDIOut calls MIDISend in CoreMIDI, but _SendSysex calls MIDISendSysex. I don’t see anywhere where latency is a parameter for sysex, but it is always a parameter for other messages. That’s all I got!

Sam