378 lines
13 KiB
C#
378 lines
13 KiB
C#
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<MidiEvent> SortMidiEventTick
|
|
= Comparer<MidiEvent>.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();
|
|
|
|
_cfg.OnValueChanged(CCVars.MaxMidiEventsPerBatch, OnMaxMidiEventsPerBatchChanged, true);
|
|
_cfg.OnValueChanged(CCVars.MaxMidiEventsPerSecond, OnMaxMidiEventsPerSecondChanged, true);
|
|
|
|
SubscribeNetworkEvent<InstrumentMidiEventEvent>(OnMidiEventRx);
|
|
SubscribeNetworkEvent<InstrumentStartMidiEvent>(OnMidiStart);
|
|
SubscribeNetworkEvent<InstrumentStopMidiEvent>(OnMidiStop);
|
|
|
|
SubscribeLocalEvent<InstrumentComponent, ComponentShutdown>(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.
|
|
instrument.Owner.SpawnTimer(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<byte> 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<InstrumentComponent>(true))
|
|
{
|
|
if (instrument.DirtyRenderer && instrument.Renderer != null)
|
|
UpdateRenderer(instrument.OwnerUid, 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.OwnerUid, events));
|
|
|
|
instrument.SentWithinASec += eventCount;
|
|
|
|
instrument.MidiEventBuffer.RemoveRange(0, eventCount);
|
|
}
|
|
}
|
|
}
|
|
}
|