linux alsa-lib snd_pcm_open函数源码分析(三)
欢迎直接到博客
linux alsa-lib snd_pcm_open函数源码分析(三)
系列文章其他部分:
linux alsa-lib snd_pcm_open函数源码分析(一)
linux alsa-lib snd_pcm_open函数源码分析(二)
linux alsa-lib snd_pcm_open函数源码分析(四)
linux alsa-lib snd_pcm_open函数源码分析(五)
linux alsa-lib snd_pcm_open函数源码分析(六)
解析配置的最后一个超复杂子函数
1. snd_config_hooks
核心函数之一,配置hooks功能,其中使用hooks功能加载并解析了配置文件中引用的其他配置文件。
所谓的其他配置文件通常为"/etc/asound.conf"及"~/.asoundrc",也就是用户经常自定义修改的配置文件。
static int snd_config_hooks_call(snd_config_t *root, snd_config_t *config, snd_config_t *private_data)
{void *h = NULL;snd_config_t *c, *func_conf = NULL;char *buf = NULL;const char *lib = NULL, *func_name = NULL;const char *str;int (*func)(snd_config_t *root, snd_config_t *config, snd_config_t **dst, snd_config_t *private_data) = NULL;int err;//在配置树中通过id寻找某个节点//实际通常这里func后会跟一个load,比如alsa.conf中的func load//目的是加载其他配置//见下文详细分析err = snd_config_search(config, "func", &c);if (err < 0) {SNDERR("Field func is missing");return err;}//功能及实现都很简单//返回string类型节点的string值//本质上就是直接把节点的值返回一下//见下文分析err = snd_config_get_string(c, &str);if (err < 0) {SNDERR("Invalid type for field func");return err;}assert(str);//配置树中查找"hook_func"节点//详细分析见下文err = snd_config_search_definition(root, "hook_func", str, &func_conf);if (err >= 0) {snd_config_iterator_t i, next;if (snd_config_get_type(func_conf) != SND_CONFIG_TYPE_COMPOUND) {SNDERR("Invalid type for func %s definition", str);err = -EINVAL;goto _err;}snd_config_for_each(i, next, func_conf) {snd_config_t *n = snd_config_iterator_entry(i);const char *id = n->id;if (strcmp(id, "comment") == 0)continue;if (strcmp(id, "lib") == 0) {err = snd_config_get_string(n, &lib);if (err < 0) {SNDERR("Invalid type for %s", id);goto _err;}continue;}if (strcmp(id, "func") == 0) {err = snd_config_get_string(n, &func_name);if (err < 0) {SNDERR("Invalid type for %s", id);goto _err;}continue;}SNDERR("Unknown field %s", id);}}if (!func_name) {int len = 16 + strlen(str) + 1;buf = malloc(len);if (! buf) {err = -ENOMEM;goto _err;}//这里即可拼接出函数的名字snprintf(buf, len, "snd_config_hook_%s", str);buf[len-1] = '\0';func_name = buf;}//对dlopen的包装,打开库,如果指定的库不存在,则打开默认的库h = snd_dlopen(lib, RTLD_NOW);//对dlsym的包装,从动态库中解析函数符号,即通过字符串查找到函数func = h ? snd_dlsym(h, func_name, SND_DLSYM_VERSION(SND_CONFIG_DLSYM_VERSION_HOOK)) : NULL;err = 0;if (!h) {SNDERR("Cannot open shared library %s", lib);err = -ENOENT;} else if (!func) {SNDERR("symbol %s is not defined inside %s", func_name, lib);snd_dlclose(h);err = -ENXIO;}_err:if (func_conf)snd_config_delete(func_conf);if (err >= 0) {snd_config_t *nroot;//这里执行了根据字符串查找出来的函数err = func(root, config, &nroot, private_data);if (err < 0)SNDERR("function %s returned error: %s", func_name, snd_strerror(err));snd_dlclose(h);if (err >= 0 && nroot)err = snd_config_substitute(root, nroot);}free(buf);if (err < 0)return err;return 0;
}
1.1 snd_config_search
在配置树中通过id查找一个子节点。id可以是一个或多个,中间以圆点(.)分隔。
如果是多个id的情况,则每个id需要依次指定前一级的复合节点。
比如在下面的配置树中,假设config是复合节点的句柄,
每个节点后的注释为找到这个节点需要用到的key
config {a 42 # "a"b { # "b"c "cee" # "b.c"d { # "b.d"e 2.71828 # "b.d.e"}}}
函数原型如下:
int snd_config_search(snd_config_t *config, const char *key, snd_config_t **result)
{SND_CONFIG_SEARCH(config, key, result, );
}
实际主要是使用了SND_CONFIG_SEARCH
宏,这类宏有多个,后面会对所有类型的宏都做详细的分析。
1.1.1 SND_CONFIG_SEARCH
此宏是相对来说最简单的宏,实现的目的比较单一,即通过Key查找节点。
其中主要调用了_snd_config_search函数,此函数在上一篇中已经分析过。
#define SND_CONFIG_SEARCH(config, key, result, extra_code) \
{ \snd_config_t *n; \int err; \const char *p; \assert(config && key); \while (1) { \//如果不是复合节点直接报错。//这是因为传入的config本身即为父节点,如果不是复合节点则本身就不应该在一个单身节点下面查找子节点if (config->type != SND_CONFIG_TYPE_COMPOUND) \return -ENOENT; \//这里执行extra_code,这也是为什么宏中可以添加代码的原因。{ extra_code ; } \//此函数返回key中第一次出现字符'.'的位置,如果每找到,则返回nullp = strchr(key, '.'); \if (p) { \//如果找到,则返回(.)的位置,注意此处的p-key//由于返回的是.的位置,即.的地址,key为字符串最开头的地址//这样p-key即为字符串开始到.的字符的个数//于是搜索key字符串的前p-key个字符,则变成了搜索key字符串第一个点前的字符串//比如搜索a.b.c的话,此时相当于搜索aerr = _snd_config_search(config, key, p - key, &n); \if (err < 0) \return err; \//注意这里,如果搜索到了返回n,则把n赋值为config,下个循环则从n开始搜索config = n; \//p+1的目的是跳过(.),这里的1其实就是这个(.)//这样一来,在下个循环的时候,相当于从刚刚搜索到的节点开始,搜索(.)后面的内容key = p + 1; \} else \//如果没有(.)则传下来的key就是要搜索的内容return _snd_config_search(config, key, -1, result); \} \
}
1.2 snd_config_get_string
非常简单,返回string类型节点的string值。
int snd_config_get_string(const snd_config_t *config, const char **ptr)
{assert(config && ptr);if (config->type != SND_CONFIG_TYPE_STRING)return -EINVAL;*ptr = config->u.string;return 0;
}
1.3 snd_config_search_definition
在配置树中查找节点,函数允许传入的参数为别名(alias),同时如果传入的参数为别名,函数会还把别名展开。
如果传入的名字中包含冒号(😃,则冒号(:)后则为snd_config_expand
展开所用的参数。
int snd_config_search_definition(snd_config_t *config,const char *base, const char *name,snd_config_t **result)
{snd_config_t *conf;char *key;//查找":"//如果返回参数作为char*,则实际是找到的:的地址const char *args = strchr(name, ':');int err;if (args) {//如果找到了:,则自增1,目的是跳过:这个字符args++;//args - name则等于:前的字符长度+1,多出的1个字符刚好作为\0key = alloca(args - name);//注意这里需要把多出来的\0字符的位置减掉//实际刚好为:前的内容memcpy(key, name, args - name - 1);//最后的位置为\0key[args - name - 1] = '\0';} else {//找不到则直接就是keykey = (char *) name;}/** if key contains dot (.), the implicit base is ignored* and the key starts from root given by the 'config' parameter*/snd_config_lock();//如果key中有(.),则传入的base会被忽略//否则如果没有找到(.),说明没有指明base,则采用传入的base//函数查找子结点,如果传入的是别名(alias)则可以展开别名//详细见下文分析err = snd_config_search_alias_hooks(config, strchr(key, '.') ? NULL : base, key, &conf);if (err < 0) {snd_config_unlock();return err;}//展开节点,并执行对应的函数//见下文分析err = snd_config_expand(conf, config, args, NULL, result);snd_config_unlock();return err;
}
1.3.1 snd_config_search_alias_hooks
在配置树下使用别名及hooks查找节点。主要用来实现snd_config_search_definition
的功能。
对别两个函数,snd_config_search_definition
的第三个参数name可以包含’:‘,
如果有’:‘则’:‘后的内容为snd_config_expand
的参数,而snd_config_search_alias_hooks
的第三个参数更纯粹,
就是key,无法包含’:'用于snd_config_expand
。
int snd_config_search_alias_hooks(snd_config_t *config,const char *base, const char *key,snd_config_t **result)
{SND_CONFIG_SEARCH_ALIAS(config, base, key, result,snd_config_searcha_hooks,snd_config_searchva_hooks);
}
这里出现了SND_CONFIG_SEARCH_ALIAS
宏,与此类似的宏由多个,这些宏分别实现类似的功能,但是彼此之间有差异。
这些宏分析起来及其复杂,因为大部分都涉及到递归处理,以及相互嵌套。
这些函数中最基础的有两个,在此做介绍,其他类似的函数的函数只做功能说明.
1.3.2 snd_config_searcha
通过key在配置树中查找节点,展开别名。注意与1.1 snd_config_search的区别。
config {a {b bb}}root {bb {c cc}cc cccccc {d {x "icks"}}}
在上面的配置树中,使用snd_config_searcha(root, config, "a.b.c.d", &result);
则最终返回d节点。
int snd_config_searcha(snd_config_t *root, snd_config_t *config, const char *key, snd_config_t **result)
{SND_CONFIG_SEARCHA(root, config, key, result, snd_config_searcha, );
}
1.3.2.1 SND_CONFIG_SEARCHA
此宏主要用来实现snd_config_searcha
,使用key查找一个配置节点,同时在root下查找是否有别名并展开。
注意传入的fcn为调用者本身,意味着此处会有递归处理。
#define SND_CONFIG_SEARCHA(root, config, key, result, fcn, extra_code) \
{ \snd_config_t *n; \int err; \const char *p; \assert(config && key); \while (1) { \//如果不是复合类型,通常意味着搜索结束或者错误if (config->type != SND_CONFIG_TYPE_COMPOUND) { \//获取string类型的字符串值if (snd_config_get_string(config, &p) < 0) \return -ENOENT; \//递归处理err = fcn(root, root, p, &config); \if (err < 0) \return err; \} \{ extra_code ; } \//此处是查看key中是否有(.)p = strchr(key, '.'); \if (p) { \err = _snd_config_search(config, key, p - key, &n); \if (err < 0) \return err; \//把找到的n赋值给config,相当于从root逐步往下查找config = n; \key = p + 1; \} else \return _snd_config_search(config, key, -1, result); \} \
}
此类函数的功能,所使用的宏,以及主要作用总结在下表:
函数名 | 功能说明 | 使用到的宏 |
---|---|---|
snd_config_search | 在配置树中根据key查找节点 | SND_CONFIG_SEARCH |
snd_config_searcha | 在配置树中根据key查找节点,展开别名。别名从root下查找 | SND_CONFIG_SEARCHA |
snd_config_searchv | 在配置树中根据key查找节点;key可以是一系列的多个key | SND_CONFIG_SEARCHV |
snd_config_searchva | 在配置树中根据key查找节点,展开别名;key可以是连续多个key | SND_CONFIG_SEARCHVA |
snd_config_search_alias | 在配置树中根据key查找节点,展开别名.与snd_config_searcha类似,但是只能在config下查找。如果config下找不到id,则函数会尝试寻找base.id | SND_CONFIG_SEARCH_ALIAS |
snd_config_search_hooks | 在配置树中根据key查找节点,并且展开hooks。与snd_config_search类似,但是搜索的任何包含hooks的节点都会被各自的hooks函数修改 | SND_CONFIG_SEARCH |
snd_config_searcha_hooks | 在配置树中根据key查找节点,并且展开alias与hooks | SND_CONFIG_SEARCHA |
snd_config_searchva_hooks | 在配置树中根据key查找节点,并且展开alias与hooks。与snd_config_searcha_hooks类似但是key可以是一系列的key | SND_CONFIG_SEARCHVA |
snd_config_search_alias_hooks | 在配置树中根据key查找节点,并且展开alias与hooks。与snd_config_search_alias相似,并且展开hooks与snd_config_search_hooks相似 | SND_CONFIG_SEARCH_ALIAS |
1.3.2 snd_config_expand
使用参数及函数展开一个配置节点。如果传入的这个节点中有参数(通过一个id为@args的子节点定义),
则这个函数会用各自的参数值,或默认的参数值或者空来取代任何以$开头的string节点。
而且任何函数都会被评估(参考snd_config_evaluate
),结果的副本将会在result中返回。
这里评估(evaluated)的意思比较模糊,从代码分析上看应该是所有的函数都被执行了。
int snd_config_expand(snd_config_t *config, snd_config_t *root, const char *args,snd_config_t *private_data, snd_config_t **result)
{int err;snd_config_t *defs, *subs = NULL, *res;//寻找参数//前面已分析过err = snd_config_search(config, "@args", &defs);if (err < 0) {if (args != NULL) {SNDERR("Unknown parameters %s", args);return -EINVAL;}//创建config的副本到res,注意是深层拷贝//也就是说如果config是复合节点//它的子节点也会被拷贝//详细见下文分析err = snd_config_copy(&res, config);if (err < 0)return err;} else {//如果找到参数,则直接创建一个top节点//前文已分析err = snd_config_top(&subs);if (err < 0)return err;//把defs里面的"default“节点添加到空的subs里面//详细见下文分析err = load_defaults(subs, defs);if (err < 0) {SNDERR("Load defaults error: %s", snd_strerror(err));goto _end;}//解析参数args//太太太复杂err = parse_args(subs, args, defs);if (err < 0) {SNDERR("Parse arguments error: %s", snd_strerror(err));goto _end;}//在运行时评估一个配置节点err = snd_config_evaluate(subs, root, private_data, NULL);if (err < 0) {SNDERR("Args evaluate error: %s", snd_strerror(err));goto _end;} }err = snd_config_walk(config, root, &res, _snd_config_expand, subs);if (err < 0) {SNDERR("Expand error (walk): %s", snd_strerror(err));goto _end;}}err = snd_config_evaluate(res, root, private_data, NULL);if (err < 0) {SNDERR("Evaluate error: %s", snd_strerror(err));snd_config_delete(res);goto _end;}*result = res;err = 1;_end:if (subs)snd_config_delete(subs);return err;
}
1.3.2.1 snd_config_evaluate
在运行时评估一个函数,此函数会评估配置树中的任何一个函数(@func),
并用各自函数的结果替换这些节点。这里的评估应该时计算的意思。
int snd_config_evaluate(snd_config_t *config, snd_config_t *root,snd_config_t *private_data, snd_config_t **result)
{/* FIXME: Only in place evaluation is currently implemented */assert(result == NULL);return snd_config_walk(config, root, result, _snd_config_evaluate, private_data);
}
1.3.2.2 snd_config_walk
这里面传入的回调函数为_snd_config_evaluate
,函数本身又会有递归,
大概目的就是一步一步查找func,找到并执行,并且由于创建了新的配置树,
会把执行函数后的节点信息替换掉原来的节点。
static int snd_config_walk(snd_config_t *src,snd_config_t *root,snd_config_t **dst,snd_config_walk_callback_t callback,snd_config_t *private_data)
{int err;snd_config_iterator_t i, next;switch (snd_config_get_type(src)) {case SND_CONFIG_TYPE_COMPOUND:err = callback(src, root, dst, SND_CONFIG_WALK_PASS_PRE, private_data);if (err <= 0)return err;snd_config_for_each(i, next, src) {snd_config_t *s = snd_config_iterator_entry(i);snd_config_t *d = NULL;err = snd_config_walk(s, root, (dst && *dst) ? &d : NULL,callback, private_data);if (err < 0)goto _error;if (err && d) {err = snd_config_add(*dst, d);if (err < 0)goto _error;}}err = callback(src, root, dst, SND_CONFIG_WALK_PASS_POST, private_data);if (err <= 0) {_error:if (dst && *dst)snd_config_delete(*dst);}break;default:err = callback(src, root, dst, SND_CONFIG_WALK_PASS_LEAF, private_data);break;}return err;
}
1.3.2.2 _snd_config_evaluate
具体执行函数,太复杂,即有循环,又有递归,注意里面的snd_dlopen
及snd_dlsym
,会从动态库中根据func符号找到函数,并且执行。
所以func其实是执行了,evaluate可以理解为计算的意思。
static int _snd_config_evaluate(snd_config_t *src,snd_config_t *root,snd_config_t **dst ATTRIBUTE_UNUSED,snd_config_walk_pass_t pass,snd_config_t *private_data)
{int err;if (pass == SND_CONFIG_WALK_PASS_PRE) {char *buf = NULL;const char *lib = NULL, *func_name = NULL;const char *str;int (*func)(snd_config_t **dst, snd_config_t *root,snd_config_t *src, snd_config_t *private_data) = NULL;void *h = NULL;snd_config_t *c, *func_conf = NULL;err = snd_config_search(src, "@func", &c);if (err < 0)return 1;err = snd_config_get_string(c, &str);if (err < 0) {SNDERR("Invalid type for @func");return err;}assert(str);err = snd_config_search_definition(root, "func", str, &func_conf);if (err >= 0) {snd_config_iterator_t i, next;if (snd_config_get_type(func_conf) != SND_CONFIG_TYPE_COMPOUND) {SNDERR("Invalid type for func %s definition", str);err = -EINVAL;goto _err;}snd_config_for_each(i, next, func_conf) {snd_config_t *n = snd_config_iterator_entry(i);const char *id = n->id;if (strcmp(id, "comment") == 0)continue;if (strcmp(id, "lib") == 0) {err = snd_config_get_string(n, &lib);if (err < 0) {SNDERR("Invalid type for %s", id);goto _err;}continue;}if (strcmp(id, "func") == 0) {err = snd_config_get_string(n, &func_name);if (err < 0) {SNDERR("Invalid type for %s", id);goto _err;}continue;}SNDERR("Unknown field %s", id);}}if (!func_name) {int len = 9 + strlen(str) + 1;buf = malloc(len);if (! buf) {err = -ENOMEM;goto _err;}snprintf(buf, len, "snd_func_%s", str);buf[len-1] = '\0';func_name = buf;}h = snd_dlopen(lib, RTLD_NOW);if (h)func = snd_dlsym(h, func_name, SND_DLSYM_VERSION(SND_CONFIG_DLSYM_VERSION_EVALUATE));err = 0;if (!h) {SNDERR("Cannot open shared library %s", lib);err = -ENOENT;goto _errbuf;} else if (!func) {SNDERR("symbol %s is not defined inside %s", func_name, lib);snd_dlclose(h);err = -ENXIO;goto _errbuf;}_err:if (func_conf)snd_config_delete(func_conf);if (err >= 0) {snd_config_t *eval;err = func(&eval, root, src, private_data);if (err < 0)SNDERR("function %s returned error: %s", func_name, snd_strerror(err));snd_dlclose(h);if (err >= 0 && eval) {/* substitute merges compound members *//* we don't want merging at all */err = snd_config_delete_compound_members(src);if (err >= 0)//替换节点err = snd_config_substitute(src, eval);}}_errbuf:free(buf);if (err < 0)return err;return 0;}return 1;
}
至此,snd_config_expand
的功能为展开节点,从节点中搜索函数,逐个执行,并最终用执行结果替换掉原来的节点。
snd_config_search_definition
则查找某个定义,查找到后使用snd_config_expand
去展开执行。
根据读取alsa的默认配置文件alsa.conf,通常会构建一个func load函数,再用构造出的函数去load配置文件。
里面又是一大堆递归。总之alsa配置的目的就是通过在配置文件中修改配置,即可控制运行时的函数执行。
为了实现这个目标,alsa丧心病狂的实现了众多嵌套,递归,为了实现代码复用,采用了众多的宏,整体让alsa变得及其复杂。
比如alsa.conf中的配置:
@hooks [{func loadfiles ["/etc/alsa/conf.d""/etc/asound.conf""~/.asoundrc"]errors false}
]
通过这个hooks,实际上构造了snd_config_hooks_load
函数,运行时会解析符号并从动态库中找到此函数,
用来加载接下来的三个配额文件,"/etc/alsa/conf.d"
及"/etc/asound.conf"
,"~/.asoundrc"
。
至此snd_config_update_r
的大致功能已经比较清晰了,从配置文件中读取配置文件,解析为配置树,
执行所有的hooks,其中可能又会包含读取配置文件,解析为配置树。执行所有的hooks函数,重新更新配置树。
返回最后的配置树。
相关文章:

linux alsa-lib snd_pcm_open函数源码分析(三)
欢迎直接到博客 linux alsa-lib snd_pcm_open函数源码分析(三) 系列文章其他部分: linux alsa-lib snd_pcm_open函数源码分析(一) linux alsa-lib snd_pcm_open函数源码分析(二) linux alsa-lib snd_pcm_open函数源码分析(四…...

基于ssm的个人健康管理系统
项目描述 临近学期结束,还是毕业设计,你还在做java程序网络编程,期末作业,老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据疫情当下,你想解决的问…...

Debian下载ISO镜像的方法
步骤 1:访问Debian官方网站 打开你的网络浏览器,在地址栏中输入 https://www.debian.org/ 并回车,这将带你到Debian的官方网站。 步骤 2:导航到下载页面 在Debian官方网站的首页上,找到并点击“Download Debian”或类…...

大厂面试真题-简单说说线程池接到新任务之后的操作流程
线程池在接到新任务后的操作流程通常遵循以下步骤,这些步骤确保了任务的高效管理和执行。 一、判断当前线程状态 线程池首先会判断当前是否存在空闲线程,即没有正在执行任务且未被标记为死亡的线程。 有空闲线程:如果存在空闲线程…...

「Mac畅玩鸿蒙与硬件23」鸿蒙UI组件篇13 - 自定义组件的创建与使用
自定义组件可以帮助开发者实现复用性强、逻辑清晰的界面模块。通过自定义组件,鸿蒙应用能够提高代码的可维护性,并简化复杂布局的构建。本篇将介绍如何创建自定义组件,如何向组件传递数据,以及如何在不同页面间复用这些组件。 关键…...

C++关键字:mutable
文章目录 一、mutable1.mutable修饰非静态的成员变量2.mutable用于lambda表达式3.mutable不能修饰的变量:静态变量、const变量 一、mutable 1.mutable修饰非静态的成员变量 1.mutable仅能修饰类中的非静态的成员变量。不能修饰全局变量、局部变量、静态变量、常量…...

Agent 智能体开发框架选型指南
编者按: 本文通过作者的实践对比发现,框架的选择应基于项目具体需求和团队特点,而不是简单追求某个特定框架。不同框架各有优势: 无框架方案实施最为简单直接,代码结构清晰,适合理解智能体原理,…...

基于Zynq FPGA对雷龙SD NAND的测试
一、SD NAND 特征 1.1 SD 卡简介 雷龙的 SD NAND 有很多型号,在测试中使用的是 CSNP4GCR01-AMW 与 CSNP32GCR01-AOW。芯片是基于 NAND FLASH 和 SD 控制器实现的 SD 卡。具有强大的坏块管理和纠错功能,并且在意外掉电的情况下同样能保证数据的安全。 …...

AOSP沙盒android 11
这里介绍一下aosp装系统 什么是aosp AOSP(Android Open Source Project)是Android操作系统的开源版本。 它由Google主导,提供了Android的源代码和相关工具,供开发者使用和修改。 AOSP包含了Android的核心组件和API,使…...

【JWT】Asp.Net Core中JWT刷新Token解决方案
Asp.Net Core中JWT刷新Token解决方案 前言方案一:当我们操作某个需要token作为请求头的接口时,返回的数据错误error.response.status === 401,说明我们的token已经过期了。方案二:实现用户无感知的刷新token值,我们希望当响应返回的数据是401身份过期时,响应阻拦器自动帮我…...

AJ-Report:一款开源且非常强大的数据可视化大屏和报表工具
嗨,大家好,我是小华同学,关注我们获得“最新、最全、最优质”开源项目和工作学习方法 AJ-Report是一个基于Java的开源报表工具,它集成了ECharts、Ant Design Vue等前端技术,致力于为企业提供一站式的数据可视化解决方案…...

stm32不小心把SWD和JTAG都给关了,程序下载不进去,怎么办?
因为想用STM32F103的PA15引脚,调试程序的时候不小心把SWD和JTAD接口都给关了,先看下罪魁祸首 GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);//关掉JTAG,不关SWGPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable, ENABLE);//关掉SW&am…...

【UE5】在材质中实现球形法线技术,常用于改善植物等表面的渲染效果
在材质中实现球形法线,这种技术常用于植被渲染等场景。通过应用球形法线可以显著提升植物再低几何体情况下的光照效果。 三二一上截图! 当然也可以用于任何你希望模型圆润的地方,下图中做了一个Cube倒角...

【MATLAB源码-第210期】基于matlab的OFDM电力线系统仿真,不同梳状导频间隔对比。三种信道估计,三种插值误码率对比
操作环境: MATLAB 2022a 1、算法描述 OFDM电力线通信系统(PLC)是一种通过电力线传输数据的通信技术,利用了OFDM(Orthogonal Frequency Division Multiplexing,正交频分复用)技术的优势来提高…...

基于SpringBoot的城镇保障性住房管理策略
3系统分析 3.1可行性分析 通过对本城镇保障性住房管理系统实行的目的初步调查和分析,提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本城镇保障性住房管理系统采用SSM框架,JA…...

支持高性能结构化数据提取的 Embedding 模型——NuExtract-v1.5
NuExtract 是一个用户友好型模型,设计用于从长文档中提取信息。它可以处理长达 20,000 个标记的输入,是合同、报告和其他商业通信的理想选择。NuExtract 的与众不同之处在于它能够处理和理解文档的整个上下文。这意味着它可以捕捉到可能分散在长文本不同…...

【C#】设计一个选购计算机配置的应用程序
文章目录 1. 创建新的 Windows Forms 应用程序项目2. 设计界面2.1 添加控件和设置属性 3. 编写事件处理代码4. 运行和测试应用程序 1. 创建新的 Windows Forms 应用程序项目 打开 Visual Studio。选择“创建新项目”。在项目类型中选择“Windows Forms 应用程序”(…...

solo博客源码使用idea编译运行
solo博客源码使用idea编译运行 solo博客开源地址本地运行IDEA 编译执行默认直接编译jar 包编译 solo博客开源地址 项目地址:GitHub - 88250/solo: 🎸 B3log 分布式社区的 Java 博客端节点系统,欢迎加入下一代社区网络。B3log distributed co…...

VBA07-方法
一、方法的定义 方法指对象所能执行的动作,它是一个动词。 二、方法的表达方式 三、关于工作簿的方法操作 3-1、新增一个工作簿 示例1: 此时,新增的工作簿的名字是系统默认的。 示例2: 【注意】: 当你尝试将工作簿…...

spring boot 统一异常处理
在 Spring Boot 应用中实现统一异常处理是非常重要的,它可以帮助我们更好地管理和响应应用程序中的错误。通过使用 ControllerAdvice 和 ExceptionHandler 注解,我们可以创建一个全局的异常处理器来捕获并处理不同类型的异常。 步骤 1: 创建一个异常处理…...

跟着小土堆学习pytorch(六)——神经网络的基本骨架(nn.model)
文章目录 一、model介绍二、Module三、张量3.1 定义3.2 用法 一、model介绍 容器 Containers 卷积层 Convolution Layers 池化层 Pooling layers 填…...

数字化落地过程中的研发效能治理如何开展?
数字化落地过程中,针对数字化识别到的问题和短板进行专项治理,是通过数字化手段持续提升研发效能非常重要的一环。组织级的数字化专项治理需要组织级团队和技术团队共同配合协同来完成。其中组织级团队负责制定整体的治理目标、流程和策略,根…...

Windows安装配置node.js
下载安装 下载 访问下载 | Node.js 中文网,下载 推荐使用长期支持版本,但是此次是学习用的,使用最新版本试一下 安装 其实一路next基本就可以了,注意调整下安装目录 查看版本 C:\Users\PC>node -v v22.11.0 C:\Users\PC>…...

Javaweb梳理9——JDBC
Javaweb梳理9——JDBC 1.JDBC概述1.1 JDBC概念1.2 JDBC本质1.3 JDBC好处 2.JDBC快速入门2.1 编写代码步骤2.2 具体操作 3 JDBC API详解3.1 DriverManager3.2 Connection3.2.1 获取执行对象3.2.2 事务管理 3.3 Statement3.3.1 概述3.3.2 代码实现 3.4 ResultSet3.4.1 概述3.4.2 …...

逆向CTF入门(如何找main)
Hello, world of reverse! start函数它在执行一些初始化操作,如获取命令行参数、获取环境变量值、初始化全局变量等,一切准备工作完成之后,再调用main函数 快速定位关键函数: 长驱直入法:当程序功能非常明确时,从程序…...

c语言归并排序
归并排序思想: 归并排序可以解释为是将放在数组里的一串数字进行拆分,拆分之后再判断大小合并的过程,每次都是从中间位置拆分,例如有七个数,第一次拆分就将它们分成前三个数为一个数组,后四个数为一个数组&…...

碳化硅陶瓷膜的特性
无机膜包括金属膜、陶瓷膜、玻璃膜等等,其中在水处理领域里出镜最多、应用最广的当属陶瓷膜。比起高分子有机膜,陶瓷膜硬度更高、通量更大、寿命更长,然其性能优越,成本也很“高昂”,故其生存空间自然是受到高分子有机…...

机器学习(三)——决策树(附核心思想、重要算法、概念(信息熵、基尼指数、剪枝处理)及Python源码)
目录 关于1 基本流程2 划分属性的选择2.1 方法一:依据信息增益选择2.2 方法二:依据增益率选择2.3 方法三:依据基尼指数选择 3 剪枝处理:防止过拟合3.1 预剪枝3.2 后剪枝 4 连续与缺失值4.1 连续值处理4.2 缺失值处理 5 多变量决策…...

良心无广,这五款电脑软件堪称必备,最后一个比快播都猛
来吧,直接上狠货! 哔哔音乐 这是一个基于哔哩哔哩开发的电脑听歌软件,众所周知!B站其实就是一个巨大的曲库,啥歌各种版本都能在这里找到。 所以如果依托B站开发听歌软件,那就是真的香,而且软件…...

Vue3中实现原生CSS完成圆形按钮点击粒子效果和定点旋转动画
效果: 源码: <script setup> import { ElMessage } from "element-plus"; const isClick () > {ElMessage.success(Clicked); }; </script><template><button click"isClick" class"button">…...