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.
- 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
|