Files
tooll3--t3/IoServices/MidiConnectionManager.cs
2026-07-13 13:13:17 +08:00

318 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using NAudio.Midi;
using T3.Core.IO;
using T3.Core.Logging;
namespace T3.IoServices;
public static class MidiConnectionManager
{
/// <summary>
/// Tracks which MIDI devices are currently being controlled by compatible devices.
/// When a device is in control mode, its messages should not be passed through to graph operators.
/// </summary>
private static readonly HashSet<string> _controlledDevices = new();
/// <summary>
/// Sets whether a MIDI device should block messages from being passed to graph operators.
/// When true, the device is in "control mode" - messages are consumed by the compatible device.
/// When false, the device is in "passthrough mode" - messages are passed to graph operators.
/// </summary>
public static void SetDeviceControlMode(string productName, bool controlMode)
{
if (controlMode)
{
if (_controlledDevices.Add(productName))
{
Log.Debug($"MIDI Device '{productName}' set to CONTROL mode (blocking passthrough)");
}
}
else
{
if (_controlledDevices.Remove(productName))
{
Log.Debug($"MIDI Device '{productName}' set to PASSTHROUGH mode");
}
}
}
/// <summary>
/// Checks if a MIDI device is currently being controlled and should block passthrough.
/// </summary>
public static bool IsDeviceInControlMode(string productName)
{
return _controlledDevices.Contains(productName);
}
/// <summary>
/// Checks if a MIDI device is currently being controlled and should block passthrough.
/// </summary>
public static bool IsDeviceInControlMode(MidiIn midiIn)
{
if (_devicesByMidiIn.TryGetValue(midiIn, out var device))
{
return _controlledDevices.Contains(device.ProductName);
}
return false;
}
public static void RegisterConsumer(IMidiConsumer consumer)
{
if (!Initialized)
{
ScanAndRegisterToMidiDevices();
Initialized = true;
}
if (_midiInputConsumers.Contains(consumer))
{
Log.Warning("MidiConsumer was already added " + consumer);
return;
}
_midiInputConsumers.Add(consumer);
foreach (var midiInputDevice in _devicesByMidiIn.Keys)
{
midiInputDevice.MessageReceived -= consumer.MessageReceivedHandler;
midiInputDevice.ErrorReceived -= consumer.ErrorReceivedHandler;
midiInputDevice.MessageReceived += consumer.MessageReceivedHandler;
midiInputDevice.ErrorReceived += consumer.ErrorReceivedHandler;
}
}
public static bool Initialized { get; private set; }
public static void UnregisterConsumer(IMidiConsumer consumer)
{
if (!_midiInputConsumers.Contains(consumer))
return;
foreach (var midiIn in _devicesByMidiIn.Keys)
{
midiIn.MessageReceived -= consumer.MessageReceivedHandler;
midiIn.ErrorReceived -= consumer.ErrorReceivedHandler;
}
_midiInputConsumers.Remove(consumer);
if (_midiInputConsumers.Count == 0)
{
CloseMidiDevices();
}
}
public static void Rescan()
{
CloseMidiDevices();
ScanAndRegisterToMidiDevices(logInformation: true);
// TODO: Clean up later
foreach (var consumer in _midiInputConsumers)
{
foreach (var midiInputDevice in _devicesByMidiIn.Keys)
{
midiInputDevice.MessageReceived -= consumer.MessageReceivedHandler;
midiInputDevice.ErrorReceived -= consumer.ErrorReceivedHandler;
midiInputDevice.MessageReceived += consumer.MessageReceivedHandler;
midiInputDevice.ErrorReceived += consumer.ErrorReceivedHandler;
}
consumer.OnSettingsChanged();
}
}
public interface IMidiConsumer
{
void MessageReceivedHandler(object sender, MidiInMessageEventArgs msg);
void ErrorReceivedHandler(object sender, MidiInMessageEventArgs msg);
/// <summary>
/// This will be called if the number of controllers or devices changed and the
/// listener should update its status.
/// </summary>
void OnSettingsChanged();
}
// FIXME: remove
public static MidiInCapabilities GetDescriptionForMidiIn(MidiIn midiIn)
{
_devicesByMidiIn.TryGetValue(midiIn, out var description);
return description;
}
public static bool TryGetMidiOut(string productName, out MidiOut midiOut)
{
midiOut = null;
foreach (var (midi, device) in _midiOutsWithDevices)
{
if (device.ProductName != productName)
continue;
midiOut = midi;
return true;
}
return false;
}
public static bool TryGetMidiIn(string productName, out MidiIn midiIn)
{
midiIn = null;
foreach (var (midiIn2, device) in _devicesByMidiIn)
{
if (device.ProductName != productName)
continue;
midiIn = midiIn2;
return true;
}
return false;
}
/// <summary>
/// For midi teaching, Tooll will capture all available midi devices.
/// This will prevent other applications from capturing them. User's can
/// prevent this in the settings.
/// </summary>
private static bool IsMidiDeviceCaptureEnabled(string deviceName)
{
var setting = CoreSettings.Config.LimitMidiDeviceCapture;
if (string.IsNullOrEmpty(setting))
return true;
foreach (var s in setting.Split("\n"))
{
if (deviceName.Contains(s.Trim(), StringComparison.InvariantCultureIgnoreCase))
return true;
}
return false;
}
private static void ScanAndRegisterToMidiDevices(bool logInformation = false)
{
Log.Debug("Capturing Midi devices...");
if (!string.IsNullOrEmpty(CoreSettings.Config.LimitMidiDeviceCapture))
{
var settingsString = CoreSettings.Config.LimitMidiDeviceCapture.Replace("\n", "; ");
Log.Debug($"NOTE: In settings Midi device capture is limited to '{settingsString}");
}
for (var index = 0; index < MidiIn.NumberOfDevices; index++)
{
var deviceInputInfo = MidiIn.DeviceInfo(index);
var deviceInfoProductName = deviceInputInfo.ProductName;
if (!IsMidiDeviceCaptureEnabled(deviceInfoProductName))
{
Log.Debug($" skipping '{deviceInfoProductName}' (disabled in setting)");
continue;
}
if (logInformation)
Log.Debug($" listening to '{deviceInfoProductName}'...");
MidiIn newMidiIn;
try
{
newMidiIn = new MidiIn(index);
}
catch (NAudio.MmException e)
{
Log.Error(e.Message == "MemoryAllocationError"
? " > The device is already being used by an application."
: $" > {e.Message} {deviceInfoProductName}");
continue;
}
newMidiIn.Start();
_devicesByMidiIn[newMidiIn] = deviceInputInfo;
}
for (var index = 0; index < MidiOut.NumberOfDevices; index++)
{
var deviceOutputInfo = MidiOut.DeviceInfo(index);
var deviceInfoProductName = deviceOutputInfo.ProductName;
if (!IsMidiDeviceCaptureEnabled(deviceInfoProductName))
{
Log.Debug($" skipping '{deviceInfoProductName}' (disabled in setting)");
continue;
}
MidiOut newMidiOut;
try
{
newMidiOut = new MidiOut(index);
}
catch (NAudio.MmException e)
{
Log.Error(e.Message == "MemoryAllocationError"
? " > The device is already being used by an application."
: $" > {e.Message} {deviceInfoProductName}");
continue;
}
_midiOutsWithDevices[newMidiOut] = deviceOutputInfo;
}
}
private static void CloseMidiDevices()
{
// Midi Ins
foreach (var midiInputDevice in _devicesByMidiIn.Keys)
{
foreach (var midiConsumer in _midiInputConsumers)
{
midiInputDevice.MessageReceived -= midiConsumer.MessageReceivedHandler;
midiInputDevice.ErrorReceived -= midiConsumer.ErrorReceivedHandler;
}
try
{
midiInputDevice.Stop();
midiInputDevice.Close();
midiInputDevice.Dispose();
}
catch (Exception e)
{
Log.Debug("exception: " + e);
}
}
_devicesByMidiIn.Clear();
// Midi outs
foreach (var midiOutputDevice in _midiOutsWithDevices.Keys)
{
try
{
midiOutputDevice.Close();
midiOutputDevice.Dispose();
}
catch (Exception e)
{
Log.Debug("exception: " + e);
}
}
_midiOutsWithDevices.Clear();
}
private static readonly List<IMidiConsumer> _midiInputConsumers = new();
/// <summary>
/// Sadly, we have to maintain this list, because NAudio does not provide an easy means
/// to get the produceName of an incoming midi-message.
/// </summary>
public static IReadOnlyDictionary<MidiIn, MidiInCapabilities> MidiIns => _devicesByMidiIn;
private static readonly Dictionary<MidiIn, MidiInCapabilities> _devicesByMidiIn = new();
public static IReadOnlyDictionary<MidiOut, MidiOutCapabilities> MidiOutsWithDevices => _midiOutsWithDevices;
private static readonly Dictionary<MidiOut, MidiOutCapabilities> _midiOutsWithDevices = new();
}