1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2025-08-03 22:42:27 -04:00

Copy audio to UDP/RTP: Opus implementation (2)

This commit is contained in:
f4exb 2019-02-18 18:29:37 +01:00
parent 44649fe486
commit 4c85516741
6 changed files with 164 additions and 19 deletions

View File

@ -30,8 +30,11 @@ AudioNetSink::AudioNetSink(QObject *parent) :
m_codec(CodecL16), m_codec(CodecL16),
m_rtpBufferAudio(0), m_rtpBufferAudio(0),
m_sampleRate(48000), m_sampleRate(48000),
m_stereo(false),
m_decimation(1), m_decimation(1),
m_decimationCount(0), m_decimationCount(0),
m_codecInputSize(960),
m_codecInputIndex(0),
m_bufferIndex(0), m_bufferIndex(0),
m_port(9998) m_port(9998)
{ {
@ -44,8 +47,11 @@ AudioNetSink::AudioNetSink(QObject *parent, int sampleRate, bool stereo) :
m_codec(CodecL16), m_codec(CodecL16),
m_rtpBufferAudio(0), m_rtpBufferAudio(0),
m_sampleRate(48000), m_sampleRate(48000),
m_stereo(false),
m_decimation(1), m_decimation(1),
m_decimationCount(0), m_decimationCount(0),
m_codecInputSize(960),
m_codecInputIndex(0),
m_bufferIndex(0), m_bufferIndex(0),
m_port(9998) m_port(9998)
{ {
@ -114,9 +120,10 @@ void AudioNetSink::setParameters(Codec codec, bool stereo, int sampleRate)
<< " sampleRate: " << sampleRate; << " sampleRate: " << sampleRate;
m_codec = codec; m_codec = codec;
m_stereo = stereo;
m_sampleRate = sampleRate; m_sampleRate = sampleRate;
setDecimationFilters(); setNewCodecData();
if (m_rtpBufferAudio) if (m_rtpBufferAudio)
{ {
@ -151,10 +158,28 @@ void AudioNetSink::setDecimation(uint32_t decimation)
{ {
m_decimation = decimation < 1 ? 1 : decimation > 6 ? 6 : decimation; m_decimation = decimation < 1 ? 1 : decimation > 6 ? 6 : decimation;
qDebug() << "AudioNetSink::setDecimation: " << m_decimation << " from: " << decimation; qDebug() << "AudioNetSink::setDecimation: " << m_decimation << " from: " << decimation;
setDecimationFilters(); setNewCodecData();
m_decimationCount = 0; 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() void AudioNetSink::setDecimationFilters()
{ {
int decimatedSampleRate = m_sampleRate / m_decimation; 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_udpSocket->writeDatagram((const char*) m_data, (qint64 ) m_udpBlockSize, m_address, m_port);
m_bufferIndex = 0; m_bufferIndex = 0;
} }
}
else if (m_codec == CodecOpus)
{
} }
else else
{ {
@ -250,7 +271,15 @@ void AudioNetSink::write(qint16 isample)
break; break;
case CodecOpus: 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; break;
case CodecL16: case CodecL16:
@ -289,7 +318,7 @@ void AudioNetSink::write(qint16 isample)
m_bufferIndex = 0; m_bufferIndex = 0;
} }
if (m_bufferIndex%2 == 0) { if (m_bufferIndex % 2 == 0) {
m_rtpBufferAudio->write((uint8_t *) &m_data[m_bufferIndex/2]); m_rtpBufferAudio->write((uint8_t *) &m_data[m_bufferIndex/2]);
} }
@ -298,6 +327,25 @@ void AudioNetSink::write(qint16 isample)
m_bufferIndex += 1; m_bufferIndex += 1;
} }
break; 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: case CodecL16:
default: default:
m_rtpBufferAudio->write((uint8_t *) &sample); m_rtpBufferAudio->write((uint8_t *) &sample);
@ -333,10 +381,43 @@ void AudioNetSink::write(qint16 ilSample, qint16 irSample)
{ {
if (m_bufferIndex >= m_udpBlockSize) 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; 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]; qint16 *p = (qint16*) &m_data[m_bufferIndex];
*p = lSample; *p = lSample;
@ -345,10 +426,50 @@ void AudioNetSink::write(qint16 ilSample, qint16 irSample)
*p = rSample; *p = rSample;
m_bufferIndex += sizeof(qint16); m_bufferIndex += sizeof(qint16);
} }
break;
}
} }
else if (m_type == SinkRTP) else if (m_type == SinkRTP)
{ {
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); m_rtpBufferAudio->write((uint8_t *) &lSample, (uint8_t *) &rSample);
break;
}
} }
} }

View File

@ -22,6 +22,7 @@
#include "audiofilter.h" #include "audiofilter.h"
#include "audiocompressor.h" #include "audiocompressor.h"
#include "audiog722.h" #include "audiog722.h"
#include "audioopus.h"
#include "export.h" #include "export.h"
#include <QObject> #include <QObject>
@ -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_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_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_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: 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; SinkType m_type;
Codec m_codec; Codec m_codec;
@ -82,18 +85,20 @@ protected:
RTPSink *m_rtpBufferAudio; RTPSink *m_rtpBufferAudio;
AudioCompressor m_audioCompressor; AudioCompressor m_audioCompressor;
AudioG722 m_g722; AudioG722 m_g722;
AudioOpus m_opus;
AudioFilter m_audioFilter; AudioFilter m_audioFilter;
int m_sampleRate; int m_sampleRate;
bool m_stereo;
uint32_t m_decimation; uint32_t m_decimation;
uint32_t m_decimationCount; uint32_t m_decimationCount;
char m_data[m_dataBlockSize]; 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; unsigned int m_bufferIndex;
QHostAddress m_address; QHostAddress m_address;
unsigned int m_port; unsigned int m_port;
}; };
#endif /* SDRBASE_AUDIO_AUDIONETSINK_H_ */ #endif /* SDRBASE_AUDIO_AUDIONETSINK_H_ */

View File

@ -2,6 +2,9 @@
// Copyright (C) 2019 F4EXB // // Copyright (C) 2019 F4EXB //
// written by Edouard Griffiths // // 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 // // 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 // // it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or // // 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; m_encoderOK = false;
return; 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) 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) if (nbBytes < 0)
{ {
qWarning("AudioOpus::encode failed: %s\n", opus_strerror(nbBytes)); qWarning("AudioOpus::encode failed: %s", opus_strerror(nbBytes));
return 0; return 0;
} }
else else

View File

@ -113,8 +113,8 @@ void RTPSink::setPayloadInformation(PayloadType payloadType, int sampleRate)
case PayloadOpus: case PayloadOpus:
m_sampleBytes = 1; m_sampleBytes = 1;
m_rtpSession.SetDefaultPayloadType(101); m_rtpSession.SetDefaultPayloadType(101);
m_packetSamples = 160; // Fixed 20ms @ 64 kbits/s packet samples m_packetSamples = 960; // Fixed 20ms @ 48 kHz as per https://tools.ietf.org/html/rfc7587
timestampinc = 160; // 1 channel timestampinc = 960; // and per single channel
break; break;
case PayloadL16Mono: case PayloadL16Mono:
default: default:

View File

@ -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) - `PCMA`: A-law 8 bit PCM (requires 8000 Hz sample rate mono)
- `PCMU`: Mu-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) - `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)
<h3>1.10 SDP string</h3> <h3>1.10 SDP string</h3>

View File

@ -350,4 +350,11 @@ void AudioDialogX::check()
QMessageBox::information(this, tr("Message"), tr("G722 must be 16000 Hz single channel")); 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"));
}
}
} }