From 0cf525b6d5a672959846dbeb516c63845a9e2ae7 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 18 Dec 2024 13:17:29 +0400 Subject: [PATCH] Get exact playback device time / capture latency. --- al/source.cpp | 28 +++++++++++++ alc/alc.cpp | 2 +- alc/backends/base.h | 4 ++ alc/backends/wasapi.cpp | 93 ++++++++++++++++++++++++++++++++++++++++- alc/export_list.h | 2 + include/AL/alext.h | 5 +++ 6 files changed, 131 insertions(+), 3 deletions(-) diff --git a/al/source.cpp b/al/source.cpp index 81a809e69e..be3cea244c 100644 --- a/al/source.cpp +++ b/al/source.cpp @@ -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, @@ -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: @@ -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; @@ -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; @@ -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; @@ -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", @@ -2252,6 +2260,26 @@ NOINLINE void GetProperty(ALsource *const Source, ALCcontext *const Context, con } break; + case AL_SAMPLE_OFFSET_CLOCK_EXACT_SOFT: + if constexpr(std::is_same_v) + { + CheckSize(3); + /* Get the source offset with the clock time first. Then get the clock + * time with the device latency. Order is important. + */ + ClockLatency clocktime{}; + nanoseconds srcclock{}; + values[0] = GetSourceSampleOffset(Source, Context, &srcclock); + { + std::lock_guard _{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) { diff --git a/alc/alc.cpp b/alc/alc.cpp index 26ce923384..0aeda21d88 100644 --- a/alc/alc.cpp +++ b/alc/alc.cpp @@ -2483,7 +2483,7 @@ ALC_API void ALC_APIENTRY alcGetInteger64vSOFT(ALCdevice *device, ALCenum pname, return; } const auto valuespan = al::span{values, static_cast(size)}; - if(!dev || dev->Type == DeviceType::Capture) + if(!dev || (dev->Type == DeviceType::Capture && pname != ALC_DEVICE_LATENCY_SOFT)) { auto ivals = std::vector(valuespan.size()); if(size_t got{GetIntegerv(dev.get(), pname, ivals)}) diff --git a/alc/backends/base.h b/alc/backends/base.h index 0e82bb6b7f..abf4c2ef36 100644 --- a/alc/backends/base.h +++ b/alc/backends/base.h @@ -20,6 +20,7 @@ using uint = unsigned int; struct ClockLatency { std::chrono::nanoseconds ClockTime; std::chrono::nanoseconds Latency; + std::chrono::nanoseconds ExactDeviceTime; }; struct BackendBase { @@ -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; } diff --git a/alc/backends/wasapi.cpp b/alc/backends/wasapi.cpp index 11cd998aba..184d32901f 100644 --- a/alc/backends/wasapi.cpp +++ b/alc/backends/wasapi.cpp @@ -1177,6 +1177,7 @@ struct WasapiPlayback final : public BackendBase, WasapiProxy { struct PlainDevice { ComPtr mClient{nullptr}; ComPtr mRender{nullptr}; + ComPtr mClock{nullptr}; }; struct SpatialDevice { ComPtr mClient{nullptr}; @@ -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)) { @@ -2126,6 +2134,7 @@ HRESULT WasapiPlayback::startProxy() } catch(...) { ERR("Failed to start thread\n"); + audio.mClock = nullptr; audio.mClient->Stop(); hr = E_FAIL; } @@ -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(); @@ -2187,6 +2196,19 @@ ClockLatency WasapiPlayback::getClockLatency() { std::lock_guard 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; @@ -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 mClient{nullptr}; @@ -2232,6 +2258,10 @@ struct WasapiCapture final : public BackendBase, WasapiProxy { std::atomic mKillNow{true}; std::thread mThread; + + std::atomic mLatency100ns{0}; + std::size_t mReadsCount{0}; + double mQueryPerformanceMultiplier = 0.; }; WasapiCapture::~WasapiCapture() @@ -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() { @@ -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); @@ -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", @@ -2770,6 +2839,26 @@ void WasapiCapture::captureSamples(std::byte *buffer, uint samples) uint WasapiCapture::availableSamples() { return static_cast(mRing->readSpace()); } +ClockLatency WasapiCapture::getClockLatency() +{ + ClockLatency ret; + + uint refcount; + do { + refcount = mDevice->waitForMix(); + ret.ClockTime = mDevice->getClockTime(); + std::atomic_thread_fence(std::memory_order_acquire); + } while(refcount != mDevice->mMixCount.load(std::memory_order_relaxed)); + + /* 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 diff --git a/alc/export_list.h b/alc/export_list.h index b83f2c38a3..17fb39435e 100644 --- a/alc/export_list.h +++ b/alc/export_list.h @@ -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), diff --git a/include/AL/alext.h b/include/AL/alext.h index 3c479e0bfb..a08d6ae966 100644 --- a/include/AL/alext.h +++ b/include/AL/alext.h @@ -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;