Сохрани накопленные исправления аудио, async и диагностики
This commit is contained in:
1 parent
b1e3193899
commit
bdcf8b5634
59 files changed
+4611
-1441
No files matched your search
@@ -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");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user