zoukankan      html  css  js  c++  java
  • .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 

      

  • 相关阅读:
    事件冒泡与捕获
    $(this)到底是个啥
    监测代码的作用及用法
    响应消息的内容类型text/html与绑定的text/xml内容类型不匹配
    MySqlConnection using MySql.Data.dll
    mysql 导出导入数据库
    ORA-06550 PLS-00103:出现符号“DROP”在需要下列之一时:
    注册InstallShield Limited Edition for Visual Studio 时无法选择国家解决方法
    访问LINQ的结果
    WPF TextBox中keydown事件组合键
  • 原文地址:https://www.cnblogs.com/lekko/p/2959174.html
Copyright © 2011-2022 走看看