Skip to content

Instantly share code, notes, and snippets.

@MikuAuahDark
Created January 6, 2021 16:34
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MikuAuahDark/6a605c74df9b8f602f4adec203cec41d to your computer and use it in GitHub Desktop.
Save MikuAuahDark/6a605c74df9b8f602f4adec203cec41d to your computer and use it in GitHub Desktop.
Simple peak meter with ANSI escape codes written in C++ using WASAPI audio loopback as audio measurement.
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <atomic>
#include <chrono>
#include <thread>
#include <type_traits>
#include <windows.h>
#include <combaseapi.h>
#include <audioclient.h>
#include <mmdeviceapi.h>
#include "capture.hpp"
static struct GlobalVars
{
std::thread audioThread;
IMMDeviceEnumerator *enumerator;
IMMDevice *device;
IAudioClient *audioClient;
IAudioCaptureClient *capture;
WAVEFORMATEX *waveFormat;
captureFunc_t func;
REFERENCE_TIME actualDuration;
size_t bufferFrameCount;
bool comInitialized;
std::atomic<bool> captureStarted;
std::atomic<bool> done;
} g = {};
const char *startCapture(captureInfoFunc_t info, captureFunc_t captureFunc)
{
constexpr REFERENCE_TIME requestedDuration = 200000;
constexpr CLSID CLSID_MMDeviceEnumerator = {0xBCDE0395, 0xE52F, 0x467C, {0x8E, 0x3D, 0xC4, 0x57, 0x92, 0x91, 0x69, 0x2E}};
constexpr IID IID_IMMDeviceEnumerator = {0xA95664D2, 0x9614, 0x4F35, {0xA7, 0x46, 0xDE, 0x8D, 0xB6, 0x36, 0x17, 0xE6}};
constexpr IID IID_IAudioClient = {0x1CB9AD4C, 0xDBFA, 0x4c32, {0xB1, 0x78, 0xC2, 0xF5, 0x68, 0xA7, 0x03, 0xB2}};
constexpr IID IID_IAudioCaptureClient = {0xC8ADBD64, 0xE71E, 0x48A0, {0xA4, 0xDE, 0x18, 0x5C, 0x39, 0x5C, 0xD3, 0x17}};
if (g.captureStarted)
return "Capture already started";
if (!(g.comInitialized = SUCCEEDED(CoInitializeEx(nullptr, COINIT_MULTITHREADED))))
return "Unable to initialize COM";
if (FAILED(CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL, IID_IMMDeviceEnumerator, (void **) &g.enumerator)))
{
stopCapture();
return "Unable to load device enumerator";
}
if (FAILED(g.enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &g.device)))
{
stopCapture();
return "Unable to get default render audio endpoint";
}
if (FAILED(g.device->Activate(IID_IAudioClient, CLSCTX_ALL, nullptr, (void **) &g.audioClient)))
{
stopCapture();
return "Unable to activate audio client";
}
if (FAILED(g.audioClient->GetMixFormat(&g.waveFormat)))
{
stopCapture();
return "Unable to get mix format";
}
if (FAILED(g.audioClient->Initialize(
AUDCLNT_SHAREMODE_SHARED,
AUDCLNT_STREAMFLAGS_LOOPBACK,
requestedDuration,
0, g.waveFormat,
nullptr)))
{
stopCapture();
return "Unable to initialize audio client";
}
UINT32 bufferFrameCount;
if (FAILED(g.audioClient->GetBufferSize(&bufferFrameCount)))
{
stopCapture();
return "Unable to get buffer size";
}
g.bufferFrameCount = (size_t) bufferFrameCount;
if (FAILED(g.audioClient->GetService(IID_IAudioCaptureClient, (void **) &g.capture)))
{
stopCapture();
return "Unable to get capture service";
}
info((int) g.waveFormat->nChannels, (int) g.waveFormat->wBitsPerSample, (size_t) g.waveFormat->nSamplesPerSec);
g.actualDuration = 10000000.0 * bufferFrameCount / g.waveFormat->nSamplesPerSec;
if (FAILED(g.audioClient->Start()))
{
stopCapture();
return "Unable to start audio capture";
}
g.captureStarted = true;
g.done = false;
g.func = captureFunc;
g.audioThread = std::thread([]() {
while (g.captureStarted)
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
UINT32 packetLength;
HRESULT hr = g.capture->GetNextPacketSize(&packetLength);
while (SUCCEEDED(hr) && packetLength != 0)
{
BYTE *data;
UINT32 frameCount;
DWORD flags;
hr = g.capture->GetBuffer(&data, &frameCount, &flags, nullptr, nullptr);
if (SUCCEEDED(hr))
{
g.func((void *) ((flags & AUDCLNT_BUFFERFLAGS_SILENT) ? nullptr : data), (size_t) frameCount);
g.capture->ReleaseBuffer(frameCount);
}
else
fprintf(stderr, "GetBuffer HRESULT %p\n", (void *) (uint32_t) hr);
hr = g.capture->GetNextPacketSize(&packetLength);
}
}
g.done = true;
});
return nullptr;
}
template<class T> void releaseObject(T *&unk)
{
if (unk)
{
unk->Release();
unk = nullptr;
}
}
void stopCapture()
{
if (g.captureStarted)
{
g.captureStarted = false;
g.audioThread.join();
g.audioClient->Stop();
}
CoTaskMemFree(g.waveFormat);
g.waveFormat = nullptr;
releaseObject(g.enumerator);
releaseObject(g.device);
releaseObject(g.audioClient);
releaseObject(g.capture);
}
bool isCapturing()
{
return g.captureStarted;
}
#ifndef _SIMPLE_CAPTURE_H_
#define _SIMPLE_CAPTURE_H_
#include <cstdint>
#include <functional>
// sampleData, sampleCount
typedef std::function<void(void *, size_t)> captureFunc_t;
// channels, bitsPerSample, sampleRate
typedef std::function<void(int, int, size_t)> captureInfoFunc_t;
const char *startCapture(captureInfoFunc_t info, captureFunc_t captureFunc);
void stopCapture();
bool isCapturing();
#endif
// clang++ -D_CRT_SECURE_NO_WARNINGS main.cpp capture.cpp -lOle32
// Code may be dirty, it's only proof-of-concept.
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <csignal>
#include <cstdio>
#include <cstdint>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <new>
#include <thread>
#include <windows.h>
#include "capture.hpp"
std::atomic<bool> stopCapturing(false);
int channels, bps;
size_t sampleRate, written = 0;
FILE *output;
inline void writeLE(char *buf, uint16_t value)
{
buf[0] = (char) (value & 0xFF);
buf[1] = (char) ((value >> 8) & 0xFF);
}
inline void writeLE(char *buf, uint32_t value)
{
buf[0] = (char) (value & 0xFF);
buf[1] = (char) ((value >> 8) & 0xFF);
buf[2] = (char) ((value >> 16) & 0xFF);
buf[3] = (char) ((value >> 24) & 0xFF);
}
static void captureInfo(int c, int b, size_t s)
{
char temp[16];
channels = c;
bps = b;
sampleRate = s;
fprintf(stderr, "Capture information:\nChannels: %d\nBits/Sample: %d\nSample Rate: %zu\n", c, b, s);
// Write WAVE header
fwrite("RIFF\0\0\0\0WAVEfmt \x10\0\0\0", 1, 20, output);
// Synthesize header
writeLE(temp, (uint16_t) 1); // Type = PCM
writeLE(temp + 2, (uint16_t) c); // Channels
writeLE(temp + 4, (uint32_t) s); // Sample Rate
writeLE(temp + 8, (uint32_t) (s * c * b / 8)); // Byte rate
writeLE(temp + 12, (uint16_t) (c * b / 8)); // Single frame size
writeLE(temp + 14, (uint16_t) b); // BPS
// Write header and start of data chunk
fwrite(temp, 1, 16, output);
fwrite("data\0\0\0\0", 1, 8, output);
}
static void captureCallback(void *buf, size_t frames)
{
constexpr size_t MAXCHAN = 16;
static bool firstTime = true;
static char *tempSilence = nullptr;
static size_t tempSilenceSize = 0;
static double peak[MAXCHAN];
void *actualBuf = buf;
size_t size = frames * channels * bps / 8;
if (actualBuf == nullptr || bps > 16)
{
// Silence
if (tempSilenceSize != frames)
{
delete[] tempSilence;
tempSilence = new (std::nothrow) char[size];
tempSilenceSize = frames;
if (actualBuf == nullptr)
std::fill(tempSilence, tempSilence + size, '\0');
}
if (actualBuf)
{
switch (bps)
{
case 32:
{
int32_t *buffer = (int32_t *) tempSilence;
// Float to int
for (size_t i = 0; i < size / sizeof(float); i++)
buffer[i] = (int32_t) (((float *) buf)[i] * 2147483647.0);
break;
}
case 64:
{
int64_t *buffer = (int64_t *) tempSilence;
// Double to int
for (size_t i = 0; i < size / sizeof(double); i++)
buffer[i] = (int64_t) (((double *) buf)[i] * 9223372036854775807.0);
break;
}
}
}
actualBuf = tempSilence;
}
written += fwrite(actualBuf, 1, size, output);
size_t maxloop = std::min((size_t) channels, MAXCHAN);
for (size_t j = 0; j < maxloop; peak[j++] = 0.0);
switch (bps)
{
case 8:
{
int8_t *pcm = (int8_t *) actualBuf;
for (size_t i = 0; i < frames; i++)
{
for (size_t j = 0; j < maxloop; j++)
peak[j] += abs(pcm[i * channels + j] / 127.0);
}
break;
}
case 16:
{
int16_t *pcm = (int16_t *) actualBuf;
for (size_t i = 0; i < frames; i++)
{
for (size_t j = 0; j < maxloop; j++)
peak[j] += abs(pcm[i * channels + j] / 32767.0);
}
break;
}
case 32:
{
float *pcm = (float *) buf;
for (size_t i = 0; i < frames; i++)
{
for (size_t j = 0; j < maxloop; j++)
peak[j] += abs(pcm[i * channels + j]);
}
break;
}
}
// Get console width
static CONSOLE_SCREEN_BUFFER_INFO csbi;
static char *printer = nullptr;
static int lastWidth = 0;
GetConsoleScreenBufferInfo(GetStdHandle(-12), &csbi);
int width = csbi.srWindow.Right - csbi.srWindow.Left + 1;
if (printer == nullptr || width != lastWidth)
{
delete[] printer;
printer = new char[width + 12]; // + ESC[XX;YYm + ESC[0m
lastWidth = width;
}
if (!firstTime)
// Restore current cursor position
fprintf(stderr, "\x1b[%zuA", maxloop);
firstTime = false;
for (size_t i = 0; i < maxloop; i++)
{
constexpr const char *white = "\x1b[37;47m";
constexpr const char *reset = "\x1b[0m";
int printerWidth = (int) (peak[i] * width / frames + 0.5);
std::fill(printer, printer + width + 12, ' ');
std::copy(white, white + 8, printer);
std::copy(reset, reset + 4, printer + 8 + printerWidth);
fwrite(printer, 1, width + 12, stderr);
}
fputs("\n", stderr);
fflush(stderr);
}
static void interruptHandler(int s)
{
(void) s;
stopCapturing = true;
}
int main(int argc, char *argv[])
{
HANDLE stderrConsole = GetStdHandle(-12);
DWORD cmode;
GetConsoleMode(stderrConsole, &cmode);
/*
if (!SetConsoleMode(stderrConsole, cmode | ENABLE_VIRTUAL_TERMINAL_PROCESSING))
{
fputs("Cannot enable ANSI codes. You're running at least Windows 10 1703 right?\n", stderr);
return 1;
}
*/
if (argc < 2)
{
fprintf(stderr, "Usage: %s <output>\n", argv[0]);
return 1;
}
output = fopen(argv[1], "wb");
if (output == nullptr)
{
perror("Cannot open file: ");
return 1;
}
setvbuf(output, nullptr, _IONBF, 0);
setvbuf(stderr, nullptr, _IONBF, 0);
const char *err = startCapture(captureInfo, captureCallback);
if (err)
{
fprintf(stderr, "Error: %s\n", err);
return 1;
}
signal(SIGINT, interruptHandler);
while (!stopCapturing)
std::this_thread::sleep_for(std::chrono::milliseconds(50));
// Stop
stopCapture();
// Write length header
char tempBuf[4];
fseek(output, 4, SEEK_SET);
writeLE(tempBuf, (uint32_t) (written + 32));
fseek(output, 40, SEEK_SET);
writeLE(tempBuf, (uint32_t) written);
fwrite(tempBuf, 1, 4, output);
fclose(output);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment