Release_Pitch Layers

Hanns Holger Rutz edited this page Nov 26, 2015 · 9 revisions

'Pitch Layers' Sonification

:back: Release Workshop

:package: Download Template


This sonification targets anomaly data with at least two dimensions:

  • the time dimension is used to unroll the data in time during playback
  • the layer dimension is used to assign pitches to the data

The variable is considered symmetrical around a threshold, for example zero for anomaly data. The variable's magnitude is used to define the amplitude (volume) of each sound component. Sound components are sine oscillators whose frequency depends on the signum of the variable and the layer.

  • if the data is above the threshold, the frequencies are two octaves above the data that falls below the threshold.
  • within these two frequency offsets, the layer axis is mapped to a defined pitch-range (by default one octave) and forced onto a semi-tone grid, where the highest layer is assigned the lowest pitch and the lowest layer is assigned the highest pitch. This is so that if pressure-levels are used for the layer dimension, low altitudes correspond to low pitches and high altitudes correspond to high pitches.

In particular, the quasi-biannual-oscillation can be made audible, appearing as downward glissandi in alternating registers.

Missing values (NaN) in the data are heard as a low volume white noise.


For three dimensional data, the variant -pan may be used. This allows to assign the third dimension to a stereo panorama position. The input data will be automatically spread from left (minimum in the pan-dimension) to right (maximum in the pan-dimension).

To better understand the sound model, the variant -calib is provided. Instead of a matrix input, a user value number box is provided to enter the data value, a virtual layer position and the minimum and maximum layer values across which the pitch range is spread.


  • Speed (1/s): tempo for advancing in time. Data is linearly interpolated in time.
  • Threshold in var: The magnitude in the data that separates the high and the low register. For anomaly data this would typically be zero.
  • Max in var: the magnitude corresponding to the highest pitch within each range. For example, if data is temperature anomaly, one may except a maximum magnitude of (plus/minus) 14 degrees celsius. In order to hear stronger sounds, one may choose a lower maximum such as 4 degrees celsius, so any anomaly exceeding plus/minus 4 degrees corresponds to the loudest sound component.
  • Pitch range (semi-tones): The number of semitones between the highest and lowest layer of the data. 12 semitones means the layers occupy the space of one octave.
  • Layer mode (0=lin,1=exp): This specifies whether the layer dimension is exponentially spread (use value 1), as is the case for pressure-levels in Pa, or linearly (use value 0). It affects the mapping to the pitch scale.

Furthermore, a short noise burst is played to indicate the tempo of the data. Typically this would be adjusted to sound at the beginning of each new year.

  • Values per tick: For example with a time resolution of 1 sample per month, a value of 12 gives tick markers every year. If the time resolution is 1 sample per day, a value of 365.2422 gives year markers.
  • Tick ampliude (0...1): This controls the volume of the tick burst from silent (0) to maximally loud (1)

Data Sets

The sonification has been developed using this data-set:


We use the temp_anom variable for temperature anomalies which demonstrates the quasi-biannual-oscillation (QBO) phenomenon. This occurs in the plev range from 10 hPa to 100 hPa around equatorial latitudes (e.g. +- 5 degrees north/south). One would thus normally index the latitude dimension and slice the pressure-levels dimensions accordingly.

It is also possible to use other anomaly data.


// Version: 25-Nov-15_5

// if `false`, exchange matrix data for single scalar input
val MATRIX  = true

// if `true`, request a dimension for panorama
val PAN     = true

case class Par(vp: GE, ok: GE, timeP: GE, layAxis: GE, 
               layLo: GE, layHi: GE, panPos: GE)

lazy val tempo          = UserValue("Speed [1/s]", 1).kr
lazy val sideThresh     = UserValue("Threshold in var", 0.0).kr
lazy val maxValue       = UserValue("Max in var", 4.0).kr
lazy val pitchRange     = UserValue("Pitch-range [semi-tones]", 12).kr
lazy val layMode        = UserValue("Layer mode [0=lin,1=exp]", 1).kr
lazy val valuesPerTick  = UserValue("Values per tick", 12).kr
lazy val tickAmp        = UserValue("Tick amplitude [0...1]", 0.1).kr

// force lazy
def userValues() = Seq(tempo, sideThresh, maxValue, pitchRange,
                       layMode, valuesPerTick, tickAmp)

lazy val trig =  // time-synchronous trigger

val par: Par = if (MATRIX) {
  val v       = Var("var")
  val dimLay  = Dim(v, "layer")
  val dimTime = Dim(v, "time" )
  val timeP   =
  val vp0     =, maxNumChannels = 100, interp = 2)
  val ok      =, post = 0) sig_== 0
  val vp      =, ok)
  Elapsed := timeP
  val layVals   = dimLay.values
  val layAxis   = vp0.axis(dimLay).values
  val layLo     = Reduce.min(layVals)
  val layHi     = Reduce.max(layVals)

  val panPos:GE = if (!PAN) 0.0 else {
    val dimPan  = Dim(v, "pan")
    val panVals = dimPan.values
    val panLo   = Reduce.min(panVals)
    val panHi   = Reduce.max(panVals)
    val panAxis = vp0.axis(dimPan).values
    val hasPan  = (panHi - panLo).signum
    val panScale= panAxis.linlin(panLo, panHi, -1, 1)
    // if !hasPan, scale is NaN!, Seq[GE](0.0, panScale))
  Par(vp = vp, ok = ok, timeP = timeP, layAxis = layAxis, 
      layLo = layLo, layHi = layHi, panPos = panPos)

} else {
  val vp      ="var"       ,     0.0).kr)
  val layAxis0="layer"     ,  5000.0).kr)
  val layLo   ="layer-low" ,  1000.0).kr)
  val layHi   ="layer-high", 10000.0).kr)
  userValues() // _after_ the manual variable fields
  val layAxis = layAxis0.clip(layLo, layHi) 
  val timeP = = trig, lo = 0, hi = 1000000000, step = 1)
  Par(vp = vp, ok = 1, timeP = timeP, layAxis = layAxis, 
      layLo = layLo, layHi = layHi, panPos = 0.0)

import par._

// val tick    = (timeP % valuesPerTick) sig_== 0
val tick    = % valuesPerTick) < 0
timeP.poll(1, "time")

val anom    = vp  // data is assumed to be anomaly already

// pressure level range:
// 100 hPa ... 10 hPa
// where 10 hPa highest pitch
//      100 hPa lowest  pitch
// values are roughly logarithmically spaced

val side        = anom > sideThresh  // too low = 0, too high = 1
val octave      = side * 2

val pcExp       = layAxis.explin(layLo, layHi, pitchRange, 0)
val pcLin       = layAxis.linlin(layLo, layHi, pitchRange, 0)
val pcSel       =, Seq(pcLin, pcExp)).roundTo(1)
val pcOk        =, post = 0) sig_== 0
val pitchClass  =, pcOk) 

val pitch       = octave * 12 + 60 + pitchClass

// --- debug printing ---
val PRINT = false

if (PRINT) {
  anom .poll(trig, "anom")
  pitch.poll(trig, "pch ")

val freq        = pitch.clip(12, 135).midicps
val ampCorr     =
val amp         = (anom / maxValue).abs.min(1.0) * ampCorr

val gain        = -3.0.dbamp / NumChannels(vp).sqrt

val osc         =, panPos) * amp
val fill        =
val sig         = (Mix(osc) * ok + fill * (1 - ok)) * gain

val tickTrig    =, dur =
val tickSig     =, 0.1).min(1) *

val outSig      = sig +

output := outSig
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.