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

时间:2016-04-07 10:23:41

标签: android audio metadata audio-recording

背景

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

问题

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

  2. 这些行为是否存在任何限制/限制?

  3. 可以使用哪种文件格式?

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

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

  6. 编辑:好的我已经查看了提供给我的解决方案(here,回复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
        }
    }
    

1 个答案:

答案 0 :(得分:2)

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

MP3是最受欢迎的一种,应该有很多选择,例如this one

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

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

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

更新

我使用an example appthis MP4 parser library编译了MediaRecorder example from SDK docs。它记录音频,将其放入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