初始提交:3 款纯音频游戏 + 共享引擎 + 完整文档与资产管线
- 共享引擎(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 模板
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from tests.utils.audio import assert_audio_valid, sine_wave
|
||||
|
||||
DURATION_SEC = 2
|
||||
|
||||
|
||||
def test_encode_decode(autoencoder):
|
||||
ae = autoencoder
|
||||
channels = ae.autoencoder.io_channels
|
||||
|
||||
audio = sine_wave(
|
||||
DURATION_SEC, ae.sample_rate, channels=channels, device=ae.device
|
||||
) # (C, T)
|
||||
|
||||
latents = ae.encode(audio, ae.sample_rate)
|
||||
recon = ae.decode(latents)
|
||||
|
||||
assert_audio_valid(recon, DURATION_SEC, ae.sample_rate)
|
||||
|
||||
# Loose reconstruction check: cosine similarity between flattened signals
|
||||
min_len = min(audio.shape[-1], recon.shape[-1])
|
||||
orig_flat = audio[..., :min_len].flatten().float()
|
||||
recon_flat = recon[0, ..., :min_len].flatten().float()
|
||||
similarity = F.cosine_similarity(
|
||||
orig_flat.unsqueeze(0), recon_flat.unsqueeze(0)
|
||||
).item()
|
||||
assert similarity > 0.5, (
|
||||
f"Reconstruction cosine similarity too low: {similarity:.3f}"
|
||||
)
|
||||
|
||||
|
||||
def test_preprocess_for_encoder(autoencoder):
|
||||
"""preprocess_audio_for_encoder resamples and pads a single (C, T) clip."""
|
||||
ae = autoencoder
|
||||
channels = ae.autoencoder.io_channels
|
||||
|
||||
# Generate at half the model rate to exercise resampling
|
||||
input_sr = ae.sample_rate // 2
|
||||
audio = sine_wave(DURATION_SEC, input_sr, channels=channels) # (C, T)
|
||||
|
||||
batch = ae.autoencoder.preprocess_audio_for_encoder(audio, in_sr=input_sr)
|
||||
|
||||
assert batch.ndim == 3, f"Expected (1, C, T), got {batch.shape}"
|
||||
assert batch.shape[0] == 1
|
||||
assert batch.shape[1] == channels
|
||||
# Output length should be a multiple of the downsampling ratio
|
||||
assert batch.shape[2] % ae.autoencoder.downsampling_ratio == 0, (
|
||||
f"Padded length {batch.shape[2]} is not a multiple of "
|
||||
f"downsampling_ratio {ae.autoencoder.downsampling_ratio}"
|
||||
)
|
||||
|
||||
|
||||
def test_preprocess_list_for_encoder(autoencoder):
|
||||
"""preprocess_audio_list_for_encoder handles clips of different lengths."""
|
||||
ae = autoencoder
|
||||
channels = ae.autoencoder.io_channels
|
||||
|
||||
clip_a = sine_wave(1, ae.sample_rate, channels=channels) # 1 s
|
||||
clip_b = sine_wave(DURATION_SEC, ae.sample_rate, channels=channels) # 2 s
|
||||
|
||||
batch = ae.autoencoder.preprocess_audio_list_for_encoder(
|
||||
[clip_a, clip_b], in_sr_list=ae.sample_rate
|
||||
)
|
||||
|
||||
assert batch.ndim == 3, f"Expected (2, C, T), got {batch.shape}"
|
||||
assert batch.shape[0] == 2
|
||||
assert batch.shape[1] == channels
|
||||
assert batch.shape[2] % ae.autoencoder.downsampling_ratio == 0
|
||||
|
||||
|
||||
def test_chunked_encode_decode(autoencoder):
|
||||
"""encode / decode with chunked=True produce valid audio."""
|
||||
ae = autoencoder
|
||||
channels = ae.autoencoder.io_channels
|
||||
|
||||
audio = sine_wave(
|
||||
DURATION_SEC, ae.sample_rate, channels=channels, device=ae.device
|
||||
) # (C, T)
|
||||
|
||||
latents = ae.encode(audio, ae.sample_rate, chunked=True, chunk_size=128, overlap=32)
|
||||
recon = ae.decode(latents, chunked=True, chunk_size=128, overlap=32)
|
||||
|
||||
assert_audio_valid(recon, DURATION_SEC, ae.sample_rate)
|
||||
|
||||
|
||||
def test_latent_save_load(autoencoder, tmp_path):
|
||||
"""Latents saved as .npy and reloaded decode to valid audio."""
|
||||
ae = autoencoder
|
||||
channels = ae.autoencoder.io_channels
|
||||
|
||||
audio = sine_wave(
|
||||
DURATION_SEC, ae.sample_rate, channels=channels, device=ae.device
|
||||
) # (C, T)
|
||||
latents = ae.encode(audio, ae.sample_rate)
|
||||
|
||||
# Save and reload via numpy
|
||||
latent_path = tmp_path / "latents.npy"
|
||||
np.save(latent_path, latents[0].cpu().numpy())
|
||||
|
||||
latent_tensor = torch.from_numpy(np.load(latent_path)).unsqueeze(0).to(ae.device)
|
||||
assert latent_tensor.shape == latents.shape
|
||||
|
||||
recon = ae.decode(latent_tensor)
|
||||
assert_audio_valid(recon, DURATION_SEC, ae.sample_rate)
|
||||
Reference in New Issue
Block a user