joonis Logo

MIDI Sound Generator for Python

midi.py [5.88 KB]
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
#! /usr/bin/env python
# -*- coding: utf-8 -*
#
#  Copyright (c) 2013 joonis new media
#  Author: Thimo Kraemer <thimo.kraemer@joonis.de>
#
#  Based on python-music-gen (MidiFileGenerator.py)
#  http://code.google.com/p/python-music-gen/
#
# 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 2 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 Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02110-1301, USA.


import struct

TICKS_PER_BEAT = 96


class NoteConverter:
    def __init__(self, base=5):
        self.base = base
        names = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
        self.notes = {}
        for note in range(128):
            name = names[note % 12]
            octave = int(note / 12) - base
            key = '%s%i' % (name, octave)
            self.notes[key] = note
            if octave == 0:
                self.notes[name] = note

    def __call__(self, note):
        return self.notes.get(str(note).upper(), note)


class MidiEvent:
    def __init__(self, time, status, channel, param_a=None, param_b=None):
        self.time = time
        self.status = status
        self.channel = channel
        self.param_a = param_a
        self.param_b = param_b

    def _compile(self):
        out = chr(self.status << 4 | self.channel)
        if self.param_a is not None:
            out += chr(self.param_a)
        if self.param_b is not None:
            out += chr(self.param_b)
        return out


class TempoEvent:
    def __init__(self, tempo):
        self.time = 0.
        self.status = 0xff
        self.tempo = tempo

    def _compile(self):
        return '\xff\x51\x03%s' % (struct.pack('>L', int(60000000. / self.tempo))[1:4])


class MidiChannel:
    note_converter = NoteConverter()

    def __init__(self, id):
        if not 0 <= id <= 15:
            raise ValueError('Channel id out of range (0-15)')
        self.id = id
        self.time = 0.
        self.events = []

    def _add_event(self, status, param_a=None, param_b=None):
        event = MidiEvent(self.time, status, self.id, param_a, param_b)
        self.events.append(event)

    def add_note(self, note, duration, velocity=0x40):
        self.add_chord([note], duration, velocity)

    def add_chord(self, notes, duration, velocity=0x40):
        if duration > 0:
            try: v1, v2 = velocity
            except TypeError:
                v1 = v2 = velocity
            notes = map(self.note_converter, notes)
            for note in notes:
                self._add_event(0x9, note, v1)
            self.add_pause(duration)
            for note in notes:
                self._add_event(0x8, note, v2)

    def add_pause(self, duration):
        self.time = self.time + duration

    def change_program(self, program):
        self._add_event(0xc, program)

    def change_control(self, number, value):
        self._add_event(0xb, number, value)


class MidiTrack:
    def __init__(self, tempo):
        self.channels = {}
        self.events = [TempoEvent(tempo)]

    def get_channel(self, channel):
        if channel not in self.channels:
            self.channels[channel] = MidiChannel(channel)
        return self.channels[channel]

    __call__ = get_channel

    def _variable_length(self, value):
        out = [chr(value & 0x7f)]
        value = value >> 7
        while value:
            out.append(chr(value & 0x7f | 0x80))
            value = value >> 7
        out.reverse()
        return ''.join(out)

    def _compile(self):
        events = self.events[:]
        for channel in self.channels.values():
            events.extend(channel.events)
        # Sort events by time, NoteOff events first, status flag desc
        events.sort(key=lambda e: (e.time, e.status != 0x8, -e.status))
        out = ''
        time = 0.
        for event in events:
            delta = event.time - time
            time = event.time
            # Append delta-time as variable length quantity
            out += self._variable_length(int(delta * TICKS_PER_BEAT))
            out += event._compile()
        out += '\x00\xff\x2f\x00'
        return 'MTrk%s%s' % (struct.pack('>L', len(out)), out)


class MidiGenerator:
    def __init__(self, tempo=60):
        self.tempo = tempo
        self.tracks = []

    def new_track(self):
        track = MidiTrack(self.tempo)
        self.tracks.append(track)
        return track

    def write(self, filename):
        f = open(filename, 'wb')
        f.write(self._compile())
        f.close()

    def _compile(self):
        header = 'MThd\x00\x00\x00\x06\x00\x01%s%s' % (
                       struct.pack('>h', len(self.tracks)),
                       struct.pack('>h', TICKS_PER_BEAT))
        return '%s%s' % (header, ''.join(t._compile() for t in self.tracks))



if __name__ == "__main__":
    # All channels in one single track
    midi = MidiGenerator(tempo=60)
    track = midi.new_track()
    ch0 = track.get_channel(0)
    ch1 = track(1) # short version
    ch0.change_program(program=41) # Violin
    ch0.add_note(note=80, duration=1.5)
    ch0.add_pause(duration=1)
    ch0.add_note(note='C1', duration=0.5, velocity=120)
    ch1.change_program(program=76) # Pan Flute
    ch1.add_note(note='F#-2', duration=2, velocity=(10,120))
    midi.write('sound.mid')

    # Each channel on a separate track
    midi = MidiGenerator(tempo=60)
    track1 = midi.new_track()
    track2 = midi.new_track()
    ch0 = track1.get_channel(0)
    ch1 = track2.get_channel(1)

A list of supported instruments (program codes) can be found here.

Can I
  help you?


Just drop me a line at
giraffe@joonis.de