Search code examples
c#naudio

Dummy output for reading NAudio buffers


I trying to do the following setup and it works fine when I use a real output. Setup

I´m not sure what the right approach is to do that, I tried to use a Timer and it works for some time, but then fails because it drifts a bit and I get a buffer full exception.

  var mixSampleProvider = new MixingSampleProvider(resampleWaveFormat);
  mixSampleProvider.AddMixerInput(inputAResampler);
  mixSampleProvider.AddMixerInput(inputBResampler);

  var mixWaveProvider = new SampleToWaveProvider(mixSampleProvider);
  savingWaveProvider = new SavingWaveProvider(mixWaveProvider);

  System.Timers.Timer timer = new System.Timers.Timer(98);
  timer.Elapsed += (sender, args) =>
  {
    var count = resampleWaveFormat.AverageBytesPerSecond / 10;
    var dummy = new byte[count];
    savingWaveProvider.Read(dummy, 0, count);

  };
  timer.Start();

I have tried to calculate how much I should read on each tick e.g.

var readCount = Math.Min(inputABufferedWaveProvider.BufferedBytes, inputBBufferedWaveProvider.BufferedBytes);

but cannot make it work, and I have tried to use the DataAvailable event, but since there are two input and they are mixed I cannot that to work either.


Solution

  • The resolution of System.Timer.Timer is approximately 15.6ms, based on the Windows clock time. You need to track the time using a more accurate mechanism and adjust your read rates based on the true time rather than the rate of timer ticks.

    The most popular method of tracking elapsed time is to use a System.Diagnostics.Stopwatch to determine how much time has actually elapsed since your process started, which you can then use to calculate the number of samples to read to stay in sync.

    Here's a IWaveOutput implementation that uses a timer and a stopwatch to figure out how many samples to read from its input:

    public class SyncedNullOutput : IWavePlayer
    {
        // where to read data from
        private IWaveProvider _source;
        // time measurement
        Stopwatch _stopwatch = null;
        double _lastTime = 0;
    
        // timer to fire our read method
        System.Timers.Timer _timer = null;
    
        PlaybackState _state = PlaybackState.Stopped;
        public PlaybackState PlaybackState { get { return _state; } }
    
        public SuncedNullOutput()
        { }
    
        public SyncedNullOutput(IWaveProvider source)
        {
            Init(source);
        }
    
        public void Dispose()
        {
            Stop();
        }
    
        void _timer_Elapsed(object sender, ElapsedEventArgs args)
        {
            // get total elapsed time, compare to last time
            double elapsed = _stopwatch.Elapsed.TotalSeconds;
            double deltaTime = elapsed - _lastTime;
            _lastTime = elapsed;
            // work out number of samples we need to read...
            int nSamples = (int)(deltaTime * _source.WaveFormat.SampleRate);
            // ...and how many bytes those samples occupy
            int nBytes = nSamples * _source.WaveFormat.BlockAlign;
    
            // Read samples from the source
            byte[] buffer = new byte[nBytes];
            _source.Read(buffer, 0, nBytes);
        }
    
        public void Play()
        {
            if (_state == PlaybackState.Stopped)
            {
                // create timer
                _timer = new System.Timers.Timer(90);
                _timer.AutoReset = true;
                _timer.Elapsed += _timer_Elapsed;
                _timer.Start();
                // create stopwatch
                _stopwatch = Stopwatch.StartNew();
                _lastTime = 0;
            }
            else if (_state == PlaybackState.Paused)
            {
                // reset stopwatch
                _stopwatch.Reset();
                _lastTime = 0;
                // restart timer
                _timer.Start();
            }
            _state = PlaybackState.Playing;
        }
    
        public void Stop()
        {
            if (_timer != null)
            {
                _timer.Stop();
                _timer.Dispose();
                _timer = null;
            }
            if (_stopwatch != null)
            {
                _stopwatch.Stop();
                _stopwatch = null;
            }
            _lastTime = 0;
            _state = PlaybackState.Stopped;
        }
    
        public void Pause()
        {
            _timer.Stop();
            _state = PlaybackState.Paused;
        }
    
        public void Init(IWaveProvider waveProvider)
        {
            Stop();
            _source = waveProvider;
        }
    
        public event EventHandler<StoppedEventArgs> PlaybackStopped;
    
        protected void OnPlaybackStopped(Exception exception = null)
        {
            if (PlaybackStopped != null)
                PlaybackStopped(this, new StoppedEventArgs(exception));
        }
    
        public float Volume {get;set;}
    }
    

    I did some tests with this hooked up to a BufferedWaveProvider that was being fed samples from a default WaveInEvent instance (8kHz PCM 16-bit mono). The timer was ticking at around 93ms instead of the requested 90ms, as judged by the total run time vs number of reads, and the input buffer remained constantly under 3800 bytes in length. Changing to 44.1kHz stereo IeeeFloat format upped the buffer size to just under 80kB... still very manageable, and no overflows. In both cases the data was arriving in blocks just under half the maximum buffer size - 35280 bytes per DataAvailable event vs 76968 bytes maximum buffer length in a 60 second run, with DataAvailable firing every 100ms on average.

    Try it out and see how well it works for you.