//========================================================================
//TITLE:
// 歌词显示的技术实现
//AUTHOR:
// norains
//DATE:
// Saturday 01-March-2008
//Environment:
// VS2005 + SDK-WINCE5.0-MIPSII
// EVC 4.0 + SDK-WINCE5.0-MIPSII
//========================================================================
确切地说,歌词显示并不是一件很困难的事情,并且实现的方式也多种多样,所以本文只讨论其中一种可能的实现。
本文所要探究的歌词显示类似于MTV的形式,同屏显示双行歌词。当任意一行歌词的显示超过限定的时间,则自动切换。
总体上来说,关键点有两个:
1.获取当前曲目的时间;
2.歌词的存储和获取。
首先我们来看第一点。如果要显示歌词,首要必须知道当前播放的位置。如果播放器是采用DShow写的,那么获取当前时间则是一件非常简单的事情:
IMediaSeeking::GetCurrentPosition(pllPos)
返回的是一个LONG LONG类型,单位为100ns,足够用来表示文件的播放长度了。
位置我们已经获取,接下来需要做的是歌词我们应该如何处理了。
一般的MP3的歌词文件后缀名为LRC,可以直接用记事本打开,里面的内容大抵如此:
[00:00.18]死了都要爱
[00:03.64]不淋漓尽致不痛快
[00:07.54]感情多深只有这样
[00:12.30]才足够表白
[00:14.62]死了都要爱
[00:18.01]不哭到微笑不痛快
简单来说,也就是“时间标签”+“显示内容”。
以双行显示为例,当我们通过IMediaSeeking::GetCurrentPosition函数获取的时间为"00:11:12",那么在屏幕上显示的歌词应该是:“感情多深只有这样”和“才足够表白” 或是 “才足够表白”和“死了都要爱”。
那么接下来最为重要的是,我们获取的歌词该如何存储,才能最快地显示出来。
回头看一下歌词文件的时间标签,为了和IMediaSeeking::GetCurrentPosition获取的数值单位一直,需要对时间标签的数值作转换。如果时间文本为“00:18.01”,那么转换为以100ns为单位的LONG LONG类型数据则是:
(LONGLONG)(00 * 60 + 18)* (10 * 1000 * 1000) + (LONGLONG)01 * 10 * 1000;
为了能做到最快捷的获取,我们可以以该时间作为索引,然后顺序存储。
在这里我是用CStrStore作为存储的容器,关于CStrStore类的信息,可以参考http://blog.csdn.net/norains/archive/2008/02/27/2125651.aspx
CStrStore PartLrc;
...
//保存每行歌词
PartLrc.Add(szLrc);
因为CStrStore主要是用来保存字符串的,没办法再保存时间信息。所以在这里我们定义了一个TIMETAB结构:
typedef struct
{
LONGLONG llTime;
int iIndex;
}TIMETAB,*PTIMETAB;
llTime表示的歌词文件里的时间,iIndex代表的是在CStrStore中存储的字符串的索引号。
我们首先根据获取的标签数量,动态分配TIMETAB数组:
m_pTimeTab = new TIMETAB [m_iPartAmount];
然后存储相关数据:
m_pTimeTab[iIndexTime].llTime = ConvertTime(pszBufA);
m_pTimeTab[iIndexTime].iIndex = iIndexLrc;
m_PartLrc.Add(pszBufW);
使用时,可根据时间标签获取相应的歌词列:
m_PartLrc.GetData(m_pTimeTab[m_iPartCurIndex].iIndex, pszLrc, iLen + 1);
这时候只要显示pszLrc指向的字符串即可。
回头看看我们为什么要将歌词的用CStrStore存储,而用TIMETAB作为索引。因为实际情形是,可能有多个时间标签对应于一句歌词,例如:
[02:27.07][00:49.61]还可以呼吸 心跳也还规律
[02:32.85][00:55.87]只除了寂寞
[02:34.55][00:57.68]它还不肯马上就平息
假设“还可以呼吸 心跳也还规律”在CStrStore中存储的索引为3,则TIMETAB数组在“02:27.07”和“00:49.61”时间段都可以指向3:
timeTab[i].llTime = ConvertTime(TEXT(“02:27.07”));
m_pTimeTab[i].iIndex = 3;
timeTab[j].llTime = ConvertTime(TEXT(“00:49.61”));
m_pTimeTab[j].iIndex = 3;
这对资源的节约是非常明显的,而在嵌入式设备中,这样的节约又是极为重要。所以将存储和索引分离,是一个非常重要的方式。
接下来我们讨论一个非常实际的问题,我们如何确定TIMETAB数组的个数。因为TIMETAB是以时间标签为索引,所以我们只要判断文件中有多少对"[]"即可算出实际个数。
一个简单的算法可以很简单完成:
int iPos = 0;
while( (iPos = FindString(pcszBufIn,"[",iPos)) != -1)
{
iLeft ++;
iPos ++;
}
iPos = 0;
while( (iPos = FindString(pcszBufIn,"]",iPos)) != -1)
{
iRight ++;
iPos ++;
}
iAmount = (iLeft > iRight ? iRight : iLeft);
m_pTimeTab = new TIMETAB [iAmount];
最后就是如何控制了。因为我们可以设想,一句歌词,最短的切换时间不应该少于1s,而这个假设是成立的。所以我们可以建立一个线程,在该线程中,每隔1s获取一次当前歌曲的时间,然后再查找索引,判断是否更新歌词列。
一个简单的行之有效的查找算法可以如下,其中m_iPartCurIndex为当前的索引号,llCurTime为当前调用GetCurrentPosition获取的时间:
while(TRUE)
{
if(m_pTimeTab[m_iPartCurIndex].llTime < llCurTime)
{
break;
}
m_iPartCurIndex --;
if(m_iPartCurIndex < 0)
{
m_iPartCurIndex = 0;
break;
}
}
while(TRUE)
{
if(m_pTimeTab[m_iPartCurIndex + 1].llTime > llCurTime)
{
break;
}
m_iPartCurIndex ++;
if(m_iPartCurIndex >= m_iPartAmount)
{
m_iPartCurIndex = m_iPartAmount - 1;
break;
}
}
最后获得的m_iPartCurIndex即为应该显示的歌词索引。
这里需要注意的是,llCurTime我们必须通过GetCurrentPosition进行获取,而不能以此方式进行累加:
while(TRUE)
{
...
Sleep(1000)
...
llCurTime += 1000;
}
因为Sleep每次休眠的时间不一定精准,随着歌曲的播放,误差会变得越大。
最后,就是歌词的绘制问题,在此就不再赘述。