rtlsdr 0.2.3 → 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/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/version.rb
CHANGED