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;
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Interaction;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Stunnable;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Instruments;
|
||||
using Content.Shared.Instruments.UI;
|
||||
@@ -17,6 +21,7 @@ using Robust.Shared.Console;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Instruments;
|
||||
|
||||
@@ -31,6 +36,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
|
||||
[Dependency] private readonly IAdminLogManager _admingLogSystem = default!;
|
||||
|
||||
private const float MaxInstrumentBandRange = 10f;
|
||||
|
||||
@@ -50,6 +56,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
|
||||
SubscribeNetworkEvent<InstrumentStopMidiEvent>(OnMidiStop);
|
||||
SubscribeNetworkEvent<InstrumentSetMasterEvent>(OnMidiSetMaster);
|
||||
SubscribeNetworkEvent<InstrumentSetFilteredChannelEvent>(OnMidiSetFilteredChannel);
|
||||
SubscribeNetworkEvent<InstrumentSetChannelsEvent>(OnMidiSetChannels);
|
||||
|
||||
Subs.BuiEvents<InstrumentComponent>(InstrumentUiKey.Key, subs =>
|
||||
{
|
||||
@@ -132,6 +139,44 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
|
||||
Clean(uid, instrument);
|
||||
}
|
||||
|
||||
|
||||
private void OnMidiSetChannels(InstrumentSetChannelsEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
var uid = GetEntity(msg.Uid);
|
||||
|
||||
if (!TryComp(uid, out InstrumentComponent? instrument) || !TryComp(uid, out ActiveInstrumentComponent? activeInstrument))
|
||||
return;
|
||||
|
||||
if (args.SenderSession.AttachedEntity != instrument.InstrumentPlayer)
|
||||
return;
|
||||
|
||||
if (msg.Tracks.Length > RobustMidiEvent.MaxChannels)
|
||||
{
|
||||
Log.Warning($"{args.SenderSession.UserId.ToString()} - Tried to send tracks over the limit! Received: {msg.Tracks.Length}; Limit: {RobustMidiEvent.MaxChannels}");
|
||||
return;
|
||||
}
|
||||
|
||||
var tracksString = string.Join("\n",
|
||||
msg.Tracks
|
||||
.Where(t => t != null)
|
||||
.Select(t => t!.ToString()));
|
||||
|
||||
_admingLogSystem.Add(
|
||||
LogType.Instrument,
|
||||
LogImpact.Low,
|
||||
$"{ToPrettyString(args.SenderSession.AttachedEntity)} set the midi channels for {ToPrettyString(uid)} to {tracksString}");
|
||||
|
||||
// Truncate any track names too long.
|
||||
foreach (var t in msg.Tracks)
|
||||
{
|
||||
t?.TruncateFields(_cfg.GetCVar(CCVars.MidiMaxChannelNameLength));
|
||||
}
|
||||
|
||||
activeInstrument.Tracks = msg.Tracks;
|
||||
|
||||
Dirty(uid, activeInstrument);
|
||||
}
|
||||
|
||||
private void OnMidiSetMaster(InstrumentSetMasterEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
var uid = GetEntity(msg.Uid);
|
||||
|
||||
@@ -472,5 +472,10 @@ public enum LogType
|
||||
/// <summary>
|
||||
/// Damaging grid collision has occurred.
|
||||
/// </summary>
|
||||
ShuttleImpact = 102
|
||||
ShuttleImpact = 102,
|
||||
|
||||
/// <summary>
|
||||
/// Events relating to midi playback.
|
||||
/// </summary>
|
||||
Instrument = 103,
|
||||
}
|
||||
|
||||
@@ -15,4 +15,10 @@ public sealed partial class CCVars
|
||||
|
||||
public static readonly CVarDef<int> MaxMidiLaggedBatches =
|
||||
CVarDef.Create("midi.max_lagged_batches", 8, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Defines the max amount of characters to allow in the "Midi channel selector".
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> MidiMaxChannelNameLength =
|
||||
CVarDef.Create("midi.max_channel_name_length", 64, CVar.SERVERONLY);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,13 @@ public abstract partial class SharedInstrumentComponent : Component
|
||||
/// Component that indicates that musical instrument was activated (ui opened).
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class ActiveInstrumentComponent : Component;
|
||||
[AutoGenerateComponentState(true)]
|
||||
public sealed partial class ActiveInstrumentComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
[AutoNetworkedField]
|
||||
public MidiTrack?[] Tracks = [];
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class InstrumentComponentState : ComponentState
|
||||
@@ -144,3 +150,72 @@ public enum InstrumentUiKey
|
||||
{
|
||||
Key,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the MIDI channels on an instrument.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class InstrumentSetChannelsEvent : EntityEventArgs
|
||||
{
|
||||
public NetEntity Uid { get; }
|
||||
public MidiTrack?[] Tracks { get; set; }
|
||||
|
||||
public InstrumentSetChannelsEvent(NetEntity uid, MidiTrack?[] tracks)
|
||||
{
|
||||
Uid = uid;
|
||||
Tracks = tracks;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single midi track with the track name, instrument name and bank instrument name extracted.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MidiTrack
|
||||
{
|
||||
/// <summary>
|
||||
/// The first specified Track Name
|
||||
/// </summary>
|
||||
public string? TrackName;
|
||||
/// <summary>
|
||||
/// The first specified instrument name
|
||||
/// </summary>
|
||||
public string? InstrumentName;
|
||||
|
||||
/// <summary>
|
||||
/// The first program change resolved to the name.
|
||||
/// </summary>
|
||||
public string? ProgramName;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Track Name: {TrackName}; Instrument Name: {InstrumentName}; Program Name: {ProgramName}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Truncates the fields based on the limit inputted into this method.
|
||||
/// </summary>
|
||||
public void TruncateFields(int limit)
|
||||
{
|
||||
if (InstrumentName != null)
|
||||
InstrumentName = Truncate(InstrumentName, limit);
|
||||
|
||||
if (TrackName != null)
|
||||
TrackName = Truncate(TrackName, limit);
|
||||
|
||||
if (ProgramName != null)
|
||||
ProgramName = Truncate(ProgramName, limit);
|
||||
}
|
||||
|
||||
private const string Postfix = "…";
|
||||
// TODO: Make a general method to use in RT? idk if we have that.
|
||||
private string Truncate(string input, int limit)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input) || limit <= 0 || input.Length <= limit)
|
||||
return input;
|
||||
|
||||
var truncatedLength = limit - Postfix.Length;
|
||||
|
||||
return input.Substring(0, truncatedLength) + Postfix;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,139 @@ instruments-component-channels-menu = MIDI Channel Selection
|
||||
instrument-component-channel-name = MIDI Channel {$number}
|
||||
instruments-component-channels-all-button = All
|
||||
instruments-component-channels-clear-button = Clear
|
||||
instruments-component-channels-track-names-toggle = Show Track Names
|
||||
instruments-component-channels-single = {$channel} {$name}
|
||||
instruments-component-channels-multi = {$channel} {$name} ({$other})
|
||||
|
||||
|
||||
# SwappableInstrumentComponent
|
||||
swappable-instrument-component-style-set = Style set to "{$style}"
|
||||
|
||||
instruments-component-menu-midi-channel-acoustic-grand-piano = Acoustic Grand Piano
|
||||
instruments-component-menu-midi-channel-bright-acoustic-piano = Bright Acoustic Piano
|
||||
instruments-component-menu-midi-channel-electric-grand-piano = Electric Grand Piano
|
||||
instruments-component-menu-midi-channel-honky-tonk-piano = Honky-tonk Piano
|
||||
instruments-component-menu-midi-channel-rhodes-piano = Rhodes Piano
|
||||
instruments-component-menu-midi-channel-chorused-piano = Chorused Piano
|
||||
instruments-component-menu-midi-channel-harpsichord = Harpsichord
|
||||
instruments-component-menu-midi-channel-clavinet = Clavinet
|
||||
instruments-component-menu-midi-channel-celesta = Celesta
|
||||
instruments-component-menu-midi-channel-glockenspiel = Glockenspiel
|
||||
instruments-component-menu-midi-channel-music-box = Music Box
|
||||
instruments-component-menu-midi-channel-vibraphone = Vibraphone
|
||||
instruments-component-menu-midi-channel-marimba = Marimba
|
||||
instruments-component-menu-midi-channel-xylophone = Xylophone
|
||||
instruments-component-menu-midi-channel-tubular-bells = Tubular Bells
|
||||
instruments-component-menu-midi-channel-dulcimer = Dulcimer
|
||||
instruments-component-menu-midi-channel-hammond-organ = Hammond Organ
|
||||
instruments-component-menu-midi-channel-percussive-organ = Percussive Organ
|
||||
instruments-component-menu-midi-channel-rock-organ = Rock Organ
|
||||
instruments-component-menu-midi-channel-church-organ = Church Organ
|
||||
instruments-component-menu-midi-channel-reed-organ = Reed Organ
|
||||
instruments-component-menu-midi-channel-accordion = Accordion
|
||||
instruments-component-menu-midi-channel-harmonica = Harmonica
|
||||
instruments-component-menu-midi-channel-tango-accordion = Tango Accordion
|
||||
instruments-component-menu-midi-channel-acoustic-nylon-guitar = Acoustic Nylon Guitar
|
||||
instruments-component-menu-midi-channel-acoustic-steel-guitar = Acoustic Steel Guitar
|
||||
instruments-component-menu-midi-channel-electric-jazz-guitar = Electric Jazz Guitar
|
||||
instruments-component-menu-midi-channel-electric-clean-guitar = Electric Clean Guitar
|
||||
instruments-component-menu-midi-channel-electric-muted-guitar = Electric Muted Guitar
|
||||
instruments-component-menu-midi-channel-overdriven-guitar = Overdriven Guitar
|
||||
instruments-component-menu-midi-channel-distortion-guitar = Distortion Guitar
|
||||
instruments-component-menu-midi-channel-guitar-harmonics = Guitar Harmonics
|
||||
instruments-component-menu-midi-channel-acoustic-bass = Acoustic Bass
|
||||
instruments-component-menu-midi-channel-fingered-electric-bass = Fingered Electric Bass
|
||||
instruments-component-menu-midi-channel-plucked-electric-bass = Plucked Electric Bass
|
||||
instruments-component-menu-midi-channel-fretless-bass = Fretless Bass
|
||||
instruments-component-menu-midi-channel-slap-bass1 = Slap Bass 1
|
||||
instruments-component-menu-midi-channel-slap-bass2 = Slap Bass 2
|
||||
instruments-component-menu-midi-channel-synth-bass1 = Synth Bass 1
|
||||
instruments-component-menu-midi-channel-synth-bass2 = Synth Bass 2
|
||||
instruments-component-menu-midi-channel-violin = Violin
|
||||
instruments-component-menu-midi-channel-viola = Viola
|
||||
instruments-component-menu-midi-channel-cello = Cello
|
||||
instruments-component-menu-midi-channel-contrabass = Contrabass
|
||||
instruments-component-menu-midi-channel-tremolo-strings = Tremolo Strings
|
||||
instruments-component-menu-midi-channel-pizzicato-strings = Pizzicato Strings
|
||||
instruments-component-menu-midi-channel-orchestral-harp = Orchestral Harp
|
||||
instruments-component-menu-midi-channel-timpani = Timpani
|
||||
instruments-component-menu-midi-channel-string-ensemble1 = String Ensemble 1
|
||||
instruments-component-menu-midi-channel-string-ensemble2 = String Ensemble 2
|
||||
instruments-component-menu-midi-channel-synth-strings1 = Synth Strings 1
|
||||
instruments-component-menu-midi-channel-synth-strings2 = Synth Strings 2
|
||||
instruments-component-menu-midi-channel-choir-aah = Choir "Aah"
|
||||
instruments-component-menu-midi-channel-voice-ooh = Voice "Ooh"
|
||||
instruments-component-menu-midi-channel-synth-choir = Synth Choir
|
||||
instruments-component-menu-midi-channel-orchestra-hit = Orchestra Hit
|
||||
instruments-component-menu-midi-channel-trumpet = Trumpet
|
||||
instruments-component-menu-midi-channel-trombone = Trombone
|
||||
instruments-component-menu-midi-channel-tuba = Tuba
|
||||
instruments-component-menu-midi-channel-muted-trumpet = Muted Trumpet
|
||||
instruments-component-menu-midi-channel-french-horn = French Horn
|
||||
instruments-component-menu-midi-channel-brass-section = Brass Section
|
||||
instruments-component-menu-midi-channel-synth-brass1 = Synth Brass 1
|
||||
instruments-component-menu-midi-channel-synth-brass2 = Synth Brass 2
|
||||
instruments-component-menu-midi-channel-soprano-sax = Soprano Sax
|
||||
instruments-component-menu-midi-channel-alto-sax = Alto Sax
|
||||
instruments-component-menu-midi-channel-tenor-sax = Tenor Sax
|
||||
instruments-component-menu-midi-channel-baritone-sax = Baritone Sax
|
||||
instruments-component-menu-midi-channel-oboe = Oboe
|
||||
instruments-component-menu-midi-channel-english-horn = English Horn
|
||||
instruments-component-menu-midi-channel-bassoon = Bassoon
|
||||
instruments-component-menu-midi-channel-clarinet = Clarinet
|
||||
instruments-component-menu-midi-channel-piccolo = Piccolo
|
||||
instruments-component-menu-midi-channel-flute = Flute
|
||||
instruments-component-menu-midi-channel-recorder = Recorder
|
||||
instruments-component-menu-midi-channel-pan-flute = Pan Flute
|
||||
instruments-component-menu-midi-channel-bottle-blow = Bottle Blow
|
||||
instruments-component-menu-midi-channel-shakuhachi = Shakuhachi
|
||||
instruments-component-menu-midi-channel-whistle = Whistle
|
||||
instruments-component-menu-midi-channel-ocarina = Ocarina
|
||||
instruments-component-menu-midi-channel-square-wave-lead = Square Wave Lead
|
||||
instruments-component-menu-midi-channel-sawtooth-wave-lead = Sawtooth Wave Lead
|
||||
instruments-component-menu-midi-channel-calliope-lead = Calliope Lead
|
||||
instruments-component-menu-midi-channel-chiff-lead = Chiff Lead
|
||||
instruments-component-menu-midi-channel-charang-lead = Charang Lead
|
||||
instruments-component-menu-midi-channel-voice-lead = Voice Lead
|
||||
instruments-component-menu-midi-channel-fiths-lead = Fiths Lead
|
||||
instruments-component-menu-midi-channel-bass-lead = Bass Lead
|
||||
instruments-component-menu-midi-channel-new-age-pad = New Age Pad
|
||||
instruments-component-menu-midi-channel-warm-pad = Warm Pad
|
||||
instruments-component-menu-midi-channel-polysynth-pad = Polysynth Pad
|
||||
instruments-component-menu-midi-channel-choir-pad = Choir Pad
|
||||
instruments-component-menu-midi-channel-bowed-pad = Bowed Pad
|
||||
instruments-component-menu-midi-channel-metallic-pad = Metallic Pad
|
||||
instruments-component-menu-midi-channel-halo-pad = Halo Pad
|
||||
instruments-component-menu-midi-channel-sweep-pad = Sweep Pad
|
||||
instruments-component-menu-midi-channel-rain-effect = Rain Effect
|
||||
instruments-component-menu-midi-channel-soundtrack-effect = Soundtrack Effect
|
||||
instruments-component-menu-midi-channel-crystal-effect = Crystal Effect
|
||||
instruments-component-menu-midi-channel-atmosphere-effect = Atmosphere Effect
|
||||
instruments-component-menu-midi-channel-brightness-effect = Brightness Effect
|
||||
instruments-component-menu-midi-channel-goblins-effect = Goblins Effect
|
||||
instruments-component-menu-midi-channel-echoes-effect = Echoes Effect
|
||||
instruments-component-menu-midi-channel-sci-fi-effect = Sci-Fi Effect
|
||||
instruments-component-menu-midi-channel-sitar = Sitar
|
||||
instruments-component-menu-midi-channel-banjo = Banjo
|
||||
instruments-component-menu-midi-channel-shamisen = Shamisen
|
||||
instruments-component-menu-midi-channel-koto = Koto
|
||||
instruments-component-menu-midi-channel-kalimba = Kalimba
|
||||
instruments-component-menu-midi-channel-bagpipe = Bagpipe
|
||||
instruments-component-menu-midi-channel-fiddle = Fiddle
|
||||
instruments-component-menu-midi-channel-shanai = Shanai
|
||||
instruments-component-menu-midi-channel-tinkle-bell = Tinkle Bell
|
||||
instruments-component-menu-midi-channel-agogo = Agogo
|
||||
instruments-component-menu-midi-channel-steel-drums = Steel Drums
|
||||
instruments-component-menu-midi-channel-woodblock = Woodblock
|
||||
instruments-component-menu-midi-channel-taiko-drum = Taiko Drum
|
||||
instruments-component-menu-midi-channel-melodic-tom = Melodic Tom
|
||||
instruments-component-menu-midi-channel-synth-drum = Synth Drum
|
||||
instruments-component-menu-midi-channel-reverse-cymbal = Reverse Cymbal
|
||||
instruments-component-menu-midi-channel-guitar-fret-noise = Guitar Fret Noise
|
||||
instruments-component-menu-midi-channel-breath-noise = Breath Noise
|
||||
instruments-component-menu-midi-channel-seashore = Seashore
|
||||
instruments-component-menu-midi-channel-bird-tweet = Bird Tweet
|
||||
instruments-component-menu-midi-channel-telephone-ring = Telephone Ring
|
||||
instruments-component-menu-midi-channel-helicopter = Helicopter
|
||||
instruments-component-menu-midi-channel-applause = Applause
|
||||
instruments-component-menu-midi-channel-gunshot = Gunshot
|
||||
|
||||
Reference in New Issue
Block a user