08-am-thanh.md · Cập nhật lần cuối: 2026-09-05 08:45 +07 · sha256 nguồn a4f002c7f3f608 — Âm thanh
Nguồn framework:
docs/framework/04-sound.mdtoàn bộ. Cấp bằng chứng: L1 ANALYSIS — dựng lại clean-room từ disasm + metadata + quan sát runtime; không phải source gốc. Mỗi dòng bên dưới mang nhãn bằng chứng riêng.
Mục tiêu
Muốn một game idle bắn ~5 phát/giây và có hàng trăm nút bấm mà nghe được — không thành tiếng ồn trắng, không tăng vọt CPU mixer — thì làm như CatGunner: một singleton, một enum, hai AudioSource, và hai tầng chống trùng. Với artist: game này ship đúng 16 clip (2 nhạc nền + 14 SFX, ~4 phút tổng) — ngân sách âm thanh nhỏ đến bất ngờ là đủ, cái tạo cảm giác là khi nào phát và chống trùng, không phải bao nhiêu.
Game gốc làm gì
| Điều game làm | Con số / hằng số | Mức bằng chứng | Nguồn |
|---|---|---|---|
Sound_manager : MonoBehaviour (TypeDefIndex 8080), 8 method, singleton qua property public static Sound_manager instance { get; set; } |
8 method | metadata-proven | dump.cs:590265 |
API phát SFX duy nhất: PlaySfx(SfxType id, Nullable<float> volumeOverride, float pitch = 1) |
— | metadata-proven | RVA 0x2ACF7D4 |
Chỉ hai AudioSource: sfxSource @0x20 và bgmSource @0x28 |
2 | metadata-proven | dump.cs:590265 |
Thư viện âm thanh SfxEntry[] sfxEntries @0x30, lập chỉ mục thành Dictionary<SfxType, SfxEntry> sfxMap @0x40 lúc Awake |
— | metadata-proven | Awake RVA 0x2ACFB34 |
Hai cơ chế chống trùng độc lập, cờ inspector: [Header("Duplicate Guard")] bool blockSameFrameDuplicate @0x38; [Header("Cooldown Guard")] bool blockCooldownDuplicate @0x39 + float cooldownSeconds @0x3C |
— | metadata-proven | dump.cs:590265 |
Trạng thái chống trùng theo từng SfxType: Dictionary<SfxType,int> lastPlayedFrame @0x48, Dictionary<SfxType,float> lastPlayedTime @0x50 |
— | metadata-proven | dump.cs:590265 |
Chỉ hai clip nhạc nền tham chiếu trực tiếp: AudioClip BGM_main @0x58, BGM_contents @0x60 |
2 | metadata-proven | dump.cs:590265 |
Chuyển nhạc bằng method không tham số: BGM_Change_Main(), BGM_Change_SpeedRun() |
— | metadata-proven | RVA 0x2ACFEC4 / 0x2ACFE14 |
Tắt nhạc riêng: ApplyBGMMute(bool mute) — chạy 3× trong 40 s |
3× | metadata-proven + runtime-observed | RVA 0x2ACD5B0; [runtime trace: runtime_ranking.json] |
enum SfxType (TypeDefIndex 8078) có 15 giá trị: Pop=0, Button=1, Spawn=2, Fusion=3, Upgrade=4, Dont=5, Rebirth=6, Purchase=7, Shoot=8, Select=9, Enter=10, Win=11, Fail=12, UnSelect=13, Tick=14 |
15 | metadata-proven | dump.cs:590227 |
SFX_BtnScript (322 instance) nối SFX vào nút ở Start: GetComponent<Button>().onClick.AddListener(Sfx); Sfx() gọi PlaySfx(_sfx, null, 1f) |
322 | disasm-proven + scene-census | RVA 0x2ACF6C8 / 0x2ACF774; scene_census.json |
SFX_BtnScript._sfx mặc định = (SfxType)1 tức Button |
— | disasm-proven | .ctor RVA 0x2ACFA74 |
Mute_Panel (19 instance) bật/tắt Cat_manager.is_Shoot_Mute (static bool @0x0) theo OnEnable/OnDisable |
19 | disasm-proven + scene-census | RVA 0x2AF00DC / 0x2AF0128; dump.cs:593380 |
Sound_manager.PlaySfx chạy 208× trong ~40 s, xấp xỉ 207 phát bắn |
208 / 207 | runtime-observed | runtime_ranking.json |
Game ship đúng 16 AudioClip, tất cả Vorbis (m_CompressionFormat = 1), export .ogg |
16 | asset-census | [report: asset_lane.md] §5 |
Hai clip dài là nhạc nền: Relaxed_7_BlossomDrift (121.26 s), Cute_5_MilkShake (105.00 s), stereo 44.1 kHz |
2 | asset-census | như trên |
| 14 clip còn lại ≤ 3.80 s — đều là SFX | 14 | asset-census | như trên |
Không có bundle âm thanh riêng; toàn bộ nằm trong data.unity3d |
— | asset-census (phủ định) | asset_lane.md §5 |
Không có AudioMixerGroup, không có âm thanh 3D (PlaySfx không nhận Vector3) |
— | không quan sát thấy | dump.cs:590265 |
Danh mục 16 clip đã ship (asset-census, [report: asset_lane.md] §5)
| Clip | ch | Hz | Độ dài | Phân loại |
|---|---|---|---|---|
Relaxed_7_BlossomDrift |
2 | 44100 | 121.26 s | nhạc nền |
Cute_5_MilkShake |
2 | 44100 | 105.00 s | nhạc nền |
etfx_explosion_chargeup2 |
2 | 48000 | 3.80 s | SFX |
Prize Wheel Spin 2 Reward |
2 | 44100 | 2.43 s | SFX |
espannol |
2 | 44100 | 2.38 s | SFX |
Positive |
1 | 44100 | 2.07 s | SFX |
Special & Powerup 45 |
2 | 44100 | 1.86 s | SFX |
Special & Powerup 43 |
2 | 44100 | 1.50 s | SFX |
Transition_In |
2 | 44100 | 0.99 s | SFX |
etfx_explosion_sharp |
2 | 48000 | 0.53 s | SFX |
Fail |
2 | 44100 | 0.40 s | SFX |
Pop 6 |
2 | 44100 | 0.30 s | SFX |
ClickyButton5a |
2 | 44100 | 0.27 s | SFX |
Popup1 |
2 | 44100 | 0.16 s | SFX |
Click |
2 | 44100 | 0.10 s | SFX |
GenericButton14 |
2 | 44100 | 0.06 s | SFX |
Không suy ra ánh xạ. 15 giá trị
SfxTypevà 14 clip SFX là một gần trùng khớp, nhưng ánh xạSfxType → AudioClipnằm trongsfxEntries[]serialize trong scene và chưa được trích. Đừng gánSfxType.Click → "Click"chỉ vì tên giống nhau.
Công thức / pseudocode
Kiến trúc một cửa — suy ra từ layout field dump.cs:590265; thân PlaySfx CHƯA dựng lại:
bất kỳ hệ thống nào
|
v
Sound_manager.instance.PlaySfx(SfxType id, float? volumeOverride, float pitch = 1)
|
+-------+--------+
| |
sfxMap[id] chống trùng (thứ tự kiểm tra: UNKNOWN)
(SfxEntry) - blockSameFrameDuplicate ? lastPlayedFrame[id] == Time.frameCount -> bỏ
- blockCooldownDuplicate ? Time.time - lastPlayedTime[id] < cooldownSeconds -> bỏ
|
v
sfxSource (MỘT AudioSource duy nhất cho toàn bộ SFX)
Field chống trùng — metadata-proven:
[Header("Duplicate Guard")] [SerializeField] bool blockSameFrameDuplicate; // @0x38
[Header("Cooldown Guard")] [SerializeField] bool blockCooldownDuplicate; // @0x39
[SerializeField] float cooldownSeconds; // @0x3C
readonly Dictionary<SfxType, int> lastPlayedFrame; // @0x48
readonly Dictionary<SfxType, float> lastPlayedTime; // @0x50
Nối SFX vào nút — L1 ANALYSIS, SFX_BtnScript RVA 0x2ACF6C8 / 0x2ACF774, 322 instance:
public SfxType _sfx = (SfxType)1; // mặc định Button (.ctor RVA 0x2ACFA74)
private void Start() { GetComponent<Button>().onClick.AddListener(Sfx); }
private void Sfx() { Sound_manager.instance.PlaySfx(_sfx, null, 1f); }
Mute theo ngữ cảnh — L1 ANALYSIS, Mute_Panel RVA 0x2AF00DC / 0x2AF0128, 19 instance:
private void OnEnable() { Cat_manager.is_Shoot_Mute = true; }
private void OnDisable() { Cat_manager.is_Shoot_Mute = false; }
// Cờ được BÊN PHÁT đọc -> tiếng bắn không bao giờ tới mixer, thay vì phát rồi bị chặn.
// Nơi đọc cờ (Cat.Attack / Cat_manager.Update): CHƯA dựng lại.
Nhạc nền — metadata-proven:
public AudioClip BGM_main; // @0x58
public AudioClip BGM_contents; // @0x60
public void BGM_Change_Main() { … } // RVA 0x2ACFEC4
public void BGM_Change_SpeedRun() { … } // RVA 0x2ACFE14
public void ApplyBGMMute(bool mute){ … } // RVA 0x2ACD5B0 — tách khỏi mute SFX
Gợi ý framework (không phải bằng chứng SOT): dùng counter thay cho bool cho mute lồng nhau; cho phép nâng lên pool AudioSource khi cần âm thanh 3D; biến tấu cao độ ở call-site bằng pitch: Random.Range(0.95f, 1.05f).
Bẫy đã gặp
- Một
AudioSourcemỗi âm thanh, hoặcPlayOneShotcho mọi sự kiện. Với 5 phát/giây + hàng trăm nút, âm thành tiếng ồn trắng và CPU audio tăng vọt. SOT dùng đúng mộtsfxSourcecho toàn bộ SFX (dump.cs:590265). - Giá trị lính canh cho volume (
-1f,0f). SOT dùngNullable<float>:null= dùng mặc định củaSfxEntry, không thể nhầm với "im lặng". - Chống trùng toàn cục. Tiếng bắn bị throttle không được làm im tiếng bấm nút. SOT lưu
lastPlayedFrame/lastPlayedTimetheo từngSfxType. - Bỏ tiêu chí cùng-frame. "10 quái chết cùng lúc, 10 tiếng nổ chồng pha hoàn hảo" là loại méo tiếng nặng nhất; cooldown không chặn được nó. So với
FXThrottle(doc 03): FX cần khoảng cách, âm thanh cần cùng-frame — hai bộ tiêu chí cho hai miền. - Nối
onClick → Sound_managerbằng tay ở 322 chỗ trong inspector. Vỡ khi đổi tên method. SOT gắn một component 9 dòng với enum mặc địnhButton. - Mỗi màn hình tự chọn nhạc. Dẫn tới tranh chấp lẫn nhau. SOT dùng method không tham số — trạng thái nhạc thuộc về trạng thái game.
Mute_Panellà bool, không phải bộ đếm. Hai panel cùng mở, một cái đóng ⇒ cờ vềfalsedù panel kia vẫn mở. Trong SOT an toàn vì UI là ngăn xếp một-panel-một-lúc; framework tổng quát nên dùng counter (doc 04 §3.5).- Gán clip theo tên.
SfxType.Click↔ clipClickchỉ là trùng tên; ánh xạ thật nằm trong scene, chưa trích (doc 04 §2).
Checklist khi tự làm
- [ ] Một cửa duy nhất
PlaySfx(enum id, float? volumeOverride, float pitch = 1)— enum, không phải string. - [ ] Hai
AudioSource: một cho SFX, một cho BGM. - [ ] Chống trùng theo từng id:
blockSameFrameDuplicate+blockCooldownDuplicate+cooldownSeconds, bật/tắt từ inspector. - [ ] Component nối-SFX-vào-nút với mặc định đúng (
Button); chỉ nút đặc biệt (Purchase,Fail,Win) mới đổi enum. - [ ] Trạng thái nhạc là method không tham số (
BGM_Change_Main()/BGM_Change_X()). - [ ] Mute nhạc tách khỏi mute SFX.
- [ ] Mute theo ngữ cảnh bằng cờ mà bên phát đọc, gắn
OnEnable/OnDisable— dùng counter để lồng nhau an toàn. - [ ] Artist: ngân sách tham chiếu 2 BGM + ~14 SFX ngắn (≤ 4 s), Vorbis, không cần bundle riêng.
- [ ] Không map
SfxType → cliptheo tên; ghi rõ ánh xạ là dữ liệu inspector.
Điều chưa biết (UNKNOWN)
- ~~Thân
PlaySfx~~: ĐÃ DỰNG (G410) — thứ tự cổngmute → clip → trùng cùng frame theo loại → < 0.05 s theo loại → volume ghi đè ?? volume hàng; pitch không bao giờ biến tấu; BGM mute = volume 0; đổi track chỉ khi clip khác. - ~~Cấu trúc
SfxEntry~~: ĐÃ ĐỌC (G410) —blockSameFrameDuplicate = blockCooldownDuplicate = true,cooldownSeconds = 0.05; volume theo hàng Shoot 0.3 · Fusion 0.4 · Button 0.5 · còn lại 1.0; 15 clip + 2 BGM byte-identical, không mixer. - Ánh xạ
SfxType → AudioClip: UNKNOWN — nằm trongsfxEntries[]serialize trongMain.unity, chưa trích. Không suy từ tên clip. (backlog T410) - ~~Giá trị
cooldownSeconds/block*Duplicate~~: ĐÃ ĐÓNG (G410) — 0.05 s / true / true. - ~~Nơi
Cat_manager.is_Shoot_Muteđược đọc~~: ĐÃ ĐÓNG (G363) —Cat.Attack: tiếng bắn chỉ phát khi!is_Shoot_Mute && !Setting.is_eco(mute kép: panel mở hoặc chế độ eco đều tắt). SfxType.Tick(=14),Dont(=5),Fusion(=3) dùng ở đâu: UNKNOWN — call-site chưa truy.- Vì sao
PlaySfxchạy 208× cònBullet.SpawnEx207×: UNKNOWN (chênh 1) — không suy diễn; khoá bằng hookPlaySfxkèm tham sốSfxType. - Có AudioMixer / nhóm mixer: không quan sát thấy. Âm thanh 3D / spatial: không quan sát thấy.
Đọc thêm
docs/framework/04-sound.md— nguồn chínhdocs/framework/03-particles-vfx.md§3.2 —FXThrottle, để so sánh hai bộ tiêu chí throttledocs/framework/05-characters-pets-enemies.md—Cat_manager, chủ của cờis_Shoot_Mutegap list (nội bộ)— G135 (SFX_BtnScript, Mute_Panel đã ship)