OSDN Git Service

465284f0771879ac14194035bc6d31aacbeceeec
[lamexp/LameXP.git] / src / Thread_FileAnalyzer_Task.cpp
1 ///////////////////////////////////////////////////////////////////////////////
2 // LameXP - Audio Encoder Front-End
3 // Copyright (C) 2004-2014 LoRd_MuldeR <MuldeR2@GMX.de>
4 //
5 // This program is free software; you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation; either version 2 of the License, or
8 // (at your option) any later version, but always including the *additional*
9 // restrictions defined in the "License.txt" file.
10 //
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 // GNU General Public License for more details.
15 //
16 // You should have received a copy of the GNU General Public License along
17 // with this program; if not, write to the Free Software Foundation, Inc.,
18 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 //
20 // http://www.gnu.org/licenses/gpl-2.0.txt
21 ///////////////////////////////////////////////////////////////////////////////
22
23 #include "Thread_FileAnalyzer_Task.h"
24
25 #include "Global.h"
26 #include "LockedFile.h"
27 #include "Model_AudioFile.h"
28
29 #include <QDir>
30 #include <QFileInfo>
31 #include <QProcess>
32 #include <QDate>
33 #include <QTime>
34 #include <QDebug>
35 #include <QImage>
36 #include <QReadLocker>
37 #include <QWriteLocker>
38 #include <QThread>
39
40 #include <math.h>
41 #include <time.h>
42 #include <assert.h>
43
44 #define IS_KEY(KEY) (key.compare(KEY, Qt::CaseInsensitive) == 0)
45 #define IS_SEC(SEC) (key.startsWith((SEC "_"), Qt::CaseInsensitive))
46 #define FIRST_TOK(STR) (STR.split(" ", QString::SkipEmptyParts).first())
47
48 ////////////////////////////////////////////////////////////
49 // Constructor
50 ////////////////////////////////////////////////////////////
51
52 AnalyzeTask::AnalyzeTask(const int taskId, const QString &inputFile, const QString &templateFile, volatile bool *abortFlag)
53 :
54         m_taskId(taskId),
55         m_inputFile(inputFile),
56         m_templateFile(templateFile),
57         m_mediaInfoBin(lamexp_lookup_tool("mediainfo.exe")),
58         m_avs2wavBin(lamexp_lookup_tool("avs2wav.exe")),
59         m_abortFlag(abortFlag)
60 {
61         if(m_mediaInfoBin.isEmpty() || m_avs2wavBin.isEmpty())
62         {
63                 qFatal("Invalid path to MediaInfo binary. Tool not initialized properly.");
64         }
65 }
66
67 AnalyzeTask::~AnalyzeTask(void)
68 {
69         emit taskCompleted(m_taskId);
70 }
71
72 ////////////////////////////////////////////////////////////
73 // Thread Main
74 ////////////////////////////////////////////////////////////
75
76 void AnalyzeTask::run()
77 {
78         try
79         {
80                 run_ex();
81         }
82         catch(const std::exception &error)
83         {
84                 fflush(stdout); fflush(stderr);
85                 fprintf(stderr, "\nGURU MEDITATION !!!\n\nException error:\n%s\n", error.what());
86                 lamexp_fatal_exit(L"Unhandeled C++ exception error, application will exit!");
87         }
88         catch(...)
89         {
90                 fflush(stdout); fflush(stderr);
91                 fprintf(stderr, "\nGURU MEDITATION !!!\n\nUnknown exception error!\n");
92                 lamexp_fatal_exit(L"Unhandeled C++ exception error, application will exit!");
93         }
94 }
95
96 void AnalyzeTask::run_ex(void)
97 {
98         int fileType = fileTypeNormal;
99         QString currentFile = QDir::fromNativeSeparators(m_inputFile);
100         qDebug("Analyzing: %s", QUTF8(currentFile));
101         
102         AudioFileModel file = analyzeFile(currentFile, &fileType);
103
104         if(*m_abortFlag)
105         {
106                 qWarning("Operation cancelled by user!");
107                 return;
108         }
109
110         switch(fileType)
111         {
112         case fileTypeDenied:
113                 qWarning("Cannot access file for reading, skipping!");
114                 break;
115         case fileTypeCDDA:
116                 qWarning("Dummy CDDA file detected, skipping!");
117                 break;
118         default:
119                 if(file.metaInfo().title().isEmpty() || file.techInfo().containerType().isEmpty() || file.techInfo().audioType().isEmpty())
120                 {
121                         fileType = fileTypeUnknown;
122                         if(!QFileInfo(currentFile).suffix().compare("cue", Qt::CaseInsensitive))
123                         {
124                                 qWarning("Cue Sheet file detected, skipping!");
125                                 fileType = fileTypeCueSheet;
126                         }
127                         else if(!QFileInfo(currentFile).suffix().compare("avs", Qt::CaseInsensitive))
128                         {
129                                 qDebug("Found a potential Avisynth script, investigating...");
130                                 if(analyzeAvisynthFile(currentFile, file))
131                                 {
132                                         fileType = fileTypeNormal;
133                                 }
134                                 else
135                                 {
136                                         qDebug("Rejected Avisynth file: %s", QUTF8(file.filePath()));
137                                 }
138                         }
139                         else
140                         {
141                                 qDebug("Rejected file of unknown type: %s", QUTF8(file.filePath()));
142                         }
143                 }
144                 break;
145         }
146
147         //Emit the file now!
148         emit fileAnalyzed(m_taskId, fileType, file);
149 }
150
151 ////////////////////////////////////////////////////////////
152 // Privtae Functions
153 ////////////////////////////////////////////////////////////
154
155 const AudioFileModel AnalyzeTask::analyzeFile(const QString &filePath, int *type)
156 {
157         *type = fileTypeNormal;
158         AudioFileModel audioFile(filePath);
159
160         QFile readTest(filePath);
161         if(!readTest.open(QIODevice::ReadOnly))
162         {
163                 *type = fileTypeDenied;
164                 return audioFile;
165         }
166         if(checkFile_CDDA(readTest))
167         {
168                 *type = fileTypeCDDA;
169                 return audioFile;
170         }
171         readTest.close();
172
173         bool skipNext = false;
174         unsigned int id_val[2] = {UINT_MAX, UINT_MAX};
175         cover_t coverType = coverNone;
176         QByteArray coverData;
177
178         QStringList params;
179         params << QString("--Inform=file://%1").arg(QDir::toNativeSeparators(m_templateFile));
180         params << QDir::toNativeSeparators(filePath);
181         
182         QProcess process;
183         lamexp_init_process(process, QFileInfo(m_mediaInfoBin).absolutePath());
184
185         process.start(m_mediaInfoBin, params);
186                 
187         if(!process.waitForStarted())
188         {
189                 qWarning("MediaInfo process failed to create!");
190                 qWarning("Error message: \"%s\"\n", process.errorString().toLatin1().constData());
191                 process.kill();
192                 process.waitForFinished(-1);
193                 return audioFile;
194         }
195
196         while(process.state() != QProcess::NotRunning)
197         {
198                 if(*m_abortFlag)
199                 {
200                         process.kill();
201                         qWarning("Process was aborted on user request!");
202                         break;
203                 }
204                 
205                 if(!process.waitForReadyRead())
206                 {
207                         if(process.state() == QProcess::Running)
208                         {
209                                 qWarning("MediaInfo time out. Killing process and skipping file!");
210                                 process.kill();
211                                 process.waitForFinished(-1);
212                                 return audioFile;
213                         }
214                 }
215
216                 QByteArray data;
217
218                 while(process.canReadLine())
219                 {
220                         QString line = QString::fromUtf8(process.readLine().constData()).simplified();
221                         if(!line.isEmpty())
222                         {
223                                 //qDebug("Line:%s", QUTF8(line));
224                                 
225                                 int index = line.indexOf('=');
226                                 if(index > 0)
227                                 {
228                                         QString key = line.left(index).trimmed();
229                                         QString val = line.mid(index+1).trimmed();
230                                         if(!key.isEmpty())
231                                         {
232                                                 updateInfo(audioFile, &skipNext, id_val, &coverType, &coverData, key, val);
233                                         }
234                                 }
235                         }
236                 }
237         }
238
239         if(audioFile.metaInfo().title().isEmpty())
240         {
241                 QString baseName = QFileInfo(filePath).fileName();
242                 int index = baseName.lastIndexOf(".");
243
244                 if(index >= 0)
245                 {
246                         baseName = baseName.left(index);
247                 }
248
249                 baseName = baseName.replace("_", " ").simplified();
250                 index = baseName.lastIndexOf(" - ");
251
252                 if(index >= 0)
253                 {
254                         baseName = baseName.mid(index + 3).trimmed();
255                 }
256
257                 audioFile.metaInfo().setTitle(baseName);
258         }
259         
260         process.waitForFinished();
261         if(process.state() != QProcess::NotRunning)
262         {
263                 process.kill();
264                 process.waitForFinished(-1);
265         }
266
267         if((coverType != coverNone) && (!coverData.isEmpty()))
268         {
269                 retrieveCover(audioFile, coverType, coverData);
270         }
271
272         if((audioFile.techInfo().audioType().compare("PCM", Qt::CaseInsensitive) == 0) && (audioFile.techInfo().audioProfile().compare("Float", Qt::CaseInsensitive) == 0))
273         {
274                 if(audioFile.techInfo().audioBitdepth() == 32) audioFile.techInfo().setAudioBitdepth(AudioFileModel::BITDEPTH_IEEE_FLOAT32);
275         }
276
277         return audioFile;
278 }
279
280 void AnalyzeTask::updateInfo(AudioFileModel &audioFile, bool *skipNext, unsigned int *id_val, cover_t *coverType, QByteArray *coverData, const QString &key, const QString &value)
281 {
282         //qWarning("'%s' -> '%s'", QUTF8(key), QUTF8(value));
283         
284         /*New Stream*/
285         if(IS_KEY("Gen_ID") || IS_KEY("Aud_ID"))
286         {
287                 if(value.isEmpty())
288                 {
289                         *skipNext = false;
290                 }
291                 else
292                 {
293                         //We ignore all ID's, except for the lowest one!
294                         bool ok = false;
295                         unsigned int id = value.toUInt(&ok);
296                         if(ok)
297                         {
298                                 if(IS_KEY("Gen_ID")) { id_val[0] = qMin(id_val[0], id); *skipNext = (id > id_val[0]); }
299                                 if(IS_KEY("Aud_ID")) { id_val[1] = qMin(id_val[1], id); *skipNext = (id > id_val[1]); }
300                         }
301                         else
302                         {
303                                 *skipNext = true;
304                         }
305                 }
306                 if(*skipNext)
307                 {
308                         qWarning("Skipping info for non-primary stream!");
309                 }
310                 return;
311         }
312
313         /*Skip or empty?*/
314         if((*skipNext) || value.isEmpty())
315         {
316                 return;
317         }
318
319         /*Playlist file?*/
320         if(IS_KEY("Aud_Source"))
321         {
322                 *skipNext = true;
323                 audioFile.techInfo().setContainerType(QString());
324                 audioFile.techInfo().setAudioType(QString());
325                 qWarning("Skipping info for playlist file!");
326                 return;
327         }
328
329         /*General Section*/
330         if(IS_SEC("Gen"))
331         {
332                 if(IS_KEY("Gen_Format"))
333                 {
334                         audioFile.techInfo().setContainerType(value);
335                 }
336                 else if(IS_KEY("Gen_Format_Profile"))
337                 {
338                         audioFile.techInfo().setContainerProfile(value);
339                 }
340                 else if(IS_KEY("Gen_Title") || IS_KEY("Gen_Track"))
341                 {
342                         audioFile.metaInfo().setTitle(value);
343                 }
344                 else if(IS_KEY("Gen_Duration"))
345                 {
346                         unsigned int tmp = parseDuration(value);
347                         if(tmp > 0) audioFile.techInfo().setDuration(tmp);
348                 }
349                 else if(IS_KEY("Gen_Artist") || IS_KEY("Gen_Performer"))
350                 {
351                         audioFile.metaInfo().setArtist(value);
352                 }
353                 else if(IS_KEY("Gen_Album"))
354                 {
355                         audioFile.metaInfo().setAlbum(value);
356                 }
357                 else if(IS_KEY("Gen_Genre"))
358                 {
359                         audioFile.metaInfo().setGenre(value);
360                 }
361                 else if(IS_KEY("Gen_Released_Date") || IS_KEY("Gen_Recorded_Date"))
362                 {
363                         unsigned int tmp = parseYear(value);
364                         if(tmp > 0) audioFile.metaInfo().setYear(tmp);
365                 }
366                 else if(IS_KEY("Gen_Comment"))
367                 {
368                         audioFile.metaInfo().setComment(value);
369                 }
370                 else if(IS_KEY("Gen_Track/Position"))
371                 {
372                         bool ok = false;
373                         unsigned int tmp = value.toUInt(&ok);
374                         if(ok) audioFile.metaInfo().setPosition(tmp);
375                 }
376                 else if(IS_KEY("Gen_Cover") || IS_KEY("Gen_Cover_Type"))
377                 {
378                         if(*coverType == coverNone)
379                         {
380                                 *coverType = coverJpeg;
381                         }
382                 }
383                 else if(IS_KEY("Gen_Cover_Mime"))
384                 {
385                         QString temp = FIRST_TOK(value);
386                         if(!temp.compare("image/jpeg", Qt::CaseInsensitive)) *coverType = coverJpeg;
387                         else if(!temp.compare("image/png", Qt::CaseInsensitive)) *coverType = coverPng;
388                         else if(!temp.compare("image/gif", Qt::CaseInsensitive)) *coverType = coverGif;
389                 }
390                 else if(IS_KEY("Gen_Cover_Data"))
391                 {
392                         if(!coverData->isEmpty()) coverData->clear();
393                         coverData->append(QByteArray::fromBase64(FIRST_TOK(value).toLatin1()));
394                 }
395                 else
396                 {
397                         qWarning("Unknown key '%s' with value '%s' found!", QUTF8(key), QUTF8(value));
398                 }
399                 return;
400         }
401
402         /*Audio Section*/
403         if(IS_SEC("Aud"))
404         {
405
406                 if(IS_KEY("Aud_Format"))
407                 {
408                         audioFile.techInfo().setAudioType(value);
409                 }
410                 else if(IS_KEY("Aud_Format_Profile"))
411                 {
412                         audioFile.techInfo().setAudioProfile(value);
413                 }
414                 else if(IS_KEY("Aud_Format_Version"))
415                 {
416                         audioFile.techInfo().setAudioVersion(value);
417                 }
418                 else if(IS_KEY("Aud_Channel(s)"))
419                 {
420                         bool ok = false;
421                         unsigned int tmp = value.toUInt(&ok);
422                         if(ok) audioFile.techInfo().setAudioChannels(tmp);
423                 }
424                 else if(IS_KEY("Aud_SamplingRate"))
425                 {
426                         bool ok = false;
427                         unsigned int tmp = value.toUInt(&ok);
428                         if(ok) audioFile.techInfo().setAudioSamplerate(tmp);
429                 }
430                 else if(IS_KEY("Aud_BitDepth"))
431                 {
432                         bool ok = false;
433                         unsigned int tmp = value.toUInt(&ok);
434                         if(ok) audioFile.techInfo().setAudioBitdepth(tmp);
435                 }
436                 else if(IS_KEY("Aud_Duration"))
437                 {
438                         unsigned int tmp = parseDuration(value);
439                         if(tmp > 0) audioFile.techInfo().setDuration(tmp);
440                 }
441                 else if(IS_KEY("Aud_BitRate"))
442                 {
443                         bool ok = false;
444                         unsigned int tmp = value.toUInt(&ok);
445                         if(ok) audioFile.techInfo().setAudioBitrate(tmp/1000);
446                 }
447                 else if(IS_KEY("Aud_BitRate_Mode"))
448                 {
449                         if(!value.compare("CBR", Qt::CaseInsensitive)) audioFile.techInfo().setAudioBitrateMode(AudioFileModel::BitrateModeConstant);
450                         if(!value.compare("VBR", Qt::CaseInsensitive)) audioFile.techInfo().setAudioBitrateMode(AudioFileModel::BitrateModeVariable);
451                 }
452                 else if(IS_KEY("Aud_Encoded_Library"))
453                 {
454                         audioFile.techInfo().setAudioEncodeLib(value);
455                 }
456                 else
457                 {
458                         qWarning("Unknown key '%s' with value '%s' found!", QUTF8(key), QUTF8(value));
459                 }
460                 return;
461         }
462
463         /*Section not recognized*/
464         qWarning("Unknown section: %s", QUTF8(key));
465 }
466
467 bool AnalyzeTask::checkFile_CDDA(QFile &file)
468 {
469         file.reset();
470         QByteArray data = file.read(128);
471         
472         int i = data.indexOf("RIFF");
473         int j = data.indexOf("CDDA");
474         int k = data.indexOf("fmt ");
475
476         return ((i >= 0) && (j >= 0) && (k >= 0) && (k > j) && (j > i));
477 }
478
479 void AnalyzeTask::retrieveCover(AudioFileModel &audioFile, cover_t coverType, const QByteArray &coverData)
480 {
481         qDebug("Retrieving cover!");
482         QString extension;
483
484         switch(coverType)
485         {
486         case coverPng:
487                 extension = QString::fromLatin1("png");
488                 break;
489         case coverGif:
490                 extension = QString::fromLatin1("gif");
491                 break;
492         default:
493                 extension = QString::fromLatin1("jpg");
494                 break;
495         }
496         
497         if(!(QImage::fromData(coverData, extension.toUpper().toLatin1().constData()).isNull()))
498         {
499                 QFile coverFile(QString("%1/%2.%3").arg(lamexp_temp_folder2(), lamexp_rand_str(), extension));
500                 if(coverFile.open(QIODevice::WriteOnly))
501                 {
502                         coverFile.write(coverData);
503                         coverFile.close();
504                         audioFile.metaInfo().setCover(coverFile.fileName(), true);
505                 }
506         }
507         else
508         {
509                 qWarning("Image data seems to be invalid :-(");
510         }
511 }
512
513 bool AnalyzeTask::analyzeAvisynthFile(const QString &filePath, AudioFileModel &info)
514 {
515         QProcess process;
516         lamexp_init_process(process, QFileInfo(m_avs2wavBin).absolutePath());
517
518         process.start(m_avs2wavBin, QStringList() << QDir::toNativeSeparators(filePath) << "?");
519
520         if(!process.waitForStarted())
521         {
522                 qWarning("AVS2WAV process failed to create!");
523                 qWarning("Error message: \"%s\"\n", process.errorString().toLatin1().constData());
524                 process.kill();
525                 process.waitForFinished(-1);
526                 return false;
527         }
528
529         bool bInfoHeaderFound = false;
530
531         while(process.state() != QProcess::NotRunning)
532         {
533                 if(*m_abortFlag)
534                 {
535                         process.kill();
536                         qWarning("Process was aborted on user request!");
537                         break;
538                 }
539                 
540                 if(!process.waitForReadyRead())
541                 {
542                         if(process.state() == QProcess::Running)
543                         {
544                                 qWarning("AVS2WAV time out. Killing process and skipping file!");
545                                 process.kill();
546                                 process.waitForFinished(-1);
547                                 return false;
548                         }
549                 }
550
551                 QByteArray data;
552
553                 while(process.canReadLine())
554                 {
555                         QString line = QString::fromUtf8(process.readLine().constData()).simplified();
556                         if(!line.isEmpty())
557                         {
558                                 int index = line.indexOf(':');
559                                 if(index > 0)
560                                 {
561                                         QString key = line.left(index).trimmed();
562                                         QString val = line.mid(index+1).trimmed();
563
564                                         if(bInfoHeaderFound && !key.isEmpty() && !val.isEmpty())
565                                         {
566                                                 if(key.compare("TotalSeconds", Qt::CaseInsensitive) == 0)
567                                                 {
568                                                         bool ok = false;
569                                                         unsigned int duration = val.toUInt(&ok);
570                                                         if(ok) info.techInfo().setDuration(duration);
571                                                 }
572                                                 if(key.compare("SamplesPerSec", Qt::CaseInsensitive) == 0)
573                                                 {
574                                                         bool ok = false;
575                                                         unsigned int samplerate = val.toUInt(&ok);
576                                                         if(ok) info.techInfo().setAudioSamplerate (samplerate);
577                                                 }
578                                                 if(key.compare("Channels", Qt::CaseInsensitive) == 0)
579                                                 {
580                                                         bool ok = false;
581                                                         unsigned int channels = val.toUInt(&ok);
582                                                         if(ok) info.techInfo().setAudioChannels(channels);
583                                                 }
584                                                 if(key.compare("BitsPerSample", Qt::CaseInsensitive) == 0)
585                                                 {
586                                                         bool ok = false;
587                                                         unsigned int bitdepth = val.toUInt(&ok);
588                                                         if(ok) info.techInfo().setAudioBitdepth(bitdepth);
589                                                 }
590                                         }
591                                 }
592                                 else
593                                 {
594                                         if(line.contains("[Audio Info]", Qt::CaseInsensitive))
595                                         {
596                                                 info.techInfo().setAudioType("Avisynth");
597                                                 info.techInfo().setContainerType("Avisynth");
598                                                 bInfoHeaderFound = true;
599                                         }
600                                 }
601                         }
602                 }
603         }
604         
605         process.waitForFinished();
606         if(process.state() != QProcess::NotRunning)
607         {
608                 process.kill();
609                 process.waitForFinished(-1);
610         }
611
612         //Check exit code
613         switch(process.exitCode())
614         {
615         case 0:
616                 qDebug("Avisynth script was analyzed successfully.");
617                 return true;
618                 break;
619         case -5:
620                 qWarning("It appears that Avisynth is not installed on the system!");
621                 return false;
622                 break;
623         default:
624                 qWarning("Failed to open the Avisynth script, bad AVS file?");
625                 return false;
626                 break;
627         }
628 }
629
630 unsigned int AnalyzeTask::parseYear(const QString &str)
631 {
632         if(str.startsWith("UTC", Qt::CaseInsensitive))
633         {
634                 QDate date = QDate::fromString(str.mid(3).trimmed().left(10), "yyyy-MM-dd");
635                 if(date.isValid())
636                 {
637                         return date.year();
638                 }
639                 else
640                 {
641                         return 0;
642                 }
643         }
644         else
645         {
646                 bool ok = false;
647                 int year = str.toInt(&ok);
648                 if(ok && year > 0)
649                 {
650                         return year;
651                 }
652                 else
653                 {
654                         return 0;
655                 }
656         }
657 }
658
659 unsigned int AnalyzeTask::parseDuration(const QString &str)
660 {
661         bool ok = false;
662         unsigned int value = str.toUInt(&ok);
663         return ok ? (value/1000) : 0;
664 }
665
666
667 ////////////////////////////////////////////////////////////
668 // Public Functions
669 ////////////////////////////////////////////////////////////
670
671 /*NONE*/
672
673 ////////////////////////////////////////////////////////////
674 // EVENTS
675 ////////////////////////////////////////////////////////////
676
677 /*NONE*/