#! /usr/bin/env ruby

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

require 'smf'
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
    TMR_TEMPO = 6
    TMR_TIMESIG = 11

    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
    def settempo(value) timer_event(TMR_TEMPO, value) end
    def timesignature(sig) timer_event(TMR_TIMESIG, sig) end

    EV_CHN_COMMON = 0x92
    EV_CHN_VOICE = 0x93
    EV_SYSEX = 0x94

    def chn_voice(dev, event, chn, note, parm)
      putbuf([EV_CHN_VOICE, dev, event, chn, note, parm, 0, 0].pack('C8'))
    end

    def chn_common(dev, event, chn, p1, p2, w14)
      putbuf([EV_CHN_COMMON, dev, event, chn, p1, p2, w14].pack('C6s'))
    end

    def sysex(dev, buf, len)
      buf2 = buf
      if len < 6
	buf2 = buf2 + "\xff" * 6
      end
      putbuf([EV_SYSEX, dev].pack('C2') + buf2[0,6])
    end

    private :chn_voice, :chn_common

    MIDI_NOTEOFF = 0x80
    MIDI_NOTEON = 0x90
    MIDI_KEY_PRESSURE = 0xa0
    MIDI_CTL_CHANGE = 0xb0
    MIDI_PGM_CHANGE = 0xc0
    MIDI_CHN_PRESSURE = 0xd0
    MIDI_PITCH_BEND = 0xe0

    def noteoff(dev, ch, note, vel)
      chn_voice(dev, MIDI_NOTEOFF, ch, note, vel)
    end

    def noteon(dev, ch, note, vel)
      chn_voice(dev, MIDI_NOTEON, ch, note, vel)
    end

    def polyphonickeypressure(dev, ch, note, val)
      chn_voice(dev, MIDI_KEY_PRESSURE, ch, note, val)
    end

    def controlchange(dev, ch, num, val)
      chn_common(dev, MIDI_CTL_CHANGE, ch, num, 0, val)
    end

    def programchange(dev, ch, num)
      chn_common(dev, MIDI_PGM_CHANGE, ch, num, 0, 0)
    end

    def channelpressure(dev, ch, val)
      chn_common(dev, MIDI_CHN_PRESSURE, ch, val, 0, 0)
    end

    def pitchbendchange(dev, ch, val)
      chn_common(dev, MIDI_PITCH_BEND, ch, 0, 0, val)
    end

    case RUBY_PLATFORM
    when /freebsd/
      SNDCTL_SEQ_NRSYNTHS = 0x4004510a
      SNDCTL_SYNTH_INFO = 0xc08c5102
      SNDCTL_TMR_TIMEBASE = 0xc0045401
      SNDCTL_TMR_TEMPO = 0xc0045405
    when /linux/
      SNDCTL_SEQ_NRSYNTHS = 0x8004510a
      SNDCTL_SYNTH_INFO = 0xc08c5102
      SNDCTL_TMR_TIMEBASE = 0xc0045401
      SNDCTL_TMR_TEMPO = 0xc0045405
    else
      raise 'unknown system'
    end

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

    def synth_info(dev)
      templ = 'A30x2i7Lix76'
      a = [0] * 28
      a[0] = ''
      a[1] = dev
      n = a.pack(templ)
      ioctl(SNDCTL_SYNTH_INFO, n)
      n.unpack(templ)
    end

    def timebase(div)
      n = [div].pack('i')
      ioctl(SNDCTL_TMR_TIMEBASE, n)
      n.unpack('i')[0]
    end

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

  end

  class Sequence

    class Play < XSCallback

      def initialize(num) @num = num end

      def header(format, ntrks, division, tc)
	@sq = DevSeq.open('/dev/sequencer2', 'w')
	puts(@sq.synth_info(@num)[0]) if $VERBOSE
	unless @num < @sq.nrsynths
	  raise 'device not available'
	end
	@sq.timebase(division)
	@sq.tempo(120)
      end

      def track_start() @offset = 0 end

      def delta(delta)
	@start_timer ||= (@sq.start_timer; true)
	prev = @offset
	@offset += delta
	if @offset > prev
	  @sq.wait_time(@offset)
	end
      end

      def noteoff(ch, note, vel) @sq.noteoff(@num, ch, note, vel) end
      def noteon(ch, note, vel) @sq.noteon(@num, ch, note, vel) end

      def polyphonickeypressure(ch, note, val)
	@sq.polyphonickeypressure(@num, ch, note, val)
      end

      def controlchange(ch, num, val) @sq.controlchange(@num, ch, num, val) end
      def programchange(ch, num) @sq.programchange(@num, ch, num) end
      def channelpressure(ch, val) @sq.channelpressure(@num, ch, val) end

      def pitchbendchange(ch, val)
	val += 0x2000
	@sq.pitchbendchange(@num, ch, val)
      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)
	i = 0
	while i < data.size
	  len = data.size - i
	  len = 6 if len > 6
	  @sq.sysex(@num, data[i,6], len)
	  i += 6
	end
      end

      private :exclusivefx

      def exclusivef0(data) exclusivefx("\xf0" + data) end
      def exclusivef7(data) exclusivefx(data) end
      def settempo(tempo) @sq.settempo(60000000 / tempo) end

      def timesignature(nn, dd, cc, bb)
	@sq.timesignature(nn << 24 | dd << 16 | cc << 8 | bb)
      end

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

    end

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

  end

end

def usage
  warn 'usage: play-oss2 [-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)
