From 3306edc514a996a7c61986d4851c9e6cfa323fca Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Mon, 25 Mar 2024 00:38:31 -0400 Subject: add jitter buffer for voice --- src/audio/jitterbuffer.hpp | 82 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/audio/jitterbuffer.hpp (limited to 'src/audio/jitterbuffer.hpp') diff --git a/src/audio/jitterbuffer.hpp b/src/audio/jitterbuffer.hpp new file mode 100644 index 0000000..3da3594 --- /dev/null +++ b/src/audio/jitterbuffer.hpp @@ -0,0 +1,82 @@ +#pragma once +#include +#include +#include + +// very simple non-RTP-based jitter buffer. does not handle out-of-order +template +class JitterBuffer { +public: + /* + * desired_latency: how many milliseconds before audio can be drawn from buffer + * maximum_latency: how many milliseconds before old audio starts to be discarded + */ + JitterBuffer(int desired_latency, int maximum_latency, int channels, int sample_rate) + : m_desired_latency(desired_latency) + , m_maximum_latency(maximum_latency) + , m_channels(channels) + , m_sample_rate(sample_rate) + , m_last_push(std::chrono::steady_clock::now()) { + } + + [[nodiscard]] size_t Available() const noexcept { + return m_samples.size(); + } + + bool PopSamples(SampleFormat *ptr, size_t amount) { + CheckBuffering(); + if (m_buffering || Available() < amount) return false; + std::copy(m_samples.begin(), m_samples.begin() + amount, ptr); + m_samples.erase(m_samples.begin(), m_samples.begin() + amount); + return true; + } + + void PushSamples(SampleFormat *ptr, size_t amount) { + m_samples.insert(m_samples.end(), ptr, ptr + amount); + m_last_push = std::chrono::steady_clock::now(); + const auto buffered = MillisBuffered(); + if (buffered > m_maximum_latency) { + const auto overflow_ms = MillisBuffered() - m_maximum_latency; + const auto overflow_samples = overflow_ms * m_channels * m_sample_rate / 1000; + m_samples.erase(m_samples.begin(), m_samples.begin() + overflow_samples); + } + } + +private: + [[nodiscard]] size_t MillisBuffered() const { + return m_samples.size() * 1000 / m_channels / m_sample_rate; + } + + void CheckBuffering() { + // if we arent buffering but the buffer is empty then we should be + if (m_samples.empty()) { + if (!m_buffering) { + m_buffering = true; + } + return; + } + + if (!m_buffering) return; + + // if we reached desired latency, we are sufficiently buffered + const auto millis_buffered = MillisBuffered(); + if (millis_buffered >= m_desired_latency) { + m_buffering = false; + } + // if we havent buffered to desired latency but max latency has elapsed, exit buffering so it doesnt get stuck + const auto now = std::chrono::steady_clock::now(); + const auto millis = std::chrono::duration_cast(now - m_last_push).count(); + if (millis >= m_maximum_latency) { + m_buffering = false; + } + } + + int m_desired_latency; + int m_maximum_latency; + int m_channels; + int m_sample_rate; + bool m_buffering = true; + std::chrono::time_point m_last_push; + + std::deque m_samples; +}; -- cgit v1.2.3