3

Background

Android supports various audio files encoding and decoding.

I record audio into an audio file using android.media.MediaRecorder 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.

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

The problem

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

I also can't find a similar class that does it.

What I've tried

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

I haven't find even a clue about this information.

The only thing I've found for MediaRecorder class, is a function called "setLocation" , which is used to indicate where the recording has started (geographically), and looking at its code, I can see it sets parameters:

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

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

    setParameter("param-geotag-latitude=" + latitudex10000);
    setParameter("param-geotag-longitude=" + longitudex10000);
}

But setParameter 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) :

private native void setParameter(String nameValuePair);

I also don't get, given an audio/video file, how to get/modify this kind of information. It's not available for SimpleExoPlayer, for example.

The questions

  1. How can I read,write, and modify metadata inside supported audio files of Android?

  2. Are there any limitations/restrictions for those actions?

  3. Which file formats are available for this?

  4. Is it possible to add the metadata while I record the audio?

  5. 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?


EDIT: ok I've looked at the solution offered to me (here, repo here, based on here) , and it seems to work well. However, it doesn't work on latest version of the library that it uses (org.mp4parser.isoparser:1.9.37 dependency of mp4parser) , so I leave this question to be answered : Why doesn't it work on latest version of this library?

Code:

object MediaMetaDataUtil {
    interface PrepareBoxListener {
        fun prepareBox(existingBox: Box?): Box
    }

    @WorkerThread
    fun <T : Box> readMetadata(mediaFilePath: String, boxType: String): T? {
        return try {
            val isoFile = IsoFile(FileDataSourceImpl(FileInputStream(mediaFilePath).channel))
            val nam = Path.getPath<T>(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<MovieBox>(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() > diff) {
            // either shrink or grow!
            freeBox.data = ByteBuffer.allocate((freeBox.data.limit() - diff).toInt())
            sizeAfter = moov.size
            diff = sizeAfter - sizeBefore
        }
        if (correctOffset && 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<Box>(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<ChunkOffsetBox>(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
    }

}

Sample usage for writing&reading title:

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<AppleNameBox>(mediaFilePath, AppleNameBox.TYPE)?.value
    }
}
android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • Does `MediaMetadataRetriever` solve your problem? http://developer.android.com/reference/android/media/MediaMetadataRetriever.html Btw, the keyword to search for is `ID3` – Lukas Knuth Apr 07 '16 at 13:59
  • @LukasKnuth Isn't ID3 only for MP3, which is only supported on Android as being decoded ? I need this for the recording, so I need to use a format that I can read and modify its metadata, and also encode using the recorder. Also, how do I use MediaMetadataRetriever for both tasks? – android developer Apr 07 '16 at 20:13

1 Answers1

3

It seems there's no way to do it uniformly for all supported audio formats in Android. There are some limited options for particular formats though, so I suggest to stick with one format.

MP3 is the most popular one and there should be a lot of options like this one.

If you don't want to deal with encoding/decoding, there are some options for a WAV format.

There's also a way to add a metadata track to a MP4 container using MediaMuxer (you can have an audio-only MP4 file) or like this.

Regarding MediaStore: here's an example (at the end of page 318) on how to add metadata to it just after using MediaRecorder. Though as far as I know the data won't be recorded inside the file.

Update

I compiled an example app using this MP4 parser library and MediaRecorder example from SDK docs. It records an audio, puts it in MP4 container and adds String metadata like this:

MetaDataInsert cmd = new MetaDataInsert();
cmd.writeRandomMetadata(fileName, "lore ipsum tralalala");

Then on the next app launch this metadata is read and displayed:

MetaDataRead cmd = new MetaDataRead();
String text = cmd.read(fileName);
tv.setText(text);

Update #2

Regarding m4a file extension: m4a is just an alias for an mp4 file with AAC audio and has the same file format. So you can use my above example app and just change the file name from audiorecordtest.mp4 to audiorecordtest.m4a and change audio encoder from MediaRecorder.AudioEncoder.AMR_NB to MediaRecorder.AudioEncoder.AAC.

android developer
  • 114,585
  • 152
  • 739
  • 1,270
Dmide
  • 6,422
  • 3
  • 24
  • 31
  • What about AMR, M4A, and others ? MP3 is a problem because it's not supported for encoding (including recording) , no? – android developer Mar 28 '19 at 20:10
  • Yes, unfortunately there's no out of the box support for MP3-encoding, only for decoding. AMR and AAC are supported for both encoding/decoding. Also MediaMuxer supports 3gp, so you can add a metadata track to it just like with MP4. I'm not sure about M4A support by MediaMuxer. – Dmide Mar 28 '19 at 20:54
  • You can check for supported codecs and containers here: https://developer.android.com/guide/topics/media/media-formats – Dmide Mar 28 '19 at 20:56
  • The link you've provided is the same as I did in the question. I know about those. I just want to know how to add,read and modify the metadata. Please show in code how to do it for each of those files (or for some of them, if it's impossible to all). – android developer Mar 28 '19 at 21:42
  • 1
    Well it turns out MediaMuxer only supports MP4 for metadata track. This track intended use is for continuos data bound to audio/video tracks timestamps, so while technically you could write arbitrary data (some Strings for example), it will be ugly and unsupported by any other metadata readers. I will add another example using MP4 parser library. – Dmide Mar 29 '19 at 10:58
  • Audio files such as m4a aren't supported? – android developer Mar 29 '19 at 19:40
  • Can you please show how to read&write the metadata for m4a file? – android developer Mar 31 '19 at 14:20
  • Please show full code of what you mean. I don't see which code you refer to. You've put multiple links, and none of them seem to handle what I wrote. I also prefer not to use a third party library, especially if it has problematic license. – android developer Mar 31 '19 at 22:35
  • Dude, this is Q&A site, not a freelance board. If you actually invest some time to look at the app I put together for you and to carefully read instructions in the second update, you will be good to go. You can use any other opensource MP4 parser you like or parse it yourself, it's not that hard. Good luck. – Dmide Mar 31 '19 at 23:00
  • Again, I don't know which link you refer to. You've put multiple ones. If the link has full answer to the question, using the Android framework API and not using some third party library (especially with problematic license), and not just links, of course I will accept it. But I can't accept something that I can't test on my own and see it's indeed correct – android developer Apr 01 '19 at 07:04
  • If you talk about this repo: https://github.com/dmide/AudioMetadataRecorder , seems to work, but it uses old version of the library ( new one is 1.9.37) . Updating to the newest version, it won't add (or read) the metadata. But again, I prefer to use the Android framework API alone, if possible. Is this possible? – android developer Apr 01 '19 at 07:44
  • If you talk about this repo: github.com/dmide/AudioMetadataRecorder , seems to work, but it uses old version of the library ( new one is 1.9.37) . Updating to the newest version, it won't add (or read) the metadata. But again, I prefer to use the Android framework API alone, if possible. Is this possible? – android developer Apr 01 '19 at 08:26
  • OK, I've checked the sample further. I think it's good enough. However, I want to know why it doesn't work on latest version of the library. I will now grant the bounty, but will accept only after it got fixed. For now I'll update my question to include updated code based on what you've offered, because it's important. Please, try to use it. If you find why it doesn't work on newest version, update the answer. – android developer Apr 01 '19 at 09:57
  • Versions before 1.9.37 have [an issue with OutOfMemory](https://github.com/sannies/mp4parser/issues/351) while the latest 1.9.37 has [an issue with NoSuchMethodError](https://github.com/sannies/mp4parser/issues/323). Track the issue and use the earlier version for now. – Dmide Apr 01 '19 at 11:06
  • You say it doesn't work for me because of these issues? Is there alternative to those on any of the recent versions? – android developer Apr 01 '19 at 11:49
  • 1
    I'm not sure, but i got these problems myself when tried some 1.9.* versions. Not to my knowledge. – Dmide Apr 01 '19 at 12:21
  • ok, so using this library is the best we can use right now, right? There is nothing currently smaller or of the Android framework that does this, right? – android developer Apr 01 '19 at 12:27
  • 1
    Not in the Android SDK for sure. There are other opensource libraries, but this one seems to have the best API, in my opinion. – Dmide Apr 01 '19 at 12:41
  • OK I see. Thank you for your time. – android developer Apr 01 '19 at 12:50