NDK 入门(二)—— 调音小项目

NDK 入门系列主要介绍 JNI 的相关内容,目录如下:

NDK 入门(一)—— JNI 初探
NDK 入门(二)—— 调音小项目
NDK 入门(三)—— JNI 注册与 JNI 线程
NDK 入门(四)—— 静态缓存与 Native 异常

前面介绍了很多 C/C++ 以及 JNI 相关的理论知识,本篇我们来实践一下,实现一个调音小 Demo。这个 Demo 需要用到一个专业的调音软件 FMOD 及其 API,先了解一下 FMOD。

1、FMOD 介绍

FMOD Ex 声音系统是为游戏开发者准备的革命性音频引擎,像 cocos2d,unity3d 游戏引擎默认就集成了 fmod。如今采用了 FMOD 作为音频引擎的游戏包括 Far Cry(孤岛惊魂)、Tom Clancy's Ghost Recon(幽灵行动),甚至著名的 World Of Warcraft(魔兽争霸)。

1.1 FMOD Studio

你可以在 FMOD 官网 下载 FMOD Studio 开始音频创作,该软件对非商业用途免费。

请添加图片描述

你可以对一段音频从音调、音色等多个方面添加特效以达到想要的特效。上图中就是对音调 PITCH 进行修改,从 0.0 ~ 2.0 有截然不同的效果,后续的 Demo 就会通过对 PITCH 设置不同的值以达到不同角色说话的效果。

1.2 FMOD API

FMOD 音效引擎库是用 C/C++ 写出来的,并且支持 Android 平台在内的多个平台,我们可以在 FMOD 官网 下载 FMOD Engine,将其 API 代码拷贝到我们的 Demo 中编译使用:

请添加图片描述

2、项目配置

主要是对 CMakeLists.txt 和模块的 build.gradle 进行配置,我们一步一步来。

2.1 拷贝 FMOD API 并配置 CMake

主要是拷贝 FMOD API 的头文件和库文件到我们的 Demo 中,并且配置 CMakeLists.txt 文件。步骤如下:

  1. 导入头文件,将 fmodstudioapi20219android\api\core\inc 目录下的头文件拷贝到项目 cpp\fmod\inc 目录下,在 CMakeLists.txt 中添加这些头文件:

    # 导入头文件
    include_directories(fmod/inc)
    
  2. 将 \fmod\inc 下的文件添加到 audio 动态库中:

    # 将所有 .c .h .cpp 文件都存入 allSources 这个对象中
    file(GLOB allSources *.c *.cpp)
    # 将 allSources 表示的文件全加入 audio 动态库中
    add_library(audio SHARED ${allSources})
    
  3. 导入库文件,将 fmodstudioapi20219android\api\core\lib 下所需平台的动态库文件拷贝到 app\main\jniLibs 目录下,并将该路径追加到 CMAKE_CXX_FLAGS 这个 C++ 编译器标志中:

    # ${CMAKE_CXX_FLAGS} 表示在 CMAKE_CXX_FLAGS 的原值上追加 -L
    # 所表示的路径:CMakeLists 所在的路径,向上一级到了 main,然后
    # 是 main/jniLibs 下 CPU 平台的文件夹
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}")
    

    注意 FMOD 提供了四个平台:arm64-v8a、armeabi-v7a、x86、x86_64 的动态库,你只需将需要的动态库拷贝到项目中就好,全都拷贝会增大 APK 体积

  4. 将 FMOD 动态库链接到目标库。第 3 步的动态库文件有两个,分别是 libfmod.so 和 libfmodL.so,需要将这两个库链接到目标库,也就是我们产出的 audio 库:

    # audio 是目标库,fmod 和 fmodL 分别是 libfmod 和 
    # libfmodL 去掉 lib 前缀的名称
    target_link_libraries( 
            audio 
            ${log-lib}
            fmod
            fmodL)
    

关于以上步骤,有几点要说明的:

  • 通过 adb 命令查看自己手机的 CPU 是哪个平台的架构:

    PS root> adb shell getprop ro.product.cpu.abi
    x86_64
    
  • 平台库要默认添加到 /app/src/main/jniLibs 目录下,因为 jniLibs 是 Gradle 默认的 C/C++ 的库目录,如果想修改,可以在 build.gradle 中修改源集:

    android {
        // ...
        sourceSets {
            main {
                jniLibs.srcDirs = ['myLibs']
            }
        }
        // ...
    }
    

有关在 AS 中对 CMake 配置的完整教程,可以参考官网教程配置 CMake。

2.2 拷贝 fmod.jar 与 gradle 配置

我们还需将 fmodstudioapi20219android\api\core\lib 下的 fmod.jar 拷贝到 app\libs 目录下,然后在 app 模块的 build.gradle 中依赖它:

dependencies {
    implementation files('libs\\fmod.jar')
}

此外,我们还需配置 abiFilters:

android {
    defaultConfig {
        ndk {
            // 仅在 64 位模拟器上运行,因此只打包该平台
            abiFilters "x86_64"
        }
    }

由于我只在模拟器上运行 Demo,所以只给 abiFilters 配置了 x86_64,这样 gradle 在编译打包时,就只会将 x86_64 目录下的库文件打包进 APK 中,避免默认情况下,gradle 将 jniLibs 下所有平台的库都打包进 APK,使 APK 体积无谓的增大。

到这里基本上就配置完成了,文件结构如下:

请添加图片描述

3、功能实现

实现效果就是点击 UI 上的按钮,然后播放相应效果的音频。原声资源文件如上图可见,存放在 src/assets 目录下。UI 效果:

请添加图片描述

那么编码就分为 Java 与 Native 两个部分。

3.1 Java 源码与 Java 头文件

Java 部分代码首先要导入动态库 audio 并且定义播放音效的 Native 方法:

	static {
        System.loadLibrary("audio");
    }

	// mode 表示采用哪种音效,audioPath 是原声文件路径
	private native void nativeChangeVoice(int mode, String audioPath);

然后定义模式常量与路径:

	// 正常
    private static final int MODE_NORMAL = 0;

    // 萝莉
    private static final int MODE_LOLITA = 1;

    // 大叔
    private static final int MODE_UNCLE = 2;

    // 惊悚
    private static final int MODE_THRILLER = 3;

    // 搞怪
    private static final int MODE_QUIRKY = 4;

    // 空灵
    private static final int MODE_ETHEREAL = 5;

    private static final String ORIGIN_AUTO_PATH = "file:///android_asset/daxian.mp3";

FMOD 需要初始化才可使用,在 Activity 销毁时要反注册 FMOD:

	@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mBinding = ActivityMainBinding.inflate(LayoutInflater.from(this));
        setContentView(mBinding.getRoot());
        initButtonListeners();

        // 初始化 FMOD
        FMOD.init(this);
    }

	@Override
    protected void onDestroy() {
        super.onDestroy();
        // 解除 FMOD 的 Receiver 注册
        FMOD.close();
    }

功能代码就是给按钮设置监听器,点击后就调用 Native 方法传入对应的音效。注意,这个 Native 方法是一个耗时方法,因此要放在子线程中调用:

	// 处理 Native 方法的线程池
	private ExecutorService mExecutorService = new ThreadPoolExecutor(1, 6, 60,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread t = Executors.defaultThreadFactory().newThread(r);
            t.setName("ThreadPool-" + t.getId());
            return t;
        }
    });

	private void initButtonListeners() {
        // native 进行的音频处理操作一定要放在子线程中执行
        mBinding.btnNormal.setOnClickListener(v -> mExecutorService.submit(() ->
                nativeChangeVoice(MODE_NORMAL, ORIGIN_AUTO_PATH)));
        mBinding.btnLuoli.setOnClickListener(v -> mExecutorService.submit(() ->
                nativeChangeVoice(MODE_LOLITA, ORIGIN_AUTO_PATH)));
        mBinding.btnDashu.setOnClickListener(v -> mExecutorService.submit(() ->
                nativeChangeVoice(MODE_UNCLE, ORIGIN_AUTO_PATH)));
        mBinding.btnJingsong.setOnClickListener(v -> mExecutorService.submit(() ->
                nativeChangeVoice(MODE_THRILLER, ORIGIN_AUTO_PATH)));
        mBinding.btnGaoguai.setOnClickListener(v -> mExecutorService.submit(() ->
                nativeChangeVoice(MODE_QUIRKY, ORIGIN_AUTO_PATH)));
        mBinding.btnKongling.setOnClickListener(v -> mExecutorService.submit(() ->
                nativeChangeVoice(MODE_ETHEREAL, ORIGIN_AUTO_PATH)));
    }

最后写一个通知方法,弹出 Toast 告知用户音频已经播放完,这个方法供 Native 调用:

	// 供 C++ 展示音频播放完毕的方法。由于 JNI 调用 Java 方法时会忽略掉方法的
    // 可见度,因此这里使用 private 并不影响 JNI 调用该方法
    private void playFinish(String msg) {
        // 因为调用 nativeChangeVoice() 是在线程池中进行的,所以 native 层调用
        // 本方法也是在子线程中,需要切换到主线程进行 UI 操作
        new Handler(getMainLooper()).post(() ->
                Toast.makeText(this, msg, Toast.LENGTH_SHORT).show());
    }

以上是 Java 代码。接下来要根据这个源文件生成对应的 JNI 的头文件。在 src\main\java 目录下运行命令:

javah com.jni.lesson3.MainActivity

会在该目录下生成一个头文件 com_jni_lesson3_MainActivity.h,里面有声明的常量和 Native 方法:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_jni_lesson3_MainActivity */

#ifndef _Included_com_jni_lesson3_MainActivity
#define _Included_com_jni_lesson3_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
#undef com_jni_lesson3_MainActivity_MODE_NORMAL
#define com_jni_lesson3_MainActivity_MODE_NORMAL 0L
#undef com_jni_lesson3_MainActivity_MODE_LOLITA
#define com_jni_lesson3_MainActivity_MODE_LOLITA 1L
#undef com_jni_lesson3_MainActivity_MODE_UNCLE
#define com_jni_lesson3_MainActivity_MODE_UNCLE 2L
#undef com_jni_lesson3_MainActivity_MODE_THRILLER
#define com_jni_lesson3_MainActivity_MODE_THRILLER 3L
#undef com_jni_lesson3_MainActivity_MODE_QUIRKY
#define com_jni_lesson3_MainActivity_MODE_QUIRKY 4L
#undef com_jni_lesson3_MainActivity_MODE_ETHEREAL
#define com_jni_lesson3_MainActivity_MODE_ETHEREAL 5L
/*
 * Class:     com_jni_lesson3_MainActivity
 * Method:    nativeChangeVoice
 * Signature: (ILjava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_com_jni_lesson3_MainActivity_nativeChangeVoice
  (JNIEnv *, jobject, jint, jstring);

#ifdef __cplusplus
}
#endif
#endif

将该文件拷贝到 src\main\cpp 目录下备用。

之所以没用 Kotlin 写上层的代码就是因为 javah 无法直接通过 kt 源代码生成头文件,而使用 kt 直接生成头文件的方法暂时还没跑通。

3.2 Native 播放音频并添加特效

在 native-lib.cpp 中,先要导入 fmod.hpp 和 com_jni_lesson3_MainActivity.h 这两个头文件,然后声明 FMOD 的命名空间,再开始实现 Native 方法:

#include <jni.h>
#include <string>
#include <fmod.hpp> // 导入 fmod 头文件
#include <unistd.h>
#include "com_jni_lesson3_MainActivity.h"

// 声明 fmod 的命名空间
using namespace FMOD;

extern "C"
JNIEXPORT void JNICALL
Java_com_jni_lesson3_MainActivity_nativeChangeVoice(JNIEnv *env, jobject thiz, jint mode,
                                                    jstring _path) {
    // C++11 将字符串字面量视为常量,因此不可以写 
    // char *resultString = "xxx",需要在其前面加 const 形成
    // 常量指针,或者使用 std 空间的 string 也是可以的:
//    std::string result = "默认音效播放完毕";
    const char *resultString = "默认音效播放完毕";
    const char *path = env->GetStringUTFChars(_path, nullptr);

    /*
     * 1.初始化 FMOD 组件
     */
    // 音效引擎系统
    System *system = nullptr;
    System_Create(&system);
    // 三个参数依次为:最大音轨数、系统初始化标记、额外的驱动数据
    system->init(32, FMOD_INIT_NORMAL, nullptr);

    // 声音
    Sound *sound = nullptr;
    // 四个参数依次为:音频文件路径、声音初始化标记、额外信息、声音对象的二级指针
    system->createSound(path, FMOD_DEFAULT, nullptr, &sound);
    // 音轨(声音播放的通道)
    Channel *channel = nullptr;
    // DSP:digital signal process 数字信号处理
    DSP *dsp = nullptr;

    /*
     * 2.播放声音
     */
    // 四个参数依次为:声音、分组音轨、是否暂停、通道
    system->playSound(sound, nullptr, false, &channel);

    /*
     * 3.增加特效
     */
    switch (mode) {
        case com_jni_lesson3_MainActivity_MODE_NORMAL:
            resultString = "原声播放完毕";
            break;
        case com_jni_lesson3_MainActivity_MODE_LOLITA:
            resultString = "萝莉播放完毕";
            // 创建 DSP 类型的 Pitch,这是音调
            system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
            // 将音调 Pitch 调节到 2.0
            dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 2.0f);
            // 添加音效到音轨中
            channel->addDSP(0, dsp);
            break;
        case com_jni_lesson3_MainActivity_MODE_UNCLE:
            resultString = "大叔播放完毕";
            // 与上一个 case 类似,将音调调至 0.7
            system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
            dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 0.7f);
            channel->addDSP(0, dsp);
            break;
        case com_jni_lesson3_MainActivity_MODE_QUIRKY:
            resultString = "搞怪小黄人播放完毕";
            // 从音轨获取频率并加快至 1.5 倍
            float frequency;
            channel->getFrequency(&frequency);
            channel->setFrequency(frequency * 1.5f);
            break;
        case com_jni_lesson3_MainActivity_MODE_THRILLER:
            resultString = "惊悚播放完毕";
            /*
             * 调整三个方面的特效:低音调 + 回声 + 颤抖,放在三个音轨上
             */
            // 1.音调调至 0.7
            system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
            dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 0.7f);
            channel->addDSP(0, dsp);

            // 2.回声
            system->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);
            // 设置回声延时为 200 ms,默认 500 ms
            dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 200);
            // 设置回声衰减度为 10,默认 50
            dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 10);
            channel->addDSP(1, dsp);

            // 3.颤抖音
            system->createDSPByType(FMOD_DSP_TYPE_TREMOLO, &dsp);
            dsp->setParameterFloat(FMOD_DSP_TREMOLO_FREQUENCY, 20);
            dsp->setParameterFloat(FMOD_DSP_TREMOLO_SKEW, 0.8f);
            channel->addDSP(2, dsp);

            break;
        case com_jni_lesson3_MainActivity_MODE_ETHEREAL:
            resultString = "空灵播放完毕";
            system->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);
            dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 200);
            dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 10);
            channel->addDSP(0, dsp);
            break;
    }

    /*
     * 4.播放完毕后进行回收操作并通知 Java 层
     */
    bool isPlaying = true;
    while (isPlaying) {
        // 播放完成后会把 isPlaying 修改为 false
        channel->isPlaying(&isPlaying);
        // 休眠 1 秒,usleep 单位是微秒
        usleep(1000 * 1000);
    }

    // 回收 JNI 对象
    sound->release();
    system->close();
    system->release();
    env->ReleaseStringUTFChars(_path, path);

    // 通知 Java 播放完毕
    jclass javaClass = env->GetObjectClass(thiz);
    jmethodID playFinishID = env->GetMethodID(javaClass, "playFinish", "(Ljava/lang/String;)V");
    jstring message = env->NewStringUTF(resultString);
    env->CallVoidMethod(thiz, playFinishID, message);
}

主要步骤分为 4 大步,已经在注释中详细说明了,基本上就是通过 dsp 对象调节音调、延迟、衰减度、回声等特效实现不同的效果。当然我们不是专业的调音师,调教出来的效果只能说是马马虎虎了。

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

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

相关文章

【错题集-编程题】重排字符串(贪心 + 构造)

牛客对应题目链接&#xff1a;重排字符串 (nowcoder.com) 力扣对应题目链接&#xff1a;1054. 距离相等的条形码 - 力扣&#xff08;LeetCode&#xff09; 一、分析题目 二、代码 1、没看题解之前AC的代码 //牛客代码(看了题解之后AC的代码) #include <iostream> #inc…

【Python BUG】connect: permission denied.

问题描述 解决方案 本质是权限问题 sudo密码即可

C语言阶段性测试错题纠正与拓展

引言&#xff1a;在2024年4月26日&#xff0c;我进行了C语言知识的“期末考试”。通过这次考试&#xff0c;我发现了我的知识漏洞。所以&#xff0c;我写下这篇博客来记录我的错题&#xff0c;并进行纠正&#xff0c;然后对于以前遗忘知识的回顾。 更多有关C语言的知识详解可前…

什么是摇摆交易?澳福一篇文章全面解读

摇摆交易是八大外汇交易策略其中一种交易策略。摇摆交易基于这样的想法&#xff0c;交易者希望利用波动范围和趋势市场的优势&#xff0c;通过选择“顶部”和“底部”&#xff0c;交易者可以相应地输入多头和空头头寸。即可以应用局部校正来最大化利润。fpmarkets澳福认为这种交…

P5931 灯泡

题目描述 相比 Wildleopard 的家&#xff0c;他的弟弟 Mildleopard 比较穷。他的房子是狭窄的&#xff0c;而且在他的房间里仅有一个灯泡。每天晚上&#xff0c;他徘徊在自己狭小的房子里&#xff0c;思考如何赚更多的钱。有一天&#xff0c;他发现他的影子的长度随着他在灯泡…

【Python的魅力】:利用Pygame实现游戏坦克大战——含完整源码

文章目录 一、游戏运行效果二、代码实现2.1 项目搭建2.2 加载我方坦克2.3 加载敌方坦克2.4 添加爆炸效果2.5 坦克大战之音效处理 三、完整代码 一、游戏运行效果 二、代码实现 坦克大战游戏 2.1 项目搭建 本游戏主要分为两个对象&#xff0c;分别是我方坦克和敌方坦克。用户可…

人工智能技术在教育中的潜力有多大

原文&#xff1a;人工智能技术在教育中的潜力有多大&#xff1f; - 知乎 作者&#xff1a;大全Prompt 链接&#xff1a;https://www.zhihu.com/question/637034129/answer/3346272227 来源&#xff1a;知乎 谢邀&#xff1a;在技术快速发展的今天&#xff0c;人工智能&#x…

数据复制的艺术:深拷贝与浅拷贝在JavaScript中的实现方式

前言 &#x1f4eb; 大家好&#xff0c;我是南木元元&#xff0c;热爱技术和分享&#xff0c;欢迎大家交流&#xff0c;一起学习进步&#xff01; &#x1f345; 个人主页&#xff1a;南木元元 目录 赋值和拷贝 浅拷贝与深拷贝区别 浅拷贝的实现方式 1.Object.assign() 2.…

变革 Perplexica:AI驱动的问答搜索引擎

Perplexica是一个开源的人工智能搜索工具&#xff0c;也可以说是一款人工智能搜索引擎&#xff0c;它深入互联网以找到答案。受Perplexity AI启发&#xff0c;它是一个开源选择&#xff0c;不仅可以搜索网络&#xff0c;还能理解您的问题。它使用先进的机器学习算法&#xff0c…

国产Sora诞生!清华团队发布Vidu大模型,可直接生成16秒视频

大模型之争已从单模态转向多模态。 4月27日&#xff0c;在2024中关村论坛年会未来人工智能先锋论坛上&#xff0c;清华大学联合北京生数科技有限公司正式发布了文生视频大模型——Vidu。 在会议上&#xff0c;清华大学人工智能研究院副院长、生数科技首席科学家朱军对外展示了…

windows11家庭版开启Hyper-v

前提&#xff1a;如果在控制面板中-->程序和功能-->启用和关闭windows功能-->没有Hyper-v 1.什么是Hyper-v&#xff1f; Hyper-v分为两个部分&#xff1a;底层的虚拟机平台、上层的虚拟机管理软件 2.Hyper-v安装 2.1新建hyper.cmd文件&#xff0c;写入下面的内容&…

C++初阶-----对运算符重载的进一步理解(2)

目录 1.对于加加&#xff0c;减减运算符的重载理解 2.const修饰的一些事情 3.日期对象之间的减法实现逻辑 1.对于加加&#xff0c;减减运算符的重载理解 &#xff08;1&#xff09;在C语言里面&#xff0c;我们已经知道并且了解加加&#xff0c;减减的一些基本的用法&#…

RepeatMasker 基因组重复区域文件

rmsk.txt 一般关注标红的几列, 各列含义: Schema for RepeatMasker - Repeating Elements by RepeatMasker "rmsk.txt" 是 UCSC Genome Browser 提供的一个文件,用于描述重复序列的注释信息。通常,它包含了以下列: 1. **bin**:UCSC Genome Browser 使用的染色…

笔记:编写程序,绘制一个展示 2013~2019 财年阿里巴 巴淘宝+天猫平台的 GMV 的柱形图,实现过程如下:

文章目录 前言一、GMV 的柱形图是什么&#xff1f;二、编写代码总结 前言 编写程序。根据实例 2 的要求&#xff0c;绘制一个展示 2013~2019 财年阿里巴 巴淘宝天猫平台的 GMV 的柱形图&#xff0c;实现过程如下&#xff1a; &#xff08;1&#xff09; 导入 matplotlib.pypl…

2024中国(江西)国际先进陶瓷材料及智能装备博览会

2024中国&#xff08;江西&#xff09;国际先进陶瓷材料及智能装备博览会 “中国&#xff08;江西&#xff09;国际先进陶瓷材料及智能装备博览会” 陶瓷三新展 &#xff08;新材料、新装备、新技术&#xff09; 绿色智能、引领未来 2024年11月1日-11月3日 中国江西 南昌…

生活服务推出品牌实惠团购,覆盖五一假期“吃喝玩乐”多场景

4月26日&#xff0c;抖音生活服务平台上线“跟着大牌过五一”活动会场&#xff0c;携手22家连锁品牌商家&#xff0c;于“五一”前推出优价团购和时令新品&#xff0c;覆盖“吃喝玩乐”多重购物需求&#xff0c;助力假期消费。同时&#xff0c;伴随各地涌现的文旅热潮&#xff…

项目:使用LNMP搭建私有云存储

目录 项目&#xff1a;使用LNMP搭建私有云存储 准备工作 回复快照&#xff0c;关闭安全软件 上传软件 设置nextcloud安装命令权限 设置数据库 重启数据库 配置nginx 安装 内网穿透 cpolar的域名信任 项目&#xff1a;使用LNMP搭建私有云存储 准备工作 回复快照&a…

C#上位机与S7-200Smart通信注意事项

S7-200SMART连接 问题描述 我们使用C#开发上位机和S7-200Smart系列PLC交互数据时&#xff0c;大多会用到Sharp7、Snap7之类的通信类库。有些通信类库默认的使用的是PG连接资源&#xff0c;而对于S7-200Smart来说&#xff0c;它的PG连接资源只有1个。 官网200smart提到的连接数…

解决idea不识别${pageContext.request.contextPath}的方法

文章目录 一、产生原因二、解决方法——直接修改web.xml文件三、修改模板——找到web.xml模板&#xff0c;修改替换 一、产生原因 由于web.xml 使用的web-app版本号过低。导致无法识别"{pageContext.request.contextPath}"。 IDEA在创建javaweb项目的时候&#xff0…

imx6ull配置交叉编译环境编译u-boot及linux所遇问题解决记录

文章目录 前言一、问题 1 及解决方法1、问题 1 描述2、问题 1 解决方法 二、问题 2 及解决方法1、问题 2 描述2、问题 2 解决方法 三、问题 3 及解决方法1、问题 3 描述2、问题 3 解决方法 四、问题 4 及解决方法1、问题 4 描述2、问题 4 解决方法 前言 CoM-iMX6UL(L) 是一款兼…