1 ///////////////////////////////////////////////////////////////////////////////
2 // LameXP - Audio Encoder Front-End
3 // Copyright (C) 2004-2012 LoRd_MuldeR <MuldeR2@GMX.de>
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.
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License along
16 // with this program; if not, write to the Free Software Foundation, Inc.,
17 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 // http://www.gnu.org/licenses/gpl-2.0.txt
20 ///////////////////////////////////////////////////////////////////////////////
22 #include "Thread_FileAnalyzer_Task.h"
25 #include "LockedFile.h"
26 #include "Model_AudioFile.h"
27 #include "PlaylistImporter.h"
36 #include <QReadLocker>
37 #include <QWriteLocker>
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())
49 QMutex
AnalyzeTask::s_waitMutex
;
50 QWaitCondition
AnalyzeTask::s_waitCond
;
51 QSet
<unsigned int> AnalyzeTask::s_threadIdx_running
;
52 unsigned int AnalyzeTask::s_threadIdx_next
= 0;
53 QSemaphore
AnalyzeTask::s_semaphore(0);
55 /* more static vars */
56 QReadWriteLock
AnalyzeTask::s_lock
;
57 unsigned int AnalyzeTask::s_filesAccepted
= 0;
58 unsigned int AnalyzeTask::s_filesRejected
= 0;
59 unsigned int AnalyzeTask::s_filesDenied
= 0;
60 unsigned int AnalyzeTask::s_filesDummyCDDA
= 0;
61 unsigned int AnalyzeTask::s_filesCueSheet
= 0;
62 QStringList
AnalyzeTask::s_additionalFiles
;
63 QSet
<QString
> AnalyzeTask::s_recentlyAdded
;
66 const int WAITCOND_TIMEOUT
= 2500;
67 const int MAX_RETRIES
= 60000 / WAITCOND_TIMEOUT
;
68 const int MAX_QUEUE_SLOTS
= 32;
70 ////////////////////////////////////////////////////////////
72 ////////////////////////////////////////////////////////////
74 AnalyzeTask::AnalyzeTask(const QString
&inputFile
, const QString
&templateFile
, volatile bool *abortFlag
)
76 m_threadIdx(makeThreadIdx()),
77 m_inputFile(inputFile
),
78 m_templateFile(templateFile
),
79 m_mediaInfoBin(lamexp_lookup_tool("mediainfo.exe")),
80 m_avs2wavBin(lamexp_lookup_tool("avs2wav.exe")),
81 m_abortFlag(abortFlag
)
83 if(m_mediaInfoBin
.isEmpty() || m_avs2wavBin
.isEmpty())
85 qFatal("Invalid path to MediaInfo binary. Tool not initialized properly.");
89 AnalyzeTask::~AnalyzeTask(void)
91 s_semaphore
.release();
94 s_threadIdx_running
.remove(m_threadIdx
);
100 ////////////////////////////////////////////////////////////
102 ////////////////////////////////////////////////////////////
104 void AnalyzeTask::run()
112 qWarning("WARNING: Caught an in exception AnalyzeTask thread!");
116 s_threadIdx_running
.remove(m_threadIdx
);
117 s_waitMutex
.unlock();
119 s_waitCond
.wakeAll();
122 void AnalyzeTask::run_ex(void)
124 int fileType
= fileTypeNormal
;
125 QString currentFile
= QDir::fromNativeSeparators(m_inputFile
);
126 qDebug("Analyzing: %s", currentFile
.toUtf8().constData());
128 emit
fileSelected(QFileInfo(currentFile
).fileName());
129 emit
progressValChanged(m_threadIdx
+ 1);
131 AudioFileModel file
= analyzeFile(currentFile
, &fileType
);
135 qWarning("Operation cancelled by user!");
138 if(fileType
== fileTypeSkip
)
140 qWarning("File was recently added, skipping!");
143 if(fileType
== fileTypeDenied
)
145 QWriteLocker
lock(&s_lock
);
148 qWarning("Cannot access file for reading, skipping!");
151 if(fileType
== fileTypeCDDA
)
153 QWriteLocker
lock(&s_lock
);
156 qWarning("Dummy CDDA file detected, skipping!");
160 //Handle files with *incomplete* meida info
161 if(file
.fileName().isEmpty() || file
.formatContainerType().isEmpty() || file
.formatAudioType().isEmpty())
163 QStringList fileList
;
164 if(PlaylistImporter::importPlaylist(fileList
, currentFile
))
166 qDebug("Imported playlist file.");
167 QWriteLocker
lock(&s_lock
);
168 s_additionalFiles
<< fileList
;
170 else if(!QFileInfo(currentFile
).suffix().compare("cue", Qt::CaseInsensitive
))
172 QWriteLocker
lock(&s_lock
);
173 qWarning("Cue Sheet file detected, skipping!");
176 else if(!QFileInfo(currentFile
).suffix().compare("avs", Qt::CaseInsensitive
))
178 qDebug("Found a potential Avisynth script, investigating...");
179 if(analyzeAvisynthFile(currentFile
, file
))
181 QWriteLocker
lock(&s_lock
);
183 s_recentlyAdded
.insert(file
.filePath().toLower());
185 waitForPreviousThreads();
186 emit
fileAnalyzed(file
);
190 QWriteLocker
lock(&s_lock
);
191 qDebug("Rejected Avisynth file: %s", file
.filePath().toUtf8().constData());
197 QWriteLocker
lock(&s_lock
);
198 qDebug("Rejected file of unknown type: %s", file
.filePath().toUtf8().constData());
205 QWriteLocker
lock(&s_lock
);
207 s_recentlyAdded
.insert(file
.filePath().toLower());
209 waitForPreviousThreads();
210 emit
fileAnalyzed(file
);
213 ////////////////////////////////////////////////////////////
215 ////////////////////////////////////////////////////////////
217 const AudioFileModel
AnalyzeTask::analyzeFile(const QString
&filePath
, int *type
)
219 *type
= fileTypeNormal
;
220 AudioFileModel
audioFile(filePath
);
222 QReadLocker
readLock(&s_lock
);
223 if(s_recentlyAdded
.contains(filePath
.toLower()))
225 *type
= fileTypeSkip
;
230 QFile
readTest(filePath
);
231 if(!readTest
.open(QIODevice::ReadOnly
))
233 *type
= fileTypeDenied
;
236 if(checkFile_CDDA(readTest
))
238 *type
= fileTypeCDDA
;
243 bool skipNext
= false;
244 unsigned int id_val
[2] = {UINT_MAX
, UINT_MAX
};
245 cover_t coverType
= coverNone
;
246 QByteArray coverData
;
249 params
<< QString("--Inform=file://%1").arg(QDir::toNativeSeparators(m_templateFile
));
250 params
<< QDir::toNativeSeparators(filePath
);
253 process
.setProcessChannelMode(QProcess::MergedChannels
);
254 process
.setReadChannel(QProcess::StandardOutput
);
255 process
.start(m_mediaInfoBin
, params
);
257 if(!process
.waitForStarted())
259 qWarning("MediaInfo process failed to create!");
260 qWarning("Error message: \"%s\"\n", process
.errorString().toLatin1().constData());
262 process
.waitForFinished(-1);
266 while(process
.state() != QProcess::NotRunning
)
271 qWarning("Process was aborted on user request!");
275 if(!process
.waitForReadyRead())
277 if(process
.state() == QProcess::Running
)
279 qWarning("MediaInfo time out. Killing process and skipping file!");
281 process
.waitForFinished(-1);
288 while(process
.canReadLine())
290 QString line
= QString::fromUtf8(process
.readLine().constData()).simplified();
293 //qDebug("Line:%s", line.toUtf8().constData());
295 int index
= line
.indexOf('=');
298 QString key
= line
.left(index
).trimmed();
299 QString val
= line
.mid(index
+1).trimmed();
302 updateInfo(audioFile
, &skipNext
, id_val
, &coverType
, &coverData
, key
, val
);
309 if(audioFile
.fileName().isEmpty())
311 QString baseName
= QFileInfo(filePath
).fileName();
312 int index
= baseName
.lastIndexOf(".");
316 baseName
= baseName
.left(index
);
319 baseName
= baseName
.replace("_", " ").simplified();
320 index
= baseName
.lastIndexOf(" - ");
324 baseName
= baseName
.mid(index
+ 3).trimmed();
327 audioFile
.setFileName(baseName
);
330 process
.waitForFinished();
331 if(process
.state() != QProcess::NotRunning
)
334 process
.waitForFinished(-1);
337 if((coverType
!= coverNone
) && (!coverData
.isEmpty()))
339 retrieveCover(audioFile
, coverType
, coverData
);
342 if((audioFile
.formatAudioType().compare("PCM", Qt::CaseInsensitive
) == 0) && (audioFile
.formatAudioProfile().compare("Float", Qt::CaseInsensitive
) == 0))
344 if(audioFile
.formatAudioBitdepth() == 32) audioFile
.setFormatAudioBitdepth(AudioFileModel::BITDEPTH_IEEE_FLOAT32
);
350 void AnalyzeTask::updateInfo(AudioFileModel
&audioFile
, bool *skipNext
, unsigned int *id_val
, cover_t
*coverType
, QByteArray
*coverData
, const QString
&key
, const QString
&value
)
352 //qWarning("'%s' -> '%s'", key.toUtf8().constData(), value.toUtf8().constData());
355 if(IS_KEY("Gen_ID") || IS_KEY("Aud_ID"))
363 //We ignore all ID's, except for the lowest one!
365 unsigned int id
= value
.toUInt(&ok
);
368 if(IS_KEY("Gen_ID")) { id_val
[0] = qMin(id_val
[0], id
); *skipNext
= (id
> id_val
[0]); }
369 if(IS_KEY("Aud_ID")) { id_val
[1] = qMin(id_val
[1], id
); *skipNext
= (id
> id_val
[1]); }
378 qWarning("Skipping info for non-primary stream!");
384 if((*skipNext
) || value
.isEmpty())
390 if(IS_KEY("Aud_Source"))
393 audioFile
.setFormatContainerType(QString());
394 audioFile
.setFormatAudioType(QString());
395 qWarning("Skipping info for playlist file!");
402 if(IS_KEY("Gen_Format"))
404 audioFile
.setFormatContainerType(value
);
406 else if(IS_KEY("Gen_Format_Profile"))
408 audioFile
.setFormatContainerProfile(value
);
410 else if(IS_KEY("Gen_Title") || IS_KEY("Gen_Track"))
412 audioFile
.setFileName(value
);
414 else if(IS_KEY("Gen_Duration"))
416 unsigned int tmp
= parseDuration(value
);
417 if(tmp
> 0) audioFile
.setFileDuration(tmp
);
419 else if(IS_KEY("Gen_Artist") || IS_KEY("Gen_Performer"))
421 audioFile
.setFileArtist(value
);
423 else if(IS_KEY("Gen_Album"))
425 audioFile
.setFileAlbum(value
);
427 else if(IS_KEY("Gen_Genre"))
429 audioFile
.setFileGenre(value
);
431 else if(IS_KEY("Gen_Released_Date") || IS_KEY("Gen_Recorded_Date"))
433 unsigned int tmp
= parseYear(value
);
434 if(tmp
> 0) audioFile
.setFileYear(tmp
);
436 else if(IS_KEY("Gen_Comment"))
438 audioFile
.setFileComment(value
);
440 else if(IS_KEY("Gen_Track/Position"))
443 unsigned int tmp
= value
.toUInt(&ok
);
444 if(ok
) audioFile
.setFilePosition(tmp
);
446 else if(IS_KEY("Gen_Cover") || IS_KEY("Gen_Cover_Type"))
448 if(*coverType
== coverNone
)
450 *coverType
= coverJpeg
;
453 else if(IS_KEY("Gen_Cover_Mime"))
455 QString temp
= FIRST_TOK(value
);
456 if(!temp
.compare("image/jpeg", Qt::CaseInsensitive
)) *coverType
= coverJpeg
;
457 else if(!temp
.compare("image/png", Qt::CaseInsensitive
)) *coverType
= coverPng
;
458 else if(!temp
.compare("image/gif", Qt::CaseInsensitive
)) *coverType
= coverGif
;
460 else if(IS_KEY("Gen_Cover_Data"))
462 if(!coverData
->isEmpty()) coverData
->clear();
463 coverData
->append(QByteArray::fromBase64(FIRST_TOK(value
).toLatin1()));
467 qWarning("Unknown key '%s' with value '%s' found!", key
.toUtf8().constData(), value
.toUtf8().constData());
476 if(IS_KEY("Aud_Format"))
478 audioFile
.setFormatAudioType(value
);
480 else if(IS_KEY("Aud_Format_Profile"))
482 audioFile
.setFormatAudioProfile(value
);
484 else if(IS_KEY("Aud_Format_Version"))
486 audioFile
.setFormatAudioVersion(value
);
488 else if(IS_KEY("Aud_Channel(s)"))
491 unsigned int tmp
= value
.toUInt(&ok
);
492 if(ok
) audioFile
.setFormatAudioChannels(tmp
);
494 else if(IS_KEY("Aud_SamplingRate"))
497 unsigned int tmp
= value
.toUInt(&ok
);
498 if(ok
) audioFile
.setFormatAudioSamplerate(tmp
);
500 else if(IS_KEY("Aud_BitDepth"))
503 unsigned int tmp
= value
.toUInt(&ok
);
504 if(ok
) audioFile
.setFormatAudioBitdepth(tmp
);
506 else if(IS_KEY("Aud_Duration"))
508 unsigned int tmp
= parseDuration(value
);
509 if(tmp
> 0) audioFile
.setFileDuration(tmp
);
511 else if(IS_KEY("Aud_BitRate"))
514 unsigned int tmp
= value
.toUInt(&ok
);
515 if(ok
) audioFile
.setFormatAudioBitrate(tmp
/1000);
517 else if(IS_KEY("Aud_BitRate_Mode"))
519 if(!value
.compare("CBR", Qt::CaseInsensitive
)) audioFile
.setFormatAudioBitrateMode(AudioFileModel::BitrateModeConstant
);
520 if(!value
.compare("VBR", Qt::CaseInsensitive
)) audioFile
.setFormatAudioBitrateMode(AudioFileModel::BitrateModeVariable
);
522 else if(IS_KEY("Aud_Encoded_Library"))
524 audioFile
.setFormatAudioEncodeLib(value
);
528 qWarning("Unknown key '%s' with value '%s' found!", key
.toUtf8().constData(), value
.toUtf8().constData());
533 /*Section not recognized*/
534 qWarning("Unknown section: %s", key
.toUtf8().constData());
537 bool AnalyzeTask::checkFile_CDDA(QFile
&file
)
540 QByteArray data
= file
.read(128);
542 int i
= data
.indexOf("RIFF");
543 int j
= data
.indexOf("CDDA");
544 int k
= data
.indexOf("fmt ");
546 return ((i
>= 0) && (j
>= 0) && (k
>= 0) && (k
> j
) && (j
> i
));
549 void AnalyzeTask::retrieveCover(AudioFileModel
&audioFile
, cover_t coverType
, const QByteArray
&coverData
)
551 qDebug("Retrieving cover!");
557 extension
= QString::fromLatin1("png");
560 extension
= QString::fromLatin1("gif");
563 extension
= QString::fromLatin1("jpg");
567 if(!(QImage::fromData(coverData
, extension
.toUpper().toLatin1().constData()).isNull()))
569 QFile
coverFile(QString("%1/%2.%3").arg(lamexp_temp_folder2(), lamexp_rand_str(), extension
));
570 if(coverFile
.open(QIODevice::WriteOnly
))
572 coverFile
.write(coverData
);
574 audioFile
.setFileCover(coverFile
.fileName(), true);
579 qWarning("Image data seems to be invalid :-(");
583 bool AnalyzeTask::analyzeAvisynthFile(const QString
&filePath
, AudioFileModel
&info
)
586 process
.setProcessChannelMode(QProcess::MergedChannels
);
587 process
.setReadChannel(QProcess::StandardOutput
);
588 process
.start(m_avs2wavBin
, QStringList() << QDir::toNativeSeparators(filePath
) << "?");
590 if(!process
.waitForStarted())
592 qWarning("AVS2WAV process failed to create!");
593 qWarning("Error message: \"%s\"\n", process
.errorString().toLatin1().constData());
595 process
.waitForFinished(-1);
599 bool bInfoHeaderFound
= false;
601 while(process
.state() != QProcess::NotRunning
)
606 qWarning("Process was aborted on user request!");
610 if(!process
.waitForReadyRead())
612 if(process
.state() == QProcess::Running
)
614 qWarning("AVS2WAV time out. Killing process and skipping file!");
616 process
.waitForFinished(-1);
623 while(process
.canReadLine())
625 QString line
= QString::fromUtf8(process
.readLine().constData()).simplified();
628 int index
= line
.indexOf(':');
631 QString key
= line
.left(index
).trimmed();
632 QString val
= line
.mid(index
+1).trimmed();
634 if(bInfoHeaderFound
&& !key
.isEmpty() && !val
.isEmpty())
636 if(key
.compare("TotalSeconds", Qt::CaseInsensitive
) == 0)
639 unsigned int duration
= val
.toUInt(&ok
);
640 if(ok
) info
.setFileDuration(duration
);
642 if(key
.compare("SamplesPerSec", Qt::CaseInsensitive
) == 0)
645 unsigned int samplerate
= val
.toUInt(&ok
);
646 if(ok
) info
.setFormatAudioSamplerate (samplerate
);
648 if(key
.compare("Channels", Qt::CaseInsensitive
) == 0)
651 unsigned int channels
= val
.toUInt(&ok
);
652 if(ok
) info
.setFormatAudioChannels(channels
);
654 if(key
.compare("BitsPerSample", Qt::CaseInsensitive
) == 0)
657 unsigned int bitdepth
= val
.toUInt(&ok
);
658 if(ok
) info
.setFormatAudioBitdepth(bitdepth
);
664 if(line
.contains("[Audio Info]", Qt::CaseInsensitive
))
666 info
.setFormatAudioType("Avisynth");
667 info
.setFormatContainerType("Avisynth");
668 bInfoHeaderFound
= true;
675 process
.waitForFinished();
676 if(process
.state() != QProcess::NotRunning
)
679 process
.waitForFinished(-1);
683 switch(process
.exitCode())
686 qDebug("Avisynth script was analyzed successfully.");
690 qWarning("It appears that Avisynth is not installed on the system!");
694 qWarning("Failed to open the Avisynth script, bad AVS file?");
700 unsigned int AnalyzeTask::parseYear(const QString
&str
)
702 if(str
.startsWith("UTC", Qt::CaseInsensitive
))
704 QDate date
= QDate::fromString(str
.mid(3).trimmed().left(10), "yyyy-MM-dd");
717 int year
= str
.toInt(&ok
);
729 unsigned int AnalyzeTask::parseDuration(const QString
&str
)
732 unsigned int value
= str
.toUInt(&ok
);
733 return ok
? (value
/1000) : 0;
736 unsigned __int64
AnalyzeTask::makeThreadIdx(void)
739 unsigned int idx
= s_threadIdx_next
++;
740 s_threadIdx_running
.insert(idx
);
741 s_waitMutex
.unlock();
746 void AnalyzeTask::waitForPreviousThreads(void)
748 //This function will block until all threads with a *lower* index have terminated.
749 //Required to make sure that the files will be added in the "correct" order!
756 bool bWaitFlag
= false;
757 QSet
<unsigned int>::const_iterator i
;
759 for(i
= s_threadIdx_running
.begin(); i
!= s_threadIdx_running
.end(); ++i
)
761 if(*i
< m_threadIdx
) { bWaitFlag
= true; break; }
764 if((!bWaitFlag
) || *m_abortFlag
)
766 s_waitMutex
.unlock();
770 if(!s_waitCond
.wait(&s_waitMutex
, WAITCOND_TIMEOUT
))
772 if(++retryCount
> MAX_RETRIES
)
774 qWarning("AnalyzeTask::waitForPreviousThreads encountered timeout !!!");
775 s_threadIdx_running
.clear();
781 ////////////////////////////////////////////////////////////
783 ////////////////////////////////////////////////////////////
785 unsigned int AnalyzeTask::filesAccepted(void)
787 QReadLocker
lock(&s_lock
);
788 return s_filesAccepted
;
791 unsigned int AnalyzeTask::filesRejected(void)
793 QReadLocker
lock(&s_lock
);
794 return s_filesRejected
;
797 unsigned int AnalyzeTask::filesDenied(void)
799 QReadLocker
lock(&s_lock
);
800 return s_filesDenied
;
803 unsigned int AnalyzeTask::filesDummyCDDA(void)
805 QReadLocker
lock(&s_lock
);
806 return s_filesDummyCDDA
;
809 unsigned int AnalyzeTask::filesCueSheet(void)
811 QReadLocker
lock(&s_lock
);
812 return s_filesCueSheet
;
815 int AnalyzeTask::getAdditionalFiles(QStringList
&fileList
)
817 QReadLocker
readLock(&s_lock
);
818 int count
= s_additionalFiles
.count();
823 QWriteLocker
lock(&s_lock
);
824 count
= s_additionalFiles
.count();
825 fileList
<< s_additionalFiles
;
826 s_additionalFiles
.clear();
833 bool AnalyzeTask::waitForFreeSlot(volatile bool *abortFlag
)
837 for(int i
= 0; i
< MAX_RETRIES
; i
++)
839 ret
= s_semaphore
.tryAcquire(1, WAITCOND_TIMEOUT
);
840 if(ret
|| (*abortFlag
)) break;
846 void AnalyzeTask::reset(void)
848 QWriteLocker
lock(&s_lock
);
852 s_filesDummyCDDA
= 0;
854 s_additionalFiles
.clear();
855 s_recentlyAdded
.clear();
859 s_threadIdx_next
= 0;
860 s_threadIdx_running
.clear();
861 s_waitMutex
.unlock();
863 int freeSlots
= s_semaphore
.available();
864 if(freeSlots
< MAX_QUEUE_SLOTS
)
866 s_semaphore
.release(MAX_QUEUE_SLOTS
- freeSlots
);
870 ////////////////////////////////////////////////////////////
872 ////////////////////////////////////////////////////////////