vishnubob / python-midi

Python MIDI library
MIT License
1.51k stars 370 forks source link

Question on how to change the pitch of certain notes (per-track) #56

Closed tleyden closed 9 years ago

tleyden commented 9 years ago

I'm new to midi, so excuse the n00b question.

Here's a snippet of the actual algorithm I'm trying to implement:

Each measure of four beats represents a decade in time, starting from the year 1300A.D. and ending at 2015.

We used a simple algorithm in which a rise or fall in the values of each climate variables is matched by a rise or fall of an equal magnitude (measured as a %) of the pitch of the corresponding musical voices. The reference value used in this model is the pre-industrial (1300-1760) averages for temperature and CO2 concentrations. So, for example, if the temperature value rises by 10% as measured against the reference value over a decade, then the pitch of the trumpet will increase by 10% for the four beats of the corresponding measure in the score.

My current plan is to:

Do you have any examples of iterating through all notes in a track? If not, can you at least point me to the function/method to call? What I had in mind was basically something similar to the mxm python-midi example

As far as grouping the notes into groups of 4 beats, is there anything provided by the library to make this easier? I guess what I mean is there a "wrapper event" to invoke a callback after X number of beats and pass all the notes in those beats? Or would I have to handle that on my own?

By the way, this is for an an art project to bring awareness about Climate Change.

tdhsmith commented 9 years ago

Well the key to iteration is this bit from the docs:

A MIDI file is represented as a hierarchical set of objects. At the top is a Pattern, which contains a list of Tracks, and a Track is is a list of MIDI Events.

So after importing you're going to have a loop something like this:

pattern = midi.read_midifile('...')
for track in pattern:
    for event in track:
        if isinstance(event, midi.NoteEvent):
            # some action for note events...

Changing the pitch up or down by a half step is as simple as event.pitch += 1 or event.pitch -= 1 (assuming you have checked it is a NoteEvent as above; other event types won't have a pitch property). However you're going to need some logic to find the NoteOffEvent associated with each NoteOnEvent you modify in order to avoid issues where you're turning off a note of a different pitch and leaving your new pitch playing.

If you don't need both the modified and original tracks in one file, it will be easier to just modify the events in place and then write the original pattern object to a new file, rather than bothering to make entirely new pattern and track objects.

I don't think there are any sort of wrapper/callback options here (unless they've been added recently). But it wouldn't be terribly hard to write your own helper function. The events are sequential, and you can convert directly from ticks to beats, so figuring out what beat you're at is pretty easy if you're in absolute tick mode. (I may be wrong here, but I think the MIDI reader always makes Patterns in relative tick mode. There is a helper function Pattern.make_ticks_abs())

tleyden commented 9 years ago

Thanks @tdhsmith for the help! This is really useful. I have a few follow up questions below.

However you're going to need some logic to find the NoteOffEvent associated with each NoteOnEvent you modify in order to avoid issues where you're turning off a note of a different pitch and leaving your new pitch playing.

Hmmm.. not quite sure I follow this. Can you give a concrete example? By the way I'm not planning to "turn off" any notes, just change their pitch up or down a half-step.

it will be easier to just modify the events in place and then write the original pattern object to a new file

Sounds like good advice, I'll take that approach!

so figuring out what beat you're at is pretty easy if you're in absolute tick mode.

I don't understand the relationship between ticks and beats. Can you explain it or point me to some docs?

tdhsmith commented 9 years ago

Hmmm.. not quite sure I follow this. Can you give a concrete example? By the way I'm not planning to "turn off" any notes, just change their pitch up or down a half-step.

Well MIDI is designed primarily as a real-time communication protocol, so it doesn't transmit the "length" or "duration" of a note (because in real-time it doesn't know how long you're going to hold it for). Instead it breaks note signals into note-on and note-off events. Most instruments will keep playing every note-on they receive until they get a corresponding note-off with the same track, channel, & pitch values.

So if you are moving along the midi file, and you change the pitch of a NoteOnEvent, it will no longer be on the same pitch as the signal that is supposed to turn the sound off, and all of your note durations will go haywire.

I'm not quite sure how I would handle this situation myself. The easiest (but not efficient) method that comes to mind right now would be to make a helper function findMatchingNoteOff(track, noteOn) that scanned for the first instance in track of a NoteOffEvent on with the same channel and pitch as noteOn, and occurring after its absolute tick value. Then you can use that function to modify the noteOff in the same manner that you modified the noteOn.

If you need efficiency too (since this is going to scan the whole track again for every note), you may have to rethink your iterator structure and maybe use enumerate and indices so you can start your search at the right location.

Of course the whole thing would probably be better handled by a library that handles notes from a musical standpoint, but that introduces a lot of complexity and this project seems to aim for reproducing MIDI and not a whole musical notation system.

tdhsmith commented 9 years ago

I should note some special cases:

before:
C>  *  *  ON -  OFF *  *
B>  *  ON -  OFF *  *  *

after moving B up half step:
C> *  ON ON OFF OFF *  *

It's hard to say what this will sound like on a particular instrument, or what you'd even want to happen in this situation. And it would certainly take a rather complex solution to address it cleanly. The best solution in my mind is to try to adjust your system to avoid the situation entirely, and if it's necessary in a few spots, just fix them by hand later. :wink:

tdhsmith commented 9 years ago

Oh and there's a bit about ticks in the main readme. I think just dividing ticks by resolution will get you the current beat number, but I always have to test it a few ways before I remember and feel confident haha.

tleyden commented 9 years ago

The easiest (but not efficient) method that comes to mind right now would be to make a helper function findMatchingNoteOff(track, noteOn) that scanned for the first instance in track of a NoteOffEvent on with the same channel and pitch as noteOn, and occurring after its absolute tick value

Ok, that makes sense. Also, the caveats you mentioned about:

also make sense, and seem like they won't be too much of an issue.

But I got lost here:

Moving notes around like this is fine and good for simple files where each voice takes its own track or channel, but be aware of how you can make really weird situations by modifying notes non-uniformly.

When you say "non-uniformly", do you mean in the context of a single track, making a mistake where the wrong NoteOff (or NoteOn with velocity 0) had it's pitch adjusted?

I read this description of midi tracks vs channels and think I have a grasp on it, ut I'm confused about what you mean by "voice" here. Maybe an example would be useful. Is this midi file a "simple file" that you mentioned? What about this one, which seems to have a left and a right track for each channel? Since I can hand pick the input files for this application, as long as I know how to only pick "simple" files, I think I can avoid the issues you mentioned.

before:
C>  *  *  ON -  OFF *  *
B>  *  ON -  OFF *  *  *

after moving B up half step:
C> *  ON ON OFF OFF *  *

^^ Scratching my head on this one .. not really sure what this is depicting exactly.

The best solution in my mind is to try to adjust your system to avoid the situation entirely

I definitely don't want to be doing any hand-fixing, so if you can provide more details on how to avoid the situation, that would be helpful.

tdhsmith commented 9 years ago

Sorry I was intentionally using the terms vaguely. I didn't mean to draw you into the precise definitions of channel/track/voice. I still confuse myself there. :sweat:

When you say "non-uniformly", do you mean in the context of a single track, making a mistake where the wrong NoteOff (or NoteOn with velocity 0) had it's pitch adjusted?

A little broader than that. I'm using uniform editing to mean a change applied to an entire MIDI unit at once (like a track). So non-uniform is any edit where you define your own 'chunks' (like measures, or time durations, or certain pitches or event types).

I bring it up because you could be matching the on-off pairs correctly and still run into "collisions" where you now have pitches overlapping, or other kinds of event cues in places they don't belong.

^^ Scratching my head on this one .. not really sure what this is depicting exactly.

Hypothetically, say you're changing all the A's in a track to C's. Then this situation is hard to resolve:

Music notation for staggered chord of an A&C where both notes are held for two beats, but the C starts a beat after the A does

Afterward you should only have C's in this measure, right? But the ordering of MIDI events will be bad, because first the 'old A' turns on, then the 'old C' does, then the 'old A' turns off, then the 'old C' turns off. So in that one pitch line, your instrument will see the sequence On On Off Off. This doesn't really make sense to it because it doesn't have a concept of multiple voices within a single pitch in a single track. The first Off will turn "both" off, and you'll probably end up with only 2 beats of sound instead of 3.

So the results of collisions can be hard to predict (and software might interpret the file different than you expect).

Ultimately all I was trying to get at is to be aware that the "boundaries" between what you edit and what you don't can cause unexpected behavior. You're working with pure MIDI data, so it's up to you to handle any higher-level "musical logic" about how notes and their properties might interact.

tdhsmith commented 9 years ago

Really though, even if you end up having these collisions, I'd guess that most instruments & software would handle it fine and you wouldn't even notice the difference. And since your project involves moving measure blocks by pitch, I'd really only expect to encounter pitch overlaps like in the example I gave, which have pretty innocuous side effects.

tleyden commented 9 years ago

But the ordering of MIDI events will be bad, because first the 'old A' turns on, then the 'old C' does, then the 'old A' turns off, then the 'old C' turns off.

Aha, this makes sense now! I'm not going to worry about that "edge case" in the first pass, but it's good to know about in case I get unexpected results. Thanks again for taking the time! I'm planning to send some Pull Requests for any helper code I write that feels like it might be reusable.

tleyden commented 9 years ago

Changing the pitch up or down by a half step is as simple as event.pitch += 1 or event.pitch -= 1

What are the boundary conditions here? Eg, what is the minimum pitch value and the maximum pitch value? (0-128?)

vishnubob commented 9 years ago

Correct, most data fields within MIDI are 7-bit. The high-order bit is reserved as a flag to indicate variable length data (such as SysEx messages, delta time values, etc). The MIDI standard was drafted in 1983 and many of its short-comings are an artifact of its age. When writing code to manipulate MIDI, it's helpful to read up on the MIDI file format and specification.

Wikipedia MIDI file format MIDI Specification