forked from ideoforms/isobar
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmidifile.py
More file actions
199 lines (168 loc) · 7.84 KB
/
midifile.py
File metadata and controls
199 lines (168 loc) · 7.84 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
195
196
197
198
from isobar.note import *
from isobar.pattern.core import *
import mido
import logging
log = logging.getLogger(__name__)
class MidiNote:
def __init__(self, pitch, velocity, location, duration = None):
# pitch = MIDI 0..127
self.pitch = pitch
# velocity = MIDI 0..127
self.velocity = velocity
# location in time, beats
self.location = location
# duration in time, beats
self.duration = duration
class MidiFileIn:
""" Read events from a MIDI file.
Requires mido. """
def __init__(self, filename):
self.filename = filename
def read(self, quantize = 0.25):
midi_reader = mido.MidiFile(self.filename)
note_tracks = list(filter(lambda track: any(message.type == 'note_on' for message in track), midi_reader.tracks))
if not note_tracks:
raise ValueError("Could not find any tracks with note data")
#------------------------------------------------------------------------
# TODO: Support for multiple tracks
#------------------------------------------------------------------------
track = note_tracks[0]
notes = []
offset = 0
for event in track:
if event.type == 'note_on' and event.velocity > 0:
#------------------------------------------------------------------------
# Found a note_on event.
#------------------------------------------------------------------------
note = MidiNote(event.note, event.velocity, offset)
notes.append(note)
offset += event.time / 480.0
elif event.type == 'note_off' or (event.type == 'note_on' and event.velocity == 0):
#------------------------------------------------------------------------
# Found a note_off event.
#------------------------------------------------------------------------
for note in reversed(notes):
if note.pitch == event.note:
note.duration = offset - note.location
break
offset += event.time / 480.0
for note in notes:
if quantize:
# note.location = round(note.location / quantize) * quantize
# note.duration = round(note.duration / quantize) * quantize
pass
print("%d (%d, %f)" % (note.pitch, note.velocity, note.duration))
#------------------------------------------------------------------------
# Construct a sequence which honours chords and relative lengths.
# First, group all notes by their starting time.
#------------------------------------------------------------------------
notes_by_time = {}
for note in notes:
log.debug("(%.2f) %d/%d, %s" % (note.location, note.pitch, note.velocity, note.duration))
location = note.location
if location in notes_by_time:
notes_by_time[location].append(note)
else:
notes_by_time[location] = [ note ]
note_dict = {
"note" : [],
"amp" : [],
"gate" : [],
"dur" : []
}
for n in notes_by_time:
print("%s - %s" % (n, notes_by_time[n]))
#------------------------------------------------------------------------
# Next, iterate through groups of notes chronologically, figuring out
# appropriate parameters for duration (eg, inter-note distance) and
# gate (eg, proportion of distance note extends across).
#------------------------------------------------------------------------
times = sorted(notes_by_time.keys())
for i in range(len(times)):
time = times[i]
notes = notes_by_time[time]
#------------------------------------------------------------------------
# Our duration is always determined by the time of the next note event.
# If a next note does not exist, this is the last note of the sequence;
# use the maximal length of note currently playing (assuming a chord)
#------------------------------------------------------------------------
if i < len(times) - 1:
next_time = times[i + 1]
else:
next_time = time + max([ note.duration for note in notes ])
dur = next_time - time
note_dict["dur"].append(dur)
if len(notes) > 1:
note_dict["note"].append(tuple(note.pitch for note in notes))
note_dict["amp"].append(tuple(note.velocity for note in notes))
note_dict["gate"].append(tuple(note.duration / dur for note in notes))
else:
note = notes[0]
note_dict["note"].append(note.pitch)
note_dict["amp"].append(note.velocity)
note_dict["gate"].append(note.duration / dur)
return note_dict
class MidiFileOut:
""" Write events to a MIDI file.
Requires the MIDIUtil package:
https://code.google.com/p/midiutil/ """
def __init__(self, filename = "score.mid", num_tracks = 16):
# requires midiutil
from midiutil.MidiFile import MIDIFile
self.filename = filename
self.score = MIDIFile(num_tracks)
self.time = 0
def tick(self, tick_length):
self.time += tick_length
def note_on(self, note = 60, velocity = 64, channel = 0, duration = 1):
#------------------------------------------------------------------------
# avoid rounding errors
#------------------------------------------------------------------------
time = round(self.time, 5)
self.score.addNote(channel, channel, note, time, duration, velocity)
def note_off(self, note = 60, channel = 0):
time = round(self.time, 5)
self.score.addNote(channel, channel, note, time, 0, 0)
def write(self):
fd = open(self.filename, 'wb')
self.score.writeFile(fd)
fd.close()
class PatternWriterMIDI:
""" Writes a pattern to a MIDI file.
Requires the MIDIUtil package:
https://code.google.com/p/midiutil/ """
def __init__(self, filename = "score.mid", numtracks = 1):
from midiutil.MidiFile import MIDIFile
self.score = MIDIFile(numtracks)
self.track = 0
self.channel = 0
self.volume = 64
def add_track(self, pattern, track_number = 0, track_name = "track", dur = 1.0):
time = 0
# naive approach: assume every duration is 1
# TODO: accept dicts or PDicts
try:
for note in pattern:
vdur = Pattern.value(dur)
if note is not None and vdur is not None:
self.score.addNote(track_number, self.channel, note, time, vdur, self.volume)
time += vdur
else:
time += vdur
except StopIteration:
#------------------------------------------------------------------------
# a StopIteration exception means that an input pattern has been
# exhausted. catch it and treat the track as completed.
#------------------------------------------------------------------------
pass
def add_timeline(self, timeline):
#------------------------------------------------------------------------
# TODO: translate entire timeline into MIDI
# difficulties: need to handle degree/transpose params
# need to handle channels properly, and reset numtracks
#------------------------------------------------------------------------
pass
def write(self, filename = "score.mid"):
fd = open(filename, 'wb')
self.score.writeFile(fd)
fd.close()