This page was generated from notebooks/mesonic-pmson.ipynb.

Parameter Mapping Sonification using mesonic#

Let’s see how we can use the Synth concept for Parameter Mapping Sonification (PMson) and use the Playback filtering feature to explore our PMson.

It is recommended to use headphones for the examples as they use panning.

[1]:
import mesonic

Preparation of Synths#

Let’s start by preparing our Context and Synths.

[2]:
context = mesonic.create_context()
Starting sclang process... Done.
Registering OSC /return callback in sclang... Done.
Loading default sc3nb SynthDefs... Done.
Booting SuperCollider Server... Done.

Let’s create two Synths

  • A discrete (s1) immutable Synth named s1i

  • and a continuous (s2) mutable Synth named s2

[3]:
s1i = context.synths.create("s1", mutable=False)
s2 = context.synths.create("s2", track=1)

Data Preparation#

We also prepare the data for the sonification.

We will use the Palmer penguins dataset for the examples.

[4]:
import seaborn as sns
[5]:
df = sns.load_dataset("penguins")
df = df.dropna(subset=["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g"])
df
[5]:
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 Male
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 Female
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 Female
4 Adelie Torgersen 36.7 19.3 193.0 3450.0 Female
5 Adelie Torgersen 39.3 20.6 190.0 3650.0 Male
... ... ... ... ... ... ... ...
338 Gentoo Biscoe 47.2 13.7 214.0 4925.0 Female
340 Gentoo Biscoe 46.8 14.3 215.0 4850.0 Female
341 Gentoo Biscoe 50.4 15.7 222.0 5750.0 Male
342 Gentoo Biscoe 45.2 14.8 212.0 5200.0 Female
343 Gentoo Biscoe 49.9 16.1 213.0 5400.0 Male

342 rows × 7 columns

[6]:
sns.pairplot(data=df, hue="species");
../_images/notebooks_mesonic-pmson_12_0.png

Mapping Preparation#

We use the mapping functions from pyamapping

[7]:
from pyamapping import linlin, midi_to_cps

And we also define a small helper class that allows us to map the values linearly

[8]:
class DFMapper:
    # We define a mapping helper class that gets a dataframe
    # and allows to provide a index and column of a value in the dataframe
    # to map the corresponding value from the source (smin,smax)
    # to the target (tmin,tmax) linearly.
    # If the source range is None we get the min / max of the column

    def __init__(self, df):
        self.df = df

    def linear(self, idx, col, tmin, tmax, smin=None, smax=None):
        if smin is None:
            smin = getattr(self.df, col).min()
        if smax is None:
            smax = getattr(self.df, col).max()
        # get the value using the idx and column name
        value = getattr(self.df, col)[idx]
        return linlin(value, smin, smax, tmin, tmax)

Create a instance for later usage

[9]:
m = DFMapper(df)

Example of a discrete PMSon#

For a discrete PMSon a immutable Synth is often very practical as we do not need to worry about

  • overlapping Synth playbacks and

  • the Synth is expected to end itself.

Let’s create the PMSon

[10]:
context.reset()

# we iterate over the penguins dataset
for idx in df.index:
    # longer flipper -> later onset
    onset = m.linear(idx, "flipper_length_mm", 0, 5)
    # use the context manager to provide time and add the species as info.
    with context.at(onset, info={"species": df.loc[idx, "species"]}):
        s1i.start(
            # heavier -> lower frequency
            freq = midi_to_cps(m.linear(idx, "body_mass_g", 96, 50)),
            # longer bill -> longer sound
            dur = m.linear(idx, "bill_length_mm", 0, 0.3),
            # pan the sounds according to sex
            pan = {"Female": 1, "Male": -1}.get(df.loc[idx, "sex"], 0),
            amp = 0.15
        )

dpmson = context.timeline.to_dict()  # we can save the times with the Events as dict and insert them later again
[11]:
context.timeline.plot()
context.timeline
[11]:
Timeline(0.0-6.0 (6.0) #entries=55)
../_images/notebooks_mesonic-pmson_23_1.png
[12]:
context.playback.start()

Example of a continuous PMSon#

For a continuous PMSon a mutable Synths is the obvious choice.

  • The Synth can be changed by setting the Parameters or using Synth.set and

  • it allows stopping the Synth playback in a controlled way and thus allows using continuous (not self ending) Synths

[13]:
context.reset()

# the continuous Synth must be started at the beginning
with context.at(0):
    s2.start()


# we use a similiar mapping here but set the Parameters
# of the continuous Synth instead of starting the Synth.
for idx in df.index:
    onset = m.linear(idx, "flipper_length_mm", 0, 5)
    with context.at(onset, info={"species": df.loc[idx, "species"]}) as tp:
        s2.freq = midi_to_cps(m.linear(idx, "body_mass_g", 96, 50))
        s2.amp = m.linear(idx, "bill_length_mm", 0, 0.2)
        s2.pan = {"Female": 1, "Male": -1}.get(df.loc[idx, "sex"], 0)


# and also don't forget to stop the continuous Synth
with context.at(context.timeline.last_timestamp + 0.1):
    s2.stop()

cpmson = context.timeline.to_dict()
[14]:
context.timeline.plot()
context.timeline
[14]:
Timeline(0-6.1 (6.1) #entries=56)
../_images/notebooks_mesonic-pmson_27_1.png
[15]:
context.playback.start()

Example of a mixed PMSon#

To mix the both PMSon types we can simply combine the two code snippets from above.

[16]:
context.reset()
# Here we simply extend the timeline with the stored Timelines.
context.timeline.extend(dpmson)
context.timeline.extend(cpmson)

The Timeline is now updated accordingly.

[17]:
context.timeline.plot()
context.timeline
[17]:
Timeline(0.0-6.1 (6.1) #entries=56)
../_images/notebooks_mesonic-pmson_33_1.png

Note that only one more entry now exists as we use the same onset as in the discrete mapping.

We can now hear both Synths playing.

[18]:
context.playback.start()
  • Using the processor we can select the single tracks.

  • We used the tracks here to divide the different PMSon types.

  • so if we select just one track we can listen to the different PMSon types separately

[19]:
context.processor.selected_tracks = [1]
[20]:
context.playback.start()

You can adjust this while the Playback is running.

[21]:
context.processor.selected_tracks = [0, 1]

However this can quickly lead to inconsistencies in the timeline

  • a continuous Synths might not be stopped or started which will then result in errors in the backend.

[22]:
context.processor.selected_tracks = [0]

A Context.stop will stop all Playbacks created by the Context aswell as trigger Backend.stop of the backend that is used. This should stop all Events from being executed and also stop all sounds that have become stuck in the backend.

[23]:
context.stop()

Filtering and Transforming the Sonification#

The Playback can be filtered by tracks as shown above.

Another way to filter the Playback is to add a event_filter to BundleProcessor.

Let’s do it with the discrete PMSon

[24]:
context.reset()
context.timeline.extend(dpmson)
context.timeline.plot()
../_images/notebooks_mesonic-pmson_48_0.png

We can filter f.e by species

[25]:
df.species.unique()
[25]:
array(['Adelie', 'Chinstrap', 'Gentoo'], dtype=object)
[26]:
def create_filter(selected_species):
    def filter_fun(event):
        if event.info.get("species", None) == selected_species:
            return event
        else:
            return None
    return filter_fun

This allows us to listen just to the single species.

[27]:
filter_adelie = create_filter("Adelie")
context.processor.event_filters.append(filter_adelie)
[28]:
context.playback.start()

The filter can be removed using the following code

[29]:
context.processor.event_filters.remove(filter_adelie)

Let’s try filtering the Chinstrap penguins

[30]:
filter_chinstrap = create_filter("Chinstrap")
context.processor.event_filters.append(filter_chinstrap)
[31]:
context.playback.start()
[32]:
context.playback.processor.event_filters.remove(filter_chinstrap)

The Gentoo penguins are the ones with the greatest body mass and the longest flippers. Therefore they will only sound quite late in the timeline.

[33]:
filter_gentoo = create_filter("Gentoo")
context.processor.event_filters.append(filter_gentoo)
[34]:
context.playback.start()

All filters can be removed by resetting the filter list

[35]:
context.processor.event_filters = []

It is also possible to use the a filter to transform the Events.

Here we create a slider that allows us to change the panning of the events during Playback.

[36]:
import ipywidgets as widgets
[37]:
pan_slider = widgets.FloatSlider(value=0, min=-1, max=1)
[38]:
pan_slider
[38]:
[39]:
pan_slider.value
[39]:
1.0
[40]:
def transform_pan(event):
    # start events
    if event.data.get("pan", None) != None:
        event.data["pan"] = pan_slider.value
    # set events
    if event.data.get("name", None) == "pan":
        print("changed set")
        event.data["new_value"] = pan_slider.value
    return event

context.processor.event_filters = []
context.processor.event_filters.append(transform_pan)
[41]:
context.playback.start()
[42]:
# set the Event processing back to normal.
context.playback.processor.event_filters = []
[43]:
context.close()
Quitting SCServer... Done.
Exiting sclang... Done.
[ ]: