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

de.sciss.mellite.gui.ActionBounceTimeline.scala Maven / Gradle / Ivy

/*
 *  ActionBounceTimeline.scala
 *  (Mellite)
 *
 *  Copyright (c) 2012-2016 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 de.sciss.mellite
package gui

import java.io.{EOFException, File}
import java.text.ParseException
import javax.swing.{JFormattedTextField, SpinnerNumberModel, SwingUtilities}

import de.sciss.audiowidgets.{AxisFormat, TimelineModel}
import de.sciss.desktop
import de.sciss.desktop.{Desktop, DialogSource, FileDialog, OptionPane, UndoManager, Window}
import de.sciss.equal
import de.sciss.file._
import de.sciss.lucre.artifact.{Artifact, ArtifactLocation}
import de.sciss.lucre.expr.{DoubleObj, LongObj}
import de.sciss.lucre.stm
import de.sciss.lucre.stm.Obj
import de.sciss.lucre.swing.defer
import de.sciss.lucre.synth.{Buffer, Server, Synth, Sys}
import de.sciss.mellite.gui.edit.EditFolderInsertObj
import de.sciss.numbers
import de.sciss.processor.impl.ProcessorImpl
import de.sciss.processor.{Processor, ProcessorLike}
import de.sciss.span.Span.SpanOrVoid
import de.sciss.span.{Span, SpanLike}
import de.sciss.swingplus.{ComboBox, Labeled, Spinner, SpinnerComboBox}
import de.sciss.synth.io.{AudioFile, AudioFileSpec, AudioFileType, SampleFormat}
import de.sciss.synth.proc.Implicits._
import de.sciss.synth.proc.{AudioCue, Bounce, Code, Folder, TimeRef, Timeline}
import de.sciss.synth
import de.sciss.synth.{SynthGraph, addToTail}

import scala.collection.immutable.{IndexedSeq => Vec}
import scala.concurrent.blocking
import scala.swing.Swing._
import scala.swing.event.{ButtonClicked, SelectionChanged}
import scala.swing.{Alignment, BoxPanel, Button, ButtonGroup, CheckBox, Component, Dialog, FlowPanel, GridPanel, Label, Orientation, ProgressBar, RadioButton, Swing, TextField}
import scala.util.Try
import scala.util.control.NonFatal

object ActionBounceTimeline {
  private val DEBUG = false

  final case class QuerySettings[S <: Sys[S]](
    file        : Option[File]          = None,
    spec        : AudioFileSpec         = AudioFileSpec(AudioFileType.AIFF, SampleFormat.Int24, numChannels = 2, sampleRate = 44100.0),
    gain        : Gain                  = Gain.normalized(-0.2f),
    span        : SpanOrVoid            = Span.Void,
    channels    : Vec[Range.Inclusive]  = Vector(0 to 0 /* 1 */),
    realtime    : Boolean               = false,
    fineControl : Boolean               = false,
    importFile  : Boolean               = false,
    location    : Option[stm.Source[S#Tx, ArtifactLocation[S]]] = None,
    transform   : Option[stm.Source[S#Tx, Code.Obj[S]]] = None
  ) {
    def prepare(group: stm.Source[S#Tx, Timeline[S]], f: File): PerformSettings[S] = {
      val server        = Server.Config()
      Mellite.applyAudioPrefs(server, useDevice = realtime, pickPort = realtime)
      if (fineControl) server.blockSize = 1
      specToServerConfig(f, spec, server)
      PerformSettings(
        realtime = realtime, group = group, server = server, gain = gain, span = span, channels = channels
      )
    }
  }

  final case class PerformSettings[S <: Sys[S]](
    realtime: Boolean,
    group: stm.Source[S#Tx, Timeline[S]],
    server: Server.Config,
    gain: Gain = Gain.normalized(-0.2f),
    span: SpanLike, channels: Vec[Range.Inclusive]
  )

  def specToServerConfig(file: File, spec: AudioFileSpec, config: Server.ConfigBuilder): Unit = {
    config.nrtOutputPath      = file.path
    config.nrtHeaderFormat    = spec.fileType
    config.nrtSampleFormat    = spec.sampleFormat
    config.sampleRate         = spec.sampleRate.toInt
    config.outputBusChannels  = spec.numChannels
    val numPrivate = Prefs.audioNumPrivate.getOrElse(Prefs.defaultAudioNumPrivate)
    config.audioBusChannels   = config.outputBusChannels + numPrivate
  }

  type CodeSource[S <: Sys[S]] = stm.Source[S#Tx, Code.Obj[S]]

  def findTransforms[S <: Sys[S]](document: Workspace[S])(implicit tx: S#Tx): Vec[Labeled[CodeSource[S]]] = {
    type Res = Vec[Labeled[CodeSource[S]]]
    def loop(xs: List[Obj[S]], res: Res): Res =
      xs match {
        case (objT: Code.Obj[S]) :: tail =>
          val res1 = objT.value match {
            case ft: Code.FileTransform => res :+ Labeled(tx.newHandle(objT))(objT.name)
            case _ => res
          }
          loop(tail, res1)
        case (objT: Folder[S]) :: tail =>
          val res1 = loop(objT.iterator.toList, res)
          loop(tail, res1)
        case _ :: tail  => loop(tail, res)
        case Nil        => res
      }

    loop(document.rootH().iterator.toList, Vector.empty)
  }

  sealed trait Selection
  case object SpanSelection     extends Selection
  case object DurationSelection extends Selection
  case object NoSelection       extends Selection

  def query[S <: Sys[S]](init: QuerySettings[S], document: Workspace[S], timelineModel: TimelineModel,
                         window: Option[Window])
                        (implicit cursor: stm.Cursor[S], undoManager: UndoManager) : (QuerySettings[S], Boolean) =
    query1(init, document, timelineModel, window, showSelection = SpanSelection, showTransform = true, showImport = true)

  def query1[S <: Sys[S]](init: QuerySettings[S], document: Workspace[S], timelineModel: TimelineModel,
                         window: Option[Window], showSelection: Selection, showTransform: Boolean,
                         showImport: Boolean)
                        (implicit cursor: stm.Cursor[S], undoManager: UndoManager) : (QuerySettings[S], Boolean) = {

    val ggFileType      = new ComboBox[AudioFileType](AudioFileType.writable)
    ggFileType.selection.item = init.spec.fileType // AudioFileType.AIFF
    val ggSampleFormat  = new ComboBox[SampleFormat](SampleFormat.fromInt16)
    desktop.Util.fixWidth(ggSampleFormat)
    // ggSampleFormat.items = fuck you scala no method here
    ggSampleFormat.selection.item = init.spec.sampleFormat
    val ggSampleRate    = new SpinnerComboBox(value0 = 44100.0, minimum = 1.0, maximum = TimeRef.SampleRate,
      step = 100.0, items = Seq(44100.0, 48000.0, 88200.0, 96000.0))

    val ggPathText = new TextField(32)

    def setPathText(file: File): Unit =
      ggPathText.text = file.replaceExt(ggFileType.selection.item.extension).path

    ggFileType.listenTo(ggFileType.selection)
    ggFileType.reactions += {
      case SelectionChanged(_) =>
        val s = ggPathText.text
        if (!s.isEmpty) setPathText(new File(s))
    }

    init.file.foreach(f => ggPathText.text = f.path)
    val ggPathDialog    = Button("...") {
      val initT = ggPathText.text
      val init  = if (initT.isEmpty) None else Some(new File(initT))
      val dlg   = FileDialog.save(init = init, title = "Bounce Audio Output File")
      dlg.show(window).foreach(setPathText)
    }
    ggPathDialog.peer.putClientProperty("JButton.buttonType", "gradient")

    val gainModel   = new SpinnerNumberModel(init.gain.decibels, -160.0, 160.0, 0.1)
    val ggGainAmt   = new Spinner(gainModel)

    val ggGainType  = new ComboBox(Seq("Normalized", "Immediate"))
    ggGainType.selection.index = if (init.gain.normalized) 0 else 1
    ggGainType.listenTo(ggGainType.selection)
    ggGainType.reactions += {
      case SelectionChanged(_) =>
        ggGainType.selection.index match {
          case 0 => gainModel.setValue(-0.2)
          case 1 => gainModel.setValue( 0.0)
        }
        ggGainAmt.requestFocus()
    }

    val ggSpanAll   = new RadioButton("Automatic")
    val tlSel       = init.span match {
      case sp: Span => sp
      case _        => timelineModel.selection
    }
    val selectionText = tlSel match {
      case Span(start, stop)  =>
        val sampleRate  = timelineModel.sampleRate
        val fmt         = AxisFormat.Time()
        s"(${fmt.format(start/sampleRate)} ... ${fmt.format(stop/sampleRate)})"
      case _ =>
        ""
    }
    lazy val ggSpanUser  = new RadioButton(s"Current Selection $selectionText")
    lazy val ggSpanGroup = new ButtonGroup(ggSpanAll, ggSpanUser)

    lazy val mDuration   = new SpinnerNumberModel(tlSel.length / TimeRef.SampleRate, 0.0, 10000.0, 0.1)

    var transformItemsCollected = false

    val ggRealtime = new CheckBox()
    ggRealtime.selected = init.realtime
    val ggFineControl = new CheckBox()
    ggFineControl.selected = init.fineControl

    def updateTransformEnabled(): Unit = {
      val enabled = ggImport.selected
      checkTransform.enabled = enabled
      ggTransform   .enabled = enabled && checkTransform.selected
      if (ggTransform.enabled && !transformItemsCollected) {
        val transform = cursor.step { implicit tx =>
          findTransforms(document)
        }
        transformItemsCollected =  true
        ggTransform.items = transform
        import equal.Implicits._
        for (t <- init.transform; lb <- transform.find(_.value === t)) {
          ggTransform.selection.item = lb
        }
      }
    }

    lazy val ggImport  = new CheckBox() {
      listenTo(this)
      reactions += {
        case ButtonClicked(_) => updateTransformEnabled()
      }
    }
    if (showTransform) updateTransformEnabled()

    lazy val checkTransform = new CheckBox() {
      listenTo(this)
      reactions += {
        case ButtonClicked(_) => updateTransformEnabled()
      }
    }
    lazy val ggTransform = new ComboBox[Labeled[stm.Source[S#Tx, Code.Obj[S]]]](Nil) // lazy filling
    val pTransform  = new BoxPanel(Orientation.Horizontal) {
      contents ++= Seq(checkTransform, HStrut(4), ggTransform)
    }

    // cf. stackoverflow #4310439 - with added spaces after comma
    // val regRanges = """^(\d+(-\d+)?)(,\s*(\d+(-\d+)?))*$""".r

    // cf. stackoverflow #16532768
    val regRanges = """(\d+(-\d+)?)""".r

    val fmtRanges = new JFormattedTextField.AbstractFormatter {
      def stringToValue(text: String): Vec[Range.Inclusive] = try {
        regRanges.findAllIn(text).toIndexedSeq match {
          case list if list.nonEmpty => list.map { s =>
            val i = s.indexOf('-')
            if (i < 0) {
              val a = s.toInt - 1
              a to a
            } else {
              val a = s.substring(0, i).toInt - 1
              val b = s.substring(i +1).toInt - 1
              a to b
            }
          }
        }
      } catch {
        case e @ NonFatal(_) =>
          e.printStackTrace()
          throw new ParseException(text, 0)
      }

      def valueToString(value: Any): String = try {
        value match {
          case sq: Vec[_] => sq.map {
            case r: Range if r.start < r.end  => s"${r.start + 1}-${r.end + 1}"
            case r: Range                     => s"${r.start + 1}"
          } .mkString(", ")
        }
      } catch {
        case NonFatal(_) => throw new ParseException(Option(value).fold("null")(_.toString), 0)
      }
    }

    val ggChannelsJ = new JFormattedTextField(fmtRanges)
    ggChannelsJ.setColumns(12)
    ggChannelsJ.setValue(init.channels)
    ggChannelsJ.setFocusLostBehavior(JFormattedTextField.COMMIT_OR_REVERT)
    val ggChannels  = Component.wrap(ggChannelsJ)
    ggChannels.tooltip = "Ranges of channels to bounce, such as 1-4 or 1,3,5"

    import Alignment.Trailing
    import Swing.EmptyIcon
    val pPath     = new FlowPanel(ggPathText, ggPathDialog)
    val pFormat   = new FlowPanel(ggFileType, ggSampleFormat, ggSampleRate, ggGainAmt, new Label("dB"), ggGainType)
    val pSpan     = new GridPanel(0, 2)
    pSpan.hGap    = 4
    pSpan.contents ++= Seq(new Label("Channels:", EmptyIcon, Trailing), ggChannels)
    import equal.Implicits._
    if (showSelection === SpanSelection) {
      ggSpanGroup.select(if (init.span.isEmpty) ggSpanAll else ggSpanUser)
      ggSpanUser.enabled   = tlSel.nonEmpty
      pSpan.contents ++= Seq(new Label("Timeline Span:", EmptyIcon, Trailing), ggSpanAll,
                             HStrut(1),                                        ggSpanUser)
    } else if (showSelection === DurationSelection) {
      val ggDuration = new Spinner(mDuration)
      pSpan.contents ++= Seq(new Label("Duration [sec]:", EmptyIcon, Trailing), ggDuration)
    }
    pSpan.contents ++= Seq(
      HStrut(1), VStrut(32),
      new Label("Run in Real-Time:" , EmptyIcon, Trailing), ggRealtime,
      new Label("Fine Control Rate:", EmptyIcon, Trailing), ggFineControl
    )
    if (showImport) {
      ggImport.selected = init.importFile
      pSpan.contents ++= Seq(new Label("Import Output File Into Workspace:", EmptyIcon, Trailing), ggImport)
    }
    if (showTransform) {
      pSpan.contents ++= Seq(new Label("Apply Transformation:", EmptyIcon, Trailing), pTransform)
    }
    val box       = new BoxPanel(Orientation.Vertical) {
      contents ++= Seq(pPath, pFormat, pSpan)
    }

    val opt = OptionPane.confirmation(message = box, optionType = OptionPane.Options.OkCancel,
      messageType = OptionPane.Message.Plain)
    val title = "Bounce to Disk"
    opt.title = title
    val ok    = opt.show(window) === OptionPane.Result.Ok
    val file  = if (ggPathText.text === "") None else Some(new File(ggPathText.text))

    val channels: Vec[Range.Inclusive] = try {
      fmtRanges.stringToValue(ggChannelsJ.getText)
    } catch {
      case _: ParseException => init.channels
    }

    val importFile  = if (showImport) ggImport.selected else init.importFile
    val numChannels = channels.map(_.size).sum

    val spanOut = showSelection match {
      case SpanSelection      => if (ggSpanUser.selected) tlSel else Span.Void
      case DurationSelection  => Span(0L, (mDuration.getNumber.doubleValue() * TimeRef.SampleRate + 0.5).toLong)
      case NoSelection        => init.span
    }

    var settings = QuerySettings(
      realtime    = ggRealtime   .selected,
      fineControl = ggFineControl.selected,
      file        = file,
      spec        = AudioFileSpec(ggFileType.selection.item, ggSampleFormat.selection.item,
        numChannels = numChannels, sampleRate = ggSampleRate.value),
      gain        = Gain(gainModel.getNumber.floatValue(), if (ggGainType.selection.index == 0) true else false),
      span        = spanOut,
      channels    = channels,
      importFile  = importFile,
      location    = init.location,
      transform   = if (checkTransform.selected) Option(ggTransform.selection.item).map(_.value) else None
    )

    file match {
      case Some(f) if importFile =>
        init.location match {
          case Some(source) if cursor.step { implicit tx =>
              val parent = source().directory
              Try(Artifact.relativize(parent, f)).isSuccess
            } =>  // ok, keep previous location

          case _ => // either no location was set, or it's not parent of the file
            ActionArtifactLocation.query[S](document.rootH, f) match {
              case Some(either) =>
                either match {
                  case Left(source) =>
                    settings = settings.copy(location = Some(source))

                  case Right((name, directory)) =>
                    val (edit, source) = cursor.step { implicit tx =>
                      val locObj  = ActionArtifactLocation.create(name = name, directory = directory)
                      val folder  = document.rootH()
                      val index   = folder.size
                      val _edit   = EditFolderInsertObj("Location", folder, index, locObj)
                      (_edit, tx.newHandle(locObj))
                    }
                    undoManager.add(edit)
                    settings = settings.copy(location = Some(source))
                }

              case _ => return (settings, false)
            }
        }
      case _ =>
    }

    file match {
      case Some(f) if ok && f.exists() =>
        val ok1 = Dialog.showConfirmation(
          message     = s"File
${f.path}
already exists.

Are you sure you want to overwrite it?", title = title, optionType = Dialog.Options.OkCancel, messageType = Dialog.Message.Warning ) === Dialog.Result.Ok (settings, ok1) case None if ok => query(settings, document, timelineModel, window) case _ => (settings, ok) } } // final case class QuerySettings[S <: Sys[S]]( // file: Option[File] = None, // spec: AudioFileSpec = AudioFileSpec(AudioFileType.AIFF, SampleFormat.Int24, numChannels = 2, sampleRate = 44100.0), // gainAmount: Double = -0.2, gainType: Gain = Normalized, // span: SpanOrVoid = Span.Void, channels: Vec[Range.Inclusive] = Vector(0 to 1), // importFile: Boolean = true, location: Option[stm.Source[S#Tx, ArtifactLocation[S]]] = None // ) def performGUI[S <: Sys[S]](document: Workspace[S], settings: QuerySettings[S], group: stm.Source[S#Tx, Timeline[S]], file: File, window: Option[Window] = None) (implicit cursor: stm.Cursor[S], compiler: Code.Compiler): Unit = { val hasTransform= settings.importFile && settings.transform.isDefined val bounceFile = if (hasTransform) { File.createTempFile("bounce", s".${settings.spec.fileType.extension}") } else { file } val pSet = settings.prepare(group, bounceFile) var process: ProcessorLike[Any, Any] = perform(document, pSet) var processCompleted = false val ggProgress = new ProgressBar() lazy val ggCancel = Button("Abort") { process.abort() // currently process doesn't seem to abort under certain errors // (e.g. buffer allocator exhausted). XXX TODO val run = new Runnable { def run() = { Thread.sleep(1000); defer(fDispose()) }} new Thread(run).start() } ggCancel.focusable = false lazy val op = OptionPane(message = ggProgress, messageType = OptionPane.Message.Plain, entries = Seq(ggCancel)) lazy val title = s"Bouncing to ${file.name} ..." op.title = title def fDispose(): Unit = { val w = SwingUtilities.getWindowAncestor(op.peer); if (w != null) w.dispose() processCompleted = true } val progDiv = if (hasTransform) 2 else 1 process.addListener { case prog @ Processor.Progress(_, _) => defer(ggProgress.value = prog.toInt / progDiv) } val onFailure: PartialFunction[Throwable, Unit] = { case Processor.Aborted() => defer(fDispose()) case ex => defer { fDispose() DialogSource.Exception(ex -> title).show(window) } } def bounceDone(): Unit = { if (DEBUG) println(s"bounceDone(). hasTransform? $hasTransform") if (hasTransform) { val ftOpt = cursor.step { implicit tx => settings.transform.flatMap(_.apply().value match { case ft: Code.FileTransform => Some(ft) case _ => None }) } if (DEBUG) println(s"file transform option = ${ftOpt.isDefined}") ftOpt match { case Some(ft) => ft.execute((bounceFile, file, { codeProc => if (DEBUG) println("code code processor") process = codeProc codeProc.addListener { case prog @ Processor.Progress(_, _) => defer(ggProgress.value = prog.toInt / progDiv + 50) } codeProc.onSuccess { case _ => allDone() } codeProc.onFailure(onFailure) })) case _ => println("WARNING: Code does not denote a file transform") defer(fDispose()) Desktop.revealFile(file) } } else { allDone() } } def allDone(): Unit = { if (DEBUG) println("allDone") defer(fDispose()) (settings.importFile, settings.location) match { case (true, Some(locSource)) => // val elemName = file.base val spec = AudioFile.readSpec(file) cursor.step { implicit tx => val loc = locSource() // loc.modifiableOption.foreach { locM => // val fileR = Artifact.relativize(locM.directory, file) // val artifact = locM.add(file) // val depArtif = Artifact.Modifiable(artifact) val depArtif = Artifact(loc, file) // locM.add(file) val depOffset = LongObj .newVar[S](0L) val depGain = DoubleObj.newVar[S](1.0) val deployed = AudioCue.Obj[S](depArtif, spec, depOffset, depGain) val depElem = deployed // Grapheme.Expr.Audio(deployed) val depObj = depElem // Obj(depElem) depObj.name = file.base // val transformOpt = settings.transform.map(_.apply()) // val recursion = Recursion(group(), settings.span, depObj, settings.gain, settings.channels, transformOpt) // val recElem = Recursion.Elem(recursion) // val recObj = Obj(recElem) // recObj.name = elemName document.rootH().addLast(depObj) // document.rootH().addLast(recObj) // } } case _ => Desktop.revealFile(file) } } process.onSuccess { case _ => bounceDone() } process.onFailure(onFailure) desktop.Util.delay(500) { if (!processCompleted) op.show(window) } } def perform[S <: Sys[S]](document: Workspace[S], settings: PerformSettings[S]) (implicit cursor: stm.Cursor[S]): Processor[File] = { import document.inMemoryBridge implicit val workspace = document // for real-time, we generally have to overshoot because in SC 3.6, DiskOut's // buffer is not flushed after synth is stopped. val realtime = settings.realtime val normalized = settings.gain.normalized val needsTemp = realtime || normalized val numChannels = settings.server.outputBusChannels val span = settings.span match { case sp: Span => sp case _ => cursor.step { implicit tx => val tl = settings.group() val start = settings.span match { case hs: Span.HasStart => hs.start case _ => tl.firstEvent.getOrElse(0L) } val stop = settings.span match { case hs: Span.HasStop => hs.stop case _ => tl.lastEvent.getOrElse(start) } Span(start, stop) } } val fileOut = file(settings.server.nrtOutputPath) val fileType = settings.server.nrtHeaderFormat val sampleFormat = settings.server.nrtSampleFormat val sampleRate = settings.server.sampleRate val fileFrames0 = (span.length * sampleRate / TimeRef.SampleRate + 0.5).toLong val fileFrames = fileFrames0 // - (fileFrames0 % settings.server.blockSize) val settings1: PerformSettings[S] = if (!needsTemp) settings else { val fTmp = File.createTempFile("bounce", ".w64") fTmp.deleteOnExit() val sConfig = Server.ConfigBuilder(settings.server) sConfig.nrtOutputPath = fTmp.path sConfig.nrtHeaderFormat = AudioFileType.Wave64 sConfig.nrtSampleFormat = SampleFormat.Float // if (realtime) sConfig.outputBusChannels = 0 settings.copy(server = sConfig) } val bounce = Bounce[S, document.I] val bnc = Bounce.Config[S] bnc.group = settings1.group :: Nil bnc.realtime = realtime bnc.server.read(settings1.server) val bncGainFactor = if (settings1.gain.normalized) 1f else settings1.gain.linear val inChans = settings1.channels.flatten val numInChans = if (inChans.isEmpty) 0 else inChans.max + 1 assert(numInChans >= numChannels) val span1 = if (!realtime) span else { val bufDur = Buffer.defaultRecBufferSize.toDouble / bnc.server.sampleRate // apart from DiskOut buffer, add a bit of head-room (100ms) to account for jitter val bufFrames = ((bufDur + 0.1) * TimeRef.SampleRate + 0.5).toLong val numFrames = span.length + bufFrames // (span.length + bufFrames - 1) / bufFrames * bufFrames Span(span.start, span.start + numFrames) } bnc.span = span1 bnc.beforePrepare = { (_tx, s) => implicit val tx = _tx // make sure no private bus overlaps with the virutal output if (numInChans > numChannels) { s.allocAudioBus(numInChans - numChannels) } val graph = SynthGraph { import synth._ import ugen._ val sigIn = In.ar(0, numInChans) val sigOut: GE = inChans.map { in => val chIn = sigIn \ in chIn * bncGainFactor } ReplaceOut.ar(0, sigOut) } Synth.play(graph)(s.defaultGroup, addAction = addToTail) } val bProcess = bounce.apply(bnc) // bProcess.addListener { // case u => println(s"UPDATE: $u") // } bProcess.start() val process = if (!needsTemp) bProcess else { val nProcess = new Normalizer(bounce = bProcess, fileOut = fileOut, fileType = fileType, sampleFormat = sampleFormat, gain = if (normalized) settings1.gain else Gain.immediate(0f), numFrames = fileFrames) nProcess.start() nProcess } process } // XXX TODO --- could use filtered console output via Poll to // measure max gain already during bounce private final class Normalizer[S <: Sys[S]](bounce: Processor[File], fileOut: File, fileType: AudioFileType, sampleFormat: SampleFormat, gain: Gain, numFrames: Long) extends ProcessorImpl[File, Processor[File]] with Processor[File] { override def toString = s"Normalize bounce $fileOut" def body(): File = blocking { val fileIn = await(bounce, weight = if (gain.normalized) 0.8 else 0.9) // arbitrary weight // tricky --- scsynth flush might not yet be seen // thus wait a few seconds until header becomes available val t0 = System.currentTimeMillis() while (AudioFile.readSpec(fileIn).numFrames == 0L && System.currentTimeMillis() - t0 < 4000) { blocking(Thread.sleep(500)) } val afIn = AudioFile.openRead(fileIn) import numbers.Implicits._ try { val bufSz = 8192 val buf = afIn.buffer(bufSz) val numFrames0 = math.min(afIn.numFrames, numFrames) // whatever... var rem = numFrames0 // if (rem >= afIn.numFrames) // throw new EOFException(s"Bounced file is too short (${afIn.numFrames} -- expected at least $rem)") if (afIn.numFrames == 0) throw new EOFException("Bounced file is empty") val mul = if (!gain.normalized) gain.linear else { var max = 0f while (rem > 0) { val chunk = math.min(bufSz, rem).toInt afIn.read(buf, 0, chunk) var ch = 0; while (ch < buf.length) { val cBuf = buf(ch) var i = 0; while (i < chunk) { val f = math.abs(cBuf(i)) if (f > max) max = f i += 1 } ch += 1 } rem -= chunk progress = (rem.toDouble / numFrames0).linlin(0, 1, 0.9, 0.8) checkAborted() } afIn.seek(0L) if (max == 0) 1f else gain.linear / max } val afOut = AudioFile.openWrite(fileOut, afIn.spec.copy(fileType = fileType, sampleFormat = sampleFormat, byteOrder = None)) try { rem = numFrames0 while (rem > 0) { val chunk = math.min(bufSz, rem).toInt afIn.read(buf, 0, chunk) if (mul != 1) { var ch = 0; while (ch < buf.length) { val cBuf = buf(ch) var i = 0; while (i < chunk) { cBuf(i) *= mul i += 1 } ch += 1 } } afOut.write(buf, 0, chunk) rem -= chunk progress = (rem.toDouble / numFrames0).linlin(0, 1, 1.0, 0.9) checkAborted() } afOut.close() afIn .close() } finally { if (afOut.isOpen) afOut.cleanUp() } } finally { if (afIn.isOpen) afIn.cleanUp() } fileOut } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy