之前在VC知识库上下载了一个基于Directshow做的音乐播放器,带歌词显示功能,觉得挺酷的。我下载了代码,编译了工程之后,运行起来的界面效果如下:

这个播放器支持的功能有:

支持播放MP3/AAC/WAV等常见的音频格式;

音乐播放控制(播放、暂停、停止、跳跃播放);

调节音量;

音乐播放过程中同步显示歌词;

歌词的文字颜色和背景色可调,并且可设置滚动方向;

歌词显示支持淡入淡出;

因为播放引擎是Directshow,这个播放器支持的音频格式挺广泛的,只要你系统上装了某种格式的解码器,都能解码对应格式的音频。播放音乐的操作封装为一个类来处理,这个类是CDXGraph,类声明如下:

class CDXGraph
{
private:IGraphBuilder *     mGraph;  IMediaControl *        mMediaControl;IMediaEventEx *       mEvent;IBasicVideo *        mBasicVideo;IBasicAudio *       mBasicAudio;IVideoWindow  *     mVideoWindow;IMediaSeeking *        mSeeking;DWORD              mObjectTableEntry; public:CDXGraph();virtual ~CDXGraph();public:virtual bool Create(void);virtual void Release(void);virtual bool Attach(IGraphBuilder * inGraphBuilder);IGraphBuilder * GetGraph(void); // Not outstanding reference countIMediaEventEx * GetEventHandle(void);bool ConnectFilters(IPin * inOutputPin, IPin * inInputPin, const AM_MEDIA_TYPE * inMediaType = 0);void DisconnectFilters(IPin * inOutputPin);//bool SetDisplayWindow(HWND inWindow);bool SetNotifyWindow(HWND inWindow);//bool ResizeVideoWindow(long inLeft, long inTop, long inWidth, long inHeight);void HandleEvent(WPARAM inWParam, LPARAM inLParam);bool Run(void);        // Control filter graphbool Stop(void);bool Pause(void);bool IsRunning(void);  // Filter graph statusbool IsStopped(void);bool IsPaused(void);//bool SetFullScreen(BOOL inEnabled);//bool GetFullScreen(void);// IMediaSeekingbool GetCurrentPosition(LONGLONG * outPosition); //获得当前播放时间(单位为100纳秒, 等于10^(-4) ms )bool GetStopPosition(LONGLONG * outPosition);bool SetCurrentPosition(LONGLONG inPosition); //设置当前播放时间(单位为100纳秒, 等于10^(-4) ms )bool SetStartStopPosition(LONGLONG inStart, LONGLONG inStop);bool GetDuration(LONGLONG * outDuration); //获得文件时间长度(单位为100纳秒, 等于10^(-4) ms )bool SetPlaybackRate(double inRate); //设置播放速度// Attention: range from -10000 to 0, and 0 is FULL_VOLUME.bool SetAudioVolume(long inVolume);//调节音量long GetAudioVolume(void);// Attention: range from -10000(left) to 10000(right), and 0 is both.bool SetAudioBalance(long inBalance);long GetAudioBalance(void);bool RenderFile(const char * inFile);private:void AddToObjectTable(void) ;void RemoveFromObjectTable(void);bool QueryInterfaces(void);
};

在分析歌词显示的代码之前,有必要介绍一下LRC歌词文件的语法和规则:

LRC 歌词是一种包含着“[*:*]”形式的“标签(tag)”的、基于纯文本的歌词专用格式。最早由郭祥祥先生(Djohan)提出并在其程序中得到应用。这种歌词文件既可以用来实现卡拉OK功能(需要专门程序),又能以普通的文字处理软件查看、编辑。当然,实际操作时通常是用专门的LRC歌词编辑软件进行高效编辑的。以下具体介绍LRC格式中的“标签”。

时间标签(Time-tag)

形式为"[mm:ss]"或"[mm:ss.fff]"(分钟数:秒数)。
数字须为非负整数,比如"[12:34.5]"是有效的,而"[0x0C:-34.5]"无效。

它可以位于某行歌词中的任意位置。一行歌词可以包含多个时间标签(比如歌词中的迭句部分)。
根据这些时间标签,用户端程序会按顺序依次高亮显示歌词,从而实现卡拉OK功能。另外,标签无须排序。

标识标签(ID-tags)

其格式为"[标识名:值]"。大小写等价。以下是预定义的标签。

[ar:艺人名]
[ti:曲名]
[al:专辑名]
[by:编者(指编辑LRC歌词的人)]
[offset:时间补偿值] 其单位是毫秒,正值表示整体提前,负值相反。这是用于总体调整显示快慢的。

样例

[ar:unknown]
[ti:sample]
[al:none]
[by:me]
[01:02.355][00:00]This line should be sung twice
[00:05.7]And this one... once only.

接着,看一个真正的歌词文件---青花瓷.lrc:

[ti:青花瓷]
[ar:周杰伦]
[al:我很忙]
[by:High Templar]

[00:00.00]周杰伦 - 青花瓷
[00:01.21]词/方文山  曲/周杰伦
[00:04.88]编辑:High Templar
[00:21.98]素胚勾勒出青花笔锋浓转淡
[00:26.42]瓶身描绘的牡丹一如你初妆
[00:30.93]冉冉檀香透过窗心事我了然
[00:35.26]宣纸上走笔至此搁一半
[00:39.77]釉色渲染仕女图韵味被私藏
[00:44.04]而你嫣然的一笑如含苞开放
[00:48.65]你的美一缕飘散
[00:51.03]去到我去不了的地方
[03:19.79][03:02.06][02:26.43][01:15.35][00:57.61]天青色等烟雨 而我在等你
[03:06.31][02:30.72][01:01.88]炊烟袅袅升起 隔江千万里
[03:10.94][02:35.30][01:06.45]在瓶底书刻隶仿前朝的飘逸
[03:15.36][02:39.70][01:10.80]就当我为遇见你伏笔
[03:24.18][01:19.64]月色被打捞起 云开了结局
[01:24.41]如传世的青花瓷自顾自美丽
[01:27.53]你眼带笑意
[03:40.60][01:32.11]
[01:50.86]色白花青的锦鲤跃然于碗底
[01:55.41]临摹宋体落款时却惦记着你
[01:59.75]你隐藏在窑烧里千年的秘密
[02:04.18]极细腻犹如绣花针落地
[02:08.70]帘外芭蕉惹骤雨门环惹铜绿
[02:13.04]而我路过那江南小镇惹了你
[02:17.74]在泼墨山水画里
[02:19.86]你从墨色深处被隐去
[02:44.17]天青色等烟雨 而我在等你
[02:48.68]月色被打捞起 云开了结局

[03:28.63][02:53.06]如传世的青花瓷自顾自美丽 你眼带笑意

理解LRC格式很简单,这里就不多讲了。

下面讲一下歌词显示窗口的实现。滚动显示歌词由一个名为CLiveWnd的类来处理,这个类继承于CWnd,是一个窗口控件。

程序里定义一个结构体TIMETAG,记录一行歌词的信息,其信息包括:该行歌词的LineIndex,播放时间,水平坐标,整行歌词的Text宽度。

typedef struct tagTime{int     lineNum;float   timeValue;int     dd;//一行歌词在水平滚动模式下的横向坐标(第一句歌词的位置为原点)int     lineWidth; //一行歌词的Text宽度
}TIMETAG;

而歌词的字符串内容呢?它被记录到另外一个数据结构里面,用一个数组来存储所有的歌词(每个元素为一行内容)。

  std::vector<CString> m_vWords; //存储每句歌词

而TIMETAG的信息也作为元素放到另外一个数组里面:

std::vector<TIMETAG> m_vTimeTags; //歌词播放的时间标签

这两个数组的元素是按顺序一一对应的,比如m_vWords数组的第一个元素就对应m_vTimeTags数组的第一个TIMETAG的信息。(疑问:为什么作者不把两个信息都弄到一个结构里面就不得而知了!)

读歌词的相关函数实现如下:

//函数作用:切分一行歌词成多个带时间标签的歌词(一行歌词可以包含多个时间标签),重唱的歌词需要分配一个独立的时间标签;
// 时间标签存储在数组m_vTimeTags里, 每句歌词存储在数组m_vWords里,两个数组通过lineNumber做映射;
// 如果一行中有多个时间标签,表示这句歌词在几个不同时间里被重唱,m_vTimeTags里的多个时间标签可以对应m_vWords的相同的歌词内容
// 如果成功添加一行歌词(不包括带说明意义的标签),返回TRUE,否则返回FALSE
BOOL CLiveWnd::SpliceLine( LPCSTR lpsz, int lineNumber)
{CString str(lpsz);CString sz_time_tag;CString strHead;str.TrimLeft();str.TrimRight();int pos, pos2;pos = str.Find("[");if(pos == -1)return FALSE;bool bAddStr = true;while(pos !=-1 ){str = str.Mid(pos+1);pos = str.Find("]");if(pos!=-1){if(lineNumber<5){if( (pos2 = str.Find(":")) != -1){strHead = str.Left(pos2);if( strHead== "ti" || strHead== "ar" || strHead=="by" || strHead=="al" || strHead == "offset" ){str = str.Left(str.GetLength()-1);bAddStr = false;break;}}  }AddTimeTag(str.Left(pos),lineNumber);str = str.Mid(pos+1);}else{bAddStr = false;return bAddStr;}pos = str.Find("[");}//whileif(bAddStr){str.TrimLeft();str.TrimRight();AddWord(str);}//TRACE("%d, %d \n", m_vTimeTags.size(), m_vWords.size());return bAddStr;
}
BOOL CLiveWnd::ReadFile(LPCSTR lpszFile)
{CStdioFile file;if( !file.Open(lpszFile, CFile::modeRead) )return FALSE;m_vTimeTags.clear();m_vWords.clear();CString line;int i = 0;while( file.ReadString(line)!= FALSE ) //读取每行字符串{//TRACE("%s\n", line);if(!line.IsEmpty())//不是空行{//解析每一行if(SpliceLine(line, i)){i++;}}line.Empty();}file.Close();//最后增加一个无歌词的空行m_vWords.push_back("");TIMETAG tm;tm.lineNum = m_vWords.size()-1;tm.timeValue = ((CTestLyricDlg*)AfxGetMainWnd())->GetSongTotalTime(); //时间值为文件的总时间长度m_vTimeTags.push_back(tm);Sort();TRACE("TimeTag Count: %d, Word Count: %d \n", m_vTimeTags.size(), m_vWords.size());SetHorzData();return TRUE;
}

CLiveWnd重载了定时器OnTimer函数,用于定时刷新窗口中滚动显示的歌词,函数实现如下:

void CLiveWnd::OnTimer(UINT nIDEvent)
{CWnd::OnTimer(nIDEvent);int CurrentTime = GetPlayPos();m_iTime = CurrentTime;int CurrentCentHeight = m_iDrawHeight/2 + m_nTextHeight;int CurrentCentWidth  = m_iDrawWidth/2  + m_nTextSpace;int i,lc,TY,TX;for(i = 0; i<= GetLineCount()-1; i++){if(GetTimeValue(i) > CurrentTime ) break;}if(m_bVertical){ if ( i >= 1 && i < GetLineCount() ){lc = m_nTextHeight * (CurrentTime - GetTimeValue(i-1) ) / (GetTimeValue(i-1) - GetTimeValue(i));CurrentCentHeight = lc + CurrentCentHeight;//当前高亮行的Y坐标TY = CurrentCentHeight - (i - 1) * m_nTextHeight;   }else{TY =  CurrentCentHeight - m_nTextHeight;}m_iCurrent = i-1;if( abs(m_iTextOutY-TY)>1 ){m_iTextOutY = TY;TextToScreen();InvalidateRect(m_rectangle, 0 );}}//ifelse  //水平滚动{if ( i >= 1 && i < GetLineCount() ){int len = GetHorzTextW(i-1);// TRACE("%d,TextLen:%d,CurrentTime:%d\n",i-1, len,CurrentTime/1000);lc = (GetHorzTextW(i-1))*(CurrentTime - GetTimeValue(i-1) ) / (GetTimeValue(i-1) - GetTimeValue(i));CurrentCentWidth = lc + CurrentCentWidth;// int xx = GetHorzLen(i-1);TX = CurrentCentWidth - GetHorzLen(i-1); }else{TX =  CurrentCentWidth + m_nTextSpace;}m_iCurrent = i-1;if(abs(m_iTextOutX- TX)>1){m_iTextOutX = TX;TextToScreen();InvalidateRect(m_rectangle, 0 );}   }//else//UpdateWindow();
}

显示歌词的核心函数是TextToScreen,代码如下:

BOOL CLiveWnd::TextToScreen( void )
{DWORD dwRead = 0;FillRect( m_dcMemoryDC, &m_rectangle, m_hBrushBackground );SetBkMode(m_dcMemoryDC, TRANSPARENT);CRect rcLine;CString strWord;if(m_bVertical)   //垂直滚动{for(int i = 0; i<GetLineCount(); i++){int y = GetY(i);if(y<-m_nTextHeight) continue;if(y>m_iDrawHeight)  break;rcLine.SetRect(0, y, m_iDrawWidth, y+m_nTextHeight);strWord = GetString(i);COLORREF cLeft,cRight,color;if(m_iCurrent-1 == i && i>=0 ){float iOnBand = m_iTime - GetTimeValue(m_iCurrent);float iDelta  = (GetTimeValue(m_iCurrent+1) - GetTimeValue(m_iCurrent))/2; //取相邻两个时间标签的时间间隔的一半为过渡时间if(m_bFadeEffect && iOnBand<iDelta) //淡出{cLeft  = m_clrTextHighlighted;cRight = m_clrForeground;color = RGB((GetRValue(cRight)-GetRValue(cLeft))*((float)iOnBand)/iDelta +GetRValue(cLeft),(GetGValue(cRight)-GetGValue(cLeft))*((float)iOnBand)/iDelta +GetGValue(cLeft),(GetBValue(cRight)-GetBValue(cLeft))*((float)iOnBand)/iDelta +GetBValue(cLeft));::SetTextColor(m_dcMemoryDC, color);}else{::SetTextColor(m_dcMemoryDC, m_clrForeground);}}else if(m_iCurrent== i ){float iOnBand = m_iTime - GetTimeValue(m_iCurrent);float iDelta  = ( GetTimeValue(m_iCurrent+1) - GetTimeValue(m_iCurrent) )/2; //取相邻两个时间标签的时间间隔的一半为过渡时间if(m_bFadeEffect && iOnBand<iDelta) //淡入{cLeft  = m_clrForeground;cRight = m_clrTextHighlighted;color = RGB((GetRValue(cRight)-GetRValue(cLeft))*((float)iOnBand)/iDelta +GetRValue(cLeft),(GetGValue(cRight)-GetGValue(cLeft))*((float)iOnBand)/iDelta +GetGValue(cLeft),(GetBValue(cRight)-GetBValue(cLeft))*((float)iOnBand)/iDelta +GetBValue(cLeft));::SetTextColor(m_dcMemoryDC, color);}else{::SetTextColor(m_dcMemoryDC, m_clrTextHighlighted);}}else{::SetTextColor(m_dcMemoryDC, m_clrForeground);}DrawTextEx( m_dcMemoryDC, strWord.GetBuffer(0), strWord.GetLength(), &rcLine, DT_CENTER, 0 ); //居中显示strWord.ReleaseBuffer();}//for}//水平滚动else{for(int i = 0; i<GetLineCount(); i++){int x = GetX(i);int w = GetHorzTextW(i);if(x<-w)               continue;if(x>m_iDrawWidth)     break;rcLine.SetRect(x, 0, x+w, m_iDrawHeight);strWord = GetString(i);COLORREF cLeft,cRight,color;if(m_iCurrent-1 == i && i>=0 ){float iOnBand = m_iTime - GetTimeValue(i+1);float iDelta  = 1000;if(m_bFadeEffect && iOnBand<iDelta){cLeft  = m_clrTextHighlighted;cRight = m_clrForeground;color = RGB((GetRValue(cRight)-GetRValue(cLeft))*((float)iOnBand)/iDelta +GetRValue(cLeft),(GetGValue(cRight)-GetGValue(cLeft))*((float)iOnBand)/iDelta +GetGValue(cLeft),(GetBValue(cRight)-GetBValue(cLeft))*((float)iOnBand)/iDelta +GetBValue(cLeft));::SetTextColor(m_dcMemoryDC, color);}else{::SetTextColor(m_dcMemoryDC, m_clrForeground);}}else if(m_iCurrent== i ){::SetTextColor(m_dcMemoryDC, m_clrTextHighlighted);}else{::SetTextColor(m_dcMemoryDC, m_clrForeground);}DrawTextEx( m_dcMemoryDC, strWord.GetBuffer(0), strWord.GetLength(), &rcLine, DT_LEFT|DT_VCENTER|DT_SINGLELINE, 0 ); //左对齐显示strWord.ReleaseBuffer();}//for}return true;
}

另外,我运行程序时,在水平滚动模式下当歌词显示到最后一行时,程序出现崩溃,仔细检查代码发现错误是在CLiveWnd::GetHorzTextW,原来是数组越界了。我修改了一下代码,后面就没问题了。

详细的实现细节大家看代码吧, 从我的资源里下载这个工程代码:

http://download.csdn.net/download/zhoubotong2012/10257555

一个基于Directshow实现的音频播放器,支持歌词显示相关推荐

  1. 最简单的基于FFMPEG+SDL的音频播放器

    ===================================================== 最简单的基于FFmpeg的音频播放器系列文章列表: <最简单的基于FFMPEG+SDL ...

  2. 最简单的基于FFMPEG+SDL的音频播放器 ver2 (采用SDL2.0)

    ===================================================== 最简单的基于FFmpeg的音频播放器系列文章列表: <最简单的基于FFMPEG+SDL ...

  3. 最简单的基于FFMPEG SDL的音频播放器

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! ==== ...

  4. 最简单的基于FFMPEG+SDL的音频播放器:拆分-解码器和播放器

    ===================================================== 最简单的基于FFmpeg的音频播放器系列文章列表: <最简单的基于FFMPEG+SDL ...

  5. 最简单的基于FFMPEG+SDL的音频播放器 拆分-解码器和播放器

    ===================================================== 最简单的基于FFmpeg的音频播放器系列文章列表: <最简单的基于FFMPEG+SDL ...

  6. 基于Directshow架构的开源播放器和插件

    > MPC-HC MPC-HC,全称Media Player Classic Home Cinema,是Windows平台上一个非常轻量级的开源媒体播放器.它支持所有常见的视频和音频文件格式的播 ...

  7. 网页版音频播放器,歌词随音乐而动

    制作不易,多多支持,谢谢!! 我的博客里面还有关于视频播放器的,感兴趣的小伙伴可以来看看. 这个是效果图 这是利用audio做的一个歌词随音乐而动的html页面. 这个简单的音频播放器是用ajax请求 ...

  8. WIN32下使用DirectSound接口的简单音频播放器(支持wav和mp3)

    刚好最近接触了一些DirectSound,就写了一个小程序练练手,可以用来添加播放基本的wav和mp3音频文件的播放器.界面只是简单的GDI,dxsdk只使用了DirectSound8相关的接口. D ...

  9. 音乐播放器(3)--歌词显示

    上两篇文章讲述了歌词的在线获取,数据的存储,这一片我们讲解一下歌词的显示 我们现在是要在控制台下显示歌词,同时需要按照特定时间的时间显示出特定的歌词,标准的歌词形式如下: [ti:红玫瑰] [ar:陈 ...

最新文章

  1. 操作系统学习:内存分页与中断
  2. 波场DApp数据分析
  3. Javascript中的日期函数[zz]
  4. binutils-2.22编译心得
  5. POJ3244(工科数学分析)
  6. 【主席树】可持久化数组(金牌导航 可持久化数据结构-3)
  7. 如何对Javascript代码进行二次压缩(混淆)
  8. Qt工作笔记-线程池作用之一:限制系统中执行线程的数量
  9. There is no getter for property named 'XXX' in class 'aaa.bbb.ccc'(终极骚操作的解决方法)...
  10. SparkSQL源代码:GlobalTempView与LocalTempView
  11. 【LeetCode】217. Contains Duplicate (2 solutions)
  12. Admin.Admin/Login --- 后台项目中的管理员及登录模块
  13. VMware Ubuntu虚拟机卡顿慢 解决方法大全
  14. 平凡人的野望:我们为赛博世界保管记忆
  15. 连线杂志:《愤怒的小鸟》长成记
  16. android 图片存取方法,6种备份Android照片的方法
  17. 一种基于时间滑动窗口的黑产团伙挖掘算法
  18. 【GNN应用】金融风控领域
  19. 你知道怎么衡量硬件设备的算力吗?
  20. RED LION 1GS00000

热门文章

  1. java俄罗斯方块简单代码_求个简单的俄罗斯方块java代码,初学者能看懂的
  2. 牛B的35场面试,霸气侧漏
  3. CUDA安装错误解决Missing recommended library
  4. 文案撰写技巧,4招写出高转化的创意文案!
  5. ekf估计电池soc过程推导(一)状态方程列写及离散化
  6. shell脚本之创建文件,自动复制粘贴文件
  7. C++名字转换为数字问题 代码
  8. 【网络杂烩 ---> 网络安全】DLL 注入 --- c/c++ 代码实现(超 · 详细)
  9. 手写简易WEB服务器
  10. 与时间赛跑,我的2012