Generating simple audio samples with music21

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:

  • pitch
  • [precise frequency]
  • instrument
  • volume
  • duration
  • location in the sample

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]:
<music21.note.Note C>

In [4]:
s = Stream([Note('C')])

If we have MuseScore installed, we can we the music sheet representation.


In [5]:
s.show()


Out[5]:
<music21.ipython21.objects.IPythonPNGObject at 0x1046ac8d0>

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]:
'data/working/example-parametric-note/c.midi'

Properties of the Note


In [8]:
n = Note('C')
n


Out[8]:
<music21.note.Note C>

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)


<music21.note.Note C>
pitch: C
duration: <music21.duration.Duration 1.0>
name: C
full name: C
pitch class: 0
octave: None
frequency 261.6255653005985 Hz
midi: 60
pitch space: 60.0

Creating Note with parameters


In [11]:
# different note in the default octave
describe_note(Note('E'))


<music21.note.Note E>
pitch: E
duration: <music21.duration.Duration 1.0>
name: E
full name: E
pitch class: 4
octave: None
frequency 329.62755691286986 Hz
midi: 64
pitch space: 64.0

In [12]:
# a note in the specific octave
describe_note(Note('G#3'))


<music21.note.Note G#>
pitch: G#3
duration: <music21.duration.Duration 1.0>
name: G#
full name: G-sharp in octave 3
pitch class: 8
octave: 3
frequency 207.65234878997245 Hz
midi: 56
pitch space: 56.0

In [13]:
# note specified by its octave and pitch class within an octave
describe_note(Note(octave=2, pitchClass=3))


<music21.note.Note E->
pitch: E-2
duration: <music21.duration.Duration 1.0>
name: E-
full name: E-flat in octave 2
pitch class: 3
octave: 2
frequency 77.78174593052012 Hz
midi: 39
pitch space: 39.0

In [14]:
# note specified by its integer MIDI number
describe_note(Note(midi=21))


<music21.note.Note A>
pitch: A0
duration: <music21.duration.Duration 1.0>
name: A
full name: A in octave 0
pitch class: 9
octave: 0
frequency 27.499999999999947 Hz
midi: 21
pitch space: 21.0

In [15]:
# microtonal pitch using the pitch space attribute (like MIDI but floating point)
describe_note(Note(ps=21.25))


<music21.note.Note A>
pitch: A0(+25c)
duration: <music21.duration.Duration 1.0>
name: A
full name: A in octave 0 (+25c)
pitch class: 9
octave: 0
frequency 27.899996710781842 Hz
midi: 21
pitch space: 21.25

Changing duration


In [16]:
# note with duration of half of a quarter note
note = Note(midi=21, duration=Duration(0.5))
describe_note(note)


<music21.note.Note A>
pitch: A0
duration: <music21.duration.Duration 0.5>
name: A
full name: A in octave 0
pitch class: 9
octave: 0
frequency 27.499999999999947 Hz
midi: 21
pitch space: 21.0

In [17]:
# note with duration of half of a quarter note
note = Note(midi=21, duration=Duration(2.5))
describe_note(note)


<music21.note.Note A>
pitch: A0
duration: <music21.duration.Duration 2.5>
name: A
full name: A in octave 0
pitch class: 9
octave: 0
frequency 27.499999999999947 Hz
midi: 21
pitch space: 21.0

Changing volume

Volume can be specified by parameters:

  • velocity with range from 0 to 127 or by
  • velocityScalar with range from 0.0 to 1.0

In [18]:
for v in [0, 32, 64, 127]:
    print(Volume(velocity=v))


<music21.volume.Volume realized=0.0>
<music21.volume.Volume realized=0.25>
<music21.volume.Volume realized=0.5>
<music21.volume.Volume realized=1.0>

In [19]:
for v in [0, 0.25, 0.5, 1.0]:
    print(Volume(velocityScalar=v))


<music21.volume.Volume realized=0.0>
<music21.volume.Volume realized=0.25>
<music21.volume.Volume realized=0.5>
<music21.volume.Volume realized=1.0>

In [20]:
Chord(['C']).volume


Out[20]:
<music21.volume.Volume realized=0.71>

In [21]:
c = Chord([Note('C')])
c.volume = Volume(velocityScalar=0.25)
c.volume


Out[21]:
<music21.volume.Volume realized=0.25>

How to set tempo?


In [22]:
metronome = MetronomeMark(number=60)
metronome.durationToSeconds(Duration(1.0))


Out[22]:
1.0

Just add a metronome mark at the beginning of the stream.


In [23]:
Stream([MetronomeMark(number=60), Note('C')]).show()


Out[23]:
<music21.ipython21.objects.IPythonPNGObject at 0x10406e940>

Sequence of notes


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.