Unity處理MP3流播放

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 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章