public async Task LoadFileAsync(string fileName)
        {
            try { audioFile = new BufferedAudioFile(fileName); }
            catch (Exception e)
            {
                System.Windows.MessageBox.Show("Could not load music file: " + Path.GetFileName(fileName) + Environment.NewLine + Environment.NewLine + e.Message, "Problem opening file",
                                               System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Warning);
                return;
            }

            MusicFileName = fileName;

            // TODO progress inidicator for file loading
            audioFile.LoadIntoMemoryAsync(null).Forget();
            audioPlayback.Init(audioFile.CreateStream(infinite: true));

            // Initialize our user-facing value from the playback device (which apparently gets its value from Windows).
            MusicVolume = LoudnessHelper.VolumeFromLoudness(audioPlayback.Volume);

            CurrentWaveform = null;
            await RenderWaveformAsync(false);

            Notify(nameof(MusicDuration));
        }
        public PlaybackViewModel(SequencerViewModel sequencer)
        {
            this.sequencer     = sequencer;
            this.timelineModel = sequencer.GetModel();

            cursorUpdateTimer = new System.Windows.Threading.DispatcherTimer()
            {
                Interval = CURSOR_UPDATE_INTERVAL
            };
            cursorUpdateTimer.Tick += (_, __) => UpdateCursorPosition();

            ForwardPropertyEvents(nameof(MusicVolume), this, () => audioPlayback.Volume = LoudnessHelper.LoudnessFromVolume(MusicVolume));
            ForwardPropertyEvents(nameof(sequencer.CurrentViewLeftPositionTime), sequencer, InvalidateWaveform);
            // Note: Because CurrentViewLeftPositionTime depends on TimePixelScale and is always changed together with the right position,
            // it is enough to only depend on this one to reduce callback duplication.
            //ForwardPropertyEvents(nameof(sequencer.TimePixelScale), sequencer, InvalidateWaveform);
            //ForwardPropertyEvents(nameof(sequencer.CurrentViewRightPositionTime), sequencer, InvalidateWaveform);
            ForwardPropertyEvents(nameof(sequencer.CursorPosition), sequencer, OnCursorPositionChanged);
            audioPlayback.PlaybackStopped += OnPlaybackStopped;

            audioPlayback.Init(EmptySampleProvider.Singleton);
        }
        private void RenderPolygon(DrawingContext drawingContext)
        {
            var fillBrush = Brushes.LawnGreen;
            var borderPen = new Pen(Brushes.DarkGreen, 1.0);

            if (WaveformDisplayMode == WaveformDisplayMode.Linear)
            {
                sampleTransformerFunc = (x => x);
            }
            else if (WaveformDisplayMode == WaveformDisplayMode.Logarithmic)
            {
                sampleTransformerFunc = (x => Math.Sign(x) * LoudnessHelper.VolumeFromLoudness(Math.Abs(x)));
            }
            else
            {
                throw new InvalidOperationException("display mode not supported: " + WaveformDisplayMode);
            }

            var maximums = Waveform.Maximums;
            var minimums = Waveform.Minimums;

            if (maximums.Length == 0)
            {
                return;
            }
            //  px/sample = px/sec    *  sec/sample
            double xScale      = TimeScale * Waveform.TimePerSample;
            double offsetPx    = Waveform.TimeOffset * TimeScale;
            double rightmostPx = offsetPx + maximums.Length * xScale;

            StreamGeometry geometry = new StreamGeometry();

            using (var ctx = geometry.Open())
            {
                ctx.BeginFigure(new Point(offsetPx, SampleToYPosition(maximums[0])), isFilled: true, isClosed: true);

                for (int i = 1; i < maximums.Length; i++)
                {
                    ctx.LineTo(new Point(offsetPx + i * xScale, SampleToYPosition(maximums[i])), true, false);
                }

                ctx.LineTo(new Point(rightmostPx, SampleToYPosition(maximums[maximums.Length - 1])), true, false);
                ctx.LineTo(new Point(rightmostPx, SampleToYPosition(minimums[minimums.Length - 1])), true, false);

                for (int i = minimums.Length - 1; i >= 0; i--)
                {
                    ctx.LineTo(new Point(offsetPx + i * xScale, SampleToYPosition(minimums[i])), true, false);
                }
            }
            geometry.Freeze();

            drawingContext.DrawGeometry(fillBrush, borderPen, geometry);

            // Alternative rendering method that draws rectangles/lines directly onto the drawingContext.

            //for (int i=0; i<maximums.Length; i++)
            //{
            //    double y = SampleToYPosition(maximums[i]);
            //    double h = SampleToYPosition(minimums[i]) - y;
            //    if (h > halfHeight * 0.1 || i == 0)
            //    {
            //        drawingContext.DrawRectangle(fillBrush, null, new Rect(
            //            offsetPx + i * xScale, y,
            //            xScale, h));
            //    } else
            //    {
            //        double prevMid = (SampleToYPosition(maximums[i - 1]) + SampleToYPosition(minimums[i - 1])) / 2;
            //        drawingContext.DrawLine(new Pen(fillBrush, 3),
            //            new Point(offsetPx + (i-1) * xScale, prevMid),
            //            new Point(offsetPx + i * xScale, y + h/2));
            //    }
            //}
        }