looking-glass/client/src/audio.c
Geoffrey McRae 41884bfcc5 [client] audio: allow the audiodev to return the periodFrames
This change allows the audiodevs to return the minimum period frames
needed to start playback instead of having to rely on a pull to obtain
these details.

Additionally we are using this information to select an initial start
latency as well as to train the desired latency in order to keep it as
low as possible.
2022-01-27 18:03:11 +11:00

678 lines
18 KiB
C

/**
* Looking Glass
* Copyright © 2017-2022 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 "audio.h"
#include "main.h"
#include "common/array.h"
#include "common/util.h"
#include "common/ringbuffer.h"
#include "dynamic/audiodev.h"
#include <math.h>
#include <samplerate.h>
#include <stdalign.h>
#include <string.h>
typedef enum
{
STREAM_STATE_STOP,
STREAM_STATE_SETUP,
STREAM_STATE_RUN,
STREAM_STATE_DRAIN
}
StreamState;
#define STREAM_ACTIVE(state) \
(state == STREAM_STATE_SETUP || state == STREAM_STATE_RUN)
typedef struct
{
int periodFrames;
double periodSec;
int64_t nextTime;
int64_t nextPosition;
double b;
double c;
}
PlaybackDeviceData;
typedef struct
{
float * framesIn;
float * framesOut;
int framesOutSize;
int periodFrames;
double periodSec;
int64_t nextTime;
int64_t nextPosition;
double b;
double c;
int64_t devLastTime;
int64_t devNextTime;
int64_t devLastPosition;
int64_t devNextPosition;
double offsetError;
double offsetErrorIntegral;
double ratioIntegral;
SRC_STATE * src;
}
PlaybackSpiceData;
typedef struct
{
struct LG_AudioDevOps * audioDev;
struct
{
StreamState state;
int volumeChannels;
uint16_t volume[8];
bool mute;
int channels;
int sampleRate;
int stride;
RingBuffer buffer;
RingBuffer deviceTiming;
LG_Lock lock;
RingBuffer timings;
GraphHandle graph;
// These two structs contain data specifically for use in the device and
// Spice data threads respectively. Keep them on separate cache lines to
// avoid false sharing
alignas(64) PlaybackDeviceData deviceData;
alignas(64) PlaybackSpiceData spiceData;
int targetLatencyFrames;
}
playback;
struct
{
bool started;
int volumeChannels;
uint16_t volume[8];
bool mute;
int stride;
uint32_t time;
}
record;
}
AudioState;
static AudioState audio = { 0 };
typedef struct
{
int64_t nextTime;
int64_t nextPosition;
}
PlaybackDeviceTick;
static void playbackStopNL(void);
void audio_init(void)
{
// search for the best audiodev to use
for(int i = 0; i < LG_AUDIODEV_COUNT; ++i)
if (LG_AudioDevs[i]->init())
{
audio.audioDev = LG_AudioDevs[i];
LG_LOCK_INIT(audio.playback.lock);
DEBUG_INFO("Using AudioDev: %s", audio.audioDev->name);
return;
}
DEBUG_WARN("Failed to initialize an audio backend");
}
void audio_free(void)
{
if (!audio.audioDev)
return;
// immediate stop of the stream, do not wait for drain
LG_LOCK(audio.playback.lock);
playbackStopNL();
LG_UNLOCK(audio.playback.lock);
audio_recordStop();
audio.audioDev->free();
audio.audioDev = NULL;
LG_LOCK_FREE(audio.playback.lock);
}
bool audio_supportsPlayback(void)
{
return audio.audioDev && audio.audioDev->playback.start;
}
static const char * audioGraphFormatFn(const char * name,
float min, float max, float avg, float freq, float last)
{
static char title[64];
snprintf(title, sizeof(title),
"%s: min:%4.2f max:%4.2f avg:%4.2f now:%4.2f",
name, min, max, avg, last);
return title;
}
static void playbackStopNL(void)
{
if (audio.playback.state == STREAM_STATE_STOP)
return;
audio.playback.state = STREAM_STATE_STOP;
audio.audioDev->playback.stop();
ringbuffer_free(&audio.playback.buffer);
ringbuffer_free(&audio.playback.deviceTiming);
audio.playback.spiceData.src = src_delete(audio.playback.spiceData.src);
if (audio.playback.spiceData.framesIn)
{
free(audio.playback.spiceData.framesIn);
free(audio.playback.spiceData.framesOut);
audio.playback.spiceData.framesIn = NULL;
audio.playback.spiceData.framesOut = NULL;
}
if (audio.playback.timings)
{
app_unregisterGraph(audio.playback.graph);
ringbuffer_free(&audio.playback.timings);
}
}
static int playbackPullFrames(uint8_t * dst, int frames)
{
DEBUG_ASSERT(frames >= 0);
if (frames == 0)
return frames;
PlaybackDeviceData * data = &audio.playback.deviceData;
int64_t now = nanotime();
if (audio.playback.buffer)
{
static bool first = true;
// Measure the device clock and post to the Spice thread
if (frames != data->periodFrames || first)
{
if (first)
{
data->nextTime = now;
first = false;
}
data->nextTime += llrint(data->periodSec * 1.0e9);
data->nextPosition += frames;
double bandwidth = 0.05;
double omega = 2.0 * M_PI * bandwidth * data->periodSec;
data->b = M_SQRT2 * omega;
data->c = omega * omega;
}
else
{
double error = (now - data->nextTime) * 1.0e-9;
if (fabs(error) >= 0.2)
{
// Clock error is too high; slew the read pointer and reset the timing
// parameters to avoid getting too far out of sync
int slewFrames = round(error * audio.playback.sampleRate);
ringbuffer_consume(audio.playback.buffer, NULL, slewFrames);
data->periodSec = (double) frames / audio.playback.sampleRate;
data->nextTime = now + llrint(data->periodSec * 1.0e9);
data->nextPosition += slewFrames + frames;
}
else
{
data->nextTime +=
llrint((data->b * error + data->periodSec) * 1.0e9);
data->periodSec += data->c * error;
data->nextPosition += frames;
}
}
PlaybackDeviceTick tick = {
.nextTime = data->nextTime,
.nextPosition = data->nextPosition
};
ringbuffer_append(audio.playback.deviceTiming, &tick, 1);
ringbuffer_consume(audio.playback.buffer, dst, frames);
}
else
frames = 0;
if (audio.playback.state == STREAM_STATE_DRAIN &&
ringbuffer_getCount(audio.playback.buffer) <= 0)
{
LG_LOCK(audio.playback.lock);
playbackStopNL();
LG_UNLOCK(audio.playback.lock);
}
return frames;
}
void audio_playbackStart(int channels, int sampleRate, PSAudioFormat format,
uint32_t time)
{
if (!audio.audioDev)
return;
LG_LOCK(audio.playback.lock);
if (audio.playback.state != STREAM_STATE_STOP)
{
// Stop the current playback immediately. Even if the format is compatible,
// we may not have enough data left in the buffers to avoid underrunning
playbackStopNL();
}
int srcError;
audio.playback.spiceData.src =
src_new(SRC_SINC_BEST_QUALITY, channels, &srcError);
if (!audio.playback.spiceData.src)
{
DEBUG_ERROR("Failed to create resampler: %s", src_strerror(srcError));
goto done;
}
const int bufferFrames = sampleRate;
audio.playback.buffer = ringbuffer_newUnbounded(bufferFrames,
channels * sizeof(float));
audio.playback.deviceTiming = ringbuffer_new(16, sizeof(PlaybackDeviceTick));
audio.playback.channels = channels;
audio.playback.sampleRate = sampleRate;
audio.playback.stride = channels * sizeof(float);
audio.playback.state = STREAM_STATE_SETUP;
audio.playback.deviceData.nextPosition = 0;
audio.playback.spiceData.nextPosition = 0;
audio.playback.spiceData.devLastTime = INT64_MIN;
audio.playback.spiceData.devNextTime = INT64_MIN;
audio.playback.spiceData.offsetError = 0.0;
audio.playback.spiceData.offsetErrorIntegral = 0.0;
audio.playback.spiceData.ratioIntegral = 0.0;
int frames;
audio.audioDev->playback.setup(channels, sampleRate, playbackPullFrames,
&frames);
audio.playback.deviceData.periodFrames = frames;
audio.playback.targetLatencyFrames = frames;
audio.playback.deviceData.periodSec =
(double)frames / audio.playback.sampleRate;
// if a volume level was stored, set it before we return
if (audio.playback.volumeChannels)
audio.audioDev->playback.volume(
audio.playback.volumeChannels,
audio.playback.volume);
// set the inital mute state
if (audio.audioDev->playback.mute)
audio.audioDev->playback.mute(audio.playback.mute);
// if the audio dev can report it's latency setup a timing graph
audio.playback.timings = ringbuffer_new(1200, sizeof(float));
audio.playback.graph = app_registerGraph("PLAYBACK",
audio.playback.timings, 0.0f, 100.0f, audioGraphFormatFn);
audio.playback.state = STREAM_STATE_SETUP;
done:
LG_UNLOCK(audio.playback.lock);
}
void audio_playbackStop(void)
{
if (!audio.audioDev || audio.playback.state == STREAM_STATE_STOP)
return;
audio.playback.state = STREAM_STATE_DRAIN;
return;
}
void audio_playbackVolume(int channels, const uint16_t volume[])
{
if (!audio.audioDev || !audio.audioDev->playback.volume)
return;
// store the values so we can restore the state if the stream is restarted
channels = min(ARRAY_LENGTH(audio.playback.volume), channels);
memcpy(audio.playback.volume, volume, sizeof(uint16_t) * channels);
audio.playback.volumeChannels = channels;
if (!STREAM_ACTIVE(audio.playback.state))
return;
audio.audioDev->playback.volume(channels, volume);
}
void audio_playbackMute(bool mute)
{
if (!audio.audioDev || !audio.audioDev->playback.mute)
return;
// store the value so we can restore it if the stream is restarted
audio.playback.mute = mute;
if (!STREAM_ACTIVE(audio.playback.state))
return;
audio.audioDev->playback.mute(mute);
}
void audio_playbackData(uint8_t * data, size_t size)
{
if (!audio.audioDev || size == 0)
return;
if (!STREAM_ACTIVE(audio.playback.state))
return;
PlaybackSpiceData * spiceData = &audio.playback.spiceData;
PlaybackDeviceData * devData = &audio.playback.deviceData;
int64_t now = nanotime();
// Convert from s16 to f32 samples
int spiceStride = audio.playback.channels * sizeof(int16_t);
int frames = size / spiceStride;
bool periodChanged = frames != spiceData->periodFrames;
bool init = spiceData->periodFrames == 0;
if (periodChanged)
{
if (spiceData->framesIn)
{
free(spiceData->framesIn);
free(spiceData->framesOut);
}
spiceData->periodFrames = frames;
spiceData->framesIn = malloc(frames * audio.playback.stride);
spiceData->framesOutSize = round(frames * 1.1);
spiceData->framesOut =
malloc(spiceData->framesOutSize * audio.playback.stride);
}
src_short_to_float_array((int16_t *) data, spiceData->framesIn,
frames * audio.playback.channels);
// Receive timing information from the audio device thread
PlaybackDeviceTick deviceTick;
while (ringbuffer_consume(audio.playback.deviceTiming, &deviceTick, 1))
{
spiceData->devLastTime = spiceData->devNextTime;
spiceData->devLastPosition = spiceData->devNextPosition;
spiceData->devNextTime = deviceTick.nextTime;
spiceData->devNextPosition = deviceTick.nextPosition;
}
// If the buffer is getting too empty increase the target latency
static bool checkFill = false;
if (checkFill && audio.playback.state == STREAM_STATE_RUN &&
ringbuffer_getCount(audio.playback.buffer) < devData->periodFrames)
{
audio.playback.targetLatencyFrames += devData->periodFrames;
checkFill = false;
}
// Measure the Spice audio clock
int64_t curTime;
int64_t curPosition;
if (periodChanged)
{
if (init)
spiceData->nextTime = now;
curTime = spiceData->nextTime;
curPosition = spiceData->nextPosition;
spiceData->periodSec = (double) frames / audio.playback.sampleRate;
spiceData->nextTime += llrint(spiceData->periodSec * 1.0e9);
double bandwidth = 0.05;
double omega = 2.0 * M_PI * bandwidth * spiceData->periodSec;
spiceData->b = M_SQRT2 * omega;
spiceData->c = omega * omega;
}
else
{
double error = (now - spiceData->nextTime) * 1.0e-9;
if (fabs(error) >= 0.2)
{
// Clock error is too high; slew the write pointer and reset the timing
// parameters to avoid getting too far out of sync
int slewFrames = round(error * audio.playback.sampleRate);
ringbuffer_append(audio.playback.buffer, NULL, slewFrames);
curTime = now;
curPosition = spiceData->nextPosition + slewFrames;
spiceData->periodSec = (double) frames / audio.playback.sampleRate;
spiceData->nextTime = now + llrint(spiceData->periodSec * 1.0e9);
spiceData->nextPosition = curPosition;
}
else
{
curTime = spiceData->nextTime;
curPosition = spiceData->nextPosition;
spiceData->nextTime +=
llrint((spiceData->b * error + spiceData->periodSec) * 1.0e9);
spiceData->periodSec += spiceData->c * error;
}
}
// Measure the offset between the Spice position and the device position,
// and how far away this is from the target latency. We use this to adjust
// the playback speed to bring them back in line. This value can change
// quite rapidly, particularly at the start of playback, so filter it to
// avoid sudden pitch shifts which will be noticeable to the user.
double offsetError = spiceData->offsetError;
if (spiceData->devLastTime != INT64_MIN)
{
// Interpolate to calculate the current device position
double devPosition = spiceData->devLastPosition +
(spiceData->devNextPosition - spiceData->devLastPosition) *
((double) (curTime - spiceData->devLastTime) /
(spiceData->devNextTime - spiceData->devLastTime));
double actualOffset = curPosition - devPosition;
double actualOffsetError = -(actualOffset - audio.playback.targetLatencyFrames);
double error = actualOffsetError - offsetError;
spiceData->offsetError += spiceData->b * error +
spiceData->offsetErrorIntegral;
spiceData->offsetErrorIntegral += spiceData->c * error;
}
// Resample the audio to adjust the playback speed. Use a PI controller to
// adjust the resampling ratio based upon the measured offset
double kp = 0.5e-6;
double ki = 1.0e-16;
spiceData->ratioIntegral += offsetError * spiceData->periodSec;
double piOutput = kp * offsetError + ki * spiceData->ratioIntegral;
double ratio = 1.0 + piOutput;
int consumed = 0;
while (consumed < frames)
{
SRC_DATA srcData = {
.data_in = spiceData->framesIn + consumed * audio.playback.channels,
.data_out = spiceData->framesOut,
.input_frames = frames - consumed,
.output_frames = spiceData->framesOutSize,
.input_frames_used = 0,
.output_frames_gen = 0,
.end_of_input = 0,
.src_ratio = ratio
};
int error = src_process(spiceData->src, &srcData);
if (error)
{
DEBUG_ERROR("Resampling failed: %s", src_strerror(error));
return;
}
ringbuffer_append(audio.playback.buffer, spiceData->framesOut,
srcData.output_frames_gen);
consumed += srcData.input_frames_used;
spiceData->nextPosition += srcData.output_frames_gen;
}
if (audio.playback.state == STREAM_STATE_SETUP)
{
frames = ringbuffer_getCount(audio.playback.buffer);
if (frames >= max(devData->periodFrames,
ringbuffer_getLength(audio.playback.buffer) / 20))
{
audio.playback.state = STREAM_STATE_RUN;
audio.audioDev->playback.start();
}
}
// re-arm the buffer fill check if we have buffered enough
if (!checkFill && ringbuffer_getCount(audio.playback.buffer) >=
audio.playback.targetLatencyFrames)
checkFill = true;
}
bool audio_supportsRecord(void)
{
return audio.audioDev && audio.audioDev->record.start;
}
static void recordPushFrames(uint8_t * data, int frames)
{
purespice_writeAudio(data, frames * audio.record.stride, 0);
}
void audio_recordStart(int channels, int sampleRate, PSAudioFormat format)
{
if (!audio.audioDev)
return;
static int lastChannels = 0;
static int lastSampleRate = 0;
if (audio.record.started)
{
if (channels != lastChannels || sampleRate != lastSampleRate)
audio.audioDev->record.stop();
else
return;
}
lastChannels = channels;
lastSampleRate = sampleRate;
audio.record.started = true;
audio.record.stride = channels * sizeof(uint16_t);
audio.audioDev->record.start(channels, sampleRate, recordPushFrames);
// if a volume level was stored, set it before we return
if (audio.record.volumeChannels)
audio.audioDev->record.volume(
audio.playback.volumeChannels,
audio.playback.volume);
// set the inital mute state
if (audio.audioDev->record.mute)
audio.audioDev->record.mute(audio.playback.mute);
}
void audio_recordStop(void)
{
if (!audio.audioDev || !audio.record.started)
return;
audio.audioDev->record.stop();
audio.record.started = false;
}
void audio_recordVolume(int channels, const uint16_t volume[])
{
if (!audio.audioDev || !audio.audioDev->record.volume)
return;
// store the values so we can restore the state if the stream is restarted
channels = min(ARRAY_LENGTH(audio.record.volume), channels);
memcpy(audio.record.volume, volume, sizeof(uint16_t) * channels);
audio.record.volumeChannels = channels;
if (!audio.record.started)
return;
audio.audioDev->record.volume(channels, volume);
}
void audio_recordMute(bool mute)
{
if (!audio.audioDev || !audio.audioDev->record.mute)
return;
// store the value so we can restore it if the stream is restarted
audio.record.mute = mute;
if (!audio.record.started)
return;
audio.audioDev->record.mute(mute);
}
void audio_tick(unsigned long long tickCount)
{
LG_LOCK(audio.playback.lock);
if (!audio.playback.buffer)
{
LG_UNLOCK(audio.playback.lock);
return;
}
int frames = ringbuffer_getCount(audio.playback.buffer);
if (audio.audioDev->playback.latency)
frames += audio.audioDev->playback.latency();
const float latency = frames / (float)(audio.playback.sampleRate / 1000);
ringbuffer_push(audio.playback.timings, &latency);
LG_UNLOCK(audio.playback.lock);
app_invalidateGraphs();
}