灵符lingfu
L

ling-sys

↓ 0 downloads

System 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).

Windows · x86-64macOS · Intel x86-64Linux · x86-64LingOS · x86-64LingOS · Pi 5 ARM64Web · WASM
lingfu install ling-sys
Authorsling-lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / 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++
}

← Back to index