1 ///////////////////////////////////////////////////////////////////////////////
2 // LameXP - Audio Encoder Front-End
3 // Copyright (C) 2004-2017 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, 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 #include "Thread_CueSplitter.h"
27 #include "LockedFile.h"
28 #include "Model_AudioFile.h"
29 #include "Model_CueSheet.h"
30 #include "Registry_Decoder.h"
31 #include "Decoder_Abstract.h"
34 #include <MUtils/Global.h>
35 #include <MUtils/OSSupport.h>
50 ////////////////////////////////////////////////////////////
52 ////////////////////////////////////////////////////////////
54 CueSplitter::CueSplitter(const QString
&outputDir
, const QString
&baseName
, CueSheetModel
*model
, const QList
<AudioFileModel
> &inputFilesInfo
)
57 m_outputDir(outputDir
),
59 m_soxBin(lamexp_tools_lookup("sox.exe"))
61 if(m_soxBin
.isEmpty())
63 qFatal("Invalid path to SoX binary. Tool not initialized properly.");
66 m_decompressedFiles
.clear();
69 qDebug("\n[CueSplitter]");
71 int nInputFiles
= inputFilesInfo
.count();
72 for(int i
= 0; i
< nInputFiles
; i
++)
74 m_inputFilesInfo
.insert(inputFilesInfo
[i
].filePath(), inputFilesInfo
[i
]);
75 qDebug("File %02d: <%s>", i
, MUTILS_UTF8(inputFilesInfo
[i
].filePath()));
78 qDebug("All input files added.");
82 CueSplitter::~CueSplitter(void)
84 while(!m_tempFiles
.isEmpty())
86 MUtils::remove_file(m_tempFiles
.takeFirst());
90 ////////////////////////////////////////////////////////////
92 ////////////////////////////////////////////////////////////
94 void CueSplitter::run()
100 m_decompressedFiles
.clear();
101 m_activeFile
.clear();
103 if(!QDir(m_outputDir
).exists())
105 qWarning("Output directory \"%s\" does not exist!", MUTILS_UTF8(m_outputDir
));
109 QStringList inputFileList
= m_inputFilesInfo
.keys();
110 int nInputFiles
= inputFileList
.count();
112 emit
progressMaxChanged(nInputFiles
);
113 emit
progressValChanged(0);
115 //Decompress all input files
116 for(int i
= 0; i
< nInputFiles
; i
++)
118 const AudioFileModel_TechInfo
&inputFileInfo
= m_inputFilesInfo
[inputFileList
.at(i
)].techInfo();
119 if(inputFileInfo
.containerType().compare("Wave", Qt::CaseInsensitive
) || inputFileInfo
.audioType().compare("PCM", Qt::CaseInsensitive
))
121 AbstractDecoder
*decoder
= DecoderRegistry::lookup(inputFileInfo
.containerType(), inputFileInfo
.containerProfile(), inputFileInfo
.audioType(), inputFileInfo
.audioProfile(), inputFileInfo
.audioVersion());
124 m_activeFile
= shortName(QFileInfo(inputFileList
.at(i
)).fileName());
126 emit
fileSelected(m_activeFile
);
127 emit
progressValChanged(i
+1);
129 QString tempFile
= QString("%1/~%2.wav").arg(m_outputDir
, MUtils::next_rand_str());
130 connect(decoder
, SIGNAL(statusUpdated(int)), this, SLOT(handleUpdate(int)), Qt::DirectConnection
);
132 if(decoder
->decode(inputFileList
.at(i
), tempFile
, m_abortFlag
))
134 m_decompressedFiles
.insert(inputFileList
.at(i
), tempFile
);
135 m_tempFiles
.append(tempFile
);
139 qWarning("Failed to decompress file: <%s>", inputFileList
.at(i
).toLatin1().constData());
140 MUtils::remove_file(tempFile
);
143 m_activeFile
.clear();
144 MUTILS_DELETE(decoder
);
148 qWarning("Unsupported input file: <%s>", inputFileList
.at(i
).toLatin1().constData());
153 m_decompressedFiles
.insert(inputFileList
.at(i
), inputFileList
.at(i
));
156 if(MUTILS_BOOLIFY(m_abortFlag
))
159 qWarning("The user has requested to abort the process!");
164 int nFiles
= m_model
->getFileCount();
165 int nTracksTotal
= 0, nTracksComplete
= 0;
167 for(int i
= 0; i
< nFiles
; i
++)
169 nTracksTotal
+= m_model
->getTrackCount(i
);
172 emit
progressMaxChanged(10 * nTracksTotal
);
173 emit
progressValChanged(0);
175 const AudioFileModel_MetaInfo
*albumInfo
= m_model
->getAlbumInfo();
177 //Now split all files
178 for(int i
= 0; i
< nFiles
; i
++)
180 int nTracks
= m_model
->getTrackCount(i
);
181 QString trackFile
= m_model
->getFileName(i
);
184 for(int j
= 0; j
< nTracks
; j
++)
186 const AudioFileModel_MetaInfo
*trackInfo
= m_model
->getTrackInfo(i
, j
);
187 const int trackNo
= trackInfo
->position();
188 double trackOffset
= std::numeric_limits
<double>::quiet_NaN();
189 double trackLength
= std::numeric_limits
<double>::quiet_NaN();
190 m_model
->getTrackIndex(i
, j
, &trackOffset
, &trackLength
);
192 if((trackNo
< 0) || _isnan(trackOffset
) || _isnan(trackLength
))
194 qWarning("Failed to fetch information for track #%d of file #%d!", j
, i
);
199 AudioFileModel_MetaInfo
trackMetaInfo(*trackInfo
);
201 //Apply album meta data on files
202 if(trackMetaInfo
.title().trimmed().isEmpty())
204 trackMetaInfo
.setTitle(QString().sprintf("Track %02d", trackNo
));
206 trackMetaInfo
.update(*albumInfo
, false);
208 //Generate output file name
209 QString trackTitle
= trackMetaInfo
.title().isEmpty() ? QString().sprintf("Track %02d", trackNo
) : trackMetaInfo
.title();
210 QString outputFile
= QString("%1/[%2] %3 - %4.wav").arg(m_outputDir
, QString().sprintf("%02d", trackNo
), MUtils::clean_file_name(m_baseName
, true), MUtils::clean_file_name(trackTitle
, true));
211 for(int n
= 2; QFileInfo(outputFile
).exists(); n
++)
213 outputFile
= QString("%1/[%2] %3 - %4 (%5).wav").arg(m_outputDir
, QString().sprintf("%02d", trackNo
), MUtils::clean_file_name(m_baseName
, true), MUtils::clean_file_name(trackTitle
, true), QString::number(n
));
216 //Call split function
217 emit
fileSelected(shortName(QFileInfo(outputFile
).fileName()));
218 splitFile(outputFile
, trackNo
, trackFile
, trackOffset
, trackLength
, trackMetaInfo
, nTracksComplete
);
219 emit
progressValChanged(nTracksComplete
+= 10);
221 if(MUTILS_BOOLIFY(m_abortFlag
))
224 qWarning("The user has requested to abort the process!");
230 emit
progressValChanged(10 * nTracksTotal
);
231 MUtils::OS::sleep_ms(333);
233 qDebug("All files were split.\n");
237 ////////////////////////////////////////////////////////////
239 ////////////////////////////////////////////////////////////
241 void CueSplitter::handleUpdate(int progress
)
243 //QString("%1 [%2]").arg(m_activeFile, QString::number(progress)))
246 ////////////////////////////////////////////////////////////
248 ////////////////////////////////////////////////////////////
250 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
)
252 qDebug("[Track %02d]", trackNo
);
253 qDebug("File: <%s>", MUTILS_UTF8(file
));
254 qDebug("Offset: <%f> <%s>", offset
, indexToString(offset
).toLatin1().constData());
255 qDebug("Length: <%f> <%s>", length
, indexToString(length
).toLatin1().constData());
256 qDebug("Artist: <%s>", MUTILS_UTF8(metaInfo
.artist()));
257 qDebug("Title: <%s>", MUTILS_UTF8(metaInfo
.title()));
258 qDebug("Album: <%s>", MUTILS_UTF8(metaInfo
.album()));
260 int prevProgress
= baseProgress
;
262 if(!m_decompressedFiles
.contains(file
))
264 qWarning("Unknown or unsupported input file, skipping!");
269 QString baseName
= shortName(QFileInfo(output
).fileName());
270 QString decompressedInput
= m_decompressedFiles
[file
];
271 qDebug("Input: <%s>", MUTILS_UTF8(decompressedInput
));
273 AudioFileModel
outFileInfo(output
);
274 outFileInfo
.setMetaInfo(metaInfo
);
276 AudioFileModel_TechInfo
&outFileTechInfo
= outFileInfo
.techInfo();
277 outFileTechInfo
.setContainerType("Wave");
278 outFileTechInfo
.setAudioType("PCM");
279 outFileTechInfo
.setDuration(static_cast<unsigned int>(abs(length
)));
282 args
<< "-S" << "-V3";
283 args
<< "--guard" << "--temp" << ".";
284 args
<< QDir::toNativeSeparators(decompressedInput
);
285 args
<< QDir::toNativeSeparators(output
);
287 //Add trim parameters, if needed
291 args
<< indexToString(offset
);
295 args
<< indexToString(length
);
299 QRegExp
rxProgress("In:(\\d+)(\\.\\d+)*%", Qt::CaseInsensitive
);
300 QRegExp
rxChannels("Channels\\s*:\\s*(\\d+)", Qt::CaseInsensitive
);
301 QRegExp
rxSamplerate("Sample Rate\\s*:\\s*(\\d+)", Qt::CaseInsensitive
);
302 QRegExp
rxPrecision("Precision\\s*:\\s*(\\d+)-bit", Qt::CaseInsensitive
);
303 QRegExp
rxDuration("Duration\\s*:\\s*(\\d\\d):(\\d\\d):(\\d\\d).(\\d\\d)", Qt::CaseInsensitive
);
306 MUtils::init_process(process
, m_outputDir
);
308 process
.start(m_soxBin
, args
);
310 if(!process
.waitForStarted())
312 qWarning("SoX process failed to create!");
313 qWarning("Error message: \"%s\"\n", process
.errorString().toLatin1().constData());
315 process
.waitForFinished(-1);
320 while(process
.state() != QProcess::NotRunning
)
322 if(MUTILS_BOOLIFY(m_abortFlag
))
325 qWarning("Process was aborted on user request!");
328 process
.waitForReadyRead(m_processTimeoutInterval
);
329 if(!process
.bytesAvailable() && process
.state() == QProcess::Running
)
332 qWarning("SoX process timed out <-- killing!");
335 while(process
.bytesAvailable() > 0)
337 QByteArray line
= process
.readLine();
338 QString text
= QString::fromUtf8(line
.constData()).simplified();
339 if(rxProgress
.lastIndexIn(text
) >= 0)
342 int progress
= rxProgress
.cap(1).toInt(&ok
);
345 const int newProgress
= baseProgress
+ qRound(static_cast<double>(qBound(0, progress
, 100)) / 10.0);
346 if(newProgress
> prevProgress
)
348 emit
progressValChanged(newProgress
);
349 prevProgress
= newProgress
;
353 else if(rxChannels
.lastIndexIn(text
) >= 0)
356 unsigned int channels
= rxChannels
.cap(1).toUInt(&ok
);
357 if(ok
) outFileInfo
.techInfo().setAudioChannels(channels
);
359 else if(rxSamplerate
.lastIndexIn(text
) >= 0)
362 unsigned int samplerate
= rxSamplerate
.cap(1).toUInt(&ok
);
363 if(ok
) outFileInfo
.techInfo().setAudioSamplerate(samplerate
);
365 else if(rxPrecision
.lastIndexIn(text
) >= 0)
368 unsigned int precision
= rxPrecision
.cap(1).toUInt(&ok
);
369 if(ok
) outFileInfo
.techInfo().setAudioBitdepth(precision
);
371 else if(rxDuration
.lastIndexIn(text
) >= 0)
373 bool ok1
= false, ok2
= false, ok3
= false;
374 unsigned int hh
= rxDuration
.cap(1).toUInt(&ok1
);
375 unsigned int mm
= rxDuration
.cap(2).toUInt(&ok2
);
376 unsigned int ss
= rxDuration
.cap(3).toUInt(&ok3
);
377 if(ok1
&& ok2
&& ok3
)
379 unsigned intputLen
= (hh
* 3600) + (mm
* 60) + ss
;
380 if(length
== std::numeric_limits
<double>::infinity())
382 qDebug("Duration updated from SoX info!");
383 int duration
= intputLen
- static_cast<int>(floor(offset
+ 0.5));
384 if(duration
< 0) qWarning("Track is out of bounds: Track offset exceeds input file duration!");
385 outFileInfo
.techInfo().setDuration(qMax(0, duration
));
389 unsigned int trackEnd
= static_cast<unsigned int>(floor(offset
+ 0.5)) + static_cast<unsigned int>(floor(length
+ 0.5));
390 if(trackEnd
> intputLen
) qWarning("Track is out of bounds: End of track exceeds input file duration!");
397 process
.waitForFinished();
398 if(process
.state() != QProcess::NotRunning
)
401 process
.waitForFinished(-1);
404 if(process
.exitCode() != EXIT_SUCCESS
|| QFileInfo(output
).size() == 0)
406 qWarning("Splitting has failed !!!");
411 emit
fileSplit(outFileInfo
);
415 QString
CueSplitter::indexToString(const double index
) const
417 if(!_finite(index
) || (index
< 0.0) || (index
> 86400.0))
422 QTime time
= QTime().addMSecs(static_cast<int>(floor(0.5 + (index
* 1000.0))));
423 return time
.toString(time
.hour() ? "H:mm:ss.zzz" : "m:ss.zzz");
426 QString
CueSplitter::shortName(const QString
&longName
) const
428 static const int maxLen
= 54;
430 if(longName
.length() > maxLen
)
432 return QString("%1...%2").arg(longName
.left(maxLen
/2).trimmed(), longName
.right(maxLen
/2).trimmed());
438 ////////////////////////////////////////////////////////////
440 ////////////////////////////////////////////////////////////