Not All Instruments Are Bike Horns (#2386)

* instrument updates from wizden

* chore: automatically update REUSE headers

* remove deprecated component in yaml

* chore: automatically update REUSE headers

---------

Co-authored-by: monolith8319 <195513600+monolith8319@users.noreply.github.com>
This commit is contained in:
bitcrushing
2025-10-09 02:32:35 +01:00
committed by GitHub
parent d6cad00cb6
commit 44d349e405
21 changed files with 1176 additions and 54 deletions
@@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: 2025 bitcrushing
//
// SPDX-License-Identifier: MPL-2.0
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;
}
}
+61 -11
View File
@@ -1,3 +1,20 @@
// SPDX-FileCopyrightText: 2020 Vince
// SPDX-FileCopyrightText: 2020 Víctor Aguilera Puerto
// SPDX-FileCopyrightText: 2020 zumorica
// SPDX-FileCopyrightText: 2021 20kdc
// SPDX-FileCopyrightText: 2021 Acruid
// SPDX-FileCopyrightText: 2021 DrSmugleaf
// SPDX-FileCopyrightText: 2021 metalgearsloth
// SPDX-FileCopyrightText: 2022 Leon Friedrich
// SPDX-FileCopyrightText: 2022 Vera Aguilera Puerto
// SPDX-FileCopyrightText: 2022 mirrorcult
// SPDX-FileCopyrightText: 2023 Visne
// SPDX-FileCopyrightText: 2024 Pieter-Jan Briers
// SPDX-FileCopyrightText: 2025 bitcrushing
//
// SPDX-License-Identifier: MPL-2.0
using System.IO;
using System.Linq;
using Content.Shared.CCVar;
using Content.Shared.Instruments;
@@ -12,7 +29,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 +40,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 +57,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)
@@ -167,11 +206,14 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
private void UpdateRendererMaster(InstrumentComponent instrument)
{
if (instrument.Renderer == null || instrument.Master == null)
if (instrument.Renderer == null)
return;
if (!TryComp(instrument.Master, out InstrumentComponent? masterInstrument) || masterInstrument.Renderer == null)
if (instrument.Master == null || !TryComp(instrument.Master, out InstrumentComponent? masterInstrument) || masterInstrument.Renderer == null)
{
instrument.Renderer.Master = null;
return;
}
instrument.Renderer.Master = masterInstrument.Renderer;
}
@@ -196,15 +238,16 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
return;
}
instrument.Renderer?.SystemReset();
instrument.Renderer?.ClearAllEvents();
if (instrument.Renderer is { } renderer)
{
renderer.Master = null;
renderer.SystemReset();
renderer.ClearAllEvents();
var renderer = instrument.Renderer;
// We dispose of the synth two seconds from now to allow the last notes to stop from playing.
// Don't use timers bound to the entity in case it is getting deleted.
if (renderer != null)
// We dispose of the synth two seconds from now to allow the last notes to stop from playing.
// Don't use timers bound to the entity in case it is getting deleted.
Timer.Spawn(2000, () => { renderer.Dispose(); });
}
instrument.Renderer = null;
instrument.MidiEventBuffer.Clear();
@@ -248,7 +291,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;
@@ -259,7 +308,8 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
return false;
SetMaster(uid, null);
instrument.MidiEventBuffer.Clear();
TrySetChannels(uid, data);
instrument.Renderer.OnMidiEvent += instrument.MidiEventBuffer.Add;
return true;
}
@@ -0,0 +1,151 @@
// SPDX-FileCopyrightText: 2025 bitcrushing
//
// SPDX-License-Identifier: MPL-2.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
{
/// <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());
}
}
@@ -0,0 +1,190 @@
// SPDX-FileCopyrightText: 2025 bitcrushing
//
// SPDX-License-Identifier: MPL-2.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<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
// This string can potentially contain control characters, including 0x00 which can cause problems if it ends up in database entries via admin logs
// we sanitize TrackName and InstrumentName after they have been send to the server
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;
}
}
@@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: 2025 bitcrushing
//
// SPDX-License-Identifier: MPL-2.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];
}
/// <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,63 @@
// SPDX-FileCopyrightText: 2023 Vera Aguilera Puerto
// SPDX-FileCopyrightText: 2024 Nemanja
// SPDX-FileCopyrightText: 2024 metalgearsloth
// SPDX-FileCopyrightText: 2025 bitcrushing
//
// SPDX-License-Identifier: MPL-2.0
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 +88,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,20 @@
// SPDX-FileCopyrightText: 2019 Víctor Aguilera Puerto
// SPDX-FileCopyrightText: 2021 Acruid
// SPDX-FileCopyrightText: 2021 Vera Aguilera Puerto
// SPDX-FileCopyrightText: 2022 mirrorcult
// SPDX-FileCopyrightText: 2023 DrSmugleaf
// SPDX-FileCopyrightText: 2023 TemporalOroboros
// SPDX-FileCopyrightText: 2024 Leon Friedrich
// SPDX-FileCopyrightText: 2024 Nemanja
// SPDX-FileCopyrightText: 2024 metalgearsloth
// SPDX-FileCopyrightText: 2025 Milon
// SPDX-FileCopyrightText: 2025 SpaceManiac
// SPDX-FileCopyrightText: 2025 bitcrushing
//
// SPDX-License-Identifier: MPL-2.0
using Content.Shared.ActionBlocker;
using Content.Shared.Instruments;
using Content.Shared.Instruments.UI;
using Content.Shared.Interaction;
using Robust.Client.Audio.Midi;
@@ -101,9 +117,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();
}
@@ -1,3 +1,26 @@
// SPDX-FileCopyrightText: 2019 Víctor Aguilera Puerto
// SPDX-FileCopyrightText: 2020 DTanxxx
// SPDX-FileCopyrightText: 2020 Radrark
// SPDX-FileCopyrightText: 2020 Radrark <null>
// SPDX-FileCopyrightText: 2020 ShadowCommander
// SPDX-FileCopyrightText: 2021 Acruid
// SPDX-FileCopyrightText: 2021 DrSmugleaf
// SPDX-FileCopyrightText: 2021 Vera Aguilera Puerto
// SPDX-FileCopyrightText: 2021 metalgearsloth
// SPDX-FileCopyrightText: 2022 Andreas Kämper
// SPDX-FileCopyrightText: 2022 Leon Friedrich
// SPDX-FileCopyrightText: 2022 Paul Ritter
// SPDX-FileCopyrightText: 2022 mirrorcult
// SPDX-FileCopyrightText: 2022 wrexbe
// SPDX-FileCopyrightText: 2023 Kara
// SPDX-FileCopyrightText: 2024 Hannah Giovanna Dawson
// SPDX-FileCopyrightText: 2024 Nemanja
// SPDX-FileCopyrightText: 2024 Pieter-Jan Briers
// SPDX-FileCopyrightText: 2024 slarticodefast
// SPDX-FileCopyrightText: 2025 bitcrushing
//
// SPDX-License-Identifier: MPL-2.0
using System.IO;
using System.Numerics;
using System.Threading.Tasks;
@@ -11,6 +34,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;
@@ -128,7 +152,7 @@ namespace Content.Client.Instruments.UI
// or focus the previously-opened window.
_isMidiFileDialogueWindowOpen = true;
await using var file = await _dialogs.OpenFile(filters);
await using var file = await _dialogs.OpenFile(filters, FileAccess.Read);
_isMidiFileDialogueWindowOpen = false;
@@ -145,10 +169,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 +176,7 @@ namespace Content.Client.Instruments.UI
if (!_entManager.System<InstrumentSystem>()
.OpenMidi(Entity,
memStream.GetBuffer().AsSpan(0, (int) memStream.Length),
file.CopyToArray(),
instrument))
{
return;
@@ -1,4 +1,29 @@
using Content.Server.UserInterface;
// SPDX-FileCopyrightText: 2020 Jackson Lewis
// SPDX-FileCopyrightText: 2020 Pieter-Jan Briers
// SPDX-FileCopyrightText: 2020 Tyler Young
// SPDX-FileCopyrightText: 2020 Vera Aguilera Puerto
// SPDX-FileCopyrightText: 2020 Víctor Aguilera Puerto
// SPDX-FileCopyrightText: 2020 chairbender
// SPDX-FileCopyrightText: 2020 derek
// SPDX-FileCopyrightText: 2020 zumorica
// SPDX-FileCopyrightText: 2021 20kdc
// SPDX-FileCopyrightText: 2021 Acruid
// SPDX-FileCopyrightText: 2021 Galactic Chimp
// SPDX-FileCopyrightText: 2021 Paul
// SPDX-FileCopyrightText: 2021 Paul Ritter
// SPDX-FileCopyrightText: 2021 Visne
// SPDX-FileCopyrightText: 2021 mirrorcult
// SPDX-FileCopyrightText: 2022 wrexbe
// SPDX-FileCopyrightText: 2023 Checkraze
// SPDX-FileCopyrightText: 2023 DrSmugleaf
// SPDX-FileCopyrightText: 2023 Kara
// SPDX-FileCopyrightText: 2023 Leon Friedrich
// SPDX-FileCopyrightText: 2024 Jake Huxell
// SPDX-FileCopyrightText: 2024 metalgearsloth
// SPDX-FileCopyrightText: 2025 bitcrushing
//
// SPDX-License-Identifier: MPL-2.0
using Content.Shared.Instruments;
using Robust.Shared.Player;
using ActivatableUIComponent = Content.Shared.UserInterface.ActivatableUIComponent;
@@ -21,8 +46,3 @@ public sealed partial class InstrumentComponent : SharedInstrumentComponent
_entMan.GetComponentOrNull<ActivatableUIComponent>(Owner)?.CurrentSingleUser
?? _entMan.GetComponentOrNull<ActorComponent>(Owner)?.PlayerSession.AttachedEntity;
}
[RegisterComponent]
public sealed partial class ActiveInstrumentComponent : Component
{
}
+57 -8
View File
@@ -18,15 +18,20 @@
// SPDX-FileCopyrightText: 2024 Cojoke
// SPDX-FileCopyrightText: 2024 Pieter-Jan Briers
// SPDX-FileCopyrightText: 2024 Plykiya
// SPDX-FileCopyrightText: 2025 J
// SPDX-FileCopyrightText: 2025 Redrover1760
// SPDX-FileCopyrightText: 2025 bitcrushing
//
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: MPL-2.0
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;
@@ -41,6 +46,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;
@@ -55,6 +61,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;
@@ -74,6 +81,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 =>
{
@@ -156,6 +164,47 @@ 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;
}
foreach (var t in msg.Tracks)
{
// Remove any control characters that may be part of the midi file so they don't end up in the admin logs.
t?.SanitizeFields();
// Truncate any track names too long.
t?.TruncateFields(_cfg.GetCVar(CCVars.MidiMaxChannelNameLength));
}
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}");
activeInstrument.Tracks = msg.Tracks;
Dirty(uid, activeInstrument);
}
private void OnMidiSetMaster(InstrumentSetMasterEvent msg, EntitySessionEventArgs args)
{
var uid = GetEntity(msg.Uid);
@@ -247,20 +296,20 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
public (NetEntity, string)[] GetBands(EntityUid uid)
{
var metadataQuery = EntityManager.GetEntityQuery<MetaDataComponent>();
var metadataQuery = GetEntityQuery<MetaDataComponent>();
if (Deleted(uid))
return Array.Empty<(NetEntity, string)>();
var list = new ValueList<(NetEntity, string)>();
var instrumentQuery = EntityManager.GetEntityQuery<InstrumentComponent>();
var instrumentQuery = GetEntityQuery<InstrumentComponent>();
if (!TryComp(uid, out InstrumentComponent? originInstrument)
|| originInstrument.InstrumentPlayer is not {} originPlayer)
return Array.Empty<(NetEntity, string)>();
// It's probably faster to get all possible active instruments than all entities in range
var activeEnumerator = EntityManager.EntityQueryEnumerator<ActiveInstrumentComponent>();
var activeEnumerator = EntityQueryEnumerator<ActiveInstrumentComponent>();
while (activeEnumerator.MoveNext(out var entity, out _))
{
if (entity == uid)
@@ -403,8 +452,8 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
_bandRequestQueue.Clear();
}
var activeQuery = EntityManager.GetEntityQuery<ActiveInstrumentComponent>();
var transformQuery = EntityManager.GetEntityQuery<TransformComponent>();
var activeQuery = GetEntityQuery<ActiveInstrumentComponent>();
var transformQuery = GetEntityQuery<TransformComponent>();
var query = AllEntityQuery<ActiveInstrumentComponent, InstrumentComponent>();
while (query.MoveNext(out var uid, out _, out var instrument))
@@ -437,7 +486,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
{
if (instrument.InstrumentPlayer is {Valid: true} mob)
{
_stuns.TryParalyze(mob, TimeSpan.FromSeconds(1), true);
_stuns.TryParalyze(mob, TimeSpan.FromSeconds(1), true); // Mono - Wizden does TryUpdateParalyzeDuration here
_popup.PopupEntity(Loc.GetString("instrument-component-finger-cramps-max-message"),
uid, mob, PopupType.LargeCaution);
@@ -1,3 +1,10 @@
// SPDX-FileCopyrightText: 2022 EmoGarbage404
// SPDX-FileCopyrightText: 2023 DrSmugleaf
// SPDX-FileCopyrightText: 2024 Whatstone
// SPDX-FileCopyrightText: 2025 bitcrushing
//
// SPDX-License-Identifier: MPL-2.0
namespace Content.Server.Instruments;
[RegisterComponent]
@@ -11,11 +18,4 @@ public sealed partial class SwappableInstrumentComponent : Component
/// </summary>
[DataField("instrumentList", required: true)]
public Dictionary<string, (byte, byte)> InstrumentList = new();
// Frontier: harpy instruments
/// <summary>
/// When true, only the instrument entity itself can swap its sound.
/// </summary>
[DataField]
public bool OnlySetBySelf;
}
@@ -1,3 +1,13 @@
// SPDX-FileCopyrightText: 2022 EmoGarbage404
// SPDX-FileCopyrightText: 2022 Leon Friedrich
// SPDX-FileCopyrightText: 2022 Vera Aguilera Puerto
// SPDX-FileCopyrightText: 2023 metalgearsloth
// SPDX-FileCopyrightText: 2024 Tayrtahn
// SPDX-FileCopyrightText: 2024 Whatstone
// SPDX-FileCopyrightText: 2025 bitcrushing
//
// SPDX-License-Identifier: MPL-2.0
using Content.Shared.Instruments;
using Content.Shared.Popups;
using Content.Shared.Verbs;
@@ -25,9 +35,6 @@ public sealed class SwappableInstrumentSystem : EntitySystem
if (!TryComp<InstrumentComponent>(uid, out var instrument))
return;
if (component.OnlySetBySelf && uid != args.User) // Frontier: restrict instrument changes
return; // Frontier: restrict instrument changes
var priority = 0;
foreach (var entry in component.InstrumentList)
{
+25
View File
@@ -1,7 +1,32 @@
// SPDX-FileCopyrightText: 2021 20kdc
// SPDX-FileCopyrightText: 2021 Acruid
// SPDX-FileCopyrightText: 2021 Paul
// SPDX-FileCopyrightText: 2021 Vera Aguilera Puerto
// SPDX-FileCopyrightText: 2021 hubismal
// SPDX-FileCopyrightText: 2022 Kara
// SPDX-FileCopyrightText: 2022 mirrorcult
// SPDX-FileCopyrightText: 2022 wrexbe
// SPDX-FileCopyrightText: 2023 Checkraze
// SPDX-FileCopyrightText: 2023 DrSmugleaf
// SPDX-FileCopyrightText: 2023 Nemanja
// SPDX-FileCopyrightText: 2023 ShadowCommander
// SPDX-FileCopyrightText: 2023 Visne
// SPDX-FileCopyrightText: 2023 deltanedas
// SPDX-FileCopyrightText: 2023 deltanedas <@deltanedas:kde.org>
// SPDX-FileCopyrightText: 2023 faint
// SPDX-FileCopyrightText: 2024 Leon Friedrich
// SPDX-FileCopyrightText: 2024 Whatstone
// SPDX-FileCopyrightText: 2024 metalgearsloth
// SPDX-FileCopyrightText: 2025 bitcrushing
// SPDX-FileCopyrightText: 2025 slarticodefast
//
// SPDX-License-Identifier: MPL-2.0
using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Instruments;
using Content.Server.Kitchen.Components;
using Content.Shared.Instruments;
using Content.Shared.Interaction.Events;
using Content.Shared.Mind.Components;
using Content.Shared.PAI;
@@ -1,6 +1,13 @@
using System.Linq;
// SPDX-FileCopyrightText: 2022 Nemanja
// SPDX-FileCopyrightText: 2023 DrSmugleaf
// SPDX-FileCopyrightText: 2025 bitcrushing
//
// SPDX-License-Identifier: MPL-2.0
using System.Linq;
using Content.Server.Instruments;
using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
using Content.Shared.Instruments;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems;
+44 -1
View File
@@ -1,3 +1,41 @@
// SPDX-FileCopyrightText: 2021 Javier Guardia Fernández
// SPDX-FileCopyrightText: 2021 Paul
// SPDX-FileCopyrightText: 2021 Pieter-Jan Briers
// SPDX-FileCopyrightText: 2021 Vera Aguilera Puerto
// SPDX-FileCopyrightText: 2021 mirrorcult
// SPDX-FileCopyrightText: 2022 Jacob Tong
// SPDX-FileCopyrightText: 2022 Júlio César Ueti
// SPDX-FileCopyrightText: 2022 Kara
// SPDX-FileCopyrightText: 2022 Moony
// SPDX-FileCopyrightText: 2022 Rane
// SPDX-FileCopyrightText: 2022 ShadowCommander
// SPDX-FileCopyrightText: 2022 metalgearsloth
// SPDX-FileCopyrightText: 2023 Chief-Engineer
// SPDX-FileCopyrightText: 2023 Debug
// SPDX-FileCopyrightText: 2023 DrSmugleaf
// SPDX-FileCopyrightText: 2023 Dvir
// SPDX-FileCopyrightText: 2023 HerCoyote23
// SPDX-FileCopyrightText: 2023 Nemanja
// SPDX-FileCopyrightText: 2024 Alice "Arimah" Heurlin
// SPDX-FileCopyrightText: 2024 GreaseMonk
// SPDX-FileCopyrightText: 2024 Leon Friedrich
// SPDX-FileCopyrightText: 2024 Trevor Day
// SPDX-FileCopyrightText: 2024 TsjipTsjip
// SPDX-FileCopyrightText: 2024 Winkarst
// SPDX-FileCopyrightText: 2024 keronshb
// SPDX-FileCopyrightText: 2024 nikthechampiongr
// SPDX-FileCopyrightText: 2025 Ark
// SPDX-FileCopyrightText: 2025 Checkraze
// SPDX-FileCopyrightText: 2025 Dylan Hunter Whittingham
// SPDX-FileCopyrightText: 2025 Ilya246
// SPDX-FileCopyrightText: 2025 Palladinium
// SPDX-FileCopyrightText: 2025 Redrover1760
// SPDX-FileCopyrightText: 2025 Simon
// SPDX-FileCopyrightText: 2025 Whatstone
// SPDX-FileCopyrightText: 2025 bitcrushing
//
// SPDX-License-Identifier: MPL-2.0
namespace Content.Shared.Database;
// DO NOT CHANGE THE NUMERIC VALUES OF THESE
@@ -480,5 +518,10 @@ public enum LogType
/// <summary>
/// Damaging grid collision has occurred.
/// </summary>
ShuttleImpact = 102
ShuttleImpact = 102,
/// <summary>
/// Events relating to midi playback.
/// </summary>
Instrument = 103,
}
+12 -1
View File
@@ -1,4 +1,9 @@
using Robust.Shared.Configuration;
// SPDX-FileCopyrightText: 2024 Simon
// SPDX-FileCopyrightText: 2025 bitcrushing
//
// SPDX-License-Identifier: MPL-2.0
using Robust.Shared.Configuration;
namespace Content.Shared.CCVar;
@@ -15,4 +20,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);
}
@@ -1,4 +1,20 @@
// SPDX-FileCopyrightText: 2020 Tyler Young
// SPDX-FileCopyrightText: 2020 Vera Aguilera Puerto
// SPDX-FileCopyrightText: 2020 Víctor Aguilera Puerto
// SPDX-FileCopyrightText: 2020 zumorica
// SPDX-FileCopyrightText: 2021 Acruid
// SPDX-FileCopyrightText: 2021 DrSmugleaf
// SPDX-FileCopyrightText: 2021 Visne
// SPDX-FileCopyrightText: 2022 Pieter-Jan Briers
// SPDX-FileCopyrightText: 2022 mirrorcult
// SPDX-FileCopyrightText: 2022 wrexbe
// SPDX-FileCopyrightText: 2023 metalgearsloth
// SPDX-FileCopyrightText: 2025 bitcrushing
//
// SPDX-License-Identifier: MPL-2.0
using System.Collections;
using System.Text;
using Robust.Shared.Audio.Midi;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
@@ -34,6 +50,18 @@ public abstract partial class SharedInstrumentComponent : Component
public BitArray FilteredChannels { get; set; } = new(RobustMidiEvent.MaxChannels, true);
}
/// <summary>
/// Component that indicates that musical instrument was activated (ui opened).
/// </summary>
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState(true)]
public sealed partial class ActiveInstrumentComponent : Component
{
[DataField]
[AutoNetworkedField]
public MidiTrack?[] Tracks = [];
}
[Serializable, NetSerializable]
public sealed class InstrumentComponentState : ComponentState
{
@@ -138,3 +166,97 @@ 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);
}
public void SanitizeFields()
{
if (InstrumentName != null)
InstrumentName = Sanitize(InstrumentName);
if (TrackName != null)
TrackName = Sanitize(TrackName);
if (ProgramName != null)
ProgramName = Sanitize(ProgramName);
}
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;
}
private static string Sanitize(string input)
{
var sanitized = new StringBuilder(input.Length);
foreach (char c in input)
{
if (!char.IsControl(c) && c <= 127) // no control characters, only ASCII
sanitized.Append(c);
}
return sanitized.ToString();
}
}
@@ -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
@@ -1,4 +1,16 @@
- type: entity
# SPDX-FileCopyrightText: 2023 DanSAussieITS
# SPDX-FileCopyrightText: 2023 Nemanja
# SPDX-FileCopyrightText: 2023 metalgearsloth
# SPDX-FileCopyrightText: 2024 Dvir
# SPDX-FileCopyrightText: 2024 Moomoobeef
# SPDX-FileCopyrightText: 2024 Nairod
# SPDX-FileCopyrightText: 2024 SlamBamActionman
# SPDX-FileCopyrightText: 2024 lzk
# SPDX-FileCopyrightText: 2025 bitcrushing
#
# SPDX-License-Identifier: MPL-2.0
- type: entity
parent: BaseHandheldInstrument
id: GlockenspielInstrument
name: glockenspiel
@@ -138,7 +150,6 @@
- type: Instrument
allowPercussion: true
allowProgramChange: true
respectMidiLimits: false
- type: Sprite
sprite: Objects/Fun/Instruments/h_synthesizer.rsi
state: supersynth
@@ -146,3 +157,11 @@
heldPrefix: super
size: Normal
sprite: Objects/Fun/Instruments/h_synthesizer.rsi
- type: entity
parent: SuperSynthesizerInstrument
id: SuperSynthesizerNoLimitInstrument
suffix: NoLimits
components:
- type: Instrument
respectMidiLimits: false
@@ -5,7 +5,9 @@
# SPDX-FileCopyrightText: 2025 Ark
# SPDX-FileCopyrightText: 2025 Dvir
# SPDX-FileCopyrightText: 2025 Redrover1760
# SPDX-FileCopyrightText: 2025 ScyronX
# SPDX-FileCopyrightText: 2025 Whatstone
# SPDX-FileCopyrightText: 2025 bitcrushing
# SPDX-FileCopyrightText: 2025 core-mene
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@@ -34,7 +36,6 @@
"Piano": {1: 0}
"Church Organ": {19: 0}
"Harp": {46: 0}
onlySetBySelf: true
- type: UserInterface
interfaces:
enum.InstrumentUiKey.Key: