1 /*
2   Copyright (C) 2005 Benjamin Meyer <ben at meyerhome dot net>
3 
4   This program is free software; you can redistribute it and/or modify
5   it under the terms of the GNU General Public License as published by
6   the Free Software Foundation; either version 2 of the License, or
7   (at your option) any later version.
8 
9   This program is distributed in the hope that it will be useful,
10   but WITHOUT ANY WARRANTY; without even the implied warranty of
11   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12   GNU General Public License for more details.
13 
14   You should have received a copy of the GNU General Public License
15   along with this program; if not, write to the Free Software
16   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
17   USA.
18 */
19 
20 #include <config-audiocd.h>
21 
22 #include "audiocd_kio_debug.h"
23 #include "audiocd_lame_encoder.h"
24 #include "encoderlame.h"
25 
26 #include <QDir>
27 #include <QFileInfo>
28 #include <QStandardPaths>
29 #include <QStringList>
30 #include <QTemporaryFile>
31 
32 Q_LOGGING_CATEGORY(AUDIOCD_KIO_LOG, "kf.kio.slaves.audiocd")
33 
34 extern "C" {
create_audiocd_encoders(KIO::SlaveBase * slave,QList<AudioCDEncoder * > & encoders)35 AUDIOCDPLUGINS_EXPORT void create_audiocd_encoders(KIO::SlaveBase *slave, QList<AudioCDEncoder *> &encoders)
36 {
37     encoders.append(new EncoderLame(slave));
38 }
39 }
40 
41 static int bitrates[] = {32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320};
42 
43 class EncoderLame::Private
44 {
45 public:
46     int bitrate;
47     bool waitingForWrite;
48     bool processHasExited;
49     QString lastErrorMessage;
50     QStringList genreList;
51     uint lastSize;
52     KProcess *currentEncodeProcess;
53     QTemporaryFile *tempFile;
54 };
55 
EncoderLame(KIO::SlaveBase * slave)56 EncoderLame::EncoderLame(KIO::SlaveBase *slave)
57     : QObject()
58     , AudioCDEncoder(slave)
59 {
60     d = new Private();
61     d->waitingForWrite = false;
62     d->processHasExited = false;
63     d->lastSize = 0;
64     loadSettings();
65 }
66 
~EncoderLame()67 EncoderLame::~EncoderLame()
68 {
69     delete d;
70 }
71 
getConfigureWidget(KConfigSkeleton ** manager) const72 QWidget *EncoderLame::getConfigureWidget(KConfigSkeleton **manager) const
73 {
74     (*manager) = Settings::self();
75     auto config = new EncoderLameConfig();
76     config->cbr_settings->hide();
77     return config;
78 }
79 
init()80 bool EncoderLame::init()
81 {
82     // Determine if lame is installed on the system or not.
83     if (QStandardPaths::findExecutable(QStringLiteral("lame")).isEmpty())
84         return false;
85 
86     // Ask lame for the list of genres it knows; otherwise it barfs when doing
87     // e.g. lame --tg 'Vocal Jazz'
88     KProcess proc;
89     proc.setOutputChannelMode(KProcess::MergedChannels);
90     proc << QStringLiteral("lame") << QStringLiteral("--genre-list");
91     proc.execute();
92 
93     if (proc.exitStatus() != QProcess::NormalExit)
94         return false;
95 
96     QByteArray array = proc.readAll();
97     QString str = QString::fromLocal8Bit(array);
98     d->genreList = str.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
99     // Remove the numbers in front of every genre
100     for (QStringList::Iterator it = d->genreList.begin(); it != d->genreList.end(); ++it) {
101         QString &genre = *it;
102         int i = 0;
103         while (i < genre.length() && (genre[i].isSpace() || genre[i].isDigit()))
104             ++i;
105         genre = genre.mid(i);
106     }
107     // qCDebug(AUDIOCD_KIO_LOG) << "Available genres:" << d->genreList;
108 
109     return true;
110 }
111 
loadSettings()112 void EncoderLame::loadSettings()
113 {
114     // Generate the command line arguments for the current settings
115     args.clear();
116 
117     Settings *settings = Settings::self();
118 
119     // Should we bitswap? I'm unsure about the proper logic for this.
120     // KDE3 always swapped on little-endian hosts,
121     // while #171065 says we shouldn't always do so.
122     // So... let's make it configurable, at least.
123 
124     bool swap = false;
125     switch (settings->byte_swap()) {
126     case Settings::EnumByte_swap::Yes:
127         swap = true;
128         break;
129     case Settings::EnumByte_swap::No:
130         swap = false;
131         break;
132     default:
133 #if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
134         swap = false; // it looks like lame is now clever enough to do the right
135                       // thing by default? (#171065)
136 #else
137         swap = false;
138 #endif
139     }
140     if (swap)
141         args << QStringLiteral("-x");
142 
143     int quality = settings->quality();
144     if (quality < 0)
145         quality = quality * -1;
146     if (quality > 9)
147         quality = 9;
148 
149     int method = settings->bitrate_constant() ? 0 : 1;
150 
151     if (method == 0) {
152         // Constant Bitrate Encoding
153         args.append(QStringLiteral("-b"));
154         args.append(QStringLiteral("%1").arg(bitrates[settings->cbr_bitrate()]));
155         d->bitrate = bitrates[settings->cbr_bitrate()];
156         args.append(QStringLiteral("-q"));
157         args.append(QStringLiteral("%1").arg(quality));
158     } else {
159         // Variable Bitrate Encoding
160         if (settings->vbr_average_br()) {
161             args.append(QStringLiteral("--abr"));
162             args.append(QStringLiteral("%1").arg(bitrates[settings->vbr_mean_brate()]));
163             d->bitrate = bitrates[settings->vbr_mean_brate()];
164             if (settings->vbr_min_br()) {
165                 args.append(QStringLiteral("-b"));
166                 args.append(QStringLiteral("%1").arg(bitrates[settings->vbr_min_brate()]));
167             }
168             if (settings->vbr_min_hard())
169                 args.append(QStringLiteral("-F"));
170             if (settings->vbr_max_br()) {
171                 args.append(QStringLiteral("-B"));
172                 args.append(QStringLiteral("%1").arg(bitrates[settings->vbr_max_brate()]));
173             }
174         } else {
175             d->bitrate = 128;
176             args.append(QStringLiteral("-V"));
177             args.append(QStringLiteral("%1").arg(quality));
178         }
179         if (!settings->vbr_xing_tag())
180             args.append(QStringLiteral("-t"));
181     }
182 
183     args.append(QStringLiteral("-m"));
184     switch (settings->stereo()) {
185     case 0:
186         args.append(QStringLiteral("s"));
187         break;
188     case 1:
189         args.append(QStringLiteral("j"));
190         break;
191     case 2:
192         args.append(QStringLiteral("d"));
193         break;
194     case 3:
195         args.append(QStringLiteral("m"));
196         break;
197     default:
198         args.append(QStringLiteral("s"));
199         break;
200     }
201 
202     if (settings->copyright())
203         args.append(QStringLiteral("-c"));
204     if (!settings->original())
205         args.append(QStringLiteral("-o"));
206     if (settings->iso())
207         args.append(QStringLiteral("--strictly-enforce-ISO"));
208     if (settings->crc())
209         args.append(QStringLiteral("-p"));
210 
211     if (settings->enable_lowpass()) {
212         args.append(QStringLiteral("--lowpass"));
213         args.append(QStringLiteral("%1").arg(settings->lowfilterfreq()));
214 
215         if (settings->set_lpf_width()) {
216             args.append(QStringLiteral("--lowpass-width"));
217             args.append(QStringLiteral("%1").arg(settings->lowfilterwidth()));
218         }
219     }
220 
221     if (settings->enable_highpass()) {
222         args.append(QStringLiteral("--hipass"));
223         args.append(QStringLiteral("%1").arg(settings->highfilterfreq()));
224 
225         if (settings->set_hpf_width()) {
226             args.append(QStringLiteral("--hipass-width"));
227             args.append(QStringLiteral("%1").arg(settings->highfilterwidth()));
228         }
229     }
230 }
231 
size(long time_secs) const232 unsigned long EncoderLame::size(long time_secs) const {
233     return (time_secs * d->bitrate * 1000) / 8;
234 }
235 
readInit(long)236 long EncoderLame::readInit(long /*size*/)
237 {
238     // Create KProcess
239     d->currentEncodeProcess = new KProcess();
240     d->tempFile = new QTemporaryFile(QDir::tempPath() + QLatin1String("/kaudiocd_XXXXXX") + QLatin1String(".mp3"));
241     d->tempFile->open();
242     d->lastErrorMessage.clear();
243     d->processHasExited = false;
244 
245     // -r raw/pcm
246     // -s 44.1 (because it is raw you have to specify this)
247     *(d->currentEncodeProcess) << QStringLiteral("lame") << QStringLiteral("--verbose") << QStringLiteral("-r") << QStringLiteral("-s")
248                                << QStringLiteral("44.1");
249     *(d->currentEncodeProcess) << args;
250     if (Settings::self()->id3_tag())
251         *d->currentEncodeProcess << trackInfo;
252 
253     // Read in stdin, output to the temp file
254     *d->currentEncodeProcess << QStringLiteral("-") << d->tempFile->fileName();
255 
256     // qCDebug(AUDIOCD_KIO_LOG) << d->currentEncodeProcess->args();
257 
258     connect(d->currentEncodeProcess, &KProcess::readyReadStandardOutput, this, &EncoderLame::receivedStdout);
259     connect(d->currentEncodeProcess, &KProcess::readyReadStandardError, this, &EncoderLame::receivedStderr);
260     // 	connect(d->currentEncodeProcess, &KProcess::bytesWritten,
261     //                          this, &EncoderLame::wroteStdin);
262 
263     connect(d->currentEncodeProcess, QOverload<int, QProcess::ExitStatus>::of(&KProcess::finished), this, &EncoderLame::processExited);
264 
265     // Launch!
266     d->currentEncodeProcess->setOutputChannelMode(KProcess::SeparateChannels);
267     d->currentEncodeProcess->start();
268     return 0;
269 }
270 
processExited(int exitCode,QProcess::ExitStatus)271 void EncoderLame::processExited(int exitCode, QProcess::ExitStatus /*status*/)
272 {
273     qCDebug(AUDIOCD_KIO_LOG) << "Lame Encoding process exited with: " << exitCode;
274     d->processHasExited = true;
275 }
276 
receivedStderr()277 void EncoderLame::receivedStderr()
278 {
279     QByteArray error = d->currentEncodeProcess->readAllStandardError();
280     qCDebug(AUDIOCD_KIO_LOG) << "Lame stderr: " << error;
281     if (!d->lastErrorMessage.isEmpty())
282         d->lastErrorMessage += QLatin1Char('\t');
283     d->lastErrorMessage += QString::fromLocal8Bit(error);
284 }
285 
receivedStdout()286 void EncoderLame::receivedStdout()
287 {
288     QString output = QString::fromLocal8Bit(d->currentEncodeProcess->readAllStandardOutput());
289     qCDebug(AUDIOCD_KIO_LOG) << "Lame stdout: " << output;
290 }
291 
292 // void EncoderLame::wroteStdin(){
293 // 	d->waitingForWrite = false;
294 // }
295 
read(qint16 * buf,int frames)296 long EncoderLame::read(qint16 *buf, int frames)
297 {
298     if (!d->currentEncodeProcess)
299         return 0;
300     if (d->processHasExited)
301         return -1;
302 
303     // Pipe the raw data to lame
304     char *cbuf = reinterpret_cast<char *>(buf);
305     d->currentEncodeProcess->write(cbuf, frames * 4);
306     // We can't return until the buffer has been written
307     d->currentEncodeProcess->waitForBytesWritten(-1);
308 
309     // Determine the file size increase
310     QFileInfo file(d->tempFile->fileName());
311     uint change = file.size() - d->lastSize;
312     d->lastSize = file.size();
313     return change;
314 }
315 
readCleanup()316 long EncoderLame::readCleanup()
317 {
318     if (!d->currentEncodeProcess)
319         return 0;
320 
321     // Let lame tag the first frame of the mp3
322     d->currentEncodeProcess->closeWriteChannel();
323     d->currentEncodeProcess->waitForFinished(-1);
324 
325     // Now copy the file out of the temp into kio
326     QFile file(d->tempFile->fileName());
327     if (file.open(QIODevice::ReadOnly)) {
328         char data[1024];
329         while (!file.atEnd()) {
330             uint read = file.read(data, 1024);
331             QByteArray output(data, read);
332             ioslave->data(output);
333         }
334         file.close();
335     }
336 
337     // cleanup the process and temp
338     delete d->currentEncodeProcess;
339     delete d->tempFile;
340     d->lastSize = 0;
341 
342     return 0;
343 }
344 
fillSongInfo(KCDDB::CDInfo info,int track,const QString & comment)345 void EncoderLame::fillSongInfo(KCDDB::CDInfo info, int track, const QString &comment)
346 {
347     trackInfo.clear();
348     trackInfo.append(QStringLiteral("--tt"));
349     trackInfo.append(info.track(track - 1).get(Title).toString());
350 
351     trackInfo.append(QStringLiteral("--ta"));
352     trackInfo.append(info.track(track - 1).get(Artist).toString());
353 
354     trackInfo.append(QStringLiteral("--tl"));
355     trackInfo.append(info.get(Title).toString());
356 
357     trackInfo.append(QStringLiteral("--ty"));
358     trackInfo.append(QStringLiteral("%1").arg(info.get(Year).toString()));
359 
360     trackInfo.append(QStringLiteral("--tc"));
361     trackInfo.append(comment);
362 
363     trackInfo.append(QStringLiteral("--tn"));
364     trackInfo.append(QStringLiteral("%1").arg(track));
365 
366     const QString genre = info.get(Genre).toString();
367     if (d->genreList.indexOf(genre) != -1) {
368         trackInfo.append(QStringLiteral("--tg"));
369         trackInfo.append(genre);
370     }
371 }
372 
lastErrorMessage() const373 QString EncoderLame::lastErrorMessage() const
374 {
375     return d->lastErrorMessage;
376 }
377