Unity处理MP3流播放
PCMReaderCallback回调
Unity音频数据是通过AudioClip去处理的,它提供了PCMReaderCallback回调,用于加载流音频数据。它的声明如下:
public static AudioClip Create(string name, int lengthSamples, int channels, int frequency, bool stream, AudioClip.PCMReaderCallback pcmreadercallback);
public delegate void PCMReaderCallback(float[] data);
PCMReaderCallback中的PCM指的是音频数据格式,下面会讲到。在stream模式下,lengthSamples表示播放时,每次循环处理的sample数,这会影响到回调参数data的大小。回调函数参数data就是需要填充的音频数据,它的格式是PCM,data的长度可以由lengthSamples * channels计算出来。
值得注意的是,PCMReaderCallback执行环境不一定,它是由Unity回调的,可能是主线程,也可能是子线程。所以当它卡住的时候,可能会卡掉整个应用。
PCM格式
PCM是Pulse Code Modulation的缩写,它是未压缩的音频数据格式,也是声卡能接受的格式。PCM包含一系列的sample,每个sample代表的是音频声音有多大。单个sample可以用不同的bit depth去记录,有PCM8,PCM16,PCM24,PCM32等,其中最通用的是PCM16。
Unity中我们主要关注PCM float格式,因为这是PCMReaderCallback需要的格式。PCM float一般是一组介于-1到1的浮点数,也就是用1代表最大声音。下面PCM16转PCM float的代码,出自AudioStream插件。
#region audio byte array
public static int ByteArrayToFloatArray(byte[] byteArray, uint byteArray_length, ref float[] resultFloatArray)
{
if (resultFloatArray == null || resultFloatArray.Length != (byteArray_length / 2))
resultFloatArray = new float[byteArray_length / 2];
int arrIdx = 0;
for (int i = 0; i < byteArray_length; i += 2)
resultFloatArray[arrIdx++] = BytesToFloat(byteArray[i], byteArray[i + 1]);
return resultFloatArray.Length;
}
static float BytesToFloat(byte firstByte, byte secondByte)
{
return (float)((short)((int)secondByte << 8 | (int)firstByte)) / 32768f;
}
#endregion
这段代码就是把PCM16转成PCM float的,因为16位整数short,占两个字节,最大为32768;所以除以32768f就转化成介于-1和1之间的float了。
minimp3库
minimp3是用于MP3解码的开源C库,它只有一个header文件,使用起来比较简单。主要API有两个,mp3dec_init和mp3dec_decode_frame。mp3dec_init负责解码库初始化,mp3dec_decode_frame负责实际解码。下面是Unity这边so库的代码,也很简单,只是封装了三个方法。这里要说明的一点是,minimp3支持PCM float输出,所以这里decode_samples函数接收的是float类型的pcm buffer。
mp3dec_t mp3d;
int open_dec()
{
mp3dec_init(&mp3d);
return 1;
}
int close_dec()
{
memset(&mp3d, 0, sizeof(mp3d));
return 0;
}
int decode_samples(uint8_t* buf, int bytes, float* pcm, mp3dec_frame_info_t* info)
{
int ret = mp3dec_decode_frame(&mp3d, buf, bytes, pcm, info);
return ret;
}
mp3dec_decode_frame方法
mp3dec_decode_frame官方介绍是从输入buffer中解码一帧,所以输入buffer必须足够大能够容纳一帧的数据。这个帧是MP3的概念,MP3文件格式将音频数据切分成一个个帧,一个帧可以理解成一小段声音。所以解码MP3就是挨个去解码每一帧,这样播放出来就是最终的音乐了。
每一帧包含帧信息和很多个sample,帧信息的结构如下:
[StructLayout(LayoutKind.Sequential)]
public struct Mp3DecFrameInfo
{
public int frame_bytes;//帧长度
public int channels;//声道数,单声道还是双声道
public int hz;//采样频率
public int layer;
public int bitrate_kbps;
}
函数返回值是sample数量(用samples表示),解压出来的结果,写入到pcm对应的buffer中,写入的长度可以通过samples * frameInfo.channels计算出来。因为这里的samples计算的是单个声音片段的数量,如果是多声道的话,每个声音片段会对应多个pcm sample。
Unity与minimp3交互
Unity与minimp3交互就是一个平台调用过程,之前也分享过Unity与平台的交互(P/Invoke方式)。这里主要讲一下没有涉及到的东西。
GCHandle
GCHandle提供了一种方式允许非托管代码访问托管对象或者内存。通过GCHandle.Alloc和GCHandleType.Pinned可以避免GC回收托管对象,然后通过GCHandle.AddrOfPinnedObject获取托管对象地址。
如minimp3解码出来的pcm数据,就是通过pcmPtr指针存储在pcmData托管数组中的。
pcmData = new float[MiniMp3.MINIMP3_MAX_SAMPLES_PER_FRAME];
pcmPinned = GCHandle.Alloc(pcmData, GCHandleType.Pinned);
pcmPtr = pcmPinned.AddrOfPinnedObject();
注意GCHandle.Alloc接收的是object类型,当struct等值类型传入时,会发生装箱操作;这样会导致AddrOfPinnedObject指向的struct,不是原来的struct。非托管代码对struct所做的修改,不会同步到托管代码中struct变量。
MP3边下边播
DownloadHandlerScript
UnityWebRequest提供了自定义下载处理类DownloadHandlerScript,在边下边播处理中,主要复写了两个方法,
protected void ReceiveContentLength(int contentLength);
protected bool ReceiveData(byte[] data, int dataLength);
ReceiveContentLength用来获取MP3文件的大小,就是读取Content-Length响应头;在没有这个header的情况下,这个方法不会被回调。一般情况下都会有这个Header。
自定义下载主要回调是ReceiveData,UnityWebRequest每次下载到的数据,会通过这个接口回调回来。由于下载的速度可能快于播放的速度,data需要自己缓存起来,在Quizdom项目中采用文件形式进行缓存。
MP3读取处理
AudioClip.Create方法需要声道数(channels),采样频率(frequency) 等一些信息,这些存在MP3帧信息里。所以在开始播放MP3前,需要先缓存到一个MP3帧的数据;然后创建AudioClip,重新读取这一MP3帧数据,进行播放。
这样就涉及到两个接口,读取音频数据和设置读取位置的接口,Quizdom下定义了IDownloadHandlerStream接口,如下所示:
public interface IDownloadHandlerStream
{
void SetReadPos(int pos);
int Read(byte[] buffer, int offset, int count);
}
在Quizdom中,通过Stream协程去读取第一个MP3帧信息,读到之后调用StreamStarting方法初始化PCMReaderCallback流播放回调,正式开始播放。函数声明如下:
private IEnumerator Stream()
private void StreamStarting(Mp3DecFrameInfo info)
private void PCMReaderCallback(float[] pData)
当网络比较慢,播放快于下载时,Read没有更多数据可以读取,返回值是0。这个时候需要根据情况进行处理。在Stream阶段中,由于需要拿到第一个MP3帧的数据,会每一帧循环读取,直到有新数据返回或者出错。示意代码如下:
do{ if (samples <= 0 && frameInfo.frame_bytes <= 0) { needMore = true; yield return new WaitForEndOfFrame(); } while(needMore)
在播放过程中,由于不能卡住PCMReaderCallback播放回调,所以数据不够时,需要用其他数据来填充PCM buffer,简单的话就是0(静音)。示意代码如下:
do{ if (frameInfo.frame_bytes <= 0 && remaing > 0) { break; } } while(true) //数据不够填充 if (bytes > 0){ //填充0 for (int i = pData.Length - bytes; i < pData.Length; ++i) pData[i] = 0; }
总结
Unity本身是支持MP3格式,但是没有开放对应的解码接口,AudioClip也不直接支持MP3格式的流播放。所以MP3流播放,一般需要第三方插件实现。在Quizdom项目中,由于第三方插件AudioStream基于Fmod,不太好用,所以这边参考AudioStream插件,实现了基于开源MiniMp3库的流播放功能。
本文提供了Unity实现MP3流播放的一种思路,主要供大家参考。
附录
[minimp3项目地址] https://github.com/lieff/minimp3
[AudioStream插件连接] https://assetstore.unity.com/packages/tools/audio/audiostream-65411