radio 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,150 @@
|
|
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
|
+
|
47
|
+
#TODO need a nicer pattern to force JIT compile
|
48
|
+
@bfo_filter.call(NArray.scomplex(1)) {}
|
49
|
+
@af_filter.call(NArray.sfloat(1)) {}
|
50
|
+
@agc.call(NArray.sfloat(1)) {}
|
51
|
+
@iq.call(NArray.scomplex(1)) {}
|
52
|
+
|
53
|
+
register @af_queue = Queue.new
|
54
|
+
@af_thread = Thread.new &method(:af_thread)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
old_af_thread.join if old_af_thread
|
58
|
+
end
|
59
|
+
|
60
|
+
def tune= freq
|
61
|
+
@ssb_semaphore.synchronize do
|
62
|
+
return unless @af
|
63
|
+
@bfo_filter.decimation_mix = freq / rate
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def set_lsb
|
68
|
+
@ssb_semaphore.synchronize do
|
69
|
+
@bfo_filter.decimation_fir = @lsb_coef if @bfo_filter
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def set_usb
|
74
|
+
@ssb_semaphore.synchronize do
|
75
|
+
@bfo_filter.decimation_fir = @usb_coef if @bfo_filter
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def af_thread
|
82
|
+
begin
|
83
|
+
loop do
|
84
|
+
in_data = @af_queue.pop
|
85
|
+
@ssb_semaphore.synchronize do
|
86
|
+
if @af_filter and @af
|
87
|
+
@bfo_filter.call(in_data) do |iq|
|
88
|
+
@iq.call!(iq) do |iq|
|
89
|
+
@agc.call(iq.real + iq.imag) do |pcm|
|
90
|
+
@af_filter.call(pcm) do |data|
|
91
|
+
@af.out data
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
rescue Exception => e
|
100
|
+
p "ERROR #{e.message}: #{e.backtrace.first}" #TODO logger
|
101
|
+
raise e
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def bfo_mixer
|
106
|
+
rate = self.rate.to_f
|
107
|
+
decimate = rate / 6000
|
108
|
+
unless decimate == decimate.floor
|
109
|
+
raise "unable to filter #{rate} to 6000"
|
110
|
+
end
|
111
|
+
bands = []
|
112
|
+
bands[0] = 0.0 / rate
|
113
|
+
bands[1] = 2400.0 / rate
|
114
|
+
bands[2] = 2800.0 / rate
|
115
|
+
bands[3] = 0.5
|
116
|
+
taps = kaiser_estimate passband:0.01, stopband:0.01, transition:bands[2]-bands[1]
|
117
|
+
p taps
|
118
|
+
fir1 = firpm numtaps: taps, type: :bandpass,
|
119
|
+
bands: bands, desired: [1,1,0,0], weights: [1,2000]
|
120
|
+
fir2 = firpm numtaps: taps, type: :hilbert,
|
121
|
+
bands: bands, desired: [1,1,0,0], weights: [1,2000]
|
122
|
+
@usb_coef = NArray.scomplex fir1.size
|
123
|
+
@usb_coef[true] = fir1.to_a
|
124
|
+
@usb_coef.imag = fir2.to_a
|
125
|
+
@lsb_coef = @usb_coef.conj
|
126
|
+
Filter.new fir:@usb_coef, decimate:decimate, mix:0
|
127
|
+
end
|
128
|
+
|
129
|
+
def af_generate_filter
|
130
|
+
return nil unless @af
|
131
|
+
bands = [0,nil,nil,0.5]
|
132
|
+
bands[1] = 2400.0 / @af.rate
|
133
|
+
bands[2] = 2800.0 / @af.rate
|
134
|
+
taps = kaiser_estimate passband:0.01, stopband:0.01, transition:bands[2]-bands[1]
|
135
|
+
p taps
|
136
|
+
fir = firpm numtaps: taps, type: :bandpass,
|
137
|
+
bands: bands, desired: [1,1,0,0], weights: [1,1000]
|
138
|
+
interpolate = @af.rate.to_f / 6000
|
139
|
+
unless interpolate == interpolate.floor
|
140
|
+
raise "unable to filter 6000 to #{@af.rate}"
|
141
|
+
end
|
142
|
+
Filter.new fir:fir, interpolate:interpolate
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
@@ -14,7 +14,7 @@
|
|
14
14
|
|
15
15
|
|
16
16
|
class Radio
|
17
|
-
module
|
17
|
+
module Signal
|
18
18
|
|
19
19
|
# Keep this namespace clean because we search inputs by
|
20
20
|
# finding the classes from Radio::Inputs.constants.
|
@@ -23,17 +23,17 @@ class Radio
|
|
23
23
|
# dependencies. This allows us to present a basic debug screen.
|
24
24
|
def self.status
|
25
25
|
s = {}
|
26
|
-
|
26
|
+
constants.collect do |input_name|
|
27
27
|
s[input_name] = eval(input_name.to_s).status
|
28
28
|
end
|
29
29
|
s
|
30
30
|
end
|
31
31
|
|
32
32
|
# Consolidate all sources from all inputs and add the class name to the keys.
|
33
|
-
def self.
|
33
|
+
def self.devices
|
34
34
|
s = {}
|
35
|
-
|
36
|
-
eval(type.to_s).
|
35
|
+
constants.each do |type|
|
36
|
+
eval(type.to_s).devices.each do |id, source|
|
37
37
|
s[[type, id]] = source
|
38
38
|
end
|
39
39
|
end
|
@@ -41,20 +41,24 @@ class Radio
|
|
41
41
|
end
|
42
42
|
|
43
43
|
# You can't new a module so this switches into the specific class.
|
44
|
-
def self.new
|
45
|
-
|
46
|
-
|
44
|
+
def self.new options
|
45
|
+
type = options.delete :type
|
46
|
+
options[:output] = [options[:output]].flatten if options[:output]
|
47
|
+
options[:input] = [options[:input]].flatten if options[:input]
|
48
|
+
unless constants.include? type.to_sym
|
47
49
|
raise NameError, "uninitialized constant Radio::Input::#{type}"
|
48
50
|
end
|
49
|
-
|
51
|
+
device = eval(type.to_s).new options
|
50
52
|
# Ask for and discard the first sample to report errors here
|
51
53
|
begin
|
52
|
-
|
54
|
+
device.out NArray.scomplex(1) if device.output_channels == 2
|
55
|
+
device.out NArray.sfloat(1) if device.output_channels == 1
|
56
|
+
device.in 1 if device.input_channels > 0
|
53
57
|
rescue Exception => e
|
54
|
-
|
58
|
+
device.stop
|
55
59
|
raise e
|
56
60
|
end
|
57
|
-
|
61
|
+
device
|
58
62
|
end
|
59
63
|
|
60
64
|
end
|
@@ -20,13 +20,13 @@ end
|
|
20
20
|
|
21
21
|
|
22
22
|
class Radio
|
23
|
-
module
|
23
|
+
module Signal
|
24
24
|
|
25
25
|
class ALSA
|
26
26
|
|
27
27
|
def self.status
|
28
28
|
if defined? ::ALSA::PCM
|
29
|
-
return "Loaded: %d
|
29
|
+
return "Loaded: %d devices" % devices.count
|
30
30
|
end
|
31
31
|
unless defined? @is_linux
|
32
32
|
@is_linux = (`uname`.strip == 'Linux') rescue false
|
@@ -42,7 +42,7 @@ class Radio
|
|
42
42
|
CP_REGEX = /:\s*(capture|playback)\s*(\d+)\s*$/
|
43
43
|
CD_REGEX = /^(\d+)-(\d+):\s*/
|
44
44
|
|
45
|
-
def self.
|
45
|
+
def self.devices
|
46
46
|
return {} unless defined? ::ALSA::PCM
|
47
47
|
# Funky read to get around old Linux bug
|
48
48
|
# http://kerneltrap.org/mailarchive/git-commits-head/2009/4/17/5510664
|
@@ -63,31 +63,37 @@ class Radio
|
|
63
63
|
result[device] = {
|
64
64
|
name: line,
|
65
65
|
rates: [params.sample_rate],
|
66
|
-
|
66
|
+
input: params.channels,
|
67
|
+
output: 0
|
67
68
|
}
|
68
69
|
end rescue nil
|
69
70
|
end
|
70
71
|
result
|
71
72
|
end
|
72
73
|
|
73
|
-
def initialize
|
74
|
+
def initialize options
|
74
75
|
@stream = ::ALSA::PCM::Capture.new
|
75
|
-
@stream.open id
|
76
|
-
|
77
|
-
|
76
|
+
@stream.open options[:id]
|
77
|
+
if input = options[:input]
|
78
|
+
@channel_i = input[0]
|
79
|
+
@channel_q = input[1]
|
80
|
+
end
|
78
81
|
end
|
79
82
|
|
80
83
|
def rate
|
81
84
|
@stream.hardware_parameters.sample_rate
|
82
85
|
end
|
83
86
|
|
84
|
-
def
|
87
|
+
def input_channels
|
85
88
|
return 2 if @channel_q and @stream.hardware_parameters.channels > 1
|
86
89
|
1
|
87
90
|
end
|
88
91
|
|
89
|
-
def
|
90
|
-
|
92
|
+
def output_channels
|
93
|
+
0
|
94
|
+
end
|
95
|
+
|
96
|
+
def in samples
|
91
97
|
out=nil
|
92
98
|
buf_size = @stream.hw_params.buffer_size_for(samples)
|
93
99
|
FFI::MemoryPointer.new(:char, buf_size) do |buffer|
|
@@ -96,12 +102,13 @@ class Radio
|
|
96
102
|
NArray.to_na(out, NArray::SINT).to_f.div! 32767
|
97
103
|
end
|
98
104
|
stream_channels = @stream.hardware_parameters.channels
|
99
|
-
|
105
|
+
sample_size = buf_size / samples / stream_channels
|
106
|
+
out = case sample_size
|
100
107
|
when 1 then NArray.to_na(out,NArray::BYTE).to_f.collect!{|v|(v-128)/127}
|
101
108
|
when 2 then NArray.to_na(out,NArray::SINT).to_f.div! 32767
|
102
109
|
# when 3 then NArray.to_na(d,NArray::???).to_f.collect!{|v|(v-8388608)/8388607}
|
103
110
|
else
|
104
|
-
raise "Unsupported sample size: #{@
|
111
|
+
raise "Unsupported sample size: #{@sample_size}"
|
105
112
|
end
|
106
113
|
return out if channels == 1 and stream_channels == 1
|
107
114
|
out.reshape! stream_channels, out.size/stream_channels
|
@@ -123,13 +130,3 @@ class Radio
|
|
123
130
|
|
124
131
|
end
|
125
132
|
end
|
126
|
-
|
127
|
-
|
128
|
-
if $0 == __FILE__
|
129
|
-
|
130
|
-
require 'narray'
|
131
|
-
a = Radio::Input::ALSA
|
132
|
-
i = a.new 'default', 44100, 0, nil
|
133
|
-
p i.call 8
|
134
|
-
|
135
|
-
end
|
@@ -0,0 +1,136 @@
|
|
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
|
+
module Signal
|
18
|
+
|
19
|
+
class CoreAudio
|
20
|
+
|
21
|
+
VERSION = '>= 0.0.9'
|
22
|
+
BUFFER = 0.3 #seconds of buffer
|
23
|
+
JITTER = 0.6 #percent of buffer for output adjust
|
24
|
+
|
25
|
+
begin
|
26
|
+
gem 'coreaudio', VERSION
|
27
|
+
require 'coreaudio'
|
28
|
+
rescue LoadError => e
|
29
|
+
@bad_coreaudio = gem 'coreaudio'
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.status
|
33
|
+
if defined? ::CoreAudio
|
34
|
+
return "Loaded: %d devices" % devices.count
|
35
|
+
end
|
36
|
+
unless defined? @is_darwin
|
37
|
+
@is_darwin = (`uname`.strip == 'Darwin') rescue false
|
38
|
+
end
|
39
|
+
return 'Unsupported: requires Apple OS' unless @is_darwin
|
40
|
+
return 'Unavailable: gem update coreaudio '+ VERSION.dump if @bad_coreaudio
|
41
|
+
return 'Unavailable: gem install coreaudio'
|
42
|
+
end
|
43
|
+
|
44
|
+
# I don't see a way to automatically set the CoreAudio CODEC rate.
|
45
|
+
# We'll present the nominal_rate as the only option.
|
46
|
+
def self.devices
|
47
|
+
return {} unless defined? ::CoreAudio
|
48
|
+
result = {}
|
49
|
+
::CoreAudio.devices.each do |dev|
|
50
|
+
result[dev.devid] = {
|
51
|
+
name: dev.name,
|
52
|
+
rates: [dev.nominal_rate.to_i],
|
53
|
+
input: dev.input_stream.channels,
|
54
|
+
output: dev.output_stream.channels,
|
55
|
+
}
|
56
|
+
end
|
57
|
+
result
|
58
|
+
end
|
59
|
+
|
60
|
+
attr_reader :input_channels, :output_channels
|
61
|
+
|
62
|
+
def initialize options
|
63
|
+
@device = ::CoreAudio::AudioDevice.new options[:id].to_i
|
64
|
+
@input_channels = @output_channels = 0
|
65
|
+
if @input = options[:input]
|
66
|
+
@input_channels = @input.size
|
67
|
+
buffer_size = @input_channels * rate * BUFFER
|
68
|
+
@input_buf = @device.input_buffer buffer_size
|
69
|
+
@input_buf.start
|
70
|
+
end
|
71
|
+
if @output = options[:output]
|
72
|
+
@output_channels = @output.size
|
73
|
+
buffer_size = @output_channels * rate * BUFFER
|
74
|
+
@output_buf = @device.output_buffer buffer_size
|
75
|
+
@output_buf.start
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# This is called on its own thread in Rig and is expected to block.
|
80
|
+
def in samples
|
81
|
+
# CoreAudio range of -32767..32767 makes easy conversion to -1.0..1.0
|
82
|
+
if @input_channels == 1
|
83
|
+
@input_buf.read(samples)[@input[0],true].to_f.div!(32767)
|
84
|
+
else
|
85
|
+
b = @input_buf.read samples
|
86
|
+
c_out = NArray.scomplex samples
|
87
|
+
c_out[0..-1] = b[@input[0],true].to_f.div!(32767)
|
88
|
+
c_out.imag = b[@input[1],true].to_f.div!(32767)
|
89
|
+
c_out
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def out data
|
94
|
+
resetting = false
|
95
|
+
if @output_buf.dropped_frame > 0
|
96
|
+
p 'filling because frame dropped' #TODO logger
|
97
|
+
resetting = true
|
98
|
+
filler = rate * BUFFER * JITTER - data.size
|
99
|
+
@output_buf << NArray.sfloat(filler) if filler > 0
|
100
|
+
end
|
101
|
+
output_buf_space = @output_buf.space
|
102
|
+
if output_buf_space < data.size
|
103
|
+
p 'dropping some data' #TODO logger
|
104
|
+
# we'll drop all the data that won't fit
|
105
|
+
@drop_data = data.size - output_buf_space
|
106
|
+
# plus enough data to swing back
|
107
|
+
@drop_data += rate * BUFFER * JITTER
|
108
|
+
end
|
109
|
+
out = nil
|
110
|
+
if @drop_data
|
111
|
+
if @drop_data < data.size
|
112
|
+
out = data[@drop_data..-1] * 32767
|
113
|
+
end
|
114
|
+
@drop_data -= data.size
|
115
|
+
@drop_data = nil if @drop_data <= 0
|
116
|
+
else
|
117
|
+
out = data * 32767
|
118
|
+
end
|
119
|
+
@output_buf << out if out
|
120
|
+
@output_buf.reset_dropped_frame if resetting
|
121
|
+
end
|
122
|
+
|
123
|
+
# Once stopped, rig won't attempt starting again on this object.
|
124
|
+
def stop
|
125
|
+
@input_buf.stop if @input_buf
|
126
|
+
@output_buf.stop if @output_buf
|
127
|
+
end
|
128
|
+
|
129
|
+
def rate
|
130
|
+
@device.nominal_rate
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
end
|
@@ -15,36 +15,37 @@
|
|
15
15
|
|
16
16
|
|
17
17
|
class Radio
|
18
|
-
module
|
18
|
+
module Signal
|
19
19
|
class File
|
20
20
|
|
21
21
|
def self.status
|
22
|
-
"Loaded: %d files found" %
|
22
|
+
"Loaded: %d files found" % devices.size
|
23
23
|
end
|
24
24
|
|
25
|
-
def self.
|
25
|
+
def self.devices
|
26
26
|
result = {}
|
27
27
|
files = Dir.glob ::File.expand_path '../../../../test/wav/**/*.wav', __FILE__
|
28
28
|
files.each do |file|
|
29
29
|
begin
|
30
|
-
f = new file, 0,
|
30
|
+
f = new id:file, input:[0,1]
|
31
31
|
rescue Exception => e
|
32
32
|
next
|
33
33
|
end
|
34
34
|
result[file] = {
|
35
35
|
name: file,
|
36
36
|
rates: [f.rate],
|
37
|
-
|
37
|
+
input: f.input_channels,
|
38
|
+
output: 0
|
38
39
|
}
|
39
40
|
end
|
40
41
|
result
|
41
42
|
end
|
42
43
|
|
43
44
|
# You can load any file, not just the ones in sources.
|
44
|
-
def initialize
|
45
|
+
def initialize options
|
45
46
|
self.class.constants.each do |x|
|
46
47
|
klass = eval x.to_s
|
47
|
-
@reader = klass.new
|
48
|
+
@reader = klass.new options
|
48
49
|
break if @reader
|
49
50
|
end
|
50
51
|
raise 'Unknown format' unless @reader
|
@@ -57,4 +58,3 @@ class Radio
|
|
57
58
|
end
|
58
59
|
end
|
59
60
|
end
|
60
|
-
|