rosc 0.1.3
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/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
|