blob: 2e4269a9aef10668c28709ac353e4631155554cc [file] [log] [blame]
/*
* Copyright (c) 2023, 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 "iamf/cli/wav_writer.h"
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "absl/base/nullability.h"
#include "absl/functional/any_invocable.h"
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/memory/memory.h"
#include "absl/status/status.h"
#include "absl/strings/str_cat.h"
#include "absl/types/span.h"
#include "iamf/cli/sample_processor_base.h"
#include "iamf/common/utils/macros.h"
#include "iamf/common/utils/sample_processing_utils.h"
#include "src/dsp/write_wav_file.h"
namespace iamf_tools {
namespace {
// Some audio to tactile functions return 0 on success and 1 on failure.
constexpr int kAudioToTactileResultFailure = 0;
constexpr int kAudioToTactileResultSuccess = 1;
// This class is implemented to consume all samples without producing output
// samples.
constexpr size_t kMaxOutputSamplesPerFrame = 0;
// Write samples for all channels.
absl::Status WriteSamplesInternal(absl::Nullable<FILE*> file,
size_t num_channels, int bit_depth,
size_t max_num_samples_per_frame,
const std::vector<uint8_t>& buffer,
size_t& total_samples_accumulator) {
if (file == nullptr) {
// Wav writer may have been aborted.
return absl::FailedPreconditionError(
"Wav writer is not accepting samples.");
}
const auto buffer_size = buffer.size();
if (buffer_size == 0) {
// Nothing to write.
return absl::OkStatus();
}
if (buffer_size % (bit_depth * num_channels / 8) != 0) {
return absl::InvalidArgumentError(
"Must write an integer number of samples.");
}
// Calculate how many samples there are.
const int bytes_per_sample = bit_depth / 8;
const size_t num_total_samples = (buffer_size) / bytes_per_sample;
const size_t num_samples_per_channel = num_total_samples / num_channels;
if (num_samples_per_channel > max_num_samples_per_frame) {
return absl::InvalidArgumentError(
absl::StrCat("Too many samples per frame. The `WavWriter` is "
"configured with a maximum number of "
"samples per frame of: ",
max_num_samples_per_frame,
". The number of samples per frame received is: ",
num_samples_per_channel));
}
int write_sample_result = kAudioToTactileResultFailure;
if (bit_depth == 16) {
// Arrange the input samples into an int16_t to match the expected input of
// `WriteWavSamples`.
std::vector<int16_t> samples(num_total_samples, 0);
for (int i = 0; i < num_total_samples * bytes_per_sample;
i += bytes_per_sample) {
samples[i / bytes_per_sample] = (buffer[i + 1] << 8) | buffer[i];
}
write_sample_result = WriteWavSamples(file, samples.data(), samples.size());
} else if (bit_depth == 24) {
// Arrange the input samples into an int32_t to match the expected input of
// `WriteWavSamples24Bit` with the lowest byte unused.
std::vector<int32_t> samples(num_total_samples, 0);
for (int i = 0; i < num_total_samples * bytes_per_sample;
i += bytes_per_sample) {
samples[i / bytes_per_sample] =
(buffer[i + 2] << 24) | buffer[i + 1] << 16 | buffer[i] << 8;
}
write_sample_result =
WriteWavSamples24Bit(file, samples.data(), samples.size());
} else if (bit_depth == 32) {
// Arrange the input samples into an int32_t to match the expected input of
// `WriteWavSamples32Bit`.
std::vector<int32_t> samples(num_total_samples, 0);
for (int i = 0; i < num_total_samples * bytes_per_sample;
i += bytes_per_sample) {
samples[i / bytes_per_sample] = buffer[i + 3] << 24 |
buffer[i + 2] << 16 | buffer[i + 1] << 8 |
buffer[i];
}
write_sample_result =
WriteWavSamples32Bit(file, samples.data(), samples.size());
} else {
// This should never happen because the factory method would never create
// an object with disallowed `bit_depth_` values.
LOG(FATAL) << "WavWriter only supports 16, 24, and 32-bit samples; got "
<< bit_depth;
}
if (write_sample_result == kAudioToTactileResultSuccess) {
total_samples_accumulator += num_total_samples;
return absl::OkStatus();
}
// It's not clear why this would happen.
return absl::UnknownError(
absl::StrCat("Error writing samples to wav file. write_sample_result= ",
write_sample_result));
}
void MaybeFinalizeFile(size_t sample_rate_hz, size_t num_channels,
auto& wav_header_writer, FILE*& file,
size_t& total_samples_written) {
if (file == nullptr) {
return;
}
// Finalize the temporary header based on the total number of samples written
// and close the file.
if (wav_header_writer) {
std::fseek(file, 0, SEEK_SET);
wav_header_writer(file, total_samples_written, sample_rate_hz,
num_channels);
}
std::fclose(file);
file = nullptr;
}
} // namespace
std::unique_ptr<WavWriter> WavWriter::Create(const std::string& wav_filename,
int num_channels,
int sample_rate_hz, int bit_depth,
size_t num_samples_per_frame,
bool write_header) {
// Open the file to write to.
LOG(INFO) << "Writer \"" << wav_filename << "\"";
auto* file = std::fopen(wav_filename.c_str(), "wb");
if (file == nullptr) {
LOG(ERROR).WithPerror() << "Error opening file \"" << wav_filename << "\"";
return nullptr;
}
// Write a dummy header. This will be overwritten in the destructor.
WavHeaderWriter wav_header_writer;
switch (bit_depth) {
case 16:
wav_header_writer = WriteWavHeader;
break;
case 24:
wav_header_writer = WriteWavHeader24Bit;
break;
case 32:
wav_header_writer = WriteWavHeader32Bit;
break;
default:
LOG(WARNING) << "This implementation does not support writing "
<< bit_depth << "-bit wav files.";
std::fclose(file);
std::remove(wav_filename.c_str());
return nullptr;
}
// Set to an empty writer. The emptiness can be checked to skip writing the
// header.
if (!write_header) {
wav_header_writer = WavHeaderWriter();
} else if (wav_header_writer(file, 0, sample_rate_hz, num_channels) ==
kAudioToTactileResultFailure) {
LOG(ERROR) << "Error writing header of file \"" << wav_filename << "\"";
return nullptr;
}
return absl::WrapUnique(
new WavWriter(wav_filename, num_channels, sample_rate_hz, bit_depth,
num_samples_per_frame, file, std::move(wav_header_writer)));
}
WavWriter::~WavWriter() {
// Finalize the header, in case the user did not call `Flush()`.
MaybeFinalizeFile(sample_rate_hz_, num_channels_, wav_header_writer_, file_,
total_samples_written_);
}
absl::Status WavWriter::PushFrameDerived(
absl::Span<const std::vector<int32_t>> time_channel_samples) {
// Flatten down the serialized PCM for compatibility with the internal
// `WriteSamplesInternal` function.
const size_t num_ticks = time_channel_samples.size();
const size_t num_channels =
time_channel_samples.empty() ? 0 : time_channel_samples[0].size();
if (!std::all_of(
time_channel_samples.begin(), time_channel_samples.end(),
[&](const auto& tick) { return tick.size() == num_channels; })) {
return absl::InvalidArgumentError(
"All ticks must have the same number of channels.");
}
std::vector<uint8_t> samples_as_pcm(num_channels * num_ticks * bit_depth_ / 8,
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_,
/*big_endian=*/false,
samples_as_pcm.data(), write_position));
}
}
return WriteSamplesInternal(file_, num_channels_, bit_depth_,
max_input_samples_per_frame_, samples_as_pcm,
total_samples_written_);
}
absl::Status WavWriter::FlushDerived() {
// No more samples are coming, finalize the header and close the file.
MaybeFinalizeFile(sample_rate_hz_, num_channels_, wav_header_writer_, file_,
total_samples_written_);
return absl::OkStatus();
}
absl::Status WavWriter::WritePcmSamples(const std::vector<uint8_t>& buffer) {
return WriteSamplesInternal(file_, num_channels_, bit_depth_,
max_input_samples_per_frame_, buffer,
total_samples_written_);
}
void WavWriter::Abort() {
std::fclose(file_);
std::remove(filename_to_remove_.c_str());
file_ = nullptr;
}
WavWriter::WavWriter(const std::string& filename_to_remove, int num_channels,
int sample_rate_hz, int bit_depth,
size_t num_samples_per_frame, FILE* file,
WavHeaderWriter wav_header_writer)
: SampleProcessorBase(num_samples_per_frame, num_channels,
kMaxOutputSamplesPerFrame),
sample_rate_hz_(sample_rate_hz),
bit_depth_(bit_depth),
total_samples_written_(0),
file_(file),
filename_to_remove_(filename_to_remove),
wav_header_writer_(std::move(wav_header_writer)) {}
} // namespace iamf_tools