Created
October 24, 2013 10:20
-
-
Save maddievision/7134674 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module NLMidi | |
require "unimidi" | |
#standard midi events | |
STATUS_NOTE_OFF = 0x80 | |
STATUS_NOTE_ON = 0x90 | |
STATUS_NOTE_PRESSURE = 0xA0 | |
STATUS_CONTROL = 0xB0 | |
STATUS_PROGRAM = 0xC0 | |
STATUS_PRESSURE = 0xD0 | |
STATUS_PITCH_WHEEL = 0xE0 | |
#meta events | |
META_EVENT = 0xFF | |
META_ID_SEQ_ID = 0x00 | |
META_ID_TEXT = 0x01 | |
META_ID_COPYRIGHT = 0x02 | |
META_ID_SEQ_NAME = 0x03 | |
META_ID_INST_NAME = 0x04 | |
META_ID_LYRIC = 0x05 | |
META_ID_MARKER = 0x06 | |
META_ID_CUE_POINT = 0x07 | |
META_ID_END = 0x2F | |
META_ID_TEMPO = 0x51 | |
META_ID_TIME_SIG = 0x58 | |
META_ID_KEY_SIG = 0x59 | |
META_ID_INFO = 0x7F | |
NOTEBASE = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"] | |
NOTEFLAT = ["","D-","","E-","","","F-","","G-","","A-"] | |
class ::String | |
def to_note | |
x, n, o = self.split /([A-G][#\-]?)([0-9]+)/ | |
(o.to_i * 12) + (NOTEBASE.index(n) || NOTEFLAT.index(n)) | |
end | |
end | |
class ::Array | |
def to_notes | |
return self.map {|x| x.kind_of?(Array) ? x.to_notes : x.to_note} | |
end | |
end | |
class MidiEventPlayer | |
@ppqn = 24 | |
def set_ppqn(val) | |
@ppqn = val | |
end | |
end | |
class MidiPlayer < MidiEventPlayer | |
def initialize | |
@midiout = nil | |
@tempo = 500000 #120 BPM | |
@ppqn = 24 | |
@verbose = false | |
@speed = 1.0 | |
@output = nil | |
end | |
attr_accessor :verbose | |
attr_accessor :speed | |
def open(portnum=0) | |
@midiout = UniMIDI::Output.open(portnum) | |
end | |
def list | |
# ports = [] | |
# @midiout.port_names.each_with_index{|name,index| ports << [index, name] } | |
end | |
def out(*args) | |
# pp args | |
@midiout.puts *args | |
append_output args.map {|x| "%02X" % x}.join " " + "\n" if @verbose | |
end | |
def gm_reset | |
out 0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7 | |
end | |
def all_notes_off | |
for i in 0..15 | |
out 0xB0+i, 123, 0 | |
end | |
end | |
def disable_reverb | |
for i in 0..15 | |
out 0xB0 + i, 91, 0 | |
end | |
end | |
def play_async(sequence) | |
Enumerator.new do |en| | |
@ppqn = sequence.ppqn | |
playertrack = MidiTrack.new | |
sequence.tracks.each {|t| playertrack.merge_with t} | |
playertrack.get_event_list.each do |e| | |
if e[0] > 0 | |
sleepspeed = ((e[0].to_f / @ppqn.to_f) * (@tempo.to_f / 1000000.0)) / @speed | |
sleep sleepspeed | |
append_output "\n" if verbose | |
en.yield sleepspeed | |
end | |
case e[1][0] | |
when META_EVENT | |
case e[1][1] | |
when META_ID_TEMPO | |
@tempo = (e[1][3] << 16) + (e[1][4] << 8) + e[1][5] | |
end | |
when 0x80..0xEF | |
self.out *e[1] | |
end | |
end | |
end | |
end | |
def play(sequence) | |
e = play_async(sequence) | |
for s in e | |
o = get_output | |
puts o unless o.nil? | |
end | |
end | |
def append_output(text) | |
if @output.nil? | |
@output = text | |
else | |
@output += text | |
end | |
end | |
def get_output | |
a = @output | |
@output = nil | |
a | |
end | |
end | |
class MidiSequence | |
def initialize(ppqn=24,format=0,*args) | |
@ppqn = ppqn | |
@format = format | |
@tracks = args | |
end | |
attr_reader :tracks | |
attr_reader :ppqn | |
attr_reader :format | |
def read(fn) | |
File.open(fn,"rb") do |f| | |
header, clen, @format, trkcount, @ppqn = f.read(14).unpack("A4Nnnn") | |
if header != "MThd" | |
raise "Invalid MIDI File Header" | |
elsif clen != 6 | |
raise "Invalid MIDI File Header length" | |
end | |
puts "Format #{@format} Tracks #{trkcount} PPQN #{@ppqn}" | |
for i in 1..trkcount | |
puts "Track #{i}" | |
header, tlen = f.read(8).unpack("A4N") | |
if header != "MTrk" | |
raise "Invalid MIDI Track Header" | |
end | |
t = MidiTrack.new | |
rstat = 0 | |
endoftrack = false | |
while !endoftrack | |
vlq = 0 | |
while 1 | |
c = f.read(1).unpack("C")[0] | |
vlq = (vlq << 7) + (c & 0x7F) | |
if (c & 0x80) == 0 | |
break | |
end | |
end | |
t.advance vlq | |
# puts "vlq: #{vlq}" | |
ev = [] | |
c = f.read(1).unpack("C")[0] | |
case c | |
when 0..0x7F | |
ev << rstat | |
ev << c | |
if rstat < 0xC0 or rstat > 0xDF | |
ev << f.read(1).unpack("C")[0] | |
end | |
# puts "runstat" | |
# p ev.map {|x| x.to_s(16)} | |
when META_EVENT | |
ev << c | |
m,l = f.read(2).unpack("CC") | |
ev += [m,l] | |
if m == META_ID_END | |
endoftrack = true | |
end | |
if l > 0 | |
ev += f.read(l).unpack("C" * l) | |
end | |
# puts "meta" | |
# p ev.map {|x| x.to_s(16)} | |
when 0x80..0xEF | |
rstat = c | |
ev << rstat | |
if rstat < 0xC0 or rstat > 0xDF | |
ev += f.read(2).unpack("CC") | |
else | |
ev += f.read(1).unpack("C") | |
end | |
# puts "stat" | |
# p ev.map {|x| x.to_s(16)} | |
when 0xF0 | |
ev << c | |
while 1 | |
d = f.read(1).unpack("C")[0] | |
ev << d | |
if d == 0xF7 | |
break | |
end | |
end | |
# puts "sysex" | |
# p ev.map {|x| x.to_s(16)} | |
else | |
raise "Unknown MIDI Event #{c}" | |
end | |
t.write ev | |
end | |
tracks << t | |
end | |
end | |
end | |
end | |
class MidiWriter < MidiEventPlayer | |
def vlq(val) #convert int to MIDI variable length bytes | |
out = [val & 0x7F] | |
val = val >> 7 | |
while val > 0 | |
out << (val & 0x7f) + 0x80 | |
val = val >> 7 | |
end | |
out.reverse | |
end | |
def end_of_track | |
[META_EVENT, META_ID_END, 0] | |
end | |
def write(fn,sequence) | |
File.open(fn,"wb") do |f| | |
f.write ["MThd", 6, (args.length) > 1 ? (sequence.format) : 0, args.length, sequence.ppqn].pack "A4Nnnn" #header chunk | |
sequence.tracks.each do |trk| | |
events = trk.get_event_list | |
track = [] | |
runstatus = -1 | |
lmeta = 0 | |
events.each do |e| | |
track += self.vlq e[0] | |
case e[1][0] | |
when runstatus | |
track += e[1][1..-1] | |
when 0x80..0xEF #standard midi status event | |
runstatus = e[1][0] | |
track += e[1] | |
when META_EVENT | |
track += e[1] | |
lmeta = e[1][1] | |
end | |
end | |
track += self.vlq(0) + self.end_of_track if lmeta != META_ID_END | |
f.write ["MTrk", track.length].pack "A4N" # track chunk | |
f.write track.pack "c" * track.length | |
end | |
end | |
end | |
end | |
class MidiEvents | |
def note_off(ch,note) | |
[STATUS_NOTE_OFF + ch, note, 0] | |
end | |
def note_on(ch,note,vel=127) | |
[STATUS_NOTE_ON + ch, note, vel] | |
end | |
def note_pressure(ch,note,val) | |
[STATUS_NOTE_PRESSURE + ch, note, val] | |
end | |
def control(ch,cc,val) | |
[STATUS_CONTROL + ch, cc, val] | |
end | |
def program(ch,prg) | |
[STATUS_PROGRAM + ch, prg] | |
end | |
def pressure(ch,val) | |
[STATUS_PRESSURE + ch, val] | |
end | |
def pitch_wheel(ch,pitch,depth=12) | |
pitch_int = [[((pitch.to_f / depth.to_f) * (0x1FFF).to_f).to_i + 0x2000, | |
0].max, 0x3FFF].min | |
[STATUS_PITCH_WHEEL + ch, pitch_int & 0x7F, (pitch_int >> 7) & 0x7F] | |
end | |
def meta(meta_id,*data) | |
[META_EVENT, meta_id, data.length] + data | |
end | |
def tempo(bpm) | |
bpm_int = (60000000. / bpm.to_f).to_int | |
meta(META_ID_TEMPO, (bpm_int >> 16) & 0xFF, (bpm_int >> 8) & 0xFF, bpm_int & 0xFF) | |
end | |
end | |
class MidiEvent | |
def initialize(timestamp, eid, *data) | |
@timestamp = timestamp | |
@eid = eid | |
@data = data | |
end | |
attr_accessor :timestamp, :eid, :data | |
def status_type | |
@data[0] & 0xF0 | |
end | |
def status_channel | |
@data[0] & 0x0F | |
end | |
def arg1 | |
@data[1] | |
end | |
def arg2 | |
@data[2] | |
end | |
def is_note_on? | |
(status_type == 0x90) && (arg2 > 0) | |
end | |
def is_note_off? | |
(status_type == 0x80) || ((status_type == 0x90) & (arg2 == 0)) | |
end | |
def is_program? | |
(status_type == 0xC0) | |
end | |
def is_control? | |
(status_type == 0xB0) | |
end | |
def is_pitch? | |
(status_type == 0xE0) | |
end | |
def note | |
arg1 | |
end | |
def velocity | |
arg2 | |
end | |
def program | |
arg1 | |
end | |
def control | |
arg1 | |
end | |
def value | |
arg2 | |
end | |
def pitch(depth=1) | |
((((arg1) + (arg2 << 7)) - 0x2000).to_f / 0x3FFF.to_f) * depth.to_f | |
end | |
end | |
class MidiTrack | |
def initialize | |
@p = 0 | |
@elid = 0 | |
@e = MidiEvents.new | |
@events = [] | |
@pstack = [] | |
end | |
attr_reader :events | |
def merge_with(track) | |
for e in track.events | |
@events << MidiEvent.new(e.timestamp,@elid,*e.data) | |
@elid += 1 | |
end | |
end | |
def reset | |
@p = 0 | |
@pstack = [] | |
@elid = 0 | |
end | |
def advance(t) | |
@p += t | |
end | |
def seek(t) | |
@p = t | |
end | |
def push | |
@pstack << @p | |
end | |
def pop | |
@p = @pstack.pop | |
end | |
def rewind | |
@p = @pstack.last | |
end | |
def write(event) | |
@events << MidiEvent.new(@p,@elid,*event) | |
@elid += 1 | |
end | |
def note(ch,vel,len,*args) | |
for nset in args | |
notes = nset.kind_of?(Array) ? nset : [nset] | |
for n in notes | |
self.write @e.note_on ch,n,vel if n > -1 | |
end | |
self.advance len | |
for n in notes | |
self.write @e.note_off ch,n if n > -1 | |
end | |
end | |
end | |
def program(ch,prg) | |
self.write @e.program ch, prg | |
end | |
def volume(ch,vol) | |
self.write @e.control ch, 7, vol | |
end | |
def pan(ch,val) | |
self.write @e.control ch, 10, val | |
end | |
def expression(ch,val) | |
self.write @e.control ch, 11, val | |
end | |
def sustain(ch,val) | |
self.write @e.control ch, 64, (val ? 127 : 0) | |
end | |
def rpn_select(ch,msb,lsb) | |
self.write @e.control ch, 0x65, msb | |
self.write @e.control ch, 0x64, lsb | |
end | |
def rpn_write(ch,msb,lsb) | |
self.write @e.control ch, 0x06, msb | |
self.write @e.control ch, 0x38, lsb | |
end | |
def pitch_wheel_depth(ch,semitones=2,cents=0) | |
semitones = semitones + 0x80 if semitones < 0 | |
cents = cents + 0x80 if cents < 0 | |
rpn_select ch,0,0 | |
rpn_write semitones,cents | |
end | |
def tempo(bpm) | |
self.write @e.tempo bpm | |
end | |
def get_event_list | |
es = @events.sort {|a,b| [a.timestamp,a.eid] <=> [b.timestamp,b.eid] } | |
eventlist = [ [0, es[0].data] ] | |
es.each_cons(2) {|a,b| eventlist << [b.timestamp-a.timestamp, b.data]} | |
eventlist | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment