diff --git a/sdrbase/audio/audionetsink.cpp b/sdrbase/audio/audionetsink.cpp index 0d7e3a3b7..275539c81 100644 --- a/sdrbase/audio/audionetsink.cpp +++ b/sdrbase/audio/audionetsink.cpp @@ -30,8 +30,11 @@ AudioNetSink::AudioNetSink(QObject *parent) : m_codec(CodecL16), m_rtpBufferAudio(0), m_sampleRate(48000), + m_stereo(false), m_decimation(1), m_decimationCount(0), + m_codecInputSize(960), + m_codecInputIndex(0), m_bufferIndex(0), m_port(9998) { @@ -44,8 +47,11 @@ AudioNetSink::AudioNetSink(QObject *parent, int sampleRate, bool stereo) : m_codec(CodecL16), m_rtpBufferAudio(0), m_sampleRate(48000), + m_stereo(false), m_decimation(1), m_decimationCount(0), + m_codecInputSize(960), + m_codecInputIndex(0), m_bufferIndex(0), m_port(9998) { @@ -114,9 +120,10 @@ void AudioNetSink::setParameters(Codec codec, bool stereo, int sampleRate) << " sampleRate: " << sampleRate; m_codec = codec; + m_stereo = stereo; m_sampleRate = sampleRate; - setDecimationFilters(); + setNewCodecData(); if (m_rtpBufferAudio) { @@ -151,10 +158,28 @@ void AudioNetSink::setDecimation(uint32_t decimation) { m_decimation = decimation < 1 ? 1 : decimation > 6 ? 6 : decimation; qDebug() << "AudioNetSink::setDecimation: " << m_decimation << " from: " << decimation; - setDecimationFilters(); + setNewCodecData(); m_decimationCount = 0; } +void AudioNetSink::setNewCodecData() +{ + if (m_codec == CodecOpus) + { + m_codecInputSize = m_sampleRate / (m_decimation * 50); // 20ms = 1/50s - size is per channel + m_codecInputSize = m_codecInputSize > 960 ? 960 : m_codecInputSize; // hard limit of 48 kS/s + m_codecRatio = (m_sampleRate / m_decimation) / (AudioOpus::m_bitrate / 8); // compressor ratio + qDebug() << "AudioNetSink::setNewCodecData: CodecOpus:" + << " m_codecInputSize: " << m_codecInputSize + << " m_codecRatio: " << m_codecRatio + << " Fs: " << m_sampleRate/m_decimation + << " stereo: " << m_stereo; + m_opus.setEncoder(m_sampleRate/m_decimation, m_stereo ? 2 : 1); + } + + setDecimationFilters(); +} + void AudioNetSink::setDecimationFilters() { int decimatedSampleRate = m_sampleRate / m_decimation; @@ -206,10 +231,6 @@ void AudioNetSink::write(qint16 isample) m_udpSocket->writeDatagram((const char*) m_data, (qint64 ) m_udpBlockSize, m_address, m_port); m_bufferIndex = 0; } - } - else if (m_codec == CodecOpus) - { - } else { @@ -250,7 +271,15 @@ void AudioNetSink::write(qint16 isample) break; case CodecOpus: { + if (m_codecInputIndex == m_codecInputSize) + { + int nbBytes = m_opus.encode(m_codecInputSize, m_opusIn, (uint8_t *) m_data); + nbBytes = nbBytes > m_udpBlockSize ? m_udpBlockSize : nbBytes; + m_udpSocket->writeDatagram((const char*) m_data, (qint64 ) nbBytes, m_address, m_port); + m_codecInputIndex = 0; + } + m_opusIn[m_codecInputIndex++] = sample; } break; case CodecL16: @@ -289,7 +318,7 @@ void AudioNetSink::write(qint16 isample) m_bufferIndex = 0; } - if (m_bufferIndex%2 == 0) { + if (m_bufferIndex % 2 == 0) { m_rtpBufferAudio->write((uint8_t *) &m_data[m_bufferIndex/2]); } @@ -298,6 +327,25 @@ void AudioNetSink::write(qint16 isample) m_bufferIndex += 1; } break; + case CodecOpus: + { + if (m_codecInputIndex == m_codecInputSize) + { + int nbBytes = m_opus.encode(m_codecInputSize, m_opusIn, (uint8_t *) m_data); + if (nbBytes != AudioOpus::m_bitrate/400) { // 8 bits for 1/50s (20ms) + qWarning("AudioNetSink::write: CodecOpus mono: unexpected output frame size: %d bytes", nbBytes); + } + m_bufferIndex = 0; + m_codecInputIndex = 0; + } + + if (m_codecInputIndex % m_codecRatio == 0) { + m_rtpBufferAudio->write((uint8_t *) &m_data[m_bufferIndex++]); + } + + m_opusIn[m_codecInputIndex++] = sample; + } + break; case CodecL16: default: m_rtpBufferAudio->write((uint8_t *) &sample); @@ -333,10 +381,43 @@ void AudioNetSink::write(qint16 ilSample, qint16 irSample) { if (m_bufferIndex >= m_udpBlockSize) { - m_udpSocket->writeDatagram((const char*)m_data, (qint64 ) m_udpBlockSize, m_address, m_port); + m_udpSocket->writeDatagram((const char*) m_data, (qint64 ) m_udpBlockSize, m_address, m_port); m_bufferIndex = 0; } - else + + switch(m_codec) + { + case CodecPCMA: + case CodecPCMU: + case CodecG722: + break; // mono modes - do nothing + case CodecOpus: + { + if (m_codecInputIndex == m_codecInputSize) + { + int nbBytes = m_opus.encode(m_codecInputSize, m_opusIn, (uint8_t *) m_data); + nbBytes = nbBytes > m_udpBlockSize ? m_udpBlockSize : nbBytes; + m_udpSocket->writeDatagram((const char*) m_data, (qint64 ) nbBytes, m_address, m_port); + m_codecInputIndex = 0; + } + + m_opusIn[2*m_codecInputIndex] = lSample; + m_opusIn[2*m_codecInputIndex+1] = rSample; + m_codecInputIndex++; + } + break; + case CodecL8: + { + qint8 *p = (qint8*) &m_data[m_bufferIndex]; + *p = lSample / 256; + m_bufferIndex += sizeof(qint8); + p = (qint8*) &m_data[m_bufferIndex]; + *p = rSample / 256; + m_bufferIndex += sizeof(qint8); + } + break; + case CodecL16: + default: { qint16 *p = (qint16*) &m_data[m_bufferIndex]; *p = lSample; @@ -345,10 +426,50 @@ void AudioNetSink::write(qint16 ilSample, qint16 irSample) *p = rSample; m_bufferIndex += sizeof(qint16); } + break; + } } else if (m_type == SinkRTP) { - m_rtpBufferAudio->write((uint8_t *) &lSample, (uint8_t *) &rSample); + switch(m_codec) + { + case CodecPCMA: + case CodecPCMU: + case CodecG722: + break; // mono modes - do nothing + case CodecOpus: + { + if (m_codecInputIndex == m_codecInputSize) + { + int nbBytes = m_opus.encode(m_codecInputSize, m_opusIn, (uint8_t *) m_data); + if (nbBytes != AudioOpus::m_bitrate/400) { // 8 bits for 1/50s (20ms) + qWarning("AudioNetSink::write: CodecOpus stereo: unexpected output frame size: %d bytes", nbBytes); + } + m_bufferIndex = 0; + m_codecInputIndex = 0; + } + + if (m_codecInputIndex % m_codecRatio == 0) { + m_rtpBufferAudio->write((uint8_t *) &m_data[m_bufferIndex++]); + } + + m_opusIn[2*m_codecInputIndex] = lSample; + m_opusIn[2*m_codecInputIndex+1] = rSample; + m_codecInputIndex++; + } + break; + case CodecL8: + { + qint8 pl = lSample / 256; + qint8 pr = rSample / 256; + m_rtpBufferAudio->write((uint8_t *) &pl, (uint8_t *) &pr); + } + break; + case CodecL16: + default: + m_rtpBufferAudio->write((uint8_t *) &lSample, (uint8_t *) &rSample); + break; + } } } diff --git a/sdrbase/audio/audionetsink.h b/sdrbase/audio/audionetsink.h index 0d173352d..1409f456f 100644 --- a/sdrbase/audio/audionetsink.h +++ b/sdrbase/audio/audionetsink.h @@ -22,6 +22,7 @@ #include "audiofilter.h" #include "audiocompressor.h" #include "audiog722.h" +#include "audioopus.h" #include "export.h" #include @@ -72,9 +73,11 @@ public: static const int m_dataBlockSize = 65536; // room for G722 conversion (64000 = 12800*5 largest to date) static const int m_g722BlockSize = 12800; // number of resulting G722 bytes (80*20ms frames) static const int m_opusBlockSize = 960*2; // provision for 20ms of 2 int16 channels at 48 kS/s + static const int m_opusOutputSize = 160; // output frame: 20ms of 8 bit data @ 64 kbits/s = 160 bytes protected: - void setDecimationFilters(); + void setNewCodecData(); // actions to take when changes affecting codec dependent data occurs + void setDecimationFilters(); // set decimation filters limits depending on effective sample rate and codec SinkType m_type; Codec m_codec; @@ -82,18 +85,20 @@ protected: RTPSink *m_rtpBufferAudio; AudioCompressor m_audioCompressor; AudioG722 m_g722; + AudioOpus m_opus; AudioFilter m_audioFilter; int m_sampleRate; + bool m_stereo; uint32_t m_decimation; uint32_t m_decimationCount; char m_data[m_dataBlockSize]; - uint16_t m_opusIn[m_opusBlockSize]; + int16_t m_opusIn[m_opusBlockSize]; + int m_codecInputSize; // codec input block size - for codecs with actual encoding (Opus only for now) + int m_codecInputIndex; // codec input block fill index + int m_codecRatio; // codec compression ratio unsigned int m_bufferIndex; QHostAddress m_address; unsigned int m_port; }; - - - #endif /* SDRBASE_AUDIO_AUDIONETSINK_H_ */ diff --git a/sdrbase/audio/audioopus.cpp b/sdrbase/audio/audioopus.cpp index 2bccf1a55..cdf5b2057 100644 --- a/sdrbase/audio/audioopus.cpp +++ b/sdrbase/audio/audioopus.cpp @@ -2,6 +2,9 @@ // Copyright (C) 2019 F4EXB // // written by Edouard Griffiths // // // +// In this version we will use a fixed constant bit rate of 64kbit/s. // +// With a frame time of 20ms the encoder output size is always 160 bytes. // +// // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU General Public License as published by // // the Free Software Foundation as version 3 of the License, or // @@ -69,6 +72,15 @@ void AudioOpus::setEncoder(int32_t fs, int nChannels) m_encoderOK = false; return; } + + error = opus_encoder_ctl(m_encoderState, OPUS_SET_VBR(0)); // force constant bit rate + + if (error != OPUS_OK) + { + qWarning("AudioOpus::setEncoder: set constant bitrate error: %s", opus_strerror(error)); + m_encoderOK = false; + return; + } } int AudioOpus::encode(int frameSize, int16_t *in, uint8_t *out) @@ -77,7 +89,7 @@ int AudioOpus::encode(int frameSize, int16_t *in, uint8_t *out) if (nbBytes < 0) { - qWarning("AudioOpus::encode failed: %s\n", opus_strerror(nbBytes)); + qWarning("AudioOpus::encode failed: %s", opus_strerror(nbBytes)); return 0; } else diff --git a/sdrbase/util/rtpsink.cpp b/sdrbase/util/rtpsink.cpp index 0073d7ae7..af1e3a53a 100644 --- a/sdrbase/util/rtpsink.cpp +++ b/sdrbase/util/rtpsink.cpp @@ -113,8 +113,8 @@ void RTPSink::setPayloadInformation(PayloadType payloadType, int sampleRate) case PayloadOpus: m_sampleBytes = 1; m_rtpSession.SetDefaultPayloadType(101); - m_packetSamples = 160; // Fixed 20ms @ 64 kbits/s packet samples - timestampinc = 160; // 1 channel + m_packetSamples = 960; // Fixed 20ms @ 48 kHz as per https://tools.ietf.org/html/rfc7587 + timestampinc = 960; // and per single channel break; case PayloadL16Mono: default: diff --git a/sdrgui/audio.md b/sdrgui/audio.md index e93371980..9cb03a137 100644 --- a/sdrgui/audio.md +++ b/sdrgui/audio.md @@ -67,7 +67,7 @@ This is the codec applied before sending the stream via UDP. The following are a - `PCMA`: A-law 8 bit PCM (requires 8000 Hz sample rate mono) - `PCMU`: Mu-law 8 bit PCM (requires 8000 Hz sample rate mono) - `G722`: G722 64 kbit/s (requires 16000 Hz sample rate mono) - - `OPUS` : Opus 64 kbit/s + - `OPUS` : Opus constant 64 kbit/s (requires 48, 24, 16 or 12 kHz sample rates)

1.10 SDP string

diff --git a/sdrgui/gui/audiodialog.cpp b/sdrgui/gui/audiodialog.cpp index d1ebac714..d85276a62 100644 --- a/sdrgui/gui/audiodialog.cpp +++ b/sdrgui/gui/audiodialog.cpp @@ -350,4 +350,11 @@ void AudioDialogX::check() QMessageBox::information(this, tr("Message"), tr("G722 must be 16000 Hz single channel")); } } + else if (m_outputDeviceInfo.udpChannelCodec == AudioOutput::UDPCodecOpus) + { + int effectiveSampleRate = m_outputDeviceInfo.sampleRate/decimationFactor; + if ((effectiveSampleRate != 48000) || (effectiveSampleRate != 24000) || (effectiveSampleRate != 16000) || (effectiveSampleRate != 12000)) { + QMessageBox::information(this, tr("Message"), tr("Opus takes only 48, 24, 16 or 12 kHz sample rates")); + } + } }