ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Audacity 的 StretchingSequence 如何实现反向与随机采样访问?

2026/9/15 20:15:49 拓冰建站 浏览量
Audacity 的 StretchingSequence 如何实现反向与随机采样访问? Audacity 的 StretchingSequence 如何实现反向与随机采样访问【免费下载链接】audacityAudio Editor项目地址: https://gitcode.com/GitHub_Trending/au/audacity如果你在阅读或扩展 Audacity 新一代 au3 架构中的变速变调time-and-pitch播放管线会碰到 au3/libraries/au3-stretching-sequence 这个库StretchingSequence把一个由多个 clip 组成的可播放序列包装成支持变速后的采样访问入口同时承担 Audacity scrubbing拖动播放功能中的反向播放以及任意起始位置的随机采样访问。本文沿真实源码把这两条路径走一遍从GetFloats入口到段segment链的构建到 clip 读取时的原地翻转最后用库内自带的 Catch2 测试核对行为。适用前提是你在 C 环境下列读或二次开发 au3 目录下的源码。设计前提STFT 的“有状态性”决定了访问成本readme.md 先给出一个概念模型变速实现基于 STFT可以简化成一条长度为 1 的延迟线。由此推出三种访问的成本差异连续正向请求样本 0 后只需再推入样本 10 就弹出来。每次请求对应一次 push成本最低也是最常见的场景。连续反向实现被扩展为“在 0 之后推入 -1、-2……”即反向请求与正向走同一条延迟线模型方向相反。随机访问如果刚取出样本 0用户突然请求样本 10就需要两次push 而不是两次中的两次里的……准确说是两次 push 而非一次。readme 的结论是StretchingSequence的处理方式是重新填充缓冲并用变速算法产出请求的样本——可行但有计算开销。对应到代码里这个“状态”由几样东西构成当前段链mAudioSegments、段迭代器mActiveAudioSegmentIt、期望起始采样位置mExpectedStart、播放方向mPlaybackDirection以及每个ClipSegment内部持有的StaffPadTimeAndPitch处理器自身状态。统一入口一次 GetFloats 调用如何决定方向与是否重置创建入口是静态工厂 StretchingSequence.h 中的StretchingSequence::Create(sequence, clips)它用序列的采样率构造AudioSegmentFactory。所有采样读取都经过DoGet(..., start, len, backwards, ...)后者委托给MutableGet见 StretchingSequence.cpp#L174-L189if ( !mExpectedStart.has_value() || *mExpectedStart ! start || (mPlaybackDirection PlaybackDirection::backward ! backwards)) { const auto t start.as_double() / mSequence.GetRate(); ResetCursor( t, backwards ? PlaybackDirection::backward : PlaybackDirection::forward); } return GetNext(reinterpret_castfloat* const*(buffers), nBuffers, len);三个重置条件对应三类情形首次未初始化、请求的start与上次预期位置不符随机访问、方向标志与当前状态不一致方向翻转。start除以序列速率换算成时间t然后交给ResetCursor。方向本身由 PlaybackDirection.h 中的enum class PlaybackDirection { forward, backward }表达。GetNextStretchingSequence.cpp#L46-L87是纯粹的消费循环从当前段GetFloats取出样本段Empty()后迭代到下一段剩余部分补零最后按方向把mExpectedStart加或减numSamples。注意其中的注释——反向样本在进入变速算法前已经是反过来的GetNext层不需要再做翻转numProcessedSamples segment-GetFloats( offsetBuffers, numSamples - numProcessedSamples); // No need to reverse, we feed the // time-stretching algorithm with // reversed samples already.反向访问段链倒序构建 clip 读取后原地翻转ResetCursorStretchingSequence.cpp#L37-L44把方向传给工厂mAudioSegmentFactory-CreateAudioSegmentSequence(t, direction)。工厂在 AudioSegmentFactory.cpp 中按方向分派到两条完全不同的构建路径正向CreateAudioSegmentSequenceForward第 39–65 行按GetPlayStartTime()升序排序 clipclip 起点晚于t0时插入SilenceSegment补齐静默间隔起点早于t0的部分通过durationToDiscard t0 - clipStart丢弃产出ClipSegment(clip, discard, forward)t0推进到该 clip 的播放终点。反向CreateAudioSegmentSequenceBackward第 67–94 行按GetPlayEndTime()降序排序clip 终点早于t0时同样插入静默durationToDiscard clip-GetPlayEndTime() - t0产出ClipSegment(clip, discard, backward)t0回推到 clip 起点。也就是说反向播放并不是把整段音频倒过来再正向读而是从右往左重排 clip 的遍历顺序并把“从哪一端读起、先丢弃多少”编码进段参数。真正读到原始样本的是 ClipTimeAndPitchSource.cpp 的Pull第 43–85 行。它始终以正向方式向ClipInterface请求样本视图反向时请求区间从mLastReadSample - numSamplesToRead开始拷贝完成后调用ReverseSamples把刚读到的样本块原地翻转然后mLastReadSample向负方向推进越过 clip 起点后对缓冲补零const auto start forward ? mLastReadSample : mLastReadSample - numSamplesToRead; ... if (!forward) { ReverseSamples( reinterpret_castsamplePtr(buffers[i]), floatSample, 0, numSamplesToRead); }每个ClipSegmentClipSegment.cpp#L37-L62拿到方向参数后用它构造ClipTimeAndPitchSource作为输入源并创建StaffPadTimeAndPitch处理器参数来自clip.GetStretchRatio()、GetCentShift()等。反向与正向共用同一个处理器方向差异完全在“喂给它的样本序列”这一层完成。ClipSegment::GetFloats还会在每次取样本前检查两个原子标志把播放中实时变化的音高偏移cent shift与共振峰保持开关同步给处理器。随机采样访问检测到跳变就整链重建随机访问的处理就是前面重置条件的直接结果start与mExpectedStart不符MutableGet就调用ResetCursor丢弃整条旧段链从请求位置重新走一遍工厂构建——这正是 readme 所说的“refill the buffer and have it time stretched to produce the requested sample”。开销来自两处重新构建段链与ClipSegment含重新创建StaffPadTimeAndPitch以及durationToDiscard决定要跳过的部分仍会被变速算法“读过去再丢弃”。readme 在 Afterthoughts 一节指出循环播放是最常见的随机访问来源目前实现是 loop-unaware不感知循环边界的每次绕圈都会触发一次状态重建readme 将其列为可能延后的优化方向如果告知循环边界可以避免这些重置。用库内 Catch2 测试核对行为该库的测试位于 au3/libraries/au3-stretching-sequence/tests/基于 Catch2#include catch2/catch.hpp。与本文两个主题直接相关的断言反向读取的正确性StretchingSequenceTest.cpp#L37-L70“Samples can be queried backwards”构造“1s 静默 1s clip{1,2,3} 1s 静默 1s clip{4,5,6}”的序列采样率 3从start 12起、len 2、backwards true连续查询注释说明这是在模拟客户端按旧 API 反向读样本的方式。文档给出的期望序列为{6,5} {4,0} {0,0} {3,2} {1,0} {0,0}——能看出反向读依次命中第二支 clip 的尾部、静默区、第一支 clip 的尾部越界与静默区补零。更底层的单测 ClipTimeAndPitchSourceTest.cpp 对{1,2,3,4,5}的单声道 clip 用GENERATE(forward, backward)分别拉取两次forward 得到{1,2,3}然后{4,5,0}backward 得到{5,4,3}然后{2,1,0}验证的正是“正向读 原地翻转 越界补零”这条机制。随机访问的重建开销StretchingSequenceTest.cpp#L72-L110“reconstructs segment sequence when expected”用MockAudioSegmentFactory统计工厂调用次数以下为文档中的示例计数操作工厂调用次数变化首次查询start10初始化不计—紧接着查start13位置连续、方向不变0重复查start13与预期不符1查start16且backwardstrue方向翻转1回到预期位置的 backward 查询0这组断言把“随机访问 整链重建、连续访问 零重建成本”从 readme 的纸面说法落实成了可运行的验证。此外 StretchingSequenceTest.cpp#L114-L189 有一组“with real audio”用例读取CMAKE_SOURCE_DIR下tests/samples/FifeAndDrumsStereo.wav对单 clipstretchRatio取 0.75 / 1.5与多 clip 布局分别以backwards为GENERATE(false, true)跑通整条GetFloats路径测试中写 wav 输出的outputDir默认被注释掉仅用于开发者本地听感核对。边界与限制声道数GetNext中有assert(mSequence.NChannels() 2)注释标明 “More-than-stereo isnt supported”即多于立体声的序列不受支持。随机访问开销是已知代价readme 明确承认 loop-unaware 实现在循环场景下“suboptimally”并且是刻意选择更简单代码的结果如果你扩展该库做循环播放优化ResetCursor的触发条件是主要切入点。设计阶段的定位StretchingSequence.h#L25-L27 的注释写明该类最初 “assumes forward reading”第一目标是支撑导出与渲染export and rendering反向与随机访问是在这个骨架上按backwards标志扩展进来的读代码时留意这条演进脉络。ClipSegment头文件提示其对象必须在同一线程构造与销毁因为它持有Observer::Subscription扩展时不要跨线程挪动段对象。进一步阅读可从 ClipInterface.hclip 提供的采样视图与订阅接口和 StretchingSequenceIntegrationTest.cpp 入手处理器本身的实现位于 au3-time-and-pitch 库的StaffPadTimeAndPitch它消费的就是上面描述的“已按方向整理好”的样本流。【免费下载链接】audacityAudio Editor项目地址: https://gitcode.com/GitHub_Trending/au/audacity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考