From 4a711ebe7d0aa34243e8169bbe437b9d1235dcf4 Mon Sep 17 00:00:00 2001 From: LeshaInc Date: Tue, 8 Jan 2019 14:18:01 +0200 Subject: [PATCH] Implement beep --- build.sbt | 3 + .../scala/ocelot/desktop/OcelotDesktop.scala | 8 +-- .../scala/ocelot/desktop/ui/UiHandler.scala | 25 ++++++- .../scala/ocelot/desktop/util/Audio.scala | 69 +++++++++++++++++++ 4 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 src/main/scala/ocelot/desktop/util/Audio.scala diff --git a/build.sbt b/build.sbt index e569847..29070da 100644 --- a/build.sbt +++ b/build.sbt @@ -23,6 +23,9 @@ libraryDependencies += "org.lwjgl" % "lwjgl-glfw" % lwjglVersion classifier "nat libraryDependencies += "org.lwjgl" % "lwjgl-opengl" % lwjglVersion libraryDependencies += "org.lwjgl" % "lwjgl-opengl" % lwjglVersion classifier "natives-linux" libraryDependencies += "org.lwjgl" % "lwjgl-opengl" % lwjglVersion classifier "natives-windows" +libraryDependencies += "org.lwjgl" % "lwjgl-openal" % lwjglVersion +libraryDependencies += "org.lwjgl" % "lwjgl-openal" % lwjglVersion classifier "natives-linux" +libraryDependencies += "org.lwjgl" % "lwjgl-openal" % lwjglVersion classifier "natives-windows" assemblyMergeStrategy in assembly := { case PathList("META-INF", "MANIFEST.MF") => MergeStrategy.discard diff --git a/src/main/scala/ocelot/desktop/OcelotDesktop.scala b/src/main/scala/ocelot/desktop/OcelotDesktop.scala index b9985df..b6b7d5a 100644 --- a/src/main/scala/ocelot/desktop/OcelotDesktop.scala +++ b/src/main/scala/ocelot/desktop/OcelotDesktop.scala @@ -4,7 +4,7 @@ import java.io.{PrintWriter, StringWriter} import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.widget.ScreenWidget -import ocelot.desktop.util.ResourceManager +import ocelot.desktop.util.{Audio, ResourceManager} import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.scala.Logging import org.lwjgl.Version @@ -97,15 +97,15 @@ object OcelotDesktop extends Logging { private def setupEventHandlers(): Unit = { EventBus.listenTo(classOf[BeepEvent], { case event: BeepEvent => - println(s"[EVENT] Beep (address = ${event.address}, frequency = ${event.frequency}, duration = ${event.duration})") + Audio.beep(event.frequency, event.duration) }) EventBus.listenTo(classOf[BeepPatternEvent], { case event: BeepPatternEvent => - println(s"[EVENT] Beep (address = ${event.address}, pattern = ${event.pattern})") + logger.info(s"[EVENT] Beep (address = ${event.address}, pattern = ${event.pattern})") }) EventBus.listenTo(classOf[MachineCrashEvent], { case event: MachineCrashEvent => - println(s"[EVENT] Machine crash! (address = ${event.address}, ${event.message})") + logger.info(s"[EVENT] Machine crash! (address = ${event.address}, ${event.message})") }) EventBus.listenTo(classOf[TextBufferSetEvent], { case event: TextBufferSetEvent => diff --git a/src/main/scala/ocelot/desktop/ui/UiHandler.scala b/src/main/scala/ocelot/desktop/ui/UiHandler.scala index c6169db..4e9d944 100644 --- a/src/main/scala/ocelot/desktop/ui/UiHandler.scala +++ b/src/main/scala/ocelot/desktop/ui/UiHandler.scala @@ -1,11 +1,14 @@ package ocelot.desktop.ui +import java.nio.{ByteBuffer, IntBuffer} + import ocelot.desktop.geometry.{Size2D, Vector2D} import ocelot.desktop.graphics.Graphics import ocelot.desktop.ui.widget.RootWidget -import ocelot.desktop.util.{FPSCalculator, FontLoader} +import ocelot.desktop.util.{Audio, FPSCalculator, FontLoader} import org.apache.logging.log4j.scala.Logging import org.lwjgl.glfw.{GLFW, GLFWErrorCallback} +import org.lwjgl.openal._ import org.lwjgl.opengl.{GL, GL11} import org.lwjgl.system.MemoryUtil.NULL @@ -18,6 +21,9 @@ class UiHandler(val root: RootWidget) extends Logging { private var window: Long = _ private var fullRedraw = true + private var soundDevice: Long = _ + private var soundContext: Long = _ + private val fpsCalculator = new FPSCalculator def fps: Float = fpsCalculator.fps @@ -66,12 +72,25 @@ class UiHandler(val root: RootWidget) extends Logging { FontLoader.init() graphics = new Graphics + + soundDevice = ALC10.alcOpenDevice(null.asInstanceOf[ByteBuffer]) + if (soundDevice == 0) throw new IllegalStateException("Unable to create OpenAL device") + + logger.info(s"OpenAL device: ${ALC10.alcGetString(soundDevice, ALC10.ALC_DEVICE_SPECIFIER)}") + + soundContext = ALC10.alcCreateContext(soundDevice, null.asInstanceOf[IntBuffer]) + if (!ALC10.alcMakeContextCurrent(soundContext)) throw new IllegalStateException("Failed to make OpenAL context current") + + val alcCapabilities = ALC.createCapabilities(soundDevice) + AL.createCapabilities(alcCapabilities) } def start(): Unit = { while (!GLFW.glfwWindowShouldClose(window)) { graphics.setViewport(windowSize.width.asInstanceOf[Int], windowSize.height.asInstanceOf[Int]) + Audio.update() + KeyHandler.reset() MouseHandler.reset() ScrollHandler.reset() @@ -93,6 +112,10 @@ class UiHandler(val root: RootWidget) extends Logging { def terminate(): Unit = { GLFW.glfwTerminate() GLFW.glfwSetErrorCallback(null).free() + + ALC10.alcMakeContextCurrent(NULL) + ALC10.alcDestroyContext(soundContext) + ALC10.alcCloseDevice(soundDevice) } private def update(): Unit = { diff --git a/src/main/scala/ocelot/desktop/util/Audio.scala b/src/main/scala/ocelot/desktop/util/Audio.scala new file mode 100644 index 0000000..5589f4a --- /dev/null +++ b/src/main/scala/ocelot/desktop/util/Audio.scala @@ -0,0 +1,69 @@ +package ocelot.desktop.util + +import java.nio.ByteBuffer + +import org.apache.logging.log4j.scala.Logging +import org.lwjgl.openal.AL10 + +import scala.collection.mutable +import scala.collection.mutable.ArrayBuffer + +object Audio extends Logging { + private val sampleRate: Int = 44100 + private val amplitude: Int = 32 + + private val sources = new mutable.HashMap[Int, Int]() + private val scheduled = new ArrayBuffer[(Short, Short)]() + + def beep(frequency: Short, duration: Short): Unit = { + scheduled += ((frequency, duration)) + } + + private def _beep(frequency: Short, duration: Short): Unit = { + val source = AL10.alGenSources() + AL10.alSourcef(source, AL10.AL_PITCH, 1) + AL10.alSourcef(source, AL10.AL_GAIN, 0.3f) + AL10.alSource3f(source, AL10.AL_POSITION, 0, 0, 0) + AL10.alSourcei(source, AL10.AL_LOOPING, AL10.AL_FALSE) + + val samples = duration * sampleRate / 1000 + val data = ByteBuffer.allocateDirect(samples) + val step = frequency / sampleRate.toFloat + var offset = 1f + + for (_ <- 0 until samples) { + val angle = 2 * math.Pi * offset + val value = (math.signum(math.sin(angle)) * amplitude).toByte ^ 0x80 + + offset += step + if (offset > 1) offset -= 1 + + data.put(value.toByte) + } + + data.rewind() + + val buffer = AL10.alGenBuffers() + AL10.alBufferData(buffer, AL10.AL_FORMAT_MONO8, data, sampleRate) + + AL10.alSourceQueueBuffers(source, buffer) + AL10.alSourcePlay(source) + + sources += (source -> buffer) + } + + def update(): Unit = { + for ((frequency, duration) <- scheduled) + _beep(frequency, duration) + + scheduled.clear() + + sources.retain((source, buffer) => { + AL10.alGetSourcei(source, AL10.AL_SOURCE_STATE) != AL10.AL_PLAYING && { + AL10.alDeleteSources(source) + AL10.alDeleteBuffers(buffer) + true + } + }) + } +}