InstrumentSystem.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. using System.Linq;
  2. using Content.Shared.CCVar;
  3. using Content.Shared.Instruments;
  4. using Content.Shared.Physics;
  5. using JetBrains.Annotations;
  6. using Robust.Client.Audio.Midi;
  7. using Robust.Shared.Audio.Midi;
  8. using Robust.Shared.Configuration;
  9. using Robust.Shared.GameStates;
  10. using Robust.Shared.Network;
  11. using Robust.Shared.Timing;
  12. namespace Content.Client.Instruments;
  13. public sealed class InstrumentSystem : SharedInstrumentSystem
  14. {
  15. [Dependency] private readonly IClientNetManager _netManager = default!;
  16. [Dependency] private readonly IMidiManager _midiManager = default!;
  17. [Dependency] private readonly IGameTiming _gameTiming = default!;
  18. [Dependency] private readonly IConfigurationManager _cfg = default!;
  19. public readonly TimeSpan OneSecAgo = TimeSpan.FromSeconds(-1);
  20. public int MaxMidiEventsPerBatch { get; private set; }
  21. public int MaxMidiEventsPerSecond { get; private set; }
  22. public override void Initialize()
  23. {
  24. base.Initialize();
  25. UpdatesOutsidePrediction = true;
  26. Subs.CVar(_cfg, CCVars.MaxMidiEventsPerBatch, OnMaxMidiEventsPerBatchChanged, true);
  27. Subs.CVar(_cfg, CCVars.MaxMidiEventsPerSecond, OnMaxMidiEventsPerSecondChanged, true);
  28. SubscribeNetworkEvent<InstrumentMidiEventEvent>(OnMidiEventRx);
  29. SubscribeNetworkEvent<InstrumentStartMidiEvent>(OnMidiStart);
  30. SubscribeNetworkEvent<InstrumentStopMidiEvent>(OnMidiStop);
  31. SubscribeLocalEvent<InstrumentComponent, ComponentShutdown>(OnShutdown);
  32. SubscribeLocalEvent<InstrumentComponent, ComponentHandleState>(OnHandleState);
  33. }
  34. private void OnHandleState(EntityUid uid, SharedInstrumentComponent component, ref ComponentHandleState args)
  35. {
  36. if (args.Current is not InstrumentComponentState state)
  37. return;
  38. component.Playing = state.Playing;
  39. component.InstrumentProgram = state.InstrumentProgram;
  40. component.InstrumentBank = state.InstrumentBank;
  41. component.AllowPercussion = state.AllowPercussion;
  42. component.AllowProgramChange = state.AllowProgramChange;
  43. component.RespectMidiLimits = state.RespectMidiLimits;
  44. component.Master = EnsureEntity<InstrumentComponent>(state.Master, uid);
  45. component.FilteredChannels = state.FilteredChannels;
  46. if (component.Playing)
  47. SetupRenderer(uid, true, component);
  48. else
  49. EndRenderer(uid, true, component);
  50. }
  51. private void OnShutdown(EntityUid uid, InstrumentComponent component, ComponentShutdown args)
  52. {
  53. EndRenderer(uid, false, component);
  54. }
  55. public void SetMaster(EntityUid uid, EntityUid? masterUid)
  56. {
  57. if (!HasComp<InstrumentComponent>(uid))
  58. return;
  59. RaiseNetworkEvent(new InstrumentSetMasterEvent(GetNetEntity(uid), GetNetEntity(masterUid)));
  60. }
  61. public void SetFilteredChannel(EntityUid uid, int channel, bool value)
  62. {
  63. if (!TryComp(uid, out InstrumentComponent? instrument))
  64. return;
  65. if(value)
  66. instrument.Renderer?.SendMidiEvent(RobustMidiEvent.AllNotesOff((byte)channel, 0), false);
  67. RaiseNetworkEvent(new InstrumentSetFilteredChannelEvent(GetNetEntity(uid), channel, value));
  68. }
  69. public override bool ResolveInstrument(EntityUid uid, ref SharedInstrumentComponent? component)
  70. {
  71. if (component is not null)
  72. return true;
  73. TryComp<InstrumentComponent>(uid, out var localComp);
  74. component = localComp;
  75. return component != null;
  76. }
  77. public override void SetupRenderer(EntityUid uid, bool fromStateChange, SharedInstrumentComponent? component = null)
  78. {
  79. if (!ResolveInstrument(uid, ref component))
  80. return;
  81. if (component is not InstrumentComponent instrument)
  82. {
  83. return;
  84. }
  85. if (instrument.IsRendererAlive)
  86. {
  87. if (fromStateChange)
  88. {
  89. UpdateRenderer(uid, instrument);
  90. }
  91. return;
  92. }
  93. instrument.SequenceDelay = 0;
  94. instrument.SequenceStartTick = 0;
  95. instrument.Renderer = _midiManager.GetNewRenderer();
  96. if (instrument.Renderer != null)
  97. {
  98. instrument.Renderer.SendMidiEvent(RobustMidiEvent.SystemReset(instrument.Renderer.SequencerTick));
  99. UpdateRenderer(uid, instrument);
  100. instrument.Renderer.OnMidiPlayerFinished += () =>
  101. {
  102. instrument.PlaybackEndedInvoke();
  103. EndRenderer(uid, fromStateChange, instrument);
  104. };
  105. }
  106. if (!fromStateChange)
  107. {
  108. RaiseNetworkEvent(new InstrumentStartMidiEvent(GetNetEntity(uid)));
  109. }
  110. }
  111. public void UpdateRenderer(EntityUid uid, InstrumentComponent? instrument = null)
  112. {
  113. if (!Resolve(uid, ref instrument) || instrument.Renderer == null)
  114. return;
  115. instrument.Renderer.TrackingEntity = uid;
  116. instrument.Renderer.FilteredChannels.SetAll(false);
  117. instrument.Renderer.FilteredChannels.Or(instrument.FilteredChannels);
  118. instrument.Renderer.DisablePercussionChannel = !instrument.AllowPercussion;
  119. instrument.Renderer.DisableProgramChangeEvent = !instrument.AllowProgramChange;
  120. for (int i = 0; i < RobustMidiEvent.MaxChannels; i++)
  121. {
  122. if(instrument.FilteredChannels[i])
  123. instrument.Renderer.SendMidiEvent(RobustMidiEvent.AllNotesOff((byte)i, 0));
  124. }
  125. if (!instrument.AllowProgramChange)
  126. {
  127. instrument.Renderer.MidiBank = instrument.InstrumentBank;
  128. instrument.Renderer.MidiProgram = instrument.InstrumentProgram;
  129. }
  130. UpdateRendererMaster(instrument);
  131. instrument.Renderer.LoopMidi = instrument.LoopMidi;
  132. }
  133. private void UpdateRendererMaster(InstrumentComponent instrument)
  134. {
  135. if (instrument.Renderer == null || instrument.Master == null)
  136. return;
  137. if (!TryComp(instrument.Master, out InstrumentComponent? masterInstrument) || masterInstrument.Renderer == null)
  138. return;
  139. instrument.Renderer.Master = masterInstrument.Renderer;
  140. }
  141. public override void EndRenderer(EntityUid uid, bool fromStateChange, SharedInstrumentComponent? component = null)
  142. {
  143. if (!ResolveInstrument(uid, ref component))
  144. return;
  145. if (component is not InstrumentComponent instrument)
  146. return;
  147. if (instrument.IsInputOpen)
  148. {
  149. CloseInput(uid, fromStateChange, instrument);
  150. return;
  151. }
  152. if (instrument.IsMidiOpen)
  153. {
  154. CloseMidi(uid, fromStateChange, instrument);
  155. return;
  156. }
  157. instrument.Renderer?.SystemReset();
  158. instrument.Renderer?.ClearAllEvents();
  159. var renderer = instrument.Renderer;
  160. // We dispose of the synth two seconds from now to allow the last notes to stop from playing.
  161. // Don't use timers bound to the entity in case it is getting deleted.
  162. if (renderer != null)
  163. Timer.Spawn(2000, () => { renderer.Dispose(); });
  164. instrument.Renderer = null;
  165. instrument.MidiEventBuffer.Clear();
  166. if (!fromStateChange && _netManager.IsConnected)
  167. {
  168. RaiseNetworkEvent(new InstrumentStopMidiEvent(GetNetEntity(uid)));
  169. }
  170. }
  171. public void SetPlayerTick(EntityUid uid, int playerTick, InstrumentComponent? instrument = null)
  172. {
  173. if (!Resolve(uid, ref instrument))
  174. return;
  175. if (instrument.Renderer is not { Status: MidiRendererStatus.File })
  176. return;
  177. instrument.MidiEventBuffer.Clear();
  178. var tick = instrument.Renderer.SequencerTick-1;
  179. instrument.MidiEventBuffer.Add(RobustMidiEvent.SystemReset(tick));
  180. instrument.Renderer.PlayerTick = playerTick;
  181. }
  182. public bool OpenInput(EntityUid uid, InstrumentComponent? instrument = null)
  183. {
  184. if (!Resolve(uid, ref instrument, false))
  185. return false;
  186. SetupRenderer(uid, false, instrument);
  187. if (instrument.Renderer == null || !instrument.Renderer.OpenInput())
  188. return false;
  189. SetMaster(uid, null);
  190. instrument.MidiEventBuffer.Clear();
  191. instrument.Renderer.OnMidiEvent += instrument.MidiEventBuffer.Add;
  192. return true;
  193. }
  194. public bool OpenMidi(EntityUid uid, ReadOnlySpan<byte> data, InstrumentComponent? instrument = null)
  195. {
  196. if (!Resolve(uid, ref instrument))
  197. return false;
  198. SetupRenderer(uid, false, instrument);
  199. if (instrument.Renderer == null || !instrument.Renderer.OpenMidi(data))
  200. return false;
  201. SetMaster(uid, null);
  202. instrument.MidiEventBuffer.Clear();
  203. instrument.Renderer.OnMidiEvent += instrument.MidiEventBuffer.Add;
  204. return true;
  205. }
  206. public bool CloseInput(EntityUid uid, bool fromStateChange, InstrumentComponent? instrument = null)
  207. {
  208. if (!Resolve(uid, ref instrument))
  209. return false;
  210. if (instrument.Renderer == null || !instrument.Renderer.CloseInput())
  211. {
  212. return false;
  213. }
  214. EndRenderer(uid, fromStateChange, instrument);
  215. return true;
  216. }
  217. public bool CloseMidi(EntityUid uid, bool fromStateChange, InstrumentComponent? instrument = null)
  218. {
  219. if (!Resolve(uid, ref instrument))
  220. return false;
  221. if (instrument.Renderer == null || !instrument.Renderer.CloseMidi())
  222. {
  223. return false;
  224. }
  225. EndRenderer(uid, fromStateChange, instrument);
  226. return true;
  227. }
  228. private void OnMaxMidiEventsPerSecondChanged(int obj)
  229. {
  230. MaxMidiEventsPerSecond = obj;
  231. }
  232. private void OnMaxMidiEventsPerBatchChanged(int obj)
  233. {
  234. MaxMidiEventsPerBatch = obj;
  235. }
  236. private void OnMidiEventRx(InstrumentMidiEventEvent midiEv)
  237. {
  238. var uid = GetEntity(midiEv.Uid);
  239. if (!TryComp(uid, out InstrumentComponent? instrument))
  240. return;
  241. var renderer = instrument.Renderer;
  242. if (renderer != null)
  243. {
  244. // If we're the ones sending the MidiEvents, we ignore this message.
  245. if (instrument.IsInputOpen || instrument.IsMidiOpen)
  246. return;
  247. }
  248. else
  249. {
  250. // if we haven't started or finished some sequence
  251. if (instrument.SequenceStartTick == 0)
  252. {
  253. // we may have arrived late
  254. SetupRenderer(uid, true, instrument);
  255. }
  256. // might be our own notes after we already finished playing
  257. return;
  258. }
  259. if (instrument.SequenceStartTick <= 0)
  260. {
  261. instrument.SequenceStartTick = midiEv.MidiEvent.Min(x => x.Tick) - 1;
  262. }
  263. var sqrtLag = MathF.Sqrt((_netManager.ServerChannel?.Ping ?? 0)/ 1000f);
  264. var delay = (uint) (renderer.SequencerTimeScale * (.2 + sqrtLag));
  265. var delta = delay - instrument.SequenceStartTick;
  266. instrument.SequenceDelay = Math.Max(instrument.SequenceDelay, delta);
  267. SendMidiEvents(midiEv.MidiEvent, instrument);
  268. }
  269. private void SendMidiEvents(IReadOnlyList<RobustMidiEvent> midiEvents, InstrumentComponent instrument)
  270. {
  271. if (instrument.Renderer == null)
  272. {
  273. Log.Warning($"Tried to send Midi events to an instrument without a renderer.");
  274. return;
  275. }
  276. var currentTick = instrument.Renderer.SequencerTick;
  277. // ReSharper disable once ForCanBeConvertedToForeach
  278. for (uint i = 0; i < midiEvents.Count; i++)
  279. {
  280. // I am surprised this doesn't take uint...
  281. var ev = midiEvents[(int)i];
  282. var scheduled = ev.Tick + instrument.SequenceDelay;
  283. if (scheduled < currentTick)
  284. {
  285. instrument.SequenceDelay += currentTick - ev.Tick;
  286. scheduled = ev.Tick + instrument.SequenceDelay;
  287. }
  288. // The order of events with the same timestamp is undefined in Fluidsynth's sequencer...
  289. // Therefore we add the event index to the scheduled time to ensure every event has an unique timestamp.
  290. instrument.Renderer?.ScheduleMidiEvent(ev, scheduled+i, true);
  291. }
  292. }
  293. private void OnMidiStart(InstrumentStartMidiEvent ev)
  294. {
  295. SetupRenderer(GetEntity(ev.Uid), true);
  296. }
  297. private void OnMidiStop(InstrumentStopMidiEvent ev)
  298. {
  299. EndRenderer(GetEntity(ev.Uid), true);
  300. }
  301. public override void Update(float frameTime)
  302. {
  303. base.Update(frameTime);
  304. if (!_gameTiming.IsFirstTimePredicted)
  305. {
  306. return;
  307. }
  308. var query = EntityQueryEnumerator<InstrumentComponent>();
  309. while (query.MoveNext(out var uid, out var instrument))
  310. {
  311. // For cases where the master renderer was not created yet.
  312. if (instrument is { Renderer.Master: null, Master: not null })
  313. UpdateRendererMaster(instrument);
  314. if (instrument is { IsMidiOpen: false, IsInputOpen: false })
  315. continue;
  316. var now = _gameTiming.RealTime;
  317. var oneSecAGo = now.Add(OneSecAgo);
  318. if (instrument.LastMeasured <= oneSecAGo)
  319. {
  320. instrument.LastMeasured = now;
  321. instrument.SentWithinASec = 0;
  322. }
  323. if (instrument.MidiEventBuffer.Count == 0)
  324. continue;
  325. var max = instrument.RespectMidiLimits
  326. ? Math.Min(MaxMidiEventsPerBatch, MaxMidiEventsPerSecond - instrument.SentWithinASec)
  327. : instrument.MidiEventBuffer.Count;
  328. if (max <= 0)
  329. {
  330. // hit event/sec limit, have to lag the batch or drop events
  331. continue;
  332. }
  333. // fix cross-fade events generating retroactive events
  334. // also handle any significant backlog of events after midi finished
  335. var bufferTicks = instrument.IsRendererAlive && instrument.Renderer!.Status != MidiRendererStatus.None
  336. ? instrument.Renderer.SequencerTimeScale * .2f
  337. : 0;
  338. var bufferedTick = instrument.IsRendererAlive
  339. ? instrument.Renderer!.SequencerTick - bufferTicks
  340. : int.MaxValue;
  341. // TODO: Remove LINQ brain-rot.
  342. var events = instrument.MidiEventBuffer
  343. .TakeWhile(x => x.Tick < bufferedTick)
  344. .Take(max)
  345. .ToArray();
  346. var eventCount = events.Length;
  347. if (eventCount == 0)
  348. continue;
  349. RaiseNetworkEvent(new InstrumentMidiEventEvent(GetNetEntity(uid), events));
  350. instrument.SentWithinASec += eventCount;
  351. instrument.MidiEventBuffer.RemoveRange(0, eventCount);
  352. }
  353. }
  354. }