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()