package ocelot.desktop.audio import ocelot.desktop.Settings import ocelot.desktop.util.{Logging, Transaction} import org.lwjgl.LWJGLException import org.lwjgl.openal.{AL, AL10, ALC10} import java.nio.{ByteBuffer, ByteOrder} import scala.collection.mutable import scala.util.control.Exception.catching object Audio extends Logging { val sampleRate: Int = 44100 private val sources = new mutable.HashMap[SoundSource, Int] private var _disabled = true /** * Should be called _before_ initializing any sound-related resources */ def init(): Unit = { try { AL.create() logger.info(s"OpenAL device: ${ALC10.alcGetString(AL.getDevice, ALC10.ALC_DEVICE_SPECIFIER)}") _disabled = false } catch { case e: LWJGLException => logger.error("Unable to initialize OpenAL. Disabling sound") e.printStackTrace() } } def isDisabled: Boolean = _disabled 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 { OpenAlException.ignoring { Transaction.runAbortable { tx => if (!Audio.isDisabled) { val sourceId = if (sources.contains(source)) { sources(source) } else { Transaction.run { tx => val sourceId = AL10W.alGenSources() tx.onFailure { AL10W.alDeleteSources(sourceId) } AL10W.alSourcef(sourceId, AL10.AL_PITCH, source.pitch) setPosition(sourceId, source) setGain(sourceId, source) sources.put(source, sourceId) sourceId } } cleanupSourceBuffers(sourceId) val bufferId = samples.genBuffer().getOrElse { tx.abort() } tx.onFailure { AL10W.alDeleteBuffers(bufferId) } 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 val sourceId = sources(source) catching(classOf[OpenAlException]) opt AL10W.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) match { case Some(AL10.AL_PLAYING) => SoundSource.Status.Playing case Some(AL10.AL_PAUSED) => SoundSource.Status.Paused case _ => SoundSource.Status.Stopped } } def playSource(source: SoundSource): Unit = synchronized { OpenAlException.ignoring { if (Audio.isDisabled) { return } if (getSourceStatus(source) == SoundSource.Status.Playing) { return } if (sources.contains(source)) { AL10W.alSourcePlay(sources(source)) return } Transaction.runAbortable { tx => val sourceId = AL10W.alGenSources() tx.onFailure { AL10W.alDeleteSources(sourceId) } source.kind match { case SoundSource.KindSoundBuffer(buffer) => buffer.bufferId match { case Some(bufferId) => AL10W.alSourcei(sourceId, AL10.AL_BUFFER, bufferId) case None => logger.error(s"Called play on a SoundBuffer $buffer with bufferId = None") tx.abort() } case SoundSource.KindSoundSamples(samples) => Transaction.run { innerTx => val bufferId = samples.genBuffer().getOrElse { tx.abort() } innerTx.onFailure { AL10W.alDeleteBuffers(bufferId) } AL10W.alSourceQueueBuffers(sourceId, bufferId) } case SoundSource.KindStream(_) => } AL10W.alSourcef(sourceId, AL10.AL_PITCH, source.pitch) setPosition(sourceId, source) AL10W.alSourcei(sourceId, AL10.AL_LOOPING, if (source.looping) AL10.AL_TRUE else AL10.AL_FALSE) setGain(sourceId, source) AL10W.alSourcePlay(sourceId) sources.put(source, sourceId) } } } def pauseSource(source: SoundSource): Unit = synchronized { OpenAlException.ignoring { if (Audio.isDisabled) return if (getSourceStatus(source) == SoundSource.Status.Paused) return if (sources.contains(source)) { AL10W.alSourcePause(sources(source)) } } } def stopSource(source: SoundSource): Unit = synchronized { OpenAlException.ignoring { if (Audio.isDisabled) { return } if (getSourceStatus(source) == SoundSource.Status.Stopped) return if (sources.contains(source)) { AL10W.alSourceStop(sources(source)) } } } private def setPosition(sourceId: Int, source: SoundSource): Unit = { AL10W.alSource3f(sourceId, AL10.AL_POSITION, source.position.x, source.position.y, source.position.z) } private def setGain(sourceId: Int, source: SoundSource): Unit = { AL10W.alSourcef( sourceId, AL10.AL_GAIN, source.volume * SoundCategory.getSettingsValue(source.soundCategory) * Settings.get.volumeMaster ) } def update(): Unit = synchronized { if (isDisabled) return sources.filterInPlace { case (source, sourceId) => OpenAlException.defaulting(false) { cleanupSourceBuffers(sourceId) setPosition(sourceId, source) setGain(sourceId, source) AL10W.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) match { case AL10.AL_STOPPED => deleteSource(sourceId) false case _ => true } } } } def destroy(): Unit = synchronized { if (isDisabled) return for ((_, sourceId) <- sources) { OpenAlException.ignoring { deleteSource(sourceId) } } sources.clear() AL.destroy() _disabled = true } @throws[OpenAlException] private def deleteSource(sourceId: Int): Unit = { AL10W.alSourceStop(sourceId) cleanupSourceBuffers(sourceId) AL10W.alDeleteSources(sourceId) } @throws[OpenAlException] 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)) } } }