#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
require 'dl/import'

module Enumerable
def rest
return [] if empty?
self[1..-1]
end
end

class LiveMIDI
ON = 0x90
OFF = 0x80
PC = 0xC0

attr_reader :interval

@@singleton = nil
def self.use(bpm=120)
return @@singleton = self.new(bpm) if @@singleton.nil?
@@singleton.bpm = bpm
return @@singleton
end

def initialize(bpm=120)
self.bpm = bpm
@timer = Timer.get(@interval/10)
open
end

def bpm=(bpm)
@interval = 60.0 / bpm
end

def play(channel, note, duration, velocity=100, time=nil)
on_time = time || Time.now.to_f
@timer.at(on_time) { note_on(channel, note, velocity) }

off_time = on_time + duration
@timer.at(off_time) { note_off(channel, note, velocity) }
end

def note_on(channel, note, velocity=64)
message(ON | channel, note, velocity)
end

def note_off(channel, note, velocity=64)
message(OFF | channel, note, velocity)
end

def program_change(channel, preset)
message(PC | channel, preset)
end
end

class NoMIDIDestinations < Exception; end

if RUBY_PLATFORM.include?('mswin')

class LiveMIDI
module C
extend DL::Importable
dlload 'winmm'

extern "int midiOutOpen(HMIDIOUT*, int, int, int, int)"
extern "int midiOutClose(int)"
extern "int midiOutShortMsg(int, int)"
end

def open
@device = DL.malloc(DL.sizeof('I'))
C.midiOutOpen(@device, -1, 0, 0, 0)
end

def close
C.midiOutClose(@device.ptr.to_i)
end

def message(one, two=0, three=0)
message = one + (two << 8) + (three << 16)
C.midiOutShortMsg(@device.ptr.to_i, message)
end
end

elsif RUBY_PLATFORM.include?('darwin')

class LiveMIDI
module C
extend DL::Importable
dlload '/System/Library/Frameworks/CoreMIDI.framework/Versions/Current/CoreMIDI'

extern "int MIDIClientCreate(void *, void *, void *, void *)"
extern "int MIDIClientDispose(void *)"
extern "int MIDIGetNumberOfDestinations()"
extern "void * MIDIGetDestination(int)"
extern "int MIDIOutputPortCreate(void *, void *, void *)"
extern "void * MIDIPacketListInit(void *)"
extern "void * MIDIPacketListAdd(void *, int, void *, int, int, int, void *)"
extern "int MIDISend(void *, void *, void *)"
end

module CF
extend DL::Importable
dlload '/System/Library/Frameworks/CoreFoundation.framework/Versions/Current/CoreFoundation'

extern "void * CFStringCreateWithCString (void *, char *, int)"
end

def open
client_name = CF.cFStringCreateWithCString(nil, "RubyMIDI", 0)
@client = DL::PtrData.new(nil)
C.mIDIClientCreate(client_name, nil, nil, @client.ref);

port_name = CF.cFStringCreateWithCString(nil, "Output", 0)
@outport = DL::PtrData.new(nil)
C.mIDIOutputPortCreate(@client, port_name, @outport.ref);

num = C.mIDIGetNumberOfDestinations()
raise NoMIDIDestinations if num < 1
@destination = C.mIDIGetDestination(0)
end

def close
C.mIDIClientDispose(@client)
end

def message(*args)
format = "C" * args.size
bytes = args.pack(format).to_ptr
packet_list = DL.malloc(256)
packet_ptr = C.mIDIPacketListInit(packet_list)
# Pass in two 32 bit 0s for the 64 bit time
packet_ptr = C.mIDIPacketListAdd(packet_list, 256, packet_ptr, 0, 0, args.size, bytes)
C.mIDISend(@outport, @destination, packet_list)
end
end

elsif RUBY_PLATFORM.include?('linux')

class LiveMIDI
module C
extend DL::Importable
dlload 'libasound.so'

extern "int snd_rawmidi_open(void*, void*, char*, int)"
extern "int snd_rawmidi_close(void*)"
extern "int snd_rawmidi_write(void*, void*, int)"
extern "int snd_rawmidi_drain(void*)"
end

def open
@output = DL::PtrData.new(nil)
C.snd_rawmidi_open(nil, @output.ref, "virtual", 0)
end

def close
C.snd_rawmidi_close(@output)
end

def message(*args)
format = "C" * args.size
bytes = args.pack(format).to_ptr
C.snd_rawmidi_write(@output, bytes, args.size)
C.snd_rawmidi_drain(@output)
end
end

else
raise "Couldn't find a LiveMIDI implementation for your platform"
end

class Timer
def self.get(interval)
@timers ||= {}
return @timers[interval] if @timers[interval]
return @timers[interval] = self.new(interval)
end

def initialize(resolution)
@resolution = resolution
@queue = []

Thread.new do
while true
dispatch
sleep(@resolution)
end
end
end
end