I am new to both NAudio and C# and I managed to create a simple MP3 player where you can choose a file and play it. It also has a play/pause button.
I would now like to add a seek bar but have no clue on how to do this. Also is it possible to have seekbar in waveform style?
The openButton click Handler
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;
}
Apart from the purely UI-based concerns, there are three basic things you need to be able to do:
Read song length.
Get playback position.
Set playback position.
Song length and current playback position are simple enough - they are available via the TotalTime
and CurrentTime
properties of the WaveStream
object, which means your audioFileReader
object supports them too. Once constructed, audioFileReader.TotalTime
will give you a TimeSpan
object with the total length of the file, and audioFileReader.CurrentTime
will give you the current playback position.
You can also set the playback position by assigning to audioFileReader.CurrentTime
... but doing so is a tricky process unless you know what you're doing. The following code to skip ahead 2.5 seconds works sometimes and crashes horribly at others:
audioFileReader.CurrentTime = audioFileReader.CurrentTime.Add(TimeSpan.FromSeconds(2.5));
The problem here is that the resultant stream position (in bytes) may not be correctly aligned to the start of a sample, for several reasons (including floating point math done in the background). This can quickly turn your output to garbage.
The better option is to use the Position
property of the stream when you want to change playback position. Position
is the currently playback position in bytes, so is a tiny bit harder to work on. Not too much though:
audioFileReader.Position += audioFileReader.WaveFormat.AverageBytesPerSecond;
If you're stepping forward or backwards an integer number of seconds, that's fine. If not, you need to make sure that you are always positioning at a sample boundary, using the WaveFormat.BlockAlign
property to figure out where those boundaries are.
// 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;
The simple thing to do here is define a set of extensions to the WaveStream
class that will handle block aligning during a seek operation. The basic align-to-block operation can be called by variations that just calculate the new position from whatever you put in, so something like this:
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));
}
}
With that in place, you can call audioFileReader.SetPosition(10.0)
to jump to playback position 00:00:10.0
, call audioFileReader.Seek(-5)
to jump back 5 seconds, etc. without worrying about seeking to a point half way through a sample.
So... add some buttons to your form and set them up to call the Seek
method with +/- values to move around. Then add a slider of some sort that you can use to display and set the playback position. Throw in a timer to update the slider position to the current playback position and you're about done.
There is a good answer but I want to add another way of building a seekbar in WPF, since I was also working on a similar project.
Here is the XAML code for the seeker:
<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>
CurrentTrackLenght
and CurrentTrackPosition
in our ViewModel are:
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));
}
}
The idea is really simple; once we start playing:
First we get the lenght of the audio file in seconds and assign it to the CurrentTrackLenght
property and it will be bound to seekbar's Maximum
property.
Then as we are playing the audio file, we continuously update the CurrentTrackPosition
property that is in turn driving the Value
property of our seekbar.
So when we press the "Play" button, following command in our ViewModel will run:
private void StartPlayback(object p)
{
if (_playbackState == PlaybackState.Stopped)
{
if (CurrentTrack != null)
{
_audioPlayer.LoadFile(CurrentTrack.Filepath, CurrentVolume);
CurrentTrackLenght = _audioPlayer.GetLenghtInSeconds();
}
}
_audioPlayer.TogglePlayPause(CurrentVolume);
}
_audioPlayer
is an abstraction I used to ease the Play/Pause/Stop, so you can replace those with your own code. But the important part is:
CurrentTrackLenght = _audioPlayer.GetLenghtInSeconds();
And the code for the GetLenghtInSeconds()
in AudioPlayer
is:
public double GetLenghtInSeconds()
{
if (_audioFileReader != null)
{
return _audioFileReader.TotalTime.TotalSeconds;
}
else
{
return 0;
}
}
So with this we initialize our seekbar's Maximum
value for each audio file we start playing.
Now, we need to update our seekbar as audio plays.
First we need to determine the current position of our audio file in seconds. I choose seconds here because our seekbar's Maximum
is in seconds as well so they will match correctly.
To do this we need the following method in AudioPlayer
:
public double GetPositionInSeconds()
{
if (_audioFileReader != null)
{
return _audioFileReader.CurrentTime.TotalSeconds;
}
else
{
return 0;
}
}
With this code done, we can move on to our ViewModel. First we need to set up a timer in our constructor.
var timer = new System.Timers.Timer();
timer.Interval = 300;
timer.Elapsed += Timer_Elapsed;
timer.Start();
And add Timer_Elapsed()
and UpdateSeekBar()
methods:
private void UpdateSeekBar()
{
if (_playbackState == PlaybackState.Playing)
{
CurrentTrackPosition = _audioPlayer.GetPositionInSeconds();
}
}
private void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
UpdateSeekBar();
}
With this done, now when we play an audio file our seekbar should move as expected.
Now for the actual seeking part, first we need a SetPosition()
method in our AudioPlayer
class.
public void SetPosition(double value)
{
if (_audioFileReader != null)
{
_audioFileReader.CurrentTime = TimeSpan.FromSeconds(value);
}
}
This code sets the current time to the value we pass therefore effectively seeking to the new position.
Finally we need 4 methods to finalize our ViewModel commands for PreviewMouseDown
and PreviewMouseUp
events.
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;
}
If you would like to see exactly how these are implemented, you can go to my github page and see the complete implementation.