Передача данных через звук
Аудиомодем, возрождение технологии передачи данных через звук
В эпоху высокоскоростного интернета мы почти забыли характерный звук dial-up модемов, которые когда-то были единственным способом подключения к сети. Но эта технология не ушла в прошлое — она обрела новую жизнь в современных приложениях для передачи данных между устройствами через звук. Данная статья представляет реализацию FSK-модема. В этом примере мы создадим систему, которая позволяет передавать текстовые данные через звуковые сигналы, используя частотную модуляцию (FSK) — тот же принцип, что использовался в старых модемах.
Этот пример актуален для разработчиков, интересующихся обработкой сигналов, исследователей в области аудиокоммуникаций и для всех, кто хочет понять принципы работы старых модемов. Этот пример демонстрирует, как можно реализовать простой, но эффективный протокол передачи данных, используя только звуковые возможности устройств — технологию, которая находит применение в офлайн-передаче данных, IoT-устройствах и резервных каналах связи.
Пример ниже реализует:
-
FSK-модуляцию с 96 частотами в диапазоне 1-5.5 кГц
-
Разбиение данных на 4-битные полубайты(nibble)
-
Простую проверку целостности данных (вместо Reed-Solomon)
-
Синхросигналы в начале и конце передачи
-
Демодуляцию с использованием FFT для анализа частот
-
Сохранение в WAV-файл
Теперь перейдём к реализации, для начала вызовем вспомогательные библиотеки.
Pkg.add(["WAV", "FFTW", "LinearAlgebra"])
using WAV, FFTW, LinearAlgebra
Теперь определим структуру данных для 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-битных блоков (полубайтов) с добавлением проверки целостности, в этом примере реализована упрощенная проверка ошибок вместо полноценного Reed-Solomon, алгоритм вычисляет XOR всех полубайтов.
Как это работает на примере символа "A" (ASCII 65):
-
Байт:
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.1 кГц и длительностью символа 50 мс, после чего строка "Hello, FSK modulation! 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("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 для задач цифровой обработки сигналов и открывает перспективы для разработки аудиокоммуникационных решений в условиях ограниченных каналов связи.