mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
724 lines
23 KiB
C#
724 lines
23 KiB
C#
using System;
|
|
using System.Collections;
|
|
using JetBrains.Annotations;
|
|
using NFluidsynth;
|
|
using Robust.Client.Graphics;
|
|
using Robust.Shared.Asynchronous;
|
|
using Robust.Shared.Audio;
|
|
using Robust.Shared.Audio.Midi;
|
|
using Robust.Shared.Audio.Sources;
|
|
using Robust.Shared.GameObjects;
|
|
using Robust.Shared.Log;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Maths;
|
|
using Robust.Shared.Utility;
|
|
using Robust.Shared.ViewVariables;
|
|
|
|
namespace Robust.Client.Audio.Midi;
|
|
|
|
internal sealed class MidiRenderer : IMidiRenderer
|
|
{
|
|
private readonly IMidiManager _midiManager;
|
|
private readonly ITaskManager _taskManager;
|
|
|
|
// TODO: Make this a replicated CVar in MidiManager
|
|
private const int MidiSizeLimit = 2000000;
|
|
private const double BytesToMegabytes = 0.000001d;
|
|
private const int ChannelCount = RobustMidiEvent.MaxChannels;
|
|
|
|
private readonly ISawmill _midiSawmill;
|
|
|
|
private readonly Settings _settings;
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)] private bool _debugEvents = false;
|
|
|
|
// Kept around to avoid the loader callbacks getting GC'd
|
|
// ReSharper disable once NotAccessedField.Local
|
|
private readonly SoundFontLoader _soundFontLoader;
|
|
private readonly Synth _synth;
|
|
private readonly Sequencer _sequencer;
|
|
private NFluidsynth.Player? _player;
|
|
private MidiDriver? _driver;
|
|
private byte _midiProgram = 1;
|
|
private byte _midiBank = 1;
|
|
private uint _midiSoundfont = 0;
|
|
private bool _loopMidi = false;
|
|
private const int SampleRate = 44100;
|
|
private const int Buffers = SampleRate / 2205;
|
|
private readonly object _playerStateLock = new();
|
|
private readonly SequencerClientId _synthRegister;
|
|
private readonly SequencerClientId _robustRegister;
|
|
private readonly SequencerClientId _debugRegister;
|
|
|
|
[ViewVariables] private MidiRendererState _rendererState = new();
|
|
|
|
private IMidiRenderer? _master;
|
|
public MidiRendererState RendererState => _rendererState;
|
|
public IBufferedAudioSource Source { get; set; }
|
|
IBufferedAudioSource IMidiRenderer.Source => Source;
|
|
|
|
[ViewVariables]
|
|
public bool Disposed { get; private set; } = false;
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public byte MidiProgram
|
|
{
|
|
get => _midiProgram;
|
|
set
|
|
{
|
|
var disable = DisableProgramChangeEvent;
|
|
DisableProgramChangeEvent = false;
|
|
|
|
lock (_playerStateLock)
|
|
{
|
|
for (byte i = 0; i < ChannelCount; i++)
|
|
{
|
|
// Don't change percussion channel instrument.
|
|
if (i == RobustMidiEvent.PercussionChannel)
|
|
continue;
|
|
|
|
SendMidiEvent(RobustMidiEvent.ProgramChange(i, value, SequencerTick));
|
|
}
|
|
}
|
|
|
|
DisableProgramChangeEvent = disable;
|
|
_midiProgram = value;
|
|
}
|
|
}
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public byte MidiBank
|
|
{
|
|
get => _midiBank;
|
|
set
|
|
{
|
|
var disable = DisableProgramChangeEvent;
|
|
DisableProgramChangeEvent = false;
|
|
|
|
lock (_playerStateLock)
|
|
{
|
|
for (byte i = 0; i < ChannelCount; i++)
|
|
{
|
|
// Don't change percussion channel bank.
|
|
if (i == RobustMidiEvent.PercussionChannel)
|
|
continue;
|
|
|
|
SendMidiEvent(RobustMidiEvent.BankSelect(i, value, SequencerTick));
|
|
|
|
// Re-select program.
|
|
SendMidiEvent(RobustMidiEvent.ProgramChange(i, _midiProgram, SequencerTick));
|
|
}
|
|
}
|
|
|
|
DisableProgramChangeEvent = disable;
|
|
_midiBank = value;
|
|
}
|
|
}
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public uint MidiSoundfont
|
|
{
|
|
get => _midiSoundfont;
|
|
set
|
|
{
|
|
lock (_playerStateLock)
|
|
{
|
|
for (var i = 0; i < _synth.MidiChannelCount; i++)
|
|
{
|
|
_synth.SoundFontSelect(i, value);
|
|
}
|
|
}
|
|
|
|
_midiSoundfont = value;
|
|
}
|
|
}
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public bool DisablePercussionChannel
|
|
{
|
|
get => FilteredChannels[RobustMidiEvent.PercussionChannel];
|
|
set => FilteredChannels[RobustMidiEvent.PercussionChannel] = value;
|
|
}
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public bool DisableProgramChangeEvent { get; set; } = true;
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public int PlayerTotalTick => _player?.GetTotalTicks ?? 0;
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public int PlayerTick
|
|
{
|
|
get => _player?.CurrentTick ?? 0;
|
|
set
|
|
{
|
|
lock (_playerStateLock)
|
|
{
|
|
_player?.Seek(Math.Max(Math.Min(value, PlayerTotalTick-1), 0));
|
|
}
|
|
}
|
|
}
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public uint SequencerTick => !Disposed ? _sequencer?.Tick ?? 0 : 0;
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public double SequencerTimeScale
|
|
{
|
|
get => !Disposed ? _sequencer?.TimeScale ?? 0 : 0;
|
|
set => _sequencer.TimeScale = value;
|
|
}
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public bool Mono { get; set; }
|
|
|
|
[ViewVariables]
|
|
public MidiRendererStatus Status { get; private set; } = MidiRendererStatus.None;
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public bool LoopMidi
|
|
{
|
|
get => _loopMidi;
|
|
set
|
|
{
|
|
lock (_playerStateLock)
|
|
{
|
|
_player?.SetLoop(value ? -1 : 0);
|
|
}
|
|
|
|
_loopMidi = value;
|
|
}
|
|
}
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
[Obsolete($"Use {nameof(VelocityOverride)} instead, you can set it to 127 to achieve the same effect.")]
|
|
public bool VolumeBoost
|
|
{
|
|
get => VelocityOverride == 127;
|
|
set => VelocityOverride = value ? 127 : null;
|
|
}
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public EntityUid? TrackingEntity { get; set; } = null;
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public MapCoordinates? TrackingCoordinates { get; set; } = null;
|
|
|
|
[ViewVariables]
|
|
public BitArray FilteredChannels { get; } = new(RobustMidiEvent.MaxChannels);
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public byte? VelocityOverride { get; set; } = null;
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public IMidiRenderer? Master
|
|
{
|
|
get => _master;
|
|
set
|
|
{
|
|
if (value == _master)
|
|
return;
|
|
|
|
if (_master is { Disposed: false })
|
|
{
|
|
try
|
|
{
|
|
_master.OnMidiEvent -= SendMidiEvent;
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
}
|
|
|
|
_master = value;
|
|
|
|
if (_master == null)
|
|
return;
|
|
|
|
_master.OnMidiEvent += SendMidiEvent;
|
|
ApplyState(_master.RendererState, true);
|
|
MidiBank = _midiBank;
|
|
}
|
|
}
|
|
|
|
[ViewVariables, UsedImplicitly]
|
|
private double CpuLoad => !_synth.Disposed ? _synth.CpuLoad : 0;
|
|
|
|
public event Action<RobustMidiEvent>? OnMidiEvent;
|
|
public event Action? OnMidiPlayerFinished;
|
|
|
|
internal MidiRenderer(Settings settings, SoundFontLoader soundFontLoader, bool mono,
|
|
IMidiManager midiManager, IAudioInternal clydeAudio, ITaskManager taskManager, ISawmill midiSawmill)
|
|
{
|
|
_midiManager = midiManager;
|
|
_taskManager = taskManager;
|
|
_midiSawmill = midiSawmill;
|
|
|
|
Source = clydeAudio.CreateBufferedAudioSource(Buffers, true);
|
|
Source.SampleRate = SampleRate;
|
|
_settings = settings;
|
|
_soundFontLoader = soundFontLoader;
|
|
_synth = new Synth(_settings);
|
|
_sequencer = new Sequencer(false);
|
|
_debugRegister = _sequencer.RegisterClient("honk", DumpSequencerEvent);
|
|
_robustRegister = _sequencer.RegisterClient("henk", SendAsRobustMidiEvent);
|
|
|
|
// We need to register at least one synthesizer or the sequencer will refuse to work properly.
|
|
_synthRegister = _sequencer.RegisterFluidsynth(_synth);
|
|
|
|
_synth.AddSoundFontLoader(soundFontLoader);
|
|
|
|
Mono = mono;
|
|
Source.EmptyBuffers();
|
|
Source.StartPlaying();
|
|
}
|
|
|
|
private void DumpSequencerEvent(uint time, SequencerEvent midiEvent)
|
|
{
|
|
// ReSharper disable once UseStringInterpolation
|
|
_midiSawmill.Debug($"{time:D8}: {MidiManager.SequencerEventToString(midiEvent)}");
|
|
|
|
midiEvent.Dest = _robustRegister;
|
|
_sequencer.SendNow(midiEvent);
|
|
}
|
|
|
|
private void SendAsRobustMidiEvent(uint time, SequencerEvent midiEvent)
|
|
{
|
|
var robustEvent = _midiManager.FromSequencerEvent(midiEvent, time);
|
|
|
|
// Check if the command is correct.
|
|
if (robustEvent.Command != 0)
|
|
{
|
|
SendMidiEvent(robustEvent);
|
|
midiEvent.Dispose();
|
|
}
|
|
else
|
|
{
|
|
// Unsupported command, send it to the synth directly.
|
|
midiEvent.Dest = _synthRegister;
|
|
_sequencer.SendNow(midiEvent);
|
|
}
|
|
}
|
|
|
|
public bool OpenInput()
|
|
{
|
|
if (Disposed)
|
|
return false;
|
|
|
|
if (Status != MidiRendererStatus.File) CloseMidi();
|
|
|
|
lock (_playerStateLock)
|
|
{
|
|
Status = MidiRendererStatus.Input;
|
|
StopAllNotes();
|
|
|
|
_driver = new MidiDriver(_settings, MidiDriverEventHandler);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public bool OpenMidi(ReadOnlySpan<byte> buffer)
|
|
{
|
|
if (Disposed)
|
|
return false;
|
|
|
|
if (Status == MidiRendererStatus.Input) CloseInput();
|
|
|
|
lock (_playerStateLock)
|
|
{
|
|
Status = MidiRendererStatus.File;
|
|
StopAllNotes();
|
|
|
|
if (buffer.Length > MidiSizeLimit)
|
|
{
|
|
_midiSawmill.Error("MIDI file selected is too big! It was {0} MB but it should be less than {1} MB.",
|
|
buffer.Length * BytesToMegabytes, MidiSizeLimit * BytesToMegabytes);
|
|
CloseMidi();
|
|
return false;
|
|
}
|
|
|
|
_player?.Dispose();
|
|
_player = new NFluidsynth.Player(_synth);
|
|
_player.SetPlaybackCallback(MidiPlayerEventHandler);
|
|
_player.AddMem(buffer);
|
|
_player.Seek(0);
|
|
_player.Play();
|
|
_player.SetLoop(LoopMidi ? -1 : 1);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public bool CloseInput()
|
|
{
|
|
if (Status != MidiRendererStatus.Input) return false;
|
|
|
|
lock (_playerStateLock)
|
|
{
|
|
Status = MidiRendererStatus.None;
|
|
_driver?.Dispose();
|
|
_driver = null;
|
|
}
|
|
|
|
StopAllNotes();
|
|
return true;
|
|
}
|
|
|
|
public bool CloseMidi()
|
|
{
|
|
if (Status != MidiRendererStatus.File) return false;
|
|
lock (_playerStateLock)
|
|
{
|
|
Status = MidiRendererStatus.None;
|
|
if (_player == null) return false;
|
|
_player?.Stop();
|
|
_player?.Join();
|
|
_player?.Dispose();
|
|
_player = null;
|
|
}
|
|
|
|
StopAllNotes();
|
|
return true;
|
|
}
|
|
|
|
private int MidiPlayerEventHandler(MidiEvent midiEvent)
|
|
{
|
|
if (Disposed || Status != MidiRendererStatus.File && _player?.Status == FluidPlayerStatus.Playing)
|
|
return 0;
|
|
|
|
var midiEv = _midiManager.FromFluidEvent(midiEvent, SequencerTick);
|
|
midiEvent.Dispose();
|
|
SendMidiEvent(midiEv);
|
|
return 0;
|
|
}
|
|
|
|
private int MidiDriverEventHandler(MidiEvent midiEvent)
|
|
{
|
|
if (Disposed || Status != MidiRendererStatus.Input)
|
|
return 0;
|
|
|
|
var midiEv = _midiManager.FromFluidEvent(midiEvent, SequencerTick);
|
|
midiEvent.Dispose();
|
|
SendMidiEvent(midiEv);
|
|
return 0;
|
|
}
|
|
|
|
public void StopAllNotes()
|
|
{
|
|
for (byte i = 0; i < ChannelCount; i++)
|
|
{
|
|
SendMidiEvent(RobustMidiEvent.AllNotesOff(i, SequencerTick));
|
|
}
|
|
}
|
|
|
|
public void SystemReset()
|
|
{
|
|
SendMidiEvent(RobustMidiEvent.SystemReset(SequencerTick));
|
|
}
|
|
|
|
public void ClearAllEvents()
|
|
{
|
|
_sequencer.RemoveEvents(SequencerClientId.Wildcard, SequencerClientId.Wildcard, -1);
|
|
}
|
|
|
|
public void LoadSoundfont(string filename, bool resetPresets = true)
|
|
{
|
|
lock (_playerStateLock)
|
|
{
|
|
_synth.LoadSoundFont(filename, resetPresets);
|
|
MidiSoundfont = 1;
|
|
}
|
|
}
|
|
|
|
void IMidiRenderer.Render()
|
|
{
|
|
Render();
|
|
}
|
|
|
|
private void Render(int length = SampleRate / 250)
|
|
{
|
|
if (Disposed) return;
|
|
|
|
var buffersProcessed = Source.GetNumberOfBuffersProcessed();
|
|
|
|
if(buffersProcessed == Buffers)
|
|
_midiSawmill.Warning("MIDI buffer overflow!");
|
|
|
|
if (buffersProcessed == 0)
|
|
return;
|
|
|
|
var bufferLength = length * 2;
|
|
|
|
unsafe
|
|
{
|
|
Span<int> buffers = stackalloc int[buffersProcessed];
|
|
Span<float> audio = stackalloc float[bufferLength * buffers.Length];
|
|
|
|
Source.GetBuffersProcessed(buffers);
|
|
|
|
lock (_playerStateLock)
|
|
{
|
|
// _sequencer.Process(10);
|
|
_synth?.WriteSampleFloat(length * buffers.Length, audio, 0, Mono ? 1 : 2,
|
|
audio, Mono ? length * buffers.Length : 1, Mono ? 1 : 2);
|
|
}
|
|
if (Mono) // Turn audio to mono
|
|
{
|
|
var l = length * buffers.Length;
|
|
|
|
NumericsHelpers.Add(audio[..l], audio[l..]);
|
|
}
|
|
|
|
for (var i = 0; i < buffers.Length; i++)
|
|
{
|
|
var buffer = buffers[i];
|
|
Source.WriteBuffer(buffer, audio.Slice(i * length, bufferLength));
|
|
}
|
|
|
|
Source.QueueBuffers(buffers);
|
|
}
|
|
|
|
lock (_playerStateLock)
|
|
{
|
|
// Fluidsynth's player sometimes doesn't set itself to done, so also check current tick vs. total ticks.
|
|
if (Status == MidiRendererStatus.File && (_player?.Status == FluidPlayerStatus.Done || PlayerTick >= PlayerTotalTick))
|
|
{
|
|
_taskManager.RunOnMainThread(() => OnMidiPlayerFinished?.Invoke());
|
|
CloseMidi();
|
|
}
|
|
}
|
|
|
|
Source.StartPlaying();
|
|
}
|
|
|
|
public void ApplyState(MidiRendererState state, bool filterChannels = false)
|
|
{
|
|
lock (_playerStateLock)
|
|
{
|
|
_synth.SystemReset();
|
|
|
|
for (var channel = 0; channel < ChannelCount; channel++)
|
|
{
|
|
if (filterChannels && !FilteredChannels[channel])
|
|
continue;
|
|
|
|
_synth.AllNotesOff(channel);
|
|
|
|
_synth.PitchBend(channel, state.PitchBend.AsSpan[channel]);
|
|
_synth.ChannelPressure(channel, state.ChannelPressure.AsSpan[channel]);
|
|
|
|
for (var controller = 0; controller < state.Controllers.AsSpan[channel].AsSpan.Length; controller++)
|
|
{
|
|
var value = state.Controllers.AsSpan[channel].AsSpan[controller];
|
|
|
|
if (value == _synth.GetCC(channel, controller))
|
|
continue;
|
|
|
|
try
|
|
{
|
|
_synth.CC(channel, controller, value);
|
|
}
|
|
catch (FluidSynthInteropException e)
|
|
{
|
|
_midiSawmill.Error($"CH:{channel} CC:{controller} VAL:{value} {e.ToStringBetter()}");
|
|
}
|
|
}
|
|
|
|
var program = DisableProgramChangeEvent ? MidiProgram : state.Program.AsSpan[channel];
|
|
_synth.ProgramChange(channel, program);
|
|
|
|
for (var key = 0; key < state.NoteVelocities.AsSpan[channel].AsSpan.Length; key++)
|
|
{
|
|
var velocity = state.NoteVelocities.AsSpan[channel].AsSpan[key];
|
|
|
|
if (velocity <= 0)
|
|
continue;
|
|
|
|
try
|
|
{
|
|
_synth.NoteOn(channel, key, velocity);
|
|
}
|
|
catch (FluidSynthInteropException e)
|
|
{
|
|
_midiSawmill.Error($"CH:{channel} KEY:{key} VEL:{velocity} {e.ToStringBetter()}");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sorry PJB, I have to copy it.
|
|
_rendererState = state;
|
|
}
|
|
}
|
|
|
|
private void SendMidiEvent(RobustMidiEvent midiEvent)
|
|
{
|
|
SendMidiEvent(midiEvent, true);
|
|
}
|
|
|
|
public void SendMidiEvent(RobustMidiEvent midiEvent, bool raiseEvent)
|
|
{
|
|
if (Disposed)
|
|
return;
|
|
|
|
try
|
|
{
|
|
lock(_playerStateLock)
|
|
{
|
|
// Use MidiCommand as it's more readable with switch statements.
|
|
switch (midiEvent.MidiCommand)
|
|
{
|
|
case RobustMidiCommand.NoteOff:
|
|
_rendererState.NoteVelocities.AsSpan[midiEvent.Channel].AsSpan[midiEvent.Key] = 0;
|
|
_synth.NoteOff(midiEvent.Channel, midiEvent.Key);
|
|
break;
|
|
|
|
case RobustMidiCommand.NoteOn:
|
|
if (FilteredChannels[midiEvent.Channel])
|
|
break;
|
|
|
|
var velocity = VelocityOverride ?? midiEvent.Velocity;
|
|
|
|
_rendererState.NoteVelocities.AsSpan[midiEvent.Channel].AsSpan[midiEvent.Key] = velocity;
|
|
_synth.NoteOn(midiEvent.Channel, midiEvent.Key, velocity);
|
|
break;
|
|
|
|
case RobustMidiCommand.AfterTouch:
|
|
_rendererState.NoteVelocities.AsSpan[midiEvent.Channel].AsSpan[midiEvent.Key] = midiEvent.Value;
|
|
_synth.KeyPressure(midiEvent.Channel, midiEvent.Key, midiEvent.Value);
|
|
break;
|
|
|
|
case RobustMidiCommand.ControlChange:
|
|
// CC0 is bank selection
|
|
if (midiEvent.Control == 0x0 && DisableProgramChangeEvent)
|
|
break;
|
|
|
|
_rendererState.Controllers.AsSpan[midiEvent.Channel].AsSpan[midiEvent.Control] = midiEvent.Value;
|
|
if(midiEvent.Control != 0x0)
|
|
_synth.CC(midiEvent.Channel, midiEvent.Control, midiEvent.Value);
|
|
else // Fluidsynth doesn't seem to respect CC0 as bank selection, so we have to do it manually.
|
|
_synth.BankSelect(midiEvent.Channel, midiEvent.Value);
|
|
break;
|
|
|
|
case RobustMidiCommand.ProgramChange:
|
|
if (DisableProgramChangeEvent)
|
|
break;
|
|
|
|
_rendererState.Program.AsSpan[midiEvent.Channel] = midiEvent.Program;
|
|
_synth.ProgramChange(midiEvent.Channel, midiEvent.Program);
|
|
break;
|
|
|
|
case RobustMidiCommand.ChannelPressure:
|
|
_rendererState.ChannelPressure.AsSpan[midiEvent.Channel] = midiEvent.Pressure;
|
|
_synth.ChannelPressure(midiEvent.Channel, midiEvent.Pressure);
|
|
break;
|
|
|
|
case RobustMidiCommand.PitchBend:
|
|
_rendererState.PitchBend.AsSpan[midiEvent.Channel] = (ushort)midiEvent.Pitch;
|
|
_synth.PitchBend(midiEvent.Channel, midiEvent.Pitch);
|
|
break;
|
|
|
|
// Sometimes MIDI files spam these for no good reason and I can't find any info on what they are.
|
|
case (RobustMidiCommand) 0x00:
|
|
case (RobustMidiCommand) 0x01:
|
|
case (RobustMidiCommand) 0x05:
|
|
case (RobustMidiCommand) 0x50: // MetaEvent -- SetTempo, handled by the player.
|
|
return;
|
|
|
|
case RobustMidiCommand.SystemMessage:
|
|
switch (midiEvent.Control)
|
|
{
|
|
case 0x0 when midiEvent.Status == 0xFF:
|
|
_rendererState = new MidiRendererState();
|
|
_synth.SystemReset();
|
|
|
|
// Reset the instrument to the one we were using.
|
|
if (DisableProgramChangeEvent)
|
|
{
|
|
MidiBank = _midiBank;
|
|
MidiProgram = _midiProgram;
|
|
}
|
|
|
|
break;
|
|
|
|
case 0x0B:
|
|
_rendererState.NoteVelocities = default;
|
|
_synth.AllNotesOff(midiEvent.Channel);
|
|
break;
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
_midiSawmill.Warning($"Unhandled midi event of type 0x{midiEvent.Command:X}! Event: {midiEvent}");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
catch (IndexOutOfRangeException)
|
|
{
|
|
// FIXME: Handle malicious MIDI events properly, do sanity so they can't cause this exception on the state.
|
|
}
|
|
catch (FluidSynthInteropException)
|
|
{
|
|
// This spams NoteOff errors most of the time for no good reason.
|
|
//_midiSawmill.Error("Exception while sending midi event of type {0}: {1}", midiEvent.Type, e, midiEvent);
|
|
}
|
|
|
|
if (raiseEvent)
|
|
{
|
|
_taskManager.RunOnMainThread(() => OnMidiEvent?.Invoke(midiEvent));
|
|
}
|
|
}
|
|
|
|
public void ScheduleMidiEvent(RobustMidiEvent midiEvent, uint time, bool absolute = false)
|
|
{
|
|
if (Disposed) return;
|
|
|
|
var seqEv = _midiManager.ToSequencerEvent(midiEvent);
|
|
seqEv.Dest = _debugEvents ? _debugRegister : _robustRegister;
|
|
|
|
// If this is an old event, send it right now.
|
|
if(absolute && time <= SequencerTick || !absolute && time <= 0)
|
|
_sequencer.SendNow(seqEv);
|
|
else
|
|
_sequencer.SendAt(seqEv, time, absolute);
|
|
seqEv.Dispose();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Disposed = true;
|
|
|
|
switch (Status)
|
|
{
|
|
case MidiRendererStatus.Input:
|
|
CloseInput();
|
|
break;
|
|
case MidiRendererStatus.File:
|
|
CloseMidi();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
void IMidiRenderer.InternalDispose()
|
|
{
|
|
OnMidiEvent = null;
|
|
OnMidiPlayerFinished = null;
|
|
|
|
Source?.Dispose();
|
|
_driver?.Dispose();
|
|
|
|
// Do NOT dispose of the sequencer after the synth or it'll cause a segfault for some fucking reason.
|
|
_sequencer?.UnregisterClient(_debugRegister);
|
|
_sequencer?.UnregisterClient(_robustRegister);
|
|
_sequencer?.UnregisterClient(_synthRegister);
|
|
_sequencer?.Dispose();
|
|
|
|
_synth?.Dispose();
|
|
_player?.Dispose();
|
|
}
|
|
}
|