radio 0.0.2 → 0.0.3

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