created prototype simulation_distanzerkennung.py
This commit is contained in:
commit
e90e7b831b
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal 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
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
pygame>=2.5.0
|
||||
numpy>=1.24.0
|
||||
sounddevice>=0.4.6
|
||||
170
src/simulation_distanzerkennung.py
Normal file
170
src/simulation_distanzerkennung.py
Normal 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()
|
||||
Loading…
x
Reference in New Issue
Block a user