Skip to content

Advanced Topics

This section delves into more advanced topics related to SoundFlow, including extending the engine with custom components, optimizing performance, and understanding threading considerations.

Extending SoundFlow

One of SoundFlow’s key strengths is its extensibility. You can tailor the engine to your specific needs by creating custom:

  • Sound Components (SoundComponent)
  • Sound Modifiers (SoundModifier)
  • Visualizers (IVisualizer)
  • Audio Backends (AudioEngine)

Custom Sound Components

Creating custom SoundComponent classes allows you to implement unique audio processing logic and integrate it seamlessly into the SoundFlow audio graph.

  1. Inherit from SoundComponent: Create a new class that inherits from the abstract SoundComponent class.
  2. Implement GenerateAudio: Override the GenerateAudio(Span<float> buffer) method. This is where you’ll write the core audio processing code for your component.
    • If your component generates audio (e.g., an oscillator), write samples to the provided buffer.
    • If your component modifies audio, read from connected input components (using a temporary buffer if necessary), process the audio, and then write to the provided buffer.
  3. Override other methods (optional): You can override methods like ConnectInput, ConnectOutput, AddModifier, etc., to customize how your component interacts with the audio graph.
  4. Add properties (optional): Add properties to your component to expose configurable parameters that users can adjust.

Example:

using SoundFlow.Abstracts;
using System;
public class CustomGainComponent : SoundComponent
{
public float Gain { get; set; } = 1.0f; // Default gain
public override string Name { get; set; } = "Custom Gain";
protected override void GenerateAudio(Span<float> buffer)
{
// Multiply each sample by the gain factor
for (int i = 0; i < buffer.Length; i++)
{
buffer[i] *= Gain;
}
}
}

Usage:

// Create an instance of your custom component
var gainComponent = new CustomGainComponent { Gain = 0.5f };
// Connect it to the audio graph
var player = new SoundPlayer(new StreamDataProvider(File.OpenRead("audio.wav")));
player.ConnectOutput(gainComponent);
Mixer.Master.AddComponent(gainComponent);
// ...

Custom Sound Modifiers

Custom SoundModifier classes allow you to implement your own audio effects.

  1. Inherit from SoundModifier: Create a new class that inherits from the abstract SoundModifier class.
  2. Implement ProcessSample: Override the ProcessSample(float sample, int channel) method. This method takes a single audio sample and the channel index as input and returns the modified sample.
  3. Add properties (optional): Add properties to your modifier to expose configurable parameters.

Example:

using SoundFlow.Abstracts;
using System;
public class CustomDistortionModifier : SoundModifier
{
public float Threshold { get; set; } = 0.5f;
public override string Name { get; set; } = "Custom Distortion";
public override float ProcessSample(float sample, int channel)
{
// Simple hard clipping distortion
if (sample > Threshold)
{
return Threshold;
}
else if (sample < -Threshold)
{
return -Threshold;
}
else
{
return sample;
}
}
}

Usage:

// Create an instance of your custom modifier
var distortion = new CustomDistortionModifier { Threshold = 0.7f };
// Add it to a SoundComponent
var player = new SoundPlayer(new StreamDataProvider(File.OpenRead("audio.wav")));
player.AddModifier(distortion);
// ...

Custom Visualizers

Custom IVisualizer classes allow you to create unique visual representations of audio data.

  1. Implement IVisualizer: Create a new class that implements the IVisualizer interface.
  2. Implement ProcessOnAudioData: This method receives a Span<float> containing audio data. You should process this data and store the relevant information needed for rendering.
  3. Implement Render: This method receives an IVisualizationContext. Use the drawing methods provided by the context (e.g., DrawLine, DrawRectangle) to render your visualization.
  4. Raise VisualizationUpdated: When the visualization data changes (e.g., after processing new audio data), raise the VisualizationUpdated event to notify the UI to update the display.

Example:

using SoundFlow.Interfaces;
using System;
using System.Numerics;
public class CustomBarGraphVisualizer : IVisualizer
{
private float _level;
public string Name => "Custom Bar Graph";
public event EventHandler? VisualizationUpdated;
public void ProcessOnAudioData(Span<float> audioData)
{
// Calculate the average level (simplified for this example)
float sum = 0;
for (int i = 0; i < audioData.Length; i++)
{
sum += Math.Abs(audioData[i]);
}
_level = sum / audioData.Length;
// Notify that the visualization needs to be updated
VisualizationUpdated?.Invoke(this, EventArgs.Empty);
}
public void Render(IVisualizationContext context)
{
// Clear the drawing area
context.Clear();
// Draw a simple bar graph based on the calculated level
float barHeight = _level * 200; // Scale the level for visualization
context.DrawRectangle(10, 200 - barHeight, 30, barHeight, new Color(0, 1, 0));
}
public void Dispose()
{
// Dispose any resources.
}
}

Adding Audio Backends

SoundFlow is designed to support multiple audio backends. Currently, it includes a MiniAudio backend. You can add support for other audio APIs (e.g., WASAPI, ASIO, CoreAudio) by creating a new backend.

  1. Create a new class that inherits from AudioEngine.
  2. Implement the abstract methods:
    • InitializeAudioDevice(): Initialize the audio device using the new backend’s API.
    • ProcessAudioData(): Implement the main audio processing loop, reading input from the device, processing the audio graph, and writing output to the device.
    • CleanupAudioDevice(): Clean up any resources used by the audio device.
    • CreateEncoder(...): Create an ISoundEncoder implementation for the new backend.
    • CreateDecoder(...): Create an ISoundDecoder implementation for the new backend.
  3. Implement ISoundEncoder and ISoundDecoder: Create classes that implement these interfaces to handle audio encoding and decoding for your chosen backend.

Example (Skeleton):

using SoundFlow.Abstracts;
using SoundFlow.Interfaces;
using System;
using System.IO;
public class MyNewAudioEngine : AudioEngine
{
public MyNewAudioEngine(int sampleRate, Capability capability, SampleFormat sampleFormat, int channels)
: base(sampleRate, capability, sampleFormat, channels)
{
// ...
}
protected override void InitializeAudioDevice()
{
// Initialize your audio device using the new backend's API
}
protected override void ProcessAudioData()
{
// Implement the audio processing loop
}
protected override void CleanupAudioDevice()
{
// Clean up audio device resources
}
protected internal override ISoundEncoder CreateEncoder(string filePath, EncodingFormat encodingFormat, SampleFormat sampleFormat, int encodingChannels, int sampleRate)
{
// Return an instance of your custom ISoundEncoder implementation
return new MyNewAudioEncoder(filePath, encodingFormat, sampleFormat, encodingChannels, sampleRate);
}
protected internal override ISoundDecoder CreateDecoder(Stream stream)
{
// Return an instance of your custom ISoundDecoder implementation
return new MyNewAudioDecoder(stream);
}
// ... (Implement MyNewAudioEncoder and MyNewAudioDecoder)
}