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