SeanetResnetBlock.reset_state() does not reset StreamingAdd buffers, breaking multi-call decode_step
When using Mimi.decode_step() across multiple independent audio generations (e.g. a TTS daemon serving repeated synthesis requests), audio quality degrades after the first call. The second and subsequent calls produce distorted audio with misaligned timing.
Root cause
SeanetResnetBlock.reset_state() resets the convolution blocks and the shortcut but does not reset self.streaming_add:
# moshi_mlx/modules/seanet.py
def reset_state(self):
if self.shortcut is not None:
self.shortcut.reset_state()
for b in self.block:
b.reset_state()
# streaming_add._lhs and ._rhs are never clearedStreamingAdd.step() buffers leftover partial samples in _lhs / _rhs when the residual branch and convolution branch produce different-length outputs. After a generation completes, these leftover samples persist. On the next generation, Mimi.reset_all() delegates to reset_state(), which misses StreamingAdd, so the stale samples get prepended to the new audio — misaligning every residual skip connection in the decoder.
The non-streaming __call__ path (Mimi.decode) is not affected because it uses plain xs + residual instead of StreamingAdd.step().
Fix
def reset_state(self):
if self.shortcut is not None:
self.shortcut.reset_state()
for b in self.block:
b.reset_state()
self.streaming_add._lhs = None
self.streaming_add._rhs = NoneReproduction
- Load a TTS model and Mimi codec once
- Call
mimi.reset_all()then run a full generation usingdecode_step()in a streaming callback — audio is correct - Call
mimi.reset_all()again and run a second generation — audio is distorted with wrong timing
The first generation always works. Every subsequent generation degrades because StreamingAdd carries stale partial-frame buffers across the reset boundary.
Affected version
moshi_mlx 0.3.0
Source: kyutai-labs/moshi