1 /*
2 Copyright (C) 2005 Benjamin Meyer <ben at meyerhome dot net>
3 Copyright (C) 2018 Yuri Chornoivan <yurchor@mageia.org>
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
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
18 USA.
19 */
20
21 #include <config-audiocd.h>
22
23 #include "audiocd_kio_debug.h"
24 #include "audiocd_opus_encoder.h"
25 #include "encoderopus.h"
26
27 #include <QDir>
28 #include <QFileInfo>
29 #include <QStandardPaths>
30 #include <QStringList>
31 #include <QTemporaryFile>
32
33 Q_LOGGING_CATEGORY(AUDIOCD_KIO_LOG, "kf.kio.slaves.audiocd")
34
35 extern "C" {
create_audiocd_encoders(KIO::SlaveBase * slave,QList<AudioCDEncoder * > & encoders)36 AUDIOCDPLUGINS_EXPORT void create_audiocd_encoders(KIO::SlaveBase *slave, QList<AudioCDEncoder *> &encoders)
37 {
38 encoders.append(new EncoderOpus(slave));
39 }
40 }
41
42 static const int bitrates[] = {6, 12, 24, 48, 64, 80, 96, 128, 160, 192, 256};
43
44 class EncoderOpus::Private
45 {
46 public:
47 int bitrate;
48 bool write_opus_comments;
49 bool waitingForWrite;
50 bool processHasExited;
51 QString lastErrorMessage;
52 uint lastSize;
53 KProcess *currentEncodeProcess = nullptr;
54 QTemporaryFile *tempFile = nullptr;
55 };
56
EncoderOpus(KIO::SlaveBase * slave)57 EncoderOpus::EncoderOpus(KIO::SlaveBase *slave)
58 : QObject()
59 , AudioCDEncoder(slave)
60 {
61 d = new Private();
62 d->waitingForWrite = false;
63 d->processHasExited = false;
64 d->lastSize = 0;
65 loadSettings();
66 }
67
~EncoderOpus()68 EncoderOpus::~EncoderOpus()
69 {
70 delete d;
71 }
72
getConfigureWidget(KConfigSkeleton ** manager) const73 QWidget *EncoderOpus::getConfigureWidget(KConfigSkeleton **manager) const
74 {
75 (*manager) = Settings::self();
76 auto config = new EncoderOpusConfig();
77 config->kcfg_opus_complexity->setRange(0, 10);
78 config->kcfg_opus_complexity->setSingleStep(1);
79 config->opus_bitrate_settings->hide();
80 return config;
81 }
82
init()83 bool EncoderOpus::init()
84 {
85 // Determine if opusenc is installed on the system or not.
86 if (QStandardPaths::findExecutable(QStringLiteral("opusenc")).isEmpty())
87 return false;
88
89 return true;
90 }
91
loadSettings()92 void EncoderOpus::loadSettings()
93 {
94 // Generate the command line arguments for the current settings
95 args.clear();
96
97 Settings *settings = Settings::self();
98
99 if (settings->opus_enc_complexity()) {
100 args.append(QStringLiteral("--comp"));
101 args.append(QStringLiteral("%1").arg(settings->opus_complexity()));
102 } else {
103 // Constant Bitrate Encoding
104 if (settings->set_opus_cbr()) {
105 args.append(QStringLiteral("--bitrate"));
106 args.append(QStringLiteral("%1").arg(bitrates[settings->opus_cbr()]));
107 d->bitrate = settings->opus_cbr();
108 args.append(QStringLiteral("--hard-cbr"));
109 };
110 // Constrained Variable Bitrate Encoding
111 if (settings->set_opus_cvbr()) {
112 args.append(QStringLiteral("--bitrate"));
113 args.append(QStringLiteral("%1").arg(bitrates[settings->opus_cvbr()]));
114 d->bitrate = bitrates[settings->opus_cvbr()];
115 args.append(QStringLiteral("--cvbr"));
116 };
117 // Average Variable Bitrate Encoding
118 if (settings->set_opus_vbr()) {
119 args.append(QStringLiteral("--bitrate"));
120 args.append(QStringLiteral("%1").arg(bitrates[settings->opus_vbr()]));
121 d->bitrate = bitrates[settings->opus_vbr()];
122 args.append(QStringLiteral("--vbr"));
123 };
124 };
125
126 d->write_opus_comments = settings->opus_comments();
127 }
128
size(long time_secs) const129 unsigned long EncoderOpus::size(long time_secs) const {
130 return (time_secs * d->bitrate * 1000) / 8;
131 }
132
readInit(long)133 long EncoderOpus::readInit(long /*size*/)
134 {
135 // Create KProcess
136 d->currentEncodeProcess = new KProcess();
137 d->tempFile = new QTemporaryFile(QDir::tempPath() + QLatin1String("/kaudiocd_XXXXXX") + QLatin1String(".opus"));
138 d->tempFile->open();
139 d->lastErrorMessage.clear();
140 d->processHasExited = false;
141
142 // --raw raw/pcm
143 // --raw-rate 44100 (because it is raw you have to specify this)
144 *(d->currentEncodeProcess) << QStringLiteral("opusenc") << QStringLiteral("--raw") << QStringLiteral("--raw-rate") << QStringLiteral("44100");
145 *(d->currentEncodeProcess) << args;
146 *d->currentEncodeProcess << trackInfo;
147
148 // Read in stdin, output to the temp file
149 *d->currentEncodeProcess << QStringLiteral("-") << d->tempFile->fileName();
150
151 // qCDebug(AUDIOCD_KIO_LOG) << args;
152
153 connect(d->currentEncodeProcess, &KProcess::readyReadStandardOutput, this, &EncoderOpus::receivedStdout);
154 connect(d->currentEncodeProcess, &KProcess::readyReadStandardError, this, &EncoderOpus::receivedStderr);
155 connect(d->currentEncodeProcess, QOverload<int, QProcess::ExitStatus>::of(&KProcess::finished), this, &EncoderOpus::processExited);
156
157 // Launch!
158 d->currentEncodeProcess->setOutputChannelMode(KProcess::SeparateChannels);
159 d->currentEncodeProcess->start();
160 return 0;
161 }
162
processExited(int exitCode,QProcess::ExitStatus)163 void EncoderOpus::processExited(int exitCode, QProcess::ExitStatus /*status*/)
164 {
165 qCDebug(AUDIOCD_KIO_LOG) << "Opusenc Encoding process exited with: " << exitCode;
166 d->processHasExited = true;
167 }
168
receivedStderr()169 void EncoderOpus::receivedStderr()
170 {
171 QByteArray error = d->currentEncodeProcess->readAllStandardError();
172 qCDebug(AUDIOCD_KIO_LOG) << "Opusenc stderr: " << error;
173 if (!d->lastErrorMessage.isEmpty())
174 d->lastErrorMessage += QLatin1Char('\t');
175 d->lastErrorMessage += QString::fromLocal8Bit(error);
176 }
177
receivedStdout()178 void EncoderOpus::receivedStdout()
179 {
180 QString output = QString::fromLocal8Bit(d->currentEncodeProcess->readAllStandardOutput());
181 qCDebug(AUDIOCD_KIO_LOG) << "Opusenc stdout: " << output;
182 }
183
read(qint16 * buf,int frames)184 long EncoderOpus::read(qint16 *buf, int frames)
185 {
186 if (!d->currentEncodeProcess)
187 return 0;
188 if (d->processHasExited)
189 return -1;
190
191 // Pipe the raw data to opusenc
192 char *cbuf = reinterpret_cast<char *>(buf);
193 d->currentEncodeProcess->write(cbuf, frames * 4);
194 // We can't return until the buffer has been written
195 d->currentEncodeProcess->waitForBytesWritten(-1);
196
197 // Determine the file size increase
198 QFileInfo file(d->tempFile->fileName());
199 uint change = file.size() - d->lastSize;
200 d->lastSize = file.size();
201 return change;
202 }
203
readCleanup()204 long EncoderOpus::readCleanup()
205 {
206 if (!d->currentEncodeProcess)
207 return 0;
208
209 // Let opusenc tag the first frame of the opus
210 d->currentEncodeProcess->closeWriteChannel();
211 d->currentEncodeProcess->waitForFinished(-1);
212
213 // Now copy the file out of the temp into kio
214 QFile file(d->tempFile->fileName());
215 if (file.open(QIODevice::ReadOnly)) {
216 char data[1024];
217 while (!file.atEnd()) {
218 uint read = file.read(data, 1024);
219 QByteArray output(data, read);
220 ioslave->data(output);
221 }
222 file.close();
223 }
224
225 // cleanup the process and temp
226 delete d->currentEncodeProcess;
227 delete d->tempFile;
228 d->lastSize = 0;
229
230 return 0;
231 }
232
fillSongInfo(KCDDB::CDInfo info,int track,const QString & comment)233 void EncoderOpus::fillSongInfo(KCDDB::CDInfo info, int track, const QString &comment)
234 {
235 trackInfo.clear();
236
237 if (!d->write_opus_comments)
238 return;
239
240 trackInfo.append(QStringLiteral("--album"));
241 trackInfo.append(info.get(Title).toString());
242
243 trackInfo.append(QStringLiteral("--artist"));
244 trackInfo.append(info.track(track - 1).get(Artist).toString());
245
246 trackInfo.append(QStringLiteral("--title"));
247 trackInfo.append(info.track(track - 1).get(Title).toString());
248
249 trackInfo.append(QStringLiteral("--date"));
250 trackInfo.append(QDate(info.get(Year).toInt(), 1, 1).toString(Qt::ISODate));
251
252 trackInfo.append(QStringLiteral("--comment"));
253 trackInfo.append(QStringLiteral("DESCRIPTION=") + comment);
254
255 trackInfo.append(QStringLiteral("--comment"));
256 trackInfo.append(QStringLiteral("TRACKNUMBER=") + QString::number(track));
257
258 trackInfo.append(QStringLiteral("--genre"));
259 trackInfo.append(QStringLiteral("%1").arg(info.get(Genre).toString()));
260 }
261
lastErrorMessage() const262 QString EncoderOpus::lastErrorMessage() const
263 {
264 return d->lastErrorMessage;
265 }
266