OSDN Git Service

Updated Ukrainian translation file.
[lamexp/LameXP.git] / src / Thread_CueSplitter.cpp
1 ///////////////////////////////////////////////////////////////////////////////
2 // LameXP - Audio Encoder Front-End
3 // Copyright (C) 2004-2012 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.
9 //
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.
14 //
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.
18 //
19 // http://www.gnu.org/licenses/gpl-2.0.txt
20 ///////////////////////////////////////////////////////////////////////////////
21
22 #include "Thread_CueSplitter.h"
23
24 #include "Global.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"
30
31 #include <QDir>
32 #include <QFileInfo>
33 #include <QProcess>
34 #include <QDate>
35 #include <QTime>
36 #include <QDebug>
37
38 #include <math.h>
39 #include <float.h>
40 #include <limits>
41
42 ////////////////////////////////////////////////////////////
43 // Constructor
44 ////////////////////////////////////////////////////////////
45
46 CueSplitter::CueSplitter(const QString &outputDir, const QString &baseName, CueSheetModel *model, const QList<AudioFileModel> &inputFilesInfo)
47 :
48         m_model(model),
49         m_outputDir(outputDir),
50         m_baseName(baseName),
51         m_soxBin(lamexp_lookup_tool("sox.exe"))
52 {
53         if(m_soxBin.isEmpty())
54         {
55                 qFatal("Invalid path to SoX binary. Tool not initialized properly.");
56         }
57
58         m_decompressedFiles.clear();
59         m_tempFiles.clear();
60
61         qDebug("\n[CueSplitter]");
62
63         int nInputFiles = inputFilesInfo.count();
64         for(int i = 0; i < nInputFiles; i++)
65         {
66                 m_inputFilesInfo.insert(inputFilesInfo[i].filePath(), inputFilesInfo[i]);
67                 qDebug("File %02d: <%s>", i, inputFilesInfo[i].filePath().toUtf8().constData());
68         }
69         
70         qDebug("All input files added.");
71         m_bSuccess = false;
72 }
73
74 CueSplitter::~CueSplitter(void)
75 {
76         while(!m_tempFiles.isEmpty())
77         {
78                 lamexp_remove_file(m_tempFiles.takeFirst());
79         }
80 }
81
82 ////////////////////////////////////////////////////////////
83 // Thread Main
84 ////////////////////////////////////////////////////////////
85
86 void CueSplitter::run()
87 {
88         m_bSuccess = false;
89         m_bAborted = false;
90         m_abortFlag = false;
91         m_nTracksSuccess = 0;
92         m_nTracksSkipped = 0;
93         m_decompressedFiles.clear();
94         m_activeFile.clear();
95         
96         if(!QDir(m_outputDir).exists())
97         {
98                 qWarning("Output directory \"%s\" does not exist!", m_outputDir.toUtf8().constData());
99                 return;
100         }
101         
102         QStringList inputFileList = m_inputFilesInfo.keys();
103         int nInputFiles = inputFileList.count();
104         
105         emit progressMaxChanged(nInputFiles);
106         emit progressValChanged(0);
107
108         //Decompress all input files
109         for(int i = 0; i < nInputFiles; i++)
110         {
111                 AudioFileModel &inputFileInfo = m_inputFilesInfo[inputFileList.at(i)];
112                 if(inputFileInfo.formatContainerType().compare("Wave", Qt::CaseInsensitive) || inputFileInfo.formatAudioType().compare("PCM", Qt::CaseInsensitive))
113                 {
114                         AbstractDecoder *decoder = DecoderRegistry::lookup(inputFileInfo.formatContainerType(), inputFileInfo.formatContainerProfile(), inputFileInfo.formatAudioType(), inputFileInfo.formatAudioProfile(), inputFileInfo.formatAudioVersion());
115                         if(decoder)
116                         {
117                                 m_activeFile = shortName(QFileInfo(inputFileList.at(i)).fileName());
118                                 
119                                 emit fileSelected(m_activeFile);
120                                 emit progressValChanged(i+1);
121                                 
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);
124                                 
125                                 if(decoder->decode(inputFileList.at(i), tempFile, &m_abortFlag))
126                                 {
127                                         m_decompressedFiles.insert(inputFileList.at(i), tempFile);
128                                         m_tempFiles.append(tempFile);
129                                 }
130                                 else
131                                 {
132                                         qWarning("Failed to decompress file: <%s>", inputFileList.at(i).toLatin1().constData());
133                                         lamexp_remove_file(tempFile);
134                                 }
135                                 
136                                 m_activeFile.clear();
137                                 LAMEXP_DELETE(decoder);
138                         }
139                         else
140                         {
141                                 qWarning("Unsupported input file: <%s>", inputFileList.at(i).toLatin1().constData());
142                         }
143                 }
144                 else
145                 {
146                         m_decompressedFiles.insert(inputFileList.at(i), inputFileList.at(i));
147                 }
148
149                 if(m_abortFlag)
150                 {
151                         m_bAborted = true;
152                         qWarning("The user has requested to abort the process!");
153                         return;
154                 }
155         }
156
157         int nFiles = m_model->getFileCount();
158         int nTracksTotal = 0, nTracksComplete = 0;
159
160         for(int i = 0; i < nFiles; i++)
161         {
162                 nTracksTotal += m_model->getTrackCount(i);
163         }
164
165         emit progressMaxChanged(nTracksTotal);
166         emit progressValChanged(0);
167
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();
172
173         //Now split all files
174         for(int i = 0; i < nFiles; i++)
175         {
176                 int nTracks = m_model->getTrackCount(i);
177                 QString trackFile = m_model->getFileName(i);
178                 int maxProgress = 0;
179
180                 //Process all tracks
181                 for(int j = 0; j < nTracks; j++)
182                 {
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);
188                         
189                         if((trackNo < 0) || _isnan(trackOffset) || _isnan(trackLength))
190                         {
191                                 qWarning("Failed to fetch information for track #%d of file #%d!", j, i);
192                                 continue;
193                         }
194                         
195                         //Setup meta info
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);
202                         
203                         //Apply album meta data on files
204                         if(trackMetaInfo.fileName().trimmed().isEmpty())
205                         {
206                                 trackMetaInfo.setFileName(QString().sprintf("Track %02d", trackNo));
207                         }
208                         if(!albumTitle.isEmpty())
209                         {
210                                 trackMetaInfo.setFileAlbum(albumTitle);
211                         }
212                         if(!albumPerformer.isEmpty() && trackMetaInfo.fileArtist().isEmpty())
213                         {
214                                 trackMetaInfo.setFileArtist(albumPerformer);
215                         }
216                         if(!albumGenre.isEmpty() && trackMetaInfo.fileGenre().isEmpty())
217                         {
218                                 trackMetaInfo.setFileGenre(albumGenre);
219                         }
220                         if((albumYear > 0) && (trackMetaInfo.fileYear() == 0))
221                         {
222                                 trackMetaInfo.setFileYear(albumYear);
223                         }
224                         if(_finite(trackLength))
225                         {
226                                 trackMetaInfo.setFileDuration(static_cast<unsigned int>(abs(trackLength)));
227                         }
228
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++)
233                         {
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));
235                         }
236
237                         //Call split function
238                         emit fileSelected(shortName(QFileInfo(outputFile).fileName()));
239                         splitFile(outputFile,  trackNo, trackFile, trackOffset, trackLength, trackMetaInfo, maxProgress);
240
241                         if(m_abortFlag)
242                         {
243                                 m_bAborted = true;
244                                 qWarning("The user has requested to abort the process!");
245                                 return;
246                         }
247                 }
248         }
249
250         qDebug("All files were split.\n");
251         m_bSuccess = true;
252 }
253
254 ////////////////////////////////////////////////////////////
255 // Slots
256 ////////////////////////////////////////////////////////////
257
258 void CueSplitter::handleUpdate(int progress)
259 {
260         emit fileSelected(QString("%1 [%2%]").arg(m_activeFile, QString::number(progress)));
261 }
262
263 ////////////////////////////////////////////////////////////
264 // Privtae Functions
265 ////////////////////////////////////////////////////////////
266
267 void CueSplitter::splitFile(const QString &output, const int trackNo, const QString &file, const double offset, const double length, const AudioFileModel &metaInfo, int &maxProgress)
268 {
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());
275         
276         if(!m_decompressedFiles.contains(file))
277         {
278                 qWarning("Unknown or unsupported input file, skipping!");
279                 m_nTracksSkipped++;
280                 return;
281         }
282
283         QString baseName = shortName(QFileInfo(output).fileName());
284         QString decompressedInput = m_decompressedFiles[file];
285         qDebug("Input: <%s>", decompressedInput.toUtf8().constData());
286         
287         emit fileSelected(QString("%1 [%2%]").arg(baseName, QString::number(maxProgress)));
288
289         AudioFileModel outFileInfo(metaInfo);
290         outFileInfo.setFilePath(output);
291         outFileInfo.setFormatContainerType("Wave");
292         outFileInfo.setFormatAudioType("PCM");
293
294         QStringList args;
295         args << "-S" << "-V3";
296         args << "--guard" << "--temp" << ".";
297         args << QDir::toNativeSeparators(decompressedInput);
298         args << QDir::toNativeSeparators(output);
299         
300         //Add trim parameters, if needed
301         if(_finite(offset))
302         {
303                 args << "trim";
304                 args << indexToString(offset);
305                 
306                 if(_finite(length))
307                 {
308                         args << indexToString(length);
309                 }
310         }
311
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);
317
318         QProcess process;
319         process.setProcessChannelMode(QProcess::MergedChannels);
320         process.setReadChannel(QProcess::StandardOutput);
321         process.setWorkingDirectory(m_outputDir);
322         process.start(m_soxBin, args);
323                 
324         if(!process.waitForStarted())
325         {
326                 qWarning("SoX process failed to create!");
327                 qWarning("Error message: \"%s\"\n", process.errorString().toLatin1().constData());
328                 process.kill();
329                 process.waitForFinished(-1);
330                 m_nTracksSkipped++;
331                 return;
332         }
333
334         while(process.state() != QProcess::NotRunning)
335         {
336                 if(m_abortFlag)
337                 {
338                         process.kill();
339                         qWarning("Process was aborted on user request!");
340                         break;
341                 }
342                 process.waitForReadyRead(m_processTimeoutInterval);
343                 if(!process.bytesAvailable() && process.state() == QProcess::Running)
344                 {
345                         process.kill();
346                         qWarning("SoX process timed out <-- killing!");
347                         break;
348                 }
349                 while(process.bytesAvailable() > 0)
350                 {
351                         QByteArray line = process.readLine();
352                         QString text = QString::fromUtf8(line.constData()).simplified();
353                         if(rxProgress.lastIndexIn(text) >= 0)
354                         {
355                                 bool ok = false;
356                                 int progress = rxProgress.cap(1).toInt(&ok);
357                                 if(ok)
358                                 {
359                                         maxProgress = qMax(maxProgress, progress);
360                                         emit fileSelected(QString("%1 [%2%]").arg(baseName, QString::number(maxProgress)));
361                                 }
362                         }
363                         else if(rxChannels.lastIndexIn(text) >= 0)
364                         {
365                                 bool ok = false;
366                                 unsigned int channels = rxChannels.cap(1).toUInt(&ok);
367                                 if(ok) outFileInfo.setFormatAudioChannels(channels);
368                         }
369                         else if(rxSamplerate.lastIndexIn(text) >= 0)
370                         {
371                                 bool ok = false;
372                                 unsigned int samplerate = rxSamplerate.cap(1).toUInt(&ok);
373                                 if(ok) outFileInfo.setFormatAudioSamplerate(samplerate);
374                         }
375                         else if(rxPrecision.lastIndexIn(text) >= 0)
376                         {
377                                 bool ok = false;
378                                 unsigned int precision = rxPrecision.cap(1).toUInt(&ok);
379                                 if(ok) outFileInfo.setFormatAudioBitdepth(precision);
380                         }
381                         else if(rxDuration.lastIndexIn(text) >= 0)
382                         {
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)
388                                 {
389                                         unsigned intputLen = (hh * 3600) + (mm * 60) + ss;
390                                         if(length == std::numeric_limits<double>::infinity())
391                                         {
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));
396                                         }
397                                         else
398                                         {
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!");
401                                         }
402                                 }
403                         }
404                 }
405         }
406
407         process.waitForFinished();
408         if(process.state() != QProcess::NotRunning)
409         {
410                 process.kill();
411                 process.waitForFinished(-1);
412         }
413
414         if(process.exitCode() != EXIT_SUCCESS || QFileInfo(output).size() == 0)
415         {
416                 qWarning("Splitting has failed !!!");
417                 m_nTracksSkipped++;
418                 return;
419         }
420
421         emit fileSplit(outFileInfo);
422         m_nTracksSuccess++;
423 }
424
425 QString CueSplitter::indexToString(const double index) const
426 {
427         if(!_finite(index) || (index < 0.0) || (index > 86400.0))
428         {
429                 return QString();
430         }
431         
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");
434 }
435
436 QString CueSplitter::shortName(const QString &longName) const
437 {
438         static const int maxLen = 54;
439         
440         if(longName.length() > maxLen)
441         {
442                 return QString("%1...%2").arg(longName.left(maxLen/2).trimmed(), longName.right(maxLen/2).trimmed());
443         }
444
445         return longName;
446 }
447
448 ////////////////////////////////////////////////////////////
449 // EVENTS
450 ////////////////////////////////////////////////////////////
451
452 /*NONE*/