| // Copyright 2017 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 "garnet/lib/media/wav_writer/wav_writer.h" |
| #include <endian.h> |
| #include <fcntl.h> |
| #include <iomanip> |
| #include <lib/fdio/io.h> |
| #include <limits> |
| #include <optional> |
| #include <unistd.h> |
| #include <zircon/compiler.h> |
| |
| namespace media { |
| namespace audio { |
| |
| namespace { |
| |
| // Consts for WAV file location, name (instance_count_ is appended), extension |
| constexpr const char* kDefaultWavFilePathName = "/tmp/wav_writer_"; |
| constexpr const char* kWavFileExtension = ".wav"; |
| |
| // |
| // Struct and const definitions related to RIFF file format |
| // |
| |
| // Encode a 32-bit 'fourcc' value from these 4 byte values |
| static inline constexpr uint32_t make_fourcc(uint8_t a, uint8_t b, uint8_t c, |
| uint8_t d) { |
| return (static_cast<uint32_t>(d) << 24) | (static_cast<uint32_t>(c) << 16) | |
| (static_cast<uint32_t>(b) << 8) | static_cast<uint32_t>(a); |
| } |
| |
| // clang-format off |
| constexpr uint32_t RIFF_FOUR_CC = make_fourcc('R', 'I', 'F', 'F'); |
| constexpr uint32_t WAVE_FOUR_CC = make_fourcc('W', 'A', 'V', 'E'); |
| constexpr uint32_t FMT_FOUR_CC = make_fourcc('f', 'm', 't', ' '); |
| constexpr uint32_t DATA_FOUR_CC = make_fourcc('d', 'a', 't', 'a'); |
| constexpr uint16_t FORMAT_LPCM = 0x0001; |
| constexpr uint16_t FORMAT_FLOAT = 0x0003; |
| // clang-format on |
| |
| // The RIFF file specification (and the child specification for WAV content) |
| // defines the layout and contents of WAV audio files. |
| // |
| // RIFF files consist of so-called _chunks_ (self-describing sections of the |
| // file). These files begin with a RIFF header chunk that describes the primary |
| // format of the file contents, followed by the data itself (in a chunk of its |
| // own). Additional chunks may also be present, containing metadata and/or other |
| // information to support optional features. Because all chunks include a length |
| // field, any unknown chunks can be safely skipped by file readers. |
| // |
| // The WAV file format specifies an initial 'RIFF' chunk of type 'WAVE' (length |
| // 24), followed by two required Subchunks: 'fmt ' (length 24) and 'data' |
| // (length 8 + the size of the subsequent audio data). Audio data should |
| // immediately follow these first 8 bytes of the 'data' subchunk. Once the |
| // entirety of audio data has been written into the file, the 'length' field for |
| // the 'data' subchunk should be updated with the number of bytes of audio. |
| // Likewise, the overall length for the parent 'RIFF' chunk (which conceptually |
| // contains the two 'fmt ' and 'data' subchunks) must be updated at this point, |
| // to describe its total size (including subchunk headers and the audio data). |
| // Thus, although all audio data follows the file headers, we must update the |
| // headers once all audio has been written. |
| // |
| // ** Note, lest our RiffChunkHeader struct definition mislead the uninformed ** |
| // These struct definitions actually conceptually relocate the final 32-bit |
| // value of the initial RIFF chunk into the subsequent 'fmt ' subchunk instead. |
| // Because the sequence of fields is maintained, this does not create a problem. |
| // We do this so that we can reuse our RIFF struct definition for the 'data' |
| // subchunk as well. |
| struct __PACKED RiffChunkHeader { |
| uint32_t four_cc; |
| uint32_t length = 0; |
| |
| // RIFF files are stored in little-endian, regardless of host-architecture. |
| void FixupEndian() { |
| four_cc = htole32(four_cc); |
| length = htole32(length); |
| } |
| }; |
| |
| // As mentioned above, the WAVE_FOUR_CC is actually a menber of the previous |
| // RIFF chunk, but we include it here so that we can manage our parent 'RIFF' |
| // chunk and our 'data' subchunk with common code. |
| struct __PACKED WavHeader { |
| uint32_t wave_four_cc = WAVE_FOUR_CC; |
| uint32_t fmt_four_cc = FMT_FOUR_CC; |
| uint32_t fmt_chunk_len = sizeof(WavHeader) - offsetof(WavHeader, format); |
| uint16_t format = 0; |
| uint16_t channel_count = 0; |
| uint32_t frame_rate = 0; |
| uint32_t average_byte_rate = 0; |
| uint16_t frame_size = 0; |
| uint16_t bits_per_sample = 0; |
| |
| // RIFF files are stored in little-endian, regardless of host-architecture. |
| void FixupEndian() { |
| // clang-format off |
| wave_four_cc = htole32(wave_four_cc); |
| fmt_four_cc = htole32(fmt_four_cc); |
| fmt_chunk_len = htole32(fmt_chunk_len); |
| format = htole16(format); |
| channel_count = htole16(channel_count); |
| frame_rate = htole32(frame_rate); |
| average_byte_rate = htole32(average_byte_rate); |
| frame_size = htole16(frame_size); |
| bits_per_sample = htole16(bits_per_sample); |
| // clang-format on |
| } |
| }; |
| |
| // |
| // Constants |
| // |
| // This is the number of bytes from beginning of file, to first audio data byte. |
| constexpr const uint32_t kWavHeaderOverhead = |
| sizeof(RiffChunkHeader) + sizeof(WavHeader) + sizeof(RiffChunkHeader); |
| |
| // |
| // Locally-scoped utility functions |
| // |
| // This function is used by Initialize to create WAV file headers. Given an |
| // already-created file, it specifically creates a 'RIFF' chunk of type 'WAVE' |
| // (length 24) plus its two required Subchunks 'fmt ' (of length 24) and 'data' |
| // (of length 8 + eventual audio data). Following this call, the file write |
| // cursor is positioned immediately after the headers, at the correct location |
| // to write any audio samples we are given. |
| // This private function assumes the given file_desc is valid. |
| zx_status_t WriteNewHeader(int file_desc, |
| fuchsia::media::AudioSampleFormat sample_format, |
| uint32_t channel_count, uint32_t frame_rate, |
| uint16_t bits_per_sample) { |
| if (channel_count > std::numeric_limits<uint16_t>::max()) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| ::lseek(file_desc, 0, SEEK_SET); |
| RiffChunkHeader riff_header; |
| riff_header.four_cc = RIFF_FOUR_CC; |
| riff_header.length = kWavHeaderOverhead; |
| riff_header.FixupEndian(); |
| if (::write(file_desc, &riff_header, sizeof(riff_header)) < 0) { |
| return ZX_ERR_IO; |
| } |
| |
| if (sample_format == fuchsia::media::AudioSampleFormat::FLOAT) { |
| FXL_DCHECK(bits_per_sample == 32); |
| } |
| |
| WavHeader wave_header; |
| // wave_four_cc already set |
| // fmt_four_cc already set |
| // fmt_chunk_len already set |
| wave_header.format = |
| (sample_format == fuchsia::media::AudioSampleFormat::FLOAT) ? FORMAT_FLOAT |
| : FORMAT_LPCM; |
| wave_header.channel_count = channel_count; |
| wave_header.frame_rate = frame_rate; |
| wave_header.average_byte_rate = |
| (bits_per_sample >> 3) * channel_count * frame_rate; |
| wave_header.frame_size = (bits_per_sample >> 3) * channel_count; |
| wave_header.bits_per_sample = bits_per_sample; |
| |
| wave_header.FixupEndian(); |
| if (::write(file_desc, &wave_header, sizeof(wave_header)) < 0) { |
| return ZX_ERR_IO; |
| } |
| |
| riff_header.four_cc = DATA_FOUR_CC; |
| riff_header.FixupEndian(); |
| if (::write(file_desc, &riff_header, sizeof(riff_header)) < 0) { |
| return ZX_ERR_IO; |
| } |
| |
| ::lseek(file_desc, kWavHeaderOverhead, SEEK_SET); |
| return ZX_OK; |
| } |
| |
| // This function is used to update the 'length' fields in the WAV file header, |
| // after audio data has been written into the file. Specifically, it updates the |
| // total length of the 'RIFF' chunk (which includes the headers and all audio |
| // data), as well as the length of the 'data' subchunk (which includes only the |
| // audio data). Following this call, the file's write cursor is moved to the end |
| // of any previously-written audio data, so that subsequent audio writes will be |
| // correctly appended to the file. |
| // This private function assumes the given file_desc is valid. |
| zx_status_t UpdateHeaderLengths(int file_desc, size_t payload_len) { |
| if (payload_len > |
| (std::numeric_limits<uint32_t>::max() - kWavHeaderOverhead)) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| uint32_t file_offset = offsetof(RiffChunkHeader, length); |
| ::lseek(file_desc, file_offset, SEEK_SET); |
| uint32_t new_length = |
| htole32(static_cast<uint32_t>(kWavHeaderOverhead + payload_len)); |
| if (::write(file_desc, &new_length, sizeof(new_length)) < 0) { |
| return ZX_ERR_IO; |
| } |
| |
| file_offset += sizeof(RiffChunkHeader) + sizeof(WavHeader); |
| ::lseek(file_desc, file_offset, SEEK_SET); |
| new_length = htole32(static_cast<uint32_t>(payload_len)); |
| if (::write(file_desc, &new_length, sizeof(new_length)) < 0) { |
| return ZX_ERR_IO; |
| } |
| |
| ::lseek(file_desc, kWavHeaderOverhead + payload_len, SEEK_SET); |
| return ZX_OK; |
| } |
| |
| // This function appends audio data to the WAV file. It assumes that the file's |
| // write cursor is correctly placed after any previously written audio data. |
| // This private function assumes the given file_desc is valid. |
| ssize_t WriteData(int file_desc, const void* const buffer, size_t num_bytes) { |
| return ::write(file_desc, buffer, num_bytes); |
| } |
| |
| } // namespace |
| |
| // Private static ('enabled' specialization) member, for default WAV file name |
| template <> |
| std::atomic<uint32_t> WavWriter<true>::instance_count_(0u); |
| |
| // |
| // Public instance methods (general template implementation) |
| // (Note: the .h contains a 'not enabled' specialization, consisting of no-op |
| // implementations that are compiler-optimized away. I.e. disabled == zero_cost) |
| // |
| |
| // Create the audio file; save the RIFF chunk and 'fmt ' / 'data' sub-chunks. |
| // If this object already had a file open, the header is not updated. |
| // TODO(mpuryear): leverage utility code elsewhere for bytes-per-sample lookup, |
| // for either FIDL-defined sample types and/or driver defined sample packings. |
| template <bool enabled> |
| bool WavWriter<enabled>::Initialize( |
| const char* const file_name, |
| fuchsia::media::AudioSampleFormat sample_format, uint32_t channel_count, |
| uint32_t frame_rate, uint32_t bits_per_sample) { |
| // Open our output file. |
| uint32_t instance_count = instance_count_.fetch_add(1); |
| if (file_name == nullptr || strlen(file_name) == 0) { |
| file_name_ = kDefaultWavFilePathName; |
| file_name_ += (std::to_string(instance_count) + kWavFileExtension); |
| } else { |
| file_name_ = file_name; |
| } |
| |
| int file_desc = ::open(file_name_.c_str(), O_CREAT | O_WRONLY | O_TRUNC); |
| file_.reset(file_desc); |
| if (!file_.is_valid()) { |
| FXL_LOG(WARNING) << "::open failed for " << std::quoted(file_name_) |
| << ", returned " << file_desc << ", errno " << errno; |
| return false; |
| } |
| |
| // Save the media format params |
| sample_format_ = sample_format; |
| channel_count_ = channel_count; |
| frame_rate_ = frame_rate; |
| bits_per_sample_ = bits_per_sample; |
| payload_written_ = 0; |
| |
| // Write inital WAV header |
| zx_status_t status = |
| WriteNewHeader(file_.get(), sample_format_, channel_count_, frame_rate_, |
| bits_per_sample_); |
| if (status != ZX_OK) { |
| Delete(); |
| FXL_LOG(WARNING) << "Failed (" << status << ") writing initial header for " |
| << std::quoted(file_name_); |
| return false; |
| } |
| FXL_LOG(INFO) << "WavWriter[" << this << "] recording Format " |
| << fidl::ToUnderlying(sample_format_) << ", " |
| << bits_per_sample_ << "-bit, " << frame_rate_ << " Hz, " |
| << channel_count_ << "-chan PCM to " << std::quoted(file_name_); |
| return true; |
| } |
| |
| // Write audio data to the file. This assumes that SEEK_SET is at end of file. |
| // This can be called repeatedly without updating the header's length fields, if |
| // desired. To update the header, the caller should also invoke UpdateHeader(). |
| template <bool enabled> |
| bool WavWriter<enabled>::Write(void* const buffer, uint32_t num_bytes) { |
| if (!file_.is_valid()) { |
| return false; |
| } |
| |
| ssize_t amt = WriteData(file_.get(), buffer, num_bytes); |
| if (amt < 0) { |
| FXL_LOG(WARNING) << "Failed (" << amt << ") while writing to " |
| << std::quoted(file_name_); |
| return false; |
| } |
| |
| payload_written_ += amt; |
| if (amt < num_bytes) { |
| FXL_LOG(WARNING) << "Could not write all bytes to the file. " |
| << "Closing file to try to save already-written data."; |
| Close(); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // We've previously written audio data to the file, so update the length fields. |
| // This method need not write the entire header -- only the two length fields. |
| template <bool enabled> |
| bool WavWriter<enabled>::UpdateHeader() { |
| if (!file_.is_valid()) { |
| return false; |
| } |
| |
| zx_status_t status = UpdateHeaderLengths(file_.get(), payload_written_); |
| if (status < 0) { |
| FXL_LOG(WARNING) << "Failed (" << status << ") to update WavHeader for " |
| << std::quoted(file_name_); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // Discard all previously written audio data, and return the WAV file to an |
| // empty (but ready to be written) state. Reclaim file space as possible. |
| template <bool enabled> |
| bool WavWriter<enabled>::Reset() { |
| if (!file_.is_valid()) { |
| FXL_LOG(WARNING) << "Invalid file " << std::quoted(file_name_); |
| return false; |
| } |
| |
| payload_written_ = 0; |
| if (!UpdateHeader()) { |
| return false; |
| } |
| |
| if (::ftruncate(file_.get(), kWavHeaderOverhead) < 0) { |
| FXL_LOG(WARNING) << "Failed to truncate " << std::quoted(file_name_) |
| << ", in WavWriter::Reset()."; |
| Close(); |
| return false; |
| } |
| |
| FXL_LOG(INFO) << "Reset WAV file " << std::quoted(file_name_); |
| return true; |
| } |
| |
| // Finalize the file (update lengths in headers), and reset our file handle. |
| // Any subsequent file updates will fail (although Delete can still succeed). |
| template <bool enabled> |
| bool WavWriter<enabled>::Close() { |
| if (!file_.is_valid()) { |
| FXL_LOG(WARNING) << "Invalid file " << std::quoted(file_name_); |
| return false; |
| } |
| |
| // Keep any additional content since the last header update. |
| if (!UpdateHeader()) { |
| return false; |
| } |
| |
| file_.reset(); |
| FXL_LOG(INFO) << "Closed WAV file " << std::quoted(file_name_); |
| return true; |
| } |
| |
| // Eliminate the WAV file (even if we've already closed it). |
| template <bool enabled> |
| bool WavWriter<enabled>::Delete() { |
| file_.reset(); |
| |
| // If called before Initialize, do nothing. |
| if (file_name_.empty()) { |
| return true; |
| } |
| |
| if (::unlink(file_name_.c_str()) < 0) { |
| FXL_LOG(WARNING) << "Could not delete " << std::quoted(file_name_); |
| return false; |
| } |
| |
| FXL_LOG(INFO) << "Deleted WAV file " << std::quoted(file_name_); |
| return true; |
| } |
| |
| // It should always be possible for a client to enable the WavWriter. |
| template class WavWriter<true>; |
| |
| } // namespace audio |
| } // namespace media |