PROGRAMMING SOUND AND MUSIC¶
Loud noise may damage your hearing and your speakers! Make sure to turn your computer volume down to a safe level so you don’t end up like Beethoven.
Jupylet is full of sound. Here are some of its features:
Play audio tracks and samples in WAV, FLAC, and OGG formats.
Basic support of SFZ format for playing multi-sampled instruments.
Simple to use MIDI support - just hook your piano keyboard and play.
A novel framework for programming sound synthesizers and effects.
Live loops and music coding in the spirit of Sonic Pi.
Play it, Sam¶
If all you want is to add some music or sound effects to your game then there is nothing easier. First, let’s power up Jupylet’s audio with the following import statement:
from jupylet.audio.bundle import *
Then create a
Sample instance and load it into
memory. Here is how its done in the examples/21-pong.ipynb
pong_sound = Sample('sounds/pong-blip.wav').load()
You can play any WAV, FLAC, or OGG audio file. If you would like to play an unsupported audio file such as MP3 you can convert it to one of the supported formats with a conversion tool or an online service such as Convertio.
Once you have loaded the sample you can play it any time you want with:
play() method accepts an amp
parameter between 0 and 1 that controls output volume, and a pan parameter
between -1 and 1 that controls the left-right balance. So to play the sound
effect half as loud and just on the left side try:
By default, Jupylet sounds are monophonic,
which means that if you call the
method twice in quick succession you will not hear two instances of the sample
being played simultaneously. Instead Jupylet will stop the first sound before
it starts playing the second.
You can play multiple instances of the same sound polyphonically (e.g. think of how fireworks sound) like this:
pong_sound.play_poly() pong_sound.play_poly() pong_sound.play_poly()
If the sample is a long playing sound track you may stop it any time with:
A Few Notes¶
Let’s move on to something more interesting; let’s play a simple middle C note for the duration of a full note on a predefined synthesizer:
The tb303 is a predefined synthesizer that produces a sound reminiscent of the Roland TB-303 synthesizer from the early 80s that initially failed commercially but years later became a staple of electronic music. We will see later how it can be implemented in Jupylet using just a few lines of code.
To play a sequence of notes insert a special sleep instructions between them:
tb303.play(G, 1/8) await sleep(1/8) tb303.play(G, 1/8) await sleep(1/8) tb303.play(G, 1/8) await sleep(1/8) tb303.play(Eb, 3/4)
The await instruction is part of asynchronous Python programming which may be considered advanced Python. In general, you can only use await in an IPython interpreter session, in a Jupyter notebook cell, or inside asynchronous functions such as a Jupylet live loop. You cannot directly use await in a regular python script. i.e. if you copy the code above into a Jupyter notebook cell, it should work just fine, but if you copy it into a text file and try to run it as a Python script, it will exit with an error.
In the code above Eb means E flat, and similarly Es would mean E sharp.
The unit of duration is a full note, and you can set the beats per minute and
the note value with
Normally if you play a new note the previous note will cease as if the
synthesizer is monophonic. If you would like to play multiple notes together
play_poly() method instead. It
will start playing a new note and return a reference to the new sound instance
so you may control it as it plays:
c = tb303.play_poly(C5) f = tb303.play_poly(F5) await sleep(1) c.play_release() await sleep(1) f.play_release()
Playing the tb303 without specifying a duration will generate a note that goes
on indefinitely, like pressing a keyboard key without releasing it. The code
play_release() to release the
notes individually. You can also release all the currently playing sounds of a
synthesizer like this:
tb303.play_poly(C5) tb303.play_poly(F5) await sleep(1) tb303.play_release()
There are a few more predefined synthesizers to choose from, notably one that is reminiscent of the famous Hammond organ that you can instantiate and use like this:
hammond = Hammond() hammond.play_poly(C4) await sleep(1/2) hammond.play_poly(D4) await sleep(1/2) hammond.play_poly(G4) await sleep(1) hammond.play_release()
Playing with Virtual Instruments¶
Besides synthesizers, you can also play music with virtual sampled instruments. Jupylet includes basic support for the SFZ format that will enable you to play a variety of instruments that you can find online.
xylo = Sample('sounds/VCSL/Xylophone/Xylophone - Medium Mallets.sfz', amp=12) xylo.play_poly(C) await sleep(1/8) xylo.play_poly(D) await sleep(1/8) xylo.play_poly(E) await sleep(1/8) xylo.play_poly(F) await sleep(1/8) xylo.play_poly(G) await sleep(1/8) xylo.play_poly(F) await sleep(1/8) xylo.play_poly(E) await sleep(1/8) xylo.play_poly(D) await sleep(1/8) xylo.play_poly(C) await sleep(1/2) xylo.play_poly(C5) await sleep(1/2)
Make Some Space¶
Jupylet let’s you apply a varaiety of effects to a sound instance or to the entire audio stream.
For example let’s add a distortion effect to the tb303 synthesizer:
tb303.set_effects(Overdrive(gain=4)) tb303.play_poly(C3) tb303.play_poly(E3) tb303.play_poly(G3) await sleep(4) tb303.play_release() tb303.set_effects()
Another kind of effect is the convolution reverb which applies a recorded impulse response to a sound instance or to the entire audio stream. Impulse responses are recorded by specialists and capture the sonic signature of a room or any other kind of physical space.
I like Damian’s Maes Howe impulse response in particular. It adds a nice sense of space and a touch of realism to the generated sound.
Let’s apply it to the entire audio stream intermittently so you may notice the effect; and make sure to try it with a good pair of headphones:
for i in range(5): if i % 2: print('Reverb on') set_effects(ConvolutionReverb('sounds/impulses/MaesHowe.flac')) else: print('Reverb off') set_effects() tb303.play_poly(C, 1) await sleep(1) tb303.play_poly(E, 1) await sleep(1) tb303.play_poly(G, 1) await sleep(1)
You may have noticed how the examples above became progressively more elaborate, starting with playing a single note, then multiple notes at the same time, then a sequence of notes, and finally a sequence of notes in a loop.
As the code becomes more elaborate we can do more interesting stuff but we also have a new problem.
When we play a single note the Jupyter notebook appears to remain responsive. This allows us for example to type in an instruction to start a second note or to release the first note.
However if you run the loop above you may notice that while you can type in a new instruction in the next notebook cell, it will not be run until the loop is done. In other words, in some sense the notebook becomes unresponsive.
We have already seen a similar problem when we programmed the alien drifting animation in the previous chapter and we solved it there by setting up a schedulled handler.
A similar construct can help us here as well. It is called the live loop and it is a central concept in Sam Aaron’s totally awesome code-based music creation and performance tool Sonic Pi.
It turns out a Jupyter notebook is the perfect environment for Python based music live coding and live loops.
To program live loops we first need to create an app instance like this:
app = sonic_py()
Now let’s rewrite the code above as a live loop:
@app.sonic_live_loop(times=5) async def loop0(ncall): if ncall % 2: print('Reverb on') set_effects(ConvolutionReverb('sounds/impulses/MaesHowe.flac')) else: print('Reverb off') set_effects() tb303.play_poly(C, 1) await sleep(1) tb303.play_poly(E, 1) await sleep(1) tb303.play_poly(G, 1) await sleep(1)
The function name loop0 is arbitrary. You can name the function anything you want. The times parameter is optional. Without it the loop will continue indefinitely. To stop the loop at any time call:
The ncall parameter is also optional. A simpler live loop would look like this:
@app.sonic_live_loop async def loop0(): tb303.play_poly(C, 1) await sleep(1) tb303.play_poly(E, 1) await sleep(1) tb303.play_poly(G, 1) await sleep(1)
There is another problem that we need to take care of. When you call
play_poly() the new note is scheduled to
play as soon as possible. The problem with that is that minor mistimings in
sleep() calls are normal in desktop
operating systems and may result in noticeable playing out of tempo.
The correct way to play notes with accurate tempo in a live loop is the following:
@app.sonic_live_loop async def loop0(): use(tb303) play(C3, 1) await sleep(1) play(E3, 1) await sleep(1) play(G3, 1) await sleep(1)
You can play multiple loops simultaneously. Let’s add another voice:
@app.sonic_live_loop async def loop1(): use(hammond) play(E, 1) await sleep(1) play(C, 2) await sleep(2) play(G, 1) await sleep(1) play(C, 2) await sleep(2) play(B, 2-1/3) await sleep(2-1/3) play(G, 1/3) await sleep(1/3) play(F, 2/3) await sleep(2/3) play(G, 1/3) await sleep(1/3) play(F, 2/3) await sleep(2/3) await sleep(1/3) play(E, 2) await sleep(2)
Select both Jupyter cells and run them together to start the two loops in sync.
You can modify the code of a live loop while it is playing, and when you run the Jupyter cell with the new code, the live loop will immediately restart and play the new code.
However, sometimes it is more desirable to wait for the currently running
loop to complete its cycle. If you decorate a live loop with
@app.sonic_live_loop2 and run it,
the new code will kick in only after the currently playing loop completes a
The MIDI (Musical Instrument Digital Interface) standard is a specification that makes it possible to connect digital musical instruments to your computer.
If you have an electronic (piano) keyboard, chances are it has a MIDI port that you can connect to your computer with a MIDI to USB cable.
If you installed Jupylet with MIDI support you are good to go. If not, open a miniconda console and type in:
pip install jupylet[midi]
To enable midi in Jupylet you just need to choose a sound instance to use. Let’s hook it up with the hammond synthesizer:
Alternatively, if you want full control you can program your own MIDI handler like this:
@app.event def midi_message(msg): ... do whatever you want here ...
That’s all there is to it.
Well, almost. By default most computer audio systems incur a short delay (also called latency) between the time you insturct the computer to play a note to the time it is actually played.
Normally, for games and live loops this short delay is not noticeable, but you may find that it makes it difficult to play a MIDI keyboard.
To minimize audio latency you can try this command:
Reducing audio latency may cause the computer audio system to emit unpleasant stuttering sound. If this happens Jupylet will automatically attenuate output volume. Nevertheless, make sure to turn your computer’s volume down to prevent damage to your speakers and ears!
Lowering audio latency may cause the computer audio system to emit unpleasant stuttering sounds if your computer is unable to keep up with the required computations. If this happens you may set latency back to its default value with:
Then, you may try to address the problem by switching your computer’s power mode to Best performance or by eliminating CPU intensive sound computations. Once you do that you may try to set latency back to minimal.
To switch your computer’s power mode to Best performance on Windows 10 select the Battery icon on the taskbar and then drag the slider all the way to the right to Best performance mode as shown in the following figure:
To reduce CPU load try removing sound effects or changing instruments.