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 | |
| parent | a3639860761dcdb5ef9c31bb34497f32cadd9ff3 (diff) | |
| download | tangara-fw-885eb1812c15263ad759741ad138cf7188fdf739.tar.gz | |
Add WavPack support
Diffstat (limited to 'src')
| -rw-r--r-- | src/codecs/CMakeLists.txt | 4 | ||||
| -rw-r--r-- | src/codecs/codec.cpp | 5 | ||||
| -rw-r--r-- | src/codecs/include/types.hpp | 1 | ||||
| -rw-r--r-- | src/codecs/include/wavpack.hpp | 46 | ||||
| -rw-r--r-- | src/codecs/wavpack.cpp | 161 | ||||
| -rw-r--r-- | src/tangara/audio/fatfs_stream_factory.cpp | 2 | ||||
| -rw-r--r-- | src/tangara/database/tag_parser.cpp | 3 | ||||
| -rw-r--r-- | src/tangara/database/tag_parser.hpp | 3 | ||||
| -rw-r--r-- | src/tangara/database/track.hpp | 1 |
9 files changed, 223 insertions, 3 deletions
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<ICodec*> { 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 <ayumi@noreply.codeberg.org> + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include <cstddef> +#include <cstdint> +#include <memory> +#include <optional> +#include <string> +#include <utility> + +#include "wavpack.h" +#include "sample.hpp" + +#include "codec.hpp" + +namespace codecs { + +class WavPackDecoder : public ICodec { + public: + WavPackDecoder(); + ~WavPackDecoder(); + + auto OpenStream(std::shared_ptr<IStream> input, uint32_t offset) + -> cpp::result<OutputFormat, Error> override; + + auto DecodeTo(std::span<sample::Sample> destination) + -> cpp::result<OutputInfo, Error> override; + + WavPackDecoder(const WavPackDecoder&) = delete; + WavPackDecoder& operator=(const WavPackDecoder&) = delete; + + private: + std::shared_ptr<IStream> 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 <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 diff --git a/src/tangara/audio/fatfs_stream_factory.cpp b/src/tangara/audio/fatfs_stream_factory.cpp index 94f22ae9..9089735c 100644 --- a/src/tangara/audio/fatfs_stream_factory.cpp +++ b/src/tangara/audio/fatfs_stream_factory.cpp @@ -88,6 +88,8 @@ auto FatfsStreamFactory::ContainerToStreamType(database::Container enc) return codecs::StreamType::kFlac; case database::Container::kOpus: return codecs::StreamType::kOpus; + case database::Container::kWavPack: + return codecs::StreamType::kWavPack; case database::Container::kUnsupported: default: return {}; diff --git a/src/tangara/database/tag_parser.cpp b/src/tangara/database/tag_parser.cpp index 6c95d496..0be6cb35 100644 --- a/src/tangara/database/tag_parser.cpp +++ b/src/tangara/database/tag_parser.cpp @@ -413,6 +413,9 @@ auto GenericTagParser::ReadAndParseTags(std::string_view p) case Fopus: out->encoding(Container::kOpus); break; + case Fwavpack: + out->encoding(Container::kWavPack); + break; default: out->encoding(Container::kUnsupported); } diff --git a/src/tangara/database/tag_parser.hpp b/src/tangara/database/tag_parser.hpp index 220339c0..eb0f4c7c 100644 --- a/src/tangara/database/tag_parser.hpp +++ b/src/tangara/database/tag_parser.hpp @@ -63,7 +63,8 @@ class GenericTagParser : public ITagParser { // supported audio formats here: // https://cooltech.zone/tangara/docs/music-library/ static constexpr std::string supported_exts[] = {"flac", "mp3", "ogg", - "ogx", "opus", "wav"}; + "ogx", "opus", "wav", + "wv"}; }; } // namespace database diff --git a/src/tangara/database/track.hpp b/src/tangara/database/track.hpp index c7dff425..d6039451 100644 --- a/src/tangara/database/track.hpp +++ b/src/tangara/database/track.hpp @@ -45,6 +45,7 @@ enum class Container { kOgg = 3, kFlac = 4, kOpus = 5, + kWavPack = 6, }; enum class MediaType { |
