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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bdda82a845fb18a7c4e62aa92047ed89c380ff8cb4eeb09c0d67791fd6504fc2
4
- data.tar.gz: 4427f96657f4c5be7e36693fc1c217ed138caf89128d3203201d4c76dbfc5599
3
+ metadata.gz: 1b12c3758778bb23282dd45b6fd7a7875ab966902921da11a01e20e091e695f3
4
+ data.tar.gz: 9d89756d0d0777d2f60e650809c38c5aad56a33e115e873e6557bb5586720b02
5
5
  SHA512:
6
- metadata.gz: fa87ae066f74a465ba70b33ad0ec5d0619abd698216911325754182e2bfb6b6a872aef24e82d430ebea12a304b342fbf007d5afbe36464011ee9f4806bd844e1
7
- data.tar.gz: 96b90536e0c46f750ae06d22ff5608dbfdfeb06879221f7ada51c5ec2c50560029744d8296c170a69173c33e4e07e472bdc64ca96e1ed475736e19affdb62835
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
  # =========================================================================
@@ -12,5 +12,5 @@ module RTLSDR
12
12
  #
13
13
  # @return [String] Current gem version
14
14
  # @since 0.1.0
15
- VERSION = "0.2.3"
15
+ VERSION = "0.2.4"
16
16
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rtlsdr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - joshfng