#! /usr/bin/env ruby

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

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

module SMF

  class Wave

    def initialize(rate=8000)
      @rate = rate
      @wave = []
    end

    def sin(start, stop, freq, amp)
      (start..stop).each do |i|
	r = amp * Math.sin(2 * Math::PI * freq * (i.to_f / @rate))
	@wave[i] ||= 0.0
	@wave[i] += r
      end
    end

    def dump
      out = [ 'RIFF', @wave.length + 36, 'WAVE', 'fmt', 16, 1, 1, @rate, @rate,
	1, 8, 'data', @wave.length].pack('A4 V A4 A4 V v v V V v v A4 V')
      @wave.collect!{|x| x || 0.0}
      peak = @wave.max
      @wave.each_with_index do |x, i|
	@wave[i] = 128 + (127 * x / peak).round
      end
      out << @wave.pack('C*')
      out
    end

  end

  class Sequence

    class EncodeWav < XSCallback

      FREQ = []
      (0..127).each do |n|
	FREQ << 440 * 2**((n-69.0)/12)
      end

      def initialize(tm)
	@tm = tm
	@rate = 8000
	@wave = Wave.new(@rate)
      end

      def track_start
	@offset = 0
	@noteon = []
      end

      def delta(delta) @offset += delta end

      def noteoff(ch, note, vel)
	return if ch == 9	# GM perc.
	val = (@noteon[ch] || {}).delete(note)
	if val
	  start, vel = val
	  @wave.sin(pos(start), pos(@offset), FREQ[note], vel)
	end
      end

      def noteon(ch, note, vel)
	return if ch == 9	# GM perc.
	@noteon[ch] ||= {}
	@noteon[ch][note] ||= [@offset, vel]
      end

      def pos(offset)
	e = @tm.offset2elapse(offset)
	(e * @rate).round
      end

      def result() @wave.dump end

    end

    def encode_wav
      tm = TempoMap.new(self)
      self.class::WS.new(self, self.class::EncodeWav.new(tm)).read
    end

    def write_wav(io)
      io.binmode.write(encode_wav)
    end

    def save_wav(fn)
      open(fn, 'w') do |io|
	write_wav(io)
      end
    end

  end

end

def usage
  warn 'usage: smf2wav [-o output] [input]'
  exit 1
end

usage unless opt = Gopt.gopt('o:')
usage unless $*.size >= 0 && $*.size <= 1

ifile = $*.shift
ifile = nil if ifile == '-'

ofile = opt[:o] if opt[:o]
ofile ||= File.basename(ifile, '.mid') + '.wav' if ifile
ofile = nil if ofile == '-'

sq = unless ifile then Sequence.read($stdin) else Sequence.load(ifile) end
unless ofile then sq.write_wav($stdout) else sq.save_wav(ofile) end
