1 ///////////////////////////////////////////////////////////////////////////////
2 // LameXP - Audio Encoder Front-End
3 // Copyright (C) 2004-2011 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"
26 #include <QApplication>
35 ////////////////////////////////////////////////////////////
37 ////////////////////////////////////////////////////////////
42 virtual const char* type(void) = 0;
43 virtual bool isValid(void) { return false; }
46 class CueSheetTrack : public CueSheetItem
49 CueSheetTrack(CueSheetFile *parent, int trackNo)
54 m_startIndex = std::numeric_limits<double>::quiet_NaN();
55 m_duration = std::numeric_limits<double>::infinity();
57 int trackNo(void) { return m_trackNo; }
58 double startIndex(void) { return m_startIndex; }
59 double duration(void) { return m_duration; }
60 QString title(void) { return m_title; }
61 QString performer(void) { return m_performer; }
62 CueSheetFile *parent(void) { return m_parent; }
63 void setStartIndex(double startIndex) { m_startIndex = startIndex; }
64 void setDuration(double duration) { m_duration = duration; }
65 void setTitle(const QString &title, bool update = false) { if(!update || (m_title.isEmpty() && !title.isEmpty())) m_title = title; }
66 void setPerformer(const QString &performer, bool update = false) { if(!update || (m_performer.isEmpty() && !performer.isEmpty())) m_performer = performer; }
67 virtual bool isValid(void) { return !(_isnan(m_startIndex) || (m_trackNo < 0)); }
68 virtual const char* type(void) { return "CueSheetTrack"; }
75 CueSheetFile *m_parent;
78 class CueSheetFile : public CueSheetItem
81 CueSheetFile(const QString &fileName) : m_fileName(fileName) {}
82 ~CueSheetFile(void) { while(!m_tracks.isEmpty()) delete m_tracks.takeLast(); }
83 QString fileName(void) { return m_fileName; }
84 void addTrack(CueSheetTrack *track) { m_tracks.append(track); }
85 void clearTracks(void) { while(!m_tracks.isEmpty()) delete m_tracks.takeLast(); }
86 CueSheetTrack *track(int index) { return m_tracks.at(index); }
87 int trackCount(void) { return m_tracks.count(); }
88 virtual bool isValid(void) { return m_tracks.count() > 0; }
89 virtual const char* type(void) { return "CueSheetFile"; }
91 const QString m_fileName;
92 QList<CueSheetTrack*> m_tracks;
95 ////////////////////////////////////////////////////////////
96 // Constructor & Destructor
97 ////////////////////////////////////////////////////////////
99 QMutex CueSheetModel::m_mutex(QMutex::Recursive);
101 CueSheetModel::CueSheetModel()
103 m_fileIcon(":/icons/music.png"),
104 m_trackIcon(":/icons/control_play_blue.png")
108 for(int i = 0; i < 5; i++)
110 CueSheetFile *currentFile = new CueSheetFile(QString().sprintf("File %02d.wav", i+1));
111 for(int j = 0; j < 8; j++)
113 CueSheetTrack *currentTrack = new CueSheetTrack(currentFile, trackNo++);
114 currentTrack->setTitle("ATWA (Air Trees Water Animals)");
115 currentTrack->setPerformer("System of a Down");
116 currentFile->addTrack(currentTrack);
118 m_files.append(currentFile);
122 CueSheetModel::~CueSheetModel(void)
124 while(!m_files.isEmpty()) delete m_files.takeLast();
127 ////////////////////////////////////////////////////////////
129 ////////////////////////////////////////////////////////////
131 QModelIndex CueSheetModel::index(int row, int column, const QModelIndex &parent) const
133 QMutexLocker lock(&m_mutex);
135 if(!parent.isValid())
137 return (row < m_files.count()) ? createIndex(row, column, m_files.at(row)) : QModelIndex();
140 CueSheetItem *parentItem = static_cast<CueSheetItem*>(parent.internalPointer());
141 if(CueSheetFile *filePtr = dynamic_cast<CueSheetFile*>(parentItem))
143 return (row < filePtr->trackCount()) ? createIndex(row, column, filePtr->track(row)) : QModelIndex();
146 return QModelIndex();
149 int CueSheetModel::columnCount(const QModelIndex &parent) const
151 QMutexLocker lock(&m_mutex);
155 int CueSheetModel::rowCount(const QModelIndex &parent) const
157 QMutexLocker lock(&m_mutex);
159 if(!parent.isValid())
161 return m_files.count();
164 CueSheetItem *parentItem = static_cast<CueSheetItem*>(parent.internalPointer());
165 if(CueSheetFile *filePtr = dynamic_cast<CueSheetFile*>(parentItem))
167 return filePtr->trackCount();
173 QModelIndex CueSheetModel::parent(const QModelIndex &child) const
175 QMutexLocker lock(&m_mutex);
179 CueSheetItem *childItem = static_cast<CueSheetItem*>(child.internalPointer());
180 if(CueSheetTrack *trackPtr = dynamic_cast<CueSheetTrack*>(childItem))
182 return createIndex(m_files.indexOf(trackPtr->parent()), 0, trackPtr->parent());
186 return QModelIndex();
189 QVariant CueSheetModel::headerData (int section, Qt::Orientation orientation, int role) const
191 QMutexLocker lock(&m_mutex);
193 if(role == Qt::DisplayRole)
201 return tr("File / Track");
207 return tr("Duration");
220 QVariant CueSheetModel::data(const QModelIndex &index, int role) const
222 QMutexLocker lock(&m_mutex);
224 if(role == Qt::DisplayRole)
226 CueSheetItem *item = reinterpret_cast<CueSheetItem*>(index.internalPointer());
228 if(CueSheetFile *filePtr = dynamic_cast<CueSheetFile*>(item))
230 switch(index.column())
233 return tr("File %1").arg(QString().sprintf("%02d", index.row() + 1)).append(" ");
236 return QFileInfo(filePtr->fileName()).fileName();
243 else if(CueSheetTrack *trackPtr = dynamic_cast<CueSheetTrack*>(item))
245 switch(index.column())
248 return tr("Track %1").arg(QString().sprintf("%02d", trackPtr->trackNo())).append(" ");
251 if(!trackPtr->title().isEmpty() && !trackPtr->performer().isEmpty())
253 return QString("%1 - %2").arg(trackPtr->performer(), trackPtr->title());
255 else if(!trackPtr->title().isEmpty())
257 return QString("%1 - %2").arg(tr("Unknown Artist"), trackPtr->title());
259 else if(!trackPtr->performer().isEmpty())
261 return QString("%1 - %2").arg(trackPtr->performer(), tr("Unknown Title"));
265 return QString("%1 - %2").arg(tr("Unknown Artist"), tr("Unknown Title"));
269 return indexToString(trackPtr->startIndex());
272 return indexToString(trackPtr->duration());
280 else if(role == Qt::ToolTipRole)
282 CueSheetItem *item = reinterpret_cast<CueSheetItem*>(index.internalPointer());
284 if(CueSheetFile *filePtr = dynamic_cast<CueSheetFile*>(item))
286 return QDir::toNativeSeparators(filePtr->fileName());
288 else if(CueSheetTrack *trackPtr = dynamic_cast<CueSheetTrack*>(item))
290 return QDir::toNativeSeparators(trackPtr->parent()->fileName());
293 else if(role == Qt::DecorationRole)
295 if(index.column() == 0)
297 CueSheetItem *item = reinterpret_cast<CueSheetItem*>(index.internalPointer());
299 if(dynamic_cast<CueSheetFile*>(item))
303 else if(dynamic_cast<CueSheetTrack*>(item))
309 else if(role == Qt::FontRole)
311 QFont font("Monospace");
312 font.setStyleHint(QFont::TypeWriter);
313 if((index.column() == 1))
315 CueSheetItem *item = reinterpret_cast<CueSheetItem*>(index.internalPointer());
316 font.setBold(dynamic_cast<CueSheetFile*>(item) != NULL);
320 else if(role == Qt::ForegroundRole)
322 if((index.column() == 1))
324 CueSheetItem *item = reinterpret_cast<CueSheetItem*>(index.internalPointer());
325 if(CueSheetFile *filePtr = dynamic_cast<CueSheetFile*>(item))
327 return (QFileInfo(filePtr->fileName()).size() > 4) ? QColor("mediumblue") : QColor("darkred");
330 else if((index.column() == 3))
332 CueSheetItem *item = reinterpret_cast<CueSheetItem*>(index.internalPointer());
333 if(CueSheetTrack *trackPtr = dynamic_cast<CueSheetTrack*>(item))
335 if(trackPtr->duration() == std::numeric_limits<double>::infinity())
337 return QColor("dimgrey");
346 void CueSheetModel::clearData(void)
348 QMutexLocker lock(&m_mutex);
351 while(!m_files.isEmpty()) delete m_files.takeLast();
355 ////////////////////////////////////////////////////////////
357 ////////////////////////////////////////////////////////////
359 int CueSheetModel::getFileCount(void)
361 QMutexLocker lock(&m_mutex);
362 return m_files.count();
365 QString CueSheetModel::getFileName(int fileIndex)
367 QMutexLocker lock(&m_mutex);
369 if(fileIndex < 0 || fileIndex >= m_files.count())
374 return m_files.at(fileIndex)->fileName();
377 int CueSheetModel::getTrackCount(int fileIndex)
379 QMutexLocker lock(&m_mutex);
381 if(fileIndex < 0 || fileIndex >= m_files.count())
386 return m_files.at(fileIndex)->trackCount();
389 int CueSheetModel::getTrackNo(int fileIndex, int trackIndex)
391 QMutexLocker lock(&m_mutex);
393 if(fileIndex >= 0 && fileIndex < m_files.count())
395 CueSheetFile *currentFile = m_files.at(fileIndex);
396 if(trackIndex >= 0 && trackIndex < currentFile->trackCount())
398 return currentFile->track(trackIndex)->trackNo();
405 void CueSheetModel::getTrackIndex(int fileIndex, int trackIndex, double *startIndex, double *duration)
407 QMutexLocker lock(&m_mutex);
409 *startIndex = std::numeric_limits<double>::quiet_NaN();
410 *duration = std::numeric_limits<double>::quiet_NaN();
412 if(fileIndex >= 0 && fileIndex < m_files.count())
414 CueSheetFile *currentFile = m_files.at(fileIndex);
415 if(trackIndex >= 0 && trackIndex < currentFile->trackCount())
417 CueSheetTrack *currentTrack = currentFile->track(trackIndex);
418 *startIndex = currentTrack->startIndex();
419 *duration = currentTrack->duration();
424 QString CueSheetModel::getTrackPerformer(int fileIndex, int trackIndex)
426 QMutexLocker lock(&m_mutex);
428 if(fileIndex >= 0 && fileIndex < m_files.count())
430 CueSheetFile *currentFile = m_files.at(fileIndex);
431 if(trackIndex >= 0 && trackIndex < currentFile->trackCount())
433 CueSheetTrack *currentTrack = currentFile->track(trackIndex);
434 return currentTrack->performer();
441 QString CueSheetModel::getTrackTitle(int fileIndex, int trackIndex)
443 QMutexLocker lock(&m_mutex);
445 if(fileIndex >= 0 && fileIndex < m_files.count())
447 CueSheetFile *currentFile = m_files.at(fileIndex);
448 if(trackIndex >= 0 && trackIndex < currentFile->trackCount())
450 CueSheetTrack *currentTrack = currentFile->track(trackIndex);
451 return currentTrack->title();
458 QString CueSheetModel::getAlbumPerformer(void)
460 QMutexLocker lock(&m_mutex);
461 return m_albumPerformer;
464 QString CueSheetModel::getAlbumTitle(void)
466 QMutexLocker lock(&m_mutex);
470 ////////////////////////////////////////////////////////////
472 ////////////////////////////////////////////////////////////
474 int CueSheetModel::loadCueSheet(const QString &cueFileName, QCoreApplication *application)
476 QMutexLocker lock(&m_mutex);
478 QFile cueFile(cueFileName);
479 if(!cueFile.open(QIODevice::ReadOnly))
481 return ErrorIOFailure;
487 int iResult = parseCueFile(cueFile, QDir(QFileInfo(cueFile).canonicalPath()), application);
493 int CueSheetModel::parseCueFile(QFile &cueFile, const QDir &baseDir, QCoreApplication *application)
496 qDebug("\n[Cue Sheet Import]");
498 //Reject very large files, as parsing might take until forever
499 if(cueFile.size() >= 10485760i64)
501 qWarning("File is very big. Probably not a Cue Sheet. Rejecting...");
505 //Check for UTF-8 BOM in order to guess encoding
506 QByteArray bomCheck = cueFile.peek(128);
507 bool bUTF8 = bomCheck.contains("\xef\xbb\xbf");
508 qDebug("Encoding is %s.", (bUTF8 ? "UTF-8" : "Local 8-Bit"));
511 QRegExp rxFile("^FILE\\s+\"([^\"]+)\"\\s+(\\w+)$", Qt::CaseInsensitive);
512 QRegExp rxTrack("^TRACK\\s+(\\d+)\\s(\\w+)$", Qt::CaseInsensitive);
513 QRegExp rxIndex("^INDEX\\s+(\\d+)\\s+([0-9:]+)$", Qt::CaseInsensitive);
514 QRegExp rxTitle("^TITLE\\s+\"([^\"]+)\"$", Qt::CaseInsensitive);
515 QRegExp rxPerformer("^PERFORMER\\s+\"([^\"]+)\"$", Qt::CaseInsensitive);
517 bool bPreamble = true;
518 bool bUnsupportedTrack = false;
520 CueSheetFile *currentFile = NULL;
521 CueSheetTrack *currentTrack = NULL;
523 m_albumTitle.clear();
524 m_albumPerformer.clear();
526 //Loop over the Cue Sheet until all lines were processed
527 for(int lines = 0; lines < INT_MAX; lines++)
531 application->processEvents();
532 if(lines < 128) Sleep(10);
535 QByteArray lineData = cueFile.readLine();
536 if(lineData.size() <= 0)
538 qDebug("End of Cue Sheet file.");
542 QString line = bUTF8 ? QString::fromUtf8(lineData.constData(), lineData.size()).trimmed() : QString::fromLocal8Bit(lineData.constData(), lineData.size()).trimmed();
545 if(rxFile.indexIn(line) >= 0)
547 qDebug("%03d File: <%s> <%s>", lines, rxFile.cap(1).toUtf8().constData(), rxFile.cap(2).toUtf8().constData());
552 if(currentTrack->isValid())
554 currentFile->addTrack(currentTrack);
559 LAMEXP_DELETE(currentTrack);
562 if(currentFile->isValid())
564 m_files.append(currentFile);
569 LAMEXP_DELETE(currentFile);
574 LAMEXP_DELETE(currentTrack);
576 if(!rxFile.cap(2).compare("WAVE", Qt::CaseInsensitive) || !rxFile.cap(2).compare("MP3", Qt::CaseInsensitive) || !rxFile.cap(2).compare("AIFF", Qt::CaseInsensitive))
578 currentFile = new CueSheetFile(baseDir.absoluteFilePath(rxFile.cap(1)));
579 qDebug("%03d File path: <%s>", lines, currentFile->fileName().toUtf8().constData());
583 bUnsupportedTrack = true;
584 qWarning("%03d Skipping unsupported file of type '%s'.", lines, rxFile.cap(2).toUtf8().constData());
593 if(rxTrack.indexIn(line) >= 0)
597 qDebug("%03d Track: <%s> <%s>", lines, rxTrack.cap(1).toUtf8().constData(), rxTrack.cap(2).toUtf8().constData());
600 if(currentTrack->isValid())
602 currentFile->addTrack(currentTrack);
607 LAMEXP_DELETE(currentTrack);
610 if(!rxTrack.cap(2).compare("AUDIO", Qt::CaseInsensitive))
612 currentTrack = new CueSheetTrack(currentFile, rxTrack.cap(1).toInt());
616 bUnsupportedTrack = true;
617 qWarning("%03d Skipping unsupported track of type '%s'.", lines, rxTrack.cap(2).toUtf8().constData());
623 LAMEXP_DELETE(currentTrack);
630 if(rxIndex.indexIn(line) >= 0)
632 if(currentFile && currentTrack)
634 qDebug("%03d Index: <%s> <%s>", lines, rxIndex.cap(1).toUtf8().constData(), rxIndex.cap(2).toUtf8().constData());
635 if(rxIndex.cap(1).toInt() == 1)
637 currentTrack->setStartIndex(parseTimeIndex(rxIndex.cap(2)));
644 if(rxTitle.indexIn(line) >= 0)
648 m_albumTitle = rxTitle.cap(1).simplified();
650 else if(currentFile && currentTrack)
652 qDebug("%03d Title: <%s>", lines, rxTitle.cap(1).toUtf8().constData());
653 currentTrack->setTitle(rxTitle.cap(1));
658 /* --- PERFORMER --- */
659 if(rxPerformer.indexIn(line) >= 0)
663 m_albumPerformer = rxPerformer.cap(1).simplified();
665 else if(currentFile && currentTrack)
667 qDebug("%03d Title: <%s>", lines, rxPerformer.cap(1).toUtf8().constData());
668 currentTrack->setPerformer(rxPerformer.cap(1));
674 //Append the very last track/file that is still pending
679 if(currentTrack->isValid())
681 currentFile->addTrack(currentTrack);
686 LAMEXP_DELETE(currentTrack);
689 if(currentFile->isValid())
691 m_files.append(currentFile);
696 LAMEXP_DELETE(currentFile);
700 //Finally calculate duration of each track
701 int nFiles = m_files.count();
702 for(int i = 0; i < nFiles; i++)
706 application->processEvents();
710 CueSheetFile *currentFile = m_files.at(i);
711 int nTracks = currentFile->trackCount();
714 for(int j = 1; j < nTracks; j++)
716 CueSheetTrack *currentTrack = currentFile->track(j);
717 CueSheetTrack *previousTrack = currentFile->track(j-1);
718 double duration = currentTrack->startIndex() - previousTrack->startIndex();
719 previousTrack->setDuration(max(0.0, duration));
724 //Sanity check of track numbers
727 int previousTrackNo = -1;
729 for(int i = 0; i < 100; i++)
733 for(int i = 0; i < nFiles; i++)
737 application->processEvents();
740 CueSheetFile *currentFile = m_files.at(i);
741 int nTracks = currentFile->trackCount();
744 for(int j = 0; j < nTracks; j++)
746 int currentTrackNo = currentFile->track(j)->trackNo();
747 if(currentTrackNo > 99)
749 qWarning("Track #%02d is invalid (maximum is 99), Cue Sheet is inconsistent!", currentTrackNo);
750 return ErrorInconsistent;
752 if(currentTrackNo <= previousTrackNo)
754 qWarning("Non-increasing track numbers, Cue Sheet is inconsistent!", currentTrackNo);
755 return ErrorInconsistent;
757 if(trackNo[currentTrackNo])
759 qWarning("Track #%02d exists multiple times, Cue Sheet is inconsistent!", currentTrackNo);
760 return ErrorInconsistent;
762 trackNo[currentTrackNo] = true;
763 previousTrackNo = currentTrackNo;
771 return bUnsupportedTrack ? ErrorUnsupported : ErrorBadFile;
775 double CueSheetModel::parseTimeIndex(const QString &index)
777 QRegExp rxTimeIndex("\\s*(\\d+)\\s*:\\s*(\\d+)\\s*:\\s*(\\d+)\\s*");
779 if(rxTimeIndex.indexIn(index) >= 0)
782 bool minOK, secOK, frmOK;
784 min = rxTimeIndex.cap(1).toInt(&minOK);
785 sec = rxTimeIndex.cap(2).toInt(&secOK);
786 frm = rxTimeIndex.cap(3).toInt(&frmOK);
788 if(minOK && secOK && frmOK)
790 return static_cast<double>(60 * min) + static_cast<double>(sec) + (static_cast<double>(frm) / 75.0);
794 qWarning(" Bad time index: '%s'", index.toUtf8().constData());
795 return std::numeric_limits<double>::quiet_NaN();
798 QString CueSheetModel::indexToString(const double index) const
800 if(!_finite(index) || (index < 0.0) || (index > 86400.0))
802 return QString("??:??.???");
805 QTime time = QTime().addMSecs(static_cast<int>(floor(0.5 + (index * 1000.0))));
807 if(time.minute() < 100)
809 return time.toString("mm:ss.zzz");
813 return QString("99:99.999");