#! /usr/bin/env ruby

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

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

module SMF

  class Kernel32 < Win32API

    def initialize(proc, import, export)
      super('kernel32', proc, import, export)
    end

  end

  class WinMM < Win32API

    def initialize(proc, import, export)
      super('winmm', proc, import, export)
    end

  end

  module WinBase

    NORMAL_PRIORITY_CLASS = 0x00000020
    IDEL_PRIORITY_CLASS = 0x00000040
    HIGH_PRIORITY_CLASS = 0x00000080
    REALTIME_PRIORITY_CLASS = 0x00000100

    @@GetCurrentProcess = Kernel32.new('GetCurrentProcess', %w(), 'l')
    @@SetPriorityClass = Kernel32.new('SetPriorityClass', %w(l l), 'l')

    def setpriorityclass(prc=NORMAL_PRIORITY_CLASS)
      prh = @@GetCurrentProcess.call
      @@SetPriorityClass.call(prh, prc)
    end

    module_function :setpriorityclass

  end

  class DevMidiOut

    @@GetNumDevs = WinMM.new('midiOutGetNumDevs', %w(), 'l')
    @@GetDevCaps = WinMM.new('midiOutGetDevCaps', %w(l p l), 'l')
    @@Open = WinMM.new('midiOutOpen', %w(p l l l l), 'l')
    @@Reset = WinMM.new('midiOutReset', %w(l), 'l')
    @@Close = WinMM.new('midiOutClose', %w(l), 'l')
    @@ShortMsg = WinMM.new('midiOutShortMsg', %w(l l), 'l')
    @@LongMsg = WinMM.new('midiOutLongMsg', %w(l p l), 'l')
    @@PrepareHeader = WinMM.new('midiOutPrepareHeader', %w(l p l), 'l')
    @@UnprepareHeader = WinMM.new('midiOutUnprepareHeader', %w(l p l), 'l')

    def self.getnumdev() @@GetNumDevs.call end

    def self.getdevcaps(did)
      caps = "\000" * 52
      @@GetDevCaps.call(did, caps, caps.size)
      caps.unpack('S2LZ32S4L')
    end

    def initialize(did)
      mo = "\000" * 4
      @@Open.call(mo, did, 0, 0, 0)
      @mo = mo.unpack('L')[0]
    end

    def reset() @@Reset.call(@mo) end
    def close() @@Close.call(@mo) end
    def shortmsg(msg) @@ShortMsg.call(@mo, msg) end

    def longmsg(msg)
      moh = [msg, msg.size, 0, 0, 0, 0, 0, 0, ''].pack('PL7A32')
      @@PrepareHeader.call(@mo, moh, moh.size)
      @@LongMsg.call(@mo, moh, moh.size)
      @@UnprepareHeader.call(@mo, moh, moh.size)
    end

  end

  class Sequence

    class Timer

      def initialize() @start = Time.now end
      def elapse() Time.now - @start end

    end

    class Play < XSCallback

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

      def header(format, ntrks, division, tc)
	WinBase.setpriorityclass(WinBase::HIGH_PRIORITY_CLASS)
	ndev = DevMidiOut.getnumdev
	puts(DevMidiOut.getdevcaps(@num)[3]) if $VERBOSE
	unless @num < ndev
	  raise 'device not available'
	end
	@mo = DevMidiOut.new(@num)
      end

      def track_start() @offset = 0 end

      def delta(delta)
	@timer ||= Timer.new
	if delta.nonzero?
	  @offset += delta
	  e = @tm.offset2elapse(@offset) - @timer.elapse
	  if e > 0
	    sleep(e.to_f)
	  end
	end
      end

      def midimsg(sb, db1, db2=0)
	@mo.shortmsg(sb | (db1 << 8) | (db2 << 16))
      end

      private :midimsg

      def noteoff(ch, note, vel) midimsg(ch | 0x80, note, vel) end
      def noteon(ch, note, vel) midimsg(ch | 0x90, note, vel) end

      def polyphonickeypressure(ch, note, val)
	midimsg(ch | 0xa0, note, val)
      end

      def controlchange(ch, num, val) midimsg(ch | 0xb0, num, val) end
      def programchange(ch, num) midimsg(ch | 0xc0, num) end
      def channelpressure(ch, val) midimsg(ch | 0xd0, val) end

      def pitchbendchange(ch, val)
	val += 0x2000
	lsb =  val       & 0x7f
	msb = (val >> 7) & 0x7f
	midimsg(ch | 0xe0, lsb, 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) @mo.longmsg(data) end

      private :exclusivefx

      def exclusivef0(data) exclusivefx("\xf0" + data) end
      def exclusivef7(data) exclusivefx(data) end

      def result() @mo.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-win [-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)
