1 ///////////////////////////////////////////////////////////////////////////////
2 // LameXP - Audio Encoder Front-End
3 // Copyright (C) 2004-2013 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.
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 "Thread_CueSplitter.h"
25 #include "LockedFile.h"
26 #include "Model_AudioFile.h"
27 #include "Model_CueSheet.h"
28 #include "Registry_Decoder.h"
29 #include "Decoder_Abstract.h"
42 ////////////////////////////////////////////////////////////
44 ////////////////////////////////////////////////////////////
46 CueSplitter::CueSplitter(const QString
&outputDir
, const QString
&baseName
, CueSheetModel
*model
, const QList
<AudioFileModel
> &inputFilesInfo
)
49 m_outputDir(outputDir
),
51 m_soxBin(lamexp_lookup_tool("sox.exe"))
53 if(m_soxBin
.isEmpty())
55 qFatal("Invalid path to SoX binary. Tool not initialized properly.");
58 m_decompressedFiles
.clear();
61 qDebug("\n[CueSplitter]");
63 int nInputFiles
= inputFilesInfo
.count();
64 for(int i
= 0; i
< nInputFiles
; i
++)
66 m_inputFilesInfo
.insert(inputFilesInfo
[i
].filePath(), inputFilesInfo
[i
]);
67 qDebug("File %02d: <%s>", i
, inputFilesInfo
[i
].filePath().toUtf8().constData());
70 qDebug("All input files added.");
74 CueSplitter::~CueSplitter(void)
76 while(!m_tempFiles
.isEmpty())
78 lamexp_remove_file(m_tempFiles
.takeFirst());
82 ////////////////////////////////////////////////////////////
84 ////////////////////////////////////////////////////////////
86 void CueSplitter::run()
93 m_decompressedFiles
.clear();
96 if(!QDir(m_outputDir
).exists())
98 qWarning("Output directory \"%s\" does not exist!", m_outputDir
.toUtf8().constData());
102 QStringList inputFileList
= m_inputFilesInfo
.keys();
103 int nInputFiles
= inputFileList
.count();
105 emit
progressMaxChanged(nInputFiles
);
106 emit
progressValChanged(0);
108 //Decompress all input files
109 for(int i
= 0; i
< nInputFiles
; i
++)
111 AudioFileModel
&inputFileInfo
= m_inputFilesInfo
[inputFileList
.at(i
)];
112 if(inputFileInfo
.formatContainerType().compare("Wave", Qt::CaseInsensitive
) || inputFileInfo
.formatAudioType().compare("PCM", Qt::CaseInsensitive
))
114 AbstractDecoder
*decoder
= DecoderRegistry::lookup(inputFileInfo
.formatContainerType(), inputFileInfo
.formatContainerProfile(), inputFileInfo
.formatAudioType(), inputFileInfo
.formatAudioProfile(), inputFileInfo
.formatAudioVersion());
117 m_activeFile
= shortName(QFileInfo(inputFileList
.at(i
)).fileName());
119 emit
fileSelected(m_activeFile
);
120 emit
progressValChanged(i
+1);
122 QString tempFile
= QString("%1/~%2.wav").arg(m_outputDir
, lamexp_rand_str());
123 connect(decoder
, SIGNAL(statusUpdated(int)), this, SLOT(handleUpdate(int)), Qt::DirectConnection
);
125 if(decoder
->decode(inputFileList
.at(i
), tempFile
, &m_abortFlag
))
127 m_decompressedFiles
.insert(inputFileList
.at(i
), tempFile
);
128 m_tempFiles
.append(tempFile
);
132 qWarning("Failed to decompress file: <%s>", inputFileList
.at(i
).toLatin1().constData());
133 lamexp_remove_file(tempFile
);
136 m_activeFile
.clear();
137 LAMEXP_DELETE(decoder
);
141 qWarning("Unsupported input file: <%s>", inputFileList
.at(i
).toLatin1().constData());
146 m_decompressedFiles
.insert(inputFileList
.at(i
), inputFileList
.at(i
));
152 qWarning("The user has requested to abort the process!");
157 int nFiles
= m_model
->getFileCount();
158 int nTracksTotal
= 0, nTracksComplete
= 0;
160 for(int i
= 0; i
< nFiles
; i
++)
162 nTracksTotal
+= m_model
->getTrackCount(i
);
165 emit
progressMaxChanged(nTracksTotal
);
166 emit
progressValChanged(0);
168 QString albumPerformer
= m_model
->getAlbumPerformer();
169 QString albumTitle
= m_model
->getAlbumTitle();
170 QString albumGenre
= m_model
->getAlbumGenre();
171 unsigned int albumYear
= m_model
->getAlbumYear();
173 //Now split all files
174 for(int i
= 0; i
< nFiles
; i
++)
176 int nTracks
= m_model
->getTrackCount(i
);
177 QString trackFile
= m_model
->getFileName(i
);
181 for(int j
= 0; j
< nTracks
; j
++)
183 emit
progressValChanged(++nTracksComplete
);
184 int trackNo
= m_model
->getTrackNo(i
, j
);
185 double trackOffset
= std::numeric_limits
<double>::quiet_NaN();
186 double trackLength
= std::numeric_limits
<double>::quiet_NaN();
187 m_model
->getTrackIndex(i
, j
, &trackOffset
, &trackLength
);
189 if((trackNo
< 0) || _isnan(trackOffset
) || _isnan(trackLength
))
191 qWarning("Failed to fetch information for track #%d of file #%d!", j
, i
);
196 AudioFileModel
trackMetaInfo(QString().sprintf("cue://File%02d/Track%02d", i
, j
));
197 trackMetaInfo
.setFileName(m_model
->getTrackTitle(i
, j
));
198 trackMetaInfo
.setFileArtist(m_model
->getTrackPerformer(i
, j
));
199 trackMetaInfo
.setFileGenre(m_model
->getTrackGenre(i
, j
));
200 trackMetaInfo
.setFileYear(m_model
->getTrackYear(i
, j
));
201 trackMetaInfo
.setFilePosition(trackNo
);
203 //Apply album meta data on files
204 if(trackMetaInfo
.fileName().trimmed().isEmpty())
206 trackMetaInfo
.setFileName(QString().sprintf("Track %02d", trackNo
));
208 if(!albumTitle
.isEmpty())
210 trackMetaInfo
.setFileAlbum(albumTitle
);
212 if(!albumPerformer
.isEmpty() && trackMetaInfo
.fileArtist().isEmpty())
214 trackMetaInfo
.setFileArtist(albumPerformer
);
216 if(!albumGenre
.isEmpty() && trackMetaInfo
.fileGenre().isEmpty())
218 trackMetaInfo
.setFileGenre(albumGenre
);
220 if((albumYear
> 0) && (trackMetaInfo
.fileYear() == 0))
222 trackMetaInfo
.setFileYear(albumYear
);
224 if(_finite(trackLength
))
226 trackMetaInfo
.setFileDuration(static_cast<unsigned int>(abs(trackLength
)));
229 //Generate output file name
230 QString trackTitle
= trackMetaInfo
.fileName().isEmpty() ? QString().sprintf("Track %02d", trackNo
) : trackMetaInfo
.fileName();
231 QString outputFile
= QString("%1/[%2] %3 - %4.wav").arg(m_outputDir
, QString().sprintf("%02d", trackNo
), lamexp_clean_filename(m_baseName
), lamexp_clean_filename(trackTitle
));
232 for(int n
= 2; QFileInfo(outputFile
).exists(); n
++)
234 outputFile
= QString("%1/[%2] %3 - %4 (%5).wav").arg(m_outputDir
, QString().sprintf("%02d", trackNo
), lamexp_clean_filename(m_baseName
), lamexp_clean_filename(trackTitle
), QString::number(n
));
237 //Call split function
238 emit
fileSelected(shortName(QFileInfo(outputFile
).fileName()));
239 splitFile(outputFile
, trackNo
, trackFile
, trackOffset
, trackLength
, trackMetaInfo
, maxProgress
);
244 qWarning("The user has requested to abort the process!");
250 qDebug("All files were split.\n");
254 ////////////////////////////////////////////////////////////
256 ////////////////////////////////////////////////////////////
258 void CueSplitter::handleUpdate(int progress
)
260 emit
fileSelected(QString("%1 [%2%]").arg(m_activeFile
, QString::number(progress
)));
263 ////////////////////////////////////////////////////////////
265 ////////////////////////////////////////////////////////////
267 void CueSplitter::splitFile(const QString
&output
, const int trackNo
, const QString
&file
, const double offset
, const double length
, const AudioFileModel
&metaInfo
, int &maxProgress
)
269 qDebug("[Track %02d]", trackNo
);
270 qDebug("File: <%s>", file
.toUtf8().constData());
271 qDebug("Offset: <%f> <%s>", offset
, indexToString(offset
).toLatin1().constData());
272 qDebug("Length: <%f> <%s>", length
, indexToString(length
).toLatin1().constData());
273 qDebug("Artist: <%s>", metaInfo
.fileArtist().toUtf8().constData());
274 qDebug("Title: <%s>", metaInfo
.fileName().toUtf8().constData());
276 if(!m_decompressedFiles
.contains(file
))
278 qWarning("Unknown or unsupported input file, skipping!");
283 QString baseName
= shortName(QFileInfo(output
).fileName());
284 QString decompressedInput
= m_decompressedFiles
[file
];
285 qDebug("Input: <%s>", decompressedInput
.toUtf8().constData());
287 emit
fileSelected(QString("%1 [%2%]").arg(baseName
, QString::number(maxProgress
)));
289 AudioFileModel
outFileInfo(metaInfo
);
290 outFileInfo
.setFilePath(output
);
291 outFileInfo
.setFormatContainerType("Wave");
292 outFileInfo
.setFormatAudioType("PCM");
295 args
<< "-S" << "-V3";
296 args
<< "--guard" << "--temp" << ".";
297 args
<< QDir::toNativeSeparators(decompressedInput
);
298 args
<< QDir::toNativeSeparators(output
);
300 //Add trim parameters, if needed
304 args
<< indexToString(offset
);
308 args
<< indexToString(length
);
312 QRegExp
rxProgress("In:(\\d+)(\\.\\d+)*%", Qt::CaseInsensitive
);
313 QRegExp
rxChannels("Channels\\s*:\\s*(\\d+)", Qt::CaseInsensitive
);
314 QRegExp
rxSamplerate("Sample Rate\\s*:\\s*(\\d+)", Qt::CaseInsensitive
);
315 QRegExp
rxPrecision("Precision\\s*:\\s*(\\d+)-bit", Qt::CaseInsensitive
);
316 QRegExp
rxDuration("Duration\\s*:\\s*(\\d\\d):(\\d\\d):(\\d\\d).(\\d\\d)", Qt::CaseInsensitive
);
319 process
.setProcessChannelMode(QProcess::MergedChannels
);
320 process
.setReadChannel(QProcess::StandardOutput
);
321 process
.setWorkingDirectory(m_outputDir
);
322 process
.start(m_soxBin
, args
);
324 if(!process
.waitForStarted())
326 qWarning("SoX process failed to create!");
327 qWarning("Error message: \"%s\"\n", process
.errorString().toLatin1().constData());
329 process
.waitForFinished(-1);
334 while(process
.state() != QProcess::NotRunning
)
339 qWarning("Process was aborted on user request!");
342 process
.waitForReadyRead(m_processTimeoutInterval
);
343 if(!process
.bytesAvailable() && process
.state() == QProcess::Running
)
346 qWarning("SoX process timed out <-- killing!");
349 while(process
.bytesAvailable() > 0)
351 QByteArray line
= process
.readLine();
352 QString text
= QString::fromUtf8(line
.constData()).simplified();
353 if(rxProgress
.lastIndexIn(text
) >= 0)
356 int progress
= rxProgress
.cap(1).toInt(&ok
);
359 maxProgress
= qMax(maxProgress
, progress
);
360 emit
fileSelected(QString("%1 [%2%]").arg(baseName
, QString::number(maxProgress
)));
363 else if(rxChannels
.lastIndexIn(text
) >= 0)
366 unsigned int channels
= rxChannels
.cap(1).toUInt(&ok
);
367 if(ok
) outFileInfo
.setFormatAudioChannels(channels
);
369 else if(rxSamplerate
.lastIndexIn(text
) >= 0)
372 unsigned int samplerate
= rxSamplerate
.cap(1).toUInt(&ok
);
373 if(ok
) outFileInfo
.setFormatAudioSamplerate(samplerate
);
375 else if(rxPrecision
.lastIndexIn(text
) >= 0)
378 unsigned int precision
= rxPrecision
.cap(1).toUInt(&ok
);
379 if(ok
) outFileInfo
.setFormatAudioBitdepth(precision
);
381 else if(rxDuration
.lastIndexIn(text
) >= 0)
383 bool ok1
= false, ok2
= false, ok3
= false;
384 unsigned int hh
= rxDuration
.cap(1).toUInt(&ok1
);
385 unsigned int mm
= rxDuration
.cap(2).toUInt(&ok2
);
386 unsigned int ss
= rxDuration
.cap(3).toUInt(&ok3
);
387 if(ok1
&& ok2
&& ok3
)
389 unsigned intputLen
= (hh
* 3600) + (mm
* 60) + ss
;
390 if(length
== std::numeric_limits
<double>::infinity())
392 qDebug("Duration updated from SoX info!");
393 int duration
= intputLen
- static_cast<int>(floor(offset
+ 0.5));
394 if(duration
< 0) qWarning("Track is out of bounds: Track offset exceeds input file duration!");
395 outFileInfo
.setFileDuration(qMax(0, duration
));
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!");
407 process
.waitForFinished();
408 if(process
.state() != QProcess::NotRunning
)
411 process
.waitForFinished(-1);
414 if(process
.exitCode() != EXIT_SUCCESS
|| QFileInfo(output
).size() == 0)
416 qWarning("Splitting has failed !!!");
421 emit
fileSplit(outFileInfo
);
425 QString
CueSplitter::indexToString(const double index
) const
427 if(!_finite(index
) || (index
< 0.0) || (index
> 86400.0))
432 QTime time
= QTime().addMSecs(static_cast<int>(floor(0.5 + (index
* 1000.0))));
433 return time
.toString(time
.hour() ? "H:mm:ss.zzz" : "m:ss.zzz");
436 QString
CueSplitter::shortName(const QString
&longName
) const
438 static const int maxLen
= 54;
440 if(longName
.length() > maxLen
)
442 return QString("%1...%2").arg(longName
.left(maxLen
/2).trimmed(), longName
.right(maxLen
/2).trimmed());
448 ////////////////////////////////////////////////////////////
450 ////////////////////////////////////////////////////////////