.NET4.5之自制多LRC转SRT小工具

  前段时间写的博文“.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 

  

posted @ 2013-03-14 13:20  Lekko.Li  阅读(2818)  评论(1编辑  收藏  举报