1 /* BEGIN_COMMON_COPYRIGHT_HEADER
2 * (c)LGPL2+
3 *
4 * Flacon - audio File Encoder
5 * https://github.com/flacon/flacon
6 *
7 * Copyright: 2017
8 * Alexander Sokoloff <sokoloff.a@gmail.com>
9 *
10 * This library is free software; you can redistribute it and/or
11 * modify it under the terms of the GNU Lesser General Public
12 * License as published by the Free Software Foundation; either
13 * version 2.1 of the License, or (at your option) any later version.
14
15 * This library is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 * Lesser General Public License for more details.
19
20 * You should have received a copy of the GNU Lesser General Public
21 * License along with this library; if not, write to the Free Software
22 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
23 *
24 * END_COMMON_COPYRIGHT_HEADER */
25
26 #include "tools.h"
27
28 #include <QTest>
29 #include <QDir>
30 #include <QFile>
31 #include <QTextStream>
32 #include <QProcess>
33 #include <QCryptographicHash>
34 #include <QIODevice>
35 #include <QBuffer>
36 #include <QDebug>
37 #include "../settings.h"
38 #include "../cue.h"
39 #include "../disc.h"
40 #include "../converter/decoder.h"
41
42 class HashDevice : public QIODevice
43 {
44 public:
HashDevice(QCryptographicHash::Algorithm method,QObject * parent=nullptr)45 HashDevice(QCryptographicHash::Algorithm method, QObject *parent = nullptr) :
46 QIODevice(parent),
47 mHash(method),
48 mInHeader(true)
49 {
50 }
51
result() const52 QByteArray result() const { return mHash.result(); }
53
54 protected:
readData(char *,qint64)55 qint64 readData(char *, qint64) { return -1; }
56
writeData(const char * data,qint64 len)57 qint64 writeData(const char *data, qint64 len)
58 {
59 if (mInHeader) {
60 mBuf.append(data, len);
61 int n = mBuf.indexOf("data");
62 if (n > -1 && n < mBuf.length() - 8) {
63 mInHeader = false;
64 mHash.addData(mBuf.data() + n + 8, mBuf.length() - n - 8);
65 }
66 return len;
67 }
68
69 mHash.addData(data, len);
70 return len;
71 }
72
73 private:
74 QByteArray mBuf;
75 QCryptographicHash mHash;
76 bool mInHeader;
77 };
78
79 /************************************************
80 *
81 ************************************************/
calcAudioHash(const QString & fileName)82 QString calcAudioHash(const QString &fileName)
83 {
84 Conv::Decoder decoder;
85 try {
86 decoder.open(fileName);
87 }
88 catch (FlaconError &err) {
89 FAIL(QString("Can't open input file '%1': %2").arg(fileName, err.what()).toLocal8Bit());
90 return "";
91 }
92
93 if (!decoder.audioFormat()) {
94 FAIL("Unknown format");
95 decoder.close();
96 return "";
97 }
98
99 HashDevice hash(QCryptographicHash::Md5);
100 hash.open(QIODevice::WriteOnly);
101 decoder.extract(CueTime(), CueTime(), &hash);
102 decoder.close();
103
104 return hash.result().toHex();
105 }
106
107 /************************************************
108 *
109 ************************************************/
TestCueFile(const QString & fileName)110 TestCueFile::TestCueFile(const QString &fileName) :
111 mFileName(fileName)
112 {
113 }
114
115 /************************************************
116 *
117 ************************************************/
setWavFile(const QString & value)118 void TestCueFile::setWavFile(const QString &value)
119 {
120 mWavFile = value;
121 }
122
123 /************************************************
124 *
125 ************************************************/
addTrack(const QString & index0,const QString & index1)126 void TestCueFile::addTrack(const QString &index0, const QString &index1)
127 {
128 mTracks << TestCueTrack(index0, index1);
129 }
130
131 /************************************************
132 *
133 ************************************************/
addTrack(const QString & index1)134 void TestCueFile::addTrack(const QString &index1)
135 {
136 addTrack("", index1);
137 }
138
139 /************************************************
140 *
141 ************************************************/
write()142 void TestCueFile::write()
143 {
144 QFile f(mFileName);
145 if (!f.open(QFile::WriteOnly | QFile::Truncate))
146 QFAIL(QString("Can't create cue file '%1': %2").arg(mFileName).arg(f.errorString()).toLocal8Bit());
147
148 QTextStream cue(&f);
149
150 cue << QString("FILE \"%1\" WAVE\n").arg(mWavFile);
151 for (int i = 0; i < mTracks.count(); ++i) {
152 TestCueTrack track = mTracks.at(i);
153
154 cue << QString("\nTRACK %1 AUDIO\n").arg(i + 1);
155 if (track.index0 != "")
156 cue << QString(" INDEX 00 %1\n").arg(track.index0);
157
158 if (track.index1 != "")
159 cue << QString(" INDEX 01 %1\n").arg(track.index1);
160 }
161
162 f.close();
163 }
164
165 /************************************************
166 *
167 ************************************************/
compareAudioHash(const QString & file1,const QString & expected)168 bool compareAudioHash(const QString &file1, const QString &expected)
169 {
170 if (calcAudioHash(file1) != expected) {
171 FAIL(QString("Compared hases are not the same for:\n"
172 " [%1] %2\n"
173 " [%3] %4\n")
174
175 .arg(calcAudioHash(file1))
176 .arg(file1)
177
178 .arg(expected)
179 .arg("expected")
180
181 .toLocal8Bit());
182 return false;
183 }
184 return true;
185 }
186
187 /************************************************
188 *
189 ************************************************/
writeHexString(const QString & str,QIODevice * out)190 void writeHexString(const QString &str, QIODevice *out)
191 {
192 for (QString line : str.split('\n')) {
193 for (int i = 0; i < line.length() - 1;) {
194 // Skip comments
195 if (line.at(i) == '/')
196 break;
197
198 if (line.at(i).isSpace()) {
199 ++i;
200 continue;
201 }
202
203 union {
204 quint16 n16;
205 char b;
206 };
207
208 bool ok;
209 n16 = line.mid(i, 2).toShort(&ok, 16);
210 if (!ok)
211 throw QString("Incorrect HEX data at %1:\n%2").arg(i).arg(line);
212
213 out->write(&b, 1);
214 i += 2;
215 }
216 }
217 }
218
219 /************************************************
220 *
221 ************************************************/
writeHexString(const QString & str)222 QByteArray writeHexString(const QString &str)
223 {
224 QBuffer data;
225 data.open(QBuffer::ReadWrite);
226 writeHexString(str, &data);
227 return data.buffer();
228 }
229
230 /************************************************
231 *
232 ************************************************/
writeTestWavData(QIODevice * device,quint64 dataSize)233 static void writeTestWavData(QIODevice *device, quint64 dataSize)
234 {
235 static const int BUF_SIZE = 1024 * 1024;
236
237 quint32 x = 123456789, y = 362436069, z = 521288629;
238 union {
239 quint32 t;
240 char bytes[4];
241 };
242
243 QByteArray buf;
244
245 buf.reserve(4 * 1024 * 1024);
246 buf.reserve(BUF_SIZE);
247 for (uint i = 0; i < (dataSize / sizeof(quint32)); ++i) {
248 // xorshf96 ...................
249 x ^= x << 16;
250 x ^= x >> 5;
251 x ^= x << 1;
252
253 t = x;
254 x = y;
255 y = z;
256 z = t ^ x ^ y;
257 // xorshf96 ...................
258
259 buf.append(bytes, 4);
260 if (buf.size() >= BUF_SIZE) {
261 device->write(buf);
262 buf.resize(0);
263 }
264 }
265
266 device->write(buf);
267 }
268
269 /************************************************
270 *
271 ************************************************/
createWavFile(const QString & fileName,const QString & header,const int duration)272 void createWavFile(const QString &fileName, const QString &header, const int duration)
273 {
274 QBuffer wavHdr;
275 wavHdr.open(QBuffer::ReadWrite);
276 writeHexString(header, &wavHdr);
277
278 if (duration) {
279 quint32 bitsPerSample =
280 (quint8(wavHdr.buffer()[28]) << 0) + (quint8(wavHdr.buffer()[29]) << 8) + (quint8(wavHdr.buffer()[30]) << 16) + (quint8(wavHdr.buffer()[31]) << 24);
281
282 quint32 ckSize = bitsPerSample * duration + wavHdr.buffer().size() - 8 + 8;
283 wavHdr.buffer()[4] = quint8(ckSize >> 0);
284 wavHdr.buffer()[5] = quint8(ckSize >> 8);
285 wavHdr.buffer()[6] = quint8(ckSize >> 16);
286 wavHdr.buffer()[7] = quint8(ckSize >> 24);
287 }
288
289 // See http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
290 quint32 ckSize =
291 (quint8(wavHdr.buffer()[4]) << 0) + (quint8(wavHdr.buffer()[5]) << 8) + (quint8(wavHdr.buffer()[6]) << 16) + (quint8(wavHdr.buffer()[7]) << 24);
292
293 quint32 dataSize = ckSize - wavHdr.size();
294
295 QFile file(fileName);
296
297 if (file.exists() && file.size() == ckSize + 8)
298 return;
299
300 if (!file.open(QFile::WriteOnly | QFile::Truncate))
301 QFAIL(QString("Can't create file '%1': %2").arg(fileName, file.errorString()).toLocal8Bit());
302
303 file.write(wavHdr.buffer());
304 file.write("data", 4);
305 char buf[4];
306 buf[0] = quint8(dataSize);
307 buf[1] = quint8(dataSize >> 8);
308 buf[2] = quint8(dataSize >> 16);
309 buf[3] = quint8(dataSize >> 24);
310 file.write(buf, 4);
311
312 writeTestWavData(&file, dataSize);
313 file.close();
314 }
315
316 /************************************************
317 *
318 ************************************************/
319 class TestWavHeader : public Conv::WavHeader
320 {
321 public:
TestWavHeader(quint16 bitsPerSample,quint32 sampleRate,quint16 numChannels,uint durationSec)322 TestWavHeader(quint16 bitsPerSample, quint32 sampleRate, quint16 numChannels, uint durationSec)
323 {
324 mFormat = Conv::WavHeader::Format_PCM;
325 mNumChannels = numChannels;
326 mSampleRate = sampleRate;
327 mBitsPerSample = bitsPerSample;
328 mFmtSize = FmtChunkSize::FmtChunkMin;
329 mByteRate = mSampleRate * mBitsPerSample / 8 * mNumChannels;
330 mBlockAlign = mBitsPerSample * mNumChannels / 8;
331
332 mDataStartPos = 12 + 8 + mFmtSize;
333 mDataSize = durationSec * mByteRate;
334
335 mFileSize = mDataStartPos + mDataSize;
336
337 mExtSize = 0;
338 }
339 };
340
341 /************************************************
342 *
343 ************************************************/
createWavFile(const QString & fileName,quint16 bitsPerSample,quint32 sampleRate,uint durationSec)344 void createWavFile(const QString &fileName, quint16 bitsPerSample, quint32 sampleRate, uint durationSec)
345 {
346 TestWavHeader header(bitsPerSample, sampleRate, 2, durationSec);
347
348 QFile file(fileName);
349
350 if (file.exists() && quint64(file.size()) != header.fileSize() + 8)
351 return;
352
353 if (!file.open(QFile::WriteOnly | QFile::Truncate))
354 QFAIL(QString("Can't create file '%1': %2").arg(fileName, file.errorString()).toLocal8Bit());
355
356 file.write(header.toByteArray());
357 writeTestWavData(&file, header.dataSize());
358 file.close();
359 }
360
361 /************************************************
362 *
363 ************************************************/
encodeAudioFile(const QString & wavFileName,const QString & outFileName)364 void encodeAudioFile(const QString &wavFileName, const QString &outFileName)
365 {
366 if (QFileInfo(outFileName).exists() && QFileInfo(outFileName).size() > 1024)
367 return;
368
369 QString program;
370 QStringList args;
371
372 QString ext = QFileInfo(outFileName).suffix();
373
374 if (ext == "ape") {
375 program = "mac";
376 args << wavFileName;
377 args << outFileName;
378 args << "-c2000";
379 }
380
381 else if (ext == "flac") {
382 program = "flac";
383 args << "--silent";
384 args << "--force";
385 args << "-o" << outFileName;
386 args << wavFileName;
387 }
388
389 else if (ext == "wv") {
390 program = "wavpack";
391 args << wavFileName;
392 args << "-y";
393 args << "-q";
394 args << "-o" << outFileName;
395 }
396
397 else if (ext == "tta") {
398 program = "ttaenc";
399 args << "-o" << outFileName;
400 args << "-e";
401 args << wavFileName;
402 args << "/";
403 }
404
405 else {
406 QFAIL(QString("Can't create file '%1': unknown file format").arg(outFileName).toLocal8Bit());
407 }
408
409 bool ok = true;
410 QProcess proc;
411 proc.start(program, args);
412 ok = proc.waitForStarted(3 * 1000);
413 ok = ok && proc.waitForFinished(5 * 60 * 1000);
414 ok = ok && (proc.exitStatus() == 0);
415
416 if (!ok) {
417 QFAIL(QString("Can't encode %1 %2:")
418 .arg(program)
419 .arg(args.join(" "))
420 .toLocal8Bit()
421 + proc.readAllStandardError());
422 }
423
424 if (!QFileInfo(outFileName).isFile()) {
425 QFAIL(QString("Can't encode to file '%1' (file don't exists'):")
426 .arg(outFileName)
427 .toLocal8Bit()
428 + proc.readAllStandardError());
429 }
430 }
431
432 /************************************************
433 *
434 ************************************************/
testFail(const QString & message,const char * file,int line)435 void testFail(const QString &message, const char *file, int line)
436 {
437 QTest::qFail(message.toLocal8Bit().data(), file, line);
438 }
439
440 /************************************************
441 *
442 ************************************************/
loadFromCue(const QString & cueFile)443 Disc *loadFromCue(const QString &cueFile)
444 {
445 try {
446 Cue cue(cueFile);
447 Disc *res = new Disc(cue);
448 return res;
449 }
450 catch (FlaconError &err) {
451 FAIL(err.what());
452 }
453 return nullptr;
454 }
455
456 /************************************************
457 *
458 ************************************************/
apply(const QMap<QString,QVariant> & values)459 void TestSettings::apply(const QMap<QString, QVariant> &values)
460 {
461 for (auto i = values.constBegin(); i != values.constEnd(); ++i) {
462 setValue(i.key(), i.value());
463 }
464 sync();
465 }
466