diff --git a/Robust.Client/Audio/Midi/IMidiRenderer.cs b/Robust.Client/Audio/Midi/IMidiRenderer.cs index e5ca9563162..5057e6cb441 100644 --- a/Robust.Client/Audio/Midi/IMidiRenderer.cs +++ b/Robust.Client/Audio/Midi/IMidiRenderer.cs @@ -216,4 +216,30 @@ 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); + + /// + /// Enables or disables per-channel reverb send (CC91). + /// + void SetChannelReverb(int channel, bool enabled); } diff --git a/Robust.Client/Audio/Midi/MidiRenderer.cs b/Robust.Client/Audio/Midi/MidiRenderer.cs index b6dd9e32bbd..b45ec9948ff 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,25 @@ 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 const int ReverbCc = 91; // CC91 (Effects 1 Depth / Reverb Send) + + private double _tempoScale = 1.0; + private readonly SequencerClientId _robustRegister; private readonly SequencerClientId _debugRegister; @@ -362,6 +382,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 +580,53 @@ 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 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)