基于 Android 16 实现 2声道或多声道录音的方法
基于 Android 16 实现 2声道或多声道录音的方法
Qidi Huang 2025.08.07
0. 前言
2017 年,我为一个基于 Android 5.1 的项目添加 5.1 声道 录音功能。当时发现 AOSP 并不支持除了单、双声道以外的录音,于是从上到下对 Java Frameworks --> Native Frameworks --> Audio HAL --> Audio Driver 代码都进行了修改,才实现这个功能。
今天一个同事问我怎么在 Android 14 上使用 APP 录制 12 声道 的音频。于是我重新查看 Android 14 和 16 的代码,才发现 8 年过去了,尽管 Android Audio 的设计已经发生了很大变化,尽管 AOSP 已经添加了对车机场景的支持,但依然不支持使用 setChannelMask() 方式在 APP 层录制两声道以上的声音。不过,后续 Android 版本中新增了 setChannelIndexMask() 方法,我们只要留意一下使用上的限制,就可以很方便地通过它达到在 APP 层进行多声道录音的目的。
AOSP 对多声道录音功能做的限制主要在以下几方面。
1. Java Frameworks 的限制
在 Android 系统中,一个声道参数值是由低位和高位构成的。低 30 位代表声道数量(也意味着声道数量最多支持 30 个),剩余高位代表声道表示方式。
声道表示方式有两种:
- 位置表示法 (AUDIO_CHANNEL_REPRESENTATION_POSITION)
- 数量表示法 (AUDIO_CHANNEL_REPRESENTATION_INDEX)
所谓位置表示法,是指 CHANNEL_IN_LEFT、CHANNEL_IN_RIGHT、CHANNEL_IN_FRONT 等枚举值。
每一个枚举代表一个声道,同时也描述了该声道所期望的相对于听者/说话者的位置。如果想指定多个声道,那么就把多个枚举值或起来使用,比如 (CHANNEL_IN_LEFT | CHANNEL_IN_RIGHT)。采用此种表示法时,通常在代码中使用 setChannelMask() 函数进行设置。
在 AudioFormat.java 文件注释里,有对这些位置的说明。如下:
* TFL ----- TFC ----- TFR T is Top
* | \ | / |
* | FL --- FC --- FR | F is Front
* | |\ | /| |
* | | BFL-BFC-BFR | | BF is Bottom Front
* | | | |
* | FWL lis FWR | W is Wide
* | | | |
* TSL SL TC SR TSR S is Side
* | | | |
* | BL --- BC -- BR | B is Back
* | / \ |
* TBL ----- TBC ----- TBR C is Center, L/R is Left/Right
* All "T" (top) channels are above the listener, all "BF" (bottom-front) channels are below the
* listener, all others are in the listener's horizontal plane. When used in conjunction, LFE1 and
* LFE2 are below the listener, when used alone, LFE plane is undefined.
* See the channel definitions for the abbreviations
所谓数量表示法,是指直接给出声道总数(大于 0,小于 30),而不关心这些声道相对于听者/说话者的位置。采用此种表示法时,通常在代码中使用 setChannelIndexMask() 函数进行设置。
1.1 AudioFormat 的限制
如果使用 位置表示法,在 Android 16 的 AudioFormat.java 文件中,最多只能找到 CHANNEL_IN_5POINT1,也就是 6 声道 的录音参数,而没有 12 声道 的录音参数。
当然,有人会说可以通过 位或 运算来组装 12 声道 参数,从而绕过参数未定义的问题。是的,可以这样组装参数,比如 (CHANNEL_IN_LEFT | CHANNEL_IN_CENTER | CHANNEL_IN_RIGHT | CHANNEL_IN_BACK_LEFT | CHANNEL_IN_BACK_RIGHT | CHANNEL_IN_LOW_FREQUENCY | CHANNEL_IN_TOP_LEFT | CHANNEL_IN_TOP_RIGHT | CHANNEL_IN_FRONT | CHANNEL_IN_BACK | CHANNEL_IN_X_AXIS | CHANNEL_IN_Y_AXIS),但遗憾的是这个参数并不能生效,原因见下一小节。
1.2 AudioRecord 的限制
APP 录音免不了使用 AudioRecord。在 Android 16 中,构造 AudioRecord 的方式有两种:
- new AudioRecord();
- AudioRecord.Builder();
同样,如果使用 位置表示法,那么无论你使用何种 AudioRecord 构造方式,代码都会执行 getChannelMaskFromLegacyConfig() 函数对传入的声道参数进行检查。 即便我们按上一小节的方法组装出 12 声道 录音参数,也会在执行到该检查函数的时候抛出异常,因为它只接受单、双声道参数。代码如下:
// Convenience method for the constructor's parameter checks.
// This, getChannelMaskFromLegacyConfig and audioBuffSizeCheck are where constructor
// IllegalArgumentException-s are thrown
private static int getChannelMaskFromLegacyConfig(int inChannelConfig,
boolean allowLegacyConfig) {
int mask;
switch (inChannelConfig) {
case AudioFormat.CHANNEL_IN_DEFAULT: // AudioFormat.CHANNEL_CONFIGURATION_DEFAULT
case AudioFormat.CHANNEL_IN_MONO:
case AudioFormat.CHANNEL_CONFIGURATION_MONO:
mask = AudioFormat.CHANNEL_IN_MONO;
break;
case AudioFormat.CHANNEL_IN_STEREO:
case AudioFormat.CHANNEL_CONFIGURATION_STEREO:
mask = AudioFormat.CHANNEL_IN_STEREO;
break;
case (AudioFormat.CHANNEL_IN_FRONT | AudioFormat.CHANNEL_IN_BACK):
mask = inChannelConfig;
break;
default:
throw new IllegalArgumentException("Unsupported channel configuration.");
}
if (!allowLegacyConfig && ((inChannelConfig == AudioFormat.CHANNEL_CONFIGURATION_MONO)
|| (inChannelConfig == AudioFormat.CHANNEL_CONFIGURATION_STEREO))) {
// only happens with the constructor that uses AudioAttributes and AudioFormat
throw new IllegalArgumentException("Unsupported deprecated configuration.");
}
return mask;
}
如果使用 数量表示法,同时配合使用 AudioRecord.Builder(),则可以绕过这个限制。 从代码里可以看到 Builder 构造 AudioRecord 对象时,不会调用 getChannelMaskFromLegacyConfig() 函数检查声道参数,而是直接使用 mChannelMask,也就是通过 setChannelIndexMask() 设置进来的值。代码如下:
private AudioRecord(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,
int sessionId, @Nullable Context context,
int maxSharedAudioHistoryMs, int halInputFlags) throws IllegalArgumentException {
......
if ((format.getPropertySetMask()
& AudioFormat.AUDIO_FORMAT_HAS_PROPERTY_CHANNEL_INDEX_MASK) != 0) {
mChannelIndexMask = format.getChannelIndexMask();
mChannelCount = format.getChannelCount();
}
if ((format.getPropertySetMask()
& AudioFormat.AUDIO_FORMAT_HAS_PROPERTY_CHANNEL_MASK) != 0) {
mChannelMask = getChannelMaskFromLegacyConfig(format.getChannelMask(), false);
mChannelCount = format.getChannelCount();
} else if (mChannelIndexMask == 0) {
mChannelMask = getChannelMaskFromLegacyConfig(AudioFormat.CHANNEL_IN_DEFAULT, false);
mChannelCount = AudioFormat.channelCountFromInChannelMask(mChannelMask);
}
......
try (ScopedParcelState attributionSourceState = attributionSource.asScopedParcelState()) {
int initResult = native_setup(new WeakReference<AudioRecord>(this), mAudioAttributes,
sampleRate, mChannelMask, mChannelIndexMask, mAudioFormat,
mNativeBufferSizeInBytes, session, attributionSourceState.getParcel(),
0 /*nativeRecordInJavaObj*/, maxSharedAudioHistoryMs, mHalInputFlags);
if (initResult != SUCCESS) {
loge("Error code " + initResult + " when initializing native AudioRecord object.");
return; // with mState == STATE_UNINITIALIZED
}
}
......
}
另一方面,APP 代码通常会使用 getMinBufferSize() 来获取录音数据的 buffer 大小(推荐值),但在这个函数中,即使 Android 16 也最多只支持对 6 声道 参数的计算。代码如下:
static public int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat) {
int channelCount = 0;
switch (channelConfig) {
case AudioFormat.CHANNEL_IN_DEFAULT: // AudioFormat.CHANNEL_CONFIGURATION_DEFAULT
case AudioFormat.CHANNEL_IN_MONO:
case AudioFormat.CHANNEL_CONFIGURATION_MONO:
channelCount = 1;
break;
case AudioFormat.CHANNEL_IN_STEREO:
case AudioFormat.CHANNEL_CONFIGURATION_STEREO:
case (AudioFormat.CHANNEL_IN_FRONT | AudioFormat.CHANNEL_IN_BACK):
channelCount = 2;
break;
case AudioFormat.CHANNEL_IN_5POINT1:
channelCount = 6;
break;
case AudioFormat.CHANNEL_INVALID:
default:
loge("getMinBufferSize(): Invalid channel configuration.");
return ERROR_BAD_VALUE;
}
int size = native_get_min_buff_size(sampleRateInHz, channelCount, audioFormat);
if (size == 0) {
return ERROR_BAD_VALUE;
}
else if (size == -1) {
return ERROR;
}
else {
return size;
}
}
有人说可以自己手动计算并显式设置这个 buffer 大小,而不是使用 getMinBufferSize()。当然,这是可以的。
如果 APP 代码中不显式指定声道参数,那么 AudioRecord 构造时将默认使用 CHANNEL_IN_MONO 作为录音声道参数。
2. Native Frameworks 的限制
即便通过暴改 AudioRecord 成功传入 12 声道 参数,也不代表可以进行录音了,因为在 Threads.cpp 里还有第二道限制。
AudioRecord 对象构造过程中会在 AudioFlinger 里启动录音线程 RecordThread。线程构造时会通过 readInputParameters_l() 函数从 HAL 读取录音参数,关键代码如下:
RecordThread::RecordThread(const sp<IAfThreadCallback>& afThreadCallback,
ThreadBase::type_t type,
AudioStreamIn *input,
audio_io_handle_t id,
bool systemReady
) :
ThreadBase(afThreadCallback, id, type, systemReady, false /* isOut */),
mInput(input),
mSource(mInput),
mActiveTracks(&this->mLocalLog),
mRsmpInBuffer(NULL),
// mRsmpInFrames, mRsmpInFramesP2, and mRsmpInFramesOA are set by readInputParameters_l()
mRsmpInRear(0)
, mReadOnlyHeap(new MemoryDealer(kRecordThreadReadOnlyHeapSize,
"RecordThreadRO", MemoryHeapBase::READ_ONLY))
, mFastCaptureFutex(0)
, mPipeFramesP2(0)
, mFastTrackAvail(false)
, mBtNrecSuspended(false)
{
snprintf(mThreadName, kThreadNameLength, "AudioIn_%X", id);
mFlagsAsString = toString(input->flags);
if (mInput->audioHwDev != nullptr) {
mIsMsdDevice = strcmp(
mInput->audioHwDev->moduleName(), AUDIO_HARDWARE_MODULE_ID_MSD) == 0;
}
readInputParameters_l();
......
}
而在 readInputParameters_l() 中有对声道参数有一个断言。代码如下:
void RecordThread::readInputParameters_l()
{
const audio_config_base_t audioConfig = mInput->getAudioProperties();
mSampleRate = audioConfig.sample_rate;
mChannelMask = audioConfig.channel_mask;
if (!audio_is_input_channel(mChannelMask)) {
LOG_ALWAYS_FATAL("Channel mask %#x not valid for input", mChannelMask);
}
mChannelCount = audio_channel_count_from_in_mask(mChannelMask);
// Get actual HAL format.
status_t result = mInput->stream->getAudioProperties(nullptr, nullptr, &mHALFormat);
LOG_ALWAYS_FATAL_IF(result != OK, "Error when retrieving input stream format: %d", result);
// Get format from the shim, which will be different than the HAL format
// if recording compressed audio from IEC61937 wrapped sources.
mFormat = audioConfig.format;
if (!audio_is_valid_format(mFormat)) {
LOG_ALWAYS_FATAL("Format %#x not valid for input", mFormat);
}
if (audio_is_linear_pcm(mFormat)) {
LOG_ALWAYS_FATAL_IF(mChannelCount > FCC_LIMIT, "HAL channel count %d > %d",
mChannelCount, FCC_LIMIT);
} else {
// Can have more that FCC_LIMIT channels in encoded streams.
ALOGI("HAL format %#x is not linear pcm", mFormat);
}
......
}
这导致当 mChannelCount 值大于 FCC_LIMIT 时,audioserver 进程直接退出,录音也就无法进行了。
好消息是,在 Android 16 上,FCC_LIMIT 的值恰好为 12。而 Android 11 及之前的版本上,这个值是 8。
3. Android 16 如何支持多声道录音
正如本文开始时提到的,可以使用 Builder,通过 setChannelIndexMask() 方法来设置声道参数,达到多声道录音的目的。不过,由于前述 1.2节 中的限制,我们在构造 AudioRecord 对象时,不可以使用 getMinBufferSize() 来计算 bufferSize 参数值,而应该手动计算出合适的值。否则,APP 将抛出异常并中止运行。
于是,用作多声道录音的 AudioRecord 对象构造代码可以写成类似下方:
int bufferSize_MonoRecord = AudioRecord.getMinBufferSize(SAMPLE_RATE, AudioFormat.CHANNEL_CONFIGURATION_MONO, AudioFormat.ENCODING_PCM_16BIT);
int bufferSize_12ChRecord = bufferSize_MonoRecord * 12;
AudioRecord ar = new AudioRecord.Builder()
.setAudioSource(AudioSource.MIC)
.setAudioFormat(new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(SAMPLE_RATE)
.setChannelIndexMask(0xFFF)
.build()
)
.setBufferSizeInBytes(bufferSize_12ChRecord)
.build();
本文来自博客园,作者:Qidi_Huang,转载请注明原文链接:https://www.cnblogs.com/qidi-huang/p/19027309

浙公网安备 33010602011771号