From e90e7b831b23635c85e153a7b5ede706f9322f32 Mon Sep 17 00:00:00 2001 From: Bigfoggin Date: Thu, 10 Sep 2026 17:16:56 +0200 Subject: [PATCH] created prototype simulation_distanzerkennung.py --- .gitignore | 31 ++++++ requirements.txt | 3 + src/simulation_distanzerkennung.py | 170 +++++++++++++++++++++++++++++ 3 files changed, 204 insertions(+) create mode 100644 .gitignore create mode 100644 requirements.txt create mode 100644 src/simulation_distanzerkennung.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f5db28e --- /dev/null +++ b/.gitignore @@ -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/ \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cb22a54 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pygame>=2.5.0 +numpy>=1.24.0 +sounddevice>=0.4.6 \ No newline at end of file diff --git a/src/simulation_distanzerkennung.py b/src/simulation_distanzerkennung.py new file mode 100644 index 0000000..f7a5bb2 --- /dev/null +++ b/src/simulation_distanzerkennung.py @@ -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() \ No newline at end of file