CD Code Digest Hub /CS:GO

语音与视频:麦克风到扬声器,以及视频播放入口

35 分钟阅读 9 个章节 2 张图

本页分两部分。第一部分追踪游戏内语音:客户端采集并编码(引擎编解码器或 Steam),经 clc_VoiceData 上行,服务器依据游戏规则计算的“谁能听到谁”位图转发为 svc_VoiceData,接收端解码后作为特殊声道送入混音器;CS:GO 的队伍、死亡、观察者语音限制都在游戏 DLL 的 CCSGameRules::CanPlayerHearTalker 中判定。第二部分说明 avi/ 模块的视频入口(AVI/Bink/QuickTime)在本快照的编译配置下哪些真正可用,并澄清 videocfg/ 与视频播放无关。

相关页面:引擎音频管线 · 网络通道与协议 · Demo 与 GOTV · 比赛规则与回合 · 场景与响应规则(无线电) · 匹配框架

相关源码文件#

  • engine/audio/private/voice.cpp:Voice_Init、录音、压缩、Voice_AddIncomingData、语音声道
  • engine/audio/private/voice_sound_engine_interface.cpp:语音声道接入混音器
  • engine/host.cpp、engine/host_cmd.cpp:CL_SendVoicePacket、+voicerecord
  • engine/sv_main.cpp、engine/sv_client.cpp、engine/voiceserver_impl.cpp:编解码器协商与服务器转发
  • engine/servermsghandler.cpp:客户端 SVCMsg_VoiceInit/SVCMsg_VoiceData
  • engine/voice_codecs/celt/voiceencoder_celt.cpp、engine/voice_codecs/frame_encoder/voice_codec_frame.cpp:默认 CELT 编解码器
  • game/shared/voice_gamemgr.cpp、game/shared/voice_status.cpp:游戏层听觉掩码与屏蔽
  • game/shared/cstrike15/cs_gamerules.cpp:CS:GO 语音/聊天可听规则
  • engine/audio/private/snd_stubs.cpp:大厅语音 CEngineVoiceSteam
  • avi/valve_avi.vpc、vpc_scripts/source_video_base.vpc、engine/sys_mainwind.cpp、game/client/vgui_video.cpp:视频模块与播放入口
  • videocfg/videocfg.cpp:显示设置配置(非视频播放)

1. 组件总览#

组件构建条件职责证据
voice.cpp!DEDICATED && !X360录音、压缩、接收解码、语音声道管理engine/engine_inc.vpc:518
voice_record_dsound.cpp / voice_record_openal.cpp / AudioQueueWindows / OSX+Linux 客户端 / OSX麦克风采集engine/engine_inc.vpc:520、engine/engine_inc.vpc:565-567
voice_sound_engine_interface.cpp!DEDICATED && !X360把语音声道作为 S_StartSound 声道播放engine/engine_inc.vpc:521
voiceserver_impl.cpp总是编译IVoiceServer:游戏 DLL 设置收听位图engine/engine_inc.vpc:377、engine/voiceserver_impl.cpp:70
vaudio_celt / vaudio_speex / vaudio_miles独立 DLL,不在 csgo_partner 组引擎编解码器vpc_scripts/projects.vgc:1391-1398、vpc_scripts/projects.vgc:1741-1743、vpc_scripts/groups.vgc:11-103
voice_gamemgr.cppserver.dll周期性计算收听掩码并通知引擎game/server/server_base.vpc:719
voice_status.cpp、voice_banmgr.cppclient.dll说话图标、玩家屏蔽、上报 vbangame/client/client_base.vpc:461-462
CEngineVoiceSteam客户端,非 NO_STEAM匹配大厅语音(IEngineVoice 扩展)engine/host.cpp:5715-5732

engine/voice_codecs/celt/ 在本快照中只跟踪了 vaudio_celt.vpc 与 voiceencoder_celt.cpp,工程引用的 source/libcelt*/libcelt.lib 不在仓库内(engine/voice_codecs/celt/vaudio_celt.vpc:43-47),所以默认编解码器无法从本仓库构建(推断)。

2. 编解码器协商:svc_VoiceInit#

  • 服务器 ConVar:sv_voicecodec 默认 vaudio_celt(复制到客户端),sv_voiceenable 为 0 时丢弃所有语音:engine/sv_main.cpp:259-260。
  • SV_CreateBaseline 先把 CSVCMsg_VoiceInit 写入签入缓冲:多人时带编解码器名、quality = 5、version = VOICE_CURRENT_VERSION(3);单人时编解码器名为空:engine/sv_main.cpp:2312-2319、engine/sv_main.cpp:2466-2468、engine/audio/public/voice.h:30。
  • 客户端 SVCMsg_VoiceInit:编解码器名为空则 Voice_Deinit,否则 Voice_Init( codec, version ):engine/servermsghandler.cpp:455-469。
  • Voice_Init:voice_system_enable 为 0 时不初始化;Speex 使用 11025 Hz,其它编解码器 22050 Hz:engine/audio/private/voice.cpp:775-805、engine/audio/public/voice.h:22-23。随后创建录音设备(失败只提示无法说话)、只允许加载白名单内的 vaudio_celt/vaudio_speex/vaudio_miles(注释说明这是 2016 年披露的远程代码执行问题的修复),为编码器和每个接收声道各创建一个编解码器实例:engine/audio/private/voice.cpp:809-869。接收声道数 VOICE_NUM_CHANNELS = 5:engine/audio/private/voice_sound_engine_interface.h:23。
  • CELT 参数表按版本索引,版本 3 为 22050 Hz、每帧 512 样本、每包 64 字节;帧编解码器把 Init( quality ) 原样传为版本号:engine/voice_codecs/celt/voiceencoder_celt.cpp:59-85、engine/voice_codecs/celt/voiceencoder_celt.cpp:140-158、engine/voice_codecs/frame_encoder/voice_codec_frame.cpp:46-48。接口名即模块名 vaudio_celt:engine/voice_codecs/celt/voiceencoder_celt.cpp:115-121。

3. 采集与上行:clc_VoiceData#

  • 命令:+voicerecord、-voicerecord、voicerecord_toggle:engine/host_cmd.cpp:2769-2771。所有 DLL 工程都定义 CSTRIKE15(vpc_scripts/source_dll_base.vpc:20),在该宏下取消 NO_ENGINE_VOICE,走引擎录音;voice_vox 为真(开放麦)时按键命令被忽略:engine/host_cmd.cpp:2500-2505、engine/host_cmd.cpp:2511-2546。松开按键时先发最后一包再停止录音:engine/host_cmd.cpp:2558-2566。
  • 录音源:voice_record_steam(默认 0)为真时调用 Steam StartVoiceRecording,否则用引擎录音设备:engine/audio/private/voice.cpp:94、engine/audio/private/voice.cpp:259-291。
  • 每个 tick:引擎定义了 VOICE_OVER_IP,tick 循环中调用 CL_ProcessVoiceData;Voice_Idle 按 tick 间隔节流后返回真时 CL_SendVoicePacket(false):engine/engine_inc.vpc:28、engine/host.cpp:4279-4287、engine/host.cpp:2832-2846、engine/audio/private/voice.cpp:1014-1034。
  • Voice_GetCompressedData:Steam 路径直接取 Steam 压缩数据并标记 VoiceFormat_Steam;引擎路径从录音设备读样本后用编码器压缩:engine/audio/private/voice.cpp:1276-1330。
  • CL_SendVoicePacket 组装 CCLCMsg_VoiceData:数据、格式(STEAM/ENGINE)、本地玩家 XUID、分段号/字节序号/未压缩样本偏移,然后 SendNetMsg:engine/host.cpp:2757-2797。消息字段见 common/netmessages.proto:240-254。
  • 语音消息模板参数为 INetChannelInfo::VOICE 组、不可靠:common/netmessages.h:444、common/netmessages.h:475、common/netmessages.h:49。

4. 服务器转发:谁能听到谁#

4.1 引擎侧#

  • CGameClient::CLCMsg_VoiceData 先通知游戏 DLL ClientVoice(玩家 OnVoiceTransmit、记录说话时间),再 SV_BroadcastVoiceData:engine/sv_client.cpp:317-323、game/server/gameinterface.cpp:3788-3798。
  • SV_BroadcastVoiceData:sv_voiceenable 为 0 直接返回;只构造一次 CSVCMsg_VoiceData;遍历所有 active、非分屏附属的目标客户端,用 IsHearingClient/IsProximityHearingClient 填 audible_mask/proximity(分屏伙伴按位合并);听不到且不是自己则跳过;是自己但未开回环时发送零长度消息作“服务器已收到”确认;否则把完整语音写入目标的语音流(SendNetMsg(msg, false, true)):engine/sv_main.cpp:2324-2456、engine/net_chan.cpp:2965-2980。
  • CGameClient::IsHearingClient:GOTV 客户端听到所有人;自己听自己取决于 userinfo voice_loopback;同机分屏伙伴受 ss_voice_hearpartner 控制;其余查说话者的 m_VoiceStreams 位图:engine/sv_client.cpp:743-771、engine/sv_client.cpp:695、engine/audio/private/voice.cpp:90。位图在客户端失活/清理时清零:engine/sv_client.cpp:807-810、engine/sv_client.cpp:866-869。
  • m_VoiceStreams/m_VoiceProximity 只能由游戏 DLL 经 IVoiceServer::SetClientListening/SetClientProximity 修改(参数为 1 起的实体索引):engine/voiceserver_impl.cpp:31-66。

4.2 游戏侧:CVoiceGameMgr#

  • CGameRules 构造时以 g_pVoiceGameMgrHelper 初始化语音管理器,CGameRules::Think 每帧调用 Update,累计满 0.3 秒才执行 UpdateMasks:game/shared/gamerules.cpp:143-148、game/shared/gamerules.cpp:594-596、game/shared/voice_gamemgr.cpp:20、game/shared/voice_gamemgr.cpp:124-132。
  • UpdateMasks 对每个玩家:首次请求客户端上报 VModEnable;若已启用则对每个其他玩家调用 helper 的 CanPlayerHearPlayer 得到规则掩码;掩码或屏蔽掩码变化时发 CS_UM_VoiceMask 用户消息;最后对每一对调用 SetClientListening( listener, talker, 规则允许 && 未被屏蔽 ):game/shared/voice_gamemgr.cpp:193-270。
  • 客户端经 vban <hex...> 与 VModEnable <0|1> 命令上报屏蔽掩码与开关,由 CGameRules::ClientCommand 转交:game/shared/voice_gamemgr.cpp:146-190、game/shared/gamerules.cpp:576。客户端 CVoiceStatus::UpdateServerState 根据本地屏蔽列表与“隐藏通讯”设置生成这些命令,并挂接 VoiceMask/RequestState 消息:game/shared/voice_status.cpp:541-600、game/shared/voice_status.cpp:122-135、game/shared/voice_status.cpp:211-212。
  • CS:GO 的 helper 只把判定转发给 CSGameRules()->CanPlayerHearTalker( listener, talker, false ),从不设置 bProximity,因此 CS:GO 语音不使用距离衰减(推断):game/shared/cstrike15/cs_gamerules.cpp:2728-2743。

4.3 CS:GO 可听规则#

CanPlayerHearTalker 先决定是否允许听到敌方:sv_talk_enemy_living 与 sv_talk_enemy_dead 同时为真时允许;双方都死亡时看 sv_talk_enemy_dead;双方都存活时看 sv_talk_enemy_living;生死不同时不允许。然后交给共享的 CanPlayerHear(文字聊天也用它,只是敌方开关换成 sv_allchat):game/shared/cstrike15/cs_gamerules.cpp:15640-15664。

证据:上图每个分支对应 game/shared/cstrike15/cs_gamerules.cpp:15582-15638;“协作教练”通过 GetAssociatedTeamNumber 视作队友:game/shared/cstrike15/cs_gamerules.cpp:15592-15594。

相关 ConVar:sv_alltalk 已标注“Deprecated”,在上述判定中不再被读取;sv_deadtalk、sv_full_alltalk、sv_talk_enemy_dead、sv_talk_enemy_living、sv_spec_hear(默认 1)定义于 game/shared/cstrike15/cs_gamerules.cpp:2579-2594。CCSMatch::SetPhase 在热身、半场、比赛结束时自动打开 sv_full_alltalk,其余阶段关闭,可由 sv_auto_full_alltalk_during_warmup_half_end(默认 1)禁用:game/shared/cstrike15/cs_gamerules.cpp:3576、game/shared/cstrike15/cs_gamerules.cpp:3698-3705、game/shared/cstrike15/cs_gamerules.cpp:2591。无线电消息复用同一函数但传 bTeamOnly = true(合作任务除外):game/server/cstrike15/cs_player.cpp:8457-8463。

4.4 GOTV#

GOTV 客户端被视作能听到所有人(engine/sv_client.cpp:745-750),收到的语音进入 HLTV 帧的语音缓冲,tv_relayvoice(默认 1)为真时转播给观众:engine/hltvserver.cpp:101、engine/hltvserver.cpp:492-500;中继节点把上游的 svc_VoiceInit/svc_VoiceData 原样转发:engine/hltvclientstate.cpp:500-514。

5. 接收端解码与播放#

证据:掩码更新 game/shared/voice_gamemgr.cpp:258-268;上行 engine/host.cpp:2772-2794;服务器处理 engine/sv_client.cpp:317-323、engine/sv_main.cpp:2383-2449;接收与解码 engine/servermsghandler.cpp:533-558;播放 engine/audio/private/voice_sound_engine_interface.cpp:308-345。

  • CClientState::SVCMsg_VoiceData:若来源是自己则刷新“本地正在说话”确认;cl_voice_filter 可按名字过滤;零长度数据(服务器确认)直接返回;语音系统未启用则丢弃;带 caster 标记的数据只有在 voice_caster_enable 指定了该解说员账号时才播放;为实体分配或复用语音声道(缓冲时长在观看 GOTV 时取 cl_voice_hltv_buffer_time),再 Voice_AddIncomingData:engine/servermsghandler.cpp:473-563、engine/audio/private/voice.cpp:134-136。
  • Voice_AssignChannel:每实体复用一个声道,最多 5 个,没有空闲声道时该说话者被忽略:engine/audio/private/voice.cpp:1468-1500。
  • Voice_AddIncomingData:利用分段号与字节序号检测丢包,小量丢失时让编解码器做丢包补偿(Decompress(NULL, lostBytes)),异常跳变则重置编解码器并补静音;Steam 格式用 DecompressVoice 解码,引擎格式用该声道的编解码器解码:engine/audio/private/voice.cpp:1597-1652、engine/audio/private/voice.cpp:1665-1711。
  • 播放:语音声道以 CHAN_VOICE_BASE + i 进入普通混音器;proximity 为真时以 SNDLVL_TALKING 挂在说话者实体上,否则以 SNDLVL_IDLE 挂在本地视角实体上(即不随距离衰减):engine/audio/private/voice_sound_engine_interface.cpp:308-345。Voice_Spatialize 目前为空实现:engine/audio/private/voice.cpp:2075-2078。

6. 大厅语音:IEngineVoice#

与游戏内语音并行,引擎在 Host_Init 中把一个 IEngineVoice 实现注册为匹配框架扩展:主机平台用 XVoice,PC(非 NO_STEAM)用 CEngineVoiceSteam,否则用桩:engine/host.cpp:5715-5732、engine/audio/private/snd_stubs.cpp:28-32。CEngineVoiceSteam 直接调用 Steam 的 GetVoice/DecompressVoice,播放时同样走 Voice_AddIncomingData(VoiceFormat_Steam):engine/audio/private/snd_stubs.cpp:554-590、engine/audio/private/snd_stubs.cpp:895。匹配会话据此管理语音列表并播放大厅中收到的语音:matchmaking/sys_session.cpp:1088-1094、matchmaking/sys_session.cpp:1205-1209。会话与大厅本身见 匹配框架。

7. 视频#

7.1 valve_avi 模块与编译开关#

  • valve_avi 在 csgo_partner 组中:vpc_scripts/groups.vgc:14。它在 Windows 编译 avi.cpp(Video for Windows),非 OSX 编译 bink.cpp,开启 QuickTime 时编译 quicktime.cpp;Bink SDK 头文件一行被注释掉:avi/valve_avi.vpc:40-53。
  • 视频宏由 source_video_base.vpc 统一注入:Windows(未开 QUICKTIME_WINDOWS)只定义 AVI_VIDEO;WMV_VIDEO,没有 BINK_VIDEO:vpc_scripts/source_video_base.vpc:8-11、vpc_scripts/source_video_base.vpc:37-38。文件注释声称“注释掉 QuickTime 宏即可启用 BINK”(vpc_scripts/source_video_base.vpc:20-25),与实际定义不符——这是文档与配置的漂移,可能与 partner depot 裁剪有关(推断)。bink.cpp 整体包在 #if defined( BINK_VIDEO ) 中:avi/bink.cpp:7、avi/bink.cpp:1938;主机平台的 BINK_ENABLED_FOR_CONSOLE 也依赖 BINK_VIDEO:public/tier0/platform.h:209-211;专用服务器另加 NO_BINK:engine/engine_inc.vpc:30。
  • 装配:启动器按宏把 valve_avi 的 IAvi/IBik/IQuickTime 加入 AppSystem 组:launcher/launcher.cpp:751-763。CEngineAPI::Connect 在 Windows 上要求 IAvi 存在,IBik 只在 BINK_VIDEO 下要求:engine/sys_dll2.cpp:565-576;client.dll 同理:game/client/cdll_client_int.cpp:1504-1515。

7.2 使用场景#

场景入口本快照 Windows 配置下的状态证据
启动视频CVideoMode_Common::DrawStartupVideo 只播放一次 CGame::PlayStartupVideos:-novid/-dev 等跳过,读 media/startupvids*.txt,逐个 PlayVideoAndWaitPlayVideoAndWait 函数体全部在 BINK_VIDEO 内,等于空操作(推断);Linux 窗口实现直接为空engine/sys_getmodes.cpp:736-751、engine/sys_mainwind.cpp:1620-1697、engine/sys_mainwind.cpp:2065-2073、engine/sys_mainwind.cpp:2170-2174、engine/sys_mainwind.cpp:2437、engine/sys_linuxwind.cpp:60
playvideo 系列命令 / VideoPanelclient.dll 控制台命令创建 VGUI 视频面板,以 Bink 材质渲染;主菜单“制作人员”播放 creditsVideoPanel 直接调用全局 bik,而 bik 只在 BINK_VIDEO 下被赋值,否则保持 NULL(推断:调用会失败)game/client/vgui_video.cpp:295-330、game/client/vgui_video.cpp:925-934、game/client/cstrike15/gameui/BasePanel.cpp:4025-4028、game/client/cdll_client_int.cpp:250、game/client/cdll_client_int.cpp:1508-1511
电影预缓存CHLClient::PrecacheMovie/UncacheAllMovies仅记录名字,Bink 调用被宏去掉game/client/cdll_client_int.cpp:2985-3009
AVI/BIK 精灵材质模型加载器 GetSpriteInfo 用 IAvi/IBik 取尺寸和帧数(专用服务器也会运行)AVI 分支可用;BIK 分支依赖 bikengine/modelloader.cpp:5518-5550
startmovie 录制CL_StartMovie 以 IAvi::StartAVI 写 AVI,混音器把声音追加进影片可用(Windows)engine/cl_main.cpp:2241-2260、engine/audio/private/snd_mix.cpp:5433
Bink 音频接管DirectSound/SDL 设备初始化时把声卡交给 Bink宏关闭engine/audio/private/snd_dev_direct.cpp:177-195

7.3 videocfg 不是视频播放#

videocfg 的文件头写明是“Video configuration management”,读写 cfg/video.txt、cfg/videodefaults.txt,按 CPU/GPU/显存/显卡等级生成显示设置(如 setting.cpu_level、setting.gpu_level):videocfg/videocfg.cpp:1-30、videocfg/videocfg.cpp:66-67。它作为静态库链接进引擎:engine/engine_inc.vpc:1256,与 avi/ 无关。显示与材质设置参见 材质系统。

约束与边界#

  • 不变量:语音转发的唯一依据是每个说话者的 m_VoiceStreams 位图,它只由游戏 DLL 每 0.3 秒刷新一次,所以规则变化(如死亡、换队)最多滞后约 0.3 秒生效(推断,game/shared/voice_gamemgr.cpp:20)。语音始终走不可靠的语音流,丢包由接收端补偿而非重传。
  • 失败路径:编解码器名不在白名单、DLL 缺失或 Init 失败时语音整体关闭(engine/audio/private/voice.cpp:850-856);5 个接收声道用尽时新的说话者被丢弃;sv_voiceenable 0 时服务器丢弃全部语音。
  • 条件编译:X360 定义 NO_VOICE(public/tier0/platform.h:99-101);DEDICATED 构建不含录放代码,但 voiceserver_impl.cpp 与 SV_BroadcastVoiceData 保留,专用服务器照常转发;视频依赖 AVI_VIDEO/BINK_VIDEO/QUICKTIME_VIDEO。
  • 观察:CVoiceServer::GetClientListening(receiver, sender) 实际调用 sv.GetClient(sender)->IsHearingClient(receiver),按 IsHearingClient 的语义这查询的是“sender 能否听到 receiver”,与函数名方向相反(推断,engine/voiceserver_impl.cpp:19-29、engine/sv_client.cpp:743-765);SV_BroadcastVoiceData 在检查分屏用户前就无条件把 (i << 1) 并入 audible_mask(engine/sv_main.cpp:2388-2398)。二者对 PC 单人单机路径影响有限,未进一步验证。
  • 证据不足:CELT/Miles 库与 Bink SDK 不在仓库内;无法确认发行版是否启用了 BINK_VIDEO,也无法确认 media/*.txt、视频文件等内容。
  • 本页未展开:语音增益/自动音量(voice_gain.cpp)、混音器麦克风控制、voice_tweak 工具、录音写文件调试、主机平台 XVoice/PS3 语音、PlayerVoiceListener 的用途、HUD 语音图标渲染。