【问题标题】:Why can I sometimes concatenate audio data using NodeJS Buffers, and sometimes I cannot?为什么有时可以使用 NodeJS 缓冲区连接音频数据,而有时却不能?
【发布时间】:2020-06-30 04:56:33
【问题描述】:

作为我正在进行的项目的一部分,需要将多段音频数据连接到一个大型音频文件中。音频文件由四个来源生成,各个文件存储在 Google Cloud 存储桶中。每个文件都是一个 mp3 文件,并且很容易验证每个单独的文件是否正确生成(单独的,我可以播放它们,在我最喜欢的软件中编辑它们等)。

为了将音频文件合并在一起,nodejs 服务器使用 axios POST 请求将文件从 Google Cloud 存储加载为数组缓冲区。从那里,它使用Buffer.from() 将每个数组缓冲区放入一个节点缓冲区,所以现在我们有一个缓冲区对象数组。然后它使用Buffer.concat() 将 Buffer 对象连接成一个大 Buffer,然后我们将其转换为 Base64 数据并发送到客户端服务器。

这很酷,但是在连接从不同来源生成的音频时会出现问题。我上面提到的 4 个来源是 Text to Speech 软件平台,例如 Google Cloud Voice 和 Amazon Polly。具体来说,我们有来自 Google Cloud Voice、Amazon Polly、IBM Watson 和 Microsoft Azure Text to Speech 的文件。基本上只有五个文本到语音的解决方案。同样,所有单独的文件都可以工作,但是通过这种方法将它们连接在一起时会产生一些有趣的效果。

当声音文件被连接时,似乎取决于它们来自哪个平台,声音数据要么会包含在最终的声音文件中,要么不会包含在最终的声音文件中。以下是基于我的测试的“兼容性”表:

|------------|--------|--------|-----------|-----|
| Platform / | Google | Amazon | Microsoft | IBM |
|------------|--------|--------|-----------|-----|
| Google     | Yes    | No     | No        | No  |
|------------|--------|--------|-----------|-----|
| Amazon     |        | No     | No        | Yes |
|------------|--------|--------|-----------|-----|
| Microsoft  |        |        | Yes       | No  |
|------------|--------|--------|-----------|-----|
| IBM        |        |        |           | Yes |
|------------|--------|--------|-----------|-----|

效果如下:当我播放大输出文件时,它总是开始播放包含的第一个声音文件。从那里,如果下一个声音文件兼容,就会听到,否则会完全跳过(没有空声音或任何东西)。如果它被跳过,则该文件的“长度”(例如 10 秒长的音频文件)包含在生成的输出声音文件的末尾。但是,当我的音频播放器到达播放最后一个“兼容”音频的位置时,它会立即跳到结尾。

作为一个场景:

Input:
sound1.mp3 (3s) -> Google
sound2.mp3 (5s) -> Amazon
sound3.mp3 (7s)-> Google
sound4.mp3 (11s) -> IBM

Output:
output.mp3 (26s) -> first 10s is sound1 and sound3, last 16s is skipped.

在这种情况下,输出声音文件的长度为 26 秒。在前 10 秒,您会听到 sound1.mp3sound3.mp3 连续播放。然后在 10 秒时(至少在 Firefox 中播放这个 mp3 文件),播放器在 26 秒时立即跳到结尾。

我的问题是:有没有人知道为什么有时我可以以这种方式连接音频数据,而其他时候却不能?怎么会在输出文件的末尾包含这个“缺失”的数据?如果它适用于某些情况,那么连接二进制数据是否应该在所有情况下都有效,因为所有文件都具有 mp3 编码?如果我错了,请告诉我我可以做些什么来成功连接任何 mp3 文件:) 我可以提供我的nodeJS后端代码,但是使用的过程和方法如上所述。

感谢阅读?

【问题讨论】:

    标签: node.js audio concatenation buffer mp3


    【解决方案1】:

    问题的潜在来源

    采样率

    44.1 kHz 通常用于音乐,因为它用于 CD 音频。 48 kHz 通常用于视频,因为它是用于 DVD 的。这两种采样率都远高于语音所需的采样率,因此您的各种文本转语音提供商可能会输出不同的内容。 22.05 kHz(44.1 kHz 的一半)很常见,11.025 kHz 也很常见。

    虽然每个帧都指定了自己的采样率,从而可以生成具有不同采样率的流,但我从未见过解码器尝试在流中切换采样率。我怀疑解码器正在跳过这些帧,或者甚至可能跳过任意块,直到它再次获得一致的数据。

    使用FFmpeg(或FFprobe)之类的东西来确定文件的采样率是多少:

    ffmpeg -i sound2.mp3
    

    你会得到这样的输出:

    Duration: 00:13:50.22, start: 0.011995, bitrate: 192 kb/s
      Stream #0:0: Audio: mp3, 44100 Hz, stereo, fltp, 192 kb/s
    

    在本例中,44.1 kHz 是采样率。

    频道数

    我希望您的语音 MP3 是单声道的,但检查确定不会有什么坏处。和上面一样,检查 FFmpeg 的输出。在我上面的例子中,它说stereo

    与采样率一样,从技术上讲,每一帧都可以指定自己的通道数,但我不知道有哪个播放器会在中途切换通道数。因此,如果要连接,则需要确保所有通道数都相同。

    ID3 标签

    ID3 metadata 在文件的开头 (ID3v2) 和/或结尾 (ID3v1) 很常见。不太期望在中途拥有这些数据。您需要确保在连接之前将这些元数据全部删除。

    MP3 位储器

    MP3 帧不一定是独立的。如果您有一个恒定的比特率流,编码器可能仍然使用较少的数据来编码一帧,而使用更多的数据来编码另一帧。发生这种情况时,某些帧包含其他帧的数据。这样,可以从额外带宽中受益的帧可以获得它,同时仍然将整个流安装在恒定比特率内。这就是“比特水库”。

    如果您剪切一个流并拼接到另一个流中,您可能会拆分一个帧及其相关帧。这通常会导致音频故障,但也可能导致解码器向前跳过。一些表现不佳的解码器将完全停止播放。在你的例子中,你没有削减任何东西,所以这可能不是你的麻烦的根源......但我在这里提到它是因为它绝对与你处理这些流的方式有关。

    另请参阅:http://wiki.hydrogenaud.io/index.php?title=Bit_reservoir

    解决方案

    选择“正常”格式,对不符合要求的文件重新采样和重新编码

    如果您的大多数来源都是完全相同的格式并且只有一两个未完成,您可以转换不合格的文件。从那里,从所有内容中剥离 ID3 标签并连接起来。

    要进行转换,我建议将其作为child process 转移到FFmpeg。

    child_process.spawn('ffmpeg' [
      // Input
      '-i', inputFile, // Use '-' to write to STDIN instead
    
      // Set sample rate
      '-ar', '44100',
    
      // Set audio channel count
      '-ac', '1',
    
      // Audio bitrate... try to match others, but not as critical
      '-b:a', '64k',
    
      // Ensure we output an MP3
      '-f', 'mp3',
    
      // Output
      outputFile // As with input, use '-' to write to STDOUT
    ]);
    

    最佳解决方案:让 FFmpeg(或类似的)为您完成工作

    解决所有这些问题的最简单、最强大的解决方案是让 FFmpeg 为您构建一个全新的流。这将导致您的音频文件被解码为 PCM,并生成一个新的流。您可以添加参数以重新采样这些输入,并在需要时修改通道数。然后输出一个流。使用concat filter

    这样,您可以接受任何类型的音频文件,您不必编写代码来将这些流混合在一起,并且一旦设置您就不必担心。

    唯一的缺点是它需要对所有内容进行重新编码,这意味着另一代质量会丢失。无论如何,这对于任何不符合要求的文件都是必需的,而且只是语音,所以我不会再考虑了。

    【讨论】:

    • 感谢您的回答!在过去的几天里,我一直在尝试您的建议。首先,我尝试了让 FFMpeg 处理所有事情的解决方案,但是因为在我的情况下,输入文件存储在 Google Cloud Storage 上,所以我们将文件存储在内存中而不是服务器 HD 上。不幸的是,因为 ffmpeg 只有标准输入来支持从内存中读取(从我能找到的),它支持一个输入流而不是多个。因此,我正在尝试第一个建议(转换每个文件然后连接)。当我有结果时我会报告结果!再次感谢!
    • @Japser36 很酷,很高兴你能成功。请注意,FFmpeg 可以读取 HTTP(S) URL 作为输入,因此您始终可以从 Cloud Storage 方向对 URL 进行签名,并根据需要用作输入。
    【解决方案2】:

    @Brad 的答案就是解决方案!他建议的第一个解决方案奏效了。让 FFMpeg 正常工作需要一些麻烦,但最终使用 fluent-ffmpeg 库工作。

    在我的案例中,每个文件都存储在 Google Cloud Storage 上,而不是服务器的硬盘上。这给 FFmpeg 带来了一些问题,因为它需要文件路径有多个文件,或者一个输入流(但只支持一个,因为只有一个 STDIN)。

    一种解决方案是将文件临时放在硬盘上,但这不适用于我们的用例,因为我们可能会大量使用此功能并且硬盘会增加延迟。

    因此,我们按照建议进行操作,并将每个文件加载到 ffmpeg 中,以将其转换为标准化格式。这有点棘手,但最终将每个文件请求为流,使用该流作为 ffmpeg 的输入,然后使用 fluent-ffmpeg 的 pipe() 方法(返回一个流)作为输出工作。

    然后,我们将事件侦听器绑定到此管道的“数据”事件,并将数据推送到数组 (bufs.push(data)),在流“结束”上,我们使用 Buffer.concat(bufs) 连接此数组,然后是承诺解决。 然后,一旦所有请求的承诺都得到解决,我们就可以确定 ffmpeg 已经处理了每个文件,然后像以前一样使用Buffer.concat() 将这些缓冲区连接到所需的组中,转换为 base64 数据,然后发送到客户端。

    这很好用,现在它似乎能够处理我可以扔给它的所有文件/源组合!

    结论:

    问题的答案是 mp3 数据必须以不同的方式编码(不同的通道、采样率等),并通过 ffmpeg 加载并以“统一”方式输出使 mp3 数据兼容。

    解决方案是分别处理 ffmpeg 中的每个文件,将 ffmpeg 输出通过管道传输到缓冲区,然后连接缓冲区。

    感谢@Brad 的建议和详细解答!

    【讨论】:

    • 你为什么要转换成base64发送给客户端?你应该能够保持二进制。
    • 这就是我们之前设置系统的方式。来自服务器的“获取合并数据”基于单个声音文件的“获取单个数据”副本。编写那段代码的开发人员以 base64 格式发送数据,这就是为什么这个新函数也以 base64 格式发送数据。除此之外没有其他特别的原因。客户端接收数据,从中创建一个文件,并将其呈现给用户以供下载。与 base64 数据相比,发送二进制数据是否有明显的好处?
    • 是的,当然。除了每端的 CPU 和内存需求外,Base64 编码数据还会增加 33% 的大小开销。而且,当然还有少量增加的复杂性。这种浪费没有理由。
    • 再次感谢您的信息 :) 我开始实现发送二进制数据而不是 base64 数据,但后来我发现为什么我们首先使用 base64 数据:我们的客户端服务器需要一个 JSON 对象元数据的多个其他位,并且 JSON 不支持二进制,因此使用 base64 编码。我知道我们可以改为发送错误状态代码,但是仍然存在通过一个请求将多个文件作为二进制数据发送的问题,因为有时服务器会组合(例如)文件 A 和 B,并分别组合 C 和 D。最容易将其保留为两个 base64 字符串。
    • 对于请求数据,使用多部分表单数据很简单。这是通常的完成方式。 (想象一个具有多个文件输入的普通表单元素。)您可以拥有您的数据,如果您愿意,甚至可以使用 JSON,以及所有文件。这将节省客户端的开销,因为它们可以流式传输 HTTP 请求数据,同时节省带宽并减少服务器的开销。此外,这听起来对您的用例没有必要,但总有 CBOR。在 JavaScript 中解析很简单,支持流式处理、二进制类型、日期等。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-11-24
    • 1970-01-01
    • 2012-12-03
    • 1970-01-01
    • 2021-09-07
    • 1970-01-01
    • 2021-05-02
    相关资源
    最近更新 更多