Skip to content

Commit 9e30935

Browse files
authored
Merge pull request #121 from Nytra/LinuxFixes
Fix MIDI on Linux
2 parents b578e73 + cf85deb commit 9e30935

7 files changed

Lines changed: 644 additions & 125 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,4 +352,6 @@ MigrationBackup/
352352
# Ionide (cross platform F# VS Code tools) working folder
353353
.ionide/
354354

355-
.idea
355+
.idea
356+
357+
.vscode/

ProjectObsidian/Elements/MIDI.cs

Lines changed: 126 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4-
using System.Text;
4+
using System.Runtime.InteropServices;
55
using System.Threading.Tasks;
66
using Commons.Music.Midi;
7-
using Obsidian.Components.Devices.MIDI;
87
using Elements.Core;
9-
using FrooxEngine;
108
using Elements.Data;
119

1210
namespace Obsidian.Elements;
1311

1412
public struct TimestampedMidiEvent
1513
{
16-
public MidiEvent midiEvent;
14+
public MidiEvent evt;
1715
public long timestamp;
18-
public TimestampedMidiEvent(MidiEvent _midiEvent, long _timestamp)
16+
public TimestampedMidiEvent(MidiEvent _evt, long _timestamp)
1917
{
20-
midiEvent = _midiEvent;
18+
evt = _evt;
2119
timestamp = _timestamp;
2220
}
21+
public override string ToString()
22+
{
23+
return $"{timestamp}, {evt}";
24+
}
2325
}
2426

2527
public interface IMidiInputListener
@@ -65,23 +67,19 @@ public class MidiInputConnection
6567

6668
public List<IMidiInputListener> Listeners = new();
6769

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

71-
private const long MESSAGE_BUFFER_TIME_MILLISECONDS = 3;
72-
73-
private long _lastMessageBufferStartTime = 0;
72+
private const long BUFFER_TIME_MILLISECONDS = 3;
7473

75-
private int _bufferedMessagesToHandle = 0;
74+
private long _lastEventBufferStartTime = 0;
7675

7776
private const bool DEBUG = false;
7877

7978
public void Initialize()
8079
{
8180
Input = null;
8281
_eventBuffer.Clear();
83-
_lastMessageBufferStartTime = 0;
84-
_bufferedMessagesToHandle = 0;
82+
_lastEventBufferStartTime = 0;
8583
Listeners.Clear();
8684
}
8785

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

9896
private bool IsCCFineMessage()
9997
{
100-
if (_eventBuffer.Count == 0) return false;
101-
long timestamp = _eventBuffer[0].timestamp;
102-
if (_eventBuffer.Count >= 2
103-
&& _eventBuffer[0].midiEvent.EventType == MidiEvent.CC && _eventBuffer[1].midiEvent.EventType == MidiEvent.CC
104-
&& _eventBuffer[0].midiEvent.Msb == _eventBuffer[1].midiEvent.Msb - 32)
98+
if (_eventBuffer.Count < 2) return false;
99+
if (_eventBuffer[0].evt.EventType == MidiEvent.CC && _eventBuffer[1].evt.EventType == MidiEvent.CC
100+
&& _eventBuffer[0].evt.Msb == _eventBuffer[1].evt.Msb - 32)
105101
{
106102
return true;
107103
}
@@ -121,175 +117,187 @@ private void FlushMessageBuffer()
121117

122118
while (_eventBuffer.Count > 0)
123119
{
124-
120+
// 14bit CC
125121
while (IsCCFineMessage())
126122
{
127-
var e1 = _eventBuffer[0].midiEvent;
123+
var e1 = _eventBuffer[0];
128124
if (DEBUG) UniLog.Log(e1.ToString());
129-
var e2 = _eventBuffer[1].midiEvent;
125+
var e2 = _eventBuffer[1];
130126
if (DEBUG) UniLog.Log(e2.ToString());
131-
var finalValue = CombineBytes(e2.Lsb, e1.Lsb);
127+
var finalValue = CombineBytes(e2.evt.Lsb, e1.evt.Lsb);
132128
if (DEBUG) UniLog.Log($"CC fine. Value: " + finalValue.ToString());
133-
Listeners.ForEach(l => l.TriggerControl(new MIDI_CC_EventData(e1.Channel, e1.Msb, finalValue, _coarse: false)));
129+
Listeners.ForEach(l => l.TriggerControl(new MIDI_CC_EventData(e1.evt.Channel, e1.evt.Msb, finalValue, _coarse: false)));
134130
_eventBuffer.RemoveRange(0, 2);
135-
_bufferedMessagesToHandle -= 2;
136131
}
137132

138133
if (_eventBuffer.Count == 0) break;
139134

140-
var e = _eventBuffer[0].midiEvent;
135+
var e = _eventBuffer[0];
141136
if (DEBUG) UniLog.Log(e.ToString());
142-
switch (e.EventType)
137+
switch (_eventBuffer[0].evt.EventType)
143138
{
144139
case MidiEvent.CC:
145140
if (DEBUG) UniLog.Log("CC");
146-
Listeners.ForEach(l => l.TriggerControl(new MIDI_CC_EventData(e.Channel, e.Msb, e.Lsb, _coarse: true)));
141+
Listeners.ForEach(l => l.TriggerControl(new MIDI_CC_EventData(e.evt.Channel, e.evt.Msb, e.evt.Lsb, _coarse: true)));
147142
break;
143+
148144
// 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
149145
case MidiEvent.Program:
150146
if (DEBUG) UniLog.Log("Program");
151-
Listeners.ForEach(l => l.TriggerProgram(new MIDI_ProgramEventData(e.Channel, e.Msb)));
147+
Listeners.ForEach(l => l.TriggerProgram(new MIDI_ProgramEventData(e.evt.Channel, e.evt.Msb)));
152148
break;
153149

154-
// Unhandled events:
155-
156-
//SysEx events are probably not worth handling
157-
case MidiEvent.SysEx1:
158-
if (DEBUG) UniLog.Log("UnhandledEvent: SysEx1");
159-
break;
160-
case MidiEvent.SysEx2:
161-
// Same as EndSysEx
162-
if (DEBUG) UniLog.Log("UnhandledEvent: SysEx2");
163-
break;
164-
case MidiEvent.MtcQuarterFrame:
165-
if (DEBUG) UniLog.Log("UnhandledEvent: MtcQuarterFrame");
166-
break;
167-
case MidiEvent.SongPositionPointer:
168-
if (DEBUG) UniLog.Log("UnhandledEvent: SongPositionPointer");
169-
break;
170-
case MidiEvent.SongSelect:
171-
if (DEBUG) UniLog.Log("UnhandledEvent: SongSelect");
172-
break;
173-
case MidiEvent.TuneRequest:
174-
if (DEBUG) UniLog.Log("UnhandledEvent: TuneRequest");
175-
break;
176150
default:
151+
// This should never happen
152+
if (DEBUG) UniLog.Log($"Unrecognized event type! {_eventBuffer[0].evt.EventType}");
177153
break;
178154
}
179155
_eventBuffer.RemoveAt(0);
180-
_bufferedMessagesToHandle -= 1;
181156
}
182157
if (DEBUG) UniLog.Log("Finished flushing message buffer from start time: " + batchStartTime.ToString());
183-
if (_bufferedMessagesToHandle != 0)
184-
{
185-
// Just in case some messages got lost somehow
186-
UniLog.Warning("Did not handle all buffered messages! " + _bufferedMessagesToHandle.ToString());
187-
}
188-
_bufferedMessagesToHandle = 0;
189158
}
190159

191160
public async void OnMessageReceived(object sender, MidiReceivedEventArgs args)
192161
{
162+
if (DEBUG) UniLog.FlushEveryMessage = true;
163+
193164
if (DEBUG) UniLog.Log($"*** New midi message");
194165
if (DEBUG) UniLog.Log($"* Received {args.Length} bytes");
195-
if (DEBUG) UniLog.Log($"* Timestamp: {args.Timestamp}");
166+
if (DEBUG) UniLog.Log($"* Start: {args.Start}");
196167

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

199-
if (args.Length == 1)
170+
long timestamp = args.Timestamp;
171+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) // Timestamp is always zero on Linux with the Alsa Midi API
200172
{
201-
// system realtime message, do not buffer these, execute immediately
202-
if (DEBUG) UniLog.Log($"* System realtime message");
203-
foreach (var e in events)
204-
{
205-
var str = e.ToString();
206-
if (DEBUG) UniLog.Log("* " + str);
207-
switch (e.StatusByte)
208-
{
209-
case MidiEvent.MidiClock:
210-
if (DEBUG) UniLog.Log("* MidiClock");
211-
Listeners.ForEach(l => l.TriggerMidiClock(new MIDI_SystemRealtimeEventData()));
212-
break;
213-
case MidiEvent.MidiTick:
214-
if (DEBUG) UniLog.Log("* MidiTick");
215-
Listeners.ForEach(l => l.TriggerMidiTick(new MIDI_SystemRealtimeEventData()));
216-
break;
217-
case MidiEvent.MidiStart:
218-
if (DEBUG) UniLog.Log("* MidiStart");
219-
Listeners.ForEach(l => l.TriggerMidiStart(new MIDI_SystemRealtimeEventData()));
220-
break;
221-
case MidiEvent.MidiStop:
222-
if (DEBUG) UniLog.Log("* MidiStop");
223-
Listeners.ForEach(l => l.TriggerMidiStop(new MIDI_SystemRealtimeEventData()));
224-
break;
225-
case MidiEvent.MidiContinue:
226-
if (DEBUG) UniLog.Log("* MidiContinue");
227-
Listeners.ForEach(l => l.TriggerMidiContinue(new MIDI_SystemRealtimeEventData()));
228-
break;
229-
case MidiEvent.ActiveSense:
230-
if (DEBUG) UniLog.Log("* ActiveSense");
231-
Listeners.ForEach(l => l.TriggerActiveSense(new MIDI_SystemRealtimeEventData()));
232-
break;
233-
case MidiEvent.Reset:
234-
// Same as Meta
235-
if (DEBUG) UniLog.Log("* Reset");
236-
Listeners.ForEach(l => l.TriggerReset(new MIDI_SystemRealtimeEventData()));
237-
break;
238-
}
239-
}
240-
return;
173+
timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();
241174
}
242175

243-
// other types of messages: channel message (voice or channel mode), system common message, system exclusive message
176+
if (DEBUG) UniLog.Log($"* Timestamp: {timestamp}");
177+
178+
var events = MidiEvent.Convert(args.Data, args.Start, args.Length);
179+
180+
// DO NOT CALL events.Count() IT BREAKS THE RUNNING STATUS ON LINUX
181+
244182
foreach (var e in events)
245183
{
246-
var str = e.ToString();
247-
if (DEBUG) UniLog.Log("* " + str);
184+
if (DEBUG)
185+
{
186+
var str = e.ToString();
187+
UniLog.Log("* " + str);
188+
}
248189

190+
bool shouldBuffer = false;
249191
switch (e.EventType)
250192
{
193+
// System realtime
194+
case MidiEvent.MidiClock:
195+
if (DEBUG) UniLog.Log("* MidiClock");
196+
Listeners.ForEach(l => l.TriggerMidiClock(new MIDI_SystemRealtimeEventData()));
197+
break;
198+
case MidiEvent.MidiTick:
199+
if (DEBUG) UniLog.Log("* MidiTick");
200+
Listeners.ForEach(l => l.TriggerMidiTick(new MIDI_SystemRealtimeEventData()));
201+
break;
202+
case MidiEvent.MidiStart:
203+
if (DEBUG) UniLog.Log("* MidiStart");
204+
Listeners.ForEach(l => l.TriggerMidiStart(new MIDI_SystemRealtimeEventData()));
205+
break;
206+
case MidiEvent.MidiStop:
207+
if (DEBUG) UniLog.Log("* MidiStop");
208+
Listeners.ForEach(l => l.TriggerMidiStop(new MIDI_SystemRealtimeEventData()));
209+
break;
210+
case MidiEvent.MidiContinue:
211+
if (DEBUG) UniLog.Log("* MidiContinue");
212+
Listeners.ForEach(l => l.TriggerMidiContinue(new MIDI_SystemRealtimeEventData()));
213+
break;
214+
case MidiEvent.ActiveSense:
215+
if (DEBUG) UniLog.Log("* ActiveSense");
216+
Listeners.ForEach(l => l.TriggerActiveSense(new MIDI_SystemRealtimeEventData()));
217+
break;
218+
case MidiEvent.Reset:
219+
// Same as Meta
220+
if (DEBUG) UniLog.Log("* Reset");
221+
Listeners.ForEach(l => l.TriggerReset(new MIDI_SystemRealtimeEventData()));
222+
break;
223+
224+
// other types of messages: channel message (voice or channel mode), system common message, system exclusive message
251225
case MidiEvent.NoteOn:
252226
if (DEBUG) UniLog.Log("* NoteOn");
253227
if (e.Lsb == 0)
254228
{
255229
if (DEBUG) UniLog.Log("* Zero velocity, so it's actually a NoteOff");
256230
Listeners.ForEach(l => l.TriggerNoteOff(new MIDI_NoteEventData(e.Channel, e.Msb, e.Lsb)));
257-
return;
231+
break;
258232
}
259233
Listeners.ForEach(l => l.TriggerNoteOn(new MIDI_NoteEventData(e.Channel, e.Msb, e.Lsb)));
260-
return;
234+
break;
261235
case MidiEvent.NoteOff:
262236
if (DEBUG) UniLog.Log("* NoteOff");
263237
Listeners.ForEach(l => l.TriggerNoteOff(new MIDI_NoteEventData(e.Channel, e.Msb, e.Lsb)));
264-
return;
238+
break;
265239
case MidiEvent.CAf:
266240
if (DEBUG) UniLog.Log("* CAf");
267241
Listeners.ForEach(l => l.TriggerChannelAftertouch(new MIDI_ChannelAftertouchEventData(e.Channel, e.Msb)));
268-
return;
242+
break;
269243
case MidiEvent.Pitch:
270244
if (DEBUG) UniLog.Log("* Pitch");
271245
Listeners.ForEach(l => l.TriggerPitchWheel(new MIDI_PitchWheelEventData(e.Channel, CombineBytes(e.Msb, e.Lsb))));
272-
return;
246+
break;
273247
case MidiEvent.PAf:
274248
if (DEBUG) UniLog.Log("* PAf");
275249
Listeners.ForEach(l => l.TriggerPolyphonicAftertouch(new MIDI_PolyphonicAftertouchEventData(e.Channel, e.Msb, e.Lsb)));
276-
return;
250+
break;
251+
252+
// Unhandled events:
253+
254+
case MidiEvent.SysEx1:
255+
if (DEBUG) UniLog.Log("UnhandledEvent: SysEx1");
256+
break;
257+
case MidiEvent.SysEx2:
258+
// AKA EndSysEx
259+
if (DEBUG) UniLog.Log("UnhandledEvent: SysEx2");
260+
break;
261+
case MidiEvent.MtcQuarterFrame:
262+
if (DEBUG) UniLog.Log("UnhandledEvent: MtcQuarterFrame");
263+
break;
264+
case MidiEvent.SongPositionPointer:
265+
if (DEBUG) UniLog.Log("UnhandledEvent: SongPositionPointer");
266+
break;
267+
case MidiEvent.SongSelect:
268+
if (DEBUG) UniLog.Log("UnhandledEvent: SongSelect");
269+
break;
270+
case MidiEvent.TuneRequest:
271+
if (DEBUG) UniLog.Log("UnhandledEvent: TuneRequest");
272+
break;
273+
277274
default:
275+
shouldBuffer = true;
278276
break;
279277
}
280278

281-
// buffer CC messages because consecutive ones may need to be combined
282-
// also buffer Program messages
283-
_eventBuffer.Add(new TimestampedMidiEvent(e, args.Timestamp));
284-
_bufferedMessagesToHandle += 1;
279+
if (shouldBuffer)
280+
{
281+
// buffer CC messages because consecutive ones may need to be combined
282+
// also buffer Program messages
283+
lock (_eventBuffer)
284+
{
285+
_eventBuffer.Add(new TimestampedMidiEvent(e, timestamp));
286+
}
287+
}
285288
}
286289

287-
if (events.Count() > 0 && args.Timestamp - _lastMessageBufferStartTime > MESSAGE_BUFFER_TIME_MILLISECONDS)
290+
int count;
291+
lock (_eventBuffer)
292+
count = _eventBuffer.Count;
293+
294+
if (count > 0 && timestamp - _lastEventBufferStartTime > BUFFER_TIME_MILLISECONDS)
288295
{
289-
_lastMessageBufferStartTime = args.Timestamp;
290-
if (DEBUG) UniLog.Log("* New message batch created: " + args.Timestamp.ToString());
291-
await Task.Delay((int)MESSAGE_BUFFER_TIME_MILLISECONDS);
292-
FlushMessageBuffer();
296+
_lastEventBufferStartTime = timestamp;
297+
if (DEBUG) UniLog.Log("* New message batch created: " + timestamp.ToString());
298+
await Task.Delay((int)BUFFER_TIME_MILLISECONDS);
299+
lock (_eventBuffer)
300+
FlushMessageBuffer();
293301
}
294302
}
295303
}

ProjectObsidian/Settings/LocaleHelper.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,20 @@ public static class SettingsLocaleHelper
1010
private static StaticLocaleProvider localeProvider;
1111
private static string lastOverrideLocale;
1212
private const string overrideLocaleString = "somethingRandomJustToMakeItChange";
13-
public static void Update(LocaleData _localeData)
13+
internal static LocaleData _lastLocaleData;
14+
public static void Update(LocaleData _localeData=null)
1415
{
15-
UpdateDelayed(_localeData);
16-
Settings.RegisterValueChanges<LocaleSettings>(localeSettings => UpdateDelayed(_localeData));
16+
var data = _localeData ?? _lastLocaleData;
17+
if (data == null) return;
18+
_lastLocaleData = data;
19+
UpdateDelayed(data);
20+
Settings.RegisterValueChanges<LocaleSettings>(localeSettings => UpdateDelayed(data));
1721

1822
}
1923
private static void UpdateDelayed(LocaleData _localeData)
2024
{
21-
// I hate having to do an arbitrary delay, but it doesn't work otherwise
22-
Userspace.UserspaceWorld.RunInUpdates(7, () => UpdateIntern(_localeData));
25+
// I hate having to do an arbitrary delay
26+
Userspace.UserspaceWorld.RunInUpdates(15, () => UpdateIntern(_localeData));
2327
}
2428
private static void UpdateIntern(LocaleData _localeData)
2529
{

0 commit comments

Comments
 (0)