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.
- data/README.md +7 -6
- data/lib/radio.rb +5 -4
- data/lib/radio/controls/civ.rb +284 -0
- data/lib/radio/controls/si570avr.rb +0 -1
- data/lib/radio/filter.rb +33 -34
- data/lib/radio/filters/agc.rb +65 -0
- data/lib/radio/filters/fir.rb +264 -33
- data/lib/radio/filters/iq.rb +142 -0
- data/lib/radio/gif.rb +38 -40
- data/lib/radio/psk31/rx.rb +17 -7
- data/lib/radio/rig.rb +25 -3
- data/lib/radio/rig/rx.rb +11 -6
- data/lib/radio/rig/spectrum.rb +54 -43
- data/lib/radio/rig/ssb.rb +190 -0
- data/lib/radio/rig/ssb.rb_ +150 -0
- data/lib/radio/{input.rb → signal.rb} +16 -12
- data/lib/radio/{inputs → signals}/alsa.rb +20 -23
- data/lib/radio/signals/coreaudio.rb +136 -0
- data/lib/radio/{inputs → signals}/file.rb +8 -8
- data/lib/radio/{inputs → signals}/wav.rb +41 -28
- data/lib/radio/utils/firpm.rb +395 -0
- data/lib/radio/utils/misc.rb +39 -0
- data/lib/radio/utils/window.rb +37 -0
- data/lib/radio/version.rb +1 -1
- data/www/index.erb +81 -12
- data/www/lo.erb +2 -2
- data/www/setup/af.erb +72 -0
- data/www/setup/lo.erb +31 -0
- data/www/setup/{input.erb → rx.erb} +11 -10
- data/www/ssb.erb +20 -0
- data/www/tune.erb +5 -0
- data/www/waterfall.erb +1 -1
- metadata +38 -26
- data/lib/radio/inputs/coreaudio.rb +0 -102
- data/lib/radio/psk31/fir_coef.rb +0 -292
- data/test/test.rb +0 -76
- data/test/wav/bpsk8k.wav +0 -0
- data/test/wav/qpsk8k.wav +0 -0
- data/test/wav/ssb.wav +0 -0
data/lib/radio/filters/fir.rb
CHANGED
@@ -15,49 +15,280 @@
|
|
15
15
|
|
16
16
|
class Radio
|
17
17
|
class Filter
|
18
|
-
|
19
|
-
module
|
20
|
-
|
21
|
-
def
|
22
|
-
@
|
23
|
-
@
|
24
|
-
@
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
18
|
+
|
19
|
+
module SetupFir
|
20
|
+
|
21
|
+
def interpolation_mix= mix
|
22
|
+
@interpolation_mix = mix
|
23
|
+
@interpolation_fir_pos = 0 # sync to mixer
|
24
|
+
@interpolation_phase, @interpolation_inc =
|
25
|
+
new_mixer @interpolation_mix, @interpolation_size
|
26
|
+
setup_interpolation if @interpolation_fir_orig
|
27
|
+
end
|
28
|
+
|
29
|
+
def interpolation_fir= coef
|
30
|
+
# expand interpolation filter for matrix by padding with 0s
|
31
|
+
remainder = coef.size % @interpolation_size
|
32
|
+
if remainder > 0
|
33
|
+
coef = coef.to_a + [0]*(@interpolation_size-remainder)
|
34
|
+
end
|
35
|
+
if @interpolation_fir_orig
|
36
|
+
raise "can't grow filter" if coef.size > @interpolation_fir_size
|
37
|
+
else
|
38
|
+
@interpolation_fir_pos = 0
|
39
|
+
@interpolation_fir_size = coef.size / @interpolation_size
|
40
|
+
@interpolation_buf_f = NArray.sfloat @interpolation_fir_size
|
41
|
+
@interpolation_buf_c = NArray.scomplex @interpolation_fir_size
|
42
|
+
end
|
43
|
+
@interpolation_fir_orig = coef
|
44
|
+
setup_interpolation
|
45
|
+
end
|
46
|
+
|
47
|
+
def decimation_mix= mix
|
48
|
+
@decimation_mix = mix
|
49
|
+
@decimation_fir_pos = 0 # sync to mixer
|
50
|
+
@decimation_phase, @decimation_inc =
|
51
|
+
new_mixer @decimation_mix, @decimation_size
|
52
|
+
setup_decimation if @decimation_fir_orig
|
53
|
+
end
|
54
|
+
alias :mix= :decimation_mix=
|
55
|
+
|
56
|
+
def decimation_fir= coef
|
57
|
+
if @decimation_fir_orig
|
58
|
+
raise "can't grow filter" if coef.size > @decimation_fir_size
|
59
|
+
else
|
60
|
+
@decimation_fir_pos = 0
|
61
|
+
@decimation_fir_size = coef.size
|
62
|
+
@decimation_buf = NArray.scomplex @decimation_fir_size
|
63
|
+
# decimation allows fractions for digital work
|
64
|
+
if Float === @decimation_size
|
65
|
+
@decimation_pos = 0.0
|
66
|
+
else
|
67
|
+
@decimation_pos = 0
|
68
|
+
end
|
69
|
+
end
|
70
|
+
@decimation_fir_orig = coef
|
71
|
+
setup_decimation
|
72
|
+
end
|
73
|
+
alias :fir= :decimation_fir=
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def setup
|
78
|
+
if @interpolation_size = @options[:interpolate]
|
79
|
+
@interpolation_size = @interpolation_size.to_i
|
80
|
+
end
|
81
|
+
@decimation_size = @options[:decimate]
|
82
|
+
if mix = @options[:mix]
|
83
|
+
if @interpolation_size and @decimation_size
|
84
|
+
self.interpolation_mix = mix[0]
|
85
|
+
self.decimation_mix = mix[1]
|
86
|
+
elsif @interpolation_size
|
87
|
+
self.interpolation_mix = mix
|
88
|
+
else
|
89
|
+
self.decimation_mix = mix
|
90
|
+
end
|
91
|
+
end
|
92
|
+
if coef = @options[:fir]
|
93
|
+
if @interpolation_size and @decimation_size
|
94
|
+
self.interpolation_fir = coef[0]
|
95
|
+
self.decimation_fir = coef[1]
|
96
|
+
elsif @interpolation_size
|
97
|
+
self.interpolation_fir = coef
|
98
|
+
else # decimation (use for 1:1 too)
|
99
|
+
self.decimation_fir = coef
|
100
|
+
end
|
101
|
+
end
|
30
102
|
super
|
31
103
|
end
|
104
|
+
|
105
|
+
def setup_interpolation
|
106
|
+
staging = double_filter premix_filter @interpolation_fir_orig, @interpolation_mix
|
107
|
+
# interpolation is shaped to avoid 0*0 ops
|
108
|
+
ranks = staging.size / @interpolation_size
|
109
|
+
pivot = staging.reshape(@interpolation_size, ranks)
|
110
|
+
staging.reshape!(ranks, @interpolation_size)
|
111
|
+
@interpolation_size.times {|rank| staging[true,rank] = pivot[rank,true]}
|
112
|
+
@interpolation_fir_coef = preslice_filter staging
|
113
|
+
end
|
114
|
+
|
115
|
+
def setup_decimation
|
116
|
+
coef = premix_filter @decimation_fir_orig, @decimation_mix
|
117
|
+
@decimation_fir_coef = preslice_filter double_filter coef
|
118
|
+
end
|
119
|
+
|
120
|
+
# There's no obviously easy way to get NArray to
|
121
|
+
# mul_accum on a slice without making a temporary
|
122
|
+
# object. So we make them all and store them in a
|
123
|
+
# regular Ruby array.
|
124
|
+
def preslice_filter filter
|
125
|
+
slices = []
|
126
|
+
steps = filter.shape[0] / 2
|
127
|
+
steps.times do |i|
|
128
|
+
f_start = steps-i
|
129
|
+
f_end = -1-i
|
130
|
+
if filter.rank == 2
|
131
|
+
slices << filter[f_start..f_end, true]
|
132
|
+
else
|
133
|
+
slices << filter[f_start..f_end]
|
134
|
+
end
|
135
|
+
end
|
136
|
+
slices
|
137
|
+
end
|
138
|
+
|
139
|
+
# We build filters with two copies of data so a
|
140
|
+
# circular buffer can mul_accum on a slice.
|
141
|
+
def double_filter coef
|
142
|
+
coef = coef.to_a
|
143
|
+
if Complex === coef[0]
|
144
|
+
new_coef = NArray.scomplex coef.size * 2
|
145
|
+
else
|
146
|
+
new_coef = NArray.sfloat coef.size * 2
|
147
|
+
end
|
148
|
+
# reverse into position
|
149
|
+
new_coef[coef.size-1..0] = coef
|
150
|
+
new_coef[-1..coef.size] = coef
|
151
|
+
new_coef
|
152
|
+
end
|
153
|
+
|
154
|
+
# The signal is pre-mixed into the filter.
|
155
|
+
# We adjust the master phase every time we filter.
|
156
|
+
# mix_phase *= mix_phase_inc # faster than sin+cos
|
157
|
+
# Take out the rounding errors once in a while with:
|
158
|
+
# mix_phase /= mix_phase.abs
|
159
|
+
def premix_filter coef, mix
|
160
|
+
return coef unless mix and mix != 0
|
161
|
+
rate = PI2 * mix
|
162
|
+
i = coef.size
|
163
|
+
coef.collect do |coef|
|
164
|
+
i -= 1
|
165
|
+
coef * Math.exp(Complex(0.0,rate*i))
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def new_mixer mix, size
|
170
|
+
size ||= 1
|
171
|
+
return [Complex(1.0,1.0)/Complex(1.0,1.0).abs,
|
172
|
+
Math.exp(Complex(0.0, PI2 * mix * size))]
|
173
|
+
end
|
174
|
+
|
32
175
|
end
|
33
176
|
|
34
|
-
module
|
35
|
-
include
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
if @dec_pos == 0
|
45
|
-
@dec_pos = @dec_size
|
46
|
-
iq = @fir_buf.mul_accum @fir_coef[@fir_size-@fir_pos..-1-@fir_pos],0
|
47
|
-
yield iq[0]
|
177
|
+
module Fir
|
178
|
+
include SetupFir
|
179
|
+
module Complex
|
180
|
+
def call data
|
181
|
+
out = NArray.scomplex data.size
|
182
|
+
data.size.times do |i|
|
183
|
+
@decimation_fir_pos = @decimation_fir_size if @decimation_fir_pos == 0
|
184
|
+
@decimation_fir_pos -= 1
|
185
|
+
@decimation_buf[@decimation_fir_pos] = data[i..i]
|
186
|
+
out[i] = @decimation_fir_coef[@decimation_fir_pos].mul_accum @decimation_buf, 0
|
48
187
|
end
|
188
|
+
yield out
|
49
189
|
end
|
50
190
|
end
|
51
191
|
end
|
52
192
|
|
53
|
-
module
|
54
|
-
include
|
193
|
+
module Mix
|
194
|
+
include SetupFir
|
195
|
+
def call data, &block
|
196
|
+
call! data.dup, &block
|
197
|
+
end
|
198
|
+
def call! data
|
199
|
+
@decimation_phase /= @decimation_phase.abs
|
200
|
+
yield(data.collect! do |v|
|
201
|
+
v * @decimation_phase *= @decimation_inc
|
202
|
+
end)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
module MixFir
|
207
|
+
include SetupFir
|
208
|
+
def call data, &block
|
209
|
+
call! data.dup, &block
|
210
|
+
end
|
211
|
+
def call! data
|
212
|
+
@decimation_phase /= @decimation_phase.abs
|
213
|
+
data.collect! do |value|
|
214
|
+
@decimation_buf[@decimation_fir_pos] = value
|
215
|
+
@decimation_fir_pos += 1
|
216
|
+
@decimation_fir_pos = 0 if @decimation_fir_pos == @decimation_fir_size
|
217
|
+
@decimation_phase *= @decimation_inc
|
218
|
+
value = @decimation_fir_coef[@decimation_fir_pos].mul_accum @decimation_buf, 0
|
219
|
+
value[0] * @decimation_phase
|
220
|
+
end
|
221
|
+
yield data
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
module MixDecimateFir
|
226
|
+
include SetupFir
|
55
227
|
def call data
|
56
|
-
|
57
|
-
@
|
58
|
-
|
59
|
-
|
60
|
-
|
228
|
+
data_size = data.size
|
229
|
+
@decimation_phase /= @decimation_phase.abs
|
230
|
+
out_size = data_size / @decimation_size
|
231
|
+
out_size += 1 if @decimation_size - @decimation_pos <= data_size % @decimation_size
|
232
|
+
out = NArray.scomplex out_size
|
233
|
+
out_count = 0
|
234
|
+
i = 0
|
235
|
+
while i < data_size
|
236
|
+
want = (@decimation_size - @decimation_pos).round
|
237
|
+
remaining = data_size - i
|
238
|
+
space = @decimation_fir_size - @decimation_fir_pos
|
239
|
+
actual = [want,remaining,space].min
|
240
|
+
new_fir_pos = @decimation_fir_pos + actual
|
241
|
+
@decimation_buf[@decimation_fir_pos...new_fir_pos] = data[i...i+actual]
|
242
|
+
@decimation_fir_pos = new_fir_pos
|
243
|
+
@decimation_fir_pos = 0 if @decimation_fir_pos == @decimation_fir_size
|
244
|
+
@decimation_pos += actual
|
245
|
+
if @decimation_pos >= @decimation_size
|
246
|
+
@decimation_pos -= @decimation_size
|
247
|
+
j = @decimation_fir_coef[@decimation_fir_pos].mul_accum @decimation_buf, 0
|
248
|
+
out[out_count] = j * @decimation_phase *= @decimation_inc
|
249
|
+
out_count += 1
|
250
|
+
end
|
251
|
+
i += actual
|
252
|
+
end
|
253
|
+
yield out unless out.empty?
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
module InterpolateFir
|
258
|
+
include SetupFir
|
259
|
+
module Float
|
260
|
+
def call data
|
261
|
+
out = NArray.sfloat @interpolation_size, data.size
|
262
|
+
index = 0
|
263
|
+
data.each do |value|
|
264
|
+
@interpolation_buf_f[@interpolation_fir_pos] = value
|
265
|
+
@interpolation_fir_pos += 1
|
266
|
+
@interpolation_fir_pos = 0 if @interpolation_fir_pos == @interpolation_fir_size
|
267
|
+
iq = @interpolation_fir_coef[@interpolation_fir_pos].mul_accum @interpolation_buf_f, 0
|
268
|
+
out[true,index] = iq.reshape!(iq.size).mul!(@interpolation_size)
|
269
|
+
index += 1
|
270
|
+
end
|
271
|
+
yield out.reshape!(out.size)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
module MixInterpolateFir
|
277
|
+
include SetupFir
|
278
|
+
def call data
|
279
|
+
@interpolation_phase /= @interpolation_phase.abs
|
280
|
+
out = NArray.scomplex @interpolation_size, data.size
|
281
|
+
index = 0
|
282
|
+
data.each do |value|
|
283
|
+
@interpolation_buf_c[@interpolation_fir_pos] = value
|
284
|
+
@interpolation_fir_pos += 1
|
285
|
+
@interpolation_fir_pos = 0 if @interpolation_fir_pos == @interpolation_fir_size
|
286
|
+
iq = @interpolation_fir_coef[@interpolation_fir_pos].mul_accum @interpolation_buf_c, 0
|
287
|
+
@interpolation_phase *= @interpolation_inc
|
288
|
+
out[true,index] = iq.reshape!(iq.size).mul!(@interpolation_phase * @interpolation_size)
|
289
|
+
index += 1
|
290
|
+
end
|
291
|
+
yield out.reshape!(out.size)
|
61
292
|
end
|
62
293
|
end
|
63
294
|
|
@@ -0,0 +1,142 @@
|
|
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
|
+
class Filter
|
18
|
+
|
19
|
+
module Iq
|
20
|
+
|
21
|
+
def setup
|
22
|
+
@bins = 512
|
23
|
+
@tries = 5
|
24
|
+
@dc_rate = 0.00001
|
25
|
+
@iq_rate = 0.0001
|
26
|
+
@biasI = 0.0
|
27
|
+
@biasQ = 0.0
|
28
|
+
@gain = 1.0
|
29
|
+
@phase = 0.0
|
30
|
+
@fft = NArray.scomplex @bins
|
31
|
+
@fft_pos = 0
|
32
|
+
end
|
33
|
+
|
34
|
+
module Complex
|
35
|
+
def call data, &block
|
36
|
+
call! data.dup, &block
|
37
|
+
end
|
38
|
+
|
39
|
+
def call! data
|
40
|
+
remove_dc_bias! data
|
41
|
+
collect data
|
42
|
+
adjust! data, @phase, @gain
|
43
|
+
yield data
|
44
|
+
analyze # this is slow
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# This maintains a buffer of recent data
|
49
|
+
# that we can grab for analysis
|
50
|
+
def collect data
|
51
|
+
i = 0
|
52
|
+
data_size = data.size
|
53
|
+
while i < data_size
|
54
|
+
remaining = data_size - i
|
55
|
+
space = @bins - @fft_pos
|
56
|
+
actual = [remaining,space].min
|
57
|
+
new_fft_pos = @fft_pos + actual
|
58
|
+
@fft[@fft_pos...new_fft_pos] = data[i...i+actual]
|
59
|
+
@fft_pos = new_fft_pos
|
60
|
+
if @fft_pos == @bins
|
61
|
+
@fft_pos = 0
|
62
|
+
@next_fft = @fft.dup
|
63
|
+
end
|
64
|
+
i += actual
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Once per call, we will do an FFT to either start a
|
69
|
+
# new round of guessing or try again on the current set.
|
70
|
+
def analyze
|
71
|
+
return unless @cur_fft or @next_fft
|
72
|
+
if !@cur_fft_count or @cur_fft_count > @tries
|
73
|
+
@cur_fft = @next_fft
|
74
|
+
@cur_fft_count = 0
|
75
|
+
fft = @cur_fft.dup
|
76
|
+
adjust! fft, @phase, @gain
|
77
|
+
@cur_fft_best = detect_energies FFTW3::fft(fft)
|
78
|
+
else
|
79
|
+
@cur_fft_count += 1
|
80
|
+
phaseIncrement = @iq_rate * rand_direction
|
81
|
+
gainIncrement = @iq_rate * rand_direction
|
82
|
+
fft = @cur_fft.dup
|
83
|
+
adjust! fft, @phase + phaseIncrement, @gain + gainIncrement
|
84
|
+
det = detect_energies FFTW3::fft(fft)
|
85
|
+
if det > @cur_fft_best
|
86
|
+
@cur_fft_best = det
|
87
|
+
@gain += gainIncrement
|
88
|
+
@phase += phaseIncrement
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
def remove_dc_bias! data
|
95
|
+
data.collect! do |v|
|
96
|
+
temp = @biasI * (1 - @dc_rate) + v.real * @dc_rate
|
97
|
+
@biasI = temp unless temp.nan?
|
98
|
+
real = v.real - @biasQ
|
99
|
+
temp = @biasQ * (1 - @dc_rate) + v.imag * @dc_rate
|
100
|
+
@biasQ = temp unless temp.nan?
|
101
|
+
imag = v.imag - @biasQ
|
102
|
+
Complex(real,imag)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def adjust! data, phase, gain
|
107
|
+
data.collect! do |v|
|
108
|
+
Complex(v.real + phase * v.imag, v.imag * gain)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def detect_energies spectrum
|
113
|
+
result = 0.0
|
114
|
+
length = @bins.size
|
115
|
+
halfLength = length / 2
|
116
|
+
start = (0.20 * halfLength).round
|
117
|
+
finish = (0.90 * halfLength).round
|
118
|
+
spectrum = spectrum.abs
|
119
|
+
frag = spectrum[start..finish]
|
120
|
+
min = frag.min
|
121
|
+
max = frag.max
|
122
|
+
threshold = max - (max-min) * 0.5
|
123
|
+
(start..finish).each do |i|
|
124
|
+
cur = spectrum[length - 1 - i]
|
125
|
+
next unless cur > threshold
|
126
|
+
diff = cur - spectrum[i]
|
127
|
+
next unless diff > 0
|
128
|
+
result += diff
|
129
|
+
end
|
130
|
+
result
|
131
|
+
end
|
132
|
+
|
133
|
+
# this is the IQ detection idea from the sdr# project
|
134
|
+
def rand_direction
|
135
|
+
rand*2-1
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|