1 ///////////////////////////////////////////////////////////////////////////////
2 // LameXP - Audio Encoder Front-End
3 // Copyright (C) 2004-2013 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 ///////////////////////////////////////////////////////////////////////////////
23 #include "Model_CueSheet.h"
24 #include "Model_AudioFile.h"
27 #include <QApplication>
33 #include <QTextStream>
34 #include <QMutexLocker>
39 #define UNQUOTE(STR) STR.split("\"", QString::SkipEmptyParts).first().trimmed()
41 ////////////////////////////////////////////////////////////
43 ////////////////////////////////////////////////////////////
48 virtual const char* type(void) = 0;
49 virtual bool isValid(void) { return false; }
52 class CueSheetTrack
: public CueSheetItem
55 CueSheetTrack(CueSheetFile
*parent
, int trackNo
)
59 m_startIndex
= std::numeric_limits
<double>::quiet_NaN();
60 m_duration
= std::numeric_limits
<double>::infinity();
61 m_metaInfo
.setPosition(trackNo
);
65 CueSheetFile
*parent(void) { return m_parent
; }
66 double startIndex(void) { return m_startIndex
; }
67 double duration(void) { return m_duration
; }
68 AudioFileModel_MetaInfo
&metaInfo(void) { return m_metaInfo
; }
71 void setStartIndex(double startIndex
) { m_startIndex
= startIndex
; }
72 void setDuration(double duration
) { m_duration
= duration
; }
75 virtual bool isValid(void) { return !(_isnan(m_startIndex
) || (m_metaInfo
.position() == 0)); }
76 virtual const char* type(void) { return "CueSheetTrack"; }
81 AudioFileModel_MetaInfo m_metaInfo
;
82 CueSheetFile
*const m_parent
;
85 class CueSheetFile
: public CueSheetItem
88 CueSheetFile(const QString
&fileName
) : m_fileName(fileName
) {}
89 ~CueSheetFile(void) { while(!m_tracks
.isEmpty()) delete m_tracks
.takeLast(); }
92 QString
fileName(void) { return m_fileName
; }
93 CueSheetTrack
*track(int index
) { return m_tracks
.at(index
); }
94 int trackCount(void) { return m_tracks
.count(); }
97 void addTrack(CueSheetTrack
*track
) { m_tracks
.append(track
); }
98 void clearTracks(void) { while(!m_tracks
.isEmpty()) delete m_tracks
.takeLast(); }
101 virtual bool isValid(void) { return m_tracks
.count() > 0; }
102 virtual const char* type(void) { return "CueSheetFile"; }
105 const QString m_fileName
;
106 QList
<CueSheetTrack
*> m_tracks
;
109 ////////////////////////////////////////////////////////////
110 // Constructor & Destructor
111 ////////////////////////////////////////////////////////////
113 QMutex
CueSheetModel::m_mutex(QMutex::Recursive
);
115 CueSheetModel::CueSheetModel()
117 m_fileIcon(":/icons/music.png"),
118 m_trackIcon(":/icons/control_play_blue.png")
123 CueSheetModel::~CueSheetModel(void)
125 while(!m_files
.isEmpty()) delete m_files
.takeLast();
128 ////////////////////////////////////////////////////////////
130 ////////////////////////////////////////////////////////////
132 QModelIndex
CueSheetModel::index(int row
, int column
, const QModelIndex
&parent
) const
134 QMutexLocker
lock(&m_mutex
);
136 if(!parent
.isValid())
138 return (row
< m_files
.count()) ? createIndex(row
, column
, m_files
.at(row
)) : QModelIndex();
141 CueSheetItem
*parentItem
= static_cast<CueSheetItem
*>(parent
.internalPointer());
142 if(CueSheetFile
*filePtr
= dynamic_cast<CueSheetFile
*>(parentItem
))
144 return (row
< filePtr
->trackCount()) ? createIndex(row
, column
, filePtr
->track(row
)) : QModelIndex();
147 return QModelIndex();
150 int CueSheetModel::columnCount(const QModelIndex
&parent
) const
152 QMutexLocker
lock(&m_mutex
);
156 int CueSheetModel::rowCount(const QModelIndex
&parent
) const
158 QMutexLocker
lock(&m_mutex
);
160 if(!parent
.isValid())
162 return m_files
.count();
165 CueSheetItem
*parentItem
= static_cast<CueSheetItem
*>(parent
.internalPointer());
166 if(CueSheetFile
*filePtr
= dynamic_cast<CueSheetFile
*>(parentItem
))
168 return filePtr
->trackCount();
174 QModelIndex
CueSheetModel::parent(const QModelIndex
&child
) const
176 QMutexLocker
lock(&m_mutex
);
180 CueSheetItem
*childItem
= static_cast<CueSheetItem
*>(child
.internalPointer());
181 if(CueSheetTrack
*trackPtr
= dynamic_cast<CueSheetTrack
*>(childItem
))
183 return createIndex(m_files
.indexOf(trackPtr
->parent()), 0, trackPtr
->parent());
187 return QModelIndex();
190 QVariant
CueSheetModel::headerData (int section
, Qt::Orientation orientation
, int role
) const
192 QMutexLocker
lock(&m_mutex
);
194 if(role
== Qt::DisplayRole
)
202 return tr("File / Track");
208 return tr("Duration");
221 QVariant
CueSheetModel::data(const QModelIndex
&index
, int role
) const
223 QMutexLocker
lock(&m_mutex
);
225 if(role
== Qt::DisplayRole
)
227 CueSheetItem
*item
= reinterpret_cast<CueSheetItem
*>(index
.internalPointer());
229 if(CueSheetFile
*filePtr
= dynamic_cast<CueSheetFile
*>(item
))
231 switch(index
.column())
234 return tr("File %1").arg(QString().sprintf("%02d", index
.row() + 1)).append(" ");
237 return QFileInfo(filePtr
->fileName()).fileName();
244 else if(CueSheetTrack
*trackPtr
= dynamic_cast<CueSheetTrack
*>(item
))
246 const AudioFileModel_MetaInfo
&trackInfo
= trackPtr
->metaInfo();
247 switch(index
.column())
250 return tr("Track %1").arg(QString().sprintf("%02d", trackInfo
.position())).append(" ");
253 if(!trackInfo
.title().isEmpty() && !trackInfo
.artist().isEmpty())
255 return QString("%1 - %2").arg(trackInfo
.artist(), trackInfo
.title());
257 else if(!trackInfo
.title().isEmpty())
259 return QString("%1 - %2").arg(tr("Unknown Artist"), trackInfo
.title());
261 else if(!trackInfo
.artist().isEmpty())
263 return QString("%1 - %2").arg(trackInfo
.artist(), tr("Unknown Title"));
267 return QString("%1 - %2").arg(tr("Unknown Artist"), tr("Unknown Title"));
271 return indexToString(trackPtr
->startIndex());
274 return indexToString(trackPtr
->duration());
282 else if(role
== Qt::ToolTipRole
)
284 CueSheetItem
*item
= reinterpret_cast<CueSheetItem
*>(index
.internalPointer());
286 if(CueSheetFile
*filePtr
= dynamic_cast<CueSheetFile
*>(item
))
288 return QDir::toNativeSeparators(filePtr
->fileName());
290 else if(CueSheetTrack
*trackPtr
= dynamic_cast<CueSheetTrack
*>(item
))
292 return QDir::toNativeSeparators(trackPtr
->parent()->fileName());
295 else if(role
== Qt::DecorationRole
)
297 if(index
.column() == 0)
299 CueSheetItem
*item
= reinterpret_cast<CueSheetItem
*>(index
.internalPointer());
301 if(dynamic_cast<CueSheetFile
*>(item
))
305 else if(dynamic_cast<CueSheetTrack
*>(item
))
311 else if(role
== Qt::FontRole
)
313 QFont
font("Monospace");
314 font
.setStyleHint(QFont::TypeWriter
);
315 if((index
.column() == 1))
317 CueSheetItem
*item
= reinterpret_cast<CueSheetItem
*>(index
.internalPointer());
318 font
.setBold(dynamic_cast<CueSheetFile
*>(item
) != NULL
);
322 else if(role
== Qt::ForegroundRole
)
324 if((index
.column() == 1))
326 CueSheetItem
*item
= reinterpret_cast<CueSheetItem
*>(index
.internalPointer());
327 if(CueSheetFile
*filePtr
= dynamic_cast<CueSheetFile
*>(item
))
329 return (QFileInfo(filePtr
->fileName()).size() > 4) ? QColor("mediumblue") : QColor("darkred");
332 else if((index
.column() == 3))
334 CueSheetItem
*item
= reinterpret_cast<CueSheetItem
*>(index
.internalPointer());
335 if(CueSheetTrack
*trackPtr
= dynamic_cast<CueSheetTrack
*>(item
))
337 if(trackPtr
->duration() == std::numeric_limits
<double>::infinity())
339 return QColor("dimgrey");
348 void CueSheetModel::clearData(void)
350 QMutexLocker
lock(&m_mutex
);
353 while(!m_files
.isEmpty()) delete m_files
.takeLast();
357 ////////////////////////////////////////////////////////////
359 ////////////////////////////////////////////////////////////
361 int CueSheetModel::getFileCount(void)
363 QMutexLocker
lock(&m_mutex
);
364 return m_files
.count();
367 QString
CueSheetModel::getFileName(int fileIndex
)
369 QMutexLocker
lock(&m_mutex
);
371 if(fileIndex
< 0 || fileIndex
>= m_files
.count())
376 return m_files
.at(fileIndex
)->fileName();
379 int CueSheetModel::getTrackCount(int fileIndex
)
381 QMutexLocker
lock(&m_mutex
);
383 if(fileIndex
< 0 || fileIndex
>= m_files
.count())
388 return m_files
.at(fileIndex
)->trackCount();
391 const AudioFileModel_MetaInfo
*CueSheetModel::getTrackInfo(int fileIndex
, int trackIndex
)
393 QMutexLocker
lock(&m_mutex
);
395 if(fileIndex
>= 0 && fileIndex
< m_files
.count())
397 CueSheetFile
*currentFile
= m_files
.at(fileIndex
);
398 if(trackIndex
>= 0 && trackIndex
< currentFile
->trackCount())
400 return ¤tFile
->track(trackIndex
)->metaInfo();
407 bool CueSheetModel::getTrackIndex(int fileIndex
, int trackIndex
, double *startIndex
, double *duration
)
409 QMutexLocker
lock(&m_mutex
);
411 *startIndex
= std::numeric_limits
<double>::quiet_NaN();
412 *duration
= std::numeric_limits
<double>::quiet_NaN();
414 if(fileIndex
>= 0 && fileIndex
< m_files
.count())
416 CueSheetFile
*currentFile
= m_files
.at(fileIndex
);
417 if(trackIndex
>= 0 && trackIndex
< currentFile
->trackCount())
419 CueSheetTrack
*currentTrack
= currentFile
->track(trackIndex
);
420 *startIndex
= currentTrack
->startIndex();
421 *duration
= currentTrack
->duration();
429 const AudioFileModel_MetaInfo
*CueSheetModel::getAlbumInfo(void)
431 QMutexLocker
lock(&m_mutex
);
435 ////////////////////////////////////////////////////////////
437 ////////////////////////////////////////////////////////////
439 int CueSheetModel::loadCueSheet(const QString
&cueFileName
, QCoreApplication
*application
, QTextCodec
*forceCodec
)
441 QMutexLocker
lock(&m_mutex
);
442 const QTextCodec
*codec
= (forceCodec
!= NULL
) ? forceCodec
: QTextCodec::codecForName("System");
444 QFile
cueFile(cueFileName
);
445 if(!cueFile
.open(QIODevice::ReadOnly
))
447 return ErrorIOFailure
;
453 int iResult
= parseCueFile(cueFile
, QDir(QFileInfo(cueFile
).canonicalPath()), application
, codec
);
459 int CueSheetModel::parseCueFile(QFile
&cueFile
, const QDir
&baseDir
, QCoreApplication
*application
, const QTextCodec
*codec
)
462 qDebug("\n[Cue Sheet Import]");
463 bool bForceLatin1
= false;
465 //Reject very large files, as parsing might take until forever
466 if(cueFile
.size() >= 10485760i64
)
468 qWarning("File is very big. Probably not a Cue Sheet. Rejecting...");
472 //Test selected Codepage for decoding errors
473 qDebug("Character encoding is: %s.", codec
->name().constData());
474 const QString replacementSymbol
= QString(QChar(QChar::ReplacementCharacter
));
475 QByteArray testData
= cueFile
.peek(1048576);
476 if((!testData
.isEmpty()) && codec
->toUnicode(testData
.constData(), testData
.size()).contains(replacementSymbol
))
478 qWarning("Decoding error using selected codepage (%s). Enforcing Latin-1.", codec
->name().constData());
484 QTextStream
cueStream(&cueFile
);
485 cueStream
.setAutoDetectUnicode(false);
486 cueStream
.setCodec(bForceLatin1
? "latin1" : codec
->name());
487 cueStream
.seek(0i64
);
489 //Create regular expressions
490 QRegExp
rxFile("^FILE\\s+(\"[^\"]+\"|\\S+)\\s+(\\w+)$", Qt::CaseInsensitive
);
491 QRegExp
rxTrack("^TRACK\\s+(\\d+)\\s(\\w+)$", Qt::CaseInsensitive
);
492 QRegExp
rxIndex("^INDEX\\s+(\\d+)\\s+([0-9:]+)$", Qt::CaseInsensitive
);
493 QRegExp
rxTitle("^TITLE\\s+(\"[^\"]+\"|\\S+)$", Qt::CaseInsensitive
);
494 QRegExp
rxPerformer("^PERFORMER\\s+(\"[^\"]+\"|\\S+)$", Qt::CaseInsensitive
);
495 QRegExp
rxGenre("^REM\\s+GENRE\\s+(\"[^\"]+\"|\\S+)$", Qt::CaseInsensitive
);
496 QRegExp
rxYear("^REM\\s+DATE\\s+(\\d+)$", Qt::CaseInsensitive
);
498 bool bPreamble
= true;
499 bool bUnsupportedTrack
= false;
501 CueSheetFile
*currentFile
= NULL
;
502 CueSheetTrack
*currentTrack
= NULL
;
506 //Loop over the Cue Sheet until all lines were processed
507 for(int lines
= 0; lines
< INT_MAX
; lines
++)
511 application
->processEvents();
512 if(lines
< 128) lamexp_sleep(10);
515 if(cueStream
.atEnd())
517 qDebug("End of Cue Sheet file.");
521 QString line
= cueStream
.readLine().trimmed();
524 if(rxFile
.indexIn(line
) >= 0)
526 qDebug("%03d File: <%s> <%s>", lines
, rxFile
.cap(1).toUtf8().constData(), rxFile
.cap(2).toUtf8().constData());
531 if(currentTrack
->isValid())
533 currentFile
->addTrack(currentTrack
);
538 LAMEXP_DELETE(currentTrack
);
541 if(currentFile
->isValid())
543 m_files
.append(currentFile
);
548 LAMEXP_DELETE(currentFile
);
553 LAMEXP_DELETE(currentTrack
);
555 if(!rxFile
.cap(2).compare("WAVE", Qt::CaseInsensitive
) || !rxFile
.cap(2).compare("MP3", Qt::CaseInsensitive
) || !rxFile
.cap(2).compare("AIFF", Qt::CaseInsensitive
))
557 currentFile
= new CueSheetFile(baseDir
.absoluteFilePath(UNQUOTE(rxFile
.cap(1))));
558 qDebug("%03d File path: <%s>", lines
, currentFile
->fileName().toUtf8().constData());
562 bUnsupportedTrack
= true;
563 qWarning("%03d Skipping unsupported file of type '%s'.", lines
, rxFile
.cap(2).toUtf8().constData());
572 if(rxTrack
.indexIn(line
) >= 0)
576 qDebug("%03d Track: <%s> <%s>", lines
, rxTrack
.cap(1).toUtf8().constData(), rxTrack
.cap(2).toUtf8().constData());
579 if(currentTrack
->isValid())
581 currentFile
->addTrack(currentTrack
);
586 LAMEXP_DELETE(currentTrack
);
589 if(!rxTrack
.cap(2).compare("AUDIO", Qt::CaseInsensitive
))
591 currentTrack
= new CueSheetTrack(currentFile
, rxTrack
.cap(1).toInt());
595 bUnsupportedTrack
= true;
596 qWarning("%03d Skipping unsupported track of type '%s'.", lines
, rxTrack
.cap(2).toUtf8().constData());
602 LAMEXP_DELETE(currentTrack
);
609 if(rxIndex
.indexIn(line
) >= 0)
611 if(currentFile
&& currentTrack
)
613 qDebug("%03d Index: <%s> <%s>", lines
, rxIndex
.cap(1).toUtf8().constData(), rxIndex
.cap(2).toUtf8().constData());
614 if(rxIndex
.cap(1).toInt() == 1)
616 currentTrack
->setStartIndex(parseTimeIndex(rxIndex
.cap(2)));
623 if(rxTitle
.indexIn(line
) >= 0)
627 m_albumInfo
.setAlbum(UNQUOTE(rxTitle
.cap(1)).simplified());
629 else if(currentFile
&& currentTrack
)
631 qDebug("%03d Title: <%s>", lines
, rxTitle
.cap(1).toUtf8().constData());
632 currentTrack
->metaInfo().setTitle(UNQUOTE(rxTitle
.cap(1)).simplified());
637 /* --- PERFORMER --- */
638 if(rxPerformer
.indexIn(line
) >= 0)
642 m_albumInfo
.setArtist(UNQUOTE(rxPerformer
.cap(1)).simplified());
644 else if(currentFile
&& currentTrack
)
646 qDebug("%03d Title: <%s>", lines
, rxPerformer
.cap(1).toUtf8().constData());
647 currentTrack
->metaInfo().setArtist(UNQUOTE(rxPerformer
.cap(1)).simplified());
653 if(rxGenre
.indexIn(line
) >= 0)
657 QString temp
= UNQUOTE(rxGenre
.cap(1)).simplified();
658 for(int i
= 0; g_lamexp_generes
[i
]; i
++)
660 if(temp
.compare(g_lamexp_generes
[i
], Qt::CaseInsensitive
) == 0)
662 m_albumInfo
.setGenre(QString(g_lamexp_generes
[i
]));
667 else if(currentFile
&& currentTrack
)
669 qDebug("%03d Genre: <%s>", lines
, rxGenre
.cap(1).toUtf8().constData());
670 QString temp
= UNQUOTE(rxGenre
.cap(1).simplified());
671 for(int i
= 0; g_lamexp_generes
[i
]; i
++)
673 if(temp
.compare(g_lamexp_generes
[i
], Qt::CaseInsensitive
) == 0)
675 currentTrack
->metaInfo().setGenre(QString(g_lamexp_generes
[i
]));
684 if(rxYear
.indexIn(line
) >= 0)
689 unsigned int temp
= rxYear
.cap(1).toUInt(&ok
);
690 if(ok
) m_albumInfo
.setYear(temp
);
692 else if(currentFile
&& currentTrack
)
694 qDebug("%03d Year: <%s>", lines
, rxPerformer
.cap(1).toUtf8().constData());
696 unsigned int temp
= rxYear
.cap(1).toUInt(&ok
);
697 if(ok
) currentTrack
->metaInfo().setYear(temp
);
703 //Append the very last track/file that is still pending
708 if(currentTrack
->isValid())
710 currentFile
->addTrack(currentTrack
);
715 LAMEXP_DELETE(currentTrack
);
718 if(currentFile
->isValid())
720 m_files
.append(currentFile
);
725 LAMEXP_DELETE(currentFile
);
729 //Finally calculate duration of each track
730 int nFiles
= m_files
.count();
731 for(int i
= 0; i
< nFiles
; i
++)
735 application
->processEvents();
739 CueSheetFile
*currentFile
= m_files
.at(i
);
740 int nTracks
= currentFile
->trackCount();
743 for(int j
= 1; j
< nTracks
; j
++)
745 CueSheetTrack
*currentTrack
= currentFile
->track(j
);
746 CueSheetTrack
*previousTrack
= currentFile
->track(j
-1);
747 double duration
= currentTrack
->startIndex() - previousTrack
->startIndex();
748 previousTrack
->setDuration(qMax(0.0, duration
));
753 //Sanity check of track numbers
756 bool hasTracks
= false;
757 int previousTrackNo
= -1;
759 for(int i
= 0; i
< 100; i
++)
764 for(int i
= 0; i
< nFiles
; i
++)
768 application
->processEvents();
771 CueSheetFile
*currentFile
= m_files
.at(i
);
772 int nTracks
= currentFile
->trackCount();
775 for(int j
= 0; j
< nTracks
; j
++)
777 int currentTrackNo
= currentFile
->track(j
)->metaInfo().position();
778 if(currentTrackNo
> 99)
780 qWarning("Track #%02d is invalid (maximum is 99), Cue Sheet is inconsistent!", currentTrackNo
);
781 return ErrorInconsistent
;
783 if(currentTrackNo
<= previousTrackNo
)
785 qWarning("Non-increasing track numbers (%02d -> %02d), Cue Sheet is inconsistent!", previousTrackNo
, currentTrackNo
);
786 return ErrorInconsistent
;
788 if(trackNo
[currentTrackNo
])
790 qWarning("Track #%02d exists multiple times, Cue Sheet is inconsistent!", currentTrackNo
);
791 return ErrorInconsistent
;
793 trackNo
[currentTrackNo
] = true;
794 previousTrackNo
= currentTrackNo
;
802 qWarning("Could not find at least one valid track in the Cue Sheet!");
803 return ErrorInconsistent
;
810 qWarning("Could not find at least one valid input file in the Cue Sheet!");
811 return bUnsupportedTrack
? ErrorUnsupported
: ErrorBadFile
;
815 double CueSheetModel::parseTimeIndex(const QString
&index
)
817 QRegExp
rxTimeIndex("\\s*(\\d+)\\s*:\\s*(\\d+)\\s*:\\s*(\\d+)\\s*");
819 if(rxTimeIndex
.indexIn(index
) >= 0)
822 bool minOK
, secOK
, frmOK
;
824 min
= rxTimeIndex
.cap(1).toInt(&minOK
);
825 sec
= rxTimeIndex
.cap(2).toInt(&secOK
);
826 frm
= rxTimeIndex
.cap(3).toInt(&frmOK
);
828 if(minOK
&& secOK
&& frmOK
)
830 return static_cast<double>(60 * min
) + static_cast<double>(sec
) + (static_cast<double>(frm
) / 75.0);
834 qWarning(" Bad time index: '%s'", index
.toUtf8().constData());
835 return std::numeric_limits
<double>::quiet_NaN();
838 QString
CueSheetModel::indexToString(const double index
) const
840 if(!_finite(index
) || (index
< 0.0) || (index
> 86400.0))
842 return QString("??:??.???");
845 QTime time
= QTime().addMSecs(static_cast<int>(floor(0.5 + (index
* 1000.0))));
847 if(time
.minute() < 100)
849 return time
.toString("mm:ss.zzz");
853 return QString("99:99.999");