blob: fa07036fadcc6beea6bbb16634dd1f2fe69a1483 [file] [log] [blame]
// 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/wav/wav_reader.h"
#include <endian.h>
#include <fcntl.h>
#include <lib/fdio/io.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>
#include <zircon/compiler.h>
#include <algorithm>
#include <iomanip>
#include <limits>
#include <optional>
#include "src/media/audio/lib/wav/wav_internal.h"
namespace media::audio {
namespace {
// clang-format off
using wav::internal::RIFF_FOUR_CC;
using wav::internal::FMT_FOUR_CC;
using wav::internal::DATA_FOUR_CC;
using wav::internal::WAVE_FOUR_CC;
using wav::internal::RiffChunkHeader;
using wav::internal::WavHeader;
// clang-format on
} // namespace
// static
fpromise::result<std::unique_ptr<WavReader>, zx_status_t> WavReader::Open(
const std::string& file_name) {
fbl::unique_fd fd(open(file_name.c_str(), O_RDONLY));
if (fd.get() < 0) {
FX_LOGS(WARNING) << "open failed for " << std::quoted(file_name) << ", returned " << fd.get()
<< ", errno " << errno;
return fpromise::error(ZX_ERR_NOT_FOUND);
}
// 'RIFF'
RiffChunkHeader riff_header;
if (read(fd.get(), &riff_header, sizeof(riff_header)) != sizeof(riff_header)) {
FX_LOGS(WARNING) << "read initial header failed for " << std::quoted(file_name)
<< ", amount read was too small; errno " << errno;
return fpromise::error(ZX_ERR_IO);
}
riff_header.FixupEndianForReading();
if (riff_header.four_cc != RIFF_FOUR_CC) {
FX_LOGS(WARNING) << "read initial header failed for " << std::quoted(file_name)
<< ", unknown RIFF type '"
<< wav::internal::fourcc_to_string(riff_header.four_cc) << "' (0x" << std::hex
<< riff_header.four_cc << ") -- expected '"
<< wav::internal::fourcc_to_string(RIFF_FOUR_CC) << "' (0x" << RIFF_FOUR_CC
<< ")";
return fpromise::error(ZX_ERR_IO);
}
if (auto want = sizeof(WavHeader) + sizeof(RiffChunkHeader); riff_header.length < want) {
FX_LOGS(WARNING) << "RIFF header incorrect for " << std::quoted(file_name)
<< ", read length of " << riff_header.length << ", expected at least " << want;
return fpromise::error(ZX_ERR_IO);
}
uint32_t header_size = sizeof(riff_header);
FX_LOGS(DEBUG) << "Successfully read '" << wav::internal::fourcc_to_string(RIFF_FOUR_CC)
<< "' header (data length " << riff_header.length << ")";
// 'WAVE' form_type + 'fmt ' chunk
WavHeader wav_header;
if (read(fd.get(), &wav_header, sizeof(wav_header)) != sizeof(wav_header)) {
FX_LOGS(WARNING) << "read RIFF chunk failed for " << std::quoted(file_name)
<< ", amount read was too small; errno " << errno;
return fpromise::error(ZX_ERR_IO);
}
wav_header.FixupEndianForReading();
if (wav_header.wave_four_cc != WAVE_FOUR_CC) {
FX_LOGS(WARNING) << "read RIFF form_type failed for " << std::quoted(file_name)
<< ", unknown type '"
<< wav::internal::fourcc_to_string(wav_header.wave_four_cc) << "' (0x"
<< std::hex << wav_header.wave_four_cc << ") -- expected '"
<< wav::internal::fourcc_to_string(WAVE_FOUR_CC) << "' (0x" << WAVE_FOUR_CC
<< ")";
return fpromise::error(ZX_ERR_IO);
}
if (wav_header.fmt_four_cc != FMT_FOUR_CC) {
FX_LOGS(WARNING) << "read WAV header failed for " << std::quoted(file_name)
<< ", unknown chunk '"
<< wav::internal::fourcc_to_string(wav_header.fmt_four_cc) << "' (0x"
<< std::hex << wav_header.fmt_four_cc << ") -- expected '"
<< wav::internal::fourcc_to_string(FMT_FOUR_CC) << "' (0x" << FMT_FOUR_CC
<< ")";
return fpromise::error(ZX_ERR_IO);
}
if (wav_header.bits_per_sample != 8 && wav_header.bits_per_sample != 16 &&
wav_header.bits_per_sample != 24 && wav_header.bits_per_sample != 32) {
FX_LOGS(WARNING) << "read WAV header failed for " << std::quoted(file_name)
<< ", unsupported bits_per_sample: " << wav_header.bits_per_sample;
return fpromise::error(ZX_ERR_IO);
}
// In the WAV file definition, the format chunk is not constant-size; it specifies its own length.
// Valid WAV files might have a fmt_chunk_len of 14, 16, 18, 40, etc. (representing valid
// WAVEFORMAT, PCMWAVEFORMAT, WAVEFORMATEX, WAVEFORMATEXTENSIBLE file types). We can support them
// all, by reading the essential format info, then skipping the rest of the 'fmt ' chunk.
auto wav_header_size = offsetof(WavHeader, fmt_chunk_len) + sizeof(wav_header.fmt_chunk_len) +
wav_header.fmt_chunk_len;
header_size += wav_header_size;
if (wav_header_size != sizeof(WavHeader)) {
FX_LOGS(INFO) << "'fmt ' chunk is not PCMWAVEFORMAT, adjusting read position by "
<< static_cast<int>(wav_header_size) - static_cast<int>(sizeof(WavHeader));
// File read position is at end of 'fmt ' chunkj (we assumed PCMWAVEFORMAT). If fmt_chunk_len
// is different than that size (could be more or theoretically less), then adjust accordingly.
// This keeps the file read position in sync with the header_size value.
off_t pos = lseek(fd.get(), header_size, SEEK_SET);
if (pos < 0) {
FX_LOGS(WARNING) << "read RIFF chunk failed for " << std::quoted(file_name)
<< ", could not seek past the wave header; errno " << errno;
return fpromise::error(ZX_ERR_IO);
}
FX_CHECK(pos == header_size);
}
FX_LOGS(DEBUG) << "Successfully read '" << wav::internal::fourcc_to_string(FMT_FOUR_CC)
<< "' header (data length " << wav_header.fmt_chunk_len << ")";
// We find the actual audio samples in a 'data' chunk, usually immediately after the 'fmt ' chunk.
// Although 'fmt ' and 'data' are the only required chunks in a RIFF-WAV file, optional chunks are
// fairly common (for metadata like Artist Name, Song Title, etc). By definition, file readers can
// safely skip any optional chunks, so after the 'fmt ' chunk ends, we skip to the 'data' chunk.
RiffChunkHeader data_header;
if (read(fd.get(), &data_header, sizeof(data_header)) != sizeof(data_header)) {
FX_LOGS(WARNING) << "read data header failed for " << std::quoted(file_name) << ", errno "
<< errno;
return fpromise::error(ZX_ERR_IO);
}
data_header.FixupEndianForReading();
// Keep looping until we find the 'data' chunk
while (data_header.four_cc != DATA_FOUR_CC) {
// Skip over this unknown chunk (consisting of a RiffChunkHeader, plus 'length' bytes of data)
RiffChunkHeader other_header = data_header;
header_size += sizeof(other_header);
FX_LOGS(INFO) << "Skipping '" << wav::internal::fourcc_to_string(other_header.four_cc)
<< "' chunk (data length " << other_header.length << ")";
header_size += other_header.length;
lseek(fd.get(), header_size, SEEK_SET);
// Try again after that chunk: read the next header and fix it up for reading
if (read(fd.get(), &data_header, sizeof(data_header)) != sizeof(data_header)) {
// We reached the end of the file before we found a 'DATA' chunk.
FX_LOGS(WARNING) << "header read (at byte position " << header_size << ") failed for "
<< std::quoted(file_name) << ", errno " << errno;
return fpromise::error(ZX_ERR_IO);
}
data_header.FixupEndianForReading();
}
FX_LOGS(DEBUG) << "Successfully read '" << wav::internal::fourcc_to_string(DATA_FOUR_CC)
<< "' header; " << data_header.length << " data bytes follow...";
header_size += sizeof(data_header);
FX_LOGS(DEBUG) << "Total header_size for this file: " << header_size << " bytes";
std::unique_ptr<WavReader> out(new WavReader);
out->sample_format_ = wav_header.sample_format();
out->channel_count_ = wav_header.channel_count;
out->frame_rate_ = wav_header.frame_rate;
out->bits_per_sample_ = wav_header.bits_per_sample;
out->length_ = data_header.length;
out->header_size_ = header_size;
out->file_ = std::move(fd);
if (wav_header.bits_per_sample == 24) {
out->bits_per_sample_ = 32;
out->length_ = data_header.length * 4 / 3;
out->packed_24_ = true;
out->packed_24_buffer_ = std::make_unique<uint8_t[]>(kPacked24BufferSize);
}
return fpromise::ok(std::move(out));
}
fpromise::result<size_t, int> WavReader::Read(void* buffer, size_t requested_bytes) {
// In the majority, non-packed-24 case, just read the bytes directly to the client buffer.
if (!packed_24_) {
int64_t file_bytes = read(file_.get(), buffer, requested_bytes);
if (file_bytes < 0) {
return fpromise::error(errno);
}
return fpromise::ok(file_bytes);
}
// If packed-24, read the file just once, to avoid potential performance problems, then
// decompress each sample (from 3 to 4 bytes) as we write sequentially into the client buffer.
int64_t file_bytes_needed = std::min(
kPacked24BufferSize, (static_cast<int64_t>(requested_bytes) + last_modulo_4_) * 3 / 4);
int64_t file_bytes = read(file_.get(), packed_24_buffer_.get(), file_bytes_needed);
if (file_bytes < 0) {
return fpromise::error(static_cast<int>(errno));
}
auto client_buffer = reinterpret_cast<uint8_t*>(buffer);
int64_t client_offset = 0, packed_offset = 0;
while (packed_offset < file_bytes) {
if ((last_modulo_4_ + client_offset) % 4 == 0) {
client_buffer[client_offset++] = 0;
} else {
client_buffer[client_offset++] = packed_24_buffer_[packed_offset++];
}
}
FX_CHECK(client_offset <= static_cast<int64_t>(requested_bytes));
last_modulo_4_ = (last_modulo_4_ + client_offset) % 4;
return fpromise::ok(static_cast<size_t>(client_offset));
}
int WavReader::Reset() {
off_t n = lseek(file_.get(), header_size_, SEEK_SET);
if (n < 0) {
return static_cast<int>(errno);
}
FX_CHECK(n == header_size_);
return 0;
}
} // namespace media::audio