diff --git a/tools/chatgui/src/audio_event_queue.h b/tools/chatgui/src/audio_event_queue.h index 658a7507..94811639 100644 --- a/tools/chatgui/src/audio_event_queue.h +++ b/tools/chatgui/src/audio_event_queue.h @@ -1,24 +1,28 @@ +/* Ограниченная FIFO MPSC: несколько producers, ровно один consumer. reset — только при остановленных участниках. + * Резервирование и публикация разделены sequence слота; ещё не опубликованная голова блокирует чтение следующих элементов. + * Обычные данные используют Capacity-8 мест, control может занять всю ёмкость; приоритета обхода FIFO у команд нет. + * T должен допускать конструирование по умолчанию и копирующее присваивание без исключений; стоимость копирования задаёт T. */ #pragma once #include #include #include #include -/* Ограниченная очередь с несколькими producers. Producer не меняет позицию consumer. - * Резервирование и публикация разделены sequence каждого слота; callback не ждёт занятый слот. - * reset допустим только до запуска всех участников. Последние 8 слотов зарезервированы для команд. */ template class AudioEventQueue { static_assert(Capacity > 8); - struct Slot { std::atomic sequence{0}; T value{}; }; - std::array m_slots; - alignas(64) std::atomic m_write{0}; - alignas(64) std::atomic m_read{0}; + struct Slot { std::atomic sequence{0}; T value{}; }; // Номер оборота/готовность публикации и собственная копия T. + std::array m_slots; // Фиксированные слоты, без динамического роста. + alignas(64) std::atomic m_write{0}; // Следующая позиция резервирования, общая для producers. + alignas(64) std::atomic m_read{0}; // Следующая позиция чтения; изменяет только consumer. public: AudioEventQueue() { reset(); } + /* Сбрасывает позиции/sequence без очистки сохранённых T; разрешён до запуска либо после завершения всех участников. */ void reset() { m_write = 0; m_read = 0; for (size_t i = 0; i < Capacity; ++i) m_slots[i].sequence = i; } + /* Копирует value в зарезервированный слот и публикует; true — принято, false — ёмкость/слот недоступны либо конкуренция. + * Не ждёт освобождения слота, не более восьми попыток резервирования; false не означает строго полную очередь. */ bool push(const T& value, bool control = false) { uint64_t position = m_write.load(std::memory_order_relaxed); for (int attempt = 0; attempt < 8; ++attempt) { @@ -37,6 +41,8 @@ public: } return false; } + /* Только consumer: true копирует следующий опубликованный элемент; false оставляет value неизменным. + * false возможен и при size()>0, если producer головы ещё выполняет копирование. */ bool pop(T& value) { uint64_t position = m_read.load(std::memory_order_relaxed); auto& slot = m_slots[position % Capacity]; @@ -46,6 +52,7 @@ public: m_read.store(position + 1, std::memory_order_release); return true; } + /* Приблизительный снимок 0..Capacity, включая резервирования до публикации; не гарантия успеха pop/push. */ size_t size() const { uint64_t read = m_read.load(std::memory_order_acquire); uint64_t count = m_write.load(std::memory_order_acquire) - read; diff --git a/tools/chatgui/src/voice_capture_clock.h b/tools/chatgui/src/voice_capture_clock.h index 2408cb5e..e8167b7f 100644 --- a/tools/chatgui/src/voice_capture_clock.h +++ b/tools/chatgui/src/voice_capture_clock.h @@ -1,18 +1,23 @@ +/* Capture worker переводит непрерывную позицию PCM 48кГц в монотонное время, плавно уточняя привязку по backend. + * sequence — индекс PCM-фрейма на канал, observedUs — наблюдаемое время первого фрейма в микросекундах. + * Сам не обнаруживает разрывы: владелец вызывает reset при потере sequence, смене route или скачке времени. */ #pragma once #include #include #include -// Позиция PCM задаёт непрерывное время; backend уточняет привязку без скачков на каждом обновлении latency. -// Единственный владелец — capture worker. Настоящий разрыв sequence/route/time вызывает reset у владельца. struct VoiceCaptureClock { - bool initialized = false; - uint64_t originSequence = 0, previousSequence = 0; - int64_t originUs = 0, errorUs = 0, adjustmentUs = 0; - double phaseUs = 0; + bool initialized = false; // Получена первая пара sequence/observedUs. + uint64_t originSequence = 0, previousSequence = 0; // Начальная и последняя позиции в непрерывном отрезке PCM. + int64_t originUs = 0, errorUs = 0, adjustmentUs = 0; // Начальное время, ошибка перед коррекцией и последняя коррекция, мкс. + double phaseUs = 0; // Накопленная плавная поправка к номинальной шкале 48кГц, мкс. + /* Начинает новый независимый отрезок, очищая привязку и диагностические метрики. */ void reset() { *this = {}; } + /* sequence должен не убывать и не переполняться до следующего reset. Первый вызов возвращает observedUs. + * Далее возвращает originUs + номинальное время PCM + фазовую поправку: фильтр с постоянной 2с, + * изменение поправки не более 0.5% прошедшего PCM-времени (100мкс на 20мс); errorUs/adjustmentUs обновляются. */ int64_t map(uint64_t sequence, int64_t observedUs) { if (!initialized) { initialized = true; originSequence = previousSequence = sequence; originUs = observedUs; diff --git a/tools/chatgui/src/voice_playback_buffer.h b/tools/chatgui/src/voice_playback_buffer.h index b9883a46..d7cac04f 100644 --- a/tools/chatgui/src/voice_playback_buffer.h +++ b/tools/chatgui/src/voice_playback_buffer.h @@ -1,17 +1,23 @@ +/* Политика резерва готового PCM desktop, единственный владелец — RX worker. Сам PCM/очередь не хранит. + * Резерв 1..4 блока по 960 фреймов/20мс (48кГц); частично сыгранный блок находится сверх этого резерва. + * bufferMs=20/40/60/80 проверяет VoiceAudioIo::start, конструктор этого класса не валидирует аргумент. + * Четыре места данных доступны в AudioEventQueue; у звонка sample вызывается по progress вывода. */ #pragma once #include #include -/* Резерв готового PCM desktop: bufferMs = 20..80, кратно 20 (проверяет VoiceAudioIo::start). - * Размер callback и настроенный запас задают нижнюю границу; недостача добавляет - * один блок, 10 секунд стабильного вывода убирают один. Частично сыгранный блок — сверх резерва. - * Только RX worker; время монотонное. Четыре PCM-слота доступны в AudioEventQueue. */ class VoicePlaybackBuffer { public: + /* Сохраняет настроенный минимум bufferMs/20 и сбрасывает адаптацию. */ explicit VoicePlaybackBuffer(unsigned bufferMs = 20) : minimumBlocks(bufferMs / 20) { reset(); } - unsigned blocks = 1; - unsigned callbackFrames = 960; + unsigned blocks = 1; // Текущая цель полной очереди, от настроенного минимума до четырёх блоков. + unsigned callbackFrames = 960; // Размер последнего callback из sample, фреймов на канал. + /* Возвращает настроенный минимум и 10с пробный интервал, забывая историю starvation и callback. */ void reset() { blocks = minimumBlocks; callbackFrames = 960; stableSince = 0; lastGrowth = 0; lastSample = 0; probeUs = 10000000; } + /* nowUs — монотонные мкс; frames — полный размер выходного callback, missing — нехватка хотя бы одного PCM-фрейма. + * Минимум — max(настройка, ceil(frames/960), ограниченное 1..4). При missing рост на блок не чаще 500мс; + * каждая такая попытка удваивает интервал снижения с 10с до 300с. Стабильный вывод убирает по одному блоку + * после этого интервала; пауза sample >1с сбрасывает стабильность, но не адаптированный интервал. */ void sample(int64_t nowUs, unsigned frames, bool missing) { callbackFrames = frames; unsigned minimum = std::max(minimumBlocks, std::clamp((frames + 959) / 960, 1u, 4u)); @@ -29,6 +35,6 @@ public: } } private: - unsigned minimumBlocks; - int64_t stableSince = 0, lastGrowth = 0, lastSample = 0, probeUs = 10000000; + unsigned minimumBlocks; // Настроенный минимум, не уменьшается адаптацией. + int64_t stableSince = 0, lastGrowth = 0, lastSample = 0, probeUs = 10000000; // Начало стабильности/рост/опрос/интервал снижения, мкс. };