如何在Android上将元数据获取和修改为支持的音频文件? [英] How to get&modify metadata to supported audio files on Android?

查看:117
本文介绍了如何在Android上将元数据获取和修改为支持的音频文件?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

Android支持 各种音频文件 编码和解码.

Android supports various audio files encoding and decoding.

我使用 android.media将音频录制到音频文件中. MediaRecorder 类,但是我也希望显示有关已记录文件的信息(不是标准数据,而是纯文本,甚至可由用户配置),我认为最好将其存储文件中的信息.

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

MediaRecorder类没有任何我可以找到的功能,无法添加甚至读取已录制音频文件的元数据.

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.

我尝试搜索特定文件类型的操作方法,还尝试找到一个可以执行此操作的库.

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.

我为MediaRecorder类找到的唯一东西是一个名为" setLocation ",它用于指示录制从何处开始(地理上),并查看其代码,可以看到它设置了参数:

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);
}

但是setParameter是私有的,我不确定是否可以将我想要的任何内容放入其中,即使我有访问它的方式(例如,反射):

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);

考虑到音频/视频文件,我也不会获得如何获取/修改此类信息.它不适用于 SimpleExoPlayer .

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.

  1. 如何读取,写入和修改Android支持的音频文件中的元数据?

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

这些操作是否有任何限制/限制?

Are there any limitations/restrictions for those actions?

可以使用哪些文件格式?

Which file formats are available for this?

在录制音频时是否可以添加元数据?

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

是否有可能通过MediaStore?但是那我该怎么做呢?以及支持哪些文件?元数据是否保留在文件中?

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?


确定,我已经查看了提供给我的解决方案( 此处 ,根据 此处 .com/sannies/mp4parser/tree/master/examples/src/main/java/org/mp4parser/examples/metadata"rel =" nofollow noreferrer> 此处 ),然后似乎运作良好.但是,它不适用于它使用的库的最新版本(org.mp4parser.isoparser:1.9.37依赖项rel ="nofollow noreferrer"> mp4parser ),所以我要回答这个问题:为什么它不适用于该库的最新版本?


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?

代码:

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中所有受支持的音频格式进行此操作.不过,对于特定格式,有些选项是有限的,所以我建议坚持使用一种格式.

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是最受欢迎的一种,应该有很多选项,例如

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

如果您不想处理编码/解码,则有 WAV格式的一些选项.

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

还有一种将元数据轨道添加到MP4容器的方法使用MediaMuxer (您可以拥有纯音频MP4文件)或像这样

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.

关于MediaStore:这是一个示例(位于第318页的末尾),介绍如何在使用MediaRecorder之后立即向其中添加元数据.尽管据我所知,数据不会记录在文件中.

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.

更新

我使用示例应用 sannies/mp4parser"rel =" nofollow noreferrer>此MP4解析器库和

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);

更新#2

关于m4a文件扩展名: m4a只是mp4文件的别名带有AAC音频,并且具有相同的文件格式.因此,您可以使用我上面的示例应用程序,只需将文件名从audiorecordtest.mp4更改为audiorecordtest.m4a,并将音频编码器从MediaRecorder.AudioEncoder.AMR_NB更改为MediaRecorder.AudioEncoder.AAC.

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上将元数据获取和修改为支持的音频文件?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆