音视频同步版本【基于音频】

其实和基于外部时钟的原理操作基本上一模一样。只不过音频帧不需要去匹配现实时钟了,只有视频帧需要匹配现实时钟。而视频帧需要去匹配音频帧的时间,那么就需要给时钟设置一个补偿,因为现在是以音频帧为标准。假如现在现实时钟到了50pts,而音频帧到了80pts,那么10ms后出现的视频帧匹配的时钟pts应该就是90pts【现实时钟的60pts加上补偿的30pts(30=80-50,这里是在读取音频帧的时候设置的补偿)】

#include <iostream>
#include <windows.h>
#include<queue>
#include<chrono>
#include<ctime>
#ifdef __cplusplus  ///
extern "C"
{
// 包含ffmpeg头文件
#include "libavutil/avutil.h"
#include"libavformat/avformat.h"
#include"libswscale/swscale.h"
#include"libswresample/swresample.h"
// 包含SDL头文件
#include"SDL.h"
}
#endif

using namespace std;

class AVSync{
public:
    AVSync() {

    }

    void init()
    {
        start_time = getNowMilliseconds();
    }

    // 获取当前时间的pts应该是多少了
    int getPts() {
        /* +上drift代表补偿偏差的时间,
           如果drift大于0,代表当前时间比音频时间慢了,所以实际上pts要更大才对
           如果小于0,则相反
        */
        return getNowMilliseconds() - start_time + drift;
    }

    // 设置音频pts与现实时钟的偏差
    // pts_单位是秒
    void setClock(double pts_) {
        // 现实时钟的pts
        int real_pts = getNowMilliseconds() - start_time;
        // 更新偏差值
        drift = pts_ * 1000 - real_pts;//音频比现实时钟快了多少
    }

    // 毫秒时间戳。【获取1970年到现在过去了多少微秒,例如:1672531199876】
    Uint64 getNowMilliseconds() {
        return getNowMicroseconds() / 1000;
    }

    // 微秒时间戳。【获取1970年到现在过去了多少微秒,例如:1672531199876543】
    Uint64 getNowMicroseconds() {
        using namespace std::chrono;
        //
        system_clock::time_point time_point_now = system_clock::now();
        system_clock::duration duration = time_point_now.time_since_epoch();
        return duration_cast<microseconds>(duration).count();
    }

private:
    // 音视频播放启动的时间--毫秒时间戳
    Uint64 start_time = 0;
    // 音频pts与现实时钟的偏差【毫秒值,而不是时间戳】
    int drift = 0;
};

// 线程停止运行标识,0为正在运行,1为停止
int thread_exit = 0;
// 当前帧音频PCM数据
static Uint8 *audio_pcm_g;
// 当前帧音频PCM数据的字节总大小长度
static Uint32 audio_len_g;
// 音视频帧队列
queue<AVFrame*> audio_frame_queue_;
queue<AVFrame*> video_frame_queue_;
// 将main方法中的变量提取到全局以供两个线程函数中使用
AVFormatContext *input_fmt_ctx = NULL;
int video_idx = -1;
int audio_idx = -1;
AVCodecContext *audio_codec_ctx;
// 音视频同步工具类
AVSync sync_;

// 输出错误信息
void showError(int ret, const char *methodName = "method")
{
    if(ret == 0) {
        return ;
    }
    // 错误消息日志
    char err2str[256];
    // 将返回结果转化为字符串信息
    av_strerror(ret, err2str, sizeof(err2str));
    printf("%s failed, ret:%d, msg:%s\n", methodName, ret, err2str);
}

// 填充PCM数据到SDL中
void fill_audio_pcm(void *udata, Uint8 *stream, int len) {
    // 清空上一帧的数据
    SDL_memset(stream, 0, len);
    // 如果外部线程【主线程读帧】还未读取到数据,那么无法填充PCM到SDL中进行播放
    if(audio_len_g == 0)
    {
        return ;
    }
    // 本次回调结束最多只能取len字节的数据
    // 如果外部读取的帧小于len字节,那么直接填充外部读取到的所有数据即可
    // 如果外部读取的帧大于len字节,那么本次填充len字节的数据,等下次回调再填充 audio_len_g - len字节的数据
    // 【如果audio_len_g - len 还是大于了len字节,那么继续取len填充即可】
    len = len > audio_len_g ? audio_len_g : len;
    //填充PCM数据到SDL中
    SDL_MixAudio(stream, audio_pcm_g, len, SDL_MIX_MAXVOLUME/2);// SDL_MIX_MAXVOLUME/2 为音频大小,在0-128之间调整
    // 更新pcm内存指针指向位置,已经【又】使用了len个字节空间,那么下次需要从当前位置+len的位置开始使用
    audio_pcm_g += len;
    // 更新剩余字节大小数量,已经读取了len个字节大小的数据,那么下次还剩 audio_len_g - len 个字节大小的数据可以使用
    audio_len_g -= len;

}

// 视频播放线程
int play_video_thread(void *opaque) {
    // SDL
    // 初始化视频
    if(SDL_Init(SDL_INIT_VIDEO)) {
        return -1;
    }
    // 视频宽度
    int video_width_ = input_fmt_ctx->streams[video_idx]->codecpar->width;
    // 视频高度
    int video_height_ = input_fmt_ctx->streams[video_idx]->codecpar->height;
    // 创建窗口--显示器
    // 在这里设置显示出来的窗口的总大小
    SDL_Window *win_ = SDL_CreateWindow("苏花末测试窗口", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
                                    video_width_, video_height_, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
    if(!win_) {
        return -1;
    }
    // 渲染器,用于将纹理渲染到窗口上
    SDL_Renderer *renderer_ = SDL_CreateRenderer(win_, -1, 0);
    if(!renderer_) {
        return -1;
    }
    // 纹理,用于设置渲染图片数据
    SDL_Texture *texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_IYUV,  SDL_TEXTUREACCESS_STREAMING, video_width_, video_height_);
    if(!texture_) {
        return -1;
    }
    // Rect--页面显示区域
    SDL_Rect rect_;
    // 刷新事件队列【防止有缓存】
    SDL_PumpEvents();
    SDL_Event event;
    // 线程运行中
    while(thread_exit == 0)
    {
        // 在没有事件的情况下才能刷新页面
        if(SDL_PollEvent(&event) != 0) {
            continue;
        }
        AVFrame *frame = video_frame_queue_.front();
        // 帧的相对时间过去了多久【单位:1/刻度 秒】
        double pts = frame->pts * av_q2d(input_fmt_ctx->streams[video_idx]->time_base);
        printf("video.pts: %f ; now.pts: %f\n", pts, sync_.getPts() / 1000.0);
        pts = pts * 1000;//转换毫秒
        // 当前帧如果实际上应该播放的时间超过了当前时间,则代表当前帧应该在未来播放,现在不能播放
        // 故在这里等待时间
        if(pts > sync_.getPts())
        {
            SDL_Delay(pts - sync_.getPts());//等待时间直到时间到了pts,那么才播放
            // 这里可以使用continue,或者也可以直接向下运行,加个continue方便盘逻辑
            continue;
        }
        // 将当前帧移除待播放队列,因为这一帧马上就播放了
        video_frame_queue_.pop();
        // 如果当前帧已经延迟了500ms了,那么丢弃该帧,直接播放下一帧
        if(pts < sync_.getPts() - 500)
        {
            continue;
        }
        // SDL: output
        // 清空之前的页面
        SDL_RenderClear(renderer_);
        // 设置rect所占区域
        rect_.x = 0;
        rect_.y = 0;
        // 在这里设置rect区域的大小,如果这里和窗口总大小不一样,那么其他地方是黑屏显示
        // 故这里也体现了一个win可以设置多个rect,每个rect可以占据不同的位置
        rect_.w = video_width_;
        rect_.h = video_height_;
        // 通过YUV格式渲染图片
        SDL_UpdateYUVTexture(texture_, &rect_,
                frame->data[0], frame->linesize[0],
                frame->data[1], frame->linesize[1],
                frame->data[2], frame->linesize[2]);
        // 页面内容设置
        SDL_RenderCopy(renderer_, texture_, NULL, &rect_);
        // 显示新的页面
        SDL_RenderPresent(renderer_);
        // 释放内存
        av_frame_free(&frame);
    }
    return 0;
}

// 音频播放线程
int play_audio_thread(void *opaque) {
    int ret = 0;
    // 初始化音频
    if(SDL_Init(SDL_INIT_AUDIO)) {
        return -1;
    }
    // 音频播放上下文,音频播放只能通过这个结构体进行操作
    // 创建 SwrContext 只能使用 swr_alloc() 函数
    SwrContext *swrContext = swr_alloc();
    if(!swrContext)
    {
        cout << "初始化swrContext对象失败" << endl;
        return -1;
    }
    // 设置具体参数来创建 SwrContext对象
    /* channel布局:如立体声、5.1声道、单声道等
     * 采样格式:不同音频格式的采样格式不同,如AAC的采样格式是 AV_SAMPLE_FMT_FLTP,
     *      而MP3的采样格式是 AV_SAMPLE_FMT_S16P
     * 采样率:一秒钟采集多少次样本
     * */
    swrContext = swr_alloc_set_opts(NULL,   //是否需要继承一个存在的SwrContext的内容
                                    AV_CH_LAYOUT_STEREO, //输出的channel布局
                                    AV_SAMPLE_FMT_S16, //输出的采样格式
                                    44100, //输出的采样率
                                    av_get_default_channel_layout(audio_codec_ctx->channels), //输入的channel布局
                                    audio_codec_ctx->sample_fmt, //输入的采用格式
                                    audio_codec_ctx->sample_rate, //输入的采用率
                                    0,
                                    NULL);
    /* 为什么要重采样?
     * 是因为输入的音频可能是mp4格式的,但是我们的电脑只能播放avi格式的音频,
     *  所以需要转换数据,转换为确保我们的电脑一定能播放的格式。
     * */
    // 初始化重采样上下文
    ret = swr_init(swrContext);
    // 初始化重采样失败,那么音频无法播放
    if(ret < 0)
    {
        cout << "初始化重采样上下文失败" << endl;
        return -1;
    }
    // SDL_AudioSpc 是音频播放参数的结构体
    // 期望能够实现的音频参数
    SDL_AudioSpec wanted_spec;
    wanted_spec.freq = 44100; //期望的采样率
    wanted_spec.format = AUDIO_S16SYS; //期望的采样格式
    wanted_spec.channels = 2; //期望的通道格式
    wanted_spec.silence = 0; //期望中静音大小的值
    wanted_spec.samples = 1024; //期望中一帧的数据大小,即样本数
    wanted_spec.callback = fill_audio_pcm;//播放音频时会开启一个线程,反复调用这个回调函数,用来给音频填充PCM
    wanted_spec.userdata = audio_codec_ctx; //回调函数中第一个参数的对象
    // 按照指定参数打开真实的物理设备
    ret = SDL_OpenAudio(&wanted_spec, NULL);
    if(ret < 0)
    {
        cout << "打开音频设备失败" << endl;
        return -1;
    }
    // 开始播放音频
    SDL_PauseAudio(0);
    // 分配输出音频数据
    Uint8 *out_buffer = nullptr;
    // 线程运行中
    while(thread_exit == 0)
    {
        // 如果队列为空,则等待帧
        if(audio_frame_queue_.empty())
        {
            SDL_Delay(1);
            continue;
        }
        AVFrame *frame = audio_frame_queue_.front();
        // 帧的相对时间过去了多久【单位:1/刻度 秒】
        double pts = frame->pts * av_q2d(input_fmt_ctx->streams[audio_idx]->time_base);
        // 更新时钟【音频为基准】
        sync_.setClock(pts);
        // 将当前帧移除待播放队列,因为这一帧马上就播放了
        audio_frame_queue_.pop();
        // 获取输入的样本数
        int in_samples = frame->nb_samples;
        // 目标样本数【想要输出的样本数】
        int dst_samples = av_rescale_rnd(in_samples, wanted_spec.freq, frame->sample_rate, AV_ROUND_UP);
        // 计算需要输出的样本数内存空间大小
        int out_buffer_size = av_samples_get_buffer_size(NULL, wanted_spec.channels,
                                                         dst_samples, AV_SAMPLE_FMT_S16, 0);
        // 如果输出的音频数据未开辟过空间,那么开辟空间
        if(!out_buffer)
        {
            // 输出数据的空间大小即为计算出来需要输出的样本数大小
            out_buffer = (Uint8 *)av_malloc(out_buffer_size);
        }
        // 返回每个通道需要输出的样本数,错误时返回负值
        int sample_count = swr_convert(swrContext, &out_buffer, dst_samples,
                                      (const Uint8 **)frame->data, in_samples);// frame->data 即为采样到的数据
        // 释放内存
        av_frame_free(&frame);
        // 获取不到样本数了,那么进行下一个包数据的读取
        if(sample_count < 0)
        {
            break;
        }
        // 计算这一帧的字节数大小/长度
        int out_size = sample_count * wanted_spec.channels *av_get_bytes_per_sample(AV_SAMPLE_FMT_S16);
        // 如果回调函数中的字节数还未处理完,那么不能进行下一个音频帧的处理
        while(audio_len_g > 0)
            SDL_Delay(1);
        // 回调函数中的字节已经处理完了,那么可以填充下一个音频帧需要的数据了
        // 这一帧的字节长度
        audio_len_g = out_size;
        // 填充pcm数据
        audio_pcm_g = (Uint8 *)out_buffer;
    }
    return 0;
}

// 将帧写入队列
void push_frame(queue<AVFrame*> &queue_, AVFrame *frame_) {
    AVFrame *frame = av_frame_alloc();
    av_frame_move_ref(frame, frame_);
    queue_.push(frame);
}

#undef main
int main(int argc, char *argv[])
{
    SetConsoleOutputCP(CP_UTF8);
    if(argc < 2)
    {
        cout << "请输入视频地址" << endl;
        return -1;
    }
    // 获取视频地址
    char *url = argv[1];
    // 方法调用结果
    int ret = 0;
    // FFmpeg
    // AVFormatContext 是音视频开发使用到最多的结构体,无论什么函数基本上都会用到它
    // AVFormatContext 只能通过 avformat_alloc_context() 创建空的对象
    input_fmt_ctx = avformat_alloc_context();
    // 加载视频内容到音视频格式上下文中
    ret = avformat_open_input(&input_fmt_ctx, url, NULL, NULL);
    // 输出日志
    showError(ret);
    // 查看流信息,可以不写,只是单纯拿返回值来做校验的
    ret = avformat_find_stream_info(input_fmt_ctx, NULL);
    // 输出日志
    showError(ret);
    // 输出视频信息,可以不写
    av_dump_format(input_fmt_ctx, 0, url, 0);
    // 查找指定流的idx,如果使用不到,可以不写; AVMEDIA_TYPE_VIDEO 代表视频流,AVMEDIA_TYPE_AUDIO代表音频流
    video_idx = av_find_best_stream(input_fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    audio_idx = av_find_best_stream(input_fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
    printf("video_idx: %d , audio_idx: %d\n", video_idx, audio_idx);
    // AVCodecContext 是解码器上下文,需要对帧处理基本上都会用到它
    // AVCodecContext 只能通过 avcodec_alloc_context3(NULL) 创建空的对象
    // 视频解码器上下文
    AVCodecContext *video_codec_ctx = avcodec_alloc_context3(NULL);
    // 将音视频格式上下文中的参数加载到解码器上下文对象中
    ret = avcodec_parameters_to_context(video_codec_ctx, input_fmt_ctx->streams[video_idx]->codecpar);
    // 输出日志
    showError(ret);
    // 指定物理解码器;这里参数传的是codec_ctx->codec_id,实际上物理解码器有很多中,这里可以传不同的内容
    AVCodec *video_codec = avcodec_find_decoder(video_codec_ctx->codec_id);
    // 将物理解码器加载到解码器上下文中
    ret = avcodec_open2(video_codec_ctx, video_codec, NULL);
    // 音频解码器上下文
    audio_codec_ctx = avcodec_alloc_context3(NULL);
    // 将音视频格式上下文中的参数加载到解码器上下文对象中
    ret = avcodec_parameters_to_context(audio_codec_ctx, input_fmt_ctx->streams[audio_idx]->codecpar);
    // 输出日志
    showError(ret);
    // 指定物理解码器;这里参数传的是codec_ctx->codec_id,实际上物理解码器有很多中,这里可以传不同的内容
    AVCodec *audio_codec = avcodec_find_decoder(audio_codec_ctx->codec_id);
    // 将物理解码器加载到解码器上下文中
    ret = avcodec_open2(audio_codec_ctx, audio_codec, NULL);
    // 输出日志
    showError(ret);
    // 包,用来获取音视频格式上下文中的数据
    // AVPacket 只能通过 av_packet_alloc() 创建对象
    AVPacket pkt;
    // 开启播放线程
    SDL_CreateThread(play_video_thread, NULL, NULL);
    SDL_CreateThread(play_audio_thread, NULL, NULL);
    // 设置时钟
    sync_.init();
    // output and readFrame
    while(1)
    {
        // printf("video_queue.size: %d ; audio_queue.size: %d\n", video_frame_queue_.size(), audio_frame_queue_.size());
        // 防止读取内存过大
        if(video_frame_queue_.size() >= 100 || audio_frame_queue_.size() >= 100)
        {
            SDL_Delay(1);
            continue;
        }
        // FFmpeg: readFrame
        // 获取该音视频格式上下文中的第一个包,并将从音视频格式上下文中移除
        // 则代表了每次调用都会获取到新的包,之前的包不会再在该音视频格式上下文中找到了
        ret = av_read_frame(input_fmt_ctx, &pkt);
        // 如果包数据读取完毕,则代表视频播放结束了
        if(ret < 0)
        {
            cout << "play video finish" << endl;
            break;
        }
        // AVFrame 只能通过 av_frame_alloc() 创建对象
        AVFrame *frame = av_frame_alloc();
        // 音频帧
        if(pkt.stream_index == audio_idx)
        {
            // 将包加载到解码器上下文中进行解码
            ret = avcodec_send_packet(audio_codec_ctx, &pkt);
            // 对应音频的包数据来说,一次包读取,可以获取到多个frame
            while(1)
            {
                // 读取解码后的包中的帧
                ret = avcodec_receive_frame(audio_codec_ctx, frame);
                // 如果 AVERROR(EAGAIN) == ret,则代表这个包无法获取到帧,需要再次加载下一个包配合解析帧
                // 如果所有的帧都读取完成了,那么开始读取下一个包
                if(ret == AVERROR(EAGAIN))
                {
                    break;
                }
                // 将帧添加到队列中
                push_frame(audio_frame_queue_, frame);
            }
        }
        // 视频帧
        else if(pkt.stream_index == video_idx)
        {
            // 将包加载到解码器上下文中进行解码
            ret = avcodec_send_packet(video_codec_ctx, &pkt);
            // 读取解码后的包中的帧
            ret = avcodec_receive_frame(video_codec_ctx, frame);
            // 如果 AVERROR(EAGAIN) == ret,则代表这个包无法获取到帧,需要再次加载下一个包配合解析帧
            if(AVERROR(EAGAIN) == ret)
            {
                continue;
            }
            // 将帧添加到队列中
            push_frame(video_frame_queue_, frame);
        }
        // 释放内存
        av_packet_unref(&pkt);
    }
    // 加载帧完成了,现在需要等待所有帧播放完毕
    while(!video_frame_queue_.empty() || !audio_frame_queue_.empty())
    {
        printf("video_queue.size: %d ; audio_queue.size: %d --wait_over\n", video_frame_queue_.size(), audio_frame_queue_.size());
        SDL_Delay(10);
    }
    // 标记线程结束了
    thread_exit = 1;
    system("pause");
    return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Spring Boot框架下的酒店住宿登记系统

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常…

PIL处理器在环测试

目录 PIL处理器在环测试介绍 PIL测试过程 编译模块 测试结果 PIL处理器在环测试介绍 SIL测试是验证代码和模型的一致性&#xff0c;代码运行在Windows平台上&#xff0c;某种程度上说&#xff0c;这并不能保证代码到目标处理器上的运行结果也能够和模型保持一致。所以&…

ctfshow的sql注入解题思路171-211

ctfshow-SQL注入 web171&#xff1a;爆库名->爆表名->爆字段名->爆字段值 -1 union select 1,database() ,3 -- //返回数据库名 -1 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema库名 -- //获取数据库里的表名 -…

【华为\荣耀、中兴、华三路由器IPV6设置】

华为\荣耀、中兴、华三路由器ipv6设置 华为\荣耀设置-路由器拨号情况下中兴设置-路由器拨号情况下华三设置-光猫拨号情况下&#xff08;待续&#xff09; 华为\荣耀设置-路由器拨号情况下 如图设置就行 中兴设置-路由器拨号情况下 中兴路由器有两个设置地方也是如图设置 …

一站式AI自动化剪辑 内置多种功能 永久免费

AI影视解说自动化剪辑工具&#xff0c;功能非常强大&#xff0c;吊打所有视频解说&#xff0c;解放双手&#xff0c;从我开始 【资源名称】&#xff1a;纳拉托艾 【资源大小】&#xff1a;1.27 【资源版本】&#xff1a;0.1 【测试机型】&#xff1a;Win11. 【资源介绍】&a…

基于SSM+小程序的智慧旅游平台登录管理系统(旅游2)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1、项目介绍 旅游平台开发微信小程序功能有管理员和用户。 1、管理员功能有个人中心&#xff0c;用户管理&#xff0c;景点分类管理&#xff0c;旅游景点管理&#xff0c;景点购票管理&#xff0c;景…

Leetcode刷题笔记13

DP35 【模板】二维前缀和 【模板】二维前缀和_牛客题霸_牛客网 解法一&#xff1a;暴力解法 -> 模拟 直接算区间里面的和 每次询问都要遍历数组一遍 时间复杂度&#xff1a;O(n*m*q) 解法二&#xff1a;前缀和 1. 预处理出来一个前缀和矩阵 dp[i][j]表示&#xff1a;从[…

VisionPro Basic - 01- 有关应用和作业

前言&#xff1a; VP&#xff08;VisionPro&#xff09;的保存文件都是.vpp&#xff0c;所以&#xff0c;你在保存的时候&#xff0c;一定要注意区别。否则&#xff0c;过了几天&#xff0c;你都搞不清楚自己当年哪个的应用&#xff0c;哪个是作业... 环境&#xff1a; 例子1&…

高级网络互联技术:AS3001与AS3000的路由交换方案

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

数字化转型项目实施方案建议书|168页PPT

文 档是一份关于数字化转型项目的实施方案建议书&#xff0c;由某咨询公司为***集团制定。文档详细介绍了项目的实施范围、信息系统现状、建设目标、高阶方案建议以及项目组织和计划。 以下是对文档内容的解读&#xff1a; 项目实施范围&#xff1a;涵盖了数字化转型路线图中…

CSP-J2024 全网首发

T1:扑克牌 题目描述 Description 小 P 从同学小 Q 那儿借来一副 n 张牌的扑克牌。 本题中我们不考虑大小王&#xff0c;此时每张牌具有两个属性:花色和点数。花色共有 4种: 方片、草花、红桃和黑桃。点数共有 13 种&#xff0c;从小到大分别为A 2 3 4 5 6 7 8 9 T J Q K。注意…

【3DMAX科研绘图】3DMAX饼状图生成插件PieChart使用方法详解

3DMAX饼状图生成插件PieChart&#xff0c;一款用于制作3D饼状图的工具。可以设置任意数量的切片&#xff0c;以及随机或指定切片颜色。 饼状图&#xff08;Pie Chart&#xff09;是一种常用的数据可视化工具&#xff0c;它主要用于展示不同类别数据的比例关系。在饼状图中&…

ERPS环网配置

ERPS&#xff08;Ethernet Ring Protection Switching&#xff09;&#xff1a;以太网多环保护技术 ERPS节点信息 1、RPL owner 节点&#xff08;主节点&#xff09; 一个 ERPS 环只有一个 RPL owner 节点&#xff0c;由用户配置决定&#xff0c;通过阻塞 RPL 端口来防止 ERP…

.NET 一款内网渗透中替代PowerShell的工具

01阅读须知 此文所提供的信息只为网络安全人员对自己所负责的网站、服务器等&#xff08;包括但不限于&#xff09;进行检测或维护参考&#xff0c;未经授权请勿利用文章中的技术资料对任何计算机系统进行入侵操作。利用此文所提供的信息而造成的直接或间接后果和损失&#xf…

笔记整理—linux驱动开发部分(1)驱动梗概

驱动可以分为广义上的和狭义上的驱动。广义上的驱动是用于操作硬件的代码&#xff0c;而狭义上的驱动为基于内核系统之上让硬件去被操作的逻辑方法。 linux体系架构&#xff1a; 1.分层思想 &#xff1a;在OS中间还会有许多层。 : 2.驱动的上面是系统调用&#xff08;API&…

Springboot 整合 Java DL4J 实现智能客服

&#x1f9d1; 博主简介&#xff1a;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/literature?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;精通Java编程&#xff0c;…

大语言模型的Scaling Law【Power Low】

NLP-大语言模型学习系列目录 一、注意力机制基础——RNN,Seq2Seq等基础知识 二、注意力机制【Self-Attention,自注意力模型】 三、Transformer图文详解【Attention is all you need】 四、大语言模型的Scaling Law【Power Low】 文章目录 NLP-大语言模型学习系列目录一、什么是…

隧道煤矿甬道的可视化大屏,关键时刻起关键作用

隧道、煤矿甬道的可视化大屏在关键时刻确实能发挥关键作用。它可以实时显示内部的环境参数&#xff0c;如温度、湿度、瓦斯浓度等&#xff0c;帮助工作人员及时掌握潜在危险情况。 同时&#xff0c;大屏能展示人员分布和设备运行状态&#xff0c;便于高效调度和管理。 在紧急…

计算机网络:网络层 —— IPv4 地址与 MAC 地址 | ARP 协议

文章目录 IPv4地址与MAC地址的封装位置IPv4地址与MAC地址的关系地址解析协议ARP工作原理ARP高速缓存表 IPv4地址与MAC地址的封装位置 在数据传输过程中&#xff0c;每一层都会添加自己的头部信息&#xff0c;最终形成完整的数据包。具体来说&#xff1a; 应用层生成的应用程序…

技术成神之路:设计模式(二十一)外观模式

相关文章&#xff1a;技术成神之路&#xff1a;二十三种设计模式(导航页) 介绍 外观模式&#xff08;Facade Pattern&#xff09;是一种结构型设计模式&#xff0c;它为子系统中的一组接口提供一个统一的接口。外观模式定义了一个高层接口&#xff0c;使得子系统更容易使用。 …