Files
JustOneCacophony/src/io/wav_writer.cpp
T
TheM14 42d6da6302
ci / ubuntu-latest (push) Failing after 13s
ci / macos-latest (push) Has been cancelled
ci / windows-latest (push) Has been cancelled
Initial pure C++ implementation
2026-09-25 04:17:30 +08:00

243 lines
8.7 KiB
C++

#include "io/wav_writer.h"
#include "foundation/fs_utf8.h"
#include <cmath>
#include <cstring>
#include <filesystem>
#include <limits>
#include <vector>
namespace joc::io {
namespace {
constexpr std::uint16_t kWaveFormatPcm = 0x0001;
constexpr std::uint16_t kWaveFormatIeeeFloat = 0x0003;
constexpr std::uint16_t kWaveFormatExtensible = 0xFFFE;
constexpr std::uint8_t kPcmGuid[16] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00,
0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71};
constexpr std::uint8_t kFloatGuid[16] = {0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00,
0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71};
void put_u16(std::string* out, std::uint16_t value) {
char buffer[2];
std::memcpy(buffer, &value, 2);
out->append(buffer, 2);
}
void put_u32(std::string* out, std::uint32_t value) {
char buffer[4];
std::memcpy(buffer, &value, 4);
out->append(buffer, 4);
}
void put_u64(std::string* out, std::uint64_t value) {
char buffer[8];
std::memcpy(buffer, &value, 8);
out->append(buffer, 8);
}
// Port of speaker_wav._fmt_chunk.
std::string fmt_chunk(std::uint32_t channels, std::uint32_t rate, SampleFormat format, WavInfo* info) {
std::uint16_t simple_tag = 0;
const std::uint8_t* guid = nullptr;
if (format == SampleFormat::Float32) {
info->bits_per_sample = 32;
info->bytes_per_sample = 4;
simple_tag = kWaveFormatIeeeFloat;
guid = kFloatGuid;
} else {
info->bits_per_sample = 24;
info->bytes_per_sample = 3;
simple_tag = kWaveFormatPcm;
guid = kPcmGuid;
}
const std::uint32_t block_align = channels * info->bytes_per_sample;
const std::uint32_t byte_rate = rate * block_align;
info->block_align = block_align;
std::string body;
if (channels <= 2) {
put_u16(&body, simple_tag);
put_u16(&body, static_cast<std::uint16_t>(channels));
put_u32(&body, rate);
put_u32(&body, byte_rate);
put_u16(&body, static_cast<std::uint16_t>(block_align));
put_u16(&body, static_cast<std::uint16_t>(info->bits_per_sample));
} else {
put_u16(&body, kWaveFormatExtensible);
put_u16(&body, static_cast<std::uint16_t>(channels));
put_u32(&body, rate);
put_u32(&body, byte_rate);
put_u16(&body, static_cast<std::uint16_t>(block_align));
put_u16(&body, static_cast<std::uint16_t>(info->bits_per_sample));
put_u16(&body, 22);
put_u16(&body, static_cast<std::uint16_t>(info->bits_per_sample));
put_u32(&body, 0);
body.append(reinterpret_cast<const char*>(guid), 16);
}
return body;
}
// int32 conversion identical to NumPy's float32 -> int32 cast after clipping.
std::int32_t to_int32(const float value) {
if (!std::isfinite(value)) {
return std::numeric_limits<std::int32_t>::min();
}
return static_cast<std::int32_t>(value);
}
} // namespace
void pack_int24(const float* interleaved, std::size_t frames, std::size_t channels,
std::string* out) {
const std::size_t count = frames * channels;
out->resize(count * 3);
char* target = out->data();
for (std::size_t i = 0; i < count; ++i) {
float value = interleaved[i];
if (value > 1.0f) {
value = 1.0f;
} else if (value < -1.0f) {
value = -1.0f;
}
const std::int32_t scaled = to_int32(value * 8388607.0f);
const std::uint32_t bits = static_cast<std::uint32_t>(scaled);
target[i * 3 + 0] = static_cast<char>(bits & 0xFFu);
target[i * 3 + 1] = static_cast<char>((bits >> 8) & 0xFFu);
target[i * 3 + 2] = static_cast<char>((bits >> 16) & 0xFFu);
}
}
WavWriter::~WavWriter() {
if (file_ != nullptr) {
std::fclose(file_);
file_ = nullptr;
}
}
Status WavWriter::open(const std::string& path, std::uint32_t channels, std::uint32_t rate,
SampleFormat format, std::uint64_t total_frames) {
if (channels == 0 || rate == 0) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kOutput,
"WAV writer needs a positive channel count and rate");
}
path_ = path;
channels_ = channels;
total_frames_ = total_frames;
frames_written_ = 0;
finalized_ = false;
info_ = WavInfo{};
info_.format = format;
const std::string fmt = fmt_chunk(channels, rate, format, &info_);
const std::uint64_t data_size = total_frames * info_.block_align;
info_.data_bytes = data_size;
const std::uint64_t riff_file_size = 12u + 8u + fmt.size() + 8u + data_size;
const bool rf64 = (riff_file_size - 8u) > 0xFFFFFFFFull;
info_.rf64 = rf64;
std::string header;
if (rf64) {
const std::uint64_t file_size = 12u + 36u + 8u + fmt.size() + 8u + data_size;
header.append("RF64", 4);
put_u32(&header, 0xFFFFFFFFu);
header.append("WAVE", 4);
header.append("ds64", 4);
put_u32(&header, 28);
put_u64(&header, file_size - 8u);
put_u64(&header, data_size);
put_u64(&header, total_frames);
put_u32(&header, 0);
} else {
header.append("RIFF", 4);
put_u32(&header, static_cast<std::uint32_t>(riff_file_size - 8u));
header.append("WAVE", 4);
}
header.append("fmt ", 4);
put_u32(&header, static_cast<std::uint32_t>(fmt.size()));
header.append(fmt);
header.append("data", 4);
put_u32(&header, rf64 ? 0xFFFFFFFFu : static_cast<std::uint32_t>(data_size));
file_ = fs_utf8::fopen(path, "wb");
if (file_ == nullptr) {
// The reference creates the parent directory itself.
std::error_code ignored;
const std::filesystem::path parent = std::filesystem::path(path).parent_path();
if (!parent.empty()) {
std::filesystem::create_directories(parent, ignored);
}
file_ = fs_utf8::fopen(path, "wb");
}
if (file_ == nullptr) {
return Status::fail(JOC_ERR_OUTPUT_OPEN, stage::kOutput, "cannot open " + path);
}
if (std::fwrite(header.data(), 1, header.size(), file_) != header.size()) {
std::fclose(file_);
file_ = nullptr;
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "cannot write header to " + path);
}
return Status::success();
}
Status WavWriter::write(const double* interleaved, std::size_t frames) {
if (file_ == nullptr) {
return Status::fail(JOC_ERR_STATE, stage::kOutput, "WAV writer is not open");
}
if (frames == 0) {
return Status::success();
}
if (frames_written_ + frames > total_frames_) {
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput,
"WAV writer received more frames than the header declared (declared " +
std::to_string(total_frames_) + ", written " +
std::to_string(frames_written_) + ", requested " +
std::to_string(frames) + ")");
}
const std::size_t count = frames * channels_;
if (info_.format == SampleFormat::Float32) {
std::vector<float> converted(count);
for (std::size_t i = 0; i < count; ++i) {
converted[i] = static_cast<float>(interleaved[i]);
}
if (std::fwrite(converted.data(), sizeof(float), count, file_) != count) {
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "write failed for " + path_);
}
} else {
std::vector<float> converted(count);
for (std::size_t i = 0; i < count; ++i) {
converted[i] = static_cast<float>(interleaved[i]);
}
std::string packed;
pack_int24(converted.data(), frames, channels_, &packed);
if (std::fwrite(packed.data(), 1, packed.size(), file_) != packed.size()) {
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "write failed for " + path_);
}
}
frames_written_ += frames;
return Status::success();
}
Status WavWriter::finalize() {
if (file_ == nullptr) {
return Status::fail(JOC_ERR_STATE, stage::kOutput, "WAV writer is not open");
}
if (frames_written_ != total_frames_) {
std::fclose(file_);
file_ = nullptr;
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput,
"WAV writer wrote " + std::to_string(frames_written_) + " of " +
std::to_string(total_frames_) + " frames");
}
const int result = std::fclose(file_);
file_ = nullptr;
finalized_ = true;
if (result != 0) {
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "close failed for " + path_);
}
return Status::success();
}
} // namespace joc::io