如何在 Android 上获取和修改元数据以支持音频文件?

Posted

技术标签:

【中文标题】如何在 Android 上获取和修改元数据以支持音频文件?【英文标题】:How to get&modify metadata to supported audio files on Android? 【发布时间】:2016-04-07 10:23:41 【问题描述】:

背景

android 支持various audio files 编码和解码。

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

可能要存储的数据示例:记录时间、记录地点、用户注释...

问题

MediaRecorder 类没有我能找到的任何函数,用于添加甚至读取录制的音频文件的元数据。

我也找不到类似的课程。

我尝试过的

我尝试搜索如何针对特定文件类型执行此操作,并且还尝试找到可以执行此操作的库。

我什至没有找到有关此信息的线索。

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

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 是私有的,我不确定是否可以将我想要的任何内容放入其中,即使我有办法访问它(例如反射):

private native void setParameter(String nameValuePair);

对于音频/视频文件,我也不知道如何获取/修改此类信息。例如,它不适用于SimpleExoPlayer

问题

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

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

    可以使用哪些文件格式?

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

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


编辑:好的,我查看了提供给我的解决方案(here,repo here,基于here),它似乎运作良好。但是,它不适用于它使用的最新版本的库(org.mp4parser.isoparser:1.9.37 依赖于 mp4parser),所以我留下这个问题有待回答:为什么它不适用于这个库的最新版本?

代码:

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
    


写作和阅读标题的示例用法:

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
    

【问题讨论】:

MediaMetadataRetriever 能解决您的问题吗? developer.android.com/reference/android/media/…顺便说一句,要搜索的关键字是ID3 @LukasKnuth ID3 不是仅用于 MP3,仅在 Android 上支持解码?我需要这个来录制,所以我需要使用一种可以读取和修改其元数据的格式,还需要使用录制器进行编码。另外,如何将 MediaMetadataRetriever 用于这两个任务? 【参考方案1】:

似乎没有办法对 Android 中所有支持的音频格式进行统一处理。但是对于特定格式有一些有限的选项,所以我建议坚持使用一种格式。

MP3 是最受欢迎的,应该有很多选择,比如this one。

如果不想处理编码/解码,有some options for a WAV format。

还有一种方法可以将元数据轨道添加到 MP4 容器 using MediaMuxer(您可以拥有纯音频 MP4 文件)或 like this。

关于 MediaStore:here's an example(在第 318 页末尾)关于如何在使用 MediaRecorder 之后向其中添加元数据。虽然据我所知,数据不会记录在文件中。

更新

我使用this MP4 parser library 和MediaRecorder example from SDK docs 编译了an example app。它记录一个音频,将其放入 MP4 容器中,然后像这样添加 String 元数据:

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

然后在下一次应用启动时读取并显示此元数据:

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

更新 #2

关于 m4a 文件扩展名:m4a is just an alias for an mp4 file with AAC audio and has the same file format。所以你可以使用我上面的示例应用程序,只需将文件名从 audiorecordtest.mp4 更改为 audiorecordtest.m4a 并将音频编码器从 MediaRecorder.AudioEncoder.AMR_NB 更改为 MediaRecorder.AudioEncoder.AAC

【讨论】:

AMR、M4A 等呢? MP3 是个问题,因为它不支持编码(包括录音),不是吗? 是的,遗憾的是没有对 MP3 编码的开箱即用支持,仅用于解码。编码/解码都支持 AMR 和 AAC。 MediaMuxer 还支持 3gp,因此您可以像使用 MP4 一样向其添加元数据轨道。我不确定 MediaMuxer 是否支持 M4A。 事实证明 MediaMuxer 仅支持 MP4 用于元数据轨道。此轨道的预期用途是用于绑定到音频/视频轨道时间戳的连续数据,因此虽然从技术上讲您可以编写任意数据(例如某些字符串),但它会很丑陋并且不受任何其他元数据阅读器的支持。我将添加另一个使用 MP4 解析器库的示例。 我不确定,但我自己在尝试一些 1.9.* 版本时遇到了这些问题。据我所知,没有。 肯定不在 Android SDK 中。还有其他开源库,但在我看来,这个似乎拥有最好的 API。

以上是关于如何在 Android 上获取和修改元数据以支持音频文件?的主要内容,如果未能解决你的问题,请参考以下文章

如何在同一网络上以编程方式获取其他支持Wifi的设备的IP地址?

Win11承诺的支持安卓App终于更新了!大神教你如何在国区使用,上班刷抖音不是梦...

如何以编程方式获取用户在 Android OS 配置上设置的数据使用限制?

任何现代文件系统都支持任意元数据处理吗?

如何在 Android 中获取视频的位置数据?

使用 PHP 从音频流中提取音轨信息