1. 多路录音到底难在哪:从AudioRecord的底层逻辑说起
Android录音这件事,表面上看就是拿个AudioRecord往缓冲区里读PCM数据,简单得不能再简单。但一旦把需求改成“同时录6个麦克风”,事情就完全不一样了。我在第一次接到这个需求的时候,第一反应是“开6个AudioRecord实例不就行了”,结果实测下来直接翻车——要么只能打开一两个,要么打开之后数据全是零,要么直接抛异常。后来花了不少时间啃AudioRecord的源码和AudioFlinger的混音策略,才把这条路走通。
先把结论放在前面:Android 13上想同时录制6路麦克风,核心不在于AudioRecord怎么写,而在于设备本身有没有6个物理麦克风通道、HAL层有没有把多通道数据暴露出来、以及AudioRecord能不能用CHANNEL_IN_6这个通道掩码去打开它。三者缺一不可。很多人卡在第一步就以为是自己代码写错了,其实是硬件根本不支持。
这篇文章适合谁看?如果你正在做车载语音、会议全向拾音、工业设备声源定位、或者多麦克风阵列的Android端采集,那这篇内容基本就是给你写的。我会把从设备能力探测、通道掩码选择、AudioRecord参数配置、到实际读数据的完整链路拆开讲,代码可以直接抄。如果你只是想做普通的单麦克风录音,那这篇可能有点“杀鸡用牛刀”,但里面关于AudioRecord参数选择的思路同样适用。
先解释一个最容易被忽略的概念:Android的录音通道数和物理麦克风数量不是一回事。手机上有3个麦克风,不代表你能录到3通道数据。系统默认只会把多麦克风的数据在HAL层做波束成形、降噪之后,混成1路或者2路(立体声)给上层。你想拿到原始的6路独立数据,必须满足两个条件:一是设备的audio_policy_configuration.xml里声明了对应的输入设备支持6通道;二是这个输入设备没有被系统默认的混音策略吃掉。这也是为什么很多人用CHANNEL_IN_STEREO能录到2路,但换成CHANNEL_IN_6就直接报错——不是AudioRecord不认这个常量,是底层根本没有对应的输入profile。
提示:
CHANNEL_IN_6这个常量在Android SDK里是存在的,但它只是一个“请求”,能不能兑现完全看设备。不要以为写了这个常量就一定能录到6路。
我在实际项目里踩过的最大的坑,就是拿一台普通手机去测6路录音,折腾了两天以为是代码问题,最后用AudioManager.getDevices()一查,发现设备只暴露了一个TYPE_BUILTIN_MIC,通道数最大就是2。换了一台带多路MEMS麦克风阵列的开发板之后,同样的代码一次就跑通了。所以下面第一节,我先讲怎么判断你的设备到底行不行,这比写代码重要得多。
2. 动手之前先探底:设备到底支不支持6路输入
2.1 用AudioManager把输入设备的能力翻个底朝天
很多人写录音代码的习惯是直接new一个AudioRecord就开始读,从来不查设备能力。单路录音这么干没问题,多路录音这么干就是纯碰运气。正确的做法是先通过AudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)拿到所有输入设备,然后逐个看它的AudioDeviceInfo里声明的通道掩码和采样率。
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS); for (AudioDeviceInfo device : devices) { int[] channelMasks = device.getChannelMasks(); int[] sampleRates = device.getSampleRates(); int[] encodings = device.getEncodings(); Log.d("AudioProbe", "设备类型: " + device.getType()); Log.d("AudioProbe", "通道掩码: " + Arrays.toString(channelMasks)); Log.d("AudioProbe", "采样率: " + Arrays.toString(sampleRates)); Log.d("AudioProbe", "编码格式: " + Arrays.toString(encodings)); }这段代码跑出来的结果,就是你判断设备能力的唯一依据。如果channelMasks里出现了AudioFormat.CHANNEL_IN_6对应的值(也就是0x3F,二进制6个1),那说明设备在框架层声明了支持6通道输入。如果只有CHANNEL_IN_STEREO(0xC)或者CHANNEL_IN_MONO(0x10),那你就别指望用标准API录到6路了,得走别的路子。
这里有个细节要注意:getChannelMasks()返回的是设备支持的通道掩码集合,不是当前可用的通道数。有些设备会声明支持6通道,但实际打开的时候因为资源被占用或者HAL限制,只能给你2通道。所以探测到支持只是第一步,真正能不能打开还得看AudioRecord.getState()。
2.2 从audio_policy_configuration.xml反推硬件能力
如果你有设备的root权限或者能拿到系统镜像,直接看/vendor/etc/audio_policy_configuration.xml是最准的。这个文件里会定义每个输入设备的profile,包括channelMasks和samplingRates。我一般会重点看这几个字段:
<devicePort tagName="Built-In Mic" type="AUDIO_DEVICE_IN_BUILTIN_MIC" role="source"> <profile name="" format="AUDIO_FORMAT_PCM_16_BIT" samplingRates="48000" channelMasks="AUDIO_CHANNEL_IN_6"/> </devicePort>如果这里写的是AUDIO_CHANNEL_IN_6,那恭喜你,硬件和HAL层是支持的。如果写的是AUDIO_CHANNEL_IN_STEREO,那上层再怎么折腾也拿不到6路。这个文件是只读的,改不了,所以它其实就是设备能力的“判决书”。
注意:有些厂商会在HAL层做手脚,xml里声明支持6通道,但实际驱动只接了2个麦克风,剩下的通道填的是静音数据。这种情况你用AudioRecord能打开,也能读到数据,但读到的6路里可能有4路全是0。判断方法是录一段有声音的音频,然后看每一路的RMS值,如果某几路一直是0,那就是硬件没接。
2.3 一个快速验证设备是否真的能出6路数据的方法
与其纠结配置文件,不如直接写个最小验证程序:用CHANNEL_IN_6打开AudioRecord,录3秒钟,然后把6个通道的数据分别算一下能量。如果6个通道都有明显的能量变化,说明硬件是真的6路;如果只有前2路有数据,后面4路是0,那就是假的6路。
int minBufferSize = AudioRecord.getMinBufferSize( 48000, AudioFormat.CHANNEL_IN_6, AudioFormat.ENCODING_PCM_16BIT); AudioRecord recorder = new AudioRecord( MediaRecorder.AudioSource.MIC, 48000, AudioFormat.CHANNEL_IN_6, AudioFormat.ENCODING_PCM_16BIT, minBufferSize * 2); if (recorder.getState() != AudioRecord.STATE_INITIALIZED) { Log.e("AudioProbe", "6通道初始化失败,设备不支持"); return; }getMinBufferSize这一步很关键。如果设备不支持CHANNEL_IN_6,这个函数会返回ERROR_BAD_VALUE,你就能提前知道走不通,不用等到startRecording才报错。我一般会把这个检查放在最前面,省得后面白忙活。
3. AudioRecord六通道参数怎么配才不翻车
3.1 通道掩码、采样率、缓冲区三者的取舍逻辑
确认设备支持6通道之后,接下来就是配参数。AudioRecord的构造函数有5个关键参数:audioSource、sampleRateInHz、channelConfig、audioFormat、bufferSizeInBytes。这5个参数里,任何一个配错都会导致初始化失败或者录出来是噪音。
先说audioSource。多路录音场景下,我一般用MediaRecorder.AudioSource.MIC或者MediaRecorder.AudioSource.UNPROCESSED。这两个的区别在于,MIC会经过系统的降噪、AGC等处理,UNPROCESSED则是尽量拿原始数据。如果你做的是声源定位、波束成形这类对相位敏感的应用,必须用UNPROCESSED,因为系统的降噪处理会改变各通道之间的相位关系,导致算法失效。但UNPROCESSED不是所有设备都支持,得先用AudioManager.getProperty查一下。
String unprocessedSupported = audioManager.getProperty( AudioManager.PROPERTY_SUPPORT_AUDIO_SOURCE_UNPROCESSED); if ("true".equals(unprocessedSupported)) { audioSource = MediaRecorder.AudioSource.UNPROCESSED; } else { audioSource = MediaRecorder.AudioSource.MIC; }再说采样率。6通道录音对带宽的要求是单通道的6倍。48kHz、16bit、6通道,一秒钟的数据量是48000 × 2 × 6 = 576KB。这个带宽对大多数设备来说不算大,但有些低端设备的I2S总线可能扛不住,会丢帧。如果遇到丢帧,可以降到16kHz试试。我实测下来,48kHz在大多数支持6通道的设备上都能跑,但如果是车载场景,16kHz其实够用了,因为语音的主要能量集中在4kHz以下。
缓冲区大小的计算有个经验公式:bufferSize = minBufferSize × 2。getMinBufferSize返回的是能维持录音不丢帧的最小值,但实际用的时候建议翻倍,给系统留点余量。如果缓冲区太小,read会频繁返回0或者负数;如果太大,延迟会变高。6通道场景下,我一般会再乘一个通道系数:
int minBufferSize = AudioRecord.getMinBufferSize(48000, AudioFormat.CHANNEL_IN_6, AudioFormat.ENCODING_PCM_16BIT); int bufferSize = minBufferSize * 2;3.2 为什么CHANNEL_IN_6不是随便写的
AudioFormat.CHANNEL_IN_6这个常量的值是0x3F,也就是二进制的0011 1111,低6位全是1。Android用位掩码来表示通道布局,每一位代表一个通道。CHANNEL_IN_STEREO是0xC(1100),CHANNEL_IN_MONO是0x10(1 0000)。这些掩码不是随便定的,它们对应的是audio_channel_mask_t这个底层枚举。
当你传CHANNEL_IN_6给AudioRecord的时候,框架会拿这个掩码去匹配设备的输入profile。如果设备的profile里声明的channelMasks包含0x3F,匹配成功,录音通道打开;如果不包含,getMinBufferSize就会返回错误值。这就是为什么我一直强调“先探测再写代码”——掩码对不对,不是你能决定的,是设备决定的。
还有一个容易踩的坑:有些设备支持6通道,但只支持CHANNEL_IN_6,不支持CHANNEL_IN_5或者CHANNEL_IN_4。你如果想录4路,不能直接写CHANNEL_IN_4,得先查设备支持哪些掩码。我遇到过一台设备只声明了CHANNEL_IN_6和CHANNEL_IN_STEREO,想录4路的话只能录6路然后丢掉2路。这种硬件设计上的“任性”,只能靠探测来规避。
3.3 数据读取:6通道PCM在内存里是怎么排的
6通道录音读出来的数据是交错排列的,也就是L1 R1 L2 R2 L3 R3 ...这种形式。每一帧包含6个采样点,每个采样点2字节(16bit)。假设你一次读1024字节,那就是1024 / (2 × 6) = 85帧,每帧6个采样点。
short[] buffer = new short[frameCount * 6]; int read = recorder.read(buffer, 0, buffer.length); // 分离6个通道 for (int i = 0; i < read / 6; i++) { short ch1 = buffer[i * 6]; short ch2 = buffer[i * 6 + 1]; short ch3 = buffer[i * 6 + 2]; short ch4 = buffer[i * 6 + 3]; short ch5 = buffer[i * 6 + 4]; short ch6 = buffer[i * 6 + 5]; // 分别处理 }这里有个性能上的注意点:不要在录音线程里做复杂的通道分离和算法处理。录音线程的唯一任务就是把数据从AudioRecord读到内存里,然后丢给一个阻塞队列,让工作线程去处理。如果你在录音线程里做FFT或者波束成形,很容易导致读数据不及时,进而丢帧。我一般会用一个ArrayBlockingQueue做缓冲,录音线程只管往里塞,工作线程只管从里取。
提示:
read返回的是实际读到的short数量,不是帧数。计算帧数的时候要除以通道数。如果read返回负数,说明录音出错了,常见的是ERROR_INVALID_OPERATION和ERROR_BAD_VALUE。
4. 完整代码实现:从初始化到落盘的每一步
4.1 初始化阶段的完整代码与参数注释
下面这段代码是我在实际项目里用的初始化逻辑,包含了设备探测、参数选择、异常处理。你可以直接复制到项目里改改就能用。
public class MultiChannelRecorder { private static final String TAG = "MultiChannelRecorder"; private static final int SAMPLE_RATE = 48000; private static final int CHANNEL_COUNT = 6; private static final int CHANNEL_MASK = AudioFormat.CHANNEL_IN_6; private static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT; private AudioRecord audioRecord; private Thread recordThread; private volatile boolean isRecording = false; private ArrayBlockingQueue<short[]> dataQueue; public boolean init(Context context) { AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); // 第一步:探测设备是否支持6通道 AudioDeviceInfo[] devices = am.getDevices(AudioManager.GET_DEVICES_INPUTS); boolean support6Ch = false; for (AudioDeviceInfo device : devices) { for (int mask : device.getChannelMasks()) { if (mask == CHANNEL_MASK) { support6Ch = true; break; } } } if (!support6Ch) { Log.e(TAG, "设备不支持6通道输入"); return false; } // 第二步:计算最小缓冲区 int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_MASK, ENCODING); if (minBufferSize == AudioRecord.ERROR_BAD_VALUE || minBufferSize == AudioRecord.ERROR) { Log.e(TAG, "缓冲区计算失败: " + minBufferSize); return false; } // 第三步:选择音频源 int audioSource = MediaRecorder.AudioSource.MIC; String unprocessed = am.getProperty( AudioManager.PROPERTY_SUPPORT_AUDIO_SOURCE_UNPROCESSED); if ("true".equals(unprocessed)) { audioSource = MediaRecorder.AudioSource.UNPROCESSED; } // 第四步:创建AudioRecord int bufferSize = minBufferSize * 2; audioRecord = new AudioRecord(audioSource, SAMPLE_RATE, CHANNEL_MASK, ENCODING, bufferSize); if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) { Log.e(TAG, "AudioRecord初始化失败"); audioRecord.release(); audioRecord = null; return false; } dataQueue = new ArrayBlockingQueue<>(64); Log.i(TAG, "初始化成功, bufferSize=" + bufferSize); return true; } }这段代码里有几个地方值得展开说。第一,getMinBufferSize的返回值一定要检查,它返回负数的时候说明参数组合不被支持,这时候不要硬着头皮往下走。第二,UNPROCESSED的检查用getProperty,这个属性在Android 7.0之后才有,低版本会返回null,所以判断的时候用"true".equals()而不是直接比较。第三,bufferSize我用了minBufferSize * 2,这是经验值,如果你发现录音有杂音或者丢帧,可以再往上加。
4.2 录音线程与数据分发
录音线程的设计原则是“只读不处理”。下面这个recordThread只做一件事:从AudioRecord读数据,然后塞进队列。
public void startRecording() { if (audioRecord == null || isRecording) return; isRecording = true; audioRecord.startRecording(); recordThread = new Thread(() -> { short[] buffer = new short[1024 * CHANNEL_COUNT]; while (isRecording) { int read = audioRecord.read(buffer, 0, buffer.length); if (read > 0) { short[] data = new short[read]; System.arraycopy(buffer, 0, data, 0, read); try { dataQueue.put(data); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } else { Log.w(TAG, "read返回异常: " + read); } } }, "AudioRecordThread"); recordThread.start(); }这里buffer的大小是1024 * 6,也就是一次读1024帧。这个值不是固定的,你可以根据实际情况调整。读到的数据我做了个拷贝再入队,因为buffer是复用的,不拷贝的话下一轮读会覆盖掉。这个拷贝有内存开销,但6通道场景下数据量不大,可以接受。如果你追求极致性能,可以用双缓冲或者直接操作ByteBuffer,但代码复杂度会上升不少。
工作线程从队列里取数据,做通道分离和后续处理:
public void startProcessing() { new Thread(() -> { while (isRecording || !dataQueue.isEmpty()) { try { short[] data = dataQueue.poll(100, TimeUnit.MILLISECONDS); if (data == null) continue; processMultiChannelData(data); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } }, "ProcessThread").start(); } private void processMultiChannelData(short[] data) { int frameCount = data.length / CHANNEL_COUNT; short[][] channels = new short[CHANNEL_COUNT][frameCount]; for (int i = 0; i < frameCount; i++) { for (int ch = 0; ch < CHANNEL_COUNT; ch++) { channels[ch][i] = data[i * CHANNEL_COUNT + ch]; } } // 到这里channels[0]到channels[5]就是6路独立数据 // 可以送去做波束成形、声源定位、或者直接写文件 }4.3 落盘:把6路数据写成WAV文件
调试阶段最实用的功能就是把6路数据分别写成WAV文件,用Audacity或者Adobe Audition打开看波形。WAV头是44字节,写起来不复杂,但有几个字段容易写错。
public static void writeWavHeader(OutputStream out, int sampleRate, int channelCount, int totalDataLen) throws IOException { byte[] header = new byte[44]; int byteRate = sampleRate * channelCount * 2; // RIFF header[0] = 'R'; header[1] = 'I'; header[2] = 'F'; header[3] = 'F'; writeIntLE(header, 4, 36 + totalDataLen); header[8] = 'W'; header[9] = 'A'; header[10] = 'V'; header[11] = 'E'; // fmt header[12] = 'f'; header[13] = 'm'; header[14] = 't'; header[15] = ' '; writeIntLE(header, 16, 16); // fmt chunk size writeShortLE(header, 20, (short) 1); // PCM writeShortLE(header, 22, (short) channelCount); writeIntLE(header, 24, sampleRate); writeIntLE(header, 28, byteRate); writeShortLE(header, 32, (short) (channelCount * 2)); // block align writeShortLE(header, 34, (short) 16); // bits per sample // data header[36] = 'd'; header[37] = 'a'; header[38] = 't'; header[39] = 'a'; writeIntLE(header, 40, totalDataLen); out.write(header); }写WAV的时候有个坑:totalDataLen是数据字节数,不是帧数。6通道16bit的话,每帧12字节。如果你录了1000帧,totalDataLen就是12000。这个值写错了,播放器打开文件会显示时长不对或者直接报错。我一般会先把数据写到临时文件,最后再回填头部,这样就不用提前知道总长度。
注意:如果你要把6路数据写成6个独立的单通道WAV,那每个文件的
channelCount都写1,byteRate也要相应改成sampleRate * 2。不要直接把6通道的数据塞进单通道WAV头里,那样播放出来是噪音。
5. 踩坑实录:那些文档里不会写的坑
5.1 权限、后台限制与录音中断
Android 13对录音权限管得很严。RECORD_AUDIO是危险权限,必须动态申请。但很多人不知道的是,Android 9之后,后台应用默认不能录音。如果你的应用切到后台还想继续录,必须在Foreground Service里录,并且声明foregroundServiceType="microphone"。
<service android:name=".RecordService" android:foregroundServiceType="microphone" android:exported="false"/>没有这个声明,切后台之后AudioRecord.read会返回0或者直接抛异常。我遇到过最诡异的情况是:前台录得好好的,锁屏之后数据就断了,查了半天才发现是后台限制。另外,Android 13还引入了“录音时状态栏显示绿点”的机制,这个不影响功能,但用户能看到,做产品的时候要考虑这个UI提示。
还有一个容易忽略的点:电话打进来的时候,录音会被系统强制中断。这时候AudioRecord会进入STATE_UNINITIALIZED,你需要监听AudioManager.ACTION_AUDIO_BECOMING_NOISY广播,在中断后重新初始化。这个在车载场景里特别重要,因为车载系统经常有电话接入。
5.2 多应用同时录音的冲突问题
Android 9之前,多个应用同时录音是允许的,谁先打开谁先录。Android 9之后,系统引入了“录音焦点”机制,默认情况下同一时间只有一个应用能录音。如果你的应用在录音,另一个应用也想录,后者会录到静音数据。
这个机制对多路录音的影响在于:如果你的应用被别的应用抢了录音焦点,你的6路数据会全部变成0。解决办法是在AndroidManifest.xml里声明android:allowAudioPlaybackCapture(这个是针对播放捕获的,录音焦点没有直接的豁免声明),或者引导用户关闭其他录音应用。更稳妥的做法是监听AudioManager.AudioRecordingCallback,在录音被抢占的时候及时保存数据并提示用户。
AudioManager.AudioRecordingCallback callback = new AudioManager.AudioRecordingCallback() { @Override public void onRecordingConfigChanged(List<AudioRecordingConfiguration> configs) { super.onRecordingConfigChanged(configs); for (AudioRecordingConfiguration config : configs) { if (config.getClientAudioSource() == MediaRecorder.AudioSource.MIC) { // 有其他应用在录音 Log.w(TAG, "录音焦点被抢占"); } } } }; audioManager.registerAudioRecordingCallback(callback, null);5.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
getMinBufferSize返回ERROR_BAD_VALUE | 设备不支持CHANNEL_IN_6 | 用getDevices查通道掩码 | 换设备或降级到2通道 |
| 初始化成功但读到的数据全是0 | 录音焦点被抢占或硬件通道未接 | 检查AudioRecordingCallback | 关闭其他录音应用或换设备 |
| 6路里只有前2路有数据 | HAL层只接了2个麦克风 | 分别计算6路RMS | 确认硬件规格 |
| 录音有杂音或断断续续 | 缓冲区太小或采样率过高 | 增大bufferSize或降低采样率 | 48kHz降到16kHz试试 |
| 切后台后录音停止 | 没有用Foreground Service | 检查Service声明 | 加foregroundServiceType |
| 录出来的WAV时长不对 | WAV头totalDataLen写错 | 检查头部第40字节 | 用临时文件回填头部 |
read返回ERROR_INVALID_OPERATION | AudioRecord状态异常 | 检查getState() | 重新初始化 |
| 6路数据相位不一致 | 用了MIC而不是UNPROCESSED | 检查audioSource | 改用UNPROCESSED |
这张表里的每一条都是我实际踩过的。特别是“6路里只有前2路有数据”这一条,我一开始以为是代码问题,后来用示波器量了麦克风的I2S信号,才发现硬件上只焊了2个麦克风,另外4个通道是悬空的。所以做多路录音之前,一定要先确认硬件规格书,别像我一样白折腾。
5.4 几个提升稳定性的实操心得
第一个心得:录音线程的优先级要调高。Android默认的线程优先级是THREAD_PRIORITY_DEFAULT,录音线程建议调到THREAD_PRIORITY_URGENT_AUDIO。这个在AudioRecord内部其实已经做了,但如果你自己起了线程去读,最好手动设一下。
Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);第二个心得:不要在录音过程中频繁调用getState()。这个函数会加锁,频繁调用会影响录音线程的实时性。我一般只在初始化之后检查一次,运行过程中靠read的返回值来判断状态。
第三个心得:6通道数据的内存拷贝要小心。48kHz、16bit、6通道,一分钟的数据是34MB左右。如果你在录音线程里做深拷贝,GC压力会很大。我一般用short[]池化复用,或者直接用ByteBuffer的direct模式,减少堆内存分配。
第四个心得:测试的时候先用AudioSource.DEFAULT跑通,再换UNPROCESSED。有些设备对UNPROCESSED的支持不完整,用DEFAULT能录到数据,换UNPROCESSED就全是0。先用DEFAULT确认链路通了,再逐步替换参数,这样排查问题的时候变量少。
第五个心得:如果设备支持6通道但只给你2通道数据,试试AudioRecord.Builder。Android 6.0之后推荐用Builder模式创建AudioRecord,它比构造函数更灵活,可以设置setAudioFormat和setBufferSizeInBytes。有些设备用构造函数打不开6通道,用Builder反而可以。
AudioRecord recorder = new AudioRecord.Builder() .setAudioSource(MediaRecorder.AudioSource.UNPROCESSED) .setAudioFormat(new AudioFormat.Builder() .setEncoding(AudioFormat.ENCODING_PCM_16BIT) .setSampleRate(48000) .setChannelMask(AudioFormat.CHANNEL_IN_6) .build()) .setBufferSizeInBytes(bufferSize) .build();这个Builder模式我在几个不同厂商的设备上都试过,兼容性确实比直接调构造函数好一些。特别是那些Android 13的定制ROM,用Builder能绕过一些厂商自己加的检查逻辑。
最后说一个关于CHANNEL_IN_6的冷知识:这个常量在Android的官方文档里其实没有详细说明,很多人以为它只能用于特定场景。但实际上,只要设备的HAL层声明了6通道输入,CHANNEL_IN_6就是通用的。我甚至在Android 13的模拟器上试过,模拟器默认不支持6通道,但如果你在config.ini里把hw.audio.input.channels改成6,重启之后就能用CHANNEL_IN_6录到6路数据(虽然模拟器的6路数据都是同一个麦克风复制出来的)。这个技巧在开发阶段用来验证代码逻辑很有用,不用等硬件到位就能先把代码跑通。