radio 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +202 -0
- data/README.md +67 -1
- data/bin/radio-server +8 -0
- data/lib/radio.rb +41 -1
- data/lib/radio/controls/null.rb +32 -0
- data/lib/radio/controls/si570avr.rb +77 -0
- data/lib/radio/filter.rb +69 -0
- data/lib/radio/filters/fir.rb +65 -0
- data/lib/radio/gif.rb +84 -0
- data/lib/radio/http/file.rb +88 -0
- data/lib/radio/http/script.rb +133 -0
- data/lib/radio/http/server.rb +94 -0
- data/lib/radio/http/session.rb +43 -0
- data/lib/radio/input.rb +62 -0
- data/lib/radio/inputs/alsa.rb +135 -0
- data/lib/radio/inputs/coreaudio.rb +102 -0
- data/lib/radio/inputs/file.rb +60 -0
- data/lib/radio/inputs/wav.rb +124 -0
- data/lib/radio/psk31/bit_detect.rb +19 -4
- data/lib/radio/psk31/decoder.rb +18 -3
- data/lib/radio/psk31/fir_coef.rb +6 -1
- data/lib/radio/psk31/rx.rb +27 -32
- data/lib/radio/psk31/varicode.rb +15 -0
- data/lib/radio/rig.rb +46 -0
- data/lib/radio/rig/lo.rb +57 -0
- data/lib/radio/rig/rx.rb +61 -0
- data/lib/radio/rig/spectrum.rb +96 -0
- data/lib/radio/version.rb +3 -0
- data/test/test.rb +76 -0
- data/test/wav/bpsk8k.wav +0 -0
- data/test/wav/qpsk8k.wav +0 -0
- data/test/wav/ssb.wav +0 -0
- data/www/index.erb +40 -0
- data/www/jquery-1.7.js +9300 -0
- data/www/lo.erb +21 -0
- data/www/setup/input.erb +64 -0
- data/www/setup/lo.erb +36 -0
- data/www/waterfall.erb +20 -0
- metadata +77 -9
- data/lib/radio/psk31/filters.rb +0 -220
@@ -0,0 +1,65 @@
|
|
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
|
+
class Filter
|
18
|
+
|
19
|
+
module FirSetup
|
20
|
+
# It's ok that not all filter patterns use all instance variables.
|
21
|
+
def setup data
|
22
|
+
@mix_phase = 0.0
|
23
|
+
@mix_phase_inc = @options[:mix]
|
24
|
+
@dec_pos = @dec_size = @options[:decimate]
|
25
|
+
coef = @options[:fir]
|
26
|
+
@fir_pos = 0
|
27
|
+
@fir_size = coef.size
|
28
|
+
@fir_coef = NArray.to_na coef.reverse*2
|
29
|
+
@fir_buf = NArray.complex @fir_size
|
30
|
+
super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
module FloatEachMixDecimateFir
|
35
|
+
include FirSetup
|
36
|
+
def call data
|
37
|
+
data.each do |energy|
|
38
|
+
@fir_pos = @fir_size if @fir_pos == 0
|
39
|
+
@fir_pos -= 1
|
40
|
+
@fir_buf[@fir_pos] = Complex(Math.cos(@mix_phase)*energy, -Math.sin(@mix_phase)*energy)
|
41
|
+
@mix_phase += @mix_phase_inc
|
42
|
+
@mix_phase -= PI2 if @mix_phase >= PI2
|
43
|
+
@dec_pos -= 1
|
44
|
+
if @dec_pos == 0
|
45
|
+
@dec_pos = @dec_size
|
46
|
+
iq = @fir_buf.mul_accum @fir_coef[@fir_size-@fir_pos..-1-@fir_pos],0
|
47
|
+
yield iq[0]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
module ComplexFir
|
54
|
+
include FirSetup
|
55
|
+
def call data
|
56
|
+
@fir_pos = @fir_size if @fir_pos == 0
|
57
|
+
@fir_pos -= 1
|
58
|
+
@fir_buf[@fir_pos] = data
|
59
|
+
iq = @fir_buf.mul_accum @fir_coef[@fir_size-@fir_pos..-1-@fir_pos],0
|
60
|
+
yield iq[0]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
data/lib/radio/gif.rb
ADDED
@@ -0,0 +1,84 @@
|
|
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
|
+
# 2D Array of numbers will be mapped to 1..127 colors.
|
17
|
+
# This 7-bit data is encoded uncompressed and left for
|
18
|
+
# http to compress. Background color reserved for future use.
|
19
|
+
|
20
|
+
class Radio
|
21
|
+
class Gif
|
22
|
+
|
23
|
+
# This is an optimized GIF generator for waterfalls.
|
24
|
+
# It requires 128 RGB colors, the first is unused and
|
25
|
+
# reserved for transparency if we ever need it.
|
26
|
+
def self.waterfall colors, data
|
27
|
+
|
28
|
+
gif = [
|
29
|
+
'GIF87a', # Start Header
|
30
|
+
data[0].size, # width
|
31
|
+
data.size, # height
|
32
|
+
0xF6, # 128 24-bit colors
|
33
|
+
0x00, # background color index
|
34
|
+
0x00 # aspect ratio
|
35
|
+
].pack 'a6vvCCC'
|
36
|
+
|
37
|
+
gif += colors.flatten.pack 'C*'
|
38
|
+
|
39
|
+
gif += [
|
40
|
+
0x2C, # Start Image Block
|
41
|
+
0x0000, # Left position
|
42
|
+
0x0000, # Top position
|
43
|
+
data[0].size, # width
|
44
|
+
data.size, # height
|
45
|
+
0x00, # No color table, not interlaced
|
46
|
+
0x07 # LZW code size
|
47
|
+
].pack('CvvvvCC')
|
48
|
+
|
49
|
+
data.each_with_index do |vals, row|
|
50
|
+
col = 0
|
51
|
+
min = vals.min
|
52
|
+
range = [1e-99, vals.max - min].max
|
53
|
+
while col < vals.size
|
54
|
+
# Uncompressed GIF trickery avoids bit packing too
|
55
|
+
# 126 byte chunks with reset keeps LZW in 8 bit codes
|
56
|
+
col_end = [col+126,vals.size].min
|
57
|
+
slice = vals.slice(col...col_end).to_a
|
58
|
+
# This 126 because palette is 1..127
|
59
|
+
slice = slice.collect { |x| (x - min) / range * 126 + 1 }
|
60
|
+
slice = slice.pack 'C*'
|
61
|
+
newstuff = [
|
62
|
+
slice.size+1,
|
63
|
+
slice,
|
64
|
+
0x80 # LZW reset
|
65
|
+
].pack('Ca*C')
|
66
|
+
gif += newstuff
|
67
|
+
col += 126
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
gif += [
|
73
|
+
0x01, # end image blocks
|
74
|
+
0x81, # final image block: LZW end
|
75
|
+
0x00, # end image blocks
|
76
|
+
0x3B # end gif
|
77
|
+
].pack('C*')
|
78
|
+
|
79
|
+
return gif
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,88 @@
|
|
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
|
+
class HTTP
|
18
|
+
class FileResponse
|
19
|
+
|
20
|
+
def initialize(env, filename, content_type = nil)
|
21
|
+
@env = env
|
22
|
+
@filename = filename
|
23
|
+
@status = 200
|
24
|
+
@headers = {}
|
25
|
+
@body = []
|
26
|
+
|
27
|
+
begin
|
28
|
+
raise Errno::EPERM unless File.file?(filename) and File.readable?(filename)
|
29
|
+
rescue SystemCallError
|
30
|
+
@body = ["404 Not Found\n"]
|
31
|
+
@headers["Content-Length"] = @body.first.size.to_s
|
32
|
+
@headers["Content-Type"] = 'text/plain'
|
33
|
+
@headers["X-Cascade"] = 'pass'
|
34
|
+
@status = 404
|
35
|
+
return
|
36
|
+
end
|
37
|
+
|
38
|
+
# Caching strategy
|
39
|
+
mod_since = Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']) rescue nil
|
40
|
+
last_modified = File.mtime(filename)
|
41
|
+
@status = 304 and return if last_modified == mod_since
|
42
|
+
@headers["Last-Modified"] = last_modified.httpdate
|
43
|
+
if env['QUERY_STRING'] =~ /^[0-9]{9,10}$/ and last_modified == Time.at(env['QUERY_STRING'].to_i)
|
44
|
+
@headers["Cache-Control"] = 'max-age=86400, public' # one day
|
45
|
+
else
|
46
|
+
@headers["Cache-Control"] = 'max-age=0, private, must-revalidate'
|
47
|
+
end
|
48
|
+
|
49
|
+
# Sending the file or reading an unknown length stream to send
|
50
|
+
@body = self
|
51
|
+
unless size = File.size?(filename)
|
52
|
+
@body = [File.read(filename)]
|
53
|
+
size = @body.first.respond_to?(:bytesize) ? @body.first.bytesize : @body.first.size
|
54
|
+
end
|
55
|
+
@headers["Content-Length"] = size.to_s
|
56
|
+
@headers["Content-Type"] = content_type || Rack::Mime.mime_type(File.extname(filename), 'text/plain')
|
57
|
+
end
|
58
|
+
|
59
|
+
# Support using self as a response body.
|
60
|
+
# @yield [String] 8k blocks
|
61
|
+
def each
|
62
|
+
File.open(@filename, "rb") do |file|
|
63
|
+
while part = file.read(8192)
|
64
|
+
yield part
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Filename attribute.
|
70
|
+
# Alias is used by some rack servers to detach from Ruby early.
|
71
|
+
# @return [String]
|
72
|
+
attr_reader :filename
|
73
|
+
alias :to_path :filename
|
74
|
+
|
75
|
+
# Was the file in the system and ready to be served?
|
76
|
+
def found?
|
77
|
+
@status == 200 or @status == 304
|
78
|
+
end
|
79
|
+
|
80
|
+
# Present the final response for rack.
|
81
|
+
# @return (Array)[status, headers, body]
|
82
|
+
def finish
|
83
|
+
[@status, @headers, @body]
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,133 @@
|
|
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
|
+
require 'erb'
|
17
|
+
|
18
|
+
class Radio
|
19
|
+
|
20
|
+
class HTTP
|
21
|
+
|
22
|
+
# A Script instance is the context in which scripts are rendered.
|
23
|
+
# It inherits everything from Rack::Request and supplies a Response instance
|
24
|
+
# you can use for redirects, cookies, and other controller actions.
|
25
|
+
class Script < Rack::Request
|
26
|
+
|
27
|
+
include ERB::Util
|
28
|
+
|
29
|
+
class NotFound < StandardError
|
30
|
+
end
|
31
|
+
|
32
|
+
class RenderStackOverflow < StandardError
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(env, filename)
|
36
|
+
super(env)
|
37
|
+
@render_stack = []
|
38
|
+
@response = original_response = Rack::Response.new
|
39
|
+
rendering = render(filename)
|
40
|
+
if @response == original_response and @response.empty?
|
41
|
+
@response.write rendering
|
42
|
+
end
|
43
|
+
rescue RenderStackOverflow, NotFound => e
|
44
|
+
if @render_stack.size > 0
|
45
|
+
# Make errors appear from the render instead of the engine.call
|
46
|
+
e.set_backtrace e.backtrace[1..-1]
|
47
|
+
raise e
|
48
|
+
end
|
49
|
+
@response.status = 404
|
50
|
+
@response.write "404 Not Found\n"
|
51
|
+
@response.header["X-Cascade"] = "pass"
|
52
|
+
@response.header["Content-Type"] = "text/plain"
|
53
|
+
end
|
54
|
+
|
55
|
+
# After rendering, #finish will be sent to the client.
|
56
|
+
# If you replace the response or add to the response#body,
|
57
|
+
# the script engine rendering will not be added.
|
58
|
+
# @return [Rack::Response]
|
59
|
+
attr_accessor :response
|
60
|
+
|
61
|
+
# An array of filenames representing the current render stack.
|
62
|
+
# @example
|
63
|
+
# <%= if render_stack.size == 1
|
64
|
+
# render 'html_version'
|
65
|
+
# else
|
66
|
+
# render 'included_version'
|
67
|
+
# end
|
68
|
+
# %>
|
69
|
+
# @return [<Array>]
|
70
|
+
attr_reader :render_stack
|
71
|
+
|
72
|
+
# Render another Script.
|
73
|
+
# @example view_test.erb
|
74
|
+
# <%= render 'util/logger_popup' %>
|
75
|
+
# @param (String) filename Relative to current Script.
|
76
|
+
# @param (Hash) locals Local variables for the Script.
|
77
|
+
def render(filename, locals = {})
|
78
|
+
if render_stack.size > 100
|
79
|
+
# Since nobody sane should recurse through here, this mainly
|
80
|
+
# finds a render self that you might get after a copy and paste
|
81
|
+
raise RenderStackOverflow
|
82
|
+
elsif render_stack.size > 0
|
83
|
+
# Hooray for relative paths and easily movable files
|
84
|
+
filename = File.expand_path(filename, File.dirname(render_stack.last))
|
85
|
+
else
|
86
|
+
# Underbar scripts are partials by convention; keep them from rendering at root
|
87
|
+
filename = File.expand_path(filename)
|
88
|
+
raise NotFound if File.basename(filename) =~ /^_/
|
89
|
+
end
|
90
|
+
ext = File.extname(filename)
|
91
|
+
files1 = [filename]
|
92
|
+
files1 << filename + '.html' if ext == ''
|
93
|
+
files1 << filename.sub(/.html$/,'') if ext == '.html'
|
94
|
+
files1.each do |filename1|
|
95
|
+
files2 = [filename1+'.erb']
|
96
|
+
files2 << filename1.gsub(/.html$/, '.erb') if File.extname(filename1) == '.html'
|
97
|
+
unless filename1 =~ /^_/ or render_stack.empty?
|
98
|
+
files2 = files2 + files2.collect {|f| "#{File.dirname(f)}/_#{File.basename(f)}"}
|
99
|
+
end
|
100
|
+
files2.each do |filename2|
|
101
|
+
if File.file?(filename2) and File.readable?(filename2)
|
102
|
+
if render_stack.empty?
|
103
|
+
response.header["Content-Type"] = Rack::Mime.mime_type(File.extname(filename1), 'text/html')
|
104
|
+
end
|
105
|
+
render_stack.push filename2
|
106
|
+
erb = ::ERB.new(File.read(filename2), nil, '-')
|
107
|
+
erb.filename = filename2
|
108
|
+
set_locals = locals.keys.map { |k| "#{k}=locals[#{k.inspect}];" }.join
|
109
|
+
instance_binding = instance_eval{binding}
|
110
|
+
eval set_locals, instance_binding
|
111
|
+
result = erb.result instance_binding
|
112
|
+
render_stack.pop
|
113
|
+
return result
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
raise NotFound
|
118
|
+
end
|
119
|
+
|
120
|
+
# Helper for finding files relative to Scripts.
|
121
|
+
# @param [String] filename
|
122
|
+
# @return [String] absolute filesystem path
|
123
|
+
def expand_path(filename, dir=nil)
|
124
|
+
dir ||= File.dirname render_stack.last
|
125
|
+
File.expand_path filename, dir
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
@@ -0,0 +1,94 @@
|
|
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
|
+
require 'rack'
|
17
|
+
require 'erb'
|
18
|
+
|
19
|
+
class Radio
|
20
|
+
class HTTP
|
21
|
+
|
22
|
+
MOUNTS = [
|
23
|
+
['', File.expand_path(File.join(File.dirname(__FILE__), '../../../www'))]
|
24
|
+
]
|
25
|
+
|
26
|
+
class Server
|
27
|
+
|
28
|
+
ENV_THREAD_BYPASS = 'radio.thread_bypass'
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
@working_dir = Dir.getwd
|
32
|
+
end
|
33
|
+
|
34
|
+
# Thin can run some processes in threads if we provide the logic.
|
35
|
+
# Static files are served without deferring to a thread.
|
36
|
+
# Everything else is tried in the EventMachine thread pool.
|
37
|
+
def deferred? env
|
38
|
+
path_info = Rack::Utils.unescape(env['PATH_INFO'])
|
39
|
+
return false if path_info =~ /\.(\.|erb$)/ # unsafe '..' and '.erb'
|
40
|
+
MOUNTS.each do |path, dir|
|
41
|
+
if path_info =~ %r{^#{Regexp.escape(path)}(/.*|)$}
|
42
|
+
filename = File.join(dir, $1)
|
43
|
+
Dir.chdir @working_dir
|
44
|
+
response = FileResponse.new(env, filename)
|
45
|
+
if !response.found? and File.extname(path_info) == ''
|
46
|
+
response = FileResponse.new(env, filename + '.html')
|
47
|
+
end
|
48
|
+
if response.found?
|
49
|
+
env[ENV_THREAD_BYPASS] = response
|
50
|
+
return false
|
51
|
+
end
|
52
|
+
env[ENV_THREAD_BYPASS] = filename
|
53
|
+
end
|
54
|
+
end
|
55
|
+
return true
|
56
|
+
end
|
57
|
+
|
58
|
+
# Rack interface.
|
59
|
+
# @param (Hash) env Rack environment.
|
60
|
+
# @return (Array)[status, headers, body]
|
61
|
+
def call(env)
|
62
|
+
# The preprocessing left us with nothing, a response,
|
63
|
+
# or a filename that we should try to run.
|
64
|
+
case deferred_result = env.delete(ENV_THREAD_BYPASS)
|
65
|
+
when String
|
66
|
+
filename = deferred_result
|
67
|
+
response = Script.new(env, filename).response
|
68
|
+
if response.header["X-Cascade"] == 'pass'
|
69
|
+
index_response = Script.new(env, filename + '/index').response
|
70
|
+
response = index_response unless index_response.header["X-Cascade"] == 'pass'
|
71
|
+
end
|
72
|
+
response.finish
|
73
|
+
when NilClass
|
74
|
+
not_found
|
75
|
+
else
|
76
|
+
deferred_result.finish
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Status 404 with X-Cascade => pass.
|
81
|
+
# @return (Array)[status, headers, body]
|
82
|
+
def not_found
|
83
|
+
return @not_found if @not_found
|
84
|
+
body = "404 Not Found\n"
|
85
|
+
@not_found = [404, {'Content-Type' => 'text/plain',
|
86
|
+
'Content-Length' => body.size.to_s,
|
87
|
+
'X-Cascade' => 'pass'},
|
88
|
+
[body]]
|
89
|
+
@not_found
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|