Updated for recent change in MUtils library.
[LameXP.git] / src / Model_CueSheet.cpp
blob3ba5089ce17795ca85b81ed234b98cab0ae8761b
1 ///////////////////////////////////////////////////////////////////////////////
2 // LameXP - Audio Encoder Front-End
3 // Copyright (C) 2004-2018 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, 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 ///////////////////////////////////////////////////////////////////////////////
23 //Internal
24 #include "Global.h"
25 #include "Model_CueSheet.h"
26 #include "Model_AudioFile.h"
27 #include "Genres.h"
29 //MUtils
30 #include <MUtils/Global.h>
31 #include <MUtils/OSSupport.h>
33 //Qt
34 #include <QApplication>
35 #include <QDir>
36 #include <QFileInfo>
37 #include <QFont>
38 #include <QTime>
39 #include <QTextCodec>
40 #include <QTextStream>
41 #include <QMutexLocker>
43 //CRT
44 #include <float.h>
45 #include <limits>
47 #define UNQUOTE(STR) STR.split("\"", QString::SkipEmptyParts).first().trimmed()
49 ////////////////////////////////////////////////////////////
50 // Helper Classes
51 ////////////////////////////////////////////////////////////
53 class CueSheetItem
55 public:
56 virtual const char* type(void) = 0;
57 virtual bool isValid(void) { return false; }
60 class CueSheetTrack : public CueSheetItem
62 public:
63 CueSheetTrack(CueSheetFile *parent, int trackNo)
65 m_parent(parent)
67 m_startIndex = std::numeric_limits<double>::quiet_NaN();
68 m_duration = std::numeric_limits<double>::infinity();
69 m_metaInfo.setPosition(trackNo);
72 //Getter
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; }
78 //Setter
79 void setStartIndex(double startIndex) { m_startIndex = startIndex; }
80 void setDuration(double duration) { m_duration = duration; }
82 //Misc
83 virtual bool isValid(void) { return !(_isnan(m_startIndex) || (m_metaInfo.position() == 0)); }
84 virtual const char* type(void) { return "CueSheetTrack"; }
86 private:
87 double m_startIndex;
88 double m_duration;
89 AudioFileModel_MetaInfo m_metaInfo;
90 CueSheetFile *const m_parent;
93 class CueSheetFile : public CueSheetItem
95 public:
96 CueSheetFile(const QString &fileName) : m_fileName(fileName) {}
97 ~CueSheetFile(void) { while(!m_tracks.isEmpty()) delete m_tracks.takeLast(); }
99 //Getter
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(); }
104 //Modifier
105 void addTrack(CueSheetTrack *track) { m_tracks.append(track); }
106 void clearTracks(void) { while(!m_tracks.isEmpty()) delete m_tracks.takeLast(); }
108 //Misc
109 virtual bool isValid(void) { return m_tracks.count() > 0; }
110 virtual const char* type(void) { return "CueSheetFile"; }
112 private:
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")
128 /*nothing to do*/
131 CueSheetModel::~CueSheetModel(void)
133 while(!m_files.isEmpty()) delete m_files.takeLast();
136 ////////////////////////////////////////////////////////////
137 // Model Functions
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);
161 return 4;
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();
179 return 0;
182 QModelIndex CueSheetModel::parent(const QModelIndex &child) const
184 QMutexLocker lock(&m_mutex);
186 if(child.isValid())
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)
204 switch(section)
206 case 0:
207 return tr("No.");
208 break;
209 case 1:
210 return tr("File / Track");
211 break;
212 case 2:
213 return tr("Index");
214 break;
215 case 3:
216 return tr("Duration");
217 break;
218 default:
219 return QVariant();
220 break;
223 else
225 return QVariant();
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())
241 case 0:
242 return tr("File %1").arg(QString().sprintf("%02d", index.row() + 1)).append(" ");
243 break;
244 case 1:
245 return QFileInfo(filePtr->fileName()).fileName();
246 break;
247 default:
248 return QVariant();
249 break;
252 else if(CueSheetTrack *trackPtr = dynamic_cast<CueSheetTrack*>(item))
254 const AudioFileModel_MetaInfo &trackInfo = trackPtr->metaInfo();
255 switch(index.column())
257 case 0:
258 return tr("Track %1").arg(QString().sprintf("%02d", trackInfo.position())).append(" ");
259 break;
260 case 1:
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"));
273 else
275 return QString("%1 - %2").arg(tr("Unknown Artist"), tr("Unknown Title"));
277 break;
278 case 2:
279 return indexToString(trackPtr->startIndex());
280 break;
281 case 3:
282 return indexToString(trackPtr->duration());
283 break;
284 default:
285 return QVariant();
286 break;
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))
311 return m_fileIcon;
313 else if(dynamic_cast<CueSheetTrack*>(item))
315 return m_trackIcon;
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);
328 return font;
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");
353 return QVariant();
356 void CueSheetModel::clearData(void)
358 QMutexLocker lock(&m_mutex);
360 beginResetModel();
361 while(!m_files.isEmpty()) delete m_files.takeLast();
362 endResetModel();
365 ////////////////////////////////////////////////////////////
366 // External API
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())
381 return QString();
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())
393 return -1;
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 &currentFile->track(trackIndex)->metaInfo();
412 return NULL;
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();
430 return true;
434 return false;
437 const AudioFileModel_MetaInfo *CueSheetModel::getAlbumInfo(void)
439 QMutexLocker lock(&m_mutex);
440 return &m_albumInfo;
443 ////////////////////////////////////////////////////////////
444 // Cue Sheet Parser
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;
458 clearData();
460 beginResetModel();
461 int iResult = parseCueFile(cueFile, QDir(QFileInfo(cueFile).canonicalPath()), application, codec);
462 endResetModel();
464 return iResult;
467 int CueSheetModel::parseCueFile(QFile &cueFile, const QDir &baseDir, QCoreApplication *application, const QTextCodec *codec)
469 cueFile.reset();
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...");
477 return 2;
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());
487 bForceLatin1 = true;
489 testData.clear();
491 //Init text stream
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;
512 m_albumInfo.reset();
514 //Loop over the Cue Sheet until all lines were processed
515 for(int lines = 0; lines < INT_MAX; lines++)
517 if(application)
519 application->processEvents();
520 if(lines < 128) MUtils::OS::sleep_ms(10);
523 if(cueStream.atEnd())
525 qDebug("End of Cue Sheet file.");
526 break;
529 QString line = cueStream.readLine().trimmed();
531 /* --- FILE --- */
532 if(rxFile.indexIn(line) >= 0)
534 qDebug("%03d File: <%s> <%s>", lines, MUTILS_UTF8(rxFile.cap(1)), MUTILS_UTF8(rxFile.cap(2)));
535 if(currentFile)
537 if(currentTrack)
539 if(currentTrack->isValid())
541 currentFile->addTrack(currentTrack);
542 currentTrack = NULL;
544 else
546 MUTILS_DELETE(currentTrack);
549 if(currentFile->isValid())
551 m_files.append(currentFile);
552 currentFile = NULL;
554 else
556 MUTILS_DELETE(currentFile);
559 else
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());
568 else
570 bUnsupportedTrack = true;
571 qWarning("%03d Skipping unsupported file of type '%s'.", lines, MUTILS_UTF8(rxFile.cap(2)));
572 currentFile = NULL;
574 bPreamble = false;
575 currentTrack = NULL;
576 continue;
579 /* --- TRACK --- */
580 if(rxTrack.indexIn(line) >= 0)
582 if(currentFile)
584 qDebug("%03d Track: <%s> <%s>", lines, MUTILS_UTF8(rxTrack.cap(1)), MUTILS_UTF8(rxTrack.cap(2)));
585 if(currentTrack)
587 if(currentTrack->isValid())
589 currentFile->addTrack(currentTrack);
590 currentTrack = NULL;
592 else
594 MUTILS_DELETE(currentTrack);
597 if(!rxTrack.cap(2).compare("AUDIO", Qt::CaseInsensitive))
599 currentTrack = new CueSheetTrack(currentFile, rxTrack.cap(1).toInt());
601 else
603 bUnsupportedTrack = true;
604 qWarning("%03d Skipping unsupported track of type '%s'.", lines, MUTILS_UTF8(rxTrack.cap(2)));
605 currentTrack = NULL;
608 else
610 MUTILS_DELETE(currentTrack);
612 bPreamble = false;
613 continue;
616 /* --- INDEX --- */
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)));
627 continue;
630 /* --- TITLE --- */
631 if(rxTitle.indexIn(line) >= 0)
633 if(bPreamble)
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());
642 continue;
645 /* --- PERFORMER --- */
646 if(rxPerformer.indexIn(line) >= 0)
648 if(bPreamble)
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());
657 continue;
660 /* --- GENRE --- */
661 if(rxGenre.indexIn(line) >= 0)
663 if(bPreamble)
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]));
671 break;
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]));
684 break;
688 continue;
691 /* --- YEAR --- */
692 if(rxYear.indexIn(line) >= 0)
694 if(bPreamble)
696 bool ok = false;
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)));
703 bool ok = false;
704 unsigned int temp = rxYear.cap(1).toUInt(&ok);
705 if(ok) currentTrack->metaInfo().setYear(temp);
707 continue;
711 //Append the very last track/file that is still pending
712 if(currentFile)
714 if(currentTrack)
716 if(currentTrack->isValid())
718 currentFile->addTrack(currentTrack);
719 currentTrack = NULL;
721 else
723 MUTILS_DELETE(currentTrack);
726 if(currentFile->isValid())
728 m_files.append(currentFile);
729 currentFile = NULL;
731 else
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++)
741 if(application)
743 application->processEvents();
744 MUtils::OS::sleep_ms(10);
747 CueSheetFile *currentFile = m_files.at(i);
748 int nTracks = currentFile->trackCount();
749 if(nTracks > 1)
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
762 if(nFiles > 0)
764 bool hasTracks = false;
765 int previousTrackNo = -1;
766 bool trackNo[100];
767 for(int i = 0; i < 100; i++)
769 trackNo[i] = false;
772 for(int i = 0; i < nFiles; i++)
774 if(application)
776 application->processEvents();
777 MUtils::OS::sleep_ms(10);
779 CueSheetFile *currentFile = m_files.at(i);
780 int nTracks = currentFile->trackCount();
781 if(nTracks > 1)
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;
803 hasTracks = true;
808 if(!hasTracks)
810 qWarning("Could not find at least one valid track in the Cue Sheet!");
811 return ErrorInconsistent;
814 return ErrorSuccess;
816 else
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)
829 int time[3];
830 if(MUtils::regexp_parse_int32(rxTimeIndex, time, 3))
832 return static_cast<double>(60 * time[0]) + static_cast<double>(time[1]) + (static_cast<double>(time[2]) / 75.0);
836 qWarning(" Bad time index: '%s'", MUTILS_UTF8(index));
837 return std::numeric_limits<double>::quiet_NaN();
840 QString CueSheetModel::indexToString(const double index) const
842 if((!_finite(index)) || (index < 0.0) || (index > 86400.0))
844 return QString("??:??.???");
847 const int minutes = static_cast<int>(index / 60.0);
848 if(minutes < 100)
850 const MUtils::fp_parts_t seconds = MUtils::break_fp(fmod(index, 60.0));
851 return QString().sprintf("%02d:%02d.%03d", minutes, static_cast<int>(seconds.parts[0]), static_cast<int>(seconds.parts[1] * 1000.0));
854 return QString("99:99.999");