mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
MidiRenderer master/puppet and various improvements/cleanup (#4184)
This commit is contained in:
committed by
GitHub
parent
8eae802fb6
commit
e106d3f72b
@@ -35,15 +35,20 @@ END TEMPLATE-->
|
||||
|
||||
### Breaking changes
|
||||
|
||||
*None yet*
|
||||
* Obsoleted unused `IMidiRenderer.VolumeBoost` property. Use `IMidiRenderer.VelocityOverride` instead.
|
||||
* `IMidiRenderer.TrackedCoordinates` is now a `MapCoordinates`.
|
||||
|
||||
### New features
|
||||
|
||||
*None yet*
|
||||
* Added `Master` property to `IMidiRenderer`, which allows it to copy all MIDI events from another renderer.
|
||||
* Added `FilteredChannels` property to `IMidiRenderer`, which allows it to filter out notes from certain channels.
|
||||
* Added `SystemReset` helper property to `IMidiRenderer`, which allows you to easily send it a SystemReset MIDI message.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
*None yet*
|
||||
* Fixed some cases were `MidiRenderer` would not respect the `MidiBank` and `MidiProgram.
|
||||
* Fixed user soundfonts not loading.
|
||||
* Fixed `ItemList` item selection unselecting everything when in `Multiple` mode.
|
||||
|
||||
### Other
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Audio.Midi;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -15,7 +17,6 @@ public enum MidiRendererStatus : byte
|
||||
|
||||
public interface IMidiRenderer : IDisposable
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The buffered audio source of this renderer.
|
||||
/// </summary>
|
||||
@@ -34,6 +35,7 @@ public interface IMidiRenderer : IDisposable
|
||||
/// <summary>
|
||||
/// This increases all note on velocities to 127.
|
||||
/// </summary>
|
||||
[Obsolete($"Use {nameof(VelocityOverride)} instead, you can set it to 127 to achieve the same effect.")]
|
||||
bool VolumeBoost { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -94,6 +96,27 @@ public interface IMidiRenderer : IDisposable
|
||||
/// </summary>
|
||||
double SequencerTimeScale { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this renderer will subscribe to another and copy its events.
|
||||
/// See <see cref="FilteredChannels"/> to filter specific channels.
|
||||
/// </summary>
|
||||
IMidiRenderer? Master { get; set; }
|
||||
|
||||
// NOTE: Why is the properties below BitArray, you ask?
|
||||
// Well see, MIDI 2.0 supports up to 256(!) channels as opposed to MIDI 1.0's meekly 16 channels...
|
||||
// I'd like us to support MIDI 2.0 one day so I'm just future-proofing here. Also BitArray is cool!
|
||||
|
||||
/// <summary>
|
||||
/// Allows you to filter out note events from certain channels.
|
||||
/// Only NoteOn will be filtered.
|
||||
/// </summary>
|
||||
BitArray FilteredChannels { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Allows you to override all NoteOn velocities. Set to null to disable.
|
||||
/// </summary>
|
||||
byte? VelocityOverride { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Start listening for midi input.
|
||||
/// </summary>
|
||||
@@ -120,6 +143,11 @@ public interface IMidiRenderer : IDisposable
|
||||
/// </summary>
|
||||
void StopAllNotes();
|
||||
|
||||
/// <summary>
|
||||
/// Reset renderer back to a clean state.
|
||||
/// </summary>
|
||||
void SystemReset();
|
||||
|
||||
/// <summary>
|
||||
/// Clears all scheduled events.
|
||||
/// </summary>
|
||||
@@ -156,7 +184,7 @@ public interface IMidiRenderer : IDisposable
|
||||
/// This is only used if <see cref="Mono"/> is set to True
|
||||
/// and <see cref="TrackingEntity"/> is null.
|
||||
/// </summary>
|
||||
EntityCoordinates? TrackingCoordinates { get; set; }
|
||||
MapCoordinates? TrackingCoordinates { get; set; }
|
||||
|
||||
MidiRendererState RendererState { get; }
|
||||
|
||||
@@ -164,7 +192,8 @@ public interface IMidiRenderer : IDisposable
|
||||
/// Send a midi event for the renderer to play.
|
||||
/// </summary>
|
||||
/// <param name="midiEvent">The midi event to be played</param>
|
||||
void SendMidiEvent(RobustMidiEvent midiEvent);
|
||||
/// <param name="raiseEvent">Whether to raise an event for this event.</param>
|
||||
void SendMidiEvent(RobustMidiEvent midiEvent, bool raiseEvent = true);
|
||||
|
||||
/// <summary>
|
||||
/// Schedule a MIDI event to be played at a later time.
|
||||
@@ -177,7 +206,7 @@ public interface IMidiRenderer : IDisposable
|
||||
/// <summary>
|
||||
/// Apply a certain state to the renderer.
|
||||
/// </summary>
|
||||
void ApplyState(MidiRendererState state);
|
||||
void ApplyState(MidiRendererState state, bool filterChannels = false);
|
||||
|
||||
/// <summary>
|
||||
/// Actually disposes of this renderer. Do NOT use outside the MIDI thread.
|
||||
|
||||
@@ -3,20 +3,27 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NFluidsynth;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.Audio.Midi;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Exceptions;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Threading;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
@@ -26,6 +33,13 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
{
|
||||
public const string SoundfontEnvironmentVariable = "ROBUST_SOUNDFONT_OVERRIDE";
|
||||
|
||||
private int _minRendererParallel;
|
||||
private float _occlusionUpdateDelay;
|
||||
private float _positionUpdateDelay;
|
||||
|
||||
[ViewVariables] private TimeSpan _nextOcclusionUpdate = TimeSpan.Zero;
|
||||
[ViewVariables] private TimeSpan _nextPositionUpdate = TimeSpan.Zero;
|
||||
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IResourceCacheInternal _resourceManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
@@ -33,6 +47,9 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
[Dependency] private readonly IClydeAudio _clydeAudio = default!;
|
||||
[Dependency] private readonly ITaskManager _taskManager = default!;
|
||||
[Dependency] private readonly ILogManager _logger = default!;
|
||||
[Dependency] private readonly IParallelManager _parallel = default!;
|
||||
[Dependency] private readonly IRuntimeLog _runtime = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private SharedPhysicsSystem _broadPhaseSystem = default!;
|
||||
|
||||
@@ -59,11 +76,10 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables]
|
||||
private readonly List<IMidiRenderer> _renderers = new();
|
||||
[ViewVariables] private readonly List<IMidiRenderer> _renderers = new();
|
||||
|
||||
private bool _alive = true;
|
||||
private Settings? _settings;
|
||||
[ViewVariables] private Settings? _settings;
|
||||
private Thread? _midiThread;
|
||||
private ISawmill _midiSawmill = default!;
|
||||
private float _volume = 0f;
|
||||
@@ -115,7 +131,7 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
private bool _failedInitialize;
|
||||
|
||||
private NFluidsynth.Logger.LoggerDelegate _loggerDelegate = default!;
|
||||
private ISawmill _sawmill = default!;
|
||||
private ISawmill _fluidsynthSawmill = default!;
|
||||
private float _maxCastLength;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
@@ -130,20 +146,28 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
{
|
||||
if (FluidsynthInitialized || _failedInitialize) return;
|
||||
|
||||
_volume = _cfgMan.GetCVar(CVars.MidiVolume);
|
||||
_cfgMan.OnValueChanged(CVars.MidiVolume, value =>
|
||||
{
|
||||
_volume = value;
|
||||
_volumeDirty = true;
|
||||
}, true);
|
||||
|
||||
_cfgMan.OnValueChanged(CVars.MidiMinRendererParallel,
|
||||
value => _minRendererParallel = value, true);
|
||||
|
||||
_cfgMan.OnValueChanged(CVars.MidiOcclusionUpdateDelay,
|
||||
value => _occlusionUpdateDelay = value, true);
|
||||
|
||||
_cfgMan.OnValueChanged(CVars.MidiPositionUpdateDelay,
|
||||
value => _positionUpdateDelay = value, true);
|
||||
|
||||
_midiSawmill = _logger.GetSawmill("midi");
|
||||
#if DEBUG
|
||||
_midiSawmill.Level = LogLevel.Debug;
|
||||
#else
|
||||
_midiSawmill.Level = LogLevel.Error;
|
||||
#endif
|
||||
_sawmill = _logger.GetSawmill("midi.fluidsynth");
|
||||
_fluidsynthSawmill = _logger.GetSawmill("midi.fluidsynth");
|
||||
_loggerDelegate = LoggerDelegate;
|
||||
|
||||
if (!_resourceManager.UserData.Exists(CustomSoundfontDirectory))
|
||||
@@ -167,8 +191,6 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
_settings["synth.lock-memory"].IntValue = 0;
|
||||
_settings["synth.threadsafe-api"].IntValue = 1;
|
||||
_settings["synth.gain"].DoubleValue = 1.0d;
|
||||
_settings["synth.polyphony"].IntValue = 1024;
|
||||
_settings["synth.cpu-cores"].IntValue = 2;
|
||||
_settings["synth.midi-channels"].IntValue = 16;
|
||||
_settings["synth.overflow.age"].DoubleValue = 3000;
|
||||
_settings["audio.driver"].StringValue = "file";
|
||||
@@ -176,8 +198,11 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
_settings["audio.period-size"].IntValue = 4096;
|
||||
_settings["midi.autoconnect"].IntValue = 1;
|
||||
_settings["player.reset-synth"].IntValue = 0;
|
||||
_settings["synth.midi-channels"].IntValue = Math.Clamp(RobustMidiEvent.MaxChannels, 16, 256);
|
||||
_settings["synth.midi-bank-select"].StringValue = "gm";
|
||||
//_settings["synth.verbose"].IntValue = 1; // Useful for debugging.
|
||||
|
||||
_parallel.AddAndInvokeParallelCountChanged(UpdateParallelCount);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -195,6 +220,18 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
FluidsynthInitialized = true;
|
||||
}
|
||||
|
||||
private void UpdateParallelCount()
|
||||
{
|
||||
if (_settings == null)
|
||||
return;
|
||||
|
||||
_settings["synth.polyphony"].IntValue = Math.Clamp(1024 + (int)(Math.Log2(_parallel.ParallelProcessCount) * 2048), 1, 65535);
|
||||
_settings["synth.cpu-cores"].IntValue = Math.Clamp(_parallel.ParallelProcessCount, 1, 256);
|
||||
|
||||
_midiSawmill.Debug($"Synth Cores: {_settings["synth.cpu-cores"].IntValue}");
|
||||
_midiSawmill.Debug($"Synth Polyphony: {_settings["synth.polyphony"].IntValue}");
|
||||
}
|
||||
|
||||
private void OnRaycastLengthChanged(float value)
|
||||
{
|
||||
_maxCastLength = value;
|
||||
@@ -211,7 +248,7 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
NFluidsynth.Logger.LogLevel.Debug => LogLevel.Debug,
|
||||
_ => LogLevel.Debug
|
||||
};
|
||||
_sawmill.Log(rLevel, message);
|
||||
_fluidsynthSawmill.Log(rLevel, message);
|
||||
}
|
||||
|
||||
public IMidiRenderer? GetNewRenderer(bool mono = true)
|
||||
@@ -238,7 +275,7 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
|
||||
var renderer = new MidiRenderer(_settings!, soundfontLoader, mono, this, _clydeAudio, _taskManager, _midiSawmill);
|
||||
|
||||
_midiSawmill.Debug($"Loading soundfont {FallbackSoundfont}");
|
||||
_midiSawmill.Debug($"Loading fallback soundfont {FallbackSoundfont}");
|
||||
// Since the last loaded soundfont takes priority, we load the fallback soundfont before the soundfont.
|
||||
renderer.LoadSoundfont(FallbackSoundfont);
|
||||
|
||||
@@ -252,8 +289,8 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
|
||||
try
|
||||
{
|
||||
_midiSawmill.Debug($"Loading OS soundfont {filepath}");
|
||||
renderer.LoadSoundfont(filepath);
|
||||
_midiSawmill.Debug($"Loaded Linux soundfont {filepath}");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@@ -267,7 +304,7 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
{
|
||||
if (File.Exists(OsxSoundfont) && SoundFont.IsSoundFont(OsxSoundfont))
|
||||
{
|
||||
_midiSawmill.Debug($"Loading soundfont {OsxSoundfont}");
|
||||
_midiSawmill.Debug($"Loading OS soundfont {OsxSoundfont}");
|
||||
renderer.LoadSoundfont(OsxSoundfont);
|
||||
}
|
||||
}
|
||||
@@ -275,7 +312,7 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
{
|
||||
if (File.Exists(WindowsSoundfont) && SoundFont.IsSoundFont(WindowsSoundfont))
|
||||
{
|
||||
_midiSawmill.Debug($"Loading soundfont {WindowsSoundfont}");
|
||||
_midiSawmill.Debug($"Loading OS soundfont {WindowsSoundfont}");
|
||||
renderer.LoadSoundfont(WindowsSoundfont);
|
||||
}
|
||||
}
|
||||
@@ -286,27 +323,31 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
{
|
||||
if (File.Exists(soundfontOverride) && SoundFont.IsSoundFont(soundfontOverride))
|
||||
{
|
||||
_midiSawmill.Debug($"Loading soundfont {soundfontOverride} from environment variable.");
|
||||
_midiSawmill.Debug($"Loading environment variable soundfont {soundfontOverride}");
|
||||
renderer.LoadSoundfont(soundfontOverride);
|
||||
}
|
||||
}
|
||||
|
||||
// Load content-specific custom soundfonts, which should override the system/fallback soundfont.
|
||||
_midiSawmill.Debug($"Loading soundfonts from {ContentCustomSoundfontDirectory}");
|
||||
_midiSawmill.Debug($"Loading soundfonts from content directory {ContentCustomSoundfontDirectory}");
|
||||
foreach (var file in _resourceManager.ContentFindFiles(ContentCustomSoundfontDirectory))
|
||||
{
|
||||
if (file.Extension != "sf2" && file.Extension != "dls" && file.Extension != "sf3") continue;
|
||||
_midiSawmill.Debug($"Loading soundfont {file}");
|
||||
_midiSawmill.Debug($"Loading content soundfont {file}");
|
||||
renderer.LoadSoundfont(file.ToString());
|
||||
}
|
||||
|
||||
var userDataPath = _resourceManager.UserData.RootDir == null
|
||||
? CustomSoundfontDirectory
|
||||
: new ResPath(_resourceManager.UserData.RootDir) / CustomSoundfontDirectory.ToRelativePath();
|
||||
|
||||
// Load every soundfont from the user data directory last, since those may override any other soundfont.
|
||||
_midiSawmill.Debug($"Loading soundfonts from {{USERDATA}} {CustomSoundfontDirectory}");
|
||||
var enumerator = _resourceManager.UserData.Find($"{CustomSoundfontDirectory.ToRelativePath()}/*").Item1;
|
||||
_midiSawmill.Debug($"Loading soundfonts from user data directory {userDataPath}");
|
||||
var enumerator = _resourceManager.UserData.Find($"{CustomSoundfontDirectory.ToRelativePath()}*").Item1;
|
||||
foreach (var file in enumerator)
|
||||
{
|
||||
if (file.Extension != "sf2" && file.Extension != "dls" && file.Extension != "sf3") continue;
|
||||
_midiSawmill.Debug($"Loading soundfont {{USERDATA}} {file}");
|
||||
_midiSawmill.Debug($"Loading user soundfont {file}");
|
||||
renderer.LoadSoundfont(file.ToString());
|
||||
}
|
||||
|
||||
@@ -336,72 +377,108 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
|
||||
lock (_renderers)
|
||||
{
|
||||
foreach (var renderer in _renderers)
|
||||
if (_renderers.Count == 0)
|
||||
return;
|
||||
|
||||
var transQuery = _entityManager.GetEntityQuery<TransformComponent>();
|
||||
var physicsQuery = _entityManager.GetEntityQuery<PhysicsComponent>();
|
||||
var opts = new ParallelOptions { MaxDegreeOfParallelism = _parallel.ParallelProcessCount };
|
||||
|
||||
if (_renderers.Count > _minRendererParallel)
|
||||
{
|
||||
if (renderer.Disposed)
|
||||
continue;
|
||||
|
||||
if(_volumeDirty)
|
||||
renderer.Source.SetVolume(Volume);
|
||||
|
||||
if (!renderer.Mono)
|
||||
Parallel.ForEach(_renderers, opts, renderer => UpdateRenderer(renderer, transQuery, physicsQuery));
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var renderer in _renderers)
|
||||
{
|
||||
renderer.Source.SetGlobal();
|
||||
continue;
|
||||
}
|
||||
|
||||
MapCoordinates? mapPos = null;
|
||||
var trackingEntity = renderer.TrackingEntity != null && !_entityManager.Deleted(renderer.TrackingEntity);
|
||||
if (trackingEntity)
|
||||
{
|
||||
renderer.TrackingCoordinates = _entityManager.GetComponent<TransformComponent>(renderer.TrackingEntity!.Value).Coordinates;
|
||||
}
|
||||
|
||||
if (renderer.TrackingCoordinates != null)
|
||||
{
|
||||
mapPos = renderer.TrackingCoordinates.Value.ToMap(_entityManager);
|
||||
}
|
||||
|
||||
if (mapPos != null && mapPos.Value.MapId == _eyeManager.CurrentMap)
|
||||
{
|
||||
var pos = mapPos.Value;
|
||||
|
||||
var sourceRelative = pos.Position - _eyeManager.CurrentEye.Position.Position;
|
||||
var occlusion = 0f;
|
||||
if (sourceRelative.Length() > 0)
|
||||
{
|
||||
occlusion = _broadPhaseSystem.IntersectRayPenetration(
|
||||
pos.MapId,
|
||||
new CollisionRay(
|
||||
_eyeManager.CurrentEye.Position.Position,
|
||||
sourceRelative.Normalized(),
|
||||
OcclusionCollisionMask),
|
||||
MathF.Min(sourceRelative.Length(), _maxCastLength),
|
||||
renderer.TrackingEntity);
|
||||
}
|
||||
|
||||
renderer.Source.SetOcclusion(occlusion);
|
||||
|
||||
if (!renderer.Source.SetPosition(pos.Position))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (trackingEntity)
|
||||
{
|
||||
var vel = _broadPhaseSystem.GetMapLinearVelocity(renderer.TrackingEntity!.Value);
|
||||
renderer.Source.SetVelocity(vel);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
renderer.Source.SetOcclusion(float.MaxValue);
|
||||
UpdateRenderer(renderer, transQuery, physicsQuery);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (_nextOcclusionUpdate < _timing.RealTime)
|
||||
_nextOcclusionUpdate = _timing.RealTime.Add(TimeSpan.FromSeconds(_occlusionUpdateDelay));
|
||||
|
||||
if (_nextPositionUpdate < _timing.RealTime)
|
||||
_nextPositionUpdate = _timing.RealTime.Add(TimeSpan.FromSeconds(_positionUpdateDelay));
|
||||
|
||||
_volumeDirty = false;
|
||||
}
|
||||
private void UpdateRenderer(IMidiRenderer renderer, EntityQuery<TransformComponent> transQuery,
|
||||
EntityQuery<PhysicsComponent> physicsQuery)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (renderer.Disposed)
|
||||
return;
|
||||
|
||||
if (_volumeDirty)
|
||||
renderer.Source.SetVolume(Volume);
|
||||
|
||||
if (!renderer.Mono)
|
||||
{
|
||||
renderer.Source.SetGlobal();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_nextPositionUpdate < _timing.RealTime)
|
||||
{
|
||||
if (renderer.TrackingEntity is {} trackedEntity && !_entityManager.Deleted(trackedEntity))
|
||||
{
|
||||
renderer.TrackingCoordinates = transQuery.GetComponent(renderer.TrackingEntity!.Value).MapPosition;
|
||||
}
|
||||
else if (renderer.TrackingCoordinates == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!renderer.Source.SetPosition(renderer.TrackingCoordinates.Value.Position))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var vel = _broadPhaseSystem.GetMapLinearVelocity(renderer.TrackingEntity!.Value,
|
||||
xformQuery: transQuery, physicsQuery: physicsQuery);
|
||||
renderer.Source.SetVelocity(vel);
|
||||
}
|
||||
|
||||
if (renderer.TrackingCoordinates != null && renderer.TrackingCoordinates.Value.MapId == _eyeManager.CurrentMap)
|
||||
{
|
||||
if (_nextOcclusionUpdate >= _timing.RealTime)
|
||||
return;
|
||||
|
||||
var pos = renderer.TrackingCoordinates.Value;
|
||||
|
||||
var sourceRelative = pos.Position - _eyeManager.CurrentEye.Position.Position;
|
||||
var occlusion = 0f;
|
||||
if (sourceRelative.Length() > 0)
|
||||
{
|
||||
occlusion = _broadPhaseSystem.IntersectRayPenetration(
|
||||
pos.MapId,
|
||||
new CollisionRay(
|
||||
_eyeManager.CurrentEye.Position.Position,
|
||||
sourceRelative.Normalized(),
|
||||
OcclusionCollisionMask),
|
||||
MathF.Min(sourceRelative.Length(), _maxCastLength),
|
||||
renderer.TrackingEntity);
|
||||
}
|
||||
|
||||
renderer.Source.SetOcclusion(occlusion);
|
||||
}
|
||||
else
|
||||
{
|
||||
renderer.Source.SetOcclusion(float.MaxValue);
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_runtime.LogException(ex, _midiSawmill.Name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main method for the thread rendering the midi audio.
|
||||
@@ -416,7 +493,12 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
{
|
||||
var renderer = _renderers[i];
|
||||
if (!renderer.Disposed)
|
||||
{
|
||||
if (renderer.Master is { Disposed: true })
|
||||
renderer.Master = null;
|
||||
|
||||
renderer.Render();
|
||||
}
|
||||
else
|
||||
{
|
||||
renderer.InternalDispose();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using JetBrains.Annotations;
|
||||
using NFluidsynth;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Asynchronous;
|
||||
@@ -9,7 +11,6 @@ using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Logger = Robust.Shared.Log.Logger;
|
||||
|
||||
namespace Robust.Client.Audio.Midi;
|
||||
|
||||
@@ -21,14 +22,13 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
// TODO: Make this a replicated CVar in MidiManager
|
||||
private const int MidiSizeLimit = 2000000;
|
||||
private const double BytesToMegabytes = 0.000001d;
|
||||
private const int ChannelCount = 16;
|
||||
private const int ChannelCount = RobustMidiEvent.MaxChannels;
|
||||
|
||||
private readonly ISawmill _midiSawmill;
|
||||
|
||||
private readonly Settings _settings;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
private bool _debugEvents = false;
|
||||
[ViewVariables(VVAccess.ReadWrite)] private bool _debugEvents = false;
|
||||
|
||||
// Kept around to avoid the loader callbacks getting GC'd
|
||||
// ReSharper disable once NotAccessedField.Local
|
||||
@@ -48,8 +48,9 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
private readonly SequencerClientId _robustRegister;
|
||||
private readonly SequencerClientId _debugRegister;
|
||||
|
||||
[ViewVariables]
|
||||
private MidiRendererState _rendererState = new();
|
||||
[ViewVariables] private MidiRendererState _rendererState = new();
|
||||
|
||||
private IMidiRenderer? _master;
|
||||
public MidiRendererState RendererState => _rendererState;
|
||||
public IClydeBufferedAudioSource Source { get; set; }
|
||||
IClydeBufferedAudioSource IMidiRenderer.Source => Source;
|
||||
@@ -70,8 +71,8 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
{
|
||||
for (byte i = 0; i < ChannelCount; i++)
|
||||
{
|
||||
// Channel 9 is the percussion channel. Let's not change its instrument...
|
||||
if (i == 9)
|
||||
// Don't change percussion channel instrument.
|
||||
if (i == RobustMidiEvent.PercussionChannel)
|
||||
continue;
|
||||
|
||||
SendMidiEvent(RobustMidiEvent.ProgramChange(i, value, SequencerTick));
|
||||
@@ -96,11 +97,14 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
{
|
||||
for (byte i = 0; i < ChannelCount; i++)
|
||||
{
|
||||
// Channel 9 is the percussion channel. Let's not change its bank...
|
||||
if (i == 9)
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +132,11 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool DisablePercussionChannel { get; set; } = true;
|
||||
public bool DisablePercussionChannel
|
||||
{
|
||||
get => FilteredChannels[RobustMidiEvent.PercussionChannel];
|
||||
set => FilteredChannels[RobustMidiEvent.PercussionChannel] = value;
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool DisableProgramChangeEvent { get; set; } = true;
|
||||
@@ -181,13 +189,62 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool VolumeBoost { get; set; }
|
||||
[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 EntityCoordinates? TrackingCoordinates { get; set; } = null;
|
||||
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, IClydeAudio clydeAudio, ITaskManager taskManager, ISawmill midiSawmill)
|
||||
@@ -354,6 +411,11 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
}
|
||||
}
|
||||
|
||||
public void SystemReset()
|
||||
{
|
||||
SendMidiEvent(RobustMidiEvent.SystemReset(SequencerTick));
|
||||
}
|
||||
|
||||
public void ClearAllEvents()
|
||||
{
|
||||
_sequencer.RemoveEvents(SequencerClientId.Wildcard, SequencerClientId.Wildcard, -1);
|
||||
@@ -368,9 +430,6 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
}
|
||||
}
|
||||
|
||||
public event Action<RobustMidiEvent>? OnMidiEvent;
|
||||
public event Action? OnMidiPlayerFinished;
|
||||
|
||||
void IMidiRenderer.Render()
|
||||
{
|
||||
Render();
|
||||
@@ -432,7 +491,7 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
if (!Source.IsPlaying) Source.StartPlaying();
|
||||
}
|
||||
|
||||
public void ApplyState(MidiRendererState state)
|
||||
public void ApplyState(MidiRendererState state, bool filterChannels = false)
|
||||
{
|
||||
lock (_playerStateLock)
|
||||
{
|
||||
@@ -440,6 +499,9 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
|
||||
for (var channel = 0; channel < ChannelCount; channel++)
|
||||
{
|
||||
if (filterChannels && !FilteredChannels[channel])
|
||||
continue;
|
||||
|
||||
_synth.AllNotesOff(channel);
|
||||
|
||||
_synth.PitchBend(channel, state.PitchBend.AsSpan[channel]);
|
||||
@@ -462,7 +524,8 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
}
|
||||
}
|
||||
|
||||
_synth.ProgramChange(channel, state.Program.AsSpan[channel]);
|
||||
var program = DisableProgramChangeEvent ? MidiProgram : state.Program.AsSpan[channel];
|
||||
_synth.ProgramChange(channel, program);
|
||||
|
||||
for (var key = 0; key < state.NoteVelocities.AsSpan[channel].AsSpan.Length; key++)
|
||||
{
|
||||
@@ -487,7 +550,12 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
}
|
||||
}
|
||||
|
||||
public void SendMidiEvent(RobustMidiEvent midiEvent)
|
||||
private void SendMidiEvent(RobustMidiEvent midiEvent)
|
||||
{
|
||||
SendMidiEvent(midiEvent, true);
|
||||
}
|
||||
|
||||
public void SendMidiEvent(RobustMidiEvent midiEvent, bool raiseEvent)
|
||||
{
|
||||
if (Disposed)
|
||||
return;
|
||||
@@ -505,11 +573,10 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
break;
|
||||
|
||||
case RobustMidiCommand.NoteOn:
|
||||
// Channel 9 is the percussion channel. We only block NoteOn events to it.
|
||||
if (DisablePercussionChannel && midiEvent.Channel == 9)
|
||||
return;
|
||||
if (FilteredChannels[midiEvent.Channel])
|
||||
break;
|
||||
|
||||
var velocity = (byte)(VolumeBoost ? 127 : midiEvent.Velocity);
|
||||
var velocity = VelocityOverride ?? midiEvent.Velocity;
|
||||
|
||||
_rendererState.NoteVelocities.AsSpan[midiEvent.Channel].AsSpan[midiEvent.Key] = velocity;
|
||||
_synth.NoteOn(midiEvent.Channel, midiEvent.Key, velocity);
|
||||
@@ -523,7 +590,7 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
case RobustMidiCommand.ControlChange:
|
||||
// CC0 is bank selection
|
||||
if (midiEvent.Control == 0x0 && DisableProgramChangeEvent)
|
||||
return;
|
||||
break;
|
||||
|
||||
_rendererState.Controllers.AsSpan[midiEvent.Channel].AsSpan[midiEvent.Control] = midiEvent.Value;
|
||||
if(midiEvent.Control != 0x0)
|
||||
@@ -534,7 +601,7 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
|
||||
case RobustMidiCommand.ProgramChange:
|
||||
if (DisableProgramChangeEvent)
|
||||
return;
|
||||
break;
|
||||
|
||||
_rendererState.Program.AsSpan[midiEvent.Channel] = midiEvent.Program;
|
||||
_synth.ProgramChange(midiEvent.Channel, midiEvent.Program);
|
||||
@@ -561,14 +628,14 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
switch (midiEvent.Control)
|
||||
{
|
||||
case 0x0 when midiEvent.Status == 0xFF:
|
||||
_rendererState = new ();
|
||||
_rendererState = new MidiRendererState();
|
||||
_synth.SystemReset();
|
||||
|
||||
// Reset the instrument to the one we were using.
|
||||
if (DisableProgramChangeEvent)
|
||||
{
|
||||
MidiProgram = _midiProgram;
|
||||
MidiBank = _midiBank;
|
||||
MidiProgram = _midiProgram;
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -597,7 +664,10 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
//_midiSawmill.Error("Exception while sending midi event of type {0}: {1}", midiEvent.Type, e, midiEvent);
|
||||
}
|
||||
|
||||
_taskManager.RunOnMainThread(() => OnMidiEvent?.Invoke(midiEvent));
|
||||
if (raiseEvent)
|
||||
{
|
||||
_taskManager.RunOnMainThread(() => OnMidiEvent?.Invoke(midiEvent));
|
||||
}
|
||||
}
|
||||
|
||||
public void ScheduleMidiEvent(RobustMidiEvent midiEvent, uint time, bool absolute = false)
|
||||
@@ -633,6 +703,9 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
/// <inheritdoc />
|
||||
void IMidiRenderer.InternalDispose()
|
||||
{
|
||||
OnMidiEvent = null;
|
||||
OnMidiPlayerFinished = null;
|
||||
|
||||
Source?.Dispose();
|
||||
_driver?.Dispose();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.Audio.Midi;
|
||||
|
||||
@@ -12,7 +13,7 @@ public struct MidiRendererState
|
||||
internal FixedArray16<byte> ChannelPressure;
|
||||
internal FixedArray16<ushort> PitchBend;
|
||||
|
||||
internal Span<byte> AsSpan => MemoryMarshal.CreateSpan(ref NoteVelocities._00._00, 4160);
|
||||
[ViewVariables] internal Span<byte> AsSpan => MemoryMarshal.CreateSpan(ref NoteVelocities._00._00, 4160);
|
||||
|
||||
static unsafe MidiRendererState()
|
||||
{
|
||||
|
||||
@@ -88,9 +88,9 @@ namespace Robust.Client.UserInterface.Controls
|
||||
_scrollBar.MoveToEnd();
|
||||
}
|
||||
|
||||
public Item AddItem(string text, Texture? icon = null, bool selectable = true)
|
||||
public Item AddItem(string text, Texture? icon = null, bool selectable = true, object? metadata = null)
|
||||
{
|
||||
var item = new Item(this) {Text = text, Icon = icon, Selectable = selectable};
|
||||
var item = new Item(this) {Text = text, Icon = icon, Selectable = selectable, Metadata = metadata};
|
||||
Add(item);
|
||||
return item;
|
||||
}
|
||||
@@ -448,7 +448,8 @@ namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
if (item.Selected && SelectMode != ItemListSelectMode.Button)
|
||||
{
|
||||
ClearSelected();
|
||||
if(SelectMode != ItemListSelectMode.Multiple)
|
||||
ClearSelected();
|
||||
item.Selected = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Robust.Shared.Audio.Midi
|
||||
@@ -9,6 +10,9 @@ namespace Robust.Shared.Audio.Midi
|
||||
[Serializable, NetSerializable]
|
||||
public readonly struct RobustMidiEvent
|
||||
{
|
||||
public const int MaxChannels = 16;
|
||||
public const int PercussionChannel = 9;
|
||||
|
||||
#region Data
|
||||
|
||||
/// <summary>
|
||||
@@ -49,6 +53,17 @@ namespace Robust.Shared.Audio.Midi
|
||||
Tick = tick;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clones another event but with a different tick value.
|
||||
/// </summary>
|
||||
public RobustMidiEvent(RobustMidiEvent ev, uint tick)
|
||||
{
|
||||
Status = ev.Status;
|
||||
Data1 = ev.Data1;
|
||||
Data2 = ev.Data2;
|
||||
Tick = tick;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} >> CHANNEL: 0x{Channel:X} || COMMAND: 0x{Command:X} {MidiCommand} || DATA1: 0x{Data1:X} || DATA2: 0x{Data2:X} || TICK: {Tick} <<";
|
||||
|
||||
@@ -1264,6 +1264,15 @@ namespace Robust.Shared
|
||||
public static readonly CVarDef<float> MidiVolume =
|
||||
CVarDef.Create("midi.volume", 0f, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
|
||||
public static readonly CVarDef<int> MidiMinRendererParallel =
|
||||
CVarDef.Create("midi.min_renderers_parallel_update", 3, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
|
||||
public static readonly CVarDef<float> MidiPositionUpdateDelay =
|
||||
CVarDef.Create("midi.position_update_delay", 0.125f, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
|
||||
public static readonly CVarDef<float> MidiOcclusionUpdateDelay =
|
||||
CVarDef.Create("midi.occlusion_update_delay", 0.25f, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
|
||||
/*
|
||||
* HUB
|
||||
* CVars related to public master server hub
|
||||
|
||||
@@ -305,6 +305,7 @@ Types:
|
||||
ConcurrentQueue`1: { All: True }
|
||||
ConcurrentStack`1: { All: True }
|
||||
System.Collections:
|
||||
BitArray: { All: True }
|
||||
IEnumerable: { All: True }
|
||||
IEnumerator: { All: True }
|
||||
IReadOnlyList`1: { All: True }
|
||||
|
||||
Reference in New Issue
Block a user