diff --git a/appveyor.yml b/appveyor.yml index 28a034ef173..b6d09873616 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -50,7 +50,7 @@ for: install: - sudo apt-get update - - sudo apt-get -y install gdb libavformat-dev libchromaprint-dev libfaad-dev libflac-dev libid3tag0-dev libmad0-dev libmodplug-dev libmp3lame-dev libmp4v2-dev libopusfile-dev libportmidi-dev libprotobuf-dev libqt5opengl5-dev libqt5sql5-sqlite libqt5svg5-dev librubberband-dev libshout3-dev libsndfile1-dev libsqlite3-dev libtag1-dev libupower-glib-dev libusb-1.0-0-dev libwavpack-dev portaudio19-dev protobuf-compiler qt5-default qtscript5-dev libqt5x11extras5-dev scons qtkeychain-dev liblilv-dev libsoundtouch-dev + - sudo apt-get -y install gdb libavformat-dev libchromaprint-dev libfaad-dev libflac-dev libid3tag0-dev libmad0-dev libmodplug-dev libmp3lame-dev libmp4v2-dev libopus-dev libopusfile-dev libportmidi-dev libprotobuf-dev libqt5opengl5-dev libqt5sql5-sqlite libqt5svg5-dev librubberband-dev libshout3-dev libsndfile1-dev libsqlite3-dev libtag1-dev libupower-glib-dev libusb-1.0-0-dev libwavpack-dev portaudio19-dev protobuf-compiler qt5-default qtscript5-dev libqt5x11extras5-dev scons qtkeychain-dev liblilv-dev libsoundtouch-dev build_script: - scons -j4 test=1 mad=1 faad=1 ffmpeg=1 opus=1 modplug=1 wv=1 hss1394=0 virtualize=0 debug_assertions_fatal=1 verbose=0 localecompare=1 diff --git a/build/features.py b/build/features.py index 399ce035d37..495a25e114e 100644 --- a/build/features.py +++ b/build/features.py @@ -747,20 +747,27 @@ def configure(self, build, conf): # Support for Opus (RFC 6716) # More info http://http://www.opus-codec.org/ - if not conf.CheckLib(['opusfile', 'libopusfile']): + if not conf.CheckLib(['opusfile', 'libopusfile']) or not conf.CheckLib(['opus', 'libopus']): if explicit: - raise Exception('Could not find libopusfile.') + raise Exception('Could not find opus or libopusfile.') else: build.flags['opus'] = 0 return - build.env.Append(CPPDEFINES='__OPUS__') + if build.platform_is_windows and build.static_dependencies: + for opus_lib in ['celt', 'silk_common', 'silk_float']: + if not conf.CheckLib(opus_lib): + raise Exception('Missing opus static library %s -- exiting' % opus_lib) if build.platform_is_linux or build.platform_is_bsd: build.env.ParseConfig('pkg-config opusfile opus --silence-errors --cflags --libs') + + build.env.Append(CPPDEFINES='__OPUS__') def sources(self, build): - return ['src/sources/soundsourceopus.cpp'] + return ['src/sources/soundsourceopus.cpp', + 'src/encoder/encoderopus.cpp', + 'src/encoder/encoderopussettings.cpp'] class FFMPEG(Feature): diff --git a/src/broadcast/defs_broadcast.h b/src/broadcast/defs_broadcast.h index dad19eaea81..9addf26c664 100644 --- a/src/broadcast/defs_broadcast.h +++ b/src/broadcast/defs_broadcast.h @@ -25,6 +25,7 @@ #define BROADCAST_FORMAT_MP3 "MP3" #define BROADCAST_FORMAT_OV "OggVorbis" +#define BROADCAST_FORMAT_OPUS "Opus" // EngineNetworkStream can't use locking mechanisms to protect its // internal worker list against concurrency issues, as it is used by diff --git a/src/encoder/encoder.cpp b/src/encoder/encoder.cpp index 87b95224944..77cbe87f58f 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 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 EncoderFactory::getFormats() const @@ -86,20 +96,27 @@ EncoderPointer EncoderFactory::getNewEncoder(Encoder::Format format, pEncoder = std::make_shared(pCallback); pEncoder->setEncoderSettings(EncoderFlacSettings(pConfig)); } else if (format.internalName == ENCODING_MP3) { - #ifdef __FFMPEGFILE_ENCODERS__ +#ifdef __FFMPEGFILE_ENCODERS__ pEncoder = std::make_shared(pCallback); - #else +#else pEncoder = std::make_shared(pCallback); - #endif +#endif pEncoder->setEncoderSettings(EncoderMp3Settings(pConfig)); } else if (format.internalName == ENCODING_OGG) { - #ifdef __FFMPEGFILE_ENCODERS__ +#ifdef __FFMPEGFILE_ENCODERS__ pEncoder = std::make_shared(pCallback); - #else +#else pEncoder = std::make_shared(pCallback); - #endif +#endif pEncoder->setEncoderSettings(EncoderVorbisSettings(pConfig)); - } else { + } +#ifdef __OPUS__ + else if (format.internalName == ENCODING_OPUS) { + pEncoder = std::make_shared(pCallback); + pEncoder->setEncoderSettings(EncoderOpusSettings(pConfig)); + } +#endif + else { qWarning() << "Unsupported format requested! " << format.internalName; DEBUG_ASSERT(false); pEncoder = std::make_shared(pCallback); @@ -121,6 +138,8 @@ EncoderSettingsPointer EncoderFactory::getEncoderSettings(Encoder::Format format return std::make_shared(pConfig); } else if (format.internalName == ENCODING_OGG) { return std::make_shared(pConfig); + } else if (format.internalName == ENCODING_OPUS) { + return std::make_shared(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 00000000000..09a116b3032 --- /dev/null +++ b/src/encoder/encoderopus.cpp @@ -0,0 +1,465 @@ +// encoderopus.cpp +// Create on August 15th 2017 by Palakis + +#include +#include +#include +#include + +#include "encoder/encoderopussettings.h" +#include "engine/sidechain/enginesidechain.h" +#include "util/logger.h" + +#include "encoder/encoderopus.h" + +// Opus only supports 48 and 96 kHz samplerates +constexpr int EncoderOpus::MASTER_SAMPLERATE = 48000; + +const char* EncoderOpus::INVALID_SAMPLERATE_MESSAGE = + "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."; + +namespace { +// From libjitsi's Opus encoder: +// 1 byte TOC + maximum frame size (1275) +// See https://tools.ietf.org/html/rfc6716#section-3.2 +constexpr int kMaxOpusBufferSize = 1+1275; +// Opus frame duration in milliseconds. Fixed to 60ms +constexpr int kOpusFrameMs = 60; +constexpr int kOpusChannelCount = 2; + +const mixxx::Logger kLogger("EncoderOpus"); + +QString 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 getSerial() { + static int prevSerial = 0; + + int serial; + do { + serial = qrand(); + } while(prevSerial == serial); + + prevSerial = serial; + kLogger.debug() << "RETURNING SERIAL " << serial; + return serial; +} +} + +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 * kOpusChannelCount), + m_pFifoChunkBuffer(), + 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); +} + +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 != MASTER_SAMPLERATE) { + kLogger.warning() << "initEncoder failed: samplerate not supported by Opus"; + + QString invalidSamplerateMessage = QObject::tr(INVALID_SAMPLERATE_MESSAGE); + + ErrorDialogProperties* props = ErrorDialogHandler::instance()->newDialogProperties(); + props->setType(DLG_WARNING); + props->setTitle(QObject::tr("Encoder")); + props->setText(invalidSamplerateMessage); + props->setKey(invalidSamplerateMessage); + 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 = std::make_unique(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 = reinterpret_cast(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 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 = reinterpret_cast(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(); +} diff --git a/src/encoder/encoderopus.h b/src/encoder/encoderopus.h new file mode 100644 index 00000000000..8c1e5a31e22 --- /dev/null +++ b/src/encoder/encoderopus.h @@ -0,0 +1,60 @@ +// encoderopus.h +// Create on August 15th 2017 by Palakis + +#ifndef ENCODER_ENCODEROPUS_H +#define ENCODER_ENCODEROPUS_H + +#include +#include +#include + +#include +#include + +#include "encoder/encoder.h" +#include "encoder/encodercallback.h" +#include "util/fifo.h" +#include "util/memory.h" +#include "util/sample.h" +#include "util/samplebuffer.h" + +class EncoderOpus: public Encoder { + public: + static const int MASTER_SAMPLERATE; + static const char* INVALID_SAMPLERATE_MESSAGE; + + explicit EncoderOpus(EncoderCallback* pCallback = nullptr); + ~EncoderOpus() override; + + 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: + 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 m_fifoBuffer; + std::unique_ptr m_pFifoChunkBuffer; + OpusEncoder* m_pOpus; + QVector m_opusDataBuffer; + ogg_stream_state m_oggStream; + ogg_page m_oggPage; + bool m_header_write; + int m_packetNumber; + ogg_int64_t m_granulePos; + QMap m_opusComments; +}; + +#endif // ENCODER_ENCODEROPUS_H diff --git a/src/encoder/encoderopussettings.cpp b/src/encoder/encoderopussettings.cpp new file mode 100644 index 00000000000..19a462c9d61 --- /dev/null +++ b/src/encoder/encoderopussettings.cpp @@ -0,0 +1,152 @@ +// encoderopussettings.cpp +// Create on August 15th 2017 by Palakis + +#include + +#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 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 for the option list. Using QMap::values() + // ensures that the returned QList 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())); +} + +QList 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(ConfigKey(RECORDING_PREF_KEY, kQualityKey), indexValue); +} + +void EncoderOpusSettings::setQualityByIndex(int qualityIndex) { + if (qualityIndex >= 0 && qualityIndex < m_qualList.size()) { + m_pConfig->setValue(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 EncoderOpusSettings::getOptionGroups() const { + return m_radioList; +} + +void EncoderOpusSettings::setGroupOption(QString groupCode, int optionIndex) { + bool found = false; + for (OptionsGroup group : qAsConst(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 : qAsConst(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 00000000000..adc4210ae93 --- /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: + explicit EncoderOpusSettings(UserSettingsPointer pConfig); + ~EncoderOpusSettings() override = default; + + // 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 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 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 m_qualList; + QList m_radioList; +}; + +#endif // ENCODER_ENCODEROPUSSETTINGS_H diff --git a/src/engine/sidechain/enginesidechain.cpp b/src/engine/sidechain/enginesidechain.cpp index 3dbd3f45903..6a88b08e77e 100644 --- a/src/engine/sidechain/enginesidechain.cpp +++ b/src/engine/sidechain/enginesidechain.cpp @@ -34,8 +34,6 @@ #include "util/timer.h" #include "util/trace.h" -#define SIDECHAIN_BUFFER_SIZE 65536 - EngineSideChain::EngineSideChain(UserSettingsPointer pConfig) : m_pConfig(pConfig), m_bStopThread(false), diff --git a/src/engine/sidechain/enginesidechain.h b/src/engine/sidechain/enginesidechain.h index 8497c07d91b..6cfa6a1adc7 100644 --- a/src/engine/sidechain/enginesidechain.h +++ b/src/engine/sidechain/enginesidechain.h @@ -49,6 +49,8 @@ class EngineSideChain : public QThread, public AudioDestination { // Thread-safe, blocking. void addSideChainWorker(SideChainWorker* pWorker); + static const int SIDECHAIN_BUFFER_SIZE = 65536; + private: void run() override; diff --git a/src/engine/sidechain/shoutconnection.cpp b/src/engine/sidechain/shoutconnection.cpp index 5e3aa5fd08f..14f3f100657 100644 --- a/src/engine/sidechain/shoutconnection.cpp +++ b/src/engine/sidechain/shoutconnection.cpp @@ -22,6 +22,7 @@ #include "control/controlpushbutton.h" #include "encoder/encoder.h" #include "encoder/encoderbroadcastsettings.h" +#include "encoder/encoderopus.h" #include "mixer/playerinfo.h" #include "preferences/usersettings.h" #include "recording/defs_recording.h" @@ -59,6 +60,7 @@ ShoutConnection::ShoutConnection(BroadcastProfilePtr profile, m_firstCall(false), m_format_is_mp3(false), m_format_is_ov(false), + m_format_is_opus(false), m_protocol_is_icecast1(false), m_protocol_is_icecast2(false), m_protocol_is_shoutcast(false), @@ -340,9 +342,10 @@ void ShoutConnection::updateFromPreferences() { m_format_is_mp3 = !qstrcmp(baFormat.constData(), BROADCAST_FORMAT_MP3); m_format_is_ov = !qstrcmp(baFormat.constData(), BROADCAST_FORMAT_OV); + m_format_is_opus = !qstrcmp(baFormat.constData(), BROADCAST_FORMAT_OPUS); if (m_format_is_mp3) { format = SHOUT_FORMAT_MP3; - } else if (m_format_is_ov) { + } else if (m_format_is_ov || m_format_is_opus) { format = SHOUT_FORMAT_OGG; } else { qWarning() << "Error: unknown format:" << baFormat.constData(); @@ -360,7 +363,7 @@ void ShoutConnection::updateFromPreferences() { int iMasterSamplerate = m_pMasterSamplerate->get(); if (m_format_is_ov && iMasterSamplerate == 96000) { - errorDialog(tr("Broadcasting at 96kHz with Ogg Vorbis is not currently " + errorDialog(tr("Broadcasting at 96 kHz with Ogg Vorbis is not currently " "supported. Please try a different sample-rate or switch " "to a different encoding."), tr("See https://bugs.launchpad.net/mixxx/+bug/686212 for more " @@ -368,6 +371,14 @@ void ShoutConnection::updateFromPreferences() { return; } + if(m_format_is_opus && iMasterSamplerate != EncoderOpus::MASTER_SAMPLERATE) { + errorDialog( + tr(EncoderOpus::INVALID_SAMPLERATE_MESSAGE), + tr("Unsupported samplerate") + ); + return; + } + if (shout_set_audio_info( m_pShout, SHOUT_AI_BITRATE, QByteArray::number(iBitrate).constData()) != SHOUTERR_SUCCESS) { @@ -411,7 +422,14 @@ void ShoutConnection::updateFromPreferences() { m_encoder = EncoderFactory::getFactory().getNewEncoder( EncoderFactory::getFactory().getFormatFor(ENCODING_OGG), m_pConfig, this); m_encoder->setEncoderSettings(broadcastSettings); - } else { + } +#ifdef __OPUS__ + else if (m_format_is_opus) { + m_encoder = EncoderFactory::getFactory().getNewEncoder( + EncoderFactory::getFactory().getFormatFor(ENCODING_OPUS), m_pConfig, this); + } +#endif + else { kLogger.warning() << "**** Unknown Encoder Format"; setState(NETWORKSTREAMWORKER_STATE_ERROR); m_lastErrorStr = "Encoder format error"; diff --git a/src/engine/sidechain/shoutconnection.h b/src/engine/sidechain/shoutconnection.h index 225fdef9b8e..d4e158f1516 100644 --- a/src/engine/sidechain/shoutconnection.h +++ b/src/engine/sidechain/shoutconnection.h @@ -141,6 +141,7 @@ class ShoutConnection bool m_format_is_mp3; bool m_format_is_ov; + bool m_format_is_opus; bool m_protocol_is_icecast1; bool m_protocol_is_icecast2; bool m_protocol_is_shoutcast; diff --git a/src/preferences/dialog/dlgprefbroadcast.cpp b/src/preferences/dialog/dlgprefbroadcast.cpp index bf4db034ced..dd868f9cc91 100644 --- a/src/preferences/dialog/dlgprefbroadcast.cpp +++ b/src/preferences/dialog/dlgprefbroadcast.cpp @@ -120,6 +120,9 @@ DlgPrefBroadcast::DlgPrefBroadcast(QWidget *parent, // Encoding format combobox comboBoxEncodingFormat->addItem(tr("MP3"), BROADCAST_FORMAT_MP3); comboBoxEncodingFormat->addItem(tr("Ogg Vorbis"), BROADCAST_FORMAT_OV); +#ifdef __OPUS__ + comboBoxEncodingFormat->addItem(tr("Opus"), BROADCAST_FORMAT_OPUS); +#endif // Encoding channels combobox comboBoxEncodingChannels->addItem(tr("Automatic"), diff --git a/src/recording/defs_recording.h b/src/recording/defs_recording.h index 50600096cf8..ea4583a1613 100644 --- a/src/recording/defs_recording.h +++ b/src/recording/defs_recording.h @@ -7,6 +7,7 @@ #define ENCODING_AIFF "AIFF" #define ENCODING_OGG "OGG" #define ENCODING_MP3 "MP3" +#define ENCODING_OPUS "Opus" #define RECORD_OFF 0.0 #define RECORD_READY 1.0