前段时间写的博文“.NET4.5之初识async与await”有朋友反应说例子不够好,于是也在琢磨着能改善一下。好在最近在做个电子相册,用的背景歌曲是好几首歌,我真不想一点一点地往相册转录的视频里加字幕(有近半个小时的长度),于是想到用这些歌的lrc歌词文件制成一个srt字幕文件,就能直接用于视频播放了。这里正好有文件读写操作,可以使用.NET4.5的新型异步方式来写。
1、LRC格式
歌词文件的格式非常简单,给个示例:
[ti:被遗忘的时光]
[ar:蔡琴]
[al:出塞曲]
[offset:500][00:00.80]是谁在敲打我窗
[00:09.50]是谁在撩动琴弦
这里,ti是标题,ar是歌手,al是专辑,offset是延时(单位ms,正数代表整体后延,负数代表整体前提),后面再就是具体哪个时间后(分:秒.毫秒)显示哪些歌词了。而在视频播放中,实际上只需要用到offset和后面的具体时间。
2、SRT格式
SRT格式是一种非常简单的字幕文件格式,示例:
1
00:00:22,027 --> 00:00:24,320
世人都喜欢抱怨。2
00:00:25,865 --> 00:00:29,783
可事实却是:万事无绝对。
这便是两条字幕,每条字幕有三行,第1行是当前字幕序号(从1开始),第二行是字幕显示的时间段(时:分:秒,毫秒 --> 时:分:秒,毫秒),第三行才是要显示的字幕。对比可以看到,这两种格式的文件还是非常相似的,要做的便是一行一行地读取LRC文件,然后得到时间,组合成起止时间,写成SRT格式。需要提醒的是SRT时间是用逗号来分隔毫秒部分的,而LRC是用的点号。
3、基础数据
作为一个LRC文件,我们的数据结构大概是这样的:
1 public class LRC 2 { 3 public LRC(string path,int rank) 4 { 5 Path = path; 6 Rank = rank; 7 } 8 public string Path {get;set;} 9 public int Rank {get;set;} 10 public TimeSpan Length {get;set;} 11 public int Delay {get;set;} 12 }
在此我省略了一些内容(主要就是INotifyPropertyChanged接口的实现部分),Path是一个歌词文件的所在路径,Rank是它在转换成字幕文件时所在的序号,Length则是歌曲应有的长度(默认为0,程序将计算lrc文件中最后一行歌词),Delay是歌曲的延时(也以ms为单位,正为延时,负为提前),但它与前面LRC文件中的offset不同,这是另外一个校对时间,是由我们自己来输入的,与LRC文件内容无关,默认也为0。
为加载每个歌词文件的信息还会有一个列表,它与前台的列表控件的ItemsSource进行绑定:
// 歌词列表 private ObservableCollection<LRC> _LrcList = new ObservableCollection<LRC>(); public ObservableCollection<LRC> LrcList { get { return _LrcList; } }
还有就是SRT文件目前已经记录的字幕索引:
// 字幕行数 public int Index { get; set; }
4、导出为SRT
这里,不好多说,上省略后的代码:
1 // 生成 2 private async void BTN_Produce_Click(object sender, RoutedEventArgs e) 3 { 4 ....... 5 // 导出 6 try 7 { 8 using (var ms = await CreateStream()) 9 using (FileStream fs = new FileStream(FileName, FileMode.Create)) 10 { 11 ms.WriteTo(fs); 12 ...... 13 } 14 } 15 catch (System.Exception ex) 16 { 17 TB_Message.Text = "导出失败:" + ex.Message; 18 } 19 } 20 21 // 新建工作流 22 private async Task<MemoryStream> CreateStream() 23 { 24 var stream = new MemoryStream(); 25 TimeSpan curTime = new TimeSpan(); 26 Index = 1; 27 foreach (var lrc in LrcList) 28 curTime = await WriteStream(stream, curTime, lrc); 29 return stream; 30 } 31 32 // 读写数据 33 private async Task<TimeSpan> WriteStream(MemoryStream stream, TimeSpan startTime, LRC lrc) 34 { 35 var baseTime = startTime.Add(new TimeSpan(0, 0, 0, 0, lrc.Delay)); 36 var preTime = baseTime; 37 bool isFirstLine = true; 38 string preStr = ""; 39 StreamReader reader = new StreamReader(lrc.Path, Encoding.GetEncoding("GB2312")); 40 StreamWriter writer = new StreamWriter(stream, Encoding.UTF8); 41 Regex timeReg = new Regex(@"(?<=^\[)(\d|\:|\.)+(?=])"); 42 Regex strReg = new Regex(@"(?<=]).+", RegexOptions.RightToLeft); 43 do 44 { 45 try 46 { 47 string line = await reader.ReadLineAsync(); 48 line = line.Trim(); 49 if (line != "") 50 { 51 var match = timeReg.Match(line); 52 // 是时间 53 if (match.Success) 54 { 55 // 计时 56 if (isFirstLine) 57 { 58 preTime = baseTime.Add(TimeSpan.Parse("00:" + match.Value)); // 第一行 59 isFirstLine = false; 60 } 61 else 62 { 63 var curTime = baseTime.Add(TimeSpan.Parse("00:" + match.Value)); // 歌词行 64 // 写入前一行的歌词 LRC格式01:48.292 SRT格式00:01:48,292 65 await writer.WriteAsync((Index++).ToString() + "\n" + 66 string.Format("{0:d2}:{1:d2}:{2:d2},{3:d3}", preTime.Hours, preTime.Minutes, preTime.Seconds, preTime.Milliseconds) + " --> " + 67 string.Format("{0:d2}:{1:d2}:{2:d2},{3:d3}", curTime.Hours, curTime.Minutes, curTime.Seconds, curTime.Milliseconds) + "\n" + 68 preStr + "\n\n"); 69 await writer.FlushAsync(); 70 preTime = curTime; 71 } 72 // 歌词 73 var strMatch = strReg.Match(line); 74 preStr = strMatch.Success ? strMatch.Value : ""; 75 } 76 else 77 { 78 Regex offsetReg = new Regex(@"(?<=^\[offset:)\d+(?=])"); 79 match = offsetReg.Match(line); 80 // 是延时 81 if (match.Success) 82 { 83 var offset = Convert.ToInt32(match.Value); 84 baseTime = baseTime.Add(new TimeSpan(0, 0, 0, 0, offset)); 85 } 86 } 87 } 88 } 89 catch(Exception ex) 90 { 91 TB_Message.Text = "转化时遇到了一个错误,行:" + Index + ",错误:" + ex.Message; 92 } 93 } while (!reader.EndOfStream); 94 // 根据歌曲长度延长时间 95 var addTime = lrc.Length + baseTime - preTime; 96 if (addTime.TotalMilliseconds > 0) 97 preTime = preTime.Add(addTime); 98 // 返回新的起始时间 99 return preTime; 100 }
5、效果图
上图左边就是我最后做成的小工具,因为我用的背景歌曲是一首放完马上放另外一首,所以所有的校时(对应的Delay字段)都是为0,歌曲长度则是通过图中右侧的另外一款软件来获取的(读取的各MP3文件,获取精确到毫秒的长度),特别注意普通的mp3播放软件是不能精确到毫秒的,如果填入的时间只能精确到秒,那么最后生成的字幕随着歌词增多,会越来越不准确。
生成的字幕我经过测试是完全匹配的,任务完成,这样我做的电子相册也能外挂字幕了。如果你想把字幕嵌到画面上,可以考虑用其它软件来合成一次(如强大的MediaCoder)。
附上源代码及可执行文件:LrcToSrt.rar
转载请注明原址:http://www.cnblogs.com/lekko/archive/2013/03/14/2959174.html