基于 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_LEFTCHANNEL_IN_RIGHTCHANNEL_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();
posted @ 2025-08-07 18:02  Qidi_Huang  阅读(322)  评论(0)    收藏  举报