歌词显示的技术实现

//========================================================================
//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每次休眠的时间不一定精准,随着歌曲的播放,误差会变得越大。
   
    最后,就是歌词的绘制问题,在此就不再赘述。 

你可能感兴趣的:(WinCE)