Module: PWN::SDR::Decoder::DSP
- Defined in:
- lib/pwn/sdr/decoder/dsp.rb
Overview
Pure-Ruby DSP primitives shared by every PWN::SDR::Decoder::* module.
Nothing in here shells out. Everything operates on plain Ruby Arrays
of Float samples (normalised -1.0..1.0), so decoders can consume the
48 kHz s16le mono audio that PWN::SDR::GQRX streams over UDP without
any external sox / multimon-ng / minimodem / etc dependency.
These are intentionally simple, readable, allocation-heavy reference implementations — good enough for ≤48 kHz audio-rate work on a modern CPU. For MHz-rate raw I/Q you would want SIMD/native code, which is out of scope for a pure-Ruby decoder namespace.
Constant Summary collapse
- TWO_PI =
Math::PI * 2
- BAUDOT_LTRS =
ITA2 / Baudot 5-bit → ASCII (LTRS + FIGS shift tables). Index = code.
[ "\0", 'E', "\n", 'A', ' ', 'S', 'I', 'U', "\r", 'D', 'R', 'J', 'N', 'F', 'C', 'K', 'T', 'Z', 'L', 'W', 'H', 'Y', 'P', 'Q', 'O', 'B', 'G', nil, 'M', 'X', 'V', nil ].freeze
- BAUDOT_FIGS =
[ "\0", '3', "\n", '-', ' ', "'", '8', '7', "\r", '$', '4', "\a", ',', '!', ':', '(', '5', '+', ')', '2', '#', '6', '0', '1', '9', '?', '&', nil, '.', '/', ';', nil ].freeze
- MORSE_TABLE =
International Morse Code (dit='.', dah='-') → ASCII.
{ '.-' => 'A', '-...' => 'B', '-.-.' => 'C', '-..' => 'D', '.' => 'E', '..-.' => 'F', '--.' => 'G', '....' => 'H', '..' => 'I', '.---' => 'J', '-.-' => 'K', '.-..' => 'L', '--' => 'M', '-.' => 'N', '---' => 'O', '.--.' => 'P', '--.-' => 'Q', '.-.' => 'R', '...' => 'S', '-' => 'T', '..-' => 'U', '...-' => 'V', '.--' => 'W', '-..-' => 'X', '-.--' => 'Y', '--..' => 'Z', '-----' => '0', '.----' => '1', '..---' => '2', '...--' => '3', '....-' => '4', '.....' => '5', '-....' => '6', '--...' => '7', '---..' => '8', '----.' => '9', '.-.-.-' => '.', '--..--' => ',', '..--..' => '?', '-..-.' => '/', '-....-' => '-', '-.--.' => '(', '-.--.-' => ')', '.-...' => '&', '---...' => ':', '-...-' => '=', '.-.-.' => '+', '.--.-.' => '@' }.freeze
Class Method Summary collapse
-
.authors ⇒ Object
- Author(s)
0day Inc.
-
.baudot_decode(opts = {}) ⇒ Object
- Supported Method Parameters
txt = PWN::SDR::Decoder::DSP.baudot_decode(bits: Array<0|1>) 5-bit ITA2 with LTRS(31)/FIGS(27) shift, LSB-first per character.
-
.bch_31_21_syndrome(opts = {}) ⇒ Object
BCH(31,21) syndrome — generator poly 0b11101101001 (0x769).
-
.bits_to_int(opts = {}) ⇒ Object
- Supported Method Parameters
int = PWN::SDR::Decoder::DSP.bits_to_int(bits: [1,0,1,...]).
-
.dc_block(opts = {}) ⇒ Object
- Supported Method Parameters
y = PWN::SDR::Decoder::DSP.dc_block( samples: 'required - Array
', alpha: 'optional - pole (default 0.995)' ).
-
.envelope(opts = {}) ⇒ Object
- Supported Method Parameters
env = PWN::SDR::Decoder::DSP.envelope( samples: 'required - Array
', window: 'optional - moving-average window in samples (default 32)' ).
-
.envelope_signed(opts = {}) ⇒ Object
Signed moving average (like envelope but keeps sign).
-
.even_parity_ok?(opts = {}) ⇒ Boolean
- Supported Method Parameters
ok = PWN::SDR::Decoder::DSP.even_parity_ok?(word: Integer, width: 32).
-
.find_sync(opts = {}) ⇒ Object
- Supported Method Parameters
idx = PWN::SDR::Decoder::DSP.find_sync( bits: 'required - Array<0|1>', pattern: 'required - Array<0|1> or Integer (MSB-first)', width: 'optional - bit-width when pattern is Integer', max_err: 'optional - allowed bit errors (default 0)', from: 'optional - start index (default 0)' ).
-
.fsk_slice(opts = {}) ⇒ Object
- Supported Method Parameters
bits = PWN::SDR::Decoder::DSP.fsk_slice( samples: 'required - Array
', rate: 'required - sample rate (Hz)', baud: 'required - symbol rate', mark_hz: 'required - mark tone (bit=1)', space_hz:'required - space tone (bit=0)' ) Non-coherent 2-FSK: per-symbol Goertzel on mark/space, pick the winner.
-
.goertzel(opts = {}) ⇒ Object
- Supported Method Parameters
power = PWN::SDR::Decoder::DSP.goertzel( samples: 'required - Array
', rate: 'required - sample rate (Hz)', freq: 'required - target tone frequency (Hz)' ).
-
.help ⇒ Object
Display Usage for this Module.
-
.nrz_slice(opts = {}) ⇒ Object
- Supported Method Parameters
bits = PWN::SDR::Decoder::DSP.nrz_slice( samples: 'required - Array
(post-FM-discriminator baseband)', rate: 'required - sample rate (Hz)', baud: 'required - symbol rate', invert: 'optional - flip bit polarity (default false)' ) Simple mid-bit sampler with zero-crossing resync.
-
.resample(opts = {}) ⇒ Object
- Supported Method Parameters
out = PWN::SDR::Decoder::DSP.resample( samples: 'required - Array
', src_rate: 'required - input sample rate (Hz)', dst_rate: 'required - output sample rate (Hz)' ).
-
.rms_dbfs(opts = {}) ⇒ Object
- Supported Method Parameters
dbfs = PWN::SDR::Decoder::DSP.rms_dbfs(samples: Array
).
-
.unpack_s16le(opts = {}) ⇒ Object
- Supported Method Parameters
samples = PWN::SDR::Decoder::DSP.unpack_s16le( data: 'required - raw String of little-endian signed 16-bit PCM' ).
Class Method Details
.authors ⇒ Object
- Author(s)
0day Inc. support@0dayinc.com
365 366 367 |
# File 'lib/pwn/sdr/decoder/dsp.rb', line 365 public_class_method def self. "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n" end |
.baudot_decode(opts = {}) ⇒ Object
- Supported Method Parameters
txt = PWN::SDR::Decoder::DSP.baudot_decode(bits: Array<0|1>) 5-bit ITA2 with LTRS(31)/FIGS(27) shift, LSB-first per character.
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 |
# File 'lib/pwn/sdr/decoder/dsp.rb', line 330 public_class_method def self.baudot_decode(opts = {}) bits = opts[:bits] figs = false out = +'' bits.each_slice(5) do |ch| next if ch.length < 5 code = ch.each_with_index.sum { |b, i| b << i } case code when 31 then figs = false when 27 then figs = true else tbl = figs ? BAUDOT_FIGS : BAUDOT_LTRS c = tbl[code] out << c if c end end out end |
.bch_31_21_syndrome(opts = {}) ⇒ Object
BCH(31,21) syndrome — generator poly 0b11101101001 (0x769). Used by POCSAG and FLEX codewords (bits 31..1 are BCH, bit 0 parity).
- Supported Method Parameters
syn = PWN::SDR::Decoder::DSP.bch_31_21_syndrome(word: Integer)
316 317 318 319 320 321 322 323 324 |
# File 'lib/pwn/sdr/decoder/dsp.rb', line 316 public_class_method def self.bch_31_21_syndrome(opts = {}) word = (opts[:word].to_i >> 1) & 0x7FFFFFFF gen = 0x769 reg = word 30.downto(10) do |i| reg ^= (gen << (i - 10)) if (reg >> i).odd? end reg & 0x3FF end |
.bits_to_int(opts = {}) ⇒ Object
- Supported Method Parameters
int = PWN::SDR::Decoder::DSP.bits_to_int(bits: [1,0,1,...])
293 294 295 296 297 298 |
# File 'lib/pwn/sdr/decoder/dsp.rb', line 293 public_class_method def self.bits_to_int(opts = {}) bits = opts[:bits] v = 0 bits.each { |b| v = (v << 1) | (b & 1) } v end |
.dc_block(opts = {}) ⇒ Object
- Supported Method Parameters
y = PWN::SDR::Decoder::DSP.dc_block(
samples: 'required - Array
146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/pwn/sdr/decoder/dsp.rb', line 146 public_class_method def self.dc_block(opts = {}) samples = opts[:samples] alpha = (opts[:alpha] || 0.995).to_f y_prev = 0.0 x_prev = 0.0 samples.map do |x| y = x - x_prev + (alpha * y_prev) x_prev = x y_prev = y y end end |
.envelope(opts = {}) ⇒ Object
- Supported Method Parameters
env = PWN::SDR::Decoder::DSP.envelope(
samples: 'required - Array
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/pwn/sdr/decoder/dsp.rb', line 123 public_class_method def self.envelope(opts = {}) samples = opts[:samples] window = (opts[:window] || 32).to_i window = 1 if window < 1 acc = 0.0 buf = Array.new(window, 0.0) out = Array.new(samples.length) samples.each_with_index do |x, i| v = x.abs slot = i % window acc += v - buf[slot] buf[slot] = v out[i] = acc / window end out end |
.envelope_signed(opts = {}) ⇒ Object
Signed moving average (like envelope but keeps sign).
- Supported Method Parameters
y = PWN::SDR::Decoder::DSP.envelope_signed(samples:, window:)
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
# File 'lib/pwn/sdr/decoder/dsp.rb', line 202 public_class_method def self.envelope_signed(opts = {}) samples = opts[:samples] window = (opts[:window] || 8).to_i window = 1 if window < 1 acc = 0.0 buf = Array.new(window, 0.0) out = Array.new(samples.length) samples.each_with_index do |x, i| slot = i % window acc += x - buf[slot] buf[slot] = x out[i] = acc / window end out end |
.even_parity_ok?(opts = {}) ⇒ Boolean
- Supported Method Parameters
ok = PWN::SDR::Decoder::DSP.even_parity_ok?(word: Integer, width: 32)
303 304 305 306 307 308 309 |
# File 'lib/pwn/sdr/decoder/dsp.rb', line 303 public_class_method def self.even_parity_ok?(opts = {}) word = opts[:word].to_i width = (opts[:width] || 32).to_i p = 0 width.times { |i| p ^= (word >> i) & 1 } p.zero? end |
.find_sync(opts = {}) ⇒ Object
- Supported Method Parameters
idx = PWN::SDR::Decoder::DSP.find_sync( bits: 'required - Array<0|1>', pattern: 'required - Array<0|1> or Integer (MSB-first)', width: 'optional - bit-width when pattern is Integer', max_err: 'optional - allowed bit errors (default 0)', from: 'optional - start index (default 0)' )
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 |
# File 'lib/pwn/sdr/decoder/dsp.rb', line 259 public_class_method def self.find_sync(opts = {}) bits = opts[:bits] pattern = opts[:pattern] width = opts[:width] max_err = (opts[:max_err] || 0).to_i from = (opts[:from] || 0).to_i pat = if pattern.is_a?(Integer) w = width || pattern.bit_length Array.new(w) { |i| (pattern >> (w - 1 - i)) & 1 } else pattern end plen = pat.length upto = bits.length - plen i = from while i <= upto err = 0 j = 0 while j < plen err += 1 if bits[i + j] != pat[j] break if err > max_err j += 1 end return i if err <= max_err i += 1 end nil end |
.fsk_slice(opts = {}) ⇒ Object
- Supported Method Parameters
bits = PWN::SDR::Decoder::DSP.fsk_slice(
samples: 'required - Array
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 |
# File 'lib/pwn/sdr/decoder/dsp.rb', line 228 public_class_method def self.fsk_slice(opts = {}) samples = opts[:samples] rate = opts[:rate].to_f baud = opts[:baud].to_f mark_hz = opts[:mark_hz].to_f space_hz = opts[:space_hz].to_f spb = rate / baud nsym = (samples.length / spb).floor bits = Array.new(nsym) i = 0 while i < nsym a = (i * spb).floor b = ((i + 1) * spb).floor win = samples[a...b] pm = goertzel(samples: win, rate: rate, freq: mark_hz) ps = goertzel(samples: win, rate: rate, freq: space_hz) bits[i] = pm >= ps ? 1 : 0 i += 1 end bits end |
.goertzel(opts = {}) ⇒ Object
- Supported Method Parameters
power = PWN::SDR::Decoder::DSP.goertzel(
samples: 'required - Array
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/pwn/sdr/decoder/dsp.rb', line 97 public_class_method def self.goertzel(opts = {}) samples = opts[:samples] rate = opts[:rate].to_f freq = opts[:freq].to_f n = samples.length return 0.0 if n.zero? k = (0.5 + ((n * freq) / rate)).floor w = (TWO_PI * k) / n coeff = 2.0 * Math.cos(w) s1 = 0.0 s2 = 0.0 samples.each do |x| s0 = x + (coeff * s1) - s2 s2 = s1 s1 = s0 end ((s1 * s1) + (s2 * s2) - (coeff * s1 * s2)) / n end |
.help ⇒ Object
Display Usage for this Module
371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 |
# File 'lib/pwn/sdr/decoder/dsp.rb', line 371 public_class_method def self.help puts "USAGE (pure-Ruby DSP primitives — no external binaries): #{self}.unpack_s16le(data: raw_bytes) #{self}.resample(samples:, src_rate:, dst_rate:) #{self}.goertzel(samples:, rate:, freq:) #{self}.envelope(samples:, window: 32) #{self}.envelope_signed(samples:, window: 8) #{self}.dc_block(samples:, alpha: 0.995) #{self}.nrz_slice(samples:, rate:, baud:, invert: false) #{self}.fsk_slice(samples:, rate:, baud:, mark_hz:, space_hz:) #{self}.find_sync(bits:, pattern:, width:, max_err: 0, from: 0) #{self}.bits_to_int(bits:) #{self}.even_parity_ok?(word:, width: 32) #{self}.bch_31_21_syndrome(word:) #{self}.baudot_decode(bits:) #{self}.rms_dbfs(samples:) Constants: MORSE_TABLE, BAUDOT_LTRS, BAUDOT_FIGS #{self}.authors " end |
.nrz_slice(opts = {}) ⇒ Object
- Supported Method Parameters
bits = PWN::SDR::Decoder::DSP.nrz_slice(
samples: 'required - Array
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/pwn/sdr/decoder/dsp.rb', line 168 public_class_method def self.nrz_slice(opts = {}) samples = opts[:samples] rate = opts[:rate].to_f baud = opts[:baud].to_f invert = opts[:invert] spb = rate / baud return [] if spb < 2.0 || samples.empty? # DC-block then low-pass via short moving average (~1/4 symbol). lp_win = [(spb / 4.0).round, 1].max filt = envelope_signed(samples: dc_block(samples: samples), window: lp_win) bits = [] phase = spb / 2.0 prev = filt.first.to_f filt.each do |v| # zero-crossing → resync to mid-symbol phase = spb / 2.0 if (prev.negative? && v >= 0) || (prev.positive? && v.negative?) phase -= 1.0 if phase <= 0 b = v.negative? ? 0 : 1 b ^= 1 if invert bits << b phase += spb end prev = v end bits end |
.resample(opts = {}) ⇒ Object
- Supported Method Parameters
out = PWN::SDR::Decoder::DSP.resample(
samples: 'required - Array
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/pwn/sdr/decoder/dsp.rb', line 68 public_class_method def self.resample(opts = {}) samples = opts[:samples] src_rate = opts[:src_rate].to_f dst_rate = opts[:dst_rate].to_f return samples.dup if (src_rate - dst_rate).abs < 1e-6 ratio = src_rate / dst_rate out_len = (samples.length / ratio).floor out = Array.new(out_len) i = 0 while i < out_len pos = i * ratio idx = pos.floor frac = pos - idx a = samples[idx] || 0.0 b = samples[idx + 1] || a out[i] = a + ((b - a) * frac) i += 1 end out end |
.rms_dbfs(opts = {}) ⇒ Object
- Supported Method Parameters
dbfs = PWN::SDR::Decoder::DSP.rms_dbfs(samples: Array
353 354 355 356 357 358 359 360 361 |
# File 'lib/pwn/sdr/decoder/dsp.rb', line 353 public_class_method def self.rms_dbfs(opts = {}) samples = opts[:samples] return -120.0 if samples.nil? || samples.empty? ms = samples.sum { |v| v * v } / samples.length return -120.0 if ms <= 0 10.0 * Math.log10(ms) end |
.unpack_s16le(opts = {}) ⇒ Object
- Supported Method Parameters
samples = PWN::SDR::Decoder::DSP.unpack_s16le( data: 'required - raw String of little-endian signed 16-bit PCM' )
55 56 57 58 59 |
# File 'lib/pwn/sdr/decoder/dsp.rb', line 55 public_class_method def self.unpack_s16le(opts = {}) data = opts[:data].to_s norm = 1.0 / 32_768.0 data.unpack('s<*').map { |v| v * norm } end |