using System; using System.Collections.Generic; using System.Diagnostics.Metrics; using System.Linq; using Content.Shared.CCVar; using Content.Shared.Instruments; using Content.Shared.Physics; using JetBrains.Annotations; using Robust.Client.Audio.Midi; using Robust.Shared.Audio.Midi; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Network; using Robust.Shared.Timing; using SharpFont; namespace Content.Client.Instruments { [UsedImplicitly] public class InstrumentSystem : SharedInstrumentSystem { [Dependency] private readonly IClientNetManager _netManager = default!; [Dependency] private readonly IMidiManager _midiManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; public readonly TimeSpan OneSecAgo = TimeSpan.FromSeconds(-1); public readonly Comparer SortMidiEventTick = Comparer.Create((x, y) => x.Tick.CompareTo(y.Tick)); public int MaxMidiEventsPerBatch { get; private set; } public int MaxMidiEventsPerSecond { get; private set; } public override void Initialize() { base.Initialize(); UpdatesOutsidePrediction = true; _cfg.OnValueChanged(CCVars.MaxMidiEventsPerBatch, OnMaxMidiEventsPerBatchChanged, true); _cfg.OnValueChanged(CCVars.MaxMidiEventsPerSecond, OnMaxMidiEventsPerSecondChanged, true); SubscribeNetworkEvent(OnMidiEventRx); SubscribeNetworkEvent(OnMidiStart); SubscribeNetworkEvent(OnMidiStop); SubscribeLocalEvent(OnShutdown); } public override void Shutdown() { base.Shutdown(); _cfg.UnsubValueChanged(CCVars.MaxMidiEventsPerBatch, OnMaxMidiEventsPerBatchChanged); _cfg.UnsubValueChanged(CCVars.MaxMidiEventsPerSecond, OnMaxMidiEventsPerSecondChanged); } private void OnShutdown(EntityUid uid, InstrumentComponent component, ComponentShutdown args) { EndRenderer(uid, false, component); } public override void SetupRenderer(EntityUid uid, bool fromStateChange, SharedInstrumentComponent? component = null) { if (!Resolve(uid, ref component)) return; if (component is not InstrumentComponent instrument || instrument.IsRendererAlive) return; instrument.SequenceDelay = 0; instrument.SequenceStartTick = 0; _midiManager.OcclusionCollisionMask = (int) CollisionGroup.Impassable; instrument.Renderer = _midiManager.GetNewRenderer(); if (instrument.Renderer != null) { UpdateRenderer(uid, instrument); instrument.Renderer.OnMidiPlayerFinished += () => { instrument.PlaybackEndedInvoke(); EndRenderer(uid, fromStateChange, instrument); }; } if (!fromStateChange) { RaiseNetworkEvent(new InstrumentStartMidiEvent(uid)); } } public void UpdateRenderer(EntityUid uid, InstrumentComponent? instrument = null) { if (!Resolve(uid, ref instrument) || instrument.Renderer == null) return; instrument.Renderer.MidiBank = instrument.InstrumentBank; instrument.Renderer.MidiProgram = instrument.InstrumentProgram; instrument.Renderer.TrackingEntity = instrument.Owner; instrument.Renderer.DisablePercussionChannel = !instrument.AllowPercussion; instrument.Renderer.DisableProgramChangeEvent = !instrument.AllowProgramChange; instrument.Renderer.LoopMidi = instrument.LoopMidi; instrument.DirtyRenderer = false; } public override void EndRenderer(EntityUid uid, bool fromStateChange, SharedInstrumentComponent? component = null) { if (!Resolve(uid, ref component)) return; if (component is not InstrumentComponent instrument) return; if (instrument.IsInputOpen) { CloseInput(uid, fromStateChange, instrument); return; } if (instrument.IsMidiOpen) { CloseMidi(uid, fromStateChange, instrument); return; } instrument.Renderer?.StopAllNotes(); var renderer = instrument.Renderer; // We dispose of the synth two seconds from now to allow the last notes to stop from playing. // Don't use timers bound to the entity in case it is getting deleted. Timer.Spawn(2000, () => { renderer?.Dispose(); }); instrument.Renderer = null; instrument.MidiEventBuffer.Clear(); if (!fromStateChange && _netManager.IsConnected) { RaiseNetworkEvent(new InstrumentStopMidiEvent(uid)); } } public void SetPlayerTick(EntityUid uid, int playerTick, InstrumentComponent? instrument = null) { if (!Resolve(uid, ref instrument)) return; if (instrument.Renderer == null || instrument.Renderer.Status != MidiRendererStatus.File) return; instrument.MidiEventBuffer.Clear(); instrument.Renderer.PlayerTick = playerTick; var tick = instrument.Renderer.SequencerTick; // We add a "all notes off" message. for (byte i = 0; i < 16; i++) { instrument.MidiEventBuffer.Add(new MidiEvent() { Tick = tick, Type = 176, Control = 123, Velocity = 0, Channel = i, }); } // Now we add a Reset All Controllers message. instrument.MidiEventBuffer.Add(new MidiEvent() { Tick = tick, Type = 176, Control = 121, Value = 0, }); } public bool OpenInput(EntityUid uid, InstrumentComponent? instrument = null) { if (!Resolve(uid, ref instrument, false)) return false; SetupRenderer(uid, false, instrument); if (instrument.Renderer != null && instrument.Renderer.OpenInput()) { instrument.Renderer.OnMidiEvent += instrument.MidiEventBuffer.Add; return true; } return false; } public bool OpenMidi(EntityUid uid, ReadOnlySpan data, InstrumentComponent? instrument = null) { if (!Resolve(uid, ref instrument)) return false; SetupRenderer(uid, false, instrument); if (instrument.Renderer == null || !instrument.Renderer.OpenMidi(data)) { return false; } instrument.Renderer.OnMidiEvent += instrument.MidiEventBuffer.Add; return true; } public bool CloseInput(EntityUid uid, bool fromStateChange, InstrumentComponent? instrument = null) { if (!Resolve(uid, ref instrument)) return false; if (instrument.Renderer == null || !instrument.Renderer.CloseInput()) { return false; } EndRenderer(uid, fromStateChange, instrument); return true; } public bool CloseMidi(EntityUid uid, bool fromStateChange, InstrumentComponent? instrument = null) { if (!Resolve(uid, ref instrument)) return false; if (instrument.Renderer == null || !instrument.Renderer.CloseMidi()) { return false; } EndRenderer(uid, fromStateChange, instrument); return true; } private void OnMaxMidiEventsPerSecondChanged(int obj) { MaxMidiEventsPerSecond = obj; } private void OnMaxMidiEventsPerBatchChanged(int obj) { MaxMidiEventsPerBatch = obj; } private void OnMidiEventRx(InstrumentMidiEventEvent midiEv) { var uid = midiEv.Uid; if (!EntityManager.TryGetComponent(uid, out InstrumentComponent? instrument)) return; var renderer = instrument.Renderer; if (renderer != null) { // If we're the ones sending the MidiEvents, we ignore this message. if (instrument.IsInputOpen || instrument.IsMidiOpen) return; } else { // if we haven't started or finished some sequence if (instrument.SequenceStartTick == 0) { // we may have arrived late SetupRenderer(uid, true, instrument); } // might be our own notes after we already finished playing return; } if (instrument.SequenceStartTick <= 0) { instrument.SequenceStartTick = midiEv.MidiEvent.Min(x => x.Tick) - 1; } var sqrtLag = MathF.Sqrt(_netManager.ServerChannel!.Ping / 1000f); var delay = (uint) (renderer.SequencerTimeScale * (.2 + sqrtLag)); var delta = delay - instrument.SequenceStartTick; instrument.SequenceDelay = Math.Max(instrument.SequenceDelay, delta); var currentTick = renderer.SequencerTick; // ReSharper disable once ForCanBeConvertedToForeach for (var i = 0; i < midiEv.MidiEvent.Length; i++) { var ev = midiEv.MidiEvent[i]; var scheduled = ev.Tick + instrument.SequenceDelay; if (scheduled <= currentTick) { instrument.SequenceDelay += currentTick - ev.Tick; scheduled = ev.Tick + instrument.SequenceDelay; } instrument.Renderer?.ScheduleMidiEvent(ev, scheduled, true); } } private void OnMidiStart(InstrumentStartMidiEvent ev) { SetupRenderer(ev.Uid, true); } private void OnMidiStop(InstrumentStopMidiEvent ev) { EndRenderer(ev.Uid, true); } public override void Update(float frameTime) { base.Update(frameTime); if (!_gameTiming.IsFirstTimePredicted) { return; } foreach (var instrument in EntityManager.EntityQuery(true)) { if (instrument.DirtyRenderer && instrument.Renderer != null) UpdateRenderer(instrument.Owner, instrument); if (!instrument.IsMidiOpen && !instrument.IsInputOpen) continue; var now = _gameTiming.RealTime; var oneSecAGo = now.Add(OneSecAgo); if (instrument.LastMeasured <= oneSecAGo) { instrument.LastMeasured = now; instrument.SentWithinASec = 0; } if (instrument.MidiEventBuffer.Count == 0) continue; var max = instrument.RespectMidiLimits ? Math.Min(MaxMidiEventsPerBatch, MaxMidiEventsPerSecond - instrument.SentWithinASec) : instrument.MidiEventBuffer.Count; if (max <= 0) { // hit event/sec limit, have to lag the batch or drop events continue; } // fix cross-fade events generating retroactive events // also handle any significant backlog of events after midi finished instrument.MidiEventBuffer.Sort(SortMidiEventTick); var bufferTicks = instrument.IsRendererAlive && instrument.Renderer!.Status != MidiRendererStatus.None ? instrument.Renderer.SequencerTimeScale * .2f : 0; var bufferedTick = instrument.IsRendererAlive ? instrument.Renderer!.SequencerTick - bufferTicks : int.MaxValue; var events = instrument.MidiEventBuffer .TakeWhile(x => x.Tick < bufferedTick) .Take(max) .ToArray(); var eventCount = events.Length; if (eventCount == 0) continue; RaiseNetworkEvent(new InstrumentMidiEventEvent(instrument.Owner, events)); instrument.SentWithinASec += eventCount; instrument.MidiEventBuffer.RemoveRange(0, eventCount); } } } }