mirror of
https://gitlab.com/cc-ru/ocelot/ocelot-desktop.git
synced 2025-12-20 02:59:19 +01:00
252 lines
6.9 KiB
Scala
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))
|
|
}
|
|
}
|
|
}
|