diff --git a/QuickLook.Plugin/QuickLook.Plugin.VideoViewer/AudioTrack/MidiPlayer.cs b/QuickLook.Plugin/QuickLook.Plugin.VideoViewer/AudioTrack/MidiPlayer.cs index a93ffd02..21708ed2 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.VideoViewer/AudioTrack/MidiPlayer.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.VideoViewer/AudioTrack/MidiPlayer.cs @@ -1,19 +1,41 @@ -using Melanchall.DryWetMidi.Core; +// Copyright © 2024 QL-Win Contributors +// +// This file is part of QuickLook program. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using Melanchall.DryWetMidi.Core; using Melanchall.DryWetMidi.Interaction; using Melanchall.DryWetMidi.Multimedia; +using QuickLook.Common.Annotations; using QuickLook.Common.Helpers; using QuickLook.Common.Plugin; using System; +using System.ComponentModel; using System.Globalization; using System.IO; using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Input; namespace QuickLook.Plugin.VideoViewer.AudioTrack; -internal class MidiPlayer : IDisposable +internal class MidiPlayer : IDisposable, INotifyPropertyChanged { private ViewerPanel _vp; private ContextObject _context; @@ -21,7 +43,22 @@ internal class MidiPlayer : IDisposable private OutputDevice _outputDevice; private Playback _playback; private TimeSpan _duration; - private MethodInfo _setShouldLoop; // _vp.set_ShouldLoop() + private MethodInfo _setShouldLoop; // Reflection to invoke `_vp.set_ShouldLoop()` + + private long _currentTicks = 0L; + + public long CurrentTicks + { + get => _currentTicks; + private set + { + if (value == _currentTicks) return; + _currentTicks = value; + OnPropertyChanged(); + } + } + + public event PropertyChangedEventHandler PropertyChanged; public MidiPlayer(ViewerPanel panle, ContextObject context) { @@ -33,12 +70,14 @@ public void Dispose() { _vp = null; _context = null; - _outputDevice?.Dispose(); _playback?.Stop(); _playback?.Dispose(); + _playback = null; + _outputDevice?.Dispose(); + _outputDevice = null; } - public void LoadAndPlay(string path, MediaInfo.MediaInfo info) + public void LoadAndPlay(string path, MediaInfoLib info) { _midiFile = MidiFile.Read(path); _vp.metaTitle.Text = Path.GetFileName(path); @@ -57,8 +96,16 @@ public void LoadAndPlay(string path, MediaInfo.MediaInfo info) { timeText.Text = "00:00"; _vp.metaLength.Text = durationString; - _vp.sliderProgress.Maximum = _duration.Ticks; // Unbinding + _vp.sliderProgress.IsSelectionRangeEnabled = false; + _vp.sliderProgress.SelectionEnd = 0L; // Unbinding _vp.sliderProgress.Value = 0L; // Unbinding + _vp.sliderProgress.Maximum = _duration.Ticks; // Unbinding + _vp.sliderProgress.SetBinding(Slider.ValueProperty, new Binding(nameof(CurrentTicks)) // Rebinding + { + Source = this, + Mode = BindingMode.TwoWay, + UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged, + }); } } @@ -67,12 +114,12 @@ public void LoadAndPlay(string path, MediaInfo.MediaInfo info) if (_playback.IsRunning) { _playback.Stop(); - _vp.buttonPlayPause.Content = "\xE768"; + _vp.buttonPlayPause.Content = FontSymbols.Play; } else { _playback.Start(); - _vp.buttonPlayPause.Content = "\xE769"; + _vp.buttonPlayPause.Content = FontSymbols.Pause; } }; @@ -88,22 +135,9 @@ public void LoadAndPlay(string path, MediaInfo.MediaInfo info) _playback.Loop = _vp.ShouldLoop; }; - //_vp.sliderProgress.ValueChanged += (_, _) => - //{ - // if (!_isValueHandling) - // { - // _playback?.Stop(); - - // double seekPercent = _vp.sliderProgress.Value / _duration.Ticks; - // long moveTime = (long)(_duration.Ticks * seekPercent); - // TimeSpan timeSpan = TimeSpan.FromTicks(moveTime); - // _playback?.MoveToTime(new MetricTimeSpan(timeSpan)); - - // _playback.Start(); - // } - //}; - - _vp.sliderProgress.PreviewMouseDown += (_, e) => + // Event Slider.PreviewMouseDown will be prevented from being handled by the slider itself + // So we should add a handler by ourself + _vp.sliderProgress.AddHandler(UIElement.PreviewMouseDownEvent, new MouseButtonEventHandler((_, e) => { _playback?.Stop(); @@ -112,15 +146,13 @@ public void LoadAndPlay(string path, MediaInfo.MediaInfo info) double seekPercent = newValue / _duration.Ticks; long moveTime = (long)(_duration.Ticks * seekPercent); TimeSpan timeSpan = TimeSpan.FromTicks(moveTime); + _playback?.MoveToTime(new MetricTimeSpan(timeSpan)); + CurrentTicks = timeSpan.Ticks; - _playback.Start(); - }; - _vp.sliderProgress.IsSelectionRangeEnabled = false; - _vp.sliderProgress.PreviewMouseUp += (_, _) => - { - //_playback.Start(); - }; + _playback?.Start(); + _vp.buttonPlayPause.Content = FontSymbols.Pause; + }), true); // Disable unsupported functionality { @@ -140,7 +172,7 @@ public void LoadAndPlay(string path, MediaInfo.MediaInfo info) var current = TimeSpan.FromMilliseconds(metricTimeSpan.TotalMilliseconds); var currentString = new TimeTickToShortStringConverter().Convert(current.Ticks, typeof(string), null, CultureInfo.InvariantCulture).ToString(); - _vp.sliderProgress.Value = current.Ticks; + CurrentTicks = current.Ticks; if (_vp?.buttonTime?.Content is TextBlock timeText) { @@ -155,7 +187,7 @@ public void LoadAndPlay(string path, MediaInfo.MediaInfo info) var current = TimeSpan.FromMilliseconds(metricTimeSpan.TotalMilliseconds); var subtractString = new TimeTickToShortStringConverter().Convert(_duration.Ticks - current.Ticks, typeof(string), null, CultureInfo.InvariantCulture).ToString(); - _vp.sliderProgress.Value = current.Ticks; + CurrentTicks = current.Ticks; if (_vp?.buttonTime?.Content is TextBlock timeText) { @@ -172,12 +204,31 @@ public void LoadAndPlay(string path, MediaInfo.MediaInfo info) _playback.MoveToStart(); _vp.Dispatcher.Invoke(() => { - _vp.buttonPlayPause.Content = "\xE768"; + _vp.buttonPlayPause.Content = FontSymbols.Play; }); } }; + + // Playback supported by DryWetMidi will block the current thread + // So we should run it in a new thread _ = Task.Run(() => _playback?.Play()); - _vp.buttonPlayPause.Content = "\xE769"; + _vp.buttonPlayPause.Content = FontSymbols.Pause; _context.IsBusy = false; } + + [NotifyPropertyChangedInvocator] + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + /// + /// Segoe Fluent Icons + /// https://learn.microsoft.com/en-us/windows/apps/design/style/segoe-fluent-icons-font + /// + private sealed class FontSymbols + { + public const string Play = "\xe768"; + public const string Pause = "\xe769"; + } } diff --git a/QuickLook.Plugin/QuickLook.Plugin.VideoViewer/GlobalUsing.cs b/QuickLook.Plugin/QuickLook.Plugin.VideoViewer/GlobalUsing.cs new file mode 100644 index 00000000..f17dda82 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.VideoViewer/GlobalUsing.cs @@ -0,0 +1,18 @@ +// Copyright © 2024 QL-Win Contributors +// +// This file is part of QuickLook program. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +global using MediaInfoLib = MediaInfo.MediaInfo; diff --git a/QuickLook.Plugin/QuickLook.Plugin.VideoViewer/Plugin.cs b/QuickLook.Plugin/QuickLook.Plugin.VideoViewer/Plugin.cs index de718981..b50c8bdb 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.VideoViewer/Plugin.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.VideoViewer/Plugin.cs @@ -26,7 +26,7 @@ namespace QuickLook.Plugin.VideoViewer; public class Plugin : IViewer { - private static MediaInfo.MediaInfo _mediaInfo; + private static MediaInfoLib _mediaInfo; private ViewerPanel _vp; @@ -34,7 +34,7 @@ public class Plugin : IViewer static Plugin() { - _mediaInfo = new MediaInfo.MediaInfo(Path.Combine( + _mediaInfo = new MediaInfoLib(Path.Combine( Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), Environment.Is64BitProcess ? "MediaInfo-x64\\" : "MediaInfo-x86\\")); _mediaInfo.Option("Cover_Data", "base64"); diff --git a/QuickLook.Plugin/QuickLook.Plugin.VideoViewer/ViewerPanel.xaml.cs b/QuickLook.Plugin/QuickLook.Plugin.VideoViewer/ViewerPanel.xaml.cs index 87511c09..c8c427fc 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.VideoViewer/ViewerPanel.xaml.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.VideoViewer/ViewerPanel.xaml.cs @@ -257,7 +257,7 @@ private void PlayerStateChanged(PlayerState oldState, PlayerState newState) } } - private void UpdateMeta(string path, MediaInfo.MediaInfo info) + private void UpdateMeta(string path, MediaInfoLib info) { if (HasVideo) return; @@ -382,20 +382,18 @@ private void ToggleShouldLoop(object sender, EventArgs e) ShouldLoop = !ShouldLoop; } - public void LoadAndPlay(string path, MediaInfo.MediaInfo info) + public void LoadAndPlay(string path, MediaInfoLib info) { + // Detect whether it is other playback formats if (!HasVideo) { - if (info != null) - { - string audioCodec = info.Get(StreamKind.Audio, 0, "Format"); + string audioCodec = info?.Get(StreamKind.Audio, 0, "Format"); - if (audioCodec == "MIDI") - { - _midiPlayer = new MidiPlayer(this, _context); - _midiPlayer.LoadAndPlay(path, info); - return; - } + if (audioCodec?.Equals("MIDI", StringComparison.OrdinalIgnoreCase) ?? false) + { + _midiPlayer = new MidiPlayer(this, _context); + _midiPlayer.LoadAndPlay(path, info); + return; // MIDI player will handle the playback at all } }