Recursive AI

Decode audio from a stream

All oneaudio recipes

Decode audio in C# from a stream that can’t seek, such as a network response or a pipe, block by block with flat memory, with limits and typed errors for untrusted input.

C#
using System.Diagnostics;
using OneAudio;
 
// A stream that can't seek, like a network response or a pipe: here the
// output of `cat`.
var cat = Process.Start(new ProcessStartInfo("cat", "music.ogg")
{
    RedirectStandardOutput = true,
})!;
Stream pipe = cat.StandardOutput.BaseStream;
Console.WriteLine($"pipe: CanSeek {pipe.CanSeek}");
 
// Limits for input you don't trust. Sizes read from the file are checked
// before anything is allocated.
var limits = new AudioLimits { MaxChannels = 2, MaxSampleRate = 48000 };
using var cancel = new CancellationTokenSource(TimeSpan.FromSeconds(30));
 
using IAudioDecoder decoder = Audio.Open(pipe, leaveOpen: false, limits, cancel.Token);
int channels = decoder.Format.Channels;
Console.WriteLine($"{decoder.Format.SampleRate} Hz, {channels} ch, CanSeek {decoder.CanSeek}");
 
// Block by block: memory use doesn't grow with the length of the file.
float[] block = new float[1024 * channels];
long frames = 0;
double sumSquares = 0;
int read;
while ((read = decoder.Read(block)) > 0)
{
    foreach (float s in block.AsSpan(0, read * channels))
        sumSquares += s * s;
    frames += read;
}
double rms = 10 * Math.Log10(sumSquares / (frames * channels));
Console.WriteLine($"{frames} frames, RMS {rms:F1} dBFS");
 
try
{
    decoder.Seek(0);
}
catch (NotSupportedException)
{
    Console.WriteLine("Seek on a pipe: NotSupportedException");
}
 
// A file over the limits is refused with an exception that names the limit.
try
{
    using IAudioDecoder tooBig = Audio.Open("music.wav", new AudioLimits { MaxSampleRate = 32000 });
}
catch (AudioLimitExceededException e)
{
    Console.WriteLine(e.Message);
}
 
// Bytes that aren't audio at all.
try
{
    using IAudioDecoder notAudio = Audio.Open(new MemoryStream("not audio"u8.ToArray()));
}
catch (UnsupportedAudioFormatException e)
{
    Console.WriteLine($"{e.GetType().Name}: {e.Message}");
}
Text
pipe: CanSeek False
44100 Hz, 2 ch, CanSeek False
1411200 frames, RMS -24.8 dBFS
Seek on a pipe: NotSupportedException
MaxSampleRate limit exceeded: input needs 44100, limit is 32000.
UnsupportedAudioFormatException: No registered container format recognises this data.

How it works

  • Audio.Open(stream) finds the format in the first 512 bytes. A stream that can’t seek is read through a small replay buffer, so detection works on pipes and sockets too.
  • The decoder reads only as much of the stream as each Read needs. Memory doesn’t grow with the length of the file, and no decoder allocates per read.
  • CanSeek is false for a stream that can’t seek, and Seek throws NotSupportedException. LengthFrames is −1 when the length can’t be known in advance.
  • leaveOpen: false disposes the stream with the decoder.

Untrusted input

  • AudioLimits caps the channel count, sample rate, packet and frame sizes, metadata, codec setup data and container elements. Every size read from the input is checked before the allocation it guards. AudioLimits.Default applies when you pass none.
  • Every error about the data is a OneAudioException. AudioFormatException means malformed data. UnsupportedAudioFormatException, a subclass, means valid data in a format that isn’t registered or supported. AudioLimitExceededException names the limit, the maximum and what the file asked for.
  • Apart from those, the library throws only argument exceptions, NotSupportedException, ObjectDisposedException, the stream’s own I/O exceptions and OperationCanceledException.
  • Every parser and decoder is fuzzed. See Results in the README.

For the decoder interface in full, see Decoding and Errors and limits.