552 lines
19 KiB
Python
552 lines
19 KiB
Python
"""
|
||
音频编辑器
|
||
提供可视化编辑音频场景和参数的功能
|
||
"""
|
||
|
||
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) |