PROGRAMMING SOUND AND MUSIC
===========================
Da-Da-Da-DUM!
-------------
.. warning::
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:
.. code-block:: python
from jupylet.audio.bundle import *
Then create a :class:`~jupylet.audio.sample.Sample` instance and load it into
memory. Here is how its done in the `examples/21-pong.ipynb `_
example notebook:
.. code-block:: python
pong_sound = Sample('sounds/pong-blip.wav').load()
.. note::
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:
.. code-block:: python
pong_sound.play()
The :meth:`~jupylet.audio.sound.GatedSound.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:
.. code-block:: python
pong_sound.play(amp=0.5, pan=-1)
By default, Jupylet sounds are `monophonic `_,
which means that if you call the :meth:`~jupylet.audio.sound.GatedSound.play()`
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:
.. code-block:: python
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:
.. code-block:: python
pong_sound.play_release()
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:
.. code-block:: python
tb303.play(C4, 1)
.. note::
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:
.. code-block:: python
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)
.. note::
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 :func:`~jupylet.audio.set_bpm` and
:func:`~jupylet.audio.set_note_value` respectively.
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
call the :meth:`~jupylet.audio.sound.GatedSound.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:
.. code-block:: python
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
above calls :meth:`~jupylet.audio.sound.GatedSound.play_release` to release the
notes individually. You can also release all the currently playing sounds of a
synthesizer like this:
.. code-block:: python
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:
.. code-block:: python
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.
The piano notebook `examples/14-piano.ipynb `_
uses a multisampled xylophone from the `VCSL library `_
by Versilian Studios. Let's see how:
.. code-block:: python
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:
.. code-block:: python
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.
Jupylet includes `three impulses responses `_
recorded by `Damian Murphy `_ and you can find
many more impulse responses in his website and elsewhere.
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:
.. code-block:: python
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)
Sonic Py(thon)
--------------
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 :any:`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:
.. code-block:: python
app = sonic_py()
Now let's rewrite the code above as a live loop:
.. code-block:: python
@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:
.. code-block:: python
app.stop(loop0)
The `ncall` parameter is also optional. A simpler live loop would look like
this:
.. code-block:: python
@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
:meth:`~jupylet.audio.sound.GatedSound.play_poly` the new note is scheduled to
play as soon as possible. The problem with that is that minor mistimings in
"wakeups" from :func:`~jupylet.audio.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:
.. code-block:: python
@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:
.. code-block:: python
@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
:func:`@app.sonic_live_loop2 ` and run it,
the new code will kick in only after the currently playing loop completes a
cycle.
MIDI Keyboards
--------------
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.
MIDI support requires the ``python-rtmidi`` package. If it's not already
installed, see the box below for how to install it.
.. note::
**Installing python-rtmidi**
``python-rtmidi`` needs a C++ compiler to build, on every platform, since
it has no wheel of its own on PyPI. You have two options: use a
precompiled wheel, or install a compiler yourself.
**Option 1 - use a precompiled wheel (recommended):**
.. code-block:: bash
pip install --extra-index-url https://nir.github.io/jupylet-wheels/ --only-binary python-rtmidi python-rtmidi
**Option 2 - install a compiler and build it yourself:**
**On Windows 11** -- Jupylet needs Microsoft's C++ Build Tools to build
``python-rtmidi``. If you're not sure whether you already have them
installed, download and run `the Build Tools for Visual Studio installer
`_, and in it
select the "Desktop development with C++" workload. Once that's done,
install ``python-rtmidi``:
.. code-block:: bash
pip install python-rtmidi
**On macOS** -- Jupylet needs Apple's Xcode Command Line Tools to build
``python-rtmidi``. If you're not sure whether you already have them
installed, it's safe to just run this command anyway - it won't do
anything if they're already there:
.. code-block:: bash
xcode-select --install
Once that's done, install ``python-rtmidi``:
.. code-block:: bash
pip install python-rtmidi
**On Ubuntu Linux** -- install the required system packages, then
``python-rtmidi``:
.. code-block:: bash
sudo apt install build-essential libasound2-dev libjack-dev
pip install python-rtmidi
If you install ``python-rtmidi`` while a Jupyter kernel is already
running, restart the kernel afterwards and re-run the steps above (the
``from jupylet.audio.bundle import *`` import in particular) so Jupylet
picks it up.
To enable midi in Jupylet you just need to choose a sound instance to use.
Let's hook it up with the hammond synthesizer:
.. code-block:: python
app.set_midi_sound(hammond)
Alternatively, if you want full control you can program your own MIDI handler
like this:
.. code-block:: python
@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:
.. code-block:: python
set_latency('minimal')
.. warning::
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:
.. code-block:: python
set_latency('high')
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:
.. image:: ../images/power-mode.png
To reduce CPU load try removing sound effects or changing instruments.