While we now have a way to initialize the device during boot. I doubt many end users are going to want to change the code and rebuild the project.
Since I want to keep everything modular, and extendable to other devices where possible, this is where a new generic sysex operator will fill the gap.
MidiSysexOutput Operator

Functionality:
When triggered, will send a midi sysex command to the specified device.
Explanation:
Since there is no generic byte type or byte list in Tixl, we can easily use a string instead.
We have to parse the string with a separator to break it into individual strings that represent HEX values, in this case using space ” “.
We take these new string values and try to parse them into bytes, if they are not legitimate hex (00-FF), it sends an error.
Midi sysex also has specific bytes it uses to identify the starting, and ending of the command being sent, these are F0 for start, and F7 for end.
We also verify that the starts and ends with these bytes, and error if they do not.
Show Code
using NAudio.Midi;
using Operators.Utils;
namespace Lib.io.midi;
[Guid("78cadb24-5a77-41e2-908f-01d61140c769")]
internal sealed class MidiSysexOutput : Instance<MidiSysexOutput>
,MidiConnectionManager.IMidiConsumer,ICustomDropdownHolder,IStatusProvider
{
[Output(Guid = "b996a54f-0fe7-4eb3-8f29-66938eeb113e")]
public readonly Slot<Command> Result = new();
// We start with initialized true so it does not run when the project is loaded, only when triggered
private bool _initialized = true;
// Registered is true only if we have a midi device registered/connected
private bool _registered;
private List<byte> _bytes = new();
public MidiSysexOutput()
{
Result.UpdateAction = Update;
}
protected override void Dispose(bool isDisposing)
{
if(!isDisposing) return;
if (_initialized)
{
MidiConnectionManager.UnregisterConsumer(this);
_registered = false;
}
}
private void Update(EvaluationContext context)
{
var triggerActive = TriggerSend.GetValue(context);
var deviceName = Device.GetValue(context);
var foundDevice = false;
if (triggerActive)
{
_initialized = !_initialized;
// Clear the current registered midi device
Dispose(true);
// Clear the byte list
_bytes.Clear();
}
if (!_initialized)
{
if (!_registered)
{
// Register the midi device
MidiConnectionManager.RegisterConsumer(this);
_registered = true;
}
var contextval = SysexString.GetValue(context);
// Split the string by space character
var characters = contextval.Split(' ');
// Iterate and build our byte list
foreach (string c in characters)
{
try
{
byte byteValue = byte.Parse(c, System.Globalization.NumberStyles.HexNumber);
_bytes.Add(byteValue);
}
catch (Exception e)
{
_lastErrorMessage = $"Failed to convert [{c}] to a byte: " + e.Message;
Log.Warning(_lastErrorMessage, this);
// Set initialized so we do not loop
_initialized = true;
return;
}
}
// If the starting byte of the sysex string does not equal midi exclusive start
if (_bytes.ElementAt(0) != 240)
{
_lastErrorMessage = "Sysex String needs to start with midi exclusive start byte: F0";
Log.Warning(_lastErrorMessage, this);
// Set initialized so we do not loop
_initialized = true;
return;
}
// If the end byte of the sysex string does not equal midi exclusive end
if (_bytes.ElementAt(_bytes.Count-1) != 247)
{
_lastErrorMessage = "Sysex String needs to end with midi exclusive end byte: F7";
Log.Warning(_lastErrorMessage, this);
// Set initialized so we do not loop
_initialized = true;
return;
}
foreach (var (m, device) in MidiConnectionManager.MidiOutsWithDevices)
{
if (device.ProductName != deviceName)
continue;
try
{
if (_bytes != null && triggerActive)
{
m.SendBuffer(_bytes.ToArray());
Log.Debug($"Sent Sysex: [ " + contextval + " ] To: [ " + deviceName + " ]", this);
}
foundDevice = true;
break;
}
catch (Exception e)
{
_lastErrorMessage = $"Failed to send midi to {deviceName}: " + e.Message;
Log.Warning(_lastErrorMessage, this);
}
}
_lastErrorMessage = !foundDevice ? $"Can't find MidiDevice {deviceName}" : null;
Log.Warning(_lastErrorMessage, this);
_initialized = true;
}
}
#region device dropdown
string ICustomDropdownHolder.GetValueForInput(Guid inputId)
{
return Device.Value;
}
IEnumerable<string> ICustomDropdownHolder.GetOptionsForInput(Guid inputId)
{
if (inputId != Device.Id)
{
yield return "undefined";
yield break;
}
foreach (var device in MidiConnectionManager.MidiOutsWithDevices.Values)
{
yield return device.ProductName;
}
}
void ICustomDropdownHolder.HandleResultForInput(Guid inputId, string selected, bool isAListItem)
{
Log.Debug($"Got {selected}", this);
Device.SetTypedInputValue(selected);
}
#endregion
#region Implement statuslevel
IStatusProvider.StatusLevel IStatusProvider.GetStatusLevel()
{
return string.IsNullOrEmpty(_lastErrorMessage) ? IStatusProvider.StatusLevel.Success : IStatusProvider.StatusLevel.Error;
}
string IStatusProvider.GetStatusMessage()
{
return _lastErrorMessage;
}
// We don't actually receive midi in this operator, those methods can remain empty, we just want the MIDI connection thread up
public void MessageReceivedHandler(object sender, MidiInMessageEventArgs msg) {}
public void ErrorReceivedHandler(object sender, MidiInMessageEventArgs msg) {}
public void OnSettingsChanged() {}
private string _lastErrorMessage;
#endregion
[Input(Guid = "611e8c42-9954-421c-8071-1e959478c8fe")]
public readonly InputSlot<bool> TriggerSend = new ();
[Input(Guid = "d7189d4a-b6c7-4d4f-9cee-1c37ed55b4e5")]
public readonly InputSlot<string> Device = new ();
[Input(Guid = "e054dece-d5ce-4675-9e24-d29e616ce7de")]
public readonly InputSlot<string> SysexString = new();
}Conclusion:
With community feedback, a little brainstorming brought about the current possibilities of extending this operator:
– Decimal based input – Since most operator can use Int values, but not HEX directly, it may be worth finding a conversion method. This may be best left to an external operator. We will still need to make sure the decimal value is valid to parse back to a byte (0-255)
– Customize the separator pattern – We are currently checking for spaces between the HEX strings, by allowing to change the separator pattern, we can be more flexible on the input. but it can add complexity and more possibilities for things to break.
Example: if end user decides to use “0xAA, 0xBB, 0xCC” formatting. But with separator “, 0x” – It would still fail with the initial 0x at the beginning of the string. 0x is always redundant in that case anyways.
Another note, some documentation also defines the hex strings like: F0h 00h 01h 02h F7h. – A separator “h ” will also fail because of the trailing h.
Maybe be endless fail cases
