Search code examples
naudiolibpd

Preparing Sound Data when needed


I am using libPd to generate sound. LibPd can process a predefined number of Ticks and then fill a float[] with the generated output. I then use a BufferedWaveProvider to store the float[] after converting to a byte[].

Generation of audio can be really fast, so it is possible to compute 1 second of sound in quite short time, but it can also be slow, depending on the Pd patch. Is there a way to trigger processing data, when the BufferedWaveProvider has less than a predefined amount of data left?

Currently I am generating audio in a background thread an just sleep for a while and hope that this is enough, and hope for BufferedWaveProvider not overflowing, and even when, then discard that data.

public void ProcessData(float[] output, int ticks)
{
    while (LibPD.Process(ticks, new float[0], output) == 0)
    {
        if (BufferReady != null)
        {
            BufferReady(this, new BufferReadyEventArgs(output));
        }
        Thread.Sleep(999*BlockSize * ticks / 44100);
    }
} 

public void StartProcessing(float[] output)
{
    ThreadPool.QueueUserWorkItem(new WaitCallback(SetPdOutput), output);
}

private void SetPdOutput(object state)
{
    float[] output = state as float[];
    if (state == null)
    {
        return;
    }
    ProcessData(output, Ticks);
}

While my code to glue together libPd and NAudio is this:

Buffer = new float[2 * _player.Ticks * _player.BlockSize]; // stereo, number of Ticks
_soundOutput = new WasapiOut(AudioClientShareMode.Shared, 100);
_audioBuffer = new BufferedWaveProvider(WaveFormat.CreateIeeeFloatWaveFormat(44100, 2))
{
    BufferDuration = TimeSpan.FromSeconds(10),
    DiscardOnBufferOverflow = true
};
_soundOutput.Init(_audioBuffer);
_soundOutput.Play();

_player.BufferReady += ((sender, eventArgs) =>
{
    _audioBuffer.AddSamples(PcmFromFloat(output), 0, output.Length * 4);
});
_player.StartProcessing(Buffer);

Solution

  • After reading the source of NAudio, I have come up with a different solution: Writing a custom IWaveProvider, that uses a CircularBuffer analogous to BufferedWaveProvider, but requests new processed audio from lidPd whenever a threshold is underrun.

    This is the gist of the operation:

    class PdProvider : IWaveProvider
    {
        readonly CircularBuffer _circularBuffer;
    
        public PdProvider()
        {
            _buffer = new float[BufferSize];
            _player.BufferReady += PdBufferReady;
            _circularBuffer =  new CircularBuffer(SampleRate * 5); // 5 seconds should be enough for anybody
            _minBuffer =  SampleRate / 2; // 0.5 second
            RefillBuffer();
        }
    
        void RefillBuffer()
        {
            if (_circularBuffer.Count < _minBuffer)
            {
                // Get data from libPd and add to _circularBuffer
            }
        }
    
        public int Read(byte[] buffer, int offset, int count)
        {
            var read = _circularBuffer.Read(buffer, offset, count);
            RefillBuffer();
            return read;
        }
    
        public WaveFormat WaveFormat
        {
            get
            {
                return WaveFormat.CreateIeeeFloatWaveFormat(_player.SampleRate, 2);
            }
        }
    }