1 #include "core/logging.h"
2 #include "core/timeconstants.h"
3 #include "gmereader.h"
4 #include "tagreader.h"
5 
6 #include <apefile.h>
7 #include <tag.h>
8 #include <QByteArray>
9 #include <QChar>
10 #include <QFile>
11 #include <QFileInfo>
12 #include <QString>
13 #include <QtEndian>
14 
IsSupportedFormat(const QFileInfo & file_info)15 bool GME::IsSupportedFormat(const QFileInfo& file_info) {
16   return (file_info.completeSuffix().endsWith("spc") ||
17           file_info.completeSuffix().endsWith("vgm"));
18 }
19 
ReadFile(const QFileInfo & file_info,pb::tagreader::SongMetadata * song_info)20 void GME::ReadFile(const QFileInfo& file_info,
21                    pb::tagreader::SongMetadata* song_info) {
22   if (file_info.completeSuffix().endsWith("spc"))
23     SPC::Read(file_info, song_info);
24   if (file_info.completeSuffix().endsWith("vgm"))
25     VGM::Read(file_info, song_info);
26 }
27 
Read(const QFileInfo & file_info,pb::tagreader::SongMetadata * song_info)28 void GME::SPC::Read(const QFileInfo& file_info,
29                     pb::tagreader::SongMetadata* song_info) {
30   QFile file(file_info.filePath());
31   if (!file.open(QIODevice::ReadOnly)) return;
32 
33   qLog(Debug) << "Reading tags from SPC file: " << file_info.fileName();
34 
35   // Check for header -- more reliable than file name alone.
36   if (!file.read(33).startsWith(QString("SNES-SPC700").toLatin1())) return;
37 
38   /*
39    * First order of business -- get any tag values that exist within the core
40    * file information. These only allow for a certain number of bytes
41    * per field, so they will likely be overwritten either by the id666 standard
42    * or the APETAG format (as used by other players, such as foobar and winamp)
43    *
44    * Make sure to check id6 documentation before changing the read values!
45    */
46 
47   file.seek(HAS_ID6_OFFSET);
48   bool has_id6 = (file.read(1)[0] == (char)xID6_STATUS::ON);
49 
50   file.seek(SONG_TITLE_OFFSET);
51   song_info->set_title(QString::fromLatin1(file.read(32)).toStdString());
52 
53   file.seek(GAME_TITLE_OFFSET);
54   song_info->set_album(QString::fromLatin1(file.read(32)).toStdString());
55 
56   file.seek(ARTIST_OFFSET);
57   song_info->set_artist(QString::fromLatin1(file.read(32)).toStdString());
58 
59   file.seek(INTRO_LENGTH_OFFSET);
60   QByteArray length_bytes = file.read(INTRO_LENGTH_SIZE);
61   quint64 length_in_sec = 0;
62   if (length_bytes.size() >= INTRO_LENGTH_SIZE) {
63     length_in_sec = ConvertSPCStringToNum(length_bytes);
64     qLog(Debug) << length_in_sec << "------ LENGTH";
65 
66     if (!length_in_sec || length_in_sec >= 0x1FFF) {
67       // This means that parsing the length as a string failed, so get value LE.
68       length_in_sec =
69           length_bytes[0] | (length_bytes[1] << 8) | (length_bytes[2] << 16);
70     }
71 
72     if (length_in_sec < 0x1FFF) {
73       song_info->set_length_nanosec(length_in_sec * kNsecPerSec);
74     }
75   }
76 
77   file.seek(FADE_LENGTH_OFFSET);
78   QByteArray fade_bytes = file.read(FADE_LENGTH_SIZE);
79   if (fade_bytes.size() >= FADE_LENGTH_SIZE) {
80     quint64 fade_length_in_ms = ConvertSPCStringToNum(fade_bytes);
81     qLog(Debug) << fade_length_in_ms << "------ Fade Length";
82 
83     if (fade_length_in_ms > 0x7FFF) {
84       fade_length_in_ms = fade_bytes[0] | (fade_bytes[1] << 8) |
85                           (fade_bytes[2] << 16) | (fade_bytes[3] << 24);
86     }
87   }
88 
89   /*  Check for XID6 data -- this is infrequently used, but being able to fill
90    * in data from this is ideal before trying to rely on APETAG values. XID6
91    * format follows EA's binary file format standard named "IFF" */
92   file.seek(XID6_OFFSET);
93   if (has_id6 && file.read(4) == QString("xid6").toLatin1()) {
94     QByteArray xid6_head_data = file.read(4);
95     if (xid6_head_data.size() >= 4) {
96       qint64 xid6_size = xid6_head_data[0] | (xid6_head_data[1] << 8) |
97                          (xid6_head_data[2] << 16) | xid6_head_data[3];
98       /* This should be the size remaining for entire ID6 block, but it
99        * seems that most files treat this as the size of the remaining header
100        * space... */
101 
102       qLog(Debug) << file_info.fileName() << " has ID6 tag.";
103 
104       while ((file.pos()) + 4 < XID6_OFFSET + xid6_size) {
105         QByteArray arr = file.read(4);
106         if (arr.size() < 4) break;
107 
108         qint8 id = arr[0];
109         qint8 type = arr[1];
110         qint16 length = arr[2] | (arr[3] << 8);
111 
112         file.read(GetNextMemAddressAlign32bit(length));
113       }
114     }
115   }
116 
117   /* Music Players that support SPC tend to support additional tagging data as
118    * an APETAG entry at the bottom of the file instead of writing into the xid6
119    * tagging space. This is where a lot of the extra data for a file is stored,
120    * such as genre or replaygain data.
121    *
122    * This data is currently supported by TagLib, so we will simply use that for
123    * the remaining values. */
124   TagLib::APE::File ape(file_info.filePath().toStdString().data());
125   if (ape.hasAPETag()) {
126     TagLib::Tag* tag = ape.tag();
127     if (!tag) return;
128 
129     song_info->set_year(tag->year());
130     song_info->set_track(tag->track());
131     TagReader::Decode(tag->artist(), nullptr, song_info->mutable_artist());
132     TagReader::Decode(tag->title(), nullptr, song_info->mutable_title());
133     TagReader::Decode(tag->album(), nullptr, song_info->mutable_album());
134     TagReader::Decode(tag->genre(), nullptr, song_info->mutable_genre());
135   }
136 
137   song_info->set_valid(true);
138   song_info->set_type(pb::tagreader::SongMetadata_Type_SPC);
139 }
140 
GetNextMemAddressAlign32bit(qint16 input)141 qint16 GME::SPC::GetNextMemAddressAlign32bit(qint16 input) {
142   return ((input + 0x3) & ~0x3);
143   // Plus 0x3 for rounding up (not down), AND NOT to flatten out on a 32 bit
144   // level.
145 }
146 
ConvertSPCStringToNum(const QByteArray & arr)147 quint64 GME::SPC::ConvertSPCStringToNum(const QByteArray& arr) {
148   quint64 result = 0;
149   for (auto it = arr.begin(); it != arr.end(); it++) {
150     unsigned int num = *it - '0';
151     if (num > 9) break;
152     result = (result * 10) + num;  // Shift Left and add.
153   }
154   return result;
155 }
156 
Read(const QFileInfo & file_info,pb::tagreader::SongMetadata * song_info)157 void GME::VGM::Read(const QFileInfo& file_info,
158                     pb::tagreader::SongMetadata* song_info) {
159   QFile file(file_info.filePath());
160   if (!file.open(QIODevice::ReadOnly)) return;
161 
162   qLog(Debug) << "Reading tags from VGM file: " << file_info.fileName();
163 
164   if (!file.read(4).startsWith(QString("Vgm ").toLatin1())) return;
165 
166   file.seek(GD3_TAG_PTR);
167   QByteArray gd3_head = file.read(4);
168   if (gd3_head.size() < 4) return;
169 
170   quint64 pt = (unsigned char)gd3_head[0] | ((unsigned char)gd3_head[1] << 8) |
171                ((unsigned char)gd3_head[2] << 16) |
172                ((unsigned)gd3_head[3] << 24);
173 
174   file.seek(SAMPLE_COUNT);
175   QByteArray sample_count_bytes = file.read(4);
176   file.seek(LOOP_SAMPLE_COUNT);
177   QByteArray loop_count_bytes = file.read(4);
178   quint64 length = 0;
179 
180   if (!GetPlaybackLength(sample_count_bytes, loop_count_bytes, length)) return;
181 
182   file.seek(GD3_TAG_PTR + pt);
183   QByteArray gd3_version = file.read(4);
184 
185   file.seek(file.pos() + 4);
186   QByteArray gd3_length_bytes = file.read(4);
187   quint32 gd3_length =
188       (unsigned char)gd3_length_bytes[0] | ((unsigned char)gd3_head[1] << 8) |
189       ((unsigned char)gd3_head[2] << 16) | ((unsigned char)gd3_head[3] << 24);
190 
191   QByteArray gd3Data = file.read(gd3_length);
192   QTextStream fileTagStream(gd3Data, QIODevice::ReadOnly);
193   // Stored as 16 bit UTF string, two bytes per letter.
194   fileTagStream.setCodec("UTF-16");
195   QStringList strings = fileTagStream.readLine(0).split(QChar('\0'));
196   if (strings.count() < 10) return;
197 
198   /* VGM standard dictates string tag data exist in specific order.
199    * Order alternates between English and Japanese version of data.
200    * Read GD3 tag standard for more details. */
201   song_info->set_title(strings[0].toStdString());
202   song_info->set_album(strings[2].toStdString());
203   song_info->set_artist(strings[6].toStdString());
204   song_info->set_year(strings[8].left(4).toInt());
205   song_info->set_length_nanosec(length * kNsecPerMsec);
206   song_info->set_valid(true);
207   song_info->set_type(pb::tagreader::SongMetadata_Type_VGM);
208 }
209 
GetPlaybackLength(const QByteArray & sample_count_bytes,const QByteArray & loop_count_bytes,quint64 & out_length)210 bool GME::VGM::GetPlaybackLength(const QByteArray& sample_count_bytes,
211                                  const QByteArray& loop_count_bytes,
212                                  quint64& out_length) {
213   if (sample_count_bytes.size() != 4) return false;
214   if (loop_count_bytes.size() != 4) return false;
215 
216   quint64 sample_count = (unsigned char)sample_count_bytes[0] |
217                          ((unsigned char)sample_count_bytes[1] << 8) |
218                          ((unsigned char)sample_count_bytes[2] << 16) |
219                          ((unsigned char)sample_count_bytes[3] << 24);
220 
221   qLog(Debug) << QString::number(sample_count, 16);
222   qLog(Debug) << sample_count_bytes.toHex();
223 
224   if (sample_count <= 0) return false;
225 
226   quint64 loop_sample_count = (unsigned char)loop_count_bytes[0] |
227                               ((unsigned char)loop_count_bytes[1] << 8) |
228                               ((unsigned char)loop_count_bytes[2] << 16) |
229                               ((unsigned char)loop_count_bytes[3] << 24);
230 
231   if (loop_sample_count <= 0) {
232     out_length = sample_count * 1000 / SAMPLE_TIMEBASE;
233     return true;
234   }
235 
236   quint64 intro_length_ms =
237       (sample_count - loop_sample_count) * 1000 / SAMPLE_TIMEBASE;
238   quint64 loop_length_ms = (loop_sample_count)*1000 / SAMPLE_TIMEBASE;
239   out_length = intro_length_ms + (loop_length_ms * 2) + GST_GME_LOOP_TIME_MS;
240   return true;
241 }
242