Updated CueImportDialog and CueSheetModel as well as the CueSheet helper classes...
[LameXP.git] / src / Model_CueSheet.cpp
blob11f1056f1367a39b67eb537df8ad6b0cf0ba26a2
1 ///////////////////////////////////////////////////////////////////////////////
2 // LameXP - Audio Encoder Front-End
3 // Copyright (C) 2004-2013 LoRd_MuldeR <MuldeR2@GMX.de>
4 //
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.
9 //
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 "Global.h"
23 #include "Model_CueSheet.h"
24 #include "Model_AudioFile.h"
25 #include "Genres.h"
27 #include <QApplication>
28 #include <QDir>
29 #include <QFileInfo>
30 #include <QFont>
31 #include <QTime>
32 #include <QTextCodec>
33 #include <QTextStream>
34 #include <QMutexLocker>
36 #include <float.h>
37 #include <limits>
39 #define UNQUOTE(STR) STR.split("\"", QString::SkipEmptyParts).first().trimmed()
41 ////////////////////////////////////////////////////////////
42 // Helper Classes
43 ////////////////////////////////////////////////////////////
45 class CueSheetItem
47 public:
48 virtual const char* type(void) = 0;
49 virtual bool isValid(void) { return false; }
52 class CueSheetTrack : public CueSheetItem
54 public:
55 CueSheetTrack(CueSheetFile *parent, int trackNo)
57 m_parent(parent)
59 m_startIndex = std::numeric_limits<double>::quiet_NaN();
60 m_duration = std::numeric_limits<double>::infinity();
61 m_metaInfo.setPosition(trackNo);
64 //Getter
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; }
70 //Setter
71 void setStartIndex(double startIndex) { m_startIndex = startIndex; }
72 void setDuration(double duration) { m_duration = duration; }
74 //Misc
75 virtual bool isValid(void) { return !(_isnan(m_startIndex) || (m_metaInfo.position() == 0)); }
76 virtual const char* type(void) { return "CueSheetTrack"; }
78 private:
79 double m_startIndex;
80 double m_duration;
81 AudioFileModel_MetaInfo m_metaInfo;
82 CueSheetFile *const m_parent;
85 class CueSheetFile : public CueSheetItem
87 public:
88 CueSheetFile(const QString &fileName) : m_fileName(fileName) {}
89 ~CueSheetFile(void) { while(!m_tracks.isEmpty()) delete m_tracks.takeLast(); }
91 //Getter
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(); }
96 //Modifier
97 void addTrack(CueSheetTrack *track) { m_tracks.append(track); }
98 void clearTracks(void) { while(!m_tracks.isEmpty()) delete m_tracks.takeLast(); }
100 //Misc
101 virtual bool isValid(void) { return m_tracks.count() > 0; }
102 virtual const char* type(void) { return "CueSheetFile"; }
104 private:
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")
120 /*nothing to do*/
123 CueSheetModel::~CueSheetModel(void)
125 while(!m_files.isEmpty()) delete m_files.takeLast();
128 ////////////////////////////////////////////////////////////
129 // Model Functions
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);
153 return 4;
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();
171 return 0;
174 QModelIndex CueSheetModel::parent(const QModelIndex &child) const
176 QMutexLocker lock(&m_mutex);
178 if(child.isValid())
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)
196 switch(section)
198 case 0:
199 return tr("No.");
200 break;
201 case 1:
202 return tr("File / Track");
203 break;
204 case 2:
205 return tr("Index");
206 break;
207 case 3:
208 return tr("Duration");
209 break;
210 default:
211 return QVariant();
212 break;
215 else
217 return QVariant();
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())
233 case 0:
234 return tr("File %1").arg(QString().sprintf("%02d", index.row() + 1)).append(" ");
235 break;
236 case 1:
237 return QFileInfo(filePtr->fileName()).fileName();
238 break;
239 default:
240 return QVariant();
241 break;
244 else if(CueSheetTrack *trackPtr = dynamic_cast<CueSheetTrack*>(item))
246 const AudioFileModel_MetaInfo &trackInfo = trackPtr->metaInfo();
247 switch(index.column())
249 case 0:
250 return tr("Track %1").arg(QString().sprintf("%02d", trackInfo.position())).append(" ");
251 break;
252 case 1:
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"));
265 else
267 return QString("%1 - %2").arg(tr("Unknown Artist"), tr("Unknown Title"));
269 break;
270 case 2:
271 return indexToString(trackPtr->startIndex());
272 break;
273 case 3:
274 return indexToString(trackPtr->duration());
275 break;
276 default:
277 return QVariant();
278 break;
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))
303 return m_fileIcon;
305 else if(dynamic_cast<CueSheetTrack*>(item))
307 return m_trackIcon;
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);
320 return font;
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");
345 return QVariant();
348 void CueSheetModel::clearData(void)
350 QMutexLocker lock(&m_mutex);
352 beginResetModel();
353 while(!m_files.isEmpty()) delete m_files.takeLast();
354 endResetModel();
357 ////////////////////////////////////////////////////////////
358 // External API
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())
373 return QString();
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())
385 return -1;
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 &currentFile->track(trackIndex)->metaInfo();
404 return NULL;
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();
422 return true;
426 return false;
429 const AudioFileModel_MetaInfo *CueSheetModel::getAlbumInfo(void)
431 QMutexLocker lock(&m_mutex);
432 return &m_albumInfo;
435 ////////////////////////////////////////////////////////////
436 // Cue Sheet Parser
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;
450 clearData();
452 beginResetModel();
453 int iResult = parseCueFile(cueFile, QDir(QFileInfo(cueFile).canonicalPath()), application, codec);
454 endResetModel();
456 return iResult;
459 int CueSheetModel::parseCueFile(QFile &cueFile, const QDir &baseDir, QCoreApplication *application, const QTextCodec *codec)
461 cueFile.reset();
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...");
469 return 2;
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());
479 bForceLatin1 = true;
481 testData.clear();
483 //Init text stream
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;
504 m_albumInfo.reset();
506 //Loop over the Cue Sheet until all lines were processed
507 for(int lines = 0; lines < INT_MAX; lines++)
509 if(application)
511 application->processEvents();
512 if(lines < 128) lamexp_sleep(10);
515 if(cueStream.atEnd())
517 qDebug("End of Cue Sheet file.");
518 break;
521 QString line = cueStream.readLine().trimmed();
523 /* --- FILE --- */
524 if(rxFile.indexIn(line) >= 0)
526 qDebug("%03d File: <%s> <%s>", lines, rxFile.cap(1).toUtf8().constData(), rxFile.cap(2).toUtf8().constData());
527 if(currentFile)
529 if(currentTrack)
531 if(currentTrack->isValid())
533 currentFile->addTrack(currentTrack);
534 currentTrack = NULL;
536 else
538 LAMEXP_DELETE(currentTrack);
541 if(currentFile->isValid())
543 m_files.append(currentFile);
544 currentFile = NULL;
546 else
548 LAMEXP_DELETE(currentFile);
551 else
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());
560 else
562 bUnsupportedTrack = true;
563 qWarning("%03d Skipping unsupported file of type '%s'.", lines, rxFile.cap(2).toUtf8().constData());
564 currentFile = NULL;
566 bPreamble = false;
567 currentTrack = NULL;
568 continue;
571 /* --- TRACK --- */
572 if(rxTrack.indexIn(line) >= 0)
574 if(currentFile)
576 qDebug("%03d Track: <%s> <%s>", lines, rxTrack.cap(1).toUtf8().constData(), rxTrack.cap(2).toUtf8().constData());
577 if(currentTrack)
579 if(currentTrack->isValid())
581 currentFile->addTrack(currentTrack);
582 currentTrack = NULL;
584 else
586 LAMEXP_DELETE(currentTrack);
589 if(!rxTrack.cap(2).compare("AUDIO", Qt::CaseInsensitive))
591 currentTrack = new CueSheetTrack(currentFile, rxTrack.cap(1).toInt());
593 else
595 bUnsupportedTrack = true;
596 qWarning("%03d Skipping unsupported track of type '%s'.", lines, rxTrack.cap(2).toUtf8().constData());
597 currentTrack = NULL;
600 else
602 LAMEXP_DELETE(currentTrack);
604 bPreamble = false;
605 continue;
608 /* --- INDEX --- */
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)));
619 continue;
622 /* --- TITLE --- */
623 if(rxTitle.indexIn(line) >= 0)
625 if(bPreamble)
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());
634 continue;
637 /* --- PERFORMER --- */
638 if(rxPerformer.indexIn(line) >= 0)
640 if(bPreamble)
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());
649 continue;
652 /* --- GENRE --- */
653 if(rxGenre.indexIn(line) >= 0)
655 if(bPreamble)
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]));
663 break;
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]));
676 break;
680 continue;
683 /* --- YEAR --- */
684 if(rxYear.indexIn(line) >= 0)
686 if(bPreamble)
688 bool ok = false;
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());
695 bool ok = false;
696 unsigned int temp = rxYear.cap(1).toUInt(&ok);
697 if(ok) currentTrack->metaInfo().setYear(temp);
699 continue;
703 //Append the very last track/file that is still pending
704 if(currentFile)
706 if(currentTrack)
708 if(currentTrack->isValid())
710 currentFile->addTrack(currentTrack);
711 currentTrack = NULL;
713 else
715 LAMEXP_DELETE(currentTrack);
718 if(currentFile->isValid())
720 m_files.append(currentFile);
721 currentFile = NULL;
723 else
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++)
733 if(application)
735 application->processEvents();
736 lamexp_sleep(10);
739 CueSheetFile *currentFile = m_files.at(i);
740 int nTracks = currentFile->trackCount();
741 if(nTracks > 1)
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
754 if(nFiles > 0)
756 bool hasTracks = false;
757 int previousTrackNo = -1;
758 bool trackNo[100];
759 for(int i = 0; i < 100; i++)
761 trackNo[i] = false;
764 for(int i = 0; i < nFiles; i++)
766 if(application)
768 application->processEvents();
769 lamexp_sleep(10);
771 CueSheetFile *currentFile = m_files.at(i);
772 int nTracks = currentFile->trackCount();
773 if(nTracks > 1)
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;
795 hasTracks = true;
800 if(!hasTracks)
802 qWarning("Could not find at least one valid track in the Cue Sheet!");
803 return ErrorInconsistent;
806 return ErrorSuccess;
808 else
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)
821 int min, sec, frm;
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");
851 else
853 return QString("99:99.999");