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

329 lines
9.0 KiB
Python
Raw 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.

"""
音频工具类
提供音频处理相关的实用工具函数
"""
import math
from typing import Tuple, List, Dict, Any
from panda3d.core import Vec3
def calculate_distance(pos1: Tuple[float, float, float],
pos2: Tuple[float, float, float]) -> float:
"""
计算两点间距离
Args:
pos1: 第一个点 (x, y, z)
pos2: 第二个点 (x, y, z)
Returns:
距离
"""
dx = pos1[0] - pos2[0]
dy = pos1[1] - pos2[1]
dz = pos1[2] - pos2[2]
return math.sqrt(dx*dx + dy*dy + dz*dz)
def calculate_volume_attenuation(distance: float, max_distance: float = 50.0,
min_volume: float = 0.0, max_volume: float = 1.0) -> float:
"""
计算基于距离的音量衰减
Args:
distance: 距离
max_distance: 最大距离(在此距离外音量为最小值)
min_volume: 最小音量
max_volume: 最大音量
Returns:
计算后的音量
"""
if distance >= max_distance:
return min_volume
# 线性衰减
attenuation = 1.0 - (distance / max_distance)
volume = min_volume + (max_volume - min_volume) * attenuation
return max(min_volume, min(max_volume, volume))
def calculate_pan(position: Tuple[float, float, float],
listener_position: Tuple[float, float, float]) -> float:
"""
计算立体声平移(左/右声道平衡)
Args:
position: 音源位置
listener_position: 听者位置
Returns:
平移值 (-1.0 到 1.0-1为左1为右)
"""
# 计算相对位置
rel_x = position[0] - listener_position[0]
rel_z = position[2] - listener_position[2]
# 计算角度
angle = math.atan2(rel_x, rel_z)
# 转换为平移值
pan = math.sin(angle)
return max(-1.0, min(1.0, pan))
def interpolate_positions(start_pos: Tuple[float, float, float],
end_pos: Tuple[float, float, float],
steps: int) -> List[Tuple[float, float, float]]:
"""
在两点间插值生成位置列表
Args:
start_pos: 起始位置
end_pos: 结束位置
steps: 插值步数
Returns:
插值位置列表
"""
if steps <= 1:
return [start_pos, end_pos]
positions = []
for i in range(steps):
t = i / (steps - 1)
x = start_pos[0] + (end_pos[0] - start_pos[0]) * t
y = start_pos[1] + (end_pos[1] - start_pos[1]) * t
z = start_pos[2] + (end_pos[2] - start_pos[2]) * t
positions.append((x, y, z))
return positions
def validate_audio_file(filepath: str) -> Dict[str, Any]:
"""
验证音频文件
Args:
filepath: 音频文件路径
Returns:
验证结果字典
"""
import os
result = {
'valid': False,
'exists': False,
'file_size': 0,
'extension': '',
'supported': False
}
# 检查文件是否存在
if not os.path.exists(filepath):
result['error'] = '文件不存在'
return result
result['exists'] = True
# 获取文件信息
result['file_size'] = os.path.getsize(filepath)
_, extension = os.path.splitext(filepath)
result['extension'] = extension.lower()
# 检查是否为支持的格式
supported_formats = ['.wav', '.ogg', '.mp3']
result['supported'] = result['extension'] in supported_formats
# 验证通过
if result['supported'] and result['file_size'] > 0:
result['valid'] = True
return result
def generate_test_tone(frequency: float = 440.0, duration: float = 1.0,
sample_rate: int = 44100) -> bytes:
"""
生成测试音调(正弦波)
Args:
frequency: 频率 (Hz)
duration: 持续时间 (秒)
sample_rate: 采样率
Returns:
音频数据字节
"""
import math
import struct
# 生成正弦波数据
num_samples = int(duration * sample_rate)
audio_data = bytearray()
for i in range(num_samples):
# 生成正弦波样本
t = i / sample_rate
sample = math.sin(2 * math.pi * frequency * t)
# 转换为16位整数
sample_int = int(sample * 32767)
audio_data.extend(struct.pack('<h', sample_int))
return bytes(audio_data)
def convert_to_mono(audio_data: bytes, channels: int = 2) -> bytes:
"""
将多声道音频转换为单声道
Args:
audio_data: 音频数据
channels: 声道数
Returns:
单声道音频数据
"""
if channels == 1:
return audio_data
# 假设16位音频数据
sample_size = 2
frame_size = sample_size * channels
mono_data = bytearray()
for i in range(0, len(audio_data), frame_size):
# 取第一个声道的数据
mono_data.extend(audio_data[i:i+sample_size])
return bytes(mono_data)
def apply_volume_multiplier(audio_data: bytes, volume: float) -> bytes:
"""
应用音量乘数到音频数据
Args:
audio_data: 音频数据
volume: 音量乘数 (0.0-1.0)
Returns:
处理后的音频数据
"""
import struct
if volume == 1.0:
return audio_data
# 假设16位音频数据
sample_count = len(audio_data) // 2
processed_data = bytearray()
for i in range(sample_count):
# 解包16位样本
sample = struct.unpack_from('<h', audio_data, i * 2)[0]
# 应用音量
sample = int(sample * volume)
# 限制范围
sample = max(-32768, min(32767, sample))
# 重新打包
processed_data.extend(struct.pack('<h', sample))
return bytes(processed_data)
def calculate_doppler_shift(source_velocity: Tuple[float, float, float],
listener_velocity: Tuple[float, float, float],
sound_speed: float = 343.0) -> float:
"""
计算多普勒频移
Args:
source_velocity: 音源速度向量
listener_velocity: 听者速度向量
sound_speed: 声速 (米/秒)
Returns:
频率比率 (播放频率/原始频率)
"""
# 计算相对速度
rel_velocity_x = source_velocity[0] - listener_velocity[0]
rel_velocity_z = source_velocity[2] - listener_velocity[2]
# 计算速度大小
source_speed = math.sqrt(rel_velocity_x**2 + rel_velocity_z**2)
# 简化的多普勒效应计算
if sound_speed > source_speed:
ratio = (sound_speed - source_speed) / sound_speed
return max(0.5, min(2.0, ratio)) # 限制在合理范围内
else:
return 0.5 # 音源速度接近或超过声速时的处理
def create_hrtf_filter(angle: float, elevation: float) -> Dict[str, List[float]]:
"""
创建HRTF头部相关传递函数滤波器
Args:
angle: 水平角度 (度)
elevation: 垂直角度 (度)
Returns:
左右声道滤波器系数
"""
# 这是一个简化的HRTF实现
# 实际应用中会使用真实的HRTF测量数据
# 简单的左右声道延迟差异
angle_rad = math.radians(angle)
delay_diff = math.sin(angle_rad) * 0.001 # 最大1毫秒延迟差异
# 简单的左右声道幅度差异
left_amp = 1.0 - max(0, math.sin(angle_rad)) * 0.3
right_amp = 1.0 - max(0, -math.sin(angle_rad)) * 0.3
return {
'left': [left_amp, 0.0, 0.0],
'right': [right_amp, 0.0, delay_diff]
}
def calculate_reverb_parameters(room_size: Tuple[float, float, float],
absorption: float = 0.5) -> Dict[str, float]:
"""
根据房间尺寸计算混响参数
Args:
room_size: 房间尺寸 (长, 宽, 高)
absorption: 吸收系数 (0.0-1.0)
Returns:
混响参数字典
"""
length, width, height = room_size
# 计算房间体积
volume = length * width * height
# 计算表面积
surface_area = 2 * (length*width + length*height + width*height)
# 简化的混响时间计算Sabine公式
reverb_time = 0.161 * volume / (surface_area * absorption)
# 限制在合理范围内
reverb_time = max(0.1, min(10.0, reverb_time))
return {
'reverb_time': reverb_time,
'early_reflections': reverb_time * 0.1,
'late_reverb': reverb_time * 0.9,
'density': min(1.0, volume / 1000.0),
'diffusion': 1.0 - absorption
}