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