音频调制解调器,通过声音的数据传输技术的复兴
在高速互联网时代,我们几乎忘记了拨号调制解调器的独特声音,这曾经是连接到网络的唯一途径。 但这项技术已经不是过去的事情—它已经在现代应用中找到了新的生命,通过声音在设备之间传输数据。 本文介绍了FSK调制解调器的实现. 在本例中,我们将创建一个系统,允许您使用频率调制(FSK)通过音频信号传输文本数据,与旧调制解调器中使用的原理相同。
此示例适用于对信号处理感兴趣的开发人员,音频通信领域的研究人员以及任何想要了解旧调制解调器如何工作的人。 此示例演示了如何仅使用设备的音频功能实现简单但有效的数据传输协议,这种技术可以在离线数据传输,物联网设备和冗余通信通道中找到应用程
下面的示例实现:
- 
FSK调制在1-5.5kHz范围内具有96个频率 
- 
将数据拆分为4位半字节(半字节) 
- 
**简单的数据完整性检查(而不是Reed-Solomon) 
- 
同步信号在传输的开始和结束 
- 
解调使用FFT进行频率分析 
- 
保存到WAV文件 
现在让我们继续实现,首先我们将调用辅助库。
# Подключение библиотек
neededLibs = ["WAV", "FFTW", "LinearAlgebra"]
for lib in neededLibs
    try
        eval(Meta.parse("using $lib"))
    catch ex
        Pkg.add(lib)
        eval(Meta.parse("using $lib"))
    end
end
现在让我们定义FSK调制器的数据结构。:
- 
sample_rate-音频信号质量(越高,传输越准确)
- 
bit_duration-每个字符听起来多长时间(影响传输速率)
- 
frequencies-用于编码16个可能值(0-15)的一组频率
- 
reed_solomon-启用/禁用纠错
struct FSKModulator
    sample_rate::Int
    bit_duration::Float64
    frequencies::Vector{Float64}
    reed_solomon::Bool
end
接下来,我们为FSKModulator结构定义一个包装构造函数。
function FSKModulator(;sample_rate=44100, bit_duration=0.1, reed_solomon=true)
    frequencies = range(1000, stop=5500, length=96)
    FSKModulator(sample_rate, bit_duration, frequencies, reed_solomon)
end
功能 encode_data 通过添加完整性检查,将字符串转换为4位块(半字节)序列。 在这个例子中,简化的错误检查被实现,而不是一个成熟的里德-所罗门,算法计算所有半字节的XOR。
它是如何工作的使用字符"A"的例子(ASCII65):
- 
字节: 01000001(65二进制)
- 
上4位: 0100= 4
- 
低4位: 0001= 1
- 
结果如何?: [4, 1]
function encode_data(modulator::FSKModulator, data::String)
    bytes = Vector{UInt8}(data)
    nibbles = UInt8[]
    for byte in bytes
        push!(nibbles, byte >> 4)   # Старшие 4 бита
        push!(nibbles, byte & 0x0F) # Младшие 4 бита
    end
    if modulator.reed_solomon
        # Здесь должна быть реализация Reed-Solomon, но для демо используем простой XOR
        checksum = reduce(⊻, nibbles)
        push!(nibbles, checksum)
    end
    nibbles
end
功能 generate_tone 产生给定频率和持续时间的纯正弦音调。 来自阵列的每个频率 **frequencies**调制器代表一个特定的4位字符,我们的功能创建每个字符的"声音实施例"进行传输。
function generate_tone(modulator::FSKModulator, frequency::Float64, duration::Float64)
    t = range(0, stop=duration, length=Int(round(modulator.sample_rate * duration)))
    0.5 .* sin.(2π * frequency .* t)
end
接下来,我们将上面描述的所有函数组合成一个算法。 根据这块代码的结果,我们将得到一个连续的音频信号,其中不同的频率代表不同的数据,类似于旧调制解调器的工作。
function modulate(modulator::FSKModulator, data::String)
    nibbles = encode_data(modulator, data)
    signal = Float64[]
    samples_per_bit = Int(round(modulator.sample_rate * modulator.bit_duration))
    sync_tone = generate_tone(modulator, 2000.0, modulator.bit_duration * 2)
    append!(signal, sync_tone)
    
    for nibble in nibbles
        freq_index = min(nibble + 1, length(modulator.frequencies))
        frequency = modulator.frequencies[freq_index]
        tone = generate_tone(modulator, frequency, modulator.bit_duration)
        append!(signal, tone)
    end
    append!(signal, sync_tone)
    signal
end
功能 find_peak_frequency 确定音频段中的主导频率并将其与4位值进行比较。 让我们逐步了解解调的工作原理。:
- 
将时间信号转换成频谱 
- 
求振幅最大的频率 
- 
我们正在寻找距离一组已知调制器最近的频率 
- 
返回原始4位值(0-15) 
function find_peak_frequency(signal_chunk::Vector{Float64}, sample_rate::Int, frequencies::Vector{Float64})
    n = length(signal_chunk)
    fft_result = fft(signal_chunk)
    fft_magnitude = abs.(fft_result[1:div(n,2)])
    freq_axis = range(0, stop=sample_rate/2, length=div(n,2))
    peak_idx = argmax(fft_magnitude)
    peak_freq = freq_axis[peak_idx]
    closest_idx = argmin(abs.(frequencies .- peak_freq))
    UInt8(closest_idx - 1)
end
下一个功能使用前一个来解调FSK信号-将声音转换回数据,其操作可分为以下阶段:
- 
**同步:**跳过初始同步信号 
- 
**分割:**将信号分割成与每个符号对应的段 
- 
**频率分析:**确定每个段的主导频率 
- 
**解码:**将频率映射到原始4位值 
- 
**完整性检查:**检查校验和 
- 
**恢复:**从4位块对中收集字节 
function demodulate(modulator::FSKModulator, signal::Vector{Float64})
    samples_per_bit = Int(round(modulator.sample_rate * modulator.bit_duration))
    nibbles = UInt8[]
    start_idx = 2 * samples_per_bit + 1
    
    for i in start_idx:samples_per_bit:(length(signal) - samples_per_bit)
        chunk_end = min(i + samples_per_bit - 1, length(signal))
        chunk = signal[i:chunk_end]
        if length(chunk) >= samples_per_bit ÷ 2
            nibble = find_peak_frequency(chunk, modulator.sample_rate, modulator.frequencies)
            push!(nibbles, nibble)
        end
    end
    if modulator.reed_solomon && length(nibbles) > 1
        received_checksum = pop!(nibbles)
        calculated_checksum = reduce(⊻, nibbles)
        if received_checksum != calculated_checksum
            @warn "Checksum mismatch! Data may be corrupted."
        end
    end
    bytes = UInt8[]
    for i in 1:2:length(nibbles)
        if i + 1 <= length(nibbles)
            byte = (nibbles[i] << 4) | nibbles[i+1]
            push!(bytes, byte)
        end
    end
    String(bytes)
end
最后,我们实现的最后一个功能允许您以WAV格式保存调制信号。
function save_to_wav(filename::String, signal::Vector{Float64}, sample_rate=44100)
    max_val = maximum(abs.(signal))
    if max_val > 0
        signal_normalized = signal ./ max_val
    else
        signal_normalized = signal
    end
    wavwrite(signal_normalized, filename, Fs=sample_rate)
end
现在让我们实现一个小测试来验证我们的算法并收听接收到的音频。
该测试演示了FSK调制解调器的整个周期:创建一个调制器,采样频率为44.1khz,符号持续时间为50毫秒,之后字符串"你好,FSK调制! 123"通过频率操纵转换成音频信号,保存到WAV文件中,然后将其解调回文本,最后进行比较。 源和接收的消息来验证数据传输的正确性。
modulator = FSKModulator(sample_rate=44100, bit_duration=0.05)
message = "Hello, FSK modulation! 123"
println("Модуляция сообщения: \"$message\"")
signal = modulate(modulator, message)
save_to_wav("fsk_transmission.wav", signal, modulator.sample_rate)
println("Сигнал сохранён в WAV-файл")
include("$(@__DIR__)/player.jl")
media_player("fsk_transmission.wav")
println("\nДемодуляция...")
received_message = demodulate(modulator, signal)
println("Полученное сообщение: \"$received_message\"")
if message == received_message
    println("✓ Передача успешна!")
else
    println("✗ Ошибка передачи!")
    println("Ожидалось: \"$message\"")
    println("Получено:  \"$received_message\"")
end
结论
总之,我们可以说,提出的FSK调制解调器的实现清楚地展示了传统数据传输技术在现代环境中通过声音的复兴。
该实验成功地证实了该方法的效率:从将字符串编码为频率音调序列并将其保存到WAV文件,到通过FFT分析准确地重建原始消息,这突出了Engee在数字信号处理任务中的实用价值,并为在有限通信信道条件下开发音频通信解决方案开辟了前景。