import Foundation /// Alle Tonquellen **eines** Programms in einer Zeile. /// /// Chrome gibt seinen Ton über Hilfsprozesse aus — einen je Tab. In der Liste /// standen deshalb „Google Chrome" und „Google Chrome Helper" nebeneinander, /// und zwar mit getrennten Reglern für dasselbe Programm: zieht man den einen, /// hört man den anderen. Fünfzehn Regler für einen Browser sind keine Auskunft, /// sondern eine Zumutung. public struct AudioProcessGroup: Identifiable, Equatable, Sendable { /// Die Kennung des **Programms**, nicht die des Prozesses. Sie ist der /// Schlüssel für Lautstärke, Stummschaltung und Auswahl. public let bundleID: String public let name: String public let processes: [AudioProcess] public var id: String { bundleID } /// Spielt einer, spielt die Gruppe. /// /// Der Ton kommt aus dem Helfer, während der Hauptprozess schweigt — die /// Zeile muss trotzdem als spielend gelten, sonst verschwindet sie. public var isPlaying: Bool { processes.contains { $0.isPlaying } } public static func group(_ processes: [AudioProcess]) -> [AudioProcessGroup] { let byApp = Dictionary(grouping: processes) { ProcessNaming.parentBundleID(for: $0.bundleID) } return byApp.map { bundleID, members in // Der Name kommt vom Programm selbst, wenn es dabei ist. Sonst vom // Helfer — dann ist er alles, was da ist. let name = members.first { $0.bundleID == bundleID }?.name ?? ProcessNaming.fallbackName(for: bundleID) return AudioProcessGroup(bundleID: bundleID, name: name, processes: members.sorted { $0.pid < $1.pid }) } // Nach Namen sortiert: Core Audio sagt keine Reihenfolge zu, und ohne // feste Sortierung springen die Zeilen bei jeder Messung umher. .sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } } }