Sunday, 3 August 2025

MAUI Audio Player App

Implementation for a MAUI audio player app with song management, favorites, and playlist features.

1. Set Up the Project

First, create a new .NET MAUI project and add these NuGet packages:

  • Plugin.Maui.Audio (for audio playback)

  • CommunityToolkit.Mvvm (for MVVM helpers)

2. Model Classes

csharp
// Models/Song.cs
public class Song
{
    public string Id { get; set; } = Guid.NewGuid().ToString();
    public string Title { get; set; }
    public string Artist { get; set; }
    public string Album { get; set; }
    public string FilePath { get; set; }
    public TimeSpan Duration { get; set; }
    public bool IsFavorite { get; set; }
    public DateTime DateAdded { get; set; } = DateTime.Now;
}

// Models/Playlist.cs
public class Playlist
{
    public string Id { get; set; } = Guid.NewGuid().ToString();
    public string Name { get; set; }
    public List<string> SongIds { get; set; } = new();
    public DateTime CreatedDate { get; set; } = DateTime.Now;
}

3. ViewModel

csharp
// ViewModels/PlayerViewModel.cs
public partial class PlayerViewModel : ObservableObject
{
    private readonly IAudioManager audioManager;
    private IAudioPlayer player;
    
    [ObservableProperty]
    private Song currentSong;
    
    [ObservableProperty]
    private bool isPlaying;
    
    [ObservableProperty]
    private TimeSpan currentPosition;
    
    [ObservableProperty]
    private double playbackProgress;
    
    [ObservableProperty]
    private ObservableCollection<Song> songs = new();
    
    [ObservableProperty]
    private ObservableCollection<Playlist> playlists = new();
    
    [ObservableProperty]
    private ObservableCollection<Song> favorites = new();
    
    public PlayerViewModel(IAudioManager audioManager)
    {
        this.audioManager = audioManager;
        
        // Load data from preferences
        LoadSongs();
        LoadPlaylists();
        LoadFavorites();
        
        // Timer to update progress
        Device.StartTimer(TimeSpan.FromMilliseconds(500), () =>
        {
            if (player != null && IsPlaying)
            {
                CurrentPosition = player.CurrentPosition;
                PlaybackProgress = player.CurrentPosition.TotalMilliseconds / player.Duration.TotalMilliseconds;
            }
            return true;
        });
    }
    
    [RelayCommand]
    private async Task AddSongs()
    {
        try
        {
            var options = new PickOptions
            {
                PickerTitle = "Please select audio files",
                FileTypes = new FilePickerFileType(
                    new Dictionary<DevicePlatform, IEnumerable<string>>
                    {
                        { DevicePlatform.WinUI, new[] { ".mp3", ".wav", ".m4a" } },
                        { DevicePlatform.Android, new[] { "audio/*" } },
                        { DevicePlatform.iOS, new[] { "public.audio" } },
                        { DevicePlatform.MacCatalyst, new[] { "public.audio" } }
                    })
            };
            
            var results = await FilePicker.Default.PickMultipleAsync(options);
            
            if (results != null)
            {
                foreach (var result in results)
                {
                    var song = new Song
                    {
                        Title = result.FileName,
                        FilePath = result.FullPath,
                        // You could use a library like TagLib# to extract metadata
                        Artist = "Unknown Artist",
                        Album = "Unknown Album"
                    };
                    
                    // Create player to get duration
                    var tempPlayer = audioManager.CreatePlayer(await result.OpenReadAsync());
                    song.Duration = tempPlayer.Duration;
                    tempPlayer.Dispose();
                    
                    Songs.Add(song);
                }
                
                SaveSongs();
            }
        }
        catch (Exception ex)
        {
            await Shell.Current.DisplayAlert("Error", ex.Message, "OK");
        }
    }
    
    [RelayCommand]
    private async Task PlaySong(Song song)
    {
        if (song == null) return;
        
        if (player != null)
        {
            player.Dispose();
        }
        
        try
        {
            var file = await FileSystem.OpenAppPackageFileAsync(song.FilePath);
            player = audioManager.CreatePlayer(file);
            
            CurrentSong = song;
            player.Play();
            IsPlaying = true;
            
            player.PlaybackEnded += (s, e) =>
            {
                IsPlaying = false;
                PlaybackProgress = 0;
                CurrentPosition = TimeSpan.Zero;
            };
        }
        catch (Exception ex)
        {
            await Shell.Current.DisplayAlert("Error", ex.Message, "OK");
        }
    }
    
    [RelayCommand]
    private void TogglePlayPause()
    {
        if (player == null) return;
        
        if (IsPlaying)
        {
            player.Pause();
        }
        else
        {
            player.Play();
        }
        
        IsPlaying = !IsPlaying;
    }
    
    [RelayCommand]
    private void Seek(double value)
    {
        if (player != null)
        {
            var newPosition = value * player.Duration.TotalMilliseconds;
            player.Seek(TimeSpan.FromMilliseconds(newPosition));
        }
    }
    
    [RelayCommand]
    private void ToggleFavorite(Song song)
    {
        song.IsFavorite = !song.IsFavorite;
        
        if (song.IsFavorite && !Favorites.Contains(song))
        {
            Favorites.Add(song);
        }
        else if (!song.IsFavorite && Favorites.Contains(song))
        {
            Favorites.Remove(song);
        }
        
        SaveFavorites();
        SaveSongs();
    }
    
    [RelayCommand]
    private async Task CreatePlaylist()
    {
        var name = await Shell.Current.DisplayPromptAsync("New Playlist", "Enter playlist name:");
        
        if (!string.IsNullOrWhiteSpace(name))
        {
            var playlist = new Playlist { Name = name };
            Playlists.Add(playlist);
            SavePlaylists();
        }
    }
    
    [RelayCommand]
    private async Task AddToPlaylist(Playlist playlist)
    {
        if (CurrentSong == null) return;
        
        if (!playlist.SongIds.Contains(CurrentSong.Id))
        {
            playlist.SongIds.Add(CurrentSong.Id);
            SavePlaylists();
            await Shell.Current.DisplayAlert("Success", $"Added to {playlist.Name}", "OK");
        }
        else
        {
            await Shell.Current.DisplayAlert("Info", "Song already in playlist", "OK");
        }
    }
    
    [RelayCommand]
    private async Task ViewPlaylistSongs(Playlist playlist)
    {
        var songsInPlaylist = Songs.Where(s => playlist.SongIds.Contains(s.Id)).ToList();
        await Shell.Current.DisplayAlert(playlist.Name, 
            string.Join("\n", songsInPlaylist.Select(s => s.Title)), "OK");
    }
    
    private void LoadSongs()
    {
        var songsJson = Preferences.Get("UserSongs", string.Empty);
        if (!string.IsNullOrEmpty(songsJson))
        {
            var songs = JsonSerializer.Deserialize<List<Song>>(songsJson);
            Songs = new ObservableCollection<Song>(songs);
        }
    }
    
    private void SaveSongs()
    {
        var songsJson = JsonSerializer.Serialize(Songs.ToList());
        Preferences.Set("UserSongs", songsJson);
    }
    
    private void LoadPlaylists()
    {
        var playlistsJson = Preferences.Get("UserPlaylists", string.Empty);
        if (!string.IsNullOrEmpty(playlistsJson))
        {
            var playlists = JsonSerializer.Deserialize<List<Playlist>>(playlistsJson);
            Playlists = new ObservableCollection<Playlist>(playlists);
        }
    }
    
    private void SavePlaylists()
    {
        var playlistsJson = JsonSerializer.Serialize(Playlists.ToList());
        Preferences.Set("UserPlaylists", playlistsJson);
    }
    
    private void LoadFavorites()
    {
        var favoritesJson = Preferences.Get("UserFavorites", string.Empty);
        if (!string.IsNullOrEmpty(favoritesJson))
        {
            var favorites = JsonSerializer.Deserialize<List<Song>>(favoritesJson);
            Favorites = new ObservableCollection<Song>(favorites);
        }
    }
    
    private void SaveFavorites()
    {
        var favoritesJson = JsonSerializer.Serialize(Favorites.ToList());
        Preferences.Set("UserFavorites", favoritesJson);
    }
}

4. Main Page (UI)

xml
<!-- MainPage.xaml -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:vm="clr-namespace:YourNamespace.ViewModels"
             x:Class="YourNamespace.MainPage"
             Title="MAUI Music Player">
    
    <ContentPage.BindingContext>
        <vm:PlayerViewModel />
    </ContentPage.BindingContext>
    
    <Grid RowDefinitions="*,Auto,Auto" Padding="10">
        <!-- Song List -->
        <RefreshView Grid.Row="0"
                     Command="{Binding LoadSongsCommand}"
                     IsRefreshing="{Binding IsBusy, Mode=TwoWay}">
            <CollectionView ItemsSource="{Binding Songs}"
                          SelectionMode="Single"
                          SelectedItem="{Binding CurrentSong}">
                <CollectionView.ItemTemplate>
                    <DataTemplate>
                        <Grid Padding="10" ColumnDefinitions="Auto,*,Auto">
                            <Image Grid.Column="0" 
                                  Source="{OnPlatform Default='music_note', iOS='music_note_ios'}"
                                  WidthRequest="40" HeightRequest="40" />
                            
                            <VerticalStackLayout Grid.Column="1" Padding="10,0">
                                <Label Text="{Binding Title}" FontSize="16" />
                                <Label Text="{Binding Artist}" FontSize="14" TextColor="Gray" />
                            </VerticalStackLayout>
                            
                            <Button Grid.Column="2" 
                                    Text="{Binding IsFavorite, Converter={StaticResource FavoriteConverter}}"
                                    Command="{Binding Source={RelativeSource AncestorType={x:Type vm:PlayerViewModel}}, Path=ToggleFavoriteCommand}"
                                    CommandParameter="{Binding .}"
                                    BackgroundColor="Transparent"
                                    FontSize="20" />
                        </Grid>
                    </DataTemplate>
                </CollectionView.ItemTemplate>
            </CollectionView>
        </RefreshView>
        
        <!-- Player Controls -->
        <Grid Grid.Row="1" Padding="10" IsVisible="{Binding CurrentSong, Converter={StaticResource NullToBoolConverter}}">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            
            <Label Grid.Row="0" Text="{Binding CurrentSong.Title}" FontSize="18" FontAttributes="Bold" />
            <Label Grid.Row="0" HorizontalOptions="End" Text="{Binding CurrentSong.Artist}" />
            
            <Slider Grid.Row="1" 
                    Minimum="0" Maximum="1" 
                    Value="{Binding PlaybackProgress}"
                    ValueChanged="Slider_ValueChanged"
                    DragCompletedCommand="{Binding SeekCommand}"
                    DragCompletedCommandParameter="{Binding Source={RelativeSource Self}, Path=Value}" />
            
            <Grid Grid.Row="2" ColumnDefinitions="*,Auto,Auto,Auto,*">
                <Label Grid.Column="1" Text="{Binding CurrentPosition, StringFormat='{0:mm\\:ss}'}" />
                <Label Grid.Column="3" Text="{Binding CurrentSong.Duration, StringFormat='{0:mm\\:ss}'}" />
                
                <Button Grid.Column="2" 
                        Text="{Binding IsPlaying, Converter={StaticResource PlayPauseConverter}}"
                        Command="{Binding TogglePlayPauseCommand}"
                        FontSize="24" />
            </Grid>
        </Grid>
        
        <!-- Bottom Menu -->
        <Grid Grid.Row="2" ColumnDefinitions="*,*,*,*" Padding="10">
            <Button Grid.Column="0" Text="Add Songs" Command="{Binding AddSongsCommand}" />
            <Button Grid.Column="1" Text="Favorites" Command="{Binding ViewFavoritesCommand}" />
            <Button Grid.Column="2" Text="Playlists" Command="{Binding ViewPlaylistsCommand}" />
            <Button Grid.Column="3" Text="Add to Playlist" Command="{Binding AddToPlaylistCommand}" />
        </Grid>
    </Grid>
</ContentPage>

5. Converters and Helpers

csharp
// Converters/FavoriteConverter.cs
public class FavoriteConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (bool)value ? "❤️" : "♡";
    }
    
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

// Converters/PlayPauseConverter.cs
public class PlayPauseConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (bool)value ? "⏸" : "▶";
    }
    
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

// Converters/NullToBoolConverter.cs
public class NullToBoolConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value != null;
    }
    
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

6. Register Services

In MauiProgram.cs:

csharp
builder.Services.AddSingleton(AudioManager.Current);
builder.Services.AddSingleton<PlayerViewModel>();
builder.Services.AddSingleton<MainPage>();

7. Additional Features to Consider

  1. Metadata Extraction: Use TagLib# to read ID3 tags from MP3 files for better song information.

  2. Background Playback: Implement background service for continuous playback when app is minimized.

  3. Visualizations: Add audio visualizations using SkiaSharp.

  4. Dark/Light Theme: Support for theme switching.

  5. Search Functionality: Add search bar to filter songs.

  6. Shuffle/Repeat: Implement playback modes.

  7. Cloud Sync: Add support for syncing with cloud storage.

8. Platform-Specific Considerations

  • Android: Request runtime permissions for file access

  • iOS: Handle audio session interruptions

  • Windows: Implement system media transport controls

This implementation provides a complete foundation for a MAUI audio player with all the requested features. You can extend it further based on your specific requirements.

No comments:

Post a Comment

Complete Guide: Building a Live Cricket Streaming App for 100M Users

Comprehensive guide to building a scalable live cricket streaming platform for 100M users, covering backend infrastructure, streaming techno...