Search code examples
c#naudio

C# Naudio, play entirely and then loop at position X to the end and return to X


I'm working on a method where the idea is, first play the sound from start to end, and loop the sound from the sample X to the end and return to that X and play it indefinitely until the user clicks the stop button.

For this I've been using this tutorial: http://mark-dot-net.blogspot.com/2009/10/looped-playback-in-net-with-naudio.html

I've changed a bit the "buttonStartStop_Click" method, my method is:

internal void PlayAudioLoopOffset(WaveOut AudioPlayer, byte[] PCMData, int Frequency, int Pitch, int Bits, int Channels, int Pan, int loopPosition)
{
    if (WaveOut.DeviceCount > 0)
    {
        if (AudioPlayer.PlaybackState == PlaybackState.Stopped)
        {
            AudioSample = new MemoryStream(PCMData);
            using (LoopStream loop = new LoopStream(new RawSourceWaveStream(AudioSample, new WaveFormat(Frequency + Pitch, Bits, Channels))))
            {
                loop.Position = loopPosition;
                VolumeSampleProvider volumeProvider = new VolumeSampleProvider(loop.ToSampleProvider());
                PanningSampleProvider panProvider = new PanningSampleProvider(volumeProvider)
                {
                    Pan = (Pan / 100)
                };
                AudioPlayer.DeviceNumber = GlobalPreferences.DefaultAudioDevice;
                AudioPlayer.Volume = 1;
                AudioPlayer.Init(panProvider);
                AudioPlayer.Play();
            }
        }
    }
    else
    {
        //Show Error
        MessageBox.Show(GenericFunctions.resourcesManager.GetString("NoAudioDevices"), "EuroSound", MessageBoxButtons.OK, MessageBoxIcon.Error);

        //Stop and dispose
        StopAudio(AudioPlayer);
    }
}

What's the problem I'm having?

When I click play, the sound starts on the X position instead of starting from the start, and then loops the sound from the start to the end indefinitely instead of from the sample X.

Represented with an image, the idea is to do this: enter image description here

Thanks to @Hazrelle now I'm a bit closer, but there's another problem, there's a bit of delay when the first audio stops and the second one starts to play, here's a recording: https://drive.google.com/file/d/1nG7OFv2QaA1FIaNM27UKWxrKw6yIco6d/view?usp=sharing

This is the new code I'm using:

internal void PlayAudioLoopPoint(WaveOut AudioPlayer, uint LoopPos, decimal Volume, byte[] PCMData, int Frequency, int Pitch, int Bits, int Channels, decimal AudioPan)
{
    MemoryStream AudioSample = new MemoryStream(PCMData);
    WaveFormat soundWavFormat = new WaveFormat(CalculateValidRate(Frequency, Pitch), Bits, Channels);
    RawSourceWaveStream FullStream = new RawSourceWaveStream(AudioSample, soundWavFormat);
    AudioPlayer.Init(FullStream);
    AudioPlayer.Play();
    AudioPlayer.PlaybackStopped += (se, ev) => audioPlayer_PlaybackStopped(AudioPlayer, PCMData, AudioSample, soundWavFormat, (int)LoopPos);
}

private void audioPlayer_PlaybackStopped(WaveOut AudioPlayer, byte[] PCMData, MemoryStream AudioSample, WaveFormat soundWavFormat, int LoopPos)
{
    MemoryStream SubsetStream = new MemoryStream(PCMData, LoopPos, (int)AudioSample.Length - LoopPos);
    LoopStream LoopStream = new LoopStream(new RawSourceWaveStream(SubsetStream, soundWavFormat));
    AudioPlayer.Init(LoopStream);
    AudioPlayer.Play();
}

Solution

  • I could finally find a solution implementing a class called "loop stream": https://markheath.net/post/looped-playback-in-net-with-naudio

    After implementing it, I've added a new property for the start position, the method that creates the IWaveProvider looks like this:

    internal IWaveProvider CreateMonoLoopWav(int startPos, int _startLoop, bool flags, byte[] _pcmData, int _frequency, float _pitch, float _pan, float _volume)
    {
        RawSourceWaveStream provider = new RawSourceWaveStream(new MemoryStream(_pcmData), new WaveFormat(SemitonesToFreq(_frequency, _pitch), 16, 1));
        LoopStream loop = new LoopStream(provider, _startLoop) { EnableLooping = flags, Position = startPos };
        PanningSampleProvider panProvider = new PanningSampleProvider(loop.ToSampleProvider()) { Pan = _pan };
        VolumeSampleProvider volumeProvider = new VolumeSampleProvider(panProvider) { Volume = _volume };
    
        return volumeProvider.ToWaveProvider();
    }
    

    And this is my version of the class loop stream:

    public class LoopStream : WaveStream
    {
        private readonly int _start;
        private readonly WaveStream sourceStream;
    
        /// <summary>
        /// Creates a new Loop stream
        /// </summary>
        /// <param name="sourceStream">The stream to read from. Note: the Read method of this stream should return 0 when it reaches the end
        /// or else we will not loop to the start again.</param>
        public LoopStream(WaveStream sourceStream, long start)
        {
            this.sourceStream = sourceStream;
            this.EnableLooping = true;
            _start = (int)(start & -2);
        }
    
        /// <summary>
        /// Use this to turn looping on or off
        /// </summary>
        public bool EnableLooping { get; set; }
    
        /// <summary>
        /// Return source stream's wave format
        /// </summary>
        public override WaveFormat WaveFormat
        {
            get { return sourceStream.WaveFormat; }
        }
    
        /// <summary>
        /// LoopStream simply returns
        /// </summary>
        public override long Length
        {
            get { return sourceStream.Length; }
        }
    
        /// <summary>
        /// LoopStream simply passes on positioning to source stream
        /// </summary>
        public override long Position
        {
            get { return sourceStream.Position; }
            set { sourceStream.Position = value; }
        }
    
        public override int Read(byte[] buffer, int offset, int count)
        {
            int totalBytesRead = 0;
    
            while (totalBytesRead < count)
            {
                int bytesRead = sourceStream.Read(buffer, offset + totalBytesRead, count - totalBytesRead);
                if (bytesRead == 0)
                {
                    if (sourceStream.Position == 0 || !EnableLooping)
                    {
                        // something wrong with the source stream
                        break;
                    }
                    // loop
                    sourceStream.Position = _start;
                }
                totalBytesRead += bytesRead;
            }
            return totalBytesRead;
        }
    }