From a60239c6f712e9a74febe5b24f2d84808d526969 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 12 Dec 2024 19:07:28 -0700 Subject: [PATCH] ktaglib: implement metadata builder --- ktaglib/src/main/cpp/CMakeLists.txt | 1 + ktaglib/src/main/cpp/JVMMetadataBuilder.cpp | 119 ++++++++++++++++++ ktaglib/src/main/cpp/JVMMetadataBuilder.h | 6 +- ktaglib/src/main/cpp/JVMTagMap.cpp | 4 +- ktaglib/src/main/cpp/JVMTagMap.h | 2 +- ktaglib/src/main/cpp/ktaglib.cpp | 2 + .../main/java/org/oxycblt/ktaglib/KTagLib.kt | 36 ++++-- 7 files changed, 154 insertions(+), 16 deletions(-) diff --git a/ktaglib/src/main/cpp/CMakeLists.txt b/ktaglib/src/main/cpp/CMakeLists.txt index f065d1edc..6b7f91aab 100644 --- a/ktaglib/src/main/cpp/CMakeLists.txt +++ b/ktaglib/src/main/cpp/CMakeLists.txt @@ -48,6 +48,7 @@ add_library(${CMAKE_PROJECT_NAME} SHARED ktaglib.cpp JVMInputStream.cpp JVMTagMap.cpp + JVMMetadataBuilder.cpp ) # Specifies libraries CMake should link to your target library. You diff --git a/ktaglib/src/main/cpp/JVMMetadataBuilder.cpp b/ktaglib/src/main/cpp/JVMMetadataBuilder.cpp index 189662428..eb2c11a91 100644 --- a/ktaglib/src/main/cpp/JVMMetadataBuilder.cpp +++ b/ktaglib/src/main/cpp/JVMMetadataBuilder.cpp @@ -3,3 +3,122 @@ // #include "JVMMetadataBuilder.h" + +#include + +JVMMetadataBuilder::JVMMetadataBuilder(JNIEnv *env) : env(env), id3v2(env), xiph(env), mp4(env), + cover(), properties(nullptr) {} + +void JVMMetadataBuilder::setMimeType(const std::string_view mimeType) { + this->mimeType = mimeType; +} + +void JVMMetadataBuilder::setId3v2(const TagLib::ID3v2::Tag &tag) { + for (auto frame: tag.frameList()) { + auto frameId = TagLib::String(frame->frameID()); + auto frameText = frame->toStringList(); + id3v2.add(frameId, frameText); + } +} + +void JVMMetadataBuilder::setXiph(const TagLib::Ogg::XiphComment &tag) { + for (auto field: tag.fieldListMap()) { + auto fieldName = TagLib::String(field.first); + auto fieldValue = field.second; + xiph.add(fieldName, fieldValue); + } +} + +void JVMMetadataBuilder::setMp4(const TagLib::MP4::Tag &tag) { + for (auto item: tag.itemMap()) { + auto atomName = item.first; + // Strip out ID Padding + while (atomName.startsWith("\251")) { + atomName = atomName.substr(1); + } + auto itemName = TagLib::String(atomName); + auto itemValue = item.second; + auto type = itemValue.type(); + + // Only read out the atoms for the reasonable tags we are expecting. + // None of the crazy binary atoms. + if (type == TagLib::MP4::Item::Type::StringList) { + auto value = itemValue.toStringList(); + mp4.add(itemName, value); + return; + } + + // Assume that taggers will be unhinged and store track numbers + // as ints, uints, or longs. + if (type == TagLib::MP4::Item::Type::Int) { + auto value = std::to_string(itemValue.toInt()); + id3v2.add(itemName, value); + return; + } + if (type == TagLib::MP4::Item::Type::UInt) { + auto value = std::to_string(itemValue.toUInt()); + id3v2.add(itemName, value); + return; + } + if (type == TagLib::MP4::Item::Type::LongLong) { + auto value = std::to_string(itemValue.toLongLong()); + id3v2.add(itemName, value); + return; + } + if (type == TagLib::MP4::Item::Type::IntPair) { + // It's inefficient going from the integer representation back into + // a string, but I fully expect taggers to just write "NN/TT" strings + // anyway. + auto value = std::to_string(itemValue.toIntPair().first) + "/" + + std::to_string(itemValue.toIntPair().second); + id3v2.add(itemName, value); + return; + } + // Nothing else makes sense to handle as far as I can tell. + } +} + +void JVMMetadataBuilder::setCover(const TagLib::List covers) { + if (covers.isEmpty()) { + return; + } + // Find the cover with a "front cover" type + for (auto cover: covers) { + auto type = cover["pictureType"].toString(); + if (type == "Front Cover") { + this->cover = cover["data"].toByteVector(); + return; + } + } + // No front cover, just pick first. + // TODO: Consider having cascading fallbacks to increasingly less + // relevant covers perhaps + this->cover = covers.front()["data"].toByteVector(); +} + +void JVMMetadataBuilder::setProperties(TagLib::AudioProperties *properties) { + this->properties = properties; +} + +jobject JVMMetadataBuilder::build() { + jclass propertiesClass = env->FindClass("org/oxycblt/ktaglib/Properties"); + jmethodID propertiesInit = env->GetMethodID(propertiesClass, "", "(Ljava/lang/String;JII)V"); + jobject propertiesObj = env->NewObject(propertiesClass, propertiesInit, + env->NewStringUTF(mimeType.data()), (jlong) properties->lengthInMilliseconds(), + properties->bitrate(), properties->sampleRate()); + env->DeleteLocalRef(propertiesClass); + + jclass metadataClass = env->FindClass("org/oxycblt/ktaglib/Metadata"); + jmethodID metadataInit = env->GetMethodID(metadataClass, "", "(Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;[BLorg/oxycblt/ktaglib/Properties;)V"); + jobject id3v2Map = id3v2.getObject(); + jobject xiphMap = xiph.getObject(); + jobject mp4Map = mp4.getObject(); + jbyteArray coverArray = nullptr; + if (cover.has_value()) { + coverArray = env->NewByteArray(cover->size()); + env->SetByteArrayRegion(coverArray, 0, cover->size(), reinterpret_cast(cover->data())); + } + jobject metadataObj = env->NewObject(metadataClass, metadataInit, id3v2Map, xiphMap, mp4Map, coverArray, propertiesObj); + env->DeleteLocalRef(metadataClass); + return metadataObj; +} \ No newline at end of file diff --git a/ktaglib/src/main/cpp/JVMMetadataBuilder.h b/ktaglib/src/main/cpp/JVMMetadataBuilder.h index 1e02f3540..dd150cbf9 100644 --- a/ktaglib/src/main/cpp/JVMMetadataBuilder.h +++ b/ktaglib/src/main/cpp/JVMMetadataBuilder.h @@ -25,7 +25,7 @@ public: void setXiph(const TagLib::Ogg::XiphComment &tag); void setMp4(const TagLib::MP4::Tag &tag); void setCover(const TagLib::List covers); - void setProperties(const TagLib::AudioProperties &properties); + void setProperties(TagLib::AudioProperties *properties); jobject build(); @@ -34,8 +34,8 @@ private: std::string_view mimeType; - TagLib::ByteVector cover; - TagLib::AudioProperties &properties; + std::optional cover; + TagLib::AudioProperties *properties; JVMTagMap id3v2; JVMTagMap xiph; diff --git a/ktaglib/src/main/cpp/JVMTagMap.cpp b/ktaglib/src/main/cpp/JVMTagMap.cpp index db572b251..c8b3723e8 100644 --- a/ktaglib/src/main/cpp/JVMTagMap.cpp +++ b/ktaglib/src/main/cpp/JVMTagMap.cpp @@ -22,9 +22,9 @@ JVMTagMap::~JVMTagMap() { env->DeleteLocalRef(hashMap); } -void JVMTagMap::add(TagLib::String &key, TagLib::String &value) { +void JVMTagMap::add(TagLib::String &key, std::string_view value) { jstring jKey = env->NewStringUTF(key.toCString(true)); - jstring jValue = env->NewStringUTF(value.toCString(true)); + jstring jValue = env->NewStringUTF(value.data()); // check if theres already a value arraylist in the map jobject existingValue = env->CallObjectMethod(hashMap, hashMapGetMethod, jKey); diff --git a/ktaglib/src/main/cpp/JVMTagMap.h b/ktaglib/src/main/cpp/JVMTagMap.h index db6f543ab..a2cba8d45 100644 --- a/ktaglib/src/main/cpp/JVMTagMap.h +++ b/ktaglib/src/main/cpp/JVMTagMap.h @@ -18,7 +18,7 @@ public: JVMTagMap(const JVMTagMap &) = delete; JVMTagMap &operator=(const JVMTagMap &) = delete; - void add(TagLib::String &key, TagLib::String &value); + void add(TagLib::String &key, std::string_view value); void add(TagLib::String &key, TagLib::StringList &value); jobject getObject(); diff --git a/ktaglib/src/main/cpp/ktaglib.cpp b/ktaglib/src/main/cpp/ktaglib.cpp index ccb6c2ce3..728deda23 100644 --- a/ktaglib/src/main/cpp/ktaglib.cpp +++ b/ktaglib/src/main/cpp/ktaglib.cpp @@ -49,5 +49,7 @@ Java_org_oxycblt_ktaglib_KTagLib_openNative( return nullptr; } + builder.setProperties(file->audioProperties()); + return builder.build(); } diff --git a/ktaglib/src/main/java/org/oxycblt/ktaglib/KTagLib.kt b/ktaglib/src/main/java/org/oxycblt/ktaglib/KTagLib.kt index e76526d74..ec54b3ea6 100644 --- a/ktaglib/src/main/java/org/oxycblt/ktaglib/KTagLib.kt +++ b/ktaglib/src/main/java/org/oxycblt/ktaglib/KTagLib.kt @@ -14,14 +14,14 @@ object KTagLib { * Note: This method is blocking and should be handled as such if * calling from a coroutine. */ - fun open(context: Context, ref: FileRef): Tag? { + fun open(context: Context, ref: FileRef): Metadata? { val inputStream = AndroidInputStream(context, ref) val tag = openNative(inputStream) inputStream.close() return tag } - private external fun openNative(ioStream: AndroidInputStream): Tag? + private external fun openNative(ioStream: AndroidInputStream): Metadata? } data class FileRef( @@ -29,28 +29,44 @@ data class FileRef( val uri: Uri ) -data class Tag( +data class Metadata( val id3v2: Map, - val vorbis: Map, - val coverData: ByteArray + val xiph: Map, + val mp4: Map, + val cover: ByteArray?, + val properties: Properties ) { override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false - other as Tag + other as Metadata if (id3v2 != other.id3v2) return false - if (vorbis != other.vorbis) return false - if (!coverData.contentEquals(other.coverData)) return false + if (xiph != other.xiph) return false + if (mp4 != other.mp4) return false + if (cover != null) { + if (other.cover == null) return false + if (!cover.contentEquals(other.cover)) return false + } else if (other.cover != null) return false + if (properties != other.properties) return false return true } override fun hashCode(): Int { var result = id3v2.hashCode() - result = 31 * result + vorbis.hashCode() - result = 31 * result + coverData.contentHashCode() + result = 31 * result + xiph.hashCode() + result = 31 * result + mp4.hashCode() + result = 31 * result + (cover?.contentHashCode() ?: 0) + result = 31 * result + properties.hashCode() return result } } + +data class Properties( + val mimeType: String, + val durationMs: Long, + val bitrate: Int, + val sampleRate: Int, +)