Сообщество Engee

Передача данных через звук

Автор
avatar-yurevyurev
Notebook

Аудиомодем, возрождение технологии передачи данных через звук

В эпоху высокоскоростного интернета мы почти забыли характерный звук dial-up модемов, которые когда-то были единственным способом подключения к сети. Но эта технология не ушла в прошлое — она обрела новую жизнь в современных приложениях для передачи данных между устройствами через звук. Данная статья представляет реализацию FSK-модема. В этом примере мы создадим систему, которая позволяет передавать текстовые данные через звуковые сигналы, используя частотную модуляцию (FSK) — тот же принцип, что использовался в старых модемах.

Этот пример актуален для разработчиков, интересующихся обработкой сигналов, исследователей в области аудиокоммуникаций и для всех, кто хочет понять принципы работы старых модемов. Этот пример демонстрирует, как можно реализовать простой, но эффективный протокол передачи данных, используя только звуковые возможности устройств — технологию, которая находит применение в офлайн-передаче данных, IoT-устройствах и резервных каналах связи.

Пример ниже реализует:

  1. FSK-модуляцию с 96 частотами в диапазоне 1-5.5 кГц

  2. Разбиение данных на 4-битные полубайты(nibble)

  3. Простую проверку целостности данных (вместо Reed-Solomon)

  4. Синхросигналы в начале и конце передачи

  5. Демодуляцию с использованием FFT для анализа частот

  6. Сохранение в WAV-файл

Теперь перейдём к реализации, для начала вызовем вспомогательные библиотеки.

In [ ]:
Pkg.add(["WAV", "FFTW", "LinearAlgebra"])
In [ ]:
using WAV, FFTW, LinearAlgebra

Теперь определим структуру данных для 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 кГц и длительностью символа 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("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-анализ, что подчеркивает практическую ценность Engee для задач цифровой обработки сигналов и открывает перспективы для разработки аудиокоммуникационных решений в условиях ограниченных каналов связи.