Android调用MicrophoneInfo空对象报错及实时音频频率检测问题
问题描述
在Android Studio中基于MicrophoneInfo相关API获取手机麦克风参数时,调用getId()、getLocation()等返回整型值的基础方法触发空对象报错,报错参考截图:
原有实现代码如下:
package com.example.mi_microfono; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import android.Manifest; import android.content.pm.PackageManager; import android.media.AudioRecord; import android.media.MediaRecorder; import android.media.AudioDeviceInfo; import android.media.MicrophoneInfo.Coordinate3F; import android.media.MicrophoneInfo; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.view.View; import android.widget.TextView; import java.io.IOException; import java.util.List; public class MainActivity extends AppCompatActivity { private MicrophoneInfo miMicrofono; private TextView infoMicrofono; private MediaRecorder grabacion; private String archivoSalida = null; private AudioDeviceInfo micMic; @RequiresApi(api = Build.VERSION_CODES.Q) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO}, 1000); } infoMicrofono = (TextView) findViewById(R.id.infoMicro); configMicro(); } public void configMicro(){ if (grabacion==null){ archivoSalida = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC) + "/grabacion5.mp3"; grabacion = new MediaRecorder(); grabacion.setAudioSource(MediaRecorder.AudioSource.MIC); grabacion.setAudioSamplingRate(44100); grabacion.setAudioEncodingBitRate(96000); grabacion.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); grabacion.setOutputFile(archivoSalida); grabacion.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB ); } try{ grabacion.prepare(); grabacion.start(); }catch (IOException e){ String mensajeError = e.getMessage(); //mensaje.setText(mensajeError); } } @RequiresApi(api = Build.VERSION_CODES.P) public void dataMicro(View view){ int data; try { data = miMicrofono.getId(); infoMicrofono.setText("Locación: " + data); }catch (NullPointerException e){ infoMicrofono.setText("error: " + e); } grabacion.stop(); } }
诉求:修复上述空对象报错,同时实现无需执行音频录制、播放操作,直接实时检测手机麦克风拾取到的音频频率,正确调用getFrequencyResponse()方法。
报错根因
代码中仅声明了MicrophoneInfo miMicrofono变量,从未对其进行赋值,调用其方法时必然触发空指针。MicrophoneInfo为系统返回的硬件信息对象,不支持手动实例化,必须通过系统服务获取。除此之外原有代码还存在两个逻辑问题:
- 权限判断逻辑错误:使用
&&判断权限状态,会出现某一个权限未授权时不发起申请的问题 - 错误依赖MediaRecorder:获取麦克风静态参数不需要启动录制,
getFrequencyResponse()返回的是麦克风硬件出厂标定的静态频率响应曲线,不是实时采集的音频频率
空指针修复方案
- 修正权限判断逻辑,将权限判断条件的
&&改为||,确保录音、存储权限任意一个未授权时都发起申请,等用户授权完成后再执行麦克风相关初始化。 - 通过
AudioManager获取麦克风信息列表,对miMicrofono变量赋值,核心代码如下:
// 在onCreate中初始化AudioManager,等权限授予后执行以下逻辑 AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { List<MicrophoneInfo> micList = audioManager.getMicrophones(); if (micList != null && !micList.isEmpty()) { // 索引0一般为设备内置主麦克风,可根据getLocation()返回值筛选需要的麦克风 miMicrofono = micList.get(0); // 此处即可正常调用getId()、getLocation()、getFrequencyResponse()等方法 // 例:获取静态频率响应参数 List<MicrophoneInfo.FrequencyResponse> staticFreqResponse = miMicrofono.getFrequencyResponse(); } }
实时检测麦克风拾取音频频率实现方案
不需要启动MediaRecorder录制文件,也不需要播放音频,通过AudioRecord读取内存中的裸音频流,配合FFT(快速傅里叶变换)计算即可得到实时频率,实现步骤如下:
- 仅需申请
RECORD_AUDIO权限,无需存储权限(全程不写入本地文件) - 初始化
AudioRecord实例,配置为44100Hz采样率、单声道、16位PCM格式,分配最小缓冲区大小,启动采集后仅在内存中读取音频帧,不做存储 - 在子线程中循环读取音频缓冲区数据,对数据做加窗处理后执行FFT变换,计算各频点的幅值,幅值最高的频点即为当前麦克风拾取声音的主频率
- 将计算结果回调到主线程更新UI,不需要检测时停止采集、释放AudioRecord资源即可
核心实现参考代码:
// 子线程内执行实时采集逻辑 private void startFrequencyDetect() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { return; } int sampleRate = 44100; int channelConfig = AudioFormat.CHANNEL_IN_MONO; int audioFormat = AudioFormat.ENCODING_PCM_16BIT; int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat); // FFT需要的样本点数取2的幂次,此处取1024/2048均可 int fftSize = 2048; AudioRecord audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, channelConfig, audioFormat, Math.max(minBufferSize, fftSize*2)); short[] audioBuffer = new short[fftSize]; audioRecord.startRecording(); // 引入JTransforms库做FFT计算,也可替换为自行实现的轻量FFT算法 FloatFFT_1D fft = new FloatFFT_1D(fftSize); float[] fftData = new float[fftSize]; boolean isRunning = true; new Thread(() -> { while (isRunning) { int readSize = audioRecord.read(audioBuffer, 0, fftSize); if (readSize <=0) continue; // 填充FFT数据,加汉宁窗减少频谱泄漏 for (int i = 0; i < fftSize; i++) { double window = 0.5 * (1 - Math.cos(2 * Math.PI * i / (fftSize - 1))); fftData[i] = (float) (audioBuffer[i] / 32768.0 * window); } fft.realForward(fftData); // 计算各频点幅值,找到最大幅值对应的频率 float maxMagnitude = 0; int maxIndex = 0; for (int i = 2; i < fftSize/2; i++) { float real = fftData[2*i]; float imag = fftData[2*i+1]; float magnitude = (float) Math.sqrt(real*real + imag*imag); if (magnitude > maxMagnitude) { maxMagnitude = magnitude; maxIndex = i; } } // 计算得到当前主频率 float currentFreq = maxIndex * sampleRate * 1.0f / fftSize; // 切到主线程更新UI runOnUiThread(() -> infoMicrofono.setText("当前实时频率:" + currentFreq + "Hz")); } audioRecord.stop(); audioRecord.release(); }).start(); }
注:
getFrequencyResponse()返回的是硬件静态参数,和实时采集的音频频率无关,不需要启动录制就可以直接调用,实时频率必须通过音频流+FFT计算得到。
内容的提问来源于stack exchange,提问作者Diego C
相关产品推荐
相关产品推荐

