1 ///////////////////////////////////////////////////////////////////////////////
2 // LameXP - Audio Encoder Front-End
3 // Copyright (C) 2004-2015 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, but always including the *additional*
9 // restrictions defined in the "License.txt" file.
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.
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.
20 // http://www.gnu.org/licenses/gpl-2.0.txt
21 ///////////////////////////////////////////////////////////////////////////////
25 #include "Model_CueSheet.h"
26 #include "Model_AudioFile.h"
30 #include <MUtils/Global.h>
31 #include <MUtils/OSSupport.h>
34 #include <QApplication>
40 #include <QTextStream>
41 #include <QMutexLocker>
47 #define UNQUOTE(STR) STR.split("\"", QString::SkipEmptyParts).first().trimmed()
49 ////////////////////////////////////////////////////////////
51 ////////////////////////////////////////////////////////////
56 virtual const char* type(void) = 0;
57 virtual bool isValid(void) { return false; }
60 class CueSheetTrack
: public CueSheetItem
63 CueSheetTrack(CueSheetFile
*parent
, int trackNo
)
67 m_startIndex
= std::numeric_limits
<double>::quiet_NaN();
68 m_duration
= std::numeric_limits
<double>::infinity();
69 m_metaInfo
.setPosition(trackNo
);
73 CueSheetFile
*parent(void) { return m_parent
; }
74 double startIndex(void) { return m_startIndex
; }
75 double duration(void) { return m_duration
; }
76 AudioFileModel_MetaInfo
&metaInfo(void) { return m_metaInfo
; }
79 void setStartIndex(double startIndex
) { m_startIndex
= startIndex
; }
80 void setDuration(double duration
) { m_duration
= duration
; }
83 virtual bool isValid(void) { return !(_isnan(m_startIndex
) || (m_metaInfo
.position() == 0)); }
84 virtual const char* type(void) { return "CueSheetTrack"; }
89 AudioFileModel_MetaInfo m_metaInfo
;
90 CueSheetFile
*const m_parent
;
93 class CueSheetFile
: public CueSheetItem
96 CueSheetFile(const QString
&fileName
) : m_fileName(fileName
) {}
97 ~CueSheetFile(void) { while(!m_tracks
.isEmpty()) delete m_tracks
.takeLast(); }
100 QString
fileName(void) { return m_fileName
; }
101 CueSheetTrack
*track(int index
) { return m_tracks
.at(index
); }
102 int trackCount(void) { return m_tracks
.count(); }
105 void addTrack(CueSheetTrack
*track
) { m_tracks
.append(track
); }
106 void clearTracks(void) { while(!m_tracks
.isEmpty()) delete m_tracks
.takeLast(); }
109 virtual bool isValid(void) { return m_tracks
.count() > 0; }
110 virtual const char* type(void) { return "CueSheetFile"; }
113 const QString m_fileName
;
114 QList
<CueSheetTrack
*> m_tracks
;
117 ////////////////////////////////////////////////////////////
118 // Constructor & Destructor
119 ////////////////////////////////////////////////////////////
121 QMutex
CueSheetModel::m_mutex(QMutex::Recursive
);
123 CueSheetModel::CueSheetModel()
125 m_fileIcon(":/icons/music.png"),
126 m_trackIcon(":/icons/control_play_blue.png")
131 CueSheetModel::~CueSheetModel(void)
133 while(!m_files
.isEmpty()) delete m_files
.takeLast();
136 ////////////////////////////////////////////////////////////
138 ////////////////////////////////////////////////////////////
140 QModelIndex
CueSheetModel::index(int row
, int column
, const QModelIndex
&parent
) const
142 QMutexLocker
lock(&m_mutex
);
144 if(!parent
.isValid())
146 return (row
< m_files
.count()) ? createIndex(row
, column
, m_files
.at(row
)) : QModelIndex();
149 CueSheetItem
*parentItem
= static_cast<CueSheetItem
*>(parent
.internalPointer());
150 if(CueSheetFile
*filePtr
= dynamic_cast<CueSheetFile
*>(parentItem
))
152 return (row
< filePtr
->trackCount()) ? createIndex(row
, column
, filePtr
->track(row
)) : QModelIndex();
155 return QModelIndex();
158 int CueSheetModel::columnCount(const QModelIndex
&parent
) const
160 QMutexLocker
lock(&m_mutex
);
164 int CueSheetModel::rowCount(const QModelIndex
&parent
) const
166 QMutexLocker
lock(&m_mutex
);
168 if(!parent
.isValid())
170 return m_files
.count();
173 CueSheetItem
*parentItem
= static_cast<CueSheetItem
*>(parent
.internalPointer());
174 if(CueSheetFile
*filePtr
= dynamic_cast<CueSheetFile
*>(parentItem
))
176 return filePtr
->trackCount();
182 QModelIndex
CueSheetModel::parent(const QModelIndex
&child
) const
184 QMutexLocker
lock(&m_mutex
);
188 CueSheetItem
*childItem
= static_cast<CueSheetItem
*>(child
.internalPointer());
189 if(CueSheetTrack
*trackPtr
= dynamic_cast<CueSheetTrack
*>(childItem
))
191 return createIndex(m_files
.indexOf(trackPtr
->parent()), 0, trackPtr
->parent());
195 return QModelIndex();
198 QVariant
CueSheetModel::headerData (int section
, Qt::Orientation orientation
, int role
) const
200 QMutexLocker
lock(&m_mutex
);
202 if(role
== Qt::DisplayRole
)
210 return tr("File / Track");
216 return tr("Duration");
229 QVariant
CueSheetModel::data(const QModelIndex
&index
, int role
) const
231 QMutexLocker
lock(&m_mutex
);
233 if(role
== Qt::DisplayRole
)
235 CueSheetItem
*item
= reinterpret_cast<CueSheetItem
*>(index
.internalPointer());
237 if(CueSheetFile
*filePtr
= dynamic_cast<CueSheetFile
*>(item
))
239 switch(index
.column())
242 return tr("File %1").arg(QString().sprintf("%02d", index
.row() + 1)).append(" ");
245 return QFileInfo(filePtr
->fileName()).fileName();
252 else if(CueSheetTrack
*trackPtr
= dynamic_cast<CueSheetTrack
*>(item
))
254 const AudioFileModel_MetaInfo
&trackInfo
= trackPtr
->metaInfo();
255 switch(index
.column())
258 return tr("Track %1").arg(QString().sprintf("%02d", trackInfo
.position())).append(" ");
261 if(!trackInfo
.title().isEmpty() && !trackInfo
.artist().isEmpty())
263 return QString("%1 - %2").arg(trackInfo
.artist(), trackInfo
.title());
265 else if(!trackInfo
.title().isEmpty())
267 return QString("%1 - %2").arg(tr("Unknown Artist"), trackInfo
.title());
269 else if(!trackInfo
.artist().isEmpty())
271 return QString("%1 - %2").arg(trackInfo
.artist(), tr("Unknown Title"));
275 return QString("%1 - %2").arg(tr("Unknown Artist"), tr("Unknown Title"));
279 return indexToString(trackPtr
->startIndex());
282 return indexToString(trackPtr
->duration());
290 else if(role
== Qt::ToolTipRole
)
292 CueSheetItem
*item
= reinterpret_cast<CueSheetItem
*>(index
.internalPointer());
294 if(CueSheetFile
*filePtr
= dynamic_cast<CueSheetFile
*>(item
))
296 return QDir::toNativeSeparators(filePtr
->fileName());
298 else if(CueSheetTrack
*trackPtr
= dynamic_cast<CueSheetTrack
*>(item
))
300 return QDir::toNativeSeparators(trackPtr
->parent()->fileName());
303 else if(role
== Qt::DecorationRole
)
305 if(index
.column() == 0)
307 CueSheetItem
*item
= reinterpret_cast<CueSheetItem
*>(index
.internalPointer());
309 if(dynamic_cast<CueSheetFile
*>(item
))
313 else if(dynamic_cast<CueSheetTrack
*>(item
))
319 else if(role
== Qt::FontRole
)
321 QFont
font("Monospace");
322 font
.setStyleHint(QFont::TypeWriter
);
323 if((index
.column() == 1))
325 CueSheetItem
*item
= reinterpret_cast<CueSheetItem
*>(index
.internalPointer());
326 font
.setBold(dynamic_cast<CueSheetFile
*>(item
) != NULL
);
330 else if(role
== Qt::ForegroundRole
)
332 if((index
.column() == 1))
334 CueSheetItem
*item
= reinterpret_cast<CueSheetItem
*>(index
.internalPointer());
335 if(CueSheetFile
*filePtr
= dynamic_cast<CueSheetFile
*>(item
))
337 return (QFileInfo(filePtr
->fileName()).size() > 4) ? QColor("mediumblue") : QColor("darkred");
340 else if((index
.column() == 3))
342 CueSheetItem
*item
= reinterpret_cast<CueSheetItem
*>(index
.internalPointer());
343 if(CueSheetTrack
*trackPtr
= dynamic_cast<CueSheetTrack
*>(item
))
345 if(trackPtr
->duration() == std::numeric_limits
<double>::infinity())
347 return QColor("dimgrey");
356 void CueSheetModel::clearData(void)
358 QMutexLocker
lock(&m_mutex
);
361 while(!m_files
.isEmpty()) delete m_files
.takeLast();
365 ////////////////////////////////////////////////////////////
367 ////////////////////////////////////////////////////////////
369 int CueSheetModel::getFileCount(void)
371 QMutexLocker
lock(&m_mutex
);
372 return m_files
.count();
375 QString
CueSheetModel::getFileName(int fileIndex
)
377 QMutexLocker
lock(&m_mutex
);
379 if(fileIndex
< 0 || fileIndex
>= m_files
.count())
384 return m_files
.at(fileIndex
)->fileName();
387 int CueSheetModel::getTrackCount(int fileIndex
)
389 QMutexLocker
lock(&m_mutex
);
391 if(fileIndex
< 0 || fileIndex
>= m_files
.count())
396 return m_files
.at(fileIndex
)->trackCount();
399 const AudioFileModel_MetaInfo
*CueSheetModel::getTrackInfo(int fileIndex
, int trackIndex
)
401 QMutexLocker
lock(&m_mutex
);
403 if(fileIndex
>= 0 && fileIndex
< m_files
.count())
405 CueSheetFile
*currentFile
= m_files
.at(fileIndex
);
406 if(trackIndex
>= 0 && trackIndex
< currentFile
->trackCount())
408 return ¤tFile
->track(trackIndex
)->metaInfo();
415 bool CueSheetModel::getTrackIndex(int fileIndex
, int trackIndex
, double *startIndex
, double *duration
)
417 QMutexLocker
lock(&m_mutex
);
419 *startIndex
= std::numeric_limits
<double>::quiet_NaN();
420 *duration
= std::numeric_limits
<double>::quiet_NaN();
422 if(fileIndex
>= 0 && fileIndex
< m_files
.count())
424 CueSheetFile
*currentFile
= m_files
.at(fileIndex
);
425 if(trackIndex
>= 0 && trackIndex
< currentFile
->trackCount())
427 CueSheetTrack
*currentTrack
= currentFile
->track(trackIndex
);
428 *startIndex
= currentTrack
->startIndex();
429 *duration
= currentTrack
->duration();
437 const AudioFileModel_MetaInfo
*CueSheetModel::getAlbumInfo(void)
439 QMutexLocker
lock(&m_mutex
);
443 ////////////////////////////////////////////////////////////
445 ////////////////////////////////////////////////////////////
447 int CueSheetModel::loadCueSheet(const QString
&cueFileName
, QCoreApplication
*application
, QTextCodec
*forceCodec
)
449 QMutexLocker
lock(&m_mutex
);
450 const QTextCodec
*codec
= (forceCodec
!= NULL
) ? forceCodec
: QTextCodec::codecForName("System");
452 QFile
cueFile(cueFileName
);
453 if(!cueFile
.open(QIODevice::ReadOnly
))
455 return ErrorIOFailure
;
461 int iResult
= parseCueFile(cueFile
, QDir(QFileInfo(cueFile
).canonicalPath()), application
, codec
);
467 int CueSheetModel::parseCueFile(QFile
&cueFile
, const QDir
&baseDir
, QCoreApplication
*application
, const QTextCodec
*codec
)
470 qDebug("\n[Cue Sheet Import]");
471 bool bForceLatin1
= false;
473 //Reject very large files, as parsing might take until forever
474 if(cueFile
.size() >= 10485760i64
)
476 qWarning("File is very big. Probably not a Cue Sheet. Rejecting...");
480 //Test selected Codepage for decoding errors
481 qDebug("Character encoding is: %s.", codec
->name().constData());
482 const QString replacementSymbol
= QString(QChar(QChar::ReplacementCharacter
));
483 QByteArray testData
= cueFile
.peek(1048576);
484 if((!testData
.isEmpty()) && codec
->toUnicode(testData
.constData(), testData
.size()).contains(replacementSymbol
))
486 qWarning("Decoding error using selected codepage (%s). Enforcing Latin-1.", codec
->name().constData());
492 QTextStream
cueStream(&cueFile
);
493 cueStream
.setAutoDetectUnicode(false);
494 cueStream
.setCodec(bForceLatin1
? "latin1" : codec
->name());
495 cueStream
.seek(0i64
);
497 //Create regular expressions
498 QRegExp
rxFile("^FILE\\s+(\"[^\"]+\"|\\S+)\\s+(\\w+)$", Qt::CaseInsensitive
);
499 QRegExp
rxTrack("^TRACK\\s+(\\d+)\\s(\\w+)$", Qt::CaseInsensitive
);
500 QRegExp
rxIndex("^INDEX\\s+(\\d+)\\s+([0-9:]+)$", Qt::CaseInsensitive
);
501 QRegExp
rxTitle("^TITLE\\s+(\"[^\"]+\"|\\S+)$", Qt::CaseInsensitive
);
502 QRegExp
rxPerformer("^PERFORMER\\s+(\"[^\"]+\"|\\S+)$", Qt::CaseInsensitive
);
503 QRegExp
rxGenre("^REM\\s+GENRE\\s+(\"[^\"]+\"|\\S+)$", Qt::CaseInsensitive
);
504 QRegExp
rxYear("^REM\\s+DATE\\s+(\\d+)$", Qt::CaseInsensitive
);
506 bool bPreamble
= true;
507 bool bUnsupportedTrack
= false;
509 CueSheetFile
*currentFile
= NULL
;
510 CueSheetTrack
*currentTrack
= NULL
;
514 //Loop over the Cue Sheet until all lines were processed
515 for(int lines
= 0; lines
< INT_MAX
; lines
++)
519 application
->processEvents();
520 if(lines
< 128) MUtils::OS::sleep_ms(10);
523 if(cueStream
.atEnd())
525 qDebug("End of Cue Sheet file.");
529 QString line
= cueStream
.readLine().trimmed();
532 if(rxFile
.indexIn(line
) >= 0)
534 qDebug("%03d File: <%s> <%s>", lines
, MUTILS_UTF8(rxFile
.cap(1)), MUTILS_UTF8(rxFile
.cap(2)));
539 if(currentTrack
->isValid())
541 currentFile
->addTrack(currentTrack
);
546 MUTILS_DELETE(currentTrack
);
549 if(currentFile
->isValid())
551 m_files
.append(currentFile
);
556 MUTILS_DELETE(currentFile
);
561 MUTILS_DELETE(currentTrack
);
563 if(!rxFile
.cap(2).compare("WAVE", Qt::CaseInsensitive
) || !rxFile
.cap(2).compare("MP3", Qt::CaseInsensitive
) || !rxFile
.cap(2).compare("AIFF", Qt::CaseInsensitive
))
565 currentFile
= new CueSheetFile(baseDir
.absoluteFilePath(UNQUOTE(rxFile
.cap(1))));
566 qDebug("%03d File path: <%s>", lines
, currentFile
->fileName().toUtf8().constData());
570 bUnsupportedTrack
= true;
571 qWarning("%03d Skipping unsupported file of type '%s'.", lines
, MUTILS_UTF8(rxFile
.cap(2)));
580 if(rxTrack
.indexIn(line
) >= 0)
584 qDebug("%03d Track: <%s> <%s>", lines
, MUTILS_UTF8(rxTrack
.cap(1)), MUTILS_UTF8(rxTrack
.cap(2)));
587 if(currentTrack
->isValid())
589 currentFile
->addTrack(currentTrack
);
594 MUTILS_DELETE(currentTrack
);
597 if(!rxTrack
.cap(2).compare("AUDIO", Qt::CaseInsensitive
))
599 currentTrack
= new CueSheetTrack(currentFile
, rxTrack
.cap(1).toInt());
603 bUnsupportedTrack
= true;
604 qWarning("%03d Skipping unsupported track of type '%s'.", lines
, MUTILS_UTF8(rxTrack
.cap(2)));
610 MUTILS_DELETE(currentTrack
);
617 if(rxIndex
.indexIn(line
) >= 0)
619 if(currentFile
&& currentTrack
)
621 qDebug("%03d Index: <%s> <%s>", lines
, MUTILS_UTF8(rxIndex
.cap(1)), MUTILS_UTF8(rxIndex
.cap(2)));
622 if(rxIndex
.cap(1).toInt() == 1)
624 currentTrack
->setStartIndex(parseTimeIndex(rxIndex
.cap(2)));
631 if(rxTitle
.indexIn(line
) >= 0)
635 m_albumInfo
.setAlbum(UNQUOTE(rxTitle
.cap(1)).simplified());
637 else if(currentFile
&& currentTrack
)
639 qDebug("%03d Title: <%s>", lines
, MUTILS_UTF8(rxTitle
.cap(1)));
640 currentTrack
->metaInfo().setTitle(UNQUOTE(rxTitle
.cap(1)).simplified());
645 /* --- PERFORMER --- */
646 if(rxPerformer
.indexIn(line
) >= 0)
650 m_albumInfo
.setArtist(UNQUOTE(rxPerformer
.cap(1)).simplified());
652 else if(currentFile
&& currentTrack
)
654 qDebug("%03d Title: <%s>", lines
, MUTILS_UTF8(rxPerformer
.cap(1)));
655 currentTrack
->metaInfo().setArtist(UNQUOTE(rxPerformer
.cap(1)).simplified());
661 if(rxGenre
.indexIn(line
) >= 0)
665 QString temp
= UNQUOTE(rxGenre
.cap(1)).simplified();
666 for(int i
= 0; g_lamexp_generes
[i
]; i
++)
668 if(temp
.compare(g_lamexp_generes
[i
], Qt::CaseInsensitive
) == 0)
670 m_albumInfo
.setGenre(QString(g_lamexp_generes
[i
]));
675 else if(currentFile
&& currentTrack
)
677 qDebug("%03d Genre: <%s>", lines
, MUTILS_UTF8(rxGenre
.cap(1)));
678 QString temp
= UNQUOTE(rxGenre
.cap(1).simplified());
679 for(int i
= 0; g_lamexp_generes
[i
]; i
++)
681 if(temp
.compare(g_lamexp_generes
[i
], Qt::CaseInsensitive
) == 0)
683 currentTrack
->metaInfo().setGenre(QString(g_lamexp_generes
[i
]));
692 if(rxYear
.indexIn(line
) >= 0)
697 unsigned int temp
= rxYear
.cap(1).toUInt(&ok
);
698 if(ok
) m_albumInfo
.setYear(temp
);
700 else if(currentFile
&& currentTrack
)
702 qDebug("%03d Year: <%s>", lines
, MUTILS_UTF8(rxPerformer
.cap(1)));
704 unsigned int temp
= rxYear
.cap(1).toUInt(&ok
);
705 if(ok
) currentTrack
->metaInfo().setYear(temp
);
711 //Append the very last track/file that is still pending
716 if(currentTrack
->isValid())
718 currentFile
->addTrack(currentTrack
);
723 MUTILS_DELETE(currentTrack
);
726 if(currentFile
->isValid())
728 m_files
.append(currentFile
);
733 MUTILS_DELETE(currentFile
);
737 //Finally calculate duration of each track
738 int nFiles
= m_files
.count();
739 for(int i
= 0; i
< nFiles
; i
++)
743 application
->processEvents();
744 MUtils::OS::sleep_ms(10);
747 CueSheetFile
*currentFile
= m_files
.at(i
);
748 int nTracks
= currentFile
->trackCount();
751 for(int j
= 1; j
< nTracks
; j
++)
753 CueSheetTrack
*currentTrack
= currentFile
->track(j
);
754 CueSheetTrack
*previousTrack
= currentFile
->track(j
-1);
755 double duration
= currentTrack
->startIndex() - previousTrack
->startIndex();
756 previousTrack
->setDuration(qMax(0.0, duration
));
761 //Sanity check of track numbers
764 bool hasTracks
= false;
765 int previousTrackNo
= -1;
767 for(int i
= 0; i
< 100; i
++)
772 for(int i
= 0; i
< nFiles
; i
++)
776 application
->processEvents();
777 MUtils::OS::sleep_ms(10);
779 CueSheetFile
*currentFile
= m_files
.at(i
);
780 int nTracks
= currentFile
->trackCount();
783 for(int j
= 0; j
< nTracks
; j
++)
785 int currentTrackNo
= currentFile
->track(j
)->metaInfo().position();
786 if(currentTrackNo
> 99)
788 qWarning("Track #%02d is invalid (maximum is 99), Cue Sheet is inconsistent!", currentTrackNo
);
789 return ErrorInconsistent
;
791 if(currentTrackNo
<= previousTrackNo
)
793 qWarning("Non-increasing track numbers (%02d -> %02d), Cue Sheet is inconsistent!", previousTrackNo
, currentTrackNo
);
794 return ErrorInconsistent
;
796 if(trackNo
[currentTrackNo
])
798 qWarning("Track #%02d exists multiple times, Cue Sheet is inconsistent!", currentTrackNo
);
799 return ErrorInconsistent
;
801 trackNo
[currentTrackNo
] = true;
802 previousTrackNo
= currentTrackNo
;
810 qWarning("Could not find at least one valid track in the Cue Sheet!");
811 return ErrorInconsistent
;
818 qWarning("Could not find at least one valid input file in the Cue Sheet!");
819 return bUnsupportedTrack
? ErrorUnsupported
: ErrorBadFile
;
823 double CueSheetModel::parseTimeIndex(const QString
&index
)
825 QRegExp
rxTimeIndex("\\s*(\\d+)\\s*:\\s*(\\d+)\\s*:\\s*(\\d+)\\s*");
827 if(rxTimeIndex
.indexIn(index
) >= 0)
830 bool minOK
, secOK
, frmOK
;
832 min
= rxTimeIndex
.cap(1).toInt(&minOK
);
833 sec
= rxTimeIndex
.cap(2).toInt(&secOK
);
834 frm
= rxTimeIndex
.cap(3).toInt(&frmOK
);
836 if(minOK
&& secOK
&& frmOK
)
838 return static_cast<double>(60 * min
) + static_cast<double>(sec
) + (static_cast<double>(frm
) / 75.0);
842 qWarning(" Bad time index: '%s'", MUTILS_UTF8(index
));
843 return std::numeric_limits
<double>::quiet_NaN();
846 QString
CueSheetModel::indexToString(const double index
) const
848 if(!_finite(index
) || (index
< 0.0) || (index
> 86400.0))
850 return QString("??:??.???");
853 QTime time
= QTime().addMSecs(static_cast<int>(floor(0.5 + (index
* 1000.0))));
855 if(time
.minute() < 100)
857 return time
.toString("mm:ss.zzz");
861 return QString("99:99.999");