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

2023-12-07

背景

安卓支持各种音频文件编码和解码。

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

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

问题

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

我也找不到类似的课程。

我尝试过的

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

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

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

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

private native void setParameter(String nameValuePair);

给定音频/视频文件,我也不知道如何获取/修改此类信息。它不适用于简单Exo播放器, 例如。

问题

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

  2. 这些行动有任何限制/限制吗?

  3. 哪些文件格式可用于此?

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

  5. 是否有可能通过 MediaStore ?但那么我该如何进行这些操作呢?支持哪些文件?元数据是否保留在文件中?


编辑:好的,我已经查看了提供给我的解决方案(here, repo here, 基于here),而且看起来效果很好。但是,它不适用于它使用的最新版本的库(org.mp4parser.isoparser:1.9.37的依赖性mp4解析器),所以我留下这个问题来回答:为什么它不能在这个库的最新版本上工作?

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
    }

}

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

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 中所有支持的音频格式进行统一处理。不过,特定格式的选项有限,因此我建议坚持使用一种格式。

MP3是最流行的一种,应该有很多选择,比如this one.

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

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

关于媒体商店:这是一个例子(第 318 页末尾)介绍如何在使用 MediaRecorder 后立即向其添加元数据。但据我所知,数据不会记录在文件中。

Update

我编译了一个示例应用程序 using 这个 MP4 解析器库 and SDK 文档中的 MediaRecorder 示例。它录制音频,将其放入 MP4 容器中并添加字符串元数据,如下所示:

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 只是带有 AAC 音频的 mp4 文件的别名,具有相同的文件格式。因此,您可以使用我上面的示例应用程序,只需将文件名更改为audiorecordtest.mp4 to audiorecordtest.m4a并更改音频编码器MediaRecorder.AudioEncoder.AMR_NB to MediaRecorder.AudioEncoder.AAC.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

随机推荐

  • 始终在 console.log 中显示 html 元素 (Chromium)

    我有一个 js 脚本console log有时会打印 html 元素 Chrome 有两种打印此类 DOM 元素的模式 在 html 样式中 例如 div class abc div 其中悬停会突出显示页面中的元素 然后单击会打开 DOM
  • iPhone 5 + iOS6 如何决定应用程序是否必须在信箱模式下运行

    iOS6 如何决定应用程序是否必须在 iPhone 5 上以信箱 兼容模式运行 这是构建设置参数 如 目标设备系列 吗 或者所有基于最新 SDK 构建的应用程序都必须支持 iPhone 5 屏幕尺寸 或者应用程序将以信箱模式运行 是否没有
  • 如何在 makefile 中使用 LDFLAGS

    我是 Linux 操作系统的新手 我正在尝试编译一个 c使用 makefile 文件 必须链接数学库 我的 makefile 如下所示 CC gcc CFLAGS Wall lm all client PHONY clean clean r
  • 如何连接两个不同形状的张量

    我有两个张量 get shape 400 和 1176 我想将它们连接成一个大小为 1576 的张量 我尝试了 concat 但它要求两者具有相同的维度 怎么办呢 希望您通过批量大小传递相同维度的输入 import tensorflow a
  • 指针前一变量

    据我所知 在 C 中检查指针是否是数组末尾的一个元素是完全合法的 如下所示 char arr 16 for char ptr arr ptr arr sizeof arr sizeof arr 0 ptr some code 我的问题是这样
  • MSbuild 任务失败,因为“任何 CPU”解决方案的构建顺序不正确

    我在 Teambuild 中构建两种解决方案 一种是应用程序本身 另一种是 WiX 安装程序 我想使用 任何 CPU 构建配置来构建应用程序 并使用 x86 来构建安装程序 我在项目文件中首先列出了 任何 CPU 解决方案 但 Teambu
  • pandas 按两列值过滤行,不区分大小写

    我有一个简单的数据框 如下所示 Last Known Date ConfigredValue ReferenceValue 0 24 Jun 17 False FALSE 1 25 Jun 17 FALSE FALSE 2 26 Jun 1
  • 正则表达式 WORD 的最后一个字符

    我正在尝试匹配单词中的最后一个字符 WORD 是非空白字符的序列 n r t f 或匹配 的空行 我为此所做的表达是 n t r f n t r f 正则表达式匹配空白字符或行尾后面的非空白字符 但我不知道如何阻止它从结果中排除以下空白字符
  • Swift 计算属性不能在 init 中使用?

    我正在尝试将 MultipeerConnectivity 框架与 Swift 一起使用 我有以下属性 var peerId MCPeerID let advertiser MCNearbyServiceAdvertiser let brow
  • 混合服务器端 java 和客户端 javascript 图表库?

    是否有一个库可以用 Java 和 Javascript 为相同的数据生成相同的图表 我的任务是 显示一些华丽的动态图表在浏览器中 更好地使用动态工具提示 缩放等 我在这里考虑Javascript 并在服务器端用 Java 生成相同 或足够相
  • 为什么 (Excel VBA) 组合框更改事件每次引用其属性之一时都会触发?

    我是这个论坛的第一次用户 这是我的场景 在用户表单上 我有一个组合框 两个文本框和一个 确定 按钮 当用户从组合框的下拉列表中进行选择时 组合框的更改事件将被触发 事件处理代码将根据用户的选择使用工作表中的信息填充文本框 然后 用户可以编辑
  • 从字符串中获取价格值

    我有一个字符串 其中价格值 544 50 可以位于字符串中的任何位置 例如 HP G60 630US 笔记本电脑 笔记本电脑 RadioShack com gt 259 97 radioshack com 我需要从字符串中获取值 259 9
  • 类型错误:“dict_keys”对象不支持索引

    def shuffle self x random None int int x random random random gt shuffle list x in place return None Optional arg random
  • 使用 PHPMailer 和 GMAIL SMTP 发送电子邮件

    我已经阅读了网络上的每个示例 但似乎仍然无法连接到 GMAIL SMTP 这是我正在运行的代码 include phpMailer class phpmailer php path to the PHPMailer class mail n
  • 多个枚举的国际化(枚举值的翻译)[重复]

    这个问题在这里已经有答案了 再次讨论过的事情before我想分享 我的 解决方案并寻求增强功能 其他方法或最佳实践 我有几个需要国际化的枚举 我需要将枚举值翻译成某些语言以便在 jsf 页面中显示它们 示例枚举 public enum Tr
  • java.io.IOException:收到的身份验证质询为空

    我需要获取响应代码 但它抛出 IOException 我不知道怎么了 try url new URL urlBuilder toString conn HttpURLConnection url openConnection conn se
  • 如何将 UCS2 字符串转换为 UTF8?

    如何将 UCS2 每个字符 2 个字节 的字符串转换为 Ruby 中的 UTF8 字符串 你应该调查一下iconv 它是 Ruby 标准库的一部分 它是为这项任务而设计的 具体来说 Iconv iconv utf 8 utf 16 str
  • 如何在子窗口出现时通过单击父窗口上的按钮来禁用父窗口

    我在jsp中设计了一个窗口 其中有一个搜索按钮 当用户单击 搜索 按钮时 会出现新窗口 但此时我希望我的父窗口被禁用 这样当用户尝试在父窗口上执行任何操作时 在 搜索 表单关闭之前 它不应该允许 如果有人知道如何执行此操作 请告诉我 我已经
  • MySQL 插入后和更新后触发

    我有两张表 其中一张名为att如下 CREATE TABLE att SID varchar 50 NOT NULL CID varchar 50 NOT NULL Date date NOT NULL H1 varchar 1 NOT N
  • 如何获取和修改 Android 上支持的音频文件的元数据?

    背景 安卓支持各种音频文件编码和解码 我使用将音频录制到音频文件中android media MediaRecorder类 但我也希望显示有关我记录的文件的信息 不是标准数据 但仍然只是文本 甚至可能由用户配置 并且我认为最好将此信息存储在