All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.sciss.fscape.stream.OffsetOverlapAdd.scala Maven / Gradle / Ivy

/*
 *  OffsetOverlapAdd.scala
 *  (FScape)
 *
 *  Copyright (c) 2001-2016 Hanns Holger Rutz. All rights reserved.
 *
 *  This software is published under the GNU General Public License v2+
 *
 *
 *  For further information, please contact Hanns Holger Rutz at
 *  [email protected]
 */

package de.sciss.fscape
package stream

import akka.stream.{Attributes, FanInShape5}
import de.sciss.fscape.stream.impl.{ChunkImpl, FilterIn5DImpl, FilterLogicImpl, StageImpl, NodeImpl}

/** Overlapping window summation with offset (fuzziness) that can be modulated. */
object OffsetOverlapAdd {
  /**
    * @param in         the signal to window
    * @param size       the window size. this is clipped to be `<= 1`
    * @param step       the step size. this is clipped to be `<= 1`. If it is greater
    *                   than `size`, parts of the input will be correctly skipped.
    * @param offset     frame offset by which each input window is shifted. Can change from window to window.
    * @param minOffset  minimum (possibly negative) offset to reserve space for. Any `offset` value
    *                   encountered smaller than this will be clipped to `minOffset`.
    */
  def apply(in: OutD, size: OutI, step: OutI, offset: OutI, minOffset: OutI)(implicit b: Builder): OutD = {
    val stage0  = new Stage
    val stage   = b.add(stage0)
    b.connect(in        , stage.in0)
    b.connect(size      , stage.in1)
    b.connect(step      , stage.in2)
    b.connect(offset    , stage.in3)
    b.connect(minOffset , stage.in4)
    stage.out
  }

  private final class Window(val buf: Array[Double]) {
    var offIn   = 0
    var offOut  = 0
    var size    = buf.length

    def inRemain    : Int = size  - offIn
    def availableOut: Int = offIn - offOut
    def outRemain   : Int = size  - offOut
  }

  private final val name = "OffsetOverlapAdd"

  private type Shape = FanInShape5[BufD, BufI, BufI, BufI, BufI, BufD]

  private final class Stage(implicit ctrl: Control) extends StageImpl[Shape](name) {
    val shape = new FanInShape5(
      in0 = InD (s"$name.in"       ),
      in1 = InI (s"$name.size"     ),
      in2 = InI (s"$name.step"     ),
      in3 = InI (s"$name.offset"   ),
      in4 = InI (s"$name.minOffset"),
      out = OutD(s"$name.out"      )
    )

    def createLogic(attr: Attributes) = new Logic(shape)
  }

  private final class Logic(shape: Shape)(implicit ctrl: Control)
    extends NodeImpl(name, shape)
      with ChunkImpl[Shape]
      with FilterLogicImpl[BufD, Shape]
      with FilterIn5DImpl[BufD, BufI, BufI, BufI, BufI] {

    private[this] var size     : Int  = _
    private[this] var step     : Int  = _
    private[this] var offset   : Int  = _   // this is already corrected against `minOffset`!
    private[this] var minOffset: Int  = _

    private[this] var bufSize         = 0
    private[this] var bufWin: Array[Double] = _     // circular

    private[this] var isNextWindow    = true
    private[this] var mixToBufRemain  = 0
    private[this] var mixToBufOff     = 0
    private[this] var bufWritten      = 0L
    private[this] var bufRead         = 0L
    private[this] var maxStop         = 0L

    private[this] var init            = true
    private[this] var flushed         = false

    protected def shouldComplete(): Boolean = flushed && bufRead == bufWritten

    @inline
    private def canPrepareStep: Boolean = bufRead == bufWritten && bufIn0 != null && !flushed

    protected def processChunk(): Boolean = {
      // println(s"processChunk(); inOff = $inOff, outOff = $outOff, inRemain = $inRemain, outRemain = $outRemain, bufRead $bufRead, bufWritten $bufWritten, inputsEnded $inputsEnded")
      var stateChange = false

      if (canPrepareStep && isNextWindow) {
        startNextWindow()
        maxStop       = math.max(maxStop, bufWritten + size + offset)
        // println(s"maxStop = $maxStop")
        isNextWindow  = false
        stateChange   = true
      }

      val chunkIn = math.min(mixToBufRemain, inRemain)
      if (chunkIn > 0) {
        mixInputToBuffer(chunkIn)
        stateChange = true

        if (mixToBufRemain == 0) {
          isNextWindow   = true
          bufWritten    += step
        }
      }
      else if (inputsEnded && !flushed) {
        bufWritten  = maxStop
        flushed     = true
        stateChange = true
        // println(s"flushed = true; bufWritten = $bufWritten")
      }

      val chunkOut = math.min(bufWritten - bufRead, outRemain).toInt
      if (chunkOut > 0) {
        copyBufferToOutput(chunkOut)
        stateChange = true
      }

      stateChange
    }

    private def startNextWindow(): Unit = {
      if (bufIn1 != null && inOff < bufIn1.size) {
        size = math.max(1, bufIn1.buf(inOff))
      }
      if (bufIn2 != null && inOff < bufIn2.size) {
        step = math.max(1, bufIn2.buf(inOff))
      }
      if (init) {
        minOffset = bufIn4.buf(inOff)
        init      = false
      }
      if (bufIn3 != null && inOff < bufIn3.size) {
        offset = math.max(0, bufIn3.buf(inOff) - minOffset)
      }

      val newBufSize = size + offset
      if (bufSize < newBufSize) {
        // cf. https://stackoverflow.com/questions/38134091/
        val oldBufSize  = bufSize
        val newBuf      = new Array[Double](newBufSize)
        if (bufWin != null) {
          val off0      = (bufRead % oldBufSize).toInt
          val off1      = (bufRead % newBufSize).toInt
          val chunk0    = math.min(oldBufSize - off0, newBufSize - off1)
          System.arraycopy(bufWin, off0, newBuf, off1, chunk0)
          val off2      = (off0 + chunk0) % oldBufSize
          val off3      = (off1 + chunk0) % newBufSize
          val chunk1    = math.min(oldBufSize - math.max(chunk0, off2), newBufSize - off3)
          System.arraycopy(bufWin, off2, newBuf, off3, chunk1)
          val off4      = (off2 + chunk1) % oldBufSize
          val off5      = (off3 + chunk1) % newBufSize
          val chunk2    = oldBufSize - (chunk0 + chunk1)
          System.arraycopy(bufWin, off4, newBuf, off5, chunk2)
          bufWin = newBuf
        }
        bufWin  = newBuf
        bufSize = newBufSize
      }

      mixToBufRemain  = size
      mixToBufOff     = ((bufWritten + offset) % bufSize).toInt // 'reset'

      // println(s"startNextWindow - size = $size; bufWritten  = $bufWritten; mixToBufOff = $mixToBufOff")
    }

    private def mixIn(chunk: Int): Unit = {
      Util.add(bufIn0.buf, inOff, bufWin, mixToBufOff, chunk)
      mixToBufOff     = (mixToBufOff + chunk) % bufSize
      mixToBufRemain -= chunk
      inOff          += chunk
      inRemain       -= chunk
    }

    private def mixInputToBuffer(chunk: Int): Unit = {
      // println(s"mixInputToBuffer($chunk); inOff = $inOff, mixToBufOff = $mixToBufOff")
      val chunk1 = math.min(chunk, bufSize - mixToBufOff)
      mixIn(chunk1)
      val chunk2 = chunk - chunk1
      if (chunk2 > 0) {
        mixIn(chunk2)
      }
    }

    private def copyOut(bufOff: Int, chunk: Int): Unit = {
      Util.copy(bufWin, bufOff, bufOut0.buf, outOff, chunk)
      // we "clean up after ourselves" here, because this is the easiest spot
      Util.clear(bufWin, bufOff, chunk)
      outOff    += chunk
      outRemain -= chunk
      bufRead   += chunk
    }

    private def copyBufferToOutput(chunk: Int): Unit = {
      val bufOff1 = (bufRead % bufSize).toInt
      // println(s"copyBufferToOutput($chunk); outOff = $outOff, bufRead = $bufRead, bufOff1 = $bufOff1")
      val chunk1  = math.min(chunk, bufSize - bufOff1)
      copyOut(bufOff1, chunk1)
      val chunk2 = chunk - chunk1
      if (chunk2 > 0) {
        copyOut(0, chunk2)
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy