MP3の音楽ファイルの曲名、添付画像などのID3v2タグ編集アプリ
MP3の音楽ファイルの曲名、アーティスト名、アルバム名、トラック番号、添付画像を編集できるアプリです。
曲名、アーティスト名、アルバム名、トラック番号、添付画像以外の情報は削除してしまいます。注意してください。
アンドロイドのスマホのシステムはID3v2.4への対応が不完全なよう?なので、MP3ファイルを作成する時にID3v2.3のタグを付加しています。
不具合が有るかもしれないので、利用は自己責任でお願いいたします。
不具合でMP3の音楽ファイルを破壊してしまうかもしれないので、元のMP3のファイルをコピーしてバックアップしておいてください。
AQUOS sense3、FireHD8第12世代2022年、Pixel 7aで動作を確認できました。
※下記のXMLファイルやKotlinのプログラムなどのコードをコピペする場合は、2文字の全角空白を4文字の半角空白に置換してください。
また、Android StudioにJavaやKotlinなどのプログラムのコードをコピペして、「import android.R」が自動で追加されてしまったら、削除してください。
「android.R」は、「R.layout.activity_main」や「R.id.◯◯◯」の「R」とは違います。
そのため、「import android.R」が有ると、コンパイル エラーが発生してしまいます。
Android StudioにJavaやKotlinなどのプログラムのコードをコピペすると、変数の名前が半角バッククォート記号(`)で囲まれる事が有ります。
Kotlinでは変数の名前を半角バッククォート記号(`)で囲むと予約語(inやnullなど)や半角空白記号( )などを変数の名前にできるそうです。
可能であれば、半角バッククォート記号(`)で囲まれた変数の名前は、半角バッククォート記号(`)で囲まずに済む名前に変更したほうが良いのでは、と個人的に思っております。
/home/◯◯◯/AndroidStudioProjects/JaSimpleMp3IdV2TagEditor/app/build.gradle
――――――――――――――――――――
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'eliphas1810.jasimplemp3idv2tageditor'
compileSdk 34
defaultConfig {
applicationId "eliphas1810.jasimplemp3idv2tageditor"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
}
――――――――――――――――――――
※build.gradleを変更したら必ずAndroid Studioで「Sync Now」をクリックして押してください。
※「Sync Now」をクリックして押して初めてAndroid Studioはbuild.gradleの変更を読み込んで必要なライブラリのjarファイルをダウンロードしてくれます。
/home/◯◯◯/AndroidStudioProjects/JaSimpleMp3IdV2TagEditor/app/src/main/AndroidManifest.xml
――――――――――――――――――――
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.JaSimpleMp3IdV2TagEditor"
tools:targetApi="31"
>
<activity
android:name=".MainActivity"
android:exported="true"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
――――――――――――――――――――
◯◯◯はLinux Mintのユーザー名です。
JaSimpleMp3IdV2TagEditorは著者が付けたAndroid Studioのプロジェクトの名前です。
/home/◯◯◯/AndroidStudioProjects/JaSimpleMp3IdV2TagEditor/app/src/main/res/values/strings.xml
――――――――――――――――――――
<resources>
<string name="app_name">JaSimpleMp3IdV2TagEditor</string>
<string name="select_mp3">Select MP3</string>
<string name="make_mp3">Make MP3</string>
<string name="select_jacket_image">Select Jacket Image</string>
<string name="title_label">Title</string>
<string name="artist_label">Artist</string>
<string name="album_label">Album</string>
<string name="track_label">Track</string>
<string name="select_mp3_message">Please select MP3 file.</string>
<string name="not_support_version_message">This appli does not support ID3v1 and ID3v2.2 .</string>
<string name="invalid_text_encoding_message">A set of the ID3v2 minor version and text encoding is invalid.</string>
<string name="select_png_or_jpeg_message">Please select PNG or JPEG image file.</string>
<string name="empty_title_message">Please input title.</string>
<string name="empty_artist_message">Please input artist.</string>
<string name="empty_track_message">Please input track.</string>
<string name="make_mp3_complete_message">MP3 file making is complete.</string>
</resources>
――――――――――――――――――――
◯◯◯はLinux Mintのユーザー名です。
JaSimpleMp3IdV2TagEditorは著者が付けたAndroid Studioのプロジェクトの名前です。
/home/◯◯◯/AndroidStudioProjects/JaSimpleMp3IdV2TagEditor/app/src/main/res/values-ja/strings.xml
――――――――――――――――――――
<resources>
<string name="app_name">JaSimpleMp3IdV2TagEditor</string>
<string name="select_mp3">MP3ファイル選択</string>
<string name="make_mp3">MP3ファイル作成</string>
<string name="select_jacket_image">ジャケット画像選択</string>
<string name="title_label">曲名</string>
<string name="artist_label">アーティスト</string>
<string name="album_label">アルバム</string>
<string name="track_label">トラック番号</string>
<string name="select_mp3_message">MP3ファイルを選択してください。</string>
<string name="not_support_version_message">当アプリはID3v2.3とID3v2.4以外には未対応です。他のアプリを利用してください。</string>
<string name="invalid_text_encoding_message">存在しないID3v2マイナーバージョンとテキスト エンコーディングの16進数表記の組み合わせです。</string>
<string name="select_png_or_jpeg_message">PNG形式かJPEG形式の画像ファイルを選択してください。</string>
<string name="empty_title_message">曲名を入力してください。</string>
<string name="empty_artist_message">アーティストを入力してください。</string>
<string name="empty_track_message">トラック番号を入力してください。</string>
<string name="make_mp3_complete_message">新MP3ファイルの作成が完了しました。</string>
</resources>
――――――――――――――――――――
◯◯◯はLinux Mintのユーザー名です。
JaSimpleMp3IdV2TagEditorは著者が付けたAndroid Studioのプロジェクトの名前です。
/home/◯◯◯/AndroidStudioProjects/JaSimpleMp3IdV2TagEditor/app/src/main/res/layout/activity_main.xml
――――――――――――――――――――
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<Button
android:id="@+id/selectMP3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/select_mp3"
android:layout_gravity="center"
/>
<TextView
android:id="@+id/titleLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/title_label"
/>
<EditText
android:id="@+id/title"
android:inputType="text"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text=""
/>
<TextView
android:id="@+id/artistLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/artist_label"
/>
<EditText
android:id="@+id/artist"
android:inputType="text"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text=""
/>
<TextView
android:id="@+id/albumLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/album_label"
/>
<EditText
android:id="@+id/album"
android:inputType="text"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text=""
/>
<TextView
android:id="@+id/trackLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/track_label"
/>
<EditText
android:id="@+id/track"
android:inputType="text"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text=""
/>
<Button
android:id="@+id/makeMP3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/make_mp3"
android:layout_gravity="center"
/>
<Button
android:id="@+id/selectJacketImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/select_jacket_image"
android:layout_gravity="center"
/>
<eliphas1810.jasimplemp3idv2tageditor.ZoomableImageView
android:id="@+id/jacketImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>
――――――――――――――――――――
◯◯◯はLinux Mintのユーザー名です。
JaSimpleMp3IdV2TagEditorは著者が付けたAndroid Studioのプロジェクトの名前です。
/home/◯◯◯/AndroidStudioProjects/JaSimpleMp3IdV2TagEditor/app/src/main/java/eliphas1810/jasimplemp3idv2tageditor/MainActivity.kt
――――――――――――――――――――
package eliphas1810.jasimplemp3idv2tageditor
import android.content.Intent
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import android.widget.*
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile
import java.io.BufferedOutputStream
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import kotlin.experimental.and
class MainActivity : AppCompatActivity() {
var titleEditText: EditText? = null
var artistEditText: EditText? = null
var albumEditText: EditText? = null
var trackEditText: EditText? = null
var jacketImageView: ZoomableImageView? = null
var imageMimetype: String? = null
var imageByteArray: ByteArray? = null
var mpegFrameByteArray: ByteArray? = null
private var selectMP3ActivityResultLauncher: ActivityResultLauncher<Intent>? = registerForActivityResult(StartActivityForResult()) { activityResult: ActivityResult ->
if (activityResult.resultCode == RESULT_OK) {
val intent = activityResult.data
val uri: Uri? = intent?.data
val documentFile = DocumentFile.fromSingleUri(applicationContext, uri!!)
val fileName: String? = documentFile?.name
if ((fileName?.matches(Regex("^.+\\.[mM][pP]3$")) ?: false) == false) {
Toast.makeText(applicationContext, getString(R.string.select_mp3_message), Toast.LENGTH_LONG).show()
return@registerForActivityResult
}
var byteArray = ByteArray(0)
contentResolver.openInputStream(uri!!).use {
byteArray = it?.readBytes() ?: ByteArray(0) //2GB以下しか読み込めません。
}
titleEditText?.setText("")
artistEditText?.setText("")
albumEditText?.setText("")
trackEditText?.setText("")
jacketImageView?.setImageBitmap(null)
imageMimetype = null
imageByteArray = null
mpegFrameByteArray = null
val id3 = String(byteArray.sliceArray(0..2), StandardCharsets.UTF_8)
if (id3 != "ID3") {
mpegFrameByteArray = byteArray
return@registerForActivityResult
}
val minorVersion: Int = (byteArray[3].toUInt() and 0xFFu).toInt()
if (minorVersion <= 2 || 5 <= minorVersion) {
Toast.makeText(applicationContext, getString(R.string.not_support_version_message), Toast.LENGTH_LONG).show()
return@registerForActivityResult
}
//val patchVersion: Int = (byteArray[4].toUInt() and 0xFFu).toInt()
val flag: Int = (byteArray[5].toUInt() and 0xFFu).toInt()
val hasExHeader: Boolean = (flag and 0x02).toInt() != 0
var headerSize = 0
headerSize += (byteArray[6].toUInt() and 0xFFu shl 21).toInt()
headerSize += (byteArray[7].toUInt() and 0xFFu shl 14).toInt()
headerSize += (byteArray[8].toUInt() and 0xFFu shl 7).toInt()
headerSize += (byteArray[9].toUInt() and 0xFFu).toInt()
var byteIndex = 10
if (hasExHeader) {
var exHeaderSize = 0
if (minorVersion == 3) {
exHeaderSize += (byteArray[10].toUInt() and 0xFFu shl 24).toInt()
exHeaderSize += (byteArray[11].toUInt() and 0xFFu shl 16).toInt()
exHeaderSize += (byteArray[12].toUInt() and 0xFFu shl 8).toInt()
exHeaderSize += (byteArray[13].toUInt() and 0xFFu).toInt()
} else {
exHeaderSize += (byteArray[10].toUInt() and 0xFFu shl 21).toInt()
exHeaderSize += (byteArray[11].toUInt() and 0xFFu shl 14).toInt()
exHeaderSize += (byteArray[12].toUInt() and 0xFFu shl 7).toInt()
exHeaderSize += (byteArray[13].toUInt() and 0xFFu).toInt()
}
byteIndex += exHeaderSize
}
while (byteIndex < headerSize) {
val frameId = String(byteArray.sliceArray(byteIndex..(byteIndex + 3)), StandardCharsets.UTF_8)
byteIndex += 4
if (byteIndex == 14 && frameId.matches(Regex("^[A-Z][A-Z][A-Z][A-Z0-9]$")) == false) {
byteIndex -= 4
var exHeaderSize = 0
if (minorVersion == 3) {
exHeaderSize += (byteArray[10].toUInt() and 0xFFu shl 24).toInt()
exHeaderSize += (byteArray[11].toUInt() and 0xFFu shl 16).toInt()
exHeaderSize += (byteArray[12].toUInt() and 0xFFu shl 8).toInt()
exHeaderSize += (byteArray[13].toUInt() and 0xFFu).toInt()
} else {
exHeaderSize += (byteArray[10].toUInt() and 0xFFu shl 21).toInt()
exHeaderSize += (byteArray[11].toUInt() and 0xFFu shl 14).toInt()
exHeaderSize += (byteArray[12].toUInt() and 0xFFu shl 7).toInt()
exHeaderSize += (byteArray[13].toUInt() and 0xFFu).toInt()
}
byteIndex += exHeaderSize
continue
}
var frameSize = 0
if (minorVersion == 3) {
frameSize += (byteArray[byteIndex].toUInt() and 0xFFu shl 24).toInt()
frameSize += (byteArray[byteIndex + 1].toUInt() and 0xFFu shl 16).toInt()
frameSize += (byteArray[byteIndex + 2].toUInt() and 0xFFu shl 8).toInt()
frameSize += (byteArray[byteIndex + 3].toUInt() and 0xFFu).toInt()
} else {
frameSize += (byteArray[byteIndex].toUInt() and 0xFFu shl 21).toInt()
frameSize += (byteArray[byteIndex + 1].toUInt() and 0xFFu shl 14).toInt()
frameSize += (byteArray[byteIndex + 2].toUInt() and 0xFFu shl 7).toInt()
frameSize += (byteArray[byteIndex + 3].toUInt() and 0xFFu).toInt()
}
byteIndex += 4
byteIndex += 2 //フレームのフラグは無視して飛ばします。
if (frameId.matches(Regex("^TIT2$|^TPE1$|^TALB$|^TRCK$"))) {
val encodingByte: Byte = byteArray[byteIndex]
var charset: Charset? = null
if ((encodingByte.toUInt() and 0xFFu).toInt() == 0x00) {
//charset = Charset.forName("ISO-8859-1")
charset = Charset.forName("Windows-31J") //過去の日本語のアプリケーションにはISO-8859-1でWindowsの日本語のテキストを書き込んでいた物が有ったそうです。
} else if ((encodingByte.toUInt() and 0xFFu).toInt() == 0x01) {
charset = Charset.forName("UTF-16")
} else if (minorVersion == 4 && (encodingByte.toUInt() and 0xFFu).toInt() == 0x02) {
charset = Charset.forName("UTF-16BE")
} else if (minorVersion == 4 && (encodingByte.toUInt() and 0xFFu).toInt() == 0x03) {
charset = Charset.forName("UTF-8")
} else {
Toast.makeText(applicationContext, getString(R.string.invalid_text_encoding_message) + " Minor Version: " + minorVersion + " Text Encoding Byte: " + encodingByte.toUInt().toInt(), Toast.LENGTH_LONG).show()
return@registerForActivityResult
}
byteIndex += 1
val content = String(byteArray.sliceArray(byteIndex..(byteIndex + frameSize - 1 - 1)), charset)
byteIndex += (frameSize - 1)
if (frameId == "TIT2") {
titleEditText?.setText(content)
} else if (frameId == "TPE1") {
artistEditText?.setText(content)
} else if (frameId == "TALB") {
albumEditText?.setText(content)
} else if (frameId == "TRCK") {
trackEditText?.setText(content)
}
} else if (frameId == "APIC") {
val encodingByte: Byte = byteArray[byteIndex]
var charset: Charset? = null
if ((encodingByte.toUInt() and 0xFFu).toInt() == 0x00) {
//charset = Charset.forName("ISO-8859-1")
charset = Charset.forName("Windows-31J") //過去の日本語のアプリケーションにはISO-8859-1でWindowsの日本語のテキストを書き込んでいた物が有ったそうです。
} else if ((encodingByte.toUInt() and 0xFFu).toInt() == 0x01) {
charset = Charset.forName("UTF-16")
} else if (minorVersion == 4 && (encodingByte.toUInt() and 0xFFu).toInt() == 0x02) {
charset = Charset.forName("UTF-16BE")
} else if (minorVersion == 4 && (encodingByte.toUInt() and 0xFFu).toInt() == 0x03) {
charset = Charset.forName("UTF-8")
} else {
Toast.makeText(applicationContext, getString(R.string.invalid_text_encoding_message) + " Minor Version: " + minorVersion + " Text Encoding Byte: " + encodingByte.toUInt().toInt(), Toast.LENGTH_LONG).show()
return@registerForActivityResult
}
byteIndex += 1
val mimetypeByteList: MutableList<Byte> = mutableListOf()
for (index in 0..(frameSize - 1 - 1)) {
val currentByte: Byte = byteArray[byteIndex + index]
if (currentByte.toUInt().toInt() == 0x00/* NULL */) {
break
}
mimetypeByteList.add(currentByte)
}
if ((frameSize - 1) <= mimetypeByteList.size) {
mimetypeByteList.clear()
}
imageMimetype = String(mimetypeByteList.toByteArray(), charset)
byteIndex += (mimetypeByteList.size + 1)
byteIndex += 1 //Picture Type(画像の種類)を無視して飛ばします。
val descriptionList: MutableList<Byte> = mutableListOf()
for (index in 0..(frameSize - 1 - mimetypeByteList.size - 1 - 1 - 1)) {
val currentByte: Byte = byteArray[byteIndex + index]
if (currentByte.toUInt().toInt() == 0x00/* NULL */) {
break
}
descriptionList.add(currentByte)
}
byteIndex += (descriptionList.size + 1)
imageByteArray = byteArray.sliceArray(byteIndex..(byteIndex + frameSize - 1 - mimetypeByteList.size - 1 - 1 - descriptionList.size - 1 - 1))
jacketImageView?.setImageBitmap(BitmapFactory.decodeByteArray(imageByteArray, 0, imageByteArray!!.size))
} else {
byteIndex += frameSize
}
}
if (headerSize < byteIndex) {
byteIndex = headerSize
}
mpegFrameByteArray = byteArray.sliceArray(byteIndex..(byteIndex + byteArray.size - headerSize - 1))
}
}
private var selectJacketImageActivityResultLauncher: ActivityResultLauncher<Intent>? = registerForActivityResult(StartActivityForResult()) { activityResult: ActivityResult ->
if (activityResult.resultCode == RESULT_OK) {
val intent = activityResult.data
val uri: Uri? = intent?.data
val documentFile = DocumentFile.fromSingleUri(applicationContext, uri!!)
val fileName: String? = documentFile?.name
if ((fileName?.matches(Regex("^.+\\.[pP][nN][gG]$|^.+\\.[jJ][pP][eE]?[gG]$")) ?: false) == false) {
Toast.makeText(applicationContext, getString(R.string.select_png_or_jpeg_message), Toast.LENGTH_LONG).show()
return@registerForActivityResult
}
if (fileName?.matches(Regex("^.+\\.[pP][nN][gG]$")) ?: false) {
imageMimetype = "image/png"
} else {
imageMimetype = "image/jpeg"
}
contentResolver.openInputStream(uri!!).use {
imageByteArray = it?.readBytes() ?: ByteArray(0) //2GB以下しか読み込めません。
}
jacketImageView?.setImageBitmap(BitmapFactory.decodeByteArray(imageByteArray, 0, imageByteArray!!.size))
}
}
private var makeMP3ActivityResultLauncher: ActivityResultLauncher<Intent>? = registerForActivityResult(StartActivityForResult()) { activityResult: ActivityResult ->
if (activityResult.resultCode == RESULT_OK) {
val intent = activityResult.data
val uri: Uri? = intent?.data
if (mpegFrameByteArray?.isEmpty() ?: false) {
Toast.makeText(applicationContext, getString(R.string.select_mp3_message), Toast.LENGTH_LONG).show()
return@registerForActivityResult
}
val title = titleEditText?.getText()?.toString() ?: ""
val artist = artistEditText?.getText()?.toString() ?: ""
val album = albumEditText?.getText()?.toString() ?: ""
val track = trackEditText?.getText()?.toString() ?: ""
//contentResolver.openOutputStream()で"wt"モードを指定しないと、書き込み前のバイト数が大きい場合、書き込み前のバイトの先頭の一部を置換するような形に成ってしまいます。
BufferedOutputStream(contentResolver.openOutputStream(uri!!, "wt")).use {
val titleByteArray = title.toByteArray(StandardCharsets.UTF_16)
val artistByteArray = artist.toByteArray(StandardCharsets.UTF_16)
val albumByteArray = album.toByteArray(StandardCharsets.UTF_16)
val trackByteArray = track.toByteArray(StandardCharsets.UTF_16)
var headerSize: Int = 0
headerSize += 10
headerSize += (10 + 1 + titleByteArray.size)
headerSize += (10 + 1 + artistByteArray.size)
headerSize += (10 + 1 + trackByteArray.size)
if (album.isEmpty() == false) {
headerSize += (10 + 1 + albumByteArray.size)
}
if (1 <= (imageByteArray?.size ?: 0)) {
headerSize += (10 + 1 + (imageMimetype?.length ?: 0) + 1 + 1 + 1 + (imageByteArray?.size ?: 0))
}
//Javaはデフォルトはビッグ エンディアン
//ID3v2タグはビッグ エンディアン
it?.write(0x49/* I */)
it?.write(0x44/* D */)
it?.write(0x33/* 3 */)
it?.write(0x03/* マイナーバージョン3 */)
it?.write(0x00/* パッチバージョン0 */)
it?.write(0x00/* ヘッダーのフラグ */)
it?.write(headerSize.toUInt().shl(4).toInt().ushr(25))
it?.write(headerSize.toUInt().shl(11).toInt().ushr(25))
it?.write(headerSize.toUInt().shl(18).toInt().ushr(25))
it?.write(headerSize.toUInt().shl(25).toInt().ushr(25))
it?.write(0x54/* T */)
it?.write(0x49/* I */)
it?.write(0x54/* T */)
it?.write(0x32/* 2 */)
it?.write((1 + titleByteArray.size).ushr(24))
it?.write((1 + titleByteArray.size).toUInt().shl(8).toInt().ushr(24))
it?.write((1 + titleByteArray.size).toUInt().shl(16).toInt().ushr(24))
it?.write((1 + titleByteArray.size).toUInt().shl(24).toInt().ushr(24))
it?.write(0x00/* フレームのフラグ */)
it?.write(0x00/* フレームのフラグ */)
it?.write(0x01/* テキストのフレームの文字コード。BOM付きUTF-16は16進数で01。 */)
for (index in 0..(titleByteArray.size - 1)) {
it?.write(titleByteArray[index].toInt())
}
it?.write(0x54/* T */)
it?.write(0x50/* P */)
it?.write(0x45/* E */)
it?.write(0x31/* 1 */)
it?.write((1 + artistByteArray.size).ushr(24))
it?.write((1 + artistByteArray.size).toUInt().shl(8).toInt().ushr(24))
it?.write((1 + artistByteArray.size).toUInt().shl(16).toInt().ushr(24))
it?.write((1 + artistByteArray.size).toUInt().shl(24).toInt().ushr(24))
it?.write(0x00/* フレームのフラグ */)
it?.write(0x00/* フレームのフラグ */)
it?.write(0x01/* テキストのフレームの文字コード。BOM付きUTF-16は16進数で01。 */)
for (index in 0..(artistByteArray.size - 1)) {
it?.write(artistByteArray[index].toInt())
}
it?.write(0x54/* T */)
it?.write(0x52/* R */)
it?.write(0x43/* C */)
it?.write(0x4B/* K */)
it?.write((1 + trackByteArray.size).ushr(24))
it?.write((1 + trackByteArray.size).toUInt().shl(8).toInt().ushr(24))
it?.write((1 + trackByteArray.size).toUInt().shl(16).toInt().ushr(24))
it?.write((1 + trackByteArray.size).toUInt().shl(24).toInt().ushr(24))
it?.write(0x00/* フレームのフラグ */)
it?.write(0x00/* フレームのフラグ */)
it?.write(0x01/* テキストのフレームの文字コード。BOM付きUTF-16は16進数で01。 */)
for (index in 0..(trackByteArray.size - 1)) {
it?.write(trackByteArray[index].toInt())
}
if (album.isEmpty() == false) {
it?.write(0x54/* T */)
it?.write(0x41/* A */)
it?.write(0x4C/* L */)
it?.write(0x42/* B */)
it?.write((1 + albumByteArray.size).ushr(24))
it?.write((1 + albumByteArray.size).toUInt().shl(8).toInt().ushr(24))
it?.write((1 + albumByteArray.size).toUInt().shl(16).toInt().ushr(24))
it?.write((1 + albumByteArray.size).toUInt().shl(24).toInt().ushr(24))
it?.write(0x00/* フレームのフラグ */)
it?.write(0x00/* フレームのフラグ */)
it?.write(0x01/* テキストのフレームの文字コード。BOM付きUTF-16は16進数で01。 */)
for (index in 0..(albumByteArray.size - 1)) {
it?.write(albumByteArray[index].toInt())
}
}
if (1 <= (imageByteArray?.size ?: 0)) {
it?.write(0x41/* A */)
it?.write(0x50/* P */)
it?.write(0x49/* I */)
it?.write(0x43/* C */)
it?.write((1 + (imageMimetype?.length ?: 0) + 1 + 1 + 1 + (imageByteArray?.size ?: 0)).ushr(24))
it?.write((1 + (imageMimetype?.length ?: 0) + 1 + 1 + 1 + (imageByteArray?.size ?: 0)).toUInt().shl(8).toInt().ushr(24))
it?.write((1 + (imageMimetype?.length ?: 0) + 1 + 1 + 1 + (imageByteArray?.size ?: 0)).toUInt().shl(16).toInt().ushr(24))
it?.write((1 + (imageMimetype?.length ?: 0) + 1 + 1 + 1 + (imageByteArray?.size ?: 0)).toUInt().shl(24).toInt().ushr(24))
it?.write(0x00/* フレームのフラグ */)
it?.write(0x00/* フレームのフラグ */)
it?.write(0x00/* テキストのフレームの文字コード。ISO-8859-1は16進数で00。 */)
val imageMimetypeByteArray: ByteArray? = imageMimetype?.toByteArray(StandardCharsets.UTF_8) //UTF-8はISO-8859-1を包含
for (index in 0..((imageMimetypeByteArray?.size ?: 0) - 1)) {
it?.write(imageMimetypeByteArray!![index].toInt())
}
it?.write(0x00/* NULLの文字コード */)
it?.write(0x03/* Picture Type(画像の種類)。Front Cover(表カバー)は16進数で03。 */)
it?.write(0x00/* Description(説明)の終了を表すNULLの文字コード。 */)
for (index in 0..((imageByteArray?.size ?: 0) - 1)) {
it?.write(imageByteArray!![index].toInt())
}
}
for (index in 0..((mpegFrameByteArray?.size ?: 0) - 1)) {
it?.write(mpegFrameByteArray!![index].toInt())
}
it.flush()
}
Toast.makeText(applicationContext, getString(R.string.make_mp3_complete_message), Toast.LENGTH_LONG).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
try {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
titleEditText = findViewById(R.id.title)
artistEditText = findViewById(R.id.artist)
albumEditText = findViewById(R.id.album)
trackEditText = findViewById(R.id.track)
jacketImageView = findViewById(R.id.jacketImage)
findViewById<Button>(R.id.selectMP3).setOnClickListener{ view ->
try {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "audio/mpeg"
selectMP3ActivityResultLauncher?.launch(intent)
} catch (exception: Exception) {
Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()
throw exception
}
}
findViewById<Button>(R.id.selectJacketImage).setOnClickListener{ view ->
try {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "image/*"
selectJacketImageActivityResultLauncher?.launch(intent)
} catch (exception: Exception) {
Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()
throw exception
}
}
findViewById<Button>(R.id.makeMP3).setOnClickListener{ view ->
try {
val title = titleEditText?.getText()?.toString() ?: ""
val artist = artistEditText?.getText()?.toString() ?: ""
val track = trackEditText?.getText()?.toString() ?: ""
if (title.isEmpty()) {
Toast.makeText(applicationContext, getString(R.string.empty_title_message), Toast.LENGTH_LONG).show()
return@setOnClickListener
}
if (artist.isEmpty()) {
Toast.makeText(applicationContext, getString(R.string.empty_artist_message), Toast.LENGTH_LONG).show()
return@setOnClickListener
}
if (track.isEmpty()) {
Toast.makeText(applicationContext, getString(R.string.empty_track_message), Toast.LENGTH_LONG).show()
return@setOnClickListener
}
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "audio/mpeg"
makeMP3ActivityResultLauncher?.launch(intent)
} catch (exception: Exception) {
Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()
throw exception
}
}
} catch (exception: Exception) {
Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()
throw exception
}
}
override fun onDestroy() {
try {
titleEditText = null
artistEditText = null
albumEditText = null
trackEditText = null
jacketImageView = null
imageMimetype = null
imageByteArray = null
mpegFrameByteArray = null
selectMP3ActivityResultLauncher?.unregister()
selectMP3ActivityResultLauncher = null
selectJacketImageActivityResultLauncher?.unregister()
selectJacketImageActivityResultLauncher = null
makeMP3ActivityResultLauncher?.unregister()
makeMP3ActivityResultLauncher = null
} catch (exception: Exception) {
Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()
throw exception
} finally {
super.onDestroy()
}
}
}
――――――――――――――――――――
◯◯◯はLinux Mintのユーザー名です。
JaSimpleMp3IdV2TagEditorは著者が付けたAndroid Studioのプロジェクトの名前です。
eliphas1810/jasimplemp3idv2tageditorは著者が付けたJavaやKotlinのプログラムのパッケージのディレクトリの相対パスです。
eliphas1810.jasimplemp3idv2tageditorは著者が付けたJavaやKotlinのプログラムのパッケージの名前です。
/home/◯◯◯/AndroidStudioProjects/JaSimpleMp3IdV2TagEditor/app/src/main/java/eliphas1810/jasimplemp3idv2tageditor/ZoomableImageView.kt
――――――――――――――――――――
package eliphas1810.jasimplemp3idv2tageditor
import android.content.Context
import android.graphics.Matrix
import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import androidx.appcompat.widget.AppCompatImageView
class ZoomableImageView(context: Context, attributeSet: AttributeSet?, defaultStyleAttribute: Int) : AppCompatImageView(context, attributeSet, defaultStyleAttribute), ScaleGestureDetector.OnScaleGestureListener {
constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0)
constructor(context: Context) : this(context, null, 0)
val scaleGestureDetector = ScaleGestureDetector(context, this)
val simpleOnGestureListener = object : GestureDetector.SimpleOnGestureListener() {
override fun onScroll(
e1: MotionEvent?,
motionEvent1: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
val imageViewWidth = width
val imageViewHeight = height
val imageMatrixValues = FloatArray(9)
imageMatrix.getValues(imageMatrixValues)
val imageWidth = imageViewWidth * imageMatrixValues[Matrix.MSCALE_X]
val imageHeight = imageViewHeight * imageMatrixValues[Matrix.MSCALE_Y]
var x = 0.0f
var y = 0.0f
//縮小中の画像が画像ビューよりも小さい場合
//
//画像が画像ビューよりも小さい場合
//
if (imageWidth < imageViewWidth) {
//画像を動かさない
//x = 0.0f
//拡大中の画像が画像ビューよりも大きい場合
//
//画像が画像ビューよりも大きい場合
//画像の左端が画像ビューと画面よりも右に離れていて、更に指を左へ動かして、画像を逆方向の更に右へ動かそうとした場合
} else if (distanceX < 0.0f && 0.0f < imageMatrixValues[Matrix.MTRANS_X]) {
//画像を元にゼロに戻す
x = 0.0f - imageMatrixValues[Matrix.MTRANS_X]
//画像の右端が画像ビューと画面よりも左に離れていて、更に指を右へ動かして、画像を逆方向の更に左へ動かそうとした場合
} else if ((imageWidth + imageMatrixValues[Matrix.MTRANS_X]) < imageViewWidth && 0.0f < distanceX) {
//画像の右端を画像ビューと画面の右端に戻す
//
//画像の右端と、画像ビューと画面の右端の差分だけ戻す
//
x = imageViewWidth - (imageWidth + imageMatrixValues[Matrix.MTRANS_X])
//その他の場合
} else {
//指で動かした分だけ逆方向へ動かす
x = 0.0f - distanceX
}
//縮小中の画像が画像ビューよりも小さい場合
//
//画像が画像ビューよりも小さい場合
//
if (imageHeight < imageViewHeight) {
//画像を動かさない
//y = 0.0f
//拡大中の画像が画像ビューよりも大きい場合
//
//画像が画像ビューよりも大きい場合
//画像の上端が画像ビューと画面よりも下に離れていて、更に指を上へ動かして、画像を逆方向の更に下へ動かそうとした場合
} else if (distanceY < 0.0f && 0.0f < imageMatrixValues[Matrix.MTRANS_Y]) {
//画像を元にゼロに戻す
y = 0.0f - imageMatrixValues[Matrix.MTRANS_Y]
//画像の下端が画像ビューと画面よりも上に離れていて、更に指を下へ動かして、画像を逆方向の更に上へ動かそうとした場合
} else if ((imageHeight + imageMatrixValues[Matrix.MTRANS_Y]) < imageViewHeight && 0.0f < distanceY) {
//画像の下端を画像ビューと画面の下端に戻す
//
//画像の下端と、画像ビューと画面の下端の差分だけ戻す
//
y = imageViewHeight - (imageHeight + imageMatrixValues[Matrix.MTRANS_Y])
//その他の場合
} else {
//指で動かした分だけ逆方向へ動かす
y = 0.0f - distanceY
}
//画像を移動
imageMatrix.postTranslate(x, y)
//画像ビューの枠内の画像を再描画
invalidate()
return super.onScroll(e1, motionEvent1, distanceX, distanceY)
}
}
val gestureDetector = GestureDetector(context, simpleOnGestureListener)
val minScaleFactor = 0.5f
override fun onTouchEvent(motionEvent: MotionEvent?): Boolean {
gestureDetector.onTouchEvent(motionEvent!!)
scaleGestureDetector.onTouchEvent(motionEvent!!)
return true
}
override fun onScaleBegin(scaleGestureDetector: ScaleGestureDetector): Boolean {
return true
}
override fun onScale(scaleGestureDetector: ScaleGestureDetector): Boolean {
var scaleFactor = scaleGestureDetector.scaleFactor
if (scaleFactor == 1.0f) {
return true
}
if (scaleFactor < minScaleFactor) {
scaleFactor = minScaleFactor
}
super.setScaleType(ScaleType.MATRIX)
val imageMatrix = super.getImageMatrix()
imageMatrix.postScale(scaleFactor, scaleFactor)
super.setImageMatrix(imageMatrix)
val layoutParams = super.getLayoutParams()
layoutParams.width = (super.getWidth() * scaleFactor).toInt()
layoutParams.height = (super.getHeight() * scaleFactor).toInt()
super.setLayoutParams(layoutParams)
return true
}
override fun onScaleEnd(scaleGestureDetector: ScaleGestureDetector) {
}
}
――――――――――――――――――――
◯◯◯はLinux Mintのユーザー名です。
JaSimpleMp3IdV2TagEditorは著者が付けたAndroid Studioのプロジェクトの名前です。
eliphas1810/jasimplemp3idv2tageditorは著者が付けたJavaやKotlinのプログラムのパッケージのディレクトリの相対パスです。
eliphas1810.jasimplemp3idv2tageditorは著者が付けたJavaやKotlinのプログラムのパッケージの名前です。