# 信号实验箱 **Repository Path**: wen-fan/signal-test-box ## Basic Information - **Project Name**: 信号实验箱 - **Description**: 基于QT、z-fft实现的微型可视化信号运算/变换/处理工具 - **Primary Language**: C++ - **License**: GPL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 33 - **Created**: 2023-09-12 - **Last Updated**: 2023-09-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 信号实验箱 [![star](https://gitee.com/finalize/signal-test-box/badge/star.svg?theme=dark)](https://gitee.com/finalize/signal-test-box/stargazers)[![fork](https://gitee.com/finalize/signal-test-box/badge/fork.svg?theme=dark)](https://gitee.com/finalize/signal-test-box/members) 本软件为一个基于表达式的信号计算、绘图软件, 主要功能为对输入的信号进行采样, 然后对采样序列进行各种运算, 并将运算结果绘制为波形, 支持使用动态库扩充软件内可调用的函数 For native English speaker, please check out [English Version Readme](./readme_en.md) That Document have the same content as this one, but write in English. 本软件支持跨平台部署, 这主要得益于QT的跨平台API以及CMake的跨平台构建能力, 你可以在一个宿主机上编译出不同平台的目标, 目前已测试过Ubuntu 22.04(基于Win11 WSL2)、Win7/Win10/Win11、Android x86/Android armeabi-v7a(基于Win11 WSA和Realme X2 pro) 本软件支持国际化, 使用QT的国际化机制实现, 目前拥有简体中文翻译数据(软件内默认使用英文作为原始语言, 然后再添加其他语言的翻译文件实现国际化, 这也是QT翻译机制的推荐做法), 软件在启动时会获取操作系统使用的语言设置, 因此会自动适配语言, 如果操作系统的语言是中文简体和英文之外的语言, 会默认使用英文 由于本软件的支持库是以`git submodule`形式引入的, 所以请在克隆仓库时, 使用`git clone --recursive`命令递归将子模块一起克隆下来, 如果克隆时忘记, 也可以在克隆完成之后在源码路径下执行`git submodule init`和`git submodule update`两条指令来补救 如果你使用`Download ZIP`功能而不是`git clone`, 那么请将[z-fft](https://gitee.com/finalize/z-fft)一同下载, 并解压到`external_libs`路径下, 最终应该形成这样的文件目录结构 ![子模块](readme_rc/submodule.png) 本文档由多个文件构成, 在此文档内提及的任意其他文件均为超链接形式(蓝色字体), 只需点击即可跳转到对应的文档 如果你在使用、阅读源码、构建等方面有任何疑问, 均可在仓库内提交`Issues`, 我看到会处理 - [1. 简单示例](#1-简单示例) - [2. 工作原理](#2-工作原理) - [3. 信号定义](#3-信号定义) - [4. 频谱模式](#4-频谱模式) - [5. 内置编译器](#5-内置编译器) - [5.1. 表达式文法](#51-表达式文法) - [5.2. 注释文法](#52-注释文法) - [5.3. 数组文法](#53-数组文法) - [5.3.1. 数组数据填充规则](#531-数组数据填充规则) - [5.4. if 条件文法](#54-if-条件文法) - [5.5. 关键字、特殊变量与常量](#55-关键字特殊变量与常量) - [6. 函数库](#6-函数库) - [7. 工作区](#7-工作区) - [8. 发行版](#8-发行版) - [9. 构建源码](#9-构建源码) - [10. 滤波器指南](#10-滤波器指南) - [11. 向本仓库提交代码](#11-向本仓库提交代码) ## 1. 简单示例 在信号列表框内右键单击(安卓平台为长按), 在弹出的菜单内选择新增信号, 然后输入信号表达式, 设置采样率和采样点数后点击计算当前信号即可看到波形, 显示波形的图表可以放大缩小(鼠标滚轮操作), 也可以平移(按住鼠标左键拖动) ![信号0](./readme_rc/sig1.png) ![信号1](./readme_rc/sig2.png) 信号可以嵌套使用, 方便拿两个信号做运算, 最大可嵌套32层 ![信号2](./readme_rc/sig3.png) 除正余弦以外, 内置还有一些其他的函数可供使用, 例如软件随机rand和硬件随机hrand ![信号3](./readme_rc/sig4.png) ![信号4](./readme_rc/sig5.png) 傅立叶变换, 对变换结果调用length是因为fft函数输出为复数, 而length函数为向量求模函数, 因此可求出幅度谱 ![输入信号](./readme_rc/mix_sig.png) ![幅度谱](./readme_rc/fft.png) 软件自带的`filters`库内带有几个滤波函数, 其中`lpf`与`hpf`为IIR滤波器, 分别为低通滤波器与高通滤波器, 接收采样序列和截止频率作为参数, `fir`为FIR滤波器, 接收采样序列、滤波器阶数和滤波器系数(可使用matlab导出)作为参数 这里给出1阶IIR低通滤波器、3阶IIR低通滤波器和32阶FIR低通滤波器的示例效果, 关于滤波器的更多信息, 请阅读[10. 滤波器指南](#10-滤波器指南) ![OrigSig](./readme_rc/orig_sig.png) ![IIR_filter1](./readme_rc/iir_lv1.png) ![IIR_filter2](./readme_rc/iir_lv3.png) ![FIR_filter](./readme_rc/fir_lv32.png) 查看滤波之后的频谱图可以发现, 高阶数的滤波器可达到较好的滤波效果 ![IIR_freq1](./readme_rc/iir_lv1_freq.png) ![IIR_freq2](./readme_rc/iir_lv3_freq.png) ![FIR_freq](./readme_rc/fir_lv32_freq.png) ## 2. 工作原理 1. 一个信号可用一个表达式 **f** 进行描述, 在任意的 **t** 时刻, 信号的强度为 **f(t)** 2. 若给定某时刻 **t** , 则表达式 **f(t)** 的运算结果即为信号 **f** 在 **t** 时刻的信号强度采样值 3. **t** 可根据软件内设置的采样频率计算得出, 然后重复采样 **N** 次, **N** 为软件内设置的采样点数 4. 软件在计算前会将要计算的信号的表达式编译为语法树 5. 语法树内每个节点在计算自身的值之前, 会先将子节点的值计算出来, 因此根节点的计算值即为表达式整体的值 6. 若自定义信号之间存在相互引用, 则内层信号将会先运算得到运算值, 然后再进行外层信号的运算 7. 信号存在最大嵌套层数限制, 防止某信号引用自身或者多个信号交叉引用造成无限递归的情况 ## 3. 信号定义 在软件内所有的信号定义均为表达式形式, 例如`sin(t)`便是代表了经过数字采样的 **sin** 信号, `sin(100*t) + sin(200*t)`便是将两个频率不同的信号叠加 信号间可以相互引用, 例如`sig0 = sin(100*t) sig1 = sin(200*t) sig2 = sig0+sig1` 信号自身引用自身或交叉引用均会触发最大嵌套限制 ## 4. 频谱模式 在使用`fft`函数计算信号的傅里叶变换后, 如果只使用`length`函数求出幅度谱的话, 图表的X轴坐标为点数, 例如下图 ![iir_freq_index](readme_rc/iir_lv1_freq_index.png) 可以看到十字光标所在位置X坐标为10, 这表示的是此数据在采样序列里的下标, 如果想要知道此点对应的频率值是多少就需要自己计算, 上图中X=10, 采样率5KHz, 采样点数为256点, 因此此处的频率为`5000/256*10=195.3Hz`, 也就是原信号里的200Hz频率成分. 进行这样的计算虽然简单, 但是却很麻烦, 而且计算所需的所有参数在软件内都有, 所以, 可以通过勾选右下角的频谱模式复选框来让软件去计算 勾选频谱模式之后, 横坐标的含义将变为实际的频率值, 例如下图中, 这里还是用的一样的信号去绘图, 只是打开了频谱模式而已, 可以看到十字光标所在位置的X值为195.313, 与我们的计算结果一致 ![IIR_freq1](./readme_rc/iir_lv1_freq.png) 需要注意的是, 频谱模式实际上只会显示一半的实际计算数据, 即采样点数为1024点时, 图表内实际上只会显示前512个数据点, 这是由于频谱具有对称性, 因此只需要前一半的频谱就足够分析了 ## 5. 内置编译器 软件内置一个简单的编译器, 支持常用的数学表达式文法、双斜杠单行注释文法、数组文法和 **if** 条件文法 ### 5.1. 表达式文法 数学表达式文法即最常用最简单的数学表达式, 例如`(sin(t)+cos(t+3))*6` 绝大部分的运算符都是支持的, 包括加减乘除和除余, 二进制与, 二进制或, 二进制异或, 逻辑与, 逻辑或 一些特殊的数学运算使用函数的形式提供, 例如卷积和傅立叶变换 ### 5.2. 注释文法 双斜杠单行注释与C语言的单行注释相同, 例如`sin(t)//sin of t`, 双斜杠后的所有字符均被忽略 ### 5.3. 数组文法 数组的文法与Python的List类似, 使用方括号作为边界, 使用逗号作为分隔符, 例如[1, 2, 3] 但是由于本软件的基础工作原理为采样, 因此每个数组的实际大小均为采样点数的大小, 所以就存在数据填充问题 #### 5.3.1. 数组数据填充规则 如果一个数组仅包含一个数字, 则使用这个数字填充至采样点数大小, 例如采样点数为128时, 数组[1]代表包含了128个1的数组, 这种情况下也等价于只写数字1, 单个数字也会被扩充至128长度 如果一个数组的大小大于1, 且不足采样点数, 则在数组后补0填充至采样点数的长度 如果一个数组的长度刚好等于采样点数, 则直接使用数组内的数据 如果一个数组的长度大于采样点数, 则截断数组, 只使用数组的前N个数据 ### 5.4. if 条件文法 **if**条件文法的执行逻辑与C语言的三目运算符相同, 但是并不使用`?:`符号而是使用**if**和**else**关键字, 并且语法顺序也略有不同, 实际上这里的语法和python的设计一致, 语法规则为`数学表达式+if关键字+比较表达式[+else+数学表达式]`, 方括号内的部分是可省略的, 例如: 1. 5 if index > 15 else 3//等价于C语言 index > 15 ? 5 : 3 2. 5 if index > 15//省略else子语句的写法, 等价于 index > 15 ? 5 : 0 **if**条件语法搭配 **index** 变量可非常方便的产生阶跃信号和冲激信号, 例如: 1. 1 if index >= 0//单位阶跃信号 2. 1 if index == 0//幅值为1的冲激信号 3. 1 if index >= 5//向右移动5的单位阶跃信号 4. 1 if index == 5//向右移动5的单位冲激信号 5. 5 if index >= 0//幅值为5的阶跃信号 6. 5 if index == 0//幅值为5的冲激信号 ### 5.5. 关键字、特殊变量与常量 编译器识别如下两个关键字 1. if: 标记if语句的开始, 后跟判断条件 2. else: 标记else子句的开始, 后跟表达式 编译器支持如下特殊变量: 1. **t**: 代表当前的时间, 也就是第 **n** 次采样除以采样频率 **fs**, `t = n/fs` 2. **index**: 代表当前是第几次采样, 也就是所谓的 **n**, 例如采样点数1024, 那么 **index** 就是[0-1023]的序列 3. N: 代表总的采样点数, 与软件内设置的采样点数相同, 注意: 这里是**大写的N**, 与**小写的n**含义不同, 编译器**并不支持**小写的n, 而是**使用index作为等价的变量** 4. **fs**: 代表当前的采样率, 单位为Hz 编译器支持如下常量: 1. **pi**: 代表圆周率 ## 6. 函数库 所有的函数均为外部函数库解析得到, 每个函数库均需要提供`pLibFunction_t lib_init(void)`函数, 可用于库被载入时执行一些初始化动作, 并且此函数需要返回函数注册表, 用于描述需要加载动态库文件里面的哪些符号, 以及这些函数所需要的参数数量(用于信号编译时的参数检查) 除此之外, 函数库还可以导出`void lib_exit(void)`函数用于在库被卸载时做一些收尾清理工作, 此函数是可选函数, 如果没有导出则不调用, 不影响库的导入 如果没有初始化动作的需求, 则`lib_init`函数可被宏`register_function_lib`替换, 此宏定义相当于定义`lib_init`函数, 并返回参数指定的函数注册表, 省去使用者编写定义函数库的代码 目前软件内置了5个库, 即`basic`、`filters`、`io`、`transform`和`window`, 分别是基本函数库、数字滤波器库、IO访问库、信号变换库和窗函数库, 具体的库函数说明请参阅[lib/readme.md](lib/readme.md) 同样的, 如果你自己构建了动态库, 你可以把他们放置在安装目录的`lib`子目录下, 然后在信号表达式内调用即可 ## 7. 工作区 软件内所有的信号、当前的采样率和采样点数的设置共同构成了一个工作区, 而软件可以将这个工作区保存为一个json文件, 并且可从json文件恢复一个工作区, 此json文件的结构是简洁明了的, 很容易看懂其结构, 也很方便手写, 所以你也可以手写一个工作区文件然后直接导入到软件内, 目前此仓库的`workspace_demo`目录下提供了一些用于验证软件功能或验证库功能的demo工作区, 你可以直接导入使用 关于工作区以及工作区文件的详细说明, 请参阅[workspace_demo/readme.md](workspace_demo/readme.md) ## 8. 发行版 为了避免使用者自行构建的麻烦, 你可以在仓库右侧, `简介`的下方找到`发行版`一栏, 在这里是编译并打包好的软件, 可直接下载使用. 但是请注意, 只有较大版本更新之后才会发布发行版, 因此你所下载到的发行版的功能可能是不如源码强的, 如果要体验最新版软件请参考[9. 构建源码](#9-构建源码)自行构建最新版软件 ## 9. 构建源码 本工程使用`CMake`作为顶层构建系统, `CMake`可产生多种下层构建系统所需文件, 例如`Makefile`或`build.ninja` 除此之外, 本工程的内置编译器并非纯手写完成, 词法分析器和语法分析器基于`flex`/`bison`构建, 因此在构建时需要用户的开发环境内有可用的`flex`和`bison` 本项目自从`commit hash: cc33da5de23c4434de462cadfa59c189196cbb15`之后, 不再使用原先的方式指定工具链和各种路径相关的信息, 目前使用`CMake`的`Preset`机制进行环境配置 如果你事先编译过`Windows`平台软件, 然后需要编译`Android`平台, 请先删除`build`目录, 反过来也是一样的 关于如何构建`Windows exe`和`Android APK`的详细流程, 请参阅[build_project.md](build_project.md) ## 10. 滤波器指南 软件自带的滤波库内的函数, 主要分为两种, 即IIR滤波器和FIR滤波器, 其中`lpf`和`hpf`属于IIR滤波器, `average`和`fir`属于FIR滤波器, 两种滤波器的使用方法是不一样的 关于如何使用各种滤波函数, 如何使用matlab设计FIR滤波器, 如何使用导出的系数, 请参阅[coefficient_demo/readme.md](coefficient_demo/readme.md) ## 11. 向本仓库提交代码 如果你希望参与到本项目的开发中, 请参照如下流程 1. 在云端仓库内点击fork, 将本仓库复制到你自己的名下, 如果遇到不能fork的情况, 说明本仓库正在进行一些较大的修改, 为了防止产生半成品分支, 在此期间会禁止fork, 请等待几天之后再次尝试 2. 将你自己名下的仓库克隆至本地 3. 在本地提交代码后, 推送到云端(此时是推送到了你名下的云端仓库) 4. 向本仓库提交pull request, 方向为[你的仓库] -> [本仓库] 5. 本仓库的成员会审核(代码风格和实现逻辑)并测试(功能是否达到预期)pull request的代码, 测试通过后此代码将被合并进入本仓库