Сохрани накопленные исправления аудио, async и диагностики

This commit is contained in:
evgeny committed 2026-10-02 01:49:32 +03:00
1 parent b1e3193899
commit bdcf8b5634
59 files changed
+4611 -1441

No files matched your search

+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""Изолированный PipeWire/PulseAudio с виртуальным sink; системный сервер не затрагивается."""
import os
from pathlib import Path
import shutil
import signal
import subprocess
import sys
import tempfile
import time
def wait_for(predicate, seconds=10):
deadline = time.monotonic() + seconds
while time.monotonic() < deadline:
if predicate():
return
time.sleep(0.02)
raise RuntimeError("private audio server/test did not reach expected state")
def main():
for executable in ("pipewire", "pipewire-pulse", "wireplumber", "dbus-run-session", "pactl"):
if not shutil.which(executable):
raise RuntimeError(f"required executable missing: {executable}")
binary = Path(sys.argv[1] if len(sys.argv) > 1 else "tools/chatgui/build/test_audio_recovery").resolve()
with tempfile.TemporaryDirectory(prefix="utun-pulse-") as root:
directory = Path(root)
config = directory / "config/wireplumber/wireplumber.conf.d"
config.mkdir(parents=True)
(config / "test.conf").write_text('''wireplumber.profiles = {
main = {
monitor.alsa = disabled
monitor.alsa-midi = disabled
monitor.bluez = disabled
monitor.bluez-midi = disabled
monitor.v4l2 = disabled
monitor.libcamera = disabled
}
}''')
env = os.environ.copy()
env.update(XDG_RUNTIME_DIR=root, PIPEWIRE_RUNTIME_DIR=root, XDG_CONFIG_HOME=str(directory / "config"),
XDG_STATE_HOME=str(directory / "state"), PIPEWIRE_REMOTE="pipewire-0", PULSE_SERVER=f"unix:{root}/pulse/native")
processes, logs = [], []
def launch(command, label):
log = (directory / f"{label}.log").open("w+")
logs.append(log)
process = subprocess.Popen(command, env=env, stdout=log, stderr=subprocess.STDOUT, start_new_session=True)
processes.append(process)
return process
def pulse_ready():
return subprocess.run(["pactl", "info"], env=env, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=2).returncode == 0
try:
launch(["pipewire"], "pipewire")
wait_for(lambda: (directory / "pipewire-0").exists())
launch(["dbus-run-session", "--", "wireplumber"], "wireplumber")
pulse = launch(["pipewire-pulse"], "pulse-before")
wait_for(pulse_ready)
# module-always-sink создаёт виртуальный dummy sink и его monitor source.
test = launch([str(binary), "--pulse-integration", root], "client")
wait_for(lambda: (directory / "ready").exists() or test.poll() is not None)
if test.poll() is not None:
raise RuntimeError("client failed before server restart")
pulse.terminate(); pulse.wait(timeout=5)
wait_for(lambda: (directory / "closed").exists() or test.poll() is not None)
launch(["pipewire-pulse"], "pulse-after")
wait_for(pulse_ready)
if test.wait(timeout=15) != 0:
raise RuntimeError("client recovery failed")
logs[3].flush(); logs[3].seek(0)
print(logs[3].read())
print("private PulseAudio server restart: PASS")
except Exception:
for log in logs:
log.flush(); log.seek(0)
print(f"--- {Path(log.name).name} ---\n{log.read()}", file=sys.stderr)
raise
finally:
for process in reversed(processes):
if process.poll() is None:
os.killpg(process.pid, signal.SIGTERM)
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
os.killpg(process.pid, signal.SIGKILL); process.wait()
for log in logs:
log.close()
if __name__ == "__main__":
main()
@@ -0,0 +1,42 @@
/* Проверяем настоящую очередь: конкуренция, переполнение, целостность и повторный запуск. */
#include "../transport/audio_diagnostics.cpp"
#include <cassert>
#include <cstdio>
#include <vector>
int main() {
debug_config_init(); debug_enable_console(0);
constexpr int producers = 4, perProducer = 20000;
std::vector<bool> seen(producers * perProducer);
std::atomic<int> done{0};
for (uint64_t i = 0; i < kCapacity; ++i) queue[i].sequence.store(i);
running = true;
// Сначала полное кольцо: producer обязан вернуть управление, потеря учтена.
for (uint64_t i = 0; i < kCapacity + 1; ++i) audio_diag_event(nullptr, AUDIO_DIAG_HOLE, AUDIO_DIAG_READ, i, 7);
assert(lost.load() == 1);
Event e;
for (uint64_t i = 0; i < kCapacity; ++i) {
assert(take(e)); assert(e.value == int64_t(i) && e.detail == 7);
}
assert(!take(e)); lost = 0;
std::vector<std::thread> workers;
for (int p = 0; p < producers; ++p) workers.emplace_back([p, &done]() {
for (int i = 0; i < perProducer; ++i)
audio_diag_event(nullptr, AUDIO_DIAG_HOLE, AUDIO_DIAG_READ, p * perProducer + i, p);
++done;
});
uint64_t received = 0;
while (done.load() != producers || readPosition != writePosition.load()) {
if (!take(e)) { std::this_thread::yield(); continue; }
assert(e.value >= 0 && e.value < producers * perProducer);
assert(e.kind == AUDIO_DIAG_HOLE && e.stage == AUDIO_DIAG_READ && e.value / perProducer == e.detail);
assert(e.wall && e.cpu && e.tid && !seen[e.value]);
seen[e.value] = true; ++received;
}
for (auto& worker : workers) worker.join();
assert(received + lost.load() == producers * perProducer);
running = false;
// Lifecycle не оставляет joinable-поток и очищает очередь между сеансами.
for (int i = 0; i < 2; ++i) { audio_diag_start(); audio_diag_event(nullptr, AUDIO_DIAG_OPEN, -1, 1, 48000); audio_diag_stop(); }
puts("test_audio_diagnostics: bounded MPSC, concurrent integrity and restart PASS");
}
+222
View File
@@ -0,0 +1,222 @@
/* Настоящие null devices и Qt timers; linker wrappers инъецируют только сбои lifecycle. */
#include "../src/audio_device.h"
#include "../src/call_audio_engine.h"
#include "../src/radio_audio_engine.h"
#include "../src/audiorecorder.h"
#include "../src/sound_manager.h"
#include <QCoreApplication>
#include <QThread>
#include <QFile>
#include <cstring>
#include <algorithm>
#include <map>
#include <vector>
extern "C" {
#include "utun_instance.h"
#include "call/call_audio.h"
#include "radio/radio_audio.h"
#include "media_async/attachment_send.h"
#include "u_async.h"
#include "debug_config.h"
}
#define REQUIRE(condition) do { if (!(condition)) qFatal("audio recovery line %d: %s", __LINE__, #condition); } while (0)
static UTUN_INSTANCE instance{};
static std::map<ma_context*, int> live;
static std::vector<ma_device*> devices;
static int starts, captureStarts, playbackStarts, contextStarts, failContexts;
static bool failStart, failContext, failDevice;
static bool pulseIntegration;
extern "C" UTUN_INSTANCE* gui_bridge_get_inst() { return &instance; }
extern "C" ma_result __real_ma_context_init(const ma_backend*, ma_uint32, const ma_context_config*, ma_context*);
extern "C" void __real_ma_context_uninit(ma_context*);
extern "C" ma_result __real_ma_device_init(ma_context*, const ma_device_config*, ma_device*);
extern "C" ma_result __real_ma_device_start(ma_device*);
extern "C" void __real_ma_device_uninit(ma_device*);
extern "C" int __real_audio_context_failed(ma_context*);
extern "C" ma_result __wrap_ma_context_init(const ma_backend*, ma_uint32, const ma_context_config*, ma_context* context) {
++contextStarts;
if (failContexts > 0) { --failContexts; return MA_ERROR; }
ma_backend backend = pulseIntegration ? ma_backend_pulseaudio : ma_backend_null;
return __real_ma_context_init(&backend, 1, nullptr, context);
}
extern "C" void __wrap_ma_context_uninit(ma_context* context) {
REQUIRE(live[context] == 0);
__real_ma_context_uninit(context);
}
extern "C" ma_result __wrap_ma_device_init(ma_context* context, const ma_device_config* config, ma_device* device) {
if (failDevice) { failDevice = false; return MA_ERROR; }
ma_result result = __real_ma_device_init(context, config, device);
if (result == MA_SUCCESS) { ++live[context]; devices.push_back(device); }
return result;
}
extern "C" ma_result __wrap_ma_device_start(ma_device* device) {
++starts;
if (device->type == ma_device_type_capture) {
++captureStarts;
// Некоторые backends вызывают data callback ещё до возврата start.
std::vector<int16_t> input(1200 * device->capture.channels, 1000);
if (!pulseIntegration) device->onData(device, nullptr, input.data(), 1200);
} else ++playbackStarts;
if (failStart) { failStart = false; return MA_ERROR; }
return __real_ma_device_start(device);
}
extern "C" void __wrap_ma_device_uninit(ma_device* device) {
auto context = device->pContext;
__real_ma_device_uninit(device);
REQUIRE(live[context] > 0);
--live[context];
devices.erase(std::find(devices.begin(), devices.end(), device));
}
extern "C" int __wrap_audio_context_failed(ma_context* context) {
if (failContext) { failContext = false; return 1; }
return __real_audio_context_failed(context);
}
static void pump(int milliseconds) {
QElapsedTimer timer; timer.start();
while (timer.elapsed() < milliseconds) { QCoreApplication::processEvents(); QThread::msleep(1); }
}
static void pulse_recovery(SoundManager* manager, const QString& control) {
REQUIRE(manager->context()->backend == ma_backend_pulseaudio);
AudioRecorder recorder; recorder.init(); recorder.startRecording();
std::vector<int16_t> pcm(48000 * 20, 1000);
manager->playRawPcm(pcm.data(), (int)pcm.size(), 48000, 1);
for (int i = 0; i < 100 && recorder.durationMs() < 300; ++i) pump(50);
REQUIRE(recorder.durationMs() >= 300 && manager->currentPcmCursor() > 0);
int duration = recorder.durationMs();
auto cursor = manager->currentPcmCursor();
QFile ready(control + "/ready"); REQUIRE(ready.open(QIODevice::WriteOnly)); ready.close();
for (int i = 0; i < 100 && manager->context(); ++i) pump(50);
REQUIRE(!manager->context());
QFile closed(control + "/closed"); REQUIRE(closed.open(QIODevice::WriteOnly)); closed.close();
for (int i = 0; i < 150 && (!manager->context() || recorder.durationMs() <= duration + 200); ++i) pump(50);
REQUIRE(manager->context() && recorder.durationMs() > duration + 200 && manager->currentPcmCursor() > cursor);
qInfo("PulseAudio progress: capture=%d -> %dms playback=%llu -> %llu frames", duration, recorder.durationMs(),
(unsigned long long)cursor, manager->currentPcmCursor());
recorder.stopRecording(); manager->shutdown();
REQUIRE(devices.empty());
qInfo("real PulseAudio: capture/playback progress restored after server restart PASS");
}
int main(int argc, char** argv) {
QCoreApplication app(argc, argv);
pulseIntegration = argc == 3 && std::strcmp(argv[1], "--pulse-integration") == 0;
debug_config_init(); debug_set_level(pulseIntegration ? DEBUG_LEVEL_INFO : DEBUG_LEVEL_WARN);
auto* manager = SoundManager::instance();
REQUIRE(manager->init() && manager->context());
if (pulseIntegration) { pulse_recovery(manager, QString::fromLocal8Bit(argv[2])); return 0; }
ma_device_id id{};
auto outputs = manager->enumeratePlaybackDevices();
REQUIRE(!outputs.empty() && SoundManager::deviceId(outputs.front().id, &id));
REQUIRE(!SoundManager::deviceId(outputs.front().id + '!', &id));
{
AudioDevice device;
auto config = ma_device_config_init(ma_device_type_capture);
config.capture.format = ma_format_s16; config.capture.channels = 1; config.sampleRate = 48000;
REQUIRE(device.init(manager->context(), config, "test-partial-start", 0, [](const void*, void*, unsigned int) {}));
int count = live[manager->context()];
failStart = true;
REQUIRE(!device.start() && !device.initialized());
REQUIRE(live[manager->context()] == count - 1);
failDevice = true;
REQUIRE(!device.init(manager->context(), config, "test-init-failure", 0, [](const void*, void*, unsigned int) {}));
REQUIRE(!device.initialized() && live[manager->context()] == count - 1);
}
{
AudioRecorder first, second;
first.init(); second.init();
auto inputs = manager->enumerateCaptureDevices();
REQUIRE(!inputs.empty());
first.setCaptureDevice(inputs.front().id);
int previous = captureStarts;
failStart = true;
first.startRecording(); second.startRecording();
REQUIRE(captureStarts == previous + 3); // Выбранный input не стартовал: default запущен после полного uninit.
pump(60);
first.stopRecording(); second.stopRecording();
attachment_send_req a{}, b{};
REQUIRE(first.takeRecording(&a) && second.takeRecording(&b));
REQUIRE(a.pcm_count >= 1200 && b.pcm_count >= 1200);
REQUIRE(a.pcm[0] == 1000 && b.pcm[0] == 1000); // pUserData маршрутизирует каждый callback своему recorder.
a.pcm_release(a.pcm_owner); b.pcm_release(b.pcm_owner);
}
instance.ua = uasync_create(); REQUIRE(instance.ua);
REQUIRE(call_audio_init(&instance) == 0 && radio_audio_init(&instance) == 0);
auto* call = CallAudioEngine::instance();
REQUIRE(call->start(42) == 0);
pump(60);
ma_device* playback = nullptr;
for (auto* device : devices)
if (device->type == ma_device_type_playback && device->playback.format == ma_format_s16 && device->playback.channels == 1)
playback = device;
REQUIRE(playback);
int previousCapture = captureStarts, previousPlayback = playbackStarts;
REQUIRE(ma_device_stop(playback) == MA_SUCCESS);
pump(800);
REQUIRE(captureStarts == previousCapture && playbackStarts > previousPlayback);
REQUIRE(call->callId() == 42 && call->isActive());
auto* radio = RadioAudioEngine::instance();
REQUIRE(radio->start(7) == 0);
AudioRecorder recorder; recorder.init(); recorder.startRecording();
std::vector<int16_t> notification(48000 * 10, 1000);
manager->playRawPcm(notification.data(), (int)notification.size(), 48000, 1);
pump(60);
auto cursor = manager->currentPcmCursor();
REQUIRE(cursor > 0);
int previousContext = contextStarts;
failContext = true; failContexts = 2;
pump(1800);
REQUIRE(contextStarts >= previousContext + 3 && manager->context());
REQUIRE(call->callId() == 42 && radio->groupId() == 7 && recorder.isRecording());
REQUIRE(manager->currentPcmCursor() > cursor && manager->isCurrentPcmPlaying());
call->stop(); radio->stop(); recorder.stopRecording();
int previousStarts = starts;
pump(600);
REQUIRE(starts == previousStarts); // Отложенное recovery не воскрешает остановленную сессию.
REQUIRE(call->start(43) == 0);
call->beginEnd(43);
REQUIRE(call->start(43) == 0);
pump(600);
REQUIRE(call->isActive() && call->callId() == 43); // Старый end timer не закрывает новое поколение того же call ID.
call->stop();
failContext = true; failContexts = 1;
recorder.startRecording();
pump(220);
REQUIRE(!manager->context());
recorder.stopRecording();
previousCapture = captureStarts;
pump(1000);
REQUIRE(manager->context() && captureStarts == previousCapture);
// Предел TX действует до помещения задачи в uasync; старое поколение не отправляется после stop/start.
uasync_poll(instance.ua, 0);
REQUIRE(call_audio_start(99) == 0);
int16_t silence[960]{};
for (int i = 0; i < 100; ++i) call_audio_feed_pcm(99, silence, 960);
int pending = 0;
for (auto* task = instance.ua->posted_tasks_head; task; task = task->next) ++pending;
REQUIRE(pending == 8);
call_audio_stop();
REQUIRE(call_audio_start(100) == 0);
for (int i = 0; i < 100; ++i) call_audio_feed_pcm(100, silence, 960);
pending = 0;
for (auto* task = instance.ua->posted_tasks_head; task; task = task->next) ++pending;
REQUIRE(pending == 8); // Новый звонок также учитывает ещё не освобождённые задачи старого.
call_audio_stop();
uasync_poll(instance.ua, 0);
REQUIRE(!instance.ua->posted_tasks_head);
REQUIRE(radio_audio_start(8, 1) == 0);
radio_audio_talk_begin(8); radio_audio_talk_end(8); radio_audio_stop();
REQUIRE(instance.ua->posted_tasks_head);
// Завершение ядра освобождает ещё не выполненные PTT-задачи вместе с аргументами.
call_audio_destroy(&instance); radio_audio_destroy(&instance);
uasync_destroy(instance.ua, 0); instance.ua = nullptr;
manager->shutdown();
REQUIRE(devices.empty());
qInfo("audio recovery: partial start, independent streams, two recorders, context restart, cancellation, bounded TX PASS");
return 0;
}
+17
View File
@@ -20,6 +20,11 @@ static int decode(void* arg, const uint8_t* enc, int len, int16_t* pcm, int cap)
for (int i = 0; i < cap; i++) pcm[i] = (int16_t)(8000 * std::sin(2 * 3.141592653589793 * 440 * i / 48000));
return cap;
}
struct broken_decoder { int attempts = 0; };
static int decode_bad(void* arg, const uint8_t*, int, int16_t*, int) {
++static_cast<broken_decoder*>(arg)->attempts;
return -1;
}
static void push(call_jitter* j, int id, int64_t received_ms = -1) {
call_jitter_push(j, (const uint8_t*)&id, sizeof(id), (uint32_t)(id * 20), received_ms < 0 ? id * 20 : received_ms);
}
@@ -247,6 +252,18 @@ int main() {
CHECK(audible);
call_audio_release(125);
}
{
broken_decoder decoder;
auto* jitter = call_jitter_create(decode_bad, &decoder, 1000);
for (int i = 0; i < CALL_JITTER_MAX_FRAMES; ++i) push(jitter, i);
CHECK(call_jitter_pull(jitter, pcm, 480) == 0);
CHECK(decoder.attempts > 0 && decoder.attempts <= 9);
call_jitter_reset(jitter);
int depth = 0;
call_jitter_get_stats(jitter, &depth, nullptr, nullptr, nullptr, nullptr);
CHECK(depth <= 1000);
call_jitter_destroy(jitter);
}
std::printf("call_jitter: %s\n", failures ? "FAILED" : "PASSED");
return failures ? 1 : 0;
}
+163
View File
@@ -0,0 +1,163 @@
/* Реальные ветки miniaudio на null backend: аппаратный звук и сеть не нужны. */
#include "../transport/audio_diagnostics.h"
#include "../../../lib/speex_aec.h"
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
static uint64_t observed_unfilled, observed_no_progress, observed_holes;
static uint32_t callback_count;
static void test_event(const void* device, int kind, int stage, int64_t value, int64_t detail) {
if (kind == AUDIO_DIAG_UNFILLED) observed_unfilled += (uint64_t)value;
if (kind == AUDIO_DIAG_NO_PROGRESS) observed_no_progress += (uint64_t)value;
if (kind == AUDIO_DIAG_HOLE) observed_holes += (uint64_t)value;
audio_diag_event(device, kind, stage, value, detail);
}
#define MA_DIAGNOSTIC_EVENT(device, kind, stage, value, detail) test_event(device, kind, stage, value, detail)
#define MA_DIAGNOSTIC_LOG(log, level, message) audio_diag_log(log, level, message)
#define MA_PULSE_WAIT_TIMEOUT_MS 20
#define MINIAUDIO_IMPLEMENTATION
#include "../../../lib/miniaudio.h"
static void callback(ma_device* device, void* out, const void* in, ma_uint32 count) {
(void)device; (void)in;
callback_count++;
if (out) for (ma_uint32 i = 0; i < count; i++) ((int16_t*)out)[i] = 12000;
}
static void starvation(int duplex) {
ma_backend backend = ma_backend_null;
ma_context context;
ma_device device;
ma_device_config config = ma_device_config_init(duplex ? ma_device_type_duplex : ma_device_type_playback);
config.capture.format = config.playback.format = ma_format_s16;
config.capture.channels = config.playback.channels = 1;
config.sampleRate = 48000; config.periodSizeInFrames = 480; config.dataCallback = callback;
assert(ma_context_init(&backend, 1, NULL, &context) == MA_SUCCESS);
assert(ma_device_init(&context, &config, &device) == MA_SUCCESS);
if (duplex) assert(ma_duplex_rb_init(ma_format_s16, 1, 48000, 48000, 480, NULL, &device.duplexRB) == MA_SUCCESS);
/* Worker не запускаем: стенд синхронно вызывает backend-обработчик. */
ma_device__set_state(&device, ma_device_state_started);
audio_diag_label(&device, duplex ? 1 : 2, duplex ? "test-duplex" : "test-playback");
int16_t input[480] = {0}, output[480];
observed_unfilled = 0; callback_count = 0;
uint64_t untouched = 0;
for (int tick = 0; tick < 20; tick++) {
if (duplex && tick < 3) assert(ma_device_handle_backend_data_callback(&device, NULL, input, 480) == MA_SUCCESS);
for (int i = 0; i < 480; i++) output[i] = 2345;
assert(ma_device_handle_backend_data_callback(&device, output, NULL, 480) == MA_SUCCESS);
for (int i = 0; i < 480; i++) untouched += output[i] == 2345;
}
assert(untouched == observed_unfilled);
assert(untouched == 0 && callback_count >= 20);
printf("starvation duplex=%d unfilled=%llu callback_count=%u PASS\n", duplex,
(unsigned long long)untouched, callback_count);
ma_device__set_state(&device, ma_device_state_stopped);
if (duplex) ma_duplex_rb_uninit(&device.duplexRB);
ma_device_uninit(&device); ma_context_uninit(&context);
}
#ifdef MA_SUPPORT_PULSEAUDIO
static size_t zero_writable(const ma_pa_stream* stream) {
(void)stream;
/* Без внешнего stop callback обязан вернуть управление сам. */
return 0;
}
static void no_progress(void) {
ma_context context;
ma_device device;
memset(&context, 0, sizeof(context)); memset(&device, 0, sizeof(device));
device.pContext = &context;
device.playback.internalFormat = ma_format_s16; device.playback.internalChannels = 1;
context.pulse.pa_stream_writable_size = (ma_proc)zero_writable;
ma_device__set_state(&device, ma_device_state_started);
observed_no_progress = 0;
ma_device_on_write__pulse((ma_pa_stream*)&device, 960, &device);
assert(observed_no_progress == 1);
audio_diag_event(&device, AUDIO_DIAG_CLOSE, -1, 0, 0);
puts("PulseAudio zero-progress detection PASS");
}
static unsigned int wait_cancelled, wait_unrefed;
static ma_pa_operation_state_t wait_state(const ma_pa_operation* operation) { (void)operation; return MA_PA_OPERATION_RUNNING; }
static void wait_cancel(ma_pa_operation* operation) { (void)operation; ++wait_cancelled; }
static void wait_unref(ma_pa_operation* operation) { (void)operation; ++wait_unrefed; }
static int wait_iterate(ma_pa_mainloop* loop, int block, int* result) {
(void)loop; (void)result;
assert(block == 0);
return 0;
}
static void bounded_wait(void) {
ma_context context;
ma_timer timer;
memset(&context, 0, sizeof(context));
context.pulse.pa_operation_get_state = (ma_proc)wait_state;
context.pulse.pa_operation_cancel = (ma_proc)wait_cancel;
context.pulse.pa_operation_unref = (ma_proc)wait_unref;
context.pulse.pa_mainloop_iterate = (ma_proc)wait_iterate;
wait_cancelled = wait_unrefed = 0;
ma_timer_init(&timer);
assert(ma_wait_for_operation_and_unref__pulse(&context, NULL, (ma_pa_operation*)&context) == MA_TIMEOUT);
assert(wait_cancelled == 1 && wait_unrefed == 1);
assert(ma_timer_get_time_in_seconds(&timer) < 0.5);
puts("PulseAudio deadline and callback cancellation PASS");
}
static unsigned int hole_frames, hole_drops;
static int hole_peek(ma_pa_stream* stream, const void** data, size_t* bytes) {
(void)stream; *data = NULL; *bytes = 9600; return 0;
}
static int hole_drop(ma_pa_stream* stream) { (void)stream; ++hole_drops; return 0; }
static void hole_capture(ma_device* device, void* out, const void* in, ma_uint32 frames) {
(void)device; (void)out;
assert(in);
for (ma_uint32 i = 0; i < frames; ++i) assert(((const int16_t*)in)[i] == 0);
hole_frames += frames;
}
static void capture_hole(void) {
ma_backend backend = ma_backend_null;
ma_context context;
ma_device device;
ma_device_config config = ma_device_config_init(ma_device_type_capture);
config.capture.format = ma_format_s16; config.capture.channels = 1; config.sampleRate = 48000;
config.noFixedSizedCallback = MA_TRUE; config.dataCallback = hole_capture;
assert(ma_context_init(&backend, 1, NULL, &context) == MA_SUCCESS);
assert(ma_device_init(&context, &config, &device) == MA_SUCCESS);
context.pulse.pa_stream_peek = (ma_proc)hole_peek;
context.pulse.pa_stream_drop = (ma_proc)hole_drop;
observed_holes = hole_frames = hole_drops = 0;
ma_device__set_state(&device, ma_device_state_started);
ma_device_on_read__pulse((ma_pa_stream*)&device, 9600, &device);
assert(observed_holes == 4800 && hole_frames == 4800 && hole_drops == 1);
ma_device__set_state(&device, ma_device_state_stopped);
ma_device_uninit(&device); ma_context_uninit(&context);
puts("PulseAudio capture hole preserves duration and silence PASS");
}
#endif
/* Характеризация найденной потери хвоста; после исправления ожидаем два кадра. */
static void aec_variable_chunks(void) {
speex_aec_t* aec = speex_aec_create(48000, 960, 14400, 8);
int16_t pcm[1920] = {0};
assert(aec != NULL);
speex_aec_feed_playback(aec, pcm, 1200);
speex_aec_feed_playback(aec, pcm + 1200, 720);
const int frames = speex_aec_delay_fill(aec);
assert(frames == 2);
printf("AEC characterization: input=1920 queued=%d*960; all samples preserved\n", frames);
speex_aec_destroy(aec);
}
int main(void) {
audio_diag_start();
starvation(1); starvation(0);
#ifdef MA_SUPPORT_PULSEAUDIO
no_progress();
bounded_wait();
capture_hole();
#endif
aec_variable_chunks();
audio_diag_stop();
return 0;
}