From efe879bfd99d40c34c5d6ef07372d6567cc6c9e2 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 03:42:03 +0100 Subject: [PATCH] Preserve complete audio frames and reject invalid ring buffers --- tools/chatgui/CMakeLists.txt | 5 ++ tools/chatgui/src/audio_frame_ring.h | 82 +++++++++++------ tools/chatgui/tests/test_audio_frame_ring.cpp | 90 +++++++++++++++++++ 3 files changed, 152 insertions(+), 25 deletions(-) create mode 100644 tools/chatgui/tests/test_audio_frame_ring.cpp diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index e30e36d6..92c91a95 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -597,6 +597,11 @@ target_include_directories(test_config_updater PRIVATE ${CMAKE_SOURCE_DIR}) target_link_libraries(test_config_updater PRIVATE ${QT_CORE} ${QT_TEST_LIB} utun pthread) add_test(NAME test_config_updater COMMAND test_config_updater) +add_executable(test_audio_frame_ring tests/test_audio_frame_ring.cpp) +target_link_libraries(test_audio_frame_ring PRIVATE ${QT_CORE} ${QT_TEST_LIB} utun pthread) +add_test(NAME test_audio_frame_ring COMMAND test_audio_frame_ring) +set_tests_properties(test_audio_frame_ring PROPERTIES TIMEOUT 30) + add_executable(test_node_config tests/test_node_config.cpp transport/node_config.cpp transport/config_updater.cpp) target_link_libraries(test_node_config PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun pthread) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") diff --git a/tools/chatgui/src/audio_frame_ring.h b/tools/chatgui/src/audio_frame_ring.h index 40fe58eb..0d4bd64e 100644 --- a/tools/chatgui/src/audio_frame_ring.h +++ b/tools/chatgui/src/audio_frame_ring.h @@ -1,67 +1,99 @@ #pragma once #include +#include #include #include +#include #include +#include "../../../lib/debug_config.h" -/* Кольцевой буфер кадров переменной длины (SPSC, mutex-protected). +/* Кольцевой буфер непрозрачных кадров переменной длины под mutex. * - * Producer (uasync-поток) и consumer (аудио-поток). Память предвыделена одним - * блоком — ноль аллокаций на кадр. При переполнении дропается старейший кадр - * (семантика джиттер-буфера). + * push/pop/clear/size допускаются из разных потоков, включая нескольких producers/consumers. + * Данные копируются: буферы вызывающего нужны только до возврата вызова. + * Два вектора (данные и длины) выделяются конструктором, без аллокаций на кадр. + * Переполнение штатно заменяет старейший кадр. Инициализация/clear логируются в MEDIA/DEBUG, + * неверные аргументы — MEDIA/ERROR. Перед уничтожением остановить всех пользователей. */ class AudioFrameRing { public: - AudioFrameRing(int capacity, int slotSize) + // capacity/slotSize > 0; неверные размеры дают invalid_argument, переполнение — length_error. + // Ошибка выделения памяти даёт стандартное исключение vector; все отказы логируются. + AudioFrameRing(int capacity, int slotSize) try : cap_(capacity), slotSize_(slotSize), - buf_((size_t)capacity * (size_t)slotSize, 0), - lens_((size_t)capacity, 0) {} + buf_(buffer_size(capacity, slotSize), 0), lens_((size_t)capacity, 0) { + DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "AudioFrameRing: initialized capacity=%d slot_size=%d bytes=%zu", capacity, slotSize, buf_.size()); + } catch (const std::exception& error) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "AudioFrameRing: initialization failed capacity=%d slot_size=%d: %s", + capacity, slotSize, error.what()); + throw; + } - /* Producer: положить кадр. Возвращает false если len > slotSize. - * При переполнении дропает старейший кадр (успех). */ + /* Скопировать data[0..len), 1<=len<=slotSize; false при NULL/неверной длине, очередь не меняется. + * При переполнении дропает старейший кадр (true). */ bool push(const uint8_t* data, int len) { - if (len <= 0 || len > slotSize_) return false; + if (!data || len <= 0 || len > slotSize_) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "AudioFrameRing: push rejected data_present=%d len=%d slot_size=%d", data != nullptr, len, slotSize_); + return false; + } std::lock_guard lk(mtx_); if (count_ == cap_) { head_ = (head_ + 1) % cap_; /* drop oldest */ count_--; } std::memcpy(buf_.data() + (size_t)tail_ * slotSize_, data, (size_t)len); - lens_[(size_t)tail_] = (uint16_t)len; + lens_[(size_t)tail_] = len; tail_ = (tail_ + 1) % cap_; count_++; return true; } - /* Consumer: взять старейший кадр. Возвращает длину (0 = пусто). */ + /* Скопировать старейший кадр и удалить его, вернуть положительную длину; 0 — пусто. + * NULL out/недостаточный outCap дают -1, кадр остаётся для повторной попытки. */ int pop(uint8_t* out, int outCap) { - std::lock_guard lk(mtx_); + std::unique_lock lk(mtx_); if (count_ == 0) return 0; int len = (int)lens_[(size_t)head_]; - if (out && len > 0 && len <= outCap) - std::memcpy(out, buf_.data() + (size_t)head_ * slotSize_, (size_t)len); + if (!out || outCap < len) { + lk.unlock(); + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "AudioFrameRing: pop rejected output_present=%d capacity=%d required=%d", out != nullptr, outCap, len); + return -1; + } + std::memcpy(out, buf_.data() + (size_t)head_ * slotSize_, (size_t)len); head_ = (head_ + 1) % cap_; count_--; return len; } - void clear() { - std::lock_guard lk(mtx_); - head_ = tail_ = count_ = 0; + void clear() { // удалить все кадры; память сохраняется, следующий push начинает новую очередь + int removed; + { + std::lock_guard lk(mtx_); + removed = count_; + head_ = tail_ = count_ = 0; + } + DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "AudioFrameRing: cleared frames=%d capacity=%d", removed, cap_); } - int size() const { + int size() const { // моментальный снимок количества; между size и pop возможны другие операции std::lock_guard lk(mtx_); return count_; } - int capacity() const { return cap_; } + int capacity() const { return cap_; } // неизменная ёмкость в кадрах private: - int cap_, slotSize_; - int head_ = 0, tail_ = 0, count_ = 0; - std::vector buf_; - std::vector lens_; - mutable std::mutex mtx_; + static size_t buffer_size(int capacity, int slotSize) { + if (capacity <= 0 || slotSize <= 0) throw std::invalid_argument("capacity and slot size must be positive"); + if ((size_t)capacity > std::vector().max_size() / (size_t)slotSize) + throw std::length_error("frame buffer size exceeds vector limit"); + return (size_t)capacity * (size_t)slotSize; + } + + const int cap_, slotSize_; // ёмкость в кадрах, максимум байтов на кадр + int head_ = 0, tail_ = 0, count_ = 0; // индексы чтения/записи и заполнение, под mtx_ + std::vector buf_; // cap_*slotSize_ байтов, слот выбирается индексом кадра + std::vector lens_; // фактическая длина каждого занятого слота без сужения uint16_t + mutable std::mutex mtx_; // сериализует копирование и изменение индексов }; diff --git a/tools/chatgui/tests/test_audio_frame_ring.cpp b/tools/chatgui/tests/test_audio_frame_ring.cpp new file mode 100644 index 00000000..4d85dc58 --- /dev/null +++ b/tools/chatgui/tests/test_audio_frame_ring.cpp @@ -0,0 +1,90 @@ +/* Целые кадры, переполнение, ошибочные аргументы и конкурентные producers/consumer. */ +#include "../src/audio_frame_ring.h" +#include +#include +#include +#include + +class AudioFrameRingTest : public QObject { + Q_OBJECT +private slots: + void initTestCase() { debug_config_init(); debug_enable_console(1); debug_set_level(DEBUG_LEVEL_WARN); } + + void invalidGeometry() { + for (int capacity : {-1, 0, 1}) for (int slot : {-1, 0, 1}) { + if (capacity > 0 && slot > 0) continue; + QVERIFY_EXCEPTION_THROWN((void)AudioFrameRing(capacity, slot), std::invalid_argument); + } + } + + void overflowDropsOnlyOldest() { + AudioFrameRing ring(2, 1); + for (uint8_t value : {1, 2, 3}) QVERIFY(ring.push(&value, 1)); + QCOMPARE(ring.size(), 2); QCOMPARE(ring.capacity(), 2); + uint8_t value = 0; + QCOMPARE(ring.pop(&value, 1), 1); QCOMPARE(value, uint8_t(2)); + QCOMPARE(ring.pop(&value, 1), 1); QCOMPARE(value, uint8_t(3)); + QCOMPARE(ring.pop(nullptr, 0), 0); + } + + void rejectsArgumentsWithoutConsumingFrame() { + AudioFrameRing ring(3, 4); + uint8_t data[] = {1, 2, 3, 4}; + QVERIFY(ring.push(data, 4)); + QVERIFY(!ring.push(nullptr, 1)); QVERIFY(!ring.push(data, 0)); + QVERIFY(!ring.push(data, -1)); QVERIFY(!ring.push(data, 5)); + QCOMPARE(ring.pop(nullptr, 4), -1); + uint8_t output[] = {9, 9, 9, 9}; + QCOMPARE(ring.pop(output, 3), -1); QCOMPARE(output[0], uint8_t(9)); + QCOMPARE(ring.size(), 1); + QCOMPARE(ring.pop(output, 4), 4); QCOMPARE(memcmp(output, data, 4), 0); + } + + void preservesLengthBeyondUint16() { + AudioFrameRing ring(2, 70000); + const std::vector data(65536, 0x5a); + std::vector output(data.size() + 1, 0xa5); + QVERIFY(ring.push(data.data(), int(data.size()))); + QCOMPARE(ring.pop(output.data(), int(data.size())), int(data.size())); + QCOMPARE(memcmp(data.data(), output.data(), data.size()), 0); + QCOMPARE(output.back(), uint8_t(0xa5)); + } + + void clearAfterWrappedQueue() { + AudioFrameRing ring(3, 1); + uint8_t value = 7; + for (int i = 0; i < 8; ++i) QVERIFY(ring.push(&value, 1)); + ring.clear(); QCOMPARE(ring.size(), 0); QCOMPARE(ring.pop(&value, 1), 0); + value = 9; QVERIFY(ring.push(&value, 1)); value = 0; + QCOMPARE(ring.pop(&value, 1), 1); QCOMPARE(value, uint8_t(9)); + } + + void concurrentFrameIntegrity() { + constexpr int producers = 4, perProducer = 1000, total = producers * perProducer; + AudioFrameRing ring(total, sizeof(uint32_t)); + std::atomic done{0}; + std::atomic failed{false}; + std::vector threads; + const auto join = qScopeGuard([&] { for (auto& thread : threads) if (thread.joinable()) thread.join(); }); + for (int producer = 0; producer < producers; ++producer) threads.emplace_back([&, producer] { + for (uint32_t i = 0; i < perProducer; ++i) { + const uint32_t value = producer * perProducer + i; + if (!ring.push(reinterpret_cast(&value), sizeof(value))) failed = true; + } + ++done; + }); + std::vector seen(total, false); + int received = 0; + while (done.load() < producers || ring.size()) { + uint32_t value = 0; + const int length = ring.pop(reinterpret_cast(&value), sizeof(value)); + if (!length) { std::this_thread::yield(); continue; } + QCOMPARE(length, int(sizeof(value))); QVERIFY(value < total); QVERIFY(!seen[value]); + seen[value] = true; ++received; + } + QVERIFY(!failed.load()); QCOMPARE(received, total); + } +}; + +QTEST_GUILESS_MAIN(AudioFrameRingTest) +#include "test_audio_frame_ring.moc"