Spaces:
Sleeping
Sleeping
File size: 5,839 Bytes
3e3a5f1 | 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 165 166 167 168 169 170 171 172 173 | import os
import logging
import numpy as np
import scipy.io.wavfile as wavfile
SAMPLE_RATE = 44100
BIT_DURATION = 0.05
FREQ_0 = 18000
FREQ_1 = 19500
# Band-energy: sum magnitudes around target bin for robust detection
BAND_WIDTH = 3
# The known preamble from conversion.py — used for timing alignment
PREAMBLE = "001110010011100100110111001100110111001101110100011000010111001001110100"
logging.basicConfig(level=logging.INFO)
def _band_energy(mag, center_bin, width=BAND_WIDTH):
"""Sum magnitudes in a band around the center bin."""
lo = max(0, center_bin - width)
hi = min(len(mag), center_bin + width + 1)
return np.sum(mag[lo:hi])
def _decode_bits(audio, samples_per_bit, offset=0):
"""Decode audio starting at sample 'offset' into a binary string using FSK."""
pad_factor = 4
padded_len = samples_per_bit * pad_factor
output = ""
for i in range(offset, len(audio) - samples_per_bit + 1, samples_per_bit):
chunk = audio[i:i + samples_per_bit]
windowed = chunk * np.hanning(len(chunk))
fft_result = np.fft.fft(windowed, n=padded_len)
mag = np.abs(fft_result)
bin_0 = int(FREQ_0 * (padded_len / SAMPLE_RATE))
bin_1 = int(FREQ_1 * (padded_len / SAMPLE_RATE))
energy_0 = _band_energy(mag, bin_0)
energy_1 = _band_energy(mag, bin_1)
output += '0' if energy_0 > energy_1 else '1'
return output
def _preamble_score(bits, preamble=PREAMBLE):
"""How well do the first N bits match the known preamble?"""
if len(bits) < len(preamble):
return 0.0
matches = sum(1 for a, b in zip(bits, preamble) if a == b)
return matches / len(preamble)
def generateAudio(binary_content, filename="audio"):
"""Encode binary string to FSK audio WAV file. Returns the file path."""
t = np.arange(0, BIT_DURATION, 1 / SAMPLE_RATE)
logging.info("Generating audio")
arr_0 = np.sin(2 * np.pi * FREQ_0 * t)
arr_1 = np.sin(2 * np.pi * FREQ_1 * t)
window = np.hanning(len(t))
arr_0 = arr_0 * window
arr_1 = arr_1 * window
audio = []
for bit in binary_content:
audio.append(arr_0 if bit == '0' else arr_1)
audio = np.concatenate(audio)
audio = audio * 0.5
os.makedirs('audio', exist_ok=True)
wav_path = f'audio/{filename}.wav'
wavfile.write(wav_path, SAMPLE_RATE, audio.astype(np.float32))
logging.info("Audio file saved as %s", wav_path)
return wav_path
def readAudioFromFile(wav_path):
"""
Decode a WAV file back into a binary string.
Uses an alignment sweep to find the optimal bit boundary offset.
"""
sample_rate, audio = wavfile.read(wav_path)
logging.info("Reading audio from %s (sample_rate=%d, samples=%d)",
wav_path, sample_rate, len(audio))
# Convert to mono float
if audio.ndim > 1:
audio = audio[:, 0]
if audio.dtype == np.int16:
audio = audio.astype(np.float32) / 32768.0
elif audio.dtype == np.int32:
audio = audio.astype(np.float32) / 2147483648.0
# Resample if needed
if sample_rate != SAMPLE_RATE:
logging.warning("Sample rate mismatch: got %d, expected %d. Resampling.",
sample_rate, SAMPLE_RATE)
from scipy.signal import resample
num_samples = int(len(audio) * SAMPLE_RATE / sample_rate)
audio = resample(audio, num_samples)
samples_per_bit = int(SAMPLE_RATE * BIT_DURATION)
# Step 1: Find approximate signal start
audio = _hunter(audio, samples_per_bit)
logging.info("Decoding %d samples (%d potential bits)",
len(audio), len(audio) // samples_per_bit)
# Step 2: Alignment sweep — try different sub-bit offsets
# and pick the one that best matches the known preamble
best_offset = 0
best_score = 0.0
best_bits = ""
step = max(1, samples_per_bit // 20) # ~5% of a bit duration per step
for offset in range(0, samples_per_bit, step):
bits = _decode_bits(audio, samples_per_bit, offset)
score = _preamble_score(bits)
if score > best_score:
best_score = score
best_offset = offset
best_bits = bits
logging.info("Timing lock: offset=%d samples, preamble match=%.2f%% at preview index=%d",
best_offset, best_score * 100,
best_bits.find(PREAMBLE[:10]) if PREAMBLE[:10] in best_bits else -1)
if best_score < 0.50:
logging.warning("Very low preamble confidence (%.1f%%). Signal may be too noisy.",
best_score * 100)
return best_bits
def _hunter(audio, samples_per_bit):
"""Detect where the FSK signal begins in a recording."""
window_size = min(2000, len(audio) // 2)
if len(audio) < window_size:
return audio
# Baseline noise from the start
first_chunk = audio[0:window_size]
fft_result = np.fft.fft(first_chunk)
mag = np.abs(fft_result)
bin_0 = int(FREQ_0 * (len(first_chunk) / SAMPLE_RATE))
bin_1 = int(FREQ_1 * (len(first_chunk) / SAMPLE_RATE))
baseline_energy = max(_band_energy(mag, bin_0), _band_energy(mag, bin_1))
threshold = baseline_energy * 3
# Scan for signal onset
signal_start = 0
for i in range(0, len(audio) - window_size, 50):
w = audio[i:i + window_size]
fft_r = np.fft.fft(w)
m = np.abs(fft_r)
b0 = int(FREQ_0 * (len(w) / SAMPLE_RATE))
b1 = int(FREQ_1 * (len(w) / SAMPLE_RATE))
if _band_energy(m, b0) > threshold or _band_energy(m, b1) > threshold:
signal_start = i
break
# Back up by one full bit so we don't clip any preamble
safety = samples_per_bit
final = max(0, signal_start - safety)
logging.info("Signal at sample %d, trimming from %d", signal_start, final)
return audio[final:] |