Created
January 6, 2021 16:34
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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