世界的本质是旋转(9) 利用声波传输mFSK多音调频文本

在上一篇文章中 ,主要介绍的是使用专用的业余无线电设备传输调相波形。

对于买不起SDR设备的学生来说,可以使用这篇文章介绍的思路,使用声卡的数据线传输IQ路的基带数据。线路输入的好处:

  1. 不经过空气的媒介,波形的本质一直保持在电磁波,噪声极少,相位线性度很高,没有什么失真。
  2. 左右声道独立传输,同步采集,恰好可以用来表示IQ路的波形。
  3. 线路输入的采样率通常可以很高,有的声卡支持到192000,也就是192KHz。

但是并不是所有的计算机都有线路输入口。目前大量的微型主板、笔记本电脑采用的是四芯耳麦口,如果强行把麦的连线和对方耳机通道贯通,阻抗也不匹配。那如果采用喇叭直放呢?笔者本来以为只是做一个实信号到0频段复信号的搬移、滤波即可,但一通实验下来,非常抓狂,发现一旦电磁波换能为机械波,其信道特性就变了:

  1. 十几块钱的麦克风、粗制滥造的USB小喇叭的频率、相位响应千奇百怪。
  2. 一般麦克风和音箱即使能够字面上支持极高的采样率,比如192K,但其实会发现大部分产品对12KHz以上的频率衰减就很大了,几乎不能用。
  3. 操作系统里设置的均衡器要清空,否则各种“大厅”、“低音炮”模式会加重失真。

考虑到上述原因,通过麦克风接收到的声波的相位、幅度已经不太靠谱了,我们这次使用更为直接的多音调频的方式传输数据。即使在这种模式下,也是更换了3种麦克风,最终用一个比较贵的麦克风(60多元)配合 Lenovo 笔记本自带的扬声器实现了测试。10元的扬声器和麦克风收到的音频频偏很大,且回音、黏连干扰很严重,背景还有很大的干扰(类似一种嗡嗡的声音)。

1. 实验目的

生成一个多音波形,用做基于麦克风、小音箱的点对点数据传递。

2. 波形设计

协议栈沿用上几篇的BPSK连续收发代码,只对调制进行设计如下:

层级协议
网络层字符或者IP
链路层pseudo HDLC
物理层-纠错连续VIT序列
物理层-波形mFSK

2.1 mFSK多音位置选取

在归一化采样率(声卡是实信号)下,有效频带为 [ 0 , π ] [0,\pi] [0,π] [ π , 2 π ] [\pi,2\pi] [π,2π]是实信号的镜像频率。我们利用其中一半的资源,做 2 m 2^m 2m分频,其中m范围为 [1,4],对应2,4,8,16fsk。以4 fsk为例,m=2,其归一化频率位置如下:

mFSK

  • 在以采样率归一化的频率下,每个实样点相位前进 π \pi π 就是采样窗口能够表达的最高频率,比如采样率为24000Hz,对应 0~ 2 π 2\pi 2π, 折半后为12000Hz,即12KHz。
  • 在生成样点时,控制相位值每个样点前进0.25-0.75 π \pi π 之间的某个角度,即生成了上图的各个频率位置。

相位推进的代码如下:

	//symbols 里是fsk符号const double c_pi = 3.14159265355;static double phy = 0; //当前样点相位const int msk  = 2;  //2^2 = 4fskconst double freq_span =  0.5 * c_pi /((1<<msk)-1);const int N = 128;//可根据调制指数反推for (int sym :symbols){//sym from 0 to 3, 归一化频率是 nfdouble nf = c_pi * 0.25 + sym * freq_span;//声卡单声道实波形,每个符号N个样点for (int i=0;i<N;++i){//加窗抑制旁瓣double win = sin(c_pi * i/(N-1)) * exp(-4.0*(i-N/2)/N*(i-N/2)/N);//输出样点int v = 16384 * cos(phy) * win;append_wav(v);//推进相位phy += nf ;}}}return signal;

可以注意到的是:

  1. 每个符号持续的样点数可以设置,这里是128个点。样点数越长,速率越慢,但频谱越趋近梳状。比如极端情况样点设置为24000点,那在24K采样率下,1秒才传输1个符号。样点数越少,传输越快,但切换造成的频谱展宽更宽,直到完全重叠。 描述这种频谱利用率的参数叫做调制指数。由于我们先确定了频率的位置、间隔,所以可以选取一个调制指数,计算N的值。

bad
但由于低成本扬声器、话筒本身的恶劣信道特性,带来的非线性失真太严重,不能用电磁波的fsk来对待声波。这个值在 16fsk下,取到96已经是极限了,非常挑剔扬声器和话筒。此时,若以24K采样率,传输16fsk,其速率为:

δ = 24000 / 96 B d = 250 B d = 1000 b p s \delta=24000/96 Bd= 250Bd=1000bps δ=24000/96Bd=250Bd=1000bps

  1. 如果希望双工通信,则进行频率交叉或者上下分片,速率继续折半。
  2. 加窗是为了在符号交叠区域幅度归零,抑制各个符号之间的频谱旁瓣干扰。

2.2 mFSK 映射取值

为保证相邻符号只变化1bit,减少误码率,我们可以使用简单的算法构造一个距离矩阵,找出以二进制相似性为约束的映射取值:

void init_mapCodes(int map_codes[],int map_syms[])
{bool bChecked[256]={true};map_codes[0]= 0;map_syms[0] = 0;for (int i=1;i<256;++i)bChecked[i] = false;fprintf(stderr,"Init MAP : 0 ");for (int g=1;g<(1<<msk);++g){std::map<int,std::set<int> > map_dis;for (int i=1;i<(1<<msk);++i){int h = i ^ map_codes[g-1];if (!bChecked[i])map_dis[qPopulationCount((quint32)h)].insert(i);}if (!map_dis.size())break;std::set<int> set_good = map_dis.begin()->second;int test = g-2;if (set_good.size()>1 && test >=0){std::map<int,std::set<int> > map_test;for (int p:set_good){int v = 0;for (int t = 0; t<= test;++t){int h = p ^ map_codes[test - t];v += qPopulationCount((quint32)h);}map_test[v].insert(p);}set_good = map_test.begin()->second;}bChecked[*set_good.begin()] = true;map_codes[g] = *set_good.begin();map_syms[*set_good.begin()] = g;fprintf(stderr,"%X ",map_codes[g]);}fprintf(stderr,"\nMAP_CODES=");for (int i=0 ;i<(1<<msk) ;++i){fprintf(stderr,"%02X ",map_codes[i]);}fprintf(stderr,"\nMAP_SYMS=");for (int i=0 ;i<(1<<msk) ;++i){fprintf(stderr,"%02X ",map_syms[i]);}fprintf(stderr,"\n");for (int i=0 ;i<(1<<msk) ;++i){fprintf(stderr,"%02X ",map_codes[i]);for (int j=0;j<(1<<msk) ;++j){int h = map_codes[i] ^ map_codes[j];int v = qPopulationCount((quint32)h);fprintf(stderr,"%2d ",v);}fprintf(stderr,"\n");}
}

以 8fsk\16fsk为例,各个符号之间的距离矩阵如下:

   00 01 03 02 06 04 05 07 
00  0  1  2  1  2  1  2  3 
01  1  0  1  2  3  2  1  2 
03  2  1  0  1  2  3  2  1 
02  1  2  1  0  1  2  3  2 
06  2  3  2  1  0  1  2  1 
04  1  2  3  2  1  0  1  2 
05  2  1  2  3  2  1  0  1 
07  3  2  1  2  1  2  1  0 
   00 01 03 02 06 04 05 07 0F 0B 09 0D 0C 08 0A 0E 
00  0  1  2  1  2  1  2  3  4  3  2  3  2  1  2  3 
01  1  0  1  2  3  2  1  2  3  2  1  2  3  2  3  4 
03  2  1  0  1  2  3  2  1  2  1  2  3  4  3  2  3 
02  1  2  1  0  1  2  3  2  3  2  3  4  3  2  1  2 
06  2  3  2  1  0  1  2  1  2  3  4  3  2  3  2  1 
04  1  2  3  2  1  0  1  2  3  4  3  2  1  2  3  2 
05  2  1  2  3  2  1  0  1  2  3  2  1  2  3  4  3 
07  3  2  1  2  1  2  1  0  1  2  3  2  3  4  3  2 
0F  4  3  2  3  2  3  2  1  0  1  2  1  2  3  2  1 
0B  3  2  1  2  3  4  3  2  1  0  1  2  3  2  1  2 
09  2  1  2  3  4  3  2  3  2  1  0  1  2  1  2  3 
0D  3  2  3  4  3  2  1  2  1  2  1  0  1  2  3  2 
0C  2  3  4  3  2  1  2  3  2  3  2  1  0  1  2  1 
08  1  2  3  2  3  2  3  4  3  2  1  2  1  0  1  2 
0A  2  3  2  1  2  3  4  3  2  1  2  3  2  1  0  1 
0E  3  4  3  2  1  2  3  2  1  2  3  2  1  2  1  0 

上述算法保证相邻的符号只差1比特,次相邻的差2比特。这样即使左右错位,也只差1比特,用VIT比较容易纠错。

3. 数据的接收

对于电磁波实信号处理,要测量其相位,可变到0频(复数信号),直接计算角度变化,就是频率。这种计算很方便,参考《50行实现C语言FM收音机-Taskbus Stdio封装器在SDR课程中的应用》一文。

但是对声波,由于扬声器、麦克风本身的特点,我们准备采用另一种更为鲁棒的方式,即短时FFT变换来进行接收、解调。

3.1 时域波形

使用Audacity可以从声卡录制,通过时域波形可以看到明显的频率选择。对某些低频的增益大,高频部分不仅更弱,相位也有抖动。
Audacity
这种情况下,可以通过两步来完成跟踪解调。

  1. 同步红色窗口的位置,使其对准每个符号。
  2. 判决符号,生成比特流。

3.2 短时FFT偏移同步

使用N=符号长度的窗口截取波形,使用短时fft可以对符号窗口呢的数据进行变换,获得瞬时的频谱。

如果窗口没有对准符号位置,则FFT后,相邻两个符号的位置都有能量。

如果窗口对准了符号的位置,则只有当前符号的频谱能量最高。

如果已经测量当前时刻各个fsk频点位置上的能量m,并按照大小排序,最强的放在第一个,次强的第二个,以此类推,则可以定义一个测度:

η = m 1 m 2 \eta=\cfrac{m_1}{m_2} η=m2m1

作为跟踪最佳取样位置的依据。下图显示的是符号变换的过程,在最佳位置上,只有1个显著的峰值,在过渡位置,有多个。

在这里插入图片描述
下面的代码通过fft峰值跟踪,锁定最佳的判决窗口偏移。

const double c_pi = 3.14159265355;
int msk  = 3;
//double * in;
fftw_complex *out = 0, *in = 0;
fftw_plan p = 0;
int fftsize = 512;
int sym_len = 128;
int fft_win = fftsize / 2;
int sym_order = 1;
int sym_freqct = 2;
int pos_ffttest[256];std::vector<unsigned char> demod_msk(const vector<unsigned char> &packagedta)
{static unsigned long long clk = 0;static int map_codes[256]={-1},map_syms[256]={-1};static short last_syms[1024];static int offset = 0;static double max_curr_v = 0;static double max_left_v = 0;static double max_right_v = 0;static long long nextJudge = 0;static double fftv[4096];if (map_codes[0]<0){init_mapCodes(map_codes,map_syms);}std::vector<unsigned char> results;//声卡单声道实波形int nSyms = packagedta.size()/2;short * pSym = (short *) packagedta.data();bool fftout = false;for (int i=0;i<nSyms;++i){last_syms[clk % sym_len] = pSym[i];++clk;if (clk <sym_len*160+33)continue;//Judgeif ((clk - offset) % sym_len==0){max_curr_v = 0;int max_curr_s = 0;double secd = 0;//FFTfor (int j = 0; j<fftsize;++j){if (j<sym_len)in[j][0] = last_syms[(clk - sym_len -1 + j) % sym_len];elsein[j][0] = 0;in[j][1] = 0;}fftw_execute(p);for (int j=0;j<sym_freqct;++j){double absv = sqrt(out[pos_ffttest[j]][0]*out[pos_ffttest[j]][0] + out[pos_ffttest[j]][1]*out[pos_ffttest[j]][1]);if (max_curr_v < absv){secd = max_curr_v;max_curr_v = absv;max_curr_s = j;}else if (secd < absv)secd = absv;}if (sub_fft && !fftout ){for (int j=0;j<fft_win;++j){double absv = sqrt(out[j][0]*out[j][0] + out[j][1]*out[j][1]);fftv[j] = absv;}TASKBUS::push_subject(sub_fft,instance,(unsigned int)fft_win*sizeof(double),(unsigned char *)fftv);fftout = true;}int syb = map_codes[max_curr_s];for (int j=0;j<sym_order;++j)results.push_back((syb>>(sym_order-1-j))&0x01);max_curr_v = max_curr_v / (secd+0.0001);}else if ((clk - offset - 4) % sym_len==0){max_left_v = 0;double secd = 0;//FFTfor (int j = 0; j<fftsize;++j){if (j<sym_len)in[j][0] = last_syms[(clk - sym_len -1 + j) % sym_len];elsein[j][0] = 0;in[j][1] = 0;}fftw_execute(p);for (int j=0;j<sym_freqct;++j){double absv = sqrt(out[pos_ffttest[j]][0]*out[pos_ffttest[j]][0] + out[pos_ffttest[j]][1]*out[pos_ffttest[j]][1]);if (max_left_v < absv){secd = max_left_v;max_left_v = absv;}else  if (secd < absv)secd = absv;}max_left_v = max_left_v / (secd+0.0001);}else if ((clk - offset + 4) % sym_len==0){max_right_v = 0;double secd = 0;//FFTfor (int j = 0; j<fftsize;++j){if (j<sym_len)in[j][0] = last_syms[(clk - sym_len -1 + j) % sym_len];elsein[j][0] = 0;in[j][1] = 0;}fftw_execute(p);for (int j=0;j<sym_freqct;++j){double absv = sqrt(out[pos_ffttest[j]][0]*out[pos_ffttest[j]][0] + out[pos_ffttest[j]][1]*out[pos_ffttest[j]][1]);if (max_right_v < absv){secd = max_right_v;max_right_v = absv;}else  if (secd < absv)secd = absv;}		if (nextJudge ==0)nextJudge = clk + sym_len +15;max_right_v = max_right_v / (secd+0.0001);}//钟差跟踪if (clk > nextJudge && nextJudge >0){//adjust offset;if (max_curr_v < max_right_v)--offset;else if (max_curr_v < max_left_v)++offset;nextJudge = 0;}}static int cct = 0;if (++cct % 50==0)fprintf(stderr,"clk=%lld,offset=%d\n",clk,offset);return results;
}

4 taskBus 工程

发送的模块涉及4个,如下图:
taskbus

  1. ConsolePannel 是一个简易的支持键盘输入的控制台。
  2. 输入的文本分拨次进入上一节所述的 bpsk 打包器 a2psk_encap,进行链路层、纠错码和包控制。
  3. 打包好的01字符输入mFSK调制器 a3msk_mod
  4. 调制后的实声音送入声卡模块 sink_soundcard
  5. 声卡由于是持续播放,会不停的回馈水位给封包器,形成环路。此环路保证连续播放时,生产波形和消费波形的同步。

接收模块原理如下:

resv

  1. 声卡单声道输入,一方面进入fft进行监视
  2. 另一方面,进入 a3msk_demod进行解调。
  3. 解调后,输出的fft结果也进行绘图,同时,信息进入组包器。
  4. 组包器输出进入控制台。

4 运行效果

该模块运行后,可持续播放声音,并把输入的文本带到接收方。下面的视频是接收方实时还原的文字情况。

博文用图01_taskbus_soundcard_16fsk

5 代码和发行版

代码参考:

https://gitcode.net/coloreaglestdio/taskbus_course/-/tree/master/src/a3_msk

发行版参考:

https://gitcode.net/coloreaglestdio/taskbus/-/releases

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

江苏省汽车及零部件产业协作配套对接会在苏州举行

5月28日&#xff0c;江苏省汽车及零部件产业协作配套对接会暨“百场万企”大中小企业融通对接活动在苏州举办。本次活动以“深化整零协作&#xff0c;促进大中小企业融通发展”为主题&#xff0c;由江苏省工业和信息化厅、中国中检所属中国汽车工程研究院股份有限公司&#xff…

分享7个手机上堪称神器却鲜为人知的小众宝藏软件

分享7个手机上堪称神器却鲜为人知的小众宝藏软件&#xff0c;保证大家用过就舍不得卸载~ 1.志愿大师 马上就是高考季了&#xff0c;高考完的同学如果不知道该如何选专业&#xff0c;可以用这个软件来分析各个大学和专业设置等信息。 志愿大师是一款专为高考后学生设计的高效辅…

Others - 网友都是些人才,哈哈哈哈

感谢万能的网友们&#xff01; 原本枯燥的知识&#xff0c;在网友生动形象的表达下&#xff0c;也能简单易懂&#xff0c;哈哈哈哈

Prometheus+Altermanager实现钉钉告警

PrometheusAltermanager实现钉钉告警 Prometheus和Altermanager的安装这里就不赘述了&#xff0c;我之前的文章有写到 不记得的小伙伴可以去看看Prometheus和Altermanager的安装使用 直接开始上操作 下载钉钉并打开&#xff0c;先创建一个接收告警信息的钉钉群 添加一个自定…

【Nacos_bugs】java.lang.IllegalStateException: Failed to load ApplicationContext

报错原因 找不到配置文件。 Bug 排查 如果使用 Nacos 管理配置文件&#xff0c;需要检查本地 bootstrap.yml 配置是否出现问题&#xff1a; 检查点&#xff1a; 检查 Nacos 服务的地址有没有配置错误&#xff0c;如上图 ①&#xff0c;格式严格为 IP:端口号" 检查 D…

Ant Design Vue Pro流程分析记录

一、基本介绍 Ant Design Vue Pro提供了一套完整的解决方案&#xff0c;包括路由、状态管理、UI组件库、HTTP请求封装等&#xff0c;方便开发者快速搭建和维护企业级应用。 二、官网地址 Ant Design Pro of Vue 三、下载及安装 推荐使用Yarn 四、文件分布及说明 dist&#xf…

常见Rabbitmq面试题及答案总结

1、 什么是 rabbitmq 釆用AMQP高级消息队列协议的一种消息队列技术撮大的特点就是消费并不需要 确保提供方存在,实现了服务之间的高度解耦 2、 为什么要使rabbitmq &#xff08;1&#xff09; 在分布式系统下具备异步&#xff0c;削峰&#xff0c;负载均衡等一系列高级功能&…

企业百度百科如何修改

百度百科是一个可以让我们快速的了解一个企业情况的地方&#xff0c;同时也让我们的企业展示了什么&#xff0c;还有哪些是可以做的。 注册与登录 首先&#xff0c;你需要注册一个百度账号&#xff0c;并通过邮箱或手机进行验证。登录后&#xff0c;可以开始创建或编辑百度百科…

Leetcode - 周赛400

目录 一&#xff0c;3168. 候诊室中的最少椅子数 二&#xff0c;3169. 无需开会的工作日 三&#xff0c;3170. 删除星号以后字典序最小的字符串 四&#xff0c;3171. 找到按位与最接近 K 的子数组 一&#xff0c;3168. 候诊室中的最少椅子数 本题是一道模拟题&#xff0c;直…

归并排序法

归并排序法是典型的分治算法应用&#xff0c;1946年由冯.诺伊曼发明。 算法思路&#xff1a;归并排序算法有两个基本操作&#xff0c;一是分&#xff0c;也就是把原数组划分成两个子数组的过程&#xff0c;另一个是治&#xff0c;它将两个有序数组合并成一个更大的有序数组。 …

基于小波区间相关的信号降噪方法(MATLAB 2021B)

在我们处理信号过程中最重要的任务就是找到信号隐藏的规律和信号的特征。常用的傅里叶分析法只能用于在时间域或者频率域上分析信号&#xff0c;而通常的数据会在时间域和频率域均有特征。而小波分析是继傅里叶分析之后的一大突破性创新&#xff0c;也是近年来在学术界非常热门…

扩散世界模型已训练出赶超人类的智能体?

论文标题&#xff1a; Diffusion for World Modeling:Visual Details Matter in Atari 论文作者&#xff1a; Eloi Alonso, Adam Jelley, Vincent Micheli, Anssi Kanervisto, Amos Storkey, Tim Pearce, Franois Fleuret 项目地址&#xff1a; https://github.com/eloial…

基于EBAZ4205矿板的图像处理:10gamma变换

基于EBAZ4205矿板的图像处理&#xff1a;10gamma变换 项目全部文件 会上传项目全部文件&#xff0c;如果没传&#xff0c;可以私信催我一下&#xff0c;最近太忙了 先看效果 我的项目中的gamma的变换系数为2.2&#xff0c;是会让图像整体变暗的&#xff0c;看右图说明我的ga…

CPVT(ICLR 2023)论文解读

paper&#xff1a;Conditional Positional Encodings for Vision Transformers official implementation&#xff1a;GitHub - Meituan-AutoML/CPVT 存在的问题 位置编码的局限性&#xff1a;传统Transformer中的绝对位置编码&#xff08;无论是可学习的还是固定的&#xff…

【c++进阶(二)】STL之string类的模拟实现

&#x1f493;博主CSDN主页:Am心若依旧&#x1f493; ⏩专栏分类c从入门到精通⏪ &#x1f69a;代码仓库:青酒余成&#x1f69a; &#x1f339;关注我&#x1faf5;带你学习更多c   &#x1f51d;&#x1f51d; 1.前言 本章重点 本章主要介绍一些关键接口的模拟实现&#xff…

0603《哎选》已经稳定运行2年

0603《哎选》已经稳定运行2年 0603《哎选》已经稳定运行2年 介绍 2022年6月3日经过一年的努力&#xff0c;优雅草蜻蜓G系统原生版诞生&#xff0c;本产品应用于《哎选》&#xff0c;经过2年的运营不断的更新迭代&#xff0c;目前产品已经有了一定的用户量&#xff0c;本产品…

Spark 3.5.1 升级 Java 17 异常 cannot access class sun.nio.ch.DirectBuffer

异常说明 使用Spark 3.5.1 升级到Java17的时候会有一个异常&#xff0c;异常如下 SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". SLF4J: Defaulting to no-operation (NOP) logger implementation SLF4J: See http://www.slf4j.org/codes.htm…

离轴磁编案例分享 - 机器人关机模组

客户产品 六轴协作机器人产品 关机模组 关机模组内部结构 项目介绍 客户需求: 需要离轴&#xff0c;优点&#xff1a;可以中空走线&#xff0c;方便线缆从机器人中间穿过去&#xff0c;可以更好得保护好线缆&#xff0c;不需要把线漏在外面&#xff0c;影响使用和产品寿命。目…

最适合上班族和宝妈的兼职副业,一天500多,小众副业项目

近年来&#xff0c;地方特色小吃逐渐受到人们的热烈追捧&#xff0c;尤其是在直播的助力下&#xff0c;许多地方的特色小吃得以走进大众视野&#xff0c;吸引了大量流量和人气。因此&#xff0c;有很大一部分商家和创业者看准了这一商机&#xff0c;纷纷投身于地方特色小吃的制…

怎么把多种内容做成二维码?扫码展现多种内容的制作方法

现在很多的场景下都有不同类型的二维码&#xff0c;用来承载内容为用户提供内容展示&#xff0c;比如图片、视频、文字、文件、地图等等内容&#xff0c;都可以组合起来通过扫码的方式在手机上展示。那么如何制作组合内容的二维码相信有很多的小伙伴都非常的感兴趣。 其实二维…