File indexing completed on 2024-04-28 04:50:21
0001 /* 0002 SPDX-FileCopyrightText: 2011 Michal Malek <michalm@jabster.pl> 0003 SPDX-FileCopyrightText: 1998-2010 Sebastian Trueg <trueg@k3b.org> 0004 0005 SPDX-License-Identifier: GPL-2.0-or-later 0006 */ 0007 0008 #include "k3bmassaudioencodingjob.h" 0009 #include "k3baudioencoder.h" 0010 #include "k3bcuefilewriter.h" 0011 #include "k3bwavefilewriter.h" 0012 0013 #include <KLocalizedString> 0014 #include <KCDDB/CDInfo> 0015 0016 #include <QDebug> 0017 #include <QDir> 0018 #include <QFileInfo> 0019 #include <QIODevice> 0020 #include <QTextStream> 0021 0022 #include <vector> 0023 #include <algorithm> 0024 0025 namespace K3b { 0026 0027 namespace 0028 { 0029 0030 struct SortByTrackNumber 0031 { 0032 SortByTrackNumber( MassAudioEncodingJob::Tracks const& tracks ) : m_tracks( tracks ) {} 0033 0034 bool operator()( QString const& lhs, QString const& rhs ) 0035 { 0036 return m_tracks.value( lhs ) < m_tracks.value( rhs ); 0037 } 0038 0039 MassAudioEncodingJob::Tracks const& m_tracks; 0040 }; 0041 0042 struct Task { 0043 Task(MassAudioEncodingJob::Tracks::const_iterator const & it) : 0044 tracknumber(it.value()), 0045 filename (it.key()), 0046 track (it) 0047 { } 0048 0049 static 0050 bool sort_by_filename(Task const & lhs, Task const & rhs) 0051 { 0052 return lhs.filename < rhs.filename; 0053 } 0054 0055 static 0056 bool sort_by_tracknumber(Task const & lhs, Task const & rhs) 0057 { 0058 return lhs.tracknumber < rhs.tracknumber; 0059 } 0060 0061 int tracknumber; 0062 QString filename; 0063 MassAudioEncodingJob::Tracks::const_iterator track; 0064 }; 0065 0066 } // namespace 0067 0068 0069 class MassAudioEncodingJob::Private 0070 { 0071 public: 0072 Private( bool be ) 0073 : 0074 bigEndian( be ), 0075 overallBytesRead( 0 ), 0076 overallBytesToRead( 0 ), 0077 encoder( 0 ), 0078 waveFileWriter( 0 ), 0079 relativePathInPlaylist( false ), 0080 writeCueFile( false ) 0081 { 0082 } 0083 0084 const bool bigEndian; 0085 Tracks tracks; 0086 QHash<QString,Msf> lengths; 0087 qint64 overallBytesRead; 0088 qint64 overallBytesToRead; 0089 AudioEncoder* encoder; 0090 WaveFileWriter* waveFileWriter; 0091 QString fileType; 0092 KCDDB::CDInfo cddbEntry; 0093 0094 QString playlistFilename; 0095 bool relativePathInPlaylist; 0096 bool writeCueFile; 0097 }; 0098 0099 0100 MassAudioEncodingJob::MassAudioEncodingJob( bool bigEndian, JobHandler* jobHandler, QObject* parent ) 0101 : ThreadJob( jobHandler, parent ), 0102 d( new Private( bigEndian ) ) 0103 { 0104 } 0105 0106 0107 MassAudioEncodingJob::~MassAudioEncodingJob() 0108 { 0109 } 0110 0111 0112 void MassAudioEncodingJob::setCddbEntry( const KCDDB::CDInfo& cddbEntry ) 0113 { 0114 d->cddbEntry = cddbEntry; 0115 } 0116 0117 0118 const KCDDB::CDInfo& MassAudioEncodingJob::cddbEntry() const 0119 { 0120 return d->cddbEntry; 0121 } 0122 0123 0124 void MassAudioEncodingJob::setEncoder( AudioEncoder* encoder ) 0125 { 0126 d->encoder = encoder; 0127 } 0128 0129 0130 AudioEncoder* MassAudioEncodingJob::encoder() const 0131 { 0132 return d->encoder; 0133 } 0134 0135 0136 void MassAudioEncodingJob::setFileType( const QString& fileType ) 0137 { 0138 d->fileType = fileType; 0139 } 0140 0141 0142 0143 const QString& MassAudioEncodingJob::fileType() const 0144 { 0145 return d->fileType; 0146 } 0147 0148 0149 void MassAudioEncodingJob::setTrackList( const Tracks& tracks ) 0150 { 0151 d->tracks = tracks; 0152 } 0153 0154 0155 const MassAudioEncodingJob::Tracks& MassAudioEncodingJob::trackList() const 0156 { 0157 return d->tracks; 0158 } 0159 0160 0161 void MassAudioEncodingJob::setWritePlaylist( const QString& filename, bool relativePaths ) 0162 { 0163 d->playlistFilename = filename; 0164 d->relativePathInPlaylist = relativePaths; 0165 } 0166 0167 0168 void MassAudioEncodingJob::setWriteCueFile( bool writeCueFile ) 0169 { 0170 d->writeCueFile = writeCueFile; 0171 } 0172 0173 0174 QString MassAudioEncodingJob::jobDetails() const 0175 { 0176 if( d->encoder ) 0177 return i18np( "1 track (encoding to %2)", 0178 "%1 tracks (encoding to %2)", 0179 d->tracks.count(), 0180 d->encoder->fileTypeComment(d->fileType) ); 0181 else 0182 return i18np( "1 track", "%1 tracks", 0183 d->tracks.count() ); 0184 } 0185 0186 0187 QString MassAudioEncodingJob::jobTarget() const 0188 { 0189 Tracks::const_iterator it = d->tracks.constBegin(); 0190 if( it != d->tracks.constEnd() ) 0191 return QFileInfo( it.key() ).absolutePath(); 0192 else 0193 return QString(); 0194 } 0195 0196 0197 bool MassAudioEncodingJob::init() 0198 { 0199 return true; 0200 } 0201 0202 0203 void MassAudioEncodingJob::cleanup() 0204 { 0205 } 0206 0207 0208 bool MassAudioEncodingJob::run() 0209 { 0210 if ( !init() ) 0211 return false; 0212 0213 if( !d->encoder ) 0214 if( !d->waveFileWriter ) 0215 d->waveFileWriter = new K3b::WaveFileWriter(); 0216 0217 d->overallBytesRead = 0; 0218 d->overallBytesToRead = 0; 0219 d->lengths.clear(); 0220 0221 const QStringList tracksKeys = d->tracks.keys(); 0222 const QSet<QString> fileNames = QSet<QString>(tracksKeys.begin(), tracksKeys.end()); 0223 Q_FOREACH( const QString& filename, fileNames ) { 0224 d->lengths.insert( filename, 0 ); 0225 Q_FOREACH( int trackNumber, d->tracks.values( filename ) ) { 0226 const Msf length = trackLength( trackNumber ); 0227 d->lengths[ filename ] += length; 0228 d->overallBytesToRead += length.audioBytes(); 0229 } 0230 } 0231 0232 // rip tracks in *numerical* order 0233 std::vector<Task> tasks; 0234 tasks.reserve( d->tracks.size() ); 0235 for( Tracks::const_iterator i = d->tracks.constBegin(); i != d->tracks.constEnd(); ++i ) 0236 tasks.push_back( Task(i) ); 0237 std::sort( tasks.begin(), tasks.end(), Task::sort_by_tracknumber ); 0238 0239 bool success = true; 0240 QString lastFilename; 0241 std::vector<Task>::const_iterator currentTask; 0242 for( currentTask = tasks.begin(); success && currentTask != tasks.end(); ++currentTask ) { 0243 success = encodeTrack( currentTask->track.value(), currentTask->track.key(), lastFilename ); 0244 lastFilename = currentTask->track.key(); 0245 } 0246 0247 if( d->encoder ) 0248 d->encoder->closeFile(); 0249 if( d->waveFileWriter ) 0250 d->waveFileWriter->close(); 0251 0252 if( !canceled() && success && !d->playlistFilename.isNull() ) { 0253 success = success && writePlaylist(); 0254 } 0255 0256 if( !canceled() && success && d->writeCueFile ) { 0257 success = success && writeCueFile(); 0258 } 0259 0260 if( canceled() ) { 0261 if( currentTask != tasks.end() ) { 0262 if( QFile::exists( currentTask->track.key() ) ) { 0263 QFile::remove( currentTask->track.key() ); 0264 emit infoMessage( i18n("Removed partial file '%1'.", currentTask->track.key()), K3b::Job::MessageInfo ); 0265 } 0266 } 0267 0268 success = false; 0269 } 0270 0271 cleanup(); 0272 return success; 0273 } 0274 0275 0276 bool K3b::MassAudioEncodingJob::encodeTrack( int trackIndex, const QString& filename, const QString& prevFilename ) 0277 { 0278 QScopedPointer<QIODevice> source( createReader( trackIndex ) ); 0279 if( source.isNull() ) { 0280 return false; 0281 } 0282 0283 QDir dir = QFileInfo( filename ).dir(); 0284 if( !QDir().mkpath( dir.path() ) ) { 0285 emit infoMessage( i18n("Unable to create folder %1",dir.path()), K3b::Job::MessageError ); 0286 return false; 0287 } 0288 0289 // Close the previous file if the new filename is different 0290 if( prevFilename != filename ) { 0291 if( d->encoder ) 0292 d->encoder->closeFile(); 0293 if( d->waveFileWriter ) 0294 d->waveFileWriter->close(); 0295 } 0296 0297 // Open the file to write if it is not already opened 0298 if( (d->encoder && !d->encoder->isOpen()) || 0299 (d->waveFileWriter && !d->waveFileWriter->isOpen()) ) { 0300 bool isOpen = true; 0301 if( d->encoder ) { 0302 AudioEncoder::MetaData metaData; 0303 metaData.insert( AudioEncoder::META_ALBUM_ARTIST, d->cddbEntry.get( KCDDB::Artist ) ); 0304 metaData.insert( AudioEncoder::META_ALBUM_TITLE, d->cddbEntry.get( KCDDB::Title ) ); 0305 metaData.insert( AudioEncoder::META_ALBUM_COMMENT, d->cddbEntry.get( KCDDB::Comment ) ); 0306 metaData.insert( AudioEncoder::META_YEAR, d->cddbEntry.get( KCDDB::Year ) ); 0307 metaData.insert( AudioEncoder::META_GENRE, d->cddbEntry.get( KCDDB::Genre ) ); 0308 if( d->tracks.count( filename ) == 1 ) { 0309 metaData.insert( AudioEncoder::META_TRACK_NUMBER, QString::number(trackIndex).rightJustified( 2, '0' ) ); 0310 metaData.insert( AudioEncoder::META_TRACK_ARTIST, d->cddbEntry.track( trackIndex-1 ).get( KCDDB::Artist ) ); 0311 metaData.insert( AudioEncoder::META_TRACK_TITLE, d->cddbEntry.track( trackIndex-1 ).get( KCDDB::Title ) ); 0312 metaData.insert( AudioEncoder::META_TRACK_COMMENT, d->cddbEntry.track( trackIndex-1 ).get( KCDDB::Comment ) ); 0313 } 0314 else { 0315 metaData.insert( AudioEncoder::META_TRACK_ARTIST, d->cddbEntry.get( KCDDB::Artist ) ); 0316 metaData.insert( AudioEncoder::META_TRACK_TITLE, d->cddbEntry.get( KCDDB::Title ) ); 0317 metaData.insert( AudioEncoder::META_TRACK_COMMENT, d->cddbEntry.get( KCDDB::Comment ) ); 0318 } 0319 0320 isOpen = d->encoder->openFile( d->fileType, filename, d->lengths[ filename ], metaData ); 0321 if( !isOpen ) 0322 emit infoMessage( d->encoder->lastErrorString(), K3b::Job::MessageError ); 0323 } 0324 else { 0325 isOpen = d->waveFileWriter->open( filename ); 0326 } 0327 0328 if( !isOpen ) { 0329 emit infoMessage( i18n("Unable to open '%1' for writing.",filename), K3b::Job::MessageError ); 0330 return false; 0331 } 0332 } 0333 0334 trackStarted( trackIndex ); 0335 0336 // do the conversion 0337 // ---------------------- 0338 0339 char buffer[10*1024]; 0340 const qint64 bufferLength = 10LL*1024LL; 0341 qint64 readLength = 0; 0342 qint64 readFile = 0; 0343 0344 if( !source->open( QIODevice::ReadOnly ) ) { 0345 emit infoMessage( source->errorString(), Job::MessageError ); 0346 return false; 0347 } 0348 0349 while( !canceled() && !source->atEnd() && ( readLength = source->read( buffer, bufferLength ) ) > 0 ) { 0350 0351 if( d->encoder ) { 0352 0353 if( d->bigEndian ) { 0354 // the tracks produce big endian samples 0355 // and encoder encoder consumes little endian 0356 // so we need to swap the bytes here 0357 char b; 0358 for( qint64 i = 0; i < bufferLength-1; i+=2 ) { 0359 b = buffer[i]; 0360 buffer[i] = buffer[i+1]; 0361 buffer[i+1] = b; 0362 } 0363 } 0364 0365 if( d->encoder->encode( buffer, readLength ) < 0 ) { 0366 qDebug() << "error while encoding."; 0367 emit infoMessage( d->encoder->lastErrorString(), K3b::Job::MessageError ); 0368 emit infoMessage( i18n("Error while encoding track %1.",trackIndex), K3b::Job::MessageError ); 0369 return false; 0370 } 0371 } 0372 else { 0373 d->waveFileWriter->write( buffer, 0374 readLength, 0375 d->bigEndian ? WaveFileWriter::BigEndian : WaveFileWriter::LittleEndian ); 0376 } 0377 0378 d->overallBytesRead += readLength; 0379 readFile += readLength; 0380 emit subPercent( 100LL*readFile/source->size() ); 0381 emit percent( 100LL*d->overallBytesRead/d->overallBytesToRead ); 0382 } 0383 0384 if( !canceled() && !source->atEnd() ) { 0385 emit infoMessage( source->errorString(), Job::MessageError ); 0386 return false; 0387 } 0388 0389 trackFinished( trackIndex, filename ); 0390 return source->atEnd(); 0391 } 0392 0393 0394 bool MassAudioEncodingJob::writePlaylist() 0395 { 0396 QFileInfo playlistInfo( d->playlistFilename ); 0397 QDir playlistDir( playlistInfo.dir() ); 0398 0399 if( !QDir().mkpath( playlistDir.path() ) ) { 0400 emit infoMessage( i18n("Unable to create folder %1",playlistDir.path()), Job::MessageError ); 0401 return false; 0402 } 0403 0404 emit infoMessage( i18n("Writing playlist to %1.", d->playlistFilename ), Job::MessageInfo ); 0405 0406 QFile f( d->playlistFilename ); 0407 if( f.open( QIODevice::WriteOnly ) ) { 0408 QTextStream t( &f ); 0409 0410 // format descriptor 0411 t << "#EXTM3U" << Qt::endl; 0412 0413 // Get list of the ripped filenames sorted by track number 0414 QStringList filenames = d->tracks.keys(); 0415 filenames.removeDuplicates(); 0416 std::sort(filenames.begin(), filenames.end(), SortByTrackNumber(d->tracks)); 0417 0418 Q_FOREACH( const QString& trackFile, filenames ) { 0419 0420 // extra info 0421 t << "#EXTINF:" << d->lengths[trackFile].totalFrames()/75 << ","; 0422 0423 QVariant artist; 0424 QVariant title; 0425 0426 QList<int> trackNums = d->tracks.values( trackFile ); 0427 if( trackNums.count() == 1 ) { 0428 int trackIndex = trackNums.first()-1; 0429 artist = d->cddbEntry.track( trackIndex ).get( KCDDB::Artist ); 0430 title = d->cddbEntry.track( trackIndex ).get( KCDDB::Title ); 0431 } 0432 else { 0433 artist = d->cddbEntry.get( KCDDB::Artist ); 0434 title = d->cddbEntry.get( KCDDB::Title ); 0435 } 0436 0437 if( !artist.toString().isEmpty() && !title.toString().isEmpty() ) { 0438 t << artist.toString() << " - " << title.toString() << Qt::endl; 0439 } 0440 else { 0441 t << QFileInfo( trackFile ).baseName() << Qt::endl; 0442 } 0443 0444 // filename 0445 if( d->relativePathInPlaylist ) 0446 t << playlistDir.relativeFilePath( trackFile ) << Qt::endl; 0447 else 0448 t << trackFile << Qt::endl; 0449 } 0450 0451 return ( t.status() == QTextStream::Ok ); 0452 } 0453 else { 0454 emit infoMessage( i18n("Unable to open '%1' for writing.",d->playlistFilename), Job::MessageError ); 0455 qDebug() << "could not open file " << d->playlistFilename << " for writing."; 0456 return false; 0457 } 0458 } 0459 0460 0461 bool MassAudioEncodingJob::writeCueFile() 0462 { 0463 bool success = true; 0464 const QStringList tracksKeys = d->tracks.keys(); 0465 const QSet<QString> fileNames = QSet<QString>(tracksKeys.begin(), tracksKeys.end()); 0466 Q_FOREACH( const QString& filename, fileNames ) { 0467 CueFileWriter cueWriter; 0468 0469 // create a new toc and cd-text 0470 Device::Toc toc; 0471 Device::CdText text; 0472 text.setPerformer( d->cddbEntry.get( KCDDB::Artist ).toString() ); 0473 text.setTitle( d->cddbEntry.get( KCDDB::Title ).toString() ); 0474 Msf currentSector; 0475 0476 QList<int> trackNums = d->tracks.values( filename ); 0477 for( int i = 0; i < trackNums.size(); ++i ) { 0478 const int trackNum = trackNums[ i ]; 0479 const Msf length = trackLength( trackNum ); 0480 Device::Track newTrack( currentSector, currentSector + length - 1, Device::Track::TYPE_AUDIO ); 0481 toc.append( newTrack ); 0482 0483 text.track(i).setPerformer( d->cddbEntry.track( trackNum-1 ).get( KCDDB::Artist ).toString() ); 0484 text.track(i).setTitle( d->cddbEntry.track( trackNum-1 ).get( KCDDB::Title ).toString() ); 0485 0486 currentSector += length; 0487 } 0488 0489 cueWriter.setData( toc ); 0490 cueWriter.setCdText( text ); 0491 0492 QFileInfo fileInfo( filename ); 0493 0494 // we always use a relative filename here 0495 cueWriter.setImage( fileInfo.fileName(), ( d->fileType.isEmpty() ? QString("WAVE") : d->fileType ) ); 0496 0497 // use the same base name as the image file 0498 QString cueFile = fileInfo.dir().filePath( fileInfo.baseName() + ".cue" ); 0499 0500 emit infoMessage( i18n("Writing cue file to %1.",cueFile), Job::MessageInfo ); 0501 0502 success = success && cueWriter.save( cueFile ); 0503 } 0504 return success; 0505 } 0506 0507 } // namespace K3b 0508 0509 #include "moc_k3bmassaudioencodingjob.cpp"