Skip to content

Instantly share code, notes, and snippets.

@maddievision
Created October 24, 2013 10:20
Show Gist options
  • Save maddievision/7134674 to your computer and use it in GitHub Desktop.
Save maddievision/7134674 to your computer and use it in GitHub Desktop.
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