2023-10-18 13:32:08 +03:00

252 lines
6.9 KiB
Scala

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