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