radio 0.0.2 → 0.0.3

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.
@@ -20,64 +20,62 @@
20
20
  class Radio
21
21
  class Gif
22
22
 
23
- # This is an optimized GIF generator for waterfalls.
23
+ # This is a very fast GIF generator for waterfalls.
24
24
  # It requires 128 RGB colors, the first is unused and
25
25
  # reserved for transparency if we ever need it.
26
- def self.waterfall colors, data
27
-
26
+ # data is expected to be an NArray
27
+ def self.waterfall colors, data, min=nil, max=nil
28
+ width = data[true,0].size
29
+ height = data[0,true].size
28
30
  gif = [
29
31
  'GIF87a', # Start Header
30
- data[0].size, # width
31
- data.size, # height
32
+ width,
33
+ height,
32
34
  0xF6, # 128 24-bit colors
33
35
  0x00, # background color index
34
36
  0x00 # aspect ratio
35
37
  ].pack 'a6vvCCC'
36
-
37
38
  gif += colors.flatten.pack 'C*'
38
-
39
39
  gif += [
40
40
  0x2C, # Start Image Block
41
41
  0x0000, # Left position
42
42
  0x0000, # Top position
43
- data[0].size, # width
44
- data.size, # height
43
+ width,
44
+ height,
45
45
  0x00, # No color table, not interlaced
46
46
  0x07 # LZW code size
47
47
  ].pack('CvvvvCC')
48
-
49
- data.each_with_index do |vals, row|
50
- col = 0
51
- min = vals.min
52
- range = [1e-99, vals.max - min].max
53
- while col < vals.size
54
- # Uncompressed GIF trickery avoids bit packing too
55
- # 126 byte chunks with reset keeps LZW in 8 bit codes
56
- col_end = [col+126,vals.size].min
57
- slice = vals.slice(col...col_end).to_a
58
- # This 126 because palette is 1..127
59
- slice = slice.collect { |x| (x - min) / range * 126 + 1 }
60
- slice = slice.pack 'C*'
61
- newstuff = [
62
- slice.size+1,
63
- slice,
64
- 0x80 # LZW reset
65
- ].pack('Ca*C')
66
- gif += newstuff
67
- col += 126
48
+ data = data.reshape(data.size)
49
+ min ||= data.min
50
+ max ||= data.max
51
+ range = [1e-99, max - min].max
52
+ data -= min # will dup
53
+ data.div!(range).mul!(126).add!(1)
54
+ # add in a frame+reset every 126 plus the 4 byte end block
55
+ predict = gif.size + (data.size+125) / 126 * 2 + data.size + 4
56
+ out = NArray.byte predict
57
+ out[0...gif.size] = NArray.to_na gif, NArray::BYTE, gif.size
58
+ i = 0
59
+ pos = gif.size
60
+ while i < data.size
61
+ qty = [126,data.size-i].min
62
+ out[pos] = qty+1
63
+ pos+=1
64
+ if qty == 1
65
+ out[pos] = data[i]
66
+ else
67
+ out[pos...pos+qty] = data[i...i+qty]
68
68
  end
69
-
69
+ pos+=qty
70
+ out[pos] = 0x80 # LZW reset
71
+ pos+=1
72
+ i += qty
70
73
  end
71
-
72
- gif += [
73
- 0x01, # end image blocks
74
- 0x81, # final image block: LZW end
75
- 0x00, # end image blocks
76
- 0x3B # end gif
77
- ].pack('C*')
78
-
79
- return gif
80
-
74
+ out[pos] = 0x01, # end image blocks
75
+ out[pos+1] = 0x81, # final image block: LZW end
76
+ out[pos+2] = 0x00, # end image blocks
77
+ out[pos+3] = 0x3B # end gif
78
+ out.to_s
81
79
  end
82
80
 
83
81
  end
@@ -18,10 +18,18 @@ class Radio
18
18
  class PSK31
19
19
 
20
20
  class Rx
21
+
22
+ include Utils
23
+
24
+ # 16 Hz bw LP filter for data recovery
25
+ FIR_BIT = FirPM.new numtaps: 65, type: :bandpass,
26
+ bands: [0.0,0.03125,0.0625,0.5], desired: [1,1,0,0], weights: [1,286]
21
27
 
22
28
  def initialize frequency
23
- phase_inc = PI2 * frequency / 8000
24
- @dec_filter = Filter.new mix:phase_inc, decimate:16, fir:FIR_DEC16
29
+ mix = frequency.to_f / 8000
30
+ fir_500hz = firpm numtaps: 35, type: :bandpass,
31
+ bands: [0,0.0125,0.125,0.5], desired: [1,1,0,0], weights: [1,10]
32
+ @dec_filter = Filter.new mix:mix, decimate:16, fir:fir_500hz
25
33
  @bit_filter = Filter.new fir:FIR_BIT
26
34
  @bit_detect = BitDetect.new
27
35
  @decoder = Decoder.new
@@ -29,11 +37,13 @@ class Radio
29
37
 
30
38
  def call data
31
39
  decoded = ''
32
- @dec_filter.call data do |iq|
33
- @bit_filter.call iq do |iq|
34
- @bit_detect.call iq do |iq|
35
- @decoder.call iq do |symbol|
36
- decoded += symbol
40
+ @dec_filter.call data do |data|
41
+ @bit_filter.call data do |data|
42
+ data.each do |iq|
43
+ @bit_detect.call iq do |iq|
44
+ @decoder.call iq do |symbol|
45
+ decoded += symbol
46
+ end
37
47
  end
38
48
  end
39
49
  end
@@ -17,30 +17,52 @@ class Radio
17
17
 
18
18
  class Rig
19
19
 
20
+ include Utils
20
21
  include Spectrum
21
22
  include Rx
22
23
  include LO
24
+ include SSB
23
25
 
24
26
  def initialize
25
27
  @semaphore = Mutex.new
28
+ @listeners = {}
29
+ @listeners_mutex = Mutex.new
26
30
  super
27
31
  end
28
32
 
33
+ def register queue
34
+ @listeners_mutex.synchronize do
35
+ @listeners[queue] = true
36
+ end
37
+ end
38
+
39
+ def deregister queue
40
+ @listeners_mutex.synchronize do
41
+ @listeners.delete queue
42
+ end
43
+ end
44
+
29
45
  def rate
30
46
  @semaphore.synchronize do
31
47
  return @rx.rate if @rx
32
- return @tx.rate if @tx
33
48
  return 0
34
49
  end
35
50
  end
36
51
 
37
52
  def iq?
38
53
  @semaphore.synchronize do
39
- return @rx.channels == 2 if @rx
40
- return @tx.channels == 2 if @tx
54
+ return @rx.input_channels == 2 if @rx
41
55
  return false
42
56
  end
43
57
  end
58
+
59
+ private
60
+
61
+ def distribute_to_listeners data
62
+ @listeners_mutex.synchronize do
63
+ @listeners.each {|k,v| k << data }
64
+ end
65
+ end
44
66
 
45
67
  end
46
68
  end
@@ -18,14 +18,14 @@ class Radio
18
18
  class Rig
19
19
 
20
20
  module Rx
21
-
21
+
22
22
  def initialize
23
23
  @rx = @rx_thread = nil
24
24
  super
25
25
  end
26
26
 
27
27
  def rx= input
28
- old_rx_thread = new_thread = false
28
+ old_rx_thread = false
29
29
  @semaphore.synchronize do
30
30
  if @rx
31
31
  @rx.stop
@@ -46,10 +46,15 @@ class Radio
46
46
  # We want to work with powers of two but sample rates
47
47
  # are multiples of 8000. Let's use a lag of 32ms or 31.25 baud.
48
48
  def rx_thread
49
- qty_samples = @rx.rate / 125 * 4
50
- loop do
51
- samples = @rx.call qty_samples
52
- spectrum_collect samples
49
+ begin
50
+ qty_samples = @rx.rate / 125 * 4
51
+ loop do
52
+ distribute_to_listeners @rx.in qty_samples
53
+ end
54
+ rescue Exception => e
55
+ p "ERROR #{e.message}: #{e.backtrace.first}" #TODO logger
56
+ # This will be picked up by next call to rx=
57
+ raise e
53
58
  end
54
59
  end
55
60
 
@@ -19,7 +19,10 @@ class Radio
19
19
 
20
20
  def initialize
21
21
  @spectrum_buf = {}
22
+ @spectrum_semaphore = Mutex.new
22
23
  @spectrum_pending = Hash.new {|h,k|h[k]=[]}
24
+ register @spectrum_queue = Queue.new
25
+ Thread.start &method(:spectrum_collector)
23
26
  super
24
27
  end
25
28
 
@@ -31,7 +34,7 @@ class Radio
31
34
  # Results are not queued; missed processing is skipped.
32
35
  # This is meant to be an ideal interface for Web UI work, not signal analysis.
33
36
  def spectrum size, frequency=1.0, keep_alive=2.0
34
- @semaphore.synchronize do
37
+ @spectrum_semaphore.synchronize do
35
38
  @spectrum_pending[[size, frequency, keep_alive]] << Thread.current
36
39
  end
37
40
  sleep
@@ -40,53 +43,61 @@ class Radio
40
43
 
41
44
  private
42
45
 
43
- def spectrum_collect samples
44
- time_now = Time.now
45
- @semaphore.synchronize do
46
- # Ensure buffers for all requested setups are in place
47
- @spectrum_pending.keys.each do |size, frequency, keep_alive|
48
- @spectrum_buf[[size, frequency, keep_alive]] ||= [nil,[],nil]
49
- @spectrum_buf[[size, frequency, keep_alive]][0] = time_now
50
- end
51
- # Stop running anything that's not being used anymore
52
- @spectrum_buf.delete_if { |k,v| v[0] < time_now - k[2] }
53
- # Handle the data for each active buffer
54
- @spectrum_buf.each do |k,v|
55
- time, data, result = v
56
- size, frequency, keep_alive = k
57
- collect_size = size * ((Float === samples[0]) ? 2 : 1)
58
- data.push samples
59
- buf_size = data.reduce(0){|a,b|a+b.size}
60
- size_freq = collect_size*frequency
61
- # Wait until we have enough data and the time is right
62
- if buf_size > collect_size and buf_size > size_freq
63
- # Discard any extra data we won't use (frequency>1)
64
- while buf_size - data.first.size > collect_size
65
- buf_size -= data.shift.size
46
+ def spectrum_collector
47
+ begin
48
+ loop do
49
+ samples = @spectrum_queue.pop
50
+ time_now = Time.now
51
+ @spectrum_semaphore.synchronize do
52
+ # Ensure buffers for all requested setups are in place
53
+ @spectrum_pending.keys.each do |size, frequency, keep_alive|
54
+ @spectrum_buf[[size, frequency, keep_alive]] ||= [nil,[],nil]
55
+ @spectrum_buf[[size, frequency, keep_alive]][0] = time_now
66
56
  end
67
- pending_key = [size, frequency, keep_alive]
68
- if @spectrum_pending.has_key? pending_key
69
- spectrum_data = NArray.to_na data
70
- spectrum_data.reshape! spectrum_data.total
71
- spectrum_out = FFTW3.fft(spectrum_data[-collect_size..-1])
72
- if spectrum_out.size == size
73
- result = NArray.sfloat size
74
- result[0...size/2] = spectrum_out[size/2..-1].div!(size)
75
- result[size/2..-1] = spectrum_out[0...size/2].div!(size)
76
- v[2] = result.abs
77
- else
78
- v[2] = spectrum_out[0...size].div!(size).abs
57
+ # Stop running anything that's not being used anymore
58
+ @spectrum_buf.delete_if { |k,v| v[0] < time_now - k[2] }
59
+ # Handle the data for each active buffer
60
+ @spectrum_buf.each do |k,v|
61
+ time, data, result = v
62
+ size, frequency, keep_alive = k
63
+ collect_size = size * ((Float === samples[0]) ? 2 : 1)
64
+ data.push samples
65
+ buf_size = data.reduce(0){|a,b|a+b.size}
66
+ size_freq = collect_size*frequency
67
+ # Wait until we have enough data and the time is right
68
+ if buf_size > collect_size and buf_size > size_freq
69
+ # Discard any extra data we won't use (frequency>1)
70
+ while buf_size - data.first.size > collect_size
71
+ buf_size -= data.shift.size
72
+ end
73
+ pending_key = [size, frequency, keep_alive]
74
+ if @spectrum_pending.has_key? pending_key
75
+ spectrum_data = NArray.to_na data
76
+ spectrum_data.reshape! spectrum_data.total
77
+ spectrum_out = FFTW3.fft(spectrum_data[-collect_size..-1])
78
+ if spectrum_out.size == size
79
+ result = NArray.sfloat size
80
+ result[0...size/2] = spectrum_out[size/2..-1].div!(size)
81
+ result[size/2..-1] = spectrum_out[0...size/2].div!(size)
82
+ v[2] = result.abs
83
+ else
84
+ v[2] = spectrum_out[0...size].div!(size).abs
85
+ end
86
+ @spectrum_pending[pending_key].each(&:wakeup)
87
+ @spectrum_pending.delete pending_key
88
+ end
89
+ # Discard enough old buffers to accommodate the frequency
90
+ trim_size = [0, collect_size - size*frequency].max
91
+ while buf_size > trim_size
92
+ buf_size -= data.shift.size
93
+ end
79
94
  end
80
- @spectrum_pending[pending_key].each(&:wakeup)
81
- @spectrum_pending.delete pending_key
82
- end
83
- # Discard enough old buffers to accommodate the frequency
84
- trim_size = [0, collect_size - size*frequency].max
85
- while buf_size > trim_size
86
- buf_size -= data.shift.size
87
95
  end
88
96
  end
89
97
  end
98
+ rescue Exception => e
99
+ p "ERROR #{e.message}: #{e.backtrace.first}" #TODO logger
100
+ raise e
90
101
  end
91
102
  end
92
103
 
@@ -0,0 +1,190 @@
1
+ # Copyright 2012 The ham21/radio Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ class Radio
17
+
18
+ class Rig
19
+
20
+ module SSB
21
+
22
+ def initialize
23
+ @ssb_semaphore = Mutex.new
24
+ super
25
+ end
26
+
27
+ def af= output
28
+ if output and !iq?
29
+ raise 'requires i/q signal'
30
+ end
31
+ old_af_thread = false
32
+ @ssb_semaphore.synchronize do
33
+ deregister @af_queue if @af_queue
34
+ @af_queue = nil
35
+ old_rate = 0
36
+ if @af
37
+ @af.stop
38
+ old_af_thread = @af_thread
39
+ old_af_thread.kill if old_af_thread
40
+ end
41
+ if @af = output
42
+ @bfo_filter = bfo_mixer
43
+ @af_filter = af_generate_filter
44
+ @agc = Filter.new :agc => true
45
+ @iq = Filter.new :iq => true
46
+ @mix = mixmix
47
+
48
+ #TODO need a nicer pattern to force JIT compile
49
+ @bfo_filter.call(NArray.scomplex(1)) {}
50
+ @af_filter.call(NArray.sfloat(1)) {}
51
+ @agc.call(NArray.sfloat(1)) {}
52
+ @iq.call(NArray.scomplex(1)) {}
53
+
54
+ register @af_queue = Queue.new
55
+ @af_thread = Thread.new &method(:af_thread)
56
+ end
57
+ end
58
+ old_af_thread.join if old_af_thread
59
+ end
60
+
61
+ def tune= freq
62
+ @ssb_semaphore.synchronize do
63
+ @freq = freq
64
+ set_mixers
65
+ end
66
+ end
67
+
68
+ def set_lsb
69
+ @ssb_semaphore.synchronize do
70
+ if @bfo_filter
71
+ @ssb_mode = :lsb
72
+ @bfo_filter.decimation_fir = @lsb_coef
73
+ set_mixers
74
+ end
75
+ end
76
+ end
77
+
78
+ def set_usb
79
+ @ssb_semaphore.synchronize do
80
+ if @bfo_filter
81
+ @ssb_mode = :usb
82
+ @bfo_filter.decimation_fir = @usb_coef
83
+ set_mixers
84
+ end
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def set_mixers
91
+ return unless @af and @bfo_filter
92
+ freq = @freq.to_f
93
+ if @ssb_mode == :usb
94
+ @bfo_filter.decimation_mix = (freq-1300) / rate
95
+ @mix.mix = +1300.0 / 6000
96
+ else
97
+ @bfo_filter.decimation_mix = (freq+1300) / rate
98
+ @mix.mix = -1300.0 / 6000
99
+ end
100
+ end
101
+
102
+ def af_thread
103
+ begin
104
+ loop do
105
+ in_data = @af_queue.pop
106
+ @ssb_semaphore.synchronize do
107
+ if @af_filter and @af
108
+ @bfo_filter.call(in_data) do |iq|
109
+
110
+ @mix.call!(iq) do |iq|
111
+ @iq.call!(iq) do |iq|
112
+
113
+ @agc.call(iq.real + iq.imag) do |pcm|
114
+ @af_filter.call(pcm) do |data|
115
+ @af.out data
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ rescue Exception => e
125
+ p "ERROR #{e.message}: #{e.backtrace.first}" #TODO logger
126
+ raise e
127
+ end
128
+ end
129
+
130
+ def mixmix
131
+ rate = 6000
132
+ bands = []
133
+ bands[0] = 0.0 / rate
134
+ bands[1] = 2400.0 / rate
135
+ bands[2] = 2600.0 / rate
136
+ bands[3] = 0.5
137
+ taps = kaiser_estimate passband:0.01, stopband:0.01, transition:bands[2]-bands[1]
138
+ fir = firpm numtaps: taps, type: :bandpass,
139
+ bands: bands, desired: [1,1,0,0], weights: [1,1000]
140
+ Filter.new :mix => (1300.0 / rate), :fir => fir
141
+ end
142
+
143
+ def bfo_mixer
144
+ rate = self.rate.to_f
145
+ decimate = rate / 6000
146
+ unless decimate == decimate.floor
147
+ raise "unable to filter #{rate} to 6000"
148
+ end
149
+ bands = []
150
+ bands[0] = 0.0 / rate
151
+ bands[1] = 1200.0 / rate
152
+ bands[2] = 1500.0 / rate
153
+ bands[3] = 0.5
154
+ taps = kaiser_estimate passband:0.1, stopband:0.1, transition:bands[2]-bands[1]
155
+ p taps
156
+ taps = 271
157
+ p taps
158
+ fir1 = firpm numtaps: taps, type: :bandpass,
159
+ bands: bands, desired: [1,1,0,0], weights: [1,10000]
160
+ fir2 = firpm numtaps: taps, type: :hilbert,
161
+ bands: bands, desired: [1,1,0,0], weights: [1,10000]
162
+ @usb_coef = NArray.scomplex fir1.size
163
+ @usb_coef[true] = fir1.to_a
164
+ @usb_coef.imag = fir2.to_a
165
+ @lsb_coef = @usb_coef.conj
166
+ @ssb_mode = :usb
167
+ Filter.new fir:@usb_coef, decimate:decimate, mix:0
168
+ end
169
+
170
+ def af_generate_filter
171
+ return nil unless @af
172
+ bands = [0,nil,nil,0.5]
173
+ bands[1] = 2400.0 / @af.rate
174
+ bands[2] = 3000.0 / @af.rate
175
+ taps = kaiser_estimate passband:0.01, stopband:0.01, transition:bands[2]-bands[1]
176
+ fir = firpm numtaps: taps, type: :bandpass,
177
+ bands: bands, desired: [1,1,0,0], weights: [1,1000]
178
+ interpolate = @af.rate.to_f / 6000
179
+ unless interpolate == interpolate.floor
180
+ raise "unable to filter 6000 to #{@af.rate}"
181
+ end
182
+ Filter.new fir:fir, interpolate:interpolate
183
+ end
184
+
185
+
186
+ end
187
+
188
+ end
189
+ end
190
+