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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '019e21a841f8f9f9aaf543741487c734d4b1c7b2ff4b2bfd20e37cf56fbfcf00'
4
- data.tar.gz: c4c3657ae68d2b5f8f2b931c8256447b9ab8feb0d0bbdfe61e7d035e34ee4053
3
+ metadata.gz: 1b12c3758778bb23282dd45b6fd7a7875ab966902921da11a01e20e091e695f3
4
+ data.tar.gz: 9d89756d0d0777d2f60e650809c38c5aad56a33e115e873e6557bb5586720b02
5
5
  SHA512:
6
- metadata.gz: cf8de21d50dbf21fd92dfec27cdeab872ccf83e3bbe1132ebbeab74bba98ae4e84da90f794574fc8c742da761768f2f49b26f5ea3c345b49960646a0e63ea053
7
- data.tar.gz: 5d587f36838184747666800ac202e52d33584169926f9cd2bd72db1d39a5f4189be4b7025898c675f274134ba326537f08782f9682d7e6fe58dce0921f21b497
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 planning flags
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
- # FFT direction
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
@@ -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.1"
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.1
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - joshfng