表示調整
閉じる
挿絵表示切替ボタン
▼配色
▼行間
▼文字サイズ
▼メニューバー
×閉じる

ブックマークに追加しました

設定
0/400
設定を保存しました
エラーが発生しました
※文字以内
ブックマークを解除しました。

エラーが発生しました。

エラーの原因がわからない場合はヘルプセンターをご確認ください。

ブックマーク機能を使うにはログインしてください。
15/19

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のプログラムのパッケージの名前です。

評価をするにはログインしてください。
この作品をシェア
Twitter LINEで送る
ブックマークに追加
ブックマーク機能を使うにはログインしてください。
+注意+

特に記載なき場合、掲載されている作品はすべてフィクションであり実在の人物・団体等とは一切関係ありません。
特に記載なき場合、掲載されている作品の著作権は作者にあります(一部作品除く)。
作者以外の方による作品の引用を超える無断転載は禁止しており、行った場合、著作権法の違反となります。

この作品はリンクフリーです。ご自由にリンク(紹介)してください。
この作品はスマートフォン対応です。スマートフォンかパソコンかを自動で判別し、適切なページを表示します。

↑ページトップへ