summaryrefslogtreecommitdiffstats
path: root/src/encoder
diff options
context:
space:
mode:
authorStéphane Lepin <stephane.lepin@gmail.com>2017-08-15 11:00:34 +0200
committerStéphane Lepin <stephane.lepin@gmail.com>2018-12-31 15:36:46 +0100
commit792e3e6d9a79560615960c855eaf9c8772ade299 (patch)
treea0b810a6488417a7f773400b396894640fc7e832 /src/encoder
parent18e9314383de7ee83af146a44b380c80298b0d55 (diff)
Opus encoder
Diffstat (limited to 'src/encoder')
-rw-r--r--src/encoder/encoder.cpp41
-rw-r--r--src/encoder/encoderopus.cpp463
-rw-r--r--src/encoder/encoderopus.h58
-rw-r--r--src/encoder/encoderopussettings.cpp155
-rw-r--r--src/encoder/encoderopussettings.h60
5 files changed, 766 insertions, 11 deletions
diff --git a/src/encoder/encoder.cpp b/src/encoder/encoder.cpp
index 87b9522494..77cbe87f58 100644
--- a/src/encoder/encoder.cpp
+++ b/src/encoder/encoder.cpp
@@ -22,11 +22,17 @@
#endif
#include "encoder/encoderwave.h"
#include "encoder/encodersndfileflac.h"
+
#include "encoder/encodermp3settings.h"
#include "encoder/encodervorbissettings.h"
#include "encoder/encoderwavesettings.h"
#include "encoder/encoderflacsettings.h"
+#ifdef __OPUS__
+#include "encoder/encoderopus.h"
+#include "encoder/encoderopussettings.h"
+#endif
+
#include <QList>
EncoderFactory EncoderFactory::factory;
@@ -38,11 +44,15 @@ const EncoderFactory& EncoderFactory::getFactory()
EncoderFactory::EncoderFactory() {
// Add new supported formats here. Also modify the getNewEncoder/getEncoderSettings method.
- m_formats.append(Encoder::Format("WAV PCM",ENCODING_WAVE, true));
- m_formats.append(Encoder::Format("AIFF PCM",ENCODING_AIFF, true));
+ m_formats.append(Encoder::Format("WAV PCM", ENCODING_WAVE, true));
+ m_formats.append(Encoder::Format("AIFF PCM", ENCODING_AIFF, true));
m_formats.append(Encoder::Format("FLAC", ENCODING_FLAC, true));
- m_formats.append(Encoder::Format("MP3",ENCODING_MP3, false));
- m_formats.append(Encoder::Format("OGG Vorbis",ENCODING_OGG, false));
+ m_formats.append(Encoder::Format("MP3", ENCODING_MP3, false));
+ m_formats.append(Encoder::Format("OGG Vorbis", ENCODING_OGG, false));
+
+#ifdef __OPUS__
+ m_formats.append(Encoder::Format("Opus", ENCODING_OPUS, false));
+#endif
}
const QList<Encoder::Format> EncoderFactory::getFormats() const
@@ -86,20 +96,27 @@ EncoderPointer EncoderFactory::getNewEncoder(Encoder::Format format,
pEncoder = std::make_shared<EncoderSndfileFlac>(pCallback);
pEncoder->setEncoderSettings(EncoderFlacSettings(pConfig));
} else if (format.internalName == ENCODING_MP3) {
- #ifdef __FFMPEGFILE_ENCODERS__
+#ifdef __FFMPEGFILE_ENCODERS__
pEncoder = std::make_shared<EncoderFfmpegMp3>(pCallback);
- #else
+#else
pEncoder = std::make_shared<EncoderMp3>(pCallback);
- #endif
+#endif
pEncoder->setEncoderSettings(EncoderMp3Settings(pConfig));
} else if (format.internalName == ENCODING_OGG) {
- #ifdef __FFMPEGFILE_ENCODERS__
+#ifdef __FFMPEGFILE_ENCODERS__
pEncoder = std::make_shared<EncoderFfmpegVorbis>(pCallback);
- #else
+#else
pEncoder = std::make_shared<EncoderVorbis>(pCallback);
- #endif
+#endif
pEncoder->setEncoderSettings(EncoderVorbisSettings(pConfig));
- } else {
+ }
+#ifdef __OPUS__
+ else if (format.internalName == ENCODING_OPUS) {
+ pEncoder = std::make_shared<EncoderOpus>(pCallback);
+ pEncoder->setEncoderSettings(EncoderOpusSettings(pConfig));
+ }
+#endif
+ else {
qWarning() << "Unsupported format requested! " << format.internalName;
DEBUG_ASSERT(false);
pEncoder = std::make_shared<EncoderWave>(pCallback);
@@ -121,6 +138,8 @@ EncoderSettingsPointer EncoderFactory::getEncoderSettings(Encoder::Format format
return std::make_shared<EncoderMp3Settings>(pConfig);
} else if (format.internalName == ENCODING_OGG) {
return std::make_shared<EncoderVorbisSettings>(pConfig);
+ } else if (format.internalName == ENCODING_OPUS) {
+ return std::make_shared<EncoderOpusSettings>(pConfig);
} else {
qWarning() << "Unsupported format requested! " << format.internalName;
DEBUG_ASSERT(false);
diff --git a/src/encoder/encoderopus.cpp b/src/encoder/encoderopus.cpp
new file mode 100644
index 0000000000..f932169137
--- /dev/null
+++ b/src/encoder/encoderopus.cpp
@@ -0,0 +1,463 @@
+// encoderopus.cpp
+// Create on August 15th 2017 by Palakis
+
+#include <stdlib.h>
+#include <QByteArray>
+#include <QMapIterator>
+#include <QtGlobal>
+
+#include "encoder/encoderopussettings.h"
+#include "engine/sidechain/enginesidechain.h"
+#include "util/logger.h"
+
+#include "encoder/encoderopus.h"
+
+namespace {
+// From libjitsi's Opus encoder:
+// 1 byte TOC + maximum frame size (1275)
+// See https://tools.ietf.org/html/rfc6716#section-3.2
+const int kMaxOpusBufferSize = 1+1275;
+// Opus frame duration in milliseconds. Fixed to 60ms
+const int kOpusFrameMs = 60;
+// Opus only supports these samplerates
+const int kOpusSamplerate = 48000;
+
+const mixxx::Logger kLogger("EncoderOpus");
+}
+
+EncoderOpus::EncoderOpus(EncoderCallback* pCallback)
+ : m_bitrate(0),
+ m_bitrateMode(0),
+ m_channels(0),
+ m_samplerate(0),
+ m_readRequired(0),
+ m_pCallback(pCallback),
+ m_fifoBuffer(EngineSideChain::SIDECHAIN_BUFFER_SIZE * 2),
+ m_pFifoChunkBuffer(nullptr),
+ m_pOpus(nullptr),
+ m_opusDataBuffer(kMaxOpusBufferSize),
+ m_header_write(false),
+ m_packetNumber(0),
+ m_granulePos(0)
+{
+ // Regarding m_pFifoBuffer:
+ // Size the input FIFO buffer with twice the maximum possible sample count that can be
+ // processed at once, to avoid skipping frames or waiting for the required sample count
+ // and encode at a regular pace.
+ // This is set to the buffer size of the sidechain engine because
+ // Recording (which uses this engine) sends more samples at once to the encoder than
+ // the Live Broadcasting implementation
+
+ m_opusComments.insert("ENCODER", "mixxx/libopus");
+ ogg_stream_init(&m_oggStream, qrand());
+}
+
+EncoderOpus::~EncoderOpus() {
+ if (m_pOpus) {
+ opus_encoder_destroy(m_pOpus);
+ }
+
+ ogg_stream_clear(&m_oggStream);
+ delete m_pFifoChunkBuffer;
+}
+
+void EncoderOpus::setEncoderSettings(const EncoderSettings& settings) {
+ m_bitrate = settings.getQuality();
+ m_bitrateMode = settings.getSelectedOption(EncoderOpusSettings::BITRATE_MODE_GROUP);
+ switch (settings.getChannelMode()) {
+ case EncoderSettings::ChannelMode::MONO:
+ m_channels = 1;
+ break;
+ case EncoderSettings::ChannelMode::STEREO:
+ m_channels = 2;
+ break;
+ case EncoderSettings::ChannelMode::AUTOMATIC:
+ m_channels = 2;
+ break;
+ }
+}
+
+int EncoderOpus::initEncoder(int samplerate, QString errorMessage) {
+ Q_UNUSED(errorMessage);
+
+ if (samplerate != kOpusSamplerate) {
+ kLogger.warning() << "initEncoder failed: samplerate not supported by Opus";
+
+ QString invalidSamplerate = QObject::tr(
+ "Using Opus at samplerates other than 48 kHz "
+ "is not supported by the Opus encoder. Please use "
+ "48000 Hz in \"Sound Hardware\" preferences "
+ "or switch to a different encoding.");
+
+ ErrorDialogProperties* props = ErrorDialogHandler::instance()->newDialogProperties();
+ props->setType(DLG_WARNING);
+ props->setTitle(QObject::tr("Encoder"));
+ props->setText(invalidSamplerate);
+ props->setKey(invalidSamplerate);
+ ErrorDialogHandler::instance()->requestErrorDialog(props);
+ return -1;
+ }
+ m_samplerate = samplerate;
+
+ int createResult = 0;
+ m_pOpus = opus_encoder_create(m_samplerate, m_channels, OPUS_APPLICATION_AUDIO, &createResult);
+
+ if (createResult != OPUS_OK) {
+ kLogger.warning() << "opus_encoder_create failed:" << opusErrorString(createResult);
+ return -1;
+ }
+
+ // Optimize encoding for high-quality music
+ opus_encoder_ctl(m_pOpus, OPUS_SET_COMPLEXITY(10)); // Highest setting
+ opus_encoder_ctl(m_pOpus, OPUS_SET_SIGNAL(OPUS_SIGNAL_MUSIC));
+
+ if(m_bitrateMode == OPUS_BITRATE_CONSTRAINED_VBR) {
+ // == Constrained VBR ==
+ // Default mode, gives the best quality/bitrate compromise
+ opus_encoder_ctl(m_pOpus, OPUS_SET_BITRATE(m_bitrate * 1000)); // convert to bits/second
+ opus_encoder_ctl(m_pOpus, OPUS_SET_VBR(1)); // default value in libopus
+ opus_encoder_ctl(m_pOpus, OPUS_SET_VBR_CONSTRAINT(1)); // Constrained VBR
+ } else if(m_bitrateMode == OPUS_BITRATE_CBR) {
+ // == CBR (quality loss at low bitrates) ==
+ opus_encoder_ctl(m_pOpus, OPUS_SET_BITRATE(m_bitrate * 1000)); // convert to bits/second
+ opus_encoder_ctl(m_pOpus, OPUS_SET_VBR(0));
+ } else if(m_bitrateMode == OPUS_BITRATE_VBR) {
+ // == Full VBR ==
+ opus_encoder_ctl(m_pOpus, OPUS_SET_VBR(1));
+ opus_encoder_ctl(m_pOpus, OPUS_SET_VBR_CONSTRAINT(0)); // Unconstrained VBR
+ }
+
+ double samplingPeriodMs = ( 1.0 / ((double)m_samplerate) ) * 1000.0;
+ double samplesPerChannel = kOpusFrameMs / samplingPeriodMs;
+
+ m_readRequired = samplesPerChannel * m_channels;
+ m_pFifoChunkBuffer = new mixxx::SampleBuffer(m_readRequired);
+ initStream();
+
+ return 0;
+}
+
+void EncoderOpus::initStream() {
+ ogg_stream_clear(&m_oggStream);
+ ogg_stream_init(&m_oggStream, getSerial());
+ m_header_write = true;
+ m_granulePos = 0;
+ m_packetNumber = 0;
+
+ pushHeaderPacket();
+ pushTagsPacket();
+}
+
+// Binary header construction is done manually to properly
+// handle endianness of multi-byte number fields
+void EncoderOpus::pushHeaderPacket() {
+ // Opus identification header
+ // Format from https://tools.ietf.org/html/rfc7845.html#section-5.1
+
+ // Header buffer size:
+ // - Magic signature: 8 bytes
+ // - Version: 1 byte
+ // - Channel count: 1 byte
+ // - Pre-skip: 2 bytes
+ // - Samplerate: 4 bytes
+ // - Output Gain: 2 bytes
+ // - Mapping family: 1 byte
+ // - Channel mapping table: ignored
+ // Total: 19 bytes
+ const int frameSize = 19;
+ QByteArray frame;
+
+ // Magic signature (8 bytes)
+ frame.append("OpusHead", 8);
+
+ // Version number (1 byte, fixed to 1)
+ frame.append(0x01);
+
+ // Channel count (1 byte)
+ frame.append((unsigned char)m_channels);
+
+ // Pre-skip (2 bytes, little-endian)
+ int preskip = 0;
+ opus_encoder_ctl(m_pOpus, OPUS_GET_LOOKAHEAD(&preskip));
+ for (int x = 0; x < 2; x++) {
+ unsigned char preskipByte = (preskip >> (x*8)) & 0xFF;
+ frame.append(preskipByte);
+ }
+
+ // Sample rate (4 bytes, little endian)
+ for (int x = 0; x < 4; x++) {
+ unsigned char samplerateByte = (m_samplerate >> (x*8)) & 0xFF;
+ frame.append(samplerateByte);
+ }
+
+ // Output gain (2 bytes, little-endian, fixed to 0)
+ frame.append((char)0x00);
+ frame.append((char)0x00);
+
+ // Channel mapping (1 byte, fixed to 0, means one stream)
+ frame.append((char)0x00);
+
+ // Ignore channel mapping table
+
+ // Assert the built frame is of correct size
+ int actualFrameSize = frame.size();
+ if (actualFrameSize != frameSize) {
+ kLogger.warning() <<
+ QString("pushHeaderPacket: wrong frame size! expected: %1 - actual: %2")
+ .arg(frameSize).arg(actualFrameSize);
+ }
+
+ // Push finished header to stream
+ ogg_packet packet;
+ packet.b_o_s = 1;
+ packet.e_o_s = 0;
+ packet.granulepos = 0;
+ packet.packetno = m_packetNumber++;
+ packet.packet = (unsigned char*)frame.data();
+ packet.bytes = frameSize;
+
+ if (ogg_stream_packetin(&m_oggStream, &packet) != 0) {
+ // return value != 0 means an internal error happened
+ kLogger.warning() <<
+ "pushHeaderPacket: failed to send packet to Ogg stream";
+ }
+}
+
+void EncoderOpus::pushTagsPacket() {
+ // Opus comment header
+ // Format from https://tools.ietf.org/html/rfc7845.html#section-5.2
+
+ QByteArray combinedComments;
+ int commentCount = 0;
+
+ const char* vendorString = opus_get_version_string();
+ int vendorStringLength = strlen(vendorString);
+
+ // == Compute tags frame size ==
+ // - Magic signature: 8 bytes
+ // - Vendor string length: 4 bytes
+ // - Vendor string: dynamic size
+ // - Comment list length: 4 bytes
+ int frameSize = 8 + 4 + vendorStringLength + 4;
+ // - Comment list: dynamic size
+ QMapIterator<QString, QString> iter(m_opusComments);
+ while(iter.hasNext()) {
+ iter.next();
+ QString comment = iter.key() + "=" + iter.value();
+ QByteArray commentBytes = comment.toUtf8();
+ int commentBytesLength = commentBytes.size();
+
+ // One comment is:
+ // - 4 bytes of string length
+ // - string data
+
+ // Add comment length field and data to comments "list"
+ for (int x = 0; x < 4; x++) {
+ unsigned char fieldValue = (commentBytesLength >> (x*8)) & 0xFF;
+ combinedComments.append(fieldValue);
+ }
+ combinedComments.append(commentBytes);
+
+ // Don't forget to include this comment in the overall size calculation
+ frameSize += (4 + commentBytesLength);
+ commentCount++;
+ }
+
+ // == Actual frame building ==
+ QByteArray frame;
+
+ // Magic signature (8 bytes)
+ frame.append("OpusTags", 8);
+
+ // Vendor string (mandatory)
+ // length field (4 bytes, little-endian) + actual string
+ // Write length field
+ for (int x = 0; x < 4; x++) {
+ unsigned char lengthByte = (vendorStringLength >> (x*8)) & 0xFF;
+ frame.append(lengthByte);
+ }
+ // Write string
+ frame.append(vendorString, vendorStringLength);
+
+ // Number of comments (4 bytes, little-endian)
+ for (int x = 0; x < 4; x++) {
+ unsigned char commentCountByte = (commentCount >> (x*8)) & 0xFF;
+ frame.append(commentCountByte);
+ }
+
+ // Comment list (dynamic size)
+ int commentListLength = combinedComments.size();
+ frame.append(combinedComments.constData(), commentListLength);
+
+ // Assert the built frame is of correct size
+ int actualFrameSize = frame.size();
+ if (actualFrameSize != frameSize) {
+ kLogger.warning() <<
+ QString("pushTagsPacket: wrong frame size! expected: %1 - actual: %2")
+ .arg(frameSize).arg(actualFrameSize);
+ }
+
+ // Push finished tags frame to stream
+ ogg_packet packet;
+ packet.b_o_s = 0;
+ packet.e_o_s = 0;
+ packet.granulepos = 0;
+ packet.packetno = m_packetNumber++;
+ packet.packet = (unsigned char*)frame.data();
+ packet.bytes = frameSize;
+
+ if (ogg_stream_packetin(&m_oggStream, &packet) != 0) {
+ // return value != 0 means an internal error happened
+ kLogger.warning() <<
+ "pushTagsPacket: failed to send packet to Ogg stream";
+ }
+}
+
+void EncoderOpus::encodeBuffer(const CSAMPLE *samples, const int size) {
+ if (!m_pOpus) {
+ return;
+ }
+
+ int writeRequired = size;
+ int writeAvailable = m_fifoBuffer.writeAvailable();
+ if (writeRequired > writeAvailable) {
+ kLogger.warning() << "FIFO buffer too small, loosing samples!"
+ << "required:" << writeRequired
+ << "; available: " << writeAvailable;
+ }
+
+ int writeCount = math_min(writeRequired, writeAvailable);
+ if (writeCount > 0) {
+ m_fifoBuffer.write(samples, writeCount);
+ }
+
+ processFIFO();
+}
+
+void EncoderOpus::processFIFO() {
+ while (m_fifoBuffer.readAvailable() >= m_readRequired) {
+ m_fifoBuffer.read(m_pFifoChunkBuffer->data(), m_readRequired);
+
+ if ((m_readRequired % m_channels) != 0) {
+ kLogger.warning() << "processFIFO: channel count doesn't match chunk size";
+ }
+
+ int samplesPerChannel = m_readRequired / m_channels;
+ int result = opus_encode_float(m_pOpus,
+ m_pFifoChunkBuffer->data(), samplesPerChannel,
+ m_opusDataBuffer.data(), kMaxOpusBufferSize);
+
+ if (result < 1) {
+ kLogger.warning() << "opus_encode_float failed:" << opusErrorString(result);
+ return;
+ }
+
+ ogg_packet packet;
+ packet.b_o_s = 0;
+ packet.e_o_s = 0;
+ packet.granulepos = m_granulePos;
+ packet.packetno = m_packetNumber;
+ packet.packet = m_opusDataBuffer.data();
+ packet.bytes = result;
+
+ m_granulePos += samplesPerChannel;
+ m_packetNumber += 1;
+
+ writePage(&packet);
+ }
+}
+
+void EncoderOpus::writePage(ogg_packet* pPacket) {
+ if (!pPacket) {
+ return;
+ }
+
+ // Push headers prepared by initStream if not already done
+ if (m_header_write) {
+ while (true) {
+ int result = ogg_stream_flush(&m_oggStream, &m_oggPage);
+ if (result == 0)
+ break;
+
+ kLogger.debug() << "pushing headers to output";
+ m_pCallback->write(m_oggPage.header, m_oggPage.body,
+ m_oggPage.header_len, m_oggPage.body_len);
+ }
+ m_header_write = false;
+ }
+
+ // Push Opus Ogg packets to the stream
+ if (ogg_stream_packetin(&m_oggStream, pPacket) != 0) {
+ // return value != 0 means an internal error happened
+ kLogger.warning() <<
+ "writePage: failed to send packet to Ogg stream";
+ }
+
+ // Try to send available Ogg pages to the output
+ do {
+ if (ogg_stream_pageout(&m_oggStream, &m_oggPage) == 0) {
+ break;
+ }
+
+ m_pCallback->write(m_oggPage.header, m_oggPage.body,
+ m_oggPage.header_len, m_oggPage.body_len);
+ } while(!ogg_page_eos(&m_oggPage));
+}
+
+void EncoderOpus::updateMetaData(const QString& artist, const QString& title, const QString& album) {
+ m_opusComments.insert("ARTIST", artist);
+ m_opusComments.insert("TITLE", title);
+ m_opusComments.insert("ALBUM", album);
+}
+
+void EncoderOpus::flush() {
+ // At this point there may still be samples in the FIFO buffer
+ processFIFO();
+}
+
+QString EncoderOpus::opusErrorString(int error) {
+ QString errorString = "";
+ switch (error) {
+ case OPUS_OK:
+ errorString = "OPUS_OK";
+ break;
+ case OPUS_BAD_ARG:
+ errorString = "OPUS_BAD_ARG";
+ break;
+ case OPUS_BUFFER_TOO_SMALL:
+ errorString = "OPUS_BUFFER_TOO_SMALL";
+ break;
+ case OPUS_INTERNAL_ERROR:
+ errorString = "OPUS_INTERNAL_ERROR";
+ break;
+ case OPUS_INVALID_PACKET:
+ errorString = "OPUS_INVALID_PACKET";
+ break;
+ case OPUS_UNIMPLEMENTED:
+ errorString = "OPUS_UNIMPLEMENTED";
+ break;
+ case OPUS_INVALID_STATE:
+ errorString = "OPUS_INVALID_STATE";
+ break;
+ case OPUS_ALLOC_FAIL:
+ errorString = "OPUS_ALLOC_FAIL";
+ break;
+ default:
+ return "Unknown error";
+ }
+ return errorString + (QString(" (%1)").arg(error));
+}
+
+int EncoderOpus::getSerial() {
+ static int prevSerial = 0;
+
+ int serial;
+ do {
+ serial = qrand();
+ } while(prevSerial == serial);
+
+ prevSerial = serial;
+ kLogger.debug() << "RETURNING SERIAL " << serial;
+ return serial;
+}
+
diff --git a/src/encoder/encoderopus.h b/src/encoder/encoderopus.h
new file mode 100644
index 0000000000..1122a3c2e7
--- /dev/null
+++ b/src/encoder/encoderopus.h
@@ -0,0 +1,58 @@
+// encoderopus.h
+// Create on August 15th 2017 by Palakis
+
+#ifndef ENCODER_ENCODEROPUS_H
+#define ENCODER_ENCODEROPUS_H
+
+#include <QMap>
+#include <QString>
+#include <QVector>
+
+#include <ogg/ogg.h>
+#include <opus/opus.h>
+
+#include "encoder/encoder.h"
+#include "encoder/encodercallback.h"
+#include "util/fifo.h"
+#include "util/sample.h"
+#include "util/samplebuffer.h"
+
+class EncoderOpus: public Encoder {
+ public:
+ EncoderOpus(EncoderCallback* pCallback = nullptr);
+ virtual ~EncoderOpus();
+
+ int initEncoder(int samplerate, QString errorMessage) override;
+ void encodeBuffer(const CSAMPLE *samples, const int size) override;
+ void updateMetaData(const QString& artist, const QString& title, const QString& album) override;
+ void flush() override;
+ void setEncoderSettings(const EncoderSettings& settings) override;
+
+ private:
+ static QString opusErrorString(int error);
+ static int getSerial();
+ void initStream();
+ void pushHeaderPacket();
+ void pushTagsPacket();
+ void writePage(ogg_packet* pPacket);
+ void processFIFO();
+
+ int m_bitrate;
+ int m_bitrateMode;
+ int m_channels;
+ int m_samplerate;
+ int m_readRequired;
+ EncoderCallback* m_pCallback;
+ FIFO<CSAMPLE> m_fifoBuffer;
+ mixxx::SampleBuffer* m_pFifoChunkBuffer;
+ OpusEncoder* m_pOpus;
+ QVector<unsigned char> m_opusDataBuffer;
+ ogg_stream_state m_oggStream;
+ ogg_page m_oggPage;
+ bool m_header_write;
+ int m_packetNumber;
+ ogg_int64_t m_granulePos;
+ QMap<QString, QString> m_opusComments;
+};
+
+#endif // ENCODER_ENCODEROPUS_H
diff --git a/src/encoder/encoderopussettings.cpp b/src/encoder/encoderopussettings.cpp
new file mode 100644
index 0000000000..ec47fda2ce
--- /dev/null
+++ b/src/encoder/encoderopussettings.cpp
@@ -0,0 +1,155 @@
+// encoderopussettings.cpp
+// Create on August 15th 2017 by Palakis
+
+#include <QMap>
+
+#include "encoder/encoderopussettings.h"
+#include "recording/defs_recording.h"
+#include "util/logger.h"
+
+namespace {
+const int kDefaultQualityIndex = 6;
+const char* kQualityKey = "Opus_Quality";
+const mixxx::Logger kLogger("EncoderOpusSettings");
+}
+
+const QString EncoderOpusSettings::BITRATE_MODE_GROUP = "Opus_BitrateMode";
+
+EncoderOpusSettings::EncoderOpusSettings(UserSettingsPointer pConfig)
+ : m_pConfig(pConfig) {
+ m_qualList.append(32);
+ m_qualList.append(48);
+ m_qualList.append(64);
+ m_qualList.append(80);
+ m_qualList.append(96);
+ m_qualList.append(112);
+ m_qualList.append(128);
+ m_qualList.append(160);
+ m_qualList.append(192);
+ m_qualList.append(224);
+ m_qualList.append(256);
+ m_qualList.append(320);
+ m_qualList.append(510); // Max Opus bitrate
+
+ QMap<int, QString> modes;
+ modes.insert(OPUS_BITRATE_CONSTRAINED_VBR,
+ QObject::tr("Constrained VBR"));
+ modes.insert(OPUS_BITRATE_CBR, QObject::tr("CBR"));
+ modes.insert(OPUS_BITRATE_VBR, QObject::tr("Full VBR (bitrate ignored)"));
+
+ // OptionsGroup needs a QList<string> for the option list. Using QMap::values()
+ // ensures that the returned QList<string> will be sorted in ascending key order, which
+ // is what we want to be able to match OPUS_BITRATE_* number defines to the right mode
+ // in EncoderOpus.
+ m_radioList.append(OptionsGroup(
+ QObject::tr("Bitrate Mode"), BITRATE_MODE_GROUP, modes.values()));
+}
+
+EncoderOpusSettings::~EncoderOpusSettings() {
+}
+
+QList<int> EncoderOpusSettings::getQualityValues() const {
+ return m_qualList;
+}
+
+void EncoderOpusSettings::setQualityByValue(int qualityValue) {
+ // Same behavior as Vorbis: Opus does not have a fixed set of
+ // bitrates, so we can accept any value.
+ int indexValue;
+ if (m_qualList.contains(qualityValue)) {
+ indexValue = m_qualList.indexOf(qualityValue);
+ } else {
+ // If we let the user write a bitrate value, this would allow to save such value.
+ indexValue = qualityValue;
+ }
+
+ m_pConfig->setValue<int>(ConfigKey(RECORDING_PREF_KEY, kQualityKey), indexValue);
+}
+
+void EncoderOpusSettings::setQualityByIndex(int qualityIndex) {
+ if (qualityIndex >= 0 && qualityIndex < m_qualList.size()) {
+ m_pConfig->setValue<int>(ConfigKey(RECORDING_PREF_KEY, kQualityKey), qualityIndex);
+ } else {
+ kLogger.warning() << "Invalid qualityIndex given to EncoderVorbisSettings: "
+ << qualityIndex << ". Ignoring it";
+ }
+}
+int EncoderOpusSettings::getQuality() const {
+ int qualityIndex = m_pConfig->getValue(
+ ConfigKey(RECORDING_PREF_KEY, kQualityKey), kDefaultQualityIndex);
+
+ if (qualityIndex >= 0 && qualityIndex < m_qualList.size()) {
+ return m_qualList.at(qualityIndex);
+ } else {
+ // Same as Vorbis: Opus does not have a fixed set of
+ // bitrates, so we can accept any value.
+ return qualityIndex;
+ }
+}
+
+int EncoderOpusSettings::getQualityIndex() const {
+ int qualityIndex = m_pConfig->getValue(
+ ConfigKey(RECORDING_PREF_KEY, kQualityKey), kDefaultQualityIndex);
+
+ if (qualityIndex >= 0 && qualityIndex < m_qualList.size()) {
+ return qualityIndex;
+ } else {
+ kLogger.warning() << "Invalid qualityIndex in EncoderVorbisSettings "
+ << qualityIndex << "(Max is:"
+ << m_qualList.size() << ") . Ignoring it and"
+ << "returning default which is" << kDefaultQualityIndex;
+ return kDefaultQualityIndex;
+ }
+}
+
+QList<EncoderSettings::OptionsGroup> EncoderOpusSettings::getOptionGroups() const {
+ return m_radioList;
+}
+
+void EncoderOpusSettings::setGroupOption(QString groupCode, int optionIndex) {
+ bool found = false;
+ for (OptionsGroup group : m_radioList) {
+ if (groupCode == group.groupCode) {
+ found = true;
+ if (optionIndex < group.controlNames.size() || optionIndex == 1) {
+ m_pConfig->set(
+ ConfigKey(RECORDING_PREF_KEY, BITRATE_MODE_GROUP),
+ ConfigValue(optionIndex));
+ } else {
+ kLogger.warning()
+ << "Received an index out of range for:"
+ << groupCode << ", index:" << optionIndex;
+ }
+ }
+ }
+
+ if (!found) {
+ kLogger.warning()
+ << "Received an unknown groupCode on setGroupOption:" << groupCode;
+ }
+}
+
+int EncoderOpusSettings::getSelectedOption(QString groupCode) const {
+ int value = m_pConfig->getValue(
+ ConfigKey(RECORDING_PREF_KEY, BITRATE_MODE_GROUP), 0);
+
+ bool found = false;
+ for (OptionsGroup group : m_radioList) {
+ if (groupCode == group.groupCode) {
+ found = true;
+ if (value >= group.controlNames.size() && value > 1) {
+ kLogger.warning()
+ << "Value saved for" << groupCode
+ << "on preferences is out of range" << value << ". Returning 0";
+ value = 0;
+ }
+ }
+ }
+
+ if (!found) {
+ kLogger.warning()
+ << "Received an unknown groupCode on getSelectedOption:" << groupCode;
+ }
+
+ return value;
+}
diff --git a/src/encoder/encoderopussettings.h b/src/encoder/encoderopussettings.h
new file mode 100644
index 0000000000..576e4efc66
--- /dev/null
+++ b/src/encoder/encoderopussettings.h
@@ -0,0 +1,60 @@
+// encoderopussettings.h
+// Create on August 15th 2017 by Palakis
+
+#ifndef ENCODER_ENCODEROPUSSETTINGS_H
+#define ENCODER_ENCODEROPUSSETTINGS_H
+
+#include "encoder/encodersettings.h"
+#include "encoder/encoder.h"
+
+#define OPUS_BITRATE_MODES_COUNT 3
+#define OPUS_BITRATE_CONSTRAINED_VBR 0
+#define OPUS_BITRATE_CBR 1
+#define OPUS_BITRATE_VBR 2
+
+class EncoderOpusSettings: public EncoderSettings {
+ public:
+ EncoderOpusSettings(UserSettingsPointer pConfig);
+ virtual ~EncoderOpusSettings();
+
+ // Indicates that it uses the quality slider section of the preferences
+ bool usesQualitySlider() const override {
+ return true;
+ }
+ // Indicates that it uses the compression slider section of the preferences
+ bool usesCompressionSlider() const override {
+ return false;
+ }
+ // Indicates that it uses the radio button section of the preferences.
+ bool usesOptionGroups() const override {
+ return true;
+ }
+
+ // Returns the list of quality values that it supports, to assign them to the slider
+ QList<int> getQualityValues() const override;
+ // Sets the quality value by its value
+ void setQualityByValue(int qualityValue) override;
+ // Sets the quality value by its index
+ void setQualityByIndex(int qualityIndex) override;
+ // Returns the current quality value
+ int getQuality() const override;
+ int getQualityIndex() const override;
+
+ // Returns the list of radio options to show to the user
+ QList<OptionsGroup> getOptionGroups() const override;
+ // Selects the option by its index. If it is a single-element option,
+ // index 0 means disabled and 1 enabled.
+ void setGroupOption(QString groupCode, int optionIndex) override;
+ // Return the selected option of the group. If it is a single-element option,
+ // 0 means disabled and 1 enabled.
+ int getSelectedOption(QString groupCode) const override;
+
+ static const QString BITRATE_MODE_GROUP;
+
+ private:
+ UserSettingsPointer m_pConfig;
+ QList<int> m_qualList;
+ QList<OptionsGroup> m_radioList;
+};
+
+#endif // ENCODER_ENCODEROPUSSETTINGS_H