天天看點

OpenGL ES 實作可視化實時音頻音頻資料的采集音頻可視化實作代碼聯系與交流

作者:位元組流動

來源:

https://blog.csdn.net/Kennethdroid/article/details/106128767
OpenGL ES 實作可視化實時音頻音頻資料的采集音頻可視化實作代碼聯系與交流

音頻資料的采集

OpenGL 實作可視化實時音頻的思路比較清晰,可以利用 Java 層的 API AudioRecorder 采集到未編碼的音頻裸資料(PCM 資料),也可以利用 OpenSLES 接口在 Native 層采集,然後将采集到的音頻資料看作一組音頻的強度(Level)值,再根據這組強度值生成網格,最後進行實時繪制。

本文為友善展示,直接采用 Android 的 API AudioRecorder 采集音頻裸資料,然後通過 JNI 傳入 Native 層,最後生成網格進行繪制。

在使用 AudioRecorder 采集格式為 ENCODING_PCM_16BIT 音頻資料需要了解:所采集到的音頻資料在記憶體中位元組的存放模式是小端模式(小端序)(Little-Endian),即低位址存放低位、高位址存放高位,是以如果用 2 個位元組轉換為 short 型的資料需要特别注意。另外,大端序與小端序相反,即低位址存放高位、高位址存放低位。

OpenGL ES 實作可視化實時音頻音頻資料的采集音頻可視化實作代碼聯系與交流
OpenGL ES 實作可視化實時音頻音頻資料的采集音頻可視化實作代碼聯系與交流

在 Java 中小端序存儲的 byte 資料轉為 short 型數值可以采用如下方式:

byte firstByte = 0x10, secondByte = 0x01; //0x0110
ByteBuffer bb = ByteBuffer.allocate(2);
bb.order(ByteOrder.LITTLE_ENDIAN);
bb.put(firstByte);
bb.put(secondByte);
short shortVal = bb.getShort(0);      

為了避免資料轉換的麻煩,Android 的 AudioRecorder 類也提供了直接可以輸出 short 型數組音頻資料的 API ,我是踩了坑之後才發現的。

public int read(short[] audioData, int offsetInShorts, int sizeInShorts, int readMode)      

Android 使用 AudioRecorder 采集音頻的大緻流程,在 Java 層對其進行一個簡單的封裝:

public class AudioCollector implements AudioRecord.OnRecordPositionUpdateListener{
    private static final String TAG = "AudioRecorderWrapper";
    private static final int RECORDER_SAMPLE_RATE = 44100; //采樣率
    private static final int RECORDER_CHANNELS = 1; //通道數
    private static final int RECORDER_AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT; //音頻格式
    private static final int RECORDER_ENCODING_BIT = 16;
    private AudioRecord mAudioRecord;
    private Thread mThread;
    private short[] mAudioBuffer;
    private Handler mHandler;
    private int mBufferSize;
    private Callback mCallback;
    public AudioCollector() {
        //計算 buffer 大小
        mBufferSize = 2 * AudioRecord.getMinBufferSize(RECORDER_SAMPLE_RATE,
                RECORDER_CHANNELS, RECORDER_AUDIO_ENCODING);
    }
    public void init() {
        mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, RECORDER_SAMPLE_RATE,
                RECORDER_CHANNELS, RECORDER_AUDIO_ENCODING, mBufferSize);
        mAudioRecord.startRecording();
        //在一個新的工作線程裡不停地采集音頻資料
        mThread = new Thread("Audio-Recorder") {
            @Override
            public void run() {
                super.run();
                mAudioBuffer = new short[mBufferSize];
                Looper.prepare();
                mHandler = new Handler(Looper.myLooper());
                //通過 AudioRecord.OnRecordPositionUpdateListener 不停地采集音頻資料
                mAudioRecord.setRecordPositionUpdateListener(AudioCollector.this, mHandler);
                int bytePerSample = RECORDER_ENCODING_BIT / 8;
                float samplesToDraw = mBufferSize / bytePerSample;
                mAudioRecord.setPositionNotificationPeriod((int) samplesToDraw);
                mAudioRecord.read(mAudioBuffer, 0, mBufferSize);
                Looper.loop();
            }
        };
        mThread.start();
    }
    public void unInit() {
        if(mAudioRecord != null) {
            mAudioRecord.stop();
            mAudioRecord.release();
            mHandler.getLooper().quitSafely();
            mHandler = null;
            mAudioRecord = null;
        }
    }
    public void addCallback(Callback callback) {
        mCallback = callback;
    }
    @Override
    public void onMarkerReached(AudioRecord recorder) {
    }
    @Override
    public void onPeriodicNotification(AudioRecord recorder) {
        if (mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING
                && mAudioRecord.read(mAudioBuffer, 0, mAudioBuffer.length) != -1)
        {
            if(mCallback != null)
                //通過接口回調将音頻資料傳到 Native 層
                mCallback.onAudioBufferCallback(mAudioBuffer);
        }
    }
    public interface Callback {
        void onAudioBufferCallback(short[] buffer);
    }
}      

音頻可視化

在 Native 層擷取到 AudioRecorder 所采集的 PCM 音頻資料(short 類型數組),然後根據數組的長度将紋理坐标系的 S 軸進行等距離劃分,再以數組中的數值(類似聲音的強度值)為高度建構條狀圖,生成相應的紋理坐标和頂點坐标。

OpenGL ES 實作可視化實時音頻音頻資料的采集音頻可視化實作代碼聯系與交流

由于“一幀”音頻資料對應的數組比較大,繪制出來的音頻條狀圖成了一坨 shi ,要想直覺性地表現時域上的音頻,還需要在繪制之前對資料進行适當的采樣。

float dx = 1.0f / m_RenderDataSize;
for (int i = 0; i < m_RenderDataSize; ++i) {
    int index = i * RESAMPLE_LEVEL; //RESAMPLE_LEVEL 表示采樣間隔
    float y = m_pAudioData[index] * dy * -1;
    y = y < 0 ? y : -y; //表示音頻的數值轉為正數
    //建構條狀矩形的 4 個點
    vec2 p1(i * dx, 0 + 1.0f);
    vec2 p2(i * dx, y + 1.0f);
    vec2 p3((i + 1) * dx, y + 1.0f);
    vec2 p4((i + 1) * dx, 0 + 1.0f);
    //建構紋理坐标
    m_pTextureCoords[i * 6 + 0] = p1;
    m_pTextureCoords[i * 6 + 1] = p2;
    m_pTextureCoords[i * 6 + 2] = p4;
    m_pTextureCoords[i * 6 + 3] = p4;
    m_pTextureCoords[i * 6 + 4] = p2;
    m_pTextureCoords[i * 6 + 5] = p3;
    
    //建構頂點坐标,将紋理坐标轉為頂點坐标
    m_pVerticesCoords[i * 6 + 0] = GLUtils::texCoordToVertexCoord(p1);
    m_pVerticesCoords[i * 6 + 1] = GLUtils::texCoordToVertexCoord(p2);
    m_pVerticesCoords[i * 6 + 2] = GLUtils::texCoordToVertexCoord(p4);
    m_pVerticesCoords[i * 6 + 3] = GLUtils::texCoordToVertexCoord(p4);
    m_pVerticesCoords[i * 6 + 4] = GLUtils::texCoordToVertexCoord(p2);
    m_pVerticesCoords[i * 6 + 5] = GLUtils::texCoordToVertexCoord(p3);
}      

Java 層輸入“一幀”音頻資料,Native 層繪制一幀:

void VisualizeAudioSample::Draw(int screenW, int screenH) {
    LOGCATE("VisualizeAudioSample::Draw()");
    if (m_ProgramObj == GL_NONE) return;
    
    //加互斥鎖,保證音頻資料繪制與更新同步
    std::unique_lock<std::mutex> lock(m_Mutex);
    //根據音頻資料更新紋理坐标和頂點坐标
    UpdateMesh();
    UpdateMVPMatrix(m_MVPMatrix, m_AngleX, m_AngleY, (float) screenW / screenH);
    // Generate VBO Ids and load the VBOs with data
    if(m_VboIds[0] == 0)
    {
        glGenBuffers(2, m_VboIds);
        glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]);
        glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * m_RenderDataSize * 6 * 3, m_pVerticesCoords, GL_DYNAMIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[1]);
        glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * m_RenderDataSize * 6 * 2, m_pTextureCoords, GL_DYNAMIC_DRAW);
    }
    else
    {
        glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]);
        glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(GLfloat) * m_RenderDataSize * 6 * 3, m_pVerticesCoords);
        glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[1]);
        glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(GLfloat) * m_RenderDataSize * 6 * 2, m_pTextureCoords);
    }
    if(m_VaoId == GL_NONE)
    {
        glGenVertexArrays(1, &m_VaoId);
        glBindVertexArray(m_VaoId);
        glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (const void *) 0);
        glBindBuffer(GL_ARRAY_BUFFER, GL_NONE);
        glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[1]);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (const void *) 0);
        glBindBuffer(GL_ARRAY_BUFFER, GL_NONE);
        glBindVertexArray(GL_NONE);
    }
    // Use the program object
    glUseProgram(m_ProgramObj);
    glBindVertexArray(m_VaoId);
    glUniformMatrix4fv(m_MVPMatLoc, 1, GL_FALSE, &m_MVPMatrix[0][0]);
    GLUtils::setFloat(m_ProgramObj, "drawType", 1.0f);
    glDrawArrays(GL_TRIANGLES, 0, m_RenderDataSize * 6);
    GLUtils::setFloat(m_ProgramObj, "drawType", 0.0f);
    glDrawArrays(GL_LINES, 0, m_RenderDataSize * 6);
}      

實時音頻的繪制結果如下:

OpenGL ES 實作可視化實時音頻音頻資料的采集音頻可視化實作代碼聯系與交流

但是,上面這個實時音頻的繪制效果并不能給人時間流逝的感覺,就是單純地繪制完一組接着繪制另外一組資料,中間沒有任何過渡。

我們是在時域上(也可以通過傅立葉變換轉換成頻域)繪制音頻資料,要想繪制出來的效果有時間流逝的感覺,那就需要在 Buffer 上進行偏移繪制,即逐漸丢棄舊的資料,同時逐漸添加新的資料,這樣繪制出來的效果就有時間流逝的感覺。

OpenGL ES 實作可視化實時音頻音頻資料的采集音頻可視化實作代碼聯系與交流

首先我們的 Buffer 要擴大一倍(也可以是幾倍),采集 2 幀音頻資料填滿 Buffer ,這個時候阻塞音頻采集線程,然後通知渲染線程(資料準備好了)進行繪制,然後指向 Buffer 的指針按照特定的步長進行偏移,偏移一次繪制一次。

當指針偏移到上圖所示的邊界,這個時候 Buffer 中的資料都被繪制完畢,渲染線程暫停繪制,通知音頻采集線程解除阻塞,将 Buffer2 中的資料拷貝的 Buffer1 中,并接收新的資料放到 Buffer2 中,這個時候再次阻塞音頻采集線程,通知渲染線程資料更新完畢,可以進行繪制了。

void VisualizeAudioSample::UpdateMesh() {
    //設定一個偏移步長
    int step = m_AudioDataSize / 64;
    //判斷指針是否偏移到邊界
    if(m_pAudioBuffer + m_AudioDataSize - m_pCurAudioData >= step)
    {
        float dy = 0.5f / MAX_AUDIO_LEVEL;
        float dx = 1.0f / m_RenderDataSize;
        for (int i = 0; i < m_RenderDataSize; ++i) {
            int index = i * RESAMPLE_LEVEL;
            float y = m_pCurAudioData[index] * dy * -1;
            y = y < 0 ? y : -y;
            vec2 p1(i * dx, 0 + 1.0f);
            vec2 p2(i * dx, y + 1.0f);
            vec2 p3((i + 1) * dx, y + 1.0f);
            vec2 p4((i + 1) * dx, 0 + 1.0f);
            m_pTextureCoords[i * 6 + 0] = p1;
            m_pTextureCoords[i * 6 + 1] = p2;
            m_pTextureCoords[i * 6 + 2] = p4;
            m_pTextureCoords[i * 6 + 3] = p4;
            m_pTextureCoords[i * 6 + 4] = p2;
            m_pTextureCoords[i * 6 + 5] = p3;
            m_pVerticesCoords[i * 6 + 0] = GLUtils::texCoordToVertexCoord(p1);
            m_pVerticesCoords[i * 6 + 1] = GLUtils::texCoordToVertexCoord(p2);
            m_pVerticesCoords[i * 6 + 2] = GLUtils::texCoordToVertexCoord(p4);
            m_pVerticesCoords[i * 6 + 3] = GLUtils::texCoordToVertexCoord(p4);
            m_pVerticesCoords[i * 6 + 4] = GLUtils::texCoordToVertexCoord(p2);
            m_pVerticesCoords[i * 6 + 5] = GLUtils::texCoordToVertexCoord(p3);
        }
        m_pCurAudioData += step;
    }
    else
    {   
        //偏移到邊界時,通知音頻采集線程更新資料
        m_bAudioDataReady = false;
        m_Cond.notify_all();
        return;
    }
}
void VisualizeAudioSample::LoadShortArrData(short *const pShortArr, int arrSize) {
    if (pShortArr == nullptr || arrSize == 0)
        return;
    m_FrameIndex++;
    std::unique_lock<std::mutex> lock(m_Mutex);
    //前兩幀資料直接填充 Buffer
    if(m_FrameIndex == 1)
    {
        m_pAudioBuffer = new short[arrSize * 2];
        memcpy(m_pAudioBuffer, pShortArr, sizeof(short) * arrSize);
        m_AudioDataSize = arrSize;
        return;
    }
    //前兩幀資料直接填充 Buffer
    if(m_FrameIndex == 2)
    {
        memcpy(m_pAudioBuffer + arrSize, pShortArr, sizeof(short) * arrSize);
        m_RenderDataSize = m_AudioDataSize / RESAMPLE_LEVEL;
        m_pVerticesCoords = new vec3[m_RenderDataSize * 6]; //(x,y,z) * 6 points
        m_pTextureCoords = new vec2[m_RenderDataSize * 6]; //(x,y) * 6 points
    }
    //将 Buffer2 中的資料拷貝的 Buffer1 中,并接收新的資料放到 Buffer2 中,
    if(m_FrameIndex > 2)
    {
        memcpy(m_pAudioBuffer, m_pAudioBuffer + arrSize, sizeof(short) * arrSize);
        memcpy(m_pAudioBuffer + arrSize, pShortArr, sizeof(short) * arrSize);
    }
    //這個時候阻塞音頻采集線程,通知渲染線程資料更新完畢
    m_bAudioDataReady = true;
    m_pCurAudioData = m_pAudioBuffer;
    m_Cond.wait(lock);
}      

實作代碼

關注微信公衆号: 位元組流動,背景回複: OpenGL教程,即可擷取相關實作代碼。

聯系與交流

技術交流可以添加我的微信:Byte-Flow

「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。
OpenGL ES 實作可視化實時音頻音頻資料的采集音頻可視化實作代碼聯系與交流

繼續閱讀