mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-23 15:41:31 +00:00
Some checks are pending
build / client (Debug, map[cc:clang cxx:clang++], libdecor) (push) Waiting to run
build / client (Debug, map[cc:clang cxx:clang++], xdg-shell) (push) Waiting to run
build / client (Debug, map[cc:gcc cxx:g++], libdecor) (push) Waiting to run
build / client (Debug, map[cc:gcc cxx:g++], xdg-shell) (push) Waiting to run
build / client (Release, map[cc:clang cxx:clang++], libdecor) (push) Waiting to run
build / client (Release, map[cc:clang cxx:clang++], xdg-shell) (push) Waiting to run
build / client (Release, map[cc:gcc cxx:g++], libdecor) (push) Waiting to run
build / client (Release, map[cc:gcc cxx:g++], xdg-shell) (push) Waiting to run
build / module (push) Waiting to run
build / host-linux (push) Waiting to run
build / host-windows-cross (push) Waiting to run
build / host-windows-native (push) Waiting to run
build / idd (push) Waiting to run
build / obs (clang) (push) Waiting to run
build / obs (gcc) (push) Waiting to run
build / docs (push) Waiting to run
A stable clock model can request a negative recovery slew while the unbounded ring is physically underrun. This leaves the reader ahead of the writer indefinitely and outputs silence until playback is restarted. Floor active recovery at the physical low-water requirement. Rebase the device-clock phase by any extra refill so modeled latency remains at the target without restarting the underrun correction cycle. Add a deterministic regression for negative physical debt opposing the stable clock model.
1053 lines
29 KiB
C
1053 lines
29 KiB
C
/**
|
|
* 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 testPlaybackJitter(void)
|
|
{
|
|
PlaybackSourceData source = { 0 };
|
|
playbackPrepareMediaClock(&source, NULL);
|
|
|
|
bool discontinuity = false;
|
|
playbackMapMediaTime(&source, NULL, 480, 48000, 0,
|
|
&discontinuity);
|
|
source.inputPosition = 480;
|
|
source.lastArrivalTime = 0;
|
|
|
|
discontinuity = true;
|
|
playbackMapMediaTime(&source, NULL, 480, 48000,
|
|
INT64_C(110000000), &discontinuity);
|
|
CHECK(discontinuity);
|
|
CHECK(source.arrivalJitterSec == 0.0);
|
|
|
|
memset(&source, 0, sizeof(source));
|
|
playbackPrepareMediaClock(&source, NULL);
|
|
discontinuity = false;
|
|
playbackMapMediaTime(&source, NULL, 480, 48000, 0,
|
|
&discontinuity);
|
|
source.inputPosition = 480;
|
|
source.lastArrivalTime = 0;
|
|
playbackMapMediaTime(&source, NULL, 480, 48000,
|
|
INT64_C(20000000), &discontinuity);
|
|
CHECK(source.arrivalJitterSec > 0.009);
|
|
CHECK(source.arrivalJitterSec < 0.011);
|
|
}
|
|
|
|
static void testPlaybackRecovery(void)
|
|
{
|
|
reset();
|
|
startAudio();
|
|
|
|
const LG_AudioFormat format = makeFormat(LG_AUDIO_FMT_S16_LE);
|
|
CHECK(playbackStart(&format, NULL, false, false));
|
|
CHECK(audio.playback.rateControl == PLAYBACK_RATE_BACKEND);
|
|
playbackSetState(STREAM_STATE_RUN);
|
|
|
|
PlaybackSourceData * source = &audio.playback.sourceData;
|
|
const int64_t arrival = nanotime();
|
|
playbackClockReset(&source->deviceClock,
|
|
arrival, 0.0, 1.0 / format.sampleRate);
|
|
source->deviceClockStable = true;
|
|
source->outputPosition = 0;
|
|
source->lastRatio = 0.999;
|
|
source->lastClockRatio = 1.0002;
|
|
source->sourcePacketDurationSec = 0.010;
|
|
playbackPublishDeviceTiming(16, arrival, 0, 0.0, 0);
|
|
|
|
CHECK(ringbuffer_consume(audio.playback.buffer, NULL, 16) == 16);
|
|
CHECK(ringbuffer_getCount(audio.playback.buffer) == -16);
|
|
uint8_t frames[1200 * 2 * 2] = { 0 };
|
|
CHECK(playbackData(frames, 1200, NULL, arrival) ==
|
|
PLAYBACK_DATA_PROCESSED);
|
|
|
|
CHECK(source->sourcePacketDurationSec < 0.010);
|
|
CHECK(source->sourcePacketDurationSec > 0.009);
|
|
CHECK(source->sourcePacketRecovering);
|
|
CHECK(source->outputPosition == 1770);
|
|
CHECK(ringbuffer_getCount(audio.playback.buffer) == 1754);
|
|
CHECK(fabs(atomic_load(&audio.playback.backendResampleRatio) -
|
|
source->lastClockRatio) < 0.000001);
|
|
|
|
CHECK(playbackData(frames, 1200, NULL, arrival + 1000) ==
|
|
PLAYBACK_DATA_PROCESSED);
|
|
CHECK(source->sourcePacketDurationSec < 0.010);
|
|
CHECK(source->sourcePacketRecovering);
|
|
|
|
CHECK(playbackData(frames, 480, NULL,
|
|
arrival + INT64_C(10000000)) == PLAYBACK_DATA_PROCESSED);
|
|
CHECK(!source->sourcePacketRecovering);
|
|
CHECK(fabs(source->sourcePacketDurationSec - 0.010) < 0.000001);
|
|
|
|
stopAudio();
|
|
}
|
|
|
|
static void testPlaybackStableUnderrun(void)
|
|
{
|
|
reset();
|
|
startAudio();
|
|
|
|
const LG_AudioFormat format = makeFormat(LG_AUDIO_FMT_S16_LE);
|
|
CHECK(playbackStart(&format, NULL, false, false));
|
|
CHECK(audio.playback.rateControl == PLAYBACK_RATE_BACKEND);
|
|
playbackSetState(STREAM_STATE_RUN);
|
|
|
|
PlaybackSourceData * source = &audio.playback.sourceData;
|
|
const int64_t arrival = nanotime();
|
|
playbackClockReset(&source->deviceClock,
|
|
arrival, 0.0, 1.0 / format.sampleRate);
|
|
source->deviceClockStable = true;
|
|
source->devicePositionOffsetFrames = 0.0;
|
|
source->outputPosition = 1000;
|
|
source->lastRatio = 0.999;
|
|
source->lastClockRatio = 1.0002;
|
|
source->sourcePacketDurationSec = 0.010;
|
|
playbackPublishDeviceTiming(16, arrival, 0, 0.0, 0);
|
|
|
|
CHECK(source->outputPosition - computeDevicePosition(arrival) > 900.0);
|
|
CHECK(ringbuffer_append(audio.playback.buffer, NULL, 1000) == 1000);
|
|
CHECK(ringbuffer_consume(audio.playback.buffer, NULL, 1128) == 1128);
|
|
CHECK(ringbuffer_getCount(audio.playback.buffer) == -128);
|
|
uint8_t frames[64 * 2 * 2];
|
|
uint8_t output[64 * 2 * 2];
|
|
memset(frames, 0x5a, sizeof(frames));
|
|
CHECK(playbackData(frames, 64, NULL, arrival) ==
|
|
PLAYBACK_DATA_PROCESSED);
|
|
|
|
const int recoveredOccupancy =
|
|
ringbuffer_getCount(audio.playback.buffer) - 64;
|
|
const double recoveredPosition = source->outputPosition - 64;
|
|
const double recoveredLatency = recoveredPosition -
|
|
computeDevicePosition(source->sourceClock.time);
|
|
const double targetLowWater =
|
|
audio.playback.deviceMaxPeriodFrames * 1.1 +
|
|
0.001 * format.sampleRate;
|
|
const double sourceReserve =
|
|
max(source->sourcePacketDurationSec * 0.5,
|
|
source->sourcePhaseReserveSec) * format.sampleRate;
|
|
const double targetLatency = targetLowWater + sourceReserve;
|
|
CHECK(recoveredOccupancy >= (int)ceil(targetLowWater));
|
|
CHECK(fabs(recoveredLatency - targetLatency) < 0.000001);
|
|
CHECK(!source->bufferOverrunPending);
|
|
CHECK(source->deviceClockStable);
|
|
CHECK(source->devicePositionOffsetFrames > 0.0);
|
|
CHECK(fabs(source->deviceClock.frameSec -
|
|
1.0 / format.sampleRate) < 0.000000001);
|
|
CHECK(fabs(atomic_load(&audio.playback.backendResampleRatio) -
|
|
source->lastClockRatio) < 0.000001);
|
|
|
|
CHECK(ringbuffer_consume(audio.playback.buffer, output, 64) == 64);
|
|
CHECK(ringbuffer_getCount(audio.playback.buffer) == recoveredOccupancy);
|
|
|
|
const double recoveredOffset =
|
|
source->devicePositionOffsetFrames;
|
|
const int64_t secondPacketArrival = arrival +
|
|
llrint(64.0e9 / format.sampleRate);
|
|
CHECK(playbackData(frames, 64, NULL,
|
|
secondPacketArrival) == PLAYBACK_DATA_PROCESSED);
|
|
CHECK(source->devicePositionOffsetFrames == recoveredOffset);
|
|
CHECK(ringbuffer_getCount(audio.playback.buffer) ==
|
|
recoveredOccupancy + 64);
|
|
CHECK(ringbuffer_consume(audio.playback.buffer, output, 64) == 64);
|
|
CHECK(ringbuffer_getCount(audio.playback.buffer) == recoveredOccupancy);
|
|
|
|
stopAudio();
|
|
}
|
|
|
|
static void testPlaybackResume(void)
|
|
{
|
|
reset();
|
|
g_params.audioDebug = true;
|
|
startAudio();
|
|
|
|
struct Provider p;
|
|
initProvider(&p, "provider", true);
|
|
lgAudio_setFallback(&ops, &p);
|
|
CHECK(p.events);
|
|
|
|
const LG_AudioFormat format = makeFormat(LG_AUDIO_FMT_S16_LE);
|
|
p.events->playbackStart(p.eventOpaque, 11, &format, NULL);
|
|
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);
|
|
playbackSetState(STREAM_STATE_RUN);
|
|
audio.playback.backendStartTime = nanotime();
|
|
|
|
uint8_t output[16 * 2 * 2];
|
|
CHECK(b.pull(output, 16) == 16);
|
|
CHECK(b.pull(output, 16) == 16);
|
|
CHECK(atomic_load(&audio.playback.underruns) == 1);
|
|
|
|
const int refill = -ringbuffer_getCount(audio.playback.buffer) + 16;
|
|
CHECK(ringbuffer_append(audio.playback.buffer, NULL, refill) == refill);
|
|
CHECK(b.pull(output, 16) == 16);
|
|
CHECK(!audio.playback.deviceData.underrunning);
|
|
CHECK(b.pull(output, 16) == 16);
|
|
CHECK(b.pull(output, 16) == 16);
|
|
CHECK(atomic_load(&audio.playback.underruns) == 2);
|
|
|
|
const unsigned int oldSyncEpoch = atomic_load_explicit(
|
|
&audio.playback.syncDiagnosticsEpoch, memory_order_acquire);
|
|
LG_LOCK(audio.playback.sourceLock);
|
|
PlaybackDiagnostics * diagnostics = playbackDiagnosticsLocked();
|
|
diagnostics->sync = (PlaybackSyncDiagnostics)
|
|
{
|
|
.epoch = oldSyncEpoch,
|
|
.underruns = 99,
|
|
};
|
|
diagnostics->pending |= PLAYBACK_DIAGNOSTIC_SYNC_LOG;
|
|
LG_UNLOCK(audio.playback.sourceLock);
|
|
atomic_store(&audio.playback.backlogTrimTarget, 0);
|
|
atomic_store(&audio.playback.backlogTrimmedFrames, 123);
|
|
audio.playback.sourceData.bufferOverruns = 7;
|
|
audio.playback.sourceData.backlogTrimArmed = false;
|
|
|
|
p.events->playbackStop(p.eventOpaque, 11);
|
|
CHECK(playbackGetState() == STREAM_STATE_KEEP_ALIVE);
|
|
CHECK(atomic_load(&audio.playback.underruns) == 0);
|
|
CHECK(!audio.playback.deviceData.underrunning);
|
|
CHECK(atomic_load(&audio.playback.backlogTrimTarget) == -1);
|
|
CHECK(atomic_load(&audio.playback.backlogTrimmedFrames) == 0);
|
|
CHECK(audio.playback.sourceData.bufferOverruns == 0);
|
|
CHECK(audio.playback.sourceData.backlogTrimArmed);
|
|
CHECK(atomic_load(&audio.playback.syncDiagnosticsEpoch) != oldSyncEpoch);
|
|
CHECK(!(audio.playback.diagnostics.pending &
|
|
PLAYBACK_DIAGNOSTIC_SYNC_LOG));
|
|
|
|
CHECK(b.pull(output, 16) == 16);
|
|
CHECK(b.pull(output, 16) == 16);
|
|
CHECK(atomic_load(&audio.playback.underruns) == 0);
|
|
|
|
const unsigned int setupN = atomic_load(&b.setupN);
|
|
p.events->playbackStart(p.eventOpaque, 12, &format, NULL);
|
|
CHECK(atomic_load(&audio.playback.streamGeneration) == 12);
|
|
CHECK(playbackGetState() == STREAM_STATE_RESUMING);
|
|
CHECK(atomic_load(&b.setupN) == setupN);
|
|
|
|
uint8_t input[480 * 2 * 2] = { 0 };
|
|
p.events->playbackData(p.eventOpaque, 12, input, 480, NULL);
|
|
CHECK(atomic_load(&audio.playback.streamGeneration) == 12);
|
|
CHECK(playbackGetState() == STREAM_STATE_RUN);
|
|
CHECK(atomic_load(&audio.playback.underruns) == 0);
|
|
|
|
p.events->playbackStop(p.eventOpaque, 12);
|
|
CHECK(playbackGetState() == STREAM_STATE_KEEP_ALIVE);
|
|
|
|
PlaybackSourceData * source = &audio.playback.sourceData;
|
|
const int64_t arrival = nanotime();
|
|
const int64_t idleFrames = INT64_C(30) * format.sampleRate;
|
|
const double disciplinedFrameSec =
|
|
1.0002 / format.sampleRate;
|
|
playbackClockReset(&source->deviceClock,
|
|
arrival - INT64_C(30000000000), 0.0, disciplinedFrameSec);
|
|
playbackClockReset(&source->outputClock,
|
|
arrival - INT64_C(30000000000), 0.0, disciplinedFrameSec);
|
|
source->deviceClockStable = true;
|
|
source->devicePositionOffsetFrames = 37.5;
|
|
source->deviceTimingSequence = atomic_load_explicit(
|
|
&audio.playback.deviceTiming.sequence, memory_order_acquire);
|
|
const int idleDebt = clamp(
|
|
(int64_t)ringbuffer_getCount(audio.playback.buffer) + idleFrames,
|
|
INT64_C(0), (int64_t)INT_MAX);
|
|
CHECK(ringbuffer_consume(
|
|
audio.playback.buffer, NULL, idleDebt) == idleDebt);
|
|
playbackPublishDeviceTiming(16, arrival, idleFrames,
|
|
(double)idleFrames, source->deviceDiscontinuity);
|
|
|
|
p.events->playbackStart(p.eventOpaque, 13, &format, NULL);
|
|
CHECK(atomic_load(&audio.playback.streamGeneration) == 13);
|
|
p.events->playbackData(p.eventOpaque, 13, input, 480, NULL);
|
|
CHECK(playbackGetState() == STREAM_STATE_RUN);
|
|
CHECK(source->deviceClock.time == arrival);
|
|
CHECK(source->deviceClock.position == idleFrames);
|
|
CHECK(fabs(source->deviceClock.frameSec - disciplinedFrameSec) < 1.0e-15);
|
|
CHECK(source->devicePositionOffsetFrames == 37.5);
|
|
|
|
p.events->playbackStop(p.eventOpaque, 13);
|
|
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 },
|
|
{ "jitter" , testPlaybackJitter },
|
|
{ "recovery" , testPlaybackRecovery },
|
|
{ "stable-underrun", testPlaybackStableUnderrun },
|
|
{ "resume" , testPlaybackResume },
|
|
{ "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;
|
|
}
|