深入解读.NET MAUI音乐播放器项目(一):概述与架构
系列文章将分步解读音乐播放器核心业务及代码:
- 深入解读.NET MAUI音乐播放器项目(一):概述与架构
- 深入解读.NET MAUI音乐播放器项目(二):播放内核
- 深入解读.NET MAUI音乐播放器项目(三):界面与交互
为什么想起来这个项目了呢?
这是一个Windows Phone 8的老项目,2014年用作为兴趣写了个叫“番茄播放器”的App,顺便提高编程技能。
这个项目的架构历经多次迁移,从WP8到UWP再到Xamarin.Forms。去年底随着MAUI的正式发布,又尝试把它迁移到MAUI上来。
虽然历经数次迁移,但命名空间和播放内核的代码基本没怎么改动,这个项目随着解决方案升级,依赖库、API调用方式的变更,见证了微软在移动互联网领域的动荡。我偶然发现8年前提交到微软商店的App,竟然还能够打开下载页面 - Microsoft应用商店,但由于我手边没有一台Windows Phone设备,也没法让它在任何的模拟器中跑起来。也只能从商店截图和源代码中重温这个物件和那段时光。
这个项目现在已经没有任何的商业价值,但我知道它对于我意味着什么,曾给我带来的在编程时的那种欣喜和享受,可以说真正让我知道什么叫“Code 4 Fun”——编程带来的快乐,对于那时刚进入社会的我,树立信心和坚持道路有莫大的帮助。
这个项目可能从来就没有价值。那么写博文和开源能发挥多少价值就算多少吧。
当下在.Net平台上有不少开源的音频封装库,如Plugin.Maui.Audio,本项目没有依赖任何音频的第三方库,希望大家以学习的态度交流,如果您有更好的实现方式,欢迎在文章下留言。因为代码年代久远且近年来没有重构,C#语言版本和代码写法上会有不少繁冗,这里还要向大家说声抱歉。

架构
使用Abp框架,我之前写过如何 将Abp移植进.NET MAUI项目,本项目也是按照这篇博文完成项目搭建。
跨平台
使用.NET MAU实现跨平台支持,从Xamarin.Forms移植的应用可以在Android和iOS平台上顺利运行。
播放内核是由分部类提供跨平台支持的,在Xamarin.Forms时代,需要维护不同平台的项目,MAUI是单个项目支持多个平台。
MAUI 应用项目包含 一个 Platform 文件夹,每个子文件夹表示 .NET MAUI 可以面向的平台
每个文件夹代表了每个平台特定的代码, 在默认的情况下 编译阶段仅仅会编译当前选择的平台文件夹代码。
这属于利用分部类和方法创建平台特定内容,详情请参考官方文档
如IMusicControlService在项目中分部类实现:
MatoMusic.Core\Impl\MusicControlService.cs
MatoMusic.Core\Platforms\Android\MusicControlService.cs
MatoMusic.Core\Platforms\iOS\MusicControlService.cs
MatoMusic.Core\Platforms\Windows\MusicControlService.cs
核心类
在设计播放内核时,从用户的交互路径思考,抽象出了曲目管理器IMusicInfoManager和播放控制服务IMusicControlService,
播放器行为和曲目操作行为在各自领域相互隔离,通过生产-消费模型,数据流转和消息通知冒泡协调一致。尽量规避了大规模使用线程锁,以及复杂的线程同步逻辑。在跨平台方案中,通过分部类实现了这些接口,类图如下:

音乐播放相关服务类MusicRelatedService是播放控制服务的一层封装,在实际播放器业务逻辑上,利用封装的代码能更方便的完成任务。
项目遵循MVVM设计模式,MusicRelatedViewModel作为音乐播放相关ViewModel的基类,包含了曲目管理器IMusicInfoManager和播放控制服务IMusicControlService对象,通过双向绑定开发者可以从表现层轻松进行音乐控制和曲目访问
ViewModelBase是个基础类,它继承自AbpServiceBase,封装了Abp框架通用功能的调用。比如Setting、Localization和UnitOfWork功能。并且实现了INotifyPropertyChanged,它为绑定类型的每个属性提供变更事件。
核心类图如下

定义
- Queue - 歌曲队列,当前用于播放歌曲的有序列表
- Playlist - 歌单,存储可播放内容的集合,用于收藏曲目,添加到我的最爱等。
- PlaylistEntry - 歌单条目,可播放内容,关联一个本地音乐或在线音乐信息
- MyFavourite - 我的最爱,一个id为1的特殊的歌单,不可编辑和删除,用于记录点亮歌曲小红心
- MusicInfo - 曲目信息
- AlbumInfo - 专辑信息
- ArtistInfo - 艺术家信息
- BillboardInfo - 排行榜,在线音乐歌单
曲目
曲目包含:
- Title - 音乐标题
- AlbumTitle - 专辑标题
- GroupHeader - 标题头,用于列表分组显示的依据
- Url - 音频文件地址
- Artist - 艺术家
- Genre - 流派
- IsFavourite - 是否已“我最喜爱”
- IsPlaying - 是否正在播放
- AlbumArtPath - 封面图片
- Duration - 歌曲总时长
如果配合模糊搜索控件,需要实现IClueObject,使用方式请参考AutoComplete控件
public class MusicInfo : ObservableObject, IBasicInfo, IClueObject
{ .. }
public List<string> ClueStrings
{
get
{
var result = new List<string>();
result.Add(Title);
result.Add(Artist);
result.Add(AlbumTitle);
return result;
}
}
它继承自ObservableObject,构造函数中注册属性更改事件
IsFavourite更改时,将调用MusicInfoManager将当前曲目设为或取消设为“我最喜爱”
private void MusicInfo_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
var MusicInfoManager = IocManager.Instance.Resolve<MusicInfoManager>();
if (e.PropertyName == nameof(IsFavourite))
{
if (IsFavourite)
{
MusicInfoManager.CreatePlaylistEntryToMyFavourite(this);
}
else
{
MusicInfoManager.DeletePlaylistEntryFromMyFavourite(this);
}
}
}
曲目集合
曲目集合是歌单,音乐专辑或者艺术家(演唱者)创作的音乐的抽象,它包含:
- Title - 标题,歌单,音乐专辑或者艺术家名称
- GroupHeader - 标题头,用于列表分组显示的依据
- Musics - 曲目信息集合
- AlbumArtPath - 封面图片
- Count - 歌曲集合曲目数
- Time - 歌曲集合总时长
它继承自ObservableObject
AlbumInfo,ArtistInfo,PlaylistInfo,BillboardInfo 都是曲目集合的子类

Musics是曲目集合的内容,类型为ObservableCollection<MusicInfo>,双向绑定时提供队列变更事件。
集合曲目数和集合总时长依赖这个变量
public int Count => Musics.Count();
public string Time
{
get
{
var totalSec = Math.Truncate((double)Musics.Sum(c => (long)c.Duration));
var totalTime = TimeSpan.FromSeconds(totalSec);
var time = totalTime.ToString("g");
return time;
}
}
当集合内容增删时,同步通知歌曲集合曲目数以及总时长变更
private void _musics_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Remove || e.Action == NotifyCollectionChangedAction.Add)
{
RaisePropertyChanged(nameof(Time));
RaisePropertyChanged(nameof(Count));
}
}
GroupHeader标题头,一般取得是标题的首字母,若标题为中文,则使用Microsoft.International.Converters.PinYinConverter获取中文第一个字的拼音首字母,跨平台实现方式如下:
private partial string GetGroupHeader(string title)
{
string result = string.Empty;
if (!string.IsNullOrEmpty(title))
{
if (Regex.IsMatch(title.Substring(0, 1), @"^[\u4e00-\u9fa5]+$"))
{
try
{
var chinese = new ChineseChar(title.First());
result = chinese.Pinyins[0].Substring(0, 1);
}
catch (Exception ex)
{
return string.Empty;
}
}
else
{
result = title.Substring(0, 1);
}
}
return result;
}
GroupHeader用于列表分组显示的内容将在后续文章中阐述
数据库
应用程序里使用Sqlite,作为播放列表,歌单,设置等数据的持久化
,使用CodeFirst方式用EF初始化Sqlite数据库文件:mato.db
在MatoMusic.Core项目的appsettings.json中添加本地sqlite连接字符串
"ConnectionStrings": {
"Default": "Data Source=file:{0};"
},
...
这里文件是一个占位符,通过代码hardcode到配置文件
在MatoMusicCoreModule.cs中,重写PreInitialize并设置Configuration.DefaultNameOrConnectionString:
public override void PreInitialize()
{
LocalizationConfigurer.Configure(Configuration.Localization);
Configuration.Settings.Providers.Add<CommonSettingProvider>();
string documentsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), MatoMusicConsts.LocalizationSourceName);
var configuration = AppConfigurations.Get(documentsPath, development);
var connectionString = configuration.GetConnectionString(MatoMusicConsts.ConnectionStringName);
var dbName = "mato.db";
string dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), MatoMusicConsts.LocalizationSourceName, dbName);
Configuration.DefaultNameOrConnectionString = String.Format(connectionString, dbPath);
base.PreInitialize();
}
接下来定义实体类
播放队列
定义于\MatoMusic.Core\Models\Entities\Queue.cs
public class Queue : FullAuditedEntity<long>
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public override long Id { get; set; }
public long MusicInfoId { get; set; }
public int Rank { get; set; }
public string MusicTitle { get; set; }
}
歌单
定义于\MatoMusic.Core\Models\Entities\Playlist.cs
public class Playlist : FullAuditedEntity<long>
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public override long Id { get; set; }
public string Title { get; set; }
public bool IsHidden { get; set; }
public bool IsRemovable { get; set; }
public ICollection<PlaylistItem> PlaylistItems { get; set; }
}
歌单条目
定义于\MatoMusic.Core\Models\Entities\PlaylistItem.cs
public class PlaylistItem : FullAuditedEntity<long>
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public override long Id { get; set; }
public int Rank { get; set; }
public long PlaylistId { get; set; }
[ForeignKey("PlaylistId")]
public Playlist Playlist { get; set; }
public string MusicTitle { get; set; }
public long MusicInfoId { get; set; }
}
配置
数据库上下文对象MatoMusicDbContext定义如下
public class MatoMusicDbContext : AbpDbContext
{
//Add DbSet properties for your entities...
public DbSet<Queue> Queue { get; set; }
public DbSet<Playlist> Playlist { get; set; }
public DbSet<PlaylistItem> PlaylistItem { get; set; }
...
MatoMusic.EntityFrameworkCore是应用程序数据库的维护和管理项目,依赖于Abp.EntityFrameworkCore。
在MatoMusic.EntityFrameworkCore项目中csproj文件中,引用下列包
<PackageReference Include="Abp.EntityFrameworkCore" Version="7.4.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="1.1.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.0">
在该项目MatoMusicEntityFrameworkCoreModule.cs 中,将注册上下文对象,并在程序初始化运行迁移,此时将在设备上生成mato.db文件
public override void PostInitialize()
{
Helper.WithDbContextHelper.WithDbContext<MatoMusicDbContext>(IocManager, RunMigrate);
if (!SkipDbSeed)
{
SeedHelper.SeedHostDb(IocManager);
}
}
public static void RunMigrate(MatoMusicDbContext dbContext)
{
dbContext.Database.Migrate();
}
项目地址
下一章将介绍播放器核心功能:播放服务类
深入解读.NET MAUI音乐播放器项目(一):概述与架构的更多相关文章
- swift 音乐播放器项目-《lxy的杰伦情歌》开发实战演练
近期准备将项目转化为OC与swift混合开发.试着写一个swift音乐播放器的demo,体会到了swift相对OC的优势所在.废话不多说.先上效果图: watermark/2/text/aHR0cDo ...
- Android(java)学习笔记234: 服务(service)之音乐播放器
1.我们播放音乐,希望在后台长期运行,不希望因为内存不足等等原因,从而导致被gc回收,音乐播放终止,所以我们这里使用服务Service创建一个音乐播放器. 2.创建一个音乐播放器项目(使用服务) (1 ...
- Android基于发展Service音乐播放器
这是一个基于Service组件的音乐播放器,程序的音乐将会由后台的Service组件负责播放,当后台的播放状态改变时,程序将会通过发送广播通知前台Activity更新界面:当用户单击前台Activit ...
- Android(java)学习笔记177: 服务(service)之音乐播放器
1.我们播放音乐,希望在后台长期运行,不希望因为内存不足等等原因,从而导致被gc回收,音乐播放终止,所以我们这里使用服务Service创建一个音乐播放器. 2.创建一个音乐播放器项目(使用服务) (1 ...
- 躁!DJ 风格 Java 桌面音乐播放器
本文适合有 Java 基础知识的人群,跟着本文可学习和运行 Java 版桌面 DJ 音乐播放器. 本文作者:HelloGitHub-秦人 HelloGitHub 推出的<讲解开源项目>系列 ...
- Andriod小项目——在线音乐播放器
转载自: http://blog.csdn.net/sunkes/article/details/51189189 Andriod小项目——在线音乐播放器 Android在线音乐播放器 从大一开始就已 ...
- 项目源码--Android类似酷狗音乐播放器
下载源码 知识技能概要: 1.音乐文件的扫描与管理 2.音频流的解码 3. UI控件的综合使用 4.播放列表方式管理 5.随机播放方式 6.源码带详细的中文注释 ...... 详细介绍 1. 音乐文件 ...
- 团队项目 NABCD分析java音乐播放器
NABCD分析java音乐播放器 程设计题目:java音乐播放器 一.课程设计目的 1.编程设计音乐播放软件,使之实现音乐播放的功能. 2.培养学生用程序解决实际问题的能力和兴趣. 3.加深java中 ...
- HTML5项目笔记4:使用Audio API设计绚丽的HTML5音乐播放器
HTML5 有两个很炫的元素,就是Audio和 Video,可以用他们在页面上创建音频播放器和视频播放器,制作一些效果很不错的应用. 无论是视屏还是音频,都是一个容器文件,包含了一些音频轨道,视频轨道 ...
- android 音乐播放器
本章以音乐播放器为载体,介绍android开发中,通知模式Notification应用.主要涉及知识点Notification,seekbar,service. 1.功能需求 完善音乐播放器 有播放列 ...
随机推荐
- 禁用显卡自动更新(解决官办驱动和OEM驱动相冲)
起因 有一天打开MC(我的世界)时候突然显示显卡不支持,想了想可能是自己捣鼓电脑的时候弄坏了,提示说版本不支持,一看自己的驱动还是上古版本,但是之前都是可以正常玩的,想着更新下驱动就行了呗,更新完&q ...
- nydusd 源码理解(一)
" 尝试通过 nydus[1] 源码理解工作流程.可能由于代码变动导致和本文记录的内容有出入. 1. 环境准备 git clone https://github.com/dragonflyo ...
- JavaEE Day00 Java Web课程介绍
1.什么是Java Web? 使用Java语言开发互联网项目,简单理解为使用Java语言开发网站 2.课程介绍:30天 1.数据库(5天,第一阶段) 2.静态网页前端(5天,第二阶段) 3.Web核心 ...
- .NET 6 基于IDistributedCache实现Redis与MemoryCache的缓存帮助类
本文通过IDistributedCache的接口方法,实现Redis与MemoryCache统一帮助类.只需要在配置文件中简单的配置一下,就可以实现Redis与MemoryCache的切换. 目录 I ...
- python3中的常见知识点2
python3中的常见知识点2 列表与栈和队列 map()函数 python列表遍历的4种方式 参考链接 列表栈和队列 1.列表作为栈使用 栈:先进后出,First In Last Out 使用 ap ...
- 全网最全的linux上docker安装oracle的详细文档,遇到了n个问题,查了几十篇文章,最终汇总版,再有解决不了的,私聊我,我帮你解决
目录 全网最全的linux上docker安装oracle的详细文档,遇到了n个问题,查了几十篇文章,最终汇总版,再有解决不了的,私聊我,我帮你解决 1. 拉取阿里镜像oracle 2. 创建初始化数据 ...
- elasticsearch global 、 filters 和 cardinality 聚合
目录 1. 背景 2.解释 1.global 2.filters 3.cardinality 3.需求 4.前置条件 4.1 创建mapping 4.2 准备数据 5.实现3的需求 5.1 dsl 5 ...
- MySQL函数--时间格式--流程控制if判断
目录 一:函数 1.MySQL什么是函数? 2.通过help查看函数帮助 3.移除指定字符 4.大小写转换 5.获取左右起始指定个数字符 6.返回读音相似值(对英文效果) 二:时间格式实战案例 1.时 ...
- 第一百一十四篇: JS数组Array(三)数组常用方法
好家伙,本篇为<JS高级程序设计>第六章"集合引用类型"学习笔记 1.数组的复制和填充 批量复制方法 copyWithin(),以及填充数组方法fill(). 这两 ...
- MongoDB从入门到实战之MongoDB简介
前言 相信很多同学对MongoDB这个非关系型数据库都应该挺熟悉的,在一些高性能.动态扩缩容.高可用.海量数据存储.数据价值较低.高扩展的业务场景下MongoDB可能是我们的首选,因为MongoDB通 ...