语音与视频:麦克风到扬声器,以及视频播放入口
本页分两部分。第一部分追踪游戏内语音:客户端采集并编码(引擎编解码器或 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、+voicerecordengine/sv_main.cpp、engine/sv_client.cpp、engine/voiceserver_impl.cpp:编解码器协商与服务器转发engine/servermsghandler.cpp:客户端SVCMsg_VoiceInit/SVCMsg_VoiceDataengine/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:大厅语音CEngineVoiceSteamavi/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 / AudioQueue | Windows / 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.cpp | server.dll | 周期性计算收听掩码并通知引擎 | game/server/server_base.vpc:719 |
voice_status.cpp、voice_banmgr.cpp | client.dll | 说话图标、玩家屏蔽、上报 vban | game/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)为真时调用 SteamStartVoiceRecording,否则用引擎录音设备: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先通知游戏 DLLClientVoice(玩家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 客户端听到所有人;自己听自己取决于 userinfovoice_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,逐个 PlayVideoAndWait | PlayVideoAndWait 函数体全部在 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 系列命令 / VideoPanel | client.dll 控制台命令创建 VGUI 视频面板,以 Bink 材质渲染;主菜单“制作人员”播放 credits | VideoPanel 直接调用全局 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 分支依赖 bik | engine/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 语音图标渲染。