Unity处理MP3流播放

拟墨画扇 提交于 2020-01-10 08:19:46

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 

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!