We'd like to synthesize simple audio samples containing a single note or a chord. The samples, however, should be parameterized by several attributes. We'd like to modify:
The main output should be MIDI which can be synthesized to audio. Besides that we'd like to store the parameters and metadata like the the pitch class, frequency, etc.
In this notebook we'll explore how to acomplish this with the music21
library.
In [27]:
import music21
from music21.chord import Chord
from music21.duration import Duration
from music21.instrument import Instrument
from music21.note import Note, Rest
from music21.stream import Stream
from music21.tempo import MetronomeMark
from music21.volume import Volume
In [2]:
import os
data_dir = 'data/working/example-parametric-note'
os.makedirs(data_dir, exist_ok=True)
We're about to create a Note object which represents a single note and both its pitch and duration.
In [3]:
Note('C')
Out[3]:
In [4]:
s = Stream([Note('C')])
If we have MuseScore installed, we can we the music sheet representation.
In [5]:
s.show()
Out[5]:
In [6]:
s.show('midi')
Note that there's some rest at the beginning and end of the MIDI file. It looks like a quarter-note rest. The reason is that "MIDI controllers may not be able to play notes at deltaTime=0" See: https://groups.google.com/d/msg/music21list/ne_P_ZUvRNk/XynaiODzAgAJ
Anyway this might be good in the audio processing, since there will be a space for the windowing function.
In [7]:
s.write('midi', data_dir + '/c.midi')
Out[7]:
In [8]:
n = Note('C')
n
Out[8]:
In [9]:
def describe_note(note):
p = note.pitch
print(note)
print('pitch:', note.pitch)
print('duration:', note.duration)
print('name:', p.name)
print('full name:', p.fullName)
print('pitch class:', p.pitchClass)
print('octave:', p.octave)
print('frequency', p.frequency, 'Hz')
print('midi:', p.midi)
print('pitch space:', p.ps) # like MIDI, but floating point
In [10]:
describe_note(n)
In [11]:
# different note in the default octave
describe_note(Note('E'))
In [12]:
# a note in the specific octave
describe_note(Note('G#3'))
In [13]:
# note specified by its octave and pitch class within an octave
describe_note(Note(octave=2, pitchClass=3))
In [14]:
# note specified by its integer MIDI number
describe_note(Note(midi=21))
In [15]:
# microtonal pitch using the pitch space attribute (like MIDI but floating point)
describe_note(Note(ps=21.25))
In [16]:
# note with duration of half of a quarter note
note = Note(midi=21, duration=Duration(0.5))
describe_note(note)
In [17]:
# note with duration of half of a quarter note
note = Note(midi=21, duration=Duration(2.5))
describe_note(note)
In [18]:
for v in [0, 32, 64, 127]:
print(Volume(velocity=v))
In [19]:
for v in [0, 0.25, 0.5, 1.0]:
print(Volume(velocityScalar=v))
In [20]:
Chord(['C']).volume
Out[20]:
In [21]:
c = Chord([Note('C')])
c.volume = Volume(velocityScalar=0.25)
c.volume
Out[21]:
In [22]:
metronome = MetronomeMark(number=60)
metronome.durationToSeconds(Duration(1.0))
Out[22]:
Just add a metronome mark at the beginning of the stream.
In [23]:
Stream([MetronomeMark(number=60), Note('C')]).show()
Out[23]:
In [24]:
def make_instrument(id):
i = Instrument()
i.midiProgram = id
return i
def chord_with_volume(chord, volume):
chord.volume = Volume(velocityScalar=volume)
return chord
def generate_single_note(midi_number, midi_instrument=0, volume=1.0, duration=1.0, tempo=120):
"""
Generates a stream containing a single note with given parameters.
midi_number - MIDI note number, 0 to 127
midi_instrument - MIDI intrument number, 0 to 127
duration - floating point number (in quarter note lengths)
volume - 0.0 to 1.0
tempo - number of quarter notes per minute (eg. 120)
Note that there's a quarter note rest at the beginning and at the end.
"""
return Stream([
MetronomeMark(number=tempo),
make_instrument(midi_instrument),
chord_with_volume(Chord([
Note(midi=midi_number, duration=Duration(duration))
]), volume)
])
In [25]:
generate_single_note(60).show('midi')
Let's make a sequence. Note that by just passing a list of notes to the Stream we get a chord, not a sequence, so we must append each note separately.
In [ ]:
s = Stream()
s.append(make_instrument(50))
s.append(Note(midi=60))
s.append(Note(midi=64))
s.append(Note(midi=67))
s.write('midi', data_dir + '/sequence_separated.midi')
s.show('midi')
In the previous example we see, that notes may overlap. So let's add some rests to make better separation.
In [35]:
s = Stream()
s.append(make_instrument(50))
s.append(Note(midi=60))
s.append(Rest(duration=Duration(2.0)))
s.append(Note(midi=64))
s.append(Rest(duration=Duration(2.0)))
s.append(Note(midi=67))
s.write('midi', data_dir + '/sequence_separated.midi')
s.show('midi')
It might be useful to generate a big sequence into a single MIDI file with notes being separated and then cutting the audio to pieces, rather than producing a lot of tiny MIDI files and calling FluidSynth for each. However, there's a risk of spilling sound across the samples.