linux.sipsoftphone/utils/ton_generator.py
data 71e5b92b50 Freizeichen/Besetztzeichen + Klingelton-Fix
- TonGenerator: Lokales Freizeichen (425 Hz, 1s/4s) und Besetztzeichen
  (425 Hz, 480ms/480ms) über generierte WAV-Dateien + paplay.
  Kein PJSUA2-Conference-Bridge-Eingriff mehr.
- KlingeltonPlayer: kill() statt terminate() für sofortiges Stoppen
- Hauptfenster: Freizeichen bei ausgehenden Anrufen, Besetztzeichen
  bei 486 Busy, automatisches Stoppen bei Verbindung/Auflegen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 13:40:57 +01:00

146 lines
4.7 KiB
Python

"""Ton-Generator - Lokale Freizeichen/Besetztzeichen über WAV + paplay.
Erzeugt WAV-Dateien mit Sinustönen und spielt sie über paplay ab.
Nutzt NICHT die PJSUA2 Conference Bridge, um Konflikte mit dem
aktiven Anruf zu vermeiden.
"""
import math
import struct
import subprocess
import tempfile
import wave
from pathlib import Path
from PySide6.QtCore import QObject, QTimer
class TonGenerator(QObject):
"""Erzeugt lokale Signaltöne über generierte WAV-Dateien + paplay.
Deutsche Standard-Töne:
- Freizeichen: 425 Hz, 1s an / 4s aus
- Besetztzeichen: 425 Hz, 480ms an / 480ms aus
"""
def __init__(self, parent=None):
super().__init__(parent)
self._prozess = None
self._loop_timer = QTimer(self)
self._loop_timer.timeout.connect(self._erneut_abspielen)
self._aktuelle_datei = ""
self._laeuft = False
# Temp-Verzeichnis für generierte WAV-Dateien
self._temp_dir = Path(tempfile.mkdtemp(prefix="sipwebapp_toene_"))
# WAV-Dateien vorgenerieren
self._freizeichen_wav = str(self._temp_dir / "freizeichen.wav")
self._besetztzeichen_wav = str(self._temp_dir / "besetztzeichen.wav")
self._wav_generieren(
self._freizeichen_wav, freq=425,
on_ms=1000, off_ms=4000, zyklen=1,
)
self._wav_generieren(
self._besetztzeichen_wav, freq=425,
on_ms=480, off_ms=480, zyklen=3,
)
@staticmethod
def _wav_generieren(pfad, freq, on_ms, off_ms, zyklen):
"""WAV-Datei mit Sinuston-Muster generieren.
Args:
pfad: Ziel-Dateipfad
freq: Frequenz in Hz (425 Hz für Deutschland)
on_ms: Ton-Dauer in Millisekunden
off_ms: Stille-Dauer in Millisekunden
zyklen: Anzahl der Wiederholungen im WAV
"""
sample_rate = 8000
amplitude = 16000 # Moderate Lautstärke (max 32767)
samples = []
for _ in range(zyklen):
# Ton an
on_samples = int(sample_rate * on_ms / 1000)
for i in range(on_samples):
wert = int(amplitude * math.sin(
2 * math.pi * freq * i / sample_rate))
samples.append(wert)
# Stille
off_samples = int(sample_rate * off_ms / 1000)
samples.extend([0] * off_samples)
with wave.open(pfad, "w") as wf:
wf.setnchannels(1)
wf.setsampwidth(2) # 16-bit
wf.setframerate(sample_rate)
wf.writeframes(struct.pack(f"<{len(samples)}h", *samples))
def freizeichen_starten(self):
"""Deutsches Freizeichen abspielen (425 Hz, 1s an / 4s aus)."""
# 1 Zyklus = 5000ms, Loop-Timer etwas länger
self._abspielen(self._freizeichen_wav, loop_ms=5200)
def besetztzeichen_starten(self):
"""Deutsches Besetztzeichen abspielen (425 Hz, 480ms an / 480ms aus)."""
# 3 Zyklen = 2880ms, Loop-Timer etwas länger
self._abspielen(self._besetztzeichen_wav, loop_ms=3100)
def _abspielen(self, datei, loop_ms):
"""WAV-Datei mit paplay abspielen und loopen."""
self.stoppen()
self._aktuelle_datei = datei
self._laeuft = True
self._paplay_starten()
self._loop_timer.start(loop_ms)
def _paplay_starten(self):
"""paplay Subprocess starten."""
if not self._laeuft:
return
# Alten Prozess aufräumen
if self._prozess and self._prozess.poll() is not None:
self._prozess = None
try:
self._prozess = subprocess.Popen(
["paplay", self._aktuelle_datei],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except (FileNotFoundError, OSError):
self._laeuft = False
def _erneut_abspielen(self):
"""Timer-Callback: Ton in Schleife abspielen."""
if not self._laeuft:
self._loop_timer.stop()
return
self._paplay_starten()
def stoppen(self):
"""Ton sofort stoppen."""
self._laeuft = False
self._loop_timer.stop()
if self._prozess:
try:
self._prozess.kill() # SIGKILL für sofortiges Stoppen
self._prozess.wait(timeout=1)
except (OSError, subprocess.TimeoutExpired):
pass
self._prozess = None
def aufraumen(self):
"""Temp-Dateien löschen und aufräumen."""
self.stoppen()
import shutil
try:
shutil.rmtree(self._temp_dir, ignore_errors=True)
except Exception:
pass
@property
def laeuft(self):
"""True wenn gerade ein Ton abgespielt wird."""
return self._laeuft