OpenGL ES 实现可视化实时音频

该原创文章首发于微信公众号字节活动

OpenGL ES 实现可视化实时音频

音频数据的收集

OpenGL 完成可视化实时音频的思路比较明晰,能够利用 Java 层的 API AudioRecorder 收集到未编码的音频裸数据(PCM 数据),也能够利用 OpenS/ t fLES 接口在 Native 层收集,然后将收集到的音频数据看作一组音频的强度(Level)值,再依据这组强度值生成网格,最后进行实时制作。

本文为便利展示,直接选用 Android 的 API AudioRec~ o j # b F 3 [order 收集音频裸数据,然后通过 JNI 传入 Native 层,最后生成网 a V格进行制作。

在运用 AudioRecorC ; Z Xdx T . l *er 收集格局为 ENCODINV : n w J n _ N `G_PCM_16BIT 音频数据需求了解:所C 6 a收集到的音频数据在内存中字节的寄存模式是小端模: 2 v K B %式(小端序)(Little-Endian),即低地址寄存低位、高地址寄存高位,所以假如用 2 个字节转化为 short 型的数据需求特别注意。另外,大端序与o u 7 6 u : & Y A小端序相反,即低地址寄存高位、高地址寄存低位。

OpenGL ES 实现可视化实时音频
OpenGL ES 实现可视化实时音频

在 Java 中小端序存储R 2 [ y l O 7 ; {的 byte 数据转4 W G ! – y + 2为 short 型数值能够选用如下方式:

bye o } 6 & E ^te firstByte = 0x10, secondByte = 0x01; //0x; h v G z ?0110
B= _ ~ yyteBuffer bb = ByteBuffer.allocate(2);
bb.order(ByteOrder.LITTLE_ENDIAN{ H 4 4);
bb.put(firstByte);
bb.put(secondByte);
short shortVal = bb.getShort(0);

为了防止数据转化的麻烦,Android 的 Audif c | $ w G r z roRecorder 类也供给了直接能够输出 short 型数组音频数据的 API ,我是踩了坑之后才发现的。

publa B [ @ c / 4 sic iN o h D 0 C I mnt read(short[] audioData, in T 7t offsetInShorts, int sizeInShd  Z d q w $ W *orts, int readMode)

Android 运a ; F用 AudioRecorder 收集音频的大致流程,在 Java 层对其进行一个简略的封装:

public class AudioCollector implements AudioRecord.OnRecordPositionUpdateListe7 8 h y ) , Dner{
private static final String TAG = "AudioRecorderWrapp6 l - zer";
private static final int RECORDER_SAMPLU E R 8 7 vE_RATE = 44100; // ~ m w 8 r采样率
private static final int RECORDER_CHANNELS = 1; //通道数
private static final int RECORDER_AUDIO_l $ X S 6ENCODING = AudioFo! c l a q x F Qrmat.ENCODING_PCM_16BIT; //音频格局
private stati7 c # = | L [ I $c} g F f V T W  ( final int RECORDER_EN? h + FCODING_BIT = 16;
private AudioRecord mAudk B u ioRecord;
private Thread mThread;
private short[] mAudioBuffer;
private Handler mHandler.  ; 5 O D Q G;
private ia r % )nt mBufferSize;
private Callback mCallback;
public AudioCollector() {
//核算 buffer 巨细
mBufferSize = 2 * AudioRecord.getMi+ 9 L W Q +nBufferSize(RECORDER_SAMPLE_RATE,
RECORDER_CHANNELS, RECORDER_AUDIO_ENCODING);
}
public void init() {
mA+ w @udioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, RECORDER_SAMPLE_RATE,
RC X M v d !ECORDER_CHANNELS, RECORDER_AUDIO_ENCODING, mBufferSize);
mAudioRecord.startRecording();
//在一个新的工作线程里不停地收集音频数据
mThread = new Thread(` ?  Q M z 0 4 q"Audio-Recorder") {
@Override
public void run() {
super.run();
mAudioBuffer = new short[mBufferSize];
Looper.prepare();
mHandler = new HP 7 I Mandler(LooT + Z * v T : &p+ ; g o 5 4 , ver.myLooper());
//通过 AudioRef q p m & N vcord.OnRecordPositionUpdateListener 不停地收集音频数据
mAudioRecord.setRecordPositionUpdateListener(AudioCollector.this, mHandler);
int bytePerSample = RECORDER_ENCODING_BIT / 8;
fT { ~ e ; k Mloat samplesToDraw = mBuffe7 K I 7 I rSize / bytePerSample;
mAudioRecord.setPositionNotificationPe/ % & t L 2 ~riod((int) samplesToDraw);
mAudioRecord.read(mAudioBuffer, 0, mBufferSize);
Looper.loop();
}
};
mThread.start();
}i m 4 - h
public void unInit() {
if(mAudioRecord != null) {
mAudioRecord.stop();
mAudioRecord.relv d =ease();
mHandler1 z : S f.getLoope& g E * _r().quitSafely();
mHandler = null;
mAudioRecord = null;
}
}
public void addCallback(Callbac_ % @ Nk callback) {
mCallback = callbe @ | i b . |ack;
}
@Override
public void onMarkerReached(AudioRecord recorder) {
}
@OverriB ~ D = { 4 fde
pubT s w blic void onPeriodicNotification(AudioRecord re4 1 - Xcorder) {
if (mAudioRecord.getRecorH M p ; OdingState() == AudioRe! F S ; ; Dcord.RECORDSTATE_RECORDING
&& mAudioRecord.read(mAudioBuffer, 0, mAudioBuffer.length) != -1)
{
if(mCallback != null)
//通过接口回调将音频数据传到 Native 层
mCallback.onAudioBufferCallbF , f ] | R w ! xack(mAuO K @ ) 4 m m ydiof N ~ HBuffer);
}
}
public interface CalT $ ] x e w 6 # Slback {
void onAudioBufferCallback(short[] buffer);
}
}

音频可视化

在 Native 层获取到 AudioRecords ) m } e T uer 所收集的 PCM 音频数据(short 类型数组),然后依据数组的长度将纹路坐标系的 S 轴进行等距离划分,再以数( o w组中的数值(类似声响的强度值)为高度构建条状图,生成相应的纹路坐标和极点O I 7 V | 坐标。

OpenGL ES 实现可视化实时音频

由于“一帧”( 4 { z音频数据对应的数组比较大,制作出来的音频条状图成了一坨 shi ,要想直观性地表现时域上的音频,还需求在制作之前对数据进行适当的采样。

float dx = 1.0f / m_RenderDataSize;
for (int i = 0; i < m_RenderDataSize; ++i) {
int index = i * RESAMPLE_LEx t m b ^ 7 sVEL; //RESAMPLE_LEVEL 表明采样距离
float y = m_pAudioData[index] * dy *Q J M -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(p k ;(i + 1) * dx, 0 + 1.0f);
//构建纹路坐标
m_pTextureCoords[i * 6 +z x d I e M g q 0] = p1;
m_pTextureCoords[i * 6 + 1] = p2;
m_pTextureCoords[i * 6 + 2] = p3;
m_pTextureCoords[i * 6 + 3] = p1;
m_pTextureCoords[i * 6 + 4] = p3;
m_pTextureCoords[i * 6 + 5] = p4;
m_pTextureCoords[i * 6 + 2] = p4;
m_pTextu1 g #reCoords[i * 6 + 3] = p4;
m_pTextureCoords[i * 6 + 4] = p2;
m_pTextureCoords[i * 6 + 5] =& ` T $ , T h R W p3;
//3 ; e I M 0构建极点坐标,将纹路坐标q d 0 ? n ^ G z 2转为极点坐标
m_pVerticesCoords[i * 6 +m D p 0] = GLUtils::texCoordToVertexCoord(p1);
m_pVerticesCoords[i * 6 + 1] = GLUtils::texCoordToVertexCoor` + S O $ F Cd(p2);
m_pVerticesCoords[i * 6 + 2] = GLUtils::texCoordToVertexCoord(p3);
m_pVerticesCoords[i * 6 + 3] = GLUtils::texCoordToVertexCoord(p1);
m_pV: % 4 k 5 AerticesCoords[i * 6 +i T . = { ; B 4] = GLUtils::texCo7 f s 8 | m , ! gordT( # YoVertexCoord(p3);
m_pVerticesCoords[i * 6 + 5[ q D Y x E r] = GLUtils::texCoordToVertexCoord(p4);
m_pVerticesCoordS 6 h { ms[i * 6 + 2] = GLUtils::texCoordToVertexCoord(p4);
m_pVerticesCoords[i * 6 + 3] = GLUt, + p iils::texCoordToVertexCoord(p4);
m_pVerticesC, 3 y W + ;oords[i * 6 + 4] = GLUtils::texCoordToVertexCoord(p2);
m_pVertice& 5 B R T 9 :sCoords[i * 6 + 5] = GLUtils::texCoordToVertexCoord(p3)& Q p k . y :;
}

Java 层输入“一帧”音频数据,Native 层制作一帧:

void VisualizeAudioSample::Draw(int screenW, int screenH) {
LOGCATE("Visu+ n c 8 b 0 d =alizeAudioSample::Draw()");
if (m_ProgramObj == GL_NONE) return;
/l S a } 0 o : N/加互斥锁,保证音频数据制作与更新同步
std::unique_lock<std::mutex> lock(m_Mutex);
//依据音频数据更新纹路坐标和极点坐标
UpdateMesh();
UpdateMVPMatrix(m_MVPMatrix, m_AngleX, m_Anglek e K N iY, (float) screenW / screenH);
// Generate VBO Ids and load the VBOs with data
if(m_VboIds[0] == 0)
{
glG5 e 0 a p R senBufferP O ! 7 M j a Ms(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) *F r d 0 k f 6 j o m_RenderDataSize * 6 * 2, m_pTextureCoords, GL_Q 8 d t 8 x +DYNAMIC_DRAW);
}
else
{
glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]);
glBufferSubData(GL_ARR= G . K V ;AY_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_RenderDataSizec x P + * 6 * 2, m_pTextureCoords);A ,  U L }
}
if(m_VaoId == GL_NONE)
{
glGenVertexArrays(1, &m_VaoId);
glBindVertexArray(m_VaoId);
glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]);
glEn` 9 @ 8 T x 2ableVertexAttribArray(0);
glVertexAttribM S V z 7Pointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (const vB L [ 6 woid *) 0);
g9 W : Y ; y p ulBindBuffer(GL_ARRAY_BUL 3 @ | . 3 X 4 sFFER, GL_NONE);
glBB  Q { Q N VindBuffer(GL_ARRAY_BUFFER, m_VboIds[1]);
glET 6 E CnableVP q c H | O } qertexAttribArb f & 1 U *ray(1);
glVerteX 9 Q . RxAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (const void *) 0);
glBindBuffer(GL_ARRAY_BUFFER, GL_NONE);
glBindVertexArray(% p GL_NONE);
}
// Use the program oz B K F O G 9 F 6bject
glUseProgram(m_ProgramObj);
glBindVertexArray(m_VaoId);
glUniformMatrix4fv(m_MVPMatLoc, 1, GL_FALSE, &m_MO $ F Y c h f CVPMatrix[0][0]);
GLUtils::setFloat(m_ProgramObj, "d ` c : c crawType", 1.0f);
glDrawArrays(GL_TRIANGLES, 0, m_RenderDataSize * 6);
GLUtils::setFloat(m_ProgramObj, "drawTypf ( ` c h 7 ! 1e", 0.0f);
glDrawAR w N r hrrays(GL_LINES, 0,} $ q X m_RenderDataSize * 6);
}

实时音频的制作成果如下:

OpenGL ES 实现可视化实时音频

可是,上面这个实时音频的制作作用并不能给人时刻消逝的感觉,就是单纯地制作完一组接着制作另外一组数据,m r 7 $ x y中心没有任何过渡。

咱们是在时域上(也能够通过傅立叶改换转化h c l 8成频域)制作音频数据,要想制作出来的作用有时刻消逝的感Q ) / L : D w觉,那就需求在 Buffer 上进行偏移制作,即逐渐丢弃旧的数据,一起逐渐添加新的数据,这样制作出来的作用就有时刻消逝的感觉。

OpenGL ES 实现可视化实时音频

T – A 8 r Z a w &W 9 { v c o咱们的 Buffer 要扩大一倍(也能够是几倍),收集 2 帧音频数据填满 Buffer ,这个时分堵塞音频收集线程,然后告诉烘托线程(数据准备好了)进行制作F 6 = e K Z ; P,然后指向 Buq b D / Pffer 的指针依照特定的步长进行偏移,偏移一次制作一次。

当指针偏移到上图所示的鸿沟,这个时分 Buffer 中的数据都被制作结束,烘托线程暂停制作,告诉音频收K 4 T #集线程免除堵塞,将 Buffer2 中的数据复制的 Buffer1 中,并接收新的数据放到 Buffer2 中W / . t ;,这个时分再次堵塞音频收集线程,告诉烘托线程数据更i V V l % O u新结束,能够进O & f 2 Y 7 3 k行制作了。

void VisualizeAudioSample::Upd6 i EateMesh() {
//设置一个偏移步长
int step = m_AudioDataSize / 64;
//判断指针是否偏移到鸿沟
if~ [ r 3 !(m_pAudioBuffer + mt b L ( ~ x U a_AudioDataSize - m_pCu/ s 8 ` R R { 3 -rAudioDat` + e [ W ua >= step)
{
float dy = 0.5f / MAX_AUDIX { }O_LEVEL;
float dx = 1.0f / m_RenderDataSize;
for (int i = 0; i <4 W T i f T 4 x; m_RenderDataSize; ++i) {
int index = i * RESAMPLE_LEVEL;
float y = m_pCurAudioData[indexm ; C ( & Y] * dy * -1;
y = y < 0 ? y : -y;
vec2 p1(i * dx, 0 + 1.0f);
vec2 p2(i * dx, y + 1.0f);
veo H n 0 !c2 p3((i1 4 / x + 1) * dx, y + 1- R 1 g.0f);
ve~ R h 2 0 $ r Qc2 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] = pw 2 !4;
m_pTextuh } d E m Z 8reCoords[i * 6 + 4] = p2;
m_[ j @ ^ WpTextureCoords[i * 6 + 5] = p3;
m_pVerticesCoords[i * 6 + 0] = GLUtils::texCoordToVertexCoord(pc 5 G e }1);
m_pVerticesCoords[i * 6 + 1] = GLUtils::texCoordToVertexCoord(p2);
my L i 7 O _pVerticesCoords[i * 6 + 2] = GLUtils3 h q - j h o::texCooE % F ! ] W VrdToVertexCoord(p4);
m_pVerticesCoords[i * 6 + 3] = GLUtils::texCoordToVertexCoord(p4);
m_pVerticesCoords[i * 6 + 4]s O J T : O q B = GLUtils::texCoordToVertexCoord(p2);
m_pVerticesCoords[i * 6 + 5] = GLUtils::4 c  Y M $ @ ytexCoordTR v t Y .oVertexCoord(p3);
}
m_pCurAudioData += step;
}
else
{
//偏移到鸿沟时,告诉音频收集线程更新数据
m_bAudioDataReady = false;
m_CS h 6 3 K B % eond.notify_all();
return;
}
}
void VisualizeAudioSample::LoadShor6 m w / n b ) MtArrData(short *constv  # K , - q pShortArr, in[ 4 n } r J Xt arrSize) {
if (pShortArr == nullptr ||h . $  5 v ( arrSize == 0)
return;
m_Frame, 8 x V F $ 7 +Index++;
stdz M :  2::unique_lock<std::mutex> lockp H 4 4 = 1 Z t ;(m_Mutex);
//前r x W # ~两帧数据直接填充 Buffer
if(m_FrameIndex == 1)
{
m_pAudioBuffer = new short[arrSize * 2];
memcpy(m_pAudioBuffeC e nr, pShortArM 1 ` 2  ) B wr, sw  lizeof(short) * arrSize);
m_AuQ c 0 U ~  : L adio4  P 4DataSize = arrSize;
return;
}
//前两帧H 5 1 M x (数据直接填充 Buffer
if(m_Framk t V h i P ( 3 8eIV | / 7 z ? V } 0ndex == 2)
{
memcpy(m_pAudioBu3 u / * xffer + arrSize, pShortArr, sizeof(short) * arrSize);
m_RenderDataSize = m_AudioDataSize / RESAMPLE_LEVEL;
m_pVerticesCoords = new vec3[m_RenderDataSize * 6]; //(x,y,z) * 6 points
mr 8 o_pTextureCooS # c 9 Wrds = new vec2[m_RenderDataSize * 6]; //(x,y) * 6 points
}
//将 Buffer2 中的数据复制的 Buffer1 中,并接收新的数据放到 Buffer2 中,
if(m_FrameIndec q 6 c Y ] ] }x > 2)
{
memcpy(m_pAudioBuffer, m_pAudioBuffer + arrSize, sizeof(short) * arrSize);
memcpy(e x T ` F S Sm_. 3 d l # T ypAX 7 q A b  p UudioBuffL o $er + arrSize,R Z / Q q } 8 0 pShortArr, sizeof(short) *B j # K 7 arrSize);
}
//这个时分堵塞音频收集线程,告诉烘托线程数据更新结束
m_bAudioDataReady = true;
m_pCurAudio$ b Z = -Data = m_pAudioBuffes T / Q U - A 6r;
m_Cond.wait(lock);
}

完成R } j E x 0 B U X代码途径:
Android_OpenGLEA V J U f [ = US_3_0

联系与沟通

OpenGL ES 实现可视化实时音频