Fun with the endpoint volume interfaces - let's add metering...

Yesterday I posted a quick&dirty OSD (complete with cheesy text graphics).  Today I'm going to add support for metering to the application.

Start with yesterdays application and add (this is the routine that actually implements the meter):

 struct TimerContext 
{ 
    IAudioMeterInformation *_Meter; 
}; 

const int TimerPeriodicityMS = 100; 
void CALLBACK TimerMeterCallback(PTP_CALLBACK_INSTANCE CallbackInstance, PVOID Context, PTP_TIMER Timer) 
{ 
    TimerContext *timerContext = (TimerContext *)Context; 
    wchar_t outputString[256]; 
    float peakValue; 
    DWORD written; 
    COORD writeCoord; 
    StringCbCopy(outputString, sizeof(outputString), L"Meter: "); 

    timerContext->_Meter->GetPeakValue(&peakValue); 
    for (size_t i = 0 ; i < peakValue*100; i += 1) 
    { 
        StringCbCat(outputString, sizeof(outputString), L"*"); 
    } 
    for (size_t i = (size_t)(peakValue*100) ; i < 100; i += 1) 
    { 
        StringCbCat(outputString, sizeof(outputString), L"."); 
    } 
    writeCoord.X = 0; 
    writeCoord.Y = 1; 
    WriteConsoleOutputCharacter(GetStdHandle(STD_OUTPUT_HANDLE), outputString, (DWORD)wcslen(outputString), writeCoord, &written); 
} 

at the start of main(), add: 

     HANDLE      handle; 
    TP_CALLBACK_ENVIRON callbackEnvironment; 
    PTP_CLEANUP_GROUP cleanupGroup; 
    PTP_TIMER timer; 
    TimerContext context; 

    InitializeThreadpoolEnvironment(&callbackEnvironment); 
    cleanupGroup = CreateThreadpoolCleanupGroup(); 
    SetThreadpoolCallbackCleanupGroup(&callbackEnvironment, cleanupGroup, NULL); 

    timer = CreateThreadpoolTimer(TimerMeterCallback, &context, &callbackEnvironment); 

and just before the while() loop, add: (please note that the code that releases the default device came from yesterday's program.

     hr = defaultDevice->Activate(__uuidof(IAudioMeterInformation), CLSCTX_INPROC_SERVER, NULL, (LPVOID *)&context._Meter); 
    defaultDevice->Release(); 
    defaultDevice = NULL; 
    // Set a 100 millisecond timer. 
    LARGE_INTEGER timerPeriodicityLI; 
    FILETIME timerPeriodicity; 
    timerPeriodicityLI.QuadPart = -1*(TimerPeriodicityMS* 10000 ); 

    timerPeriodicity.dwLowDateTime = timerPeriodicityLI.LowPart; 
    timerPeriodicity.dwHighDateTime = timerPeriodicityLI.HighPart; 
             
    SetThreadpoolTimer(timer, &timerPeriodicity, TimerPeriodicityMS, 10); 

and finally, at the end of main(), add:

     CloseThreadpoolCleanupGroupMembers(cleanupGroup, FALSE, NULL); 
    CloseThreadpoolCleanupGroup(cleanupGroup); 
    cleanupGroup = NULL; 
    DestroyThreadpoolEnvironment(&callbackEnvironment); 

    context._Meter->Release(); 

that's it - now, if you compile the application, there will be a 2nd line which displays metering information about the sounds currently being played (it helps if your console window is more than 100 characters wide).

 

The meat of the code is the line ...Meter->GetPeakValue() - this retrieves the highest sample since the last time the API was called.  There are also multi channel versions of the API if you want to display per-channel bars :).

 

I'm also using the new Vista threadpool APIs, after using them for a while, I've grown to really like them - they allow significantly more control than the previous APIs did.

 

Some self criticism: Again, I've removed all error handling for clarity - this is NOT production ready code.  For instance the code that generates the meter is indescribably inefficient,  there is no question it could be considerably improved (not repainting every 10ms would be a good start as would not scanning through the string to append one character).