-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
164 lines (135 loc) · 6.31 KB
/
main.py
File metadata and controls
164 lines (135 loc) · 6.31 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
import random
import struct
import wave
from typing import List
def generate_white_noise_clicktrain(sample_rate: int, burst_ms: float, repetition_hz: float,
duration_s: float, peak_amplitude: float = 0.8) -> List[int]:
"""生成 16-bit PCM 的白噪声爆发列(click train)。
- 采样率 sample_rate
- 每周期仅前 burst_ms 为白噪声,其余为 0(静音)
- 重复频率 repetition_hz(即 40/60 Hz)
- 总时长 duration_s
- 100% 调制深度(静音完全为 0)
返回: PCM16 整数列表(范围 [-32768, 32767])
"""
total_samples = int(round(sample_rate * duration_s))
period_samples = max(1, int(round(sample_rate / repetition_hz)))
burst_samples = max(1, int(round(sample_rate * burst_ms / 1000.0)))
pcm: List[int] = [0] * total_samples
idx = 0
max_int16 = 32767
noise_scale = int(max_int16 * peak_amplitude)
while idx < total_samples:
end_idx = min(idx + burst_samples, total_samples)
for i in range(idx, end_idx):
# 简单白噪声:均匀分布 [-1, 1)
val = (2.0 * random.random() - 1.0) * noise_scale
pcm[i] = max(-32768, min(32767, int(val)))
idx += period_samples
return pcm
def write_wav_pcm16(filepath: str, samples: List[int], sample_rate: int) -> None:
with wave.open(filepath, 'wb') as wf:
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(sample_rate)
# 小端有符号 16-bit
frames = struct.pack('<' + 'h' * len(samples), *samples)
wf.writeframes(frames)
def save_waveform_svg(svg_path: str, samples_a: List[int], samples_b: List[int], sample_rate: int,
view_ms: float = 100.0) -> None:
width = 1000
height = 400
padding = 30
mid_a = padding + (height // 2 - padding * 2) // 2
mid_b = height - padding - (height // 2 - padding * 2) // 2
view_samples = max(1, int(round(sample_rate * view_ms / 1000.0)))
def polyline_points(samples: List[int], mid_y: int) -> str:
xs = min(view_samples, len(samples))
points = []
for n in range(xs):
x = padding + n * (width - 2 * padding) / max(1, view_samples - 1)
y = mid_y - samples[n] / 32768.0 * (height // 2 - padding * 1.5)
points.append(f"{x:.2f},{y:.2f}")
return ' '.join(points)
pts_a = polyline_points(samples_a, mid_a)
pts_b = polyline_points(samples_b, mid_b)
svg = f"""
<svg xmlns='http://www.w3.org/2000/svg' width='{width}' height='{height}' viewBox='0 0 {width} {height}'>
<rect x='0' y='0' width='{width}' height='{height}' fill='white'/>
<g stroke='black' stroke-width='1' fill='none'>
<line x1='{padding}' y1='{mid_a}' x2='{width - padding}' y2='{mid_a}' stroke='#888'/>
<polyline points='{pts_a}' stroke='#1f77b4'/>
</g>
<g stroke='black' stroke-width='1' fill='none'>
<line x1='{padding}' y1='{mid_b}' x2='{width - padding}' y2='{mid_b}' stroke='#888'/>
<polyline points='{pts_b}' stroke='#ff7f0e'/>
</g>
<g font-family='sans-serif' font-size='14' fill='black'>
<text x='{padding}' y='20'>40 Hz 白噪声爆发(前 {view_ms:.0f} ms)</text>
<text x='{padding}' y='{height - 10}'>60 Hz 白噪声爆发(前 {view_ms:.0f} ms)</text>
</g>
</svg>
"""
with open(svg_path, 'w', encoding='utf-8') as f:
f.write(svg)
def analyze_click_train_wav(filepath: str, expected_fs: int = 44100) -> None:
"""读取 PCM16 单声道 WAV,检测爆发起点并估计重复频率。仅用标准库。
方法:
1) 读入样本,计算绝对值
2) 自适应阈值(max_abs 的 10%,最低 200)检测爆发“上升沿”
3) 通过最小间隔抑制同一爆发的重复触发(5 ms)
4) 计算相邻起点间隔的均值/标准差与频率
"""
with wave.open(filepath, 'rb') as wf:
n_channels = wf.getnchannels()
sampwidth = wf.getsampwidth()
fs = wf.getframerate()
n_frames = wf.getnframes()
raw = wf.readframes(n_frames)
if n_channels != 1 or sampwidth != 2:
print(f"{filepath}: 当前实现仅支持单声道16-bit PCM。检测到 channels={n_channels}, sampwidth={sampwidth}")
return
samples = list(struct.unpack('<' + 'h' * n_frames, raw))
if fs != expected_fs:
print(f"⚠️ 采样率与预期不同: {fs} Hz (期望 {expected_fs} Hz)")
abs_vals = [abs(x) for x in samples]
max_abs = max(abs_vals) if abs_vals else 0
thr = max(200, int(max_abs * 0.10))
min_sep_samples = int(expected_fs * 0.005) # 5 ms 去抖
peaks: List[int] = []
last_peak = -min_sep_samples
prev_over = False
for i, v in enumerate(abs_vals):
over = v >= thr
if over and not prev_over and (i - last_peak) >= min_sep_samples:
peaks.append(i)
last_peak = i
prev_over = over
if len(peaks) < 2:
print(f"{filepath}: 未检测到足够的爆发以估计频率。")
return
intervals = [peaks[i + 1] - peaks[i] for i in range(len(peaks) - 1)]
mean_samp = sum(intervals) / len(intervals)
mean_ms = mean_samp / fs * 1000.0
# 标准差
var = sum((x - mean_samp) ** 2 for x in intervals) / len(intervals)
std_ms = (var ** 0.5) / fs * 1000.0
freq = fs / mean_samp if mean_samp > 0 else 0.0
print(f"{filepath}:")
print(f" 检测到爆发数: {len(peaks)}")
print(f" 平均间隔: {mean_ms:.2f} ms (Std: {std_ms:.2f} ms)")
print(f" 估计频率: {freq:.2f} Hz (理论应≈40.00 Hz)")
if __name__ == '__main__':
fs = 44100
duration_seconds = 5.0
burst_ms = 1.0
sig40 = generate_white_noise_clicktrain(fs, burst_ms, 40.0, duration_seconds, peak_amplitude=0.8)
sig60 = generate_white_noise_clicktrain(fs, burst_ms, 60.0, duration_seconds, peak_amplitude=0.8)
write_wav_pcm16("assr_clicktrain_40Hz.wav", sig40, fs)
write_wav_pcm16("assr_clicktrain_60Hz.wav", sig60, fs)
print("已生成 WAV: assr_clicktrain_40Hz.wav, assr_clicktrain_60Hz.wav")
save_waveform_svg("assr_clicktrain_waveforms.svg", sig40, sig60, fs, view_ms=100.0)
print("已保存波形图: assr_clicktrain_waveforms.svg(前 100 ms)")
# 分析并“证明”频率
analyze_click_train_wav("assr_clicktrain_40Hz.wav", expected_fs=fs)
analyze_click_train_wav("assr_clicktrain_60Hz.wav", expected_fs=fs)