How to get&modify metadata to supported audio files on Android?
<h2>Background</h2>

<p>Android supports <a href="http://developer.android.com/guide/appendix/media-formats.html" rel="nofollow noreferrer"><strong>various audio files</strong></a> encoding and decoding.</p>

<p>I record audio into an audio file using <strong><a href="http://developer.android.com/reference/android/media/MediaRecorder.html" rel="nofollow noreferrer">android.media.MediaRecorder</a></strong> class, but I also wish to show information about the files I've recorded (not standard data, but still just text, maybe even configurable by user), and I think it's best to store this information within the files. </p>

<p>examples of possible data to store: when it was recorded, where it was recorded, notes by the user...</p>

<h2>The problem</h2>

<p>The MediaRecorder class doesn't have any function that I can find, to add or even read metadata of the recorded audio file.</p>

<p>I also can't find a similar class that does it. </p>

<h2>What I've tried</h2>

<p>I tried searching how to do it for specific files types, and also tried to find a library that does it.</p>

<p>I haven't find even a clue about this information.</p>

<p>The only thing I've found for MediaRecorder class, is a function called "<a href="https://developer.android.com/reference/android/media/MediaRecorder#setLocation(float,%20float)" rel="nofollow noreferrer"><strong>setLocation</strong></a>" , which is used to indicate where the recording has started (geographically), and looking at its code, I can see it sets parameters:</p>

<pre><code>public void setLocation(float latitude, float longitude) {
    int latitudex10000  = (int) (latitude * 10000 + 0.5);
    int longitudex10000 = (int) (longitude * 10000 + 0.5);

    if (latitudex10000 &gt; 900000 || latitudex10000 &lt; -900000) {
        String msg = "Latitude: " + latitude + " out of range.";
        throw new IllegalArgumentException(msg);
    }
    if (longitudex10000 &gt; 1800000 || longitudex10000 &lt; -1800000) {
        String msg = "Longitude: " + longitude + " out of range";
        throw new IllegalArgumentException(msg);
    }

    setParameter("param-geotag-latitude=" + latitudex10000);
    setParameter("param-geotag-longitude=" + longitudex10000);
}
</code></pre>

<p>But <code>setParameter</code> is private, and I'm not sure if it's ok to put anything I want into it, even if I had a way to access it (reflection, for example) :</p>

<pre><code>private native void setParameter(String nameValuePair);
</code></pre>

<p>I also don't get, given an audio/video file, how to get/modify this kind of information. It's not available for <a href="https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/SimpleExoPlayer.html" rel="nofollow noreferrer"><strong>SimpleExoPlayer</strong></a>, for example.</p>

<h2>The questions</h2>

<ol>
<li><p>How can I read,write, and modify metadata inside supported audio files of Android? </p></li>
<li><p>Are there any limitations/restrictions for those actions? </p></li>
<li><p>Which file formats are available for this?</p></li>
<li><p>Is it possible to add the metadata while I record the audio?</p></li>
<li><p>Is it possible perhaps via MediaStore ? But then how do I do those operations? And which files are supported? And does the metadata stay within the file?</p></li>
</ol>

<hr>

<p>EDIT: ok I've looked at the solution offered to me (<a href="https://stackoverflow.com/a/55401252/878126"><strong>here</strong></a>, repo <a href="https://github.com/dmide/AudioMetadataRecorder" rel="nofollow noreferrer"><strong>here</strong></a>, based on <a href="https://github.com/sannies/mp4parser/tree/master/examples/src/main/java/org/mp4parser/examples/metadata" rel="nofollow noreferrer"><strong>here</strong></a>) , and it seems to work well. However, it doesn't work on latest version of the library that it uses (<code>org.mp4parser.isoparser:1.9.37</code> dependency of <a href="https://github.com/sannies/mp4parser" rel="nofollow noreferrer"><strong>mp4parser</strong></a>) , so I leave this question to be answered : Why doesn't it work on latest version of this library?</p>

<p>Code:</p>

<pre><code>object MediaMetaDataUtil {
    interface PrepareBoxListener {
        fun prepareBox(existingBox: Box?): Box
    }

    @WorkerThread
    fun &lt;T : Box&gt; readMetadata(mediaFilePath: String, boxType: String): T? {
        return try {
            val isoFile = IsoFile(FileDataSourceImpl(FileInputStream(mediaFilePath).channel))
            val nam = Path.getPath&lt;T&gt;(isoFile, "/moov[0]/udta[0]/meta[0]/ilst/$boxType")
            isoFile.close()
            nam
        } catch (e: Exception) {
            null
        }
    }

    /**
     * @param boxType the type of the box. Example is "©nam" (AppleNameBox.TYPE). More available here: https://kdenlive.org/en/project/adding-meta-data-to-mp4-video/
     * @param listener used to prepare the existing or new box
     * */
    @WorkerThread
    @Throws(IOException::class)
    fun writeMetadata(mediaFilePath: String, boxType: String, listener: PrepareBoxListener) {
        val videoFile = File(mediaFilePath)
        if (!videoFile.exists()) {
            throw FileNotFoundException("File $mediaFilePath not exists")
        }
        if (!videoFile.canWrite()) {
            throw IllegalStateException("No write permissions to file $mediaFilePath")
        }
        val isoFile = IsoFile(mediaFilePath)
        val moov = isoFile.getBoxes&lt;MovieBox&gt;(MovieBox::class.java)[0]
        var freeBox = findFreeBox(moov)
        val correctOffset = needsOffsetCorrection(isoFile)
        val sizeBefore = moov.size
        var offset: Long = 0
        for (box in isoFile.boxes) {
            if ("moov" == box.type) {
                break
            }
            offset += box.size
        }
        // Create structure or just navigate to Apple List Box.
        var userDataBox: UserDataBox? = Path.getPath(moov, "udta")
        if (userDataBox == null) {
            userDataBox = UserDataBox()
            moov.addBox(userDataBox)
        }
        var metaBox: MetaBox? = Path.getPath(userDataBox, "meta")
        if (metaBox == null) {
            metaBox = MetaBox()
            val hdlr = HandlerBox()
            hdlr.handlerType = "mdir"
            metaBox.addBox(hdlr)
            userDataBox.addBox(metaBox)
        }
        var ilst: AppleItemListBox? = Path.getPath(metaBox, "ilst")
        if (ilst == null) {
            ilst = AppleItemListBox()
            metaBox.addBox(ilst)
        }
        if (freeBox == null) {
            freeBox = FreeBox(128 * 1024)
            metaBox.addBox(freeBox)
        }
        // Got Apple List Box
        var nam: Box? = Path.getPath(ilst, boxType)
        nam = listener.prepareBox(nam)
        ilst.addBox(nam)
        var sizeAfter = moov.size
        var diff = sizeAfter - sizeBefore
        // This is the difference of before/after
        // can we compensate by resizing a Free Box we have found?
        if (freeBox.data.limit() &gt; diff) {
            // either shrink or grow!
            freeBox.data = ByteBuffer.allocate((freeBox.data.limit() - diff).toInt())
            sizeAfter = moov.size
            diff = sizeAfter - sizeBefore
        }
        if (correctOffset &amp;&amp; diff != 0L) {
            correctChunkOffsets(moov, diff)
        }
        val baos = BetterByteArrayOutputStream()
        moov.getBox(Channels.newChannel(baos))
        isoFile.close()
        val fc: FileChannel = if (diff != 0L) {
            // this is not good: We have to insert bytes in the middle of the file
            // and this costs time as it requires re-writing most of the file's data
            splitFileAndInsert(videoFile, offset, sizeAfter - sizeBefore)
        } else {
            // simple overwrite of something with the file
            RandomAccessFile(videoFile, "rw").channel
        }
        fc.position(offset)
        fc.write(ByteBuffer.wrap(baos.buffer, 0, baos.size()))
        fc.close()
    }

    @WorkerThread
    @Throws(IOException::class)
    fun splitFileAndInsert(f: File, pos: Long, length: Long): FileChannel {
        val read = RandomAccessFile(f, "r").channel
        val tmp = File.createTempFile("ChangeMetaData", "splitFileAndInsert")
        val tmpWrite = RandomAccessFile(tmp, "rw").channel
        read.position(pos)
        tmpWrite.transferFrom(read, 0, read.size() - pos)
        read.close()
        val write = RandomAccessFile(f, "rw").channel
        write.position(pos + length)
        tmpWrite.position(0)
        var transferred: Long = 0
        while (true) {
            transferred += tmpWrite.transferTo(0, tmpWrite.size() - transferred, write)
            if (transferred == tmpWrite.size())
                break
            //System.out.println(transferred);
        }
        //System.out.println(transferred);
        tmpWrite.close()
        tmp.delete()
        return write
    }

    @WorkerThread
    private fun needsOffsetCorrection(isoFile: IsoFile): Boolean {
        if (Path.getPath&lt;Box&gt;(isoFile, "moov[0]/mvex[0]") != null) {
            // Fragmented files don't need a correction
            return false
        } else {
            // no correction needed if mdat is before moov as insert into moov want change the offsets of mdat
            for (box in isoFile.boxes) {
                if ("moov" == box.type) {
                    return true
                }
                if ("mdat" == box.type) {
                    return false
                }
            }
            throw RuntimeException("I need moov or mdat. Otherwise all this doesn't make sense")
        }
    }

    @WorkerThread
    private fun findFreeBox(c: Container): FreeBox? {
        for (box in c.boxes) {
            //            System.err.println(box.type)
            if (box is FreeBox)
                return box
            if (box is Container) {
                val freeBox = findFreeBox(box as Container)
                if (freeBox != null) {
                    return freeBox
                }
            }
        }
        return null
    }

    @WorkerThread
    private fun correctChunkOffsets(movieBox: MovieBox, correction: Long) {
        var chunkOffsetBoxes = Path.getPaths&lt;ChunkOffsetBox&gt;(movieBox as Box, "trak/mdia[0]/minf[0]/stbl[0]/stco[0]")
        if (chunkOffsetBoxes.isEmpty())
            chunkOffsetBoxes = Path.getPaths(movieBox as Box, "trak/mdia[0]/minf[0]/stbl[0]/st64[0]")
        for (chunkOffsetBox in chunkOffsetBoxes) {
            val cOffsets = chunkOffsetBox.chunkOffsets
            for (i in cOffsets.indices)
                cOffsets[i] += correction
        }
    }

    private class BetterByteArrayOutputStream : ByteArrayOutputStream() {
        val buffer: ByteArray
            get() = buf
    }

}
</code></pre>

<p>Sample usage for writing&amp;reading title:</p>

<pre><code>object MediaMetaData {
    @JvmStatic
    @Throws(IOException::class)
    fun writeTitle(mediaFilePath: String, title: String) {
        MediaMetaDataUtil.writeMetadata(mediaFilePath, AppleNameBox.TYPE, object : MediaMetaDataUtil.PrepareBoxListener {
            override fun prepareBox(existingBox: Box?): Box {
                var nam: AppleNameBox? = existingBox as AppleNameBox?
                if (nam == null)
                    nam = AppleNameBox()
                nam.dataCountry = 0
                nam.dataLanguage = 0
                nam.value = title
                return nam
            }
        })
    }

    @JvmStatic
    fun readTitle(mediaFilePath: String): String? {
        return MediaMetaDataUtil.readMetadata&lt;AppleNameBox&gt;(mediaFilePath, AppleNameBox.TYPE)?.value
    }
}
</code></pre>