系列文章將分步解讀音樂播放器核心業務及程式碼:
- 深入解讀.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();
}
專案地址
下一章將介紹播放器核心功能:播放服務類