Upgrade iamf_tools to e42e82092f6a46df4c1eebc0e67fa1fdd6788696

This project was upgraded with external_updater.
Usage: tools/external_updater/updater.sh update external/iamf_tools
For more info, check https://cs.android.com/android/platform/superproject/main/+/main:tools/external_updater/README.md

Bug: 388336285
Test: TreeHugger
Change-Id: I26bb4762aa7508ea05e87aafb92d7a4e761a6ae0
diff --git a/METADATA b/METADATA
index 0727d11..6d8d34d 100644
--- a/METADATA
+++ b/METADATA
@@ -14,13 +14,13 @@
   last_upgrade_date {
     year: 2025
     month: 3
-    day: 6
+    day: 24
   }
   homepage: "https://aomediacodec.github.io/iamf/"
   identifier {
     type: "Git"
     value: "https://github.com/AOMediaCodec/iamf-tools"
-    version: "612bf29777031b1d154a0126ba5ffe7b3b7a3c0f"
+    version: "e42e82092f6a46df4c1eebc0e67fa1fdd6788696"
     primary_source: true
   }
 }
diff --git a/iamf/api/conversion/BUILD b/iamf/api/conversion/BUILD
index 9ccd766..4285786 100644
--- a/iamf/api/conversion/BUILD
+++ b/iamf/api/conversion/BUILD
@@ -12,6 +12,9 @@
         "//iamf/api:types",
         "//iamf/obu:mix_presentation",
         "@com_google_absl//absl/log",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/status:statusor",
+        "@com_google_absl//absl/strings",
     ],
 )
 # keep-sorted end
diff --git a/iamf/api/conversion/mix_presentation_conversion.cc b/iamf/api/conversion/mix_presentation_conversion.cc
index 3cb7b65..cbf5c89 100644
--- a/iamf/api/conversion/mix_presentation_conversion.cc
+++ b/iamf/api/conversion/mix_presentation_conversion.cc
@@ -12,11 +12,61 @@
 
 #include "iamf/api/conversion/mix_presentation_conversion.h"
 
+#include <variant>
+
 #include "absl/log/log.h"
+#include "absl/status/status.h"
+#include "absl/strings/str_cat.h"
 #include "iamf/api/types.h"
 #include "iamf/obu/mix_presentation.h"
 
 namespace iamf_tools {
+namespace {
+
+absl::StatusOr<api::OutputLayout> InternalLayoutToApiLayout(
+    const LoudspeakersReservedOrBinauralLayout& specific_layout) {
+  return absl::InvalidArgumentError("Invalid layout type.");
+}
+
+absl::StatusOr<api::OutputLayout> InternalLayoutToApiLayout(
+    const LoudspeakersSsConventionLayout& specific_layout) {
+  switch (specific_layout.sound_system) {
+    case LoudspeakersSsConventionLayout::kSoundSystemA_0_2_0:
+      return api::OutputLayout::kItu2051_SoundSystemA_0_2_0;
+    case LoudspeakersSsConventionLayout::kSoundSystemB_0_5_0:
+      return api::OutputLayout::kItu2051_SoundSystemB_0_5_0;
+    case LoudspeakersSsConventionLayout::kSoundSystemC_2_5_0:
+      return api::OutputLayout::kItu2051_SoundSystemC_2_5_0;
+    case LoudspeakersSsConventionLayout::kSoundSystemD_4_5_0:
+      return api::OutputLayout::kItu2051_SoundSystemD_4_5_0;
+    case LoudspeakersSsConventionLayout::kSoundSystemE_4_5_1:
+      return api::OutputLayout::kItu2051_SoundSystemE_4_5_1;
+    case LoudspeakersSsConventionLayout::kSoundSystemF_3_7_0:
+      return api::OutputLayout::kItu2051_SoundSystemF_3_7_0;
+    case LoudspeakersSsConventionLayout::kSoundSystemG_4_9_0:
+      return api::OutputLayout::kItu2051_SoundSystemG_4_9_0;
+    case LoudspeakersSsConventionLayout::kSoundSystemH_9_10_3:
+      return api::OutputLayout::kItu2051_SoundSystemH_9_10_3;
+    case LoudspeakersSsConventionLayout::kSoundSystemI_0_7_0:
+      return api::OutputLayout::kItu2051_SoundSystemI_0_7_0;
+    case LoudspeakersSsConventionLayout::kSoundSystemJ_4_7_0:
+      return api::OutputLayout::kItu2051_SoundSystemJ_4_7_0;
+    case LoudspeakersSsConventionLayout::kSoundSystem10_2_7_0:
+      return api::OutputLayout::kIAMF_SoundSystemExtension_2_7_0;
+    case LoudspeakersSsConventionLayout::kSoundSystem11_2_3_0:
+      return api::OutputLayout::kIAMF_SoundSystemExtension_2_3_0;
+    case LoudspeakersSsConventionLayout::kSoundSystem12_0_1_0:
+      return api::OutputLayout::kIAMF_SoundSystemExtension_0_1_0;
+    case LoudspeakersSsConventionLayout::kSoundSystem13_6_9_0:
+      return api::OutputLayout::kIAMF_SoundSystemExtension_6_9_0;
+    default:
+      return absl::InvalidArgumentError(
+          absl::StrCat("Invalid layout: ", specific_layout.sound_system));
+  };
+}
+
+}  // namespace
+
 Layout MakeLayout(LoudspeakersSsConventionLayout::SoundSystem sound_system) {
   return {.layout_type = Layout::kLayoutTypeLoudspeakersSsConvention,
           .specific_layout =
@@ -58,4 +108,12 @@
   LOG(FATAL) << "Invalid output layout.";
 }
 
+absl::StatusOr<api::OutputLayout> InternalToApiType(Layout internal_layout) {
+  return std::visit(
+      [](const auto& specific_layout) {
+        return InternalLayoutToApiLayout(specific_layout);
+      },
+      internal_layout.specific_layout);
+}
+
 }  // namespace iamf_tools
diff --git a/iamf/api/conversion/mix_presentation_conversion.h b/iamf/api/conversion/mix_presentation_conversion.h
index b5215fb..78d0cf1 100644
--- a/iamf/api/conversion/mix_presentation_conversion.h
+++ b/iamf/api/conversion/mix_presentation_conversion.h
@@ -13,6 +13,7 @@
 #ifndef API_CONVERSION_MIX_PRESENTATION_METADATA_H_
 #define API_CONVERSION_MIX_PRESENTATION_METADATA_H_
 
+#include "absl/status/statusor.h"
 #include "iamf/api/types.h"
 #include "iamf/obu/mix_presentation.h"
 
@@ -21,6 +22,9 @@
 /*!\brief Converts the API-requested OutputLayout to an internal IAMF Layout. */
 Layout ApiToInternalType(api::OutputLayout api_output_layout);
 
+/*!\brief Converts the internal IAMF Layout to the API OutputLayout. */
+absl::StatusOr<api::OutputLayout> InternalToApiType(Layout internal_layout);
+
 }  // namespace iamf_tools
 
 #endif  // API_CONVERSION_MIX_PRESENTATION_METADATA_H_
diff --git a/iamf/api/conversion/tests/BUILD b/iamf/api/conversion/tests/BUILD
index 7486ab5..e029d85 100644
--- a/iamf/api/conversion/tests/BUILD
+++ b/iamf/api/conversion/tests/BUILD
@@ -6,6 +6,7 @@
         "//iamf/api:types",
         "//iamf/api/conversion:mix_presentation_conversion",
         "//iamf/obu:mix_presentation",
+        "@com_google_absl//absl/status:status_matchers",
         "@com_google_googletest//:gtest_main",
     ],
 )
diff --git a/iamf/api/conversion/tests/mix_presentation_conversion_test.cc b/iamf/api/conversion/tests/mix_presentation_conversion_test.cc
index 047f53e..f3604ff 100644
--- a/iamf/api/conversion/tests/mix_presentation_conversion_test.cc
+++ b/iamf/api/conversion/tests/mix_presentation_conversion_test.cc
@@ -15,6 +15,8 @@
 #include <utility>
 #include <variant>
 
+#include "absl/status/status_matchers.h"
+#include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "iamf/api/types.h"
 #include "iamf/obu/mix_presentation.h"
@@ -22,12 +24,43 @@
 namespace iamf_tools {
 namespace {
 
+using ::absl_testing::IsOk;
 using ::testing::TestWithParam;
 
 using LayoutPair =
     std::pair<api::OutputLayout, LoudspeakersSsConventionLayout::SoundSystem>;
 using ApiToInternalType_OutputLayout = TestWithParam<LayoutPair>;
 
+auto kApiOutputToInternalSoundSystemPairs = ::testing::Values(
+    LayoutPair(api::OutputLayout::kItu2051_SoundSystemA_0_2_0,
+               LoudspeakersSsConventionLayout::kSoundSystemA_0_2_0),
+    LayoutPair(api::OutputLayout::kItu2051_SoundSystemB_0_5_0,
+               LoudspeakersSsConventionLayout::kSoundSystemB_0_5_0),
+    LayoutPair(api::OutputLayout::kItu2051_SoundSystemC_2_5_0,
+               LoudspeakersSsConventionLayout::kSoundSystemC_2_5_0),
+    LayoutPair(api::OutputLayout::kItu2051_SoundSystemD_4_5_0,
+               LoudspeakersSsConventionLayout::kSoundSystemD_4_5_0),
+    LayoutPair(api::OutputLayout::kItu2051_SoundSystemE_4_5_1,
+               LoudspeakersSsConventionLayout::kSoundSystemE_4_5_1),
+    LayoutPair(api::OutputLayout::kItu2051_SoundSystemF_3_7_0,
+               LoudspeakersSsConventionLayout::kSoundSystemF_3_7_0),
+    LayoutPair(api::OutputLayout::kItu2051_SoundSystemG_4_9_0,
+               LoudspeakersSsConventionLayout::kSoundSystemG_4_9_0),
+    LayoutPair(api::OutputLayout::kItu2051_SoundSystemH_9_10_3,
+               LoudspeakersSsConventionLayout::kSoundSystemH_9_10_3),
+    LayoutPair(api::OutputLayout::kItu2051_SoundSystemI_0_7_0,
+               LoudspeakersSsConventionLayout::kSoundSystemI_0_7_0),
+    LayoutPair(api::OutputLayout::kItu2051_SoundSystemJ_4_7_0,
+               LoudspeakersSsConventionLayout::kSoundSystemJ_4_7_0),
+    LayoutPair(api::OutputLayout::kIAMF_SoundSystemExtension_2_7_0,
+               LoudspeakersSsConventionLayout::kSoundSystem10_2_7_0),
+    LayoutPair(api::OutputLayout::kIAMF_SoundSystemExtension_2_3_0,
+               LoudspeakersSsConventionLayout::kSoundSystem11_2_3_0),
+    LayoutPair(api::OutputLayout::kIAMF_SoundSystemExtension_0_1_0,
+               LoudspeakersSsConventionLayout::kSoundSystem12_0_1_0),
+    LayoutPair(api::OutputLayout::kIAMF_SoundSystemExtension_6_9_0,
+               LoudspeakersSsConventionLayout::kSoundSystem13_6_9_0));
+
 TEST_P(ApiToInternalType_OutputLayout, ConvertsOutputStereoToInternalLayout) {
   const auto& [api_output_layout, expected_specific_layout] = GetParam();
 
@@ -43,38 +76,30 @@
       expected_specific_layout);
 }
 
-INSTANTIATE_TEST_SUITE_P(
-    ApiToInternalType_OutputLayout_Instantiation,
-    ApiToInternalType_OutputLayout,
-    ::testing::Values(
-        LayoutPair(api::OutputLayout::kItu2051_SoundSystemA_0_2_0,
-                   LoudspeakersSsConventionLayout::kSoundSystemA_0_2_0),
-        LayoutPair(api::OutputLayout::kItu2051_SoundSystemB_0_5_0,
-                   LoudspeakersSsConventionLayout::kSoundSystemB_0_5_0),
-        LayoutPair(api::OutputLayout::kItu2051_SoundSystemC_2_5_0,
-                   LoudspeakersSsConventionLayout::kSoundSystemC_2_5_0),
-        LayoutPair(api::OutputLayout::kItu2051_SoundSystemD_4_5_0,
-                   LoudspeakersSsConventionLayout::kSoundSystemD_4_5_0),
-        LayoutPair(api::OutputLayout::kItu2051_SoundSystemE_4_5_1,
-                   LoudspeakersSsConventionLayout::kSoundSystemE_4_5_1),
-        LayoutPair(api::OutputLayout::kItu2051_SoundSystemF_3_7_0,
-                   LoudspeakersSsConventionLayout::kSoundSystemF_3_7_0),
-        LayoutPair(api::OutputLayout::kItu2051_SoundSystemG_4_9_0,
-                   LoudspeakersSsConventionLayout::kSoundSystemG_4_9_0),
-        LayoutPair(api::OutputLayout::kItu2051_SoundSystemH_9_10_3,
-                   LoudspeakersSsConventionLayout::kSoundSystemH_9_10_3),
-        LayoutPair(api::OutputLayout::kItu2051_SoundSystemI_0_7_0,
-                   LoudspeakersSsConventionLayout::kSoundSystemI_0_7_0),
-        LayoutPair(api::OutputLayout::kItu2051_SoundSystemJ_4_7_0,
-                   LoudspeakersSsConventionLayout::kSoundSystemJ_4_7_0),
-        LayoutPair(api::OutputLayout::kIAMF_SoundSystemExtension_2_7_0,
-                   LoudspeakersSsConventionLayout::kSoundSystem10_2_7_0),
-        LayoutPair(api::OutputLayout::kIAMF_SoundSystemExtension_2_3_0,
-                   LoudspeakersSsConventionLayout::kSoundSystem11_2_3_0),
-        LayoutPair(api::OutputLayout::kIAMF_SoundSystemExtension_0_1_0,
-                   LoudspeakersSsConventionLayout::kSoundSystem12_0_1_0),
-        LayoutPair(api::OutputLayout::kIAMF_SoundSystemExtension_6_9_0,
-                   LoudspeakersSsConventionLayout::kSoundSystem13_6_9_0)));
+INSTANTIATE_TEST_SUITE_P(ApiToInternalType_OutputLayout_Instantiation,
+                         ApiToInternalType_OutputLayout,
+                         kApiOutputToInternalSoundSystemPairs);
 
+Layout MakeLayout(LoudspeakersSsConventionLayout::SoundSystem sound_system) {
+  return {.layout_type = Layout::kLayoutTypeLoudspeakersSsConvention,
+          .specific_layout =
+              LoudspeakersSsConventionLayout{.sound_system = sound_system}};
+};
+
+using InternalTypeToApi_OutputLayout = TestWithParam<LayoutPair>;
+
+TEST_P(InternalTypeToApi_OutputLayout, ConvertsInternalStereoToOutputLayout) {
+  const auto& [expected_api_output_layout, internal_sound_system] = GetParam();
+  Layout internal_layout = MakeLayout(internal_sound_system);
+
+  auto api_output_layout = InternalToApiType(internal_layout);
+
+  EXPECT_THAT(api_output_layout, IsOk());
+  EXPECT_EQ(*api_output_layout, expected_api_output_layout);
+}
+
+INSTANTIATE_TEST_SUITE_P(InternalTypeToApi_OutputLayout_Instantiation,
+                         InternalTypeToApi_OutputLayout,
+                         kApiOutputToInternalSoundSystemPairs);
 }  // namespace
 }  // namespace iamf_tools
diff --git a/iamf/api/decoder/BUILD b/iamf/api/decoder/BUILD
index 566e56b..dfe3aa8 100644
--- a/iamf/api/decoder/BUILD
+++ b/iamf/api/decoder/BUILD
@@ -10,12 +10,12 @@
     deps = [
         "//iamf/api:types",
         "//iamf/api/conversion:mix_presentation_conversion",
-        "//iamf/cli:audio_frame_with_data",
         "//iamf/cli:obu_processor",
-        "//iamf/cli:parameter_block_with_data",
         "//iamf/cli:rendering_mix_presentation_finalizer",
         "//iamf/common:read_bit_buffer",
         "//iamf/common/utils:macros",
+        "//iamf/common/utils:sample_processing_utils",
+        "//iamf/obu:mix_presentation",
         "@com_google_absl//absl/log",
         "@com_google_absl//absl/status",
         "@com_google_absl//absl/status:statusor",
diff --git a/iamf/api/decoder/iamf_decoder.cc b/iamf/api/decoder/iamf_decoder.cc
index b1c6bb4..b844eed 100644
--- a/iamf/api/decoder/iamf_decoder.cc
+++ b/iamf/api/decoder/iamf_decoder.cc
@@ -12,8 +12,8 @@
 
 #include "iamf/api/decoder/iamf_decoder.h"
 
+#include <cstddef>
 #include <cstdint>
-#include <list>
 #include <memory>
 #include <optional>
 #include <queue>
@@ -25,12 +25,12 @@
 #include "absl/types/span.h"
 #include "iamf/api/conversion/mix_presentation_conversion.h"
 #include "iamf/api/types.h"
-#include "iamf/cli/audio_frame_with_data.h"
 #include "iamf/cli/obu_processor.h"
-#include "iamf/cli/parameter_block_with_data.h"
 #include "iamf/cli/rendering_mix_presentation_finalizer.h"
 #include "iamf/common/read_bit_buffer.h"
 #include "iamf/common/utils/macros.h"
+#include "iamf/common/utils/sample_processing_utils.h"
+#include "iamf/obu/mix_presentation.h"
 
 namespace iamf_tools {
 namespace api {
@@ -40,8 +40,10 @@
 // Holds the internal state of the decoder to hide it and necessary includes
 // from API users.
 struct IamfDecoder::DecoderState {
-  DecoderState(std::unique_ptr<StreamBasedReadBitBuffer> read_bit_buffer)
-      : read_bit_buffer(std::move(read_bit_buffer)) {}
+  DecoderState(std::unique_ptr<StreamBasedReadBitBuffer> read_bit_buffer,
+               const Layout& initial_requested_layout)
+      : read_bit_buffer(std::move(read_bit_buffer)),
+        layout(initial_requested_layout) {}
 
   // Current status of the decoder.
   Status status = Status::kAcceptingData;
@@ -59,8 +61,13 @@
   // temporal units currently available.
   std::queue<std::vector<std::vector<int32_t>>> rendered_pcm_samples;
 
-  // The layout requested by the caller for the rendered output audio.
-  OutputLayout requested_layout = OutputLayout::kItu2051_SoundSystemA_0_2_0;
+  // The layout used for the rendered output audio.
+  // Initially set to the requested Layout but updated by ObuProcessor.
+  Layout layout;
+
+  // TODO(b/379122580):  Use the bit depth of the underlying content.
+  // Defaulting to int32 for now.
+  OutputSampleType output_sample_type = OutputSampleType::kInt32LittleEndian;
 };
 
 namespace {
@@ -70,19 +77,19 @@
 // OBUs have been processed. Contracted to only return a resource exhausted
 // error if there is not enough data to process the descriptor OBUs.
 absl::StatusOr<std::unique_ptr<ObuProcessor>> CreateObuProcessor(
-    const OutputLayout& requested_layout, bool contains_all_descriptor_obus,
-    absl::Span<const uint8_t> bitstream,
-    StreamBasedReadBitBuffer* read_bit_buffer) {
+    bool contains_all_descriptor_obus, absl::Span<const uint8_t> bitstream,
+    StreamBasedReadBitBuffer* read_bit_buffer, Layout& in_out_layout) {
   // Happens only in the pure streaming case.
   auto start_position = read_bit_buffer->Tell();
   bool insufficient_data;
-  // TODO(b/394376153): Update once we support other layouts.
   auto obu_processor = ObuProcessor::CreateForRendering(
-      ApiToInternalType(requested_layout),
+      in_out_layout,
       RenderingMixPresentationFinalizer::ProduceNoSampleProcessors,
       /*is_exhaustive_and_exact=*/contains_all_descriptor_obus, read_bit_buffer,
-      insufficient_data);
+      in_out_layout, insufficient_data);
   if (obu_processor == nullptr) {
+    // `insufficient_data` is true iff everything so far is valid but more data
+    // is needed.
     if (insufficient_data && !contains_all_descriptor_obus) {
       return absl::ResourceExhaustedError(
           "Have not received enough data yet to process descriptor "
@@ -103,22 +110,24 @@
   bool continue_processing = true;
   while (continue_processing) {
     auto start_position_for_temporal_unit = read_bit_buffer->Tell();
-    std::list<AudioFrameWithData> audio_frames_for_temporal_unit;
-    std::list<ParameterBlockWithData> parameter_blocks_for_temporal_unit;
-    std::optional<int32_t> timestamp_for_temporal_unit;
+    std::optional<ObuProcessor::OutputTemporalUnit> output_temporal_unit;
     // TODO(b/395889878): Add support for partial temporal units.
     RETURN_IF_NOT_OK(obu_processor->ProcessTemporalUnit(
-        audio_frames_for_temporal_unit, parameter_blocks_for_temporal_unit,
-        timestamp_for_temporal_unit, continue_processing));
+        /*eos_is_end_of_sequence=*/false, output_temporal_unit,
+        continue_processing));
+    if (!output_temporal_unit.has_value()) {
+      break;
+    }
 
     // Trivial IA Sequences may have empty temporal units. Do not try to
     // render empty temporal unit.
-    if (timestamp_for_temporal_unit.has_value()) {
+    if (output_temporal_unit.has_value()) {
       absl::Span<const std::vector<int32_t>>
           rendered_pcm_samples_for_temporal_unit;
       RETURN_IF_NOT_OK(obu_processor->RenderTemporalUnitAndMeasureLoudness(
-          *timestamp_for_temporal_unit, audio_frames_for_temporal_unit,
-          parameter_blocks_for_temporal_unit,
+          output_temporal_unit->output_timestamp,
+          output_temporal_unit->output_audio_frames,
+          output_temporal_unit->output_parameter_blocks,
           rendered_pcm_samples_for_temporal_unit));
       rendered_pcm_samples.push(
           std::vector(rendered_pcm_samples_for_temporal_unit.begin(),
@@ -135,6 +144,43 @@
   return absl::OkStatus();
 }
 
+size_t BytesPerSample(OutputSampleType sample_type) {
+  switch (sample_type) {
+    case OutputSampleType::kInt16LittleEndian:
+      return 2;
+    case OutputSampleType::kInt32LittleEndian:
+      return 4;
+    default:
+      return 0;
+  }
+}
+
+absl::Status WriteFrameToSpan(const std::vector<std::vector<int32_t>>& frame,
+                              OutputSampleType sample_type,
+                              absl::Span<uint8_t>& output_bytes,
+                              size_t& bytes_written) {
+  const size_t bytes_per_sample = BytesPerSample(sample_type);
+  const size_t bits_per_sample = bytes_per_sample * 8;
+  const size_t required_size =
+      frame.size() * frame[0].size() * bytes_per_sample;
+  if (output_bytes.size() < required_size) {
+    return absl::InvalidArgumentError(
+        "Span does not have enough space to write output bytes.");
+  }
+  const bool big_endian = false;
+  size_t write_position = 0;
+  uint8_t* data = output_bytes.data();
+  for (int t = 0; t < frame.size(); t++) {
+    for (int c = 0; c < frame[0].size(); ++c) {
+      const uint32_t sample = static_cast<uint32_t>(frame[t][c]);
+      RETURN_IF_NOT_OK(WritePcmSample(sample, bits_per_sample, big_endian, data,
+                                      write_position));
+    }
+  }
+  bytes_written = write_position;
+  return absl::OkStatus();
+}
+
 }  // namespace
 
 IamfDecoder::IamfDecoder(std::unique_ptr<DecoderState> state)
@@ -147,29 +193,31 @@
 IamfDecoder::IamfDecoder(IamfDecoder&&) = default;
 IamfDecoder& IamfDecoder::operator=(IamfDecoder&&) = default;
 
-absl::StatusOr<IamfDecoder> IamfDecoder::Create() {
+absl::StatusOr<IamfDecoder> IamfDecoder::Create(
+    const OutputLayout& requested_layout) {
   std::unique_ptr<StreamBasedReadBitBuffer> read_bit_buffer =
       StreamBasedReadBitBuffer::Create(kInitialBufferSize);
   if (read_bit_buffer == nullptr) {
     return absl::InternalError("Failed to create read bit buffer.");
   }
-  std::unique_ptr<DecoderState> state =
-      std::make_unique<DecoderState>(std::move(read_bit_buffer));
+  std::unique_ptr<DecoderState> state = std::make_unique<DecoderState>(
+      std::move(read_bit_buffer), ApiToInternalType(requested_layout));
   return IamfDecoder(std::move(state));
 }
 
 absl::StatusOr<IamfDecoder> IamfDecoder::CreateFromDescriptors(
+    const OutputLayout& requested_layout,
     absl::Span<const uint8_t> descriptor_obus) {
-  absl::StatusOr<IamfDecoder> decoder = Create();
+  absl::StatusOr<IamfDecoder> decoder = Create(requested_layout);
   if (!decoder.ok()) {
     return decoder.status();
   }
   RETURN_IF_NOT_OK(
       decoder->state_->read_bit_buffer->PushBytes(descriptor_obus));
   absl::StatusOr<std::unique_ptr<ObuProcessor>> obu_processor =
-      CreateObuProcessor(decoder->state_->requested_layout,
-                         /*contains_all_descriptor_obus=*/true, descriptor_obus,
-                         decoder->state_->read_bit_buffer.get());
+      CreateObuProcessor(/*contains_all_descriptor_obus=*/true, descriptor_obus,
+                         decoder->state_->read_bit_buffer.get(),
+                         decoder->state_->layout);
   if (!obu_processor.ok()) {
     return obu_processor.status();
   }
@@ -184,12 +232,12 @@
   }
   RETURN_IF_NOT_OK(state_->read_bit_buffer->PushBytes(bitstream));
   if (!IsDescriptorProcessingComplete()) {
-    auto obu_processor =
-        CreateObuProcessor(state_->requested_layout,
-                           /*contains_all_descriptor_obus=*/false, bitstream,
-                           state_->read_bit_buffer.get());
+    auto obu_processor = CreateObuProcessor(
+        /*contains_all_descriptor_obus=*/false, bitstream,
+        state_->read_bit_buffer.get(), state_->layout);
     if (obu_processor.ok()) {
       state_->obu_processor = *std::move(obu_processor);
+      return absl::OkStatus();
     } else if (absl::IsResourceExhausted(obu_processor.status())) {
       // Don't have enough data to process the descriptor OBUs yet, but no
       // errors have occurred.
@@ -213,53 +261,90 @@
       "ConfigureMixPresentationId is not yet implemented.");
 }
 
-absl::Status IamfDecoder::ConfigureOutputLayout(OutputLayout output_layout) {
-  return absl::UnimplementedError(
-      "ConfigureOutputLayout is not yet implemented.");
-}
-
-absl::Status IamfDecoder::ConfigureBitDepth(OutputFileBitDepth bit_depth) {
-  return absl::UnimplementedError("ConfigureBitDepth is not yet implemented.");
+void IamfDecoder::ConfigureOutputSampleType(
+    OutputSampleType output_sample_type) {
+  state_->output_sample_type = output_sample_type;
 }
 
 absl::Status IamfDecoder::GetOutputTemporalUnit(
-    std::vector<std::vector<int32_t>>& output_decoded_temporal_unit) {
+    absl::Span<uint8_t> output_bytes, size_t& bytes_written) {
+  bytes_written = 0;
   if (state_->rendered_pcm_samples.empty()) {
-    output_decoded_temporal_unit.clear();
     return absl::OkStatus();
   }
-  output_decoded_temporal_unit = state_->rendered_pcm_samples.front();
-  state_->rendered_pcm_samples.pop();
-  return absl::OkStatus();
+  OutputSampleType output_sample_type = GetOutputSampleType();
+  absl::Status status =
+      WriteFrameToSpan(state_->rendered_pcm_samples.front(), output_sample_type,
+                       output_bytes, bytes_written);
+  if (status.ok()) {
+    state_->rendered_pcm_samples.pop();
+    return absl::OkStatus();
+  }
+  return status;
 }
 
-bool IamfDecoder::IsTemporalUnitAvailable() {
+bool IamfDecoder::IsTemporalUnitAvailable() const {
   return !state_->rendered_pcm_samples.empty();
 }
 
-bool IamfDecoder::IsDescriptorProcessingComplete() {
+bool IamfDecoder::IsDescriptorProcessingComplete() const {
   return state_->obu_processor != nullptr;
 }
 
+absl::StatusOr<OutputLayout> IamfDecoder::GetOutputLayout() const {
+  if (!IsDescriptorProcessingComplete()) {
+    return absl::FailedPreconditionError(
+        "GetOutputLayout() cannot be called before descriptor processing is "
+        "complete.");
+  }
+  return InternalToApiType(state_->layout);
+}
+
+absl::StatusOr<int> IamfDecoder::GetNumberOfOutputChannels() const {
+  if (!IsDescriptorProcessingComplete()) {
+    return absl::FailedPreconditionError(
+        "GetNumberOfOutputChannels() cannot be called before descriptor "
+        "processing is complete.");
+  }
+  int num_channels;
+  RETURN_IF_NOT_OK(MixPresentationObu::GetNumChannelsFromLayout(state_->layout,
+                                                                num_channels));
+  return num_channels;
+}
+
 absl::Status IamfDecoder::GetMixPresentations(
-    std::vector<MixPresentationMetadata>& output_mix_presentation_metadata) {
+    std::vector<MixPresentationMetadata>& output_mix_presentation_metadata)
+    const {
   return absl::UnimplementedError(
       "GetMixPresentations is not yet implemented.");
 }
-
-absl::Status IamfDecoder::GetSampleRate(uint32_t& output_sample_rate) {
-  return absl::UnimplementedError("GetSampleRate is not yet implemented.");
+OutputSampleType IamfDecoder::GetOutputSampleType() const {
+  return state_->output_sample_type;
 }
 
-absl::Status IamfDecoder::GetFrameSize(uint32_t& output_frame_size) {
-  return absl::UnimplementedError("GetFrameSize is not yet implemented.");
+absl::StatusOr<uint32_t> IamfDecoder::GetSampleRate() const {
+  if (!IsDescriptorProcessingComplete()) {
+    return absl::FailedPreconditionError(
+        "GetSampleRate() cannot be called before descriptor processing is "
+        "complete.");
+  }
+  return state_->obu_processor->GetOutputSampleRate();
 }
 
-absl::Status IamfDecoder::Flush(
-    std::vector<std::vector<int32_t>>& output_decoded_temporal_unit,
-    bool& output_is_done) {
+absl::StatusOr<uint32_t> IamfDecoder::GetFrameSize() const {
+  if (!IsDescriptorProcessingComplete()) {
+    return absl::FailedPreconditionError(
+        "GetFrameSize() cannot be called before descriptor processing is "
+        "complete.");
+  }
+
+  return state_->obu_processor->GetOutputFrameSize();
+}
+
+absl::Status IamfDecoder::Flush(absl::Span<uint8_t> output_bytes,
+                                size_t& bytes_written, bool& output_is_done) {
   state_->status = Status::kFlushCalled;
-  RETURN_IF_NOT_OK(GetOutputTemporalUnit(output_decoded_temporal_unit));
+  RETURN_IF_NOT_OK(GetOutputTemporalUnit(output_bytes, bytes_written));
   output_is_done = state_->rendered_pcm_samples.empty();
   return absl::OkStatus();
 }
diff --git a/iamf/api/decoder/iamf_decoder.h b/iamf/api/decoder/iamf_decoder.h
index b146b4f..643c04a 100644
--- a/iamf/api/decoder/iamf_decoder.h
+++ b/iamf/api/decoder/iamf_decoder.h
@@ -13,8 +13,7 @@
 #ifndef API_DECODER_IAMF_DECODER_H_
 #define API_DECODER_IAMF_DECODER_H_
 
-#include <sys/types.h>
-
+#include <cstddef>
 #include <cstdint>
 #include <memory>
 #include <vector>
@@ -70,22 +69,32 @@
    * This function should be used for pure streaming applications in which the
    * descriptor OBUs are not known in advance.
    *
+   * \param requested_layout Specifies the desired output layout. This layout
+   *        will be used so long as it is present in the Descriptor OBUs that
+   *        are later provided to Decode(). If not, a default layout will be
+   *        selected.
+   *
    * \return IamfDecoder upon success. Other specific statuses on
    *         failure.
    */
-  static absl::StatusOr<IamfDecoder> Create();
+  static absl::StatusOr<IamfDecoder> Create(
+      const OutputLayout& requested_layout);
 
   /*!\brief Creates an IamfDecoder from a known set of descriptor OBUs.
    *
    * This function should be used for applications in which the descriptor OBUs
    * are known in advance.
    *
+   * \param requested_layout Specifies the desired output layout. This layout
+   *        will be used so long as it is present in the Descriptor OBUs that
+   *        are provided. If not, a default layout will be selected.
    * \param descriptor_obus Bitstream containing all the descriptor OBUs and
    *        only descriptor OBUs.
    * \return IamfDecoder upon success. Other specific statuses on
    *         failure.
    */
   static absl::StatusOr<IamfDecoder> CreateFromDescriptors(
+      const OutputLayout& requested_layout,
       absl::Span<const uint8_t> descriptor_obus);
 
   /*!\brief Configures the decoder with the desired mix presentation.
@@ -97,25 +106,13 @@
   absl::Status ConfigureMixPresentationId(
       MixPresentationId mix_presentation_id);
 
-  /*!\brief Configures the decoder with the desired output layout.
-   *
-   * \param output_layout Specifies the desired output layout.
-   * \return `absl::OkStatus()` upon success. Other specific statuses on
-   *         failure.
-   */
-  absl::Status ConfigureOutputLayout(OutputLayout output_layout);
-
   /*!\brief Configures the decoder with the desired bit depth.
    *
-   * \param bit_depth Specifies the desired bit depth.
-   * \return `absl::OkStatus()` upon success. Other specific statuses on
-   *         failure.
+   * Call this method to specify a specific output sample type.  If it is not
+   * called, the output samples will be a default value, retrievable by
+   * `GetOutputSampleType`.
    */
-  // TODO(b/379124235): Update OutputFileBitDepth to OutputBitDepth to
-  //                  indicate that this is not specific to file-based decoding.
-  // TODO(b/379122580): Decide how we would like to support float-based
-  // decoding.
-  absl::Status ConfigureBitDepth(OutputFileBitDepth bit_depth);
+  void ConfigureOutputSampleType(OutputSampleType output_sample_type);
 
   /*!\brief Decodes the bitstream provided.
    *
@@ -123,8 +120,10 @@
    * both. User can provide as much data as they would like. To receive decoded
    * temporal units, GetOutputTemporalUnit() should be called. If
    * GetOutputTemporalUnit() has not been called, this function guarantees that
-   * any temporal units received thus far have not been lost. See sample usages
-   * for more details.
+   * any temporal units received thus far have not been lost. If descriptors are
+   * processed for the first time, function will exit before processing any
+   * temporal units. This provides the user a chance to configure the decoder as
+   * they see fit. See sample usages for more details.
    *
    * \param bitstream Bitstream to decode.
    * \return `absl::OkStatus()` upon success. Other specific statuses on
@@ -139,14 +138,15 @@
    * may be more than one temporal unit available. When this returns empty, the
    * user should call Decode() again with more data.
    *
-   * \param output_decoded_temporal_unit Output parameter for the next temporal
-   *        unit of decoded audio. The outer vector corresponds to a tick, while
-   *        the inner vector corresponds to a channel.
+   * \param output_bytes Output buffer to receive bytes.  Must be large enough
+   *        to receive bytes.  Maximum necessary size can be determined by
+   *        GetFrameSize and GetOutputSampleType.
+   * \param bytes_written Number of bytes written to the output_bytes.
    * \return `absl::OkStatus()` upon success. Other specific statuses on
    *         failure.
    */
-  absl::Status GetOutputTemporalUnit(
-      std::vector<std::vector<int32_t>>& output_decoded_temporal_unit);
+  absl::Status GetOutputTemporalUnit(absl::Span<uint8_t> output_bytes,
+                                     size_t& bytes_written);
 
   /*!\brief Returns true iff a decoded temporal unit is available.
    *
@@ -155,46 +155,89 @@
    *
    * \return true iff a decoded temporal unit is available.
    */
-  bool IsTemporalUnitAvailable();
+  bool IsTemporalUnitAvailable() const;
 
   /*!\brief Returns true iff the descriptor OBUs have been parsed.
    *
    * This function can be used for determining when configuration setters that
-   * rely on descriptor OBU parsing can be called.
+   * rely on Descriptor OBU parsing can be called.
    *
-   * \return true iff the descriptor OBUs have been parsed.
+   * \return true iff the Descriptor OBUs have been parsed.
    */
-  bool IsDescriptorProcessingComplete();
+  bool IsDescriptorProcessingComplete() const;
+
+  /*!\brief Gets the layout that will be used to render the audio.
+   *
+   * The actual Layout used for rendering may not the same as requested when
+   * creating the IamfDecoder, if the requested Layout could not be used.
+   * This function allows verifying the actual Layout used after Descriptor OBU
+   * parsing is complete.
+   *
+   * This function can only be used after all Descriptor OBUs have been parsed,
+   * i.e. IsDescriptorProcessingComplete() returns true.
+   *
+   * \return OutputLayout or error statuses on failure.
+   */
+  absl::StatusOr<OutputLayout> GetOutputLayout() const;
+
+  /*!\brief Gets the number of output channels.
+   *
+   * This function can only be used after all Descriptor OBUs have been parsed,
+   * i.e. IsDescriptorProcessingComplete() returns true.
+   *
+   * \return
+   */
+  absl::StatusOr<int> GetNumberOfOutputChannels() const;
 
   /*!\brief Provides mix presentation information from the descriptor OBUs.
    *
    * This function can be used to determine which mix presentation the user
-   * would like to configure the decoder with. It will fail if the descriptor
-   * OBUs have not been parsed yet.
+   * would like to configure the decoder with.
+   *
+   * This function can only be used after all Descriptor OBUs have been parsed,
+   * i.e. IsDescriptorProcessingComplete() returns true.
    *
    * \param output_mix_presentation_metadatas Output parameter for the mix
    *        presentation metadata.
    * \return `absl::OkStatus()` upon success. Other specific statuses on
    *         failure.
    */
-  absl::Status GetMixPresentations(
-      std::vector<MixPresentationMetadata>& output_mix_presentation_metadatas);
+  absl::Status GetMixPresentations(std::vector<MixPresentationMetadata>&
+                                       output_mix_presentation_metadatas) const;
+
+  /*!\brief Returns the current OutputSampleType.
+   *
+   * The value is either the value set by ConfigureOutputSampleType or a default
+   * which may vary based on content.
+   *
+   * This function can only be used after all Descriptor OBUs have been parsed,
+   * i.e. IsDescriptorProcessingComplete() returns true.
+   */
+  OutputSampleType GetOutputSampleType() const;
 
   /*!\brief Gets the sample rate.
    *
-   * \param output_sample_rate Output parameter for the sample rate.
+   * This function can only be used after all Descriptor OBUs have been parsed,
+   * i.e. IsDescriptorProcessingComplete() returns true.
+   *
    * \return `absl::OkStatus()` upon success. Other specific statuses on
    *         failure.
    */
-  absl::Status GetSampleRate(uint32_t& output_sample_rate);
+  absl::StatusOr<uint32_t> GetSampleRate() const;
 
   /*!\brief Gets the number of samples per frame.
    *
-   * \param output_frame_size Output parameter for the frame size.
-   * \return `absl::OkStatus()` upon success. Other specific statuses on
-   *         failure.
+   * This function can only be used after all Descriptor OBUs have been parsed,
+   * i.e. IsDescriptorProcessingComplete() returns true.
+   *
+   * Returns the number of samples per frame of the output audio. The total
+   * number of samples in a time tick is the number of channels times the number
+   * of samples per frame.
+   *
+   * \return Number of samples per frame upon success. Other specific statuses
+   *         on failure.
    */
-  absl::Status GetFrameSize(uint32_t& output_frame_size);
+  absl::StatusOr<uint32_t> GetFrameSize() const;
 
   /*!\brief Outputs the last temporal unit(s) of decoded audio.
    *
@@ -210,9 +253,8 @@
    * \return `absl::OkStatus()` upon success. Other specific statuses on
    *         failure.
    */
-  absl::Status Flush(
-      std::vector<std::vector<int32_t>>& output_decoded_temporal_unit,
-      bool& output_is_done);
+  absl::Status Flush(absl::Span<uint8_t> output_bytes, size_t& bytes_written,
+                     bool& output_is_done);
 
   /*!\brief Closes the decoder.
    *
diff --git a/iamf/api/decoder/tests/BUILD b/iamf/api/decoder/tests/BUILD
index 8c6255b..2d1493a 100644
--- a/iamf/api/decoder/tests/BUILD
+++ b/iamf/api/decoder/tests/BUILD
@@ -1,16 +1,37 @@
+load("@rules_cc//cc:cc_test.bzl", "cc_test")
+
 # keep-sorted start block=yes prefix_order=cc_test newline_separated=yes
 cc_test(
+    name = "iamf_decoder_fuzz_test",
+    srcs = ["iamf_decoder_fuzz_test.cc"],
+    target_compatible_with = [
+        "@platforms//os:linux",
+    ],
+    deps = [
+        "//iamf/api:types",
+        "//iamf/api/decoder:iamf_decoder",
+        "@com_google_absl//absl/status:status_matchers",
+        "@com_google_absl//absl/types:span",
+        "@com_google_fuzztest//fuzztest",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
     name = "iamf_decoder_test",
     srcs = ["iamf_decoder_test.cc"],
     deps = [
         "//iamf/api:types",
         "//iamf/api/decoder:iamf_decoder",
         "//iamf/cli:audio_element_with_data",
+        "//iamf/cli:audio_frame_with_data",
+        "//iamf/cli:parameter_block_with_data",
         "//iamf/cli/tests:cli_test_utils",
         "//iamf/obu:audio_frame",
         "//iamf/obu:codec_config",
         "//iamf/obu:ia_sequence_header",
         "//iamf/obu:mix_presentation",
+        "//iamf/obu:obu_base",
         "//iamf/obu:obu_header",
         "//iamf/obu:temporal_delimiter",
         "//iamf/obu:types",
@@ -20,5 +41,4 @@
         "@com_google_googletest//:gtest_main",
     ],
 )
-
 # keep-sorted end
diff --git a/iamf/api/decoder/tests/iamf_decoder_fuzz_test.cc b/iamf/api/decoder/tests/iamf_decoder_fuzz_test.cc
new file mode 100644
index 0000000..d07aeca
--- /dev/null
+++ b/iamf/api/decoder/tests/iamf_decoder_fuzz_test.cc
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2024, Alliance for Open Media. All rights reserved
+ *
+ * This source code is subject to the terms of the BSD 3-Clause Clear License
+ * and the Alliance for Open Media Patent License 1.0. If the BSD 3-Clause Clear
+ * License was not distributed with this source code in the LICENSE file, you
+ * can obtain it at www.aomedia.org/license/software-license/bsd-3-c-c. If the
+ * Alliance for Open Media Patent License 1.0 was not distributed with this
+ * source code in the PATENTS file, you can obtain it at
+ * www.aomedia.org/license/patent.
+ */
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include "absl/status/status_matchers.h"
+#include "absl/types/span.h"
+#include "fuzztest/fuzztest.h"
+#include "gmock/gmock.h"
+// [internal] Placeholder for FLAC fuzzing include.
+#include "iamf/api/decoder/iamf_decoder.h"
+#include "iamf/api/types.h"
+
+namespace iamf_tools {
+namespace {
+
+using api::OutputLayout;
+using ::fuzztest::Arbitrary;
+using ::fuzztest::ElementOf;
+
+constexpr OutputLayout kStereoLayout =
+    OutputLayout::kItu2051_SoundSystemA_0_2_0;
+
+void DoesNotDieWithBasicDecode(const std::string& data) {
+  absl::StatusOr<api::IamfDecoder> iamf_decoder =
+      api::IamfDecoder::Create(kStereoLayout);
+  std::vector<uint8_t> bitstream(data.begin(), data.end());
+  ASSERT_THAT(iamf_decoder, ::absl_testing::IsOk());
+
+  auto decode_status = iamf_decoder->Decode(bitstream);
+}
+
+FUZZ_TEST(IamfDecoderFuzzTest_ArbitraryBytes, DoesNotDieWithBasicDecode);
+
+void DoesNotDieCreateFromDescriptors(const std::string& data) {
+  std::vector<uint8_t> bitstream(data.begin(), data.end());
+
+  absl::StatusOr<api::IamfDecoder> iamf_decoder =
+      api::IamfDecoder::CreateFromDescriptors(kStereoLayout, bitstream);
+
+  if (iamf_decoder.ok()) {
+    auto decoder_status = iamf_decoder->Decode(bitstream);
+  }
+}
+
+FUZZ_TEST(IamfDecoderFuzzTest_ArbitraryBytesToDescriptors,
+          DoesNotDieCreateFromDescriptors);
+
+void DoesNotDieAllParams(api::OutputLayout output_layout,
+                         api::OutputSampleType output_sample_type,
+                         uint32_t mix_presentation_id, std::string data) {
+  std::vector<uint8_t> bitstream(data.begin(), data.end());
+  absl::StatusOr<api::IamfDecoder> iamf_decoder =
+      api::IamfDecoder::Create(kStereoLayout);
+  ASSERT_THAT(iamf_decoder, ::absl_testing::IsOk());
+
+  auto decode_status = iamf_decoder->Decode(bitstream);
+  iamf_decoder->ConfigureOutputSampleType(output_sample_type);
+}
+
+// // TODO(b/378912426): Update this to support all output layouts.
+auto AnyOutputLayout() {
+  return ElementOf<api::OutputLayout>({
+      api::OutputLayout::kItu2051_SoundSystemA_0_2_0,
+      api::OutputLayout::kItu2051_SoundSystemB_0_5_0,
+      api::OutputLayout::kItu2051_SoundSystemC_2_5_0,
+      api::OutputLayout::kItu2051_SoundSystemD_4_5_0,
+      api::OutputLayout::kItu2051_SoundSystemE_4_5_1,
+      api::OutputLayout::kItu2051_SoundSystemF_3_7_0,
+      api::OutputLayout::kItu2051_SoundSystemG_4_9_0,
+      api::OutputLayout::kItu2051_SoundSystemH_9_10_3,
+      api::OutputLayout::kItu2051_SoundSystemI_0_7_0,
+      api::OutputLayout::kItu2051_SoundSystemJ_4_7_0,
+      api::OutputLayout::kIAMF_SoundSystemExtension_2_7_0,
+      api::OutputLayout::kIAMF_SoundSystemExtension_2_3_0,
+      api::OutputLayout::kIAMF_SoundSystemExtension_0_1_0,
+      api::OutputLayout::kIAMF_SoundSystemExtension_6_9_0,
+  });
+}
+
+auto AnyOutputSampleType() {
+  return ElementOf<api::OutputSampleType>({
+      api::OutputSampleType::kInt16LittleEndian,
+      api::OutputSampleType::kInt32LittleEndian,
+  });
+}
+
+FUZZ_TEST(IamfDecoderFuzzTest_AllArbitraryParams, DoesNotDieAllParams)
+    .WithDomains(AnyOutputLayout(),          // output_layout,
+                 AnyOutputSampleType(),      // output_sample_type,
+                 Arbitrary<uint32_t>(),      // mix_presentation_id,
+                 Arbitrary<std::string>());  // data
+
+}  // namespace
+}  // namespace iamf_tools
diff --git a/iamf/api/decoder/tests/iamf_decoder_test.cc b/iamf/api/decoder/tests/iamf_decoder_test.cc
index 7d87332..e72c794 100644
--- a/iamf/api/decoder/tests/iamf_decoder_test.cc
+++ b/iamf/api/decoder/tests/iamf_decoder_test.cc
@@ -13,6 +13,7 @@
 #include "iamf/api/decoder/iamf_decoder.h"
 
 #include <array>
+#include <cstddef>
 #include <cstdint>
 #include <list>
 #include <vector>
@@ -24,11 +25,14 @@
 #include "gtest/gtest.h"
 #include "iamf/api/types.h"
 #include "iamf/cli/audio_element_with_data.h"
+#include "iamf/cli/audio_frame_with_data.h"
+#include "iamf/cli/parameter_block_with_data.h"
 #include "iamf/cli/tests/cli_test_utils.h"
 #include "iamf/obu/audio_frame.h"
 #include "iamf/obu/codec_config.h"
 #include "iamf/obu/ia_sequence_header.h"
 #include "iamf/obu/mix_presentation.h"
+#include "iamf/obu/obu_base.h"
 #include "iamf/obu/obu_header.h"
 #include "iamf/obu/temporal_delimiter.h"
 #include "iamf/obu/types.h"
@@ -37,8 +41,15 @@
 namespace {
 
 using ::absl_testing::IsOk;
+using ::absl_testing::IsOkAndHolds;
+using ::testing::Not;
+
+using api::OutputLayout;
+using ::testing::Not;
 
 constexpr DecodedUleb128 kFirstCodecConfigId = 1;
+constexpr uint32_t kNumSamplesPerFrame = 8;
+constexpr uint32_t kBitDepth = 16;
 constexpr DecodedUleb128 kSampleRate = 48000;
 constexpr DecodedUleb128 kFirstAudioElementId = 2;
 constexpr DecodedUleb128 kFirstSubstreamId = 18;
@@ -53,8 +64,8 @@
       ObuHeader(), IASequenceHeaderObu::kIaCode,
       ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
   absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_configs;
-  AddLpcmCodecConfigWithIdAndSampleRate(kFirstCodecConfigId, kSampleRate,
-                                        codec_configs);
+  AddLpcmCodecConfig(kFirstCodecConfigId, kNumSamplesPerFrame, kBitDepth,
+                     kSampleRate, codec_configs);
   absl::flat_hash_map<DecodedUleb128, AudioElementWithData> audio_elements;
   AddAmbisonicsMonoAudioElementWithSubstreamIds(
       kFirstAudioElementId, kFirstCodecConfigId, {kFirstSubstreamId},
@@ -71,12 +82,116 @@
 
 TEST(IsDescriptorProcessingComplete,
      ReturnsFalseBeforeDescriptorObusAreProcessed) {
-  auto decoder = api::IamfDecoder::Create();
+  auto decoder =
+      api::IamfDecoder::Create(OutputLayout::kItu2051_SoundSystemA_0_2_0);
+
   EXPECT_FALSE(decoder->IsDescriptorProcessingComplete());
 }
 
+TEST(IamfDecoder,
+     MethodsDependingOnDescriptorsFailBeforeDescriptorObusAreProcessed) {
+  auto decoder =
+      api::IamfDecoder::Create(OutputLayout::kItu2051_SoundSystemA_0_2_0);
+
+  EXPECT_THAT(decoder->GetOutputLayout(), Not(IsOk()));
+  EXPECT_THAT(decoder->GetNumberOfOutputChannels(), Not(IsOk()));
+  EXPECT_THAT(decoder->GetSampleRate(), Not(IsOk()));
+  EXPECT_THAT(decoder->GetFrameSize(), Not(IsOk()));
+  std::vector<api::MixPresentationMetadata> output_mix_presentation_metadatas;
+  EXPECT_THAT(decoder->GetMixPresentations(output_mix_presentation_metadatas),
+              Not(IsOk()));
+}
+
+TEST(GetOutputLayout, ReturnsOutputLayoutAfterDescriptorObusAreProcessed) {
+  auto decoder = api::IamfDecoder::CreateFromDescriptors(
+      OutputLayout::kItu2051_SoundSystemA_0_2_0, GenerateBasicDescriptorObus());
+
+  EXPECT_TRUE(decoder->IsDescriptorProcessingComplete());
+  auto output_layout = decoder->GetOutputLayout();
+  EXPECT_THAT(output_layout.status(), IsOk());
+  EXPECT_EQ(*output_layout, OutputLayout::kItu2051_SoundSystemA_0_2_0);
+  auto number_of_output_channels = decoder->GetNumberOfOutputChannels();
+  EXPECT_THAT(number_of_output_channels.status(), IsOk());
+  EXPECT_EQ(*number_of_output_channels, 2);
+}
+
+TEST(GetOutputLayout, ReturnsDefaultStereoLayoutIfNoMatchingLayoutExists) {
+  auto decoder = api::IamfDecoder::CreateFromDescriptors(
+      OutputLayout::kItu2051_SoundSystemE_4_5_1, GenerateBasicDescriptorObus());
+
+  EXPECT_TRUE(decoder->IsDescriptorProcessingComplete());
+  auto output_layout = decoder->GetOutputLayout();
+  EXPECT_THAT(output_layout.status(), IsOk());
+  EXPECT_EQ(*output_layout, OutputLayout::kItu2051_SoundSystemA_0_2_0);
+  auto number_of_output_channels = decoder->GetNumberOfOutputChannels();
+  EXPECT_THAT(number_of_output_channels.status(), IsOk());
+  EXPECT_EQ(*number_of_output_channels, 2);
+}
+
+TEST(GetOutputLayout,
+     ReturnsDefaultStereoLayoutIfNoMatchingLayoutExistsUsingDecode) {
+  auto decoder =
+      api::IamfDecoder::Create(OutputLayout::kItu2051_SoundSystemE_4_5_1);
+  std::vector<uint8_t> source_data = GenerateBasicDescriptorObus();
+  TemporalDelimiterObu temporal_delimiter_obu =
+      TemporalDelimiterObu(ObuHeader());
+  auto temporal_delimiter_bytes =
+      SerializeObusExpectOk({&temporal_delimiter_obu});
+  source_data.insert(source_data.end(), temporal_delimiter_bytes.begin(),
+                     temporal_delimiter_bytes.end());
+
+  EXPECT_THAT(decoder->Decode(source_data), IsOk());
+
+  EXPECT_TRUE(decoder->IsDescriptorProcessingComplete());
+  auto output_layout = decoder->GetOutputLayout();
+  EXPECT_THAT(output_layout.status(), IsOk());
+  EXPECT_EQ(*output_layout, OutputLayout::kItu2051_SoundSystemA_0_2_0);
+  auto number_of_output_channels = decoder->GetNumberOfOutputChannels();
+  EXPECT_THAT(number_of_output_channels.status(), IsOk());
+  EXPECT_EQ(*number_of_output_channels, 2);
+}
+
+TEST(GetOutputLayout, ReturnsNonStereoLayoutWhenPresentInDescriptorObus) {
+  // Add a mix presentation with a non-stereo layout.
+  const IASequenceHeaderObu ia_sequence_header(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_configs;
+  AddLpcmCodecConfigWithIdAndSampleRate(kFirstCodecConfigId, kSampleRate,
+                                        codec_configs);
+  absl::flat_hash_map<DecodedUleb128, AudioElementWithData> audio_elements;
+  AddAmbisonicsMonoAudioElementWithSubstreamIds(
+      kFirstAudioElementId, kFirstCodecConfigId, {kFirstSubstreamId},
+      codec_configs, audio_elements);
+  std::vector<LoudspeakersSsConventionLayout::SoundSystem>
+      mix_presentation_layouts = {
+          LoudspeakersSsConventionLayout::kSoundSystemA_0_2_0,
+          LoudspeakersSsConventionLayout::kSoundSystemB_0_5_0};
+  std::list<MixPresentationObu> mix_presentation_obus;
+  AddMixPresentationObuWithConfigurableLayouts(
+      kFirstMixPresentationId, {kFirstAudioElementId},
+      kCommonMixGainParameterId, kCommonParameterRate, mix_presentation_layouts,
+      mix_presentation_obus);
+  std::vector<uint8_t> descriptor_obus = SerializeObusExpectOk(
+      {&ia_sequence_header, &codec_configs.at(kFirstCodecConfigId),
+       &audio_elements.at(kFirstAudioElementId).obu,
+       &mix_presentation_obus.front()});
+
+  auto decoder = api::IamfDecoder::CreateFromDescriptors(
+      OutputLayout::kItu2051_SoundSystemB_0_5_0, descriptor_obus);
+
+  EXPECT_TRUE(decoder->IsDescriptorProcessingComplete());
+  auto output_layout = decoder->GetOutputLayout();
+  EXPECT_THAT(output_layout.status(), IsOk());
+  EXPECT_EQ(*output_layout, OutputLayout::kItu2051_SoundSystemB_0_5_0);
+  auto number_of_output_channels = decoder->GetNumberOfOutputChannels();
+  EXPECT_THAT(number_of_output_channels.status(), IsOk());
+  EXPECT_EQ(*number_of_output_channels, 6);
+}
+
 TEST(Create, SucceedsAndDecodeSucceedsWithPartialData) {
-  auto decoder = api::IamfDecoder::Create();
+  auto decoder =
+      api::IamfDecoder::Create(OutputLayout::kItu2051_SoundSystemA_0_2_0);
   EXPECT_THAT(decoder, IsOk());
 
   std::vector<uint8_t> source_data = {0x01, 0x23, 0x45};
@@ -84,9 +199,24 @@
   EXPECT_FALSE(decoder->IsDescriptorProcessingComplete());
 }
 
-TEST(CreateFromDescriptors, Succeeds) {
+TEST(Create, SucceedsWithNonStereoLayout) {
   auto decoder =
-      api::IamfDecoder::CreateFromDescriptors(GenerateBasicDescriptorObus());
+      api::IamfDecoder::Create(OutputLayout::kItu2051_SoundSystemB_0_5_0);
+  EXPECT_THAT(decoder, IsOk());
+}
+
+TEST(CreateFromDescriptors, Succeeds) {
+  auto decoder = api::IamfDecoder::CreateFromDescriptors(
+      OutputLayout::kItu2051_SoundSystemA_0_2_0, GenerateBasicDescriptorObus());
+
+  EXPECT_THAT(decoder, IsOk());
+  EXPECT_TRUE(decoder->IsDescriptorProcessingComplete());
+}
+
+TEST(CreateFromDescriptors, SucceedsWithNonStereoLayout) {
+  auto decoder = api::IamfDecoder::CreateFromDescriptors(
+      OutputLayout::kItu2051_SoundSystemB_0_5_0, GenerateBasicDescriptorObus());
+
   EXPECT_THAT(decoder, IsOk());
   EXPECT_TRUE(decoder->IsDescriptorProcessingComplete());
 }
@@ -95,13 +225,16 @@
   auto descriptors = GenerateBasicDescriptorObus();
   // remove the last byte to make the descriptor OBUs incomplete.
   descriptors.pop_back();
-  auto decoder = api::IamfDecoder::CreateFromDescriptors(descriptors);
+
+  auto decoder = api::IamfDecoder::CreateFromDescriptors(
+      OutputLayout::kItu2051_SoundSystemA_0_2_0, descriptors);
+
   EXPECT_FALSE(decoder.ok());
 }
 
 TEST(CreateFromDescriptors, FailsWithDescriptorObuInSubsequentDecode) {
-  auto decoder =
-      api::IamfDecoder::CreateFromDescriptors(GenerateBasicDescriptorObus());
+  auto decoder = api::IamfDecoder::CreateFromDescriptors(
+      OutputLayout::kItu2051_SoundSystemA_0_2_0, GenerateBasicDescriptorObus());
   EXPECT_THAT(decoder, IsOk());
   EXPECT_TRUE(decoder->IsDescriptorProcessingComplete());
 
@@ -115,7 +248,8 @@
 }
 
 TEST(Decode, SucceedsAndProcessesDescriptorsWithTemporalDelimiterAtEnd) {
-  auto decoder = api::IamfDecoder::Create();
+  auto decoder =
+      api::IamfDecoder::Create(OutputLayout::kItu2051_SoundSystemA_0_2_0);
   ASSERT_THAT(decoder, IsOk());
   std::vector<uint8_t> source_data = GenerateBasicDescriptorObus();
   TemporalDelimiterObu temporal_delimiter_obu =
@@ -130,7 +264,8 @@
 }
 
 TEST(Decode, SucceedsWithMultiplePushesOfDescriptorObus) {
-  auto decoder = api::IamfDecoder::Create();
+  auto decoder =
+      api::IamfDecoder::Create(OutputLayout::kItu2051_SoundSystemA_0_2_0);
   ASSERT_THAT(decoder, IsOk());
   std::vector<uint8_t> source_data = GenerateBasicDescriptorObus();
   TemporalDelimiterObu temporal_delimiter_obu =
@@ -151,7 +286,8 @@
 
 TEST(Decode, SucceedsWithSeparatePushesOfDescriptorAndTemporalUnits) {
   std::vector<uint8_t> source_data = GenerateBasicDescriptorObus();
-  auto decoder = api::IamfDecoder::CreateFromDescriptors(source_data);
+  auto decoder = api::IamfDecoder::CreateFromDescriptors(
+      OutputLayout::kItu2051_SoundSystemA_0_2_0, source_data);
   ASSERT_THAT(decoder, IsOk());
   EXPECT_FALSE(decoder->IsTemporalUnitAvailable());
   AudioFrameObu audio_frame(ObuHeader(), kFirstSubstreamId,
@@ -162,7 +298,8 @@
 }
 
 TEST(Decode, SucceedsWithOneTemporalUnit) {
-  auto decoder = api::IamfDecoder::Create();
+  auto decoder =
+      api::IamfDecoder::Create(OutputLayout::kItu2051_SoundSystemA_0_2_0);
   ASSERT_THAT(decoder, IsOk());
   std::vector<uint8_t> source_data = GenerateBasicDescriptorObus();
   AudioFrameObu audio_frame(ObuHeader(), kFirstSubstreamId,
@@ -175,7 +312,8 @@
 }
 
 TEST(Decode, SucceedsWithMultipleTemporalUnits) {
-  auto decoder = api::IamfDecoder::Create();
+  auto decoder =
+      api::IamfDecoder::Create(OutputLayout::kItu2051_SoundSystemA_0_2_0);
   ASSERT_THAT(decoder, IsOk());
   std::vector<uint8_t> source_data = GenerateBasicDescriptorObus();
   AudioFrameObu audio_frame(ObuHeader(), kFirstSubstreamId,
@@ -187,8 +325,65 @@
   EXPECT_THAT(decoder->Decode(source_data), IsOk());
 }
 
+TEST(Decode, SucceedsWithMultipleTemporalUnitsForNonStereoLayout) {
+  auto decoder =
+      api::IamfDecoder::Create(OutputLayout::kIAMF_SoundSystemExtension_0_1_0);
+  ASSERT_THAT(decoder, IsOk());
+
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  AddLpcmCodecConfigWithIdAndSampleRate(kFirstCodecConfigId, kSampleRate,
+                                        codec_config_obus);
+  absl::flat_hash_map<DecodedUleb128, AudioElementWithData>
+      audio_elements_with_data;
+  AddAmbisonicsMonoAudioElementWithSubstreamIds(
+      kFirstAudioElementId, kFirstCodecConfigId, {kFirstSubstreamId},
+      codec_config_obus, audio_elements_with_data);
+  std::list<MixPresentationObu> mix_presentation_obus;
+  std::vector<LoudspeakersSsConventionLayout::SoundSystem>
+      sound_system_layouts = {
+          LoudspeakersSsConventionLayout::kSoundSystemA_0_2_0,
+          LoudspeakersSsConventionLayout::kSoundSystem12_0_1_0};
+  AddMixPresentationObuWithConfigurableLayouts(
+      kFirstMixPresentationId, {kFirstAudioElementId},
+      kCommonMixGainParameterId, kCommonParameterRate, sound_system_layouts,
+      mix_presentation_obus);
+
+  const std::list<AudioFrameWithData> empty_audio_frames_with_data = {};
+  const std::list<ParameterBlockWithData> empty_parameter_blocks_with_data = {};
+
+  const IASequenceHeaderObu ia_sequence_header(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  std::list<const ObuBase*> input_ia_sequence(
+      {&codec_config_obus.at(kFirstCodecConfigId),
+       &audio_elements_with_data.at(kFirstAudioElementId).obu,
+       &mix_presentation_obus.front()});
+  input_ia_sequence.push_front(&ia_sequence_header);
+
+  std::vector<uint8_t> source_data = SerializeObusExpectOk(input_ia_sequence);
+  AudioFrameObu audio_frame(ObuHeader(), kFirstSubstreamId,
+                            kEightSampleAudioFrame);
+  auto temporal_units = SerializeObusExpectOk({&audio_frame, &audio_frame});
+  source_data.insert(source_data.end(), temporal_units.begin(),
+                     temporal_units.end());
+
+  EXPECT_THAT(decoder->Decode(source_data), IsOk());
+  // Calling with empty due to forced exit after descriptor processing, so that
+  // we can get the output temporal unit.
+  EXPECT_THAT(decoder->Decode({}), IsOk());
+
+  const size_t expected_output_size = 8 * 4;  // 8 samples, 32-bit ints, mono.
+  std::vector<uint8_t> output_data(expected_output_size);
+  size_t bytes_written;
+  EXPECT_THAT(decoder->GetOutputTemporalUnit(absl::MakeSpan(output_data),
+                                             bytes_written),
+              IsOk());
+  EXPECT_EQ(bytes_written, expected_output_size);
+}
+
 TEST(Decode, FailsWhenCalledAfterFlush) {
-  auto decoder = api::IamfDecoder::Create();
+  auto decoder =
+      api::IamfDecoder::Create(OutputLayout::kItu2051_SoundSystemA_0_2_0);
   ASSERT_THAT(decoder, IsOk());
   std::vector<uint8_t> source_data = GenerateBasicDescriptorObus();
   AudioFrameObu audio_frame(ObuHeader(), kFirstSubstreamId,
@@ -196,23 +391,34 @@
   auto temporal_units = SerializeObusExpectOk({&audio_frame, &audio_frame});
   source_data.insert(source_data.end(), temporal_units.begin(),
                      temporal_units.end());
-  EXPECT_THAT(decoder->Decode(source_data), IsOk());
-  std::vector<std::vector<int32_t>> output_decoded_temporal_unit;
+  ASSERT_THAT(decoder->Decode(source_data), IsOk());
+  ASSERT_FALSE(decoder->IsTemporalUnitAvailable());
+  ASSERT_THAT(decoder->Decode({}), IsOk());
+  ASSERT_TRUE(decoder->IsTemporalUnitAvailable());
+
+  size_t expected_size = 2 * 8 * 4;  // Stereo * 8 samples * int32.
+  std::vector<uint8_t> output_data(expected_size);
+  size_t bytes_written;
   bool output_is_done;
-  EXPECT_THAT(decoder->Flush(output_decoded_temporal_unit, output_is_done),
+  EXPECT_THAT(decoder->Flush(absl::MakeSpan(output_data), bytes_written,
+                             output_is_done),
               IsOk());
+  EXPECT_TRUE(output_is_done);
+  EXPECT_EQ(bytes_written, expected_size);
   EXPECT_FALSE(decoder->Decode(source_data).ok());
 }
 
 TEST(IsTemporalUnitAvailable, ReturnsFalseAfterCreateFromDescriptorObus) {
-  auto decoder =
-      api::IamfDecoder::CreateFromDescriptors(GenerateBasicDescriptorObus());
+  auto decoder = api::IamfDecoder::CreateFromDescriptors(
+      OutputLayout::kItu2051_SoundSystemA_0_2_0, GenerateBasicDescriptorObus());
   ASSERT_THAT(decoder, IsOk());
   EXPECT_FALSE(decoder->IsTemporalUnitAvailable());
 }
 
-TEST(IsTemporalUnitAvailable, ReturnsTrueAfterDecodingOneTemporalUnit) {
-  auto decoder = api::IamfDecoder::Create();
+TEST(IsTemporalUnitAvailable,
+     TemporalUnitIsNotAvailableAfterDecodeWithNoTemporalDelimiterAtEnd) {
+  auto decoder =
+      api::IamfDecoder::Create(OutputLayout::kItu2051_SoundSystemA_0_2_0);
   ASSERT_THAT(decoder, IsOk());
   std::vector<uint8_t> source_data = GenerateBasicDescriptorObus();
   AudioFrameObu audio_frame(ObuHeader(), kFirstSubstreamId,
@@ -222,11 +428,41 @@
                      temporal_unit.end());
 
   ASSERT_THAT(decoder->Decode(source_data), IsOk());
+  EXPECT_FALSE(decoder->IsTemporalUnitAvailable());
+}
+
+TEST(IsTemporalUnitAvailable,
+     ReturnsTrueAfterDecodingOneTemporalUnitWithTemporalDelimiterAtEnd) {
+  auto decoder =
+      api::IamfDecoder::Create(OutputLayout::kItu2051_SoundSystemA_0_2_0);
+  ASSERT_THAT(decoder, IsOk());
+  std::vector<uint8_t> source_data = GenerateBasicDescriptorObus();
+  AudioFrameObu audio_frame(ObuHeader(), kFirstSubstreamId,
+                            kEightSampleAudioFrame);
+  auto temporal_delimiter_obu = TemporalDelimiterObu(ObuHeader());
+  auto temporal_unit =
+      SerializeObusExpectOk({&audio_frame, &temporal_delimiter_obu});
+  source_data.insert(source_data.end(), temporal_unit.begin(),
+                     temporal_unit.end());
+
+  ASSERT_THAT(decoder->Decode(source_data), IsOk());
+  EXPECT_TRUE(decoder->IsDescriptorProcessingComplete());
+  // Even though a temporal unit was provided, it has not been decoded yet. This
+  // is because Decode() returns after processing the descriptor OBUs, even if
+  // there is more data available. This is done to give the user a chance to do
+  // any configurations based on the descriptors before beginning to decode the
+  // temporal units.
+  EXPECT_FALSE(decoder->IsTemporalUnitAvailable());
+  // The user can call Decode() again to process the temporal unit still
+  // available in the buffer.
+  EXPECT_THAT(decoder->Decode({}), IsOk());
+  // Now, the temporal unit has been decoded and is available for output.
   EXPECT_TRUE(decoder->IsTemporalUnitAvailable());
 }
 
 TEST(IsTemporalUnitAvailable, ReturnsTrueAfterDecodingMultipleTemporalUnits) {
-  auto decoder = api::IamfDecoder::Create();
+  auto decoder =
+      api::IamfDecoder::Create(OutputLayout::kItu2051_SoundSystemA_0_2_0);
   ASSERT_THAT(decoder, IsOk());
   std::vector<uint8_t> source_data = GenerateBasicDescriptorObus();
   AudioFrameObu audio_frame(ObuHeader(), kFirstSubstreamId,
@@ -236,28 +472,14 @@
                      temporal_units.end());
 
   ASSERT_THAT(decoder->Decode(source_data), IsOk());
+  EXPECT_FALSE(decoder->IsTemporalUnitAvailable());
+  EXPECT_THAT(decoder->Decode({}), IsOk());
   EXPECT_TRUE(decoder->IsTemporalUnitAvailable());
 }
 
-TEST(IsTemporalUnitAvailable, ReturnsFalseAfterPoppingLastTemporalUnit) {
-  auto decoder = api::IamfDecoder::Create();
-  ASSERT_THAT(decoder, IsOk());
-  std::vector<uint8_t> source_data = GenerateBasicDescriptorObus();
-  AudioFrameObu audio_frame(ObuHeader(), kFirstSubstreamId,
-                            kEightSampleAudioFrame);
-  auto temporal_unit = SerializeObusExpectOk({&audio_frame});
-  source_data.insert(source_data.end(), temporal_unit.begin(),
-                     temporal_unit.end());
-
-  ASSERT_THAT(decoder->Decode(source_data), IsOk());
-  std::vector<std::vector<int32_t>> output_decoded_temporal_unit;
-  ASSERT_THAT(decoder->GetOutputTemporalUnit(output_decoded_temporal_unit),
-              IsOk());
-  EXPECT_FALSE(decoder->IsTemporalUnitAvailable());
-}
-
-TEST(GetOutputTemporalUnit, FillsOutputVectorWithMultipleTemporalUnits) {
-  auto decoder = api::IamfDecoder::Create();
+TEST(GetOutputTemporalUnit, FillsOutputVectorWithLastTemporalUnit) {
+  auto decoder =
+      api::IamfDecoder::Create(OutputLayout::kItu2051_SoundSystemA_0_2_0);
   ASSERT_THAT(decoder, IsOk());
   std::vector<uint8_t> source_data = GenerateBasicDescriptorObus();
   AudioFrameObu audio_frame(ObuHeader(), kFirstSubstreamId,
@@ -266,65 +488,159 @@
   source_data.insert(source_data.end(), temporal_units.begin(),
                      temporal_units.end());
   ASSERT_THAT(decoder->Decode(source_data), IsOk());
+  EXPECT_FALSE(decoder->IsTemporalUnitAvailable());
+  EXPECT_THAT(decoder->Decode({}), IsOk());
+  EXPECT_TRUE(decoder->IsTemporalUnitAvailable());
 
-  const std::vector<std::vector<int32_t>> expected_decoded_temporal_unit = {
-      {23772706, 23773107},   {47591754, 47592556},   {71410802, 71412005},
-      {95229849, 95231454},   {119048897, 119050903}, {142867944, 142870353},
-      {166686992, 166689802}, {190506039, 190509251}};
+  size_t expected_size = 2 * 8 * 4;
+  std::vector<uint8_t> output_data(expected_size);
+  size_t bytes_written;
+  EXPECT_THAT(decoder->GetOutputTemporalUnit(absl::MakeSpan(output_data),
+                                             bytes_written),
+              IsOk());
 
-  // Get the first temporal unit.
-  std::vector<std::vector<int32_t>> output_decoded_temporal_unit;
-  EXPECT_THAT(decoder->GetOutputTemporalUnit(output_decoded_temporal_unit),
+  EXPECT_EQ(bytes_written, expected_size);
+  EXPECT_FALSE(decoder->IsTemporalUnitAvailable());
+}
+
+TEST(GetOutputTemporalUnit, FillsOutputVectorWithInt16) {
+  auto decoder =
+      api::IamfDecoder::Create(OutputLayout::kItu2051_SoundSystemA_0_2_0);
+  decoder->ConfigureOutputSampleType(api::OutputSampleType::kInt16LittleEndian);
+  ASSERT_THAT(decoder, IsOk());
+  std::vector<uint8_t> source_data = GenerateBasicDescriptorObus();
+  AudioFrameObu audio_frame(ObuHeader(), kFirstSubstreamId,
+                            kEightSampleAudioFrame);
+  auto temporal_units = SerializeObusExpectOk({&audio_frame, &audio_frame});
+  source_data.insert(source_data.end(), temporal_units.begin(),
+                     temporal_units.end());
+
+  ASSERT_THAT(decoder->Decode(source_data), IsOk());
+  EXPECT_FALSE(decoder->IsTemporalUnitAvailable());
+  EXPECT_THAT(decoder->Decode({}), IsOk());
+  EXPECT_TRUE(decoder->IsTemporalUnitAvailable());
+
+  size_t expected_size = 2 * 8 * 2;  // Stereo * 8 samples * 2 bytes (int16).
+  std::vector<uint8_t> output_data(expected_size);
+  size_t bytes_written;
+  EXPECT_THAT(decoder->GetOutputTemporalUnit(absl::MakeSpan(output_data),
+                                             bytes_written),
               IsOk());
-  EXPECT_EQ(output_decoded_temporal_unit, expected_decoded_temporal_unit);
-  output_decoded_temporal_unit.clear();
-  // Get the second temporal unit.
-  EXPECT_THAT(decoder->GetOutputTemporalUnit(output_decoded_temporal_unit),
-              IsOk());
-  EXPECT_EQ(output_decoded_temporal_unit, expected_decoded_temporal_unit);
+
+  EXPECT_EQ(bytes_written, expected_size);
+}
+
+TEST(GetOutputTemporalUnit, FailsWhenBufferTooSmall) {
+  auto decoder =
+      api::IamfDecoder::Create(OutputLayout::kItu2051_SoundSystemA_0_2_0);
+  decoder->ConfigureOutputSampleType(api::OutputSampleType::kInt16LittleEndian);
+  ASSERT_THAT(decoder, IsOk());
+  std::vector<uint8_t> source_data = GenerateBasicDescriptorObus();
+  AudioFrameObu audio_frame(ObuHeader(), kFirstSubstreamId,
+                            kEightSampleAudioFrame);
+  auto temporal_units = SerializeObusExpectOk({&audio_frame, &audio_frame});
+  source_data.insert(source_data.end(), temporal_units.begin(),
+                     temporal_units.end());
+
+  ASSERT_THAT(decoder->Decode(source_data), IsOk());
+  EXPECT_FALSE(decoder->IsTemporalUnitAvailable());
+  EXPECT_THAT(decoder->Decode({}), IsOk());
+  EXPECT_TRUE(decoder->IsTemporalUnitAvailable());
+
+  size_t needed_size = 2 * 8 * 2;  // Stereo * 8 samples * 2 bytes (int16).
+  std::vector<uint8_t> output_data(needed_size - 1);  // Buffer too small.
+  size_t bytes_written;
+  EXPECT_THAT(decoder->GetOutputTemporalUnit(absl::MakeSpan(output_data),
+                                             bytes_written),
+              Not(IsOk()));
+  EXPECT_EQ(bytes_written, 0);
 }
 
 TEST(GetOutputTemporalUnit,
      DoesNotFillOutputVectorWhenNoTemporalUnitIsAvailable) {
   std::vector<uint8_t> source_data = GenerateBasicDescriptorObus();
-  auto decoder = api::IamfDecoder::CreateFromDescriptors(source_data);
+  auto decoder = api::IamfDecoder::CreateFromDescriptors(
+      OutputLayout::kItu2051_SoundSystemA_0_2_0, source_data);
   ASSERT_THAT(decoder, IsOk());
 
-  std::vector<std::vector<int32_t>> output_decoded_temporal_unit;
-  EXPECT_THAT(decoder->GetOutputTemporalUnit(output_decoded_temporal_unit),
+  std::vector<uint8_t> output_data;
+  size_t bytes_written;
+  EXPECT_THAT(decoder->GetOutputTemporalUnit(absl::MakeSpan(output_data),
+                                             bytes_written),
               IsOk());
-  EXPECT_TRUE(output_decoded_temporal_unit.empty());
+
+  EXPECT_EQ(bytes_written, 0);
 }
 
 TEST(Flush, SucceedsWithMultipleTemporalUnits) {
-  auto decoder = api::IamfDecoder::Create();
+  auto decoder =
+      api::IamfDecoder::Create(OutputLayout::kItu2051_SoundSystemA_0_2_0);
   ASSERT_THAT(decoder, IsOk());
   std::vector<uint8_t> source_data = GenerateBasicDescriptorObus();
   AudioFrameObu audio_frame(ObuHeader(), kFirstSubstreamId,
                             kEightSampleAudioFrame);
-  auto temporal_units = SerializeObusExpectOk({&audio_frame, &audio_frame});
+  auto temporal_delimiter_obu = TemporalDelimiterObu(ObuHeader());
+  auto temporal_units =
+      SerializeObusExpectOk({&audio_frame, &temporal_delimiter_obu,
+                             &audio_frame, &temporal_delimiter_obu});
   source_data.insert(source_data.end(), temporal_units.begin(),
                      temporal_units.end());
   ASSERT_THAT(decoder->Decode(source_data), IsOk());
-  std::vector<std::vector<int32_t>> output_decoded_temporal_unit;
+  EXPECT_FALSE(decoder->IsTemporalUnitAvailable());
+  EXPECT_THAT(decoder->Decode({}), IsOk());
+  EXPECT_TRUE(decoder->IsTemporalUnitAvailable());
+
+  // Stereo * 8 samples * 4 bytes per sample
+  const size_t expected_size_per_temp_unit = 2 * 8 * 4;
+  std::vector<uint8_t> output_data(expected_size_per_temp_unit);
+  size_t bytes_written;
   bool output_is_done;
-  EXPECT_THAT(decoder->Flush(output_decoded_temporal_unit, output_is_done),
+  EXPECT_THAT(decoder->Flush(absl::MakeSpan(output_data), bytes_written,
+                             output_is_done),
               IsOk());
+  EXPECT_EQ(bytes_written, expected_size_per_temp_unit);
   EXPECT_FALSE(output_is_done);
-  EXPECT_THAT(decoder->Flush(output_decoded_temporal_unit, output_is_done),
+  EXPECT_THAT(decoder->Flush(absl::MakeSpan(output_data), bytes_written,
+                             output_is_done),
               IsOk());
+  EXPECT_EQ(bytes_written, expected_size_per_temp_unit);
   EXPECT_TRUE(output_is_done);
 }
 
 TEST(Flush, SucceedsWithNoTemporalUnits) {
-  auto decoder = api::IamfDecoder::Create();
+  auto decoder =
+      api::IamfDecoder::Create(OutputLayout::kItu2051_SoundSystemA_0_2_0);
   ASSERT_THAT(decoder, IsOk());
+
   std::vector<std::vector<int32_t>> output_decoded_temporal_unit;
+  std::vector<uint8_t> output_data;
+  size_t bytes_written;
   bool output_is_done;
-  EXPECT_THAT(decoder->Flush(output_decoded_temporal_unit, output_is_done),
+  EXPECT_THAT(decoder->Flush(absl::MakeSpan(output_data), bytes_written,
+                             output_is_done),
               IsOk());
+
+  EXPECT_EQ(bytes_written, 0);
   EXPECT_TRUE(output_is_done);
 }
 
+TEST(GetSampleRate, ReturnsSampleRateBasedOnCodecConfigObu) {
+  std::vector<uint8_t> source_data = GenerateBasicDescriptorObus();
+  auto decoder = api::IamfDecoder::CreateFromDescriptors(
+      api::OutputLayout::kItu2051_SoundSystemA_0_2_0, source_data);
+  ASSERT_THAT(decoder, IsOk());
+
+  EXPECT_THAT(decoder->GetSampleRate(), IsOkAndHolds(kSampleRate));
+}
+
+TEST(GetFrameSize, ReturnsFrameSizeBasedOnCodecConfigObu) {
+  std::vector<uint8_t> source_data = GenerateBasicDescriptorObus();
+  auto decoder = api::IamfDecoder::CreateFromDescriptors(
+      api::OutputLayout::kItu2051_SoundSystemA_0_2_0, source_data);
+  ASSERT_THAT(decoder, IsOk());
+
+  EXPECT_THAT(decoder->GetFrameSize(), IsOkAndHolds(kNumSamplesPerFrame));
+}
+
 }  // namespace
 }  // namespace iamf_tools
diff --git a/iamf/api/types.h b/iamf/api/types.h
index 173b712..76a7f85 100644
--- a/iamf/api/types.h
+++ b/iamf/api/types.h
@@ -57,13 +57,10 @@
   kIAMF_SoundSystemExtension_6_9_0 = 13,
 };
 
-/*!\brief Determines the format of the output file. */
-enum OutputFileBitDepth {
-  kBitDepthAutomatic,  // Automatically determine based on the bit-depth of
-                       // the input file.
-  kBitDepth16,
-  kBitDepth24,
-  kBitDepth32,
+/*!\brief The requested format of the output samples. */
+enum class OutputSampleType {
+  kInt16LittleEndian = 1,
+  kInt32LittleEndian = 2,
 };
 
 /*!\brief A unique identifier for a `MixPresentation` in the IAMF stream. */
diff --git a/iamf/cli/BUILD b/iamf/cli/BUILD
index 231777c..5791758 100644
--- a/iamf/cli/BUILD
+++ b/iamf/cli/BUILD
@@ -292,6 +292,7 @@
         ":sample_processor_base",
         "//iamf/common:read_bit_buffer",
         "//iamf/common/utils:macros",
+        "//iamf/common/utils:validation_utils",
         "//iamf/obu:audio_element",
         "//iamf/obu:audio_frame",
         "//iamf/obu:codec_config",
diff --git a/iamf/cli/adm_to_user_metadata/adm/panner.cc b/iamf/cli/adm_to_user_metadata/adm/panner.cc
index bd35d81..43ea63d 100644
--- a/iamf/cli/adm_to_user_metadata/adm/panner.cc
+++ b/iamf/cli/adm_to_user_metadata/adm/panner.cc
@@ -178,7 +178,7 @@
     std::vector<uint8_t> output_buffer_char(
         num_samples_to_read * op_wav_nch *
         (ip_wav_bits_per_sample / kBitsPerByte));
-    int write_position = 0;
+    size_t write_position = 0;
     for (size_t i = 0; i < num_samples_to_read * op_wav_nch; ++i) {
       RETURN_IF_NOT_OK(WritePcmSample(
           op_buffer_int32[i], ip_wav_bits_per_sample,
diff --git a/iamf/cli/adm_to_user_metadata/iamf/mix_presentation_handler.cc b/iamf/cli/adm_to_user_metadata/iamf/mix_presentation_handler.cc
index 076fe49..85817ad 100644
--- a/iamf/cli/adm_to_user_metadata/iamf/mix_presentation_handler.cc
+++ b/iamf/cli/adm_to_user_metadata/iamf/mix_presentation_handler.cc
@@ -225,11 +225,9 @@
   mix_presentation_obu_metadata.add_annotations_language("en-us");
   mix_presentation_obu_metadata.add_localized_presentation_annotations(
       "test_mix_pres");
-  mix_presentation_obu_metadata.set_num_sub_mixes(1);
 
   auto& mix_presentation_sub_mix =
       *mix_presentation_obu_metadata.add_sub_mixes();
-  mix_presentation_sub_mix.set_num_audio_elements(audio_objects.size());
   for (const auto& audio_object : audio_objects) {
     const auto status = SubMixAudioElementMetadataBuilder(
         audio_object, audio_object_id_to_audio_element_id_[audio_object.id],
diff --git a/iamf/cli/adm_to_user_metadata/iamf/tests/mix_presentation_handler_test.cc b/iamf/cli/adm_to_user_metadata/iamf/tests/mix_presentation_handler_test.cc
index 9d92713..672a3e2 100644
--- a/iamf/cli/adm_to_user_metadata/iamf/tests/mix_presentation_handler_test.cc
+++ b/iamf/cli/adm_to_user_metadata/iamf/tests/mix_presentation_handler_test.cc
@@ -109,9 +109,9 @@
 TEST(PopulateMixPresentation, PopulatesStereoSubmix) {
   const auto& mix_presentation_metadata = GetMixObuMetataExpectOk();
 
-  EXPECT_EQ(mix_presentation_metadata.num_sub_mixes(), 1);
+  EXPECT_EQ(mix_presentation_metadata.sub_mixes_size(), 1);
   const auto& submix = mix_presentation_metadata.sub_mixes(0);
-  EXPECT_EQ(submix.num_audio_elements(), 1);
+  EXPECT_EQ(submix.audio_elements_size(), 1);
   const auto& audio_element = submix.audio_elements(0);
 
   const uint32_t kExpectedAudioElementId = 0;
@@ -245,14 +245,14 @@
 TEST(PopulateMixPresentation, AlwaysPopulatesExactlyOneSubmix) {
   EXPECT_EQ(
       GetMixObuMetataExpectOk({GetStereoAudioObject(), Get5_1AudioObject()})
-          .num_sub_mixes(),
+          .sub_mixes_size(),
       1);
 }
 TEST(PopulateMixPresentation, PopulatesOneAudioElementPerAudioObject) {
   EXPECT_EQ(
       GetMixObuMetataExpectOk({GetStereoAudioObject(), Get5_1AudioObject()})
           .sub_mixes(0)
-          .num_audio_elements(),
+          .audio_elements_size(),
       2);
 }
 
diff --git a/iamf/cli/adm_to_user_metadata/iamf/tests/user_metadata_generator_test.cc b/iamf/cli/adm_to_user_metadata/iamf/tests/user_metadata_generator_test.cc
index cddb207..8cae426 100644
--- a/iamf/cli/adm_to_user_metadata/iamf/tests/user_metadata_generator_test.cc
+++ b/iamf/cli/adm_to_user_metadata/iamf/tests/user_metadata_generator_test.cc
@@ -197,7 +197,7 @@
   EXPECT_EQ(user_metadata.mix_presentation_metadata().size(), 1);
   EXPECT_EQ(user_metadata.mix_presentation_metadata(0)
                 .sub_mixes(0)
-                .num_audio_elements(),
+                .audio_elements_size(),
             1);
 }
 
@@ -216,16 +216,16 @@
       GetAdmWithStereoAndToaObjectsAndTwoAudioProgrammes());
 
   EXPECT_EQ(user_metadata.mix_presentation_metadata().size(), 2);
-  EXPECT_EQ(user_metadata.mix_presentation_metadata(0).num_sub_mixes(), 1);
+  EXPECT_EQ(user_metadata.mix_presentation_metadata(0).sub_mixes_size(), 1);
   EXPECT_EQ(user_metadata.mix_presentation_metadata(0)
                 .sub_mixes(0)
-                .num_audio_elements(),
+                .audio_elements_size(),
             kExpectedFirstMixPresentationNumAudioElements);
 
-  EXPECT_EQ(user_metadata.mix_presentation_metadata(1).num_sub_mixes(), 1);
+  EXPECT_EQ(user_metadata.mix_presentation_metadata(1).sub_mixes_size(), 1);
   EXPECT_EQ(user_metadata.mix_presentation_metadata(1)
                 .sub_mixes(0)
-                .num_audio_elements(),
+                .audio_elements_size(),
             kExpectedSecondMixPresentationNumAudioElements);
 }
 
diff --git a/iamf/cli/ambisonic_encoder/ambisonic_encoder.h b/iamf/cli/ambisonic_encoder/ambisonic_encoder.h
index c9e367f..f7ac799 100644
--- a/iamf/cli/ambisonic_encoder/ambisonic_encoder.h
+++ b/iamf/cli/ambisonic_encoder/ambisonic_encoder.h
@@ -23,7 +23,7 @@
 
 namespace iamf_tools {
 
-// TODO(b/400635711): Use the one in the iamfbr library once it is open-sourced.
+// TODO(b/400635711): Use the one in the obr library once it is open-sourced.
 class AmbisonicEncoder {
  public:
   /*!\brief Ambisonic Encoder constructor.
diff --git a/iamf/cli/ambisonic_encoder/ambisonic_utils.h b/iamf/cli/ambisonic_encoder/ambisonic_utils.h
index 6e8eb83..236b136 100644
--- a/iamf/cli/ambisonic_encoder/ambisonic_utils.h
+++ b/iamf/cli/ambisonic_encoder/ambisonic_utils.h
@@ -18,7 +18,7 @@
 
 #include "absl/log/check.h"
 
-// TODO(b/400635711): Use the one in the iamfbr library once it is open-sourced.
+// TODO(b/400635711): Use the one in the obr library once it is open-sourced.
 // This code is forked from Resonance Audio's `misc_math.h`.
 namespace iamf_tools {
 // Defines conversion factor from degrees to radians.
diff --git a/iamf/cli/ambisonic_encoder/associated_legendre_polynomials_generator.cc b/iamf/cli/ambisonic_encoder/associated_legendre_polynomials_generator.cc
index 68deb2a..4ea7352 100644
--- a/iamf/cli/ambisonic_encoder/associated_legendre_polynomials_generator.cc
+++ b/iamf/cli/ambisonic_encoder/associated_legendre_polynomials_generator.cc
@@ -9,7 +9,7 @@
  * source code in the PATENTS file, you can obtain it at
  * www.aomedia.org/license/patent.
  */
-// TODO(b/400635711): Use the one in the iamfbr library once it is open-sourced.
+// TODO(b/400635711): Use the one in the obr library once it is open-sourced.
 #include "iamf/cli/ambisonic_encoder/associated_legendre_polynomials_generator.h"
 
 #include <cmath>
diff --git a/iamf/cli/ambisonic_encoder/associated_legendre_polynomials_generator.h b/iamf/cli/ambisonic_encoder/associated_legendre_polynomials_generator.h
index f88f987..5a9ebc7 100644
--- a/iamf/cli/ambisonic_encoder/associated_legendre_polynomials_generator.h
+++ b/iamf/cli/ambisonic_encoder/associated_legendre_polynomials_generator.h
@@ -9,7 +9,7 @@
  * source code in the PATENTS file, you can obtain it at
  * www.aomedia.org/license/patent.
  */
-// TODO(b/400635711): Use the one in the iamfbr library once it is open-sourced.
+// TODO(b/400635711): Use the one in the obr library once it is open-sourced.
 #ifndef CLI_AMBISONIC_ENCODER_ASSOCIATED_LEGENDRE_POLYNOMIALS_GENERATOR_H_
 #define CLI_AMBISONIC_ENCODER_ASSOCIATED_LEGENDRE_POLYNOMIALS_GENERATOR_H_
 
diff --git a/iamf/cli/cli_util.cc b/iamf/cli/cli_util.cc
index 07c7acd..5e6e554 100644
--- a/iamf/cli/cli_util.cc
+++ b/iamf/cli/cli_util.cc
@@ -102,7 +102,7 @@
     const Layout& layout, int& output_submix_index, int& output_layout_index) {
   for (int s = 0; s < mix_presentation_sub_mixes.size(); s++) {
     const auto& sub_mix = mix_presentation_sub_mixes[s];
-    for (int l = 0; l < sub_mix.num_layouts; l++) {
+    for (int l = 0; l < sub_mix.layouts.size(); l++) {
       const auto& mix_presentation_layout = sub_mix.layouts[l];
       if (layout == mix_presentation_layout.loudness_layout) {
         output_submix_index = s;
@@ -184,24 +184,19 @@
 }
 
 absl::Status WritePcmFrameToBuffer(
-    const std::vector<std::vector<int32_t>>& frame,
-    uint32_t samples_to_trim_at_start, uint32_t samples_to_trim_at_end,
-    uint8_t bit_depth, bool big_endian, std::vector<uint8_t>& buffer) {
-  if (bit_depth % 8 != 0) {
+    const std::vector<std::vector<int32_t>>& frame, uint8_t bit_depth,
+    bool big_endian, std::vector<uint8_t>& buffer) {
+  if (bit_depth % 8 != 0) [[unlikely]] {
     return absl::InvalidArgumentError(
         "This function only supports an integer number of bytes.");
   }
-  const size_t num_samples =
-      (frame.size() - samples_to_trim_at_start - samples_to_trim_at_end) *
-      frame[0].size();
-
+  const size_t num_samples = frame.size() * frame[0].size();
   buffer.resize(num_samples * (bit_depth / 8));
 
   // The input frame is arranged in (time, channel) axes. Interlace these in
-  // the output PCM and skip over any trimmed samples.
-  int write_position = 0;
-  for (int t = samples_to_trim_at_start;
-       t < frame.size() - samples_to_trim_at_end; t++) {
+  // the output PCM.
+  size_t write_position = 0;
+  for (int t = 0; t < frame.size(); t++) {
     for (int c = 0; c < frame[0].size(); ++c) {
       const uint32_t sample = static_cast<uint32_t>(frame[t][c]);
       RETURN_IF_NOT_OK(WritePcmSample(sample, bit_depth, big_endian,
diff --git a/iamf/cli/cli_util.h b/iamf/cli/cli_util.h
index 5a36510..f28d238 100644
--- a/iamf/cli/cli_util.h
+++ b/iamf/cli/cli_util.h
@@ -90,8 +90,6 @@
 /*!\brief Writes interlaced PCM samples into the output buffer.
  *
  * \param frame Input frames arranged in (time, channel) axes.
- * \param samples_to_trim_at_start Samples to trim at the beginning.
- * \param samples_to_trim_at_end Samples to trim at the end.
  * \param bit_depth Sample size in bits.
  * \param big_endian Whether the sample should be written as big or little
  *        endian.
@@ -99,9 +97,8 @@
  * \return `absl::OkStatus()` on success. A specific status on failure.
  */
 absl::Status WritePcmFrameToBuffer(
-    const std::vector<std::vector<int32_t>>& frame,
-    uint32_t samples_to_trim_at_start, uint32_t samples_to_trim_at_end,
-    uint8_t bit_depth, bool big_endian, std::vector<uint8_t>& buffer);
+    const std::vector<std::vector<int32_t>>& frame, uint8_t bit_depth,
+    bool big_endian, std::vector<uint8_t>& buffer);
 
 /*!\brief Gets the common output sample rate and bit-deph of the input sets.
  *
diff --git a/iamf/cli/codec/aac_encoder.cc b/iamf/cli/codec/aac_encoder.cc
index b8959c2..aed5eb7 100644
--- a/iamf/cli/codec/aac_encoder.cc
+++ b/iamf/cli/codec/aac_encoder.cc
@@ -11,6 +11,7 @@
  */
 #include "iamf/cli/codec/aac_encoder.h"
 
+#include <cstddef>
 #include <cstdint>
 #include <memory>
 #include <utility>
@@ -221,7 +222,7 @@
   const bool big_endian = IsNativeBigEndian();
   std::vector<INT_PCM> encoder_input_pcm(
       num_samples_per_channel * num_channels_, 0);
-  int write_position = 0;
+  size_t write_position = 0;
   for (int t = 0; t < samples.size(); t++) {
     for (int c = 0; c < samples[0].size(); ++c) {
       // Convert all frames to INT_PCM samples for input for `fdk_aac` (usually
diff --git a/iamf/cli/codec/lpcm_encoder.cc b/iamf/cli/codec/lpcm_encoder.cc
index 217636b..e00bad5 100644
--- a/iamf/cli/codec/lpcm_encoder.cc
+++ b/iamf/cli/codec/lpcm_encoder.cc
@@ -72,10 +72,8 @@
   auto& audio_frame = partial_audio_frame_with_data->obu.audio_frame_;
   const bool big_endian = !(decoder_config_.sample_format_flags_bitmask_ &
                             LpcmDecoderConfig::kLpcmLittleEndian);
-  RETURN_IF_NOT_OK(WritePcmFrameToBuffer(
-      samples, /*samples_to_trim_at_start=*/0,
-      /*samples_to_trim_at_end=*/0, decoder_config_.sample_size_, big_endian,
-      audio_frame));
+  RETURN_IF_NOT_OK(WritePcmFrameToBuffer(samples, decoder_config_.sample_size_,
+                                         big_endian, audio_frame));
 
   absl::MutexLock lock(&mutex_);
   finalized_audio_frames_.emplace_back(
diff --git a/iamf/cli/codec/opus_encoder.cc b/iamf/cli/codec/opus_encoder.cc
index dd77564..e212cbd 100644
--- a/iamf/cli/codec/opus_encoder.cc
+++ b/iamf/cli/codec/opus_encoder.cc
@@ -11,6 +11,7 @@
  */
 #include "iamf/cli/codec/opus_encoder.h"
 
+#include <cstddef>
 #include <cstdint>
 #include <memory>
 #include <utility>
@@ -90,7 +91,7 @@
   // Convert input to the array that will be passed to `opus_encode`.
   std::vector<opus_int16> encoder_input_pcm(
       num_samples_per_channel * num_channels, 0);
-  int write_position = 0;
+  size_t write_position = 0;
   for (int t = 0; t < samples.size(); t++) {
     for (int c = 0; c < samples[0].size(); ++c) {
       // Convert all frames to 16-bit samples for input to Opus.
diff --git a/iamf/cli/demixing_module.cc b/iamf/cli/demixing_module.cc
index 3be62a8..07f9d68 100644
--- a/iamf/cli/demixing_module.cc
+++ b/iamf/cli/demixing_module.cc
@@ -28,6 +28,7 @@
 #include "absl/status/status.h"
 #include "absl/status/statusor.h"
 #include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
 #include "iamf/cli/audio_element_with_data.h"
 #include "iamf/cli/audio_frame_decoder.h"
 #include "iamf/cli/audio_frame_with_data.h"
@@ -614,8 +615,8 @@
 }
 
 // NOOP function if the frame is not a DecodedAudioFrame.
-absl::Status PassThroughReconGainData(const AudioFrameWithData& audio_frame,
-                                      LabeledFrame& labeled_frame) {
+absl::Status PassThroughReconGainData(const AudioFrameWithData& /*audio_frame*/,
+                                      LabeledFrame& /*labeled_frame*/) {
   return absl::OkStatus();
 }
 
@@ -757,21 +758,16 @@
           "Unsupported audio element type= ", obu.GetAudioElementType()));
   }
 }
-void LogForAudioElementId(
-    DecodedUleb128 audio_element_id,
-    const IdLabeledFrameMap& id_to_labeled_frame,
-    const IdLabeledFrameMap& id_to_labeled_decoded_frame) {
-  if (!id_to_labeled_frame.contains(audio_element_id) ||
-      id_to_labeled_decoded_frame.contains(audio_element_id)) {
+void LogForAudioElementId(absl::string_view log_prefix,
+                          DecodedUleb128 audio_element_id,
+                          const IdLabeledFrameMap& id_to_labeled_frame) {
+  if (!id_to_labeled_frame.contains(audio_element_id)) {
     return;
   }
   for (const auto& [label, samples] :
        id_to_labeled_frame.at(audio_element_id).label_to_samples) {
-    const auto& decoded_samples =
-        id_to_labeled_decoded_frame.at(audio_element_id)
-            .label_to_samples.at(label);
-    LOG(INFO) << "  Channel " << label << ":\tframe size= " << samples.size()
-              << "; decoded frame size= " << decoded_samples.size();
+    LOG_FIRST_N(INFO, 1) << "  Channel " << label << ":\t" << log_prefix
+                         << " frame size= " << samples.size() << ".";
   }
 }
 
@@ -813,7 +809,8 @@
         audio_element_id_to_demixing_metadata[audio_element_id]));
   }
 
-  return DemixingModule(std::move(audio_element_id_to_demixing_metadata));
+  return DemixingModule(DemixingMode::kDownMixingAndReconstruction,
+                        std::move(audio_element_id_to_demixing_metadata));
 }
 
 absl::StatusOr<DemixingModule> DemixingModule::CreateForReconstruction(
@@ -839,7 +836,8 @@
     iter->second.down_mixers.clear();
   }
 
-  return DemixingModule(std::move(audio_element_id_to_demixing_metadata));
+  return DemixingModule(DemixingMode::kReconstruction,
+                        std::move(audio_element_id_to_demixing_metadata));
 }
 
 absl::Status DemixingModule::DownMixSamplesToSubstreams(
@@ -923,11 +921,14 @@
 }
 
 // TODO(b/288240600): Down-mix audio samples in a standalone function too.
-absl::Status DemixingModule::DemixAudioSamples(
-    const std::list<AudioFrameWithData>& audio_frames,
-    const std::list<DecodedAudioFrame>& decoded_audio_frames,
-    IdLabeledFrameMap& id_to_labeled_frame,
-    IdLabeledFrameMap& id_to_labeled_decoded_frame) const {
+absl::StatusOr<IdLabeledFrameMap> DemixingModule::DemixOriginalAudioSamples(
+    const std::list<AudioFrameWithData>& audio_frames) const {
+  if (demixing_mode_ == DemixingMode::kReconstruction) {
+    return absl::FailedPreconditionError(
+        "Demixing original audio samples is not available in reconstruction "
+        "mode.");
+  }
+  IdLabeledFrameMap id_to_labeled_frame;
   for (const auto& [audio_element_id, demixing_metadata] :
        audio_element_id_to_demixing_metadata_) {
     // Process the original audio frames.
@@ -939,6 +940,18 @@
           ApplyDemixers(demixing_metadata.demixers, labeled_frame));
       id_to_labeled_frame[audio_element_id] = std::move(labeled_frame);
     }
+
+    LogForAudioElementId("Original", audio_element_id, id_to_labeled_frame);
+  }
+
+  return id_to_labeled_frame;
+}
+
+absl::StatusOr<IdLabeledFrameMap> DemixingModule::DemixDecodedAudioSamples(
+    const std::list<DecodedAudioFrame>& decoded_audio_frames) const {
+  IdLabeledFrameMap id_to_labeled_decoded_frame;
+  for (const auto& [audio_element_id, demixing_metadata] :
+       audio_element_id_to_demixing_metadata_) {
     // Process the decoded audio frames.
     LabeledFrame labeled_decoded_frame;
     RETURN_IF_NOT_OK(StoreSamplesForAudioElementId(
@@ -951,11 +964,11 @@
           std::move(labeled_decoded_frame);
     }
 
-    LogForAudioElementId(audio_element_id, id_to_labeled_frame,
+    LogForAudioElementId("Decoded", audio_element_id,
                          id_to_labeled_decoded_frame);
   }
 
-  return absl::OkStatus();
+  return id_to_labeled_decoded_frame;
 }
 
 absl::Status DemixingModule::GetDownMixers(
diff --git a/iamf/cli/demixing_module.h b/iamf/cli/demixing_module.h
index a34049a..aa9b1f5 100644
--- a/iamf/cli/demixing_module.h
+++ b/iamf/cli/demixing_module.h
@@ -174,20 +174,28 @@
       absl::flat_hash_map<uint32_t, SubstreamData>&
           substream_id_to_substream_data) const;
 
-  /*!\brief Demix audio samples.
+  /*!\brief Demix original audio samples.
+   *
+   * This is most useful when the original (before lossy codec) samples are
+   * known, such as when encoding original audio.
    *
    * \param audio_frames Audio Frames.
-   * \param decoded_audio_frames Decoded Audio Frames.
-   * \param id_to_labeled_frame Output data structure for samples.
-   * \param id_to_labeled_decoded_frame Output data structure for decoded
-   *        samples.
-   * \return `absl::OkStatus()` on success. A specific status on failure.
+   * \return Output data structure for samples, or a specific status on failure.
    */
-  absl::Status DemixAudioSamples(
-      const std::list<AudioFrameWithData>& audio_frames,
-      const std::list<DecodedAudioFrame>& decoded_audio_frames,
-      IdLabeledFrameMap& id_to_labeled_frame,
-      IdLabeledFrameMap& id_to_labeled_decoded_frame) const;
+  absl::StatusOr<IdLabeledFrameMap> DemixOriginalAudioSamples(
+      const std::list<AudioFrameWithData>& audio_frames) const;
+
+  /*!\brief Demix decoded audio samples.
+   *
+   * This is most useful when the decoded (after lossy codec) samples are
+   * known, such as when decoding an IA Sequence, or when analyzing the effect
+   * of a lossy codec to determine appropriate recon gain values.
+   *
+   * \param decoded_audio_frames Decoded Audio Frames.
+   * \return Output data structure for samples, or a specific status on failure.
+   */
+  absl::StatusOr<IdLabeledFrameMap> DemixDecodedAudioSamples(
+      const std::list<DecodedAudioFrame>& decoded_audio_frame) const;
 
   /*!\brief Gets the down-mixers associated with an Audio Element ID.
    *
@@ -208,20 +216,27 @@
                            const std::list<Demixer>*& demixers) const;
 
  private:
+  enum class DemixingMode { kDownMixingAndReconstruction, kReconstruction };
+
   /*!\brief Private constructor.
    *
    * For use with `CreateForDownMixingAndReconstruction` and
    * `CreateForReconstruction`.
    *
+   * \param demixing_mode Mode of the class.
    * \param audio_element_id_to_demixing_metadata Mapping from audio element ID
    *        to demixing metadata.
    */
   DemixingModule(
+      DemixingMode demixing_mode,
       absl::flat_hash_map<DecodedUleb128, DemixingMetadataForAudioElementId>&&
           audio_element_id_to_demixing_metadata)
-      : audio_element_id_to_demixing_metadata_(
+      : demixing_mode_(demixing_mode),
+        audio_element_id_to_demixing_metadata_(
             std::move(audio_element_id_to_demixing_metadata)) {}
 
+  DemixingMode demixing_mode_;
+
   const absl::flat_hash_map<DecodedUleb128, DemixingMetadataForAudioElementId>
       audio_element_id_to_demixing_metadata_;
 };
diff --git a/iamf/cli/iamf_encoder.cc b/iamf/cli/iamf_encoder.cc
index 339c322..01b0752 100644
--- a/iamf/cli/iamf_encoder.cc
+++ b/iamf/cli/iamf_encoder.cc
@@ -306,18 +306,24 @@
     decoded_audio_frames.emplace_back(*decoded_audio_frame);
   }
 
-  // Demix the audio frames.
-  IdLabeledFrameMap id_to_labeled_frame;
-  IdLabeledFrameMap id_to_labeled_decoded_frame;
-  RETURN_IF_NOT_OK(demixing_module_.DemixAudioSamples(
-      audio_frames, decoded_audio_frames, id_to_labeled_frame,
-      id_to_labeled_decoded_frame));
+  // Demix the original and decoded audio frames, differences between them are
+  // useful to compute the recon gain parameters.
+  const auto id_to_labeled_frame =
+      demixing_module_.DemixOriginalAudioSamples(audio_frames);
+  if (!id_to_labeled_frame.ok()) {
+    return id_to_labeled_frame.status();
+  }
+  const auto id_to_labeled_decoded_frame =
+      demixing_module_.DemixDecodedAudioSamples(decoded_audio_frames);
+  if (!id_to_labeled_decoded_frame.ok()) {
+    return id_to_labeled_decoded_frame.status();
+  }
 
   // Recon gain parameter blocks are generated based on the original and
   // demixed audio frames.
   RETURN_IF_NOT_OK(parameter_block_generator_.GenerateReconGain(
-      id_to_labeled_frame, id_to_labeled_decoded_frame, *global_timing_module_,
-      temp_recon_gain_parameter_blocks_));
+      *id_to_labeled_frame, *id_to_labeled_decoded_frame,
+      *global_timing_module_, temp_recon_gain_parameter_blocks_));
 
   // Move all generated parameter blocks belonging to this temporal unit to
   // the output.
@@ -335,7 +341,7 @@
   }
 
   return mix_presentation_finalizer_.PushTemporalUnit(
-      id_to_labeled_frame, output_start_timestamp, output_end_timestamp,
+      *id_to_labeled_frame, output_start_timestamp, output_end_timestamp,
       parameter_blocks);
 }
 
diff --git a/iamf/cli/obu_processor.cc b/iamf/cli/obu_processor.cc
index d600402..b639300 100644
--- a/iamf/cli/obu_processor.cc
+++ b/iamf/cli/obu_processor.cc
@@ -45,6 +45,7 @@
 #include "iamf/cli/sample_processor_base.h"
 #include "iamf/common/read_bit_buffer.h"
 #include "iamf/common/utils/macros.h"
+#include "iamf/common/utils/validation_utils.h"
 #include "iamf/obu/audio_element.h"
 #include "iamf/obu/audio_frame.h"
 #include "iamf/obu/codec_config.h"
@@ -204,9 +205,9 @@
   return absl::OkStatus();
 }
 
-// Returns an iterator to the first supported mix presentation in the list of
-// mix presentation OBUs or nullptr if none are supported.
-std::list<MixPresentationObu>::iterator GetFirstSupportedMixPresentation(
+// Returns a list of pointers to the supported mix presentations. Empty if none
+// are supported.
+std::list<MixPresentationObu*> GetSupportedMixPresentations(
     const absl::flat_hash_map<uint32_t, AudioElementWithData>& audio_elements,
     std::list<MixPresentationObu>& mix_presentation_obus) {
   // TODO(b/377554944): Support `ProfileVersion::kIamfBaseEnhancedProfile`.
@@ -214,6 +215,7 @@
   const absl::flat_hash_set<ProfileVersion> kSupportedProfiles = {
       ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile};
 
+  std::list<MixPresentationObu*> supported_mix_presentations;
   std::string cumulative_error_message;
   for (auto iter = mix_presentation_obus.begin();
        iter != mix_presentation_obus.end(); ++iter) {
@@ -221,18 +223,51 @@
     const auto status = ProfileFilter::FilterProfilesForMixPresentation(
         audio_elements, *iter, profiles);
     if (status.ok()) {
-      return iter;
+      supported_mix_presentations.push_back(&*iter);
     }
     absl::StrAppend(&cumulative_error_message, status.message(), "\n");
   }
-  LOG(ERROR) << absl::StrCat(
-      "No supported mix presentation presentation found in the bitstream.",
-      cumulative_error_message);
-  return mix_presentation_obus.end();
+  LOG(INFO) << "Filtered mix presentations: " << cumulative_error_message;
+  return supported_mix_presentations;
 }
 
-// Resets the buffer to `start_position` and sets the `insufficient_data` flag
-// to `true`. Clears the output maps.
+// Searches for the desired layout in the supported mix presentations. If found,
+// the output_playback_layout is the same as the desired_layout. Otherwise, we
+// default to the first layout in the first unsupported mix presentation.
+absl::StatusOr<MixPresentationObu*> GetPlaybackLayoutAndMixPresentation(
+    const std::list<MixPresentationObu*>& supported_mix_presentations,
+    const Layout& desired_layout, Layout& output_playback_layout) {
+  for (const auto& mix_presentation : supported_mix_presentations) {
+    for (const auto& sub_mix : mix_presentation->sub_mixes_) {
+      for (const auto& layout : sub_mix.layouts) {
+        if (layout.loudness_layout == desired_layout) {
+          output_playback_layout = layout.loudness_layout;
+          return mix_presentation;
+        }
+      }
+    }
+  }
+  // If we get here, we didn't find the desired layout in any of the supported
+  // mix presentations. We default to the first layout in the first mix
+  // presentation.
+  MixPresentationObu* output_mix_presentation =
+      supported_mix_presentations.front();
+  if (output_mix_presentation->sub_mixes_.empty()) {
+    return absl::InvalidArgumentError(
+        "No submixes found in the first mix presentation.");
+  }
+  if (output_mix_presentation->sub_mixes_.front().layouts.empty()) {
+    return absl::InvalidArgumentError(
+        "No layouts found in the first submix of the first mix presentation.");
+  }
+  output_playback_layout = output_mix_presentation->sub_mixes_.front()
+                               .layouts.front()
+                               .loudness_layout;
+  return output_mix_presentation;
+}
+
+// Resets the buffer to `start_position` and sets the `insufficient_data`
+// flag to `true`. Clears the output maps.
 absl::Status InsufficientDataReset(
     ReadBitBuffer& read_bit_buffer, const int64_t start_position,
     bool& insufficient_data,
@@ -253,20 +288,36 @@
       "more data and try again.");
 }
 
+void GetSampleRateAndFrameSize(
+    const absl::flat_hash_map<DecodedUleb128, CodecConfigObu>&
+        output_codec_config_obus,
+    std::optional<uint32_t>& output_sample_rate,
+    std::optional<uint32_t>& output_frame_size) {
+  if (output_codec_config_obus.size() != 1) {
+    LOG(WARNING) << "Expected exactly one codec config OBUs, but found "
+                 << output_codec_config_obus.size();
+    return;
+  }
+  const auto& first_codec_config_obu = output_codec_config_obus.begin()->second;
+  output_sample_rate = first_codec_config_obu.GetOutputSampleRate();
+  output_frame_size = first_codec_config_obu.GetNumSamplesPerFrame();
+}
+
 }  // namespace
 
 absl::Status ObuProcessor::InitializeInternal(bool is_exhaustive_and_exact,
-                                              bool& insufficient_data) {
+                                              bool& output_insufficient_data) {
   // Process the descriptor OBUs.
   LOG(INFO) << "Starting Descriptor OBU processing";
   RETURN_IF_NOT_OK(ObuProcessor::ProcessDescriptorObus(
       is_exhaustive_and_exact, *read_bit_buffer_, ia_sequence_header_,
       codec_config_obus_, audio_elements_, mix_presentations_,
-      insufficient_data));
+      output_insufficient_data));
   LOG(INFO) << "Processed Descriptor OBUs";
   RETURN_IF_NOT_OK(CollectAndValidateParamDefinitions(
       audio_elements_, mix_presentations_, param_definition_variants_));
-
+  GetSampleRateAndFrameSize(codec_config_obus_, output_sample_rate_,
+                            output_frame_size_);
   // Mapping from substream IDs to pointers to audio element with data.
   for (const auto& [audio_element_id, audio_element_with_data] :
        audio_elements_) {
@@ -300,7 +351,10 @@
     absl::flat_hash_map<DecodedUleb128, AudioElementWithData>&
         output_audio_elements_with_data,
     std::list<MixPresentationObu>& output_mix_presentation_obus,
-    bool& insufficient_data) {
+    bool& output_insufficient_data) {
+  // `output_insufficient_data` indicates a specific error condition and so is
+  // true iff we've received valid data but need more of it.
+  output_insufficient_data = false;
   auto audio_element_obu_map =
       absl::flat_hash_map<DecodedUleb128, AudioElementObu>();
   const int64_t global_position_before_all_obus = read_bit_buffer.Tell();
@@ -314,9 +368,9 @@
           absl::StatusCode::kResourceExhausted) {
         // Can't read header because there is not enough data.
         return InsufficientDataReset(
-            read_bit_buffer, global_position_before_all_obus, insufficient_data,
-            output_codec_config_obus, output_audio_elements_with_data,
-            output_mix_presentation_obus);
+            read_bit_buffer, global_position_before_all_obus,
+            output_insufficient_data, output_codec_config_obus,
+            output_audio_elements_with_data, output_mix_presentation_obus);
       } else {
         // Some other error occurred, propagate it.
         return header_metadata.status();
@@ -342,8 +396,7 @@
       if (!processed_ia_header) {
         return absl::InvalidArgumentError(
             "An IA Sequence and/or descriptor OBUs must always start with an "
-            "IA "
-            "Header.");
+            "IA Header.");
       }
       // Break out of the while loop since we've reached the end of the
       // descriptor OBUs; should not seek back to the beginning of the buffer
@@ -355,9 +408,9 @@
     if (!read_bit_buffer.CanReadBytes(header_metadata->total_obu_size)) {
       // This is a descriptor OBU for which we don't have enough data.
       return InsufficientDataReset(
-          read_bit_buffer, global_position_before_all_obus, insufficient_data,
-          output_codec_config_obus, output_audio_elements_with_data,
-          output_mix_presentation_obus);
+          read_bit_buffer, global_position_before_all_obus,
+          output_insufficient_data, output_codec_config_obus,
+          output_audio_elements_with_data, output_mix_presentation_obus);
     }
     // Now we know we can read the entire obu.
     const int64_t position_before_header = read_bit_buffer.Tell();
@@ -455,7 +508,7 @@
         audio_elements_with_data,
     const absl::flat_hash_map<DecodedUleb128, CodecConfigObu>&
         codec_config_obus,
-    const absl::flat_hash_map<DecodedUleb128, const AudioElementWithData*>
+    const absl::flat_hash_map<DecodedUleb128, const AudioElementWithData*>&
         substream_id_to_audio_element,
     const absl::flat_hash_map<DecodedUleb128, ParamDefinitionVariant>&
         param_definition_variants,
@@ -465,16 +518,32 @@
     std::optional<ParameterBlockWithData>& output_parameter_block_with_data,
     std::optional<TemporalDelimiterObu>& output_temporal_delimiter,
     bool& continue_processing) {
-  if (!read_bit_buffer.IsDataAvailable()) {
-    continue_processing = false;
-    return absl::OkStatus();
-  }
-
   continue_processing = true;
   output_audio_frame_with_data.reset();
   output_parameter_block_with_data.reset();
   output_temporal_delimiter.reset();
 
+  auto header_metadata = ObuHeader::PeekObuTypeAndTotalObuSize(read_bit_buffer);
+  if (!header_metadata.ok()) {
+    if (header_metadata.status().code() ==
+        absl::StatusCode::kResourceExhausted) {
+      // Can't read header because there is not enough data. This is not an
+      // error, but we're done processing for now.
+      continue_processing = false;
+      return absl::OkStatus();
+    } else {
+      // Some other error occurred, propagate it.
+      return header_metadata.status();
+    }
+  }
+
+  if (!read_bit_buffer.CanReadBytes(header_metadata->total_obu_size)) {
+    // This is a temporal unit OBU for which we don't have enough data. This is
+    // not an error, but we're done processing for now.
+    continue_processing = false;
+    return absl::OkStatus();
+  }
+
   const int64_t position_before_header = read_bit_buffer.Tell();
 
   // Read in the header and determines the size of the payload in bytes.
@@ -571,14 +640,17 @@
 
 std::unique_ptr<ObuProcessor> ObuProcessor::Create(
     bool is_exhaustive_and_exact, ReadBitBuffer* read_bit_buffer,
-    bool& insufficient_data) {
+    bool& output_insufficient_data) {
+  // `output_insufficient_data` indicates a specific error condition and so is
+  // true iff we've received valid data but need more of it.
+  output_insufficient_data = false;
   if (read_bit_buffer == nullptr) {
     return nullptr;
   }
   std::unique_ptr<ObuProcessor> obu_processor =
       absl::WrapUnique(new ObuProcessor(read_bit_buffer));
   if (const auto status = obu_processor->InitializeInternal(
-          is_exhaustive_and_exact, insufficient_data);
+          is_exhaustive_and_exact, output_insufficient_data);
       !status.ok()) {
     LOG(ERROR) << status;
     return nullptr;
@@ -587,25 +659,28 @@
 }
 
 std::unique_ptr<ObuProcessor> ObuProcessor::CreateForRendering(
-    const Layout& playback_layout,
+    const Layout& desired_layout,
     const RenderingMixPresentationFinalizer::SampleProcessorFactory&
         sample_processor_factory,
     bool is_exhaustive_and_exact, ReadBitBuffer* read_bit_buffer,
-    bool& insufficient_data) {
+    Layout& output_layout, bool& output_insufficient_data) {
+  // `output_insufficient_data` indicates a specific error condition and so is
+  // true iff we've received valid data but need more of it.
+  output_insufficient_data = false;
   if (read_bit_buffer == nullptr) {
     return nullptr;
   }
   std::unique_ptr<ObuProcessor> obu_processor =
       absl::WrapUnique(new ObuProcessor(read_bit_buffer));
   if (const auto status = obu_processor->InitializeInternal(
-          is_exhaustive_and_exact, insufficient_data);
+          is_exhaustive_and_exact, output_insufficient_data);
       !status.ok()) {
     LOG(ERROR) << status;
     return nullptr;
   }
 
   if (const auto status = obu_processor->InitializeForRendering(
-          playback_layout, sample_processor_factory);
+          desired_layout, sample_processor_factory, output_layout);
       !status.ok()) {
     LOG(ERROR) << status;
     return nullptr;
@@ -613,19 +688,25 @@
   return obu_processor;
 }
 
+absl::StatusOr<uint32_t> ObuProcessor::GetOutputSampleRate() const {
+  RETURN_IF_NOT_OK(
+      ValidateHasValue(output_sample_rate_,
+                       "Output sample rate, was this a trivial IA Sequence?"));
+  return *output_sample_rate_;
+}
+
+absl::StatusOr<uint32_t> ObuProcessor::GetOutputFrameSize() const {
+  RETURN_IF_NOT_OK(
+      ValidateHasValue(output_frame_size_,
+                       "Output frame size, was this a trivial IA Sequence?"));
+  return *output_frame_size_;
+}
+
 absl::Status ObuProcessor::InitializeForRendering(
-    const Layout& playback_layout,
+    const Layout& desired_layout,
     const RenderingMixPresentationFinalizer::SampleProcessorFactory&
-        sample_processor_factory) {
-  // TODO(b/339500539): Add support for other layouts. Downstream code is simple
-  //                    and assumes there will be a matching layout in the first
-  //                    Mix Presentation OBU. The IAMF spec REQUIRES this for
-  //                    stereo. In general, layouts may require more careful
-  //                    selection according to 7.3.1.
-  // TODO(b/395625514): Add test coverage for this.
-  if (!IsStereoLayout(playback_layout)) {
-    return absl::InvalidArgumentError("Layout type is not supported.");
-  }
+        sample_processor_factory,
+    Layout& output_layout) {
   if (mix_presentations_.empty()) {
     return absl::InvalidArgumentError("No mix presentation OBUs found.");
   }
@@ -652,32 +733,39 @@
 
   // TODO(b/340289717): Add a way to select the mix presentation if multiple
   //                    are supported.
-  const auto mix_presentation_to_render =
-      GetFirstSupportedMixPresentation(audio_elements_, mix_presentations_);
-  if (mix_presentation_to_render == mix_presentations_.end()) {
-    return absl::NotFoundError("No supportedmix presentation OBUs found.");
+  const std::list<MixPresentationObu*> supported_mix_presentations =
+      GetSupportedMixPresentations(audio_elements_, mix_presentations_);
+  if (supported_mix_presentations.empty()) {
+    return absl::NotFoundError("No supported mix presentation OBUs found.");
   }
-  int desired_sub_mix_index;
-  int desired_layout_index;
-  RETURN_IF_NOT_OK(GetIndicesForLayout(mix_presentation_to_render->sub_mixes_,
-                                       playback_layout, desired_sub_mix_index,
-                                       desired_layout_index));
+  Layout playback_layout;
+  auto mix_presentation_to_render = GetPlaybackLayoutAndMixPresentation(
+      supported_mix_presentations, desired_layout, output_layout);
+  if (!mix_presentation_to_render.ok()) {
+    return mix_presentation_to_render.status();
+  }
+  int playback_sub_mix_index;
+  int playback_layout_index;
+  RETURN_IF_NOT_OK(GetIndicesForLayout(
+      (*mix_presentation_to_render)->sub_mixes_, output_layout,
+      playback_sub_mix_index, playback_layout_index));
   decoding_layout_info_ = {
-      .mix_presentation_id = mix_presentation_to_render->GetMixPresentationId(),
-      .sub_mix_index = desired_sub_mix_index,
-      .layout_index = desired_layout_index,
+      .mix_presentation_id =
+          (*mix_presentation_to_render)->GetMixPresentationId(),
+      .sub_mix_index = playback_sub_mix_index,
+      .layout_index = playback_layout_index,
   };
   auto forward_on_desired_layout =
       [&sample_processor_factory, mix_presentation_to_render,
-       desired_sub_mix_index, desired_layout_index](
+       playback_sub_mix_index, playback_layout_index](
           DecodedUleb128 mix_presentation_id, int sub_mix_index,
           int layout_index, const Layout& layout, int num_channels,
           int sample_rate, int bit_depth, size_t max_input_samples_per_frame)
       -> std::unique_ptr<SampleProcessorBase> {
     if (mix_presentation_id ==
-            mix_presentation_to_render->GetMixPresentationId() &&
-        desired_sub_mix_index == sub_mix_index &&
-        desired_layout_index == layout_index) {
+            (*mix_presentation_to_render)->GetMixPresentationId() &&
+        playback_sub_mix_index == sub_mix_index &&
+        playback_layout_index == layout_index) {
       return sample_processor_factory(
           mix_presentation_id, sub_mix_index, layout_index, layout,
           num_channels, sample_rate, bit_depth, max_input_samples_per_frame);
@@ -732,11 +820,11 @@
 }
 
 absl::Status ObuProcessor::ProcessTemporalUnit(
-    std::list<AudioFrameWithData>& output_audio_frames,
-    std::list<ParameterBlockWithData>& output_parameter_blocks,
-    std::optional<InternalTimestamp>& output_timestamp,
+    bool eos_is_end_of_sequence,
+    std::optional<OutputTemporalUnit>& output_temporal_unit,
     bool& continue_processing) {
-  while (true) {
+  continue_processing = true;
+  while (continue_processing) {
     std::optional<AudioFrameWithData> audio_frame_with_data;
     std::optional<ParameterBlockWithData> parameter_block_with_data;
     std::optional<TemporalDelimiterObu> temporal_delimiter;
@@ -759,17 +847,22 @@
 
     // The current temporal unit is considered finished if any of the
     // following conditions is met:
-    // - No more data to consume (i.e. `continue_processing == true`).
+    // - The end of sequence is reached.
     // - The timestamp has advanced (i.e. when the next temporal unit gets its
     //   timestamp).
     // - A temporal delimiter is encountered.
-    if (!continue_processing || next_temporal_unit_.timestamp.has_value() ||
+    if ((!continue_processing && eos_is_end_of_sequence) ||
+        next_temporal_unit_.timestamp.has_value() ||
         current_temporal_unit_.temporal_delimiter.has_value()) {
-      output_audio_frames.splice(output_audio_frames.end(),
-                                 current_temporal_unit_.audio_frames);
-      output_parameter_blocks.splice(output_parameter_blocks.end(),
-                                     current_temporal_unit_.parameter_blocks);
-      output_timestamp = current_temporal_unit_.timestamp;
+      output_temporal_unit = OutputTemporalUnit();
+      output_temporal_unit->output_audio_frames =
+          std::move(current_temporal_unit_.audio_frames);
+      output_temporal_unit->output_parameter_blocks =
+          std::move(current_temporal_unit_.parameter_blocks);
+      if (current_temporal_unit_.timestamp.has_value()) {
+        output_temporal_unit->output_timestamp =
+            current_temporal_unit_.timestamp.value();
+      }
       current_temporal_unit_ = std::move(next_temporal_unit_);
       next_temporal_unit_ = TemporalUnitData();
       break;
@@ -806,8 +899,6 @@
   }
 
   // Decode the temporal unit.
-  const std::list<AudioFrameWithData> kIgnoreOriginalAudioFrames;
-  IdLabeledFrameMap unused_original_labeled_frames;
   std::optional<InternalTimestamp> end_timestamp;
 
   // This resizing should happen only once per IA sequence, since all the
@@ -835,18 +926,16 @@
   }
 
   // Reconstruct the temporal unit and store the result in the output map.
-  IdLabeledFrameMap decoded_labeled_frames_for_temporal_unit;
-  RETURN_IF_NOT_OK(demixing_module_->DemixAudioSamples(
-      kIgnoreOriginalAudioFrames, decoded_frames_for_temporal_unit_,
-      unused_original_labeled_frames,
-      decoded_labeled_frames_for_temporal_unit));
-
-  // To be safe clear the unused map. But we expect it to be empty.
-  unused_original_labeled_frames.clear();
+  const auto decoded_labeled_frames_for_temporal_unit =
+      demixing_module_->DemixDecodedAudioSamples(
+          decoded_frames_for_temporal_unit_);
+  if (!decoded_labeled_frames_for_temporal_unit.ok()) {
+    return decoded_labeled_frames_for_temporal_unit.status();
+  }
 
   RETURN_IF_NOT_OK(mix_presentation_finalizer_->PushTemporalUnit(
-      decoded_labeled_frames_for_temporal_unit, start_timestamp, *end_timestamp,
-      parameter_blocks));
+      *decoded_labeled_frames_for_temporal_unit, start_timestamp,
+      *end_timestamp, parameter_blocks));
 
   auto rendered_samples =
       mix_presentation_finalizer_->GetPostProcessedSamplesAsSpan(
diff --git a/iamf/cli/obu_processor.h b/iamf/cli/obu_processor.h
index 8c9e7d1..cc607e3 100644
--- a/iamf/cli/obu_processor.h
+++ b/iamf/cli/obu_processor.h
@@ -21,6 +21,7 @@
 
 #include "absl/container/flat_hash_map.h"
 #include "absl/status/status.h"
+#include "absl/status/statusor.h"
 #include "absl/types/span.h"
 #include "iamf/cli/audio_element_with_data.h"
 #include "iamf/cli/audio_frame_decoder.h"
@@ -70,6 +71,8 @@
    * \return `absl::OkStatus()` if the process is successful. A specific status
    *         on failure.
    */
+  [[deprecated(
+      "Remove when all tests are ported. Use the non-static version instead.")]]
   static absl::Status ProcessDescriptorObus(
       bool is_exhaustive_and_exact, ReadBitBuffer& read_bit_buffer,
       IASequenceHeaderObu& output_sequence_header,
@@ -113,7 +116,7 @@
           audio_elements_with_data,
       const absl::flat_hash_map<DecodedUleb128, CodecConfigObu>&
           codec_config_obus,
-      const absl::flat_hash_map<DecodedUleb128, const AudioElementWithData*>
+      const absl::flat_hash_map<DecodedUleb128, const AudioElementWithData*>&
           substream_id_to_audio_element,
       const absl::flat_hash_map<DecodedUleb128, ParamDefinitionVariant>&
           param_definition_variants,
@@ -134,13 +137,14 @@
    *        descriptor OBUs.
    * \param read_bit_buffer Pointer to the read bit buffer that reads the IAMF
    *        bitstream.
-   * \param insufficient_data Whether the bitstream provided is insufficient to
-   *        process all descriptor OBUs.
+   * \param output_insufficient_data True iff the bitstream provided is
+   *        insufficient to process all descriptor OBUs and there is no other
+   *        error.
    * \return std::unique_ptr<ObuProcessor> on success. `nullptr` on failure.
    */
   static std::unique_ptr<ObuProcessor> Create(bool is_exhaustive_and_exact,
                                               ReadBitBuffer* read_bit_buffer,
-                                              bool& insufficient_data);
+                                              bool& output_insufficient_data);
 
   /*!\brief Move constructor. */
   ObuProcessor(ObuProcessor&& obu_processor) = delete;
@@ -150,8 +154,10 @@
    * Creation succeeds only if the descriptor OBUs are successfully processed
    * and all rendering modules are successfully initialized.
    *
-   * \param playback_layout Specifies the layout that will be used to render the
-   *        audio.
+   * \param desired_layout Specifies the desired layout that will be used to
+   *        render the audio, if available in the mix presentations. If not
+   *        available, the first layout in the first mix presentation will be
+   *        used.
    * \param sample_processor_factory Factory to create post processors.
    * \param is_exhaustive_and_exact Whether the bitstream provided is meant to
    *        include all descriptor OBUs and no other data. This should only be
@@ -159,16 +165,37 @@
    *        descriptor OBUs.
    * \param read_bit_buffer Pointer to the read bit buffer that reads the IAMF
    *        bitstream.
-   * \param insufficient_data Whether the bitstream provided is insufficient to
-   *        process all descriptor OBUs.
+   * \param output_layout The layout that will be used to render the audio. This
+   *        is the same as `desired_layout` if it is available in the mix
+   *        presentations, otherwise a default layout is used.
+   * \param output_insufficient_data True iff the bitstream provided is
+   *        insufficient to process all descriptor OBUs and there is no other
+   *        error.
    * \return Pointer to an ObuProcessor on success. `nullptr` on failure.
    */
   static std::unique_ptr<ObuProcessor> CreateForRendering(
-      const Layout& playback_layout,
+      const Layout& desired_layout,
       const RenderingMixPresentationFinalizer::SampleProcessorFactory&
           sample_processor_factory,
       bool is_exhaustive_and_exact, ReadBitBuffer* read_bit_buffer,
-      bool& insufficient_data);
+      Layout& output_layout, bool& output_insufficient_data);
+
+  /*!\brief Gets the sample rate of the output audio.
+   *
+   * \return Sample rate of the output audio, or a specific error code on
+   *         failure.
+   */
+  absl::StatusOr<uint32_t> GetOutputSampleRate() const;
+
+  /*!\brief Gets the frame size of the output audio.
+   *
+   * Useful to determine the maximum number of samples per
+   * `RenderTemporalUnitAndMeasureLoudness` call.
+   *
+   * \return Number of samples in per frame of the output audio, or a specific
+   *         specific error code on failure.
+   */
+  absl::StatusOr<uint32_t> GetOutputFrameSize() const;
 
   // TODO(b/381072155): Consider removing this one in favor of
   //                    `ProcessTemporalUnit()`, which outputs all OBUs
@@ -192,26 +219,28 @@
       std::optional<TemporalDelimiterObu>& output_temporal_delimiter,
       bool& continue_processing);
 
+  struct OutputTemporalUnit {
+    std::list<AudioFrameWithData> output_audio_frames;
+    std::list<ParameterBlockWithData> output_parameter_blocks;
+    InternalTimestamp output_timestamp;
+  };
+
   // TODO(b/379819959): Also handle Temporal Delimiter OBUs.
   /*!\brief Processes all OBUs from a Temporal Unit from the stored IA Sequence.
    *
-   * `Initialize()` must be called first to ready the input bitstream.
-   *
-   * \param output_audio_frames Output Audio Frames with the requisite
-   *        data.
-   * \param output_parameter_blocks Output Parameter Blocks with the
-   *        requisite data.
-   * \param output_timestamp Timestamp for the output temporal unit.
-   * \param insufficient_data Whether the bitstream provided is insufficient to
-   *        process all descriptor OBUs.
+   * \param eos_is_end_of_sequence Whether reaching the end of the stream
+   *        should be considered as the end of the sequence, and therefore the
+   *        end of the temporal unit.
+   * \param output_temporal_unit Contains the data from the temporal unit that
+   *        is processed.
    * \param continue_processing Whether the processing should be continued.
    * \return `absl::OkStatus()` if the process is successful. A specific status
    *         on failure.
    */
   absl::Status ProcessTemporalUnit(
-      std::list<AudioFrameWithData>& output_audio_frames,
-      std::list<ParameterBlockWithData>& output_parameter_blocks,
-      std::optional<int32_t>& output_timestamp, bool& continue_processing);
+      bool eos_is_end_of_sequence,
+      std::optional<OutputTemporalUnit>& output_temporal_unit,
+      bool& continue_processing);
 
   /*!\brief Renders a temporal unit and measures loudness.
    *
@@ -258,26 +287,33 @@
    *        include all descriptor OBUs and no other data. This should only be
    *        set to true if the user knows the exact boundaries of their set of
    *        descriptor OBUs.
+    \param output_insufficient_data True iff the bitstream provided is
+   *        insufficient to process all descriptor OBUs and there is no other
+   *        error.
    * \return `absl::OkStatus()` if initialization is successful. A specific
    *        status on failure.
    */
   absl::Status InitializeInternal(bool is_exhaustive_and_exact,
-                                  bool& insufficient_data);
+                                  bool& output_insufficient_data);
 
   /*!\brief Initializes the OBU processor for rendering.
    *
    * Must be called after `Initialize()` is called.
    *
-   * \param playback_layout Specifies the layout that will be used to render the
-   *        audio.
+   * \param desired_layout Specifies the layout that will be used to render the
+   *        audio, if available.
    * \param sample_processor_factory Factory to create post processors.
+   * \param output_layout The layout that will be used to render the audio. This
+   *        is the same as `desired_layout` if it is available, otherwise a
+   *        default layout is used.
    * \return `absl::OkStatus()` if the process is successful. A specific status
    *         on failure.
    */
   absl::Status InitializeForRendering(
-      const Layout& playback_layout,
+      const Layout& desired_layout,
       const RenderingMixPresentationFinalizer::SampleProcessorFactory&
-          sample_processor_factory);
+          sample_processor_factory,
+      Layout& output_layout);
 
   struct DecodingLayoutInfo {
     DecodedUleb128 mix_presentation_id;
@@ -312,9 +348,11 @@
         current_temporal_unit.timestamp = new_timestamp;
       }
       if (*current_temporal_unit.timestamp == new_timestamp) {
-        current_temporal_unit.GetList<T>().push_back(std::move(obu_with_data));
+        current_temporal_unit.GetList<T>().push_back(
+            std::forward<T>(obu_with_data));
       } else {
-        next_temporal_unit.GetList<T>().push_back(std::move(obu_with_data));
+        next_temporal_unit.GetList<T>().push_back(
+            std::forward<T>(obu_with_data));
         next_temporal_unit.timestamp = new_timestamp;
       }
     }
@@ -330,6 +368,9 @@
     };
   };
 
+  std::optional<uint32_t> output_sample_rate_;
+  std::optional<uint32_t> output_frame_size_;
+
   absl::flat_hash_map<DecodedUleb128, ParamDefinitionVariant>
       param_definition_variants_;
   absl::flat_hash_map<DecodedUleb128, const AudioElementWithData*>
diff --git a/iamf/cli/obu_sequencer_base.cc b/iamf/cli/obu_sequencer_base.cc
index f8ec893..038019c 100644
--- a/iamf/cli/obu_sequencer_base.cc
+++ b/iamf/cli/obu_sequencer_base.cc
@@ -62,6 +62,42 @@
  */
 typedef absl::btree_map<int32_t, TemporalUnitView> TemporalUnitMap;
 
+/*!\brief Helper class to abort an `ObuSequencerBase` on destruction.
+ *
+ * This class calls on an `ObuSequencerBase::Abort` on destruction. Or does
+ * nothing if `CancelAbort` is called.
+ *
+ * Typically, this is useful to create an instance of this class on the stack,
+ * in the scope of a function which has many locations where it may return an
+ * un-recoverable error. When those exit points are reached, the sequencer will
+ * automatically be aborted.
+ *
+ * Before any successful exit point, `CancelAbort` should be called, which will
+ * prevent the sequencer from being aborting.
+ */
+class AbortOnDestruct {
+ public:
+  /*!\brief Constructor
+   *
+   * \param obu_sequencer The `ObuSequencerBase` to abort on destruction.
+   */
+  explicit AbortOnDestruct(ObuSequencerBase* obu_sequencer)
+      : obu_sequencer(obu_sequencer) {}
+
+  /*!\brief Destructor */
+  ~AbortOnDestruct() {
+    if (obu_sequencer != nullptr) {
+      obu_sequencer->Abort();
+    }
+  }
+
+  /*!\brief Cancels the abort on destruction. */
+  void CancelAbort() { obu_sequencer = nullptr; }
+
+ private:
+  ObuSequencerBase* obu_sequencer;
+};
+
 template <typename KeyValueMap, typename KeyComparator>
 std::vector<uint32_t> SortedKeys(const KeyValueMap& map,
                                  const KeyComparator& comparator) {
@@ -77,6 +113,8 @@
 // frames. These would decode to an empty stream. Fallback to some reasonable,
 // but arbitrary default values, when the true value is undefined.
 
+// Fallback number of samples per frame when there are no audio frames.
+constexpr uint32_t kFallbackSamplesPerFrame = 1024;
 // Fallback sample rate when there are no Codec Config OBUs.
 constexpr uint32_t kFallbackSampleRate = 48000;
 // Fallback bit-depth when there are no Codec Config OBUs.
@@ -106,38 +144,6 @@
   return num_channels;
 }
 
-// Gets the first Presentation Timestamp (PTS); the timestamp of the first
-// sample that is not trimmed. Or zero of there are no untrimmed samples.
-absl::StatusOr<int64_t> GetFirstUntrimmedTimestamp(
-    const TemporalUnitMap& temporal_unit_map) {
-  if (temporal_unit_map.empty()) {
-    return kFallbackFirstPts;
-  }
-
-  std::optional<int64_t> first_untrimmed_timestamp;
-  for (const auto& [start_timestamp, temporal_unit] : temporal_unit_map) {
-    if (temporal_unit.num_untrimmed_samples_ == 0) {
-      // Fully trimmed frame. Wait for more.
-      continue;
-    }
-    if (temporal_unit.num_samples_to_trim_at_start_ > 0 &&
-        first_untrimmed_timestamp.has_value()) {
-      return absl::InvalidArgumentError(
-          "Temporal units must not have samples trimmed from the start, after "
-          "the first untrimmed sample.");
-    }
-
-    // Found the first untrimmed sample. Get the timestamp. We only continue
-    // looping to check that no more temporal units have samples trimmed from
-    // the start, after the first untrimmed sample.
-    first_untrimmed_timestamp =
-        start_timestamp + temporal_unit.num_samples_to_trim_at_start_;
-  }
-
-  return first_untrimmed_timestamp.has_value() ? *first_untrimmed_timestamp
-                                               : kFallbackFirstPts;
-}
-
 // Gets the common sample rate and bit depth for the given codec config OBUs. Or
 // falls back to default values if there are no codec configs.
 absl::Status GetCommonSampleRateAndBitDepth(
@@ -177,6 +183,33 @@
   return absl::OkStatus();
 }
 
+absl::Status FillDescriptorStatistics(
+    const absl::flat_hash_map<uint32_t, CodecConfigObu>& codec_config_obus,
+    auto& descriptor_statistics) {
+  descriptor_statistics.common_samples_per_frame = kFallbackSamplesPerFrame;
+  descriptor_statistics.common_sample_rate = kFallbackSampleRate;
+  descriptor_statistics.common_bit_depth = kFallbackBitDepth;
+  descriptor_statistics.num_channels = kFallbackNumChannels;
+
+  bool requires_resampling = false;
+  RETURN_IF_NOT_OK(GetCommonSampleRateAndBitDepth(
+      codec_config_obus, descriptor_statistics.common_sample_rate,
+      descriptor_statistics.common_bit_depth, requires_resampling));
+  if (requires_resampling) {
+    return absl::UnimplementedError(
+        "Codec Config OBUs with different bit-depths and/or sample "
+        "rates are not in base-enhanced/base/simple profile; they are not "
+        "allowed in ISOBMFF.");
+  }
+
+  // This assumes all Codec Configs have the same sample rate and frame size.
+  // We may need to be more careful if IA Samples do not all (except the
+  // final) have the same duration in the future.
+  return GetCommonSamplesPerFrame(
+      codec_config_obus, descriptor_statistics.common_samples_per_frame);
+}
+
+[[deprecated("Remove this, and related types when `PickAndPlace` is removed.")]]
 absl::Status GenerateTemporalUnitMap(
     const std::list<AudioFrameWithData>& audio_frames,
     const std::list<ParameterBlockWithData>& parameter_blocks,
@@ -223,6 +256,7 @@
 
   return absl::OkStatus();
 }
+
 }  // namespace
 
 absl::Status ObuSequencerBase::WriteTemporalUnit(
@@ -350,9 +384,131 @@
     : leb_generator_(leb_generator),
       delay_descriptors_until_first_untrimmed_sample_(
           delay_descriptors_until_first_untrimmed_sample),
-      include_temporal_delimiters_(include_temporal_delimiters) {}
+      include_temporal_delimiters_(include_temporal_delimiters),
+      wb_(kBufferStartSize, leb_generator) {}
 
-ObuSequencerBase::~ObuSequencerBase() {};
+ObuSequencerBase::~ObuSequencerBase() {
+  switch (state_) {
+    case kInitialized:
+      return;
+    case kPushDescriptorObusCalled:
+    case kPushSerializedDescriptorsCalled:
+      LOG(ERROR) << "OBUs have been pushed, but `ObuSequencerBase` is being "
+                    "destroyed without calling `Close` or `Abort`.";
+      return;
+    case kClosed:
+      return;
+  }
+  // The above switch is exhaustive.
+  LOG(FATAL) << "Unexpected state: " << static_cast<int>(state_);
+};
+
+absl::Status ObuSequencerBase::PushDescriptorObus(
+    const IASequenceHeaderObu& ia_sequence_header_obu,
+    const absl::flat_hash_map<uint32_t, CodecConfigObu>& codec_config_obus,
+    const absl::flat_hash_map<uint32_t, AudioElementWithData>& audio_elements,
+    const std::list<MixPresentationObu>& mix_presentation_obus,
+    const std::list<ArbitraryObu>& arbitrary_obus) {
+  // Many failure points should call `Abort`. We want to avoid leaving
+  // sequencers open if they may have invalid or corrupted IAMF data.
+  AbortOnDestruct abort_on_destruct(this);
+  switch (state_) {
+    case kInitialized:
+      break;
+    case kPushDescriptorObusCalled:
+    case kPushSerializedDescriptorsCalled:
+      return absl::FailedPreconditionError(
+          "`PushDescriptorObus` can only be called once.");
+    case kClosed:
+      return absl::FailedPreconditionError(
+          "`PushDescriptorObus` cannot be called after `Close` or `Abort`.");
+  }
+  state_ = kPushDescriptorObusCalled;
+  wb_.Reset();
+
+  // Serialize descriptor OBUS and adjacent arbitrary OBUs.
+  RETURN_IF_NOT_OK(ArbitraryObu::WriteObusWithHook(
+      ArbitraryObu::kInsertionHookBeforeDescriptors, arbitrary_obus, wb_));
+  // Write out the descriptor OBUs.
+  RETURN_IF_NOT_OK(ObuSequencerBase::WriteDescriptorObus(
+      ia_sequence_header_obu, codec_config_obus, audio_elements,
+      mix_presentation_obus, arbitrary_obus, wb_));
+  RETURN_IF_NOT_OK(ArbitraryObu::WriteObusWithHook(
+      ArbitraryObu::kInsertionHookAfterDescriptors, arbitrary_obus, wb_));
+  // Cache the descriptor OBUs, so we can validate "functional" equivalence if
+  // the user calls `UpdateDescriptorObusAndClose`.
+  DescriptorStatistics descriptor_statistics{.descriptor_obus =
+                                                 wb_.bit_buffer()};
+  RETURN_IF_NOT_OK(
+      FillDescriptorStatistics(codec_config_obus, descriptor_statistics));
+  descriptor_statistics_.emplace(std::move(descriptor_statistics));
+
+  if (!delay_descriptors_until_first_untrimmed_sample_) {
+    // Avoid unnecessary delay, for concrete classes that don't need
+    // `first_pts`.
+    RETURN_IF_NOT_OK(PushSerializedDescriptorObus(
+        descriptor_statistics_->common_samples_per_frame,
+        descriptor_statistics_->common_sample_rate,
+        descriptor_statistics_->common_bit_depth,
+        descriptor_statistics_->first_untrimmed_timestamp,
+        descriptor_statistics_->num_channels,
+        descriptor_statistics_->descriptor_obus));
+
+    state_ = kPushSerializedDescriptorsCalled;
+  }
+
+  abort_on_destruct.CancelAbort();
+  return absl::OkStatus();
+}
+
+absl::Status ObuSequencerBase::PushTemporalUnit(
+    const TemporalUnitView& temporal_unit) {
+  // Many failure points should call `Abort`. We want to avoid leaving
+  // sequencers open if they may have invalid or corrupted IAMF data.
+  AbortOnDestruct abort_on_destruct(this);
+  switch (state_) {
+    case kInitialized:
+      return absl::FailedPreconditionError(
+          "PushDescriptorObus must be called before PushTemporalUnit.");
+      break;
+    case kPushDescriptorObusCalled:
+    case kPushSerializedDescriptorsCalled:
+      break;
+    case kClosed:
+      return absl::FailedPreconditionError(
+          "PushTemporalUnit can only be called before `Close` or `Abort`.");
+  }
+  wb_.Reset();
+
+  // Cache the frame for later
+  const int64_t start_timestamp =
+      static_cast<int64_t>(temporal_unit.start_timestamp_);
+  int num_samples = 0;
+  RETURN_IF_NOT_OK(WriteTemporalUnit(include_temporal_delimiters_,
+                                     temporal_unit, wb_, num_samples));
+  cumulative_num_samples_for_logging_ += num_samples;
+  num_temporal_units_for_logging_++;
+
+  if (!descriptor_statistics_->first_untrimmed_timestamp.has_value()) {
+    // Treat the initial temporal units as a special case, this helps gather
+    // statistics about the first untrimmed sample.
+    RETURN_IF_NOT_OK(HandleInitialTemporalUnits(
+        temporal_unit, absl::MakeConstSpan(wb_.bit_buffer())));
+
+  } else if (temporal_unit.num_samples_to_trim_at_start_ > 0) {
+    return absl::InvalidArgumentError(
+        "A unit has samples to trim at start, but the first untrimmed sample "
+        "was already found.");
+  } else [[likely]] {
+    // This is by far the most common case, after we have seen the first real
+    // frame of audio, we can handle this simply.
+    RETURN_IF_NOT_OK(PushSerializedTemporalUnit(
+        start_timestamp, num_samples, absl::MakeConstSpan(wb_.bit_buffer())));
+  }
+
+  abort_on_destruct.CancelAbort();
+  return absl::OkStatus();
+}
 
 absl::Status ObuSequencerBase::PickAndPlace(
     const IASequenceHeaderObu& ia_sequence_header_obu,
@@ -362,106 +518,172 @@
     const std::list<AudioFrameWithData>& audio_frames,
     const std::list<ParameterBlockWithData>& parameter_blocks,
     const std::list<ArbitraryObu>& arbitrary_obus) {
-  switch (state_) {
-    case kInitialized:
-      break;
-    case kFlushed:
-      return absl::FailedPreconditionError(
-          "`PickAndPlace` should only be called once per instance.");
-  }
-
-  uint32_t common_sample_rate;
-  uint8_t common_bit_depth;
-  bool requires_resampling;
-  RETURN_IF_NOT_OK(
-      GetCommonSampleRateAndBitDepth(codec_config_obus, common_sample_rate,
-                                     common_bit_depth, requires_resampling));
-  if (requires_resampling) {
-    return absl::UnimplementedError(
-        "Codec Config OBUs with different bit-depths and/or sample "
-        "rates are not in base-enhanced/base/simple profile; they are not "
-        "allowed in ISOBMFF.");
-  }
-
-  // This assumes all Codec Configs have the same sample rate and frame size.
-  // We may need to be more careful if IA Samples do not all (except the
-  // final) have the same duration in the future.
-  uint32_t common_samples_per_frame = 0;
-  RETURN_IF_NOT_OK(
-      GetCommonSamplesPerFrame(codec_config_obus, common_samples_per_frame));
-
-  // Write the descriptor OBUs.
-  WriteBitBuffer wb(kBufferStartSize, leb_generator_);
-
-  RETURN_IF_NOT_OK(ArbitraryObu::WriteObusWithHook(
-      ArbitraryObu::kInsertionHookBeforeDescriptors, arbitrary_obus, wb));
-  // Write out the descriptor OBUs.
-  RETURN_IF_NOT_OK(ObuSequencerBase::WriteDescriptorObus(
-      ia_sequence_header_obu, codec_config_obus, audio_elements,
-      mix_presentation_obus, arbitrary_obus, wb));
-  RETURN_IF_NOT_OK(ArbitraryObu::WriteObusWithHook(
-      ArbitraryObu::kInsertionHookAfterDescriptors, arbitrary_obus, wb));
+  RETURN_IF_NOT_OK(PushDescriptorObus(ia_sequence_header_obu, codec_config_obus,
+                                      audio_elements, mix_presentation_obus,
+                                      arbitrary_obus));
 
   TemporalUnitMap temporal_unit_map;
   RETURN_IF_NOT_OK(GenerateTemporalUnitMap(audio_frames, parameter_blocks,
                                            arbitrary_obus, temporal_unit_map));
+  for (const auto& [timestamp, temporal_unit] : temporal_unit_map) {
+    RETURN_IF_NOT_OK(PushTemporalUnit(temporal_unit));
+  }
+  return Close();
+}
 
-  // If `delay_descriptors_until_first_untrimmed_sample` is true, then concrete
-  // class needs `first_untrimmed_timestamp`. Otherwise, it would cause an
-  // unnecessary delay, because the PTS cannot be determined until the first
-  // untrimmed sample is received.
-  std::optional<int64_t> first_untrimmed_timestamp;
-  if (delay_descriptors_until_first_untrimmed_sample_) {
-    // TODO(b/397637224): When this class can be used iteratively, we need to
-    //                    determine the first PTS from the initial audio frames
-    //                    only.
-    const auto temp_first_untrimmed_timestamp =
-        GetFirstUntrimmedTimestamp(temporal_unit_map);
-    if (!temp_first_untrimmed_timestamp.ok()) {
-      return temp_first_untrimmed_timestamp.status();
-    }
-    first_untrimmed_timestamp = *temp_first_untrimmed_timestamp;
+absl::Status ObuSequencerBase::UpdateDescriptorObusAndClose(
+    const IASequenceHeaderObu& ia_sequence_header_obu,
+    const absl::flat_hash_map<uint32_t, CodecConfigObu>& codec_config_obus,
+    const absl::flat_hash_map<uint32_t, AudioElementWithData>& audio_elements,
+    const std::list<MixPresentationObu>& mix_presentation_obus,
+    const std::list<ArbitraryObu>& arbitrary_obus) {
+  // Many failure points should call `Abort`. We want to avoid leaving
+  // sequencers open if they may have invalid or corrupted IAMF data.
+  AbortOnDestruct abort_on_destruct(this);
+  switch (state_) {
+    case kInitialized:
+      return absl::FailedPreconditionError(
+          "`UpdateDescriptorObusAndClose` must be called after "
+          "`PushDescriptorObus`.");
+    case kPushDescriptorObusCalled:
+    case kPushSerializedDescriptorsCalled:
+      break;
+    case kClosed:
+      return absl::FailedPreconditionError(
+          "`Abort` or `Close` previously called.");
+  }
+  wb_.Reset();
+
+  // Serialize descriptor OBUS and adjacent arbitrary OBUs.
+  RETURN_IF_NOT_OK(ArbitraryObu::WriteObusWithHook(
+      ArbitraryObu::kInsertionHookBeforeDescriptors, arbitrary_obus, wb_));
+  // Write out the descriptor OBUs.
+  RETURN_IF_NOT_OK(ObuSequencerBase::WriteDescriptorObus(
+      ia_sequence_header_obu, codec_config_obus, audio_elements,
+      mix_presentation_obus, arbitrary_obus, wb_));
+  RETURN_IF_NOT_OK(ArbitraryObu::WriteObusWithHook(
+      ArbitraryObu::kInsertionHookAfterDescriptors, arbitrary_obus, wb_));
+
+  // We're a bit loose with what types of metadata we allow to change. Check
+  // at least the "functional" statistics are equivalent.
+  DescriptorStatistics descriptor_statistics{.descriptor_obus =
+                                                 wb_.bit_buffer()};
+  RETURN_IF_NOT_OK(
+      FillDescriptorStatistics(codec_config_obus, descriptor_statistics));
+  if (descriptor_statistics_->common_samples_per_frame !=
+          descriptor_statistics.common_samples_per_frame ||
+      descriptor_statistics_->common_sample_rate !=
+          descriptor_statistics.common_sample_rate ||
+      descriptor_statistics_->common_bit_depth !=
+          descriptor_statistics.common_bit_depth ||
+      descriptor_statistics_->num_channels !=
+          descriptor_statistics.num_channels) {
+    return absl::FailedPreconditionError(
+        "Descriptor OBUs have changed size between finalizing and "
+        "closing.");
+  }
+  if (descriptor_statistics_->descriptor_obus.size() !=
+      descriptor_statistics.descriptor_obus.size()) {
+    return absl::UnimplementedError(
+        "Descriptor OBUs have changed size between finalizing and closing.");
   }
 
-  int64_t cumulative_num_samples_for_logging = 0;
-  int64_t num_temporal_units_for_logging = 0;
-  const auto wrote_ia_sequence = [&]() -> absl::Status {
-    RETURN_IF_NOT_OK(PushSerializedDescriptorObus(
-        common_samples_per_frame, common_sample_rate, common_bit_depth,
-        first_untrimmed_timestamp, GetNumberOfChannels(audio_elements),
-        absl::MakeConstSpan(wb.bit_buffer())));
-    wb.Reset();
+  RETURN_IF_NOT_OK(
+      PushFinalizedDescriptorObus(absl::MakeConstSpan(wb_.bit_buffer())));
+  state_ = kPushSerializedDescriptorsCalled;
+  RETURN_IF_NOT_OK(Close());
 
-    for (const auto& [timestamp, temporal_unit] : temporal_unit_map) {
-      // Write the IA Sample to a `MediaSample`.
-      int num_samples = 0;
-
-      RETURN_IF_NOT_OK(WriteTemporalUnit(include_temporal_delimiters_,
-                                         temporal_unit, wb, num_samples));
-      RETURN_IF_NOT_OK(PushSerializedTemporalUnit(
-          static_cast<int64_t>(timestamp), num_samples, wb.bit_buffer()));
-
-      cumulative_num_samples_for_logging += num_samples;
-      num_temporal_units_for_logging++;
-      wb.Reset();
-    }
-    return absl::OkStatus();
-  }();
-  if (!wrote_ia_sequence.ok()) {
-    // Something failed when writing the IA Sequence. Signal to clean up the
-    // output, such as removing a bad file.
-    Abort();
-    return wrote_ia_sequence;
-  }
-
-  LOG(INFO) << "Wrote " << num_temporal_units_for_logging
-            << " temporal units with a total of "
-            << cumulative_num_samples_for_logging
-            << " samples excluding padding.";
-
-  Flush();
-  state_ = kFlushed;
+  abort_on_destruct.CancelAbort();
   return absl::OkStatus();
 }
 
+absl::Status ObuSequencerBase::Close() {
+  switch (state_) {
+    case kInitialized:
+      break;
+    case kPushDescriptorObusCalled: {
+      // Ok, trivial IA sequences don't have a first untrimmed timestamp. So
+      // we will simply push the descriptors with a fallback PTS of 0.
+      descriptor_statistics_->first_untrimmed_timestamp = kFallbackFirstPts;
+
+      RETURN_IF_NOT_OK(PushSerializedDescriptorObus(
+          descriptor_statistics_->common_samples_per_frame,
+          descriptor_statistics_->common_sample_rate,
+          descriptor_statistics_->common_bit_depth,
+          descriptor_statistics_->first_untrimmed_timestamp,
+          descriptor_statistics_->num_channels,
+          descriptor_statistics_->descriptor_obus));
+      state_ = kPushSerializedDescriptorsCalled;
+      break;
+    }
+    case kPushSerializedDescriptorsCalled:
+      break;
+    case kClosed:
+      return absl::FailedPreconditionError(
+          "`Abort` or `Close` previously called.");
+  }
+  CloseDerived();
+  state_ = kClosed;
+  return absl::OkStatus();
+}
+
+void ObuSequencerBase::Abort() {
+  AbortDerived();
+  state_ = kClosed;
+}
+
+absl::Status ObuSequencerBase::HandleInitialTemporalUnits(
+    const TemporalUnitView& temporal_unit,
+    absl::Span<const uint8_t> serialized_temporal_unit) {
+  const bool found_first_untrimmed_sample =
+      temporal_unit.num_untrimmed_samples_ != 0;
+  if (found_first_untrimmed_sample) {
+    // Gather the PTS. For internal accuracy, we store this even if we don't
+    // need to delay the descriptors.
+    descriptor_statistics_->first_untrimmed_timestamp =
+        temporal_unit.start_timestamp_ +
+        temporal_unit.num_samples_to_trim_at_start_;
+  }
+
+  // Push immediately if we don't need to delay the descriptors.
+  if (!delay_descriptors_until_first_untrimmed_sample_) {
+    return PushSerializedTemporalUnit(temporal_unit.start_timestamp_,
+                                      temporal_unit.num_untrimmed_samples_,
+                                      serialized_temporal_unit);
+  }
+
+  if (!found_first_untrimmed_sample) {
+    // This frame is fully trimmed. Cache it for later.
+    delayed_temporal_units_.push_back(SerializedTemporalUnit{
+        .start_timestamp = temporal_unit.start_timestamp_,
+        .num_untrimmed_samples = temporal_unit.num_untrimmed_samples_,
+        .data = std::vector<uint8_t>(serialized_temporal_unit.begin(),
+                                     serialized_temporal_unit.end())});
+    return absl::OkStatus();
+  }
+
+  // Found the first untrimmed sample. Push out all delayed OBUs.
+  RETURN_IF_NOT_OK(PushSerializedDescriptorObus(
+      descriptor_statistics_->common_samples_per_frame,
+      descriptor_statistics_->common_sample_rate,
+      descriptor_statistics_->common_bit_depth,
+      descriptor_statistics_->first_untrimmed_timestamp,
+      descriptor_statistics_->num_channels,
+      descriptor_statistics_->descriptor_obus));
+  state_ = kPushSerializedDescriptorsCalled;
+
+  // Flush any delayed temporal units.
+  for (const auto& delayed_temporal_unit : delayed_temporal_units_) {
+    RETURN_IF_NOT_OK(PushSerializedTemporalUnit(
+        delayed_temporal_unit.start_timestamp,
+        delayed_temporal_unit.num_untrimmed_samples,
+        absl::MakeConstSpan(delayed_temporal_unit.data)));
+  }
+  delayed_temporal_units_.clear();
+  // Then finally, flush the current temporal unit.
+  return PushSerializedTemporalUnit(temporal_unit.start_timestamp_,
+                                    temporal_unit.num_untrimmed_samples_,
+                                    serialized_temporal_unit);
+}
+
 }  // namespace iamf_tools
diff --git a/iamf/cli/obu_sequencer_base.h b/iamf/cli/obu_sequencer_base.h
index cd7b748..955f14c 100644
--- a/iamf/cli/obu_sequencer_base.h
+++ b/iamf/cli/obu_sequencer_base.h
@@ -15,6 +15,7 @@
 #include <cstdint>
 #include <list>
 #include <optional>
+#include <vector>
 
 #include "absl/container/flat_hash_map.h"
 #include "absl/status/status.h"
@@ -42,10 +43,26 @@
  *   // Create a concrete sequencer. Interface is dependent on the conreate
  *   // sequencer.
  *   std::unique_ptr<ObuSequencerBase> sequencer = ...;
- *   // Gather all the OBUs.
- *   // Sequence all of the OBUs.
- *   sequencer->.PickAndPlace(...);
  *
+ *  // Call the `PushDescriptorObus` method.
+ *  RETURN_IF_NOT_OK(sequencer->PushDescriptorObus(...));
+ *
+ *  while (more data is available) {
+ *    // Call the `PushTemporalUnit` method.
+ *    RETURN_IF_NOT_OK(sequencer->PushTemporalUnit(...));
+ *   }
+ *  // Signal that no more data is coming.
+ *  // Depending on the context, choose one of the closing functions. Either
+ *  // `UpdateDescriptorObusAndClose` (preferred) or `Close`.
+ *  RETURN_IF_NOT_OK(sequencer->UpdateDescriptorObusAndClose(...));
+ *  // Or:
+ *  RETURN_IF_NOT_OK(sequencer->Close());
+ *
+ * // Optionally. `Abort` may be called to clean up output. E.g. file-based
+ * // sequencers could delete their output file. `Abort` is most useful when
+ * // some component outside the class failes; failures in `PushDescriptorObus`,
+ * `PushTemporalUnit`, or `UpdateDescriptorObusAndClose` automatically call
+ * `Abort`.
  */
 class ObuSequencerBase {
  public:
@@ -61,6 +78,7 @@
    * \param num_samples Number of samples written out.
    * \return `absl::OkStatus()` on success. A specific status on failure.
    */
+  [[deprecated("Use this class as per the class documentation instead.")]]
   static absl::Status WriteTemporalUnit(bool include_temporal_delimiters,
                                         const TemporalUnitView& temporal_unit,
                                         WriteBitBuffer& wb, int& num_samples);
@@ -78,6 +96,7 @@
    * \param wb Write buffer to write to.
    * \return `absl::OkStatus()` on success. A specific status on failure.
    */
+  [[deprecated("Use this class as per the class documentation instead.")]]
   static absl::Status WriteDescriptorObus(
       const IASequenceHeaderObu& ia_sequence_header_obu,
       const absl::flat_hash_map<uint32_t, CodecConfigObu>& codec_config_obus,
@@ -101,6 +120,58 @@
   /*!\brief Destructor.*/
   virtual ~ObuSequencerBase() = 0;
 
+  /*!\brief Gathers statistics on and pushes the OBUs to some output.
+   *
+   * \param ia_sequence_header_obu IA Sequence Header OBU to write.
+   * \param codec_config_obus Codec Config OBUs to write.
+   * \param audio_elements Audio Element OBUs with data to write.
+   * \param mix_presentation_obus Mix Presentation OBUs to write.
+   * \param arbitrary_obus Arbitrary OBUs to write.
+   * \return `absl::OkStatus()` on success. A specific status on failure.
+   */
+  absl::Status PushDescriptorObus(
+      const IASequenceHeaderObu& ia_sequence_header_obu,
+      const absl::flat_hash_map<uint32_t, CodecConfigObu>& codec_config_obus,
+      const absl::flat_hash_map<uint32_t, AudioElementWithData>& audio_elements,
+      const std::list<MixPresentationObu>& mix_presentation_obus,
+      const std::list<ArbitraryObu>& arbitrary_obus);
+
+  /*!\brief Gathers statistics on and pushes the temporal unit to some output.
+   *
+   * \param temporal_unit Temporal unit to push.
+   * \return `absl::OkStatus()` on success. A specific status on failure.
+   */
+  absl::Status PushTemporalUnit(const TemporalUnitView& temporal_unit);
+
+  /*!\brief Finalizes the descriptor OBUs and closes the output.
+   *
+   * \param ia_sequence_header_obu IA Sequence Header OBU to write.
+   * \param codec_config_obus Codec Config OBUs to write.
+   * \param audio_elements Audio Element OBUs with data to write.
+   * \param mix_presentation_obus Mix Presentation OBUs to write.
+   * \param arbitrary_obus Arbitrary OBUs to write.
+   * \return `absl::OkStatus()` on success. A specific status on failure.
+   */
+  absl::Status UpdateDescriptorObusAndClose(
+      const IASequenceHeaderObu& ia_sequence_header_obu,
+      const absl::flat_hash_map<uint32_t, CodecConfigObu>& codec_config_obus,
+      const absl::flat_hash_map<uint32_t, AudioElementWithData>& audio_elements,
+      const std::list<MixPresentationObu>& mix_presentation_obus,
+      const std::list<ArbitraryObu>& arbitrary_obus);
+
+  /*!\brief Signals that no more data is coming, and closes the output.
+   *
+   * \return `absl::OkStatus()` on success. A specific status on failure.
+   */
+  absl::Status Close();
+
+  /*!\brief Aborts writing the output.
+   *
+   * Useful for sequencers which want to clean up their output. Such as to avoid
+   * leaving a stray file when encoding fails.
+   */
+  void Abort();
+
   /*!\brief Pick and places OBUs and write to some output.
    *
    * \param ia_sequence_header_obu IA Sequence Header OBU to write.
@@ -112,6 +183,7 @@
    * \param arbitrary_obus Arbitrary OBUs to write.
    * \return `absl::OkStatus()` on success. A specific status on failure.
    */
+  [[deprecated("Use this class as per the class documentation instead.")]]
   absl::Status PickAndPlace(
       const IASequenceHeaderObu& ia_sequence_header_obu,
       const absl::flat_hash_map<uint32_t, CodecConfigObu>& codec_config_obus,
@@ -157,25 +229,87 @@
       int64_t timestamp, int num_samples,
       absl::Span<const uint8_t> temporal_unit) = 0;
 
-  /*!\brief Signals that no more data is coming. */
-  virtual void Flush() = 0;
+  /*!\brief Pushes the finalized descriptor OBUs to some output.
+   *
+   * \param descriptor_obus Serialized finalized descriptor OBUs to push.
+   * \return `absl::OkStatus()` on success. A specific status on failure.
+   */
+  virtual absl::Status PushFinalizedDescriptorObus(
+      absl::Span<const uint8_t> descriptor_obus) = 0;
+
+  /*!\brief Signals that no more data is coming, and closes the output. */
+  virtual void CloseDerived() = 0;
 
   /*!\brief Aborts writing the output.
    *
    * Useful for sequencers which want to clean up their output. Such as to avoid
    * leaving a stray file when encoding fails.
    */
-  virtual void Abort() = 0;
+  virtual void AbortDerived() = 0;
 
   // The `LebGenerator` to use when writing OBUs.
   const LebGenerator leb_generator_;
 
  private:
-  enum State { kInitialized, kFlushed };
+  /*!\brief Handles the initial temporal units.
+   *
+   * This function manages state to help process the initial temporal units up
+   * to and including the first one that has a real sample. In a typical IA
+   * Sequence, this would rarely be more few frames.
+   *
+   * \param temporal_unit Temporal unit to push.
+   * \param serialized_temporal_unit Serialized temporla unit.
+   * \return `absl::OkStatus()` on success. A specific status on failure.
+   */
+  absl::Status HandleInitialTemporalUnits(
+      const TemporalUnitView& temporal_unit,
+      absl::Span<const uint8_t> serialized_temporal_unit);
+
+  enum State {
+    // Initial state.
+    kInitialized,
+    // `PushDescriptorObus` has been called, but it may have been delayed when
+    // `delay_descriptors_until_first_untrimmed_sample_` is `true`.
+    kPushDescriptorObusCalled,
+    // Descriptors have been pushed, in this state temporal units are no longer
+    // delayed.
+    kPushSerializedDescriptorsCalled,
+    // `Close` or `Abort` has been called.
+    kClosed
+  };
   State state_ = kInitialized;
 
   const bool delay_descriptors_until_first_untrimmed_sample_;
   const bool include_temporal_delimiters_;
+
+  // Statistics for the current IA Sequence. Convenient to hold, in order to
+  // validate that the finalized OBUs are consistent with the initial ones.
+  struct DescriptorStatistics {
+    uint32_t common_samples_per_frame = 0;
+    uint32_t common_sample_rate = 0;
+    uint8_t common_bit_depth = 0;
+    int num_channels = 0;
+    std::optional<int64_t> first_untrimmed_timestamp;
+    std::vector<uint8_t> descriptor_obus;
+  };
+  std::optional<DescriptorStatistics> descriptor_statistics_;
+
+  // Reusable scratch buffer.
+  WriteBitBuffer wb_;
+
+  int64_t num_temporal_units_for_logging_ = 0;
+  int64_t cumulative_num_samples_for_logging_ = 0;
+
+  // State for delayed OBUs. `delay_descriptors_until_first_untrimmed_sample_ ==
+  // true` implies we must cache and delayed OBUs until the first untrimmed
+  // sample is seen. In practical IA Sequences, this is rarely more than a few
+  // temporal units.
+  struct SerializedTemporalUnit {
+    int64_t start_timestamp;
+    uint32_t num_untrimmed_samples;
+    std::vector<uint8_t> data;
+  };
+  std::list<SerializedTemporalUnit> delayed_temporal_units_;
 };
 
 }  // namespace iamf_tools
diff --git a/iamf/cli/obu_sequencer_iamf.cc b/iamf/cli/obu_sequencer_iamf.cc
index cb2e313..a55bd2d 100644
--- a/iamf/cli/obu_sequencer_iamf.cc
+++ b/iamf/cli/obu_sequencer_iamf.cc
@@ -87,14 +87,31 @@
   return wb_.FlushAndWriteToFile(output_iamf_);
 }
 
-void ObuSequencerIamf::Flush() {
+absl::Status ObuSequencerIamf::PushFinalizedDescriptorObus(
+    absl::Span<const uint8_t> descriptor_obus) {
+  if (output_iamf_.has_value()) {
+    // For good practice, restore the previous position in the file after we
+    // rewrite. But in reality this function usually will be called right before
+    // closing the file.
+    const auto previous_position = output_iamf_->tellg();
+    output_iamf_->seekg(0, std::ios::beg);
+    RETURN_IF_NOT_OK(wb_.WriteUint8Span(descriptor_obus));
+    RETURN_IF_NOT_OK(wb_.FlushAndWriteToFile(output_iamf_));
+
+    output_iamf_->seekg(previous_position);
+  }
+
+  return absl::OkStatus();
+}
+
+void ObuSequencerIamf::CloseDerived() {
   if (output_iamf_.has_value() && output_iamf_->is_open()) {
     output_iamf_->close();
     output_iamf_ = std::nullopt;
   }
 }
 
-void ObuSequencerIamf::Abort() {
+void ObuSequencerIamf::AbortDerived() {
   LOG(INFO) << "Aborting ObuSequencerIamf.";
   MaybeRemoveFile(iamf_filename_, output_iamf_);
 }
diff --git a/iamf/cli/obu_sequencer_iamf.h b/iamf/cli/obu_sequencer_iamf.h
index 4d83018..21a6b9a 100644
--- a/iamf/cli/obu_sequencer_iamf.h
+++ b/iamf/cli/obu_sequencer_iamf.h
@@ -72,14 +72,22 @@
       int64_t /*timestamp*/, int /*num_samples*/,
       absl::Span<const uint8_t> temporal_unit) override;
 
+  /*!\brief Pushes the finalized descriptor OBUs to the IAMF file.
+   *
+   * \param descriptor_obus Serialized finalized descriptor OBUs to push.
+   * \return `absl::OkStatus()` on success. A specific status on failure.
+   */
+  absl::Status PushFinalizedDescriptorObus(
+      absl::Span<const uint8_t> descriptor_obus) override;
+
   /*!\brief Signals that no more data is coming. */
-  void Flush() override;
+  void CloseDerived() override;
 
   /*!\brief Aborts writing the output.
    *
    * Cleans up the output file if it exists.
    */
-  void Abort() override;
+  void AbortDerived() override;
 
   const std::string iamf_filename_;
   std::optional<std::fstream> output_iamf_;
diff --git a/iamf/cli/profile_filter.cc b/iamf/cli/profile_filter.cc
index 1088277..e8c3414 100644
--- a/iamf/cli/profile_filter.cc
+++ b/iamf/cli/profile_filter.cc
@@ -219,9 +219,9 @@
 
 absl::Status FilterProfileForNumSubmixes(
     absl::string_view mix_presentation_id_for_debugging,
-    int num_submixes_in_mix_presentation,
+    int num_sub_mixes_in_mix_presentation,
     absl::flat_hash_set<ProfileVersion>& profile_versions) {
-  if (num_submixes_in_mix_presentation > 1) {
+  if (num_sub_mixes_in_mix_presentation > 1) {
     profile_versions.erase(ProfileVersion::kIamfSimpleProfile);
     profile_versions.erase(ProfileVersion::kIamfBaseProfile);
     profile_versions.erase(ProfileVersion::kIamfBaseEnhancedProfile);
@@ -230,7 +230,7 @@
   if (profile_versions.empty()) {
     return absl::InvalidArgumentError(
         absl::StrCat(mix_presentation_id_for_debugging, " has ",
-                     num_submixes_in_mix_presentation,
+                     num_sub_mixes_in_mix_presentation,
                      " sub mixes, but the requested profiles "
                      "do not support this number of sub-mixes."));
   }
@@ -289,7 +289,7 @@
   num_audio_elements_in_mix_presentation = 0;
   num_channels_in_mix_presentation = 0;
   for (const auto& sub_mix : mix_presentation_obu.sub_mixes_) {
-    num_audio_elements_in_mix_presentation += sub_mix.num_audio_elements;
+    num_audio_elements_in_mix_presentation += sub_mix.audio_elements.size();
     for (const auto& sub_mix_audio_element : sub_mix.audio_elements) {
       auto iter = audio_elements.find(sub_mix_audio_element.audio_element_id);
       if (iter == audio_elements.end()) {
diff --git a/iamf/cli/proto/mix_presentation.proto b/iamf/cli/proto/mix_presentation.proto
index 72df1a4..a10815f 100644
--- a/iamf/cli/proto/mix_presentation.proto
+++ b/iamf/cli/proto/mix_presentation.proto
@@ -135,7 +135,9 @@
 }
 
 message AnchoredLoudness {
-  optional uint32 num_anchored_loudness = 1;
+  // `num_anchored_loudness` is ignored. The value in the bitstream is inferred
+  // based on the number of `anchor_elements`.
+  optional uint32 num_anchored_loudness = 1 [deprecated = true];
   repeated AnchorElement anchor_elements = 2;
 }
 
@@ -179,14 +181,18 @@
 }
 
 message MixPresentationSubMix {
-  optional uint32 num_audio_elements = 1;
+  // `num_audio_elements` is ignored. The value in the bitstream is inferred
+  // based on the number of `audio_elements`.
+  optional uint32 num_audio_elements = 1 [deprecated = true];
   repeated SubMixAudioElement audio_elements = 2;
 
   // Superseded by `output_mix_gain`.
   optional OutputMixConfig output_mix_config = 3 [deprecated = true];
   optional MixGainParamDefinition output_mix_gain = 6;
 
-  optional uint32 num_layouts = 4;
+  // `num_layouts` is ignored. The value in the bitstream is inferred
+  // based on the number of `layouts`.
+  optional uint32 num_layouts = 4 [deprecated = true];
   repeated MixPresentationLayout layouts = 5;
 
   // Next ID: 7
@@ -221,7 +227,9 @@
   // Length should be equal to `count_label`.
   repeated string localized_presentation_annotations = 10;
 
-  optional uint32 num_sub_mixes = 3;
+  // `num_sub_mixes` is ignored. The value in the bitstream is inferred
+  // based on the number of `sub_mixes`.
+  optional uint32 num_sub_mixes = 3 [deprecated = true];
   repeated MixPresentationSubMix sub_mixes = 4;
 
   // When false [default]: The encoder will ignore the below
diff --git a/iamf/cli/proto_conversion/proto_to_obu/mix_presentation_generator.cc b/iamf/cli/proto_conversion/proto_to_obu/mix_presentation_generator.cc
index 2a18707..2e839d6 100644
--- a/iamf/cli/proto_conversion/proto_to_obu/mix_presentation_generator.cc
+++ b/iamf/cli/proto_conversion/proto_to_obu/mix_presentation_generator.cc
@@ -85,20 +85,25 @@
   }
 }
 
-void FillNumSubMixes(const iamf_tools_cli_proto::MixPresentationObuMetadata&
-                         mix_presentation_metadata,
-                     DecodedUleb128& num_sub_mixes,
-                     std::vector<MixPresentationSubMix>& sub_mixes) {
-  num_sub_mixes = mix_presentation_metadata.num_sub_mixes();
+void ReserveNumSubMixes(const iamf_tools_cli_proto::MixPresentationObuMetadata&
+                            mix_presentation_metadata,
+                        std::vector<MixPresentationSubMix>& sub_mixes) {
+  if (mix_presentation_metadata.has_num_sub_mixes()) {
+    LOG(WARNING) << "Ignoring deprecated `num_sub_mixes` field."
+                 << "Please remove it.";
+  }
 
-  sub_mixes.reserve(mix_presentation_metadata.num_sub_mixes());
+  sub_mixes.reserve(mix_presentation_metadata.sub_mixes_size());
 }
 
-void FillSubMixNumAudioElements(
+void ReserveSubMixNumAudioElements(
     const iamf_tools_cli_proto::MixPresentationSubMix& input_sub_mix,
     MixPresentationSubMix& sub_mix) {
-  sub_mix.num_audio_elements = input_sub_mix.num_audio_elements();
-  sub_mix.audio_elements.reserve(input_sub_mix.num_audio_elements());
+  if (input_sub_mix.has_num_audio_elements()) {
+    LOG(WARNING) << "Ignoring deprecated `num_audio_elements` field."
+                 << "Please remove it.";
+  }
+  sub_mix.audio_elements.reserve(input_sub_mix.audio_elements_size());
 }
 
 absl::Status FillLocalizedElementAnnotations(
@@ -228,18 +233,13 @@
 absl::Status FillLayouts(
     const iamf_tools_cli_proto::MixPresentationSubMix& input_sub_mix,
     MixPresentationSubMix& sub_mix) {
-  sub_mix.num_layouts = input_sub_mix.num_layouts();
-
-  if (input_sub_mix.layouts().size() != input_sub_mix.num_layouts()) {
-    return absl::InvalidArgumentError(absl::StrCat(
-        "Inconsistent number of layouts in user input. "
-        "input_sub_mix.num_layouts()= ",
-        input_sub_mix.num_layouts(), " vs  input_sub_mix.layouts().size()= ",
-        input_sub_mix.layouts().size()));
+  if (input_sub_mix.has_num_layouts()) {
+    LOG(WARNING) << "Ignoring deprecated `num_layouts` field."
+                 << "Please remove it.";
   }
 
   // Reserve the layouts vector and copy in the layouts.
-  sub_mix.layouts.reserve(input_sub_mix.num_layouts());
+  sub_mix.layouts.reserve(input_sub_mix.layouts_size());
 
   for (const auto& input_layout : input_sub_mix.layouts()) {
     const auto& input_loudness_layout = input_layout.loudness_layout();
@@ -315,10 +315,10 @@
   const size_t num_tags = mix_presentation_tags.tags().size() +
                           (append_build_information_tag ? 1 : 0);
   // At the OBU it must fit into a `uint8_t`.
+  uint8_t obu_num_tags;
   RETURN_IF_NOT_OK(StaticCastIfInRange<size_t, uint8_t>(
-      "MixPresentationTags.num_tags", num_tags,
-      obu_mix_presentation_tags->num_tags));
-  obu_mix_presentation_tags->tags.reserve(num_tags);
+      "Total number of MixPresentationTags.tags", num_tags, obu_num_tags));
+  obu_mix_presentation_tags->tags.reserve(obu_num_tags);
   for (const auto& input_tag : mix_presentation_tags.tags()) {
     obu_mix_presentation_tags->tags.emplace_back(MixPresentationTags::Tag{
         .tag_name = input_tag.tag_name(),
@@ -416,11 +416,16 @@
     // Not using anchored loudness.
     return absl::OkStatus();
   }
+  if (user_loudness.anchored_loudness().has_num_anchored_loudness()) {
+    LOG(WARNING) << "Ignoring deprecated `num_anchored_loudness` field. Please "
+                    "remove it.";
+  }
 
-  RETURN_IF_NOT_OK(StaticCastIfInRange<uint32_t, uint8_t>(
-      "LoudnessInfo.anchored_loudness.num_anchored_loudness",
-      user_loudness.anchored_loudness().num_anchored_loudness(),
-      output_loudness.anchored_loudness.num_anchored_loudness));
+  uint8_t num_anchored_loudness;
+  RETURN_IF_NOT_OK(StaticCastIfInRange<size_t, uint8_t>(
+      "Number of LoudnessInfo.anchored_loudness",
+      user_loudness.anchored_loudness().anchor_elements_size(),
+      num_anchored_loudness));
 
   for (const auto& metadata_anchor_element :
        user_loudness.anchored_loudness().anchor_elements()) {
@@ -481,7 +486,6 @@
       // Length `count_label`.
       std::vector<std::string> localized_presentation_annotations;
 
-      DecodedUleb128 num_sub_mixes;
       // Length `num_sub_mixes`.
       std::vector<MixPresentationSubMix> sub_mixes;
 
@@ -496,12 +500,11 @@
         obu_args.annotations_language,
         obu_args.localized_presentation_annotations);
 
-    FillNumSubMixes(mix_presentation_metadata, obu_args.num_sub_mixes,
-                    obu_args.sub_mixes);
+    ReserveNumSubMixes(mix_presentation_metadata, obu_args.sub_mixes);
     for (const auto& input_sub_mix : mix_presentation_metadata.sub_mixes()) {
       MixPresentationSubMix sub_mix;
 
-      FillSubMixNumAudioElements(input_sub_mix, sub_mix);
+      ReserveSubMixNumAudioElements(input_sub_mix, sub_mix);
       for (const auto& input_sub_mix_audio_element :
            input_sub_mix.audio_elements()) {
         SubMixAudioElement sub_mix_audio_element;
@@ -541,8 +544,7 @@
         GetHeaderFromMetadata(mix_presentation_metadata.obu_header()),
         obu_args.mix_presentation_id, obu_args.count_label,
         obu_args.annotations_language,
-        obu_args.localized_presentation_annotations, obu_args.num_sub_mixes,
-        obu_args.sub_mixes);
+        obu_args.localized_presentation_annotations, obu_args.sub_mixes);
     obu.mix_presentation_tags_ = obu_args.mix_presentation_tags;
     mix_presentation_obus.emplace_back(std::move(obu));
   }
diff --git a/iamf/cli/proto_conversion/proto_to_obu/tests/mix_presentation_generator_test.cc b/iamf/cli/proto_conversion/proto_to_obu/tests/mix_presentation_generator_test.cc
index a322f7c..d80d4db 100644
--- a/iamf/cli/proto_conversion/proto_to_obu/tests/mix_presentation_generator_test.cc
+++ b/iamf/cli/proto_conversion/proto_to_obu/tests/mix_presentation_generator_test.cc
@@ -80,9 +80,7 @@
       R"pb(
         mix_presentation_id: 42
         count_label: 0
-        num_sub_mixes: 1
         sub_mixes {
-          num_audio_elements: 1
           audio_elements {
             audio_element_id: 300
             rendering_config {
@@ -442,7 +440,6 @@
 
   const auto& first_obu = generated_obus.front();
   ASSERT_TRUE(first_obu.mix_presentation_tags_.has_value());
-  EXPECT_EQ(first_obu.mix_presentation_tags_->num_tags, 0);
   EXPECT_TRUE(first_obu.mix_presentation_tags_->tags.empty());
 }
 
@@ -464,7 +461,6 @@
   // Ok safely ignore the deprecated `num_tags` field.
   const auto& first_obu = generated_obus.front();
   ASSERT_TRUE(first_obu.mix_presentation_tags_.has_value());
-  EXPECT_EQ(first_obu.mix_presentation_tags_->num_tags, 0);
   EXPECT_TRUE(first_obu.mix_presentation_tags_->tags.empty());
 }
 
@@ -533,7 +529,6 @@
 
   const auto& first_obu = generated_obus.front();
   ASSERT_TRUE(first_obu.mix_presentation_tags_.has_value());
-  EXPECT_EQ(first_obu.mix_presentation_tags_->num_tags, 2);
   ASSERT_EQ(first_obu.mix_presentation_tags_->tags.size(), 2);
   EXPECT_EQ(first_obu.mix_presentation_tags_->tags[0].tag_name,
             "content_language");
@@ -596,7 +591,7 @@
 
   if (test_case.expected_num_tags.has_value()) {
     EXPECT_TRUE(first_obu.mix_presentation_tags_.has_value());
-    EXPECT_EQ(first_obu.mix_presentation_tags_->num_tags,
+    EXPECT_EQ(first_obu.mix_presentation_tags_->tags.size(),
               *test_case.expected_num_tags);
     // If the tags are present, the last tag may be the build information tag.
     if (test_case.expect_build_information_tag_to_be_present) {
@@ -879,17 +874,64 @@
       generator.Generate(kAppendBuildInformationTag, generated_obus_).ok());
 }
 
-TEST_F(MixPresentationGeneratorTest, InvalidInconsistentNumberOfLayouts) {
-  // There is one element in the `layouts` array.
-  ASSERT_EQ(mix_presentation_metadata_.at(0).sub_mixes(0).layouts().size(), 1);
-  // `num_layouts` is inconsistent with the number of layouts in the array.
-  const uint32_t kInconsistentNumLayouts = 2;
-  mix_presentation_metadata_.at(0).mutable_sub_mixes(0)->set_num_layouts(
-      kInconsistentNumLayouts);
+TEST_F(MixPresentationGeneratorTest, IgnoresDeprecatedNumSubMixes) {
+  // This test assumes the default metadata has one sub mix.
+  constexpr uint32_t kExpectedNumSubMixes = 1;
+  ASSERT_EQ(mix_presentation_metadata_.at(0).sub_mixes_size(),
+            kExpectedNumSubMixes);
+  // Include a strange value for the deprecated `num_sub_mixes` field.
+  constexpr uint32_t kIncorrectIgnoredNumSubMixes = 2;
+  mix_presentation_metadata_.at(0).set_num_sub_mixes(
+      kIncorrectIgnoredNumSubMixes);
   MixPresentationGenerator generator(mix_presentation_metadata_);
 
-  EXPECT_FALSE(
-      generator.Generate(kAppendBuildInformationTag, generated_obus_).ok());
+  EXPECT_THAT(generator.Generate(kAppendBuildInformationTag, generated_obus_),
+              IsOk());
+
+  // Regardless of the deprecated `num_layouts` field, the number of layouts is
+  // inferred the `layouts` array.
+  EXPECT_EQ(generated_obus_.back().GetNumSubMixes(), kExpectedNumSubMixes);
+  EXPECT_EQ(generated_obus_.back().sub_mixes_.size(), kExpectedNumSubMixes);
+}
+
+TEST_F(MixPresentationGeneratorTest, IgnoresDeprecatedNumAudioElements) {
+  // This test assumes the default metadata has one audio element.
+  constexpr uint32_t kExpectedNumAudioElements = 1;
+  ASSERT_EQ(mix_presentation_metadata_.at(0).sub_mixes(0).audio_elements_size(),
+            kExpectedNumAudioElements);
+  // Include a strange value for the deprecated `num_audio_elements`.
+  constexpr uint32_t kIncorrectIgnoredNumAudioElements = 2;
+  mix_presentation_metadata_.at(0).mutable_sub_mixes(0)->set_num_audio_elements(
+      kIncorrectIgnoredNumAudioElements);
+  MixPresentationGenerator generator(mix_presentation_metadata_);
+
+  EXPECT_THAT(generator.Generate(kAppendBuildInformationTag, generated_obus_),
+              IsOk());
+
+  // Regardless of the deprecated `num_audio_elements` field, the number of
+  // audio elements the `audio_elements` array.
+  EXPECT_EQ(generated_obus_.back().sub_mixes_[0].audio_elements.size(),
+            kExpectedNumAudioElements);
+}
+
+TEST_F(MixPresentationGeneratorTest, IgnoresDeprecatedNumLayouts) {
+  // This test assumes the default metadata has one layout.
+  constexpr uint32_t kExpectedNumLayouts = 1;
+  ASSERT_EQ(mix_presentation_metadata_.at(0).sub_mixes(0).layouts().size(),
+            kExpectedNumLayouts);
+  // Include a strange value for the deprecated `num_layouts`.
+  constexpr uint32_t kIncorrectIgnoredNumLayouts = 2;
+  mix_presentation_metadata_.at(0).mutable_sub_mixes(0)->set_num_layouts(
+      kIncorrectIgnoredNumLayouts);
+  MixPresentationGenerator generator(mix_presentation_metadata_);
+
+  EXPECT_THAT(generator.Generate(kAppendBuildInformationTag, generated_obus_),
+              IsOk());
+
+  // Regardless of the deprecated `num_layouts` field, the number of layouts is
+  // inferred from the `layouts` array.
+  EXPECT_EQ(generated_obus_.back().sub_mixes_[0].layouts.size(),
+            kExpectedNumLayouts);
 }
 
 TEST_F(MixPresentationGeneratorTest, CopiesUserLoudness) {
@@ -1138,7 +1180,6 @@
   google::protobuf::TextFormat::ParseFromString(
       R"pb(
         anchored_loudness {
-          num_anchored_loudness: 2
           anchor_elements:
           [ { anchor_element: ANCHOR_TYPE_DIALOGUE anchored_loudness: 1000 }
             , { anchor_element: ANCHOR_TYPE_ALBUM anchored_loudness: 1001 }]
@@ -1148,7 +1189,6 @@
   // Configured expected data. The function only writes to the
   // `AnchoredLoudness`.
   const AnchoredLoudness expected_output_loudness = {
-      .num_anchored_loudness = 2,
       .anchor_elements = {
           {.anchor_element = AnchoredLoudnessElement::kAnchorElementDialogue,
            .anchored_loudness = 1000},
@@ -1161,6 +1201,22 @@
   EXPECT_EQ(output_loudness.anchored_loudness, expected_output_loudness);
 }
 
+TEST(CopyUserAnchoredLoudness, IgnoresDeprecatedNumAnchoredLoudnessField) {
+  // Set up an anchored loudness which no anchor elements, but incorrectly
+  // claims there is one.
+  LoudnessInfo output_loudness = {.info_type = LoudnessInfo::kAnchoredLoudness};
+  iamf_tools_cli_proto::LoudnessInfo user_loudness;
+  user_loudness.mutable_anchored_loudness()->set_num_anchored_loudness(1);
+
+  EXPECT_THAT(MixPresentationGenerator::CopyUserAnchoredLoudness(
+                  user_loudness, output_loudness),
+              IsOk());
+
+  // Regardless of the deprecated `num_anchored_loudness` field, the number of
+  // anchor elements is inferred from the `anchor_elements` array.
+  EXPECT_TRUE(output_loudness.anchored_loudness.anchor_elements.empty());
+}
+
 TEST(CopyUserAnchoredLoudness, IllegalUnknownAnchorElementEnum) {
   // `info_type` must be configured as a prerequisite.
   LoudnessInfo output_loudness = {.info_type = LoudnessInfo::kAnchoredLoudness};
@@ -1170,7 +1226,6 @@
   google::protobuf::TextFormat::ParseFromString(
       R"pb(
         anchored_loudness {
-          num_anchored_loudness: 1
           anchor_elements:
           [ { anchor_element: ANCHOR_TYPE_NOT_DEFINED anchored_loudness: 1000 }
       )pb",
diff --git a/iamf/cli/rendering_mix_presentation_finalizer.cc b/iamf/cli/rendering_mix_presentation_finalizer.cc
index 95e3400..9fce5fa 100644
--- a/iamf/cli/rendering_mix_presentation_finalizer.cc
+++ b/iamf/cli/rendering_mix_presentation_finalizer.cc
@@ -157,7 +157,10 @@
     const uint32_t output_sample_rate =
         sub_mix_audio_element.codec_config->GetOutputSampleRate();
     if (common_sample_rate != output_sample_rate) {
-      // TODO(b/274689885): Convert to a common sample rate and/or bit-depth.
+      // Theoretically, we would have to resample this audio element to the
+      // common sample rate. However, as of IAMF v1.1.0, the spec forbids
+      // multiple Codec Config OBUs. This case is not possible to occur with a
+      // single Codec Config OBU.
       return absl::UnimplementedError(
           absl::StrCat("OBUs with different sample rates not supported yet: (",
                        common_sample_rate, " != ", output_sample_rate, ")."));
@@ -622,10 +625,13 @@
         rendering_bit_depth, common_num_samples_per_frame,
         requires_resampling));
     if (requires_resampling) {
-      // TODO(b/274689885): Convert to a common sample rate and/or bit-depth.
+      // Detected multiple Codec Config OBUs with different sample rates or
+      // bit-depths. As of IAMF v1.1.0, multiple Codec  Config OBUs in the same
+      // IA sequence are never permitted. The spec implies we would have to
+      // resample to a common sample rate and/or bit-depth.
       return absl::UnimplementedError(
-          "This implementation does not support mixing different sample rates "
-          "or bit-depths.");
+          "This implementation does not support mixing Codec Config OBUs with "
+          "different sample rates or bit-depths.");
     }
     RETURN_IF_NOT_OK(GenerateRenderingMetadataForLayouts(
         renderer_factory, loudness_calculator_factory, sample_processor_factory,
@@ -910,6 +916,7 @@
     RETURN_IF_NOT_OK(FillLoudnessForMixPresentation(
         validate_loudness, sub_mix_rendering_metadata_it->second,
         mix_presentation_obu));
+    mix_presentation_obu.PrintObu();
   }
 
   // Flush the finalized OBUs and mark that this class should not use them
diff --git a/iamf/cli/temporal_unit_view.cc b/iamf/cli/temporal_unit_view.cc
index 8b4d5c5..f234c56 100644
--- a/iamf/cli/temporal_unit_view.cc
+++ b/iamf/cli/temporal_unit_view.cc
@@ -216,21 +216,24 @@
                                                          arbitrary_obus.end());
   absl::c_sort(sorted_parameter_blocks, CompareParameterId);
   absl::c_sort(sorted_audio_frames, CompareAudioElementIdAudioSubstreamId);
-  return TemporalUnitView(std::move(sorted_parameter_blocks),
-                          std::move(sorted_audio_frames),
-                          std::move(copied_arbitrary_obus),
-                          statistics->num_samples_to_trim_at_start,
-                          statistics->num_untrimmed_samples);
+  return TemporalUnitView(
+      std::move(sorted_parameter_blocks), std::move(sorted_audio_frames),
+      std::move(copied_arbitrary_obus), statistics->start_timestamp,
+      statistics->end_timestamp, statistics->num_samples_to_trim_at_start,
+      statistics->num_untrimmed_samples);
 }
 
 TemporalUnitView::TemporalUnitView(
     std::vector<const ParameterBlockWithData*>&& parameter_blocks,
     std::vector<const AudioFrameWithData*>&& audio_frames,
     std::vector<const ArbitraryObu*>&& arbitrary_obus,
+    InternalTimestamp start_timestamp, InternalTimestamp end_timestamp,
     uint32_t num_samples_to_trim_at_start, uint32_t num_untrimmed_samples)
     : parameter_blocks_(std::move(parameter_blocks)),
       audio_frames_(std::move(audio_frames)),
       arbitrary_obus_(std::move(arbitrary_obus)),
+      start_timestamp_(start_timestamp),
+      end_timestamp_(end_timestamp),
       num_samples_to_trim_at_start_(num_samples_to_trim_at_start),
       num_untrimmed_samples_(num_untrimmed_samples) {}
 
diff --git a/iamf/cli/temporal_unit_view.h b/iamf/cli/temporal_unit_view.h
index 59f9d20..8d6a245 100644
--- a/iamf/cli/temporal_unit_view.h
+++ b/iamf/cli/temporal_unit_view.h
@@ -22,6 +22,7 @@
 #include "iamf/cli/audio_frame_with_data.h"
 #include "iamf/cli/parameter_block_with_data.h"
 #include "iamf/obu/arbitrary_obu.h"
+#include "iamf/obu/types.h"
 
 namespace iamf_tools {
 
@@ -93,6 +94,8 @@
   const std::vector<const ArbitraryObu*> arbitrary_obus_;
 
   // Common statistics for this temporal unit.
+  const InternalTimestamp start_timestamp_;
+  const InternalTimestamp end_timestamp_;
   const uint32_t num_samples_to_trim_at_start_;
   const uint32_t num_untrimmed_samples_;
 
@@ -104,6 +107,8 @@
    * \param parameter_blocks Parameter blocks to include in the view.
    * \param audio_frames Audio frames to include in the view.
    * \param arbitrary_obus Arbitrary OBUs to include in the view.
+   * \param start_timestamp Start timestamp of the temporal unit.
+   * \param end_timestamp End timestamp of the temporal unit.
    * \param num_samples_to_trim_at_start Number of samples to trim at the start
    *        of the audio frames.
    * \param num_untrimmed_samples Number of samples in the audio frames before
@@ -113,6 +118,7 @@
       std::vector<const ParameterBlockWithData*>&& parameter_blocks,
       std::vector<const AudioFrameWithData*>&& audio_frames,
       std::vector<const ArbitraryObu*>&& arbitrary_obus,
+      InternalTimestamp start_timestamp, InternalTimestamp end_timestamp,
       uint32_t num_samples_to_trim_at_start, uint32_t num_untrimmed_samples);
 };
 
diff --git a/iamf/cli/tests/BUILD b/iamf/cli/tests/BUILD
index 94d3211..16b4b1f 100644
--- a/iamf/cli/tests/BUILD
+++ b/iamf/cli/tests/BUILD
@@ -342,6 +342,7 @@
         "//iamf/cli:audio_frame_with_data",
         "//iamf/cli:obu_sequencer_iamf",
         "//iamf/cli:parameter_block_with_data",
+        "//iamf/cli:temporal_unit_view",
         "//iamf/common:leb_generator",
         "//iamf/common:read_bit_buffer",
         "//iamf/obu:arbitrary_obu",
diff --git a/iamf/cli/tests/cli_test_utils.cc b/iamf/cli/tests/cli_test_utils.cc
index e3821fa..3d72e5a 100644
--- a/iamf/cli/tests/cli_test_utils.cc
+++ b/iamf/cli/tests/cli_test_utils.cc
@@ -36,6 +36,7 @@
 #include "absl/status/status_matchers.h"
 #include "absl/status/statusor.h"
 #include "absl/strings/str_cat.h"
+#include "absl/strings/str_replace.h"
 #include "absl/strings/string_view.h"
 #include "absl/types/span.h"
 #include "gmock/gmock.h"
@@ -128,15 +129,14 @@
   int temporal_unit_count = 0;
   LOG(INFO) << "Starting Temporal Unit OBU processing";
   while (continue_processing) {
-    std::list<AudioFrameWithData> audio_frames_for_temporal_unit;
-    std::list<ParameterBlockWithData> parameter_blocks_for_temporal_unit;
-    std::optional<int32_t> timestamp_for_temporal_unit;
+    std::optional<ObuProcessor::OutputTemporalUnit> output_temporal_unit;
     RETURN_IF_NOT_OK(obu_processor->ProcessTemporalUnit(
-        audio_frames_for_temporal_unit, parameter_blocks_for_temporal_unit,
-        timestamp_for_temporal_unit, continue_processing));
-    audio_frames.splice(audio_frames.end(), audio_frames_for_temporal_unit);
+        /*eos_is_end_of_sequence=*/true, output_temporal_unit,
+        continue_processing));
+    audio_frames.splice(audio_frames.end(),
+                        output_temporal_unit->output_audio_frames);
     parameter_blocks.splice(parameter_blocks.end(),
-                            parameter_blocks_for_temporal_unit);
+                            output_temporal_unit->output_parameter_blocks);
     temporal_unit_count++;
   }
   LOG(INFO) << "Processed " << temporal_unit_count << " Temporal Unit OBUs";
@@ -149,8 +149,9 @@
   return absl::OkStatus();
 }
 
-void AddLpcmCodecConfigWithIdAndSampleRate(
-    uint32_t codec_config_id, uint32_t sample_rate,
+void AddLpcmCodecConfig(
+    DecodedUleb128 codec_config_id, uint32_t num_samples_per_frame,
+    uint8_t sample_size, uint32_t sample_rate,
     absl::flat_hash_map<uint32_t, CodecConfigObu>& codec_config_obus) {
   // Initialize the Codec Config OBU.
   ASSERT_EQ(codec_config_obus.find(codec_config_id), codec_config_obus.end());
@@ -158,15 +159,26 @@
   CodecConfigObu obu(
       ObuHeader(), codec_config_id,
       {.codec_id = CodecConfig::kCodecIdLpcm,
-       .num_samples_per_frame = 8,
+       .num_samples_per_frame = num_samples_per_frame,
        .decoder_config = LpcmDecoderConfig{
            .sample_format_flags_bitmask_ = LpcmDecoderConfig::kLpcmLittleEndian,
-           .sample_size_ = 16,
+           .sample_size_ = sample_size,
            .sample_rate_ = sample_rate}});
   EXPECT_THAT(obu.Initialize(kOverrideAudioRollDistance), IsOk());
   codec_config_obus.emplace(codec_config_id, std::move(obu));
 }
 
+void AddLpcmCodecConfigWithIdAndSampleRate(
+    uint32_t codec_config_id, uint32_t sample_rate,
+    absl::flat_hash_map<uint32_t, CodecConfigObu>& codec_config_obus) {
+  // Many tests either don't care about the details. Or assumed these "default"
+  // values.
+  constexpr uint32_t kNumSamplesPerFrame = 8;
+  constexpr uint8_t kSampleSize = 16;
+  return AddLpcmCodecConfig(codec_config_id, kNumSamplesPerFrame, kSampleSize,
+                            sample_rate, codec_config_obus);
+}
+
 void AddOpusCodecConfigWithId(
     uint32_t codec_config_id,
     absl::flat_hash_map<uint32_t, CodecConfigObu>& codec_config_obus) {
@@ -305,30 +317,42 @@
     const std::vector<DecodedUleb128>& audio_element_ids,
     DecodedUleb128 common_parameter_id, DecodedUleb128 common_parameter_rate,
     std::list<MixPresentationObu>& mix_presentations) {
+  // Configure one of the simplest mix presentation. Mix presentations REQUIRE
+  // at least one sub-mix and a stereo layout.
+  AddMixPresentationObuWithConfigurableLayouts(
+      mix_presentation_id, audio_element_ids, common_parameter_id,
+      common_parameter_rate,
+      {LoudspeakersSsConventionLayout::kSoundSystemA_0_2_0}, mix_presentations);
+}
+
+void AddMixPresentationObuWithConfigurableLayouts(
+    DecodedUleb128 mix_presentation_id,
+    const std::vector<DecodedUleb128>& audio_element_ids,
+    DecodedUleb128 common_parameter_id, DecodedUleb128 common_parameter_rate,
+    const std::vector<LoudspeakersSsConventionLayout::SoundSystem>&
+        sound_system_layouts,
+    std::list<MixPresentationObu>& mix_presentations) {
   MixGainParamDefinition common_mix_gain_param_definition;
   common_mix_gain_param_definition.parameter_id_ = common_parameter_id;
   common_mix_gain_param_definition.parameter_rate_ = common_parameter_rate;
   common_mix_gain_param_definition.param_definition_mode_ = true;
   common_mix_gain_param_definition.default_mix_gain_ = 0;
+  std::vector<MixPresentationLayout> layouts;
+  for (const auto& sound_system : sound_system_layouts) {
+    layouts.push_back(
+        {.loudness_layout = {.layout_type =
+                                 Layout::kLayoutTypeLoudspeakersSsConvention,
+                             .specific_layout =
+                                 LoudspeakersSsConventionLayout{
+                                     .sound_system = sound_system,
+                                     .reserved = 0}},
+         .loudness = {
+             .info_type = 0, .integrated_loudness = 0, .digital_peak = 0}});
+  }
 
-  // Configure one of the simplest mix presentation. Mix presentations REQUIRE
-  // at least one sub-mix and a stereo layout.
   std::vector<MixPresentationSubMix> sub_mixes = {
-      {.num_audio_elements =
-           static_cast<DecodedUleb128>(audio_element_ids.size()),
-       .output_mix_gain = common_mix_gain_param_definition,
-       .num_layouts = 1,
-       .layouts = {
-           {.loudness_layout =
-                {.layout_type = Layout::kLayoutTypeLoudspeakersSsConvention,
-                 .specific_layout =
-                     LoudspeakersSsConventionLayout{
-                         .sound_system = LoudspeakersSsConventionLayout::
-                             kSoundSystemA_0_2_0,
-                         .reserved = 0}},
-            .loudness = {.info_type = 0,
-                         .integrated_loudness = 0,
-                         .digital_peak = 0}}}}};
+      {.output_mix_gain = common_mix_gain_param_definition,
+       .layouts = layouts}};
   for (const auto& audio_element_id : audio_element_ids) {
     sub_mixes[0].audio_elements.push_back({
         .audio_element_id = audio_element_id,
@@ -343,9 +367,9 @@
     });
   }
 
-  mix_presentations.push_back(MixPresentationObu(
-      ObuHeader(), mix_presentation_id,
-      /*count_label=*/0, {}, {}, sub_mixes.size(), sub_mixes));
+  mix_presentations.push_back(
+      MixPresentationObu(ObuHeader(), mix_presentation_id,
+                         /*count_label=*/0, {}, {}, sub_mixes));
 }
 
 void AddParamDefinitionWithMode0AndOneSubblock(
@@ -408,19 +432,17 @@
 std::string GetAndCleanupOutputFileName(absl::string_view suffix) {
   const testing::TestInfo* const test_info =
       testing::UnitTest::GetInstance()->current_test_info();
-  std::string file_name =
-      absl::StrCat(test_info->name(), "-", test_info->test_suite_name(), "-",
-                   test_info->test_case_name(), suffix);
+  std::string filename = absl::StrCat(test_info->name(), "-",
+                                      test_info->test_suite_name(), suffix);
 
-  // It is possible that the test suite name and test case name contain the '/'
-  // character. Replace it with '-' to form a legal file name.
-  std::transform(file_name.begin(), file_name.end(), file_name.begin(),
-                 [](char c) { return (c == '/') ? '-' : c; });
-  const std::filesystem::path test_specific_file_name =
-      std::filesystem::path(::testing::TempDir()) / file_name;
+  // It is possible that the test suite name contains the '/' character.
+  // Replace it with '-' to form a legal file name.
+  absl::StrReplaceAll({{"/", "-"}}, &filename);
+  const std::filesystem::path test_specific_filename =
+      std::filesystem::path(::testing::TempDir()) / filename;
 
-  std::filesystem::remove(test_specific_file_name);
-  return test_specific_file_name.string();
+  std::filesystem::remove(test_specific_filename);
+  return test_specific_filename.string();
 }
 
 std::string GetAndCreateOutputDirectory(absl::string_view suffix) {
@@ -474,8 +496,8 @@
   std::vector<DecodeSpecification> decode_specifications;
   for (const auto& mix_presentation :
        user_metadata.mix_presentation_metadata()) {
-    for (int i = 0; i < mix_presentation.num_sub_mixes(); ++i) {
-      for (int j = 0; j < mix_presentation.sub_mixes(i).num_layouts(); ++j) {
+    for (int i = 0; i < mix_presentation.sub_mixes_size(); ++i) {
+      for (int j = 0; j < mix_presentation.sub_mixes(i).layouts_size(); ++j) {
         DecodeSpecification decode_specification;
         decode_specification.mix_presentation_id =
             mix_presentation.mix_presentation_id();
diff --git a/iamf/cli/tests/cli_test_utils.h b/iamf/cli/tests/cli_test_utils.h
index 5d74c94..eb86a9a 100644
--- a/iamf/cli/tests/cli_test_utils.h
+++ b/iamf/cli/tests/cli_test_utils.h
@@ -91,6 +91,19 @@
 /*!\brief Adds a configurable LPCM `CodecConfigObu` to the output argument.
  *
  * \param codec_config_id `codec_config_id` of the OBU to create.
+ * \param num_samples_per_frame Number of samples per frame.
+ * \param sample_size Sample size.
+ * \param sample_rate `sample_rate` of the OBU to create.
+ * \param codec_config_obus Map to add the OBU to keyed by `codec_config_id`.
+ */
+void AddLpcmCodecConfig(
+    DecodedUleb128 codec_config_id, uint32_t num_samples_per_frame,
+    uint8_t sample_size, uint32_t sample_rate,
+    absl::flat_hash_map<uint32_t, CodecConfigObu>& codec_config_obus);
+
+/*!\brief Adds a configurable LPCM `CodecConfigObu` to the output argument.
+ *
+ * \param codec_config_id `codec_config_id` of the OBU to create.
  * \param sample_rate `sample_rate` of the OBU to create.
  * \param codec_config_obus Map to add the OBU to keyed by `codec_config_id`.
  */
@@ -162,13 +175,32 @@
  *        created OBU.
  * \param common_parameter_rate `parameter_rate` of all parameters within the
  *        created OBU.
- * \param mix_presentations List to add OBU to.
+ * \param output_mix_presentations List to add OBU to.
  */
 void AddMixPresentationObuWithAudioElementIds(
     DecodedUleb128 mix_presentation_id,
     const std::vector<DecodedUleb128>& audio_element_id,
     DecodedUleb128 common_parameter_id, DecodedUleb128 common_parameter_rate,
-    std::list<MixPresentationObu>& mix_presentations);
+    std::list<MixPresentationObu>& output_mix_presentations);
+
+/*!\brief Adds a configurable `MixPresentationObu` to the output argument.
+ *
+ * \param mix_presentation_id `mix_presentation_id` of the OBU to create.
+ * \param audio_element_ids `audio_element_id`s of the OBU to create.
+ * \param common_parameter_id `parameter_id` of all parameters within the
+ *        created OBU.
+ * \param common_parameter_rate `parameter_rate` of all parameters within the
+ *        created OBU.
+ * \param sound_system_layouts `sound_system`s of the OBU to create.
+ * \param output_mix_presentations List to add OBU to.
+ */
+void AddMixPresentationObuWithConfigurableLayouts(
+    DecodedUleb128 mix_presentation_id,
+    const std::vector<DecodedUleb128>& audio_element_id,
+    DecodedUleb128 common_parameter_id, DecodedUleb128 common_parameter_rate,
+    const std::vector<LoudspeakersSsConventionLayout::SoundSystem>&
+        sound_system_layouts,
+    std::list<MixPresentationObu>& output_mix_presentations);
 
 /*!\brief Adds a configurable mix gain param definition to the output argument.
  *
@@ -550,7 +582,7 @@
       : ObuSequencerBase(leb_generator, include_temporal_delimiters,
                          delay_descriptors_until_first_untrimmed_sample) {}
 
-  MOCK_METHOD(void, Abort, (), (override));
+  MOCK_METHOD(void, AbortDerived, (), (override));
 
   MOCK_METHOD(absl::Status, PushSerializedDescriptorObus,
               (uint32_t common_samples_per_frame, uint32_t common_sample_rate,
@@ -564,7 +596,10 @@
                absl::Span<const uint8_t> temporal_unit),
               (override));
 
-  MOCK_METHOD(void, Flush, (), (override));
+  MOCK_METHOD(absl::Status, PushFinalizedDescriptorObus,
+              (absl::Span<const uint8_t> descriptor_obus), (override));
+
+  MOCK_METHOD(void, CloseDerived, (), (override));
 };
 
 }  // namespace iamf_tools
diff --git a/iamf/cli/tests/cli_util_test.cc b/iamf/cli/tests/cli_util_test.cc
index 0b3fee6..b9a56f8 100644
--- a/iamf/cli/tests/cli_util_test.cc
+++ b/iamf/cli/tests/cli_util_test.cc
@@ -48,7 +48,6 @@
 
 constexpr DecodedUleb128 kCodecConfigId = 21;
 constexpr DecodedUleb128 kAudioElementId = 100;
-constexpr DecodedUleb128 kSecondAudioElementId = 101;
 constexpr DecodedUleb128 kMixPresentationId = 100;
 constexpr DecodedUleb128 kParameterId = 99999;
 constexpr DecodedUleb128 kParameterRate = 48000;
@@ -62,12 +61,9 @@
   std::vector<std::vector<int32_t>> frame_to_write = {{0x7f000000, 0x7e000000},
                                                       {0x7f000000, 0x7e000000}};
   const uint8_t kBitDepth = 24;
-  const uint32_t kSamplesToTrimAtStart = 0;
-  const uint32_t kSamplesToTrimAtEnd = 0;
   const bool kBigEndian = false;
   std::vector<uint8_t> output_buffer;
-  EXPECT_THAT(WritePcmFrameToBuffer(frame_to_write, kSamplesToTrimAtStart,
-                                    kSamplesToTrimAtEnd, kBitDepth, kBigEndian,
+  EXPECT_THAT(WritePcmFrameToBuffer(frame_to_write, kBitDepth, kBigEndian,
                                     output_buffer),
               IsOk());
 
@@ -78,12 +74,9 @@
   std::vector<std::vector<int32_t>> frame_to_write = {{0x7f001200, 0x7e003400},
                                                       {0x7f005600, 0x7e007800}};
   const uint8_t kBitDepth = 24;
-  const uint32_t kSamplesToTrimAtStart = 0;
-  const uint32_t kSamplesToTrimAtEnd = 0;
   const bool kBigEndian = true;
   std::vector<uint8_t> output_buffer;
-  EXPECT_THAT(WritePcmFrameToBuffer(frame_to_write, kSamplesToTrimAtStart,
-                                    kSamplesToTrimAtEnd, kBitDepth, kBigEndian,
+  EXPECT_THAT(WritePcmFrameToBuffer(frame_to_write, kBitDepth, kBigEndian,
                                     output_buffer),
               IsOk());
 
@@ -96,12 +89,9 @@
   std::vector<std::vector<int32_t>> frame_to_write = {{0x7f001200, 0x7e003400},
                                                       {0x7f005600, 0x7e007800}};
   const uint8_t kBitDepth = 24;
-  const uint32_t kSamplesToTrimAtStart = 0;
-  const uint32_t kSamplesToTrimAtEnd = 0;
   const bool kBigEndian = false;
   std::vector<uint8_t> output_buffer;
-  EXPECT_THAT(WritePcmFrameToBuffer(frame_to_write, kSamplesToTrimAtStart,
-                                    kSamplesToTrimAtEnd, kBitDepth, kBigEndian,
+  EXPECT_THAT(WritePcmFrameToBuffer(frame_to_write, kBitDepth, kBigEndian,
                                     output_buffer),
               IsOk());
 
@@ -110,35 +100,14 @@
   EXPECT_EQ(output_buffer, kExpectedBytes);
 }
 
-TEST(WritePcmFrameToBuffer, TrimsSamples) {
-  std::vector<std::vector<int32_t>> frame_to_write = {{0x7f001200, 0x7e003400},
-                                                      {0x7f005600, 0x7e007800}};
-  const uint8_t kBitDepth = 24;
-  const uint32_t kSamplesToTrimAtStart = 1;
-  const uint32_t kSamplesToTrimAtEnd = 0;
-  const bool kBigEndian = false;
-  std::vector<uint8_t> output_buffer;
-  EXPECT_THAT(WritePcmFrameToBuffer(frame_to_write, kSamplesToTrimAtStart,
-                                    kSamplesToTrimAtEnd, kBitDepth, kBigEndian,
-                                    output_buffer),
-              IsOk());
-
-  const std::vector<uint8_t> kExpectedBytes = {0x56, 0x00, 0x7f,
-                                               0x78, 0x00, 0x7e};
-  EXPECT_EQ(output_buffer, kExpectedBytes);
-}
-
 TEST(WritePcmFrameToBuffer, RequiresBitDepthIsMultipleOfEight) {
   std::vector<std::vector<int32_t>> frame_to_write = {{0x7f001200, 0x7e003400},
                                                       {0x7f005600, 0x7e007800}};
   const uint8_t kBitDepth = 23;
-  const uint32_t kSamplesToTrimAtStart = 0;
-  const uint32_t kSamplesToTrimAtEnd = 0;
   const bool kBigEndian = false;
   std::vector<uint8_t> output_buffer;
 
-  EXPECT_FALSE(WritePcmFrameToBuffer(frame_to_write, kSamplesToTrimAtStart,
-                                     kSamplesToTrimAtEnd, kBitDepth, kBigEndian,
+  EXPECT_FALSE(WritePcmFrameToBuffer(frame_to_write, kBitDepth, kBigEndian,
                                      output_buffer)
                    .ok());
 }
diff --git a/iamf/cli/tests/demixing_module_test.cc b/iamf/cli/tests/demixing_module_test.cc
index 9d1768b..503d258 100644
--- a/iamf/cli/tests/demixing_module_test.cc
+++ b/iamf/cli/tests/demixing_module_test.cc
@@ -48,10 +48,13 @@
 namespace {
 
 using ::absl_testing::IsOk;
+using ::absl_testing::IsOkAndHolds;
 using enum ChannelLabel::Label;
-using testing::DoubleEq;
-using testing::DoubleNear;
-using testing::Pointwise;
+using ::testing::DoubleEq;
+using ::testing::DoubleNear;
+using ::testing::IsEmpty;
+using ::testing::Not;
+using ::testing::Pointwise;
 
 constexpr DecodedUleb128 kAudioElementId = 137;
 constexpr std::array<uint8_t, 12> kReconGainValues = {
@@ -263,7 +266,21 @@
   EXPECT_TRUE(demixer->empty());
 }
 
-TEST(DemixAudioSamples, OutputContainsOriginalAndDemixedSamples) {
+TEST(DemixOriginalAudioSamples, ReturnsErrorAfterCreateForReconstruction) {
+  absl::flat_hash_map<DecodedUleb128, AudioElementWithData> audio_elements;
+  InitAudioElementWithLabelsAndLayers(
+      {{kMonoSubstreamId, {kMono}}, {kL2SubstreamId, {kL2}}},
+      {ChannelAudioLayerConfig::kLayoutMono,
+       ChannelAudioLayerConfig::kLayoutStereo},
+      audio_elements);
+  auto demixing_module =
+      DemixingModule::CreateForReconstruction(audio_elements);
+  ASSERT_THAT(demixing_module, IsOk());
+
+  EXPECT_THAT(demixing_module->DemixOriginalAudioSamples({}), Not(IsOk()));
+}
+
+TEST(DemixDecodedAudioSamples, OutputContainsOriginalAndDemixedSamples) {
   absl::flat_hash_map<DecodedUleb128, AudioElementWithData> audio_elements;
   InitAudioElementWithLabelsAndLayers(
       {{kMonoSubstreamId, {kMono}}, {kL2SubstreamId, {kL2}}},
@@ -290,23 +307,18 @@
   auto demixing_module =
       DemixingModule::CreateForReconstruction(audio_elements);
   ASSERT_THAT(demixing_module, IsOk());
-  IdLabeledFrameMap id_labeled_frame;
-  IdLabeledFrameMap id_to_labeled_decoded_frame;
-  EXPECT_THAT(demixing_module->DemixAudioSamples({}, decoded_audio_frames,
-                                                 id_labeled_frame,
-                                                 id_to_labeled_decoded_frame),
-              IsOk());
+  const auto id_to_labeled_decoded_frame =
+      demixing_module->DemixDecodedAudioSamples(decoded_audio_frames);
+  ASSERT_THAT(id_to_labeled_decoded_frame, IsOk());
+  ASSERT_TRUE(id_to_labeled_decoded_frame->contains(kAudioElementId));
 
-  const auto& labeled_frame = id_to_labeled_decoded_frame.at(kAudioElementId);
+  const auto& labeled_frame = id_to_labeled_decoded_frame->at(kAudioElementId);
   EXPECT_TRUE(labeled_frame.label_to_samples.contains(kL2));
   EXPECT_TRUE(labeled_frame.label_to_samples.contains(kMono));
   EXPECT_TRUE(labeled_frame.label_to_samples.contains(kDemixedR2));
-  // When being used for reconstruction the original audio frames are not
-  // output.
-  EXPECT_FALSE(id_labeled_frame.contains(kAudioElementId));
 }
 
-TEST(DemixAudioSamples, OutputEchoesTimingInformation) {
+TEST(DemixDecodedAudioSamples, OutputEchoesTimingInformation) {
   // These values are not very sensible, but as long as they are consistent
   // between related frames it is OK.
   const DecodedUleb128 kExpectedStartTimestamp = 99;
@@ -340,14 +352,13 @@
   const auto demixing_module =
       DemixingModule::CreateForReconstruction(audio_elements);
   ASSERT_THAT(demixing_module, IsOk());
-  IdLabeledFrameMap unused_id_labeled_frame;
-  IdLabeledFrameMap id_to_labeled_decoded_frame;
-  EXPECT_THAT(demixing_module->DemixAudioSamples({}, decoded_audio_frames,
-                                                 unused_id_labeled_frame,
-                                                 id_to_labeled_decoded_frame),
-              IsOk());
 
-  const auto& labeled_frame = id_to_labeled_decoded_frame.at(kAudioElementId);
+  const auto id_to_labeled_decoded_frame =
+      demixing_module->DemixDecodedAudioSamples(decoded_audio_frames);
+  ASSERT_THAT(id_to_labeled_decoded_frame, IsOk());
+  ASSERT_TRUE(id_to_labeled_decoded_frame->contains(kAudioElementId));
+
+  const auto& labeled_frame = id_to_labeled_decoded_frame->at(kAudioElementId);
   EXPECT_EQ(labeled_frame.end_timestamp, kExpectedEndTimestamp);
   EXPECT_EQ(labeled_frame.samples_to_trim_at_end,
             kExpectedNumSamplesToTrimAtEnd);
@@ -355,7 +366,7 @@
             kExpectedNumSamplesToTrimAtStart);
 }
 
-TEST(DemixAudioSamples, OutputEchoesOriginalLabels) {
+TEST(DemixDecodedAudioSamples, OutputEchoesOriginalLabels) {
   absl::flat_hash_map<DecodedUleb128, AudioElementWithData> audio_elements;
   InitAudioElementWithLabelsAndLayers(
       {{kMonoSubstreamId, {kMono}}, {kL2SubstreamId, {kL2}}},
@@ -384,14 +395,13 @@
   ASSERT_THAT(demixing_module, IsOk());
 
   IdLabeledFrameMap unused_id_labeled_frame;
-  IdLabeledFrameMap id_to_labeled_decoded_frame;
-  EXPECT_THAT(demixing_module->DemixAudioSamples({}, decoded_audio_frames,
-                                                 unused_id_labeled_frame,
-                                                 id_to_labeled_decoded_frame),
-              IsOk());
+  const auto id_to_labeled_decoded_frame =
+      demixing_module->DemixDecodedAudioSamples(decoded_audio_frames);
+  ASSERT_THAT(id_to_labeled_decoded_frame, IsOk());
+  ASSERT_TRUE(id_to_labeled_decoded_frame->contains(kAudioElementId));
 
   // Examine the demixed frame.
-  const auto& labeled_frame = id_to_labeled_decoded_frame.at(kAudioElementId);
+  const auto& labeled_frame = id_to_labeled_decoded_frame->at(kAudioElementId);
   constexpr std::array<int32_t, 3> kExpectedMonoSamples = {1, 2, 3};
   constexpr std::array<int32_t, 3> kExpectedL2Samples = {9, 10, 11};
   EXPECT_THAT(
@@ -402,7 +412,7 @@
       Pointwise(InternalSampleMatchesIntegralSample(), kExpectedL2Samples));
 }
 
-TEST(DemixAudioSamples, OutputHasReconstructedLayers) {
+TEST(DemixDecodedAudioSamples, OutputHasReconstructedLayers) {
   absl::flat_hash_map<DecodedUleb128, AudioElementWithData> audio_elements;
 
   InitAudioElementWithLabelsAndLayers(
@@ -431,21 +441,19 @@
       DemixingModule::CreateForReconstruction(audio_elements);
   ASSERT_THAT(demixing_module, IsOk());
 
-  IdLabeledFrameMap unused_id_time_labeled_frame;
-  IdLabeledFrameMap id_to_labeled_decoded_frame;
-  EXPECT_THAT(demixing_module->DemixAudioSamples({}, decoded_audio_frames,
-                                                 unused_id_time_labeled_frame,
-                                                 id_to_labeled_decoded_frame),
-              IsOk());
+  const auto id_to_labeled_decoded_frame =
+      demixing_module->DemixDecodedAudioSamples(decoded_audio_frames);
+  ASSERT_THAT(id_to_labeled_decoded_frame, IsOk());
+  ASSERT_TRUE(id_to_labeled_decoded_frame->contains(kAudioElementId));
 
   // Examine the demixed frame.
-  const auto& labeled_frame = id_to_labeled_decoded_frame.at(kAudioElementId);
+  const auto& labeled_frame = id_to_labeled_decoded_frame->at(kAudioElementId);
   // D_R2 =  M - (L2 - 6 dB)  + 6 dB.
   EXPECT_THAT(labeled_frame.label_to_samples.at(kDemixedR2),
               Pointwise(InternalSampleMatchesIntegralSample(), {500}));
 }
 
-TEST(DemixAudioSamples, OutputContainsReconGainAndLayerInfo) {
+TEST(DemixDecodedAudioSamples, OutputContainsReconGainAndLayerInfo) {
   absl::flat_hash_map<DecodedUleb128, AudioElementWithData> audio_elements;
   InitAudioElementWithLabelsAndLayers(
       {{kMonoSubstreamId, {kMono}}, {kL2SubstreamId, {kL2}}},
@@ -479,14 +487,12 @@
   const auto demixing_module =
       DemixingModule::CreateForReconstruction(audio_elements);
   ASSERT_THAT(demixing_module, IsOk());
-  IdLabeledFrameMap id_labeled_frame;
-  IdLabeledFrameMap id_to_labeled_decoded_frame;
-  EXPECT_THAT(demixing_module->DemixAudioSamples({}, decoded_audio_frames,
-                                                 id_labeled_frame,
-                                                 id_to_labeled_decoded_frame),
-              IsOk());
+  const auto id_to_labeled_decoded_frame =
+      demixing_module->DemixDecodedAudioSamples(decoded_audio_frames);
+  ASSERT_THAT(id_to_labeled_decoded_frame, IsOk());
+  ASSERT_TRUE(id_to_labeled_decoded_frame->contains(kAudioElementId));
 
-  const auto& labeled_frame = id_to_labeled_decoded_frame.at(kAudioElementId);
+  const auto& labeled_frame = id_to_labeled_decoded_frame->at(kAudioElementId);
   EXPECT_TRUE(labeled_frame.label_to_samples.contains(kL2));
   EXPECT_TRUE(labeled_frame.label_to_samples.contains(kMono));
   EXPECT_TRUE(labeled_frame.label_to_samples.contains(kDemixedR2));
@@ -995,19 +1001,18 @@
         .label_to_samples[label] = expected_demixed_samples_as_internal_type;
   }
 
-  void TestDemixing(int expected_number_of_down_mixers) {
-    IdLabeledFrameMap unused_id_to_labeled_frame, id_to_labeled_decoded_frame;
-
+  void TestLosslessDemixing(int expected_number_of_down_mixers) {
     TestCreateDemixingModule(expected_number_of_down_mixers);
 
-    EXPECT_THAT(demixing_module_->DemixAudioSamples(
-                    audio_frames_, decoded_audio_frames_,
-                    unused_id_to_labeled_frame, id_to_labeled_decoded_frame),
-                IsOk());
+    const auto id_to_labeled_decoded_frame =
+        demixing_module_->DemixDecodedAudioSamples(decoded_audio_frames_);
+    ASSERT_THAT(id_to_labeled_decoded_frame, IsOk());
+    ASSERT_TRUE(id_to_labeled_decoded_frame->contains(kAudioElementId));
 
     // Check that the demixed samples have the correct values.
     const auto& actual_label_to_samples =
-        id_to_labeled_decoded_frame[kAudioElementId].label_to_samples;
+        id_to_labeled_decoded_frame->at(kAudioElementId).label_to_samples;
+
     const auto& expected_label_to_samples =
         expected_id_to_labeled_decoded_frame_[kAudioElementId].label_to_samples;
     EXPECT_EQ(actual_label_to_samples.size(), expected_label_to_samples.size());
@@ -1018,6 +1023,15 @@
       EXPECT_THAT(samples, Pointwise(DoubleNear(kErrorTolerance),
                                      expected_label_to_samples.at(label)));
     }
+
+    // Also, since this is lossless, we expect demixing the original samples
+    // should give the same result.
+    const auto id_to_labeled_frame =
+        demixing_module_->DemixOriginalAudioSamples(audio_frames_);
+    ASSERT_THAT(id_to_labeled_frame, IsOk());
+    ASSERT_TRUE(id_to_labeled_frame->contains(kAudioElementId));
+    EXPECT_EQ(id_to_labeled_frame->at(kAudioElementId).label_to_samples,
+              actual_label_to_samples);
   }
 
  protected:
@@ -1027,22 +1041,22 @@
   IdLabeledFrameMap expected_id_to_labeled_decoded_frame_;
 };  // namespace
 
-TEST(DemixingModule, DemixingAudioSamplesSucceedsWithEmptyInputs) {
+TEST(DemixingModule, DemixingOriginalAudioSamplesSucceedsWithEmptyInputs) {
   const auto demixing_module =
       DemixingModule::CreateForDownMixingAndReconstruction({});
   ASSERT_THAT(demixing_module, IsOk());
 
-  // Call `DemixAudioSamples()`.
-  IdLabeledFrameMap id_to_labeled_frame, id_to_labeled_decoded_frame;
-  EXPECT_THAT(demixing_module->DemixAudioSamples(
-                  /*audio_frames=*/{},
-                  /*decoded_audio_frames=*/{}, id_to_labeled_frame,
-                  id_to_labeled_decoded_frame),
-              IsOk());
+  EXPECT_THAT(demixing_module->DemixOriginalAudioSamples({}),
+              IsOkAndHolds(IsEmpty()));
+}
 
-  // Expect empty outputs.
-  EXPECT_TRUE(id_to_labeled_frame.empty());
-  EXPECT_TRUE(id_to_labeled_decoded_frame.empty());
+TEST(DemixingModule, DemixingDecodedAudioSamplesSucceedsWithEmptyInputs) {
+  const auto demixing_module =
+      DemixingModule::CreateForDownMixingAndReconstruction({});
+  ASSERT_THAT(demixing_module, IsOk());
+
+  EXPECT_THAT(demixing_module->DemixDecodedAudioSamples({}),
+              IsOkAndHolds(IsEmpty()));
 }
 
 TEST_F(DemixingModuleTest, AmbisonicsHasNoDemixers) {
@@ -1053,7 +1067,7 @@
   ConfigureLosslessAudioFrameAndDecodedAudioFrame({kA2}, {{1}});
   ConfigureLosslessAudioFrameAndDecodedAudioFrame({kA3}, {{1}});
 
-  TestDemixing(0);
+  TestLosslessDemixing(0);
 }
 
 TEST_F(DemixingModuleTest, S1ToS2Demixer) {
@@ -1069,11 +1083,11 @@
   // D_R2 =  M - (L2 - 6 dB)  + 6 dB.
   ConfiguredExpectedDemixingChannelFrame(kDemixedR2, {500, 1000});
 
-  TestDemixing(1);
+  TestLosslessDemixing(1);
 }
 
 TEST_F(DemixingModuleTest,
-       DemixAudioSamplesReturnsErrorIfAudioFrameIsMissingPcmSamples) {
+       DemixOriginalAudioSamplesReturnsErrorIfAudioFrameIsMissingPcmSamples) {
   ConfigureAudioFrameMetadata({kL2, kR2});
   ConfigureLosslessAudioFrameAndDecodedAudioFrame({kMono}, {{750}, {1500}});
   ConfigureLosslessAudioFrameAndDecodedAudioFrame({kL2}, {{1000}, {2000}});
@@ -1082,11 +1096,8 @@
   // Destroy the raw samples.
   audio_frames_.back().pcm_samples = std::nullopt;
 
-  EXPECT_FALSE(demixing_module_
-                   ->DemixAudioSamples(audio_frames_, decoded_audio_frames_,
-                                       unused_id_to_labeled_frame,
-                                       id_to_labeled_decoded_frame)
-                   .ok());
+  EXPECT_THAT(demixing_module_->DemixOriginalAudioSamples(audio_frames_),
+              Not(IsOk()));
 }
 
 TEST_F(DemixingModuleTest, S2ToS3Demixer) {
@@ -1108,7 +1119,7 @@
   ConfiguredExpectedDemixingChannelFrame(kDemixedL3, {-1344, 993});
   ConfiguredExpectedDemixingChannelFrame(kDemixedR3, {-1344, 993});
 
-  TestDemixing(1);
+  TestLosslessDemixing(1);
 }
 
 TEST_F(DemixingModuleTest, S3ToS5AndTf2ToT2Demixers) {
@@ -1143,7 +1154,7 @@
   ConfiguredExpectedDemixingChannelFrame(kDemixedLtf2, {-1165});
   ConfiguredExpectedDemixingChannelFrame(kDemixedRtf2, {-165});
 
-  TestDemixing(2);
+  TestLosslessDemixing(2);
 }
 
 TEST_F(DemixingModuleTest, S5ToS7Demixer) {
@@ -1176,7 +1187,7 @@
   ConfiguredExpectedDemixingChannelFrame(kDemixedLrs7, {8000});
   ConfiguredExpectedDemixingChannelFrame(kDemixedRrs7, {7000});
 
-  TestDemixing(1);
+  TestLosslessDemixing(1);
 }
 
 TEST_F(DemixingModuleTest, T2ToT4Demixer) {
@@ -1205,7 +1216,7 @@
   ConfiguredExpectedDemixingChannelFrame(kDemixedLtb4, {9000});
   ConfiguredExpectedDemixingChannelFrame(kDemixedRtb4, {18000});
 
-  TestDemixing(1);
+  TestLosslessDemixing(1);
 }
 
 }  // namespace
diff --git a/iamf/cli/tests/iamf_encoder_test.cc b/iamf/cli/tests/iamf_encoder_test.cc
index 638c74d..45cf7ba 100644
--- a/iamf/cli/tests/iamf_encoder_test.cc
+++ b/iamf/cli/tests/iamf_encoder_test.cc
@@ -107,9 +107,7 @@
       R"pb(
         mix_presentation_id: 42
         count_label: 0
-        num_sub_mixes: 1
         sub_mixes {
-          num_audio_elements: 1
           audio_elements {
             audio_element_id: 300
             rendering_config {
@@ -134,7 +132,6 @@
             }
             default_mix_gain: 0
           }
-          num_layouts: 1
           layouts {
             loudness_layout {
               layout_type: LAYOUT_TYPE_LOUDSPEAKERS_SS_CONVENTION
diff --git a/iamf/cli/tests/obu_processor_test.cc b/iamf/cli/tests/obu_processor_test.cc
index 0982765..4ce2376 100644
--- a/iamf/cli/tests/obu_processor_test.cc
+++ b/iamf/cli/tests/obu_processor_test.cc
@@ -12,9 +12,11 @@
 
 #include "iamf/cli/obu_processor.h"
 
+#include <array>
 #include <cstddef>
 #include <cstdint>
 #include <filesystem>
+#include <iterator>
 #include <list>
 #include <memory>
 #include <optional>
@@ -59,6 +61,7 @@
 namespace {
 
 using ::absl_testing::IsOk;
+using ::absl_testing::IsOkAndHolds;
 using ::testing::IsNull;
 using ::testing::Not;
 using ::testing::NotNull;
@@ -76,6 +79,8 @@
 constexpr DecodedUleb128 kSecondMixPresentationId = 4;
 constexpr DecodedUleb128 kThirdMixPresentationId = 5;
 constexpr DecodedUleb128 kCommonMixGainParameterId = 999;
+constexpr uint32_t kFrameSize = 1024;
+constexpr uint32_t kBitDepth = 16;
 constexpr DecodedUleb128 kSampleRate = 48000;
 constexpr DecodedUleb128 kCommonParameterRate = kSampleRate;
 
@@ -85,8 +90,8 @@
 constexpr int64_t kBufferCapacity = 1024;
 
 constexpr std::optional<uint8_t> kNoOutputFileBitDepthOverride = std::nullopt;
-constexpr uint8_t kOutputBitDepth24 = 24;
-constexpr uint8_t kOutputBitDepth32 = 32;
+constexpr std::array<uint8_t, 16> kArbitraryAudioFrame = {
+    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
 
 std::vector<uint8_t> AddSequenceHeaderAndSerializeObusExpectOk(
     const std::list<const ObuBase*>& input_ia_sequence_without_header) {
@@ -111,7 +116,7 @@
   };
 }
 
-TEST(ProcessDescriptorObus, InvalidWithoutIaSequenceHeader) {
+TEST(ProcessDescriptorObus, FailsWithEmptyBitstream) {
   const std::vector<uint8_t> bitstream_without_ia_sequence_header =
       SerializeObusExpectOk({});
   IASequenceHeaderObu ia_sequence_header;
@@ -123,13 +128,16 @@
   auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
       kBufferCapacity,
       absl::MakeConstSpan(bitstream_without_ia_sequence_header));
-  bool insufficient_data = false;
+  bool insufficient_data;
   EXPECT_FALSE(ObuProcessor::ProcessDescriptorObus(
                    /*is_exhaustive_and_exact=*/false, *read_bit_buffer,
                    ia_sequence_header, codec_config_obu,
                    audio_elements_with_data, mix_presentation_obus,
                    insufficient_data)
                    .ok());
+  // There's no data (and `is_exhaustive_and_exact` is false), so we need more
+  // data to proceed.
+  EXPECT_TRUE(insufficient_data);
 }
 
 TEST(ProcessDescriptorObus, CollectsCodecConfigsBeforeATemporalUnit) {
@@ -151,7 +159,7 @@
 
   auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
       kBufferCapacity, absl::MakeConstSpan(two_codec_configs_and_audio_frame));
-  bool insufficient_data = false;
+  bool insufficient_data;
   EXPECT_THAT(
       ObuProcessor::ProcessDescriptorObus(
           /*is_exhaustive_and_exact=*/false, *read_bit_buffer,
@@ -162,6 +170,10 @@
   EXPECT_EQ(output_codec_config_obus.size(), 2);
   EXPECT_TRUE(output_codec_config_obus.contains(kFirstCodecConfigId));
   EXPECT_TRUE(output_codec_config_obus.contains(kSecondCodecConfigId));
+  // `insufficient_data` is false because we have successfully read all provided
+  // descriptor obus AND `is_exhaustive_and_exact` is true, meaning that the
+  // caller has indicated that there are no future Descriptor OBUs coming.
+  EXPECT_FALSE(insufficient_data);
 }
 
 TEST(ProcessDescriptorObus, CollectsCodecConfigsAtEndOfBitstream) {
@@ -181,13 +193,14 @@
   auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
       kBufferCapacity,
       absl::MakeConstSpan(two_codec_configs_at_end_of_bitstream));
-  bool insufficient_data = false;
+  bool insufficient_data;
   EXPECT_THAT(
       ObuProcessor::ProcessDescriptorObus(
           /*is_exhaustive_and_exact=*/true, *read_bit_buffer,
           ia_sequence_header, codec_config_obus, audio_elements_with_data,
           mix_presentation_obus, insufficient_data),
       IsOk());
+  // `is_exhaustive_and_exact` is true so it could not be a more-data situation.
   EXPECT_FALSE(insufficient_data);
 
   EXPECT_EQ(codec_config_obus.size(), 2);
@@ -214,13 +227,15 @@
       kBufferCapacity,
       absl::MakeConstSpan(two_codec_configs_at_end_of_bitstream));
   auto start_position = read_bit_buffer->Tell();
-  bool insufficient_data = false;
+  bool insufficient_data;
   EXPECT_THAT(
       ObuProcessor::ProcessDescriptorObus(
           /*is_exhaustive_and_exact=*/false, *read_bit_buffer,
           ia_sequence_header, codec_config_obus, audio_elements_with_data,
           mix_presentation_obus, insufficient_data),
       Not(IsOk()));
+  // `is_exhaustive_and_exact` is false so we won't know it's the end of the
+  // bitstream until we see a temporal unit.  Need more data to know we're done.
   EXPECT_TRUE(insufficient_data);
   EXPECT_EQ(codec_config_obus.size(), 0);
   EXPECT_EQ(read_bit_buffer->Tell(), start_position);
@@ -237,7 +252,7 @@
 
   auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
       kBufferCapacity, absl::MakeConstSpan(only_ia_sequence_header));
-  bool insufficient_data = false;
+  bool insufficient_data;
   EXPECT_THAT(
       ObuProcessor::ProcessDescriptorObus(
           /*is_exhaustive_and_exact=*/true, *read_bit_buffer,
@@ -249,6 +264,7 @@
             ProfileVersion::kIamfSimpleProfile);
   EXPECT_EQ(ia_sequence_header.GetAdditionalProfile(),
             ProfileVersion::kIamfBaseProfile);
+  EXPECT_FALSE(insufficient_data);
 }
 
 TEST(ProcessDescriptorObus, DescriptorObusMustStartWithIaSequenceHeader) {
@@ -272,7 +288,7 @@
   auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
       kBufferCapacity,
       absl::MakeConstSpan(ia_sequence_header_then_codec_config));
-  bool insufficient_data = false;
+  bool insufficient_data;
   EXPECT_THAT(ObuProcessor::ProcessDescriptorObus(
                   /*is_exhaustive_and_exact=*/true, *read_bit_buffer,
                   unused_ia_sequence_header, unused_codec_config_obus,
@@ -295,6 +311,9 @@
                    unused_audio_elements_with_data,
                    unused_mix_presentation_obus, insufficient_data)
                    .ok());
+  // `insufficient_data` is false as the error was due to an invalid ordering of
+  // OBUs, rather than not having enough data.
+  EXPECT_FALSE(insufficient_data);
 }
 
 TEST(ProcessDescriptorObus, SucceedsWithSuccessiveRedundantSequenceHeaders) {
@@ -311,7 +330,7 @@
 
   auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
       kBufferCapacity, absl::MakeConstSpan(bitstream));
-  bool insufficient_data = false;
+  bool insufficient_data;
   EXPECT_THAT(
       ObuProcessor::ProcessDescriptorObus(
           /*is_exhaustive_and_exact=*/true, *read_bit_buffer,
@@ -343,7 +362,7 @@
 
   auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
       kBufferCapacity, absl::MakeConstSpan(buffer));
-  bool insufficient_data = false;
+  bool insufficient_data;
   EXPECT_THAT(
       ObuProcessor::ProcessDescriptorObus(
           /*is_exhaustive_and_exact=*/true, *read_bit_buffer,
@@ -376,7 +395,7 @@
   auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
       kBufferCapacity,
       absl::MakeConstSpan(ia_sequence_header_with_codec_configs));
-  bool insufficient_data = false;
+  bool insufficient_data;
   EXPECT_THAT(
       ObuProcessor::ProcessDescriptorObus(
           /*is_exhaustive_and_exact=*/true, *read_bit_buffer,
@@ -427,7 +446,7 @@
   auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
       kBufferCapacity,
       absl::MakeConstSpan(zeroth_order_ambisonics_descriptor_obus));
-  bool insufficient_data = false;
+  bool insufficient_data;
 
   EXPECT_THAT(
       ObuProcessor::ProcessDescriptorObus(
@@ -464,7 +483,7 @@
       kBufferCapacity,
       absl::MakeConstSpan(zeroth_order_ambisonics_descriptor_obus));
   auto start_position = read_bit_buffer->Tell();
-  bool insufficient_data = false;
+  bool insufficient_data;
 
   EXPECT_THAT(
       ObuProcessor::ProcessDescriptorObus(
@@ -473,6 +492,7 @@
           mix_presentation_obus, insufficient_data),
       Not(IsOk()));
 
+  // We've received a valid bitstream so far but not complete.
   EXPECT_TRUE(insufficient_data);
   EXPECT_EQ(codec_config_obus.size(), 0);
   EXPECT_EQ(audio_elements_with_data.size(), 0);
@@ -502,7 +522,7 @@
   auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
       kBufferCapacity, absl::MakeConstSpan(bitstream));
   auto start_position = read_bit_buffer->Tell();
-  bool insufficient_data = false;
+  bool insufficient_data;
 
   EXPECT_THAT(
       ObuProcessor::ProcessDescriptorObus(
@@ -538,7 +558,7 @@
 
   auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
       kBufferCapacity, absl::MakeConstSpan(bitstream));
-  bool insufficient_data = false;
+  bool insufficient_data;
 
   EXPECT_THAT(
       ObuProcessor::ProcessDescriptorObus(
@@ -586,7 +606,7 @@
   auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
       kBufferCapacity, absl::MakeConstSpan(bitstream));
   auto start_position = read_bit_buffer->Tell();
-  bool insufficient_data = false;
+  bool insufficient_data;
 
   EXPECT_THAT(
       ObuProcessor::ProcessDescriptorObus(
@@ -595,6 +615,7 @@
           mix_presentation_obus, insufficient_data),
       Not(IsOk()));
 
+  // We've received a valid bitstream so far but not complete.
   EXPECT_TRUE(insufficient_data);
   // Expect the reader position to be unchanged since we returned an error.
   EXPECT_EQ(read_bit_buffer->Tell(), start_position);
@@ -617,7 +638,7 @@
   auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
       kBufferCapacity, absl::MakeConstSpan(bitstream));
   auto start_position = read_bit_buffer->Tell();
-  bool insufficient_data = false;
+  bool insufficient_data;
 
   EXPECT_THAT(
       ObuProcessor::ProcessDescriptorObus(
@@ -626,6 +647,7 @@
           mix_presentation_obus, insufficient_data),
       Not(IsOk()));
 
+  // We've received a valid bitstream so far but not complete.
   EXPECT_TRUE(insufficient_data);
   EXPECT_EQ(codec_config_obus.size(), 0);
   EXPECT_EQ(audio_elements_with_data.size(), 0);
@@ -1261,6 +1283,496 @@
   EXPECT_EQ(parameter_block_with_data->end_timestamp, kParameterBlockDuration);
 }
 
+TEST(ProcessTemporalUnitObus,
+     ConsumesAllTemporalUnitsWithAnIncompleteHeaderAtEnd) {
+  AudioFrameObu audio_frame_obu(ObuHeader(), kFirstSubstreamId,
+                                kArbitraryAudioFrame);
+
+  auto one_temporal_unit = SerializeObusExpectOk({&audio_frame_obu});
+
+  // Set up inputs with descriptors, one audio frame, and one incomplete header.
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  AddOpusCodecConfigWithId(kFirstCodecConfigId, codec_config_obus);
+  absl::flat_hash_map<DecodedUleb128, AudioElementWithData>
+      audio_elements_with_data;
+  AddAmbisonicsMonoAudioElementWithSubstreamIds(
+      kFirstAudioElementId, kFirstCodecConfigId, {kFirstSubstreamId},
+      codec_config_obus, audio_elements_with_data);
+  auto global_timing_module =
+      GlobalTimingModule::Create(audio_elements_with_data,
+                                 /*param_definitions=*/{});
+  ASSERT_THAT(global_timing_module, NotNull());
+
+  const absl::flat_hash_map<DecodedUleb128, const AudioElementWithData*>
+      substream_id_to_audio_element = {
+          {kFirstSubstreamId,
+           &audio_elements_with_data.at(kFirstAudioElementId)}};
+  ParametersManager parameters_manager(audio_elements_with_data);
+  ASSERT_THAT(parameters_manager.Initialize(), IsOk());
+  absl::flat_hash_map<DecodedUleb128, ParamDefinitionVariant> param_definitions;
+  // Add a single byte to the end of the temporal unit to represent an
+  // incomplete header (A header requires at least 2 bytes).
+  one_temporal_unit.push_back(0);
+  auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
+      kBufferCapacity, absl::MakeConstSpan(one_temporal_unit));
+
+  // Confirm that the first temporal unit is processed successfully.
+  bool continue_processing = true;
+  std::optional<AudioFrameWithData> audio_frame_with_data;
+  std::optional<ParameterBlockWithData> parameter_block_with_data;
+  std::optional<TemporalDelimiterObu> temporal_delimiter;
+  EXPECT_THAT(
+      ObuProcessor::ProcessTemporalUnitObu(
+          audio_elements_with_data, codec_config_obus,
+          substream_id_to_audio_element, param_definitions, parameters_manager,
+          *read_bit_buffer, *global_timing_module, audio_frame_with_data,
+          parameter_block_with_data, temporal_delimiter, continue_processing),
+      IsOk());
+  EXPECT_TRUE(audio_frame_with_data.has_value());
+
+  // Confirm that the second temporal unit it is incomplete.
+  auto start_position = read_bit_buffer->Tell();
+  EXPECT_THAT(
+      ObuProcessor::ProcessTemporalUnitObu(
+          audio_elements_with_data, codec_config_obus,
+          substream_id_to_audio_element, param_definitions, parameters_manager,
+          *read_bit_buffer, *global_timing_module, audio_frame_with_data,
+          parameter_block_with_data, temporal_delimiter, continue_processing),
+      IsOk());
+  EXPECT_FALSE(audio_frame_with_data.has_value());
+  EXPECT_FALSE(parameter_block_with_data.has_value());
+  EXPECT_FALSE(temporal_delimiter.has_value());
+  EXPECT_FALSE(continue_processing);
+  EXPECT_EQ(read_bit_buffer->Tell(), start_position);
+}
+
+TEST(ProcessTemporalUnitObus,
+     ConsumesAllTemporalUnitsWithAnIncompleteObuAtEnd) {
+  AudioFrameObu audio_frame_obu(ObuHeader(), kFirstSubstreamId,
+                                kArbitraryAudioFrame);
+
+  auto ia_sequence = SerializeObusExpectOk({&audio_frame_obu});
+
+  // Set up inputs with descriptors, one audio frame, and one incomplete obu
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  AddOpusCodecConfigWithId(kFirstCodecConfigId, codec_config_obus);
+  absl::flat_hash_map<DecodedUleb128, AudioElementWithData>
+      audio_elements_with_data;
+  AddAmbisonicsMonoAudioElementWithSubstreamIds(
+      kFirstAudioElementId, kFirstCodecConfigId, {kFirstSubstreamId},
+      codec_config_obus, audio_elements_with_data);
+  auto global_timing_module =
+      GlobalTimingModule::Create(audio_elements_with_data,
+                                 /*param_definitions=*/{});
+  ASSERT_THAT(global_timing_module, NotNull());
+
+  const absl::flat_hash_map<DecodedUleb128, const AudioElementWithData*>
+      substream_id_to_audio_element = {
+          {kFirstSubstreamId,
+           &audio_elements_with_data.at(kFirstAudioElementId)}};
+  ParametersManager parameters_manager(audio_elements_with_data);
+  ASSERT_THAT(parameters_manager.Initialize(), IsOk());
+  absl::flat_hash_map<DecodedUleb128, ParamDefinitionVariant> param_definitions;
+  std::vector<uint8_t> extra_audio_frame_obu_header_bytes = {
+      kObuIaAudioFrameId0 << kObuTypeBitShift,
+      // `obu_size`. -> Non-zero size, but we have no bytes following.
+      0x7f};
+  ia_sequence.insert(ia_sequence.end(),
+                     extra_audio_frame_obu_header_bytes.begin(),
+                     extra_audio_frame_obu_header_bytes.end());
+  auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
+      kBufferCapacity, absl::MakeConstSpan(ia_sequence));
+
+  // Confirm that the first temporal unit is processed successfully.
+  bool continue_processing = true;
+  std::optional<AudioFrameWithData> audio_frame_with_data;
+  std::optional<ParameterBlockWithData> parameter_block_with_data;
+  std::optional<TemporalDelimiterObu> temporal_delimiter;
+  EXPECT_THAT(
+      ObuProcessor::ProcessTemporalUnitObu(
+          audio_elements_with_data, codec_config_obus,
+          substream_id_to_audio_element, param_definitions, parameters_manager,
+          *read_bit_buffer, *global_timing_module, audio_frame_with_data,
+          parameter_block_with_data, temporal_delimiter, continue_processing),
+      IsOk());
+  EXPECT_TRUE(audio_frame_with_data.has_value());
+
+  // Confirm that the second temporal unit it is incomplete.
+  auto start_position = read_bit_buffer->Tell();
+  EXPECT_THAT(
+      ObuProcessor::ProcessTemporalUnitObu(
+          audio_elements_with_data, codec_config_obus,
+          substream_id_to_audio_element, param_definitions, parameters_manager,
+          *read_bit_buffer, *global_timing_module, audio_frame_with_data,
+          parameter_block_with_data, temporal_delimiter, continue_processing),
+      IsOk());
+  EXPECT_FALSE(audio_frame_with_data.has_value());
+  EXPECT_FALSE(parameter_block_with_data.has_value());
+  EXPECT_FALSE(temporal_delimiter.has_value());
+  EXPECT_FALSE(continue_processing);
+  EXPECT_EQ(read_bit_buffer->Tell(), start_position);
+}
+
+using OutputTemporalUnit = ObuProcessor::OutputTemporalUnit;
+
+TEST(ProcessTemporalUnit, ConsumesOneAudioFrameAsTemporalUnit) {
+  // Set up inputs with a single audio frame.
+  auto bitstream = InitAllDescriptorsForZerothOrderAmbisonics();
+  AudioFrameObu audio_frame_obu(ObuHeader(), kFirstSubstreamId,
+                                kArbitraryAudioFrame);
+  auto temporal_unit_obus = SerializeObusExpectOk({&audio_frame_obu});
+  bitstream.insert(bitstream.end(), temporal_unit_obus.begin(),
+                   temporal_unit_obus.end());
+  auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
+      kBufferCapacity, absl::MakeConstSpan(bitstream));
+  bool insufficient_data;
+  auto obu_processor =
+      ObuProcessor::Create(/*is_exhaustive_and_exact=*/false,
+                           read_bit_buffer.get(), insufficient_data);
+  ASSERT_THAT(obu_processor, NotNull());
+  ASSERT_FALSE(insufficient_data);
+
+  // Call `ProcessTemporalUnit()` with `eos_is_end_of_sequence` set to true.
+  // This means that we can assume that the end of the stream implies the end of
+  // the temporal unit.
+  std::optional<OutputTemporalUnit> output_temporal_unit;
+  bool continue_processing = true;
+  EXPECT_THAT(obu_processor->ProcessTemporalUnit(
+                  /*eos_is_end_of_sequence=*/true, output_temporal_unit,
+                  continue_processing),
+              IsOk());
+
+  EXPECT_FALSE(continue_processing);
+  EXPECT_EQ(output_temporal_unit->output_audio_frames.size(), 1);
+}
+
+TEST(ProcessTemporalUnit, DoesNotConsumeOneAudioFrameAsTemporalUnit) {
+  // Set up inputs with a single audio frame.
+  auto bitstream = InitAllDescriptorsForZerothOrderAmbisonics();
+  AudioFrameObu audio_frame_obu(ObuHeader(), kFirstSubstreamId,
+                                /*audio_frame=*/{2, 3, 4, 5, 6, 7, 8});
+  auto temporal_unit_obus = SerializeObusExpectOk({&audio_frame_obu});
+  bitstream.insert(bitstream.end(), temporal_unit_obus.begin(),
+                   temporal_unit_obus.end());
+  auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
+      kBufferCapacity, absl::MakeConstSpan(bitstream));
+  bool insufficient_data;
+  auto obu_processor =
+      ObuProcessor::Create(/*is_exhaustive_and_exact=*/false,
+                           read_bit_buffer.get(), insufficient_data);
+  ASSERT_THAT(obu_processor, NotNull());
+  ASSERT_FALSE(insufficient_data);
+
+  std::optional<OutputTemporalUnit> output_temporal_unit;
+  bool continue_processing = true;
+  EXPECT_THAT(obu_processor->ProcessTemporalUnit(
+                  /*eos_is_end_of_sequence=*/false, output_temporal_unit,
+                  continue_processing),
+              IsOk());
+
+  EXPECT_FALSE(continue_processing);
+  EXPECT_FALSE(output_temporal_unit.has_value());
+}
+
+TEST(ProcessTemporalUnit, ConsumesMultipleTemporalUnitsWithTemporalDelimiters) {
+  // Set up inputs with two audio frames and temporal delimiters.
+  auto bitstream = InitAllDescriptorsForZerothOrderAmbisonics();
+  auto temporal_delimiter_obu = TemporalDelimiterObu(ObuHeader());
+  std::vector<AudioFrameObu> audio_frame_obus;
+  audio_frame_obus.push_back(
+      AudioFrameObu(ObuHeader(), kFirstSubstreamId, kArbitraryAudioFrame));
+  audio_frame_obus.push_back(
+      AudioFrameObu(ObuHeader(), kFirstSubstreamId, kArbitraryAudioFrame));
+  const auto two_temporal_units_with_delimiter_obu =
+      SerializeObusExpectOk({&audio_frame_obus[0], &temporal_delimiter_obu,
+                             &audio_frame_obus[1], &temporal_delimiter_obu});
+  bitstream.insert(bitstream.end(),
+                   two_temporal_units_with_delimiter_obu.begin(),
+                   two_temporal_units_with_delimiter_obu.end());
+  auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
+      kBufferCapacity, absl::MakeConstSpan(bitstream));
+  bool insufficient_data;
+  auto obu_processor =
+      ObuProcessor::Create(/*is_exhaustive_and_exact=*/false,
+                           read_bit_buffer.get(), insufficient_data);
+  ASSERT_THAT(obu_processor, NotNull());
+  ASSERT_FALSE(insufficient_data);
+
+  std::optional<OutputTemporalUnit> output_temporal_unit;
+  bool continue_processing = true;
+  EXPECT_THAT(obu_processor->ProcessTemporalUnit(
+                  /*eos_is_end_of_sequence=*/true, output_temporal_unit,
+                  continue_processing),
+              IsOk());
+
+  // The first temporal unit is consumed; it should only contain the first
+  // audio frame.
+  EXPECT_TRUE(continue_processing);
+  EXPECT_EQ(output_temporal_unit->output_audio_frames.size(), 1);
+
+  output_temporal_unit.reset();
+  continue_processing = true;
+  EXPECT_THAT(obu_processor->ProcessTemporalUnit(
+                  /*eos_is_end_of_sequence=*/true, output_temporal_unit,
+                  continue_processing),
+              IsOk());
+  // Seeing a temporal delimiter at the end of the stream implies that the
+  // stream is incomplete.
+  EXPECT_TRUE(continue_processing);
+  EXPECT_EQ(output_temporal_unit->output_audio_frames.size(), 1);
+}
+
+TEST(ProcessTemporalUnit,
+     ConsumesMultipleTemporalUnitsWithoutTemporalDelimiters) {
+  // Set up inputs with two audio frames. Two audio frames are known to be in a
+  // separate temporal unit if they have the same substream ID. Their underlying
+  // timestamps are different.
+  auto bitstream = InitAllDescriptorsForZerothOrderAmbisonics();
+  std::vector<AudioFrameObu> audio_frame_obus;
+  audio_frame_obus.push_back(
+      AudioFrameObu(ObuHeader(), kFirstSubstreamId, kArbitraryAudioFrame));
+  audio_frame_obus.push_back(
+      AudioFrameObu(ObuHeader(), kFirstSubstreamId, kArbitraryAudioFrame));
+  const auto two_temporal_units =
+      SerializeObusExpectOk({&audio_frame_obus[0], &audio_frame_obus[1]});
+  bitstream.insert(bitstream.end(), two_temporal_units.begin(),
+                   two_temporal_units.end());
+  auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
+      kBufferCapacity, absl::MakeConstSpan(bitstream));
+  bool insufficient_data;
+  auto obu_processor =
+      ObuProcessor::Create(/*is_exhaustive_and_exact=*/false,
+                           read_bit_buffer.get(), insufficient_data);
+  ASSERT_THAT(obu_processor, NotNull());
+  ASSERT_FALSE(insufficient_data);
+
+  std::optional<OutputTemporalUnit> output_temporal_unit;
+  bool continue_processing = true;
+  EXPECT_THAT(obu_processor->ProcessTemporalUnit(
+                  /*eos_is_end_of_sequence=*/true, output_temporal_unit,
+                  continue_processing),
+              IsOk());
+
+  // The first temporal unit is consumed; it should only contain the first
+  // audio frame.
+  EXPECT_TRUE(continue_processing);
+  EXPECT_EQ(output_temporal_unit->output_audio_frames.size(), 1);
+
+  output_temporal_unit.reset();
+  continue_processing = true;
+  EXPECT_THAT(obu_processor->ProcessTemporalUnit(
+                  /*eos_is_end_of_sequence=*/true, output_temporal_unit,
+                  continue_processing),
+              IsOk());
+
+  EXPECT_FALSE(continue_processing);
+  EXPECT_EQ(output_temporal_unit->output_audio_frames.size(), 1);
+}
+
+TEST(ProcessTemporalUnit, ConsumesOnlyOneTemporalUnitFromTwoAudioFrames) {
+  // eos_is_end_of_sequence is false. Only one temporal unit is consumed because
+  // we don't know that the second temporal unit is finished.
+
+  // Set up inputs with two audio frames. Two audio
+  // frames are known to be in a separate temporal unit if they have the same
+  // substream ID. Their underlying timestamps are different.
+  auto bitstream = InitAllDescriptorsForZerothOrderAmbisonics();
+  std::vector<AudioFrameObu> audio_frame_obus;
+  audio_frame_obus.push_back(
+      AudioFrameObu(ObuHeader(), kFirstSubstreamId,
+                    /*audio_frame=*/{2, 3, 4, 5, 6, 7, 8}));
+  audio_frame_obus.push_back(
+      AudioFrameObu(ObuHeader(), kFirstSubstreamId,
+                    /*audio_frame=*/{2, 3, 4, 5, 6, 7, 8}));
+  const auto two_temporal_units =
+      SerializeObusExpectOk({&audio_frame_obus[0], &audio_frame_obus[1]});
+  bitstream.insert(bitstream.end(), two_temporal_units.begin(),
+                   two_temporal_units.end());
+  auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
+      kBufferCapacity, absl::MakeConstSpan(bitstream));
+  bool insufficient_data;
+  auto obu_processor =
+      ObuProcessor::Create(/*is_exhaustive_and_exact=*/false,
+                           read_bit_buffer.get(), insufficient_data);
+  ASSERT_THAT(obu_processor, NotNull());
+  ASSERT_FALSE(insufficient_data);
+
+  std::optional<OutputTemporalUnit> output_temporal_unit;
+  bool continue_processing = true;
+  EXPECT_THAT(obu_processor->ProcessTemporalUnit(
+                  /*eos_is_end_of_sequence=*/false, output_temporal_unit,
+                  continue_processing),
+              IsOk());
+
+  // The first temporal unit is consumed; it should only contain the first
+  // audio frame.
+  EXPECT_TRUE(continue_processing);
+  EXPECT_EQ(output_temporal_unit->output_audio_frames.size(), 1);
+
+  output_temporal_unit.reset();
+  continue_processing = true;
+  EXPECT_THAT(obu_processor->ProcessTemporalUnit(
+                  /*eos_is_end_of_sequence=*/false, output_temporal_unit,
+                  continue_processing),
+              IsOk());
+
+  EXPECT_FALSE(continue_processing);
+  EXPECT_FALSE(output_temporal_unit.has_value());
+}
+
+TEST(ProcessTemporalUnit,
+     ConsumesOnlyOneTemporalUnitFromTwoAudioFramesAndIncompleteObuAtEnd) {
+  // eos_is_end_of_sequence is false. Only one temporal unit is consumed because
+  // we don't know that the second temporal unit is finished.
+
+  // Set up inputs with two audio frames. Two audio
+  // frames are known to be in a separate temporal unit if they have the same
+  // substream ID. Their underlying timestamps are different.
+  auto bitstream = InitAllDescriptorsForZerothOrderAmbisonics();
+  std::vector<AudioFrameObu> audio_frame_obus;
+  audio_frame_obus.push_back(
+      AudioFrameObu(ObuHeader(), kFirstSubstreamId,
+                    /*audio_frame=*/{2, 3, 4, 5, 6, 7, 8}));
+  audio_frame_obus.push_back(
+      AudioFrameObu(ObuHeader(), kFirstSubstreamId,
+                    /*audio_frame=*/{2, 3, 4, 5, 6, 7, 8}));
+  auto two_temporal_units =
+      SerializeObusExpectOk({&audio_frame_obus[0], &audio_frame_obus[1]});
+  std::vector<uint8_t> extra_audio_frame_obu_header_bytes = {
+      kObuIaAudioFrameId0 << kObuTypeBitShift,
+      // `obu_size`. -> Non-zero size, but we have no bytes following.
+      0x7f};
+  two_temporal_units.insert(two_temporal_units.end(),
+                            extra_audio_frame_obu_header_bytes.begin(),
+                            extra_audio_frame_obu_header_bytes.end());
+  bitstream.insert(bitstream.end(), two_temporal_units.begin(),
+                   two_temporal_units.end());
+  auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
+      kBufferCapacity, absl::MakeConstSpan(bitstream));
+  bool insufficient_data;
+  auto obu_processor =
+      ObuProcessor::Create(/*is_exhaustive_and_exact=*/false,
+                           read_bit_buffer.get(), insufficient_data);
+  ASSERT_THAT(obu_processor, NotNull());
+  ASSERT_FALSE(insufficient_data);
+
+  std::optional<OutputTemporalUnit> output_temporal_unit;
+  bool continue_processing = false;
+  EXPECT_THAT(obu_processor->ProcessTemporalUnit(
+                  /*eos_is_end_of_sequence=*/false, output_temporal_unit,
+                  continue_processing),
+              IsOk());
+
+  // The first temporal unit is consumed; it should only contain the first
+  // audio frame.
+  EXPECT_TRUE(continue_processing);
+  EXPECT_EQ(output_temporal_unit->output_audio_frames.size(), 1);
+
+  output_temporal_unit.reset();
+  continue_processing = true;
+  EXPECT_THAT(obu_processor->ProcessTemporalUnit(
+                  /*eos_is_end_of_sequence=*/false, output_temporal_unit,
+                  continue_processing),
+              IsOk());
+
+  // The second temporal unit is not consumed since we don't know that it is
+  // complete.
+  EXPECT_FALSE(continue_processing);
+  EXPECT_FALSE(output_temporal_unit.has_value());
+}
+
+TEST(ProcessTemporalUnit, ConsumesMultipleTemporalUnitsChunkedArbitrarily) {
+  // Set up inputs with two audio frames. Two audio frames are known to be in a
+  // separate temporal unit if they have the same substream ID. Their underlying
+  // timestamps are different.
+  auto bitstream = InitAllDescriptorsForZerothOrderAmbisonics();
+  auto read_bit_buffer = StreamBasedReadBitBuffer::Create(kBufferCapacity);
+  // Push descriptors.
+  ASSERT_THAT(read_bit_buffer->PushBytes(bitstream), IsOk());
+  bool insufficient_data;
+  auto obu_processor =
+      ObuProcessor::Create(/*is_exhaustive_and_exact=*/true,
+                           read_bit_buffer.get(), insufficient_data);
+  ASSERT_THAT(obu_processor, NotNull());
+  ASSERT_FALSE(insufficient_data);
+
+  std::vector<AudioFrameObu> audio_frame_obus;
+  audio_frame_obus.push_back(
+      AudioFrameObu(ObuHeader(), kFirstSubstreamId,
+                    /*audio_frame=*/{2, 3, 4, 5, 6, 7, 8}));
+  audio_frame_obus.push_back(
+      AudioFrameObu(ObuHeader(), kFirstSubstreamId,
+                    /*audio_frame=*/{2, 3, 4, 5, 6, 7, 8}));
+  const auto two_temporal_units =
+      SerializeObusExpectOk({&audio_frame_obus[0], &audio_frame_obus[1]});
+
+  // Split the temporal units into three chunks.
+  const int64_t chunk_size = two_temporal_units.size() / 3;
+  std::vector<uint8_t> chunk_1(two_temporal_units.begin(),
+                               two_temporal_units.begin() + chunk_size);
+  std::vector<uint8_t> chunk_2(two_temporal_units.begin() + chunk_size,
+                               two_temporal_units.begin() + 2 * chunk_size);
+  std::vector<uint8_t> chunk_3(two_temporal_units.begin() + 2 * chunk_size,
+                               two_temporal_units.end());
+
+  // Chunk 1.
+  ASSERT_THAT(read_bit_buffer->PushBytes(chunk_1), IsOk());
+  std::optional<OutputTemporalUnit> output_temporal_unit;
+  bool continue_processing = true;
+  EXPECT_THAT(obu_processor->ProcessTemporalUnit(
+                  /*eos_is_end_of_sequence=*/false, output_temporal_unit,
+                  continue_processing),
+              IsOk());
+
+  // Chunk 1 is not enough to finish reading the first audio frame, so the
+  // first temporal unit is not finished.
+  EXPECT_FALSE(continue_processing);
+  EXPECT_FALSE(output_temporal_unit.has_value());
+
+  // Chunk 2.
+  ASSERT_THAT(read_bit_buffer->PushBytes(chunk_2), IsOk());
+  continue_processing = true;
+  EXPECT_THAT(obu_processor->ProcessTemporalUnit(
+                  /*eos_is_end_of_sequence=*/false, output_temporal_unit,
+                  continue_processing),
+              IsOk());
+
+  // Chunk 2 is enough to finish reading the first audio frame, but not the
+  // second. Since we haven't finished reading the second audio frame, we cannot
+  // know that the first temporal unit is complete. Therefore we still do not
+  // have a temporal unit.
+  EXPECT_FALSE(continue_processing);
+  EXPECT_FALSE(output_temporal_unit.has_value());
+
+  // Chunk 3.
+  ASSERT_THAT(read_bit_buffer->PushBytes(chunk_3), IsOk());
+  continue_processing = true;
+  EXPECT_THAT(obu_processor->ProcessTemporalUnit(
+                  /*eos_is_end_of_sequence=*/false, output_temporal_unit,
+                  continue_processing),
+              IsOk());
+
+  // Chunk 3 is enough to finish reading the second audio frame, so the first
+  // temporal unit is now complete. But we don't know that the second temporal
+  // unit is complete since more data could be coming behind it.
+  EXPECT_TRUE(continue_processing);
+  EXPECT_EQ(output_temporal_unit->output_audio_frames.size(), 1);
+
+  // To get the second temporal unit, we make one final call with
+  // `eos_is_end_of_sequence` set to true. At this point, the bitstream is
+  // exhausted, but we can get the second temporal unit that we previously
+  // processed since we now know that the sequence is complete.
+  continue_processing = true;
+  output_temporal_unit.reset();
+  EXPECT_THAT(obu_processor->ProcessTemporalUnit(
+                  /*eos_is_end_of_sequence=*/true, output_temporal_unit,
+                  continue_processing),
+              IsOk());
+
+  EXPECT_FALSE(continue_processing);
+  EXPECT_EQ(output_temporal_unit->output_audio_frames.size(), 1);
+}
+
 // TODO(b/377772983): Test rejecting processing temporal units with mismatching
 //                    durations from parameter blocks and audio frames.
 // TODO(b/377772983): Test rejecting processing temporal units where the
@@ -1277,7 +1789,7 @@
 TEST(CollectObusFromIaSequence, ConsumesIaSequenceAndCollectsAllObus) {
   auto bitstream = InitAllDescriptorsForZerothOrderAmbisonics();
   AudioFrameObu audio_frame_obu(ObuHeader(), kFirstSubstreamId,
-                                /*audio_frame=*/{2, 3, 4, 5, 6, 7, 8});
+                                kArbitraryAudioFrame);
   const auto temporal_unit_obus = SerializeObusExpectOk({&audio_frame_obu});
   bitstream.insert(bitstream.end(), temporal_unit_obus.begin(),
                    temporal_unit_obus.end());
@@ -1318,7 +1830,7 @@
       SerializeObusExpectOk({&input_non_redundant_ia_sequence_header});
   auto non_trivial_ia_sequence = InitAllDescriptorsForZerothOrderAmbisonics();
   AudioFrameObu audio_frame_obu(ObuHeader(), kFirstSubstreamId,
-                                /*audio_frame=*/{2, 3, 4, 5, 6, 7, 8});
+                                kArbitraryAudioFrame);
   const auto temporal_unit_obus = SerializeObusExpectOk({&audio_frame_obu});
   non_trivial_ia_sequence.insert(non_trivial_ia_sequence.end(),
                                  temporal_unit_obus.begin(),
@@ -1366,7 +1878,7 @@
 TEST(CollectObusFromIaSequence, ConsumesUpToNextIaSequence) {
   auto bitstream = InitAllDescriptorsForZerothOrderAmbisonics();
   AudioFrameObu audio_frame_obu(ObuHeader(), kFirstSubstreamId,
-                                /*audio_frame=*/{2, 3, 4, 5, 6, 7, 8});
+                                kArbitraryAudioFrame);
   const auto temporal_unit_obus = SerializeObusExpectOk({&audio_frame_obu});
   bitstream.insert(bitstream.end(), temporal_unit_obus.begin(),
                    temporal_unit_obus.end());
@@ -1398,11 +1910,11 @@
   EXPECT_EQ(read_bit_buffer->Tell(), first_ia_sequence_size * 8);
 }
 
-TEST(NonStatic, CreateSucceeds) {
+TEST(Create, Succeeds) {
   auto bitstream = InitAllDescriptorsForZerothOrderAmbisonics();
   auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
       kBufferCapacity, absl::MakeConstSpan(bitstream));
-  bool insufficient_data = false;
+  bool insufficient_data;
 
   auto obu_processor =
       ObuProcessor::Create(/*is_exhaustive_and_exact=*/true,
@@ -1415,8 +1927,24 @@
   EXPECT_EQ(obu_processor->mix_presentations_.size(), 1);
 }
 
-TEST(NonStatic, CreateFailsOnNullReadBitBuffer) {
-  bool insufficient_data = false;
+TEST(Create, SucceedsForTrivialIaSequence) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  auto buffer = SerializeObusExpectOk({&kIaSequenceHeader});
+  auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
+      kBufferCapacity, absl::MakeConstSpan(buffer));
+  bool insufficient_data;
+  auto obu_processor =
+      ObuProcessor::Create(/*is_exhaustive_and_exact=*/true,
+                           read_bit_buffer.get(), insufficient_data);
+
+  EXPECT_THAT(obu_processor, NotNull());
+  EXPECT_FALSE(insufficient_data);
+}
+
+TEST(Create, FailsOnNullReadBitBuffer) {
+  bool insufficient_data;
 
   auto obu_processor = ObuProcessor::Create(/*is_exhaustive_and_exact=*/false,
                                             nullptr, insufficient_data);
@@ -1425,31 +1953,150 @@
   EXPECT_FALSE(insufficient_data);
 }
 
-TEST(NonStatic, CreateFailsOnInsufficientData) {
+TEST(Create, FailsOnInsufficientData) {
   auto bitstream = InitAllDescriptorsForZerothOrderAmbisonics();
   auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
       kBufferCapacity, absl::MakeConstSpan(bitstream));
-  bool insufficient_data = true;
+  bool insufficient_data;
 
   auto obu_processor =
       ObuProcessor::Create(/*is_exhaustive_and_exact=*/false,
                            read_bit_buffer.get(), insufficient_data);
 
   EXPECT_THAT(obu_processor, IsNull());
+  // We've received a valid bitstream so far but not complete.
   EXPECT_TRUE(insufficient_data);
 }
 
+TEST(GetOutputSampleRate, ReturnsSampleRateBasedOnCodecConfigObu) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  AddLpcmCodecConfigWithIdAndSampleRate(kFirstCodecConfigId, kSampleRate,
+                                        codec_config_obus);
+  const auto buffer = SerializeObusExpectOk(
+      {&kIaSequenceHeader, &codec_config_obus.at(kFirstCodecConfigId)});
+  auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
+      kBufferCapacity, absl::MakeConstSpan(buffer));
+  bool insufficient_data;
+  auto obu_processor =
+      ObuProcessor::Create(/*is_exhaustive_and_exact=*/true,
+                           read_bit_buffer.get(), insufficient_data);
+  ASSERT_THAT(obu_processor, NotNull());
+
+  EXPECT_THAT(obu_processor->GetOutputSampleRate(), IsOkAndHolds(kSampleRate));
+}
+
+TEST(GetOutputSampleRate, FailsForTrivialIaSequence) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  const auto buffer = SerializeObusExpectOk({&kIaSequenceHeader});
+  auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
+      kBufferCapacity, absl::MakeConstSpan(buffer));
+  bool insufficient_data;
+  auto obu_processor =
+      ObuProcessor::Create(/*is_exhaustive_and_exact=*/true,
+                           read_bit_buffer.get(), insufficient_data);
+  ASSERT_THAT(obu_processor, NotNull());
+
+  EXPECT_THAT(obu_processor->GetOutputSampleRate(), Not(IsOk()));
+}
+
+TEST(GetOutputSampleRate, FailsForMultipleCodecConfigObus) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  AddLpcmCodecConfigWithIdAndSampleRate(kFirstCodecConfigId, kSampleRate,
+                                        codec_config_obus);
+  AddLpcmCodecConfigWithIdAndSampleRate(kSecondCodecConfigId, kSampleRate,
+                                        codec_config_obus);
+  const auto buffer = SerializeObusExpectOk(
+      {&kIaSequenceHeader, &codec_config_obus.at(kFirstCodecConfigId),
+       &codec_config_obus.at(kSecondCodecConfigId)});
+  auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
+      kBufferCapacity, absl::MakeConstSpan(buffer));
+  bool insufficient_data;
+  auto obu_processor =
+      ObuProcessor::Create(/*is_exhaustive_and_exact=*/true,
+                           read_bit_buffer.get(), insufficient_data);
+  ASSERT_THAT(obu_processor, NotNull());
+
+  EXPECT_THAT(obu_processor->GetOutputSampleRate(), Not(IsOk()));
+}
+
+TEST(GetOutputFrameSize, ReturnsSampleRateBasedOnCodecConfigObu) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  AddLpcmCodecConfig(kFirstCodecConfigId, kFrameSize, kBitDepth, kSampleRate,
+                     codec_config_obus);
+  const auto buffer = SerializeObusExpectOk(
+      {&kIaSequenceHeader, &codec_config_obus.at(kFirstCodecConfigId)});
+  auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
+      kBufferCapacity, absl::MakeConstSpan(buffer));
+  bool insufficient_data;
+  auto obu_processor =
+      ObuProcessor::Create(/*is_exhaustive_and_exact=*/true,
+                           read_bit_buffer.get(), insufficient_data);
+  ASSERT_THAT(obu_processor, NotNull());
+
+  EXPECT_THAT(obu_processor->GetOutputSampleRate(), IsOkAndHolds(kSampleRate));
+}
+
+TEST(GetOutputFrameSize, FailsForTrivialIaSequence) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  const auto buffer = SerializeObusExpectOk({&kIaSequenceHeader});
+  auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
+      kBufferCapacity, absl::MakeConstSpan(buffer));
+  bool insufficient_data;
+  auto obu_processor =
+      ObuProcessor::Create(/*is_exhaustive_and_exact=*/true,
+                           read_bit_buffer.get(), insufficient_data);
+  ASSERT_THAT(obu_processor, NotNull());
+
+  EXPECT_THAT(obu_processor->GetOutputFrameSize(), Not(IsOk()));
+}
+
+TEST(GetOutputFrameSize, FailsForMultipleCodecConfigObus) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  AddLpcmCodecConfigWithIdAndSampleRate(kFirstCodecConfigId, kSampleRate,
+                                        codec_config_obus);
+  AddLpcmCodecConfigWithIdAndSampleRate(kSecondCodecConfigId, kSampleRate,
+                                        codec_config_obus);
+  const auto buffer = SerializeObusExpectOk(
+      {&kIaSequenceHeader, &codec_config_obus.at(kFirstCodecConfigId),
+       &codec_config_obus.at(kSecondCodecConfigId)});
+  auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
+      kBufferCapacity, absl::MakeConstSpan(buffer));
+  bool insufficient_data;
+  auto obu_processor =
+      ObuProcessor::Create(/*is_exhaustive_and_exact=*/true,
+                           read_bit_buffer.get(), insufficient_data);
+  ASSERT_THAT(obu_processor, NotNull());
+
+  EXPECT_THAT(obu_processor->GetOutputFrameSize(), Not(IsOk()));
+}
+
 TEST(NonStatic, ProcessTemporalUnitObu) {
   auto bitstream = InitAllDescriptorsForZerothOrderAmbisonics();
   AudioFrameObu audio_frame_obu(ObuHeader(), kFirstSubstreamId,
-                                /*audio_frame=*/{2, 3, 4, 5, 6, 7, 8});
+                                kArbitraryAudioFrame);
   const auto temporal_unit_obus = SerializeObusExpectOk({&audio_frame_obu});
   bitstream.insert(bitstream.end(), temporal_unit_obus.begin(),
                    temporal_unit_obus.end());
 
   auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
       kBufferCapacity, absl::MakeConstSpan(bitstream));
-  bool insufficient_data = false;
+  bool insufficient_data;
 
   auto obu_processor =
       ObuProcessor::Create(/*is_exhaustive_and_exact=*/false,
@@ -1481,14 +2128,15 @@
     const std::vector<uint8_t>& bitstream_of_descriptors) {
   auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
       kBufferCapacity, absl::MakeConstSpan(bitstream_of_descriptors));
-  bool insufficient_data = false;
+  bool insufficient_data;
 
   const std::string output_filename_string(output_filename);
+  Layout unused_output_layout;
   auto obu_processor = ObuProcessor::CreateForRendering(
       kStereoLayout,
       CreateAllWavWriters(output_filename_string, write_wav_header),
       /*is_exhaustive_and_exact=*/true, read_bit_buffer.get(),
-      insufficient_data);
+      unused_output_layout, insufficient_data);
   ASSERT_THAT(obu_processor, NotNull());
   ASSERT_FALSE(insufficient_data);
   absl::Span<const std::vector<int32_t>> output_rendered_pcm_samples;
@@ -1722,12 +2370,13 @@
   auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
       kBufferCapacity, absl::MakeConstSpan(bitstream));
 
-  bool insufficient_data = false;
+  Layout unused_output_layout;
+  bool insufficient_data;
   auto obu_processor = ObuProcessor::CreateForRendering(
       kStereoLayout,
       RenderingMixPresentationFinalizer::ProduceNoSampleProcessors,
       /*is_exhaustive_and_exact=*/true, read_bit_buffer.get(),
-      insufficient_data);
+      unused_output_layout, insufficient_data);
   ASSERT_THAT(obu_processor, NotNull());
   ASSERT_FALSE(insufficient_data);
   absl::Span<const std::vector<int32_t>> output_rendered_pcm_samples;
@@ -1832,14 +2481,15 @@
          &mix_presentation_obus.front()});
     auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
         kBufferCapacity, absl::MakeConstSpan(bitstream));
-    bool insufficient_data = false;
 
+    Layout unused_output_layout;
+    bool insufficient_data;
     const std::string output_filename_string(output_filename);
     auto obu_processor = ObuProcessor::CreateForRendering(
         kStereoLayout,
         CreateAllWavWriters(output_filename_string, kWriteWavHeader),
         /*is_exhaustive_and_exact=*/true, read_bit_buffer.get(),
-        insufficient_data);
+        unused_output_layout, insufficient_data);
     ASSERT_THAT(obu_processor, NotNull());
     ASSERT_FALSE(insufficient_data);
 
@@ -1993,12 +2643,13 @@
   // Expect that the `ObuProcessor` rejects the rendering request.
   auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
       kBufferCapacity, absl::MakeConstSpan(bitstream));
-  bool insufficient_data = false;
+  Layout unused_output_layout;
+  bool insufficient_data;
   auto obu_processor = ObuProcessor::CreateForRendering(
       kStereoLayout,
       RenderingMixPresentationFinalizer::ProduceNoSampleProcessors,
       /*is_exhaustive_and_exact=*/true, read_bit_buffer.get(),
-      insufficient_data);
+      unused_output_layout, insufficient_data);
   EXPECT_FALSE(insufficient_data);
   EXPECT_THAT(obu_processor, IsNull());
 }
@@ -2106,7 +2757,7 @@
        &mix_presentation_obus.front()});
   auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
       kBufferCapacity, absl::MakeConstSpan(bitstream));
-  bool insufficient_data = false;
+  bool insufficient_data;
 
   // We expect arguments to be forwarded from the OBUs to the sample processor
   // factory.
@@ -2130,12 +2781,211 @@
   RenderingMixPresentationFinalizer::SampleProcessorFactory
       sample_processor_factory = mock_sample_processor_factory.AsStdFunction();
 
+  Layout unused_output_layout;
   EXPECT_THAT(ObuProcessor::CreateForRendering(
                   kStereoLayout, sample_processor_factory,
                   /*is_exhaustive_and_exact=*/true, read_bit_buffer.get(),
-                  insufficient_data),
+                  unused_output_layout, insufficient_data),
               NotNull());
 }
 
+using testing::_;
+
+constexpr Layout k5_1_Layout = {
+    .layout_type = Layout::kLayoutTypeLoudspeakersSsConvention,
+    .specific_layout = LoudspeakersSsConventionLayout{
+        .sound_system = LoudspeakersSsConventionLayout::kSoundSystemB_0_5_0}};
+
+TEST(CreateForRendering, ForwardsChosenLayoutToSampleProcessorFactory) {
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  AddLpcmCodecConfigWithIdAndSampleRate(kFirstCodecConfigId, kSampleRate,
+                                        codec_config_obus);
+  absl::flat_hash_map<DecodedUleb128, AudioElementWithData>
+      audio_elements_with_data;
+  AddAmbisonicsMonoAudioElementWithSubstreamIds(
+      kFirstAudioElementId, kFirstCodecConfigId,
+      {kFirstSubstreamId, kSecondSubstreamId, kThirdSubstreamId,
+       kFourthSubstreamId},
+      codec_config_obus, audio_elements_with_data);
+  std::list<MixPresentationObu> mix_presentation_obus;
+  std::vector<LoudspeakersSsConventionLayout::SoundSystem>
+      sound_system_layouts = {
+          LoudspeakersSsConventionLayout::kSoundSystemA_0_2_0,
+          LoudspeakersSsConventionLayout::kSoundSystemB_0_5_0};
+  AddMixPresentationObuWithConfigurableLayouts(
+      kFirstMixPresentationId, {kFirstAudioElementId},
+      kCommonMixGainParameterId, kCommonParameterRate, sound_system_layouts,
+      mix_presentation_obus);
+
+  const std::list<AudioFrameWithData> empty_audio_frames_with_data = {};
+  const std::list<ParameterBlockWithData> empty_parameter_blocks_with_data = {};
+
+  const auto bitstream = AddSequenceHeaderAndSerializeObusExpectOk(
+      {&codec_config_obus.at(kFirstCodecConfigId),
+       &audio_elements_with_data.at(kFirstAudioElementId).obu,
+       &mix_presentation_obus.front()});
+  auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
+      kBufferCapacity, absl::MakeConstSpan(bitstream));
+  bool insufficient_data;
+
+  // We expect to use the second layout, since this is the only one that matches
+  // the desired layout.
+  constexpr int kSubmixIndex = 0;
+  constexpr int kLayoutIndex = 1;
+  const auto& forwarded_layout =
+      mix_presentation_obus.front().sub_mixes_[0].layouts[1].loudness_layout;
+
+  MockSampleProcessorFactory mock_sample_processor_factory;
+  EXPECT_CALL(mock_sample_processor_factory,
+              Call(kFirstMixPresentationId, kSubmixIndex, kLayoutIndex,
+                   forwarded_layout, /*num_channels=*/6, _, _, _));
+  RenderingMixPresentationFinalizer::SampleProcessorFactory
+      sample_processor_factory = mock_sample_processor_factory.AsStdFunction();
+
+  Layout output_layout;
+  EXPECT_THAT(ObuProcessor::CreateForRendering(
+                  k5_1_Layout, sample_processor_factory,
+                  /*is_exhaustive_and_exact=*/true, read_bit_buffer.get(),
+                  output_layout, insufficient_data),
+              NotNull());
+  EXPECT_EQ(output_layout, k5_1_Layout);
+}
+
+TEST(CreateForRendering, ForwardsDefaultLayoutToSampleProcessorFactory) {
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  AddLpcmCodecConfigWithIdAndSampleRate(kFirstCodecConfigId, kSampleRate,
+                                        codec_config_obus);
+  absl::flat_hash_map<DecodedUleb128, AudioElementWithData>
+      audio_elements_with_data;
+  AddAmbisonicsMonoAudioElementWithSubstreamIds(
+      kFirstAudioElementId, kFirstCodecConfigId,
+      {kFirstSubstreamId, kSecondSubstreamId, kThirdSubstreamId,
+       kFourthSubstreamId},
+      codec_config_obus, audio_elements_with_data);
+  std::list<MixPresentationObu> mix_presentation_obus;
+  std::vector<LoudspeakersSsConventionLayout::SoundSystem>
+      sound_system_layouts = {
+          LoudspeakersSsConventionLayout::kSoundSystemA_0_2_0,
+          LoudspeakersSsConventionLayout::kSoundSystemJ_4_7_0};
+  AddMixPresentationObuWithConfigurableLayouts(
+      kFirstMixPresentationId, {kFirstAudioElementId},
+      kCommonMixGainParameterId, kCommonParameterRate, sound_system_layouts,
+      mix_presentation_obus);
+
+  const std::list<AudioFrameWithData> empty_audio_frames_with_data = {};
+  const std::list<ParameterBlockWithData> empty_parameter_blocks_with_data = {};
+
+  const auto bitstream = AddSequenceHeaderAndSerializeObusExpectOk(
+      {&codec_config_obus.at(kFirstCodecConfigId),
+       &audio_elements_with_data.at(kFirstAudioElementId).obu,
+       &mix_presentation_obus.front()});
+  auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
+      kBufferCapacity, absl::MakeConstSpan(bitstream));
+  bool insufficient_data;
+
+  // We expect to use the first layout as default, since the desired layout is
+  // not available in the mix presentation.
+  constexpr int kSubmixIndex = 0;
+  constexpr int kLayoutIndex = 0;
+  const auto& forwarded_layout =
+      mix_presentation_obus.front().sub_mixes_[0].layouts[0].loudness_layout;
+
+  MockSampleProcessorFactory mock_sample_processor_factory;
+  EXPECT_CALL(mock_sample_processor_factory,
+              Call(kFirstMixPresentationId, kSubmixIndex, kLayoutIndex,
+                   forwarded_layout, /*num_channels=*/2, _, _, _));
+  RenderingMixPresentationFinalizer::SampleProcessorFactory
+      sample_processor_factory = mock_sample_processor_factory.AsStdFunction();
+
+  Layout unused_output_layout;
+  EXPECT_THAT(ObuProcessor::CreateForRendering(
+                  k5_1_Layout, sample_processor_factory,
+                  /*is_exhaustive_and_exact=*/true, read_bit_buffer.get(),
+                  unused_output_layout, insufficient_data),
+              NotNull());
+}
+
+TEST(CreateForRendering,
+     ForwardsChosenLayoutToSampleProcessorFactoryWithMultipleMixPresentations) {
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  AddLpcmCodecConfigWithIdAndSampleRate(kFirstCodecConfigId, kSampleRate,
+                                        codec_config_obus);
+  absl::flat_hash_map<DecodedUleb128, AudioElementWithData>
+      audio_elements_with_data;
+  AddAmbisonicsMonoAudioElementWithSubstreamIds(
+      kFirstAudioElementId, kFirstCodecConfigId,
+      {kFirstSubstreamId, kSecondSubstreamId, kThirdSubstreamId,
+       kFourthSubstreamId},
+      codec_config_obus, audio_elements_with_data);
+  std::list<MixPresentationObu> mix_presentation_obus;
+  std::vector<LoudspeakersSsConventionLayout::SoundSystem>
+      sound_system_layouts_first_mix_presentation = {
+          LoudspeakersSsConventionLayout::kSoundSystemA_0_2_0,
+          LoudspeakersSsConventionLayout::kSoundSystem10_2_7_0};
+  AddMixPresentationObuWithConfigurableLayouts(
+      kFirstMixPresentationId, {kFirstAudioElementId},
+      kCommonMixGainParameterId, kCommonParameterRate,
+      sound_system_layouts_first_mix_presentation, mix_presentation_obus);
+  std::vector<LoudspeakersSsConventionLayout::SoundSystem>
+      sound_system_layouts_second_mix_presentation = {
+          LoudspeakersSsConventionLayout::kSoundSystemA_0_2_0,
+          LoudspeakersSsConventionLayout::kSoundSystemB_0_5_0};
+  AddMixPresentationObuWithConfigurableLayouts(
+      kSecondMixPresentationId, {kFirstAudioElementId},
+      kCommonMixGainParameterId, kCommonParameterRate,
+      sound_system_layouts_second_mix_presentation, mix_presentation_obus);
+
+  const std::list<AudioFrameWithData> empty_audio_frames_with_data = {};
+  const std::list<ParameterBlockWithData> empty_parameter_blocks_with_data = {};
+
+  const auto bitstream = AddSequenceHeaderAndSerializeObusExpectOk(
+      {&codec_config_obus.at(kFirstCodecConfigId),
+       &audio_elements_with_data.at(kFirstAudioElementId).obu,
+       &mix_presentation_obus.front(),
+       &*(std::next(mix_presentation_obus.begin()))});
+  auto read_bit_buffer = MemoryBasedReadBitBuffer::CreateFromSpan(
+      kBufferCapacity, absl::MakeConstSpan(bitstream));
+  bool insufficient_data;
+
+  // We expect to use the second layout in the second mix presentation, since
+  // this is the only one that matches the desired layout.
+  constexpr int kSubmixIndex = 0;
+  constexpr int kLayoutIndex = 1;
+  const auto& forwarded_layout = (std::next(mix_presentation_obus.begin()))
+                                     ->sub_mixes_[0]
+                                     .layouts[1]
+                                     .loudness_layout;
+
+  MockSampleProcessorFactory mock_sample_processor_factory;
+  EXPECT_CALL(mock_sample_processor_factory,
+              Call(kSecondMixPresentationId, kSubmixIndex, kLayoutIndex,
+                   forwarded_layout, /*num_channels=*/6, _, _, _));
+  RenderingMixPresentationFinalizer::SampleProcessorFactory
+      sample_processor_factory = mock_sample_processor_factory.AsStdFunction();
+
+  Layout output_layout;
+  EXPECT_THAT(ObuProcessor::CreateForRendering(
+                  k5_1_Layout, sample_processor_factory,
+                  /*is_exhaustive_and_exact=*/true, read_bit_buffer.get(),
+                  output_layout, insufficient_data),
+              NotNull());
+  EXPECT_EQ(output_layout, k5_1_Layout);
+}
+
+TEST(CreateForRendering, NullReadBitBufferRejected) {
+  MockSampleProcessorFactory mock_sample_processor_factory;
+  auto sample_processor_factory = mock_sample_processor_factory.AsStdFunction();
+  ReadBitBuffer* read_bit_buffer_nullptr = nullptr;
+  bool insufficient_data;
+
+  Layout unused_output_layout;
+  EXPECT_THAT(ObuProcessor::CreateForRendering(
+                  kStereoLayout, sample_processor_factory,
+                  /*is_exhaustive_and_exact=*/true, read_bit_buffer_nullptr,
+                  unused_output_layout, insufficient_data),
+              IsNull());
+  EXPECT_FALSE(insufficient_data);
+}
+
 }  // namespace
 }  // namespace iamf_tools
diff --git a/iamf/cli/tests/obu_sequencer_base_test.cc b/iamf/cli/tests/obu_sequencer_base_test.cc
index 81fc97d..fd33b8f 100644
--- a/iamf/cli/tests/obu_sequencer_base_test.cc
+++ b/iamf/cli/tests/obu_sequencer_base_test.cc
@@ -673,6 +673,24 @@
               IsOk());
 }
 
+TEST(PushDescriptorObus, SucceedsWithIaSequenceHeaderOnly) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  const absl::flat_hash_map<DecodedUleb128, CodecConfigObu> kNoCodecConfigObus;
+  const absl::flat_hash_map<uint32_t, AudioElementWithData> kNoAudioElements;
+  const std::list<MixPresentationObu> kNoMixPresentationObus;
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+
+  EXPECT_THAT(mock_obu_sequencer.PushDescriptorObus(
+                  kIaSequenceHeader, kNoCodecConfigObus, kNoAudioElements,
+                  kNoMixPresentationObus, kNoArbitraryObus),
+              IsOk());
+}
+
 TEST(PickAndPlace, SucceedsWithIaSequenceHeaderOnly) {
   const IASequenceHeaderObu kIaSequenceHeader(
       ObuHeader(), IASequenceHeaderObu::kIaCode,
@@ -694,6 +712,28 @@
               IsOk());
 }
 
+TEST(PushDescriptorObus, FailsWhenCalledTwice) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  const absl::flat_hash_map<DecodedUleb128, CodecConfigObu> kNoCodecConfigObus;
+  const absl::flat_hash_map<uint32_t, AudioElementWithData> kNoAudioElements;
+  const std::list<MixPresentationObu> kNoMixPresentationObus;
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+  EXPECT_THAT(mock_obu_sequencer.PushDescriptorObus(
+                  kIaSequenceHeader, kNoCodecConfigObus, kNoAudioElements,
+                  kNoMixPresentationObus, kNoArbitraryObus),
+              IsOk());
+
+  EXPECT_THAT(mock_obu_sequencer.PushDescriptorObus(
+                  kIaSequenceHeader, kNoCodecConfigObus, kNoAudioElements,
+                  kNoMixPresentationObus, kNoArbitraryObus),
+              Not(IsOk()));
+}
+
 TEST(PickAndPlace, FailsWhenCalledTwice) {
   const IASequenceHeaderObu kIaSequenceHeader(
       ObuHeader(), IASequenceHeaderObu::kIaCode,
@@ -720,7 +760,44 @@
               Not(IsOk()));
 }
 
-TEST(PickAndPlace, ForwardsPropertiesToPushDescriptorObus) {
+TEST(PushDescriptorObus, ForwardsPropertiesToPushSerializedDescriptorObus) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  absl::flat_hash_map<uint32_t, AudioElementWithData> audio_elements;
+  std::list<MixPresentationObu> mix_presentation_obus;
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  InitializeDescriptorObusForTwoMonoAmbisonicsAudioElement(
+      codec_config_obus, audio_elements, mix_presentation_obus);
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+
+  // Several properties should match values derived from the descriptor OBUs.
+  const auto& codec_config_obu = codec_config_obus.begin()->second;
+  const uint32_t kExpectedCommonSamplesPerFrame =
+      codec_config_obu.GetNumSamplesPerFrame();
+  const uint32_t kExpectedCommonSampleRate =
+      codec_config_obu.GetOutputSampleRate();
+  const uint8_t kExpectedCommonBitDepth =
+      codec_config_obu.GetBitDepthToMeasureLoudness();
+  const std::optional<int64_t> kOmitFirstPts = std::nullopt;
+  const int kExpectedNumChannels = 2;
+  const std::vector<uint8_t> descriptor_obus = {1, 2, 3};
+  EXPECT_CALL(
+      mock_obu_sequencer,
+      PushSerializedDescriptorObus(
+          kExpectedCommonSamplesPerFrame, kExpectedCommonSampleRate,
+          kExpectedCommonBitDepth, kOmitFirstPts, kExpectedNumChannels, _));
+
+  EXPECT_THAT(mock_obu_sequencer.PushDescriptorObus(
+                  kIaSequenceHeader, codec_config_obus, audio_elements,
+                  mix_presentation_obus, kNoArbitraryObus),
+              IsOk());
+}
+
+TEST(PickAndPlace, ForwardsPropertiesToPushSerializedDescriptorObus) {
   const IASequenceHeaderObu kIaSequenceHeader(
       ObuHeader(), IASequenceHeaderObu::kIaCode,
       ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
@@ -760,6 +837,80 @@
               IsOk());
 }
 
+TEST(PushDescriptorObus,
+     WhenDescriptorsAreNotDelayedDescriptorsAreForwardedImmediately) {
+  // Configure the OBU sequencer to not delay descriptors. This means the
+  // properties can be forwarded right away.
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  const absl::flat_hash_map<DecodedUleb128, CodecConfigObu> kNoCodecConfigObus;
+  const absl::flat_hash_map<uint32_t, AudioElementWithData> kNoAudioElements;
+  const std::list<MixPresentationObu> kNoMixPresentationObus;
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+
+  // The properties themselves are arbitrary, but "reasonable" defaults. This is
+  // to ensure certain OBU sequencers can have a file with reasonable
+  // properties, even if the IA Sequence is trivial.
+  const uint32_t kExpectedCommonSamplesPerFrame = 1024;
+  const uint32_t kExpectedCommonSampleRate = 48000;
+  const uint8_t kExpectedCommonBitDepth = 16;
+  const std::optional<int64_t> kFirstUntrimmedTimestamp = std::nullopt;
+  const int kExpectedNumChannels = 2;
+  EXPECT_CALL(mock_obu_sequencer,
+              PushSerializedDescriptorObus(
+                  kExpectedCommonSamplesPerFrame, kExpectedCommonSampleRate,
+                  kExpectedCommonBitDepth, kFirstUntrimmedTimestamp,
+                  kExpectedNumChannels, _));
+
+  EXPECT_THAT(mock_obu_sequencer.PushDescriptorObus(
+                  kIaSequenceHeader, kNoCodecConfigObus, kNoAudioElements,
+                  kNoMixPresentationObus, kNoArbitraryObus),
+              IsOk());
+}
+
+TEST(
+    PushDescriptorObus,
+    WhenDescriptorsAreDelayedPropertiesAreForwardedAfterCloseForTrivialIaSequences) {
+  // Configure the OBU sequencer to delay descriptors until the first untrimmed
+  // sample is known. We can't detect it is a trivial IA Sequence, until the
+  // sequencer is closed.
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  const absl::flat_hash_map<DecodedUleb128, CodecConfigObu> kNoCodecConfigObus;
+  const absl::flat_hash_map<uint32_t, AudioElementWithData> kNoAudioElements;
+  const std::list<MixPresentationObu> kNoMixPresentationObus;
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  MockObuSequencer mock_obu_sequencer(*LebGenerator::Create(),
+                                      kDoNotIncludeTemporalDelimiters,
+                                      kDelayDescriptorsUntilTrimAtStartIsKnown);
+  EXPECT_THAT(mock_obu_sequencer.PushDescriptorObus(
+                  kIaSequenceHeader, kNoCodecConfigObus, kNoAudioElements,
+                  kNoMixPresentationObus, kNoArbitraryObus),
+              IsOk());
+
+  // The properties themselves are arbitrary, but "reasonable" defaults. This is
+  // to ensure certain OBU sequencers can have a file with reasonable
+  // properties, even if the IA Sequence is trivial.
+  const uint32_t kExpectedCommonSamplesPerFrame = 1024;
+  const uint32_t kExpectedCommonSampleRate = 48000;
+  const uint8_t kExpectedCommonBitDepth = 16;
+  const std::optional<int64_t> kFirstUntrimmedTimestamp = 0;
+  const int kExpectedNumChannels = 2;
+  EXPECT_CALL(mock_obu_sequencer,
+              PushSerializedDescriptorObus(
+                  kExpectedCommonSamplesPerFrame, kExpectedCommonSampleRate,
+                  kExpectedCommonBitDepth, kFirstUntrimmedTimestamp,
+                  kExpectedNumChannels, _));
+  // Finally at close time, we detect that there are no audio frames. Therefore
+  // we can make up a fake first timestamp.
+  EXPECT_THAT(mock_obu_sequencer.Close(), IsOk());
+}
+
 TEST(PickAndPlace, ForwardsDefaultPropertiesForTrivialIaSequences) {
   const IASequenceHeaderObu kIaSequenceHeader(
       ObuHeader(), IASequenceHeaderObu::kIaCode,
@@ -777,7 +928,7 @@
   // The properties themselves are arbitrary, but "reasonable" defaults. This is
   // to ensure certain OBU sequencers can have a file with reasonable
   // properties, even if the IA Sequence is trivial.
-  const uint32_t kExpectedCommonSamplesPerFrame = 0;
+  const uint32_t kExpectedCommonSamplesPerFrame = 1024;
   const uint32_t kExpectedCommonSampleRate = 48000;
   const uint8_t kExpectedCommonBitDepth = 16;
   const std::optional<int64_t> kFirstUntrimmedTimestamp = 0;
@@ -795,6 +946,34 @@
               IsOk());
 }
 
+TEST(PushDescriptorObus, ForwardsSerializedDescriptorObusToPushDescriptorObus) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  absl::flat_hash_map<uint32_t, AudioElementWithData> audio_elements;
+  std::list<MixPresentationObu> mix_presentation_obus;
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  InitializeDescriptorObusForOneMonoAmbisonicsAudioElement(
+      codec_config_obus, audio_elements, mix_presentation_obus);
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+
+  // The spec prescribes an order among different types of descriptor OBUs.
+  const auto descriptor_obus = SerializeObusExpectOk(std::list<const ObuBase*>{
+      &kIaSequenceHeader, &codec_config_obus.begin()->second,
+      &audio_elements.begin()->second.obu, &mix_presentation_obus.front()});
+  EXPECT_CALL(mock_obu_sequencer,
+              PushSerializedDescriptorObus(_, _, _, _, _,
+                                           MakeConstSpan(descriptor_obus)));
+
+  EXPECT_THAT(mock_obu_sequencer.PushDescriptorObus(
+                  kIaSequenceHeader, codec_config_obus, audio_elements,
+                  mix_presentation_obus, kNoArbitraryObus),
+              IsOk());
+}
+
 TEST(PickAndPlace, ForwardsSerializedDescriptorObusToPushDescriptorObus) {
   const IASequenceHeaderObu kIaSequenceHeader(
       ObuHeader(), IASequenceHeaderObu::kIaCode,
@@ -826,6 +1005,33 @@
               IsOk());
 }
 
+TEST(PushDescriptorObus, ForwardsArbitraryObusToPushSerializedDescriptorObus) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  const absl::flat_hash_map<DecodedUleb128, CodecConfigObu> kNoCodecConfigObus;
+  const absl::flat_hash_map<uint32_t, AudioElementWithData> kNoAudioElements;
+  const std::list<MixPresentationObu> kNoMixPresentationObus;
+  const std::list<ArbitraryObu> kArbitraryObuAfterIaSequenceHeader(
+      {ArbitraryObu(kObuIaReserved25, ObuHeader(), {},
+                    ArbitraryObu::kInsertionHookAfterIaSequenceHeader)});
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+
+  // Custom arbitrary OBUs can be placed according to their hook.
+  const auto descriptor_obus = SerializeObusExpectOk(std::list<const ObuBase*>{
+      &kIaSequenceHeader, &kArbitraryObuAfterIaSequenceHeader.front()});
+  EXPECT_CALL(mock_obu_sequencer,
+              PushSerializedDescriptorObus(_, _, _, _, _,
+                                           MakeConstSpan(descriptor_obus)));
+
+  EXPECT_THAT(mock_obu_sequencer.PushDescriptorObus(
+                  kIaSequenceHeader, kNoCodecConfigObus, kNoAudioElements,
+                  kNoMixPresentationObus, kArbitraryObuAfterIaSequenceHeader),
+              IsOk());
+}
+
 TEST(PickAndPlace, ForwardsArbitraryObusToPushSerializedDescriptorObus) {
   const IASequenceHeaderObu kIaSequenceHeader(
       ObuHeader(), IASequenceHeaderObu::kIaCode,
@@ -856,6 +1062,41 @@
               IsOk());
 }
 
+TEST(PushTemporalUnit, ForwardsPropertiesToPushAllTemporalUnits) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  absl::flat_hash_map<uint32_t, AudioElementWithData> audio_elements;
+  std::list<MixPresentationObu> mix_presentation_obus;
+  std::list<AudioFrameWithData> audio_frames;
+  const std::list<ParameterBlockWithData> kNoParameterBlocks;
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  InitializeOneFrameIaSequenceWithMixPresentation(
+      codec_config_obus, audio_elements, mix_presentation_obus, audio_frames);
+  audio_frames.front().obu.header_.num_samples_to_trim_at_start = 2;
+  audio_frames.front().obu.header_.num_samples_to_trim_at_end = 1;
+  const auto temporal_unit = TemporalUnitView::Create(
+      kNoParameterBlocks, audio_frames, kNoArbitraryObus);
+  ASSERT_THAT(temporal_unit, IsOk());
+  // We expect eight samples per frame, less the trimmed samples.
+  constexpr int kExpectedTimestamp = 0;
+  constexpr int kExpectedNumSamples = 5;
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+  EXPECT_THAT(mock_obu_sequencer.PushDescriptorObus(
+                  kIaSequenceHeader, codec_config_obus, audio_elements,
+                  mix_presentation_obus, kNoArbitraryObus),
+              IsOk());
+
+  EXPECT_CALL(
+      mock_obu_sequencer,
+      PushSerializedTemporalUnit(kExpectedTimestamp, kExpectedNumSamples, _));
+
+  EXPECT_THAT(mock_obu_sequencer.PushTemporalUnit(*temporal_unit), IsOk());
+}
+
 TEST(PickAndPlace, ForwardsPropertiesToPushAllTemporalUnits) {
   const IASequenceHeaderObu kIaSequenceHeader(
       ObuHeader(), IASequenceHeaderObu::kIaCode,
@@ -925,6 +1166,55 @@
               IsOk());
 }
 
+TEST(PushTemporalUnit,
+     ForwardsNumUntrimmedSamplesToPushSerializedTemporalUnitWhenConfigured) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  absl::flat_hash_map<uint32_t, AudioElementWithData> audio_elements;
+  std::list<MixPresentationObu> mix_presentation_obus;
+  const std::list<ParameterBlockWithData> kNoParameterBlocks;
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  std::list<AudioFrameWithData> first_audio_frame;
+  InitializeOneFrameIaSequenceWithMixPresentation(
+      codec_config_obus, audio_elements, mix_presentation_obus,
+      first_audio_frame);
+  first_audio_frame.back().obu.header_.num_samples_to_trim_at_start = 8;
+  const auto first_temporal_unit = TemporalUnitView::Create(
+      kNoParameterBlocks, first_audio_frame, kNoArbitraryObus);
+  ASSERT_THAT(first_temporal_unit, IsOk());
+  std::list<AudioFrameWithData> second_audio_frame;
+  AddEmptyAudioFrameWithAudioElementIdSubstreamIdAndTimestamps(
+      kFirstAudioElementId, kFirstSubstreamId, kSecondTimestamp,
+      kThirdTimestamp, audio_elements, second_audio_frame);
+  second_audio_frame.back().obu.header_.num_samples_to_trim_at_start = 3;
+  const auto second_temporal_unit = TemporalUnitView::Create(
+      kNoParameterBlocks, second_audio_frame, kNoArbitraryObus);
+  ASSERT_THAT(second_temporal_unit, IsOk());
+  // The first frame is fully trimmed. The second frame is partially trimmed.
+  constexpr std::optional<int64_t> kExpectedFirstUntrimmedTimestamp = 11;
+  MockObuSequencer mock_obu_sequencer(*LebGenerator::Create(),
+                                      kDoNotIncludeTemporalDelimiters,
+                                      kDelayDescriptorsUntilTrimAtStartIsKnown);
+  // Neither the initial descriptors, nor the first temporal unit have enough
+  // information to determine the first untrimmed timestamp.
+  EXPECT_THAT(mock_obu_sequencer.PushDescriptorObus(
+                  kIaSequenceHeader, codec_config_obus, audio_elements,
+                  mix_presentation_obus, kNoArbitraryObus),
+              IsOk());
+  EXPECT_THAT(mock_obu_sequencer.PushTemporalUnit(*first_temporal_unit),
+              IsOk());
+
+  // But by the second temporal unit, we can see the cumulative number of
+  // samples to trim at the start for this IA Sequence.
+  EXPECT_CALL(mock_obu_sequencer,
+              PushSerializedDescriptorObus(
+                  _, _, _, kExpectedFirstUntrimmedTimestamp, _, _));
+  EXPECT_THAT(mock_obu_sequencer.PushTemporalUnit(*second_temporal_unit),
+              IsOk());
+}
+
 TEST(PickAndPlace,
      ForwardsNumUntrimmedSamplesToPushAllTemporalUnitsWhenConfigured) {
   const IASequenceHeaderObu kIaSequenceHeader(
@@ -962,6 +1252,35 @@
               IsOk());
 }
 
+TEST(PushDescriptorObus, ReturnsErrorWhenResamplingWouldBeRequired) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  // Theoretically, a future profile may support multiple codec config OBUs with
+  // different sample rates. The underlying code is written to only support IAMF
+  // v1.1.0 profiles, which all only support a single codec config OBU.
+  constexpr uint32_t kCodecConfigId = 1;
+  constexpr uint32_t kSecondCodecConfigId = 2;
+  constexpr uint32_t kSampleRate = 48000;
+  constexpr uint32_t kSecondSampleRate = 44100;
+  AddLpcmCodecConfigWithIdAndSampleRate(kCodecConfigId, kSampleRate,
+                                        codec_config_obus);
+  AddLpcmCodecConfigWithIdAndSampleRate(kSecondCodecConfigId, kSecondSampleRate,
+                                        codec_config_obus);
+  const absl::flat_hash_map<uint32_t, AudioElementWithData> kNoAudioElements;
+  const std::list<MixPresentationObu> kNoMixPresentationObus;
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  MockObuSequencer mock_obu_sequencer(*LebGenerator::Create(),
+                                      kDoNotIncludeTemporalDelimiters,
+                                      kDelayDescriptorsUntilTrimAtStartIsKnown);
+
+  EXPECT_THAT(mock_obu_sequencer.PushDescriptorObus(
+                  kIaSequenceHeader, codec_config_obus, kNoAudioElements,
+                  kNoMixPresentationObus, kNoArbitraryObus),
+              Not(IsOk()));
+}
+
 TEST(PickAndPlace, ReturnsErrorWhenResamplingWouldBeRequired) {
   const IASequenceHeaderObu kIaSequenceHeader(
       ObuHeader(), IASequenceHeaderObu::kIaCode,
@@ -994,6 +1313,50 @@
               Not(IsOk()));
 }
 
+TEST(PushTemporalUnit,
+     ReturnsErrorWhenSamplesAreTrimmedFromStartAfterFirstUntrimmedSample) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  absl::flat_hash_map<uint32_t, AudioElementWithData> audio_elements;
+  std::list<MixPresentationObu> mix_presentation_obus;
+  const std::list<ParameterBlockWithData> kNoParameterBlocks;
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  std::list<AudioFrameWithData> first_audio_frame;
+  InitializeOneFrameIaSequenceWithMixPresentation(
+      codec_config_obus, audio_elements, mix_presentation_obus,
+      first_audio_frame);
+  first_audio_frame.back().obu.header_.num_samples_to_trim_at_start = 0;
+  const auto first_temporal_unit = TemporalUnitView::Create(
+      kNoParameterBlocks, first_audio_frame, kNoArbitraryObus);
+  ASSERT_THAT(first_temporal_unit, IsOk());
+  // Corrupt the data by adding a second frame with samples trimmed from the
+  // start, after the first frame had no trimmed samples.
+  std::list<AudioFrameWithData> second_audio_frame;
+  AddEmptyAudioFrameWithAudioElementIdSubstreamIdAndTimestamps(
+      kFirstAudioElementId, kFirstSubstreamId, kSecondTimestamp,
+      kThirdTimestamp, audio_elements, second_audio_frame);
+  second_audio_frame.back().obu.header_.num_samples_to_trim_at_start = 1;
+  const auto second_temporal_unit = TemporalUnitView::Create(
+      kNoParameterBlocks, second_audio_frame, kNoArbitraryObus);
+  ASSERT_THAT(second_temporal_unit, IsOk());
+  MockObuSequencer mock_obu_sequencer(*LebGenerator::Create(),
+                                      kDoNotIncludeTemporalDelimiters,
+                                      kDelayDescriptorsUntilTrimAtStartIsKnown);
+  EXPECT_THAT(mock_obu_sequencer.PushDescriptorObus(
+                  kIaSequenceHeader, codec_config_obus, audio_elements,
+                  mix_presentation_obus, kNoArbitraryObus),
+              IsOk());
+  EXPECT_THAT(mock_obu_sequencer.PushTemporalUnit(*first_temporal_unit),
+              IsOk());
+
+  // The second temporal unit is corrupt, because it has samples trimmed from
+  // the start after the first temporal unit had no trimmed samples.
+  EXPECT_THAT(mock_obu_sequencer.PushTemporalUnit(*second_temporal_unit),
+              Not(IsOk()));
+}
+
 TEST(PickAndPlace,
      ReturnsErrorWhenSamplesAreTrimmedFromStartAfterFirstUntrimmedSample) {
   const IASequenceHeaderObu kIaSequenceHeader(
@@ -1025,6 +1388,43 @@
               Not(IsOk()));
 }
 
+TEST(PushTemporalUnit, ForwardsObusToPushSerializedTemporalUnit) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  absl::flat_hash_map<uint32_t, AudioElementWithData> audio_elements;
+  std::list<MixPresentationObu> mix_presentation_obus;
+  std::list<AudioFrameWithData> audio_frames;
+  DemixingParamDefinition param_definition =
+      CreateDemixingParamDefinition(kFirstDemixingParameterId);
+  std::list<ParameterBlockWithData> parameter_blocks;
+  InitializeOneParameterBlockAndOneAudioFrame(
+      param_definition, parameter_blocks, audio_frames, codec_config_obus,
+      audio_elements);
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  const auto temporal_unit = TemporalUnitView::Create(
+      parameter_blocks, audio_frames, kNoArbitraryObus);
+  ASSERT_THAT(temporal_unit, IsOk());
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+  EXPECT_THAT(mock_obu_sequencer.PushDescriptorObus(
+                  kIaSequenceHeader, codec_config_obus, audio_elements,
+                  mix_presentation_obus, kNoArbitraryObus),
+              IsOk());
+
+  // The spec prescribes an order among different types of OBUs.
+  const std::vector<uint8_t> serialized_temporal_unit =
+      SerializeObusExpectOk(std::list<const ObuBase*>{
+          parameter_blocks.front().obu.get(), &audio_frames.front().obu});
+  EXPECT_CALL(mock_obu_sequencer,
+              PushSerializedTemporalUnit(
+                  _, _, MakeConstSpan(serialized_temporal_unit)));
+
+  EXPECT_THAT(mock_obu_sequencer.PushTemporalUnit(*temporal_unit), IsOk());
+}
+
 TEST(PickAndPlace, ForwardsObusToPushSerializedTemporalUnit) {
   const IASequenceHeaderObu kIaSequenceHeader(
       ObuHeader(), IASequenceHeaderObu::kIaCode,
@@ -1059,6 +1459,44 @@
               IsOk());
 }
 
+TEST(PushTemporalUnit, ForwardsArbitraryObusToPushSerializedTemporalUnit) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  absl::flat_hash_map<uint32_t, AudioElementWithData> audio_elements;
+  std::list<MixPresentationObu> mix_presentation_obus;
+  std::list<AudioFrameWithData> audio_frames;
+  InitializeOneFrameIaSequenceWithMixPresentation(
+      codec_config_obus, audio_elements, mix_presentation_obus, audio_frames);
+  const std::list<ParameterBlockWithData> kNoParameterBlocks;
+  const std::list<ArbitraryObu> kArbitraryObuBeforeFirstAudioFrame(
+      {ArbitraryObu(kObuIaReserved25, ObuHeader(), {},
+                    ArbitraryObu::kInsertionHookAfterAudioFramesAtTick,
+                    kFirstTimestamp)});
+  const auto first_temporal_unit = TemporalUnitView::Create(
+      kNoParameterBlocks, audio_frames, kArbitraryObuBeforeFirstAudioFrame);
+  ASSERT_THAT(first_temporal_unit, IsOk());
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+  EXPECT_THAT(mock_obu_sequencer.PushDescriptorObus(
+                  kIaSequenceHeader, codec_config_obus, audio_elements,
+                  mix_presentation_obus, kArbitraryObuBeforeFirstAudioFrame),
+              IsOk());
+
+  // Custom arbitrary OBUs can be placed according to their hook.
+  const std::vector<uint8_t> serialized_audio_frame = SerializeObusExpectOk(
+      std::list<const ObuBase*>{&audio_frames.front().obu,
+                                &kArbitraryObuBeforeFirstAudioFrame.front()});
+  EXPECT_CALL(
+      mock_obu_sequencer,
+      PushSerializedTemporalUnit(_, _, MakeConstSpan(serialized_audio_frame)));
+
+  EXPECT_THAT(mock_obu_sequencer.PushTemporalUnit(*first_temporal_unit),
+              IsOk());
+}
+
 TEST(PickAndPlace, ForwardsArbitraryObusToPushSerializedTemporalUnit) {
   const IASequenceHeaderObu kIaSequenceHeader(
       ObuHeader(), IASequenceHeaderObu::kIaCode,
@@ -1093,7 +1531,28 @@
               IsOk());
 }
 
-TEST(PickAndPlace, CallsFlushWhenDone) {
+TEST(Close, CallsCloseDerived) {
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+
+  // `CloseDerived` is called when done, which allows concrete implementation to
+  // finalize and optionally close their output streams.
+  EXPECT_CALL(mock_obu_sequencer, CloseDerived());
+
+  EXPECT_THAT(mock_obu_sequencer.Close(), IsOk());
+}
+
+TEST(Close, FailsWhenCalledTwice) {
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+  EXPECT_THAT(mock_obu_sequencer.Close(), IsOk());
+
+  EXPECT_THAT(mock_obu_sequencer.Close(), Not(IsOk()));
+}
+
+TEST(PickAndPlace, CallsCloseDerivedWhenDone) {
   const IASequenceHeaderObu kIaSequenceHeader(
       ObuHeader(), IASequenceHeaderObu::kIaCode,
       ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
@@ -1109,9 +1568,9 @@
       *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
       kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
 
-  // `Flush` is called when done, which allows concrete implementation to
+  // `CloseDerived` is called when done, which allows concrete implementation to
   // finalize and optionally close their output streams.
-  EXPECT_CALL(mock_obu_sequencer, Flush());
+  EXPECT_CALL(mock_obu_sequencer, CloseDerived());
 
   EXPECT_THAT(mock_obu_sequencer.PickAndPlace(
                   kIaSequenceHeader, kNoCodecConfigObus, kNoAudioElements,
@@ -1120,7 +1579,46 @@
               IsOk());
 }
 
-TEST(PickAndPlace, CallsAbortWhenPushDescriptorObusFails) {
+TEST(Abort, CallsAbortDerived) {
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+
+  // `CloseDerived` is called when done, which allows concrete implementation to
+  // finalize and optionally close their output streams.
+  EXPECT_CALL(mock_obu_sequencer, AbortDerived());
+
+  mock_obu_sequencer.Abort();
+}
+
+TEST(PushDescriptorObus, CallsAbortDerivedWhenPushDescriptorObusFails) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  absl::flat_hash_map<uint32_t, AudioElementWithData> audio_elements;
+  std::list<MixPresentationObu> mix_presentation_obus;
+  InitializeDescriptorObusForTwoMonoAmbisonicsAudioElement(
+      codec_config_obus, audio_elements, mix_presentation_obus);
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+
+  // If `PushSerializedDescriptorObus` fails, `Abort` is called. This allows
+  // concrete implementation to clean up and remove the file in one place.
+  EXPECT_CALL(mock_obu_sequencer,
+              PushSerializedDescriptorObus(_, _, _, _, _, _))
+      .WillOnce(Return(absl::InternalError("")));
+  EXPECT_CALL(mock_obu_sequencer, AbortDerived()).Times(1);
+
+  EXPECT_THAT(mock_obu_sequencer.PushDescriptorObus(
+                  kIaSequenceHeader, codec_config_obus, audio_elements,
+                  mix_presentation_obus, kNoArbitraryObus),
+              Not(IsOk()));
+}
+
+TEST(PickAndPlace, CallsAbortDerivedWhenPushDescriptorObusFails) {
   const IASequenceHeaderObu kIaSequenceHeader(
       ObuHeader(), IASequenceHeaderObu::kIaCode,
       ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
@@ -1141,7 +1639,7 @@
   EXPECT_CALL(mock_obu_sequencer,
               PushSerializedDescriptorObus(_, _, _, _, _, _))
       .WillOnce(Return(absl::InternalError("")));
-  EXPECT_CALL(mock_obu_sequencer, Abort()).Times(1);
+  EXPECT_CALL(mock_obu_sequencer, AbortDerived()).Times(1);
 
   EXPECT_THAT(mock_obu_sequencer.PickAndPlace(
                   kIaSequenceHeader, codec_config_obus, audio_elements,
@@ -1150,7 +1648,40 @@
               Not(IsOk()));
 }
 
-TEST(PickAndPlace, CallsAbortWhenPushAllTemporalUnitsFails) {
+TEST(PushTemporalUnit, CallsAbortDerivedWhenPushAllTemporalUnitsFails) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  absl::flat_hash_map<uint32_t, AudioElementWithData> audio_elements;
+  std::list<MixPresentationObu> mix_presentation_obus;
+  std::list<AudioFrameWithData> audio_frames;
+  InitializeOneFrameIaSequenceWithMixPresentation(
+      codec_config_obus, audio_elements, mix_presentation_obus, audio_frames);
+  const std::list<ParameterBlockWithData> kNoParameterBlocks;
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  const auto temporal_unit = TemporalUnitView::Create(
+      kNoParameterBlocks, audio_frames, kNoArbitraryObus);
+  ASSERT_THAT(temporal_unit, IsOk());
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+  EXPECT_THAT(mock_obu_sequencer.PushDescriptorObus(
+                  kIaSequenceHeader, codec_config_obus, audio_elements,
+                  mix_presentation_obus, kNoArbitraryObus),
+              IsOk());
+
+  // If `PushSerializedTemporalUnit` fails, `AbortDerived` is called. This
+  // allows concrete implementation to clean up and remove the file in one
+  // place.
+  EXPECT_CALL(mock_obu_sequencer, PushSerializedTemporalUnit(_, _, _))
+      .WillOnce(Return(absl::InternalError("")));
+  EXPECT_CALL(mock_obu_sequencer, AbortDerived()).Times(1);
+
+  EXPECT_THAT(mock_obu_sequencer.PushTemporalUnit(*temporal_unit), Not(IsOk()));
+}
+
+TEST(PickAndPlace, CallsAbortDerivedWhenPushAllTemporalUnitsFails) {
   const IASequenceHeaderObu kIaSequenceHeader(
       ObuHeader(), IASequenceHeaderObu::kIaCode,
       ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
@@ -1166,11 +1697,12 @@
       *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
       kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
 
-  // If `PushSerializedTemporalUnit` fails, `Abort` is called. This allows
-  // concrete implementation to clean up and remove the file in one place.
+  // If `PushSerializedTemporalUnit` fails, `AbortDerived` is called. This
+  // allows concrete implementation to clean up and remove the file in one
+  // place.
   EXPECT_CALL(mock_obu_sequencer, PushSerializedTemporalUnit(_, _, _))
       .WillOnce(Return(absl::InternalError("")));
-  EXPECT_CALL(mock_obu_sequencer, Abort()).Times(1);
+  EXPECT_CALL(mock_obu_sequencer, AbortDerived()).Times(1);
 
   EXPECT_THAT(mock_obu_sequencer.PickAndPlace(
                   kIaSequenceHeader, codec_config_obus, audio_elements,
@@ -1179,5 +1711,261 @@
               Not(IsOk()));
 }
 
+TEST(PushTemporalUnit, FailsWhenBeforePushDescriptorObus) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  absl::flat_hash_map<uint32_t, AudioElementWithData> audio_elements;
+  std::list<MixPresentationObu> mix_presentation_obus;
+  std::list<AudioFrameWithData> audio_frames;
+  InitializeOneFrameIaSequenceWithMixPresentation(
+      codec_config_obus, audio_elements, mix_presentation_obus, audio_frames);
+  const std::list<ParameterBlockWithData> kNoParameterBlocks;
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  const auto temporal_unit = TemporalUnitView::Create(
+      kNoParameterBlocks, audio_frames, kNoArbitraryObus);
+  ASSERT_THAT(temporal_unit, IsOk());
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+  // Omitted call to `PushDescriptorObus`. We can't accept temporal units yet.
+
+  EXPECT_THAT(mock_obu_sequencer.PushTemporalUnit(*temporal_unit), Not(IsOk()));
+}
+
+TEST(PushTemporalUnit, FailsWhenCalledAfterClose) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  absl::flat_hash_map<uint32_t, AudioElementWithData> audio_elements;
+  std::list<MixPresentationObu> mix_presentation_obus;
+  std::list<AudioFrameWithData> audio_frames;
+  InitializeOneFrameIaSequenceWithMixPresentation(
+      codec_config_obus, audio_elements, mix_presentation_obus, audio_frames);
+  const std::list<ParameterBlockWithData> kNoParameterBlocks;
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  const auto temporal_unit = TemporalUnitView::Create(
+      kNoParameterBlocks, audio_frames, kNoArbitraryObus);
+  ASSERT_THAT(temporal_unit, IsOk());
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+  EXPECT_THAT(mock_obu_sequencer.Close(), IsOk());
+
+  EXPECT_THAT(mock_obu_sequencer.PushTemporalUnit(*temporal_unit), Not(IsOk()));
+}
+
+TEST(PushTemporalUnit, FailsWhenCalledAfterAbort) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  absl::flat_hash_map<uint32_t, AudioElementWithData> audio_elements;
+  std::list<MixPresentationObu> mix_presentation_obus;
+  std::list<AudioFrameWithData> audio_frames;
+  InitializeOneFrameIaSequenceWithMixPresentation(
+      codec_config_obus, audio_elements, mix_presentation_obus, audio_frames);
+  const std::list<ParameterBlockWithData> kNoParameterBlocks;
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  const auto temporal_unit = TemporalUnitView::Create(
+      kNoParameterBlocks, audio_frames, kNoArbitraryObus);
+  ASSERT_THAT(temporal_unit, IsOk());
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+  mock_obu_sequencer.Abort();
+
+  EXPECT_THAT(mock_obu_sequencer.PushTemporalUnit(*temporal_unit), Not(IsOk()));
+}
+
+TEST(UpdateDescriptorObusAndClose,
+     ForwardsDescriptorObusToPushFinalizedDescriptorObus) {
+  const IASequenceHeaderObu kOriginalIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  const IASequenceHeaderObu kUpdatedIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfBaseProfile, ProfileVersion::kIamfBaseProfile);
+  const absl::flat_hash_map<DecodedUleb128, CodecConfigObu> kNoCodecConfigObus;
+  const absl::flat_hash_map<uint32_t, AudioElementWithData> kNoAudioElements;
+  const std::list<MixPresentationObu> kNoMixPresentationObus;
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+  EXPECT_THAT(mock_obu_sequencer.PushDescriptorObus(
+                  kOriginalIaSequenceHeader, kNoCodecConfigObus,
+                  kNoAudioElements, kNoMixPresentationObus, kNoArbitraryObus),
+              IsOk());
+  const auto expected_finalized_descriptor_obus = SerializeObusExpectOk(
+      std::list<const ObuBase*>{&kUpdatedIaSequenceHeader});
+
+  // Several properties should match values derived from the descriptor
+  // OBUs.
+  EXPECT_CALL(mock_obu_sequencer, PushFinalizedDescriptorObus(MakeConstSpan(
+                                      expected_finalized_descriptor_obus)));
+
+  EXPECT_THAT(mock_obu_sequencer.UpdateDescriptorObusAndClose(
+                  kUpdatedIaSequenceHeader, kNoCodecConfigObus,
+                  kNoAudioElements, kNoMixPresentationObus, kNoArbitraryObus),
+              IsOk());
+}
+
+TEST(UpdateDescriptorObusAndClose, FailsBeforePushDescriptorObus) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  const absl::flat_hash_map<DecodedUleb128, CodecConfigObu> kNoCodecConfigObus;
+  const absl::flat_hash_map<uint32_t, AudioElementWithData> kNoAudioElements;
+  const std::list<MixPresentationObu> kNoMixPresentationObus;
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+
+  EXPECT_THAT(mock_obu_sequencer.UpdateDescriptorObusAndClose(
+                  kIaSequenceHeader, kNoCodecConfigObus, kNoAudioElements,
+                  kNoMixPresentationObus, kNoArbitraryObus),
+              Not(IsOk()));
+}
+
+TEST(UpdateDescriptorObusAndClose, SubsequentCloseCallsFails) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  const absl::flat_hash_map<DecodedUleb128, CodecConfigObu> kNoCodecConfigObus;
+  const absl::flat_hash_map<uint32_t, AudioElementWithData> kNoAudioElements;
+  const std::list<MixPresentationObu> kNoMixPresentationObus;
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+  EXPECT_THAT(mock_obu_sequencer.PushDescriptorObus(
+                  kIaSequenceHeader, kNoCodecConfigObus, kNoAudioElements,
+                  kNoMixPresentationObus, kNoArbitraryObus),
+              IsOk());
+  EXPECT_THAT(mock_obu_sequencer.UpdateDescriptorObusAndClose(
+                  kIaSequenceHeader, kNoCodecConfigObus, kNoAudioElements,
+                  kNoMixPresentationObus, kNoArbitraryObus),
+              IsOk());
+
+  EXPECT_THAT(mock_obu_sequencer.Close(), Not(IsOk()));
+}
+
+TEST(UpdateDescriptorObusAndClose, CallsCloseDerived) {
+  const IASequenceHeaderObu kOriginalIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  const IASequenceHeaderObu kUpdatedIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfBaseProfile, ProfileVersion::kIamfBaseProfile);
+  const absl::flat_hash_map<DecodedUleb128, CodecConfigObu> kNoCodecConfigObus;
+  const absl::flat_hash_map<uint32_t, AudioElementWithData> kNoAudioElements;
+  const std::list<MixPresentationObu> kNoMixPresentationObus;
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+  EXPECT_THAT(mock_obu_sequencer.PushDescriptorObus(
+                  kOriginalIaSequenceHeader, kNoCodecConfigObus,
+                  kNoAudioElements, kNoMixPresentationObus, kNoArbitraryObus),
+              IsOk());
+  EXPECT_CALL(mock_obu_sequencer, CloseDerived()).Times(1);
+
+  EXPECT_THAT(mock_obu_sequencer.UpdateDescriptorObusAndClose(
+                  kUpdatedIaSequenceHeader, kNoCodecConfigObus,
+                  kNoAudioElements, kNoMixPresentationObus, kNoArbitraryObus),
+              IsOk());
+}
+
+TEST(UpdateDescriptorObusAndClose,
+     CallsAbortDerivedWhenPushFinalizedDescriptorObusFails) {
+  const IASequenceHeaderObu kOriginalIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  const IASequenceHeaderObu kUpdatedIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfBaseProfile, ProfileVersion::kIamfBaseProfile);
+  const absl::flat_hash_map<DecodedUleb128, CodecConfigObu> kNoCodecConfigObus;
+  const absl::flat_hash_map<uint32_t, AudioElementWithData> kNoAudioElements;
+  const std::list<MixPresentationObu> kNoMixPresentationObus;
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+  EXPECT_THAT(mock_obu_sequencer.PushDescriptorObus(
+                  kOriginalIaSequenceHeader, kNoCodecConfigObus,
+                  kNoAudioElements, kNoMixPresentationObus, kNoArbitraryObus),
+              IsOk());
+  ON_CALL(mock_obu_sequencer, PushFinalizedDescriptorObus(_))
+      .WillByDefault(Return(absl::InternalError("")));
+  EXPECT_CALL(mock_obu_sequencer, AbortDerived()).Times(1);
+
+  EXPECT_THAT(mock_obu_sequencer.UpdateDescriptorObusAndClose(
+                  kUpdatedIaSequenceHeader, kNoCodecConfigObus,
+                  kNoAudioElements, kNoMixPresentationObus, kNoArbitraryObus),
+              Not(IsOk()));
+}
+
+TEST(UpdateDescriptorObusAndClose, FailsWhenSerializedSizeChanges) {
+  const IASequenceHeaderObu kOriginalIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  const IASequenceHeaderObu kResizedIaSequenceHeader(
+      ObuHeader{.obu_extension_flag = true, .extension_header_size = 0},
+      IASequenceHeaderObu::kIaCode, ProfileVersion::kIamfBaseProfile,
+      ProfileVersion::kIamfBaseProfile);
+  const absl::flat_hash_map<DecodedUleb128, CodecConfigObu> kNoCodecConfigObus;
+  const absl::flat_hash_map<uint32_t, AudioElementWithData> kNoAudioElements;
+  const std::list<MixPresentationObu> kNoMixPresentationObus;
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+  EXPECT_THAT(mock_obu_sequencer.PushDescriptorObus(
+                  kOriginalIaSequenceHeader, kNoCodecConfigObus,
+                  kNoAudioElements, kNoMixPresentationObus, kNoArbitraryObus),
+              IsOk());
+
+  // Derived classes may assume the descriptor OBUs are the same size, to
+  // permit writes in place. We could lift this restriction, but it's not
+  // clear it's worth the effort.
+  EXPECT_THAT(mock_obu_sequencer.UpdateDescriptorObusAndClose(
+                  kResizedIaSequenceHeader, kNoCodecConfigObus,
+                  kNoAudioElements, kNoMixPresentationObus, kNoArbitraryObus),
+              Not(IsOk()));
+}
+
+TEST(UpdateDescriptorObusAndClose, FailsWhenCodecConfigPropertiesChange) {
+  const IASequenceHeaderObu kIaSequenceHeader(
+      ObuHeader(), IASequenceHeaderObu::kIaCode,
+      ProfileVersion::kIamfSimpleProfile, ProfileVersion::kIamfBaseProfile);
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu>
+      original_codec_config_obus;
+  AddLpcmCodecConfigWithIdAndSampleRate(kCodecConfigId, 48000,
+                                        original_codec_config_obus);
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu>
+      modified_codec_config_obus;
+  AddLpcmCodecConfigWithIdAndSampleRate(kCodecConfigId, 44100,
+                                        modified_codec_config_obus);
+  const absl::flat_hash_map<uint32_t, AudioElementWithData> kNoAudioElements;
+  const std::list<MixPresentationObu> kNoMixPresentationObus;
+  const std::list<ArbitraryObu> kNoArbitraryObus;
+  MockObuSequencer mock_obu_sequencer(
+      *LebGenerator::Create(), kDoNotIncludeTemporalDelimiters,
+      kDoNotDelayDescriptorsUntilTrimAtStartIsKnown);
+  EXPECT_THAT(mock_obu_sequencer.PushDescriptorObus(
+                  kIaSequenceHeader, original_codec_config_obus,
+                  kNoAudioElements, kNoMixPresentationObus, kNoArbitraryObus),
+              IsOk());
+
+  EXPECT_THAT(mock_obu_sequencer.UpdateDescriptorObusAndClose(
+                  kIaSequenceHeader, modified_codec_config_obus,
+                  kNoAudioElements, kNoMixPresentationObus, kNoArbitraryObus),
+              Not(IsOk()));
+}
+
 }  // namespace
 }  // namespace iamf_tools
diff --git a/iamf/cli/tests/obu_sequencer_iamf_test.cc b/iamf/cli/tests/obu_sequencer_iamf_test.cc
index 3e539f8..12c9078 100644
--- a/iamf/cli/tests/obu_sequencer_iamf_test.cc
+++ b/iamf/cli/tests/obu_sequencer_iamf_test.cc
@@ -28,6 +28,7 @@
 #include "iamf/cli/audio_element_with_data.h"
 #include "iamf/cli/audio_frame_with_data.h"
 #include "iamf/cli/parameter_block_with_data.h"
+#include "iamf/cli/temporal_unit_view.h"
 #include "iamf/cli/tests/cli_test_utils.h"
 #include "iamf/common/leb_generator.h"
 #include "iamf/common/read_bit_buffer.h"
@@ -126,7 +127,7 @@
   });
 }
 
-class ObuSequencerTest : public ::testing::Test {
+class ObuSequencerIamfTest : public ::testing::Test {
  public:
   void InitializeDescriptorObus() {
     ia_sequence_header_obu_.emplace(ObuHeader(), IASequenceHeaderObu::kIaCode,
@@ -270,7 +271,7 @@
               IsOk());
 }
 
-TEST_F(ObuSequencerTest, PickAndPlaceCreatesFileWithOneFrameIaSequence) {
+TEST_F(ObuSequencerIamfTest, PickAndPlaceCreatesFileWithOneFrameIaSequence) {
   const std::string kOutputIamfFilename = GetAndCleanupOutputFileName(".iamf");
   InitObusForOneFrameIaSequence();
   ObuSequencerIamf sequencer(kOutputIamfFilename,
@@ -286,7 +287,7 @@
   EXPECT_TRUE(std::filesystem::exists(kOutputIamfFilename));
 }
 
-TEST_F(ObuSequencerTest, PickAndPlaceFileCanBeReadBacks) {
+TEST_F(ObuSequencerIamfTest, PickAndPlaceFileCanBeReadBacks) {
   const std::string kOutputIamfFilename = GetAndCleanupOutputFileName(".iamf");
   InitializeDescriptorObus();
   AddEmptyAudioFrameWithAudioElementIdSubstreamIdAndTimestamps(
@@ -327,7 +328,8 @@
   EXPECT_TRUE(parameter_blocks.empty());
 }
 
-TEST_F(ObuSequencerTest, PickAndPlaceLeavesNoFileWhenDescriptorsAreInvalid) {
+TEST_F(ObuSequencerIamfTest,
+       PickAndPlaceLeavesNoFileWhenDescriptorsAreInvalid) {
   constexpr uint32_t kInvalidIaCode = IASequenceHeaderObu::kIaCode + 1;
   const std::string kOutputIamfFilename = GetAndCleanupOutputFileName(".iamf");
   InitObusForOneFrameIaSequence();
@@ -349,7 +351,8 @@
   EXPECT_FALSE(std::filesystem::exists(kOutputIamfFilename));
 }
 
-TEST_F(ObuSequencerTest, PickAndPlaceLeavesNoFileWhenTemporalUnitsAreInvalid) {
+TEST_F(ObuSequencerIamfTest,
+       PickAndPlaceLeavesNoFileWhenTemporalUnitsAreInvalid) {
   constexpr bool kInvalidateTemporalUnit = true;
   const std::string kOutputIamfFilename = GetAndCleanupOutputFileName(".iamf");
   InitObusForOneFrameIaSequence();
@@ -371,7 +374,7 @@
   EXPECT_FALSE(std::filesystem::exists(kOutputIamfFilename));
 }
 
-TEST_F(ObuSequencerTest,
+TEST_F(ObuSequencerIamfTest,
        PickAndPlaceOnInvalidTemporalUnitFailsWhenOutputFileIsOmitted) {
   constexpr bool kInvalidateTemporalUnit = true;
   InitObusForOneFrameIaSequence();
@@ -391,5 +394,57 @@
                    .ok());
 }
 
+TEST_F(ObuSequencerIamfTest,
+       FileContainsUpdatedDescriptorObusAfterUpdateDescriptorObusAndClose) {
+  const ProfileVersion kOriginalProfile = ProfileVersion::kIamfBaseProfile;
+  const ProfileVersion kUpdatedProfile =
+      ProfileVersion::kIamfBaseEnhancedProfile;
+  InitObusForOneFrameIaSequence();
+  parameter_blocks_.clear();
+  ia_sequence_header_obu_ =
+      IASequenceHeaderObu(ObuHeader(), IASequenceHeaderObu::kIaCode,
+                          kOriginalProfile, kOriginalProfile);
+  const std::string kOutputIamfFilename = GetAndCleanupOutputFileName(".iamf");
+  ObuSequencerIamf sequencer(kOutputIamfFilename,
+                             kDoNotIncludeTemporalDelimiters,
+                             *LebGenerator::Create());
+  EXPECT_THAT(sequencer.PushDescriptorObus(
+                  *ia_sequence_header_obu_, codec_config_obus_, audio_elements_,
+                  mix_presentation_obus_, arbitrary_obus_),
+              IsOk());
+  const auto temporal_unit = TemporalUnitView::Create(
+      parameter_blocks_, audio_frames_, arbitrary_obus_);
+  ASSERT_THAT(temporal_unit, IsOk());
+  EXPECT_THAT(sequencer.PushTemporalUnit(*temporal_unit), IsOk());
+  // As a toy example, we will update the IA Sequence Header.
+  ia_sequence_header_obu_ =
+      IASequenceHeaderObu(ObuHeader(), IASequenceHeaderObu::kIaCode,
+                          kUpdatedProfile, kUpdatedProfile);
+
+  // Finalize the descriptor OBUs with a new IA Sequence Header.
+  EXPECT_THAT(sequencer.UpdateDescriptorObusAndClose(
+                  *ia_sequence_header_obu_, codec_config_obus_, audio_elements_,
+                  mix_presentation_obus_, arbitrary_obus_),
+              IsOk());
+
+  auto read_bit_buffer = FileBasedReadBitBuffer::CreateFromFilePath(
+      kReadBitBufferCapacity, kOutputIamfFilename);
+  ASSERT_NE(read_bit_buffer, nullptr);
+  IASequenceHeaderObu read_ia_sequence_header;
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> read_codec_config_obus;
+  absl::flat_hash_map<DecodedUleb128, AudioElementWithData> read_audio_elements;
+  std::list<MixPresentationObu> read_mix_presentation_obus;
+  std::list<AudioFrameWithData> read_audio_frames;
+  std::list<ParameterBlockWithData> read_parameter_blocks;
+  EXPECT_THAT(
+      CollectObusFromIaSequence(*read_bit_buffer, read_ia_sequence_header,
+                                read_codec_config_obus, read_audio_elements,
+                                read_mix_presentation_obus, read_audio_frames,
+                                read_parameter_blocks),
+      IsOk());
+  // Finally we expect to see evidence of the modified IA Sequence Header.
+  EXPECT_EQ(read_ia_sequence_header.GetPrimaryProfile(), kUpdatedProfile);
+}
+
 }  // namespace
 }  // namespace iamf_tools
diff --git a/iamf/cli/tests/profile_filter_test.cc b/iamf/cli/tests/profile_filter_test.cc
index 8154681..9b90334 100644
--- a/iamf/cli/tests/profile_filter_test.cc
+++ b/iamf/cli/tests/profile_filter_test.cc
@@ -453,7 +453,6 @@
       kCommonMixGainParameterRate;
   common_mix_gain_param_definition.param_definition_mode_ = true;
   common_mix_gain_param_definition.default_mix_gain_ = 0;
-  constexpr DecodedUleb128 kNumAudioElementsPerSubmix = 1;
   const RenderingConfig kRenderingConfig = {
       .headphones_rendering_mode =
           RenderingConfig::kHeadphonesRenderingModeStereo,
@@ -471,30 +470,26 @@
       .loudness = {
           .info_type = 0, .integrated_loudness = 0, .digital_peak = 0}};
   std::vector<MixPresentationSubMix> sub_mixes;
-  sub_mixes.push_back({.num_audio_elements = kNumAudioElementsPerSubmix,
-                       .audio_elements = {{
+  sub_mixes.push_back({.audio_elements = {{
                            .audio_element_id = kFirstAudioElementId,
                            .localized_element_annotations = {},
                            .rendering_config = kRenderingConfig,
                            .element_mix_gain = common_mix_gain_param_definition,
                        }},
                        .output_mix_gain = common_mix_gain_param_definition,
-                       .num_layouts = 1,
                        .layouts = {kStereoLayout}});
-  sub_mixes.push_back({.num_audio_elements = kNumAudioElementsPerSubmix,
-                       .audio_elements = {{
+  sub_mixes.push_back({.audio_elements = {{
                            .audio_element_id = kSecondAudioElementId,
                            .localized_element_annotations = {},
                            .rendering_config = kRenderingConfig,
                            .element_mix_gain = common_mix_gain_param_definition,
                        }},
                        .output_mix_gain = common_mix_gain_param_definition,
-                       .num_layouts = 1,
                        .layouts = {kStereoLayout}});
 
-  mix_presentation_obus.push_back(MixPresentationObu(
-      ObuHeader(), kFirstMixPresentationId,
-      /*count_label=*/0, {}, {}, sub_mixes.size(), sub_mixes));
+  mix_presentation_obus.push_back(
+      MixPresentationObu(ObuHeader(), kFirstMixPresentationId,
+                         /*count_label=*/0, {}, {}, sub_mixes));
 }
 
 TEST(FilterProfilesForMixPresentation,
diff --git a/iamf/cli/tests/temporal_unit_view_test.cc b/iamf/cli/tests/temporal_unit_view_test.cc
index 965aa27..10e3760 100644
--- a/iamf/cli/tests/temporal_unit_view_test.cc
+++ b/iamf/cli/tests/temporal_unit_view_test.cc
@@ -288,6 +288,40 @@
   EXPECT_EQ(temporal_unit->arbitrary_obus_, arbitrary_obus_ptrs);
 }
 
+TEST(Create, SetsStartTimestamp) {
+  constexpr InternalTimestamp kExpectedStartTimestamp = 123456789;
+  constexpr InternalTimestamp kEndTimestamp = kExpectedStartTimestamp + 8;
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  absl::flat_hash_map<uint32_t, AudioElementWithData> audio_elements;
+  std::list<AudioFrameWithData> audio_frames;
+  InitializeOneFrameIaSequence(codec_config_obus, audio_elements, audio_frames);
+  audio_frames.front().start_timestamp = kExpectedStartTimestamp;
+  audio_frames.front().end_timestamp = kEndTimestamp;
+
+  const auto temporal_unit = TemporalUnitView::Create(
+      kNoParameterBlocks, audio_frames, kNoArbitraryObus);
+  ASSERT_THAT(temporal_unit, IsOk());
+
+  EXPECT_EQ(temporal_unit->start_timestamp_, kExpectedStartTimestamp);
+}
+
+TEST(Create, SetsEndTimestamp) {
+  constexpr InternalTimestamp kStartTimestamp = 123456789;
+  constexpr InternalTimestamp kExpectedEndTimestamp = kStartTimestamp + 8;
+  absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
+  absl::flat_hash_map<uint32_t, AudioElementWithData> audio_elements;
+  std::list<AudioFrameWithData> audio_frames;
+  InitializeOneFrameIaSequence(codec_config_obus, audio_elements, audio_frames);
+  audio_frames.front().start_timestamp = kStartTimestamp;
+  audio_frames.front().end_timestamp = kExpectedEndTimestamp;
+
+  const auto temporal_unit = TemporalUnitView::Create(
+      kNoParameterBlocks, audio_frames, kNoArbitraryObus);
+  ASSERT_THAT(temporal_unit, IsOk());
+
+  EXPECT_EQ(temporal_unit->end_timestamp_, kExpectedEndTimestamp);
+}
+
 TEST(Create, SetsNumSamplesToTrimAtStart) {
   absl::flat_hash_map<DecodedUleb128, CodecConfigObu> codec_config_obus;
   absl::flat_hash_map<uint32_t, AudioElementWithData> audio_elements;
diff --git a/iamf/cli/wav_writer.cc b/iamf/cli/wav_writer.cc
index e67ef59..2e4269a 100644
--- a/iamf/cli/wav_writer.cc
+++ b/iamf/cli/wav_writer.cc
@@ -225,7 +225,7 @@
 
   std::vector<uint8_t> samples_as_pcm(num_channels * num_ticks * bit_depth_ / 8,
                                       0);
-  int write_position = 0;
+  size_t write_position = 0;
   for (const auto& tick : time_channel_samples) {
     for (const auto& channel_sample : tick) {
       RETURN_IF_NOT_OK(WritePcmSample(channel_sample, bit_depth_,
diff --git a/iamf/common/utils/sample_processing_utils.cc b/iamf/common/utils/sample_processing_utils.cc
index bde2910..0f0d13c 100644
--- a/iamf/common/utils/sample_processing_utils.cc
+++ b/iamf/common/utils/sample_processing_utils.cc
@@ -11,6 +11,7 @@
  */
 #include "iamf/common/utils/sample_processing_utils.h"
 
+#include <cstddef>
 #include <cstdint>
 
 #include "absl/log/check.h"
@@ -22,16 +23,16 @@
 
 absl::Status WritePcmSample(uint32_t sample, uint8_t sample_size,
                             bool big_endian, uint8_t* const buffer,
-                            int& write_position) {
+                            size_t& write_position) {
   // Validate assumptions of the logic in the `for` loop below.
-  if (sample_size % 8 != 0 || sample_size > 32) {
+  if (sample_size % 8 != 0 || sample_size > 32) [[unlikely]] {
     return absl::InvalidArgumentError(
         absl::StrCat("Invalid sample size: ", sample_size));
   }
 
   for (int shift = 32 - sample_size; shift < 32; shift += 8) {
     uint8_t byte = 0;
-    if (big_endian) {
+    if (big_endian) [[unlikely]] {
       byte = (sample >> ((32 - sample_size) + (32 - (shift + 8)))) & 0xff;
     } else {
       byte = (sample >> shift) & 0xff;
diff --git a/iamf/common/utils/sample_processing_utils.h b/iamf/common/utils/sample_processing_utils.h
index a4de471..d3f9f8a 100644
--- a/iamf/common/utils/sample_processing_utils.h
+++ b/iamf/common/utils/sample_processing_utils.h
@@ -44,7 +44,7 @@
  */
 absl::Status WritePcmSample(uint32_t sample, uint8_t sample_size,
                             bool big_endian, uint8_t* buffer,
-                            int& write_position);
+                            size_t& write_position);
 
 /*!\brief Arranges the input samples by time and channel.
  *
diff --git a/iamf/common/utils/tests/sample_processing_utils_test.cc b/iamf/common/utils/tests/sample_processing_utils_test.cc
index c06a260..6af390b 100644
--- a/iamf/common/utils/tests/sample_processing_utils_test.cc
+++ b/iamf/common/utils/tests/sample_processing_utils_test.cc
@@ -33,7 +33,7 @@
 
 TEST(WritePcmSample, LittleEndian32Bits) {
   std::vector<uint8_t> buffer(4, 0);
-  int write_position = 0;
+  size_t write_position = 0;
   EXPECT_THAT(WritePcmSample(0x12345678, 32, /*big_endian=*/false,
                              buffer.data(), write_position),
               IsOk());
@@ -44,7 +44,7 @@
 
 TEST(WritePcmSample, BigEndian32bits) {
   std::vector<uint8_t> buffer(4, 0);
-  int write_position = 0;
+  size_t write_position = 0;
   EXPECT_THAT(WritePcmSample(0x12345678, 32, /*big_endian=*/true, buffer.data(),
                              write_position),
               IsOk());
@@ -55,7 +55,7 @@
 
 TEST(WritePcmSample, LittleEndian24Bits) {
   std::vector<uint8_t> buffer(3, 0);
-  int write_position = 0;
+  size_t write_position = 0;
   EXPECT_THAT(WritePcmSample(0x12345600, 24, /*big_endian=*/false,
                              buffer.data(), write_position),
               IsOk());
@@ -66,7 +66,7 @@
 
 TEST(WritePcmSample, BigEndian24Bits) {
   std::vector<uint8_t> buffer(3, 0);
-  int write_position = 0;
+  size_t write_position = 0;
   EXPECT_THAT(WritePcmSample(0x12345600, 24, /*big_endian=*/true, buffer.data(),
                              write_position),
               IsOk());
@@ -77,7 +77,7 @@
 
 TEST(WritePcmSample, LittleEndian16Bits) {
   std::vector<uint8_t> buffer(2, 0);
-  int write_position = 0;
+  size_t write_position = 0;
   EXPECT_THAT(WritePcmSample(0x12340000, 16, /*big_endian=*/false,
                              buffer.data(), write_position),
               IsOk());
@@ -88,7 +88,7 @@
 
 TEST(WritePcmSample, BigEndian16Bits) {
   std::vector<uint8_t> buffer(2, 0);
-  int write_position = 0;
+  size_t write_position = 0;
   EXPECT_THAT(WritePcmSample(0x12340000, 16, /*big_endian=*/true, buffer.data(),
                              write_position),
               IsOk());
@@ -99,7 +99,7 @@
 
 TEST(WritePcmSample, InvalidOver32Bits) {
   std::vector<uint8_t> buffer(5, 0);
-  int write_position = 0;
+  size_t write_position = 0;
   EXPECT_EQ(WritePcmSample(0x00000000, 40, /*big_endian=*/false, buffer.data(),
                            write_position)
                 .code(),
diff --git a/iamf/obu/BUILD b/iamf/obu/BUILD
index 1715813..7ba1922 100644
--- a/iamf/obu/BUILD
+++ b/iamf/obu/BUILD
@@ -119,6 +119,7 @@
         "//iamf/common:write_bit_buffer",
         "//iamf/common/utils:macros",
         "//iamf/common/utils:map_utils",
+        "//iamf/common/utils:numeric_utils",
         "//iamf/common/utils:validation_utils",
         "@com_google_absl//absl/base:no_destructor",
         "@com_google_absl//absl/container:flat_hash_map",
diff --git a/iamf/obu/mix_presentation.cc b/iamf/obu/mix_presentation.cc
index cd25fe7..40b32c5 100644
--- a/iamf/obu/mix_presentation.cc
+++ b/iamf/obu/mix_presentation.cc
@@ -25,6 +25,7 @@
 #include "iamf/common/read_bit_buffer.h"
 #include "iamf/common/utils/macros.h"
 #include "iamf/common/utils/map_utils.h"
+#include "iamf/common/utils/numeric_utils.h"
 #include "iamf/common/utils/validation_utils.h"
 #include "iamf/common/write_bit_buffer.h"
 #include "iamf/obu/obu_header.h"
@@ -137,11 +138,11 @@
         layout.loudness.anchored_loudness.anchor_elements));
     const AnchoredLoudness& anchored_loudness =
         layout.loudness.anchored_loudness;
-    RETURN_IF_NOT_OK(
-        wb.WriteUnsignedLiteral(anchored_loudness.num_anchored_loudness, 8));
-    RETURN_IF_NOT_OK(ValidateContainerSizeEqual(
-        "anchor_elements", anchored_loudness.anchor_elements,
-        anchored_loudness.num_anchored_loudness));
+    uint8_t num_anchor_elements;
+    RETURN_IF_NOT_OK(StaticCastIfInRange(
+        "num_anchor_elements", anchored_loudness.anchor_elements.size(),
+        num_anchor_elements));
+    RETURN_IF_NOT_OK(wb.WriteUnsignedLiteral(num_anchor_elements, 8));
     for (const auto& anchor_element : anchored_loudness.anchor_elements) {
       RETURN_IF_NOT_OK(wb.WriteUnsignedLiteral(
           static_cast<uint8_t>(anchor_element.anchor_element), 8));
@@ -166,27 +167,25 @@
                                     const MixPresentationSubMix& sub_mix,
                                     WriteBitBuffer& wb) {
   // IAMF requires there to be at least one audio element.
-  RETURN_IF_NOT_OK(ValidateNotEqual(
-      DecodedUleb128{0}, sub_mix.num_audio_elements, "num_audio_elements"));
+  const DecodedUleb128 num_audio_elements = sub_mix.audio_elements.size();
+  RETURN_IF_NOT_OK(ValidateNotEqual(DecodedUleb128{0}, num_audio_elements,
+                                    "num_audio_elements"));
 
   // Write the main portion of a `MixPresentationSubMix`.
-  RETURN_IF_NOT_OK(wb.WriteUleb128(sub_mix.num_audio_elements));
+  RETURN_IF_NOT_OK(wb.WriteUleb128(num_audio_elements));
 
   // Loop to write the `audio_elements` array.
-  RETURN_IF_NOT_OK(ValidateContainerSizeEqual(
-      "audio_elements", sub_mix.audio_elements, sub_mix.num_audio_elements));
   for (const auto& sub_mix_audio_element : sub_mix.audio_elements) {
     RETURN_IF_NOT_OK(ValidateAndWriteSubMixAudioElement(
         count_label, sub_mix_audio_element, wb));
   }
 
   RETURN_IF_NOT_OK(sub_mix.output_mix_gain.ValidateAndWrite(wb));
-  RETURN_IF_NOT_OK(wb.WriteUleb128(sub_mix.num_layouts));
+  const DecodedUleb128 num_layouts = sub_mix.layouts.size();
+  RETURN_IF_NOT_OK(wb.WriteUleb128(num_layouts));
 
   // Loop to write the `layouts` array.
   bool found_stereo_layout = false;
-  RETURN_IF_NOT_OK(ValidateContainerSizeEqual("layouts", sub_mix.layouts,
-                                              sub_mix.num_layouts));
   for (const auto& layout : sub_mix.layouts) {
     RETURN_IF_NOT_OK(ValidateAndWriteLayout(layout, found_stereo_layout, wb));
   }
@@ -246,10 +245,10 @@
   }
   // Conditionally read `anchored_loudness` based on `info_type`.
   if (loudness.info_type & LoudnessInfo::kAnchoredLoudness) {
-    RETURN_IF_NOT_OK(rb.ReadUnsignedLiteral(
-        8, loudness.anchored_loudness.num_anchored_loudness));
+    uint8_t num_anchored_loudness;
+    RETURN_IF_NOT_OK(rb.ReadUnsignedLiteral(8, num_anchored_loudness));
 
-    for (int i = 0; i < loudness.anchored_loudness.num_anchored_loudness; ++i) {
+    for (int i = 0; i < num_anchored_loudness; ++i) {
       AnchoredLoudnessElement anchor_loudness_element;
       uint8_t anchor_element;
       RETURN_IF_NOT_OK(rb.ReadUnsignedLiteral(8, anchor_element));
@@ -308,6 +307,7 @@
 
 absl::Status MixPresentationSubMix::ReadAndValidate(const int32_t& count_label,
                                                     ReadBitBuffer& rb) {
+  DecodedUleb128 num_audio_elements;
   RETURN_IF_NOT_OK(rb.ReadULeb128(num_audio_elements));
   // IAMF requires there to be at least one audio element.
   RETURN_IF_NOT_OK(ValidateNotEqual(DecodedUleb128{0}, num_audio_elements,
@@ -319,6 +319,7 @@
   }
 
   RETURN_IF_NOT_OK(output_mix_gain.ReadAndValidate(rb));
+  DecodedUleb128 num_layouts;
   RETURN_IF_NOT_OK(rb.ReadULeb128(num_layouts));
   for (int i = 0; i < num_layouts; ++i) {
     MixPresentationLayout mix_presentation_layout;
@@ -343,8 +344,9 @@
 }
 
 absl::Status MixPresentationTags::ValidateAndWrite(WriteBitBuffer& wb) const {
+  uint8_t num_tags;
+  RETURN_IF_NOT_OK(StaticCastIfInRange("num_tags", tags.size(), num_tags));
   RETURN_IF_NOT_OK(wb.WriteUnsignedLiteral(num_tags, 8));
-  RETURN_IF_NOT_OK(ValidateContainerSizeEqual("tags", tags, num_tags));
 
   int count_content_language_tag = 0;
 
@@ -472,13 +474,12 @@
     RETURN_IF_NOT_OK(wb.WriteString(localized_presentation_annotation));
   }
 
-  RETURN_IF_NOT_OK(wb.WriteUleb128(num_sub_mixes_));
+  const DecodedUleb128 num_sub_mixes = sub_mixes_.size();
+  RETURN_IF_NOT_OK(wb.WriteUleb128(num_sub_mixes));
 
   // Loop to write the `sub_mixes` array.
-  RETURN_IF_NOT_OK(ValidateNumSubMixes(num_sub_mixes_));
+  RETURN_IF_NOT_OK(ValidateNumSubMixes(num_sub_mixes));
   RETURN_IF_NOT_OK(ValidateUniqueAudioElementIds(sub_mixes_));
-  RETURN_IF_NOT_OK(
-      ValidateContainerSizeEqual("sub_mixes", sub_mixes_, num_sub_mixes_));
   for (const auto& sub_mix : sub_mixes_) {
     RETURN_IF_NOT_OK(ValidateAndWriteSubMix(count_label_, sub_mix, wb));
   }
@@ -520,10 +521,11 @@
         localized_presentation_annotation);
   }
 
-  RETURN_IF_NOT_OK(rb.ReadULeb128(num_sub_mixes_));
+  DecodedUleb128 num_sub_mixes;
+  RETURN_IF_NOT_OK(rb.ReadULeb128(num_sub_mixes));
 
   // Loop to read the `sub_mixes` array.
-  for (int i = 0; i < num_sub_mixes_; ++i) {
+  for (int i = 0; i < num_sub_mixes; ++i) {
     MixPresentationSubMix sub_mix;
     RETURN_IF_NOT_OK(sub_mix.ReadAndValidate(count_label_, rb));
     sub_mixes_.push_back(sub_mix);
@@ -531,7 +533,7 @@
   // TODO(b/329705373): Examine how many bytes were read so far. Use this to
   //                    determine if Mix Presentation Tags should be read.
 
-  RETURN_IF_NOT_OK(ValidateNumSubMixes(num_sub_mixes_));
+  RETURN_IF_NOT_OK(ValidateNumSubMixes(num_sub_mixes));
   RETURN_IF_NOT_OK(ValidateUniqueAudioElementIds(sub_mixes_));
 
   return absl::OkStatus();
@@ -560,15 +562,15 @@
     LOG(INFO) << "    localized_presentation_annotations[" << i << "]= \""
               << localized_presentation_annotations_[i] << "\"";
   }
-  LOG(INFO) << "  num_sub_mixes= " << num_sub_mixes_;
+  LOG(INFO) << "  num_sub_mixes= " << sub_mixes_.size();
 
   // Submixes.
-  for (int i = 0; i < num_sub_mixes_; ++i) {
+  for (int i = 0; i < sub_mixes_.size(); ++i) {
     const auto& sub_mix = sub_mixes_[i];
     LOG(INFO) << "  // sub_mixes[" << i << "]:";
-    LOG(INFO) << "    num_audio_elements= " << sub_mix.num_audio_elements;
+    LOG(INFO) << "    num_audio_elements= " << sub_mix.audio_elements.size();
     // Audio elements.
-    for (int j = 0; j < sub_mix.num_audio_elements; ++j) {
+    for (int j = 0; j < sub_mix.audio_elements.size(); ++j) {
       const auto& audio_element = sub_mix.audio_elements[j];
       LOG(INFO) << "    // audio_elements[" << j << "]:";
       LOG(INFO) << "      audio_element_id= " << audio_element.audio_element_id;
@@ -594,10 +596,10 @@
     LOG(INFO) << "    output_mix_gain:";
     sub_mix.output_mix_gain.Print();
 
-    LOG(INFO) << "    num_layouts= " << sub_mix.num_layouts;
+    LOG(INFO) << "    num_layouts= " << sub_mix.layouts.size();
 
     // Layouts.
-    for (int j = 0; j < sub_mix.num_layouts; j++) {
+    for (int j = 0; j < sub_mix.layouts.size(); j++) {
       const auto& layout = sub_mix.layouts[j];
       LOG(INFO) << "    // layouts[" << j << "]:";
       LOG(INFO) << "      loudness_layout:";
@@ -634,7 +636,7 @@
         const auto& anchored_loudness = loudness.anchored_loudness;
         LOG(INFO) << "        anchored_loudness: ";
         LOG(INFO) << "          num_anchored_loudness= "
-                  << absl::StrCat(anchored_loudness.num_anchored_loudness);
+                  << absl::StrCat(anchored_loudness.anchor_elements.size());
         for (int i = 0; i < anchored_loudness.anchor_elements.size(); i++) {
           LOG(INFO) << "          anchor_element[" << i << "]= "
                     << absl::StrCat(
@@ -656,6 +658,17 @@
       }
     }
   }
+  if (mix_presentation_tags_.has_value()) {
+    LOG(INFO) << "  mix_presentation_tags:";
+    for (int i = 0; i < mix_presentation_tags_->tags.size(); ++i) {
+      const auto& tag = mix_presentation_tags_->tags[i];
+      LOG(INFO) << "    tags[" << i << "]:";
+      LOG(INFO) << "      tag_name= \"" << tag.tag_name << "\"";
+      LOG(INFO) << "      tag_value= \"" << tag.tag_value << "\"";
+    }
+  } else {
+    LOG(INFO) << "  No mix_presentation_tags detected.";
+  }
 }
 
 }  // namespace iamf_tools
diff --git a/iamf/obu/mix_presentation.h b/iamf/obu/mix_presentation.h
index 08a5c22..19b81f8 100644
--- a/iamf/obu/mix_presentation.h
+++ b/iamf/obu/mix_presentation.h
@@ -92,8 +92,8 @@
   friend bool operator==(const AnchoredLoudness& lhs,
                          const AnchoredLoudness& rhs) = default;
 
-  uint8_t num_anchored_loudness = 0;
-  // Length `num_anchored_loudness`.
+  // `num_anchored_loudness` is implicit based on the size of
+  // `anchor_elements`.
   std::vector<AnchoredLoudnessElement> anchor_elements = {};
 };
 
@@ -293,16 +293,14 @@
    */
   absl::Status ReadAndValidate(const int32_t& count_label, ReadBitBuffer& rb);
 
-  DecodedUleb128 num_audio_elements;
-  // Length `num_audio_elements`.
+  // `num_audio_elements` is implicit based on the size of `audio_elements`.
   std::vector<SubMixAudioElement> audio_elements;
 
   // The gain value to be applied in post-processing the mixed audio signal to
   // generate the audio signal for playback.
   MixGainParamDefinition output_mix_gain;
 
-  DecodedUleb128 num_layouts;
-  // Length `num_layouts`.
+  // `num_layouts` is implicit based on the size of `layouts`.
   std::vector<MixPresentationLayout> layouts;
 };
 
@@ -325,7 +323,7 @@
    */
   absl::Status ValidateAndWrite(WriteBitBuffer& wb) const;
 
-  uint8_t num_tags;
+  // `num_tags` is implicit based on the size of `tags`.
   std::vector<Tag> tags;
 };
 
@@ -366,7 +364,6 @@
    *        `annotations_language`s in the OBU.
    * \param localized_presentation_annotations Vector representing all of the
    *        `localized_presentation_annotations`s in the OBU.
-   * \param num_sub_mixes `num_sub_mixes` in the OBU.
    * \param sub_mixes Vector representing all of the sub mixes in the OBU.
    */
   MixPresentationObu(
@@ -374,15 +371,14 @@
       DecodedUleb128 count_label,
       const std::vector<std::string>& annotations_language,
       const std::vector<std::string>& localized_presentation_annotations,
-      DecodedUleb128 num_sub_mixes,
       std::vector<MixPresentationSubMix>& sub_mixes)
       : ObuBase(header, kObuIaMixPresentation),
         sub_mixes_(std::move(sub_mixes)),
         mix_presentation_id_(mix_presentation_id),
         count_label_(count_label),
         annotations_language_(annotations_language),
-        localized_presentation_annotations_(localized_presentation_annotations),
-        num_sub_mixes_(num_sub_mixes) {}
+        localized_presentation_annotations_(
+            localized_presentation_annotations) {}
 
   /*!\brief Creates a `MixPresentationObu` from a `ReadBitBuffer`.
    *
@@ -426,7 +422,7 @@
     return localized_presentation_annotations_;
   }
 
-  DecodedUleb128 GetNumSubMixes() const { return num_sub_mixes_; }
+  DecodedUleb128 GetNumSubMixes() const { return sub_mixes_.size(); }
 
   std::vector<MixPresentationSubMix> sub_mixes_;
 
@@ -442,7 +438,7 @@
   // Length `count_label`.
   std::vector<std::string> localized_presentation_annotations_;
 
-  DecodedUleb128 num_sub_mixes_;
+  // `num_sub_mixes_` is implicit based on the size of `sub_mixes_`.
 
   // Used only by the factory create function.
   explicit MixPresentationObu(const ObuHeader& header)
@@ -451,8 +447,7 @@
         mix_presentation_id_(DecodedUleb128()),
         count_label_(DecodedUleb128()),
         annotations_language_({}),
-        localized_presentation_annotations_({}),
-        num_sub_mixes_(DecodedUleb128()) {}
+        localized_presentation_annotations_({}) {}
   /*!\brief Writes the OBU payload to the buffer.
    *
    * \param wb Buffer to write to.
diff --git a/iamf/obu/tests/mix_presentation_test.cc b/iamf/obu/tests/mix_presentation_test.cc
index def25da..b046b1c 100644
--- a/iamf/obu/tests/mix_presentation_test.cc
+++ b/iamf/obu/tests/mix_presentation_test.cc
@@ -79,10 +79,8 @@
         count_label_(1),
         annotations_language_({"en-us"}),
         localized_presentation_annotations_({"Mix 1"}),
-        num_sub_mixes_(1),
         sub_mixes_(
-            {{.num_audio_elements = 1,
-              .audio_elements = {{
+            {{.audio_elements = {{
                   .audio_element_id = 11,
                   .localized_element_annotations = {"Submix 1"},
                   .rendering_config =
@@ -92,7 +90,6 @@
                        .rendering_config_extension_size = 0,
                        .rendering_config_extension_bytes = {}},
               }},
-              .num_layouts = 1,
               .layouts = {{.loudness_layout =
                                {.layout_type =
                                     Layout::kLayoutTypeLoudspeakersSsConvention,
@@ -143,7 +140,6 @@
   // Length `count_label`.
   std::vector<std::string> localized_presentation_annotations_;
 
-  DecodedUleb128 num_sub_mixes_;
   // Length `num_sub_mixes`.
   std::vector<MixPresentationSubMix> sub_mixes_;
 
@@ -205,7 +201,7 @@
     // Construct and transfer ownership of the memory to the OBU.
     obu_ = std::make_unique<MixPresentationObu>(
         header_, mix_presentation_id_, count_label_, annotations_language_,
-        localized_presentation_annotations_, num_sub_mixes_, sub_mixes_);
+        localized_presentation_annotations_, sub_mixes_);
   }
 };
 
@@ -252,7 +248,6 @@
 }
 
 TEST_F(MixPresentationObuTest, ValidateAndWriteFailsWithInvalidNumSubMixes) {
-  num_sub_mixes_ = 0;
   sub_mixes_.clear();
   dynamic_sub_mix_args_.clear();
 
@@ -301,7 +296,6 @@
   ASSERT_EQ(sub_mixes_.size(), 1);
   ASSERT_EQ(sub_mixes_[0].audio_elements.size(), 1);
   // Add an extra audio element to sub-mix.
-  sub_mixes_[0].num_audio_elements = 2;
   sub_mixes_[0].audio_elements.push_back(sub_mixes_[0].audio_elements[0]);
   dynamic_sub_mix_args_[0].element_mix_gain_subblocks = {{}, {}};
 
@@ -320,7 +314,6 @@
   ASSERT_EQ(sub_mixes_.size(), 1);
   // Reconfigure the sub-mix to have no audio elements and no `element_mix`
   // gains which are typically 1:1 with the audio elements.
-  sub_mixes_[0].num_audio_elements = 0;
   sub_mixes_[0].audio_elements.clear();
   dynamic_sub_mix_args_[0].element_mix_gain_subblocks.clear();
 
@@ -332,7 +325,6 @@
 TEST_F(MixPresentationObuTest, TwoAnchorElements) {
   sub_mixes_[0].layouts[0].loudness.info_type = LoudnessInfo::kAnchoredLoudness;
   sub_mixes_[0].layouts[0].loudness.anchored_loudness = {
-      2,
       {{.anchor_element = AnchoredLoudnessElement::kAnchorElementAlbum,
         .anchored_loudness = 20},
        {.anchor_element = AnchoredLoudnessElement::kAnchorElementDialogue,
@@ -369,7 +361,6 @@
       LoudnessInfo::kAnchoredLoudness | LoudnessInfo::kTruePeak;
   sub_mixes_[0].layouts[0].loudness.true_peak = 22;
   sub_mixes_[0].layouts[0].loudness.anchored_loudness = {
-      1,
       {{.anchor_element = AnchoredLoudnessElement::kAnchorElementAlbum,
         .anchored_loudness = 20}}};
 
@@ -405,7 +396,6 @@
        ValidateAndWriteFailsWithInvalidNonUniqueAnchorElement) {
   sub_mixes_[0].layouts[0].loudness.info_type = LoudnessInfo::kAnchoredLoudness;
   sub_mixes_[0].layouts[0].loudness.anchored_loudness = {
-      2,
       {{.anchor_element = AnchoredLoudnessElement::kAnchorElementAlbum,
         .anchored_loudness = 20},
        {.anchor_element = AnchoredLoudnessElement::kAnchorElementAlbum,
@@ -468,7 +458,7 @@
       0x80 | 1, 0x00,
       // `language_label` and `mix_presentation_annotations`.
       'e', 'n', '-', 'u', 's', '\0', 'M', 'i', 'x', ' ', '1', '\0',
-      // `num_submixes` is affected by the `LebGenerator`.
+      // `num_sub_mixes` is affected by the `LebGenerator`.
       0x80 | 1, 0x00,
       // Start Submix 1
       // `num_audio_elements` is affected by the `LebGenerator`.
@@ -681,10 +671,8 @@
 }
 
 TEST_F(MixPresentationObuTest, MultipleSubmixesAndLayouts) {
-  num_sub_mixes_ = 2;
   sub_mixes_.push_back(
-      {.num_audio_elements = 1,
-       .audio_elements = {{
+      {.audio_elements = {{
            .audio_element_id = 21,
            .localized_element_annotations = {"Submix 2"},
            .rendering_config =
@@ -694,8 +682,6 @@
                 .rendering_config_extension_size = 0,
                 .rendering_config_extension_bytes = {}},
        }},
-
-       .num_layouts = 3,
        .layouts = {
            {.loudness_layout = {.layout_type = Layout::kLayoutTypeReserved0,
                                 .specific_layout =
@@ -818,7 +804,7 @@
   };
   InitExpectOk();
   obu_->mix_presentation_tags_ =
-      MixPresentationTags{.num_tags = 1, .tags = {{"tag", "value"}}};
+      MixPresentationTags{.tags = {{"tag", "value"}}};
 
   WriteBitBuffer wb(1024);
   EXPECT_THAT(obu_->ValidateAndWriteObu(wb), IsOk());
@@ -943,7 +929,6 @@
   ASSERT_FALSE(sub_mixes_.empty());
   sub_mixes_[0].layouts.push_back(
       MixPresentationLayout{Layout{.layout_type = kBeyondLayoutType}});
-  sub_mixes_[0].num_layouts = sub_mixes_[0].layouts.size();
 
   InitExpectOk();
   WriteBitBuffer unused_wb(0);
@@ -986,7 +971,7 @@
       'e', 'n', '-', 'u', 's', '\0',
       // localized_presentation_annotations[0]
       'M', 'i', 'x', ' ', '1', '\0',
-      // num_submixes
+      // num_sub_mixes
       0,
       // End Mix OBU.
   };
@@ -1014,7 +999,7 @@
       'e', 'n', '-', 'u', 's', '\0',
       // localized_presentation_annotations[0]
       'M', 'i', 'x', ' ', '1', '\0',
-      // num_submixes
+      // num_sub_mixes
       1,
       // Start Submix.
       1, 21,
@@ -1069,7 +1054,7 @@
       10,
       // count_label
       0,
-      // num_submixes
+      // num_sub_mixes
       1,
       // Start Submix.
       1, 21,
@@ -1119,7 +1104,7 @@
       10,
       // count_label
       0,
-      // num_submixes
+      // num_sub_mixes
       1,
       // Start Submix.
       1, 21,
@@ -1217,7 +1202,7 @@
                 {.sound_system =
                      LoudspeakersSsConventionLayout::kSoundSystemA_0_2_0}));
   EXPECT_EQ(layout.loudness.info_type, LoudnessInfo::kAnchoredLoudness);
-  EXPECT_EQ(layout.loudness.anchored_loudness.num_anchored_loudness, 2);
+  EXPECT_EQ(layout.loudness.anchored_loudness.anchor_elements.size(), 2);
   EXPECT_EQ(layout.loudness.anchored_loudness.anchor_elements[0].anchor_element,
             AnchoredLoudnessElement::kAnchorElementAlbum);
   EXPECT_EQ(
@@ -1372,8 +1357,7 @@
 
 TEST(MixPresentationTagsWriteAndValidate, WritesWithZeroTags) {
   constexpr uint8_t kZeroNumTags = 0;
-  const MixPresentationTags kMixPresentationTagsWithZeroTags = {
-      .num_tags = kZeroNumTags};
+  const MixPresentationTags kMixPresentationTagsWithZeroTags = {.tags = {}};
   const std::vector<uint8_t> kExpectedBuffer = {
       // `num_tags`.
       kZeroNumTags,
@@ -1388,7 +1372,7 @@
 TEST(MixPresentationTagsWriteAndValidate, WritesContentLanguageTag) {
   constexpr uint8_t kOneTag = 1;
   const MixPresentationTags kMixPresentationTagsWithContentLanguageTag = {
-      .num_tags = kOneTag, .tags = {{"content_language", "eng"}}};
+      .tags = {{"content_language", "eng"}}};
   const std::vector<uint8_t> kExpectedBuffer = {
       // `num_tags`.
       kOneTag,
@@ -1407,9 +1391,8 @@
 
 TEST(MixPresentationTagsWriteAndValidate,
      InvalidWhenContentLanguageTagNotThreeCharacters) {
-  constexpr uint8_t kOneTag = 1;
   const MixPresentationTags kMixPresentationTagsWithContentLanguageTag = {
-      .num_tags = kOneTag, .tags = {{"content_language", "en-us"}}};
+      .tags = {{"content_language", "en-us"}}};
 
   WriteBitBuffer wb(0);
 
@@ -1420,7 +1403,7 @@
 TEST(MixPresentationTagsWriteAndValidate, WritesArbitraryTags) {
   constexpr uint8_t kNumTags = 1;
   const MixPresentationTags kMixPresentationTagsWithArbitraryTag = {
-      .num_tags = kNumTags, .tags = {{"ABC", "123"}}};
+      .tags = {{"ABC", "123"}}};
   const std::vector<uint8_t> kExpectedBuffer = {// `num_tags`.
                                                 kNumTags,
                                                 // `tag_name[0]`.
@@ -1438,7 +1421,7 @@
 TEST(MixPresentationTagsWriteAndValidate, WritesDuplicateArbitraryTags) {
   constexpr uint8_t kTwoTags = 2;
   const MixPresentationTags kMixPresentationTagsWithArbitraryTag = {
-      .num_tags = kTwoTags, .tags = {{"tag", "value"}, {"tag", "value"}}};
+      .tags = {{"tag", "value"}, {"tag", "value"}}};
   const std::vector<uint8_t> kExpectedBuffer = {// `num_tags`.
                                                 kTwoTags,
                                                 // `tag_name[0]`.
@@ -1458,10 +1441,8 @@
 }
 
 TEST(MixPresentationTagsWriteAndValidate, InvalidForDuplicateContentIdTag) {
-  constexpr uint8_t kTwoTags = 2;
   const MixPresentationTags
       kMixPresentationTagsWithDuplicateContentLanguageTag = {
-          .num_tags = kTwoTags,
           .tags = {{"content_language", "eng"}, {"content_language", "kor"}}};
 
   WriteBitBuffer wb(1024);