mirror of
https://github.com/f4exb/sdrangel.git
synced 2025-08-04 06:52:26 -04:00
Copy audio to UDP/RTP: Opus implementation (2)
This commit is contained in:
parent
44649fe486
commit
4c85516741
@ -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)
|
||||||
{
|
{
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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_ */
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user