• [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 ClueStrings
    {
        get
        {
            var result = new List();
            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();
    
        if (e.PropertyName == nameof(IsFavourite))
        {
            if (IsFavourite)
            {
                MusicInfoManager.CreatePlaylistEntryToMyFavourite(this);
            }
            else
            {
                MusicInfoManager.DeletePlaylistEntryFromMyFavourite(this);
            }
        }
    }
    

    曲目集合

    曲目集合是歌单,音乐专辑或者艺术家(演唱者)创作的音乐的抽象,它包含:

    • Title - 标题,歌单,音乐专辑或者艺术家名称
    • GroupHeader - 标题头,用于列表分组显示的依据
    • Musics - 曲目信息集合
    • AlbumArtPath - 封面图片
    • Count - 歌曲集合曲目数
    • Time - 歌曲集合总时长

    它继承自ObservableObject

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

    Musics是曲目集合的内容,类型为ObservableCollection,双向绑定时提供队列变更事件。

    集合曲目数和集合总时长依赖这个变量

    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();
    
        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 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(IocManager, RunMigrate);
        if (!SkipDbSeed)
        {
            SeedHelper.SeedHostDb(IocManager);
        }
    }
    
    public static void RunMigrate(MatoMusicDbContext dbContext)
    {
        dbContext.Database.Migrate();
    }
    

    项目地址

    GitHub:MatoMusic

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

  • 相关阅读:
    vue基础操作(vue基础)
    教练式管理OKR的原因和具体方法
    什么是DNS解析DNSPod?它有哪些功能和优势?
    【C++】模板初阶 【 深入浅出理解 模板 】
    java多线程-单例模式与多线程
    点云进阶文章目录汇总
    Websocket搭建(Vue+Springboot)
    跨境erp系统功能分析
    下厨房网站月度最佳栏目菜谱数据获取及分析PLus
    计算机中的 原码、反码、补码 详解
  • 原文地址:https://www.cnblogs.com/jevonsflash/p/17113139.html