Files
2026-07-13 13:13:17 +08:00

523 lines
20 KiB
C#

using NAudio.Midi;
using T3.IoServices;
using T3.Core.Animation;
using T3.Core.IO;
using T3.Core.Stats;
using T3.Core.Utils;
namespace Lib.io.midi;
[Guid("59a0458e-2f3a-4856-96cd-32936f783cc5")]
public sealed class MidiInput : Instance<MidiInput>, MidiConnectionManager.IMidiConsumer, IStatusProvider
{
[Output(Guid = "01706780-D25B-4C30-A741-8B7B81E04D82")]
public readonly Slot<float> Result = new();
[Output(Guid = "D7114289-4B1D-47E9-B5C1-DCDC8A371087")]
public readonly Slot<List<float>> Range = new();
[Output(Guid = "4BF74648-207F-4275-83BA-09E1C048C33B")]
public readonly Slot<bool> WasHit = new();
public MidiInput()
{
Result.UpdateAction += Update;
Range.UpdateAction += Update;
WasHit.UpdateAction += Update;
}
private bool _initialized;
private double _lastUpdateTime = -1;
private bool _restoredPersistedValue;
protected override void Dispose(bool isDisposing)
{
if (!isDisposing)
return;
MidiConnectionManager.UnregisterConsumer(this);
SimulatedIoBus.MidiEventReceived -= HandleSimulatedMidi;
}
private void Update(EvaluationContext context)
{
if (Math.Abs(context.LocalFxTime - _lastUpdateTime) < 0.0001f)
return;
_lastUpdateTime= context.LocalFxTime;
if (!_initialized)
{
MidiConnectionManager.RegisterConsumer(this);
// Also subscribe to the simulated-event bus so SimulateIoData can drive this
// MidiInput exactly like a real device — even without the original controller
// connected, matched by device name.
SimulatedIoBus.MidiEventReceived += HandleSimulatedMidi;
_initialized = true;
}
_trainedDeviceName = Device.GetValue(context);
if (MidiConnectionManager.TryGetMidiIn(_trainedDeviceName, out _))
{
_warningMessage = null;
this.ClearErrorState();
}
else
{
_warningMessage = $"Midi device '{_trainedDeviceName}' is not captured.\nYou can try Windows » Settings » Midi » Rescan Devices.";
//this.LogWarningState(_warningMessage);
}
_trainedChannel = Channel.GetValue(context);
_trainedControllerId = Control.GetValue(context);
_trainedEventType = EventType.GetEnumValue<MidiEventTypes>(context);
var defaultOutputValue = DefaultOutputValue.GetValue(context);
var damping = Damping.GetValue(context);
var outRange = OutputRange.GetValue(context);
_controlRange = ControlRange.GetValue(context);
_printLogMessages = PrintLogMessages.GetValue(context);
var controlRangeSize = (_controlRange.Height - _controlRange.Width).Clamp(1, 128);
if (!_restoredPersistedValue)
{
var persisted = LastKnownControllerValue.GetValue(context);
if (persisted > 0)
{
if (UseControlRange)
{
_currentControllerId = (int)persisted;
}
else
{
_currentControllerValue = persisted;
_dampedOutputValue = MathUtils.Remap(persisted, 0, 127,
OutputRange.GetValue(context).X,
OutputRange.GetValue(context).Y);
}
_isDefaultValue = false;
}
_restoredPersistedValue = true;
}
if (_valuesForControlRange == null || _valuesForControlRange.Count != controlRangeSize)
{
_valuesForControlRange = new List<float>(controlRangeSize);
_valuesForControlRange.AddRange(new float[controlRangeSize]); //initialize list values
}
if (MathUtils.WasTriggered(TeachTrigger.GetValue(context), ref _oldTeachTrigger))
{
//MidiInConnectionManager.Rescan();
_teachingActive = true;
_lastMatchingSignals.Clear();
_currentControllerValue = 0;
}
var wasHit = false;
lock (this)
{
foreach (var signal in _lastMatchingSignals)
{
if (_teachingActive)
{
// The teaching mode shouldn't override the connected nodes
if (!Device.HasInputConnections) {
Device.SetTypedInputValue(_lastMessageDeviceName);
_trainedDeviceName = _lastMessageDeviceName;
}
if (!Channel.HasInputConnections) {
Channel.SetTypedInputValue(signal.Channel);
_trainedChannel = signal.Channel;
}
if (!Control.HasInputConnections) {
Control.SetTypedInputValue(signal.ControllerId);
_trainedControllerId = signal.ControllerId;
}
if (!EventType.HasInputConnections) {
EventType.SetTypedInputValue((int)signal.EventType);
_trainedEventType = signal.EventType;
}
TeachTrigger.SetTypedInputValue(false);
_teachingActive = false;
}
var hasValueChanged = Math.Abs(_currentControllerValue - signal.ControllerValue) > 0.001f;
_currentControllerValue = signal.ControllerValue;
_currentControllerId = signal.ControllerId;
var isWithinControlRange =
signal.ControllerId >= _controlRange.Width &&
signal.ControllerId < _controlRange.Height;
if (isWithinControlRange)
{
var index = signal.ControllerId - _controlRange.Width;
_valuesForControlRange[index] = signal.ControllerValue;
}
if (hasValueChanged && signal.ControllerValue > 0)
wasHit = true;
LastMessageTime = Playback.RunTimeInSecs;
_isDefaultValue = false;
}
_lastMatchingSignals.Clear();
}
if (_initialized)
{
if (UseControlRange)
{
if (_currentControllerValue > 0) // note-on only, filters NoteOff
LastKnownControllerValue.SetTypedInputValue((float)_currentControllerId);
}
else
{
if (_trainedEventType != MidiEventTypes.Notes || _currentControllerValue > 0)
LastKnownControllerValue.SetTypedInputValue(_currentControllerValue);
}
}
if (ResetToDefaultTrigger.GetValue(context))
{
ResetToDefaultTrigger.SetTypedInputValue(false);
_isDefaultValue = true;
LastKnownControllerValue.SetTypedInputValue(0f); // clear persisted value
}
if (_isDefaultValue && _trainedEventType != MidiEventTypes.MidiTime)
{
Result.Value = defaultOutputValue;
Result.DirtyFlag.Clear();
Range.DirtyFlag.Clear();
WasHit.DirtyFlag.Clear();
return;
}
var currentValue = UseControlRange
? _currentControllerId
: MathUtils.Remap(_currentControllerValue, 0, 127, outRange.X, outRange.Y);
if (_trainedEventType == MidiEventTypes.MidiTime)
{
currentValue = (float)(_timingMsgCount / (24.0 * 4 ));
}
_dampedOutputValue = MathUtils.Lerp(currentValue, _dampedOutputValue, damping);
var reachTarget = MathF.Abs(_dampedOutputValue - currentValue) < 0.0001f;
var needsUpdateNextFrame = !reachTarget || wasHit;
Result.DirtyFlag.Trigger = needsUpdateNextFrame ? DirtyFlagTrigger.Animated : DirtyFlagTrigger.None;
Range.DirtyFlag.Trigger = needsUpdateNextFrame ? DirtyFlagTrigger.Animated : DirtyFlagTrigger.None;
WasHit.DirtyFlag.Trigger = needsUpdateNextFrame ? DirtyFlagTrigger.Animated : DirtyFlagTrigger.None;
if (reachTarget)
{
_dampedOutputValue = currentValue;
}
if (!float.IsNormal(_dampedOutputValue))
_dampedOutputValue = 0;
WasHit.Value = wasHit;
Result.Value = _dampedOutputValue;
Range.Value = _valuesForControlRange;
// Result.DirtyFlag.Clear();
// Range.DirtyFlag.Clear();
// WasHit.DirtyFlag.Clear();
}
public void ErrorReceivedHandler(object sender, MidiInMessageEventArgs msg)
{
}
/// <summary>
/// This will cause update to be called on next frame
/// </summary>
private void FlagAsDirty()
{
// Disable until invalidation is fixed
// Result.DirtyFlag.Invalidate();
// Range.DirtyFlag.Invalidate();
// WasHit.DirtyFlag.Invalidate();
Result.DirtyFlag.Trigger = DirtyFlagTrigger.Animated;
Range.DirtyFlag.Trigger = DirtyFlagTrigger.Animated;
WasHit.DirtyFlag.Trigger = DirtyFlagTrigger.Animated;
}
/// <remarks>
/// This comes in multi threaded
/// </remarks>
public void MessageReceivedHandler(object sender, MidiInMessageEventArgs msg)
{
if (sender is not MidiIn midiIn || msg.MidiEvent == null)
return;
// Skip messages from devices that are being controlled by compatible MIDI devices
// (when in control mode, not passthrough mode)
if (MidiConnectionManager.IsDeviceInControlMode(midiIn))
return;
var device = MidiConnectionManager.GetDescriptionForMidiIn(midiIn);
ProcessParsedMidiEvent(msg.MidiEvent, device.ProductName);
}
/// <summary>
/// Receives events from <see cref="SimulatedIoBus"/> — the replay path that lets a
/// recorded session drive this MidiInput when the original device isn't connected.
/// The bus delivers a NAudio <see cref="MidiEvent"/> plus the recorded device
/// product name; matching against <c>Device</c> works identically to the live path.
/// </summary>
private void HandleSimulatedMidi(SimulatedIoBus.SimulatedMidiEvent ev)
=> ProcessParsedMidiEvent(ev.Event, ev.DeviceProductName);
/// <summary>
/// Shared event-processing core for both the real <see cref="MessageReceivedHandler"/>
/// path and the simulated <see cref="HandleSimulatedMidi"/> path. Translates a parsed
/// NAudio <see cref="MidiEvent"/> into a <see cref="MidiSignal"/>, applies the
/// configured device / channel / controller filters, and queues the signal for the
/// next <see cref="Update"/> tick.
/// </summary>
private void ProcessParsedMidiEvent(MidiEvent midiEvent, string deviceProductName)
{
lock (this)
{
MidiSignal newSignal = null;
switch (midiEvent)
{
case ControlChangeEvent controlEvent:
{
if (_printLogMessages)
Log.Debug($"{deviceProductName}/{controlEvent}", this);
if (!UseControlRange)
{
newSignal = new MidiSignal()
{
Channel = controlEvent.Channel,
ControllerId = (int)controlEvent.Controller,
ControllerValue = controlEvent.ControllerValue,
EventType = MidiEventTypes.ControllerChanges,
};
}
break;
}
case NoteEvent noteEvent:
switch (noteEvent.CommandCode)
{
case MidiCommandCode.NoteOn:
{
if (_printLogMessages)
Log.Debug($"{deviceProductName}/{noteEvent} ControlValue :{noteEvent.NoteNumber}", this);
newSignal = new MidiSignal()
{
Channel = noteEvent.Channel,
ControllerId = noteEvent.NoteNumber,
ControllerValue = noteEvent.Velocity,
EventType = MidiEventTypes.Notes,
};
break;
}
case MidiCommandCode.NoteOff:
newSignal = new MidiSignal()
{
Channel = noteEvent.Channel,
ControllerId = noteEvent.NoteNumber,
ControllerValue = 0,
EventType = MidiEventTypes.Notes,
};
break;
}
break;
case PitchWheelChangeEvent pwEvent:
newSignal = new MidiSignal()
{
Channel = pwEvent.Channel,
ControllerId = 10000 + (int)pwEvent.CommandCode,
ControllerValue = pwEvent.Pitch,
EventType = MidiEventTypes.MidiEvent,
};
Log.Debug("Pitch " + pwEvent.Pitch);
break;
case PatchChangeEvent patchChangeEvent:
newSignal = new MidiSignal()
{
Channel = patchChangeEvent.Channel,
ControllerId = 10000 + (int)patchChangeEvent.CommandCode,
ControllerValue = patchChangeEvent.Patch,
EventType = MidiEventTypes.MidiEvent,
};
break;
case ChannelAfterTouchEvent afterTouchEvent:
newSignal = new MidiSignal()
{
Channel = afterTouchEvent.Channel,
ControllerId = 10000 + (int)afterTouchEvent.CommandCode,
ControllerValue = afterTouchEvent.AfterTouchPressure,
EventType = MidiEventTypes.MidiEvent,
};
break;
}
if (!_teachingActive && midiEvent.CommandCode == MidiCommandCode.TimingClock)
{
_timingMsgCount++;
if (_trainedEventType == MidiEventTypes.MidiTime)
{
newSignal = new MidiSignal()
{
Channel = midiEvent.Channel,
ControllerId = 0,
ControllerValue = _timingMsgCount,
EventType = MidiEventTypes.MidiTime,
};
}
}
if (newSignal == null)
return;
var matchesDevice = string.IsNullOrEmpty(_trainedDeviceName) || deviceProductName == _trainedDeviceName;
var matchesChannel = _trainedChannel < 0 || newSignal.Channel == _trainedChannel;
var matchesSingleController = _trainedControllerId < 0 || newSignal.ControllerId == _trainedControllerId;
var matchesControlRange = _trainedControllerId < 0 ||
(newSignal.ControllerId >= _controlRange.Width && newSignal.ControllerId <= _controlRange.Height);
var matchesController = UseControlRange
? matchesControlRange
: matchesSingleController;
var matchesEventType = _trainedEventType == MidiEventTypes.All || _trainedEventType == newSignal.EventType;
if (_teachingActive || (matchesDevice && matchesChannel && matchesController && matchesEventType))
{
_lastMatchingSignals.Add(newSignal);
_lastMessageDeviceName = deviceProductName;
_isDefaultValue = false;
FlagAsDirty();
}
}
}
public void OnSettingsChanged()
{
Result.DirtyFlag.Invalidate();
Range.DirtyFlag.Invalidate();
WasHit.DirtyFlag.Invalidate();
}
public IStatusProvider.StatusLevel GetStatusLevel()
{
return string.IsNullOrEmpty(_warningMessage) ? IStatusProvider.StatusLevel.Success : IStatusProvider.StatusLevel.Error;
}
public string GetStatusMessage()
{
return _warningMessage;
}
private string _warningMessage;
private Int2 _controlRange;
private bool UseControlRange => _controlRange.Width > 0 || _controlRange.Height > 0;
private List<float> _valuesForControlRange;
private class MidiSignal
{
public int Channel;
public int ControllerId;
public int ControllerValue;
public MidiEventTypes EventType;
}
public double LastMessageTime; // used for OpUi
private bool _printLogMessages;
private bool _isDefaultValue = true;
private bool _oldTeachTrigger;
private bool _teachingActive;
private string _trainedDeviceName;
private int _trainedChannel = -1;
private int _trainedControllerId = -1;
private MidiEventTypes _trainedEventType;
private readonly List<MidiSignal> _lastMatchingSignals = new(10);
// The most recent matching event's device name, captured for teach-mode write-back.
// Stored as a string (rather than the NAudio MidiInCapabilities struct it used to be)
// so the simulated replay path can populate it without a real MidiIn instance.
private string _lastMessageDeviceName;
private float _currentControllerValue;
private float _dampedOutputValue;
private int _currentControllerId;
private int _timingMsgCount;
private enum MidiEventTypes
{
All,
Notes,
ControllerChanges,
MidiTime,
MidiEvent,
}
[Input(Guid = "AAD1E576-F144-423F-83B5-5694B1119C23")]
public readonly InputSlot<Vector2> OutputRange = new();
[Input(Guid = "4636D6CF-8233-4281-8840-5BA079B5F1A6")]
public readonly InputSlot<float> DefaultOutputValue = new();
[Input(Guid = "CA3CE08D-6A19-4AD5-9435-08B050753311")]
public readonly InputSlot<float> Damping = new();
[Input(Guid = "7C681EE6-D071-4284-8585-1C3E03A089EA")]
public readonly InputSlot<bool> TeachTrigger = new();
[Input(Guid = "044168EB-791C-405F-867F-3D5702924165", MappedType = typeof(MidiEventTypes))]
public readonly InputSlot<int> EventType = new();
[Input(Guid = "23C34F4C-4BA3-4834-8D51-3E3909751F84")]
public readonly InputSlot<string> Device = new();
[Input(Guid = "9B0D32DE-C53C-4DF6-8B29-5E68A5A9C5F9")]
public readonly InputSlot<int> Channel = new();
[Input(Guid = "DF81B7B3-F39E-4E5D-8B97-F29DD576A76D")]
public readonly InputSlot<int> Control = new();
[Input(Guid = "F650985F-00A7-452A-B3E4-69A8E9A78C3F")]
public readonly InputSlot<Int2> ControlRange = new();
[Input(Guid = "6C15E743-9A70-47E7-A0A4-75636817E441")]
public readonly InputSlot<bool> PrintLogMessages = new();
[Input(Guid = "AC35E75A-BEC5-497C-9C68-6B809B12CD8B")]
public readonly InputSlot<bool> ResetToDefaultTrigger = new();
[Input(Guid = "ddbe4c30-74d7-41bc-a062-e19152de1080")]
public readonly InputSlot<float> LastKnownControllerValue = new();
}