Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to fast-forward or backward an mp3 file while playing it?

I have a class to help me to play mp3 files from URL sources. It works good when playing, pausing and resuming. But I am confused about to fast-forward or backward.

I am using temporary files to store mp3 data and I want to re-position the FileStream according to position the user selected. But there is a problem for it.

Problem: If the position doesn't exist yet. (Not downloaded yet)enter image description here

This can be solved using WebRequest.AddRange() but in this case, we have to open a new FileStream to store bytes separately and calling AddRange() method every time that user wants to go forward or backward means that the file is going to be re-downloaded from that position. However, if this is done too often, we have to download the file as many as the number of the forward or backward.

So, if there is a simple and quota-friendly solution please let me know it. I can't figure out how to do it. Help please!

My code:

public class NAudioPlayer
{
    HttpWebRequest req;
    HttpWebResponse resp;
    Stream stream;
    WaveOut waveOut;
    Mp3WaveFormat format;
    AcmMp3FrameDecompressor decompressor;
    BufferedWaveProvider provider;
    FileStream tempFileStream;
    System.Windows.Forms.Timer ticker;
    private int bufferedDuration;   

    string url, path;
    long size, streamPos;
    int timeOffset, timePosition, avgBytes, duration;
    bool formatKnown, waitinloop, exitloop;

    State currentState;

    public NAudioPlayer(string mp3Url)
    {
        this.url = mp3Url;
        this.currentState = State.Stopped;
        this.size = -1;
        this.timeOffset = 0;
        this.timePosition = 0;
        this.avgBytes = 0;
        this.duration = 0;
        this.format = null;
        this.ticker = new System.Windows.Forms.Timer();
        this.waveOut = new WaveOut();
        this.waitinloop = false;

        ticker.Interval = 250;
        ticker.Tick += ticker_Tick;

    }
    int target = 0;
    void ticker_Tick(object sender, EventArgs e)
    {
        if (waveOut.PlaybackState == PlaybackState.Playing)
        {
            timePosition = timeOffset + (int)(waveOut.GetPosition() * 1d / waveOut.OutputWaveFormat.AverageBytesPerSecond);
            Debug.WriteLine(timePosition);
        }
        if (duration != 0 && timePosition >= duration) 
        {
            waveOut.Stop();
            ticker.Stop();
        }

        if (timePosition == target && timePosition < duration - 5 && 
            provider != null && provider.BufferedDuration.TotalSeconds < 5)
        {
            waveOut.Pause();
            currentState = State.Buffering;
            target = timePosition + 5;
        }
        if (currentState == State.Buffering && provider != null && provider.BufferedDuration.TotalSeconds >= 5)
        {
            waveOut.Play();
        }
    }

    public void Play()
    {
        int range = avgBytes <= 0 ? 0 : timeOffset * avgBytes;
        int readBytes = 0;
        long pos = 0;
        this.streamPos = 0;
        exitloop = false;
        disposeAllResources();
        ticker.Start();

        Task.Run(() =>
        {

            //Crate WebRequest using AddRange to enable repositioning the mp3
            req = WebRequest.Create(url) as HttpWebRequest;
            req.AllowAutoRedirect = true;
            req.ServicePoint.ConnectionLimit = 100;
            req.UserAgent = "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:31.0) Gecko/20100101 Firefox/31.0";
            req.AddRange(range);
            resp = req.GetResponse() as HttpWebResponse;
            stream = resp.GetResponseStream();
            size = resp.ContentLength;

            //Create a unique file to store data
            path = Path.GetTempPath() + Guid.NewGuid().ToString() + ".mp3";
            tempFileStream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);

            waveOut.Stop();
            waveOut = new WaveOut();
            if (provider != null)
                waveOut.Init(provider);

            byte[] buffer = new byte[17 * 1024];

            while ((readBytes = stream.Read(buffer, 0, buffer.Length)) > 0 ||
                    timePosition <= duration)
            {
                while (waitinloop)
                    Thread.Sleep(500);

                if (exitloop)
                    break;

                Mp3Frame frame = null;
                tempFileStream.Write(buffer, 0, readBytes);
                tempFileStream.Flush();

                //Read the stream starting from the point 
                //where we were at the last reading
                using (MemoryStream ms = new MemoryStream(ReadStreamPartially(tempFileStream, streamPos, 1024 * 10)))
                {
                    ms.Position = 0;
                    try
                    {
                        frame = Mp3Frame.LoadFromStream(ms);
                    }
                    catch { continue; } //Sometimes it throws Unexpected End of Stream exception
                    //Couldn't find the problem out, try catch is working for now

                    if (frame == null)
                        continue;

                    pos = ms.Position;
                    streamPos += pos;
                }

                if (!formatKnown)
                {
                    format = new Mp3WaveFormat(frame.SampleRate, frame.ChannelMode == ChannelMode.Mono ? 1 : 2,
                                                                frame.FrameLength, frame.BitRate);
                    duration = (int)(Math.Ceiling(resp.ContentLength * 1d / format.AverageBytesPerSecond));

                    avgBytes = format.AverageBytesPerSecond;
                    formatKnown = true;
                }

                if (decompressor == null)
                {
                    decompressor = new AcmMp3FrameDecompressor(format);
                    provider = new BufferedWaveProvider(decompressor.OutputFormat);
                    provider.BufferDuration = TimeSpan.FromSeconds(20);
                    waveOut.Init(provider);
                    waveOut.Play();
                }

                int decompressed = decompressor.DecompressFrame(frame, buffer, 0);

                if (IsBufferNearlyFull(provider))
                {
                    Thread.Sleep(500);
                }


                provider.AddSamples(buffer, 0, decompressed);
            }
        });
    }


    void disposeAllResources()
    {
        if (resp != null)
            resp.Close();
        if (stream != null)
            stream.Close();
        if (provider != null)
            provider.ClearBuffer();
    }

    public void Pause()
    {
        if (waveOut.PlaybackState == PlaybackState.Playing && !waitinloop)
        {
            waitinloop = true;
            waveOut.Pause();
            Thread.Sleep(200);
        }
    }
    public void Resume()
    {
        if (waveOut.PlaybackState == PlaybackState.Paused && waitinloop)
        {
            waitinloop = false;
            waveOut.Play();
            Thread.Sleep(200);
        }
    }
    public void ForwardOrBackward(int targetTimePos)
    {
        waitinloop = false;
        exitloop = true;
        timeOffset = targetTimePos;
        Thread.Sleep(100);
        waveOut.Stop();
        ticker.Stop();
        this.Play();
    }
    public static byte[] ReadStreamPartially(System.IO.Stream stream, long offset, long count)
    {
        long originalPosition = 0;

        if (stream.CanSeek)
        {
            originalPosition = stream.Position;
            stream.Position = offset;
        }

        try
        {
            byte[] readBuffer = new byte[4096];
            byte[] total = new byte[count];
            int totalBytesRead = 0;
            int byteRead;

            while ((byteRead = stream.ReadByte()) != -1)
            {
                Buffer.SetByte(total, totalBytesRead, (byte)byteRead);
                totalBytesRead++;
                if (totalBytesRead == count)
                {
                    stream.Position = originalPosition;
                    break;
                }
            }
            if (totalBytesRead < count)
            {
                byte[] temp = new byte[totalBytesRead];
                Buffer.BlockCopy(total, 0, temp, 0, totalBytesRead);
                stream.Position = originalPosition;
                return temp;
            }
            return total;
        }
        finally
        {
            if (stream.CanSeek)
            {
                stream.Position = originalPosition;
            }
        }
    }
    private bool IsBufferNearlyFull(BufferedWaveProvider bufferedWaveProvider)
    {
        return bufferedWaveProvider != null &&
               bufferedWaveProvider.BufferLength - bufferedWaveProvider.BufferedBytes
               < bufferedWaveProvider.WaveFormat.AverageBytesPerSecond / 4;
    }

    public int Duration
    {
        get
        {
            return duration;
        }
    }
    public int TimePosition
    {
        get
        {
            return timePosition;
        }
    }
    public int BufferedDuration
    {
        get { return (int)provider.BufferedDuration.TotalSeconds; }
    }
    public int TimeOffset
    {
        get
        {
            return timeOffset;
        }
    }
}
public enum State
{
    Paused,
    Playing,
    Stopped,
    Buffering
}
like image 428
Ali Tor Avatar asked Sep 26 '16 10:09

Ali Tor


People also ask

How do I save a file as MP3 format?

Step 1. Click on the "File" button at the top of the screen. Step 2. In the drop-down menu, click "Export" and then choose "Export as MP3."


1 Answers

I can show you, how i would try to do it - assuming that the buffer of "waveOut" is not totally different to a DirectSound SecondaryBuffer.

Playing a Stream could behave like this:

The way it's meant to be played.

There is data allready downloaded and maybe played and not downloaded data in between. To save this fractioned downloaded data, we need to add additional information to it - the time \ playorder.

To make it easier, we divide the File / Stream in atomic Subchunks of a fixed size, e.g. 100kByte. If the file is 5001 kByte -> 51 Subchunks are needed.

You can save them to a file in the downloaded order, and search for the id int you need - then reload the subchunk in your playbuffer. To do so you have to use this version of AddRange to load a subchunk:

public void AddRange( int from, int to ) https://msdn.microsoft.com/de-de/library/7fy67z6d(v=vs.110).aspx

enter image description here

I hope you get the point.

  • Load with other method and keep old stream

  • Let a playbuffer test if refilling his queque is necessary.

  • Only download a subchunk if it's not allready saved in memory or file.

A way reading to a file could be handled:

File description

like image 91
Florian p.i. Avatar answered Nov 10 '22 00:11

Florian p.i.