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.
This commit is contained in:
Fingercomp 2025-08-04 01:06:57 +03:00
parent d1d2deddff
commit 4bcdebedfa
No known key found for this signature in database
GPG Key ID: BBC71CEE45D86E37
7 changed files with 84 additions and 19 deletions

View File

@ -62,6 +62,8 @@ object Audio extends Logging {
sources.put(source, sourceId) sources.put(source, sourceId)
logger.debug(s"created new source $sourceId for $source")
sourceId sourceId
} }
} }
@ -70,6 +72,7 @@ object Audio extends Logging {
val bufferId = samples.genBuffer().getOrElse { tx.abort() } val bufferId = samples.genBuffer().getOrElse { tx.abort() }
tx.onFailure { AL10W.alDeleteBuffers(bufferId) } tx.onFailure { AL10W.alDeleteBuffers(bufferId) }
logger.debug(s"enqueued buffer $bufferId for source $sourceId ($source)", new Exception())
AL10W.alSourceQueueBuffers(sourceId, bufferId) AL10W.alSourceQueueBuffers(sourceId, bufferId)
if (AL10W.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) != AL10.AL_PLAYING) { if (AL10W.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) != AL10.AL_PLAYING) {
@ -226,6 +229,7 @@ object Audio extends Logging {
@throws[OpenAlException] @throws[OpenAlException]
private def deleteSource(sourceId: Int): Unit = { private def deleteSource(sourceId: Int): Unit = {
logger.debug(s"deleting source $sourceId")
AL10W.alSourceStop(sourceId) AL10W.alSourceStop(sourceId)
cleanupSourceBuffers(sourceId) cleanupSourceBuffers(sourceId)
AL10W.alDeleteSources(sourceId) AL10W.alDeleteSources(sourceId)

View File

@ -1,13 +1,14 @@
package ocelot.desktop.inventory package ocelot.desktop.inventory
import ocelot.desktop.inventory.Inventory.SlotObserver 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 totoro.ocelot.brain.event.NodeEvent
import scala.collection.mutable import scala.collection.mutable
/** Provides an inventory — a collection of [[Item]]s indexed by slots. */ /** 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 // parallels totoro.ocelot.brain.entity.traits.Inventory
// this is intentional // this is intentional
@ -20,6 +21,16 @@ trait Inventory extends EventAware {
private val itemSlots = mutable.HashMap.empty[I, Int] private val itemSlots = mutable.HashMap.empty[I, Int]
private val observers = mutable.HashMap.empty[Int, WeakHashSet[SlotObserver]] 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. /** Called after a new item is added to the inventory.
* *
* @param slot the slot the item was added to * @param slot the slot the item was added to
@ -72,7 +83,7 @@ trait Inventory extends EventAware {
for (slot <- inventoryIterator; item <- slot.get) { for (slot <- inventoryIterator; item <- slot.get) {
event match { 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) case _ => item.handleEvent(event)
} }
} }
@ -148,7 +159,6 @@ trait Inventory extends EventAware {
} }
final object Slot { final object Slot {
/** Creates a proxy to an inventory slot. */ /** Creates a proxy to an inventory slot. */
def apply(index: Int) = new Slot(index) def apply(index: Int) = new Slot(index)
} }

View File

@ -1,22 +1,50 @@
package ocelot.desktop.inventory.item package ocelot.desktop.inventory.item
import ocelot.desktop.audio.{Audio, SoundCategory, SoundSamples, SoundSource, SoundStream}
import ocelot.desktop.graphics.IconSource import ocelot.desktop.graphics.IconSource
import ocelot.desktop.inventory.traits.{CardItem, ComponentItem, PersistableItem} import ocelot.desktop.inventory.traits.{CardItem, ComponentItem, PersistableItem}
import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer} 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.card.SoundCardWindow
import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry} import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
import ocelot.desktop.ui.widget.window.Windowed 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.sound_card.SoundCard
import totoro.ocelot.brain.entity.traits.{Entity, Environment} 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
import totoro.ocelot.brain.util.Tier.Tier import totoro.ocelot.brain.util.Tier.Tier
class SoundCardItem(val soundCard: SoundCard) 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 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 = { override def fillRmbMenu(menu: ContextMenu): Unit = {
menu.addEntry(ContextMenuEntry("Open card interface", IconSource.Window) { menu.addEntry(ContextMenuEntry("Open card interface", IconSource.Window) {

View File

@ -11,7 +11,6 @@ import ocelot.desktop.ui.event.handlers.DiskActivityHandler
import ocelot.desktop.ui.particle.Particle import ocelot.desktop.ui.particle.Particle
import ocelot.desktop.util.Messages import ocelot.desktop.util.Messages
import ocelot.desktop.{ColorScheme, Settings => DesktopSettings} import ocelot.desktop.{ColorScheme, Settings => DesktopSettings}
import totoro.ocelot.brain.Settings
import totoro.ocelot.brain.entity.traits.{Entity, Environment, WorkspaceAware} import totoro.ocelot.brain.entity.traits.{Entity, Environment, WorkspaceAware}
import totoro.ocelot.brain.event._ import totoro.ocelot.brain.event._
@ -25,10 +24,6 @@ abstract class ComputerAwareNode(entity: Entity with Environment with WorkspaceA
with BoomCardFxHandler with BoomCardFxHandler
with ShiftClickNode { 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 += { eventHandlers += {
case BrainEvent(event: MachineCrashEvent) => case BrainEvent(event: MachineCrashEvent) =>
val message = Messages.lift(event.message) match { 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 => case BrainEvent(event: BeepPatternEvent) if !Audio.isDisabled =>
BeepGenerator.newBeep(event.pattern, 1000, 200).play() 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 { protected def drawOverlay(g: Graphics): Unit = HolidayIcon match {

View File

@ -1,9 +1,9 @@
package ocelot.desktop.ui.widget.window package ocelot.desktop.ui.widget.window
import ocelot.desktop.util.Persistable import ocelot.desktop.util.{Disposable, Persistable}
import totoro.ocelot.brain.nbt.NBTTagCompound import totoro.ocelot.brain.nbt.NBTTagCompound
trait Windowed[T <: Window] extends Persistable { trait Windowed[T <: Window] extends Persistable with Disposable {
protected def createWindow(): T protected def createWindow(): T
private var _window: Option[T] = None private var _window: Option[T] = None
@ -33,6 +33,10 @@ trait Windowed[T <: Window] extends Persistable {
window window
} }
override def dispose(): Unit = {
super.dispose()
closeAndDisposeWindow()
}
// ------------------------------- NBT ------------------------------- // ------------------------------- NBT -------------------------------
protected def windowNBTKey: String = "window" protected def windowNBTKey: String = "window"

View File

@ -17,7 +17,13 @@ import totoro.ocelot.brain.util.Tier.Tier
import scala.math.Ordering.Implicits.infixOrderingOps import scala.math.Ordering.Implicits.infixOrderingOps
import scala.reflect.ClassTag 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 override type I = Item with EntityItem
def computer: Computer with TieredPersistable def computer: Computer with TieredPersistable

View File

@ -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)
}