mirror of
https://github.com/space-wizards/space-station-14.git
synced 2026-02-15 03:31:30 +01:00
Add the instrument names to the MIDI channel selector (#38083)
* Add the instrument to the MIDI channel selector * Reviews Adds support for chained masters Makes the channel UI update on its own when the midi changes (Works with bands too!) * add to admin logs and limit track count * Limit track names by length too * remove left over comment * Requested changes * Reviews
This commit is contained in:
54
Content.Client/Instruments/InstrumentSystem.MidiParsing.cs
Normal file
54
Content.Client/Instruments/InstrumentSystem.MidiParsing.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Instruments;
|
||||
using Robust.Shared.Audio.Midi;
|
||||
|
||||
namespace Content.Client.Instruments;
|
||||
|
||||
public sealed partial class InstrumentSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to parse the input data as a midi and set the channel names respectively.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Thank you to http://www.somascape.org/midi/tech/mfile.html for providing an awesome resource for midi files.
|
||||
/// </remarks>
|
||||
/// <remarks>
|
||||
/// This method has exception tolerance and does not throw, even if the midi file is invalid.
|
||||
/// </remarks>
|
||||
private bool TrySetChannels(EntityUid uid, byte[] data)
|
||||
{
|
||||
if (!MidiParser.MidiParser.TryGetMidiTracks(data, out var tracks, out var error))
|
||||
{
|
||||
Log.Error(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
var resolvedTracks = new List<MidiTrack?>();
|
||||
for (var index = 0; index < tracks.Length; index++)
|
||||
{
|
||||
var midiTrack = tracks[index];
|
||||
if (midiTrack is { TrackName: null, ProgramName: null, InstrumentName: null})
|
||||
continue;
|
||||
|
||||
switch (midiTrack)
|
||||
{
|
||||
case { TrackName: not null, ProgramName: not null }:
|
||||
case { TrackName: not null, InstrumentName: not null }:
|
||||
case { TrackName: not null }:
|
||||
case { ProgramName: not null }:
|
||||
resolvedTracks.Add(midiTrack);
|
||||
break;
|
||||
default:
|
||||
resolvedTracks.Add(null); // Used so the channel still displays as MIDI Channel X and doesn't just take the next valid one in the UI
|
||||
break;
|
||||
}
|
||||
|
||||
Log.Debug($"Channel name: {resolvedTracks.Last()}");
|
||||
}
|
||||
|
||||
RaiseNetworkEvent(new InstrumentSetChannelsEvent(GetNetEntity(uid), resolvedTracks.Take(RobustMidiEvent.MaxChannels).ToArray()));
|
||||
Log.Debug($"Resolved {resolvedTracks.Count} channels.");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Instruments;
|
||||
@@ -12,7 +13,7 @@ using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Instruments;
|
||||
|
||||
public sealed class InstrumentSystem : SharedInstrumentSystem
|
||||
public sealed partial class InstrumentSystem : SharedInstrumentSystem
|
||||
{
|
||||
[Dependency] private readonly IClientNetManager _netManager = default!;
|
||||
[Dependency] private readonly IMidiManager _midiManager = default!;
|
||||
@@ -23,6 +24,8 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
|
||||
public int MaxMidiEventsPerBatch { get; private set; }
|
||||
public int MaxMidiEventsPerSecond { get; private set; }
|
||||
|
||||
public event Action? OnChannelsUpdated;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -38,6 +41,26 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
|
||||
|
||||
SubscribeLocalEvent<InstrumentComponent, ComponentShutdown>(OnShutdown);
|
||||
SubscribeLocalEvent<InstrumentComponent, ComponentHandleState>(OnHandleState);
|
||||
SubscribeLocalEvent<ActiveInstrumentComponent, AfterAutoHandleStateEvent>(OnActiveInstrumentAfterHandleState);
|
||||
}
|
||||
|
||||
private bool _isUpdateQueued = false;
|
||||
|
||||
private void OnActiveInstrumentAfterHandleState(Entity<ActiveInstrumentComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
// Called in the update loop so that the components update client side for resolving them in TryComps.
|
||||
_isUpdateQueued = true;
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
base.FrameUpdate(frameTime);
|
||||
|
||||
if (!_isUpdateQueued)
|
||||
return;
|
||||
|
||||
_isUpdateQueued = false;
|
||||
OnChannelsUpdated?.Invoke();
|
||||
}
|
||||
|
||||
private void OnHandleState(EntityUid uid, SharedInstrumentComponent component, ref ComponentHandleState args)
|
||||
@@ -252,7 +275,13 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
|
||||
|
||||
}
|
||||
|
||||
[Obsolete("Use overload that takes in byte[] instead.")]
|
||||
public bool OpenMidi(EntityUid uid, ReadOnlySpan<byte> data, InstrumentComponent? instrument = null)
|
||||
{
|
||||
return OpenMidi(uid, data.ToArray(), instrument);
|
||||
}
|
||||
|
||||
public bool OpenMidi(EntityUid uid, byte[] data, InstrumentComponent? instrument = null)
|
||||
{
|
||||
if (!Resolve(uid, ref instrument))
|
||||
return false;
|
||||
@@ -263,6 +292,8 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
|
||||
return false;
|
||||
|
||||
SetMaster(uid, null);
|
||||
TrySetChannels(uid, data);
|
||||
|
||||
instrument.MidiEventBuffer.Clear();
|
||||
instrument.Renderer.OnMidiEvent += instrument.MidiEventBuffer.Add;
|
||||
return true;
|
||||
|
||||
147
Content.Client/Instruments/MidiParser/MidiInstrument.cs
Normal file
147
Content.Client/Instruments/MidiParser/MidiInstrument.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Instruments.MidiParser;
|
||||
|
||||
// This file was autogenerated. Based on https://www.ccarh.org/courses/253/handout/gminstruments/
|
||||
public enum MidiInstrument : byte
|
||||
{
|
||||
AcousticGrandPiano = 0,
|
||||
BrightAcousticPiano = 1,
|
||||
ElectricGrandPiano = 2,
|
||||
HonkyTonkPiano = 3,
|
||||
RhodesPiano = 4,
|
||||
ChorusedPiano = 5,
|
||||
Harpsichord = 6,
|
||||
Clavinet = 7,
|
||||
Celesta = 8,
|
||||
Glockenspiel = 9,
|
||||
MusicBox = 10,
|
||||
Vibraphone = 11,
|
||||
Marimba = 12,
|
||||
Xylophone = 13,
|
||||
TubularBells = 14,
|
||||
Dulcimer = 15,
|
||||
HammondOrgan = 16,
|
||||
PercussiveOrgan = 17,
|
||||
RockOrgan = 18,
|
||||
ChurchOrgan = 19,
|
||||
ReedOrgan = 20,
|
||||
Accordion = 21,
|
||||
Harmonica = 22,
|
||||
TangoAccordion = 23,
|
||||
AcousticNylonGuitar = 24,
|
||||
AcousticSteelGuitar = 25,
|
||||
ElectricJazzGuitar = 26,
|
||||
ElectricCleanGuitar = 27,
|
||||
ElectricMutedGuitar = 28,
|
||||
OverdrivenGuitar = 29,
|
||||
DistortionGuitar = 30,
|
||||
GuitarHarmonics = 31,
|
||||
AcousticBass = 32,
|
||||
FingeredElectricBass = 33,
|
||||
PluckedElectricBass = 34,
|
||||
FretlessBass = 35,
|
||||
SlapBass1 = 36,
|
||||
SlapBass2 = 37,
|
||||
SynthBass1 = 38,
|
||||
SynthBass2 = 39,
|
||||
Violin = 40,
|
||||
Viola = 41,
|
||||
Cello = 42,
|
||||
Contrabass = 43,
|
||||
TremoloStrings = 44,
|
||||
PizzicatoStrings = 45,
|
||||
OrchestralHarp = 46,
|
||||
Timpani = 47,
|
||||
StringEnsemble1 = 48,
|
||||
StringEnsemble2 = 49,
|
||||
SynthStrings1 = 50,
|
||||
SynthStrings2 = 51,
|
||||
ChoirAah = 52,
|
||||
VoiceOoh = 53,
|
||||
SynthChoir = 54,
|
||||
OrchestraHit = 55,
|
||||
Trumpet = 56,
|
||||
Trombone = 57,
|
||||
Tuba = 58,
|
||||
MutedTrumpet = 59,
|
||||
FrenchHorn = 60,
|
||||
BrassSection = 61,
|
||||
SynthBrass1 = 62,
|
||||
SynthBrass2 = 63,
|
||||
SopranoSax = 64,
|
||||
AltoSax = 65,
|
||||
TenorSax = 66,
|
||||
BaritoneSax = 67,
|
||||
Oboe = 68,
|
||||
EnglishHorn = 69,
|
||||
Bassoon = 70,
|
||||
Clarinet = 71,
|
||||
Piccolo = 72,
|
||||
Flute = 73,
|
||||
Recorder = 74,
|
||||
PanFlute = 75,
|
||||
BottleBlow = 76,
|
||||
Shakuhachi = 77,
|
||||
Whistle = 78,
|
||||
Ocarina = 79,
|
||||
SquareWaveLead = 80,
|
||||
SawtoothWaveLead = 81,
|
||||
CalliopeLead = 82,
|
||||
ChiffLead = 83,
|
||||
CharangLead = 84,
|
||||
VoiceLead = 85,
|
||||
FithsLead = 86,
|
||||
BassLead = 87,
|
||||
NewAgePad = 88,
|
||||
WarmPad = 89,
|
||||
PolysynthPad = 90,
|
||||
ChoirPad = 91,
|
||||
BowedPad = 92,
|
||||
MetallicPad = 93,
|
||||
HaloPad = 94,
|
||||
SweepPad = 95,
|
||||
RainEffect = 96,
|
||||
SoundtrackEffect = 97,
|
||||
CrystalEffect = 98,
|
||||
AtmosphereEffect = 99,
|
||||
BrightnessEffect = 100,
|
||||
GoblinsEffect = 101,
|
||||
EchoesEffect = 102,
|
||||
SciFiEffect = 103,
|
||||
Sitar = 104,
|
||||
Banjo = 105,
|
||||
Shamisen = 106,
|
||||
Koto = 107,
|
||||
Kalimba = 108,
|
||||
Bagpipe = 109,
|
||||
Fiddle = 110,
|
||||
Shanai = 111,
|
||||
TinkleBell = 112,
|
||||
Agogo = 113,
|
||||
SteelDrums = 114,
|
||||
Woodblock = 115,
|
||||
TaikoDrum = 116,
|
||||
MelodicTom = 117,
|
||||
SynthDrum = 118,
|
||||
ReverseCymbal = 119,
|
||||
GuitarFretNoise = 120,
|
||||
BreathNoise = 121,
|
||||
Seashore = 122,
|
||||
BirdTweet = 123,
|
||||
TelephoneRing = 124,
|
||||
Helicopter = 125,
|
||||
Applause = 126,
|
||||
Gunshot = 127,
|
||||
}
|
||||
|
||||
public static class MidiInstrumentExt
|
||||
{
|
||||
/// <summary>
|
||||
/// Turns the given enum value into it's string representation to be used in localization.
|
||||
/// </summary>
|
||||
public static string GetStringRep(this MidiInstrument instrument)
|
||||
{
|
||||
return CaseConversion.PascalToKebab(instrument.ToString());
|
||||
}
|
||||
}
|
||||
184
Content.Client/Instruments/MidiParser/MidiParser.cs
Normal file
184
Content.Client/Instruments/MidiParser/MidiParser.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using Content.Shared.Instruments;
|
||||
|
||||
namespace Content.Client.Instruments.MidiParser;
|
||||
|
||||
public static class MidiParser
|
||||
{
|
||||
// Thanks again to http://www.somascape.org/midi/tech/mfile.html
|
||||
public static bool TryGetMidiTracks(
|
||||
byte[] data,
|
||||
[NotNullWhen(true)] out MidiTrack[]? tracks,
|
||||
[NotNullWhen(false)] out string? error)
|
||||
{
|
||||
tracks = null;
|
||||
error = null;
|
||||
|
||||
var stream = new MidiStreamWrapper(data);
|
||||
|
||||
if (stream.ReadString(4) != "MThd")
|
||||
{
|
||||
error = "Invalid file header";
|
||||
return false;
|
||||
}
|
||||
|
||||
var headerLength = stream.ReadUInt32();
|
||||
// MIDI specs define that the header is 6 bytes, we only look at the 6 bytes, if its more, we skip ahead.
|
||||
|
||||
stream.Skip(2); // format
|
||||
var trackCount = stream.ReadUInt16();
|
||||
stream.Skip(2); // time div
|
||||
|
||||
// We now skip ahead if we still have any header length left
|
||||
stream.Skip((int)(headerLength - 6));
|
||||
|
||||
var parsedTracks = new List<MidiTrack>();
|
||||
|
||||
for (var i = 0; i < trackCount; i++)
|
||||
{
|
||||
if (stream.ReadString(4) != "MTrk")
|
||||
{
|
||||
tracks = null;
|
||||
error = "Track contains invalid header";
|
||||
return false;
|
||||
}
|
||||
|
||||
var track = new MidiTrack();
|
||||
|
||||
var trackLength = stream.ReadUInt32();
|
||||
var trackEnd = stream.StreamPosition + trackLength;
|
||||
var hasMidiEvent = false;
|
||||
byte? lastStatusByte = null;
|
||||
|
||||
while (stream.StreamPosition < trackEnd)
|
||||
{
|
||||
stream.ReadVariableLengthQuantity();
|
||||
|
||||
/*
|
||||
* If the first (status) byte is less than 128 (hex 80), this implies that running status is in effect,
|
||||
* and that this byte is actually the first data byte (the status carrying over from the previous MIDI event).
|
||||
* This can only be the case if the immediately previous event was also a MIDI event,
|
||||
* i.e. SysEx and Meta events interrupt (clear) running status.
|
||||
* See http://www.somascape.org/midi/tech/mfile.html#events
|
||||
*/
|
||||
|
||||
var firstByte = stream.ReadByte();
|
||||
if (firstByte >= 0x80)
|
||||
{
|
||||
lastStatusByte = firstByte;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Running status: push byte back for reading as data
|
||||
stream.Skip(-1);
|
||||
}
|
||||
|
||||
// The first event in each MTrk chunk must specify status.
|
||||
if (lastStatusByte == null)
|
||||
{
|
||||
tracks = null;
|
||||
error = "Track data not valid, expected status byte, got nothing.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var eventType = (byte)(lastStatusByte & 0xF0);
|
||||
|
||||
switch (lastStatusByte)
|
||||
{
|
||||
// Meta events
|
||||
case 0xFF:
|
||||
{
|
||||
var metaType = stream.ReadByte();
|
||||
var metaLength = stream.ReadVariableLengthQuantity();
|
||||
var metaData = stream.ReadBytes((int)metaLength);
|
||||
if (metaType == 0x00) // SequenceNumber event
|
||||
continue;
|
||||
|
||||
// Meta event types 01 through 0F are reserved for text and all follow the basic FF 01 len text format
|
||||
if (metaType is < 0x01 or > 0x0F)
|
||||
break;
|
||||
|
||||
// 0x03 is TrackName,
|
||||
// 0x04 is InstrumentName
|
||||
|
||||
var text = Encoding.ASCII.GetString(metaData, 0, (int)metaLength);
|
||||
switch (metaType)
|
||||
{
|
||||
case 0x03 when track.TrackName == null:
|
||||
track.TrackName = text;
|
||||
break;
|
||||
case 0x04 when track.InstrumentName == null:
|
||||
track.InstrumentName = text;
|
||||
break;
|
||||
}
|
||||
|
||||
// still here? then we dont care about the event
|
||||
break;
|
||||
}
|
||||
|
||||
// SysEx events
|
||||
case 0xF0:
|
||||
case 0xF7:
|
||||
{
|
||||
var sysexLength = stream.ReadVariableLengthQuantity();
|
||||
stream.Skip((int)sysexLength);
|
||||
// Sysex events and meta-events cancel any running status which was in effect.
|
||||
// Running status does not apply to and may not be used for these messages.
|
||||
lastStatusByte = null;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
default:
|
||||
switch (eventType)
|
||||
{
|
||||
// Program Change
|
||||
case 0xC0:
|
||||
{
|
||||
var programNumber = stream.ReadByte();
|
||||
if (track.ProgramName == null)
|
||||
{
|
||||
if (programNumber < Enum.GetValues<MidiInstrument>().Length)
|
||||
track.ProgramName = Loc.GetString($"instruments-component-menu-midi-channel-{((MidiInstrument)programNumber).GetStringRep()}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x80: // Note Off
|
||||
case 0x90: // Note On
|
||||
case 0xA0: // Polyphonic Key Pressure
|
||||
case 0xB0: // Control Change
|
||||
case 0xE0: // Pitch Bend
|
||||
{
|
||||
hasMidiEvent = true;
|
||||
stream.Skip(2);
|
||||
break;
|
||||
}
|
||||
|
||||
case 0xD0: // Channel Pressure
|
||||
{
|
||||
hasMidiEvent = true;
|
||||
stream.Skip(1);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
error = $"Unknown MIDI event type {lastStatusByte:X2}";
|
||||
tracks = null;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (hasMidiEvent)
|
||||
parsedTracks.Add(track);
|
||||
}
|
||||
|
||||
tracks = parsedTracks.ToArray();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
103
Content.Client/Instruments/MidiParser/MidiStreamWrapper.cs
Normal file
103
Content.Client/Instruments/MidiParser/MidiStreamWrapper.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace Content.Client.Instruments.MidiParser;
|
||||
|
||||
public sealed class MidiStreamWrapper
|
||||
{
|
||||
private readonly MemoryStream _stream;
|
||||
private byte[] _buffer;
|
||||
|
||||
public long StreamPosition => _stream.Position;
|
||||
|
||||
public MidiStreamWrapper(byte[] data)
|
||||
{
|
||||
_stream = new MemoryStream(data, writable: false);
|
||||
_buffer = new byte[4];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Skips X number of bytes in the stream.
|
||||
/// </summary>
|
||||
/// <param name="count">The number of bytes to skip. If 0, no operations on the stream are performed.</param>
|
||||
public void Skip(int count)
|
||||
{
|
||||
if (count == 0)
|
||||
return;
|
||||
|
||||
_stream.Seek(count, SeekOrigin.Current);
|
||||
}
|
||||
|
||||
public byte ReadByte()
|
||||
{
|
||||
var b = _stream.ReadByte();
|
||||
if (b == -1)
|
||||
throw new Exception("Unexpected end of stream");
|
||||
|
||||
return (byte)b;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads N bytes using the buffer.
|
||||
/// </summary>
|
||||
public byte[] ReadBytes(int count)
|
||||
{
|
||||
if (_buffer.Length < count)
|
||||
{
|
||||
Array.Resize(ref _buffer, count);
|
||||
}
|
||||
|
||||
var read = _stream.Read(_buffer, 0, count);
|
||||
if (read != count)
|
||||
throw new Exception("Unexpected end of stream");
|
||||
|
||||
return _buffer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a 4 byte big-endian uint.
|
||||
/// </summary>
|
||||
public uint ReadUInt32()
|
||||
{
|
||||
var bytes = ReadBytes(4);
|
||||
return (uint)((bytes[0] << 24) |
|
||||
(bytes[1] << 16) |
|
||||
(bytes[2] << 8) |
|
||||
(bytes[3]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a 2 byte big-endian ushort.
|
||||
/// </summary>
|
||||
public ushort ReadUInt16()
|
||||
{
|
||||
var bytes = ReadBytes(2);
|
||||
return (ushort)((bytes[0] << 8) | bytes[1]);
|
||||
}
|
||||
|
||||
public string ReadString(int count)
|
||||
{
|
||||
var bytes = ReadBytes(count);
|
||||
return Encoding.UTF8.GetString(bytes, 0, count);
|
||||
}
|
||||
|
||||
public uint ReadVariableLengthQuantity()
|
||||
{
|
||||
uint value = 0;
|
||||
|
||||
// variable-length-quantities encode ints using 7 bits per byte
|
||||
// the highest bit (7) is used for a continuation flag. We read until the high bit is 0
|
||||
|
||||
while (true)
|
||||
{
|
||||
var b = ReadByte();
|
||||
value = (value << 7) | (uint)(b & 0x7f); // Shift current value and add 7 bits
|
||||
// value << 7, make room for the next 7 bits
|
||||
// b & 0x7F mask out the high bit to just get the 7 bit payload
|
||||
if ((b & 0x80) == 0)
|
||||
break; // This was the last bit.
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -7,5 +7,7 @@
|
||||
<Button Name="AllButton" Text="{Loc 'instruments-component-channels-all-button'}" HorizontalExpand="true" VerticalExpand="true" SizeFlagsStretchRatio="1"/>
|
||||
<Button Name="ClearButton" Text="{Loc 'instruments-component-channels-clear-button'}" HorizontalExpand="true" VerticalExpand="true" SizeFlagsStretchRatio="1"/>
|
||||
</BoxContainer>
|
||||
<CheckButton Name="DisplayTrackNames"
|
||||
Text="{Loc 'instruments-component-channels-track-names-toggle'}" />
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
||||
@@ -1,26 +1,56 @@
|
||||
using Content.Shared.Instruments;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Audio.Midi;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Instruments.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ChannelsMenu : DefaultWindow
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = null!;
|
||||
|
||||
private readonly InstrumentBoundUserInterface _owner;
|
||||
|
||||
public ChannelsMenu(InstrumentBoundUserInterface owner) : base()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_owner = owner;
|
||||
|
||||
ChannelList.OnItemSelected += OnItemSelected;
|
||||
ChannelList.OnItemDeselected += OnItemDeselected;
|
||||
AllButton.OnPressed += OnAllPressed;
|
||||
ClearButton.OnPressed += OnClearPressed;
|
||||
DisplayTrackNames.OnPressed += OnDisplayTrackNamesPressed;
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
|
||||
_owner.Instruments.OnChannelsUpdated += UpdateChannelList;
|
||||
}
|
||||
|
||||
private void OnDisplayTrackNamesPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
DisplayTrackNames.SetClickPressed(!DisplayTrackNames.Pressed);
|
||||
Populate();
|
||||
}
|
||||
|
||||
private void UpdateChannelList()
|
||||
{
|
||||
Populate(); // This is kind of in-efficent because we don't filter for which instrument updated its channels, but idc
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
_owner.Instruments.OnChannelsUpdated -= UpdateChannelList;
|
||||
}
|
||||
|
||||
private void OnItemSelected(ItemList.ItemListSelectedEventArgs args)
|
||||
@@ -51,15 +81,71 @@ public sealed partial class ChannelsMenu : DefaultWindow
|
||||
}
|
||||
}
|
||||
|
||||
public void Populate(InstrumentComponent? instrument)
|
||||
/// <summary>
|
||||
/// Walks up the tree of instrument masters to find the truest master of them all.
|
||||
/// </summary>
|
||||
private ActiveInstrumentComponent ResolveActiveInstrument(InstrumentComponent? comp)
|
||||
{
|
||||
comp ??= _entityManager.GetComponent<InstrumentComponent>(_owner.Owner);
|
||||
|
||||
var instrument = new Entity<InstrumentComponent>(_owner.Owner, comp);
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (instrument.Comp.Master == null)
|
||||
break;
|
||||
|
||||
instrument = new Entity<InstrumentComponent>((EntityUid)instrument.Comp.Master,
|
||||
_entityManager.GetComponent<InstrumentComponent>((EntityUid)instrument.Comp.Master));
|
||||
}
|
||||
|
||||
return _entityManager.GetComponent<ActiveInstrumentComponent>(instrument.Owner);
|
||||
}
|
||||
|
||||
public void Populate()
|
||||
{
|
||||
ChannelList.Clear();
|
||||
var instrument = _entityManager.GetComponent<InstrumentComponent>(_owner.Owner);
|
||||
var activeInstrument = ResolveActiveInstrument(instrument);
|
||||
|
||||
for (int i = 0; i < RobustMidiEvent.MaxChannels; i++)
|
||||
{
|
||||
var item = ChannelList.AddItem(_owner.Loc.GetString("instrument-component-channel-name",
|
||||
("number", i)), null, true, i);
|
||||
var label = _owner.Loc.GetString("instrument-component-channel-name",
|
||||
("number", i));
|
||||
if (activeInstrument != null
|
||||
&& activeInstrument.Tracks.TryGetValue(i, out var resolvedMidiChannel)
|
||||
&& resolvedMidiChannel != null)
|
||||
{
|
||||
if (DisplayTrackNames.Pressed)
|
||||
{
|
||||
label = resolvedMidiChannel switch
|
||||
{
|
||||
{ TrackName: not null, InstrumentName: not null } =>
|
||||
Loc.GetString("instruments-component-channels-multi",
|
||||
("channel", i),
|
||||
("name", resolvedMidiChannel.TrackName),
|
||||
("other", resolvedMidiChannel.InstrumentName)),
|
||||
{ TrackName: not null } =>
|
||||
Loc.GetString("instruments-component-channels-single",
|
||||
("channel", i),
|
||||
("name", resolvedMidiChannel.TrackName)),
|
||||
_ => label,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
label = resolvedMidiChannel switch
|
||||
{
|
||||
{ ProgramName: not null } =>
|
||||
Loc.GetString("instruments-component-channels-single",
|
||||
("channel", i),
|
||||
("name", resolvedMidiChannel.ProgramName)),
|
||||
_ => label,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var item = ChannelList.AddItem(label, null, true, i);
|
||||
|
||||
item.Selected = !instrument?.FilteredChannels[i] ?? false;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Instruments;
|
||||
using Content.Shared.Instruments.UI;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Client.Audio.Midi;
|
||||
@@ -101,9 +102,7 @@ namespace Content.Client.Instruments.UI
|
||||
public void OpenChannelsMenu()
|
||||
{
|
||||
_channelsMenu ??= new ChannelsMenu(this);
|
||||
EntMan.TryGetComponent(Owner, out InstrumentComponent? instrument);
|
||||
|
||||
_channelsMenu.Populate(instrument);
|
||||
_channelsMenu.Populate();
|
||||
_channelsMenu.OpenCenteredRight();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
using Range = Robust.Client.UserInterface.Controls.Range;
|
||||
|
||||
@@ -145,10 +146,6 @@ namespace Content.Client.Instruments.UI
|
||||
if (!PlayCheck())
|
||||
return;
|
||||
|
||||
await using var memStream = new MemoryStream((int) file.Length);
|
||||
|
||||
await file.CopyToAsync(memStream);
|
||||
|
||||
if (!_entManager.TryGetComponent<InstrumentComponent>(Entity, out var instrument))
|
||||
{
|
||||
return;
|
||||
@@ -156,7 +153,7 @@ namespace Content.Client.Instruments.UI
|
||||
|
||||
if (!_entManager.System<InstrumentSystem>()
|
||||
.OpenMidi(Entity,
|
||||
memStream.GetBuffer().AsSpan(0, (int) memStream.Length),
|
||||
file.CopyToArray(),
|
||||
instrument))
|
||||
{
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user