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