at.iem.sysson.binaural.Renderer.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of sysson_2.11 Show documentation
Show all versions of sysson_2.11 Show documentation
Sonification platform of the IEM SysSon project
The newest version!
/*
* Renderer.scala
* (SysSon)
*
* Copyright (c) 2013-2017 Institute of Electronic Music and Acoustics, Graz.
* Copyright (c) 2014-2019 Hanns Holger Rutz. All rights reserved.
*
* This software is published under the GNU General Public License v3+
*
*
* For further information, please contact Hanns Holger Rutz at
* [email protected]
*/
package at.iem.sysson.binaural
import de.sciss.lucre.synth.{Bus, Group, BusNodeSetter, AudioBus, Node, Buffer, Synth, Txn}
import de.sciss.mellite.Prefs
import de.sciss.synth.io.AudioFile
import de.sciss.{synth, numbers}
import de.sciss.synth.{addBefore, ControlSet, addToTail, addToHead, AddAction, SynthGraph, message}
import de.sciss.file._
import scala.collection.immutable.{IndexedSeq => Vec}
object Renderer {
var DEBUG = false
final case class Person(pos: Point2D, azi: Radians, id: Int)
// - there are 360 / 15 = 24 azimuth samples
// - there are 90 / 15 = 6 elevation samples (downwards elevation not needed)
// - not all elevation samples are taken for any azimuth
// -
final case class IR(t: Int, p: Int) {
require(t % 15 == 0 && t >= 0 && t < 360)
require(p % 15 == 0 && ((p >= 0 && p <= 90) || (p >= 315 && p < 360)))
def toPolar: Polar = {
val lat = if (p < 90) p else p - 360
val lon = t
val theta = (90 - lat).toRadians
val phi = ((lon + 360) % 360).toRadians
Polar(theta, phi)
}
def toCartesian: Point3D = toPolar.toCartesian
def file(directory: File, id: Int = 1004): File =
directory / s"IRC_${id}_C" / f"IRC_${id}_C_R0195_T$t%03d_P$p%03d.wav"
override def toString = f"T$t%03d_P$p%03d"
}
// final val Samples = Vector[IR](
// IR(000, 000), IR(000, 015), IR(000, 030), IR(000, 045), IR(000, 060), IR(000, 075), IR(000, 090),
// IR(015, 000), IR(015, 015), IR(015, 030), IR(015, 045),
// IR(030, 000), IR(030, 015), IR(030, 030), IR(030, 045), IR(030, 060),
// IR(045, 000), IR(045, 015), IR(045, 030), IR(045, 045),
// IR(060, 000), IR(060, 015), IR(060, 030), IR(060, 045), IR(060, 060), IR(060, 075),
// IR(075, 000), IR(075, 015), IR(075, 030), IR(075, 045),
// IR(090, 000), IR(090, 015), IR(090, 030), IR(090, 045), IR(090, 060),
// IR(105, 000), IR(105, 015), IR(105, 030), IR(105, 045),
// IR(120, 000), IR(120, 015), IR(120, 030), IR(120, 045), IR(120, 060), IR(120, 075),
// ...
// )
final val Samples: Vec[IR] = (0 until 360 by 15).flatMap { t =>
val pm = if (t % 360 == 0) 90 else if (t % 60 == 0) 75 else if (t % 30 == 0) 60 else 45
(0 to pm by 15).map { p =>
IR(t, p)
}
}
final val SamplePoints: Vec[(Point3D, Int)] = Samples.map(_.toCartesian).zipWithIndex
final case class Position(index: Int, distance: Double) {
/** Acoustical delay in seconds, based on `distance`
* and a speed of sound of 342 m/s
*/
def delay: Double = distance / 342
/** Amplitude factor (less than 1) in Forum Stadtpark, based on `distance` and
* a distance of 1.5 meters corresponding to a factor of 1.0
*/
def attenuation: Double = {
import numbers.Implicits._
val d = distance.clip(1.5, 5.8)
val decibels = 1.1034 * d.squared - 12.6433 * d + 14.3775
(decibels + 1.2).dbAmp
}
override def toString = f"Position(${Samples(index)}, distance = $distance%1.2f meters"
}
/** Calculates the closest HRIR sample index and distance of a person with respect
* to a given speaker
*
* @param listener position and orientation of listener
* @param q speaker position
* @return a pair of HRIR sample index and distance in meters
*/
def calc(listener: Person, q: Point2D, metersPerPixel: Double): Position = {
// val q = Turbulence.ChannelToMatrixMap(spk).toPoint.equalize
val p = listener.pos // .equalize
val azi0 = p angleTo q
val azi = azi0 - listener.azi
val dh = (p distanceTo q) * metersPerPixel
val dv = 1.5 // ja?
val dist = math.sqrt(dh * dh + dv * dv)
val ele = math.atan2(dv, dh)
// val ll = LatLon(lat = ele.toDegrees, lon = azi.value.toDegrees)
val lat = ele.toDegrees
val lon = azi.value.toDegrees
val theta = (90 - lat).toRadians
val phi = ((lon + 360) % 360).toRadians
val ll = Polar(theta, phi)
val r = ll.toCartesian
val idx = SamplePoints.minBy(_._1 distanceTo r)._2
Position(index = idx, distance = dist)
}
case class PreparePartConv(sourceBuf: Buffer, fftSize: Int)
extends message.BufferGen.Command {
def name: String = "PreparePartConv"
def isSynchronous: Boolean = true // !
def args: Seq[Any] = Seq(sourceBuf.id, fftSize)
}
private def mkTail(listener: Person, target: Node, addAction: AddAction,
delayBus: AudioBus, stereoBus: AudioBus, verbFile: File,
inBus: AudioBus, speakers: Vec[Point2D], metersPerPixel: Double)(implicit tx: Txn): Synth = {
// import Turbulence.{ChannelIndices, Channels, audioWork, NumChannels => N}
// require(delayBus .numChannels == N)
require(delayBus.numChannels == inBus.numChannels)
require(stereoBus.numChannels == 2)
val irSpec = AudioFile.readSpec(verbFile)
val irSize = irSpec.numFrames.toInt
val fftSize = 2048
val numPart = (irSize * 2.0 / fftSize).ceil.toInt // 49
val partSize = fftSize * numPart // 100352
val s = target.server
val partBufL = Buffer(s)(numFrames = partSize)
val partBufR = Buffer(s)(numFrames = partSize)
val fullBufL = Buffer(s)(numFrames = irSize )
val fullBufR = Buffer(s)(numFrames = irSize )
fullBufL.readChannel(verbFile.absolutePath, channels = 0 :: Nil)
fullBufR.readChannel(verbFile.absolutePath, channels = 1 :: Nil)
// currently no predefined method for this command!
tx.addMessage(partBufL, message.BufferGen(partBufL.id, PreparePartConv(fullBufL, fftSize)),
dependencies = fullBufL :: Nil)
tx.addMessage(partBufR, message.BufferGen(partBufR.id, PreparePartConv(fullBufR, fftSize)),
dependencies = fullBufL :: Nil)
// fullBufL.dispose()
// fullBufR.dispose()
val N = inBus.numChannels
val tailGraph = SynthGraph {
import synth._
import ugen._
import Ops.stringToControl
// val in = In.ar(ChannelIndices)
val in = In.ar("in".kr, N)
val inF = Flatten(in)
val dlyT = "delay".ir(Vec.fill(N)(0f))
val amp = "amp" .kr(Vec.fill(N)(0f))
val inA = DelayN.ar(inF, dlyT, dlyT) * amp
val mix = Mix(inA)
// RunningSum.ar(mix).poll(1, "sum")
val bufL = "bufL".ir
val bufR = "bufR".ir
val convL = PartConv.ar(mix, fftSize, bufL)
val convR = PartConv.ar(mix, fftSize, bufR)
// RunningSum.ar(convL).poll(1, "conv")
val outR = "reverb-out".kr
val outD = "delay-out".kr
// out.poll(0, "out")
Out.ar(outD, inA)
// todo - correct delay (PartConv versus Convolution2 / binaural-kernel)
Out.ar(outR, Seq(convL, convR))
}
// val pos = Channels.map { spk => calc(listener, spk) }
val pos = speakers.map { spk => calc(listener, spk, metersPerPixel = metersPerPixel) }
val dlySet: ControlSet = "delay" -> pos.map(_.delay .toFloat)
val attSet: ControlSet = "amp" -> pos.map(_.attenuation.toFloat)
val res = Synth(s, tailGraph, Some("reverb-tail"))
val reverbBusW = BusNodeSetter.writer("reverb-out", stereoBus, res)
val delayBusW = BusNodeSetter.writer("delay-out" , delayBus , res)
val args = dlySet :: attSet :: List[ControlSet]("bufL" -> partBufL.id, "bufR" -> partBufR.id)
res.play(target = target, args = args,
addAction = addAction, dependencies = partBufL :: partBufR :: Nil)
reverbBusW.add()
delayBusW .add()
res.onEndTxn { implicit tx =>
reverbBusW.remove()
delayBusW .remove()
fullBufL .dispose()
fullBufR .dispose()
partBufL .dispose()
partBufR .dispose()
}
res
}
def build(target: Node, addAction: AddAction, listener: Person, inBus: AudioBus, speakers: Vec[Point2D],
verbFile: File, metersPerPixel: Double, irDirectory: File)
(implicit tx: Txn): Group = {
// import Turbulence.{NumChannels => N}
val N = inBus.numChannels
val s = target.server
val g = Group.play(target, addAction)
val stereoBus = Bus.audio(s, 2)
val delayBus = Bus.audio(s, N)
mkTail(listener, g, addToHead, delayBus = delayBus, stereoBus = stereoBus, inBus = inBus, speakers = speakers,
verbFile = verbFile, metersPerPixel = metersPerPixel)
val rplcGraph = SynthGraph {
import synth._
import ugen._
import Ops.stringToControl
val in = "in".kr
val sig = In.ar(in, 2)
// Mix(sig).poll(1, "route")
// ReplaceOut.ar(0, sig)
val asr = Env.asr(attack = 0.1, release = 0.1, curve = Curve.lin)
val fade = EnvGen.kr(asr, gate = "gate".kr(1f), doneAction = freeGroup)
val out = "out".kr
XOut.ar(out, sig, fade)
}
val rplcSynth = Synth(s, rplcGraph, Some("binaural-mix"))
val headphones = Prefs.headphonesBus.getOrElse(Prefs.defaultHeadphonesBus)
rplcSynth.play(g, List("out" -> headphones), addToTail, Nil)
val stereoBusR = BusNodeSetter.reader("in", stereoBus, rplcSynth)
stereoBusR.add()
rplcSynth.onEndTxn { implicit tx =>
stereoBusR.remove()
}
var binBufs = Map.empty[Int, (Buffer, Buffer)]
lazy val chanGraph = SynthGraph {
import synth._
import ugen._
import Ops.stringToControl
val in = "in".kr
val ch = "chan".kr
val inSig = Select.ar(ch, In.ar(in, N)) // todo - not nice. this is simply because delayBus is multi-chan
val bufL = "bufL".ir
val bufR = "bufR".ir
val convL = Convolution2.ar(inSig, bufL, frameSize = 512)
val convR = Convolution2.ar(inSig, bufR, frameSize = 512)
val outSig: GE = Seq(convL, convR)
val out = "out".kr
Out.ar(out, outSig)
}
speakers.zipWithIndex.foreach { case (spk, offset) =>
val pos = calc(listener, spk, metersPerPixel = metersPerPixel)
if (DEBUG) println(s"$spk - $pos")
if (pos.distance < 6) { // use binaural for less than 6 meters distance
val chanSynth = Synth(s, chanGraph, Some("chan-bin"))
val (bufL, bufR) = binBufs.getOrElse(pos.index, {
val ir = Samples(pos.index)
val path = ir.file(directory = irDirectory, id = listener.id).absolutePath
val _bufL = Buffer(s)(numFrames = 512)
val _bufR = Buffer(s)(numFrames = 512)
_bufL.readChannel(path, 0 :: Nil)
_bufR.readChannel(path, 1 :: Nil)
val tup = (_bufL, _bufR)
binBufs += pos.index -> tup
tup
})
val args: List[ControlSet] = List("chan" -> offset, "bufL" -> bufL.id, "bufR" -> bufR.id)
chanSynth.play(rplcSynth, args, addBefore, bufL :: bufR :: Nil)
val stereoBusW = BusNodeSetter.writer("out", stereoBus, chanSynth)
val delayBusR = BusNodeSetter.reader("in" , delayBus , chanSynth)
stereoBusW.add()
delayBusR .add()
chanSynth.onEndTxn { implicit tx =>
stereoBusW.remove()
delayBusR .remove()
}
} else {
// ignore - just use the reverb tail and we're fine
}
}
if (DEBUG) println(s"Number of binaural filters: ${binBufs.size}")
g.onEndTxn { implicit tx =>
binBufs.valuesIterator.foreach { case (bufL, bufR) =>
bufL.dispose()
bufR.dispose()
}
}
g
}
}