OSDN Git Service

b4c972cf009f57a29f9422d7a09f08242e9fa8c6
[lamexp/LameXP.git] / src / Thread_CueSplitter.cpp
1 ///////////////////////////////////////////////////////////////////////////////
2 // LameXP - Audio Encoder Front-End
3 // Copyright (C) 2004-2020 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!
10 //
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.
15 //
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.
19 //
20 // http://www.gnu.org/licenses/gpl-2.0.txt
21 ///////////////////////////////////////////////////////////////////////////////
22
23 #include "Thread_CueSplitter.h"
24
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"
32
33 //MUtils
34 #include <MUtils/Global.h>
35 #include <MUtils/OSSupport.h>
36
37 //Qt
38 #include <QDir>
39 #include <QFileInfo>
40 #include <QProcess>
41 #include <QDate>
42 #include <QTime>
43 #include <QDebug>
44
45 //CRT
46 #include <math.h>
47 #include <float.h>
48 #include <limits>
49
50 ////////////////////////////////////////////////////////////
51 // Constructor
52 ////////////////////////////////////////////////////////////
53
54 CueSplitter::CueSplitter(const QString &outputDir, const QString &baseName, CueSheetModel *model, const QList<AudioFileModel> &inputFilesInfo)
55 :
56         m_model(model),
57         m_outputDir(outputDir),
58         m_baseName(baseName),
59         m_soxBin(lamexp_tools_lookup("sox.exe"))
60 {
61         if(m_soxBin.isEmpty())
62         {
63                 qFatal("Invalid path to SoX binary. Tool not initialized properly.");
64         }
65
66         m_decompressedFiles.clear();
67         m_tempFiles.clear();
68
69         qDebug("\n[CueSplitter]");
70
71         int nInputFiles = inputFilesInfo.count();
72         for(int i = 0; i < nInputFiles; i++)
73         {
74                 m_inputFilesInfo.insert(inputFilesInfo[i].filePath(), inputFilesInfo[i]);
75                 qDebug("File %02d: <%s>", i, MUTILS_UTF8(inputFilesInfo[i].filePath()));
76         }
77         
78         qDebug("All input files added.");
79         m_bSuccess = false;
80 }
81
82 CueSplitter::~CueSplitter(void)
83 {
84         while(!m_tempFiles.isEmpty())
85         {
86                 MUtils::remove_file(m_tempFiles.takeFirst());
87         }
88 }
89
90 ////////////////////////////////////////////////////////////
91 // Thread Main
92 ////////////////////////////////////////////////////////////
93
94 void CueSplitter::run()
95 {
96         m_bSuccess = false;
97         m_bAborted = false;
98         m_nTracksSuccess = 0;
99         m_nTracksSkipped = 0;
100         m_decompressedFiles.clear();
101         m_activeFile.clear();
102         
103         if(!QDir(m_outputDir).exists())
104         {
105                 qWarning("Output directory \"%s\" does not exist!", MUTILS_UTF8(m_outputDir));
106                 return;
107         }
108         
109         QStringList inputFileList = m_inputFilesInfo.keys();
110         int nInputFiles = inputFileList.count();
111         
112         emit progressMaxChanged(nInputFiles);
113         emit progressValChanged(0);
114
115         //Decompress all input files
116         for(int i = 0; i < nInputFiles; i++)
117         {
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))
120                 {
121                         AbstractDecoder *decoder = DecoderRegistry::lookup(inputFileInfo.containerType(), inputFileInfo.containerProfile(), inputFileInfo.audioType(), inputFileInfo.audioProfile(), inputFileInfo.audioVersion());
122                         if(decoder)
123                         {
124                                 m_activeFile = shortName(QFileInfo(inputFileList.at(i)).fileName());
125                                 
126                                 emit fileSelected(m_activeFile);
127                                 emit progressValChanged(i+1);
128                                 
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);
131                                 
132                                 if(decoder->decode(inputFileList.at(i), tempFile, m_abortFlag))
133                                 {
134                                         m_decompressedFiles.insert(inputFileList.at(i), tempFile);
135                                         m_tempFiles.append(tempFile);
136                                 }
137                                 else
138                                 {
139                                         qWarning("Failed to decompress file: <%s>", inputFileList.at(i).toLatin1().constData());
140                                         MUtils::remove_file(tempFile);
141                                 }
142                                 
143                                 m_activeFile.clear();
144                                 MUTILS_DELETE(decoder);
145                         }
146                         else
147                         {
148                                 qWarning("Unsupported input file: <%s>", inputFileList.at(i).toLatin1().constData());
149                         }
150                 }
151                 else
152                 {
153                         m_decompressedFiles.insert(inputFileList.at(i), inputFileList.at(i));
154                 }
155
156                 if(MUTILS_BOOLIFY(m_abortFlag))
157                 {
158                         m_bAborted = true;
159                         qWarning("The user has requested to abort the process!");
160                         return;
161                 }
162         }
163
164         int nFiles = m_model->getFileCount();
165         int nTracksTotal = 0, nTracksComplete = 0;
166
167         for(int i = 0; i < nFiles; i++)
168         {
169                 nTracksTotal += m_model->getTrackCount(i);
170         }
171
172         emit progressMaxChanged(10 * nTracksTotal);
173         emit progressValChanged(0);
174
175         const AudioFileModel_MetaInfo *albumInfo = m_model->getAlbumInfo();
176
177         //Now split all files
178         for(int i = 0; i < nFiles; i++)
179         {
180                 int nTracks = m_model->getTrackCount(i);
181                 QString trackFile = m_model->getFileName(i);
182
183                 //Process all tracks
184                 for(int j = 0; j < nTracks; j++)
185                 {
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);
191                         
192                         if((trackNo < 0) || _isnan(trackOffset) || _isnan(trackLength))
193                         {
194                                 qWarning("Failed to fetch information for track #%d of file #%d!", j, i);
195                                 continue;
196                         }
197                         
198                         //Setup meta info
199                         AudioFileModel_MetaInfo trackMetaInfo(*trackInfo);
200                         
201                         //Apply album meta data on files
202                         if(trackMetaInfo.title().trimmed().isEmpty())
203                         {
204                                 trackMetaInfo.setTitle(QString().sprintf("Track %02d", trackNo));
205                         }
206                         trackMetaInfo.update(*albumInfo, false);
207
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++)
212                         {
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));
214                         }
215
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);
220
221                         if(MUTILS_BOOLIFY(m_abortFlag))
222                         {
223                                 m_bAborted = true;
224                                 qWarning("The user has requested to abort the process!");
225                                 return;
226                         }
227                 }
228         }
229
230         emit progressValChanged(10 * nTracksTotal);
231         MUtils::OS::sleep_ms(333);
232
233         qDebug("All files were split.\n");
234         m_bSuccess = true;
235 }
236
237 ////////////////////////////////////////////////////////////
238 // Slots
239 ////////////////////////////////////////////////////////////
240
241 void CueSplitter::handleUpdate(int progress)
242 {
243         //QString("%1 [%2]").arg(m_activeFile, QString::number(progress)))
244 }
245
246 ////////////////////////////////////////////////////////////
247 // Privtae Functions
248 ////////////////////////////////////////////////////////////
249
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)
251 {
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()));
259         
260         int prevProgress = baseProgress;
261
262         if(!m_decompressedFiles.contains(file))
263         {
264                 qWarning("Unknown or unsupported input file, skipping!");
265                 m_nTracksSkipped++;
266                 return;
267         }
268
269         QString baseName = shortName(QFileInfo(output).fileName());
270         QString decompressedInput = m_decompressedFiles[file];
271         qDebug("Input: <%s>", MUTILS_UTF8(decompressedInput));
272         
273         AudioFileModel outFileInfo(output);
274         outFileInfo.setMetaInfo(metaInfo);
275         
276         AudioFileModel_TechInfo &outFileTechInfo = outFileInfo.techInfo();
277         outFileTechInfo.setContainerType("Wave");
278         outFileTechInfo.setAudioType("PCM");
279         outFileTechInfo.setDuration(static_cast<unsigned int>(abs(length)));
280
281         QStringList args;
282         args << "-S" << "-V3";
283         args << "--guard" << "--temp" << ".";
284         args << QDir::toNativeSeparators(decompressedInput);
285         args << QDir::toNativeSeparators(output);
286         
287         //Add trim parameters, if needed
288         if(_finite(offset))
289         {
290                 args << "trim";
291                 args << indexToString(offset);
292                 
293                 if(_finite(length))
294                 {
295                         args << indexToString(length);
296                 }
297         }
298
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);
304
305         QProcess process;
306         MUtils::init_process(process, m_outputDir);
307
308         process.start(m_soxBin, args);
309                 
310         if(!process.waitForStarted())
311         {
312                 qWarning("SoX process failed to create!");
313                 qWarning("Error message: \"%s\"\n", process.errorString().toLatin1().constData());
314                 process.kill();
315                 process.waitForFinished(-1);
316                 m_nTracksSkipped++;
317                 return;
318         }
319
320         while(process.state() != QProcess::NotRunning)
321         {
322                 if(MUTILS_BOOLIFY(m_abortFlag))
323                 {
324                         process.kill();
325                         qWarning("Process was aborted on user request!");
326                         break;
327                 }
328                 process.waitForReadyRead(m_processTimeoutInterval);
329                 if(!process.bytesAvailable() && process.state() == QProcess::Running)
330                 {
331                         process.kill();
332                         qWarning("SoX process timed out <-- killing!");
333                         break;
334                 }
335                 while(process.bytesAvailable() > 0)
336                 {
337                         QByteArray line = process.readLine();
338                         QString text = QString::fromUtf8(line.constData()).simplified();
339                         if(rxProgress.lastIndexIn(text) >= 0)
340                         {
341                                 bool ok = false;
342                                 int progress = rxProgress.cap(1).toInt(&ok);
343                                 if(ok)
344                                 {
345                                         const int newProgress = baseProgress + qRound(static_cast<double>(qBound(0, progress, 100)) / 10.0);
346                                         if(newProgress > prevProgress)
347                                         {
348                                                 emit progressValChanged(newProgress);
349                                                 prevProgress = newProgress;
350                                         }
351                                 }
352                         }
353                         else if(rxChannels.lastIndexIn(text) >= 0)
354                         {
355                                 bool ok = false;
356                                 unsigned int channels = rxChannels.cap(1).toUInt(&ok);
357                                 if(ok) outFileInfo.techInfo().setAudioChannels(channels);
358                         }
359                         else if(rxSamplerate.lastIndexIn(text) >= 0)
360                         {
361                                 bool ok = false;
362                                 unsigned int samplerate = rxSamplerate.cap(1).toUInt(&ok);
363                                 if(ok) outFileInfo.techInfo().setAudioSamplerate(samplerate);
364                         }
365                         else if(rxPrecision.lastIndexIn(text) >= 0)
366                         {
367                                 bool ok = false;
368                                 unsigned int precision = rxPrecision.cap(1).toUInt(&ok);
369                                 if(ok) outFileInfo.techInfo().setAudioBitdepth(precision);
370                         }
371                         else if(rxDuration.lastIndexIn(text) >= 0)
372                         {
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)
378                                 {
379                                         unsigned intputLen = (hh * 3600) + (mm * 60) + ss;
380                                         if(length == std::numeric_limits<double>::infinity())
381                                         {
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));
386                                         }
387                                         else
388                                         {
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!");
391                                         }
392                                 }
393                         }
394                 }
395         }
396
397         process.waitForFinished();
398         if(process.state() != QProcess::NotRunning)
399         {
400                 process.kill();
401                 process.waitForFinished(-1);
402         }
403
404         if(process.exitCode() != EXIT_SUCCESS || QFileInfo(output).size() == 0)
405         {
406                 qWarning("Splitting has failed !!!");
407                 m_nTracksSkipped++;
408                 return;
409         }
410
411         emit fileSplit(outFileInfo);
412         m_nTracksSuccess++;
413 }
414
415 QString CueSplitter::indexToString(const double index) const
416 {
417         if(!_finite(index) || (index < 0.0) || (index > 86400.0))
418         {
419                 return QString();
420         }
421         
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");
424 }
425
426 QString CueSplitter::shortName(const QString &longName) const
427 {
428         static const int maxLen = 54;
429         
430         if(longName.length() > maxLen)
431         {
432                 return QString("%1...%2").arg(longName.left(maxLen/2).trimmed(), longName.right(maxLen/2).trimmed());
433         }
434
435         return longName;
436 }
437
438 ////////////////////////////////////////////////////////////
439 // EVENTS
440 ////////////////////////////////////////////////////////////
441
442 /*NONE*/