AS3歌詞同步詳解

AS3實例: LRC 歌詞同步 一、準備工作
 既然要製作歌詞同步程序,首先要準備一首歌,我們就以“周杰倫-青花瓷”爲例。首先要下載這首“青花瓷.mp3”,保存爲“C:\My Player\Music\青花瓷.mp3”。還要下載青花瓷的 LRC 文件,大家可以到網上下載(地址見附錄),將文本內容保存爲“C:\My Player\LRC\青花瓷.lrc”。我們的程序(類和FLA)則保存在“C:\My Player\”文件夾下。
青花瓷.lrc 文件:
[ti:青花瓷]
[ar:周杰倫]
[al:我很忙]
[by:張琪]
[00:00.00]發送短信18到291199下載該歌曲到手機
[00:01.11]青花瓷
[03:36.49]
[00:21.39]素眉勾勒鞦韆話北風龍轉丹 
[00:26.08]屏層鳥繪的牡丹一如你梳妝
[00:30.46]黯然騰香透過窗心事我瞭然 
[00:34.93]宣紙上皺邊直尺各一半
[00:39.49]油色渲染侍女圖因爲被失藏 
[00:43.83]而你嫣然的一笑如含苞待放
[00:48.30]你的美一縷飄散 
[00:50.77]去到我去不了的地方
[02:23.97][00:55.77]
[03:01.92][02:25.63][00:56.90]天正在等煙雨 
[03:03.57][02:27.91][00:58.99]而我在等你 
[03:05.92][02:30.44][01:00.93]炊煙裊裊升起 
[03:07.76][02:32.25][01:03.49]隔江千×××
[03:10.36][02:34.85][01:05.84]在平地書刻你房間上的飄影 
[03:14.67][02:38.73][01:09.87]就當我爲遇見你伏筆
[03:18.83][02:43.35][01:14.34]天正在等煙雨 
[03:21.20][02:45.60][01:16.68]而我在等你 
[03:23.71][02:48.01][01:18.99]月色被打撈起 
[03:25.74][02:50.10][01:21.18]掩蓋了結局
[03:28.33][02:52.54][01:23.72]如傳世的青花瓷在獨自美麗 
[03:32.30][02:56.67][01:27.65]你眼的笑意
[01:50.25]色白花青的景已躍然於碗底 
[01:54.69]臨摹宋體落款時卻惦記着你
[01:59.22]你隱藏在藥效裏一千年的祕密 
[02:03.75]急溪裏猶如羞花沾落地
[02:08.32]林外芭蕉 惹咒語 
[02:10.57]夢幻的銅綠
[02:12.84]而我路過那江南小鎮的等你
[02:17.19]在潑墨山水畫裏 
[02:19.75]你從墨色深處被隱去
 大家也可以把這個文本內容複製下來,然後在“C:\My Player\LRC\”下創建一個文本文檔,將內容粘貼上去,再將文檔保存爲“青花瓷.lrc”,注意擴展名是“.lrc”。



二、LRC 內容分析
 準備工作完成了,下面分析一下這個 LRC 文件。之所以叫 LRC ,是因爲它是 Lyric (歌詞) 的縮寫。這種格式真是一目瞭然,前面“[ ]”中的數字表示其後歌詞的開始時間。例如,“[01:50.25]色白花青的景已躍然於碗底”表示在1分50.25秒時,歌詞內容是“色白花青的景已躍然於碗底”。
 還有一種形式是“[03:01.92][02:25.63][00:56.90]天正在等煙雨”這種形式常用於賦格部分(俗稱:歌曲的高潮部分),它表示在 03:01.92, 02:25.63, 00:56.90 時的歌詞都是“天正在等煙雨”。由於這種形式的存在,使後面的編程稍顯複雜,不過沒關係,憑藉各位的聰明智慧一定沒問題。




三、預備知識
1. ActionScript 3 中默認使用 Unicode 來解碼外部文件,如果讀取的文本不是 Unicode 編碼,而是按照操作系統代碼頁編寫的,比如 GB2312,那麼需要先導入 flash.system.System 類,並在加載外部文本的語句前將 System.useCodePage 設爲 true,默認情況下爲 false,即默認不使用操作系統頁解碼。
 如果 System.useCodePage = false 且外部 LRC 文件編碼格式是 ANSI 的話,那麼顯示的中文歌詞會是亂碼。解決辦法有兩個:一是,將外部 LRC 文件編碼格式改爲 Unicode;二是,不改變外部文件編碼格式,只在文檔類中加入一句 System.useCodePage=true 即可。由於後一種方法使用簡便,我們就採用第二種方法。




2.讀取聲音:
var sound:Sound=new Sound();
sound.load(new URLRequest("Music/青花瓷.mp3"));




3.播放聲音及獲取當前播放時間(毫秒):
var sc:SoundChannel;
var sound:Sound=new Sound();
sound.load(new URLRequest("Music/青花瓷.mp3"));
sc=sound.play();
stage.addEventListener(Event.ENTER_FRAME,EnterFrame);

function EnterFrame(evt:Event):void {
trace(sc.position);
}
這裏將 sc 聲明爲全局變量(或類變量),因爲在多個方法中都要使用它。




4.讀取外部文件:
var loader:URLLoader=new URLLoader();
loader.load(new URLRequest("LRC/青花瓷.lrc"));
loader.addEventListener(Event.COMPLETE,LoadFinish);
function LoadFinish(evt:Event):void {
trace(evt.target.data);
}




5.將字符串按分隔符分隔爲數組(String.split):
var str:String="FL Basic Theory Master";
var array:Array=str.split(" ");
trace(array);
//輸出數組:[[FL],[Basic],[Theory],[Master]]
str=" http://blog.sina.com.cn/yyy98";
array=str.split(".");
trace(array);
//輸出數組:[[http://blog],[sina,com],[cn/yyy98]]




6.簡單的正則表達式應用:
1>獲取匹配次數:
var Pattern:RegExp=/Window/g;
//意思是所有名爲“Window”的字符串
var str:String="Windows seems like a Window, so called Windows OS! ";
trace(str.match(Pattern).length)
//結果:3
2>獲取正確匹配:
var foo:RegExp=/[0-3][0-9]\/[0-1][0-9]\/[0-2][0-9][0-9][0-9]/g;
//意思是所有格式爲“日/月/年”的字符串
var str:String="Date Format: 2006/12/25 2006-12-25 12/25/2007 25/12/2007"
trace(str.match(foo))
//結果:25/12/2007




7.字符串取子串操作(String.substr):
var str:String="[03:01.92][02:25.63][00:56.90]天正在等煙雨";
trace(str.substr(0,30));
//從0號索引開始,取30個字符
//結果:[03:01.92][02:25.63][00:56.90]
trace(str.substr(30));
//只寫一個參數,表示從該索引處到字符串結束位置
//結果:天正在等煙雨




8.數組排序中比較函數的應用:
var a:Object={price:20,number:3};
var b:Object={price:10,number:7};
var c:Object={price:50,number:1};
var amountAry:Array=[a,b,c];
function compare(paraA:Object,paraB:Object):int {
var resultA =paraA.price*paraA.number;
var resultB =paraB.price*paraB.number;
if (resultA > resultB) return 1;  
if (resultA < resultB) return -1;
return 0;
}
amountAry.sort(compare);
trace(amountAry[0].price);  //輸出:50
trace(amountAry[1].price);  //輸出:20
trace(amountAry[2].price);  //輸出:10






四、LRC 的讀取與存儲轉換(使用文檔類設計)
1.讀取 LRC 文件,這一步非常簡單與讀取普通的文本文件是一樣的;
public function LRCPlayer() {
var loader:URLLoader=new URLLoader();
loader.load(new URLRequest("LRC/青花瓷.lrc"));
loader.addEventListener(Event.COMPLETE,LoadFinish);

}
function LoadFinish(evt:Event):void {
trace(evt.target.data);
}
2.將讀取的 LRC 數據按行分割( "\n" 爲換行符),數組的每一個元素代表 LRC 的一行內容;
function LoadFinish(evt:Event):void {
var list:String=evt.target.data;
var listarray:Array=list.split("\n");
trace(listarray);
}
3.在數組中提取每一行的時間及歌詞,解決單時間序列的問題;

(注意!此段代碼只作講解,不以應用)
LRC 內容如下:
[00:43.83]而你嫣然的一笑如含苞待放
[00:48.30]你的美一縷飄散 
[00:50.77]去到我去不了的地方
[03:01.92]天正在等煙雨 
[03:03.57]而我在等你 
[03:05.92]炊煙裊裊升起 
[03:07.76]隔江千×××
代碼如下:
function LoadFinish(evt:Event):void {
var list:String=evt.target.data;
var listarray:Array=list.split("\n");
for (var i=0; i < listarray.length; i++) {
var info:String=listarray[i];
 //提取每行內容,用變量 info 保存
var lyric:String=info.substr(10);
 //將歌詞內容提取到 lyric 變量中
var ctime:String =info.substr(0,10);
 //提取時間序列字串
var ntime:Number=Number(ctime.substr(1,2))*60+Number(ctime.substr(4,5));
 //將時間字串轉換爲計算機可讀取的時間
 var obj:Object=new Object();
obj.timer=ntime*1000;
obj.lyric=lyric;
LRCarray.push(obj);
 //將時間與歌詞保存到一個 Object 中,並壓入LRCarray 數組
 trace(obj.timer,obj.lyric);
}
}
輸出結果:
43830 而你嫣然的一笑如含苞待放
48300 你的美一縷飄散
50770 去到我去不了的地方
181920 天正在等煙雨
183570 而我在等你
185920 炊煙裊裊升起
187760 隔江千×××
4.在LRC文件,還有多時間序列的存在,所以單時間序列算法不能滿足實際需要,下面就來解決多時間序列問題;
LRC 內容如下:
[00:43.83]而你嫣然的一笑如含苞待放
[00:48.30]你的美一縷飄散 
[00:50.77]去到我去不了的地方
[03:01.92][02:25.63][00:56.90]天正在等煙雨 
[03:03.57][02:27.91][00:58.99]而我在等你 
[03:05.92][02:30.44][01:00.93]炊煙裊裊升起 
[03:07.76][02:32.25][01:03.49]隔江千×××
代碼如下:
function LoadFinish(evt:Event):void {
var list:String=evt.target.data;
var listarray:Array=list.split("\n");
var reg:RegExp=/\[[0-5][0-9]:[0-5][0-9].[0-9][0-9]\]/g;
//建立正則表達式,範圍:[00:00.00]~[59:59.99]
for (var i=0; i < listarray.length; i++) {
var info:String=listarray[i];
 //提取每行內容,用變量 info 保存
var len:int=info.match(reg).length;
 //該行擁有時間序列的個數
var timeAry:Array=info.match(reg);
 //將匹配的時間序列保存到 timeAry 數組中
var lyric:String=info.substr(len*10);
 //根據每個時間序列佔10個字符,找出歌詞內容的起點


 //將歌詞提取到 lyric 變量中
 for (var k:int=0; k < timeAry.length; k++) {
var obj:Object=new Object();
  var ctime:String=timeAry[k];
  var ntime:Number=Number(ctime.substr(1,2))*60+Number(ctime.substr(4,5));
  obj.timer=ntime*1000;
  obj.lyric=lyric;
  LRCarray.push(obj);
  trace(obj.timer,obj.lyric);

 }
 //將時間序列轉換爲毫秒並與歌詞一起保存爲一個數組元素
}
}
輸出結果:
43830 而你嫣然的一笑如含苞待放
48300 你的美一縷飄散 
50770 去到我去不了的地方
181920 天正在等煙雨 
145630 天正在等煙雨 
56900 天正在等煙雨 
183570 而我在等你 
147910 而我在等你 
58990 而我在等你 
185920 炊煙裊裊升起 
150440 炊煙裊裊升起 
60930 炊煙裊裊升起 
187760 隔江千×××
152250 隔江千×××
63490 隔江千×××
5.將獲得的 LRCarray 數組按起始時間排序,這對於按序讀取歌詞有重要意義;
LRCarray.sort(
compare);
private function compare(paraA:Object,paraB:Object):int{
if (paraA.timer > paraB.timer) {
 return 1;
}
if (paraA.timer < paraB.timer) {
 return -1;
}
return 0;
}
結果如下:
43830 而你嫣然的一笑如含苞待放
48300 你的美一縷飄散
50770 去到我去不了的地方
56900 天正在等煙雨
58990 而我在等你
60930 炊煙裊裊升起
63490 隔江千×××
145630 天正在等煙雨
147910 而我在等你
150440 炊煙裊裊升起
152250 隔江千×××
181920 天正在等煙雨
183570 而我在等你
185920 炊煙裊裊升起
187760 隔江千×××
6.最後,隨着音樂的播放,讀取播放時間段內的歌詞。用當前播放時間與 LRCarray 中的時間相比較,如果當前時間小於 LRCarray[i].timer 的時間,那麼就顯示 LRCarray[i-1].lyric 的歌詞。爲什麼要顯示 [i-1] 的歌詞呢?比如說當前播放到第 500 秒,讀取的 LRCarray[20].timer 時間是 400 秒,那麼 i++ 。下一次讀取的 LRCarray[21].timer 時間是 700 秒,這時當前播放時間小於讀取的這個時間,就說明當前的第 500 秒仍處於 LRCarray[20].timer 的時間範圍內。
var lrc_txt:TextField=new TextField();
var LRCarray:Array=new Array();
var sc:SoundChannel;
public function LRCPlayer() {
lrc_txt.width=500;
lrc_txt.selectable=false;
addChild(lrc_txt);
//歌詞在文本 lrc_txt 中顯示
var loader:URLLoader=new URLLoader();
loader.load(new URLRequest("LRC/青花瓷.lrc"));
loader.addEventListener(Event.COMPLETE,LoadFinish);
var sound:Sound=new Sound();
sound.load(new URLRequest("Music/青花瓷.mp3"));
sc=sound.play();
//播放聲音,並生成 sc 變量,SoundChannel 類的實例
stage.addEventListener(Event.ENTER_FRAME,SoundPlaying);
//實時刷新歌詞
}
function SoundPlaying(evt:Event):void {
for (var i=1; i < LRCarray.length; i++) {
 if (sc.position < LRCarray[i].timer) {
  lrc_txt.text=LRCarray[i-1].lyric;
  break;
//找到歌詞,跳出循環體
 }
 lrc_txt.text=LRCarray[LRCarray.length-1].lyric;
 //找不到歌詞,說明已超出了最後一句的時間,因此顯示最後一句歌詞
}
}




五、全部代碼(文檔類 LRCPlayer.as):
package {
import flash.display.Sprite;
import flash.net.URLRequest;
import flash.net.URLLoader;
import flash.media.Sound;
import flash.media.SoundChannel;
import flash.events.Event;
import flash.text.TextField;
import flash.system.System;
public class LRCPlayer extends Sprite {
var lrc_txt:TextField=new TextField();
var LRCarray:Array=new Array();
var sc:SoundChannel;
public function LRCPlayer() {
System.useCodePage=true;
lrc_txt.width=500;
lrc_txt.selectable=false;
addChild(lrc_txt);
var loader:URLLoader=new URLLoader();
loader.load(new URLRequest("LRC/青花瓷.lrc"));
loader.addEventListener(Event.COMPLETE,LoadFinish);
var sound:Sound=new Sound();
sound.load(new URLRequest("Music/青花瓷.mp3"));
sc=sound.play();
stage.addEventListener(Event.ENTER_FRAME,SoundPlaying);
}
function SoundPlaying(evt:Event):void {
for (var i=1; i < LRCarray.length; i++) {
 if (sc.position < LRCarray[i].timer) {
  lrc_txt.text=LRCarray[i-1].lyric;
  break;
 }
 lrc_txt.text=LRCarray[LRCarray.length-1].lyric;
}
}
function LoadFinish(evt:Event):void {
var list:String=evt.target.data;
var listarray:Array=list.split("\n");
var reg:RegExp=/\[[0-5][0-9]:[0-5][0-9].[0-9][0-9]\]/g;
for (var i=0; i < listarray.length; i++) {
 var info:String=listarray[i];
 var len:int=info.match(reg).length;
 var timeAry:Array=info.match(reg);
 var lyric:String=info.substr(len*10);
 for (var k:int=0; k < timeAry.length; k++) {
  var obj:Object=new Object();
  var ctime:String=timeAry[k];
  var ntime:Number=Number(ctime.substr(1,2))*60+Number(ctime.substr(4,5));
  obj.timer=ntime*1000;
  obj.lyric=lyric;
  LRCarray.push(obj);
 }
}
LRCarray.sort(compare);
}
private function compare(paraA:Object,paraB:Object):int {
if (paraA.timer > paraB.timer) {
 return 1;
}
if (paraA.timer < paraB.timer) {
 return -1;
}
return 0;
}
}
}




六、*無處不在的優化
 至此,該程序已經可以順利執行了,此處只討論一下優化問題,看不懂可以跳過。
以這段代碼爲例:
function SoundPlaying(evt:Event):void {
for (var i=1; i < LRCarray.length; i++) {
if (sc.position < LRCarray[i].timer) {
 lrc_txt.text=LRCarray[i-1].lyric;
 break;
}
lrc_txt.text=LRCarray[LRCarray.length-1].lyric;
}
}
如果要進行優化,那麼這個 for 循環,應該寫成:
for (var i=1,j=LRCarray.length; i < j; i++) {… …}
這樣在執行判斷時,不必每次都進行 LRCarray.length 操作,該操用於讀取數組長度,執行 Array 類的 length 方法,屬於高級操作,花費的時間要比低級操作多。其實,只要讀取一次長度,然後將結果保存在變量 j 中,每次判斷時讀取 j 的值即可。取值與賦值都屬於低級別的操作,速度較快。同樣的道理,在 if (sc.position < LRCarray[i].timer) {… …} 中的 sc.position 在每次判斷時都要讀取一遍,這時就應將它在循環之前保存到一個變量裏,這段代碼優化後應是這樣:
function SoundPlaying(evt:Event):void {
var now:Number=sc.position;
for (var i=1,j=LRCarray.length; i < j; i++) {
if (now < LRCarray[i].timer) {
 lrc_txt.text=LRCarray[i-1].lyric;
 break;
}
lrc_txt.text=LRCarray[j-1].lyric;
}
}
 在我們的文檔類中還有幾個地方用到了 for 循環,請大家按照上述方法自行優化。
 其實,代碼優化無處不在,其中的學問不勝枚舉,有興趣的朋友可以到我的博客中看一下關於代碼優化的總結貼,見附錄。



七、附錄
LRC 文件下載地址:
http://lrc.bzmtv.com/

http://www.5ilrc.com/


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章