Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions engine/Sandbox.Engine/Systems/Audio/Mixer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,10 @@ void MixVoice( SoundHandle voice )
}

var samples = voice.sampler.GetLastReadSamples();

// Apply per-voice audio event processors before any other processing
voice.ApplyEventProcessors( samples );

var buffer = samples.Get( AudioChannel.Left );

// Store the levels on the sound for use later
Expand Down
120 changes: 120 additions & 0 deletions engine/Sandbox.Engine/Systems/Audio/Processors/AudioEventProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using System.Runtime.InteropServices;

namespace Sandbox.Audio;

/// <summary>
/// An audio processor that can be applied to individual sound events/handles.
/// Unlike AudioProcessor which works at the mixer level (processing all mixed sounds),
/// AudioEventProcessor processes samples for a single sound before mixing.
/// </summary>
[Expose]
public abstract class AudioEventProcessor
{
/// <summary>
/// Is this processor active?
/// </summary>
[Group( "Processor Settings" )]
public bool Enabled { get; set; } = true;

/// <summary>
/// Should we fade the influence of this processor in?
/// </summary>
[Range( 0, 1 )]
[Group( "Processor Settings" )]
public float Mix { get; set; } = 1;

private MultiChannelBuffer scratch;

/// <summary>
/// The sound handle this processor is attached to.
/// </summary>
[Hide]
protected SoundHandle Sound { get; private set; }

/// <summary>
/// Called when the processor is attached to a sound handle.
/// </summary>
internal void SetSound( SoundHandle sound )
{
Sound = sound;
OnAttached();
}

/// <summary>
/// Called when this processor is attached to a sound handle.
/// Override to initialize any state.
/// </summary>
protected virtual void OnAttached()
{
}

/// <summary>
/// Should process input into output
/// </summary>
internal virtual void Process( MultiChannelBuffer input, MultiChannelBuffer output )
{
Assert.True( input.ChannelCount <= output.ChannelCount );

output.CopyFrom( input );
ProcessEachChannel( output );
}

/// <summary>
/// Will process the buffer, and copy it back to output
/// </summary>
internal void ProcessInPlace( MultiChannelBuffer inputoutput )
{
if ( scratch is null || scratch.ChannelCount != inputoutput.ChannelCount )
{
scratch?.Dispose();
scratch = new MultiChannelBuffer( inputoutput.ChannelCount );
}

scratch.Silence();
Process( inputoutput, scratch );

for ( int i = 0; i < inputoutput.ChannelCount; i++ )
{
inputoutput.Get( i ).CopyFrom( scratch.Get( i ) );
}
}

/// <summary>
/// Called internally to process each channel in a buffer
/// </summary>
private unsafe void ProcessEachChannel( MultiChannelBuffer buffer )
{
for ( int i = 0; i < buffer.ChannelCount; i++ )
{
using ( buffer.Get( i ).DataPointer( out var ptr ) )
{
Span<float> memory = new Span<float>( (float*)ptr, AudioEngine.MixBufferSize );
ProcessSingleChannel( new AudioChannel( i ), memory );
}
}
}

/// <summary>
/// For implementations that process each channel individually.
/// Override this method for simple per-channel effects.
/// </summary>
protected virtual unsafe void ProcessSingleChannel( AudioChannel channel, Span<float> samples )
{
}

/// <summary>
/// Called when the processor is being destroyed or removed.
/// </summary>
protected virtual void OnDestroy()
{
}

internal void DestroyInternal()
{
OnDestroy();
scratch?.Dispose();
scratch = null;
}

public override string ToString() => GetType().Name;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
namespace Sandbox.Audio;

/// <summary>
/// A high-pass filter for individual sound events.
/// Allows frequencies above the cutoff to pass through while attenuating lower frequencies.
/// </summary>
[Expose]
public sealed class HighPassEventProcessor : AudioEventProcessor
{
/// <summary>
/// Cutoff frequency of the high-pass filter (0 to 1, where 1 is Nyquist frequency).
/// Higher values = less bass, more treble.
/// </summary>
[Range( 0, 1 )]
public float Cutoff { get; set; } = 0.5f;

private PerChannel<float> _previousInput;
private PerChannel<float> _previousOutput;

protected override unsafe void ProcessSingleChannel( AudioChannel channel, Span<float> samples )
{
if ( samples.Length == 0 )
return;

float alpha = Cutoff;
float prevIn = _previousInput.Get( channel );
float prevOut = _previousOutput.Get( channel );

for ( int i = 0; i < samples.Length; i++ )
{
float current = samples[i];
samples[i] = prevOut + alpha * (current - prevIn);
prevIn = current;
prevOut = samples[i];
}

_previousInput.Set( channel, prevIn );
_previousOutput.Set( channel, prevOut );
}
}
167 changes: 167 additions & 0 deletions engine/Sandbox.Engine/Systems/Audio/SoundHandle.Processors.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
using Sandbox.Audio;

namespace Sandbox;

partial class SoundHandle
{
/// <summary>
/// List of audio processors applied to this sound.
/// </summary>
private List<AudioEventProcessor> _processors;

/// <summary>
/// Scratch buffer for processor mixing.
/// </summary>
private MultiChannelBuffer _processorBuffer;

/// <summary>
/// Add an audio processor to this sound handle.
/// Processors are applied in order before the sound is mixed.
/// </summary>
public void AddProcessor( AudioEventProcessor processor )
{
if ( processor is null )
return;

lock ( this )
{
_processors ??= new List<AudioEventProcessor>();
_processors.Add( processor );
processor.SetSound( this );
}
}

/// <summary>
/// Remove an audio processor from this sound handle.
/// </summary>
public void RemoveProcessor( AudioEventProcessor processor )
{
if ( processor is null )
return;

lock ( this )
{
if ( _processors?.Remove( processor ) == true )
{
processor.DestroyInternal();
}
}
}

/// <summary>
/// Remove all audio processors from this sound handle.
/// </summary>
public void ClearProcessors()
{
lock ( this )
{
if ( _processors is null )
return;

foreach ( var processor in _processors )
{
processor.DestroyInternal();
}

_processors.Clear();
}
}

/// <summary>
/// Get a copy of the current processor list.
/// </summary>
public AudioEventProcessor[] GetProcessors()
{
lock ( this )
{
return _processors?.ToArray() ?? Array.Empty<AudioEventProcessor>();
}
}

/// <summary>
/// Get the first processor of a specific type.
/// </summary>
public T GetProcessor<T>() where T : AudioEventProcessor
{
lock ( this )
{
return _processors?.OfType<T>().FirstOrDefault();
}
}

/// <summary>
/// Check if this sound has any processors attached.
/// </summary>
internal bool HasProcessors => _processors is not null && _processors.Count > 0;

/// <summary>
/// Apply all processors to the sample buffer.
/// Called from the mixing thread before spatialization.
/// </summary>
internal void ApplyEventProcessors( MultiChannelBuffer samples )
{
if ( _processors is null || _processors.Count == 0 )
return;

lock ( this )
{
foreach ( var processor in _processors )
{
if ( !processor.Enabled )
continue;

if ( processor.Mix <= 0 )
continue;

try
{
if ( processor.Mix >= 1.0f )
{
// Full wet - process in place
processor.ProcessInPlace( samples );
}
else
{
// Mix dry/wet
if ( _processorBuffer is null || _processorBuffer.ChannelCount != samples.ChannelCount )
{
_processorBuffer?.Dispose();
_processorBuffer = new MultiChannelBuffer( samples.ChannelCount );
}

_processorBuffer.CopyFrom( samples );
processor.ProcessInPlace( _processorBuffer );

// Blend: output = dry * (1 - mix) + wet * mix
samples.Scale( 1.0f - processor.Mix );
samples.MixFrom( _processorBuffer, processor.Mix );
}
}
catch ( Exception e )
{
Log.Warning( e, $"Exception running event processor: {processor} - {e.Message}" );
}
}
}
}

/// <summary>
/// Clean up processor resources when sound is disposed.
/// </summary>
private void DisposeProcessors()
{
if ( _processors is not null )
{
foreach ( var processor in _processors )
{
processor.DestroyInternal();
}

_processors.Clear();
_processors = null;
}

_processorBuffer?.Dispose();
_processorBuffer = null;
}
}
1 change: 1 addition & 0 deletions engine/Sandbox.Engine/Systems/Audio/SoundHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ public void Dispose()
_sfx = default;

DisposeSources();
DisposeProcessors();

MainThread.QueueDispose( sampler );
sampler = null;
Expand Down
Loading