#! /usr/bin/env ruby

# play-oss.rb: Written by Tadayoshi Funaba 1999-2006
# $Id: play-oss.rb,v 1.23 2006-11-10 21:57:06+09 tadf Exp $

require 'smf'
require 'smf/toy/tempomap'
require 'gopt'
include  SMF

module SMF

  class DevSeq < File

    def putbuf(s)
      @buf ||= ''
      if @buf.size > 4096
	dumpbuf
      end
      @buf << s
    end

    def dumpbuf
      syswrite(@buf)
      @buf = ''
    end

    EV_TIMING = 0x81

    def timer_event(ev, parm)
      putbuf([EV_TIMING, ev, 0, 0, parm].pack('C4I'))
    end

    private :timer_event

    TMR_START = 4
    TMR_STOP = 3
    TMR_WAIT_ABS = 2

    def start_timer() timer_event(TMR_START, 0) end
    def stop_timer() timer_event(TMR_STOP, 0) end
    def wait_time(ticks) timer_event(TMR_WAIT_ABS, ticks) end

    SEQ_MIDIPUTC = 5

    def midiout(device, byte)
      putbuf([SEQ_MIDIPUTC, byte, device, 0].pack('C4'))
    end

    case RUBY_PLATFORM
    when /freebsd/
      SNDCTL_SEQ_CTRLRATE = 0xc0045103
      SNDCTL_SEQ_NRMIDIS  = 0x4004510b
      SNDCTL_MIDI_INFO = 0xc074510c
    when /linux/
      SNDCTL_SEQ_CTRLRATE = 0xc0045103
      SNDCTL_SEQ_NRMIDIS  = 0x8004510b
      SNDCTL_MIDI_INFO = 0xc074510c
    else
      raise 'unknown system'
    end

    def ctrlrate
      n = [0].pack('i')
      ioctl(SNDCTL_SEQ_CTRLRATE, n)
      n.unpack('i')[0]
    end

    def nrmidis
      n = [0].pack('i')
      ioctl(SNDCTL_SEQ_NRMIDIS, n)
      n.unpack('i')[0]
    end

    def midi_info(dev)
      templ = 'A30x2iLix72'
      a = [0] * 22
      a[0] = ''
      a[1] = dev
      n = a.pack(templ)
      ioctl(SNDCTL_MIDI_INFO, n)
      n.unpack(templ)
    end

  end

  class Sequence

    class Play < XSCallback

      def initialize(tm, num) @tm, @num = tm, num end

      def header(format, ntrks, division, tc)
	@sq = DevSeq.open('/dev/sequencer', 'w')
	puts(@sq.midi_info(@num)[0]) if $VERBOSE
	unless @num < @sq.nrmidis
	  raise 'device not available'
	end
	@ctrlrate = @sq.ctrlrate
      end

      def track_start() @offset = 0 end

      def delta(delta)
	@start_timer ||= (@sq.start_timer; true)
	if delta.nonzero?
	  @offset += delta
	  e = @tm.offset2elapse(@offset)
	  @sq.wait_time((e * @ctrlrate).to_i)
	end
      end

      def noteoff(ch, note, vel)
	@sq.midiout(@num, ch | 0x80)
	@sq.midiout(@num, note)
	@sq.midiout(@num, vel)
      end

      def noteon(ch, note, vel)
	@sq.midiout(@num, ch | 0x90)
	@sq.midiout(@num, note)
	@sq.midiout(@num, vel)
      end

      def polyphonickeypressure(ch, note, val)
	@sq.midiout(@num, ch | 0xa0)
	@sq.midiout(@num, note)
	@sq.midiout(@num, val)
      end

      def controlchange(ch, num, val)
	@sq.midiout(@num, ch | 0xb0)
	@sq.midiout(@num, num)
	@sq.midiout(@num, val)
      end

      def programchange(ch, num)
	@sq.midiout(@num, ch | 0xc0)
	@sq.midiout(@num, num)
      end

      def channelpressure(ch, val)
	@sq.midiout(@num, ch | 0xd0)
	@sq.midiout(@num, val)
      end

      def pitchbendchange(ch, val)
	@sq.midiout(@num, ch | 0xe0)
	val += 0x2000
	lsb =  val       & 0x7f
	msb = (val >> 7) & 0x7f
	@sq.midiout(@num, lsb)
	@sq.midiout(@num, msb)
      end

      def channelmodemessage(ch, num, val) controlchange(ch, num, val) end

      private :channelmodemessage

      def allsoundoff(ch) channelmodemessage(ch, 0x78, 0) end
      def resetallcontrollers(ch) channelmodemessage(ch, 0x79, 0) end
      def localcontrol(ch, val) channelmodemessage(ch, 0x7a, val) end
      def allnotesoff(ch) channelmodemessage(ch, 0x7b, 0) end
      def omnioff(ch) channelmodemessage(ch, 0x7c, 0) end
      def omnion(ch) channelmodemessage(ch, 0x7d, 0) end
      def monomode(ch, val) channelmodemessage(ch, 0x7e, val) end
      def polymode(ch) channelmodemessage(ch, 0x7f, 0) end

      def exclusivefx(data)
	data.each_byte do |x|
	  @sq.midiout(@num, x)
	end
      end

      private :exclusivefx

      def exclusivef0(data)
	@sq.midiout(@num, 0xf0)
	exclusivefx(data)
      end

      def exclusivef7(data) exclusivefx(data) end

      def result
	@sq.dumpbuf
	@sq.stop_timer
	@sq.close
      end

    end

    def play(num=0)
      j = join
      tm = TempoMap.new(j)
      WS.new(j, Play.new(tm, num)).read
    end

  end

end

def usage
  warn 'usage: play-oss [-d num] [input]'
  exit 1
end

usage unless opt = Gopt.gopt('d:')
usage unless $*.size >= 0 && $*.size <= 1
file = $*.shift
file = nil if file == '-'

num = (opt[:d] || '0').to_i

sq = unless file then Sequence.read($stdin) else Sequence.load(file) end
sq.play(num)
