-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscene_manager.py
More file actions
232 lines (198 loc) · 9.04 KB
/
scene_manager.py
File metadata and controls
232 lines (198 loc) · 9.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
"""
Scene Manager for PyRoomStudio.
Manages sound sources, listeners, and their 3D positions in the scene.
"""
import numpy as np
from typing import List, Optional, Tuple
from dataclasses import dataclass
import json
from datetime import datetime
@dataclass
class SoundSource:
"""Represents a sound source in the 3D scene"""
position: np.ndarray # [x, y, z] in world coordinates
audio_file: str # Path to audio file
volume: float = 1.0 # Volume multiplier (0.0 to 1.0)
name: str = "" # User-friendly name
marker_color: Tuple[int, int, int] = (255, 100, 100) # Red-ish
def __post_init__(self):
"""Ensure position is numpy array"""
if not isinstance(self.position, np.ndarray):
self.position = np.array(self.position, dtype=np.float32)
if not self.name:
self.name = f"Sound Source {id(self) % 1000}"
def to_dict(self):
"""Serialize to dictionary for saving"""
return {
'position': self.position.tolist(),
'audio_file': self.audio_file,
'volume': self.volume,
'name': self.name,
'marker_color': self.marker_color
}
@classmethod
def from_dict(cls, data):
"""Deserialize from dictionary"""
return cls(
position=np.array(data['position'], dtype=np.float32),
audio_file=data['audio_file'],
volume=data.get('volume', 1.0),
name=data.get('name', ''),
marker_color=tuple(data.get('marker_color', (255, 100, 100)))
)
@dataclass
class Listener:
"""Represents a listener/microphone in the 3D scene"""
position: np.ndarray # [x, y, z] in world coordinates
name: str = "" # User-friendly name
orientation: Optional[np.ndarray] = None # Optional direction vector for directional mics
marker_color: Tuple[int, int, int] = (100, 100, 255) # Blue-ish
def __post_init__(self):
"""Ensure position is numpy array"""
if not isinstance(self.position, np.ndarray):
self.position = np.array(self.position, dtype=np.float32)
if self.orientation is not None and not isinstance(self.orientation, np.ndarray):
self.orientation = np.array(self.orientation, dtype=np.float32)
if not self.name:
self.name = f"Listener {id(self) % 1000}"
def to_dict(self):
"""Serialize to dictionary for saving"""
return {
'position': self.position.tolist(),
'name': self.name,
'orientation': self.orientation.tolist() if self.orientation is not None else None,
'marker_color': self.marker_color
}
@classmethod
def from_dict(cls, data):
"""Deserialize from dictionary"""
orientation = data.get('orientation')
if orientation is not None:
orientation = np.array(orientation, dtype=np.float32)
return cls(
position=np.array(data['position'], dtype=np.float32),
name=data.get('name', ''),
orientation=orientation,
marker_color=tuple(data.get('marker_color', (100, 100, 255)))
)
class SceneManager:
"""Manages the acoustic scene with sound sources and listeners"""
def __init__(self):
self.sound_sources: List[SoundSource] = []
self.listeners: List[Listener] = []
self.selected_source_index: Optional[int] = None
self.selected_listener_index: Optional[int] = None
def add_sound_source(self, position: np.ndarray, audio_file: str,
volume: float = 1.0, name: str = "") -> SoundSource:
"""Add a sound source to the scene"""
source = SoundSource(position, audio_file, volume, name)
self.sound_sources.append(source)
print(f"Added sound source at {position} with audio: {audio_file}")
return source
def add_listener(self, position: np.ndarray, name: str = "",
orientation: Optional[np.ndarray] = None) -> Listener:
"""Add a listener to the scene"""
listener = Listener(position, name, orientation)
self.listeners.append(listener)
print(f"Added listener at {position}")
return listener
def remove_sound_source(self, index: int) -> bool:
"""Remove a sound source by index"""
if 0 <= index < len(self.sound_sources):
removed = self.sound_sources.pop(index)
print(f"Removed sound source: {removed.name}")
if self.selected_source_index == index:
self.selected_source_index = None
elif self.selected_source_index is not None and self.selected_source_index > index:
self.selected_source_index -= 1
return True
return False
def remove_listener(self, index: int) -> bool:
"""Remove a listener by index"""
if 0 <= index < len(self.listeners):
removed = self.listeners.pop(index)
print(f"Removed listener: {removed.name}")
if self.selected_listener_index == index:
self.selected_listener_index = None
elif self.selected_listener_index is not None and self.selected_listener_index > index:
self.selected_listener_index -= 1
return True
return False
def clear_all(self):
"""Remove all sources and listeners"""
self.sound_sources.clear()
self.listeners.clear()
self.selected_source_index = None
self.selected_listener_index = None
print("Cleared all sound sources and listeners")
def get_sound_source(self, index: int) -> Optional[SoundSource]:
"""Get a sound source by index"""
if 0 <= index < len(self.sound_sources):
return self.sound_sources[index]
return None
def get_listener(self, index: int) -> Optional[Listener]:
"""Get a listener by index"""
if 0 <= index < len(self.listeners):
return self.listeners[index]
return None
def has_minimum_objects(self) -> bool:
"""Check if scene has minimum required objects for simulation"""
return len(self.sound_sources) >= 1 and len(self.listeners) >= 1
def get_all_positions(self) -> Tuple[List[np.ndarray], List[np.ndarray]]:
"""Get all source and listener positions"""
source_positions = [source.position for source in self.sound_sources]
listener_positions = [listener.position for listener in self.listeners]
return source_positions, listener_positions
def select_source(self, index: Optional[int]):
"""Select a sound source (for editing/deletion)"""
if index is None or 0 <= index < len(self.sound_sources):
self.selected_source_index = index
self.selected_listener_index = None # Deselect listeners
def select_listener(self, index: Optional[int]):
"""Select a listener (for editing/deletion)"""
if index is None or 0 <= index < len(self.listeners):
self.selected_listener_index = index
self.selected_source_index = None # Deselect sources
def get_selected_object(self) -> Optional[Tuple[str, int]]:
"""Get the currently selected object type and index"""
if self.selected_source_index is not None:
return ("source", self.selected_source_index)
elif self.selected_listener_index is not None:
return ("listener", self.selected_listener_index)
return None
def delete_selected(self) -> bool:
"""Delete the currently selected object"""
if self.selected_source_index is not None:
return self.remove_sound_source(self.selected_source_index)
elif self.selected_listener_index is not None:
return self.remove_listener(self.selected_listener_index)
return False
def save_to_file(self, filepath: str):
"""Save scene to JSON file"""
data = {
'version': '1.0',
'timestamp': datetime.now().isoformat(),
'sound_sources': [source.to_dict() for source in self.sound_sources],
'listeners': [listener.to_dict() for listener in self.listeners]
}
with open(filepath, 'w') as f:
json.dump(data, f, indent=2)
print(f"Saved scene to {filepath}")
def load_from_file(self, filepath: str):
"""Load scene from JSON file"""
with open(filepath, 'r') as f:
data = json.load(f)
self.clear_all()
# Load sound sources
for source_data in data.get('sound_sources', []):
source = SoundSource.from_dict(source_data)
self.sound_sources.append(source)
# Load listeners
for listener_data in data.get('listeners', []):
listener = Listener.from_dict(listener_data)
self.listeners.append(listener)
print(f"Loaded scene from {filepath}: {len(self.sound_sources)} sources, {len(self.listeners)} listeners")
def get_summary(self) -> str:
"""Get a text summary of the scene"""
return (f"Scene: {len(self.sound_sources)} sound source(s), "
f"{len(self.listeners)} listener(s)")