拓冰建站拓冰建站
首页 / 资讯中心 / 正文

TiXL 视频音频播放方案解析:FFmpeg 解码、BASS Push Stream 与音频图路由的完整落地路径

TiXL 视频音频播放方案解析FFmpeg 解码、BASS Push Stream 与音频图路由的完整落地路径【免费下载链接】t3TiXL is an open source software to create realtime motion graphics.项目地址: https://gitcode.com/GitHub_Trending/t3/t3导读本文基于 TiXL 仓库中.agentic/Plans/Plan_VideoAudio.md这一技术规划文档完整梳理「视频文件音轨 → 音频处理图audio graph」这条从解码到出声的实现链路视频的音轨如何由 FFmpeg 独立解码、经重采样变成 BASS push stream再作为普通音频图源被[AudioBus]路由、分组、加混响与闪避。读者读完可以掌握该方案的六条关键决策D1–D6、VideoAudioTrack喂料器feeder的同步模型、确定性导出render-to-file的按帧喂料机制以及文档中记录的在真实测试中暴露并修复的若干缺陷——这些结论均有仓库源码与测试用例佐证。背景视频有画面、没有声音的里程碑在音频图方案落地之前FFmpeg 视频工作对应归档计划archive/Plan_FfmpegVideo.md与archive/Plan_VideoZeroCopyDecode.md只解码视频轨音轨被留空[PlayVideo]/[VideoClip]上的Volume输入只是 no-op 占位符源码Operators/Video/Symbols/lib/io/video/PlayVideo.cs、VideoClip.cs中的 Audio is silent in this milestone 注释即此状态。本文档的目标正是补齐这一缺口把视频文件音轨解码出来作为普通音频图源接入 TiXL 的音频处理管线。目标与明确不做的范围方案目标文档 Goal 节音轨与时间线同步播放——时间线为主音频跟随Volume终于真正起作用音轨可以通过[AudioBus]/[CombineAudio]/[AudioReverb]/[DuckAudioLevel]路由、分组、混音、闪避和插入 FX——与其他任何音频源走同一套机制零视频专用管道导出渲染时确定性捕获。非目标初始阶段变调变速音频、音频 scrubbing刮擦预览、多轨/声道选择、环绕声下混。这些留到 Phase 4 / 延后。音频图带来的架构转向从“定制路径”到“一个句柄 一个节点”本文档最核心的一段是 What the audio graph changes原始计划早于AudioGraphNode落地的版本设计了一条专属路径——新的AudioEngine.UseVideoAudio入口、每算子一个 stale token、硬编码的 mixer 选择[PlayVideo]→OperatorMixer[VideoClip]→SoundtrackMixer。这些全部被音频图取代图本身已经解决了路由、生命周期、transport 门控、组增益、FX 和计量视频侧只需要产出一个BASS channel 句柄交给节点即可。由此锁定的六条关键决策D1 — 第二个输出SlotAudioGraphNode[VideoClip]与[PlayVideo]各新增一个AudioReference输出并实现IAudioSourceCore/Audio/IAudioSource.cs。未接线 → 走隐式默认总线AudioGraphCollector.CollectLooseSources已接线 →[AudioBus]负责成员资格与增益。Texture保持为第一个输出默认连接目标AudioReference追加为第二个。D2 —Volume变成节点的Gainnode.Gain Volume收集时由组合器combinator增益折叠realiser 应用。与[AudioClip]、[AudioToneGenerator]行为一致。D3 —ExternallyManagedChannel true解码侧拥有 push stream 的生命周期与喂料位置图只拥有成员资格与增益。因此总线路由它时不加MixerChanBuffer、也从不释放它。已知后果与[AudioClip]相同[AudioLevel]旁支 tap 无法计量它——要计量就走总线或把 tap 内联接线让其自行 realise 一个 submix。源码依据AudioGraphNode.ExternallyManagedChannel字段Core/DataTypes/AudioGraphNode.cs与DeclareAnalysisTap的说明——引擎拥有的 channel 只有在被 realise 为带缓冲 submix 后才能被计量。D4 — 纹理路径拥有时间与喂料音频输出只发布 channel gain总线求值AudioReference时提供的EvaluationContext.LocalTime不经过TimeClip重映射该重映射发生在TimeClipSlot内所以无法驱动喂料器。规则是drawn audible有画面才有声音一个没有[VideoClipPlayer]求值的[VideoClip]是静音的——没有画面的视频本就不该有声音。与[AudioClip]不同这里不需要“总线当心跳”。D5 — 预滚必须静音_ProcessVideoClips会在切入前约 0.5 秒拉取Upcoming片段预热解码器。喂料器必须用Active门控而不是“本帧是否被求值”。D6 — 音频解码会话独立、且永远走原始路径见下一节这是相对初稿最大的改动。D6 详解为什么音频要有自己的 demuxer初稿让音频解码搭在VideoDecoderSession/VideoPlaybackController内每次SeekToKeyframeBefore都冲刷其队列。细读控制器后发现这不可行ProcessLatestRequest在缓存命中和目标未变时会提前返回。平滑播放以及每次刮回已缓存区域时根本不发生 demux——音频恰好会在播放顺利时饿死。零拷贝路径没有VideoFrameCacheGPU 表面在_lock下跨线程交接把音频队列插进这个握手协议风险大于收益。代理proxy没有音轨ProxyTranscoder设置ExportAudio false而VideoPlaybackEngine.ResolveEffectivePath会静默地为预览替换成代理。耦合音频在代理预览开启时必然无声——一个令人困惑的无形故障。因此AudioDecoderSession拥有自己独立的FormatContext 音频CodecContextSwrContext永远打开原始路径绝不打开代理视频流设置为AVDISCARD_ALL。它自带 seek 与 flush由请求的播放头驱动。代价是文件被打开两次、字节被读两次OS 页缓存吸收大部分开销因为两个读者走相同偏移视频包在 demuxer 处被丢弃没有第二次解码。为的是在缓存、零拷贝、代理、导出四种场景下行为一致的路径。源码佐证VideoServices/AudioDecoderSession.cs 的类注释明确写着“deliberatelynotshared withVideoDecoderSession: the video worker skips demuxing whenever it serves a frame from its cache, and a preview proxy carries no audio track at all”。架构时钟模型与项目边界时钟模型时间线为主音频跟随时间线Playback.TimeInBars→SecondsFromBars是主时钟音频跟随——与普通媒体播放器相反。视频路径已把时间线映射为源秒[VideoClip]夹入SourceRangeTimeToFrameMapper.ResolvePlaybackSeconds循环/夹取音频被交给同一个映射后的秒数因此一帧画面和它的声音共享一个时钟。项目边界BASS 在 CoreFFmpeg 在 VideoServicesVideoServicesFFmpeg 侧拥有AudioDecoderSession与驱动它的喂料器Core/AudioBASS 侧拥有VideoAudioStream——push stream。VideoServices引用Core所以 PCM 以正确方向跨越程序集边界BASS 不进入 FFmpeg 程序集channel 句柄通过独立的引擎入口回到算子IVideoPlaybackEngine.RequestAudio(streamId, absolutePath, sourceSeconds, loop) → channel而不是塞进VideoFrameResult。保持独立调用方才能在静音状态下继续拉帧——这正是预滚D5与导出的场景也与 D6 的解耦方向一致。源码佐证Core/Video/VideoPlayback.cs 中IVideoPlaybackEngine.RequestAudio(Guid streamId, string absolutePath, double sourceSeconds, bool loop, bool renderingToFile)的 xmldoc音频在独立 demuxer 与独立节奏上解码调用方必须能在保持静音的同时持续拉帧时间线片段在切入前预滚解码器。Push stream ≠ 文件流为什么要新类型OperatorAudioStreamBase和SoundtrackClipStream是可 seek 的文件/解码流从路径创建、按字节位置 seek、经ChannelGetData导出。而push stream 不可 seek——它完全由“喂了什么、何时喂”控制。所以视频音频需要新的VideoAudioStream与OperatorAudioStreamBase平行而不是其子类。它复用按流的Volume属性与 mixer 成员资格用feeder取代 load / seek / position。源码佐证Core/Audio/VideoAudioStream.cs 类注释push stream 没有自己的位置它只播放被喂入的内容所以同步由喂料器负责它刻意从不加入 mixerchannel 句柄在AudioGraphNode叶上传递由音频图决定成员与增益。为什么不让 BASS 直接打开文件BASS 能直接打开部分容器可复用现有机制且几乎零新代码。因编解码器覆盖被否决目标内容如 Silo / Foundation Web-DL是E-AC-3 / DDP5.1原版 BASS 不解码 → 静音。而 FFmpeg 已能解码所有关心的编解码器。生命周期图已经给我们的东西初稿的 stale-token 机制step 4被删除算子不再被求值→RequestFrame停止被调用 → 引擎的IdleTimeoutMs驱逐机制 dispose 控制器喂料器停止。喂料也会在算子停止请求时立即停止push stream 在一个 buffer 长度内欠载到静音总线不再被求值→AudioBusRegistry暂停其 submix2 帧松弛transport 停止→PlaybackSpeed 0暂停显式总线与隐式默认总线算子被 dispose→ReleaseStream(streamId)已在Dispose中运行同时释放音频流。喂料器feeder承载主要新工作的部分push stream 播放被喂入的任何内容按 48 kHz 墙钟时间。同步 让 push buffer 持有从当前播放头开始的约50–100 ms音频最终实现定为 200 ms。正向 1× 播放从播放头按 PTS 顺序解码把 push buffer 补到目标填充量。BASS 按墙钟播放在 1× 时等于时间线漂移校正比较已喂音频位置与请求时间。小漂移放任不可感知跳变seek/scrub→冲刷并重新填充暂停 / 超出范围 / 预滚停止喂料buffer 排空到静音Scrub冲刷并静音音频刮擦很少需要留待 grain 播放速度 ≠ 1×初始静音原始 push 会以错误音高播放后续用atempo或 BASS_FX欠载短暂静音可接受——音频解码远比已实时的视频解码轻。可听性规则一个条件代替四个标志VideoAudioTrack采用单一规则只有当请求时间每个 tick 正向推进 0 Δ ≤ 0.25 s 时音轨才播放。停止、倒放、刮擦、循环回绕和快进全部不满足该测试并冲刷到静音——这覆盖了“scrub / seek / 速度 ≠ 1× 时静音”的四种场景用一个条件代替四个标志。约 100 ms 内未调用RequestAudio也会静音所以预滚与导出无需额外门控。源码佐证VideoServices/VideoAudioTrack.cs 中MaxForwardStepSeconds 0.25、RequiredForwardSteps 2、SilenceAfterIdleMs 100、StationaryTimeoutMs 120等常量直接对应文档所述阈值。实机测试中发现并修复的两个缺陷2026-08-09假“播放头停止”worker 的 tick 速度比算子发布请求快未变化的时间被误读为“播放头停止”导致每两帧渲染之间队列被冲刷。修复只有当时间实际变化时才判定步进并以墙钟超时判定真正的停止。阈值抖动队列长度随 mixer 按块拉取而摆动数十毫秒用原始值对比 120 ms 阈值导致每秒约 8 次重同步。修复漂移改为指数平滑阈值放宽到300 ms稳定播放本就不需要重同步步进门控已捕获所有跳变。源码佐证ResyncThresholdSeconds 0.3、DriftSmoothing 0.05约 200 ms 平滑对应 10 ms tick。诊断行输出平滑后的driftSettings → Show Audio Logs 可见也正是后续“恒定偏移”修正需要的测量值。相邻 Core 修复AudioGraphCollector在设备变更时从不使其静态_defaultBus失效切换音频设备后死句柄被永远复用所有 loose 图音频不止视频保持静音直到重启。现按图计划规则检查AudioMixerManager.ResetGeneration——源码依据Core/Audio/AudioGraphCollector.cs 顶部_resetGeneration ! AudioMixerManager.ResetGeneration时重建_defaultBus与_routed集合。导出render-to-file确定性的按帧喂料渲染到文件是非实时、逐帧混音AudioRendering.GetFullMixDownBuffer每个导出帧调用一次编码器已经 mux 它的返回——所以编码器侧零改动。需要两件事确定性时间切片喂料对每个导出帧的切片[t, tframeDur]喂料器必须提供恰好该切片的 PCM 而不是提前缓冲——这是独立的导出喂料模式别的都不需要图路由的源经其总线到达导出混音导出路径已强制求值总线AudioRendering.PrepareRecording快照实时总线。这曾是难点且已为[AudioClip]解决。实现细节VideoAudioTrack.RequestForExport见 VideoServices/VideoAudioTrack.csRequestAudio增加renderingToFile标志置位时在调用线程上同步解码并排队本帧音频随后的 mixdown 拉取看到正确 PCM该路径不参考墙钟队列每次调用恰好前进一个导出帧帧时长取自两次请求之间的步进无需把渲染 fps 穿透算子 API只在真实不连续处重 seek首帧或剪辑点绝不因漂移重 seek——一次虚假重 seek 会让渲染不可重复喂料目标是一帧的量mixdown 每帧恰好拉那么多队列既不饿死也不增长线程安全新增_workLock保护会话、push stream 与喂料位置worker 在任何导出请求后 500 ms 内退让时间戳而非模式标志可自清除Dispose也取同一把锁——求值线程现在可能正卡在 track 内部“worker 已退出所以无竞争”的旧推理不再成立。相邻 Editor 修复AudioGraphCollector.CollectLooseSources在渲染时被整体跳过导致未接入[AudioBus]的源被解码和喂料但从不路由——导出文件里静音而实时有声。现在导出期间也运行它。这影响每个loose 图源普通[AudioToneGenerator]同样受影响collector 自己的 transport 门控本就豁免录音所以该调用本就在预期中。阶段划分与完成状态阶段内容状态1解码 重采样仅 VideoServicesAudioDecoderSession——打开最佳音轨、丢弃视频、重采样为交错 float / 48 kHz / 立体声暴露HasAudio、SeekTo(seconds)、TryDecodeChunk(out pcm, out startSeconds)、Flush()自动化测试 64/64 通过仅此阶段有自动化覆盖2Push stream feederCore/Audio/VideoAudioStreamVideoServices/VideoAudioTrackIVideoPlaybackEngine.RequestAudio打通✅ 已落地2026-08-09尚未经耳验证3[PlayVideo]入图新增AudioReference输出GUID12473a41-5839-4b9b-9c79-2541fe8b630bDirtyFlagTrigger.Always追加在最后使Texture保持默认连接实现IAudioSourceno-opEnsureChannelFromStaticInputs——channel 来自被求值的纹理路径从不来自静态输入。Volume → node.GainVolume 0或渲染导出时完全不请求音频纯纹理用途的视频零解码开销✅ 已落地 实机验证有声PlayVideo → [AudioReverb] → [AudioBus]插入生效4[VideoClip]上时间线同样结构——AudioReference输出GUIDd9da88fb-a5ac-451f-8727-a8ce126432d8IAudioSource。音频请求以片段active为门控复用为导出停顿计算的isActive测试在源时间SourceRange内min/max 使倒放片段仍被门控预滚拉取保持静音D5。时间来自帧请求所用的同一个clampedTime✅ 已落地2026-08-09需实机测试5确定性导出✅ 已落地2026-08-09需实机测试文档记录当时导出尚未验证6打磨变速atempo、scrub grains、A/V 漂移遥测、多轨/声道选择、环绕下混待办Phase 3/4 源码佐证Operators/Video/Symbols/lib/io/video/PlayVideo.cs——AudioReference输出定义、UpdateAudioTrack中以Volume 0与context.Playback.IsRenderingToFile决定是否RequestAudio、SourceChannel写入VideoClip.cs中AudioReference输出d9da88fb-a5ac-451f-8727-a8ce126432d8。打开的问题与已做的决定向后兼容响度已决定两者保持 1.0两个算子默认Volume 1.0所以开启音频后所有现有项目的视频立即出声。接受——视频播放它的声音是最不意外的行为默认静音反而像 bug。需发布说明。代理再生成音频永远读原始文件代理过期不影响音频——但也意味着代理预览不再消除对原始文件的所有 I/O。是否可接受待定。多发送两个总线同时收集同一个视频源会每帧互相窃取 channel这是文档记载的图级限制直到 split streams 落地。视频同理这里无需额外工作。每流线程/IO 成本可接受每个可听源在引擎每流视频解码线程之上增加一个 feeder 线程与第二个 demuxer。在观测到的并发度VideoPlaybackEnginemostly 0-3 streams, rarely 7下只是一小撮线程与额外读 pass不值得做共享 feeder 池或解码器去重。_streamId是每算子实例的同一文件不同时间的两个片段正确独立去重要以 (file, time) 为键且几乎永不命中。便宜的缓解Volume 0的源完全不请求音频纯纹理视频零成本。mixer 侧缓冲导致的固定 A/V 偏移feeder 测的是fed − queued覆盖了 push 队列但看不到图路由时 mixer 加的缓冲AudioGraphCollector以MixerChanBuffer添加 loose 源BASS 设备缓冲还在更下层。结果是小的恒定滞后——低于重同步阈值所以不会反复搅动但也不会被修正。等音频可听后测量一次折进重同步目标作为偏移放在现有AudioSyncingOffset -2/60 s旁边。Phase 4 调优。循环接缝循环回绕不满足正向步进测试会静音一拍再重新 seek每个循环[PlayVideo]回绕处有一次可闻间隙。修复方向按时长解开步进并在接缝处预解码。Phase 6。陈旧 channel 失效2026-08-09 实机测试发现并修复node.SourceChannel只有纹理路径写。当算子停止被求值——远离剪辑点的[VideoClip]、断线的[PlayVideo]——引擎在 5 s 空闲超时后驱逐流并释放 channel但节点继续广播死句柄。路由总线每帧求值AudioReferenceDirtyFlagTrigger.Always于是永久重试已释放句柄[AudioBus] failed to route channel negative handle每帧一次。逐帧对账无法自愈——没有别的东西会清这个字段。两个算子现在在UpdateAudioTrack盖章帧号并在 2 帧间隔后从 reference 路径清除SourceChannel——即 feeder 已执行的 drawn audible 规则应用到线值而非喂料。不支持纯音频使用按设计实机确认用户接受只把AudioReference接入总线、Texture输出不接任何地方是静音的——纹理路径驱动 feederD4未求值的算子什么都不请求。对[VideoClip]这是结构性的时间线→源重映射在TimeClipSlot内且只在纹理输出上发生。对[PlayVideo]可以解耦它没有 TimeClipreference 路径可自行计算时间但只有在顶层才正确——在时间重映射的子图内音频路径会绕过纹理路径本应施加的重映射。当前不值得为此引入不对称。值得做的是当AudioReference已接线但算子数帧未被求值时给出IStatusProvider警告让静音自我解释而不是像 bug——与[AudioLevel]旁支提示同一模式。Phase 2 已解决重采样目标用AudioConfig.MixerFrequency而非硬编码 48 kHzfeeder 跑在每流专用 worker 上——D6 的全部意义就是与视频 worker 节奏解耦。验证状态自动化、实机与未验证自动化VideoServices.Tests64/64仅覆盖AudioDecoderSession——打开 / 无音轨 / 文件缺失、440 Hz 正弦经编码→解码 48 kHz 往返、重采样到 44.1 kHz、seek 锚定。测试源码VideoServices.Tests/AudioDecoderSessionTests.cs——ToneHz 440、EncodedSampleRate 48000、Decode_ResamplesToTheRequestedRate以 44100 目标速率断言音高与时长不变。批次内其余内容无自动化覆盖。实机手测 14/142026-08-09video-audio-playback.md手动测试——一分钟以上的播放与同步、音频反应算子、含 0 的音量、transport 停止、scrub/倒放/2× 静音、取消求值、无画面音频、总线路由、混响 分组、无音轨文件、时间线片段 in/out、源裁剪、双片段交接、代理预览。已写但未运行两个最新步骤——动画音量以及[AudioReaction]跟随单源。完全未验证导出Phase 5 未开始——视频音频当时在渲染文件中刻意静音两条新路径上的设备切换重建VideoAudioStream.IsInvalidated→DropAfterDeviceChange及新的AudioGraphCollector重置[AudioLevel]重构到节点 helper 上构造上行为保持但该算子此前工作正常、现跑在新代码上接线模式下的[AudioReaction]包括 Live Interactive 模式是否可用。审阅重点关注推理替代了测量的地方见文档 Review handoff 节VideoAudioTrack线程划分靠注释而非结构保证Dispose与 worker 的 Join(2 s) 超时兜底AudioDecoderSession.TryDecodeChunk返回的ReadOnlySpanfloat指向复用缓冲、仅在下一次调用前有效仅由 xmldoc 约束FFmpeg interop 的swr_convert输出尺寸计算swr_get_delayav_rescale_rnd、raw-extended_data读取、TryOpen失败分支的swr_freedouble-free / 泄漏、seek 时swr_init重初始化VideoAudioStream.Dispose调MixerRemoveChannel时图可能仍认为持有该 channelCore 表面扩大AudioAnalysisContext构造函数 private → internalChannelAudioAnalysis与四个AudioGraphNode成员成为新公共 API[AudioReaction]约 30 个现存实例的向后兼容。待测量/待决定稳定态drift值预期是 mixer 侧缓冲造成的小恒定滞后测知后作为重同步目标的固定偏移接线[AudioReaction]在 Live Interactive 模式下的分析正确性submix 路径不 consultAudioSource应当可行——未测。关键文件速查关注点文件线值叶channel gain 外部管理标志Core/DataTypes/AudioGraphNode.csloose 源收集进隐式默认总线Core/Audio/AudioGraphCollector.cs、Core/Audio/IAudioSource.cs显式路由 / FX / 计量 realiserOperators/Lib/io/audio/AudioBus.csBASS 初始化 三个 mixerCore/Audio/AudioMixerManager.cs确定性导出混音Core/Audio/AudioRendering.csFFmpeg demuxer/decoder视频那个——音频有自己的VideoServices/VideoDecoderSession.cs、VideoPlaybackController.cs代理替换为何音频读原始路径VideoServices/VideoPlaybackEngine.cs、VideoServices/ProxyTranscoder.csBASS push stream本方案新增Core/Audio/VideoAudioStream.csFFmpeg 音频解码会话本方案新增VideoServices/AudioDecoderSession.cs每流喂料 worker本方案新增VideoServices/VideoAudioTrack.cs音频解码自动化测试VideoServices.Tests/AudioDecoderSessionTests.cs驱动它们的算子Operators/Video/Symbols/lib/io/video/PlayVideo.cs、VideoClip.cs、_ProcessVideoClips.cs总结TiXL 的视频音频方案是一次典型的架构让路式重构早期设计为视频音频铺设专属管道专用引擎入口、stale token、硬编码 mixer而音频处理图落地后这一切收敛为「FFmpeg 独立解码音轨 → 重采样 → 一个 BASS push stream 句柄 →AudioGraphNode叶节点」路由、增益、FX、计量、transport 门控与导出全部复用既有机制。喂料器以正向步进 漂移平滑 阈值重同步维持时间线同步以drawn audible维持生命周期一致以按帧同步喂料保证导出确定性。该方案同时修复了三个相邻 bug默认总线设备切换失效、导出期间 loose 源不路由、陈旧 channel 广播。截至文档写作时2026-08-09解码与重采样有 64 项自动化测试覆盖播放路径经 14 项实机手测导出与设备切换重建仍待验证——这正是读者可以继续在仓库中跟进与验证的部分。【免费下载链接】t3TiXL is an open source software to create realtime motion graphics.项目地址: https://gitcode.com/GitHub_Trending/t3/t3创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门