最新要闻
- 女子戴金手镯做核磁共振:手腕被烫出一圈水泡
- 环球短讯!三亚旅游发现拔白发服务一小时50元 网友叹服:发量不允许
- 【全球聚看点】多地对体育中考项目作出调整:取消/选考中考男女生长跑 800米对健康不利
- 全球短讯!情人节多部爱情电影集中上映:跟邓超新电影强势对垒
- 每日热文:ChatGPT爆火!争议声也越来越大了
- 可怕又惊喜!87岁老人棺材内复生 亲属称席都吃了:目前一切正常
- 全球讯息:200M内存就能启动 Win11极限精简版升级:去除广告
- 【独家】19岁男孩患阿尔茨海默病 专家:这么做可以远离
- 全球即时:PC销量下滑 AMD的Zen2处理器重新出山:配置没法看
- 清华教授花20多万为村民3D打印住宅:直言房子一点不贵 方式会普及
- 观速讯丨雷军再次力荐小米13和Redmi K60!一小米之家上午开门就卖了7台
- 今亮点!土耳其专家称遇上地震是命遭主持人怒斥:网友热议说的没毛病
- 【天天报资讯】小伙入职1小时被HR告知招错人:补偿50元
- 当前热讯:日本大飞机失败!印度砸1000亿美元买空客波音500架飞机:为何不自研?
- 世界观点:杨元庆评兰奇:联想登顶全球PC市场的关键先生去世了
- 世界热议:女子小巷停车失误一口气撞了10辆:被救时还在踩油门
手机
iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
- 警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- 男子被关545天申国赔:获赔18万多 驳回精神抚慰金
- 3天内26名本土感染者,辽宁确诊人数已超安徽
- 广西柳州一男子因纠纷杀害三人后自首
- 洱海坠机4名机组人员被批准为烈士 数千干部群众悼念
家电
全球观焦点:深入解读.NET MAUI音乐播放器项目(二):播放内核
播放控制服务
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 OnPlayFinished; event EventHandler OnRebuildMusicInfosFinished; event EventHandler OnProgressChanged; event EventHandler OnPlayStatusChanged; public IMusicInfoManager MusicInfoManager { get; set; } int[] ShuffleMap { get; } List 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 queueRepository, IRepository playlistItemRepository, IRepository playlistRepository, IUnitOfWorkManager unitOfWorkManager ){ ...}
读取播放队列
播放队列具有一定的代表性,歌单的逻辑与播放队列类似,所以本篇博文着重讲述播放队列的业务
播放队列存在于本地数据库的Queue表中,全部将他们读取。
播放队列的Entry项和设备中的媒体条目是一种弱关联,需要将他们“螯合”起来,连表左联查询后取得MusicInfo集合。
[UnitOfWork]public async Task> 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 CreateQueueEntrys(List musicInfos){ var lastItemRank = queueRepository.GetAll().OrderBy(c => c.Rank).Select(c => c.Rank).LastOrDefault(); var entrys = new List(); 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 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 CreatePlaylist(Playlist playlist); Task CreatePlaylistEntry(MusicInfo musicInfo, long playlistId); Task CreatePlaylistEntrys(List musics, long playlistId); Task CreatePlaylistEntrys(MusicCollectionInfo musicCollectionInfo, long playlistId); Task CreatePlaylistEntrysToMyFavourite(List musics); Task CreatePlaylistEntrysToMyFavourite(MusicCollectionInfo musicCollectionInfo); Task CreatePlaylistEntryToMyFavourite(MusicInfo musicInfo); Task CreateQueueEntry(MusicInfo musicInfo); Task CreateQueueEntrys(List musicInfos); Task CreateQueueEntrys(MusicCollectionInfo musics); Task DeleteMusicInfoFormQueueEntry(MusicInfo musicInfo); Task DeleteMusicInfoFormQueueEntry(string musicTitle); Task DeletePlaylist(long playlistId); Task DeletePlaylist(Playlist playlist); Task DeletePlaylistEntry(MusicInfo musicInfo, long playlistId); Task DeletePlaylistEntry(string musicTitle, long playlistId); Task DeletePlaylistEntryFromMyFavourite(MusicInfo musicInfo); Task>> GetAlbumInfos(); Task> GetAlphaGroupedAlbumInfo(); Task> GetAlphaGroupedArtistInfo(); Task> GetAlphaGroupedMusicInfo(); Task>> GetArtistInfos(); Task GetIsMyFavouriteContains(MusicInfo musicInfo); Task GetIsMyFavouriteContains(string musicTitle); Task GetIsPlaylistContains(MusicInfo musicInfo, long playlistId); Task GetIsPlaylistContains(string musicTitle, long playlistId); Task GetIsQueueContains(string musicTitle); Task>> GetMusicInfos(); Task> GetPlaylist(); Task> GetPlaylistEntry(long playlistId); Task> GetPlaylistEntryFormMyFavourite(); Task> GetPlaylistInfo(); Task> GetQueueEntry(); Task InsertToEndQueueEntry(MusicInfo musicInfo); Task InsertToEndQueueEntrys(List musicInfos); Task InsertToNextQueueEntry(MusicInfo musicInfo, MusicInfo currentMusic); Task UpdatePlaylist(Playlist playlist);}
获取本地音乐
Android中的实现
在Android平台中MatoMusic.Core\Platforms\Android\MusicInfoManager.cs
MediaStore类是Android平台的多媒体数据库,它包含了音频,视频,图片等所有多媒体文件信息。
Android扫描服务会在后台自动扫描设备文件资源,将设备上的音乐媒体信息加入到MediaStore数据库中。应用程序通过Android平台提供的ContentProvider包含的API直接从MediaStore中读取相应的媒体信息。
获取设备多媒体信息的实现方式如下:
public IList GetAllSongs(){ IList songs = new ObservableCollection(); 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>> GetMusicInfos(){ List 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(); result = false; } return new InfoResult>(result, musicInfos);}
iOS中的实现
在iOS平台中MatoMusic.Core\Platforms\iOS\MusicInfoManager.cs
在iOS平台中获取音乐信息要简单得多,MPMediaQuery这个类获取通系统自带的‘音乐’软件下载的,或通过iTunes导入的本地歌曲文件
MPMediaQuery 类使用方式可以参考官方文档
获取音乐信息集合
public partial async Task>> GetMusicInfos(){ List 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(); result = false; } return new InfoResult>(result, musicInfos);}
Windows中的实现
在Windows设备中,需要指定一个主目录来扫描音乐文件,我们指定一个缺省目录,如“音乐”
文件夹(KnownFolders.MusicLibrary
),好跟之前两个平台的行为保持一致
private async Task> SetMusicListAsync(StorageFolder musicFolder = null){ var localSongs = new List(); List songfiles = new List(); if (musicFolder == null) { musicFolder = KnownFolders.MusicLibrary; } await GetLocalSongsAysnc(songfiles, musicFolder); localSongs = await PopulateSongListAsync(songfiles); return localSongs;}
递归调用GetLocalSongsAysnc,遍历主目录以及其子目录的所有.mp3
文件
private async Task GetLocalSongsAysnc(List 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> PopulateSongListAsync(List songFiles){ var localSongs = new List(); 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>> GetMusicInfos(){ List musicInfos; var result = false; if (await MediaLibraryAuthorization()) { musicInfos = await SetMusicListAsync(); result = true; } else { musicInfos = new List(); result = false; } return new InfoResult>(result, musicInfos);}
获取专辑和艺术家
专辑信息包含了音乐集合
获取专辑和艺术家的跨平台的实现方式大同小异,以Android平台为例
GetAlbumInfos方法用于获取AlbumInfo集合
public partial async Task>> GetAlbumInfos(){ List 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(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(); result = false; } return new InfoResult>(result, albumInfo);}
GetArtistInfos方法用于获取ArtistInfo集合
public partial async Task>> GetArtistInfos(){ List 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(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(); result = false; } return new InfoResult>(result, artistInfo);}
项目地址
GitHub:MatoMusic
关键词: 获取分组
全球观焦点:深入解读.NET MAUI音乐播放器项目(二):播放内核
女子戴金手镯做核磁共振:手腕被烫出一圈水泡
环球短讯!三亚旅游发现拔白发服务一小时50元 网友叹服:发量不允许
【全球聚看点】多地对体育中考项目作出调整:取消/选考中考男女生长跑 800米对健康不利
全球短讯!情人节多部爱情电影集中上映:跟邓超新电影强势对垒
每日热文:ChatGPT爆火!争议声也越来越大了
焦点速递!fusion app 常用小技巧
可怕又惊喜!87岁老人棺材内复生 亲属称席都吃了:目前一切正常
全球讯息:200M内存就能启动 Win11极限精简版升级:去除广告
【独家】19岁男孩患阿尔茨海默病 专家:这么做可以远离
全球微头条丨Pytorch环境安装
全球即时:PC销量下滑 AMD的Zen2处理器重新出山:配置没法看
《分布式技术原理与算法解析》学习笔记Day08
清华教授花20多万为村民3D打印住宅:直言房子一点不贵 方式会普及
观速讯丨雷军再次力荐小米13和Redmi K60!一小米之家上午开门就卖了7台
今亮点!土耳其专家称遇上地震是命遭主持人怒斥:网友热议说的没毛病
【天天报资讯】小伙入职1小时被HR告知招错人:补偿50元
全球热门:基于ModelViewSet写接口
当前热讯:日本大飞机失败!印度砸1000亿美元买空客波音500架飞机:为何不自研?
世界观点:杨元庆评兰奇:联想登顶全球PC市场的关键先生去世了
世界热议:女子小巷停车失误一口气撞了10辆:被救时还在踩油门
环球看热讯:女子路边买鹅蛋煮后蛋清是粉色 当事人懵了:不敢吃
全球实时:6档调节 MacBook都能用:诺西笔记本/平板铝合金支架19.9元
世界播报:加倍水润 杰士邦零感爆款避孕套大促:30只19.9元
当前简讯:索尼最强旗舰!Xperia 1 V渲染图首曝
焦点热文:中国女子土耳其地震中被埋:用纸吸雨水求生成功获救
环球热议:基于九个视图子类写五个接口
程序员画图软件推荐
JVM classpath的理解和设置总结
印度官员呼吁将情人节改成抱牛日:可增加幸福感
全球要闻:比尔·盖茨:ChatGPT像互联网发明一样重要、将会改变世界
【环球新视野】C++ 地球人口承载力
头条:5个python中编程的大坑
《流浪地球2》笨笨、门门设计稿公布 网友:别忘了MOSS
特斯拉股价2个月翻倍 马斯克有望夺回首富:只差400亿
天天速递!安卓14第一版正式发布 国产机泛滥的功能终于有了
世界观察:网易一面:select分页要调优100倍,说说你的思路?(内含Mysql的36军规)
买车送游戏机 丰田展示任天堂涂装版“大汉兰达”:内置大屏
最新资讯:好货不等人!森马官方清仓:春季高帮厚底板鞋79元大促(减190元)
每日快报!【踩坑日记】nginx server_name配置多域名的坑
日本独居长臂猿生娃动物园排查其情史:与“邻居”通过9毫米小孔交配
三句话惹生气?百度PLATO大火 网友:智能抬杠机器人
25%高增速:长城汽车2023年冲刺160万辆
星际飞船点火测试成功 马斯克预言:人类10年内登上火星
全球看点:对ChatGPT的几个提问,当码农小帮手可行
环球今日讯!状态模式
全球今热点:9.98万起真香 新款比亚迪秦PLUS DM-i上市 网友:不给合资留活路
LOJ 3395 集训队作业 Yet Another Permutation Problem 题解 (生成函数技巧)
重庆将为18万在校女学生免费接种HPV疫苗:全面预防宫颈癌
快资讯丨全球首创双枪充电!比亚迪赵长江:腾势D9纯电版20日左右交付
新消息丨做国内第一!深圳拼了:全市5G网速平均必500Mbps 上行下载更狠
全球球精选!你退了吗?网易已为超112万暴雪国服玩家退款
焦点快播:chatGPT接入个人微信(国内可用)
里程碑!ChatGPT参加美执业医师资格考试成绩合格 研究者:出了名的难
全球速看:雷军:小米汽车研发团队已超2300人 明年年一季度量产
最新快讯!真维斯官旗抄底:100%纯棉T恤4件99元包邮
2、如何验证是否存在CDN?
上天入海!我国最新研发飞行器“同济飞鱼”:可实现水陆两栖
ChatGPT或导致20种工作失业?远远不止 比尔盖茨:ChatGPT将改变世界
14不用管!小米小顽智能全自动猫砂盆预售:1399元
天天动态:Java8函数式编程读后总结与感想
新消息丨利用微PE制作官方最新纯净系统
【天天快播报】首富想干啥?马斯克公司遭调查:涉嫌非法运输危险病原体
全球热议:eSIM终于等来出头之日
5人将苹果翻新机当正品卖:赚到1.16亿!最终获刑
当前速递!80后、90后的“死去的回忆”:都完整保存在这里
当前热门:《原神》国外配音演员龌龊劣迹被锤:涉嫌骚扰未成年粉丝
环球百事通!【垃圾回收】v8引擎垃圾回收策略
世界快资讯:大爷大妈景区爬松树站树顶拍照 景区称出事不负责!网友被吓到
小白也能做应用(三)之fusion app远程更新及免登录
环球热文:kx00016-顺序表--用C语言实现:多种方法合并2个非递减顺序表
2月87款国产游戏获批版号:腾讯、网易新游来了
python之路68 drf从入门到成神 9 drf_jwt源码执行流程、自定义用户表签发和认证、simpleui的使用、权限控制(acl、rbac)
每日关注!导出域用户hash姿势总结
世界滚动:readelf命令读取elf文件的详细信息
再不用装一堆软件了!Windows 11可原生控制RGB信仰灯
天天热头条丨西安最倒霉面馆顾客暴涨员工数翻4倍 很多人慕名而来:感谢大家照顾
每日热门:56.函数模板
热门看点:学习打卡01- java入门
通讯!男子躲查酒驾3次跳河 血检结果却为0!结果令人舒适
流畅度飙升!小米:MIUI 14第一批正式版机型已全量推送
开了比亚迪之后 男子直呼不会开油车了:跟老人机一样
【当前独家】配1200W永磁同步电机!宗申TL3两轮版上市:可载重300公斤
世界快消息!别以为老师看不出来ChatGPT生成的论文!网友:你什么水平老师心里有数
世界热头条丨基于声网互动白板实现一个多人数独游戏
当前最新:学习笔记——尚好房项目(配置ssm环境、测试ssm环境)
《Towards Cooperation in Sequential Prisoner’s Dilemmas: a Deep Multiagent Reinfo
世界滚动:R机器学习:重复抽样在机器学习模型建立过程中的地位理解
【世界聚看点】火狐浏览器国内最受欢迎的扩展出炉:ABP等神级工具没入围
真相令人大跌眼镜!研究称莫奈印象派画风实则是雾霾
焦点要闻:土耳其地震“震出”隐秘地层:覆盖地球的44%、颠覆板块运动
今日最新!女子应聘财务被要求给五六人煮饭 负责人:吃饭不方便 可以不煮
多色多款 杰克琼斯卫衣89元包邮:低至1.3折清仓
项目终于用上了低代码,才知道为什么真香了!
机器学习-PCA
天天热消息:kx00015-顺序表--用C语言实现:删除顺序表中元素值等于x的所有元素
精彩看点:批处理脚本教程_编程入门自学教程_菜鸟教程-免费教程分享
vuex相关笔记
最新消息:realme 10 Pro可口可乐版发布:骁龙695 背壳logo爱了
【世界热闻】微信、微博全部断更!老干妈回应“退网”:经营一切正常