diff --git a/pics/prototyp_distanzerkennung_cropped.png b/pics/prototyp_distanzerkennung_cropped.png new file mode 100644 index 0000000..c45dff0 Binary files /dev/null and b/pics/prototyp_distanzerkennung_cropped.png differ diff --git a/pics/prototyp_multiobjekt_distanzerkennung.png b/pics/prototyp_multiobjekt_distanzerkennung.png new file mode 100644 index 0000000..5b26e1e Binary files /dev/null and b/pics/prototyp_multiobjekt_distanzerkennung.png differ diff --git a/src/00simulation_distanzerkennung.py b/src/00simulation_distanzerkennung.py new file mode 100644 index 0000000..039a475 --- /dev/null +++ b/src/00simulation_distanzerkennung.py @@ -0,0 +1,184 @@ +import sys +import numpy as np +import pygame +import sounddevice as sd + +# ==================== KONFIGURATION & PARAMETER ==================== +ROOM_SIZE_M = 20.0 +WINDOW_SIZE = 800 +PIXELS_PER_METER = WINDOW_SIZE / ROOM_SIZE_M + +# Audio-Parameter +SAMPLE_RATE = 44100 +BLOCK_SIZE = 1024 +C_SOUND = 343.0 +HEAD_RADIUS_M = 0.0875 + +# Ethereal / Musical Pitch Boundaries (A3 -> A5 Pentatonic / Harmonic range) +FREQ_MIN_DIST = 880.0 # Near (0 m) -> High ethereal sheen (A5) +FREQ_MAX_DIST = 220.0 # Far (>= 10 m) -> Deep warm root note (A3) +MAX_MAPPED_DIST = 10.0 + +# Global States +target_freq = 220.0 +target_itd_samples = 0.0 +target_gain_left = 0.2 +target_gain_right = 0.2 + +# Audio State Smoothing (Prevents Crackling) +current_freq = 220.0 +current_itd = 0.0 +phase_1 = 0.0 +phase_2 = 0.0 +phase_3 = 0.0 + + +# ==================== AUDIO-CALLBACK ==================== +def audio_callback(outdata, frames, time_info, status): + global phase_1, phase_2, phase_3, current_freq, current_itd + global target_freq, target_itd_samples, target_gain_left, target_gain_right + + if status: + print(status, file=sys.stderr) + + # 1. Smooth parameter transitions per frame vector to eliminate clicks/crackles + freq_vec = np.linspace(current_freq, target_freq, frames) + itd_vec = np.linspace(current_itd, target_itd_samples, frames) + + current_freq = target_freq + current_itd = target_itd_samples + + # 2. Phase-continuous synthesis for 3 ethereal chord intervals (Root, Minor 3rd/Fifth blend, Octave) + dphase_1 = 2 * np.pi * freq_vec / SAMPLE_RATE + dphase_2 = 2 * np.pi * (freq_vec * 1.498) / SAMPLE_RATE # Perfect Fifth interval + dphase_3 = 2 * np.pi * (freq_vec * 2.0) / SAMPLE_RATE # Octave shimmer + + phases_1 = phase_1 + np.cumsum(dphase_1) + phases_2 = phase_2 + np.cumsum(dphase_2) + phases_3 = phase_3 + np.cumsum(dphase_3) + + phase_1 = phases_1[-1] % (2 * np.pi) + phase_2 = phases_2[-1] % (2 * np.pi) + phase_3 = phases_3[-1] % (2 * np.pi) + + # Ethereal Pad synthesis (Pure sine combinations with smooth volume envelopes) + wave_root = np.sin(phases_1) + wave_fifth = 0.35 * np.sin(phases_2) + wave_shimmer = 0.15 * np.sin(phases_3) + + synth_signal = 0.12 * (wave_root + wave_fifth + wave_shimmer) + + # 3. Smooth ITD delay processing without edge interpolation crackles + t_indices = np.arange(frames) + + # Calculate fractional delay per sample + idx_l = t_indices + (itd_vec / 2.0) + idx_r = t_indices - (itd_vec / 2.0) + + # Extend buffer indexing cleanly via continuous interpolation + signal_l = np.interp(idx_l, t_indices, synth_signal) + signal_r = np.interp(idx_r, t_indices, synth_signal) + + # Apply spatial gains + outdata[:, 0] = signal_l * target_gain_left + outdata[:, 1] = signal_r * target_gain_right + + +# ==================== MAIN PROGRAM ==================== +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: Ethereal Spatial Audio Tracking") + clock = pygame.time.Clock() + + 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 + + # Screen coordinates to meters + mouse_px, mouse_py = pygame.mouse.get_pos() + x_m = (mouse_px - WINDOW_SIZE / 2.0) / PIXELS_PER_METER + y_m = (WINDOW_SIZE / 2.0 - mouse_py) / PIXELS_PER_METER + + # Distance calculation + dist_m = np.sqrt(x_m**2 + y_m**2) + + # Smooth exponential pitch scaling + clamped_dist = min(dist_m, MAX_MAPPED_DIST) + norm_dist = clamped_dist / MAX_MAPPED_DIST + target_freq = FREQ_MIN_DIST * ((FREQ_MAX_DIST / FREQ_MIN_DIST) ** norm_dist) + + # Spatial angle (Azimuth) + azimuth = np.arctan2(x_m, y_m) + + # ITD (Woodworth Model) + itd_sec = (HEAD_RADIUS_M / C_SOUND) * (np.sin(azimuth) + azimuth) + target_itd_samples = itd_sec * SAMPLE_RATE + + # ILD (Stereo Panning) + 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) + + # Visual Rendering + screen.fill((15, 18, 25)) + center_px = WINDOW_SIZE // 2 + + # Grid + pygame.draw.line(screen, (35, 40, 55), (0, center_px), (WINDOW_SIZE, center_px), 1) + pygame.draw.line(screen, (35, 40, 55), (center_px, 0), (center_px, WINDOW_SIZE), 1) + + # Distance circles + for r_m in range(2, 11, 2): + r_px = int(r_m * PIXELS_PER_METER) + pygame.draw.circle(screen, (30, 35, 50), (center_px, center_px), r_px, 1) + + # User head + head_radius_px = int(HEAD_RADIUS_M * 3 * PIXELS_PER_METER) + pygame.draw.circle(screen, (180, 190, 200), (center_px, center_px), head_radius_px) + pygame.draw.polygon(screen, (230, 90, 90), [ + (center_px - 8, center_px - head_radius_px), + (center_px + 8, center_px - head_radius_px), + (center_px, center_px - head_radius_px - 12) + ]) + + # Object + pygame.draw.circle(screen, (80, 220, 160), (mouse_px, mouse_py), 8) + pygame.draw.line(screen, (80, 220, 160, 80), (center_px, center_px), (mouse_px, mouse_py), 1) + + # HUD + 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, (200, 200, 210)) + 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 diff --git a/src/01simulation_multiple_objects.py b/src/01simulation_multiple_objects.py new file mode 100644 index 0000000..780ce08 --- /dev/null +++ b/src/01simulation_multiple_objects.py @@ -0,0 +1,248 @@ +import sys +import random +import numpy as np +import pygame +import sounddevice as sd + +# ==================== CONFIGURATION & PARAMETERS ==================== +ROOM_SIZE_M = 20.0 +WINDOW_SIZE = 800 +PIXELS_PER_METER = WINDOW_SIZE / ROOM_SIZE_M + +SAMPLE_RATE = 44100 +BLOCK_SIZE = 1024 +C_SOUND = 343.0 +HEAD_RADIUS_M = 0.0875 +MAX_MAPPED_DIST = 10.0 + +# Harmonic base multipliers (Musical Ratios) to distinguish multiple objects +CHORD_RATIOS = [1.0, 1.2, 1.498, 1.782, 2.0, 2.4] + + +# ==================== SOUND OBJECT CLASS ==================== +class SoundObject: + """Represents an active object moving in 2D space generating sound.""" + def __init__(self, x_m, y_m, base_ratio=1.0): + self.x_m = x_m + self.y_m = y_m + + # Smooth random velocity vector (meters per frame) + self.vx = random.uniform(-0.03, 0.03) + self.vy = random.uniform(-0.03, 0.03) + + # Unique musical ratio for ethereal distinction + self.base_ratio = base_ratio + + # Dynamic DSP parameters (updated frame by frame) + self.target_freq = 220.0 + self.target_itd_samples = 0.0 + self.target_gain_l = 0.15 + self.target_gain_r = 0.15 + + # Internal DSP state variables + self.current_freq = 220.0 + self.current_itd = 0.0 + self.phase_1 = random.uniform(0, 2 * np.pi) + self.phase_2 = random.uniform(0, 2 * np.pi) + + def update_physics(self): + """Move object and bounce off room boundaries (-10m to +10m).""" + self.x_m += self.vx + self.y_m += self.vy + + # Slightly perturb velocity to make movement organic + self.vx += random.uniform(-0.002, 0.002) + self.vy += random.uniform(-0.002, 0.002) + + # Clamp speed + speed = np.sqrt(self.vx**2 + self.vy**2) + if speed > 0.05: + self.vx = (self.vx / speed) * 0.05 + self.vy = (self.vy / speed) * 0.05 + + # Wall bounce limits (-10 to 10 m) + half_r = ROOM_SIZE_M / 2.0 - 0.5 + if abs(self.x_m) > half_r: + self.vx *= -1.0 + self.x_m = np.clip(self.x_m, -half_r, half_r) + if abs(self.y_m) > half_r: + self.vy *= -1.0 + self.y_m = np.clip(self.y_m, -half_r, half_r) + + def update_dsp_params(self): + """Compute frequency, ITD, and ILD based on distance and azimuth.""" + dist_m = np.sqrt(self.x_m**2 + self.y_m**2) + clamped_dist = min(dist_m, MAX_MAPPED_DIST) + norm_dist = clamped_dist / MAX_MAPPED_DIST + + # Pitch mapping: 880Hz (near) down to 220Hz (far), modified by object chord ratio + base_f_near = 880.0 * self.base_ratio + base_f_far = 220.0 * self.base_ratio + self.target_freq = base_f_near * ((base_f_far / base_f_near) ** norm_dist) + + # Spatial Azimuth + azimuth = np.arctan2(self.x_m, self.y_m) + + # ITD (Woodworth Model) + itd_sec = (HEAD_RADIUS_M / C_SOUND) * (np.sin(azimuth) + azimuth) + self.target_itd_samples = itd_sec * SAMPLE_RATE + + # ILD / Panning Gain + pan = np.sin(azimuth) + # Scaled down gain per object to prevent stereo master clipping + master_vol = 0.2 + self.target_gain_l = np.clip(0.5 * (1.0 - pan), 0.05, 1.0) * master_vol + self.target_gain_r = np.clip(0.5 * (1.0 + pan), 0.05, 1.0) * master_vol + + +# Active sound objects list (Thread-shared) +objects_list = [] + + +# ==================== MULTI-OBJECT AUDIO CALLBACK ==================== +def audio_callback(outdata, frames, time_info, status): + if status: + print(status, file=sys.stderr) + + # Initialize stereo output buffer + outdata.fill(0.0) + + if not objects_list: + return + + t_indices = np.arange(frames) + + # Sum audio contributions from all active objects + for obj in list(objects_list): + # Smooth vector parameter transitions + freq_vec = np.linspace(obj.current_freq, obj.target_freq, frames) + itd_vec = np.linspace(obj.current_itd, obj.target_itd_samples, frames) + + obj.current_freq = obj.target_freq + obj.current_itd = obj.target_itd_samples + + # Calculate phase accumulation for continuous smooth tones + dphase_1 = 2 * np.pi * freq_vec / SAMPLE_RATE + dphase_2 = 2 * np.pi * (freq_vec * 1.5) / SAMPLE_RATE # Perfect fifth shimmer + + phases_1 = obj.phase_1 + np.cumsum(dphase_1) + phases_2 = obj.phase_2 + np.cumsum(dphase_2) + + obj.phase_1 = phases_1[-1] % (2 * np.pi) + obj.phase_2 = phases_2[-1] % (2 * np.pi) + + # Ethereal Pad synthesis (Root sine + soft 5th) + wave_1 = np.sin(phases_1) + wave_2 = 0.25 * np.sin(phases_2) + raw_signal = 0.2 * (wave_1 + wave_2) + + # Fractional ITD shift + idx_l = t_indices + (itd_vec / 2.0) + idx_r = t_indices - (itd_vec / 2.0) + + sig_l = np.interp(idx_l, t_indices, raw_signal) * obj.target_gain_l + sig_r = np.interp(idx_r, t_indices, raw_signal) * obj.target_gain_r + + # Mix to master out + outdata[:, 0] += sig_l + outdata[:, 1] += sig_r + + +# ==================== MAIN APPLICATION ==================== +def main(): + pygame.init() + screen = pygame.display.set_mode((WINDOW_SIZE, WINDOW_SIZE)) + pygame.display.set_caption("Simulation: Multi-Object Ethereal Spatial Tracking") + clock = pygame.time.Clock() + + 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 + elif event.key == pygame.K_c: + # Press 'C' to clear all objects + objects_list.clear() + + elif event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1: # Left Mouse Click to drop object + m_px, m_py = event.pos + x_m = (m_px - WINDOW_SIZE / 2.0) / PIXELS_PER_METER + y_m = (WINDOW_SIZE / 2.0 - m_py) / PIXELS_PER_METER + + # Assign next musical ratio in round-robin fashion + ratio = CHORD_RATIOS[len(objects_list) % len(CHORD_RATIOS)] + new_obj = SoundObject(x_m, y_m, base_ratio=ratio) + objects_list.append(new_obj) + + # Update physics and audio mapping for all spawned objects + for obj in objects_list: + obj.update_physics() + obj.update_dsp_params() + + # --- RENDERING --- + screen.fill((15, 18, 25)) + center_px = WINDOW_SIZE // 2 + + # Coordinate Grid + pygame.draw.line(screen, (35, 40, 55), (0, center_px), (WINDOW_SIZE, center_px), 1) + pygame.draw.line(screen, (35, 40, 55), (center_px, 0), (center_px, WINDOW_SIZE), 1) + + # Distance Rings + for r_m in range(2, 11, 2): + r_px = int(r_m * PIXELS_PER_METER) + pygame.draw.circle(screen, (30, 35, 50), (center_px, center_px), r_px, 1) + + # Head/User representation + head_radius_px = int(HEAD_RADIUS_M * 3 * PIXELS_PER_METER) + pygame.draw.circle(screen, (180, 190, 200), (center_px, center_px), head_radius_px) + pygame.draw.polygon(screen, (230, 90, 90), [ + (center_px - 8, center_px - head_radius_px), + (center_px + 8, center_px - head_radius_px), + (center_px, center_px - head_radius_px - 12) + ]) + + # Draw spawned moving objects + for i, obj in enumerate(objects_list): + o_px = int(center_px + obj.x_m * PIXELS_PER_METER) + o_py = int(center_px - obj.y_m * PIXELS_PER_METER) + + # Vector line to user + pygame.draw.line(screen, (80, 220, 160, 60), (center_px, center_px), (o_px, o_py), 1) + + # Glowing object body + pygame.draw.circle(screen, (100, 255, 180), (o_px, o_py), 7) + pygame.draw.circle(screen, (80, 220, 160), (o_px, o_py), 12, 1) + + # HUD Instructions + font = pygame.font.SysFont("Consolas", 15) + hud_info = [ + f"Active Objects: {len(objects_list)}", + "[ Left Click ] : Drop new moving sound object", + "[ Key 'C' ] : Clear all sound objects", + "[ ESC ] : Exit Simulation" + ] + + for idx, text in enumerate(hud_info): + txt_surface = font.render(text, True, (200, 200, 210)) + screen.blit(txt_surface, (15, 15 + idx * 20)) + + pygame.display.flip() + clock.tick(60) + + pygame.quit() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/02simulation_multiple_objects_fused.py b/src/02simulation_multiple_objects_fused.py new file mode 100644 index 0000000..c4044bf --- /dev/null +++ b/src/02simulation_multiple_objects_fused.py @@ -0,0 +1,311 @@ +import sys +import random +import numpy as np +import pygame +import sounddevice as sd + +# ==================== KONFIGURATION & PARAMETER ==================== +ROOM_SIZE_M = 20.0 +WINDOW_SIZE = 800 +PIXELS_PER_METER = WINDOW_SIZE / ROOM_SIZE_M + +SAMPLE_RATE = 44100 +BLOCK_SIZE = 1024 +C_SOUND = 343.0 +HEAD_RADIUS_M = 0.0875 +MAX_MAPPED_DIST = 10.0 + +# Schwellenwert in Metern für die Verbindung/Fusion von Punkten zu einem Körper +CLUSTER_THRESHOLD_M = 1.8 + +# Musikalische Intervalle zur Unterscheidung verschiedener fused Körper +CHORD_RATIOS = [1.0, 1.2, 1.498, 1.782, 2.0, 2.4] + + +# ==================== PUNKTE (POINT OBJECTS) ==================== +class MovingPoint: + """Repräsentiert einen einzelnen physikalischen Punkt im Raum.""" + def __init__(self, x_m, y_m): + self.x_m = x_m + self.y_m = y_m + self.vx = random.uniform(-0.025, 0.025) + self.vy = random.uniform(-0.025, 0.025) + + def update_physics(self): + """Autonome Bewegung und Kollision mit den Raumgrenzen.""" + self.x_m += self.vx + self.y_m += self.vy + + self.vx += random.uniform(-0.001, 0.001) + self.vy += random.uniform(-0.001, 0.001) + + speed = np.sqrt(self.vx**2 + self.vy**2) + if speed > 0.04: + self.vx = (self.vx / speed) * 0.04 + self.vy = (self.vy / speed) * 0.04 + + half_r = ROOM_SIZE_M / 2.0 - 0.5 + if abs(self.x_m) > half_r: + self.vx *= -1.0 + self.x_m = np.clip(self.x_m, -half_r, half_r) + if abs(self.y_m) > half_r: + self.vy *= -1.0 + self.y_m = np.clip(self.y_m, -half_r, half_r) + + +# ==================== FUSED BODY (ZUSAMMENGESETZTER KÖRPER) ==================== +class FusedBody: + """Repräsentiert einen dynamischen Körper (1 oder mehrere verschmolzene Punkte).""" + def __init__(self, points, base_ratio=1.0): + self.points = points # Liste von MovingPoint-Objekten + self.base_ratio = base_ratio + + # Audio-Mapping-Parameter (basiert auf dem NÄCHSTEN Punkt zum Nutzer) + self.closest_point = None + self.target_freq = 220.0 + self.target_itd_samples = 0.0 + self.target_gain_l = 0.15 + self.target_gain_r = 0.15 + + # Interne Audio-States für stufenlosen Klang + self.current_freq = 220.0 + self.current_itd = 0.0 + self.phase_1 = random.uniform(0, 2 * np.pi) + self.phase_2 = random.uniform(0, 2 * np.pi) + + def update_audio_params(self): + """Findet den nächsten Punkt zum Ursprung (Kopf) und berechnet 1 Welle.""" + if not self.points: + return + + # Nächstgelegenen Punkt des Körpers zum Kopf (0,0) ermitteln + self.closest_point = min( + self.points, + key=lambda p: np.sqrt(p.x_m**2 + p.y_m**2) + ) + + dist_m = np.sqrt(self.closest_point.x_m**2 + self.closest_point.y_m**2) + clamped_dist = min(dist_m, MAX_MAPPED_DIST) + norm_dist = clamped_dist / MAX_MAPPED_DIST + + # Frequenz-Mapping (nah = hoch, fern = tief) + base_f_near = 880.0 * self.base_ratio + base_f_far = 220.0 * self.base_ratio + self.target_freq = base_f_near * ((base_f_far / base_f_near) ** norm_dist) + + # Azimut bezogen auf den nächsten Punkt + azimuth = np.arctan2(self.closest_point.x_m, self.closest_point.y_m) + + # ITD & ILD Berechnung + itd_sec = (HEAD_RADIUS_M / C_SOUND) * (np.sin(azimuth) + azimuth) + self.target_itd_samples = itd_sec * SAMPLE_RATE + + pan = np.sin(azimuth) + master_vol = 0.22 + self.target_gain_l = np.clip(0.5 * (1.0 - pan), 0.05, 1.0) * master_vol + self.target_gain_r = np.clip(0.5 * (1.0 + pan), 0.05, 1.0) * master_vol + + +# ==================== GLOBALE VARIABLEN & CLUSTER-LOGIK ==================== +points_list = [] +fused_bodies = [] + + +def update_clusters(): + """Identifiziert nahe beieinander liegende Punkte und verschmilzt sie zu FusedBody-Objekten.""" + global fused_bodies + + n = len(points_list) + if n == 0: + fused_bodies = [] + return + + # Adjazenzmatrix zur Graph-Cluster-Erkennung + visited = [False] * n + clusters = [] + + for i in range(n): + if not visited[i]: + cluster = [] + queue = [i] + visited[i] = True + + while queue: + curr = queue.pop(0) + cluster.append(points_list[curr]) + + for neighbor in range(n): + if not visited[neighbor]: + dx = points_list[curr].x_m - points_list[neighbor].x_m + dy = points_list[curr].y_m - points_list[neighbor].y_m + dist = np.sqrt(dx**2 + dy**2) + + if dist <= CLUSTER_THRESHOLD_M: + visited[neighbor] = True + queue.append(neighbor) + + clusters.append(cluster) + + # Zuordnung zu bestehenden FusedBody-Objekten oder Neuerstellung + new_fused_bodies = [] + for idx, cluster_points in enumerate(clusters): + ratio = CHORD_RATIOS[idx % len(CHORD_RATIOS)] + body = FusedBody(cluster_points, base_ratio=ratio) + body.update_audio_params() + new_fused_bodies.append(body) + + fused_bodies = new_fused_bodies + + +# ==================== AUDIO CALLBACK ==================== +def audio_callback(outdata, frames, time_info, status): + if status: + print(status, file=sys.stderr) + + outdata.fill(0.0) + if not fused_bodies: + return + + t_indices = np.arange(frames) + + # Rendere exakt EIN Signal pro zusammengesetztem Körper (Cluster) + for body in list(fused_bodies): + freq_vec = np.linspace(body.current_freq, body.target_freq, frames) + itd_vec = np.linspace(body.current_itd, body.target_itd_samples, frames) + + body.current_freq = body.target_freq + body.current_itd = body.target_itd_samples + + dphase_1 = 2 * np.pi * freq_vec / SAMPLE_RATE + dphase_2 = 2 * np.pi * (freq_vec * 1.498) / SAMPLE_RATE + + phases_1 = body.phase_1 + np.cumsum(dphase_1) + phases_2 = body.phase_2 + np.cumsum(dphase_2) + + body.phase_1 = phases_1[-1] % (2 * np.pi) + body.phase_2 = phases_2[-1] % (2 * np.pi) + + wave_1 = np.sin(phases_1) + wave_2 = 0.25 * np.sin(phases_2) + raw_signal = 0.18 * (wave_1 + wave_2) + + idx_l = t_indices + (itd_vec / 2.0) + idx_r = t_indices - (itd_vec / 2.0) + + sig_l = np.interp(idx_l, t_indices, raw_signal) * body.target_gain_l + sig_r = np.interp(idx_r, t_indices, raw_signal) * body.target_gain_r + + outdata[:, 0] += sig_l + outdata[:, 1] += sig_r + + +# ==================== HAUPTPROGRAMM ==================== +def main(): + pygame.init() + screen = pygame.display.set_mode((WINDOW_SIZE, WINDOW_SIZE)) + pygame.display.set_caption("Simulation 02: Dynamic Object Fusion & Clustered Audio Wave") + clock = pygame.time.Clock() + + 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 + elif event.key == pygame.K_c: + points_list.clear() + + elif event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1: + m_px, m_py = event.pos + x_m = (m_px - WINDOW_SIZE / 2.0) / PIXELS_PER_METER + y_m = (WINDOW_SIZE / 2.0 - m_py) / PIXELS_PER_METER + points_list.append(MovingPoint(x_m, y_m)) + + # 1. Punkte bewegen + for pt in points_list: + pt.update_physics() + + # 2. Cluster und dynamische Körper berechnen + update_clusters() + + # --- RENDERING --- + screen.fill((15, 18, 25)) + center_px = WINDOW_SIZE // 2 + + # Raster & Abstandskreise + pygame.draw.line(screen, (35, 40, 55), (0, center_px), (WINDOW_SIZE, center_px), 1) + pygame.draw.line(screen, (35, 40, 55), (center_px, 0), (center_px, WINDOW_SIZE), 1) + + for r_m in range(2, 11, 2): + r_px = int(r_m * PIXELS_PER_METER) + pygame.draw.circle(screen, (30, 35, 50), (center_px, center_px), r_px, 1) + + # Nutzer-Kopf + head_radius_px = int(HEAD_RADIUS_M * 3 * PIXELS_PER_METER) + pygame.draw.circle(screen, (180, 190, 200), (center_px, center_px), head_radius_px) + pygame.draw.polygon(screen, (230, 90, 90), [ + (center_px - 8, center_px - head_radius_px), + (center_px + 8, center_px - head_radius_px), + (center_px, center_px - head_radius_px - 12) + ]) + + # 3. Rote Verbindungsstriche zwischen zusammengehörigen Punkten zeichnen + for body in fused_bodies: + pts = body.points + # Verbinde nahe Punkte innerhalb desselben Körpers rot + for i in range(len(pts)): + for j in range(i + 1, len(pts)): + dx = pts[i].x_m - pts[j].x_m + dy = pts[i].y_m - pts[j].y_m + if np.sqrt(dx**2 + dy**2) <= CLUSTER_THRESHOLD_M: + px1 = int(center_px + pts[i].x_m * PIXELS_PER_METER) + py1 = int(center_px - pts[i].y_m * PIXELS_PER_METER) + px2 = int(center_px + pts[j].x_m * PIXELS_PER_METER) + py2 = int(center_px - pts[j].y_m * PIXELS_PER_METER) + pygame.draw.line(screen, (240, 60, 60), (px1, py1), (px2, py2), 3) + + # Richtungsvektor zum nächstgelegenen Punkt des Körpers (aktiver Schallgeber) + if body.closest_point: + cp_px = int(center_px + body.closest_point.x_m * PIXELS_PER_METER) + cp_py = int(center_px - body.closest_point.y_m * PIXELS_PER_METER) + pygame.draw.line(screen, (80, 220, 160, 80), (center_px, center_px), (cp_px, cp_py), 1) + + # 4. Punkte selbst zeichnen + for pt in points_list: + px = int(center_px + pt.x_m * PIXELS_PER_METER) + py = int(center_px - pt.y_m * PIXELS_PER_METER) + pygame.draw.circle(screen, (100, 255, 180), (px, py), 6) + + # HUD + font = pygame.font.SysFont("Consolas", 15) + hud_info = [ + f"Punkte gesamt : {len(points_list)}", + f"Aktive Körper : {len(fused_bodies)} (Waves)", + "[ Links-Klick ] : Punkt droppen", + "[ Taste 'C' ] : Alle Punkte löschen", + "[ ESC ] : Beenden" + ] + + for idx, text in enumerate(hud_info): + txt_surface = font.render(text, True, (200, 200, 210)) + screen.blit(txt_surface, (15, 15 + idx * 20)) + + pygame.display.flip() + clock.tick(60) + + pygame.quit() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/simulation_distanzerkennung.py b/src/simulation_distanzerkennung.py deleted file mode 100644 index f7a5bb2..0000000 --- a/src/simulation_distanzerkennung.py +++ /dev/null @@ -1,170 +0,0 @@ -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