rtlsdr 0.2.1 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +21 -0
- data/lib/rtlsdr/demod.rb +105 -0
- data/lib/rtlsdr/ffi.rb +4 -3
- data/lib/rtlsdr/fftw.rb +30 -2
- data/lib/rtlsdr/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1b12c3758778bb23282dd45b6fd7a7875ab966902921da11a01e20e091e695f3
|
|
4
|
+
data.tar.gz: 9d89756d0d0777d2f60e650809c38c5aad56a33e115e873e6557bb5586720b02
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 57cfd89abe99c55d86e66e3fee3b0da12da94f6feceab7860be79871cd9611bf31ce3f9a2fb8235f1cb2f5ae06bb353df56c3bae3df5c2b4f3affc0267d3bce9
|
|
7
|
+
data.tar.gz: 1f9c2f119f049eaa84aff2c4afbef95854ebcd3a241441d0676a0f9220b064730b91ee6e95b7d6afbbcdfc40e673418de1f8a70ae1ffea041a65bde26b7c047c
|
data/README.md
CHANGED
|
@@ -289,6 +289,22 @@ audio = RTLSDR::Demod.usb(samples, sample_rate: 2_048_000, bfo_offset: 1500)
|
|
|
289
289
|
audio = RTLSDR::Demod.lsb(samples, sample_rate: 2_048_000, bfo_offset: 1500)
|
|
290
290
|
```
|
|
291
291
|
|
|
292
|
+
### FSK Demodulation
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
# Demodulate FSK signal at 1200 baud
|
|
296
|
+
bits = RTLSDR::Demod.fsk(samples, sample_rate: 48_000, baud_rate: 1200)
|
|
297
|
+
|
|
298
|
+
# RTTY at 45.45 baud
|
|
299
|
+
bits = RTLSDR::Demod.fsk(samples, sample_rate: 48_000, baud_rate: 45.45)
|
|
300
|
+
|
|
301
|
+
# Invert mark/space if needed
|
|
302
|
+
bits = RTLSDR::Demod.fsk(samples, sample_rate: 48_000, baud_rate: 1200, invert: true)
|
|
303
|
+
|
|
304
|
+
# Get raw discriminator output for debugging/visualization
|
|
305
|
+
waveform = RTLSDR::Demod.fsk_raw(samples, sample_rate: 48_000, baud_rate: 1200)
|
|
306
|
+
```
|
|
307
|
+
|
|
292
308
|
### Helper Functions
|
|
293
309
|
|
|
294
310
|
```ruby
|
|
@@ -542,6 +558,11 @@ ruby examples/spectrum_analyzer.rb
|
|
|
542
558
|
- `Demod.usb(samples, sample_rate:, audio_rate:, bfo_offset:)` - Upper Sideband
|
|
543
559
|
- `Demod.lsb(samples, sample_rate:, audio_rate:, bfo_offset:)` - Lower Sideband
|
|
544
560
|
|
|
561
|
+
#### FSK Demodulation
|
|
562
|
+
|
|
563
|
+
- `Demod.fsk(samples, sample_rate:, baud_rate:, invert:)` - FSK to bits
|
|
564
|
+
- `Demod.fsk_raw(samples, sample_rate:, baud_rate:)` - Raw discriminator output
|
|
565
|
+
|
|
545
566
|
#### Helper Functions
|
|
546
567
|
|
|
547
568
|
- `Demod.complex_oscillator(length, frequency, sample_rate)` - Generate carrier
|
data/lib/rtlsdr/demod.rb
CHANGED
|
@@ -363,6 +363,111 @@ module RTLSDR
|
|
|
363
363
|
normalize_audio(audio)
|
|
364
364
|
end
|
|
365
365
|
|
|
366
|
+
# =========================================================================
|
|
367
|
+
# FSK Demodulation
|
|
368
|
+
# =========================================================================
|
|
369
|
+
|
|
370
|
+
# FSK (Frequency Shift Keying) demodulation
|
|
371
|
+
#
|
|
372
|
+
# Demodulates FSK signals by using an FM discriminator to extract
|
|
373
|
+
# instantaneous frequency, then thresholding to recover bits.
|
|
374
|
+
# FSK encodes data by switching between two frequencies (mark and space).
|
|
375
|
+
#
|
|
376
|
+
# @param [Array<Complex>] samples Input IQ samples
|
|
377
|
+
# @param [Integer] sample_rate Input sample rate in Hz
|
|
378
|
+
# @param [Numeric] baud_rate Symbol rate in baud (symbols per second)
|
|
379
|
+
# @param [Boolean] invert Swap mark/space interpretation (default: false)
|
|
380
|
+
# @return [Array<Integer>] Recovered bits (0 or 1)
|
|
381
|
+
# @example Demodulate 1200 baud FSK
|
|
382
|
+
# bits = RTLSDR::Demod.fsk(samples, sample_rate: 48_000, baud_rate: 1200)
|
|
383
|
+
# @example Demodulate RTTY at 45.45 baud
|
|
384
|
+
# bits = RTLSDR::Demod.fsk(samples, sample_rate: 48_000, baud_rate: 45.45)
|
|
385
|
+
def self.fsk(samples, sample_rate:, baud_rate:, invert: false)
|
|
386
|
+
return [] if samples.empty? || samples.length < 2
|
|
387
|
+
|
|
388
|
+
# Step 1: FM discriminator to get instantaneous frequency
|
|
389
|
+
freq = phase_diff(samples)
|
|
390
|
+
return [] if freq.empty?
|
|
391
|
+
|
|
392
|
+
# Step 2: Lowpass filter to smooth transitions (cutoff at 1.5x baud rate)
|
|
393
|
+
filter_cutoff = [baud_rate * 1.5, (sample_rate / 2.0) - 1].min
|
|
394
|
+
filter = DSP::Filter.lowpass(
|
|
395
|
+
cutoff: filter_cutoff,
|
|
396
|
+
sample_rate: sample_rate,
|
|
397
|
+
taps: 63
|
|
398
|
+
)
|
|
399
|
+
complex_freq = freq.map { |f| Complex(f, 0) }
|
|
400
|
+
smoothed = filter.apply(complex_freq).map(&:real)
|
|
401
|
+
|
|
402
|
+
# Step 3: Decimate to ~4x baud rate for bit decisions
|
|
403
|
+
target_rate = (baud_rate * 4).to_i
|
|
404
|
+
target_rate = [target_rate, sample_rate].min
|
|
405
|
+
|
|
406
|
+
if sample_rate > target_rate && target_rate.positive?
|
|
407
|
+
decimated = DSP.resample(
|
|
408
|
+
smoothed.map { |s| Complex(s, 0) },
|
|
409
|
+
from_rate: sample_rate,
|
|
410
|
+
to_rate: target_rate
|
|
411
|
+
).map(&:real)
|
|
412
|
+
effective_rate = target_rate
|
|
413
|
+
else
|
|
414
|
+
decimated = smoothed
|
|
415
|
+
effective_rate = sample_rate
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
return [] if decimated.empty?
|
|
419
|
+
|
|
420
|
+
# Step 4: Threshold at midpoint to get raw bits
|
|
421
|
+
threshold = decimated.sum / decimated.length.to_f
|
|
422
|
+
raw_bits = decimated.map { |s| s > threshold ? 1 : 0 }
|
|
423
|
+
raw_bits = raw_bits.map { |b| 1 - b } if invert
|
|
424
|
+
|
|
425
|
+
# Step 5: Sample at symbol centers
|
|
426
|
+
samples_per_symbol = effective_rate.to_f / baud_rate
|
|
427
|
+
return raw_bits if samples_per_symbol < 1
|
|
428
|
+
|
|
429
|
+
output_bits = []
|
|
430
|
+
offset = (samples_per_symbol / 2.0).to_i
|
|
431
|
+
index = offset
|
|
432
|
+
|
|
433
|
+
while index < raw_bits.length
|
|
434
|
+
output_bits << raw_bits[index]
|
|
435
|
+
index += samples_per_symbol.round
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
output_bits
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# FSK demodulation returning raw discriminator output
|
|
442
|
+
#
|
|
443
|
+
# Returns the smoothed frequency discriminator output without bit slicing.
|
|
444
|
+
# Useful for visualizing FSK signals, debugging, or implementing custom
|
|
445
|
+
# clock recovery algorithms.
|
|
446
|
+
#
|
|
447
|
+
# @param [Array<Complex>] samples Input IQ samples
|
|
448
|
+
# @param [Integer] sample_rate Input sample rate in Hz
|
|
449
|
+
# @param [Numeric] baud_rate Symbol rate in baud (used for filter cutoff)
|
|
450
|
+
# @return [Array<Float>] Smoothed discriminator output
|
|
451
|
+
# @example Get raw FSK waveform for plotting
|
|
452
|
+
# waveform = RTLSDR::Demod.fsk_raw(samples, sample_rate: 48_000, baud_rate: 1200)
|
|
453
|
+
def self.fsk_raw(samples, sample_rate:, baud_rate:)
|
|
454
|
+
return [] if samples.empty? || samples.length < 2
|
|
455
|
+
|
|
456
|
+
# FM discriminator
|
|
457
|
+
freq = phase_diff(samples)
|
|
458
|
+
return [] if freq.empty?
|
|
459
|
+
|
|
460
|
+
# Lowpass filter
|
|
461
|
+
filter_cutoff = [baud_rate * 1.5, (sample_rate / 2.0) - 1].min
|
|
462
|
+
filter = DSP::Filter.lowpass(
|
|
463
|
+
cutoff: filter_cutoff,
|
|
464
|
+
sample_rate: sample_rate,
|
|
465
|
+
taps: 63
|
|
466
|
+
)
|
|
467
|
+
complex_freq = freq.map { |f| Complex(f, 0) }
|
|
468
|
+
filter.apply(complex_freq).map(&:real)
|
|
469
|
+
end
|
|
470
|
+
|
|
366
471
|
# =========================================================================
|
|
367
472
|
# Private Helpers
|
|
368
473
|
# =========================================================================
|
data/lib/rtlsdr/ffi.rb
CHANGED
|
@@ -140,10 +140,11 @@ module RTLSDR
|
|
|
140
140
|
attach_function :rtlsdr_get_offset_tuning, [:rtlsdr_dev_t], :int
|
|
141
141
|
|
|
142
142
|
# Streaming functions
|
|
143
|
+
# Note: blocking: true releases the GVL so other Ruby threads can run
|
|
143
144
|
attach_function :rtlsdr_reset_buffer, [:rtlsdr_dev_t], :int
|
|
144
|
-
attach_function :rtlsdr_read_sync, %i[rtlsdr_dev_t pointer int pointer], :int
|
|
145
|
-
attach_function :rtlsdr_wait_async, %i[rtlsdr_dev_t rtlsdr_read_async_cb_t pointer], :int
|
|
146
|
-
attach_function :rtlsdr_read_async, %i[rtlsdr_dev_t rtlsdr_read_async_cb_t pointer uint32 uint32], :int
|
|
145
|
+
attach_function :rtlsdr_read_sync, %i[rtlsdr_dev_t pointer int pointer], :int, blocking: true
|
|
146
|
+
attach_function :rtlsdr_wait_async, %i[rtlsdr_dev_t rtlsdr_read_async_cb_t pointer], :int, blocking: true
|
|
147
|
+
attach_function :rtlsdr_read_async, %i[rtlsdr_dev_t rtlsdr_read_async_cb_t pointer uint32 uint32], :int, blocking: true
|
|
147
148
|
attach_function :rtlsdr_cancel_async, [:rtlsdr_dev_t], :int
|
|
148
149
|
|
|
149
150
|
# Bias tee functions
|
data/lib/rtlsdr/fftw.rb
CHANGED
|
@@ -45,21 +45,49 @@ module RTLSDR
|
|
|
45
45
|
attr_reader :load_error
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
# FFTW
|
|
48
|
+
# @!group FFTW Planning Flags
|
|
49
|
+
# These constants control how FFTW plans are created. Higher effort flags
|
|
50
|
+
# produce faster transforms but take longer to plan.
|
|
51
|
+
|
|
52
|
+
# @return [Integer] Measure execution time to find optimal plan
|
|
49
53
|
FFTW_MEASURE = 0
|
|
54
|
+
|
|
55
|
+
# @return [Integer] Allow input array to be destroyed during planning
|
|
50
56
|
FFTW_DESTROY_INPUT = 1
|
|
57
|
+
|
|
58
|
+
# @return [Integer] Don't assume arrays are aligned in memory
|
|
51
59
|
FFTW_UNALIGNED = 2
|
|
60
|
+
|
|
61
|
+
# @return [Integer] Minimize memory usage at cost of speed
|
|
52
62
|
FFTW_CONSERVE_MEMORY = 4
|
|
63
|
+
|
|
64
|
+
# @return [Integer] Try all possible algorithms (very slow planning)
|
|
53
65
|
FFTW_EXHAUSTIVE = 8
|
|
66
|
+
|
|
67
|
+
# @return [Integer] Preserve input array contents during transform
|
|
54
68
|
FFTW_PRESERVE_INPUT = 16
|
|
69
|
+
|
|
70
|
+
# @return [Integer] Like MEASURE but try harder (slower planning)
|
|
55
71
|
FFTW_PATIENT = 32
|
|
72
|
+
|
|
73
|
+
# @return [Integer] Use quick heuristic to pick plan (fast planning, default)
|
|
56
74
|
FFTW_ESTIMATE = 64
|
|
75
|
+
|
|
76
|
+
# @return [Integer] Only use wisdom (cached plans), fail if none available
|
|
57
77
|
FFTW_WISDOM_ONLY = 2_097_152
|
|
58
78
|
|
|
59
|
-
#
|
|
79
|
+
# @!endgroup
|
|
80
|
+
|
|
81
|
+
# @!group FFT Direction Constants
|
|
82
|
+
|
|
83
|
+
# @return [Integer] Forward FFT direction (time to frequency domain)
|
|
60
84
|
FFTW_FORWARD = -1
|
|
85
|
+
|
|
86
|
+
# @return [Integer] Backward/Inverse FFT direction (frequency to time domain)
|
|
61
87
|
FFTW_BACKWARD = 1
|
|
62
88
|
|
|
89
|
+
# @!endgroup
|
|
90
|
+
|
|
63
91
|
if available?
|
|
64
92
|
# Memory allocation
|
|
65
93
|
attach_function :fftw_malloc, [:size_t], :pointer
|
data/lib/rtlsdr/version.rb
CHANGED