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

de.sciss.proc.impl.BufferWrite.scala Maven / Gradle / Ivy

/*
 *  BufferWrite.scala
 *  (SoundProcesses)
 *
 *  Copyright (c) 2010-2024 Hanns Holger Rutz. All rights reserved.
 *
 *	This software is published under the GNU Affero General Public License v3+
 *
 *
 *  For further information, please contact Hanns Holger Rutz at
 *  [email protected]
 */

package de.sciss.proc.impl

import de.sciss.asyncfile.Ops.URIOps
import de.sciss.audiofile.{AudioFile, AudioFileSpec, AudioFileType}
import de.sciss.lucre.synth.{Buffer, Executor, RT, Resource, Synth}
import de.sciss.lucre.{Artifact, synth}
import de.sciss.numbers.Implicits.intNumberWrapper
import de.sciss.osc
import de.sciss.proc.impl.BufferTransfer.MAX_CLUMP
import de.sciss.proc.{AuralContext, AuralNode, SoundProcesses}
import de.sciss.synth.message.BufferInfo
import de.sciss.synth.proc.graph.Action
import de.sciss.synth.proc.graph.impl.SendReplyResponder
import de.sciss.synth.{GE, message, ugen}

import scala.collection.{IndexedSeq => CVec}
import scala.concurrent.Future

// XXX TODO --- DRY with BufferPrepare
// XXX TODO --- investigate why this is so much slower than `BufferPrepare` (chunked b_setn)
// (one possibility to speed up a bit could be to send b_getn right after receiving b_setn,
//  writing to disk in parallel; although it's unclear if that is involved in the bottle neck)
// cf. https://scsynth.org/t/why-would-b-getn-be-much-slower-than-b-setn/
/** Asynchronously transfers a buffer's content to a file chunks by chunk.
  * This works in two steps, as it is triggered from within the node.
  * First, the `Starter` is installed on the aural node. When it detects
  * the trigger, it calls `BufferWrite.apply` (which is similar to `BufferPrepare()`).
  */
object BufferWrite {
  // via SendReply
  def replyName(key: String): String = s"/$$wb_$key"

  // via n_set
  def doneName (key: String): String = s"/$$wb_done_$key"

  def makeUGen(g: Action.WriteBuf): GE = {
    import g._
    import ugen._
    val bufFrames     = BufFrames     .ir(buf)
    val bufChannels   = BufChannels   .ir(buf)
    val bufSampleRate = BufSampleRate .ir(buf)
    val values: GE = Seq(
      buf           ,
      bufFrames     ,
      bufChannels   ,
      bufSampleRate ,
      numFrames     ,
      startFrame    ,
      fileType      ,
      sampleFormat  ,
    )
    SendReply.kr(trig = trig, values = values, msgName = BufferWrite.replyName(key), id = 0)
    ControlProxyFactory.fromString(BufferWrite.doneName(key)).tr
  }

  final class Starter[T <: synth.Txn[T]](f: Artifact.Value, key: String, nr: AuralNode[T])
                                        (implicit context: AuralContext[T])
    extends SendReplyResponder {

    private[this] val Name    = replyName(key)
    private[this] val NodeId  = synth.peer.id

    override protected def synth: Synth = nr.synth

    override protected def added()(implicit tx: RT): Unit = ()

    override protected val body: Body = {
      case osc.Message(Name, NodeId, 0,
          bufIdF          : Float,
          bufFramesF      : Float,
          bufChannelsF    : Float,
          bufSampleRate   : Float,
          numFramesF      : Float,
          startFrameF     : Float,
          fileTypeIdF     : Float,
          sampleFormatIdF : Float,
        ) =>

        val bufId           = bufIdF          .toInt
        val bufFrames       = bufFramesF      .toInt
        val bufChannels     = bufChannelsF    .toInt
        val startFrame      = startFrameF     .toInt
        val numFrames0      = numFramesF      .toInt
        val numFrames       = if (numFrames0 >= 0) numFrames0 else bufFrames - startFrame
        val fileTypeId      = fileTypeIdF     .toInt
        val sampleFormatId  = sampleFormatIdF .toInt
        val fileType        = if (fileTypeId < 0) {
          val e = f.extL
          AudioFileType.writable.find(_.extensions.contains(e)).getOrElse(AudioFileType.AIFF)
        } else {
          Action.WriteBuf.fileType(fileTypeId.clip(0, Action.WriteBuf.maxFileTypeId))
        }
        val sampleFormat    = Action.WriteBuf.sampleFormat(sampleFormatId.clip(0, Action.WriteBuf.maxSampleFormatId))
        val server          = nr.server
        val sPeer           = server.peer
        val bufInfo         = BufferInfo.Data(bufId = bufId, numFrames = bufFrames, numChannels = bufChannels,
          sampleRate = bufSampleRate)
        val bufPeer         = bufInfo.asBuffer(sPeer)
        val spec            = AudioFileSpec(fileType, sampleFormat, numChannels = bufChannels,
          sampleRate = bufSampleRate, numFrames = numFrames)

        import context.universe.cursor
        SoundProcesses.step[T](s"BufferWrite($synth, $key)") { implicit tx: T =>
          val buf     = Buffer.wrap(server, bufPeer)
          val config  = Config(f, spec, offset = startFrame, buf = buf, key = key)
          val bw      = BufferWrite[T](config)
          nr.addResource(bw)
          tx.afterCommit {
            bw.foreach { _ =>
              val server  = nr.server
              val sPeer   = server.peer
              sPeer ! message.NodeSet(synth.peer.id, BufferWrite.doneName(key) -> 1f)
            } (Executor.executionContext)
          }
        }
    }
  }

  /** The configuration of the buffer transfer.
    *
    * @param f          the audio file to write to
    * @param spec       the file's specification (number of channels and frames)
    * @param offset     the offset into the buffer to start from
    * @param buf        the buffer to write from.
    * @param key        the key of the `graph.Buffer` element, used for setting the synth control eventually
    */
  case class Config(f: Artifact.Value, spec: AudioFileSpec, offset: Int, buf: Buffer, key: String) {
    override def productPrefix = "BufferWrite.Config"
    override def toString: String = {
      import spec.{productPrefix => _, _}
      s"$productPrefix($f, numChannels = $numChannels, numFrames = $numFrames, offset = $offset, key = $key)"
    }
  }

  /** Creates and launches the process. */
  def apply[T <: synth.Txn[T]](config: Config)(implicit tx: T): Future[Any] with Resource = {
    import config._
    if (!buf.isOnline) sys.error("Buffer must be allocated")
    val numFrL = spec.numFrames
    if (numFrL > buf.numFrames - offset) sys.error(s"File $f spec ($numFrL frames) is larger than buffer (${buf.numFrames}, offset $offset)")
//    if (numFrL > 0x3FFFFFFF) sys.error(s"File $f is too large ($numFrL frames) for an in-memory buffer")
    import spec.numChannels
    val blockSize = BufferTransfer.calcBlockSize(buf, numChannels = numChannels)
    val res = new GetN[T](f = f, numFrames = numFrL.toInt, off0 = offset,
      spec = config.spec.copy(numFrames = 0L), blockSize = blockSize, buf = buf, key = key)
    tx.afterCommit(res.start()(Executor.executionContext))
    res
  }

  private final class GetN[T <: synth.Txn[T]](f: Artifact.Value, numFrames: Int, off0: Int, spec: AudioFileSpec,
                                              blockSize: Int, buf: Buffer, key: String)
    extends BufferTransfer.GetN[T](
      blockSize   = blockSize,
      numFrames   = numFrames,
      offset0     = off0,
      buf         = buf,
      key         = key
    ) {

    override protected val numChannels: Int = spec.numChannels

    protected def runBody(): Future[Prod] = {
      AudioFile.openWriteAsync(f, spec).flatMap { af =>
//        if (off0 > 0) af.seek(off0)
//        val afBuf = allocBuf()
        val afBuf = af.buffer(blockSize * MAX_CLUMP)
        val fut0 = runCore { dataSq =>
          var chunk = 0
          val itSq = dataSq.iterator
          while (itSq.hasNext) {
            val data: CVec[Float]   = itSq.next()
            val it: Iterator[Float] = data.iterator
            while (it.hasNext) {
              var ch = 0
              while (ch < numChannels) {
                val chBuf = afBuf(ch)
                chBuf(chunk) = it.next() // de-interleave channel data
                ch += 1
              }
              chunk += 1
            }
          }
          af.write(afBuf, off = 0, len = chunk)
        }

        fut0.andThen { case _ =>
          af.close()
        }
      }
    }

    override def toString = s"BufferWrite.GetN($f, $buf)@${hashCode().toHexString}"
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy