[client] audio: test streams and microphone consent

This commit is contained in:
Geoffrey McRae
2026-08-12 03:46:08 +10:00
parent 49fa27b573
commit 8bf7c5b49e
2 changed files with 818 additions and 0 deletions

780
client/tests/audio_test.c Normal file
View File

@@ -0,0 +1,780 @@
/**
* Looking Glass
* Copyright © 2017-2026 The Looking Glass Authors
* https://looking-glass.io
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 59
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "interface/audiodev.h"
static struct LG_AudioDevOps dev;
struct LG_AudioDevOps * LG_AudioDevs[] = { &dev, &dev, NULL };
/* Keep the conversion and validation helpers in this unit-test translation
* unit. The public provider API is used for all lifecycle tests below. */
#include "../src/audio.c"
#include "test.h"
#include <math.h>
#include <pthread.h>
#include <stdarg.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define WAIT_MS 2000U
struct AppState g_state;
struct AppParams g_params;
struct CursorState g_cursor;
struct Backend
{
atomic_uint initN;
atomic_uint freeN;
atomic_uint setupN;
atomic_uint playStartN;
atomic_uint playStopN;
atomic_uint recStartN;
atomic_uint recStopN;
atomic_bool setupOK;
atomic_bool playOK;
atomic_bool recOK;
LG_AudioFormat playFormat;
LG_AudioPullFn pull;
LG_AudioFailureFn playFail;
uint32_t playCookie;
LG_AudioFormat recFormat;
LG_AudioPushFn push;
LG_AudioFailureFn recFail;
uint32_t recCookie;
};
struct Provider
{
const char * name;
bool available;
bool attachOK;
uint32_t generation;
LG_AudioStatusFn status;
void * statusOpaque;
const LG_AudioEventOps * events;
void * eventOpaque;
unsigned int listenN;
unsigned int unlistenN;
unsigned int attachN;
unsigned int detachN;
unsigned int dataN;
atomic_bool badDetach;
};
struct UI
{
MsgBoxConfirmCallback confirm;
void * opaque;
MsgBoxHandle handle;
unsigned int confirmN;
unsigned int closeN;
atomic_bool block;
atomic_bool entered;
atomic_bool release;
atomic_bool inConfirm;
};
static struct Backend b;
static struct UI ui;
static bool devInit(void)
{
atomic_fetch_add(&b.initN, 1);
return true;
}
static void devFree(void)
{
atomic_fetch_add(&b.freeN, 1);
}
static bool playSetup(const LG_AudioFormat * format,
int period, bool requestResampler, bool * resamplerEnabled,
int * maxPeriod, int * startFrames, LG_AudioPullFn pull)
{
(void)period;
b.playFormat = *format;
b.pull = pull;
*resamplerEnabled = requestResampler;
*maxPeriod = 256;
*startFrames = 0;
atomic_fetch_add(&b.setupN, 1);
return atomic_load(&b.setupOK);
}
static bool playStart(LG_AudioFailureFn fail, uint32_t cookie)
{
b.playFail = fail;
b.playCookie = cookie;
atomic_fetch_add(&b.playStartN, 1);
return atomic_load(&b.playOK);
}
static void playStop(void)
{
b.playFail = NULL;
b.playCookie = 0;
atomic_fetch_add(&b.playStopN, 1);
}
static bool playRate(double * ratio)
{
(void)ratio;
return true;
}
static uint64_t playLatency(void)
{
return 0;
}
static bool recStart(const LG_AudioFormat * format, LG_AudioPushFn push,
LG_AudioFailureFn fail, uint32_t cookie)
{
b.recFormat = *format;
b.push = push;
b.recFail = fail;
b.recCookie = cookie;
atomic_fetch_add(&b.recStartN, 1);
return atomic_load(&b.recOK);
}
static void recStop(void)
{
b.push = NULL;
b.recFail = NULL;
b.recCookie = 0;
atomic_fetch_add(&b.recStopN, 1);
}
static void setListener(void * opaque, LG_AudioStatusFn callback,
void * callbackOpaque)
{
struct Provider * p = opaque;
p->status = callback;
p->statusOpaque = callbackOpaque;
if (!callback)
{
++p->unlistenN;
return;
}
++p->listenN;
const LG_AudioStatus status =
{
.available = p->available,
.generation = p->generation,
};
callback(callbackOpaque, &status);
}
static bool attach(void * opaque, const LG_AudioEventOps * events,
void * eventOpaque)
{
struct Provider * p = opaque;
++p->attachN;
p->events = events;
p->eventOpaque = eventOpaque;
return p->attachOK;
}
static void detach(void * opaque)
{
struct Provider * p = opaque;
if (atomic_load(&ui.inConfirm))
atomic_store(&p->badDetach, true);
++p->detachN;
p->events = NULL;
p->eventOpaque = NULL;
}
static bool recordData(void * opaque, uint32_t generation,
const void * data, size_t frames, const LG_AudioClock * clock)
{
struct Provider * p = opaque;
(void)generation;
(void)data;
(void)frames;
(void)clock;
++p->dataN;
return true;
}
static const LG_AudioOps ops =
{
.name = "test",
.setStatusListener = setListener,
.attach = attach,
.detach = detach,
.recordData = recordData,
};
GraphHandle app_registerGraph(const char * name, RingBuffer buffer,
float min, float max, GraphFormatFn formatFn)
{
(void)name;
(void)buffer;
(void)min;
(void)max;
(void)formatFn;
return NULL;
}
void app_unregisterGraph(GraphHandle handle)
{
(void)handle;
}
void app_invalidateGraph(GraphHandle handle)
{
(void)handle;
}
void app_showRecord(bool show)
{
(void)show;
}
void app_alert(LG_MsgAlert type, const char * format, ...)
{
(void)type;
(void)format;
}
MsgBoxHandle app_confirmMsgBox(const char * caption,
MsgBoxConfirmCallback callback, void * opaque, const char * format, ...)
{
(void)caption;
(void)format;
ui.confirm = callback;
ui.opaque = opaque;
ui.handle = (MsgBoxHandle)(uintptr_t)(++ui.confirmN);
if (atomic_load(&ui.block))
{
atomic_store(&ui.inConfirm, true);
atomic_store(&ui.entered, true);
while (!atomic_load(&ui.release))
usleep(1000);
atomic_store(&ui.inConfirm, false);
}
return ui.handle;
}
void app_msgBoxClose(MsgBoxHandle handle)
{
if (handle)
++ui.closeN;
}
static void waitFor(atomic_uint * value, unsigned int target)
{
for (unsigned int i = 0; i < WAIT_MS; ++i)
{
if (atomic_load_explicit(value, memory_order_acquire) >= target)
return;
usleep(1000);
}
fprintf(stderr, "timeout waiting for %u, got %u\n",
target, atomic_load(value));
CHECK(atomic_load(value) >= target);
}
static void waitBool(atomic_bool * value)
{
for (unsigned int i = 0; i < WAIT_MS; ++i)
{
if (atomic_load_explicit(value, memory_order_acquire))
return;
usleep(1000);
}
CHECK(atomic_load(value));
}
static LG_AudioFormat makeFormat(LG_AudioSampleFormat sampleFormat)
{
return (LG_AudioFormat)
{
.sampleFormat = sampleFormat,
.sampleRate = 48000,
.channelCount = 2,
.channels =
{
LG_AUDIO_CH_FRONT_LEFT,
LG_AUDIO_CH_FRONT_RIGHT,
},
};
}
static void reset(void)
{
memset(&audio, 0, sizeof(audio));
memset(&b, 0, sizeof(b));
memset(&ui, 0, sizeof(ui));
memset(&g_state, 0, sizeof(g_state));
memset(&g_params, 0, sizeof(g_params));
memset(&g_cursor, 0, sizeof(g_cursor));
atomic_init(&b.initN, 0);
atomic_init(&b.freeN, 0);
atomic_init(&b.setupN, 0);
atomic_init(&b.playStartN, 0);
atomic_init(&b.playStopN, 0);
atomic_init(&b.recStartN, 0);
atomic_init(&b.recStopN, 0);
atomic_init(&b.setupOK, true);
atomic_init(&b.playOK, true);
atomic_init(&b.recOK, true);
atomic_init(&ui.block, false);
atomic_init(&ui.entered, false);
atomic_init(&ui.release, false);
atomic_init(&ui.inConfirm, false);
dev = (struct LG_AudioDevOps)
{
.name = "test",
.init = devInit,
.free = devFree,
.playback =
{
.setup = playSetup,
.start = playStart,
.stop = playStop,
.setRate = playRate,
.latency = playLatency,
},
.record =
{
.start = recStart,
.stop = recStop,
},
};
g_params.audioResampler = AUDIO_RESAMPLER_BACKEND;
g_state.micDefaultState = MIC_DEFAULT_PROMPT;
}
static void startAudio(void)
{
lgAudio_init();
CHECK(atomic_load(&b.initN) == 1);
CHECK(lgAudio_supportsPlayback());
CHECK(lgAudio_supportsRecord());
}
static void stopAudio(void)
{
lgAudio_free();
CHECK(atomic_load(&b.freeN) == 1);
}
static void initProvider(struct Provider * p, const char * name,
bool available)
{
memset(p, 0, sizeof(*p));
p->name = name;
p->available = available;
p->attachOK = true;
p->generation = 1;
atomic_init(&p->badDetach, false);
}
static void setStatus(struct Provider * p, bool available,
uint32_t generation)
{
p->available = available;
p->generation = generation;
CHECK(p->status);
const LG_AudioStatus status =
{
.available = available,
.generation = generation,
};
p->status(p->statusOpaque, &status);
}
static bool near(float a, float expected)
{
return fabsf(a - expected) < 0.000001f;
}
static void testFormat(void)
{
LG_AudioFormat format = makeFormat(LG_AUDIO_FMT_S16_LE);
CHECK(audioFormatValid(&format));
CHECK(!audioFormatValid(NULL));
const LG_AudioSampleFormat samples[] =
{
LG_AUDIO_FMT_U8,
LG_AUDIO_FMT_S16_LE,
LG_AUDIO_FMT_S24_LE,
LG_AUDIO_FMT_S32_LE,
LG_AUDIO_FMT_F32_LE,
LG_AUDIO_FMT_F64_LE,
LG_AUDIO_FMT_F32_NE,
};
for (size_t i = 0; i < ARRAY_LENGTH(samples); ++i)
{
format.sampleFormat = samples[i];
CHECK(audioFormatValid(&format));
}
format.sampleFormat = (LG_AudioSampleFormat)-1;
CHECK(!audioFormatValid(&format));
format = makeFormat(LG_AUDIO_FMT_S16_LE);
format.sampleRate = 7999;
CHECK(!audioFormatValid(&format));
format.sampleRate = 384001;
CHECK(!audioFormatValid(&format));
format = makeFormat(LG_AUDIO_FMT_S16_LE);
format.channelCount = 0;
CHECK(!audioFormatValid(&format));
format.channelCount = LG_AUDIO_MAX_CHANNELS + 1;
CHECK(!audioFormatValid(&format));
format = makeFormat(LG_AUDIO_FMT_S16_LE);
format.channels[1] = (LG_AudioChannel)(LG_AUDIO_CH_TOP_REAR_RIGHT + 1);
CHECK(!audioFormatValid(&format));
LG_AudioFormat same = makeFormat(LG_AUDIO_FMT_S16_LE);
CHECK(audioFormatEqual(&same, &same));
format = same;
format.channels[1] = LG_AUDIO_CH_MONO;
CHECK(!audioFormatEqual(&format, &same));
}
static void testConvert(void)
{
float out[4];
const uint8_t u8[] = { 0, 128, 255 };
CHECK(audioConvertToFloat(out, u8, ARRAY_LENGTH(u8), LG_AUDIO_FMT_U8));
CHECK(near(out[0], -1.0f));
CHECK(near(out[1], 0.0f));
CHECK(near(out[2], 127.0f / 128.0f));
const uint8_t s16[] =
{
0x00, 0x80,
0xff, 0xff,
0x00, 0x00,
0xff, 0x7f,
};
CHECK(audioConvertToFloat(out, s16, 4, LG_AUDIO_FMT_S16_LE));
CHECK(near(out[0], -1.0f));
CHECK(near(out[1], -1.0f / 32768.0f));
CHECK(near(out[2], 0.0f));
CHECK(near(out[3], 32767.0f / 32768.0f));
const uint8_t s24[] =
{
0x00, 0x00, 0x80,
0xff, 0xff, 0x7f,
};
CHECK(audioConvertToFloat(out, s24, 2, LG_AUDIO_FMT_S24_LE));
CHECK(near(out[0], -1.0f));
CHECK(near(out[1], 8388607.0f / 8388608.0f));
const uint8_t s32[] =
{
0x00, 0x00, 0x00, 0x80,
0xff, 0xff, 0xff, 0x7f,
};
CHECK(audioConvertToFloat(out, s32, 2, LG_AUDIO_FMT_S32_LE));
CHECK(near(out[0], -1.0f));
CHECK(near(out[1], 1.0f));
const uint8_t f32le[] =
{
0x00, 0x00, 0x80, 0xbe,
0x00, 0x00, 0x40, 0x3f,
};
CHECK(audioConvertToFloat(out, f32le, 2, LG_AUDIO_FMT_F32_LE));
CHECK(near(out[0], -0.25f));
CHECK(near(out[1], 0.75f));
const float f32ne[] = { -0.25f, 0.75f };
CHECK(audioConvertToFloat(out, f32ne, 2, LG_AUDIO_FMT_F32_NE));
CHECK(near(out[0], -0.25f));
CHECK(near(out[1], 0.75f));
const uint8_t f64le[] =
{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xbf,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x3f,
};
CHECK(audioConvertToFloat(out, f64le, 2, LG_AUDIO_FMT_F64_LE));
CHECK(near(out[0], -0.5f));
CHECK(near(out[1], 0.125f));
CHECK(!audioConvertToFloat(NULL, f32ne, 2, LG_AUDIO_FMT_F32_NE));
CHECK(!audioConvertToFloat(out, NULL, 2, LG_AUDIO_FMT_F32_NE));
CHECK(!audioConvertToFloat(out, f32ne, 2, (LG_AudioSampleFormat)-1));
}
static void testProvider(void)
{
reset();
startAudio();
struct Provider fallback;
struct Provider transport;
initProvider(&fallback, "fallback", true);
initProvider(&transport, "transport", false);
lgAudio_setFallback(&ops, &fallback);
CHECK(fallback.attachN == 1);
CHECK(fallback.detachN == 0);
lgAudio_setTransport(&ops, &transport);
CHECK(transport.attachN == 0);
CHECK(fallback.detachN == 0);
transport.attachOK = false;
setStatus(&transport, true, 2);
CHECK(transport.attachN == 1);
CHECK(transport.detachN == 1);
CHECK(fallback.detachN == 1);
CHECK(fallback.attachN == 2);
transport.attachOK = true;
setStatus(&transport, true, 3);
CHECK(transport.attachN == 2);
CHECK(fallback.detachN == 2);
setStatus(&transport, false, 4);
CHECK(transport.detachN == 2);
CHECK(fallback.attachN == 3);
const unsigned int detached = fallback.detachN;
lgAudio_dropFallback();
CHECK(fallback.detachN == detached);
stopAudio();
}
static void testPlaybackRetry(void)
{
reset();
atomic_store(&b.setupOK, false);
startAudio();
struct Provider p;
initProvider(&p, "provider", true);
lgAudio_setFallback(&ops, &p);
CHECK(p.events);
LG_AudioFormat format = makeFormat(LG_AUDIO_FMT_S16_LE);
p.events->playbackStart(p.eventOpaque, 11, &format, NULL);
waitFor(&b.setupN, 2);
CHECK(atomic_load(&b.playStartN) == 0);
const unsigned int failed = atomic_load(&b.setupN);
atomic_store(&b.setupOK, true);
uint8_t frames[480 * 2 * 2] = { 0 };
usleep(10000);
p.events->playbackData(p.eventOpaque, 11, frames, 480, NULL);
waitFor(&b.setupN, failed + 1);
for (unsigned int i = 0; i < WAIT_MS; ++i)
{
if (atomic_load_explicit(&audio.playback.streamGeneration,
memory_order_acquire) == 11)
break;
usleep(1000);
}
CHECK(atomic_load(&audio.playback.streamGeneration) == 11);
for (unsigned int i = 0; i < 8; ++i)
p.events->playbackData(p.eventOpaque, 11, frames, 480, NULL);
waitFor(&b.playStartN, 1);
LG_AudioFailureFn fail = b.playFail;
const uint32_t cookie = b.playCookie;
CHECK(fail);
const unsigned int stopped = atomic_load(&b.playStopN);
fail(cookie);
waitFor(&b.playStopN, stopped + 1);
const unsigned int retry = atomic_load(&b.setupN);
usleep(10000);
p.events->playbackData(p.eventOpaque, 11, frames, 480, NULL);
waitFor(&b.setupN, retry + 1);
p.events->playbackStop(p.eventOpaque, 11);
stopAudio();
}
static void testConsent(void)
{
reset();
startAudio();
struct Provider p;
initProvider(&p, "provider", true);
lgAudio_setFallback(&ops, &p);
LG_AudioFormat format = makeFormat(LG_AUDIO_FMT_S16_LE);
p.events->recordStart(p.eventOpaque, 20, &format);
CHECK(ui.confirmN == 1);
CHECK(ui.confirm);
MsgBoxConfirmCallback stale = ui.confirm;
void * staleOpaque = ui.opaque;
p.events->recordStop(p.eventOpaque, 20);
CHECK(ui.closeN == 1);
stale(true, staleOpaque);
CHECK(atomic_load(&b.recStartN) == 0);
p.events->recordStart(p.eventOpaque, 21, &format);
CHECK(ui.confirmN == 2);
CHECK(ui.confirm);
stale(true, staleOpaque);
CHECK(atomic_load(&b.recStartN) == 0);
ui.confirm(true, ui.opaque);
waitFor(&b.recStartN, 1);
CHECK(b.recFormat.sampleRate == format.sampleRate);
CHECK(b.recFormat.channelCount == format.channelCount);
CHECK(b.push);
uint8_t frames[16] = { 0 };
CHECK(b.push(frames, 4, NULL));
CHECK(p.dataN == 1);
p.events->recordStop(p.eventOpaque, 21);
waitFor(&b.recStopN, 1);
stopAudio();
}
struct RecordCall
{
struct Provider * provider;
LG_AudioFormat format;
};
static void * callRecord(void * opaque)
{
struct RecordCall * call = opaque;
call->provider->events->recordStart(call->provider->eventOpaque,
30, &call->format);
return NULL;
}
struct SwitchCall
{
struct Provider * provider;
};
static void * switchProvider(void * opaque)
{
struct SwitchCall * call = opaque;
lgAudio_setTransport(&ops, call->provider);
return NULL;
}
static void testQuiesce(void)
{
reset();
startAudio();
struct Provider fallback;
struct Provider transport;
initProvider(&fallback, "fallback", true);
initProvider(&transport, "transport", true);
lgAudio_setFallback(&ops, &fallback);
atomic_store(&ui.block, true);
struct RecordCall record =
{
.provider = &fallback,
.format = makeFormat(LG_AUDIO_FMT_S16_LE),
};
pthread_t recordThread;
CHECK(pthread_create(&recordThread, NULL, callRecord, &record) == 0);
waitBool(&ui.entered);
struct SwitchCall change = { .provider = &transport };
pthread_t switchThread;
CHECK(pthread_create(&switchThread, NULL, switchProvider, &change) == 0);
for (unsigned int i = 0; i < WAIT_MS; ++i)
{
if (atomic_load_explicit(&audio.activeCallbacks,
memory_order_acquire) & ACTIVE_CALLBACK_WAITING)
break;
usleep(1000);
}
CHECK(atomic_load(&audio.activeCallbacks) & ACTIVE_CALLBACK_WAITING);
CHECK(fallback.detachN == 0);
CHECK(!atomic_load(&fallback.badDetach));
MsgBoxConfirmCallback stale = ui.confirm;
void * staleOpaque = ui.opaque;
atomic_store(&ui.release, true);
CHECK(pthread_join(recordThread, NULL) == 0);
CHECK(pthread_join(switchThread, NULL) == 0);
CHECK(fallback.detachN == 1);
CHECK(transport.attachN == 1);
CHECK(ui.closeN == 1);
CHECK(!atomic_load(&fallback.badDetach));
CHECK(stale);
stale(true, staleOpaque);
CHECK(atomic_load(&b.recStartN) == 0);
stopAudio();
}
struct Test
{
const char * name;
void (*run)(void);
};
static const struct Test tests[] =
{
{ "format" , testFormat },
{ "convert" , testConvert },
{ "provider" , testProvider },
{ "playback-retry", testPlaybackRetry },
{ "consent" , testConsent },
{ "quiesce" , testQuiesce },
};
int main(int argc, char ** argv)
{
debug_init();
if (argc == 2)
{
for (size_t i = 0; i < ARRAY_LENGTH(tests); ++i)
if (strcmp(argv[1], tests[i].name) == 0)
{
tests[i].run();
return 0;
}
fprintf(stderr, "unknown test: %s\n", argv[1]);
return EXIT_FAILURE;
}
if (argc != 1)
return EXIT_FAILURE;
for (size_t i = 0; i < ARRAY_LENGTH(tests); ++i)
tests[i].run();
return 0;
}