Audio Samples#

Summary

In this tutorial, we’ll see how to load audio samples (either from file urls or numpy arrays) in the front-end and play them with a player or a sampler.

Note: you can download this tutorial as a Jupyter notebook.

import ipytone
import numpy as np
import matplotlib.pyplot as plt
Matplotlib is building the font cache; this may take a moment.
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[1], line 3
      1 import ipytone
      2 import numpy as np
----> 3 import matplotlib.pyplot as plt

File ~/checkouts/readthedocs.org/user_builds/ipytone/conda/stable/lib/python3.11/site-packages/matplotlib/pyplot.py:52
     50 from cycler import cycler
     51 import matplotlib
---> 52 import matplotlib.colorbar
     53 import matplotlib.image
     54 from matplotlib import _api

File ~/checkouts/readthedocs.org/user_builds/ipytone/conda/stable/lib/python3.11/site-packages/matplotlib/colorbar.py:19
     16 import numpy as np
     18 import matplotlib as mpl
---> 19 from matplotlib import _api, cbook, collections, cm, colors, contour, ticker
     20 import matplotlib.artist as martist
     21 import matplotlib.patches as mpatches

File ~/checkouts/readthedocs.org/user_builds/ipytone/conda/stable/lib/python3.11/site-packages/matplotlib/contour.py:13
     11 import matplotlib as mpl
     12 from matplotlib import _api, _docstring
---> 13 from matplotlib.backend_bases import MouseButton
     14 from matplotlib.text import Text
     15 import matplotlib.path as mpath

File ~/checkouts/readthedocs.org/user_builds/ipytone/conda/stable/lib/python3.11/site-packages/matplotlib/backend_bases.py:45
     42 import numpy as np
     44 import matplotlib as mpl
---> 45 from matplotlib import (
     46     _api, backend_tools as tools, cbook, colors, _docstring, text,
     47     _tight_bbox, transforms, widgets, get_backend, is_interactive, rcParams)
     48 from matplotlib._pylab_helpers import Gcf
     49 from matplotlib.backend_managers import ToolManager

File ~/checkouts/readthedocs.org/user_builds/ipytone/conda/stable/lib/python3.11/site-packages/matplotlib/text.py:16
     14 from . import _api, artist, cbook, _docstring
     15 from .artist import Artist
---> 16 from .font_manager import FontProperties
     17 from .patches import FancyArrowPatch, FancyBboxPatch, Rectangle
     18 from .textpath import TextPath, TextToPath  # noqa # Logically located here

File ~/checkouts/readthedocs.org/user_builds/ipytone/conda/stable/lib/python3.11/site-packages/matplotlib/font_manager.py:1548
   1544     _log.info("generated new fontManager")
   1545     return fm
-> 1548 fontManager = _load_fontmanager()
   1549 findfont = fontManager.findfont
   1550 get_font_names = fontManager.get_font_names

File ~/checkouts/readthedocs.org/user_builds/ipytone/conda/stable/lib/python3.11/site-packages/matplotlib/font_manager.py:1542, in _load_fontmanager(try_read_cache)
   1540             _log.debug("Using fontManager instance from %s", fm_path)
   1541             return fm
-> 1542 fm = FontManager()
   1543 json_dump(fm, fm_path)
   1544 _log.info("generated new fontManager")

File ~/checkouts/readthedocs.org/user_builds/ipytone/conda/stable/lib/python3.11/site-packages/matplotlib/font_manager.py:1016, in FontManager.__init__(self, size, weight)
   1013 for path in [*findSystemFonts(paths, fontext=fontext),
   1014              *findSystemFonts(fontext=fontext)]:
   1015     try:
-> 1016         self.addfont(path)
   1017     except OSError as exc:
   1018         _log.info("Failed to open font file %s: %s", path, exc)

File ~/checkouts/readthedocs.org/user_builds/ipytone/conda/stable/lib/python3.11/site-packages/matplotlib/font_manager.py:1043, in FontManager.addfont(self, path)
   1041     self.afmlist.append(prop)
   1042 else:
-> 1043     font = ft2font.FT2Font(path)
   1044     prop = ttfFontProperty(font)
   1045     self.ttflist.append(prop)

KeyboardInterrupt: 

Audio buffers#

AudioBuffer and AudioBuffers can be used to create one or more audio buffers in the front-end from either files or a numpy arrays.

Example 1: load one sample from a numpy array#

Let’s first create a custom waveform with numpy:

# sine + noise waveform

sample_rate = 44100
duration = 1
frequency = 440

size = int(sample_rate * duration)

factor = frequency * np.pi * 2 / sample_rate
waveform = np.sin(np.arange(size) * factor)
waveform += np.random.uniform(-0.1, 0.1, size=size)

Let’s have a look at the waveform:

plt.plot(waveform[0:1000]);

We can then directly pass the numpy array to the AudioBuffer constructor:

sine_noise_buffer = ipytone.AudioBuffer(url_or_array=waveform)

Important

Create a buffer from a numpy array is currently limited to samples of a duration less or equal to 10 seconds.

Example 2: load multiple samples from files (urls)#

Let’s create new buffers from wav or mp3 file urls.

Important

Depending on the given urls, creating the buffers may be blocked due to the server CORS policy.

If you want to load local files from within JupyterLab, you can get a valid url by going in the file browser, right-click on the file and “Copy Download Link”.

# These urls are likely wrong if you've downloaded this notebook and run it locally
base_url = "http://localhost:8888/files/docs/"
kick_url = "kick.wav?_xsrf=2%7C1ced6df3%7C0514fa79e3ccfbcffefe3f864a0d4032%7C1654092692"
snare_url = "snare.wav?_xsrf=2%7C1ced6df3%7C0514fa79e3ccfbcffefe3f864a0d4032%7C1654092692"
drum_buffers = ipytone.AudioBuffers(
    base_url=base_url,
    urls={"kick": kick_url, "snare": snare_url},
)

Single buffers may be accessed via the buffers property, e.g.,

drum_buffers.buffers["kick"]

Buffer attributes#

Audio buffers have a few read-only attributes like loaded, duration, length, sample_rate, etc.

# whether all drum buffers are loaded in the front-end
drum_buffers.loaded
# clip duration (in seconds)
drum_buffers.buffers["kick"].duration
# clip length in number of samples
drum_buffers.buffers["kick"].length

And a reverse property that can be read or written:

drum_buffers.buffers["kick"].reverse

Player#

Player is a source audio node for playing samples.

Let’s create a new player from the custom sine/noise buffer created above:

player = ipytone.Player(sine_noise_buffer).to_destination()

Note

We can also directly pass an url to Player, which will automatically create an audio buffer.

Like any other source, it has start() ant stop() methods:

player.start().stop("+3")

It also has some additional properties, e.g., to add fade-in/out, play it looped, change its playback rate, etc.

player.loop = True
player.fade_in = 0.2
player.playback_rate = 0.5
player.start().stop("+3")

For convenience, Players can be used to create multiple Player instances at once.

Let’s create players from the drum buffers created above:

drums = ipytone.Players(drum_buffers.buffers).to_destination()

We can get the individual players with get_player():

drums.get_player("kick").start().stop("+1")
drums.get_player("snare").start("+0.5").stop("+1.5")

Sampler#

As an alternative to Player, Sampler is an instrument based on samples.

For example, let’s create a sampler with the sine/noise buffer created above. The name of the buffers must correspond to something that can be interpreted like a musical note.

sampler = ipytone.Sampler({"A4": sine_noise_buffer}, volume=-10).to_destination()

Being an instrument, Sampler provides the same interface than any other instrument (see the instruments tutorial). When playing a note, the sampler selects the closest sample and adapts the pitch to generate the corresponding note.

sampler.trigger_attack_release("C2", 0.2)
sampler.trigger_attack_release("A2", 0.2, time="+0.2")
sampler.trigger_attack_release("C3", 0.2, time="+0.4")
sampler.trigger_attack_release("C4", 0.2, time="+0.6")

Sampler is a polyphonic instrument:

# trigger a chord
sampler.trigger_attack_release(["C4", "E4", "G4"], 0.5)

Dispose buffers#

Like for other audio nodes, audio buffers should be disposed if they are not used anymore.

sine_noise_buffer.dispose()
drum_buffers.dispose()

# note: dispose the player(s) or the sampler
# would also dispose the buffers
player.dispose()
drums.dispose()
sampler.dispose()