This is one of the 100 recipes of the IPython Cookbook, the definitive guide to high-performance scientific computing and data science in Python.

3.5. Using interactive widgets: a piano in the notebook

You need to download the Piano dataset on the book's website. (http://ipython-books.github.io)

This dataset contains synthetized piano notes obtained on archive.org (CC0 1.0 Universal licence). (https://archive.org/details/SynthesizedPianoNotes)

  1. Let's import a few modules.
In [ ]:
import numpy as np
import os
from IPython.display import Audio, display, clear_output
from IPython.html import widgets
from functools import partial
  1. To create a piano, we will draw one button per note. The corresponding note plays when the user clicks on the button. This is implemented by displaying an <audio> element.
In [ ]:
dir = 'data/synth'
In [ ]:
# This is the list of notes.
notes = 'C,C#,D,D#,E,F,F#,G,G#,A,A#,B,C'.split(',')
In [ ]:
def play(note, octave=0):
    """This function displays an HTML Audio element
    that plays automatically when it appears."""
    f = os.path.join(dir, 
         "piano_{i}.mp3".format(i=note+12*octave))
    clear_output()
    display(Audio(filename=f, autoplay=True))
  1. We are going to place all buttons within a container widget. In IPython 2.0+, widgets can be organized hierarchically. One common use case is to organize several widgets in a given layout. Here, piano will contain 12 buttons for the 12 notes.
In [ ]:
piano = widgets.ContainerWidget()
  1. We create our first widget: a slider control that specifies the octave (0 or 1 here).
In [ ]:
octave_slider = widgets.IntSliderWidget()
octave_slider.max = 1
octave_slider
  1. Now, we create the buttons. There are several steps. First, we instantiate a ButtonWidget object. Then, we specify a callback function that plays the corresponding note (given by an index) at a given octave (given by the current value of the octave slider). Finally, we set the CSS of each button, notably the white or black color.
In [ ]:
buttons = []
for i, note in enumerate(notes):
    button = widgets.ButtonWidget(description=note)
    
    def on_button_clicked(i, _):
        play(i+1, octave_slider.value)
        
    button.on_click(partial(on_button_clicked, i))
    
    button.set_css({'width': '30px', 
                    'height': '60px',
                    'padding': '0',
                    'color': ('black', 
                              'white')['#' in note],
                    'background': ('white', 'black')['#' in note],
                    'border': '1px solid black',
                    'float': 'left'})
    
    buttons.append(button)
  1. Finally, we arrange all widgets with the containers. The piano container contains the buttons, and the main container (container) contains the slider and the piano.
In [ ]:
piano.children = buttons
In [ ]:
container = widgets.ContainerWidget()
container.children = [octave_slider,
                      piano]

By default, widgets are organized vertically within a container. Here, the octave slider will be above the piano.

In [ ]:
display(container)
piano.remove_class('vbox')
piano.add_class('hbox')

Within the piano, we want all notes to be arranged horizontally. We do this by replacing the default vbox CSS class by the hbox class.

You'll find all the explanations, figures, references, and much more in the book (to be released later this summer).

IPython Cookbook, by Cyrille Rossant, Packt Publishing, 2014 (500 pages).