EG/plugins/user/spatial_audio/editor/audio_editor.py
2025-12-12 16:16:15 +08:00

552 lines
19 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
音频编辑器
提供可视化编辑音频场景和参数的功能
"""
from typing import Dict, List, Any, Optional
from panda3d.core import NodePath, Vec3
class AudioEditor:
"""
音频编辑器
提供可视化编辑音频场景和参数的功能
"""
def __init__(self, plugin, world):
"""
初始化音频编辑器
Args:
plugin: 3D音频插件实例
world: 3D世界实例
"""
self.plugin = plugin
self.world = world
self.visible = False
self.editor_node = None
self.zone_visualizations = {}
self.sound_visualizations = {}
self.selected_item = None
# 编辑器状态
self.edit_mode = False
self.selected_tool = "select" # select, add_sound, add_zone
self.grid_size = 5.0
self.snap_to_grid = True
# GUI元素
self.gui_elements = []
self.tool_buttons = {}
def toggle_visibility(self):
"""切换编辑器可见性"""
if self.visible:
self.hide()
else:
self.show()
def show(self):
"""显示编辑器"""
if self.visible:
return
self.visible = True
self._create_editor_visualization()
self._create_editor_gui()
print("✓ 音频编辑器已显示")
def hide(self):
"""隐藏编辑器"""
if not self.visible:
return
self.visible = False
self._cleanup_editor_visualization()
self._cleanup_editor_gui()
print("✓ 音频编辑器已隐藏")
def _create_editor_visualization(self):
"""创建编辑器可视化元素"""
try:
if not self.world.render:
return
# 创建编辑器根节点
self.editor_node = self.world.render.attachNewNode("audio_editor")
# 可视化现有的音频区域
self._visualize_zones()
# 可视化现有的音频源
self._visualize_sounds()
except Exception as e:
print(f"⚠ 编辑器可视化创建失败: {e}")
def _cleanup_editor_visualization(self):
"""清理编辑器可视化元素"""
try:
# 清理区域可视化
for zone_node in self.zone_visualizations.values():
zone_node.removeNode()
self.zone_visualizations.clear()
# 清理音源可视化
for sound_node in self.sound_visualizations.values():
sound_node.removeNode()
self.sound_visualizations.clear()
# 清理编辑器根节点
if self.editor_node:
self.editor_node.removeNode()
self.editor_node = None
except Exception as e:
print(f"⚠ 编辑器可视化清理失败: {e}")
def _visualize_zones(self):
"""可视化音频区域"""
if not self.editor_node:
return
# 获取所有区域信息
zones_info = self.plugin.environment_audio_manager.get_all_zones_info()
for zone_info in zones_info:
zone_id = zone_info['id']
position = zone_info['position']
radius = zone_info['radius']
# 创建区域可视化节点
zone_node = self.editor_node.attachNewNode(f"zone_{zone_id}")
zone_node.setPos(position[0], position[1], position[2])
# 这里应该创建一个球体或其他可视化表示
# 为简化起见,我们只是存储节点
self.zone_visualizations[zone_id] = zone_node
def _visualize_sounds(self):
"""可视化音频源"""
if not self.editor_node:
return
# 获取所有音频源
for sound_id, sound_info in self.plugin.audio_source_manager.audio_sources.items():
position = sound_info['position']
# 创建音源可视化节点
sound_node = self.editor_node.attachNewNode(f"sound_{sound_id}")
sound_node.setPos(position[0], position[1], position[2])
# 这里应该创建一个图标或其他可视化表示
# 为简化起见,我们只是存储节点
self.sound_visualizations[sound_id] = sound_node
def _create_editor_gui(self):
"""创建编辑器GUI"""
try:
if not hasattr(self.world, 'gui_manager') or not self.world.gui_manager:
print("⚠ GUI管理器不可用无法创建编辑器界面")
return
gui_manager = self.world.gui_manager
# 创建编辑器面板标题
panel = gui_manager.createGUIButton(
pos=(0.7, 0, 0.95),
text="音频编辑器",
size=0.06
)
self.gui_elements.append(panel)
# 创建工具按钮
tools = [
("选择", "select"),
("添加音效", "add_sound"),
("添加区域", "add_zone")
]
for i, (name, tool) in enumerate(tools):
button = gui_manager.createGUIButton(
pos=(0.7, 0, 0.88 - i * 0.06),
text=name,
size=0.04
)
self.gui_elements.append(button)
self.tool_buttons[tool] = button
self.world.accept(f"audio_tool_{tool}",
lambda t=tool: self.set_edit_tool(t))
# 创建编辑功能按钮
functions = [
("网格对齐", self.toggle_grid_snap),
("播放测试音", self.play_test_sound),
("环境音效", self.toggle_environment_audio),
("保存场景", self.save_audio_scene),
("加载场景", self.load_audio_scene)
]
for i, (name, callback) in enumerate(functions):
button = gui_manager.createGUIButton(
pos=(0.82, 0, 0.88 - i * 0.06),
text=name,
size=0.04
)
self.gui_elements.append(button)
self.world.accept(f"audio_func_{name}", callback)
except Exception as e:
print(f"⚠ 编辑器GUI创建失败: {e}")
def _cleanup_editor_gui(self):
"""清理编辑器GUI"""
try:
if hasattr(self.world, 'gui_manager') and self.world.gui_manager:
gui_manager = self.world.gui_manager
for element in self.gui_elements:
gui_manager.deleteGUIElement(element)
# 清理事件监听
tools = ["select", "add_sound", "add_zone"]
for tool in tools:
self.world.ignore(f"audio_tool_{tool}")
functions = [
"网格对齐", "播放测试音", "环境音效",
"保存场景", "加载场景"
]
for func in functions:
self.world.ignore(f"audio_func_{func}")
self.gui_elements.clear()
self.tool_buttons.clear()
except Exception as e:
print(f"⚠ 编辑器GUI清理失败: {e}")
def set_edit_tool(self, tool: str):
"""设置编辑工具"""
self.selected_tool = tool
print(f"✓ 切换到 {tool} 工具")
# 更新按钮状态
for tool_name, button in self.tool_buttons.items():
if tool_name == tool:
# 高亮选中工具
button.setColor(0.5, 1.0, 0.5, 1.0) # 绿色
else:
# 恢复默认颜色
button.setColor(1.0, 1.0, 1.0, 1.0) # 白色
def toggle_grid_snap(self):
"""切换网格对齐"""
self.snap_to_grid = not self.snap_to_grid
print(f"✓ 网格对齐已{'启用' if self.snap_to_grid else '禁用'}")
def play_test_sound(self):
"""播放测试音效"""
print("✓ 播放测试音效")
def toggle_environment_audio(self):
"""切换环境音频"""
self.plugin.environment_audio_manager.toggle_enabled()
print("✓ 环境音频切换")
def save_audio_scene(self):
"""保存音频场景"""
print("✓ 保存音频场景")
def load_audio_scene(self):
"""加载音频场景"""
print("✓ 加载音频场景")
def handle_mouse_click(self, mouse_x: float, mouse_y: float):
"""处理鼠标点击事件"""
if not self.edit_mode:
return
# 将屏幕坐标转换为世界坐标
world_pos = self._screen_to_world(mouse_x, mouse_y)
if not world_pos:
return
# 根据当前工具执行操作
if self.selected_tool == "add_sound":
self._add_sound_at_position(world_pos)
elif self.selected_tool == "add_zone":
self._add_zone_at_position(world_pos)
elif self.selected_tool == "select":
self._select_item_at_position(world_pos)
def _screen_to_world(self, screen_x: float, screen_y: float) -> Optional[Vec3]:
"""
将屏幕坐标转换为世界坐标
Args:
screen_x: 屏幕X坐标 (-1 到 1)
screen_y: 屏幕Y坐标 (-1 到 1)
Returns:
世界坐标如果转换失败则返回None
"""
# 这是一个简化的实现
# 实际应用中需要使用摄像机的射线检测
# 简单地将屏幕坐标映射到XZ平面
x = screen_x * 20 # 假设场景大小为40x40
z = screen_y * 20
y = 0 # 在地面高度
# 网格对齐
if self.snap_to_grid:
x = round(x / self.grid_size) * self.grid_size
z = round(z / self.grid_size) * self.grid_size
return Vec3(x, y, z)
def _add_sound_at_position(self, position: Vec3):
"""
在指定位置添加音效
Args:
position: 位置
"""
print(f"✓ 在位置 {position} 添加音效")
# 这里应该打开一个对话框让用户选择音频文件等参数
def _add_zone_at_position(self, position: Vec3):
"""
在指定位置添加区域
Args:
position: 位置
"""
print(f"✓ 在位置 {position} 添加音频区域")
# 这里应该打开一个对话框让用户设置区域参数
def _select_item_at_position(self, position: Vec3):
"""
选择指定位置的项目
Args:
position: 位置
"""
print(f"✓ 选择位置 {position} 的项目")
# 这里应该实现项目选择逻辑
def toggle_edit_mode(self):
"""切换编辑模式"""
self.edit_mode = not self.edit_mode
if self.edit_mode:
print("✓ 进入音频编辑模式")
else:
print("✓ 退出音频编辑模式")
def update(self, dt: float):
"""更新编辑器状态"""
# 处理键盘输入
if self.world.mouseWatcherNode.hasMouse():
mouse_pos = self.world.mouseWatcherNode.getMouse()
# 检查鼠标按键
from panda3d.core import MouseWatcher
if self.world.mouseWatcherNode.isButtonDown(MouseWatcher.getGlobalPointer()):
self.handle_mouse_click(mouse_pos.x, mouse_pos.y)
# 检查键盘快捷键
from panda3d.core import KeyboardButton
if self.world.mouseWatcherNode.isButtonDown(KeyboardButton.asciiKey('e')):
self.toggle_edit_mode()
def cleanup(self):
"""清理编辑器资源"""
self.hide()
self.plugin = None
self.world = None
self.selected_item = None
class AudioSceneSerializer:
"""
音频场景序列化器
处理音频场景的保存和加载
"""
def __init__(self, plugin):
"""
初始化序列化器
Args:
plugin: 3D音频插件实例
"""
self.plugin = plugin
def save_scene(self, filepath: str) -> bool:
"""
保存音频场景
Args:
filepath: 文件路径
Returns:
是否保存成功
"""
try:
import json
# 收集场景数据
scene_data = {
'version': '1.0',
'zones': self._collect_zones_data(),
'sounds': self._collect_sounds_data(),
'effects': self._collect_effects_data()
}
# 保存到文件
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(scene_data, f, indent=2, ensure_ascii=False)
print(f"✓ 音频场景已保存到: {filepath}")
return True
except Exception as e:
print(f"✗ 保存音频场景失败: {e}")
return False
def load_scene(self, filepath: str) -> bool:
"""
加载音频场景
Args:
filepath: 文件路径
Returns:
是否加载成功
"""
try:
import json
# 从文件加载
with open(filepath, 'r', encoding='utf-8') as f:
scene_data = json.load(f)
# 应用场景数据
self._apply_zones_data(scene_data.get('zones', []))
self._apply_sounds_data(scene_data.get('sounds', []))
self._apply_effects_data(scene_data.get('effects', []))
print(f"✓ 音频场景已从 {filepath} 加载")
return True
except Exception as e:
print(f"✗ 加载音频场景失败: {e}")
return False
def _collect_zones_data(self) -> List[Dict[str, Any]]:
"""收集区域数据"""
zones_data = []
zones_info = self.plugin.environment_audio_manager.get_all_zones_info()
for zone_info in zones_info:
zone_id = zone_info['id']
# 获取区域的完整信息
zone = self.plugin.environment_audio_manager.zones[zone_id]
zone_data = {
'id': zone_id,
'position': zone['position'],
'radius': zone['radius'],
'properties': zone['properties'],
'active': zone['active']
}
zones_data.append(zone_data)
return zones_data
def _collect_sounds_data(self) -> List[Dict[str, Any]]:
"""收集音效数据"""
sounds_data = []
for sound_id, sound_info in self.plugin.audio_source_manager.audio_sources.items():
sound_data = {
'id': sound_id,
'filepath': sound_info['filepath'],
'type': sound_info['type'],
'position': sound_info['position'],
'loop': sound_info['loop'],
'volume': sound_info['volume']
}
sounds_data.append(sound_data)
return sounds_data
def _collect_effects_data(self) -> List[Dict[str, Any]]:
"""收集效果数据"""
effects_data = []
for effect_id, effect_info in self.plugin.audio_effect_processor.effects.items():
effect_data = {
'id': effect_id,
'type': effect_info['type'],
'parameters': effect_info['parameters'],
'active': effect_info['active']
}
effects_data.append(effect_data)
return effects_data
def _apply_zones_data(self, zones_data: List[Dict[str, Any]]):
"""应用区域数据"""
for zone_data in zones_data:
zone_id = zone_data['id']
position = zone_data['position']
radius = zone_data['radius']
properties = zone_data['properties']
# 创建区域
self.plugin.environment_audio_manager.create_zone(
zone_id, position, radius, properties)
# 设置活动状态
if not zone_data.get('active', True):
self.plugin.environment_audio_manager.disable_zone(zone_id)
def _apply_sounds_data(self, sounds_data: List[Dict[str, Any]]):
"""应用音效数据"""
for sound_data in sounds_data:
sound_id = sound_data['id']
filepath = sound_data['filepath']
sound_type = sound_data['type']
position = sound_data['position']
loop = sound_data['loop']
volume = sound_data['volume']
# 创建音效
if sound_type == '3d':
new_sound_id = self.plugin.audio_source_manager.create_3d_sound(
filepath, position, loop, volume)
else:
new_sound_id = self.plugin.audio_source_manager.create_2d_sound(
filepath, loop, volume)
# 注意由于ID可能已更改需要处理关联关系
if new_sound_id:
# 播放音效
self.plugin.audio_source_manager.play_sound(new_sound_id)
def _apply_effects_data(self, effects_data: List[Dict[str, Any]]):
"""应用效果数据"""
for effect_data in effects_data:
effect_id = effect_data['id']
effect_type = effect_data['type']
parameters = effect_data['parameters']
# 创建效果
new_effect_id = self.plugin.audio_effect_processor.create_effect(
effect_type, parameters)
# 设置活动状态
if not effect_data.get('active', True):
if new_effect_id:
self.plugin.audio_effect_processor.disable_effect(new_effect_id)