From 4cbd00d392df57b917ace23ee5716ad89661e87a Mon Sep 17 00:00:00 2001 From: Neenner Date: Wed, 18 Mar 2026 21:26:26 -0600 Subject: [PATCH 1/5] Add Fluidsynth-backed midicontrols --- Robust.Client/Audio/Midi/IMidiRenderer.cs | 22 ++++++++- Robust.Client/Audio/Midi/MidiRenderer.cs | 56 +++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/Robust.Client/Audio/Midi/IMidiRenderer.cs b/Robust.Client/Audio/Midi/IMidiRenderer.cs index e5ca9563162..21bb0655ae4 100644 --- a/Robust.Client/Audio/Midi/IMidiRenderer.cs +++ b/Robust.Client/Audio/Midi/IMidiRenderer.cs @@ -17,7 +17,6 @@ public enum MidiRendererStatus : byte File, } -[NotContentImplementable] public interface IMidiRenderer : IDisposable { /// @@ -216,4 +215,25 @@ public interface IMidiRenderer : IDisposable internal void InternalDispose(); byte MinVolume { get; set; } + + /// + /// Directly sets the SF2 GEN_FILTERFC generator (low-pass cutoff) for a channel + /// via fluid_synth_set_gen. A lower value muffles the channel. + /// Valid range: ~1500 (fully closed) to 13500 (fully open / no filter). + /// + void SetChannelFilterCutoff(int channel, float cutoffCents); + + /// + /// Scales MIDI file playback speed via fluid_player_set_tempo + /// 1.0 = normal, 2.0 = double speed, 0.5 = half speed. + /// Stored and re-applied when a new MIDI file is opened. + /// Has no effect during MIDI input mode (no player). + /// + void SetTempoScale(double scale); + + /// + /// Per-channel pitch offset in semitones via SF2 GEN_COARSETUNE. + /// Range typically -12 to +12 (one octave). 0 = no shift. + /// + void SetChannelPitch(int channel, int semitones); } diff --git a/Robust.Client/Audio/Midi/MidiRenderer.cs b/Robust.Client/Audio/Midi/MidiRenderer.cs index b6dd9e32bbd..f362f366987 100644 --- a/Robust.Client/Audio/Midi/MidiRenderer.cs +++ b/Robust.Client/Audio/Midi/MidiRenderer.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Runtime.InteropServices; using JetBrains.Annotations; using NFluidsynth; using Robust.Client.Graphics; @@ -48,6 +49,24 @@ internal sealed partial class MidiRenderer : IMidiRenderer private const int Buffers = SampleRate / 2205; private readonly object _playerStateLock = new(); private readonly SequencerClientId _synthRegister; + + // P/Invoke for fluid_synth_set_gen — lets us set SF2 generator values per channel + // directly, bypassing MIDI CC limitations. + [DllImport("libfluidsynth-3", CallingConvention = CallingConvention.Cdecl)] + private static extern int fluid_synth_set_gen(IntPtr synth, int chan, int param, float value); + + private const int GenFilterCutoff = 8; // GEN_FILTERFC in the SF2 spec + // P/Invoke for fluid_player_set_tempo — scales MIDI playback BPM by a multiplier. + // FLUID_PLAYER_TEMPO_INTERNAL (0) applies the value as a speed multiplier while + // still honouring in-file SetTempo events: 1.0 = normal, 2.0 = double, 0.5 = half. + [DllImport("libfluidsynth-3", CallingConvention = CallingConvention.Cdecl)] + private static extern int fluid_player_set_tempo(IntPtr player, int tempoType, double tempo); + + private const int FluidPlayerTempoInternal = 0; // FLUID_PLAYER_TEMPO_INTERNAL + private const int GenCoarseTune = 51; // GEN_COARSETUNE — semitone pitch offset + + private double _tempoScale = 1.0; + private readonly SequencerClientId _robustRegister; private readonly SequencerClientId _debugRegister; @@ -362,6 +381,8 @@ public bool OpenMidi(ReadOnlySpan buffer) _player.Seek(0); _player.Play(); _player.SetLoop(LoopMidi ? -1 : 1); + if (_tempoScale != 1.0) + fluid_player_set_tempo(_player.Handle, FluidPlayerTempoInternal, _tempoScale); } return true; @@ -558,6 +579,41 @@ private void SendMidiEvent(RobustMidiEvent midiEvent) SendMidiEvent(midiEvent, true); } + public void SetChannelFilterCutoff(int channel, float cutoffCents) + { + if (Disposed) + return; + + lock (_playerStateLock) + { + fluid_synth_set_gen(_synth.Handle, channel, GenFilterCutoff, cutoffCents); + } + } + + public void SetTempoScale(double scale) + { + if (Disposed) + return; + + _tempoScale = scale; + lock (_playerStateLock) + { + if (_player != null) + fluid_player_set_tempo(_player.Handle, FluidPlayerTempoInternal, scale); + } + } + + public void SetChannelPitch(int channel, int semitones) + { + if (Disposed) + return; + + lock (_playerStateLock) + { + fluid_synth_set_gen(_synth.Handle, channel, GenCoarseTune, (float)semitones); + } + } + public void SendMidiEvent(RobustMidiEvent midiEvent, bool raiseEvent) { if (Disposed) From 2072b463a22e8414361ea2c3453e1f9866c38d28 Mon Sep 17 00:00:00 2001 From: Neenner Date: Wed, 18 Mar 2026 21:37:00 -0600 Subject: [PATCH 2/5] Add Fluidsynth-backed midicontrols --- Robust.Client/Audio/Midi/IMidiRenderer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Robust.Client/Audio/Midi/IMidiRenderer.cs b/Robust.Client/Audio/Midi/IMidiRenderer.cs index 21bb0655ae4..de54c6e4e57 100644 --- a/Robust.Client/Audio/Midi/IMidiRenderer.cs +++ b/Robust.Client/Audio/Midi/IMidiRenderer.cs @@ -17,6 +17,7 @@ public enum MidiRendererStatus : byte File, } +[NotContentImplementable] public interface IMidiRenderer : IDisposable { /// From 5604b7431501ce0eece756cbdd691b717fb265f6 Mon Sep 17 00:00:00 2001 From: QTNeen <34391034+QTNeen@users.noreply.github.com> Date: Sat, 21 Mar 2026 03:49:23 -0600 Subject: [PATCH 3/5] Added reverb to IMidiRenderer --- Robust.Client/Audio/Midi/IMidiRenderer.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Robust.Client/Audio/Midi/IMidiRenderer.cs b/Robust.Client/Audio/Midi/IMidiRenderer.cs index de54c6e4e57..607dd074ffc 100644 --- a/Robust.Client/Audio/Midi/IMidiRenderer.cs +++ b/Robust.Client/Audio/Midi/IMidiRenderer.cs @@ -237,4 +237,11 @@ public interface IMidiRenderer : IDisposable /// Range typically -12 to +12 (one octave). 0 = no shift. /// void SetChannelPitch(int channel, int semitones); + + /// + /// Enables or disables per-channel reverb send (CC91). + /// + void SetChannelReverb(int channel, bool enabled); +} + } From 7740a6965ca6f2e0fd998847c228c521702bd43c Mon Sep 17 00:00:00 2001 From: QTNeen <34391034+QTNeen@users.noreply.github.com> Date: Sat, 21 Mar 2026 03:51:00 -0600 Subject: [PATCH 4/5] Implement SetChannelReverb Add method to set reverb for a specific MIDI channel. --- Robust.Client/Audio/Midi/MidiRenderer.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Robust.Client/Audio/Midi/MidiRenderer.cs b/Robust.Client/Audio/Midi/MidiRenderer.cs index f362f366987..b45ec9948ff 100644 --- a/Robust.Client/Audio/Midi/MidiRenderer.cs +++ b/Robust.Client/Audio/Midi/MidiRenderer.cs @@ -64,6 +64,7 @@ internal sealed partial class MidiRenderer : IMidiRenderer private const int FluidPlayerTempoInternal = 0; // FLUID_PLAYER_TEMPO_INTERNAL private const int GenCoarseTune = 51; // GEN_COARSETUNE — semitone pitch offset + private const int ReverbCc = 91; // CC91 (Effects 1 Depth / Reverb Send) private double _tempoScale = 1.0; @@ -614,6 +615,18 @@ public void SetChannelPitch(int channel, int semitones) } } + public void SetChannelReverb(int channel, bool enabled) + { + if (Disposed) + return; + + lock (_playerStateLock) + { + _rendererState.Controllers.AsSpan[channel].AsSpan[ReverbCc] = (byte) (enabled ? 127 : 0); + _synth.CC(channel, ReverbCc, enabled ? 127 : 0); + } + } + public void SendMidiEvent(RobustMidiEvent midiEvent, bool raiseEvent) { if (Disposed) From 4ddfbc5c13e73423762e1b18198a9bb5db64a0eb Mon Sep 17 00:00:00 2001 From: QTNeen <34391034+QTNeen@users.noreply.github.com> Date: Sat, 21 Mar 2026 03:51:46 -0600 Subject: [PATCH 5/5] Clean up formatting in IMidiRenderer.cs Removed unnecessary blank lines and adjusted formatting --- Robust.Client/Audio/Midi/IMidiRenderer.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Robust.Client/Audio/Midi/IMidiRenderer.cs b/Robust.Client/Audio/Midi/IMidiRenderer.cs index 607dd074ffc..5057e6cb441 100644 --- a/Robust.Client/Audio/Midi/IMidiRenderer.cs +++ b/Robust.Client/Audio/Midi/IMidiRenderer.cs @@ -237,11 +237,9 @@ public interface IMidiRenderer : IDisposable /// Range typically -12 to +12 (one octave). 0 = no shift. /// void SetChannelPitch(int channel, int semitones); - + /// /// Enables or disables per-channel reverb send (CC91). /// void SetChannelReverb(int channel, bool enabled); } - -}