diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index 7a3aa7d347..ffc4aec16f 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -38,6 +38,7 @@ ALPHATYPE
AModifier
AMPROPERTY
AMPROPSETID
+amr
ANDSCANS
animatedvisuals
ansicolor
@@ -488,6 +489,7 @@ Filterkeyboard
Filterx
findfast
FIXEDFILEINFO
+flac
flyouts
FOF
FOFX
diff --git a/src/modules/peek/Peek.Common/Models/Win32/PropertyKey.cs b/src/modules/peek/Peek.Common/Models/Win32/PropertyKey.cs
index 2a7a50caa5..a3b6c9b6b9 100644
--- a/src/modules/peek/Peek.Common/Models/Win32/PropertyKey.cs
+++ b/src/modules/peek/Peek.Common/Models/Win32/PropertyKey.cs
@@ -63,5 +63,9 @@ namespace Peek.Common.Models
public static readonly PropertyKey FileType = new PropertyKey(new Guid(0xb725f130, 0x47ef, 0x101a, 0xa5, 0xf1, 0x02, 0x60, 0x8c, 0x9e, 0xeb, 0xac), 4);
public static readonly PropertyKey FrameWidth = new PropertyKey(new Guid(0x64440491, 0x4C8B, 0x11D1, 0x8B, 0x70, 0x08, 0x00, 0x36, 0xB1, 0x1A, 0x03), 3);
public static readonly PropertyKey FrameHeight = new PropertyKey(new Guid(0x64440491, 0x4C8B, 0x11D1, 0x8B, 0x70, 0x08, 0x00, 0x36, 0xB1, 0x1A, 0x03), 4);
+ public static readonly PropertyKey MusicTitle = new PropertyKey(new Guid(0xf29f85e0, 0x4ff9, 0x1068, 0xab, 0x91, 0x08, 0x00, 0x2b, 0x27, 0xb3, 0xd9), 2);
+ public static readonly PropertyKey MusicDisplayArtist = new PropertyKey(new Guid(0xFD122953, 0xFA93, 0x4EF7, 0x92, 0xC3, 0x04, 0xC9, 0x46, 0xB2, 0xF7, 0xC8), 100);
+ public static readonly PropertyKey MusicAlbum = new PropertyKey(new Guid(0x56a3372e, 0xce9c, 0x11d2, 0x9f, 0xe, 0x0, 0x60, 0x97, 0xc6, 0x86, 0xf6), 4);
+ public static readonly PropertyKey MusicDuration = new PropertyKey(new Guid(0x64440490, 0x4c8b, 0x11d1, 0x8b, 0x70, 0x8, 0x0, 0x36, 0xb1, 0x1a, 0x3), 3);
}
}
diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/AudioControl.xaml b/src/modules/peek/Peek.FilePreviewer/Controls/AudioControl.xaml
new file mode 100644
index 0000000000..3ed9e163b3
--- /dev/null
+++ b/src/modules/peek/Peek.FilePreviewer/Controls/AudioControl.xaml
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/AudioControl.xaml.cs b/src/modules/peek/Peek.FilePreviewer/Controls/AudioControl.xaml.cs
new file mode 100644
index 0000000000..c52f701c92
--- /dev/null
+++ b/src/modules/peek/Peek.FilePreviewer/Controls/AudioControl.xaml.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Peek.FilePreviewer.Previewers.MediaPreviewer.Models;
+
+namespace Peek.FilePreviewer.Controls
+{
+ public sealed partial class AudioControl : UserControl
+ {
+ public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(
+ nameof(Source),
+ typeof(AudioPreviewData),
+ typeof(AudioControl),
+ new PropertyMetadata(null, new PropertyChangedCallback((d, e) => ((AudioControl)d).SourcePropertyChanged())));
+
+ public static readonly DependencyProperty ToolTipTextProperty = DependencyProperty.Register(
+ nameof(ToolTipText),
+ typeof(string),
+ typeof(AudioControl),
+ new PropertyMetadata(null));
+
+ public AudioPreviewData? Source
+ {
+ get { return (AudioPreviewData)GetValue(SourceProperty); }
+ set { SetValue(SourceProperty, value); }
+ }
+
+ public string ToolTipText
+ {
+ get { return (string)GetValue(ToolTipTextProperty); }
+ set { SetValue(ToolTipTextProperty, value); }
+ }
+
+ public AudioControl()
+ {
+ this.InitializeComponent();
+ }
+
+ private void SourcePropertyChanged()
+ {
+ if (Source == null)
+ {
+ PlayerElement.MediaPlayer.Pause();
+ PlayerElement.MediaPlayer.Source = null;
+ }
+ }
+
+ private void KeyboardAccelerator_Space_Invoked(Microsoft.UI.Xaml.Input.KeyboardAccelerator sender, Microsoft.UI.Xaml.Input.KeyboardAcceleratorInvokedEventArgs args)
+ {
+ var mediaPlayer = PlayerElement.MediaPlayer;
+
+ if (mediaPlayer.Source == null || !mediaPlayer.CanPause)
+ {
+ return;
+ }
+
+ if (mediaPlayer.CurrentState == Windows.Media.Playback.MediaPlayerState.Playing)
+ {
+ mediaPlayer.Pause();
+ }
+ else
+ {
+ mediaPlayer.Play();
+ }
+
+ // Prevent the keyboard accelerator to be called twice
+ args.Handled = true;
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/DriveControl.xaml b/src/modules/peek/Peek.FilePreviewer/Controls/DriveControl.xaml
index 59bd179d0a..9861147fff 100644
--- a/src/modules/peek/Peek.FilePreviewer/Controls/DriveControl.xaml
+++ b/src/modules/peek/Peek.FilePreviewer/Controls/DriveControl.xaml
@@ -10,6 +10,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
SizeChanged="SizeChanged_Handler"
mc:Ignorable="d">
+
@@ -52,6 +52,12 @@
+
+
Previewer as IVideoPreviewer;
+ public IAudioPreviewer? AudioPreviewer => Previewer as IAudioPreviewer;
+
public IBrowserPreviewer? BrowserPreviewer => Previewer as IBrowserPreviewer;
public IArchivePreviewer? ArchivePreviewer => Previewer as IArchivePreviewer;
@@ -152,6 +155,8 @@ namespace Peek.FilePreviewer
Previewer = null;
ImagePreview.Visibility = Visibility.Collapsed;
VideoPreview.Visibility = Visibility.Collapsed;
+
+ AudioPreview.Visibility = Visibility.Collapsed;
BrowserPreview.Visibility = Visibility.Collapsed;
ArchivePreview.Visibility = Visibility.Collapsed;
DrivePreview.Visibility = Visibility.Collapsed;
@@ -159,6 +164,8 @@ namespace Peek.FilePreviewer
ImagePreview.FlowDirection = FlowDirection.LeftToRight;
VideoPreview.FlowDirection = FlowDirection.LeftToRight;
+
+ AudioPreview.FlowDirection = FlowDirection.LeftToRight;
BrowserPreview.FlowDirection = FlowDirection.LeftToRight;
ArchivePreview.FlowDirection = FlowDirection.LeftToRight;
DrivePreview.FlowDirection = FlowDirection.LeftToRight;
@@ -203,7 +210,7 @@ namespace Peek.FilePreviewer
await Previewer.LoadPreviewAsync(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
- await UpdateImageTooltipAsync(cancellationToken);
+ await UpdateTooltipAsync(cancellationToken);
}
catch (OperationCanceledException)
{
@@ -225,6 +232,7 @@ namespace Peek.FilePreviewer
VideoPreview.MediaPlayer.Source = null;
VideoPreview.Source = null;
+ AudioPreview.Source = null;
ImagePreview.Source = null;
ArchivePreview.Source = null;
BrowserPreview.Source = null;
@@ -327,7 +335,7 @@ namespace Peek.FilePreviewer
args.Handled = true;
}
- private async Task UpdateImageTooltipAsync(CancellationToken cancellationToken)
+ private async Task UpdateTooltipAsync(CancellationToken cancellationToken)
{
if (Item == null)
{
@@ -353,7 +361,7 @@ namespace Peek.FilePreviewer
string fileSizeFormatted = string.IsNullOrEmpty(fileSize) ? string.Empty : "\n" + ReadableStringHelper.FormatResourceString("PreviewTooltip_FileSize", fileSize);
sb.Append(fileSizeFormatted);
- ImageInfoTooltip = sb.ToString();
+ InfoTooltip = sb.ToString();
}
}
}
diff --git a/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj b/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj
index 8c411077fd..8ffcb6cc8a 100644
--- a/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj
+++ b/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj
@@ -17,6 +17,7 @@
+
@@ -28,6 +29,7 @@
+
@@ -47,6 +49,12 @@
+
+
+ MSBuild:Compile
+
+
+
MSBuild:Compile
diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IAudioPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IAudioPreviewer.cs
new file mode 100644
index 0000000000..8e728dd321
--- /dev/null
+++ b/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IAudioPreviewer.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Peek.FilePreviewer.Previewers.MediaPreviewer.Models;
+
+namespace Peek.FilePreviewer.Previewers.Interfaces
+{
+ public interface IAudioPreviewer : IPreviewer
+ {
+ public AudioPreviewData? Preview { get; }
+ }
+}
diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/AudioPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/AudioPreviewer.cs
new file mode 100644
index 0000000000..696efe906a
--- /dev/null
+++ b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/AudioPreviewer.cs
@@ -0,0 +1,175 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using Microsoft.UI.Dispatching;
+using Microsoft.UI.Xaml.Media.Imaging;
+using Peek.Common.Extensions;
+using Peek.Common.Helpers;
+using Peek.Common.Models;
+using Peek.FilePreviewer.Models;
+using Peek.FilePreviewer.Previewers.Helpers;
+using Peek.FilePreviewer.Previewers.Interfaces;
+using Peek.FilePreviewer.Previewers.MediaPreviewer.Models;
+using Windows.Foundation;
+using Windows.Media.Core;
+using Windows.Storage;
+
+namespace Peek.FilePreviewer.Previewers.MediaPreviewer
+{
+ public partial class AudioPreviewer : ObservableObject, IAudioPreviewer
+ {
+ [ObservableProperty]
+ private PreviewState _state;
+
+ [ObservableProperty]
+ private AudioPreviewData _preview;
+
+ private IFileSystemItem Item { get; }
+
+ private DispatcherQueue Dispatcher { get; }
+
+ public AudioPreviewer(IFileSystemItem file)
+ {
+ Item = file;
+ Dispatcher = DispatcherQueue.GetForCurrentThread();
+ Preview = new AudioPreviewData();
+ }
+
+ public async Task CopyAsync()
+ {
+ await Dispatcher.RunOnUiThread(async () =>
+ {
+ var storageItem = await Item.GetStorageItemAsync();
+ ClipboardHelper.SaveToClipboard(storageItem);
+ });
+ }
+
+ public Task GetPreviewSizeAsync(CancellationToken cancellationToken)
+ {
+ var size = new Size(680, 400);
+ var previewSize = new PreviewSize { MonitorSize = size, UseEffectivePixels = true };
+ return Task.FromResult(previewSize);
+ }
+
+ public async Task LoadPreviewAsync(CancellationToken cancellationToken)
+ {
+ State = PreviewState.Loading;
+
+ var thumbnailTask = LoadThumbnailAsync(cancellationToken);
+ var sourceTask = LoadSourceAsync(cancellationToken);
+ var metadataTask = LoadMetadataAsync(cancellationToken);
+
+ await Task.WhenAll(thumbnailTask, sourceTask, metadataTask);
+
+ if (!thumbnailTask.Result || !sourceTask.Result || !metadataTask.Result)
+ {
+ State = PreviewState.Error;
+ }
+ else
+ {
+ State = PreviewState.Loaded;
+ }
+ }
+
+ public Task LoadThumbnailAsync(CancellationToken cancellationToken)
+ {
+ return TaskExtension.RunSafe(async () =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ await Dispatcher.RunOnUiThread(async () =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var thumbnail = await IconHelper.GetThumbnailAsync(Item.Path, cancellationToken)
+ ?? await IconHelper.GetIconAsync(Item.Path, cancellationToken);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ Preview.Thumbnail = thumbnail ?? new SvgImageSource(new Uri("ms-appx:///Assets/Peek/DefaultFileIcon.svg"));
+ });
+ });
+ }
+
+ private Task LoadSourceAsync(CancellationToken cancellationToken)
+ {
+ return TaskExtension.RunSafe(async () =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var storageFile = await Item.GetStorageItemAsync() as StorageFile;
+
+ await Dispatcher.RunOnUiThread(() =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ Preview.MediaSource = MediaSource.CreateFromStorageFile(storageFile);
+ });
+ });
+ }
+
+ private Task LoadMetadataAsync(CancellationToken cancellationToken)
+ {
+ return TaskExtension.RunSafe(async () =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await Dispatcher.RunOnUiThread(() =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ Preview.Title = PropertyStoreHelper.TryGetStringProperty(Item.Path, PropertyKey.MusicTitle)
+ ?? Item.Name[..^Item.Extension.Length];
+
+ cancellationToken.ThrowIfCancellationRequested();
+ var artist = PropertyStoreHelper.TryGetStringProperty(Item.Path, PropertyKey.MusicDisplayArtist);
+ Preview.Artist = artist != null
+ ? string.Format(CultureInfo.CurrentCulture, ResourceLoaderInstance.ResourceLoader.GetString("Audio_Artist"), artist)
+ : string.Empty;
+
+ cancellationToken.ThrowIfCancellationRequested();
+ var album = PropertyStoreHelper.TryGetStringProperty(Item.Path, PropertyKey.MusicAlbum);
+ Preview.Album = album != null
+ ? string.Format(CultureInfo.CurrentCulture, ResourceLoaderInstance.ResourceLoader.GetString("Audio_Album"), album)
+ : string.Empty;
+
+ cancellationToken.ThrowIfCancellationRequested();
+ var ticksLength = PropertyStoreHelper.TryGetUlongProperty(Item.Path, PropertyKey.MusicDuration);
+ if (ticksLength.HasValue)
+ {
+ var length = TimeSpan.FromTicks((long)ticksLength);
+ var truncatedLength = new TimeSpan(length.Hours, length.Minutes, length.Seconds).ToString("g", CultureInfo.CurrentCulture);
+ Preview.Length = string.Format(CultureInfo.CurrentCulture, ResourceLoaderInstance.ResourceLoader.GetString("Audio_Length"), truncatedLength);
+ }
+ else
+ {
+ Preview.Length = string.Empty;
+ }
+ });
+ });
+ }
+
+ public static bool IsFileTypeSupported(string fileExt)
+ {
+ return _supportedFileTypes.Contains(fileExt);
+ }
+
+ private static readonly HashSet _supportedFileTypes = new()
+ {
+ ".aac",
+ ".ac3",
+ ".amr",
+ ".flac",
+ ".m4a",
+ ".mp3",
+ ".ogg",
+ ".wav",
+ ".wma",
+ };
+ }
+}
diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/Models/AudioPreviewData.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/Models/AudioPreviewData.cs
new file mode 100644
index 0000000000..d680dd3da4
--- /dev/null
+++ b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/Models/AudioPreviewData.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.Mvvm.ComponentModel;
+using Microsoft.UI.Xaml.Media;
+using Windows.Media.Core;
+
+namespace Peek.FilePreviewer.Previewers.MediaPreviewer.Models
+{
+ public partial class AudioPreviewData : ObservableObject
+ {
+ [ObservableProperty]
+ private MediaSource? _mediaSource;
+
+ [ObservableProperty]
+ private ImageSource? _thumbnail;
+
+ [ObservableProperty]
+ private string _title;
+
+ [ObservableProperty]
+ private string _artist;
+
+ [ObservableProperty]
+ private string _album;
+
+ [ObservableProperty]
+ private string _length;
+
+ public AudioPreviewData()
+ {
+ Artist = string.Empty;
+ Title = string.Empty;
+ Album = string.Empty;
+ Length = string.Empty;
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs
index 089b96bff5..23f54e846d 100644
--- a/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs
+++ b/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs
@@ -9,6 +9,7 @@ using Peek.Common.Models;
using Peek.FilePreviewer.Models;
using Peek.FilePreviewer.Previewers.Archives;
using Peek.FilePreviewer.Previewers.Drive;
+using Peek.FilePreviewer.Previewers.MediaPreviewer;
using Peek.UI.Telemetry.Events;
namespace Peek.FilePreviewer.Previewers
@@ -32,6 +33,10 @@ namespace Peek.FilePreviewer.Previewers
{
return new VideoPreviewer(file);
}
+ else if (AudioPreviewer.IsFileTypeSupported(file.Extension))
+ {
+ return new AudioPreviewer(file);
+ }
else if (WebBrowserPreviewer.IsFileTypeSupported(file.Extension))
{
return new WebBrowserPreviewer(file, _previewSettings);
diff --git a/src/modules/peek/Peek.UI/PeekXAML/Views/TitleBar.xaml.cs b/src/modules/peek/Peek.UI/PeekXAML/Views/TitleBar.xaml.cs
index b4d9786697..50f7da2cc9 100644
--- a/src/modules/peek/Peek.UI/PeekXAML/Views/TitleBar.xaml.cs
+++ b/src/modules/peek/Peek.UI/PeekXAML/Views/TitleBar.xaml.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation
+// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
diff --git a/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw b/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw
index dba48568a3..9334638508 100644
--- a/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw
+++ b/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw
@@ -306,4 +306,16 @@
Unknown
Used for unknown drive type or file system
+
+ Album: {0}
+ {0} is the title of the album read from file metadata
+
+
+ Artist: {0}
+ {0} is the artist read from file metadata
+
+
+ Length: {0}
+ {0} is the duration of the audio read from file metadata
+
\ No newline at end of file