summaryrefslogtreecommitdiff
path: root/audio/tests/KSPosTst/pintest.cpp
blob: be6598966134aa7920f1c438ab12c8a839629de3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
// ------------------------------------------------------------------------------
//
// Copyright (C) Microsoft Corporation. All rights reserved.
//
// Module Name:
//
//  pintest.cpp
//
// Abstract:
//
//  Implementation file for test cases
//
// -------------------------------------------------------------------------------

#include "PreComp.h"
#include "KsPosTestTaef.h"
#include "tests.h"

#include <bestfit.h>
#include <cmath>

#define DEFAULT_PERIODICITY 0
#define DEFAULT_LATENCY_COEFFICIENT 0
#define xGetTime    g_pKsPosTst->m_pTimer->Proc

// ------------------------------------
// Test_DriftAndJitter helper functions
// ------------------------------------

HRESULT CollectSampleData
(
    CHalfApp* pHalfApp,
    PFORMAT_ENTRY pFormatEntry,
    HNSTIME requestedPeriodicity
)
{
    DWORD           periodicityInMs = (DWORD)ceil(HNSTIME_TO_MILLISECONDS_DOUBLE(requestedPeriodicity));

    if (!pFormatEntry)
    {
        ERR(g_pBasicLog, "FAIL: Current Format Entry is NULL.");
        return E_INVALIDARG;
    }

    PPERF_RESULTS pResults = &pFormatEntry->perfResults;
    PPOSITION_SET pPosSet = pResults->argPosSets;
    double        tStart = 0, tPre = 0, tPost = 0;
    UINT          i = 0;
    UINT64        u64Position;
    UINT64        positionHNS;
    UINT64        u64Frequency;
    LARGE_INTEGER liNow;
    PLONGLONG     frequency = &g_pKsPosTst->m_qpcFrequency;
    PLONGLONG     pBaseHns = &g_pKsPosTst->m_testBaseHns;
    CRtThreadRegistration* threadPriority = &g_pKsPosTst->m_threadPriority;

    // Register with MMCSS
    threadPriority->RegisterWithMMCSS();

    VERIFY_SUCCEEDED(pHalfApp->InitializeEndpoint());
    VERIFY_SUCCEEDED(pHalfApp->CreateSineToneDataBuffer(pHalfApp->m_pCurrentFormat.get()));
    VERIFY_SUCCEEDED(pHalfApp->InitializeStream(requestedPeriodicity, DEFAULT_LATENCY_COEFFICIENT));

    // run the adapter sink pin
    VERIFY_SUCCEEDED(pHalfApp->StartStream());

    tStart = xGetTime();
    Sleep(DELAY_FOR_STABILIZE);
    // Get the stream frequency in order to determine the time offset
    VERIFY_SUCCEEDED(pHalfApp->m_pAudioClock->GetFrequency(&u64Frequency));

    // Start collecting the data
    for (i = 0; i < DATA_POINTS;)
    {
        Sleep(periodicityInMs);

        tPre = xGetTime();
        VERIFY_SUCCEEDED(pHalfApp->GetPosition(&u64Position, &positionHNS));
        QueryPerformanceCounter(&liNow);
        tPost = xGetTime();

        if (u64Position > 0)
        {
            // only count this result if the GetPosition and GetTime happened relatively atomically
            if ((tPost - tPre) < 1.)
            {
                // Convert QPC to microseconds and then HNS. This is to prevent overflows from converting directly to HNS.
                UINT64 testHNS = liNow.QuadPart * MICROSECONDS_PER_SECOND / *frequency;
                testHNS *= HNSTIME_UNITS_PER_MICROSECOND;
                // In both sampling rate and jitter calculation, the time used is in milliseconds, so we can convert it right here
                pPosSet[i].testTime = HNSTIME_TO_MILLISECONDS_DOUBLE(testHNS - *pBaseHns);
                pPosSet[i].positionTime = HNSTIME_TO_MILLISECONDS_DOUBLE(positionHNS - *pBaseHns);
                // We store the values as seconds because it is easier to convert to bytes per second, on sampling rate calculation, and milliseconds, on jitter calculation.
                pPosSet[i].position = (double)u64Position / (double)u64Frequency;

                // Ignore repeated positions (that means they have the same QPC and should have the same position value)
                if ((i > 0) && (pPosSet[i - 1].positionTime == pPosSet[i].positionTime))
                {
                    // If the position is the same, decrement the count so that sample gets discarded by overwriting it
                    i--;
                    WARN(g_pBasicLog, "The same position was reported at %f ms: pos = %f seconds", pPosSet[i].testTime, pPosSet[i].position);
                }
                i++;
            }
            else
            {
                LOG(g_pBasicLog, "GetPosition function too slow");
            }
        }
        // If a non-zero position has not been reported during one second, break the loop and check if there is enough data
        else if ((tPost - tStart) > MILLISECONDS_PER_SECOND)
        {
            break;
        }

        pResults->cPosSets = i;
    }

    VERIFY_SUCCEEDED(pHalfApp->StopStream());

    // Unregister with MMCSS
    threadPriority->UnRegisterWithMMCSS();

    //  Did we get enough data?
    VERIFY_IS_TRUE(pResults->cPosSets >= 30);

    return S_OK;
}

// --------------------------------------------------------------------------------------------------
// CalculateSampleRate
// --------------------------------------------------------------------------------------------------
HRESULT CalculateSampleRate
(
    PFORMAT_ENTRY pFormatEntry
)
{
    WAVEFORMATEX* pwfx = &pFormatEntry->wfxFormat.Format;
    PPERF_RESULTS   pResults = &pFormatEntry->perfResults;

    double dError, dErrorPercent;

    // Pair the positions with test HNS time to find the slope
    // Alocate memory for this type-specific array
    wil::unique_cotaskmem_ptr<DPOINT> pointArray((PDPOINT)CoTaskMemAlloc(DATA_POINTS * sizeof(DPOINT)));
    PDPOINT pArray = pointArray.get();
    // Get a pointer to the sample set
    PPOSITION_SET posSet = pResults->argPosSets;
    for (int i = 0; i < DATA_POINTS; i++)
    {
        pArray[i].x = posSet[i].positionTime;
        pArray[i].y = posSet[i].position * pwfx->nAvgBytesPerSec; // Convert to bytes
    }

    ULONG   cArray = pResults->cPosSets; // number of points

    // calculate the slope and intercept (bytes per second and latency)
    pResults->bytesPerSecond = SLOPE(pArray, cArray);
    pResults->dOffset = INTERCEPT(pArray, cArray, pResults->bytesPerSecond);

    // Convert from bytes/ms to bytes/sec
    pResults->bytesPerSecond *= MILLISECONDS_PER_SECOND;

    // The offset is used in the jitter calculation and it is used in milliseconds, so we can save it in those units
    pResults->dOffset = pResults->dOffset / pResults->bytesPerSecond * MILLISECONDS_PER_SECOND;

    // calculate errors
    dError = pResults->bytesPerSecond - (double)pwfx->nAvgBytesPerSec;
    dErrorPercent = (dError / (double)pwfx->nAvgBytesPerSec) * 100.0;

    if (g_pKsPosTst->m_fLogHistograms)
    {
        LOG(g_pBasicLog, "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
        LOG(g_pBasicLog, "SampleRate calculated from histogram (method of least squares)\n");

        LOG(g_pBasicLog, "Collected %d data points", pResults->cPosSets);
        LOG(g_pBasicLog, "Specified sample rate  = %6.3f bytes/sec, %6.3f samples/sec", (double)(pwfx->nBlockAlign * pwfx->nSamplesPerSec), (double)pwfx->nSamplesPerSec);
        LOG(g_pBasicLog, "Calculated sample rate = %6.3f bytes/sec, %6.3f samples/sec", pResults->bytesPerSecond, pResults->bytesPerSecond / (double)pwfx->nBlockAlign);
        LOG(g_pBasicLog, "Error = %.3f %%", dErrorPercent);
        LOG(g_pBasicLog, "Calculated time offset = %f ms", pResults->dOffset);
    }

    // evaluate the results
    VERIFY_IS_TRUE(abs(dErrorPercent) <= SAMPLERATE_THRESHOLD);

    return S_OK;
}

// --------------------------------------------------------------------------------------------------
// CalculateJitter
// --------------------------------------------------------------------------------------------------
HRESULT CalculateJitter
(
    PFORMAT_ENTRY pFormatEntry
)
{
    PPERF_RESULTS   pResults = &pFormatEntry->perfResults;

    double          dError        = 0.0;
    double          dErrorAve     = 0.0;
    double          dErrorMax     = 0.0;
    double          dStdDeviation = 0.0;
    double          reportedPosition;
    double          timeDifference;
    ULONG           i;
    PPOSITION_SET   pPosSet = pResults->argPosSets;

    if (g_pKsPosTst->m_fLogHistograms)
    {
        LOG(g_pBasicLog, "Reported    QPC         Compensated Expected");
        LOG(g_pBasicLog, "position    adjust      position    position    Error");
        LOG(g_pBasicLog, "(ms)        (ms)        (ms)        (ms)        (ms)");
        LOG(g_pBasicLog, "==========  ==========  ==========  ==========  ==========");
    }

    for (i = 0; i < pResults->cPosSets; i++)
    {
        // Convert the time offset from seconds to milliseconds
        reportedPosition = pPosSet[i].position * MILLISECONDS_PER_SECOND; // milliseconds

        // First, we compensate for the latency of the samples (they were taken 100 ms after stream start)
        pPosSet[i].position = reportedPosition - pResults->dOffset; // The offset is already in ms

        // Then, before comparing the expected and actual position, adjust the given sample to get the position corresponding to the test time.
        timeDifference = pPosSet[i].testTime - pPosSet[i].positionTime; // milliseconds
        // If the difference is positive, the time of the position is behind of what is expected and that position gets increased accordingly.
        // If the difference is negative, the time of the position is ahead of what is expected and that position gets decreased accordingly.
        // In the rare case when the QPC and time are the same, the difference is zero because we are on the spot and that is the position that should be compared.
        pPosSet[i].position += timeDifference; // milliseconds

        dError = pPosSet[i].testTime - pPosSet[i].position;
        dErrorAve += dError;

        if (g_pKsPosTst->m_fLogHistograms)
        {
            LOG(g_pBasicLog, "%10.2f, %10.2f, %10.2f, %10.2f, %10.2f", reportedPosition, timeDifference, pPosSet[i].position, pPosSet[i].testTime, dError);
        }

        if (dError < 0.)
            dError = -dError;
        if (dError > dErrorMax)
            dErrorMax = dError;
    }

    dErrorAve /= (double)pResults->cPosSets;

    VERIFY_IS_TRUE((ULONG)dErrorAve < 2);      // if this is not true then, there's something wrong with our compensation...

    for (i = 0; i < pResults->cPosSets; i++)
    {
        dError = pPosSet[i].testTime - pPosSet[i].position;
        dStdDeviation += ((dError - dErrorAve) * (dError - dErrorAve));
    }

    dStdDeviation /= (double)pResults->cPosSets;
    dStdDeviation = sqrt(dStdDeviation);

    if (g_pKsPosTst->m_fLogHistograms)
    {
        LOG(g_pBasicLog, "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
        LOG(g_pBasicLog, "Jitter calculated from histogram (with (samplerate drift and latency) compensation)\n");
        LOG(g_pBasicLog, "Max deviation      = %6.3f ms", dErrorMax);
        LOG(g_pBasicLog, "Standard deviation = %6.3f ms", dStdDeviation);
    }

    // evaluate the results ~~~~~~~~~~~~~
    VERIFY_IS_TRUE(abs(dErrorMax) < JITTER_MAX_THRESHOLD);
    VERIFY_IS_TRUE(abs(dStdDeviation) < JITTER_STD_THRESHOLD);

    return S_OK;
}

// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
void Test_DriftAndJitter_Main
(
    HNSTIME requestedPeriodicity
)
{
    CHalfApp* pHalfApp = g_pKsPosTst->m_pHalf;
    CHalfApp* pHostHalfApp = g_pKsPosTst->m_pHostHalf;
    BOOL      isLoopbackPin = pHalfApp->m_ConnectorType == eLoopbackConnector;

    // Test will ignore Bluetooth devices until proper thresholds are in place.
    if (pHalfApp->m_bIsBluetooth)
    {
        SKIP(g_pBasicLog, "Test is not supported for Bluetooth pins");
        return;
    }

    // If this is a loobpack pin, start the host pin's stream
    // This can be done before collecting the samples, since the host pin will keep playing until stopped
    if (isLoopbackPin)
    {
        // Match loopback stream format with host stream format
        CloneWaveFormat(pHostHalfApp->m_pCurrentFormat.get(), wil::out_param(pHalfApp->m_pCurrentFormat));
        pHalfApp->m_u32CurrentDefaultPeriodicityInFrames = pHostHalfApp->m_u32CurrentDefaultPeriodicityInFrames;
        pHalfApp->m_u32CurrentFundamentalPeriodicityInFrames = pHostHalfApp->m_u32CurrentFundamentalPeriodicityInFrames;
        pHalfApp->m_u32CurrentMinPeriodicityInFrames = pHostHalfApp->m_u32CurrentMinPeriodicityInFrames;
        pHalfApp->m_u32CurrentMaxPeriodicityInFrames = pHostHalfApp->m_u32CurrentMaxPeriodicityInFrames;

        // Initialize and start host stream
        VERIFY_SUCCEEDED(pHostHalfApp->InitializeEndpoint());
        VERIFY_SUCCEEDED(pHostHalfApp->CreateSineToneDataBuffer(pHostHalfApp->m_pCurrentFormat.get()));
        VERIFY_SUCCEEDED(pHostHalfApp->InitializeStream(requestedPeriodicity, DEFAULT_LATENCY_COEFFICIENT));
        VERIFY_SUCCEEDED(pHostHalfApp->StartStream());
    }

    FORMAT_ENTRY results;
    memset(&results, 0, sizeof(FORMAT_ENTRY));
    results.wfxFormat.Format = *pHalfApp->m_pCurrentFormat.get();
    // Alocate memory for the sample data
    wil::unique_cotaskmem_ptr<POSITION_SET> pPositionSet((PPOSITION_SET)CoTaskMemAlloc(DATA_POINTS * sizeof(POSITION_SET)));
    results.perfResults.argPosSets = pPositionSet.get();

    // collect the data ~~~~~~~~~~~~~~~~~
    IF_FAILED_JUMP(CollectSampleData(pHalfApp, &results, requestedPeriodicity), Log);

    // Stop and cleanup the host stream since we are done collecting samples
    if (isLoopbackPin)
    {
        VERIFY_SUCCEEDED(pHostHalfApp->StopStream());
        VERIFY_SUCCEEDED(pHostHalfApp->CleanupStream());
        VERIFY_SUCCEEDED(pHostHalfApp->ReleaseEndpoint());
        VERIFY_SUCCEEDED(pHostHalfApp->ReleaseSineToneDataBuffer());
    }

    // calculate drift ~~~~~~~~~~~~~~~~~~
    IF_FAILED_JUMP(CalculateSampleRate(&results), Log);

    // calculate Jitter ~~~~~~~~~~~~~~~~~
    VERIFY_SUCCEEDED(CalculateJitter(&results));

    return;

Log:
    // If the drift test or the collection of samples fail,
    // there will be no logging of data (from inside the jitter test).
    // So, log whatever available values here.
    if (g_pKsPosTst->m_fLogHistograms)
    {
        PERF_RESULTS* data = &results.perfResults;
        LOG(g_pBasicLog, "Report      Reported    Position");
        LOG(g_pBasicLog, "time        position    timestamp");
        LOG(g_pBasicLog, "(ms)        (s)         (ms)");
        LOG(g_pBasicLog, "==========  ==========  ==========");
        for (UINT i = 0; i < data->cPosSets; i++)
        {
            LOG(g_pBasicLog, "%10.2f, %10.2f, %10.2f", data->argPosSets[i].testTime, data->argPosSets[i].position, data->argPosSets[i].positionTime);
        }
    }

    ERR(g_pBasicLog, "Drift and jitter test failed");
}

// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
void Test_DriftAndJitter_Default(void)
{
    HNSTIME requestedTime = FRAMES_TO_HNSTIME_DOUBLE(g_pKsPosTst->m_pHalf->m_u32CurrentDefaultPeriodicityInFrames, g_pKsPosTst->m_pHalf->m_pCurrentFormat->nSamplesPerSec);
    if (g_pKsPosTst->m_fLogHistograms)
    {
        LOG(g_pBasicLog, "Periodicity: %d frames, %f ms", g_pKsPosTst->m_pHalf->m_u32CurrentDefaultPeriodicityInFrames, HNSTIME_TO_MILLISECONDS_DOUBLE(requestedTime));
    }
    Test_DriftAndJitter_Main(requestedTime);
}

/*
  Perform a cycle of creating a pin, starting a stream, stoping and destroying.

  This is done with the idea of putting the driver and audio hardware in an active state
  before doing time sensitive tests.
*/
void ActivateDriver(CHalfApp* pHalfApp)
{
    VERIFY_SUCCEEDED(pHalfApp->InitializeEndpoint());
    VERIFY_SUCCEEDED(pHalfApp->CreateSineToneDataBuffer(pHalfApp->m_pCurrentFormat.get()));
    VERIFY_SUCCEEDED(pHalfApp->InitializeStream(DEFAULT_PERIODICITY, DEFAULT_LATENCY_COEFFICIENT));
    VERIFY_SUCCEEDED(pHalfApp->StartStream());
    Sleep(100);
    VERIFY_SUCCEEDED(pHalfApp->StopStream());
    VERIFY_SUCCEEDED(pHalfApp->CleanupStream());
    VERIFY_SUCCEEDED(pHalfApp->ReleaseEndpoint());
    VERIFY_SUCCEEDED(pHalfApp->ReleaseSineToneDataBuffer());
}

// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
void Test_Latency_Main (HNSTIME requestedPeriodicity)
{
    CHalfApp* pHalfApp = g_pKsPosTst->m_pHalf;
    CHalfApp* pHostHalfApp = g_pKsPosTst->m_pHostHalf;
    NTSTATUS  lTimerResStatus;
    BOOL      isLoopbackPin = pHalfApp->m_ConnectorType == eLoopbackConnector;
    PLONGLONG qpcFrequency = &g_pKsPosTst->m_qpcFrequency;
    CRtThreadRegistration* threadPriority = &g_pKsPosTst->m_threadPriority;

    // Test will ignore Bluetooth devices until proper thresholds are in place.
    if (pHalfApp->m_bIsBluetooth)
    {
        SKIP(g_pBasicLog, "Test is not supported for Bluetooth pins");
        return;
    }

    lTimerResStatus = NtSetTimerResolution(timerResolution, TRUE, &actualTimerResolution);

    if (!NT_SUCCESS(lTimerResStatus))
    {
        SKIP(g_pBasicLog, "Fail to set timer precision. Skip the test.");
        return;
    }

    // Register with MMCSS
    threadPriority->RegisterWithMMCSS();

    // If it is a loopback pin, get the host pin and start the stream
    if (isLoopbackPin)
    {
        // Match loopback stream format with host stream format
        CloneWaveFormat(pHostHalfApp->m_pCurrentFormat.get(), wil::out_param(pHalfApp->m_pCurrentFormat));
        pHalfApp->m_u32CurrentDefaultPeriodicityInFrames = pHostHalfApp->m_u32CurrentDefaultPeriodicityInFrames;
        pHalfApp->m_u32CurrentFundamentalPeriodicityInFrames = pHostHalfApp->m_u32CurrentFundamentalPeriodicityInFrames;
        pHalfApp->m_u32CurrentMinPeriodicityInFrames = pHostHalfApp->m_u32CurrentMinPeriodicityInFrames;
        pHalfApp->m_u32CurrentMaxPeriodicityInFrames = pHostHalfApp->m_u32CurrentMaxPeriodicityInFrames;

        // Initialize and start host stream
        VERIFY_SUCCEEDED(pHostHalfApp->InitializeEndpoint());
        VERIFY_SUCCEEDED(pHostHalfApp->CreateSineToneDataBuffer(pHostHalfApp->m_pCurrentFormat.get()));
        VERIFY_SUCCEEDED(pHostHalfApp->InitializeStream(requestedPeriodicity, DEFAULT_LATENCY_COEFFICIENT));
        VERIFY_SUCCEEDED(pHostHalfApp->StartStream());
    }

    // Perform a stream cycle to make sure the driver is on a good state
    ActivateDriver(pHalfApp);

    // Initialize the stream
    VERIFY_SUCCEEDED(pHalfApp->InitializeEndpoint());
    VERIFY_SUCCEEDED(pHalfApp->CreateSineToneDataBuffer(pHalfApp->m_pCurrentFormat.get()));
    VERIFY_SUCCEEDED(pHalfApp->InitializeStream(requestedPeriodicity, DEFAULT_LATENCY_COEFFICIENT));

    UINT64 u64Position = 0;
    UINT64 driverHns = 0;

    double tPosPre = 0.0;
    double tPosPost = 0.0;

    LARGE_INTEGER timeStamp = { 0 };

    // mark the time
    double tRunPre = xGetTime();
    // Start the stream
    VERIFY_SUCCEEDED(pHalfApp->StartStream());
    // mark the time
    double tRunPost = xGetTime();

    LOG(g_pBasicLog, "Starting the stream took %.03f ms", (tRunPost - tRunPre));

    // loop until we get a non-zero position
    while (u64Position == 0)
    {
        Sleep(1);
        // timer before position call
        tPosPre = xGetTime();
        VERIFY_SUCCEEDED(pHalfApp->GetPosition(&u64Position, &driverHns));
        QueryPerformanceCounter(&timeStamp);
        // timer after position call
        tPosPost = xGetTime();
        if (g_pKsPosTst->m_fLogHistograms)
        {
            LOG(g_pBasicLog, "Testing first non-zero position reported: %llu samples, %llu HNS at %.03f ms", u64Position, driverHns, tPosPost - tRunPre);
        }

        // Warn if the cursor does not move within 500 ms. Do not fail the test as this might be because the test overslept.
        if ((tPosPost - tRunPost) >= 500.0)
        {
            WARN(g_pBasicLog, "Pin did not report a position in 500 ms");
            break;
        }
    }

    // Before stopping, get the frequency of the stream
    UINT64 u64Frequency;
    VERIFY_SUCCEEDED(pHalfApp->m_pAudioClock->GetFrequency(&u64Frequency));

    // Stop the endpoint
    VERIFY_SUCCEEDED(pHalfApp->StopStream());

    // Unregister with MMCSS
    threadPriority->UnRegisterWithMMCSS();

    // the uncertainty in time is the max possible value minus the min possible value divided by 2 (for +/- purposes)
    double tMax = tPosPost - tRunPre;
    double tMin = tPosPre - tRunPost;
    double tAve = (tMax + tMin) / 2.0;
    double tUncertainty = (tMax - tMin) / 2.0;

    // Convert the stream position to an offset of milliseconds
    double tOffset = MILLISECONDS_PER_SECOND * (double)u64Position / (double)u64Frequency;

    // Calculate the difference in time of the position time versus the test time
    // Convert QPC to microseconds and then HNS. This is to prevent overflows from converting directly to HNS.
    UINT64 testHns = MICROSECONDS_PER_SECOND * timeStamp.QuadPart / *qpcFrequency;
    testHns *= HNSTIME_UNITS_PER_MICROSECOND;

    // Make the HNS times smaller to avoid overflows (or underflows)
    PLONGLONG pBaseHns = &g_pKsPosTst->m_testBaseHns;
    INT64 adjustedTestHns = (INT64)(testHns - *pBaseHns);
    INT64 adjustedDriverHns = (INT64)(driverHns - *pBaseHns);

    INT64 timeDiff = adjustedTestHns - adjustedDriverHns;

    // Compensate the positon to take this difference into account
    tOffset += (double)timeDiff / HNSTIME_UNITS_PER_MILLISECOND;

    if (g_pKsPosTst->m_fLogHistograms)
    {
        LOG(g_pBasicLog, "Base test time: %lld HNS", *pBaseHns);
        LOG(g_pBasicLog, "Times (HNS): %llu, %llu", testHns, driverHns);
        LOG(g_pBasicLog, "Time difference: %lld HNS (%f ms)", timeDiff, (double)timeDiff / HNSTIME_UNITS_PER_MILLISECOND);
        LOG(g_pBasicLog, "First non-zero position = %llu at %g ms", u64Position, tAve);
        LOG(g_pBasicLog, "Time offset / max time: %f ms / %f ms", tOffset, tMax);
        LOG(g_pBasicLog, "Frequency: %llu", u64Frequency);
    }

    double           tLatency;
    tLatency = tAve - tOffset;
    if (tLatency < 0)
    {
        // This means that the position is between the average and max times, which should be OK (as long as it is not above the max)
        tLatency *= -1;
        LOG(g_pBasicLog, "The position is greater than the average time");
    }
    LOG(g_pBasicLog, "Adjusted latency                 = %.03f ms (+/- %.3f ms)", tLatency, tUncertainty);

    // In case the position reported by the pin (tOffset) is ahead of the run time of the pin (tMax),
    // assume that position might have jitter and apply the propper limits instead of failing the test.
    if (tOffset > tMax + JITTER_STD_THRESHOLD)
    {
        WARN(g_pBasicLog, "Pin's time offset is greater than the time it has been in the RUN state. This will become an error in the future.");
        return;
    }

    // Verify that the latency is within the threshold
    if (tLatency > LATENCY_THRESHOLD)
    {
        WARN(g_pBasicLog, "The latency exceeds the acceptable threshold. This will become an error in the future.");
        return;
    }

    // If this was a loopback pin, stop the host stream also
    if (isLoopbackPin)
    {
        VERIFY_SUCCEEDED(pHostHalfApp->StopStream());
        VERIFY_SUCCEEDED(pHostHalfApp->CleanupStream());
        VERIFY_SUCCEEDED(pHostHalfApp->ReleaseEndpoint());
        VERIFY_SUCCEEDED(pHostHalfApp->ReleaseSineToneDataBuffer());
    }

    // Turn off the higher resolution timer
    NtSetTimerResolution(0, FALSE, &actualTimerResolution);
}

// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
void Test_Latency_Default(void)
{
    HNSTIME requestedTime = FRAMES_TO_HNSTIME_DOUBLE(g_pKsPosTst->m_pHalf->m_u32CurrentDefaultPeriodicityInFrames, g_pKsPosTst->m_pHalf->m_pCurrentFormat->nSamplesPerSec);
    LOG(g_pBasicLog, "Periodicity: %d frames, %f ms", g_pKsPosTst->m_pHalf->m_u32CurrentDefaultPeriodicityInFrames, HNSTIME_TO_MILLISECONDS_DOUBLE(requestedTime));
    Test_Latency_Main(requestedTime);
}