=begin $Id: main.rb,v 1.2 2006-03-15 04:37:46 matju Exp $ GridFlow Copyright (c) 2001,2002,2003,2004 by Mathieu Bouchard This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. See file ../COPYING for further informations on licensing terms. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. =end require "socket" require "fcntl" module GridFlow class<<self def max_rank; 16; end def max_size; 64*1024**2; end def max_packet; 1024*2; end end ENDIAN_BIG,ENDIAN_LITTLE,ENDIAN_SAME,ENDIAN_DIFF = 0,1,2,3 OurByteOrder = case [1].pack("L") when "\0\0\0\1"; ENDIAN_BIG # Mac, Sun, SiliconGraphics when "\1\0\0\0"; ENDIAN_LITTLE # Intel else raise "Cannot determine byte order" end class Format < GridObject FF_R,FF_W = 4,2 # flags indicating support of :in and :out respectively. attr_accessor :parent =begin API (version 0.8) mode is :in or :out def initialize(mode,*args) : open a file handler (do it via .new of class) attr_reader :description : a _literal_ (constant) string describing the format handler def self.info() optional : return a string describing the format handler differently than self.description(). in particular, it can list compile-time options and similar things. for example, quicktime returns a list of codecs. def frame() : read one frame, send through outlet 0 return values : Integer >= 0 : frame number of frame read. false : no frame was read : end of sequence. nil : a frame was read, but can't say its number. note that trying to read a nonexistent frame should no longer rewind automatically (@in handles that part), nor re-read the last frame (mpeg/quicktime used to do this) def seek(Integer i) : select one frame to be read next (by number) def length() : ^Integer returns number of frames (never implemented ?) def close() : close a handler inlet 0 : grid : frame to write other : special options outlet 0 : grid : frame just read outlet 1 : everything else =end def initialize(mode,*) super @cast = :int32 @colorspace = :rgb @mode = mode @frame = 0 @parent = nil @stream = nil flags = self.class.instance_eval{if defined?@flags then @flags else 6 end} # FF_W, FF_R, FF_RW case mode when :in; flags[2]==1 when :out; flags[1]==1 else raise "Format opening mode is incorrect" end or raise \ "Format '#{self.class.instance_eval{@symbol_name}}'"\ " does not support mode '#{mode}'" end def close @stream.close if defined? @stream and @stream end def self.suffixes_are(*suffixes) suffixes.map{|s|s.split(/[, ]/)}.flatten.each {|suffix| Format.suffixes[suffix] = self } end class<<self attr_reader :symbol_name attr_reader :description attr_reader :flags attr_reader :suffixes end @suffixes = {} def seek frame (rewind; return) if frame == 0 raise "don't know how to seek for frame other than # 0" end # this is what you should use to rewind # different file-sources may redefine this as something else # (eg: gzip) def rewind raise "Nothing to rewind about..." if not @stream @stream.seek 0,IO::SEEK_SET @frame = 0 end # This is different from IO#eof, which waits until a read has failed # doesn't work in nonblocking mode? (I don't recall why) def eof? thispos = (@stream.seek 0,IO::SEEK_CUR; @stream.tell) lastpos = (@stream.seek 0,IO::SEEK_END; @stream.tell) @stream.seek thispos,IO::SEEK_SET return thispos == lastpos rescue Errno::ESPIPE # just ignore if seek is not possible return false end # "ideal" buffer size or something # the buffer may be bigger than this but usually not by much. def self.buffersize; 16384 end def _0_headerless(*args) #!@#$ goes in FormatGrid ? args=args[0] if Array===args[0] #raise "expecting dimension list..." args.map! {|a| Numeric===a or raise "expecting dimension list..." a.to_i } @headerless = args end def _0_headerful #!@#$ goes in FormatGrid ? @headerless = nil end def _0_type arg #!@#$ goes in FormatGrid ? #!@#$ bug: should not be able to modify this _during_ a transfer case arg when :uint8; @bpv=8; @bp=BitPacking.new(ENDIAN_LITTLE,1,[0xff]) when :int16; @bpv=16; @bp=BitPacking.new(ENDIAN_LITTLE,1,[0xffff]) when :int32; @bpv=32; @bp=nil else raise "unsupported number type: #{arg}" end end def _0_cast arg case arg when :uint8, :int16, :int32, :int64, :float32, :float64 @cast = arg else raise "unsupported number type: #{arg}" end end def frame; @frame+=1; @frame-1 end end # common parts between GridIn and GridOut module GridIO def check_file_open; if not @format then raise "can't do that: file not open" end end def _0_close; check_file_open; @format.close; @format = nil end def delete; @format.close if @format; @format = nil; super end attr_reader :format def _0_open(sym,*a) sym = sym.intern if String===sym if a.length==0 and /\./ =~ sym.to_s then a=[sym]; sym=:file end qlass = GridFlow.fclasses["\#io:#{sym}"] if not qlass then raise "unknown file format identifier: #{sym}" end _0_close if @format @format = qlass.new @mode, *a @format.connect 0,self,1 @format.connect 1,self,2 @format.parent = self @loop = true end def _0_timelog flag; @timelog = Integer(flag)!=0 end def _0_loop flag; @loop = Integer(flag)!=0 end def method_missing(*message) sel = message[0].to_s if sel =~ /^_0_/ message[0] = sel.sub(/^_0_/,"").intern @format.send_in 0, *message elsif sel =~ /^_2_/ sel = sel.sub(/^_2_/,"").intern message.shift send_out 1, sel, *message else return super end end end GridObject.subclass("#in",1,2) { install_rgrid 0 include GridIO def initialize(*a) super @format = nil @timelog = false @framecount = 0 @time = Time.new @mode = :in return if a.length==0 _0_open(*a) end def _0_bang check_file_open framenum = @format.frame if framenum == false send_out 1 return if not @loop @format.seek 0 framenum = @format.frame if framenum == false raise "can't read frame: the end is at the beginning???" end end send_out 1, framenum if framenum end def _0_float frame; _0_set frame; _0_bang end def _0_set frame; check_file_open; @format.seek frame end def _0_reset; check_file_open; @format.seek 0; end def _1_grid(*a) send_out 0,:grid,*a end def _0_load(*a); _0_open(*a); _0_bang; _0_close end } GridObject.subclass("#out",1,1) { include GridIO def initialize(*a) super @format = nil @timelog = false @framecount = 0 @time = Time.new @mode = :out return if a.length==0 if Integer===a[0] or Float===a[0] _0_open :x11,:here _0_out_size a[0],a[1] else _0_open(*a) end end def _0_list(*a) @format._0_list(*a) end # hacks def _1_grid(*a) send_out 0,:grid,*a end # for aalib def _1_position(*a) send_out 0,:position,*a end def _1_keypress(*a) send_out 0,:keypress,*a end def _1_keyrelease(*a) send_out 0,:keyrelease,*a end def _0_grid(*a) check_file_open @format._0_grid(*a) send_out 0,:bang log if @timelog @framecount+=1 end def log time = Time.new post("\#out: frame#%04d time: %10.3f s; diff: %5d ms", @framecount, time, ((time-@time)*1000).to_i) @time = time end install_rgrid 0 } class BitPacking alias pack pack2 alias unpack unpack2 end # adding event-driven IO to a Format class module EventIO def read_wait?; !!@action; end def initialize(*) @acceptor = nil @buffer = nil @action = nil @chunksize = nil @rewind_redefined = false @clock = Clock.new self @delay = 100 # ms super end def call() try_read end def on_read(n,&action) @action = action @chunksize = n end def try_accept #!@#$ use setsockopt(SO_REUSEADDR) here??? TCPSocket.do_not_reverse_lookup = true # hack @acceptor.nonblock = true @stream = @acceptor.accept @stream.nonblock = true @stream.sync = true @clock.unset # send_out 0, :accept # does not work rescue Errno::EAGAIN end def try_read(dummy=nil) n = @chunksize-(if @buffer then @buffer.length else 0 end) t = @stream.read(n) # or raise EOFError if not t raise "heck" if not @stream.eof? rewind t = @stream.read(n) or raise "can't read any of #{n} bytes?" end if @buffer then @buffer << t else @buffer = t end if @buffer.length == @chunksize action,buffer = @action,@buffer @action,@buffer = nil,"" @clock.unset action.call buffer end rescue Errno::EAGAIN post "read would block" end def raw_open_gzip_in(filename) r,w = IO.pipe if pid=fork GridFlow.subprocesses[pid]=true w.close @stream = r else r.close STDOUT.reopen w STDIN.reopen filename, "r" exec "gzip", "-dc" end end def raw_open_gzip_out(filename) r,w = IO.pipe if pid=fork GridFlow.subprocesses[pid]=true r.close @stream = w else w.close STDIN.reopen r STDOUT.reopen filename, "w" exec "gzip", "-c" end end def raw_open(mode,source,*args) @raw_open_args = mode,source,*args fmode = case mode when :in; "r" when :out; "w" else raise "bad mode" end @stream.close if @stream case source when :file filename = args[0].to_s filename = GridFlow.find_file filename if mode==:in @stream = File.open filename, fmode when :gzfile filename = args[0].to_s filename = GridFlow.find_file filename if mode==:in if mode==:in then raw_open_gzip_in filename else raw_open_gzip_out filename end def self.rewind raw_open(*@raw_open_args) @frame = 0 end unless @rewind_redefined @rewind_redefined = true when :tcp if RUBY_VERSION < "1.6.6" raise "use at least 1.6.6 (reason: bug in socket code)" end post "-----------" time = Time.new TCPSocket.do_not_reverse_lookup = true # hack @stream = TCPSocket.open(args[0].to_s,args[1].to_i) post "----------- #{Time.new-time}" @stream.nonblock = true @stream.sync = true @clock.delay @delay when :tcpserver TCPSocket.do_not_reverse_lookup = true # hack TCPServer.do_not_reverse_lookup = true # hack post "-----------" time = Time.new @acceptor = TCPServer.open(args[0].to_s) post "----------- #{Time.new-time}" @acceptor.nonblock = true #$tasks[self] = proc {self.try_accept} #!!!!! else raise "unknown access method '#{source}'" end end def close @acceptor.close if @acceptor @stream.close if @stream GridFlow.hunt_zombies end end Format.subclass("#io:file",1,1) { def self.new(mode,file) file=file.to_s a = [mode,:file,file] if not /\./=~file then raise "no filename suffix?" end suf=file.split(/\./)[-1] h=Format.suffixes[suf] if not h then raise "unknown suffix '.#{suf}'" end h.new(*a) end @comment="format autodetection proxy" } Format.subclass("#io:grid",1,1) { include EventIO install_rgrid 0 @comment = "GridFlow file format" suffixes_are "grid" =begin This is the Grid format I defined: 1 uint8: 0x7f 4 uint8: "GRID" big endian | "grid" little endian 1 uint8: type { number of bits in 8,16,32,64, plus one of: 1:unsigned 2:float but float8,float16 are not allowed (!) } 1 uint8: reserved (supported: 0) 1 uint8: number of dimensions N (supported: at least 0..4) N uint32: number of elements per dimension D[0]..D[N-1] raw data goes there. =end # bits per value: 32 only attr_accessor :bpv # Fixnum: bits-per-value # endianness # attr_accessor :endian # ENDIAN_LITTLE or ENDIAN_BIG # IO or File or TCPSocket attr_reader :stream # nil=headerful; array=assumed dimensions of received grids #attr_accessor :headerless def initialize(mode,source,*args) super @bpv = 32 @headerless = nil @endian = OurByteOrder raw_open mode,source,*args end def post(*s) # because i'm using miller_0_38 and it can't disable the console # i am using fprintf stderr instead of post. ### STDERR.puts(sprintf(*s)) # disabled because i don't need it now end # rewinding and starting def frame raise "can't get frame when there is no connection" if not @stream raise "already waiting for input" if read_wait? return false if eof? post "----- 1" if @headerless then @n_dim=@headerless.length @dim = @headerless @dex = 0 set_bufsize send_out_grid_begin 0, @dim on_read(bufsize) {|data| frame3 data } else on_read(8) {|data| frame1 data } end post "----- 2" (try_read nil while read_wait?) if not TCPSocket===@stream post "----- 3" super post "----- 4" end def set_bufsize @prod = 1 @dim.each {|x| @prod *= x } n = @prod/@dim[0] k = GridFlow.max_packet / n k=1 if k<1 @bufsize = k*n*@bpv/8 @bufsize = @prod if @bufsize > @prod end # the header def frame1 data post "----- frame1" head,@bpv,reserved,@n_dim = data.unpack "a5ccc" @endian = case head when "\x7fGRID"; ENDIAN_BIG when "\x7fgrid"; ENDIAN_LITTLE else raise "grid header: invalid (#{data.inspect})" end case bpv when 8, 16, 32; # ok else raise "unsupported bpv (#{@bpv})" end if reserved!=0 raise "reserved field is not zero" end if @n_dim > GridFlow.max_rank raise "too many dimensions (#{@n_dim})" end on_read(4*@n_dim) {|data| frame2 data } end # the dimension list def frame2 data post "----- frame2" @dim = data.unpack(if @endian==ENDIAN_LITTLE then "V*" else "N*" end) set_bufsize if @prod > GridFlow.max_size raise "dimension list: invalid prod (#{@prod})" end send_out_grid_begin 0, @dim, @cast on_read(bufsize) {|data| frame3 data } @dex = 0 end attr_reader :bufsize # for each slice of the body def frame3 data post "----- frame3 with dex=#{@dex.inspect}, prod=#{@prod.inspect}" n = data.length nn = n*8/@bpv # is/was there a problem with the size of the data being read? case @bpv when 8 @bp = BitPacking.new(@endian,1,[0xff]) send_out_grid_flow(0, @bp.unpack(data)) @dex += data.length when 16 @bp = BitPacking.new(@endian,2,[0xffff]) send_out_grid_flow(0, @bp.unpack(data)) @dex += data.length/2 when 32 data.swap32! if @endian!=OurByteOrder send_out_grid_flow 0, data @dex += data.length/4 end if @dex >= @prod @clock.unset else on_read(bufsize) {|data| frame3 data } end end def _0_rgrid_begin if not @stream raise "can't send frame when there is no connection" end @dim = inlet_dim 0 post "@dim=#{@dim.inspect}" return if @headerless # header @stream.write( [if @endian==ENDIAN_LITTLE then "\x7fgrid" else "\x7fGRID" end, @bpv,0,@dim.length].pack("a5ccc")) # dimension list @stream.write( @dim.to_a.pack(if @endian==ENDIAN_LITTLE then "V*" else "N*" end)) end def _0_rgrid_flow data case @bpv when 8, 16 @stream.write @bp.pack(data) when 32 data.swap32! if GridFlow::OurByteOrder != @endian @stream.write data end end def _0_rgrid_end; @stream.flush end def endian(a) @endian = case a when :little; ENDIAN_LITTLE when :big; ENDIAN_BIG when :same; ENDIAN_SAME else raise "argh" end end def headerless(*args) args=args[0] if Array===args[0] args.map! {|a| Numeric===a or raise "expecting dimension list..." a.to_i } @headerless = args end def headerful; @headerless = nil end #!@#$ method name conflict ? def type(nt) #!@#$ bug: should not be able to modify this _during_ a transfer case nt when :uint8; @bpv= 8; @bp=BitPacking.new(ENDIAN_LITTLE,1,[0xff]) when :int16; @bpv=16; @bp=BitPacking.new(ENDIAN_LITTLE,1,[0xffff]) when :int32; @bpv=32; @bp=nil else raise "unsupported number type" end end } module PPMandTarga # "and false" disables features that may cause crashes and don't # accelerate gridflow that much. def frame_read_body height, width, channels bs = width*channels n = bs*height bs = (self.class.buffersize/bs)*bs+bs # smallest multiple of bs over BufferSize buf = "" if RUBY_VERSION >= "1.8.0" and false data = "x"*bs # must preallocate (bug in 1.8.0.pre1-3) while n>0 do bs=n if bs>n @stream.read(bs,data) or raise EOFError if @bp then send_out_grid_flow 0, @bp.unpack(data,buf) else send_out_grid_flow 0, data, :uint8 end n-=bs end else nothing = "" while n>0 do bs=n if bs>n data = @stream.read(bs) or raise EOFError if @bp then send_out_grid_flow 0, @bp.unpack(data,buf) else send_out_grid_flow 0, data, :uint8 end data.replace nothing and false # prevent clogging memory n-=bs end end end end Format.subclass("#io:ppm",1,1) { install_rgrid 0 @comment = "Portable PixMap (PPM) File Format" suffixes_are "ppm" include EventIO, PPMandTarga def initialize(mode,source,*args) @bp = if mode==:out BitPacking.new(ENDIAN_LITTLE,3,[0x0000ff,0x00ff00,0xff0000]) else nil end super raw_open mode,source,*args end def frame #@stream.sync = false metrics=[] return false if eof? line = @stream.gets (rewind; line = @stream.gets) if not line # hack line.chomp! if line != "P6" then raise "Wrong format (needing PPM P6)" end while metrics.length<3 line = @stream.gets next if line =~ /^#/ metrics.push(*(line.split(/\s+/).map{|x| Integer x })) end metrics[2]==255 or raise "Wrong color depth (max_value=#{metrics[2]} instead of 255)" send_out_grid_begin 0, [metrics[1], metrics[0], 3], @cast frame_read_body metrics[1], metrics[0], 3 super end def _0_rgrid_begin dim = inlet_dim 0 raise "expecting (rows,columns,channels)" if dim.length!=3 raise "expecting channels=3" if dim[2]!=3 @stream.write "P6\n" @stream.write "# generated using GridFlow #{GF_VERSION}\n" @stream.write "#{dim[1]} #{dim[0]}\n255\n" @stream.flush inlet_set_factor 0, 3 end def _0_rgrid_flow(data) @stream.write @bp.pack(data) end def _0_rgrid_end; @stream.flush end self }.subclass("#io:tk",1,1) { install_rgrid 0 def initialize(mode) if mode!=:out then raise "only #out" end super(mode,:file,"/tmp/tk-#{$$}-#{object_id}.ppm") GridFlow.gui "toplevel .#{object_id}\n" GridFlow.gui "wm title . GridFlow/Tk\n" GridFlow.gui "image create photo #{object_id} -width 320 -height 240\n" GridFlow.gui "pack [label .#{object_id}.im -image #{object_id}]\n" end def _0_rgrid_end super @stream.seek 0,IO::SEEK_SET GridFlow.gui "image create photo #{object_id} -file /tmp/tk-#{$$}-#{object_id}.ppm\n" end def delete GridFlow.gui "destroy .#{object_id}\n" GridFlow.gui "image delete #{object_id}\n" end alias close delete } Format.subclass("#io:targa",1,1) { install_rgrid 0 @comment = "TrueVision Targa" suffixes_are "tga" include EventIO, PPMandTarga =begin targa header is like: [:comment, Uint8, :length], [:colortype, Uint8], [:colors, Uint8], 5, [:origin_x, Int16], [:origin_y, Int16], [:w, Uint16], [:h, Uint16], [:depth, Uint8], 1, [:comment, String8Unpadded, :data], =end def initialize(mode,source,*args) super raw_open mode,source,*args end def set_bitpacking depth @bp = case depth #!@#$ endian here doesn't seem to be changing much ? when 24; BitPacking.new(ENDIAN_LITTLE,3,[0xff0000,0x00ff00,0x0000ff]) when 32; BitPacking.new(ENDIAN_LITTLE,4, [0x00ff0000,0x0000ff00,0x000000ff,0xff000000]) else raise "tga: unsupported colour depth: #{depth}\n" end end def frame return false if eof? head = @stream.read(18) comment_length,colortype,colors,w,h,depth = head.unpack("cccx9vvcx") comment = @stream.read(comment_length) raise "unsupported color format: #{colors}" if colors != 2 # post "tga: size y=#{h} x=#{w} depth=#{depth} colortype=#{colortype}" # post "tga: comment: \"#{comment}\"" set_bitpacking depth send_out_grid_begin 0, [ h, w, depth/8 ], @cast frame_read_body h, w, depth/8 super end def _0_rgrid_begin dim = inlet_dim 0 raise "expecting (rows,columns,channels)" if dim.length!=3 raise "expecting channels=3 or 4" if dim[2]!=3 and dim[2]!=4 # comment = "created using GridFlow" #!@#$ why did i use that comment again? comment = "generated using GridFlow #{GF_VERSION}" @stream.write [comment.length,colortype=0,colors=2,"\0"*9, dim[1],dim[0],8*dim[2],(8*(dim[2]-3))|32,comment].pack("ccca9vvcca*") set_bitpacking 8*dim[2] inlet_set_factor 0, dim[2] end def _0_rgrid_flow data; @stream.write @bp.pack(data) end def _0_rgrid_end; @stream.flush end } end # module GridFlow