测量不同平台UAC RMS曲线的方法

一、不同平台的UAC之间的差异

1.Android

安卓平台的UAC,是基于对PCM数据本身进行衰减后下发到Device的,枚举的时候会下发一个0db给Device(即最大音量),所以Device端获取不到Host端的音量,从而无法同步,做不了相应的UI显示。

2.PC&IOS

PC与IOS平台的UAC,与安卓存在差异,下发数据时,PCM不会随着Host端的音量改变而改变,下发的永远是音频的原始增益,他们的同步机制是利用Feature Unit Control与Android的Host Digital Gain存在明显差异,这也是UAC最标准的做法,如果手机端提前衰减会导致后级处理失去一定的调整空间。

二、不同平台UAC的数据通路

1.Android(国产)

Android Framework

|

↓

AudioFlinger

|

↓

Audio HAL

|

↓

USB Audio HAL

|

↓

USB PCM

|

↓

UAC Device

很多Android设备把USB Audio当普通播放设备:

Host负责最终音量控制,USB设备只是播放器。

所以它不会依赖UAC Feature Unit。

2.Samsung

三星经过RMS测试比较特殊。

它也是Android,但是USB Audio实现比很多国产机更接近标准。

Android Audio Framework

|

↓

数字衰减

|

↓

USB PCM

和国产安卓类似。

三星可能存在:

  • 不同OneUI版本
  • 不同USB模式
  • UAC1/UAC2差异

导致行为变化。

3.IOS

苹果是最标准的一类。

链路:

iOS Audio HAL

|

↓

USB Audio Class

|

↓

USB PCM

|

↓

Device Feature Unit

iOS更倾向:

如果设备声明支持Volume Control,就交给设备处理。

所以:

系统音量:

100%

↓

50%

发送:

SET_CUR

Feature Unit

Volume = -xx dB

Device端根据db值进行对应的音量调整

4.Windows PC

Windows最复杂。

正常UAC设备:

复制代码
Application
       |
Windows Audio Engine
       |
Endpoint Volume
       |
USB Audio Driver
       |
SET_CUR Volume
       |
Device

如果设备支持Feature Unit Volume

Windows会:

  • 保持PCM
  • 发控制命令来同步音量

三、实际RMS测试

测试步骤

1.首先用audicity生成一个振幅为0.1的1khz正弦波,它的RMS为-23.01dBFS

2.关闭Device端的UAC音量同步功能,不响应Host端的音量调节指令,保证I2S输出USB下发的Raw data

3.Device连接不同平台,将音量调到最大,并分别播放第一步生成的正弦波

3.使用逻辑分析仪抓I2S包,并且配置好解码参数,导出CSV文件

4.调用python转化脚本(附在文末),将CSV转化为PCM或者WAV文件

5.audicity打开测试音频,可以看到是逐级递减的,打开RMS分析即可获取各段音量的RMS

测试结果

四、总结

基于以上测试,可得知:Android对于UAC的Feature Unit Voiume 没有强制要求,基本上是采取软增益的方式调整的音频音量,这种方式简单,但是会导致丢失数字动态范围以及各个平台的音量曲线不同,且Device端无法根据手机音量做对应的UI显示,不利于产品推广,在评估UI时需要考虑到这一点

五、CSV转化脚本

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
I2S CSV 转 24bit PCM 工具(自动识别两种 CSV 格式)

支持格式 1(Saleae 逻辑分析仪标准格式):
    name,type,start_time,duration,channel,data
    "I2S / PCM","data",0.0000147,0.00000748,0x0000000000000001,0x0000000000F80F46

支持格式 2(国产逻辑分析仪 / 简化导出格式):
    Time [s],Channel,Value
    0.000010420000000,2,0xFFFE71

用法:
    python i2s_to_pcm.py input.csv output.pcm
    python i2s_to_pcm.py input.csv output.wav --wav
"""

import csv
import struct
import sys
import argparse
import wave


def parse_hex(val: str) -> int:
    """解析十六进制字符串为整数"""
    return int(val.strip(), 16)


def i2s_data_to_int24(raw: int) -> int:
    """
    将原始数据转为 24bit 有符号整数
    自动判断:低24位有效
    """
    val = raw & 0xFFFFFF  # 取低 24 位
    # 24bit 有符号:最高位为符号位
    if val & 0x800000:
        val -= 0x1000000
    return val


def int24_to_bytes_le(val: int) -> bytes:
    """24bit 有符号整数 → 3 字节小端格式"""
    if val < 0:
        val += 0x1000000
    return struct.pack('<I', val)[:3]


def detect_csv_format(csv_path: str):
    """自动识别 CSV 格式,返回格式类型和表头"""
    with open(csv_path, 'r', encoding='utf-8') as f:
        reader = csv.reader(f)
        header = next(reader)

    header_lower = [h.strip().lower() for h in header]

    # 格式 1: Saleae 标准格式(有 name, type, channel, data 列)
    if 'name' in header_lower and 'type' in header_lower and 'channel' in header_lower and 'data' in header_lower:
        return 'saleae', header

    # 格式 2: 简化格式(有 Time, Channel, Value 列)
    if any('time' in h for h in header_lower) and 'channel' in header_lower and 'value' in header_lower:
        return 'simple', header

    raise ValueError(f"无法识别的 CSV 格式,表头: {header}")


def convert_csv_to_pcm(csv_path: str, pcm_path: str,
                       sample_rate: int = 48000,
                       bits_per_sample: int = 24,
                       channels: int = 2,
                       swap_channels: bool = False):
    """
    读取 I2S CSV,输出交错立体声 PCM 文件

    输出格式:
        24bit 小端,立体声,左声道在前,右声道在后(L R L R ...)
    """
    # 自动识别格式
    fmt, header = detect_csv_format(csv_path)
    print(f"[信息] 检测到 CSV 格式: {fmt}")

    left_samples = []   # 左声道样本列表
    right_samples = []  # 右声道样本列表

    total_rows = 0
    data_rows = 0

    with open(csv_path, 'r', encoding='utf-8') as f:
        reader = csv.DictReader(f)

        for row in reader:
            total_rows += 1

            if fmt == 'saleae':
                # 格式1: Saleae 标准格式
                if row.get('type', '').strip() != 'data':
                    continue
                try:
                    ch = parse_hex(row['channel'])
                    data = parse_hex(row['data'])
                except (KeyError, ValueError):
                    continue

                # Saleae: channel=0 → 左声道,channel=1 → 右声道
                # (I2S 标准: WS=0 左声道, WS=1 右声道)
                is_left = (ch == 0)

            elif fmt == 'simple':
                # 格式2: 简化格式
                try:
                    ch = int(row['Channel'].strip())
                    data = parse_hex(row['Value'])
                except (KeyError, ValueError):
                    continue

                # 简化格式: Channel=1 → 左声道,Channel=2 → 右声道
                is_left = (ch == 1)

            else:
                continue

            sample = i2s_data_to_int24(data)
            data_rows += 1

            if swap_channels:
                is_left = not is_left

            if is_left:
                left_samples.append(sample)
            else:
                right_samples.append(sample)

    # 对齐左右声道长度(取较短的)
    min_len = min(len(left_samples), len(right_samples))
    left_samples = left_samples[:min_len]
    right_samples = right_samples[:min_len]

    # 写入 PCM 文件(交错立体声)
    with open(pcm_path, 'wb') as f:
        for i in range(min_len):
            f.write(int24_to_bytes_le(left_samples[i]))
            f.write(int24_to_bytes_le(right_samples[i]))

    # 统计信息
    duration_sec = min_len / sample_rate if sample_rate > 0 else 0
    print(f"[完成] 转换结果:")
    print(f"  CSV 总行数:       {total_rows}")
    print(f"  有效数据行:       {data_rows}")
    print(f"  左声道样本数:     {len(left_samples)}")
    print(f"  右声道样本数:     {len(right_samples)}")
    print(f"  采样率:           {sample_rate} Hz")
    print(f"  位深:             {bits_per_sample} bit")
    print(f"  声道数:           {channels} (立体声)")
    print(f"  音频时长:         {duration_sec:.2f} 秒")
    print(f"  PCM 文件大小:     {min_len * channels * bits_per_sample // 8} 字节")
    print(f"  输出文件:         {pcm_path}")

    return min_len


def pcm_to_wav(pcm_path: str, wav_path: str,
               sample_rate: int = 48000,
               bits_per_sample: int = 24,
               channels: int = 2):
    """PCM 裸流加 WAV 头"""
    with open(pcm_path, 'rb') as f:
        pcm_data = f.read()

    with wave.open(wav_path, 'wb') as wf:
        wf.setnchannels(channels)
        wf.setsampwidth(bits_per_sample // 8)
        wf.setframerate(sample_rate)
        wf.writeframes(pcm_data)

    print(f"  WAV 文件:         {wav_path}")


def main():
    parser = argparse.ArgumentParser(description='I2S CSV 转 24bit PCM / WAV(自动识别格式)')
    parser.add_argument('input_csv', help='输入 CSV 文件路径')
    parser.add_argument('output', help='输出 PCM 文件路径')
    parser.add_argument('--rate', type=int, default=48000,
                        help='采样率 (默认 48000)')
    parser.add_argument('--bits', type=int, default=24,
                        help='位深 (默认 24)')
    parser.add_argument('--wav', action='store_true',
                        help='同时输出 WAV 格式(带文件头)')
    parser.add_argument('--swap-channels', action='store_true',
                        help='交换左右声道')

    args = parser.parse_args()

    print(f"[信息] 输入: {args.input_csv}")
    print(f"[信息] 输出: {args.output}")
    print(f"[信息] 采样率: {args.rate} Hz, 位深: {args.bits} bit")
    print()

    # 转换
    num_samples = convert_csv_to_pcm(
        args.input_csv, args.output,
        sample_rate=args.rate,
        bits_per_sample=args.bits,
        swap_channels=args.swap_channels
    )

    # 可选输出 WAV
    if args.wav:
        wav_path = args.output.rsplit('.', 1)[0] + '.wav'
        pcm_to_wav(args.output, wav_path,
                   sample_rate=args.rate,
                   bits_per_sample=args.bits)

    print()
    print("[提示] 播放 PCM 文件:")
    print(f"  ffplay -f s24le -ac 2 -ar {args.rate} {args.output}")
    print(f"  Audacity → 导入 → 原始数据 → 24bit 有符号 小端 立体声 {args.rate}Hz")


if __name__ == '__main__':
    main()
相关推荐
Android系统攻城狮7 小时前
Linux Gstreamer深度解析之gst_audio_encoder_get_frame_samples_min调用流程与实战(六十)
linux·运维·服务器·音视频·gstreamer音视频·音视频进阶·gstreamer音视频进阶
黑妹天下第一乖7 小时前
第09讲 · 多媒体与音频 SDK:硬件编解码与端侧语音
人工智能·嵌入式硬件·深度学习·机器人·音视频·iot
ken223210 小时前
(aaa) piper TTS 文字转语音 安装、使用;与 calibre 电子书朗读 (****)
音视频
开开心心就好13 小时前
本地文件搜索工具推荐,占用小速度还快
智能手机·ffmpeg·ocr·word·音视频·网络攻击模型·安全架构
阿明副业观察14 小时前
动漫AI视频创作工具在哪找到的?2026年最新动漫AI视频平台与软件指南
人工智能·ai作画·aigc·音视频·ai写作
Eric.4614 小时前
ComfyUI 本地部署 AI 漫剧与 AI 视频流水线:消费级显卡显存优化、角色一致性工程实战
人工智能·自动化·音视频·comfyui·ai漫剧
指针向南15 小时前
视频截图发黑先检查透明区域
图像处理·人工智能·计算机视觉·音视频
daad77716 小时前
手写一个 LPC 语音 Codec:从一帧真实音频看编码到解码的全过程
人工智能·音视频·语音识别
我就是不信17 小时前
用 C 语言实现一个支持 HTML 与视频的 HTTP(S) 服务器
c语言·html·音视频