AS3歌词秀源码之歌词分析篇

[ 2007-05-13 13:45:11 | 作者: Conanlwl ]
字体大小: | |

想做实现歌词秀效果,首先第一步就是得加载歌词,而现在通用的还是LRC歌词格式,所以在加载歌词之前,我们有必要先了解一下什么是LRC歌词格式.(从百度百科COPY过来的..)

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]Thislineshouldbesungtwice
    [0:5.7]Andthisone...onceonly.
  开发标准(供程序员阅读参考)以下列出了开发支持LRC格式的软件时应遵守的一些标准。
     1.无论是否在行首,行内凡具有“[*:*]”形式的都应认为是标签。(注意:其中的冒号并非全角字符“:”)
  2.凡是标签都不应显示。
  3.凡是标签,且被冒号分隔的两部分都为非负数,则应认为是时间标签。
  4.因此,对于非标准形式(非“[mm:ss]”)的时间标签也应能识别(如“[m:s]”)。
        5.应能正确识别连续的时间标签.如[00:01.555][01:03.234]歌词内容
  6.凡是标签,且非时间标签的,应认为是标识标签。
  7.标识名中大小写等价。
  8.为了向后兼容,应对未定义的新标签作忽略处理。另应对注释标签([:])后的同一行内容作忽略处理。
  9.应允许一行中存在多个标签,并能正确处理。
  10.应能正确处理未排序的标签。

鉴于这上面的一大堆的开发标准,我们必须编写一个容错性较高的歌词解释程序.另外在编写代码之前,建议没有正则表达式认识的人,先恶补一下正则的知识再来吧,因为分析字符串最好的工具莫过于正则表达式了.

在写主代码之前,先贴一个事件类LRCEvents,它的作用也就是存放一些用于广播事件的字符串常量罢了.

package net.conanlwl.LRC
{
    import flash.events.Event;   
    public class LRCEvents extends Event
    {
        public function LRCEvents(type:String, bubbles:Boolean=false, cancelable:Boolean=false)
        {
            super(type, bubbles, cancelable);           
        }        
        public static const LRC_READY:String = "LRCReady";
        public static const LRC_ERROR:String = "LRCError";
        public static const LRC_CHANGE:String = "LRCChange";
    }
}

对歌词分析的最终目的就是,就是生成一个二维数组,二维数组的每一个元素的格式都是[播放歌词的时间且以毫秒为单位 ,  歌词内容]. 所以我们在提取歌词中的时间以后[mm:ss.fff],还得将它换算为毫秒.

我们第一个用到的正则表达式就是:

var r:RegExp = /(\[(\d{1,2}):(\d{1,2})(|\.\d{1,3})\])+[^\[]*/g;

r表示的意思为一句时间歌词格式.也即包括时间戳标签以及该时间所对应的歌词.表达式中的"+"号表示允许同时有多个连续的时间戳标签存在(注意:多个时间标签之间不能有空格.否则空格将也会被认为是歌词),这也正是符合上面提到的开发标准中的第5条.那么,如何描述歌词呢?我认为,一句歌词就是从时间标签结束以后一直到下一个"["出现之前,就为一句歌词的内容了.所以用一个非左括号表达式来匹配歌词内容.

接下来我们只要使用字符串中的match方法来匹配歌词,并将匹配结果存于一个数组中.注意,表达式r的最后必须加上全局标识g,才能够匹配所有符合格式的歌词,否则只会匹配第一句而已.

假设字符串变量s为整个LRC文件里的所有字符串.使用arr_lrc数组来存放歌词

var arr_lrc:Array = s.match(r);

我用了一个辅助表达式r1来匹配一句歌词中的时间标签.此次不需全局标识g了.

var r1:RegExp = /\[(\d{1,2}):(\d{1,2})(|\.\d{1,3})\]+/;

另外,我们还需要一个函数来将时间标签换算为毫秒.我用一个getTimeArray(time:String):Array函数来实现.注意,它的返回值为一个数值,那是因为输入的time字符串有可能是多个连续的时间标签.以下是getTimeArray(time:String):Array函数的实现代码:

        //解析时间戳.如[01:25.36][01.28.39][01.38.30]为毫秒数组
        private function getTimeArray(time:String):Array{
            var r:RegExp = /\[(\d{1,3}):(\d{1,2})(|\.\d{1,3})\]/g;//[mm:ss.fff],全局搜索           
            var result:Array = new Array();
            var m:Array = r.exec(time);
            while(m != null){
                var t:Number = Number(m[1]) * 60 + Number(m[2]) + Number(m[3]);//总秒数
                t -= m_offset/1000;//添加偏移值
                t = Math.floor(t * 100) * 10; //总毫秒数,将ss.fff精确到 ss.ff位.               
                if(t<0)t=0;
                result.push(t);
                m = r.exec(time);
            }
            return result;           
        }

至此,我们就可以通过getLRCArray(value:String):Array函数来分析歌词了,参数value就为整个LRC歌词字符串.

       public function getLRCArray(value:String):Array{
            var arr:Array = new Array();
            var r:RegExp = /(\[(\d{1,2}):(\d{1,2})(|\.\d{1,3})\])+[^\[]*/g;//[mm:ss.fff][mm:ss.fff]...歌词
            var r1:RegExp = /\[(\d{1,2}):(\d{1,2})(|\.\d{1,3})\]+/;//[mm:ss.fff][mm:ss.fff]...
            var s:String = value;//创建歌词副本
            var arr_lrc:Array = s.match(r);
            for(var i:uint=0; i < arr_lrc.length; i++){
                var txt:String = String(arr_lrc[i]).replace(r1,""); //歌词
                txt = txt.replace(/\r|\n/g,""); //去掉换行符
                var timeArray:Array = getTimeArray(r1.exec(arr_lrc[i])[0]);
                for(var j:uint = 0; j<timeArray.length;j++){
                    arr.push([timeArray[j],txt]);
                }
            }
            arr.sortOn("0",Array.NUMERIC);//根据第一维排序
            return arr;
        }

至于其它的标签,如人名,专辑名什么的,我们可以用一个通用的函数getTag(tag:String):String来实现:

public function getTag(tag:String):String{
            var r:RegExp = new RegExp("\\[" + tag + ":([^\\]]*)\\]","i");
            var m:Array = r.exec(lrc);
            if(m != null && m.length == 2){
                return m[1];
            }else    return "";
        }

以下就是分析加载并分析歌词的全代码:

package net.conanlwl.LRC
{
    import flash.events.ErrorEvent;
    import flash.events.Event;
    import flash.events.EventDispatcher;
    import flash.events.IOErrorEvent;
    import flash.net.URLLoader;
    import flash.net.URLLoaderDataFormat;
    import flash.net.URLRequest;
    import flash.utils.ByteArray;
   
    import net.conanlwl.utils.ArrayUtilities;

   
    public class LoadLRC extends EventDispatcher    {
        private var load:URLLoader = new URLLoader();
       
        private var m_lrcURL:String;
        private var lrc:String = "";
        private var m_offset:Number;//时间补偿值,其单位是毫秒,正值表示整体提前,负值相反。这是用于总体调整显示快慢的。
        private var lrcArray:Array = new Array();//一个二维数组.第一维保存播放时间,第二维保存歌词内容
        public function LoadLRC(lrcURL:String)
        {           
            this.m_lrcURL = lrcURL;                   
            load.load(new URLRequest(lrcURL));
            load.dataFormat = URLLoaderDataFormat.BINARY;
            load.addEventListener(Event.COMPLETE,onLRCLoaded);
            load.addEventListener(IOErrorEvent.IO_ERROR,onError);
        }
       
        public function get LRCArray():Array{
            return ArrayUtilities.duplicate(lrcArray,true) as Array; //复制一个副本
        }
       
        private function onLRCLoaded(e:Event):void{
            var bytes:ByteArray = load.data as ByteArray;           
            if(bytes != null)
            {
                //bytes.position = 0;
                lrc = bytes.readMultiByte(bytes.bytesAvailable,"gb2312");
                m_offset = this.offset;
                lrcArray = getLRCArray(lrc);
                dispatchEvent(new LRCEvents(LRCEvents.LRC_READY));
            }
            else
            {//加载错误
                dispatchEvent(new LRCEvents(LRCEvents.LRC_ERROR));
            }
           
        }
       
        private function onError(e:ErrorEvent):void{
            dispatchEvent(new LRCEvents(LRCEvents.LRC_ERROR));
        }
       
        public function getLRCArray(value:String):Array{
            var arr:Array = new Array();
            var r:RegExp = /(\[(\d{1,2}):(\d{1,2})(|\.\d{1,3})\])+[^\[]*/g;//[mm:ss.fff][mm:ss.fff]...歌词
            var r1:RegExp = /\[(\d{1,2}):(\d{1,2})(|\.\d{1,3})\]+/;//[mm:ss.fff][mm:ss.fff]...
            var s:String = value;//创建歌词副本
            var arr_lrc:Array = s.match(r);
            for(var i:uint=0; i < arr_lrc.length; i++){
                var txt:String = String(arr_lrc[i]).replace(r1,""); //歌词
                txt = txt.replace(/\r|\n/g,""); //去掉换行符
                var timeArray:Array = getTimeArray(r1.exec(arr_lrc[i])[0]);
                for(var j:uint = 0; j<timeArray.length;j++){
                    arr.push([timeArray[j],txt]);
                }
            }
            arr.sortOn("0",Array.NUMERIC);//根据第一维排序
            return arr;
        }
        //解析时间戳,如[01:25.36][01.28.39][01.38.30]为毫秒数组
        private function getTimeArray(time:String):Array{
            var r:RegExp = /\[(\d{1,3}):(\d{1,2})(|\.\d{1,3})\]/g;//[mm:ss.fff],全局搜索           
            var result:Array = new Array();
            var m:Array = r.exec(time);
            while(m != null){
                var t:Number = Number(m[1]) * 60 + Number(m[2]) + Number(m[3]);//总秒数
                t -= m_offset/1000;//添加偏移值
                t = Math.floor(t * 100) * 10; //总毫秒数,将ss.fff精确到 ss.ff位.               
                if(t<0)t=0;
                result.push(t);
                m = r.exec(time);
            }
            return result;           
        }       
       
        //得到指定标签的内容
        public function getTag(tag:String):String{
            var r:RegExp = new RegExp("\\[" + tag + ":([^\\]]*)\\]","i");
            var m:Array = r.exec(lrc);
            if(m != null && m.length == 2){
                return m[1];
            }else    return "";
        }       

        //获得艺人名 artist
        public function get Ar():String{
            return getTag("ar");
        }
        //获得曲名 Title
        public function get Ti():String{
            return getTag("ti");
        }
        //获得专辑album
        public function get Al():String{
            return getTag("al");
        }
        //获得编辑LRC歌词的作者by
        public function get By():String{
            return getTag("by");
        }
        //获得总体时间偏移值
        public function get offset():Number{
            var o:String = getTag("offset");
            if(o==null || isNaN(Number(o)))return 0;
            else return Number(o);           
        }

    }
}

这是工具类.目前的功能只有复制数组

package net.conanlwl.utils
{
    public class ArrayUtilities
    {
        public function ArrayUtilities()
        {
        }       
       
        //复制数组,bRecursive为true时,可以复制二维数组
        public static function duplicate(oArray:Object, bRecursive:Boolean = false):Object {
          var oDuplicate:Object;
          if(bRecursive) {
            if(oArray is Array) {
              oDuplicate = new Array();
              for(var i:Number = 0; i < oArray.length; i++) {
                if(oArray[i] is Object) {
                  oDuplicate[i] = duplicate(oArray[i]);
                }
                else {
                  oDuplicate[i] = oArray[i];
                }
              }
              return oDuplicate;
            }
            else {
              var oDuplicate:Object = new Object();
              for(var sItem:String in oArray) {
                if(oArray[sItem] is Object && !(oArray[sItem] is String) && !(oArray[sItem] is Boolean) && !(oArray[sItem] is Number)) {
                  oDuplicate[sItem] = duplicate(oArray[sItem], bRecursive);
                }
                else {
                  oDuplicate[sItem] = oArray[sItem];
                }
              }
              return oDuplicate;
            }
          }
          else {
            if(oArray is Array) {
              return oArray.concat();
            }
            else {
              var oDuplicate:Object = new Object();
              for(var sItem:String in oArray) {
                oDuplicate[sItem] = oArray[sItem];
              }
              return oDuplicate;
            }
          }
    }

    }
}

 

标签: as3  歌词秀  源码  歌词分析 
评论Feed 评论Feed: http://www.conanlwl.net/Feed/Comment/170.aspx
UTF-8 Encoding 引用链接: http://www.conanlwl.net/TrackBack/Save/170.aspx

浏览模式: 显示全部 | 评论: 2 | 引用: 0 | 排序 | 浏览: 531
Loading加载评论中...
Loading加载引用中...

发表评论
相关文章
Loading加载相关文章中...