Unity实现卡拉OK歌词过渡效果
程序员文章站
2023-03-01 13:18:08
好长时间之前做过的一个项目 , 其中设计到用unity模拟卡拉ok歌词过渡的效果 , 如下图所示 ↓ , 这里简单把原理部分分享一下.
文章目录
演示效果 ↓...
好长时间之前做过的一个项目 , 其中设计到用unity模拟卡拉ok歌词过渡的效果 , 如下图所示 ↓ , 这里简单把原理部分分享一下.
文章目录
- 演示效果 ↓
- 歌词效果类 ↓
- 配套资源下载
演示效果 ↓
- 实现歌词动态调整功能
- 实现动态读取歌词文件功能
- 实现歌曲快进快退功能
- 实现歌曲单字时间匹配功能
- 实现可动态更换歌词前景色背景色功能
注:
这里为实现精准过渡效果使用的是ksc歌词文件, 并不是lrc文件哦 .
这其中我认为就是如何实现歌词部分的前景色向后景色过渡的效果了, 开始的时候我想的也是很复杂 , 使用shader的形式实现 ,网上找了一些相关代码 , 发现不是特别理想 , 最终还是自己尝试着用mask来实现的, 发现效果还不错 !
因为今天下班就过年回家啦! 其他细节之后会完善的 , 今天把工程文件先上传了 .
歌词效果类 ↓
using system.collections; using system.collections.generic; using unityengine; using unityengine.ui; using system; using dg.tweening; using dg.tweening.core; /// <summary> /// 用于显示歌词过渡的效果 /// 1. 获得路径加载并解析歌词文件信息 /// 2. 判断当前歌曲是否播放( 歌曲暂停的时候歌词效果也暂停 , 歌曲停止的时候歌词效果消失 ) /// 3. 判断歌曲快进或快退事件 /// </summary> public class layricpaneleffect : monosingleton<layricpaneleffect> { #region *********************************************************************字段 //由外部传入的声音资源 [hideininspector,serializefield] public audiosource audiosource; //歌词前景颜色;歌词后景颜色 [serializefield] public color32 fronttextcolor = color.white, backtextcolor = color.black, outlinecolor = color.white; //歌词面板的前景部分和后景部分 public recttransform rectfrontlyrictext, rectbacklyricmask; public slider slider; //歌词文件路径 [hideininspector,serializefield] public string lyricfilepath; //是否开始播放当前行歌词内容 public bool isstartlyriceffecttransition = true; //歌词调整进度 ( 纠错 ) // [hideininspector] public float lyricadjust = -5f; //歌词文本信息 // [hideininspector] [serializefield,hideininspector] public text _lyrictext; public text _textcontentlyric, _textlogmessage; private vector2 tempfrontsizedelta, tempbacksizedelta; //用于访问歌词正文部分的内容在kscword类中 private ksc.kscword kscword = new ksc.kscword (); private ksc.kscword curkscword = new ksc.kscword (); //内部定时器( 由外部传入参数来控制 , 用来记录歌曲播放的当前时间轴 ) private float _timer = 0.00f; #endregion /// <summary> /// 初始化一些变量 /// </summary> void initsomething () { //坚持对歌词文件进行赋值操作 if (_lyrictext == null || rectfrontlyrictext.getcomponent <contentsizefitter> () == null) { if (rectfrontlyrictext.getcomponent <text> () == null) { _lyrictext = rectfrontlyrictext.gameobject.addcomponent <text> (); } _lyrictext = rectfrontlyrictext.getcomponent <text> (); //保持歌词实现自适应 rectfrontlyrictext.getcomponent <contentsizefitter> ().horizontalfit = contentsizefitter.fitmode.preferredsize; rectfrontlyrictext.getcomponent <contentsizefitter> ().verticalfit = contentsizefitter.fitmode.preferredsize; } rectbacklyricmask.getcomponentinchildren <text> ().text = _lyrictext.text; //歌词颜色的更改初始化 rectbacklyricmask.getcomponentinchildren <text> ().color = backtextcolor; rectbacklyricmask.getcomponentinchildren <outline> ().effectcolor = outlinecolor; rectfrontlyrictext.getcomponent <text> ().color = fronttextcolor; //歌词过渡的前景部分 ( 用于判断过度遮罩的长度范围 ) tempfrontsizedelta = rectfrontlyrictext.sizedelta; tempbacksizedelta = rectbacklyricmask.sizedelta; //是否开始当前歌词行播放标志位 isstartlyriceffecttransition = true; } void awake () { //初始化 initsomething (); } /// <summary> /// 控制歌词面板的显示 /// 1. 仅仅显示歌词面板信息 , 没有过渡效果! /// </summary> /// <param name="row">歌词正文部分行号.</param> /// <param name="ispanelview">if set to <c>true</c> 显示面板歌词</param> public void lyricpanelcontrollerview (ksc.kscword currowinfo, bool ispanelview) { // debug.log ("当前行是否开始=====>" + ispanelview.tostring ()); _textlogmessage.text = isstartlyriceffecttransition.tostring (); rectbacklyricmask.sizedelta = new vector2 (0f, rectfrontlyrictext.sizedelta.y); rectbacklyricmask.getcomponentinchildren <text> ().text = _lyrictext.text = ""; if (ispanelview) { //根据时间得到当前播放的是第i行的歌词 //处理歌词面板信息 , 显示歌词 foreach (var item in currowinfo.perlinelyrics) { _lyrictext.text += item; rectbacklyricmask.getcomponentinchildren<text> ().text = _lyrictext.text; } startcoroutine (lyricpanelcontrollereffect (currowinfo, ispanelview)); } else { stopallcoroutines (); rectbacklyricmask.sizedelta = new vector2 (0f, rectfrontlyrictext.sizedelta.y); // startcoroutine (lyricpanelcontrollereffect (currowinfo, ispanelview)); //当前歌词结束以后将歌词框初始化成0 rectbacklyricmask.getcomponentinchildren <text> ().text = _lyrictext.text = string.empty; } } /// <summary> /// 开始实现歌此过渡效果, 仅仅效果实现 /// 1. 使用dotween的dosizedata实现 /// 2. 动态调整蒙板的sizedata宽度 /// 3. 根据歌曲当前播放的时间进度进行调整 /// </summary> /// <returns>the panel controller effect.</returns> /// <param name="ispaneleffect">if set to <c>true</c> is panel effect.</param> public ienumerator lyricpanelcontrollereffect (ksc.kscword currowinfo, bool ispaneleffect) { //当前时间歌词播放进度的百分比比例 int curwordindex = 0; if (ispaneleffect) { rectbacklyricmask.dorewind (); yield return null; rectbacklyricmask.sizedelta = new vector2 (0f, rectfrontlyrictext.sizedelta.y); //开始效果过渡 if (audiosource.isplaying) { for (int i = 0; i < curkscword.perlineperlyrictime.length; i++) { rectbacklyricmask.dosizedelta ( new vector2 (((float)(i + 1) / curkscword.perlineperlyrictime.length) * rectfrontlyrictext.sizedelta.x, rectfrontlyrictext.sizedelta.y) , curkscword.perlineperlyrictime [i] / 1000f , false).setease (ease.linear); // debug.log ("第" + i + "个歌词时间"); yield return new waitforseconds (curkscword.perlineperlyrictime [i] / 1000f); } } else { debug.logerror ("歌曲没有播放 !!!!"); } } else { yield return null; rectbacklyricmask.dosizedelta (new vector2 (0f, rectfrontlyrictext.sizedelta.y), 0f, true); } } /// <summary> /// 开始播放音乐的时候调用 /// </summary> /// <param name="lyricfilepath">歌词文件路径.</param> /// <param name="audiosource">audiosource用于判断播放状态.</param> /// <param name="frontcolor">前景色.</param> /// <param name="backcolor">后景.</param> /// <param name="isigronlyriccolor">如果设置为 <c>true</c> 则使用系统配置的默认颜色.</param> public void startplaymusic (string lyricfilepath, audiosource audiosource, color frontcolor, color backcolor, color outlinecolor, bool isigronlyriccolor) { _timer = 0f; //初始化ksc文件 ksc.initksc (lyricfilepath); this.lyricfilepath = lyricfilepath; this.audiosource = audiosource; _textcontentlyric.text = string.empty; if (!isigronlyriccolor) { //歌曲颜色信息 this.fronttextcolor = frontcolor; this.backtextcolor = backcolor; this.outlinecolor = outlinecolor; } #region ****************************************************输出歌词文件信息 //对初始化完成后的信息进行输出 if (ksc.instance.songname != null) { print ("歌名==========>" + ksc.instance.songname); } if (ksc.instance.singer != null) { print ("歌手==========>" + ksc.instance.singer); } if (ksc.instance.pinyin != null) { print ("拼音==========>" + ksc.instance.pinyin); } if (ksc.instance.songclass != null) { print ("歌类==========>" + ksc.instance.songclass); } if (ksc.instance.internalnumber > 0) { print ("歌曲编号=======>" + ksc.instance.internalnumber); } if (ksc.instance.mcolor != color.clear) { print ("男唱颜色=======>" + ksc.instance.mcolor); } if (ksc.instance.mcolor != color.clear) { print ("女唱颜色=======>" + ksc.instance.wcolor); } if (ksc.instance.songstyle != null) { print ("风格==========>" + ksc.instance.songstyle); } if (ksc.instance.wordcount > 0) { print ("歌名字数=======>" + ksc.instance.wordcount); } if (ksc.instance.langclass != null) { print ("语言种类=======>" + ksc.instance.langclass); } //一般是独唱歌曲的时候使用全tag标签展现参数信息 foreach (var item in ksc.instance.listtags) { print (item); } #endregion //显示整个歌词内容 for (int i = 0; i < ksc.instance.add.values.count; i++) { ksc.instance.add.trygetvalue (i, out kscword); for (int j = 0; j < kscword.perlinelyrics.length; j++) { _textcontentlyric.text += kscword.perlinelyrics [j]; } _textcontentlyric.text += "\n"; } } /// <summary> /// 停止播放按钮 /// </summary> public void stopplaymusic () { debug.log ("停止播放按钮"); } /// <summary> /// 主要用于歌词部分的卡拉ok过渡效果 /// 1. 动态赋值歌词框的长度 /// 2. 支持快进快退同步显示 /// </summary> int row = 0, temprow = 0; void fixedupdate () { #region *********************************************************播放过渡效果核心代码 //如果是播放状态并且没有快进或快退 , 获得当前播放时间 , 如果都下一句歌词了 , 则切换到下一句歌词进行过渡效果 //1. 是否是暂停; //2. 是否开始播放 //3. 是否播放停止 if (audiosource != null && audiosource.isplaying) { //进度条 slider.value = _timer / audiosource.clip.length; //快进快退快捷键 if (input.getkey (keycode.rightarrow)) { audiosource.time = mathf.clamp ((audiosource.time + 1f), 0f, 4.35f * 60f); } else if (input.getkey (keycode.leftarrow)) { audiosource.time = mathf.clamp ((audiosource.time - 1f), 0f, 4.35f * 60f); // } else if (input.getkeyup (keycode.leftarrow)) { isstartlyriceffecttransition = true; rectbacklyricmask.getcomponentinchildren <text> ().text = rectfrontlyrictext.getcomponent <text> ().text = string.empty; } //实时计时 _timer = audiosource.time; //歌曲开始播放的时间 _textlogmessage.text = _timer.tostring ("f2"); for (int i = 0; i < ksc.instance.add.count; i++) { ksc.instance.add.trygetvalue (i, out kscword); //根据时间判断当前播放的是哪一行的歌词文件 ( 减去0.01可保证两句歌词衔接太快的时候的bug ) if ((_timer >= (kscword.perlinelyricstarttimer + lyricadjust + 0.1f) && _timer <= (kscword.perlintlyricendtimer + lyricadjust - 0.1f)) && isstartlyriceffecttransition) { temprow = i; ksc.instance.add.trygetvalue (temprow, out curkscword); isstartlyriceffecttransition = false; debug.log ("当前播放====>" + i + "行"); //歌词面板显示当前播放内容 lyricpanelcontrollerview (curkscword, !isstartlyriceffecttransition); } else if ((_timer >= (curkscword.perlintlyricendtimer + lyricadjust)) && !isstartlyriceffecttransition) { isstartlyriceffecttransition = true; //设置不显示歌词内容 lyricpanelcontrollerview (curkscword, !isstartlyriceffecttransition); } } // ksc.instance.add.trygetvalue (row, out kscword); // // //根据时间判断当前播放的是哪一行的歌词文件 ( 减去0.01可保证两句歌词衔接太快的时候的bug ) // if ((_timer >= (kscword.perlinelyricstarttimer + lyricadjust + 0.1f) && _timer <= (kscword.perlintlyricendtimer + lyricadjust)) && isstartlyriceffecttransition) { // temprow = row; // ksc.instance.add.trygetvalue (temprow, out curkscword); // isstartlyriceffecttransition = false; // debug.log ("当前播放====>" + row + "行"); // //歌词面板显示当前播放内容 // lyricpanelcontrollerview (curkscword, !isstartlyriceffecttransition); // } else if ((_timer >= (curkscword.perlintlyricendtimer + lyricadjust)) && !isstartlyriceffecttransition) { // isstartlyriceffecttransition = true; // //设置不显示歌词内容 // lyricpanelcontrollerview (curkscword, !isstartlyriceffecttransition); // row = (row + 1) % ksc.instance.add.count; // } #endregion } } }
###ksc文件解析类 ↓
using system.collections; using system.collections.generic; using unityengine; using system.io; using system.text; using unityengine.ui; using system; using system.text.regularexpressions; using system.runtime.interopservices; /// <summary> /// ksc歌词文件解析属性, 单例工具类 ( 解析解析解析解析解析解析解析解析解析!!!!!!重要的事情多说几遍 ) /// 1. 歌词部分标题信息用单例instance访问 /// 2. 正文信息部分使用kscword对象访问( 每句开始时间\结束时间\每句歌词文字的数组\每句歌词文件时间的数组 ) /// </summary> public class ksc : singleton<ksc> { /// <summary> /// 歌曲 歌名 /// </summary> public string songname { get; set; } /// <summary> /// 歌名字数 歌名字数 /// </summary> public int wordcount{ get; set; } /// <summary> /// 歌名字数 歌名的拼音声母 /// </summary> public string pinyin{ get; set; } /// <summary> /// 歌名字数 歌曲语言种类 /// </summary> public string langclass{ get; set; } /// <summary> /// 歌类,如男女乐队等 /// </summary> public string songclass{ get; set; } /// <summary> /// 艺术家 演唱者,对唱则用斜杠"/"分隔 /// </summary> public string singer { get; set; } /// <summary> /// 歌曲编号 歌曲编号 /// </summary> public int internalnumber{ get; set; } /// <summary> /// 歌曲风格 /// </summary> public string songstyle{ get; set; } /// <summary> /// 视频编号 /// </summary> public string videofilename{ get; set; } /// <summary> /// 前景颜色 /// </summary> public color mcolor{ get; set; } /// <summary> /// 后景颜色 /// </summary> public color wcolor{ get; set; } /// <summary> /// 偏移量 /// </summary> public string offset { get; set; } /// <summary> /// 各类标签 /// </summary> public list<string> listtags = new list<string> (); /// <summary> /// 歌词正文部分信息 ( key = 行号 value = 解析出来的歌词正文部分的每句歌词信息 ) /// </summary> public dictionary<int,kscword> add = new dictionary<int, kscword> (); /// <summary> /// 获得歌词信息 /// </summary> /// <param name="lrcpath">歌词路径</param> /// <returns>返回歌词信息(lrc实例)</returns> public static ksc initksc (string lrcpath) { int row = 0; //kscword对象 //清除之前的歌曲歌词, 保持当前 ksc.instance.add.clear (); using (filestream fs = new filestream (lrcpath, filemode.open, fileaccess.read, fileshare.read)) { string line = string.empty; using (streamreader sr = new streamreader (fs, encoding.default)) { while ((line = sr.readline ()) != null) { //每次循环新建一个对象用于存储不同行数内容 ksc.kscword kscword = new ksc.kscword (); #region ######################################合唱歌曲格式 if (line.startswith ("karaoke.songname := '")) { instance.songname = splitstrinfo (line); } else if (line.startswith ("karaoke.internalnumber := ")) { if (splitintinfo (line) != 0) { instance.internalnumber = splitintinfo (line); } } else if (line.startswith ("karaoke.singer := '")) { instance.singer = splitstrinfo (line); } else if (line.startswith ("karaoke.wordcount := ")) { if (splitintinfo (line) != 0) { instance.wordcount = splitintinfo (line); } } else if (line.startswith ("karaoke.pinyin := '")) { instance.pinyin = splitstrinfo (line); } else if (line.startswith ("karaoke.langclass := '")) { instance.langclass = splitstrinfo (line); } else if (line.startswith ("karaoke.songclass := '")) { instance.songclass = splitstrinfo (line); } else if (line.startswith ("karaoke.songstyle := '")) { instance.songstyle = splitstrinfo (line); } else if (line.startswith ("karaoke.videofilename :='")) { instance.videofilename = splitstrinfo (line); } else if (line.startswith ("mcolor :=rgb(")) { if (splitcolorinfo (line) != color.clear) { instance.mcolor = splitcolorinfo (line); } } else if (line.startswith ("wcolor :=rgb(")) { if (splitcolorinfo (line) != color.clear) { instance.wcolor = splitcolorinfo (line); } #endregion #region ##################################################独唱歌曲风格 } else if (line.startswith ("karaoke.tag('")) { //获取tag标签的参数信息 ksc.instance.listtags.add (splittaginfo (line)); #endregion #region ################################################歌词正文部分解析 } else if (line.startswith (("karaoke.add"))) { //表示歌词正文部分 if (splitlyricstarttime (line) != null) { //行号 ( 从0行开始 ) //获取每句歌词部分的开始时间 kscword.perlinelyricstarttimer = splitlyricstarttime (line); //获取每句歌词部分的结束时间 kscword.perlintlyricendtimer = splitlyricendtime (line); //获取每行歌词的内容,并存储到kscword中 ( 歌词文字的数组信息 左 → 右 ) kscword.perlinelyrics = splitperlinelyrics (line); //获取每行中单个文字的过渡时间数组 ( 歌词文字过渡时间数组 左 → 右 ) kscword.perlineperlyrictime = splitperlineperlyrictime (line); ksc.instance.add.add (row, kscword); row++; } } else { //忽略ksc文件中的其他部分 if (line != "" && !line.contains ("createkaraokeobject") && !line.contains ("karaoke.rows") && !line.contains ("karaoke.clear;") && !regex.ismatch (line, @"^\//")) { debug.logwarning ("歌词含有不能解析的部分 ===> " + line); } } #endregion } } } debug.log ("lyricfileinitialized" + " path : \n" + lrcpath); return instance; } #region ****************************************************************解析歌词用的正则表达式部分 需更新 /// <summary> /// 处理信息(私有方法) /// </summary> /// <param name="line"></param> /// <returns>返回基础信息</returns> public static string splitstrinfo (string line) { // char[] ch = new char[]{ '\0', '\0' }; // return line.substring (line.indexof ("'") + 1).trimend (ch); string pattern = @"'\s{1,20}'"; //获取歌曲标签信息 match match = regex.match (line, pattern); //去除两端的小分号 string resout = string.empty; resout = match.value.replace ("\'", string.empty); return resout; } /// <summary> /// 处理参数是数字的情况 /// </summary> /// <returns>the int info.</returns> /// <param name="line">line.</param> public static int splitintinfo (string line) { string pattern = @"\d+"; //获取歌曲标签参数为数字的信息 match match = regex.match (line, pattern); //去除两端的小分号 int result = 0; result = int32.parse (match.value); return result; } /// <summary> /// 处理参数颜色色值的情况 如: mcolor :=rgb(0, 0, 255); /// </summary> /// <returns>the color info.</returns> /// <param name="line">line.</param> public static color32 splitcolorinfo (string line) { string pattern = @"[r,r][g,g][b,g]?[\(](2[0-4][0-9])|25[0-5]|[01]?[0-9][0-9]?"; //获取歌曲标签参数为颜色值的信息 matchcollection matches = regex.matches (line, pattern); return new color (float.parse (matches [0].tostring ()), float.parse (matches [1].tostring ()), float.parse (matches [2].tostring ())); } /// <summary> /// 获取歌曲的标签部分信息 如 : karaoke.tag('语种', '国语'); // 国语/粤语/台语/外语 /// </summary> /// <returns>the tag info.</returns> public static string splittaginfo (string line) { string temp; string pattern = @"\([\w|\w]+\)"; //获取歌曲标签参数为颜色值的信息 match match = regex.match (line, pattern); temp = match.value.replace ("(", string.empty).replace (")", string.empty).replace ("'", string.empty).replace (",", ":"); return temp; } /// <summary> /// 获取每句歌词正文部分的开始时间 (单位 : 秒) /// </summary> /// <returns>the lyric start time.</returns> /// <param name="line">line.</param> public static float splitlyricstarttime (string line) { float time = 0f; regex regex = new regex (@"\d{2}:\d{2}\.\d{2,3}", regexoptions.ignorecase); //匹配单句歌词时间 如: karaoke.add('00:29.412', '00:32.655' matchcollection mc = regex.matches (line); time = (float)timespan.parse ("00:" + mc [0].value).totalseconds; return time; } /// <summary> /// 获取每句歌词正文部分的结束时间 (单位 : 秒) /// </summary> /// <returns>the lyric start time.</returns> /// <param name="line">line.</param> public static float splitlyricendtime (string line) { regex regex = new regex (@"\d{2}:\d{2}\.\d{2,3}", regexoptions.ignorecase); //匹配单句歌词时间 如: karaoke.add('00:29.412', '00:32.655' matchcollection mc = regex.matches (line); float time = (float)timespan.parse ("00:" + mc [1].value).totalseconds; return time; } /// <summary> /// 获取每句歌词部分的每个文字 和 perlineperlyrictime相匹配 (单位 : 秒) /// </summary> /// <returns>the line lyrics.</returns> /// <param name="line">line.</param> public static string[] splitperlinelyrics (string line) { list<string> liststrresults = new list<string> (); string pattern1 = @"\[[\w|\w]{1,}]{1,}"; //获取歌曲正文每个单词 如 : karaoke.add('00:25.183', '00:26.730', '[五][十][六][个][星][座]', '312,198,235,262,249,286'); string pattern2 = @"\'(\w){1,}\'"; //获取歌曲正文每个单词 如 : karaoke.add('00:28.420', '00:35.431', '夕阳底晚风里', '322,1256,2820,217,1313,1083'); match match = (line.contains ("[") && line.contains ("]")) ? regex.match (line, pattern1) : regex.match (line, pattern2); //删除掉 [ ] ' if (match.value.contains ("[") && match.value.contains ("]")) { //用于合唱类型的歌词文件 string[] resultstr = match.value.replace ("][", "/").replace ("[", string.empty).replace ("]", string.empty).split ('/'); foreach (var item in resultstr) { liststrresults.add (item); } } else if (match.value.contains ("'")) { //用于独唱类型的歌词文件 ( 尚未测试英文歌词文件!!!!!!!!!!!!!!!!!!!!!!! ) char[] tempchar = match.value.replace ("'", string.empty).tochararray (); foreach (var item in tempchar) { liststrresults.add (item.tostring ()); } } return liststrresults.toarray (); } /// <summary> /// 获取每句歌词部分的每个文字需要的过渡时间 和 perlinelyrics相匹配 (单位 : 秒) /// </summary> /// <returns>the line per lyric time.</returns> /// <param name="line">line.</param> public static float[] splitperlineperlyrictime (string line) { string pattern = @"\'((\d){0,}\,{0,1}){0,}\'"; //获取歌曲正文每个单词过渡时间 如 : karaoke.add('00:25.183', '00:26.730', '[五][十][六][个][星][座]', '312,198,235,262,249,286'); string str = null; list<float> listfloat = new list<float> (); //删除掉 多余项 str = regex.match (line, pattern).value.replace ("'", string.empty); // debug.log (str); foreach (var item in str.split (',')) { listfloat.add (float.parse (item)); } return listfloat.toarray (); } #endregion #region ********************************************************************歌词正文部分的时间与文字信息 /// <summary> /// 用单独的类来管理歌词的正文部分 ( 在ksc类下 )主要用来存储每句歌词和每个歌词的时间信息 /// 1. 每句歌词的时间的 ( 开始 - 结束 ) /// 2. 每句歌词中单个文字的时间信息 (集合的形式实现) /// </summary> public class kscword { /// <summary> /// 每行歌词部分开始的时间 (单位 : 秒) (key=行号,value=时间) /// </summary> public float perlinelyricstarttimer { get; set; } /// <summary> /// 每行歌词部分结束时间 (单位 : 秒) (key=行号,value=时间) /// </summary> public float perlintlyricendtimer { get; set; } /// <summary> /// 每行歌词的单个文字集合 /// </summary> public string[] perlinelyrics{ get; set; } /// <summary> /// 每行歌词中单个文字的速度过渡信息 (单位 : 毫秒) /// </summary> public float[] perlineperlyrictime{ get; set; } } #endregion }
不敢说代码如何清新脱俗 , 自己也在学习的路上 , 有程序爱好者想要交流的话欢迎交流啊 . 有心的朋友可以看看 , 年后看时间再完善啦
配套资源下载
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。