Skip to content

Commit

Permalink
Get exact playback device time / capture latency.
Browse files Browse the repository at this point in the history
  • Loading branch information
john-preston committed Dec 18, 2024
1 parent 90191ed commit 5e3a306
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 3 deletions.
27 changes: 27 additions & 0 deletions al/source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,9 @@ enum SourceProp : ALenum {
srcStereoMode = AL_STEREO_MODE_SOFT,
srcSuperStereoWidth = AL_SUPER_STEREO_WIDTH_SOFT,

/* ALC_SOFT_device_clock_exact */
srcSampleOffsetClockExactSOFT = AL_SAMPLE_OFFSET_CLOCK_EXACT_SOFT,

/* AL_SOFT_buffer_sub_data */
srcByteRWOffsetsSOFT = AL_BYTE_RW_OFFSETS_SOFT,
srcSampleRWOffsetsSOFT = AL_SAMPLE_RW_OFFSETS_SOFT,
Expand Down Expand Up @@ -1098,6 +1101,7 @@ constexpr ALuint IntValsByProp(ALenum prop)

case AL_SAMPLE_OFFSET_LATENCY_SOFT:
case AL_SAMPLE_OFFSET_CLOCK_SOFT:
case AL_SAMPLE_OFFSET_CLOCK_EXACT_SOFT:
case AL_STEREO_ANGLES:
break; /* i64 only */
case AL_SEC_OFFSET_LATENCY_SOFT:
Expand Down Expand Up @@ -1166,6 +1170,7 @@ constexpr ALuint Int64ValsByProp(ALenum prop)

case AL_SAMPLE_OFFSET_LATENCY_SOFT:
case AL_SAMPLE_OFFSET_CLOCK_SOFT:
case AL_SAMPLE_OFFSET_CLOCK_EXACT_SOFT:
case AL_STEREO_ANGLES:
return 2;

Expand Down Expand Up @@ -1259,6 +1264,7 @@ constexpr ALuint FloatValsByProp(ALenum prop)
break; /* i/i64 only */
case AL_SAMPLE_OFFSET_LATENCY_SOFT:
case AL_SAMPLE_OFFSET_CLOCK_SOFT:
case AL_SAMPLE_OFFSET_CLOCK_EXACT_SOFT:
break; /* i64 only */
}
return 0;
Expand Down Expand Up @@ -1332,6 +1338,7 @@ constexpr ALuint DoubleValsByProp(ALenum prop)
break; /* i/i64 only */
case AL_SAMPLE_OFFSET_LATENCY_SOFT:
case AL_SAMPLE_OFFSET_CLOCK_SOFT:
case AL_SAMPLE_OFFSET_CLOCK_EXACT_SOFT:
break; /* i64 only */
}
return 0;
Expand Down Expand Up @@ -1457,6 +1464,7 @@ NOINLINE void SetProperty(ALsource *const Source, ALCcontext *const Context, con
case AL_SAMPLE_OFFSET_LATENCY_SOFT:
case AL_SEC_OFFSET_LATENCY_SOFT:
case AL_SAMPLE_OFFSET_CLOCK_SOFT:
case AL_SAMPLE_OFFSET_CLOCK_EXACT_SOFT:
case AL_SEC_OFFSET_CLOCK_SOFT:
/* Query only */
throw al::context_error{AL_INVALID_OPERATION, "Setting read-only source property 0x%04x",
Expand Down Expand Up @@ -2252,6 +2260,25 @@ NOINLINE void GetProperty(ALsource *const Source, ALCcontext *const Context, con
}
break;

case AL_SAMPLE_OFFSET_CLOCK_EXACT_SOFT:
if constexpr(std::is_same_v<T,int64_t>)
{
CheckSize(3);
/* Get the source offset with the clock time first. Then get the clock
* time with the device latency. Order is important.
*/
nanoseconds srcclock{};
values[0] = GetSourceSampleOffset(Source, Context, &srcclock);
{
std::lock_guard<std::mutex> _{device->StateLock};
clocktime = GetClockLatency(device, device->Backend.get());
}
values[1] = clocktime.ClockTime.count();
values[2] = clocktime.ExactDeviceTime.count();
return;
}
break;

case AL_SEC_OFFSET_LATENCY_SOFT:
if constexpr(std::is_same_v<T,double>)
{
Expand Down
2 changes: 1 addition & 1 deletion alc/alc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2483,7 +2483,7 @@ ALC_API void ALC_APIENTRY alcGetInteger64vSOFT(ALCdevice *device, ALCenum pname,
return;
}
const auto valuespan = al::span{values, static_cast<uint>(size)};
if(!dev || dev->Type == DeviceType::Capture)
if(!dev || (dev->Type == DeviceType::Capture && pname != ALC_DEVICE_LATENCY_SOFT))
{
auto ivals = std::vector<int>(valuespan.size());
if(size_t got{GetIntegerv(dev.get(), pname, ivals)})
Expand Down
4 changes: 4 additions & 0 deletions alc/backends/base.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ using uint = unsigned int;
struct ClockLatency {
std::chrono::nanoseconds ClockTime;
std::chrono::nanoseconds Latency;
std::chrono::nanoseconds ExactDeviceTime;
};

struct BackendBase {
Expand Down Expand Up @@ -67,6 +68,9 @@ inline ClockLatency GetClockLatency(DeviceBase *device, BackendBase *backend)
{
ClockLatency ret{backend->getClockLatency()};
ret.Latency += device->FixedLatency;
if (!ret.ExactDeviceTime.count()) {
ret.ExactDeviceTime = ret.ClockTime;
}
return ret;
}

Expand Down
93 changes: 91 additions & 2 deletions alc/backends/wasapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,7 @@ struct WasapiPlayback final : public BackendBase, WasapiProxy {
struct PlainDevice {
ComPtr<IAudioClient> mClient{nullptr};
ComPtr<IAudioRenderClient> mRender{nullptr};
ComPtr<IAudioClock> mClock{nullptr};
};
struct SpatialDevice {
ComPtr<ISpatialAudioClient> mClient{nullptr};
Expand Down Expand Up @@ -2057,6 +2058,13 @@ HRESULT WasapiPlayback::resetProxy()
return hr;
}

hr = audio.mClient->GetService(__uuidof(IAudioClock), al::out_ptr(audio.mClock));
if(FAILED(hr))
{
ERR("Failed to get IAudioClock: 0x%08lx\n", hr);
return hr;
}

hr = audio.mClient->GetService(__uuidof(IAudioRenderClient), al::out_ptr(audio.mRender));
if(FAILED(hr))
{
Expand Down Expand Up @@ -2126,6 +2134,7 @@ HRESULT WasapiPlayback::startProxy()
}
catch(...) {
ERR("Failed to start thread\n");
audio.mClock = nullptr;
audio.mClient->Stop();
hr = E_FAIL;
}
Expand Down Expand Up @@ -2173,7 +2182,7 @@ void WasapiPlayback::stopProxy()
mThread.join();

auto stop_plain = [](PlainDevice &audio) -> void
{ audio.mClient->Stop(); };
{ audio.mClock = nullptr; audio.mClient->Stop(); };
auto stop_spatial = [](SpatialDevice &audio) -> void
{
audio.mRender->Stop();
Expand All @@ -2187,6 +2196,19 @@ ClockLatency WasapiPlayback::getClockLatency()
{
std::lock_guard<std::mutex> dlock{mMutex};
ClockLatency ret{};

std::visit(overloaded{[&](PlainDevice &audio) {
if (audio.mClock) {
UINT64 pos = 0;
UINT64 freq = 1;
audio.mClock->GetPosition(&pos, nullptr);
audio.mClock->GetFrequency(&freq);
ret.ExactDeviceTime = std::chrono::nanoseconds{
std::int64_t(std::round(double(pos) / freq * 1'000'000'000.))
};
}
}, [](SpatialDevice &audio) -> void {}, mAudio);

ret.ClockTime = mDevice->getClockTime();
ret.Latency = seconds{mPadding.load(std::memory_order_relaxed)};
ret.Latency /= mFormat.Format.nSamplesPerSec;
Expand Down Expand Up @@ -2217,9 +2239,13 @@ struct WasapiCapture final : public BackendBase, WasapiProxy {
void stop() override;
void stopProxy() override;

ClockLatency getClockLatency() override;

void captureSamples(std::byte *buffer, uint samples) override;
uint availableSamples() override;

void updateLatency(DWORD flags, UINT64 counter);

HRESULT mOpenStatus{E_FAIL};
DeviceHandle mMMDev{nullptr};
ComPtr<IAudioClient> mClient{nullptr};
Expand All @@ -2232,6 +2258,10 @@ struct WasapiCapture final : public BackendBase, WasapiProxy {

std::atomic<bool> mKillNow{true};
std::thread mThread;

std::atomic<int> mLatency100ns{0};
std::size_t mReadsCount{0};
double mQueryPerformanceMultiplier = 0.;
};

WasapiCapture::~WasapiCapture()
Expand All @@ -2245,6 +2275,34 @@ WasapiCapture::~WasapiCapture()
mNotifyEvent = nullptr;
}

void WasapiCapture::updateLatency(DWORD flags, UINT64 counter) {
const auto counterDelta = [&] {
if ((flags & AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY)
|| (flags & AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR)
|| !(mReadsCount++ % 100)) {
return 0.;
}
LARGE_INTEGER counterValue;
QueryPerformanceCounter(&counterValue);
const auto wasCounter = double(counter);
const auto nowCounter = (mQueryPerformanceMultiplier > 0.)
? (mQueryPerformanceMultiplier * counterValue.QuadPart)
: 0.;
const auto result = (nowCounter - wasCounter);
constexpr auto kBadDelayMs = 200;
if (result < 0. || result > 10'000. * kBadDelayMs) {
WARN("Bad WASAPI latency %lf", result);
return 0.;
}
return result;
}();
const auto queued = mRing->readSpace();

const auto deviceFrequencyMultiplier = 10'000'000. / mDevice->Frequency;
const auto fullDelay = counterDelta
+ (queued * deviceFrequencyMultiplier);
mLatency100ns = int(std::round(fullDelay));
}

FORCE_ALIGN int WasapiCapture::recordProc()
{
Expand All @@ -2270,12 +2328,16 @@ FORCE_ALIGN int WasapiCapture::recordProc()
UINT32 numsamples;
DWORD flags;
BYTE *rdata;
UINT64 position = 0;
UINT64 counter = 0;

hr = mCapture->GetBuffer(&rdata, &numsamples, &flags, nullptr, nullptr);
hr = mCapture->GetBuffer(&rdata, &numsamples, &flags, &position, &counter);
if(FAILED(hr))
ERR("Failed to get capture buffer: 0x%08lx\n", hr);
else
{
updateLatency(flags, counter);

if(mChannelConv.is_active())
{
samples.resize(numsamples*2_uz);
Expand Down Expand Up @@ -2356,6 +2418,13 @@ void WasapiCapture::open(std::string_view name)
"Failed to create notify events"};
}

// Query performance frequency.
LARGE_INTEGER counterFrequency{};
QueryPerformanceFrequency(&counterFrequency);
if (counterFrequency.QuadPart) {
mQueryPerformanceMultiplier = 10'000'000. / counterFrequency.QuadPart;
}

mOpenStatus = pushMessage(MsgType::OpenDevice, name).get();
if(FAILED(mOpenStatus))
throw al::backend_exception{al::backend_error::DeviceError, "Device init failed: 0x%08lx",
Expand Down Expand Up @@ -2770,6 +2839,26 @@ void WasapiCapture::captureSamples(std::byte *buffer, uint samples)
uint WasapiCapture::availableSamples()
{ return static_cast<uint>(mRing->readSpace()); }

ClockLatency WasapiCapture::getClockLatency()
{
ClockLatency ret;

uint refcount;
do {
refcount = mDevice->waitForMix();
ret.ClockTime = GetDeviceClockTime(mDevice);
std::atomic_thread_fence(std::memory_order_acquire);
} while(refcount != ReadRef(mDevice->MixCount));

/* NOTE: The device will generally have about all but one periods filled at
* any given time during playback. Without a more accurate measurement from
* the output, this is an okay approximation.
*/
ret.Latency = std::chrono::nanoseconds{ 100LL * mLatency100ns.load() };

return ret;
}

} // namespace


Expand Down
2 changes: 2 additions & 0 deletions alc/export_list.h
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,8 @@ inline const EnumExport alcEnumerations[]{
DECL(AL_SAMPLE_OFFSET_CLOCK_SOFT),
DECL(AL_SEC_OFFSET_CLOCK_SOFT),

DECL(AL_SAMPLE_OFFSET_CLOCK_EXACT_SOFT),

DECL(ALC_OUTPUT_MODE_SOFT),
DECL(ALC_ANY_SOFT),
DECL(ALC_STEREO_BASIC_SOFT),
Expand Down
5 changes: 5 additions & 0 deletions include/AL/alext.h
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,11 @@ AL_API void AL_APIENTRY alGetBufferPtrvSOFT(ALuint buffer, ALenum param, ALvoid
#define ALC_SURROUND_7_1_SOFT 0x1506
#endif

#ifndef ALC_SOFT_device_clock_exact
#define ALC_SOFT_device_clock_exact 1
#define AL_SAMPLE_OFFSET_CLOCK_EXACT_SOFT 0x1215
#endif

#ifndef AL_SOFT_source_start_delay
#define AL_SOFT_source_start_delay
typedef void (AL_APIENTRY*LPALSOURCEPLAYATTIMESOFT)(ALuint source, ALint64SOFT start_time) AL_API_NOEXCEPT17;
Expand Down

0 comments on commit 5e3a306

Please sign in to comment.