diff --git a/sdk/audio/decode/AudioStreamDecoder.cpp b/sdk/audio/decode/AudioStreamDecoder.cpp index dd5dba769..febd7a9d7 100644 --- a/sdk/audio/decode/AudioStreamDecoder.cpp +++ b/sdk/audio/decode/AudioStreamDecoder.cpp @@ -213,9 +213,23 @@ void AudioStreamDecoder::stop() } m_stopRequest.store(false); m_abortDecode.store(false); + // #113:join 已完成,未消费的 seek 目标不得留到下一次任务。cancelRequested() + // 把「槽内有值」当取消,残留会让 stop() 返回后该谓词仍为真。start() 本就会在 + // stop() 之后清槽,这里补齐 stop() 自身的退出状态,正常路径行为等价。 + m_seekRequestMs.store(-1); m_state.store(StreamDecoderState::IDLE); } +void AudioStreamDecoder::setPendingSeekForTest(int64_t targetMs) +{ + m_seekRequestMs.store(targetMs, std::memory_order_release); +} + +int64_t AudioStreamDecoder::pendingSeekMsForTest() const +{ + return m_seekRequestMs.load(std::memory_order_acquire); +} + void AudioStreamDecoder::seekToMs(uint64_t targetMs) { if (m_state.load() == StreamDecoderState::IDLE || m_state.load() == StreamDecoderState::ERROR) { diff --git a/sdk/audio/decode/AudioStreamDecoder.h b/sdk/audio/decode/AudioStreamDecoder.h index 225e65513..216174089 100644 --- a/sdk/audio/decode/AudioStreamDecoder.h +++ b/sdk/audio/decode/AudioStreamDecoder.h @@ -70,6 +70,16 @@ class AudioStreamDecoder : public NonCopyable, // 只有解码器自己看得见的静默窗口(SEEKING / 已请求停止 / 已置取消位)里的调用才计数。 void noteSilenceFill(); + // 测试专用(决策与验收见 issue #112 / #113):直接读写「尚未被解码线程取走的 seek 目标」槽, + // 用于锁定 stop() 必须清空该槽的状态契约。AudioStreamDecoder 是 SDK 内部类型, + // 不进 soundbridge::Player 公开 API;生产路径零调用,测试里只在控制线程调用。 + // 但槽本身跨线程共享:解码线程用 exchange(-1) 取走目标,因此 setter 用 release 写、 + // getter 用 acquire 读,不能因为「只在测试里开」就降为 relaxed;stop() 的清槽沿用 + // 该槽既有的默认 seq_cst store,与 acquire 读兼容。 + // 放行仍须走「master 与分支同轮数 ctest -j4 对照」稳定性闸门,本注释不构成任何豁免。 + void setPendingSeekForTest(int64_t targetMs); + int64_t pendingSeekMsForTest() const; + // 刻意不提供同步的 decodeNext()(#18)。它曾对三类 codec 有三套语义: // FLAC/Vorbis 每次调用新建解码器 → 只解首帧、返回「有进度」但内容每轮完全相同, // 通用路径又一次性解完整段。结果是 `while (ret != EOS)` 在 FLAC/OGG 上永不终止。 diff --git a/sdk/test/unit/test_stream_decoder.cpp b/sdk/test/unit/test_stream_decoder.cpp index 33683c0de..6dcf61552 100644 --- a/sdk/test/unit/test_stream_decoder.cpp +++ b/sdk/test/unit/test_stream_decoder.cpp @@ -823,3 +823,26 @@ TEST_SUITE("AudioStreamDecoder Edge Cases") decoder.stop(); } } + + +// #113:stop() 必须清空「尚未被解码线程取走」的 seek 目标槽。 +// 不启解码线程、不依赖真实素材、不命中任何时序窗口 —— 判据确定性、毫秒级、可红可绿。 +// 背景:临时探针取证显示「seek 提交后立刻 stop」窗口每轮 2/3 次命中(160/240,三组一致), +// 但该窗口在 Windows 侧 0 崩溃;本用例锁的是状态契约本身,不宣称修复 SIGSEGV。 +TEST_SUITE("AudioStreamDecoder Stop Contract") +{ + TEST_CASE("stop clears an unconsumed seek request") + { + AudioRingBuffer ring(AudioRingBuffer::kDefaultCapacity); + AudioStreamDecoder decoder(&ring, makeDevSpec()); + + decoder.setPendingSeekForTest(5000); + // 前置 sanity:验证 setter 确实写了槽,避免 setter 失效让下面的判据假绿。 + CHECK(decoder.pendingSeekMsForTest() == 5000); + + decoder.stop(); + + CHECK_MESSAGE(decoder.pendingSeekMsForTest() == -1, + "stop() 必须清空未消费的 seek 目标,否则 cancelRequested() 在停止后仍为真"); + } +}