// Spike C — Core Audio Process Taps auf M5 Pro / macOS 26 // // Fragen: // 1. Liefert kAudioHardwarePropertyProcessObjectList die Audio-Prozesse? // 2. Lässt sich pro Prozess ein Tap erzeugen (AudioHardwareCreateProcessTap)? // 3. Lässt sich ein privates Aggregate-Device mit diesem Tap bauen? // 4. Liefert ein IOProc darauf tatsächlich Samples? // // Der Tap wird mit muteBehavior = .unmuted angelegt — dieser Spike hört nur zu und // verändert NICHTS an der Wiedergabe. Der stummschaltende Modus, den der spätere // Mixer braucht, wird hier bewusst nicht benutzt. // // Bauen: Spikes/build-audiotap-probe.sh import Foundation import AppKit import AVFoundation import CoreAudio import AudioToolbox // MARK: - Property-Hilfen func address(_ selector: AudioObjectPropertySelector, _ scope: AudioObjectPropertyScope = kAudioObjectPropertyScopeGlobal) -> AudioObjectPropertyAddress { AudioObjectPropertyAddress(mSelector: selector, mScope: scope, mElement: kAudioObjectPropertyElementMain) } func readArray(_ object: AudioObjectID, _ addr: AudioObjectPropertyAddress, _ type: T.Type) -> [T] { var addr = addr var size: UInt32 = 0 guard AudioObjectGetPropertyDataSize(object, &addr, 0, nil, &size) == noErr, size > 0 else { return [] } let count = Int(size) / MemoryLayout.stride let raw = UnsafeMutableRawPointer.allocate(byteCount: Int(size), alignment: MemoryLayout.alignment) defer { raw.deallocate() } guard AudioObjectGetPropertyData(object, &addr, 0, nil, &size, raw) == noErr else { return [] } return Array(UnsafeBufferPointer(start: raw.assumingMemoryBound(to: T.self), count: count)) } func readValue(_ object: AudioObjectID, _ addr: AudioObjectPropertyAddress, _ initial: T) -> T? { var addr = addr var value = initial var size = UInt32(MemoryLayout.size) guard AudioObjectGetPropertyData(object, &addr, 0, nil, &size, &value) == noErr else { return nil } return value } func readString(_ object: AudioObjectID, _ addr: AudioObjectPropertyAddress) -> String? { var addr = addr var cf: CFString? = nil var size = UInt32(MemoryLayout.size) let status = withUnsafeMutablePointer(to: &cf) { AudioObjectGetPropertyData(object, &addr, 0, nil, &size, $0) } guard status == noErr, let cf else { return nil } return cf as String } let system = AudioObjectID(kAudioObjectSystemObject) print("Core-Audio-Process-Tap-Probe") print("macOS \(ProcessInfo.processInfo.operatingSystemVersionString)") print(String(repeating: "─", count: 68)) // MARK: - 1. Audio-Prozesse auflisten let processes = readArray(system, address(kAudioHardwarePropertyProcessObjectList), AudioObjectID.self) print("\n1. Audio-Prozesse: \(processes.count)") struct AudioProc { let objectID: AudioObjectID let pid: pid_t let bundleID: String let running: Bool var name: String { guard let app = NSRunningApplication(processIdentifier: pid) else { return bundleID } return app.localizedName ?? bundleID } } var procs: [AudioProc] = [] for obj in processes { let pid = readValue(obj, address(kAudioProcessPropertyPID), pid_t(0)) ?? -1 let bundle = readString(obj, address(kAudioProcessPropertyBundleID)) ?? "—" let running = (readValue(obj, address(kAudioProcessPropertyIsRunningOutput), UInt32(0)) ?? 0) != 0 procs.append(AudioProc(objectID: obj, pid: pid, bundleID: bundle, running: running)) } for p in procs.sorted(by: { $0.running && !$1.running }) { let mark = p.running ? "▶ SPIELT AB" : " still" print(" \(mark) pid \(String(p.pid).padding(toLength: 6, withPad: " ", startingAt: 0)) \(p.bundleID)") } guard let target = procs.first(where: { $0.running }) else { print("\nKein Prozess gibt gerade Audio aus — bitte etwas abspielen und erneut starten.") exit(2) } print("\n Ziel: \(target.bundleID) (pid \(target.pid))") // MARK: - 2. Tap erzeugen print("\n2. Process Tap") let tapDescription = CATapDescription(stereoMixdownOfProcesses: [target.objectID]) tapDescription.uuid = UUID() tapDescription.name = "Onyx Spike Tap" tapDescription.isPrivate = true // Bewusst NICHT .mutedWhenTapped — dieser Spike verändert die Wiedergabe nicht. tapDescription.muteBehavior = .unmuted var tapID = AudioObjectID(kAudioObjectUnknown) let tapStatus = AudioHardwareCreateProcessTap(tapDescription, &tapID) guard tapStatus == noErr else { print(" FEHLGESCHLAGEN: OSStatus \(tapStatus)") if tapStatus == 1751737219 { // 'nope' — kAudioHardwareIllegalOperationError print(" → Vermutlich fehlt die TCC-Berechtigung für Systemaudioaufnahme.") print(" Systemeinstellungen → Datenschutz & Sicherheit → Audioaufnahme") } exit(1) } print(" erzeugt, Tap-Objekt \(tapID)") defer { AudioHardwareDestroyProcessTap(tapID) } let tapFormat = readValue(tapID, address(kAudioTapPropertyFormat), AudioStreamBasicDescription()) if let f = tapFormat { print(" Format: \(Int(f.mSampleRate)) Hz, \(f.mChannelsPerFrame) Kanäle, \(f.mBitsPerChannel) bit") } // MARK: - 3. Privates Aggregate-Device print("\n3. Privates Aggregate-Device") guard let outputDevice = readValue(system, address(kAudioHardwarePropertyDefaultOutputDevice), AudioObjectID(kAudioObjectUnknown)), let outputUID = readString(outputDevice, address(kAudioDevicePropertyDeviceUID)) else { print(" Standard-Ausgabegerät nicht ermittelbar") exit(1) } print(" Ausgabegerät: \(readString(outputDevice, address(kAudioObjectPropertyName)) ?? outputUID)") let aggregateUID = UUID().uuidString let description: [String: Any] = [ kAudioAggregateDeviceNameKey: "Onyx Spike Aggregate", kAudioAggregateDeviceUIDKey: aggregateUID, kAudioAggregateDeviceIsPrivateKey: true, kAudioAggregateDeviceIsStackedKey: false, kAudioAggregateDeviceMainSubDeviceKey: outputUID, kAudioAggregateDeviceSubDeviceListKey: [[kAudioSubDeviceUIDKey: outputUID]], kAudioAggregateDeviceTapListKey: [[ kAudioSubTapUIDKey: tapDescription.uuid.uuidString, kAudioSubTapDriftCompensationKey: true, ]], ] var aggregateID = AudioObjectID(kAudioObjectUnknown) let aggStatus = AudioHardwareCreateAggregateDevice(description as CFDictionary, &aggregateID) guard aggStatus == noErr else { print(" FEHLGESCHLAGEN: OSStatus \(aggStatus)") exit(1) } print(" erzeugt, Device \(aggregateID), UID \(aggregateUID)") defer { AudioHardwareDestroyAggregateDevice(aggregateID) } // MARK: - 4. IOProc — kommen Samples an? print("\n4. IOProc (5 s messen)") final class Meter: @unchecked Sendable { var callbacks = 0 var frames = 0 var peak: Float = 0 var silentBuffers = 0 let lock = NSLock() } let meter = Meter() var procID: AudioDeviceIOProcID? let ioStatus = AudioDeviceCreateIOProcIDWithBlock(&procID, aggregateID, nil) { _, inputData, _, _, _ in let buffers = UnsafeMutableAudioBufferListPointer(UnsafeMutablePointer(mutating: inputData)) var localPeak: Float = 0 var frameCount = 0 for buffer in buffers { guard let data = buffer.mData else { continue } let count = Int(buffer.mDataByteSize) / MemoryLayout.size frameCount += count let samples = data.assumingMemoryBound(to: Float.self) for i in 0.. 0 && pk > 0 { print("ERGEBNIS: Tap liefert echte Samples. Per-App-Mixer ist umsetzbar.") } else if cb > 0 { print("ERGEBNIS: Callbacks kommen, aber nur Stille — spielte das Ziel wirklich ab?") } else { print("ERGEBNIS: keine Callbacks. Aggregate-Device liefert nichts.") }