Skip to content

Conversation

paulomorgado
Copy link

@paulomorgado paulomorgado commented May 29, 2025

Enhance audio source interface and project configurations

  • Added EncodeAudio(ReadOnlySpan<short> pcm, AudioFormat format, IBufferWriter<byte> destination) to IAudioEncoder to avoid downstream methods having to allocate arrays.
  • Added event Action<uint, ReadOnlyMemory<byte>> OnAudioSourceEncodedSampleEx to IAudioSource to allow sliced memory to be used.
  • Updated SIPSorceryMedia.Abstractions.csproj to set the language version to 12.0.
  • Modified SIPSorceryMedia.Abstractions.UnitTest.csproj to support multi-targeting for net462 and net8.0, with conditional xUnit runner package references based on the target framework.

✅ Why Action<uint, ReadOnlyMemory<byte>> is Better

1. No Custom Delegate Needed

  • Uses built-in Action<,> delegate.
  • Reduces boilerplate and simplifies the codebase.

2. Improved Memory Efficiency

  • ReadOnlyMemory<byte> avoids unnecessary allocations.
  • Safer than byte[], which is mutable and may require defensive copying.

3. Better Interoperability

  • Action<,> integrates well with modern .NET APIs.
  • Easier to use with lambdas and method groups.

4. More Expressive and Future-Proof

  • ReadOnlyMemory<byte> signals intent: read-only, sliceable, and efficient.
  • Ideal for high-performance or streaming scenarios.

5. Less Cognitive Overhead

  • Developers immediately understand Action<,>.
  • No need to look up or maintain a custom delegate.

📊 Summary Table

Feature EncodedSampleDelegate Action<uint, ReadOnlyMemory<byte>>
Boilerplate Requires extra declaration Built-in, no extra code
Readability More semantic meaning More concise and familiar
Performance Uses byte[] (mutable) Uses ReadOnlyMemory<byte> (safe)
Flexibility Less flexible for composition Easily used with lambdas, LINQ, etc.
Interop with modern APIs Less direct Very compatible

@paulomorgado
Copy link
Author

Although this solves the immediate problem, maybe we should consider a notification sink instead.

Food for thought:

🎧 Events vs. Notification Sink in C#

Overview

When designing an audio source API, you can notify consumers using either:

  • Events (e.g., event Action<uint, ReadOnlyMemory<byte>>)
  • Notification Sink Interfaces (e.g., IAudioSourceNotificationSink)

This document compares both approaches and explains why a sink interface may be a better long-term design.


✅ Why IAudioSourceNotificationSink is Better Than Events

1. Encapsulation and Lifecycle Control

  • Events expose public subscription (+=, -=), which can lead to tight coupling and uncontrolled access.
  • A sink interface allows the source to manage the lifecycle of the listener explicitly.

2. Extensibility

  • Adding a new notification is as simple as adding a method to the interface.
  • With events, each new notification requires a new delegate and event declaration.
public interface IAudioSourceNotificationSink
{
    void OnEncodedSample(uint duration, ReadOnlyMemory<byte> sample);
    void OnVolumeChanged(float level); // Easy to extend
}
`` `

Instead of subscribing to events, consumers implement this interface and register themselves via a method like:

```csharp
void SetNotificationSink(IAudioNotificationSink sink);

✅ Advantages Over Events

1. Encapsulation and Lifecycle Control

  • Events expose public subscription (+=, -=), which can lead to tight coupling and uncontrolled access.
  • A sink interface allows the source to manage the lifecycle of the listener explicitly.

2. Extensibility

  • Adding a new notification is as simple as adding a method to the interface.
  • With events, each new notification requires a new delegate and event declaration.

3. Testability

  • Interfaces are easy to mock and verify in unit tests.
  • Events are harder to intercept and assert against.

4. Avoids Common Event Pitfalls

  • Events can cause memory leaks if subscribers forget to unsubscribe.
  • One faulty event handler can crash the entire invocation chain.
  • Sink interfaces avoid these issues by using a single, controlled reference.

5. Dependency Injection Friendly

  • Interfaces integrate naturally with DI containers.
  • You can inject different sinks for different environments (e.g., logging, UI, metrics).

6. Centralized API

  • Events are scattered and harder to discover.
  • A sink interface groups all notifications in one place, improving discoverability and documentation.

7. Async and Thread-Safe

  • Events are synchronous by default.
  • Sink methods can be implemented as async or thread-safe more easily.

🔁 Multiple Handlers: Events vs. Sink Interface

Events: Built-in Multicast

Events support multiple subscribers out of the box:

Pros:

  • Simple and familiar.
  • No extra code needed for multiple listeners.

Cons:

  • No control over execution order.
  • One failing handler can break the chain.
  • Harder to unsubscribe all at once.
  • Difficult to manage in complex systems.

Sink Interface: Composite Pattern

To support multiple listeners, you implement a composite sink:

public class CompositeAudioSink : IAudioNotificationSink
{
    private readonly List<IAudioNotificationSink> _sinks = new();

    public void AddSink(IAudioNotificationSink sink) => _sinks.Add(sink);

    public void OnEncodedSample(uint duration, ReadOnlyMemory<byte> sample)
    {
        foreach (var sink in _sinks)
        {
            sink.OnEncodedSample(duration, sample);
        }
    }
}

Pros:

  • Full control over execution order and error handling.
  • Can support filtering, prioritization, or async dispatch.
  • Easier to manage lifecycle and dependencies.

Cons:

  • Requires more boilerplate.
  • You must implement the composite logic yourself.

📊 Summary Table

Feature Events IAudioNotificationSink Interface
Multicast Support ✅ Built-in ❌ Manual (via composite)
Execution Order Control ❌ No ✅ Yes
Error Isolation ❌ No ✅ Yes
Memory Management ❌ Risk of leaks ✅ Controlled
Testability ⚠️ Harder ✅ Easy
Extensibility ⚠️ New event per notification ✅ Add method to interface
Dependency Injection ❌ Not ideal ✅ Fully compatible
Async Support ⚠️ Manual ✅ Natural
API Discoverability ❌ Scattered ✅ Centralized

🧠 Conclusion

Use events when:

  • You need simple, fire-and-forget notifications.
  • You expect multiple listeners and want minimal setup.

Use a sink interface when:

  • You need control, testability, or extensibility.
  • You're building a scalable or framework-level API.
  • You want to avoid common event pitfalls.
  • You want to manage multiple handlers with full control.

- Added new using directives and an event for handling encoded audio samples in `MediaEndPoints.cs`.
- Updated `SIPSorceryMedia.Abstractions.csproj` to set the language version to 12.0.
- Modified `SIPSorceryMedia.Abstractions.UnitTest.csproj` to support multi-targeting for `net462` and `net8.0`, with conditional xUnit runner package references based on the target framework.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant