Repository navigation
Expand file tree
/
Copy pathbpc_decoder.py
More file actions
1300 lines (1081 loc) · 59.1 KB
/
Copy pathbpc_decoder.py
File metadata and controls
1300 lines (1081 loc) · 59.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
"""
BPC授时信号解码器
基于音频输入解码BPC授时信号的4种脉冲宽度
"""
import numpy as np
import pyaudio
import threading
import time
import queue
from collections import deque
import argparse
import datetime
import logging
import matplotlib.pyplot as plt
# 配置日志
logging.basicConfig(
level=logging.INFO,
# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(),
logging.FileHandler('bpc_decoder.log', encoding='utf-8')
]
)
logger = logging.getLogger(__name__)
class BPCDecoder:
"""BPC授时信号解码器 - 部分参考bclswl0827/BPC-Clock的算法"""
# BPC脉冲宽度定义(毫秒)- 按照专利文档格式
PULSE_WIDTHS = [100, 200, 300, 400] # 对应四进制数值0,1,2,3 (0.1s, 0.2s, 0.3s, 0.4s)
# BPC信号参数
BPC_FREQUENCY = 68500 # 68.5kHz
FRAME_LENGTH = 20 # 每帧20位数据(除去定位符)
# 定位符参数
SYNC_CYCLE_MIN = 1800 # 定位符最小周期(ms)
SYNC_CYCLE_MAX = 2100 # 定位符最大周期(ms)
# 脉冲有效范围
PULSE_MIN = 30 # 最小脉冲宽度(ms)
PULSE_MAX = 420 # 最大脉冲宽度(ms) - 调整为400ms+容差
CYCLE_MIN = 600 # 最小周期时间(ms) REFRESH_TIME = 1
THRESHOLD = 0.09 # 音频幅度阈值(根据实际测试调整)
# 信号平滑参数
SIGNAL_SMOOTH_WINDOW = 50 # 信号平滑窗口大小(毫秒)
MIN_SILENCE_DURATION = 30 # 最小有效无声间隔(毫秒),小于此值的无声会被忽略
SIGNAL_HISTORY_SIZE = 20 # 信号历史记录大小 # 新增:抗干扰参数
MOVING_AVERAGE_WINDOW = 10 # 滑动平均窗口大小(采样点数)
MEDIAN_FILTER_WINDOW = 5 # 中值滤波窗口大小(采样点数)
MIN_PULSE_DURATION = 40 # 最小有效脉冲持续时间(毫秒)- 进一步调整为50ms
MIN_SILENCE_GAP = 25 # 最小有效无声间隔(毫秒)- 调整为25ms
SIGNAL_DEBOUNCE_TIME = 10 # 信号防抖时间(毫秒)- 调整为10ms,减少延迟
THRESHOLD_POSITION = 0.4 # 阈值位置(0-1之间)
def __init__(self, sample_rate=44100):
self.sample_rate = sample_rate
# 脉冲检测参数
self.threshold = self.THRESHOLD # 使用类常量初始化实例属性
self.min_signal_duration = 0.05 # 最小信号持续时间50ms
# BPC状态 - 对应源码中的BPCStatus
self.bpc_status = {
'status': False, # 是否在脉冲中
'time': 0, # 脉冲开始时间
'previous': 0, # 上一次脉冲开始时间
'cycle': 0, # 方波周期时间(ms)
'pulse': 0 # 脉冲持续时间(ms)
}
# 解码器状态 - 对应源码中的DecoderStatus
self.decoder_status = {
'status': False, # 解码器运行状态
'done': False, # 解码是否完成
'counter': 0, # 数据缓存位置计数器
'data': [0] * 20 # 数据缓存数组(除去定位符后的19位+1位)
}
# 解码后的时间信息
self.date_info = {
'carry': False, # 根据秒数判断是否需进位
'second': 0, # 秒
'hour': 0, # 时
'minute': 0, # 分
'week': '', # 星期
'forenoon': True, # 午前
'day': 0, # 日
'month': 0, # 月
'year': 0 # 年
}
# 信号检测状态
self.in_signal = False
self.signal_start_time = 0
self.last_signal_end = 0 # 统计信息
self.stats = {
'total_pulses': 0,
'valid_pulses': 0,
'invalid_pulses': 0,
'sync_detected': 0,
'frames_decoded': 0,
'decode_errors': 0,
'smooth_gaps_ignored': 0, # 被忽略的短暂无声间隙数量
'smooth_spikes_ignored': 0, # 被忽略的短暂信号突刺数量
'short_cycles_ignored': 0 # 被忽略的短周期噪声数量
}
# 频率追踪参数
self.enable_frequency_tracking = True # 是否启用频率追踪
self.target_frequency = None # 追踪到的目标频率
self.frequency_lock_threshold = 5 # 频率锁定阈值(连续检测次数)
self.frequency_lock_count = 0 # 频率锁定计数器
self.frequency_search_range = (300, 3000) # 搜索频率范围(Hz)
self.frequency_bin_size = 50 # 频率分析精度(Hz)
# FFT分析相关
self.fft_size = 2048 # FFT大小
self.frequency_history = deque(maxlen=10) # 频率历史记录
self.audio_buffer = np.array([]) # 音频数据缓冲区,用于累积足够的数据进行FFT
# 带通滤波器相关(简单的数字滤波)
self.filter_enabled = False
self.filter_center_freq = None
self.filter_bandwidth = 200 # 滤波器带宽(Hz)
# 添加调试信息
self._last_debug_time = 0
self._frequency_update_time = 0
# 信号平滑相关
self.signal_history = deque(maxlen=self.SIGNAL_HISTORY_SIZE) # 信号强度历史
self.signal_state_history = deque(maxlen=self.SIGNAL_HISTORY_SIZE) # 信号状态历史
self.last_signal_time = 0 # 上次信号时间
self.last_silence_start = 0 # 上次无声开始时间
self.in_temporary_silence = False # 是否在临时无声中
self.signal_smooth_buffer = deque(maxlen=int(self.SIGNAL_SMOOTH_WINDOW * self.sample_rate / 1000)) # 信号平滑缓冲区
# 新增:保存(rms, 时间戳) - 保存15秒的数据用于Web显示
self.rms_history = deque(maxlen=1500) # 假设每秒100个样本,15秒=1500个点
# 新增:信号滤波相关
self.rms_buffer = deque(maxlen=self.MOVING_AVERAGE_WINDOW) # RMS滑动平均缓冲区
self.rms_median_buffer = deque(maxlen=self.MEDIAN_FILTER_WINDOW) # 中值滤波缓冲区
self.smoothed_rms_history = deque(maxlen=1500) # 平滑后的RMS历史,同样保存15秒
# 新增:BPC静音间隔检测状态机(BPC使用静音间隔编码数据)
self.silence_state = 'carrier' # 静音状态:carrier, silence_start, in_silence, silence_end
self.silence_start_time = 0 # 静音开始时间
self.silence_end_time = 0 # 静音结束时间
self.carrier_start_time = 0 # 载波开始时间
self.last_state_change_time = 0 # 上次状态变化时间
# 新增:防抖动相关
self.debounce_high_start = 0 # 高电平防抖开始时间
self.debounce_low_start = 0 # 低电平防抖开始时间
self.confirmed_state = False # 确认的信号状态
# 新增:自适应阈值相关
self.adaptive_threshold_enabled = True # 是否启用自适应阈值
self.threshold_update_interval = 3.0 # 阈值更新间隔(秒)
self.threshold_history_duration = 3.0 # 用于计算阈值的历史数据时长(秒)
self.last_threshold_update_time = 0 # 上次更新阈值的时间
self.rms_for_threshold = deque(maxlen=int(self.threshold_history_duration * self.sample_rate / 1024)) # 用于阈值计算的RMS历史
self.initial_threshold = self.THRESHOLD # 保存初始阈值
self.threshold_stats = {
'updates': 0,
'min_threshold': self.THRESHOLD,
'max_threshold': self.THRESHOLD,
'current_min_rms': 0,
'current_max_rms': 0
}
# 新增:信噪比计算相关
self.signal_rms_history = deque(maxlen=100) # 信号(载波)RMS历史
self.noise_rms_history = deque(maxlen=100) # 噪声(静音)RMS历史
self.current_snr = 0.0 # 当前信噪比
self.last_snr_update_time = 0 # 上次更新信噪比的时间
self.snr_update_interval = 1.0 # 信噪比更新间隔(秒)
logger.info(f"BPC授时信号解码器初始化")
logger.info(f" 目标频率: {self.BPC_FREQUENCY} Hz")
logger.info(f" 帧长度: {self.FRAME_LENGTH}位数据")
logger.info(f" 定位符周期: {self.SYNC_CYCLE_MIN}-{self.SYNC_CYCLE_MAX}ms")
logger.info(f" 脉冲范围: {self.PULSE_MIN}-{self.PULSE_MAX}ms")
logger.info(f" 最小周期时间: {self.CYCLE_MIN}ms (短于此值将被忽略为噪声)")
logger.info(f" 信号阈值: {self.THRESHOLD} (自适应阈值已启用)")
logger.info(f" 信号平滑: 最小无声间隔 {self.MIN_SILENCE_DURATION}ms, 历史窗口 {self.SIGNAL_HISTORY_SIZE}")
logger.info(f" 平滑窗口: {self.SIGNAL_SMOOTH_WINDOW}ms")
logger.info(f" 自适应阈值: 每{self.threshold_update_interval}秒更新,基于最近{self.threshold_history_duration}秒数据")
def plot_rms_history(self):
"""绘制RMS历史记录图表(横轴为相对时间,纵轴为RMS幅度)"""
if not self.rms_history:
logger.warning("没有RMS历史记录可供绘图")
return
rms_values, times = zip(*self.rms_history)
# 横轴为相对时间(秒)
t0 = times[0]
rel_times = [float(t - t0) for t in times]
plt.figure(figsize=(15, 8))
# 绘制原始RMS信号
plt.subplot(2, 1, 1)
plt.plot(rel_times, rms_values, label='Original RMS', color='blue', alpha=0.7)
# 绘制滤波后的RMS信号(如果有的话)
if self.smoothed_rms_history:
smoothed_values, smoothed_times = zip(*self.smoothed_rms_history)
smoothed_rel_times = [float(t - t0) for t in smoothed_times]
plt.plot(smoothed_rel_times, smoothed_values, label='Filtered RMS', color='red', linewidth=2)
# 绘制阈值线
plt.axhline(y=self.threshold, color='green', linestyle='--', label=f'Threshold ({self.threshold})')
plt.xlabel('Time (s)')
plt.ylabel('RMS Amplitude')
plt.title('BPC Signal RMS Amplitude Over Time - Original vs Filtered')
plt.grid(True)
plt.legend()
# 绘制脉冲状态(如果有滤波后的数据)
if self.smoothed_rms_history:
plt.subplot(2, 1, 2)
smoothed_values, smoothed_times = zip(*self.smoothed_rms_history)
smoothed_rel_times = [float(t - t0) for t in smoothed_times]
# 绘制二值化信号状态
binary_signal = [1 if v > self.threshold else 0 for v in smoothed_values]
plt.plot(smoothed_rel_times, binary_signal, label='Binary Signal State', color='orange', linewidth=2)
plt.xlabel('Time (s)')
plt.ylabel('Signal State')
plt.title('Binary Signal State (After Filtering)')
plt.grid(True)
plt.legend()
plt.ylim(-0.1, 1.1)
plt.tight_layout()
plt.ticklabel_format(style='plain', axis='x') # 避免科学计数法
plt.show()
def detect_signal(self, audio_data):
"""检测音频中的BPC信号 - 基于GitHub项目算法,增加高级信号滤波和状态机"""
current_time = time.time()
# 1. 频率追踪 - 自动锁定最强频率
if self.enable_frequency_tracking:
self.update_frequency_tracking(audio_data)
# 2. 带通滤波 - 仅处理目标频率的信号
processed_audio = self.apply_bandpass_filter(audio_data)
# 3. 计算RMS(均方根)作为信号强度
rms = np.sqrt(np.mean(processed_audio ** 2))
# 记录原始RMS
self.rms_history.append((rms, current_time))
# 4. 应用信号滤波(滑动平均 + 中值滤波)
filtered_rms = self.apply_signal_filtering(rms) # 记录滤波后的RMS
self.smoothed_rms_history.append((filtered_rms, current_time))
# 5. 自适应阈值更新
if self.adaptive_threshold_enabled:
self.update_adaptive_threshold(filtered_rms, current_time)
# 6. 更新信噪比计算
self.update_snr_calculation(filtered_rms, current_time)
# 7. 使用BPC静音间隔检测状态机
self.bpc_silence_state_machine(filtered_rms, current_time)
# 添加调试信息 - 每秒显示一次信号强度和状态
if not hasattr(self, '_last_debug_time'):
self._last_debug_time = 0
if current_time - self._last_debug_time >= 1.0: # 每秒显示一次
freq_status = f"未锁定"
if self.target_frequency is not None:
freq_status = f"已锁定 {self.target_frequency:.1f}Hz (强度:{self.frequency_lock_count})"
# 信噪比状态
snr_status = "未知"
if self.current_snr != 0.0:
if self.current_snr == float('inf'):
snr_status = "∞ dB"
else:
snr_status = f"{self.current_snr:.1f} dB"
logger.debug(f"[DEBUG] 原始RMS: {rms:.4f}, 滤波后: {filtered_rms:.4f}, 阈值: {self.threshold:.4f}")
logger.debug(f"[DEBUG] 静音状态: {self.silence_state}, 确认状态: {self.confirmed_state}, 频率: {freq_status}")
logger.debug(f"[DEBUG] 信噪比: {snr_status}, 信号样本: {len(self.signal_rms_history)}, 噪声样本: {len(self.noise_rms_history)}")
self._last_debug_time = current_time
def smooth_signal(self, rms, current_time):
"""信号平滑处理 - 忽略过短的无声间隙"""
current_time_ms = int(current_time * 1000)
# 基础信号状态判断
raw_signal_high = rms > self.threshold
# 添加到信号历史
self.signal_history.append((current_time_ms, rms))
self.signal_state_history.append((current_time_ms, raw_signal_high))
# 如果历史记录不足,直接返回原始状态
if len(self.signal_state_history) < 3:
return raw_signal_high
# 获取最近的信号状态
recent_states = list(self.signal_state_history)[-5:] # 最近5个状态
# 检测短暂的无声间隙
if self.detect_short_silence_gap(recent_states, current_time_ms):
# 如果检测到短暂无声间隙,保持信号状态为高
self.stats['smooth_gaps_ignored'] += 1
logger.debug(f"[信号平滑] 忽略短暂无声间隙 at {current_time_ms}ms")
return True
# 检测短暂的信号突刺(噪声)
if self.detect_short_signal_spike(recent_states, current_time_ms):
# 如果检测到短暂信号突刺,保持信号状态为低
self.stats['smooth_spikes_ignored'] += 1
logger.debug(f"[信号平滑] 忽略短暂信号突刺 at {current_time_ms}ms")
return False
return raw_signal_high
def detect_short_silence_gap(self, recent_states, current_time_ms):
"""检测短暂的无声间隙"""
if len(recent_states) < 5:
return False
# 检查模式:信号-无声-信号,且无声时间很短
for i in range(len(recent_states) - 2):
if (recent_states[i][1] and # 前面有信号
not recent_states[i + 1][1] and # 中间无信号
recent_states[i + 2][1]): # 后面有信号
# 计算无声间隔时间
silence_duration = recent_states[i + 2][0] - recent_states[i][0]
# 如果无声间隔小于最小有效间隔,认为是短暂间隙
if silence_duration < self.MIN_SILENCE_DURATION:
return True
return False
def detect_short_signal_spike(self, recent_states, current_time_ms):
"""检测短暂的信号突刺(噪声)"""
if len(recent_states) < 5:
return False
# 检查模式:无声-信号-无声,且信号时间很短
for i in range(len(recent_states) - 2):
if (not recent_states[i][1] and # 前面无信号
recent_states[i + 1][1] and # 中间有信号
not recent_states[i + 2][1]): # 后面无信号
# 计算信号持续时间
signal_duration = recent_states[i + 2][0] - recent_states[i][0]
# 如果信号持续时间小于最小信号持续时间,认为是噪声突刺
if signal_duration < self.MIN_SILENCE_DURATION:
return True
return False
def bpc_decode_pulse(self):
"""BPC脉冲解码函数 - 对应源码中的BPCDecoder()"""
# 检查解码器状态
if not self.decoder_status['status']:
return
# 显示解码进度
if self.decoder_status['counter'] < 19:
progress = int(self.decoder_status['counter'] * 100 / 19)
# 添加信噪比信息
snr_info = ""
if self.current_snr != 0.0:
if self.current_snr == float('inf'):
snr_info = f" (SNR: ∞ dB)"
else:
snr_info = f" (SNR: {self.current_snr:.1f} dB)"
logger.info(f" 解码进度: {progress}% ({self.decoder_status['counter']}/19){snr_info}")
# 检查脉冲是否在有效范围内
cycle = self.bpc_status['cycle']
pulse = self.bpc_status['pulse']
# 首先检查周期时间是否有效
if cycle < self.CYCLE_MIN:
logger.info(f" 忽略短周期噪声: cycle={cycle}ms < {self.CYCLE_MIN}ms")
self.stats['short_cycles_ignored'] += 1
return # 直接忽略此次检测,不进行任何解码操作
# 帧同步失锁检查 - 注意:pulse现在是数据值(0-3),不是毫秒数
if pulse < 0 or pulse > 3:
logger.info(f" 帧同步失锁: cycle={cycle}ms, pulse={pulse} (数据值)")
logger.info(f" 原因: 数据值超出范围 (0-3)")
# 如果这是刚检测到定位符后的第一个脉冲,且pulse超出范围
# 很可能这个脉冲就是定位符脉冲本身,不应该解码
if self.decoder_status['counter'] == 0:
logger.info(f" 跳过定位符脉冲,等待数据脉冲")
return # 不重置状态,等待下一个脉冲
# 否则确实是帧同步失锁
self.decoder_status['counter'] = 0
self.decoder_status['status'] = False
self.stats['decode_errors'] += 1
return
# 注意:现在pulse表示静音间隔时长,数据值在process_detected_silence中已经计算好了
data_value = pulse # 这里pulse实际上已经是解码后的数据值(0-3)
logger.info(f" 静音间隔解码: {pulse} -> 数据值: {data_value}")
# 将数据推送到数组
if self.decoder_status['counter'] < 19:
self.decoder_status['data'][self.decoder_status['counter']] = data_value
self.decoder_status['counter'] += 1
self.stats['valid_pulses'] += 1
logger.info(f" 存储数据到位置 {self.decoder_status['counter']-1}: {data_value}")
elif self.decoder_status['counter'] == 19:
# 已经收集了19个数据,触发解码
logger.info(f" 已收集19个数据,开始解码帧")
# 记录解码前的状态,以便判断解码是否成功
decoder_status_before = self.decoder_status['status']
self.parse_bpc_frame()
# 检查解码器是否被重置(解码失败的情况)
if not self.decoder_status['status'] or self.decoder_status['status'] != decoder_status_before:
# 解码器已被重置,说明解码失败,忽略当前数据,等待新的定位符
logger.info(f" 解码失败导致重置,忽略当前数据: {data_value},等待新的定位符")
return
# 解码成功,将当前数据作为下一帧的第一个数据
logger.info(f" 解码成功,将当前数据作为下一帧第一个数据: {data_value}")
self.decoder_status['counter'] = 1 # 重置为1,因为已经存储了一个数据
self.decoder_status['data'][0] = data_value # 存储为下一帧的第一个数据
self.stats['valid_pulses'] += 1
self.stats['frames_decoded'] += 1
logger.info(" 本次解码完成,下一帧解码已开始...")
logger.info(f" 已解码 {self.stats['frames_decoded']} 帧")
return # 提前返回,避免重复处理
# 检查解码是否完成(这个分支现在应该不会被执行)
if self.decoder_status['done']:
# 重置计数器准备下一帧,但保持解码器状态为激活
self.decoder_status['counter'] = 0
self.decoder_status['done'] = False
# 注意:不重置 self.decoder_status['status'],保持为True以继续解码下一帧
self.stats['frames_decoded'] += 1
logger.info(" 本次解码完成,继续解码下一帧...")
logger.info(f" 已解码 {self.stats['frames_decoded']} 帧")
def parse_bpc_frame(self):
"""解析BPC帧数据 - 对应源码中的GetDate函数"""
data = self.decoder_status['data']
logger.info(f"\n=== BPC帧解码 #{self.stats['frames_decoded'] + 1} ===")
logger.info(f"原始数据: {data[:19]}")
# data = list(map(lambda x: x + 1, data))
# 根据源码解析各个字段
try:
# P1标记(位置0)- 用于计算秒数
second_marker = data[0]
# 小时(位置2-3,四进制)
hour_1 = data[2] if len(data) > 2 else 0
hour_2 = data[3] if len(data) > 3 else 0
# 分钟(位置4-6,四进制)
minute_1 = data[4] if len(data) > 4 else 0
minute_2 = data[5] if len(data) > 5 else 0
minute_3 = data[6] if len(data) > 6 else 0
# 星期(位置7-8,四进制)
week_1 = data[7] if len(data) > 7 else 0
week_2 = data[8] if len(data) > 8 else 0
# 上午/下午标记与P3校验位复用(位置9)
p3_forenoon = data[9] if len(data) > 9 else 0
# 日期(位置10-12,四进制)
day_1 = data[10] if len(data) > 10 else 0
day_2 = data[11] if len(data) > 11 else 0
day_3 = data[12] if len(data) > 12 else 0
# 月份(位置13-14,四进制)
month_1 = data[13] if len(data) > 13 else 0
month_2 = data[14] if len(data) > 14 else 0
# 年份(位置15-17,四进制)- 位置18是P4校验位与年最高位复用
year_1 = data[15] if len(data) > 15 else 0
year_2 = data[16] if len(data) > 16 else 0
year_3 = data[17] if len(data) > 17 else 0
p4_year_high = data[18] if len(data) > 18 else 0
# 解析秒数(基于P1标记)
self.parse_seconds(second_marker)
# 解析小时(四进制转十进制)
self.date_info['hour'] = self.quaternary_to_decimal(hour_1 * 10 + hour_2)
# 解析分钟(四进制转十进制)
self.date_info['minute'] = self.quaternary_to_decimal(
minute_1 * 100 + minute_2 * 10 + minute_3
)
# 解析星期
week_value = self.quaternary_to_decimal(week_1 * 10 + week_2)
self.date_info['week'] = self.parse_week(week_value)
# 解析P3复用:上午/下午标记与校验位
# P3: 0和1表示午前,2和3表示午后
# 同时P3也是前半部分的奇偶校验位
if p3_forenoon == 0 or p3_forenoon == 1:
self.date_info['forenoon'] = True
else:
self.date_info['forenoon'] = False
self.date_info['hour'] += 12 # 下午加12小时
# 解析日期
self.date_info['day'] = self.quaternary_to_decimal(
day_1 * 100 + day_2 * 10 + day_3
)
# 解析月份
self.date_info['month'] = self.quaternary_to_decimal(month_1 * 10 + month_2)
# 解析P4复用:年份低3位 + 年份最高位 + 校验位
year_low_3_bits = self.quaternary_to_decimal(year_1 * 100 + year_2 * 10 + year_3)
# P4的年份最高位:0和1表示最高位为0,2和3表示最高位为1
if p4_year_high == 0 or p4_year_high == 1:
year_high_bit = 0
else:
year_high_bit = 1
# 组合完整年份(最高位 + 低3位)
self.date_info['year'] = year_high_bit * 128 + year_low_3_bits
# 处理进位
if self.date_info['carry']:
self.date_info['minute'] += 1
if self.date_info['minute'] == 60:
self.date_info['hour'] += 1
self.date_info['minute'] = 0
# 输出解码结果
self.print_decoded_time()
# TODO: 添加校验位验证
# checksum_valid = self.verify_checksum(data)
# if not checksum_valid:
# logger.warning("警告: 校验位验证失败,数据可能有误")
except Exception as e:
logger.error(f"解码错误: {e}")
self.stats['decode_errors'] += 1
def parse_seconds(self, second_marker):
"""解析秒数 - 根据专利文档P1标志含义"""
if second_marker == 0:
# P1=0表示帧起始于第1秒
self.date_info['carry'] = False
self.date_info['second'] = 1
elif second_marker == 1:
# P1=1表示帧起始于第21秒
self.date_info['carry'] = False
self.date_info['second'] = 21
elif second_marker == 2:
# P1=2表示帧起始于第41秒
self.date_info['carry'] = False
self.date_info['second'] = 41
else:
# 默认情况
self.date_info['carry'] = False
self.date_info['second'] = 0
def parse_week(self, week_value):
"""解析星期"""
week_names = {
1: "Mon", 2: "Tue", 3: "Wed", 4: "Thu",
5: "Fri", 6: "Sat", 0: "Sun", 7: "Sun"
}
return week_names.get(week_value, "Sun")
def quaternary_to_decimal(self, n):
"""四进制转十进制 - 对应源码中的Quaternary2Decimal"""
dec = 0
i = 0
while n != 0:
rem = n % 10
n //= 10
dec += rem * (4 ** i)
i += 1
return dec
def print_decoded_time(self):
"""输出解码的时间"""
year = 2000 + self.date_info['year'] # 年份加2000
month = self.date_info['month']
day = self.date_info['day']
hour = self.date_info['hour']
minute = self.date_info['minute']
second = self.date_info['second']
week = self.date_info['week']
# 基本数据有效性检查
if not self.validate_decoded_time(year, month, day, hour, minute, second):
logger.info(f"解码错误: {year:04d}-{month:02d}-{day:02d} {week} {hour:02d}:{minute:02d}:{second:02d} HKT")
logger.error("解码数据无效,重置解码器状态")
self.reset_decoder_state()
return
logger.info(f"解码帧开始时间: {year:04d}-{month:02d}-{day:02d} {week} {hour:02d}:{minute:02d}:{second:02d} HKT")
logger.info(f"上午/下午: {'上午' if self.date_info['forenoon'] else '下午'}")
logger.info(f"是否进位: {'是' if self.date_info['carry'] else '否'}")
# 计算与当前系统时间的差异
try:
import datetime
decoded_time = datetime.datetime(year, month, day, hour, minute, second)
current_time = datetime.datetime.now()
time_diff = decoded_time - current_time
logger.info(f"当前系统时间: {current_time.strftime('%Y-%m-%d %H:%M:%S')}")
logger.info(f"时间差异: {time_diff.total_seconds():.1f}秒(标准值:-20s)")
# 检查时间差异是否合理(不超过1天)
if abs(time_diff.total_seconds()) > 86400*7: # 24小时 = 86400秒
logger.warning(f"时间差异过大: {time_diff.total_seconds():.1f}秒,可能解码错误")
logger.warning("重置解码器状态,等待重新同步")
self.reset_decoder_state()
except Exception as e:
logger.error(f"时间差异计算失败: {e}")
logger.error("解码数据可能错位或无效,重置解码器状态")
self.reset_decoder_state()
def format_time(self, timestamp):
"""格式化时间戳"""
dt = datetime.datetime.fromtimestamp(timestamp)
return dt.strftime("%H:%M:%S.%f")[:-3] # 显示到毫秒
def print_statistics(self):
"""打印统计信息"""
logger.info(f"\n=== BPC解码统计 ===")
logger.info(f"总脉冲数: {self.stats['total_pulses']}")
logger.info(f"有效脉冲: {self.stats['valid_pulses']}")
logger.info(f"无效脉冲: {self.stats['invalid_pulses']}")
logger.info(f"同步检测: {self.stats['sync_detected']}")
logger.info(f"解码帧数: {self.stats['frames_decoded']}")
logger.info(f"解码错误: {self.stats['decode_errors']}")
logger.info(f"信号滤波与噪声抑制:")
logger.info(f" 忽略的短暂无声间隙: {self.stats['smooth_gaps_ignored']}")
logger.info(f" 忽略的信号突刺: {self.stats['smooth_spikes_ignored']}")
logger.info(f" 忽略的短周期噪声: {self.stats['short_cycles_ignored']}")
logger.info(f"信号滤波和状态机:")
logger.info(f" 当前静音状态: {self.silence_state}")
logger.info(f" 滤波器窗口: 滑动平均{self.MOVING_AVERAGE_WINDOW}点, 中值滤波{self.MEDIAN_FILTER_WINDOW}点")
logger.info(f" 最小静音间隔: {self.MIN_PULSE_DURATION}ms")
logger.info(f" 最小载波恢复时间: {self.MIN_SILENCE_GAP}ms")
logger.info(f" 防抖时间: {self.SIGNAL_DEBOUNCE_TIME}ms")
logger.info(f" 最小周期时间阈值: {self.CYCLE_MIN}ms")
# 新增:自适应阈值统计
if self.adaptive_threshold_enabled:
logger.info(f"自适应阈值统计:")
logger.info(f" 当前阈值: {self.threshold:.4f}")
logger.info(f" 初始阈值: {self.initial_threshold:.4f}")
logger.info(f" 阈值更新次数: {self.threshold_stats['updates']}")
logger.info(f" 阈值范围: {self.threshold_stats['min_threshold']:.4f} - {self.threshold_stats['max_threshold']:.4f}")
logger.info(f" 当前信号范围: {self.threshold_stats['current_min_rms']:.4f} - {self.threshold_stats['current_max_rms']:.4f}")
logger.info(f" 更新间隔: {self.threshold_update_interval}秒,历史时长: {self.threshold_history_duration}秒")
# 新增:信噪比统计
if self.current_snr != 0.0:
logger.info(f"信号质量统计:")
if self.current_snr == float('inf'):
logger.info(f" 信噪比(SNR): ∞ dB (完美信号)")
else:
logger.info(f" 信噪比(SNR): {self.current_snr:.1f} dB")
if self.current_snr >= 7:
logger.info(f" 信号质量: 优秀")
elif self.current_snr >= 6:
logger.info(f" 信号质量: 良好")
elif self.current_snr >= 5:
logger.info(f" 信号质量: 一般")
else:
logger.info(f" 信号质量: 较差")
logger.info(f" 信号样本数: {len(self.signal_rms_history)}")
logger.info(f" 噪声样本数: {len(self.noise_rms_history)}")
if self.stats['total_pulses'] > 0:
accuracy = (self.stats['valid_pulses'] / self.stats['total_pulses']) * 100
logger.info(f"识别准确率: {accuracy:.1f}%")
total_filtered = (self.stats['smooth_gaps_ignored'] +
self.stats['smooth_spikes_ignored'] +
self.stats['short_cycles_ignored'])
if total_filtered > 0:
logger.info(f"噪声过滤有效性: 过滤了 {total_filtered} 个干扰事件")
logger.info(f" 短暂间隙: {self.stats['smooth_gaps_ignored']}")
logger.info(f" 信号突刺: {self.stats['smooth_spikes_ignored']}")
logger.info(f" 短周期噪声: {self.stats['short_cycles_ignored']}")
def analyze_frequency_spectrum(self, audio_data):
"""分析音频频谱,找到最强频率"""
# 将新的音频数据添加到缓冲区
self.audio_buffer = np.concatenate([self.audio_buffer, audio_data])
# 限制缓冲区大小,防止内存无限增长(保留最新的3倍FFT大小的数据)
max_buffer_size = self.fft_size * 3
if len(self.audio_buffer) > max_buffer_size:
self.audio_buffer = self.audio_buffer[-max_buffer_size:]
# 如果缓冲区数据不足,返回None
if len(self.audio_buffer) < self.fft_size:
return None
# 使用最新的fft_size长度的数据进行分析
analysis_data = self.audio_buffer[-self.fft_size:]
# 使用汉宁窗减少频谱泄漏
windowed_data = analysis_data * np.hanning(self.fft_size)
# 执行FFT
fft_result = np.fft.fft(windowed_data)
fft_magnitude = np.abs(fft_result[:self.fft_size//2])
# 计算频率轴
freqs = np.fft.fftfreq(self.fft_size, 1/self.sample_rate)[:self.fft_size//2]
# 在搜索范围内找到最强频率
min_freq, max_freq = self.frequency_search_range
freq_mask = (freqs >= min_freq) & (freqs <= max_freq)
if not np.any(freq_mask):
return None
masked_magnitude = fft_magnitude[freq_mask]
masked_freqs = freqs[freq_mask]
# 找到最大幅度对应的频率
max_idx = np.argmax(masked_magnitude)
strongest_freq = masked_freqs[max_idx]
max_amplitude = masked_magnitude[max_idx]
return strongest_freq, max_amplitude
def update_frequency_tracking(self, audio_data):
"""更新频率追踪"""
current_time = time.time()
# 每0.5秒更新一次频率分析
if current_time - self._frequency_update_time < 0.5:
return
self._frequency_update_time = current_time
# 分析频谱
freq_result = self.analyze_frequency_spectrum(audio_data)
if freq_result is None:
return
strongest_freq, amplitude = freq_result
# 添加到历史记录
self.frequency_history.append((strongest_freq, amplitude))
# 如果还没有锁定频率,尝试锁定
if self.target_frequency is None:
if len(self.frequency_history) >= 3:
# 检查最近几次检测的频率是否稳定
recent_freqs = [f for f, a in list(self.frequency_history)[-3:]]
freq_std = np.std(recent_freqs)
if freq_std < self.frequency_bin_size: # 频率变化小于阈值
self.target_frequency = np.mean(recent_freqs)
self.filter_center_freq = self.target_frequency
self.filter_enabled = True
self.frequency_lock_count = self.frequency_lock_threshold
logger.info(f"[频率追踪] 锁定目标频率: {self.target_frequency:.1f} Hz")
logger.info(f"[频率追踪] 启用带通滤波,中心频率: {self.filter_center_freq:.1f} Hz")
else:
# 已锁定频率,检查是否需要更新
freq_diff = abs(strongest_freq - self.target_frequency)
if freq_diff < self.frequency_bin_size:
# 频率稳定,增强锁定
self.frequency_lock_count = min(self.frequency_lock_count + 1,
self.frequency_lock_threshold * 2)
else:
# 频率偏移,减弱锁定
self.frequency_lock_count -= 1
if self.frequency_lock_count <= 0:
logger.info(f"[频率追踪] 频率偏移过大,重新搜索 (当前: {strongest_freq:.1f} Hz, 目标: {self.target_frequency:.1f} Hz)")
self.target_frequency = None
self.filter_enabled = False
self.frequency_history.clear()
def apply_bandpass_filter(self, audio_data):
"""应用简单的数字带通滤波器"""
if not self.filter_enabled or self.filter_center_freq is None:
return audio_data
original_length = len(audio_data)
# 对于短数据,使用零填充到FFT大小,或者使用较小的FFT
if original_length < self.fft_size:
# 使用零填充
padded_data = np.zeros(self.fft_size)
padded_data[:original_length] = audio_data
fft_data = np.fft.fft(padded_data)
else:
# 使用原始数据
fft_data = np.fft.fft(audio_data[:self.fft_size])
freqs = np.fft.fftfreq(self.fft_size, 1/self.sample_rate)
# 创建带通滤波器掩码
center_freq = self.filter_center_freq
bandwidth = self.filter_bandwidth
# 允许通过的频率范围
lower_bound = center_freq - bandwidth/2
upper_bound = center_freq + bandwidth/2
# 创建滤波器掩码(保留目标频率附近的信号)
filter_mask = np.zeros_like(fft_data, dtype=bool)
pos_mask = (freqs >= lower_bound) & (freqs <= upper_bound)
neg_mask = (freqs >= -upper_bound) & (freqs <= -lower_bound)
filter_mask[pos_mask] = True
filter_mask[neg_mask] = True
# 应用滤波器
filtered_fft = fft_data.copy()
filtered_fft[~filter_mask] *= 0.1 # 衰减其他频率而不是完全消除
# 逆FFT
filtered_data = np.fft.ifft(filtered_fft).real
# 返回原始长度的数据
return filtered_data[:original_length]
def apply_signal_filtering(self, rms):
"""应用信号滤波:滑动平均 + 中值滤波"""
# 1. 添加到缓冲区
self.rms_buffer.append(rms)
self.rms_median_buffer.append(rms)
# 2. 滑动平均滤波
if len(self.rms_buffer) >= 3:
moving_avg = np.mean(list(self.rms_buffer))
else:
moving_avg = rms
# 3. 中值滤波(去除极端毛刺)
if len(self.rms_median_buffer) >= 3:
median_filtered = np.median(list(self.rms_median_buffer))
else:
median_filtered = moving_avg
return median_filtered
def update_adaptive_threshold(self, filtered_rms, current_time):
"""更新自适应阈值 - 基于最近3秒内信号强度的统计"""
# 添加当前RMS到历史记录
self.rms_for_threshold.append(filtered_rms)
# 检查是否需要更新阈值
if current_time - self.last_threshold_update_time >= self.threshold_update_interval:
# 确保有足够的数据样本(至少1秒的数据)
if len(self.rms_for_threshold) >= self.sample_rate / 1024: # 约1秒的数据
# 计算统计量
rms_values = list(self.rms_for_threshold)
min_rms = np.min(rms_values)
max_rms = np.max(rms_values)
mean_rms = np.mean(rms_values)
std_rms = np.std(rms_values)
# 更新统计信息
self.threshold_stats['current_min_rms'] = min_rms
self.threshold_stats['current_max_rms'] = max_rms
# 如果信号强度变化足够大,则更新阈值
if max_rms > min_rms * 1.5: # 最大值至少是最小值的1.5倍
# 计算新阈值:取最大值和最小值的中间值
# 对于BPC信号,载波和静音应该有明确的区分,使用中间值是最佳选择
new_threshold = min_rms + (max_rms - min_rms) * self.THRESHOLD_POSITION
# 限制阈值变化范围,避免过度调整
min_allowed = self.initial_threshold * 0.1 # 不低于初始阈值的10%
max_allowed = self.initial_threshold * 10.0 # 不高于初始阈值的10倍
new_threshold = np.clip(new_threshold, min_allowed, max_allowed)
# 如果新阈值与当前阈值差异足够大,则更新
if abs(new_threshold - self.threshold) > self.threshold * 0.1: # 变化超过10%
old_threshold = self.threshold
self.threshold = new_threshold
# 更新统计
self.threshold_stats['updates'] += 1
self.threshold_stats['min_threshold'] = min(self.threshold_stats['min_threshold'], new_threshold)
self.threshold_stats['max_threshold'] = max(self.threshold_stats['max_threshold'], new_threshold)
logger.info(f"[自适应阈值] 阈值更新: {old_threshold:.4f} -> {new_threshold:.4f}")
logger.info(f"[自适应阈值] 信号范围: {min_rms:.4f} - {max_rms:.4f} (均值: {mean_rms:.4f}, 标准差: {std_rms:.4f})")
else:
logger.debug(f"[自适应阈值] 阈值变化不足,保持当前值: {self.threshold:.4f}")
else:
logger.debug(f"[自适应阈值] 信号变化不够大,保持当前阈值: {self.threshold:.4f} (范围: {min_rms:.4f} - {max_rms:.4f})")
self.last_threshold_update_time = current_time
def update_snr_calculation(self, filtered_rms, current_time):
"""更新信噪比计算"""
# 根据当前阈值判断是信号还是噪声
if filtered_rms > self.threshold:
# 载波信号
self.signal_rms_history.append(filtered_rms)
else:
# 静音/噪声
self.noise_rms_history.append(filtered_rms)
# 定期更新信噪比计算
if current_time - self.last_snr_update_time >= self.snr_update_interval:
if len(self.signal_rms_history) >= 10 and len(self.noise_rms_history) >= 10:
# 计算信号和噪声的均方根功率
signal_power = np.mean([rms**2 for rms in self.signal_rms_history])
noise_power = np.mean([rms**2 for rms in self.noise_rms_history])
# 避免除以零
if noise_power > 0:
# 计算信噪比 (dB)
snr_db = 10 * np.log10(signal_power / noise_power)
self.current_snr = snr_db
else:
self.current_snr = float('inf')
self.last_snr_update_time = current_time
def bpc_silence_state_machine(self, filtered_rms, current_time):
"""BPC静音间隔检测状态机 - 检测载波中断的静音时间"""
current_time_ms = int(current_time * 1000)
signal_high = filtered_rms > self.threshold # 有载波信号
# 防抖动处理
if signal_high and not self.confirmed_state:
if self.debounce_high_start == 0:
self.debounce_high_start = current_time_ms
elif current_time_ms - self.debounce_high_start >= self.SIGNAL_DEBOUNCE_TIME:
self.confirmed_state = True # 确认有载波
self.debounce_low_start = 0
elif not signal_high and self.confirmed_state:
if self.debounce_low_start == 0:
self.debounce_low_start = current_time_ms
elif current_time_ms - self.debounce_low_start >= self.SIGNAL_DEBOUNCE_TIME:
self.confirmed_state = False # 确认静音
self.debounce_high_start = 0
# BPC静音间隔状态机
if self.silence_state == 'carrier':
# 正常载波状态
if not self.confirmed_state:
# 载波中断,进入静音
self.silence_state = 'silence_start'
self.silence_start_time = current_time_ms
logger.debug(f"[BPC静音] 载波中断,静音开始 at {current_time_ms}ms")
elif self.silence_state == 'silence_start':
if not self.confirmed_state:
# 检查静音是否持续足够长时间
silence_duration = current_time_ms - self.silence_start_time
if silence_duration >= self.MIN_PULSE_DURATION: # 最小静音间隔
self.silence_state = 'in_silence'
logger.debug(f"[BPC静音] 确认进入静音状态,持续时间: {silence_duration}ms")
else:
# 载波恢复,检查静音是否足够长
silence_duration = current_time_ms - self.silence_start_time
if silence_duration >= self.MIN_PULSE_DURATION:
# 静音时间足够长,应该进入 silence_end 状态
self.silence_state = 'silence_end'
self.silence_end_time = current_time_ms
logger.debug(f"[BPC静音] 载波恢复,静音时长: {silence_duration}ms >= {self.MIN_PULSE_DURATION}ms,进入结束阶段")
else:
# 静音太短,回到载波状态
self.silence_state = 'carrier'