【问题标题】:How to fast-forward or backward an mp3 file while playing it?如何在播放 mp3 文件时快进或后退?
【发布时间】:2017-02-03 14:59:34
【问题描述】:

我有一门课可以帮助我播放来自 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
}

【问题讨论】:

  • 您能否提供完整的代码(包括用户界面),或者作为独立的 Visual Studio 解决方案?
  • 老实说,我目前正在设计 UI 以创建一个播放器栏来显示当前时间并允许用户快进或快退。但是真的有必要考虑解决方案吗?

标签: c# audio-streaming


【解决方案1】:

我可以告诉你,我会怎么做——假设“waveOut”的缓冲区与 DirectSound SecondaryBuffer 没有完全不同。

播放流的行为可能如下:

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

为了更容易,我们将文件/流划分为固定大小的原子子块,例如100 字节。如果文件是 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

我希望你明白这一点。

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

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

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

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

【讨论】:

  • 这个解决方案很有趣,我以前从未见过。在这个解决方法中,如果用户跳转到某个位置,我们如何将块写入文件?我的意思是如果下载的块是[1][2][3][4] 并且用户快进到第 11 个块。那么我们可以在不写入第 4 和第 11 块之间的块的情况下制作像 [1][2][3][4][11][12]... 这样的文件吗?
  • 是的,这是可能的,因为“内部”顺序由块 ID 描述。我用一个小信息图更新了我的答案。
  • 弗洛里安,这对我的问题来说是一个很好的解决方案。我现在正在使用这种方法并且它正在工作。再次感谢您。
  • 我很高兴听到这个消息。 :)
猜你喜欢
  • 2012-04-11
  • 2016-07-19
  • 2016-08-12
  • 1970-01-01
  • 2013-05-28
  • 2010-09-30
  • 2021-08-14
  • 1970-01-01
  • 2021-06-05
相关资源
最近更新 更多