Files
Scarriffle ed8b095377 Overdrive je Programm: bis 400 %, aber nur auf Ansage
Der Regler ging bisher immer bis 200 %, ohne dass man das wollen musste. Jetzt
endet er bei 100 % — der Stellung, in der ein Programm klingt, wie es gemeint
war —, und ein Blitz in der Zeile gibt den Bereich darüber frei. Je Programm:
ein Browser braucht ihn für eine leise Konferenz, die Musik daneben nicht.

Oberhalb von 100 % gilt die Kennlinie nicht mehr. Dort steht die Zahl für den
Faktor selbst: 400 % sind das Vierfache der Amplitude. Mit der Kennlinie wären
daraus 4^1,66 ≈ 10,4 geworden — eine Beschriftung, die etwas anderes verspricht,
als der Regler tut. Bei genau 100 % stimmen beide Hälften überein, die Kurve hat
also keinen Sprung, nur einen Knick. Unterhalb bleibt alles wie es war.

Beim Abschalten wird die Lautstärke zurückgeholt. Sonst bliebe ein Programm
dreifach verstärkt, während sein Regler am rechten Anschlag steht und 100 %
anzeigt. Dasselbe beim Laden alter Einstellungen: ein gespeichertes 180 % aus
der Zeit ohne Freigabe fällt auf 100 %.

Die 100 %-Markierung im Regler erscheint nur noch mit Overdrive — ohne ihn läge
sie am rechten Anschlag und sagte dort nichts mehr.

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

153 lines
5.1 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Testing
import Foundation
@testable import AudioProvider
// Hier wird in den Ton eingegriffen, der aus den Lautsprechern kommt. Ein
// Rechenfehler klingt nicht falsch er knackt, verzerrt oder ist schmerzhaft
// laut. Deshalb liegt die Rechnung als reine Funktion vor und wird vollständig
// durchgeprüft, bevor sie an einen Audiopuffer kommt.
@Suite("Verstärkung")
struct GainTests {
@Test("Volle Lautstärke lässt das Signal unverändert")
func unityIsTransparent() {
#expect(Gain.factor(forVolume: 1.0) == 1.0)
}
@Test("Stumm ist wirklich still")
func zeroIsSilent() {
#expect(Gain.factor(forVolume: 0) == 0)
}
@Test("Die Skala ist wahrnehmungsgerecht, nicht linear")
func scaleIsPerceptual() {
// Halber Reglerweg soll halb so laut klingen, nicht halbe Amplitude
// haben das sind rund 10 dB, also etwa Faktor 0,3. Linear wäre der
// Regler in der unteren Hälfte praktisch wirkungslos.
let half = Gain.factor(forVolume: 0.5)
#expect(half > 0.2 && half < 0.4)
}
@Test("Die Skala steigt durchgehend")
func scaleIsMonotonic() {
var previous = -1.0
for step in 0...100 {
let value = Gain.factor(forVolume: Double(step) / 100)
#expect(value >= previous)
previous = value
}
}
@Test("Über 100 Prozent wird tatsächlich verstärkt")
func boostAmplifies() {
#expect(Gain.factor(forVolume: 2.0) > 1.0)
}
@Test("Der Regelbereich ist begrenzt — auch bei absurder Eingabe")
func rangeIsClamped() {
#expect(Gain.factor(forVolume: -5) == 0)
// Die Kappung sitzt beim höchsten überhaupt erreichbaren Wert. Ob ein
// Programm so weit aufgedreht werden **darf**, entscheidet der Overdrive
// im VolumeStore, nicht die Umrechnung hier.
#expect(Gain.factor(forVolume: 99) == Gain.factor(forVolume: Gain.overdriveMaximum))
}
}
@Suite("Begrenzer")
struct LimiterTests {
@Test("Leise Signale gehen unverändert durch")
func quietPassesThrough() {
#expect(abs(SoftClip.apply(0.3) - 0.3) < 0.001)
#expect(abs(SoftClip.apply(-0.3) + 0.3) < 0.001)
}
@Test("Nichts verlässt je den gültigen Bereich")
func neverExceedsFullScale() {
// Der eigentliche Zweck: bei 200 Prozent entstehen Werte weit über 1,0.
// Ungebremst werden die von der Hardware hart abgeschnitten, und das
// knackt hörbar.
for value in stride(from: -8.0, through: 8.0, by: 0.05) {
let result = SoftClip.apply(Float(value))
#expect(result <= 1.0 && result >= -1.0)
}
}
@Test("Die Kennlinie ist punktsymmetrisch")
func curveIsSymmetric() {
for value in stride(from: 0.0, through: 4.0, by: 0.1) {
let positive = SoftClip.apply(Float(value))
let negative = SoftClip.apply(Float(-value))
#expect(abs(positive + negative) < 0.0001)
}
}
@Test("Die Kennlinie steigt durchgehend — sonst klingt sie verzerrt")
func curveIsMonotonic() {
var previous = Float(-2)
for value in stride(from: -6.0, through: 6.0, by: 0.01) {
let result = SoftClip.apply(Float(value))
#expect(result >= previous - 0.0001)
previous = result
}
}
@Test("Null bleibt null")
func zeroStaysZero() {
#expect(SoftClip.apply(0) == 0)
}
}
@Suite("Lautstärke-Gedächtnis")
struct VolumeStoreTests {
@Test("Ein gemerkter Wert kommt zurück")
func roundTrip() {
var store = VolumeStore()
store.set(0.4, for: "com.spotify.client")
#expect(store.volume(for: "com.spotify.client") == 0.4)
}
@Test("Ein unbekanntes Programm startet auf voller Lautstärke")
func unknownDefaultsToFull() {
// Nicht auf null: ein Programm, das nach dem Start stumm ist, wirkt
// kaputt, und niemand sucht die Ursache im Mixer.
#expect(VolumeStore().volume(for: "com.example.neu") == 1.0)
}
@Test("Stummschaltung ist getrennt von der Lautstärke")
func muteIsSeparate() {
// Sonst verliert man beim Stummschalten den eingestellten Wert und
// muss ihn danach neu suchen.
var store = VolumeStore()
store.set(0.6, for: "app")
store.setMuted(true, for: "app")
#expect(store.isMuted(for: "app"))
#expect(store.volume(for: "app") == 0.6)
store.setMuted(false, for: "app")
#expect(store.volume(for: "app") == 0.6)
}
@Test("Die wirksame Verstärkung ist null, solange stumm geschaltet ist")
func mutedMeansSilent() {
var store = VolumeStore()
store.set(0.8, for: "app")
store.setMuted(true, for: "app")
#expect(store.effectiveGain(for: "app") == 0)
}
@Test("Werte außerhalb des Bereichs werden eingefangen")
func valuesAreClamped() {
var store = VolumeStore()
store.set(-1, for: "app")
#expect(store.volume(for: "app") == 0)
store.set(999, for: "app")
#expect(store.volume(for: "app") == Gain.maximumVolume)
}
}