using System;
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Instruments;
using Content.Shared.Physics;
using Robust.Client.Audio.Midi;
using Robust.Shared.Audio.Midi;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
namespace Content.Client.Instruments
{
[RegisterComponent]
public class InstrumentComponent : SharedInstrumentComponent
{
///
/// Called when a midi song stops playing.
///
public event Action? OnMidiPlaybackEnded;
[Dependency] private readonly IMidiManager _midiManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IClientNetManager _netManager = default!;
private IMidiRenderer? _renderer;
private InstrumentSystem _instrumentSystem = default!;
[DataField("program")]
private byte _instrumentProgram = 1;
[DataField("bank")]
private byte _instrumentBank;
private uint _sequenceDelay;
private uint _sequenceStartTick;
[DataField("allowPercussion")]
private bool _allowPercussion;
[DataField("allowProgramChange")]
private bool _allowProgramChange;
[DataField("respectMidiLimits")]
private bool _respectMidiLimits = true;
///
/// A queue of MidiEvents to be sent to the server.
///
[ViewVariables]
private readonly List _midiEventBuffer = new();
///
/// Whether a midi song will loop or not.
///
[ViewVariables(VVAccess.ReadWrite)]
public bool LoopMidi
{
get => _renderer?.LoopMidi ?? false;
set
{
if (_renderer != null)
{
_renderer.LoopMidi = value;
}
}
}
///
/// Changes the instrument the midi renderer will play.
///
[ViewVariables(VVAccess.ReadWrite)]
public override byte InstrumentProgram
{
get => _instrumentProgram;
set
{
_instrumentProgram = value;
if (_renderer != null)
{
_renderer.MidiProgram = _instrumentProgram;
}
}
}
///
/// Changes the instrument bank the midi renderer will use.
///
[ViewVariables(VVAccess.ReadWrite)]
public override byte InstrumentBank
{
get => _instrumentBank;
set
{
_instrumentBank = value;
if (_renderer != null)
{
_renderer.MidiBank = _instrumentBank;
}
}
}
[ViewVariables(VVAccess.ReadWrite)]
public override bool AllowPercussion
{
get => _allowPercussion;
set
{
_allowPercussion = value;
if (_renderer != null)
{
_renderer.DisablePercussionChannel = !_allowPercussion;
}
}
}
[ViewVariables(VVAccess.ReadWrite)]
public override bool AllowProgramChange
{
get => _allowProgramChange;
set
{
_allowProgramChange = value;
if (_renderer != null)
{
_renderer.DisableProgramChangeEvent = !_allowProgramChange;
}
}
}
///
/// Whether this instrument is handheld or not.
///
[ViewVariables]
[DataField("handheld")]
public bool Handheld { get; set; } // TODO: Replace this by simply checking if the entity has an ItemComponent.
///
/// Whether there's a midi song being played or not.
///
[ViewVariables]
public bool IsMidiOpen => _renderer?.Status == MidiRendererStatus.File;
///
/// Whether the midi renderer is listening for midi input or not.
///
[ViewVariables]
public bool IsInputOpen => _renderer?.Status == MidiRendererStatus.Input;
///
/// Whether the midi renderer is alive or not.
///
[ViewVariables]
public bool IsRendererAlive => _renderer != null;
[ViewVariables]
public int PlayerTotalTick => _renderer?.PlayerTotalTick ?? 0;
[ViewVariables]
public int PlayerTick
{
get => _renderer?.PlayerTick ?? 0;
set
{
if (!IsRendererAlive || _renderer!.Status != MidiRendererStatus.File) return;
_midiEventBuffer.Clear();
_renderer.PlayerTick = value;
var tick = _renderer.SequencerTick;
// We add a "all notes off" message.
for (byte i = 0; i < 16; i++)
{
_midiEventBuffer.Add(new MidiEvent()
{
Tick = tick, Type = 176,
Control = 123, Velocity = 0, Channel = i,
});
}
// Now we add a Reset All Controllers message.
_midiEventBuffer.Add(new MidiEvent()
{
Tick = tick, Type = 176,
Control = 121, Value = 0,
});
}
}
public override void Initialize()
{
base.Initialize();
IoCManager.InjectDependencies(this);
_instrumentSystem = EntitySystem.Get();
}
protected virtual void SetupRenderer(bool fromStateChange = false)
{
if (IsRendererAlive) return;
_sequenceDelay = 0;
_sequenceStartTick = 0;
_midiManager.OcclusionCollisionMask = (int) CollisionGroup.Impassable;
_renderer = _midiManager.GetNewRenderer();
if (_renderer != null)
{
_renderer.MidiBank = _instrumentBank;
_renderer.MidiProgram = _instrumentProgram;
_renderer.TrackingEntity = Owner;
_renderer.DisablePercussionChannel = !_allowPercussion;
_renderer.DisableProgramChangeEvent = !_allowProgramChange;
_renderer.OnMidiPlayerFinished += () =>
{
OnMidiPlaybackEnded?.Invoke();
EndRenderer(fromStateChange);
};
}
if (!fromStateChange)
{
SendNetworkMessage(new InstrumentStartMidiMessage());
}
}
protected void EndRenderer(bool fromStateChange = false)
{
if (IsInputOpen)
{
CloseInput(fromStateChange);
return;
}
if (IsMidiOpen)
{
CloseMidi(fromStateChange);
return;
}
_renderer?.StopAllNotes();
var renderer = _renderer;
// We dispose of the synth two seconds from now to allow the last notes to stop from playing.
Owner.SpawnTimer(2000, () => { renderer?.Dispose(); });
_renderer = null;
_midiEventBuffer.Clear();
if (!fromStateChange)
{
SendNetworkMessage(new InstrumentStopMidiMessage());
}
}
protected override void Shutdown()
{
base.Shutdown();
EndRenderer();
}
public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession? session = null)
{
base.HandleNetworkMessage(message, channel, session);
switch (message)
{
case InstrumentMidiEventMessage midiEventMessage:
if (IsRendererAlive)
{
// If we're the ones sending the MidiEvents, we ignore this message.
if (IsInputOpen || IsMidiOpen) break;
}
else
{
// if we haven't started or finished some sequence
if (_sequenceStartTick == 0)
{
// we may have arrived late
SetupRenderer(true);
}
// might be our own notes after we already finished playing
return;
}
if (_sequenceStartTick <= 0)
{
_sequenceStartTick = midiEventMessage.MidiEvent
.Min(x => x.Tick) - 1;
}
var sqrtLag = MathF.Sqrt(_netManager.ServerChannel!.Ping / 1000f);
var delay = (uint) (_renderer!.SequencerTimeScale * (.2 + sqrtLag));
var delta = delay - _sequenceStartTick;
_sequenceDelay = Math.Max(_sequenceDelay, delta);
var currentTick = _renderer.SequencerTick;
// ReSharper disable once ForCanBeConvertedToForeach
for (var i = 0; i < midiEventMessage.MidiEvent.Length; i++)
{
var ev = midiEventMessage.MidiEvent[i];
var scheduled = ev.Tick + _sequenceDelay;
if (scheduled <= currentTick)
{
_sequenceDelay += currentTick - ev.Tick;
scheduled = ev.Tick + _sequenceDelay;
}
_renderer?.ScheduleMidiEvent(ev, scheduled, true);
}
break;
case InstrumentStartMidiMessage _:
{
SetupRenderer(true);
break;
}
case InstrumentStopMidiMessage _:
{
EndRenderer(true);
break;
}
}
}
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
base.HandleComponentState(curState, nextState);
if (curState is not InstrumentState state) return;
if (state.Playing)
{
SetupRenderer(true);
}
else
{
EndRenderer(true);
}
AllowPercussion = state.AllowPercussion;
AllowProgramChange = state.AllowProgramChange;
InstrumentBank = state.InstrumentBank;
InstrumentProgram = state.InstrumentProgram;
}
///
public bool OpenInput()
{
SetupRenderer();
if (_renderer != null && _renderer.OpenInput())
{
_renderer.OnMidiEvent += RendererOnMidiEvent;
return true;
}
return false;
}
///
public bool CloseInput(bool fromStateChange = false)
{
if (_renderer == null || !_renderer.CloseInput())
{
return false;
}
EndRenderer(fromStateChange);
return true;
}
public bool OpenMidi(ReadOnlySpan data)
{
SetupRenderer();
if (_renderer == null || !_renderer.OpenMidi(data))
{
return false;
}
_renderer.OnMidiEvent += RendererOnMidiEvent;
return true;
}
///
public bool CloseMidi(bool fromStateChange = false)
{
if (_renderer == null || !_renderer.CloseMidi())
{
return false;
}
EndRenderer(fromStateChange);
return true;
}
///
/// Called whenever the renderer receives a midi event.
///
/// The received midi event
private void RendererOnMidiEvent(MidiEvent midiEvent)
{
_midiEventBuffer.Add(midiEvent);
}
private TimeSpan _lastMeasured = TimeSpan.MinValue;
private int _sentWithinASec;
private static readonly TimeSpan OneSecAgo = TimeSpan.FromSeconds(-1);
private static readonly Comparer SortMidiEventTick
= Comparer.Create((x, y)
=> x.Tick.CompareTo(y.Tick));
public override void Update(float delta)
{
if (!IsMidiOpen && !IsInputOpen) return;
var now = _gameTiming.RealTime;
var oneSecAGo = now.Add(OneSecAgo);
if (_lastMeasured <= oneSecAGo)
{
_lastMeasured = now;
_sentWithinASec = 0;
}
if (_midiEventBuffer.Count == 0) return;
var max = _respectMidiLimits ?
Math.Min(_instrumentSystem.MaxMidiEventsPerBatch, _instrumentSystem.MaxMidiEventsPerSecond - _sentWithinASec)
: _midiEventBuffer.Count;
if (max <= 0)
{
// hit event/sec limit, have to lag the batch or drop events
return;
}
// fix cross-fade events generating retroactive events
// also handle any significant backlog of events after midi finished
_midiEventBuffer.Sort(SortMidiEventTick);
var bufferTicks = IsRendererAlive && _renderer!.Status != MidiRendererStatus.None
? _renderer.SequencerTimeScale * .2f
: 0;
var bufferedTick = IsRendererAlive
? _renderer!.SequencerTick - bufferTicks
: int.MaxValue;
var events = _midiEventBuffer
.TakeWhile(x => x.Tick < bufferedTick)
.Take(max)
.ToArray();
var eventCount = events.Length;
if (eventCount == 0) return;
SendNetworkMessage(new InstrumentMidiEventMessage(events));
_sentWithinASec += eventCount;
_midiEventBuffer.RemoveRange(0, eventCount);
}
}
}