自定义音频采集和渲染
声网默认的音频模块可以满足在 App 中使用基本音频功能的需求。声网 SDK 支持使用自定义的音频源和自定义的音频渲染模块为你的 App 添加特殊的音频功能。
技术原理
实时音频传输过程中,声网 SDK 通常会开启默认的音频模块。在以下场景中,你可能会发现默认的音频模块无法满足开发需求,需要自定义音频采集或自定义音频渲染。例如:
- App 中已有自己的音频模块。
- 需要使用前处理库处理采集到的音频。
- 某些音频采集设备被系统独占。为避免与其他业务产生冲突,需要灵活的设备管理策略。
使用自定义音频源管理音频帧的采集、处理和播放时,需要使用声网 SDK 外部方法。
音频数据传输
下图展示在自定义音频采集、音频渲染时,音频数据的传输过程。
自定义音频采集
- 你需要使用 SDK 外部方法自行实现采集模块。
- 调用
PushAudioFrame
,将采集到的音频帧发送给 SDK。
自定义音频渲染
- 你需要使用 SDK 外部方法自行实现渲染模块。
- 调用
PullPlaybackAudioFrame
获取远端用户发送的音频数据。
前提条件
在进行操作之前,请确保你已经在项目中实现了基本的实时音视频功能。详见实现音视频互动。
实现方法
自定义音频采集
本节介绍如何实现自定义音频采集。
下图展示实现自定义音频采集的流程:
API 调用步骤
本节介绍如何实现自定义音频采集。
参考如下步骤,在你的项目中实现自定义音频采集功能:
-
调用
JoinChannel
加入频道前,调用CreateCustomAudioTrack
创建自定义音频轨道并获取轨道 ID。C#private System.Object _rtcLock = new System.Object();
internal uint AUDIO_TRACK_ID = 0;
private void CreateCustomAudioSource()
{
lock (_rtcLock)
{
AudioTrackConfig audioTrackConfig = new AudioTrackConfig(true);
// 创建自定义音频轨道
AUDIO_TRACK_ID = RtcEngine.CreateCustomAudioTrack(AUDIO_TRACK_TYPE.AUDIO_TRACK_MIXABLE, audioTrackConfig);
this.Log.UpdateLog("CreateCustomAudioTrack id:" + AUDIO_TRACK_ID);
}
}
//加入频道,并且设置好自定义音频的轨道 id
private void JoinChannel()
{
lock (_rtcLock)
{ // 设置用户角色为主播即可在频道中发流
RtcEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER);
ChannelMediaOptions channelMediaOptions = new ChannelMediaOptions();
// 发布自定义采集的音频流
channelMediaOptions.publishCustomAudioTrack.SetValue(true);
// 设置自定义音频轨道 ID
channelMediaOptions.publishCustomAudioTrackId.SetValue((int)AUDIO_TRACK_ID);
// 音频自采集场景下,不发布麦克风采集的音频
channelMediaOptions.publishMicrophoneTrack.SetValue(false);
// 加入频道
RtcEngine.JoinChannel(_token, _channelName, 0, channelMediaOptions);
}
} -
使用 SDK 外部方法自行实现音频的处理。以下示例代码以打开一个
.wav
文件并读取其中的音频数据为例。C#private bool ReadAudioHeaderAndDetectFormat(FileStream fileStream, AudioFrame audioFrame)
{
// 确保文件至少有 44 字节
if (fileStream.Length < 44)
{
Debug.LogError("文件太短,不是有效的 WAV 文件。");
return false;
}
// 读取 RIFF 头
byte[] riffHeader = new byte[4];
fileStream.Read(riffHeader, 0, 4);
string riff = System.Text.Encoding.ASCII.GetString(riffHeader);
if (riff != "RIFF")
{
Debug.LogError("不是有效的 WAV 文件。");
return false;
}
// 读取文件大小
byte[] fileSizeBytes = new byte[4];
fileStream.Read(fileSizeBytes, 0, 4);
int fileSize = BitConverter.ToInt32(fileSizeBytes, 0);
// 读取 WAVE 标签
byte[] waveHeader = new byte[4];
fileStream.Read(waveHeader, 0, 4);
string wave = System.Text.Encoding.ASCII.GetString(waveHeader);
if (wave != "WAVE")
{
Debug.LogError("不是有效的 WAV 文件。");
return false;
}
// 读取格式块标签 'fmt '
byte[] fmtHeader = new byte[4];
fileStream.Read(fmtHeader, 0, 4);
string fmt = System.Text.Encoding.ASCII.GetString(fmtHeader);
if (fmt != "fmt ")
{
Debug.LogError("不支持的格式块。");
return false;
}
// 读取格式块大小
byte[] fmtSizeBytes = new byte[4];
fileStream.Read(fmtSizeBytes, 0, 4);
int fmtSize = BitConverter.ToInt32(fmtSizeBytes, 0);
// 读取音频格式(PCM=1)
byte[] audioFormatBytes = new byte[2];
fileStream.Read(audioFormatBytes, 0, 2);
int audioFormat = BitConverter.ToInt16(audioFormatBytes, 0);
if (audioFormat != 1)
{
Debug.LogError("不支持的音频格式。");
return false;
}
// 读取通道数
byte[] channelsBytes = new byte[2];
fileStream.Read(channelsBytes, 0, 2);
int channels = BitConverter.ToInt16(channelsBytes, 0);
// 读取采样率
byte[] sampleRateBytes = new byte[4];
fileStream.Read(sampleRateBytes, 0, 4);
int sampleRate = BitConverter.ToInt32(sampleRateBytes, 0);
// 读取字节率(数据传输率)
byte[] byteRateBytes = new byte[4];
fileStream.Read(byteRateBytes, 0, 4);
int byteRate = BitConverter.ToInt32(byteRateBytes, 0);
// 读取块对齐
byte[] blockAlignBytes = new byte[2];
fileStream.Read(blockAlignBytes, 0, 2);
int blockAlign = BitConverter.ToInt16(blockAlignBytes, 0);
// 读取位深
byte[] bitsPerSampleBytes = new byte[2];
fileStream.Read(bitsPerSampleBytes, 0, 2);
int bitsPerSample = BitConverter.ToInt16(bitsPerSampleBytes, 0);
Debug.Log($"格式: PCM");
Debug.Log($"通道数: {channels}");
Debug.Log($"采样率: {sampleRate} Hz");
Debug.Log($"字节率: {byteRate} bps");
Debug.Log($"块对齐: {blockAlign}");
Debug.Log($"位深: {bitsPerSample} bits");
// 设置音频帧的属性
audioFrame.bytesPerSample = BYTES_PER_SAMPLE.TWO_BYTES_PER_SAMPLE;
audioFrame.type = AUDIO_FRAME_TYPE.FRAME_TYPE_PCM16;
audioFrame.samplesPerSec = sampleRate;
audioFrame.samplesPerChannel = sampleRate / PUSH_FREQ_PER_SEC;
audioFrame.channels = channels;
audioFrame.renderTimeMs = 0;
audioFrame.RawBuffer = new byte[audioFrame.samplesPerChannel * (int)BYTES_PER_SAMPLE.TWO_BYTES_PER_SAMPLE * channels];
return true;
} -
开启一个子线程,并在子线程中不断调用
PushAudioFrame
,将音频帧发送给 SDK,留作备用。C#// 每秒推送音频帧的次数
private const int PUSH_FREQ_PER_SEC = 20;
// 推送音频帧的线程
private Thread _pushAudioFrameThread;
// 开始推送音频帧
private void StartPushAudioFrame()
{
Action<string> action = (filePath) =>
{
_pushAudioFrameThread = new Thread(PushAudioFrameThread);
_pushAudioFrameThread.Start(filePath);
};
StartCoroutine(PreparationFilePath(action));
}
// 推送音频帧的线程函数
private void PushAudioFrameThread(object file)
{
string filePath = (string)file;
using (FileStream fileStream = new FileStream(filePath, FileMode.Open))
{
var audioFrame = new AudioFrame();
// 读取音频头并检测格式
if (!ReadAudioHeaderAndDetectFormat(fileStream, audioFrame))
{
return;
}
// 定位音频数据块
SeekToAudioData(fileStream);
// 获取开始时间戳
double startMillisecond = GetTimestamp();
long tick = 0;
// 计算每帧的时间间隔
int freq = 1000 / PUSH_FREQ_PER_SEC;
while (true)
{
// 加锁以确保线程安全
lock (_rtcLock)
{
if (RtcEngine == null) break;
// 读取音频数据
int bytesRead = fileStream.Read(audioFrame.RawBuffer, 0, audioFrame.RawBuffer.Length);
// 推送音频帧
int nRet = RtcEngine.PushAudioFrame(audioFrame, AUDIO_TRACK_ID);
if (bytesRead == 0)
{
// 如果读取到文件末尾,则重新开始读取
fileStream.Seek(0, SeekOrigin.Begin);
}
if (nRet == 0)
{
tick++;
// 计算下一帧的时间戳
double nextMillisecond = startMillisecond + tick * freq;
// 获取当前时间戳
double curMillisecond = GetTimestamp();
// 计算需要休眠的时间
int sleepMillisecond = (int)Math.Ceiling(nextMillisecond - curMillisecond);
if (sleepMillisecond > 0)
{
// 休眠指定时间
Thread.Sleep(sleepMillisecond);
}
else
{
// 如果时间不足,则最小休眠1毫秒
Thread.Sleep(1);
}
}
else
{
// 如果推送失败,则休眠一个帧间隔
Thread.Sleep(freq);
// 重置开始时间戳
startMillisecond = GetTimestamp();
tick = 0;
}
}
}
}
}
// 获取当前时间戳(毫秒)
private double GetTimestamp()
{
return (DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalMilliseconds;
} -
当你不需要再推送音频帧时,停止第三步中的子线程,调用
DestroyCustomAudioTrack
销毁自定义音频轨道,调用InitEventHandler
取消注册回调事件,调用LeaveChannel
离开当前频道。如果你还需要释放所有资源,调用Dispose
。C#private void OnDestroy()
{
Debug.Log("OnDestroy");
// 加锁以确保线程安全
lock (_rtcLock)
{
// 如果 RtcEngine 为 null,直接返回
if (RtcEngine == null) return;
// 取消注册回调事件
RtcEngine.InitEventHandler(null);
// 离开当前频道
RtcEngine.LeaveChannel();
// 销毁自定义音频轨道
RtcEngine.DestroyCustomAudioTrack(AUDIO_TRACK_ID);
// 释放 RtcEngine 资源
RtcEngine.Dispose();
RtcEngine = null;
}
// 等待 _pushAudioFrameThread 线程停止
_pushAudioFrameThread.Join();
}
自定义音频渲染
本节介绍如何实现自定义音频渲染。你可以采用如下两种方式实现自定义音频渲染。
下图展示自定义音频渲染的实现流程:
-
调用
JoinChannel
加入频道前,调用SetExternalAudioSink
开启和配置自定义音频渲染。C#// 声道数设为 2
private const int CHANNEL = 2;
// 采样率设为 44100 Hz
private const int SAMPLE_RATE = 44100;
// 每秒拉取音频帧的次数
private const int PULL_FREQ_PER_SEC = 100;
private void JoinChannel()
{
lock (_rtcLock)
{
RtcEngine.EnableAudio();
// 设置开启外部音频渲染
var nRet = RtcEngine.SetExternalAudioSink(true, SAMPLE_RATE, CHANNEL);
this.Log.UpdateLog("SetExternalAudioSink ret:" + nRet);
// 加入频道
RtcEngine.JoinChannel(_token, _channelName,"",0);
}
} -
加入频道后,调用
PullAudioFrame
获取远端用户发送的音频数据。使用你自己的音频渲染器处理音频数据,然后播放已渲染的数据。C#// 启动拉取音频帧的线程
private void StartPullAudioFrame(AudioSource aud, string clipName)
{
// 创建并启动一个新线程来执行 PullAudioFrameThread 方法
_pullAudioFrameThread = new Thread(PullAudioFrameThread);
_pullAudioFrameThread.Start();
}
// 拉取音频帧
private void PullAudioFrameThread()
{
// 初始化音频帧相关变量
var avsync_type = 0;
var bytesPerSample = 2;
var type = AUDIO_FRAME_TYPE.FRAME_TYPE_PCM16;
var channels = CHANNEL;
var samplesPerChannel = SAMPLE_RATE / PULL_FREQ_PER_SEC;
var samplesPerSec = SAMPLE_RATE;
var byteBuffer = new byte[samplesPerChannel * bytesPerSample * channels];
var freq = 1000 / PULL_FREQ_PER_SEC;
// 创建并初始化 AudioFrame 对象
AudioFrame audioFrame = new AudioFrame
{
type = type,
samplesPerChannel = samplesPerChannel,
bytesPerSample = BYTES_PER_SAMPLE.TWO_BYTES_PER_SAMPLE,
channels = channels,
samplesPerSec = samplesPerSec,
avsync_type = avsync_type
};
audioFrame.buffer = Marshal.AllocHGlobal(samplesPerChannel * bytesPerSample * channels);
// 获取开始时间戳
double startMillisecond = GetTimestamp();
// 初始化计数器
long tick = 0;
// 进入无限循环,持续拉取音频帧
while (true)
{
int nRet;
// 使用锁确保线程安全
lock (_rtcLock)
{
// 检查 RtcEngine 是否为空,如果为空则退出循环
if (RtcEngine == null)
{
break;
}
nRet = -1;
// 拉取音频帧
nRet = RtcEngine.PullAudioFrame(audioFrame);
Debug.Log("PullAudioFrame returns: " + nRet);
//hightlight-end
if (nRet == 0)
{
Marshal.Copy((IntPtr)audioFrame.buffer, byteBuffer, 0, byteBuffer.Length);
}
}
// 如果成功拉取音频帧,进行时间同步
if (nRet == 0)
{
tick++;
// 计算下一次应该拉取帧的时间
double nextMillisecond = startMillisecond + tick * freq;
// 获取当前时间
double curMillisecond = GetTimestamp();
// 计算需要休眠的时间
int sleepMillisecond = (int)Math.Ceiling(nextMillisecond - curMillisecond);
//Debug.Log("sleepMillisecond : " + sleepMillisecond);
// 如果需要休眠,则进行休眠
if (sleepMillisecond > 0)
{
Thread.Sleep(sleepMillisecond);
}
}
}
// 释放为 audioFrame.buffer 分配的内存
Marshal.FreeHGlobal(audioFrame.buffer);
}
使用原始音频数据回调
开始前,请确保你的项目中已实现原始音频数据的采集和处理。详见原始音频数据。
参考如下步骤,在你的项目中调用原始音频数据 API 实现自定义音频渲染:
- 从
OnRecordAudioFrame
,OnPlaybackAudioFrame
,OnMixedAudioFrame
或者OnPlaybackAudioFrameBeforeMixing
获取待播放的音频数据。 - 自行渲染并播放远端音频数据。
参考信息
示例项目
声网提供了开源的音频自采集和音频自渲染的示例项目供你参考,你可以前往下载或查看其中的源代码。