Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a93d721
Buffer fix
Nytra Jul 25, 2024
dcd9a28
Attempt Linux fixes
Nytra Sep 27, 2025
074c92b
ignore .vscode
Nytra Sep 27, 2025
1031447
Merge main
Nytra Sep 27, 2025
4f13278
Add source gen submodule
Nytra Sep 27, 2025
56bc504
Restore linux fixes and add alsa-lib
Nytra Sep 27, 2025
c71b400
Merge branch 'Custom-Extension-Works:main' into LinuxFixes
Nytra Sep 27, 2025
e87f121
Added running status
Nytra Sep 27, 2025
52e1ac4
Check fixed data size
Nytra Sep 27, 2025
8cd4c3a
Use better struct for running status events
Nytra Sep 27, 2025
1e035e6
More fixups
Nytra Sep 27, 2025
04d132a
Polish and rename some stuff
Nytra Sep 27, 2025
d5ad950
Fixed lost data from status byte?
Nytra Sep 27, 2025
36995c9
Make actual event type into a property
Nytra Sep 28, 2025
91232eb
Final fixes hopefully
Nytra Sep 28, 2025
a55809f
Add some comments
Nytra Sep 28, 2025
d755a82
Fix running status not storing channel info
Nytra Sep 28, 2025
48162bd
refresh devices on settings enumeration
Nytra Sep 28, 2025
5669d57
fix refresh devices and make locale refresh when buttons are pressed
Nytra Sep 28, 2025
1326143
Change name of refresh locale button
Nytra Sep 28, 2025
968a477
Improvements
Nytra Sep 28, 2025
c634e93
Fix data length check
Nytra Sep 28, 2025
fdb49e8
Remove call to events.Count which fixes events being created twice
Nytra Sep 28, 2025
fcc0ae3
Push latest
Nytra Sep 28, 2025
3db05c6
Update managed-midi to latest source, this includes my fix for runnin…
Nytra Sep 28, 2025
aaa3c78
Fix for event types
Nytra Sep 28, 2025
3441aca
Update managed-midi which now has another of my fixes
Nytra Sep 28, 2025
f399ba7
Move handling of system messages out of flush buffer method
Nytra Sep 28, 2025
c7d330c
Remove calls to locale update
Nytra Sep 28, 2025
8691e86
remove useless stuff
Nytra Sep 28, 2025
8ffc8a9
Turn off debug
Nytra Sep 30, 2025
efbcaa3
Remove debug stuff
Nytra Sep 30, 2025
cf85deb
Adjust a debug thing
Nytra Sep 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -352,4 +352,6 @@ MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/

.idea
.idea

.vscode/
244 changes: 126 additions & 118 deletions ProjectObsidian/Elements/MIDI.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Commons.Music.Midi;
using Obsidian.Components.Devices.MIDI;
using Elements.Core;
using FrooxEngine;
using Elements.Data;

namespace Obsidian.Elements;

public struct TimestampedMidiEvent
{
public MidiEvent midiEvent;
public MidiEvent evt;
public long timestamp;
public TimestampedMidiEvent(MidiEvent _midiEvent, long _timestamp)
public TimestampedMidiEvent(MidiEvent _evt, long _timestamp)
{
midiEvent = _midiEvent;
evt = _evt;
timestamp = _timestamp;
}
public override string ToString()
{
return $"{timestamp}, {evt}";
}
}

public interface IMidiInputListener
Expand Down Expand Up @@ -65,23 +67,19 @@ public class MidiInputConnection

public List<IMidiInputListener> Listeners = new();

// I am using this like a Queue so it could possibly be turned into a Queue instead...
private List<TimestampedMidiEvent> _eventBuffer = new();

private const long MESSAGE_BUFFER_TIME_MILLISECONDS = 3;

private long _lastMessageBufferStartTime = 0;
private const long BUFFER_TIME_MILLISECONDS = 3;

private int _bufferedMessagesToHandle = 0;
private long _lastEventBufferStartTime = 0;

private const bool DEBUG = false;

public void Initialize()
{
Input = null;
_eventBuffer.Clear();
_lastMessageBufferStartTime = 0;
_bufferedMessagesToHandle = 0;
_lastEventBufferStartTime = 0;
Listeners.Clear();
}

Expand All @@ -97,11 +95,9 @@ private ushort CombineBytes(byte First, byte Second)

private bool IsCCFineMessage()
{
if (_eventBuffer.Count == 0) return false;
long timestamp = _eventBuffer[0].timestamp;
if (_eventBuffer.Count >= 2
&& _eventBuffer[0].midiEvent.EventType == MidiEvent.CC && _eventBuffer[1].midiEvent.EventType == MidiEvent.CC
&& _eventBuffer[0].midiEvent.Msb == _eventBuffer[1].midiEvent.Msb - 32)
if (_eventBuffer.Count < 2) return false;
if (_eventBuffer[0].evt.EventType == MidiEvent.CC && _eventBuffer[1].evt.EventType == MidiEvent.CC
&& _eventBuffer[0].evt.Msb == _eventBuffer[1].evt.Msb - 32)
{
return true;
}
Expand All @@ -121,175 +117,187 @@ private void FlushMessageBuffer()

while (_eventBuffer.Count > 0)
{

// 14bit CC
while (IsCCFineMessage())
{
var e1 = _eventBuffer[0].midiEvent;
var e1 = _eventBuffer[0];
if (DEBUG) UniLog.Log(e1.ToString());
var e2 = _eventBuffer[1].midiEvent;
var e2 = _eventBuffer[1];
if (DEBUG) UniLog.Log(e2.ToString());
var finalValue = CombineBytes(e2.Lsb, e1.Lsb);
var finalValue = CombineBytes(e2.evt.Lsb, e1.evt.Lsb);
if (DEBUG) UniLog.Log($"CC fine. Value: " + finalValue.ToString());
Listeners.ForEach(l => l.TriggerControl(new MIDI_CC_EventData(e1.Channel, e1.Msb, finalValue, _coarse: false)));
Listeners.ForEach(l => l.TriggerControl(new MIDI_CC_EventData(e1.evt.Channel, e1.evt.Msb, finalValue, _coarse: false)));
_eventBuffer.RemoveRange(0, 2);
_bufferedMessagesToHandle -= 2;
}

if (_eventBuffer.Count == 0) break;

var e = _eventBuffer[0].midiEvent;
var e = _eventBuffer[0];
if (DEBUG) UniLog.Log(e.ToString());
switch (e.EventType)
switch (_eventBuffer[0].evt.EventType)
{
case MidiEvent.CC:
if (DEBUG) UniLog.Log("CC");
Listeners.ForEach(l => l.TriggerControl(new MIDI_CC_EventData(e.Channel, e.Msb, e.Lsb, _coarse: true)));
Listeners.ForEach(l => l.TriggerControl(new MIDI_CC_EventData(e.evt.Channel, e.evt.Msb, e.evt.Lsb, _coarse: true)));
break;

// Program events are buffered because they can be sent after a CC fine message for Bank Select, one of my devices sends consecutively: CC (Bank Select) -> CC (Bank Select Lsb) -> Program for some buttons
case MidiEvent.Program:
if (DEBUG) UniLog.Log("Program");
Listeners.ForEach(l => l.TriggerProgram(new MIDI_ProgramEventData(e.Channel, e.Msb)));
Listeners.ForEach(l => l.TriggerProgram(new MIDI_ProgramEventData(e.evt.Channel, e.evt.Msb)));
break;

// Unhandled events:

//SysEx events are probably not worth handling
case MidiEvent.SysEx1:
if (DEBUG) UniLog.Log("UnhandledEvent: SysEx1");
break;
case MidiEvent.SysEx2:
// Same as EndSysEx
if (DEBUG) UniLog.Log("UnhandledEvent: SysEx2");
break;
case MidiEvent.MtcQuarterFrame:
if (DEBUG) UniLog.Log("UnhandledEvent: MtcQuarterFrame");
break;
case MidiEvent.SongPositionPointer:
if (DEBUG) UniLog.Log("UnhandledEvent: SongPositionPointer");
break;
case MidiEvent.SongSelect:
if (DEBUG) UniLog.Log("UnhandledEvent: SongSelect");
break;
case MidiEvent.TuneRequest:
if (DEBUG) UniLog.Log("UnhandledEvent: TuneRequest");
break;
default:
// This should never happen
if (DEBUG) UniLog.Log($"Unrecognized event type! {_eventBuffer[0].evt.EventType}");
break;
}
_eventBuffer.RemoveAt(0);
_bufferedMessagesToHandle -= 1;
}
if (DEBUG) UniLog.Log("Finished flushing message buffer from start time: " + batchStartTime.ToString());
if (_bufferedMessagesToHandle != 0)
{
// Just in case some messages got lost somehow
UniLog.Warning("Did not handle all buffered messages! " + _bufferedMessagesToHandle.ToString());
}
_bufferedMessagesToHandle = 0;
}

public async void OnMessageReceived(object sender, MidiReceivedEventArgs args)
{
if (DEBUG) UniLog.FlushEveryMessage = true;

if (DEBUG) UniLog.Log($"*** New midi message");
if (DEBUG) UniLog.Log($"* Received {args.Length} bytes");
if (DEBUG) UniLog.Log($"* Timestamp: {args.Timestamp}");
if (DEBUG) UniLog.Log($"* Start: {args.Start}");

var events = MidiEvent.Convert(args.Data, args.Start, args.Length);
if (DEBUG) UniLog.Log($"* Raw bytes: {string.Join(",", args.Data.Skip(args.Start).Take(args.Length).Select(b => string.Format("{0:X}", b)))}");

if (args.Length == 1)
long timestamp = args.Timestamp;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) // Timestamp is always zero on Linux with the Alsa Midi API
{
// system realtime message, do not buffer these, execute immediately
if (DEBUG) UniLog.Log($"* System realtime message");
foreach (var e in events)
{
var str = e.ToString();
if (DEBUG) UniLog.Log("* " + str);
switch (e.StatusByte)
{
case MidiEvent.MidiClock:
if (DEBUG) UniLog.Log("* MidiClock");
Listeners.ForEach(l => l.TriggerMidiClock(new MIDI_SystemRealtimeEventData()));
break;
case MidiEvent.MidiTick:
if (DEBUG) UniLog.Log("* MidiTick");
Listeners.ForEach(l => l.TriggerMidiTick(new MIDI_SystemRealtimeEventData()));
break;
case MidiEvent.MidiStart:
if (DEBUG) UniLog.Log("* MidiStart");
Listeners.ForEach(l => l.TriggerMidiStart(new MIDI_SystemRealtimeEventData()));
break;
case MidiEvent.MidiStop:
if (DEBUG) UniLog.Log("* MidiStop");
Listeners.ForEach(l => l.TriggerMidiStop(new MIDI_SystemRealtimeEventData()));
break;
case MidiEvent.MidiContinue:
if (DEBUG) UniLog.Log("* MidiContinue");
Listeners.ForEach(l => l.TriggerMidiContinue(new MIDI_SystemRealtimeEventData()));
break;
case MidiEvent.ActiveSense:
if (DEBUG) UniLog.Log("* ActiveSense");
Listeners.ForEach(l => l.TriggerActiveSense(new MIDI_SystemRealtimeEventData()));
break;
case MidiEvent.Reset:
// Same as Meta
if (DEBUG) UniLog.Log("* Reset");
Listeners.ForEach(l => l.TriggerReset(new MIDI_SystemRealtimeEventData()));
break;
}
}
return;
timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();
}

// other types of messages: channel message (voice or channel mode), system common message, system exclusive message
if (DEBUG) UniLog.Log($"* Timestamp: {timestamp}");

var events = MidiEvent.Convert(args.Data, args.Start, args.Length);

// DO NOT CALL events.Count() IT BREAKS THE RUNNING STATUS ON LINUX

foreach (var e in events)
{
var str = e.ToString();
if (DEBUG) UniLog.Log("* " + str);
if (DEBUG)
{
var str = e.ToString();
UniLog.Log("* " + str);
}

bool shouldBuffer = false;
switch (e.EventType)
{
// System realtime
case MidiEvent.MidiClock:
if (DEBUG) UniLog.Log("* MidiClock");
Listeners.ForEach(l => l.TriggerMidiClock(new MIDI_SystemRealtimeEventData()));
break;
case MidiEvent.MidiTick:
if (DEBUG) UniLog.Log("* MidiTick");
Listeners.ForEach(l => l.TriggerMidiTick(new MIDI_SystemRealtimeEventData()));
break;
case MidiEvent.MidiStart:
if (DEBUG) UniLog.Log("* MidiStart");
Listeners.ForEach(l => l.TriggerMidiStart(new MIDI_SystemRealtimeEventData()));
break;
case MidiEvent.MidiStop:
if (DEBUG) UniLog.Log("* MidiStop");
Listeners.ForEach(l => l.TriggerMidiStop(new MIDI_SystemRealtimeEventData()));
break;
case MidiEvent.MidiContinue:
if (DEBUG) UniLog.Log("* MidiContinue");
Listeners.ForEach(l => l.TriggerMidiContinue(new MIDI_SystemRealtimeEventData()));
break;
case MidiEvent.ActiveSense:
if (DEBUG) UniLog.Log("* ActiveSense");
Listeners.ForEach(l => l.TriggerActiveSense(new MIDI_SystemRealtimeEventData()));
break;
case MidiEvent.Reset:
// Same as Meta
if (DEBUG) UniLog.Log("* Reset");
Listeners.ForEach(l => l.TriggerReset(new MIDI_SystemRealtimeEventData()));
break;

// other types of messages: channel message (voice or channel mode), system common message, system exclusive message
case MidiEvent.NoteOn:
if (DEBUG) UniLog.Log("* NoteOn");
if (e.Lsb == 0)
{
if (DEBUG) UniLog.Log("* Zero velocity, so it's actually a NoteOff");
Listeners.ForEach(l => l.TriggerNoteOff(new MIDI_NoteEventData(e.Channel, e.Msb, e.Lsb)));
return;
break;
}
Listeners.ForEach(l => l.TriggerNoteOn(new MIDI_NoteEventData(e.Channel, e.Msb, e.Lsb)));
return;
break;
case MidiEvent.NoteOff:
if (DEBUG) UniLog.Log("* NoteOff");
Listeners.ForEach(l => l.TriggerNoteOff(new MIDI_NoteEventData(e.Channel, e.Msb, e.Lsb)));
return;
break;
case MidiEvent.CAf:
if (DEBUG) UniLog.Log("* CAf");
Listeners.ForEach(l => l.TriggerChannelAftertouch(new MIDI_ChannelAftertouchEventData(e.Channel, e.Msb)));
return;
break;
case MidiEvent.Pitch:
if (DEBUG) UniLog.Log("* Pitch");
Listeners.ForEach(l => l.TriggerPitchWheel(new MIDI_PitchWheelEventData(e.Channel, CombineBytes(e.Msb, e.Lsb))));
return;
break;
case MidiEvent.PAf:
if (DEBUG) UniLog.Log("* PAf");
Listeners.ForEach(l => l.TriggerPolyphonicAftertouch(new MIDI_PolyphonicAftertouchEventData(e.Channel, e.Msb, e.Lsb)));
return;
break;

// Unhandled events:

case MidiEvent.SysEx1:
if (DEBUG) UniLog.Log("UnhandledEvent: SysEx1");
break;
case MidiEvent.SysEx2:
// AKA EndSysEx
if (DEBUG) UniLog.Log("UnhandledEvent: SysEx2");
break;
case MidiEvent.MtcQuarterFrame:
if (DEBUG) UniLog.Log("UnhandledEvent: MtcQuarterFrame");
break;
case MidiEvent.SongPositionPointer:
if (DEBUG) UniLog.Log("UnhandledEvent: SongPositionPointer");
break;
case MidiEvent.SongSelect:
if (DEBUG) UniLog.Log("UnhandledEvent: SongSelect");
break;
case MidiEvent.TuneRequest:
if (DEBUG) UniLog.Log("UnhandledEvent: TuneRequest");
break;

default:
shouldBuffer = true;
break;
}

// buffer CC messages because consecutive ones may need to be combined
// also buffer Program messages
_eventBuffer.Add(new TimestampedMidiEvent(e, args.Timestamp));
_bufferedMessagesToHandle += 1;
if (shouldBuffer)
{
// buffer CC messages because consecutive ones may need to be combined
// also buffer Program messages
lock (_eventBuffer)
{
_eventBuffer.Add(new TimestampedMidiEvent(e, timestamp));
}
}
}

if (events.Count() > 0 && args.Timestamp - _lastMessageBufferStartTime > MESSAGE_BUFFER_TIME_MILLISECONDS)
int count;
lock (_eventBuffer)
count = _eventBuffer.Count;

if (count > 0 && timestamp - _lastEventBufferStartTime > BUFFER_TIME_MILLISECONDS)
{
_lastMessageBufferStartTime = args.Timestamp;
if (DEBUG) UniLog.Log("* New message batch created: " + args.Timestamp.ToString());
await Task.Delay((int)MESSAGE_BUFFER_TIME_MILLISECONDS);
FlushMessageBuffer();
_lastEventBufferStartTime = timestamp;
if (DEBUG) UniLog.Log("* New message batch created: " + timestamp.ToString());
await Task.Delay((int)BUFFER_TIME_MILLISECONDS);
lock (_eventBuffer)
FlushMessageBuffer();
}
}
}
Expand Down
14 changes: 9 additions & 5 deletions ProjectObsidian/Settings/LocaleHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,20 @@ public static class SettingsLocaleHelper
private static StaticLocaleProvider localeProvider;
private static string lastOverrideLocale;
private const string overrideLocaleString = "somethingRandomJustToMakeItChange";
public static void Update(LocaleData _localeData)
internal static LocaleData _lastLocaleData;
public static void Update(LocaleData _localeData=null)
{
UpdateDelayed(_localeData);
Settings.RegisterValueChanges<LocaleSettings>(localeSettings => UpdateDelayed(_localeData));
var data = _localeData ?? _lastLocaleData;
if (data == null) return;
_lastLocaleData = data;
UpdateDelayed(data);
Settings.RegisterValueChanges<LocaleSettings>(localeSettings => UpdateDelayed(data));

}
private static void UpdateDelayed(LocaleData _localeData)
{
// I hate having to do an arbitrary delay, but it doesn't work otherwise
Userspace.UserspaceWorld.RunInUpdates(7, () => UpdateIntern(_localeData));
// I hate having to do an arbitrary delay
Userspace.UserspaceWorld.RunInUpdates(15, () => UpdateIntern(_localeData));
}
private static void UpdateIntern(LocaleData _localeData)
{
Expand Down
Loading