L
ling-sys
↓ 0 downloadsSystem activity and performance monitor in the Ling Design System — dials, meshes, drums and crosshatch for CPU, GPU, memory, disk, network, board, processes and audio, with every subsystem given a voice. A LingOS app for rpi5: live board/CPU/memory/process stats from the kernel (stats-lingos.ling).
lingfu install ling-sysCode / Files (13)
collector-audio.ps1
# LINGSYS audio collector. Reads two things about ~12 times a second and writes
# them to audio.txt (atomically) so the AUDIO tab moves like a real VU meter:
# out the default speakers' peak, from the Windows mixer's endpoint meter
# in the default microphone's peak. The mixer only meters a capture device
# while some app is recording, so this opens its OWN short capture
# stream (winmm waveIn) and keeps only the loudest sample of each ~64 ms
# block. Nothing is recorded, stored or sent: the audio is discarded as
# it is measured. It is switched off by writing "0" to mic.cfg (the MIC
# switch in the app's CONTROLS panel), which also releases the device.
# mic 1 = stream open, 0 = switched off, -1 = Windows refused it (see the
# "Microphone access" privacy setting)
# Exits when the app's heartbeat goes stale.
param([string]$OutDir = $PSScriptRoot)
$ErrorActionPreference = 'SilentlyContinue'
$outPath = Join-Path $OutDir 'audio.txt'
$tmpPath = Join-Path $OutDir 'audio.tmp'
$beatPath = Join-Path $OutDir 'app.beat'
$micCfg = Join-Path $OutDir 'mic.cfg'
$enc = New-Object System.Text.UTF8Encoding $false
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
[Guid("C02216F6-8C67-4B5B-9D00-D008E73E0064"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IAudioMeterInformation { int GetPeakValue(out float pfPeak); }
[Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IAudioEndpointVolume {
int RegisterControlChangeNotify(IntPtr n);
int UnregisterControlChangeNotify(IntPtr n);
int GetChannelCount(out uint c);
int SetMasterVolumeLevel(float f, Guid g);
int SetMasterVolumeLevelScalar(float f, Guid g);
int GetMasterVolumeLevel(out float f);
int GetMasterVolumeLevelScalar(out float f);
}
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDevice { int Activate(ref Guid iid, int clsCtx, IntPtr p, [MarshalAs(UnmanagedType.IUnknown)] out object o); }
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDeviceEnumerator {
int EnumAudioEndpoints(int df, int mask, out IntPtr devices);
int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice endpoint);
}
[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] class MMDeviceEnumeratorComObject { }
public class LingAudioFast {
static IAudioMeterInformation _render;
static IAudioMeterInformation _capture;
static IAudioEndpointVolume _vol;
public static void Open() {
var en = (IMMDeviceEnumerator)new MMDeviceEnumeratorComObject();
IMMDevice dev; object o;
var mid = typeof(IAudioMeterInformation).GUID;
en.GetDefaultAudioEndpoint(0, 1, out dev);
dev.Activate(ref mid, 1, IntPtr.Zero, out o);
_render = (IAudioMeterInformation)o;
var vid = typeof(IAudioEndpointVolume).GUID;
dev.Activate(ref vid, 23, IntPtr.Zero, out o);
_vol = (IAudioEndpointVolume)o;
try {
en.GetDefaultAudioEndpoint(1, 1, out dev);
dev.Activate(ref mid, 1, IntPtr.Zero, out o);
_capture = (IAudioMeterInformation)o;
} catch { _capture = null; }
}
public static float Out() { float p; _render.GetPeakValue(out p); return p; }
public static float In() { if (_capture == null) return 0f; float p; _capture.GetPeakValue(out p); return p; }
public static float Vol() { float v; _vol.GetMasterVolumeLevelScalar(out v); return v; }
}
// A level-only microphone tap: 4 x 1024 samples at 16 kHz mono, polled.
public class LingMic {
[StructLayout(LayoutKind.Sequential)]
struct WAVEFORMATEX { public ushort wFormatTag, nChannels; public uint nSamplesPerSec, nAvgBytesPerSec; public ushort nBlockAlign, wBitsPerSample, cbSize; }
[StructLayout(LayoutKind.Sequential)]
struct WAVEHDR { public IntPtr lpData; public uint dwBufferLength, dwBytesRecorded; public IntPtr dwUser; public uint dwFlags, dwLoops; public IntPtr lpNext; public IntPtr reserved; }
[DllImport("winmm.dll")] static extern int waveInOpen(out IntPtr h, uint dev, ref WAVEFORMATEX fmt, IntPtr cb, IntPtr inst, uint flags);
[DllImport("winmm.dll")] static extern int waveInPrepareHeader(IntPtr h, IntPtr hdr, int size);
[DllImport("winmm.dll")] static extern int waveInUnprepareHeader(IntPtr h, IntPtr hdr, int size);
[DllImport("winmm.dll")] static extern int waveInAddBuffer(IntPtr h, IntPtr hdr, int size);
[DllImport("winmm.dll")] static extern int waveInStart(IntPtr h);
[DllImport("winmm.dll")] static extern int waveInReset(IntPtr h);
[DllImport("winmm.dll")] static extern int waveInClose(IntPtr h);
const int N = 4, SAMPLES = 1024;
static IntPtr hwi = IntPtr.Zero;
static IntPtr[] hdr = new IntPtr[N];
static IntPtr[] buf = new IntPtr[N];
static float last = 0f;
static int hs = Marshal.SizeOf(typeof(WAVEHDR));
public static int Open() {
if (hwi != IntPtr.Zero) return 0;
var f = new WAVEFORMATEX();
f.wFormatTag = 1; f.nChannels = 1; f.nSamplesPerSec = 16000; f.nAvgBytesPerSec = 32000;
f.nBlockAlign = 2; f.wBitsPerSample = 16; f.cbSize = 0;
IntPtr h;
int r = waveInOpen(out h, 0xFFFFFFFF, ref f, IntPtr.Zero, IntPtr.Zero, 0);
if (r != 0) return r;
hwi = h;
for (int i = 0; i < N; i++) {
buf[i] = Marshal.AllocHGlobal(SAMPLES * 2);
hdr[i] = Marshal.AllocHGlobal(hs);
var w = new WAVEHDR();
w.lpData = buf[i]; w.dwBufferLength = (uint)(SAMPLES * 2);
Marshal.StructureToPtr(w, hdr[i], false);
waveInPrepareHeader(hwi, hdr[i], hs);
waveInAddBuffer(hwi, hdr[i], hs);
}
return waveInStart(hwi);
}
// The loudest sample (0..1) across the blocks finished since the last call;
// the previous value when none finished yet.
public static float Peak() {
if (hwi == IntPtr.Zero) return 0f;
int best = -1;
for (int i = 0; i < N; i++) {
var w = (WAVEHDR)Marshal.PtrToStructure(hdr[i], typeof(WAVEHDR));
if ((w.dwFlags & 1) == 0) continue; // WHDR_DONE
int n = (int)(w.dwBytesRecorded / 2);
for (int k = 0; k < n; k++) {
int s = Math.Abs((int)Marshal.ReadInt16(buf[i], k * 2));
if (s > best) best = s;
}
waveInAddBuffer(hwi, hdr[i], hs); // hand it back
}
if (best >= 0) last = Math.Min(1f, best / 32768f);
return last;
}
public static void Close() {
if (hwi == IntPtr.Zero) return;
waveInReset(hwi);
for (int i = 0; i < N; i++) {
waveInUnprepareHeader(hwi, hdr[i], hs);
Marshal.FreeHGlobal(buf[i]); Marshal.FreeHGlobal(hdr[i]);
}
waveInClose(hwi);
hwi = IntPtr.Zero; last = 0f;
}
}
'@
function Quit { try { [LingMic]::Close() } catch {}; exit }
try { [LingAudioFast]::Open() } catch { exit }
$startedAt = Get-Date
$beatSeen = $false
$micOn = $false
$micState = 0
$i = 0
while ($true) {
if (($i % 25) -eq 0) {
# A beat left over from an earlier session proves nothing: only one
# written since this collector started means the app is up.
$bt = if (Test-Path $beatPath) { (Get-Item $beatPath).LastWriteTime } else { $null }
if ($bt -and $bt -ge $startedAt) { $beatSeen = $true }
if ($beatSeen -and $bt) {
if (((Get-Date) - $bt).TotalSeconds -gt 25) { Quit }
} elseif ($beatSeen) {
Quit
} elseif (((Get-Date) - $startedAt).TotalSeconds -gt 90) {
Quit
}
# endpoints can change; reopen now and then
try { [LingAudioFast]::Open() } catch {}
# the microphone follows the MIC switch (default: on)
$want = $true
if (Test-Path $micCfg) { $want = ((Get-Content $micCfg -Raw).Trim() -ne '0') }
if ($want -and -not $micOn) {
$r = 1
try { $r = [LingMic]::Open() } catch {}
if ($r -eq 0) { $micOn = $true; $micState = 1 } else { $micState = -1 }
} elseif (-not $want -and ($micOn -or $micState -ne 0)) {
try { [LingMic]::Close() } catch {}
$micOn = $false; $micState = 0
}
}
$o = 0; $n = 0; $v = 0
try {
$o = [math]::Round([LingAudioFast]::Out() * 100, 1)
$e = [LingAudioFast]::In()
$m = 0
if ($micOn) { $m = [LingMic]::Peak() }
$n = [math]::Round([math]::Max($e, $m) * 100, 1)
$v = [math]::Round([LingAudioFast]::Vol() * 100)
} catch {}
[System.IO.File]::WriteAllText($tmpPath, "out=$o`nin=$n`nvol=$v`nmic=$micState`n", $enc)
Move-Item -Force $tmpPath $outPath
Start-Sleep -Milliseconds 80
$i++
}