From 06751311955a9a5800b68029de28bf888cde17ff Mon Sep 17 00:00:00 2001 From: Brian Bicknell Date: Sun, 2 Mar 2025 23:28:28 -0500 Subject: [PATCH] Updated SongPictureInfo use to async copy of picture data stream. Update other classes accordingly. --- .../Caching/AudioImageMemoryCache.cs | 6 +- Harmonia.Core/Imaging/AudioImageExtractor.cs | 10 +- Harmonia.Core/Imaging/IAudioImageExtractor.cs | 4 +- Harmonia.Core/Imaging/SongPictureInfo.cs | 45 +++--- Harmonia.Core/Library/IAudioLibrary.cs | 22 +-- Harmonia.Core/Scanner/AudioFileScanner.cs | 13 +- Harmonia.Core/Scanner/IAudioFileScanner.cs | 4 +- .../ViewModels/PlaybackBarViewModel.cs | 41 ++++- Harmonia.UI/Views/MainView.axaml | 11 +- Harmonia.UI/Views/PlaybackBar.axaml | 146 ++++++++++-------- Harmonia.UI/Views/PlaybackBar.axaml.cs | 25 --- 11 files changed, 178 insertions(+), 149 deletions(-) diff --git a/Harmonia.Core/Caching/AudioImageMemoryCache.cs b/Harmonia.Core/Caching/AudioImageMemoryCache.cs index 9a679aa..059df7d 100644 --- a/Harmonia.Core/Caching/AudioImageMemoryCache.cs +++ b/Harmonia.Core/Caching/AudioImageMemoryCache.cs @@ -30,11 +30,11 @@ public class AudioImageMemoryCache(IAudioImageExtractor audioImageExtractor) : M return null; } - protected override ValueTask FetchAsync(Song key, CancellationToken cancellationToken) + protected override async ValueTask FetchAsync(Song key, CancellationToken cancellationToken) { - SongPictureInfo? songPictureInfo = audioImageExtractor.ExtractImage(key.FileName); + SongPictureInfo? songPictureInfo = await audioImageExtractor.ExtractImageAsync(key.FileName, cancellationToken); - return ValueTask.FromResult(songPictureInfo); + return songPictureInfo; } protected override long GetEntrySize(SongPictureInfo entry) diff --git a/Harmonia.Core/Imaging/AudioImageExtractor.cs b/Harmonia.Core/Imaging/AudioImageExtractor.cs index f9fc515..2a9b90e 100644 --- a/Harmonia.Core/Imaging/AudioImageExtractor.cs +++ b/Harmonia.Core/Imaging/AudioImageExtractor.cs @@ -46,20 +46,20 @@ public class AudioImageExtractor : IAudioImageExtractor ]; } - public SongPictureInfo? ExtractImage(string path) + public async Task ExtractImageAsync(string path, CancellationToken cancellationToken) { SongTagInfo songTagInfo = _tagResolver.GetSongTagInfo(path); - return ExtractImage(path, songTagInfo); + return await ExtractImageAsync(path, songTagInfo, cancellationToken); } - public SongPictureInfo? ExtractImage(string path, SongTagInfo songTagInfo) + public async Task ExtractImageAsync(string path, SongTagInfo songTagInfo, CancellationToken cancellationToken) { if (songTagInfo.FrontCover != null) { using MemoryStream memoryStream = new(songTagInfo.FrontCover.Data); - return SongPictureInfo.FromStream(memoryStream); + return await SongPictureInfo.FromStreamAsync(memoryStream, cancellationToken); } else { @@ -68,7 +68,7 @@ public class AudioImageExtractor : IAudioImageExtractor if (string.IsNullOrWhiteSpace(imagePath)) return null; - return SongPictureInfo.FromFile(path); + return await SongPictureInfo.FromFileAsync(path, cancellationToken); } } diff --git a/Harmonia.Core/Imaging/IAudioImageExtractor.cs b/Harmonia.Core/Imaging/IAudioImageExtractor.cs index 8021bef..dca7f3c 100644 --- a/Harmonia.Core/Imaging/IAudioImageExtractor.cs +++ b/Harmonia.Core/Imaging/IAudioImageExtractor.cs @@ -4,6 +4,6 @@ namespace Harmonia.Core.Imaging; public interface IAudioImageExtractor { - SongPictureInfo? ExtractImage(string path); - SongPictureInfo? ExtractImage(string path, SongTagInfo songTagInfo); + Task ExtractImageAsync(string path, CancellationToken cancellationToken); + Task ExtractImageAsync(string path, SongTagInfo songTagInfo, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/Harmonia.Core/Imaging/SongPictureInfo.cs b/Harmonia.Core/Imaging/SongPictureInfo.cs index 5ebd912..5ecf5e7 100644 --- a/Harmonia.Core/Imaging/SongPictureInfo.cs +++ b/Harmonia.Core/Imaging/SongPictureInfo.cs @@ -2,23 +2,23 @@ namespace Harmonia.Core.Imaging; -public class SongPictureInfo : IDisposable +public class SongPictureInfo { - public required Stream Stream { get; init; } - public string? ImageHash { get; init; } + public required byte[] Data { get; init; } + public string? ImageHash { get; private set; } public required string ImageName { get; init; } - public long Size { get; init; } + public long Size => Data.Length; private SongPictureInfo() { } - public static SongPictureInfo FromStream(Stream stream) + public static async Task FromStreamAsync(Stream stream, CancellationToken cancellationToken) { string imageHash = ComputeImageHash(stream); - return GetSongPictureInfo(stream, "Embedded", imageHash); + return await GetSongPictureInfoAsync(stream, "Embedded", imageHash, cancellationToken); } private static string ComputeImageHash(Stream stream) @@ -29,31 +29,30 @@ public class SongPictureInfo : IDisposable return Convert.ToHexStringLower(hash); } - public static SongPictureInfo FromFile(string fileName) + public static async Task FromFileAsync(string fileName, CancellationToken cancellationToken) { using FileStream fileStream = File.OpenRead(fileName); - return GetSongPictureInfo(fileStream, fileName); + return await GetSongPictureInfoAsync(fileStream, fileName, cancellationToken: cancellationToken); } - private static SongPictureInfo GetSongPictureInfo(Stream stream, string imageName, string? imageHash = null) + private static async Task GetSongPictureInfoAsync(Stream stream, string imageName, string? imageHash = null, CancellationToken cancellationToken = default) + { + return new() + { + Data = await GetImageDataAsync(stream, cancellationToken), + ImageHash = imageHash, + ImageName = imageName + }; + } + + public static async Task GetImageDataAsync(Stream stream, CancellationToken cancellationToken) { stream.Seek(0, SeekOrigin.Begin); - SongPictureInfo songPictureInfo = new() - { - Stream = stream, - ImageHash = imageHash, - ImageName = imageName, - Size = stream.Length - }; + using MemoryStream memoryStream = new(); + await stream.CopyToAsync(memoryStream, cancellationToken); - return songPictureInfo; - } - - public void Dispose() - { - Stream.Dispose(); - GC.SuppressFinalize(this); + return memoryStream.ToArray(); } } \ No newline at end of file diff --git a/Harmonia.Core/Library/IAudioLibrary.cs b/Harmonia.Core/Library/IAudioLibrary.cs index 1074acf..03fcf80 100644 --- a/Harmonia.Core/Library/IAudioLibrary.cs +++ b/Harmonia.Core/Library/IAudioLibrary.cs @@ -6,7 +6,7 @@ namespace Harmonia.Core.Library; public interface IAudioLibrary { - void AddLocation(string path); + ValueTask AddLocationAsync(string path, CancellationToken cancellationToken); void RemoveLocation(string path); void RemoveLocations(string[] paths); void Scan(string path); @@ -25,12 +25,12 @@ public abstract class AudioLibrary : IAudioLibrary public event EventHandler? ScanStarted; public event EventHandler? ScanFinished; - protected abstract bool IncludeLocation(string path); - protected abstract void ScanLocation(string path); + protected abstract ValueTask IncludeLocationAsync(string path, CancellationToken cancellationToken); + protected abstract ValueTask ScanLocationAsync(string path, CancellationToken cancellationToken); - public void AddLocation(string path) + public async ValueTask AddLocationAsync(string path, CancellationToken cancellationToken) { - if (IncludeLocation(path) == false) + if (await IncludeLocationAsync(path, cancellationToken) == false) return; LocationAdded?.Invoke(this, new EventArgs()); @@ -69,13 +69,13 @@ public abstract class AudioLibrary : IAudioLibrary public class AudioDatabaseLibrary(IAudioFileScanner audioFileScanner, AudioLibraryContext context) : AudioLibrary { - protected override bool IncludeLocation(string path) + protected override async ValueTask IncludeLocationAsync(string path, CancellationToken cancellationToken) { if (CanAddLocation(path) == false) return false; Location location = GetLocation(path) ?? CreateLocation(path); - ScanLocation(location); + await ScanLocationAsync(location, cancellationToken); return true; } @@ -104,19 +104,19 @@ public class AudioDatabaseLibrary(IAudioFileScanner audioFileScanner, AudioLibra return context.Locations.FirstOrDefault(location => location.Name == path); } - protected override void ScanLocation(string path) + protected override async ValueTask ScanLocationAsync(string path, CancellationToken cancellationToken) { Location? location = GetLocation(path); if (location == null) return; - ScanLocation(location); + await ScanLocationAsync(location, cancellationToken); } - private void ScanLocation(Location location) + private async ValueTask ScanLocationAsync(Location location, CancellationToken cancellationToken) { - SongModel[] songModels = audioFileScanner.GetSongsFromPath(location.Name); + SongModel[] songModels = await audioFileScanner.GetSongsFromPathAsync(location.Name, cancellationToken); AddFolders(location, songModels); AddSongs(songModels); diff --git a/Harmonia.Core/Scanner/AudioFileScanner.cs b/Harmonia.Core/Scanner/AudioFileScanner.cs index 23521e7..1922f6d 100644 --- a/Harmonia.Core/Scanner/AudioFileScanner.cs +++ b/Harmonia.Core/Scanner/AudioFileScanner.cs @@ -7,7 +7,7 @@ namespace Harmonia.Core.Scanner; public class AudioFileScanner(IAudioEngine audioEngine, ITagResolver tagResolver, IAudioImageExtractor audioImageExtractor) : IAudioFileScanner { - public Song[] GetSongs(string[] fileNames) + public async Task GetSongsAsync(string[] fileNames, CancellationToken cancellationToken) { List songs = []; @@ -16,7 +16,7 @@ public class AudioFileScanner(IAudioEngine audioEngine, ITagResolver tagResolver if (string.IsNullOrWhiteSpace(fileName) || File.Exists(fileName) == false) continue; - Song? song = GetSong(fileName); + Song? song = await GetSongAsync(fileName, cancellationToken); if (song == null) continue; @@ -27,11 +27,12 @@ public class AudioFileScanner(IAudioEngine audioEngine, ITagResolver tagResolver return [.. songs]; } - private Song GetSong(string fileName) + private async Task GetSongAsync(string fileName, CancellationToken cancellationToken) { FileInfo fileInfo = new(fileName); SongTagInfo songTagInfo = tagResolver.GetSongTagInfo(fileName); - using SongPictureInfo? songPictureInfo = audioImageExtractor.ExtractImage(fileName, songTagInfo); + //using SongPictureInfo? songPictureInfo = audioImageExtractor.ExtractImage(fileName, songTagInfo); + SongPictureInfo? songPictureInfo = await audioImageExtractor.ExtractImageAsync(fileName, songTagInfo, cancellationToken); Song song = new() { @@ -56,12 +57,12 @@ public class AudioFileScanner(IAudioEngine audioEngine, ITagResolver tagResolver return song; } - public Song[] GetSongsFromPath(string path) + public async Task GetSongsFromPathAsync(string path, CancellationToken cancellationToken) { FileInfo[] fileInfoList = GetAllFilesFromDirectory(path); string[] fileNames = [.. fileInfoList.Select(x => x.FullName)]; - return GetSongs(fileNames); + return await GetSongsAsync(fileNames, cancellationToken); } private FileInfo[] GetAllFilesFromDirectory(string directoryName) diff --git a/Harmonia.Core/Scanner/IAudioFileScanner.cs b/Harmonia.Core/Scanner/IAudioFileScanner.cs index 305a092..d5bc5cb 100644 --- a/Harmonia.Core/Scanner/IAudioFileScanner.cs +++ b/Harmonia.Core/Scanner/IAudioFileScanner.cs @@ -4,6 +4,6 @@ namespace Harmonia.Core.Scanner; public interface IAudioFileScanner { - Song[] GetSongs(string[] fileNames); - Song[] GetSongsFromPath(string path); + Task GetSongsAsync(string[] fileNames, CancellationToken cancellationToken); + Task GetSongsFromPathAsync(string path, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/Harmonia.UI/ViewModels/PlaybackBarViewModel.cs b/Harmonia.UI/ViewModels/PlaybackBarViewModel.cs index 5229752..671f848 100644 --- a/Harmonia.UI/ViewModels/PlaybackBarViewModel.cs +++ b/Harmonia.UI/ViewModels/PlaybackBarViewModel.cs @@ -1,6 +1,7 @@ using Avalonia.Controls; using Avalonia.Media.Imaging; using Avalonia.Threading; +using CommunityToolkit.Mvvm.Input; using Harmonia.Core.Caching; using Harmonia.Core.Imaging; using Harmonia.Core.Models; @@ -8,9 +9,11 @@ using Harmonia.Core.Player; using Harmonia.Core.Playlists; using Harmonia.Core.Scanner; using System; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using System.Windows.Input; namespace Harmonia.UI.ViewModels; @@ -33,6 +36,7 @@ public partial class PlaybackBarViewModel : ViewModelBase { _song = value; OnPropertyChanged(); + OnPropertyChanged(nameof(FormattedSongInfo)); CurrentPosition = 0; // What if player is being loaded and returning to save state? Position = 0; @@ -54,6 +58,10 @@ public partial class PlaybackBarViewModel : ViewModelBase } } + public string FormattedSongInfo => Song != null + ? $"{Song.FileType} - {Song.BitRate} kbps - {Song.SampleRate} Hz" + : string.Empty; + private double _currentPosition; public double CurrentPosition { @@ -123,7 +131,11 @@ public partial class PlaybackBarViewModel : ViewModelBase } } - public string Greeting => "Welcome to Harmonia!"; + public ICommand PlaySongCommand => new RelayCommand(Play); + public ICommand PauseSongCommand => new RelayCommand(Pause); + public ICommand StopSongCommand => new RelayCommand(Stop); + public ICommand PreviousSongCommand => new RelayCommand(Previous); + public ICommand NextSongCommand => new RelayCommand(Next); public PlaybackBarViewModel(IAudioPlayer audioPlayer, IAudioImageCache audioImageCache, IPlaylistRepository playlistRepository, IAudioFileScanner audioFileScanner) { @@ -133,6 +145,27 @@ public partial class PlaybackBarViewModel : ViewModelBase _audioImageCache = audioImageCache; _timer = new(TimeSpan.FromMilliseconds(100), DispatcherPriority.Default, TickTock); + + PlayDemoSong(playlistRepository, audioFileScanner); + } + + private async Task PlayDemoSong(IPlaylistRepository playlistRepository, IAudioFileScanner audioFileScanner, CancellationToken cancellationToken = default) + { + if (playlistRepository.Get().Count == 0) + { + playlistRepository.AddPlaylist(); + + Playlist playlist = playlistRepository.Get().First(); + + //string songPath = @"D:\Music\Game Music\Bobby Prince\Doom II"; + //string songPath = @"D:\Music\Anime Music\HimeHina"; + string songPath = @"D:\Music\K-Pop"; + + Song[] songs = await audioFileScanner.GetSongsFromPathAsync(songPath, cancellationToken); + playlist.AddSongs(songs); + } + + await _audioPlayer.LoadAsync(playlistRepository.Get().First().Songs[0], PlaybackMode.LoadAndPlay); } private void OnAudioPlayerPlayingSongChanged(object? sender, EventArgs e) @@ -163,7 +196,11 @@ public partial class PlaybackBarViewModel : ViewModelBase private void SetSongImageSource(SongPictureInfo songPictureInfo) { - SongImageSource = new(songPictureInfo.Stream); + if (songPictureInfo.Data.Length == 0) + return; + + using MemoryStream stream = new(songPictureInfo.Data); + SongImageSource = new(stream); } private void TickTock(object? sender, object e) diff --git a/Harmonia.UI/Views/MainView.axaml b/Harmonia.UI/Views/MainView.axaml index 8cf1808..21bd62e 100644 --- a/Harmonia.UI/Views/MainView.axaml +++ b/Harmonia.UI/Views/MainView.axaml @@ -14,9 +14,12 @@ - - - - + + + + + + + diff --git a/Harmonia.UI/Views/PlaybackBar.axaml b/Harmonia.UI/Views/PlaybackBar.axaml index 24c75c0..6f13f91 100644 --- a/Harmonia.UI/Views/PlaybackBar.axaml +++ b/Harmonia.UI/Views/PlaybackBar.axaml @@ -14,80 +14,94 @@ - - - - - - - - - - - - - + + + + + + + + + + + + - - + + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - - - - - - - - - + + + + + + + + + + - + diff --git a/Harmonia.UI/Views/PlaybackBar.axaml.cs b/Harmonia.UI/Views/PlaybackBar.axaml.cs index 6b40747..6574f2d 100644 --- a/Harmonia.UI/Views/PlaybackBar.axaml.cs +++ b/Harmonia.UI/Views/PlaybackBar.axaml.cs @@ -45,29 +45,4 @@ public partial class PlaybackBar : UserControl _viewModel.CurrentPosition = slider.Value; _viewModel.IsPositionChangeInProgress = false; } - - private void PlayButton_Click(object? sender, RoutedEventArgs e) - { - _viewModel.Play(); - } - - private void StopButton_Click(object? sender, RoutedEventArgs e) - { - _viewModel.Stop(); - } - - private void PauseButton_Click(object? sender, RoutedEventArgs e) - { - _viewModel.Pause(); - } - - private void PreviousSongButton_Click(object? sender, RoutedEventArgs e) - { - _viewModel.Previous(); - } - - private void NextSongButton_Click(object sender, RoutedEventArgs e) - { - _viewModel.Next(); - } } \ No newline at end of file