如何在C# / NAudio音乐播放器中创建一个进度条?

12

我对NAudio和C#都是新手,但我成功创建了一个简单的MP3播放器,您可以选择文件并播放它。它还有一个播放/暂停按钮。

现在我想添加一个进度条,但不知道如何操作。同时,能否以波形样式呈现进度条?

openButton点击处理程序

private void openButton_Click(object sender, EventArgs e)
{
    OpenFileDialog open = new OpenFileDialog();
    open.Filter = "Audio File|*.mp3;";

    if (open.ShowDialog() != DialogResult.OK) 
        return;

    CloseWaveOut();  // disposes the waveOutDevice and audiofilereader
    waveOutDevice = new WaveOut();
    audioFileReader = new AudioFileReader(open.FileName);

    waveOutDevice.Init(audioFileReader);
    waveOutDevice.Play();

    pauseButton.Enabled = true;            
}

你使用的是WinForms还是WPF? - Mark Heath
我正在使用Windows表单。 - GunJack
2个回答

32

除了纯粹基于UI的问题之外,您需要能够完成以下三件基本事情:

  1. 读取歌曲长度。

  2. 获取播放位置。

  3. 设置播放位置。

歌曲长度和当前播放位置相当简单-它们可以通过WaveStream对象的TotalTimeCurrentTime属性获得,这意味着您的audioFileReader对象也支持它们。构造后,audioFileReader.TotalTime将为您提供一个包含文件总长度的TimeSpan对象,而audioFileReader.CurrentTime将为您提供当前播放位置。

您还可以通过将值分配给audioFileReader.CurrentTime设置播放位置...但这样做是一个棘手的过程,除非您知道自己在做什么。下面的代码跳过2.5秒有时会工作,但在其他情况下会崩溃:

audioFileReader.CurrentTime = audioFileReader.CurrentTime.Add(TimeSpan.FromSeconds(2.5));

这里的问题在于由于某些原因(包括后台执行的浮点数计算),结果流(以字节为单位)的位置可能无法正确对齐到样本的开头。这会很快使您的输出变成垃圾。

更好的选择是在想要更改播放位置时使用流的Position属性。Position是当前以字节为单位的播放位置,因此稍微难以操作一点。但不是太难:

audioFileReader.Position += audioFileReader.WaveFormat.AverageBytesPerSecond;

如果你向前或向后移动整数秒,那没问题。如果不是,你需要确保始终定位在采样边界上,使用 WaveFormat.BlockAlign 属性来确定这些边界位置。

// Calculate new position
long newPos = audioFileReader.Position + (long)(audioFileReader.WaveFormat.AverageBytesPerSecond * 2.5);
// Force it to align to a block boundary
if ((newPos % audioFileReader.WaveFormat.BlockAlign) != 0)
    newPos -= newPos % audioFileReader.WaveFormat.BlockAlign;
// Force new position into valid range
newPos = Math.Max(0, Math.Min(audioFileReader.Length, newPos));
// set position
audioFileReader.Position = newPos;

这里简单的做法是定义一组扩展WaveStream类的操作,以在寻找操作期间处理块对齐。基本的块对齐操作可以通过变化进行调用,只需从您放入的任何内容计算新位置,例如:

public static class WaveStreamExtensions
{
    // Set position of WaveStream to nearest block to supplied position
    public static void SetPosition(this WaveStream strm, long position)
    {
        // distance from block boundary (may be 0)
        long adj = position % strm.WaveFormat.BlockAlign;
        // adjust position to boundary and clamp to valid range
        long newPos = Math.Max(0, Math.Min(strm.Length, position - adj));
        // set playback position
        strm.Position = newPos;
    }

    // Set playback position of WaveStream by seconds
    public static void SetPosition(this WaveStream strm, double seconds)
    {
        strm.SetPosition((long)(seconds * strm.WaveFormat.AverageBytesPerSecond));
    }

    // Set playback position of WaveStream by time (as a TimeSpan)
    public static void SetPosition(this WaveStream strm, TimeSpan time)
    {
        strm.SetPosition(time.TotalSeconds);
    }

    // Set playback position of WaveStream relative to current position
    public static void Seek(this WaveStream strm, double offset)
    {
        strm.SetPosition(strm.Position + (long)(offset* strm.WaveFormat.AverageBytesPerSecond));
    }
}

有了这个功能,您可以调用 audioFileReader.SetPosition(10.0) 来跳转到播放位置 00:00:10.0,调用 audioFileReader.Seek(-5) 后退 5 秒等等,而不用担心在采样点中间寻找。

因此... 在您的表单中添加一些按钮,并设置它们调用 Seek 方法来移动 +/- 值。然后添加一个滑块,可以用它来显示和设置播放位置。加入一个计时器来更新滑块位置到当前播放位置,这样就完成了。


这就是我在寻找的东西。所以,非常棒。 - GunJack
2
非常完整的答案。希望我能点赞两次。 - dotNET
@dotNET 很高兴看到一个两年前的答案仍然有用 :) - Corey
1
@Corey 不仅仅是有用,而是非常有用——而且完全绕过了框架的概念。很棒的答案。你有我的10票支持。 - Alan Wayne

5
有一个好的答案,但我想再添加另一种在WPF中构建seekbar的方式,因为我也正在进行类似的项目。
以下是用于该查找器的XAML代码:
<Slider Grid.Column="0" Minimum="0" Maximum="{Binding CurrentTrackLenght, Mode=OneWay}" Value="{Binding CurrentTrackPosition, Mode=TwoWay}" x:Name="SeekbarControl" VerticalAlignment="Center">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="PreviewMouseDown">
            <i:InvokeCommandAction Command="{Binding TrackControlMouseDownCommand}"></i:InvokeCommandAction>
        </i:EventTrigger>
        <i:EventTrigger EventName="PreviewMouseUp">
            <i:InvokeCommandAction Command="{Binding TrackControlMouseUpCommand}"></i:InvokeCommandAction>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Slider>

我们 ViewModel 中的 CurrentTrackLenghtCurrentTrackPosition 是:

public double CurrentTrackLenght
{
    get { return _currentTrackLenght; }
    set
    {
        if (value.Equals(_currentTrackLenght)) return;
        _currentTrackLenght = value;
        OnPropertyChanged(nameof(CurrentTrackLenght));
    }
}

public double CurrentTrackPosition
{
    get { return _currentTrackPosition; }
    set
    {
        if (value.Equals(_currentTrackPosition)) return;
        _currentTrackPosition = value;
        OnPropertyChanged(nameof(CurrentTrackPosition));
    }
}

这个想法非常简单;一旦我们开始播放:
首先,我们获取音频文件的长度(以秒为单位),并将其分配给CurrentTrackLenght属性,它将绑定到seekbar的Maximum属性。 然后,当我们播放音频文件时,我们不断更新CurrentTrackPosition属性,这反过来又驱动了我们的seekbar的Value属性。
因此,当我们按下“播放”按钮时,我们ViewModel中的以下命令将运行:
private void StartPlayback(object p)
{
    if (_playbackState == PlaybackState.Stopped)
    {
        if (CurrentTrack != null)
        {
            _audioPlayer.LoadFile(CurrentTrack.Filepath, CurrentVolume);
            CurrentTrackLenght = _audioPlayer.GetLenghtInSeconds();
        }
    }

    _audioPlayer.TogglePlayPause(CurrentVolume);
}

_audioPlayer是我用来简化播放/暂停/停止的抽象,因此您可以用自己的代码替换它们。但是重要的部分是:

CurrentTrackLenght = _audioPlayer.GetLenghtInSeconds();

AudioPlayerGetLenghtInSeconds() 的代码如下:

public double GetLenghtInSeconds()
{
    if (_audioFileReader != null)
    {
        return _audioFileReader.TotalTime.TotalSeconds;
    }
    else
    {
        return 0;
    }
}

因此,我们需要为每个开始播放的音频文件初始化seekbar的最大值。

现在,我们需要在音频播放时更新我们的seekbar。

首先,我们需要确定音频文件的当前位置(以秒为单位)。在这里我选择以秒为单位,因为我们的seekbar的Maximum也是以秒为单位的,所以它们将正确匹配。

要做到这一点,我们需要在AudioPlayer中使用以下方法:

public double GetPositionInSeconds()
{
    if (_audioFileReader != null)
    {
        return _audioFileReader.CurrentTime.TotalSeconds;
    }
    else
    {
        return 0;
    }
}

代码完成后,我们可以继续处理ViewModel。首先,我们需要在构造函数中设置一个计时器。

var timer = new System.Timers.Timer();
timer.Interval = 300;
timer.Elapsed += Timer_Elapsed;
timer.Start();

添加 Timer_Elapsed()UpdateSeekBar() 方法:

private void UpdateSeekBar()
{
    if (_playbackState == PlaybackState.Playing)
    {
        CurrentTrackPosition = _audioPlayer.GetPositionInSeconds();
    }
}

private void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
    UpdateSeekBar();
}

完成这个步骤后,现在我们播放音频文件时,滑动条应该按预期移动。
接下来是实际的寻找部分,首先我们需要在AudioPlayer类中添加一个SetPosition()方法。
public void SetPosition(double value)
{
    if (_audioFileReader != null)
    {
        _audioFileReader.CurrentTime = TimeSpan.FromSeconds(value);
    }
}

这段代码将当前时间设置为我们传递的值,因此有效地寻找到新位置。

最后,我们需要4个方法来完成PreviewMouseDownPreviewMouseUp事件的ViewModel命令。

private void TrackControlMouseDown(object p)
{
    _audioPlayer.Pause();
}

private void TrackControlMouseUp(object p)
{
    _audioPlayer.SetPosition(CurrentTrackPosition);
    _audioPlayer.Play(NAudio.Wave.PlaybackState.Paused, CurrentVolume);
}

private bool CanTrackControlMouseDown(object p)
{
    if (_playbackState == PlaybackState.Playing)
    {
        return true;
    }
    return false;
}

private bool CanTrackControlMouseUp(object p)
{
    if (_playbackState == PlaybackState.Paused)
    {
        return true;
    }
    return false;
}

如果您想准确了解这些是如何实现的,您可以访问我的GitHub页面并查看完整的实现。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接