Phase 9: Einrichtung, Autostart, Notarisierung — und die Mixer-Optik
Die Einrichtung beim ersten Start erklärt jede der vier Sachen einzeln und fragt jede einzeln an. Vier Systemdialoge hintereinander wären der übliche Weg und der falsche: wer nicht weiß wofür, klickt viermal „Nicht erlauben", und danach fragt macOS nie wieder. Sie kommt beim ersten Start immer, auch wenn zufällig schon alles erlaubt ist — sie erklärt auch den Helfer und den Autostart. Beim Autostart ist `requiresApproval` der Zustand, der weh tut. Wer das Anmeldeobjekt in den Systemeinstellungen abgeschaltet hat, kann in Onyx auf den Schalter drücken, so oft er will. Als „aus" angezeigt führt das in eine Schleife, also steht dort jetzt, wo es weitergeht. Der Mixer verliert den Schalter je Programm. Zehn Mini-Schalter untereinander sind das, was den Eindruck macht; am Regler zu ziehen ist ohnehin die Entscheidung, dieses Programm zu regeln, und der große Ausschalter stellt alles zurück. Der Regler ist selbst gezeichnet — der Systemregler bringt sein eigenes Erscheinungsbild mit und kann nicht zeigen, worauf es hier ankommt: den Pegel und die Grenze bei 100 %. Der Pegel wird im selben Durchlauf gemessen, in dem verstärkt wird, und über `Atomic` weitergereicht. Eine gewöhnliche Eigenschaft wäre ein Wettlauf, eine Sperre im Audiothread der klassische Weg zu Aussetzern. Dabei fällt das `Date()` aus dem Echtzeitpfad, das dort nie hingehört hat. Zwei Funde beim Bauen des Release-Wegs, beide hätten erst nach dem Hochladen zur Ablehnung geführt: der Helfer wurde ohne sicheren Zeitstempel signiert, und das Adapter-Framework ad-hoc. Beides gilt jetzt nur noch im Debug. Scripts/release.sh prüft Zertifikat und Notarisierungsprofil vorweg, statt nach zehn Minuten Bauen zu scheitern. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import Synchronization
|
||||
import AudioToolbox
|
||||
import CoreAudio
|
||||
import AVFoundation
|
||||
@@ -34,15 +35,43 @@ final class ProcessTap {
|
||||
/// aber ein einzelner `Float` wird auf arm64 atomar geschrieben.
|
||||
private(set) var gain: Float = 1
|
||||
|
||||
/// Der höchste Ausschlag seit dem letzten Abholen — als Bitmuster, weil
|
||||
/// `Atomic` keine Fließkommazahlen führt.
|
||||
///
|
||||
/// Sperrenfrei und damit im Echtzeitpfad erlaubt. Eine gewöhnliche
|
||||
/// Eigenschaft zu beschreiben wäre ein Wettlauf mit dem Leser, und eine
|
||||
/// Sperre im Audiothread ist der klassische Weg zu Aussetzern.
|
||||
private let peakBits = Atomic<UInt32>(0)
|
||||
|
||||
/// Wann zuletzt Ton kam — in Sekunden seit dem Programmstart, damit der
|
||||
/// Echtzeitpfad keine `Date` anfassen muss.
|
||||
private let lastAudioUptime = Atomic<UInt64>(0)
|
||||
|
||||
private var tapID = AudioObjectID(kAudioObjectUnknown)
|
||||
private var aggregateID = AudioObjectID(kAudioObjectUnknown)
|
||||
private var procID: AudioDeviceIOProcID?
|
||||
private let tapUUID = UUID()
|
||||
|
||||
/// Wann zuletzt ein Puffer mit Inhalt kam — für den Wachhund.
|
||||
private(set) var lastAudioAt = Date()
|
||||
private(set) var isRunning = false
|
||||
|
||||
/// Wie lange dieser Tap schon nur Stille liefert.
|
||||
var secondsSinceAudio: TimeInterval {
|
||||
let stored = lastAudioUptime.load(ordering: .relaxed)
|
||||
guard stored > 0 else { return 0 }
|
||||
return max(0, Self.uptime() - Double(stored) / 1000)
|
||||
}
|
||||
|
||||
/// Der Ausschlag seit dem letzten Abruf, 0…1. Holt ihn ab und setzt zurück,
|
||||
/// damit die Anzeige abfällt, statt auf dem Höchstwert stehen zu bleiben.
|
||||
func takePeak() -> Float {
|
||||
Float(bitPattern: peakBits.exchange(0, ordering: .relaxed))
|
||||
}
|
||||
|
||||
private static func uptime() -> Double {
|
||||
Double(DispatchTime.now().uptimeNanoseconds) / 1_000_000_000
|
||||
}
|
||||
|
||||
init(process: AudioProcess) {
|
||||
self.process = process
|
||||
}
|
||||
@@ -111,7 +140,7 @@ final class ProcessTap {
|
||||
}
|
||||
|
||||
isRunning = true
|
||||
lastAudioAt = Date()
|
||||
lastAudioUptime.store(UInt64(Self.uptime() * 1000), ordering: .relaxed)
|
||||
log.notice("Tap aktiv für \(self.process.name, privacy: .public)")
|
||||
}
|
||||
|
||||
@@ -126,7 +155,7 @@ final class ProcessTap {
|
||||
let outputList = UnsafeMutableAudioBufferListPointer(output)
|
||||
|
||||
let factor = gain
|
||||
var sawAudio = false
|
||||
var peak: Float = 0
|
||||
|
||||
for (index, outputBuffer) in outputList.enumerated() {
|
||||
guard index < inputList.count,
|
||||
@@ -143,12 +172,23 @@ final class ProcessTap {
|
||||
// Nur begrenzen, wenn wirklich verstärkt wird — unterhalb der
|
||||
// Schwelle ist die Kennlinie ohnehin die Identität, und im
|
||||
// Echtzeitpfad zählt jede eingesparte Rechnung.
|
||||
outputSamples[sample] = factor > 1 ? SoftClip.apply(value) : value
|
||||
if !sawAudio, value != 0 { sawAudio = true }
|
||||
let limited = factor > 1 ? SoftClip.apply(value) : value
|
||||
outputSamples[sample] = limited
|
||||
// Der Ausschlag wird im selben Durchlauf mitgenommen: ein
|
||||
// zweiter Durchgang nur zum Messen wäre im Echtzeitpfad die
|
||||
// teuerste Art, an dieselbe Zahl zu kommen.
|
||||
let magnitude = abs(limited)
|
||||
if magnitude > peak { peak = magnitude }
|
||||
}
|
||||
}
|
||||
|
||||
if sawAudio { lastAudioAt = Date() }
|
||||
if peak > 0 {
|
||||
// Höchstwert behalten, bis die Anzeige ihn abholt — sonst
|
||||
// verschluckt ein Puffer mit Stille die Spitze davor.
|
||||
let previous = Float(bitPattern: peakBits.load(ordering: .relaxed))
|
||||
if peak > previous { peakBits.store(peak.bitPattern, ordering: .relaxed) }
|
||||
lastAudioUptime.store(UInt64(Self.uptime() * 1000), ordering: .relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Abbau
|
||||
|
||||
Reference in New Issue
Block a user