Release_Resonant Rain

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

'Resonant Rain' Sonification

:back: Release Workshop

:package: Download Template

Description

This sonification model produces an overall sound texture based on two different but synchronised (time wise) variables. It was created for data centred around zero, i.e. anomaly data. The sound is that of a rain with resonant frequencies, whereby one variable controls the rain texture and the other controls the type of resonances.

Depending on the combination of positive and negative excess of each variable, the following matrix of sound colours appears:

--rain no rain ++rain
++reso dry bright metallic hail bright metallic bright metallic shower
no reso dry 'hail' low intensity light drizzle dense shower
--reso dry tubular hail sonorous tubular dense tubular shower

This is also illustrated in the following figure:

Sound Quadrants

Since the distribution of the anomaly data typically has a exponential decay to each side, the boost factor attempts to 'equalise' or 'linearise' this distribution, as indicated by the arrows in the figure.

Controls

  • Speed (1/s): number of data points (samples) played per second. Due to the granular and reverberant nature of the sound model, this should probably not be larger than 6 to be able to distinguish the data points
  • Rain thresh: the symmetric threshold (plus/minus) beyond with anomalies for the rain texture are taken into consideration
  • Reso thresh: the symmetric threshold (plus/minus) beyond with anomalies for the resonance texture are taken into consideration
  • Rain boost: a factor applied in the normalisation process of the rain variable. Default value is 1. Larger values emphasise the extremes of the rain texture.
  • Reso boost: a factor applied in the normalisation process of the rain variable. Default value is 1. Larger values emphasise the extremes of the resonance texture, i.e. produce stronger resonances even with less pronounced anomalies.

Data Sets

Anomaly data sets such as pr_fut_anom.nc, pr_tas_anom.nc.

Patch

// Version: 24-Nov-15_5

// allow to use this both for
// matrix data and single scalar input
// (for testing)
val MATRIX = true

case class Par(pr: GE, ta: GE, lonVals: GE, lonAxis: GE /* , latVals: GE */)

// ---- user controls ----

val speed   = if (MATRIX) UserValue("Speed [1/s]", 1).kr else DC.kr(1)
val thrPr   = UserValue("Rain thresh" , 1e-5).kr // .atan
val thrTa   = UserValue("Reso thresh", 0.15).kr // .atan

// ---- input data ----
  
val par: Par = if (MATRIX) {

  val vPr     = Var("rain")
  val vTa     = Var("reso")
  
  val dPr     = Dim(vPr, "time")
  val dTa     = Dim(vTa, "time")
  
  // val dimLat  = Dim(vPr, "lat")
  val dimLon  = Dim(vPr, "pan")
  
  val tPr     = dPr.play(speed)
  val tTa     = dTa.play(speed)
  
  Elapsed := tPr
  
  val pr0     = vPr.play(tPr, maxNumChannels = 100)
  val ta0     = vTa.play(tTa, maxNumChannels = 100)
  val lonVals = dimLon.values
  val lonAxis = pr0.axis(dimLon).values(maxSize = 100)
  // val latVals = dimLat.values
  
  val prOk    = CheckBadValues.ar(pr0, post = 0) sig_== 0
  val taOk    = CheckBadValues.ar(ta0, post = 0) sig_== 0
  val pr      = Gate.ar(pr0, prOk)
  val ta      = Gate.ar(ta0, taOk)
  
  Par(pr = pr, ta = ta, lonVals = lonVals, 
      lonAxis = lonAxis /*, latVals = latVals */)
} else {
 
  val pr      = K2A.ar(UserValue("Rain anom", 0.0).kr)
  val ta      = K2A.ar(UserValue("Reso anom", 0.0).kr)
  val lonVals = DC.ar(0)
  // val latVals = DC.ar(0)
  val lonAxis = DC.ar(0)
  
  Par(pr = pr, ta = ta, lonVals = lonVals, 
      lonAxis = lonAxis /* , latVals = latVals */)
}

import par._

// ---- equalize input data ----

val minPr   = (-2.29e-4) // .atan
val maxPr   = (+4.39e-4) // .atan
// val limPr   = math.max(-minPr, maxPr)

val minTa   = (-17.6) // .atan
val maxTa   = (+15.7) // .atan
// val limTa   = math.max(-minTa, maxTa)

val prLin   = pr // .atan
val taLin   = ta // .atan

val minLoPr = minPr 
val maxLoPr = -thrPr
val minHiPr = thrPr
val maxHiPr = maxPr 

val minLoTa = minTa
val maxLoTa = -thrTa
val minHiTa = thrTa
val maxHiTa = maxTa

val boostPr = UserValue("Rain boost", 1.0).kr * 128 // 64
val boostTa = UserValue("Reso boost", 1.0).kr  * 18 // 36 // 24

val prHi    = prLin > minHiPr
val prLo    = prLin < maxLoPr

val taHi    = taLin > minHiTa
val taLo    = taLin < maxLoTa

val prGate  = prHi + prLo
val taGate  = taHi + taLo

def scalePrLo(in: GE) = (in * boostPr / minPr).atan / math.Pi
def scalePrHi(in: GE) = (in * boostPr / maxPr).atan / math.Pi

def scaleTaLo(in: GE) = (in * boostTa / minTa).atan / math.Pi
def scaleTaHi(in: GE) = (in * boostTa / maxTa).atan / math.Pi

val prLoOff   = scalePrLo(maxLoPr)
val prHiOff   = scalePrHi(minHiPr)

val taLoOff   = scaleTaLo(maxLoTa)
val taHiOff   = scaleTaHi(minHiTa)

val prLoMul   = (scalePrLo(minPr) - prLoOff).reciprocal
val prHiMul   = (scalePrHi(maxPr) - prHiOff).reciprocal

val taLoMul   = (scaleTaLo(minTa) - taLoOff).reciprocal
val taHiMul   = (scaleTaHi(maxTa) - taHiOff).reciprocal

if (false) {  // inspect scaling factors
  prLoOff.poll(0, "prLoOff")
  prLoMul.poll(0, "prLoMul")
  prHiOff.poll(0, "prHiOff")
  prHiMul.poll(0, "prHiMul")
}

def mkPrLo(in: GE) = ((scalePrLo(in) - prLoOff) * prLoMul).max(0)
def mkPrHi(in: GE) = ((scalePrHi(in) - prHiOff) * prHiMul).max(0)

def mkTaLo(in: GE) = ((scaleTaLo(in) - taLoOff) * taLoMul).max(0)
def mkTaHi(in: GE) = ((scaleTaHi(in) - taHiOff) * taHiMul).max(0)

val prLoNorm  = mkPrLo(prLin) // .clip(-1, 1)
val prHiNorm  = mkPrHi(prLin) // .clip(-1, 1)

val taLoNorm  = mkTaLo(taLin)
val taHiNorm  = mkTaHi(taLin)

if (false) {  // verify extreme values
  mkPrLo(minPr) .poll(0, "min-pr")
  mkPrLo(-thrPr).poll(0, "-thrPr")
  mkPrHi(maxPr) .poll(0, "max-pr")
  mkPrHi( thrPr).poll(0, "+thrPr")
  
  mkTaLo(minTa) .poll(0, "min-ta")
  mkTaLo(-thrTa).poll(0, "-thrTa")
  mkTaHi(maxTa) .poll(0, "max-ta")
  mkTaHi( thrTa).poll(0, "+thrTa")
}

if (false) {  // verify scaled data
  val poll = Impulse.ar(speed)
  prLoNorm.poll(poll * prLo, "pr-lo")
  prHiNorm.poll(poll * prHi, "pr-hi")
  taLoNorm.poll(poll * taLo, "ta-lo")
  taHiNorm.poll(poll * taHi, "ta-hi")
}

if (false) {
  prLoNorm.poll(prLoNorm < 0 | prLoNorm > 1, "prLoNorm OUT")
  prHiNorm.poll(prHiNorm < 0 | prHiNorm > 1, "prHiNorm OUT")
}

if (false) {  // verify single scaled data
  val poll = Impulse.ar(speed)
  val prSingle = prHiNorm - prLoNorm
  prLoNorm.poll(prSingle < -1 | prSingle > 1, "prLoNorm OUT")
  prHiNorm.poll(prSingle < -1 | prSingle > 1, "prHiNorm OUT")
  val taSingle = taHiNorm - taLoNorm
  taLoNorm.poll(taSingle < -1 | taSingle > 1, "taLoNorm OUT")
  taHiNorm.poll(taSingle < -1 | taSingle > 1, "taHiNorm OUT")
  
  prLin   .poll(poll, "prLin")
  prLoNorm.poll(poll, "prLoNorm")
  prHiNorm.poll(poll, "prHiNorm")
  prSingle.poll(poll, "prSingle")
}

// ---- dust generator ----

// where lo and hi are zero for
// no anomly and go towards one
// for maximal extremes
def dustFreq(lo: GE, hi: GE): GE = {
  // val single = hi - lo // -1 ... +1
  // single.linlin(-1, 1, 0, 80)
  val max = lo max hi
  max.linexp(0, 1, 10, 90)
}

val dust0   = Dust2.ar(dustFreq(prLoNorm, prHiNorm))
val dust    = LPF.ar(dust0, 8000 /* 7000 */)

// ---- noise generator ----

val noiseLvl= 0  // XXX TODO
val noise   = PinkNoise.ar(noiseLvl)

// ---- pr: sum and filter ----

val sumFilter: GE = {
  val gate1  = HPF.ar(noise + dust, 300 /* 400 */)
  val hpf    = HPF.ar(gate1, 3000)
  val gate2  = LinXFade2.ar(hpf, gate1, pan = Lag.kr(prGate * 2 - 1, 0.2))
  val lpf    = LPF.ar(gate2, 3000 /* 1000 */)
  val lpfPan = Lag.kr((1 - prLo) * 2 - 1, 0.2)
  val res    = LinXFade2.ar(lpf, gate2, pan = lpfPan)
  res
}

// ---- ta: resonances ----

val panIn: GE = {
  val in = sumFilter
  val f1    = 100.0
  val f2    = 400.0
  val t1    = f1.reciprocal
  val t2    = f2.reciprocal
  val tm    = t1 max t2
  val d1    = 0.75
  val d2    = 0.1
  val d0    = 0.0125 / 2
  
  val n0    = 0.0001
  val n1    = 0.001
  val n     = taLoNorm.linexp(0, 1, n0, n1) - n0
  
  val noise = PinkNoise.ar(n)
  val time  = Select.ar(taHi, Seq(t1, t2))
  val decay = (taLoNorm.linexp /* lin */(0, 1, d0, d1) +
               taHiNorm.linexp(0, 1, d0, d2)) * taGate
  
  val gain = taLoNorm * 1.3 + 1
  
  CombN.ar(in, maxDelayTime = tm, delayTime = noise + time, 
    decayTime = decay) * gain
}

// ---- pan ----

val minLon  = Reduce.min(lonVals) // currently no other means
val maxLon  = Reduce.max(lonVals) // ...to get these values

if (false) {
  minLon.poll(0, "minLon")
  maxLon.poll(0, "maxLon")
  NumChannels(lonVals).poll(0, "lon-size")
}

val isMono = (maxLon - minLon).signum
val panPos:GE = if (!MATRIX) 0.0 else {
  val scale = lonAxis.linlin(minLon, maxLon, -1, 1)
  // if isMono, scale is NaN!
  Select.ar(isMono, Seq[GE](0.0, scale))
}
  
val pan     = Pan2.ar(panIn, panPos)
val panL    = pan \ 0
val panR    = pan \ 1

// ---- reverb ----

def earlyLvl(lo: GE, hi: GE): GE = {
  val single = hi - lo // -1 ... +1
  single.linlin(-1, 1, 0.3, 0.7)
}

def tailLvl(lo: GE, hi: GE): GE = {
  val single = hi - lo // -1 ... +1
  single.linlin(-1, 1, 0.0, 0.3).squared
}

def dryLvl(lo: GE, hi: GE): GE = {
  val single = hi - lo // -1 ... +1
  single.linlin(-1, 1, 0.8, 0.0).squared
}

def mkVerbChan(chanIn: GE, post: Boolean = false): GE = {
  val el = earlyLvl(prLoNorm, prHiNorm)
  val tl = tailLvl (prLoNorm, prHiNorm)
  val dl = dryLvl  (prLoNorm, prHiNorm)
  
  // what we'll do is avoid having
  // dozens of reverb generators.
  // instead we have (per channel)
  // one early and one late generator,
  // and the levels are taking by
  // pre-scaling the input.
  
  if (post) {
    el.poll(1, "ely")
    tl.poll(1, "tal")
    dl.poll(1, "dry")
  }
  
  val earlyIn = Mix(chanIn * el)
  val tailIn  = Mix(chanIn * tl)
  val dry     = Mix(chanIn * dl)
  
  val ev = GVerb.ar(earlyIn, 
    roomSize      = 250, 
    revTime       = 100, 
    damping       = 0.75, // 0.25,
    dryLevel      = 0.0,
    earlyRefLevel = 1.0,
    tailLevel     = 0.0
  )
  val tv = GVerb.ar(tailIn, 
    roomSize      = 250, 
    revTime       = 100, 
    damping       = 0.75, // 0.25,
    dryLevel      = 0.0,
    earlyRefLevel = 0.0,
    tailLevel     = 1.0
  )
  (ev + tl + dry).tanh
}

val verbL   = mkVerbChan(panL, post = false)
val verbR   = mkVerbChan(panR)
val verb0   = verbL * Seq( 0.0.dbamp, -6.0.dbamp) + 
              verbR * Seq(-6.0.dbamp,  0.0.dbamp)
// val verb0   = Seq(verbL \ 0, verbR \ 1): GE
val verb    = verb0.tanh

// ---- output ----

val numData = NumChannels(pr)
if (MATRIX) {
  numData.poll(0, "matrix-size")
  // lonVals.poll(0, "longitudes")
  // latVals.poll(0, "latitudes")
}

val sig     = verb / numData.sqrt
val amp     = -6.0.dbamp

output := sig * amp
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.