From 44372bbad401536ed7133c7e37440d6ed36ff515 Mon Sep 17 00:00:00 2001 From: hsd Date: Thu, 23 Apr 2026 17:04:28 +0200 Subject: [PATCH 01/13] allow dynamic lc3plus dec reconfiguration while the ISAR frame duration stays the same --- lib_com/options.h | 1 + lib_isar/isar_lc3plus_dec.c | 280 +++++++++++++++++++++++++++++++++++- 2 files changed, 280 insertions(+), 1 deletion(-) diff --git a/lib_com/options.h b/lib_com/options.h index 69964200b..625698113 100644 --- a/lib_com/options.h +++ b/lib_com/options.h @@ -165,6 +165,7 @@ #define FIX_FLOAT_1560_SVD_NO_OPT_MAX_W_SIGN /* FhG: float issue 1560: Avoid optimizing the division on the result of maxWithSign() with -funsafe-math-optimizations */ #define FIX_2095_REMOVE_UNUSED_ISAR_TABLES /* Dolby: remove unused ISAR */ #define FIX_FLOAT_1582_STEREO_DFT_QUANTIZE_ITD /* FhG: float issue 1582: Remove unncessary statement from stereo_dft_quantize_itd() */ +#define FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE /* Fhg: Allow dynamic reconfiguration of the LC3plus Decoder when bitstream format changes */ /* #################### End BE switches ################################## */ diff --git a/lib_isar/isar_lc3plus_dec.c b/lib_isar/isar_lc3plus_dec.c index de4515aa0..d3769942c 100644 --- a/lib_isar/isar_lc3plus_dec.c +++ b/lib_isar/isar_lc3plus_dec.c @@ -30,6 +30,7 @@ *******************************************************************************************************/ +#include #include #include "options.h" #include "prot.h" @@ -41,6 +42,205 @@ #include "wmc_auto.h" +#ifdef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE +static ivas_error isar_lc3plus_dec_init_handle( + const LC3PLUS_CONFIG config, /* i : LC3plus decoder configuration */ + ISAR_LC3PLUS_DEC_HANDLE *handle /* o : decoder handle */ +) +{ + LC3PLUS_Error err; + int32_t decoder_size; + int16_t i; + + if ( 0 == config.lc3plus_frame_duration_us ) + { + ISAR_LC3PLUS_DEC_Close( handle ); + return IVAS_ERROR( IVAS_ERR_WRONG_PARAMS, "Invalid lc3plus_frame_duration_us (0)\n" ); + } + + if ( config.channels > ISAR_LC3PLUS_MAX_NUM_DECODERS ) + { + ISAR_LC3PLUS_DEC_Close( handle ); + return IVAS_ERROR( IVAS_ERR_INIT_ERROR, "Maximum number of channels exceeds ISAR_LC3PLUS_MAX_NUM_DECODERS\n" ); + } + + ( *handle )->num_decs = 0; + ( *handle )->pcm_conversion_buffer = NULL; + ( *handle )->handles = NULL; + ( *handle )->selective_decoding_states = NULL; + ( *handle )->bitstream_caches = NULL; + + if ( ( ( *handle )->handles = malloc( config.channels * sizeof( ISAR_LC3PLUS_DEC_HANDLE ) ) ) == NULL ) + { + ISAR_LC3PLUS_DEC_Close( handle ); + return IVAS_ERROR( IVAS_ERR_FAILED_ALLOC, "Can not allocate memory for LC3plus wrapper handle\n" ); + } + + if ( ( ( *handle )->selective_decoding_states = malloc( config.channels * sizeof( ISAR_LC3PLUS_DEC_SELECTIVE_DECODING_STATE * ) ) ) == NULL ) + { + ISAR_LC3PLUS_DEC_Close( handle ); + return IVAS_ERROR( IVAS_ERR_FAILED_ALLOC, "Can not allocate memory for LC3plus wrapper handle\n" ); + } + + for ( i = 0; i < config.channels; ++i ) + { + ( *handle )->handles[i] = NULL; + ( *handle )->selective_decoding_states[i] = NULL; + } + + if ( ( ( *handle )->bitstream_caches = malloc( config.channels * sizeof( ISAR_LC3PLUS_DEC_BITSTREAM_CACHE * ) ) ) == NULL ) + { + ISAR_LC3PLUS_DEC_Close( handle ); + return IVAS_ERROR( IVAS_ERR_FAILED_ALLOC, "Can not allocate memory for LC3plus wrapper handle\n" ); + } + for ( i = 0; i < config.channels; ++i ) + { + ( *handle )->bitstream_caches[i] = NULL; + } + + ( *handle )->num_decs = config.channels; + for ( int32_t iCh = 0; iCh < config.channels; iCh++ ) + { + ( *handle )->selective_decoding_states[iCh] = NULL; + if ( NULL != ( *handle )->bitstream_caches ) + { + ( *handle )->bitstream_caches[iCh] = NULL; + } + /* allocate and configure LC3plus decoder */ + decoder_size = lc3plus_dec_get_size( config.samplerate, 1 ); + if ( 0 == decoder_size ) + { + ISAR_LC3PLUS_DEC_Close( handle ); + return IVAS_ERROR( IVAS_ERR_INTERNAL, "lc3plus_dec_get_size failed\n" ); + } + + if ( ( ( *handle )->handles[iCh] = malloc( decoder_size ) ) == NULL ) + { + ISAR_LC3PLUS_DEC_Close( handle ); + return IVAS_ERROR( IVAS_ERR_FAILED_ALLOC, "Can not allocate memory for LC3plus decoder\n" ); + } + + err = lc3plus_dec_init( ( *handle )->handles[iCh], config.samplerate, 1, LC3PLUS_PLC_ADVANCED, config.high_res_mode_enabled ); + + if ( LC3PLUS_OK != err ) + { + ISAR_LC3PLUS_DEC_Close( handle ); + return IVAS_ERROR( ISAR_LC3PLUS_LC3plusErrToIvasErr( err ), "lc3plus_dec_init failed\n" ); + } + + err = lc3plus_dec_set_frame_dms( ( *handle )->handles[iCh], IVAS_LC3PLUS_UsToLC3plusFrameDuration( config.lc3plus_frame_duration_us ) ); + if ( LC3PLUS_OK != err ) + { + ISAR_LC3PLUS_DEC_Close( handle ); + return IVAS_ERROR( ISAR_LC3PLUS_LC3plusErrToIvasErr( err ), "lc3plus_dec_set_frame_dms failed\n" ); + } + + /* allocate and configure per LC3plus decoder skip state */ + if ( ( ( *handle )->selective_decoding_states[iCh] = malloc( sizeof( ISAR_LC3PLUS_DEC_SELECTIVE_DECODING_STATE ) ) ) == NULL ) + { + ISAR_LC3PLUS_DEC_Close( handle ); + return IVAS_ERROR( IVAS_ERR_FAILED_ALLOC, "Can not allocate memory for LC3plus decoder\n" ); + } + + ( *handle )->selective_decoding_states[iCh]->has_skipped_a_frame = 0; + ( *handle )->selective_decoding_states[iCh]->shall_decode_cached_frame = 0; + ( *handle )->selective_decoding_states[iCh]->frame_action = DEC_ACTION_DECODE_AND_USE; + + /* allocate and configure per LC3plus decoder bitstream cache */ + if ( ( ( *handle )->bitstream_caches[iCh] = malloc( sizeof( ISAR_LC3PLUS_DEC_BITSTREAM_CACHE ) ) ) == NULL ) + { + ISAR_LC3PLUS_DEC_Close( handle ); + return IVAS_ERROR( IVAS_ERR_FAILED_ALLOC, "Can not allocate memory for LC3plus decoder\n" ); + } + + ( *handle )->bitstream_caches[iCh]->bitstream_cache_capacity = 400 /*LC3plus max non-HR octet count*/; + + if ( ( ( *handle )->bitstream_caches[iCh]->bitstream_cache = malloc( ( *handle )->bitstream_caches[iCh]->bitstream_cache_capacity ) ) == NULL ) + { + ISAR_LC3PLUS_DEC_Close( handle ); + return IVAS_ERROR( IVAS_ERR_FAILED_ALLOC, "Can not allocate memory for LC3plus decoder\n" ); + } + ( *handle )->bitstream_caches[iCh]->bitstream_cache_size = 0; + } + + ( *handle )->config = config; + if ( config.isar_frame_duration_us < config.lc3plus_frame_duration_us || config.isar_frame_duration_us % config.lc3plus_frame_duration_us != 0 ) + { + ISAR_LC3PLUS_DEC_Close( handle ); + return IVAS_ERROR( IVAS_ERR_NOT_IMPLEMENTED, "Current pcm_conversion_buffer sizing requires that lc3plus uses a shorter or equal frame duration than ivas\n" ); + } + + if ( ( ( *handle )->pcm_conversion_buffer = malloc( sizeof( int16_t ) * config.samplerate * config.lc3plus_frame_duration_us / 1000000 ) ) == NULL ) + { + ISAR_LC3PLUS_DEC_Close( handle ); + return IVAS_ERROR( IVAS_ERR_FAILED_ALLOC, "Can not allocate memory for LC3plus decoder wrapper pcm_conversion_buffer\n" ); + } + + return IVAS_ERR_OK; +} + +static void isar_lc3plus_dec_deinit_handle( + ISAR_LC3PLUS_DEC_HANDLE handle /* i/o: decoder handle */ +) +{ + if ( NULL == handle ) + { + return; + } + for ( uint32_t iDec = 0; iDec < handle->num_decs; iDec++ ) + { + if ( NULL != handle->handles && NULL != handle->handles[iDec] ) + { + lc3plus_free_decoder_structs( handle->handles[iDec] ); + free( handle->handles[iDec] ); + } + + if ( NULL != handle->selective_decoding_states && NULL != handle->selective_decoding_states[iDec] ) + { + free( handle->selective_decoding_states[iDec] ); + } + + if ( NULL != handle->bitstream_caches && NULL != handle->bitstream_caches[iDec] ) + { + free( handle->bitstream_caches[iDec]->bitstream_cache ); + free( handle->bitstream_caches[iDec] ); + } + } + + if ( NULL != handle->pcm_conversion_buffer ) + { + free( handle->pcm_conversion_buffer ); + } + free( handle->handles ); + + if ( NULL != handle->bitstream_caches ) + { + free( handle->bitstream_caches ); + } + free( handle->selective_decoding_states ); + + return; +} + +/*------------------------------------------------------------------------- + * ISAR_LC3PLUS_DEC_Open() + * + * + *------------------------------------------------------------------------*/ + +ivas_error ISAR_LC3PLUS_DEC_Open( + const LC3PLUS_CONFIG config, /* i : LC3plus decoder configuration */ + ISAR_LC3PLUS_DEC_HANDLE *handle /* o : decoder handle */ +) +{ + if ( ( *handle = malloc( sizeof( struct ISAR_LC3PLUS_DEC_HANDLE ) ) ) == NULL ) + { + return IVAS_ERROR( IVAS_ERR_FAILED_ALLOC, "Can not allocate memory for LC3plus wrapper handle\n" ); + } + + return isar_lc3plus_dec_init_handle( config, handle ); +} +#else /*------------------------------------------------------------------------- * ISAR_LC3PLUS_DEC_Open() * @@ -185,7 +385,7 @@ ivas_error ISAR_LC3PLUS_DEC_Open( return IVAS_ERR_OK; } - +#endif /*------------------------------------------------------------------------- * ISAR_LC3PLUS_DEC_GetDelay() @@ -232,6 +432,29 @@ ivas_error ISAR_LC3PLUS_DEC_GetDelay( } +#ifdef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE +/*------------------------------------------------------------------------- + * ISAR_LC3PLUS_DEC_Close() + * + * + *------------------------------------------------------------------------*/ + +void ISAR_LC3PLUS_DEC_Close( + ISAR_LC3PLUS_DEC_HANDLE *handle /* i/o: Pointer to LC3plus decoder handle */ +) +{ + if ( NULL == handle || NULL == *handle ) + { + return; + } + isar_lc3plus_dec_deinit_handle( *handle ); + + free( *handle ); + *handle = NULL; + + return; +} +#else /*------------------------------------------------------------------------- * ISAR_LC3PLUS_DEC_Close() * @@ -283,6 +506,7 @@ void ISAR_LC3PLUS_DEC_Close( return; } +#endif /*------------------------------------------------------------------------- @@ -341,6 +565,9 @@ static ivas_error isar_LC3PLUS_DEC_Decode_or_Conceal_internal( int32_t ivasSampleIndex; int16_t numSamplesPerLC3plusChannel; ivas_error err; +#ifdef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE + bool reInitRequired = false; +#endif if ( NULL == handle ) { @@ -368,7 +595,9 @@ static ivas_error isar_LC3PLUS_DEC_Decode_or_Conceal_internal( return IVAS_ERROR( IVAS_ERR_NOT_IMPLEMENTED, "isar_frame_duration_us must be equal or multiple of lc3plus_frame_duration_us \n" ); } +#ifndef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE config_num_media_times = handle->config.isar_frame_duration_us / handle->config.lc3plus_frame_duration_us; +#endif if ( !badFrameIndicator ) { if ( LC3PLUS_RTP_payload_deserialize( &payload, bitstream_in, bitstream_in_size ) != LC3PLUS_RTP_ERR_NO_ERROR ) @@ -383,6 +612,34 @@ static ivas_error isar_LC3PLUS_DEC_Decode_or_Conceal_internal( { return IVAS_ERROR( IVAS_ERR_NOT_IMPLEMENTED, "LC3plus config change (number of channels) in bitstream is not supported\n" ); } +#ifdef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE + if ( payload.frame_duration_us != handle->config.lc3plus_frame_duration_us ) + { + if (payload.num_media_times * payload.frame_duration_us == handle->config.isar_frame_duration_us) + { + reInitRequired = true; + } + else + { + return IVAS_ERROR( IVAS_ERR_NOT_IMPLEMENTED, "LC3plus config change (frame duration) in bitstream is only supported when the ISAR frame duration stays the same\n" ); + } + } + if ( payload.high_resolution_enabled != handle->config.high_res_mode_enabled ) + { + reInitRequired = true; + } + if ( payload.num_media_times != handle->config.isar_frame_duration_us / handle->config.lc3plus_frame_duration_us ) + { + if (payload.num_media_times * payload.frame_duration_us == handle->config.isar_frame_duration_us) + { + reInitRequired = true; + } + else + { + return IVAS_ERROR( IVAS_ERR_NOT_IMPLEMENTED, "LC3plus config change (number of media times per frame data block) in bitstream is only supported when the ISAR frame duration stays the same\n" ); + } + } +#else if ( payload.frame_duration_us != handle->config.lc3plus_frame_duration_us ) { return IVAS_ERROR( IVAS_ERR_NOT_IMPLEMENTED, "LC3plus config change (frame duration) in bitstream is not supported\n" ); @@ -395,8 +652,29 @@ static ivas_error isar_LC3PLUS_DEC_Decode_or_Conceal_internal( { return IVAS_ERROR( IVAS_ERR_NOT_IMPLEMENTED, "LC3plus config change (number of media times per frame data block) in bitstream is not supported\n" ); } +#endif + } + +#ifdef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE + if (reInitRequired) + { + isar_lc3plus_dec_deinit_handle( handle ); + LC3PLUS_CONFIG newConfig; + newConfig.channels = payload.num_channels; + newConfig.samplerate = payload.sampling_rate_hz; + newConfig.high_res_mode_enabled = payload.high_resolution_enabled; + newConfig.isar_frame_duration_us = payload.num_media_times * payload.frame_duration_us; + newConfig.lc3plus_frame_duration_us = payload.frame_duration_us; + newConfig.samplerate = payload.sampling_rate_hz; + err = isar_lc3plus_dec_init_handle(newConfig , &handle ); + if (err != IVAS_ERR_OK) + { + return IVAS_ERROR( err, "ISAR_LC3PLUS_DEC_Open failed\n" ); + } } + config_num_media_times = handle->config.isar_frame_duration_us / handle->config.lc3plus_frame_duration_us; +#endif numSamplesPerLC3plusChannel = (int16_t) ( handle->config.samplerate / ( 1000000 / handle->config.isar_frame_duration_us ) / config_num_media_times ); for ( iDec = 0; iDec < handle->num_decs; iDec++ ) { -- GitLab From 24b215da84c4e1ace40f4d4d9e25edd221da4500 Mon Sep 17 00:00:00 2001 From: Kacper Sagnowski Date: Mon, 27 Apr 2026 16:21:59 +0200 Subject: [PATCH 02/13] Extend isar_bstool.py with a command to patch file header (for testing purposes) Type hints were also improved. --- scripts/split_rendering/isar_bstool.py | 253 +++++++++++++++++-------- 1 file changed, 172 insertions(+), 81 deletions(-) diff --git a/scripts/split_rendering/isar_bstool.py b/scripts/split_rendering/isar_bstool.py index 10c111966..927128bd5 100755 --- a/scripts/split_rendering/isar_bstool.py +++ b/scripts/split_rendering/isar_bstool.py @@ -1,53 +1,58 @@ #!/usr/bin/env python3 """ - (C) 2022-2026 IVAS codec Public Collaboration with portions copyright Dolby International AB, Ericsson AB, - Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V., Huawei Technologies Co. LTD., - Koninklijke Philips N.V., Nippon Telegraph and Telephone Corporation, Nokia Technologies Oy, Orange, - Panasonic Holdings Corporation, Qualcomm Technologies, Inc., VoiceAge Corporation, and other - contributors to this repository. All Rights Reserved. - - This software is protected by copyright law and by international treaties. - The IVAS codec Public Collaboration consisting of Dolby International AB, Ericsson AB, - Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V., Huawei Technologies Co. LTD., - Koninklijke Philips N.V., Nippon Telegraph and Telephone Corporation, Nokia Technologies Oy, Orange, - Panasonic Holdings Corporation, Qualcomm Technologies, Inc., VoiceAge Corporation, and other - contributors to this repository retain full ownership rights in their respective contributions in - the software. This notice grants no license of any kind, including but not limited to patent - license, nor is any license granted by implication, estoppel or otherwise. - - Contributors are required to enter into the IVAS codec Public Collaboration agreement before making - contributions. - - This software is provided "AS IS", without any express or implied warranties. The software is in the - development stage. It is intended exclusively for experts who have experience with such software and - solely for the purpose of inspection. All implied warranties of non-infringement, merchantability - and fitness for a particular purpose are hereby disclaimed and excluded. - - Any dispute, controversy or claim arising under or in relation to providing this software shall be - submitted to and settled by the final, binding jurisdiction of the courts of Munich, Germany in - accordance with the laws of the Federal Republic of Germany excluding its conflict of law rules and - the United Nations Convention on Contracts on the International Sales of Goods. +(C) 2022-2026 IVAS codec Public Collaboration with portions copyright Dolby International AB, Ericsson AB, +Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V., Huawei Technologies Co. LTD., +Koninklijke Philips N.V., Nippon Telegraph and Telephone Corporation, Nokia Technologies Oy, Orange, +Panasonic Holdings Corporation, Qualcomm Technologies, Inc., VoiceAge Corporation, and other +contributors to this repository. All Rights Reserved. + +This software is protected by copyright law and by international treaties. +The IVAS codec Public Collaboration consisting of Dolby International AB, Ericsson AB, +Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V., Huawei Technologies Co. LTD., +Koninklijke Philips N.V., Nippon Telegraph and Telephone Corporation, Nokia Technologies Oy, Orange, +Panasonic Holdings Corporation, Qualcomm Technologies, Inc., VoiceAge Corporation, and other +contributors to this repository retain full ownership rights in their respective contributions in +the software. This notice grants no license of any kind, including but not limited to patent +license, nor is any license granted by implication, estoppel or otherwise. + +Contributors are required to enter into the IVAS codec Public Collaboration agreement before making +contributions. + +This software is provided "AS IS", without any express or implied warranties. The software is in the +development stage. It is intended exclusively for experts who have experience with such software and +solely for the purpose of inspection. All implied warranties of non-infringement, merchantability +and fitness for a particular purpose are hereby disclaimed and excluded. + +Any dispute, controversy or claim arising under or in relation to providing this software shall be +submitted to and settled by the final, binding jurisdiction of the courts of Munich, Germany in +accordance with the laws of the Federal Republic of Germany excluding its conflict of law rules and +the United Nations Convention on Contracts on the International Sales of Goods. """ from __future__ import annotations + import argparse +import io import math import sys +from collections.abc import Callable from pathlib import Path +from typing import Protocol, cast, final class IsarBstoolError(Exception): pass +@final class IsarBitstream: def __init__(self, file_path: Path) -> None: self.file_path = file_path with open(file_path, "rb") as reader: self.header = IsarFileHeader(reader) - self.frames = [] + self.frames: list[IsarFileFrame] = [] while reader.peek(1): self.frames.append(IsarFileFrame(reader)) @@ -138,10 +143,10 @@ class IsarBitstream: self.file_path = file_path with open(file_path, "wb") as writer: - writer.write(self.header.as_bytes) + self.header.write(writer) for frame in self.frames: - writer.write(frame.as_bytes) + frame.write(writer) def trim(self, start_time_s: float, length_s: float | None = None) -> IsarBitstream: if length_s is None: @@ -182,58 +187,64 @@ class IsarBitstream: return self.header == other.header and self.frames == other.frames -class _AsBytes: - def __init__(self) -> None: - self.as_bytes = bytearray() - - def _read(self, reader, num_bytes): - bytes_ = reader.read(num_bytes) - self.as_bytes.extend(bytes_) - return bytes_ - - def __eq__(self, value: object, /) -> bool: - if not isinstance(value, _AsBytes): - return False - return self.as_bytes == value.as_bytes +@final +class IsarFileHeader: + FILE_HEADER = b"MAIN_SPLITH" - -class IsarFileHeader(_AsBytes): - def __init__(self, reader) -> None: + def __init__(self, reader: io.BufferedReader) -> None: super().__init__() - FILE_HEADER = b"MAIN_SPLITH" - file_header_top = self._read(reader, len(FILE_HEADER)) - if file_header_top != FILE_HEADER: + file_header_top = reader.read(len(self.FILE_HEADER)) + if file_header_top != self.FILE_HEADER: raise IsarBstoolError(f"Not a valid ISAR file: {reader.name}") - self.delay_ns = _int_from_bytes(self._read(reader, 4)) - self.codec = _codec_from_bytes(self._read(reader, 4)) - self.pose_correction = _pose_corr_from_bytes(self._read(reader, 4)) - self.codec_frame_size_ms = _int_from_bytes(self._read(reader, 2)) - self.isar_frame_size_ms = _int_from_bytes(self._read(reader, 2)) - self.sample_rate = _int_from_bytes(self._read(reader, 4)) - self.lc3plus_hires = bool(_int_from_bytes(self._read(reader, 2))) - - -class IsarFileFrame(_AsBytes): - def __init__(self, reader) -> None: + self.delay_ns = _int_from_bytes(reader.read(4)) + self.codec = _codec_from_bytes(reader.read(4)) + self.pose_correction = _pose_corr_from_bytes(reader.read(4)) + self.codec_frame_size_ms = _int_from_bytes(reader.read(2)) + self.isar_frame_size_ms = _int_from_bytes(reader.read(2)) + self.sample_rate = _int_from_bytes(reader.read(4)) + self.lc3plus_hires = bool(_int_from_bytes(reader.read(2))) + + def write(self, writer: io.BufferedWriter) -> None: + _write_exact(writer, self.FILE_HEADER) + _write_exact(writer, _int_to_bytes(self.delay_ns, 4)) + _write_exact(writer, _codec_to_bytes(self.codec)) + _write_exact(writer, _pose_corr_to_bytes(self.pose_correction)) + _write_exact(writer, _int_to_bytes(self.codec_frame_size_ms, 2)) + _write_exact(writer, _int_to_bytes(self.isar_frame_size_ms, 2)) + _write_exact(writer, _int_to_bytes(self.sample_rate, 4)) + _write_exact(writer, _int_to_bytes(int(self.lc3plus_hires), 2)) + + +@final +class IsarFileFrame: + FRAME_HEADER = b"SPLIT_FRAME" + VERSION = 0 + + def __init__(self, reader: io.BufferedReader) -> None: super().__init__() - FRAME_HEADER = b"SPLIT_FRAME" - frame_header = self._read(reader, len(FRAME_HEADER)) - if frame_header != FRAME_HEADER: + frame_header = reader.read(len(self.FRAME_HEADER)) + if frame_header != self.FRAME_HEADER: raise IsarBstoolError(f"Not a valid ISAR file: {reader.name}") - version = _int_from_bytes(self._read(reader, 1)) - if version != 0: + version = _int_from_bytes(reader.read(1)) + if version != self.VERSION: raise IsarBstoolError( f"Unupported version of ISAR file format: {reader.name}" ) - self.num_bits = _int_from_bytes(self._read(reader, 4)) + self.num_bits = _int_from_bytes(reader.read(4)) payload_size = math.ceil(self.num_bits / 8) - self.payload = self._read(reader, payload_size) + self.payload = reader.read(payload_size) + + def write(self, writer: io.BufferedWriter) -> None: + _write_exact(writer, self.FRAME_HEADER) + _write_exact(writer, _int_to_bytes(self.VERSION, 1)) + _write_exact(writer, _int_to_bytes(self.num_bits, 4)) + _write_exact(writer, self.payload) ###################################################################################### @@ -241,11 +252,24 @@ class IsarFileFrame(_AsBytes): ###################################################################################### -def _int_from_bytes(bytes_): +def _write_exact(writer: io.BufferedWriter, data: bytes) -> None: + num_written = writer.write(data) + if num_written != len(data): + file_name = getattr(writer, "name", "") + raise IsarBstoolError( + f"Failed to write to {file_name}: wrote {num_written} of {len(data)} bytes" + ) + + +def _int_from_bytes(bytes_: bytes) -> int: return int.from_bytes(bytes_, byteorder="little") -def _codec_from_bytes(bytes_): +def _int_to_bytes(x: int, num_bytes: int) -> bytes: + return x.to_bytes(num_bytes, byteorder="little") + + +def _codec_from_bytes(bytes_: bytes) -> str: # Refer to ISAR_SPLIT_REND_CODEC enum in C code CODECS = ["LCLD", "LC3PLUS", "DEFAULT", "NONE"] x = _int_from_bytes(bytes_) @@ -256,7 +280,13 @@ def _codec_from_bytes(bytes_): return "UNKNOWN" -def _pose_corr_from_bytes(bytes_): +def _codec_to_bytes(codec: str) -> bytes: + # Refer to ISAR_SPLIT_REND_CODEC enum in C code + CODECS = {"LCLD": 0, "LC3PLUS": 1, "DEFAULT": 2, "NONE": 3} + return CODECS.get(codec, 255).to_bytes(4, byteorder="little") + + +def _pose_corr_from_bytes(bytes_: bytes) -> str: # Refer to ISAR_SPLIT_REND_POSE_CORRECTION_MODE enum in C code POSE_CORR_MODES = ["NONE", "CLDFB"] x = _int_from_bytes(bytes_) @@ -267,12 +297,23 @@ def _pose_corr_from_bytes(bytes_): return "UNKNOWN" +def _pose_corr_to_bytes(pose_corr: str) -> bytes: + # Refer to ISAR_SPLIT_REND_POSE_CORRECTION_MODE enum in C code + POSE_CORR_MODES = {"NONE": 0, "CLDFB": 1} + return POSE_CORR_MODES.get(pose_corr, 255).to_bytes(4, byteorder="little") + + ###################################################################################### # subcommand functions ###################################################################################### -def _subcmd_info(args): +class _InfoArgs(Protocol): + file_in: Path + only: str | None + + +def _subcmd_info(args: _InfoArgs) -> None: bs = IsarBitstream(args.file_in) match args.only: @@ -314,9 +355,37 @@ def _subcmd_info(args): raise IsarBstoolError(f"Not a valid parameter value: '{args.only}'") -def _subcmd_trim(args): +class _TrimArgs(Protocol): + file_in: Path + file_out: Path + start_time: str + length: str | None + + +def _subcmd_trim(args: _TrimArgs) -> None: bs = IsarBitstream(args.file_in) - bs.trim(float(args.start_time), float(args.length) if args.length else None) + _ = bs.trim(float(args.start_time), float(args.length) if args.length else None) + bs.write(args.file_out) + + +class _PatchCodecFrameSizeArgs(Protocol): + file_in: Path + file_out: Path + codec_frame_size_ms: str + + +def _subcmd_patch_codec_frame_size(args: _PatchCodecFrameSizeArgs) -> None: + bs = IsarBitstream(args.file_in) + codec_frame_size_ms = int(args.codec_frame_size_ms) + + match codec_frame_size_ms: + case 5 | 10 | 20: + bs.header.codec_frame_size_ms = codec_frame_size_ms + case _: + raise IsarBstoolError( + f"Invalid codec frame size (in ms): {args.codec_frame_size_ms}. Valid values are 5 and 10." + ) + bs.write(args.file_out) @@ -324,17 +393,18 @@ def _subcmd_trim(args): # main ###################################################################################### + if __name__ == "__main__": parser = argparse.ArgumentParser( prog="isar_bstool", description="Utility for inspecting and modifying ISAR bitstreams", ) - parser.set_defaults(func=lambda _: parser.print_help()) + parser.set_defaults(func=lambda: parser.print_help()) subparsers = parser.add_subparsers(title="Commands") info = subparsers.add_parser("info", help="Print information about a bitstream") - info.add_argument("file_in", help="Path to input file") - info.add_argument( + _ = info.add_argument("file_in", type=Path, help="Path to input file") + _ = info.add_argument( "--only", help="Print only a specific parameter", default=None, @@ -362,23 +432,44 @@ if __name__ == "__main__": trim = subparsers.add_parser( "trim", help="Remove initial frames from a bitstream file" ) - trim.add_argument("file_in", help="Path to input file") - trim.add_argument("file_out", help="Path to output file") - trim.add_argument( + _ = trim.add_argument("file_in", type=Path, help="Path to input file") + _ = trim.add_argument("file_out", type=Path, help="Path to output file") + _ = trim.add_argument( "start_time", help="Start point (in s) from which content should be copied to the output.", ) - trim.add_argument( + _ = trim.add_argument( "--length", help="Amount of time (in s) to copy to the output. If not given, content is copied until the end of the input is reached.", default=None, ) trim.set_defaults(func=_subcmd_trim) + patch_codec_frame_size = subparsers.add_parser( + "patch_codec_frame_size", + help="Overwrite the codec frame size field in the header of an ISAR bitstream file", + description=( + "Note that this doesn't modify the actual frames in the file. The purpose of this command " + "is to inject incorrect frame size info into the header for testing LC3plus reconfiguration handling." + ), + ) + _ = patch_codec_frame_size.add_argument( + "file_in", type=Path, help="Path to input file" + ) + _ = patch_codec_frame_size.add_argument( + "file_out", type=Path, help="Path to output file" + ) + _ = patch_codec_frame_size.add_argument( + "codec_frame_size_ms", + help="Codec frame size (in ms) to write into the header of the output file", + ) + patch_codec_frame_size.set_defaults(func=_subcmd_patch_codec_frame_size) + args = parser.parse_args() + func = cast(Callable[[argparse.Namespace], None], args.func) try: - args.func(args) + func(args) except (FileNotFoundError, PermissionError, IsarBstoolError) as e: print(e, file=sys.stderr) sys.exit(1) -- GitLab From 8acb9b229a5346ed49b25ac6097079e0359f88bb Mon Sep 17 00:00:00 2001 From: Kacper Sagnowski Date: Mon, 27 Apr 2026 17:39:53 +0200 Subject: [PATCH 03/13] Add test for LC3plus framing reconfiguration --- tests/split_rendering/constants.py | 3 ++ tests/split_rendering/test_split_rendering.py | 35 +++++++++++++++++++ tests/split_rendering/utils.py | 27 ++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/tests/split_rendering/constants.py b/tests/split_rendering/constants.py index f7b222180..fccd044c7 100644 --- a/tests/split_rendering/constants.py +++ b/tests/split_rendering/constants.py @@ -82,6 +82,9 @@ RENDERER_CONFIGS_FASTCONV_RENDERER = [ RENDERER_CONFIGS_FRAMING = [ str(cfg.stem) for cfg in RENDER_FRAMING_CFG_DIR.glob("framing*.txt") ] +RENDERER_CONFIGS_FRAMING_LC3PLUS = [ + str(cfg.stem) for cfg in RENDER_FRAMING_CFG_DIR.glob("framing_lc3plus*.txt") +] RENDERER_CONFIGS_TO_TEST_AMBI = ( RENDERER_CONFIGS_DEFAULT_CODEC + RENDERER_CONFIGS_LC3PLUS_CODEC diff --git a/tests/split_rendering/test_split_rendering.py b/tests/split_rendering/test_split_rendering.py index 7d0bb9d1c..75451d7ce 100644 --- a/tests/split_rendering/test_split_rendering.py +++ b/tests/split_rendering/test_split_rendering.py @@ -740,3 +740,38 @@ def test_framing_combinations_full_chain_split( get_odg_bin=get_odg_bin, delay_profile=SCRIPTS_DIR / "dly_error_profiles" / f"{delay_profile}.dat" if delay_profile else None, ) + + +""" +Tests cases where the transport codec framing information is incorrect and the LC3plus decoder needs +to reconfigure on the first frame. Required for RTP use cases. +""" +@pytest.mark.parametrize("render_config", RENDERER_CONFIGS_FRAMING_LC3PLUS) +@pytest.mark.parametrize("pre_rend_fr", SPLIT_RENDERER_PRE_FRAMINGS) +@pytest.mark.parametrize("post_rend_fr", SPLIT_RENDERER_POST_FRAMINGS) +@pytest.mark.parametrize("patch_codec_frame_size_ms", [5, 10]) +def test_lc3plus_framing_reconfiguration( + record_property, + props_to_record, + test_info, + render_config, + post_rend_fr, + pre_rend_fr, + patch_codec_frame_size_ms, +): + post_trajectory = HR_TRAJECTORY_DIR.joinpath("rotate_euler_quaternion_30s.csv") + pre_trajectory = post_trajectory.with_stem(f"{post_trajectory.stem}_delayed") + + run_external_split_rendering( + record_property, + props_to_record, + test_info, + in_fmt="5_1", + render_config=RENDER_FRAMING_CFG_DIR.joinpath(f"{render_config}.txt"), + pre_trajectory=pre_trajectory, + post_trajectory=post_trajectory, + binary_suffix=EXE_SUFFIX, + post_rend_fr=post_rend_fr, + pre_rend_fr=pre_rend_fr, + patch_codec_frame_size_ms=patch_codec_frame_size_ms + ) diff --git a/tests/split_rendering/utils.py b/tests/split_rendering/utils.py index 2f878c0eb..7c5414a2f 100644 --- a/tests/split_rendering/utils.py +++ b/tests/split_rendering/utils.py @@ -61,6 +61,7 @@ from ..conftest import parse_properties sys.path.append(SCRIPTS_DIR) from pyaudio3dtools.audiofile import readfile, writefile +from split_rendering.isar_bstool import IsarBitstream def lc3plus_used(test_info, in_fmt, render_config): @@ -345,6 +346,7 @@ def run_external_split_rendering( get_ssnr=False, get_odg=False, get_odg_bin=False, + patch_codec_frame_size_ms: int | None = None, ) -> Tuple[np.ndarray, int]: """ Runs the exeternal split rendering chain consisting of @@ -362,6 +364,9 @@ def run_external_split_rendering( if plc_error_pattern: filename_base += f"_plc_{plc_error_pattern.stem}" + if patch_codec_frame_size_ms: + filename_base += f"_tcfrmod_{patch_codec_frame_size_ms}ms" + split_bitstream_stem = f"{filename_base}.splt.bit" if renderer_fmt == "BINAURAL_SPLIT_PCM": split_md_file_stem = f"{filename_base}.spltmd.bit" @@ -415,6 +420,14 @@ def run_external_split_rendering( run_isar_ext_rend_cmd(split_pre_cmd, test_info=test_info) + # If patch_codec_frame_size_ms is set, overwrite the frame size in the split bitstream + # header with the provided value. This tests LC3plus framing reconfiguration. + if patch_codec_frame_size_ms: + isar_bs = IsarBitstream(split_bitstream) + isar_bs.header.codec_frame_size_ms = patch_codec_frame_size_ms + split_bitstream_mod = split_bitstream.with_stem(split_bitstream.stem + f"_patched") + isar_bs.write(split_bitstream_mod) + # run ISAR post-renderer split_post_cmd = SPLIT_POST_REND_CMD[:] @@ -435,6 +448,20 @@ def run_external_split_rendering( run_isar_ext_rend_cmd(split_post_cmd, test_info=test_info) + if patch_codec_frame_size_ms: + split_post_cmd[4] = str(split_bitstream_mod) + out_file_mod = out_file.with_stem(out_file.stem + f"_patched") + split_post_cmd[8] = str(out_file_mod) + run_isar_ext_rend_cmd(split_post_cmd, test_info=test_info) + output_differs, reason = cmp_pcm( + out_file, + out_file_mod, + 2, # is always "BINAURAL", + 48000, # currently only 48 kHz tests + ) + if output_differs[0]: + pytest.fail(f"ISAR output differs when codec frame size in bitstream is patched: ({reason[0]})") + if test_info.config.option.create_cut: # CUT creation mode will run a comparison with REF out_file_ref = OUTPUT_PATH_REF.joinpath(out_file_stem) -- GitLab From f11560a1ba2b002029485c007f61b86f4cfa26bf Mon Sep 17 00:00:00 2001 From: Kacper Sagnowski Date: Tue, 28 Apr 2026 16:56:39 +0200 Subject: [PATCH 04/13] Correctly differentiate between ISAR frame size, transport codec frame size and the ISAR_post_rend output frame size --- apps/decoder.c | 8 ++++++++ apps/isar_post_rend.c | 15 +++++++++++++++ lib_isar/lib_isar_pre_rend.c | 3 +++ 3 files changed, 26 insertions(+) diff --git a/apps/decoder.c b/apps/decoder.c index dd9885e7c..4500e97e8 100644 --- a/apps/decoder.c +++ b/apps/decoder.c @@ -3897,9 +3897,17 @@ static ivas_error decodeVoIP( } else if ( decodedGoodFrame ) { +#ifdef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE + srInfo.bitrateKbps = splitRendBits->bits_written * 1000 / splitRendBits->isar_frame_size_ms; +#else srInfo.bitrateKbps = splitRendBits->bits_written * 1000 / splitRendBits->codec_frame_size_ms; +#endif srInfo.codec = ( splitRendBits->codec == ISAR_SPLIT_REND_CODEC_LC3PLUS ) ? IVAS_SR_TRANSPORT_LC3PLUS : IVAS_SR_TRANSPORT_LCLD; +#ifdef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE + srInfo.codecFrameSizeMs = (uint32_t) splitRendBits->isar_frame_size_ms; +#else srInfo.codecFrameSizeMs = (uint32_t) splitRendBits->codec_frame_size_ms; +#endif if ( ( error = IVAS_RTP_WriteNextFrame( &srRtp, splitRendBits->bits_buf, &srInfo, (int16_t) splitRendBits->bits_written, false, false ) ) != IVAS_ERR_OK ) { diff --git a/apps/isar_post_rend.c b/apps/isar_post_rend.c index c3790facb..60705be06 100644 --- a/apps/isar_post_rend.c +++ b/apps/isar_post_rend.c @@ -812,6 +812,13 @@ static ivas_error parseSRParamsFile( *codec = ( srInfo.codec == IVAS_SR_TRANSPORT_LCLD ) ? ISAR_SPLIT_REND_CODEC_LCLD : ISAR_SPLIT_REND_CODEC_LC3PLUS; *codec_frame_size_ms = (int16_t) srInfo.codecFrameSizeMs; *isar_frame_size_ms = *codec_frame_size_ms; /* for rtp force codec framesize as isar renderer frame size */ +#ifdef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE + if ( *codec == ISAR_SPLIT_REND_CODEC_LC3PLUS && *isar_frame_size_ms == 20 ) + { + /* For LC3plus, the codec frame size is limited to max 10 ms */ + *codec_frame_size_ms = 10; + } +#endif break; } } @@ -1194,7 +1201,11 @@ int main( bitsBuffer.config.bitsRead = 0; bitsBuffer.config.bitsWritten = 0; +#ifdef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE + while ( frameMS < (int16_t) ( args.render_num_subframes * 5 ) ) +#else while ( frameMS < bitsBuffer.config.isar_frame_size_ms ) +#endif { error = IVAS_RTP_ReadNextFrame( &srRTP, bitBuffer, &auSizeBits, &rtpTimeStamp, &rtpSequenceNumber, &nextPacketRcvTime_ms, &srInfo, &qBit ); if ( error != IVAS_ERR_OK ) @@ -1220,7 +1231,11 @@ int main( bitBuffer += ( auSizeBits + 7 ) / 8; bitsBuffer.config.bitsWritten += auSizeBits; bitsBuffer.config.codec = srInfo.codec == IVAS_SR_TRANSPORT_LC3PLUS ? ISAR_SPLIT_REND_CODEC_LC3PLUS : ISAR_SPLIT_REND_CODEC_LCLD; +#ifdef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE + frameMS += bitsBuffer.config.isar_frame_size_ms; +#else frameMS += bitsBuffer.config.codec_frame_size_ms; +#endif } } else if ( ( hSplitRendFileReadWrite != NULL ) && splitBinNeedsNewFrame ) diff --git a/lib_isar/lib_isar_pre_rend.c b/lib_isar/lib_isar_pre_rend.c index 6ba81ec70..0f3821de8 100644 --- a/lib_isar/lib_isar_pre_rend.c +++ b/lib_isar/lib_isar_pre_rend.c @@ -354,6 +354,9 @@ ivas_error ISAR_PRE_REND_MultiBinToSplitBinaural( available_bits = ( SplitRendBitRate * hSplitBin->hSplitBinLCLDEnc->iNumBlocks * hSplitBin->hSplitBinLCLDEnc->iNumIterations ) / ( 16 * FRAMES_PER_SEC ); available_bits -= pBits->bits_written; pBits->codec_frame_size_ms = codec_frame_size_ms; +#ifdef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE + pBits->isar_frame_size_ms = isar_frame_size_ms; +#endif isar_splitBinLCLDEncProcess( hSplitBin->hSplitBinLCLDEnc, Cldfb_In_BinReal, Cldfb_In_BinImag, available_bits, pBits ); } else -- GitLab From ff9e6f525e2415f14968fc43ee505a259f2c5c3a Mon Sep 17 00:00:00 2001 From: Kacper Sagnowski Date: Tue, 28 Apr 2026 17:10:19 +0200 Subject: [PATCH 05/13] Update preprocessor switch name --- apps/decoder.c | 4 ++-- apps/isar_post_rend.c | 6 +++--- lib_com/options.h | 2 +- lib_isar/isar_lc3plus_dec.c | 12 ++++++------ lib_isar/lib_isar_pre_rend.c | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/decoder.c b/apps/decoder.c index 4500e97e8..57e8ac75c 100644 --- a/apps/decoder.c +++ b/apps/decoder.c @@ -3897,13 +3897,13 @@ static ivas_error decodeVoIP( } else if ( decodedGoodFrame ) { -#ifdef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE +#ifdef FIX_1515_ISAR_FRAME_SIZES_IN_RTP srInfo.bitrateKbps = splitRendBits->bits_written * 1000 / splitRendBits->isar_frame_size_ms; #else srInfo.bitrateKbps = splitRendBits->bits_written * 1000 / splitRendBits->codec_frame_size_ms; #endif srInfo.codec = ( splitRendBits->codec == ISAR_SPLIT_REND_CODEC_LC3PLUS ) ? IVAS_SR_TRANSPORT_LC3PLUS : IVAS_SR_TRANSPORT_LCLD; -#ifdef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE +#ifdef FIX_1515_ISAR_FRAME_SIZES_IN_RTP srInfo.codecFrameSizeMs = (uint32_t) splitRendBits->isar_frame_size_ms; #else srInfo.codecFrameSizeMs = (uint32_t) splitRendBits->codec_frame_size_ms; diff --git a/apps/isar_post_rend.c b/apps/isar_post_rend.c index 60705be06..c85f190cf 100644 --- a/apps/isar_post_rend.c +++ b/apps/isar_post_rend.c @@ -812,7 +812,7 @@ static ivas_error parseSRParamsFile( *codec = ( srInfo.codec == IVAS_SR_TRANSPORT_LCLD ) ? ISAR_SPLIT_REND_CODEC_LCLD : ISAR_SPLIT_REND_CODEC_LC3PLUS; *codec_frame_size_ms = (int16_t) srInfo.codecFrameSizeMs; *isar_frame_size_ms = *codec_frame_size_ms; /* for rtp force codec framesize as isar renderer frame size */ -#ifdef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE +#ifdef FIX_1515_ISAR_FRAME_SIZES_IN_RTP if ( *codec == ISAR_SPLIT_REND_CODEC_LC3PLUS && *isar_frame_size_ms == 20 ) { /* For LC3plus, the codec frame size is limited to max 10 ms */ @@ -1201,7 +1201,7 @@ int main( bitsBuffer.config.bitsRead = 0; bitsBuffer.config.bitsWritten = 0; -#ifdef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE +#ifdef FIX_1515_ISAR_FRAME_SIZES_IN_RTP while ( frameMS < (int16_t) ( args.render_num_subframes * 5 ) ) #else while ( frameMS < bitsBuffer.config.isar_frame_size_ms ) @@ -1231,7 +1231,7 @@ int main( bitBuffer += ( auSizeBits + 7 ) / 8; bitsBuffer.config.bitsWritten += auSizeBits; bitsBuffer.config.codec = srInfo.codec == IVAS_SR_TRANSPORT_LC3PLUS ? ISAR_SPLIT_REND_CODEC_LC3PLUS : ISAR_SPLIT_REND_CODEC_LCLD; -#ifdef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE +#ifdef FIX_1515_ISAR_FRAME_SIZES_IN_RTP frameMS += bitsBuffer.config.isar_frame_size_ms; #else frameMS += bitsBuffer.config.codec_frame_size_ms; diff --git a/lib_com/options.h b/lib_com/options.h index 625698113..c591fd3f4 100644 --- a/lib_com/options.h +++ b/lib_com/options.h @@ -165,7 +165,7 @@ #define FIX_FLOAT_1560_SVD_NO_OPT_MAX_W_SIGN /* FhG: float issue 1560: Avoid optimizing the division on the result of maxWithSign() with -funsafe-math-optimizations */ #define FIX_2095_REMOVE_UNUSED_ISAR_TABLES /* Dolby: remove unused ISAR */ #define FIX_FLOAT_1582_STEREO_DFT_QUANTIZE_ITD /* FhG: float issue 1582: Remove unncessary statement from stereo_dft_quantize_itd() */ -#define FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE /* Fhg: Allow dynamic reconfiguration of the LC3plus Decoder when bitstream format changes */ +#define FIX_1515_ISAR_FRAME_SIZES_IN_RTP /* FhG: Fix ISAR frame sizes in RTP. This includes reconfiguration of the LC3plus decoder */ /* #################### End BE switches ################################## */ diff --git a/lib_isar/isar_lc3plus_dec.c b/lib_isar/isar_lc3plus_dec.c index d3769942c..9f9d932d3 100644 --- a/lib_isar/isar_lc3plus_dec.c +++ b/lib_isar/isar_lc3plus_dec.c @@ -42,7 +42,7 @@ #include "wmc_auto.h" -#ifdef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE +#ifdef FIX_1515_ISAR_FRAME_SIZES_IN_RTP /* "open" and "close" were refactored (mostly copy-paste) into separate steps of (de)allocate handle and (de)init handle */ static ivas_error isar_lc3plus_dec_init_handle( const LC3PLUS_CONFIG config, /* i : LC3plus decoder configuration */ ISAR_LC3PLUS_DEC_HANDLE *handle /* o : decoder handle */ @@ -432,7 +432,7 @@ ivas_error ISAR_LC3PLUS_DEC_GetDelay( } -#ifdef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE +#ifdef FIX_1515_ISAR_FRAME_SIZES_IN_RTP /*------------------------------------------------------------------------- * ISAR_LC3PLUS_DEC_Close() * @@ -565,7 +565,7 @@ static ivas_error isar_LC3PLUS_DEC_Decode_or_Conceal_internal( int32_t ivasSampleIndex; int16_t numSamplesPerLC3plusChannel; ivas_error err; -#ifdef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE +#ifdef FIX_1515_ISAR_FRAME_SIZES_IN_RTP bool reInitRequired = false; #endif @@ -595,7 +595,7 @@ static ivas_error isar_LC3PLUS_DEC_Decode_or_Conceal_internal( return IVAS_ERROR( IVAS_ERR_NOT_IMPLEMENTED, "isar_frame_duration_us must be equal or multiple of lc3plus_frame_duration_us \n" ); } -#ifndef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE +#ifndef FIX_1515_ISAR_FRAME_SIZES_IN_RTP config_num_media_times = handle->config.isar_frame_duration_us / handle->config.lc3plus_frame_duration_us; #endif if ( !badFrameIndicator ) @@ -612,7 +612,7 @@ static ivas_error isar_LC3PLUS_DEC_Decode_or_Conceal_internal( { return IVAS_ERROR( IVAS_ERR_NOT_IMPLEMENTED, "LC3plus config change (number of channels) in bitstream is not supported\n" ); } -#ifdef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE +#ifdef FIX_1515_ISAR_FRAME_SIZES_IN_RTP if ( payload.frame_duration_us != handle->config.lc3plus_frame_duration_us ) { if (payload.num_media_times * payload.frame_duration_us == handle->config.isar_frame_duration_us) @@ -655,7 +655,7 @@ static ivas_error isar_LC3PLUS_DEC_Decode_or_Conceal_internal( #endif } -#ifdef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE +#ifdef FIX_1515_ISAR_FRAME_SIZES_IN_RTP if (reInitRequired) { isar_lc3plus_dec_deinit_handle( handle ); diff --git a/lib_isar/lib_isar_pre_rend.c b/lib_isar/lib_isar_pre_rend.c index 0f3821de8..cee1f3d9b 100644 --- a/lib_isar/lib_isar_pre_rend.c +++ b/lib_isar/lib_isar_pre_rend.c @@ -354,7 +354,7 @@ ivas_error ISAR_PRE_REND_MultiBinToSplitBinaural( available_bits = ( SplitRendBitRate * hSplitBin->hSplitBinLCLDEnc->iNumBlocks * hSplitBin->hSplitBinLCLDEnc->iNumIterations ) / ( 16 * FRAMES_PER_SEC ); available_bits -= pBits->bits_written; pBits->codec_frame_size_ms = codec_frame_size_ms; -#ifdef FIX_1234_SPLIT_REND_LC3PLUS_RECONFIGURE +#ifdef FIX_1515_ISAR_FRAME_SIZES_IN_RTP pBits->isar_frame_size_ms = isar_frame_size_ms; #endif isar_splitBinLCLDEncProcess( hSplitBin->hSplitBinLCLDEnc, Cldfb_In_BinReal, Cldfb_In_BinImag, available_bits, pBits ); -- GitLab From 00a40bb00e9154e82fb29e917b77a57507388628 Mon Sep 17 00:00:00 2001 From: Kacper Sagnowski Date: Tue, 28 Apr 2026 17:18:48 +0200 Subject: [PATCH 06/13] Accept leftover part of old accepted switch - only affects function name --- .../lc3plus_float/ivas_lc3plus_unit_test.c | 8 -------- 1 file changed, 8 deletions(-) diff --git a/scripts/split_rendering/lc3plus_float/ivas_lc3plus_unit_test.c b/scripts/split_rendering/lc3plus_float/ivas_lc3plus_unit_test.c index 5ba66f721..6202307a5 100644 --- a/scripts/split_rendering/lc3plus_float/ivas_lc3plus_unit_test.c +++ b/scripts/split_rendering/lc3plus_float/ivas_lc3plus_unit_test.c @@ -576,11 +576,7 @@ static int encodeAndDecodeOneStereoFrameIvas10msLc3_10ms_48kHz_96kbpsPerChannel( return encodeAndDecodeOneStereoFrame( config, config.channels * 98 * 1000 ); } -#ifdef LC3PLUS_LEA_COMPAT_BITRATES_48_6 static int encodeAndDecodeOneStereoFrameIvas10msLc3_10ms_48kHz_126kbpsPerChannel( void ) -#else -static int encodeAndDecodeOneStereoFrameIvas10msLc3_10ms_48kHz_124kbpsPerChannel( void ) -#endif { LC3PLUS_CONFIG config = { .lc3plus_frame_duration_us = 10 * 1000, .isar_frame_duration_us = 10 * 1000, .channels = 2, .samplerate = 48000, .high_res_mode_enabled = 0 }; return encodeAndDecodeOneStereoFrame( config, config.channels * 126 * 1000 ); @@ -691,11 +687,7 @@ int main( ret = encodeAndDecodeOneStereoFrameIvas10msLc3_10ms_48kHz_96kbpsPerChannel(); if ( ret != 0 ) return 1; -#ifdef LC3PLUS_LEA_COMPAT_BITRATES_48_6 ret = encodeAndDecodeOneStereoFrameIvas10msLc3_10ms_48kHz_126kbpsPerChannel(); -#else - ret = encodeAndDecodeOneStereoFrameIvas10msLc3_10ms_48kHz_124kbpsPerChannel(); -#endif if ( ret != 0 ) return 1; ret = encodeAndDecodeOneStereoFrameIvas10msLc3_10ms_48kHz_800kbpsPerChannel(); -- GitLab From aa080c27c410876622d4447d769f2348ca14b1aa Mon Sep 17 00:00:00 2001 From: Kacper Sagnowski Date: Tue, 28 Apr 2026 18:10:52 +0200 Subject: [PATCH 07/13] Fix formatting --- lib_isar/isar_lc3plus_dec.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib_isar/isar_lc3plus_dec.c b/lib_isar/isar_lc3plus_dec.c index 9f9d932d3..0a56e19dd 100644 --- a/lib_isar/isar_lc3plus_dec.c +++ b/lib_isar/isar_lc3plus_dec.c @@ -175,12 +175,12 @@ static ivas_error isar_lc3plus_dec_init_handle( ISAR_LC3PLUS_DEC_Close( handle ); return IVAS_ERROR( IVAS_ERR_FAILED_ALLOC, "Can not allocate memory for LC3plus decoder wrapper pcm_conversion_buffer\n" ); } - + return IVAS_ERR_OK; } static void isar_lc3plus_dec_deinit_handle( - ISAR_LC3PLUS_DEC_HANDLE handle /* i/o: decoder handle */ + ISAR_LC3PLUS_DEC_HANDLE handle /* i/o: decoder handle */ ) { if ( NULL == handle ) @@ -615,7 +615,7 @@ static ivas_error isar_LC3PLUS_DEC_Decode_or_Conceal_internal( #ifdef FIX_1515_ISAR_FRAME_SIZES_IN_RTP if ( payload.frame_duration_us != handle->config.lc3plus_frame_duration_us ) { - if (payload.num_media_times * payload.frame_duration_us == handle->config.isar_frame_duration_us) + if ( payload.num_media_times * payload.frame_duration_us == handle->config.isar_frame_duration_us ) { reInitRequired = true; } @@ -630,7 +630,7 @@ static ivas_error isar_LC3PLUS_DEC_Decode_or_Conceal_internal( } if ( payload.num_media_times != handle->config.isar_frame_duration_us / handle->config.lc3plus_frame_duration_us ) { - if (payload.num_media_times * payload.frame_duration_us == handle->config.isar_frame_duration_us) + if ( payload.num_media_times * payload.frame_duration_us == handle->config.isar_frame_duration_us ) { reInitRequired = true; } @@ -656,7 +656,7 @@ static ivas_error isar_LC3PLUS_DEC_Decode_or_Conceal_internal( } #ifdef FIX_1515_ISAR_FRAME_SIZES_IN_RTP - if (reInitRequired) + if ( reInitRequired ) { isar_lc3plus_dec_deinit_handle( handle ); LC3PLUS_CONFIG newConfig; @@ -665,9 +665,9 @@ static ivas_error isar_LC3PLUS_DEC_Decode_or_Conceal_internal( newConfig.high_res_mode_enabled = payload.high_resolution_enabled; newConfig.isar_frame_duration_us = payload.num_media_times * payload.frame_duration_us; newConfig.lc3plus_frame_duration_us = payload.frame_duration_us; - newConfig.samplerate = payload.sampling_rate_hz; - err = isar_lc3plus_dec_init_handle(newConfig , &handle ); - if (err != IVAS_ERR_OK) + newConfig.samplerate = payload.sampling_rate_hz; + err = isar_lc3plus_dec_init_handle( newConfig, &handle ); + if ( err != IVAS_ERR_OK ) { return IVAS_ERROR( err, "ISAR_LC3PLUS_DEC_Open failed\n" ); } -- GitLab From 3a3dc1f17485cb8da676768a2995edc58c9a6c3c Mon Sep 17 00:00:00 2001 From: Kacper Sagnowski Date: Tue, 28 Apr 2026 18:15:49 +0200 Subject: [PATCH 08/13] Fix integer conversion warnings --- lib_isar/isar_lc3plus_dec.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib_isar/isar_lc3plus_dec.c b/lib_isar/isar_lc3plus_dec.c index 0a56e19dd..8cec8106d 100644 --- a/lib_isar/isar_lc3plus_dec.c +++ b/lib_isar/isar_lc3plus_dec.c @@ -663,8 +663,8 @@ static ivas_error isar_LC3PLUS_DEC_Decode_or_Conceal_internal( newConfig.channels = payload.num_channels; newConfig.samplerate = payload.sampling_rate_hz; newConfig.high_res_mode_enabled = payload.high_resolution_enabled; - newConfig.isar_frame_duration_us = payload.num_media_times * payload.frame_duration_us; - newConfig.lc3plus_frame_duration_us = payload.frame_duration_us; + newConfig.isar_frame_duration_us = (int16_t) ( payload.num_media_times * payload.frame_duration_us ); + newConfig.lc3plus_frame_duration_us = (int16_t) payload.frame_duration_us; newConfig.samplerate = payload.sampling_rate_hz; err = isar_lc3plus_dec_init_handle( newConfig, &handle ); if ( err != IVAS_ERR_OK ) -- GitLab From 21b250efc3059169a1493c26ba0462c3f207573a Mon Sep 17 00:00:00 2001 From: Kacper Sagnowski Date: Tue, 28 Apr 2026 18:34:13 +0200 Subject: [PATCH 09/13] Mark test with invalid conditions as xfail --- tests/split_rendering/test_split_rendering.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/split_rendering/test_split_rendering.py b/tests/split_rendering/test_split_rendering.py index 75451d7ce..88ba30816 100644 --- a/tests/split_rendering/test_split_rendering.py +++ b/tests/split_rendering/test_split_rendering.py @@ -759,6 +759,9 @@ def test_lc3plus_framing_reconfiguration( pre_rend_fr, patch_codec_frame_size_ms, ): + if int(patch_codec_frame_size_ms) > int(pre_rend_fr): + pytest.xfail("Codec frame size must fit within the ISAR frame size") + post_trajectory = HR_TRAJECTORY_DIR.joinpath("rotate_euler_quaternion_30s.csv") pre_trajectory = post_trajectory.with_stem(f"{post_trajectory.stem}_delayed") -- GitLab From d77b6bdbceda257caf3a772857942a60c33d3377 Mon Sep 17 00:00:00 2001 From: hsd Date: Tue, 28 Apr 2026 19:10:08 +0200 Subject: [PATCH 10/13] fix vs warning "potentially uninitialized local variable 'payload'" by making sure we only reinitialize on good frames --- lib_isar/isar_lc3plus_dec.c | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/lib_isar/isar_lc3plus_dec.c b/lib_isar/isar_lc3plus_dec.c index 8cec8106d..fa5ad3b05 100644 --- a/lib_isar/isar_lc3plus_dec.c +++ b/lib_isar/isar_lc3plus_dec.c @@ -639,6 +639,22 @@ static ivas_error isar_LC3PLUS_DEC_Decode_or_Conceal_internal( return IVAS_ERROR( IVAS_ERR_NOT_IMPLEMENTED, "LC3plus config change (number of media times per frame data block) in bitstream is only supported when the ISAR frame duration stays the same\n" ); } } + if ( reInitRequired ) + { + isar_lc3plus_dec_deinit_handle( handle ); + LC3PLUS_CONFIG newConfig; + newConfig.channels = payload.num_channels; + newConfig.samplerate = payload.sampling_rate_hz; + newConfig.high_res_mode_enabled = payload.high_resolution_enabled; + newConfig.isar_frame_duration_us = (int16_t) ( payload.num_media_times * payload.frame_duration_us ); + newConfig.lc3plus_frame_duration_us = (int16_t) payload.frame_duration_us; + newConfig.samplerate = payload.sampling_rate_hz; + err = isar_lc3plus_dec_init_handle( newConfig, &handle ); + if ( err != IVAS_ERR_OK ) + { + return IVAS_ERROR( err, "ISAR_LC3PLUS_DEC_Open failed\n" ); + } + } #else if ( payload.frame_duration_us != handle->config.lc3plus_frame_duration_us ) { @@ -656,23 +672,6 @@ static ivas_error isar_LC3PLUS_DEC_Decode_or_Conceal_internal( } #ifdef FIX_1515_ISAR_FRAME_SIZES_IN_RTP - if ( reInitRequired ) - { - isar_lc3plus_dec_deinit_handle( handle ); - LC3PLUS_CONFIG newConfig; - newConfig.channels = payload.num_channels; - newConfig.samplerate = payload.sampling_rate_hz; - newConfig.high_res_mode_enabled = payload.high_resolution_enabled; - newConfig.isar_frame_duration_us = (int16_t) ( payload.num_media_times * payload.frame_duration_us ); - newConfig.lc3plus_frame_duration_us = (int16_t) payload.frame_duration_us; - newConfig.samplerate = payload.sampling_rate_hz; - err = isar_lc3plus_dec_init_handle( newConfig, &handle ); - if ( err != IVAS_ERR_OK ) - { - return IVAS_ERROR( err, "ISAR_LC3PLUS_DEC_Open failed\n" ); - } - } - config_num_media_times = handle->config.isar_frame_duration_us / handle->config.lc3plus_frame_duration_us; #endif numSamplesPerLC3plusChannel = (int16_t) ( handle->config.samplerate / ( 1000000 / handle->config.isar_frame_duration_us ) / config_num_media_times ); -- GitLab From 4611624e766d71714794063cd4a98c35bd0229e8 Mon Sep 17 00:00:00 2001 From: Kacper Sagnowski Date: Tue, 28 Apr 2026 19:18:10 +0200 Subject: [PATCH 11/13] Fix equality checks for IsarBitstream, IsarFileHeader, and IsarFileFrame --- scripts/split_rendering/isar_bstool.py | 30 ++++++++++++++++++-- tests/test_be_for_jbm_neutral_dly_profile.py | 2 +- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/scripts/split_rendering/isar_bstool.py b/scripts/split_rendering/isar_bstool.py index 927128bd5..01f4b6c44 100755 --- a/scripts/split_rendering/isar_bstool.py +++ b/scripts/split_rendering/isar_bstool.py @@ -38,7 +38,7 @@ import math import sys from collections.abc import Callable from pathlib import Path -from typing import Protocol, cast, final +from typing import Protocol, cast, final, override class IsarBstoolError(Exception): @@ -183,7 +183,11 @@ class IsarBitstream: return self - def is_same_as(self, other: IsarBitstream) -> bool: + @override + def __eq__(self, other: object) -> bool: + if not isinstance(other, IsarBitstream): + return NotImplemented + return self.header == other.header and self.frames == other.frames @@ -206,6 +210,21 @@ class IsarFileHeader: self.sample_rate = _int_from_bytes(reader.read(4)) self.lc3plus_hires = bool(_int_from_bytes(reader.read(2))) + @override + def __eq__(self, other: object) -> bool: + if not isinstance(other, IsarFileHeader): + return NotImplemented + + return ( + self.delay_ns == other.delay_ns + and self.codec == other.codec + and self.pose_correction == other.pose_correction + and self.codec_frame_size_ms == other.codec_frame_size_ms + and self.isar_frame_size_ms == other.isar_frame_size_ms + and self.sample_rate == other.sample_rate + and self.lc3plus_hires == other.lc3plus_hires + ) + def write(self, writer: io.BufferedWriter) -> None: _write_exact(writer, self.FILE_HEADER) _write_exact(writer, _int_to_bytes(self.delay_ns, 4)) @@ -240,6 +259,13 @@ class IsarFileFrame: payload_size = math.ceil(self.num_bits / 8) self.payload = reader.read(payload_size) + @override + def __eq__(self, other: object) -> bool: + if not isinstance(other, IsarFileFrame): + return NotImplemented + + return self.num_bits == other.num_bits and self.payload == other.payload + def write(self, writer: io.BufferedWriter) -> None: _write_exact(writer, self.FRAME_HEADER) _write_exact(writer, _int_to_bytes(self.VERSION, 1)) diff --git a/tests/test_be_for_jbm_neutral_dly_profile.py b/tests/test_be_for_jbm_neutral_dly_profile.py index dee54b7d9..330f6aad4 100644 --- a/tests/test_be_for_jbm_neutral_dly_profile.py +++ b/tests/test_be_for_jbm_neutral_dly_profile.py @@ -228,7 +228,7 @@ def compare_audio(non_voip_output, voip_output, sampling_rate_khz): def compare_isar_files(non_voip_isar, voip_isar): isar_bs = IsarBitstream(non_voip_isar) isar_bs_voip = IsarBitstream(voip_isar).trim(JBM_NEUTRAL_DELAY_MS / 1000) - if not isar_bs_voip.is_same_as(isar_bs): + if isar_bs_voip != isar_bs: pytest.fail( "Difference between no jbm and zero-delay jbm decoding found! ISAR files differ" ) -- GitLab From 9873bd3cf1183d5f88e5df8e128b34eb523ad3d5 Mon Sep 17 00:00:00 2001 From: Kacper Sagnowski Date: Tue, 28 Apr 2026 19:18:43 +0200 Subject: [PATCH 12/13] Improve parsing in isar_bstool.py --- scripts/split_rendering/isar_bstool.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/split_rendering/isar_bstool.py b/scripts/split_rendering/isar_bstool.py index 01f4b6c44..3412776d7 100755 --- a/scripts/split_rendering/isar_bstool.py +++ b/scripts/split_rendering/isar_bstool.py @@ -303,13 +303,13 @@ def _codec_from_bytes(bytes_: bytes) -> str: if x < len(CODECS): return CODECS[x] - return "UNKNOWN" + raise IsarBstoolError(f"Unknown codec value in ISAR file header: {x}") def _codec_to_bytes(codec: str) -> bytes: # Refer to ISAR_SPLIT_REND_CODEC enum in C code CODECS = {"LCLD": 0, "LC3PLUS": 1, "DEFAULT": 2, "NONE": 3} - return CODECS.get(codec, 255).to_bytes(4, byteorder="little") + return CODECS[codec].to_bytes(4, byteorder="little") def _pose_corr_from_bytes(bytes_: bytes) -> str: @@ -320,13 +320,13 @@ def _pose_corr_from_bytes(bytes_: bytes) -> str: if x < len(POSE_CORR_MODES): return POSE_CORR_MODES[x] - return "UNKNOWN" + raise IsarBstoolError(f"Unknown pose correction value in ISAR file header: {x}") def _pose_corr_to_bytes(pose_corr: str) -> bytes: # Refer to ISAR_SPLIT_REND_POSE_CORRECTION_MODE enum in C code POSE_CORR_MODES = {"NONE": 0, "CLDFB": 1} - return POSE_CORR_MODES.get(pose_corr, 255).to_bytes(4, byteorder="little") + return POSE_CORR_MODES[pose_corr].to_bytes(4, byteorder="little") ###################################################################################### -- GitLab From 8a3d5bc80df7aec9fc349dcebf3c0ef81d1e7d5b Mon Sep 17 00:00:00 2001 From: Kacper Sagnowski Date: Tue, 28 Apr 2026 19:40:15 +0200 Subject: [PATCH 13/13] Fix missing parameters in the test_lc3plus_framing_reconfiguration test case --- tests/split_rendering/test_split_rendering.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/split_rendering/test_split_rendering.py b/tests/split_rendering/test_split_rendering.py index 88ba30816..06b002939 100644 --- a/tests/split_rendering/test_split_rendering.py +++ b/tests/split_rendering/test_split_rendering.py @@ -753,6 +753,11 @@ to reconfigure on the first frame. Required for RTP use cases. def test_lc3plus_framing_reconfiguration( record_property, props_to_record, + get_mld, + get_mld_lim, + get_ssnr, + get_odg, + get_odg_bin, test_info, render_config, post_rend_fr, @@ -760,7 +765,7 @@ def test_lc3plus_framing_reconfiguration( patch_codec_frame_size_ms, ): if int(patch_codec_frame_size_ms) > int(pre_rend_fr): - pytest.xfail("Codec frame size must fit within the ISAR frame size") + pytest.xfail("Codec frame size must fit within the ISAR frame size") post_trajectory = HR_TRAJECTORY_DIR.joinpath("rotate_euler_quaternion_30s.csv") pre_trajectory = post_trajectory.with_stem(f"{post_trajectory.stem}_delayed") @@ -776,5 +781,10 @@ def test_lc3plus_framing_reconfiguration( binary_suffix=EXE_SUFFIX, post_rend_fr=post_rend_fr, pre_rend_fr=pre_rend_fr, - patch_codec_frame_size_ms=patch_codec_frame_size_ms + get_mld=get_mld, + mld_lim=get_mld_lim, + get_ssnr=get_ssnr, + get_odg=get_odg, + get_odg_bin=get_odg_bin, + patch_codec_frame_size_ms=patch_codec_frame_size_ms, ) -- GitLab