webrtc中是如何处理音频数据的?

在这里插入图片描述

前言

本文是基于PineAppRtc项目

在webrtc中音频的录制和播放都是封装在内部,一般情况下我们也不需要关注,直接使用即可。

但是最近有一个需求,需要将我们自己的数据进行传输,所以就需要将这些接口暴露出来使用。所以就需要去研究一下它的源码,就有了这篇文章。

音频引擎

在webrtc中其实是有不只一套音频引擎的,其中有native层的使用OpenSL ES实现的,另外还有一套java层通过android api实现的。

这里注意,java层这套是在audio_device_java.jar中,包名是org.webrtc.voiceengine。但是在最新的官网webrtc代码中还有一套包名org.webrtc.audio的,貌似是替代前面那套的。

但是在PineAppRtc项目中使用的版本只有org.webrtc.voiceengine这套。

默认情况下是使用OpenSL ES这套。但是可以使用

WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true /* enable */);

禁用这套,这样就会使用java层的那套引擎。

那么我们如何将它们暴露出来,我们可以直接将这个包的源码放到项目下,然后将这个jar包删掉,这样就可以直接修改代码了。

录音并发送

在audio_device_java.jar中WebRtcAudioRecord这个类是负责录音的。

这个类及下面函数都是webrtc底层自动调用,所以我们不需要考虑参数的来源,知道怎么使用就好。

首先是构造函数

WebRtcAudioRecord(long nativeAudioRecord) { 
    this.nativeAudioRecord = nativeAudioRecord; 
    ... 
}

这个nativeAudioRecord很重要,是后续调用接口需要用到的重要参数。

下面再来看看init函数

private int initRecording(int sampleRate, int channels) {
    if (this.audioRecord != null) {
        this.reportWebRtcAudioRecordInitError("InitRecording called twice without StopRecording.");
        return -1;
    } else {
        int bytesPerFrame = channels * 2;
        int framesPerBuffer = sampleRate / 100;
        this.byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * framesPerBuffer);
        this.emptyBytes = new byte[this.byteBuffer.capacity()];
        this.nativeCacheDirectBufferAddress(this.byteBuffer, this.nativeAudioRecord);
       ...
    }
    return framesPerBuffer;
}

两个参数分别是采样率和声道(1是单声道,2是双声道)。这两个参数也很重要,是webrtc通过前期socket协商后选定的。我们也可以修改这两个参数,后面会说。

注意这里不能随便修改bytebuffer的容量大小,因为底层会进行校验。这个大小只能是(采样率 / 100 * 声道数 * 2),实际上就是每秒发送100次数据。
如果改动大小,native层会crash,报错是 Check failed: frames_per_buffer_ == audio_parameters_.frames_per_10ms_buffer() (xxx vs. xxx)

最重要的是nativeCacheDirectBufferAddress这函数,可以看到传入了一个bytebuffernativeAudioRecord,后面就会用到。

nativeCacheDirectBufferAddress之后就是初始化AudioRecorder等。

然后再看看startRecording

private boolean startRecording() {
    ...
    if (this.audioRecord.getRecordingState() != 3) {
        ...
    } else {
        this.audioThread = new WebRtcAudioRecord.AudioRecordThread("AudioRecordJavaThread");
        this.audioThread.start();
        return true;
    }
}

可以看到启动了一个线程,线程里做了什么

public void run() {
    ...
    while(this.keepAlive) {
        int bytesRead = WebRtcAudioRecord.this.audioRecord.read(WebRtcAudioRecord.this.byteBuffer, WebRtcAudioRecord.this.byteBuffer.capacity());
        if (bytesRead == WebRtcAudioRecord.this.byteBuffer.capacity()) {
            ...
            if (this.keepAlive) {
                WebRtcAudioRecord.this.nativeDataIsRecorded(bytesRead, WebRtcAudioRecord.this.nativeAudioRecord);
            }
        } else {
            ...
        }
    }
    ...
}

record中拿到数据后,调用了nativeDataIsRecorded函数。

这里看到从record中拿到数据时传入的时之前的bytebuffer,而调用nativeDataIsRecorded时,只传入了长度和nativeAudioRecord

所以可以看到,如果要用自己的数据(即不录音)就需要先有nativeAudioRecord(通过构造函数获得);然后调用nativeCacheDirectBufferAddress初始化;然后循环向bytebuffer写入数据,写入一次调用一次nativeDataIsRecorded发送出去。

接收并播放

在audio_device_java.jar中WebRtcAudioTrack是负责播放的。

这个类及下面函数也是webrtc底层自动调用,所以我们不需要考虑参数的来源,知道怎么使用就好。

同样先是构造函数

WebRtcAudioTrack(long nativeAudioTrack) {
    ...
    this.nativeAudioTrack = nativeAudioTrack;
    ...
}

同样nativeAudioTrack很重要,跟上面的nativeAudioRecord类似

然后来看看init函数

private boolean initPlayout(int sampleRate, int channels) {
    ...
    int bytesPerFrame = channels * 2;
    this.byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * (sampleRate / 100));
    this.emptyBytes = new byte[this.byteBuffer.capacity()];
    this.nativeCacheDirectBufferAddress(this.byteBuffer, this.nativeAudioTrack);
    ...
    return true;
}

采样率和声道跟上面一样,这里也创建了一个bytebuffer并传入nativeCacheDirectBufferAddress

这里的bytebuffer容量与录音一样不能随意改动,否则crash。

然后再看看start函数

private boolean startPlayout() {
    ...
    if (this.audioTrack.getPlayState() != 3) {
        ...
    } else {
        this.audioThread = new WebRtcAudioTrack.AudioTrackThread("AudioTrackJavaThread");
        this.audioThread.start();
        return true;
    }
}

也是开启了一个线程,线程里

public void run() {
    ...
    for(int sizeInBytes = WebRtcAudioTrack.this.byteBuffer.capacity(); this.keepAlive; WebRtcAudioTrack.this.byteBuffer.rewind()) {
        WebRtcAudioTrack.this.nativeGetPlayoutData(sizeInBytes, WebRtcAudioTrack.this.nativeAudioTrack);
        ...
        int bytesWritten;
        if (WebRtcAudioUtils.runningOnLollipopOrHigher()) {
            bytesWritten = this.writeOnLollipop(WebRtcAudioTrack.this.audioTrack, WebRtcAudioTrack.this.byteBuffer, sizeInBytes);
        } else {
            bytesWritten = this.writePreLollipop(WebRtcAudioTrack.this.audioTrack, WebRtcAudioTrack.this.byteBuffer, sizeInBytes);
        }
        ...
}

其实跟录音逻辑差不多,只不过这里先调用nativeGetPlayoutData让底层将收到的数据写入bytebuffer中,然后再通过write函数播放(这两个write函数最终都调用AudioTrack的write函数)。

所以如果我们要自己处理接收的数据,只需要在这里调用nativeGetPlayoutData,然后从bytebuffer中读取数据自己处理即可,后面的代码都可以删掉。

总结同样跟录音一样,先构造函数拿nativeAudioTrack这值,然后创建了一个bytebuffer并传入nativeCacheDirectBufferAddress,然后循环调用nativeGetPlayoutData获取数据处理

参数设定

关于这些参数的设定,是双方经过协商定的,应该是一方将能支持的参数发送给另一方,另一方根据自己能支持的选出一个合适返回,然后双方就都这个参数处理数据。

但是我们是否可以干预这个过程,比如双方都支持的可能不只一个,我们不想使用自动选择的那个合适的,怎么做?

在audio_device_java.jar中还有两个类WebRtcAudioManagerWebRtcAudioUtils

这两个里就可以做一些设置,比如

采样率

WebRtcAudioManager

private int getNativeOutputSampleRate() {
//        if (WebRtcAudioUtils.runningOnEmulator()) {
//            Logging.d("WebRtcAudioManager", "Running emulator, overriding sample rate to 8 kHz.");
//            return 8000;
//        } else if (WebRtcAudioUtils.isDefaultSampleRateOverridden()) {
//            Logging.d("WebRtcAudioManager", "Default sample rate is overriden to " + WebRtcAudioUtils.getDefaultSampleRateHz() + " Hz");
//            return WebRtcAudioUtils.getDefaultSampleRateHz();
//        } else {
//            int sampleRateHz;
//            if (WebRtcAudioUtils.runningOnJellyBeanMR1OrHigher()) {
//                sampleRateHz = this.getSampleRateOnJellyBeanMR10OrHigher();
//            } else {
//                sampleRateHz = WebRtcAudioUtils.getDefaultSampleRateHz();
//            }
//
//            Logging.d("WebRtcAudioManager", "Sample rate is set to " + sampleRateHz + " Hz");
//            return sampleRateHz;
//        }
    return 16000;
}

将原代码去掉,直接返回我们想要的采样率。

声道

同样在WebRtcAudioManager

public static synchronized boolean getStereoOutput() {
    return useStereoOutput;
}

public static synchronized boolean getStereoInput() {
    return useStereoInput;
}

因为这两个的返回值直接影响声道数:

private void storeAudioParameters() {
    this.outputChannels = getStereoOutput() ? 2 : 1;
    this.inputChannels = getStereoInput() ? 2 : 1;
    this.sampleRate = this.getNativeOutputSampleRate();
    this.hardwareAEC = isAcousticEchoCancelerSupported();
    this.hardwareAGC = false;
    this.hardwareNS = isNoiseSuppressorSupported();
    this.lowLatencyOutput = this.isLowLatencyOutputSupported();
    this.lowLatencyInput = this.isLowLatencyInputSupported();
    this.proAudio = this.isProAudioSupported();
    this.outputBufferSize = this.lowLatencyOutput ? this.getLowLatencyOutputFramesPerBuffer() : getMinOutputFrameSize(this.sampleRate, this.outputChannels);
    this.inputBufferSize = this.lowLatencyInput ? this.getLowLatencyInputFramesPerBuffer() : getMinInputFrameSize(this.sampleRate, this.inputChannels);
}

上面的代码中可以看到还有其他设定,需要的话可以进行相应修改。

总结

这里我们只是简单分析了一下录制和播放的过程,知道我们应该从哪入手及怎么才能传送现有音频并获取对方音频数据,至于如果改造和后续的处理大家可以自己发挥了。

至于视频数据的处理,可以看我的另外一篇文章《WebRtc中是如何处理视频数据的?》

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BennuCTech

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值