diff --git a/Content.Client/Instruments/InstrumentSystem.MidiParsing.cs b/Content.Client/Instruments/InstrumentSystem.MidiParsing.cs deleted file mode 100644 index 16aed930f6..0000000000 --- a/Content.Client/Instruments/InstrumentSystem.MidiParsing.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Linq; -using Content.Shared.Instruments; -using Robust.Shared.Audio.Midi; - -namespace Content.Client.Instruments; - -public sealed partial class InstrumentSystem -{ - /// - /// Tries to parse the input data as a midi and set the channel names respectively. - /// - /// - /// Thank you to http://www.somascape.org/midi/tech/mfile.html for providing an awesome resource for midi files. - /// - /// - /// This method has exception tolerance and does not throw, even if the midi file is invalid. - /// - 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(); - 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; - } -} diff --git a/Content.Client/Instruments/InstrumentSystem.cs b/Content.Client/Instruments/InstrumentSystem.cs index d861f4163b..abc3fa8210 100644 --- a/Content.Client/Instruments/InstrumentSystem.cs +++ b/Content.Client/Instruments/InstrumentSystem.cs @@ -1,4 +1,3 @@ -using System.IO; using System.Linq; using Content.Shared.CCVar; using Content.Shared.Instruments; @@ -13,7 +12,7 @@ using Robust.Shared.Timing; namespace Content.Client.Instruments; -public sealed partial class InstrumentSystem : SharedInstrumentSystem +public sealed class InstrumentSystem : SharedInstrumentSystem { [Dependency] private readonly IClientNetManager _netManager = default!; [Dependency] private readonly IMidiManager _midiManager = default!; @@ -24,8 +23,6 @@ public sealed partial 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(); @@ -41,26 +38,6 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnHandleState); - SubscribeLocalEvent(OnActiveInstrumentAfterHandleState); - } - - private bool _isUpdateQueued = false; - - private void OnActiveInstrumentAfterHandleState(Entity 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) @@ -275,13 +252,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem } - [Obsolete("Use overload that takes in byte[] instead.")] public bool OpenMidi(EntityUid uid, ReadOnlySpan 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; @@ -292,8 +263,6 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem return false; SetMaster(uid, null); - TrySetChannels(uid, data); - instrument.MidiEventBuffer.Clear(); instrument.Renderer.OnMidiEvent += instrument.MidiEventBuffer.Add; return true; diff --git a/Content.Client/Instruments/MidiParser/MidiInstrument.cs b/Content.Client/Instruments/MidiParser/MidiInstrument.cs deleted file mode 100644 index 93946496eb..0000000000 --- a/Content.Client/Instruments/MidiParser/MidiInstrument.cs +++ /dev/null @@ -1,147 +0,0 @@ -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 -{ - /// - /// Turns the given enum value into it's string representation to be used in localization. - /// - public static string GetStringRep(this MidiInstrument instrument) - { - return CaseConversion.PascalToKebab(instrument.ToString()); - } -} diff --git a/Content.Client/Instruments/MidiParser/MidiParser.cs b/Content.Client/Instruments/MidiParser/MidiParser.cs deleted file mode 100644 index 937384e439..0000000000 --- a/Content.Client/Instruments/MidiParser/MidiParser.cs +++ /dev/null @@ -1,184 +0,0 @@ -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(); - - 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().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; - } -} diff --git a/Content.Client/Instruments/MidiParser/MidiStreamWrapper.cs b/Content.Client/Instruments/MidiParser/MidiStreamWrapper.cs deleted file mode 100644 index 1886417a56..0000000000 --- a/Content.Client/Instruments/MidiParser/MidiStreamWrapper.cs +++ /dev/null @@ -1,103 +0,0 @@ -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]; - } - - /// - /// Skips X number of bytes in the stream. - /// - /// The number of bytes to skip. If 0, no operations on the stream are performed. - 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; - } - - /// - /// Reads N bytes using the buffer. - /// - 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; - } - - /// - /// Reads a 4 byte big-endian uint. - /// - public uint ReadUInt32() - { - var bytes = ReadBytes(4); - return (uint)((bytes[0] << 24) | - (bytes[1] << 16) | - (bytes[2] << 8) | - (bytes[3])); - } - - /// - /// Reads a 2 byte big-endian ushort. - /// - 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; - } -} diff --git a/Content.Client/Instruments/UI/ChannelsMenu.xaml b/Content.Client/Instruments/UI/ChannelsMenu.xaml index 20e4a3e923..1bf4647609 100644 --- a/Content.Client/Instruments/UI/ChannelsMenu.xaml +++ b/Content.Client/Instruments/UI/ChannelsMenu.xaml @@ -7,7 +7,5 @@