"""

================================================================================

	mingus - Music theory Python package, MIDI Track
	Copyright (C) 2008-2009, Bart Spaans

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    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 General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

================================================================================

	The [refMingusMidiMiditrack MidiTrack] class is kept in this module 
	and provides methods for working with MIDI data as bytes.

	The MIDI file format specification I used can be found here:
	http://www.sonicspot.com/guide/midifiles.html

================================================================================

"""

from binascii import a2b_hex
from struct import pack, unpack
from math import log
from MidiEvents import *
from mingus.core.diatonic import basic_keys
from mingus.containers.Note import Note

class MidiTrack():
	"""This class is used to generate MIDI events from the
	objects in mingus.containers."""

	track_data = ''
	delta_time = '\x00'
	delay = 0
	bpm = 120
	change_instrument = False
	instrument = 1

	def __init__(self, start_bpm = 120):
		self.track_data =''
		self.set_tempo(start_bpm)


	def end_of_track(self):
		"""Returns the bytes for an end of track meta event."""
		return "\x00\xff\x2f\x00"

	def play_Note(self, note):
		"""Converts a Note object to a midi event and adds it \
to the track_data. To set the channel on which to play this note, set \
Note.channel, the same goes for Note.velocity."""
		velocity = 64
		channel = 1
		if hasattr(note, "dynamics"):
			if 'velocity' in note.dynamics:
				velocity = note.dynamics["velocity"]
			if 'channel' in note.dynamics:
				channel = note.dynamics["channel"]
		if hasattr(note, "channel"):
			channel = note.channel
		if hasattr(note, "velocity"):
			velocity = note.velocity

		if self.change_instrument:
			self.set_instrument(channel, self.instrument)
			self.change_instrument = False

		self.track_data += self.note_on(channel, int(note) + 12, velocity)

	def play_NoteContainer(self, notecontainer):
		"""Converts a mingus.containers.NoteContainer to the \
equivalent midi events and adds it to the track_data. Note.channel \
and Note.velocity can be set as well."""
		if len(notecontainer) <= 1:
			[self.play_Note(x) for x in notecontainer]
		else:
			self.play_Note(notecontainer[0])
			self.set_deltatime(0)
			[self.play_Note(x) for x in notecontainer[1:]]



	def play_Bar(self, bar):
		"""Converts a Bar object to MIDI events and writes them \
to the track_data."""
		self.set_deltatime(self.delay)
		self.delay = 0

		self.set_meter(bar.meter)
		self.set_deltatime(0)
		self.set_key(bar.key)
		for x in bar:
			tick = int(round((1.0 / x[1] * 288)))
			if x[2] is None or len(x[2]) == 0:
				self.delay += tick
			else:
				self.set_deltatime(self.delay)
				self.delay = 0
				if hasattr(x[2], "bpm"):
					self.set_deltatime(0)
					self.set_tempo(x[2].bpm)
				self.play_NoteContainer(x[2])

				self.set_deltatime(self.int_to_varbyte(tick))
				self.stop_NoteContainer(x[2])

	def play_Track(self, track):
		"""Converts a Track object to MIDI events and writes \
them to the track_data."""
		if hasattr(track, "name"):
			self.set_track_name(track.name)
		self.delay = 0
		instr = track.instrument
		if hasattr(instr, "instrument_nr"):
			self.change_instrument = True
			self.instrument = instr.instrument_nr
		for bar in track:
			self.play_Bar(bar)


	def stop_Note(self, note):
		"""Adds a note_off event for note to event_track"""
		velocity = 64
		channel = 1
		if hasattr(note, "dynamics"):
			if 'velocity' in note.dynamics:
				velocity = note.dynamics["velocity"]
			if 'channel' in note.dynamics:
				channel = note.dynamics["channel"]
		if hasattr(note, "channel"):
			channel = note.channel
		if hasattr(note, "velocity"):
			velocity = note.velocity


		self.track_data += self.note_off(channel, int(note) + 12,
					velocity)


	def stop_NoteContainer(self, notecontainer):
		"""Adds note_off events for each note in the \
NoteContainer to the track_data."""

		# if there is more than one note in the container, 
		# the deltatime should be set back to zero after the 
		# first one has been stopped
		if len(notecontainer) <= 1:
			[self.stop_Note(x) for x in notecontainer]
		else:
			self.stop_Note(notecontainer[0])
			self.set_deltatime(0)
			[self.stop_Note(x) for x in notecontainer[1:]]


	def set_instrument(self, channel, instr, bank = 1):
		"""Adds an program change and bank select event \
to the track_data"""
		self.track_data += self.select_bank(channel, bank)
		self.track_data += self.program_change_event(channel, instr)

	def header(self):
		"""Returns the bytes for the header of track. NB. \
The header contains the length of the track_data, so \
you'll have to call this function when you're done \
adding data (when you're not using get_midi_data)."""
		chunk_size = a2b_hex("%08x" % (len(self.track_data) +\
				len(self.end_of_track())))
		return TRACK_HEADER + chunk_size

	def get_midi_data(self):
		"""Returns the MIDI data in bytes for this track. \
Includes header, track_data and the end of track \
meta event."""
		return self.header() + self.track_data + self.end_of_track()

	def midi_event(self, event_type, channel, param1, param2 = None):
		"""Converts and returns the paraters as a MIDI event in bytes."""
		"""Parameters should be given as integers."""
		"""event_type and channel: 4 bits."""
		"""param1 and param2: 1 byte."""
		assert event_type < 128 and event_type >= 0
		assert channel < 16 and channel >= 0
		tc = a2b_hex("%x%x" % (event_type, channel))

		if param2 is None:
			params = a2b_hex("%02x" % (param1))
		else:
			params = a2b_hex("%02x%02x" % (param1, param2))
		return self.delta_time + tc + params


	def note_off(self, channel, note, velocity):
		"""Returns bytes for a `note off` event."""
		return self.midi_event(NOTE_OFF, channel, note, velocity)

	def note_on(self, channel, note, velocity):
		"""Returns bytes for a `note_on` event."""
		return self.midi_event(NOTE_ON, channel, note, velocity)

	def controller_event(self, channel, contr_nr, contr_val):
		"""Returns the bytes for a MIDI controller event."""
		return self.midi_event(CONTROLLER, channel, contr_nr, contr_val)

	def reset(self):
		"""Resets track_data and delta_time."""
		self.track_data = ''
		self.delta_time = '\x00'

	def set_deltatime(self, delta_time):
		"""Sets the delta_time. Can be an integer or a \
variable length byte."""
		if type(delta_time) == int:
			delta_time = self.int_to_varbyte(delta_time)

		self.delta_time = delta_time

	def select_bank(self, channel, bank):
		"""Returns the MIDI event for a select bank \
controller event."""
		return self.controller_event(BANK_SELECT, channel, bank)


	def program_change_event(self, channel, instr):
		"""Returns the bytes for a program change \
controller event."""
		return self.midi_event(PROGRAM_CHANGE, channel, instr)

	def set_tempo(self, bpm):
		"""Converts the bpm to a midi event and writes it to the track_data"""
		self.bpm = bpm
		self.track_data += self.set_tempo_event(self.bpm)


	def set_tempo_event(self, bpm):
		"""Calculates the microseconds per quarter note """
		"""and returns tempo event."""
		ms_per_min = 60000000
		mpqn = a2b_hex("%06x" % (ms_per_min / bpm))
		return self.delta_time + META_EVENT + SET_TEMPO + \
				"\x03" + mpqn
	def set_meter(self, meter = (4,4)):
		"""Adds a time signature event for meter to track_data"""
		self.track_data += self.time_signature_event(meter)

	def time_signature_event(self, meter = (4,4)):
		"""Returns a time signature event for meter."""
		numer = a2b_hex("%02x" % meter[0])
		denom = a2b_hex("%02x" % int(log(meter[1], 2)))
		return self.delta_time + META_EVENT + TIME_SIGNATURE + \
				"\x04" + numer + denom + "\x18\x08" 

	def set_key(self, key = 'C'):
		"""Adds a key signature event to the track_data """
		if isinstance(key, Note):
			key = key.name
		self.track_data += self.key_signature_event(key)

	def key_signature_event(self, key = 'C'):
		"""Returns the bytes for a key signature event."""
		val = basic_keys.index(key) - 6
		if val < 0:
			val = 256 + val

		key = a2b_hex("%02x" % val)
		return self.delta_time + META_EVENT + KEY_SIGNATURE + \
				"\x02" + key + "\x00"
		
	def set_track_name(self, name):
		"""Adds a meta event for the track."""
		self.track_data += self.track_name_event(name)
	
	def track_name_event(self, name):
		"""Returns the bytes for a track name meta event."""
		l = self.int_to_varbyte(len(name))
		return "\x00" + META_EVENT + TRACK_NAME + l + name



	def int_to_varbyte(self, value):
		"""A lot of MIDI variables can be of variable length. \
This method converts an integer into a variable length byte. \
How it works: the bytes are stored in big-endian (significant bit first), \
the highest bit of the byte (mask 0x80) is set when there are more \
bytes following. The remaining 7 bits (mask 0x7F) are used to store the \
value."""
		# Warning: bit kung-fu ahead.
		# The length of the integer in bytes
		length = int(log(max(value, 1), 128)) + 1
	
		# Remove the highest bit and move the bits to the right
		# if length > 1
		bytes = [(value >> (i*7)) & 0x7F for i in range(length)]
		bytes.reverse()

		# Set the first bit on every one but the last bit.
		for i in range(len(bytes)-1):
			bytes[i] = bytes[i] | 0x80

		return pack('%sB' % len(bytes), *bytes)


