如何将 PCM 字节数组转换为小端和单声道?

How to turn a PCM byte array into little-endian and mono?

我正在尝试将在线通信应用程序中的音频输入 Vosk 语音识别 API。

音频以字节数组的形式出现,并采用这种音频格式 PCM_SIGNED 48000.0 Hz, 16 bit, stereo, 4 bytes/frame, big-endian。 为了能够用 Vosk 处理它,它需要 monolittle-endian.

这是我目前的尝试:

        byte[] audioData = userAudio.getAudioData(1);
        short[] convertedAudio = new short[audioData.length / 2];
        ByteBuffer buffer = ByteBuffer.allocate(convertedAudio.length * Short.BYTES);
        
        // Convert to mono, I don't think I did it right though
        int j = 0;
        for (int i = 0; i < audioData.length; i += 2)
            convertedAudio[j++] = (short) (audioData[i] << 8 | audioData[i + 1] & 0xFF);

        // Convert to little endian
        buffer.order(ByteOrder.BIG_ENDIAN);
        for (short s : convertedAudio)
            buffer.putShort(s);
        buffer.order(ByteOrder.LITTLE_ENDIAN);
        buffer.rewind();

        for (int i = 0; i < convertedAudio.length; i++)
            convertedAudio[i] = buffer.getShort();

        queue.add(convertedAudio);

当然支持签名PCM。问题是 48000 fps 不是。我觉得直接Java支持的最高帧率是44100.

至于采取什么行动,我不确定该推荐什么。也许有可以使用的图书馆?当然可以直接使用字节数据手动进行转换,您可以在其中强制执行预期的数据格式。

如果需要,我可以写更多关于转换过程本身的内容(将字节组装成 PCM、操纵 PCM、从 PCM 创建字节)。 VOSK 是否也期望 48000 fps?


从立体声到单声道实际上就是左右 PCM 值的总和。添加一个步骤以确保不超出范围是很常见的。 (如果 PCM 编码为标准化浮点数,则 16 位范围 = -1 到 1,如果 PCM 编码为短整数,则范围 = -32768 到 32767。)

以下代码片段是采用单个 PCM 值(有符号浮点数,标准化为 -1 和 1 之间的范围)并以小端顺序生成两个字节(16 位)的示例。数组 bufferfloat 类型并保存 PCM 值。数组 audioBytes 的类型为 byte.

buffer[i] *= 32767;
        
audioBytes[i*2] = (byte) buffer[i];
audioBytes[i*2 + 1] = (byte)((int)buffer[i] >> 8 );

要使其成为大端,只需交换 audioBytes 的索引,或 (byte) buffer[i](byte)((int)buffer[i] >> 8 ) 的操作。这段代码来自class AudioCue,我写的一个class,作为一个增强的Clip。请参阅第 1391-1394 行。

我认为您可以推断出相反的过程(将传入字节转换为 PCM)。但这是代码行 391-393 中执行此操作的示例。在这种情况下,temp 是一个 float 数组,它将保存从字节流计算出的 PCM 值。在我的代码中,该值将很快除以 32767f 以使其归一化。 (第 400 行)

temp[clipIdx++] = ( buffer[bufferIdx++] & 0xff ) | ( buffer[bufferIdx++] << 8 ) ;

对于 big endian,您可以颠倒 & 0xff<< 8 的顺序。

如何遍历结构取决于您的个人喜好。 IDK,我在这里选择了最佳方法。对于您的情况,我很想将 PCM 值保存在 short 中(范围从 -32768 到 32767),而不是标准化为 -1 到 1 浮点数。如果您正在处理来自多个来源的音频数据,则规范化更有意义。但是您要做的唯一处理是将左右 PCM 加在一起以获得单声道值。顺便说一下,在左右求和之后,确保不超出数值范围是很好的——因为这会产生一些非常严重的失真。

我遇到了同样的问题,发现这个 post 将原始 pcm 字节数组转换为音频输入流。

我假设您使用的是 Java Discord API (JDA),所以这是我使用 vosk 的 'handleUserAudio()' 函数的初始代码,以及我在上面提供的 link 中的代码:

                // Define audio format that vosk uses
            AudioFormat target = new AudioFormat(
                    16000, 16, 1, true, false);

            try {
                byte[] data = userAudio.getAudioData(1.0f);
                // Create audio stream that uses the target format and the byte array input stream from discord
                AudioInputStream inputStream = AudioSystem.getAudioInputStream(target,
                        new AudioInputStream(
                                new ByteArrayInputStream(data), AudioReceiveHandler.OUTPUT_FORMAT, data.length));

                // This is what was used before
//                InputStream inputStream = new ByteArrayInputStream(data);

                int nbytes;
                byte[] b = new byte[4096];
                while ((nbytes = inputStream.read(b)) >= 0) {
                    if (recognizer.acceptWaveForm(b, nbytes)) {
                        System.out.println(recognizer.getResult());
                    } else {
                        System.out.println(recognizer.getPartialResult());
                    }
                }
//                queue.add(data);
            } catch (Exception e) {
                e.printStackTrace();
            }

到目前为止,这是可行的,但是,它会将所有内容都投入到识别器的“.getPartialResult()”方法中,但至少 vosk 能够理解来自 discord 机器人的音频。