Instrument UI MinVolume slider (#41272)

* Added localization string for MIDI input volume.

* Added MinVolume property to SharedInstrumentComponent & InstrumentComponentState.

* Implemented MinVolume property into InstrumentSystem's.

* Added UI slider for MinVolume to InstrumentMenu.

* Added network event for updating MinVolume.

* Removed unnecessary empty line.

* Readability fix as suggested by Pok27

Co-authored-by: Pok <113675512+Pok27@users.noreply.github.com>

* Added MinVolumeControl

* Added MinVolumeControl to instrument menu configuration (and cleaned up Content.Client.InstrumentSystem.SetMinVolume)

* Added label to MinVolumeSlider indicating the current value

* Minor cleanup

---------

Co-authored-by: Pok <113675512+Pok27@users.noreply.github.com>
This commit is contained in:
Ataman 2026-07-28 06:29:08 +02:00 • committed by GitHub
parent d9943c9753
commit 82b7cfa947
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 139 additions and 1 deletions

View file

@ -15,6 +15,9 @@ namespace Content.Client.Instruments;
public sealed partial class InstrumentSystem : SharedInstrumentSystem
{
private const int MidiMinVolume = 0;
private const int MidiMaxVolume = 127;
[Dependency] private IClientNetManager _netManager = default!;
[Dependency] private IMidiManager _midiManager = default!;
[Dependency] private IGameTiming _gameTiming = default!;
@ -38,6 +41,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
SubscribeNetworkEvent<InstrumentMidiEventEvent>(OnMidiEventRx);
SubscribeNetworkEvent<InstrumentStartMidiEvent>(OnMidiStart);
SubscribeNetworkEvent<InstrumentStopMidiEvent>(OnMidiStop);
SubscribeNetworkEvent<InstrumentSetMidiMinVolumeEvent>(OnSetMidiMinVolume);
SubscribeLocalEvent<InstrumentComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<InstrumentComponent, ComponentHandleState>(OnHandleState);
@ -75,6 +79,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
component.AllowProgramChange = state.AllowProgramChange;
component.RespectMidiLimits = state.RespectMidiLimits;
component.Master = EnsureEntity<InstrumentComponent>(state.Master, uid);
component.MinVolume = state.MinVolume;
component.FilteredChannels = state.FilteredChannels;
if (component.Playing)
@ -96,6 +101,18 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
RaiseNetworkEvent(new InstrumentSetMasterEvent(GetNetEntity(uid), GetNetEntity(masterUid)));
}
public void SetMinVolume(EntityUid uid, int volume)
{
if (!TryComp(uid, out InstrumentComponent? instrument))
return;
var byteMinVolume = (byte)Math.Min(Math.Max(MidiMinVolume, volume), MidiMaxVolume);
instrument.MinVolume = byteMinVolume;
RaiseNetworkEvent(new InstrumentSetMidiMinVolumeEvent(GetNetEntity(uid), byteMinVolume));
UpdateRenderer(uid);
}
public void SetFilteredChannel(EntityUid uid, int channel, bool value)
{
if (!TryComp(uid, out InstrumentComponent? instrument))
@ -186,6 +203,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
UpdateRendererMaster(instrument);
instrument.Renderer.LoopMidi = instrument.LoopMidi;
instrument.Renderer.MinVolume = instrument.MinVolume;
}
private void UpdateRendererMaster(InstrumentComponent instrument)
@ -418,6 +436,15 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
EndRenderer(GetEntity(ev.Uid), true);
}
private void OnSetMidiMinVolume(InstrumentSetMidiMinVolumeEvent ev)
{
if (!TryComp(GetEntity(ev.Uid), out InstrumentComponent? instrument))
return;
instrument.MinVolume = ev.MinVolume;
UpdateRenderer(GetEntity(ev.Uid), instrument);
}
public override void Update(float frameTime)
{
base.Update(frameTime);

View file

@ -30,6 +30,7 @@ public sealed partial class InstrumentBoundUserInterface : BoundUserInterface
private readonly ChannelsControl _channelsControl = new();
private readonly MidiCollectionUtilsControl _midiCollectionUtilsControl = new();
private readonly MinVolumeControl _minVolumeControl = new();
private InstrumentMenu? _instrumentMenu;
@ -63,6 +64,9 @@ public sealed partial class InstrumentBoundUserInterface : BoundUserInterface
_channelsControl.ChannelsUpdateRequest += OnChannelsUpdateRequest;
_channelsControl.SwitchFilteredChannel += OnSwitchFilteredChannel;
_minVolumeControl.MinVolumeChanged += OnMinVolumeChanged;
_minVolumeControl.MinVolume = instrument.MinVolume;
_instrumentMenu = this.CreateWindow<InstrumentMenu>();
if (EntMan.TryGetComponent<MetaDataComponent>(Owner, out var metaData))
@ -84,6 +88,9 @@ public sealed partial class InstrumentBoundUserInterface : BoundUserInterface
_instrumentMenu.AddConfigurationControl(
_loc.GetString("instruments-component-midi-file-collection-label"),
_midiCollectionUtilsControl);
_instrumentMenu.AddConfigurationControl(
_loc.GetString("instruments-component-menu-midi-min-volume-slider-label"),
_minVolumeControl);
}
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
@ -139,6 +146,11 @@ public sealed partial class InstrumentBoundUserInterface : BoundUserInterface
UpdateChannels();
}
private void OnMinVolumeChanged(int volume)
{
_instruments.SetMinVolume(Owner, volume);
}
private void OnMidiPlaybackEnded()
{
_fileSource.SelectNextTrack();

View file

@ -0,0 +1,6 @@
<Control xmlns="https://spacestation14.io">
<BoxContainer Orientation="Horizontal" VerticalExpand="True" Margin="2 2 0 0">
<Slider Name="MidiMinVolumeSlider" HorizontalExpand="True" MinValue="0" MaxValue="127" />
<Label Name="MidiMinVolumeValueLabel" MinWidth="28" Margin="6 0 3 0" Align="Right" />
</BoxContainer>
</Control>

View file

@ -0,0 +1,60 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Input;
namespace Content.Client.Instruments.UI;
[GenerateTypedNameReferences]
public sealed partial class MinVolumeControl : Control
{
/// <summary>
/// Raised when the user changes the min volume for MIDI input.
/// </summary>
public event Action<int>? MinVolumeChanged;
public int MinVolume
{
get => (int)Math.Ceiling(MidiMinVolumeSlider.Value);
set
{
MidiMinVolumeSlider.Value = value;
UpdateLabel();
}
}
public MinVolumeControl()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
MidiMinVolumeSlider.OnValueChanged += OnMidiMinVolumeSliderValueChanged;
MidiMinVolumeSlider.OnKeyBindUp += OnMidiMinVolumeSliderKeyUp;
MidiMinVolumeSlider.Value = 0;
}
private void OnMidiMinVolumeSliderValueChanged(Robust.Client.UserInterface.Controls.Range _)
{
UpdateLabel();
// Do not update while still grabbed.
if (MidiMinVolumeSlider.Grabbed)
return;
MinVolumeChanged?.Invoke(MinVolume);
}
private void OnMidiMinVolumeSliderKeyUp(GUIBoundKeyEventArgs args)
{
if (args.Function != EngineKeyFunctions.UIClick)
return;
UpdateLabel();
MinVolumeChanged?.Invoke(MinVolume);
}
private void UpdateLabel()
{
MidiMinVolumeValueLabel.Text = ((int)Math.Ceiling(MidiMinVolumeSlider.Value)).ToString();
}
}

View file

@ -58,6 +58,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
SubscribeNetworkEvent<InstrumentSetMasterEvent>(OnMidiSetMaster);
SubscribeNetworkEvent<InstrumentSetFilteredChannelEvent>(OnMidiSetFilteredChannel);
SubscribeNetworkEvent<InstrumentSetChannelsEvent>(OnMidiSetChannels);
SubscribeNetworkEvent<InstrumentSetMidiMinVolumeEvent>(OnMidiSetMinVolume);
Subs.BuiEvents<InstrumentComponent>(InstrumentUiKey.Key, subs =>
{
@ -82,7 +83,8 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
AllowProgramChange = component.AllowProgramChange,
RespectMidiLimits = component.RespectMidiLimits,
Master = GetNetEntity(component.Master),
FilteredChannels = component.FilteredChannels
FilteredChannels = component.FilteredChannels,
MinVolume = component.MinVolume,
};
}
@ -140,6 +142,17 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
Clean(uid, instrument);
}
private void OnMidiSetMinVolume(InstrumentSetMidiMinVolumeEvent msg, EntitySessionEventArgs args)
{
var uid = GetEntity(msg.Uid);
if (!TryComp(uid, out InstrumentComponent? instrument))
return;
instrument.MinVolume = msg.MinVolume;
Dirty(uid, instrument);
}
private void OnMidiSetChannels(InstrumentSetChannelsEvent msg, EntitySessionEventArgs args)
{

View file

@ -28,6 +28,9 @@ public abstract partial class SharedInstrumentComponent : Component
[DataField]
public bool RespectMidiLimits { get; set; } = true;
[DataField]
public byte MinVolume { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public EntityUid? Master { get; set; } = null;
@ -63,10 +66,26 @@ public sealed class InstrumentComponentState : ComponentState
public bool RespectMidiLimits;
public NetEntity? Master;
public byte MinVolume;
public BitArray FilteredChannels = default!;
}
/// <summary>
/// This message is sent to the client to update midi min volume.
/// </summary>
[Serializable, NetSerializable]
public sealed class InstrumentSetMidiMinVolumeEvent : EntityEventArgs
{
public NetEntity Uid { get; }
public byte MinVolume { get; set; }
public InstrumentSetMidiMinVolumeEvent(NetEntity uid, byte minVolume)
{
Uid = uid;
MinVolume = minVolume;
}
}
/// <summary>
/// This message is sent to the client to completely stop midi input and midi playback.

View file

@ -45,6 +45,7 @@ instruments-component-midi-file-collection-size-label = {$count} MIDI files load
instruments-component-midi-file-collection-open-dir-button = Open Folder
instruments-component-midi-file-collection-reload-button = Reload Collection
instruments-component-midi-file-collection-delete-all-button = Clear Collection
instruments-component-menu-midi-min-volume-slider-label = MIDI Input Min Volume
# SwappableInstrumentComponent