rosc 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +13 -0
- data/CHANGELOG +4 -0
- data/GPL.txt +340 -0
- data/LICENSE +57 -0
- data/Manifest +16 -0
- data/README +58 -0
- data/Rakefile +21 -0
- data/TODO +3 -0
- data/examples/readme.rb +19 -0
- data/lib/osc.rb +324 -0
- data/lib/osc/pattern.rb +131 -0
- data/lib/osc/server.rb +94 -0
- data/lib/osc/transport.rb +42 -0
- data/lib/osc/udp.rb +30 -0
- data/rosc.gemspec +31 -0
- data/setup.rb +1551 -0
- data/test/test_osc.rb +160 -0
- metadata +84 -0
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
|
4
|
+
require 'echoe'
|
5
|
+
Echoe.new('rosc')
|
6
|
+
|
7
|
+
task :default => :rdoc
|
8
|
+
Rake::RDocTask.new do |rd|
|
9
|
+
rd.rdoc_files.add ['README','AUTHORS','TODO','lib/**/*.rb']
|
10
|
+
#rd.template = ENV['HOME']+'/src/allison/allison.rb'
|
11
|
+
rd.rdoc_dir = 'doc'
|
12
|
+
rd.options = ['-x_darcs','-xtest']
|
13
|
+
rd.title = 'rosc'
|
14
|
+
rd.options += ['--line-numbers','--inline-source']
|
15
|
+
end
|
16
|
+
|
17
|
+
Rake::TestTask.new do |t|
|
18
|
+
#t.verbose = true
|
19
|
+
end
|
20
|
+
|
21
|
+
# vim: filetype=ruby
|
data/examples/readme.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'osc'
|
2
|
+
Host = 'localhost'
|
3
|
+
Port = 5000
|
4
|
+
|
5
|
+
s = OSC::UDPServer.new
|
6
|
+
s.bind Host, Port
|
7
|
+
|
8
|
+
c = OSC::UDPSocket.new
|
9
|
+
m = OSC::Message.new('/foo', 'fi', Math::PI, 42)
|
10
|
+
c.send m, 0, Host, Port
|
11
|
+
|
12
|
+
s.add_method '/f*', 'fi' do |msg|
|
13
|
+
domain, port, host, ip = msg.source
|
14
|
+
puts "#{msg.address} -> #{msg.args.inspect} from #{host}:#{port}"
|
15
|
+
end
|
16
|
+
Thread.new do
|
17
|
+
s.serve
|
18
|
+
end
|
19
|
+
sleep 5
|
data/lib/osc.rb
ADDED
@@ -0,0 +1,324 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'forwardable'
|
3
|
+
require 'stringio'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
# Test for broken pack/unpack
|
7
|
+
if [1].pack('n') == "\001\000"
|
8
|
+
class String
|
9
|
+
alias_method :broken_unpack, :unpack
|
10
|
+
def unpack(spec)
|
11
|
+
broken_unpack(spec.tr("nNvV","vVnN"))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
class Array
|
15
|
+
alias_method :broken_pack, :pack
|
16
|
+
def pack(spec)
|
17
|
+
broken_pack(spec.tr("nNvV","vVnN"))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
class StringIO
|
24
|
+
def skip(n)
|
25
|
+
self.seek(n, IO::SEEK_CUR)
|
26
|
+
end
|
27
|
+
def skip_padding
|
28
|
+
self.skip((4-pos)%4)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Of particular interest are OSC::Client, OSC::Server, OSC::Message and
|
33
|
+
# OSC::Bundle.
|
34
|
+
module OSC
|
35
|
+
# 64-bit big-endian fixed-point time tag
|
36
|
+
class TimeTag
|
37
|
+
JAN_1970 = 0x83aa7e80
|
38
|
+
# nil:: immediately
|
39
|
+
# Numeric:: seconds since January 1, 1900 00:00
|
40
|
+
# Numeric,Numeric:: int,frac parts of a TimeTag.
|
41
|
+
# Time:: convert from Time object
|
42
|
+
def initialize(*args)
|
43
|
+
t = args
|
44
|
+
t = t.first if t and t.size == 1
|
45
|
+
case t
|
46
|
+
when NIL # immediately
|
47
|
+
@int = 0
|
48
|
+
@frac = 1
|
49
|
+
when Numeric
|
50
|
+
@int, fr = t.divmod(1)
|
51
|
+
@frac = (fr * (2**32)).to_i
|
52
|
+
when Array
|
53
|
+
@int,@frac = t
|
54
|
+
when Time
|
55
|
+
@int, fr = (t.to_f+JAN_1970).divmod(1)
|
56
|
+
@frac = (fr * (2**32)).to_i
|
57
|
+
else
|
58
|
+
raise ArgumentError
|
59
|
+
end
|
60
|
+
end
|
61
|
+
attr_accessor :int, :frac
|
62
|
+
def to_i; to_f.to_i; end
|
63
|
+
# Ruby's Float can handle the 64 bits so we have the luxury of dealing with
|
64
|
+
# Float directly
|
65
|
+
def to_f; @int.to_f + @frac.to_f/(2**32); end
|
66
|
+
# [int,frac]
|
67
|
+
def to_a; [@int,@frac]; end
|
68
|
+
# Human-readable, like the output of Time#to_s
|
69
|
+
def to_s; to_time.to_s; end
|
70
|
+
# Ruby Time object
|
71
|
+
def to_time; Time.at(to_f-JAN_1970); end
|
72
|
+
alias :time :to_time
|
73
|
+
def self.now; TimeTag.new(Time.now); end
|
74
|
+
def method_missing(sym, *args)
|
75
|
+
time.__send__(sym, *args)
|
76
|
+
end
|
77
|
+
def to_yaml
|
78
|
+
to_a.to_yaml
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class Blob < String
|
83
|
+
end
|
84
|
+
|
85
|
+
class Message
|
86
|
+
attr_accessor :address, :args
|
87
|
+
# The source of this message, usually something like ["AF_INET", 50475,
|
88
|
+
# 'localhost','127.0.0.1']
|
89
|
+
attr_accessor :source
|
90
|
+
|
91
|
+
# address:: The OSC address (a String)
|
92
|
+
# types:: The OSC type tags string
|
93
|
+
# args:: arguments. must match type tags in arity
|
94
|
+
#
|
95
|
+
# Example:
|
96
|
+
# Message.new('/foo','ff', Math::PI, Math::E)
|
97
|
+
#
|
98
|
+
# Arguments will be coerced as indicated by the type tags. If types is nil,
|
99
|
+
# type tags will be inferred from arguments.
|
100
|
+
def initialize(address, types=nil, *args)
|
101
|
+
if types and types.size != args.size
|
102
|
+
raise ArgumentError, 'type/args arity mismatch'
|
103
|
+
end
|
104
|
+
|
105
|
+
@address = address
|
106
|
+
@args = []
|
107
|
+
|
108
|
+
if types
|
109
|
+
args.each_with_index do |arg, i|
|
110
|
+
case types[i]
|
111
|
+
when ?i; @args << arg.to_i
|
112
|
+
when ?f; @args << arg.to_f
|
113
|
+
when ?s; @args << arg.to_s
|
114
|
+
when ?b; @args << Blob.new(arg)
|
115
|
+
else
|
116
|
+
raise ArgumentError, "unknown type tag '#{@types[i].inspect}'"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
else
|
120
|
+
args.each do |arg|
|
121
|
+
case arg
|
122
|
+
when Fixnum,Float,String,TimeTag,Blob
|
123
|
+
@args << arg
|
124
|
+
else
|
125
|
+
raise ArgumentError, "Object has unknown OSC type: '#{arg}'"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def types
|
132
|
+
@args.collect {|a| Packet.tag a}.join
|
133
|
+
end
|
134
|
+
alias :typetag :types
|
135
|
+
|
136
|
+
# Encode this message for transport
|
137
|
+
def encode
|
138
|
+
Packet.encode(self)
|
139
|
+
end
|
140
|
+
# string representation. *not* the raw representation, for that use
|
141
|
+
# encode.
|
142
|
+
def to_s
|
143
|
+
"#{address},#{types},#{args.collect{|a| a.to_s}.join(',')}"
|
144
|
+
end
|
145
|
+
def to_yaml
|
146
|
+
{'address'=>address, 'types'=>types, 'args'=>args}.to_yaml
|
147
|
+
end
|
148
|
+
|
149
|
+
extend Forwardable
|
150
|
+
include Enumerable
|
151
|
+
|
152
|
+
de = (Array.instance_methods - self.instance_methods)
|
153
|
+
de -= %w(assoc flatten flatten! pack rassoc transpose)
|
154
|
+
de += %w(include? sort)
|
155
|
+
|
156
|
+
def_delegators(:@args, *de)
|
157
|
+
|
158
|
+
undef_method :zip
|
159
|
+
end
|
160
|
+
|
161
|
+
# bundle of messages and/or bundles
|
162
|
+
class Bundle
|
163
|
+
attr_accessor :timetag
|
164
|
+
attr_accessor :args
|
165
|
+
attr_accessor :source
|
166
|
+
alias :timestamp :timetag
|
167
|
+
alias :messages :args
|
168
|
+
alias :contents :args
|
169
|
+
alias :to_a :args
|
170
|
+
|
171
|
+
# New bundle with time and messages
|
172
|
+
def initialize(t=nil, *args)
|
173
|
+
@timetag =
|
174
|
+
case t
|
175
|
+
when TimeTag
|
176
|
+
t
|
177
|
+
else
|
178
|
+
TimeTag.new(t)
|
179
|
+
end
|
180
|
+
@args = args
|
181
|
+
end
|
182
|
+
|
183
|
+
def to_yaml
|
184
|
+
{'timestamp'=>timetag, 'contents'=>contents}.to_yaml
|
185
|
+
end
|
186
|
+
|
187
|
+
extend Forwardable
|
188
|
+
include Enumerable
|
189
|
+
|
190
|
+
de = (Array.instance_methods - self.instance_methods)
|
191
|
+
de -= %w(assoc flatten flatten! pack rassoc transpose)
|
192
|
+
de += %w(include? sort)
|
193
|
+
|
194
|
+
def_delegators(:@args, *de)
|
195
|
+
|
196
|
+
undef_method :zip
|
197
|
+
|
198
|
+
def encode
|
199
|
+
Packet.encode(self)
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
203
|
+
|
204
|
+
# Unit of transmission. Really needs revamping
|
205
|
+
module Packet
|
206
|
+
# XXX I might fold this and its siblings back into the decode case
|
207
|
+
# statement
|
208
|
+
def self.decode_int32(io)
|
209
|
+
i = io.read(4).unpack('N')[0]
|
210
|
+
i = 2**32 - i if i > (2**31-1) # two's complement
|
211
|
+
i
|
212
|
+
end
|
213
|
+
|
214
|
+
def self.decode_float32(io)
|
215
|
+
f = io.read(4).unpack('g')[0]
|
216
|
+
f
|
217
|
+
end
|
218
|
+
|
219
|
+
def self.decode_string(io)
|
220
|
+
s = io.gets("\0").chomp("\0")
|
221
|
+
io.skip_padding
|
222
|
+
s
|
223
|
+
end
|
224
|
+
|
225
|
+
def self.decode_blob(io)
|
226
|
+
l = io.read(4).unpack('N')[0]
|
227
|
+
b = io.read(l)
|
228
|
+
io.skip_padding
|
229
|
+
b
|
230
|
+
end
|
231
|
+
|
232
|
+
def self.decode_timetag(io)
|
233
|
+
t1 = io.read(4).unpack('N')[0]
|
234
|
+
t2 = io.read(4).unpack('N')[0]
|
235
|
+
TimeTag.new [t1,t2]
|
236
|
+
end
|
237
|
+
|
238
|
+
# Takes a string containing one packet
|
239
|
+
def self.decode(packet)
|
240
|
+
# XXX I think it would have been better to use a StringScanner. Maybe I
|
241
|
+
# will convert it someday...
|
242
|
+
io = StringIO.new(packet)
|
243
|
+
id = decode_string(io)
|
244
|
+
if id == '#bundle'
|
245
|
+
b = Bundle.new(decode_timetag(io))
|
246
|
+
until io.eof?
|
247
|
+
l = io.read(4).unpack('N')[0]
|
248
|
+
s = io.read(l)
|
249
|
+
b << decode(s)
|
250
|
+
end
|
251
|
+
b
|
252
|
+
elsif id =~ /^\//
|
253
|
+
m = Message.new(id)
|
254
|
+
if io.getc == ?,
|
255
|
+
tags = decode_string(io)
|
256
|
+
tags.scan(/./) do |t|
|
257
|
+
case t
|
258
|
+
when 'i'
|
259
|
+
m << decode_int32(io)
|
260
|
+
when 'f'
|
261
|
+
m << decode_float32(io)
|
262
|
+
when 's'
|
263
|
+
m << decode_string(io)
|
264
|
+
when 'b'
|
265
|
+
m << decode_blob(io)
|
266
|
+
|
267
|
+
# right now we skip over nonstandard datatypes, but we'll want to
|
268
|
+
# add these datatypes too.
|
269
|
+
when /[htd]/; io.read(8)
|
270
|
+
when 'S'; decode_string(io)
|
271
|
+
when /[crm]/; io.read(4)
|
272
|
+
when /[TFNI\[\]]/;
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
m
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def self.pad(s)
|
281
|
+
s + ("\000" * ((4 - s.size)%4))
|
282
|
+
end
|
283
|
+
|
284
|
+
def self.tag(o)
|
285
|
+
case o
|
286
|
+
when Fixnum; 'i'
|
287
|
+
when TimeTag; 't'
|
288
|
+
when Float; 'f'
|
289
|
+
when Blob; 'b'
|
290
|
+
when String; 's'
|
291
|
+
else; nil
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def self.encode(o)
|
296
|
+
case o
|
297
|
+
when Fixnum; [o].pack 'N'
|
298
|
+
when Float; [o].pack 'g'
|
299
|
+
when Blob; pad([o.size].pack('N') + o)
|
300
|
+
when String; pad(o.sub(/\000.*\Z/, '') + "\000")
|
301
|
+
when TimeTag; o.to_a.pack('NN')
|
302
|
+
|
303
|
+
when Message
|
304
|
+
s = encode(o.address)
|
305
|
+
s << encode(','+o.types)
|
306
|
+
s << o.args.collect{|x| encode(x)}.join
|
307
|
+
|
308
|
+
when Bundle
|
309
|
+
s = encode('#bundle')
|
310
|
+
s << encode(o.timetag)
|
311
|
+
s << o.args.collect { |x|
|
312
|
+
x2 = encode(x); [x2.size].pack('N') + x2
|
313
|
+
}.join
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
private_class_method :decode_int32, :decode_float32, :decode_string,
|
318
|
+
:decode_blob, :decode_timetag
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
require 'osc/pattern'
|
323
|
+
require 'osc/server'
|
324
|
+
require 'osc/udp'
|
data/lib/osc/pattern.rb
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'set'
|
2
|
+
module OSC
|
3
|
+
class Pattern < String
|
4
|
+
# Create an OSC pattern from a string or (experimental) from a Regex.
|
5
|
+
def initialize(s)
|
6
|
+
case s
|
7
|
+
when Regexp # This is experimental
|
8
|
+
s = Regexp.source s
|
9
|
+
s.gsub! /(\\\\)*\[^\/\]\*/, "\1*"
|
10
|
+
s.gsub! /(\\\\)*\[^\/\]/, "\1?"
|
11
|
+
s.gsub! /(\\\\)*\[^/, "\1[!"
|
12
|
+
s.gsub! /(\\\\)*\(/, "\1{"
|
13
|
+
s.gsub! /(\\\\)*\|/, "\1,"
|
14
|
+
s.gsub! /(\\\\)*\)/, "\1}"
|
15
|
+
s.gsub! /\\\\/, "\\"
|
16
|
+
end
|
17
|
+
super s
|
18
|
+
end
|
19
|
+
|
20
|
+
# Return a Regex representing this pattern
|
21
|
+
def regexp
|
22
|
+
s = Regexp.escape self
|
23
|
+
s.gsub! /\\\?/, '[^/]'
|
24
|
+
s.gsub! /\\\*/, '[^/]*'
|
25
|
+
s.gsub! /\\\[!/, '[^'
|
26
|
+
s.gsub! /\\\]/, ']'
|
27
|
+
s.gsub! /\\\{/, '('
|
28
|
+
s.gsub! /,/, '|'
|
29
|
+
s.gsub! /\\\}/, ')'
|
30
|
+
Regexp.new s
|
31
|
+
end
|
32
|
+
|
33
|
+
# Do these two patterns intersect?
|
34
|
+
#--
|
35
|
+
# This might be improved by following the (much simpler, but related)
|
36
|
+
# algorithm here:
|
37
|
+
#
|
38
|
+
# http://groups.google.com/group/comp.theory/browse_frm/thread/f33e033269bd5ab0/c87e19081f45454c?lnk=st&q=regular+expression+intersection&rnum=1&hl=en#c87e19081f45454c
|
39
|
+
#
|
40
|
+
# That is, convert each regexp into an NFA, then generate the set of valid
|
41
|
+
# state pairs, then check if the pair of final states is included.
|
42
|
+
# That's basically what I'm doing here, but I'm not generating all the
|
43
|
+
# state pairs, I'm just doing a search. My way may be faster and/or
|
44
|
+
# smaller, or it may not. My initial feeling is that it is faster since
|
45
|
+
# we're basically doing a depth-first search and OSC patterns are going to
|
46
|
+
# tend to be fairly simple. Still it might be a fun experiment for the
|
47
|
+
# masochistic.
|
48
|
+
def self.intersect?(s1,s2)
|
49
|
+
r = /\*|\?|\[[^\]]*\]|\{[^\}]*\}|./
|
50
|
+
a = s1.to_s.scan r
|
51
|
+
b = s2.to_s.scan r
|
52
|
+
q = [[a,b]]
|
53
|
+
until q.empty?
|
54
|
+
q.uniq!
|
55
|
+
a,b = q.pop
|
56
|
+
a = a.dup
|
57
|
+
b = b.dup
|
58
|
+
|
59
|
+
return true if a.empty? and b.empty?
|
60
|
+
next if a.empty? or b.empty?
|
61
|
+
|
62
|
+
x,y = a.shift, b.shift
|
63
|
+
|
64
|
+
# branch {}
|
65
|
+
if x =~ /^\{/
|
66
|
+
x.scan /[^\{\},]+/ do |x|
|
67
|
+
q.push [x.scan(/./)+a,[y]+b]
|
68
|
+
end
|
69
|
+
next
|
70
|
+
end
|
71
|
+
if y =~ /^\{/
|
72
|
+
y.scan /[^\{\},]+/ do |y|
|
73
|
+
q.push [[x]+a,y.scan(/./)+b]
|
74
|
+
end
|
75
|
+
next
|
76
|
+
end
|
77
|
+
|
78
|
+
# sort
|
79
|
+
if y =~ /^\[/
|
80
|
+
x,y = y,x
|
81
|
+
a,b = b,a
|
82
|
+
end
|
83
|
+
if y =~ /^(\*|\?)/
|
84
|
+
x,y = y,x
|
85
|
+
a,b = b,a
|
86
|
+
end
|
87
|
+
|
88
|
+
# match
|
89
|
+
case x
|
90
|
+
when '*'
|
91
|
+
unless y == '/'
|
92
|
+
q.push [a,b]
|
93
|
+
q.push [[x]+a,b]
|
94
|
+
end
|
95
|
+
if y == '*'
|
96
|
+
q.push [a,[y]+b]
|
97
|
+
q.push [[x]+a,b]
|
98
|
+
end
|
99
|
+
when '?'
|
100
|
+
q.push [a,b] unless y == '/'
|
101
|
+
q.push [a,[y]+b] if y == '*'
|
102
|
+
when /^\[/
|
103
|
+
xinv = (x[1] == ?!)
|
104
|
+
yinv = (y =~ /^\[!/)
|
105
|
+
x = x[(xinv ? 2 : 1)..-2].scan(/./).to_set
|
106
|
+
if y =~ /^\[/
|
107
|
+
y = y[(yinv ? 2 : 1)..-2].scan(/./).to_set
|
108
|
+
else
|
109
|
+
y = [y].to_set
|
110
|
+
end
|
111
|
+
|
112
|
+
# simplifying assumption: nobody in their right mind is going to do
|
113
|
+
# [^everyprintablecharacter]
|
114
|
+
if xinv and yinv
|
115
|
+
q.push [a,b]
|
116
|
+
elsif xinv and not yinv
|
117
|
+
q.push [a,b] unless (y-x).empty?
|
118
|
+
elsif not xinv and yinv
|
119
|
+
q.push [a,b] unless (x-y).empty?
|
120
|
+
else
|
121
|
+
q.push [a,b] unless (x&y).empty?
|
122
|
+
end
|
123
|
+
else
|
124
|
+
q.push [a,b] if x == y
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
false # no intersection
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|