| // Copyright 2023 The Cobalt Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include "starboard/shared/starboard/player/decoded_audio_internal.h" |
| |
| #include <cmath> |
| #include <utility> |
| |
| #include "starboard/common/log.h" |
| #include "starboard/shared/starboard/media/media_util.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace starboard { |
| namespace shared { |
| namespace starboard { |
| namespace player { |
| namespace { |
| |
| using ::starboard::shared::starboard::media::GetBytesPerSample; |
| |
| constexpr int kChannels = 2; |
| constexpr SbTime kTimestamp = kSbTimeSecond; |
| constexpr int kSizeInBytes = 4192; |
| constexpr int kSampleRate = 22050; |
| |
| constexpr double kPi = 3.1415926535897932384626; |
| |
| constexpr SbMediaAudioSampleType kSampleTypes[] = { |
| kSbMediaAudioSampleTypeInt16Deprecated, kSbMediaAudioSampleTypeFloat32}; |
| constexpr SbMediaAudioFrameStorageType kStorageTypes[] = { |
| kSbMediaAudioFrameStorageTypeInterleaved, |
| kSbMediaAudioFrameStorageTypePlanar}; |
| |
| // The following two functions fill `data` with audio samples of a sine wave |
| // starting from `initial_angle`. `stride` is the number of samples per group. |
| // For example: |
| // 1. When `stride` is one, all samples filled with be continuous. |
| // 2. When `stride` is 2, filling `data` and `data + 1` fills an interleaved |
| // stereo audio buffer with samples. |
| void Fill(int16_t* data, |
| double initial_angle, |
| double angle_step, |
| int count, |
| int stride) { |
| SB_DCHECK(data); |
| |
| auto current_angel = initial_angle; |
| for (int i = 0; i < count; ++i) { |
| *data = static_cast<int16_t>(sin(current_angel) * 32767); |
| current_angel += angle_step; |
| data += stride; |
| } |
| } |
| |
| void Fill(float* data, |
| double initial_angle, |
| double angle_step, |
| int count, |
| int stride) { |
| SB_DCHECK(data); |
| |
| auto current_angel = initial_angle; |
| for (int i = 0; i < count; ++i) { |
| *data = static_cast<float>(sin(current_angel)); |
| current_angel += angle_step; |
| data += stride; |
| } |
| } |
| |
| // The following two functions verify `data` with audio samples against a sine |
| // wave starting from `initial_angle`. `stride` is the number of samples per |
| // group. For example: |
| // 1. When `stride` is one, all samples will be treated as continuous. |
| // 2. When `stride` is 2, verifying against `data` and `data + 1` verifies an |
| // interleaved stereo audio buffer. |
| void Verify(const int16_t* data, |
| double initial_angle, |
| double angle_step, |
| int count, |
| int stride) { |
| SB_DCHECK(data); |
| |
| auto current_angel = initial_angle; |
| for (int i = 0; i < count; ++i) { |
| const auto current_value = static_cast<int16_t>(sin(current_angel) * 32767); |
| // Using `ASSERT_NEAR()` to allow for small value drifting due to |
| // conversions between sample types. |
| ASSERT_NEAR(static_cast<double>(*data), static_cast<double>(current_value), |
| 2.0 / 32767); |
| current_angel += angle_step; |
| data += stride; |
| } |
| } |
| |
| void Verify(const float* data, |
| double initial_angle, |
| double angle_step, |
| int count, |
| int stride) { |
| SB_DCHECK(data); |
| |
| auto current_angel = initial_angle; |
| for (int i = 0; i < count; ++i) { |
| const auto current_value = static_cast<float>(sin(current_angel)); |
| // Using `ASSERT_NEAR()` to allow for small value drifting due to |
| // conversions between sample types. |
| ASSERT_NEAR(static_cast<double>(*data), static_cast<double>(current_value), |
| 2.0 / 32767); |
| current_angel += angle_step; |
| data += stride; |
| } |
| } |
| |
| // Fill `decoded_audio` with sine wave samples, with phase shift of Pi/2 on each |
| // channel. |
| void Fill(scoped_refptr<DecodedAudio>* decoded_audio) { |
| SB_DCHECK(decoded_audio); |
| SB_DCHECK(*decoded_audio); |
| |
| bool is_int16 = |
| (*decoded_audio)->sample_type() == kSbMediaAudioSampleTypeInt16Deprecated; |
| bool is_interleaved = (*decoded_audio)->storage_type() == |
| kSbMediaAudioFrameStorageTypeInterleaved; |
| |
| for (int i = 0; i < (*decoded_audio)->channels(); ++i) { |
| if (is_int16 && is_interleaved) { |
| Fill((*decoded_audio)->data_as_int16() + i, kPi / 2 * i, kPi / 180, |
| (*decoded_audio)->frames(), (*decoded_audio)->channels()); |
| } else if (!is_int16 && is_interleaved) { |
| Fill((*decoded_audio)->data_as_float32() + i, kPi / 2 * i, kPi / 180, |
| (*decoded_audio)->frames(), (*decoded_audio)->channels()); |
| } else if (is_int16 && !is_interleaved) { |
| Fill((*decoded_audio)->data_as_int16() + (*decoded_audio)->frames() * i, |
| kPi / 2 * i, kPi / 180, (*decoded_audio)->frames(), 1); |
| } else if (!is_int16 && !is_interleaved) { |
| Fill((*decoded_audio)->data_as_float32() + (*decoded_audio)->frames() * i, |
| kPi / 2 * i, kPi / 180, (*decoded_audio)->frames(), 1); |
| } |
| } |
| } |
| |
| // verify `decoded_audio` against sine wave samples, with phase shift of Pi/2 on |
| // each channel. |
| void Verify(const scoped_refptr<DecodedAudio>& decoded_audio) { |
| SB_DCHECK(decoded_audio); |
| |
| bool is_int16 = |
| decoded_audio->sample_type() == kSbMediaAudioSampleTypeInt16Deprecated; |
| bool is_interleaved = |
| decoded_audio->storage_type() == kSbMediaAudioFrameStorageTypeInterleaved; |
| |
| for (int i = 0; i < decoded_audio->channels(); ++i) { |
| if (is_int16 && is_interleaved) { |
| ASSERT_NO_FATAL_FAILURE( |
| Verify(decoded_audio->data_as_int16() + i, kPi / 2 * i, kPi / 180, |
| decoded_audio->frames(), decoded_audio->channels())); |
| } else if (!is_int16 && is_interleaved) { |
| ASSERT_NO_FATAL_FAILURE( |
| Verify(decoded_audio->data_as_float32() + i, kPi / 2 * i, kPi / 180, |
| decoded_audio->frames(), decoded_audio->channels())); |
| } else if (is_int16 && !is_interleaved) { |
| ASSERT_NO_FATAL_FAILURE( |
| Verify(decoded_audio->data_as_int16() + decoded_audio->frames() * i, |
| kPi / 2 * i, kPi / 180, decoded_audio->frames(), 1)); |
| } else if (!is_int16 && !is_interleaved) { |
| ASSERT_NO_FATAL_FAILURE( |
| Verify(decoded_audio->data_as_float32() + decoded_audio->frames() * i, |
| kPi / 2 * i, kPi / 180, decoded_audio->frames(), 1)); |
| } |
| } |
| } |
| |
| TEST(DecodedAudioTest, DefaultCtor) { |
| scoped_refptr<DecodedAudio> decoded_audio(new DecodedAudio); |
| EXPECT_TRUE(decoded_audio->is_end_of_stream()); |
| } |
| |
| TEST(DecodedAudioTest, CtorWithSize) { |
| for (auto sample_type : kSampleTypes) { |
| for (auto storage_type : kStorageTypes) { |
| scoped_refptr<DecodedAudio> decoded_audio(new DecodedAudio( |
| kChannels, sample_type, storage_type, kTimestamp, kSizeInBytes)); |
| |
| EXPECT_FALSE(decoded_audio->is_end_of_stream()); |
| EXPECT_EQ(decoded_audio->channels(), kChannels); |
| EXPECT_EQ(decoded_audio->sample_type(), sample_type); |
| EXPECT_EQ(decoded_audio->storage_type(), storage_type); |
| EXPECT_EQ(decoded_audio->size_in_bytes(), kSizeInBytes); |
| EXPECT_EQ(decoded_audio->frames(), |
| kSizeInBytes / GetBytesPerSample(decoded_audio->sample_type()) / |
| kChannels); |
| |
| Fill(&decoded_audio); |
| Verify(decoded_audio); |
| } |
| } |
| } |
| |
| TEST(DecodedAudioTest, AdjustForSeekTime) { |
| for (int channels = 1; channels <= 6; ++channels) { |
| for (auto sample_type : kSampleTypes) { |
| scoped_refptr<DecodedAudio> original_decoded_audio(new DecodedAudio( |
| kChannels, sample_type, kSbMediaAudioFrameStorageTypeInterleaved, |
| kTimestamp, kSizeInBytes)); |
| Fill(&original_decoded_audio); |
| |
| scoped_refptr<DecodedAudio> adjusted_decoded_audio = |
| original_decoded_audio->Clone(); |
| |
| // Adjust to the beginning of `adjusted_decoded_audio` should be a no-op. |
| adjusted_decoded_audio->AdjustForSeekTime( |
| kSampleRate, adjusted_decoded_audio->timestamp()); |
| ASSERT_EQ(*original_decoded_audio, *adjusted_decoded_audio); |
| |
| // Adjust to an invalid timestamp before the time range of |
| // `adjusted_decoded_audio`, it's a no-op. |
| adjusted_decoded_audio->AdjustForSeekTime( |
| kSampleRate, adjusted_decoded_audio->timestamp() - kSbTimeSecond / 2); |
| ASSERT_EQ(*original_decoded_audio, *adjusted_decoded_audio); |
| |
| // Adjust to an invalid timestamp after the time range of |
| // `adjusted_decoded_audio`, it's also a no-op. |
| adjusted_decoded_audio->AdjustForSeekTime( |
| kSampleRate, |
| adjusted_decoded_audio->timestamp() + kSbTimeSecond * 100); |
| ASSERT_EQ(*original_decoded_audio, *adjusted_decoded_audio); |
| |
| const SbTime duration = media::AudioFramesToDuration( |
| adjusted_decoded_audio->frames(), kSampleRate); |
| const SbTime duration_of_one_frame = |
| media::AudioFramesToDuration(1, kSampleRate) + 1; |
| for (int i = 1; i < 10; ++i) { |
| adjusted_decoded_audio = original_decoded_audio->Clone(); |
| // Adjust to the middle of `adjusted_decoded_audio`. |
| SbTime seek_time = |
| adjusted_decoded_audio->timestamp() + duration * i / 10; |
| adjusted_decoded_audio->AdjustForSeekTime(kSampleRate, seek_time); |
| ASSERT_NEAR(adjusted_decoded_audio->frames(), |
| original_decoded_audio->frames() * (10 - i) / 10, 1); |
| |
| ASSERT_LE(adjusted_decoded_audio->timestamp(), seek_time); |
| ASSERT_NEAR(adjusted_decoded_audio->timestamp(), seek_time, |
| duration_of_one_frame); |
| |
| auto offset_in_bytes = original_decoded_audio->size_in_bytes() - |
| adjusted_decoded_audio->size_in_bytes(); |
| ASSERT_TRUE(memcmp(adjusted_decoded_audio->data(), |
| original_decoded_audio->data() + offset_in_bytes, |
| adjusted_decoded_audio->size_in_bytes()) == 0); |
| } |
| } |
| } |
| } |
| |
| TEST(DecodedAudioTest, AdjustForDiscardedDurations) { |
| for (int channels = 1; channels <= 6; ++channels) { |
| for (auto sample_type : kSampleTypes) { |
| scoped_refptr<DecodedAudio> original_decoded_audio(new DecodedAudio( |
| kChannels, sample_type, kSbMediaAudioFrameStorageTypeInterleaved, |
| kTimestamp, kSizeInBytes)); |
| Fill(&original_decoded_audio); |
| |
| scoped_refptr<DecodedAudio> adjusted_decoded_audio = |
| original_decoded_audio->Clone(); |
| |
| adjusted_decoded_audio->AdjustForDiscardedDurations(kSampleRate, 0, 0); |
| ASSERT_EQ(*original_decoded_audio, *adjusted_decoded_audio); |
| |
| auto duration_of_decoded_audio = media::AudioFramesToDuration( |
| original_decoded_audio->frames(), kSampleRate); |
| auto quarter_duration = duration_of_decoded_audio / 4; |
| auto duration_of_one_frame = |
| media::AudioFramesToDuration(1, kSampleRate) + 1; |
| adjusted_decoded_audio->AdjustForDiscardedDurations( |
| kSampleRate, quarter_duration, quarter_duration); |
| ASSERT_NEAR(adjusted_decoded_audio->frames(), |
| original_decoded_audio->frames() / 2, 2); |
| ASSERT_NEAR(adjusted_decoded_audio->timestamp(), |
| original_decoded_audio->timestamp() + quarter_duration, |
| duration_of_one_frame * 2); |
| |
| adjusted_decoded_audio = original_decoded_audio->Clone(); |
| // Adjust more frames than it has from front |
| adjusted_decoded_audio->AdjustForDiscardedDurations( |
| kSampleRate, duration_of_decoded_audio * 2, 0); |
| ASSERT_EQ(adjusted_decoded_audio->frames(), 0); |
| ASSERT_NEAR( |
| adjusted_decoded_audio->timestamp(), |
| original_decoded_audio->timestamp() + duration_of_decoded_audio, |
| duration_of_one_frame * 2); |
| |
| adjusted_decoded_audio = original_decoded_audio->Clone(); |
| // Adjust more frames than it has from back |
| adjusted_decoded_audio->AdjustForDiscardedDurations( |
| kSampleRate, 0, duration_of_decoded_audio * 2); |
| ASSERT_EQ(adjusted_decoded_audio->frames(), 0); |
| ASSERT_EQ(adjusted_decoded_audio->timestamp(), |
| original_decoded_audio->timestamp()); |
| } |
| } |
| } |
| |
| TEST(DecodedAudioTest, SwitchFormatTo) { |
| for (auto original_sample_type : kSampleTypes) { |
| for (auto original_storage_type : kStorageTypes) { |
| scoped_refptr<DecodedAudio> original_decoded_audio( |
| new DecodedAudio(kChannels, original_sample_type, |
| original_storage_type, kTimestamp, kSizeInBytes)); |
| |
| Fill(&original_decoded_audio); |
| |
| for (auto new_sample_type : kSampleTypes) { |
| for (auto new_storage_type : kStorageTypes) { |
| if (!original_decoded_audio->IsFormat(new_sample_type, |
| new_storage_type)) { |
| scoped_refptr<DecodedAudio> new_decoded_audio = |
| original_decoded_audio->SwitchFormatTo(new_sample_type, |
| new_storage_type); |
| |
| EXPECT_FALSE(new_decoded_audio->is_end_of_stream()); |
| EXPECT_EQ(new_decoded_audio->channels(), |
| original_decoded_audio->channels()); |
| EXPECT_EQ(new_decoded_audio->timestamp(), |
| original_decoded_audio->timestamp()); |
| EXPECT_EQ(new_decoded_audio->frames(), |
| original_decoded_audio->frames()); |
| EXPECT_TRUE( |
| new_decoded_audio->IsFormat(new_sample_type, new_storage_type)); |
| |
| ASSERT_NO_FATAL_FAILURE(Verify(new_decoded_audio)); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| TEST(DecodedAudioTest, Clone) { |
| for (auto sample_type : kSampleTypes) { |
| scoped_refptr<DecodedAudio> decoded_audio(new DecodedAudio( |
| kChannels, sample_type, kSbMediaAudioFrameStorageTypeInterleaved, |
| kTimestamp, kSizeInBytes)); |
| Fill(&decoded_audio); |
| auto copy = decoded_audio->Clone(); |
| ASSERT_EQ(*copy, *decoded_audio); |
| ASSERT_GT(decoded_audio->size_in_bytes(), 0); |
| decoded_audio->data()[0] = ~decoded_audio->data()[0]; |
| ASSERT_NE(*copy, *decoded_audio); |
| } |
| } |
| |
| } // namespace |
| } // namespace player |
| } // namespace starboard |
| } // namespace shared |
| } // namespace starboard |