Module: PWN::FFI::Liquid

Extended by:
Library
Defined in:
lib/pwn/ffi/liquid.rb

Overview

Thin liquid-dsp binding.

Provides the high-level DSP building blocks that PWN::SDR::Decoder::* needs beyond pure-Ruby primitives: FM / frequency demodulation, multi-stage arbitrary resampling, and Kaiser FIR filters. Opaque liquid handles are held as FFI::Pointer and always freed in an ensure — no compile step, no shells. If libliquid is absent .available? is false and DSP falls back to pure Ruby.

Complex samples use the GCC float _Complex ABI: interleaved float (re, im) per sample, which is how liquid_float_complex is laid out under LIQUID_DEFINE_COMPLEX.

Constant Summary collapse

LIQUID_NCO =

── nco_crcf (numerically-controlled oscillator / mixer) ──────

0
LIQUID_VCO =
1

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.load_errorObject (readonly)

Returns the value of attribute load_error.



32
33
34
# File 'lib/pwn/ffi/liquid.rb', line 32

def load_error
  @load_error
end

Class Method Details

.authorsObject

Author(s)

0day Inc. support@0dayinc.com



390
391
392
# File 'lib/pwn/ffi/liquid.rb', line 390

public_class_method def self.authors
  "AUTHOR(S):\n  0day Inc. <support@0dayinc.com>\n"
end

.available?Boolean

Supported Method Parameters

PWN::FFI::Liquid.available?

Returns:

  • (Boolean)


103
104
105
106
107
# File 'lib/pwn/ffi/liquid.rb', line 103

public_class_method def self.available?
  !@load_error && respond_to?(:freqdem_create, true)
rescue StandardError
  false
end

.dc_block(opts = {}) ⇒ Object

Supported Method Parameters

out = PWN::FFI::Liquid.dc_block( samples: 'required - Array', m: 'optional - prototype semi-length (default 7 → len 15)', as_db: 'optional - stop-band attenuation dB (default 60.0)' )



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/pwn/ffi/liquid.rb', line 219

public_class_method def self.dc_block(opts = {})
  raise 'ERROR: libliquid not available' unless available?

  samples = opts[:samples]
  m       = (opts[:m] || 7).to_i
  as_db   = (opts[:as_db] || 60.0).to_f
  return [] if samples.empty?

  q = firfilt_rrrf_create_dc_blocker(m, as_db)
  raise 'ERROR: firfilt_rrrf_create_dc_blocker failed' if q.null?

  begin
    n = samples.length
    in_ptr = float_ptr(samples)
    out_ptr = PubFFI::MemoryPointer.new(:float, n)
    rc = firfilt_rrrf_execute_block(q, in_ptr, n, out_ptr)
    raise "ERROR: firfilt_rrrf_execute_block rc=#{rc}" unless rc.zero?

    out_ptr.read_array_of_float(n)
  ensure
    firfilt_rrrf_destroy(q)
  end
end

.fir_kaiser(opts = {}) ⇒ Object

Supported Method Parameters

out = PWN::FFI::Liquid.fir_kaiser( samples: 'required - Array', length: 'optional - filter length (default 51, odd preferred)', fc: 'optional - normalised cutoff 0..0.5 (default 0.1)', as_db: 'optional - stop-band attenuation dB (default 60.0)', mu: 'optional - fractional sample offset (default 0.0)' ) Returns Array same length as input.



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/pwn/ffi/liquid.rb', line 186

public_class_method def self.fir_kaiser(opts = {})
  raise 'ERROR: libliquid not available' unless available?

  samples = opts[:samples]
  length  = (opts[:length] || 51).to_i
  fc      = (opts[:fc] || 0.1).to_f
  as_db   = (opts[:as_db] || 60.0).to_f
  mu      = (opts[:mu] || 0.0).to_f
  return [] if samples.empty?

  q = firfilt_rrrf_create_kaiser(length, fc, as_db, mu)
  raise 'ERROR: firfilt_rrrf_create_kaiser failed' if q.null?

  begin
    n = samples.length
    in_ptr = float_ptr(samples)
    out_ptr = PubFFI::MemoryPointer.new(:float, n)
    rc = firfilt_rrrf_execute_block(q, in_ptr, n, out_ptr)
    raise "ERROR: firfilt_rrrf_execute_block rc=#{rc}" unless rc.zero?

    out_ptr.read_array_of_float(n)
  ensure
    firfilt_rrrf_destroy(q)
  end
end

.freq_demod(opts = {}) ⇒ Object

Supported Method Parameters

audio = PWN::FFI::Liquid.freq_demod( iq: 'required - interleaved I/Q Array (even length)', kf: 'optional - modulation factor (default 0.5)' ) Returns Array of demodulated real samples (length = iq.length/2).



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/pwn/ffi/liquid.rb', line 116

public_class_method def self.freq_demod(opts = {})
  raise 'ERROR: libliquid not available' unless available?

  iq = opts[:iq]
  kf = (opts[:kf] || 0.5).to_f
  n  = iq.length / 2
  return [] if n.zero?

  fd = freqdem_create(kf)
  raise 'ERROR: freqdem_create failed' if fd.null?

  begin
    in_ptr = float_ptr(iq.first(n * 2))
    out_ptr = PubFFI::MemoryPointer.new(:float, n)
    rc = freqdem_demodulate_block(fd, in_ptr, n, out_ptr)
    raise "ERROR: freqdem_demodulate_block rc=#{rc}" unless rc.zero?

    out_ptr.read_array_of_float(n)
  ensure
    freqdem_destroy(fd)
  end
end

.gmsk_demod(opts = {}) ⇒ Object

Supported Method Parameters

bits = PWN::FFI::Liquid.gmsk_demod( iq: 'required - interleaved I/Q Array, length = k·sym·2', sps: 'required - samples per symbol (integer ≥ 2)', m: 'optional - filter delay (default 3)', bt: 'optional - Gaussian BT (default 0.35)' ) Returns Array<0|1> (one bit per symbol, floor(n/sps) bits).



319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/pwn/ffi/liquid.rb', line 319

public_class_method def self.gmsk_demod(opts = {})
  raise 'ERROR: libliquid not available' unless available?

  iq  = opts[:iq]
  k   = opts[:sps].to_i
  m   = (opts[:m] || 3).to_i
  bt  = (opts[:bt] || 0.35).to_f
  raise 'ERROR: sps must be ≥ 2' if k < 2

  n = iq.length / 2
  nsym = n / k
  return [] if nsym.zero?

  q = gmskdem_create(k, m, bt)
  raise 'ERROR: gmskdem_create failed' if q.null?

  begin
    in_ptr  = float_ptr(iq.first(nsym * k * 2))
    bit_ptr = PubFFI::MemoryPointer.new(:uint, 1)
    bits = Array.new(nsym)
    i = 0
    while i < nsym
      gmskdem_demodulate(q, in_ptr + (i * k * 2 * 4), bit_ptr)
      bits[i] = bit_ptr.read_uint & 1
      i += 1
    end
    bits
  ensure
    gmskdem_destroy(q)
  end
end

.helpObject

Display Usage for this Module



396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
# File 'lib/pwn/ffi/liquid.rb', line 396

public_class_method def self.help
  puts "USAGE:
    #{self}.available?                              # => true/false
    #{self}.freq_demod(iq:, kf: 0.5)                # FM I/Q → audio
    #{self}.resample(samples:, rate:, as_db: 60.0)  # arbitrary rate
    #{self}.fir_kaiser(samples:, length: 51, fc: 0.1, as_db: 60.0)
    #{self}.dc_block(samples:, m: 7, as_db: 60.0)
#{self}.resample_iq(iq:, rate:, as_db: 60.0)     # complex resample
#{self}.mix_down(iq:, freq:)                     # NCO mix (rad/samp)
#{self}.gmsk_demod(iq:, sps:, m: 3, bt: 0.35)    # I/Q → bits
#{self}.mfsk_demod(iq:, m:, sps:, bw: 0.25)      # I/Q → symbols

    #{self}.authors
  "
end

.mfsk_demod(opts = {}) ⇒ Object

Supported Method Parameters

syms = PWN::FFI::Liquid.mfsk_demod( iq: 'required - interleaved I/Q Array', m: 'required - bits per symbol (1=2FSK, 2=4FSK, …)', sps: 'required - samples per symbol (integer ≥ 2^m)', bw: 'optional - normalised bandwidth 0..0.5 (default 0.25)' ) Returns Array of demodulated symbols (0..2^m-1).



360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
# File 'lib/pwn/ffi/liquid.rb', line 360

public_class_method def self.mfsk_demod(opts = {})
  raise 'ERROR: libliquid not available' unless available?

  iq  = opts[:iq]
  mb  = opts[:m].to_i
  k   = opts[:sps].to_i
  bw  = (opts[:bw] || 0.25).to_f
  n   = iq.length / 2
  nsym = n / k
  return [] if nsym.zero?

  q = fskdem_create(mb, k, bw, 0)
  raise 'ERROR: fskdem_create failed' if q.null?

  begin
    in_ptr = float_ptr(iq.first(nsym * k * 2))
    syms = Array.new(nsym)
    i = 0
    while i < nsym
      syms[i] = fskdem_demodulate(q, in_ptr + (i * k * 2 * 4))
      i += 1
    end
    syms
  ensure
    fskdem_destroy(q)
  end
end

.mix_down(opts = {}) ⇒ Object

Supported Method Parameters

out = PWN::FFI::Liquid.mix_down( iq: 'required - interleaved I/Q Array', freq: 'required - normalised angular freq (rad/sample, i.e. 2π·f/fs)' ) Returns interleaved Array shifted down by freq.



286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/pwn/ffi/liquid.rb', line 286

public_class_method def self.mix_down(opts = {})
  raise 'ERROR: libliquid not available' unless available?

  iq   = opts[:iq]
  freq = opts[:freq].to_f
  n    = iq.length / 2
  return [] if n.zero?

  q = nco_crcf_create(LIQUID_NCO)
  raise 'ERROR: nco_crcf_create failed' if q.null?

  begin
    nco_crcf_set_frequency(q, freq)
    in_ptr  = float_ptr(iq.first(n * 2))
    out_ptr = PubFFI::MemoryPointer.new(:float, n * 2)
    rc = nco_crcf_mix_block_down(q, in_ptr, out_ptr, n)
    raise "ERROR: nco_crcf_mix_block_down rc=#{rc}" unless rc.zero?

    out_ptr.read_array_of_float(n * 2)
  ensure
    nco_crcf_destroy(q)
  end
end

.resample(opts = {}) ⇒ Object

Supported Method Parameters

out = PWN::FFI::Liquid.resample( samples: 'required - Array', rate: 'required - output/input ratio (>0)', as_db: 'optional - stop-band attenuation dB (default 60.0)' ) Returns Array of resampled samples.



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
174
# File 'lib/pwn/ffi/liquid.rb', line 147

public_class_method def self.resample(opts = {})
  raise 'ERROR: libliquid not available' unless available?

  samples = opts[:samples]
  rate    = opts[:rate].to_f
  as_db   = (opts[:as_db] || 60.0).to_f
  raise 'ERROR: rate must be > 0' unless rate.positive?
  return samples.dup if samples.empty?

  q = msresamp_rrrf_create(rate, as_db)
  raise 'ERROR: msresamp_rrrf_create failed' if q.null?

  begin
    nx = samples.length
    # allocate generously: ceil(1 + 2*r*nx)
    ny_max = [1, (1 + (2 * rate * nx)).ceil].max
    in_ptr = float_ptr(samples)
    out_ptr = PubFFI::MemoryPointer.new(:float, ny_max)
    ny_ptr = PubFFI::MemoryPointer.new(:uint, 1)
    rc = msresamp_rrrf_execute(q, in_ptr, nx, out_ptr, ny_ptr)
    raise "ERROR: msresamp_rrrf_execute rc=#{rc}" unless rc.zero?

    ny = ny_ptr.read_uint
    out_ptr.read_array_of_float(ny)
  ensure
    msresamp_rrrf_destroy(q)
  end
end

.resample_iq(opts = {}) ⇒ Object

Supported Method Parameters

out = PWN::FFI::Liquid.resample_iq( iq: 'required - interleaved I/Q Array', rate: 'required - output/input ratio (>0)', as_db: 'optional - stop-band attenuation dB (default 60.0)' ) Returns interleaved Array [I0,Q0,…] resampled by rate.



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/pwn/ffi/liquid.rb', line 251

public_class_method def self.resample_iq(opts = {})
  raise 'ERROR: libliquid not available' unless available?

  iq    = opts[:iq]
  rate  = opts[:rate].to_f
  as_db = (opts[:as_db] || 60.0).to_f
  n     = iq.length / 2
  raise 'ERROR: rate must be > 0' unless rate.positive?
  return iq.dup if n.zero?

  q = msresamp_crcf_create(rate, as_db)
  raise 'ERROR: msresamp_crcf_create failed' if q.null?

  begin
    ny_max = [1, (2 + (2 * rate * n)).ceil].max
    in_ptr = float_ptr(iq.first(n * 2))
    out_ptr = PubFFI::MemoryPointer.new(:float, ny_max * 2)
    ny_ptr  = PubFFI::MemoryPointer.new(:uint, 1)
    rc = msresamp_crcf_execute(q, in_ptr, n, out_ptr, ny_ptr)
    raise "ERROR: msresamp_crcf_execute rc=#{rc}" unless rc.zero?

    ny = ny_ptr.read_uint
    out_ptr.read_array_of_float(ny * 2)
  ensure
    msresamp_crcf_destroy(q)
  end
end