# 信号实验箱
**Repository Path**: freebsd/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**: 35
- **Created**: 2023-01-11
- **Last Updated**: 2023-01-11
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 信号实验箱
[](https://gitee.com/finalize/signal-test-box/stargazers)[](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翻译机制的推荐做法), 如果你希望添加新的语言, 可以使用QT语言家工具生成当前工程的.ts文件, 例如本工程现有的中文翻译文件[ui/zh_CN.ts](ui/zh_CN.ts), 然后再为生成的ts文件内的英文原文添加目标语言的译文, 添加完成后保存文件, 将ts文件发布为qm文件即可
目前软件在启动时会获取操作系统使用的语言设置, 因此会自动适配语言, 如果操作系统的语言是中文简体和英文之外的语言, 会默认使用英文(如果你自行添加了其他语言的翻译文件就可以显示翻译后的语言, 总的来说就是翻译不了的一律不翻译)
由于本软件的支持库是以`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`路径下, 最终应该形成这样的文件目录结构

如果你在使用、阅读源码、构建等方面有任何疑问, 均可在仓库内提交`Issues`, 我看到会处理
- [1. 简单示例](#1-简单示例)
- [2. 工作原理](#2-工作原理)
- [3. 信号定义](#3-信号定义)
- [4. 频谱模式](#4-频谱模式)
- [5. 内置编译器](#5-内置编译器)
- [6. 函数库](#6-函数库)
- [7. 工作区](#7-工作区)
- [8. 发行版](#8-发行版)
- [9. 构建源码](#9-构建源码)
- [9.1. 首选方式](#91-首选方式)
- [9.2. 手动方式](#92-手动方式)
- [10. 滤波器指南](#10-滤波器指南)
- [11. 安卓平台编译指南](#11-安卓平台编译指南)
## 1. 简单示例
在信号列表框内右键单击(安卓平台为长按), 在弹出的菜单内选择新增信号, 然后输入信号表达式, 设置采样率和采样点数后点击计算当前信号即可看到波形


信号可以嵌套使用, 方便拿两个信号做运算, 最大可嵌套32层

除正余弦以外, 内置还有一些其他的函数可供使用, 例如软件随机rand和硬件随机hrand


傅立叶变换, 对变换结果调用length是因为fft函数输出为复数, 而length函数为向量求模函数, 因此可求出幅度谱


软件自带的`filters`库内带有几个滤波函数, 其中`lpf`与`hpf`为IIR滤波器, 分别为低通滤波器与高通滤波器, 接收采样序列和截止频率作为参数, `fir`为FIR滤波器, 接收采样序列、滤波器阶数和滤波器系数(可使用matlab导出)作为参数
这里给出1阶IIR低通滤波器、3阶IIR低通滤波器和32阶FIR低通滤波器的示例效果, 关于滤波器的更多信息, 请阅读[10. 滤波器指南](#10-滤波器指南)




查看滤波之后的频谱图可以发现, 高阶数的滤波器可达到较好的滤波效果



## 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轴坐标为点数, 例如下图

可以看到十字光标所在位置X坐标为10, 这表示的是此数据在采样序列里的下标, 如果想要知道此点对应的频率值是多少就需要自己计算, 上图中X=10, 采样率5KHz, 采样点数为256点, 因此此处的频率为`5000/256*10=195.3Hz`, 也就是原信号里的200Hz频率成分. 进行这样的计算虽然简单, 但是却很麻烦, 而且计算所需的所有参数在软件内都有, 所以, 可以通过勾选右下角的频谱模式复选框来让软件去计算
勾选频谱模式之后, 横坐标的含义将变为实际的频率值, 例如下图中, 这里还是用的一样的信号去绘图, 只是打开了频谱模式而已, 可以看到十字光标所在位置的X值为195.313, 与我们的计算结果一致

需要注意的是, 频谱模式实际上只会显示一半的实际计算数据, 即采样点数为1024点时, 图表内实际上只会显示前512个数据点, 这是由于频谱具有对称性, 因此只需要前一半的频谱就足够分析了
另外一个需要注意的就是, fft函数的输出实际上只有一半的频谱, 这同样是利用频谱的对称性, 比如我们假定采样点数为1024, 那么1024个实数输入后得到512个复数(依然是1024个float变量, 但是一半实部一半虚部), 并且在调用length函数计算每个复数的模长之后, 会得到512个实数模长, 因此如果不用频谱模式显示的话, 你会发现图表的右侧(即后512个数据点)全是0, 因为不在频谱模式下的图表会显示1024个数据, 如果你打开频谱模式, 结合上面说的频谱模式的特性, 图表只会显示512个数据点, 这样就可以最大利用显示区域去显示有用的数据
## 5. 内置编译器
软件内置一个简单的编译器, 支持常用的数学表达式文法、双斜杠单行注释文法和 **if** 条件文法
数学表达式文法即最常用最简单的数学表达式, 例如`(sin(t)+cos(t+3))*6`
双斜杠单行注释与C语言的单行注释相同, 例如`sin(t)//sin of t`, 双斜杠后的所有字符均被忽略
**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//单位冲激信号
3. 1 if index >= 5//向右移动5的单位阶跃信号
4. 1 if index == 5//向右移动5的单位冲激信号
5. 5 if index >= 0//幅值为5的阶跃信号
6. 5 if index == 0//幅值为5的冲激信号
编译器支持如下特殊变量:
1. **t** , 代表当前的时间, 也就是第 **n** 次采样除以采样频率 **fs**, `t = n/fs`
2. **index** , 代表当前是第几次采样, 也就是所谓的 **n**, 例如采样点数1024, 那么 **index** 就是[0-1023]的序列
编译器支持一个常量 **pi** , 代表圆周率
编译器对于符号的处理和解析是大小写无关的, pi等价PI Pi pI, t也等价于T
## 6. 函数库
所有的函数均为外部函数库解析得到, 每个函数库均需要提供`pLibFunction_t lib_init(void)`函数, 可用于库被载入时执行一些初始化动作, 并且此函数需要返回函数注册表, 用于描述需要加载动态库文件里面的哪些符号, 以及这些函数所需要的参数数量(用于信号编译时的参数检查)
除此之外, 函数库还可以导出`void lib_exit(void)`函数用于在库被卸载时做一些收尾清理工作, 此函数是可选函数, 如果没有导出则不调用, 不影响库的导入
如果没有初始化动作的需求, 则`lib_init`函数可被宏`register_function_lib`替换, 此宏定义相当于定义`lib_init`函数, 并返回参数指定的函数注册表, 节去使用者编写定义函数库的代码
目前软件内置了4个库, 即`basic`、`filters`、`io`和`transform`, 分别是常用的数学函数库, 数字滤波器库, IO访问库和信号变换库
数学库包括了`sin`、`cos`、`length`、`min/max`、`rand/hrand`、`abs`、`conv`、`sum`等常用函数, 可用于产生信号或者数值计算, `sin`/`cos`为正弦/余弦函数, `length`为复数求模函数, `min`/`max`为最大/最小值函数, `rand`/`hrand`为软件/硬件随机数函数, `abs`为绝对值函数, `conv`为卷积运算函数, `sum`为序列求和函数
滤波器库包括了常见的数字滤波器, 例如低通滤波器`lpf`、高通滤波器`hpf`、FIR滤波器`fir`和均值滤波器`average`, 其中`lpf`和`hpf`为IIR滤波器
变换库目前有`fft`、`ifft`、`dft`和`idft`函数, 调用`z-fft`完成傅里叶正变换和反变换, 其中`dft`和`idft`是基于傅里叶变换定义式实现的原始算法, `fft`和`ifft`是`dft`和`idf`的高效实现算法(基2的DIT算法)
IO访问库目前只实现了`read_file`和`write_file`两个读写文件的接口, 可用于导入信号, 导入滤波器系数或者保存信号
同样的, 如果你自己构建了动态库, 你可以把他们放置在安装目录的lib子目录下, 然后在信号表达式内调用即可
## 7. 工作区
软件内所有的信号、当前的采样率和采样点数的设置共同构成了一个工作区, 而软件可以将这个工作区保存为一个json文件, 并且可从json文件恢复一个工作区, 此json文件的结构是简洁明了的, 很容易看懂其结构, 也很方便手写, 所以你也可以手写一个工作区文件然后直接导入到软件内, 目前此仓库的`workspace_demo`目录下提供了一些用于验证软件功能或验证库功能的demo工作区, 你可以直接导入使用
关于工作区以及工作区文件的详细说明, 请参阅[workspace_demo/readme.md](workspace_demo/readme.md)
## 8. 发行版
为了避免使用者自行构建的麻烦, 你可以在仓库右侧, `简介`的下方找到`发行版`一栏, 在这里是编译并打包好的软件, 可直接下载使用. 但是请注意, 只有较大版本更新之后才会发布发行版, 因此你所下载到的发行版的功能可能是不如源码强的, 如果要体验最新版软件请参考[9. 构建源码](#9-构建源码)自行构建最新版软件
## 9. 构建源码
本工程使用`CMake`作为顶层构建系统, `CMake`可产生多种下层构建系统所需文件, 例如`Makefile`
除此之外, 本工程的内置编译器并非纯手写完成, 词法分析器和语法分析器基于`flex`/`bison`构建, 因此在构建时需要用户的开发环境内有可用的`flex`和`bison`
### 9.1. 首选方式
本人使用vscode开发本工程, 所使用到的工作区文件也在源码目录下, 完整下载源码后双击[tiny-signal-box.code-workspace](./tiny-signal-box.code-workspace)文件即可自动打开vscode对代码进行编辑和浏览.工作区文件内包括了vscode的设置项, 构建所用的task以及调试所需要的launch配置, 因此一般来说无需使用者再次配置, 只要你是通过工作区文件打开的工程, 那么你所使用的配置就和我的是一样的
构建之前, 请先安装`CMake`和`Ninja`, 前者用于产生后者所需的文件, 后者则负责执行文件中的规则, 调用编译器编译和链接程序
然后是`QT`, 本软件使用了`QT 5.15`, 安装时请注意不要安装MSVC编译器版本的, 要安装GCC编译器版本的QT, 同时也要选中`Charts`模块和`GCC 8.1.0`编译器本身
为了能让`CMake`找到`QT`, 还需要将`QT`安装目录下的`Qt5Config.cmake`文件所在的文件夹配置到`CMAKE_PREFIX_PATH`环境变量下, 否则`CMake`在配置工程的时候会在`find_package`语句上报错, 此文件位于`QT安装目录\版本号\mingw81_32或者mingw81_64\lib\cmake\Qt5`
除此之外, 还需要安装`flex`和`bison`, 推荐的方式是安装**虚拟的linux环境**, 比如`Cygwin`, 在安装`Cygwin`时, 选择`flex`和`bison`的软件包即可, 在`Cygwin`安装完成后, 将vscode的内置终端替换为`Cygwin`所使用的`bash.exe`, 并且将`Cygwin`的`bin`目录的路径加入到`PATH环境变量中`
如果安装`Cygwin`时没有选择`flex`和`bison`, 可以在安装完成后再次运行安装程序重新选择, 实际上这并不会重新安装, 只会把你对软件包的各种修改应用到已装好的`Cygwin`里
关于如何配置vscode的内置终端, 请查看[Terminal profiles](https://code.visualstudio.com/docs/editor/integrated-terminal#_terminal-profiles)
如果要在vscode内构建, 请按照工作区文件给出的推荐, 安装推荐的扩展程序(打开工作区时, 如果有一些推荐扩展你没有安装, vscode也会弹窗提示), 此处列出的是扩展程序的ID, 在搜索时可以唯一确定到某个具体的扩展程序而不会出现重名的问题

安装好后再次打开工作区文件, 在左下角选择QT提供的工具链然后在vscode内按下Ctrl+Shift+B快捷键即可开始编译, 若你的生成任务默认快捷键不是这个, 也可以手动执行
选择工具链的方式如下:


如果你之前没有使用过cmake扩展程序, 这里的列表应该是空的, 你可以选择`[scan for kits]`来让它扫描`PATH`环境变量里已有的编译器, 也可以手动配置`%HOMEPATH%\AppData\Local\CMakeTools\cmake-tools-kits.json`文件, 在此文件里你可以配置编译器路径或工具链文件, 两种方式均可向cmake扩展程序提供一个工具链使用, 配置完成之后, 选择工具链的列表就会出现已配置的工具链, 选择`QT 5.15`安装目录下的`GCC8.1.0`工具链即可, 下方是我的配置, 包括了QT的Windows工具链和安卓工具链


编译完成后, 按下Crtl+Shift+T快捷键运行测试任务即可看到程序运行效果
若运行中出现问题, 直接在vscode内调试即可(快捷键F5), 但是请确保工作区文件内的gdb可执行文件的路径配置正确
如果你想编译出apk文件并在安卓平台上运行本软件, 请参阅[安卓平台编译指南](#11-安卓平台编译指南)
### 9.2. 手动方式
首选方式的本质实际上只不过是把命令的调用交给了vscode的扩展程序来做而已, 本质上依然是在控制台执行命令, 因此这个过程我们可以手动来做, 这样就不再依赖于vscode及其扩展程序
cmake的编译过程分为配置(configure)和构建(build)两步, 配置的步骤会生成底层构建系统所需的文件, 例如Makefile或ninja.build, 而构建的步骤实际上就是调用make或ninja去编译工程
本软件在Windows下编译exe的的配置命令为`cmake -DCMAKE_BUILD_TYPE:STRING=Debug -DCMAKE_C_COMPILER:FILEPATH=你的QT GCC编译器路径 -DCMAKE_CXX_COMPILER:FILEPATH=你的QT G++编译器路径 -B build -G Ninja`
配置完成之后的构建命令为`cmake --build build --target all`
两条命令执行完成之后, 就可以在build目录下看到可执行文件了
安卓平台的配置命令过于冗长, 因此不推荐手动执行, 源码内已提供编译脚本和工具链文件, 两种方式可二选一使用, 详细请参阅[安卓平台编译指南](#11-安卓平台编译指南)
## 10. 滤波器指南
软件自带的滤波库内的函数, 主要分为两种, 即IIR滤波器和FIR滤波器, 其中`lpf`和`hpf`属于IIR滤波器, `average`和`fir`属于FIR滤波器, 两种滤波器的使用方法是不一样的
`lpf`和`hpf`本身都是一阶的IIR滤波器, 并且无需使用者提供系数, 只需要提供采样序列和截止频率即可, 由于所有的函数的输出都是采样序列, 因此可以嵌套使用构成任意阶数的IIR滤波器
`average`内部使用滑动均值滤波, 参数为采样序列和窗口大小, 它可看做系数为`1/窗口大小`的FIR滤波器, 也是最简单的一种fir滤波器
`fir`为直接型FIR滤波器, 参数为采样序列、滤波器阶数和滤波器系数, 内部对信号和滤波器系数做卷积运算, 因此改变系数就可以变为任何一种FIR滤波器, 此系数可由matlab计算导出保存为文件, 再使用`read_file`函数将系数读入为采样序列, 通过传参的方式让`fir`函数使用
目前此仓库的`coefficient_demo`目录下提供了一些用matlab设计好的系数, 可以在信号表达式内直接使用, 这些demo参数文件的命名方式为: 类型_截止频率_采样率, 系数的采样率和软件设置的采样率一致才可使用
关于如何使用matlab设计FIR滤波器, 如何使用导出的系数, 请参阅[coefficient_demo/readme.md](coefficient_demo/readme.md)
## 11. 安卓平台编译指南
在进行安卓的编译之前, 首先要配置安卓的开发环境, 包括`JDK`、`安卓SDK`、`安卓NDK`以及`QT For Andriod`, 这些环境的配置方式网络上有许多教程, 此处不再赘述
你可能已经注意到本软件的源码路径下已经有了一个名为[android_build.sh](android_build.sh)的shell脚本文件, 此文件的确是用于编译安卓apk的脚本文件, 如果你的环境配置正确, 那么此脚本就可以顺利编译出apk文件, 需要注意的是, 一些路径是直接写在此脚本内的, 所以你可能需要手动修改其中的一些路径才能正确执行此脚本
上述脚本可被`bash`程序正确执行, 在Windows下你可以用前文提到的`Cygwin`环境提供的bash, Linux环境下执行此脚本就更简单了
除了在控制台手动调用脚本编译apk以外, 还有一种更为优雅的方式, 你可以将源码路径下的[android_build.cmake](android_build.cmake)文件按照上述脚本的配置方式配置其中的路径, 然后将此文件拷贝到源码路径之外的任何位置
, 推荐拷贝到`QT安装目录\版本号\android\lib\cmake`路径, 并在`cmake-tools-kits.json`文件内添加一项, 使用工具链文件的方式指定到刚才拷贝过去的工具链文件, 再回到vscode, 删除掉`build`文件夹, 重新选择工具链之后再次按下Ctrl+Shift+B快捷键或手动执行`build`任务, 然后你会发现控制台里已经在跑安卓的构建流程了, 这样一来切换不同的目标平台就会非常容易
需要注意的是`QT 5.15`的安卓提供了四种不同的体系结构的库, 源码路径下的.cmake文件指定的目标平台是x86, 如果你需要在手机上跑请把`set(ANDROID_ABI x86)`语句改为`set(ANDROID_ABI armeabi-v7a)`即可, shell脚本也是同理, 但是shell脚本还需要额外修改`-DANDROID_BUILD_ABI_armeabi-v7a:BOOL=OFF \`为`-DANDROID_BUILD_ABI_armeabi-v7a:BOOL=ON \`, 并且把`-DANDROID_BUILD_ABI_x86:BOOL=ON \`修改为`-DANDROID_BUILD_ABI_x86:BOOL=OFF \`