#! /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)