Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
de.sciss.mellite.MainFrame.scala Maven / Gradle / Ivy
/*
* MainFrame.scala
* (Mellite)
*
* Copyright (c) 2012-2023 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.mellite
import de.sciss.audiowidgets.PeakMeter
import de.sciss.desktop.impl.WindowImpl
import de.sciss.desktop.{Desktop, Menu, Preferences, Window, WindowHandler}
import de.sciss.icons.raphael
import de.sciss.log.Level
import de.sciss.lucre.TxnLike
import de.sciss.lucre.swing.LucreSwing.deferTx
import de.sciss.lucre.synth.{Bus, Group, RT, Server, Synth}
import de.sciss.mellite.Log.{log, timeline => logTimeline}
import de.sciss.mellite.Mellite.executionContext
import de.sciss.mellite.impl.ApiBrowser
import de.sciss.mellite.impl.component.NoMenuBarActions
import de.sciss.proc.AuralSystem.{Running, Stopped}
import de.sciss.proc.SensorSystem
import de.sciss.proc.SoundProcesses.{logAural, logTransport}
import de.sciss.proc.gui.{AudioBusMeter, Oscilloscope, ScopeBase, SpectrumAnalyzer}
import de.sciss.synth.swing.ServerStatusPanel
import de.sciss.synth.{SynthGraph, addAfter, addBefore, addToHead, addToTail}
import de.sciss.{desktop, osc}
import java.awt.{Color, Font}
import java.net.URI
import javax.imageio.ImageIO
import scala.collection.immutable.{IndexedSeq => Vec}
import scala.concurrent.stm.{Ref, atomic}
import scala.swing.Swing._
import scala.swing.event.{ButtonClicked, ValueChanged}
import scala.swing.{Action, Alignment, BoxPanel, Button, CheckBox, Component, FlowPanel, Label, Orientation, Slider, ToggleButton}
final class MainFrame extends desktop.impl.WindowImpl { me =>
import de.sciss.mellite.Mellite.{auralSystem, sensorSystem}
def handler: WindowHandler = Application.windowHandler
private[this] val lbSensors = new Label("Sensors:")
private[this] val lbAudio = new Label("Audio:")
private[this] lazy val ggSensors = {
val res = new PeakMeter
res.orientation = Orientation.Horizontal
res.holdPainted = false
res.rmsPainted = false
res
}
{
if (!Desktop.isLinux) {
val is = getClass.getResourceAsStream("/application.png")
if (is != null) {
val img = ImageIO.read(is)
is.close()
Desktop.setDockImage(img)
}
}
lbSensors.horizontalAlignment = Alignment.Trailing
lbAudio .horizontalAlignment = Alignment.Trailing
if (Prefs.useSensorMeters) {
ggSensors.focusable = false
// this is so it looks the same as the audio-boot button on OS X
ggSensors.peer.putClientProperty("JButton.buttonType", "bevel")
ggSensors.peer.putClientProperty("JComponent.sizeVariant", "small")
}
val p1 = lbSensors.preferredSize
val p2 = lbAudio .preferredSize
p1.width = math.max(p1.width, p2.width)
p2.width = p1.width
lbSensors.preferredSize = p1
lbAudio .preferredSize = p2
}
private[this] val ggDumpSensors: CheckBox = new CheckBox("Dump") {
listenTo(this)
reactions += {
case ButtonClicked(_) =>
val dumpMode = if (selected) osc.Dump.Text else osc.Dump.Off
step { implicit tx =>
sensorSystem.serverOption.foreach { s =>
// println(s"dump($dumpMode)")
s.dump(dumpMode)
}
}
}
enabled = false
}
private[this] val actionStartStopSensors: Action = new Action("Start") {
def apply(): Unit = {
val isRunning = step { implicit tx =>
sensorSystem.serverOption.isDefined
}
if (isRunning) stopSensorSystem() else Mellite.startSensorSystem()
}
}
private[this] val sensorServerPane = new BoxPanel(Orientation.Horizontal) {
contents += HStrut(4)
contents += lbSensors
contents += HStrut(4)
contents += new Button(actionStartStopSensors) {
focusable = false
}
contents += HStrut(16)
contents += ggDumpSensors
contents += HGlue
}
private[this] val audioServerPane = new ServerStatusPanel()
audioServerPane.bootAction = Some(() => {
Mellite.startAuralSystem()
// val tmp = new ServerSocket(0)
// val clientPort = tmp.getLocalPort
// tmp.close()
//
// Mellite.connectAuralSystem(
// new InetSocketAddress("klangpi01.local", 57110),
// Some(
// new InetSocketAddress("aleph.local", clientPort),
// )
// )
// ()
})
private[this] val boxPane = new BoxPanel(Orientation.Vertical)
if (Prefs.sensorChannels.getOrElse(0) > 0) boxPane.contents += sensorServerPane
boxPane.contents += new BoxPanel(Orientation.Horizontal) {
contents += HStrut(4)
contents += lbAudio
contents += HStrut(2)
contents += audioServerPane
contents += HGlue
}
resizable = false
contents = boxPane
private[this] val iconOnline = raphael.Icon(extent = 14)(raphael.Shapes.Firefox)
private class BrowseAction(uri: URI, text: String) extends Action(text) {
icon = iconOnline
def apply(): Unit = Desktop.browseURI(uri)
}
{
import de.sciss.desktop.Menu.{Group, Item}
val mf = handler.menuFactory
val gHelp = Group("help", "Help")
val itAbout = Item.About(Application)(About.show())
if (itAbout.visible) gHelp.add(itAbout)
val me = Some(this)
mf.get("file.new").foreach {
case g: Menu.Group =>
g.add(me, Item("repl", InterpreterFrame.Action))
case _ =>
}
gHelp
.add(Item("api")("API Documentation")(ApiBrowser.openBase(None)))
.add(Item("shortcuts")("Keyboard Shortcuts")(Help.shortcuts()))
.addLine()
.add(Item("index" , new BrowseAction(new URI(Mellite.homepage), "Online Documentation")))
.add(Item("issues", new BrowseAction(new URI(Mellite.issueTracker), "Report an Issue")))
.add(Item("chat" , new BrowseAction(new URI("https://gitter.im/Sciss/Mellite") , "Chat Room")))
// .add(Item("issues")("Report an Issue \u2197")(
// Desktop.browseURI(new URI("https://git.iem.at/sciss/Mellite/issues"))))
// .add(Item("chat")("Chat Room \u2197")(
// Desktop.browseURI(new URI("https://gitter.im/Sciss/Mellite"))))
mf.get("actions").foreach {
case g: Menu.Group =>
g.add(me, Menu.Item("server-tree" , ActionShowTree ))
g.add(me, Menu.Item("server-scope" , ActionScope ))
g.add(me, Menu.Item("server-freq-scope" , ActionFreqScope ))
g.add(me, Menu.Item("toggle-debug-log")("Toggle Debug Log")(toggleDebugLog()))
case _ =>
}
mf.add(me, gHelp)
}
private def toggleDebugLog(): Unit = {
val lvl = if (logTimeline.level == Level.Debug) Level.Off else Level.Debug
logTimeline .level = lvl
logAural .level = lvl
logTransport.level = lvl
}
private abstract class ActionScopeBase(scopeName: String) extends Action(s"Show $scopeName") {
enabled = false
protected def mkScope(server: Server)(implicit tx: RT): ScopeBase
def apply(): Unit = {
val scopeOpt = step { implicit tx =>
val sOpt = Mellite.auralSystem.serverOption
sOpt.map { server =>
val scope = mkScope(server)
// Oscilloscope(server)
// scope.bus = Bus.soundOut(server, math.min(2, server.config.outputBusChannels))
scope.start()
scope
}
}
scopeOpt.foreach { scope =>
new WindowImpl with NoMenuBarActions { frame =>
def handler: WindowHandler = Application.windowHandler
override protected def style: Window.Style = Window.Auxiliary
protected def undoRedoActions: Option[(Action, Action)] = None
private[this] val scopeC = Component.wrap(scope.component)
contents = scopeC
title = scopeName
closeOperation = Window.CloseIgnore // CloseDispose
initNoMenuBarActions(scopeC)
reactions += {
case Window.Closing(_) => handleClose()
}
protected def handleClose(): Unit = {
step { implicit tx =>
scope.dispose()
}
dispose()
}
pack()
desktop.Util.placeWindow(frame, horizontal = 1f, vertical = 0.125f, padding = 60)
front()
}
}
}
}
private object ActionScope extends ActionScopeBase("Oscilloscope") {
override protected def mkScope(server: Server)(implicit tx: RT): Oscilloscope = {
val scope = Oscilloscope(server)
scope.bus = Bus.soundOut(server, math.min(2, server.config.outputBusChannels))
scope
}
}
private object ActionFreqScope extends ActionScopeBase("Spectrum Analyzer") {
override protected def mkScope(server: Server)(implicit tx: RT): SpectrumAnalyzer = {
val scope = SpectrumAnalyzer(server)
scope.bus = Bus.soundOut(server, 1)
scope
}
}
private object ActionShowTree extends Action("Show Server Node Tree") {
enabled = false
def apply(): Unit = {
val sOpt = step { implicit tx => Mellite.auralSystem.serverOption }
sOpt.foreach { server =>
import de.sciss.synth.swing.Implicits._
server.peer.gui.tree()
}
}
}
private[this] val metersRef = Ref(List.empty[AudioBusMeter])
private[this] var onlinePane = Option.empty[Component]
private[this] val smallFont = new Font("SansSerif", Font.PLAIN, 9)
private def step[A](fun: RT => A): A = atomic { implicit itx =>
implicit val tx: RT = RT.wrap(itx)
fun(tx)
}
private[this] val synOpt = Ref(Option.empty[Synth])
private[this] var ggMainVolumeOpt = Option.empty[Slider]
def setMainVolume(linear: Double)(implicit tx: RT): Unit = {
synOpt.get(tx.peer).foreach { syn => syn.set("amp" -> linear) }
deferTx {
import de.sciss.numbers.Implicits._
ggMainVolumeOpt.foreach { slid =>
val db = math.min(18, math.max(-72, (linear.ampDb + 0.5).toInt))
slid.value = db
}
}
}
private def auralSystemStarted(s: Server)(implicit tx: RT): Unit = {
log.debug("MainFrame: AuralSystem started")
val numIns = s.peer.config.inputBusChannels
val numOuts = s.peer.config.outputBusChannels
val group = Group.play(s.defaultGroup, addAfter)
val mGroup = Group.play(group, addToHead)
val graph = SynthGraph {
import de.sciss.synth
import synth.Import._
import synth.Ops.stringToControl
import synth.ugen._
val in = In.ar(0, numOuts)
val mainAmp = Lag.ar("amp".kr(0f))
val mainIn = in * mainAmp
val ceil = -0.2.dbAmp
val mainLim = Limiter.ar(mainIn, level = ceil)
val lim = Lag.ar("limiter".kr(0f) * 2 - 1)
// we fade between plain signal and limited signal
// to allow for minimum latency when limiter is not used.
val mainOut = LinXFade2.ar(mainIn, mainLim, pan = lim)
val hpBusL = "hp-bus".kr(0f)
val hpBusR = hpBusL + 1
val hpAmp = Lag.ar("hp-amp".kr(0f))
val hpInL = Mix.tabulate((numOuts + 1) / 2)(i => in.out(i * 2))
def _hpInR = Mix.tabulate( numOuts / 2)(i => in.out(i * 2 + 1))
val hpInR = if (numOuts == 1) DC.ar(0) else _hpInR
val hpLimL = Limiter.ar(hpInL * hpAmp, level = ceil)
val hpLimR = Limiter.ar(hpInR * hpAmp, level = ceil)
val hpActive = Lag.ar("hp".kr(0f))
val out = (0 until numOuts).map { i =>
val isL = hpActive & (hpBusL sig_== i)
val isR = hpActive & (hpBusR sig_== i)
val isHP = isL | isR
(mainOut out i) * (1 - isHP) + hpLimL * isL + hpLimR * isR
}
ReplaceOut.ar(0, out)
}
val hpBus = Prefs.headphonesBus.getOrElse(Prefs.defaultHeadphonesBus)
val syn = Synth.playOnce(graph = graph, nameHint = Some("main"))(target = group,
addAction = addToTail, args = List("hp-bus" -> hpBus), dependencies = Nil)
synOpt.set(Some(syn))(tx.peer)
val meters = if (!Prefs.useAudioMeters) List.empty else {
val res0 = if (numOuts == 0) Nil else {
val outBus = Bus.soundOut(s, numOuts )
val mOut = AudioBusMeter(AudioBusMeter.Strip(outBus, mGroup, addToHead) :: Nil)
deferTx {
mOut.component.tooltip = "Output Levels" // as long as we do not have an explicit label component
}
mOut :: Nil
}
if (numIns == 0) res0 else {
val inBus = Bus.soundIn(s, numIns)
val mIn = AudioBusMeter(AudioBusMeter.Strip(inBus, s.defaultGroup, addBefore) :: Nil)
deferTx {
mIn.component.tooltip = "Input Levels" // as long as we do not have an explicit label component
}
mIn :: res0
}
}
metersRef.set(meters)(tx.peer)
deferTx {
mkAuralGUI(s = s, syn = syn, meters = meters, group = group, mGroup = mGroup)
}
}
// group -- main group; mGroup -- metering group
private def mkAuralGUI(s: Server, syn: Synth, meters: List[AudioBusMeter], group: Group, mGroup: Group): Unit = {
ActionShowTree .enabled = true
ActionScope .enabled = true
ActionFreqScope .enabled = true
audioServerPane.server = Some(s.peer)
val p = new FlowPanel() // new BoxPanel(Orientation.Horizontal)
meters.foreach { m =>
p.contents += m.component
p.contents += HStrut(8)
}
def mkAmpFader(ctl: String, prefs: Preferences.Entry[Int]): Slider = mkFader(prefs, 0) { db =>
import de.sciss.numbers.Implicits._
val amp = if (db == -72) 0f else db.dbAmp
step { implicit tx => syn.set(ctl -> amp) }
}
val ggMainVolume = mkAmpFader("amp" , Prefs.audioMainVolume)
val ggHPVolume = mkAmpFader("hp-amp", Prefs.headphonesVolume )
ggMainVolumeOpt = Some(ggMainVolume)
def mkToggle(label: String, prefs: Preferences.Entry[Boolean], default: Boolean = false)
(fun: Boolean => Unit): ToggleButton = {
val res = new ToggleButton
res.action = Action(label) {
val v = res.selected
fun(v)
prefs.put(v)
}
res.peer.putClientProperty("JComponent.sizeVariant", "mini")
res.peer.putClientProperty("JButton.buttonType", "square")
val v0 = prefs.getOrElse(default)
res.selected = v0
res.focusable = false
fun(v0)
res
}
val ggPost = mkToggle("post", Prefs.audioMainPostMeter) { post =>
step { implicit tx =>
if (post) mGroup.moveToTail(group) else mGroup.moveToHead(group)
}
}
val ggLim = mkToggle("limiter", Prefs.audioMainLimiter) { lim =>
val on = if (lim) 1f else 0f
step { implicit tx => syn.set("limiter" -> on) }
}
val ggHPActive = mkToggle("active", Prefs.headphonesActive) { active =>
val on = if (active) 1f else 0f
step { implicit tx => syn.set("hp" -> on) }
}
def mkBorder(comp: Component, label: String): Unit = {
val res = TitledBorder(LineBorder(Color.gray), label)
res.setTitleFont(smallFont)
res.setTitleColor(comp.foreground)
res.setTitleJustification(javax.swing.border.TitledBorder.CENTER)
comp.border = res
}
val stripMain = new BoxPanel(Orientation.Vertical) {
contents += ggPost
contents += ggLim
contents += ggMainVolume
mkBorder(this, "Main")
}
val stripHP = new BoxPanel(Orientation.Vertical) {
contents += VStrut(ggPost.preferredSize.height)
contents += ggHPActive
contents += ggHPVolume
mkBorder(this, "Phones")
}
p.contents += stripMain
p.contents += stripHP
p.contents += HGlue
onlinePane = Some(p)
boxPane.contents += p
// resizable = true
pack()
}
private def mkFader(prefs: Preferences.Entry[Int], default: Int)(fun: Int => Unit): Slider = {
val zeroMark = "0\u25C0"
val lbMap: Map[Int, Label] = (-72 to 18 by 12).iterator.map { dec =>
val txt = if (dec == -72) "-\u221E" else if (dec == 0) zeroMark else dec.toString
val lb = new Label(txt)
lb.font = smallFont
(dec, lb)
} .toMap
val lbZero = lbMap(0)
val sl: Slider = new Slider {
orientation = Orientation.Vertical
min = -72
max = 18
value = prefs.getOrElse(default)
minorTickSpacing = 3
majorTickSpacing = 12
paintTicks = true
paintLabels = true
private var isZero = true // will be initialized
peer.putClientProperty("JComponent.sizeVariant", "small")
peer.putClientProperty("JSlider.isFilled", true) // used by Metal-lnf
labels = lbMap
private def perform(store: Boolean): Unit = {
val v = value
fun(v)
if (isZero) {
if (v != 0) {
isZero = false
lbZero.text = "0"
repaint()
}
} else {
if (v == 0) {
isZero = true
lbZero.text = zeroMark
repaint()
}
}
if (store) prefs.put(v)
}
listenTo(this)
reactions += {
case ValueChanged(_) => perform(store = true)
}
perform(store = false)
}
sl
}
private def auralSystemStopped()(implicit tx: RT): Unit = {
log.debug("MainFrame: AuralSystem stopped")
metersRef .swap(Nil)(tx.peer).foreach(_.dispose())
synOpt.swap(None)(tx.peer)
deferTx {
ActionShowTree .enabled = false
ActionScope .enabled = false
ActionFreqScope .enabled = false
audioServerPane.server = None
onlinePane.foreach { p =>
onlinePane = None
boxPane.contents.remove(boxPane.contents.indexOf(p))
// resizable = false
pack()
}
}
}
def stopSensorSystem(): Unit =
step { implicit tx =>
sensorSystem.stop()
}
private def sensorSystemStarted(/*s: SensorSystem.Server*/)(implicit tx: TxnLike): Unit = {
log.debug("MainFrame: SensorSystem started")
deferTx {
actionStartStopSensors.title = "Stop"
ggDumpSensors.enabled = true
if (Prefs.useSensorMeters) {
ggSensors.numChannels = Prefs.sensorChannels.getOrElse(Prefs.defaultSensorChannels)
ggSensors.preferredSize = (260, ggSensors.numChannels * 4 + 2)
sensorServerPane.contents += ggSensors
}
pack()
}
}
private def sensorSystemStopped()(implicit tx: TxnLike): Unit = {
log.debug("MainFrame: SensorSystem stopped")
deferTx {
ggMainVolumeOpt = None
actionStartStopSensors.title = "Start"
ggDumpSensors.enabled = false
ggDumpSensors.selected = false
if (Prefs.useSensorMeters) {
sensorServerPane.contents.remove(sensorServerPane.contents.size - 1)
}
pack()
}
}
private def updateSensorMeter(value: Vec[Float]): Unit = {
val b = Vec.newBuilder[Float]
b.sizeHint(32)
value.foreach { peak =>
import de.sciss.numbers.Implicits._
b += peak.pow(0.65f).linExp(0f, 1f, 0.99e-3f, 1f) // XXX TODO roughly linearized
b += 0f
}
ggSensors.clearMeter() // XXX TODO: should have option to switch off ballistics
ggSensors.update(b.result())
}
step { implicit tx =>
auralSystem.react { implicit tx => {
case Running(s) => me.auralSystemStarted(s)
case Stopped => me.auralSystemStopped()
case _ =>
}}
sensorSystem.addClient(new SensorSystem.Client {
def sensorsStarted(s: SensorSystem.Server)(implicit tx: TxnLike): Unit = me.sensorSystemStarted()
def sensorsStopped() (implicit tx: TxnLike): Unit = me.sensorSystemStopped()
def sensorsUpdate(values: Vec[Float]) (implicit tx: TxnLike): Unit =
if (Prefs.useSensorMeters) deferTx(updateSensorMeter(values))
})
}
// XXX TODO: removeClient
title = Application.name
closeOperation = Window.CloseIgnore
reactions += {
case Window.Closing(_) => Desktop.mayQuit().foreach(_ => Application.quit())
}
pack()
front()
if (Prefs.audioAutoBoot .getOrElse(false)) Mellite.startAuralSystem()
if (Prefs.sensorAutoStart.getOrElse(false)) Mellite.startSensorSystem()
}