- 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>
146 lines
4.7 KiB
Python
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
|