AS3歌词秀源码之歌词分析篇
想做实现歌词秀效果,首先第一步就是得加载歌词,而现在通用的还是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函数的实现代码:
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{ 至于其它的标签,如人名,专辑名什么的,我们可以用一个通用的函数getTag(tag:String):String来实现: public function getTag(tag:String):String{ 以下就是分析加载并分析歌词的全代码: package net.conanlwl.LRC 这是工具类.目前的功能只有复制数组
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;
}
var r:RegExp = new RegExp("\\[" + tag + ":([^\\]]*)\\]","i");
var m:Array = r.exec(lrc);
if(m != null && m.length == 2){
return m[1];
}else return "";
}
{
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);
}
}
}
{
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;
}
}
}
}
}
评论Feed: http://www.conanlwl.net/Feed/Comment/170.aspx
引用链接: http://www.conanlwl.net/TrackBack/Save/170.aspx
加载评论中...
加载引用中...
加载相关文章中...
