diff options
| author | ayumi <ayumi@noreply.codeberg.org> | 2025-01-31 19:08:39 +0100 |
|---|---|---|
| committer | ayumi <ayumi@noreply.codeberg.org> | 2025-03-13 03:29:03 +0100 |
| commit | 885eb1812c15263ad759741ad138cf7188fdf739 (patch) | |
| tree | 24aff12a5d67f77675281fd70c0857164e913331 /src/codecs/wavpack.cpp | |
| parent | a3639860761dcdb5ef9c31bb34497f32cadd9ff3 (diff) | |
| download | tangara-fw-885eb1812c15263ad759741ad138cf7188fdf739.tar.gz | |
Add WavPack support
Diffstat (limited to 'src/codecs/wavpack.cpp')
| -rw-r--r-- | src/codecs/wavpack.cpp | 161 |
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 |
