///////////////////////////////////////////////////////////////////////////////
// LameXP - Audio Encoder Front-End
-// Copyright (C) 2004-2011 LoRd_MuldeR <MuldeR2@GMX.de>
+// Copyright (C) 2004-2015 LoRd_MuldeR <MuldeR2@GMX.de>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
-// (at your option) any later version.
+// (at your option) any later version, but always including the *additional*
+// restrictions defined in the "License.txt" file.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
#include "Thread_CueSplitter.h"
+//Internal
#include "Global.h"
#include "LockedFile.h"
#include "Model_AudioFile.h"
-#include "PlaylistImporter.h"
+#include "Model_CueSheet.h"
+#include "Registry_Decoder.h"
+#include "Decoder_Abstract.h"
+//MUtils
+#include <MUtils/Global.h>
+#include <MUtils/OSSupport.h>
+
+//Qt
#include <QDir>
#include <QFileInfo>
#include <QProcess>
#include <QTime>
#include <QDebug>
+//CRT
#include <math.h>
+#include <float.h>
+#include <limits>
////////////////////////////////////////////////////////////
// Constructor
////////////////////////////////////////////////////////////
-CueSplitter::CueSplitter(const QString &outputDir, const QString &baseName, const QList<AudioFileModel> &inputFiles)
+CueSplitter::CueSplitter(const QString &outputDir, const QString &baseName, CueSheetModel *model, const QList<AudioFileModel> &inputFilesInfo)
:
+ m_model(model),
m_outputDir(outputDir),
m_baseName(baseName),
- m_soxBin(lamexp_lookup_tool("sox.exe"))
+ m_soxBin(lamexp_tools_lookup("sox.exe"))
{
if(m_soxBin.isEmpty())
{
qFatal("Invalid path to SoX binary. Tool not initialized properly.");
}
- m_albumPerformer.clear();
- m_albumTitle.clear();
+ m_decompressedFiles.clear();
+ m_tempFiles.clear();
- qDebug("\n[CueSplitter::CueSplitter]");
+ qDebug("\n[CueSplitter]");
- int nInputFiles = inputFiles.count();
+ int nInputFiles = inputFilesInfo.count();
for(int i = 0; i < nInputFiles; i++)
{
- m_inputFiles.insert(inputFiles[i].filePath(), inputFiles[i]);
- qDebug("%02d <%s>", i, inputFiles[i].filePath());
+ m_inputFilesInfo.insert(inputFilesInfo[i].filePath(), inputFilesInfo[i]);
+ qDebug("File %02d: <%s>", i, MUTILS_UTF8(inputFilesInfo[i].filePath()));
}
qDebug("All input files added.");
m_bSuccess = false;
}
+CueSplitter::~CueSplitter(void)
+{
+ while(!m_tempFiles.isEmpty())
+ {
+ MUtils::remove_file(m_tempFiles.takeFirst());
+ }
+}
+
////////////////////////////////////////////////////////////
// Thread Main
////////////////////////////////////////////////////////////
void CueSplitter::run()
{
m_bSuccess = false;
+ m_bAborted = false;
+ m_abortFlag = false;
m_nTracksSuccess = 0;
m_nTracksSkipped = 0;
- m_abortFlag = false;
-
- int nTracks = min(min(min(m_trackFile.count(), m_trackNo.count()), min(m_trackOffset.count(), m_trackLength.count())), m_trackMetaInfo.count());
+ m_decompressedFiles.clear();
+ m_activeFile.clear();
if(!QDir(m_outputDir).exists())
{
- qWarning("Output directory \"%s\" does not exist!", m_outputDir.toUtf8().constData());
+ qWarning("Output directory \"%s\" does not exist!", MUTILS_UTF8(m_outputDir));
return;
}
+
+ QStringList inputFileList = m_inputFilesInfo.keys();
+ int nInputFiles = inputFileList.count();
+
+ emit progressMaxChanged(nInputFiles);
+ emit progressValChanged(0);
- for(int i = 0; i < nTracks; i++)
+ //Decompress all input files
+ for(int i = 0; i < nInputFiles; i++)
{
- QString outputFile = QString("%1/%2 - Track %3.wav").arg(m_outputDir, m_baseName, QString().sprintf("%02d", m_trackNo.at(i)));
- for(int n = 2; QFileInfo(outputFile).exists(); n++)
+ const AudioFileModel_TechInfo &inputFileInfo = m_inputFilesInfo[inputFileList.at(i)].techInfo();
+ if(inputFileInfo.containerType().compare("Wave", Qt::CaseInsensitive) || inputFileInfo.audioType().compare("PCM", Qt::CaseInsensitive))
+ {
+ AbstractDecoder *decoder = DecoderRegistry::lookup(inputFileInfo.containerType(), inputFileInfo.containerProfile(), inputFileInfo.audioType(), inputFileInfo.audioProfile(), inputFileInfo.audioVersion());
+ if(decoder)
+ {
+ m_activeFile = shortName(QFileInfo(inputFileList.at(i)).fileName());
+
+ emit fileSelected(m_activeFile);
+ emit progressValChanged(i+1);
+
+ QString tempFile = QString("%1/~%2.wav").arg(m_outputDir, MUtils::rand_str());
+ connect(decoder, SIGNAL(statusUpdated(int)), this, SLOT(handleUpdate(int)), Qt::DirectConnection);
+
+ if(decoder->decode(inputFileList.at(i), tempFile, &m_abortFlag))
+ {
+ m_decompressedFiles.insert(inputFileList.at(i), tempFile);
+ m_tempFiles.append(tempFile);
+ }
+ else
+ {
+ qWarning("Failed to decompress file: <%s>", inputFileList.at(i).toLatin1().constData());
+ MUtils::remove_file(tempFile);
+ }
+
+ m_activeFile.clear();
+ MUTILS_DELETE(decoder);
+ }
+ else
+ {
+ qWarning("Unsupported input file: <%s>", inputFileList.at(i).toLatin1().constData());
+ }
+ }
+ else
{
- outputFile = QString("%1/%2 - Track %3 (%4).wav").arg(m_outputDir, m_baseName, QString().sprintf("%02d", m_trackNo.at(i)), QString::number(n));
+ m_decompressedFiles.insert(inputFileList.at(i), inputFileList.at(i));
}
- emit fileSelected(QFileInfo(outputFile).fileName());
- splitFile(outputFile, m_trackNo.at(i), m_trackFile.at(i), m_trackOffset.at(i), m_trackLength.at(i), m_trackMetaInfo.at(i));
+ if(m_abortFlag)
+ {
+ m_bAborted = true;
+ qWarning("The user has requested to abort the process!");
+ return;
+ }
}
- qDebug("All files were split.\n");
- m_bSuccess = true;
-}
+ int nFiles = m_model->getFileCount();
+ int nTracksTotal = 0, nTracksComplete = 0;
-void CueSplitter::addTrack(const int trackNo, const QString &file, const double offset, const double length, const AudioFileModel &metaInfo)
-{
-
- if(m_inputFiles.contains(file))
+ for(int i = 0; i < nFiles; i++)
{
- m_trackFile << file;
- m_trackNo << trackNo;
- m_trackOffset << offset;
- m_trackLength << length;
- m_trackMetaInfo << metaInfo;
+ nTracksTotal += m_model->getTrackCount(i);
}
- else
+
+ emit progressMaxChanged(10 * nTracksTotal);
+ emit progressValChanged(0);
+
+ const AudioFileModel_MetaInfo *albumInfo = m_model->getAlbumInfo();
+
+ //Now split all files
+ for(int i = 0; i < nFiles; i++)
{
- throw "Unknown input file!";
+ int nTracks = m_model->getTrackCount(i);
+ QString trackFile = m_model->getFileName(i);
+
+ //Process all tracks
+ for(int j = 0; j < nTracks; j++)
+ {
+ const AudioFileModel_MetaInfo *trackInfo = m_model->getTrackInfo(i, j);
+ const int trackNo = trackInfo->position();
+ double trackOffset = std::numeric_limits<double>::quiet_NaN();
+ double trackLength = std::numeric_limits<double>::quiet_NaN();
+ m_model->getTrackIndex(i, j, &trackOffset, &trackLength);
+
+ if((trackNo < 0) || _isnan(trackOffset) || _isnan(trackLength))
+ {
+ qWarning("Failed to fetch information for track #%d of file #%d!", j, i);
+ continue;
+ }
+
+ //Setup meta info
+ AudioFileModel_MetaInfo trackMetaInfo(*trackInfo);
+
+ //Apply album meta data on files
+ if(trackMetaInfo.title().trimmed().isEmpty())
+ {
+ trackMetaInfo.setTitle(QString().sprintf("Track %02d", trackNo));
+ }
+ trackMetaInfo.update(*albumInfo, false);
+
+ //Generate output file name
+ QString trackTitle = trackMetaInfo.title().isEmpty() ? QString().sprintf("Track %02d", trackNo) : trackMetaInfo.title();
+ QString outputFile = QString("%1/[%2] %3 - %4.wav").arg(m_outputDir, QString().sprintf("%02d", trackNo), MUtils::clean_file_name(m_baseName), MUtils::clean_file_name(trackTitle));
+ for(int n = 2; QFileInfo(outputFile).exists(); n++)
+ {
+ outputFile = QString("%1/[%2] %3 - %4 (%5).wav").arg(m_outputDir, QString().sprintf("%02d", trackNo), MUtils::clean_file_name(m_baseName), MUtils::clean_file_name(trackTitle), QString::number(n));
+ }
+
+ //Call split function
+ emit fileSelected(shortName(QFileInfo(outputFile).fileName()));
+ splitFile(outputFile, trackNo, trackFile, trackOffset, trackLength, trackMetaInfo, nTracksComplete);
+ emit progressValChanged(nTracksComplete += 10);
+
+ if(m_abortFlag)
+ {
+ m_bAborted = true;
+ qWarning("The user has requested to abort the process!");
+ return;
+ }
+ }
}
+
+ emit progressValChanged(10 * nTracksTotal);
+ MUtils::OS::sleep_ms(333);
+
+ qDebug("All files were split.\n");
+ m_bSuccess = true;
}
-void CueSplitter::setAlbumInfo(const QString &performer, const QString &title)
+////////////////////////////////////////////////////////////
+// Slots
+////////////////////////////////////////////////////////////
+
+void CueSplitter::handleUpdate(int progress)
{
- if(!performer.isEmpty()) m_albumPerformer = performer;
- if(!title.isEmpty()) m_albumTitle = title;
+ //QString("%1 [%2]").arg(m_activeFile, QString::number(progress)))
}
////////////////////////////////////////////////////////////
// Privtae Functions
////////////////////////////////////////////////////////////
-void CueSplitter::splitFile(const QString &output, const int trackNo, const QString &file, const double offset, const double length, const AudioFileModel &metaInfo)
+void CueSplitter::splitFile(const QString &output, const int trackNo, const QString &file, const double offset, const double length, const AudioFileModel_MetaInfo &metaInfo, const int baseProgress)
{
- qDebug("\n[Track %02d]", trackNo);
- qDebug("File: <%s>", file.toUtf8().constData());
- qDebug("Offset: %f", offset);
- qDebug("Length: %f", length);
- qDebug("Artist: <%s>", metaInfo.fileArtist().toUtf8().constData());
- qDebug("Title: <%s>", metaInfo.fileName().toUtf8().constData());
-
- QString baseName = QFileInfo(output).fileName();
-
- if(!m_inputFiles.contains(file))
- {
- qWarning("Unknown input file, skipping!");
- m_nTracksSkipped++;
- return;
- }
+ qDebug("[Track %02d]", trackNo);
+ qDebug("File: <%s>", MUTILS_UTF8(file));
+ qDebug("Offset: <%f> <%s>", offset, indexToString(offset).toLatin1().constData());
+ qDebug("Length: <%f> <%s>", length, indexToString(length).toLatin1().constData());
+ qDebug("Artist: <%s>", MUTILS_UTF8(metaInfo.artist()));
+ qDebug("Title: <%s>", MUTILS_UTF8(metaInfo.title()));
+ qDebug("Album: <%s>", MUTILS_UTF8(metaInfo.album()));
+
+ int prevProgress = baseProgress;
- AudioFileModel &inputFileInfo = m_inputFiles[file];
- if(inputFileInfo.formatContainerType().compare("Wave", Qt::CaseInsensitive) || inputFileInfo.formatAudioType().compare("PCM", Qt::CaseInsensitive))
+ if(!m_decompressedFiles.contains(file))
{
- qWarning("Sorry, only Wave/PCM files are supported at this time!");
+ qWarning("Unknown or unsupported input file, skipping!");
m_nTracksSkipped++;
return;
}
- AudioFileModel outFileInfo(metaInfo);
- outFileInfo.setFilePath(output);
- outFileInfo.setFormatContainerType(inputFileInfo.formatContainerType());
- outFileInfo.setFormatAudioType(inputFileInfo.formatAudioType());
+ QString baseName = shortName(QFileInfo(output).fileName());
+ QString decompressedInput = m_decompressedFiles[file];
+ qDebug("Input: <%s>", MUTILS_UTF8(decompressedInput));
- if(length != std::numeric_limits<double>::infinity())
- {
- outFileInfo.setFileDuration(static_cast<unsigned int>(abs(length)));
- }
- if(!m_albumTitle.isEmpty())
- {
- outFileInfo.setFileAlbum(m_albumTitle);
- }
- if(!m_albumPerformer.isEmpty() && outFileInfo.fileArtist().isEmpty())
- {
- outFileInfo.setFileArtist(m_albumPerformer);
- }
+ AudioFileModel outFileInfo(output);
+ outFileInfo.setMetaInfo(metaInfo);
+
+ AudioFileModel_TechInfo &outFileTechInfo = outFileInfo.techInfo();
+ outFileTechInfo.setContainerType("Wave");
+ outFileTechInfo.setAudioType("PCM");
+ outFileTechInfo.setDuration(static_cast<unsigned int>(abs(length)));
QStringList args;
args << "-S" << "-V3";
args << "--guard" << "--temp" << ".";
- args << QDir::toNativeSeparators(file);
+ args << QDir::toNativeSeparators(decompressedInput);
args << QDir::toNativeSeparators(output);
- if(offset != 0.0 || length != std::numeric_limits<double>::infinity())
+ //Add trim parameters, if needed
+ if(_finite(offset))
{
args << "trim";
args << indexToString(offset);
-
- if((length != std::numeric_limits<double>::quiet_NaN()) && (length != std::numeric_limits<double>::infinity()))
+
+ if(_finite(length))
{
args << indexToString(length);
}
QRegExp rxDuration("Duration\\s*:\\s*(\\d\\d):(\\d\\d):(\\d\\d).(\\d\\d)", Qt::CaseInsensitive);
QProcess process;
- process.setProcessChannelMode(QProcess::MergedChannels);
- process.setReadChannel(QProcess::StandardOutput);
- process.setWorkingDirectory(m_outputDir);
+ MUtils::init_process(process, m_outputDir);
+
process.start(m_soxBin, args);
if(!process.waitForStarted())
if(m_abortFlag)
{
process.kill();
+ qWarning("Process was aborted on user request!");
break;
}
- process.waitForReadyRead();
+ process.waitForReadyRead(m_processTimeoutInterval);
if(!process.bytesAvailable() && process.state() == QProcess::Running)
{
process.kill();
int progress = rxProgress.cap(1).toInt(&ok);
if(ok)
{
- emit fileSelected(QString("%1 [%2%]").arg(baseName, QString::number(progress)));
+ const int newProgress = baseProgress + qRound(static_cast<double>(qBound(0, progress, 100)) / 10.0);
+ if(newProgress > prevProgress)
+ {
+ emit progressValChanged(newProgress);
+ prevProgress = newProgress;
+ }
}
}
else if(rxChannels.lastIndexIn(text) >= 0)
{
bool ok = false;
unsigned int channels = rxChannels.cap(1).toUInt(&ok);
- if(ok) outFileInfo.setFormatAudioChannels(channels);
+ if(ok) outFileInfo.techInfo().setAudioChannels(channels);
}
else if(rxSamplerate.lastIndexIn(text) >= 0)
{
bool ok = false;
unsigned int samplerate = rxSamplerate.cap(1).toUInt(&ok);
- if(ok) outFileInfo.setFormatAudioSamplerate(samplerate);
+ if(ok) outFileInfo.techInfo().setAudioSamplerate(samplerate);
}
else if(rxPrecision.lastIndexIn(text) >= 0)
{
bool ok = false;
unsigned int precision = rxPrecision.cap(1).toUInt(&ok);
- if(ok) outFileInfo.setFormatAudioBitdepth(precision);
+ if(ok) outFileInfo.techInfo().setAudioBitdepth(precision);
}
else if(rxDuration.lastIndexIn(text) >= 0)
{
if(length == std::numeric_limits<double>::infinity())
{
qDebug("Duration updated from SoX info!");
- outFileInfo.setFileDuration(max(0, intputLen - static_cast<unsigned int>(floor(offset + 0.5))));
+ int duration = intputLen - static_cast<int>(floor(offset + 0.5));
+ if(duration < 0) qWarning("Track is out of bounds: Track offset exceeds input file duration!");
+ outFileInfo.techInfo().setDuration(qMax(0, duration));
}
else
{
process.waitForFinished(-1);
}
- if(process.exitStatus() != QProcess::NormalExit || QFileInfo(output).size() == 0)
+ if(process.exitCode() != EXIT_SUCCESS || QFileInfo(output).size() == 0)
{
qWarning("Splitting has failed !!!");
m_nTracksSkipped++;
QString CueSplitter::indexToString(const double index) const
{
- if(index == std::numeric_limits<double>::quiet_NaN() || index == std::numeric_limits<double>::infinity() || index < 0.0)
+ if(!_finite(index) || (index < 0.0) || (index > 86400.0))
{
return QString();
}
+
+ QTime time = QTime().addMSecs(static_cast<int>(floor(0.5 + (index * 1000.0))));
+ return time.toString(time.hour() ? "H:mm:ss.zzz" : "m:ss.zzz");
+}
- int temp = static_cast<int>(floor(0.5 + (index * 1000.0)));
-
- int msec = temp % 1000;
- int secs = temp / 1000;
-
- if(secs >= 3600)
- {
- return QString().sprintf("%d:%02d:%02d.%03d", min(99, secs / 3600), min(59, (secs % 3600) / 60), min(59, (secs % 3600) % 60), min(99, msec));
- }
- else if(secs >= 60)
- {
- return QString().sprintf("%d:%02d.%03d", min(99, secs / 60), min(59, secs % 60), min(99, msec));
- }
- else
+QString CueSplitter::shortName(const QString &longName) const
+{
+ static const int maxLen = 54;
+
+ if(longName.length() > maxLen)
{
- return QString().sprintf("%d.%03d", min(59, secs % 60), min(99, msec));
+ return QString("%1...%2").arg(longName.left(maxLen/2).trimmed(), longName.right(maxLen/2).trimmed());
}
+
+ return longName;
}
////////////////////////////////////////////////////////////