#nullable enable using T3.Core.Animation; using T3.Core.DataTypes.DataSet; using T3.IoServices; using T3CoreDataClip = T3.Core.DataTypes.DataSet.DataClip; namespace Lib.io.midi; /// /// Loads a standard MIDI file (.mid / .midi) and presents it as a /// timeline-bound , converted to the same channel conventions /// as live MIDI recording. Owns the load + /// placement, then publishes the result as a /// single output — downstream ops (e.g. SimulateIoData) /// consume it exactly like a recorded .data clip. /// /// /// Parsing flows through the shared cache, so multiple /// LoadMidiFile ops referencing the same file share one converted /// . The Resource<DataSet> wrapper additionally gives /// file-watch invalidation when the source file changes. /// [Guid("b4766419-8bca-4fa0-a398-e6af90ef8971")] internal sealed class LoadMidiFile : Instance, IStatusProvider, IDescriptiveFilename { [Output(Guid = "a668ec72-17a6-443d-9a85-04d9c37ec9d3", DirtyFlagTrigger = DirtyFlagTrigger.Animated)] public readonly TimeClipSlot Clip = new(); public LoadMidiFile() { _dataSetResource = new Resource(FilePath, TryLoad); _dataSetResource.AddDependentSlots(Clip); Clip.UpdateAction += Update; } private bool TryLoad(FileResource file, DataSet? currentValue, [NotNullWhen(true)] out DataSet? newValue, [NotNullWhen(false)] out string? failureReason) { // The converter cache keys by (path, last-write timestamp), so a changed file // re-converts while unchanged files are shared across all LoadMidiFile instances. if (!MidiFileToDataSet.TryGet(file.AbsolutePath, out var loaded, out var reason)) { newValue = null; failureReason = reason; _errorMessageForStatus = reason; return false; } newValue = loaded; failureReason = null; _errorMessageForStatus = string.Empty; return true; } private void Update(EvaluationContext context) { var dataSet = _dataSetResource.GetValue(context); if (dataSet == null) { Clip.Value = null; return; } // SourceRange is in file-time (bars), TimeRange in timeline-time (bars) — the same // identity-mapping convention as LoadDataClip / PlayVideoClip, so split, trim and // body-drag math stay consistent across clip types. var timeRange = Clip.TimeClip.TimeRange; var sourceRange = Clip.TimeClip.SourceRange; var mapping = new TimeRangeMapping(timeRange, sourceRange, context.Playback.Bpm); Clip.Value = new T3CoreDataClip { Set = dataSet, Mapping = mapping, }; Clip.DirtyFlag.Clear(); } IStatusProvider.StatusLevel IStatusProvider.GetStatusLevel() => string.IsNullOrEmpty(_errorMessageForStatus) ? IStatusProvider.StatusLevel.Success : IStatusProvider.StatusLevel.Warning; string IStatusProvider.GetStatusMessage() => _errorMessageForStatus; public IEnumerable FileFilter => _fileFilter; public InputSlot SourcePathSlot => FilePath; private static readonly string[] _fileFilter = ["*.mid", "*.midi"]; [Input(Guid = "10c6978a-5910-4949-8382-95834dd1b4d1")] public readonly InputSlot FilePath = new(); private readonly Resource _dataSetResource; private string _errorMessageForStatus = string.Empty; }