You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
448 lines
11 KiB
Python
448 lines
11 KiB
Python
# -*- coding: ISO-8859-1 -*-
|
|
|
|
from MidiOutStream import MidiOutStream
|
|
from RawOutstreamFile import RawOutstreamFile
|
|
|
|
from constants import *
|
|
from DataTypeConverters import fromBytes, writeVar
|
|
|
|
class MidiOutFile(MidiOutStream):
|
|
|
|
|
|
"""
|
|
MidiOutFile is an eventhandler that subclasses MidiOutStream.
|
|
"""
|
|
|
|
|
|
def __init__(self, raw_out=''):
|
|
|
|
self.raw_out = RawOutstreamFile(raw_out)
|
|
MidiOutStream.__init__(self)
|
|
|
|
|
|
def write(self):
|
|
self.raw_out.write()
|
|
|
|
|
|
def event_slice(self, slc):
|
|
"""
|
|
Writes the slice of an event to the current track. Correctly
|
|
inserting a varlen timestamp too.
|
|
"""
|
|
trk = self._current_track_buffer
|
|
trk.writeVarLen(self.rel_time())
|
|
trk.writeSlice(slc)
|
|
|
|
|
|
#####################
|
|
## Midi events
|
|
|
|
|
|
def note_on(self, channel=0, note=0x40, velocity=0x40):
|
|
|
|
"""
|
|
channel: 0-15
|
|
note, velocity: 0-127
|
|
"""
|
|
slc = fromBytes([NOTE_ON + channel, note, velocity])
|
|
self.event_slice(slc)
|
|
|
|
|
|
def note_off(self, channel=0, note=0x40, velocity=0x40):
|
|
|
|
"""
|
|
channel: 0-15
|
|
note, velocity: 0-127
|
|
"""
|
|
slc = fromBytes([NOTE_OFF + channel, note, velocity])
|
|
self.event_slice(slc)
|
|
|
|
|
|
def aftertouch(self, channel=0, note=0x40, velocity=0x40):
|
|
|
|
"""
|
|
channel: 0-15
|
|
note, velocity: 0-127
|
|
"""
|
|
slc = fromBytes([AFTERTOUCH + channel, note, velocity])
|
|
self.event_slice(slc)
|
|
|
|
|
|
def continuous_controller(self, channel, controller, value):
|
|
|
|
"""
|
|
channel: 0-15
|
|
controller, value: 0-127
|
|
"""
|
|
slc = fromBytes([CONTINUOUS_CONTROLLER + channel, controller, value])
|
|
self.event_slice(slc)
|
|
# These should probably be implemented
|
|
# http://users.argonet.co.uk/users/lenny/midi/tech/spec.html#ctrlnums
|
|
|
|
|
|
def patch_change(self, channel, patch):
|
|
|
|
"""
|
|
channel: 0-15
|
|
patch: 0-127
|
|
"""
|
|
slc = fromBytes([PATCH_CHANGE + channel, patch])
|
|
self.event_slice(slc)
|
|
|
|
|
|
def channel_pressure(self, channel, pressure):
|
|
|
|
"""
|
|
channel: 0-15
|
|
pressure: 0-127
|
|
"""
|
|
slc = fromBytes([CHANNEL_PRESSURE + channel, pressure])
|
|
self.event_slice(slc)
|
|
|
|
|
|
def pitch_bend(self, channel, value):
|
|
|
|
"""
|
|
channel: 0-15
|
|
value: 0-16383
|
|
"""
|
|
msb = (value>>7) & 0xFF
|
|
lsb = value & 0xFF
|
|
slc = fromBytes([PITCH_BEND + channel, msb, lsb])
|
|
self.event_slice(slc)
|
|
|
|
|
|
|
|
|
|
#####################
|
|
## System Exclusive
|
|
|
|
# def sysex_slice(sysex_type, data):
|
|
# ""
|
|
# sysex_len = writeVar(len(data)+1)
|
|
# self.event_slice(SYSTEM_EXCLUSIVE + sysex_len + data + END_OFF_EXCLUSIVE)
|
|
#
|
|
def system_exclusive(self, data):
|
|
|
|
"""
|
|
data: list of values in range(128)
|
|
"""
|
|
sysex_len = writeVar(len(data)+1)
|
|
self.event_slice(chr(SYSTEM_EXCLUSIVE) + sysex_len + data + chr(END_OFF_EXCLUSIVE))
|
|
|
|
|
|
#####################
|
|
## Common events
|
|
|
|
def midi_time_code(self, msg_type, values):
|
|
"""
|
|
msg_type: 0-7
|
|
values: 0-15
|
|
"""
|
|
value = (msg_type<<4) + values
|
|
self.event_slice(fromBytes([MIDI_TIME_CODE, value]))
|
|
|
|
|
|
def song_position_pointer(self, value):
|
|
|
|
"""
|
|
value: 0-16383
|
|
"""
|
|
lsb = (value & 0x7F)
|
|
msb = (value >> 7) & 0x7F
|
|
self.event_slice(fromBytes([SONG_POSITION_POINTER, lsb, msb]))
|
|
|
|
|
|
def song_select(self, songNumber):
|
|
|
|
"""
|
|
songNumber: 0-127
|
|
"""
|
|
self.event_slice(fromBytes([SONG_SELECT, songNumber]))
|
|
|
|
|
|
def tuning_request(self):
|
|
|
|
"""
|
|
No values passed
|
|
"""
|
|
self.event_slice(chr(TUNING_REQUEST))
|
|
|
|
|
|
#########################
|
|
# header does not really belong here. But anyhoo!!!
|
|
|
|
def header(self, format=0, nTracks=1, division=96):
|
|
|
|
"""
|
|
format: type of midi file in [0,1,2]
|
|
nTracks: number of tracks. 1 track for type 0 file
|
|
division: timing division ie. 96 ppq.
|
|
|
|
"""
|
|
raw = self.raw_out
|
|
raw.writeSlice('MThd')
|
|
bew = raw.writeBew
|
|
bew(6, 4) # header size
|
|
bew(format, 2)
|
|
bew(nTracks, 2)
|
|
bew(division, 2)
|
|
|
|
|
|
def eof(self):
|
|
|
|
"""
|
|
End of file. No more events to be processed.
|
|
"""
|
|
# just write the file then.
|
|
self.write()
|
|
|
|
|
|
#####################
|
|
## meta events
|
|
|
|
|
|
def meta_slice(self, meta_type, data_slice):
|
|
"Writes a meta event"
|
|
slc = fromBytes([META_EVENT, meta_type]) + \
|
|
writeVar(len(data_slice)) + data_slice
|
|
self.event_slice(slc)
|
|
|
|
|
|
def meta_event(self, meta_type, data):
|
|
"""
|
|
Handles any undefined meta events
|
|
"""
|
|
self.meta_slice(meta_type, fromBytes(data))
|
|
|
|
|
|
def start_of_track(self, n_track=0):
|
|
"""
|
|
n_track: number of track
|
|
"""
|
|
self._current_track_buffer = RawOutstreamFile()
|
|
self.reset_time()
|
|
self._current_track += 1
|
|
|
|
|
|
def end_of_track(self):
|
|
"""
|
|
Writes the track to the buffer.
|
|
"""
|
|
raw = self.raw_out
|
|
raw.writeSlice(TRACK_HEADER)
|
|
track_data = self._current_track_buffer.getvalue()
|
|
# wee need to know size of track data.
|
|
eot_slice = writeVar(self.rel_time()) + fromBytes([META_EVENT, END_OF_TRACK, 0])
|
|
raw.writeBew(len(track_data)+len(eot_slice), 4)
|
|
# then write
|
|
raw.writeSlice(track_data)
|
|
raw.writeSlice(eot_slice)
|
|
|
|
|
|
|
|
def sequence_number(self, value):
|
|
|
|
"""
|
|
value: 0-65535
|
|
"""
|
|
self.meta_slice(meta_type, writeBew(value, 2))
|
|
|
|
|
|
def text(self, text):
|
|
"""
|
|
Text event
|
|
text: string
|
|
"""
|
|
self.meta_slice(TEXT, text)
|
|
|
|
|
|
def copyright(self, text):
|
|
|
|
"""
|
|
Copyright notice
|
|
text: string
|
|
"""
|
|
self.meta_slice(COPYRIGHT, text)
|
|
|
|
|
|
def sequence_name(self, text):
|
|
"""
|
|
Sequence/track name
|
|
text: string
|
|
"""
|
|
self.meta_slice(SEQUENCE_NAME, text)
|
|
|
|
|
|
def instrument_name(self, text):
|
|
|
|
"""
|
|
text: string
|
|
"""
|
|
self.meta_slice(INSTRUMENT_NAME, text)
|
|
|
|
|
|
def lyric(self, text):
|
|
|
|
"""
|
|
text: string
|
|
"""
|
|
self.meta_slice(LYRIC, text)
|
|
|
|
|
|
def marker(self, text):
|
|
|
|
"""
|
|
text: string
|
|
"""
|
|
self.meta_slice(MARKER, text)
|
|
|
|
|
|
def cuepoint(self, text):
|
|
|
|
"""
|
|
text: string
|
|
"""
|
|
self.meta_slice(CUEPOINT, text)
|
|
|
|
|
|
def midi_ch_prefix(self, channel):
|
|
|
|
"""
|
|
channel: midi channel for subsequent data
|
|
(deprecated in the spec)
|
|
"""
|
|
self.meta_slice(MIDI_CH_PREFIX, chr(channel))
|
|
|
|
|
|
def midi_port(self, value):
|
|
|
|
"""
|
|
value: Midi port (deprecated in the spec)
|
|
"""
|
|
self.meta_slice(MIDI_CH_PREFIX, chr(value))
|
|
|
|
|
|
def tempo(self, value):
|
|
|
|
"""
|
|
value: 0-2097151
|
|
tempo in us/quarternote
|
|
(to calculate value from bpm: int(60,000,000.00 / BPM))
|
|
"""
|
|
hb, mb, lb = (value>>16 & 0xff), (value>>8 & 0xff), (value & 0xff)
|
|
self.meta_slice(TEMPO, fromBytes([hb, mb, lb]))
|
|
|
|
|
|
def smtp_offset(self, hour, minute, second, frame, framePart):
|
|
|
|
"""
|
|
hour,
|
|
minute,
|
|
second: 3 bytes specifying the hour (0-23), minutes (0-59) and
|
|
seconds (0-59), respectively. The hour should be
|
|
encoded with the SMPTE format, just as it is in MIDI
|
|
Time Code.
|
|
frame: A byte specifying the number of frames per second (one
|
|
of : 24, 25, 29, 30).
|
|
framePart: A byte specifying the number of fractional frames,
|
|
in 100ths of a frame (even in SMPTE-based tracks
|
|
using a different frame subdivision, defined in the
|
|
MThd chunk).
|
|
"""
|
|
self.meta_slice(SMTP_OFFSET, fromBytes([hour, minute, second, frame, framePart]))
|
|
|
|
|
|
|
|
def time_signature(self, nn, dd, cc, bb):
|
|
|
|
"""
|
|
nn: Numerator of the signature as notated on sheet music
|
|
dd: Denominator of the signature as notated on sheet music
|
|
The denominator is a negative power of 2: 2 = quarter
|
|
note, 3 = eighth, etc.
|
|
cc: The number of MIDI clocks in a metronome click
|
|
bb: The number of notated 32nd notes in a MIDI quarter note
|
|
(24 MIDI clocks)
|
|
"""
|
|
self.meta_slice(TIME_SIGNATURE, fromBytes([nn, dd, cc, bb]))
|
|
|
|
|
|
|
|
|
|
def key_signature(self, sf, mi):
|
|
|
|
"""
|
|
sf: is a byte specifying the number of flats (-ve) or sharps
|
|
(+ve) that identifies the key signature (-7 = 7 flats, -1
|
|
= 1 flat, 0 = key of C, 1 = 1 sharp, etc).
|
|
mi: is a byte specifying a major (0) or minor (1) key.
|
|
"""
|
|
self.meta_slice(KEY_SIGNATURE, fromBytes([sf, mi]))
|
|
|
|
|
|
|
|
def sequencer_specific(self, data):
|
|
|
|
"""
|
|
data: The data as byte values
|
|
"""
|
|
self.meta_slice(SEQUENCER_SPECIFIC, data)
|
|
|
|
|
|
|
|
|
|
|
|
# #####################
|
|
# ## realtime events
|
|
|
|
# These are of no use in a midi file, so they are ignored!!!
|
|
|
|
# def timing_clock(self):
|
|
# def song_start(self):
|
|
# def song_stop(self):
|
|
# def song_continue(self):
|
|
# def active_sensing(self):
|
|
# def system_reset(self):
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
out_file = 'test/midifiles/midiout.mid'
|
|
midi = MidiOutFile(out_file)
|
|
|
|
#format: 0, nTracks: 1, division: 480
|
|
#----------------------------------
|
|
#
|
|
#Start - track #0
|
|
#sequence_name: Type 0
|
|
#tempo: 500000
|
|
#time_signature: 4 2 24 8
|
|
#note_on - ch:00, note:48, vel:64 time:0
|
|
#note_off - ch:00, note:48, vel:40 time:480
|
|
#End of track
|
|
#
|
|
#End of file
|
|
|
|
|
|
midi.header(0, 1, 480)
|
|
|
|
midi.start_of_track()
|
|
midi.sequence_name('Type 0')
|
|
midi.tempo(750000)
|
|
midi.time_signature(4, 2, 24, 8)
|
|
ch = 0
|
|
for i in range(127):
|
|
midi.note_on(ch, i, 0x64)
|
|
midi.update_time(96)
|
|
midi.note_off(ch, i, 0x40)
|
|
midi.update_time(0)
|
|
|
|
midi.update_time(0)
|
|
midi.end_of_track()
|
|
|
|
midi.eof() # currently optional, should it do the write instead of write??
|
|
|
|
|
|
midi.write() |