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>
36 #define UNQUOTE(STR) STR.split("\"", QString::SkipEmptyParts).first().trimmed()
38 ////////////////////////////////////////////////////////////
40 ////////////////////////////////////////////////////////////
45 virtual const char* type(void) = 0;
46 virtual bool isValid(void) { return false; }
49 class CueSheetTrack : public CueSheetItem
52 CueSheetTrack(CueSheetFile *parent, int trackNo)
57 m_startIndex = std::numeric_limits<double>::quiet_NaN();
58 m_duration = std::numeric_limits<double>::infinity();
61 int trackNo(void) { return m_trackNo; }
62 double startIndex(void) { return m_startIndex; }
63 double duration(void) { return m_duration; }
64 QString title(void) { return m_title; }
65 QString performer(void) { return m_performer; }
66 QString genre(void) { return m_genre; }
67 unsigned int year(void) { return m_year; }
68 CueSheetFile *parent(void) { return m_parent; }
69 void setStartIndex(double startIndex) { m_startIndex = startIndex; }
70 void setDuration(double duration) { m_duration = duration; }
71 void setTitle(const QString &title, bool update = false) { if(!update || (m_title.isEmpty() && !title.isEmpty())) m_title = title; }
72 void setPerformer(const QString &performer, bool update = false) { if(!update || (m_performer.isEmpty() && !performer.isEmpty())) m_performer = performer; }
73 void setGenre(const QString &genre, bool update = false) { if(!update || (m_genre.isEmpty() && !m_genre.isEmpty())) m_genre = genre; }
74 void setYear(const unsigned int year, bool update = false) { if(!update || (year == 0)) m_year = year; }
75 virtual bool isValid(void) { return !(_isnan(m_startIndex) || (m_trackNo < 0)); }
76 virtual const char* type(void) { return "CueSheetTrack"; }
85 CueSheetFile *m_parent;
88 class CueSheetFile : public CueSheetItem
91 CueSheetFile(const QString &fileName) : m_fileName(fileName) {}
92 ~CueSheetFile(void) { while(!m_tracks.isEmpty()) delete m_tracks.takeLast(); }
93 QString fileName(void) { return m_fileName; }
94 void addTrack(CueSheetTrack *track) { m_tracks.append(track); }
95 void clearTracks(void) { while(!m_tracks.isEmpty()) delete m_tracks.takeLast(); }
96 CueSheetTrack *track(int index) { return m_tracks.at(index); }
97 int trackCount(void) { return m_tracks.count(); }
98 virtual bool isValid(void) { return m_tracks.count() > 0; }
99 virtual const char* type(void) { return "CueSheetFile"; }
101 const QString m_fileName;
102 QList<CueSheetTrack*> m_tracks;
105 ////////////////////////////////////////////////////////////
106 // Constructor & Destructor
107 ////////////////////////////////////////////////////////////
109 QMutex CueSheetModel::m_mutex(QMutex::Recursive);
111 CueSheetModel::CueSheetModel()
113 m_fileIcon(":/icons/music.png"),
114 m_trackIcon(":/icons/control_play_blue.png")
119 for(int i = 0; i < 5; i++)
121 CueSheetFile *currentFile = new CueSheetFile(QString().sprintf("File %02d.wav", i+1));
122 for(int j = 0; j < 8; j++)
124 CueSheetTrack *currentTrack = new CueSheetTrack(currentFile, trackNo++);
125 currentTrack->setTitle("ATWA (Air Trees Water Animals)");
126 currentTrack->setPerformer("System of a Down");
127 currentFile->addTrack(currentTrack);
129 m_files.append(currentFile);
133 CueSheetModel::~CueSheetModel(void)
135 while(!m_files.isEmpty()) delete m_files.takeLast();
138 ////////////////////////////////////////////////////////////
140 ////////////////////////////////////////////////////////////
142 QModelIndex CueSheetModel::index(int row, int column, const QModelIndex &parent) const
144 QMutexLocker lock(&m_mutex);
146 if(!parent.isValid())
148 return (row < m_files.count()) ? createIndex(row, column, m_files.at(row)) : QModelIndex();
151 CueSheetItem *parentItem = static_cast<CueSheetItem*>(parent.internalPointer());
152 if(CueSheetFile *filePtr = dynamic_cast<CueSheetFile*>(parentItem))
154 return (row < filePtr->trackCount()) ? createIndex(row, column, filePtr->track(row)) : QModelIndex();
157 return QModelIndex();
160 int CueSheetModel::columnCount(const QModelIndex &parent) const
162 QMutexLocker lock(&m_mutex);
166 int CueSheetModel::rowCount(const QModelIndex &parent) const
168 QMutexLocker lock(&m_mutex);
170 if(!parent.isValid())
172 return m_files.count();
175 CueSheetItem *parentItem = static_cast<CueSheetItem*>(parent.internalPointer());
176 if(CueSheetFile *filePtr = dynamic_cast<CueSheetFile*>(parentItem))
178 return filePtr->trackCount();
184 QModelIndex CueSheetModel::parent(const QModelIndex &child) const
186 QMutexLocker lock(&m_mutex);
190 CueSheetItem *childItem = static_cast<CueSheetItem*>(child.internalPointer());
191 if(CueSheetTrack *trackPtr = dynamic_cast<CueSheetTrack*>(childItem))
193 return createIndex(m_files.indexOf(trackPtr->parent()), 0, trackPtr->parent());
197 return QModelIndex();
200 QVariant CueSheetModel::headerData (int section, Qt::Orientation orientation, int role) const
202 QMutexLocker lock(&m_mutex);
204 if(role == Qt::DisplayRole)
212 return tr("File / Track");
218 return tr("Duration");
231 QVariant CueSheetModel::data(const QModelIndex &index, int role) const
233 QMutexLocker lock(&m_mutex);
235 if(role == Qt::DisplayRole)
237 CueSheetItem *item = reinterpret_cast<CueSheetItem*>(index.internalPointer());
239 if(CueSheetFile *filePtr = dynamic_cast<CueSheetFile*>(item))
241 switch(index.column())
244 return tr("File %1").arg(QString().sprintf("%02d", index.row() + 1)).append(" ");
247 return QFileInfo(filePtr->fileName()).fileName();
254 else if(CueSheetTrack *trackPtr = dynamic_cast<CueSheetTrack*>(item))
256 switch(index.column())
259 return tr("Track %1").arg(QString().sprintf("%02d", trackPtr->trackNo())).append(" ");
262 if(!trackPtr->title().isEmpty() && !trackPtr->performer().isEmpty())
264 return QString("%1 - %2").arg(trackPtr->performer(), trackPtr->title());
266 else if(!trackPtr->title().isEmpty())
268 return QString("%1 - %2").arg(tr("Unknown Artist"), trackPtr->title());
270 else if(!trackPtr->performer().isEmpty())
272 return QString("%1 - %2").arg(trackPtr->performer(), tr("Unknown Title"));
276 return QString("%1 - %2").arg(tr("Unknown Artist"), tr("Unknown Title"));
280 return indexToString(trackPtr->startIndex());
283 return indexToString(trackPtr->duration());
291 else if(role == Qt::ToolTipRole)
293 CueSheetItem *item = reinterpret_cast<CueSheetItem*>(index.internalPointer());
295 if(CueSheetFile *filePtr = dynamic_cast<CueSheetFile*>(item))
297 return QDir::toNativeSeparators(filePtr->fileName());
299 else if(CueSheetTrack *trackPtr = dynamic_cast<CueSheetTrack*>(item))
301 return QDir::toNativeSeparators(trackPtr->parent()->fileName());
304 else if(role == Qt::DecorationRole)
306 if(index.column() == 0)
308 CueSheetItem *item = reinterpret_cast<CueSheetItem*>(index.internalPointer());
310 if(dynamic_cast<CueSheetFile*>(item))
314 else if(dynamic_cast<CueSheetTrack*>(item))
320 else if(role == Qt::FontRole)
322 QFont font("Monospace");
323 font.setStyleHint(QFont::TypeWriter);
324 if((index.column() == 1))
326 CueSheetItem *item = reinterpret_cast<CueSheetItem*>(index.internalPointer());
327 font.setBold(dynamic_cast<CueSheetFile*>(item) != NULL);
331 else if(role == Qt::ForegroundRole)
333 if((index.column() == 1))
335 CueSheetItem *item = reinterpret_cast<CueSheetItem*>(index.internalPointer());
336 if(CueSheetFile *filePtr = dynamic_cast<CueSheetFile*>(item))
338 return (QFileInfo(filePtr->fileName()).size() > 4) ? QColor("mediumblue") : QColor("darkred");
341 else if((index.column() == 3))
343 CueSheetItem *item = reinterpret_cast<CueSheetItem*>(index.internalPointer());
344 if(CueSheetTrack *trackPtr = dynamic_cast<CueSheetTrack*>(item))
346 if(trackPtr->duration() == std::numeric_limits<double>::infinity())
348 return QColor("dimgrey");
357 void CueSheetModel::clearData(void)
359 QMutexLocker lock(&m_mutex);
362 while(!m_files.isEmpty()) delete m_files.takeLast();
366 ////////////////////////////////////////////////////////////
368 ////////////////////////////////////////////////////////////
370 int CueSheetModel::getFileCount(void)
372 QMutexLocker lock(&m_mutex);
373 return m_files.count();
376 QString CueSheetModel::getFileName(int fileIndex)
378 QMutexLocker lock(&m_mutex);
380 if(fileIndex < 0 || fileIndex >= m_files.count())
385 return m_files.at(fileIndex)->fileName();
388 int CueSheetModel::getTrackCount(int fileIndex)
390 QMutexLocker lock(&m_mutex);
392 if(fileIndex < 0 || fileIndex >= m_files.count())
397 return m_files.at(fileIndex)->trackCount();
400 int CueSheetModel::getTrackNo(int fileIndex, int trackIndex)
402 QMutexLocker lock(&m_mutex);
404 if(fileIndex >= 0 && fileIndex < m_files.count())
406 CueSheetFile *currentFile = m_files.at(fileIndex);
407 if(trackIndex >= 0 && trackIndex < currentFile->trackCount())
409 return currentFile->track(trackIndex)->trackNo();
416 void CueSheetModel::getTrackIndex(int fileIndex, int trackIndex, double *startIndex, double *duration)
418 QMutexLocker lock(&m_mutex);
420 *startIndex = std::numeric_limits<double>::quiet_NaN();
421 *duration = std::numeric_limits<double>::quiet_NaN();
423 if(fileIndex >= 0 && fileIndex < m_files.count())
425 CueSheetFile *currentFile = m_files.at(fileIndex);
426 if(trackIndex >= 0 && trackIndex < currentFile->trackCount())
428 CueSheetTrack *currentTrack = currentFile->track(trackIndex);
429 *startIndex = currentTrack->startIndex();
430 *duration = currentTrack->duration();
435 QString CueSheetModel::getTrackPerformer(int fileIndex, int trackIndex)
437 QMutexLocker lock(&m_mutex);
439 if(fileIndex >= 0 && fileIndex < m_files.count())
441 CueSheetFile *currentFile = m_files.at(fileIndex);
442 if(trackIndex >= 0 && trackIndex < currentFile->trackCount())
444 CueSheetTrack *currentTrack = currentFile->track(trackIndex);
445 return currentTrack->performer();
452 QString CueSheetModel::getTrackTitle(int fileIndex, int trackIndex)
454 QMutexLocker lock(&m_mutex);
456 if(fileIndex >= 0 && fileIndex < m_files.count())
458 CueSheetFile *currentFile = m_files.at(fileIndex);
459 if(trackIndex >= 0 && trackIndex < currentFile->trackCount())
461 CueSheetTrack *currentTrack = currentFile->track(trackIndex);
462 return currentTrack->title();
469 QString CueSheetModel::getTrackGenre(int fileIndex, int trackIndex)
471 QMutexLocker lock(&m_mutex);
473 if(fileIndex >= 0 && fileIndex < m_files.count())
475 CueSheetFile *currentFile = m_files.at(fileIndex);
476 if(trackIndex >= 0 && trackIndex < currentFile->trackCount())
478 CueSheetTrack *currentTrack = currentFile->track(trackIndex);
479 return currentTrack->genre();
486 unsigned int CueSheetModel::getTrackYear(int fileIndex, int trackIndex)
488 QMutexLocker lock(&m_mutex);
490 if(fileIndex >= 0 && fileIndex < m_files.count())
492 CueSheetFile *currentFile = m_files.at(fileIndex);
493 if(trackIndex >= 0 && trackIndex < currentFile->trackCount())
495 CueSheetTrack *currentTrack = currentFile->track(trackIndex);
496 return currentTrack->year();
503 QString CueSheetModel::getAlbumPerformer(void)
505 QMutexLocker lock(&m_mutex);
506 return m_albumPerformer;
509 QString CueSheetModel::getAlbumTitle(void)
511 QMutexLocker lock(&m_mutex);
515 QString CueSheetModel::getAlbumGenre(void)
517 QMutexLocker lock(&m_mutex);
521 unsigned int CueSheetModel::getAlbumYear(void)
523 QMutexLocker lock(&m_mutex);
526 ////////////////////////////////////////////////////////////
528 ////////////////////////////////////////////////////////////
530 int CueSheetModel::loadCueSheet(const QString &cueFileName, QCoreApplication *application)
532 QMutexLocker lock(&m_mutex);
534 QFile cueFile(cueFileName);
535 if(!cueFile.open(QIODevice::ReadOnly))
537 return ErrorIOFailure;
543 int iResult = parseCueFile(cueFile, QDir(QFileInfo(cueFile).canonicalPath()), application);
549 int CueSheetModel::parseCueFile(QFile &cueFile, const QDir &baseDir, QCoreApplication *application)
552 qDebug("\n[Cue Sheet Import]");
554 //Reject very large files, as parsing might take until forever
555 if(cueFile.size() >= 10485760i64)
557 qWarning("File is very big. Probably not a Cue Sheet. Rejecting...");
561 //Get system local Codec
562 const QTextCodec *codec = QTextCodec::codecForName("System");
564 //Check for UTF-8 BOM in order to guess encoding
565 bool bUTF8 = false, bForceLatin1 = false;
566 QByteArray bomCheck = cueFile.peek(128);
567 bUTF8 = (!bomCheck.isEmpty()) && bomCheck.contains("\xef\xbb\xbf");
568 qDebug("Encoding is %s.", (bUTF8 ? "UTF-8" : "Local 8-Bit"));
571 //Test selected Codepage for decoding errors
574 const QString replacementSymbol = QString(QChar(QChar::ReplacementCharacter));
575 QByteArray data = cueFile.peek(1048576);
576 if((!data.isEmpty()) && codec->toUnicode(data.constData(), data.size()).contains(replacementSymbol))
578 qWarning("Decoding error using local 8-Bit codepage. Enforcing Latin-1.");
583 QRegExp rxFile("^FILE\\s+(\"[^\"]+\"|\\S+)\\s+(\\w+)$", Qt::CaseInsensitive);
584 QRegExp rxTrack("^TRACK\\s+(\\d+)\\s(\\w+)$", Qt::CaseInsensitive);
585 QRegExp rxIndex("^INDEX\\s+(\\d+)\\s+([0-9:]+)$", Qt::CaseInsensitive);
586 QRegExp rxTitle("^TITLE\\s+(\"[^\"]+\"|\\S+)$", Qt::CaseInsensitive);
587 QRegExp rxPerformer("^PERFORMER\\s+(\"[^\"]+\"|\\S+)$", Qt::CaseInsensitive);
588 QRegExp rxGenre("^REM\\s+GENRE\\s+(\"[^\"]+\"|\\S+)$", Qt::CaseInsensitive);
589 QRegExp rxYear("^REM\\s+DATE\\s+(\\d+)$", Qt::CaseInsensitive);
591 bool bPreamble = true;
592 bool bUnsupportedTrack = false;
594 CueSheetFile *currentFile = NULL;
595 CueSheetTrack *currentTrack = NULL;
597 m_albumTitle.clear();
598 m_albumPerformer.clear();
599 m_albumGenre.clear();
602 //Loop over the Cue Sheet until all lines were processed
603 for(int lines = 0; lines < INT_MAX; lines++)
607 application->processEvents();
608 if(lines < 128) Sleep(10);
611 QByteArray lineData = cueFile.readLine();
612 if(lineData.size() <= 0)
614 qDebug("End of Cue Sheet file.");
618 QString line = (bUTF8 ? QString::fromUtf8(lineData.constData(), lineData.size()) : (bForceLatin1 ? QString::fromLatin1(lineData.constData(), lineData.size()) : codec->toUnicode(lineData.constData(), lineData.size()))).trimmed();
621 if(rxFile.indexIn(line) >= 0)
623 qDebug("%03d File: <%s> <%s>", lines, rxFile.cap(1).toUtf8().constData(), rxFile.cap(2).toUtf8().constData());
628 if(currentTrack->isValid())
630 currentFile->addTrack(currentTrack);
635 LAMEXP_DELETE(currentTrack);
638 if(currentFile->isValid())
640 m_files.append(currentFile);
645 LAMEXP_DELETE(currentFile);
650 LAMEXP_DELETE(currentTrack);
652 if(!rxFile.cap(2).compare("WAVE", Qt::CaseInsensitive) || !rxFile.cap(2).compare("MP3", Qt::CaseInsensitive) || !rxFile.cap(2).compare("AIFF", Qt::CaseInsensitive))
654 currentFile = new CueSheetFile(baseDir.absoluteFilePath(UNQUOTE(rxFile.cap(1))));
655 qDebug("%03d File path: <%s>", lines, currentFile->fileName().toUtf8().constData());
659 bUnsupportedTrack = true;
660 qWarning("%03d Skipping unsupported file of type '%s'.", lines, rxFile.cap(2).toUtf8().constData());
669 if(rxTrack.indexIn(line) >= 0)
673 qDebug("%03d Track: <%s> <%s>", lines, rxTrack.cap(1).toUtf8().constData(), rxTrack.cap(2).toUtf8().constData());
676 if(currentTrack->isValid())
678 currentFile->addTrack(currentTrack);
683 LAMEXP_DELETE(currentTrack);
686 if(!rxTrack.cap(2).compare("AUDIO", Qt::CaseInsensitive))
688 currentTrack = new CueSheetTrack(currentFile, rxTrack.cap(1).toInt());
692 bUnsupportedTrack = true;
693 qWarning("%03d Skipping unsupported track of type '%s'.", lines, rxTrack.cap(2).toUtf8().constData());
699 LAMEXP_DELETE(currentTrack);
706 if(rxIndex.indexIn(line) >= 0)
708 if(currentFile && currentTrack)
710 qDebug("%03d Index: <%s> <%s>", lines, rxIndex.cap(1).toUtf8().constData(), rxIndex.cap(2).toUtf8().constData());
711 if(rxIndex.cap(1).toInt() == 1)
713 currentTrack->setStartIndex(parseTimeIndex(rxIndex.cap(2)));
720 if(rxTitle.indexIn(line) >= 0)
724 m_albumTitle = UNQUOTE(rxTitle.cap(1)).simplified();
726 else if(currentFile && currentTrack)
728 qDebug("%03d Title: <%s>", lines, rxTitle.cap(1).toUtf8().constData());
729 currentTrack->setTitle(UNQUOTE(rxTitle.cap(1)).simplified());
734 /* --- PERFORMER --- */
735 if(rxPerformer.indexIn(line) >= 0)
739 m_albumPerformer = UNQUOTE(rxPerformer.cap(1)).simplified();
741 else if(currentFile && currentTrack)
743 qDebug("%03d Title: <%s>", lines, rxPerformer.cap(1).toUtf8().constData());
744 currentTrack->setPerformer(UNQUOTE(rxPerformer.cap(1)).simplified());
750 if(rxGenre.indexIn(line) >= 0)
754 QString temp = UNQUOTE(rxGenre.cap(1)).simplified();
755 for(int i = 0; g_lamexp_generes[i]; i++)
757 if(temp.compare(g_lamexp_generes[i], Qt::CaseInsensitive) == 0)
759 m_albumGenre = QString(g_lamexp_generes[i]);
764 else if(currentFile && currentTrack)
766 qDebug("%03d Genre: <%s>", lines, rxGenre.cap(1).toUtf8().constData());
767 QString temp = UNQUOTE(rxGenre.cap(1).simplified());
768 for(int i = 0; g_lamexp_generes[i]; i++)
770 if(temp.compare(g_lamexp_generes[i], Qt::CaseInsensitive) == 0)
772 currentTrack->setGenre(QString(g_lamexp_generes[i]));
781 if(rxYear.indexIn(line) >= 0)
786 unsigned int temp = rxYear.cap(1).toUInt(&ok);
787 if(ok) m_albumYear = temp;
789 else if(currentFile && currentTrack)
791 qDebug("%03d Year: <%s>", lines, rxPerformer.cap(1).toUtf8().constData());
793 unsigned int temp = rxYear.cap(1).toUInt(&ok);
794 if(ok) currentTrack->setYear(temp);
800 //Append the very last track/file that is still pending
805 if(currentTrack->isValid())
807 currentFile->addTrack(currentTrack);
812 LAMEXP_DELETE(currentTrack);
815 if(currentFile->isValid())
817 m_files.append(currentFile);
822 LAMEXP_DELETE(currentFile);
826 //Finally calculate duration of each track
827 int nFiles = m_files.count();
828 for(int i = 0; i < nFiles; i++)
832 application->processEvents();
836 CueSheetFile *currentFile = m_files.at(i);
837 int nTracks = currentFile->trackCount();
840 for(int j = 1; j < nTracks; j++)
842 CueSheetTrack *currentTrack = currentFile->track(j);
843 CueSheetTrack *previousTrack = currentFile->track(j-1);
844 double duration = currentTrack->startIndex() - previousTrack->startIndex();
845 previousTrack->setDuration(qMax(0.0, duration));
850 //Sanity check of track numbers
853 bool hasTracks = false;
854 int previousTrackNo = -1;
856 for(int i = 0; i < 100; i++)
861 for(int i = 0; i < nFiles; i++)
865 application->processEvents();
868 CueSheetFile *currentFile = m_files.at(i);
869 int nTracks = currentFile->trackCount();
872 for(int j = 0; j < nTracks; j++)
874 int currentTrackNo = currentFile->track(j)->trackNo();
875 if(currentTrackNo > 99)
877 qWarning("Track #%02d is invalid (maximum is 99), Cue Sheet is inconsistent!", currentTrackNo);
878 return ErrorInconsistent;
880 if(currentTrackNo <= previousTrackNo)
882 qWarning("Non-increasing track numbers (%02d -> %02d), Cue Sheet is inconsistent!", previousTrackNo, currentTrackNo);
883 return ErrorInconsistent;
885 if(trackNo[currentTrackNo])
887 qWarning("Track #%02d exists multiple times, Cue Sheet is inconsistent!", currentTrackNo);
888 return ErrorInconsistent;
890 trackNo[currentTrackNo] = true;
891 previousTrackNo = currentTrackNo;
899 qWarning("Could not find at least one valid track in the Cue Sheet!");
900 return ErrorInconsistent;
907 qWarning("Could not find at least one valid input file in the Cue Sheet!");
908 return bUnsupportedTrack ? ErrorUnsupported : ErrorBadFile;
912 double CueSheetModel::parseTimeIndex(const QString &index)
914 QRegExp rxTimeIndex("\\s*(\\d+)\\s*:\\s*(\\d+)\\s*:\\s*(\\d+)\\s*");
916 if(rxTimeIndex.indexIn(index) >= 0)
919 bool minOK, secOK, frmOK;
921 min = rxTimeIndex.cap(1).toInt(&minOK);
922 sec = rxTimeIndex.cap(2).toInt(&secOK);
923 frm = rxTimeIndex.cap(3).toInt(&frmOK);
925 if(minOK && secOK && frmOK)
927 return static_cast<double>(60 * min) + static_cast<double>(sec) + (static_cast<double>(frm) / 75.0);
931 qWarning(" Bad time index: '%s'", index.toUtf8().constData());
932 return std::numeric_limits<double>::quiet_NaN();
935 QString CueSheetModel::indexToString(const double index) const
937 if(!_finite(index) || (index < 0.0) || (index > 86400.0))
939 return QString("??:??.???");
942 QTime time = QTime().addMSecs(static_cast<int>(floor(0.5 + (index * 1000.0))));
944 if(time.minute() < 100)
946 return time.toString("mm:ss.zzz");
950 return QString("99:99.999");