|
|
|
/*
|
|
|
|
*
|
|
|
|
* $Id: k3baudioprojectconvertingthread.cpp 619556 2007-01-03 17:38:12Z trueg $
|
|
|
|
* Copyright (C) 2005 Sebastian Trueg <trueg@k3b.org>
|
|
|
|
*
|
|
|
|
* This file is part of the K3b project.
|
|
|
|
* Copyright (C) 1998-2007 Sebastian Trueg <trueg@k3b.org>
|
|
|
|
*
|
|
|
|
* This program is free software; you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
* See the file "COPYING" for the exact licensing terms.
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
#include "k3baudioprojectconvertingthread.h"
|
|
|
|
#include "k3bpatternparser.h"
|
|
|
|
|
|
|
|
#include <k3bjob.h>
|
|
|
|
#include <k3baudiodoc.h>
|
|
|
|
#include <k3baudiotrack.h>
|
|
|
|
#include <k3baudioencoder.h>
|
|
|
|
#include <k3bwavefilewriter.h>
|
|
|
|
#include "k3bcuefilewriter.h"
|
|
|
|
|
|
|
|
#include <k3bglobals.h>
|
|
|
|
|
|
|
|
#include <tqfile.h>
|
|
|
|
#include <tqtimer.h>
|
|
|
|
|
|
|
|
#include <kdebug.h>
|
|
|
|
#include <klocale.h>
|
|
|
|
#include <kstandarddirs.h>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class K3bAudioProjectConvertingThread::Private
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
Private()
|
|
|
|
: encoder(0),
|
|
|
|
waveFileWriter(0),
|
|
|
|
canceled(false) {
|
|
|
|
}
|
|
|
|
|
|
|
|
// the index of the currently ripped track in m_tracks
|
|
|
|
int currentTrackIndex;
|
|
|
|
long long overallBytesRead;
|
|
|
|
long long overallBytesToRead;
|
|
|
|
|
|
|
|
K3bAudioEncoder* encoder;
|
|
|
|
K3bWaveFileWriter* waveFileWriter;
|
|
|
|
|
|
|
|
bool canceled;
|
|
|
|
|
|
|
|
TQString fileType;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
K3bAudioProjectConvertingThread::K3bAudioProjectConvertingThread( K3bAudioDoc* doc )
|
|
|
|
: K3bThread(),
|
|
|
|
m_doc(doc)
|
|
|
|
{
|
|
|
|
d = new Private();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
K3bAudioProjectConvertingThread::~K3bAudioProjectConvertingThread()
|
|
|
|
{
|
|
|
|
delete d->waveFileWriter;
|
|
|
|
delete d;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void K3bAudioProjectConvertingThread::setFileType( const TQString& t )
|
|
|
|
{
|
|
|
|
d->fileType = t;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void K3bAudioProjectConvertingThread::setEncoder( K3bAudioEncoder* f )
|
|
|
|
{
|
|
|
|
d->encoder = f;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void K3bAudioProjectConvertingThread::init()
|
|
|
|
{
|
|
|
|
d->canceled = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void K3bAudioProjectConvertingThread::run()
|
|
|
|
{
|
|
|
|
emitStarted();
|
|
|
|
emitNewTask( i18n("Converting Audio Tracks") );
|
|
|
|
|
|
|
|
if( !d->encoder )
|
|
|
|
if( !d->waveFileWriter )
|
|
|
|
d->waveFileWriter = new K3bWaveFileWriter();
|
|
|
|
|
|
|
|
|
|
|
|
d->overallBytesRead = 0;
|
|
|
|
d->overallBytesToRead = m_doc->length().audioBytes();
|
|
|
|
|
|
|
|
if( m_singleFile ) {
|
|
|
|
TQString& filename = m_tracks[0].second;
|
|
|
|
|
|
|
|
TQString dir = filename.left( filename.findRev("/") );
|
|
|
|
if( !TDEStandardDirs::makeDir( dir ) ) {
|
|
|
|
emitInfoMessage( i18n("Unable to create directory %1").arg(dir), K3bJob::ERROR );
|
|
|
|
emitFinished(false);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// initialize
|
|
|
|
bool isOpen = true;
|
|
|
|
if( d->encoder ) {
|
|
|
|
if( isOpen = d->encoder->openFile( d->fileType, filename, m_doc->length() ) ) {
|
|
|
|
// here we use cd Title and Artist
|
|
|
|
d->encoder->setMetaData( K3bAudioEncoder::META_TRACK_ARTIST, m_cddbEntry.cdArtist );
|
|
|
|
d->encoder->setMetaData( K3bAudioEncoder::META_TRACK_TITLE, m_cddbEntry.cdTitle );
|
|
|
|
d->encoder->setMetaData( K3bAudioEncoder::META_TRACK_COMMENT, m_cddbEntry.cdExtInfo );
|
|
|
|
d->encoder->setMetaData( K3bAudioEncoder::META_ALBUM_ARTIST, m_cddbEntry.cdArtist );
|
|
|
|
d->encoder->setMetaData( K3bAudioEncoder::META_ALBUM_TITLE, m_cddbEntry.cdTitle );
|
|
|
|
d->encoder->setMetaData( K3bAudioEncoder::META_ALBUM_COMMENT, m_cddbEntry.cdExtInfo );
|
|
|
|
d->encoder->setMetaData( K3bAudioEncoder::META_YEAR, TQString::number(m_cddbEntry.year) );
|
|
|
|
d->encoder->setMetaData( K3bAudioEncoder::META_GENRE, m_cddbEntry.genre );
|
|
|
|
}
|
|
|
|
else
|
|
|
|
emitInfoMessage( d->encoder->lastErrorString(), K3bJob::ERROR );
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
isOpen = d->waveFileWriter->open( filename );
|
|
|
|
}
|
|
|
|
|
|
|
|
if( !isOpen ) {
|
|
|
|
emitInfoMessage( i18n("Unable to open '%1' for writing.").arg(filename), K3bJob::ERROR );
|
|
|
|
emitFinished(false);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
emitInfoMessage( i18n("Converting to single file '%1'.").arg(filename), K3bJob::INFO );
|
|
|
|
}
|
|
|
|
|
|
|
|
bool success = true;
|
|
|
|
K3bAudioTrack* track = m_doc->firstTrack();
|
|
|
|
unsigned int i = 0;
|
|
|
|
while( track ) {
|
|
|
|
d->currentTrackIndex = i;
|
|
|
|
if( !convertTrack( track, m_singleFile ? m_tracks[0].second : m_tracks[i].second ) ) {
|
|
|
|
success = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
emitInfoMessage( i18n("Successfully converted track %1.").arg(i+1), K3bJob::INFO );
|
|
|
|
|
|
|
|
track = track->next();
|
|
|
|
++i;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( m_singleFile ) {
|
|
|
|
if( d->encoder )
|
|
|
|
d->encoder->closeFile();
|
|
|
|
else
|
|
|
|
d->waveFileWriter->close();
|
|
|
|
}
|
|
|
|
|
|
|
|
if( !d->canceled && success && m_writePlaylist ) {
|
|
|
|
success = success && writePlaylist();
|
|
|
|
}
|
|
|
|
|
|
|
|
if( !d->canceled && success && m_writeCueFile && m_singleFile ) {
|
|
|
|
success = success && writeCueFile();
|
|
|
|
}
|
|
|
|
|
|
|
|
if( d->canceled ) {
|
|
|
|
if( d->currentTrackIndex >= 0 && d->currentTrackIndex < (int)m_tracks.count() ) {
|
|
|
|
if( TQFile::exists( m_tracks[d->currentTrackIndex].second ) ) {
|
|
|
|
TQFile::remove( m_tracks[d->currentTrackIndex].second );
|
|
|
|
emitInfoMessage( i18n("Removed partial file '%1'.").arg(m_tracks[d->currentTrackIndex].second), K3bJob::INFO );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
emitCanceled();
|
|
|
|
emitFinished(false);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
emitFinished(success);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool K3bAudioProjectConvertingThread::convertTrack( K3bAudioTrack* track, const TQString& filename )
|
|
|
|
{
|
|
|
|
TQString dir = filename.left( filename.findRev("/") );
|
|
|
|
if( !TDEStandardDirs::makeDir( dir ) ) {
|
|
|
|
emitInfoMessage( i18n("Unable to create directory %1").arg(dir), K3bJob::ERROR );
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// initialize
|
|
|
|
bool isOpen = true;
|
|
|
|
if( !m_singleFile ) {
|
|
|
|
if( d->encoder ) {
|
|
|
|
if( isOpen = d->encoder->openFile( d->fileType,
|
|
|
|
filename,
|
|
|
|
track->length() ) ) {
|
|
|
|
|
|
|
|
d->encoder->setMetaData( K3bAudioEncoder::META_TRACK_ARTIST, m_cddbEntry.artists[d->currentTrackIndex] );
|
|
|
|
d->encoder->setMetaData( K3bAudioEncoder::META_TRACK_TITLE, m_cddbEntry.titles[d->currentTrackIndex] );
|
|
|
|
d->encoder->setMetaData( K3bAudioEncoder::META_TRACK_COMMENT, m_cddbEntry.extInfos[d->currentTrackIndex] );
|
|
|
|
d->encoder->setMetaData( K3bAudioEncoder::META_TRACK_NUMBER, TQString::number(d->currentTrackIndex+1).rightJustify( 2, '0' ) );
|
|
|
|
d->encoder->setMetaData( K3bAudioEncoder::META_ALBUM_ARTIST, m_cddbEntry.cdArtist );
|
|
|
|
d->encoder->setMetaData( K3bAudioEncoder::META_ALBUM_TITLE, m_cddbEntry.cdTitle );
|
|
|
|
d->encoder->setMetaData( K3bAudioEncoder::META_ALBUM_COMMENT, m_cddbEntry.cdExtInfo );
|
|
|
|
d->encoder->setMetaData( K3bAudioEncoder::META_YEAR, TQString::number(m_cddbEntry.year) );
|
|
|
|
d->encoder->setMetaData( K3bAudioEncoder::META_GENRE, m_cddbEntry.genre );
|
|
|
|
}
|
|
|
|
else
|
|
|
|
emitInfoMessage( d->encoder->lastErrorString(), K3bJob::ERROR );
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
isOpen = d->waveFileWriter->open( filename );
|
|
|
|
}
|
|
|
|
|
|
|
|
if( !isOpen ) {
|
|
|
|
emitInfoMessage( i18n("Unable to open '%1' for writing.").arg(filename), K3bJob::ERROR );
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if( !m_cddbEntry.artists[d->currentTrackIndex].isEmpty() &&
|
|
|
|
!m_cddbEntry.titles[d->currentTrackIndex].isEmpty() )
|
|
|
|
emitNewSubTask( i18n("Converting track %1 (%2 - %3)")
|
|
|
|
.arg(d->currentTrackIndex+1)
|
|
|
|
.arg(m_cddbEntry.artists[d->currentTrackIndex])
|
|
|
|
.arg(m_cddbEntry.titles[d->currentTrackIndex]) );
|
|
|
|
else
|
|
|
|
emitNewSubTask( i18n("Converting track %1").arg(d->currentTrackIndex+1) );
|
|
|
|
|
|
|
|
|
|
|
|
// do the conversion
|
|
|
|
// ----------------------
|
|
|
|
|
|
|
|
char buffer[10*1024];
|
|
|
|
const int bufferLength = 10*1024;
|
|
|
|
int readLength = 0;
|
|
|
|
long long readFile = 0;
|
|
|
|
track->seek(0);
|
|
|
|
while( !d->canceled && ( readLength = track->read( buffer, bufferLength ) ) > 0 ) {
|
|
|
|
|
|
|
|
if( d->encoder ) {
|
|
|
|
// the tracks produce big endian samples
|
|
|
|
// so we need to swap the bytes here
|
|
|
|
char b;
|
|
|
|
for( int i = 0; i < bufferLength-1; i+=2 ) {
|
|
|
|
b = buffer[i];
|
|
|
|
buffer[i] = buffer[i+1];
|
|
|
|
buffer[i+1] = b;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( d->encoder->encode( buffer, readLength ) < 0 ) {
|
|
|
|
kdDebug() << "(K3bAudioProjectConvertingThread) error while encoding." << endl;
|
|
|
|
emitInfoMessage( d->encoder->lastErrorString(), K3bJob::ERROR );
|
|
|
|
emitInfoMessage( i18n("Error while encoding track %1.").arg(d->currentTrackIndex+1), K3bJob::ERROR );
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
d->waveFileWriter->write( buffer,
|
|
|
|
readLength,
|
|
|
|
K3bWaveFileWriter::BigEndian );
|
|
|
|
}
|
|
|
|
|
|
|
|
d->overallBytesRead += readLength;
|
|
|
|
readFile += readLength;
|
|
|
|
emitSubPercent( 100*readFile/track->size() );
|
|
|
|
emitPercent( 100*d->overallBytesRead/d->overallBytesToRead );
|
|
|
|
}
|
|
|
|
|
|
|
|
if( !m_singleFile ) {
|
|
|
|
if( d->encoder )
|
|
|
|
d->encoder->closeFile();
|
|
|
|
else
|
|
|
|
d->waveFileWriter->close();
|
|
|
|
}
|
|
|
|
|
|
|
|
return ( readLength == 0 );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void K3bAudioProjectConvertingThread::cancel()
|
|
|
|
{
|
|
|
|
d->canceled = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool K3bAudioProjectConvertingThread::writePlaylist()
|
|
|
|
{
|
|
|
|
// this is an absolut path so there is always a "/"
|
|
|
|
TQString playlistDir = m_playlistFilename.left( m_playlistFilename.findRev( "/" ) );
|
|
|
|
|
|
|
|
if( !TDEStandardDirs::makeDir( playlistDir ) ) {
|
|
|
|
emitInfoMessage( i18n("Unable to create directory %1").arg(playlistDir), K3bJob::ERROR );
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
emitInfoMessage( i18n("Writing playlist to %1.").arg( m_playlistFilename ), K3bJob::INFO );
|
|
|
|
|
|
|
|
TQFile f( m_playlistFilename );
|
|
|
|
if( f.open( IO_WriteOnly ) ) {
|
|
|
|
TQTextStream t( &f );
|
|
|
|
|
|
|
|
// format descriptor
|
|
|
|
t << "#EXTM3U" << endl;
|
|
|
|
|
|
|
|
// now write the entries (or the entry if m_singleFile)
|
|
|
|
if( m_singleFile ) {
|
|
|
|
// extra info
|
|
|
|
t << "#EXTINF:" << m_doc->length().lba() << ",";
|
|
|
|
if( !m_cddbEntry.cdArtist.isEmpty() && !m_cddbEntry.cdTitle.isEmpty() )
|
|
|
|
t << m_cddbEntry.cdArtist << " - " << m_cddbEntry.cdTitle << endl;
|
|
|
|
else
|
|
|
|
t << m_tracks[0].second.mid(m_tracks[0].second.findRev("/") + 1,
|
|
|
|
m_tracks[0].second.length() - m_tracks[0].second.findRev("/") - 5)
|
|
|
|
<< endl; // filename without extension
|
|
|
|
|
|
|
|
// filename
|
|
|
|
if( m_relativePathInPlaylist )
|
|
|
|
t << findRelativePath( m_tracks[0].second, playlistDir )
|
|
|
|
<< endl;
|
|
|
|
else
|
|
|
|
t << m_tracks[0].second << endl;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
for( unsigned int i = 0; i < m_tracks.count(); ++i ) {
|
|
|
|
int trackIndex = m_tracks[i].first-1;
|
|
|
|
|
|
|
|
// extra info
|
|
|
|
t << "#EXTINF:" << m_doc->length().totalFrames()/75 << ",";
|
|
|
|
|
|
|
|
if( !m_cddbEntry.artists[trackIndex].isEmpty() && !m_cddbEntry.titles[trackIndex].isEmpty() )
|
|
|
|
t << m_cddbEntry.artists[trackIndex] << " - " << m_cddbEntry.titles[trackIndex] << endl;
|
|
|
|
else
|
|
|
|
t << m_tracks[i].second.mid(m_tracks[i].second.findRev("/") + 1,
|
|
|
|
m_tracks[i].second.length()
|
|
|
|
- m_tracks[i].second.findRev("/") - 5)
|
|
|
|
<< endl; // filename without extension
|
|
|
|
|
|
|
|
// filename
|
|
|
|
if( m_relativePathInPlaylist )
|
|
|
|
t << findRelativePath( m_tracks[i].second, playlistDir )
|
|
|
|
<< endl;
|
|
|
|
else
|
|
|
|
t << m_tracks[i].second << endl;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ( t.device()->status() == IO_Ok );
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
emitInfoMessage( i18n("Unable to open '%1' for writing.").arg(m_playlistFilename), K3bJob::ERROR );
|
|
|
|
kdDebug() << "(K3bAudioProjectConvertingThread) could not open file " << m_playlistFilename << " for writing." << endl;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool K3bAudioProjectConvertingThread::writeCueFile()
|
|
|
|
{
|
|
|
|
K3bCueFileWriter cueWriter;
|
|
|
|
|
|
|
|
// create a new toc and cd-text
|
|
|
|
K3bDevice::Toc toc;
|
|
|
|
K3bDevice::CdText text;
|
|
|
|
text.setPerformer( m_cddbEntry.cdArtist );
|
|
|
|
text.setTitle( m_cddbEntry.cdTitle );
|
|
|
|
text.reserve( m_tracks.count() );
|
|
|
|
K3b::Msf currentSector;
|
|
|
|
K3bAudioTrack* track = m_doc->firstTrack();
|
|
|
|
int trackNum = 1;
|
|
|
|
while( track ) {
|
|
|
|
|
|
|
|
K3bDevice::Track newTrack( currentSector, (currentSector+=track->length()) - 1, K3bDevice::Track::AUDIO );
|
|
|
|
toc.append( newTrack );
|
|
|
|
|
|
|
|
K3bDevice::TrackCdText trackText;
|
|
|
|
trackText.setPerformer( m_cddbEntry.artists[trackNum-1] );
|
|
|
|
trackText.setTitle( m_cddbEntry.titles[trackNum-1] );
|
|
|
|
text.append( trackText );
|
|
|
|
|
|
|
|
track = track->next();
|
|
|
|
++trackNum;
|
|
|
|
}
|
|
|
|
|
|
|
|
cueWriter.setData( toc );
|
|
|
|
cueWriter.setCdText( text );
|
|
|
|
|
|
|
|
|
|
|
|
// we always use a relative filename here
|
|
|
|
TQString imageFile = m_tracks[0].second.section( '/', -1 );
|
|
|
|
cueWriter.setImage( imageFile, ( d->fileType.isEmpty() ? TQString("WAVE") : d->fileType ) );
|
|
|
|
|
|
|
|
// use the same base name as the image file
|
|
|
|
TQString cueFile = m_tracks[0].second;
|
|
|
|
cueFile.truncate( cueFile.findRev(".") );
|
|
|
|
cueFile += ".cue";
|
|
|
|
|
|
|
|
emitInfoMessage( i18n("Writing cue file to %1.").arg(cueFile), K3bJob::INFO );
|
|
|
|
|
|
|
|
return cueWriter.save( cueFile );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TQString K3bAudioProjectConvertingThread::findRelativePath( const TQString& absPath, const TQString& baseDir )
|
|
|
|
{
|
|
|
|
TQString baseDir_ = K3b::prepareDir( K3b::fixupPath(baseDir) );
|
|
|
|
TQString path = K3b::fixupPath( absPath );
|
|
|
|
|
|
|
|
// both paths have an equal beginning. That's just how it's configured by K3b
|
|
|
|
int pos = baseDir_.find( "/" );
|
|
|
|
int oldPos = pos;
|
|
|
|
while( pos != -1 && path.left( pos+1 ) == baseDir_.left( pos+1 ) ) {
|
|
|
|
oldPos = pos;
|
|
|
|
pos = baseDir_.find( "/", pos+1 );
|
|
|
|
}
|
|
|
|
|
|
|
|
// now the paths are equal up to oldPos, so that's how "deep" we go
|
|
|
|
path = path.mid( oldPos+1 );
|
|
|
|
baseDir_ = baseDir_.mid( oldPos+1 );
|
|
|
|
int numberOfDirs = baseDir_.contains( '/' );
|
|
|
|
for( int i = 0; i < numberOfDirs; ++i )
|
|
|
|
path.prepend( "../" );
|
|
|
|
|
|
|
|
return path;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TQString K3bAudioProjectConvertingThread::jobDescription() const
|
|
|
|
{
|
|
|
|
if( m_cddbEntry.cdTitle.isEmpty() )
|
|
|
|
return i18n("Converting Audio Tracks");
|
|
|
|
else
|
|
|
|
return i18n("Converting Audio Tracks From '%1'").arg(m_cddbEntry.cdTitle);
|
|
|
|
}
|
|
|
|
|
|
|
|
TQString K3bAudioProjectConvertingThread::jobDetails() const
|
|
|
|
{
|
|
|
|
if( d->encoder )
|
|
|
|
return i18n("1 track (encoding to %1)",
|
|
|
|
"%n tracks (encoding to %1)",
|
|
|
|
m_tracks.count() ).arg(d->encoder->fileTypeComment(d->fileType));
|
|
|
|
else
|
|
|
|
return i18n("1 track", "%n tracks", m_doc->numOfTracks() );
|
|
|
|
}
|
|
|
|
|