免費論壇 繁體 | 簡體
Sclub交友聊天~加入聊天室當版主
分享
返回列表 发帖

AS3制作 LRC 歌词同步

本文来自:★flash之路-flash技术交流★ 转帖请注明出处! 作者:flashroad 您是第1706个浏览者

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
【版权】
  1. http://www.blogjava.net/qianbiguabi/archive/2009/01/10/250789.html
复制代码

一、准备工作

既然要制作歌词同步程序,首先要准备一首歌,我们就以“周杰伦-青花瓷”为例。首先要下载这首“青花瓷.mp3”,保存为“C:\My Player\Music\青花瓷.mp3”。
还要下载青花瓷的 LRC 文件,大家可以到网上下载,也可以把下面的文本内容复制下来,然后在“C:\My Player\LRC\”下创建一个文本文档,将内容粘贴上去,再将文档保存为“青花瓷.lrc”,注意扩展名是“.lrc”。我们的程序(类和FLA)则保存在“C:\My Player\”文件夹下。

青花瓷.lrc 文件:
  1. [ti:青花瓷]
  2. [ar:周杰伦]
  3. [al:我很忙]
  4. [by:张琪]
  5. [00:00.00]青花瓷
  6. [03:36.49]
  7. [00:21.39]素眉勾勒秋千话北风龙转丹 
  8. [00:26.08]屏层鸟绘的牡丹一如你梳妆
  9. [00:30.46]黯然腾香透过窗心事我了然 
  10. [00:34.93]宣纸上皱边直尺各一半
  11. [00:39.49]油色渲染侍女图因为被失藏 
  12. [00:43.83]而你嫣然的一笑如含苞待放
  13. [00:48.30]你的美一缕飘散 
  14. [00:50.77]去到我去不了的地方
  15. [02:23.97][00:55.77]
  16. [03:01.92][02:25.63][00:56.90]天正在等烟雨 
  17. [03:03.57][02:27.91][00:58.99]而我在等你 
  18. [03:05.92][02:30.44][01:00.93]炊烟袅袅升起 
  19. [03:07.76][02:32.25][01:03.49]隔江千万里
  20. [03:10.36][02:34.85][01:05.84]在平地书刻你房间上的飘影 
  21. [03:14.67][02:38.73][01:09.87]就当我为遇见你伏笔
  22. [03:18.83][02:43.35][01:14.34]天正在等烟雨 
  23. [03:21.20][02:45.60][01:16.68]而我在等你 
  24. [03:23.71][02:48.01][01:18.99]月色被打捞起 
  25. [03:25.74][02:50.10][01:21.18]掩盖了结局
  26. [03:28.33][02:52.54][01:23.72]如传世的青花瓷在独自美丽 
  27. [03:32.30][02:56.67][01:27.65]你眼的笑意
  28. [01:50.25]色白花青的景已跃然于碗底 
  29. [01:54.69]临摹宋体落款时却惦记着你
  30. [01:59.22]你隐藏在药效里一千年的秘密 
  31. [02:03.75]急溪里犹如羞花沾落地
  32. [02:08.32]林外芭蕉 惹咒语 
  33. [02:10.57]梦幻的铜绿
  34. [02:12.84]而我路过那江南小镇的等你
  35. [02:17.19]在泼墨山水画里 
  36. [02:19.75]你从墨色深处被隐去
复制代码

TOP

二、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 时的歌词都是“天正在等烟雨”。
由于这种形式的存在,使后面的编程稍显复杂,不过没关系,凭借各位的聪明智慧一定没问题。

TOP

三、预备知识

1. ActionScript 3 中默认使用 Unicode 来解码外部文件
如果读取的文本不是 Unicode 编码,而是按照操作系统代码页编写的,比如 GB2312,
那么需要先导入 flash.system.System 类,并在加载外部文本的语句前将 System.useCodePage 设为 true,默认情况下为 false,即默认不使用操作系统页解码。

  如果 System.useCodePage = false 且外部 LRC 文件编码格式是 ANSI 的话,那么显示的中文歌词会是乱码。
解决办法有两个:一是,将外部 LRC 文件编码格式改为 Unicode;
二是,不改变外部文件编码格式,只在文档类中加入一句
  1. System.useCodePage=true
复制代码
即可。
由于后一种方法使用简便,我们就采用第二种方法。



2.读取声音:
  1. var sound:Sound=new Sound();
  2. sound.load(new URLRequest("Music/青花瓷.mp3"));
复制代码
3.播放声音及获取当前播放时间(毫秒):
  1. var sc:SoundChannel;
  2. var sound:Sound=new Sound();
  3. sound.load(new URLRequest("Music/青花瓷.mp3"));
  4. sc=sound.play();
  5. stage.addEventListener(Event.ENTER_FRAME,EnterFrame);
  6. function EnterFrame(evt:Event):void {
  7. trace(sc.position);
  8. }
复制代码
这里将 sc 声明为全局变量(或类变量),因为在多个方法中都要使用它。



4.读取外部文件:
  1. var loader:URLLoader=new URLLoader();
  2. loader.load(new URLRequest("LRC/青花瓷.lrc"));
  3. loader.addEventListener(Event.COMPLETE,LoadFinish);
  4. function LoadFinish(evt:Event):void {
  5. trace(evt.target.data);
  6. }
复制代码
5.将字符串按分隔符分隔为数组(String.split):
  1. var str:String="FL Basic Theory Master";
  2. var array:Array=str.split(" ");
  3. trace(array);
  4. //输出数组:[[FL],[Basic],[Theory],[Master]]
  5. str=" http://blog.sina.com.cn/yyy98";
  6. array=str.split(".");
  7. trace(array);
  8. //输出数组:[[http://blog],[sina,com],[cn/yyy98]]
复制代码
6.简单的正则表达式应用:
1>获取匹配次数:
  1. var Pattern:RegExp=/Window/g;
  2. //意思是所有名为“Window”的字符串
  3. var str:String="Windows seems like a Window, so called Windows OS! ";
  4. trace(str.match(Pattern).length)
  5. //结果:3
复制代码
2>获取正确匹配:
  1. var foo:RegExp=/[0-3][0-9]\/[0-1][0-9]\/[0-2][0-9][0-9][0-9]/g;
  2. //意思是所有格式为“日/月/年”的字符串
  3. var str:String="Date Format: 2006/12/25 2006-12-25 12/25/2007 25/12/2007"
  4. trace(str.match(foo))
  5. //结果:25/12/2007
复制代码
7.字符串取子串操作(String.substr):
  1. var str:String="[03:01.92][02:25.63][00:56.90]天正在等烟雨";
  2. trace(str.substr(0,30));
  3. //从0号索引开始,取30个字符
  4. //结果:[03:01.92][02:25.63][00:56.90]
  5. trace(str.substr(30));
  6. //只写一个参数,表示从该索引处到字符串结束位置
  7. //结果:天正在等烟雨
复制代码
8.数组排序中比较函数的应用:
  1. var a:Object={price:20,number:3};
  2. var b:Object={price:10,number:7};
  3. var c:Object={price:50,number:1};
  4. var amountAry:Array=[a,b,c];
  5. function compare(paraA:Object,paraB:Object):int {
  6. var resultA =paraA.price*paraA.number;
  7. var resultB =paraB.price*paraB.number;
  8. if (resultA > resultB) return 1;  
  9. if (resultA < resultB) return -1;
  10. return 0;
  11. }
  12. amountAry.sort(compare);
  13. trace(amountAry[0].price);  //输出:50
  14. trace(amountAry[1].price);  //输出:20
  15. trace(amountAry[2].price);  //输出:10
复制代码

TOP

四、LRC 的读取与存储转换(使用文档类设计)

1.读取 LRC 文件,这一步非常简单与读取普通的文本文件是一样的;
  1. public function LRCPlayer() {
  2. var loader:URLLoader=new URLLoader();
  3. loader.load(new URLRequest("LRC/青花瓷.lrc"));
  4. loader.addEventListener(Event.COMPLETE,LoadFinish);
  5. }
  6. function LoadFinish(evt:Event):void {
  7. trace(evt.target.data);
  8. }
复制代码
2.将读取的 LRC 数据按行分割( "\n" 为换行符),数组的每一个元素代表 LRC 的一行内容;
  1. function LoadFinish(evt:Event):void {
  2. var list:String=evt.target.data;
  3. var listarray:Array=list.split("\n");
  4. trace(listarray);
  5. }
复制代码


3.在数组中提取每一行的时间及歌词,解决单时间序列的问题;

(注意!此段代码只作讲解,不以应用)
LRC 内容如下:
  1. [00:43.83]而你嫣然的一笑如含苞待放
  2. [00:48.30]你的美一缕飘散 
  3. [00:50.77]去到我去不了的地方
  4. [03:01.92]天正在等烟雨 
  5. [03:03.57]而我在等你 
  6. [03:05.92]炊烟袅袅升起 
  7. [03:07.76]隔江千万里
复制代码

代码如下:
  1. function LoadFinish(evt:Event):void {
  2. var list:String=evt.target.data;
  3. var listarray:Array=list.split("\n");
  4. for (var i=0; i < listarray.length; i++) {
  5.   var info:String=listarray;
  6.   //提取每行内容,用变量 info 保存
  7.   var lyric:String=info.substr(10);
  8.   //将歌词内容提取到 lyric 变量中
  9.   var ctime:String =info.substr(0,10);
  10.   //提取时间序列字串
  11.   var ntime:Number=Number(ctime.substr(1,2))*60+Number(ctime.substr(4,5));
  12.   //将时间字串转换为计算机可读取的时间
  13.   var obj:Object=new Object();
  14.   obj.timer=ntime*1000;
  15.   obj.lyric=lyric;
  16.   LRCarray.push(obj);
  17.   //将时间与歌词保存到一个 Object 中,并压入LRCarray 数组
  18.   trace(obj.timer,obj.lyric);
  19. }
  20. }
复制代码

输出结果:
43830 而你嫣然的一笑如含苞待放
48300 你的美一缕飘散
50770 去到我去不了的地方
181920 天正在等烟雨
183570 而我在等你
185920 炊烟袅袅升起
187760 隔江千万里4.在LRC文件,还有多时间序列的存在,所以单时间序列算法不能满足实际需要,下面就来解决多时间序列问题;
LRC 内容如下:
  1. [00:43.83]而你嫣然的一笑如含苞待放
  2. [00:48.30]你的美一缕飘散 
  3. [00:50.77]去到我去不了的地方
  4. [03:01.92][02:25.63][00:56.90]天正在等烟雨 
  5. [03:03.57][02:27.91][00:58.99]而我在等你 
  6. [03:05.92][02:30.44][01:00.93]炊烟袅袅升起 
  7. [03:07.76][02:32.25][01:03.49]隔江千万里
复制代码

代码如下:
  1. function LoadFinish(evt:Event):void {
  2. var list:String=evt.target.data;
  3. var listarray:Array=list.split("\n");
  4. var reg:RegExp=/\[[0-5][0-9]:[0-5][0-9].[0-9][0-9]\]/g;
  5. //建立正则表达式,范围:[00:00.00]~[59:59.99]
  6. for (var i=0; i < listarray.length; i++) {
  7.   var info:String=listarray;
  8.   //提取每行内容,用变量 info 保存
  9.   var len:int=info.match(reg).length;
  10.   //该行拥有时间序列的个数
  11.   var timeAry:Array=info.match(reg);
  12.   //将匹配的时间序列保存到 timeAry 数组中
  13.   var lyric:String=info.substr(len*10);
  14.   //根据每个时间序列占10个字符,找出歌词内容的起点

  15.   //将歌词提取到 lyric 变量中
  16.   for (var k:int=0; k < timeAry.length; k++) {
  17.   var obj:Object=new Object();
  18.    var ctime:String=timeAry[k];
  19.    var ntime:Number=Number(ctime.substr(1,2))*60+Number(ctime.substr(4,5));
  20.    obj.timer=ntime*1000;
  21.    obj.lyric=lyric;
  22.    LRCarray.push(obj);
  23.    trace(obj.timer,obj.lyric);
  24.   }
  25.   //将时间序列转换为毫秒并与歌词一起保存为一个数组元素
  26. }
  27. }
复制代码

输出结果:
43830 而你嫣然的一笑如含苞待放
48300 你的美一缕飘散 
50770 去到我去不了的地方
181920 天正在等烟雨 
145630 天正在等烟雨 
56900 天正在等烟雨 
183570 而我在等你 
147910 而我在等你 
58990 而我在等你 
185920 炊烟袅袅升起 
150440 炊烟袅袅升起 
60930 炊烟袅袅升起 
187760 隔江千万里
152250 隔江千万里
63490 隔江千万里5.将获得的 LRCarray 数组按起始时间排序,这对于按序读取歌词有重要意义;
  1. LRCarray.sort(compare);
  2. private function compare(paraA:Object,paraB:Object):int{
  3. if (paraA.timer > paraB.timer) {
  4.   return 1;
  5. }
  6. if (paraA.timer < paraB.timer) {
  7.   return -1;
  8. }
  9. return 0;
  10. }
复制代码

结果如下:
43830 而你嫣然的一笑如含苞待放
48300 你的美一缕飘散
50770 去到我去不了的地方
56900 天正在等烟雨
58990 而我在等你
60930 炊烟袅袅升起
63490 隔江千万里
145630 天正在等烟雨
147910 而我在等你
150440 炊烟袅袅升起
152250 隔江千万里
181920 天正在等烟雨
183570 而我在等你
185920 炊烟袅袅升起
187760 隔江千万里
6.最后,随着音乐的播放,读取播放时间段内的歌词。用当前播放时间与 LRCarray 中的时间相比较,如果当前时间小于 LRCarray.timer 的时间,那么就显示 LRCarray[i-1].lyric 的歌词。为什么要显示 [i-1] 的歌词呢?比如说当前播放到第 500 秒,读取的 LRCarray[20].timer 时间是 400 秒,那么 i++ 。下一次读取的 LRCarray[21].timer 时间是 700 秒,这时当前播放时间小于读取的这个时间,就说明当前的第 500 秒仍处于 LRCarray[20].timer 的时间范围内。
  1. var lrc_txt:TextField=new TextField();
  2. var LRCarray:Array=new Array();
  3. var sc:SoundChannel;
  4. public function LRCPlayer() {
  5. lrc_txt.width=500;
  6. lrc_txt.selectable=false;
  7. addChild(lrc_txt);
  8. //歌词在文本 lrc_txt 中显示
  9. var loader:URLLoader=new URLLoader();
  10. loader.load(new URLRequest("LRC/青花瓷.lrc"));
  11. loader.addEventListener(Event.COMPLETE,LoadFinish);
  12. var sound:Sound=new Sound();
  13. sound.load(new URLRequest("Music/青花瓷.mp3"));
  14. sc=sound.play();
  15. //播放声音,并生成 sc 变量,SoundChannel 类的实例
  16. stage.addEventListener(Event.ENTER_FRAME,SoundPlaying);
  17. //实时刷新歌词
  18. }
  19. function SoundPlaying(evt:Event):void {
  20. for (var i=1; i < LRCarray.length; i++) {
  21.   if (sc.position < LRCarray.timer) {
  22.    lrc_txt.text=LRCarray[i-1].lyric;
  23.    break;
  24.   //找到歌词,跳出循环体
  25.   }
  26.   lrc_txt.text=LRCarray[LRCarray.length-1].lyric;
  27.   //找不到歌词,说明已超出了最后一句的时间,因此显示最后一句歌词
  28. }
  29. }
复制代码

TOP

五、全部代码(文档类 LRCPlayer.as):

  1. package {
  2. import flash.display.Sprite;
  3. import flash.net.URLRequest;
  4. import flash.net.URLLoader;
  5. import flash.media.Sound;
  6. import flash.media.SoundChannel;
  7. import flash.events.Event;
  8. import flash.text.TextField;
  9. import flash.system.System;
  10. public class LRCPlayer extends Sprite {
  11. var lrc_txt:TextField=new TextField();
  12. var LRCarray:Array=new Array();
  13. var sc:SoundChannel;
  14. public function LRCPlayer() {
  15. System.useCodePage=true;
  16. lrc_txt.width=500;
  17. lrc_txt.selectable=false;
  18. addChild(lrc_txt);
  19. var loader:URLLoader=new URLLoader();
  20. loader.load(new URLRequest("LRC/青花瓷.lrc"));
  21. loader.addEventListener(Event.COMPLETE,LoadFinish);
  22. var sound:Sound=new Sound();
  23. sound.load(new URLRequest("Music/青花瓷.mp3"));
  24. sc=sound.play();
  25. stage.addEventListener(Event.ENTER_FRAME,SoundPlaying);
  26. }
  27. function SoundPlaying(evt:Event):void {
  28. for (var i=1; i < LRCarray.length; i++) {
  29.   if (sc.position < LRCarray.timer) {
  30.    lrc_txt.text=LRCarray[i-1].lyric;
  31.    break;
  32.   }
  33.   lrc_txt.text=LRCarray[LRCarray.length-1].lyric;
  34. }
  35. }
  36. function LoadFinish(evt:Event):void {
  37. var list:String=evt.target.data;
  38. var listarray:Array=list.split("\n");
  39. var reg:RegExp=/\[[0-5][0-9]:[0-5][0-9].[0-9][0-9]\]/g;
  40. for (var i=0; i < listarray.length; i++) {
  41.   var info:String=listarray;
  42.   var len:int=info.match(reg).length;
  43.   var timeAry:Array=info.match(reg);
  44.   var lyric:String=info.substr(len*10);
  45.   for (var k:int=0; k < timeAry.length; k++) {
  46.    var obj:Object=new Object();
  47.    var ctime:String=timeAry[k];
  48.    var ntime:Number=Number(ctime.substr(1,2))*60+Number(ctime.substr(4,5));
  49.    obj.timer=ntime*1000;
  50.    obj.lyric=lyric;
  51.    LRCarray.push(obj);
  52.   }
  53. }
  54. LRCarray.sort(compare);
  55. }
  56. private function compare(paraA:Object,paraB:Object):int {
  57. if (paraA.timer > paraB.timer) {
  58.   return 1;
  59. }
  60. if (paraA.timer < paraB.timer) {
  61.   return -1;
  62. }
  63. return 0;
  64. }
  65. }
  66. }
复制代码

TOP

六、*无处不在的优化

至此,该程序已经可以顺利执行了,此处只讨论一下优化问题,看不懂可以跳过。
以这段代码为例:
  1. function SoundPlaying(evt:Event):void {
  2. for (var i=1; i < LRCarray.length; i++) {
  3. if (sc.position < LRCarray[i].timer) {
  4.   lrc_txt.text=LRCarray[i-1].lyric;
  5.   break;
  6. }
  7. lrc_txt.text=LRCarray[LRCarray.length-1].lyric;
  8. }
  9. }
复制代码
如果要进行优化,那么这个 for 循环,应该写成:
  1. for (var i=1,j=LRCarray.length; i < j; i++) {… …}
复制代码
这样在执行判断时,不必每次都进行 LRCarray.length 操作,该操用于读取数组长度,执行 Array 类的 length 方法,属于高级操作,花费的时间要比低级操作多。其实,只要读取一次长度,然后将结果保存在变量 j 中,每次判断时读取 j 的值即可。取值与赋值都属于低级别的操作,速度较快。同样的道理,在 if (sc.position < LRCarray.timer) {… …} 中的 sc.position 在每次判断时都要读取一遍,这时就应将它在循环之前保存到一个变量里,这段代码优化后应是这样:
  1. function SoundPlaying(evt:Event):void {
  2. var now:Number=sc.position;
  3. for (var i=1,j=LRCarray.length; i < j; i++) {
  4. if (now < LRCarray[i].timer) {
  5.   lrc_txt.text=LRCarray[i-1].lyric;
  6.   break;
  7. }
  8. lrc_txt.text=LRCarray[j-1].lyric;
  9. }
  10. }
复制代码
在我们的文档类中还有几个地方用到了 for 循环,请大家按照上述方法自行优化。

TOP

返回列表