diff --git a/lib/ocelot-brain b/lib/ocelot-brain index e7b477c..7b2f4ce 160000 --- a/lib/ocelot-brain +++ b/lib/ocelot-brain @@ -1 +1 @@ -Subproject commit e7b477cf4d369d91132275237e72c80ad5440e40 +Subproject commit 7b2f4ce827d79a7c231bf70c76c28469b705bb9e diff --git a/sprites/items/SoundCard.png b/sprites/items/SoundCard.png new file mode 100644 index 0000000..b62c7db Binary files /dev/null and b/sprites/items/SoundCard.png differ diff --git a/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.png b/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.png index 23faa56..46d2d7a 100644 Binary files a/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.png and b/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.png differ diff --git a/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.txt b/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.txt index 7716500..c66d46a 100644 --- a/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.txt +++ b/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.txt @@ -1,113 +1,114 @@ BackgroundPattern 0 0 304 304 -BarSegment 493 105 16 4 +BarSegment 488 51 16 4 ComputerMotherboard 305 0 79 70 Empty 9 316 1 1 -EmptySlot 441 147 18 18 +EmptySlot 458 147 18 18 Knob 385 0 50 50 KnobCenter 436 0 50 50 KnobLimits 305 71 50 50 ShadowBorder 0 305 1 24 -ShadowCorner 424 122 24 24 -TabArrow 458 105 8 14 -buttons/BottomDrawerClose 460 147 18 18 -buttons/BottomDrawerOpen 479 147 18 18 -buttons/PowerOff 424 180 18 18 -buttons/PowerOn 443 180 18 18 -icons/ButtonCheck 424 199 17 17 -icons/ButtonClipboard 442 199 17 17 -icons/ButtonRandomize 460 199 17 17 -icons/CPU 478 199 16 16 -icons/Card 495 199 16 16 -icons/ComponentBus 449 122 16 16 -icons/DragLMB 462 180 21 14 -icons/DragRMB 484 180 21 14 -icons/EEPROM 466 122 16 16 -icons/Floppy 483 122 16 16 -icons/HDD 356 219 16 16 -icons/LMB 500 0 11 14 -icons/Memory 373 219 16 16 -icons/NA 390 219 16 16 -icons/NotificationError 499 18 11 11 -icons/NotificationInfo 487 33 11 11 -icons/NotificationWarning 499 33 11 11 -icons/RMB 487 18 11 14 -icons/RackMountable 407 219 16 16 -icons/SettingsSound 498 147 12 17 -icons/SettingsUI 487 0 12 17 -icons/Tier0 424 219 16 16 -icons/Tier1 441 219 16 16 -icons/Tier2 458 219 16 16 -items/APU0 356 122 16 96 -items/APU1 373 122 16 96 -items/APU2 390 122 16 96 -items/CPU0 475 219 16 16 -items/CPU1 492 219 16 16 -items/CPU2 305 251 16 16 -items/CardBase 322 251 16 16 -items/CircuitBoard 339 251 16 16 -items/ComponentBus0 356 251 16 16 -items/ComponentBus1 373 251 16 16 -items/ComponentBus2 390 251 16 16 -items/ComponentBus3 407 251 16 16 +ShadowCorner 441 122 24 24 +TabArrow 502 122 8 14 +buttons/BottomDrawerClose 477 147 18 18 +buttons/BottomDrawerOpen 441 180 18 18 +buttons/PowerOff 460 180 18 18 +buttons/PowerOn 479 180 18 18 +icons/ButtonCheck 466 122 17 17 +icons/ButtonClipboard 484 122 17 17 +icons/ButtonRandomize 373 219 17 17 +icons/CPU 391 219 16 16 +icons/Card 408 219 16 16 +icons/ComponentBus 425 219 16 16 +icons/DragLMB 441 199 21 14 +icons/DragRMB 463 199 21 14 +icons/EEPROM 442 219 16 16 +icons/Floppy 459 219 16 16 +icons/HDD 476 219 16 16 +icons/LMB 402 51 11 14 +icons/Memory 493 219 16 16 +icons/NA 305 251 16 16 +icons/NotificationError 426 51 11 11 +icons/NotificationInfo 438 51 11 11 +icons/NotificationWarning 450 51 11 11 +icons/RMB 414 51 11 14 +icons/RackMountable 322 251 16 16 +icons/SettingsSound 498 180 12 17 +icons/SettingsUI 496 147 12 17 +icons/Tier0 339 251 16 16 +icons/Tier1 356 251 16 16 +icons/Tier2 373 251 16 16 +items/APU0 373 122 16 96 +items/APU1 390 122 16 96 +items/APU2 407 122 16 96 +items/CPU0 390 251 16 16 +items/CPU1 407 251 16 16 +items/CPU2 424 251 16 16 +items/CardBase 441 251 16 16 +items/CircuitBoard 458 251 16 16 +items/ComponentBus0 475 251 16 16 +items/ComponentBus1 492 251 16 16 +items/ComponentBus2 305 268 16 16 +items/ComponentBus3 322 268 16 16 items/DataCard0 305 122 16 128 items/DataCard1 322 122 16 128 items/DataCard2 339 122 16 128 -items/DebugCard 424 251 16 16 -items/DiskDriveMountable 441 251 16 16 -items/EEPROM 458 251 16 16 -items/FloppyDisk_dyeBlack 475 251 16 16 -items/FloppyDisk_dyeBlue 492 251 16 16 -items/FloppyDisk_dyeBrown 305 268 16 16 -items/FloppyDisk_dyeCyan 322 268 16 16 -items/FloppyDisk_dyeGray 339 268 16 16 -items/FloppyDisk_dyeGreen 356 268 16 16 -items/FloppyDisk_dyeLightBlue 373 268 16 16 -items/FloppyDisk_dyeLightGray 390 268 16 16 -items/FloppyDisk_dyeLime 407 268 16 16 -items/FloppyDisk_dyeMagenta 424 268 16 16 -items/FloppyDisk_dyeOrange 441 268 16 16 -items/FloppyDisk_dyePink 458 268 16 16 -items/FloppyDisk_dyePurple 475 268 16 16 -items/FloppyDisk_dyeRed 492 268 16 16 -items/FloppyDisk_dyeWhite 305 285 16 16 -items/FloppyDisk_dyeYellow 322 285 16 16 -items/GraphicsCard0 339 285 16 16 -items/GraphicsCard1 356 285 16 16 -items/GraphicsCard2 373 285 16 16 -items/HardDiskDrive0 390 285 16 16 -items/HardDiskDrive1 407 285 16 16 -items/HardDiskDrive2 424 285 16 16 -items/InternetCard 424 147 16 32 -items/LinkedCard 407 122 16 96 -items/Memory0 441 285 16 16 -items/Memory1 458 285 16 16 -items/Memory2 475 285 16 16 -items/Memory3 492 285 16 16 -items/Memory4 356 71 16 16 -items/Memory5 373 71 16 16 -items/NetworkCard 390 71 16 16 -items/RedstoneCard0 407 71 16 16 -items/RedstoneCard1 424 71 16 16 -items/Server0 441 71 16 16 -items/Server1 458 71 16 16 -items/Server2 475 71 16 16 -items/Server3 492 71 16 16 -items/WirelessNetworkCard0 356 88 16 16 -items/WirelessNetworkCard1 373 88 16 16 -nodes/Cable 356 236 8 8 -nodes/Computer 390 88 16 16 -nodes/ComputerActivityOverlay 407 88 16 16 -nodes/ComputerErrorOverlay 424 88 16 16 -nodes/ComputerOnOverlay 441 88 16 16 -nodes/DiskDrive 458 88 16 16 -nodes/DiskDriveActivity 475 88 16 16 -nodes/DiskDriveFloppy 492 88 16 16 -nodes/IronNoteBlock 356 105 16 16 -nodes/NewNode 373 105 16 16 -nodes/NoteBlock 390 105 16 16 -nodes/Relay 407 105 16 16 -nodes/Screen 424 105 16 16 -nodes/ScreenOnOverlay 441 105 16 16 +items/DebugCard 339 268 16 16 +items/DiskDriveMountable 356 268 16 16 +items/EEPROM 373 268 16 16 +items/FloppyDisk_dyeBlack 390 268 16 16 +items/FloppyDisk_dyeBlue 407 268 16 16 +items/FloppyDisk_dyeBrown 424 268 16 16 +items/FloppyDisk_dyeCyan 441 268 16 16 +items/FloppyDisk_dyeGray 458 268 16 16 +items/FloppyDisk_dyeGreen 475 268 16 16 +items/FloppyDisk_dyeLightBlue 492 268 16 16 +items/FloppyDisk_dyeLightGray 305 285 16 16 +items/FloppyDisk_dyeLime 322 285 16 16 +items/FloppyDisk_dyeMagenta 339 285 16 16 +items/FloppyDisk_dyeOrange 356 285 16 16 +items/FloppyDisk_dyePink 373 285 16 16 +items/FloppyDisk_dyePurple 390 285 16 16 +items/FloppyDisk_dyeRed 407 285 16 16 +items/FloppyDisk_dyeWhite 424 285 16 16 +items/FloppyDisk_dyeYellow 441 285 16 16 +items/GraphicsCard0 458 285 16 16 +items/GraphicsCard1 475 285 16 16 +items/GraphicsCard2 492 285 16 16 +items/HardDiskDrive0 356 71 16 16 +items/HardDiskDrive1 373 71 16 16 +items/HardDiskDrive2 390 71 16 16 +items/InternetCard 441 147 16 32 +items/LinkedCard 424 122 16 96 +items/Memory0 407 71 16 16 +items/Memory1 424 71 16 16 +items/Memory2 441 71 16 16 +items/Memory3 458 71 16 16 +items/Memory4 475 71 16 16 +items/Memory5 492 71 16 16 +items/NetworkCard 356 88 16 16 +items/RedstoneCard0 373 88 16 16 +items/RedstoneCard1 390 88 16 16 +items/Server0 407 88 16 16 +items/Server1 424 88 16 16 +items/Server2 441 88 16 16 +items/Server3 458 88 16 16 +items/SoundCard 356 122 16 128 +items/WirelessNetworkCard0 475 88 16 16 +items/WirelessNetworkCard1 492 88 16 16 +nodes/Cable 373 237 8 8 +nodes/Computer 356 105 16 16 +nodes/ComputerActivityOverlay 373 105 16 16 +nodes/ComputerErrorOverlay 390 105 16 16 +nodes/ComputerOnOverlay 407 105 16 16 +nodes/DiskDrive 424 105 16 16 +nodes/DiskDriveActivity 441 105 16 16 +nodes/DiskDriveFloppy 458 105 16 16 +nodes/IronNoteBlock 475 105 16 16 +nodes/NewNode 492 105 16 16 +nodes/NoteBlock 487 0 16 16 +nodes/Relay 487 17 16 16 +nodes/Screen 487 34 16 16 +nodes/ScreenOnOverlay 385 51 16 16 panel/BorderB 8 305 4 4 panel/BorderL 63 305 4 2 panel/BorderR 13 305 4 4 @@ -117,16 +118,16 @@ panel/CornerBR 28 305 4 4 panel/CornerTL 33 305 4 4 panel/CornerTR 38 305 4 4 panel/Fill 6 316 2 2 -particles/Note 485 105 7 10 +particles/Note 480 51 7 10 screen/BorderB 5 305 2 8 screen/BorderT 2 305 2 10 -screen/CornerBL 365 236 8 8 -screen/CornerBR 374 236 8 8 -screen/CornerTL 467 105 8 10 -screen/CornerTR 476 105 8 10 +screen/CornerBL 382 237 8 8 +screen/CornerBR 391 237 8 8 +screen/CornerTL 462 51 8 10 +screen/CornerTR 471 51 8 10 window/BorderDark 2 316 1 4 window/BorderLight 4 316 1 4 -window/CloseButton 383 236 7 6 +window/CloseButton 400 237 7 6 window/CornerBL 43 305 4 4 window/CornerBR 48 305 4 4 window/CornerTL 53 305 4 4 diff --git a/src/main/scala/ocelot/desktop/audio/AL10W.scala b/src/main/scala/ocelot/desktop/audio/AL10W.scala new file mode 100644 index 0000000..a993a62 --- /dev/null +++ b/src/main/scala/ocelot/desktop/audio/AL10W.scala @@ -0,0 +1,89 @@ +package ocelot.desktop.audio + +import ocelot.desktop.util.Logging +import org.lwjgl.openal.AL10 + +import java.nio.{ByteBuffer, IntBuffer} + +object AL10W extends Logging { + private def run[T](func: String)(f: => T): T = { +// logger.debug(func) + val res = f + val err = AL10.alGetError() + if (err != AL10.AL_NO_ERROR) { + val errName = classOf[AL10].getDeclaredFields.find(field => { + try { + field.getInt() == err + } catch { + case _: Exception => false + } + }).map(_.getName).getOrElse(err.toHexString) + logger.error(s"OpenAL error: ${func}: ${errName}") + } + res + } + + def alIsExtensionPresent(name: String): Boolean = run("alIsExtensionPresent") { + AL10.alIsExtensionPresent(name) + } + + def alGenBuffers(): Int = run("alGenBuffers") { + AL10.alGenBuffers() + } + + def alBufferData(buffer: Int, format: Int, data: ByteBuffer, freq: Int): Unit = run("alBufferData") { + AL10.alBufferData(buffer, format, data, freq) + } + + def alGetBufferi(buffer: Int, pname: Int): Int = run("alGetBufferi") { + AL10.alGetBufferi(buffer, pname) + } + + def alDeleteBuffers(buffer: Int): Unit = run("alDeleteBuffers") { + AL10.alDeleteBuffers(buffer) + } + + def alGenSources(): Int = run("alGenSources") { + AL10.alGenSources() + } + + def alSourceQueueBuffers(source: Int, buffer: Int): Unit = run("alSourceQueueBuffers") { + AL10.alSourceQueueBuffers(source, buffer) + } + + def alSourceUnqueueBuffers(source: Int, buffers: IntBuffer): Unit = run("alSourceUnqueueBuffers") { + AL10.alSourceUnqueueBuffers(source, buffers) + } + + def alGetSourcei(source: Int, pname: Int): Int = run("alGetSourcei") { + AL10.alGetSourcei(source, pname) + } + + def alSourcei(source: Int, pname: Int, value: Int): Unit = run("alSourcei") { + AL10.alSourcei(source, pname, value) + } + + def alSourcef(source: Int, pname: Int, value: Float): Unit = run("alSourcef") { + AL10.alSourcef(source, pname, value) + } + + def alSource3f(source: Int, pname: Int, v1: Float, v2: Float, v3: Float): Unit = run("alSource3f") { + AL10.alSource3f(source, pname, v1, v2, v3) + } + + def alSourcePlay(source: Int): Unit = run("alSourcePlay") { + AL10.alSourcePlay(source) + } + + def alSourcePause(source: Int): Unit = run("alSourceStop") { + AL10.alSourcePause(source) + } + + def alSourceStop(source: Int): Unit = run("alSourceStop") { + AL10.alSourceStop(source) + } + + def alDeleteSources(source: Int): Unit = run("alDeleteSources") { + AL10.alDeleteSources(source) + } +} diff --git a/src/main/scala/ocelot/desktop/audio/Audio.scala b/src/main/scala/ocelot/desktop/audio/Audio.scala index 7099034..63b2242 100644 --- a/src/main/scala/ocelot/desktop/audio/Audio.scala +++ b/src/main/scala/ocelot/desktop/audio/Audio.scala @@ -5,12 +5,13 @@ import ocelot.desktop.util.Logging import org.lwjgl.LWJGLException import org.lwjgl.openal.{AL, AL10, ALC10} +import java.nio.{ByteBuffer, ByteOrder} import scala.collection.mutable object Audio extends Logging { val sampleRate: Int = 44100 - private val sources = new mutable.HashMap[SoundSource, SourceState] + private val sources = new mutable.HashMap[SoundSource, Int] private var _disabled = true /** @@ -19,7 +20,6 @@ object Audio extends Logging { def init(): Unit = { try { AL.create() - AL10.alGetError() logger.info(s"OpenAL device: ${ALC10.alcGetString(AL.getDevice, ALC10.ALC_DEVICE_SPECIFIER)}") _disabled = false } catch { @@ -31,16 +31,49 @@ object Audio extends Logging { def isDisabled: Boolean = _disabled - def numSources: Int = synchronized { sources.size } + def numSources: Int = synchronized { + sources.size + } + + def newStream(soundCategory: SoundCategory.Value, pitch: Float = 1f, + volume: Float = 1f): (SoundStream, SoundSource) = + { + var source: SoundSource = null + + val stream = new SoundStream { + override def enqueue(samples: SoundSamples): Unit = Audio.synchronized { + val sourceId = if (sources.contains(source)) { + sources(source) + } else { + val sourceId = AL10W.alGenSources() + + AL10W.alSourcef(sourceId, AL10.AL_PITCH, source.pitch) + AL10W.alSource3f(sourceId, AL10.AL_POSITION, 0f, 0f, 0f) + AL10W.alSourcef(sourceId, AL10.AL_GAIN, source.volume * SoundCategory.getSettingsValue(source.soundCategory) * Settings.get.volumeMaster) + sources.put(source, sourceId) + + sourceId + } + + cleanupSourceBuffers(sourceId) + + val bufferId = samples.genBuffer() + AL10W.alSourceQueueBuffers(sourceId, bufferId) + if (AL10W.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) != AL10.AL_PLAYING) + AL10W.alSourcePlay(sourceId) + } + } + + source = SoundSource.fromStream(stream, soundCategory, looping = false, pitch, volume) + (stream, source) + } def getSourceStatus(source: SoundSource): SoundSource.Status.Value = synchronized { if (!sources.contains(source)) return SoundSource.Status.Stopped - sources(source).status - - val state = sources(source) - AL10.alGetSourcei(state.sourceId, AL10.AL_SOURCE_STATE) match { + val sourceId = sources(source) + AL10W.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) match { case AL10.AL_PLAYING => SoundSource.Status.Playing case AL10.AL_PAUSED => SoundSource.Status.Paused case _ => SoundSource.Status.Stopped @@ -48,75 +81,63 @@ object Audio extends Logging { } def playSource(source: SoundSource): Unit = synchronized { + if (getSourceStatus(source) == SoundSource.Status.Playing) + return + if (sources.contains(source)) { - AL10.alSourcePlay(sources(source).sourceId) + AL10W.alSourcePlay(sources(source)) return } - val sourceId = AL10.alGenSources() - val (bufferId, isAssociated) = source.kind match { - case SoundSource.KindSoundBuffer(buffer) => (buffer.bufferId, false) - case SoundSource.KindSoundSamples(SoundSamples(buffer, rate, channels)) => - val bufferId = AL10.alGenBuffers() - val format = channels match { - case 2 => AL10.AL_FORMAT_STEREO16 - case 1 => AL10.AL_FORMAT_MONO16 - } - - AL10.alBufferData(bufferId, format, buffer, rate) - val err = AL10.alGetError - if (err != AL10.AL_NO_ERROR) { - logger.error(s"Unable to create OpenAL buffer: $err, $format, $rate") - return - } - - (bufferId, true) + val sourceId = AL10W.alGenSources() + source.kind match { + case SoundSource.KindSoundBuffer(buffer) => + AL10W.alSourcei(sourceId, AL10.AL_BUFFER, buffer.bufferId) + case SoundSource.KindSoundSamples(samples) => + val bufferId = samples.genBuffer() + if (bufferId != -1) AL10W.alSourceQueueBuffers(sourceId, bufferId) + case SoundSource.KindStream(_) => } - AL10.alSourcei(sourceId, AL10.AL_BUFFER, bufferId) - AL10.alSourcef(sourceId, AL10.AL_PITCH, source.pitch) - AL10.alSource3f(sourceId, AL10.AL_POSITION, 0f, 0f, 0f) - AL10.alSourcei(sourceId, AL10.AL_LOOPING, if (source.looping) AL10.AL_TRUE else AL10.AL_FALSE) - AL10.alSourcef(sourceId, AL10.AL_GAIN, source.volume * SoundCategory.getSettingsValue(source.soundCategory) * Settings.get.volumeMaster) - AL10.alSourcePlay(sourceId) + AL10W.alSourcef(sourceId, AL10.AL_PITCH, source.pitch) + AL10W.alSource3f(sourceId, AL10.AL_POSITION, 0f, 0f, 0f) + AL10W.alSourcei(sourceId, AL10.AL_LOOPING, if (source.looping) AL10.AL_TRUE else AL10.AL_FALSE) + AL10W.alSourcef(sourceId, AL10.AL_GAIN, source.volume * SoundCategory.getSettingsValue(source.soundCategory) * Settings.get.volumeMaster) + AL10W.alSourcePlay(sourceId) - val state = SourceState(sourceId, associatedBufferId = if (isAssociated) bufferId else -1, SoundSource.Status.Playing) - sources.put(source, state) + sources.put(source, sourceId) } def pauseSource(source: SoundSource): Unit = synchronized { + if (getSourceStatus(source) == SoundSource.Status.Paused) + return + if (sources.contains(source)) { - AL10.alSourcePause(sources(source).sourceId) + AL10W.alSourcePause(sources(source)) } } def stopSource(source: SoundSource): Unit = synchronized { + if (getSourceStatus(source) == SoundSource.Status.Stopped) + return + if (sources.contains(source)) { - val state = sources(source) - AL10.alSourceStop(state.sourceId) - AL10.alDeleteSources(state.sourceId) - if (state.associatedBufferId != -1) - AL10.alDeleteBuffers(state.associatedBufferId) + AL10W.alSourceStop(sources(source)) } } def update(): Unit = synchronized { if (isDisabled) return - sources.filterInPlace { case (source, state) => - AL10.alSourcef(state.sourceId, AL10.AL_GAIN, source.volume * SoundCategory.getSettingsValue(source.soundCategory) * Settings.get.volumeMaster) - AL10.alGetSourcei(state.sourceId, AL10.AL_SOURCE_STATE) match { - case AL10.AL_PLAYING => - state.status = SoundSource.Status.Playing - true - case AL10.AL_PAUSED => - state.status = SoundSource.Status.Paused - true - case _ => - AL10.alDeleteSources(state.sourceId) - if (state.associatedBufferId != -1) - AL10.alDeleteBuffers(state.associatedBufferId) + sources.filterInPlace { case (source, sourceId) => + cleanupSourceBuffers(sourceId) + + AL10W.alSourcef(sourceId, AL10.AL_GAIN, source.volume * SoundCategory.getSettingsValue(source.soundCategory) * Settings.get.volumeMaster) + AL10W.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) match { + case AL10.AL_STOPPED => + deleteSource(sourceId) false + case _ => true } } } @@ -124,15 +145,32 @@ object Audio extends Logging { def destroy(): Unit = synchronized { if (isDisabled) return - for ((_, state) <- sources) { - AL10.alDeleteSources(state.sourceId) - if (state.associatedBufferId != -1) - AL10.alDeleteBuffers(state.associatedBufferId) + for ((_, sourceId) <- sources) { + deleteSource(sourceId) } sources.clear() _disabled = true } - private case class SourceState(sourceId: Int, associatedBufferId: Int, var status: SoundSource.Status.Value) + private def deleteSource(sourceId: Int): Unit = { + AL10W.alSourceStop(sourceId) + cleanupSourceBuffers(sourceId) + AL10W.alDeleteSources(sourceId) + } + + private def cleanupSourceBuffers(sourceId: Int): Unit = { + val count = AL10W.alGetSourcei(sourceId, AL10.AL_BUFFERS_PROCESSED) + if (count <= 0) return + + val buff = ByteBuffer.allocateDirect(count * 4) + buff.order(ByteOrder.nativeOrder()) + val buf = buff.asIntBuffer() + + AL10W.alSourceUnqueueBuffers(sourceId, buf) + + for (i <- 0 until count) { + AL10W.alDeleteBuffers(buf.get(i)) + } + } } diff --git a/src/main/scala/ocelot/desktop/audio/BeepGenerator.scala b/src/main/scala/ocelot/desktop/audio/BeepGenerator.scala index 42aa308..24b87a2 100644 --- a/src/main/scala/ocelot/desktop/audio/BeepGenerator.scala +++ b/src/main/scala/ocelot/desktop/audio/BeepGenerator.scala @@ -30,6 +30,6 @@ object BeepGenerator { } data.rewind() - SoundSource.fromSamples(SoundSamples(data, Audio.sampleRate, 1), SoundCategory.Beep) + SoundSource.fromSamples(SoundSamples(data, Audio.sampleRate, SoundSamples.Format.Mono16), SoundCategory.Beep) } } diff --git a/src/main/scala/ocelot/desktop/audio/OggDecoder.scala b/src/main/scala/ocelot/desktop/audio/OggDecoder.scala index 5c632a7..aebac9b 100644 --- a/src/main/scala/ocelot/desktop/audio/OggDecoder.scala +++ b/src/main/scala/ocelot/desktop/audio/OggDecoder.scala @@ -36,6 +36,11 @@ object OggDecoder { data.put(dataOut.toByteArray) data.rewind() - SoundSamples(data, rate, channels) + val format = channels match { + case 1 => SoundSamples.Format.Mono16 + case 2 => SoundSamples.Format.Stereo16 + } + + SoundSamples(data, rate, format) } } diff --git a/src/main/scala/ocelot/desktop/audio/SoundBuffer.scala b/src/main/scala/ocelot/desktop/audio/SoundBuffer.scala index 2278da0..926dcab 100644 --- a/src/main/scala/ocelot/desktop/audio/SoundBuffer.scala +++ b/src/main/scala/ocelot/desktop/audio/SoundBuffer.scala @@ -13,9 +13,9 @@ class SoundBuffer(val file: String) extends Resource with Logging { return logger.debug(s"Loading sound buffer from '$file'...") - _bufferId = AL10.alGenBuffers() + _bufferId = AL10W.alGenBuffers() - if (AL10.alIsExtensionPresent("AL_EXT_vorbis")) { + if (AL10W.alIsExtensionPresent("AL_EXT_vorbis")) { initWithExt() } else { initFallback() @@ -29,21 +29,12 @@ class SoundBuffer(val file: String) extends Resource with Logging { return } - AL10.alBufferData(bufferId, AL10.AL_FORMAT_VORBIS_EXT, fileBuffer, -1) + AL10W.alBufferData(bufferId, AL10.AL_FORMAT_VORBIS_EXT, fileBuffer, -1) } private def initFallback(): Unit = { val ogg = OggDecoder.decode(getClass.getResourceAsStream(file)) - - val format = ogg.channels match { - case 2 => AL10.AL_FORMAT_STEREO16 - case 1 => AL10.AL_FORMAT_MONO16 - } - - AL10.alBufferData(bufferId, format, ogg.data, ogg.rate) - if (AL10.alGetError != AL10.AL_NO_ERROR) { - logger.error(s"Unable to create OpenAL buffer from '$file'!") - } + _bufferId = ogg.genBuffer() } def numSamples: Int = { @@ -51,9 +42,9 @@ class SoundBuffer(val file: String) extends Resource with Logging { return 0 } - val sizeBytes = AL10.alGetBufferi(bufferId, AL10.AL_SIZE) - val channels = AL10.alGetBufferi(bufferId, AL10.AL_CHANNELS) - val bits = AL10.alGetBufferi(bufferId, AL10.AL_BITS) + val sizeBytes = AL10W.alGetBufferi(bufferId, AL10.AL_SIZE) + val channels = AL10W.alGetBufferi(bufferId, AL10.AL_CHANNELS) + val bits = AL10W.alGetBufferi(bufferId, AL10.AL_BITS) sizeBytes * 8 / channels / bits } @@ -62,7 +53,7 @@ class SoundBuffer(val file: String) extends Resource with Logging { if (bufferId == -1) { 44100 } else { - AL10.alGetBufferi(bufferId, AL10.AL_FREQUENCY) + AL10W.alGetBufferi(bufferId, AL10.AL_FREQUENCY) } } @@ -70,7 +61,7 @@ class SoundBuffer(val file: String) extends Resource with Logging { def freeResource(): Unit = { if (bufferId != -1) { - AL10.alDeleteBuffers(bufferId) + AL10W.alDeleteBuffers(bufferId) logger.debug(s"Destroyed sound buffer (ID: $bufferId) loaded from $file") } } diff --git a/src/main/scala/ocelot/desktop/audio/SoundSamples.scala b/src/main/scala/ocelot/desktop/audio/SoundSamples.scala index e3879c8..ccf172b 100644 --- a/src/main/scala/ocelot/desktop/audio/SoundSamples.scala +++ b/src/main/scala/ocelot/desktop/audio/SoundSamples.scala @@ -1,5 +1,28 @@ package ocelot.desktop.audio +import ocelot.desktop.util.Logging +import org.lwjgl.openal.AL10 + import java.nio.ByteBuffer -case class SoundSamples(data: ByteBuffer, rate: Int, channels: Int) +case class SoundSamples(data: ByteBuffer, rate: Int, format: SoundSamples.Format.Value) extends Logging { + def genBuffer(): Int = { + val bufferId = AL10W.alGenBuffers() + val formatId = format match { + case SoundSamples.Format.Stereo16 => AL10.AL_FORMAT_STEREO16 + case SoundSamples.Format.Mono16 => AL10.AL_FORMAT_MONO16 + case SoundSamples.Format.Mono8 => AL10.AL_FORMAT_MONO8 + } + + AL10W.alBufferData(bufferId, formatId, data, rate) + bufferId + } + + +} + +object SoundSamples { + object Format extends Enumeration { + val Stereo16, Mono8, Mono16 = Value + } +} \ No newline at end of file diff --git a/src/main/scala/ocelot/desktop/audio/SoundSource.scala b/src/main/scala/ocelot/desktop/audio/SoundSource.scala index 3931ace..d94f95d 100644 --- a/src/main/scala/ocelot/desktop/audio/SoundSource.scala +++ b/src/main/scala/ocelot/desktop/audio/SoundSource.scala @@ -12,8 +12,13 @@ class SoundSource(val kind: SoundSource.Kind, val seconds = kind match { case SoundSource.KindSoundBuffer(buffer) => buffer.numSamples.toFloat / buffer.sampleRate - case SoundSource.KindSoundSamples(SoundSamples(buffer, rate, channels)) => - buffer.limit().toFloat / (rate * channels) + case SoundSource.KindSoundSamples(SoundSamples(buffer, rate, format)) => + val bps = format match { + case SoundSamples.Format.Stereo16 => 2 + case SoundSamples.Format.Mono8 => 1 + case SoundSamples.Format.Mono16 => 2 + } + buffer.limit().toFloat / (rate * bps) } Duration.ofNanos((seconds * 1e9).toLong) @@ -36,15 +41,15 @@ class SoundSource(val kind: SoundSource.Kind, } def play(): Unit = { - if (!isPlaying) Audio.playSource(this) + Audio.playSource(this) } def pause(): Unit = { - if (!isPaused) Audio.pauseSource(this) + Audio.pauseSource(this) } def stop(): Unit = { - if (!isStopped) Audio.stopSource(this) + Audio.stopSource(this) } } @@ -55,6 +60,8 @@ object SoundSource { case class KindSoundSamples(samples: SoundSamples) extends Kind + case class KindStream(stream: SoundStream) extends Kind + def fromBuffer(buffer: SoundBuffer, soundCategory: SoundCategory.Value, looping: Boolean = false, pitch: Float = 1f, volume: Float = 1f): SoundSource = { new SoundSource(SoundSource.KindSoundBuffer(buffer), soundCategory, looping, pitch, volume) @@ -65,6 +72,12 @@ object SoundSource { new SoundSource(SoundSource.KindSoundSamples(samples), soundCategory, looping, pitch, volume) } + def fromStream(stream: SoundStream, soundCategory: SoundCategory.Value, + looping: Boolean = false, pitch: Float = 1f, volume: Float = 1f): SoundSource = + { + new SoundSource(SoundSource.KindStream(stream), soundCategory, looping, pitch, volume) + } + object Status extends Enumeration { val Playing, Paused, Stopped = Value } diff --git a/src/main/scala/ocelot/desktop/audio/SoundStream.scala b/src/main/scala/ocelot/desktop/audio/SoundStream.scala new file mode 100644 index 0000000..fddc9da --- /dev/null +++ b/src/main/scala/ocelot/desktop/audio/SoundStream.scala @@ -0,0 +1,5 @@ +package ocelot.desktop.audio + +trait SoundStream { + def enqueue(samples: SoundSamples): Unit +} diff --git a/src/main/scala/ocelot/desktop/node/nodes/ComputerNode.scala b/src/main/scala/ocelot/desktop/node/nodes/ComputerNode.scala index c0ba85a..57485dd 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/ComputerNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/ComputerNode.scala @@ -13,10 +13,11 @@ import org.lwjgl.input.Keyboard import totoro.ocelot.brain.entity.traits.{Entity, Environment, Floppy, GenericCPU, Inventory} import totoro.ocelot.brain.entity.{CPU, Case, EEPROM, GraphicsCard, HDDManaged, HDDUnmanaged, Memory} import totoro.ocelot.brain.event.FileSystemActivityType.Floppy -import totoro.ocelot.brain.event.{BeepEvent, BeepPatternEvent, FileSystemActivityEvent, MachineCrashEvent} +import totoro.ocelot.brain.event.{BeepEvent, BeepPatternEvent, FileSystemActivityEvent, MachineCrashEvent, SoundCardAudioEvent} import totoro.ocelot.brain.loot.Loot import totoro.ocelot.brain.util.Tier +import java.nio.ByteBuffer import scala.reflect.ClassTag import scala.util.Random @@ -29,6 +30,7 @@ class ComputerNode(val computer: Case) extends Node(computer) with Logging { var floppySlot: Option[FloppySlot] = None private val soundComputerRunning = SoundSource.fromBuffer(SoundBuffers.MachineComputerRunning, SoundCategory.Environment, looping = true) + private var soundCardStream: SoundStream = _ setupSlots() refitSlots() @@ -48,6 +50,17 @@ class ComputerNode(val computer: Case) extends Node(computer) with Logging { val soundHDDAccess = SoundBuffers.MachineHDDAccess.map(buffer => SoundSource.fromBuffer(buffer, SoundCategory.Environment)) val sound = if (event.activityType == Floppy) soundFloppyAccess else soundHDDAccess sound(Random.between(0, sound.length)).play() + + case BrainEvent(event: SoundCardAudioEvent) if !Audio.isDisabled => + val samples = SoundSamples(event.data, 44100, SoundSamples.Format.Mono8) +// SoundSource.fromSamples(samples, SoundCategory.Beep).play() + if (soundCardStream == null) { + val (stream, source) = Audio.newStream(SoundCategory.Beep) + soundCardStream = stream + soundCardStream.enqueue(samples) + } else { + soundCardStream.enqueue(samples) + } } override def shouldReceiveEventsFor(address: String): Boolean = super.shouldReceiveEventsFor(address) || diff --git a/src/main/scala/ocelot/desktop/ui/UiHandler.scala b/src/main/scala/ocelot/desktop/ui/UiHandler.scala index 0dd2ecc..f4960c9 100644 --- a/src/main/scala/ocelot/desktop/ui/UiHandler.scala +++ b/src/main/scala/ocelot/desktop/ui/UiHandler.scala @@ -249,11 +249,10 @@ object UiHandler extends Logging { if (Display.isCloseRequested) OcelotDesktop.exit() - Audio.update() - OcelotDesktop.withTickLockAcquired { updateWindowSizeAndPosition() + Audio.update() KeyEvents.update() MouseEvents.update() diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/CardRegistry.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/CardRegistry.scala index f18d615..544b3d7 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/slot/CardRegistry.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/slot/CardRegistry.scala @@ -1,6 +1,7 @@ package ocelot.desktop.ui.widget.slot import ocelot.desktop.graphics.IconDef +import totoro.ocelot.brain.entity.sound_card.SoundCard import totoro.ocelot.brain.entity.traits.{Entity, Tiered} import totoro.ocelot.brain.entity.{DataCard, GraphicsCard, InternetCard, LinkedCard, NetworkCard, Redstone, WirelessNetworkCard} @@ -15,7 +16,7 @@ object CardRegistry { val iconByClassAndTier: mutable.HashMap[(String, Int), IconDef] = mutable.HashMap() val entries: mutable.ArrayBuffer[Entry] = mutable.ArrayBuffer() - def addEntry[T <: Entity: ClassTag](name: String, tier: Int, icon: IconDef, factory: () => T): Unit = { + def addEntry[T <: Entity : ClassTag](name: String, tier: Int, icon: IconDef, factory: () => T): Unit = { val entry = Entry(name, tier, icon, factory) entries += entry val clazz = classTag[T].runtimeClass.getName @@ -73,5 +74,8 @@ object CardRegistry { () => new DataCard.Tier2) addEntry("Data Card", 2, new IconDef("items/DataCard2", animation = DataCardAnimation), () => new DataCard.Tier3) + + addEntry("Sound Card", 1, new IconDef("items/SoundCard", animation = DataCardAnimation), + () => new SoundCard) }