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.
@@ -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 Input
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
- Radio::Input.constants.collect do |input_name|
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.sources
33
+ def self.devices
34
34
  s = {}
35
- Radio::Input.constants.each do |type|
36
- eval(type.to_s).sources.each do |id, source|
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 type, id, rate, channel_i, channel_q=nil
45
- # defend the eval
46
- unless Radio::Input.constants.include? type.to_sym
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
- input = eval(type.to_s).new id, rate, channel_i, channel_q
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
- input.call 1
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
- input.stop
58
+ device.stop
55
59
  raise e
56
60
  end
57
- input
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 Input
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 input devices" % sources.count
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.sources
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
- channels: params.channels
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 id, rate, channel_i, channel_q
74
+ def initialize options
74
75
  @stream = ::ALSA::PCM::Capture.new
75
- @stream.open id
76
- @channel_i = channel_i
77
- @channel_q = channel_q
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 channels
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 call samples
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
- out = case buf_size / samples / stream_channels
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: #{@bit_sample}"
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 Input
18
+ module Signal
19
19
  class File
20
20
 
21
21
  def self.status
22
- "Loaded: %d files found" % sources.size
22
+ "Loaded: %d files found" % devices.size
23
23
  end
24
24
 
25
- def self.sources
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, 0, 1
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
- channels: f.channels
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 id, rate, channel_i, channel_q
45
+ def initialize options
45
46
  self.class.constants.each do |x|
46
47
  klass = eval x.to_s
47
- @reader = klass.new id, rate, channel_i, channel_q rescue nil
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
-