Compare commits

...

1 Commits

30 changed files with 3239 additions and 334 deletions

28
include/bitstream/LICENSE Normal file
View File

@ -0,0 +1,28 @@
BSD 3-Clause License
Copyright (c) 2023, Krede
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Stanislav Denisov (nxrighthere@gmail.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1 @@
0.1.4

View File

@ -0,0 +1,22 @@
#pragma once
// Quantization
#include "quantization/bounded_range.h"
#include "quantization/half_precision.h"
#include "quantization/smallest_three.h"
// Stream
#include "stream/bit_measure.h"
#include "stream/bit_reader.h"
#include "stream/bit_writer.h"
#include "stream/byte_buffer.h"
#include "stream/serialize_traits.h"
// Traits
#include "traits/array_traits.h"
#include "traits/bool_trait.h"
#include "traits/enum_trait.h"
#include "traits/float_trait.h"
#include "traits/integral_traits.h"
#include "traits/quantization_traits.h"
#include "traits/string_traits.h"

View File

@ -0,0 +1,104 @@
#pragma once
/*
* Copyright (c) 2018 Stanislav Denisov
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <cstdint>
namespace bitstream
{
/**
* @brief Class for quantizing single-precision floats into a range and precision
*/
class bounded_range
{
public:
constexpr bounded_range() noexcept :
m_Min(0),
m_Max(0),
m_Precision(0),
m_BitsRequired(0),
m_Mask(0) {}
constexpr bounded_range(float min, float max, float precision) noexcept :
m_Min(min),
m_Max(max),
m_Precision(precision),
m_BitsRequired(log2(static_cast<uint32_t>((m_Max - m_Min) * (1.0f / precision) + 0.5f)) + 1),
m_Mask((1U << m_BitsRequired) - 1U) {}
constexpr inline float get_min() const noexcept { return m_Min; }
constexpr inline float get_max() const noexcept { return m_Max; }
constexpr inline float get_precision() const noexcept { return m_Precision; }
constexpr inline uint32_t get_bits_required() const noexcept { return m_BitsRequired; }
constexpr inline uint32_t quantize(float value) const noexcept
{
if (value < m_Min)
value = m_Min;
else if (value > m_Max)
value = m_Max;
return static_cast<uint32_t>(static_cast<float>((value - m_Min) * (1.0f / m_Precision)) + 0.5f) & m_Mask;
}
constexpr inline float dequantize(uint32_t data) const noexcept
{
float adjusted = (static_cast<float>(data) * m_Precision) + m_Min;
if (adjusted < m_Min)
adjusted = m_Min;
else if (adjusted > m_Max)
adjusted = m_Max;
return adjusted;
}
private:
constexpr inline static uint32_t log2(uint32_t value) noexcept
{
value |= value >> 1;
value |= value >> 2;
value |= value >> 4;
value |= value >> 8;
value |= value >> 16;
return DE_BRUIJN[(value * 0x07C4ACDDU) >> 27];
}
private:
float m_Min;
float m_Max;
float m_Precision;
uint32_t m_BitsRequired;
uint32_t m_Mask;
constexpr inline static uint32_t DE_BRUIJN[32]
{
0, 9, 1, 10, 13, 21, 2, 29,
11, 14, 16, 18, 22, 25, 3, 30,
8, 12, 20, 28, 15, 17, 24, 7,
19, 27, 23, 6, 26, 5, 4, 31
};
};
}

View File

@ -0,0 +1,114 @@
#pragma once
/*
* Copyright (c) 2018 Stanislav Denisov
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <cstdint>
#include <cstring>
namespace bitstream
{
/**
* @brief Class for quantizing single-precision floats into half-precision
*/
class half_precision
{
public:
inline static uint16_t quantize(float value) noexcept
{
int32_t tmp;
std::memcpy(&tmp, &value, sizeof(float));
int32_t s = (tmp >> 16) & 0x00008000;
int32_t e = ((tmp >> 23) & 0X000000FF) - (127 - 15);
int32_t m = tmp & 0X007FFFFF;
if (e <= 0) {
if (e < -10)
return static_cast<uint16_t>(s);
m |= 0x00800000;
int32_t t = 14 - e;
int32_t a = (1 << (t - 1)) - 1;
int32_t b = (m >> t) & 1;
m = (m + a + b) >> t;
return static_cast<uint16_t>(s | m);
}
if (e == 0XFF - (127 - 15)) {
if (m == 0)
return static_cast<uint16_t>(s | 0X7C00);
m >>= 13;
return static_cast<uint16_t>(s | 0X7C00 | m | ((m == 0) ? 1 : 0));
}
m = m + 0X00000FFF + ((m >> 13) & 1);
if ((m & 0x00800000) != 0) {
m = 0;
e++;
}
if (e > 30)
return static_cast<uint16_t>(s | 0X7C00);
return static_cast<uint16_t>(s | (e << 10) | (m >> 13));
}
inline static float dequantize(uint16_t value) noexcept
{
uint32_t tmp;
uint32_t mantissa = static_cast<uint32_t>(value & 1023);
uint32_t exponent = 0XFFFFFFF2;
if ((value & -33792) == 0) {
if (mantissa != 0) {
while ((mantissa & 1024) == 0) {
exponent--;
mantissa <<= 1;
}
mantissa &= 0XFFFFFBFF;
tmp = ((static_cast<uint32_t>(value) & 0x8000) << 16) | ((exponent + 127) << 23) | (mantissa << 13);
}
else
{
tmp = static_cast<uint32_t>((value & 0x8000) << 16);
}
}
else
{
tmp = ((static_cast<uint32_t>(value) & 0x8000) << 16) | (((((static_cast<uint32_t>(value) >> 10) & 0X1F) - 15) + 127) << 23) | (mantissa << 13);
}
float result;
std::memcpy(&result, &tmp, sizeof(float));
return result;
}
};
}

View File

@ -0,0 +1,156 @@
#pragma once
/*
* Copyright (c) 2020 Stanislav Denisov, Maxim Munning, Davin Carten
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <cstdint>
#include <cmath>
namespace bitstream
{
/**
* @brief A quantized representation of a quaternion
*/
struct quantized_quaternion
{
uint32_t m;
uint32_t a;
uint32_t b;
uint32_t c;
constexpr quantized_quaternion() noexcept :
m(0),
a(0),
b(0),
c(0) {}
constexpr quantized_quaternion(uint32_t w, uint32_t x, uint32_t y, uint32_t z) noexcept :
m(w), a(x), b(y), c(z) {}
};
/**
* @brief Class for quantizing a user-specified quaternion into fewer bits using the smallest-three algorithm
* @tparam T The quaternion-type to quantize
*/
template<typename T, size_t BitsPerElement = 12>
class smallest_three
{
private:
static constexpr float SMALLEST_THREE_UNPACK = 0.70710678118654752440084436210485f + 0.0000001f;
static constexpr float SMALLEST_THREE_PACK = 1.0f / SMALLEST_THREE_UNPACK;
public:
inline static quantized_quaternion quantize(const T& quaternion) noexcept
{
constexpr float half_range = static_cast<float>(1 << (BitsPerElement - 1));
constexpr float packer = SMALLEST_THREE_PACK * half_range;
float max_value = -1.0f;
bool sign_minus = false;
uint32_t m = 0;
uint32_t a = 0;
uint32_t b = 0;
uint32_t c = 0;
for (uint32_t i = 0; i < 4; i++)
{
float element = quaternion[i];
float abs = element > 0.0f ? element : -element;
if (abs > max_value)
{
sign_minus = element < 0.0f;
m = i;
max_value = abs;
}
}
float af = 0.0f;
float bf = 0.0f;
float cf = 0.0f;
switch (m)
{
case 0:
af = quaternion[1];
bf = quaternion[2];
cf = quaternion[3];
break;
case 1:
af = quaternion[0];
bf = quaternion[2];
cf = quaternion[3];
break;
case 2:
af = quaternion[0];
bf = quaternion[1];
cf = quaternion[3];
break;
default: // case 3
af = quaternion[0];
bf = quaternion[1];
cf = quaternion[2];
break;
}
if (sign_minus)
{
a = static_cast<uint32_t>((-af * packer) + half_range);
b = static_cast<uint32_t>((-bf * packer) + half_range);
c = static_cast<uint32_t>((-cf * packer) + half_range);
}
else
{
a = static_cast<uint32_t>((af * packer) + half_range);
b = static_cast<uint32_t>((bf * packer) + half_range);
c = static_cast<uint32_t>((cf * packer) + half_range);
}
return { m, a, b, c };
}
inline static T dequantize(const quantized_quaternion& data) noexcept
{
constexpr uint32_t half_range = (1 << (BitsPerElement - 1));
constexpr float unpacker = SMALLEST_THREE_UNPACK * (1.0f / half_range);
float a = static_cast<float>(data.a * unpacker - half_range * unpacker);
float b = static_cast<float>(data.b * unpacker - half_range * unpacker);
float c = static_cast<float>(data.c * unpacker - half_range * unpacker);
float d = std::sqrt(1.0f - ((a * a) + (b * b) + (c * c)));
switch (data.m)
{
case 0:
return T{ d, a, b, c };
case 1:
return T{ a, d, b, c };
case 2:
return T{ a, b, d, c };
default: // case 3
return T{ a, b, c, d };
}
}
};
}

View File

@ -0,0 +1,235 @@
#pragma once
#include "../utility/assert.h"
#include "../utility/crc.h"
#include "../utility/endian.h"
#include "../utility/meta.h"
#include "byte_buffer.h"
#include "serialize_traits.h"
#include <cstdint>
#include <cstring>
#include <limits>
#include <memory>
#include <type_traits>
namespace bitstream
{
/**
* @brief A stream for writing objects tightly into a buffer
* @note Does not take ownership of the buffer
*/
class bit_measure
{
public:
static constexpr bool writing = true;
static constexpr bool reading = false;
/**
* @brief Default construct a writer pointing to a null buffer
*/
bit_measure() noexcept :
m_NumBitsWritten(0),
m_TotalBits((std::numeric_limits<uint32_t>::max)()) {}
/**
* @brief Construct a writer pointing to the given byte array with @p num_bytes size
* @param num_bytes The number of bytes in the array
*/
bit_measure(uint32_t num_bytes) noexcept :
m_NumBitsWritten(0),
m_TotalBits(num_bytes * 8) {}
bit_measure(const bit_measure&) = delete;
bit_measure(bit_measure&& other) noexcept :
m_NumBitsWritten(other.m_NumBitsWritten),
m_TotalBits(other.m_TotalBits)
{
other.m_NumBitsWritten = 0;
other.m_TotalBits = 0;
}
bit_measure& operator=(const bit_measure&) = delete;
bit_measure& operator=(bit_measure&& rhs) noexcept
{
m_NumBitsWritten = rhs.m_NumBitsWritten;
m_TotalBits = rhs.m_TotalBits;
rhs.m_NumBitsWritten = 0;
rhs.m_TotalBits = 0;
return *this;
}
/**
* @brief Returns the buffer that this writer is currently serializing into
* @return The buffer
*/
[[nodiscard]] uint8_t* get_buffer() const noexcept { return nullptr; }
/**
* @brief Returns the number of bits which have been written to the buffer
* @return The number of bits which have been written
*/
[[nodiscard]] uint32_t get_num_bits_serialized() const noexcept { return m_NumBitsWritten; }
/**
* @brief Returns the number of bytes which have been written to the buffer
* @return The number of bytes which have been written
*/
[[nodiscard]] uint32_t get_num_bytes_serialized() const noexcept { return m_NumBitsWritten > 0U ? ((m_NumBitsWritten - 1U) / 8U + 1U) : 0U; }
/**
* @brief Returns whether the @p num_bits can fit in the buffer
* @param num_bits The number of bits to test
* @return Whether the number of bits can fit in the buffer
*/
[[nodiscard]] bool can_serialize_bits(uint32_t num_bits) const noexcept { return m_NumBitsWritten + num_bits <= m_TotalBits; }
/**
* @brief Returns the number of bits which have not been written yet
* @note The same as get_total_bits() - get_num_bits_serialized()
* @return The remaining space in the buffer
*/
[[nodiscard]] uint32_t get_remaining_bits() const noexcept { return m_TotalBits - m_NumBitsWritten; }
/**
* @brief Returns the size of the buffer, in bits
* @return The size of the buffer, in bits
*/
[[nodiscard]] uint32_t get_total_bits() const noexcept { return m_TotalBits; }
/**
* @brief Instructs the writer that you intend to use `serialize_checksum()` later on, and to reserve the first 32 bits.
* @return Returns false if anything has already been written to the buffer or if there's no space to write the checksum
*/
[[nodiscard]] bool prepend_checksum() noexcept
{
BS_ASSERT(m_NumBitsWritten == 0);
BS_ASSERT(can_serialize_bits(32U));
m_NumBitsWritten += 32U;
return true;
}
/**
* @brief Writes a checksum of the @p protocol_version and the rest of the buffer as the first 32 bits
* @param protocol_version A unique version number
* @return The number of bytes written to the buffer
*/
uint32_t serialize_checksum(uint32_t protocol_version) noexcept
{
return m_NumBitsWritten;
}
/**
* @brief Pads the buffer up to the given number of bytes with zeros
* @param num_bytes The byte number to pad to
* @return Returns false if the current size of the buffer is bigger than @p num_bytes
*/
[[nodiscard]] bool pad_to_size(uint32_t num_bytes) noexcept
{
BS_ASSERT(num_bytes * 8U <= m_TotalBits);
BS_ASSERT(num_bytes * 8U >= m_NumBitsWritten);
m_NumBitsWritten = num_bytes * 8U;
return true;
}
/**
* @brief Pads the buffer up with the given number of bytes
* @param num_bytes The amount of bytes to pad
* @return Returns false if the current size of the buffer is bigger than @p num_bytes or if the padded bits are not zeros.
*/
[[nodiscard]] bool pad(uint32_t num_bytes) noexcept
{
return pad_to_size(get_num_bytes_serialized() + num_bytes);
}
/**
* @brief Pads the buffer with up to 8 zeros, so that the next write is byte-aligned
* @return Success
*/
[[nodiscard]] bool align() noexcept
{
uint32_t remainder = m_NumBitsWritten % 8U;
if (remainder != 0U)
m_NumBitsWritten += 8U - remainder;
return true;
}
/**
* @brief Writes the first @p num_bits bits of @p value into the buffer
* @param value The value to serialize
* @param num_bits The number of bits of the @p value to serialize
* @return Returns false if @p num_bits is less than 1 or greater than 32 or if writing the given number of bits would overflow the buffer
*/
[[nodiscard]] bool serialize_bits(uint32_t value, uint32_t num_bits) noexcept
{
BS_ASSERT(num_bits > 0U && num_bits <= 32U);
BS_ASSERT(can_serialize_bits(num_bits));
m_NumBitsWritten += num_bits;
return true;
}
/**
* @brief Writes the first @p num_bits bits of the given byte array, 32 bits at a time
* @param bytes The bytes to serialize
* @param num_bits The number of bits of the @p bytes to serialize
* @return Returns false if @p num_bits is less than 1 or if writing the given number of bits would overflow the buffer
*/
[[nodiscard]] bool serialize_bytes(const uint8_t* bytes, uint32_t num_bits) noexcept
{
BS_ASSERT(num_bits > 0U);
BS_ASSERT(can_serialize_bits(num_bits));
m_NumBitsWritten += num_bits;
return true;
}
/**
* @brief Writes to the buffer, using the given @p Trait.
* @note The Trait type in this function must always be explicitly declared
* @tparam Trait A template specialization of serialize_trait<>
* @tparam ...Args The types of the arguments to pass to the serialize function
* @param ...args The arguments to pass to the serialize function
* @return Whether successful or not
*/
template<typename Trait, typename... Args, typename = utility::has_serialize_t<Trait, bit_measure, Args...>>
[[nodiscard]] bool serialize(Args&&... args) noexcept(utility::is_serialize_noexcept_v<Trait, bit_measure, Args...>)
{
return serialize_traits<Trait>::serialize(*this, std::forward<Args>(args)...);
}
/**
* @brief Writes to the buffer, by trying to deduce the trait.
* @note The Trait type in this function is always implicit and will be deduced from the first argument if possible.
* If the trait cannot be deduced it will not compile.
* @tparam Trait The type of the first argument, which will be used to deduce the trait specialization
* @tparam ...Args The types of the arguments to pass to the serialize function
* @param arg The first argument to pass to the serialize function
* @param ...args The rest of the arguments to pass to the serialize function
* @return Whether successful or not
*/
template<typename... Args, typename Trait, typename = utility::has_deduce_serialize_t<Trait, bit_measure, Args...>>
[[nodiscard]] bool serialize(Trait&& arg, Args&&... args) noexcept(utility::is_deduce_serialize_noexcept_v<Trait, bit_measure, Args...>)
{
return serialize_traits<utility::deduce_trait_t<Trait, bit_measure, Args...>>::serialize(*this, std::forward<Trait>(arg), std::forward<Args>(args)...);
}
private:
uint32_t m_NumBitsWritten;
uint32_t m_TotalBits;
};
}

View File

@ -0,0 +1,343 @@
#pragma once
#include "../utility/assert.h"
#include "../utility/crc.h"
#include "../utility/endian.h"
#include "../utility/meta.h"
#include "byte_buffer.h"
#include "serialize_traits.h"
#include "stream_traits.h"
#include <cstdint>
#include <cstring>
#include <string>
#include <type_traits>
namespace bitstream
{
/**
* @brief A stream for reading objects from a tightly packed buffer
* @tparam Policy The underlying representation of the buffer
*/
template<typename Policy>
class bit_reader
{
public:
static constexpr bool writing = false;
static constexpr bool reading = true;
/**
* @brief Construct a reader with the parameters passed to the underlying policy
* @param ...args The arguments to pass to the policy
*/
template<typename... Ts,
typename = std::enable_if_t<std::is_constructible_v<Policy, Ts...>>>
bit_reader(Ts&&... args)
noexcept(std::is_nothrow_constructible_v<Policy, Ts...>) :
m_Policy(std::forward<Ts>(args) ...),
m_Scratch(0),
m_ScratchBits(0),
m_WordIndex(0) {}
bit_reader(const bit_reader&) = delete;
bit_reader(bit_reader&& other) noexcept :
m_Policy(std::move(other.m_Policy)),
m_Scratch(other.m_Scratch),
m_ScratchBits(other.m_ScratchBits),
m_WordIndex(other.m_WordIndex)
{
other.m_Scratch = 0;
other.m_ScratchBits = 0;
other.m_WordIndex = 0;
}
bit_reader& operator=(const bit_reader&) = delete;
bit_reader& operator=(bit_reader&& rhs) noexcept
{
m_Policy = std::move(rhs.m_Policy);
m_Scratch = rhs.m_Scratch;
m_ScratchBits = rhs.m_ScratchBits;
m_WordIndex = rhs.m_WordIndex;
rhs.m_Scratch = 0;
rhs.m_ScratchBits = 0;
rhs.m_WordIndex = 0;
return *this;
}
/**
* @brief Returns the buffer that this reader is currently serializing from
* @return The buffer
*/
[[nodiscard]] const uint8_t* get_buffer() const noexcept { return reinterpret_cast<const uint8_t*>(m_Policy.get_buffer()); }
/**
* @brief Returns the number of bits which have been read from the buffer
* @return The number of bits which have been read
*/
[[nodiscard]] uint32_t get_num_bits_serialized() const noexcept { return m_Policy.get_num_bits_serialized(); }
/**
* @brief Returns the number of bytes which have been read from the buffer
* @return The number of bytes which have been read
*/
[[nodiscard]] uint32_t get_num_bytes_serialized() const noexcept { return get_num_bits_serialized() > 0U ? ((get_num_bits_serialized() - 1U) / 8U + 1U) : 0U; }
/**
* @brief Returns whether the @p num_bits be read from the buffer
* @param num_bits The number of bits to test
* @return Whether the number of bits can be read from the buffer
*/
[[nodiscard]] bool can_serialize_bits(uint32_t num_bits) const noexcept { return m_Policy.can_serialize_bits(num_bits); }
/**
* @brief Returns the number of bits which have not been read yet
* @note The same as get_total_bits() - get_num_bits_serialized()
* @return The remaining space in the buffer
*/
[[nodiscard]] uint32_t get_remaining_bits() const noexcept { return get_total_bits() - get_num_bits_serialized(); }
/**
* @brief Returns the size of the buffer, in bits
* @return The size of the buffer, in bits
*/
[[nodiscard]] uint32_t get_total_bits() const noexcept { return m_Policy.get_total_bits(); }
/**
* @brief Reads the first 32 bits of the buffer and compares it to a checksum of the @p protocol_version and the rest of the buffer
* @param protocol_version A unique version number
* @return Whether the checksum matches what was written
*/
[[nodiscard]] bool serialize_checksum(uint32_t protocol_version) noexcept
{
BS_ASSERT(get_num_bits_serialized() == 0);
BS_ASSERT(can_serialize_bits(32U));
uint32_t num_bytes = (get_total_bits() - 1U) / 8U + 1U;
const uint32_t* buffer = m_Policy.get_buffer();
// Generate checksum to compare against
uint32_t generated_checksum = utility::crc_uint32(reinterpret_cast<const uint8_t*>(&protocol_version), reinterpret_cast<const uint8_t*>(buffer + 1), num_bytes - 4);
// Advance the reader by the size of the checksum (32 bits / 1 word)
m_WordIndex++;
BS_ASSERT(m_Policy.extend(32U));
// Read the checksum
uint32_t checksum = *buffer;
// Compare the checksum
return generated_checksum == checksum;
}
/**
* @brief Pads the buffer up to the given number of bytes
* @param num_bytes The byte number to pad to
* @return Returns false if the current size of the buffer is bigger than @p num_bytes or if the padded bits are not zeros.
*/
[[nodiscard]] bool pad_to_size(uint32_t num_bytes) noexcept
{
uint32_t num_bits_read = get_num_bits_serialized();
BS_ASSERT(num_bytes * 8U >= num_bits_read);
BS_ASSERT(can_serialize_bits(num_bytes * 8U - num_bits_read));
uint32_t remainder = (num_bytes * 8U - num_bits_read) % 32U;
uint32_t zero;
// Test the last word more carefully, as it may have data
if (remainder != 0U)
{
bool status = serialize_bits(zero, remainder);
BS_ASSERT(status && zero == 0);
}
uint32_t offset = get_num_bits_serialized() / 32;
uint32_t max = num_bytes / 4;
// Test for zeros in padding
for (uint32_t i = offset; i < max; i++)
{
bool status = serialize_bits(zero, 32);
BS_ASSERT(status && zero == 0);
}
return true;
}
/**
* @brief Pads the buffer up with the given number of bytes
* @param num_bytes The amount of bytes to pad
* @return Returns false if the current size of the buffer is bigger than @p num_bytes or if the padded bits are not zeros.
*/
[[nodiscard]] bool pad(uint32_t num_bytes) noexcept
{
return pad_to_size(get_num_bytes_serialized() + num_bytes);
}
/**
* @brief Pads the buffer with up to 8 zeros, so that the next read is byte-aligned
* @notes Return false if the padded bits are not zeros
* @return Returns false if the padded bits are not zeros
*/
[[nodiscard]] bool align() noexcept
{
uint32_t remainder = get_num_bits_serialized() % 8U;
if (remainder != 0U)
{
uint32_t zero;
bool status = serialize_bits(zero, 8U - remainder);
BS_ASSERT(status && zero == 0U && get_num_bits_serialized() % 8U == 0U);
}
return true;
}
/**
* @brief Reads the first @p num_bits bits of @p value from the buffer
* @param value The value to serialize
* @param num_bits The number of bits of the @p value to serialize
* @return Returns false if @p num_bits is less than 1 or greater than 32 or if reading the given number of bits would overflow the buffer
*/
[[nodiscard]] bool serialize_bits(uint32_t& value, uint32_t num_bits) noexcept
{
BS_ASSERT(num_bits > 0U && num_bits <= 32U);
BS_ASSERT(m_Policy.extend(num_bits));
// This is actually slower
// Possibly due to unlikely branching
/*if (num_bits == 32U && m_ScratchBits == 0U)
{
const uint32_t* ptr = m_Policy.get_buffer() + m_WordIndex;
value = utility::to_big_endian32(*ptr);
m_WordIndex++;
return true;
}*/
if (m_ScratchBits < num_bits)
{
const uint32_t* ptr = m_Policy.get_buffer() + m_WordIndex;
uint64_t ptr_value = static_cast<uint64_t>(utility::to_big_endian32(*ptr)) << (32U - m_ScratchBits);
m_Scratch |= ptr_value;
m_ScratchBits += 32U;
m_WordIndex++;
}
uint32_t offset = 64U - num_bits;
value = static_cast<uint32_t>(m_Scratch >> offset);
m_Scratch <<= num_bits;
m_ScratchBits -= num_bits;
return true;
}
/**
* @brief Reads the first @p num_bits bits of the given byte array, 32 bits at a time
* @param bytes The bytes to serialize
* @param num_bits The number of bits of the @p bytes to serialize
* @return Returns false if @p num_bits is less than 1 or if reading the given number of bits would overflow the buffer
*/
[[nodiscard]] bool serialize_bytes(uint8_t* bytes, uint32_t num_bits) noexcept
{
BS_ASSERT(num_bits > 0U);
BS_ASSERT(can_serialize_bits(num_bits));
// Read the byte array as words
uint32_t* word_buffer = reinterpret_cast<uint32_t*>(bytes);
uint32_t num_words = num_bits / 32U;
if (m_ScratchBits % 32U == 0U && num_words > 0U)
{
BS_ASSERT(m_Policy.extend(num_words * 32U));
// If the read buffer is word-aligned, just memcpy it
std::memcpy(word_buffer, m_Policy.get_buffer() + m_WordIndex, num_words * 4U);
m_WordIndex += num_words;
}
else
{
// If the buffer is not word-aligned, serialize a word at a time
for (uint32_t i = 0U; i < num_words; i++)
{
uint32_t value;
BS_ASSERT(serialize_bits(value, 32U));
// Casting a byte-array to an int is wrong on little-endian systems
// We have to swap the bytes around
word_buffer[i] = utility::to_big_endian32(value);
}
}
// Early exit if the word-count matches
if (num_bits % 32 == 0)
return true;
uint32_t remaining_bits = num_bits - num_words * 32U;
uint32_t num_bytes = (remaining_bits - 1U) / 8U + 1U;
for (uint32_t i = 0; i < num_bytes; i++)
{
uint32_t value;
BS_ASSERT(serialize_bits(value, (std::min)(remaining_bits - i * 8U, 8U)));
bytes[num_words * 4 + i] = static_cast<uint8_t>(value);
}
return true;
}
/**
* @brief Reads from the buffer, using the given @p Trait.
* @note The Trait type in this function must always be explicitly declared
* @tparam Trait A template specialization of serialize_trait<>
* @tparam ...Args The types of the arguments to pass to the serialize function
* @param ...args The arguments to pass to the serialize function
* @return Whether successful or not
*/
template<typename Trait, typename... Args, typename = utility::has_serialize_t<Trait, bit_reader, Args...>>
[[nodiscard]] bool serialize(Args&&... args) noexcept(utility::is_serialize_noexcept_v<Trait, bit_reader, Args...>)
{
return serialize_traits<Trait>::serialize(*this, std::forward<Args>(args)...);
}
/**
* @brief Reads from the buffer, by trying to deduce the trait.
* @note The Trait type in this function is always implicit and will be deduced from the first argument if possible.
* If the trait cannot be deduced it will not compile.
* @tparam Trait The type of the first argument, which will be used to deduce the trait specialization
* @tparam ...Args The types of the arguments to pass to the serialize function
* @param arg The first argument to pass to the serialize function
* @param ...args The rest of the arguments to pass to the serialize function
* @return Whether successful or not
*/
template<typename... Args, typename Trait, typename = utility::has_deduce_serialize_t<Trait, bit_reader, Args...>>
[[nodiscard]] bool serialize(Trait&& arg, Args&&... args) noexcept(utility::is_deduce_serialize_noexcept_v<Trait, bit_reader, Args...>)
{
return serialize_traits<utility::deduce_trait_t<Trait, bit_reader, Args...>>::serialize(*this, std::forward<Trait>(arg), std::forward<Args>(args)...);
}
private:
Policy m_Policy;
uint64_t m_Scratch;
uint32_t m_ScratchBits;
uint32_t m_WordIndex;
};
using fixed_bit_reader = bit_reader<fixed_policy>;
}

View File

@ -0,0 +1,400 @@
#pragma once
#include "../utility/assert.h"
#include "../utility/crc.h"
#include "../utility/endian.h"
#include "../utility/meta.h"
#include "byte_buffer.h"
#include "serialize_traits.h"
#include "stream_traits.h"
#include <cstdint>
#include <cstring>
#include <memory>
#include <type_traits>
namespace bitstream
{
/**
* @brief A stream for writing objects tightly into a buffer
* @tparam Policy The underlying representation of the buffer
*/
template<typename Policy>
class bit_writer
{
public:
static constexpr bool writing = true;
static constexpr bool reading = false;
/**
* @brief Construct a writer with the parameters passed to the underlying policy
* @param ...args The arguments to pass to the policy
*/
template<typename... Ts,
typename = std::enable_if_t<std::is_constructible_v<Policy, Ts...>>>
bit_writer(Ts&&... args)
noexcept(std::is_nothrow_constructible_v<Policy, Ts...>) :
m_Policy(std::forward<Ts>(args) ...),
m_Scratch(0),
m_ScratchBits(0),
m_WordIndex(0) {}
bit_writer(const bit_writer&) = delete;
bit_writer(bit_writer&& other) noexcept :
m_Policy(std::move(other.m_Policy)),
m_Scratch(other.m_Scratch),
m_ScratchBits(other.m_ScratchBits),
m_WordIndex(other.m_WordIndex)
{
other.m_Scratch = 0;
other.m_ScratchBits = 0;
other.m_WordIndex = 0;
}
bit_writer& operator=(const bit_writer&) = delete;
bit_writer& operator=(bit_writer&& rhs) noexcept
{
m_Policy = std::move(rhs.m_Policy);
m_Scratch = rhs.m_Scratch;
m_ScratchBits = rhs.m_ScratchBits;
m_WordIndex = rhs.m_WordIndex;
rhs.m_Scratch = 0;
rhs.m_ScratchBits = 0;
rhs.m_WordIndex = 0;
return *this;
}
/**
* @brief Returns the buffer that this writer is currently serializing into
* @return The buffer
*/
[[nodiscard]] uint8_t* get_buffer() const noexcept { return reinterpret_cast<uint8_t*>(m_Policy.get_buffer()); }
/**
* @brief Returns the number of bits which have been written to the buffer
* @return The number of bits which have been written
*/
[[nodiscard]] uint32_t get_num_bits_serialized() const noexcept { return m_Policy.get_num_bits_serialized(); }
/**
* @brief Returns the number of bytes which have been written to the buffer
* @return The number of bytes which have been written
*/
[[nodiscard]] uint32_t get_num_bytes_serialized() const noexcept { return get_num_bits_serialized() > 0U ? ((get_num_bits_serialized() - 1U) / 8U + 1U) : 0U; }
/**
* @brief Returns whether the @p num_bits can fit in the buffer
* @param num_bits The number of bits to test
* @return Whether the number of bits can fit in the buffer
*/
[[nodiscard]] bool can_serialize_bits(uint32_t num_bits) const noexcept { return m_Policy.can_serialize_bits(num_bits); }
/**
* @brief Returns the number of bits which have not been written yet
* @note The same as get_total_bits() - get_num_bits_serialized()
* @return The remaining space in the buffer
*/
[[nodiscard]] uint32_t get_remaining_bits() const noexcept { return get_total_bits() - get_num_bits_serialized(); }
/**
* @brief Returns the size of the buffer, in bits
* @return The size of the buffer, in bits
*/
[[nodiscard]] uint32_t get_total_bits() const noexcept { return m_Policy.get_total_bits(); }
/**
* @brief Flushes any remaining bits into the buffer. Use this when you no longer intend to write anything to the buffer.
* @return The number of bytes written to the buffer
*/
uint32_t flush() noexcept
{
if (m_ScratchBits > 0U)
{
uint32_t* ptr = m_Policy.get_buffer() + m_WordIndex;
uint32_t ptr_value = static_cast<uint32_t>(m_Scratch >> 32U);
*ptr = utility::to_big_endian32(ptr_value);
m_Scratch = 0U;
m_ScratchBits = 0U;
m_WordIndex++;
}
return get_num_bits_serialized();
}
/**
* @brief Instructs the writer that you intend to use `serialize_checksum()` later on, and to reserve the first 32 bits.
* @return Returns false if anything has already been written to the buffer or if there's no space to write the checksum
*/
[[nodiscard]] bool prepend_checksum() noexcept
{
BS_ASSERT(get_num_bits_serialized() == 0);
BS_ASSERT(m_Policy.extend(32U));
// Advance the reader by the size of the checksum (32 bits / 1 word)
m_WordIndex++;
return true;
}
/**
* @brief Writes a checksum of the @p protocol_version and the rest of the buffer as the first 32 bits
* @param protocol_version A unique version number
* @return The number of bytes written to the buffer
*/
uint32_t serialize_checksum(uint32_t protocol_version) noexcept
{
uint32_t num_bits = flush();
BS_ASSERT(num_bits > 32U);
// Copy protocol version to buffer
uint32_t* buffer = m_Policy.get_buffer();
*buffer = protocol_version;
// Generate checksum of version + data
uint32_t checksum = utility::crc_uint32(reinterpret_cast<uint8_t*>(buffer), get_num_bytes_serialized());
// Put checksum at beginning
*buffer = checksum;
return num_bits;
}
/**
* @brief Pads the buffer up to the given number of bytes with zeros
* @param num_bytes The byte number to pad to
* @return Returns false if the current size of the buffer is bigger than @p num_bytes
*/
[[nodiscard]] bool pad_to_size(uint32_t num_bytes) noexcept
{
uint32_t num_bits_written = get_num_bits_serialized();
BS_ASSERT(num_bytes * 8U >= num_bits_written);
BS_ASSERT(can_serialize_bits(num_bytes * 8U - num_bits_written));
if (num_bits_written == 0)
{
BS_ASSERT(m_Policy.extend(num_bytes * 8U - num_bits_written));
std::memset(m_Policy.get_buffer(), 0, num_bytes);
m_Scratch = 0;
m_ScratchBits = 0;
m_WordIndex = num_bytes / 4;
return true;
}
uint32_t remainder = (num_bytes * 8U - num_bits_written) % 32U;
uint32_t zero = 0;
// Align to byte
if (remainder != 0U)
BS_ASSERT(serialize_bits(zero, remainder));
uint32_t offset = get_num_bits_serialized() / 32;
uint32_t max = num_bytes / 4;
// Serialize words
for (uint32_t i = offset; i < max; i++)
BS_ASSERT(serialize_bits(zero, 32));
return true;
}
/**
* @brief Pads the buffer up with the given number of bytes
* @param num_bytes The amount of bytes to pad
* @return Returns false if the current size of the buffer is bigger than @p num_bytes or if the padded bits are not zeros.
*/
[[nodiscard]] bool pad(uint32_t num_bytes) noexcept
{
return pad_to_size(get_num_bytes_serialized() + num_bytes);
}
/**
* @brief Pads the buffer with up to 8 zeros, so that the next write is byte-aligned
* @return Success
*/
[[nodiscard]] bool align() noexcept
{
uint32_t remainder = m_ScratchBits % 8U;
if (remainder != 0U)
{
uint32_t zero = 0U;
bool status = serialize_bits(zero, 8U - remainder);
BS_ASSERT(status && get_num_bits_serialized() % 8U == 0U);
}
return true;
}
/**
* @brief Writes the first @p num_bits bits of @p value into the buffer
* @param value The value to serialize
* @param num_bits The number of bits of the @p value to serialize
* @return Returns false if @p num_bits is less than 1 or greater than 32 or if writing the given number of bits would overflow the buffer
*/
[[nodiscard]] bool serialize_bits(uint32_t value, uint32_t num_bits) noexcept
{
BS_ASSERT(num_bits > 0U && num_bits <= 32U);
BS_ASSERT(m_Policy.extend(num_bits));
// This is actually slower
// Possibly due to unlikely branching
/*if (num_bits == 32U && m_ScratchBits == 0U)
{
uint32_t* ptr = m_Policy.get_buffer() + m_WordIndex;
*ptr = utility::to_big_endian32(value);
m_WordIndex++;
return true;
}*/
uint32_t offset = 64U - num_bits - m_ScratchBits;
uint64_t ls_value = static_cast<uint64_t>(value) << offset;
m_Scratch |= ls_value;
m_ScratchBits += num_bits;
if (m_ScratchBits >= 32U)
{
uint32_t* ptr = m_Policy.get_buffer() + m_WordIndex;
uint32_t ptr_value = static_cast<uint32_t>(m_Scratch >> 32U);
*ptr = utility::to_big_endian32(ptr_value);
m_Scratch <<= 32ULL;
m_ScratchBits -= 32U;
m_WordIndex++;
}
return true;
}
/**
* @brief Writes the first @p num_bits bits of the given byte array, 32 bits at a time
* @param bytes The bytes to serialize
* @param num_bits The number of bits of the @p bytes to serialize
* @return Returns false if @p num_bits is less than 1 or if writing the given number of bits would overflow the buffer
*/
[[nodiscard]] bool serialize_bytes(const uint8_t* bytes, uint32_t num_bits) noexcept
{
BS_ASSERT(num_bits > 0U);
BS_ASSERT(can_serialize_bits(num_bits));
// Write the byte array as words
const uint32_t* word_buffer = reinterpret_cast<const uint32_t*>(bytes);
uint32_t num_words = num_bits / 32U;
if (m_ScratchBits % 32U == 0U && num_words > 0U)
{
BS_ASSERT(m_Policy.extend(num_words * 32U));
// If the written buffer is word-aligned, just memcpy it
std::memcpy(m_Policy.get_buffer() + m_WordIndex, word_buffer, num_words * 4U);
m_WordIndex += num_words;
}
else
{
// If the buffer is not word-aligned, serialize a word at a time
for (uint32_t i = 0U; i < num_words; i++)
{
// Casting a byte-array to an int is wrong on little-endian systems
// We have to swap the bytes around
uint32_t value = utility::to_big_endian32(word_buffer[i]);
BS_ASSERT(serialize_bits(value, 32U));
}
}
// Early exit if the word-count matches
if (num_bits % 32U == 0U)
return true;
uint32_t remaining_bits = num_bits - num_words * 32U;
uint32_t num_bytes = (remaining_bits - 1U) / 8U + 1U;
for (uint32_t i = 0U; i < num_bytes; i++)
{
uint32_t value = static_cast<uint32_t>(bytes[num_words * 4U + i]);
BS_ASSERT(serialize_bits(value, (std::min)(remaining_bits - i * 8U, 8U)));
}
return true;
}
/**
* @brief Writes the contents of the buffer into the given @p writer. Essentially copies the entire buffer without modifying it.
* @param writer The writer to copy into
* @return Returns false if writing would overflow the buffer
*/
[[nodiscard]] bool serialize_into(bit_writer& writer) const noexcept
{
uint8_t* buffer = reinterpret_cast<uint8_t*>(m_Policy.get_buffer());
uint32_t num_bits = get_num_bits_serialized();
uint32_t remainder_bits = num_bits % 8U;
BS_ASSERT(writer.serialize_bytes(buffer, num_bits - remainder_bits));
if (remainder_bits > 0U)
{
uint32_t byte_value = buffer[num_bits / 8U] >> (8U - remainder_bits);
BS_ASSERT(writer.serialize_bits(byte_value, remainder_bits));
}
return true;
}
/**
* @brief Writes to the buffer, using the given @p Trait.
* @note The Trait type in this function must always be explicitly declared
* @tparam Trait A template specialization of serialize_trait<>
* @tparam ...Args The types of the arguments to pass to the serialize function
* @param ...args The arguments to pass to the serialize function
* @return Whether successful or not
*/
template<typename Trait, typename... Args, typename = utility::has_serialize_t<Trait, bit_writer, Args...>>
[[nodiscard]] bool serialize(Args&&... args) noexcept(utility::is_serialize_noexcept_v<Trait, bit_writer, Args...>)
{
return serialize_traits<Trait>::serialize(*this, std::forward<Args>(args)...);
}
/**
* @brief Writes to the buffer, by trying to deduce the trait.
* @note The Trait type in this function is always implicit and will be deduced from the first argument if possible.
* If the trait cannot be deduced it will not compile.
* @tparam Trait The type of the first argument, which will be used to deduce the trait specialization
* @tparam ...Args The types of the arguments to pass to the serialize function
* @param arg The first argument to pass to the serialize function
* @param ...args The rest of the arguments to pass to the serialize function
* @return Whether successful or not
*/
template<typename... Args, typename Trait, typename = utility::has_deduce_serialize_t<Trait, bit_writer, Args...>>
[[nodiscard]] bool serialize(Trait&& arg, Args&&... args) noexcept(utility::is_deduce_serialize_noexcept_v<Trait, bit_writer, Args...>)
{
return serialize_traits<utility::deduce_trait_t<Trait, bit_writer, Args...>>::serialize(*this, std::forward<Trait>(arg), std::forward<Args>(args)...);
}
private:
Policy m_Policy;
uint64_t m_Scratch;
int m_ScratchBits;
size_t m_WordIndex;
};
using fixed_bit_writer = bit_writer<fixed_policy>;
template<typename T>
using growing_bit_writer = bit_writer<growing_policy<T>>;
}

View File

@ -0,0 +1,22 @@
#pragma once
#include <cstddef>
#include <cstdint>
namespace bitstream
{
/**
* @brief A byte buffer aligned to 4 bytes.
* Can be used with bit_reader and bit_writer.
* @note Size must be a multiple of 4
*/
template<size_t Size>
struct byte_buffer
{
static_assert(Size % 4 == 0, "Buffer size must be a multiple of 4");
alignas(uint32_t) uint8_t Bytes[Size];
uint8_t& operator[](size_t i) noexcept { return Bytes[i]; }
};
}

View File

@ -0,0 +1,12 @@
#pragma once
namespace bitstream
{
/**
* @brief A class for specializing trait serialization functions
* @tparam Trait Make a specialization on this type
* @tparam Void Use std::enable_if here if you need to, otherwise leave empty
*/
template<typename Trait, typename Void = void>
struct serialize_traits;
}

View File

@ -0,0 +1,96 @@
#pragma once
#include "byte_buffer.h"
#include <cstddef>
#include <cstdint>
#include <limits>
#include <type_traits>
namespace bitstream
{
struct fixed_policy
{
/**
* @brief Construct a stream pointing to the given byte array with @p num_bytes size
* @param bytes The byte array to serialize to/from. Must be 4-byte aligned and the size must be a multiple of 4
* @param num_bytes The number of bytes in the array
*/
fixed_policy(void* buffer, uint32_t num_bits) noexcept :
m_Buffer(static_cast<uint32_t*>(buffer)),
m_NumBitsSerialized(0),
m_TotalBits(num_bits) {}
/**
* @brief Construct a stream pointing to the given @p buffer
* @param buffer The buffer to serialize to/from
* @param num_bits The maximum number of bits that we can read
*/
template<size_t Size>
fixed_policy(byte_buffer<Size>& buffer, uint32_t num_bits) noexcept :
m_Buffer(reinterpret_cast<uint32_t*>(buffer.Bytes)),
m_NumBitsSerialized(0),
m_TotalBits(num_bits) {}
/**
* @brief Construct a stream pointing to the given @p buffer
* @param buffer The buffer to serialize to/from
*/
template<size_t Size>
fixed_policy(byte_buffer<Size>& buffer) noexcept :
m_Buffer(reinterpret_cast<uint32_t*>(buffer.Bytes)),
m_NumBitsSerialized(0),
m_TotalBits(Size * 8) {}
uint32_t* get_buffer() const noexcept { return m_Buffer; }
// TODO: Transition sizes to size_t
uint32_t get_num_bits_serialized() const noexcept { return m_NumBitsSerialized; }
bool can_serialize_bits(uint32_t num_bits) const noexcept { return m_NumBitsSerialized + num_bits <= m_TotalBits; }
uint32_t get_total_bits() const noexcept { return m_TotalBits; }
bool extend(uint32_t num_bits) noexcept
{
if (!can_serialize_bits(num_bits))
return false;
m_NumBitsSerialized += num_bits;
return true;
}
uint32_t* m_Buffer;
// TODO: Transition sizes to size_t
uint32_t m_NumBitsSerialized;
uint32_t m_TotalBits;
};
template<typename T>
struct growing_policy
{
growing_policy(T& container) noexcept :
m_Buffer(container),
m_NumBitsSerialized(0) {}
uint32_t* get_buffer() const noexcept { return m_Buffer.data(); }
uint32_t get_num_bits_serialized() const noexcept { return m_NumBitsSerialized; }
bool can_serialize_bits(uint32_t num_bits) const noexcept { return true; }
uint32_t get_total_bits() const noexcept { return (std::numeric_limits<uint32_t>::max)(); }
bool extend(uint32_t num_bits)
{
m_NumBitsSerialized += num_bits;
uint32_t num_bytes = (m_NumBitsSerialized - 1) / 8U + 1;
m_Buffer.resize(num_bytes);
return true;
}
T& m_Buffer;
uint32_t m_NumBitsSerialized;
};
}

View File

@ -0,0 +1,165 @@
#pragma once
#include "../utility/assert.h"
#include "../utility/meta.h"
#include "../utility/parameter.h"
#include "../stream/serialize_traits.h"
#include "../traits/bool_trait.h"
#include "../traits/integral_traits.h"
#include <cstdint>
namespace bitstream
{
/**
* @brief Wrapper type for subsets of arrays
* @tparam T The type of the array
*/
template<typename T, typename = T>
struct array_subset;
/**
* @brief A trait used for serializing a subset of an array of objects
* @tparam T The type of the object in the array
* @tparam Trait
*/
template<typename T, typename Trait>
struct serialize_traits<array_subset<T, Trait>>
{
private:
template<uint32_t Min, uint32_t Max, typename Stream>
static bool serialize_difference(Stream& stream, int& previous, int& current, uint32_t& difference)
{
bool use_bits;
if constexpr (Stream::writing)
use_bits = difference <= Max;
BS_ASSERT(stream.template serialize<bool>(use_bits));
if (use_bits)
{
using bounded_trait = bounded_int<uint32_t, Min, Max>;
BS_ASSERT(stream.template serialize<bounded_trait>(difference));
if constexpr (Stream::reading)
current = previous + difference;
previous = current;
return true;
}
return false;
}
template<typename Stream>
static bool serialize_index(Stream& stream, int& previous, int& current, int max_size)
{
uint32_t difference;
if constexpr (Stream::writing)
{
BS_ASSERT(previous < current);
difference = current - previous;
BS_ASSERT(difference > 0);
}
// +1 (1 bit)
bool plus_one;
if constexpr (Stream::writing)
plus_one = difference == 1;
BS_ASSERT(stream.template serialize<bool>(plus_one));
if (plus_one)
{
if constexpr (Stream::reading)
current = previous + 1;
previous = current;
return true;
}
// [+2,5] -> [0,3] (2 bits)
if (serialize_difference<2, 5>(stream, previous, current, difference))
return true;
// [6,13] -> [0,7] (3 bits)
if (serialize_difference<6, 13>(stream, previous, current, difference))
return true;
// [14,29] -> [0,15] (4 bits)
if (serialize_difference<14, 29>(stream, previous, current, difference))
return true;
// [30,61] -> [0,31] (5 bits)
if (serialize_difference<30, 61>(stream, previous, current, difference))
return true;
// [62,125] -> [0,63] (6 bits)
if (serialize_difference<62, 125>(stream, previous, current, difference))
return true;
// [126,MaxObjects+1]
BS_ASSERT(stream.template serialize<uint32_t>(difference, 126, max_size));
if constexpr (Stream::reading)
current = previous + difference;
previous = current;
return true;
}
public:
/**
* @brief Writes a subset of the array @p values into the writer
* @tparam Compare A function type which returns a bool
* @tparam ...Args The types of any additional arguments
* @param writer The stream to write to
* @param values The array of objects to serialize
* @param max_size The size of the array
* @param compare A function which returns true if the object should be written, false otherwise
* @param ...args Any additional arguments to use when serializing each individual object
* @return Success
*/
template<typename Stream, typename Compare, typename... Args>
typename utility::is_writing_t<Stream>
static serialize(Stream& writer, T* values, int max_size, Compare compare, Args&&... args) noexcept
{
int prev_index = -1;
for (int index = 0; index < max_size; index++)
{
if (!compare(values[index]))
continue;
BS_ASSERT(serialize_index(writer, prev_index, index, max_size));
BS_ASSERT(writer.template serialize<Trait>(values[index], std::forward<Args>(args)...));
}
BS_ASSERT(serialize_index(writer, prev_index, max_size, max_size));
return true;
}
/**
* @brief Writes a subset of a serialized array into @p values
* @tparam ...Args The types of any additional arguments
* @param reader The stream to read from
* @param values The array of objects to read into
* @param max_size The size of the array
* @param compare Not used, but kept for compatibility with the serialize write function
* @param ...args Any additional arguments to use when serializing each individual object
* @return Success
*/
template<typename Stream, typename... Args>
typename utility::is_reading_t<Stream>
static serialize(Stream& reader, T* values, int max_size, Args&&... args) noexcept
{
int prev_index = -1;
int index = 0;
while (true)
{
BS_ASSERT(serialize_index(reader, prev_index, index, max_size));
if (index == max_size)
break;
BS_ASSERT(reader.template serialize<Trait>(values[index], std::forward<Args>(args)...));
}
return true;
}
};
}

View File

@ -0,0 +1,74 @@
#pragma once
#include "../utility/assert.h"
#include "../utility/meta.h"
#include "../utility/parameter.h"
#include "../stream/serialize_traits.h"
namespace bitstream
{
/**
* @brief A trait used to serialize a boolean as a single bit
*/
template<>
struct serialize_traits<bool>
{
template<typename Stream>
typename utility::is_writing_t<Stream>
static serialize(Stream& writer, in<bool> value) noexcept
{
uint32_t unsigned_value = value;
return writer.serialize_bits(unsigned_value, 1U);
}
template<typename Stream>
typename utility::is_reading_t<Stream>
static serialize(Stream& reader, out<bool> value) noexcept
{
uint32_t unsigned_value;
BS_ASSERT(reader.serialize_bits(unsigned_value, 1U));
value = unsigned_value;
return true;
}
};
/**
* @brief A trait used to serialize multiple boolean values
*/
template<size_t Size>
struct serialize_traits<bool[Size]>
{
template<typename Stream>
typename utility::is_writing_t<Stream>
static serialize(Stream& writer, const bool* values) noexcept
{
uint32_t unsigned_value;
for (size_t i = 0; i < Size; i++)
{
unsigned_value = values[i];
BS_ASSERT(writer.serialize_bits(unsigned_value, 1U));
}
return writer.serialize_bits(unsigned_value, 1U);
}
template<typename Stream>
typename utility::is_reading_t<Stream>
static serialize(Stream& reader, bool* values) noexcept
{
uint32_t unsigned_value;
for (size_t i = 0; i < Size; i++)
{
BS_ASSERT(reader.serialize_bits(unsigned_value, 1U));
values[i] = unsigned_value;
}
return true;
}
};
}

View File

@ -0,0 +1,81 @@
#pragma once
#include "../utility/assert.h"
#include "../utility/meta.h"
#include "../utility/parameter.h"
#include "../stream/serialize_traits.h"
#include "../traits/integral_traits.h"
namespace bitstream
{
/**
* @brief Wrapper type for compiletime known integer bounds
* @tparam T
*/
template<typename T, std::underlying_type_t<T> = (std::numeric_limits<T>::min)(), std::underlying_type_t<T> = (std::numeric_limits<T>::max)()>
struct bounded_enum;
/**
* @brief A trait used to serialize an enum type with runtime bounds
*/
template<typename T>
struct serialize_traits<T, typename std::enable_if_t<std::is_enum_v<T>>>
{
using value_type = std::underlying_type_t<T>;
template<typename Stream>
typename utility::is_writing_t<Stream>
static serialize(Stream& writer, T value, value_type min = 0, value_type max = (std::numeric_limits<value_type>::max)()) noexcept
{
value_type unsigned_value = static_cast<value_type>(value);
return writer.template serialize<value_type>(unsigned_value, min, max);
}
template<typename Stream>
typename utility::is_reading_t<Stream>
static serialize(Stream& reader, T& value, value_type min = 0, value_type max = (std::numeric_limits<value_type>::max)()) noexcept
{
value_type unsigned_value;
BS_ASSERT(reader.template serialize<value_type>(unsigned_value, min, max));
value = static_cast<T>(unsigned_value);
return true;
}
};
/**
* @brief A trait used to serialize an enum type with compiletime bounds
*/
template<typename T, std::underlying_type_t<T> Min, std::underlying_type_t<T> Max>
struct serialize_traits<bounded_enum<T, Min, Max>, typename std::enable_if_t<std::is_enum_v<T>>>
{
using value_type = std::underlying_type_t<T>;
using bound_type = bounded_int<value_type, Min, Max>;
template<typename Stream>
typename utility::is_writing_t<Stream>
static serialize(Stream& writer, T value) noexcept
{
value_type unsigned_value = static_cast<value_type>(value);
return writer.template serialize<bound_type>(unsigned_value);
}
template<typename Stream>
typename utility::is_reading_t<Stream>
static serialize(Stream& reader, T& value) noexcept
{
value_type unsigned_value;
BS_ASSERT(reader.template serialize<bound_type>(unsigned_value));
value = static_cast<T>(unsigned_value);
return true;
}
};
}

View File

@ -0,0 +1,102 @@
#pragma once
#include "../utility/assert.h"
#include "../utility/meta.h"
#include "../utility/parameter.h"
#include "../stream/serialize_traits.h"
#include <cstdint>
#include <cstring>
namespace bitstream
{
/**
* @brief A trait used to serialize a float as-is, without any bound checking or quantization
*/
template<>
struct serialize_traits<float>
{
/**
* @brief Serializes a whole float into the writer
* @param writer The stream to write to
* @param value The float to serialize
* @return Success
*/
template<typename Stream>
typename utility::is_writing_t<Stream>
static serialize(Stream& writer, in<float> value) noexcept
{
uint32_t tmp;
std::memcpy(&tmp, &value, sizeof(float));
BS_ASSERT(writer.serialize_bits(tmp, 32));
return true;
}
/**
* @brief Serializes a whole float from the reader
* @param reader The stream to read from
* @param value The float to serialize to
* @return Success
*/
template<typename Stream>
typename utility::is_reading_t<Stream>
static serialize(Stream& reader, float& value) noexcept
{
uint32_t tmp;
BS_ASSERT(reader.serialize_bits(tmp, 32));
std::memcpy(&value, &tmp, sizeof(float));
return true;
}
};
/**
* @brief A trait used to serialize a double as-is, without any bound checking or quantization
*/
template<>
struct serialize_traits<double>
{
/**
* @brief Serializes a whole double into the writer
* @param writer The stream to write to
* @param value The double to serialize
* @return Success
*/
template<typename Stream>
typename utility::is_writing_t<Stream>
static serialize(Stream& writer, in<double> value) noexcept
{
uint32_t tmp[2];
std::memcpy(tmp, &value, sizeof(double));
BS_ASSERT(writer.serialize_bits(tmp[0], 32));
BS_ASSERT(writer.serialize_bits(tmp[1], 32));
return true;
}
/**
* @brief Serializes a whole double from the reader
* @param reader The stream to read from
* @param value The double to serialize to
* @return Success
*/
template<typename Stream>
typename utility::is_reading_t<Stream>
static serialize(Stream& reader, double& value) noexcept
{
uint32_t tmp[2];
BS_ASSERT(reader.serialize_bits(tmp[0], 32));
BS_ASSERT(reader.serialize_bits(tmp[1], 32));
std::memcpy(&value, tmp, sizeof(double));
return true;
}
};
}

View File

@ -0,0 +1,233 @@
#pragma once
#include "../utility/assert.h"
#include "../utility/bits.h"
#include "../utility/meta.h"
#include "../utility/parameter.h"
#include "../stream/serialize_traits.h"
#include <cstdint>
#include <limits>
#include <type_traits>
namespace bitstream
{
/**
* @brief Wrapper type for compiletime known integer bounds
* @tparam T
*/
template<typename T, T = (std::numeric_limits<T>::min)(), T = (std::numeric_limits<T>::max)()>
struct bounded_int;
#pragma region const integral types
/**
* @brief A trait used to serialize integer values with compiletime bounds
* @tparam T A type matching an integer value
* @tparam Min The lower bound. Inclusive
* @tparam Max The upper bound. Inclusive
*/
template<typename T, T Min, T Max>
struct serialize_traits<bounded_int<T, Min, Max>, typename std::enable_if_t<std::is_integral_v<T> && !std::is_const_v<T>>>
{
static_assert(sizeof(T) <= 8, "Integers larger than 8 bytes are currently not supported. You will have to write this functionality yourself");
/**
* @brief Writes an integer into the @p writer
* @param writer The stream to write to
* @param value The value to serialize
* @return Success
*/
template<typename Stream>
typename utility::is_writing_t<Stream>
static serialize(Stream& writer, in<T> value) noexcept
{
static_assert(Min < Max);
BS_ASSERT(value >= Min && value <= Max);
constexpr uint32_t num_bits = utility::bits_in_range(Min, Max);
static_assert(num_bits <= sizeof(T) * 8);
if constexpr (sizeof(T) > 4 && num_bits > 32)
{
// If the given range is bigger than a word (32 bits)
uint32_t unsigned_value = static_cast<uint32_t>(value - Min);
BS_ASSERT(writer.serialize_bits(unsigned_value, 32));
unsigned_value = static_cast<uint32_t>((value - Min) >> 32);
BS_ASSERT(writer.serialize_bits(unsigned_value, num_bits - 32));
}
else
{
// If the given range is smaller than or equal to a word (32 bits)
uint32_t unsigned_value = static_cast<uint32_t>(value - Min);
BS_ASSERT(writer.serialize_bits(unsigned_value, num_bits));
}
return true;
}
/**
* @brief Reads an integer from the @p writer into @p value
* @param reader The stream to read from
* @param value The value to serialize
* @return Success
*/
template<typename Stream>
typename utility::is_reading_t<Stream>
static serialize(Stream& reader, T& value) noexcept
{
static_assert(Min < Max);
constexpr uint32_t num_bits = utility::bits_in_range(Min, Max);
static_assert(num_bits <= sizeof(T) * 8);
if constexpr (sizeof(T) > 4 && num_bits > 32)
{
// If the given range is bigger than a word (32 bits)
value = 0;
uint32_t unsigned_value;
BS_ASSERT(reader.serialize_bits(unsigned_value, 32));
value |= static_cast<T>(unsigned_value);
BS_ASSERT(reader.serialize_bits(unsigned_value, num_bits - 32));
value |= static_cast<T>(unsigned_value) << 32;
value += Min;
}
else
{
// If the given range is smaller than or equal to a word (32 bits)
uint32_t unsigned_value;
BS_ASSERT(reader.serialize_bits(unsigned_value, num_bits));
value = static_cast<T>(unsigned_value) + Min;
}
BS_ASSERT(value >= Min && value <= Max);
return true;
}
};
#pragma endregion
#pragma region integral types
/**
* @brief A trait used to serialize integer values with runtime bounds
* @tparam T A type matching an integer value
*/
template<typename T>
struct serialize_traits<T, typename std::enable_if_t<std::is_integral_v<T> && !std::is_const_v<T>>>
{
static_assert(sizeof(T) <= 8, "Integers larger than 8 bytes are currently not supported. You will have to write this functionality yourself");
/**
* @brief Writes an integer into the @p writer
* @param writer The stream to write to
* @param value The value to serialize
* @param min The minimum bound that @p value can be. Inclusive
* @param max The maximum bound that @p value can be. Inclusive
* @return Success
*/
template<typename Stream>
typename utility::is_writing_t<Stream>
static serialize(Stream& writer, in<T> value, T min, T max) noexcept
{
BS_ASSERT(min < max);
BS_ASSERT(value >= min && value <= max);
uint32_t num_bits = utility::bits_in_range(min, max);
BS_ASSERT(num_bits <= sizeof(T) * 8);
if constexpr (sizeof(T) > 4)
{
if (num_bits > 32)
{
// If the given range is bigger than a word (32 bits)
uint32_t unsigned_value = static_cast<uint32_t>(value - min);
BS_ASSERT(writer.serialize_bits(unsigned_value, 32));
unsigned_value = static_cast<uint32_t>((value - min) >> 32);
BS_ASSERT(writer.serialize_bits(unsigned_value, num_bits - 32));
return true;
}
}
// If the given range is smaller than or equal to a word (32 bits)
uint32_t unsigned_value = static_cast<uint32_t>(value - min);
BS_ASSERT(writer.serialize_bits(unsigned_value, num_bits));
return true;
}
/**
* @brief Reads an integer from the @p reader into @p value
* @param reader The stream to read from
* @param value The value to read into
* @param min The minimum bound that @p value can be. Inclusive
* @param max The maximum bound that @p value can be. Inclusive
* @return Success
*/
template<typename Stream>
typename utility::is_reading_t<Stream>
static serialize(Stream& reader, T& value, T min, T max) noexcept
{
BS_ASSERT(min < max);
uint32_t num_bits = utility::bits_in_range(min, max);
BS_ASSERT(num_bits <= sizeof(T) * 8);
if constexpr (sizeof(T) > 4)
{
if (num_bits > 32)
{
// If the given range is bigger than a word (32 bits)
value = 0;
uint32_t unsigned_value;
BS_ASSERT(reader.serialize_bits(unsigned_value, 32));
value |= static_cast<T>(unsigned_value);
BS_ASSERT(reader.serialize_bits(unsigned_value, num_bits - 32));
value |= static_cast<T>(unsigned_value) << 32;
value += min;
BS_ASSERT(value >= min && value <= max);
return true;
}
}
// If the given range is smaller than or equal to a word (32 bits)
uint32_t unsigned_value;
BS_ASSERT(reader.serialize_bits(unsigned_value, num_bits));
value = static_cast<T>(unsigned_value) + min;
BS_ASSERT(value >= min && value <= max);
return true;
}
/**
* @brief Writes or reads an integer into the @p stream
* @param stream The stream to serialize to/from
* @param value The value to serialize
* @return Success
*/
template<typename Stream, typename U>
static bool serialize(Stream& stream, U&& value) noexcept
{
return serialize_traits<bounded_int<T>>::serialize(stream, std::forward<U>(value));
}
};
#pragma endregion
}

View File

@ -0,0 +1,113 @@
#pragma once
#include "../quantization/bounded_range.h"
#include "../quantization/half_precision.h"
#include "../quantization/smallest_three.h"
#include "../utility/assert.h"
#include "../utility/meta.h"
#include "../utility/parameter.h"
#include "../stream/serialize_traits.h"
#include <cstdint>
namespace bitstream
{
/**
* @brief A trait used to serialize a single-precision float as half-precision
*/
template<>
struct serialize_traits<half_precision>
{
template<typename Stream>
typename utility::is_writing_t<Stream>
static serialize(Stream& stream, in<float> value) noexcept
{
uint32_t int_value = half_precision::quantize(value);
BS_ASSERT(stream.serialize_bits(int_value, 16));
return true;
}
template<typename Stream>
typename utility::is_reading_t<Stream>
static serialize(Stream& stream, out<float> value) noexcept
{
uint32_t int_value;
BS_ASSERT(stream.serialize_bits(int_value, 16));
value = half_precision::dequantize(int_value);
return true;
}
};
/**
* @brief A trait used to quantize and serialize a float to be within a given range and precision
*/
template<>
struct serialize_traits<bounded_range>
{
template<typename Stream>
typename utility::is_writing_t<Stream>
static serialize(Stream& stream, in<bounded_range> range, in<float> value) noexcept
{
uint32_t int_value = range.quantize(value);
BS_ASSERT(stream.serialize_bits(int_value, range.get_bits_required()));
return true;
}
template<typename Stream>
typename utility::is_reading_t<Stream>
static serialize(Stream& stream, in<bounded_range> range, out<float> value) noexcept
{
uint32_t int_value;
BS_ASSERT(stream.serialize_bits(int_value, range.get_bits_required()));
value = range.dequantize(int_value);
return true;
}
};
/**
* @brief A trait used to quantize and serialize quaternions using the smallest-three algorithm
*/
template<typename Q, size_t BitsPerElement>
struct serialize_traits<smallest_three<Q, BitsPerElement>>
{
template<typename Stream>
typename utility::is_writing_t<Stream>
static serialize(Stream& stream, in<Q> value) noexcept
{
quantized_quaternion quantized_quat = smallest_three<Q, BitsPerElement>::quantize(value);
BS_ASSERT(stream.serialize_bits(quantized_quat.m, 2));
BS_ASSERT(stream.serialize_bits(quantized_quat.a, BitsPerElement));
BS_ASSERT(stream.serialize_bits(quantized_quat.b, BitsPerElement));
BS_ASSERT(stream.serialize_bits(quantized_quat.c, BitsPerElement));
return true;
}
template<typename Stream>
typename utility::is_reading_t<Stream>
static serialize(Stream& stream, out<Q> value) noexcept
{
quantized_quaternion quantized_quat;
BS_ASSERT(stream.serialize_bits(quantized_quat.m, 2));
BS_ASSERT(stream.serialize_bits(quantized_quat.a, BitsPerElement));
BS_ASSERT(stream.serialize_bits(quantized_quat.b, BitsPerElement));
BS_ASSERT(stream.serialize_bits(quantized_quat.c, BitsPerElement));
value = smallest_three<Q, BitsPerElement>::dequantize(quantized_quat);
return true;
}
};
}

View File

@ -0,0 +1,344 @@
#pragma once
#include "../utility/assert.h"
#include "../utility/bits.h"
#include "../utility/meta.h"
#include "../utility/parameter.h"
#include "../stream/serialize_traits.h"
#include <cstdint>
#include <string>
namespace bitstream
{
/**
* @brief Wrapper type for compiletime known string max_size
*/
template<typename T, size_t I>
struct bounded_string;
#pragma region const char*
/**
* @brief A trait used to serialize bounded c-style strings
*/
template<>
struct serialize_traits<const char*>
{
/**
* @brief Writes a c-style string into the @p writer
* @param writer The stream to write to
* @param value The string to serialize
* @param max_size The maximum expected length of the string, including the null terminator
* @return Success
*/
template<typename Stream>
typename utility::is_writing_t<Stream>
static serialize(Stream& writer, const char* value, uint32_t max_size) noexcept
{
uint32_t length = static_cast<uint32_t>(std::char_traits<char>::length(value));
BS_ASSERT(length < max_size);
if (length == 0)
return true;
uint32_t num_bits = utility::bits_to_represent(max_size);
BS_ASSERT(writer.serialize_bits(length, num_bits));
return writer.serialize_bytes(reinterpret_cast<const uint8_t*>(value), length * 8);
}
/**
* @brief Read a c-style string from the @p reader into @p value
* @param reader The stream to read from
* @param value A pointer to the buffer that should be read into. The size of this buffer should be at least @p max_size
* @param max_size The maximum expected length of the string, including the null terminator
* @return Success
*/
template<typename Stream>
typename utility::is_reading_t<Stream>
static serialize(Stream& reader, char* value, uint32_t max_size) noexcept
{
uint32_t num_bits = utility::bits_to_represent(max_size);
uint32_t length;
BS_ASSERT(reader.serialize_bits(length, num_bits));
BS_ASSERT(length < max_size);
if (length == 0)
{
value[0] = '\0';
return true;
}
BS_ASSERT(reader.serialize_bytes(reinterpret_cast<uint8_t*>(value), length * 8));
value[length] = '\0';
return true;
}
};
/**
* @brief A trait used to serialize bounded c-style strings with compiletime bounds
* @tparam MaxSize The maximum expected length of the string, including the null terminator
*/
template<size_t MaxSize>
struct serialize_traits<bounded_string<const char*, MaxSize>>
{
/**
* @brief Writes a c-style string into the @p writer
* @param writer The stream to write to
* @param value The string to serialize
* @return Success
*/
template<typename Stream>
typename utility::is_writing_t<Stream>
static serialize(Stream& writer, const char* value) noexcept
{
uint32_t length = static_cast<uint32_t>(std::char_traits<char>::length(value));
BS_ASSERT(length < MaxSize);
if (length == 0)
return true;
constexpr uint32_t num_bits = utility::bits_to_represent(MaxSize);
BS_ASSERT(writer.serialize_bits(length, num_bits));
return writer.serialize_bytes(reinterpret_cast<const uint8_t*>(value), length * 8);
}
/**
* @brief Read a c-style string from the @p reader into @p value
* @param reader The stream to read from
* @param value A pointer to the buffer that should be read into. The size of this buffer should be at least @p max_size
* @return Success
*/
template<typename Stream>
typename utility::is_reading_t<Stream>
static serialize(Stream& reader, char* value) noexcept
{
constexpr uint32_t num_bits = utility::bits_to_represent(MaxSize);
uint32_t length;
BS_ASSERT(reader.serialize_bits(length, num_bits));
BS_ASSERT(length < MaxSize);
if (length == 0)
{
value[0] = '\0';
return true;
}
BS_ASSERT(reader.serialize_bytes(reinterpret_cast<uint8_t*>(value), length * 8));
value[length] = '\0';
return true;
}
};
#pragma endregion
#ifdef __cpp_char8_t
/**
* @brief A trait used to serialize bounded c-style UTF-8 strings
*/
template<>
struct serialize_traits<const char8_t*>
{
/**
* @brief Writes a c-style UTF-8 string into the @p writer
* @param writer The stream to write to
* @param value The string to serialize
* @param max_size The maximum expected length of the string, including the null terminator
* @return Success
*/
template<typename Stream>
typename utility::is_writing_t<Stream>
static serialize(Stream& writer, const char8_t* value, uint32_t max_size) noexcept
{
uint32_t length = static_cast<uint32_t>(std::char_traits<char8_t>::length(value));
BS_ASSERT(length < max_size);
if (length == 0)
return true;
uint32_t num_bits = utility::bits_to_represent(max_size);
BS_ASSERT(writer.serialize_bits(length, num_bits));
return writer.serialize_bytes(reinterpret_cast<const uint8_t*>(value), length * 8);
}
/**
* @brief Read a c-style UTF-8 string from the @p reader into @p value
* @param reader The stream to read from
* @param value A pointer to the buffer that should be read into. The size of this buffer should be at least @p max_size
* @param max_size The maximum expected length of the string, including the null terminator
* @return Success
*/
template<typename Stream>
typename utility::is_reading_t<Stream>
static serialize(Stream& reader, char8_t* value, uint32_t max_size) noexcept
{
uint32_t num_bits = utility::bits_to_represent(max_size);
uint32_t length;
BS_ASSERT(reader.serialize_bits(length, num_bits));
BS_ASSERT(length < max_size);
if (length == 0)
{
value[0] = '\0';
return true;
}
BS_ASSERT(reader.serialize_bytes(reinterpret_cast<uint8_t*>(value), length * 8));
value[length] = '\0';
return true;
}
};
#endif
#pragma region std::basic_string
/**
* @brief A trait used to serialize any combination of std::basic_string
* @tparam T The character type to use
* @tparam Traits The trait type for the T type
* @tparam Alloc The allocator to use
*/
template<typename T, typename Traits, typename Alloc>
struct serialize_traits<std::basic_string<T, Traits, Alloc>>
{
/**
* @brief Writes a string into the @p writer
* @param writer The stream to write to
* @param value The string to serialize
* @param max_size The maximum expected length of the string, excluding the null terminator
* @return Success
*/
template<typename Stream>
typename utility::is_writing_t<Stream>
static serialize(Stream& writer, in<std::basic_string<T, Traits, Alloc>> value, uint32_t max_size) noexcept
{
uint32_t length = static_cast<uint32_t>(value.size());
BS_ASSERT(length <= max_size);
uint32_t num_bits = utility::bits_to_represent(max_size);
BS_ASSERT(writer.serialize_bits(length, num_bits));
if (length == 0)
return true;
return writer.serialize_bytes(reinterpret_cast<const uint8_t*>(value.c_str()), length * sizeof(T) * 8);
}
/**
* @brief Reads a string from the @p reader into @p value
* @param reader The stream to read from
* @param value The string to read into. It will be resized if the read string won't fit
* @param max_size The maximum expected length of the string, excluding the null terminator
* @return Success
*/
template<typename Stream>
typename utility::is_reading_t<Stream>
static serialize(Stream& reader, out<std::basic_string<T, Traits, Alloc>> value, uint32_t max_size)
{
uint32_t num_bits = utility::bits_to_represent(max_size);
uint32_t length;
BS_ASSERT(reader.serialize_bits(length, num_bits));
BS_ASSERT(length <= max_size);
if (length == 0)
{
value->clear();
return true;
}
value->resize(length);
BS_ASSERT(reader.serialize_bytes(reinterpret_cast<uint8_t*>(value->data()), length * sizeof(T) * 8));
return true;
}
};
/**
* @brief A trait used to serialize any combination of std::basic_string with compiletime bounds
* @tparam T The character type to use
* @tparam Traits The trait type for the T type
* @tparam Alloc The allocator to use
* @tparam MaxSize The maximum expected length of the string, excluding the null terminator
*/
template<typename T, typename Traits, typename Alloc, size_t MaxSize>
struct serialize_traits<bounded_string<std::basic_string<T, Traits, Alloc>, MaxSize>>
{
/**
* @brief Writes a string into the @p writer
* @param writer The stream to write to
* @param value The string to serialize
* @return Success
*/
template<typename Stream>
typename utility::is_writing_t<Stream>
static serialize(Stream& writer, in<std::basic_string<T, Traits, Alloc>> value) noexcept
{
uint32_t length = static_cast<uint32_t>(value.size());
BS_ASSERT(length <= MaxSize);
constexpr uint32_t num_bits = utility::bits_to_represent(MaxSize);
BS_ASSERT(writer.serialize_bits(length, num_bits));
if (length == 0)
return true;
return writer.serialize_bytes(reinterpret_cast<const uint8_t*>(value.c_str()), length * sizeof(T) * 8);
}
/**
* @brief Reads a string from the @p reader into @p value
* @param reader The stream to read from
* @param value The string to read into. It will be resized if the read string won't fit
* @return Success
*/
template<typename Stream>
typename utility::is_reading_t<Stream>
static serialize(Stream& reader, out<std::basic_string<T, Traits, Alloc>> value)
{
constexpr uint32_t num_bits = utility::bits_to_represent(MaxSize);
uint32_t length;
BS_ASSERT(reader.serialize_bits(length, num_bits));
BS_ASSERT(length <= MaxSize);
if (length == 0)
{
value->clear();
return true;
}
value->resize(length);
BS_ASSERT(reader.serialize_bytes(reinterpret_cast<uint8_t*>(value->data()), length * sizeof(T) * 8));
return true;
}
};
#pragma endregion
}

View File

@ -0,0 +1,18 @@
#pragma once
#ifdef BS_DEBUG_BREAK
#if defined(_WIN32) // Windows
#define BS_BREAKPOINT() __debugbreak()
#elif defined(__linux__) // Linux
#include <csignal>
#define BS_BREAKPOINT() std::raise(SIGTRAP)
#else // Non-supported
#define BS_BREAKPOINT() throw
#endif
#define BS_ASSERT(...) if (!(__VA_ARGS__)) { BS_BREAKPOINT(); return false; }
#else // BS_DEBUG_BREAK
#define BS_ASSERT(...) if (!(__VA_ARGS__)) { return false; }
#define BS_BREAKPOINT() throw
#endif // BS_DEBUG_BREAK

View File

@ -0,0 +1,26 @@
#pragma once
#include <cstddef>
#include <cstdint>
namespace bitstream::utility
{
constexpr inline uint32_t bits_to_represent(uintmax_t n)
{
uint32_t r = 0;
if (n >> 32) { r += 32U; n >>= 32U; }
if (n >> 16) { r += 16U; n >>= 16U; }
if (n >> 8) { r += 8U; n >>= 8U; }
if (n >> 4) { r += 4U; n >>= 4U; }
if (n >> 2) { r += 2U; n >>= 2U; }
if (n >> 1) { r += 1U; n >>= 1U; }
return r + static_cast<uint32_t>(n);
}
constexpr inline uint32_t bits_in_range(intmax_t min, intmax_t max)
{
return bits_to_represent(static_cast<uintmax_t>(max) - static_cast<uintmax_t>(min));
}
}

View File

@ -0,0 +1,47 @@
#pragma once
#include <array>
#include <cstdint>
namespace bitstream::utility
{
inline constexpr auto CHECKSUM_TABLE = []()
{
constexpr uint32_t POLYNOMIAL = 0xEDB88320;
std::array<uint32_t, 0x100> table{};
for (uint32_t i = 0; i < 0x100; ++i)
{
uint32_t item = i;
for (uint32_t bit = 0; bit < 8; ++bit)
item = ((item & 1) != 0) ? (POLYNOMIAL ^ (item >> 1)) : (item >> 1);
table[i] = item;
}
return table;
}();
inline constexpr uint32_t crc_uint32(const uint8_t* bytes, uint32_t size)
{
uint32_t result = 0xFFFFFFFF;
for (uint32_t i = 0; i < size; i++)
result = CHECKSUM_TABLE[(result & 0xFF) ^ *(bytes + i)] ^ (result >> 8);
return ~result;
}
inline constexpr uint32_t crc_uint32(const uint8_t* checksum, const uint8_t* bytes, uint32_t size)
{
uint32_t result = 0xFFFFFFFF;
for (uint32_t i = 0; i < 4; i++)
result = CHECKSUM_TABLE[(result & 0xFF) ^ *(checksum + i)] ^ (result >> 8);
for (uint32_t i = 0; i < size; i++)
result = CHECKSUM_TABLE[(result & 0xFF) ^ *(bytes + i)] ^ (result >> 8);
return ~result;
}
}

View File

@ -0,0 +1,89 @@
#pragma once
#include <cstdint>
#if defined(__cpp_lib_endian) && __cpp_lib_endian >= 201907L
#include <bit>
#else // __cpp_lib_endian
#ifndef BS_LITTLE_ENDIAN
// Detect with GCC 4.6's macro.
#if defined(__BYTE_ORDER__)
#if (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
#define BS_LITTLE_ENDIAN true
#elif (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
#define BS_LITTLE_ENDIAN false
#else
#error "Unknown machine byteorder endianness detected. Need to manually define BS_LITTLE_ENDIAN."
#endif
// Detect with GLIBC's endian.h.
#elif defined(__GLIBC__)
#include <endian.h>
#if (__BYTE_ORDER == __LITTLE_ENDIAN)
#define BS_LITTLE_ENDIAN true
#elif (__BYTE_ORDER == __BIG_ENDIAN)
#define BS_LITTLE_ENDIAN false
#else
#error "Unknown machine byteorder endianness detected. Need to manually define BS_LITTLE_ENDIAN."
#endif
// Detect with _LITTLE_ENDIAN and _BIG_ENDIAN macro.
#elif defined(_LITTLE_ENDIAN) && !defined(_BIG_ENDIAN)
#define BS_LITTLE_ENDIAN true
#elif defined(_BIG_ENDIAN) && !defined(_LITTLE_ENDIAN)
#define BS_LITTLE_ENDIAN false
// Detect with architecture macros.
#elif defined(__sparc) || defined(__sparc__) || defined(_POWER) || defined(__powerpc__) || defined(__ppc__) || defined(__hpux) || defined(__hppa) || defined(_MIPSEB) || defined(_POWER) || defined(__s390__)
#define BS_LITTLE_ENDIAN false
#elif defined(__i386__) || defined(__alpha__) || defined(__ia64) || defined(__ia64__) || defined(_M_IX86) || defined(_M_IA64) || defined(_M_ALPHA) || defined(__amd64) || defined(__amd64__) || defined(_M_AMD64) || defined(__x86_64) || defined(__x86_64__) || defined(_M_X64) || defined(__bfin__)
#define BS_LITTLE_ENDIAN true
#elif defined(_MSC_VER) && (defined(_M_ARM) || defined(_M_ARM64))
#define BS_LITTLE_ENDIAN true
#else
#error "Unknown machine byteorder endianness detected. Need to manually define BS_LITTLE_ENDIAN."
#endif
#endif // BS_LITTLE_ENDIAN
#endif // __cpp_lib_endian
#if defined(_WIN32)
#include <intrin.h>
#endif
namespace bitstream::utility
{
inline constexpr bool little_endian()
{
#ifdef BS_LITTLE_ENDIAN
#if BS_LITTLE_ENDIAN
return true;
#else // BS_LITTLE_ENDIAN
return false;
#endif // BS_LITTLE_ENDIAN
#else // defined(BS_LITTLE_ENDIAN)
return std::endian::native == std::endian::little;
#endif // defined(BS_LITTLE_ENDIAN)
}
inline uint32_t endian_swap32(uint32_t value)
{
#if defined(_WIN32)
return _byteswap_ulong(value);
#elif defined(__linux__)
return __builtin_bswap32(value);
#else
const uint32_t first = (value << 24) & 0xFF000000;
const uint32_t second = (value << 8) & 0x00FF0000;
const uint32_t third = (value >> 8) & 0x0000FF00;
const uint32_t fourth = (value >> 24) & 0x000000FF;
return first | second | third | fourth;
#endif // _WIN32 || __linux__
}
inline uint32_t to_big_endian32(uint32_t value)
{
if constexpr (little_endian())
return endian_swap32(value);
else
return value;
}
}

View File

@ -0,0 +1,105 @@
#pragma once
#include "../stream/serialize_traits.h"
#include <type_traits>
namespace bitstream::utility
{
// Check if type has a serializable trait
template<typename Void, typename T, typename Stream, typename... Args>
struct has_serialize : std::false_type {};
template<typename T, typename Stream, typename... Args>
struct has_serialize<std::void_t<decltype(serialize_traits<T>::serialize(std::declval<Stream&>(), std::declval<Args>()...))>, T, Stream, Args...> : std::true_type {};
template<typename T, typename Stream, typename... Args>
using has_serialize_t = std::void_t<decltype(serialize_traits<T>::serialize(std::declval<Stream&>(), std::declval<Args>()...))>;
template<typename T, typename Stream, typename... Args>
constexpr bool has_serialize_v = has_serialize<void, T, Stream, Args...>::value;
// Check if stream is writing or reading
template<typename T, typename R = bool>
using is_writing_t = std::enable_if_t<T::writing, R>;
template<typename T, typename R = bool>
using is_reading_t = std::enable_if_t<T::reading, R>;
// Check if type is noexcept, if it exists
template<typename Void, typename T, typename Stream, typename... Args>
struct is_serialize_noexcept : std::false_type {};
template<typename T, typename Stream, typename... Args>
struct is_serialize_noexcept<std::enable_if_t<has_serialize_v<T, Stream, Args...>>, T, Stream, Args...> :
std::bool_constant<noexcept(serialize_traits<T>::serialize(std::declval<Stream&>(), std::declval<Args>()...))> {};
template<typename T, typename Stream, typename... Args>
constexpr bool is_serialize_noexcept_v = is_serialize_noexcept<void, T, Stream, Args...>::value;
// Get the underlying type without &, &&, * or const
template<typename T>
using base_t = typename std::remove_const_t<std::remove_pointer_t<std::decay_t<T>>>;
// Meta functions for guessing the trait type from the first argument
template<typename Void, typename Trait, typename Stream, typename... Args>
struct deduce_trait
{
using type = Trait;
};
// Non-const value
template<typename Trait, typename Stream, typename... Args>
struct deduce_trait<std::enable_if_t<
!std::is_pointer_v<std::decay_t<Trait>> &&
has_serialize_v<base_t<Trait>, Stream, Trait, Args...>>,
Trait, Stream, Args...>
{
using type = base_t<Trait>;
};
// Const value
template<typename Trait, typename Stream, typename... Args>
struct deduce_trait<std::enable_if_t<
!std::is_pointer_v<std::decay_t<Trait>> &&
has_serialize_v<std::add_const_t<base_t<Trait>>, Stream, Trait, Args...>>,
Trait, Stream, Args...>
{
using type = std::add_const_t<base_t<Trait>>;
};
// Non-const pointer
template<typename Trait, typename Stream, typename... Args>
struct deduce_trait<std::enable_if_t<
std::is_pointer_v<std::decay_t<Trait>> &&
has_serialize_v<std::add_pointer_t<base_t<Trait>>, Stream, Trait, Args...>>,
Trait, Stream, Args...>
{
using type = std::add_pointer_t<base_t<Trait>>;
};
// Const pointer
template<typename Trait, typename Stream, typename... Args>
struct deduce_trait<std::enable_if_t<
std::is_pointer_v<std::decay_t<Trait>> &&
has_serialize_v<std::add_pointer_t<std::add_const_t<base_t<Trait>>>, Stream, Trait, Args...>>,
Trait, Stream, Args...>
{
using type = std::add_pointer_t<std::add_const_t<base_t<Trait>>>;
};
template<typename Trait, typename Stream, typename... Args>
using deduce_trait_t = typename deduce_trait<void, Trait, Stream, Args...>::type;
// Shorthands for deduced type_traits
template<typename Trait, typename Stream, typename... Args>
using has_deduce_serialize_t = has_serialize_t<deduce_trait_t<Trait, Stream, Args...>, Stream, Trait, Args...>;
template<typename Trait, typename Stream, typename... Args>
constexpr bool is_deduce_serialize_noexcept_v = is_serialize_noexcept_v<deduce_trait_t<Trait, Stream, Args...>, Stream, Trait, Args...>;
}

View File

@ -0,0 +1,124 @@
#pragma once
#include "assert.h"
#include <utility>
#include <type_traits>
#ifdef __cpp_constexpr_dynamic_alloc
#define BS_CONSTEXPR constexpr
#else // __cpp_constexpr_dynamic_alloc
#define BS_CONSTEXPR
#endif // __cpp_constexpr_dynamic_alloc
namespace bitstream
{
#ifdef BS_DEBUG_BREAK
template<typename T>
class out
{
public:
BS_CONSTEXPR out(T& value) noexcept :
m_Value(value),
m_Constructed(false) {}
out(const out&) = delete;
out(out&&) = delete;
BS_CONSTEXPR ~out()
{
if (!m_Constructed)
BS_BREAKPOINT();
}
template<typename U, typename = std::enable_if_t<std::is_assignable_v<T&, U>>>
BS_CONSTEXPR out& operator=(U&& arg) noexcept(std::is_nothrow_assignable_v<T&, U>)
{
m_Value = std::forward<U>(arg);
m_Constructed = true;
return *this;
}
BS_CONSTEXPR T* operator->() noexcept
{
m_Constructed = true;
return &m_Value;
}
BS_CONSTEXPR T& operator*() noexcept
{
m_Constructed = true;
return m_Value;
}
private:
T& m_Value;
bool m_Constructed;
};
#else
template<typename T>
class out
{
public:
BS_CONSTEXPR out(T& value) noexcept :
m_Value(value) {}
out(const out&) = delete;
out(out&&) = delete;
template<typename U, typename = std::enable_if_t<std::is_assignable_v<T&, U>>>
BS_CONSTEXPR out& operator=(U&& arg) noexcept(std::is_nothrow_assignable_v<T&, U>)
{
m_Value = std::forward<U>(arg);
return *this;
}
BS_CONSTEXPR T* operator->() noexcept { return &m_Value; }
BS_CONSTEXPR T& operator*() noexcept { return m_Value; }
private:
T& m_Value;
};
#endif
/**
* @brief Passes by const or const reference depending on size
*/
template<typename T>
using in = std::conditional_t<(sizeof(T) <= 16 && std::is_trivially_copy_constructible_v<T>), std::add_const_t<T>, std::add_lvalue_reference_t<std::add_const_t<T>>>;
/**
* @brief Passes by reference
*/
template<typename Stream, typename T>
using inout = std::conditional_t<Stream::writing, in<T>, std::add_lvalue_reference_t<T>>;
/**
* @brief Test type
*/
template<typename Lambda>
class finally
{
public:
constexpr finally(Lambda func) noexcept :
m_Lambda(func) {}
~finally()
{
m_Lambda();
}
private:
Lambda m_Lambda;
};
template<typename Lambda>
finally(Lambda func) -> finally<Lambda>;
}

View File

@ -5,7 +5,7 @@
#include <stdexcept>
#include <vector>
#include "bitstream.h"
#include "bitstream/bitstream.h"
/**
* @class FECEncoder
@ -30,26 +30,30 @@ public:
* @param data The input BitStream to be encoded.
* @return The encoded BitStream.
*/
BitStream encode(const BitStream& data) {
BitStream input_data(data);
BitStream output_data;
void encode(bitstream::growing_bit_writer<std::vector<uint8_t>>& output_data, bitstream::fixed_bit_reader& input_data) {
std::vector<uint8_t> intermediate_buffer;
bitstream::growing_bit_writer<std::vector<uint8_t>> intermediate_data(intermediate_buffer);
while (input_data.get_remaining_bits() > 0) {
uint32_t bit;
input_data.serialize_bits(bit, 1);
while (input_data.hasNext()) {
uint8_t bit = input_data.getNextBit();
// Shift the input bit into the shift register
shift_register = ((shift_register << 1) | bit) & 0x7F;
// Calculate T1 and T2 using the generator polynomials
uint8_t t1 = calculateT1();
uint8_t t2 = calculateT2();
uint32_t t1 = calculateT1();
uint32_t t2 = calculateT2();
// Append T1 and T2 to the encoded data
output_data.putBit(t1);
output_data.putBit(t2);
intermediate_data.serialize_bits(t1, 1);
intermediate_data.serialize_bits(t2, 1);
}
bitstream::fixed_bit_reader intermediate_reader(intermediate_buffer.data(), intermediate_data.get_num_bits_serialized());
// Apply repetition or puncturing based on baud rate and operation mode
return adjustRate(output_data);
return adjustRate(output_data, intermediate_reader);
}
private:
@ -62,7 +66,9 @@ private:
* @return The calculated T1 bit.
*/
uint8_t calculateT1() {
return (shift_register >> 6) ^ ((shift_register >> 4) & 0x01) ^ ((shift_register >> 3) & 0x01) ^ ((shift_register >> 1) & 0x01) ^ (shift_register & 0x01);
return (shift_register >> 6) ^ ((shift_register >> 4) & 0x01) ^
((shift_register >> 3) & 0x01) ^ ((shift_register >> 1) & 0x01) ^
(shift_register & 0x01);
}
/**
@ -70,7 +76,9 @@ private:
* @return The calculated T2 bit.
*/
uint8_t calculateT2() {
return (shift_register >> 6) ^ ((shift_register >> 5) & 0x01) ^ ((shift_register >> 4) & 0x01) ^ ((shift_register >> 1) & 0x01) ^ (shift_register & 0x01);
return (shift_register >> 6) ^ ((shift_register >> 5) & 0x01) ^
((shift_register >> 4) & 0x01) ^ ((shift_register >> 1) & 0x01) ^
(shift_register & 0x01);
}
/**
@ -78,32 +86,46 @@ private:
* @param encoded_data The encoded BitStream to be adjusted.
* @return The adjusted BitStream.
*/
BitStream adjustRate(const BitStream& encoded_data) {
BitStream adjusted_data;
void adjustRate(bitstream::growing_bit_writer<std::vector<uint8_t>>& adjusted_data, bitstream::fixed_bit_reader& encoded_data) {
size_t repetition_factor = getRepetitionFactor();
if ((baud_rate == 300 || baud_rate == 150 || baud_rate == 75) && is_frequency_hopping) {
// Repetition for frequency-hopping operation at lower baud rates
size_t repetition_factor = (baud_rate == 300) ? 2 : (baud_rate == 150) ? 4 : 8;
for (size_t i = 0; i < encoded_data.getMaxBitIndex(); i += 2) {
for (size_t j = 0; j < repetition_factor; j++) {
adjusted_data.putBit(encoded_data.getBitVal(i));
adjusted_data.putBit(encoded_data.getBitVal(i + 1));
}
if (repetition_factor == 1) {
while (encoded_data.get_remaining_bits() > 0) {
uint32_t bit;
encoded_data.serialize_bits(bit, 1);
adjusted_data.serialize_bits(bit, 1);
}
} else if ((baud_rate == 300 || baud_rate == 150) && !is_frequency_hopping) {
// Repetition for fixed-frequency operation at lower baud rates
size_t repetition_factor = (baud_rate == 300) ? 2 : 4;
for (size_t i = 0; i < encoded_data.getMaxBitIndex(); i += 2) {
for (size_t j = 0; j < repetition_factor; j++) {
adjusted_data.putBit(encoded_data.getBitVal(i));
adjusted_data.putBit(encoded_data.getBitVal(i + 1));
}
}
} else {
adjusted_data = encoded_data;
return;
}
return adjusted_data;
while (encoded_data.get_remaining_bits() >= 2) {
uint32_t t1, t2;
encoded_data.serialize_bits(t1, 1);
encoded_data.serialize_bits(t2, 1);
for (size_t j = 0; j < repetition_factor; j++) {
adjusted_data.serialize_bits(t1, 1);
adjusted_data.serialize_bits(t2, 1);
}
}
}
size_t getRepetitionFactor() const {
if (is_frequency_hopping) {
switch (baud_rate) {
case 300: return 2;
case 150: return 4;
case 75: return 8;
default: return 1;
}
} else {
switch (baud_rate) {
case 300: return 2;
case 150: return 4;
default: return 1;
}
}
}
};

View File

@ -6,7 +6,7 @@
#include <stdexcept>
#include <vector>
#include "bitstream.h"
#include "bitstream/bitstream.h"
/**
* @class Interleaver
@ -34,30 +34,43 @@ public:
* @param input_data The input BitStream to be interleaved.
* @return A new BitStream containing the interleaved data.
*/
std::vector<uint8_t> interleaveStream(const BitStream& input_data) {
BitStream data = input_data;
BitStream interleaved_data;
std::vector<uint8_t> interleaveStream(bitstream::fixed_bit_reader& input_data) {
std::vector<uint8_t> interleaved_buffer;
bitstream::growing_bit_writer<std::vector<uint8_t>> interleaved_data(interleaved_buffer);
size_t chunk_size = rows * columns;
size_t input_index = 0;
while (input_index < data.getMaxBitIndex()) {
size_t end_index = std::min(input_index + chunk_size, data.getMaxBitIndex());
BitStream chunk = data.getSubStream(input_index, end_index);
if (chunk.getMaxBitIndex() > rows * columns) {
throw std::invalid_argument("Input data exceeds interleaver matrix size in loadChunk()");
std::vector<uint8_t> chunk_data((chunk_size + 7) / 8, 0);
while (input_data.get_remaining_bits() >= chunk_size) {
std::fill(chunk_data.begin(), chunk_data.end(), 0);
if (!input_data.serialize_bytes(chunk_data.data(), chunk_size)) {
throw std::runtime_error("Failed to serialize chunk from input data");
}
BitStream interleaved_chunk = interleaveChunk(chunk);
interleaved_data += interleaved_chunk;
input_index = end_index;
bitstream::fixed_bit_reader chunk_reader(chunk_data.data(), chunk_size);
interleaveChunk(interleaved_data, chunk_reader);
}
// Apply puncturing for 2400 bps in frequency-hopping mode (Rate 2/3)
if (baud_rate == 2400 && is_frequency_hopping) {
return applyPuncturing(interleaved_data);
std::vector<uint8_t> punctured_buffer;
bitstream::growing_bit_writer<std::vector<uint8_t>> punctured_writer(punctured_buffer);
bitstream::fixed_bit_reader interleaved_reader(interleaved_buffer.data(), interleaved_buffer.size() * 8);
applyPuncturing(punctured_writer, interleaved_reader);
interleaved_buffer = punctured_buffer;
}
std::vector<uint8_t> final_interleaved_data = groupSymbols(interleaved_data);
return final_interleaved_data;
bitstream::fixed_bit_reader final_reader(interleaved_buffer.data(), interleaved_buffer.size() * 8);
return groupSymbols(final_reader);
}
/**
@ -65,7 +78,7 @@ public:
* @return The number of bits needed for a complete flush.
*/
size_t getFlushBits() const {
return rows * columns;
return (interleave_setting == 0) ? 0 : (rows * columns);
}
private:
@ -84,21 +97,16 @@ private:
* @param input_data The input BitStream to be grouped into symbols.
* @return A vector of grouped symbols.
*/
std::vector<uint8_t> groupSymbols(BitStream& input_data) {
std::vector<uint8_t> groupSymbols(bitstream::fixed_bit_reader& input_data) {
std::vector<uint8_t> grouped_data;
size_t max_index = input_data.getMaxBitIndex();
size_t bits_per_symbol = (baud_rate == 2400) ? 3 : (baud_rate == 1200 || (baud_rate == 75 && !is_frequency_hopping)) ? 2 : 1;
size_t current_index = 0;
while (input_data.get_remaining_bits() >= bits_per_symbol) {
uint32_t symbol = 0;
while ((current_index + bits_per_symbol) < max_index) {
uint8_t symbol = 0;
input_data.serialize_bits(symbol, bits_per_symbol);
for (int i = 0; i < bits_per_symbol; i++) {
symbol = (symbol << 1) | input_data.getBitVal(current_index + i);
}
grouped_data.push_back(symbol);
current_index += bits_per_symbol;
grouped_data.push_back(static_cast<uint8_t>(symbol));
}
return grouped_data;
@ -109,16 +117,16 @@ private:
* @param input_data The input BitStream chunk.
* @return A BitStream representing the interleaved chunk.
*/
BitStream interleaveChunk(const BitStream& input_data) {
void interleaveChunk(bitstream::growing_bit_writer<std::vector<uint8_t>>& interleaved_writer, bitstream::fixed_bit_reader& input_data) {
loadChunk(input_data);
return fetchChunk();
return fetchChunk(interleaved_writer);
}
/**
* @brief Loads bits from the input BitStream into the interleaver matrix.
* @param data The input BitStream to load.
*/
void loadChunk(const BitStream& data) {
void loadChunk(bitstream::fixed_bit_reader& data) {
size_t row = 0;
size_t col = 0;
size_t index = 0;
@ -127,13 +135,19 @@ private:
// Load bits into the matrix
std::fill(matrix.begin(), matrix.end(), 0); // Clear previous data
while (index < data.getMaxBitIndex() && col < columns) {
while (data.get_remaining_bits() > 0 && col < columns) {
size_t matrix_idx = row * columns + col;
if (matrix_idx >= matrix.size()) {
throw std::out_of_range("Matrix index out of bounds in loadChunk()");
}
matrix[matrix_idx] = data.getBitVal(index++);
uint32_t bit = 0;
if (!data.serialize_bits(bit, 1)) {
throw std::runtime_error("Failed to read bit from chunk_reader in loadChunk()");
}
matrix[matrix_idx] = static_cast<uint8_t>(bit);
row = (row + row_increment) % rows;
if (row == 0) {
@ -146,21 +160,23 @@ private:
* @brief Fetches bits from the interleaver matrix in the interleaved order.
* @return A BitStream containing the fetched interleaved data.
*/
BitStream fetchChunk() {
BitStream fetched_data;
void fetchChunk(bitstream::growing_bit_writer<std::vector<uint8_t>>& interleaved_writer) {
size_t row = 0;
size_t col = 0;
size_t column_decrement = (baud_rate == 75 && interleave_setting == 2) ? 7 : 17;
// Fetch bits from the matrix
while (fetched_data.getMaxBitIndex() < rows * columns) {
for (size_t i = 0; i < rows * columns; i++) {
size_t matrix_idx = row * columns + col;
if (matrix_idx >= matrix.size()) {
throw std::out_of_range("Matrix index out of bounds in fetchChunk()");
}
fetched_data.putBit(matrix[matrix_idx]);
uint32_t bit = static_cast<uint32_t>(matrix[matrix_idx]);
if (!interleaved_writer.serialize_bits(bit, 1)) {
throw std::runtime_error("Failed to write bit to interleaved_writer in fetchChunk()");
}
row++;
if (row == rows) {
@ -170,8 +186,6 @@ private:
col = (col + columns - column_decrement) % columns;
}
}
return fetched_data;
}
@ -210,14 +224,18 @@ private:
* @param interleaved_data The interleaved data to be punctured.
* @return A BitStream containing punctured data.
*/
BitStream applyPuncturing(const BitStream& interleaved_data) {
BitStream punctured_data;
for (size_t i = 0; i < interleaved_data.getMaxBitIndex(); i++) {
if ((i % 4) != 1) { // Skip every fourth bit (the second value of T2)
punctured_data.putBit(interleaved_data.getBitVal(i));
void applyPuncturing(bitstream::growing_bit_writer<std::vector<uint8_t>>& punctured_data, bitstream::fixed_bit_reader& interleaved_data) {
size_t bit_index = 0;
while (interleaved_data.get_remaining_bits() > 0) {
uint32_t bit = 0;
interleaved_data.serialize_bits(bit, 1);
if ((bit_index % 4) != 3) {
punctured_data.serialize_bits(bit, 1);
}
bit_index++;
}
return punctured_data;
}
};

View File

@ -6,7 +6,7 @@
#include <memory>
#include <vector>
#include "bitstream.h"
#include "bitstream/bitstream.h"
#include "FECEncoder.h"
#include "Interleaver.h"
#include "MGDDecoder.h"
@ -40,7 +40,7 @@ public:
* @param data The input data stream to be transmitted. The `is_voice` parameter controls whether the modem treats it as binary file data,
* or a binary stream from the MELPe (or other) voice codec.
*/
ModemController(const size_t _baud_rate, const bool _is_voice, const bool _is_frequency_hopping, const size_t _interleave_setting, BitStream _data)
ModemController(const size_t _baud_rate, const bool _is_voice, const bool _is_frequency_hopping, const size_t _interleave_setting)
: baud_rate(_baud_rate),
is_voice(_is_voice),
is_frequency_hopping(_is_frequency_hopping),
@ -49,7 +49,6 @@ public:
scrambler(),
fec_encoder(baud_rate, is_frequency_hopping),
interleaver(baud_rate, interleave_setting, is_frequency_hopping),
input_data(std::move(_data)),
mgd_decoder(baud_rate, is_frequency_hopping),
modulator(baud_rate, 48000, 0.5, is_frequency_hopping) {}
@ -58,36 +57,47 @@ public:
* @return The scrambled data ready for modulation.
* @note The modulated signal is generated internally but is intended to be handled externally.
*/
std::vector<int16_t> transmit() {
// Step 1: Append EOM Symbols
BitStream eom_appended_data = appendEOMSymbols(input_data);
std::vector<int16_t> transmit(bitstream::fixed_bit_reader& input_data) {
// Step 1: Append EOM Symbols using a uint32_t aligned output buffer
std::vector<uint8_t> output_buffer;
bitstream::growing_bit_writer<std::vector<uint8_t>> output_writer(output_buffer);
appendEOMSymbols(output_writer, input_data);
// Step 2: Handle Baud Rate Specific Encoding
std::vector<uint8_t> processed_data;
if (baud_rate == 4800) {
processed_data = splitTribitSymbols(eom_appended_data);
// For 4800 baud, perform tribit symbol splitting
bitstream::fixed_bit_reader eom_appended_reader(output_buffer.data(), output_writer.get_num_bits_serialized());
processed_data = splitTribitSymbols(eom_appended_reader);
} else {
// Step 2: FEC Encoding
BitStream fec_encoded_data = fec_encoder.encode(eom_appended_data);
// Step 3: FEC Encoding
bitstream::fixed_bit_reader eom_appended_reader(output_buffer.data(), output_writer.get_num_bits_serialized());
std::vector<uint8_t> fec_encoded_buffer;
bitstream::growing_bit_writer<std::vector<uint8_t>> fec_encoded_writer(fec_encoded_buffer);
fec_encoder.encode(fec_encoded_writer, eom_appended_reader);
// Step 3: Interleaving
processed_data = interleaver.interleaveStream(fec_encoded_data);
// Step 4: Interleaving
bitstream::fixed_bit_reader fec_encoded_reader(fec_encoded_buffer.data(), fec_encoded_writer.get_num_bits_serialized());
processed_data = interleaver.interleaveStream(fec_encoded_reader);
}
// Step 5: MGD Decoding
std::vector<uint8_t> mgd_decoded_data = mgd_decoder.mgdDecode(processed_data);
// Step 4: Symbol Formation. This function injects the sync preamble symbols. Scrambling is built-in.
// Step 6: Symbol Formation (including sync preamble and scrambling)
std::vector<uint8_t> symbol_stream = symbol_formation.formSymbols(mgd_decoded_data);
// Step 7: Modulation
std::vector<int16_t> modulated_signal = modulator.modulate(symbol_stream);
return modulated_signal;
}
private:
size_t baud_rate; ///< The baud rate for the modem.
bool is_voice; ///< Indicates if the data being transmitted is voice.
bool is_frequency_hopping; ///< Indicates if frequency hopping is used.
BitStream input_data; ///< The input data stream.
size_t interleave_setting; ///< The interleave setting to be used.
size_t sample_rate;
@ -106,38 +116,38 @@ private:
* the FEC encoder and interleaver matrices. The function calculates the number of flush bits required
* based on the FEC and interleaver settings.
*/
BitStream appendEOMSymbols(const BitStream& input_data) const {
BitStream eom_data = input_data;
void appendEOMSymbols(bitstream::growing_bit_writer<std::vector<uint8_t>>& output_data, bitstream::fixed_bit_reader& input_data) const {
while (input_data.get_num_bits_serialized() < input_data.get_total_bits()) {
uint32_t value;
uint32_t bits_to_read = std::min(32U, input_data.get_remaining_bits());
input_data.serialize_bits(value, bits_to_read);
output_data.serialize_bits(value, bits_to_read);
}
// Append the EOM sequence (4B65A5B2 in hexadecimal)
BitStream eom_sequence({0x4B, 0x65, 0xA5, 0xB2}, 32);
eom_data += eom_sequence;
uint32_t eom_sequence = 0x4B65A5B2;
output_data.serialize_bits(eom_sequence, 32);
// Append additional zeros to flush the FEC encoder and interleaver
size_t fec_flush_bits = 144; // FEC encoder flush bits
size_t interleave_flush_bits = interleaver.getFlushBits();
size_t total_flush_bits = fec_flush_bits + ((interleave_setting == 0) ? 0 : interleave_flush_bits);
while ((eom_data.getMaxBitIndex() + total_flush_bits) % interleave_flush_bits)
total_flush_bits++;
size_t total_bytes = (total_flush_bits + 7) / 8; // Round up to ensure we have enough bytes to handle all bits.
BitStream flush_bits(std::vector<uint8_t>(total_bytes, 0), total_flush_bits);
eom_data += flush_bits;
return eom_data;
size_t current_bit_index = output_data.get_num_bits_serialized();
size_t alignment_bits_needed = (interleave_flush_bits - (current_bit_index + fec_flush_bits) % interleave_flush_bits) % interleave_flush_bits;
total_flush_bits += alignment_bits_needed;
}
std::vector<uint8_t> splitTribitSymbols(const BitStream& input_data) {
std::vector<uint8_t> splitTribitSymbols(bitstream::fixed_bit_reader& input_data) {
std::vector<uint8_t> return_vector;
size_t max_index = input_data.getMaxBitIndex();
size_t current_index = 0;
size_t total_bits = input_data.get_total_bits();
size_t num_bits_serialized = input_data.get_num_bits_serialized();
while (current_index + 2 < max_index) {
uint8_t symbol = 0;
for (int i = 0; i < 3; i++) {
symbol = (symbol << 1) | input_data.getBitVal(current_index + i);
}
return_vector.push_back(symbol);
current_index += 3;
while (num_bits_serialized + 3 <= total_bits) {
uint32_t symbol = 0;
input_data.serialize_bits(symbol, 3);
return_vector.push_back(static_cast<uint8_t>(symbol));
num_bits_serialized = input_data.get_num_bits_serialized();
}
return return_vector;

View File

@ -1,220 +0,0 @@
#ifndef BITSTREAM_H
#define BITSTREAM_H
#include <algorithm>
#include <cstdint>
#include <stdexcept>
#include <vector>
/**
* @class BitStream
* @brief A class to represent a stream of bits with bit-level read and write access.
*
* The BitStream class provides functionality to manipulate a byte stream at the bit level.
* It derives from std::vector<uint8_t> to utilize the benefits of a byte vector while providing
* additional methods for bit manipulation.
*/
class BitStream : public std::vector<uint8_t> {
public:
/**
* @brief Default constructor.
*/
BitStream() : bit_index(0), max_bit_idx(0) {}
/**
* @brief Constructs a BitStream from an existing vector of bytes.
* @param data The byte stream to be used for initializing the BitStream.
*/
BitStream(const std::vector<uint8_t>& data) : std::vector<uint8_t>(data), bit_index(0), max_bit_idx(data.size() * 8) {}
/**
* @brief Constructs a BitStream from an existing vector of bytes with a specified bit size.
* @param data The byte stream to be used for initializing the BitStream.
* @param size_in_bits The number of bits to consider in the stream.
*/
BitStream(const std::vector<uint8_t>& data, size_t size_in_bits) : std::vector<uint8_t>(data), bit_index(0), max_bit_idx(size_in_bits) {}
/**
* @brief Copy constructor from another BitStream.
* @param data The BitStream to copy from.
*/
BitStream(const BitStream& data) : std::vector<uint8_t>(data), bit_index(0), max_bit_idx(data.max_bit_idx) {}
/**
* @brief Constructs a BitStream from a substream of another BitStream.
* @param other The original BitStream.
* @param start_bit The starting bit index of the substream.
* @param end_bit The ending bit index of the substream (exclusive).
* @throws std::out_of_range if start or end indices are out of bounds.
*/
BitStream(const BitStream& other, size_t start_bit, size_t end_bit) : bit_index(0) {
if (start_bit >= other.max_bit_idx || end_bit > other.max_bit_idx || start_bit > end_bit) {
throw std::out_of_range("BitStream substream indices are out of range.");
}
max_bit_idx = end_bit - start_bit;
for (size_t i = start_bit; i < end_bit; i++) {
putBit(other.getBitVal(i));
}
}
/**
* @brief Reads the next bit from the stream.
* @return The next bit (0 or 1).
* @throws std::out_of_range if no more bits are available in the stream.
*/
int getNextBit() {
if (bit_index >= max_bit_idx) {
throw std::out_of_range("No more bits available in the stream.");
}
int bit = getBitVal(bit_index++);
return bit;
}
/**
* @brief Gets the value of a bit at a specific index.
* @param idx The index of the bit to be retrieved.
* @return The value of the bit (0 or 1).
* @throws std::out_of_range if the bit index is out of range.
*/
int getBitVal(const size_t idx) const {
if (idx >= max_bit_idx) {
throw std::out_of_range("Bit index out of range in getBitVal.");
}
size_t byte_idx = idx / 8;
size_t bit_idx = idx % 8;
uint8_t tmp = this->at(byte_idx);
uint8_t mask = 0x80 >> bit_idx;
uint8_t result = tmp & mask;
return result ? 1 : 0;
}
/**
* @brief Checks if there are more bits available in the stream.
* @return True if there are more bits available, otherwise false.
*/
bool hasNext() const {
return bit_index < max_bit_idx;
}
/**
* @brief Sets a specific bit value in the stream.
* @param idx The index of the bit to set.
* @param val The value to set the bit to (0 or 1).
*
* This function ensures that the stream has enough bytes to accommodate
* the given bit index. If the bit index is out of bounds, the stream is
* resized accordingly.
*/
void setBitVal(const size_t idx, uint8_t val) {
size_t byte_idx = idx / 8;
size_t bit_idx = idx % 8;
uint8_t mask = 0x80 >> bit_idx;
if (byte_idx >= this->size()) {
this->resize(byte_idx + 1, 0);
}
if (val == 0) {
this->at(byte_idx) &= ~mask;
} else {
this->at(byte_idx) |= mask;
}
}
/**
* @brief Appends a bit to the end of the stream.
* @param bit The value of the bit to append (0 or 1).
*
* This function keeps track of the current bit index and appends bits
* sequentially. If necessary, the stream is resized to accommodate the new bit.
*/
void putBit(uint8_t bit) {
size_t byte_idx = max_bit_idx / 8;
if (byte_idx >= this->size()) {
this->push_back(0);
}
size_t bit_idx = max_bit_idx % 8;
setBitVal(max_bit_idx, bit);
max_bit_idx += 1;
}
/**
* @brief Resets the bit index to the beginning of the stream.
*/
void resetBitIndex() {
bit_index = 0;
}
/**
* @brief Returns the maximum bit index value (total number of bits in the stream).
* @return The total number of bits in the stream.
*/
size_t getMaxBitIndex() const {
return max_bit_idx;
}
BitStream& operator=(const BitStream& other) {
this->clear();
this->resize(other.size());
std::copy(other.begin(), other.end(), this->begin());
this->bit_index = other.bit_index;
this->max_bit_idx = other.max_bit_idx;
return *this;
}
/**
* @brief Adds the contents of another BitStream to the current BitStream.
* @param other The BitStream to be added.
* @return Reference to the current BitStream after adding.
*/
BitStream& operator+=(const BitStream& other) {
size_t other_max_bit_idx = other.getMaxBitIndex();
for (size_t i = 0; i < other_max_bit_idx; i++) {
this->putBit(other.getBitVal(i));
}
return *this;
}
/**
* @brief Gets a substream from the current BitStream.
* @param start_bit The starting bit index of the substream.
* @param end_bit The ending bit index of the substream (exclusive).
* @return A new BitStream containing the specified substream.
* @throws std::out_of_range if start or end indices are out of bounds.
*/
BitStream getSubStream(size_t start_bit, size_t end_bit) const {
if (start_bit >= max_bit_idx || end_bit > max_bit_idx || start_bit > end_bit) {
throw std::out_of_range("BitStream substream indices are out of range.");
}
BitStream substream;
for (size_t i = start_bit; i < end_bit; i++) {
substream.putBit(getBitVal(i));
}
return substream;
}
/**
* @brief Returns the current bit index in the stream.
* @return The current bit index.
*/
size_t getCurrentBitIndex() const {
return bit_index;
}
private:
size_t bit_index; ///< The current bit index in the stream.
size_t max_bit_idx; ///< The total number of bits in the stream.
};
BitStream operator+(const BitStream& lhs, const BitStream& rhs) {
BitStream result = lhs;
result += rhs;
return result;
}
#endif