CatGunner CookbookBardCat 1.1.59 · L1 ANALYSIS · tài liệu học
Trang chủCookbook › 08 — Âm thanh
Nguồn: 08-am-thanh.md · Cập nhật lần cuối: 2026-09-05 08:45 +07 · sha256 nguồn a4f002c7f3f6

08 — Âm thanh

Nguồn framework: docs/framework/04-sound.md toà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 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ị SfxType và 14 clip SFX là một gần trùng khớp, nhưng ánh xạ SfxType → AudioClip nằm trong sfxEntries[] serialize trong scene và chưa được trích. Đừng gán SfxType.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 AudioSource mỗi âm thanh, hoặc PlayOneShot cho 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ột sfxSource cho toàn bộ SFX (dump.cs:590265).
  • Giá trị lính canh cho volume (-1f, 0f). SOT dùng Nullable<float>: null = dùng mặc định của SfxEntry, 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 / lastPlayedTime theo từng SfxType.
  • 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_manager bằ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 định Button.
  • 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_Panel là bool, không phải bộ đếm. Hai panel cùng mở, một cái đóng ⇒ cờ về false dù 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 ↔ clip Click chỉ 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 → clip theo 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ổng mute → 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 trong sfxEntries[] serialize trong Main.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 PlaySfx chạy 208× còn Bullet.SpawnEx 207×: UNKNOWN (chênh 1) — không suy diễn; khoá bằng hook PlaySfx kè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