mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-11-12 23:07:21 +00:00
Scope the legacy latency offset to classic SPICE and use the LGMP poll bound as its fixed arrival reserve. Keep learned jitter additive. Select provider clock feedback only when the current remote endpoint advertises it, avoiding a failed startup attempt on broker v1.
1141 lines
32 KiB
C
1141 lines
32 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);
|
|
const PlaybackPolicy policy = playbackPolicyFromOps(&ops);
|
|
CHECK(playbackStart(&format, NULL, false, false, &policy));
|
|
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);
|
|
const PlaybackPolicy policy = playbackPolicyFromOps(&ops);
|
|
CHECK(playbackStart(&format, NULL, false, false, &policy));
|
|
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 bool testClockFeedback(void * opaque, uint32_t generation,
|
|
const LG_AudioClock * clock, double targetRate)
|
|
{
|
|
(void)opaque;
|
|
(void)generation;
|
|
(void)clock;
|
|
(void)targetRate;
|
|
return true;
|
|
}
|
|
|
|
static bool testClockFeedbackAvailable(void * opaque)
|
|
{
|
|
return *(const bool *)opaque;
|
|
}
|
|
|
|
static void testLatencyPolicy(void)
|
|
{
|
|
LG_AudioOps providerOps = ops;
|
|
PlaybackPolicy policy = playbackPolicyFromOps(&providerOps);
|
|
CHECK(policy.flags == 0);
|
|
CHECK(policy.arrivalReserveUs ==
|
|
PLAYBACK_DEFAULT_ARRIVAL_RESERVE_US);
|
|
CHECK(playbackComputeLatencyOffsetFrames(
|
|
false, &policy, 6, 48000) == 0.0);
|
|
|
|
providerOps.playbackFlags =
|
|
LG_AUDIO_PLAYBACK_LEGACY_LATENCY_OFFSET;
|
|
policy = playbackPolicyFromOps(&providerOps);
|
|
CHECK(playbackComputeLatencyOffsetFrames(
|
|
false, &policy, 6, 48000) == 288.0);
|
|
CHECK(playbackComputeLatencyOffsetFrames(
|
|
true, &policy, 6, 48000) == 0.0);
|
|
|
|
providerOps.playbackFlags = 0;
|
|
providerOps.playbackArrivalReserveUs = 250;
|
|
const PlaybackPolicy lowLatencyPolicy =
|
|
playbackPolicyFromOps(&providerOps);
|
|
CHECK(lowLatencyPolicy.arrivalReserveUs == 250);
|
|
CHECK(fabs(playbackArrivalReserveFrames(
|
|
0.0, 48000, lowLatencyPolicy.arrivalReserveUs) - 12.0) < 0.001);
|
|
CHECK(fabs(playbackArrivalReserveFrames(
|
|
0.010, 48000, lowLatencyPolicy.arrivalReserveUs) - 492.0) <
|
|
0.001);
|
|
|
|
CHECK(validOps(&providerOps));
|
|
providerOps.playbackFlags = 1U << 31;
|
|
CHECK(!validOps(&providerOps));
|
|
providerOps.playbackFlags = 0;
|
|
providerOps.playbackArrivalReserveUs =
|
|
PLAYBACK_MAX_ARRIVAL_RESERVE_US + 1U;
|
|
CHECK(!validOps(&providerOps));
|
|
|
|
providerOps.playbackArrivalReserveUs = 0;
|
|
providerOps.clockFeedback = testClockFeedback;
|
|
CHECK(playbackProviderRateControlAvailable(&providerOps, NULL));
|
|
providerOps.clockFeedbackAvailable = testClockFeedbackAvailable;
|
|
bool feedbackAvailable = false;
|
|
CHECK(!playbackProviderRateControlAvailable(
|
|
&providerOps, &feedbackAvailable));
|
|
feedbackAvailable = true;
|
|
CHECK(playbackProviderRateControlAvailable(
|
|
&providerOps, &feedbackAvailable));
|
|
providerOps.clockFeedback = NULL;
|
|
CHECK(!validOps(&providerOps));
|
|
|
|
reset();
|
|
startAudio();
|
|
const LG_AudioFormat format = makeFormat(LG_AUDIO_FMT_S16_LE);
|
|
const PlaybackPolicy defaultPolicy = playbackPolicyFromOps(&ops);
|
|
CHECK(playbackStart(&format, NULL, false, false, &defaultPolicy));
|
|
CHECK(atomic_load(&b.setupN) == 1);
|
|
|
|
playbackSetState(STREAM_STATE_KEEP_ALIVE);
|
|
CHECK(playbackStart(&format, NULL, false, false, &defaultPolicy));
|
|
CHECK(atomic_load(&b.setupN) == 1);
|
|
CHECK(playbackGetState() == STREAM_STATE_RESUMING);
|
|
|
|
playbackSetState(STREAM_STATE_KEEP_ALIVE);
|
|
CHECK(playbackStart(
|
|
&format, NULL, false, false, &lowLatencyPolicy));
|
|
CHECK(atomic_load(&b.setupN) == 2);
|
|
CHECK(playbackGetState() == STREAM_STATE_SETUP_SOURCE);
|
|
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 },
|
|
{ "latency-policy" , testLatencyPolicy },
|
|
{ "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;
|
|
}
|