radio 0.0.1 → 0.0.2

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,43 @@
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
+ # This is unfinished, unused, and needs expiration management.
17
+ # We'll definitely want sessions working for authentication,
18
+
19
+ class Radio
20
+ class HTTP
21
+ class Session
22
+
23
+ CODES = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a
24
+ COOKIE_KEY = 'ham21-radio-session'
25
+
26
+ def self.prepare request, response
27
+ @sessions ||= {}
28
+ session_id = request.cookies[COOKIE_KEY]
29
+ session = @sessions[session_id]
30
+ unless session
31
+ session_id = (0...24).collect{CODES.sample}.join
32
+ session = @sessions[session_id] = new
33
+ Rack::Utils.set_cookie_header!(response.headers, COOKIE_KEY, session_id)
34
+ end
35
+ session
36
+ end
37
+
38
+ def initialize
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,62 @@
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 Input
18
+
19
+ # Keep this namespace clean because we search inputs by
20
+ # finding the classes from Radio::Inputs.constants.
21
+
22
+ # Drivers are supposed to fail silently if they can't find
23
+ # dependencies. This allows us to present a basic debug screen.
24
+ def self.status
25
+ s = {}
26
+ Radio::Input.constants.collect do |input_name|
27
+ s[input_name] = eval(input_name.to_s).status
28
+ end
29
+ s
30
+ end
31
+
32
+ # Consolidate all sources from all inputs and add the class name to the keys.
33
+ def self.sources
34
+ s = {}
35
+ Radio::Input.constants.each do |type|
36
+ eval(type.to_s).sources.each do |id, source|
37
+ s[[type, id]] = source
38
+ end
39
+ end
40
+ s
41
+ end
42
+
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
47
+ raise NameError, "uninitialized constant Radio::Input::#{type}"
48
+ end
49
+ input = eval(type.to_s).new id, rate, channel_i, channel_q
50
+ # Ask for and discard the first sample to report errors here
51
+ begin
52
+ input.call 1
53
+ rescue Exception => e
54
+ input.stop
55
+ raise e
56
+ end
57
+ input
58
+ end
59
+
60
+ end
61
+ end
62
+
@@ -0,0 +1,135 @@
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
+ begin
17
+ require 'alsa'
18
+ rescue LoadError => e
19
+ end
20
+
21
+
22
+ class Radio
23
+ module Input
24
+
25
+ class ALSA
26
+
27
+ def self.status
28
+ if defined? ::ALSA::PCM
29
+ return "Loaded: %d input devices" % sources.count
30
+ end
31
+ unless defined? @is_linux
32
+ @is_linux = (`uname`.strip == 'Linux') rescue false
33
+ end
34
+ return "Unsupported: requires Linux" unless @is_linux
35
+ if defined? ::ALSA
36
+ 'Unavailable: install ALSA to your OS'
37
+ else
38
+ 'Unavailable: gem install ruby-alsa'
39
+ end
40
+ end
41
+
42
+ CP_REGEX = /:\s*(capture|playback)\s*(\d+)\s*$/
43
+ CD_REGEX = /^(\d+)-(\d+):\s*/
44
+
45
+ def self.sources
46
+ return {} unless defined? ::ALSA::PCM
47
+ # Funky read to get around old Linux bug
48
+ # http://kerneltrap.org/mailarchive/git-commits-head/2009/4/17/5510664
49
+ pcm = ::File.open('/proc/asound/pcm') do |io|
50
+ io.read_nonblock 32768
51
+ end
52
+ result = {}
53
+ pcm.each_line do |line|
54
+ capture = 0
55
+ 2.times do
56
+ line.gsub! CP_REGEX, ''
57
+ capture = $2.to_i if $1 == 'capture'
58
+ end
59
+ line.gsub! CD_REGEX, ''
60
+ device = "hw:#{$1.to_i},#{$2.to_i}"
61
+ ::ALSA::PCM::Capture.open(device) do |stream|
62
+ params = stream.hardware_parameters
63
+ result[device] = {
64
+ name: line,
65
+ rates: [params.sample_rate],
66
+ channels: params.channels
67
+ }
68
+ end rescue nil
69
+ end
70
+ result
71
+ end
72
+
73
+ def initialize id, rate, channel_i, channel_q
74
+ @stream = ::ALSA::PCM::Capture.new
75
+ @stream.open id
76
+ @channel_i = channel_i
77
+ @channel_q = channel_q
78
+ end
79
+
80
+ def rate
81
+ @stream.hardware_parameters.sample_rate
82
+ end
83
+
84
+ def channels
85
+ return 2 if @channel_q and @stream.hardware_parameters.channels > 1
86
+ 1
87
+ end
88
+
89
+ def call samples
90
+
91
+ out=nil
92
+ buf_size = @stream.hw_params.buffer_size_for(samples)
93
+ FFI::MemoryPointer.new(:char, buf_size) do |buffer|
94
+ @stream.read_buffer buffer, samples
95
+ out = buffer.read_string(buf_size)
96
+ NArray.to_na(out, NArray::SINT).to_f.div! 32767
97
+ end
98
+ stream_channels = @stream.hardware_parameters.channels
99
+ out = case buf_size / samples / stream_channels
100
+ when 1 then NArray.to_na(out,NArray::BYTE).to_f.collect!{|v|(v-128)/127}
101
+ when 2 then NArray.to_na(out,NArray::SINT).to_f.div! 32767
102
+ # when 3 then NArray.to_na(d,NArray::???).to_f.collect!{|v|(v-8388608)/8388607}
103
+ else
104
+ raise "Unsupported sample size: #{@bit_sample}"
105
+ end
106
+ return out if channels == 1 and stream_channels == 1
107
+ out.reshape! stream_channels, out.size/stream_channels
108
+ if channels == 1
109
+ out[@channel_i,true]
110
+ else
111
+ c_out = NArray.scomplex out[0,true].size
112
+ c_out[0..-1] = out[@channel_i,true]
113
+ c_out.imag = out[@channel_q,true]
114
+ c_out
115
+ end
116
+ end
117
+
118
+ def stop
119
+ @stream.close
120
+ end
121
+
122
+ end
123
+
124
+ end
125
+ 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,102 @@
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
+ begin
17
+ require 'coreaudio'
18
+ rescue LoadError => e
19
+ end
20
+
21
+
22
+ class Radio
23
+ module Input
24
+
25
+ class CoreAudio
26
+
27
+ def self.status
28
+ if defined? ::CoreAudio
29
+ return "Loaded: %d input devices" % sources.count
30
+ end
31
+ unless defined? @is_darwin
32
+ @is_darwin = (`uname`.strip == 'Darwin') rescue false
33
+ end
34
+ return "Unsupported: requires Apple OS" unless @is_darwin
35
+ return 'Unavailable: gem install coreaudio'
36
+ end
37
+
38
+ # I don't see a way to automatically set the CoreAudio CODEC rate.
39
+ # We'll present the nominal_rate as the only option.
40
+ def self.sources
41
+ return {} unless defined? ::CoreAudio
42
+ result = {}
43
+ ::CoreAudio.devices.each do |dev|
44
+ channels = dev.input_stream.channels
45
+ if channels > 0
46
+ result[dev.devid] = {
47
+ name: dev.name,
48
+ rates: [dev.nominal_rate.to_i],
49
+ channels: channels
50
+ }
51
+ end
52
+ end
53
+ result
54
+ end
55
+
56
+ attr_reader :rate, :channels
57
+
58
+ # id is the key from the sources hash.
59
+ # rate is the desired hardware rate. do not decimate/interpolate here.
60
+ def initialize id, rate, channel_i, channel_q
61
+ @device = ::CoreAudio::AudioDevice.new id.to_i
62
+ @rate = @device.nominal_rate
63
+ raise 'I channel fail' unless channel_i < @device.input_stream.channels
64
+ @channel_i = channel_i
65
+ @channels = 1
66
+ if channel_q
67
+ raise 'Q channel fail' unless channel_q < @device.input_stream.channels
68
+ @channel_q = channel_q
69
+ extend IQ
70
+ @channels = 2
71
+ end
72
+ # Half second of buffer
73
+ coreaudio_input_buffer_size = @channels * rate / 2
74
+ @buf = @device.input_buffer coreaudio_input_buffer_size
75
+ @buf.start
76
+ end
77
+
78
+ # This is called on its own thread in Rig and is expected to block.
79
+ def call samples
80
+ # CoreAudio range of -32767..32767 makes easy conversion to -1.0..1.0
81
+ @buf.read(samples)[@channel_i,true].to_f.div!(32767)
82
+ end
83
+
84
+ # Once stopped, rig won't attempt starting again on this object.
85
+ def stop
86
+ @buf.stop
87
+ end
88
+
89
+ module IQ
90
+ def call samples
91
+ b = @buf.read samples
92
+ c_out = NArray.scomplex samples
93
+ c_out[0..-1] = b[@channel_i,true].to_f.div!(32767)
94
+ c_out.imag = b[@channel_q,true].to_f.div!(32767)
95
+ c_out
96
+ end
97
+ end
98
+
99
+ end
100
+
101
+ end
102
+ end
@@ -0,0 +1,60 @@
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
+
17
+ class Radio
18
+ module Input
19
+ class File
20
+
21
+ def self.status
22
+ "Loaded: %d files found" % sources.size
23
+ end
24
+
25
+ def self.sources
26
+ result = {}
27
+ files = Dir.glob ::File.expand_path '../../../../test/wav/**/*.wav', __FILE__
28
+ files.each do |file|
29
+ begin
30
+ f = new file, 0, 0, 1
31
+ rescue Exception => e
32
+ next
33
+ end
34
+ result[file] = {
35
+ name: file,
36
+ rates: [f.rate],
37
+ channels: f.channels
38
+ }
39
+ end
40
+ result
41
+ end
42
+
43
+ # You can load any file, not just the ones in sources.
44
+ def initialize id, rate, channel_i, channel_q
45
+ self.class.constants.each do |x|
46
+ klass = eval x.to_s
47
+ @reader = klass.new id, rate, channel_i, channel_q rescue nil
48
+ break if @reader
49
+ end
50
+ raise 'Unknown format' unless @reader
51
+ end
52
+
53
+ def method_missing meth, *args, &block
54
+ @reader.send meth, *args, &block
55
+ end
56
+
57
+ end
58
+ end
59
+ end
60
+
@@ -0,0 +1,124 @@
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
+
17
+ class Radio
18
+ module Input
19
+ class File
20
+ class WAV
21
+
22
+ attr_reader :rate
23
+
24
+ def initialize id, rate, channel_i, channel_q
25
+ @file = ::File.new id
26
+ #TODO validate header instead?
27
+ @file.read 12 # discard header
28
+ @rate = rate
29
+ @channel_i = channel_i
30
+ @channel_q = channel_q
31
+ @data = [next_data]
32
+ @time = Time.now
33
+ end
34
+
35
+ def call samples
36
+ sample_size = @channels * (@bit_sample/8)
37
+ @time += 1.0/(rate/samples)
38
+ sleep [0,@time-Time.now].max
39
+ while @data.reduce(0){|a,b|a+b.size} < samples * sample_size
40
+ @data.push next_data
41
+ end
42
+ if channels > 1
43
+ out = NArray.scomplex samples
44
+ else
45
+ out = NArray.sfloat samples
46
+ end
47
+ i = 0
48
+ while i < samples
49
+ if @data.first.size/sample_size > samples-i
50
+ out[i..-1] = convert @data.first[0...(samples-i)*sample_size]
51
+ @data[0] = @data.first[(samples-i)*sample_size..-1]
52
+ i = samples
53
+ else
54
+ converted_data = convert @data.shift
55
+ out[i...(i+converted_data.size)] = converted_data
56
+ i += converted_data.size
57
+ end
58
+ end
59
+ out
60
+ end
61
+
62
+ def channels
63
+ return 2 if @channel_q and @channels > 1
64
+ 1
65
+ end
66
+
67
+ def stop
68
+ @file.close
69
+ end
70
+
71
+ private
72
+
73
+ def convert d
74
+ out = case @bit_sample
75
+ when 8 then NArray.to_na(d,NArray::BYTE).to_f.collect!{|v|(v-128)/127}
76
+ when 16 then NArray.to_na(d,NArray::SINT).to_f.div! 32767
77
+ # when 24 then NArray.to_na(d,NArray::???).to_f.collect!{|v|(v-8388608)/8388607}
78
+ else
79
+ raise "Unsupported sample size: #{@bit_sample}"
80
+ end
81
+ return out if channels == 1 and @channels == 1
82
+ out.reshape! @channels, out.size/@channels
83
+ if channels == 1
84
+ out[@channel_i,true]
85
+ else
86
+ c_out = NArray.scomplex out[0,true].size
87
+ c_out[0..-1] = out[@channel_i,true]
88
+ c_out.imag = out[@channel_q,true]
89
+ c_out
90
+ end
91
+ end
92
+
93
+ #TODO read data in chunks smaller than size (which is often the whole file)
94
+ def next_data
95
+ loop do
96
+ until @file.eof?
97
+ type = @file.read(4)
98
+ size = @file.read(4).unpack("V")[0].to_i
99
+ case type
100
+ when 'fmt '
101
+ fmt = @file.read(size)
102
+ @id = fmt.slice(0,2).unpack('c')[0]
103
+ @channels = fmt.slice(2,2).unpack('c')[0]
104
+ @rate = fmt.slice(4,4).unpack('V').join.to_i
105
+ @byte_sec = fmt.slice(8,4).unpack('V').join.to_i
106
+ @block_size = fmt.slice(12,2).unpack('c')[0]
107
+ @bit_sample = fmt.slice(14,2).unpack('c')[0]
108
+ next
109
+ when 'data'
110
+ return @file.read size
111
+ else
112
+ raise "Unknown GIF type: #{type}"
113
+ end
114
+ end
115
+ @file.rewind
116
+ @file.read 12 # discard header
117
+ end
118
+ end
119
+
120
+ end
121
+ end
122
+ end
123
+ end
124
+