深入解读.NET MAUI音乐播放器项目(一):概述与架构

news2024/12/27 13:00:48

系列文章将分步解读音乐播放器核心业务及代码:

  • 深入解读.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为0的特殊的歌单,不可编辑和删除,用于记录点亮歌曲小红心
  • 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

AlbumInfoArtistInfoPlaylistInfoBillboardInfo 都是曲目集合的子类
在这里插入图片描述

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();
}

项目地址

GitHub:MatoMusic

下一章将介绍播放器核心功能:播放服务类

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/338066.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

部门新来了个软件测试工程师,一副毛头小子的样儿,哪想到是新一代卷王...

内卷&#xff0c;是现在热度非常高的一个词汇&#xff0c;随着热度不断攀升&#xff0c;隐隐到了“万物皆可卷”的程度。 在程序员职场上&#xff0c;什么样的人最让人反感呢? 是技术不好的人吗?并不是。技术不好的同事&#xff0c;我们可以帮他。 是技术太强的人吗?也不…

真正的云原生大数据平台,让Kubernetes又牛了一把

作为一款开源的容器编排引擎&#xff0c;始于2014年的 Kubernetes 一经推出就受到了开发者的喜爱&#xff0c;谁也不曾想到它会取得如此大的成功。如今&#xff0c;在云原生技术发展的浪潮中&#xff0c;Kubernetes 作为容器编排领域的事实标准和云原生领域的关键项目&#xff…

LeetCode-111. 二叉树的最小深度

目录题目分析递归法题目来源111. 二叉树的最小深度题目分析 这道题目容易联想到104题的最大深度&#xff0c;把代码搬过来 class Solution {public int minDepth(TreeNode root) {return dfs(root);}public static int dfs(TreeNode root){if(root null){return 0;}int left…

C++笔记之单例模式

C笔记之单例模式 前言 当一个类在程序的整个生命周期中&#xff0c;只需要一个实例的时候&#xff0c;就可以考虑把这个类设计成单例的方式&#xff0c;提供出去&#xff0c;让全局访问。一般来说比较 “重” 的一些类会设计成单例&#xff0c;比如像“引擎”&#xff0c; “x…

微搭低代码从入门到精通12-网格布局

开发小程序首要的就是考虑布局的问题&#xff0c;我们在以前的版本只能选择普通容器结合图片和文本组件来构建页面。 使用通用组件布局也可以&#xff0c;但有个问题是你要先学习CSS&#xff0c;要懂布局的概念&#xff0c;比如需要知道啥是flex布局&#xff0c;然后还得熟悉每…

分布式事务 | 使用DTM 的Saga 模式

DTM 简介前面章节提及的MassTransit、dotnetcore/CAP都提供了分布式事务的处理能力&#xff0c;但也仅局限于Saga和本地消息表模式的实现。那有没有一个独立的分布式事务解决方案&#xff0c;涵盖多种分布式事务处理模式&#xff0c;如Saga、TCC、XA模式等。有&#xff0c;目前…

【AI数学】相机成像之内参数

计算机视觉偏底层的工作会跟摄像机打交道&#xff0c;最近正好有接触&#xff0c;所以整理总结一下。 相机参数通常分为内参数、外参数&#xff0c;偶尔会有畸变参数等滤镜参数。 申明&#xff1a;本文图例均为原创&#xff0c;借用需附此文链接。 内参数&#xff1a;相机内部的…

[SSD固态硬盘技术 15] FTL映射表的神秘面纱

为什么需要映射表?固态硬盘的存储器件采用的是闪存[5],具有以下几个特点: (1)读写基本单位是以页(Page)为单位,擦除是以块(Block)为单位。

NFC概述摘要

同学,别退出呀,我可是全网最牛逼的 WIFI/BT/GPS/NFC分析博主,我写了上百篇文章,请点击下面了解本专栏,进入本博主主页看看再走呗,一定不会让你后悔的,记得一定要去看主页置顶文章哦。 原理来说,NFC和Wi-Fi类似,利用无线射频技术来实现设备间通信。NFC的工作频率为13.5…

基于c语言实现的对代码的同源性检测

完整代码&#xff1a;https://download.csdn.net/download/qq_38735017/87382389本次课程设计为了巩固上学期在软件安全课程上所学的安全知识&#xff0c;包括堆栈溢出、整数溢出等等&#xff0c;同时考察了一些课外的新事物&#xff0c;例如字符串匹配与CFG控制流程图的同源性…

Attention机制 学习笔记

学习自https://easyai.tech/ai-definition/attention/ Attention本质 Attention&#xff08;注意力&#xff09;机制如果浅层的理解&#xff0c;跟他的名字非常匹配。他的核心逻辑就是“从关注全部到关注重点”。 比如我们人在看图片时&#xff0c;对图片的不同地方的注意力…

为什么要在电子产品中使用光耦合器?

介绍 光耦合器不仅可以保护敏感电路&#xff0c;还可以使工程师设计各种硬件应用。光耦合器通过保护元件&#xff0c;可以避免更换元件的大量成本。然而&#xff0c;光耦合器比保险丝更复杂。光耦合器还可以通过光耦合器连接和断开两个电路&#xff0c;从而方便地控制两个电路…

【Markdown】markdown语法规定

这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注…

Vue3 如何实现一个函数式右键菜单(ContextMenus)

前言: 最近在公司 PC 端的项目中使用到了右键出现菜单选项这样的一个工作需求&#xff0c;并且自己现在也在实现一个偶然迸发的 idea&#xff08; 想用前端实现一个 windows 系统从开机到桌面的 UI&#xff09;&#xff0c;其中也要用到右键弹出菜单这样的一个功能&#xff0c;…

通讯录文件操作化

宝子&#xff0c;你不点个赞吗&#xff1f;不评个论吗&#xff1f;不收个藏吗&#xff1f; 最后的最后&#xff0c;关注我&#xff0c;关注我&#xff0c;关注我&#xff0c;你会看到更多有趣的博客哦&#xff01;&#xff01;&#xff01; 喵喵喵&#xff0c;你对我真的很重…

几个chatGPT的难题,关于语言转换

不同语言代码的移植一直以来是程序员面临的难题&#xff0c;最近问了问chatGPT能否解决这个问题。编写一个程序&#xff0c;实现c语言函数转换为php函数答&#xff1a;这是一个非常困难的问题&#xff0c;因为两种语言的语法、结构和标准库都不相同。如果您希望完成这个任务&am…

MySql服务多版本之间的切换

从网上总结的经验&#xff0c;然后根据自己所遇到的问题合并记录一下&#xff0c;方便日后再次需要用到 MySql服务多版本同时运行 步骤 1、如果你电脑上已经有一个mysql版本&#xff0c;例如mysql-5.7.39-winx64&#xff0c;它占据了3306端口。此时如果你想下仔另一版本&…

活动星投票紫砂新青年制作一个投票活动

“紫砂新青年”网络评选投票_免费链接投票_作品投票通道_扫码投票怎样进行现在来说&#xff0c;公司、企业、学校更多的想借助短视频推广自己。通过微信投票小程序&#xff0c;网友们就可以通过手机拍视频上传视频参加活动&#xff0c;而短视频微信投票评选活动既可以给用户发挥…

6年自动化测试,终于进华为了,年薪25w其实也并非触不可及

我的职业生涯开始和大多数测试人一样&#xff0c;开始接触都是纯功能界面测试&#xff0c;第一份测试工作就是在电商公司做功能测试&#xff0c;工作忙忙碌碌&#xff0c;每天在各种业务需求学习和点点中度过&#xff0c;过了好几年发现自己还只是一个功能测试工程师&#xff0…

锐捷(十四)mpls vxn optionc的关键问题所在和具体问题分析

用锐捷的设备搭建mpls vxn optionc的基础版和带RR的版本&#xff0c;在控制平面和转发平免上分析mpls vxn optionc的关键问题所在和具体问题分析。一 基础mpls vxn optionc&#xff1a;核心&#xff1a;两pe之间之间建立MP EBGP邻居&#xff0c;从而直接传递路由解放了ASBR。关…