created prototype simulation_distanzerkennung.py

This commit is contained in:
Bigfoggin 2026-09-10 17:16:56 +02:00
commit e90e7b831b
3 changed files with 204 additions and 0 deletions

31
.gitignore vendored Normal file
View File

@ -0,0 +1,31 @@
# Python-spezifische Caches und Binärdateien
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
# Virtuelle Umgebung (venv)
venv/
env/
ENV/
.venv/
# Entwickler-Tools & IDE-Konfigurationen
.vscode/
.idea/
*.swp
*.swo
# Betriebssystem-Dateien
.DS_Store
Thumbs.db
# Temporäre Daten / Audio-Exports
*.wav
*.mp3
*.log
*.tmp
.pytest_cache/
.coverage
htmlcov/

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
pygame>=2.5.0
numpy>=1.24.0
sounddevice>=0.4.6

View File

@ -0,0 +1,170 @@
import sys
import numpy as np
import pygame
import sounddevice as sd
# ==================== KONFIGURATION & PARAMETER ====================
# Raum- und Grafik-Amesungen
ROOM_SIZE_M = 20.0 # 20x20 Meter Raum
HALF_ROOM = ROOM_SIZE_M / 2.0 # -10m bis +10m
WINDOW_SIZE = 800 # Fenstergröße in Pixeln (800x800)
PIXELS_PER_METER = WINDOW_SIZE / ROOM_SIZE_M
# Audio-Parameter
SAMPLE_RATE = 44100
BLOCK_SIZE = 1024
C_SOUND = 343.0 # Schallgeschwindigkeit in m/s
HEAD_RADIUS_M = 0.0875 # Kopfradius (~17.5 cm Ohr-zu-Ohr Abstand)
# Frequenz-Mapping für Distanz (in Hz)
FREQ_MIN_DIST = 1200.0 # Nah (0 m) -> Hohe Frequenz
FREQ_MAX_DIST = 200.0 # Fern (>= 10 m) -> Tiefe Frequenz
MAX_MAPPED_DIST = 10.0 # Maximale Distanz für das Mapping in Metern
# Globale Variablen für Audio-State (Inter-Thread-Kommunikation)
target_freq = 440.0
target_itd_samples = 0.0
target_gain_left = 0.5
target_gain_right = 0.5
# Zähler für kontinuierliche Phase zur Vermeidung von Knacken/Sprüngen
phase = 0.0
# ==================== AUDIO-CALLBACK ====================
def audio_callback(outdata, frames, time_info, status):
global phase, target_freq, target_itd_samples, target_gain_left, target_gain_right
if status:
print(status, file=sys.stderr)
# 1. Erzeugung eines kontinuierlichen Sinussignals
t = (np.arange(frames) + phase) / SAMPLE_RATE
# Sanftes Gleiten der Frequenz zur Vermeidung von Audiorauschen
freq = target_freq
raw_signal = 0.3 * np.sin(2 * np.pi * freq * t)
phase += frames
# 2. Laufzeitverzögerung (ITD) anwenden
# Positive ITD = Signal erreicht das rechte Ohr früher
itd = target_itd_samples
t_indices = np.arange(frames)
# Indizes für linkes und rechtes Ohr berechnen
idx_l = t_indices + itd / 2.0
idx_r = t_indices - itd / 2.0
# Interpolation für stufenlose Mikroverzögerung
signal_l = np.interp(idx_l, t_indices, raw_signal)
signal_r = np.interp(idx_r, t_indices, raw_signal)
# 3. Pegeldifferenz (ILD) anwenden
outdata[:, 0] = signal_l * target_gain_left
outdata[:, 1] = signal_r * target_gain_right
# ==================== PYGAME / HAUPTPROGRAMM ====================
def main():
global target_freq, target_itd_samples, target_gain_left, target_gain_right
pygame.init()
screen = pygame.display.set_mode((WINDOW_SIZE, WINDOW_SIZE))
pygame.display.set_caption("Simulation: Frequenzbasierte Distanzerkennung & Panning")
clock = pygame.time.Clock()
# Audio-Stream starten
stream = sd.OutputStream(
channels=2,
samplerate=SAMPLE_RATE,
blocksize=BLOCK_SIZE,
callback=audio_callback
)
with stream:
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
# --- MAUSPOSITION IN RAUMKOORDINATEN UMRECHNEN ---
mouse_px, mouse_py = pygame.mouse.get_pos()
# Transformation: Fenstermitte = (0,0), Y-Achse nach oben positiv
x_m = (mouse_px - WINDOW_SIZE / 2.0) / PIXELS_PER_METER
y_m = (WINDOW_SIZE / 2.0 - mouse_py) / PIXELS_PER_METER # Y invertieren
# --- BERECHNUNG DER AKUSTISCHEN PARAMETER ---
dist_m = np.sqrt(x_m**2 + y_m**2)
# 1. Frequenz-Mapping (Exponentiell/Linear basierend auf Distanz)
clamped_dist = min(dist_m, MAX_MAPPED_DIST)
# Lineare Skalierung: Nah = Hoch (1200Hz), Fern = Tief (200Hz)
norm_dist = clamped_dist / MAX_MAPPED_DIST
target_freq = FREQ_MIN_DIST - norm_dist * (FREQ_MIN_DIST - FREQ_MAX_DIST)
# 2. Laufzeitunterschied (ITD) & Pegelunterschied (ILD)
# Winkel theta: 0 rad = Vorne (Y+), pi/2 rad = Rechts (X+)
azimuth = np.arctan2(x_m, y_m)
# Woodworth-Modell für ITD (in Sekunden)
itd_sec = (HEAD_RADIUS_M / C_SOUND) * (np.sin(azimuth) + azimuth)
target_itd_samples = itd_sec * SAMPLE_RATE
# ILD: Simples Panning-Gesetz basierend auf dem Azimut
# Panning zwischen -1 (ganz links) und +1 (ganz rechts)
pan = np.sin(azimuth)
target_gain_left = np.clip(0.5 * (1.0 - pan), 0.05, 1.0)
target_gain_right = np.clip(0.5 * (1.0 + pan), 0.05, 1.0)
# --- VISUALISIERUNG (PYGAME) ---
screen.fill((20, 20, 30)) # Dunkler Hintergrund
# Raster / Koordinatensystem zeichnen
center_px = WINDOW_SIZE // 2
pygame.draw.line(screen, (50, 50, 70), (0, center_px), (WINDOW_SIZE, center_px), 1)
pygame.draw.line(screen, (50, 50, 70), (center_px, 0), (center_px, WINDOW_SIZE), 1)
# Abstandskreise (alle 2 Meter)
for r_m in range(2, 11, 2):
r_px = int(r_m * PIXELS_PER_METER)
pygame.draw.circle(screen, (40, 40, 60), (center_px, center_px), r_px, 1)
# Kopf des Nutzers im Zentrum (Vogelperspektive)
head_radius_px = int(HEAD_RADIUS_M * 3 * PIXELS_PER_METER) # Leicht vergrößert für Lesbarkeit
pygame.draw.circle(screen, (200, 200, 200), (center_px, center_px), head_radius_px)
# Nase / Blickrichtung nach Oben (Y+)
pygame.draw.polygon(screen, (250, 100, 100), [
(center_px - 8, center_px - head_radius_px),
(center_px + 8, center_px - head_radius_px),
(center_px, center_px - head_radius_px - 12)
])
# Maus-Objekt / Schallquelle
pygame.draw.circle(screen, (0, 255, 150), (mouse_px, mouse_py), 8)
pygame.draw.line(screen, (0, 255, 150, 100), (center_px, center_px), (mouse_px, mouse_py), 1)
# Text-Overlay (Messwerte anzeigen)
font = pygame.font.SysFont("Consolas", 16)
info_texts = [
f"Position : X = {x_m:5.2f} m | Y = {y_m:5.2f} m",
f"Distanz : {dist_m:5.2f} m",
f"Frequenz : {target_freq:5.1f} Hz",
f"Azimut : {np.degrees(azimuth):5.1f} Grad",
]
for i, text in enumerate(info_texts):
txt_surface = font.render(text, True, (220, 220, 220))
screen.blit(txt_surface, (15, 15 + i * 22))
pygame.display.flip()
clock.tick(60)
pygame.quit()
if __name__ == "__main__":
main()