Files
onyx/docs/spikes/C-audiotap.md
Scarriffle d001e439a6 Mixer war still: er hat sich selbst abgegriffen
Der Mixer gibt den Ton der bearbeiteten Programme selbst aus — und steht damit
als Tonquelle in seiner eigenen Liste. Eine Zeile wird geregelt, sobald man ihren
Regler anfasst. Ein Klick auf Onyx' eigene Zeile genügte also.

Der Selbst-Tap läuft wie jeder andere mit mutedWhenTapped und schaltet damit
Onyx' gesamte Ausgabe stumm — die Wiedergabe *aller* bearbeiteten Programme.
Völlige Stille, während die Pegelanzeigen weiter ausschlagen, weil die Taps ja
Signal bekommen. Und der Zustand stand in den Einstellungen, überlebte also
jeden Neustart.

Gemessen (Spikes/mixer-path-probe.swift): Onyx' Ausgabepegel klettert von 0,388
auf 2,833, sobald er sich selbst bearbeitet. Der Weg an sich ist in Ordnung —
Tap, Aggregate-Gerät und IOProc liefern einzeln wie zu dritt volle Pegel, das
war der Hauptverdacht und ist widerlegt.

Onyx erscheint jetzt nicht mehr in der eigenen Liste, lässt sich nicht auswählen,
und ein gespeicherter Eintrag wird beim Laden entfernt und zurückgeschrieben.

Dazu ein zweiter Fund aus derselben Ecke: setVolume und setMuted griffen auf
taps[bundleID] zu. Die Taps liegen aber unter Kennung#Prozessnummer, weil ein
Programm mehrere Tonquellen haben kann — der Zugriff ging immer ins Leere. Der
Regler wirkte erst, wenn die Auffrischung drei Sekunden später nachzog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 17:57:15 +02:00

5.5 KiB

Spike C — Core Audio Process Taps auf M5 Pro / macOS 26.6.1

Gemessen am 10.08.2026. Werkzeug: Spikes/audiotap-probe.swift, signiert mit dem Team-Zertifikat. Der Tap lief mit muteBehavior = .unmuted — der Spike hört nur zu und verändert die Wiedergabe nicht.

Ergebnis: bestätigt

Schritt Ergebnis
kAudioHardwarePropertyProcessObjectList 35 Audio-Prozesse, mit PID, Bundle-ID und IsRunningOutput
AudioHardwareCreateProcessTap erzeugt, Tap-Objekt 195, Format 48 kHz / 2 Kanäle / 32 bit Float
AudioHardwareCreateAggregateDevice (privat) erzeugt, Device 196
IOProc über 5 s 469 Callbacks (93/s), 480 256 Frames, Spitzenpegel 0,4516, 0 stille Puffer

480 256 Frames ÷ 2 Kanäle ÷ 5 s = 48 025 Hz — deckt sich exakt mit der Abtastrate. 93 Callbacks/s entsprechen ~512 Frames je Puffer, also rund 10,7 ms Latenz. Für einen Lautstärkemixer unkritisch.

Der bekannte Fehlerfall „Puffer laufen nach einiger Zeit auf Null" trat im 5-Sekunden-Fenster nicht auf. Das widerlegt ihn nicht — der Watchdog aus dem Plan bleibt.

Kein TCC-Dialog erschienen. Das ist noch kein Freibrief: .unmuted ist der harmlose Fall. Ob .mutedWhenTapped — der Modus, den der Mixer wirklich braucht — eine Berechtigung anfordert, muss in Phase 7 gegengeprüft werden.

Fallstrick beim Bauen

Die verbreitete Kurzform für Core-Audio-Property-Arrays

var out = [T](repeating: unsafeBitCast(0 as Int, to: T.self), count: count)

stürzt mit SIGTRAP ab: unsafeBitCast verlangt identische Größen, Int hat 8 Byte, AudioObjectID 4. Da stdout in eine Pipe voll gepuffert ist, kam nicht einmal die erste Zeile Ausgabe — der Absturz sah aus wie ein stiller Fehlstart. MetricsProvider und AudioProvider verwenden deshalb die Variante über UnsafeMutableRawPointer.allocate mit MemoryLayout<T>.stride.

Konflikt: SoundSource ist bereits installiert

Die Prozessliste zeigt com.rogueamoeba.soundsource und com.rogueamoeba.arkaudiod, dazu liegt /Library/Audio/Plug-Ins/HAL/ARK.driver im System. SoundSource 6.1.1 ist Rogue Amoebas Per-App-Lautstärkemixer — funktional genau das, was Phase 7 bauen soll.

Zwei Folgen:

  1. Doppelung. Der Mixer aus Phase 7 wäre ein Nachbau einer bereits vorhandenen, ausgereiften und bezahlten App.
  2. Technisches Risiko. ARK.driver ist ein HAL-Plugin, das Audio abfängt, bevor es das Ausgabegerät erreicht. Leitet SoundSource einen Prozess bereits um, sieht ein Onyx-Process-Tap auf denselben Prozess unter Umständen nichts mehr — oder beide Ebenen schalten sich gegenseitig stumm. Sauber vorhersagen lässt sich das nicht; es müsste gemessen werden.

Vor Phase 7 zu klären. Bis dahin ist der Befund neutral festgehalten, nicht entschieden.

Nebenbefund

Alcove.app und Sapphire.app sind beide installiert — also die beiden Apps, die als Vorbild genannt wurden. Für die Notch-Shell in Phase 1 heißt das: mindestens eine davon greift denselben Bildschirmbereich ab. Für die Entwicklung sollten sie beendet sein, sonst überlagern sich Hover-Zonen und die Panels streiten sich um dieselbe Fläche.


Spike C2 + C3 — der Wiedergabeweg (13.08.2026)

Spike C hat nur zugehört: muteBehavior = .unmuted, kein einziges Sample zurück ans Gerät. Genau der ungeprüfte Teil war der, der im Betrieb still blieb. Werkzeuge: Spikes/mixer-path-probe.swift und Spikes/multi-tap-probe.swift, Testquelle ein eigens gebautes App-Bundle mit 440-Hz-Dauerton (/tmp/OnyxTone.app).

Der Weg selbst ist in Ordnung

Messung Ergebnis
Tap .unmuted, ein Prozess 469 Aufrufe/5 s, Spitze 0,366
Tap .mutedWhenTapped + Ausgabe über das Aggregate-Gerät 469 Aufrufe/5 s, Spitze 0,366
Puffergeometrie Eingang 1 Puffer / 2 Kanäle / 4096 Byte, Ausgang identisch
Zwei Aggregate-Geräte gleichzeitig auf demselben Lautsprecher beide 375+ Aufrufe, beide Spitze 0,366
Drei Aggregate-Geräte gleichzeitig alle drei 375+ Aufrufe, alle Spitze 0,366

Mehrere Geräte auf einem Ausgabegerät waren der Hauptverdacht — Chrome bringt mehrere Tonprozesse mit, also legt der Mixer mehrere Geräte an. Der Verdacht ist widerlegt: keines verhungert.

Der Fund: der Mixer greift sich selbst ab

Sobald der Mixer ein Programm bearbeitet, gibt er dessen Ton aus. Damit steht er selbst in kAudioHardwarePropertyProcessObjectList — und in seiner eigenen Programmliste. Eine Zeile wird geregelt, sobald man ihren Regler anfasst. Ein einziger Klick auf Onyx' eigene Zeile genügt also.

Was dann passiert, ist gemessen:

Zustand Spitze der Onyx-Ausgabe
Onyx bearbeitet nur den Testton 0,388
Onyx bearbeitet zusätzlich sich selbst 2,833

Der Pegel schaukelt sich auf, weil Onyx seine eigene Ausgabe wieder abgreift und erneut ausgibt. Entscheidend ist aber die andere Hälfte: der Selbst-Tap läuft mit .mutedWhenTapped und schaltet damit Onyx' gesamte Ausgabe stumm — also die Wiedergabe aller bearbeiteten Programme. Ergebnis: völlige Stille, während die Pegelanzeigen weiter ausschlagen, weil die Taps ja Signal bekommen.

Und der Zustand steht in den Einstellungen: er überlebt jeden Neustart.

Abhilfe (ProcessVisibility.isExcluded): Onyx erscheint nicht in der eigenen Liste, lässt sich nicht auswählen, und ein gespeicherter Eintrag wird beim Laden entfernt und zurückgeschrieben.

Nebenbefund

Ein Tap sieht das Signal vor dem Stummschalten. Ein globaler Tap zeigt deshalb weiterhin den vollen Pegel eines bereits stummgeschalteten Programms — als Nachweis, ob das Stummschalten greift, taugt er nicht.