[MAUI 项目实战] 音乐播放器(二):播放内核

开源播放器内核比较:ijkPlayer、VLC、ExoPlayer、MediaPlayer、SmartPlayer 开源播放器内核比较: ijkPlayer、VLC、ExoPlayer、MediaPlayer、SmartPlayer 阅读详情

播放控制服务

IMusicControlService:
播放控制类,用于当前平台播放器对象的操作,对当前所播放曲目的暂停/播放,下一首/上一首,快进快退(寻迹),随机、单曲模式等功能的控制。

播放控制类包含一个平台特定的播放器,由于要制作通用的播放控制类,IMusicControlService不开放播放器对象的公共访问,而是通过暴露方法操作播放器对象。

在跨平台中的实现:

  • Android平台使用Android.Media.MediaPlayer类
  • iOS平台使用AVFoundation.AVAudioPlayer类
  • Windows平台使用Windows.Media.Playback.MediaPlayer类

虽然不同平台的播放器类都提供了诸如播放,暂停,寻迹的功能,但不同平台存在着微小差别。
比如停止功能 - Stop:

在iOS中的实现:

public partial void Stop()
{
    if (!IsInitFinished()) { return; }

    if (CurrentIosPlayer.Playing)
    {
        CurrentIosPlayer.Stop();
        OnPlayStatusChanged?.Invoke(this, false);

    }
}

在Android中,由于Android.Media.MediaPlayer没有提供Stop方法,所以停止的逻辑用寻迹至0位置暂停实现的

public partial void Stop()
{
    if (CurrentAndroidPlayer.IsPlaying)
    {
        CurrentAndroidPlayer.SeekTo(0);
        CurrentAndroidPlayer.Pause();

    }
}

又如寻迹功能 - SeekTo
在iOS中的实现,postion参数为曲目开始后的时间值,单位秒。改变播放位置是通过直接赋值AVFoundation.AVAudioPlayer.CurrentTime实现的

public partial void SeekTo(double position)

{
    if (!IsInitFinished()) { return; }
    CurrentIosPlayer.CurrentTime = position;
}

在Android中,Android.Media.MediaPlayer提供了SeekTo方法,传入值是毫秒,因此要做一下转换:

public partial void SeekTo(double position)
{
    CurrentAndroidPlayer.SeekTo((int)position * 1000);

}

在传统播放器随机播放时,如果下一曲不是我想听的,我仍然想听上一曲,由于上一曲按钮是随机触发的时机,你可能找不到它了,不得不再音乐列表再搜索它。这可能是个遗憾

我在这个随机模型中引入随机播放映射表,使得在随机模式中,上一曲/下一曲仍然能发挥其作用。

刷新随机列表:
increment为跳转步数,例如increment = 1时相当于下一曲,increment = -1 时相当于上一曲:

private partial int GetShuffleMusicIndex(int originItem, int increment)
{
    var originItemIndex = 0;

    foreach (var item in ShuffleMap)
    {
        if (originItem == item)
        {
            break;
        }
        originItemIndex++;
    }
    var newItemIndex = originItemIndex + increment;
    if (newItemIndex < 0)
    {
        newItemIndex = LastIndex;
    }
    if (newItemIndex > LastIndex)
    {
        newItemIndex = 0;
    }
    var shuffleMapCount = shuffleMap.Count();

    var musicInfosCount = MusicInfos.Count();

    if (shuffleMapCount != musicInfosCount)
    {
        shuffleMap = CommonHelper.GetRandomArry(0, LastIndex);
        shuffleMapCount = shuffleMap.Count();
    }

    if (shuffleMapCount > 0 && newItemIndex < shuffleMapCount)
    {
        var resultContent = ShuffleMap[newItemIndex];
        return resultContent;
    }
    else
    {
        return -1;
    }
}


GetRandomArry 方法将产生一个指定最小值到最大值连续数列的随机数组

public static int[] GetRandomArry(int minval, int maxval)
{

    int[] arr = new int[maxval - minval + 1];
    int i;
    //初始化数组
    for (i = 0; i <= maxval - minval; i++)
    {
        arr[i] = i + minval;
    }
    //随机数
    Random r = new Random();
    for (int j = maxval - minval; j >= 1; j--)
    {
        int address = r.Next(0, j);
        int tmp = arr[address];
        arr[address] = arr[j];
        arr[j] = tmp;
    }
    //输出
    foreach (int k in arr)
    {
        Debug.WriteLine(k + " ");
    }
    return arr;
}

关键属性:

  • ShuffleMap - 随机播放映射表
  • MusicInfos - 播放器音频列表
  • LastIndex - 当前播放曲目位于器音频列表位置角标

关键方法:

  • Play - 播放
  • PauseOrResume - 暂停/恢复
  • RebuildMusicInfos - 从播放列队中读取音频列表,刷新播放器队列
  • SeekTo - 快进快退(寻迹)
  • GetNextMusic - 获取下一首曲目信息
  • GetPreMusic - 获取上一首曲目信息
  • InitPlayer - 初始化播放器
  • UpdateShuffleMap - 更新随机播放映射表
  • SetRepeatOneStatus - 设置是否单曲循环
  • Duration - 获取当前曲目时长
  • CurrentTime - 获取当前曲目播放进度
  • IsPlaying - 获取是否在播放中
  • IsInitFinished - 获取是否完成播放器初始化

关键事件:

  • OnPlayFinished - 完成当前曲目播放时触发
  • OnRebuildMusicInfosFinished - 完成刷新播放器队列触发
  • OnProgressChanged - 播放进度更改时触发
  • OnPlayStatusChanged - 播放状态变更时触发

接口定义:

public interface IMusicControlService
{
    event EventHandler<bool> OnPlayFinished;
    event EventHandler OnRebuildMusicInfosFinished;
    event EventHandler<double> OnProgressChanged;
    event EventHandler<bool> OnPlayStatusChanged;
    public IMusicInfoManager MusicInfoManager { get; set; }

    int[] ShuffleMap { get; }
    List<MusicInfo> MusicInfos { get; }
    int LastIndex { get; }
    Task RebuildMusicInfos(Action callback);
    void SeekTo(double position);
    MusicInfo GetNextMusic(MusicInfo current, bool isShuffle);
    MusicInfo GetPreMusic(MusicInfo current, bool isShuffle);
    int GetMusicIndex(MusicInfo musicInfo);
    MusicInfo GetMusicByIndex(int index);
    Task InitPlayer(MusicInfo musicInfo);
    void Play(MusicInfo currentMusic);
    void Stop();
    void PauseOrResume();
    void PauseOrResume(bool status);
    Task UpdateShuffleMap();
    void SetRepeatOneStatus(bool isRepeatOne);
    double Duration();
    double CurrentTime();
    bool IsPlaying();
    bool IsInitFinished();
}

曲目管理器设计

IMusicInfoManager:
曲目管理类,用于歌曲队列,歌单的编辑;各曲目集合增加,删除等功能

歌曲队列,歌单等信息存在于本地数据库,曲目管理类将对这些数据增、删、查、改的操作,Abp框架实现的仓储模式为我们生成了Repository对象。

在MusicInfoManager构造函数中注入各仓储依赖

public MusicInfoManager(IRepository<Queue, long> queueRepository,
    IRepository<PlaylistItem, long> playlistItemRepository,
    IRepository<Playlist, long> playlistRepository,
    IUnitOfWorkManager unitOfWorkManager
    )
{
    ...
}

读取播放队列

播放队列具有一定的代表性,歌单的逻辑与播放队列类似,所以本篇博文着重讲述播放队列的业务

播放队列存在于本地数据库的Queue表中,全部将他们读取。

播放队列的Entry项和设备中的媒体条目是一种弱关联,需要将他们“螯合”起来,连表左联查询后取得MusicInfo集合。

[UnitOfWork]
public async Task<List<MusicInfo>> GetQueueEntry()
{
    var queueEntrys = await queueRepository.GetAll().ToListAsync();
    if (_musicInfos == null || _musicInfos.Count == 0)
    {
        var isSucc = await GetMusicInfos();
        if (!isSucc.IsSucess)
        {
            //CommonHelper.ShowNoAuthorized();
        }
        _musicInfos = isSucc.Result;

    }
    var result =
        from queue in queueEntrys
        join musicInfo in _musicInfos
        on queue.MusicInfoId equals musicInfo.Id
        orderby queue.Rank
        select musicInfo;
    return result.ToList();
}

返回时依据Rank字段递增排序。

添加播放队列

播放整个专辑时,将整个专辑中的所有曲目添加到播放队列:

QueueAllAction在点击播放专辑时触发,首先清空当前播放队列,接着将当前页面绑定的曲目集合(Musics对象)插入到播放队列

private async void QueueAllAction(object obj)
{
    await MusicInfoManager.ClearQueue();
    var result = await MusicInfoManager.CreateQueueEntrys(Musics);

    ..
}

MusicInfoManager.cs 中定义了清空播放队列ClearQueue,和歌单中创建曲目集合方法CreateQueueEntrys:

[UnitOfWork]
public async Task ClearQueue()
{
    await queueRepository.DeleteAsync(c => true);

}
[UnitOfWork]
public async Task<bool> CreateQueueEntrys(List<MusicInfo> musicInfos)
{
    var lastItemRank = queueRepository.GetAll().OrderBy(c => c.Rank).Select(c => c.Rank).LastOrDefault();
    var entrys = new List<Queue>();
    foreach (var music in musicInfos)
    {
        var entry = new Queue(music.Title, lastItemRank, music.Id);
        lastItemRank++;
        entrys.Add(entry);
    }
    await queueRepository.GetDbContext().AddRangeAsync(entrys);
    return true;
}

需要注意的是,Rank字段将在队列最后一条后继续递增

曲目排序

曲目排序,原理是通过交换位置实现的,iOS和Android平台都有自己的可排序列表控件,在对选中的条目进行排序(往往是提起条目-拖拽-释放)的过程中,触发事件往往提供当前条目oldMusicInfo,和排斥条目newMusicInfo,调用ReorderQueue时将这辆个参数传入,将这两个MusicInfo的Rank值交换:

[UnitOfWork]
public void ReorderQueue(MusicInfo oldMusicInfo, MusicInfo newMusicInfo)
{
    var oldMusic = queueRepository.FirstOrDefault(c => c.MusicTitle==oldMusicInfo.Title);
    var newMusic = queueRepository.FirstOrDefault(c => c.MusicTitle==newMusicInfo.Title);
    if (oldMusic ==null || newMusic==null)
    {
        return;
    }
    var oldRank = oldMusic.Rank;
    oldMusic.Rank=newMusic.Rank;
    newMusic.Rank=oldRank;
    queueRepository.Update(oldMusic);
    queueRepository.Update(newMusic);
}

下一首播放

下一首播放将播放队列中,指定的曲目排在当前播放曲目之后,实现方式是线确保目标曲目存在于播放队列。同样,用到了排序逻辑,再将他的排序(Rank值)与当前播放曲目之后的做交换。

public partial async Task<bool> InsertToEndQueueEntry(MusicInfo musicInfo)
{
    var result = false;
    var isSuccessCreate = false;
    //如果没有则先创建
    if (!await GetIsQueueContains(musicInfo.Title))
    {
        isSuccessCreate = await CreateQueueEntry(musicInfo);
        await unitOfWorkManager.Current.SaveChangesAsync();
    }
    else
    {
        isSuccessCreate = true;
    }
    //确定包含后与下一曲交换位置
    if (isSuccessCreate)
    {
        var current = currentMusic;
        Queue newMusic = null;
        var lastItem = await queueRepository.FirstOrDefaultAsync(c => c.MusicTitle==current.Title);
        if (lastItem!=null)
        {
            newMusic = await queueRepository.FirstOrDefaultAsync(c => c.Rank==lastItem.Rank+1);
        }

        var oldMusic = await queueRepository.FirstOrDefaultAsync(c => c.MusicTitle==musicInfo.Title);

        if (oldMusic ==null || newMusic==null)
        {
            return true;
        }
        var oldRank = oldMusic.Rank;
        oldMusic.Rank=newMusic.Rank;
        newMusic.Rank=oldRank;
        queueRepository.Update(oldMusic);
        queueRepository.Update(newMusic);

        result = true;
    }
    else
    {
        result = false;
    }
    return result;
}

其它关键方法:

  • ClearQueue - 从播放队列中清除所有曲目
  • CreatePlaylist - 创建歌单
  • CreatePlaylistEntry - 歌单中创建曲目
  • CreatePlaylistEntrys - 歌单中创建曲目集合
  • CreatePlaylistEntrysToMyFavourite - “我最喜爱”中插入曲目集合
  • CreateQueueEntry - 播放队列中创建曲目
  • CreateQueueEntrys - 播放队列中创建曲目集合
  • DeleteMusicInfoFormQueueEntry - 从队列中删除指定曲目
  • DeletePlaylist - 删除歌单
  • DeletePlaylistEntry - 从歌单中删除曲目
  • DeletePlaylistEntryFromMyFavourite - 从“我最喜爱”中删除曲目
  • GetMusicInfos - 获取曲目集合
  • GetAlbumInfos - 获取专辑集合
  • GetArtistInfos - 获取艺术家集合
  • GetAlphaGroupedMusicInfo - 获取分组包装好的曲目集合
  • GetAlphaGroupedAlbumInfo - 获取分组包装好的专辑集合
  • GetAlphaGroupedArtistInfo - 获取分组包装好的艺术家集合
  • GetIsMyFavouriteContains - 曲目是否包含在"我最喜爱"中
  • GetIsPlaylistContains - 曲目是否包含在歌单中
  • GetIsQueueContains - 曲目是否包含在播放队列中
  • GetPlaylist - 获取歌单列表
  • GetPlaylistEntry - 获取歌单列表
  • GetPlaylistInfo - 获取歌单中的曲目
  • GetQueueEntry - 获取播放队列中的曲目
  • InsertToEndQueueEntry - 插入曲目到播放队列中的末尾
  • InsertToEndQueueEntrys - 插入曲目集合到播放队列中的末尾
  • InsertToNextQueueEntry - 插入曲目到队列中的下一曲(下一首播放)
  • UpdatePlaylist - 更新歌单信息

接口定义:

public interface IMusicInfoManager
{
    Task ClearQueue();
    Task<bool> CreatePlaylist(Playlist playlist);
    Task<bool> CreatePlaylistEntry(MusicInfo musicInfo, long playlistId);
    Task<bool> CreatePlaylistEntrys(List<MusicInfo> musics, long playlistId);
    Task<bool> CreatePlaylistEntrys(MusicCollectionInfo musicCollectionInfo, long playlistId);
    Task<bool> CreatePlaylistEntrysToMyFavourite(List<MusicInfo> musics);
    Task<bool> CreatePlaylistEntrysToMyFavourite(MusicCollectionInfo musicCollectionInfo);
    Task<bool> CreatePlaylistEntryToMyFavourite(MusicInfo musicInfo);
    Task<bool> CreateQueueEntry(MusicInfo musicInfo);
    Task<bool> CreateQueueEntrys(List<MusicInfo> musicInfos);
    Task<bool> CreateQueueEntrys(MusicCollectionInfo musics);
    Task<bool> DeleteMusicInfoFormQueueEntry(MusicInfo musicInfo);
    Task<bool> DeleteMusicInfoFormQueueEntry(string musicTitle);
    Task<bool> DeletePlaylist(long playlistId);
    Task<bool> DeletePlaylist(Playlist playlist);
    Task<bool> DeletePlaylistEntry(MusicInfo musicInfo, long playlistId);
    Task<bool> DeletePlaylistEntry(string musicTitle, long playlistId);
    Task<bool> DeletePlaylistEntryFromMyFavourite(MusicInfo musicInfo);
    Task<InfoResult<List<AlbumInfo>>> GetAlbumInfos();
    Task<AlphaGroupedObservableCollection<AlbumInfo>> GetAlphaGroupedAlbumInfo();
    Task<AlphaGroupedObservableCollection<ArtistInfo>> GetAlphaGroupedArtistInfo();
    Task<AlphaGroupedObservableCollection<MusicInfo>> GetAlphaGroupedMusicInfo();
    Task<InfoResult<List<ArtistInfo>>> GetArtistInfos();
    Task<bool> GetIsMyFavouriteContains(MusicInfo musicInfo);
    Task<bool> GetIsMyFavouriteContains(string musicTitle);
    Task<bool> GetIsPlaylistContains(MusicInfo musicInfo, long playlistId);
    Task<bool> GetIsPlaylistContains(string musicTitle, long playlistId);
    Task<bool> GetIsQueueContains(string musicTitle);
    Task<InfoResult<List<MusicInfo>>> GetMusicInfos();
    Task<List<Playlist>> GetPlaylist();
    Task<List<MusicInfo>> GetPlaylistEntry(long playlistId);
    Task<List<MusicInfo>> GetPlaylistEntryFormMyFavourite();
    Task<List<PlaylistInfo>> GetPlaylistInfo();
    Task<List<MusicInfo>> GetQueueEntry();
    Task<bool> InsertToEndQueueEntry(MusicInfo musicInfo);
    Task<bool> InsertToEndQueueEntrys(List<MusicInfo> musicInfos);
    Task<bool> InsertToNextQueueEntry(MusicInfo musicInfo, MusicInfo currentMusic);
    Task<bool> UpdatePlaylist(Playlist playlist);
}

获取本地音乐

Android中的实现

在Android平台中MatoMusic.Core\Platforms\Android\MusicInfoManager.cs

MediaStore类是Android平台的多媒体数据库,它包含了音频,视频,图片等所有多媒体文件信息。

Android扫描服务会在后台自动扫描设备文件资源,将设备上的音乐媒体信息加入到MediaStore数据库中。应用程序通过Android平台提供的ContentProvider包含的API直接从MediaStore中读取相应的媒体信息。

获取设备多媒体信息的实现方式如下:

public IList<MusicInfo> GetAllSongs()
{

    IList<MusicInfo> songs = new ObservableCollection<MusicInfo>();
    ICursor mediaCursor, genreCursor, albumCursor;

    mediaCursor = Application.Context.ContentResolver.Query(
        MediaStore.Audio.Media.ExternalContentUri,
        _mediaProjections, null, null,
        MediaStore.Audio.Media.InterfaceConsts.TitleKey);

    int artistColumn = mediaCursor.GetColumnIndex(MediaStore.Audio.Media.InterfaceConsts.Artist);
    int albumColumn = mediaCursor.GetColumnIndex(MediaStore.Audio.Media.InterfaceConsts.Album);
    int titleColumn = mediaCursor.GetColumnIndex(MediaStore.Audio.Media.InterfaceConsts.Title);
    int durationColumn = mediaCursor.GetColumnIndex(MediaStore.Audio.Media.InterfaceConsts.Duration);
    int uriColumn = mediaCursor.GetColumnIndex(MediaStore.Audio.Media.InterfaceConsts.Data);
    int idColumn = mediaCursor.GetColumnIndex(MediaStore.Audio.Media.InterfaceConsts.Id);
    int isMusicColumn = mediaCursor.GetColumnIndex(MediaStore.Audio.Media.InterfaceConsts.IsMusic);
    int albumIdColumn = mediaCursor.GetColumnIndex(MediaStore.Audio.Media.InterfaceConsts.AlbumId);

    int isMusic;
    ulong duration, id;
    string artist, album, title, uri, genre, artwork, artworkId;

    if (mediaCursor.MoveToFirst())
    {
        do
        {
            isMusic = int.Parse(mediaCursor.GetString(isMusicColumn));
            if (isMusic != 0)
            {
                ulong.TryParse(mediaCursor.GetString(durationColumn),out duration);
                artist = mediaCursor.GetString(artistColumn);
                album = mediaCursor.GetString(albumColumn);
                title = mediaCursor.GetString(titleColumn);
                uri = mediaCursor.GetString(uriColumn);
                ulong.TryParse(mediaCursor.GetString(idColumn), out id);
                artworkId = mediaCursor.GetString(albumIdColumn);

                genreCursor = Application.Context.ContentResolver.Query(
                    MediaStore.Audio.Genres.GetContentUriForAudioId("external", (int)id),
                    _genresProjections, null, null, null);
                int genreColumn = genreCursor.GetColumnIndex(MediaStore.Audio.Genres.InterfaceConsts.Name);
                if (genreCursor.MoveToFirst())
                {
                    genre = genreCursor.GetString(genreColumn) ?? string.Empty;
                }
                else
                {
                    genre = string.Empty;
                }
                //https://stackoverflow.com/questions/63181820/why-is-album-art-the-only-field-that-returns-null-from-mediastore-when-others-ar

                ImageSource artworkImage = null;

                if (DeviceInfo.Version.Major < 10)
                {
                    albumCursor = Application.Context.ContentResolver.Query(
                        MediaStore.Audio.Albums.ExternalContentUri,
                        _albumProjections,
                        $"{MediaStore.Audio.Albums.InterfaceConsts.Id}=?",
                        new string[] { artworkId },
                        null);
                    int artworkColumn = albumCursor.GetColumnIndex(MediaStore.Audio.Media.InterfaceConsts.AlbumArt);
                    if (albumCursor.MoveToFirst())
                    {
                        artwork = albumCursor.GetString(artworkColumn) ?? string.Empty;
                    }
                    else
                    {
                        artwork = String.Empty;
                    }

                    albumCursor?.Close();
                    artworkImage = artwork;

                }
                else
                {
                    var extUrl = MediaStore.Audio.Albums.ExternalContentUri;
                    var albumArtUri = ContentUris.WithAppendedId(extUrl, long.Parse(artworkId));

                    try
                    {
                        //var art = System.IO.Path.Combine (Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "albumart" + artworkId + ".jpg");
                        var art = System.IO.Path.Combine(Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDocuments).AbsolutePath, "albumart" + artworkId + ".jpg");

                        var bitmap = Application.Context.ContentResolver.LoadThumbnail(albumArtUri, new Android.Util.Size(1024, 1024), null);
                        var h = bitmap.Height;
                        var w = bitmap.Width;
                        var bb = bitmap.ByteCount;

                        using (Stream ms = new FileStream(art, FileMode.Create))
                        {
                            bitmap.Compress(Bitmap.CompressFormat.Png, 100, ms);
                            bitmap.Recycle();
                        }
                        artworkImage = art;
                    }
                    catch (Exception e)
                    {
                        System.Console.WriteLine(e.Message);
                    }
                }
                songs.Add(new MusicInfo()
                {
                    Id = (int)id,
                    Title = title,
                    Artist = artist,
                    AlbumTitle = album,
                    Genre = genre,
                    Duration = duration / 1000,
                    Url = uri,
                    AlbumArt = artworkImage
                });
                genreCursor?.Close();
            }
        } while (mediaCursor.MoveToNext());
    }
    mediaCursor?.Close();

    return songs;

}

获取音乐信息集合

public partial async Task<InfoResult<List<MusicInfo>>> GetMusicInfos()
{
    List<MusicInfo> musicInfos;

    var result = false;

    if (await MediaLibraryAuthorization())
    {

        musicInfos = await Task.Run(() =>
        {
            var Infos = (from item in GetAllSongs()

                            select new MusicInfo()
                            {
                                Id = item.Id,
                                Title = item.Title,
                                Duration = item.Duration,
                                Url = item.Url,
                                AlbumTitle = item.AlbumTitle,
                                Artist = item.Artist,
                                AlbumArt = item.AlbumArt,
                                GroupHeader = GetGroupHeader(item.Title),
                                IsFavourite = GetIsMyFavouriteContains(item.Title).Result,
                                IsInitFinished = true
                            }).ToList();
            return Infos;
        });

        result = true;

    }
    else
    {
        musicInfos = new List<MusicInfo>();
        result = false;
    }
    return new InfoResult<List<MusicInfo>>(result, musicInfos);

}

iOS中的实现

在iOS平台中MatoMusic.Core\Platforms\iOS\MusicInfoManager.cs

在iOS平台中获取音乐信息要简单得多,MPMediaQuery这个类获取通系统自带的‘音乐’软件下载的,或通过iTunes导入的本地歌曲文件

MPMediaQuery 类使用方式可以参考官方文档

获取音乐信息集合

public partial async Task<InfoResult<List<MusicInfo>>> GetMusicInfos()
{
    List<MusicInfo> musicInfos;

    var result = false;
    if (await MediaLibraryAuthorization())
    {
        musicInfos = await Task.Run(() =>
        {
            var Infos = (from item in MediaQuery.Items
                            where item.MediaType == MPMediaType.Music
                            select new MusicInfo()
                            {
                                Id = (int)item.PersistentID,
                                Title = item.Title,
                                Url = item.AssetURL.ToString(),
                                Duration = Convert.ToUInt64(item.PlaybackDuration),

                                AlbumTitle = item.AlbumTitle,
                                Artist = item.Artist,
                                AlbumArt = GetAlbumArtSource(item),
                                GroupHeader = GetGroupHeader(item.Title),
                                IsFavourite = GetIsMyFavouriteContains(item.Title).Result,
                                IsInitFinished = true

                            }).ToList();
            return Infos;
        });

        result = true;

    }
    else
    {
        musicInfos = new List<MusicInfo>();
        result = false;
    }
    return new InfoResult<List<MusicInfo>>(result, musicInfos);

}

Windows中的实现

在Windows设备中,需要指定一个主目录来扫描音乐文件,我们指定一个缺省目录,如“音乐”文件夹(KnownFolders.MusicLibrary),好跟之前两个平台的行为保持一致

private async Task<List<MusicInfo>> SetMusicListAsync(StorageFolder musicFolder = null)
{

    var localSongs = new List<MusicInfo>();
    List<StorageFile> songfiles = new List<StorageFile>();
    if (musicFolder == null)
    {
        musicFolder = KnownFolders.MusicLibrary;
    }

    await GetLocalSongsAysnc(songfiles, musicFolder);
    localSongs = await PopulateSongListAsync(songfiles);

    return localSongs;

}

递归调用GetLocalSongsAysnc,遍历主目录以及其子目录的所有.mp3文件

private async Task GetLocalSongsAysnc(List<StorageFile> songFiles, StorageFolder parent)
{
    foreach (var item in await parent.GetFilesAsync())
    {
        if (item.FileType == ".mp3")
            songFiles.Add(item);
    }
    foreach (var folder in await parent.GetFoldersAsync())
    {
        await GetLocalSongsAysnc(songFiles, folder);
    }
}

从本地文件读取音频信息,转成曲目信息

private async Task<List<MusicInfo>> PopulateSongListAsync(List<StorageFile> songFiles)
{

    var localSongs = new List<MusicInfo>();
    int Id = 1;

    foreach (var file in songFiles)
    {
        MusicInfo song = new MusicInfo();

        // 1. 获取文件信息
        MusicProperties musicProperty = await file.Properties.GetMusicPropertiesAsync();
        if (!string.IsNullOrEmpty(musicProperty.Title))
            song.Title = musicProperty.Title;
        else
        {
            song.Title = file.DisplayName;
        }

        StorageItemThumbnail currentThumb = await file.GetThumbnailAsync(ThumbnailMode.MusicView, 60, ThumbnailOptions.UseCurrentScale);

        // 2.将文件信息转换为数据模型

        string coverUri = "ms-appx:///Assets/Default/Default.jpg";

        song.Id = Id;
        song.Url = file.Path;
        song.GroupHeader = GetGroupHeader(song.Title);
        if (!string.IsNullOrEmpty(musicProperty.Artist))
            song.Artist = musicProperty.Artist;
        else
            song.Artist = "未知歌手";
        if (!string.IsNullOrEmpty(musicProperty.Album))
            song.AlbumTitle = musicProperty.Album;
        else
            song.AlbumTitle = "未知唱片";
        song.Duration = (ulong)musicProperty.Duration.TotalSeconds;


        //3. 添加至UI集合中

        var task01 = SaveImagesAsync(file, song);
        var result = await task01;
        var task02 = task01.ContinueWith((e) =>
            {
                if (result.IsSucess)
                {
                    song.AlbumArtPath = result.Result;
                }
                else
                {
                    song.AlbumArtPath = coverUri;

                }
            });

        Task.WaitAll(task01, task02);
        song.IsInitFinished = true;
        localSongs.Add(song);
        Id++;

    }
    return localSongs;
}

获取音乐信息集合

public partial async Task<InfoResult<List<MusicInfo>>> GetMusicInfos()
{
    List<MusicInfo> musicInfos;
    var result = false;
    if (await MediaLibraryAuthorization())
    {
        musicInfos = await SetMusicListAsync();
        result = true;
    }
    else
    {
        musicInfos = new List<MusicInfo>();
        result = false;
    }
    return new InfoResult<List<MusicInfo>>(result, musicInfos);

}

获取专辑和艺术家

专辑信息包含了音乐集合

获取专辑和艺术家的跨平台的实现方式大同小异,以Android平台为例

GetAlbumInfos方法用于获取AlbumInfo集合

public partial async Task<InfoResult<List<AlbumInfo>>> GetAlbumInfos()
{
    List<AlbumInfo> albumInfo;
    var result = false;

    if (await MediaLibraryAuthorization())
    {
        var isSucc = await GetMusicInfos();
        if (!isSucc.IsSucess)
        {
            //CommonHelper.ShowNoAuthorized();

        }
        albumInfo = await Task.Run(() =>
        {
            var info = (from item in isSucc.Result
                        group item by item.AlbumTitle
                into c
                        select new AlbumInfo()
                        {
                            Title = c.Key,
                            GroupHeader = GetGroupHeader(c.Key),

                            AlbumArt = c.FirstOrDefault().AlbumArt,
                            Musics = new ObservableCollection<MusicInfo>(c.Select(d => new MusicInfo()
                            {
                                Id = d.Id,
                                Title = d.Title,
                                Duration = d.Duration,
                                Url = d.Url,
                                AlbumTitle = d.AlbumTitle,
                                Artist = d.Artist,
                                AlbumArt = d.AlbumArt,
                                IsFavourite = GetIsMyFavouriteContains(d.Title).Result,
                                IsInitFinished = true
                            }))

                        }).ToList();
            return info;
        });

        result = true;

    }
    else
    {
        albumInfo = new List<AlbumInfo>();
        result = false;
    }
    return new InfoResult<List<AlbumInfo>>(result, albumInfo);

}

GetArtistInfos方法用于获取ArtistInfo集合

public partial async Task<InfoResult<List<ArtistInfo>>> GetArtistInfos()
{
    List<ArtistInfo> artistInfo;
    var result = false;
    if (await MediaLibraryAuthorization())
    {
        var isSucc = await GetMusicInfos();
        if (!isSucc.IsSucess)
        {
            //CommonHelper.ShowNoAuthorized();

        }
        artistInfo = await Task.Run(() =>
        {

            var info = (from item in isSucc.Result
                        group item by item.Artist
                into c
                        select new ArtistInfo()
                        {
                            Title = c.Key,
                            GroupHeader = GetGroupHeader(c.Key),
                            Musics = new ObservableCollection<MusicInfo>(c.Select(d => new MusicInfo()
                            {
                                Id = d.Id,
                                Title = d.Title,
                                Duration = d.Duration,
                                Url = d.Url,
                                AlbumTitle = d.AlbumTitle,
                                Artist = d.Artist,
                                AlbumArt = d.AlbumArt,
                                IsFavourite = GetIsMyFavouriteContains(d.Title).Result,
                                IsInitFinished = true

                            }))

                        }).ToList();
            return info;
        });
        result = true;

    }
    else
    {
        artistInfo = new List<ArtistInfo>();
        result = false;
    }
    return new InfoResult<List<ArtistInfo>>(result, artistInfo);
}

项目地址

GitHub:MatoMusic

MAUI 中创建录音机和播放器应用 在本博客中,你将了解如何在 .NET MAUI 中开发录音机和播放器。音频播放器将录制和播放音频文件。此应用程序可以在Android和iOS上部署和使用。 阅读详情

相关推荐

桌面应用窗体平滑移动实现:从动画原理到多方案实战

在图形用户界面开发中,动画是实现流畅交互体验的核心技术之一,其本质是通过插值与时序控制,在连续时间帧中渲染中间状态,形成视觉上的连贯运动。从技术原理看,这涉及消息循环、渲染管线与性能优化的协同,直接影响应用的响应度与专业感。在工程实践中,开发者常需在计时器驱动、游戏循环、现代UI框架动画引擎及DirectX渲染等方案中权衡,以适配WinForms、WPF等不同技术栈,并应对异形窗体、多显示器DPI缩放等实际场景。本文聚焦窗体平滑移动这一典型需求,通过封装可复用的动画助手类,系统解析了缓动函数、双缓冲防闪烁、

weixin_30887919的博客 326

使用新的MediaElement在.NET MAUI应用中播放音频和视频

​最受期待的.NET MAUI控件之一已发布; MediaElement。使用MediaElement,您可以在.NET MAUI应用程序中轻松播放音频和视频,在这篇文章中,您将了解有关第一个版本以及我们对未来的计划的所有信息!媒体元素是.NET MAUI社区工具包的一部分,该工具包是一个社区创建的库,由来自社区和Microsoft的全球优秀开发人员提供支持。 什么是MediaElement?

寒冰屋的专栏 1580

五大开源播放器内核深度评测:ijkPlayer、VLC、ExoPlayer、MediaPlayer与SmartPlayer的实战对比

本文深度评测了五大主流开源播放器内核:ijkPlayer、VLC、ExoPlayer、MediaPlayer与SmartPlayer。从集成难度、性能表现、功能特性及适用场景多维度进行实战对比,为开发者提供选型决策指南。其中,ExoPlayer凭借其现代化架构和出色的首帧速度,成为现代流媒体应用开发的热门选择。

weixin_28335283的博客 459

[MAUI 项目实战] 音乐播放器(一):概述与架构

为什么想起来这个项目了呢? 这是一个Windows Phone 8的老项目,2014年用作为兴趣写了个叫“番茄播放器”的App,顺便提高编程技能。 这个项目的架构历经多次迁移,从WP8到UWP再到Xamarin.Forms。去年底随着MAUI的正式发布,又尝试把它迁移到MAUI上来。

jevonsflash的专栏 5828

【MAUI】MediaElement在.NET MAUI应用中播放音频和视频

使用MediaElement,您可以获得一个强大的控件,允许您在.NET MAUI应用内播放多媒体。你可能已经从Xamarin社区工具包中知道MediaElement,它是由社区成员Peter Foot的出色工作添加的。虽然那个版本已经相当不错了,但它也有改进的余地,尤其是在Android上。这就是为什么在将MediaElement移植到.NET MAUI时,我们从头开始重建了所有内容。通过这种方式,我们可以确保保留所有已经好的部分,同时改进可能需要一些爱的东西。

德仔 726

Android 蓝牙实战——蓝牙音乐播放/暂停状态(二十二)

对于蓝牙音乐的播放状态,我们首先主要处理的是 onPlayStatusChanged() 回调,这是协议栈通知 FW 层的一个回调接口。还有一个就是 getPlayBackState() 方法,这是媒体应用在初始化时未收到回调信息主动获取当前状态的方法。我们这里就来分析一下这两个状态的获取流程。

小旭的专栏 1772

[MAUI 项目实战] 手势控制音乐播放器(一): 概述与架构

这是一篇系列博文。请关注我,学习更多.NET MAUI开发知识!在之前的博文中提到这个项目,它是为音乐播放器专门开发的基于手势控制的UI界面。此UI界面可以让用户在不看屏幕的情况下,通过手势来控制音乐播放器的各种操作,如播放、暂停、下一首、上一首。手势来控制的交互方式适合不方便看手机屏幕时简单的音乐播放需求,在驾车、运动等场景下有较好的用户体验。

jevonsflash的专栏 1358

ST-CNN实战:用Python实现交通流量预测(附完整代码)

本文详细介绍了如何利用时空卷积神经网络(ST-CNN)进行交通流量预测的实战过程。通过Python和PyTorch,从数据预处理、构建邻接矩阵,到ST-CNN核心模块的代码实现与模型训练,提供了完整的解决方案,帮助开发者掌握处理时空序列数据的核心算法。

rust6ferris的博客 925

[MAUI 项目实战] 音乐播放器(三):界面交互

UI设计的本质是对于产品的理解在界面中多种形式的映射,当需求和定位不同时,对相同的功能表达出了不同的界面和交互方式。作为播放器,界面可以是千差万别的。《番茄播放器》的iOS平台上我开发了传统版本,和基于手势播放的版本。它们界面不同,但用的同一个播放内核。作为播放内核项目,在MatoMusic.Core的工作已经结束。本系列博文重点还是在播放器思路的解读,关于MAUI动画交互,我打算有时间另外写博客(这里给自己挖个坑)。

jevonsflash的专栏 2014

【Macbook-Latex安装部署教程】【Win-Texlive镜像下载】【简单易上手】MacTex+Vscode/Texstudio

该教程记录自己在mac()上安装部署Latex环境的经验,整个教程操作简单,配置要求少,易上手。教程分成三部分:Mactex下载与安装,Vscode插件下载与配置文件修改,Texstudio安装。

sunriseOvO的博客 2568

WinIDEA基于IC5000烧录代码和调试教程

winIDEA就是ic5000用来烧录代码和调试代码的上位机,本文旨在介绍其如何去烧录代码并且调试

m0_56208280的博客 4163

[MAUI]模仿网易云音乐黑胶唱片的交互实现

由此可以无限的拨动唱盘实现连续切歌的效果。用当前唱盘的“瞬移”,看起来像唱盘被影子唱盘替换掉了,但是在屏幕中心活动的拖拽物,一直是真正的那个控件。结合上一小节写的三个PitGrid,此时拖拽唱盘,并且在拖拽开始,进入pit,离开pit,释放时,分别触发Start,In,Out,Over四个状态事件。此时已经实现了拖拽唱盘的基本功能,但是在释放唱盘时,影子唱盘并没有如预期那样移动到MiddlePit的中心点。注意,当音乐暂停后,停止旋转动画,当音乐恢复播放时,转盘应从之前停止的角度开始启动旋转动画。

jevonsflash的专栏 1102

DSP篇--C6678功能调试系列之EDMA3调试

EDMA主要用于数据的搬运,发送、接收,其搬运数据的形式有;AB模式,ABC模式以及ping-pong模式。 EDMA3分初始化、参数配置、以及中断配置。其中中断配置则需要注意的内容主要有:挂载的TC通道,对应的触发事件,以及中断的使能。

nanke_yh的博客 1720

Nitrux OS简介

Nitrux OS 是一个基于 Debian 的 Linux 发行版,官网:https://nxos.org/定制桌面环境:AppImage 软件包:无 systemd 的发行版:MauiKit 和 Maui Apps:最新的 Debian 基础和 Liquorix 内核:不可变 Linux 发行版:性能增强:系统完整性:VMetal 虚拟化功能:MauiKit 构建的应用程序:

地球空间 1103

OpenClaw 源代码深度分析报告 &如何把企业自有的MCP server 工具集成到OpenClaw里? 怎样通过OpenAI 风格SDK 解析出来OpenClaw的全部的流式输出内容?

项目属性详情名称OpenClaw版本2026.3.2许可证MIT语言最低运行时包管理器pnpm主要仓库},// 注册工具// 工具实现},});// 注册命令" }),});},方面评价模块化设计⭐⭐⭐⭐⭐ 插件架构清晰,扩展性强协议设计⭐⭐⭐⭐⭐ WebSocket 协议完善,支持多种客户端多渠道支持⭐⭐⭐⭐⭐ 20+ 消息通道,覆盖主流平台安全机制⭐⭐⭐⭐ 配对认证、设备签名、权限控制完善本地优先⭐⭐⭐⭐⭐ 数据本地存储,用户完全控制项目属性详情名称OpenClaw版本。

AI天才研究院 938

雷达参数的设置

采样率又可以叫做采样速率或者,顾名思义就是每秒采集的次数。该值越高,单位时间内对信号的采集越多,采样率的单位可以用Hz或者sps表示(sps是采样率,是每秒采样点的数量,sp表示采样点数),一般1sps=1Hz。采样时间是采样点数和采样周期的乘积得到的结果。假设实际采样率为4560ksps,采样点数为256,那么采样周期为1/4560(ms),那么ADC的采样时间就为256*(1/4560)(ms)=0.056ms=56微秒。

qq_50974341的博客 1912

模仿网易云音乐黑胶唱片的交互实现

目录用过网易云音乐App的同学应该都比较熟悉它播放界面。这是一个良好的交互设计,留声机的界面隐喻准确地向人们传达产品概念和使用方法:当手指左右滑动时,便模拟了更换唱盘从而导向切换歌曲的交互功能。今天在中我们来实现这个交互效果,先来看看效果:使用实现跨平台支持,本项目可运行于Android、iOS平台。

小罗的博客 495

Transformer模型在时间序列预测中的应用:价格和产量预测

Transformer模型由Attention is All You Need论文提出,并在机器翻译任务上取得了巨大成功。其主要特点是使用自注意力机制代替传统的递归或卷积操作进行序列建模。Transformer模型由编码器和解码器组成,其中编码器用于学习输入序列的表示,解码器用于生成目标序列。在时间序列预测中,我们只需要使用Transformer的编码器部分。编码器将历史数据作为输入,学习一个表示,然后使用该表示来预测未来的数值。

HnrzLinux的博客 738

轻松在 MAUI 应用中播放音视频,MediaElement 现已发布!

最受期待的 .NET MAUI 控件之一:MediaElement 现已发布。有了 MediaElement,您可以轻松地在 .NET MAUI 应用程序中播放音频和视频,在这篇文章中,您将了解到有关 MediaElement 第一个版本的所有信息以及我们对未来的计划!MediaElement 是的一部分,这是一个社区创建的库,受到来自全球社区和 Microsoft 的优秀开发人员的支持。什么是 MediaElement?

微软技术栈 2540

如何在多 GPU 配置上运行 Unsloth:数据并行或模型并行

当工作负载受限于内存(模型或批次无法放入单张显卡)、或受限于计算且计算与通信比率较高时(即每块 GPU 的额外计算量超过同步参数的开销,如在 DDP 中,或超过传递激活值的开销,如在模型并行中),多 GPU 配置才开始显现优势。在松散连接的显卡上(尤其是 PCIe 版本的 H100 或 A100),额外的设备间数据传输会导致性能显著低于单 GPU 运行。让我们厘清概念,明确说明如何通过模型并行(将模型拆分到多个 GPU)或数据并行(在每个 GPU 上复制模型)来跨多 GPU 运行 Unsloth。

人工智能领域专家 646

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

maui 开发音乐播放APP 优化(2)

maui 播放视频,加入当前状态 显示。以及播放音乐.视频有时可以自动播放有时候要手动。

大白菜代码的博客 374

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

MAUI技术调研2——播放音频

技术调研

qq_53373885的博客 952

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

MuPlayer:一款强大的跨平台音频播放内核

**MuPlayer** 是由百度音乐前端团队开发并维护的一款浏览器端音频播放内核。它基于 HTML5 Audio 及 Flash 音频技术,旨在提供一个多端通用、浏览器兼容性强且可扩展的音频播放解决方案。尽管项目自2015年起已不再维护,但其稳定性和灵活性在多个线上产品中得到了验证,仍然是一个值得关注的开源项目。 ## 项目技术分析 ### 技术栈 - **HTML5 Audio & Fla...

gitblog_00277的博客 887

04.视频播放器通用架构实践

04.视频播放器通用架构实践 目录介绍 01.视频播放器的痛点 02.业务需求的目标 03.该播放器框架特点 04.播放器内核封装 05.播放器UI层封装 06.如何简单使用 07.如何自定义播放器 08.该案例的拓展性分享 09.关于视频缓存方案 10.如何监控视频埋点 11.待实现的需求分析 12.一些细节上优化 13.参考案例和博客记录 00.视频播放器通用框架 基础封装视频播放器player,可以在ExoPlayer、MediaPlayer,声网RTC视频播放器内核,原生MediaPlayer

杨充 2377

maui 开发音乐播放APP (1)

NET MAUI(.NET Multi-platform App UI)是由微软推出的一种多平台应用程序开发框架,用于创建可以在不同操作系统上运行的跨平台移动应用、桌面应用和Web应用。它基于.NET平台,允许开发者使用C#和XAML等熟悉的技术来构建应用程序,同时实现了一次编码、多处运行的目标。MediaElement 是.NET MAUI框架中的一个重要控件,用于处理多媒体内容。它主要用于播放音频和视频文件,提供了一种简单的方式来集成媒体播放功能到.NET MAUI应用程序中。

大白菜代码的博客 943

Android 10 手机端控制车载蓝牙音乐上一首、暂停、下一首、获取音乐信息等流程

需求:手机端音乐暂停和播放状态从服务端告诉客户端、设备端实现暂停、播放、上一首、下一首等功能 代码路径: packages/apps/Bluetooth/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java packages/apps/Bluetooth/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java packages/a

wq892373445的博客 1万+
上一篇: [MAUI 项目实战] 音乐播放器(一):概述与架构
下一篇: [学习笔记]SQL server完全备份指南
林晓lx
林晓lx 2017年优快云移动跨平台大赛优秀奖获得者 2017年优快云移动跨平台大赛优秀奖获得者
博客等级 码龄15年 3761粉丝 123原创
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林晓lx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值