Pitch manipulation and Praat commands

Note

An online, interactive version of this example is available at Binder: binder

Pitch manipulation and Praat commands#

Another common use of Praat functionality is to manipulate certain features of an existing audio fragment. For example, in the context of a perception experiment one might want to change the pitch contour of an existing audio stimulus while keeping the rest of the acoustic features the same. Parselmouth can then be used to access the Praat algorithms that accompish this, from Python.

Since this Praat Manipulation functionality has currently not been ported to Parselmouth’s Python interface, we will need to use Parselmouth interface to access raw Praat commands.

In this example, we will increase the pitch contour of an audio recording of the word “four”, 4_b.wav, by one octave. To do so, let’s start by importing Parselmouth and opening the audio file:

[1]:
import parselmouth

sound = parselmouth.Sound("audio/4_b.wav")

We can also listen to this audio fragment:

[2]:
from IPython.display import Audio
Audio(data=sound.values, rate=sound.sampling_frequency)
[2]:

However, now we want to use the Praat Manipulation functionality, but unfortunately, Parselmouth does not yet contain a Manipulation class and the necessary functionality is not directly accessible through the Sound object sound. To directly access the Praat commands conveniently from Python, we can make use of the parselmouth.praat.call function.

[3]:
from parselmouth.praat import call

manipulation = call(sound, "To Manipulation", 0.01, 75, 600)
[4]:
type(manipulation)
[4]:
parselmouth.Data

Note how we first pass in the object(s) that would be selected in Praat’s object list. The next argument to this function is the name of the command as it would be used in a script or can be seen in the Praat user interface. Finally, the arguments to this command’s parameters are passed to the function (in this case, Praat’s default values for “Time step (s)”, “Minimum pitch (Hz)”, and “Maximum pitch (Hz)”). This call to parselmouth.praat.call will then return the result of the command as a Python type or Parselmouth object. In this case, a Praat Manipulation object would be created, so our function returns a parselmouth.Data object, as a parselmouth.Manipulation class does not exist in Parselmouth. However, we can still query the class name the underlying Praat object has:

[5]:
manipulation.class_name
[5]:
'Manipulation'

Next, we can continue using Praat functionality to further use this manipulation object similar to how one would achieve this in Praat. Here, note how we can mix normal Python (e.g. integers and lists), together with the normal use of Parselmouth as Python library (e.g., sound.xmin) as well as with the parselmouth.praat.call function.

[6]:
pitch_tier = call(manipulation, "Extract pitch tier")

call(pitch_tier, "Multiply frequencies", sound.xmin, sound.xmax, 2)

call([pitch_tier, manipulation], "Replace pitch tier")
sound_octave_up = call(manipulation, "Get resynthesis (overlap-add)")
[7]:
type(sound_octave_up)
[7]:
parselmouth.Sound

The last invocation of call resulted in a Praat Sound object being created and returned. Because Parselmouth knows that this type corresponds to a parselmouth.Sound Python object, the Python type of this object is not a parselmouth.Data. Rather, this object is now equivalent to the one we created at the start of this example. As such, we can use this new object normally, calling methods and accessing its contents. Let’s listen and see if we succeeded in increasing the pitch by one octave:

[8]:
Audio(data=sound_octave_up.values, rate=sound_octave_up.sampling_frequency)
[8]:

And similarly, we could also for example save the sound to a new file.

[9]:
sound_octave_up.save("4_b_octave_up.wav", "WAV")
[10]:
Audio(filename="4_b_octave_up.wav")
[10]:
[11]:
# Clean up the created audio file again
!rm 4_b_octave_up.wav

We can of course also turn this combination of commands into a custom function, to be reused in later code:

[12]:
def change_pitch(sound, factor):
    manipulation = call(sound, "To Manipulation", 0.01, 75, 600)

    pitch_tier = call(manipulation, "Extract pitch tier")

    call(pitch_tier, "Multiply frequencies", sound.xmin, sound.xmax, factor)

    call([pitch_tier, manipulation], "Replace pitch tier")
    return call(manipulation, "Get resynthesis (overlap-add)")

Using Jupyter widgets, one can then change the audio file or the pitch change factor, and interactively hear how this sounds.

To try this for yourself, open an online, interactive version of this notebook on Binder! (see link at the top of this notebook)

[13]:
import ipywidgets
import glob

def interactive_change_pitch(audio_file, factor):
    sound = parselmouth.Sound(audio_file)
    sound_changed_pitch = change_pitch(sound, factor)
    return Audio(data=sound_changed_pitch.values, rate=sound_changed_pitch.sampling_frequency)

w = ipywidgets.interact(interactive_change_pitch,
                        audio_file=ipywidgets.Dropdown(options=sorted(glob.glob("audio/*.wav")), value="audio/4_b.wav"),
                        factor=ipywidgets.FloatSlider(min=0.25, max=4, step=0.05, value=1.5))