|
| 1 | +/* |
| 2 | + * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +#include <data_provider/data_layout/AudioMetadata.h> |
| 18 | +#include <data_provider/players/AudioPlayer.h> |
| 19 | + |
| 20 | +#include <gtest/gtest.h> |
| 21 | + |
| 22 | +#include <vrs/DataSource.h> |
| 23 | +#include <vrs/RecordFileReader.h> |
| 24 | +#include <vrs/RecordFileWriter.h> |
| 25 | +#include <vrs/RecordFormat.h> |
| 26 | +#include <vrs/Recordable.h> |
| 27 | +#include <vrs/os/Utils.h> |
| 28 | + |
| 29 | +#include <cstdint> |
| 30 | +#include <limits> |
| 31 | +#include <vector> |
| 32 | + |
| 33 | +using namespace projectaria::tools::data_provider; |
| 34 | + |
| 35 | +namespace { |
| 36 | + |
| 37 | +constexpr uint8_t kNumChannels = 2; |
| 38 | +constexpr uint32_t kSampleRate = 16000; |
| 39 | + |
| 40 | +// Writes a single PCM/S16 stereo audio stream using the same datalayout the Aria |
| 41 | +// recorder produces. The audio content block is the trailing block, so VRS |
| 42 | +// derives the sample count from the remaining payload size (the recorder omits |
| 43 | +// an explicit sample-count field). |
| 44 | +class PcmS16AudioRecordable : public vrs::Recordable { |
| 45 | + public: |
| 46 | + explicit PcmS16AudioRecordable(std::vector<int16_t> interleavedSamples) |
| 47 | + : vrs::Recordable(vrs::RecordableTypeId::StereoAudioRecordableClass, "test/microphone"), |
| 48 | + samples_(std::move(interleavedSamples)) { |
| 49 | + addRecordFormat(vrs::Record::Type::CONFIGURATION, 1, config_.getContentBlock(), {&config_}); |
| 50 | + addRecordFormat( |
| 51 | + vrs::Record::Type::DATA, |
| 52 | + 1, |
| 53 | + data_.getContentBlock() + vrs::ContentBlock(vrs::AudioFormat::PCM, kNumChannels), |
| 54 | + {&data_}); |
| 55 | + } |
| 56 | + |
| 57 | + const vrs::Record* createConfigurationRecord() override { |
| 58 | + config_.streamId.set(0); |
| 59 | + config_.numChannels.set(kNumChannels); |
| 60 | + config_.sampleRate.set(kSampleRate); |
| 61 | + config_.sampleFormat.set(static_cast<uint8_t>(vrs::AudioSampleFormat::S16_LE)); |
| 62 | + return createRecord(0.0, vrs::Record::Type::CONFIGURATION, 1, vrs::DataSource(config_)); |
| 63 | + } |
| 64 | + |
| 65 | + const vrs::Record* createStateRecord() override { |
| 66 | + return createRecord(0.0, vrs::Record::Type::STATE, 1); |
| 67 | + } |
| 68 | + |
| 69 | + void writeDataRecord() { |
| 70 | + const auto numFrames = static_cast<uint32_t>(samples_.size() / kNumChannels); |
| 71 | + std::vector<int64_t> captureTimestampsNs(numFrames); |
| 72 | + for (uint32_t i = 0; i < numFrames; ++i) { |
| 73 | + captureTimestampsNs[i] = static_cast<int64_t>(i); |
| 74 | + } |
| 75 | + data_.captureTimestampsNs.stage(captureTimestampsNs); |
| 76 | + data_.audioMuted.set(0); |
| 77 | + createRecord( |
| 78 | + 1.0, |
| 79 | + vrs::Record::Type::DATA, |
| 80 | + 1, |
| 81 | + vrs::DataSource( |
| 82 | + data_, vrs::DataSourceChunk(samples_.data(), samples_.size() * sizeof(int16_t)))); |
| 83 | + } |
| 84 | + |
| 85 | + private: |
| 86 | + datalayout::AudioConfigRecordMetadata config_; |
| 87 | + datalayout::AudioDataRecordMetadata data_; |
| 88 | + std::vector<int16_t> samples_; |
| 89 | +}; |
| 90 | + |
| 91 | +// Removes the temp file on scope exit so an early ASSERT failure cannot leak it. |
| 92 | +struct TempFileGuard { |
| 93 | + std::string path; |
| 94 | + ~TempFileGuard() { |
| 95 | + if (!path.empty()) { |
| 96 | + vrs::os::remove(path); |
| 97 | + } |
| 98 | + } |
| 99 | +}; |
| 100 | + |
| 101 | +void writeTempPcmS16Vrs(const std::string& path, const std::vector<int16_t>& samples) { |
| 102 | + vrs::RecordFileWriter writer; |
| 103 | + PcmS16AudioRecordable recordable(samples); |
| 104 | + writer.addRecordable(&recordable); |
| 105 | + recordable.createConfigurationRecord(); |
| 106 | + recordable.createStateRecord(); |
| 107 | + recordable.writeDataRecord(); |
| 108 | + ASSERT_EQ(writer.writeToFile(path), 0); |
| 109 | +} |
| 110 | + |
| 111 | +} // namespace |
| 112 | + |
| 113 | +// AudioPlayer must read a raw PCM/S16 stream (it used to throw "Unsupported audio |
| 114 | +// sample format: int16le"), widening the int16 samples verbatim to int32 and |
| 115 | +// reporting INT16_MAX as the amplitude scale so amplitude-normalizing consumers |
| 116 | +// stay correct regardless of the on-disk sample width. |
| 117 | +TEST(AudioPlayerTest, ReadsPcmS16WidenedToInt32) { |
| 118 | + // Independently chosen interleaved [L, R] frames spanning the int16 range. |
| 119 | + const std::vector<int16_t> samples = {0, -1, 100, -100, 32767, -32768, 1234, -4321}; |
| 120 | + const std::vector<int32_t> expected(samples.begin(), samples.end()); |
| 121 | + |
| 122 | + const std::string path = |
| 123 | + vrs::os::getUniquePath(vrs::os::getTempFolder() + "projectaria_pcm_s16_audio_test"); |
| 124 | + TempFileGuard guard{path}; |
| 125 | + ASSERT_NO_FATAL_FAILURE(writeTempPcmS16Vrs(path, samples)); |
| 126 | + |
| 127 | + vrs::RecordFileReader reader; |
| 128 | + ASSERT_EQ(reader.openFile(path), 0); |
| 129 | + |
| 130 | + vrs::StreamId audioStreamId; |
| 131 | + for (const auto& id : reader.getStreams()) { |
| 132 | + if (id.getTypeId() == vrs::RecordableTypeId::StereoAudioRecordableClass) { |
| 133 | + audioStreamId = id; |
| 134 | + break; |
| 135 | + } |
| 136 | + } |
| 137 | + ASSERT_TRUE(audioStreamId.isValid()); |
| 138 | + |
| 139 | + AudioPlayer player(audioStreamId); |
| 140 | + reader.setStreamPlayer(audioStreamId, &player); |
| 141 | + ASSERT_EQ(reader.readAllRecords(), 0); |
| 142 | + reader.closeFile(); |
| 143 | + |
| 144 | + EXPECT_EQ(player.getConfigRecord().numChannels, kNumChannels); |
| 145 | + EXPECT_EQ(player.getConfigRecord().sampleRate, kSampleRate); |
| 146 | + EXPECT_EQ(player.getDetectedAudioFormat(), vrs::AudioFormat::PCM); |
| 147 | + |
| 148 | + const AudioData& audioData = player.getData(); |
| 149 | + EXPECT_DOUBLE_EQ(audioData.maxAmplitude, std::numeric_limits<int16_t>::max()); |
| 150 | + EXPECT_EQ(audioData.data, expected); |
| 151 | +} |
0 commit comments