如何在播放 mp3 文件时快进或快退?

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

我有一个 class 可以帮助我播放 URL 来源的 mp3 文件。它在播放、暂停和恢复时效果很好。但是我不知道快进还是快退。

我正在使用临时文件存储 mp3 数据,我想根据用户选择的位置重新定位 FileStream。但是有一个问题。

问题:如果职位还不存在。 (尚未下载)

这可以使用 WebRequest.AddRange() 来解决,但在这种情况下,我们必须打开一个新的 FileStream 来单独存储字节,并在每次用户想要前进或后退时调用 AddRange() 方法意味着该文件将从该位置重新下载。但是,如果这样做太频繁,我们必须下载与前进或后退次数一样多的文件。

所以,如果有简单且配额友好的解决方案,请告诉我。我不知道该怎么做。请帮忙!

我的代码:

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
}

我可以告诉你,我将如何尝试做到这一点 - 假设 "waveOut" 的缓冲区与 DirectSound SecondaryBuffer 没有完全不同。

播放流可能会像这样:

中间有已经下载和可能播放的数据和未下载的数据。为了保存这个分段下载的数据,我们需要给它添加额外的信息——时间\播放顺序。

为了更简单,我们将文件/流划分为固定大小的原子子块,例如100kByte。如果文件是 5001 kByte -> 需要 51 个子块。

您可以按下载顺序将它们保存到一个文件中,并搜索您需要的 id int - 然后在您的播放缓冲区中重新加载子块。为此,您必须使用此版本的 AddRange 来加载子块:

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

希望你明白我的意思。

  • 使用其他方法加载并保留旧流

  • 让播放缓冲区测试是否需要重新填充他的队列。

  • 仅下载尚未保存在内存或文件中的子块。

可以处理读取文件的方法: