• 音视频开发:音频编码原理+采集+编码实战


    原理:

    1. 消除冗余信息,压缩量最大,也叫有损压缩
    • 剔除人耳听觉范围外的音频信号20Hz以下和20000Hz以上;
    • 去除被掩蔽的音频信号,信号的遮蔽可以分为频域遮蔽和时域遮蔽;
    • 频域遮蔽效应
      屏蔽70分贝以下,20HZ以下,20000HZ以上
      屏蔽分贝小,频率小的声音
      两个频率相近发出的声音,去除低强度的,也就是分贝高的会盖住分贝低的

    • 时域遮蔽效应:
      根根时间推移,相近频率且同时出现的声音,声音强度高的遮蔽强度低的声音,并且去除同一时间段前后杂音,前遮蔽50毫秒,后遮蔽200毫秒,在这段时间内的声音,强度越接近就越会被屏蔽。

    1. 去除冗余信息后,再进行无损压缩;
    • 无损压缩就是压缩后的数据能够解压缩进行还原,有损则不能;
    • 熵编码中有
      哈夫曼编码:用一个很小的二进制数代替一个长的字符串,频率越高,编码越小,频率越低,编码越长
      算术编码:利用小数进行编码,在香农编码的基础改进而来的
      香农编码

    音频编码过程

    数据先同时通过 时域转频域变换器和心理学模型处理数据,前者将数据转换成多种频段的数据,然后剔除不需要的频段数据,后者会去除非人耳听到的范围声音和一些复合声音,最后将两者合并经过量化编码,无损编码之类的,形成比特流数据,在此之前还会有一些辅助数据,此后数据就会变得非常小;

    常见的音频编码器

    opus、aac、Ogg、Speex、iLBC、AMR、G.711, 最常用的编码器是opus aac。
    opus常用于直播,尤其是无延迟的直播,webrtc默认使用opus;
    AAC是应用最广泛的编解码;
    Ogg收费;
    Speex支持回音消除;
    G.711一般用于固定电话,声音损耗严重,通话会失真;

    本文福利, 免费领取C++音视频学习资料包+学习路线大纲、技术视频/代码,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs),有需要的可以进企鹅裙927239107领取哦~

     

    AAC比较适合有一定延迟的直播,AAC-LD属于低延迟编码器

    • AAC编码器:目前应用最广泛,如iOS、安卓和其他嵌入式设备都包含了AAC硬件编解码器,主要学习这个编码器;
      用来取代mp3,比mp3更高的压缩比和保真性更强;



    常用的规格有AAC LC、AAC HE V1 、AAC HE V2三种;

    AAC HE V1 = AAC + SBR;
    AAV HE V2 = AAC + SBR + PS;
    目前AAC HE V1 已经被取代 V2 取代了;

    V2的码流跟V1的差别不是很大,根据声音的数据变化,如果两个声道的差别很大,码流差别就会越小;

    AAC 中header有两种格式:

    就相当于在aac数据前面加了个Header,header里面就会包含aac数据的一些信息,方便进行编解码

    1. ADIF(Audio data interchange format): 特点是只能从头开始解码,可以确定的找到音频数据的开始部分,不能从音频数据中间开始,这种格式常用于磁盘文件中;
    2. ADTS(Audio Data Transport Format):在每一帧的数据里面都会有一个同步字,也就是每帧都有一个header,所以他可以在任意的位置开始进行解码,就像流式数据;
    • ADTS结构: 由7-9个字节组成,通常情况下是7个字节,如果有CRC 就是9个字节,字节中的每一位都有独特的含义;
      • 1~12bit:全部是1也就是0xFFF,表示是同步字;
      • 13:编码规范 0 = MPEG-4 1 = MPEG-2;
      • 14~15:总是0;
      • 16:是否有保护 1 代表 没有 CRC 0 代表有CRC;
      • 17~18:表示的是MPEG-4的音频类型:AAC LC、 AAC HE V1 、AAC HE V2
      • 19~22:表示的是采样率
      • 24~26:通道数
      • 31~33:数据长度,也包括了header的长度
    • 剩余的之后补上



    其中每一十进制数对应的含义:

    Audio Object Type: 在代码中实际获取类型的时候需要进行+1,才是下面的类型
    1 == AAC main
    2 == AAC LC
    5 == SBR == HE V1
    29 == ps == HE V2

    其中的采样率是通过十进制数表示的一个采样率,有一个表,比如:0 == 96000Hz 1 == 88200HZ 等

    音频采集实战

    每个端音频采集的底层和应用层的库是不一样的,所以使用ffmpeg中间层能够实现跨平台开发;

    • Android端的底层库是AudioRecorder,应用层是MediaRecorder;
    • iOS端的底层库是AudioUnit,应用层是AVFoundation;
    • Windows端的常用的是Directshow OpenAL 还有Windows7之上的AudioCore;

    使用ffmpeg有两种采集方式:

    1. 使用命令方式,命令详情查看ffmpeg相关指令的那篇
    2. 使用代码调用api的方式
    • 在mac下的动态库需要对动态库进行签名

    获取本地签名证书列表:/usr/bin/security find-identity -v -p codesigning
    查看动态库是否签名: codesign -d -vv 动态库文件
    签名命令:codesign -fs "iPhone Distribution: 你的签名证书." 动态库文件
    xcode环境:13.2.1
    签名了如果还是报错,关掉沙盒并且设置 Enable Hardened Runtime 为NO
    在项目中设置user header search path的时候,要使用全路径方式,我使用$(PROJECT_NAME)方式,有的头文件在链接的时候会报错;

    采集音频的步骤:

    1. 打开输入输出设备,涉及的包是avdevice avformat 注册设备 设置采集方式,根据平台选择,即设置输入 打开音频设备
    2. 获取数据包 包:avcodec 主要使用av_read_frame方法获取数据 将数据放入packet中 在读取的时候注意缓冲区无未准备好的情况
    3. 将数据输出到文件 创建文件--- fopen 将数据写入文件-- fwrite 关闭文件 -- fclose
    • 打开设备 ·
    1. void startRecorder(void) {
    2. // 上下文
    3. AVFormatContext *av_context = NULL;
    4. AVDictionary *options = NULL;
    5. // 1. 注册设备
    6. avdevice_register_all();
    7. // 2. 设置采集方式
    8. //设置采集方式 mac os 下是AVfoundation Windows下是dshow linux 下是alsa
    9. AVInputFormat *format = av_find_input_format("avfoundation");
    10. // 3. 打开设备
    11. //里面的识别格式为[[video device]:[audio device]] 这里写0 是获取第1个音频设备
    12. char *name = ":0";
    13. // url 是路径 可以是网络路径也可以是本地路径 本地路径mac下的格式是 video : audio 这里表示获取第一个音频设备
    14. int result = avformat_open_input(&av_context, name, format, &options);
    15. if (result != 0) {
    16. char errors[1024];
    17. // 根据返回值生成错误信息
    18. av_make_error_string(errors, 1024, result);
    19. printf("打开设备失败:%s\n", errors);
    20. return;
    21. }
    22. printf("打开设备成功!\n");
    23. get_audio_packet(av_context,&packet_callback);
    24. // 关闭输入 上下文
    25. avformat_close_input(&av_context);
    26. }
    • 读取数据和存储到文件
    1. void get_audio_packet(AVFormatContext *context, void (*packet_callback)(AVPacket)) {
    2. // w == 写 b == 二进制 + == 没有就创建文件
    3. FILE *f = fopen("/Users/cunw/Desktop/learning/音视频学习/音视频文件/code_recorder.pcm", "wb+");
    4. AVPacket *packet = av_packet_alloc();
    5. int result = -1;
    6. // 循环读取设备信息
    7. // result == -35 是Resource temporarily unavailable 因为获取太频繁 设备未准备好,还正在处理数据
    8. // 因为输入设备准备好需要时间 睡一秒后再读取
    9. sleep(1.0);
    10. while ((result = av_read_frame(·context, packet)) == 0 || result == -35) {
    11. if (packet->size > 0) {
    12. packet_callback(*packet);
    13. fwrite(packet->data, packet->size, 1, f);
    14. // 每读取一次 就清空数据包 不然数据包会一直增大
    15. av_packet_unref(packet);
    16. }
    17. }
    18. if (result != 0) {
    19. char errors[1024];
    20. av_make_error_string(errors, 1024, result);
    21. printf("get packet occured error is \"%s\" \n", errors);
    22. }
    23. // 将缓冲区剩余的数据 强制写入文件
    24. fflush(f);
    25. fclose(f);
    26. // 释放packet空间
    27. av_packet_free(&packet);
    28. }
    29. // 回调函数
    30. void packet_callback(AVPacket packet) {
    31. printf("packet size is %d\n",packet.size);
    32. }
    • 播放
    • ffplay 播放pcm数据: ffplay -ar(采样率) 44100 -ac(通道数) 2 -f(采样大小)f32le 文件名

    音频编解码实战

    音频重采样

    就是将音频三元组(采样率 采样大小 通道数)的值转成另外一组值

    1. 应用场景:

    1、从设备采集的音频数据与编码器要求的不一致;
    2、扬声器要求的音频数据与要播放的音频数据不一致;
    3、方便运算:例如回音消除 将多声道变为单声道;

    2. 如何判断是否需要重采样

    • 了解音频设备的参数
    • 查看ffmpeg源码

    3. 重采样的步骤

    api:需要使用libswresample库

    1. 创建重采样上下文

     - swr_alloc_set_opts 通过设置采样参数获取上下文      

    2. 设置参数

    1. - 参数大体分为输出的采样率、采样大小、声道和输入的采样率、采样大小、声道;
    2. - out_ch_layout:表示声道也可以是布局(扬声器的布局)AV_CH_LAYOUT_STEREO 立体声;
    3. - out_sample_fmt:输出的采样格式 16 = AV_SAMPLE_FMT_S16 或者 32 = AV_SAMPLE_FMT_FLT;
    4. - av_sample_fmt_s16 in_ch_layout:输入的声道布局 ;
    5. - in_sample_fmt 输入的采样格式 ;
    6. - in_sample_rate: 输入的采样率;
    7. - 后两位是log相关 0null

    3. 初始化重采样

    - swr_init 初始化上下文  

    4. 进行重采样

    1. - swr_convert 开始转换 ,目的就是将输入缓冲区的数据写入输出缓冲区
    2. out:输出结果缓冲区 out_count:每个通道的采样数
    3. in:输入的缓冲区 in_count:输入的单个通道的采样数
    4. - 因为重采样的数据需要重新构造所以需要创建输入缓冲区和输出缓冲区
    5. 使用av_sample_array_and_samples audio_data创建
    6. 其中的单通道采样数(单位是字节)`nb_samples = pkt.size / (32/ 8) / 2(通道数)`
    7. linessize:缓冲区大小 align:对齐 0
    8. - 在转换前需要将pkt的data按字节拷贝到输入缓冲区,调用memcpy需要引用string.h
    9. - 将输出数据写入文件
    10. 将输出缓冲区已经转换的数据写入文件

    5. 释放资源

    1. - 还有输入输出缓冲区的释放av_freep
    2. - swr_free释放上下文

    重采样上下文初始化代码

    1. SwrContext * init_swr_context(void) {
    2. SwrContext *context = NULL;
    3. // 假设已经提前知道输入音频数据的三要素的值 AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_FLT, 44100
    4. context = swr_alloc_set_opts(NULL,
    5. AV_CH_LAYOUT_STEREO,
    6. AV_SAMPLE_FMT_S16,
    7. 44100,
    8. AV_CH_LAYOUT_STEREO,
    9. AV_SAMPLE_FMT_FLT,
    10. 44100,
    11. 0, NULL);
    12. int result = swr_init(context);
    13. if (result != 0) {
    14. char error[1024];
    15. av_make_error_string(error, 1024, result);
    16. printf("初始化重采样上下文失败:%s", error);
    17. }
    18. return context;
    19. }

    将采集的数据重采样后 写入文件代码

    1. void get_audio_packet(AVFormatContext *context, void (*packet_callback)(AVPacket)) {
    2. // w == 写 b == 二进制 + == 没有就创建文件
    3. FILE *f = fopen("/Users/cunw/Desktop/learning/音视频学习/音视频文件/resample.pcm", "wb+");
    4. // 初始化重采样上下文
    5. SwrContext *swr_context = init_swr_context();
    6. // 初始化转换的输入输出缓冲区
    7. uint8_t **out_buffer = NULL;
    8. int linesize_out = 0;
    9. av_samples_alloc_array_and_samples(&out_buffer, &linesize_out, 2, 512, AV_SAMPLE_FMT_S16, 0);
    10. uint8_t **in_buffer = NULL;
    11. int linesize_in = 0;
    12. // nb_samples 单通道采样数 4096 / (32 / 8) / 2 = 1024
    13. av_samples_alloc_array_and_samples(&in_buffer, &linesize_in, 2, 512, AV_SAMPLE_FMT_FLT, 0);
    14. AVPacket *packet = av_packet_alloc();
    15. int result = -1;
    16. // 循环读取设备信息
    17. // result == -35 是Resource temporarily unavailable 因为获取太频繁 设备未准备好,还正在处理数据
    18. sleep(1);
    19. while (((result = av_read_frame(context, packet)) == 0 || result == -35) && isRecording == 1) {
    20. if (packet->size > 0) {
    21. // 开始转换数据
    22. // 先将音频数据拷贝到输入缓冲区 只是重采样音频的话 只需要处理数组的第一个
    23. memcpy(in_buffer[0], packet->data, packet->size);
    24. // 再进行转换
    25. swr_convert(swr_context, out_buffer, 512, (const uint8_t **)in_buffer, 512);
    26. fwrite(out_buffer[0],linesize_out, 1, f);
    27. // 每读取一次 就清空数据包 不然数据包会一直增大
    28. av_packet_unref(packet);
    29. }
    30. }
    31. if (result != 0) {
    32. char errors[1024];
    33. av_make_error_string(errors, 1024, result);
    34. printf("get packet occured error is \"%s\" \n", errors);
    35. }
    36. // 释放重采样资源
    37. if (in_buffer) {
    38. av_freep(&in_buffer[0]);
    39. }
    40. if (out_buffer) {
    41. av_freep(&out_buffer[0]);
    42. }
    43. av_freep(&in_buffer);
    44. av_freep(&out_buffer);
    45. swr_free(&swr_context);
    46. // 将缓冲区剩余的数据 强制写入文件
    47. fflush(f);
    48. fclose(f);
    49. // 释放packet空间
    50. av_packet_free(&packet);
    51. }

    ffmpeg 音频数据编码

    本文福利, 免费领取C++音视频学习资料包+学习路线大纲、技术视频/代码,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs),有需要的可以进企鹅裙927239107领取哦~

    在使用fdk_aac编码器的时候,由于默认的ffmpeg有自带的aac,所以通过avcodec_find_encoder_by_name("libfdk_aac")就获取不到。在编译的时候加上--enable-libfdk-aac。注意:重新编译安装ffmpeg之前最好先删掉之前的ffmpeg,然后更新项目中的动态库;
    如果还不行,试试单独下载安装[fdk_aac](https://www.linuxfromscratch.org/blfs/view/svn/multimedia/fdk-aac.html),再重新编译ffmpeg

    1. 创建编码器 avcodec
      1. avcodec_find_encoder 一种通过名字查找 一种是通过id查找,id的查找方式只会找默认的编码器,比如aac,如果是fdkaac就需要通过名字查找;
    2. AV_CODEC_ID_AAC | opus 其他编码器
    3. "libfdk_aac", aac默认的规格是AAC LC
    4. 创建上下文 avcodexcontext
      设置音频三要素
    5. avcodec_alloc_context3
      3表示第三个版本
    6. sample_fmt = av_sample_FMT_S16 aac编码器不支持flt 32位
    7. chnnel_layout = AV_CH_LAYOUT_STEREO( 或者chanels = 2)
    8. sample_rate = 44100
    9. bit_rate = 64000; (KB 码率)可选设置
    10. profile = FF_PROFILE_AAC_HE_V2; (只有bit_rate=0 才有用) 可选设置,设置编码器规格
    1. 打开编码器
    2. avcodex_opne2
      2表示第二个版本
      送数据给编码器时,编码器内部有一个缓冲区,缓冲一部分数据后才进行编码
    1. 编码
    2. 用AVFrame包装未编码的数据,相当于是个输入,用AVPacket包装已编码的数据,相当于是个输出;
    3. 调用avcodec_send_frame 将avframe缓冲区的数据发送给编码器,如果返回值大于0,就表示数据成功发送到了编码器,接着就可以通过循环使用 avcodec_receive_packet读取编码好的数据到AVPacket,并写入文件中,如果读取的结果是AVERROR(EAGIN)或者是AVERROR_EOF,就停止读取,如果是其他的负数,就停止编码;
    4. av_frame_alloc 堆区初始化frame
    5. 设置frame的nb_samples 单通道一个数据帧采样数 512
    1. format 每个采样的大小 av_sample_fmt_s16
    2. channel_layout 声道 av_ch_layout_stereo
    3. av_frame_get_buffer 分配frame里面buffer的大小
    4. 还要判断frame的buffer是否分配成功
    5. 将重采样后的数据memcpy到frame->data中
    6. 再将frame中的数据塞到编码器上下文中 avcodec_send_frame,该函数会返回一个int , 当结果>=0的时候表明有数据已经在编码缓冲区了;
    7. avcodec_receive_packet 读取编码好的数据 avpacket
    8. av_packet_alloc 分配编码后的数据空间
    9. 因为编码器上下文中有一个缓冲区,其中会缓存多个frame,因此并不是每塞一个frame就会有一个packet出来,所以需要通过一个while循环判断编码器的数据是否>=0,再通过avcodec_receive_packet获取packet,该函数也会返回一个int,如果返回值>=0表明获取成功,如果失败直接退出编码,这个值返回值还有其他含义,需要判断eagain 表明编码器没有数据了或者是有数据但是不够编码 这个eagain需要用AVERROR包装成一个负数,表明数据还没准备好 averror_eof 表明一点数据都没有了;
    10. 最后将数据编码后的数据写入到文件pkt->data,数据格式就是aac了;
    11. 在停止录制的时候,由于编码的缓存区可能还有数据,在最后关闭之前,再去取一遍编码数据放入文件;
    1. 释放资源

    在结束的时候释放frame(av_frame_free) 和packet(av_packet_frame);

    编码实战代码:

    1. 创建fdk_aac编码器及上下文

    1. AVCodecContext* init_codec_context(void) {
    2. // 创建aac编码器
    3. AVCodec *codec = avcodec_find_encoder_by_name("libfdk_aac");
    4. // 初始化上下文
    5. AVCodecContext *context = NULL;
    6. context = avcodec_alloc_context3(codec);
    7. context->sample_fmt = AV_SAMPLE_FMT_S16;
    8. context->sample_rate = 44100;
    9. context->channel_layout = AV_CH_LAYOUT_STEREO;
    10. context->bit_rate = 0;
    11. // bitrate == 0 才会生效
    12. context->profile = FF_PROFILE_AAC_HE_V2;
    13. int result = avcodec_open2(context, codec, NULL);
    14. if (result < 0) {
    15. char error[1024];
    16. av_make_error_string(error, 1024, result);
    17. av_log(NULL, AV_LOG_DEBUG, "创建AAC编码器失败:%s",error);
    18. }
    19. return context;
    20. }

    2. 创建输入缓冲区

    1. AVFrame* create_audio_input_frame(void) {
    2. AVFrame *codec_frame = NULL;
    3. codec_frame = av_frame_alloc();
    4. codec_frame->nb_samples = 512;
    5. codec_frame->channel_layout = AV_CH_LAYOUT_STEREO;
    6. codec_frame->format = AV_SAMPLE_FMT_S16;
    7. int buffer_result = av_frame_get_buffer(codec_frame, 0);
    8. if (buffer_result < 0) {
    9. char error[1024];
    10. av_make_error_string(error, 1024, buffer_result);
    11. printf("frame 缓冲区分配失败:%s", error);
    12. }
    13. return codec_frame;
    14. }

    3. 开始编码并写入文件

    1. void audio_encode(AVCodecContext *ctx, AVFrame *frame, AVPacket *packet, FILE *fl) {
    2. // 将数据送入编码器
    3. int codec_result = avcodec_send_frame(ctx, frame);
    4. while (codec_result >= 0) {
    5. // 从packet中循环读取编码好的数据
    6. codec_result = avcodec_receive_packet(ctx, packet);
    7. if (codec_result == AVERROR(EAGAIN) || codec_result == AVERROR_EOF) {
    8. break;
    9. } else if (codec_result < 0) {
    10. char error[1024];
    11. av_make_error_string(error, 1024, codec_result);
    12. printf("编码器出错:%s 停止编码", error);
    13. } else {
    14. fwrite(packet->data, 1,packet->size, fl);
    15. }
    16. }
    17. if (codec_result < 0) {
    18. char error[1024];
    19. av_make_error_string(error, 1024, codec_result);
    20. printf("将数据送入编码器错误: %s\n",error);
    21. }
    22. }

    4. 调用

    • 先将重采样的数据放入avframe的缓冲区中
    memcpy(codec_frame->data[0], out_buffer[0], linesize_out);
    • 再开始编码
    audio_encode(codec_context, codec_frame, codec_packet, f);
    
    • 总览
    1. void get_audio_packet(AVFormatContext *context, void (*packet_callback)(AVPacket)) {
    2. // w == 写 b == 二进制 + == 没有就创建文件
    3. FILE *f = fopen("/Users/cunw/Desktop/learning/音视频学习/音视频文件/encoder.aac", "wb+");
    4. // 创建编码器上下文
    5. AVCodecContext *codec_context = init_codec_context();
    6. // 初始化输入缓冲区 AVframe
    7. AVFrame *codec_frame = create_audio_input_frame();
    8. // 初始化编码输出缓冲区
    9. AVPacket *codec_packet = av_packet_alloc();
    10. // 初始化重采样上下文
    11. SwrContext *swr_context = init_swr_context();
    12. // 初始化重采样的缓冲区
    13. uint8_t **out_buffer = NULL;
    14. int linesize_out = 0;
    15. uint8_t **in_buffer = NULL;
    16. int linesize_in = 0;
    17. init_resammple_buffer(&in_buffer, &linesize_in, &out_buffer, &linesize_out);
    18. AVPacket *packet = av_packet_alloc();
    19. int result = -1;
    20. // 循环读取设备信息
    21. while (isRecording == 1) {
    22. result = av_read_frame(context, packet);
    23. if (packet->size > 0 && result == 0) {
    24. packet_callback(*packet);
    25. // 开始转换数据
    26. // 先将音频数据拷贝到输入缓冲区 只是重采样音频的话 只需要处理数组的第一个
    27. memcpy(in_buffer[0], packet->data, packet->size);
    28. // 再进行转换
    29. swr_convert(swr_context, out_buffer, 512, (const uint8_t **)in_buffer, 512);
    30. // 将重采样好的数据按字节拷贝到frame缓冲区
    31. memcpy(codec_frame->data[0], out_buffer[0], linesize_out);
    32. audio_encode(codec_context, codec_frame, codec_packet, f);
    33. // 每读取一次 就清空数据包 不然数据包会一直增大
    34. av_packet_unref(packet);
    35. } else if (result == -EAGAIN) {
    36. // result == -35 是Resource temporarily unavailable 因为设备未准备好,还正在处理数据
    37. av_usleep(1);
    38. }
    39. }
    40. // 把缓冲区剩余的数据拿出来编码
    41. audio_encode(codec_context, NULL, codec_packet, f);
    42. if (result != 0) {
    43. char errors[1024];
    44. av_make_error_string(errors, 1024, result);
    45. printf("get packet occured error is \"%s\" \n", errors);
    46. }
    47. // 释放重采样资源
    48. if (in_buffer) {
    49. av_freep(&in_buffer[0]);
    50. }
    51. if (out_buffer) {
    52. av_freep(&out_buffer[0]);
    53. }
    54. av_freep(&in_buffer);
    55. av_freep(&out_buffer);
    56. swr_free(&swr_context);
    57. av_frame_free(&codec_frame);
    58. av_packet_free(&codec_packet);
    59. // 将缓冲区剩余的数据 强制写入文件
    60. fflush(f);
    61. fclose(f);
    62. // 释放packet空间
    63. av_packet_free(&packet);
    64. }

  • 相关阅读:
    python数据分析之Pandas库(一)
    Cisdem Video Player for mac(高清视频播放器) v5.6.0中文版
    [论文阅读]3DSSD——基于Point的三维单阶段目标检测器
    用位运算为你的程序加速
    eProsima Fast DDS(1)
    vue项目npm intall时发生版本冲突的解决办法
    Python用requests库采集充电桩LBS位置经纬度信息
    初阶C语言 - 分支语句(if、switch)
    服务网关Gateway_微服务中的应用
    虹科分享 | 网络仿真器 | 预测云中对象存储系统的实际性能
  • 原文地址:https://blog.csdn.net/m0_73443478/article/details/134161504