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 — 第二个输出
Slot<AudioGraphNode>:[VideoClip]与[PlayVideo]各新增一个AudioReference输出并实现IAudioSource(Core/Audio/IAudioSource.cs)。未接线 → 走隐式默认总线(AudioGraphCollector.CollectLooseSources);已接线 →[AudioBus]负责成员资格与增益。Texture保持为第一个输出(默认连接目标),AudioReference追加为第二个。 - D2 —
Volume变成节点的Gain:node.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——音频恰好会在播放顺利时饿死。- 零拷贝路径没有
VideoFrameCache,GPU 表面在_lock下跨线程交接;把音频队列插进这个握手协议,风险大于收益。 - 代理(proxy)没有音轨(
ProxyTranscoder设置ExportAudio = false),而VideoPlaybackEngine.ResolveEffectivePath会静默地为预览替换成代理。耦合音频在代理预览开启时必然无声——一个令人困惑的无形故障。
因此:AudioDecoderSession拥有自己独立的FormatContext+ 音频CodecContext+SwrContext,永远打开原始路径(绝不打开代理),视频流设置为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]夹入SourceRange,TimeToFrameMapper.ResolvePlaybackSeconds循环/夹取);音频被交给同一个映射后的秒数,因此一帧画面和它的声音共享一个时钟。
项目边界:BASS 在 Core,FFmpeg 在 VideoServices
- VideoServices(FFmpeg 侧)拥有
AudioDecoderSession与驱动它的喂料器; - Core/Audio(BASS 侧)拥有
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 没有自己的位置,它只播放被喂入的内容,所以同步由喂料器负责;它刻意从不加入 mixer,channel 句柄在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暂停其 submix(2 帧松弛); - 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)。诊断行输出平滑后的drift(Settings → "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, t+frameDur],喂料器必须提供恰好该切片的 PCM 而不是提前缓冲——这是独立的导出喂料模式; - 别的都不需要:图路由的源经其总线到达导出混音,导出路径已强制求值总线(
AudioRendering.PrepareRecording快照实时总线)。这曾是难点,且已为[AudioClip]解决。
实现细节(VideoAudioTrack.RequestForExport,见 VideoServices/VideoAudioTrack.cs):
RequestAudio增加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 | 解码 + 重采样(仅 VideoServices):AudioDecoderSession——打开最佳音轨、丢弃视频、重采样为交错 float / 48 kHz / 立体声,暴露HasAudio、SeekTo(seconds)、TryDecodeChunk(out pcm, out startSeconds)、Flush() | 自动化测试 64/64 通过(仅此阶段有自动化覆盖) |
| 2 | Push stream + feeder:Core/Audio/VideoAudioStream+VideoServices/VideoAudioTrack,IVideoPlaybackEngine.RequestAudio打通 | ✅ 已落地(2026-08-09),尚未经耳验证 |
| 3 | [PlayVideo]入图:新增AudioReference输出(GUID12473a41-5839-4b9b-9c79-2541fe8b630b,DirtyFlagTrigger.Always,追加在最后使Texture保持默认连接),实现IAudioSource(no-opEnsureChannelFromStaticInputs——channel 来自被求值的纹理路径,从不来自静态输入)。Volume → node.Gain;Volume <= 0或渲染导出时完全不请求音频,纯纹理用途的视频零解码开销 | ✅ 已落地 + 实机验证:有声,PlayVideo → [AudioReverb] → [AudioBus]插入生效 |
| 4 | [VideoClip]上时间线:同样结构——AudioReference输出(GUIDd9da88fb-a5ac-451f-8727-a8ce126432d8)+IAudioSource。音频请求以片段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。在观测到的并发度(
VideoPlaybackEngine:"mostly 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,但节点继续广播死句柄。路由总线每帧求值AudioReference(DirtyFlagTrigger.Always),于是永久重试已释放句柄:[AudioBus] failed to route channel <negative handle>,每帧一次。逐帧对账无法自愈——没有别的东西会清这个字段。两个算子现在在UpdateAudioTrack盖章帧号,并在 2 帧间隔后从 reference 路径清除SourceChannel——即 feeder 已执行的 "drawn = audible" 规则应用到线值而非喂料。 - 不支持纯音频使用(按设计,实机确认,用户接受):只把
AudioReference接入总线、Texture输出不接任何地方,是静音的——纹理路径驱动 feeder(D4),未求值的算子什么都不请求。对[VideoClip]这是结构性的:时间线→源重映射在TimeClipSlot内且只在纹理输出上发生。对[PlayVideo]可以解耦(它没有 TimeClip,reference 路径可自行计算时间),但只有在顶层才正确——在时间重映射的子图内,音频路径会绕过纹理路径本应施加的重映射。当前不值得为此引入不对称。值得做的是:当AudioReference已接线但算子数帧未被求值时给出IStatusProvider警告,让静音自我解释而不是像 bug——与[AudioLevel]旁支提示同一模式。 - Phase 2 已解决:重采样目标用
AudioConfig.MixerFrequency(而非硬编码 48 kHz);feeder 跑在每流专用 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/14(2026-08-09):
video-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返回的ReadOnlySpan<float>指向复用缓冲、仅在下一次调用前有效(仅由 xmldoc 约束);FFmpeg interop 的swr_convert输出尺寸计算(swr_get_delay+av_rescale_rnd)、raw->extended_data读取、TryOpen失败分支的swr_free(double-free / 泄漏)、seek 时swr_init重初始化;VideoAudioStream.Dispose调MixerRemoveChannel时图可能仍认为持有该 channel;Core 表面扩大(AudioAnalysisContext构造函数 private → internal,ChannelAudioAnalysis与四个AudioGraphNode成员成为新公共 API);[AudioReaction]约 30 个现存实例的向后兼容。
待测量/待决定:稳定态drift=值(预期是 mixer 侧缓冲造成的小恒定滞后,测知后作为重同步目标的固定偏移);接线[AudioReaction]在 Live Interactive 模式下的分析正确性(submix 路径不 consultAudioSource,应当可行——未测)。
关键文件速查
| 关注点 | 文件 |
|---|---|
| 线值(叶:channel + gain + 外部管理标志) | Core/DataTypes/AudioGraphNode.cs |
| loose 源收集进隐式默认总线 | Core/Audio/AudioGraphCollector.cs、Core/Audio/IAudioSource.cs |
| 显式路由 / FX / 计量 realiser | Operators/Lib/io/audio/AudioBus.cs |
| BASS 初始化 + 三个 mixer | Core/Audio/AudioMixerManager.cs |
| 确定性导出混音 | Core/Audio/AudioRendering.cs |
| FFmpeg demuxer/decoder(视频那个——音频有自己的) | VideoServices/VideoDecoderSession.cs、VideoPlaybackController.cs |
| 代理替换(为何音频读原始路径) | VideoServices/VideoPlaybackEngine.cs、VideoServices/ProxyTranscoder.cs |
| BASS push stream(本方案新增) | Core/Audio/VideoAudioStream.cs |
| FFmpeg 音频解码会话(本方案新增) | 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),仅供参考