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/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/TODO ADDED
@@ -0,0 +1,3 @@
1
+ - nonstandard types
2
+ - TCP and UNIX sockets
3
+ - OSC urls
@@ -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'
@@ -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