text: // Copyright 2020 The Fuchsia Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  #include "src/media/audio/lib/test/hermetic_impulse_test.h"
  
  #include <fuchsia/media/cpp/fidl.h>
  #include <lib/syslog/cpp/macros.h>
  
  #include "src/lib/files/file.h"
  #include "src/lib/files/path.h"
  #include "src/lib/fxl/strings/string_printf.h"
  #include "src/media/audio/lib/analysis/analysis.h"
  #include "src/media/audio/lib/analysis/generators.h"
  #include "src/media/audio/lib/format/audio_buffer.h"
  #include "src/media/audio/lib/test/comparators.h"
  #include "src/media/audio/lib/test/renderer_shim.h"
  #include "src/media/audio/lib/wav/wav_writer.h"
  
  using ASF = fuchsia::media::AudioSampleFormat;
  
  namespace media::audio::test {
  
  template <ASF InputFormat, ASF OutputFormat>
  void HermeticImpulseTest::Run(const HermeticImpulseTest::TestCase<InputFormat, OutputFormat>& tc) {
    // Compute the number of input frames.
    auto start_of_last_impulse = tc.impulse_locations_in_frames.back();
    auto num_input_frames = start_of_last_impulse + tc.impulse_width_in_frames +
                            tc.pipeline.pos_filter_width + tc.pipeline.neg_filter_width;
  
    // Helper to translate from an input frame number to an output frame number.
    auto input_frame_to_output_frame = [&tc](int64_t input_frame) {
      auto input_fps = static_cast<double>(tc.input_format.frames_per_second());
      auto output_fps = static_cast<double>(tc.output_format.frames_per_second());
      return static_cast<int64_t>(std::ceil(output_fps / input_fps * input_frame));
    };
  
    auto num_output_frames = input_frame_to_output_frame(num_input_frames);
    auto device = CreateOutput(AUDIO_STREAM_UNIQUE_ID_BUILTIN_SPEAKERS, tc.output_format,
                               AddSlackToOutputFrames(num_output_frames), std::nullopt,
                               tc.pipeline.output_device_gain_db);
    auto renderer = CreateAudioRenderer(tc.input_format, num_input_frames);
  
    // Write all of the impulses to an input buffer so we can easily write the full
    // input to a WAV file for debugging. Include silence at the beginning to account
    // for ring in; this allows us to align the input and output WAV files.
    auto input_impulse_start = tc.pipeline.neg_filter_width;
    AudioBuffer<InputFormat> input(tc.input_format, num_input_frames);
    for (auto start_frame : tc.impulse_locations_in_frames) {
      start_frame += input_impulse_start;
      for (auto f = start_frame; f < start_frame + tc.impulse_width_in_frames; f++) {
        for (auto chan = 0; chan < tc.input_format.channels(); chan++) {
          input.samples()[input.SampleIndex(f, chan)] = tc.impulse_magnitude;
        }
      }
    }
  
    // Render the input at a time such that the first frame of audio will be rendered into
    // the first frame of the ring buffer.
    auto packets = renderer->AppendPackets({&input});
    renderer->PlaySynchronized(this, device, 0);
    renderer->WaitForPackets(this, packets);
  
    auto ring_buffer = device->SnapshotRingBuffer();
  
    // The ring buffer should contain the expected sequence of impulses.
    // Due to smoothing effects, the detected leading edge of each impulse might be offset
    // slightly from the expected location, however each impulse should be offset by the
    // same amount. Empirically, we see offsets as high as 0.5ms. Allow up to 1ms.
    int64_t max_impulse_offset_frames = tc.output_format.frames_per_ns().Scale(zx::msec(1).get());
    std::unordered_map<int32_t, int64_t> first_impulse_offset_per_channel;
    int64_t search_start_frame = 0;
    int64_t search_end_frame = 0;
  
    for (int64_t k = 0; k < static_cast<int64_t>(tc.impulse_locations_in_frames.size()); k++) {
      // End this search halfway between impulses k and k+1.
      int64_t input_next_midpoint_frame;
      if (k + 1 < static_cast<int64_t>(tc.impulse_locations_in_frames.size())) {
        auto curr = input_impulse_start + tc.impulse_locations_in_frames[k];
        auto next = input_impulse_start + tc.impulse_locations_in_frames[k + 1];
        input_next_midpoint_frame = curr + (next - curr) / 2;
      } else {
        input_next_midpoint_frame = num_input_frames;
      }
      search_start_frame = search_end_frame;
      search_end_frame = input_frame_to_output_frame(input_next_midpoint_frame);
  
      // We expect zero noise in the output.
      constexpr auto kNoiseFloor = 0;
  
      // Impulse should be at this frame +/- max_impulse_offset_frames.
      auto expected_output_frame =
          input_frame_to_output_frame(input_impulse_start + tc.impulse_locations_in_frames[k]);
  
      // Test each channel.
      for (int32_t chan = 0; chan < tc.output_format.channels(); chan++) {
        SCOPED_TRACE(testing::Message() << "Channel " << chan);
        auto output_chan = AudioBufferSlice<OutputFormat>(&ring_buffer).GetChannel(chan);
        auto slice = AudioBufferSlice(&output_chan, search_start_frame, search_end_frame);
        auto relative_output_frame = FindImpulseLeadingEdge(slice, kNoiseFloor);
        if (!relative_output_frame) {
          ADD_FAILURE() << "Could not find impulse " << k << " in ring buffer\n"
                        << "Expected at ring buffer frame " << expected_output_frame << "\n"
                        << "Ring buffer is:";
          output_chan.Display(search_start_frame, search_end_frame);
          continue;
        }
        int64_t output_frame = *relative_output_frame + search_start_frame;
        if (k == 0) {
          // First impulse decides the offset.
          int64_t offset = output_frame - expected_output_frame;
          EXPECT_LE(std::abs(offset), max_impulse_offset_frames)
              << "Found impulse " << k << " at an unexpected location: at frame " << output_frame
              << ", expected within " << max_impulse_offset_frames << " frames of "
              << expected_output_frame;
          first_impulse_offset_per_channel[chan] = offset;
        } else {
          // Other impulses should have the same offset.
          auto expected_offset = first_impulse_offset_per_channel[chan];
          EXPECT_EQ(expected_output_frame + expected_offset, output_frame)
              << "Found impulse " << k << " at an unexpected location; expected_offset is "
              << expected_offset;
        }
      }
    }
  
    if (save_input_and_output_files_) {
      WriteWavFile<InputFormat>(tc.test_name, "input", &input);
      WriteWavFile<OutputFormat>(tc.test_name, "ring_buffer", &ring_buffer);
    }
  }
  
  // Explicitly instantiate (almost) all possible implementations.
  // We intentionally don't instantiate implementations with OutputFormat = UNSIGNED_8
  // because such hardware is no longer in use, therefore it's not worth testing.
  #define INSTANTIATE(InputFormat, OutputFormat)                       \
    template void HermeticImpulseTest::Run<InputFormat, OutputFormat>( \
        const TestCase<InputFormat, OutputFormat>& tc);
  
  INSTANTIATE(ASF::UNSIGNED_8, ASF::SIGNED_16)
  INSTANTIATE(ASF::UNSIGNED_8, ASF::SIGNED_24_IN_32)
  INSTANTIATE(ASF::UNSIGNED_8, ASF::FLOAT)
  
  INSTANTIATE(ASF::SIGNED_16, ASF::SIGNED_16)
  INSTANTIATE(ASF::SIGNED_16, ASF::SIGNED_24_IN_32)
  INSTANTIATE(ASF::SIGNED_16, ASF::FLOAT)
  
  INSTANTIATE(ASF::SIGNED_24_IN_32, ASF::SIGNED_16)
  INSTANTIATE(ASF::SIGNED_24_IN_32, ASF::SIGNED_24_IN_32)
  INSTANTIATE(ASF::SIGNED_24_IN_32, ASF::FLOAT)
  
  INSTANTIATE(ASF::FLOAT, ASF::SIGNED_16)
  INSTANTIATE(ASF::FLOAT, ASF::SIGNED_24_IN_32)
  INSTANTIATE(ASF::FLOAT, ASF::FLOAT)
  
  }  // namespace media::audio::test
  