前些天看到新闻说Windows10自带的Windows Media Player将支持FLAC无损播放,而目前自己的播放器是采用BASS音频库去支持无损的播放的,BASS音频库的用法奇葩不说,文档也不多,遇到问题网上搜半天都找不着答案真是愁死,于是打算将我的音乐播放器重做一番了,这次重新用回WMP内核。
相信做过WPF媒体方面应用开发的同学都知道,new一个MediaPlayer就可以实现对媒体的操作了,而这个MediaPlayer其实是用的当前操作系统中的Windows Media Player,支持播放暂停停止,播放速率调节,音量设置(BASS的音量设置很蛋疼,会连系统音量都给调了),提供媒体打开,媒体结束,播放错误3个事件,因而如果要自己做一个音乐播放器的话,再加上媒体状态的判断,以及添加媒体状态/播放进度改变的事件就差不多了。因为是重做的,所以只实现了一下基础功能(播放控制,收藏,播放模式)以及几个个性功能(音乐统计,定时停止,播放进度的显示),同时BUG还很多,还在继续开发中,如果有兴趣的话可以一起开发哟,项目已经传到TFS了,地址是http://llyn23.visualstudio.com
Debug目录文件下载:http://yun.baidu.com/s/1xYWua
Solution项目源代码下载:http://yun.baidu.com/s/1mguWDCG
1.构建CoreHelper.cs
这个播放器核心类包含媒体的播放,添加取消收藏,切换播放模式,歌曲文件的读取,媒体库/播放列表/播放器配置等的保存等
2.数据的保存和读取
在程序启动(OnStartUp)时加载XML文件反序列化为对象,保存有媒体库,播放列表,播放记录,收藏的歌曲等,退出(OnExit)时则根据对象是否发生改变,将对象序列化到XML文件中
string repository = System.IO.Path.Combine(new string[] { AppDomain.CurrentDomain.BaseDirectory, "Repository" }); if (!System.IO.Directory.Exists(repository)) { return; } //加载播放器设置 PlayerConfig = SerializeHelper.ToEntity<PlayerConfigModel>(repository + "\\PlayerConfig.xml"); //加载媒体库 MediaCollection = SerializeHelper.ToEntity<List<MediaModel>>(repository + "\\MediaCollection.xml"); //加载播放列表 Playlists = SerializeHelper.ToEntity<List<PlaylistModel>>(repository + "\\Playlists.xml"); //加载播放队列 PlayQueues = SerializeHelper.ToEntity<List<PlayQueueModel>>(repository + "\\PlayQueues.xml"); //加载播放记录 PlayRecords = SerializeHelper.ToEntity<List<PlayRecordModel>>(repository + "\\PlayRecords.xml"); //加载收藏列表 Favorites = SerializeHelper.ToEntity<List<FavoriteModel>>(repository + "\\Favorites.xml"); //初始化计数器 AutoStopCounter = PlayerConfig.AutoStopTimeset;
string repository = System.IO.Path.Combine(new string[] { AppDomain.CurrentDomain.BaseDirectory, "Repository" }); if (!System.IO.Directory.Exists(repository)) { try { System.IO.Directory.CreateDirectory(repository); } catch (Exception ex) { LogHelper.Error(ex.Message); return; } } //保存播放器设置 if (IsPlayerConfigChanged) { SerializeHelper.ToXml<PlayerConfigModel>(PlayerConfig, repository + "\\PlayerConfig.xml"); } //保存媒体库 if (IsMediaCollectionChanged) { SerializeHelper.ToXml<List<MediaModel>>(MediaCollection, repository + "\\MediaCollection.xml"); }
2.样式和资源使用方面
界面元素的背景图片和颜色,字体颜色和字体大小全部存放在Application的资源字典中,可以通过代码动态更新
<ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="/Resources/Color.xaml"></ResourceDictionary> <ResourceDictionary Source="/Resources/ImageBrush.xaml"></ResourceDictionary> <ResourceDictionary Source="/Resources/LinearGradientBrush.xaml"></ResourceDictionary> <ResourceDictionary Source="/Resources/SolidBrush.xaml"></ResourceDictionary> <ResourceDictionary Source="/Styles/Border.xaml"></ResourceDictionary> <ResourceDictionary Source="/Styles/TextBlock.xaml"></ResourceDictionary> </ResourceDictionary.MergedDictionaries> </ResourceDictionary>
3.播放控制核心部分
其实就是处理MediaPlayer的4个事件,MediaOpened,MediaFailed,MediaEnded,以及自己加的事件MediaPositionChanging
private static void _mainPlayer_MediaOpened(object sender, EventArgs e) { //设置当前媒体 CurrentMedia = MediaCollection.Where(w => w.FullName == HttpUtility.UrlDecode(MainPlayer.Source.AbsolutePath).Replace("/", "\\")).FirstOrDefault(); }
private static void _mainPlayer_MediaFailed(object sender, ExceptionEventArgs e) { PlayerStatus = PlayerStatusEnum.已停止; LogHelper.Error(e.ErrorException.Message); }
private static void _mainPlayer_MediaEnded(object sender, EventArgs e) { //添加播放记录 IsPlayRecordsChanged = true; PlayRecords.Add(new PlayRecordModel { CreatedAt = DateTime.Now, Media = CurrentMedia.FullName }); //设置播放器状态 PlayerStatus = PlayerStatusEnum.已停止; //根据播放模式来决定接下要播放的歌曲 switch (PlayerConfig.PlayMode) { case (int)PlayModeEnum.单曲循环: { PlayByRepeat(); break; } case (int)PlayModeEnum.列表循环: { PlayByRecycle(); break; } case (int)PlayModeEnum.顺序播放: { PlayByOrder(); break; } case (int)PlayModeEnum.随机播放: { PlayByRandom(); break; } } }
//CoreHelper.cs中触发媒体播放进度改变事件 private static void _mainTimer1_Tick() { if (IsAutoStopOpened && AutoStopCounter <= 0) { //停止播放(跨线程处理) MainPlayer.Dispatcher.Invoke(new Action(() => { Stop(); })); AutoStopCounter = PlayerConfig.AutoStopTimeset; IsAutoStopOpened = false; return; } if (PlayerStatus == PlayerStatusEnum.播放中) { AutoStopCounter -= 1; MediaPositionChanging(); } } //在UI线程中处理媒体播放进度事件 CoreHelper.MediaPositionChanging += CoreHelper_MediaPositionChanging; private void CoreHelper_MediaPositionChanging() { this.Dispatcher.Invoke(new Action(() => { DisplayAutoStop(); TimeSpan duration = TimeSpan.FromSeconds(0); if (CoreHelper.MainPlayer.NaturalDuration.HasTimeSpan) { duration = CoreHelper.MainPlayer.NaturalDuration.TimeSpan; } double percent = 0; if (duration.TotalSeconds > 0) { percent = CoreHelper.MainPlayer.Position.TotalSeconds / duration.TotalSeconds; } //显示媒体播放进度背景 gs2.Offset = percent; gs3.Offset = percent; //显示媒体播放进度和总持续时间文本 _mediaPositionTextBlock.Text = String.Format("{0}/{1}", StringHelper.GetTimeSpanString(CoreHelper.MainPlayer.Position), StringHelper.GetTimeSpanString(duration)); })); }
5.更新媒体库
这里主要涉及到从文件中读取歌曲信息,所以一个步骤是递归读取文件夹中的文件,第二步骤是读取歌曲信息(通过第三方类库ID3.dll,TagLib.dll),第三步骤是在UI上实时读取状态
public static void FindMatchedFile(string folder, List<string> extensions, double minFileLength, double maxFileLength, UpdateMediaCollectionEventHandler callback) { string[] files = System.IO.Directory.GetFiles(folder, "*.*", SearchOption.TopDirectoryOnly); FileInfo info = null; foreach (string file in files) { info = new FileInfo(file); if (!extensions.Contains(System.IO.Path.GetExtension(file).ToLower())) { continue; } if (info.Length < minFileLength) { continue; } if (info.Length > maxFileLength) { continue; } //通过委托去通知前端UI callback(file, AddToMediaCollection(info)); } string[] directories = System.IO.Directory.GetDirectories(folder, "", SearchOption.TopDirectoryOnly); foreach (string directory in directories) { FindMatchedFile(directory, extensions, minFileLength, maxFileLength, callback); } }
public static int AddToMediaCollection(FileInfo file) { ID3Info id3 = null; try { id3 = new ID3Info(file.FullName, true); } catch (Exception ex) { LogHelper.Error(ex.Message); return -1; } MediaModel media = new MediaModel(); //创建的时间 media.CreatedTime = DateTime.Now; //文件长度 media.FileLength = file.Length; //文件全路径 media.FullName = file.FullName; //歌曲的专辑 media.Album = id3.ID3v2Info.GetTextFrame("TALB") ?? "未知专辑"; //歌曲的艺术家 media.Artists = id3.ID3v2Info.GetTextFrame("TPE1") ?? "未知歌手"; //歌曲的标题 media.Title = id3.ID3v2Info.GetTextFrame("TIT2") ?? System.IO.Path.GetFileNameWithoutExtension(file.FullName); TagLib.File f = null; try { f = TagLib.File.Create(file.FullName); //歌曲的持续时间 media.Duration = f.Properties.Duration.TotalSeconds; //保存歌曲封面 if (f.Tag.Pictures.Length > 0) { string folder = System.IO.Path.Combine(new string[] { AppDomain.CurrentDomain.BaseDirectory, "Repository", "Cover" }); if (!System.IO.Directory.Exists(folder)) { System.IO.Directory.CreateDirectory(folder); } byte[] bytes = f.Tag.Pictures[0].Data.Data; string cover = System.IO.Path.Combine(new string[] { folder, StringHelper.RemoveSpecialCharacters(media.Album) + ".jpg" }); using (FileStream fs = new FileStream(cover, FileMode.Create)) { fs.Write(bytes, 0, bytes.Length); } } } catch (Exception ex) { LogHelper.Debug(ex.Message); } MediaModel model = MediaCollection.Where(w => w.FullName == file.FullName).FirstOrDefault(); if (model == null || String.IsNullOrEmpty(model.FullName)) { MediaCollection.Add(media); LogHelper.Debug("添加了一个媒体:" + file.FullName); return 1; } //更新媒体信息 model.Album = media.Album; model.Artists = media.Artists; model.Duration = media.Duration; model.FileLength = media.FileLength; model.Title = media.Title; LogHelper.Debug("更新了一个媒体:" + file.FullName); return 0; }
private List<int> _operations = null; private void UpdateMediaCollectionTextBlock_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { UpdateMediaCollectionTextBlock.IsEnabled = false; ProcessingMediaTextBlock.Visibility = Visibility.Visible; _operations = new List<int>(); //开启线程更新媒体库 BackgroundWorker worker = new BackgroundWorker(); worker.DoWork += worker_DoWork; worker.RunWorkerCompleted += worker_RunWorkerCompleted; worker.RunWorkerAsync(); } private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { UpdateMediaCollectionTextBlock.IsEnabled = true; ProcessingMediaTextBlock.Visibility = Visibility.Collapsed; int succeed = _operations.Count(c => c == 1); int updated = _operations.Count(c => c == 0); int failed = _operations.Count(c => c < 0); if (succeed > 0 || updated > 0) { CoreHelper.IsMediaCollectionChanged = true; } UiHelper.ShowPrompt(String.Format("新增:{0}个,更新:{1}个,失败:{2}个", succeed, updated, failed)); _operations = null; }