30 changed files with 1838 additions and 411 deletions
@ -0,0 +1,54 @@
|
||||
#pragma once |
||||
#include <array> |
||||
#include <atomic> |
||||
#include <cstddef> |
||||
#include <cstdint> |
||||
|
||||
/* Ограниченная очередь с несколькими producers. Producer не меняет позицию consumer.
|
||||
* Резервирование и публикация разделены sequence каждого слота; callback не ждёт занятый слот. |
||||
* reset допустим только до запуска всех участников. Последние 8 слотов зарезервированы для команд. */ |
||||
template<class T, size_t Capacity> class AudioEventQueue { |
||||
static_assert(Capacity > 8); |
||||
struct Slot { std::atomic<uint64_t> sequence{0}; T value{}; }; |
||||
std::array<Slot, Capacity> m_slots; |
||||
alignas(64) std::atomic<uint64_t> m_write{0}; |
||||
alignas(64) std::atomic<uint64_t> m_read{0}; |
||||
public: |
||||
AudioEventQueue() { reset(); } |
||||
void reset() { |
||||
m_write = 0; m_read = 0; |
||||
for (size_t i = 0; i < Capacity; ++i) m_slots[i].sequence = i; |
||||
} |
||||
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) { |
||||
uint64_t read = m_read.load(std::memory_order_acquire); |
||||
if (read > position) { position = m_write.load(std::memory_order_relaxed); continue; } |
||||
if (position - read >= Capacity - (control ? 0 : 8)) return false; |
||||
auto& slot = m_slots[position % Capacity]; |
||||
int64_t difference = (int64_t)(slot.sequence.load(std::memory_order_acquire) - position); |
||||
if (difference < 0) return false; |
||||
if (difference == 0 && m_write.compare_exchange_weak(position, position + 1, std::memory_order_relaxed)) { |
||||
slot.value = value; |
||||
slot.sequence.store(position + 1, std::memory_order_release); |
||||
return true; |
||||
} |
||||
position = m_write.load(std::memory_order_relaxed); |
||||
} |
||||
return false; |
||||
} |
||||
bool pop(T& value) { |
||||
uint64_t position = m_read.load(std::memory_order_relaxed); |
||||
auto& slot = m_slots[position % Capacity]; |
||||
if (slot.sequence.load(std::memory_order_acquire) != position + 1) return false; |
||||
value = slot.value; |
||||
slot.sequence.store(position + Capacity, std::memory_order_release); |
||||
m_read.store(position + 1, std::memory_order_release); |
||||
return true; |
||||
} |
||||
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; |
||||
return (size_t)(count > Capacity ? Capacity : count); // Приблизительный снимок, включая ещё не опубликованные слоты.
|
||||
} |
||||
}; |
||||
@ -0,0 +1,331 @@
|
||||
#include "voice_audio_io.h" |
||||
extern "C" { |
||||
#include "call/call_audio.h" |
||||
#include "radio/radio_audio.h" |
||||
#include "speex_aec.h" |
||||
#include "debug_config.h" |
||||
} |
||||
#include <algorithm> |
||||
#include <array> |
||||
#include <chrono> |
||||
#include <cmath> |
||||
#include <cstring> |
||||
#include <system_error> |
||||
|
||||
static int64_t voice_time_us() { |
||||
return std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now().time_since_epoch()).count(); |
||||
} |
||||
|
||||
VoiceAudioIo::~VoiceAudioIo() { stop(); } |
||||
|
||||
bool VoiceAudioIo::start(Kind kind, uint64_t session) { |
||||
stop(); |
||||
m_kind = kind; m_session = session; |
||||
m_captureEvents.reset(); m_references.reset(); m_playback.reset(); |
||||
m_playPosition = 960; m_playEpoch = 1; |
||||
m_captureDropped = m_referenceDropped = m_playbackUnderruns = 0; |
||||
m_running = true; |
||||
try { m_playbackThread = std::thread(&VoiceAudioIo::playbackLoop, this); } |
||||
catch (const std::system_error& error) { |
||||
m_running = false; |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "voice-io: RX worker start failed reason=%s", error.what()); return false; |
||||
} |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "voice-io: started session=%llu kind=%s PCM reserve<=40ms", |
||||
(unsigned long long)session, kind == Kind::Call ? "call" : "radio"); |
||||
return true; |
||||
} |
||||
|
||||
void VoiceAudioIo::stop() { |
||||
stopCapture(); |
||||
m_running = false; m_wake.notify_all(); |
||||
if (m_playbackThread.joinable()) m_playbackThread.join(); |
||||
} |
||||
|
||||
bool VoiceAudioIo::startCapture(int channels, bool aecEnabled) { |
||||
stopCapture(); |
||||
if (!m_running || channels < 1 || channels > 2 || (m_kind == Kind::Call && channels != 1)) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "voice-io: invalid capture channels=%d", channels); return false; |
||||
} |
||||
m_captureEvents.reset(); m_channels = channels; m_captureSequence = 0; |
||||
m_aecRequested = aecEnabled; m_capturing = true; |
||||
try { m_captureThread = std::thread(&VoiceAudioIo::captureLoop, this); } |
||||
catch (const std::system_error& error) { |
||||
m_capturing = false; |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "voice-io: capture worker start failed reason=%s", error.what()); return false; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
void VoiceAudioIo::stopCapture() { |
||||
if (m_captureThread.joinable()) { |
||||
if (m_capturing) command(EventKind::Stop); |
||||
m_captureThread.join(); |
||||
} |
||||
m_capturing = false; |
||||
} |
||||
|
||||
/* GUI-команда не теряется при заполнении PCM: зарезервированы слоты; отказ завершает capture. */ |
||||
void VoiceAudioIo::command(EventKind kind, bool value) { |
||||
if (!m_capturing) return; |
||||
Event event{}; event.kind = kind; event.value = value; |
||||
for (int attempt = 0; attempt < 100; ++attempt) { |
||||
if (m_captureEvents.push(event, true)) { m_wake.notify_all(); return; } |
||||
m_wake.notify_all(); std::this_thread::sleep_for(std::chrono::milliseconds(1)); |
||||
} |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "voice-io: control queue full session=%llu command=%d; stopping capture", |
||||
(unsigned long long)m_session, (int)kind); |
||||
m_capturing = false; m_wake.notify_all(); |
||||
} |
||||
void VoiceAudioIo::setPtt(bool pressed) { command(EventKind::Ptt, pressed); } |
||||
void VoiceAudioIo::setMuted(bool muted) { command(EventKind::Mute, muted); } |
||||
void VoiceAudioIo::setAecEnabled(bool enabled) { |
||||
if (enabled == m_aecRequested) return; |
||||
m_aecRequested = enabled; command(EventKind::Aec, enabled); |
||||
} |
||||
void VoiceAudioIo::resetPlayback() { ++m_playEpoch; m_wake.notify_all(); } |
||||
|
||||
void VoiceAudioIo::capture(const int16_t* pcm, unsigned frames, int64_t firstTimeUs, uint64_t route, bool missing) { |
||||
if (!pcm || !m_capturing) return; |
||||
if (missing) { m_captureSequence += frames; m_captureDropped.fetch_add(frames); return; } |
||||
int64_t enqueued = voice_time_us(); |
||||
if (!firstTimeUs) firstTimeUs = enqueued - (int64_t)frames * 1000000 / 48000; |
||||
unsigned offset = 0; |
||||
while (offset < frames) { |
||||
Event event{}; event.frames = std::min(frames - offset, 960u); |
||||
event.time = firstTimeUs + (int64_t)offset * 1000000 / 48000; |
||||
event.sequence = m_captureSequence; m_captureSequence += event.frames; |
||||
event.route = route; |
||||
event.enqueued = enqueued; |
||||
memcpy(event.pcm, pcm + offset * m_channels, event.frames * m_channels * sizeof(int16_t)); |
||||
if (!m_captureEvents.push(event)) m_captureDropped.fetch_add(event.frames, std::memory_order_relaxed); |
||||
offset += event.frames; |
||||
} |
||||
m_wake.notify_all(); |
||||
} |
||||
|
||||
void VoiceAudioIo::reference(const float* pcm, unsigned frames, int64_t firstTimeUs) { |
||||
if (!m_capturing) return; |
||||
if (!firstTimeUs) firstTimeUs = voice_time_us(); |
||||
for (unsigned offset = 0; offset < frames;) { |
||||
Event event{}; event.frames = std::min(frames - offset, 960u); |
||||
event.time = firstTimeUs + (int64_t)offset * 1000000 / 48000; |
||||
event.sequence = m_playEpoch.load(); |
||||
for (unsigned i = 0; i < event.frames * 2; ++i) { |
||||
float sample = pcm[offset * 2 + i]; |
||||
event.pcm[i] = (int16_t)std::clamp(sample * 32768.0f, -32768.0f, 32767.0f); |
||||
} |
||||
if (!m_references.push(event)) m_referenceDropped.fetch_add(event.frames, std::memory_order_relaxed); |
||||
offset += event.frames; |
||||
} |
||||
m_wake.notify_all(); |
||||
} |
||||
|
||||
/* Выход всегда обслуживается независимо от capture/VAD. Уже потреблённый PCM становится референсом у владельца микшера. */ |
||||
void VoiceAudioIo::mix(float* stereo, unsigned frames) { |
||||
uint64_t epoch = m_playEpoch.load(); |
||||
for (unsigned i = 0; i < frames; ++i) { |
||||
if (m_playCurrent.epoch != epoch) m_playPosition = 960; |
||||
if (m_playPosition == 960) { |
||||
bool available = false; |
||||
for (int attempt = 0; attempt < 12 && m_playback.pop(m_playCurrent); ++attempt) { |
||||
if (m_playCurrent.epoch == epoch) { available = true; m_playPosition = 0; break; } |
||||
} |
||||
if (!available) { m_playbackUnderruns.fetch_add(frames - i, std::memory_order_relaxed); break; } |
||||
} |
||||
stereo[2*i] += m_playCurrent.pcm[2*m_playPosition] / 32768.0f; |
||||
stereo[2*i+1] += m_playCurrent.pcm[2*m_playPosition+1] / 32768.0f; |
||||
++m_playPosition; |
||||
} |
||||
m_wake.notify_all(); |
||||
} |
||||
|
||||
void VoiceAudioIo::playbackLoop() { |
||||
uint64_t epoch = m_playEpoch.load(); |
||||
while (m_running) { |
||||
uint64_t requested = m_playEpoch.load(); |
||||
if (requested != epoch) { |
||||
if (m_kind == Kind::Call) call_audio_reset_io(m_session, 1); |
||||
else radio_audio_reset_playback(m_session); |
||||
epoch = requested; |
||||
} |
||||
if (m_playback.size() < 2) { |
||||
Playback block{}; block.epoch = epoch; |
||||
if (m_kind == Kind::Call) { |
||||
int16_t mono[960]{}; |
||||
int count = std::clamp(call_audio_pull_pcm(m_session, mono, 960), 0, 960); |
||||
for (int i = 0; i < count; ++i) block.pcm[2*i] = block.pcm[2*i+1] = mono[i]; |
||||
} else radio_audio_pull_pcm(m_session, block.pcm, 1920); |
||||
if (!m_playback.push(block)) DEBUG_WARN(DEBUG_CATEGORY_CALL, "voice-io: playback queue contention"); |
||||
} else { |
||||
std::unique_lock lock(m_waitMutex); |
||||
m_wake.wait_for(lock, std::chrono::milliseconds(5)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/* Референс хранится как PCM + время, а не как фиксированный двухкадровый FIFO независимых часов. */ |
||||
void VoiceAudioIo::captureLoop() { |
||||
std::array<Event, 96> history{}; |
||||
size_t historyHead = 0, historyCount = 0; |
||||
uint64_t referenceEpoch = m_playEpoch.load(), referenceDrops = m_referenceDropped.load(); |
||||
speex_aec_t* aec = nullptr; |
||||
auto configureAec = [&](bool enabled) { |
||||
speex_aec_destroy(aec); aec = nullptr; |
||||
if (enabled) aec = speex_aec_create_mc(48000, 960, 14400, 1, m_channels, 2); |
||||
if (enabled && !aec) DEBUG_ERROR(DEBUG_CATEGORY_AEC, "voice-io: AEC unavailable session=%llu", (unsigned long long)m_session); |
||||
DEBUG_INFO(DEBUG_CATEGORY_AEC, "voice-io: capture AEC=%d mic=%d render=2 session=%llu", |
||||
aec != nullptr, m_channels, (unsigned long long)m_session); |
||||
}; |
||||
configureAec(m_aecRequested); |
||||
struct Marker { unsigned position; EventKind kind; bool value; }; |
||||
std::array<Marker, 64> markers{}; |
||||
size_t markerCount = 0; |
||||
int16_t raw[1920]{}, cleaned[1920]{}, referencePcm[1920]{}; |
||||
uint8_t muteMask[960]{}; |
||||
unsigned filled = 0; |
||||
int64_t frameTime = 0, lastReport = voice_time_us(); |
||||
uint64_t expectedSequence = 0, missingReference = 0, reportedReferenceDrops = referenceDrops, captureRoute = 0; |
||||
int64_t expectedCaptureTime = 0; |
||||
bool referenceAvailable = false; |
||||
bool muted = false; |
||||
auto applyMarker = [&](EventKind kind, bool value) { |
||||
if (kind == EventKind::Mute) muted = value; |
||||
else if (kind == EventKind::Ptt && m_kind == Kind::Radio) { |
||||
if (value) radio_audio_talk_begin(m_session); else radio_audio_talk_end(m_session); |
||||
} |
||||
}; |
||||
auto consumeReference = [&]() { |
||||
uint64_t epoch = m_playEpoch.load(), drops = m_referenceDropped.load(); |
||||
if (epoch != referenceEpoch || drops != referenceDrops) { |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_AEC, "voice-io: reference reset epoch=%llu->%llu dropped=%llu", |
||||
(unsigned long long)referenceEpoch, (unsigned long long)epoch, (unsigned long long)(drops - referenceDrops)); |
||||
historyHead = historyCount = 0; |
||||
if (aec) speex_aec_reset(aec); |
||||
referenceEpoch = epoch; referenceDrops = drops; |
||||
} |
||||
Event event{}; |
||||
for (unsigned drained = 0; drained < 32 && m_references.pop(event); ++drained) { |
||||
if (event.sequence != referenceEpoch) continue; |
||||
if (historyCount && event.time <= history[(historyHead + historyCount - 1) % history.size()].time) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_AEC, "voice-io: non-monotonic render clock, resetting AEC"); |
||||
historyHead = historyCount = 0; |
||||
if (aec) speex_aec_reset(aec); |
||||
} |
||||
if (historyCount == history.size()) { historyHead = (historyHead + 1) % history.size(); --historyCount; } |
||||
history[(historyHead + historyCount++) % history.size()] = event; |
||||
} |
||||
}; |
||||
auto alignReference = [&]() { |
||||
if (!historyCount) return false; |
||||
size_t index = 0; |
||||
for (unsigned i = 0; i < 960; ++i) { |
||||
double time = frameTime + (double)i * 1000000.0 / 48000.0; |
||||
while (index + 1 < historyCount && history[(historyHead + index + 1) % history.size()].time <= time) ++index; |
||||
auto& block = history[(historyHead + index) % history.size()]; |
||||
double interval = 1000000.0 / 48000.0; |
||||
if (index + 1 < historyCount) { |
||||
auto& next = history[(historyHead + index + 1) % history.size()]; |
||||
double measured = (double)(next.time - block.time) / block.frames; |
||||
if (measured > interval * 0.95 && measured < interval * 1.05) interval = measured; |
||||
} |
||||
double position = (time - block.time) / interval; |
||||
if (position < 0 || position >= block.frames) return false; |
||||
unsigned sample = (unsigned)position; |
||||
double fraction = position - sample; |
||||
for (unsigned channel = 0; channel < 2; ++channel) { |
||||
int first = block.pcm[2*sample+channel]; |
||||
int second = sample + 1 < block.frames ? block.pcm[2*(sample+1)+channel] : first; |
||||
referencePcm[2*i+channel] = (int16_t)(first + (second - first) * fraction); |
||||
} |
||||
} |
||||
return true; |
||||
}; |
||||
auto deliver = [&](unsigned frames) { |
||||
unsigned begin = 0; |
||||
for (size_t mark = 0; mark <= markerCount; ++mark) { |
||||
unsigned end = mark < markerCount ? markers[mark].position : frames; |
||||
if (m_kind == Kind::Call) memset(muteMask + begin, muted, end - begin); |
||||
else if (end > begin) radio_audio_feed_pcm(m_session, cleaned + begin * m_channels, (end - begin) * m_channels); |
||||
if (mark < markerCount) applyMarker(markers[mark].kind, markers[mark].value); |
||||
begin = end; |
||||
} |
||||
if (m_kind == Kind::Call && frames == 960) call_audio_feed_prepared_pcm(m_session, cleaned, muteMask); |
||||
filled = 0; markerCount = 0; |
||||
}; |
||||
bool stopping = false; |
||||
while (m_capturing && !stopping) { |
||||
consumeReference(); |
||||
Event event{}; |
||||
if (!m_captureEvents.pop(event)) { |
||||
std::unique_lock lock(m_waitMutex); m_wake.wait_for(lock, std::chrono::milliseconds(5)); continue; |
||||
} |
||||
if (event.kind == EventKind::Stop) { stopping = true; break; } |
||||
if (event.kind == EventKind::Aec) { configureAec(event.value); continue; } |
||||
if (event.kind != EventKind::Pcm) { |
||||
if (!filled) applyMarker(event.kind, event.value); |
||||
else if (markerCount < markers.size()) markers[markerCount++] = {filled, event.kind, event.value}; |
||||
else { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "voice-io: too many controls within one capture frame"); stopping = true; } |
||||
continue; |
||||
} |
||||
if (voice_time_us() - event.enqueued > 100000) { |
||||
m_captureDropped.fetch_add(event.frames); continue; |
||||
} |
||||
bool clockJump = expectedCaptureTime && std::abs(event.time - expectedCaptureTime) > 20000; |
||||
if (event.sequence != expectedSequence || (captureRoute && event.route != captureRoute) || clockJump) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "voice-io: capture discontinuity expected=%llu actual=%llu pending=%u route=%016llx->%016llx", |
||||
(unsigned long long)expectedSequence, (unsigned long long)event.sequence, filled, |
||||
(unsigned long long)captureRoute, (unsigned long long)event.route); |
||||
if (clockJump) DEBUG_WARN(DEBUG_CATEGORY_AEC, "voice-io: capture clock jump=%lldus", (long long)(event.time - expectedCaptureTime)); |
||||
for (size_t i = 0; i < markerCount; ++i) applyMarker(markers[i].kind, markers[i].value); |
||||
filled = 0; markerCount = 0; |
||||
if (aec) speex_aec_reset(aec); |
||||
if (m_kind == Kind::Radio) radio_audio_capture_discontinuity(m_session); |
||||
} |
||||
expectedSequence = event.sequence + event.frames; |
||||
captureRoute = event.route; |
||||
expectedCaptureTime = event.time + (int64_t)event.frames * 1000000 / 48000; |
||||
for (unsigned offset = 0; offset < event.frames;) { |
||||
if (!filled) frameTime = event.time + (int64_t)offset * 1000000 / 48000; |
||||
unsigned take = std::min(960 - filled, event.frames - offset); |
||||
memcpy(raw + filled * m_channels, event.pcm + offset * m_channels, take * m_channels * sizeof(int16_t)); |
||||
filled += take; offset += take; |
||||
if (filled != 960) continue; |
||||
memcpy(cleaned, raw, 960 * m_channels * sizeof(int16_t)); |
||||
consumeReference(); |
||||
bool available = aec && alignReference(); |
||||
if (available) { |
||||
speex_aec_feed_playback(aec, referencePcm, 1920); |
||||
speex_aec_process_capture(aec, raw, 960 * m_channels, cleaned); |
||||
} else if (aec) { |
||||
++missingReference; |
||||
if (referenceAvailable) speex_aec_reset(aec); |
||||
} |
||||
referenceAvailable = available; |
||||
deliver(960); |
||||
} |
||||
int64_t now = voice_time_us(); |
||||
if (now - lastReport >= 1000000) { |
||||
uint64_t captureDrops = m_captureDropped.exchange(0), refDrops = m_referenceDropped.load(); |
||||
uint64_t underruns = m_playbackUnderruns.exchange(0); |
||||
if (captureDrops || refDrops != reportedReferenceDrops || missingReference || underruns) |
||||
DEBUG_WARN(DEBUG_CATEGORY_AEC, "voice-io: session=%llu capture_drop=%llu reference_drop=%llu missing_ref=%llu output_missing=%llu", |
||||
(unsigned long long)m_session, (unsigned long long)captureDrops, (unsigned long long)refDrops, |
||||
(unsigned long long)missingReference, (unsigned long long)underruns); |
||||
else DEBUG_DEBUG(DEBUG_CATEGORY_AEC, "voice-io: session=%llu aligned history=%zu capture_queue=%zu", |
||||
(unsigned long long)m_session, historyCount, m_captureEvents.size()); |
||||
missingReference = 0; reportedReferenceDrops = refDrops; lastReport = now; |
||||
} |
||||
} |
||||
if (filled) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "voice-io: capture tail=%u frames bypasses AEC; call tail discarded, radio tail preserved", filled); |
||||
memcpy(cleaned, raw, filled * m_channels * sizeof(int16_t)); |
||||
deliver(filled); |
||||
} |
||||
uint64_t lostCapture = m_captureDropped.exchange(0), lostReference = m_referenceDropped.load() - reportedReferenceDrops; |
||||
if (lostCapture || lostReference || missingReference) |
||||
DEBUG_WARN(DEBUG_CATEGORY_AEC, "voice-io: capture stopped lost_capture=%llu lost_reference=%llu missing_reference=%llu", |
||||
(unsigned long long)lostCapture, (unsigned long long)lostReference, (unsigned long long)missingReference); |
||||
|
||||
speex_aec_destroy(aec); |
||||
m_capturing = false; |
||||
} |
||||
@ -0,0 +1,61 @@
|
||||
#pragma once |
||||
#include "audio_event_queue.h" |
||||
#include <atomic> |
||||
#include <condition_variable> |
||||
#include <mutex> |
||||
#include <thread> |
||||
#include <cstdint> |
||||
|
||||
/* Desktop: аппаратные callbacks только копируют PCM, render также выполняет готовое смешивание.
|
||||
* Capture worker — единственный владелец AEC и порядка PCM/PTT/mute. RX worker независим от VAD. |
||||
* Управляющий поток закрывает capture и отсоединяет output до stop/destruction. |
||||
* Финальный stereo render приходит с того же выходного устройства после всего микширования. */ |
||||
class VoiceAudioIo { |
||||
public: |
||||
enum class Kind { Call, Radio }; |
||||
VoiceAudioIo() = default; |
||||
~VoiceAudioIo(); |
||||
bool start(Kind kind, uint64_t session); |
||||
void stop(); |
||||
bool startCapture(int channels, bool aecEnabled); |
||||
void stopCapture(); |
||||
bool captureRunning() const { return m_capturing.load(); } |
||||
void setPtt(bool pressed); |
||||
void setMuted(bool muted); |
||||
void setAecEnabled(bool enabled); |
||||
void resetPlayback(); |
||||
/* firstTimeUs — монотонное время первого физического PCM-фрейма (backend либо оценка). */ |
||||
void capture(const int16_t* pcm, unsigned frames, int64_t firstTimeUs, uint64_t route = 0, bool missing = false); |
||||
void reference(const float* pcm, unsigned frames, int64_t firstTimeUs); |
||||
void mix(float* stereo, unsigned frames); |
||||
private: |
||||
enum class EventKind { Pcm, Ptt, Mute, Aec, Stop }; |
||||
struct Event { |
||||
EventKind kind = EventKind::Pcm; |
||||
unsigned frames = 0; |
||||
int64_t time = 0; |
||||
int64_t enqueued = 0; |
||||
uint64_t sequence = 0; |
||||
uint64_t route = 0; |
||||
bool value = false; |
||||
int16_t pcm[1920]{}; |
||||
}; |
||||
struct Playback { uint64_t epoch = 0; int16_t pcm[1920]{}; }; |
||||
AudioEventQueue<Event, 32> m_captureEvents, m_references; |
||||
AudioEventQueue<Playback, 12> m_playback; |
||||
std::atomic<bool> m_running{false}, m_capturing{false}; |
||||
std::atomic<uint64_t> m_playEpoch{0}, m_captureDropped{0}, m_referenceDropped{0}, m_playbackUnderruns{0}; |
||||
uint64_t m_captureSequence = 0; // Только capture callback; reset после его остановки.
|
||||
Kind m_kind = Kind::Call; |
||||
uint64_t m_session = 0; |
||||
int m_channels = 1; |
||||
std::atomic<bool> m_aecRequested{false}; |
||||
std::thread m_captureThread, m_playbackThread; |
||||
std::mutex m_waitMutex; |
||||
std::condition_variable m_wake; |
||||
Playback m_playCurrent{}; |
||||
unsigned m_playPosition = 960; |
||||
void command(EventKind kind, bool value = false); |
||||
void captureLoop(); |
||||
void playbackLoop(); |
||||
}; |
||||
@ -0,0 +1,50 @@
|
||||
/* MPSC: резерв управляющих сообщений, публикация слота и порядок каждого producer. */ |
||||
#include "../src/audio_event_queue.h" |
||||
#include <thread> |
||||
#include <vector> |
||||
#include <cstdio> |
||||
#include <cstdlib> |
||||
#define REQUIRE(x) do { if (!(x)) { fprintf(stderr, "queue:%d %s\n", __LINE__, #x); abort(); } } while (0) |
||||
|
||||
static std::atomic<bool> entered{false}, releaseCopy{false}; |
||||
struct Item { |
||||
int producer = 0, sequence = 0; |
||||
bool block = false; |
||||
Item& operator=(const Item& other) { |
||||
if (other.block) { entered = true; while (!releaseCopy) std::this_thread::yield(); } |
||||
producer = other.producer; sequence = other.sequence; block = other.block; return *this; |
||||
} |
||||
}; |
||||
int main() { |
||||
AudioEventQueue<Item, 16> queue; |
||||
Item item{}; |
||||
for (int i = 0; i < 8; ++i) REQUIRE(queue.push(item)); |
||||
REQUIRE(!queue.push(item)); |
||||
for (int i = 0; i < 8; ++i) REQUIRE(queue.push(item, true)); |
||||
REQUIRE(!queue.push(item, true)); |
||||
for (int i = 0; i < 16; ++i) REQUIRE(queue.pop(item)); |
||||
REQUIRE(!queue.pop(item)); |
||||
std::thread first([&] { REQUIRE(queue.push({0, 123, true})); }); |
||||
while (!entered) std::this_thread::yield(); |
||||
REQUIRE(queue.push({1, 456, false}, true)); |
||||
REQUIRE(!queue.pop(item)); // Второй слот опубликован, первый ещё принадлежит producer.
|
||||
releaseCopy = true; first.join(); |
||||
REQUIRE(queue.pop(item) && item.sequence == 123); |
||||
REQUIRE(queue.pop(item) && item.sequence == 456); |
||||
queue.reset(); |
||||
constexpr int producers = 4, count = 20000; |
||||
std::vector<std::thread> threads; |
||||
for (int p = 0; p < producers; ++p) threads.emplace_back([&, p] { |
||||
for (int i = 0; i < count; ++i) while (!queue.push({p, i, false}, i % 7 == 0)) std::this_thread::yield(); |
||||
}); |
||||
int expected[producers]{}; |
||||
for (int total = 0; total < producers * count;) { |
||||
if (!queue.pop(item)) { std::this_thread::yield(); continue; } |
||||
REQUIRE(queue.size() <= 16); |
||||
REQUIRE(item.producer >= 0 && item.producer < producers); |
||||
REQUIRE(item.sequence == expected[item.producer]++); ++total; |
||||
} |
||||
for (auto& thread : threads) thread.join(); |
||||
REQUIRE(queue.size() == 0); |
||||
puts("audio queue: control reserve, unpublished slot, 80000 ordered messages PASS"); |
||||
} |
||||
@ -0,0 +1,57 @@
|
||||
/* PA snapshot без сервера: несколько converter callbacks принадлежат одному physical PCM-блоку. */ |
||||
#define MA_DATA_CONVERTER_STACK_BUFFER_SIZE 128 |
||||
#include "../transport/miniaudio_impl.c" |
||||
#include <assert.h> |
||||
#include <stdio.h> |
||||
static int64_t times[64]; |
||||
static unsigned counts[64], seen; |
||||
static int capture_missing; |
||||
static int fake_latency(const void* stream, uint64_t* latency, int* negative) { |
||||
(void)stream; *latency = 100000; *negative = 0; return 0; |
||||
} |
||||
static void observe(ma_device* device, void* output, const void* input, ma_uint32 frames) { |
||||
(void)output; (void)input; |
||||
int measured = 0; |
||||
assert(seen < 64); |
||||
times[seen] = audio_stream_frame_time(device, frames, &measured); |
||||
counts[seen++] = frames; |
||||
assert(measured && seen < 64); |
||||
assert(audio_stream_capture_missing(device) == capture_missing); |
||||
} |
||||
static int64_t time_us(void) { |
||||
struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); |
||||
return now.tv_sec * INT64_C(1000000) + now.tv_nsec / 1000; |
||||
} |
||||
int main(void) { |
||||
/* Только dispatch/converter: backend union не подменяется у живого null-device. */ |
||||
ma_context context = {0}; ma_device device = {0}; |
||||
device.pContext = &context; device.type = ma_device_type_capture; |
||||
device.sampleRate = 48000; device.noFixedSizedCallback = MA_TRUE; device.onData = observe; |
||||
device.capture.format = ma_format_s16; device.capture.channels = 1; |
||||
device.capture.internalFormat = ma_format_f32; device.capture.internalChannels = 2; |
||||
assert(ma_device_set_master_volume(&device, 1) == MA_SUCCESS); |
||||
ma_data_converter_config converter = ma_data_converter_config_init(ma_format_f32, ma_format_s16, 2, 1, 48000, 48000); |
||||
assert(ma_data_converter_init(&converter, NULL, &device.capture.converter) == MA_SUCCESS); |
||||
device.capture.internalSampleRate = 48000; |
||||
context.backend = ma_backend_pulseaudio; |
||||
context.pulse.pa_stream_get_latency = (ma_proc)fake_latency; |
||||
device.pulse.pStreamCapture = (void*)1; |
||||
float input[180 * 2] = {0}; |
||||
int64_t now = time_us(); |
||||
assert(ma_device_handle_backend_data_callback(&device, NULL, input, 180) == MA_SUCCESS); |
||||
int64_t after = time_us(); |
||||
assert(seen == 3 && counts[0] == 64 && counts[1] == 64 && counts[2] == 52); |
||||
assert(times[0] >= now - 100000 && times[0] <= after - 100000); |
||||
for (unsigned i = 1; i < seen; ++i) { |
||||
int64_t difference = times[i] - times[0] - (int64_t)i * 64 * 1000000 / 48000; |
||||
assert(difference >= -1 && difference <= 1); |
||||
} |
||||
capture_missing = 1; seen = 0; audio_capture_hole(120); |
||||
now = time_us(); |
||||
assert(ma_device_handle_backend_data_callback(&device, NULL, input, 64) == MA_SUCCESS); |
||||
after = time_us(); |
||||
assert(seen == 1 && times[0] >= now - 100000 + 2500 && times[0] <= after - 100000 + 2500); |
||||
assert(!audio_stream_capture_missing(&device)); // Hint действует только внутри dispatch.
|
||||
ma_data_converter_uninit(&device.capture.converter, NULL); |
||||
puts("audio timing: capture read index, converter offsets, explicit hole PASS"); |
||||
} |
||||
@ -0,0 +1,106 @@
|
||||
/* Настоящие workers/AEC; граница сервисного PCM заменена наблюдателями без сети/устройства. */ |
||||
#include "../src/voice_audio_io.h" |
||||
extern "C" { |
||||
#include "call/call_audio.h" |
||||
#include "radio/radio_audio.h" |
||||
#include "debug_config.h" |
||||
} |
||||
#include <vector> |
||||
#include <array> |
||||
#include <chrono> |
||||
#include <thread> |
||||
#include <mutex> |
||||
#include <cmath> |
||||
#include <cstdio> |
||||
#include <cstdlib> |
||||
#include <algorithm> |
||||
#define REQUIRE(x) do { if (!(x)) { fprintf(stderr, "voice-io:%d %s\n", __LINE__, #x); abort(); } } while (0) |
||||
static std::mutex mutex; |
||||
static std::vector<int16_t> capture, transmitted; |
||||
static std::vector<uint8_t> masks; |
||||
static bool talking; |
||||
static std::atomic<int> pulls{0}, blocks{0}; |
||||
static std::atomic<bool> stall{false}, stalled{false}; |
||||
extern "C" int call_audio_pull_pcm(uint64_t, int16_t* out, int count) { ++pulls; std::fill_n(out, count, 1000); return count; } |
||||
extern "C" int radio_audio_pull_pcm(uint64_t, int16_t* out, int count) { |
||||
++pulls; for (int i = 0; i < count; ++i) out[i] = i % 2 ? 2000 : 1000; return count; |
||||
} |
||||
extern "C" void call_audio_reset_io(uint64_t, int) {} |
||||
extern "C" void radio_audio_reset_playback(uint64_t) {} |
||||
extern "C" void radio_audio_capture_discontinuity(uint64_t) {} |
||||
extern "C" void radio_audio_talk_begin(uint64_t) { std::lock_guard lock(mutex); talking = true; } |
||||
extern "C" void radio_audio_talk_end(uint64_t) { std::lock_guard lock(mutex); talking = false; } |
||||
extern "C" int radio_audio_feed_pcm(uint64_t, const int16_t* pcm, int count) { |
||||
if (stall) { stalled = true; while (stall) std::this_thread::sleep_for(std::chrono::milliseconds(1)); } |
||||
std::lock_guard lock(mutex); |
||||
capture.insert(capture.end(), pcm, pcm + count); |
||||
if (talking) transmitted.insert(transmitted.end(), pcm, pcm + count); |
||||
++blocks; return 0; |
||||
} |
||||
extern "C" int call_audio_feed_prepared_pcm(uint64_t, const int16_t* pcm, const uint8_t* mask) { |
||||
std::lock_guard lock(mutex); |
||||
capture.insert(capture.end(), pcm, pcm + 960); masks.insert(masks.end(), mask, mask + 960); |
||||
++blocks; return 0; |
||||
} |
||||
static int64_t now_us() { |
||||
return std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now().time_since_epoch()).count(); |
||||
} |
||||
template<class F> static void await(F ready) { |
||||
for (int i = 0; i < 2000 && !ready(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); |
||||
REQUIRE(ready()); |
||||
} |
||||
static void reset() { std::lock_guard lock(mutex); capture.clear(); transmitted.clear(); masks.clear(); talking = false; blocks = 0; } |
||||
int main() { |
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); |
||||
VoiceAudioIo voice; |
||||
std::array<int16_t, 960> pcm{}; pcm.fill(1234); |
||||
voice.start(VoiceAudioIo::Kind::Radio, 7); REQUIRE(voice.startCapture(1, false)); |
||||
voice.capture(pcm.data(), 240, 0); voice.setPtt(true); |
||||
voice.capture(pcm.data(), 360, 0); voice.setPtt(false); voice.capture(pcm.data(), 360, 0); |
||||
await([] { return blocks.load() >= 3; }); voice.stopCapture(); |
||||
REQUIRE(capture.size() == 960 && transmitted.size() == 360 && !talking); |
||||
reset(); REQUIRE(voice.startCapture(1, false)); |
||||
voice.setPtt(true); voice.capture(pcm.data(), 120, 0); voice.setPtt(false); voice.stopCapture(); |
||||
REQUIRE(capture.size() == 120 && transmitted.size() == 120 && !talking); |
||||
reset(); REQUIRE(voice.startCapture(1, false)); stall = true; |
||||
voice.capture(pcm.data(), 960, 0); await([] { return stalled.load(); }); |
||||
int before = pulls; |
||||
for (int i = 0; i < 10; ++i) { |
||||
float output[1920]{}; voice.mix(output, 960); |
||||
std::this_thread::sleep_for(std::chrono::milliseconds(3)); |
||||
} |
||||
REQUIRE(pulls > before + 3); stall = false; voice.stopCapture(); voice.stop(); |
||||
reset(); voice.start(VoiceAudioIo::Kind::Call, 42); REQUIRE(voice.startCapture(1, false)); |
||||
voice.capture(pcm.data(), 480, 0); voice.setMuted(true); voice.capture(pcm.data(), 480, 0); |
||||
await([] { return blocks.load() == 1; }); voice.stopCapture(); |
||||
REQUIRE(masks.size() == 960); |
||||
for (int i = 0; i < 960; ++i) REQUIRE(masks[i] == (i >= 480) && capture[i] == 1234); |
||||
voice.stop(); reset(); |
||||
voice.start(VoiceAudioIo::Kind::Call, 42); REQUIRE(voice.startCapture(1, false)); |
||||
voice.capture(pcm.data(), 960, now_us() - 300000); // Backend latency не является возрастом worker-очереди.
|
||||
await([] { return blocks.load() == 1; }); voice.stopCapture(); voice.stop(); |
||||
REQUIRE(capture.size() == 960); reset(); |
||||
// Референс противофазный: mono downmix дал бы ноль и не смог подавить эхо.
|
||||
voice.start(VoiceAudioIo::Kind::Call, 43); REQUIRE(voice.startCapture(1, true)); |
||||
uint32_t rng = 1234567; |
||||
double echoPower = 0, outputPower = 0; |
||||
int64_t time = now_us(); |
||||
for (int frame = 0; frame < 240; ++frame) { |
||||
float reference[1920]; |
||||
for (int i = 0; i < 960; ++i) { |
||||
rng = rng * 1664525u + 1013904223u; |
||||
int16_t sample = (int16_t)((rng >> 18) - 8192); |
||||
reference[2*i] = sample / 32768.0f; reference[2*i+1] = -reference[2*i]; |
||||
pcm[i] = sample / 2; |
||||
if (frame >= 180) echoPower += (double)pcm[i] * pcm[i]; |
||||
} |
||||
voice.reference(reference, 960, time + frame * 20000); |
||||
voice.capture(pcm.data(), 960, time + frame * 20000); |
||||
await([&] { return blocks.load() == frame + 1; }); |
||||
} |
||||
voice.stopCapture(); voice.stop(); |
||||
for (size_t i = 180 * 960; i < capture.size(); ++i) outputPower += (double)capture[i] * capture[i]; |
||||
printf("stereo reference residual ratio=%.6f\n", outputPower / echoPower); |
||||
REQUIRE(outputPower < echoPower * 0.1); |
||||
puts("voice workers: exact PTT/mute boundaries, short tail, independent RX, stereo AEC PASS"); |
||||
} |
||||
@ -0,0 +1,143 @@
|
||||
/* Реальные codec/AGC/автоматы/очередь uasync; наблюдаем сетевую границу вместо отправки пакетов. */ |
||||
extern "C" { |
||||
#include "utun_instance.h" |
||||
#include "radio/radio_audio.h" |
||||
#include "call/call_audio.h" |
||||
#include "opus_codec.h" |
||||
#include "debug_config.h" |
||||
#include "u_async.h" |
||||
#include "silero_vad.h" |
||||
} |
||||
#include <vector> |
||||
#include <array> |
||||
#include <cstring> |
||||
#include <cmath> |
||||
#include <cstdio> |
||||
#include <cstdlib> |
||||
#include <algorithm> |
||||
#include <atomic> |
||||
#include <thread> |
||||
#include <chrono> |
||||
#define REQUIRE(x) do { if (!(x)) { fprintf(stderr, "voice-tx:%d %s\n", __LINE__, #x); abort(); } } while (0) |
||||
static std::vector<char> events; |
||||
static std::vector<std::vector<uint8_t>> packets; |
||||
static float probability; |
||||
static int vad_windows; |
||||
static std::atomic<bool> stall_vad{false}, vad_stalled{false}; |
||||
extern "C" silero_vad_t* __wrap_silero_vad_create_default() { return reinterpret_cast<silero_vad_t*>(1); } |
||||
extern "C" void __wrap_silero_vad_destroy(silero_vad_t*) {} |
||||
extern "C" void __wrap_silero_vad_reset(silero_vad_t*) {} |
||||
extern "C" int __wrap_silero_vad_process(silero_vad_t*, const float*, float* result) { |
||||
if (stall_vad) { vad_stalled = true; while (stall_vad) std::this_thread::sleep_for(std::chrono::milliseconds(1)); } |
||||
++vad_windows; *result = probability; return 0; |
||||
} |
||||
extern "C" int __wrap_radio_talk_begin(UTUN_INSTANCE*, uint64_t) { events.push_back('B'); return 0; } |
||||
extern "C" int __wrap_radio_talk_end(UTUN_INSTANCE*, uint64_t) { events.push_back('F'); return 0; } |
||||
extern "C" int __wrap_radio_talk_send(UTUN_INSTANCE*, uint64_t, const uint8_t* data, int len) { |
||||
events.push_back('M'); packets.emplace_back(data, data + len); return 0; |
||||
} |
||||
extern "C" int __wrap_call_send_media(UTUN_INSTANCE*, uint64_t, const uint8_t* data, int len) { |
||||
packets.emplace_back(data, data + len); return 0; |
||||
} |
||||
extern "C" int __real_chat_setting_get_int(UTUN_INSTANCE*, const char*, int); |
||||
extern "C" int __wrap_chat_setting_get_int(UTUN_INSTANCE* inst, const char* key, int fallback) { |
||||
if (!strcmp(key, "radio_compressor_enabled") || !strcmp(key, "call_compressor_enabled")) return 0; |
||||
if (!strcmp(key, "radio_vad_hangover_ms")) return 0; |
||||
return __real_chat_setting_get_int(inst, key, fallback); |
||||
} |
||||
static void check_packet(opus_codec_encoder_t* encoder, const int16_t* pcm, size_t index) { |
||||
uint8_t data[1024]; int size = opus_codec_encode(encoder, pcm, 960, data, sizeof(data)); |
||||
REQUIRE(size > 0 && index < packets.size()); |
||||
REQUIRE(packets[index].size() == (size_t)size && !memcmp(packets[index].data(), data, size)); |
||||
} |
||||
int main() { |
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); |
||||
UTUN_INSTANCE instance{}; instance.ua = uasync_create(); REQUIRE(instance.ua); |
||||
REQUIRE(radio_audio_init(&instance) == 0 && call_audio_init(&instance) == 0); |
||||
REQUIRE(radio_audio_start(7, 0) == 0 && radio_audio_capture_start(7, 1, 0) == 0); |
||||
std::array<int16_t, 1920> input{}; |
||||
for (int i = 0; i < 1920; ++i) input[i] = (int16_t)(12000 * sin(i * 0.1)); |
||||
radio_audio_talk_begin(7); radio_audio_talk_begin(7); |
||||
REQUIRE(radio_audio_feed_pcm(7, input.data(), 1200) == 0); radio_audio_talk_end(7); |
||||
radio_audio_talk_begin(7); REQUIRE(radio_audio_feed_pcm(7, input.data() + 1200, 120) == 0); radio_audio_talk_end(7); |
||||
uasync_poll(instance.ua, 0); |
||||
REQUIRE(events == std::vector<char>({'B','M','M','F','B','M','F'})); |
||||
auto* encoder = opus_codec_encoder_create(48000, 1); REQUIRE(encoder); |
||||
check_packet(encoder, input.data(), 0); |
||||
std::array<int16_t, 960> tail{}; memcpy(tail.data(), input.data() + 960, 240 * sizeof(int16_t)); |
||||
check_packet(encoder, tail.data(), 1); |
||||
tail.fill(0); memcpy(tail.data(), input.data() + 1200, 120 * sizeof(int16_t)); check_packet(encoder, tail.data(), 2); |
||||
// Разрыв не склеивает отсчёты до и после потери внутри одного Opus-кадра.
|
||||
packets.clear(); events.clear(); |
||||
radio_audio_talk_begin(7); radio_audio_feed_pcm(7, input.data(), 100); |
||||
radio_audio_capture_discontinuity(7); radio_audio_feed_pcm(7, input.data() + 100, 100); radio_audio_talk_end(7); |
||||
uasync_poll(instance.ua, 0); |
||||
REQUIRE(events == std::vector<char>({'B','M','M','F'})); |
||||
tail.fill(0); memcpy(tail.data(), input.data(), 100 * sizeof(int16_t)); check_packet(encoder, tail.data(), 0); |
||||
tail.fill(0); memcpy(tail.data(), input.data() + 100, 100 * sizeof(int16_t)); check_packet(encoder, tail.data(), 1); |
||||
opus_codec_encoder_destroy(encoder); |
||||
// Capture teardown завершает принятую серию до FIN; следующее capture не получает старый хвост.
|
||||
packets.clear(); events.clear(); |
||||
radio_audio_talk_begin(7); radio_audio_feed_pcm(7, input.data(), 120); radio_audio_capture_stop(); |
||||
REQUIRE(radio_audio_capture_start(7, 1, 0) == 0); |
||||
radio_audio_talk_begin(7); radio_audio_feed_pcm(7, input.data() + 120, 120); radio_audio_talk_end(7); |
||||
uasync_poll(instance.ua, 0); |
||||
REQUIRE(events == std::vector<char>({'B','M','F','B','M','F'})); |
||||
// Старое прослушивание того же group ID отменено: старые BEGIN/media не попадают в новую сессию.
|
||||
packets.clear(); events.clear(); |
||||
radio_audio_talk_begin(7); radio_audio_feed_pcm(7, input.data(), 120); radio_audio_stop(); |
||||
REQUIRE(radio_audio_start(7, 1) == 0); |
||||
uasync_poll(instance.ua, 0); |
||||
REQUIRE(events.empty() && packets.empty()); |
||||
// У остановленного core ограничены не только media, но и быстрые PTT-команды.
|
||||
for (int i = 0; i < 1000; ++i) { |
||||
radio_audio_talk_begin(7); radio_audio_feed_pcm(7, input.data(), 120); radio_audio_talk_end(7); |
||||
} |
||||
int pending = 0; |
||||
for (auto* task = instance.ua->posted_tasks_head; task; task = task->next) ++pending; |
||||
REQUIRE(pending <= 24); // 16 control + 8 media, FIN каждой принятой серии уже зарезервирован.
|
||||
uasync_poll(instance.ua, 0); |
||||
packets.clear(); events.clear(); radio_audio_capture_stop(); |
||||
REQUIRE(radio_audio_capture_start(7, 1, 1) == 0 && radio_audio_vad_mode()); |
||||
auto feed = [&](int frames) { |
||||
while (frames) { int count = std::min(frames, 1920); REQUIRE(radio_audio_feed_pcm(7, input.data(), count) == 0); frames -= count; } |
||||
}; |
||||
probability = 0.9f; radio_audio_talk_begin(7); feed(3072); // Два окна VAD даже во время manual PTT.
|
||||
REQUIRE(vad_windows == 2); radio_audio_talk_end(7); REQUIRE(radio_audio_transmitting()); |
||||
probability = 0; feed(3072); REQUIRE(!radio_audio_transmitting()); // Первое окно начинает silence, второе завершает hangover.
|
||||
uasync_poll(instance.ua, 0); |
||||
REQUIRE(std::count(events.begin(), events.end(), 'B') == 1 && std::count(events.begin(), events.end(), 'F') == 1); |
||||
packets.clear(); events.clear(); |
||||
probability = 0.9f; feed(3072); REQUIRE(radio_audio_transmitting()); |
||||
radio_audio_talk_begin(7); probability = 0; feed(3072); REQUIRE(radio_audio_transmitting()); |
||||
radio_audio_talk_end(7); uasync_poll(instance.ua, 0); |
||||
REQUIRE(std::count(events.begin(), events.end(), 'B') == 1 && std::count(events.begin(), events.end(), 'F') == 1); |
||||
stall_vad = true; |
||||
std::thread tx([&] { feed(1536); }); |
||||
for (int i = 0; i < 200 && !vad_stalled; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); |
||||
REQUIRE(vad_stalled); |
||||
std::atomic<bool> rx_finished{false}; |
||||
std::thread rx([&] { int16_t out[1920]; REQUIRE(radio_audio_pull_pcm(7, out, 1920) == 1920); rx_finished = true; }); |
||||
for (int i = 0; i < 200 && !rx_finished; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); |
||||
REQUIRE(rx_finished); stall_vad = false; tx.join(); rx.join(); |
||||
radio_audio_stop(); uasync_poll(instance.ua, 0); |
||||
// Старый FIN не обращается к новому ядру, даже когда allocator повторно использовал тот же адрес.
|
||||
REQUIRE(radio_audio_start(7, 1) == 0); |
||||
radio_audio_talk_begin(7); uasync_poll(instance.ua, 0); |
||||
radio_audio_talk_end(7); radio_audio_stop(); radio_audio_destroy(&instance); |
||||
REQUIRE(radio_audio_init(&instance) == 0); |
||||
events.clear(); uasync_poll(instance.ua, 0); REQUIRE(events.empty()); |
||||
// Prepared policy исключает повторный core AEC; mute применяется только к нужной части кадра.
|
||||
packets.clear(); REQUIRE(call_audio_start_prepared(42) == 0); |
||||
REQUIRE(call_audio_start(42) == -1); |
||||
REQUIRE(call_audio_feed_pcm(42, input.data(), 960) == -1); |
||||
std::array<uint8_t, 960> mask{}; |
||||
std::fill(mask.begin() + 300, mask.end(), 1); |
||||
REQUIRE(call_audio_feed_prepared_pcm(42, input.data(), mask.data()) == 0); |
||||
uasync_poll(instance.ua, 0); |
||||
encoder = opus_codec_encoder_create(48000, 1); REQUIRE(encoder); |
||||
tail.fill(0); memcpy(tail.data(), input.data(), 300 * sizeof(int16_t)); check_packet(encoder, tail.data(), 0); |
||||
call_audio_stop(); opus_codec_encoder_destroy(encoder); |
||||
call_audio_destroy(&instance); radio_audio_destroy(&instance); uasync_destroy(instance.ua, 0); |
||||
puts("voice TX: exact Opus tail, rapid bursts, discontinuity, capture policy, final mute PASS"); |
||||
} |
||||
Loading…
Reference in new issue