diff --git a/src/main/scala/li/cil/oc/util/Audio.scala b/src/main/scala/li/cil/oc/util/Audio.scala index fbd7dc8d2..468dee7ec 100644 --- a/src/main/scala/li/cil/oc/util/Audio.scala +++ b/src/main/scala/li/cil/oc/util/Audio.scala @@ -11,7 +11,7 @@ import net.minecraft.client.audio.SoundCategory import org.lwjgl.BufferUtils import org.lwjgl.openal.AL import org.lwjgl.openal.AL10 -import org.lwjgl.openal.Util +import org.lwjgl.openal.OpenALException import scala.collection.mutable @@ -29,52 +29,68 @@ object Audio { private def volume = Minecraft.getMinecraft.gameSettings.getSoundLevel(SoundCategory.BLOCKS) - private var errored = false + private var disableAudio = false def play(x: Float, y: Float, z: Float, frequencyInHz: Int, durationInMilliseconds: Int): Unit = { play(x, y, z, ".", frequencyInHz, durationInMilliseconds) } - def play(x: Float, y: Float, z: Float, pattern: String, frequencyInHz: Int = 1000, durationInMilliseconds: Int = 200) { - val distanceBasedGain = math.max(0, 1 - Minecraft.getMinecraft.thePlayer.getDistance(x, y, z) / 12).toFloat - val gain = distanceBasedGain * volume - if (gain > 0 && AL.isCreated) { - val sampleCounts = pattern.toCharArray. - map(ch => if (ch == '.') durationInMilliseconds else 2 * durationInMilliseconds). - map(_ * sampleRate / 1000) - // 50ms pause between pattern parts. - val pauseSampleCount = 50 * sampleRate / 1000 - val data = BufferUtils.createByteBuffer(sampleCounts.sum + (sampleCounts.length - 1) * pauseSampleCount) - val step = frequencyInHz / sampleRate.toFloat - var offset = 0f - for (sampleCount <- sampleCounts) { - for (sample <- 0 until sampleCount) { - val angle = 2 * math.Pi * offset - // We could sort of fake the square wave with a little less - // computational effort, but until somebody complains let's - // go with the fourier series! We leave out the 4 / Pi because - // it's just an approximation and we avoid clipping like this. - val value = (0 to 6).map(k => math.sin((1 + k * 2) * angle) / (1 + k * 2)).sum * Byte.MaxValue - // val tmp = math.sin(angle) * Byte.MaxValue - // val value = math.signum(tmp) * 0.99 + tmp * 0.01 - offset += step - if (offset > 1) offset -= 1 - data.put(value.toByte) - } - if (data.hasRemaining) { - for (sample <- 0 until pauseSampleCount) { - data.put(127: Byte) + def play(x: Float, y: Float, z: Float, pattern: String, frequencyInHz: Int = 1000, durationInMilliseconds: Int = 200): Unit = { + if (!disableAudio) { + val distanceBasedGain = math.max(0, 1 - Minecraft.getMinecraft.thePlayer.getDistance(x, y, z) / 12).toFloat + val gain = distanceBasedGain * volume + if (gain > 0 && AL.isCreated) { + val sampleCounts = pattern.toCharArray. + map(ch => if (ch == '.') durationInMilliseconds else 2 * durationInMilliseconds). + map(_ * sampleRate / 1000) + // 50ms pause between pattern parts. + val pauseSampleCount = 50 * sampleRate / 1000 + val data = BufferUtils.createByteBuffer(sampleCounts.sum + (sampleCounts.length - 1) * pauseSampleCount) + val step = frequencyInHz / sampleRate.toFloat + var offset = 0f + for (sampleCount <- sampleCounts) { + for (sample <- 0 until sampleCount) { + val angle = 2 * math.Pi * offset + // We could sort of fake the square wave with a little less + // computational effort, but until somebody complains let's + // go with the fourier series! We leave out the 4 / Pi because + // it's just an approximation and we avoid clipping like this. + val value = (0 to 6).map(k => math.sin((1 + k * 2) * angle) / (1 + k * 2)).sum * Byte.MaxValue + // val tmp = math.sin(angle) * Byte.MaxValue + // val value = math.signum(tmp) * 0.99 + tmp * 0.01 + offset += step + if (offset > 1) offset -= 1 + data.put(value.toByte) + } + if (data.hasRemaining) { + for (sample <- 0 until pauseSampleCount) { + data.put(127: Byte) + } } } - } - data.rewind() + data.rewind() - sources.synchronized(sources += new Source(x, y, z, data, gain)) + // Watch out for sound cards running out of memory... this apparently + // really does happen. I'm assuming this is due to too many sounds being + // kept loaded, since from what I can see OC's releasing its audio + // memory as it should. + try sources.synchronized(sources += new Source(x, y, z, data, gain)) catch { + case e: LessUselessOpenALException => + if (e.errorCode == AL10.AL_OUT_OF_MEMORY) { + // Well... let's just stop here. + OpenComputers.log.info("Couldn't play computer speaker sound because your sound card ran out of memory. Either your sound card is just really low-end, or there are just too many sounds in use already by other mods. Disabling computer speakers to avoid spamming your log file now.") + disableAudio = true + } + else { + OpenComputers.log.warn("Error playing computer speaker sound.", e) + } + } + } } } def update() { - if (!errored) { + if (!disableAudio) { sources.synchronized(sources --= sources.filter(_.checkFinished)) // Clear error stack. @@ -82,7 +98,7 @@ object Audio { try AL10.alGetError() catch { case _: UnsatisfiedLinkError => OpenComputers.log.warn("Negotiations with OpenAL broke down, disabling sounds.") - errored = true + disableAudio = true } } } @@ -94,25 +110,25 @@ object Audio { val (source, buffer) = { val buffer = AL10.alGenBuffers() - Util.checkALError() + checkALError() try { AL10.alBufferData(buffer, AL10.AL_FORMAT_MONO8, data, sampleRate) - Util.checkALError() + checkALError() val source = AL10.alGenSources() - Util.checkALError() + checkALError() try { AL10.alSourceQueueBuffers(source, buffer) - Util.checkALError() + checkALError() AL10.alSource3f(source, AL10.AL_POSITION, x, y, z) AL10.alSourcef(source, AL10.AL_GAIN, gain * 0.3f) - Util.checkALError() + checkALError() AL10.alSourcePlay(source) - Util.checkALError() + checkALError() (source, buffer) } @@ -136,6 +152,17 @@ object Audio { } } + // Having the error code in an accessible way is really cool, you know. + class LessUselessOpenALException(val errorCode: Int) extends OpenALException(errorCode) + + // Custom implementation of Util.checkALError() that uses our custom exception. + def checkALError(): Unit = { + val errorCode = AL10.alGetError() + if (errorCode != AL10.AL_NO_ERROR) { + throw new LessUselessOpenALException(errorCode) + } + } + FMLCommonHandler.instance.bus.register(this) @SubscribeEvent