1 #include "stdafx.h"
2 #include "CueFile.h"
3 #include "FilePathHelper.h"
4
5 // 毫秒->帧 以及 帧->毫秒 的转换是有损的,此方法执行四舍五入转换
6 // 另外此处能够保证 帧->毫秒->帧 的连续转换是无损的,这使得重新生成cue时不会因舍入误差导致时间变动
7 // 也就是ms数是不允许改动的,也不应使用别处得来的,否则偏差会有些大
8
CueTime2Time(const wstring & time_str)9 static Time CueTime2Time(const wstring& time_str)
10 {
11 int m{}, s{}, f{};
12 if (swscanf_s(time_str.c_str(), L"%d:%d:%d", &m, &s, &f) != 3)
13 return 0;
14 int ms = std::lround((f % 75) * 1000.0 / 75.0);
15 ms += (m * 60 + s) * 1000;
16 return Time(ms);
17 }
18
Time2CueTime(Time time)19 static string Time2CueTime(Time time)
20 {
21 int ms = time.toInt();
22 int ff = std::lround((ms % 1000) * 75.0 / 1000.0); // 如果ms不是CueTime2Ms产生的,这行有可能得到75
23 ff += ms / 1000 * 75; // 也就是可能需要处理进位,故不能直接将ms处理为分秒
24 char buff[16]{};
25 sprintf_s(buff, "%.2d:%.2d:%.2d", ff / 75 / 60, ff / 75 % 60, ff % 75);
26 return string(buff);
27 }
28
29
CCueFile(const std::wstring & file_path)30 CCueFile::CCueFile(const std::wstring& file_path)
31 : m_file_path(file_path)
32 {
33 string file_content;
34 if (!CCommon::GetFileContent(m_file_path.c_str(), file_content, 102400))
35 return;
36
37 // cue文件较长可以自动检测是否符合UTF8编码
38 m_file_content_wcs = CCommon::StrToUnicode(file_content, m_code_type, true);
39
40 DoAnalysis();
41 }
42
43
CCueFile()44 CCueFile::CCueFile()
45 {
46 }
47
~CCueFile()48 CCueFile::~CCueFile()
49 {
50 }
51
MoveToSongList(vector<SongInfo> & song_list)52 void CCueFile::MoveToSongList(vector<SongInfo>& song_list)
53 {
54 song_list = std::move(m_result);
55 }
56
GetAnalysisResult()57 std::vector<SongInfo>& CCueFile::GetAnalysisResult()
58 {
59 return m_result;
60 }
61
Save(std::wstring file_path)62 bool CCueFile::Save(std::wstring file_path)
63 {
64 if (file_path.empty())
65 file_path = m_file_path;
66 if (m_result.empty())
67 return false;
68 std::ofstream file_stream(file_path, std::ios::binary | std::ios::out | std::ios::trunc);
69 if (file_stream.fail())
70 return false;
71
72 SongInfo first_track{ m_result.front() };
73 //写入流派
74 if (!first_track.genre.empty())
75 file_stream << "REM GENRE " << CCommon::UnicodeToStr(first_track.genre, CodeType::UTF8_NO_BOM) << "\r\n";
76 //写入年份
77 if (!first_track.IsYearEmpty())
78 file_stream << "REM DATE " << CCommon::UnicodeToStr(first_track.get_year(), CodeType::UTF8_NO_BOM) << "\r\n";
79 //写入注释
80 if (!first_track.comment.empty())
81 file_stream << "REM COMMENT \"" << CCommon::UnicodeToStr(first_track.comment, CodeType::UTF8_NO_BOM) << "\"\r\n";
82 //写入唱片集标题
83 file_stream << "TITLE \"" << CCommon::UnicodeToStr(first_track.album, CodeType::UTF8_NO_BOM) << "\"\r\n";
84
85 //写入其他属性
86 for (const auto& property_item : m_cue_property_map)
87 {
88 if (property_item.first != L"REM GENRE" && property_item.first != L"REM DATE" && property_item.first != L"REM COMMENT" && property_item.first != L"TITLE")
89 file_stream << CCommon::UnicodeToStr(property_item.first, CodeType::UTF8_NO_BOM) << " " << CCommon::UnicodeToStr(property_item.second, CodeType::UTF8_NO_BOM) << "\r\n";
90 }
91 //写入文件名
92 auto getCueAudioFileType = [](const std::wstring& file_path) -> std::string
93 {
94 std::wstring file_ext = CFilePathHelper(file_path).GetFileExtension();
95 if (file_ext == L"mp3")
96 return "MP3";
97 else if (file_ext == L"aif" || file_ext == L"aiff")
98 return "AIFF";
99 else
100 return "WAVE";
101 };
102 std::string file_type = getCueAudioFileType(first_track.file_path);
103 file_stream << "FILE \"" << CCommon::UnicodeToStr(first_track.GetFileName(), CodeType::UTF8_NO_BOM) << "\" " << file_type << "\r\n";
104
105 //写入每个音轨
106 size_t index = 0;
107 for (const auto& song : m_result)
108 {
109 //有多个音频文件时写入新的FILE标签
110 if (song.file_path != first_track.file_path)
111 {
112 first_track = song;
113 std::string file_type = getCueAudioFileType(first_track.file_path);
114 file_stream << "FILE \"" << CCommon::UnicodeToStr(first_track.GetFileName(), CodeType::UTF8_NO_BOM) << "\" " << file_type << "\r\n";
115 }
116
117 //音轨信息
118 file_stream << " TRACK ";
119 if (song.track < 10)
120 file_stream << "0";
121 file_stream << song.track << " AUDIO\r\n";
122 //时间
123 if (song.track == 1)
124 {
125 file_stream << " INDEX 01 00:00:00\r\n";
126 }
127 else
128 {
129 if (index > 0)
130 {
131 Time pre_track_end_pos = m_result[index - 1].end_pos;
132 if (pre_track_end_pos != song.start_pos)
133 file_stream << " INDEX 00 " << Time2CueTime(pre_track_end_pos) << "\r\n";
134 file_stream << " INDEX 01 " << Time2CueTime(song.start_pos) << "\r\n";
135 }
136 }
137 //写入曲目标题
138 if (!song.title.empty())
139 file_stream << " TITLE \"" << CCommon::UnicodeToStr(song.title, CodeType::UTF8_NO_BOM) << "\"\r\n";
140 //写入艺术家
141 if (!song.artist.empty())
142 file_stream << " PERFORMER \"" << CCommon::UnicodeToStr(song.artist, CodeType::UTF8_NO_BOM) << "\"\r\n";
143 //写入其他属性
144 auto& track_property_map = m_track_property_maps[song.file_path][song.track];
145 for (const auto& property_item : track_property_map)
146 {
147 if (property_item.first != L"TRACK" && property_item.first != L"PERFORMER" && property_item.first != L"TITLE")
148 file_stream << " " << CCommon::UnicodeToStr(property_item.first, CodeType::UTF8_NO_BOM) << " " << CCommon::UnicodeToStr(property_item.second, CodeType::UTF8_NO_BOM) << "\r\n";
149 }
150
151 index++;
152 }
153
154 file_stream.close();
155 return true;
156 }
157
GetTrackInfo(const std::wstring & audio_path,int track)158 SongInfo& CCueFile::GetTrackInfo(const std::wstring& audio_path, int track)
159 {
160 static SongInfo empty_song_info;
161 if (m_track_property_maps.size() == 1)
162 {
163 for (auto& song : m_result)
164 if (song.track == track)
165 return song;
166 }
167 else
168 {
169 for (auto& song : m_result)
170 if (song.track == track && song.file_path == audio_path)
171 return song;
172 }
173 return empty_song_info;
174 }
175
GetCuePropertyMap() const176 const std::map<std::wstring, std::wstring>& CCueFile::GetCuePropertyMap() const
177 {
178 return m_cue_property_map;
179 }
180
GetTrackPropertyMap(const std::wstring & audio_path,int track)181 const std::map<std::wstring, std::wstring>& CCueFile::GetTrackPropertyMap(const std::wstring& audio_path, int track)
182 {
183 if (m_track_property_maps.size() == 1)
184 return m_track_property_maps.begin()->second[track];
185 else
186 return m_track_property_maps[audio_path][track];
187 }
188
DoAnalysis()189 void CCueFile::DoAnalysis()
190 {
191 CFilePathHelper cue_file_path{ m_file_path };
192
193 size_t index_file{};
194 index_file = m_file_content_wcs.find(L"FILE ");
195 std::wstring cue_head_contents{ m_file_content_wcs.substr(0, index_file) }; //cue的头部
196
197 SongInfo song_info_common{};
198 // 获取一次标签作为默认值,可以取得FILE之前的标签
199 song_info_common.album = GetCommand(cue_head_contents, L"TITLE");
200 song_info_common.genre = GetCommand(cue_head_contents, L"REM GENRE");
201 song_info_common.SetYear(GetCommand(cue_head_contents, L"REM DATE").c_str());
202 song_info_common.comment = GetCommand(cue_head_contents, L"REM COMMENT");
203 song_info_common.album_artist = GetCommand(cue_head_contents, L"PERFORMER ");
204 song_info_common.artist = song_info_common.album_artist;
205 song_info_common.disc_num = static_cast<BYTE>(_wtoi(GetCommand(cue_head_contents, L"REM DISCNUMBER").c_str()));
206 song_info_common.total_discs = static_cast<BYTE>(_wtoi(GetCommand(cue_head_contents, L"REM TOTALDISCS").c_str()));
207 song_info_common.is_cue = true;
208
209 //查找所有属性
210 FindAllProperty(cue_head_contents, m_cue_property_map);
211
212 CCommon::StringNormalize(song_info_common.album);
213 CCommon::StringNormalize(song_info_common.genre);
214 CCommon::StringNormalize(song_info_common.comment);
215
216 size_t index_track{};
217 size_t index_title{};
218 size_t index_artist{};
219 while (true)
220 {
221 index_track = index_file; // 恢复内层break时index_track的值,使其正常查找第一个TRACK
222 // 获取此FILE标签对应path
223 song_info_common.file_path = cue_file_path.GetDir() + GetCommand(m_file_content_wcs, L"FILE ", index_file);
224 size_t next_file_index = m_file_content_wcs.find(L"FILE ", index_file + 6);
225 while (true)
226 {
227 SongInfo song_info{ song_info_common };
228 song_info.cue_file_path = m_file_path;
229 // 查找曲目序号
230 index_track = m_file_content_wcs.find(L"TRACK ", index_track + 6);
231 // 限制TRACK在此FILE范围
232 if (index_track >= next_file_index)
233 break;
234 wstring track_str = m_file_content_wcs.substr(index_track + 6, 3);
235 song_info.track = _wtoi(track_str.c_str());
236
237 auto& track_property_map = m_track_property_maps[song_info.file_path][song_info.track];
238 track_property_map[L"TRACK"] = track_str;
239
240 size_t next_track_index = m_file_content_wcs.find(L"TRACK ", index_track + 6);
241 // 查找曲目标题
242 size_t index2, index3;
243 index_title = m_file_content_wcs.find(L"TITLE ", index_track + 6);
244 if (index_title < next_track_index)
245 {
246 index2 = m_file_content_wcs.find(L'\"', index_title);
247 index3 = m_file_content_wcs.find(L'\"', index2 + 1);
248 song_info.title = m_file_content_wcs.substr(index2 + 1, index3 - index2 - 1);
249 }
250
251 // 查找曲目艺术家
252 index_artist = m_file_content_wcs.find(L"PERFORMER ", index_track + 6);
253 if (index_artist < next_track_index)
254 {
255 index2 = m_file_content_wcs.find(L'\"', index_artist);
256 index3 = m_file_content_wcs.find(L'\"', index2 + 1);
257 song_info.artist = m_file_content_wcs.substr(index2 + 1, index3 - index2 - 1);
258 }
259
260 // 查找曲目位置
261 Time time_index00{}, time_index01{};
262 size_t index00_pos{}, index01_pos{};
263 index00_pos = m_file_content_wcs.find(L"INDEX 00", index_track + 6);
264 index01_pos = m_file_content_wcs.find(L"INDEX 01", index_track + 6);
265 if (index00_pos < next_track_index && index00_pos + 16 < m_file_content_wcs.size())
266 time_index00 = CueTime2Time(m_file_content_wcs.substr(index00_pos + 9, 8));
267 if (index01_pos < next_track_index && index01_pos + 16 < m_file_content_wcs.size())
268 time_index01 = CueTime2Time(m_file_content_wcs.substr(index01_pos + 9, 8));
269
270 song_info.start_pos = time_index01;
271
272 // 每个FILE的第一个TRACK不能执行这个来补充上个TRACK的信息,上个TRACK的结束时间应当从文件获取
273 if (!m_result.empty() && m_file_content_wcs.find(L"TRACK ", index_file + 6) != index_track)
274 {
275 if(!time_index00.isZero())
276 m_result.back().end_pos = time_index00;
277 else
278 m_result.back().end_pos = time_index01;
279 //if(!m_result.back().end_pos.isZero())
280 // m_result.back().lengh = Time(m_result.back().end_pos - m_result.back().start_pos);
281 }
282
283 CCommon::StringNormalize(song_info.title);
284 CCommon::StringNormalize(song_info.artist);
285
286 m_result.push_back(song_info);
287
288 //查找当前音轨的所有标签
289 int index_next_track = m_file_content_wcs.find(L"TRACK ", index_track + 6);
290 FindAllProperty(m_file_content_wcs.substr(index_track, index_next_track - index_track), track_property_map);
291 }
292 // 如果没有下一个FILE标签则退出
293 if (next_file_index == wstring::npos)
294 break;
295 else
296 index_file = next_file_index;
297 }
298
299 //设置曲目总数
300 int total_tracks = m_result.size();
301 for (auto& song_info : m_result)
302 {
303 song_info.total_tracks = total_tracks;
304 }
305 }
306
GetCommand(const wstring & str_contents,const wstring & str,size_t pos)307 wstring CCueFile::GetCommand(const wstring& str_contents, const wstring& str, size_t pos)
308 {
309 if (pos == wstring::npos)
310 return wstring();
311
312 wstring command;
313 size_t index1 = str_contents.find(str, pos);
314 if (index1 == wstring::npos)
315 return wstring();
316 size_t index2 = str_contents.find(L'\"', index1 + str.size());
317 size_t index3 = str_contents.find(L'\"', index2 + 1);
318 size_t index_rtn = str_contents.find(L'\n', index1);
319 if (index2 < index_rtn) //当前行找到了引号,则获取引号之间的字符串
320 {
321 command = str_contents.substr(index2 + 1, index3 - index2 - 1);
322 }
323 else //当前行没有找到引号
324 {
325 index2 = str_contents.find(L' ', index1 + str.size());
326 index3 = index_rtn;
327 size_t count = index3 - index2 - 1;
328 if (count > 0)
329 command = str_contents.substr(index2 + 1, count);
330 }
331
332 return command;
333 }
334
FindAllProperty(const wstring & str_contents,std::map<std::wstring,std::wstring> & property_map)335 void CCueFile::FindAllProperty(const wstring& str_contents, std::map<std::wstring, std::wstring>& property_map)
336 {
337 std::vector<std::wstring> contents_list;
338 CCommon::StringSplitWithMulitChars(str_contents, L"\r\n", contents_list);
339 for (const auto& str : contents_list)
340 {
341 size_t index_quote = str.find(L'\"'); //查找引号
342 size_t index_space1 = str.find(L' '); //查找第1个空格
343 if (index_space1 == std::wstring::npos || index_space1 > index_quote) //空格必须在引号前
344 break;
345 size_t index_space2 = str.find(L' ', index_space1 + 1); //查找第2个空格
346 if (index_space2 > index_quote)
347 index_space2 = std::wstring::npos; //第2个空格在引号后面,则认为无效
348 size_t index{ index_space2 == std::wstring::npos ? index_space1 : index_space2 };
349 std::wstring key = str.substr(0, index);
350 std::wstring value = str.substr(index + 1);
351 CCommon::StringNormalize(key);
352 CCommon::StringNormalize(value);
353 if (CCommon::StringLeftMatch(key, L"FILE") || CCommon::StringLeftMatch(key, L"TRACK") || CCommon::StringLeftMatch(key, L"INDEX"))
354 continue;
355 if (!key.empty())
356 {
357 property_map[key] = value;
358 }
359 }
360 }
361