Compare commits

...

81 commits

Author SHA1 Message Date
Alexander Capehart
1dc33c1ae1
musikr: fix use-after-free in iostream shim 2025-02-21 08:49:41 -07:00
Alexander Capehart
e22f811c81
musikr: pass iostream ptr to cpp 2025-02-19 16:38:32 -07:00
Alexander Capehart
47604724a4
musikr: dont pass JObject ownership
90% sure this causes the JObject to be dropped incorrectly.
2025-02-19 16:04:58 -07:00
Alexander Capehart
894e265a2c
musikr: temp disable stripping 2025-02-19 16:04:19 -07:00
Alexander Capehart
774b2a90bb
musikr: cleanup 2025-02-19 14:16:47 -07:00
Alexander Capehart
2d7cafbdbf
musikr: strip down lib size 2025-02-19 13:04:23 -07:00
Alexander Capehart
89ab6ee336
musikr: clean up buildscript 2025-02-19 13:04:22 -07:00
Alexander Capehart
2b73be1995
actions: install rust targets 2025-02-19 12:28:38 -07:00
Alexander Capehart
78a06a0430
musikr: remove old cpp module 2025-02-19 12:23:16 -07:00
Alexander Capehart
97bac886d0
musikr: depend on cargo build 2025-02-19 12:22:35 -07:00
Alexander Capehart
b60e3c5880
musikr: improve id3v2 tag mgmt 2025-02-19 12:08:12 -07:00
Alexander Capehart
313365d118
musikr: reformat 2025-02-19 11:29:18 -07:00
Alexander Capehart
03c596e03c
musikr: fix incorrect borrowing in jtagmap 2025-02-19 11:27:20 -07:00
Alexander Capehart
a3d853226f
musikr: use correct object in jtagmap impl 2025-02-19 10:57:57 -07:00
Alexander Capehart
a8dcb5de71
musikr: cleanup 2025-02-19 10:47:25 -07:00
Alexander Capehart
f4a760e3b6
musikr: remove unsound file_ref drop impl 2025-02-19 10:47:10 -07:00
Alexander Capehart
ca169a9354
musikr: fix jni env borrows 2025-02-19 10:46:44 -07:00
Alexander Capehart
11ab27df97
musikr: fix file_shim linking 2025-02-19 10:46:27 -07:00
Alexander Capehart
c91286826f
musikr: log panic messages
JNI errors wont report it normally
2025-02-19 10:46:14 -07:00
Alexander Capehart
269c593a7e
musikr: disable implict cpp linking 2025-02-19 10:45:10 -07:00
Alexander Capehart
4655e314b9
musikr: connect metajni lib to app 2025-02-19 10:43:38 -07:00
Alexander Capehart
f50d9680a9
musikr: explicitly declare c++_static dep 2025-02-19 10:43:21 -07:00
Alexander Capehart
e68242b8ee
musikr: fix exception unwind linking 2025-02-19 10:43:07 -07:00
Alexander Capehart
57eb30701d
musikr: cleanup and cover stuff 2025-02-17 21:33:10 -07:00
Alexander Capehart
8415db30ff
musikr: implement main flow 2025-02-17 21:10:39 -07:00
Alexander Capehart
de9a60d182
musikr: enforce this contract 2025-02-17 20:59:14 -07:00
Alexander Capehart
fc5c49081f
musikr: remove ref map entry returns
Not needed
2025-02-17 18:40:34 -07:00
Alexander Capehart
12caac1f80
musikr: fix iostream lifecycle
Finally pass over ownership of the RsIOStream to the normal
IOStream.
2025-02-17 17:26:05 -07:00
Alexander Capehart
da43ebda96
musikr: add wav files 2025-02-17 16:28:19 -07:00
Alexander Capehart
a3f01f152b
musikr: add mp4 file 2025-02-17 15:20:21 -07:00
Alexander Capehart
f939e9d251
musikr: hide this and bridge modules 2025-02-17 15:09:23 -07:00
Alexander Capehart
d4890af780
musikr: add id3v1 tags to files 2025-02-17 14:39:59 -07:00
Alexander Capehart
60e5614e63
musikr: add flac id3v2 tags 2025-02-17 14:37:38 -07:00
Alexander Capehart
6385928150
musikr: add metadata builders 2025-02-17 14:24:33 -07:00
Alexander Capehart
1cb2f5026f
musikr: remove pinning for immutable refs
Not actually needed, can just use plain refs
2025-02-17 11:58:57 -07:00
Alexander Capehart
98bf82ea15
musikr: make ogg field list map consistent 2025-02-17 11:39:15 -07:00
Alexander Capehart
2c03cf8fed
musikr: add mp4 item bindings 2025-02-17 11:35:15 -07:00
Alexander Capehart
608082a49f
musikr: remove explicit lifecycles now in ffi 2025-02-17 10:11:13 -07:00
Alexander Capehart
c4f51b749a
musikr: document this contract 2025-02-17 10:10:50 -07:00
Alexander Capehart
65d8959bcf
musikr: add basic mp4 tag 2025-02-17 09:25:17 -07:00
Alexander Capehart
6ef79a9aa5
musikr: fix id3v1 shim 2025-02-17 08:43:14 -07:00
Alexander Capehart
fa87ea09b8
musikr: improve xiph fieldlistmap lifecycle mgmt 2025-02-17 08:43:05 -07:00
Alexander Capehart
024cadf530
musikr: add id3v1 tag 2025-02-17 08:39:09 -07:00
Alexander Capehart
0cb1b3a309
musikr: introduce this abstraction
Gets rid of a lot of the duplicate pin stuff
2025-02-17 08:17:04 -07:00
Alexander Capehart
c8d645c282
musikr: reformat 2025-02-15 16:02:55 -07:00
Alexander Capehart
b45e41bc3b
musikr: cleanup 2025-02-15 16:02:30 -07:00
Alexander Capehart
59f66978ff
musikr: make id3v2 naming consistent 2025-02-15 15:47:33 -07:00
Alexander Capehart
f7d61cd1dc
musikr: add id3v2 tags 2025-02-15 15:37:54 -07:00
Alexander Capehart
0f3bed413d
musikr: improve lifecycles 2025-02-15 13:04:50 -07:00
Alexander Capehart
f5de03dfee
musikr: refined flac pic support 2025-02-15 12:02:32 -07:00
Alexander Capehart
3dfcf0f67a
musikr: initial flac pic support
Probably broken
2025-02-14 23:24:22 -07:00
Alexander Capehart
61069bd4fe
musikr: redo bridge api
Try to introduce some level of standardization in use.
2025-02-14 22:20:48 -07:00
Alexander Capehart
03d8f70ecd
musikr: get rid of warnings 2025-02-14 21:15:32 -07:00
Alexander Capehart
7906fcf5af
musikr: improve iostream lifecycle mgmt 2025-02-14 21:05:54 -07:00
Alexander Capehart
249915c3be
musikr: fix iostream pinning 2025-02-14 15:14:14 -07:00
Alexander Capehart
74edd1dbdf
musikr: fix rustiostream ownership problem
Sadly this requires me to more or less give an &mut off to TagLib to
own while also owning the core type to drop later, but since stream
is only owned to be dropped it's fine.
2025-02-14 14:26:25 -07:00
Alexander Capehart
3ecdbf289b
musikr: reorganize ffi module
Still don't fully like the segregation, but now at least the bridge
unsafety is fully isolated.
2025-02-12 22:45:56 -07:00
Alexander Capehart
86b04eaead
musikr: add flac file 2025-02-12 21:40:48 -07:00
Alexander Capehart
42dfe4edcc
musikr: reformat 2025-02-12 19:15:01 -07:00
Alexander Capehart
f3f349847a
Revert "musikr: use no_std in crate"
This reverts commit 7f84349f2e.
2025-02-12 19:14:25 -07:00
Alexander Capehart
520e52b100
musikr: improve taglib buildscript 2025-02-12 19:11:31 -07:00
Alexander Capehart
854164a523
musikr: eliminate need for libc++_shared 2025-02-12 17:44:50 -07:00
Alexander Capehart
7f84349f2e
musikr: use no_std in crate
Doesn't really help since jni uses std excessively.
2025-02-10 13:02:02 -07:00
Alexander Capehart
c115e34aac
musikr: reformat shim 2025-02-10 11:21:49 -07:00
Alexander Capehart
20785300bb
musikr: partially clean up ffi mod 2025-02-10 10:48:49 -07:00
Alexander Capehart
5c4d0ab5f6
musikr: remove taglib build shell script 2025-02-08 21:46:46 -07:00
Alexander Capehart
289582964c
musikr: start putting unsafe stuff into ffi mod
Aiming for like 3 layers of abstraction:
Layer 1: Top-level taglib-esque API translated to jni
Layer 2: Slightly extended unsafe wrappers over bindings
Level 3: Raw taglib bindings and shims
2025-02-08 21:42:39 -07:00
Alexander Capehart
3aa39a7065
musikr: basic vorbis support 2025-02-08 19:52:10 -07:00
Alexander Capehart
013f25f46f
musikr: more fine-grained file handling 2025-02-08 17:45:20 -07:00
Alexander Capehart
acee4ddedd
musikr: minimize ffi shims 2025-02-08 17:03:47 -07:00
Alexander Capehart
cf597cb98e
msuikr: split up shims 2025-02-08 16:16:03 -07:00
Alexander Capehart
cd102369a0
musikr: ignore .vscode 2025-02-08 16:15:43 -07:00
Alexander Capehart
16fc14a4da
musikr: add format-specific file api 2025-02-08 15:27:17 -07:00
Alexander Capehart
005898d776
musikr: add audioproperties to metajni interface 2025-02-08 15:08:38 -07:00
Alexander Capehart
2ee9556564
musikr: integrate metadatajni into app
Just extracts title rn while I still work on the ffi.
2025-02-08 11:56:28 -07:00
Alexander Capehart
729a3c3273
musikr: basic taglib rust shim 2025-02-08 10:15:36 -07:00
Alexander Capehart
534f06d7e1
musikr: link to library 2025-02-06 14:46:29 -07:00
Alexander Capehart
ed0abb661c
musikr: implement taglib build step for rust module 2025-02-04 17:02:59 -07:00
Alexander Capehart
6216e1d591
musikr: move taglib to new rust module 2025-02-04 16:20:48 -07:00
Alexander Capehart
d6cf484d61
musikr: init rust metadata jni library 2025-02-04 16:15:17 -07:00
Alexander Capehart
df68768842
musikr.metadata: handle case w/no mp4 covers
Otherwise could have triggered an exception.
2025-01-31 14:41:44 -07:00
72 changed files with 3838 additions and 1501 deletions

View file

@ -13,6 +13,14 @@ jobs:
steps:
- name: Install ninja-build
run: sudo apt-get install -y ninja-build
- name: Setup arm rust target
run: rustup target add armv7-linux-androideabi
- name: Setup x86 rust target
run: rustup target add i686-linux-android
- name: Setup arm64 rust target
run: rustup target add aarch64-linux-android
- name: Setup x86_64 rust target
run: rustup target add x86_64-linux-android
- name: Clone repository
uses: actions/checkout@v4
- name: Clone submodules

6
.gitmodules vendored
View file

@ -2,7 +2,7 @@
path = media
url = https://github.com/OxygenCobalt/media.git
[submodule "musikr/src/main/cpp/taglib"]
path = musikr/src/main/cpp/taglib
[submodule "musikr/src/main/jni/taglib"]
path = musikr/src/main/jni/taglib
url = https://github.com/taglib/taglib.git
tag = v2.0.2
tag = v2.0.2

View file

@ -42,10 +42,6 @@ android {
debug {
applicationIdSuffix ".debug"
versionNameSuffix "-DEBUG"
ndk {
debugSymbolLevel 'FULL'
}
}
release {

View file

@ -33,6 +33,7 @@ plugins {
id "com.google.devtools.ksp" version '2.0.21-1.0.25' apply false
// We use spotless in the root build.gradle to apply to all modules.
id "com.diffplug.spotless" version "6.25.0" apply true
id "org.mozilla.rust-android-gradle.rust-android" version "0.9.6" apply false
}
spotless {

View file

@ -25,3 +25,7 @@ android.nonFinalResIds=true
org.gradle.parallel=true
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
project.RUST_ANDROID_GRADLE_ARMV7_LINUX_ANDROIDEABI_NDK_PATH=android.ndkDirectory
project.RUST_ANDROID_GRADLE_I686_LINUX_ANDROID_NDK_PATH=android.ndkDirectory
project.RUST_ANDROID_GRADLE_AARCH64_LINUX_ANDROID_NDK_PATH=android.ndkDirectory
project.RUST_ANDROID_GRADLE_X64_64_LINUX_ANDROID_NDK_PATH=android.ndkDirectory

2
media

@ -1 +1 @@
Subproject commit 9a0e432c08ee572056f99b9c26d9657753c87fe2
Subproject commit 4b3084e1b63185eaeffa7cac9d7015040e0e2aa5

4
musikr/.gitignore vendored
View file

@ -1 +1,3 @@
/build
/build
taglib/build/
taglib/pkg/

View file

@ -7,6 +7,7 @@ plugins {
id "com.diffplug.spotless"
id "kotlin-parcelize"
id "org.jetbrains.dokka"
id "org.mozilla.rust-android-gradle.rust-android"
}
android {
@ -20,27 +21,20 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
externalNativeBuild {
cmake {
cppFlags ""
}
ndk {
stl "c++_static"
}
}
buildTypes {
release {
// R8 flips out that random classes were stripped for unknown reasons
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.22.1"
}
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_17
@ -57,6 +51,15 @@ android {
}
}
cargo {
module = "./src/main/jni"
libname = "metadatajni"
targets = ["arm", "arm64", "x86", "x86_64"]
prebuiltToolchains = true
profile = "release"
}
dependencies {
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
@ -82,22 +85,12 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
}
task assembleTaglib(type: Exec) {
def jniDir = "$projectDir/src/main/cpp"
def libs = new File("$jniDir/taglib/pkg")
if (libs.exists()) {
commandLine "true"
return
tasks.configureEach { task ->
if ((task.name == 'javaPreCompileDebug' || task.name == 'javaPreCompileRelease')) {
task.dependsOn 'cargoBuild'
}
commandLine "sh", "-c", "$jniDir/build_taglib.sh $jniDir $android.ndkDirectory"
}
afterEvaluate {
preDebugBuild.dependsOn assembleTaglib
preReleaseBuild.dependsOn assembleTaglib
}
clean {
delete "$projectDir/src/main/cpp/taglib/pkg"
delete "$projectDir/src/main/cpp/taglib/build"
delete "$projectDir/build/rustJniLibs"
}

View file

@ -1,71 +0,0 @@
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html.
# For more examples on how to use CMake, see https://github.com/android/ndk-samples.
# Sets the minimum CMake version required for this project.
cmake_minimum_required(VERSION 3.22.1)
# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
# Since this is the top level CMakeLists.txt, the project name is also accessible
# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
# build script scope).
project("tagJNI") # becomes "libtagJNI.so"
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
#
# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
# is preferred for the same purpose.
#
# In order to load a library into your app from Java/Kotlin, you must call
# System.loadLibrary() and pass the name of the library defined here;
# for GameActivity/NativeActivity derived applications, the same library name must be
# used in the AndroidManifest.xml file.
set(taglib_location "${CMAKE_CURRENT_SOURCE_DIR}/taglib")
set(taglib_pkg "${taglib_location}/pkg/${ANDROID_ABI}")
set(taglib_lib "${taglib_pkg}/lib")
set(taglib_include "${taglib_pkg}/include")
set(taglib_file_name libtag.a)
set(taglib_file_path ${taglib_lib}/${taglib_file_name})
set(taglib_lib_name, "taglib")
add_library(
"taglib"
STATIC
IMPORTED)
set_target_properties(
"taglib" PROPERTIES
IMPORTED_LOCATION
${taglib_file_path}
INTERFACE_INCLUDE_DIRECTORIES
${taglib_include})
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
taglib_jni.cpp
JInputStream.cpp
JTagMap.cpp
JMetadataBuilder.cpp
JClassRef.cpp
JObjectRef.cpp
JStringRef.cpp
JByteArrayRef.cpp
)
target_link_options(${CMAKE_PROJECT_NAME}
# @Tolriq found that these flags can reduce the size of the linked
# taglib + jni shim shared library. Kudos to them.
# https://github.com/taglib/taglib/issues/1212#issuecomment-2326456903
# Additionally, enable 16kb page size. I believe taglib can support this fine,
# as a cursory glance indicates that it doesn't hardcode any page sizes.
PRIVATE "-Wl,--exclude-libs,ALL,-z,max-page-size=16384")
# Specifies libraries CMake should link to your target library. You
# can link libraries from various origins, such as libraries defined in this
# build script, prebuilt third-party libraries, or Android system libraries.
target_link_libraries(${CMAKE_PROJECT_NAME}
# List libraries link to the target library
PRIVATE android
PRIVATE log
PRIVATE taglib)

View file

@ -1,47 +0,0 @@
/*
* Copyright (c) 2025 Auxio Project
* JByteArrayRef.cpp is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "JByteArrayRef.h"
JByteArrayRef::JByteArrayRef(JNIEnv *env, TagLib::ByteVector &data) : env(env) {
auto size = static_cast<jsize>(data.size());
array = env->NewByteArray(size);
env->SetByteArrayRegion(array, 0, static_cast<jsize>(size),
reinterpret_cast<const jbyte*>(data.data()));
}
JByteArrayRef::JByteArrayRef(JNIEnv *env, jbyteArray array) : env(env), array(
array) {
}
JByteArrayRef::~JByteArrayRef() {
env->DeleteLocalRef(array);
}
TagLib::ByteVector JByteArrayRef::copy() {
jsize length = env->GetArrayLength(array);
auto data = env->GetByteArrayElements(array, nullptr);
TagLib::ByteVector byteVector(reinterpret_cast<const char*>(data), length);
env->ReleaseByteArrayElements(array, data, JNI_ABORT);
return byteVector;
}
jbyteArray& JByteArrayRef::operator*() {
return array;
}

View file

@ -1,45 +0,0 @@
/*
* Copyright (c) 2025 Auxio Project
* JByteArrayRef.h is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef AUXIO_JBYTEARRAYREF_H
#define AUXIO_JBYTEARRAYREF_H
#include <jni.h>
#include <taglib/tbytevector.h>
class JByteArrayRef {
public:
JByteArrayRef(JNIEnv *env, TagLib::ByteVector &data);
JByteArrayRef(JNIEnv *env, jbyteArray array);
~JByteArrayRef();
JByteArrayRef(const JByteArrayRef&) = delete;
JByteArrayRef& operator=(const JByteArrayRef&) = delete;
TagLib::ByteVector copy();
jbyteArray& operator*();
private:
JNIEnv *env;
jbyteArray array;
};
#endif //AUXIO_JBYTEARRAYREF_H

View file

@ -1,34 +0,0 @@
/*
* Copyright (c) 2025 Auxio Project
* JClassRef.cpp is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "JClassRef.h"
JClassRef::JClassRef(JNIEnv *env, const char *classpath) : env(env) {
clazz = env->FindClass(classpath);
}
JClassRef::~JClassRef() {
env->DeleteLocalRef(clazz);
}
jmethodID JClassRef::method(const char *name, const char *signature) {
return env->GetMethodID(clazz, name, signature);
}
jclass& JClassRef::operator*() {
return clazz;
}

View file

@ -1,45 +0,0 @@
/*
* Copyright (c) 2025 Auxio Project
* JClassRef.h is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef AUXIO_JCLASSREF_H
#define AUXIO_JCLASSREF_H
#include <jni.h>
class JClassRef {
public:
JClassRef(JNIEnv *env, const char *classpath);
~JClassRef();
JClassRef(const JClassRef&) = delete;
JClassRef& operator=(const JClassRef&) = delete;
// Only exists to work around a broken lint that doesn't
// realize that this class is a smart pointer to jclass.
jmethodID method(const char *name, const char *signature);
jclass& operator*();
private:
JNIEnv *env;
jclass clazz;
};
#endif //AUXIO_JCLASSREF_H

View file

@ -1,145 +0,0 @@
/*
* Copyright (c) 2024 Auxio Project
* JInputStream.cpp is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "JInputStream.h"
#include <cmath>
#include "JClassRef.h"
#include "JByteArrayRef.h"
#include "JStringRef.h"
JInputStream::JInputStream(JNIEnv *env, jobject jInputStream) : env(env), jInputStream(
jInputStream) {
JClassRef jInputStreamClass = { env,
"org/oxycblt/musikr/metadata/NativeInputStream" };
if (!env->IsInstanceOf(jInputStream, *jInputStreamClass)) {
throw std::runtime_error("Object is not NativeInputStream");
}
jInputStreamNameMethod = jInputStreamClass.method("name",
"()Ljava/lang/String;");
jInputStreamReadBlockMethod = jInputStreamClass.method("readBlock",
"(Ljava/nio/ByteBuffer;)Z");
jInputStreamIsOpenMethod = jInputStreamClass.method("isOpen", "()Z");
jInputStreamSeekFromBeginningMethod = jInputStreamClass.method(
"seekFromBeginning", "(J)Z");
jInputStreamSeekFromCurrentMethod = jInputStreamClass.method(
"seekFromCurrent", "(J)Z");
jInputStreamSeekFromEndMethod = jInputStreamClass.method("seekFromEnd",
"(J)Z");
jInputStreamTellMethod = jInputStreamClass.method("tell", "()J");
jInputStreamLengthMethod = jInputStreamClass.method("length", "()J");
}
JInputStream::~JInputStream() {
// The implicit assumption is that inputStream is managed by the owner,
// so we don't need to delete any references here
}
TagLib::FileName JInputStream::name() const {
// Not actually used except in FileRef, can safely ignore.
JStringRef jName { env, reinterpret_cast<jstring>(env->CallObjectMethod(
jInputStream, jInputStreamNameMethod)) };
return jName.copy().toCString();
}
TagLib::ByteVector JInputStream::readBlock(size_t length) {
// We have to invert the buffer allocation here siits not a perfect system (vykeen instead of korvax0 but i warped all over the hub and i dont think its possible to find a "perfect" purple system like you would withnce the JVM ByteBuffer allocation system
// uses a bugged caching mechanism that leaks memory if used in multithreaded contexts.
TagLib::ByteVector buf { static_cast<unsigned int>(length), 0 };
jobject wrappedByteBuffer = env->NewDirectByteBuffer(buf.data(),
buf.size());
if (wrappedByteBuffer == nullptr) {
throw std::runtime_error("Failed to wrap ByteBuffer");
}
JObjectRef byteBuffer = { env, wrappedByteBuffer };
jboolean result = env->CallBooleanMethod(jInputStream,
jInputStreamReadBlockMethod, *byteBuffer);
if (!result) {
throw std::runtime_error("Failed to read block, see logs");
}
return buf;
}
void JInputStream::writeBlock(const TagLib::ByteVector &data) {
throw std::runtime_error("Not implemented");
}
void JInputStream::insert(const TagLib::ByteVector &data,
TagLib::offset_t start, size_t replace) {
throw std::runtime_error("Not implemented");
}
void JInputStream::removeBlock(TagLib::offset_t start, size_t length) {
throw std::runtime_error("Not implemented");
}
bool JInputStream::readOnly() const {
return true;
}
bool JInputStream::isOpen() const {
return env->CallBooleanMethod(jInputStream, jInputStreamIsOpenMethod);
}
void JInputStream::seek(TagLib::offset_t offset, Position p) {
auto joffset = static_cast<jlong>(std::llround(offset));
jboolean result;
switch (p) {
case Beginning:
result = env->CallBooleanMethod(jInputStream,
jInputStreamSeekFromBeginningMethod, joffset);
break;
case Current:
result = env->CallBooleanMethod(jInputStream,
jInputStreamSeekFromCurrentMethod, joffset);
break;
case End:
result = env->CallBooleanMethod(jInputStream,
jInputStreamSeekFromEndMethod, joffset);
break;
}
if (!result) {
throw std::runtime_error("Failed to seek, see logs");
}
}
void JInputStream::clear() {
// Nothing to do
}
TagLib::offset_t JInputStream::tell() const {
jlong jposition = env->CallLongMethod(jInputStream, jInputStreamTellMethod);
if (jposition == INT64_MIN) {
throw std::runtime_error("Failed to get position, see logs");
}
return static_cast<TagLib::offset_t>(jposition);
}
TagLib::offset_t JInputStream::length() {
jlong jlength = env->CallLongMethod(jInputStream, jInputStreamLengthMethod);
if (jlength == INT64_MIN) {
throw std::runtime_error("Failed to get length, see logs");
}
return static_cast<TagLib::offset_t>(jlength);
}
void JInputStream::truncate(TagLib::offset_t length) {
throw std::runtime_error("Not implemented");
}

View file

@ -1,128 +0,0 @@
/*
* Copyright (c) 2024 Auxio Project
* JInputStream.h is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef AUXIO_JINPUTSTREAM_H
#define AUXIO_JINPUTSTREAM_H
#include <jni.h>
#include "JObjectRef.h"
#include "taglib/tiostream.h"
class JInputStream: public TagLib::IOStream {
public:
JInputStream(JNIEnv *env, jobject jInputStream);
~JInputStream();
JInputStream(const JInputStream&) = delete;
JInputStream& operator=(const JInputStream&) = delete;
/*!
* Returns the stream name in the local file system encoding.
*/
TagLib::FileName name() const override;
/*!
* Reads a block of size \a length at the current get pointer.
*/
TagLib::ByteVector readBlock(size_t length) override;
/*!
* Attempts to write the block \a data at the current get pointer. If the
* file is currently only opened read only -- i.e. readOnly() returns \c true --
* this attempts to reopen the file in read/write mode.
*
* \note This should be used instead of using the streaming output operator
* for a ByteVector. And even this function is significantly slower than
* doing output with a char[].
*/
void writeBlock(const TagLib::ByteVector &data) override;
/*!
* Insert \a data at position \a start in the file overwriting \a replace
* bytes of the original content.
*
* \note This method is slow since it requires rewriting all of the file
* after the insertion point.
*/
void insert(const TagLib::ByteVector &data, TagLib::offset_t start = 0,
size_t replace = 0) override;
/*!
* Removes a block of the file starting a \a start and continuing for
* \a length bytes.
*
* \note This method is slow since it involves rewriting all of the file
* after the removed portion.
*/
void removeBlock(TagLib::offset_t start = 0, size_t length = 0) override;
/*!
* Returns \c true if the file is read only (or if the file can not be opened).
*/
bool readOnly() const override;
/*!
* Since the file can currently only be opened as an argument to the
* constructor (sort-of by design), this returns if that open succeeded.
*/
bool isOpen() const override;
/*!
* Move the I/O pointer to \a offset in the stream from position \a p. This
* defaults to seeking from the beginning of the stream.
*
* \see Position
*/
void seek(TagLib::offset_t offset, Position p = Beginning) override;
/*!
* Reset the end-of-stream and error flags on the stream.
*/
void clear() override;
/*!
* Returns the current offset within the stream.
*/
TagLib::offset_t tell() const override;
/*!
* Returns the length of the stream.
*/
TagLib::offset_t length() override;
/*!
* Truncates the stream to a \a length.
*/
void truncate(TagLib::offset_t length) override;
private:
JNIEnv *env;
jobject jInputStream;
jmethodID jInputStreamNameMethod;
jmethodID jInputStreamReadBlockMethod;
jmethodID jInputStreamIsOpenMethod;
jmethodID jInputStreamSeekFromBeginningMethod;
jmethodID jInputStreamSeekFromCurrentMethod;
jmethodID jInputStreamSeekFromEndMethod;
jmethodID jInputStreamTellMethod;
jmethodID jInputStreamLengthMethod;
};
#endif //AUXIO_JINPUTSTREAM_H

View file

@ -1,217 +0,0 @@
/*
* Copyright (c) 2024 Auxio Project
* JMetadataBuilder.cpp is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "JMetadataBuilder.h"
#include "util.h"
#include <taglib/mp4tag.h>
#include <taglib/textidentificationframe.h>
#include <taglib/attachedpictureframe.h>
#include <taglib/tpropertymap.h>
#include "JObjectRef.h"
#include "JClassRef.h"
#include "JStringRef.h"
#include "JByteArrayRef.h"
JMetadataBuilder::JMetadataBuilder(JNIEnv *env) : env(env), id3v2(env), xiph(
env), mp4(env), cover(), properties(nullptr) {
}
void JMetadataBuilder::setMimeType(TagLib::String type) {
mimeType = type;
}
void JMetadataBuilder::setId3v1(TagLib::ID3v1::Tag &tag) {
id3v2.add_id("TIT2", tag.title());
id3v2.add_id("TPE1", tag.artist());
id3v2.add_id("TALB", tag.album());
id3v2.add_id("TRCK", std::to_string(tag.track()));
id3v2.add_id("TYER", std::to_string(tag.year()));
const int genreNumber = tag.genreNumber();
if (genreNumber != 255) {
id3v2.add_id("TCON", std::to_string(genreNumber));
}
}
void JMetadataBuilder::setId3v2(TagLib::ID3v2::Tag &tag) {
// We want to ideally find the front cover, fall back to the first picture otherwise.
std::optional<TagLib::ID3v2::AttachedPictureFrame*> firstPic;
std::optional<TagLib::ID3v2::AttachedPictureFrame*> frontCoverPic;
for (auto frame : tag.frameList()) {
if (auto txxxFrame =
dynamic_cast<TagLib::ID3v2::UserTextIdentificationFrame*>(frame)) {
TagLib::String id = frame->frameID();
TagLib::StringList frameText = txxxFrame->fieldList();
if (frameText.isEmpty())
continue;
auto begin = frameText.begin();
TagLib::String description = *begin;
frameText.erase(begin);
id3v2.add_combined(id, description, frameText);
} else if (auto textFrame =
dynamic_cast<TagLib::ID3v2::TextIdentificationFrame*>(frame)) {
TagLib::String key = frame->frameID();
TagLib::StringList frameText = textFrame->fieldList();
id3v2.add_id(key, frameText);
} else if (auto pictureFrame =
dynamic_cast<TagLib::ID3v2::AttachedPictureFrame*>(frame)) {
if (!firstPic) {
firstPic = pictureFrame;
}
if (!frontCoverPic
&& pictureFrame->type()
== TagLib::ID3v2::AttachedPictureFrame::FrontCover) {
frontCoverPic = pictureFrame;
}
} else {
continue;
}
}
if (frontCoverPic) {
auto pic = *frontCoverPic;
cover = pic->picture();
} else if (firstPic) {
auto pic = *firstPic;
cover = pic->picture();
}
}
void JMetadataBuilder::setXiph(TagLib::Ogg::XiphComment &tag) {
for (auto field : tag.fieldListMap()) {
auto key = field.first.upper();
auto values = field.second;
xiph.add_custom(key, values);
}
auto pics = tag.pictureList();
setFlacPictures(pics);
}
template<typename T>
void mp4AddImpl(JTagMap &map, TagLib::String &itemName, T itemValue) {
if (itemName.startsWith("----")) {
// Split this into it's atom name and description
auto split = itemName.find(':');
auto atomName = itemName.substr(0, split);
auto atomDescription = itemName.substr(split + 1);
map.add_combined(atomName, atomDescription, itemValue);
} else {
map.add_id(itemName, itemValue);
}
}
void JMetadataBuilder::setMp4(TagLib::MP4::Tag &tag) {
auto map = tag.itemMap();
std::optional < TagLib::MP4::CoverArt > firstCover;
for (auto item : map) {
auto itemName = item.first;
auto itemValue = item.second;
if (itemName == "covr") {
// Special cover case.
// MP4 has no types, so just prioritize easier to decode covers (PNG, JPEG)
auto pics = itemValue.toCoverArtList();
for (auto &pic : pics) {
auto format = pic.format();
if (format == TagLib::MP4::CoverArt::PNG
|| format == TagLib::MP4::CoverArt::JPEG) {
cover = pic.data();
continue;
}
}
cover = pics.front().data();
continue;
}
auto type = itemValue.type();
std::string serializedValue;
switch (type) {
// Normal expected MP4 items
case TagLib::MP4::Item::Type::StringList:
mp4AddImpl(mp4, itemName, itemValue.toStringList());
break;
// Weird MP4 items I'm 90% sure I'll encounter.
case TagLib::MP4::Item::Type::Int:
serializedValue = std::to_string(itemValue.toInt());
break;
case TagLib::MP4::Item::Type::UInt:
serializedValue = std::to_string(itemValue.toUInt());
break;
case TagLib::MP4::Item::Type::LongLong:
serializedValue = std::to_string(itemValue.toLongLong());
break;
case 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, and musikr doesn't have to do as much fiddly variant handling.
serializedValue = std::to_string(itemValue.toIntPair().first) + "/"
+ std::to_string(itemValue.toIntPair().second);
break;
default:
// Don't care about the other types
continue;
}
mp4AddImpl(mp4, itemName, TagLib::String(serializedValue));
}
}
void JMetadataBuilder::setFlacPictures(
TagLib::List<TagLib::FLAC::Picture*> &pics) {
// Find the front cover image. If it doesn't exist, fall back to the first image.
for (auto pic : pics) {
if (pic->type() == TagLib::FLAC::Picture::FrontCover) {
cover = pic->data();
return;
}
}
if (!pics.isEmpty()) {
cover = pics.front()->data();
}
}
void JMetadataBuilder::setProperties(TagLib::AudioProperties *properties) {
this->properties = properties;
}
jobject JMetadataBuilder::build() {
JClassRef jPropertiesClass { env, "org/oxycblt/musikr/metadata/Properties" };
jmethodID jPropertiesInitMethod = jPropertiesClass.method("<init>",
"(Ljava/lang/String;JII)V");
JStringRef jMimeType { env, this->mimeType };
JObjectRef jProperties { env, env->NewObject(*jPropertiesClass,
jPropertiesInitMethod, *jMimeType,
(jlong) properties->lengthInMilliseconds(), properties->bitrate(),
properties->sampleRate()) };
JClassRef jMetadataClass { env, "org/oxycblt/musikr/metadata/Metadata" };
jmethodID jMetadataInitMethod = jMetadataClass.method("<init>",
"(Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;[BLorg/"
"oxycblt/musikr/metadata/Properties;)V");
auto jId3v2Map = id3v2.getObject();
auto jXiphMap = xiph.getObject();
auto jMp4Map = mp4.getObject();
if (cover.has_value()) {
JByteArrayRef jCoverArray { env, cover.value() };
jobject result = env->NewObject(*jMetadataClass, jMetadataInitMethod,
**jId3v2Map, **jXiphMap, **jMp4Map, *jCoverArray, *jProperties);
return result;
}
return env->NewObject(*jMetadataClass, jMetadataInitMethod, **jId3v2Map,
**jXiphMap, **jMp4Map, nullptr, *jProperties);
}

View file

@ -1,61 +0,0 @@
/*
* Copyright (c) 2024 Auxio Project
* JMetadataBuilder.h is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef AUXIO_JMETADATABUILDER_H
#define AUXIO_JMETADATABUILDER_H
#include <jni.h>
#include <string_view>
#include <optional>
#include "taglib/id3v1tag.h"
#include "taglib/id3v2tag.h"
#include "taglib/xiphcomment.h"
#include "taglib/mp4tag.h"
#include "taglib/audioproperties.h"
#include "JTagMap.h"
class JMetadataBuilder {
public:
JMetadataBuilder(JNIEnv *env);
void setMimeType(TagLib::String type);
void setId3v1(TagLib::ID3v1::Tag &tag);
void setId3v2(TagLib::ID3v2::Tag &tag);
void setXiph(TagLib::Ogg::XiphComment &tag);
void setMp4(TagLib::MP4::Tag &tag);
void setFlacPictures(TagLib::List<TagLib::FLAC::Picture*> &pics);
void setProperties(TagLib::AudioProperties *properties);
jobject build();
private:
JNIEnv *env;
TagLib::String mimeType;
std::optional<TagLib::ByteVector> cover;
TagLib::AudioProperties *properties;
JTagMap id3v2;
JTagMap xiph;
JTagMap mp4;
};
#endif //AUXIO_JMETADATABUILDER_H

View file

@ -1,30 +0,0 @@
/*
* Copyright (c) 2025 Auxio Project
* JObjectRef.cpp is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "JObjectRef.h"
JObjectRef::JObjectRef(JNIEnv *env, jobject object) : env(env), object(object) {
}
JObjectRef::~JObjectRef() {
env->DeleteLocalRef(object);
}
jobject& JObjectRef::operator*() {
return object;
}

View file

@ -1,44 +0,0 @@
/*
* Copyright (c) 2025 Auxio Project
* JObjectRef.h is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef AUXIO_JOBJECTREF_H
#define AUXIO_JOBJECTREF_H
#include <jni.h>
#include <memory>
#include <taglib/tstring.h>
#include "JObjectRef.h"
class JObjectRef {
public:
JObjectRef(JNIEnv *env, jobject object);
~JObjectRef();
JObjectRef(const JObjectRef&) = delete;
JObjectRef& operator=(const JObjectRef&) = delete;
jobject& operator*();
private:
JNIEnv *env;
jobject object;
};
#endif //AUXIO_JOBJECTREF_H

View file

@ -1,43 +0,0 @@
/*
* Copyright (c) 2025 Auxio Project
* JStringRef.cpp is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "JStringRef.h"
#include "util.h"
JStringRef::JStringRef(JNIEnv *env, jstring jString) : env(env), string(jString) {
}
JStringRef::JStringRef(JNIEnv *env, const TagLib::String string) {
this->env = env;
this->string = env->NewStringUTF(string.toCString(true));
}
JStringRef::~JStringRef() {
env->DeleteLocalRef(string);
}
TagLib::String JStringRef::copy() {
auto chars = env->GetStringUTFChars(string, nullptr);
TagLib::String result = chars;
env->ReleaseStringUTFChars(string, chars);
return result;
}
jstring& JStringRef::operator*() {
return string;
}

View file

@ -1,46 +0,0 @@
/*
* Copyright (c) 2025 Auxio Project
* JStringRef.h is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef AUXIO_JSTRINGREF_H
#define AUXIO_JSTRINGREF_H
#include <jni.h>
#include <taglib/tstring.h>
class JStringRef {
public:
JStringRef(JNIEnv *env, jstring jString);
JStringRef(JNIEnv *env, TagLib::String string);
~JStringRef();
JStringRef(const JStringRef&) = delete;
JStringRef& operator=(const JStringRef&) = delete;
TagLib::String copy();
jstring& operator*();
private:
JNIEnv *env;
jstring string;
};
#endif //AUXIO_JSTRINGREF_H

View file

@ -1,118 +0,0 @@
/*
* Copyright (c) 2024 Auxio Project
* JTagMap.cpp is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "JTagMap.h"
#include "JStringRef.h"
JTagMap::JTagMap(JNIEnv *env) : env(env) {
auto jTagMapClass = std::make_unique < JClassRef
> (env, "org/oxycblt/musikr/metadata/NativeTagMap");
auto jTagMapInitMethod = jTagMapClass->method("<init>", "()V");
jTagMap = std::move(
std::make_unique < JObjectRef
> (env, env->NewObject(**jTagMapClass, jTagMapInitMethod)));
jTagMapAddIdSingleMethod = jTagMapClass->method("addID",
"(Ljava/lang/String;Ljava/lang/String;)V");
jTagMapAddIdListMethod = jTagMapClass->method("addID",
"(Ljava/lang/String;Ljava/util/List;)V");
jTagMapAddCustomSingleMethod = jTagMapClass->method("addCustom",
"(Ljava/lang/String;Ljava/lang/String;)V");
jTagMapAddCustomListMethod = jTagMapClass->method("addCustom",
"(Ljava/lang/String;Ljava/util/List;)V");
jTagMapAddCombinedSingleMethod = jTagMapClass->method("addCombined",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
jTagMapAddCombinedListMethod = jTagMapClass->method("addCombined",
"(Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V");
jTagMapGetObjectMethod = jTagMapClass->method("getObject",
"()Ljava/util/Map;");
jArrayListClass = std::make_unique < JClassRef
> (env, "java/util/ArrayList");
jArrayListInitMethod = jArrayListClass->method("<init>", "()V");
jArrayListAddMethod = jArrayListClass->method("add",
"(Ljava/lang/Object;)Z");
}
void JTagMap::add_id(const TagLib::String id, const TagLib::String value) {
JStringRef jId { env, id };
JStringRef jValue { env, value };
env->CallVoidMethod(**jTagMap, jTagMapAddIdSingleMethod, *jId, *jValue);
}
void JTagMap::add_id(const TagLib::String id, const TagLib::StringList values) {
JStringRef jId { env, id };
JObjectRef jValues { env, env->NewObject(**jArrayListClass,
jArrayListInitMethod) };
for (auto &value : values) {
JStringRef jValue { env, value };
env->CallBooleanMethod(*jValues, jArrayListAddMethod, *jValue);
}
env->CallVoidMethod(**jTagMap, jTagMapAddIdListMethod, *jId, *jValues);
}
void JTagMap::add_custom(const TagLib::String description,
const TagLib::String value) {
JStringRef jDescription { env, description };
JStringRef jValue { env, value };
env->CallVoidMethod(**jTagMap, jTagMapAddCustomSingleMethod, *jDescription,
*jValue);
}
void JTagMap::add_custom(const TagLib::String description,
const TagLib::StringList values) {
JStringRef jDescription { env, description };
JObjectRef jValues { env, env->NewObject(**jArrayListClass,
jArrayListInitMethod) };
for (auto &value : values) {
JStringRef jValue { env, value };
env->CallBooleanMethod(*jValues, jArrayListAddMethod, *jValue);
}
env->CallVoidMethod(**jTagMap, jTagMapAddCustomListMethod, *jDescription,
*jValues);
}
void JTagMap::add_combined(const TagLib::String id,
const TagLib::String description, const TagLib::String value) {
JStringRef jId { env, id };
JStringRef jDescription { env, description };
JStringRef jValue { env, value };
env->CallVoidMethod(**jTagMap, jTagMapAddCombinedSingleMethod, *jId,
*jDescription, *jValue);
}
void JTagMap::add_combined(const TagLib::String id,
const TagLib::String description, const TagLib::StringList values) {
JStringRef jId { env, id };
JStringRef jDescription { env, description };
JObjectRef jValues { env, env->NewObject(**jArrayListClass,
jArrayListInitMethod) };
for (auto &value : values) {
JStringRef jValue { env, value };
env->CallBooleanMethod(*jValues, jArrayListAddMethod, *jValue);
}
env->CallVoidMethod(**jTagMap, jTagMapAddCombinedListMethod, *jId,
*jDescription, *jValues);
}
std::unique_ptr<JObjectRef> JTagMap::getObject() {
return std::move(
std::make_unique < JObjectRef
> (env, env->CallObjectMethod(**jTagMap,
jTagMapGetObjectMethod)));
}

View file

@ -1,67 +0,0 @@
/*
* Copyright (c) 2024 Auxio Project
* JTagMap.h is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef AUXIO_JTAGMAP_H
#define AUXIO_JTAGMAP_H
#include <jni.h>
#include <string_view>
#include <taglib/tstring.h>
#include <taglib/tstringlist.h>
#include "JObjectRef.h"
#include "JClassRef.h"
class JTagMap {
public:
JTagMap(JNIEnv *env);
JTagMap(const JTagMap&) = delete;
JTagMap& operator=(const JTagMap&) = delete;
void add_id(TagLib::String id, TagLib::String value);
void add_id(TagLib::String id, TagLib::StringList values);
void add_custom(TagLib::String description, TagLib::String value);
void add_custom(TagLib::String description, TagLib::StringList values);
void add_combined(TagLib::String id, TagLib::String description,
TagLib::String value);
void add_combined(TagLib::String id, TagLib::String description,
TagLib::StringList values);
std::unique_ptr<JObjectRef> getObject();
private:
JNIEnv *env;
std::unique_ptr<JObjectRef> jTagMap;
jmethodID jTagMapAddIdSingleMethod;
jmethodID jTagMapAddIdListMethod;
jmethodID jTagMapAddCustomSingleMethod;
jmethodID jTagMapAddCustomListMethod;
jmethodID jTagMapAddCombinedSingleMethod;
jmethodID jTagMapAddCombinedListMethod;
jmethodID jTagMapGetObjectMethod;
std::unique_ptr<JClassRef> jArrayListClass;
jmethodID jArrayListInitMethod;
jmethodID jArrayListAddMethod;
};
#endif //AUXIO_JTAGMAP_H

View file

@ -1,42 +0,0 @@
set -e
WORKING_DIR=$1
echo "Working directory is at $WORKING_DIR"
cd "$WORKING_DIR"
TAGLIB_SRC_DIR=${WORKING_DIR}/taglib
TAGLIB_DST_DIR=${WORKING_DIR}/taglib/build
TAGLIB_PKG_DIR=${WORKING_DIR}/taglib/pkg
NDK_TOOLCHAIN=${WORKING_DIR}/android.toolchain.cmake
NDK_PATH=$2
echo "Taglib source is at $TAGLIB_SRC_DIR"
echo "Taglib build is at $TAGLIB_DST_DIR"
echo "Taglib package is at $TAGLIB_PKG_DIR"
echo "NDK toolchain is at $NDK_TOOLCHAIN"
echo "NDK path is at $NDK_PATH"
X86_ARCH=x86
X86_64_ARCH=x86_64
ARMV7_ARCH=armeabi-v7a
ARMV8_ARCH=arm64-v8a
build_for_arch() {
local ARCH=$1
local DST_DIR=$TAGLIB_DST_DIR/$ARCH
local PKG_DIR=$TAGLIB_PKG_DIR/$ARCH
cd $TAGLIB_SRC_DIR
cmake -B $DST_DIR -DANDROID_NDK_PATH=${NDK_PATH} -DCMAKE_TOOLCHAIN_FILE=${NDK_TOOLCHAIN} \
-DANDROID_ABI=$ARCH -DBUILD_SHARED_LIBS=OFF -DVISIBILITY_HIDDEN=ON -DBUILD_TESTING=OFF \
-DBUILD_EXAMPLES=OFF -DBUILD_BINDINGS=OFF -DWITH_ZLIB=OFF -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS="-fPIC"
# Try to parallelize the build
cmake --build $DST_DIR --config Release -j$(nproc)
cd $WORKING_DIR
cmake --install $DST_DIR --config Release --prefix $PKG_DIR --strip
}
build_for_arch $X86_ARCH
build_for_arch $X86_64_ARCH
build_for_arch $ARMV7_ARCH
build_for_arch $ARMV8_ARCH

@ -1 +0,0 @@
Subproject commit 648f5e588209464702e4955de614e391d3768ec8

View file

@ -1,200 +0,0 @@
/*
* Copyright (c) 2024 Auxio Project
* taglib_jni.cpp is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <jni.h>
#include <string>
#include "JInputStream.h"
#include "JMetadataBuilder.h"
#include "util.h"
#include "taglib/fileref.h"
#include "taglib/flacfile.h"
#include "taglib/mp4file.h"
#include "taglib/mpegfile.h"
#include "taglib/opusfile.h"
#include "taglib/vorbisfile.h"
#include "taglib/wavfile.h"
bool parseMpeg(const char *name, TagLib::File *file,
JMetadataBuilder &jBuilder) {
auto *mpegFile = dynamic_cast<TagLib::MPEG::File*>(file);
if (mpegFile == nullptr) {
return false;
}
auto id3v1Tag = mpegFile->ID3v1Tag();
if (id3v1Tag != nullptr) {
try {
jBuilder.setId3v1(*id3v1Tag);
} catch (std::exception &e) {
LOGE("Unable to parse ID3v1 tag in %s: %s", name, e.what());
}
}
auto id3v2Tag = mpegFile->ID3v2Tag();
if (id3v2Tag != nullptr) {
try {
jBuilder.setId3v2(*id3v2Tag);
} catch (std::exception &e) {
LOGE("Unable to parse ID3v2 tag in %s: %s", name, e.what());
}
}
return true;
}
bool parseMp4(const char *name, TagLib::File *file,
JMetadataBuilder &jBuilder) {
auto *mp4File = dynamic_cast<TagLib::MP4::File*>(file);
if (mp4File == nullptr) {
return false;
}
auto tag = mp4File->tag();
if (tag != nullptr) {
try {
jBuilder.setMp4(*tag);
} catch (std::exception &e) {
LOGE("Unable to parse MP4 tag in %s: %s", name, e.what());
}
}
return true;
}
bool parseFlac(const char *name, TagLib::File *file,
JMetadataBuilder &jBuilder) {
auto *flacFile = dynamic_cast<TagLib::FLAC::File*>(file);
if (flacFile == nullptr) {
return false;
}
auto id3v1Tag = flacFile->ID3v1Tag();
if (id3v1Tag != nullptr) {
try {
jBuilder.setId3v1(*id3v1Tag);
} catch (std::exception &e) {
LOGE("Unable to parse ID3v1 tag in %s: %s", name, e.what());
}
}
auto id3v2Tag = flacFile->ID3v2Tag();
if (id3v2Tag != nullptr) {
try {
jBuilder.setId3v2(*id3v2Tag);
} catch (std::exception &e) {
LOGE("Unable to parse ID3v2 tag in %s: %s", name, e.what());
}
}
auto xiphComment = flacFile->xiphComment();
if (xiphComment != nullptr) {
try {
jBuilder.setXiph(*xiphComment);
} catch (std::exception &e) {
LOGE("Unable to parse Xiph comment in %s: %s", name, e.what());
}
}
auto pics = flacFile->pictureList();
jBuilder.setFlacPictures(pics);
return true;
}
bool parseOpus(const char *name, TagLib::File *file,
JMetadataBuilder &jBuilder) {
auto *opusFile = dynamic_cast<TagLib::Ogg::Opus::File*>(file);
if (opusFile == nullptr) {
return false;
}
auto tag = opusFile->tag();
if (tag != nullptr) {
try {
jBuilder.setXiph(*tag);
} catch (std::exception &e) {
LOGE("Unable to parse Xiph comment in %s: %s", name, e.what());
}
}
return true;
}
bool parseVorbis(const char *name, TagLib::File *file,
JMetadataBuilder &jBuilder) {
auto *vorbisFile = dynamic_cast<TagLib::Ogg::Vorbis::File*>(file);
if (vorbisFile == nullptr) {
return false;
}
auto tag = vorbisFile->tag();
if (tag != nullptr) {
try {
jBuilder.setXiph(*tag);
} catch (std::exception &e) {
LOGE("Unable to parse Xiph comment %s: %s", name, e.what());
}
}
return true;
}
bool parseWav(const char *name, TagLib::File *file,
JMetadataBuilder &jBuilder) {
auto *wavFile = dynamic_cast<TagLib::RIFF::WAV::File*>(file);
if (wavFile == nullptr) {
return false;
}
auto tag = wavFile->ID3v2Tag();
if (tag != nullptr) {
try {
jBuilder.setId3v2(*tag);
} catch (std::exception &e) {
LOGE("Unable to parse ID3v2 tag in %s: %s", name, e.what());
}
}
return true;
}
extern "C" JNIEXPORT jobject JNICALL
Java_org_oxycblt_musikr_metadata_TagLibJNI_openNative(JNIEnv *env,
jobject /* this */,
jobject inputStream) {
const char *name = nullptr;
try {
JInputStream jStream {env, inputStream};
name = jStream.name();
TagLib::FileRef fileRef {&jStream};
if (fileRef.isNull()) {
throw std::runtime_error("Invalid file");
}
TagLib::File *file = fileRef.file();
JMetadataBuilder jBuilder {env};
jBuilder.setProperties(file->audioProperties());
// TODO: Make some type of composable logger so I don't
// have to shoehorn this into the native code.
if (parseMpeg(name, file, jBuilder)) {
jBuilder.setMimeType("audio/mpeg");
} else if (parseMp4(name, file, jBuilder)) {
jBuilder.setMimeType("audio/mp4");
} else if (parseFlac(name, file, jBuilder)) {
jBuilder.setMimeType("audio/flac");
} else if (parseOpus(name, file, jBuilder)) {
jBuilder.setMimeType("audio/opus");
} else if (parseVorbis(name, file, jBuilder)) {
jBuilder.setMimeType("audio/vorbis");
} else if (parseWav(name, file, jBuilder)) {
jBuilder.setMimeType("audio/wav");
} else {
LOGE("File format in %s is not supported", name);
return nullptr;
}
return jBuilder.build();
} catch (std::exception &e) {
LOGE("Unable to parse metadata in %s: %s", name != nullptr ? name : "unknown file", e.what());
return nullptr;
}
}

View file

@ -24,34 +24,7 @@ internal data class Metadata(
val mp4: Map<String, List<String>>,
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 Metadata
if (id3v2 != other.id3v2) 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 + xiph.hashCode()
result = 31 * result + mp4.hashCode()
result = 31 * result + (cover?.contentHashCode() ?: 0)
result = 31 * result + properties.hashCode()
return result
}
}
)
internal data class Properties(
val mimeType: String,

View file

@ -19,6 +19,7 @@
package org.oxycblt.musikr.metadata
import android.os.ParcelFileDescriptor
import android.util.Log
import java.io.FileInputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -36,6 +37,10 @@ private object MetadataExtractorImpl : MetadataExtractor {
override suspend fun extract(deviceFile: DeviceFile, fd: ParcelFileDescriptor) =
withContext(Dispatchers.IO) {
val fis = FileInputStream(fd.fileDescriptor)
TagLibJNI.open(deviceFile, fis).also { fis.close() }
val input = NativeInputStream(deviceFile, fis)
MetadataJNI.openFile(input).also {
Log.d("MetadataExtractor", "Metadata: ${it?.id3v2}")
fis.close()
}
}
}

View file

@ -1,6 +1,6 @@
/*
* Copyright (c) 2024 Auxio Project
* util.h is part of Auxio.
* MetadataJNI.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -16,16 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef AUXIO_UTIL_H
#define AUXIO_UTIL_H
package org.oxycblt.musikr.metadata
#include <jni.h>
#include <android/log.h>
internal object MetadataJNI {
init {
System.loadLibrary("metadatajni")
}
#define LOG_TAG "taglib_jni"
#define LOGE(...) \
((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__))
#define LOGD(...) \
((void)__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__))
#endif //AUXIO_UTIL_H
// This is a rust function, Android Studio has no idea how to link to it
external fun openFile(input: NativeInputStream): Metadata?
}

View file

@ -1,42 +0,0 @@
/*
* Copyright (c) 2024 Auxio Project
* TagLibJNI.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.metadata
import java.io.FileInputStream
import org.oxycblt.musikr.fs.DeviceFile
internal object TagLibJNI {
init {
System.loadLibrary("tagJNI")
}
/**
* Open a file and extract a tag.
*
* Note: This method is blocking and should be handled as such if calling from a coroutine.
*/
fun open(deviceFile: DeviceFile, fis: FileInputStream): Metadata? {
val inputStream = NativeInputStream(deviceFile, fis)
val tag = openNative(inputStream)
inputStream.close()
return tag
}
private external fun openNative(inputStream: NativeInputStream): Metadata?
}

2
musikr/src/main/jni/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
target/
.vscode/

507
musikr/src/main/jni/Cargo.lock generated Normal file
View file

@ -0,0 +1,507 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "android_log-sys"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ecc8056bf6ab9892dcd53216c83d1597487d7dacac16c8df6b877d127df9937"
[[package]]
name = "android_logger"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b07e8e73d720a1f2e4b6014766e6039fd2e96a4fa44e2a78d0e1fa2ff49826"
dependencies = [
"android_log-sys",
"env_filter",
"log",
]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "bytemuck"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
[[package]]
name = "bytes"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
[[package]]
name = "cc"
version = "1.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9"
dependencies = [
"shlex",
]
[[package]]
name = "cesu8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.5.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c"
dependencies = [
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_lex"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "codespan-reporting"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
dependencies = [
"termcolor",
"unicode-width",
]
[[package]]
name = "combine"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
dependencies = [
"bytes",
"memchr",
]
[[package]]
name = "ctor"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "cxx"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc49567e08c72902f4cbc7242ee8d874ec9cbe97fbabf77b4e0e1f447513e13a"
dependencies = [
"cc",
"cxxbridge-cmd",
"cxxbridge-flags",
"cxxbridge-macro",
"foldhash",
"link-cplusplus",
]
[[package]]
name = "cxx-build"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe46b5309c99e9775e7a338c98e4097455f52db5b684fd793ca22848fde6e371"
dependencies = [
"cc",
"codespan-reporting",
"proc-macro2",
"quote",
"scratch",
"syn",
]
[[package]]
name = "cxxbridge-cmd"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4315c4ce8d23c26d87f2f83698725fd5718d8e6ace4a9093da2664d23294d372"
dependencies = [
"clap",
"codespan-reporting",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "cxxbridge-flags"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f55d69deb3a92f610a60ecc524a72c7374b6dc822f8fb7bb4e5d9473f10530c4"
[[package]]
name = "cxxbridge-macro"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bee7a1d9b5091462002c2b8de2a4ed0f0fde011d503cc272633f66075bd5141"
dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "env_filter"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
dependencies = [
"log",
]
[[package]]
name = "foldhash"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
[[package]]
name = "jni"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
dependencies = [
"cesu8",
"cfg-if",
"combine",
"jni-sys",
"log",
"thiserror",
"walkdir",
"windows-sys 0.45.0",
]
[[package]]
name = "jni-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "link-cplusplus"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9"
dependencies = [
"cc",
]
[[package]]
name = "log"
version = "0.4.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "metadatajni"
version = "1.0.0"
dependencies = [
"android_logger",
"bytemuck",
"ctor",
"cxx",
"cxx-build",
"jni",
"link-cplusplus",
"log",
]
[[package]]
name = "proc-macro2"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustversion"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scratch"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

View file

@ -0,0 +1,25 @@
[package]
name = "metadatajni"
version = "1.0.0"
edition = "2021"
[lib]
name = "metadatajni"
crate-type = ["cdylib"]
[dependencies]
bytemuck = "1.21.0"
cxx = "1.0.137"
jni = "0.21.1"
link-cplusplus = {version = "1.0.9", features = ["nothing"]}
android_logger = { version = "0.14.1", default-features = false }
log = "0.4.20"
ctor = "0.2.6"
[build-dependencies]
cxx-build = "1.0.137"
link-cplusplus = {version = "1.0.9", features = ["nothing"]}
[profile.release]
lto = true
codegen-units = 1

View file

@ -0,0 +1,158 @@
use cxx_build;
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::error::Error;
fn build_taglib(target: &str, working_dir: &Path) -> Result<PathBuf, Box<dyn Error>> {
let taglib_src_dir = working_dir.join("taglib");
let taglib_build_dir = taglib_src_dir.join("build");
let taglib_pkg_dir = taglib_src_dir.join("pkg");
let arch_build_dir = taglib_build_dir.join(&target);
let arch_pkg_dir = taglib_pkg_dir.join(&target);
// If lib/libtag.a exists, we don't need to build it
if arch_pkg_dir.join("lib/libtag.a").exists() {
return Ok(arch_pkg_dir);
}
// Prepare basic cmake arguments
let mut cmake_args = vec![
"-B".to_string(),
arch_build_dir.to_str().unwrap().to_string(),
"-DBUILD_SHARED_LIBS=OFF".to_string(),
"-DVISIBILITY_HIDDEN=ON".to_string(),
"-DBUILD_TESTING=OFF".to_string(),
"-DBUILD_EXAMPLES=OFF".to_string(),
"-DBUILD_BINDINGS=OFF".to_string(),
"-DWITH_ZLIB=OFF".to_string(),
"-DCMAKE_BUILD_TYPE=Release".to_string(),
"-DCMAKE_CXX_FLAGS=-fPIC".to_string(),
];
if target.contains("android") {
// Android target, we need to tack on the NDK toolchain file/args
let arch = if target == "x86_64-linux-android" {
"x86_64"
} else if target.contains("i686-linux-android") {
"x86"
} else if target.contains("aarch64-linux-android") {
"arm64-v8a"
} else if target.contains("armv7-linux-androideabi") {
"armeabi-v7a"
} else {
// should never happen
return Err(format!("Unsupported Android target: {}", target).into());
};
let clang_path = env::var("CLANG_PATH").expect("CLANG_PATH env var not set");
let toolchains_marker = "/toolchains";
let ndk_path = if let Some(pos) = clang_path.find(toolchains_marker) {
&clang_path[..pos]
} else {
return Err(format!("CLANG_PATH does not contain '{}'", toolchains_marker).into());
};
let ndk_toolchain = working_dir.join("android.toolchain.cmake");
cmake_args.extend(vec![
format!("-DANDROID_NDK_PATH={}", ndk_path),
format!("-DCMAKE_TOOLCHAIN_FILE={}", ndk_toolchain.to_str().unwrap()),
format!("-DANDROID_ABI={}", arch),
]);
}
// Configure step
let status = Command::new("cmake")
.args(&cmake_args)
.current_dir(&taglib_src_dir)
.status()?;
if !status.success() {
return Err(format!("cmake configure failed for target {}", target).into());
}
// Build step
let status = Command::new("cmake")
.arg("--build")
.arg(arch_build_dir.to_str().unwrap())
.arg("--config")
.arg("Release")
.arg(format!(
"-j{}",
std::thread::available_parallelism().unwrap().get()
))
.status()?;
if !status.success() {
return Err(format!("cmake build failed for target {}", target).into());
}
// Install step
let status = Command::new("cmake")
.arg("--install")
.arg(arch_build_dir.to_str().unwrap())
.arg("--config")
.arg("Release")
.arg("--prefix")
.arg(arch_pkg_dir.to_str().unwrap())
.arg("--strip")
.status()?;
if !status.success() {
return Err(format!("cmake install failed for arch {}", target).into());
}
Ok(arch_pkg_dir)
}
fn configure_cxx_bridge(target: &str) -> Result<(), Box<dyn Error>> {
let mut builder = cxx_build::bridge("src/taglib/bridge.rs");
builder
.file("shim/iostream_shim.cpp")
.file("shim/file_shim.cpp")
.file("shim/tk_shim.cpp")
.file("shim/picture_shim.cpp")
.file("shim/xiph_shim.cpp")
.file("shim/id3v1_shim.cpp")
.file("shim/id3v2_shim.cpp")
.file("shim/mp4_shim.cpp")
.include(format!("taglib/pkg/{}/include", target))
.include(".")
.flag_if_supported("-std=c++14");
if target.contains("android") {
builder
.cpp_link_stdlib("c++_static")
// Magic linker flags that statically link exception handling symbols
// to the library.
.flag("-fexceptions")
.flag("-funwind-tables");
}
builder.compile("taglib_cxx_bindings");
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
let working_dir = env::current_dir()?;
let target = env::var("TARGET")?;
let working_dir = Path::new(&working_dir);
let arch_pkg_dir = build_taglib(&target, &working_dir)?;
println!(
"cargo:rustc-link-search=native={}/lib",
arch_pkg_dir.display()
);
println!("cargo:rustc-link-lib=static=tag");
println!("cargo:rerun-if-changed=taglib/");
configure_cxx_bridge(&target)?;
if target.contains("android") {
// Magic linker flags that statically link the C++ runtime
// and exception handling to the library.
println!("cargo:rustc-link-lib=static=c++_static");
println!("cargo:rustc-link-lib=static=c++abi");
println!("cargo:rustc-link-lib=unwind");
}
println!("cargo:rerun-if-changed=shim/");
Ok(())
}

View file

@ -0,0 +1,39 @@
#include "file_shim.hpp"
namespace taglib_shim
{
std::unique_ptr<TagLib::FileRef> new_FileRef(TagLib::IOStream *stream) {
return std::make_unique<TagLib::FileRef>(stream);
}
TagLib::Ogg::Vorbis::File *File_asVorbis(TagLib::File *file)
{
return dynamic_cast<TagLib::Ogg::Vorbis::File *>(file);
}
TagLib::Ogg::Opus::File *File_asOpus(TagLib::File *file)
{
return dynamic_cast<TagLib::Ogg::Opus::File *>(file);
}
TagLib::MPEG::File *File_asMPEG(TagLib::File *file)
{
return dynamic_cast<TagLib::MPEG::File *>(file);
}
TagLib::FLAC::File *File_asFLAC(TagLib::File *file)
{
return dynamic_cast<TagLib::FLAC::File *>(file);
}
TagLib::MP4::File *File_asMP4(TagLib::File *file)
{
return dynamic_cast<TagLib::MP4::File *>(file);
}
TagLib::RIFF::WAV::File *File_asWAV(TagLib::File *file)
{
return dynamic_cast<TagLib::RIFF::WAV::File *>(file);
}
} // namespace taglib_shim

View file

@ -0,0 +1,27 @@
#pragma once
#include <taglib/fileref.h>
#include <taglib/tag.h>
#include <taglib/tstring.h>
#include <taglib/audioproperties.h>
#include <taglib/mpegfile.h>
#include <taglib/flacfile.h>
#include <taglib/mp4file.h>
#include <taglib/oggfile.h>
#include <taglib/opusfile.h>
#include <taglib/wavfile.h>
#include <taglib/wavpackfile.h>
#include <taglib/apefile.h>
#include <taglib/vorbisfile.h>
namespace taglib_shim
{
std::unique_ptr<TagLib::FileRef> new_FileRef(TagLib::IOStream *stream);
TagLib::Ogg::Vorbis::File *File_asVorbis(TagLib::File *file);
TagLib::Ogg::Opus::File *File_asOpus(TagLib::File *file);
TagLib::MPEG::File *File_asMPEG(TagLib::File *file);
TagLib::FLAC::File *File_asFLAC(TagLib::File *file);
TagLib::MP4::File *File_asMP4(TagLib::File *file);
TagLib::RIFF::WAV::File *File_asWAV(TagLib::File *file);
} // namespace taglib_shim

View file

@ -0,0 +1,31 @@
#include "id3v1_shim.hpp"
namespace taglib_shim {
std::unique_ptr<TagLib::String> ID3v1Tag_title(const TagLib::ID3v1::Tag& tag) {
return std::make_unique<TagLib::String>(tag.title());
}
std::unique_ptr<TagLib::String> ID3v1Tag_artist(const TagLib::ID3v1::Tag& tag) {
return std::make_unique<TagLib::String>(tag.artist());
}
std::unique_ptr<TagLib::String> ID3v1Tag_album(const TagLib::ID3v1::Tag& tag) {
return std::make_unique<TagLib::String>(tag.album());
}
std::unique_ptr<TagLib::String> ID3v1Tag_comment(const TagLib::ID3v1::Tag& tag) {
return std::make_unique<TagLib::String>(tag.comment());
}
uint ID3v1Tag_genreIndex(const TagLib::ID3v1::Tag& tag) {
return tag.genreNumber();
}
uint ID3v1Tag_year(const TagLib::ID3v1::Tag& tag) {
return tag.year();
}
uint ID3v1Tag_track(const TagLib::ID3v1::Tag& tag) {
return tag.track();
}
}

View file

@ -0,0 +1,15 @@
#pragma once
#include <memory>
#include <taglib/id3v1tag.h>
#include <taglib/tstring.h>
namespace taglib_shim {
std::unique_ptr<TagLib::String> ID3v1Tag_title(const TagLib::ID3v1::Tag& tag);
std::unique_ptr<TagLib::String> ID3v1Tag_artist(const TagLib::ID3v1::Tag& tag);
std::unique_ptr<TagLib::String> ID3v1Tag_album(const TagLib::ID3v1::Tag& tag);
std::unique_ptr<TagLib::String> ID3v1Tag_comment(const TagLib::ID3v1::Tag& tag);
uint ID3v1Tag_genreIndex(const TagLib::ID3v1::Tag& tag);
uint ID3v1Tag_year(const TagLib::ID3v1::Tag& tag);
uint ID3v1Tag_track(const TagLib::ID3v1::Tag& tag);
}

View file

@ -0,0 +1,48 @@
#include "id3v2_shim.hpp"
namespace taglib_shim {
std::unique_ptr<TagLib::ID3v2::FrameList> Tag_frameList(const TagLib::ID3v2::Tag& tag) {
return std::make_unique<TagLib::ID3v2::FrameList>(tag.frameList());
}
std::unique_ptr<std::vector<FramePointer>> FrameList_to_vector(const TagLib::ID3v2::FrameList& list) {
auto frames = std::make_unique<std::vector<FramePointer>>();
for (const auto& frame : list) {
frames->push_back(FramePointer{frame});
}
return frames;
}
const TagLib::ID3v2::TextIdentificationFrame* Frame_asTextIdentification(const TagLib::ID3v2::Frame* frame) {
return dynamic_cast<const TagLib::ID3v2::TextIdentificationFrame*>(frame);
}
const TagLib::ID3v2::UserTextIdentificationFrame* Frame_asUserTextIdentification(const TagLib::ID3v2::Frame* frame) {
return dynamic_cast<const TagLib::ID3v2::UserTextIdentificationFrame*>(frame);
}
const TagLib::ID3v2::AttachedPictureFrame* Frame_asAttachedPicture(const TagLib::ID3v2::Frame* frame) {
return dynamic_cast<const TagLib::ID3v2::AttachedPictureFrame*>(frame);
}
std::unique_ptr<TagLib::ByteVector> Frame_id(const TagLib::ID3v2::Frame& frame) {
return std::make_unique<TagLib::ByteVector>(frame.frameID());
}
std::unique_ptr<TagLib::ByteVector> AttachedPictureFrame_picture(const TagLib::ID3v2::AttachedPictureFrame& frame) {
return std::make_unique<TagLib::ByteVector>(frame.picture());
}
uint32_t AttachedPictureFrame_type(const TagLib::ID3v2::AttachedPictureFrame& frame) {
return static_cast<uint32_t>(frame.type());
}
std::unique_ptr<TagLib::StringList> TextIdentificationFrame_fieldList(const TagLib::ID3v2::TextIdentificationFrame& frame) {
return std::make_unique<TagLib::StringList>(frame.fieldList());
}
std::unique_ptr<TagLib::StringList> UserTextIdentificationFrame_fieldList(const TagLib::ID3v2::UserTextIdentificationFrame& frame) {
return std::make_unique<TagLib::StringList>(frame.fieldList());
}
}

View file

@ -0,0 +1,36 @@
#pragma once
#include "rust/cxx.h"
#include "taglib/id3v2tag.h"
#include "taglib/id3v2frame.h"
#include "taglib/textidentificationframe.h"
#include "taglib/unsynchronizedlyricsframe.h"
#include "taglib/attachedpictureframe.h"
#include "taglib/tbytevector.h"
#include "taglib/mpegfile.h"
namespace taglib_shim {
struct FramePointer {
const TagLib::ID3v2::Frame* inner;
const TagLib::ID3v2::Frame* get() const { return inner; }
};
std::unique_ptr<TagLib::ID3v2::FrameList> Tag_frameList(const TagLib::ID3v2::Tag& tag);
std::unique_ptr<std::vector<FramePointer>> FrameList_to_vector(const TagLib::ID3v2::FrameList& list);
// Frame type checking and casting
std::unique_ptr<TagLib::ByteVector> Frame_id(const TagLib::ID3v2::Frame &frame);
const TagLib::ID3v2::TextIdentificationFrame* Frame_asTextIdentification(const TagLib::ID3v2::Frame* frame);
const TagLib::ID3v2::UserTextIdentificationFrame* Frame_asUserTextIdentification(const TagLib::ID3v2::Frame* frame);
const TagLib::ID3v2::AttachedPictureFrame* Frame_asAttachedPicture(const TagLib::ID3v2::Frame* frame);
// Frame data access
std::unique_ptr<TagLib::ByteVector> AttachedPictureFrame_picture(const TagLib::ID3v2::AttachedPictureFrame& frame);
uint32_t AttachedPictureFrame_type(const TagLib::ID3v2::AttachedPictureFrame& frame);
std::unique_ptr<TagLib::StringList> TextIdentificationFrame_fieldList(const TagLib::ID3v2::TextIdentificationFrame& frame);
std::unique_ptr<TagLib::StringList> UserTextIdentificationFrame_fieldList(const TagLib::ID3v2::UserTextIdentificationFrame& frame);
// ID3v2 tag access
TagLib::ID3v2::Tag* File_ID3v2Tag(TagLib::MPEG::File* file, bool create);
}

View file

@ -0,0 +1,166 @@
#include "iostream_shim.hpp"
#include <stdexcept>
#include <rust/cxx.h>
#include <vector>
#include "metadatajni/src/taglib/bridge.rs.h"
namespace taglib_shim
{
// C++ implementation of TagLib::IOStream that delegates to Rust
class WrappedRsIOStream : public TagLib::IOStream
{
public:
explicit WrappedRsIOStream(RsIOStream *stream);
~WrappedRsIOStream() override;
// TagLib::IOStream interface implementation
TagLib::FileName name() const override;
TagLib::ByteVector readBlock(size_t length) override;
void writeBlock(const TagLib::ByteVector &data) override;
void insert(const TagLib::ByteVector &data, TagLib::offset_t start = 0, size_t replace = 0) override;
void removeBlock(TagLib::offset_t start = 0, size_t length = 0) override;
void seek(TagLib::offset_t offset, Position p = Beginning) override;
void clear() override;
void truncate(TagLib::offset_t length) override;
TagLib::offset_t tell() const override;
TagLib::offset_t length() override;
bool readOnly() const override;
bool isOpen() const override;
private:
std::string _name;
RsIOStream *rust_stream;
};
WrappedRsIOStream::WrappedRsIOStream(RsIOStream *stream) : rust_stream(stream)
{
_name = std::move(std::string(rust_stream->name()));
}
WrappedRsIOStream::~WrappedRsIOStream() = default;
TagLib::FileName WrappedRsIOStream::name() const
{
return _name.c_str();
}
TagLib::ByteVector WrappedRsIOStream::readBlock(size_t length)
{
std::vector<uint8_t> buffer(length);
size_t bytes_read = rust_stream->read(rust::Slice<uint8_t>(buffer.data(), length));
return TagLib::ByteVector(reinterpret_cast<char *>(buffer.data()), bytes_read);
}
void WrappedRsIOStream::writeBlock(const TagLib::ByteVector &data)
{
rust_stream->write(rust::Slice<const uint8_t>(
reinterpret_cast<const uint8_t *>(data.data()), data.size()));
}
void WrappedRsIOStream::insert(const TagLib::ByteVector &data, TagLib::offset_t start, size_t replace)
{
// Save current position
auto current = tell();
// Seek to insert position
seek(start);
// If replacing, remove that section first
if (replace > 0)
{
removeBlock(start, replace);
}
// Write new data
writeBlock(data);
// Restore position
seek(current);
}
void WrappedRsIOStream::removeBlock(TagLib::offset_t start, size_t length)
{
if (length == 0)
return;
// Save current position
auto current = tell();
// Get file size
auto file_length = this->length();
// Read everything after the removed section
seek(start + length);
auto remaining = readBlock(file_length - (start + length));
// Truncate to start position
seek(start);
truncate(start);
// Write remaining data
writeBlock(remaining);
// Restore position
seek(current);
}
void WrappedRsIOStream::seek(TagLib::offset_t offset, Position p)
{
int32_t whence;
switch (p)
{
case Beginning:
whence = SEEK_SET;
break;
case Current:
whence = SEEK_CUR;
break;
case End:
whence = SEEK_END;
break;
default:
return;
break;
}
rust_stream->seek(offset, whence);
}
void WrappedRsIOStream::clear()
{
truncate(0);
seek(0);
}
void WrappedRsIOStream::truncate(TagLib::offset_t length)
{
rust_stream->truncate(length);
}
TagLib::offset_t WrappedRsIOStream::tell() const
{
return rust_stream->tell();
}
TagLib::offset_t WrappedRsIOStream::length()
{
return rust_stream->length();
}
bool WrappedRsIOStream::readOnly() const
{
return rust_stream->is_readonly();
}
bool WrappedRsIOStream::isOpen() const
{
return true; // If we have a stream, it's open
}
// Factory function to create a new RustIOStream
std::unique_ptr<TagLib::IOStream> wrap_RsIOStream(RsIOStream *stream)
{
return std::unique_ptr<TagLib::IOStream>(new WrappedRsIOStream(stream));
}
} // namespace taglib_shim

View file

@ -0,0 +1,16 @@
#pragma once
#include <memory>
#include <string>
#include <taglib/tiostream.h>
#include <taglib/fileref.h>
#include "rust/cxx.h"
// Forward declare the bridge type
struct RsIOStream;
namespace taglib_shim
{
// Factory functions with external linkage
std::unique_ptr<TagLib::IOStream> wrap_RsIOStream(RsIOStream *stream);
} // namespace taglib_shim

View file

@ -0,0 +1,85 @@
#include "mp4_shim.hpp"
#include <taglib/tstring.h>
#include <memory>
#include <vector>
namespace taglib_shim {
ItemMapEntry::ItemMapEntry(TagLib::String key, TagLib::MP4::Item value)
: key_(std::move(key)), value_(std::move(value)) {}
std::unique_ptr<TagLib::String> ItemMapEntry::key() const {
return std::make_unique<TagLib::String>(key_);
}
std::unique_ptr<TagLib::MP4::Item> ItemMapEntry::value() const {
return std::make_unique<TagLib::MP4::Item>(value_);
}
std::unique_ptr<std::vector<ItemMapEntry>> ItemMap_to_entries(const TagLib::MP4::ItemMap& map) {
auto entries = std::make_unique<std::vector<ItemMapEntry>>();
for (auto it = map.begin(); it != map.end(); ++it) {
entries->emplace_back(it->first, it->second);
}
return entries;
}
IntPair::IntPair(int first, int second)
: first_(first), second_(second) {}
int IntPair::first() const {
return first_;
}
int IntPair::second() const {
return second_;
}
CoverArt::CoverArt(TagLib::MP4::CoverArt::Format format, const TagLib::ByteVector& data)
: art_(format, data) {}
uint32_t CoverArt::format() const {
return static_cast<uint32_t>(art_.format());
}
std::unique_ptr<TagLib::ByteVector> CoverArt::data() const {
return std::make_unique<TagLib::ByteVector>(art_.data());
}
CoverArtList::CoverArtList(const TagLib::MP4::CoverArtList& list)
: list_(list) {}
std::unique_ptr<std::vector<CoverArt>> CoverArtList::to_vector() const {
auto vec = std::make_unique<std::vector<CoverArt>>();
for (const auto& item : list_) {
vec->emplace_back(item.format(), item.data());
}
return vec;
}
unsigned int Item_type(const TagLib::MP4::Item& item) {
return static_cast<unsigned int>(item.type());
}
std::unique_ptr<IntPair> Item_toIntPair(const TagLib::MP4::Item& item) {
auto pair = item.toIntPair();
return std::make_unique<IntPair>(pair.first, pair.second);
}
std::unique_ptr<TagLib::StringList> Item_toStringList(const TagLib::MP4::Item& item) {
return std::make_unique<TagLib::StringList>(item.toStringList());
}
std::unique_ptr<TagLib::ByteVectorList> Item_toByteVectorList(const TagLib::MP4::Item& item) {
return std::make_unique<TagLib::ByteVectorList>(item.toByteVectorList());
}
std::unique_ptr<CoverArtList> Item_toCoverArtList(const TagLib::MP4::Item& item) {
return std::make_unique<CoverArtList>(item.toCoverArtList());
}
int64_t Item_toLongLong(const TagLib::MP4::Item& item) {
return item.toLongLong();
}
}

View file

@ -0,0 +1,59 @@
#pragma once
#include <memory>
#include <taglib/mp4tag.h>
#include <taglib/mp4item.h>
#include <taglib/tstring.h>
#include "rust/cxx.h"
namespace taglib_shim {
class ItemMapEntry {
public:
ItemMapEntry(TagLib::String key, TagLib::MP4::Item value);
std::unique_ptr<TagLib::String> key() const;
std::unique_ptr<TagLib::MP4::Item> value() const;
private:
TagLib::String key_;
TagLib::MP4::Item value_;
};
std::unique_ptr<std::vector<ItemMapEntry>> ItemMap_to_entries(const TagLib::MP4::ItemMap& map);
class IntPair {
public:
IntPair(int first, int second);
int first() const;
int second() const;
private:
int first_;
int second_;
};
class CoverArt {
public:
CoverArt(TagLib::MP4::CoverArt::Format format, const TagLib::ByteVector& data);
uint32_t format() const;
std::unique_ptr<TagLib::ByteVector> data() const;
private:
TagLib::MP4::CoverArt art_;
};
class CoverArtList {
public:
CoverArtList(const TagLib::MP4::CoverArtList& list);
std::unique_ptr<std::vector<CoverArt>> to_vector() const;
private:
TagLib::MP4::CoverArtList list_;
};
unsigned int Item_type(const TagLib::MP4::Item& item);
std::unique_ptr<IntPair> Item_toIntPair(const TagLib::MP4::Item& item);
std::unique_ptr<TagLib::StringList> Item_toStringList(const TagLib::MP4::Item& item);
std::unique_ptr<TagLib::ByteVectorList> Item_toByteVectorList(const TagLib::MP4::Item& item);
std::unique_ptr<CoverArtList> Item_toCoverArtList(const TagLib::MP4::Item& item);
int64_t Item_toLongLong(const TagLib::MP4::Item& item);
}

View file

@ -0,0 +1,36 @@
#include "picture_shim.hpp"
#include "taglib/flacfile.h"
namespace taglib_shim {
std::unique_ptr<PictureList> XiphComment_pictureList(TagLib::Ogg::XiphComment& comment) {
return std::make_unique<PictureList>(comment.pictureList());
}
std::unique_ptr<PictureList> FLACFile_pictureList(TagLib::FLAC::File& file) {
return std::make_unique<PictureList>(file.pictureList());
}
std::unique_ptr<std::vector<PicturePointer>> PictureList_to_vector(const PictureList& list) {
auto result = std::make_unique<std::vector<PicturePointer>>();
for (const auto* picture : list) {
result->emplace_back(picture);
}
return result;
}
std::unique_ptr<TagLib::String> Picture_mimeType(const TagLib::FLAC::Picture& picture) {
return std::make_unique<TagLib::String>(picture.mimeType());
}
std::unique_ptr<TagLib::String> Picture_description(const TagLib::FLAC::Picture& picture) {
return std::make_unique<TagLib::String>(picture.description());
}
std::unique_ptr<TagLib::ByteVector> Picture_data(const TagLib::FLAC::Picture& picture) {
return std::make_unique<TagLib::ByteVector>(picture.data());
}
uint32_t Picture_type(const TagLib::FLAC::Picture& picture) {
return static_cast<uint32_t>(picture.type());
}
}

View file

@ -0,0 +1,28 @@
#pragma once
#include "taglib/flacpicture.h"
#include "taglib/tstring.h"
#include "taglib/tbytevector.h"
#include "taglib/tpicturetype.h"
#include "tk_shim.hpp"
#include <memory>
#include <vector>
namespace taglib_shim {
using PictureList = TagLib::List<TagLib::FLAC::Picture *>;
class PicturePointer {
public:
PicturePointer(const TagLib::FLAC::Picture* picture) : picture(picture) {}
const TagLib::FLAC::Picture* get() const { return picture; }
private:
const TagLib::FLAC::Picture* picture;
};
std::unique_ptr<PictureList> FLACFile_pictureList(TagLib::FLAC::File& file);
std::unique_ptr<PictureList> XiphComment_pictureList(TagLib::Ogg::XiphComment& comment);
std::unique_ptr<std::vector<PicturePointer>> PictureList_to_vector(const PictureList& list);
uint32_t Picture_type(const TagLib::FLAC::Picture& picture);
std::unique_ptr<TagLib::ByteVector> Picture_data(const TagLib::FLAC::Picture& picture);
}

View file

@ -0,0 +1,45 @@
#include "tk_shim.hpp"
namespace taglib_shim
{
Property::Property(TagLib::String key, TagLib::StringList value) : key_(key), value_(value) {}
const TagLib::String &Property::key() const
{
return key_;
}
const TagLib::StringList &Property::value() const
{
return value_;
}
std::unique_ptr<std::vector<Property>> SimplePropertyMap_to_vector(const TagLib::SimplePropertyMap &map)
{
std::unique_ptr<std::vector<Property>> result = std::make_unique<std::vector<Property>>();
for (const auto &pair : map)
{
result->push_back(Property(pair.first, pair.second));
}
return result;
}
std::unique_ptr<std::vector<TagLib::String>> StringList_to_vector(const TagLib::StringList &list)
{
std::unique_ptr<std::vector<TagLib::String>> result = std::make_unique<std::vector<TagLib::String>>();
for (const auto &str : list)
{
result->push_back(str);
}
return result;
}
std::unique_ptr<std::vector<TagLib::ByteVector>> ByteVectorList_to_vector(const TagLib::ByteVectorList &list)
{
std::unique_ptr<std::vector<TagLib::ByteVector>> result = std::make_unique<std::vector<TagLib::ByteVector>>();
for (const auto &vec : list)
{
result->push_back(vec);
}
return result;
}
}

View file

@ -0,0 +1,35 @@
#pragma once
#include "taglib/tpropertymap.h"
#include "taglib/xiphcomment.h"
#include "taglib/tstring.h"
#include "taglib/tstringlist.h"
#include "taglib/flacpicture.h"
#include "taglib/flacfile.h"
#include "taglib/tbytevector.h"
#include <memory>
#include <iterator>
#include <vector>
#include <string>
#include "rust/cxx.h"
namespace taglib_shim
{
struct Property
{
Property(TagLib::String key, TagLib::StringList value);
const TagLib::String &key() const;
const TagLib::StringList &value() const;
private:
TagLib::String key_;
TagLib::StringList value_;
};
std::unique_ptr<std::vector<Property>> SimplePropertyMap_to_vector(const TagLib::SimplePropertyMap &map);
std::unique_ptr<std::vector<TagLib::String>> StringList_to_vector(const TagLib::StringList &list);
std::unique_ptr<std::vector<TagLib::ByteVector>> ByteVectorList_to_vector(const TagLib::ByteVectorList &list);
}

View file

@ -0,0 +1,26 @@
#include "xiph_shim.hpp"
namespace taglib_shim
{
FieldListEntry::FieldListEntry(TagLib::String key, TagLib::StringList value) : key_(key), value_(value) {}
std::unique_ptr<TagLib::String> FieldListEntry::key() const
{
return std::make_unique<TagLib::String>(key_);
}
std::unique_ptr<TagLib::StringList> FieldListEntry::value() const
{
return std::make_unique<TagLib::StringList>(value_);
}
std::unique_ptr<std::vector<FieldListEntry>> FieldListMap_to_entries(const TagLib::SimplePropertyMap &map)
{
std::unique_ptr<std::vector<FieldListEntry>> result = std::make_unique<std::vector<FieldListEntry>>();
for (const auto &pair : map)
{
result->push_back(FieldListEntry(pair.first, pair.second));
}
return result;
}
}

View file

@ -0,0 +1,22 @@
#include "taglib/tpropertymap.h"
#include "taglib/xiphcomment.h"
namespace taglib_shim
{
using FieldListMap = TagLib::SimplePropertyMap;
struct FieldListEntry
{
FieldListEntry(TagLib::String key, TagLib::StringList value);
std::unique_ptr<TagLib::String> key() const;
std::unique_ptr<TagLib::StringList> value() const;
private:
TagLib::String key_;
TagLib::StringList value_;
};
std::unique_ptr<std::vector<FieldListEntry>> FieldListMap_to_entries(const FieldListMap &map);
std::unique_ptr<std::vector<TagLib::String>> StringList_to_vector(const TagLib::StringList &list);
}

View file

@ -0,0 +1,249 @@
use jni::{
objects::{JObject, JValueGen},
sys::jlong,
JNIEnv,
};
use std::cell::RefCell;
use std::rc::Rc;
use crate::taglib::{
audioproperties,
flac::PictureType,
id3v1, id3v2, mp4,
xiph::{self, FLACPictureList},
};
use crate::jtagmap::JTagMap;
pub struct JMetadataBuilder<'local, 'file_ref> {
env: Rc<RefCell<JNIEnv<'local>>>,
id3v2: JTagMap<'local>,
xiph: JTagMap<'local>,
mp4: JTagMap<'local>,
cover: Option<Vec<u8>>,
properties: Option<audioproperties::AudioProperties<'file_ref>>,
mime_type: Option<String>,
}
impl<'local, 'file_ref> JMetadataBuilder<'local, 'file_ref> {
pub fn new(env: Rc<RefCell<JNIEnv<'local>>>) -> Self {
Self {
id3v2: JTagMap::new(env.clone()),
xiph: JTagMap::new(env.clone()),
mp4: JTagMap::new(env.clone()),
env,
cover: None,
properties: None,
mime_type: None,
}
}
pub fn set_mime_type(&mut self, mime_type: impl Into<String>) {
self.mime_type = Some(mime_type.into());
}
pub fn set_id3v1(&mut self, tag: &id3v1::ID3v1Tag) {
self.id3v2.add_id("TIT2", tag.title().to_string());
self.id3v2.add_id("TPE1", tag.artist().to_string());
self.id3v2.add_id("TALB", tag.album().to_string());
self.id3v2.add_id("TRCK", tag.track().to_string());
self.id3v2.add_id("TYER", tag.year().to_string());
let genre = tag.genre_index();
if genre != 255 {
self.id3v2.add_id("TCON", genre.to_string());
}
}
pub fn set_id3v2(&mut self, tag: &id3v2::ID3v2Tag) {
let mut first_pic = None;
let mut front_cover_pic = None;
for mut frame in tag.frames().to_vec() {
if let Some(user_text_frame) = frame.as_user_text_identification() {
let values = user_text_frame.values();
let mut values = values.to_vec();
if !values.is_empty() {
let description = values.remove(0).to_string();
let remaining: Vec<String> = values.into_iter().map(|s| s.to_string()).collect();
self.id3v2.add_combined_list(frame.id().to_string_lossy(), description, remaining);
}
} else if let Some(text_frame) = frame.as_text_identification() {
let field_list = text_frame.field_list();
let values: Vec<String> = field_list
.to_vec()
.into_iter()
.map(|s| s.to_string())
.collect();
self.id3v2.add_id_list(frame.id().to_string_lossy(), values);
} else if let Some(picture_frame) = frame.as_attached_picture() {
if first_pic.is_none() {
first_pic = Some(picture_frame.picture().to_vec());
}
if let (Some(PictureType::FrontCover), None) =
(picture_frame.picture_type(), &front_cover_pic)
{
front_cover_pic = Some(picture_frame.picture().to_vec());
}
}
}
// Prefer front cover, fall back to first picture
self.cover = front_cover_pic.or(first_pic);
}
pub fn set_xiph(&mut self, tag: &mut xiph::XiphComment<'file_ref>) {
for (key, values) in tag.field_list_map().to_hashmap() {
let values: Vec<String> = values.to_vec().into_iter().map(|s| s.to_string()).collect();
self.xiph.add_custom_list(key.to_uppercase(), values);
}
self.set_flac_pictures(&tag.picture_list());
}
pub fn set_mp4(&mut self, tag: &mp4::MP4Tag) {
let map = tag.item_map().to_hashmap();
for (key, item) in map {
if key == "covr" {
if let Some(mp4::MP4Data::CoverArtList(cover_list)) = item.data() {
let covers = cover_list.to_vec();
// Prefer PNG/JPEG covers
let preferred_cover = covers.iter().find(|c| {
matches!(
c.format(),
mp4::CoverArtFormat::PNG | mp4::CoverArtFormat::JPEG
)
});
if let Some(cover) = preferred_cover {
self.cover = Some(cover.data().to_vec());
} else if let Some(first_cover) = covers.first() {
self.cover = Some(first_cover.data().to_vec());
}
continue;
}
}
if let Some(data) = item.data() {
match data {
mp4::MP4Data::StringList(list) => {
let values: Vec<String> =
list.to_vec().into_iter().map(|s| s.to_string()).collect();
if key.starts_with("----") {
if let Some(split_idx) = key.find(':') {
let (atom_name, atom_desc) = key.split_at(split_idx);
let atom_desc = &atom_desc[1..]; // Skip the colon
for value in values {
self.mp4.add_combined(atom_name, atom_desc, value);
}
}
} else {
self.mp4.add_id_list(key, values);
}
}
mp4::MP4Data::Int(v) => self.mp4.add_id(key, v.to_string()),
mp4::MP4Data::UInt(v) => self.mp4.add_id(key, v.to_string()),
mp4::MP4Data::LongLong(v) => self.mp4.add_id(key, v.to_string()),
mp4::MP4Data::IntPair(pair) => {
if let Some((first, second)) = pair.to_tuple() {
self.mp4.add_id(key, format!("{}/{}", first, second));
}
}
_ => continue,
}
}
}
}
pub fn set_flac_pictures(&mut self, pictures: &FLACPictureList<'file_ref>) {
for picture in pictures.to_vec().into_iter() {
if let Some(PictureType::FrontCover) = picture.picture_type() {
self.cover = Some(picture.data().to_vec());
}
}
}
pub fn set_properties(&mut self, properties: audioproperties::AudioProperties<'file_ref>) {
self.properties = Some(properties);
}
pub fn build(&self) -> JObject {
// Create Properties object
let properties_class = self
.env
.borrow_mut()
.find_class("org/oxycblt/musikr/metadata/Properties")
.unwrap();
let properties = if let Some(props) = &self.properties {
let mime_type = self.mime_type.as_deref().unwrap_or("").to_string();
let j_mime_type = self.env.borrow().new_string(mime_type).unwrap();
self.env
.borrow_mut()
.new_object(
properties_class,
"(Ljava/lang/String;JII)V",
&[
JValueGen::from(&j_mime_type),
(props.length_in_milliseconds() as jlong).into(),
(props.bitrate() as i32).into(),
(props.sample_rate() as i32).into(),
],
)
.unwrap()
} else {
let empty_mime = self.env.borrow().new_string("").unwrap();
self.env
.borrow_mut()
.new_object(
properties_class,
"(Ljava/lang/String;JII)V",
&[
JValueGen::from(&empty_mime),
0i64.into(),
0i32.into(),
0i32.into(),
],
)
.unwrap()
};
// Create cover byte array if present
let cover_array = if let Some(cover_data) = &self.cover {
let array = self
.env
.borrow()
.new_byte_array(cover_data.len() as i32)
.unwrap();
self.env
.borrow()
.set_byte_array_region(&array, 0, bytemuck::cast_slice(cover_data))
.unwrap();
array.into()
} else {
JObject::null()
};
// Create Metadata object
let metadata_class = self
.env
.borrow_mut()
.find_class("org/oxycblt/musikr/metadata/Metadata")
.unwrap();
// Get the objects first since they need the env borrow first
let id3v2 = self.id3v2.get_object();
let xiph = self.xiph.get_object();
let mp4 = self.mp4.get_object();
self.env.borrow_mut().new_object(
metadata_class,
"(Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;[BLorg/oxycblt/musikr/metadata/Properties;)V",
&[
JValueGen::from(&id3v2),
JValueGen::from(&xiph),
JValueGen::from(&mp4),
JValueGen::from(&cover_array),
JValueGen::from(&properties),
],
).unwrap()
}
}

View file

@ -0,0 +1,117 @@
use crate::taglib::iostream::IOStream;
use crate::SharedEnv;
use jni::objects::{JObject, JValue};
use std::io::SeekFrom;
pub struct JInputStream<'local> {
env: SharedEnv<'local>,
input: &'local JObject<'local>,
}
impl<'local, 'a> JInputStream<'local> {
pub fn new(env: SharedEnv<'local>, input: &'local JObject<'local>) -> Self {
Self { env, input }
}
}
impl<'local> IOStream for JInputStream<'local> {
fn read_block(&mut self, buf: &mut [u8]) -> usize {
// Create a direct ByteBuffer from the Rust slice
let byte_buffer = unsafe {
self.env
.borrow_mut()
.new_direct_byte_buffer(buf.as_mut_ptr(), buf.len())
.expect("Failed to create ByteBuffer")
};
// Call readBlock safely
let success = self
.env
.borrow_mut()
.call_method(
&self.input,
"readBlock",
"(Ljava/nio/ByteBuffer;)Z",
&[JValue::Object(&byte_buffer)],
)
.and_then(|result| result.z())
.expect("Failed to call readBlock");
if !success {
return 0;
}
buf.len()
}
fn write_block(&mut self, _data: &[u8]) {
panic!("JInputStream is read-only");
}
fn seek(&mut self, pos: SeekFrom) {
let (method, offset) = match pos {
SeekFrom::Start(offset) => ("seekFromBeginning", offset as i64),
SeekFrom::Current(offset) => ("seekFromCurrent", offset),
SeekFrom::End(offset) => ("seekFromEnd", offset),
};
// Call the appropriate seek method safely
let success = self
.env
.borrow_mut()
.call_method(&self.input, method, "(J)Z", &[JValue::Long(offset)])
.and_then(|result| result.z())
.expect("Failed to seek");
if !success {
panic!("Failed to seek");
}
}
fn truncate(&mut self, _length: i64) {
panic!("JInputStream is read-only");
}
fn tell(&self) -> i64 {
let position = self
.env
.borrow_mut()
.call_method(&self.input, "tell", "()J", &[])
.and_then(|result| result.j())
.expect("Failed to get position");
if position == i64::MIN {
panic!("Failed to get position");
}
position
}
fn length(&self) -> i64 {
self.env
.borrow_mut()
.call_method(&self.input, "length", "()J", &[])
.and_then(|result| result.j())
.expect("Failed to get length")
}
fn name(&self) -> String {
// Call the Java name() method safely
let name = self
.env
.borrow_mut()
.call_method(&self.input, "name", "()Ljava/lang/String;", &[])
.and_then(|result| result.l())
.expect("Failed to call name() method");
self.env
.borrow_mut()
.get_string(&name.into())
.expect("Failed to convert Java string")
.into()
}
fn is_readonly(&self) -> bool {
true // JInputStream is always read-only
}
}

View file

@ -0,0 +1,182 @@
use jni::{
objects::{JClass, JObject, JString, JValueGen},
JNIEnv,
};
use std::cell::RefCell;
use std::rc::Rc;
pub struct JTagMap<'local> {
env: Rc<RefCell<JNIEnv<'local>>>,
tag_map: JObject<'local>,
array_list_class: JClass<'local>,
}
impl<'local> JTagMap<'local> {
pub fn new(env: Rc<RefCell<JNIEnv<'local>>>) -> Self {
// Get NativeTagMap class and create instance
let tag_map_class = env
.borrow_mut()
.find_class("org/oxycblt/musikr/metadata/NativeTagMap")
.unwrap();
let tag_map = env
.borrow_mut()
.new_object(&tag_map_class, "()V", &[])
.unwrap();
// Get ArrayList class
let array_list_class = env.borrow_mut().find_class("java/util/ArrayList").unwrap();
Self {
env,
tag_map,
array_list_class,
}
}
fn create_array_list(&self, values: &[String]) -> JObject<'local> {
let mut env = self.env.borrow_mut();
let array_list = env.new_object(&self.array_list_class, "()V", &[]).unwrap();
// Create all JString values first
let j_values: Vec<JString> = values
.iter()
.map(|value| env.new_string(value).unwrap())
.collect();
// Then add them to the ArrayList
for j_value in j_values {
env.call_method(
&array_list,
"add",
"(Ljava/lang/Object;)Z",
&[JValueGen::Object(&j_value)],
)
.unwrap();
}
array_list
}
pub fn add_id(&self, id: impl Into<String>, value: impl Into<String>) {
let mut env = self.env.borrow_mut();
let j_id = env.new_string(id.into()).unwrap();
let j_value = env.new_string(value.into()).unwrap();
env.call_method(
&self.tag_map,
"addID",
"(Ljava/lang/String;Ljava/lang/String;)V",
&[JValueGen::Object(&j_id), JValueGen::Object(&j_value)],
)
.unwrap();
}
pub fn add_id_list(&self, id: impl Into<String>, values: Vec<String>) {
// Create array list first while holding the borrow
let j_values = self.create_array_list(&values);
// Then create the id and make the call with a new borrow
let mut env = self.env.borrow_mut();
let j_id = env.new_string(id.into()).unwrap();
env.call_method(
&self.tag_map,
"addID",
"(Ljava/lang/String;Ljava/util/List;)V",
&[JValueGen::Object(&j_id), JValueGen::Object(&j_values)],
)
.unwrap();
}
pub fn add_custom(&self, description: impl Into<String>, value: impl Into<String>) {
let mut env = self.env.borrow_mut();
let j_description = env.new_string(description.into()).unwrap();
let j_value = env.new_string(value.into()).unwrap();
env.call_method(
&self.tag_map,
"addCustom",
"(Ljava/lang/String;Ljava/lang/String;)V",
&[
JValueGen::Object(&j_description),
JValueGen::Object(&j_value),
],
)
.unwrap();
}
pub fn add_custom_list(&self, description: impl Into<String>, values: Vec<String>) {
let j_values = self.create_array_list(&values);
let mut env = self.env.borrow_mut();
let j_description = env.new_string(description.into()).unwrap();
env.call_method(
&self.tag_map,
"addCustom",
"(Ljava/lang/String;Ljava/util/List;)V",
&[
JValueGen::Object(&j_description),
JValueGen::Object(&j_values),
],
)
.unwrap();
}
pub fn add_combined(
&self,
id: impl Into<String>,
description: impl Into<String>,
value: impl Into<String>,
) {
let mut env = self.env.borrow_mut();
let j_id = env.new_string(id.into()).unwrap();
let j_description = env.new_string(description.into()).unwrap();
let j_value = env.new_string(value.into()).unwrap();
env.call_method(
&self.tag_map,
"addCombined",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
&[
JValueGen::Object(&j_id),
JValueGen::Object(&j_description),
JValueGen::Object(&j_value),
],
)
.unwrap();
}
pub fn add_combined_list(
&self,
id: impl Into<String>,
description: impl Into<String>,
values: Vec<String>,
) {
let j_values = self.create_array_list(&values);
let mut env = self.env.borrow_mut();
let j_id = env.new_string(id.into()).unwrap();
let j_description = env.new_string(description.into()).unwrap();
env.call_method(
&self.tag_map,
"addCombined",
"(Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V",
&[
JValueGen::Object(&j_id),
JValueGen::Object(&j_description),
JValueGen::Object(&j_values),
],
)
.unwrap();
}
pub fn get_object(&self) -> JObject<'local> {
let mut env = self.env.borrow_mut();
env.call_method(&self.tag_map, "getObject", "()Ljava/util/Map;", &[])
.unwrap()
.l()
.unwrap()
}
}

View file

@ -0,0 +1,125 @@
extern crate link_cplusplus;
use android_logger::Config;
use jni::objects::{JClass, JObject};
use jni::sys::jobject;
use jni::JNIEnv;
use log::LevelFilter;
use std::cell::RefCell;
use std::panic;
use std::rc::Rc;
mod jbuilder;
mod jstream;
mod jtagmap;
mod taglib;
use jbuilder::JMetadataBuilder;
use jstream::JInputStream;
use taglib::file_ref::FileRef;
type SharedEnv<'local> = Rc<RefCell<JNIEnv<'local>>>;
// Initialize the logger and panic hook when the library is loaded
#[ctor::ctor]
fn init() {
// Initialize Android logger
android_logger::init_once(
Config::default()
.with_max_level(LevelFilter::Error)
.with_tag("musikr"),
);
// Set custom panic hook
panic::set_hook(Box::new(|panic_info| {
let message = if let Some(s) = panic_info.payload().downcast_ref::<String>() {
s.clone()
} else if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
s.to_string()
} else {
"Unknown panic message".to_string()
};
let location = if let Some(location) = panic_info.location() {
format!(
"{}:{}:{}",
location.file(),
location.line(),
location.column()
)
} else {
"Unknown location".to_string()
};
log::error!(target: "musikr", "Panic occurred at {}: {}", location, message);
}));
}
#[no_mangle]
pub extern "C" fn Java_org_oxycblt_musikr_metadata_MetadataJNI_openFile<'local>(
env: JNIEnv<'local>,
_class: JClass<'local>,
input: JObject<'local>,
) -> jobject {
// Create JInputStream from the Java input stream
let shared_env = Rc::new(RefCell::new(env));
let stream = JInputStream::new(shared_env.clone(), &input);
let file_ref = FileRef::new(stream);
let file = file_ref.file();
let mut jbuilder = JMetadataBuilder::new(shared_env.clone());
match file {
Some(mut file) => {
if let Some(properties) = file.audio_properties() {
jbuilder.set_properties(properties);
}
if let Some(vorbis) = file.as_vorbis() {
jbuilder.set_mime_type("audio/ogg");
if let Some(mut tag) = vorbis.xiph_comments() {
jbuilder.set_xiph(&mut tag);
}
}
if let Some(opus) = file.as_opus() {
jbuilder.set_mime_type("audio/opus");
if let Some(mut tag) = opus.xiph_comments() {
jbuilder.set_xiph(&mut tag);
}
}
if let Some(mut flac) = file.as_flac() {
jbuilder.set_mime_type("audio/flac");
if let Some(tag) = flac.id3v1_tag() {
jbuilder.set_id3v1(&tag);
}
if let Some(tag) = flac.id3v2_tag() {
jbuilder.set_id3v2(&tag);
}
if let Some(mut tag) = flac.xiph_comments() {
jbuilder.set_xiph(&mut tag);
}
jbuilder.set_flac_pictures(&flac.picture_list());
}
if let Some(mut mpeg) = file.as_mpeg() {
jbuilder.set_mime_type("audio/mpeg");
if let Some(tag) = mpeg.id3v1_tag() {
jbuilder.set_id3v1(&tag);
}
if let Some(tag) = mpeg.id3v2_tag() {
jbuilder.set_id3v2(&tag);
}
}
if let Some(mp4) = file.as_mp4() {
jbuilder.set_mime_type("audio/mp4");
if let Some(tag) = mp4.tag() {
jbuilder.set_mp4(&tag);
}
}
if let Some(mut wav) = file.as_wav() {
jbuilder.set_mime_type("audio/wav");
if let Some(tag) = wav.id3v2_tag() {
jbuilder.set_id3v2(&tag);
}
}
}
None => {}
}
let metadata = jbuilder.build();
metadata.into_raw()
}

View file

@ -0,0 +1,28 @@
use super::bridge::CppAudioProperties;
use super::this::RefThis;
pub struct AudioProperties<'file_ref> {
this: RefThis<'file_ref, CppAudioProperties>,
}
impl<'file_ref> AudioProperties<'file_ref> {
pub(super) fn new(this: RefThis<'file_ref, CppAudioProperties>) -> Self {
Self { this }
}
pub fn length_in_milliseconds(&self) -> i32 {
self.this.as_ref().lengthInMilliseconds()
}
pub fn bitrate(&self) -> i32 {
self.this.as_ref().bitrate()
}
pub fn sample_rate(&self) -> i32 {
self.this.as_ref().sampleRate()
}
pub fn channels(&self) -> i32 {
self.this.as_ref().channels()
}
}

View file

@ -0,0 +1,497 @@
use super::iostream::DynIOStream;
pub trait TagLibAllocated {}
pub trait TagLibRef {}
pub trait TagLibShared {}
#[cxx::bridge]
mod bridge_impl {
// Expose Rust IOStream to C++
extern "Rust" {
#[cxx_name = "RsIOStream"]
type DynIOStream<'io_stream>;
fn name(self: &DynIOStream<'_>) -> String;
fn read(self: &mut DynIOStream<'_>, buffer: &mut [u8]) -> usize;
fn write(self: &mut DynIOStream<'_>, data: &[u8]);
fn seek(self: &mut DynIOStream<'_>, offset: i64, whence: i32);
fn truncate(self: &mut DynIOStream<'_>, length: i64);
fn tell(self: &DynIOStream<'_>) -> i64;
fn length(self: &DynIOStream<'_>) -> i64;
fn is_readonly(self: &DynIOStream<'_>) -> bool;
}
#[namespace = "taglib_shim"]
unsafe extern "C++" {
include!("taglib/taglib.h");
include!("taglib/tstring.h");
include!("taglib/tstringlist.h");
include!("taglib/vorbisfile.h");
include!("taglib/xiphcomment.h");
include!("taglib/tiostream.h");
include!("taglib/flacpicture.h");
include!("taglib/tbytevector.h");
include!("taglib/mp4file.h");
include!("taglib/mp4tag.h");
include!("taglib/mp4item.h");
include!("shim/iostream_shim.hpp");
include!("shim/file_shim.hpp");
include!("shim/tk_shim.hpp");
include!("shim/picture_shim.hpp");
include!("shim/xiph_shim.hpp");
include!("shim/id3v2_shim.hpp");
include!("shim/id3v1_shim.hpp");
include!("shim/mp4_shim.hpp");
include!("taglib/mpegfile.h");
// CORE
#[namespace = "TagLib"]
#[cxx_name = "IOStream"]
type CPPIOStream<'io_stream>;
unsafe fn wrap_RsIOStream<'io_stream>(
stream: *mut DynIOStream<'io_stream>,
) -> UniquePtr<CPPIOStream<'io_stream>>;
#[namespace = "TagLib"]
#[cxx_name = "FileRef"]
type CPPFileRef;
unsafe fn new_FileRef(stream: *mut CPPIOStream) -> UniquePtr<CPPFileRef>;
fn isNull(self: &CPPFileRef) -> bool;
fn file(self: &CPPFileRef) -> *mut CPPFile;
#[namespace = "TagLib"]
#[cxx_name = "File"]
type CPPFile;
fn audioProperties(self: &CPPFile) -> *mut CppAudioProperties;
#[namespace = "taglib_shim"]
unsafe fn File_asVorbis(file: *mut CPPFile) -> *mut CPPVorbisFile;
#[namespace = "taglib_shim"]
unsafe fn File_asOpus(file: *mut CPPFile) -> *mut CPPOpusFile;
#[namespace = "taglib_shim"]
unsafe fn File_asMPEG(file: *mut CPPFile) -> *mut CPPMPEGFile;
#[namespace = "taglib_shim"]
unsafe fn File_asFLAC(file: *mut CPPFile) -> *mut CPPFLACFile;
#[namespace = "taglib_shim"]
unsafe fn File_asMP4(file: *mut CPPFile) -> *mut CPPMP4File;
#[namespace = "taglib_shim"]
unsafe fn File_asWAV(file: *mut CPPFile) -> *mut CPPWAVFile;
#[namespace = "TagLib"]
#[cxx_name = "AudioProperties"]
type CppAudioProperties;
fn lengthInMilliseconds(self: &CppAudioProperties) -> i32;
fn bitrate(self: &CppAudioProperties) -> i32;
fn sampleRate(self: &CppAudioProperties) -> i32;
fn channels(self: &CppAudioProperties) -> i32;
// XIPH
#[namespace = "TagLib::Ogg::Vorbis"]
#[cxx_name = "File"]
type CPPVorbisFile;
#[cxx_name = "tag"]
fn vorbisTag(self: &CPPVorbisFile) -> *mut CPPXiphComment;
#[namespace = "TagLib::Ogg::Opus"]
#[cxx_name = "File"]
type CPPOpusFile;
#[cxx_name = "tag"]
fn opusTag(self: &CPPOpusFile) -> *mut CPPXiphComment;
#[namespace = "TagLib::FLAC"]
#[cxx_name = "File"]
type CPPFLACFile;
fn xiphComment(self: Pin<&mut CPPFLACFile>, create: bool) -> *mut CPPXiphComment;
#[cxx_name = "ID3v1Tag"]
fn FLACID3v1Tag(self: Pin<&mut CPPFLACFile>, create: bool) -> *mut CPPID3v1Tag;
#[cxx_name = "ID3v2Tag"]
fn FLACID3v2Tag(self: Pin<&mut CPPFLACFile>, create: bool) -> *mut CPPID3v2Tag;
#[namespace = "taglib_shim"]
fn FLACFile_pictureList(file: Pin<&mut CPPFLACFile>) -> UniquePtr<CPPPictureList>;
#[namespace = "taglib_shim"]
#[cxx_name = "PictureList"]
type CPPPictureList;
#[namespace = "taglib_shim"]
fn PictureList_to_vector(
list: &CPPPictureList,
) -> UniquePtr<CxxVector<CPPFLACPicturePointer>>;
#[namespace = "taglib_shim"]
#[cxx_name = "PicturePointer"]
type CPPFLACPicturePointer;
fn get(self: &CPPFLACPicturePointer) -> *const CPPFLACPicture;
#[namespace = "TagLib::FLAC"]
#[cxx_name = "Picture"]
type CPPFLACPicture;
#[namespace = "taglib_shim"]
fn Picture_type(picture: &CPPFLACPicture) -> u32;
#[namespace = "taglib_shim"]
fn Picture_data(picture: &CPPFLACPicture) -> UniquePtr<CPPByteVector>;
// XIPHComment
#[namespace = "TagLib::Ogg"]
#[cxx_name = "XiphComment"]
type CPPXiphComment;
// Explicit lifecycle definition to state while the Pin is temporary, the CPPFieldListMap
// ref returned actually has the same lifetime as the CPPXiphComment.
fn fieldListMap(self: &CPPXiphComment) -> &CPPFieldListMap;
#[namespace = "taglib_shim"]
fn XiphComment_pictureList(comment: Pin<&mut CPPXiphComment>) -> UniquePtr<CPPPictureList>;
#[namespace = "TagLib"]
#[cxx_name = "SimplePropertyMap"]
type CPPFieldListMap;
#[namespace = "taglib_shim"]
fn FieldListMap_to_entries(
field_list_map: &CPPFieldListMap,
) -> UniquePtr<CxxVector<CPPFieldListEntry>>;
#[namespace = "taglib_shim"]
#[cxx_name = "FieldListEntry"]
type CPPFieldListEntry;
fn key(self: &CPPFieldListEntry) -> UniquePtr<CPPString>;
fn value(self: &CPPFieldListEntry) -> UniquePtr<CPPStringList>;
// MPEG
#[namespace = "TagLib::MPEG"]
#[cxx_name = "File"]
type CPPMPEGFile;
#[cxx_name = "ID3v1Tag"]
fn MPEGID3v1Tag(self: Pin<&mut CPPMPEGFile>, create: bool) -> *mut CPPID3v1Tag;
#[cxx_name = "ID3v2Tag"]
fn MPEGID3v2Tag(self: Pin<&mut CPPMPEGFile>, create: bool) -> *mut CPPID3v2Tag;
// MP4
#[namespace = "TagLib::MP4"]
#[cxx_name = "File"]
type CPPMP4File;
#[cxx_name = "tag"]
fn MP4Tag(self: &CPPMP4File) -> *mut CPPMP4Tag;
#[namespace = "TagLib::MP4"]
#[cxx_name = "Tag"]
type CPPMP4Tag;
#[namespace = "TagLib::MP4"]
#[cxx_name = "ItemMap"]
type CPPItemMap;
fn itemMap(self: &CPPMP4Tag) -> &CPPItemMap;
fn ItemMap_to_entries(map: &CPPItemMap) -> UniquePtr<CxxVector<CPPItemMapEntry>>;
#[namespace = "taglib_shim"]
#[cxx_name = "ItemMapEntry"]
type CPPItemMapEntry;
fn key(self: &CPPItemMapEntry) -> UniquePtr<CPPString>;
fn value(self: &CPPItemMapEntry) -> UniquePtr<CPPMP4Item>;
#[namespace = "TagLib::MP4"]
#[cxx_name = "Item"]
type CPPMP4Item;
fn isValid(self: &CPPMP4Item) -> bool;
fn toBool(self: &CPPMP4Item) -> bool;
fn toInt(self: &CPPMP4Item) -> i32;
fn toByte(self: &CPPMP4Item) -> u8;
fn toUInt(self: &CPPMP4Item) -> u32;
fn Item_type(item: &CPPMP4Item) -> u32;
#[namespace = "taglib_shim"]
fn Item_toIntPair(item: &CPPMP4Item) -> UniquePtr<CPPIntPair>;
#[namespace = "taglib_shim"]
fn Item_toStringList(item: &CPPMP4Item) -> UniquePtr<CPPStringList>;
#[namespace = "taglib_shim"]
fn Item_toByteVectorList(item: &CPPMP4Item) -> UniquePtr<CPPByteVectorList>;
#[namespace = "taglib_shim"]
fn Item_toCoverArtList(item: &CPPMP4Item) -> UniquePtr<CPPCoverArtList>;
#[namespace = "taglib_shim"]
fn Item_toLongLong(item: &CPPMP4Item) -> i64;
#[namespace = "taglib_shim"]
#[cxx_name = "IntPair"]
type CPPIntPair;
fn first(self: &CPPIntPair) -> i32;
fn second(self: &CPPIntPair) -> i32;
#[namespace = "taglib_shim"]
#[cxx_name = "CoverArtList"]
type CPPCoverArtList;
fn to_vector(self: &CPPCoverArtList) -> UniquePtr<CxxVector<CPPCoverArt>>;
#[namespace = "taglib_shim"]
#[cxx_name = "CoverArt"]
type CPPCoverArt;
fn format(self: &CPPCoverArt) -> u32;
fn data(self: &CPPCoverArt) -> UniquePtr<CPPByteVector>;
#[namespace = "TagLib::RIFF::WAV"]
#[cxx_name = "File"]
type CPPWAVFile;
#[cxx_name = "ID3v2Tag"]
fn WAVID3v2Tag(self: &CPPWAVFile) -> *mut CPPID3v2Tag;
// ID3v1
#[namespace = "TagLib::ID3v1"]
#[cxx_name = "Tag"]
type CPPID3v1Tag;
fn ID3v1Tag_title(tag: &CPPID3v1Tag) -> UniquePtr<CPPString>;
fn ID3v1Tag_artist(tag: &CPPID3v1Tag) -> UniquePtr<CPPString>;
fn ID3v1Tag_album(tag: &CPPID3v1Tag) -> UniquePtr<CPPString>;
fn ID3v1Tag_comment(tag: &CPPID3v1Tag) -> UniquePtr<CPPString>;
fn ID3v1Tag_genreIndex(tag: &CPPID3v1Tag) -> u32;
fn ID3v1Tag_year(tag: &CPPID3v1Tag) -> u32;
fn ID3v1Tag_track(tag: &CPPID3v1Tag) -> u32;
// ID3v2
#[namespace = "TagLib::ID3v2"]
#[cxx_name = "Tag"]
type CPPID3v2Tag;
#[namespace = "taglib_shim"]
fn Tag_frameList(tag: &CPPID3v2Tag) -> UniquePtr<CPPID3v2FrameList>;
#[namespace = "TagLib::ID3v2"]
#[cxx_name = "FrameList"]
type CPPID3v2FrameList;
#[namespace = "taglib_shim"]
fn FrameList_to_vector(list: &CPPID3v2FrameList) -> UniquePtr<CxxVector<CPPFramePointer>>;
#[namespace = "taglib_shim"]
#[cxx_name = "FramePointer"]
type CPPFramePointer;
fn get(self: &CPPFramePointer) -> *const CPPID3v2Frame;
#[namespace = "TagLib::ID3v2"]
#[cxx_name = "Frame"]
type CPPID3v2Frame;
#[namespace = "taglib_shim"]
fn Frame_id(frame: &CPPID3v2Frame) -> UniquePtr<CPPByteVector>;
#[namespace = "taglib_shim"]
unsafe fn Frame_asTextIdentification(
frame: *const CPPID3v2Frame,
) -> *const CPPID3v2TextIdentificationFrame;
#[namespace = "taglib_shim"]
unsafe fn Frame_asUserTextIdentification(
frame: *const CPPID3v2Frame,
) -> *const CPPID3v2UserTextIdentificationFrame;
#[namespace = "taglib_shim"]
unsafe fn Frame_asAttachedPicture(
frame: *const CPPID3v2Frame,
) -> *const CPPID3v2AttachedPictureFrame;
#[namespace = "TagLib::ID3v2"]
#[cxx_name = "TextIdentificationFrame"]
type CPPID3v2TextIdentificationFrame;
#[namespace = "taglib_shim"]
fn TextIdentificationFrame_fieldList(
frame: &CPPID3v2TextIdentificationFrame,
) -> UniquePtr<CPPStringList>;
#[namespace = "TagLib::ID3v2"]
#[cxx_name = "UserTextIdentificationFrame"]
type CPPID3v2UserTextIdentificationFrame;
#[namespace = "taglib_shim"]
fn UserTextIdentificationFrame_fieldList(
frame: &CPPID3v2UserTextIdentificationFrame,
) -> UniquePtr<CPPStringList>;
#[namespace = "TagLib::ID3v2"]
#[cxx_name = "AttachedPictureFrame"]
type CPPID3v2AttachedPictureFrame;
#[namespace = "taglib_shim"]
fn AttachedPictureFrame_type(frame: &CPPID3v2AttachedPictureFrame) -> u32;
#[namespace = "taglib_shim"]
fn AttachedPictureFrame_picture(
frame: &CPPID3v2AttachedPictureFrame,
) -> UniquePtr<CPPByteVector>;
#[namespace = "TagLib"]
#[cxx_name = "String"]
type CPPString;
fn toCString(self: &CPPString, unicode: bool) -> *const c_char;
#[namespace = "TagLib"]
#[cxx_name = "StringList"]
type CPPStringList;
#[namespace = "taglib_shim"]
fn StringList_to_vector(string_list: &CPPStringList) -> UniquePtr<CxxVector<CPPString>>;
#[namespace = "TagLib"]
#[cxx_name = "ByteVector"]
type CPPByteVector;
fn size(self: &CPPByteVector) -> u32;
fn data(self: &CPPByteVector) -> *const c_char;
#[namespace = "TagLib"]
#[cxx_name = "ByteVectorList"]
type CPPByteVectorList;
#[namespace = "taglib_shim"]
fn ByteVectorList_to_vector(
list: &CPPByteVectorList,
) -> UniquePtr<CxxVector<CPPByteVector>>;
}
}
#[repr(u8)]
pub enum PictureType {
Other,
FileIcon,
OtherFileIcon,
FrontCover,
BackCover,
LeafletPage,
Media,
LeadArtist,
Artist,
Conductor,
Band,
Composer,
Lyricist,
RecordingLocation,
DuringRecording,
DuringPerformance,
MovieScreenCapture,
ColoredFish,
Illustration,
BandLogo,
PublisherLogo,
}
impl PictureType {
pub fn from_u32(value: u32) -> Option<Self> {
match value {
0 => Some(Self::Other),
1 => Some(Self::FileIcon),
2 => Some(Self::OtherFileIcon),
3 => Some(Self::FrontCover),
4 => Some(Self::BackCover),
5 => Some(Self::LeafletPage),
6 => Some(Self::Media),
7 => Some(Self::LeadArtist),
8 => Some(Self::Artist),
9 => Some(Self::Conductor),
10 => Some(Self::Band),
11 => Some(Self::Composer),
12 => Some(Self::Lyricist),
13 => Some(Self::RecordingLocation),
14 => Some(Self::DuringRecording),
15 => Some(Self::DuringPerformance),
16 => Some(Self::MovieScreenCapture),
17 => Some(Self::ColoredFish),
18 => Some(Self::Illustration),
19 => Some(Self::BandLogo),
20 => Some(Self::PublisherLogo),
_ => None,
}
}
}
#[repr(u8)]
pub enum MP4ItemType {
Void,
Bool,
Int,
IntPair,
Byte,
UInt,
LongLong,
StringList,
ByteVectorList,
CoverArtList,
}
impl MP4ItemType {
pub fn from_u32(value: u32) -> Option<Self> {
match value {
0 => Some(Self::Void),
1 => Some(Self::Bool),
2 => Some(Self::Int),
3 => Some(Self::IntPair),
4 => Some(Self::Byte),
5 => Some(Self::UInt),
6 => Some(Self::LongLong),
7 => Some(Self::StringList),
8 => Some(Self::ByteVectorList),
9 => Some(Self::CoverArtList),
_ => None,
}
}
}
pub use bridge_impl::*;
impl TagLibAllocated for CPPFileRef {}
impl TagLibRef for CPPFile {}
impl TagLibRef for CppAudioProperties {}
// All of the File implementations are also TagLibRef and TagLibAllocated
impl TagLibRef for CPPVorbisFile {}
impl TagLibRef for CPPOpusFile {}
impl TagLibRef for CPPFLACFile {}
impl TagLibShared for CPPPictureList {}
impl TagLibShared for CPPFLACPicture {}
impl TagLibRef for CPPMPEGFile {}
impl TagLibRef for CPPMP4File {}
impl TagLibRef for CPPMP4Tag {}
impl TagLibRef for CPPItemMap {}
impl TagLibRef for CPPItemMapEntry {}
impl TagLibRef for CPPFieldListMap {}
impl TagLibRef for CPPFieldListEntry {}
impl TagLibRef for CPPWAVFile {}
impl TagLibRef for CPPXiphComment {}
impl TagLibRef for CPPID3v1Tag {}
impl TagLibRef for CPPID3v2Tag {}
impl TagLibShared for CPPID3v2FrameList {}
impl TagLibRef for CPPID3v2Frame {}
impl TagLibRef for CPPID3v2TextIdentificationFrame {}
impl TagLibRef for CPPID3v2UserTextIdentificationFrame {}
impl TagLibRef for CPPID3v2AttachedPictureFrame {}
impl TagLibShared for CPPMP4Item {}
impl TagLibShared for CPPCoverArt {}
impl TagLibShared for CPPIntPair {}
impl TagLibShared for CPPString {}
impl TagLibShared for CPPStringList {}
impl TagLibShared for CPPByteVector {}
impl TagLibShared for CPPByteVectorList {}
impl TagLibShared for CPPCoverArtList {}
impl<T: TagLibShared> TagLibRef for T {}
impl<T: TagLibRef> TagLibAllocated for T {}

View file

@ -0,0 +1,132 @@
use super::audioproperties::AudioProperties;
use super::bridge::{self, CPPFile};
use super::flac::FLACFile;
use super::mp4::MP4File;
use super::mpeg::MPEGFile;
use super::ogg::OpusFile;
use super::ogg::VorbisFile;
use super::riff::WAVFile;
use super::this::{RefThis, RefThisMut};
pub struct File<'file_ref> {
this: RefThisMut<'file_ref, CPPFile>,
}
impl<'file_ref> File<'file_ref> {
pub(super) fn new(this: RefThisMut<'file_ref, CPPFile>) -> Self {
Self { this }
}
pub fn audio_properties(&self) -> Option<AudioProperties<'file_ref>> {
let props_ptr = self.this.as_ref().audioProperties();
let props_ref = unsafe {
// SAFETY:
// - This points to a C++ FFI type ensured to be aligned by cxx's codegen.
// - The null-safe version is being used.
// - This points to a C++FFI type ensured to be valid by cxx's codegen.
// - There are no datapaths that will yield any mutable pointers or references
// to this, ensuring that it will not be mutated as per the aliasing rules.
props_ptr.as_ref()
};
let props_this = props_ref.map(|props| RefThis::new(props));
props_this.map(|this| AudioProperties::new(this))
}
pub fn as_opus(&mut self) -> Option<OpusFile<'file_ref>> {
let opus_file = unsafe {
// SAFETY:
// This FFI function will be a simple C++ dynamic_cast, which checks if
// the file can be cased down to an opus file. If the cast fails, a null
// pointer is returned, which will be handled by as_ref's null checking.
bridge::File_asOpus(self.this.ptr_mut())
};
let opus_ref = unsafe {
// SAFETY:
// - This points to a C++ FFI type ensured to be aligned by cxx's codegen.
// - The null-safe version is being used.
// - This points to a C++FFI type ensured to be valid by cxx's codegen.
// - There are no datapaths that will yield any mutable pointers or references
// to this, ensuring that it will not be mutated as per the aliasing rules.
opus_file.as_mut()
};
let opus_this = opus_ref.map(|opus| RefThisMut::new(opus));
opus_this.map(|this| OpusFile::new(this))
}
pub fn as_vorbis(&mut self) -> Option<VorbisFile<'file_ref>> {
let vorbis_file = unsafe {
// SAFETY:
// This FFI function will be a simple C++ dynamic_cast, which checks if
// the file can be cased down to an opus file. If the cast fails, a null
// pointer is returned, which will be handled by as_ref's null checking.
bridge::File_asVorbis(self.this.ptr_mut())
};
let vorbis_ref = unsafe {
// SAFETY:
// - This points to a C++ FFI type ensured to be aligned by cxx's codegen.
// - The null-safe version is being used.
// - This points to a C++FFI type ensured to be valid by cxx's codegen.
// - There are no datapaths that will yield any mutable pointers or references
// to this, ensuring that it will not be mutated as per the aliasing rules.
vorbis_file.as_mut()
};
let vorbis_this = vorbis_ref.map(|vorbis| RefThisMut::new(vorbis));
vorbis_this.map(|this| VorbisFile::new(this))
}
pub fn as_flac(&mut self) -> Option<FLACFile<'file_ref>> {
let flac_file = unsafe {
// SAFETY:
// This FFI function will be a simple C++ dynamic_cast, which checks if
// the file can be cased down to an opus file. If the cast fails, a null
// pointer is returned, which will be handled by as_ref's null checking.
bridge::File_asFLAC(self.this.ptr_mut())
};
let flac_ref = unsafe {
// SAFETY:
// - This points to a C++ FFI type ensured to be aligned by cxx's codegen.
// - The null-safe version is being used.
// - This points to a C++FFI type ensured to be valid by cxx's codegen.
// - There are no datapaths that will yield any mutable pointers or references
// to this, ensuring that it will not be mutated as per the aliasing rules.
flac_file.as_mut()
};
let flac_this = flac_ref.map(|flac| RefThisMut::new(flac));
flac_this.map(|this| FLACFile::new(this))
}
pub fn as_mpeg(&mut self) -> Option<MPEGFile<'file_ref>> {
let mpeg_file = unsafe {
// SAFETY:
// This FFI function will be a simple C++ dynamic_cast, which checks if
// the file can be cased down to an MPEG file. If the cast fails, a null
// pointer is returned, which will be handled by as_ref's null checking.
bridge::File_asMPEG(self.this.ptr_mut())
};
let mpeg_ref = unsafe {
// SAFETY:
// - This points to a C++ FFI type ensured to be aligned by cxx's codegen.
// - The null-safe version is being used.
// - This points to a C++FFI type ensured to be valid by cxx's codegen.
// - There are no datapaths that will yield any mutable pointers or references
// to this, ensuring that it will not be mutated as per the aliasing rules.
mpeg_file.as_mut()
};
let mpeg_this = mpeg_ref.map(|mpeg| RefThisMut::new(mpeg));
mpeg_this.map(|this| MPEGFile::new(this))
}
pub fn as_mp4(&mut self) -> Option<MP4File<'file_ref>> {
let mp4_file = unsafe { bridge::File_asMP4(self.this.ptr_mut()) };
let mp4_ref = unsafe { mp4_file.as_mut() };
let mp4_this = mp4_ref.map(|mp4| RefThisMut::new(mp4));
mp4_this.map(|this| MP4File::new(this))
}
pub fn as_wav(&mut self) -> Option<WAVFile<'file_ref>> {
let wav_file = unsafe { bridge::File_asWAV(self.this.ptr_mut()) };
let wav_ref = unsafe { wav_file.as_mut() };
let wav_this = wav_ref.map(|wav| RefThisMut::new(wav));
wav_this.map(|this| WAVFile::new(this))
}
}

View file

@ -0,0 +1,54 @@
use super::bridge::{self, CPPFileRef};
use super::file::File;
use super::iostream::{BridgedIOStream, IOStream};
use super::this::RefThisMut;
use cxx::UniquePtr;
use std::pin::Pin;
pub struct FileRef<'io> {
stream: BridgedIOStream<'io>,
this: UniquePtr<CPPFileRef>,
}
impl<'io> FileRef<'io> {
pub fn new<T: IOStream + 'io>(stream: T) -> FileRef<'io> {
let stream = BridgedIOStream::new(stream);
let cpp_stream = stream.cpp_stream().as_mut_ptr();
let file_ref = unsafe { bridge::new_FileRef(cpp_stream) };
FileRef {
stream,
this: file_ref,
}
}
pub fn file<'file_ref>(&'file_ref self) -> Option<File<'file_ref>> {
let file_ptr = unsafe {
// SAFETY:
// - This pin is only used in this unsafe scope.
// - The pin is used as a C++ this pointer in the ffi call, which does
// not change address by C++ semantics.
// - The file data is a pointer that does not depend on the
// address of self.
let this = Pin::new_unchecked(&*self.this);
// Note: This is not the rust ptr "is_null", but a taglib isNull method
// that checks for file validity. Without this check, we can get corrupted
// file ptrs.
if !this.isNull() {
Some(this.file())
} else {
None
}
};
let file_ref = file_ptr.and_then(|file| unsafe {
// SAFETY:
// - This points to a C++ FFI type ensured to be aligned by cxx's codegen.
// - The null-safe version is being used.
// - This points to a C++FFI type ensured to be valid by cxx's codegen.
// - There are no datapaths that will yield any mutable pointers or references
// to this, ensuring that it will not be mutated as per the aliasing rules.
file.as_mut()
});
let file_this = file_ref.map(|file| RefThisMut::new(file));
file_this.map(|this| File::new(this))
}
}

View file

@ -0,0 +1,95 @@
pub use super::bridge::CPPFLACFile;
pub use super::bridge::CPPFLACPicture;
pub use super::bridge::PictureType;
use super::bridge::{
CPPPictureList, FLACFile_pictureList, PictureList_to_vector, Picture_data, Picture_type,
};
use super::id3v1::ID3v1Tag;
use super::id3v2::ID3v2Tag;
use super::this::{OwnedThis, RefThis, RefThisMut, ThisMut};
use super::tk::{ByteVector, OwnedByteVector};
use super::xiph::XiphComment;
pub struct FLACFile<'file_ref> {
this: RefThisMut<'file_ref, CPPFLACFile>,
}
impl<'file_ref> FLACFile<'file_ref> {
pub(super) fn new(this: RefThisMut<'file_ref, CPPFLACFile>) -> Self {
Self { this }
}
pub fn xiph_comments(&mut self) -> Option<XiphComment<'file_ref>> {
let tag = self.this.pin_mut().xiphComment(false);
let tag_ref = unsafe {
// SAFETY: This pointer is a valid type, and can only used and accessed
// via this function and thus cannot be mutated, satisfying the aliasing rules.
tag.as_mut()
};
let tag_this = tag_ref.map(|tag| RefThisMut::new(tag));
tag_this.map(|this| XiphComment::new(this))
}
pub fn picture_list(&mut self) -> FLACPictureList<'file_ref> {
let pictures = FLACFile_pictureList(self.this.pin_mut());
let this = OwnedThis::new(pictures).unwrap();
FLACPictureList::new(this)
}
pub fn id3v1_tag(&mut self) -> Option<ID3v1Tag<'file_ref>> {
let tag = self.this.pin_mut().FLACID3v1Tag(false);
let tag_ref = unsafe { tag.as_mut() };
let tag_this = tag_ref.map(|tag| RefThisMut::new(tag));
tag_this.map(|this| ID3v1Tag::new(this))
}
pub fn id3v2_tag(&mut self) -> Option<ID3v2Tag<'file_ref>> {
let tag = self.this.pin_mut().FLACID3v2Tag(false);
let tag_ref = unsafe { tag.as_mut() };
let tag_this = tag_ref.map(|tag| RefThisMut::new(tag));
tag_this.map(|this| ID3v2Tag::new(this))
}
}
pub struct FLACPictureList<'file_ref> {
this: OwnedThis<'file_ref, CPPPictureList>,
}
impl<'file_ref> FLACPictureList<'file_ref> {
pub(super) fn new(this: OwnedThis<'file_ref, CPPPictureList>) -> Self {
Self { this }
}
pub fn to_vec(&self) -> Vec<FLACPicture<'file_ref>> {
let pictures = PictureList_to_vector(self.this.as_ref());
let mut result = Vec::new();
for picture_ptr in pictures.iter() {
let picture_ptr = picture_ptr.get();
let picture_ref = unsafe {
// SAFETY: This pointer is a valid type, and can only used and accessed
// via this function and thus cannot be mutated, satisfying the aliasing rules.
picture_ptr.as_ref().unwrap()
};
let picture_this = RefThis::new(picture_ref);
result.push(FLACPicture { this: picture_this });
}
result
}
}
pub struct FLACPicture<'file_ref> {
this: RefThis<'file_ref, CPPFLACPicture>,
}
impl<'file_ref> FLACPicture<'file_ref> {
pub fn picture_type(&self) -> Option<PictureType> {
let picture_type = Picture_type(self.this.as_ref());
PictureType::from_u32(picture_type)
}
pub fn data(&self) -> OwnedByteVector<'file_ref> {
let data = Picture_data(self.this.as_ref());
let this = OwnedThis::new(data).unwrap();
ByteVector::new(this)
}
}

View file

@ -0,0 +1,49 @@
use super::bridge::{self, CPPID3v1Tag};
use super::this::{OwnedThis, RefThisMut};
use super::tk::{OwnedString, String};
pub struct ID3v1Tag<'file_ref> {
this: RefThisMut<'file_ref, CPPID3v1Tag>,
}
impl<'file_ref> ID3v1Tag<'file_ref> {
pub(super) fn new(this: RefThisMut<'file_ref, CPPID3v1Tag>) -> Self {
Self { this }
}
pub fn title(&self) -> OwnedString<'file_ref> {
let title = bridge::ID3v1Tag_title(self.this.as_ref());
let this = OwnedThis::new(title).unwrap();
String::new(this)
}
pub fn artist(&self) -> OwnedString<'file_ref> {
let artist = bridge::ID3v1Tag_artist(self.this.as_ref());
let this = OwnedThis::new(artist).unwrap();
String::new(this)
}
pub fn album(&self) -> OwnedString<'file_ref> {
let album = bridge::ID3v1Tag_album(self.this.as_ref());
let this = OwnedThis::new(album).unwrap();
String::new(this)
}
pub fn comment(&self) -> OwnedString<'file_ref> {
let comment = bridge::ID3v1Tag_comment(self.this.as_ref());
let this = OwnedThis::new(comment).unwrap();
String::new(this)
}
pub fn genre_index(&self) -> u32 {
bridge::ID3v1Tag_genreIndex(self.this.as_ref())
}
pub fn year(&self) -> u32 {
bridge::ID3v1Tag_year(self.this.as_ref())
}
pub fn track(&self) -> u32 {
bridge::ID3v1Tag_track(self.this.as_ref())
}
}

View file

@ -0,0 +1,139 @@
use super::bridge::{
self, CPPID3v2AttachedPictureFrame, CPPID3v2Frame, CPPID3v2FrameList, CPPID3v2Tag,
CPPID3v2TextIdentificationFrame, CPPID3v2UserTextIdentificationFrame,
};
use super::this::{OwnedThis, RefThis, RefThisMut};
use super::tk::{self, ByteVector, OwnedByteVector, OwnedStringList, StringList};
pub use super::bridge::PictureType;
pub struct ID3v2Tag<'file_ref> {
this: RefThisMut<'file_ref, CPPID3v2Tag>,
}
impl<'file_ref> ID3v2Tag<'file_ref> {
pub(super) fn new(this: RefThisMut<'file_ref, CPPID3v2Tag>) -> Self {
Self { this }
}
pub fn frames(&self) -> FrameList<'file_ref> {
let frames = bridge::Tag_frameList(self.this.as_ref());
let this = OwnedThis::new(frames).unwrap();
FrameList::new(this)
}
}
pub struct FrameList<'file_ref> {
this: OwnedThis<'file_ref, CPPID3v2FrameList>,
}
impl<'file_ref> FrameList<'file_ref> {
pub(super) fn new(this: OwnedThis<'file_ref, CPPID3v2FrameList>) -> Self {
Self { this }
}
pub fn to_vec(&self) -> Vec<Frame<'file_ref>> {
let frames = bridge::FrameList_to_vector(self.this.as_ref());
frames
.iter()
.map(|frame| {
let frame_ptr = frame.get();
let frame_ref = unsafe { frame_ptr.as_ref().unwrap() };
let frame_this = RefThis::new(frame_ref);
Frame::new(frame_this)
})
.collect()
}
}
pub struct Frame<'file_ref> {
this: RefThis<'file_ref, CPPID3v2Frame>,
}
impl<'file_ref> Frame<'file_ref> {
pub(super) fn new(this: RefThis<'file_ref, CPPID3v2Frame>) -> Self {
Self { this }
}
pub fn id(&self) -> tk::OwnedByteVector<'file_ref> {
let id = bridge::Frame_id(self.this.as_ref());
let this = OwnedThis::new(id).unwrap();
ByteVector::new(this)
}
pub fn as_text_identification(&mut self) -> Option<TextIdentificationFrame<'file_ref>> {
let frame = unsafe { bridge::Frame_asTextIdentification(self.this.ptr()) };
let frame_ref = unsafe { frame.as_ref() };
let frame_this = frame_ref.map(|frame| RefThis::new(frame));
frame_this.map(|this| TextIdentificationFrame::new(this))
}
pub fn as_user_text_identification(
&mut self,
) -> Option<UserTextIdentificationFrame<'file_ref>> {
let frame = unsafe { bridge::Frame_asUserTextIdentification(self.this.ptr()) };
let frame_ref = unsafe { frame.as_ref() };
let frame_this = frame_ref.map(|frame| RefThis::new(frame));
frame_this.map(|this| UserTextIdentificationFrame::new(this))
}
pub fn as_attached_picture(&mut self) -> Option<AttachedPictureFrame<'file_ref>> {
let frame = unsafe { bridge::Frame_asAttachedPicture(self.this.ptr()) };
let frame_ref = unsafe { frame.as_ref() };
let frame_this = frame_ref.map(|frame| RefThis::new(frame));
frame_this.map(|this| AttachedPictureFrame::new(this))
}
}
pub struct TextIdentificationFrame<'file_ref> {
this: RefThis<'file_ref, CPPID3v2TextIdentificationFrame>,
}
impl<'file_ref> TextIdentificationFrame<'file_ref> {
pub(super) fn new(this: RefThis<'file_ref, CPPID3v2TextIdentificationFrame>) -> Self {
Self { this }
}
pub fn field_list(&self) -> OwnedStringList<'file_ref> {
let field_list = bridge::TextIdentificationFrame_fieldList(self.this.as_ref());
let this = OwnedThis::new(field_list).unwrap();
StringList::new(this)
}
}
pub struct UserTextIdentificationFrame<'file_ref> {
this: RefThis<'file_ref, CPPID3v2UserTextIdentificationFrame>,
}
impl<'file_ref> UserTextIdentificationFrame<'file_ref> {
pub(super) fn new(this: RefThis<'file_ref, CPPID3v2UserTextIdentificationFrame>) -> Self {
Self { this }
}
pub fn values(&self) -> OwnedStringList<'file_ref> {
let values = bridge::UserTextIdentificationFrame_fieldList(self.this.as_ref());
let this = OwnedThis::new(values).unwrap();
StringList::new(this)
}
}
pub struct AttachedPictureFrame<'file_ref> {
this: RefThis<'file_ref, CPPID3v2AttachedPictureFrame>,
}
impl<'file_ref> AttachedPictureFrame<'file_ref> {
pub(super) fn new(this: RefThis<'file_ref, CPPID3v2AttachedPictureFrame>) -> Self {
Self { this }
}
pub fn picture_type(&self) -> Option<PictureType> {
let picture_type = bridge::AttachedPictureFrame_type(self.this.as_ref());
PictureType::from_u32(picture_type)
}
pub fn picture(&self) -> OwnedByteVector<'file_ref> {
let picture = bridge::AttachedPictureFrame_picture(self.this.as_ref());
let this = OwnedThis::new(picture).unwrap();
ByteVector::new(this)
}
}

View file

@ -0,0 +1,80 @@
use super::bridge::{self, CPPIOStream};
use cxx::UniquePtr;
use std::io::SeekFrom;
pub trait IOStream {
fn read_block(&mut self, buffer: &mut [u8]) -> usize;
fn write_block(&mut self, data: &[u8]);
fn seek(&mut self, pos: SeekFrom);
fn truncate(&mut self, length: i64);
fn tell(&self) -> i64;
fn length(&self) -> i64;
fn name(&self) -> String;
fn is_readonly(&self) -> bool;
}
pub(super) struct BridgedIOStream<'io_stream> {
stream: Box<DynIOStream<'io_stream>>,
cpp_stream: UniquePtr<CPPIOStream<'io_stream>>,
}
impl<'io_stream> BridgedIOStream<'io_stream> {
pub fn new<T: IOStream + 'io_stream>(stream: T) -> Self {
let mut rs_stream: Box<DynIOStream<'io_stream>> = Box::new(DynIOStream(Box::new(stream)));
let cpp_stream: UniquePtr<CPPIOStream<'io_stream>> = unsafe {
bridge::wrap_RsIOStream(rs_stream.as_mut() as *mut _)
};
BridgedIOStream {
stream: rs_stream,
cpp_stream,
}
}
pub fn cpp_stream(&self) -> &UniquePtr<CPPIOStream> {
&self.cpp_stream
}
}
#[repr(C)]
pub(super) struct DynIOStream<'io_stream>(Box<dyn IOStream + 'io_stream>);
impl<'io_stream> DynIOStream<'io_stream> {
// Implement the exposed functions for cxx bridge
pub fn name(&self) -> String {
self.0.name()
}
pub fn read(&mut self, buffer: &mut [u8]) -> usize {
self.0.read_block(buffer)
}
pub fn write(&mut self, data: &[u8]) {
self.0.write_block(data);
}
pub fn seek(&mut self, offset: i64, whence: i32) {
let pos = match whence {
0 => SeekFrom::Start(offset as u64),
1 => SeekFrom::Current(offset),
2 => SeekFrom::End(offset),
_ => panic!("Invalid seek whence"),
};
self.0.seek(pos);
}
pub fn truncate(&mut self, length: i64) {
self.0.truncate(length);
}
pub fn tell(&self) -> i64 {
self.0.tell()
}
pub fn length(&self) -> i64 {
self.0.length()
}
pub fn is_readonly(&self) -> bool {
self.0.is_readonly()
}
}

View file

@ -0,0 +1,15 @@
pub mod audioproperties;
mod bridge;
pub mod file;
pub mod file_ref;
pub mod flac;
pub mod id3v1;
pub mod id3v2;
pub mod iostream;
pub mod mp4;
pub mod mpeg;
pub mod ogg;
pub mod riff;
mod this;
pub mod tk;
pub mod xiph;

View file

@ -0,0 +1,224 @@
pub use super::bridge::CPPMP4Tag;
use super::bridge::{
CPPIntPair, CPPItemMap, CPPMP4File, CPPMP4Item, ItemMap_to_entries, MP4ItemType,
};
use super::this::{OwnedThis, RefThis, RefThisMut};
use super::tk;
use std::collections::HashMap;
pub struct MP4File<'file_ref> {
this: RefThisMut<'file_ref, CPPMP4File>,
}
impl<'file_ref> MP4File<'file_ref> {
pub fn new(this: RefThisMut<'file_ref, CPPMP4File>) -> Self {
Self { this }
}
pub fn tag(&self) -> Option<MP4Tag<'file_ref>> {
let this = self.this.as_ref();
let tag = this.MP4Tag();
let tag_ref = unsafe { tag.as_ref() };
tag_ref.map(|tag| {
// SAFETY: The tag pointer is guaranteed to be valid for the lifetime of self
let tag_this = RefThis::new(tag);
MP4Tag::new(tag_this)
})
}
}
pub struct MP4Tag<'file_ref> {
this: RefThis<'file_ref, CPPMP4Tag>,
}
impl<'file_ref> MP4Tag<'file_ref> {
pub fn new(this: RefThis<'file_ref, CPPMP4Tag>) -> Self {
Self { this }
}
pub fn item_map(&'file_ref self) -> ItemMap<'file_ref> {
let map: &'file_ref CPPItemMap = self.this.as_ref().itemMap();
let map_this = RefThis::new(map);
ItemMap::new(map_this)
}
}
pub struct ItemMap<'file_ref> {
this: RefThis<'file_ref, CPPItemMap>,
}
impl<'file_ref> ItemMap<'file_ref> {
pub fn new(this: RefThis<'file_ref, CPPItemMap>) -> Self {
Self { this }
}
pub fn to_hashmap(&self) -> HashMap<String, MP4Item<'file_ref>> {
let cxx_vec = ItemMap_to_entries(self.this.as_ref());
let vec: Vec<(String, MP4Item<'file_ref>)> = cxx_vec
.iter()
.map(|property| {
// SAFETY:
// - This pin is only used in this unsafe scope.
// - The pin is used as a C++ this pointer in the ffi call, which does
// not change address by C++ semantics.
// - The values returned are copied and thus not dependent on the address
// of self.
let key_ref = property.key();
let key_this = OwnedThis::new(key_ref).unwrap();
let key = tk::String::new(key_this).to_string();
let value_ref = property.value();
let value_this = OwnedThis::new(value_ref).unwrap();
let value = MP4Item::new(value_this);
(key, value)
})
.collect();
HashMap::from_iter(vec)
}
}
pub struct MP4Item<'file_ref> {
this: OwnedThis<'file_ref, CPPMP4Item>,
}
impl<'file_ref> MP4Item<'file_ref> {
pub fn new(this: OwnedThis<'file_ref, CPPMP4Item>) -> Self {
Self { this }
}
pub fn data(&self) -> Option<MP4Data<'file_ref>> {
if !self.this.as_ref().isValid() {
return None;
}
let item_type = MP4ItemType::from_u32(super::bridge::Item_type(self.this.as_ref()));
item_type.and_then(|item_type| match item_type {
MP4ItemType::Void => Some(MP4Data::Void),
MP4ItemType::Bool => Some(MP4Data::Bool(self.this.as_ref().toBool())),
MP4ItemType::Int => Some(MP4Data::Int(self.this.as_ref().toInt())),
MP4ItemType::IntPair => {
let pair = super::bridge::Item_toIntPair(self.this.as_ref());
let pair_this = OwnedThis::new(pair).unwrap();
Some(MP4Data::IntPair(IntPair::new(pair_this)))
}
MP4ItemType::Byte => Some(MP4Data::Byte(self.this.as_ref().toByte())),
MP4ItemType::UInt => Some(MP4Data::UInt(self.this.as_ref().toUInt())),
MP4ItemType::LongLong => Some(MP4Data::LongLong(super::bridge::Item_toLongLong(
self.this.as_ref(),
))),
MP4ItemType::StringList => {
let string_list = super::bridge::Item_toStringList(self.this.as_ref());
let string_list_this = OwnedThis::new(string_list).unwrap();
Some(MP4Data::StringList(tk::StringList::new(string_list_this)))
}
MP4ItemType::ByteVectorList => {
let byte_vector_list = super::bridge::Item_toByteVectorList(self.this.as_ref());
let byte_vector_list_this = OwnedThis::new(byte_vector_list).unwrap();
Some(MP4Data::ByteVectorList(tk::ByteVectorList::new(
byte_vector_list_this,
)))
}
MP4ItemType::CoverArtList => {
let cover_art_list = super::bridge::Item_toCoverArtList(self.this.as_ref());
let cover_art_list_this = OwnedThis::new(cover_art_list).unwrap();
Some(MP4Data::CoverArtList(CoverArtList::new(
cover_art_list_this,
)))
}
})
}
}
pub struct CoverArtList<'file_ref> {
this: OwnedThis<'file_ref, super::bridge::CPPCoverArtList>,
}
impl<'file_ref> CoverArtList<'file_ref> {
pub fn new(this: OwnedThis<'file_ref, super::bridge::CPPCoverArtList>) -> Self {
Self { this }
}
pub fn to_vec(&self) -> Vec<CoverArt> {
let cover_arts = self.this.as_ref().to_vector();
cover_arts
.iter()
.map(|ca| {
let format = CoverArtFormat::from_u32(ca.format());
let data = ca.data();
let data_this = RefThis::new(&*data);
let data = tk::ByteVector::new(data_this).to_vec();
CoverArt { format, data }
})
.collect()
}
}
pub struct IntPair<'file_ref> {
this: OwnedThis<'file_ref, CPPIntPair>,
}
impl<'file_ref> IntPair<'file_ref> {
pub fn new(this: OwnedThis<'file_ref, CPPIntPair>) -> Self {
Self { this }
}
pub fn to_tuple(&self) -> Option<(i32, i32)> {
let this = self.this.as_ref();
let first = this.first();
let second = this.second();
Some((first, second))
}
}
pub enum MP4Data<'file_ref> {
Void,
Bool(bool),
Int(i32),
Byte(u8),
UInt(u32),
LongLong(i64),
IntPair(IntPair<'file_ref>),
StringList(tk::OwnedStringList<'file_ref>),
ByteVectorList(tk::OwnedByteVectorList<'file_ref>),
CoverArtList(CoverArtList<'file_ref>),
}
#[derive(Debug, Clone, PartialEq)]
pub struct CoverArt {
format: CoverArtFormat,
data: Vec<u8>,
}
impl CoverArt {
pub fn format(&self) -> CoverArtFormat {
self.format
}
pub fn data(&self) -> &[u8] {
&self.data
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CoverArtFormat {
Unknown,
BMP,
JPEG,
GIF,
PNG,
}
impl CoverArtFormat {
fn from_u32(value: u32) -> Self {
match value {
0 => CoverArtFormat::Unknown,
1 => CoverArtFormat::BMP,
2 => CoverArtFormat::JPEG,
3 => CoverArtFormat::GIF,
4 => CoverArtFormat::PNG,
_ => CoverArtFormat::Unknown,
}
}
}

View file

@ -0,0 +1,28 @@
use super::bridge::CPPMPEGFile;
use super::id3v1::ID3v1Tag;
use super::id3v2::ID3v2Tag;
use super::this::{RefThisMut, ThisMut};
pub struct MPEGFile<'file_ref> {
this: RefThisMut<'file_ref, CPPMPEGFile>,
}
impl<'file_ref> MPEGFile<'file_ref> {
pub(super) fn new(this: RefThisMut<'file_ref, CPPMPEGFile>) -> Self {
Self { this }
}
pub fn id3v1_tag(&mut self) -> Option<ID3v1Tag<'file_ref>> {
let tag = self.this.pin_mut().MPEGID3v1Tag(false);
let tag_ref = unsafe { tag.as_mut() };
let tag_this = tag_ref.map(|tag| RefThisMut::new(tag));
tag_this.map(|this| ID3v1Tag::new(this))
}
pub fn id3v2_tag(&mut self) -> Option<ID3v2Tag<'file_ref>> {
let tag = self.this.pin_mut().MPEGID3v2Tag(false);
let tag_ref = unsafe { tag.as_mut() };
let tag_this = tag_ref.map(|tag| RefThisMut::new(tag));
tag_this.map(|this| ID3v2Tag::new(this))
}
}

View file

@ -0,0 +1,45 @@
pub use super::bridge::{CPPOpusFile, CPPVorbisFile};
use super::this::RefThisMut;
use super::xiph::XiphComment;
pub struct VorbisFile<'file_ref> {
this: RefThisMut<'file_ref, CPPVorbisFile>,
}
impl<'file_ref> VorbisFile<'file_ref> {
pub(super) fn new(this: RefThisMut<'file_ref, CPPVorbisFile>) -> Self {
Self { this }
}
pub fn xiph_comments(&self) -> Option<XiphComment<'file_ref>> {
let tag = self.this.as_ref().vorbisTag();
let tag_ref = unsafe {
// SAFETY: This pointer is a valid type, and can only used and accessed
// via this function and thus cannot be mutated, satisfying the aliasing rules.
tag.as_mut()
};
let tag_this = tag_ref.map(|tag| RefThisMut::new(tag));
tag_this.map(|this| XiphComment::new(this))
}
}
pub struct OpusFile<'file_ref> {
this: RefThisMut<'file_ref, CPPOpusFile>,
}
impl<'file_ref> OpusFile<'file_ref> {
pub(super) fn new(this: RefThisMut<'file_ref, CPPOpusFile>) -> Self {
Self { this }
}
pub fn xiph_comments(&self) -> Option<XiphComment<'file_ref>> {
let tag = self.this.as_ref().opusTag();
let tag_ref = unsafe {
// SAFETY: This pointer is a valid type, and can only used and accessed
// via this function and thus cannot be mutated, satisfying the aliasing rules.
tag.as_mut()
};
let tag_this = tag_ref.map(|tag| RefThisMut::new(tag));
tag_this.map(|this| XiphComment::new(this))
}
}

View file

@ -0,0 +1,20 @@
use super::bridge::CPPWAVFile;
use super::id3v2::ID3v2Tag;
use super::this::RefThisMut;
pub struct WAVFile<'file_ref> {
this: RefThisMut<'file_ref, CPPWAVFile>,
}
impl<'file_ref> WAVFile<'file_ref> {
pub(super) fn new(this: RefThisMut<'file_ref, CPPWAVFile>) -> Self {
Self { this }
}
pub fn id3v2_tag(&mut self) -> Option<ID3v2Tag<'file_ref>> {
let tag = self.this.as_ref().WAVID3v2Tag();
let tag_ref = unsafe { tag.as_mut() };
let tag_this = tag_ref.map(|tag| RefThisMut::new(tag));
tag_this.map(|this| ID3v2Tag::new(this))
}
}

View file

@ -0,0 +1,98 @@
use super::bridge::{TagLibAllocated, TagLibRef, TagLibShared};
use cxx::{memory::UniquePtrTarget, UniquePtr};
use std::marker::PhantomData;
use std::pin::Pin;
pub trait This<'file_ref, T: TagLibAllocated>: AsRef<T> {}
pub trait ThisMut<'file_ref, T: TagLibAllocated>: This<'file_ref, T> {
fn pin_mut(&mut self) -> Pin<&mut T>;
}
pub struct RefThis<'file_ref, T: TagLibRef> {
this: &'file_ref T,
}
impl<'file_ref, T: TagLibRef> RefThis<'file_ref, T> {
pub fn new(this: &'file_ref T) -> Self {
Self { this }
}
pub fn ptr(&self) -> *const T {
self.this as *const T
}
}
impl<'file_ref, T: TagLibRef> AsRef<T> for RefThis<'file_ref, T> {
fn as_ref(&self) -> &T {
self.this
}
}
impl<'file_ref, T: TagLibRef> This<'file_ref, T> for RefThis<'file_ref, T> {}
pub struct RefThisMut<'file_ref, T: TagLibRef> {
this: &'file_ref mut T,
}
impl<'file_ref, T: TagLibRef> RefThisMut<'file_ref, T> {
pub fn new(this: &'file_ref mut T) -> Self {
Self { this }
}
pub fn ptr(&self) -> *const T {
self.this as *const T
}
pub fn ptr_mut(&mut self) -> *mut T {
self.this as *mut T
}
}
impl<'file_ref, T: TagLibRef> AsRef<T> for RefThisMut<'file_ref, T> {
fn as_ref(&self) -> &T {
self.this
}
}
impl<'file_ref, T: TagLibRef> This<'file_ref, T> for RefThisMut<'file_ref, T> {}
impl<'file_ref, T: TagLibRef> ThisMut<'file_ref, T> for RefThisMut<'file_ref, T> {
fn pin_mut(&mut self) -> Pin<&mut T> {
unsafe { Pin::new_unchecked(self.this) }
}
}
pub struct OwnedThis<'file_ref, T: TagLibShared + UniquePtrTarget> {
_data: PhantomData<&'file_ref ()>,
this: UniquePtr<T>,
}
impl<'file_ref, T: TagLibShared + UniquePtrTarget> OwnedThis<'file_ref, T> {
pub fn new(this: UniquePtr<T>) -> Option<Self> {
if !this.is_null() {
Some(Self {
_data: PhantomData,
this,
})
} else {
None
}
}
}
impl<'file_ref, T: TagLibShared + UniquePtrTarget> AsRef<T> for OwnedThis<'file_ref, T> {
fn as_ref(&self) -> &T {
self.this.as_ref().unwrap()
}
}
impl<'file_ref, T: TagLibShared + UniquePtrTarget> This<'file_ref, T> for OwnedThis<'file_ref, T> {}
impl<'file_ref, T: TagLibShared + UniquePtrTarget> ThisMut<'file_ref, T>
for OwnedThis<'file_ref, T>
{
fn pin_mut(&mut self) -> Pin<&mut T> {
self.this.as_mut().unwrap()
}
}

View file

@ -0,0 +1,138 @@
use super::bridge;
use super::this::{OwnedThis, RefThis, RefThisMut, This};
use std::marker::PhantomData;
use std::{ffi::CStr, string::ToString};
pub use bridge::CPPByteVector as InnerByteVector;
pub use bridge::CPPByteVectorList as InnerByteVectorList;
pub use bridge::CPPString as InnerString;
pub use bridge::CPPStringList as InnerStringList;
pub struct String<'file_ref, T: This<'file_ref, InnerString>> {
_data: PhantomData<&'file_ref ()>,
this: T,
}
impl<'file_ref, T: This<'file_ref, InnerString>> String<'file_ref, T> {
pub(super) fn new(this: T) -> Self {
Self {
_data: PhantomData,
this,
}
}
}
impl<'file_ref, T: This<'file_ref, InnerString>> ToString for String<'file_ref, T> {
fn to_string(&self) -> std::string::String {
let c_str = self.this.as_ref().toCString(true);
unsafe {
// SAFETY:
// - This is a C-string returned by a C++ method guaranteed to have
// a null terminator.
// - This C-string is fully allocated and owned by the TagString instance,
// in a continous block from start to null terminator.
// - This C-string will be non-null even if empty.
// - This pointer will not be mutated before it's entirely copied into
// rust.
// - This C-string is copied to a rust string before TagString is destroyed.
CStr::from_ptr(c_str)
}
.to_string_lossy()
.to_string()
}
}
pub type OwnedString<'file_ref> = String<'file_ref, OwnedThis<'file_ref, InnerString>>;
pub type RefString<'file_ref> = String<'file_ref, RefThis<'file_ref, InnerString>>;
pub type RefStringMut<'file_ref> = String<'file_ref, RefThisMut<'file_ref, InnerString>>;
pub struct StringList<'file_ref, T: This<'file_ref, InnerStringList>> {
_data: PhantomData<&'file_ref ()>,
this: T,
}
impl<'file_ref, T: This<'file_ref, InnerStringList>> StringList<'file_ref, T> {
pub(super) fn new(this: T) -> Self {
Self {
_data: PhantomData,
this,
}
}
pub fn to_vec(&self) -> Vec<std::string::String> {
let cxx_values = bridge::StringList_to_vector(self.this.as_ref());
cxx_values
.iter()
.map(|value| {
let this = RefThis::new(value);
String::new(this).to_string()
})
.collect()
}
}
pub type OwnedStringList<'file_ref> = StringList<'file_ref, OwnedThis<'file_ref, InnerStringList>>;
pub struct ByteVector<'file_ref, T: This<'file_ref, InnerByteVector>> {
_data: PhantomData<&'file_ref InnerByteVector>,
this: T,
}
impl<'file_ref, T: This<'file_ref, InnerByteVector>> ByteVector<'file_ref, T> {
pub(super) fn new(this: T) -> Self {
Self {
_data: PhantomData,
this,
}
}
pub fn to_vec(&self) -> Vec<u8> {
let this = self.this.as_ref();
let size = this.size().try_into().unwrap();
let data = this.data();
// Re-cast to u8
let data: *const u8 = data as *const u8;
unsafe {
// SAFETY:
// - data points to a valid buffer of size 'size' owned by the C++ ByteVector
// - we're creating a new Vec and copying the data, not taking ownership
// - the source data won't be modified while we're reading from it
std::slice::from_raw_parts(data, size).to_vec()
}
}
pub fn to_string_lossy(&self) -> std::string::String {
let this = self.this.as_ref();
let size = this.size().try_into().unwrap();
let data = this.data();
let data: *const u8 = data as *const u8;
let slice = unsafe { std::slice::from_raw_parts(data, size) };
std::string::String::from_utf8_lossy(slice).to_string()
}
}
pub type OwnedByteVector<'file_ref> = ByteVector<'file_ref, OwnedThis<'file_ref, InnerByteVector>>;
pub struct ByteVectorList<'file_ref, T: This<'file_ref, InnerByteVectorList>> {
_data: PhantomData<&'file_ref InnerByteVectorList>,
this: T,
}
impl<'file_ref, T: This<'file_ref, InnerByteVectorList>> ByteVectorList<'file_ref, T> {
pub(super) fn new(this: T) -> Self {
Self {
_data: PhantomData,
this,
}
}
pub fn to_vec(&self) -> Vec<Vec<u8>> {
let cxx_values = bridge::ByteVectorList_to_vector(self.this.as_ref());
cxx_values
.iter()
.map(|value| ByteVector::new(RefThis::new(value)).to_vec())
.collect()
}
}
pub type OwnedByteVectorList<'file_ref> =
ByteVectorList<'file_ref, OwnedThis<'file_ref, InnerByteVectorList>>;

View file

@ -0,0 +1,62 @@
pub use super::bridge::CPPXiphComment;
use super::bridge::{CPPFieldListMap, FieldListMap_to_entries, XiphComment_pictureList};
pub use super::flac::FLACPictureList;
use super::this::{OwnedThis, RefThis, RefThisMut, ThisMut};
use super::tk;
use std::collections::HashMap;
pub struct XiphComment<'file_ref> {
this: RefThisMut<'file_ref, CPPXiphComment>,
}
impl<'file_ref> XiphComment<'file_ref> {
pub fn new(this: RefThisMut<'file_ref, CPPXiphComment>) -> Self {
Self { this }
}
pub fn field_list_map(&'file_ref self) -> FieldListMap<'file_ref> {
let map: &'file_ref CPPFieldListMap = self.this.as_ref().fieldListMap();
let map_this = RefThis::new(map);
FieldListMap::new(map_this)
}
pub fn picture_list(&mut self) -> FLACPictureList<'file_ref> {
let pictures = XiphComment_pictureList(self.this.pin_mut());
let pictures_this = OwnedThis::new(pictures).unwrap();
FLACPictureList::new(pictures_this)
}
}
pub struct FieldListMap<'file_ref> {
this: RefThis<'file_ref, CPPFieldListMap>,
}
impl<'file_ref> FieldListMap<'file_ref> {
pub fn new(this: RefThis<'file_ref, CPPFieldListMap>) -> Self {
Self { this }
}
}
impl<'file_ref> FieldListMap<'file_ref> {
pub fn to_hashmap(&self) -> HashMap<String, tk::OwnedStringList<'file_ref>> {
let cxx_vec = FieldListMap_to_entries(self.this.as_ref());
cxx_vec
.iter()
.map(|property| {
// SAFETY:
// - This pin is only used in this unsafe scope.
// - The pin is used as a C++ this pointer in the ffi call, which does
// not change address by C++ semantics.
// - The values returned are copied and thus not dependent on the address
// of self.
let key_ref = property.key();
let key_this = OwnedThis::new(key_ref).unwrap();
let key = tk::String::new(key_this).to_string();
let value_ref = property.value();
let value_this = OwnedThis::new(value_ref).unwrap();
let value = tk::StringList::new(value_this);
(key, value)
})
.collect()
}
}

@ -0,0 +1 @@
Subproject commit ee1931b81116cd0091c906896f6f4fb74850be51