From 4bcdebedfa3f042a1fffb9971755efb8257776ee Mon Sep 17 00:00:00 2001 From: Fingercomp Date: Mon, 4 Aug 2025 01:06:57 +0300 Subject: [PATCH] Have each sound card create its own sound stream In addition to that, fixes a long-standing bug concerning brain event dispatch to inventory items: items could receive events coming from a component with a different address. Fixes #175. --- .../scala/ocelot/desktop/audio/Audio.scala | 4 +++ .../ocelot/desktop/inventory/Inventory.scala | 18 ++++++++--- .../inventory/item/SoundCardItem.scala | 32 +++++++++++++++++-- .../desktop/node/ComputerAwareNode.scala | 10 ------ .../desktop/ui/widget/window/Windowed.scala | 8 +++-- .../ocelot/desktop/util/ComputerAware.scala | 8 ++++- src/main/scala/ocelot/desktop/util/Lazy.scala | 23 +++++++++++++ 7 files changed, 84 insertions(+), 19 deletions(-) create mode 100644 src/main/scala/ocelot/desktop/util/Lazy.scala diff --git a/src/main/scala/ocelot/desktop/audio/Audio.scala b/src/main/scala/ocelot/desktop/audio/Audio.scala index 7bfce80..951d7f1 100644 --- a/src/main/scala/ocelot/desktop/audio/Audio.scala +++ b/src/main/scala/ocelot/desktop/audio/Audio.scala @@ -62,6 +62,8 @@ object Audio extends Logging { sources.put(source, sourceId) + logger.debug(s"created new source $sourceId for $source") + sourceId } } @@ -70,6 +72,7 @@ object Audio extends Logging { val bufferId = samples.genBuffer().getOrElse { tx.abort() } tx.onFailure { AL10W.alDeleteBuffers(bufferId) } + logger.debug(s"enqueued buffer $bufferId for source $sourceId ($source)", new Exception()) AL10W.alSourceQueueBuffers(sourceId, bufferId) if (AL10W.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) != AL10.AL_PLAYING) { @@ -226,6 +229,7 @@ object Audio extends Logging { @throws[OpenAlException] private def deleteSource(sourceId: Int): Unit = { + logger.debug(s"deleting source $sourceId") AL10W.alSourceStop(sourceId) cleanupSourceBuffers(sourceId) AL10W.alDeleteSources(sourceId) diff --git a/src/main/scala/ocelot/desktop/inventory/Inventory.scala b/src/main/scala/ocelot/desktop/inventory/Inventory.scala index bd9e1dc..374b563 100644 --- a/src/main/scala/ocelot/desktop/inventory/Inventory.scala +++ b/src/main/scala/ocelot/desktop/inventory/Inventory.scala @@ -1,13 +1,14 @@ package ocelot.desktop.inventory import ocelot.desktop.inventory.Inventory.SlotObserver -import ocelot.desktop.ui.event.{Event, EventAware} +import ocelot.desktop.ui.event.{BrainEvent, Event, EventAware} +import ocelot.desktop.util.Disposable import totoro.ocelot.brain.event.NodeEvent import scala.collection.mutable /** Provides an inventory — a collection of [[Item]]s indexed by slots. */ -trait Inventory extends EventAware { +trait Inventory extends EventAware with Disposable { // parallels totoro.ocelot.brain.entity.traits.Inventory // this is intentional @@ -20,6 +21,16 @@ trait Inventory extends EventAware { private val itemSlots = mutable.HashMap.empty[I, Int] private val observers = mutable.HashMap.empty[Int, WeakHashSet[SlotObserver]] + override def dispose(): Unit = { + super.dispose() + + for (slot <- inventoryIterator) { + val item = slot.get + slot.remove() + item.foreach(_.dispose()) + } + } + /** Called after a new item is added to the inventory. * * @param slot the slot the item was added to @@ -72,7 +83,7 @@ trait Inventory extends EventAware { for (slot <- inventoryIterator; item <- slot.get) { event match { - case n: NodeEvent if !item.shouldReceiveEventsFor(n.address) => // ignore + case BrainEvent(e: NodeEvent) if !item.shouldReceiveEventsFor(e.address) => // ignore case _ => item.handleEvent(event) } } @@ -148,7 +159,6 @@ trait Inventory extends EventAware { } final object Slot { - /** Creates a proxy to an inventory slot. */ def apply(index: Int) = new Slot(index) } diff --git a/src/main/scala/ocelot/desktop/inventory/item/SoundCardItem.scala b/src/main/scala/ocelot/desktop/inventory/item/SoundCardItem.scala index a473d2a..82c6c92 100644 --- a/src/main/scala/ocelot/desktop/inventory/item/SoundCardItem.scala +++ b/src/main/scala/ocelot/desktop/inventory/item/SoundCardItem.scala @@ -1,22 +1,50 @@ package ocelot.desktop.inventory.item +import ocelot.desktop.audio.{Audio, SoundCategory, SoundSamples, SoundSource, SoundStream} import ocelot.desktop.graphics.IconSource import ocelot.desktop.inventory.traits.{CardItem, ComponentItem, PersistableItem} import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer} +import ocelot.desktop.ui.event.BrainEvent import ocelot.desktop.ui.widget.card.SoundCardWindow import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry} import ocelot.desktop.ui.widget.window.Windowed +import ocelot.desktop.util.Lazy +import totoro.ocelot.brain.Settings import totoro.ocelot.brain.entity.sound_card.SoundCard import totoro.ocelot.brain.entity.traits.{Entity, Environment} +import totoro.ocelot.brain.event.SoundCardAudioEvent import totoro.ocelot.brain.util.Tier import totoro.ocelot.brain.util.Tier.Tier class SoundCardItem(val soundCard: SoundCard) - extends Item with ComponentItem with PersistableItem with CardItem with Windowed[SoundCardWindow] { + extends Item + with ComponentItem + with PersistableItem + with CardItem + with Windowed[SoundCardWindow] { override def createWindow(): SoundCardWindow = new SoundCardWindow(soundCard) - override def entity: Entity with Environment = soundCard + override def entity: SoundCard = soundCard + + private val streamPair = Lazy(Audio.newStream(SoundCategory.Records)) + private def stream: SoundStream = streamPair.getSync._1 + private def source: SoundSource = streamPair.getSync._2 + + eventHandlers += { + case BrainEvent(event: SoundCardAudioEvent) if !Audio.isDisabled => + val samples = SoundSamples(event.data, Settings.get.soundCardSampleRate, SoundSamples.Format.Mono8) + stream.enqueue(samples) + source.volume = event.volume + } + + override def dispose(): Unit = { + super.dispose() + + for (stream <- streamPair.getOption) { + stream._2.stop() + } + } override def fillRmbMenu(menu: ContextMenu): Unit = { menu.addEntry(ContextMenuEntry("Open card interface", IconSource.Window) { diff --git a/src/main/scala/ocelot/desktop/node/ComputerAwareNode.scala b/src/main/scala/ocelot/desktop/node/ComputerAwareNode.scala index 6946fd4..c12da38 100644 --- a/src/main/scala/ocelot/desktop/node/ComputerAwareNode.scala +++ b/src/main/scala/ocelot/desktop/node/ComputerAwareNode.scala @@ -11,7 +11,6 @@ import ocelot.desktop.ui.event.handlers.DiskActivityHandler import ocelot.desktop.ui.particle.Particle import ocelot.desktop.util.Messages import ocelot.desktop.{ColorScheme, Settings => DesktopSettings} -import totoro.ocelot.brain.Settings import totoro.ocelot.brain.entity.traits.{Entity, Environment, WorkspaceAware} import totoro.ocelot.brain.event._ @@ -25,10 +24,6 @@ abstract class ComputerAwareNode(entity: Entity with Environment with WorkspaceA with BoomCardFxHandler with ShiftClickNode { - private lazy val soundCardSounds: (SoundStream, SoundSource) = Audio.newStream(SoundCategory.Records) - private def soundCardStream: SoundStream = soundCardSounds._1 - private def soundCardSource: SoundSource = soundCardSounds._2 - eventHandlers += { case BrainEvent(event: MachineCrashEvent) => val message = Messages.lift(event.message) match { @@ -46,11 +41,6 @@ abstract class ComputerAwareNode(entity: Entity with Environment with WorkspaceA case BrainEvent(event: BeepPatternEvent) if !Audio.isDisabled => BeepGenerator.newBeep(event.pattern, 1000, 200).play() - - case BrainEvent(event: SoundCardAudioEvent) if !Audio.isDisabled => - val samples = SoundSamples(event.data, Settings.get.soundCardSampleRate, SoundSamples.Format.Mono8) - soundCardStream.enqueue(samples) - soundCardSource.volume = event.volume } protected def drawOverlay(g: Graphics): Unit = HolidayIcon match { diff --git a/src/main/scala/ocelot/desktop/ui/widget/window/Windowed.scala b/src/main/scala/ocelot/desktop/ui/widget/window/Windowed.scala index c0c4828..3983705 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/window/Windowed.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/window/Windowed.scala @@ -1,9 +1,9 @@ package ocelot.desktop.ui.widget.window -import ocelot.desktop.util.Persistable +import ocelot.desktop.util.{Disposable, Persistable} import totoro.ocelot.brain.nbt.NBTTagCompound -trait Windowed[T <: Window] extends Persistable { +trait Windowed[T <: Window] extends Persistable with Disposable { protected def createWindow(): T private var _window: Option[T] = None @@ -33,6 +33,10 @@ trait Windowed[T <: Window] extends Persistable { window } + override def dispose(): Unit = { + super.dispose() + closeAndDisposeWindow() + } // ------------------------------- NBT ------------------------------- protected def windowNBTKey: String = "window" diff --git a/src/main/scala/ocelot/desktop/util/ComputerAware.scala b/src/main/scala/ocelot/desktop/util/ComputerAware.scala index ad14e00..3bcc748 100644 --- a/src/main/scala/ocelot/desktop/util/ComputerAware.scala +++ b/src/main/scala/ocelot/desktop/util/ComputerAware.scala @@ -17,7 +17,13 @@ import totoro.ocelot.brain.util.Tier.Tier import scala.math.Ordering.Implicits.infixOrderingOps import scala.reflect.ClassTag -trait ComputerAware extends Logging with SyncedInventory with DefaultSlotItemsFillable with Windowed[ComputerWindow] { +trait ComputerAware + extends Logging + with SyncedInventory + with DefaultSlotItemsFillable + with Windowed[ComputerWindow] + with Disposable { + override type I = Item with EntityItem def computer: Computer with TieredPersistable diff --git a/src/main/scala/ocelot/desktop/util/Lazy.scala b/src/main/scala/ocelot/desktop/util/Lazy.scala new file mode 100644 index 0000000..9911e3b --- /dev/null +++ b/src/main/scala/ocelot/desktop/util/Lazy.scala @@ -0,0 +1,23 @@ +package ocelot.desktop.util + +class Lazy[A](init: () => A) { + private var _value = Option.empty[A] + + def get: A = _value match { + case Some(value) => value + + case None => + val v = init() + _value = Some(v) + + v + } + + def getSync: A = synchronized(get) + + def getOption: Option[A] = _value +} + +object Lazy { + def apply[A](init: => A): Lazy[A] = new Lazy(() => init) +}