summaryrefslogtreecommitdiff
path: root/src/codecs/wavpack.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/codecs/wavpack.cpp')
-rw-r--r--src/codecs/wavpack.cpp161
1 files changed, 161 insertions, 0 deletions
diff --git a/src/codecs/wavpack.cpp b/src/codecs/wavpack.cpp
new file mode 100644
index 00000000..fa168d32
--- /dev/null
+++ b/src/codecs/wavpack.cpp
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2025 ayumi <ayumi@noreply.codeberg.org>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "wavpack.hpp"
+
+#include <cstdint>
+#include <cstring>
+#include <algorithm>
+#include <optional>
+
+#include "esp_heap_caps.h"
+#include "codec.hpp"
+#include "esp_log.h"
+#include "result.hpp"
+#include "sample.hpp"
+#include "types.hpp"
+
+namespace codecs {
+
+[[maybe_unused]] static constexpr const char kTag[] = "wavpack";
+// kBufSize and audio::kCodecBufferLength must be equal
+static constexpr const size_t kBufSize = 2048;
+
+static inline constexpr auto loadLe16(std::byte* data) -> uint16_t {
+ return *reinterpret_cast<uint16_t*>(data);
+}
+
+static inline constexpr auto loadLe32(std::byte* data) -> uint32_t {
+ return *reinterpret_cast<uint32_t*>(data);
+}
+
+static auto readProc(void* data, void* buf, long size) -> long {
+ IStream* stream = static_cast<IStream*>(data);
+ const int32_t res = stream->Read({
+ static_cast<std::byte*>(buf),
+ static_cast<std::span<std::byte>::size_type>(size)
+ });
+ return res < 0 ? 0 : res;
+}
+
+WavPackDecoder::WavPackDecoder() : input_(), buf_() {
+ buf_ = static_cast<int32_t*>(
+ heap_caps_malloc(
+ kBufSize * sizeof(int32_t),
+ MALLOC_CAP_INTERNAL | MALLOC_CAP_CACHE_ALIGNED
+ ));
+}
+
+WavPackDecoder::~WavPackDecoder() {
+ heap_caps_free(buf_);
+}
+
+auto WavPackDecoder::OpenStream(std::shared_ptr<IStream> input, uint32_t offset)
+ -> cpp::result<OutputFormat, ICodec::Error> {
+ char error[80];
+ input_ = input;
+ wavpack_ = {};
+ if (!WavpackOpenFileInput(&wavpack_, readProc, input_.get(), error)) {
+ ESP_LOGE(kTag, "WavpackOpenFileInput: %s", error);
+ return cpp::fail(Error::kMalformedData);
+ }
+
+ channels_ = WavpackGetReducedChannels(&wavpack_);
+ bitdepth_ = WavpackGetBitsPerSample(&wavpack_);
+ size_ = kBufSize / channels_;
+ const std::optional total = WavpackGetNumSamples(&wavpack_) == -1
+ ? std::nullopt
+ : std::optional(
+ static_cast<uint64_t>(WavpackGetNumSamples(&wavpack_)) * channels_
+ );
+ const auto rate = WavpackGetSampleRate(&wavpack_);
+ if (offset && total && input_.get()->CanSeek()) {
+ const uint32_t want = offset * rate - 1;
+ if (total < want) {
+ ESP_LOGE(kTag, "seeking: offset points beyond the end of the file");
+ return cpp::fail(Error::kInternalError);
+ }
+
+ uint32_t target;
+ input_->SeekTo(0, IStream::SeekFrom::kStartOfStream);
+ while (true) {
+ std::byte header[32];
+ input_->Read(header);
+ if (memcmp(header, "wvpk", 4) != 0) {
+ ESP_LOGE(kTag, "seeking: header expected, but not found");
+ return cpp::fail(Error::kMalformedData);
+ }
+ const uint32_t size = loadLe32(header + 4);
+ const uint16_t version = loadLe16(header + 8);
+ if (version < 0x402 || version > 0x410) {
+ ESP_LOGE(kTag, "seeking: bad WavPack version (%x)", version);
+ return cpp::fail(Error::kMalformedData);
+ }
+ const uint32_t blockIndex = loadLe32(header + 16);
+ const uint32_t blockSamples = loadLe32(header + 20);
+ if (want >= blockIndex && want <= blockIndex + blockSamples) {
+ input_->SeekTo(-32, IStream::SeekFrom::kCurrentPosition);
+ target = want - blockIndex;
+ break;
+ }
+ input_->SeekTo(size - 24, IStream::SeekFrom::kCurrentPosition);
+ }
+
+ wavpack_ = {};
+ if (!WavpackOpenFileInput(&wavpack_, readProc, input_.get(), error)) {
+ ESP_LOGE(kTag, "WavpackOpenFileInput: %s", error);
+ return cpp::fail(Error::kMalformedData);
+ }
+
+ uint32_t samples = 0;
+ for (size_t i = 0, n = target / size_; i < n; i++)
+ samples += WavpackUnpackSamples(&wavpack_, buf_, size_);
+ samples += WavpackUnpackSamples(&wavpack_, buf_, target % size_);
+ if (WavpackGetNumErrors(&wavpack_) != 0) {
+ ESP_LOGE(kTag, "CRC error");
+ return cpp::fail(Error::kMalformedData);
+ } else if (samples != target || WavpackGetSampleIndex(&wavpack_) != want) {
+ ESP_LOGE(kTag, "seeking: seeking unsuccessful: want %lu, got %lu",
+ target, samples
+ );
+ return cpp::fail(Error::kInternalError);
+ }
+ } else if (offset && (!total || !input_.get()->CanSeek())) {
+ ESP_LOGE(kTag, "seeking: can’t seek");
+ return cpp::fail(Error::kInternalError);
+ }
+
+ const auto size = input->Size();
+ return OutputFormat{
+ .num_channels = channels_,
+ .sample_rate_hz = rate,
+ .total_samples = total,
+ .bitrate_kbps = size && total
+ ? std::optional(
+ ((double)size.value() * 8.0)
+ / ((double)total.value() / channels_ / rate) / 1000
+ )
+ : std::nullopt,
+ };
+}
+
+auto WavPackDecoder::DecodeTo(std::span<sample::Sample> output)
+ -> cpp::result<OutputInfo, Error> {
+ const auto size = std::min(size_, output.size() / channels_);
+ const auto samples = WavpackUnpackSamples(&wavpack_, buf_, size) * channels_;
+ if (WavpackGetNumErrors(&wavpack_) != 0) {
+ ESP_LOGE(kTag, "CRC error");
+ return cpp::fail(Error::kMalformedData);
+ }
+ for (size_t i = 0; i < samples; i++)
+ output[i] = sample::FromSigned(buf_[i], bitdepth_);
+ return OutputInfo{
+ .samples_written = samples,
+ .is_stream_finished = samples == 0,
+ };
+}
+
+} // namespace codecs