AnyMath 文档
Notebook

音频调制解调器:通过声音传输数据技术的复兴

在高速互联网时代,我们几乎已经忘记了拨号调制解调器那标志性的声音——它们曾是连接网络的唯一方式。但这项技术并未成为历史——它在现代应用中焕发了新生,通过声音实现设备间的数据传输。 本文介绍了一种FSK调制解调器的实现方案。在此示例中,我们将构建一个系统,利用频率调制(FSK)——即老式调制解调器所采用的相同原理——通过声音信号传输文本数据。

本示例适用于对信号处理感兴趣的开发者、音频通信领域的研究人员,以及所有希望了解老式调制解调器工作原理的人士。 本示例演示了如何仅利用设备的音频功能来实现一种简单而高效的数据传输协议——该技术可应用于离线数据传输、物联网设备以及备用通信信道。

下面的示例实现了:

  1. FSK调制,在1-5.5 kHz频段内使用96个频率

  2. 将数据划分为4位半字节(nibble)

  3. 简单的数据完整性校验(代替里德-所罗门校验)

  4. 传输开始和结束时的同步信号

  5. 解调:利用FFT进行频谱分析

  6. 保存为WAV文件

现在进入实现阶段,首先调用辅助库。

In [ ]:
# 链接库
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 - 启用/禁用错误校正

In [ ]:
struct FSKModulator
    sample_rate::Int
    bit_duration::Float64
    frequencies::Vector{Float64}
    reed_solomon::Bool
end

接下来,为 FSKModulator 结构体定义一个封装构造函数。

In [ ]:
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
Out[0]:
FSKModulator

函数encode_data 将字符串转换为由4位块(半字节)组成的序列,并添加了完整性校验, 本示例中实现了简化的错误校验,而非完整的里德-所罗门(Reed-Solomon)校验,该算法计算所有半字节的异或(XOR)值。

以下以字符“A”(ASCII 65)为例说明其工作原理:

  1. 字节:01000001 (二进制形式的 65)

  2. 高位 4 位:0100 = 4

  3. 低位 4 位:0001 = 1

  4. 结果:[4, 1]

In [ ]:
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
Out[0]:
encode_data (generic function with 1 method)

函数generate_tone 会生成指定频率和持续时间的纯正弦波音调。调制器数组 frequencies 中的每个频率都代表一个特定的 4 位符号,我们的函数会为每个符号生成相应的“声音表现形式”以供传输。

In [ ]:
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
Out[0]:
generate_tone (generic function with 1 method)

接下来,我们将上述所有函数整合为一个统一的算法。该代码块运行完成后,我们将获得一个连续的音频信号,其中不同的频率代表不同的数据,这与早期调制解调器的工作原理类似。

In [ ]:
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
Out[0]:
modulate (generic function with 1 method)

函数find_peak_frequency 用于确定音频片段中的主导频率,并将其与4位二进制值进行映射。让我们分步骤了解解调的工作原理:

  1. 将时域信号转换为频谱

  2. 找出振幅最大的频率

  3. 从已知的调制器频率集 中查找最接近的频率

  4. 返回原始的 4 位数值(0-15)

In [ ]:
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
Out[0]:
find_peak_frequency (generic function with 1 method)

接下来的函数利用上述过程对 FSK 信号进行解调——将声音转换回数据,其工作过程可分为以下几个阶段:

  1. 同步: 过滤掉初始同步信号

  2. 分段: 将信号划分为与每个符号对应的段

  3. 频谱分析: 针对每个段确定主导频率

  4. 解码: 将频率与原始的4位二进制值进行匹配

  5. 完整性校验: 核对校验和

  6. 重建: 将4位二进制块配对后重组为字节

In [ ]:
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
Out[0]:
demodulate (generic function with 1 method)

最后,我们实现的这一功能允许将调制后的信号以 WAV 格式保存。

In [ ]:
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
Out[0]:
save_to_wav (generic function with 2 methods)

现在,我们来实现一个简短的测试来验证我们的算法,并试听生成的音频。

该测试演示了 FSK 调制解调器的完整工作周期:创建一个采样率为 44.1 kHz、符号时长为 50 毫秒的调制器,随后字符串“Hello, FSK modulation! 123"通过频率调制方法转换为音频信号,保存为WAV文件,随后将其解调回文本,最后对原始消息和接收到的消息进行比对,以验证数据传输的正确性。

In [ ]:
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
Модуляция сообщения: "Hello, FSK modulation! 123"
该信号已保存为WAV文件
fsk_transmission.wav (1 of 1)
解调……
Полученное сообщение: "Hello, FSK modulation! 123"
✓ 传输成功!

结论

总而言之,本文介绍的FSK调制解调器实现方案生动地展示了经典声音数据传输技术在现代背景下的复兴。

实验成功验证了该方法的可行性:从将字符串编码为一串频率音调并保存为WAV文件,到通过FFT分析精确恢复原始消息, 这不仅凸显了AnyMath在数字信号处理任务中的实际价值,也为在通信信道受限的条件下开发音频通信解决方案开辟了前景。