Skip to content

Displays a stereo waveform and allows a user to change playback position.

Property ​

Name
LeftLevelBrush
RightLevelBrush
ProgressBarBrush
ProgressBarThickness
CenterLineBrush
CenterLineThickness
RepeatRegionBrush
AllowRepeatRegions
TimelineTickBrush

Methods ​

Name
RegisterSoundPlayer

Virtual Methods (override) ​

Name
OnLeftLevelBrushChanged
OnRightLevelBrushChanged
OnProgressBarBrushChanged
OnProgressBarThicknessChanged
OnCenterLineBrushChanged
OnCenterLineThicknessChanged
OnRepeatRegionBrushChanged
OnAllowRepeatRegionsChanged
OnTimelineTickBrushChanged

Example ​

xml
<dev:WaveformTimeline x:Name="WaveformTimelineSample" />

WARNING

it is best to use NAudio library to generate data.

AudioGraphEngine ​

cs
soundEngine = new AudioGraphEngine();
WaveformTimelineSample.RegisterSoundPlayer(soundEngine);
await soundEngine.OpenFileAsync(@"C:\myAudio.mp3");
soundEngine.Play();

WARNING

GenerateWaveformAsync only support simple Wav format.

cs
[GeneratedComInterface]
[Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
public unsafe partial interface IMemoryBufferByteAccess
{
    void GetBuffer(out byte* buffer, out uint capacity);
}

public sealed partial class AudioGraphEngine : ISpectrumPlayer, IWaveformPlayer, INotifyPropertyChanged, IDisposable
{
    private readonly DispatcherQueue dispatcherQueue = DispatcherQueue.GetForCurrentThread();

    private AudioGraph graph;
    private AudioFileInputNode fileNode;
    private AudioDeviceOutputNode deviceNode;
    private AudioFrameOutputNode frameNode;

    private readonly int fftDataSize = 2048;
    private readonly SampleAggregator sampleAggregator;

    private bool isPlaying;
    private double channelLength;
    private double channelPosition;

    public event PropertyChangedEventHandler PropertyChanged;
    private AudioSampleRingBuffer sampleBuffer;
    private CancellationTokenSource processingCts;

    private TimeSpan playbackStartTime;
    public TimeSpan SelectionBegin { get; set; }
    public TimeSpan SelectionEnd { get; set; }
    public AudioGraphEngine()
    {
        sampleAggregator = new SampleAggregator(fftDataSize);

        sampleBuffer = new AudioSampleRingBuffer(fftDataSize * 8);
        processingCts = new CancellationTokenSource();
        StartProcessingLoop();
    }
    private void StartProcessingLoop()
    {
        Task.Run(async () =>
        {
            float left, right;

            while (!processingCts.IsCancellationRequested)
            {
                if (sampleBuffer.TryRead(out left) &&
                    sampleBuffer.TryRead(out right))
                {
                    sampleAggregator.Add(left, right);
                }
                else
                {
                    await Task.Delay(1);
                }
            }
        }, processingCts.Token);
    }

    public bool GetFFTData(float[] fftDataBuffer)
    {
        sampleAggregator.GetFFTResults(fftDataBuffer);
        return IsPlaying;
    }

    public int GetFFTFrequencyIndex(int frequency)
    {
        double maxFrequency = graph != null
            ? graph.EncodingProperties.SampleRate / 2.0
            : 22050;

        return (int)((frequency / maxFrequency) * (fftDataSize / 2));
    }

    public double ChannelPosition
    {
        get => channelPosition;
        set
        {
            if (fileNode == null)
                return;

            value = Math.Max(0, Math.Min(value, ChannelLength));
            fileNode.Seek(TimeSpan.FromSeconds(value));
            channelPosition = value;
            NotifyPropertyChanged(nameof(ChannelPosition));
        }
    }

    public double ChannelLength
    {
        get => channelLength;
        private set
        {
            channelLength = value;
            NotifyPropertyChanged(nameof(ChannelLength));
        }
    }

    private float[] waveformData;

    public float[] WaveformData
    {
        get => waveformData;
        private set
        {
            waveformData = value;
            NotifyPropertyChanged(nameof(WaveformData));
        }
    }
    private async Task GenerateWaveformAsync(string wavPath)
    {
        await Task.Run(() =>
        {
            byte[] data = File.ReadAllBytes(wavPath);

            if (data.Length < 44)
                throw new InvalidOperationException("Invalid WAV file");

            int channels = BitConverter.ToInt16(data, 22);
            int bitsPerSample = BitConverter.ToInt16(data, 34);

            int dataChunkOffset = -1;
            int dataChunkSize = 0;

            for (int i = 12; i < data.Length - 8;)
            {
                string chunkId = Encoding.ASCII.GetString(data, i, 4);
                int chunkSize = BitConverter.ToInt32(data, i + 4);

                if (chunkId == "data")
                {
                    dataChunkOffset = i + 8;
                    dataChunkSize = chunkSize;
                    break;
                }

                i += 8 + chunkSize;
            }

            if (dataChunkOffset < 0)
                throw new InvalidOperationException("WAV data chunk not found");

            const int samplesPerBucket = 1024;

            List<float> waveform = new();

            float leftSumSq = 0, rightSumSq = 0;
            float leftPeak = 0, rightPeak = 0;
            int sampleCounter = 0;

            int bytesPerSample = bitsPerSample / 8;
            int frameSize = bytesPerSample * channels;

            for (int i = dataChunkOffset; i + frameSize <= dataChunkOffset + dataChunkSize; i += frameSize)
            {
                float l, r;

                if (bitsPerSample == 16)
                {
                    l = BitConverter.ToInt16(data, i) / 32768f;
                    r = channels == 2 ? BitConverter.ToInt16(data, i + 2) / 32768f : l;
                }
                else if (bitsPerSample == 24)
                {
                    int left = (data[i + 2] << 24) | (data[i + 1] << 16) | (data[i] << 8);
                    left >>= 8;
                    l = left / 8388608f;

                    if (channels == 2)
                    {
                        int right = (data[i + 5] << 24) | (data[i + 4] << 16) | (data[i + 3] << 8);
                        right >>= 8;
                        r = right / 8388608f;
                    }
                    else
                        r = l;
                }
                else if (bitsPerSample == 32)
                {
                    l = BitConverter.ToSingle(data, i);
                    r = channels == 2 ? BitConverter.ToSingle(data, i + 4) : l;
                }
                else
                {
                    throw new NotSupportedException($"Unsupported WAV bit depth: {bitsPerSample}");
                }

                leftPeak = Math.Max(leftPeak, Math.Abs(l));
                rightPeak = Math.Max(rightPeak, Math.Abs(r));

                leftSumSq += l * l;
                rightSumSq += r * r;
                sampleCounter++;

                if (sampleCounter >= samplesPerBucket)
                {
                    float leftRms = MathF.Sqrt(leftSumSq / sampleCounter);
                    float rightRms = MathF.Sqrt(rightSumSq / sampleCounter);

                    waveform.Add((leftRms + leftPeak) * 0.5f);
                    waveform.Add((rightRms + rightPeak) * 0.5f);

                    leftSumSq = rightSumSq = 0;
                    leftPeak = rightPeak = 0;
                    sampleCounter = 0;
                }
            }

            float max = 0f;
            for (int i = 0; i < waveform.Count; i++)
            {
                float v = Math.Abs(waveform[i]);
                if (v > max)
                    max = v;
            }

            if (max > 0)
            {
                float gain = 1f / max;
                for (int i = 0; i < waveform.Count; i++)
                    waveform[i] *= gain;
            }

            dispatcherQueue.TryEnqueue(() =>
            {
                WaveformData = waveform.ToArray();
            });
        });
    }

    public bool IsPlaying
    {
        get => isPlaying;
        private set
        {
            isPlaying = value;
            NotifyPropertyChanged(nameof(IsPlaying));
        }
    }

    public async Task OpenFile(string path)
    {
        DisposeGraph();

        var file = await StorageFile.GetFileFromPathAsync(path);

        await GenerateWaveformAsync(path);

        var settings = new AudioGraphSettings(AudioRenderCategory.Media)
        {
            QuantumSizeSelectionMode = QuantumSizeSelectionMode.ClosestToDesired,
            DesiredSamplesPerQuantum = fftDataSize
        };

        var graphResult = await AudioGraph.CreateAsync(settings);
        if (graphResult.Status != AudioGraphCreationStatus.Success)
            throw new InvalidOperationException("AudioGraph creation failed");

        graph = graphResult.Graph;

        var deviceResult = await graph.CreateDeviceOutputNodeAsync();
        deviceNode = deviceResult.DeviceOutputNode;

        var fileResult = await graph.CreateFileInputNodeAsync(file);
        fileNode = fileResult.FileInputNode;

        frameNode = graph.CreateFrameOutputNode();

        fileNode.AddOutgoingConnection(deviceNode);
        fileNode.AddOutgoingConnection(frameNode);

        ChannelLength = fileNode.Duration.TotalSeconds;

        graph.QuantumStarted += OnQuantumStarted;
    }

    public void Play()
    {
        if (graph == null)
            return;

        playbackStartTime = fileNode.Position;
        graph.Start();
        IsPlaying = true;
    }


    public void Pause()
    {
        if (graph == null)
            return;

        graph.Stop();
        IsPlaying = false;
    }

    public void Stop()
    {
        if (graph == null)
            return;

        graph.Stop();
        fileNode.Seek(TimeSpan.Zero);
        ChannelPosition = 0;
        IsPlaying = false;
    }

    private unsafe void OnQuantumStarted(AudioGraph sender, object args)
    {
        var frame = frameNode.GetFrame();
        using var buffer = frame.LockBuffer(AudioBufferAccessMode.Read);
        using var reference = buffer.CreateReference();

        ((IMemoryBufferByteAccess)reference)
            .GetBuffer(out byte* data, out uint capacity);

        float* samples = (float*)data;
        int count = (int)(capacity / sizeof(float));

        for (int i = 0; i < count; i++)
            sampleBuffer.Write(samples[i]);

        double positionSeconds = 0;
        if (fileNode != null && IsPlaying)
            positionSeconds = fileNode.Position.TotalSeconds;

        dispatcherQueue.TryEnqueue(() =>
        {
            channelPosition = positionSeconds;
            NotifyPropertyChanged(nameof(ChannelPosition));
        });
    }

    public void Dispose()
    {
        DisposeGraph();
        GC.SuppressFinalize(this);
    }

    private void DisposeGraph()
    {
        if (graph != null)
        {
            graph.QuantumStarted -= OnQuantumStarted;
            graph.Stop();
            graph.Dispose();
            graph = null;
        }

        fileNode = null;
        deviceNode = null;
        frameNode = null;
    }
    private void NotifyPropertyChanged(string name)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

public sealed partial class AudioSampleRingBuffer
{
    private readonly float[] buffer;
    private int writeIndex;
    private int readIndex;

    public AudioSampleRingBuffer(int capacity)
    {
        buffer = new float[capacity];
    }

    public void Write(float value)
    {
        buffer[writeIndex] = value;
        writeIndex = (writeIndex + 1) % buffer.Length;
    }

    public bool TryRead(out float value)
    {
        if (readIndex == writeIndex)
        {
            value = 0;
            return false;
        }

        value = buffer[readIndex];
        readIndex = (readIndex + 1) % buffer.Length;
        return true;
    }
}

public static partial class FastFourierTransform
{
    /// <summary>
    /// In-place radix-2 FFT (forward).
    /// </summary>
    public static void FFT(Complex[] data, int exponent)
    {
        int n = 1 << exponent;

        // Bit-reversal
        int j = 0;
        for (int i = 0; i < n; i++)
        {
            if (i < j)
            {
                var temp = data[i];
                data[i] = data[j];
                data[j] = temp;
            }

            int m = n >> 1;
            while (j >= m && m >= 2)
            {
                j -= m;
                m >>= 1;
            }
            j += m;
        }

        // FFT stages
        for (int stage = 1; stage <= exponent; stage++)
        {
            int step = 1 << stage;
            int halfStep = step >> 1;

            double angleStep = -2.0 * Math.PI / step;
            Complex phaseStep = new Complex(
                Math.Cos(angleStep),
                Math.Sin(angleStep));

            for (int k = 0; k < n; k += step)
            {
                Complex phase = Complex.One;

                for (int i = 0; i < halfStep; i++)
                {
                    int evenIndex = k + i;
                    int oddIndex = evenIndex + halfStep;

                    Complex even = data[evenIndex];
                    Complex odd = phase * data[oddIndex];

                    data[evenIndex] = even + odd;
                    data[oddIndex] = even - odd;

                    phase *= phaseStep;
                }
            }
        }
    }
}

public sealed partial class SampleAggregator
{
    private readonly int bufferSize;
    private readonly int fftExponent;

    private readonly Complex[] channelData;
    private int channelDataPosition;

    private float volumeLeftMaxValue;
    private float volumeLeftMinValue;
    private float volumeRightMaxValue;
    private float volumeRightMinValue;

    public SampleAggregator(int bufferSize)
    {
        if ((bufferSize & (bufferSize - 1)) != 0)
            throw new ArgumentException("FFT buffer size must be a power of two");

        this.bufferSize = bufferSize;
        fftExponent = (int)Math.Log(bufferSize, 2);
        channelData = new Complex[bufferSize];

        Clear();
    }

    public void Clear()
    {
        volumeLeftMaxValue = float.MinValue;
        volumeRightMaxValue = float.MinValue;
        volumeLeftMinValue = float.MaxValue;
        volumeRightMinValue = float.MaxValue;
        channelDataPosition = 0;
    }

    /// <summary>
    /// Adds a stereo sample.
    /// </summary>
    public void Add(float leftValue, float rightValue)
    {
        if (channelDataPosition == 0)
        {
            volumeLeftMaxValue = float.MinValue;
            volumeRightMaxValue = float.MinValue;
            volumeLeftMinValue = float.MaxValue;
            volumeRightMinValue = float.MaxValue;
        }

        // Convert stereo → mono (average)
        float mono = (leftValue + rightValue) * 0.5f;

        channelData[channelDataPosition] = new Complex(mono, 0);
        channelDataPosition++;

        volumeLeftMaxValue = Math.Max(volumeLeftMaxValue, leftValue);
        volumeLeftMinValue = Math.Min(volumeLeftMinValue, leftValue);
        volumeRightMaxValue = Math.Max(volumeRightMaxValue, rightValue);
        volumeRightMinValue = Math.Min(volumeRightMinValue, rightValue);

        if (channelDataPosition >= bufferSize)
            channelDataPosition = 0;
    }

    /// <summary>
    /// Copies FFT magnitude data into the provided buffer.
    /// Buffer length must be >= bufferSize / 2.
    /// </summary>
    public void GetFFTResults(float[] fftBuffer)
    {
        if (fftBuffer == null)
            throw new ArgumentNullException(nameof(fftBuffer));

        if (fftBuffer.Length < bufferSize / 2)
            throw new ArgumentException("FFT buffer is too small");

        // Clone so capture thread is not disturbed
        var fftData = new Complex[bufferSize];
        Array.Copy(channelData, fftData, bufferSize);

        FastFourierTransform.FFT(fftData, fftExponent);

        for (int i = 0; i < bufferSize / 2; i++)
        {
            double real = fftData[i].Real;
            double imag = fftData[i].Imaginary;

            fftBuffer[i] = (float)Math.Sqrt(real * real + imag * imag);
        }
    }

    public float LeftMaxVolume => volumeLeftMaxValue;
    public float LeftMinVolume => volumeLeftMinValue;
    public float RightMaxVolume => volumeRightMaxValue;
    public float RightMinVolume => volumeRightMinValue;
}

NAudioEngine ​

cs
soundEngine = NAudioEngine.Instance;
WaveformTimelineSample.RegisterSoundPlayer(soundEngine);
soundEngine.OpenFile(@"C:\myAudio.mp3");
soundEngine.Play();
cs
public partial class NAudioEngine : INotifyPropertyChanged, ISpectrumPlayer, IWaveformPlayer, IDisposable
{
    private readonly DispatcherQueue dispatcherQueue = DispatcherQueue.GetForCurrentThread();
    private static NAudioEngine instance;
    private readonly DispatcherTimer positionTimer = new DispatcherTimer();
    private CancellationTokenSource waveformCts;
    private Task waveformTask;
    private readonly int fftDataSize = (int)FFTDataSize.FFT2048;
    private bool disposed;
    private bool canPlay;
    private bool canPause;
    private bool canStop;
    private bool isPlaying;
    private bool inChannelTimerUpdate;
    private double channelLength;
    private double channelPosition;
    private bool inChannelSet;
    private WaveOut waveOutDevice;
    private WaveStream activeStream;
    private WaveChannel32 inputStream;
    private SampleAggregator sampleAggregator;
    private SampleAggregator waveformAggregator;
    private string pendingWaveformPath;
    private float[] fullLevelData;
    private float[] waveformData;
    private TimeSpan repeatStart;
    private TimeSpan repeatStop;
    private bool inRepeatSet;

    private const int waveformCompressedPointCount = 2000;
    private const int repeatThreshold = 200;

    public static NAudioEngine Instance
    {
        get
        {
            if (instance == null)
                instance = new NAudioEngine();
            return instance;
        }
    }
    private NAudioEngine()
    {
        positionTimer.Interval = TimeSpan.FromMilliseconds(50);
        positionTimer.Tick += positionTimer_Tick;
    }
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                StopAndCloseStream();
            }

            disposed = true;
        }
    }
    public bool GetFFTData(float[] fftDataBuffer)
    {
        sampleAggregator.GetFFTResults(fftDataBuffer);
        return isPlaying;
    }

    public int GetFFTFrequencyIndex(int frequency)
    {
        double maxFrequency;
        if (ActiveStream != null)
            maxFrequency = ActiveStream.WaveFormat.SampleRate / 2.0d;
        else
            maxFrequency = 22050; // Assume a default 44.1 kHz sample rate.
        return (int)((frequency / maxFrequency) * (fftDataSize / 2));
    }
    public TimeSpan SelectionBegin
    {
        get { return repeatStart; }
        set
        {
            if (!inRepeatSet)
            {
                inRepeatSet = true;
                TimeSpan oldValue = repeatStart;
                repeatStart = value;
                if (oldValue != repeatStart)
                    NotifyPropertyChanged("SelectionBegin");
                inRepeatSet = false;
            }
        }
    }

    public TimeSpan SelectionEnd
    {
        get { return repeatStop; }
        set
        {
            if (!inChannelSet)
            {
                inRepeatSet = true;
                TimeSpan oldValue = repeatStop;
                repeatStop = value;
                if (oldValue != repeatStop)
                    NotifyPropertyChanged("SelectionEnd");
                inRepeatSet = false;
            }
        }
    }

    public float[] WaveformData
    {
        get { return waveformData; }
        protected set
        {
            float[] oldValue = waveformData;
            waveformData = value;
            if (oldValue != waveformData)
                NotifyPropertyChanged("WaveformData");
        }
    }

    public double ChannelLength
    {
        get { return channelLength; }
        protected set
        {
            double oldValue = channelLength;
            channelLength = value;
            if (oldValue != channelLength)
                NotifyPropertyChanged("ChannelLength");
        }
    }

    public double ChannelPosition
    {
        get { return channelPosition; }
        set
        {
            if (!inChannelSet)
            {
                inChannelSet = true; // Avoid recursion
                double oldValue = channelPosition;
                double position = Math.Max(0, Math.Min(value, ChannelLength));
                if (!inChannelTimerUpdate && ActiveStream != null)
                    ActiveStream.Position = (long)((position / ActiveStream.TotalTime.TotalSeconds) * ActiveStream.Length);
                channelPosition = position;
                if (oldValue != channelPosition)
                    NotifyPropertyChanged("ChannelPosition");
                inChannelSet = false;
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyPropertyChanged(String info)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }
    private partial class WaveformGenerationParams
    {
        public WaveformGenerationParams(int points, string path)
        {
            Points = points;
            Path = path;
        }

        public int Points { get; protected set; }
        public string Path { get; protected set; }
    }

    private void GenerateWaveformData(string path)
    {
        // If a task is running, cancel it and queue the new request
        if (waveformTask != null && !waveformTask.IsCompleted)
        {
            pendingWaveformPath = path;
            waveformCts.Cancel();
            return;
        }

        if (waveformCompressedPointCount != 0)
        {
            waveformCts = new CancellationTokenSource();
            waveformTask = GenerateWaveformDataAsync(
                new WaveformGenerationParams(waveformCompressedPointCount, path),
                waveformCts.Token);
        }
    }

    private async Task GenerateWaveformDataAsync(
        WaveformGenerationParams waveformParams,
        CancellationToken token)
    {
        try
        {
            await Task.Run(() =>
            {
                using var waveformMp3Stream = new Mp3FileReader(waveformParams.Path);
                using var waveformInputStream = new WaveChannel32(waveformMp3Stream);

                waveformInputStream.Sample += waveStream_Sample;

                int frameLength = fftDataSize;
                int frameCount = (int)((double)waveformInputStream.Length / frameLength);
                int waveformLength = frameCount * 2;

                byte[] readBuffer = new byte[frameLength];
                waveformAggregator = new SampleAggregator(frameLength);

                float maxLeftPointLevel = float.MinValue;
                float maxRightPointLevel = float.MinValue;

                int currentPointIndex = 0;
                int readCount = 0;

                float[] waveformCompressedPoints = new float[waveformParams.Points];
                List<float> waveformData = new();
                List<int> waveMaxPointIndexes = new();

                for (int i = 1; i <= waveformParams.Points; i++)
                {
                    waveMaxPointIndexes.Add(
                        (int)Math.Round(
                            waveformLength * ((double)i / waveformParams.Points), 0));
                }

                while (currentPointIndex * 2 < waveformParams.Points)
                {
                    token.ThrowIfCancellationRequested();

                    waveformInputStream.ReadExactly(readBuffer);

                    waveformData.Add(waveformAggregator.LeftMaxVolume);
                    waveformData.Add(waveformAggregator.RightMaxVolume);

                    maxLeftPointLevel = Math.Max(maxLeftPointLevel, waveformAggregator.LeftMaxVolume);
                    maxRightPointLevel = Math.Max(maxRightPointLevel, waveformAggregator.RightMaxVolume);

                    if (readCount > waveMaxPointIndexes[currentPointIndex])
                    {
                        waveformCompressedPoints[currentPointIndex * 2] = maxLeftPointLevel;
                        waveformCompressedPoints[currentPointIndex * 2 + 1] = maxRightPointLevel;

                        maxLeftPointLevel = float.MinValue;
                        maxRightPointLevel = float.MinValue;
                        currentPointIndex++;
                    }

                    if (readCount % 3000 == 0)
                    {
                        var cloned = (float[])waveformCompressedPoints.Clone();
                        dispatcherQueue.TryEnqueue(() =>
                        {
                            WaveformData = cloned;
                        });
                    }

                    readCount++;
                }

                var finalClone = (float[])waveformCompressedPoints.Clone();
                dispatcherQueue.TryEnqueue(() =>
                {
                    fullLevelData = waveformData.ToArray();
                    WaveformData = finalClone;
                });
            }, token);
        }
        catch (OperationCanceledException)
        {
            // Expected on cancellation
        }
        finally
        {
            // If cancellation occurred and a new path is pending, restart
            if (!string.IsNullOrEmpty(pendingWaveformPath) &&
                waveformCompressedPointCount != 0)
            {
                string nextPath = pendingWaveformPath;
                pendingWaveformPath = null;

                waveformCts = new CancellationTokenSource();
                waveformTask = GenerateWaveformDataAsync(
                    new WaveformGenerationParams(waveformCompressedPointCount, nextPath),
                    waveformCts.Token);
            }
        }
    }
    private void StopAndCloseStream()
    {
        if (waveOutDevice != null)
        {
            waveOutDevice.Stop();
        }
        if (activeStream != null)
        {
            inputStream.Close();
            inputStream = null;
            ActiveStream.Close();
            ActiveStream = null;
        }
        if (waveOutDevice != null)
        {
            waveOutDevice.Dispose();
            waveOutDevice = null;
        }
    }
    public void Stop()
    {
        if (waveOutDevice != null)
        {
            waveOutDevice.Stop();
        }
        IsPlaying = false;
        CanStop = false;
        CanPlay = true;
        CanPause = false;
    }

    public void Pause()
    {
        if (IsPlaying && CanPause)
        {
            waveOutDevice.Pause();
            IsPlaying = false;
            CanPlay = true;
            CanPause = false;
        }
    }

    public void Play()
    {
        if (CanPlay)
        {
            waveOutDevice.Play();
            IsPlaying = true;
            CanPause = true;
            CanPlay = false;
            CanStop = true;
        }
    }

    public void OpenFile(string path)
    {
        Stop();

        if (ActiveStream != null)
        {
            SelectionBegin = TimeSpan.Zero;
            SelectionEnd = TimeSpan.Zero;
            ChannelPosition = 0;
        }

        StopAndCloseStream();

        if (System.IO.File.Exists(path))
        {
            try
            {
                waveOutDevice = new WaveOut()
                {
                    DesiredLatency = 100
                };
                ActiveStream = new Mp3FileReader(path);
                inputStream = new WaveChannel32(ActiveStream);
                sampleAggregator = new SampleAggregator(fftDataSize);
                inputStream.Sample += inputStream_Sample;
                waveOutDevice.Init(inputStream);
                ChannelLength = inputStream.TotalTime.TotalSeconds;
                GenerateWaveformData(path);
                CanPlay = true;
            }
            catch
            {
                ActiveStream = null;
                CanPlay = false;
            }
        }
    }
    public WaveStream ActiveStream
    {
        get { return activeStream; }
        protected set
        {
            WaveStream oldValue = activeStream;
            activeStream = value;
            if (oldValue != activeStream)
                NotifyPropertyChanged("ActiveStream");
        }
    }

    public bool CanPlay
    {
        get { return canPlay; }
        protected set
        {
            bool oldValue = canPlay;
            canPlay = value;
            if (oldValue != canPlay)
                NotifyPropertyChanged("CanPlay");
        }
    }

    public bool CanPause
    {
        get { return canPause; }
        protected set
        {
            bool oldValue = canPause;
            canPause = value;
            if (oldValue != canPause)
                NotifyPropertyChanged("CanPause");
        }
    }

    public bool CanStop
    {
        get { return canStop; }
        protected set
        {
            bool oldValue = canStop;
            canStop = value;
            if (oldValue != canStop)
                NotifyPropertyChanged("CanStop");
        }
    }


    public bool IsPlaying
    {
        get { return isPlaying; }
        protected set
        {
            bool oldValue = isPlaying;
            isPlaying = value;
            if (oldValue != isPlaying)
                NotifyPropertyChanged("IsPlaying");

            if (isPlaying)
                positionTimer.Start();
            else
                positionTimer.Stop();
        }
    }
    private void inputStream_Sample(object sender, SampleEventArgs e)
    {
        sampleAggregator.Add(e.Left, e.Right);
        long repeatStartPosition = (long)((SelectionBegin.TotalSeconds / ActiveStream.TotalTime.TotalSeconds) * ActiveStream.Length);
        long repeatStopPosition = (long)((SelectionEnd.TotalSeconds / ActiveStream.TotalTime.TotalSeconds) * ActiveStream.Length);
        if (((SelectionEnd - SelectionBegin) >= TimeSpan.FromMilliseconds(repeatThreshold)) && ActiveStream.Position >= repeatStopPosition)
        {
            sampleAggregator.Clear();
            ActiveStream.Position = repeatStartPosition;
        }
    }

    void waveStream_Sample(object sender, SampleEventArgs e)
    {
        waveformAggregator.Add(e.Left, e.Right);
    }

    void positionTimer_Tick(object sender, object e)
    {
        inChannelTimerUpdate = true;
        ChannelPosition = ((double)ActiveStream.Position / (double)ActiveStream.Length) * ActiveStream.TotalTime.TotalSeconds;
        inChannelTimerUpdate = false;
    }
}

public partial class SampleAggregator
{
    private float volumeLeftMaxValue;
    private float volumeLeftMinValue;
    private float volumeRightMaxValue;
    private float volumeRightMinValue;
    private Complex[] channelData;
    private int bufferSize;
    private int binaryExponentitation;
    private int channelDataPosition;

    public SampleAggregator(int bufferSize)
    {
        this.bufferSize = bufferSize;
        binaryExponentitation = (int)Math.Log(bufferSize, 2);
        channelData = new Complex[bufferSize];
    }

    public void Clear()
    {
        volumeLeftMaxValue = float.MinValue;
        volumeRightMaxValue = float.MinValue;
        volumeLeftMinValue = float.MaxValue;
        volumeRightMinValue = float.MaxValue;
        channelDataPosition = 0;
    }

    /// <summary>
    /// Add a sample value to the aggregator.
    /// </summary>
    /// <param name="value">The value of the sample.</param>
    public void Add(float leftValue, float rightValue)
    {
        if (channelDataPosition == 0)
        {
            volumeLeftMaxValue = float.MinValue;
            volumeRightMaxValue = float.MinValue;
            volumeLeftMinValue = float.MaxValue;
            volumeRightMinValue = float.MaxValue;
        }

        // Make stored channel data stereo by averaging left and right values.
        channelData[channelDataPosition].X = (leftValue + rightValue) / 2.0f;
        channelData[channelDataPosition].Y = 0;
        channelDataPosition++;

        volumeLeftMaxValue = Math.Max(volumeLeftMaxValue, leftValue);
        volumeLeftMinValue = Math.Min(volumeLeftMinValue, leftValue);
        volumeRightMaxValue = Math.Max(volumeRightMaxValue, rightValue);
        volumeRightMinValue = Math.Min(volumeRightMinValue, rightValue);

        if (channelDataPosition >= channelData.Length)
        {
            channelDataPosition = 0;
        }
    }

    /// <summary>
    /// Performs an FFT calculation on the channel data upon request.
    /// </summary>
    /// <param name="fftBuffer">A buffer where the FFT data will be stored.</param>
    public void GetFFTResults(float[] fftBuffer)
    {
        Complex[] channelDataClone = new Complex[bufferSize];
        channelData.CopyTo(channelDataClone, 0);
        FastFourierTransform.FFT(true, binaryExponentitation, channelDataClone);
        for (int i = 0; i < channelDataClone.Length / 2; i++)
        {
            // Calculate actual intensities for the FFT results.
            fftBuffer[i] = (float)Math.Sqrt(channelDataClone[i].X * channelDataClone[i].X + channelDataClone[i].Y * channelDataClone[i].Y);
        }
    }

    public float LeftMaxVolume
    {
        get { return volumeLeftMaxValue; }
    }

    public float LeftMinVolume
    {
        get { return volumeLeftMinValue; }
    }

    public float RightMaxVolume
    {
        get { return volumeRightMaxValue; }
    }

    public float RightMinVolume
    {
        get { return volumeRightMinValue; }
    }
}

WaveformTimeline

Demo ​

you can run demo and see this feature.

Released under the MIT License.