百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

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

myzbx 2025-03-23 21:39 11 浏览

原理:

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

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

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

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


音频编码过程

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

常见的音频编码器

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

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
  • 打开设备 ·
void startRecorder(void) {
    // 上下文
    AVFormatContext *av_context = NULL;
    AVDictionary *options = NULL;
    // 1. 注册设备
    avdevice_register_all();
    
    // 2. 设置采集方式
    //设置采集方式 mac os 下是AVfoundation Windows下是dshow  linux 下是alsa
    AVInputFormat *format = av_find_input_format("avfoundation");
    
    // 3. 打开设备
    //里面的识别格式为[[video device]:[audio device]]  这里写0 是获取第1个音频设备
    char *name = ":0";

    // url 是路径 可以是网络路径也可以是本地路径 本地路径mac下的格式是 video : audio 这里表示获取第一个音频设备
    int result = avformat_open_input(&av_context, name, format, &options);
    
    if (result != 0) {
        
        char errors[1024];
        // 根据返回值生成错误信息
        av_make_error_string(errors, 1024, result);
        printf("打开设备失败:%s\n", errors);
        return;
    }
    
    printf("打开设备成功!\n");
    

    get_audio_packet(av_context,&packet_callback);
    
    // 关闭输入 上下文
    avformat_close_input(&av_context);
    
}
  • 读取数据和存储到文件
void get_audio_packet(AVFormatContext *context, void (*packet_callback)(AVPacket)) {
    
    // w == 写  b == 二进制  + == 没有就创建文件
    FILE *f = fopen("/Users/cunw/Desktop/learning/音视频学习/音视频文件/code_recorder.pcm", "wb+");
    
    
    AVPacket *packet = av_packet_alloc();
    int result = -1;
    // 循环读取设备信息
    // result == -35 是Resource temporarily unavailable 因为获取太频繁 设备未准备好,还正在处理数据  
  // 因为输入设备准备好需要时间  睡一秒后再读取  
     sleep(1.0);
    while ((result = av_read_frame(·context, packet)) == 0  || result == -35) {
     
        if (packet->size > 0) {
            packet_callback(*packet);
            fwrite(packet->data, packet->size, 1, f);
            // 每读取一次 就清空数据包 不然数据包会一直增大
            av_packet_unref(packet);
          
        }
    }
    
    if (result != 0) {
        char errors[1024];
        av_make_error_string(errors, 1024, result);
        
        printf("get packet occured error is \"%s\" \n", errors);
    }
    
    // 将缓冲区剩余的数据 强制写入文件
    fflush(f);
    fclose(f);
    
    // 释放packet空间
    av_packet_free(&packet);
    
}
// 回调函数
void packet_callback(AVPacket packet) {
    
    printf("packet size is %d\n",packet.size);

}
  • 播放
  • ffplay 播放pcm数据: ffplay -ar(采样率) 44100 -ac(通道数) 2 -f(采样大小)f32le 文件名

音频编解码实战

音频重采样

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

1. 应用场景:

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

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

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

3. 重采样的步骤

api:需要使用libswresample库

1. 创建重采样上下文

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

2. 设置参数

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

3. 初始化重采样

- swr_init 初始化上下文  

4. 进行重采样

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

- 将输出数据写入文件
  将输出缓冲区已经转换的数据写入文件

5. 释放资源

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

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

重采样上下文初始化代码

SwrContext * init_swr_context(void) {
    
    SwrContext *context = NULL;
    
    // 假设已经提前知道输入音频数据的三要素的值 AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_FLT, 44100
    context = swr_alloc_set_opts(NULL,
                                 AV_CH_LAYOUT_STEREO,
                                 AV_SAMPLE_FMT_S16,
                                 44100,
                                 AV_CH_LAYOUT_STEREO,
                                 AV_SAMPLE_FMT_FLT,
                                 44100,
                                 0, NULL);
    
    int result = swr_init(context);
    
    if (result != 0) {
        char error[1024];
        av_make_error_string(error, 1024, result);
        printf("初始化重采样上下文失败:%s", error);
    }
    
    return context;
}

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

void get_audio_packet(AVFormatContext *context, void (*packet_callback)(AVPacket)) {
    
    // w == 写  b == 二进制  + == 没有就创建文件
    FILE *f = fopen("/Users/cunw/Desktop/learning/音视频学习/音视频文件/resample.pcm", "wb+");

    // 初始化重采样上下文
    SwrContext *swr_context = init_swr_context();

    // 初始化转换的输入输出缓冲区
    uint8_t **out_buffer = NULL;
    int linesize_out = 0;
    av_samples_alloc_array_and_samples(&out_buffer, &linesize_out, 2, 512, AV_SAMPLE_FMT_S16, 0);
    uint8_t **in_buffer = NULL;
    int linesize_in = 0;
    // nb_samples 单通道采样数 4096 / (32 / 8) / 2 = 1024
    av_samples_alloc_array_and_samples(&in_buffer, &linesize_in, 2, 512, AV_SAMPLE_FMT_FLT, 0);


    AVPacket *packet = av_packet_alloc();
    int result = -1;
    // 循环读取设备信息
    // result == -35 是Resource temporarily unavailable 因为获取太频繁 设备未准备好,还正在处理数据
    sleep(1);
    while (((result = av_read_frame(context, packet)) == 0  || result == -35) && isRecording == 1) {

        if (packet->size > 0) {


            // 开始转换数据
            // 先将音频数据拷贝到输入缓冲区  只是重采样音频的话  只需要处理数组的第一个
            memcpy(in_buffer[0], packet->data, packet->size);
            // 再进行转换
            swr_convert(swr_context, out_buffer, 512, (const uint8_t **)in_buffer, 512);

            fwrite(out_buffer[0],linesize_out, 1, f);
            // 每读取一次 就清空数据包 不然数据包会一直增大
            av_packet_unref(packet);
        }

    }

    if (result != 0) {
        char errors[1024];
        av_make_error_string(errors, 1024, result);

        printf("get packet occured error is \"%s\" \n", errors);
    }

    // 释放重采样资源
    if (in_buffer) {
        av_freep(&in_buffer[0]);
    }
    if (out_buffer) {
        av_freep(&out_buffer[0]);
    }
    av_freep(&in_buffer);
    av_freep(&out_buffer);
    swr_free(&swr_context);

    // 将缓冲区剩余的数据 强制写入文件
    fflush(f);
    fclose(f);

    // 释放packet空间
    av_packet_free(&packet);
    
    
}

ffmpeg 音频数据编码

在使用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编码器及上下文

AVCodecContext* init_codec_context(void) {
    
    // 创建aac编码器
    AVCodec *codec = avcodec_find_encoder_by_name("libfdk_aac");
    
    // 初始化上下文
    AVCodecContext *context = NULL;
    context = avcodec_alloc_context3(codec);
    context->sample_fmt = AV_SAMPLE_FMT_S16;
    context->sample_rate = 44100;
    context->channel_layout = AV_CH_LAYOUT_STEREO;
    context->bit_rate = 0;
    // bitrate == 0 才会生效
    context->profile = FF_PROFILE_AAC_HE_V2;
    
    int result = avcodec_open2(context, codec, NULL);
    if (result < 0) {
        char error[1024];
        av_make_error_string(error, 1024, result);
        av_log(NULL, AV_LOG_DEBUG, "创建AAC编码器失败:%s",error);
    }
    
    return context;
    
}

2. 创建输入缓冲区

AVFrame* create_audio_input_frame(void) {
    
    AVFrame *codec_frame = NULL;
    codec_frame = av_frame_alloc();
    
    codec_frame->nb_samples = 512;
    codec_frame->channel_layout = AV_CH_LAYOUT_STEREO;
    codec_frame->format = AV_SAMPLE_FMT_S16;
    int buffer_result = av_frame_get_buffer(codec_frame, 0);
    if (buffer_result < 0) {
        char error[1024];
        av_make_error_string(error, 1024, buffer_result);
        printf("frame 缓冲区分配失败:%s", error);
    }
    
    return codec_frame;
}

3. 开始编码并写入文件

void audio_encode(AVCodecContext *ctx, AVFrame *frame, AVPacket *packet, FILE *fl) {
    
    
    // 将数据送入编码器
    int codec_result = avcodec_send_frame(ctx, frame);
    while (codec_result >= 0) {
        // 从packet中循环读取编码好的数据
        codec_result = avcodec_receive_packet(ctx, packet);
        if (codec_result == AVERROR(EAGAIN) || codec_result == AVERROR_EOF) {
        
            break;
        } else if (codec_result < 0 char error1024 av_make_error_stringerror 1024 codec_result printfs error else fwritepacket->data, 1,packet->size, fl);
        }
    }
    if (codec_result < 0) {
        char error[1024];
        av_make_error_string(error, 1024, codec_result);
        printf("将数据送入编码器错误: %s\n",error);
    }
}

4. 调用

  • 先将重采样的数据放入avframe的缓冲区中
memcpy(codec_frame->data[0], out_buffer[0], linesize_out);
  • 再开始编码
audio_encode(codec_context, codec_frame, codec_packet, f);
  • 总览
void get_audio_packet(AVFormatContext *context, void (*packet_callback)(AVPacket)) {
    
    // w == 写  b == 二进制  + == 没有就创建文件
    FILE *f = fopen("/Users/cunw/Desktop/learning/音视频学习/音视频文件/encoder.aac", "wb+");
    
    // 创建编码器上下文
    AVCodecContext *codec_context = init_codec_context();
    // 初始化输入缓冲区  AVframe
    AVFrame *codec_frame = create_audio_input_frame();
    // 初始化编码输出缓冲区
    AVPacket *codec_packet = av_packet_alloc();
    
    // 初始化重采样上下文
    SwrContext *swr_context = init_swr_context();
    // 初始化重采样的缓冲区
    uint8_t **out_buffer = NULL;
    int linesize_out = 0;
    uint8_t **in_buffer = NULL;
    int linesize_in = 0;
    init_resammple_buffer(&in_buffer, &linesize_in, &out_buffer, &linesize_out);
    

    AVPacket *packet = av_packet_alloc();
    int result = -1;
    // 循环读取设备信息
    
    while (isRecording == 1) {
        
        result = av_read_frame(context, packet);
        if (packet->size > 0 && result == 0) {

            packet_callback(*packet);
            // 开始转换数据
            // 先将音频数据拷贝到输入缓冲区  只是重采样音频的话  只需要处理数组的第一个
            memcpy(in_buffer[0], packet->data, packet->size);
            // 再进行转换
            swr_convert(swr_context, out_buffer, 512, (const uint8_t **)in_buffer, 512);
            // 将重采样好的数据按字节拷贝到frame缓冲区
            memcpy(codec_frame->data[0], out_buffer[0], linesize_out);
            audio_encode(codec_context, codec_frame, codec_packet, f);
            // 每读取一次 就清空数据包 不然数据包会一直增大
            av_packet_unref(packet);
        } else if (result == -EAGAIN) {
            // result == -35 是Resource temporarily unavailable 因为设备未准备好,还正在处理数据
            av_usleep(1);
        }
    }
    // 把缓冲区剩余的数据拿出来编码
    audio_encode(codec_context, NULL, codec_packet, f);

    if (result != 0) {
        char errors[1024];
        av_make_error_string(errors, 1024, result);
        printf("get packet occured error is \"%s\" \n", errors);
    }

    // 释放重采样资源
    if (in_buffer) {
        av_freep(&in_buffer[0]);
    }
    if (out_buffer) {
        av_freep(&out_buffer[0]);
    }
    av_freep(&in_buffer);
    av_freep(&out_buffer);
    swr_free(&swr_context);
    
    av_frame_free(&codec_frame);
    av_packet_free(&codec_packet);

    // 将缓冲区剩余的数据 强制写入文件
    fflush(f);
    fclose(f);

    // 释放packet空间
    av_packet_free(&packet);
    
    
}

相关推荐

Luminati代理动态IP教程指南配置代理VMLogin中文版反指纹浏览器

介绍如何使用在VMLogin中文版设置Luminati代理。首先下载VMLogin中文版反指纹浏览器(https://cn.vmlogin.com)对于刚接触Luminati动态ip的朋友,是不是不懂...

文档中图形及子图形的处理(word中的图形对象有何特点)

【分享成果,随喜正能量】走得越远,见识越多,认识的人越多,你就越能体会到,人这一辈子,你真的在意的,同时又在意你的人,就那么几个,这几个人,就是你全部的世界。三两知己,爱人在侧,父母康健,听起来平淡无...

Python爬虫破解滑动验证码教程(python绕过滑动验证码)

破解滑动验证码通常需要结合图像识别和模拟人类操作,以下是分步骤的解决方案:1.分析验证码类型缺口识别型:背景图带缺口,滑块图带凸块轨迹验证型:除了位置还需模拟人类移动轨迹2.获取验证码图片方法一:...

「教程」5 分钟带你入门 kivy(新手kp教学)

原创:星安果AirPythonkivy语言通过编写界面UI,然后利用Python定义一些业务逻辑,可以移植很多功能模块到移动端直接执行。下面对kivy常见用法做一个汇总。1、什么是...

比呀比: Fossil Estate Canvas EW 男式复古邮差包 $70.99

Fossil是一个来自美国的全球性生活时尚品牌,始建于1984年,专注于时尚配件,是第一个将手表的价值与款式完美结合的美国品牌,如今Fossil已跃身成为美国最受欢迎的品牌之一。这款FossilE...

智能教学:如何在网上授课(网上授课怎么弄)

摘要:因为担心传统课堂可能会传播冠状病毒,许多大学已经开始在网上授课。耶鲁-新加坡国立大学的讲师凯瑟琳·谢伊·桑格(CatherineSheaSanger)解释了如何快速而有效地做到这一点。当新型冠...

wxPython库教程系列之图片:托盘图标和图片缩放、移动

1概要:=====1.1托盘图标设置1.2普通图片显示:原图显示,缩放显示,窗口与图片大小相互适应。1.3按钮图片设置1.4移动图片和zoom菜单按钮联动设置2托盘图标:========2...

UE4渲染目标开发教程(ue4渲染效果图质量怎么样)

渲染目标(RenderTarget)是你可以在运行时写入的纹理。在引擎方面,它们存储基础颜色、法线和环境光遮蔽等信息。在用户方面,渲染目标主要用作一种辅助相机。你可以将场景捕捉指向某物并将图像存储到...

比呀比: Fossil 化石 Canvas NS 男士复古帆布斜挎包 $57.59

FossilCanvasNS男士复古帆布斜挎包,尺寸约为26.5*11*33厘米。采用100%纯棉帆布面料,融合了休闲与百搭的外形,在经典的款型呈现复古质感。内设1个拉链袋,2个搭扣数码产品袋和...

比呀比: Timberland 添柏岚 Canvas Cord Case 帆布旅行手包 $5.99

Timberland添柏岚这款耐用帆布旅行手包,虽然一眼过去,觉得不咋地,但是品牌和质量还是妥妥滴,非常适合装一些零零碎碎的小东西,便于携带,多色可选,重点是价格更是感动价啊。目前这款包在6pm报价...

提炼文章/知识资料,两键转换成小红书图片

现在AI的功能已经越来越强大了,通过AI可以提高我们不少工作效率。刚好前几天做了一个几乎“一气呵成”,把长文章转成小红书卡片的流程Demo,分享给大家。之前发过两篇利用AI把长文章转成小红书图片...

python海龟绘图turtle(一):画布和窗体

海龟绘图(turtle)是python的一个有趣的内置模块,是python语言的标准库之一,是入门级的图形绘制函数库。海龟绘图(turtle)可以根据编写的控制指令(代码),让一个小“海龟”在屏幕上来...

在文档中添加画布及图片(word中如何添加画布)

【分享成果,随喜正能量】宁可正而不足,不可邪而有余。相识满天下,知心能几人。书七成,戏三分,牛皮灯影胡编成。布施不如还债,修福不如避祸。勿以恶小而为之,勿以善小而不为。。《VBA之Word应用》,是我...

知识管理神器 Obsidian,终于有了白板功能!

沙牛提示阅读本文需要3分钟,Obsidian白板功能来了!如果你喜欢本文,就分享给你的小伙伴!01白板继双链笔记之后,这一年,白板类工具开始火了起来。顾名思义,白板类工具,它给了你一张无限尺寸...

虚拟背景第一弹!教你如何在家中优雅地“学在交大”!

交大将于3月2日正式开始线上教学(3月1日举行线上教学第一课|视频直播课)目前正处于网课试课阶段交大在线课程教学以ZOOM、Canvas等作为主平台平台的虚拟背景功能可以具特别的环境效果更好地沉浸课堂...