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

查看:37
本文介绍了如何获取和修改元数据以支持 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?

<小时>

好的,我已经查看了提供给我的解决方案(此处,回购 此处,基于 这里) ,它似乎运作良好.但是,它不适用于它使用的最新版本的库(org.mp4parser.isoparser:1.9.37 依赖于 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 解析器库 和 SDK 文档中的 MediaRecorder 示例.它记录音频,将其放入 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_NBMediaRecorder.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天全站免登陆