radio 0.0.1 → 0.0.2

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