blob: 40882f3c6b2335e67219cf18c0f612e334e4550d [file] [log] [blame] [raw]
package li.cil.oc.client
import java.net.{MalformedURLException, URL, URLConnection, URLStreamHandler}
import java.util.{Timer, TimerTask, 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, Settings}
import net.minecraft.client.Minecraft
import net.minecraft.client.audio.{SoundCategory, SoundManager, 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, 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
}
}
}