Bump version.
[LameXP.git] / src / Thread_CueSplitter.cpp
blobec2bcd12a4763e5b86816d18a4e088c62f36311b
1 ///////////////////////////////////////////////////////////////////////////////
2 // LameXP - Audio Encoder Front-End
3 // Copyright (C) 2004-2021 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; always including the non-optional
9 // LAMEXP GNU GENERAL PUBLIC LICENSE ADDENDUM. See "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"
25 //Internal
26 #include "Global.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"
33 //MUtils
34 #include <MUtils/Global.h>
35 #include <MUtils/OSSupport.h>
37 //Qt
38 #include <QDir>
39 #include <QFileInfo>
40 #include <QProcess>
41 #include <QDate>
42 #include <QTime>
43 #include <QDebug>
45 //CRT
46 #include <math.h>
47 #include <float.h>
48 #include <limits>
50 ////////////////////////////////////////////////////////////
51 // Constructor
52 ////////////////////////////////////////////////////////////
54 CueSplitter::CueSplitter(const QString &outputDir, const QString &baseName, CueSheetModel *model, const QList<AudioFileModel> &inputFilesInfo)
56 m_model(model),
57 m_outputDir(outputDir),
58 m_baseName(baseName),
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();
67 m_tempFiles.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.");
79 m_bSuccess = false;
82 CueSplitter::~CueSplitter(void)
84 while(!m_tempFiles.isEmpty())
86 MUtils::remove_file(m_tempFiles.takeFirst());
90 ////////////////////////////////////////////////////////////
91 // Thread Main
92 ////////////////////////////////////////////////////////////
94 void CueSplitter::run()
96 m_bSuccess = false;
97 m_bAborted = false;
98 m_nTracksSuccess = 0;
99 m_nTracksSkipped = 0;
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));
106 return;
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());
122 if(decoder)
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);
137 else
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);
146 else
148 qWarning("Unsupported input file: <%s>", inputFileList.at(i).toLatin1().constData());
151 else
153 m_decompressedFiles.insert(inputFileList.at(i), inputFileList.at(i));
156 if(MUTILS_BOOLIFY(m_abortFlag))
158 m_bAborted = true;
159 qWarning("The user has requested to abort the process!");
160 return;
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);
183 //Process all tracks
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);
195 continue;
198 //Setup meta info
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))
223 m_bAborted = true;
224 qWarning("The user has requested to abort the process!");
225 return;
230 emit progressValChanged(10 * nTracksTotal);
231 MUtils::OS::sleep_ms(333);
233 qDebug("All files were split.\n");
234 m_bSuccess = true;
237 ////////////////////////////////////////////////////////////
238 // Slots
239 ////////////////////////////////////////////////////////////
241 void CueSplitter::handleUpdate(int progress)
243 //QString("%1 [%2]").arg(m_activeFile, QString::number(progress)))
246 ////////////////////////////////////////////////////////////
247 // Privtae Functions
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!");
265 m_nTracksSkipped++;
266 return;
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)));
281 QStringList args;
282 args << "-S" << "-V3";
283 args << "--guard" << "--temp" << ".";
284 args << QDir::toNativeSeparators(decompressedInput);
285 args << QDir::toNativeSeparators(output);
287 //Add trim parameters, if needed
288 if(_finite(offset))
290 args << "trim";
291 args << indexToString(offset);
293 if(_finite(length))
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);
305 QProcess process;
306 MUtils::init_process(process, m_outputDir);
308 qDebug("SoX args: \"%s\"", MUTILS_UTF8(args.join(QLatin1String("\", \""))));
309 process.start(m_soxBin, args);
311 if(!process.waitForStarted())
313 qWarning("SoX process failed to create!");
314 qWarning("Error message: \"%s\"\n", process.errorString().toLatin1().constData());
315 process.kill();
316 process.waitForFinished(-1);
317 m_nTracksSkipped++;
318 return;
321 while(process.state() != QProcess::NotRunning)
323 if(MUTILS_BOOLIFY(m_abortFlag))
325 process.kill();
326 qWarning("Process was aborted on user request!");
327 break;
329 process.waitForReadyRead(m_processTimeoutInterval);
330 if(!process.bytesAvailable() && process.state() == QProcess::Running)
332 process.kill();
333 qWarning("SoX process timed out <-- killing!");
334 break;
336 while(process.bytesAvailable() > 0)
338 QByteArray line = process.readLine();
339 if (line.size() > 0)
341 QString text = QString::fromUtf8(line.constData()).simplified();
342 if (!text.isEmpty())
344 if (rxProgress.lastIndexIn(text) >= 0)
346 int progress;
347 if (MUtils::regexp_parse_int32(rxProgress, progress))
349 const int newProgress = baseProgress + qRound(static_cast<double>(qBound(0, progress, 100)) / 10.0);
350 if (newProgress > prevProgress)
352 emit progressValChanged(newProgress);
353 prevProgress = newProgress;
357 else if (rxChannels.lastIndexIn(text) >= 0)
359 unsigned int channels;
360 if (MUtils::regexp_parse_uint32(rxChannels, channels))
362 outFileInfo.techInfo().setAudioChannels(channels);
365 else if (rxSamplerate.lastIndexIn(text) >= 0)
367 unsigned int samplerate;
368 if (MUtils::regexp_parse_uint32(rxSamplerate, samplerate))
370 outFileInfo.techInfo().setAudioSamplerate(samplerate);
373 else if (rxPrecision.lastIndexIn(text) >= 0)
375 unsigned int precision;
376 if (MUtils::regexp_parse_uint32(rxPrecision, precision))
378 outFileInfo.techInfo().setAudioBitdepth(precision);
381 else if (rxDuration.lastIndexIn(text) >= 0)
383 unsigned int duration[3U];
384 if (MUtils::regexp_parse_uint32(rxDuration, duration, 3U))
386 unsigned intputLen = (duration[0U] * 3600) + (duration[1U] * 60) + duration[2U];
387 if (length == std::numeric_limits<double>::infinity())
389 qDebug("Duration updated from SoX info!");
390 int duration = intputLen - static_cast<int>(floor(offset + 0.5));
391 if (duration < 0)
393 qWarning("Track is out of bounds: Track offset exceeds input file duration!");
395 outFileInfo.techInfo().setDuration(qMax(0, duration));
397 else
399 unsigned int trackEnd = static_cast<unsigned int>(floor(offset + 0.5)) + static_cast<unsigned int>(floor(length + 0.5));
400 if (trackEnd > intputLen) qWarning("Track is out of bounds: End of track exceeds input file duration!");
404 else
406 qDebug("SoX line: %s", MUTILS_UTF8(text));
413 process.waitForFinished();
414 if(process.state() != QProcess::NotRunning)
416 process.kill();
417 process.waitForFinished(-1);
420 qDebug("SoX exit code: %d", process.exitCode());
421 if(process.exitCode() != EXIT_SUCCESS || QFileInfo(output).size() == 0)
423 qWarning("Splitting has failed !!!");
424 m_nTracksSkipped++;
425 return;
428 emit fileSplit(outFileInfo);
429 m_nTracksSuccess++;
432 QString CueSplitter::indexToString(const double index) const
434 if(!_finite(index) || (index < 0.0) || (index > 86400.0))
436 return QString();
439 QTime time = QTime().addMSecs(static_cast<int>(floor(0.5 + (index * 1000.0))));
440 return time.toString(time.hour() ? "H:mm:ss.zzz" : "m:ss.zzz");
443 QString CueSplitter::shortName(const QString &longName) const
445 static const int maxLen = 54;
447 if(longName.length() > maxLen)
449 return QString("%1...%2").arg(longName.left(maxLen/2).trimmed(), longName.right(maxLen/2).trimmed());
452 return longName;
455 ////////////////////////////////////////////////////////////
456 // EVENTS
457 ////////////////////////////////////////////////////////////
459 /*NONE*/