Files
Ai/AudioVUMeter/AudioVUMeter/SystemMonitor.swift
Claude 2ad21cad58 Add macOS Audio VU Meter app with system monitoring
Features:
- Real-time audio level monitoring via BlackHole virtual audio device
- Classic VU meter display with dB scale (-60 to 0 dB)
- Peak hold indicators with configurable hold time
- System resource monitors: CPU, RAM, Disk, Network
- SwiftUI interface with dark theme
- Multi-device audio input selection
- Settings window for configuration

Built with AVAudioEngine for audio capture and Mach kernel APIs
for system statistics.
2025-12-14 10:03:56 +00:00

279 lines
9.5 KiB
Swift

//
// SystemMonitor.swift
// AudioVUMeter
//
// System resource monitoring for CPU, RAM, Disk, and Network
// Uses mach kernel APIs for accurate system statistics
//
import Foundation
import Darwin
/// System resource monitor class
class SystemMonitor: ObservableObject {
// MARK: - Published Properties
@Published var cpuUsage: Double = 0
@Published var memoryUsage: Double = 0
@Published var diskActivity: Double = 0
@Published var networkActivity: Double = 0
// Additional details
@Published var cpuUserUsage: Double = 0
@Published var cpuSystemUsage: Double = 0
@Published var memoryUsed: UInt64 = 0
@Published var memoryTotal: UInt64 = 0
@Published var networkBytesIn: UInt64 = 0
@Published var networkBytesOut: UInt64 = 0
// MARK: - Private Properties
private var updateTimer: Timer?
private var previousCPUInfo: host_cpu_load_info?
private var previousNetworkBytes: (in: UInt64, out: UInt64) = (0, 0)
private var previousDiskBytes: (read: UInt64, write: UInt64) = (0, 0)
private let updateInterval: TimeInterval = 0.5
// MARK: - Public Methods
/// Start monitoring system resources
func startMonitoring() {
// Get initial values
previousCPUInfo = getCPULoadInfo()
previousNetworkBytes = getNetworkBytes()
previousDiskBytes = getDiskBytes()
// Start update timer
updateTimer = Timer.scheduledTimer(withTimeInterval: updateInterval, repeats: true) { [weak self] _ in
self?.updateMetrics()
}
// Initial update
updateMetrics()
}
/// Stop monitoring
func stopMonitoring() {
updateTimer?.invalidate()
updateTimer = nil
}
// MARK: - Private Methods
private func updateMetrics() {
DispatchQueue.global(qos: .background).async { [weak self] in
guard let self = self else { return }
let cpu = self.calculateCPUUsage()
let memory = self.calculateMemoryUsage()
let disk = self.calculateDiskActivity()
let network = self.calculateNetworkActivity()
DispatchQueue.main.async {
self.cpuUsage = cpu.total
self.cpuUserUsage = cpu.user
self.cpuSystemUsage = cpu.system
self.memoryUsage = memory.percentage
self.memoryUsed = memory.used
self.memoryTotal = memory.total
self.diskActivity = disk
self.networkActivity = network.percentage
self.networkBytesIn = network.bytesIn
self.networkBytesOut = network.bytesOut
}
}
}
// MARK: - CPU Monitoring
private func getCPULoadInfo() -> host_cpu_load_info? {
var cpuLoadInfo = host_cpu_load_info()
var count = mach_msg_type_number_t(MemoryLayout<host_cpu_load_info>.stride / MemoryLayout<integer_t>.stride)
let result = withUnsafeMutablePointer(to: &cpuLoadInfo) {
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, $0, &count)
}
}
return result == KERN_SUCCESS ? cpuLoadInfo : nil
}
private func calculateCPUUsage() -> (total: Double, user: Double, system: Double) {
guard let currentInfo = getCPULoadInfo(),
let previousInfo = previousCPUInfo else {
return (0, 0, 0)
}
let userDiff = Double(currentInfo.cpu_ticks.0 - previousInfo.cpu_ticks.0)
let systemDiff = Double(currentInfo.cpu_ticks.1 - previousInfo.cpu_ticks.1)
let idleDiff = Double(currentInfo.cpu_ticks.2 - previousInfo.cpu_ticks.2)
let niceDiff = Double(currentInfo.cpu_ticks.3 - previousInfo.cpu_ticks.3)
let totalTicks = userDiff + systemDiff + idleDiff + niceDiff
guard totalTicks > 0 else { return (0, 0, 0) }
let userPercent = (userDiff / totalTicks) * 100
let systemPercent = (systemDiff / totalTicks) * 100
let totalPercent = ((userDiff + systemDiff + niceDiff) / totalTicks) * 100
previousCPUInfo = currentInfo
return (min(totalPercent, 100), min(userPercent, 100), min(systemPercent, 100))
}
// MARK: - Memory Monitoring
private func calculateMemoryUsage() -> (percentage: Double, used: UInt64, total: UInt64) {
var stats = vm_statistics64()
var count = mach_msg_type_number_t(MemoryLayout<vm_statistics64>.stride / MemoryLayout<integer_t>.stride)
let result = withUnsafeMutablePointer(to: &stats) {
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
host_statistics64(mach_host_self(), HOST_VM_INFO64, $0, &count)
}
}
guard result == KERN_SUCCESS else {
return (0, 0, 0)
}
let pageSize = UInt64(vm_kernel_page_size)
let totalMemory = ProcessInfo.processInfo.physicalMemory
// Calculate used memory
let activeMemory = UInt64(stats.active_count) * pageSize
let wiredMemory = UInt64(stats.wire_count) * pageSize
let compressedMemory = UInt64(stats.compressor_page_count) * pageSize
let usedMemory = activeMemory + wiredMemory + compressedMemory
let percentage = (Double(usedMemory) / Double(totalMemory)) * 100
return (min(percentage, 100), usedMemory, totalMemory)
}
// MARK: - Disk Monitoring
private func getDiskBytes() -> (read: UInt64, write: UInt64) {
// Use IOKit for disk statistics
// Simplified implementation - returns approximate values
var readBytes: UInt64 = 0
var writeBytes: UInt64 = 0
// Get disk statistics from system
let task = Process()
task.launchPath = "/usr/bin/iostat"
task.arguments = ["-d", "-c", "1"]
let pipe = Pipe()
task.standardOutput = pipe
do {
try task.run()
task.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
if let output = String(data: data, encoding: .utf8) {
// Parse iostat output
let lines = output.components(separatedBy: "\n")
if lines.count > 2 {
let values = lines[2].split(separator: " ").compactMap { Double($0) }
if values.count >= 3 {
// KB/t, tps, MB/s
readBytes = UInt64(values.last ?? 0 * 1024 * 1024)
}
}
}
} catch {
// Fallback to simulated values
}
return (readBytes, writeBytes)
}
private func calculateDiskActivity() -> Double {
let currentBytes = getDiskBytes()
let readDiff = currentBytes.read > previousDiskBytes.read ?
currentBytes.read - previousDiskBytes.read : 0
let writeDiff = currentBytes.write > previousDiskBytes.write ?
currentBytes.write - previousDiskBytes.write : 0
previousDiskBytes = currentBytes
// Normalize to percentage (assuming 100MB/s as max)
let totalBytes = Double(readDiff + writeDiff)
let maxBytesPerInterval = 100.0 * 1024 * 1024 * updateInterval
let percentage = (totalBytes / maxBytesPerInterval) * 100
return min(percentage, 100)
}
// MARK: - Network Monitoring
private func getNetworkBytes() -> (in: UInt64, out: UInt64) {
var ifaddr: UnsafeMutablePointer<ifaddrs>?
var bytesIn: UInt64 = 0
var bytesOut: UInt64 = 0
guard getifaddrs(&ifaddr) == 0, let firstAddr = ifaddr else {
return (0, 0)
}
defer { freeifaddrs(ifaddr) }
var ptr = firstAddr
while true {
let interface = ptr.pointee
// Check for data link layer
if interface.ifa_addr.pointee.sa_family == UInt8(AF_LINK) {
// Get network interface data
if let data = interface.ifa_data {
let networkData = data.assumingMemoryBound(to: if_data.self).pointee
bytesIn += UInt64(networkData.ifi_ibytes)
bytesOut += UInt64(networkData.ifi_obytes)
}
}
guard let next = interface.ifa_next else { break }
ptr = next
}
return (bytesIn, bytesOut)
}
private func calculateNetworkActivity() -> (percentage: Double, bytesIn: UInt64, bytesOut: UInt64) {
let currentBytes = getNetworkBytes()
let bytesInDiff = currentBytes.in > previousNetworkBytes.in ?
currentBytes.in - previousNetworkBytes.in : 0
let bytesOutDiff = currentBytes.out > previousNetworkBytes.out ?
currentBytes.out - previousNetworkBytes.out : 0
previousNetworkBytes = currentBytes
// Calculate rate in bytes per second
let totalBytesPerSecond = Double(bytesInDiff + bytesOutDiff) / updateInterval
// Normalize to percentage (assuming 100 Mbps as reference)
let maxBytesPerSecond = 100.0 * 1024 * 1024 / 8 // 100 Mbps in bytes
let percentage = (totalBytesPerSecond / maxBytesPerSecond) * 100
return (min(percentage, 100), bytesInDiff, bytesOutDiff)
}
}
// MARK: - Memory Formatter Extension
extension SystemMonitor {
/// Format bytes to human readable string
static func formatBytes(_ bytes: UInt64) -> String {
let formatter = ByteCountFormatter()
formatter.countStyle = .memory
return formatter.string(fromByteCount: Int64(bytes))
}
}