summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorayumi <ayumi@noreply.codeberg.org>2025-01-31 19:08:39 +0100
committerayumi <ayumi@noreply.codeberg.org>2025-03-13 03:29:03 +0100
commit885eb1812c15263ad759741ad138cf7188fdf739 (patch)
tree24aff12a5d67f77675281fd70c0857164e913331 /src
parenta3639860761dcdb5ef9c31bb34497f32cadd9ff3 (diff)
downloadtangara-fw-885eb1812c15263ad759741ad138cf7188fdf739.tar.gz
Add WavPack support
Diffstat (limited to 'src')
-rw-r--r--src/codecs/CMakeLists.txt4
-rw-r--r--src/codecs/codec.cpp5
-rw-r--r--src/codecs/include/types.hpp1
-rw-r--r--src/codecs/include/wavpack.hpp46
-rw-r--r--src/codecs/wavpack.cpp161
-rw-r--r--src/tangara/audio/fatfs_stream_factory.cpp2
-rw-r--r--src/tangara/database/tag_parser.cpp3
-rw-r--r--src/tangara/database/tag_parser.hpp3
-rw-r--r--src/tangara/database/track.hpp1
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 {