xref: /MusicPlayer2/MusicPlayer2/CueFile.cpp (revision f712e5792b62528b6d0baf5fe5a5c4f557072bfd)
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