酷狗 KRC 文件的解析
清理硬盘发现以前写过一个进行一半的代码,这次补全并从硬盘删掉。
格式说明来自 https://shansing.com/read/392/
krc解码并解压缩后得到一个字符串,例子:
[id:$00000000]
[ar:信乐团]
[ti:北京一夜]
[by:韩佯Τé]
[hash:766fe295bf2722a9ede2abdd61d580c1]
[total:278438]
[sign:大家去北京玩一夜吧!!!!]
[53883,3092]<0,632,0>One <632,784,0>Night <1416,372,0>in <1788,548,0>北<2336,755,0>京
[56675,3539]<0,560,0>我<560,416,0>留<976,392,0>下<1368,412,0>许<1780,392,0>多<2172,1366,0>情
[59914,2577]<0,549,0>不<549,276,0>管<825,252,0>你<1077,214,0>爱<1291,182,0>与<1473,212,0>不 <1685,887,0>爱
[62191,3344]<0,560,0>都<560,210,0>是<770,210,0>历<980,204,0>史<1184,202,0>的<1386,564,0>尘<1950,1387,0>埃
开头的几行就不用解释了,lrc也有。
其中快速匹配歌词的可能方式是靠计算歌曲文件的hash,以及匹配歌词与歌曲的total
歌词开始的行格式:
[此行开始时刻距0时刻的毫秒数,此行持续的毫秒数]<0,此字持续的毫秒数,0>歌<此字开始的时刻距此行开始时刻的毫秒数,此字持续的毫秒数,0>词<此字开始的时刻距此行开始时刻的毫秒数,此字持续的毫秒数,0>正<此字开始的时刻距此行开始时刻的毫秒数,此字持续的毫秒数,0>文
具体代码如下:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.IO; 6 using System.IO.Compression; 7 using ICSharpCode.SharpZipLib.Zip.Compression; 8 using ICSharpCode.SharpZipLib.Zip.Compression.Streams; 9 using System.Diagnostics; 10 11 namespace KRC.KRCLib 12 { 13 public static class KRCFile 14 { 15 /// <summary> 16 /// 异或加密 密钥 17 /// </summary> 18 public static readonly char[] KRCFileXorKey = { '@', 'G', 'a', 'w', '^', '2', 't', 'G', 'Q', '6', '1', '-', 'Î', 'Ò', 'n', 'i' }; 19 20 /// <summary> 21 /// KRC 文件头 22 /// </summary> 23 public static readonly char[] KRCFileHead = { 'k', 'r', 'c', '1' }; 24 25 /// <summary> 26 /// KRC 文件头的字节 27 /// </summary> 28 public static readonly byte[] KRCFileHeadBytes = { 0x6B, 0x72, 0x63, 0x31 }; 29 30 31 /// <summary> 32 /// 解码 33 /// </summary> 34 public static string DecodeFileToString(string krcFilePath) 35 { 36 //krc1 37 var headBytes = new byte[4]; 38 byte[] encodedBytes; 39 byte[] zipedBytes; 40 41 using (var krcfs = new FileStream(krcFilePath, FileMode.Open)) 42 { 43 encodedBytes = new byte[krcfs.Length - headBytes.Length]; 44 zipedBytes = new byte[krcfs.Length - headBytes.Length]; 45 46 //读文件头标记 47 krcfs.Read(headBytes, 0, headBytes.Length); 48 49 //读XOR加密的内容 50 krcfs.Read(encodedBytes, 0, encodedBytes.Length); 51 52 //关闭文件 53 krcfs.Close(); 54 } 55 56 for (var i = 0; i < encodedBytes.Length; i++) 57 { 58 zipedBytes[i] = (byte)(encodedBytes[i] ^ KRCFileXorKey[i % 16]); 59 } 60 61 //前面3字节是 UTF-8 的 BOM 62 var unzipedBytes = Decompress(zipedBytes); 63 64 //编码器带有BOM输出时多了3字节,所以跳过开头的3字节bom 65 var text = RemoveBom(Encoding.UTF8.GetString(unzipedBytes)); 66 67 return text; 68 } 69 70 /// <summary> 71 /// 编码到字节数组 72 /// </summary> 73 /// <param name="inText"></param> 74 /// <returns></returns> 75 public static byte[] EncodeStringToBytes(string inText) 76 { 77 //用默认的,编码时带有UTF-8的BOM 78 byte[] inbytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(inText)).ToArray(); 79 80 byte[] zipedBytes = Compress(inbytes); 81 82 int encodedBytesLength = zipedBytes.Length; 83 84 var encodedBytes = new byte[zipedBytes.Length]; 85 86 87 for (int i = 0; i < encodedBytesLength; i++) 88 { 89 int l = i % 16; 90 91 encodedBytes[i] = (byte)(zipedBytes[i] ^ KRCFileXorKey[l]); 92 } 93 94 byte[] byets = null; 95 96 using (var ms = new MemoryStream()) 97 { 98 ms.Write(KRCFileHeadBytes, 0, KRCFileHeadBytes.Length); 99 ms.Write(encodedBytes, 0, encodedBytes.Length); 100 ms.Flush(); 101 byets = ms.ToArray(); 102 } 103 104 return byets; 105 } 106 107 /// <summary> 108 /// 移除UTF-8 BOM 109 /// </summary> 110 /// <param name="p"></param> 111 /// <returns></returns> 112 private static string RemoveBom(string p) 113 { 114 string bomMarkUtf8 = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble()); 115 if (p.StartsWith(bomMarkUtf8)) 116 p = p.Remove(0, bomMarkUtf8.Length); 117 return p.Replace("\0", ""); 118 } 119 120 #region 压缩 解压缩 121 private static byte[] Compress(byte[] pBytes) 122 { 123 byte[] outdata = null; 124 using (var mMemory = new MemoryStream(pBytes)) 125 using (var mStream = new DeflaterOutputStream(mMemory, new Deflater(Deflater.DEFAULT_COMPRESSION), 131072)) 126 { 127 mStream.Write(pBytes, 0, pBytes.Length); 128 mStream.Flush(); 129 mMemory.Flush(); 130 outdata = mMemory.ToArray(); 131 } 132 return outdata; 133 } 134 135 /// <summary> 136 /// 解压缩 137 /// </summary> 138 /// <param name="data"></param> 139 /// <returns></returns> 140 private static byte[] Decompress(byte[] data) 141 { 142 byte[] outdata = null; 143 using (var ms = new MemoryStream()) 144 using (var inputStream = new InflaterInputStream(new MemoryStream(data), new Inflater(false))) 145 { 146 inputStream.CopyTo(ms); 147 ms.Flush(); 148 149 outdata = ms.ToArray(); 150 ms.Close(); 151 } 152 return outdata; 153 } 154 #endregion 155 156 157 } 158 }
1 using System; 2 using System.CodeDom; 3 using System.Diagnostics; 4 using System.Text.RegularExpressions; 5 6 namespace KRC.KRCLib 7 { 8 /// <summary> 9 /// KRC文件行字符 10 /// </summary> 11 [DebuggerDisplay("{DebuggerDisplay}")] 12 public class KRCLyricsChar 13 { 14 /// <summary> 15 /// 字符 16 /// </summary> 17 public char Char { get; set; } 18 19 /// <summary> 20 /// 字符KRC字符串 21 /// </summary> 22 public string KRCCharString 23 { 24 get 25 { 26 return string.Format(@"<{0},{1},{2}>{3}", this.CharStart.TotalMilliseconds, this.CharDuring.TotalMilliseconds, 0, this.Char); 27 } 28 } 29 30 /// <summary> 31 /// 字符起始时间(计算时加上字符所属行的起始时间) 32 /// </summary> 33 public TimeSpan CharStart { get; set; } 34 35 /// <summary> 36 /// 字符时长 37 /// </summary> 38 public TimeSpan CharDuring { get; set; } 39 40 public KRCLyricsChar() 41 { 42 this.CharStart = TimeSpan.Zero; 43 this.CharDuring = TimeSpan.Zero; 44 } 45 46 public KRCLyricsChar(string krcCharString) 47 : this() 48 { 49 var chars = Regex.Match(krcCharString, @"<(\d+),(\d+),(\d+)>(.?)"); 50 51 if (chars.Success) 52 { 53 if (chars.Groups.Count >= 4) 54 { 55 var charstart = chars.Groups[1].Value; 56 var charduring = chars.Groups[2].Value; 57 var unknowAlwaysZero = chars.Groups[3].Value; 58 59 this.CharStart = TimeSpan.FromMilliseconds(double.Parse(charstart)); 60 this.CharDuring = TimeSpan.FromMilliseconds(double.Parse(charduring)); 61 62 if (chars.Groups.Count >= 5) 63 { 64 var charchar = chars.Groups[4].Value; 65 this.Char = char.Parse(charchar); 66 } 67 else 68 { 69 this.Char = char.Parse(" "); 70 } 71 } 72 } 73 } 74 75 public string DebuggerDisplay 76 { 77 get 78 { 79 return string.Format(@"{0:hh\:mm\:ss\.fff} {1:hh\:mm\:ss\.fff} {2}", this.CharStart, this.CharDuring, this.Char); 80 } 81 } 82 } 83 }
1 using System; 2 using System.Collections.Generic; 3 using System.Diagnostics; 4 using System.Linq; 5 using System.Text.RegularExpressions; 6 7 namespace KRC.KRCLib 8 { 9 /// <summary> 10 /// KRC文件行 11 /// </summary> 12 [DebuggerDisplay("{DebuggerDisplay}")] 13 public class KRCLyricsLine 14 { 15 private readonly List<KRCLyricsChar> _chars = new List<KRCLyricsChar>(); 16 17 /// <summary> 18 /// 行字符串 19 /// </summary> 20 public string KRCLineString 21 { 22 get 23 { 24 return string.Format(@"[{0},{1}]{2}", this.LineStart.TotalMilliseconds, this.LineDuring.TotalMilliseconds, 25 string.Join("", this.Chars.Select(x => x.KRCCharString))); 26 } 27 } 28 29 /// <summary> 30 /// 行开始事件 31 /// </summary> 32 public TimeSpan LineStart { get; set; } 33 34 /// <summary> 35 /// 行总时间 36 /// </summary> 37 public TimeSpan LineDuring 38 { 39 get 40 { 41 //计算行时间 42 var sum = this.Chars.Select(x => x.CharDuring.TotalMilliseconds).Sum(); 43 return TimeSpan.FromMilliseconds(sum); 44 } 45 } 46 47 /// <summary> 48 /// 行内字符 49 /// </summary> 50 51 public List<KRCLyricsChar> Chars 52 { 53 get { return _chars; } 54 } 55 56 public KRCLyricsLine() 57 { 58 this.LineStart = TimeSpan.Zero; 59 } 60 61 62 public KRCLyricsLine(string krclinestring):this() 63 { 64 var regLineTime = new Regex(@"^\[(.*),(.*)\](.*)"); 65 66 var m1 = regLineTime.Match(krclinestring); 67 if (m1.Success && m1.Groups.Count == 4) 68 { 69 var linestart = m1.Groups[1].Value; 70 var linelength = m1.Groups[2].Value; 71 72 this.LineStart = TimeSpan.FromMilliseconds(double.Parse(linestart)); 73 //this.LineDuring = TimeSpan.FromMilliseconds(double.Parse(linelength)); 74 75 var linecontent = m1.Groups[3].Value; 76 77 var chars = Regex.Matches(linecontent, @"<(\d+),(\d+),(\d+)>(.?)"); 78 79 foreach (Match m in chars) 80 { 81 this.Chars.Add(new KRCLyricsChar(m.Value)); 82 } 83 } 84 } 85 86 public string DebuggerDisplay 87 { 88 get 89 { 90 return string.Format(@"{0:hh\:mm\:ss\.fff} {1:hh\:mm\:ss\.fff} {2}", this.LineStart, this.LineDuring, 91 string.Join(",", this.Chars.Select(x => x.Char.ToString()))); 92 } 93 } 94 } 95 }
1 using System; 2 using System.Collections.Generic; 3 using System.IO; 4 using System.Linq; 5 using System.Text; 6 using System.Text.RegularExpressions; 7 8 namespace KRC.KRCLib 9 { 10 /// <summary> 11 /// KRC歌词文件 12 /// </summary> 13 public class KRCLyrics 14 { 15 public List<KRCLyricsLine> Lines 16 { 17 get { return _lines; } 18 } 19 20 /// <summary> 21 /// 歌词文本 22 /// </summary> 23 public string KRCString { get; set; } 24 25 /// <summary> 26 /// ID (总是$00000000,意义未知) 27 /// </summary> 28 public string ID { get; set; } 29 30 /// <summary> 31 /// 艺术家 32 /// </summary> 33 public string Ar { get; set; } 34 35 /// <summary> 36 /// 37 /// </summary> 38 public string Al { get; set; } 39 40 /// <summary> 41 /// 标题 42 /// </summary> 43 public string Title { get; set; } 44 45 /// <summary> 46 /// 歌词文件作者 47 /// </summary> 48 public string By { get; set; } 49 50 /// <summary> 51 /// 歌曲文件Hash 52 /// </summary> 53 public string Hash { get; set; } 54 55 /// <summary> 56 /// 总时长 57 /// </summary> 58 public TimeSpan Total 59 { 60 get 61 { 62 //计算总时间=所有行时间 63 var sum = this.Lines.Select(x => x.LineDuring.TotalMilliseconds).Sum(); 64 return TimeSpan.FromMilliseconds(sum); 65 } 66 } 67 68 /// <summary> 69 /// 偏移 70 /// </summary> 71 public TimeSpan Offset { get; set; } 72 73 private readonly List<KRCLyricsLine> _lines = new List<KRCLyricsLine>(); 74 private readonly List<Tuple<Regex, Action<string>>> _properties; 75 private readonly Regex _regGetValueFromKeyValuePair = new Regex(@"\[(.*):(.*)\]"); 76 77 /// <summary> 78 /// 默认构造 79 /// </summary> 80 public KRCLyrics() 81 { 82 //this.Total = TimeSpan.Zero; 83 this.Offset = TimeSpan.Zero; 84 85 this._properties = new List<Tuple<Regex, Action<string>>>() 86 { 87 new Tuple<Regex, Action<string>>(new Regex("\\[id:[^\\]]+\\]"), (s) => { this.ID = s; }), 88 new Tuple<Regex, Action<string>>(new Regex("\\[al:[^\\n]+\\n"), (s) => { this.Al = s; }), 89 new Tuple<Regex, Action<string>>(new Regex("\\[ar:[^\\]]+\\]"), (s) => { this.Ar = s; }), 90 new Tuple<Regex, Action<string>>(new Regex("\\[ti:[^\\]]+\\]"), (s) => { this.Title = s; }), 91 new Tuple<Regex, Action<string>>(new Regex("\\[hash:[^\\n]+\\n"), (s) => { this.Hash = s; }), 92 new Tuple<Regex, Action<string>>(new Regex("\\[by:[^\\n]+\\n"), (s) => { this.By = s; }), 93 new Tuple<Regex, Action<string>>(new Regex("\\[total:[^\\n]+\\n"), (s) => 94 { 95 //this.Total = TimeSpan.FromMilliseconds(double.Parse(s)); 96 }), 97 new Tuple<Regex, Action<string>>(new Regex("\\[offset:[^\\n]+\\n"), (s) => 98 { 99 this.Offset = TimeSpan.FromMilliseconds(double.Parse(s)); 100 }), 101 }; 102 } 103 104 /// <summary> 105 /// 构造 106 /// </summary> 107 /// <param name="krcstring">KRC字符文本</param> 108 private KRCLyrics(string krcstring):this() 109 { 110 this.KRCString = krcstring; 111 this.LoadProperties(); 112 this.LoadLines(); 113 } 114 115 /// <summary> 116 /// 加载KRC属性 117 /// </summary> 118 private void LoadProperties() 119 { 120 foreach (var prop in _properties) 121 { 122 var m = prop.Item1.Match(this.KRCString); 123 if (m.Success) 124 { 125 var mm = _regGetValueFromKeyValuePair.Match(m.Value); 126 127 if (mm.Success && mm.Groups.Count == 3) 128 { 129 prop.Item2(mm.Groups[2].Value); 130 } 131 } 132 } 133 } 134 135 /// <summary> 136 /// 加载KRC所有行数据 137 /// </summary> 138 private void LoadLines() 139 { 140 var linesMachCollection = Regex.Matches(this.KRCString, @"\[\d{1,}[^\n]+\n"); 141 foreach (Match m in linesMachCollection) 142 { 143 this.Lines.Add(new KRCLyricsLine(m.Value)); 144 } 145 } 146 147 /// <summary> 148 /// 保存到文件 149 /// </summary> 150 /// <param name="outputFilePath"></param> 151 public void SaveToFile(string outputFilePath) 152 { 153 var sb = new StringBuilder(); 154 sb.AppendLine(string.Format("[id:{0}]", this.ID)); 155 156 157 if (!string.IsNullOrEmpty(this.Al)) 158 { 159 sb.AppendLine(string.Format("[al:{0}]", this.Al)); 160 } 161 162 if (!string.IsNullOrEmpty(this.Ar)) 163 { 164 sb.AppendLine(string.Format("[ar:{0}]", this.Ar)); 165 } 166 167 if (!string.IsNullOrEmpty(this.Title)) 168 { 169 sb.AppendLine(string.Format("[ti:{0}]", this.Title)); 170 } 171 172 if (!string.IsNullOrEmpty(this.Hash)) 173 { 174 sb.AppendLine(string.Format("[hash:{0}]", this.Hash)); 175 } 176 177 if (!string.IsNullOrEmpty(this.By)) 178 { 179 sb.AppendLine(string.Format("[by:{0}]", this.By)); 180 } 181 182 if (this.Total!= TimeSpan.Zero) 183 { 184 sb.AppendLine(string.Format("[total:{0}]", this.Total.TotalMilliseconds)); 185 } 186 187 if (this.Offset != TimeSpan.Zero) 188 { 189 sb.AppendLine(string.Format("[offset:{0}]", this.Offset.TotalMilliseconds)); 190 } 191 192 193 foreach (var line in this.Lines) 194 { 195 sb.AppendLine(line.KRCLineString); 196 } 197 198 199 var bytes = KRCFile.EncodeStringToBytes(sb.ToString()); 200 201 202 File.WriteAllBytes(outputFilePath, bytes); 203 204 } 205 206 /// <summary> 207 /// 从文件加载 208 /// </summary> 209 /// <param name="inputFilePath"></param> 210 /// <returns></returns> 211 public static KRCLyrics LoadFromFile(string inputFilePath) 212 { 213 var str = KRCFile.DecodeFileToString(inputFilePath); 214 215 return LoadFromString(str); 216 } 217 218 /// <summary> 219 /// 从文本加载 220 /// </summary> 221 /// <param name="krcstring"></param> 222 /// <returns></returns> 223 public static KRCLyrics LoadFromString(string krcstring) 224 { 225 var aa = new KRCLyrics(krcstring); 226 return aa; 227 } 228 } 229 }
1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel; 4 using System.Linq; 5 using System.Runtime.InteropServices; 6 using System.Text; 7 using System.Text.RegularExpressions; 8 using KRC.KRCLib; 9 10 namespace KRC.Test 11 { 12 class Program 13 { 14 static void Main(string[] args) 15 { 16 string inputFile = @"杨钰莹.桃花运-b0c4014bd991a6a637445defa56822f9.krc"; 17 string outputFile = @"123.krc"; 18 KRCLyrics krc = KRCLyrics.LoadFromFile(inputFile); 19 Console.WriteLine("解码 [{0}] 完毕。", inputFile); 20 krc.SaveToFile(outputFile); 21 Console.WriteLine("另存为 [{0}] 完毕。", outputFile); 22 Console.ReadLine(); 23 } 24 } 25 }