blob: 1974b1e30407d1123eaa269b483e94e876c61242 [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 com.google.common.base.Charsets
import cpw.mods.fml.client.FMLClientHandler
import cpw.mods.fml.common.eventhandler.SubscribeEvent
import cpw.mods.fml.common.gameevent.TickEvent.ClientTickEvent
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
import scala.io.Source
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
}
private var hasPreloaded = Settings.get.soundVolume <= 0
@SubscribeEvent
def onTick(e: ClientTickEvent) {
if (!hasPreloaded && soundSystem != null) {
hasPreloaded = true
new Thread(new Runnable() {
override def run(): Unit = {
val preloadConfigLocation = new ResourceLocation(Settings.resourceDomain, "sounds/preload.cfg")
val preloadConfigResource = Minecraft.getMinecraft.getResourceManager.getResource(preloadConfigLocation)
for (location <- Source.fromInputStream(preloadConfigResource.getInputStream)(Charsets.UTF_8).getLines()) {
val url = getClass.getClassLoader.getResource(location)
if (url != null) try {
val sourceName = "preload_" + location
soundSystem.newSource(false, sourceName, url, location, true, 0, 0, 0, SoundSystemConfig.ATTENUATION_NONE, 16)
soundSystem.activate(sourceName)
soundSystem.removeSource(sourceName)
} catch {
case _: Throwable => // Meh.
}
else OpenComputers.log.warn(s"Couldn't preload sound $location!")
}
}
})
}
}
@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(): Unit
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
}
}
}