#!/usr/bin/python
# -*- coding: utf-8 -*-
# mingus - Music theory Python package, midi_track module.
# Copyright (C) 2008-2009, Bart Spaans
# Copyright (C) 2011, Carlo Stemberger
#
# 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/>.
"""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 midi_events import *
from mingus.core.keys import Key, major_keys, minor_keys
from mingus.containers.note import Note
[docs]class MidiTrack(object):
"""A class 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
[docs] def __init__(self, start_bpm=120):
self.track_data = ''
self.set_tempo(start_bpm)
[docs] def end_of_track(self):
"""Return the bytes for an end of track meta event."""
return "\x00\xff\x2f\x00"
[docs] def play_Note(self, note):
"""Convert 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)
[docs] def play_NoteContainer(self, notecontainer):
"""Convert a mingus.containers.NoteContainer to the equivalent MIDI
events and add 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:]]
[docs] def play_Bar(self, bar):
"""Convert a Bar object to MIDI events and write 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])
[docs] def play_Track(self, track):
"""Convert a Track object to MIDI events and write 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)
[docs] def stop_Note(self, note):
"""Add 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)
[docs] def stop_NoteContainer(self, notecontainer):
"""Add 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:]]
[docs] def set_instrument(self, channel, instr, bank=1):
"""Add a 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)
[docs] def get_midi_data(self):
"""Return the MIDI data in bytes for this track.
Include header, track_data and the end of track meta event.
"""
return self.header() + self.track_data + self.end_of_track()
[docs] def midi_event(self, event_type, channel, param1, param2=None):
"""Convert and return the paraters as a MIDI event in bytes."""
assert event_type < 0x80 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
[docs] def note_off(self, channel, note, velocity):
"""Return bytes for a 'note off' event."""
return self.midi_event(NOTE_OFF, channel, note, velocity)
[docs] def note_on(self, channel, note, velocity):
"""Return bytes for a 'note_on' event."""
return self.midi_event(NOTE_ON, channel, note, velocity)
[docs] def controller_event(self, channel, contr_nr, contr_val):
"""Return the bytes for a MIDI controller event."""
return self.midi_event(CONTROLLER, channel, contr_nr, contr_val)
[docs] def reset(self):
"""Reset track_data and delta_time."""
self.track_data = ''
self.delta_time = '\x00'
[docs] def set_deltatime(self, delta_time):
"""Set 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
[docs] def select_bank(self, channel, bank):
"""Return the MIDI event for a select bank controller event."""
return self.controller_event(BANK_SELECT, channel, bank)
[docs] def program_change_event(self, channel, instr):
"""Return the bytes for a program change controller event."""
return self.midi_event(PROGRAM_CHANGE, channel, instr)
[docs] def set_tempo(self, bpm):
"""Convert the bpm to a midi event and write it to the track_data."""
self.bpm = bpm
self.track_data += self.set_tempo_event(self.bpm)
[docs] def set_tempo_event(self, bpm):
"""Calculate the microseconds per quarter note."""
ms_per_min = 60000000
mpqn = a2b_hex('%06x' % (ms_per_min / bpm))
return self.delta_time + META_EVENT + SET_TEMPO + '\x03' + mpqn
[docs] def set_meter(self, meter=(4, 4)):
"""Add a time signature event for meter to track_data."""
self.track_data += self.time_signature_event(meter)
[docs] def time_signature_event(self, meter=(4, 4)):
"""Return 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'
[docs] def set_key(self, key='C'):
"""Add a key signature event to the track_data."""
if isinstance(key, Key):
key = key.name[0]
self.track_data += self.key_signature_event(key)
[docs] def key_signature_event(self, key='C'):
"""Return the bytes for a key signature event."""
if key.islower():
val = minor_keys.index(key) - 7
mode = '\x01'
else:
val = major_keys.index(key) - 7
mode = '\x00'
if val < 0:
val = 256 + val
key = a2b_hex('%02x' % val)
return '{0}{1}{2}\x02{3}{4}'.format(self.delta_time, META_EVENT,
KEY_SIGNATURE, key, mode)
[docs] def set_track_name(self, name):
"""Add a meta event for the track."""
self.track_data += self.track_name_event(name)
[docs] def track_name_event(self, name):
"""Return the bytes for a track name meta event."""
l = self.int_to_varbyte(len(name))
return '\x00' + META_EVENT + TRACK_NAME + l + name
[docs] def int_to_varbyte(self, value):
"""Convert 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), 0x80)) + 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)