如何在 Java 中播放 Opus 编码的音频?

Posted

技术标签:

【中文标题】如何在 Java 中播放 Opus 编码的音频?【英文标题】:How do I play Opus encoded audio in Java? 【发布时间】:2014-12-02 18:23:15 【问题描述】:

在播放解码后的音频时,我设法产生了各种声音,从汩汩声到尖叫声再到恶魔圣歌。其中最接近的声音类似于快进播放,播放仅持续约 15 秒。我已经尝试了解码和 Audiosystem API 方法的大量参数组合,但似乎没有任何效果。

那么,是什么导致了这种音频失真?

此文件的 Opusinfo 显示如下:

Processing file "test.opus"...

New logical stream (#1, serial: 00002c88): type opus
Encoded with libopus 1.1
User comments section follows...
     ENCODER=opusenc from opus-tools 0.1.9
Opus stream 1:
    Pre-skip: 356
    Playback gain: 0 dB
    Channels: 1
    Original sample rate: 44100Hz
    Packet duration:   20.0ms (max),   20.0ms (avg),   20.0ms (min)
    Page duration:   1000.0ms (max),  996.8ms (avg),  200.0ms (min)
    Total data length: 1930655 bytes (overhead: 1.04%)
    Playback length: 4m:09.173s
    Average bitrate: 61.99 kb/s, w/o overhead: 61.34 kb/s
Logical stream 1 ended

此文件使用 VLC 正确播放。

为了解码我尝试使用以下库的文件:

VorbisJava (https://github.com/Gagravarr/VorbisJava/) - 从 OGG 容器中拉取帧

LibJitsi (https://jitsi.org/Projects/LibJitsi) - 它有一个用于 Opus 的 JNI 包装器,用于解码 opus 帧

SSCCE 下面

package me.justinb.mediapad.audio;

import org.gagravarr.ogg.OggFile;
import org.gagravarr.ogg.OggPacket;
import org.jitsi.impl.neomedia.codec.audio.opus.Opus;
import javax.sound.sampled.*;
import java.io.*;
import java.nio.ByteBuffer;

public class OpusAudioPlayer 
    private static int BUFFER_SIZE = 1024 * 1024;
    private static int INPUT_BITRATE = 48000;
    private static int OUTPUT_BITRATE = 44100;
    private OggFile oggFile;
    private long opusState;
    private ByteBuffer decodeBuffer = ByteBuffer.allocate(BUFFER_SIZE); 
    private AudioFormat audioFormat = new AudioFormat(OUTPUT_BITRATE, 16, 1, true, false);

    public static void main(String[] args) 
        try 
            OpusAudioPlayer opusAudioPlayer = new OpusAudioPlayer(new File("test.opus"));
            opusAudioPlayer.play();
         catch (IOException e) 
            e.printStackTrace();
        
    

    public OpusAudioPlayer(File audioFile) throws IOException 
        oggFile = new OggFile(new FileInputStream(audioFile));
        opusState = Opus.decoder_create(INPUT_BITRATE, 1);
        System.out.println("Audio format: " + audioFormat);
    

    private byte[] decode(byte[] packetData) 
        int frameSize = Opus.decoder_get_nb_samples(opusState, packetData, 0, packetData.length);
        int decodedSamples = Opus.decode(opusState, packetData, 0, packetData.length, decodeBuffer.array(), 0, frameSize, 0);
        if (decodedSamples < 0) 
            System.out.println("Decode error: " + decodedSamples);
            decodeBuffer.clear();
            return null;
        
        decodeBuffer.position(decodedSamples * 2); // 2 bytes per sample
        decodeBuffer.flip();

        byte[] decodedData = new byte[decodeBuffer.remaining()];
        decodeBuffer.get(decodedData);
        decodeBuffer.flip();
        System.out.println(String.format("Encoded frame size: %d bytes", packetData.length));
        System.out.println(String.format("Decoded frame size: %d bytes", decodedData.length));
        System.out.println(String.format("Decoded %d samples", decodedSamples));
        return decodedData;
    

    public void play() 
        int totalDecodedBytes = 0;
        try 
            SourceDataLine speaker = AudioSystem.getSourceDataLine(audioFormat);
            OggPacket nextPacket = oggFile.getPacketReader().getNextPacket();
            // Move to beginning of stream
            while ( !nextPacket.isBeginningOfStream()) 
                nextPacket = oggFile.getPacketReader().getNextPacket();
            
            speaker.open();
            speaker.start();
            while(nextPacket != null) 
                // Decode each packet
                byte[] decodedData = decode(nextPacket.getData());
                if(decodedData != null) 
                    // Write packet to SourceDataLine
                    speaker.write(decodedData, 0, decodedData.length);
                    totalDecodedBytes += decodedData.length;
                
                nextPacket = oggFile.getPacketReader().getNextPacket();
            
            speaker.drain();
            speaker.close();
            System.out.println(String.format("Decoded to %d bytes", totalDecodedBytes));
         catch (Exception e) 
            e.printStackTrace();
        
    

【问题讨论】:

【参考方案1】:

我的特殊问题似乎是由 VorbisJava 中的错误引起的。我现在正在使用 J-Ogg,它可以毫无问题地处理容器解析。我敢肯定有人会觉得这很有用。

这是展示如何在 Java 中播放 Opus 编码音频的最终代码:

package me.justinb.mediapad.audio;

import de.jarnbjo.ogg.FileStream;
import de.jarnbjo.ogg.LogicalOggStream;
import org.jitsi.impl.neomedia.codec.audio.opus.Opus;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.SourceDataLine;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.util.Collection;

public class OpusAudioPlayer 
    private static int BUFFER_SIZE = 1024 * 1024;
    private static int INPUT_BITRATE = 48000;
    private static int OUTPUT_BITRATE = 48000;

    private FileStream oggFile;
    private long opusState;

    private ByteBuffer decodeBuffer = ByteBuffer.allocate(BUFFER_SIZE);

    private AudioFormat audioFormat = new AudioFormat(OUTPUT_BITRATE, 16, 1, true, false);

    public static void main(String[] args) 
        try 
            OpusAudioPlayer opusAudioPlayer = new OpusAudioPlayer(new File("test.opus"));
            opusAudioPlayer.play();
         catch (IOException e) 
            e.printStackTrace();
        
    

    public OpusAudioPlayer(File audioFile) throws IOException 
        oggFile = new FileStream(new RandomAccessFile(audioFile, "r"));
        opusState = Opus.decoder_create(INPUT_BITRATE, 1);
    

    private byte[] decode(byte[] packetData) 
        int frameSize = Opus.decoder_get_nb_samples(opusState, packetData, 0, packetData.length);
        int decodedSamples = Opus.decode(opusState, packetData, 0, packetData.length, decodeBuffer.array(), 0, frameSize, 0);
        if (decodedSamples < 0) 
            System.out.println("Decode error: " + decodedSamples);
            decodeBuffer.clear();
            return null;
        
        decodeBuffer.position(decodedSamples * 2); // 2 bytes per sample
        decodeBuffer.flip();

        byte[] decodedData = new byte[decodeBuffer.remaining()];
        decodeBuffer.get(decodedData);
        decodeBuffer.flip();
        return decodedData;
    

    public void play() 
        int totalDecodedBytes = 0;
        try 
            SourceDataLine speaker = AudioSystem.getSourceDataLine(audioFormat);
            speaker.open();
            speaker.start();
            for (LogicalOggStream stream : (Collection<LogicalOggStream>) oggFile.getLogicalStreams()) 
                byte[] nextPacket = stream.getNextOggPacket();
                while (nextPacket != null) 
                    byte[] decodedData = decode(nextPacket);
                    if(decodedData != null) 
                        // Write packet to SourceDataLine
                        speaker.write(decodedData, 0, decodedData.length);
                        totalDecodedBytes += decodedData.length;
                    
                    nextPacket = stream.getNextOggPacket();
                
            
            speaker.drain();
            speaker.close();
            System.out.println(String.format("Decoded to %d bytes", totalDecodedBytes));
         catch (Exception e) 
            e.printStackTrace();
        
    

【讨论】:

对这个问题有任何想法吗? Exception in thread "main" java.lang.UnsatisfiedLinkError: no jnopus in java.library.path at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1886) at java.lang.Runtime.loadLibrary0(Runtime.java:849) at java.lang.System.loadLibrary(System.java:1088) at org.jitsi.impl.neomedia.codec.audio.opus.Opus.&lt;clinit&gt;(Opus.java:70) at OpusAudioPlayer.&lt;init&gt;(OpusAudioPlayer.java:39) at OpusAudioPlayer.main(OpusAudioPlayer.java:30) 当 jnopus 库(依赖于平台)未包含在您的构建路径中时会发生这种情况。检查您的构建并确保在 libjitsi 的“本机库依赖项”下包含您需要的本机库。【参考方案2】:

查看您的代码,我假设您误解了“帧长度”的含义。您正在获取字节数,但帧长度直接取决于文件的编码方式。

以 48000 Hz 录制的音频文件每秒有 48000 个样本。这个音频样本通常是一个 16 位整数(2 个字节),这意味着您将以非编码形式 (PCM-WAV) 每秒有 48000 * 2 个字节。

像 opus 这样的音频编码器将一次采集多个音频样本并将它们编码到一个包中。这是框架。在 48 kHz 时,这些值可能适用于作品 120、240、480、960、1920 和 2880。

【讨论】:

以上是关于如何在 Java 中播放 Opus 编码的音频?的主要内容,如果未能解决你的问题,请参考以下文章

WebRTC Native M96 音频发送流程(SendRtp)以及接收音频包播放流程(OnPacketReceived)

实时音频编解码之十一Opus编码

实时音频编解码之十一Opus编码

如何在 iPhone/iOS 中播放 xiph.org 音频编解码器?

尝试在 Discord.js 中播放音频文件

从设备发送 NAudio / Opus 编码的音频作为 RTP