MarkCWirt / MIDIUtil

A pure Python library for creating multi-track MIDI files
Other
247 stars 49 forks source link

Micro-tones - more than twelve notes per scale? #12

Closed malcolmsailor closed 7 years ago

malcolmsailor commented 7 years ago

Hello!

Thanks for writing MIDIUtil, it's been tremendously useful to me in a number of projects.

Unfortunately, I'm currently having issues trying to use it to play back some microtonal music in 31-tone equal temperament. (I'm using Fluidsynth to playback, although I've tried a few other options as well.) Since (like most "microtonal" music) this involves more divisions of the octave than the usual 12-tone equal temperament, I have to somehow assign single midi numbers to multiple frequencies. It's not clear to me how to do this, since the MIDIFile.changeNoteTuning function seems to apply globally.

Nevertheless, I have tried both of the following approaches:

Since MIDIFile.changeNoteTuning takes a track number as a parameter, I thought this latter approach would work, but with both cases the tuning changes seem to apply to all tracks.

Any ideas what I'm doing wrong? I would be happy to share the files I've been working with if that would be of use.

Help would be greatly appreciated!

Best,

Malcolm

MarkCWirt commented 7 years ago

Malcolm,

The changeNoteTuning() function can be used to assign note numbers to frequencies.

I just tried the following program to play an EDO-31 scale in fluidsynth, and it seems to work OK. Note, though, with fluidsynth you need to explicitly set the tuning bank and program

#!/usr/bin/env python

from midiutil import MIDIFile

output_file = "/tmp/edo31.mid"

frequency_mapping = [
    (69, 220.000000000000), (70, 224.974515832279), (71, 230.061512608947), (72, 235.263533685635), (73, 240.583179926894),
    (74, 246.023111006560), (75, 251.586046737508), (76, 257.274768431491), (77, 263.092120289711), (78, 269.041010824843),
    (79, 275.124414315209), (80, 281.345372291834), (81, 287.706995059126), (82, 294.212463249940), (83, 300.865029415806),
    (84, 307.668019653116), (85, 314.624835266072), (86, 321.738954467250), (87, 329.013934116606), (88, 336.453411499804),
    (89, 344.061106146758), (90, 351.840821691297), (91, 359.796447772867), (92, 367.931961981248), (93, 376.251431845236),
    (94, 384.759016866289), (95, 393.458970598169), (96, 402.355642773591), (97, 411.453481478973), (98, 420.757035378352),
    (99, 430.270955987590), (100, 440.000000000000)]

midi_file = MIDIFile(1, adjust_origin=False)

# Change the tuning
midi_file.changeNoteTuning(0, frequency_mapping, tuningProgam=0)

# Tell fluidsynth what bank and program to use (0 and 0, respectively)
midi_file.changeTuningBank(0, 0, 0, 0)
midi_file.changeTuningProgram(0, 0, 0, 0)

# Add some ones

for time in range(31):
    midi_file.addNote(0,0,69+time, time, 1, 100)

# Write to disk
with open(output_file, "wb") as out_file:
    midi_file.writeFile(out_file)

Note that frequency_mapping contains tuples that assign frequencies on a per-MIDI-note-number basis. The changeNoteTuning() call puts these note-number to frequency mapping into tuning program 0, and the changeTuningProgram() call tells fluidsynth to use that program.

In particular this mapping maps the 31 notes between MIDI 69 and MIDI 100 to a 31 EDO scale. (In practice, though, one would probably want to map all the notes between 0 and 127).

Note also that order can be kind of important. Above I set the frequencies and tuning program before I add notes. There are a whole bunch of MIDI events at time 0, and it's best to hand those events over to the synthesizer in a reasonable order. Otherwise, for example, your first note (which is also at time 0) may play before the tuning is changed.

When I render this and play it, with, for example:

fluidsynth -F out.wav /usr/share/sounds/sf2/FluidR3_GM.sf2 edo31.mid 

it seems to work. Give the above a try.

Now, having said this, if you're doing a lot with microtonal music, you may want to check out one of my other projects, Pytuning. With that package you can create tuning tables for a variety of soft synthesizers (including fluidsynth). You may find that a little more convenient than tweaking the frequencies by hand.

MarkCWirt commented 7 years ago

Note also that the latest code in the repository also supports MIDI pitch bend for microtonalities:

http://midiutil.readthedocs.io/en/latest/tuning.html#using-pitch-bend

I personally don't like pitch bend too much, because it's a channel-level event that affects all notes on that channel, but it has its uses (it came from a pull request from a user who needed it in his work).

I haven't done a release in a bit, so I'll probably do one soon, which would move this to the PyPi version.

malcolmsailor commented 7 years ago

Hi Mark,

Thanks for your rapid responses!

So it seems the reason your code works and mine did not is that you map each note of the new temperament to a unique midi number. This means that the greater the cardinality of the temperament, the fewer octaves available (e.g. in a 12-EDO scale, you have 128/12 = over ten octaves, but in 64-EDO, you would just have 2 octaves). For 31-EDO temperament this would mean a range of a little over 4-octaves, which I think is workable for what I hope to do. But I will also check out your pytuning package to see if that can make my life easier!

Using pitchbend would be easy if you had a temperament in a simple ratio to 12. E.g., in 24-EDO, you could have two channels, one for the even 12 pitch-classes, which would map to the usual notes of the 12-EDO scale, and another for the 12 odd pitch-classes, pitch-bended up 50 cents. If the temperament is in a more complex ratio to 12 (e.g., 31), however this strategy wouldn't work as well. But I could just send a pitch-bend event for every note.

To explain my original strategy a little more, I mapped the 31 pitches in every octave to 3 different tracks. These are the first 6 mappings, starting at C4:

(0, 60, 261.63), (1, 60, 267.54), (2, 60, 273.59), (0, 61, 279.78), (1, 61, 286.1), (2, 61, 292.57)

The first number is the channel number, and the following two form a tuning tuple. The virtue of this strategy would be that it would preserve the full 10+ octave range of canonical midi numbers. But it seems that this strategy doesn't work because, although the changeNoteTuning function takes a track number, the changed tuning applies globally and not only to the given track.

By the way, your websites look interesting, I look forward to perusing them a little!

Malcolm

MarkCWirt commented 7 years ago

OK. I see what you're trying to do now.

Based upon the midi tuning standard (for example, as shown here) implies that this should work. It's possible that you've run up against a limitation of fluidsynth (or whichever synth you're attempting to use).

I just tried the folloing, which defined two different tuning programs, assigned them to different channels, and the places notes on those channels:

from midiutil import MIDIFile

output_file = "/tmp/edo31.mid"

frequency_mapping  = [
 (69, 220.000000000000),(70, 224.974515832279),(71, 230.061512608947),(72, 235.263533685635),(73, 240.583179926894),
 (74, 246.023111006560),(75, 251.586046737508),(76, 257.274768431491),(77, 263.092120289711),(78, 269.041010824843),
 (79, 275.124414315209),(80, 281.345372291834),(81, 287.706995059126),(82, 294.212463249940),(83, 300.865029415806),
 (84, 307.668019653116),(85, 314.624835266072),(86, 321.738954467250),(87, 329.013934116606),(88, 336.453411499804),
 (89, 344.061106146758),(90, 351.840821691297),(91, 359.796447772867),(92, 367.931961981248),(93, 376.251431845236),
 (94, 384.759016866289),(95, 393.458970598169),(96, 402.355642773591),(97, 411.453481478973),(98, 420.757035378352),
 (99, 430.270955987590),(100, 440.000000000000)]

frequency_mapping2 = [
 (69, 440.000000000000),(70, 449.949031664558),(71, 460.123025217894),(72, 470.527067371270),(73, 481.166359853789),
 (74, 492.046222013119),(75, 503.172093475016),(76, 514.549536862982),(77, 526.184240579422),(78, 538.082021649686),
 (79, 550.248828630418),(80, 562.690744583668),(81, 575.413990118252),(82, 588.424926499879),(83, 601.730058831613),
 (84, 615.336039306231),(85, 629.249670532143),(86, 643.477908934500),(87, 658.027868233212),(88, 672.906822999607),
 (89, 688.122212293517),(90, 703.681643382594),(91, 719.592895545734),(92, 735.863923962497),(93, 752.502863690471),
 (94, 769.518033732579),(95, 786.917941196338),(96, 804.711285547181),(97, 822.906962957945),(98, 841.514070756704),
 (99, 860.541911975181),(100, 880.000000000000)]

# Create the file
midi_file = MIDIFile(1, adjust_origin=False)

# Create two tuning programs, 0 and 1
midi_file.changeNoteTuning(0, frequency_mapping, tuningProgam=0)
midi_file.changeNoteTuning(0, frequency_mapping2, tuningProgam=1)

# Assign program 0 to channel 0, program 1 to channel 1
midi_file.changeTuningBank(0, 0, 0, 0)
midi_file.changeTuningProgram(0, 0, 0, 0)
midi_file.changeTuningProgram(0, 1, 0, 1)

# Add notes to channel 0

for time in range(31):
    midi_file.addNote(0,0,69+time, time, 1, 100)

# Add notes to channel 1
for time in range(31):
    midi_file.addNote(0,1,69+time, time+34, 1, 100)

# Save fie
with open(output_file, "wb") as out_file:
    midi_file.writeFile(out_file)

And it didn't work, neither in timidity nor in fluidsynth.

I also tried a slightly different way, by writing some notes, changing the tuning program, and then writing some more notes. This didn't work either:

from midiutil import MIDIFile

output_file = "/tmp/edo31.mid"

frequency_mapping  = [
 (69, 220.000000000000),(70, 224.974515832279),(71, 230.061512608947),(72, 235.263533685635),(73, 240.583179926894),
 (74, 246.023111006560),(75, 251.586046737508),(76, 257.274768431491),(77, 263.092120289711),(78, 269.041010824843),
 (79, 275.124414315209),(80, 281.345372291834),(81, 287.706995059126),(82, 294.212463249940),(83, 300.865029415806),
 (84, 307.668019653116),(85, 314.624835266072),(86, 321.738954467250),(87, 329.013934116606),(88, 336.453411499804),
 (89, 344.061106146758),(90, 351.840821691297),(91, 359.796447772867),(92, 367.931961981248),(93, 376.251431845236),
 (94, 384.759016866289),(95, 393.458970598169),(96, 402.355642773591),(97, 411.453481478973),(98, 420.757035378352),
 (99, 430.270955987590),(100, 440.000000000000)]

frequency_mapping2 = [
 (69, 440.000000000000),(70, 449.949031664558),(71, 460.123025217894),(72, 470.527067371270),(73, 481.166359853789),
 (74, 492.046222013119),(75, 503.172093475016),(76, 514.549536862982),(77, 526.184240579422),(78, 538.082021649686),
 (79, 550.248828630418),(80, 562.690744583668),(81, 575.413990118252),(82, 588.424926499879),(83, 601.730058831613),
 (84, 615.336039306231),(85, 629.249670532143),(86, 643.477908934500),(87, 658.027868233212),(88, 672.906822999607),
 (89, 688.122212293517),(90, 703.681643382594),(91, 719.592895545734),(92, 735.863923962497),(93, 752.502863690471),
 (94, 769.518033732579),(95, 786.917941196338),(96, 804.711285547181),(97, 822.906962957945),(98, 841.514070756704),
 (99, 860.541911975181),(100, 880.000000000000)]

# Create the file
midi_file = MIDIFile(1, adjust_origin=False)

# Create two tuning programs, 0 and 1
midi_file.changeNoteTuning(0, frequency_mapping, tuningProgam=0)
midi_file.changeNoteTuning(0, frequency_mapping2, tuningProgam=1)

midi_file.changeTuningBank(0, 0, 0, 0)
midi_file.changeTuningProgram(0, 0, 0, 0)

# Add notes to channel 0

for time in range(31):
    midi_file.addNote(0,0,69+time, time, 1, 100)

midi_file.changeTuningProgram(0, 0, 33, 1)

# Add notes to channel 0, but after the upload of the program.
for time in range(31):
    midi_file.addNote(0,0,69+time, time+34, 1, 100)

# Save file
with open(output_file, "wb") as out_file:
    midi_file.writeFile(out_file)

I'll look though the code to made sure that I didn't make some silly mistake, but it's possible this is just the famous lack of support for this particular standard. It's also possible that I've misinterpreted the standard. MIDI is hard to work with, because you can't get access to the real standard without paying a lot of money to join the group. You're forced to reverse engineer the standard from data that is available, if your pockets aren't particularly deep.!

malcolmsailor commented 7 years ago

Hi Mark,

Thanks for investigating. It looks like you get the same results as me.

I ended up deciding the pitch-bend function was the simplest solution, given what I already had written. I use "31 edo midi numbers" (e.g., C4, which is usually 60 (i.e., 5 12), becomes 155 (i.e., 5 31)) and then send them to a function which calls the pitch bend and add note functions as necessary. It works well!

I will definitely check out your tuning package at some point.

Thanks for your help!

Malcolm

MarkCWirt commented 7 years ago

Glad you could get it working! I'll close the ticket.

As I said, I'll to a release soon so that the pitch bend stuff makes it to a release version.