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