blob: eb1de250b1b893de4fdc23b380fda387d5cb9c86 [file] [log] [blame] [raw]
package li.cil.oc.client
import java.net.MalformedURLException
import java.net.URL
import java.net.URLConnection
import java.net.URLStreamHandler
import java.util.Timer
import java.util.TimerTask
import java.util.UUID
import cpw.mods.fml.client.FMLClientHandler
import cpw.mods.fml.common.eventhandler.SubscribeEvent
import cpw.mods.fml.relauncher.ReflectionHelper
import li.cil.oc.OpenComputers
import li.cil.oc.Settings
import net.minecraft.client.Minecraft
import net.minecraft.client.audio.SoundCategory
import net.minecraft.client.audio.SoundManager
import net.minecraft.client.audio.SoundPoolEntry
import net.minecraft.tileentity.TileEntity
import net.minecraft.util.ResourceLocation
import net.minecraftforge.client.event.sound.SoundLoadEvent
import net.minecraftforge.event.world.WorldEvent
import paulscode.sound.SoundSystem
import paulscode.sound.SoundSystemConfig
import scala.collection.mutable
object Sound {
private val sources = mutable.Map.empty[TileEntity, PseudoLoopingStream]
private val commandQueue = mutable.PriorityQueue.empty[Command]
private var lastVolume = FMLClientHandler.instance.getClient.gameSettings.getSoundLevel(SoundCategory.BLOCKS)
private val updateTimer = new Timer("OpenComputers-SoundUpdater", true)
if (Settings.get.soundVolume > 0) {
updateTimer.scheduleAtFixedRate(new TimerTask {
override def run() {
updateVolume()
processQueue()
}
}, 500, 50)
}
// Set in init event.
var manager: SoundManager = _
def soundSystem: SoundSystem = ReflectionHelper.getPrivateValue(classOf[SoundManager], manager, "sndSystem", "field_148620_e", "e")
private def updateVolume() {
val volume = FMLClientHandler.instance.getClient.gameSettings.getSoundLevel(SoundCategory.BLOCKS)
if (volume != lastVolume) {
lastVolume = volume
sources.synchronized {
for (sound <- sources.values) {
sound.updateVolume()
}
}
}
}
private def processQueue() {
if (commandQueue.nonEmpty) {
commandQueue.synchronized {
while (commandQueue.nonEmpty && commandQueue.head.when < System.currentTimeMillis()) {
try commandQueue.dequeue()() catch {
case t: Throwable => OpenComputers.log.warn("Error processing sound command.", t)
}
}
}
}
}
def startLoop(tileEntity: TileEntity, name: String, volume: Float = 1f, delay: Long = 0) {
if (Settings.get.soundVolume > 0) {
commandQueue.synchronized {
commandQueue += new StartCommand(System.currentTimeMillis() + delay, tileEntity, name, volume)
}
}
}
def stopLoop(tileEntity: TileEntity) {
if (Settings.get.soundVolume > 0) {
commandQueue.synchronized {
commandQueue += new StopCommand(tileEntity)
}
}
}
def updatePosition(tileEntity: TileEntity) {
if (Settings.get.soundVolume > 0) {
commandQueue.synchronized {
commandQueue += new UpdatePositionCommand(tileEntity)
}
}
}
@SubscribeEvent
def onSoundLoad(event: SoundLoadEvent) {
manager = event.manager
}
@SubscribeEvent
def onWorldUnload(event: WorldEvent.Unload) {
commandQueue.synchronized(commandQueue.clear())
sources.synchronized(try sources.foreach(_._2.stop()) catch {
case _: Throwable => // Ignore.
})
}
private abstract class Command(val when: Long, val tileEntity: TileEntity) extends Ordered[Command] {
def apply()
override def compare(that: Command) = (that.when - when).toInt
}
private class StartCommand(when: Long, tileEntity: TileEntity, val name: String, val volume: Float) extends Command(when, tileEntity) {
override def apply() {
sources.synchronized {
sources.getOrElseUpdate(tileEntity, new PseudoLoopingStream(tileEntity, volume)).play(name)
}
}
}
private class StopCommand(tileEntity: TileEntity) extends Command(0, tileEntity) {
override def apply() {
sources.synchronized {
sources.remove(tileEntity) match {
case Some(sound) => sound.stop()
case _ =>
}
}
commandQueue.synchronized {
// Remove all other commands for this tile entity from the queue. This
// is inefficient, but we generally don't expect the command queue to
// be very long, so this should be OK.
commandQueue ++= commandQueue.dequeueAll.filter(_.tileEntity != tileEntity)
}
}
}
private class UpdatePositionCommand(tileEntity: TileEntity) extends Command(0, tileEntity) {
override def apply() {
sources.synchronized {
sources.get(tileEntity) match {
case Some(sound) => sound.updatePosition()
case _ =>
}
}
}
}
private class PseudoLoopingStream(val tileEntity: TileEntity, val volume: Float, val source: String = UUID.randomUUID.toString) {
var initialized = false
def updateVolume() {
soundSystem.setVolume(source, lastVolume * volume * Settings.get.soundVolume)
}
def updatePosition() {
soundSystem.setPosition(source, tileEntity.xCoord, tileEntity.yCoord, tileEntity.zCoord)
}
def play(name: String) {
val resourceName = s"${Settings.resourceDomain}:$name"
val sound = Minecraft.getMinecraft.getSoundHandler.getSound(new ResourceLocation(resourceName))
val resource = (sound.func_148720_g: SoundPoolEntry).getSoundPoolEntryLocation
if (!initialized) {
initialized = true
soundSystem.newSource(false, source, toUrl(resource), resource.toString, true, tileEntity.xCoord, tileEntity.yCoord, tileEntity.zCoord, SoundSystemConfig.ATTENUATION_LINEAR, 16)
updateVolume()
soundSystem.activate(source)
}
soundSystem.play(source)
}
def stop() {
if (soundSystem != null) try {
soundSystem.stop(source)
soundSystem.removeSource(source)
}
catch {
case _: Throwable =>
}
}
}
// This is copied from SoundManager.getURLForSoundResource, which is private.
private def toUrl(resource: ResourceLocation): URL = {
val name = s"mcsounddomain:${resource.getResourceDomain}:${resource.getResourcePath}"
try {
new URL(null, name, new URLStreamHandler {
protected def openConnection(url: URL): URLConnection = new URLConnection(url) {
def connect() {
}
override def getInputStream = Minecraft.getMinecraft.getResourceManager.getResource(resource).getInputStream
}
})
}
catch {
case _: MalformedURLException => null
}
}
}