Uneven rhythms#

In this example, we will go through the methods section of a classic study by Repp, London, and Keller (2005). A PDF for the paper is available here.

This example makes heavy use of list comprehensions. If you do not yet understand those fully, best to take a look at this tutorial before reading on.

First we do some necessary imports:

[31]:
from thebeat.music import Rhythm
from thebeat.core import Sequence, SoundStimulus, SoundSequence
import numpy as np

In the experiment in Repp, London, and Keller (2005), the production of and synchronization with ‘uneven’ rhythms was tested. These are rhythms with unusual subdivisions of timing (i.e. non-binary). Check out Figure 1 from the paper. We will only make the {2, 3} set; at the end of this example you will be able to make the other ones yourself.


We can create these rhythms easily using the Rhythm class. This class can be used to generate or plot rhythms. Note that it doesn’t contain any sound, it is in essence similar to the Sequence class, except that it has a beat_ms, a time_signature and a list is_played, which contains information about whether the notes are actually played, or whether they are rests. In addition, Rhythm objects always have end_with_interval=True (see __init__()).

Rhythm objects can be created in a number of ways. The constructor uses IOIs, e.g. r = Rhythm(500, 500, 500, 500). Easier is to think about it in terms of integer ratios (from_integer_ratios()), or possibly note values (from_note_values()).

Example rhythm#

As an example let’s create the first rhythm from set A, and then print and plot it. We’ll do it in 5/8 time signature to make the plot the same as in the illustration above.

Note

beat_ms also adheres to the denominator of the time signature. As such, changing from 5/4 to 5/8 doesn’t change the tempo.

[33]:
r_23 = Rhythm.from_integer_ratios([2, 3], beat_ms=170, time_signature=(5, 8))
print(r_23)

r_23.plot_rhythm(dpi=600);
Object of type Rhythm.
Time signature: (5, 8)
Number of bars: 1.0
Beat (ms): 170
Number of events: 2
IOIs: [340. 510.]
Onsets:[  0. 340.]
Name: None

../../_images/examples_creating_stimuli_repplondonkeller_10_1.png

To elongate this rhythm we can do:

[34]:
r_23 = r_23 * 3
r_23.plot_rhythm(dpi=600);
../../_images/examples_creating_stimuli_repplondonkeller_12_0.png
[35]:
seq = r_23.to_sequence()
fig, ax = seq.plot_sequence();
../../_images/examples_creating_stimuli_repplondonkeller_13_0.png

Creating the {2, 3} set#

In the method section it says:

“[The tempo] decreased from 170 ms in the first trial to 100 ms in the eighth trial, in steps of -10 ms.”

So, we’ll now create the two rhythms of the {2, 3} set at the eight different tempi mentioned above. For convenience, we’ll call the set ‘Set A’ from now on.

[36]:
# Create tempi
tempi = np.arange(170, 90, -10)  # as arange does not include the enpoint we stop at 90 instead of 100
print(tempi)
[170 160 150 140 130 120 110 100]
[37]:
set_a = []

for tempo in tempi:
    set_a.append(Rhythm.from_integer_ratios([2, 3] * 3,
                                            beat_ms=tempo,
                                            time_signature=(5, 4),
                                            name=f"2_3_{tempo}ms"))
    set_a.append(Rhythm.from_integer_ratios([3, 2] * 3,
                                            beat_ms=tempo,
                                            time_signature=(5, 4),
                                            name=f"3_2_{tempo}ms"))

# Let's see what Set A looks like now
print(set_a)
[Rhythm(name=2_3_170ms, n_bars=3.0, time_signature=(5, 4)), Rhythm(name=3_2_170ms, n_bars=3.0, time_signature=(5, 4)), Rhythm(name=2_3_160ms, n_bars=3.0, time_signature=(5, 4)), Rhythm(name=3_2_160ms, n_bars=3.0, time_signature=(5, 4)), Rhythm(name=2_3_150ms, n_bars=3.0, time_signature=(5, 4)), Rhythm(name=3_2_150ms, n_bars=3.0, time_signature=(5, 4)), Rhythm(name=2_3_140ms, n_bars=3.0, time_signature=(5, 4)), Rhythm(name=3_2_140ms, n_bars=3.0, time_signature=(5, 4)), Rhythm(name=2_3_130ms, n_bars=3.0, time_signature=(5, 4)), Rhythm(name=3_2_130ms, n_bars=3.0, time_signature=(5, 4)), Rhythm(name=2_3_120ms, n_bars=3.0, time_signature=(5, 4)), Rhythm(name=3_2_120ms, n_bars=3.0, time_signature=(5, 4)), Rhythm(name=2_3_110ms, n_bars=3.0, time_signature=(5, 4)), Rhythm(name=3_2_110ms, n_bars=3.0, time_signature=(5, 4)), Rhythm(name=2_3_100ms, n_bars=3.0, time_signature=(5, 4)), Rhythm(name=3_2_100ms, n_bars=3.0, time_signature=(5, 4))]

That looks OK. Now how do we get sound? For that we do three things:

  1. Generate a sound using thebeat.core.SoundStimulus.generate().

  2. Convert the Rhythm to a Sequence.

  3. Combine 1 and 2 in a SoundSequence which we can play.

[38]:
# 1. generate a sound (2640 Hz acc. to the paper, duration wasn't specified)
stim = SoundStimulus.generate(freq=2640, duration_ms=50, offramp_ms=10)

# 2. convert all Rhythms to Sequence. We use a list comprehension here:
set_a_seqs = [rhythm.to_sequence() for rhythm in set_a]

# 3. generate trials. we also copy the name of the Sequence to the SoundSequence.
trials = [SoundSequence(stim, sequence, name=sequence.name) for sequence in set_a_seqs]
[39]:
print(trials)
[SoundSequence(name=2_3_170ms, n_events=6), SoundSequence(name=3_2_170ms, n_events=6), SoundSequence(name=2_3_160ms, n_events=6), SoundSequence(name=3_2_160ms, n_events=6), SoundSequence(name=2_3_150ms, n_events=6), SoundSequence(name=3_2_150ms, n_events=6), SoundSequence(name=2_3_140ms, n_events=6), SoundSequence(name=3_2_140ms, n_events=6), SoundSequence(name=2_3_130ms, n_events=6), SoundSequence(name=3_2_130ms, n_events=6), SoundSequence(name=2_3_120ms, n_events=6), SoundSequence(name=3_2_120ms, n_events=6), SoundSequence(name=2_3_110ms, n_events=6), SoundSequence(name=3_2_110ms, n_events=6), SoundSequence(name=2_3_100ms, n_events=6), SoundSequence(name=3_2_100ms, n_events=6)]

That’s it! We can now plot, play or write all these files to disk. We’ll grab one which we’ll plot and play. How to write all these files to disk you can see in the code block at the bottom of this page.

[40]:
random_trial = trials[0]

random_trial.plot_waveform(figsize=(8, 3));
random_trial.plot_sequence(figsize=(8, 2), dpi=600);
../../_images/examples_creating_stimuli_repplondonkeller_22_0.png
../../_images/examples_creating_stimuli_repplondonkeller_22_1.png

Note

On your computer you can simply do random_trial.play() to listen to the sound. However, for this website we need some different code. So you can ignore the code below.

[41]:
from IPython.display import Audio
Audio(random_trial.samples, rate=random_trial.fs)
[41]:

Writing everything to disk#

To write all these files as wav files to disk, you could do:

for trial in trials:
    trial.write_wav('output_dir')

This would use the names that we gave the trials as the output filename, for instance “2_3_170ms.wav”.

Code summary#

[42]:
from thebeat.music import Rhythm
from thebeat.core import Sequence, SoundStimulus, SoundSequence
import numpy as np

tempi = np.arange(170, 90, -10)

set_a = []

for tempo in tempi:
    set_a.append(Rhythm.from_integer_ratios([2, 3, 2, 3, 2, 3],
                                            beat_ms=tempo,
                                            time_signature=(5, 4),
                                            name=f"2_3_{tempo}ms"))
    set_a.append(Rhythm.from_integer_ratios([3, 2, 3, 2, 3, 2],
                                            beat_ms=tempo,
                                            time_signature=(5, 4),
                                            name=f"3_2_{tempo}ms"))

stim = SoundStimulus.generate(freq=2640, duration_ms=50, offramp_ms=10)

set_a_seqs = [rhythm.to_sequence() for rhythm in set_a]

trials = [SoundSequence(stim, sequence, name=sequence.name) for sequence in set_a_seqs]