developped 3 prototypes
This commit is contained in:
parent
e90e7b831b
commit
1265dae0be
BIN
pics/prototyp_distanzerkennung_cropped.png
Normal file
BIN
pics/prototyp_distanzerkennung_cropped.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
BIN
pics/prototyp_multiobjekt_distanzerkennung.png
Normal file
BIN
pics/prototyp_multiobjekt_distanzerkennung.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 145 KiB |
184
src/00simulation_distanzerkennung.py
Normal file
184
src/00simulation_distanzerkennung.py
Normal file
@ -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()
|
||||||
248
src/01simulation_multiple_objects.py
Normal file
248
src/01simulation_multiple_objects.py
Normal file
@ -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()
|
||||||
311
src/02simulation_multiple_objects_fused.py
Normal file
311
src/02simulation_multiple_objects_fused.py
Normal file
@ -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()
|
||||||
@ -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()
|
|
||||||
Loading…
x
Reference in New Issue
Block a user