forked from vishnubob/python-midi
-
Notifications
You must be signed in to change notification settings - Fork 0
/
README
260 lines (203 loc) · 10 KB
/
README
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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
=Python MIDI=
Python, for all its amazing ability out of the box, does not provide you with
an easy means to manipulate MIDI data. There are probably about ten different
python packages out there that accomplish some part of this goal, but there is
nothing that is totally comprehensive.
This toolkit aims to furfill this goal. In particular, it strives to provide a
high level framework that is independant of hardware. It tries to offer a
reasonable object granularity to make MIDI streams a painless thing to
manipulate, sequence, record, and playback. It's important to have a good
concept of time, and the event framework provides automatic hooks so you don't
have to calculate ticks to wall clock, for example.
This MIDI Python toolkit represents about two years of scattered work. If you
are someone like me, who has spent a long time looking for a Python MIDI
framework, than this might be a good fit. It's not perfect, but it has a large
feature set to offer.
=Features=
* High level class types that represent individual MIDI events.
* A multitrack aware container, that allows you to manage your MIDI events.
* A tempo map that actively keeps track of tempo changes within a track.
* A reader and writer, so you can read and write your MIDI tracks to disk.
=Sequencer=
If you use this toolkit under Linux, you can take advantage of ALSA's
sequencer. There is a SWIG wrapper and a high level sequencer interface that
hides the ALSA details as best it can. This sequencer understands the higher
level Event framework, and will convert these Events to structures accessible
to ALSA. It tries to do as much as the hardwork for you as possible, including
adjusting the queue for tempo changes during playback. You can also record
MIDI events, and with the right set of calls, the ALSA sequencer will timestamp
your MIDI tracks at the moment the event triggers an OS hardware interrupt.
The timing is extremely accurate, even though you are using Python to manage
it.
I am extremely interested in supporting OS-X and Win32 sequencers as well, but
I need platform mavens who can help me. Are you that person? Please contact
me if you would like to help.
=Example Usage=
==Building a track from scratch==
Python 2.4.3 (#2, Apr 27 2006, 14:43:58)
[GCC 4.0.3 (Ubuntu 4.0.3-1ubuntu5)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import midi
>>> t = midi.new_stream(resolution=120, tempo=200)
>>> print t
<midi.midi.EventStream object at 0xb7d8bfcc>
>>> print x.resolution
120
>>> t.textdump()
End of Track @0 0ms C0 T0
Set Tempo @0 0ms C0 T0 [ mpqn: 300000 tempo: 200 ]
new_stream() builds a midi.StreamEvent object, and it creates a tempo event in
that stream at tick 0, hence the @0. C0 means channel 0, which can go as high
as 15. T0 means track 0, which can go as high as 256 (i think). 0ms and @0
means the same thing. @0 is the tick time, and 0ms means it occurs at
0milliseconds after playback starts, what I refer to as wall clock.
===Side Note: What is a MIDI Tick?===
The problem with ticks is that they don't give you any information about when
they occur without knowing two other pieces of information, the resolution, and
the tempo. The code handles these issues for you so all you have to do is
think about things in terms of ms, or ticks, if you care about the beat.
A tick represents the lowest level resolution of a MIDI track. Tempo is always
analogous with Beats per Minute (BPM) which is the same thing as Quarter notes
per Minute (QPM). The Resolution is also known as the Pulses per Quarter note
(PPQ). It analogous to Ticks per Beat (TPM).
Tempo is set by two things. First, a saved MIDI file encodes an initial
Resolution and Tempo. You use these values to initialize the sequencer timer.
The Resolution should be considered static to a track, as well as the
sequencer. During MIDI playback, the MIDI file may have encoded sequenced
(that is, timed) Tempo change events. These events will modulate the Tempo at
the time they specify. The Resolution, however, can not change from its
initial value during playback.
Under the hood, MIDI represents Tempo in microseconds. In other words, you
convert Tempo to Microseconds per Beat. If the Tempo was 120 BPM, the python
code to convert to microseconds looks like this:
>>> 60 * 1000000 / 120
500000
This says the Tempo is 500,000 microseconds per beat. This, in combination
with the Resolution, will allow you to convert ticks to time. If there are
500,000 microseconds per beat, and if the Resolution is 1,000 than one tick is
how much time?
>>> 500000 / 1000
500
>>> 500 / 1000000.0
0.00050000000000000001
In other words, one tick represents .0005 seconds of time or half a
millisecond. Increase the Resolution and this number gets smaller, the inverse
as the Resolution gets smaller. Same for Tempo.
Although MIDI encodes Time Signatures, it has no impact on the Tempo. However,
here is a quick refresher on Time Signatures:
http://en.wikipedia.org/wiki/Time_signature
==Creating Note On / Note Off Events==
A Note On Event specifies a few things:
- pitch: a value between 0 and 127
- velocity: a value representing the force you hit the key
(on a piano, for example), between 0 and 127
- tick: when the event occurred
- channel: MIDI supports up to 16 channels on one bus, value between 0 and 15
Here is how you would build this:
>>> on = midi.NoteOnEvent()
>>> on.channel = 2
>>> on.pitch = midi.G_3
>>> on.velocity = 100
>>> on.tick = 200
>>> print on
Note On @200 0ms C2 T0 [ G-3(43) 100 ]
The tick time is set, but the wall clock time in milliseconds is not. That's
because we need a tempo first, and for that, we need to add the event to a
track.
>>> t.add_event(on)
>>> t.textdump()
End of Track @201 502ms C0 T0
Set Tempo @0 0ms C0 T0 [ mpqn: 300000 tempo: 200 ]
Note On @200 500ms C2 T0 [ G-3(43) 100 ]
With the context of a tempo, the wall clock time can be calculated. The
tracking code also maintains a singleton EndOfTrackEvent. You can always
get at this event using:
>>> t.endoftrack
<midi.midi.EndOfTrackEvent object at 0xb7d9220c>
>>> print t.endoftrack
End of Track @201 502ms C0 T0
This allows you to easily predict how long a song will take to completely
playback, event wise (ignoring issues of sustain, for example).
If you look at the source to midi.py, you will notice it builds a bunch of
constants in the beginning. These allow you to specify notes using a more
familiar notation, like G_3, or As_7 or Db_2. A warning, however. Even
though the pitch will always be the correct key, it might not be in the
correct octave. In other words, I enumerate the octave numbers from 0 up.
some software starts at -2 or -3, and only go as high as 8 or 7. There is no
agreement on how to enumerate the octaves because the MIDI specification
doesn't say anything about octave number. It only specifies the value for
middle C.
==Writing our Track to Disk==
It's easy to save your work, using the helper function provided by the midi
module.
>>> midi.write_midifile(t, "first.mid")
==Reading our Track back from Disk==
It's just as easy to load your MIDI file from disk.
>>> z = midi.read_midifile("first.mid")
>>> z.textdump()
End of Track @202 505ms C0 T0
Set Tempo @0 0ms C0 T0 [ mpqn: 300000 tempo: 200 ]
Note On @200 500ms C2 T0 [ G-3(43) 100 ]
==Using the ALSA Sequencer==
The ALSA sequencer module is a relatively new addition, and works well, but may
change over time to accommodate future sequencer platforms. I tried to make it
as feature complete as possible, and it does some power stuff. For example,
you can enumerate your MIDI hardware using the "HardwareSequencer"
>>> import midi.sequencer as sequencer
>>> s = sequencer.SequencerHardware()
>>> print s
] client(129) "__sequencer__"
] client(64) "EMU10K1 MPU-401 (UART)"
] port(0) [r, w, sender, receiver] "EMU10K1 MPU-401 (UART)"
] client(0) "System"
] port(1) [r, sender] "Announce"
] port(0) [r, w, sender] "Timer"
] client(65) "Emu10k1 WaveTable"
] port(3) [w, receiver] "Emu10k1 Port 3"
] port(2) [w, receiver] "Emu10k1 Port 2"
] port(1) [w, receiver] "Emu10k1 Port 1"
] port(0) [w, receiver] "Emu10k1 Port 0"
] client(63) "OSS sequencer"
] port(0) [w] "Receiver"
] client(62) "Midi Through"
] port(0) [r, w, sender, receiver] "Midi Through Port-0"
This allows you to look up client numbers and ports using it's lookup methods:
>>> s["EMU10K1 MPU-401 (UART)"].client
64
>>> s["EMU10K1 MPU-401 (UART)"]["EMU10K1 MPU-401 (UART)"].port
0
Using these numbers, you can open a SequencerWriter()
>>> client_name = 'EMU10K1 MPU-401 (UART)'
>>> port_name = 'EMU10K1 MPU-401 (UART)'
>>> hardware = sequencer.SequencerHardware()
>>> client, port = hardware.get_client_and_port(client_name, port_name)
>>> s = sequencer.SequencerWrite(sequencer_resolution=120)
>>> s.subscribe_port(client, port)
And now you can start writing events, using iterevents() from the track and
event_write() from the sequencer:
def play(stream, client, port):
ppq = self.stream.resolution
seq = sequencer.SequencerWrite(sequencer_resolution=ppq)
seq.subscribe_port(client, port)
start = time.time()
seq.start_sequencer()
for event in stream.iterevents():
ret = seq.event_write(event, tick=True)
now = time.time()
eot = stream.endoftrack
songlen = eot.msdelay / 1000.0
remainder = (start + songlen) - now
time.sleep(remainder + .5)
Reading events is not much different, except you would use seq.event_read().
There is also a polling interface, and the option to set all of this to non
blocking. Finally, there is a Duplex sequencer, that allows you to read and
write your queue at the same time. This is good for maintaining a metronome,
for example, that is tick for tick the same as the input you would be
receiving.
=Thanks=
I originally wrote this to drive the electro-mechanical instruments of Ensemble
Robot, which is a boston based group of artists, programmers, and engineers.
This API, however, has applications beyond controlling this equipment. For
more information about Ensemble Robot, please visit:
http://www.ensemblerobot.org/
-giles hall <ghall [at] csh [dot] rit [dot] edu> 10/07/2006