radio 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+