- 共享引擎(app/src/main):GestureEngine 6 手势识别、AudioEngine 三层音频 (ExoPlayer BGM 交叉淡入 / 对白 ducking / SoundPool 低延迟)、Game/GameEngine 状态机接口、MainActivity 全屏封面 + TalkBack - 《声命线》storyline:末世互动广播剧,3 幕 8 抉择点 4 结局 - 《声东击西》reflex:听声辨位反应,12 波战斗 + BGM 三档 BPM 变速 - 《暗室》escape:3x3 网格密室逃脱,钥匙→铁柜→保险丝→电箱→大门谜题链 - 文档:setup(从零搭建)/ architecture(架构)/ engine-api(引擎 API)/ asset-pipeline(资产流水线)/ games/*(三款游戏设计文档) - 资产管线:Qwen3-TTS 对白 + 程序合成音效/BGM + Mureka BGM 的 gen 脚本与台账, 全部 ogg 资产入库可复现 - env.d 环境脚本:android/tts 入库;sfx/mureka 含密钥不入库,仅提交 .example.sh 模板
98 lines
3.0 KiB
Python
98 lines
3.0 KiB
Python
import numpy as np
|
|
import torch
|
|
from torch import nn
|
|
from ..data.utils import PadCrop
|
|
|
|
from torchaudio import transforms as T
|
|
|
|
def numpy_audio_to_tensor(audio):
|
|
"""Convert a numpy audio array to a float32 torch tensor with shape [channels, samples]."""
|
|
if audio.dtype == np.float32:
|
|
t = torch.from_numpy(audio)
|
|
elif audio.dtype == np.int16:
|
|
t = torch.from_numpy(audio).float().div(32767)
|
|
elif audio.dtype == np.int32:
|
|
t = torch.from_numpy(audio).float().div(2147483647)
|
|
else:
|
|
raise ValueError(f"Unsupported audio dtype: {audio.dtype}")
|
|
if t.dim() == 1:
|
|
t = t.unsqueeze(0) # [n] -> [1, n]
|
|
elif t.dim() == 2:
|
|
t = t.transpose(0, 1) # [n, channels] -> [channels, n]
|
|
return t
|
|
|
|
|
|
def preprocess_audio(sr_audio, sample_rate, sample_size, half=False):
|
|
"""Normalize a (sr, numpy_or_tensor) tuple into (sample_rate, tensor) ready for the model."""
|
|
in_sr, audio = sr_audio
|
|
|
|
if isinstance(audio, np.ndarray):
|
|
audio = numpy_audio_to_tensor(audio)
|
|
elif audio.dim() == 1:
|
|
audio = audio.unsqueeze(0)
|
|
elif audio.dim() == 2:
|
|
audio = audio.transpose(0, 1) # [n, 2] -> [2, n]
|
|
|
|
if half:
|
|
audio = audio.to(torch.float16)
|
|
|
|
if in_sr != sample_rate:
|
|
resample_tf = T.Resample(in_sr, sample_rate).to(audio.device).to(audio.dtype)
|
|
audio = resample_tf(audio)
|
|
|
|
if audio.shape[-1] > sample_size:
|
|
audio = audio[:, :sample_size]
|
|
|
|
return (sample_rate, audio)
|
|
|
|
|
|
def set_audio_channels(audio, target_channels):
|
|
# Add channel dim if it's missing
|
|
if audio.dim() == 2:
|
|
audio = audio.unsqueeze(1)
|
|
|
|
if target_channels == 1:
|
|
# Convert to mono
|
|
audio = audio.mean(1, keepdim=True)
|
|
elif target_channels == 2:
|
|
# Convert to stereo
|
|
if audio.shape[1] == 1:
|
|
audio = audio.repeat(1, 2, 1)
|
|
elif audio.shape[1] > 2:
|
|
audio = audio[:, :2, :]
|
|
return audio
|
|
|
|
def prepare_audio(audio, in_sr, target_sr, target_length, target_channels, device):
|
|
|
|
audio = audio.to(device)
|
|
|
|
if in_sr != target_sr:
|
|
resample_tf = T.Resample(in_sr, target_sr).to(device)
|
|
audio = resample_tf(audio)
|
|
|
|
audio = PadCrop(target_length, randomize=False)(audio)
|
|
|
|
# Add batch dimension
|
|
if audio.dim() == 1:
|
|
audio = audio.unsqueeze(0).unsqueeze(0)
|
|
elif audio.dim() == 2:
|
|
audio = audio.unsqueeze(0)
|
|
|
|
audio = set_audio_channels(audio, target_channels)
|
|
|
|
return audio
|
|
|
|
|
|
class PadCrop(nn.Module):
|
|
def __init__(self, n_samples, randomize=True):
|
|
super().__init__()
|
|
self.n_samples = n_samples
|
|
self.randomize = randomize
|
|
|
|
def __call__(self, signal):
|
|
n, s = signal.shape
|
|
start = 0 if (not self.randomize) else torch.randint(0, max(0, s - self.n_samples) + 1, []).item()
|
|
end = start + self.n_samples
|
|
output = signal.new_zeros([n, self.n_samples])
|
|
output[:, :min(s, self.n_samples)] = signal[:, start:end]
|
|
return output |