Android音频架构

Android音频架构

前面《Android音频API》介绍了Android系统提供的四个层面的音频API:

  1. Java层MediaRecorder&MediaPlayer系列;
  2. Java层AudioTrack&AudioRecorder系列;
  3. Jni层opensles;
  4. JNI层AAudio(Android O引入)

本文基于这些API介绍Android系统的音频架构。

下面先上这张经典的Android系统架构图:

![[Android音频架构.png]]

从图上看Andorid整个系统层面从下到上分以下四层:

  1. Linux Kernel
  2. 硬件适配层
  3. Framework层(可分为Java层与C++层)
  4. APP层

我们上面介绍的四个层面的音频API实现均在Framework层,其他各层音频相关有哪些功能?当我们调用某一API时最终是怎么驱动硬件工作的呢?下面我们先看看系统各层音频相关模块及功能。

1. 各层音频模块

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

1.1 Java层

Java层提供了 android.media API 与音频硬件进行交互。在内部,此代码会调用相应的 JNI 类,以访问与音频硬件交互的原生代码。

  • 源代码目录:frameworks/base/media/java/android/media/

  • AudioManager:音频管理器,包括音量管理、AudioFocus管理、音频设备管理、模式管理;

  • 录音:AudioRecord、MediaRecorder;

  • 播放:AudioTrack、MedaiPlayer、SoundPool、ToneGenerator;

  • 编解码:MediaCodec,音视频数据 编解码接口。

1.2 JNI层

与 android.media 关联的 JNI 代码可调用较低级别的原生代码,以访问音频硬件。JNI 位于 frameworks/base/core/jni/ 和 frameworks/base/media/jni 中。

在这里可以调用我们上篇文章介绍的AAudio和OpenSLES接口。

1.3 Native framework 原生框架层

不管是Java层还是JNI层都只是对外提供的接口,真正的实现在原生框架层。原生框架可提供相当于 android.media 软件包的原生软件包,从而调用 Binder IPC 代理以访问媒体服务器的特定于音频的服务。原生框架代码位于 frameworks/av/media/libmediaframeworks/av/media/libaudioclient中(不同版本,位置有所改变)。

1.4 Binder IPC

Binder IPC 代理用于促进跨越进程边界的通信。代理位于 frameworks/av/media/libmediaframeworks/av/media/libaudioclient 中,并以字母“I”开头。

1.5 Audio Server

Audio系统在Android中负责音频方面的数据流传输和控制功能,也负责音频设备的管理。这个部分作为Android的Audio系统的输入/输出层次,一般负责播放PCM声音输出和从外部获取PCM声音,以及管理声音设备和设置(注意:解码功能不在这里实现,在android系统里音频视频的解码是opencore或stagefright完成的,在解码之后才调用音频系统的接口,创建音频流并播放)。Audio服务在Android N(7.0)之前存在于mediaserver中,Android N开始以audioserver形式存在,这些音频服务是与HAL 实现进行交互的实际代码。媒体服务器位于 frameworks/av/services/audioflingerframeworks/av/services/audiopolicy中。

Audio服务包含AudioFlinger 和AudioPolicyService:

  • AudioFlinger:主要负责音频流设备的管理以及音频流数据的处理传输,⾳量计算,重采样、混⾳、⾳效等。

  • AudioPolicyService:主要负责⾳频策略相关,⾳量调节⽣效,设备选择,⾳频通路选择等。

1.6 HAL层

HAL 定义了由音频服务调用且手机必须实现以确保音频硬件功能正常运行的标准接口。音频 HAL 接口位于 hardware/libhardware/include/hardware 中。详情可参阅 audio.h。

1.7 内核驱动层

音频驱动程序可与硬件和 HAL 实现进行交互。我们可以使用高级 Linux 音频架构 (ALSA)、开放声音系统 (OSS) 或自定义驱动程序(HAL 与驱动程序无关)。

注意:如果使用的是 ALSA,建议将 external/tinyalsa 用于驱动程序的用户部分,因为它具有兼容的许可(标准的用户模式库已获得 GPL 许可)。

2. 音频系统架构的演进

一个好的系统架构,需要尽可能地降低上层与具体硬件的耦合,这既是操作系统的设计目的,对于音频系统也是如此。音频系统的雏形框架可以简单的用下图来表示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这个图中,除去Linux本身的Audio驱动外,整个Android音频实现都被看成了User。因而我们可以认为Audio Driver就是上层与硬件间的“隔离板”。但是如果单纯采用上图所示的框架来设计音频系统,对上层应用使用音频功能是不小的负担,显然Android开发团队还会根据自身的实际情况来进一步细化“User”部分。具体该怎么细化呢?如果是让我们去细化我们该怎么做呢?

首先作为一个操作系统要对外提供可用的API,供应用开发者调用。APP开发者开发的应用我们称APP,我们提供的API姑且叫Framework。如果Framework直接和驱动交互有什么问题呢?

  1. 首先是耦合问题,接口和实现耦合,硬件层有任何变动都需要接口层适配,我们增加一层硬件适配层;
  2. 资源统一管理的问题,如果多个APP调用相同API使用硬件资源,改怎么分配?增加统一资源管理器,其实就是对应Android系统的Audio Lib层。

细化后我们发现,整个结构对应的就就是Android的几个层次结构,包括应用层、framework层、库层以及HAL层,如下图所示:

在这里插入图片描述

我们可以结合目前已有的知识,我们分析Lib层和HAL层架构主要设计思路。

2.1 Lib层

framework层的大多数类,其实只是应用程序使用Android库文件的“中介”,它只是个壳子。因为Android应用采用java语言编写,它们需要最直接的java接口的支持,如果我们的Android系统支持另一种语言的运行时,那么可以提供另一种语言的接口支持(比如Go),这就是framework层存在的意义之一。但是作为“中介”,它们并不会真正去实现具体的功能,或者只实现其中的一部分功能,而把主要重心放在核心库中来完成。比如上面的AudioTrack、AudioRecorder、MediaPlayer和MediaRecorder等等在库中都能找到相对应的类,这些多数是C++语言编写的。

我们再从另一个线索来思考这个问题:我们提供的API供应用层调用,那么这个API最终运行在应用的进程中。如果多个应用同时使用这个功能就会冲突;再一个允许任何一个进程操作硬件也是个危险的行为。那么真相就浮出了水面:我们需要一个有权限管理和硬件交互的进程,需要调用某个硬件服务必须和我这个服务打交道。这就是Android系统的很常用的C/S结构以及Binder存在的主要原因。Android系统中的Server就是一个个系统服务,比如ServiceManager、LocationManagerService、ActivityManagerService等等,以及管理图像合成的SurfaceFlinger,和今天我们今天介绍的音频服务AudioFlinger和AudioPolicyService。它们的代码放置在frameworks/av/services/audioflinger,生成的最主要的库叫做libaudioflinger。

这里也提到了分析源码除以模块为线索外的另一种线索以进程为线索。库并不代表一个进程,但是进程则依赖于库来运行。虽然有的类是在同一个库中实现的,但并不代表它们会在同一个进程中被调用。比如AudioFlinger和AudioPolicyService都驻留于名为mediaserver的系统进程中;而AudioTrack/AudioRecorder和MediaPlayer/MediaRecorder只是应用进程的一部分,它们通过binder服务来与其它audioflinger等系统进程通信。

2.2 HAL层

硬件抽象层顾名思义为适配不同硬件而独立封装的一层,音频硬件抽象层的任务是将AudioFlinger/AudioPolicyService真正地与硬件设备关联起来,但又必须提供灵活的结构来应对变化。

从设计上来看,硬件抽象层是AudioFlinger直接访问的对象。这里体现了两方面的考虑:

  • 一方面AudioFlinger并不直接调用底层的驱动程序;
  • 另一方面,AudioFlinger上层(包括和它同一层的MediaPlayerService)的模块只需要与它进行交互就可以实现音频相关的功能了。

AudioFlinger和HAL是整个架构解耦的核心层,通过HAL层的audio.primary等库抹平音频设备间的差异,无论硬件如何变化,不需要大规模地修改上层实现,保证系统对外暴露的上层API不需要修改,达成高内聚低耦合。而对厂商而言,在定制时的重点就是如何在这部分库中进行高效实现了。

举个例子,以前Android系统中的Audio系统依赖于ALSA-lib,但后期就变为了tinyalsa,这样的转变不应该对上层造成破坏。因而Audio HAL提供了统一的接口来定义它与AudioFlinger/AudioPolicyService之间的通信方式,这就是audio_hw_device、audio_stream_in及audio_stream_out等等存在的目的,这些Struct数据类型内部大多只是函数指针的定义,是一个个句柄。当AudioFlinger/AudioPolicyService初始化时,它们会去寻找系统中最匹配的实现(这些实现驻留在以audio.primary.*,audio.a2dp.*为名的各种库中)来填充这些“壳”,可以理解成是一种“多态”的实现。

3. Linux平台下的两种主要的音频驱动架构介绍

上面我们的示例提到了ALSA,这个其实是Linux平台的一种音频驱动架构。下面介绍两种常见的Linux音频驱动架构。

3.1 OSS (Open Sound System)

早期Linux版本采用的是OSS框架,它也是Unix及类Unix系统中广泛使用的一种音频体系。OSS既可以指OSS接口本身,也可以用来表示接口的实现。OSS的作者是Hannu Savolainen,就职于4Front Technologies公司。由于涉及到知识产权问题,OSS后期的支持与改善不是很好,这也是Linux内核最终放弃OSS的一个原因。

另外,OSS在某些方面也遭到了人们的质疑,比如:

  • 对新音频特性的支持不足;

  • 缺乏对最新内核特性的支持等等。

当然,OSS做为Unix下统一音频处理操作的早期实现,本身算是比较成功的。它符合“一切都是文件”的设计理念,而且做为一种体系框架,其更多地只是规定了应用程序与操作系统音频驱动间的交互,因而各个系统可以根据实际的需求进行定制开发。总的来说,OSS使用了如下表所示的设备节点:

设备节点说明
/dev/dsp向此文件写数据à输出到外放Speaker向此文件读数据à从Microphone进行录音
/dev/mixer混音器,用于对音频设备进行相关设置,比如音量调节
/dev/midi00第一个MIDI端口,还有midi01,midi02等等
/dev/sequencer用于访问合成器(synthesizer),常用于游戏等效果的产生

更多详情,可以参考OSS的官方说明:http://www.opensound.com/

3.2 ALSA(Advanced Linux Sound Architecture)

ALSA是Linux社区为了取代OSS而提出的一种框架,是一个源代码完全开放的系统(遵循GNU GPL和GNU LGPL)。ALSA在Kernel 2.5版本中被正式引入后,OSS就逐步被排除在内核之外。当然,OSS本身还是在不断维护的,只是不再为Kernel所采用而已。

ALSA相对于OSS提供了更多,也更为复杂的API接口,因而开发难度相对来讲加大了一些。为此,ALSA专门提供了一个供开发者使用的工具库,以帮助他们更好地使用ALSA的API。根据官方文档的介绍,ALSA有如下特性:

  • 高效支持大多数类型的audio interface(不论是消费型或者是专业型的多声道声卡)
  • 高度模块化的声音驱动
  • SMP及线程安全(thread-safe)设计
  • 在用户空间提供了alsa-lib来简化应用程序的编写
  • 与OSS API保持兼容,这样子可以保证老的OSS程序在系统中正确运行

ALSA主要由下表所示的几个部分组成:

ElementDescription
alsa-driver内核驱动包
alsa-lib用户空间的函数库
alsa-utils包含了很多实用的小程序,比如alsactl:用于保存设备设置amixer:是一个命令行程序,用于声量和其它声音控制alsamixer:amixer的ncurses版acconnect和aseqview:制作MIDI连接,以及检查已连接的端口列表aplay和arecord:两个命令行程序,分别用于播放和录制多种格式的音频
alsa-tools包含一系列工具程序
alsa-firmware音频固件支持包
alsa-plugins插件包,比如jack,pulse,maemo
alsa-oss用于兼容OSS的模拟包
pyalsa用于编译Python版本的alsa lib

Alsa主要的文件节点如下:

  1. Information Interface (/proc/asound)
  2. Control Interface (/dev/snd/controlCX)
  3. Mixer Interface (/dev/snd/mixerCXDX)
  4. PCM Interface (/dev/snd/pcmCXDX)
  5. Raw MIDI Interface (/dev/snd/midiCXDX)
  6. Sequencer Interface (/dev/snd/seq)
  7. Timer Interface (/dev/snd/timer)

Android的TinyALSA是基于Linux ALSA基础改造而来。一看“Tiny”这个词,我们应该能猜到这是一个ALSA的缩减版本。实际上在Android系统的其它地方也可以看到类似的做法——既想用开源项目,又嫌工程太大太繁琐,怎么办?那就只能瘦身了,于是很多Tiny-XXX就出现了。

在早期版本中,Android系统的音频架构主要是基于ALSA的,其上层实现可以看做是ALSA的一种“应用”。后来可能是由于ALSA所存在的一些不足,Android后期版本开始不再依赖于ALSA提供的用户空间层的实现。HAL层最终依赖alsa-lib库与驱动层交互。

4. 一种新的录音方式实现

除了之前提到的系统API,我们还有其他的录音方式吗?答案是肯定的。上面我们提到HAL层依赖alsa-lib库与驱动层交互,我们直接使用alsa-lib,绕开HAL层和Framework层不也可以做到吗(当然前提是要有系统权限)?

为什么会有这种述求呢?在做家居和车载产品时,会有四麦、六麦、甚至八麦的场景。录制大于2麦的设备时需要在HAL层以及Framework层做适配,基于AOSP的修改会显得特别重,特别是一些像回声抑制,声源定位等信号处理算法,如果集成在操作系统,会有更新升级麻烦的问题,我们可以基于alsa-lib在应用层拿到多路数据调用信号处理算法,这样算法模块升级只需要升级APP即可,不需要升级整个系统。

我们先来看看Android系统自带的tinyX系列工具。

4.1 tinymix混响器

在root用户下调用tinymix可以查看硬件驱动支持的混响配置

root@android:/ # tinymix
Number of controls: 7
ctl	type	num	name                                     value
0	ENUM	1	Playback Path                            OFF
1	ENUM	1	Capture MIC Path                         MIC OFF
2	ENUM	1	Voice Call Path                          OFF
3	ENUM	1	Voip Path                                OFF
4	INT	2	Speaker Playback Volume                  0 0
5	INT	2	Headphone Playback Volume                0 0
6	ENUM	1	Modem Input Enable                       ON
root@android:/ #

那么它里面的内容是什么意思呢?

  • 首先我们要知道,一个mixer通常有多个controler,像这个,里面有7个,然后就分别列出每一个controller的信息;
  • 首先看第一个:它的编号为0,类型是ENUM型,它目前的值是OFF,它是用来控制音频输出通道;
  • 同理,第二个也控制音频输入通道;
  • 第三个,通话音频通道;
  • 第四个IP电话音频通道;
  • 第五个扬声器音量,和上层音量值无关;
  • 第六个耳机音量,和上层音量值无关;

一般Playback Path对应的枚举值有:

  1. OFF:关闭
  2. RCV
  3. SPK:扬声器
  4. HP:耳机带麦
  5. HP_NO_MIC:耳机无麦
  6. BT:蓝牙

那么我如果像改变某一项的时候,要怎么设置呢?方法是tinymix ctl value;如果tinymix只跟上控制器的编号,就会把控制器的当前状态显示出来:

# tinymix 7
Audio linein in: On
# tinymix 7 0
root@dolphin-fvd-p1:/ # **tinymix 7**
Audio linein in: Off

4.2 tinycap采集器

使用下面命令即可实现录制并保存到sd卡:

 tinycap 
Usage: tinycap file.wav [-D card] [-d device] [-c channels] [-r rate] [-b bits] [-p period_size] [-n n_periods] tinycap /sdcard/rec.wav -D 0 -d 0 –c 4 –r 16000 –b 16 –p 1024 –n 3

4.3 tinyplay播放

tinyplay
Usage: tinyplay file.wav [-D card] [-d device] [-p period_size] [-n n_periods]
tinyplay /sdcard/test44.wav -D 0 -d 0 -p 1024 -n 3

4.4 程序中集成

现在我们已经通过命令的方式实现了绕开framework的音频采集,我们在自己的app中怎么使用呢?如果还是通过命令的方式只能录制到文件,无法实现流式录制。

解决办法是我们的app依赖tinyalsa库https://android.googlesource.com/platform/external/tinyalsa/,调用asoundlib.h中的read方法模拟tinycap不断读取音频数据。

		struct pcm_config config;config.channels = 4;config.rate = 16000;config.period_size = 1024;config.period_count = 4;config.start_threshold = 0;config.stop_threshold = 0;config.silence_threshold = 0;if (bitDepth == 32)config.format = PCM_FORMAT_S32_LE;else if (bitDepth == 16)config.format = PCM_FORMAT_S16_LE;pcm = pcm_open(0, device, PCM_IN, &config);if (!pcm || !pcm_is_ready(pcm)) {return -1;}int bufferSize = pcm_get_buffer_size(pcm);char *buffer = (char*)malloc(bufferSize);int i = pcm_read(pcm, buffer, bufferSize);if(i ==0){//success}

5. 总结

本文介绍了Andorid系统的整套音频架构,以及架构各层级的功能及作用。并介绍了一种绕开framework层的新的音频采集方式。其实Andorid的音频架构实现是更复杂的一个过程,本文只是简略的对各个模块做了一些介绍,以助于更深入理解上一篇提到的各个API的实现。其实API提供出来的音频接口,都是属于接口层,不论是Java接口还是C++接口,都隶属于应用进程。以采集为例,不论我们调用哪个API,我们都会发现启动后应用进程会多出一个AudioRecord的线程:

在这里插入图片描述

我们启动的录制线程调用API只是从AudioRecord线程写入到Buffer的数据的读取。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/24417.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

探索智慧林业系统的总体架构与应用

背景: 随着人们对森林资源保护和管理的重视,智慧林业系统作为一种新兴的林业管理手段,正在逐渐受到广泛关注和应用。智慧林业系统的总体架构设计与应用,将现代信息技术与林业管理相结合,为森林资源的保护、管理和利用…

注册自定义材质实现qgis里不同比例尺下材质不被拉升的效果

前景提要: 在QGIS里的显示效果,用的是示例的/img/textures/line-interval.png材质图片。 下载示例 git clone https://gitee.com/marsgis/mars3d-vue-example.git 相关效果 比如材质是5像素,在1:100000万比例尺下,线显示的长…

树的重心-java

主要通过深度优先搜索来完成树的重心,其中关于树的重心的定义可以结合文字多加理解。 文章目录 前言☀ 一、树的重心☀ 二、算法思路☀ 1.图用邻接表存储 2.图的遍历 3.算法思路 二、代码如下☀ 1.代码如下: 2.读入数据 3,代码运行结果 总结 前言☀ 主…

电机控制系列模块解析(28)—— 其他功能概述

其他功能概述 软件侧:观测器估计发散保护、时序异常检测 主电路侧:IGBT结温估算、直流母线电容容值估算 电机侧:电机温度估计、轴承异常估计、电机退磁检测 负载侧:负载不平衡检测、掉载检测、负载惯量自适应 上述各项功能&a…

新书推荐:2.2.4 第11练:消息循环

/*------------------------------------------------------------------------ 011 编程达人win32 API每日一练 第11个例子GetMessage.c:消息循环 MSG结构 GetMessage函数 TranslateMessage函数:将虚拟键消息转换为字符消息 DispatchMessage函数…

信息系统项目管理师0148:输出(9项目范围管理—9.3规划范围管理—9.3.3输出)

点击查看专栏目录 文章目录 9.3.3 输出 9.3.3 输出 范围管理计划 范围管理计划是项目管理计划的组成部分,描述将如何定义、制定、监督、控制和确认项 目范围。范围管理计划用于指导如下过程和相关工作: ①制定项目范围说明书;②根据详细项目范…

【机器学习】XGBoost: 强化学习与梯度提升的杰作

🌈个人主页: 鑫宝Code 🔥热门专栏: 闲话杂谈| 炫酷HTML | JavaScript基础 ​💫个人格言: "如无必要,勿增实体" 文章目录 XGBoost: 强化学习与梯度提升的杰作引言1. XGBoost概览1.1 什么是XGBoost&#…

纷享销客安全体系:安全运维运营

安全运维运营(Security Operations,SecOps)是指在信息安全管理中负责监控、检测、响应和恢复安全事件的一系列运营活动。它旨在保护组织的信息系统和数据免受安全威胁和攻击的损害。 通过有效的安全运维运营,组织可以及时发现和应对安全威胁,减少安全事…

09.2手工制作docker镜像-kod服务

手工制作docker镜像-kod服务 基于centos6.9系统镜像,搭建kod服务,提交镜像 创建并进入容器 添加centos6系统的yum源和epel源 yum源 curl -o /etc/yum.repos.d/CentOS-Base.repo https://www.xmpan.com/Centos-6-Vault-Aliyun.repo epel源 curl -o /e…

定时器的使用和实现

目录 一.定时器Timer类的主要方法 二.定时器Timer类的使用 三.定时器的模拟实现 一.定时器Timer类的主要方法 定时器Timer类在java.util包中。 使用前先进行实例化,然后使用实例的schedule(TimerTask task, long delay)方法,设定指定的任务task在指…

python数据分析-心脏瓣膜手术风险分析与预测

研究背景 人的心脏有四个瓣膜,主动脉银、二尖、肺动脉和三尖源 不管是那一个膜发生了病变,都会导致心脏内的血流受到影响,这就是通常所说的心脏期膜病,很多是需要通过手术的方式进行改善的。随着人口老龄化的加剧,,心…

8. 正则表达式

正则表达式 在处理字符串时,需要查找符合某些复杂规则的字符串,正则表达式就是用于描述这些规则的工具 一、正则表达式语法 行定位符:用来描述字符串的边界 -->用来匹配一整行 符号匹配位置^行的开始$行的结尾 ^tm : 可以匹配行 tm equa…

高质量 HarmonyOS 权限管控流程

高质量 HarmonyOS 权限管控流程 在 HarmonyOS 应用开发过程中,往往会涉及到敏感数据和硬件资源的调动和访问,而这部分的调用就会涉及到管控这部分的知识和内容了。我们需要对它有所了解,才可以在应用开发中提高效率和避免踩坑。 权限管控了…

19、Go Gin框架集成Swagger

介绍: Swagger 支持在 Gin 路由中使用一系列注释来描述 API 的各个方面。以下是一些常用的 Swagger 注释属性,这些属性可以在 Gin 路由的注释中使用: Summary: 路由的简短摘要。Description: 路由的详细描述。Tags: 用于对路由进行分类的标…

数据挖掘--数据仓库与联机分析处理

什么是数据仓库 (面集时非) 面向主题的:围绕某一主题来构建集成的:图片文字杂糅在一起时变的:随时间变化的数据非易失的:硬盘存放,不易丢失 操作数据库系统(OLTP)与数据仓库(OLAP…

MySQL将错乱的水果信息,截取展示为 品名 英文名 价格 三列展示

将错乱的水果信息,截取展示为 品名 英文名 价格 三列展示 idname1苹果Apple72Plum6李子3Pineapple8菠萝4Mango5芒果5龙吐珠5Buddha’sHand6Olive9橄榄7Raspberry4树莓8Apricot5杏子9Grapefruit9柚子10火龙果Dragonfruit911倒挂金钟Hanging6LobsterClaw12巨峰葡萄Co…

AI办公自动化:批量把docx文档转换为txt文本

任务:把docx文档批量转换成txt,首先让deepseek写了一段代码,但是转换失败。用的是最流行的python-docx库来读取docx文档,但是始终无法读取成功,换成pywin32库就解决问题了。 在deepseek中输入提示词: 写一…

【背包-BM70 兑换零钱(一)】

题目 BM70 兑换零钱(一) 描述 给定数组arr,arr中所有的值都为正整数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个aim,代表要找的钱数,求组成aim的最少货币数。 如果无解,…

docker 命令 ps,inspect,top,logs详解

docker常用命令教程-4 docker ps docker ps 命令用于列出当前正在运行的容器。默认情况下,它只显示正在运行的容器,但你可以使用 -a 或 --all 选项来显示所有容器(包括已停止的容器)。 常用的选项和示例: -a 或 --…

【C语言题解】1、写一个宏来计算结构体中某成员相对于首地址的偏移量;2、写一个宏来交换一个整数二进制的奇偶位

🥰欢迎关注 轻松拿捏C语言系列,来和 小哇 一起进步!✊ 🌈感谢大家的阅读、点赞、收藏和关注 💕希望大家喜欢我本次的讲解💕 目录👑 1、写一个宏,计算结构体中某变量相对于首地址的偏…