135 lines
4.0 KiB
Python
135 lines
4.0 KiB
Python
from __future__ import annotations
|
|
|
|
import struct
|
|
|
|
import numpy as np
|
|
|
|
from app.livekit.adapters.audio_gain import (
|
|
GainEmitter,
|
|
SoftClipGain,
|
|
tts_output_gain_from_env,
|
|
)
|
|
|
|
|
|
def _pcm(*samples: int) -> bytes:
|
|
return struct.pack("<" + "h" * len(samples), *samples)
|
|
|
|
|
|
def _samples(pcm: bytes) -> list[int]:
|
|
return list(struct.unpack("<" + "h" * (len(pcm) // 2), pcm))
|
|
|
|
|
|
def test_gain_1_0_is_disabled_and_passthrough() -> None:
|
|
g = SoftClipGain(gain=1.0)
|
|
assert g.enabled is False
|
|
pcm = _pcm(1000, -2000, 3000)
|
|
assert g.process(pcm) == pcm
|
|
|
|
|
|
def test_normal_level_is_boosted_near_linear() -> None:
|
|
g = SoftClipGain(gain=2.0) # ceiling default -1 dBFS
|
|
# sinal baixo (~ -30 dBFS): boost deve ser praticamente 2x
|
|
out = _samples(g.process(_pcm(1000, -1000)))
|
|
assert abs(out[0] - 2000) <= 40
|
|
assert abs(out[1] + 2000) <= 40
|
|
|
|
|
|
def test_hot_peaks_never_clip_past_ceiling() -> None:
|
|
ceiling = 0.891 # ~ -1 dBFS
|
|
g = SoftClipGain(gain=2.0, ceiling=ceiling)
|
|
limit = int(ceiling * 32768) + 1
|
|
# picos quentes que, com 2x linear, estourariam o fundo de escala
|
|
out = _samples(g.process(_pcm(30000, -30000, 25000, -25000)))
|
|
assert all(abs(s) <= limit for s in out), out
|
|
# e continua monotonicamente crescente (sem wraparound/inversao de fase)
|
|
assert out[0] > 0 and out[1] < 0
|
|
|
|
|
|
def test_monotonic_transfer_curve() -> None:
|
|
g = SoftClipGain(gain=2.0)
|
|
xs = list(range(0, 32000, 1000))
|
|
ys = [_samples(g.process(_pcm(x)))[0] for x in xs]
|
|
assert all(b >= a for a, b in zip(ys, ys[1:])), ys
|
|
|
|
|
|
def test_odd_length_bytes_do_not_crash() -> None:
|
|
g = SoftClipGain(gain=2.0)
|
|
pcm = _pcm(1000, -1000) + b"\x7f" # 1 byte solto
|
|
out = g.process(pcm)
|
|
assert len(out) == len(pcm)
|
|
assert out[-1:] == b"\x7f"
|
|
|
|
|
|
def test_empty_input() -> None:
|
|
assert SoftClipGain(gain=2.0).process(b"") == b""
|
|
|
|
|
|
def test_env_loader_defaults_to_disabled(monkeypatch) -> None:
|
|
monkeypatch.delenv("TTS_OUTPUT_GAIN", raising=False)
|
|
monkeypatch.delenv("TTS_OUTPUT_CEILING_DBFS", raising=False)
|
|
g = tts_output_gain_from_env()
|
|
assert g.gain == 1.0
|
|
assert g.enabled is False
|
|
|
|
|
|
def test_env_loader_reads_gain_and_ceiling(monkeypatch) -> None:
|
|
monkeypatch.setenv("TTS_OUTPUT_GAIN", "2.0")
|
|
monkeypatch.setenv("TTS_OUTPUT_CEILING_DBFS", "-6")
|
|
g = tts_output_gain_from_env()
|
|
assert g.gain == 2.0
|
|
assert abs(g.ceiling - 10 ** (-6 / 20.0)) < 1e-6
|
|
|
|
|
|
class _FakeEmitter:
|
|
def __init__(self) -> None:
|
|
self.pushed: list[bytes] = []
|
|
self.initialized = False
|
|
self.flushed = False
|
|
|
|
def initialize(self, **kwargs) -> None:
|
|
self.initialized = True
|
|
|
|
def push(self, data: bytes) -> None:
|
|
self.pushed.append(data)
|
|
|
|
def flush(self) -> None:
|
|
self.flushed = True
|
|
|
|
|
|
def test_gain_emitter_transforms_push_and_forwards_rest() -> None:
|
|
inner = _FakeEmitter()
|
|
em = GainEmitter(inner, SoftClipGain(gain=2.0))
|
|
|
|
em.initialize(sample_rate=24000)
|
|
em.push(_pcm(1000, -1000))
|
|
em.flush()
|
|
|
|
assert inner.initialized is True
|
|
assert inner.flushed is True
|
|
assert len(inner.pushed) == 1
|
|
# o que chegou ao emitter real foi amplificado
|
|
out = _samples(inner.pushed[0])
|
|
assert abs(out[0] - 2000) <= 40
|
|
|
|
|
|
def test_gain_emitter_matches_numpy_reference() -> None:
|
|
gain = SoftClipGain(gain=2.0, ceiling=0.891)
|
|
pcm = _pcm(500, -12000, 28000, -31000, 0)
|
|
ref_x = np.frombuffer(pcm, dtype="<i2").astype(np.float32) / 32768.0
|
|
ref_y = 0.891 * np.tanh((2.0 / 0.891) * ref_x)
|
|
ref = np.clip(np.rint(ref_y * 32768.0), -32768.0, 32767.0).astype("<i2")
|
|
assert gain.process(pcm) == ref.tobytes()
|
|
|
|
|
|
def test_gain_configuration_rejects_non_finite_and_out_of_range_values(monkeypatch) -> None:
|
|
monkeypatch.setenv("TTS_OUTPUT_GAIN", "nan")
|
|
monkeypatch.setenv("TTS_OUTPUT_CEILING_DBFS", "inf")
|
|
gain = tts_output_gain_from_env()
|
|
assert gain.gain == 1.0
|
|
|
|
monkeypatch.setenv("TTS_OUTPUT_GAIN", "-2")
|
|
monkeypatch.setenv("TTS_OUTPUT_CEILING_DBFS", "-200")
|
|
gain = tts_output_gain_from_env()
|
|
assert gain.ceiling == 0.05
|
|
assert gain.gain == 0.0
|