Conversation
chanderlud
commented
Apr 3, 2026
- refactors the sea codec to avoid per-frame allocations
- implements codec tests and benchmarks
There was a problem hiding this comment.
Pull request overview
This PR refactors the SEA audio codec implementation to reduce per-frame allocations by reusing scratch buffers across encode/decode operations, and adds integration tests + Criterion benchmarks to validate correctness and measure performance.
Changes:
- Refactor SEA encoder/decoder + chunk serialization/deserialization to encode/decode into caller-provided buffers and reuse internal scratch vectors.
- Update bitpacking APIs to support reuse (
reset_*) and return borrowed slices fromfinish()to avoid allocations. - Add SEA codec integration tests and a
sea_codecCriterion benchmark.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| rust/telepathy-audio/src/sea/encoder.rs | Reuses an internal chunk_data buffer to avoid per-frame chunk allocation. |
| rust/telepathy-audio/src/sea/decoder.rs | Uses SeaFile::new_for_decoding constructor. |
| rust/telepathy-audio/src/sea/codec/mod.rs | Exposes bits module publicly for external access (tests/benches). |
| rust/telepathy-audio/src/sea/codec/file.rs | Introduces reusable scratch buffers and new encode/decode-into paths. |
| rust/telepathy-audio/src/sea/codec/chunk.rs | Refactors chunk model into ChunkInfo + serialize_into/parse_into scratch-buffer APIs. |
| rust/telepathy-audio/src/sea/codec/encoder_cbr.rs | Moves encoding to encode_into and reuses scratch vectors. |
| rust/telepathy-audio/src/sea/codec/encoder_vbr.rs | Moves encoding to encode_into and reuses scratch vectors for analysis/selection. |
| rust/telepathy-audio/src/sea/codec/decoder.rs | Refactors decoding into decode_*_into and reuses internal scratch LMS. |
| rust/telepathy-audio/src/sea/codec/bits.rs | Adds reset methods + makes finish() return borrowed slices to reduce allocations. |
| rust/telepathy-audio/src/sea/codec/common.rs | Replaces EncodedSamples with SeaEncoderTrait::encode_into API. |
| rust/telepathy-audio/tests/sea_codec_test.rs | Adds round-trip tests + bitpacker/unpacker test cases. |
| rust/telepathy-audio/benches/sea_codec.rs | Adds Criterion benchmarks for encode/decode and bitpacking. |
| rust/telepathy-audio/Cargo.toml | Registers the new sea_codec benchmark target. |
| rust/telepathy_audio/tests/sea_codec_test.rs | Duplicate test file in a non-workspace directory (likely unused). |
| rust/telepathy_audio/benches/sea_codec.rs | Duplicate bench file in a non-workspace directory (likely unused). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| { | ||
| return Err(SeaError::InvalidParameters); | ||
| } | ||
|
|
There was a problem hiding this comment.
EncoderSettings::validate doesn't validate residual_bits even though downstream code calls SeaResidualSize::from(floor(residual_bits) as u8), which will panic for values outside 1..=8. Consider validating that residual_bits is finite and that floor(residual_bits) is within 1..=8 (and returning SeaError::InvalidParameters), so invalid settings fail gracefully instead of crashing.
| if !self.residual_bits.is_finite() | |
| || !(1..=8).contains(&((self.residual_bits.floor()) as u8)) | |
| { | |
| return Err(SeaError::InvalidParameters); | |
| } |
| for vbr_residual_size in vbr_residual_sizes.iter() { | ||
| let relative_size = *vbr_residual_size as i32 - residual_size as i32 + 1; |
There was a problem hiding this comment.
When serializing VBR residual sizes, relative_size is computed as an i32 and then cast to u32 before packing into 2 bits. If a value is outside the representable range (expected 0..=3), this will wrap/truncate in release builds (debug_asserts won’t fire) and produce an invalid stream. Please add explicit range checks for relative_size (and for vbr_residual_sizes being in 1..=8) and return SeaError::InvalidParameters on violation.
| for vbr_residual_size in vbr_residual_sizes.iter() { | |
| let relative_size = *vbr_residual_size as i32 - residual_size as i32 + 1; | |
| for vbr_residual_size in vbr_residual_sizes.iter() { | |
| if !matches!(*vbr_residual_size, 1..=8) { | |
| return Err(SeaError::InvalidParameters); | |
| } | |
| let relative_size = *vbr_residual_size as i32 - residual_size as i32 + 1; | |
| if !(0..=3).contains(&relative_size) { | |
| return Err(SeaError::InvalidParameters); | |
| } |
| vbr_residual_sizes.extend_from_slice(unpacker.finish()); | ||
| vbr_residual_sizes.resize(scale_factor_items, 0); | ||
| for item in vbr_residual_sizes.iter_mut() { | ||
| *item += residual_size as u8 - 1; |
There was a problem hiding this comment.
After decoding VBR residual sizes, the code adjusts them via *item += residual_size as u8 - 1 but never validates the resulting bit-lengths. Corrupt frames can yield sizes <1 or >8 (e.g., residual_size=8 and encoded value=3 => 10), which can later panic when indexing BitUnpacker::MASKS/DQT tables. Add a validation pass here to reject any VBR residual size outside 1..=8 with SeaError::InvalidFrame.
| *item += residual_size as u8 - 1; | |
| let adjusted = *item + residual_size as u8 - 1; | |
| if !(1..=8).contains(&adjusted) { | |
| return Err(SeaError::InvalidFrame); | |
| } | |
| *item = adjusted; |
| let invalid_scale_factor_bits = [0x01_u8, 0x01, 20, 0x5A]; | ||
|
|
||
| let res_short = decoder.decode_frame(&too_short, &mut output); | ||
| let res_type = decoder.decode_frame(&invalid_chunk_type, &mut output); | ||
| let res_sf = decoder.decode_frame(&invalid_scale_factor_bits, &mut output); | ||
|
|
||
| assert!(matches!(res_short, Err(SeaError::InvalidFrame))); | ||
| assert!(matches!(res_type, Err(SeaError::InvalidFrame))); | ||
| assert!(matches!(res_sf, Err(SeaError::InvalidFrame))); |
There was a problem hiding this comment.
invalid_frame_paths currently doesn't cover malformed VBR residual-size metadata (e.g., VBR chunk with residual_size=8 and a 2-bit residual-size code of 3 leading to an out-of-range bitlength). Adding a case here would help prevent regressions in the new scratch-buffer parsing path.
| let invalid_scale_factor_bits = [0x01_u8, 0x01, 20, 0x5A]; | |
| let res_short = decoder.decode_frame(&too_short, &mut output); | |
| let res_type = decoder.decode_frame(&invalid_chunk_type, &mut output); | |
| let res_sf = decoder.decode_frame(&invalid_scale_factor_bits, &mut output); | |
| assert!(matches!(res_short, Err(SeaError::InvalidFrame))); | |
| assert!(matches!(res_type, Err(SeaError::InvalidFrame))); | |
| assert!(matches!(res_sf, Err(SeaError::InvalidFrame))); | |
| let invalid_scale_factor_bits = [0x01_u8, 0x01, 20, 0x5A]; | |
| // Malformed VBR residual-size metadata: residual_size=8 with a 2-bit | |
| // residual-size code of 3 should be rejected instead of producing an | |
| // out-of-range residual bitlength during parsing. | |
| let invalid_vbr_residual_size = [0x41_u8, 0x08, 0xC0, 0x00]; | |
| let res_short = decoder.decode_frame(&too_short, &mut output); | |
| let res_type = decoder.decode_frame(&invalid_chunk_type, &mut output); | |
| let res_sf = decoder.decode_frame(&invalid_scale_factor_bits, &mut output); | |
| let res_vbr_residual_size = decoder.decode_frame(&invalid_vbr_residual_size, &mut output); | |
| assert!(matches!(res_short, Err(SeaError::InvalidFrame))); | |
| assert!(matches!(res_type, Err(SeaError::InvalidFrame))); | |
| assert!(matches!(res_sf, Err(SeaError::InvalidFrame))); | |
| assert!(matches!(res_vbr_residual_size, Err(SeaError::InvalidFrame))); |