From 885eb1812c15263ad759741ad138cf7188fdf739 Mon Sep 17 00:00:00 2001 From: ayumi Date: Fri, 31 Jan 2025 19:08:39 +0100 Subject: Add WavPack support --- src/codecs/CMakeLists.txt | 4 +- src/codecs/codec.cpp | 5 ++ src/codecs/include/types.hpp | 1 + src/codecs/include/wavpack.hpp | 46 ++++++++++++ src/codecs/wavpack.cpp | 161 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 src/codecs/include/wavpack.hpp create mode 100644 src/codecs/wavpack.cpp (limited to 'src/codecs') diff --git a/src/codecs/CMakeLists.txt b/src/codecs/CMakeLists.txt index a6a48c84..1b79b863 100644 --- a/src/codecs/CMakeLists.txt +++ b/src/codecs/CMakeLists.txt @@ -4,9 +4,9 @@ idf_component_register( SRCS "dr_flac.cpp" "codec.cpp" "mad.cpp" "opus.cpp" "vorbis.cpp" - "source_buffer.cpp" "sample.cpp" "wav.cpp" "native.cpp" + "source_buffer.cpp" "sample.cpp" "wav.cpp" "native.cpp" "wavpack.cpp" INCLUDE_DIRS "include" REQUIRES "result" "libmad" "drflac" "tremor" "opusfile" "memory" "util" - "komihash") + "komihash" "wavpack") target_compile_options("${COMPONENT_LIB}" PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/codecs/codec.cpp b/src/codecs/codec.cpp index af5702ff..4ddb16ad 100644 --- a/src/codecs/codec.cpp +++ b/src/codecs/codec.cpp @@ -16,6 +16,7 @@ #include "types.hpp" #include "vorbis.hpp" #include "wav.hpp" +#include "wavpack.hpp" namespace codecs { @@ -33,6 +34,8 @@ auto StreamTypeToString(StreamType t) -> std::string { return "Opus"; case StreamType::kNative: return "Native"; + case StreamType::kWavPack: + return "WavPack"; default: return ""; } @@ -52,6 +55,8 @@ auto CreateCodecForType(StreamType type) -> std::optional { return new WavDecoder(); case StreamType::kNative: return new NativeDecoder(); + case StreamType::kWavPack: + return new WavPackDecoder(); default: return {}; } diff --git a/src/codecs/include/types.hpp b/src/codecs/include/types.hpp index 2bc63b10..493a177a 100644 --- a/src/codecs/include/types.hpp +++ b/src/codecs/include/types.hpp @@ -17,6 +17,7 @@ enum class StreamType { kOpus, kWav, kNative, + kWavPack, }; auto StreamTypeToString(StreamType t) -> std::string; diff --git a/src/codecs/include/wavpack.hpp b/src/codecs/include/wavpack.hpp new file mode 100644 index 00000000..4780b6b6 --- /dev/null +++ b/src/codecs/include/wavpack.hpp @@ -0,0 +1,46 @@ +/* + * Copyright 2025 ayumi + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "wavpack.h" +#include "sample.hpp" + +#include "codec.hpp" + +namespace codecs { + +class WavPackDecoder : public ICodec { + public: + WavPackDecoder(); + ~WavPackDecoder(); + + auto OpenStream(std::shared_ptr input, uint32_t offset) + -> cpp::result override; + + auto DecodeTo(std::span destination) + -> cpp::result override; + + WavPackDecoder(const WavPackDecoder&) = delete; + WavPackDecoder& operator=(const WavPackDecoder&) = delete; + + private: + std::shared_ptr input_; + WavpackContext wavpack_; + int32_t *buf_; + uint8_t bitdepth_; + uint8_t channels_; + size_t size_; +}; + +} // namespace codecs 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 + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "wavpack.hpp" + +#include +#include +#include +#include + +#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(data); +} + +static inline constexpr auto loadLe32(std::byte* data) -> uint32_t { + return *reinterpret_cast(data); +} + +static auto readProc(void* data, void* buf, long size) -> long { + IStream* stream = static_cast(data); + const int32_t res = stream->Read({ + static_cast(buf), + static_cast::size_type>(size) + }); + return res < 0 ? 0 : res; +} + +WavPackDecoder::WavPackDecoder() : input_(), buf_() { + buf_ = static_cast( + 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 input, uint32_t offset) + -> cpp::result { + 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(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 output) + -> cpp::result { + 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 -- cgit v1.2.3