diff --git a/ProjectObsidian.SourceGenerators/BindingGenerator.cs b/ProjectObsidian.SourceGenerators/BindingGenerator.cs index 4c57764..57822ea 100644 --- a/ProjectObsidian.SourceGenerators/BindingGenerator.cs +++ b/ProjectObsidian.SourceGenerators/BindingGenerator.cs @@ -118,14 +118,15 @@ public OrderedCount(string countVariableName, string methodName, string methodRe private readonly OrderedCount _outputCount = new("NodeOutputCount", "GetOutputInternal", "INodeOutput"); private readonly OrderedCount _impulseCount = new("NodeImpulseCount", "GetImpulseInternal", "ISyncRef"); private readonly OrderedCount _operationCount = new("NodeOperationCount", "GetOperationInternal", "INodeOperation"); - + private readonly OrderedCount _globalRefCount = new("NodeGlobalRefCount", "GetGlobalRefInternal", "ISyncRef"); + private readonly OrderedCount _inputListCount = new("NodeInputListCount", "GetInputListInternal", "ISyncList"); private readonly OrderedCount _outputListCount = new("NodeOutputListCount", "GetOutputListInternal", "ISyncList"); private readonly OrderedCount _impulseListCount = new("NodeImpulseListCount", "GetImpulseListInternal", "ISyncList"); private readonly OrderedCount _operationListCount = new("NodeOperationListCount", "GetOperationListInternal", "ISyncList"); private IEnumerable _counts => new[] - { _inputCount, _outputCount, _impulseCount, _operationCount, + { _inputCount, _outputCount, _impulseCount, _operationCount, _globalRefCount, _inputListCount, _outputListCount, _impulseListCount, _operationListCount }; private string CountOverride => string.Concat(_counts.Select(i => i.CountOverride)); @@ -224,7 +225,10 @@ public override void VisitFieldDeclaration(FieldDeclarationSyntax node) UntypedFieldDetection(type, name, "Call", "global::FrooxEngine.SyncRef", _impulseCount); UntypedFieldDetection(type, name, "Continuation", "global::FrooxEngine.SyncRef", _impulseCount); UntypedFieldDetection(type, name, "AsyncResumption", "global::FrooxEngine.SyncRef", _impulseCount); - + + //global refs + TypedFieldDetection(type, name, "GlobalRef", "global::FrooxEngine.SyncRef>", _globalRefCount); + //operations UntypedFieldDetection(type, name, "Operation", "global::FrooxEngine.ProtoFlux.SyncNodeOperation", _operationCount); diff --git a/ProjectObsidian/Components/Devices/MIDI_CC_Value.cs b/ProjectObsidian/Components/Devices/MIDI_CC_Value.cs new file mode 100644 index 0000000..8bbf2c5 --- /dev/null +++ b/ProjectObsidian/Components/Devices/MIDI_CC_Value.cs @@ -0,0 +1,114 @@ +using Elements.Core; +using FrooxEngine; +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Collections.Generic; +using Commons.Music.Midi.RtMidi; +using CoreMidi; +using Commons.Music.Midi; +using Obsidian.Elements; + +namespace Obsidian; + +[Category(new string[] { "Obsidian/Devices" })] +public class MIDI_CC_Value : Component +{ + public readonly SyncRef InputDevice; + + public readonly Sync AutoMap; + + public readonly Sync Channel; + + public readonly Sync ControllerNumber; + + public readonly Sync OverrideDefinition; + + public readonly Sync Value; + + public readonly Sync NormalizedValue; + + private MIDI_InputDevice _device; + + protected override void OnStart() + { + base.OnStart(); + InputDevice.OnTargetChange += OnTargetChange; + if (InputDevice.Target != null) + { + _device = InputDevice.Target; + InputDevice.Target.Control += OnControl; + } + } + + protected override void OnDispose() + { + base.OnDispose(); + if (_device != null) + { + _device.Control -= OnControl; + _device = null; + } + } + + private void OnControl(MIDI_InputDevice device, MIDI_CC_EventData eventData) + { + RunSynchronously(() => + { + if (AutoMap.Value) + { + AutoMap.Value = false; + ControllerNumber.Value = eventData.controller; + Channel.Value = eventData.channel; + if (OverrideDefinition.Value.HasValue) + { + if (Enum.IsDefined(typeof(MIDI_CC_Definition), eventData.controller)) + { + OverrideDefinition.Value = (MIDI_CC_Definition)Enum.ToObject(typeof(MIDI_CC_Definition), eventData.controller); + } + else + { + OverrideDefinition.Value = MIDI_CC_Definition.UNDEFINED; + } + } + } + if (Channel.Value == eventData.channel) + { + if (OverrideDefinition.Value.HasValue && OverrideDefinition.Value.Value != MIDI_CC_Definition.UNDEFINED) + { + if (eventData.controller == (int)OverrideDefinition.Value.Value) + { + Value.Value = eventData.value; + NormalizedValue.Value = eventData.value / 127f; + } + } + else + { + if (eventData.controller == ControllerNumber.Value) + { + Value.Value = eventData.value; + NormalizedValue.Value = eventData.value / 127f; + } + } + } + }); + } + + private void OnTargetChange(SyncRef syncRef) + { + if (syncRef.Target == null && _device != null) + { + _device.Control -= OnControl; + _device = null; + } + else if (syncRef.Target != null) + { + if (_device != null) + { + _device.Control -= OnControl; + } + _device = syncRef.Target; + _device.Control += OnControl; + } + } +} \ No newline at end of file diff --git a/ProjectObsidian/Components/Devices/MIDI_InputDevice.cs b/ProjectObsidian/Components/Devices/MIDI_InputDevice.cs new file mode 100644 index 0000000..5bd246d --- /dev/null +++ b/ProjectObsidian/Components/Devices/MIDI_InputDevice.cs @@ -0,0 +1,279 @@ +using Elements.Core; +using FrooxEngine; +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Collections.Generic; +using Commons.Music.Midi.RtMidi; +using CoreMidi; +using Commons.Music.Midi; +using Obsidian.Elements; + +namespace Obsidian; + +[Category(new string[] { "Obsidian/Devices" })] +public class MIDI_InputDevice : Component +{ + [NoContinuousParsing] + public readonly Sync DeviceName; + + public readonly Sync IsConnected; + + public readonly UserRef HandlingUser; + + public readonly Sync _lastEvent; + + private bool _lastIsConnected; + + private IMidiInput _inputDevice; + + private MIDI_Settings _settings => Settings.GetActiveSetting(); + + public event MIDI_NoteEventHandler NoteOn; + + public event MIDI_NoteEventHandler NoteOff; + + // Pressure for whole keyboard + public event MIDI_ChannelPressureEventHandler ChannelPressure; + + // Pressure for individual notes (polyphonic) + public event MIDI_AftertouchEventHandler Aftertouch; + + public event MIDI_CC_EventHandler Control; + + public event MIDI_PitchWheelEventHandler PitchWheel; + + private const bool DEBUG = false; + + protected override void OnStart() + { + base.OnStart(); + Settings.GetActiveSetting(); + Settings.RegisterValueChanges(OnInputDeviceSettingsChanged); + RunInUpdates(7, Update); + } + + private void OnInputDeviceSettingsChanged(MIDI_Settings setting) + { + UniLog.Log("MIDI Settings Changed!"); + MarkChangeDirty(); + } + + protected override void OnChanges() + { + //UniLog.Log("OnChanges"); + base.OnChanges(); + if (_lastEvent.WasChanged) + { + _lastEvent.WasChanged = false; + return; + } + if (IsConnected.WasChanged) + { + IsConnected.Value = _lastIsConnected; + IsConnected.WasChanged = false; + return; + } + Update(); + } + + private async void ReleaseDeviceAsync() + { + UniLog.Log("Releasing device..."); + await _inputDevice.CloseAsync(); + UniLog.Log("Device released."); + _inputDevice = null; + } + + protected override void OnDispose() + { + base.OnDispose(); + if (_inputDevice != null) + { + ReleaseDeviceAsync(); + } + } + + private void SetIsConnected(bool val) + { + IsConnected.Value = val; + _lastIsConnected = val; + } + + private void Update() + { + if (HandlingUser.Target == null) + { + if (_inputDevice != null) + { + ReleaseDeviceAsync(); + } + SetIsConnected(false); + return; + } + + if (LocalUser != HandlingUser.Target) + { + if (_inputDevice != null) + { + ReleaseDeviceAsync(); + } + return; + } + + if (!Enabled) + { + if (_inputDevice != null) + { + ReleaseDeviceAsync(); + } + SetIsConnected(false); + return; + } + + if (!string.IsNullOrWhiteSpace(DeviceName)) + { + UniLog.Log("Device name: " + DeviceName.Value); + if (!_settings.InputDevices.Any(dev => dev.DeviceName.Value == DeviceName.Value && dev.AllowConnections.Value == true)) + { + UniLog.Log("Device connection not allowed."); + if (_inputDevice != null) + { + ReleaseDeviceAsync(); + } + SetIsConnected(false); + return; + } + + if (_inputDevice != null + && (_inputDevice.Connection == MidiPortConnectionState.Open || _inputDevice.Connection == MidiPortConnectionState.Pending) + && _inputDevice.Details.Name == DeviceName.Value) + { + UniLog.Log("Already connected."); + return; + } + + var access = MidiAccessManager.Default; + var targetDevice = access.Inputs.FirstOrDefault(dev => dev.Name == DeviceName.Value); + if (targetDevice != null) + { + UniLog.Log("Found the target device."); + _inputDevice = access.OpenInputAsync(targetDevice.Id).Result; + _inputDevice.MessageReceived += OnMessageReceived; + SetIsConnected(true); + UniLog.Log("Connected."); + } + else + { + UniLog.Log("Could not find target device."); + SetIsConnected(false); + } + } + else + { + if (_inputDevice != null) + { + ReleaseDeviceAsync(); + } + SetIsConnected(false); + } + } + + // What + private ushort CombineBytes(byte First, byte Second) + { + ushort _14bit; + _14bit = Second; + _14bit <<= 7; + _14bit |= First; + return _14bit; + } + + private void OnMessageReceived(object sender, MidiReceivedEventArgs args) + { + 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); + foreach (var e in events) + { + if (DEBUG) UniLog.Log(e.ToString()); + RunSynchronously(() => + { + _lastEvent.Value = e.ToString(); + }); + switch (e.EventType) + { + case MidiEvent.NoteOn: + NoteOn?.Invoke(this, new MIDI_NoteEventData(e.Channel, e.Msb, e.Lsb)); + break; + case MidiEvent.NoteOff: + NoteOff?.Invoke(this, new MIDI_NoteEventData(e.Channel, e.Msb, e.Lsb)); + break; + case MidiEvent.CAf: + ChannelPressure?.Invoke(this, new MIDI_ChannelPressureEventData(e.Channel, e.Msb)); + break; + case MidiEvent.CC: + Control?.Invoke(this, new MIDI_CC_EventData(e.Channel, e.Msb, e.Lsb)); + break; + case MidiEvent.Pitch: + PitchWheel?.Invoke(this, new MIDI_PitchWheelEventData(e.Channel, CombineBytes(e.Msb, e.Lsb))); + break; + case MidiEvent.PAf: + Aftertouch?.Invoke(this, new MIDI_AftertouchEventData(e.Channel, e.Msb, e.Lsb)); + 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.Program: + if (DEBUG) UniLog.Log("UnhandledEvent: Program"); + 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; + case MidiEvent.MidiClock: + if (DEBUG) UniLog.Log("UnhandledEvent: Clock"); + break; + case MidiEvent.MidiTick: + if (DEBUG) UniLog.Log("UnhandledEvent: MidiTick"); + break; + case MidiEvent.MidiStart: + if (DEBUG) UniLog.Log("UnhandledEvent: MidiStart"); + break; + case MidiEvent.MidiStop: + if (DEBUG) UniLog.Log("UnhandledEvent: MidiStart"); + break; + case MidiEvent.MidiContinue: + if (DEBUG) UniLog.Log("UnhandledEvent: MidiContinue"); + break; + case MidiEvent.ActiveSense: + if (DEBUG) UniLog.Log("UnhandledEvent: ActiveSense"); + break; + case MidiEvent.Reset: + // Same as Meta + if (DEBUG) UniLog.Log("UnhandledEvent: Reset"); + break; + default: + break; + } + } + } +} \ No newline at end of file diff --git a/ProjectObsidian/Components/Devices/MIDI_PitchWheel_Value.cs b/ProjectObsidian/Components/Devices/MIDI_PitchWheel_Value.cs new file mode 100644 index 0000000..3280950 --- /dev/null +++ b/ProjectObsidian/Components/Devices/MIDI_PitchWheel_Value.cs @@ -0,0 +1,78 @@ +using Elements.Core; +using FrooxEngine; +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Collections.Generic; +using Commons.Music.Midi.RtMidi; +using CoreMidi; +using Commons.Music.Midi; +using Obsidian.Elements; +using System.Runtime.Remoting.Contexts; + +namespace Obsidian; + +[Category(new string[] { "Obsidian/Devices" })] +public class MIDI_PitchWheel_Value : Component +{ + public readonly SyncRef InputDevice; + + public readonly Sync Channel; + + public readonly Sync Value; + + public readonly Sync NormalizedValue; + + private MIDI_InputDevice _device; + + protected override void OnStart() + { + base.OnStart(); + InputDevice.OnTargetChange += OnTargetChange; + if (InputDevice.Target != null) + { + _device = InputDevice.Target; + InputDevice.Target.PitchWheel += OnPitchWheel; + } + } + + protected override void OnDispose() + { + base.OnDispose(); + if (_device != null) + { + _device.PitchWheel -= OnPitchWheel; + _device = null; + } + } + + private void OnPitchWheel(MIDI_InputDevice device, MIDI_PitchWheelEventData eventData) + { + RunSynchronously(() => + { + if (eventData.channel == Channel.Value) + { + Value.Value = eventData.value; + NormalizedValue.Value = eventData.value == 8192 ? 0f : MathX.Remap(eventData.value, 0f, 16383f, -1f, 1f); + } + }); + } + + private void OnTargetChange(SyncRef syncRef) + { + if (syncRef.Target == null && _device != null) + { + _device.PitchWheel -= OnPitchWheel; + _device = null; + } + else if (syncRef.Target != null) + { + if (_device != null) + { + _device.PitchWheel -= OnPitchWheel; + } + _device = syncRef.Target; + _device.PitchWheel += OnPitchWheel; + } + } +} \ No newline at end of file diff --git a/ProjectObsidian/Elements/MIDI.cs b/ProjectObsidian/Elements/MIDI.cs new file mode 100644 index 0000000..f73f45e --- /dev/null +++ b/ProjectObsidian/Elements/MIDI.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Elements.Core; + +namespace Obsidian.Elements; + +[DataModelType] +public enum MIDI_CC_Definition +{ + UNDEFINED = 999, + BankSelect = 0, + Modulation = 1, + Breath = 2, + Foot = 4, + PortamentoTime = 5, + DteMsb = 6, + Volume = 7, + Balance = 8, + Pan = 10, + Expression = 11, + EffectControl1 = 12, + EffectControl2 = 13, + General1 = 16, + General2 = 17, + General3 = 18, + General4 = 19, + BankSelectLsb = 32, + ModulationLsb = 33, + BreathLsb = 34, + FootLsb = 36, + PortamentoTimeLsb = 37, + DteLsb = 38, + VolumeLsb = 39, + BalanceLsb = 40, + PanLsb = 42, + ExpressionLsb = 43, + Effect1Lsb = 44, + Effect2Lsb = 45, + General1Lsb = 48, + General2Lsb = 49, + General3Lsb = 50, + General4Lsb = 51, + Hold = 64, + PortamentoSwitch = 65, + Sostenuto = 66, + SoftPedal = 67, + Legato = 68, + Hold2 = 69, + SoundController1 = 70, + SoundController2 = 71, + SoundController3 = 72, + SoundController4 = 73, + SoundController5 = 74, + SoundController6 = 75, + SoundController7 = 76, + SoundController8 = 77, + SoundController9 = 78, + SoundController10 = 79, + General5 = 80, + General6 = 81, + General7 = 82, + General8 = 83, + PortamentoControl = 84, + Rsd = 91, + Effect1 = 91, + Tremolo = 92, + Effect2 = 92, + Csd = 93, + Effect3 = 93, + Celeste = 94, //detune + Effect4 = 94, + Phaser = 95, + Effect5 = 95, + DteIncrement = 96, + DteDecrement = 97, + NrpnLsb = 98, + NrpnMsb = 99, + RpnLsb = 100, + RpnMsb = 101, + AllSoundOff = 120, + ResetAllControllers = 121, + LocalControl = 122, + AllNotesOff = 123, + OmniModeOff = 124, + OmniModeOn = 125, + PolyModeOnOff = 126, + PolyModeOn = 127 +} + +[DataModelType] +public readonly struct MIDI_PitchWheelEventData +{ + public readonly int channel; + + public readonly int value; + + public MIDI_PitchWheelEventData(in int _channel, in int _value) + { + channel = _channel; + value = _value; + } +} + +[DataModelType] +public readonly struct MIDI_NoteEventData +{ + public readonly int channel; + + public readonly int note; + + public readonly int velocity; + + public MIDI_NoteEventData(in int _channel, in int _note, in int _velocity) + { + channel = _channel; + note = _note; + velocity = _velocity; + } +} + +[DataModelType] +public readonly struct MIDI_ChannelPressureEventData +{ + public readonly int channel; + + public readonly int pressure; + + public MIDI_ChannelPressureEventData(in int _channel, in int _pressure) + { + channel = _channel; + pressure = _pressure; + } +} + +[DataModelType] +public readonly struct MIDI_AftertouchEventData +{ + public readonly int channel; + + public readonly int note; + + public readonly int pressure; + + public MIDI_AftertouchEventData(in int _channel, in int _note, in int _pressure) + { + channel = _channel; + note = _note; + pressure = _pressure; + } +} + +[DataModelType] +public readonly struct MIDI_CC_EventData +{ + public readonly int channel; + + public readonly int controller; + + public readonly int value; + + public MIDI_CC_EventData(in int _channel, in int _controller, in int _value) + { + channel = _channel; + controller = _controller; + value = _value; + } +} + +[DataModelType] +public delegate void MIDI_NoteEventHandler(MIDI_InputDevice device, MIDI_NoteEventData eventData); + +[DataModelType] +public delegate void MIDI_ChannelPressureEventHandler(MIDI_InputDevice device, MIDI_ChannelPressureEventData eventData); + +[DataModelType] +public delegate void MIDI_AftertouchEventHandler(MIDI_InputDevice device, MIDI_AftertouchEventData eventData); + +[DataModelType] +public delegate void MIDI_CC_EventHandler(MIDI_InputDevice device, MIDI_CC_EventData eventData); + +[DataModelType] +public delegate void MIDI_PitchWheelEventHandler(MIDI_InputDevice device, MIDI_PitchWheelEventData eventData); \ No newline at end of file diff --git a/ProjectObsidian/ProjectObsidian.csproj b/ProjectObsidian/ProjectObsidian.csproj index f7b1f11..6fe136e 100644 --- a/ProjectObsidian/ProjectObsidian.csproj +++ b/ProjectObsidian/ProjectObsidian.csproj @@ -70,6 +70,7 @@ + diff --git a/ProjectObsidian/ProtoFlux/Devices/MIDI_AftertouchEvent.cs b/ProjectObsidian/ProtoFlux/Devices/MIDI_AftertouchEvent.cs new file mode 100644 index 0000000..93f7955 --- /dev/null +++ b/ProjectObsidian/ProtoFlux/Devices/MIDI_AftertouchEvent.cs @@ -0,0 +1,91 @@ +using System; +using System.Linq; +using Newtonsoft.Json.Linq; +using ProtoFlux.Core; +using ProtoFlux.Runtimes.Execution; +using Elements.Core; +using FrooxEngine; +using FrooxEngine.ProtoFlux; +using Obsidian.Elements; +using Obsidian; + +namespace ProtoFlux.Runtimes.Execution.Nodes.Obsidian.Devices; + +[NodeName("MIDI Polyphonic Aftertouch Event")] +[NodeCategory("Obsidian/Devices")] +public class MIDI_AftertouchEvent : VoidNode +{ + public readonly GlobalRef Device; + + public Call Aftertouch; + + public readonly ValueOutput Channel; + + public readonly ValueOutput Note; + + public readonly ValueOutput Pressure; + + public readonly ValueOutput NormalizedPressure; + + private ObjectStore _currentDevice; + + private ObjectStore _aftertouch; + + public override bool CanBeEvaluated => false; + + private void OnDeviceChanged(MIDI_InputDevice device, FrooxEngineContext context) + { + MIDI_InputDevice device2 = _currentDevice.Read(context); + if (device == device2) + { + return; + } + if (device2 != null) + { + device2.Aftertouch -= _aftertouch.Read(context); + } + if (device != null) + { + NodeContextPath path = context.CaptureContextPath(); + context.GetEventDispatcher(out var dispatcher); + MIDI_AftertouchEventHandler value3 = delegate (MIDI_InputDevice dev, MIDI_AftertouchEventData e) + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + OnAftertouch(dev, in e, c); + }); + }; + _currentDevice.Write(device, context); + _aftertouch.Write(value3, context); + device.Aftertouch += value3; + } + else + { + _currentDevice.Clear(context); + _aftertouch.Clear(context); + } + } + + private void WriteAftertouchEventData(in MIDI_AftertouchEventData eventData, FrooxEngineContext context) + { + Channel.Write(eventData.channel, context); + Note.Write(eventData.note, context); + Pressure.Write(eventData.pressure, context); + NormalizedPressure.Write(eventData.pressure / 127f, context); + } + + private void OnAftertouch(MIDI_InputDevice device, in MIDI_AftertouchEventData eventData, FrooxEngineContext context) + { + WriteAftertouchEventData(in eventData, context); + Aftertouch.Execute(context); + } + + public MIDI_AftertouchEvent() + { + Device = new GlobalRef(this, 0); + Channel = new ValueOutput(this); + Note = new ValueOutput(this); + Pressure = new ValueOutput(this); + NormalizedPressure = new ValueOutput(this); + } +} \ No newline at end of file diff --git a/ProjectObsidian/ProtoFlux/Devices/MIDI_CC_Event.cs b/ProjectObsidian/ProtoFlux/Devices/MIDI_CC_Event.cs new file mode 100644 index 0000000..747be92 --- /dev/null +++ b/ProjectObsidian/ProtoFlux/Devices/MIDI_CC_Event.cs @@ -0,0 +1,102 @@ +using System; +using System.Linq; +using Newtonsoft.Json.Linq; +using ProtoFlux.Core; +using ProtoFlux.Runtimes.Execution; +using Elements.Core; +using FrooxEngine; +using FrooxEngine.ProtoFlux; +using Obsidian.Elements; +using Obsidian; + +namespace ProtoFlux.Runtimes.Execution.Nodes.Obsidian.Devices; + +[NodeName("MIDI CC Event")] +[NodeCategory("Obsidian/Devices")] +public class MIDI_CC_Event : VoidNode +{ + public readonly GlobalRef Device; + + public Call ControlChange; + + public readonly ValueOutput Channel; + + public readonly ValueOutput Controller; + + public readonly ValueOutput ControllerDefinition; + + public readonly ValueOutput Value; + + public readonly ValueOutput NormalizedValue; + + private ObjectStore _currentDevice; + + private ObjectStore _controlChange; + + public override bool CanBeEvaluated => false; + + private void OnDeviceChanged(MIDI_InputDevice device, FrooxEngineContext context) + { + MIDI_InputDevice device2 = _currentDevice.Read(context); + if (device == device2) + { + return; + } + if (device2 != null) + { + device2.Control -= _controlChange.Read(context); + } + if (device != null) + { + NodeContextPath path = context.CaptureContextPath(); + context.GetEventDispatcher(out var dispatcher); + MIDI_CC_EventHandler value3 = delegate (MIDI_InputDevice dev, MIDI_CC_EventData e) + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + OnControl(dev, in e, c); + }); + }; + _currentDevice.Write(device, context); + _controlChange.Write(value3, context); + device.Control += value3; + } + else + { + _currentDevice.Clear(context); + _controlChange.Clear(context); + } + } + + private void WriteCCEventData(in MIDI_CC_EventData eventData, FrooxEngineContext context) + { + Channel.Write(eventData.channel, context); + Controller.Write(eventData.controller, context); + if (Enum.IsDefined(typeof(MIDI_CC_Definition), eventData.controller)) + { + ControllerDefinition.Write((MIDI_CC_Definition)Enum.ToObject(typeof(MIDI_CC_Definition), eventData.controller), context); + } + else + { + ControllerDefinition.Write(MIDI_CC_Definition.UNDEFINED, context); + } + Value.Write(eventData.value, context); + NormalizedValue.Write(eventData.value / 127f, context); + } + + private void OnControl(MIDI_InputDevice device, in MIDI_CC_EventData eventData, FrooxEngineContext context) + { + WriteCCEventData(in eventData, context); + ControlChange.Execute(context); + } + + public MIDI_CC_Event() + { + Device = new GlobalRef(this, 0); + Channel = new ValueOutput(this); + Controller = new ValueOutput(this); + ControllerDefinition = new ValueOutput(this); + Value = new ValueOutput(this); + NormalizedValue = new ValueOutput(this); + } +} \ No newline at end of file diff --git a/ProjectObsidian/ProtoFlux/Devices/MIDI_ChannelPressureEvent.cs b/ProjectObsidian/ProtoFlux/Devices/MIDI_ChannelPressureEvent.cs new file mode 100644 index 0000000..f98bcc3 --- /dev/null +++ b/ProjectObsidian/ProtoFlux/Devices/MIDI_ChannelPressureEvent.cs @@ -0,0 +1,87 @@ +using System; +using System.Linq; +using Newtonsoft.Json.Linq; +using ProtoFlux.Core; +using ProtoFlux.Runtimes.Execution; +using Elements.Core; +using FrooxEngine; +using FrooxEngine.ProtoFlux; +using Obsidian.Elements; +using Obsidian; + +namespace ProtoFlux.Runtimes.Execution.Nodes.Obsidian.Devices; + +[NodeName("MIDI Channel Pressure Event")] +[NodeCategory("Obsidian/Devices")] +public class MIDI_ChannelPressureEvent : VoidNode +{ + public readonly GlobalRef Device; + + public Call ChannelPressure; + + public readonly ValueOutput Channel; + + public readonly ValueOutput Pressure; + + public readonly ValueOutput NormalizedPressure; + + private ObjectStore _currentDevice; + + private ObjectStore _channelPressure; + + public override bool CanBeEvaluated => false; + + private void OnDeviceChanged(MIDI_InputDevice device, FrooxEngineContext context) + { + MIDI_InputDevice device2 = _currentDevice.Read(context); + if (device == device2) + { + return; + } + if (device2 != null) + { + device2.ChannelPressure -= _channelPressure.Read(context); + } + if (device != null) + { + NodeContextPath path = context.CaptureContextPath(); + context.GetEventDispatcher(out var dispatcher); + MIDI_ChannelPressureEventHandler value3 = delegate (MIDI_InputDevice dev, MIDI_ChannelPressureEventData e) + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + OnChannelPressure(dev, in e, c); + }); + }; + _currentDevice.Write(device, context); + _channelPressure.Write(value3, context); + device.ChannelPressure += value3; + } + else + { + _currentDevice.Clear(context); + _channelPressure.Clear(context); + } + } + + private void WriteChannelPressureEventData(in MIDI_ChannelPressureEventData eventData, FrooxEngineContext context) + { + Channel.Write(eventData.channel, context); + Pressure.Write(eventData.pressure, context); + NormalizedPressure.Write(eventData.pressure / 127f, context); + } + + private void OnChannelPressure(MIDI_InputDevice device, in MIDI_ChannelPressureEventData eventData, FrooxEngineContext context) + { + WriteChannelPressureEventData(in eventData, context); + ChannelPressure.Execute(context); + } + + public MIDI_ChannelPressureEvent() + { + Device = new GlobalRef(this, 0); + Channel = new ValueOutput(this); + Pressure = new ValueOutput(this); + NormalizedPressure = new ValueOutput(this); + } +} \ No newline at end of file diff --git a/ProjectObsidian/ProtoFlux/Devices/MIDI_NoteEvents.cs b/ProjectObsidian/ProtoFlux/Devices/MIDI_NoteEvents.cs new file mode 100644 index 0000000..e105e87 --- /dev/null +++ b/ProjectObsidian/ProtoFlux/Devices/MIDI_NoteEvents.cs @@ -0,0 +1,112 @@ +using System; +using System.Linq; +using Newtonsoft.Json.Linq; +using ProtoFlux.Core; +using ProtoFlux.Runtimes.Execution; +using Elements.Core; +using FrooxEngine; +using FrooxEngine.ProtoFlux; +using Obsidian.Elements; +using Obsidian; + +namespace ProtoFlux.Runtimes.Execution.Nodes.Obsidian.Devices; + +[NodeName("MIDI Note Events")] +[NodeCategory("Obsidian/Devices")] +public class MIDI_NoteEvents : VoidNode +{ + public readonly GlobalRef Device; + + public Call NoteOn; + + public Call NoteOff; + + public readonly ValueOutput Channel; + + public readonly ValueOutput Note; + + public readonly ValueOutput Velocity; + + public readonly ValueOutput NormalizedVelocity; + + private ObjectStore _currentDevice; + + private ObjectStore _noteOn; + + private ObjectStore _noteOff; + + public override bool CanBeEvaluated => false; + + private void OnDeviceChanged(MIDI_InputDevice device, FrooxEngineContext context) + { + MIDI_InputDevice device2 = _currentDevice.Read(context); + if (device == device2) + { + return; + } + if (device2 != null) + { + device2.NoteOn -= _noteOn.Read(context); + device2.NoteOff -= _noteOff.Read(context); + } + if (device != null) + { + NodeContextPath path = context.CaptureContextPath(); + context.GetEventDispatcher(out var dispatcher); + MIDI_NoteEventHandler value = delegate (MIDI_InputDevice dev, MIDI_NoteEventData e) + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + OnNoteOn(dev, in e, c); + }); + }; + MIDI_NoteEventHandler value2 = delegate (MIDI_InputDevice dev, MIDI_NoteEventData e) + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + OnNoteOff(dev, in e, c); + }); + }; + _currentDevice.Write(device, context); + _noteOn.Write(value, context); + _noteOff.Write(value2, context); + device.NoteOn += value; + device.NoteOff += value2; + } + else + { + _currentDevice.Clear(context); + _noteOn.Clear(context); + _noteOff.Clear(context); + } + } + + private void WriteNoteOnOffEventData(in MIDI_NoteEventData eventData, FrooxEngineContext context) + { + Channel.Write(eventData.channel, context); + Note.Write(eventData.note, context); + Velocity.Write(eventData.velocity, context); + NormalizedVelocity.Write(eventData.velocity / 127f, context); + } + + private void OnNoteOn(MIDI_InputDevice device, in MIDI_NoteEventData eventData, FrooxEngineContext context) + { + WriteNoteOnOffEventData(in eventData, context); + NoteOn.Execute(context); + } + + private void OnNoteOff(MIDI_InputDevice device, in MIDI_NoteEventData eventData, FrooxEngineContext context) + { + WriteNoteOnOffEventData(in eventData, context); + NoteOff.Execute(context); + } + + public MIDI_NoteEvents() + { + Device = new GlobalRef(this, 0); + Channel = new ValueOutput(this); + Note = new ValueOutput(this); + Velocity = new ValueOutput(this); + NormalizedVelocity = new ValueOutput(this); + } +} \ No newline at end of file diff --git a/ProjectObsidian/ProtoFlux/Devices/MIDI_PitchWheelEvent.cs b/ProjectObsidian/ProtoFlux/Devices/MIDI_PitchWheelEvent.cs new file mode 100644 index 0000000..40400ea --- /dev/null +++ b/ProjectObsidian/ProtoFlux/Devices/MIDI_PitchWheelEvent.cs @@ -0,0 +1,89 @@ +using System; +using System.Linq; +using Newtonsoft.Json.Linq; +using ProtoFlux.Core; +using ProtoFlux.Runtimes.Execution; +using Elements.Core; +using FrooxEngine; +using FrooxEngine.ProtoFlux; +using Obsidian.Elements; +using Obsidian; + +namespace ProtoFlux.Runtimes.Execution.Nodes.Obsidian.Devices; + +[NodeName("MIDI Pitch Wheel Event")] +[NodeCategory("Obsidian/Devices")] +public class MIDI_PitchWheelEvent : VoidNode +{ + public readonly GlobalRef Device; + + public Call PitchWheel; + + public readonly ValueOutput Channel; + + public readonly ValueOutput Value; + + public readonly ValueOutput NormalizedValue; + + private ObjectStore _currentDevice; + + private ObjectStore _pitchWheel; + + public override bool CanBeEvaluated => false; + + private void OnDeviceChanged(MIDI_InputDevice device, FrooxEngineContext context) + { + MIDI_InputDevice device2 = _currentDevice.Read(context); + if (device == device2) + { + return; + } + if (device2 != null) + { + device2.PitchWheel -= _pitchWheel.Read(context); + } + if (device != null) + { + NodeContextPath path = context.CaptureContextPath(); + context.GetEventDispatcher(out var dispatcher); + MIDI_PitchWheelEventHandler value3 = delegate (MIDI_InputDevice dev, MIDI_PitchWheelEventData e) + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + OnPitch(dev, in e, c); + }); + }; + _currentDevice.Write(device, context); + _pitchWheel.Write(value3, context); + device.PitchWheel += value3; + } + else + { + _currentDevice.Clear(context); + _pitchWheel.Clear(context); + } + } + + private void WritePitchEventData(in MIDI_PitchWheelEventData eventData, FrooxEngineContext context) + { + Channel.Write(eventData.channel, context); + Value.Write(eventData.value, context); + + // should be 1 at 16383, -1 at 0 + NormalizedValue.Write(eventData.value == 8192 ? 0f : MathX.Remap(eventData.value, 0f, 16383f, -1f, 1f), context); + } + + private void OnPitch(MIDI_InputDevice device, in MIDI_PitchWheelEventData eventData, FrooxEngineContext context) + { + WritePitchEventData(in eventData, context); + PitchWheel.Execute(context); + } + + public MIDI_PitchWheelEvent() + { + Device = new GlobalRef(this, 0); + Channel = new ValueOutput(this); + Value = new ValueOutput(this); + NormalizedValue = new ValueOutput(this); + } +} \ No newline at end of file diff --git a/ProjectObsidian/Settings/MIDI_Settings.cs b/ProjectObsidian/Settings/MIDI_Settings.cs new file mode 100644 index 0000000..c8c8b5c --- /dev/null +++ b/ProjectObsidian/Settings/MIDI_Settings.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FrooxEngine; +using Elements.Core; +using Elements.Assets; +using Commons.Music.Midi; + +namespace Obsidian; + +[SettingCategory("Obsidian")] +public class MIDI_Settings : SettingComponent +{ + public override bool UserspaceOnly => true; + + public class MIDI_Device : SyncObject + { + [SettingIndicatorProperty(null, null, null, null, false, 0L)] + public readonly Sync DeviceName; + + [NonPersistent] + [SettingIndicatorProperty(null, null, null, null, false, 0L)] + public readonly Sync DeviceFound; + + [SettingProperty(null, null, null, false, 0L, null, null)] + public readonly Sync AllowConnections; + + public IMidiPortDetails Details { get; internal set; } + + public bool IsOutput => this.Parent.Name == "OutputDevices"; + + protected override void OnAwake() + { + base.OnAwake(); + } + + [SettingProperty(null, null, null, false, 0L, null, null)] + [SyncMethod(typeof(Action), new string[] { })] + public void Remove() + { + this.FindNearestParent>().Remove(this); + } + } + + [SettingSubcategoryList("DeviceToItem", null, null, null, null, null)] + public readonly SyncList InputDevices; + + [SettingSubcategoryList("DeviceToItem", null, null, null, null, null)] + public readonly SyncList OutputDevices; + + private LocaleData _localeData; + + private DataFeedItem DeviceToItem(ISyncMember item) + { + MIDI_Device device = (MIDI_Device)item; + DataFeedGroup dataFeedGroup = new DataFeedGroup(); + List list = new List(); + var subcat = device.IsOutput ? "OutputDevices" : "InputDevices"; + var getter = device.IsOutput ? "GetOutputDeviceForSubsetting" : "GetInputDeviceForSubsetting"; + foreach (DataFeedItem item2 in SettingsDataFeed.EnumerateSettingProperties(typeof(MIDI_Device), null, typeof(MIDI_Settings), subcat, getter, device.DeviceName.Value)) + { + // Simplify locale key + var parts = item2.ItemKey.Split('.'); + var newLocaleKey = "Settings." + string.Join(".", parts.Take(2)); + item2.InitBase(item2.ItemKey, null, null, newLocaleKey.AsLocaleKey()); + list.Add(item2); + } + dataFeedGroup.InitBase(device.DeviceName.Value, null, null, device.DeviceName.Value, null, null, null, list); + return dataFeedGroup; + } + + [SyncMethod(typeof(SubsettingGetter), new string[] { })] + public SyncObject GetInputDeviceForSubsetting(string key) + { + return InputDevices.FirstOrDefault((d) => d.DeviceName.Value == key); + } + + [SyncMethod(typeof(SubsettingGetter), new string[] { })] + public SyncObject GetOutputDeviceForSubsetting(string key) + { + return OutputDevices.FirstOrDefault((d) => d.DeviceName.Value == key); + } + + protected override void OnStart() + { + base.OnStart(); + _localeData = new LocaleData(); + _localeData.LocaleCode = "en"; + _localeData.Authors = new List() { "Nytra" }; + _localeData.Messages = new Dictionary(); + _localeData.Messages.Add("Settings.Category.Obsidian", "Obsidian"); + _localeData.Messages.Add("Settings.MIDI_Settings", "MIDI Settings"); + _localeData.Messages.Add("Settings.MIDI_Settings.RefreshDeviceLists", "Refresh Devices"); + _localeData.Messages.Add("Settings.MIDI_Settings.InputDevices", "Input Devices"); + _localeData.Messages.Add("Settings.MIDI_Settings.OutputDevices", "Output Devices"); + + _localeData.Messages.Add("Settings.MIDI_Settings.DeviceName", "Device Name"); + _localeData.Messages.Add("Settings.MIDI_Settings.OutputDevices.Breadcrumb", "MIDI Output Devices"); + _localeData.Messages.Add("Settings.MIDI_Settings.InputDevices.Breadcrumb", "MIDI Input Devices"); + _localeData.Messages.Add("Settings.MIDI_Settings.AllowConnections", "Allow Connections"); + _localeData.Messages.Add("Settings.MIDI_Settings.DeviceFound", "Device Found"); + _localeData.Messages.Add("Settings.MIDI_Settings.Remove", "Remove"); + + // Sometimes the locale is null in here, so wait a bit I guess + RunInUpdates(7, () => + { + UpdateLocale(); + Settings.RegisterValueChanges(UpdateLocale); + }); + + RefreshDeviceLists(); + } + + protected override void OnDispose() + { + base.OnDispose(); + Settings.UnregisterValueChanges(UpdateLocale); + } + + private void UpdateLocale(LocaleSettings settings = null) + { + this.GetCoreLocale()?.Asset?.Data.LoadDataAdditively(_localeData); + } + + [SettingProperty(null, null, null, false, 0L, null, null)] + [SyncMethod(typeof(Action), new string[] { })] + public void RefreshDeviceLists() + { + foreach(var device in InputDevices.Concat(OutputDevices)) + { + device.DeviceFound.Value = false; + } + var access = MidiAccessManager.Default; + foreach (var input in access.Inputs) + { + RegisterInputDevice(input); + } + foreach (var output in access.Outputs) + { + RegisterOutputDevice(output); + } + } + + private void RegisterInputDevice(IMidiPortDetails details) + { + if (string.IsNullOrEmpty(details.Name)) + { + return; + } + MIDI_Device device = InputDevices.FirstOrDefault((d) => d.DeviceName.Value == details.Name); + if (device == null) + { + device = InputDevices.Add(); + device.DeviceName.Value = details.Name; + } + device.Details = details; + device.DeviceFound.Value = true; + } + + private void RegisterOutputDevice(IMidiPortDetails details) + { + if (string.IsNullOrEmpty(details.Name)) + { + return; + } + MIDI_Device device = OutputDevices.FirstOrDefault((d) => d.DeviceName.Value == details.Name); + if (device == null) + { + device = OutputDevices.Add(); + device.DeviceName.Value = details.Name; + } + device.Details = details; + device.DeviceFound.Value = true; + } +} \ No newline at end of file diff --git a/ProjectObsidian/Settings/SettingCategoryDefinitions.cs b/ProjectObsidian/Settings/SettingCategoryDefinitions.cs new file mode 100644 index 0000000..75ae378 --- /dev/null +++ b/ProjectObsidian/Settings/SettingCategoryDefinitions.cs @@ -0,0 +1,10 @@ +using Elements.Core; +using FrooxEngine; + +// NEEDS TO BE IN GLOBAL NAMESPACE! + +[DataModelType] +public static class SettingCategoryDefinitions +{ + public static SettingCategoryInfo Obsidian => new SettingCategoryInfo(new System.Uri("https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/99/Obsidian_JE3_BE2.png/revision/latest?cb=20200124042057"), 0L); +} \ No newline at end of file diff --git a/lib/Commons.Music.Midi.dll b/lib/Commons.Music.Midi.dll new file mode 100644 index 0000000..b7312df Binary files /dev/null and b/lib/Commons.Music.Midi.dll differ diff --git a/lib/alsa-sharp.dll b/lib/alsa-sharp.dll new file mode 100644 index 0000000..76f4108 Binary files /dev/null and b/lib/alsa-sharp.dll differ