I need to fill a datagram buffer with network byte ordered audio samples per the apt-x RTP layout per RFC 7310 section 5.5:
This is how the layout of audio samples sits in my application memory:
My application buffer in this case is also compacted (audio byte layout - little endian as I am on a PC): note I reversed the LSB & MSB order below comparing it to the layout shown at the end of section 5.5 in the RFC as the RFC shows big endian samples) - ignore the last byte here, as in compacted form, this blank byte would be the LSB for next audio sample etc.)
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| LSB | MB | MSB | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
little endian memory layout for 24 bit signed pcm sample.
I need to memcpy my application buffer to the destination datagram buffer (starting at offset 12, as I already have a correctly formatted big endian RTP header already at the start of the buffer).
Should I std::reverse my application buffer and then call memcpy or do I need some special endian library call that understandas the layout of the 24 bit signed samples? I belive the nomenclature of these samples is spcm24-le (signed pcm 24 bit little endian).
Imagine the audio samples as an array std::uint32_t[number of samples] of audio in intel memory (once they arrive from the microphone). In order to convert these to spcm24-le, I can simply shift each of these samples right by 8 bits to convert each sample to a signed pcm24 (still little endian) - but I don't know how to insert this into the target datagram buffer nicely packed and organized per the RFC.
A simple example in c++ would be great - perhaps using std::endian
or std::reverse
of std::byteswap
??
For the example format, the diagram below shows how coded samples
from each channel are packed into a sample block and how sample
blocks 1, 2, and 48 are subsequently packed into the RTP packet.
C:
Channel index: Left (l) = 1, left center (lc) = 2,
center (c) = 3, right (r) = 4, right center (rc) = 5,
and surround (S) = 6.
T:
Sample Block time index: The first sample block is 1; the final
sample is 48.
S(C)(T):
The Tth sample from channel C.
Lindsay & Foerster Standards Track [Page 8]
RFC 7310 apt-X RTP Format July 2014
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| S(1)(1) | S(2)(1) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| S(2)(1) | S(3)(1) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| S(3)(1) | S(4)(1) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| S(5)(1) | S(6)(1) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| S(6)(1) | S(1)(2) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| S(2)(2) | S(3)(2) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| S(4)(2) | S(5)(2) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| S(5)(2) | S(6)(2) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| S(6)(2) | S(1)(3) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| S(6)(47) | S(1)(48) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| S(1)(48) | S(2)(48) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| S(3)(48) | S(4)(48) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| S(4)(48) | S(5)(48) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| S(5)(48) | S(6)(48) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
For the example format, the diagram below indicates the order that
coded bytes are packed into the packet payload in terms of sample
byte significance. The following abbreviations are used.
MSB:
Most Significant Byte of a 24-bit coded sample
MB:
Middle Byte of a 24-bit coded sample
LSB:
Least Significant Byte of a 24-bit coded sample
Lindsay & Foerster Standards Track [Page 9]
RFC 7310 apt-X RTP Format July 2014
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| MSB | MB | LSB | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
I would use C++23 for readability and enhanced safety. The logic can then be downgraded for older standards:
#include <bit>
#include <ranges>
void in_place_reorder(std::span<std::byte> buffer){
namespace rv = std::views;
if (std::endian::native == std::endian::big)
for(auto && [first, _ , last]: buffer | rv::adjacent<3> | rv::stride(3))
std::swap(first,last);
};
std::vector<std::byte> in_vec{/*...*/};
std::array<std::byte, N> in_arr{/*...*/};
in_place_reorder(in_vec);
in_place_reorder(in_arr);
If the source is supposed to stay intact I would just create an adapter:
namespace rv = std::views;
auto constexpr reorder = rv::chunk(3) | rv::transform(rv::reverse) | join;
std::vector<std::byte> input{/*...*/};
auto output = input | reorder | std::ranges::to<std::vector>();
std::vector<std::byte> output2(size(input));
std::ranges::copy(input | reorder, begin(output2);
If C++23 is not available equivalent can be done with ranges-v3 open source library. Last resort would be to convert this logic to old-school for
loops with great caution to avoid common bugs:
void in_place_reorder(std::span<std::byte> buffer){
if (std::endian::native == std::endian::big)
for(std::size_t i = 0; (i + 3) < size(buffer); i += 3)
std::swap(buffer[i],buffer[i+2]);
};
std::vector<std::byte> in_vec{/*...*/};
auto out_vec = in_vec;
in_place_reorder(out_vec);
The alternative that keeps source intact would be difficult to read and error-prone. Possible to write, but with extra caution and lots of tweaks. It would be a bit faster than above sample, but trickier. I will skip that part because of my obsession for good design that complicates things.